From d32fdcebc1f2f79b1727d63c7d64a19496e2427a Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 24 Apr 2026 06:09:34 +0100 Subject: [PATCH] fix(channels): keep bundled setup entries dependency-light --- CHANGELOG.md | 1 + extensions/googlechat/setup-entry.ts | 4 +- extensions/googlechat/setup-plugin-api.ts | 3 + extensions/googlechat/src/channel.setup.ts | 92 ++++++++ extensions/matrix/setup-entry.ts | 4 +- extensions/matrix/setup-plugin-api.ts | 3 + extensions/matrix/src/channel.setup.ts | 49 ++++ extensions/nostr/setup-entry.ts | 4 +- extensions/nostr/setup-plugin-api.ts | 3 + extensions/nostr/src/channel.setup.ts | 231 +++++++++++++++++++ extensions/qqbot/setup-entry.ts | 2 +- extensions/qqbot/setup-plugin-api.ts | 3 + extensions/slack/src/channel.setup.ts | 81 ++++++- extensions/slack/src/setup-core.ts | 4 +- extensions/slack/src/setup-shared.ts | 131 +++++++++++ extensions/whatsapp/src/creds-persistence.ts | 8 +- 16 files changed, 605 insertions(+), 18 deletions(-) create mode 100644 extensions/googlechat/setup-plugin-api.ts create mode 100644 extensions/googlechat/src/channel.setup.ts create mode 100644 extensions/matrix/setup-plugin-api.ts create mode 100644 extensions/matrix/src/channel.setup.ts create mode 100644 extensions/nostr/setup-plugin-api.ts create mode 100644 extensions/nostr/src/channel.setup.ts create mode 100644 extensions/qqbot/setup-plugin-api.ts create mode 100644 extensions/slack/src/setup-shared.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 0814b749ac4..a2355d3c70f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ Docs: https://docs.openclaw.ai - Codex harness: route native `request_user_input` prompts back to the originating chat, preserve queued follow-up answers, and honor newer app-server command approval amendment decisions. - Codex harness/context-engine: redact context-engine assembly failures before logging, so fallback warnings do not serialize raw error objects. (#70809) Thanks @jalehman. +- WhatsApp/onboarding: keep first-run setup entry loading off the Baileys runtime dependency path, so packaged QuickStart installs can show WhatsApp setup before runtime deps are staged. Fixes #70932. - Block streaming: suppress final assembled text after partial block-delivery aborts when the already-sent text chunks exactly cover the final reply, preventing duplicate replies without dropping unrelated short messages. Fixes #70921. - Codex harness/Windows: resolve npm-installed `codex.cmd` shims through PATHEXT before starting the native app-server, so `codex/*` models work without a manual `.exe` shim. Fixes #70913. - Slack/groups: classify MPIM group DMs as group chat context and suppress verbose tool/plan progress on Slack non-DM surfaces, so internal "Working…" traces no longer leak into rooms. Fixes #70912. diff --git a/extensions/googlechat/setup-entry.ts b/extensions/googlechat/setup-entry.ts index f9e358e5359..27c28f1e8f0 100644 --- a/extensions/googlechat/setup-entry.ts +++ b/extensions/googlechat/setup-entry.ts @@ -3,8 +3,8 @@ import { defineBundledChannelSetupEntry } from "openclaw/plugin-sdk/channel-entr export default defineBundledChannelSetupEntry({ importMetaUrl: import.meta.url, plugin: { - specifier: "./api.js", - exportName: "googlechatPlugin", + specifier: "./setup-plugin-api.js", + exportName: "googlechatSetupPlugin", }, secrets: { specifier: "./secret-contract-api.js", diff --git a/extensions/googlechat/setup-plugin-api.ts b/extensions/googlechat/setup-plugin-api.ts new file mode 100644 index 00000000000..46700ad59b1 --- /dev/null +++ b/extensions/googlechat/setup-plugin-api.ts @@ -0,0 +1,3 @@ +// Keep bundled setup entry imports narrow so setup loads do not pull the +// broader Google Chat runtime plugin surface. +export { googlechatSetupPlugin } from "./src/channel.setup.js"; diff --git a/extensions/googlechat/src/channel.setup.ts b/extensions/googlechat/src/channel.setup.ts new file mode 100644 index 00000000000..aaa5d093d48 --- /dev/null +++ b/extensions/googlechat/src/channel.setup.ts @@ -0,0 +1,92 @@ +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 type { ChannelPlugin } from "openclaw/plugin-sdk/channel-core"; +import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; +import { + listGoogleChatAccountIds, + resolveDefaultGoogleChatAccountId, + resolveGoogleChatAccount, + type ResolvedGoogleChatAccount, +} from "./accounts.js"; +import { googlechatSetupAdapter } from "./setup-core.js"; +import { googlechatSetupWizard } from "./setup-surface.js"; + +const formatGoogleChatAllowFromEntry = (entry: string) => + normalizeLowercaseStringOrEmpty( + entry + .trim() + .replace(/^(googlechat|google-chat|gchat):/i, "") + .replace(/^user:/i, "") + .replace(/^users\//i, ""), + ); + +const googleChatConfigAdapter = createScopedChannelConfigAdapter({ + sectionKey: "googlechat", + listAccountIds: listGoogleChatAccountIds, + resolveAccount: adaptScopedAccountAccessor(resolveGoogleChatAccount), + defaultAccountId: resolveDefaultGoogleChatAccountId, + clearBaseFields: [ + "serviceAccount", + "serviceAccountFile", + "audienceType", + "audience", + "webhookPath", + "webhookUrl", + "botUser", + "name", + ], + resolveAllowFrom: (account) => account.config.dm?.allowFrom, + formatAllowFrom: (allowFrom) => + formatNormalizedAllowFromEntries({ + allowFrom, + normalizeEntry: formatGoogleChatAllowFromEntry, + }), + resolveDefaultTo: (account) => account.config.defaultTo, +}); + +export const googlechatSetupPlugin: ChannelPlugin = { + id: "googlechat", + 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, + }, + 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"] }, + config: { + ...googleChatConfigAdapter, + isConfigured: (account) => account.credentialSource !== "none", + describeAccount: (account) => + describeAccountSnapshot({ + account, + configured: account.credentialSource !== "none", + extra: { + credentialSource: account.credentialSource, + }, + }), + }, +}; diff --git a/extensions/matrix/setup-entry.ts b/extensions/matrix/setup-entry.ts index bb229db97bc..eeb16770fd6 100644 --- a/extensions/matrix/setup-entry.ts +++ b/extensions/matrix/setup-entry.ts @@ -3,8 +3,8 @@ import { defineBundledChannelSetupEntry } from "openclaw/plugin-sdk/channel-entr export default defineBundledChannelSetupEntry({ importMetaUrl: import.meta.url, plugin: { - specifier: "./channel-plugin-api.js", - exportName: "matrixPlugin", + specifier: "./setup-plugin-api.js", + exportName: "matrixSetupPlugin", }, secrets: { specifier: "./secret-contract-api.js", diff --git a/extensions/matrix/setup-plugin-api.ts b/extensions/matrix/setup-plugin-api.ts new file mode 100644 index 00000000000..a46bf75a829 --- /dev/null +++ b/extensions/matrix/setup-plugin-api.ts @@ -0,0 +1,3 @@ +// Keep bundled setup entry imports narrow so setup loads do not pull the +// broader Matrix runtime plugin surface. +export { matrixSetupPlugin } from "./src/channel.setup.js"; diff --git a/extensions/matrix/src/channel.setup.ts b/extensions/matrix/src/channel.setup.ts new file mode 100644 index 00000000000..745dd3c47b8 --- /dev/null +++ b/extensions/matrix/src/channel.setup.ts @@ -0,0 +1,49 @@ +import { describeAccountSnapshot } from "openclaw/plugin-sdk/account-helpers"; +import { buildChannelConfigSchema } from "openclaw/plugin-sdk/channel-config-primitives"; +import type { ChannelPlugin } from "openclaw/plugin-sdk/channel-core"; +import { matrixConfigAdapter } from "./config-adapter.js"; +import { MatrixConfigSchema } from "./config-schema.js"; +import { resolveMatrixAccount, type ResolvedMatrixAccount } from "./matrix/accounts.js"; +import { createMatrixSetupWizardProxy, matrixSetupAdapter } from "./setup-core.js"; + +const matrixSetupWizard = createMatrixSetupWizardProxy(async () => ({ + matrixSetupWizard: (await import("./setup-surface.js")).matrixSetupWizard, +})); + +export const matrixSetupPlugin: ChannelPlugin = { + id: "matrix", + meta: { + id: "matrix", + label: "Matrix", + selectionLabel: "Matrix (plugin)", + docsPath: "/channels/matrix", + docsLabel: "matrix", + blurb: "open protocol; configure a homeserver + access token.", + order: 70, + quickstartAllowFrom: true, + }, + setupWizard: matrixSetupWizard, + setup: matrixSetupAdapter, + capabilities: { + chatTypes: ["direct", "group", "thread"], + polls: true, + reactions: true, + threads: true, + media: true, + }, + reload: { configPrefixes: ["channels.matrix"] }, + configSchema: buildChannelConfigSchema(MatrixConfigSchema), + config: { + ...matrixConfigAdapter, + isConfigured: (account) => account.configured, + describeAccount: (account) => + describeAccountSnapshot({ + account, + configured: account.configured, + extra: { + baseUrl: account.homeserver, + }, + }), + hasConfiguredState: ({ cfg }) => resolveMatrixAccount({ cfg }).configured, + }, +}; diff --git a/extensions/nostr/setup-entry.ts b/extensions/nostr/setup-entry.ts index 6dffaed9bcb..145d15dd4c9 100644 --- a/extensions/nostr/setup-entry.ts +++ b/extensions/nostr/setup-entry.ts @@ -3,7 +3,7 @@ import { defineBundledChannelSetupEntry } from "openclaw/plugin-sdk/channel-entr export default defineBundledChannelSetupEntry({ importMetaUrl: import.meta.url, plugin: { - specifier: "./api.js", - exportName: "nostrPlugin", + specifier: "./setup-plugin-api.js", + exportName: "nostrSetupPlugin", }, }); diff --git a/extensions/nostr/setup-plugin-api.ts b/extensions/nostr/setup-plugin-api.ts new file mode 100644 index 00000000000..682fb0b08a2 --- /dev/null +++ b/extensions/nostr/setup-plugin-api.ts @@ -0,0 +1,3 @@ +// Keep bundled setup entry imports narrow so setup loads do not pull the +// broader Nostr runtime plugin surface. +export { nostrSetupPlugin } from "./src/channel.setup.js"; diff --git a/extensions/nostr/src/channel.setup.ts b/extensions/nostr/src/channel.setup.ts new file mode 100644 index 00000000000..2c03a8c9231 --- /dev/null +++ b/extensions/nostr/src/channel.setup.ts @@ -0,0 +1,231 @@ +import { describeAccountSnapshot } from "openclaw/plugin-sdk/account-helpers"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; +import { patchTopLevelChannelConfigSection } from "openclaw/plugin-sdk/setup"; +import { + createDelegatedSetupWizardProxy, + createStandardChannelSetupStatus, + DEFAULT_ACCOUNT_ID, + type ChannelSetupAdapter, +} from "openclaw/plugin-sdk/setup-runtime"; +import { buildChannelConfigSchema, type ChannelPlugin } from "./channel-api.js"; +import { NostrConfigSchema } from "./config-schema.js"; +import { DEFAULT_RELAYS } from "./default-relays.js"; + +const channel = "nostr" as const; + +type NostrAccountConfig = { + enabled?: boolean; + name?: string; + defaultAccount?: string; + privateKey?: unknown; + relays?: string[]; + dmPolicy?: "pairing" | "allowlist" | "open" | "disabled"; + allowFrom?: Array; + profile?: unknown; +}; + +type ResolvedNostrSetupAccount = { + accountId: string; + name?: string; + enabled: boolean; + configured: boolean; + privateKey: string; + publicKey: string; + relays: string[]; + profile?: unknown; + config: NostrAccountConfig; +}; + +function getNostrConfig(cfg: OpenClawConfig): NostrAccountConfig | undefined { + return (cfg.channels as Record | undefined)?.nostr as + | NostrAccountConfig + | undefined; +} + +function listSetupNostrAccountIds(cfg: OpenClawConfig): string[] { + const nostrCfg = getNostrConfig(cfg); + const privateKey = typeof nostrCfg?.privateKey === "string" ? nostrCfg.privateKey.trim() : ""; + if (!privateKey) { + return []; + } + return [resolveDefaultSetupNostrAccountId(cfg)]; +} + +function resolveDefaultSetupNostrAccountId(cfg: OpenClawConfig): string { + const configured = getNostrConfig(cfg)?.defaultAccount; + return typeof configured === "string" && configured.trim() + ? configured.trim() + : DEFAULT_ACCOUNT_ID; +} + +function resolveSetupNostrAccount(params: { + cfg: OpenClawConfig; + accountId?: string | null; +}): ResolvedNostrSetupAccount { + const nostrCfg = getNostrConfig(params.cfg); + const accountId = params.accountId?.trim() || resolveDefaultSetupNostrAccountId(params.cfg); + const privateKey = typeof nostrCfg?.privateKey === "string" ? nostrCfg.privateKey.trim() : ""; + const configured = Boolean(privateKey); + return { + accountId, + name: typeof nostrCfg?.name === "string" ? nostrCfg.name : undefined, + enabled: nostrCfg?.enabled !== false, + configured, + privateKey, + publicKey: "", + relays: nostrCfg?.relays ?? DEFAULT_RELAYS, + profile: nostrCfg?.profile, + config: { + enabled: nostrCfg?.enabled, + name: nostrCfg?.name, + privateKey: nostrCfg?.privateKey, + relays: nostrCfg?.relays, + dmPolicy: nostrCfg?.dmPolicy, + allowFrom: nostrCfg?.allowFrom, + profile: nostrCfg?.profile, + }, + }; +} + +function buildNostrSetupPatch(accountId: string, patch: Record) { + return { + ...(accountId !== DEFAULT_ACCOUNT_ID ? { defaultAccount: accountId } : {}), + ...patch, + }; +} + +function parseRelayUrls(raw: string): { relays: string[]; error?: string } { + const entries = raw + .split(/[,\n]/) + .map((entry) => entry.trim()) + .filter(Boolean); + const relays: string[] = []; + for (const entry of entries) { + try { + const parsed = new URL(entry); + if (parsed.protocol !== "ws:" && parsed.protocol !== "wss:") { + return { relays: [], error: `Relay must use ws:// or wss:// (${entry})` }; + } + } catch { + return { relays: [], error: `Invalid relay URL: ${entry}` }; + } + relays.push(entry); + } + return { relays: [...new Set(relays)] }; +} + +function looksLikeNostrPrivateKey(privateKey: string): boolean { + return privateKey.startsWith("nsec1") || /^[0-9a-fA-F]{64}$/.test(privateKey); +} + +const nostrSetupAdapter: ChannelSetupAdapter = { + resolveAccountId: ({ cfg, accountId }) => + accountId?.trim() || resolveDefaultSetupNostrAccountId(cfg), + applyAccountName: ({ cfg, accountId, name }) => + patchTopLevelChannelConfigSection({ + cfg, + channel, + patch: buildNostrSetupPatch(accountId, name?.trim() ? { name: name.trim() } : {}), + }), + validateInput: ({ input }) => { + const typedInput = input as { + useEnv?: boolean; + privateKey?: string; + relayUrls?: string; + }; + if (!typedInput.useEnv) { + const privateKey = typedInput.privateKey?.trim(); + if (!privateKey) { + return "Nostr requires --private-key or --use-env."; + } + if (!looksLikeNostrPrivateKey(privateKey)) { + return "Nostr private key must be valid nsec or 64-character hex."; + } + } + if (typedInput.relayUrls?.trim()) { + return parseRelayUrls(typedInput.relayUrls).error ?? null; + } + return null; + }, + applyAccountConfig: ({ cfg, accountId, input }) => { + const typedInput = input as { + useEnv?: boolean; + privateKey?: string; + relayUrls?: string; + }; + const relayResult = typedInput.relayUrls?.trim() + ? parseRelayUrls(typedInput.relayUrls) + : { relays: [] }; + return patchTopLevelChannelConfigSection({ + cfg, + channel, + enabled: true, + clearFields: typedInput.useEnv ? ["privateKey"] : undefined, + patch: buildNostrSetupPatch(accountId, { + ...(typedInput.useEnv ? {} : { privateKey: typedInput.privateKey?.trim() }), + ...(relayResult.relays.length > 0 ? { relays: relayResult.relays } : {}), + }), + }); + }, +}; + +const nostrSetupWizard = createDelegatedSetupWizardProxy({ + channel, + loadWizard: async () => (await import("./setup-surface.js")).nostrSetupWizard, + status: { + ...createStandardChannelSetupStatus({ + channelLabel: "Nostr", + configuredLabel: "configured", + unconfiguredLabel: "needs private key", + configuredHint: "configured", + unconfiguredHint: "needs private key", + configuredScore: 1, + unconfiguredScore: 0, + includeStatusLine: true, + resolveConfigured: ({ cfg, accountId }) => + resolveSetupNostrAccount({ cfg, accountId }).configured, + resolveExtraStatusLines: ({ cfg }) => { + const account = resolveSetupNostrAccount({ cfg }); + return [`Relays: ${account.relays.length || DEFAULT_RELAYS.length}`]; + }, + }), + }, + resolveShouldPromptAccountIds: () => false, + delegatePrepare: true, + delegateFinalize: true, +}); + +export const nostrSetupPlugin: ChannelPlugin = { + id: channel, + meta: { + id: channel, + label: "Nostr", + selectionLabel: "Nostr", + docsPath: "/channels/nostr", + docsLabel: "nostr", + blurb: "Decentralized DMs via Nostr relays (NIP-04)", + order: 100, + }, + capabilities: { + chatTypes: ["direct"], + media: false, + }, + reload: { configPrefixes: ["channels.nostr"] }, + configSchema: buildChannelConfigSchema(NostrConfigSchema), + setup: nostrSetupAdapter, + setupWizard: nostrSetupWizard, + config: { + listAccountIds: listSetupNostrAccountIds, + resolveAccount: (cfg, accountId) => resolveSetupNostrAccount({ cfg, accountId }), + defaultAccountId: resolveDefaultSetupNostrAccountId, + isConfigured: (account) => account.configured, + describeAccount: (account) => + describeAccountSnapshot({ + account, + configured: account.configured, + extra: { + publicKey: account.publicKey, + }, + }), + }, +}; diff --git a/extensions/qqbot/setup-entry.ts b/extensions/qqbot/setup-entry.ts index d9fb6ab2c07..74838a01a96 100644 --- a/extensions/qqbot/setup-entry.ts +++ b/extensions/qqbot/setup-entry.ts @@ -3,7 +3,7 @@ import { defineBundledChannelSetupEntry } from "openclaw/plugin-sdk/channel-entr export default defineBundledChannelSetupEntry({ importMetaUrl: import.meta.url, plugin: { - specifier: "./api.js", + specifier: "./setup-plugin-api.js", exportName: "qqbotSetupPlugin", }, }); diff --git a/extensions/qqbot/setup-plugin-api.ts b/extensions/qqbot/setup-plugin-api.ts new file mode 100644 index 00000000000..665fb24f8e7 --- /dev/null +++ b/extensions/qqbot/setup-plugin-api.ts @@ -0,0 +1,3 @@ +// Keep bundled setup entry imports narrow so setup loads do not pull the +// broader QQ Bot runtime plugin surface. +export { qqbotSetupPlugin } from "./src/channel.setup.js"; diff --git a/extensions/slack/src/channel.setup.ts b/extensions/slack/src/channel.setup.ts index e5a86031c71..713a39fb7c4 100644 --- a/extensions/slack/src/channel.setup.ts +++ b/extensions/slack/src/channel.setup.ts @@ -1,12 +1,79 @@ +import { formatAllowFromLowercase } from "openclaw/plugin-sdk/allow-from"; +import { + adaptScopedAccountAccessor, + createScopedChannelConfigAdapter, +} from "openclaw/plugin-sdk/channel-config-helpers"; import { type ResolvedSlackAccount } from "./accounts.js"; +import { + listSlackAccountIds, + resolveDefaultSlackAccountId, + resolveSlackAccount, +} from "./accounts.js"; import { type ChannelPlugin } from "./channel-api.js"; -import { slackSetupAdapter } from "./setup-core.js"; -import { slackSetupWizard } from "./setup-surface.js"; -import { createSlackPluginBase } from "./shared.js"; +import { SlackChannelConfigSchema } from "./config-schema.js"; +import { slackSetupAdapter, createSlackSetupWizardProxy } from "./setup-core.js"; +import { + describeSlackSetupAccount, + isSlackSetupAccountConfigured, + SLACK_CHANNEL, +} from "./setup-shared.js"; + +const slackSetupWizard = createSlackSetupWizardProxy(async () => ({ + slackSetupWizard: (await import("./setup-surface.js")).slackSetupWizard, +})); + +const slackSetupConfigAdapter = createScopedChannelConfigAdapter({ + sectionKey: SLACK_CHANNEL, + listAccountIds: listSlackAccountIds, + resolveAccount: adaptScopedAccountAccessor(resolveSlackAccount), + defaultAccountId: resolveDefaultSlackAccountId, + clearBaseFields: ["botToken", "appToken", "name"], + resolveAllowFrom: (account) => account.dm?.allowFrom, + formatAllowFrom: (allowFrom) => formatAllowFromLowercase({ allowFrom }), + resolveDefaultTo: (account) => account.config.defaultTo, +}); export const slackSetupPlugin: ChannelPlugin = { - ...createSlackPluginBase({ - setupWizard: slackSetupWizard, - setup: slackSetupAdapter, - }), + id: SLACK_CHANNEL, + meta: { + id: SLACK_CHANNEL, + label: "Slack", + selectionLabel: "Slack (Socket Mode)", + detailLabel: "Slack Bot", + docsPath: "/channels/slack", + docsLabel: "slack", + blurb: "supported (Socket Mode).", + systemImage: "number", + markdownCapable: true, + preferSessionLookupForAnnounceTarget: true, + }, + setupWizard: slackSetupWizard, + capabilities: { + chatTypes: ["direct", "channel", "thread"], + reactions: true, + threads: true, + media: true, + nativeCommands: true, + }, + commands: { + nativeCommandsAutoEnabled: false, + nativeSkillsAutoEnabled: false, + resolveNativeCommandName: ({ commandKey, defaultName }) => + commandKey === "status" ? "agentstatus" : defaultName, + }, + streaming: { + blockStreamingCoalesceDefaults: { minChars: 1500, idleMs: 1000 }, + }, + reload: { configPrefixes: ["channels.slack"] }, + configSchema: SlackChannelConfigSchema, + config: { + ...slackSetupConfigAdapter, + hasConfiguredState: ({ env }) => + ["SLACK_APP_TOKEN", "SLACK_BOT_TOKEN", "SLACK_USER_TOKEN"].some( + (key) => typeof env?.[key] === "string" && env[key]?.trim().length > 0, + ), + isConfigured: (account) => isSlackSetupAccountConfigured(account), + describeAccount: (account) => describeSlackSetupAccount(account), + }, + setup: slackSetupAdapter, }; diff --git a/extensions/slack/src/setup-core.ts b/extensions/slack/src/setup-core.ts index d33f63a42ad..28bb255f1ae 100644 --- a/extensions/slack/src/setup-core.ts +++ b/extensions/slack/src/setup-core.ts @@ -24,10 +24,10 @@ import { inspectSlackAccount } from "./account-inspect.js"; import { resolveSlackAccount } from "./accounts.js"; import { buildSlackSetupLines, - SLACK_CHANNEL as channel, isSlackSetupAccountConfigured, + SLACK_CHANNEL as channel, setSlackChannelAllowlist, -} from "./shared.js"; +} from "./setup-shared.js"; function enableSlackAccount(cfg: OpenClawConfig, accountId: string): OpenClawConfig { return patchChannelConfigForAccount({ diff --git a/extensions/slack/src/setup-shared.ts b/extensions/slack/src/setup-shared.ts new file mode 100644 index 00000000000..aeaf60b4faf --- /dev/null +++ b/extensions/slack/src/setup-shared.ts @@ -0,0 +1,131 @@ +import { describeAccountSnapshot } from "openclaw/plugin-sdk/account-helpers"; +import { hasConfiguredSecretInput } from "openclaw/plugin-sdk/secret-input"; +import { patchChannelConfigForAccount } from "openclaw/plugin-sdk/setup-runtime"; +import { formatDocsLink } from "openclaw/plugin-sdk/setup-tools"; +import type { ResolvedSlackAccount } from "./accounts.js"; +import type { OpenClawConfig } from "./channel-api.js"; + +export const SLACK_CHANNEL = "slack" as const; + +function buildSlackManifest(botName: string) { + const safeName = botName.trim() || "OpenClaw"; + const manifest = { + display_information: { + name: safeName, + description: `${safeName} connector for OpenClaw`, + }, + features: { + bot_user: { + display_name: safeName, + always_online: true, + }, + app_home: { + messages_tab_enabled: true, + messages_tab_read_only_enabled: false, + }, + slash_commands: [ + { + command: "/openclaw", + description: "Send a message to OpenClaw", + should_escape: false, + }, + ], + }, + oauth_config: { + scopes: { + bot: [ + "app_mentions:read", + "assistant:write", + "channels:history", + "channels:read", + "chat:write", + "commands", + "emoji:read", + "files:read", + "files:write", + "groups:history", + "groups:read", + "im:history", + "im:read", + "im:write", + "mpim:history", + "mpim:read", + "mpim:write", + "pins:read", + "pins:write", + "reactions:read", + "reactions:write", + "users:read", + ], + }, + }, + settings: { + socket_mode_enabled: true, + event_subscriptions: { + bot_events: [ + "app_mention", + "channel_rename", + "member_joined_channel", + "member_left_channel", + "message.channels", + "message.groups", + "message.im", + "message.mpim", + "pin_added", + "pin_removed", + "reaction_added", + "reaction_removed", + ], + }, + }, + }; + return JSON.stringify(manifest, null, 2); +} + +export function buildSlackSetupLines(botName = "OpenClaw"): string[] { + return [ + "1) Slack API -> Create App -> From scratch or From manifest (with the JSON below)", + "2) Add Socket Mode + enable it to get the app-level token (xapp-...)", + "3) Install App to workspace to get the xoxb- bot token", + "4) Enable Event Subscriptions (socket) for message events", + "5) App Home -> enable the Messages tab for DMs", + "Tip: set SLACK_BOT_TOKEN + SLACK_APP_TOKEN in your env.", + `Docs: ${formatDocsLink("/slack", "slack")}`, + "", + "Manifest (JSON):", + buildSlackManifest(botName), + ]; +} + +export function setSlackChannelAllowlist( + cfg: OpenClawConfig, + accountId: string, + channelKeys: string[], +): OpenClawConfig { + const channels = Object.fromEntries(channelKeys.map((key) => [key, { enabled: true }])); + return patchChannelConfigForAccount({ + cfg, + channel: SLACK_CHANNEL, + accountId, + patch: { channels }, + }); +} + +export function isSlackSetupAccountConfigured(account: ResolvedSlackAccount): boolean { + const hasConfiguredBotToken = + Boolean(account.botToken?.trim()) || hasConfiguredSecretInput(account.config.botToken); + const hasConfiguredAppToken = + Boolean(account.appToken?.trim()) || hasConfiguredSecretInput(account.config.appToken); + return hasConfiguredBotToken && hasConfiguredAppToken; +} + +export function describeSlackSetupAccount(account: ResolvedSlackAccount) { + return describeAccountSnapshot({ + account, + configured: isSlackSetupAccountConfigured(account), + extra: { + botTokenSource: account.botTokenSource, + appTokenSource: account.appTokenSource, + }, + }); +} diff --git a/extensions/whatsapp/src/creds-persistence.ts b/extensions/whatsapp/src/creds-persistence.ts index 6c23b3e6985..7930dc8ffbb 100644 --- a/extensions/whatsapp/src/creds-persistence.ts +++ b/extensions/whatsapp/src/creds-persistence.ts @@ -2,7 +2,6 @@ import { randomUUID } from "node:crypto"; import fs from "node:fs/promises"; import path from "node:path"; import { resolveWebCredsPath } from "./creds-files.js"; -import { BufferJSON } from "./session.runtime.js"; const CREDS_FILE_MODE = 0o600; const CREDS_SAVE_FLUSH_TIMEOUT_MS = 15_000; @@ -11,6 +10,11 @@ const credsSaveQueues = new Map>(); export type CredsQueueWaitResult = "drained" | "timed_out"; +async function stringifyCreds(creds: unknown): Promise { + const { BufferJSON } = await import("./session.runtime.js"); + return JSON.stringify(creds, BufferJSON.replacer); +} + async function syncDirectory(dirPath: string): Promise { let handle: Awaited> | undefined; try { @@ -28,7 +32,7 @@ async function syncDirectory(dirPath: string): Promise { export async function writeCredsJsonAtomically(authDir: string, creds: unknown): Promise { const credsPath = resolveWebCredsPath(authDir); const tempPath = path.join(authDir, `.creds.${process.pid}.${randomUUID()}.tmp`); - const json = JSON.stringify(creds, BufferJSON.replacer); + const json = await stringifyCreds(creds); let handle: Awaited> | undefined; try {