mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-04 07:20:23 +00:00
refactor: tighten plugin sdk entry surface
This commit is contained in:
@@ -1,17 +1,12 @@
|
||||
import { formatNormalizedAllowFromEntries } from "openclaw/plugin-sdk/allow-from";
|
||||
import {
|
||||
createScopedChannelConfigAdapter,
|
||||
createScopedDmSecurityResolver,
|
||||
} from "openclaw/plugin-sdk/channel-config-helpers";
|
||||
import { createTextPairingAdapter } from "openclaw/plugin-sdk/channel-pairing";
|
||||
import { createScopedChannelConfigAdapter } from "openclaw/plugin-sdk/channel-config-helpers";
|
||||
import {
|
||||
composeWarningCollectors,
|
||||
createAllowlistProviderGroupPolicyWarningCollector,
|
||||
createConditionalWarningCollector,
|
||||
createAllowlistProviderOpenWarningCollector,
|
||||
} from "openclaw/plugin-sdk/channel-policy";
|
||||
import { createAttachedChannelResultAdapter } from "openclaw/plugin-sdk/channel-send-result";
|
||||
import { createTopLevelChannelReplyToModeResolver } from "openclaw/plugin-sdk/conversation-runtime";
|
||||
import { createChatChannelPlugin } from "openclaw/plugin-sdk/core";
|
||||
import {
|
||||
createChannelDirectoryAdapter,
|
||||
listResolvedDirectoryGroupEntriesFromMapKeys,
|
||||
@@ -30,7 +25,6 @@ import {
|
||||
resolveChannelMediaMaxBytes,
|
||||
runPassiveAccountLifecycle,
|
||||
type ChannelMessageActionAdapter,
|
||||
type ChannelPlugin,
|
||||
type ChannelStatusIssue,
|
||||
type OpenClawConfig,
|
||||
} from "../runtime-api.js";
|
||||
@@ -92,14 +86,6 @@ const googleChatConfigAdapter = createScopedChannelConfigAdapter<ResolvedGoogleC
|
||||
resolveDefaultTo: (account: ResolvedGoogleChatAccount) => account.config.defaultTo,
|
||||
});
|
||||
|
||||
const resolveGoogleChatDmPolicy = createScopedDmSecurityResolver<ResolvedGoogleChatAccount>({
|
||||
channelKey: "googlechat",
|
||||
resolvePolicy: (account) => account.config.dm?.policy,
|
||||
resolveAllowFrom: (account) => account.config.dm?.allowFrom,
|
||||
allowFromPathSuffix: "dm.",
|
||||
normalizeEntry: (raw) => formatAllowFromEntry(raw),
|
||||
});
|
||||
|
||||
const googlechatActions: ChannelMessageActionAdapter = {
|
||||
describeMessageTool: (ctx) => googlechatMessageActions.describeMessageTool?.(ctx) ?? null,
|
||||
extractToolSend: (ctx) => googlechatMessageActions.extractToolSend?.(ctx) ?? null,
|
||||
@@ -135,138 +121,258 @@ const collectGoogleChatSecurityWarnings = composeWarningCollectors<{
|
||||
),
|
||||
);
|
||||
|
||||
export const googlechatPlugin: ChannelPlugin<ResolvedGoogleChatAccount> = {
|
||||
id: "googlechat",
|
||||
meta: { ...meta },
|
||||
setup: googlechatSetupAdapter,
|
||||
setupWizard: googlechatSetupWizard,
|
||||
pairing: createTextPairingAdapter({
|
||||
idLabel: "googlechatUserId",
|
||||
message: PAIRING_APPROVED_MESSAGE,
|
||||
normalizeAllowEntry: (entry) => formatAllowFromEntry(entry),
|
||||
notify: async ({ cfg, id, message }) => {
|
||||
const account = resolveGoogleChatAccount({ cfg: cfg });
|
||||
if (account.credentialSource === "none") {
|
||||
return;
|
||||
}
|
||||
const user = normalizeGoogleChatTarget(id) ?? id;
|
||||
const target = isGoogleChatUserTarget(user) ? user : `users/${user}`;
|
||||
const space = await resolveGoogleChatOutboundSpace({ account, target });
|
||||
const { sendGoogleChatMessage } = await loadGoogleChatChannelRuntime();
|
||||
await sendGoogleChatMessage({
|
||||
account,
|
||||
space,
|
||||
text: message,
|
||||
});
|
||||
export const googlechatPlugin = createChatChannelPlugin({
|
||||
base: {
|
||||
id: "googlechat",
|
||||
meta: { ...meta },
|
||||
setup: googlechatSetupAdapter,
|
||||
setupWizard: googlechatSetupWizard,
|
||||
capabilities: {
|
||||
chatTypes: ["direct", "group", "thread"],
|
||||
reactions: true,
|
||||
threads: true,
|
||||
media: true,
|
||||
nativeCommands: false,
|
||||
blockStreaming: true,
|
||||
},
|
||||
}),
|
||||
capabilities: {
|
||||
chatTypes: ["direct", "group", "thread"],
|
||||
reactions: true,
|
||||
threads: true,
|
||||
media: true,
|
||||
nativeCommands: false,
|
||||
blockStreaming: true,
|
||||
},
|
||||
streaming: {
|
||||
blockStreamingCoalesceDefaults: { minChars: 1500, idleMs: 1000 },
|
||||
},
|
||||
reload: { configPrefixes: ["channels.googlechat"] },
|
||||
configSchema: buildChannelConfigSchema(GoogleChatConfigSchema),
|
||||
config: {
|
||||
...googleChatConfigAdapter,
|
||||
isConfigured: (account) => account.credentialSource !== "none",
|
||||
describeAccount: (account) => ({
|
||||
accountId: account.accountId,
|
||||
name: account.name,
|
||||
enabled: account.enabled,
|
||||
configured: account.credentialSource !== "none",
|
||||
credentialSource: account.credentialSource,
|
||||
streaming: {
|
||||
blockStreamingCoalesceDefaults: { minChars: 1500, idleMs: 1000 },
|
||||
},
|
||||
reload: { configPrefixes: ["channels.googlechat"] },
|
||||
configSchema: buildChannelConfigSchema(GoogleChatConfigSchema),
|
||||
config: {
|
||||
...googleChatConfigAdapter,
|
||||
isConfigured: (account) => account.credentialSource !== "none",
|
||||
describeAccount: (account) => ({
|
||||
accountId: account.accountId,
|
||||
name: account.name,
|
||||
enabled: account.enabled,
|
||||
configured: account.credentialSource !== "none",
|
||||
credentialSource: account.credentialSource,
|
||||
}),
|
||||
},
|
||||
groups: {
|
||||
resolveRequireMention: resolveGoogleChatGroupRequireMention,
|
||||
},
|
||||
messaging: {
|
||||
normalizeTarget: normalizeGoogleChatTarget,
|
||||
targetResolver: {
|
||||
looksLikeId: (raw, normalized) => {
|
||||
const value = normalized ?? raw.trim();
|
||||
return isGoogleChatSpaceTarget(value) || isGoogleChatUserTarget(value);
|
||||
},
|
||||
hint: "<spaces/{space}|users/{user}>",
|
||||
},
|
||||
},
|
||||
directory: createChannelDirectoryAdapter({
|
||||
listPeers: async (params) =>
|
||||
listResolvedDirectoryUserEntriesFromAllowFrom({
|
||||
...params,
|
||||
resolveAccount: (cfg, accountId) => resolveGoogleChatAccount({ cfg, accountId }),
|
||||
resolveAllowFrom: (account) => account.config.dm?.allowFrom,
|
||||
normalizeId: (entry) => normalizeGoogleChatTarget(entry) ?? entry,
|
||||
}),
|
||||
listGroups: async (params) =>
|
||||
listResolvedDirectoryGroupEntriesFromMapKeys({
|
||||
...params,
|
||||
resolveAccount: (cfg, accountId) => resolveGoogleChatAccount({ cfg, accountId }),
|
||||
resolveGroups: (account) => account.config.groups,
|
||||
}),
|
||||
}),
|
||||
resolver: {
|
||||
resolveTargets: async ({ inputs, kind }) => {
|
||||
const resolved = inputs.map((input) => {
|
||||
const normalized = normalizeGoogleChatTarget(input);
|
||||
if (!normalized) {
|
||||
return { input, resolved: false, note: "empty target" };
|
||||
}
|
||||
if (kind === "user" && isGoogleChatUserTarget(normalized)) {
|
||||
return { input, resolved: true, id: normalized };
|
||||
}
|
||||
if (kind === "group" && isGoogleChatSpaceTarget(normalized)) {
|
||||
return { input, resolved: true, id: normalized };
|
||||
}
|
||||
return {
|
||||
input,
|
||||
resolved: false,
|
||||
note: "use spaces/{space} or users/{user}",
|
||||
};
|
||||
});
|
||||
return resolved;
|
||||
},
|
||||
},
|
||||
actions: googlechatActions,
|
||||
status: {
|
||||
defaultRuntime: {
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
running: false,
|
||||
lastStartAt: null,
|
||||
lastStopAt: null,
|
||||
lastError: null,
|
||||
},
|
||||
collectStatusIssues: (accounts): ChannelStatusIssue[] =>
|
||||
accounts.flatMap((entry) => {
|
||||
const accountId = String(entry.accountId ?? DEFAULT_ACCOUNT_ID);
|
||||
const enabled = entry.enabled !== false;
|
||||
const configured = entry.configured === true;
|
||||
if (!enabled || !configured) {
|
||||
return [];
|
||||
}
|
||||
const issues: ChannelStatusIssue[] = [];
|
||||
if (!entry.audience) {
|
||||
issues.push({
|
||||
channel: "googlechat",
|
||||
accountId,
|
||||
kind: "config",
|
||||
message: "Google Chat audience is missing (set channels.googlechat.audience).",
|
||||
fix: "Set channels.googlechat.audienceType and channels.googlechat.audience.",
|
||||
});
|
||||
}
|
||||
if (!entry.audienceType) {
|
||||
issues.push({
|
||||
channel: "googlechat",
|
||||
accountId,
|
||||
kind: "config",
|
||||
message: "Google Chat audienceType is missing (app-url or project-number).",
|
||||
fix: "Set channels.googlechat.audienceType and channels.googlechat.audience.",
|
||||
});
|
||||
}
|
||||
return issues;
|
||||
}),
|
||||
buildChannelSummary: ({ snapshot }) =>
|
||||
buildPassiveProbedChannelStatusSummary(snapshot, {
|
||||
credentialSource: snapshot.credentialSource ?? "none",
|
||||
audienceType: snapshot.audienceType ?? null,
|
||||
audience: snapshot.audience ?? null,
|
||||
webhookPath: snapshot.webhookPath ?? null,
|
||||
webhookUrl: snapshot.webhookUrl ?? null,
|
||||
}),
|
||||
probeAccount: async ({ account }) =>
|
||||
(await loadGoogleChatChannelRuntime()).probeGoogleChat(account),
|
||||
buildAccountSnapshot: ({ account, runtime, probe }) => {
|
||||
const base = buildComputedAccountStatusSnapshot({
|
||||
accountId: account.accountId,
|
||||
name: account.name,
|
||||
enabled: account.enabled,
|
||||
configured: account.credentialSource !== "none",
|
||||
runtime,
|
||||
probe,
|
||||
});
|
||||
return {
|
||||
...base,
|
||||
credentialSource: account.credentialSource,
|
||||
audienceType: account.config.audienceType,
|
||||
audience: account.config.audience,
|
||||
webhookPath: account.config.webhookPath,
|
||||
webhookUrl: account.config.webhookUrl,
|
||||
dmPolicy: account.config.dm?.policy ?? "pairing",
|
||||
};
|
||||
},
|
||||
},
|
||||
gateway: {
|
||||
startAccount: async (ctx) => {
|
||||
const account = ctx.account;
|
||||
const statusSink = createAccountStatusSink({
|
||||
accountId: account.accountId,
|
||||
setStatus: ctx.setStatus,
|
||||
});
|
||||
ctx.log?.info(`[${account.accountId}] starting Google Chat webhook`);
|
||||
const { resolveGoogleChatWebhookPath, startGoogleChatMonitor } =
|
||||
await loadGoogleChatChannelRuntime();
|
||||
statusSink({
|
||||
running: true,
|
||||
lastStartAt: Date.now(),
|
||||
webhookPath: resolveGoogleChatWebhookPath({ account }),
|
||||
audienceType: account.config.audienceType,
|
||||
audience: account.config.audience,
|
||||
});
|
||||
await runPassiveAccountLifecycle({
|
||||
abortSignal: ctx.abortSignal,
|
||||
start: async () =>
|
||||
await startGoogleChatMonitor({
|
||||
account,
|
||||
config: ctx.cfg,
|
||||
runtime: ctx.runtime,
|
||||
abortSignal: ctx.abortSignal,
|
||||
webhookPath: account.config.webhookPath,
|
||||
webhookUrl: account.config.webhookUrl,
|
||||
statusSink,
|
||||
}),
|
||||
stop: async (unregister) => {
|
||||
unregister?.();
|
||||
},
|
||||
onStop: async () => {
|
||||
statusSink({
|
||||
running: false,
|
||||
lastStopAt: Date.now(),
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
pairing: {
|
||||
text: {
|
||||
idLabel: "googlechatUserId",
|
||||
message: PAIRING_APPROVED_MESSAGE,
|
||||
normalizeAllowEntry: (entry) => formatAllowFromEntry(entry),
|
||||
notify: async ({ cfg, id, message }) => {
|
||||
const account = resolveGoogleChatAccount({ cfg: cfg });
|
||||
if (account.credentialSource === "none") {
|
||||
return;
|
||||
}
|
||||
const user = normalizeGoogleChatTarget(id) ?? id;
|
||||
const target = isGoogleChatUserTarget(user) ? user : `users/${user}`;
|
||||
const space = await resolveGoogleChatOutboundSpace({ account, target });
|
||||
const { sendGoogleChatMessage } = await loadGoogleChatChannelRuntime();
|
||||
await sendGoogleChatMessage({
|
||||
account,
|
||||
space,
|
||||
text: message,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
security: {
|
||||
resolveDmPolicy: resolveGoogleChatDmPolicy,
|
||||
dm: {
|
||||
channelKey: "googlechat",
|
||||
resolvePolicy: (account) => account.config.dm?.policy,
|
||||
resolveAllowFrom: (account) => account.config.dm?.allowFrom,
|
||||
allowFromPathSuffix: "dm.",
|
||||
normalizeEntry: (raw) => formatAllowFromEntry(raw),
|
||||
},
|
||||
collectWarnings: collectGoogleChatSecurityWarnings,
|
||||
},
|
||||
groups: {
|
||||
resolveRequireMention: resolveGoogleChatGroupRequireMention,
|
||||
},
|
||||
threading: {
|
||||
resolveReplyToMode: createTopLevelChannelReplyToModeResolver("googlechat"),
|
||||
topLevelReplyToMode: "googlechat",
|
||||
},
|
||||
messaging: {
|
||||
normalizeTarget: normalizeGoogleChatTarget,
|
||||
targetResolver: {
|
||||
looksLikeId: (raw, normalized) => {
|
||||
const value = normalized ?? raw.trim();
|
||||
return isGoogleChatSpaceTarget(value) || isGoogleChatUserTarget(value);
|
||||
},
|
||||
hint: "<spaces/{space}|users/{user}>",
|
||||
},
|
||||
},
|
||||
directory: createChannelDirectoryAdapter({
|
||||
listPeers: async (params) =>
|
||||
listResolvedDirectoryUserEntriesFromAllowFrom({
|
||||
...params,
|
||||
resolveAccount: (cfg, accountId) => resolveGoogleChatAccount({ cfg, accountId }),
|
||||
resolveAllowFrom: (account) => account.config.dm?.allowFrom,
|
||||
normalizeId: (entry) => normalizeGoogleChatTarget(entry) ?? entry,
|
||||
}),
|
||||
listGroups: async (params) =>
|
||||
listResolvedDirectoryGroupEntriesFromMapKeys({
|
||||
...params,
|
||||
resolveAccount: (cfg, accountId) => resolveGoogleChatAccount({ cfg, accountId }),
|
||||
resolveGroups: (account) => account.config.groups,
|
||||
}),
|
||||
}),
|
||||
resolver: {
|
||||
resolveTargets: async ({ inputs, kind }) => {
|
||||
const resolved = inputs.map((input) => {
|
||||
const normalized = normalizeGoogleChatTarget(input);
|
||||
if (!normalized) {
|
||||
return { input, resolved: false, note: "empty target" };
|
||||
}
|
||||
if (kind === "user" && isGoogleChatUserTarget(normalized)) {
|
||||
return { input, resolved: true, id: normalized };
|
||||
}
|
||||
if (kind === "group" && isGoogleChatSpaceTarget(normalized)) {
|
||||
return { input, resolved: true, id: normalized };
|
||||
}
|
||||
return {
|
||||
input,
|
||||
resolved: false,
|
||||
note: "use spaces/{space} or users/{user}",
|
||||
};
|
||||
});
|
||||
return resolved;
|
||||
},
|
||||
},
|
||||
actions: googlechatActions,
|
||||
outbound: {
|
||||
deliveryMode: "direct",
|
||||
chunker: (text, limit) => getGoogleChatRuntime().channel.text.chunkMarkdownText(text, limit),
|
||||
chunkerMode: "markdown",
|
||||
textChunkLimit: 4000,
|
||||
resolveTarget: ({ to }) => {
|
||||
const trimmed = to?.trim() ?? "";
|
||||
base: {
|
||||
deliveryMode: "direct",
|
||||
chunker: (text, limit) => getGoogleChatRuntime().channel.text.chunkMarkdownText(text, limit),
|
||||
chunkerMode: "markdown",
|
||||
textChunkLimit: 4000,
|
||||
resolveTarget: ({ to }) => {
|
||||
const trimmed = to?.trim() ?? "";
|
||||
|
||||
if (trimmed) {
|
||||
const normalized = normalizeGoogleChatTarget(trimmed);
|
||||
if (!normalized) {
|
||||
return {
|
||||
ok: false,
|
||||
error: missingTargetError("Google Chat", "<spaces/{space}|users/{user}>"),
|
||||
};
|
||||
if (trimmed) {
|
||||
const normalized = normalizeGoogleChatTarget(trimmed);
|
||||
if (!normalized) {
|
||||
return {
|
||||
ok: false,
|
||||
error: missingTargetError("Google Chat", "<spaces/{space}|users/{user}>"),
|
||||
};
|
||||
}
|
||||
return { ok: true, to: normalized };
|
||||
}
|
||||
return { ok: true, to: normalized };
|
||||
}
|
||||
|
||||
return {
|
||||
ok: false,
|
||||
error: missingTargetError("Google Chat", "<spaces/{space}|users/{user}>"),
|
||||
};
|
||||
return {
|
||||
ok: false,
|
||||
error: missingTargetError("Google Chat", "<spaces/{space}|users/{user}>"),
|
||||
};
|
||||
},
|
||||
},
|
||||
...createAttachedChannelResultAdapter({
|
||||
attachedResults: {
|
||||
channel: "googlechat",
|
||||
sendText: async ({ cfg, to, text, accountId, replyToId, threadId }) => {
|
||||
const account = resolveGoogleChatAccount({
|
||||
@@ -356,114 +462,6 @@ export const googlechatPlugin: ChannelPlugin<ResolvedGoogleChatAccount> = {
|
||||
chatId: space,
|
||||
};
|
||||
},
|
||||
}),
|
||||
},
|
||||
status: {
|
||||
defaultRuntime: {
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
running: false,
|
||||
lastStartAt: null,
|
||||
lastStopAt: null,
|
||||
lastError: null,
|
||||
},
|
||||
collectStatusIssues: (accounts): ChannelStatusIssue[] =>
|
||||
accounts.flatMap((entry) => {
|
||||
const accountId = String(entry.accountId ?? DEFAULT_ACCOUNT_ID);
|
||||
const enabled = entry.enabled !== false;
|
||||
const configured = entry.configured === true;
|
||||
if (!enabled || !configured) {
|
||||
return [];
|
||||
}
|
||||
const issues: ChannelStatusIssue[] = [];
|
||||
if (!entry.audience) {
|
||||
issues.push({
|
||||
channel: "googlechat",
|
||||
accountId,
|
||||
kind: "config",
|
||||
message: "Google Chat audience is missing (set channels.googlechat.audience).",
|
||||
fix: "Set channels.googlechat.audienceType and channels.googlechat.audience.",
|
||||
});
|
||||
}
|
||||
if (!entry.audienceType) {
|
||||
issues.push({
|
||||
channel: "googlechat",
|
||||
accountId,
|
||||
kind: "config",
|
||||
message: "Google Chat audienceType is missing (app-url or project-number).",
|
||||
fix: "Set channels.googlechat.audienceType and channels.googlechat.audience.",
|
||||
});
|
||||
}
|
||||
return issues;
|
||||
}),
|
||||
buildChannelSummary: ({ snapshot }) =>
|
||||
buildPassiveProbedChannelStatusSummary(snapshot, {
|
||||
credentialSource: snapshot.credentialSource ?? "none",
|
||||
audienceType: snapshot.audienceType ?? null,
|
||||
audience: snapshot.audience ?? null,
|
||||
webhookPath: snapshot.webhookPath ?? null,
|
||||
webhookUrl: snapshot.webhookUrl ?? null,
|
||||
}),
|
||||
probeAccount: async ({ account }) =>
|
||||
(await loadGoogleChatChannelRuntime()).probeGoogleChat(account),
|
||||
buildAccountSnapshot: ({ account, runtime, probe }) => {
|
||||
const base = buildComputedAccountStatusSnapshot({
|
||||
accountId: account.accountId,
|
||||
name: account.name,
|
||||
enabled: account.enabled,
|
||||
configured: account.credentialSource !== "none",
|
||||
runtime,
|
||||
probe,
|
||||
});
|
||||
return {
|
||||
...base,
|
||||
credentialSource: account.credentialSource,
|
||||
audienceType: account.config.audienceType,
|
||||
audience: account.config.audience,
|
||||
webhookPath: account.config.webhookPath,
|
||||
webhookUrl: account.config.webhookUrl,
|
||||
dmPolicy: account.config.dm?.policy ?? "pairing",
|
||||
};
|
||||
},
|
||||
},
|
||||
gateway: {
|
||||
startAccount: async (ctx) => {
|
||||
const account = ctx.account;
|
||||
const statusSink = createAccountStatusSink({
|
||||
accountId: account.accountId,
|
||||
setStatus: ctx.setStatus,
|
||||
});
|
||||
ctx.log?.info(`[${account.accountId}] starting Google Chat webhook`);
|
||||
const { resolveGoogleChatWebhookPath, startGoogleChatMonitor } =
|
||||
await loadGoogleChatChannelRuntime();
|
||||
statusSink({
|
||||
running: true,
|
||||
lastStartAt: Date.now(),
|
||||
webhookPath: resolveGoogleChatWebhookPath({ account }),
|
||||
audienceType: account.config.audienceType,
|
||||
audience: account.config.audience,
|
||||
});
|
||||
await runPassiveAccountLifecycle({
|
||||
abortSignal: ctx.abortSignal,
|
||||
start: async () =>
|
||||
await startGoogleChatMonitor({
|
||||
account,
|
||||
config: ctx.cfg,
|
||||
runtime: ctx.runtime,
|
||||
abortSignal: ctx.abortSignal,
|
||||
webhookPath: account.config.webhookPath,
|
||||
webhookUrl: account.config.webhookUrl,
|
||||
statusSink,
|
||||
}),
|
||||
stop: async (unregister) => {
|
||||
unregister?.();
|
||||
},
|
||||
onStop: async () => {
|
||||
statusSink({
|
||||
running: false,
|
||||
lastStopAt: Date.now(),
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user