Files
openclaw/extensions/googlechat/src/channel.ts
Gustavo Madeira Santana d78512b09d Refactor: centralize native approval lifecycle assembly (#62135)
Merged via squash.

Prepared head SHA: b7c20a7398
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-04-07 14:40:26 -04:00

244 lines
8.6 KiB
TypeScript

import { describeAccountSnapshot } from "openclaw/plugin-sdk/account-helpers";
import { formatNormalizedAllowFromEntries } from "openclaw/plugin-sdk/allow-from";
import {
adaptScopedAccountAccessor,
createScopedChannelConfigAdapter,
} from "openclaw/plugin-sdk/channel-config-helpers";
import { createChatChannelPlugin } from "openclaw/plugin-sdk/channel-core";
import { buildPassiveProbedChannelStatusSummary } from "openclaw/plugin-sdk/extension-shared";
import { createLazyRuntimeNamedExport } from "openclaw/plugin-sdk/lazy-runtime";
import {
createComputedAccountStatusAdapter,
createDefaultChannelRuntimeState,
} from "openclaw/plugin-sdk/status-helpers";
import { googlechatMessageActions } from "./actions.js";
import { googleChatApprovalAuth } from "./approval-auth.js";
import {
formatAllowFromEntry,
googlechatDirectoryAdapter,
googlechatGroupsAdapter,
googlechatOutboundAdapter,
googlechatPairingTextAdapter,
googlechatSecurityAdapter,
googlechatThreadingAdapter,
} from "./channel.adapters.js";
import {
buildChannelConfigSchema,
DEFAULT_ACCOUNT_ID,
GoogleChatConfigSchema,
isGoogleChatSpaceTarget,
isGoogleChatUserTarget,
listGoogleChatAccountIds,
normalizeGoogleChatTarget,
resolveDefaultGoogleChatAccountId,
resolveGoogleChatAccount,
type ChannelMessageActionAdapter,
type ChannelStatusIssue,
type ResolvedGoogleChatAccount,
} from "./channel.deps.runtime.js";
import { collectGoogleChatMutableAllowlistWarnings } from "./doctor.js";
import { startGoogleChatGatewayAccount } from "./gateway.js";
import { collectRuntimeConfigAssignments, secretTargetRegistryEntries } from "./secret-contract.js";
import { googlechatSetupAdapter } from "./setup-core.js";
import { googlechatSetupWizard } from "./setup-surface.js";
const loadGoogleChatChannelRuntime = createLazyRuntimeNamedExport(
() => import("./channel.runtime.js"),
"googleChatChannelRuntime",
);
const meta = {
id: "googlechat",
label: "Google Chat",
selectionLabel: "Google Chat (Chat API)",
docsPath: "/channels/googlechat",
docsLabel: "googlechat",
blurb: "Google Workspace Chat app with HTTP webhook.",
aliases: ["gchat", "google-chat"],
order: 55,
detailLabel: "Google Chat",
systemImage: "message.badge",
markdownCapable: true,
};
const googleChatConfigAdapter = createScopedChannelConfigAdapter<ResolvedGoogleChatAccount>({
sectionKey: "googlechat",
listAccountIds: listGoogleChatAccountIds,
resolveAccount: adaptScopedAccountAccessor(resolveGoogleChatAccount),
defaultAccountId: resolveDefaultGoogleChatAccountId,
clearBaseFields: [
"serviceAccount",
"serviceAccountFile",
"audienceType",
"audience",
"webhookPath",
"webhookUrl",
"botUser",
"name",
],
resolveAllowFrom: (account: ResolvedGoogleChatAccount) => account.config.dm?.allowFrom,
formatAllowFrom: (allowFrom) =>
formatNormalizedAllowFromEntries({
allowFrom,
normalizeEntry: formatAllowFromEntry,
}),
resolveDefaultTo: (account: ResolvedGoogleChatAccount) => account.config.defaultTo,
});
const googlechatActions: ChannelMessageActionAdapter = {
describeMessageTool: (ctx) => googlechatMessageActions.describeMessageTool?.(ctx) ?? null,
extractToolSend: (ctx) => googlechatMessageActions.extractToolSend?.(ctx) ?? null,
handleAction: async (ctx) => {
if (!googlechatMessageActions.handleAction) {
throw new Error("Google Chat actions are not available.");
}
return await googlechatMessageActions.handleAction(ctx);
},
};
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,
},
streaming: {
blockStreamingCoalesceDefaults: { minChars: 1500, idleMs: 1000 },
},
reload: { configPrefixes: ["channels.googlechat"] },
configSchema: buildChannelConfigSchema(GoogleChatConfigSchema),
config: {
...googleChatConfigAdapter,
isConfigured: (account) => account.credentialSource !== "none",
describeAccount: (account) =>
describeAccountSnapshot({
account,
configured: account.credentialSource !== "none",
extra: {
credentialSource: account.credentialSource,
},
}),
},
approvalCapability: googleChatApprovalAuth,
secrets: {
secretTargetRegistryEntries,
collectRuntimeConfigAssignments,
},
groups: googlechatGroupsAdapter,
messaging: {
normalizeTarget: normalizeGoogleChatTarget,
targetResolver: {
looksLikeId: (raw, normalized) => {
const value = normalized ?? raw.trim();
return isGoogleChatSpaceTarget(value) || isGoogleChatUserTarget(value);
},
hint: "<spaces/{space}|users/{user}>",
},
},
directory: googlechatDirectoryAdapter,
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,
doctor: {
dmAllowFromMode: "nestedOnly",
groupModel: "route",
groupAllowFromFallbackToAllowFrom: false,
warnOnEmptyGroupSenderAllowlist: false,
collectMutableAllowlistWarnings: collectGoogleChatMutableAllowlistWarnings,
},
status: createComputedAccountStatusAdapter<ResolvedGoogleChatAccount>({
defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID),
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),
resolveAccountSnapshot: ({ account }) => ({
accountId: account.accountId,
name: account.name,
enabled: account.enabled,
configured: account.credentialSource !== "none",
extra: {
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: startGoogleChatGatewayAccount,
},
},
pairing: {
text: googlechatPairingTextAdapter,
},
security: googlechatSecurityAdapter,
threading: googlechatThreadingAdapter,
outbound: googlechatOutboundAdapter,
});