From 72bfaf6ee29c8502412fa7e0687efe8c41f2bbfe Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 22 Mar 2026 23:19:20 +0000 Subject: [PATCH] refactor: share computed channel status adapters --- extensions/bluebubbles/src/channel.ts | 7 +- extensions/discord/src/channel.ts | 715 ++++++++++++----------- extensions/imessage/src/channel.ts | 37 +- extensions/matrix/src/channel.ts | 39 +- extensions/nextcloud-talk/src/channel.ts | 38 +- extensions/nostr/src/channel.ts | 34 +- extensions/signal/src/channel.ts | 17 +- extensions/slack/src/channel.ts | 5 +- extensions/tlon/src/channel.ts | 35 +- src/plugin-sdk/status-helpers.ts | 10 +- 10 files changed, 465 insertions(+), 472 deletions(-) diff --git a/extensions/bluebubbles/src/channel.ts b/extensions/bluebubbles/src/channel.ts index a0a12cb95a4..54b2e9ca8ec 100644 --- a/extensions/bluebubbles/src/channel.ts +++ b/extensions/bluebubbles/src/channel.ts @@ -104,8 +104,8 @@ const meta = { preferOver: ["imessage"], }; -export const bluebubblesPlugin: ChannelPlugin = createChatChannelPlugin( - { +export const bluebubblesPlugin: ChannelPlugin = + createChatChannelPlugin({ base: { id: "bluebubbles", meta, @@ -365,5 +365,4 @@ export const bluebubblesPlugin: ChannelPlugin = crea }, }, }, - }, -); + }); diff --git a/extensions/discord/src/channel.ts b/extensions/discord/src/channel.ts index 50ddd98a017..f67b516c50a 100644 --- a/extensions/discord/src/channel.ts +++ b/extensions/discord/src/channel.ts @@ -309,396 +309,397 @@ function resolveDiscordOutboundSessionRoute(params: { }; } -export const discordPlugin: ChannelPlugin = createChatChannelPlugin({ - base: { - ...createDiscordPluginBase({ - setup: discordSetupAdapter, - }), - allowlist: { - ...buildLegacyDmAccountAllowlistAdapter({ - channelId: "discord", - resolveAccount: resolveDiscordAccount, - normalize: ({ cfg, accountId, values }) => - discordConfigAdapter.formatAllowFrom!({ cfg, accountId, allowFrom: values }), - resolveDmAllowFrom: (account) => account.config.allowFrom ?? account.config.dm?.allowFrom, - resolveGroupPolicy: (account) => account.config.groupPolicy, - resolveGroupOverrides: resolveDiscordAllowlistGroupOverrides, +export const discordPlugin: ChannelPlugin = + createChatChannelPlugin({ + base: { + ...createDiscordPluginBase({ + setup: discordSetupAdapter, }), - resolveNames: resolveDiscordAllowlistNames, - }, - groups: { - resolveRequireMention: resolveDiscordGroupRequireMention, - resolveToolPolicy: resolveDiscordGroupToolPolicy, - }, - mentions: { - stripPatterns: () => ["<@!?\\d+>"], - }, - agentPrompt: { - messageToolHints: () => [ - "- Discord components: set `components` when sending messages to include buttons, selects, or v2 containers.", - "- Forms: add `components.modal` (title, fields). OpenClaw adds a trigger button and routes submissions as new messages.", - ], - }, - messaging: { - normalizeTarget: normalizeDiscordMessagingTarget, - resolveSessionTarget: ({ id }) => normalizeDiscordMessagingTarget(`channel:${id}`), - parseExplicitTarget: ({ raw }) => parseDiscordExplicitTarget(raw), - inferTargetChatType: ({ to }) => parseDiscordExplicitTarget(to)?.chatType, - buildCrossContextComponents: buildDiscordCrossContextComponents, - resolveOutboundSessionRoute: (params) => resolveDiscordOutboundSessionRoute(params), - targetResolver: { - looksLikeId: looksLikeDiscordTargetId, - hint: "", - }, - }, - execApprovals: { - getInitiatingSurfaceState: ({ cfg, accountId }) => - isDiscordExecApprovalClientEnabled({ cfg, accountId }) - ? { kind: "enabled" } - : { kind: "disabled" }, - shouldSuppressLocalPrompt: ({ cfg, accountId, payload }) => - shouldSuppressLocalDiscordExecApprovalPrompt({ - cfg, - accountId, - payload, + allowlist: { + ...buildLegacyDmAccountAllowlistAdapter({ + channelId: "discord", + resolveAccount: resolveDiscordAccount, + normalize: ({ cfg, accountId, values }) => + discordConfigAdapter.formatAllowFrom!({ cfg, accountId, allowFrom: values }), + resolveDmAllowFrom: (account) => account.config.allowFrom ?? account.config.dm?.allowFrom, + resolveGroupPolicy: (account) => account.config.groupPolicy, + resolveGroupOverrides: resolveDiscordAllowlistGroupOverrides, + }), + resolveNames: resolveDiscordAllowlistNames, + }, + groups: { + resolveRequireMention: resolveDiscordGroupRequireMention, + resolveToolPolicy: resolveDiscordGroupToolPolicy, + }, + mentions: { + stripPatterns: () => ["<@!?\\d+>"], + }, + agentPrompt: { + messageToolHints: () => [ + "- Discord components: set `components` when sending messages to include buttons, selects, or v2 containers.", + "- Forms: add `components.modal` (title, fields). OpenClaw adds a trigger button and routes submissions as new messages.", + ], + }, + messaging: { + normalizeTarget: normalizeDiscordMessagingTarget, + resolveSessionTarget: ({ id }) => normalizeDiscordMessagingTarget(`channel:${id}`), + parseExplicitTarget: ({ raw }) => parseDiscordExplicitTarget(raw), + inferTargetChatType: ({ to }) => parseDiscordExplicitTarget(to)?.chatType, + buildCrossContextComponents: buildDiscordCrossContextComponents, + resolveOutboundSessionRoute: (params) => resolveDiscordOutboundSessionRoute(params), + targetResolver: { + looksLikeId: looksLikeDiscordTargetId, + hint: "", + }, + }, + execApprovals: { + getInitiatingSurfaceState: ({ cfg, accountId }) => + isDiscordExecApprovalClientEnabled({ cfg, accountId }) + ? { kind: "enabled" } + : { kind: "disabled" }, + shouldSuppressLocalPrompt: ({ cfg, accountId, payload }) => + shouldSuppressLocalDiscordExecApprovalPrompt({ + cfg, + accountId, + payload, + }), + hasConfiguredDmRoute: ({ cfg }) => hasDiscordExecApprovalDmRoute(cfg), + shouldSuppressForwardingFallback: ({ cfg, target }) => + (normalizeMessageChannel(target.channel) ?? target.channel) === "discord" && + isDiscordExecApprovalClientEnabled({ cfg, accountId: target.accountId }), + }, + directory: createChannelDirectoryAdapter({ + listPeers: async (params) => listDiscordDirectoryPeersFromConfig(params), + listGroups: async (params) => listDiscordDirectoryGroupsFromConfig(params), + ...createRuntimeDirectoryLiveAdapter({ + getRuntime: () => getDiscordRuntime().channel.discord, + listPeersLive: (runtime) => runtime.listDirectoryPeersLive, + listGroupsLive: (runtime) => runtime.listDirectoryGroupsLive, }), - hasConfiguredDmRoute: ({ cfg }) => hasDiscordExecApprovalDmRoute(cfg), - shouldSuppressForwardingFallback: ({ cfg, target }) => - (normalizeMessageChannel(target.channel) ?? target.channel) === "discord" && - isDiscordExecApprovalClientEnabled({ cfg, accountId: target.accountId }), - }, - directory: createChannelDirectoryAdapter({ - listPeers: async (params) => listDiscordDirectoryPeersFromConfig(params), - listGroups: async (params) => listDiscordDirectoryGroupsFromConfig(params), - ...createRuntimeDirectoryLiveAdapter({ - getRuntime: () => getDiscordRuntime().channel.discord, - listPeersLive: (runtime) => runtime.listDirectoryPeersLive, - listGroupsLive: (runtime) => runtime.listDirectoryGroupsLive, }), - }), - resolver: { - resolveTargets: async ({ cfg, accountId, inputs, kind }) => { - const account = resolveDiscordAccount({ cfg, accountId }); - if (kind === "group") { + resolver: { + resolveTargets: async ({ cfg, accountId, inputs, kind }) => { + const account = resolveDiscordAccount({ cfg, accountId }); + if (kind === "group") { + return resolveTargetsWithOptionalToken({ + token: account.token, + inputs, + missingTokenNote: "missing Discord token", + resolveWithToken: ({ token, inputs }) => + getDiscordRuntime().channel.discord.resolveChannelAllowlist({ + token, + entries: inputs, + }), + mapResolved: (entry) => ({ + input: entry.input, + resolved: entry.resolved, + id: entry.channelId ?? entry.guildId, + name: + entry.channelName ?? + entry.guildName ?? + (entry.guildId && !entry.channelId ? entry.guildId : undefined), + note: entry.note, + }), + }); + } return resolveTargetsWithOptionalToken({ token: account.token, inputs, missingTokenNote: "missing Discord token", resolveWithToken: ({ token, inputs }) => - getDiscordRuntime().channel.discord.resolveChannelAllowlist({ + getDiscordRuntime().channel.discord.resolveUserAllowlist({ token, entries: inputs, }), mapResolved: (entry) => ({ input: entry.input, resolved: entry.resolved, - id: entry.channelId ?? entry.guildId, - name: - entry.channelName ?? - entry.guildName ?? - (entry.guildId && !entry.channelId ? entry.guildId : undefined), + id: entry.id, + name: entry.name, note: entry.note, }), }); - } - return resolveTargetsWithOptionalToken({ - token: account.token, - inputs, - missingTokenNote: "missing Discord token", - resolveWithToken: ({ token, inputs }) => - getDiscordRuntime().channel.discord.resolveUserAllowlist({ - token, - entries: inputs, - }), - mapResolved: (entry) => ({ - input: entry.input, - resolved: entry.resolved, - id: entry.id, - name: entry.name, - note: entry.note, + }, + }, + actions: discordMessageActions, + bindings: { + compileConfiguredBinding: ({ conversationId }) => + normalizeDiscordAcpConversationId(conversationId), + matchInboundConversation: ({ compiledBinding, conversationId, parentConversationId }) => + matchDiscordAcpConversation({ + bindingConversationId: compiledBinding.conversationId, + conversationId, + parentConversationId, }), - }); }, - }, - actions: discordMessageActions, - bindings: { - compileConfiguredBinding: ({ conversationId }) => - normalizeDiscordAcpConversationId(conversationId), - matchInboundConversation: ({ compiledBinding, conversationId, parentConversationId }) => - matchDiscordAcpConversation({ - bindingConversationId: compiledBinding.conversationId, - conversationId, - parentConversationId, + status: createComputedAccountStatusAdapter({ + defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID, { + connected: false, + reconnectAttempts: 0, + lastConnectedAt: null, + lastDisconnect: null, + lastEventAt: null, }), - }, - status: createComputedAccountStatusAdapter({ - defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID, { - connected: false, - reconnectAttempts: 0, - lastConnectedAt: null, - lastDisconnect: null, - lastEventAt: null, - }), - collectStatusIssues: collectDiscordStatusIssues, - buildChannelSummary: ({ snapshot }) => - buildTokenChannelStatusSummary(snapshot, { includeMode: false }), - probeAccount: async ({ account, timeoutMs }) => - probeDiscord(account.token, timeoutMs, { - includeApplication: true, - }), - formatCapabilitiesProbe: ({ probe }) => { - const discordProbe = probe as DiscordProbe | undefined; - const lines = []; - if (discordProbe?.bot?.username) { - const botId = discordProbe.bot.id ? ` (${discordProbe.bot.id})` : ""; - lines.push({ text: `Bot: @${discordProbe.bot.username}${botId}` }); - } - if (discordProbe?.application?.intents) { - lines.push({ - text: `Intents: ${formatDiscordIntents(discordProbe.application.intents)}`, - }); - } - return lines; - }, - buildCapabilitiesDiagnostics: async ({ account, timeoutMs, target }) => { - if (!target?.trim()) { - return undefined; - } - const parsedTarget = parseDiscordTarget(target.trim(), { defaultKind: "channel" }); - const details: Record = { - target: { - raw: target, - normalized: parsedTarget?.normalized, - kind: parsedTarget?.kind, - channelId: parsedTarget?.kind === "channel" ? parsedTarget.id : undefined, - }, - }; - if (!parsedTarget || parsedTarget.kind !== "channel") { - return { - details, - lines: [ - { - text: "Permissions: Target looks like a DM user; pass channel: to audit channel permissions.", - tone: "error", - }, - ], - }; - } - const token = account.token?.trim(); - if (!token) { - return { - details, - lines: [ - { - text: "Permissions: Discord bot token missing for permission audit.", - tone: "error", - }, - ], - }; - } - try { - const perms = await fetchChannelPermissionsDiscord(parsedTarget.id, { - token, - accountId: account.accountId ?? undefined, - }); - const missingRequired = REQUIRED_DISCORD_PERMISSIONS.filter( - (permission) => !perms.permissions.includes(permission), - ); - details.permissions = { - channelId: perms.channelId, - guildId: perms.guildId, - isDm: perms.isDm, - channelType: perms.channelType, - permissions: perms.permissions, - missingRequired, - raw: perms.raw, - }; - return { - details, - lines: [ - { - text: `Permissions (${perms.channelId}): ${perms.permissions.length ? perms.permissions.join(", ") : "none"}`, - }, - missingRequired.length > 0 - ? { text: `Missing required: ${missingRequired.join(", ")}`, tone: "warn" } - : { text: "Missing required: none", tone: "success" }, - ], - }; - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - details.permissions = { channelId: parsedTarget.id, error: message }; - return { - details, - lines: [{ text: `Permissions: ${message}`, tone: "error" }], - }; - } - }, - auditAccount: async ({ account, timeoutMs, cfg }) => { - const { channelIds, unresolvedChannels } = collectDiscordAuditChannelIds({ - cfg, - accountId: account.accountId, - }); - if (!channelIds.length && unresolvedChannels === 0) { - return undefined; - } - const botToken = account.token?.trim(); - if (!botToken) { - return { - ok: unresolvedChannels === 0, - checkedChannels: 0, - unresolvedChannels, - channels: [], - elapsedMs: 0, - }; - } - const audit = await auditDiscordChannelPermissions({ - token: botToken, - accountId: account.accountId, - channelIds, - timeoutMs, - }); - return { ...audit, unresolvedChannels }; - }, - resolveAccountSnapshot: ({ account, runtime, probe, audit }) => { - const configured = - resolveConfiguredFromCredentialStatuses(account) ?? Boolean(account.token?.trim()); - const app = runtime?.application ?? (probe as { application?: unknown })?.application; - const bot = runtime?.bot ?? (probe as { bot?: unknown })?.bot; - return { - accountId: account.accountId, - name: account.name, - enabled: account.enabled, - configured, - extra: { - ...projectCredentialSnapshotFields(account), - connected: runtime?.connected ?? false, - reconnectAttempts: runtime?.reconnectAttempts, - lastConnectedAt: runtime?.lastConnectedAt ?? null, - lastDisconnect: runtime?.lastDisconnect ?? null, - lastEventAt: runtime?.lastEventAt ?? null, - application: app ?? undefined, - bot: bot ?? undefined, - audit, - }, - }; - }, - }), - gateway: { - startAccount: async (ctx) => { - const account = ctx.account; - const token = account.token.trim(); - let discordBotLabel = ""; - try { - const probe = await probeDiscord(token, 2500, { + collectStatusIssues: collectDiscordStatusIssues, + buildChannelSummary: ({ snapshot }) => + buildTokenChannelStatusSummary(snapshot, { includeMode: false }), + probeAccount: async ({ account, timeoutMs }) => + probeDiscord(account.token, timeoutMs, { includeApplication: true, - }); - const username = probe.ok ? probe.bot?.username?.trim() : null; - if (username) { - discordBotLabel = ` (@${username})`; + }), + formatCapabilitiesProbe: ({ probe }) => { + const discordProbe = probe as DiscordProbe | undefined; + const lines = []; + if (discordProbe?.bot?.username) { + const botId = discordProbe.bot.id ? ` (${discordProbe.bot.id})` : ""; + lines.push({ text: `Bot: @${discordProbe.bot.username}${botId}` }); } - ctx.setStatus({ + if (discordProbe?.application?.intents) { + lines.push({ + text: `Intents: ${formatDiscordIntents(discordProbe.application.intents)}`, + }); + } + return lines; + }, + buildCapabilitiesDiagnostics: async ({ account, timeoutMs, target }) => { + if (!target?.trim()) { + return undefined; + } + const parsedTarget = parseDiscordTarget(target.trim(), { defaultKind: "channel" }); + const details: Record = { + target: { + raw: target, + normalized: parsedTarget?.normalized, + kind: parsedTarget?.kind, + channelId: parsedTarget?.kind === "channel" ? parsedTarget.id : undefined, + }, + }; + if (!parsedTarget || parsedTarget.kind !== "channel") { + return { + details, + lines: [ + { + text: "Permissions: Target looks like a DM user; pass channel: to audit channel permissions.", + tone: "error", + }, + ], + }; + } + const token = account.token?.trim(); + if (!token) { + return { + details, + lines: [ + { + text: "Permissions: Discord bot token missing for permission audit.", + tone: "error", + }, + ], + }; + } + try { + const perms = await fetchChannelPermissionsDiscord(parsedTarget.id, { + token, + accountId: account.accountId ?? undefined, + }); + const missingRequired = REQUIRED_DISCORD_PERMISSIONS.filter( + (permission) => !perms.permissions.includes(permission), + ); + details.permissions = { + channelId: perms.channelId, + guildId: perms.guildId, + isDm: perms.isDm, + channelType: perms.channelType, + permissions: perms.permissions, + missingRequired, + raw: perms.raw, + }; + return { + details, + lines: [ + { + text: `Permissions (${perms.channelId}): ${perms.permissions.length ? perms.permissions.join(", ") : "none"}`, + }, + missingRequired.length > 0 + ? { text: `Missing required: ${missingRequired.join(", ")}`, tone: "warn" } + : { text: "Missing required: none", tone: "success" }, + ], + }; + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + details.permissions = { channelId: parsedTarget.id, error: message }; + return { + details, + lines: [{ text: `Permissions: ${message}`, tone: "error" }], + }; + } + }, + auditAccount: async ({ account, timeoutMs, cfg }) => { + const { channelIds, unresolvedChannels } = collectDiscordAuditChannelIds({ + cfg, accountId: account.accountId, - bot: probe.bot, - application: probe.application, }); - const messageContent = probe.application?.intents?.messageContent; - if (messageContent === "disabled") { - ctx.log?.warn( - `[${account.accountId}] Discord Message Content Intent is disabled; bot may not respond to channel messages. Enable it in Discord Dev Portal (Bot → Privileged Gateway Intents) or require mentions.`, - ); - } else if (messageContent === "limited") { - ctx.log?.info( - `[${account.accountId}] Discord Message Content Intent is limited; bots under 100 servers can use it without verification.`, - ); + if (!channelIds.length && unresolvedChannels === 0) { + return undefined; } - } catch (err) { - if (getDiscordRuntime().logging.shouldLogVerbose()) { - ctx.log?.debug?.(`[${account.accountId}] bot probe failed: ${String(err)}`); + const botToken = account.token?.trim(); + if (!botToken) { + return { + ok: unresolvedChannels === 0, + checkedChannels: 0, + unresolvedChannels, + channels: [], + elapsedMs: 0, + }; } - } - ctx.log?.info(`[${account.accountId}] starting provider${discordBotLabel}`); - return (await loadDiscordProviderRuntime()).monitorDiscordProvider({ - token, - accountId: account.accountId, - config: ctx.cfg, - runtime: ctx.runtime, - abortSignal: ctx.abortSignal, - mediaMaxMb: account.config.mediaMaxMb, - historyLimit: account.config.historyLimit, - setStatus: (patch) => ctx.setStatus({ accountId: account.accountId, ...patch }), - }); + const audit = await auditDiscordChannelPermissions({ + token: botToken, + accountId: account.accountId, + channelIds, + timeoutMs, + }); + return { ...audit, unresolvedChannels }; + }, + resolveAccountSnapshot: ({ account, runtime, probe, audit }) => { + const configured = + resolveConfiguredFromCredentialStatuses(account) ?? Boolean(account.token?.trim()); + const app = runtime?.application ?? (probe as { application?: unknown })?.application; + const bot = runtime?.bot ?? (probe as { bot?: unknown })?.bot; + return { + accountId: account.accountId, + name: account.name, + enabled: account.enabled, + configured, + extra: { + ...projectCredentialSnapshotFields(account), + connected: runtime?.connected ?? false, + reconnectAttempts: runtime?.reconnectAttempts, + lastConnectedAt: runtime?.lastConnectedAt ?? null, + lastDisconnect: runtime?.lastDisconnect ?? null, + lastEventAt: runtime?.lastEventAt ?? null, + application: app ?? undefined, + bot: bot ?? undefined, + audit, + }, + }; + }, + }), + gateway: { + startAccount: async (ctx) => { + const account = ctx.account; + const token = account.token.trim(); + let discordBotLabel = ""; + try { + const probe = await probeDiscord(token, 2500, { + includeApplication: true, + }); + const username = probe.ok ? probe.bot?.username?.trim() : null; + if (username) { + discordBotLabel = ` (@${username})`; + } + ctx.setStatus({ + accountId: account.accountId, + bot: probe.bot, + application: probe.application, + }); + const messageContent = probe.application?.intents?.messageContent; + if (messageContent === "disabled") { + ctx.log?.warn( + `[${account.accountId}] Discord Message Content Intent is disabled; bot may not respond to channel messages. Enable it in Discord Dev Portal (Bot → Privileged Gateway Intents) or require mentions.`, + ); + } else if (messageContent === "limited") { + ctx.log?.info( + `[${account.accountId}] Discord Message Content Intent is limited; bots under 100 servers can use it without verification.`, + ); + } + } catch (err) { + if (getDiscordRuntime().logging.shouldLogVerbose()) { + ctx.log?.debug?.(`[${account.accountId}] bot probe failed: ${String(err)}`); + } + } + ctx.log?.info(`[${account.accountId}] starting provider${discordBotLabel}`); + return (await loadDiscordProviderRuntime()).monitorDiscordProvider({ + token, + accountId: account.accountId, + config: ctx.cfg, + runtime: ctx.runtime, + abortSignal: ctx.abortSignal, + mediaMaxMb: account.config.mediaMaxMb, + historyLimit: account.config.historyLimit, + setStatus: (patch) => ctx.setStatus({ accountId: account.accountId, ...patch }), + }); + }, }, }, - }, - pairing: { - text: { - idLabel: "discordUserId", - message: PAIRING_APPROVED_MESSAGE, - normalizeAllowEntry: createPairingPrefixStripper(/^(discord|user):/i), - notify: async ({ id, message }) => { - await getDiscordRuntime().channel.discord.sendMessageDiscord(`user:${id}`, message); + pairing: { + text: { + idLabel: "discordUserId", + message: PAIRING_APPROVED_MESSAGE, + normalizeAllowEntry: createPairingPrefixStripper(/^(discord|user):/i), + notify: async ({ id, message }) => { + await getDiscordRuntime().channel.discord.sendMessageDiscord(`user:${id}`, message); + }, }, }, - }, - security: { - resolveDmPolicy: resolveDiscordDmPolicy, - collectWarnings: collectDiscordSecurityWarnings, - }, - threading: { - topLevelReplyToMode: "discord", - }, - outbound: { - base: { - deliveryMode: "direct", - chunker: null, - textChunkLimit: 2000, - pollMaxOptions: 10, - resolveTarget: ({ to }) => normalizeDiscordOutboundTarget(to), + security: { + resolveDmPolicy: resolveDiscordDmPolicy, + collectWarnings: collectDiscordSecurityWarnings, }, - attachedResults: { - channel: "discord", - sendText: async ({ cfg, to, text, accountId, deps, replyToId, silent }) => { - const send = - resolveOutboundSendDep(deps, "discord") ?? - getDiscordRuntime().channel.discord.sendMessageDiscord; - return await send(to, text, { - verbose: false, - cfg, - replyTo: replyToId ?? undefined, - accountId: accountId ?? undefined, - silent: silent ?? undefined, - }); - }, - sendMedia: async ({ - cfg, - to, - text, - mediaUrl, - mediaLocalRoots, - accountId, - deps, - replyToId, - silent, - }) => { - const send = - resolveOutboundSendDep(deps, "discord") ?? - getDiscordRuntime().channel.discord.sendMessageDiscord; - return await send(to, text, { - verbose: false, + threading: { + topLevelReplyToMode: "discord", + }, + outbound: { + base: { + deliveryMode: "direct", + chunker: null, + textChunkLimit: 2000, + pollMaxOptions: 10, + resolveTarget: ({ to }) => normalizeDiscordOutboundTarget(to), + }, + attachedResults: { + channel: "discord", + sendText: async ({ cfg, to, text, accountId, deps, replyToId, silent }) => { + const send = + resolveOutboundSendDep(deps, "discord") ?? + getDiscordRuntime().channel.discord.sendMessageDiscord; + return await send(to, text, { + verbose: false, + cfg, + replyTo: replyToId ?? undefined, + accountId: accountId ?? undefined, + silent: silent ?? undefined, + }); + }, + sendMedia: async ({ cfg, + to, + text, mediaUrl, mediaLocalRoots, - replyTo: replyToId ?? undefined, - accountId: accountId ?? undefined, - silent: silent ?? undefined, - }); + accountId, + deps, + replyToId, + silent, + }) => { + const send = + resolveOutboundSendDep(deps, "discord") ?? + getDiscordRuntime().channel.discord.sendMessageDiscord; + return await send(to, text, { + verbose: false, + cfg, + mediaUrl, + mediaLocalRoots, + replyTo: replyToId ?? undefined, + accountId: accountId ?? undefined, + silent: silent ?? undefined, + }); + }, + sendPoll: async ({ cfg, to, poll, accountId, silent }) => + await getDiscordRuntime().channel.discord.sendPollDiscord(to, poll, { + cfg, + accountId: accountId ?? undefined, + silent: silent ?? undefined, + }), }, - sendPoll: async ({ cfg, to, poll, accountId, silent }) => - await getDiscordRuntime().channel.discord.sendPollDiscord(to, poll, { - cfg, - accountId: accountId ?? undefined, - silent: silent ?? undefined, - }), }, - }, -}); + }); diff --git a/extensions/imessage/src/channel.ts b/extensions/imessage/src/channel.ts index b6efbf38e12..1847324b004 100644 --- a/extensions/imessage/src/channel.ts +++ b/extensions/imessage/src/channel.ts @@ -4,9 +4,11 @@ import { buildPassiveProbedChannelStatusSummary } from "openclaw/plugin-sdk/exte import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime"; import { resolveOutboundSendDep } from "openclaw/plugin-sdk/outbound-runtime"; import { buildOutboundBaseSessionKey, type RoutePeer } from "openclaw/plugin-sdk/routing"; -import { createDefaultChannelRuntimeState } from "openclaw/plugin-sdk/status-helpers"; import { - buildComputedAccountStatusSnapshot, + createComputedAccountStatusAdapter, + createDefaultChannelRuntimeState, +} from "openclaw/plugin-sdk/status-helpers"; +import { collectStatusIssuesFromLastError, DEFAULT_ACCOUNT_ID, formatTrimmedAllowFromEntries, @@ -107,7 +109,7 @@ function resolveIMessageOutboundSessionRoute(params: { } export const imessagePlugin: ChannelPlugin = - createChatChannelPlugin({ + createChatChannelPlugin({ base: { ...createIMessagePluginBase({ setupWizard: imessageSetupWizard, @@ -150,7 +152,7 @@ export const imessagePlugin: ChannelPlugin({ defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID, { cliPath: null, dbPath: null, @@ -163,23 +165,18 @@ export const imessagePlugin: ChannelPlugin await (await loadIMessageChannelRuntime()).probeIMessageAccount(timeoutMs), - buildAccountSnapshot: ({ account, runtime, probe }) => - buildComputedAccountStatusSnapshot( - { - accountId: account.accountId, - name: account.name, - enabled: account.enabled, - configured: account.configured, - runtime, - probe, - }, - { - cliPath: runtime?.cliPath ?? account.config.cliPath ?? null, - dbPath: runtime?.dbPath ?? account.config.dbPath ?? null, - }, - ), + resolveAccountSnapshot: ({ account, runtime }) => ({ + accountId: account.accountId, + name: account.name, + enabled: account.enabled, + configured: account.configured, + extra: { + cliPath: runtime?.cliPath ?? account.config.cliPath ?? null, + dbPath: runtime?.dbPath ?? account.config.dbPath ?? null, + }, + }), resolveAccountState: ({ enabled }) => (enabled ? "enabled" : "disabled"), - }, + }), gateway: { startAccount: async (ctx) => await (await loadIMessageChannelRuntime()).startIMessageGatewayAccount(ctx), diff --git a/extensions/matrix/src/channel.ts b/extensions/matrix/src/channel.ts index 8784cf1873b..21ba82f976f 100644 --- a/extensions/matrix/src/channel.ts +++ b/extensions/matrix/src/channel.ts @@ -22,7 +22,10 @@ import { import { buildTrafficStatusSummary } from "openclaw/plugin-sdk/extension-shared"; import { createLazyRuntimeNamedExport } from "openclaw/plugin-sdk/lazy-runtime"; import { createRuntimeOutboundDelegates } from "openclaw/plugin-sdk/outbound-runtime"; -import { createDefaultChannelRuntimeState } from "openclaw/plugin-sdk/status-helpers"; +import { + createComputedAccountStatusAdapter, + createDefaultChannelRuntimeState, +} from "openclaw/plugin-sdk/status-helpers"; import { matrixMessageActions } from "./actions.js"; import { MatrixConfigSchema } from "./config-schema.js"; import { @@ -44,7 +47,6 @@ import { resolveMatrixTargetIdentity, } from "./matrix/target-ids.js"; import { - buildComputedAccountStatusSnapshot, buildChannelConfigSchema, buildProbeChannelStatusSummary, collectStatusIssuesFromLastError, @@ -196,7 +198,7 @@ function matchMatrixAcpConversation(params: { } export const matrixPlugin: ChannelPlugin = - createChatChannelPlugin({ + createChatChannelPlugin({ base: { id: "matrix", meta, @@ -319,7 +321,7 @@ export const matrixPlugin: ChannelPlugin = parentConversationId, }), }, - status: { + status: createComputedAccountStatusAdapter({ defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID), collectStatusIssues: (accounts) => collectStatusIssuesFromLastError("matrix", accounts), buildChannelSummary: ({ snapshot }) => @@ -348,23 +350,18 @@ export const matrixPlugin: ChannelPlugin = }; } }, - buildAccountSnapshot: ({ account, runtime, probe }) => - buildComputedAccountStatusSnapshot( - { - accountId: account.accountId, - name: account.name, - enabled: account.enabled, - configured: account.configured, - runtime, - probe, - }, - { - baseUrl: account.homeserver, - lastProbeAt: runtime?.lastProbeAt ?? null, - ...buildTrafficStatusSummary(runtime), - }, - ), - }, + resolveAccountSnapshot: ({ account, runtime }) => ({ + accountId: account.accountId, + name: account.name, + enabled: account.enabled, + configured: account.configured, + extra: { + baseUrl: account.homeserver, + lastProbeAt: runtime?.lastProbeAt ?? null, + ...buildTrafficStatusSummary(runtime), + }, + }), + }), gateway: { startAccount: async (ctx) => { const account = ctx.account; diff --git a/extensions/nextcloud-talk/src/channel.ts b/extensions/nextcloud-talk/src/channel.ts index 1df568e4842..03943921a63 100644 --- a/extensions/nextcloud-talk/src/channel.ts +++ b/extensions/nextcloud-talk/src/channel.ts @@ -13,11 +13,13 @@ import { import { createAllowlistProviderRouteAllowlistWarningCollector } from "openclaw/plugin-sdk/channel-policy"; import { createChatChannelPlugin } from "openclaw/plugin-sdk/core"; import { runStoppablePassiveMonitor } from "openclaw/plugin-sdk/extension-shared"; -import { createDefaultChannelRuntimeState } from "openclaw/plugin-sdk/status-helpers"; +import { + createComputedAccountStatusAdapter, + createDefaultChannelRuntimeState, +} from "openclaw/plugin-sdk/status-helpers"; import { buildBaseChannelStatusSummary, buildChannelConfigSchema, - buildRuntimeAccountStatusSnapshot, clearAccountEntryFields, DEFAULT_ACCOUNT_ID, type ChannelPlugin, @@ -168,31 +170,25 @@ export const nextcloudTalkPlugin: ChannelPlugin = }, }, setup: nextcloudTalkSetupAdapter, - status: { + status: createComputedAccountStatusAdapter({ defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID), buildChannelSummary: ({ snapshot }) => buildBaseChannelStatusSummary(snapshot, { secretSource: snapshot.secretSource ?? "none", mode: "webhook", }), - buildAccountSnapshot: ({ account, runtime }) => { - const configured = Boolean(account.secret?.trim() && account.baseUrl?.trim()); - return buildRuntimeAccountStatusSnapshot( - { runtime }, - { - accountId: account.accountId, - name: account.name, - enabled: account.enabled, - configured, - secretSource: account.secretSource, - baseUrl: account.baseUrl ? "[set]" : "[missing]", - mode: "webhook", - lastInboundAt: runtime?.lastInboundAt ?? null, - lastOutboundAt: runtime?.lastOutboundAt ?? null, - }, - ); - }, - }, + resolveAccountSnapshot: ({ account }) => ({ + accountId: account.accountId, + name: account.name, + enabled: account.enabled, + configured: Boolean(account.secret?.trim() && account.baseUrl?.trim()), + extra: { + secretSource: account.secretSource, + baseUrl: account.baseUrl ? "[set]" : "[missing]", + mode: "webhook", + }, + }), + }), gateway: { startAccount: async (ctx) => { const account = ctx.account; diff --git a/extensions/nostr/src/channel.ts b/extensions/nostr/src/channel.ts index 33479d9cbbb..092a2649331 100644 --- a/extensions/nostr/src/channel.ts +++ b/extensions/nostr/src/channel.ts @@ -10,8 +10,8 @@ import { buildPassiveChannelStatusSummary, buildTrafficStatusSummary, } from "openclaw/plugin-sdk/extension-shared"; +import { createComputedAccountStatusAdapter } from "openclaw/plugin-sdk/status-helpers"; import { - buildComputedAccountStatusSnapshot, buildChannelConfigSchema, collectStatusIssuesFromLastError, createPreCryptoDirectDmAuthorizer, @@ -196,27 +196,25 @@ export const nostrPlugin: ChannelPlugin = createChatChanne resolveOutboundSessionRoute: (params) => resolveNostrOutboundSessionRoute(params), }, status: { - defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID), - collectStatusIssues: (accounts) => collectStatusIssuesFromLastError("nostr", accounts), - buildChannelSummary: ({ snapshot }) => - buildPassiveChannelStatusSummary(snapshot, { - publicKey: snapshot.publicKey ?? null, - }), - buildAccountSnapshot: ({ account, runtime }) => - buildComputedAccountStatusSnapshot( - { - accountId: account.accountId, - name: account.name, - enabled: account.enabled, - configured: account.configured, - runtime, - }, - { + ...createComputedAccountStatusAdapter({ + defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID), + collectStatusIssues: (accounts) => collectStatusIssuesFromLastError("nostr", accounts), + buildChannelSummary: ({ snapshot }) => + buildPassiveChannelStatusSummary(snapshot, { + publicKey: snapshot.publicKey ?? null, + }), + resolveAccountSnapshot: ({ account, runtime }) => ({ + accountId: account.accountId, + name: account.name, + enabled: account.enabled, + configured: account.configured, + extra: { publicKey: account.publicKey, profile: account.profile, ...buildTrafficStatusSummary(runtime), }, - ), + }), + }), }, gateway: { startAccount: async (ctx) => { diff --git a/extensions/signal/src/channel.ts b/extensions/signal/src/channel.ts index 35c5f209b1d..b29227be793 100644 --- a/extensions/signal/src/channel.ts +++ b/extensions/signal/src/channel.ts @@ -9,6 +9,7 @@ import { createChatChannelPlugin } from "openclaw/plugin-sdk/core"; import { resolveOutboundSendDep } from "openclaw/plugin-sdk/outbound-runtime"; import { resolveTextChunkLimit } from "openclaw/plugin-sdk/reply-runtime"; import { buildOutboundBaseSessionKey, type RoutePeer } from "openclaw/plugin-sdk/routing"; +import { createComputedAccountStatusAdapter } from "openclaw/plugin-sdk/status-helpers"; import { resolveSignalAccount, type ResolvedSignalAccount } from "./accounts.js"; import { markdownToSignalTextChunks } from "./format.js"; import { @@ -20,7 +21,6 @@ import { import { signalMessageActions } from "./message-actions.js"; import type { SignalProbe } from "./probe.js"; import { - buildBaseAccountStatusSnapshot, buildBaseChannelStatusSummary, collectStatusIssuesFromLastError, createDefaultChannelRuntimeState, @@ -295,7 +295,7 @@ export const signalPlugin: ChannelPlugin = hint: "", }, }, - status: { + status: createComputedAccountStatusAdapter({ defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID), collectStatusIssues: (accounts) => collectStatusIssuesFromLastError("signal", accounts), buildChannelSummary: ({ snapshot }) => @@ -312,9 +312,16 @@ export const signalPlugin: ChannelPlugin = (probe as SignalProbe | undefined)?.version ? [{ text: `Signal daemon: ${(probe as SignalProbe).version}` }] : [], - buildAccountSnapshot: ({ account, runtime, probe }) => - buildBaseAccountStatusSnapshot({ account, runtime, probe }, { baseUrl: account.baseUrl }), - }, + resolveAccountSnapshot: ({ account }) => ({ + accountId: account.accountId, + name: account.name, + enabled: account.enabled, + configured: account.configured, + extra: { + baseUrl: account.baseUrl, + }, + }), + }), gateway: { startAccount: async (ctx) => { const account = ctx.account; diff --git a/extensions/slack/src/channel.ts b/extensions/slack/src/channel.ts index 3c07596ef03..c166bd48a1f 100644 --- a/extensions/slack/src/channel.ts +++ b/extensions/slack/src/channel.ts @@ -343,7 +343,10 @@ const collectSlackSecurityWarnings = }, }); -export const slackPlugin: ChannelPlugin = createChatChannelPlugin({ +export const slackPlugin: ChannelPlugin = createChatChannelPlugin< + ResolvedSlackAccount, + SlackProbe +>({ base: { ...createSlackPluginBase({ setupWizard: slackSetupWizard, diff --git a/extensions/tlon/src/channel.ts b/extensions/tlon/src/channel.ts index 6eef5d21962..aca0c8c1b97 100644 --- a/extensions/tlon/src/channel.ts +++ b/extensions/tlon/src/channel.ts @@ -5,8 +5,10 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; import { createChatChannelPlugin, type ChannelPlugin } from "openclaw/plugin-sdk/core"; import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime"; import { createRuntimeOutboundDelegates } from "openclaw/plugin-sdk/outbound-runtime"; -import { createDefaultChannelRuntimeState } from "openclaw/plugin-sdk/status-helpers"; -import { buildComputedAccountStatusSnapshot } from "../api.js"; +import { + createComputedAccountStatusAdapter, + createDefaultChannelRuntimeState, +} from "openclaw/plugin-sdk/status-helpers"; import { tlonChannelConfigSchema } from "./config-schema.js"; import { resolveTlonOutboundSessionRoute } from "./session-route.js"; import { @@ -109,7 +111,7 @@ export const tlonPlugin = createChatChannelPlugin({ }, resolveOutboundSessionRoute: (params) => resolveTlonOutboundSessionRoute(params), }, - status: { + status: createComputedAccountStatusAdapter>({ defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID), collectStatusIssues: (accounts) => { return accounts.flatMap((account) => { @@ -140,22 +142,17 @@ export const tlonPlugin = createChatChannelPlugin({ } return await (await loadTlonChannelRuntime()).probeTlonAccount(account as never); }, - buildAccountSnapshot: ({ account, runtime, probe }) => - buildComputedAccountStatusSnapshot( - { - accountId: account.accountId, - name: account.name ?? undefined, - enabled: account.enabled, - configured: account.configured, - runtime, - probe, - }, - { - ship: account.ship, - url: account.url, - }, - ), - }, + resolveAccountSnapshot: ({ account }) => ({ + accountId: account.accountId, + name: account.name ?? undefined, + enabled: account.enabled, + configured: account.configured, + extra: { + ship: account.ship, + url: account.url, + }, + }), + }), gateway: { startAccount: async (ctx) => await (await loadTlonChannelRuntime()).startTlonGatewayAccount(ctx), diff --git a/src/plugin-sdk/status-helpers.ts b/src/plugin-sdk/status-helpers.ts index 2ad691baf65..b06a33470a6 100644 --- a/src/plugin-sdk/status-helpers.ts +++ b/src/plugin-sdk/status-helpers.ts @@ -166,16 +166,14 @@ export function createComputedAccountStatusAdapter< params: ComputedAccountStatusAdapterParams, ) => ComputedAccountStatusBase & { extra?: TExtra }; }, -): ChannelStatusAdapter { +): ChannelStatusAdapter { return { defaultRuntime: options.defaultRuntime, buildChannelSummary: options.buildChannelSummary, probeAccount: options.probeAccount, - formatCapabilitiesProbe: - options.formatCapabilitiesProbe as ChannelStatusAdapter["formatCapabilitiesProbe"], - auditAccount: options.auditAccount as ChannelStatusAdapter["auditAccount"], - buildCapabilitiesDiagnostics: - options.buildCapabilitiesDiagnostics as ChannelStatusAdapter["buildCapabilitiesDiagnostics"], + formatCapabilitiesProbe: options.formatCapabilitiesProbe, + auditAccount: options.auditAccount, + buildCapabilitiesDiagnostics: options.buildCapabilitiesDiagnostics, logSelfId: options.logSelfId, resolveAccountState: options.resolveAccountState, collectStatusIssues: options.collectStatusIssues,