mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-25 08:11:15 +00:00
* fix(channels): enforce configured read targets * test(channels): align policy checks with boundaries * fix: bind channel reads to trusted turn context * test: satisfy gateway lint * fix: narrow message action channel imports * fix(feishu): authorize message reads before provider access * fix(slack): await reaction clear authorization * fix(channels): align provider action contracts * fix(matrix): read direct-room account data before sync * fix(channels): reject unsupported attachment actions early * fix: restore trusted operator conversation reads * fix(matrix): authorize pin actions before provider reads * fix: preserve trusted channel read workflows * fix(discord): resolve current channel ids consistently * fix(agents): preserve message action turn capability * fix(plugins): enforce host-owned read provenance * fix(channels): harden Teams and Discord read policy * fix(channels): preserve exact-current action compatibility * fix(imessage): authorize trusted current chat aliases * fix(channels): preserve normalized current aliases * fix(channels): preserve external current target aliases * fix: reconcile channel policy with current main * fix(discord): isolate DM read policy * fix(channels): enforce provider read gates * fix(gateway): await serialized message action identity tokens * fix(ci): refresh channel protocol contracts
212 lines
7.8 KiB
TypeScript
212 lines
7.8 KiB
TypeScript
// Googlechat plugin module implements channel behavior.
|
|
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 { extractToolSend } from "openclaw/plugin-sdk/tool-send";
|
|
import {
|
|
googleChatApprovalCapability,
|
|
shouldSuppressLocalGoogleChatExecApprovalPrompt,
|
|
} from "./approval-native.js";
|
|
import { createGoogleChatPluginBase, GOOGLECHAT_CHANNEL_ID } from "./channel-base.js";
|
|
import {
|
|
googlechatDirectoryAdapter,
|
|
googlechatGroupsAdapter,
|
|
googlechatMessageAdapter,
|
|
googlechatOutboundAdapter,
|
|
googlechatPairingTextAdapter,
|
|
googlechatSecurityAdapter,
|
|
googlechatThreadingAdapter,
|
|
} from "./channel.adapters.js";
|
|
import {
|
|
buildChannelConfigSchema,
|
|
DEFAULT_ACCOUNT_ID,
|
|
GoogleChatConfigSchema,
|
|
isGoogleChatSpaceTarget,
|
|
isGoogleChatUserTarget,
|
|
listGoogleChatAccountIds,
|
|
normalizeGoogleChatTarget,
|
|
resolveGoogleChatAccount,
|
|
resolveGoogleChatOutboundSessionRoute,
|
|
type ChannelMessageActionAdapter,
|
|
type ChannelStatusIssue,
|
|
type ResolvedGoogleChatAccount,
|
|
} from "./channel.deps.runtime.js";
|
|
import {
|
|
legacyConfigRules as GOOGLECHAT_LEGACY_CONFIG_RULES,
|
|
normalizeCompatibilityConfig as normalizeGoogleChatCompatibilityConfig,
|
|
} from "./doctor-contract.js";
|
|
import { collectGoogleChatMutableAllowlistWarnings } from "./doctor.js";
|
|
import { startGoogleChatGatewayAccount } from "./gateway.js";
|
|
import { collectRuntimeConfigAssignments, secretTargetRegistryEntries } from "./secret-contract.js";
|
|
|
|
const loadGoogleChatChannelRuntime = createLazyRuntimeNamedExport(
|
|
() => import("./channel.runtime.js"),
|
|
"googleChatChannelRuntime",
|
|
);
|
|
|
|
const googlechatActions: ChannelMessageActionAdapter = {
|
|
describeMessageTool: ({ cfg, accountId }) => {
|
|
const accounts = accountId
|
|
? [resolveGoogleChatAccount({ cfg, accountId })].filter(
|
|
(account) => account.enabled && account.credentialSource !== "none",
|
|
)
|
|
: listGoogleChatAccountIds(cfg)
|
|
.map((id) => resolveGoogleChatAccount({ cfg, accountId: id }))
|
|
.filter((account) => account.enabled && account.credentialSource !== "none");
|
|
if (accounts.length === 0) {
|
|
return null;
|
|
}
|
|
return { actions: ["send"] };
|
|
},
|
|
supportsAction: ({ action }) => action === "send",
|
|
extractToolSend: ({ args }) => extractToolSend(args, "sendMessage"),
|
|
handleAction: async (ctx) => {
|
|
const { googlechatMessageActions } = await import("./actions.js");
|
|
if (!googlechatMessageActions.handleAction) {
|
|
throw new Error("Google Chat actions are not available.");
|
|
}
|
|
return await googlechatMessageActions.handleAction(ctx);
|
|
},
|
|
};
|
|
|
|
export const googlechatPlugin = createChatChannelPlugin({
|
|
base: {
|
|
...createGoogleChatPluginBase({
|
|
configSchema: buildChannelConfigSchema(GoogleChatConfigSchema),
|
|
}),
|
|
approvalCapability: googleChatApprovalCapability,
|
|
secrets: {
|
|
secretTargetRegistryEntries,
|
|
collectRuntimeConfigAssignments,
|
|
},
|
|
groups: googlechatGroupsAdapter,
|
|
messaging: {
|
|
targetPrefixes: ["googlechat", "google-chat", "gchat"],
|
|
normalizeTarget: normalizeGoogleChatTarget,
|
|
resolveOutboundSessionRoute: (params) => resolveGoogleChatOutboundSessionRoute(params),
|
|
targetResolver: {
|
|
looksLikeId: (raw, normalized) => {
|
|
const value = normalized ?? raw.trim();
|
|
return isGoogleChatSpaceTarget(value) || isGoogleChatUserTarget(value);
|
|
},
|
|
hint: "<spaces/{space}|users/{user}>",
|
|
},
|
|
},
|
|
directory: googlechatDirectoryAdapter,
|
|
message: googlechatMessageAdapter,
|
|
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,
|
|
legacyConfigRules: GOOGLECHAT_LEGACY_CONFIG_RULES,
|
|
normalizeCompatibilityConfig: normalizeGoogleChatCompatibilityConfig,
|
|
collectMutableAllowlistWarnings: collectGoogleChatMutableAllowlistWarnings,
|
|
},
|
|
status: createComputedAccountStatusAdapter<ResolvedGoogleChatAccount>({
|
|
defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID),
|
|
collectStatusIssues: (accounts): ChannelStatusIssue[] =>
|
|
accounts.flatMap((entry) => {
|
|
const accountId = 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_CHANNEL_ID,
|
|
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_CHANNEL_ID,
|
|
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,
|
|
base: {
|
|
...googlechatOutboundAdapter.base,
|
|
shouldSuppressLocalPayloadPrompt: ({ cfg, accountId, payload, hint }) =>
|
|
shouldSuppressLocalGoogleChatExecApprovalPrompt({
|
|
cfg,
|
|
accountId,
|
|
payload,
|
|
hint,
|
|
}),
|
|
},
|
|
},
|
|
});
|