refactor: share computed channel status adapters

This commit is contained in:
Peter Steinberger 2026-03-22 23:19:20 +00:00
parent 4cc0d05cfb
commit 72bfaf6ee2
10 changed files with 465 additions and 472 deletions

View File

@ -104,8 +104,8 @@ const meta = {
preferOver: ["imessage"],
};
export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount> = createChatChannelPlugin(
{
export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount, BlueBubblesProbe> =
createChatChannelPlugin<ResolvedBlueBubblesAccount, BlueBubblesProbe>({
base: {
id: "bluebubbles",
meta,
@ -365,5 +365,4 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount> = crea
},
},
},
},
);
});

View File

@ -309,396 +309,397 @@ function resolveDiscordOutboundSessionRoute(params: {
};
}
export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = 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<ResolvedDiscordAccount, DiscordProbe> =
createChatChannelPlugin<ResolvedDiscordAccount, DiscordProbe>({
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: "<channelId|user:ID|channel:ID>",
},
},
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: "<channelId|user:ID|channel:ID>",
},
},
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<ResolvedDiscordAccount, DiscordProbe, unknown>({
defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID, {
connected: false,
reconnectAttempts: 0,
lastConnectedAt: null,
lastDisconnect: null,
lastEventAt: null,
}),
},
status: createComputedAccountStatusAdapter<ResolvedDiscordAccount, DiscordProbe, unknown>({
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<string, unknown> = {
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:<id> 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<string, unknown> = {
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:<id> 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<DiscordSendFn>(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<DiscordSendFn>(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<DiscordSendFn>(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<DiscordSendFn>(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,
}),
},
},
});
});

View File

@ -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<ResolvedIMessageAccount, IMessageProbe> =
createChatChannelPlugin({
createChatChannelPlugin<ResolvedIMessageAccount, IMessageProbe>({
base: {
...createIMessagePluginBase({
setupWizard: imessageSetupWizard,
@ -150,7 +152,7 @@ export const imessagePlugin: ChannelPlugin<ResolvedIMessageAccount, IMessageProb
},
},
},
status: {
status: createComputedAccountStatusAdapter<ResolvedIMessageAccount, IMessageProbe>({
defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID, {
cliPath: null,
dbPath: null,
@ -163,23 +165,18 @@ export const imessagePlugin: ChannelPlugin<ResolvedIMessageAccount, IMessageProb
}),
probeAccount: async ({ timeoutMs }) =>
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),

View File

@ -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<ResolvedMatrixAccount, MatrixProbe> =
createChatChannelPlugin({
createChatChannelPlugin<ResolvedMatrixAccount, MatrixProbe>({
base: {
id: "matrix",
meta,
@ -319,7 +321,7 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount, MatrixProbe> =
parentConversationId,
}),
},
status: {
status: createComputedAccountStatusAdapter<ResolvedMatrixAccount, MatrixProbe>({
defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID),
collectStatusIssues: (accounts) => collectStatusIssuesFromLastError("matrix", accounts),
buildChannelSummary: ({ snapshot }) =>
@ -348,23 +350,18 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount, MatrixProbe> =
};
}
},
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;

View File

@ -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<ResolvedNextcloudTalkAccount> =
},
},
setup: nextcloudTalkSetupAdapter,
status: {
status: createComputedAccountStatusAdapter<ResolvedNextcloudTalkAccount>({
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;

View File

@ -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<ResolvedNostrAccount> = 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<ResolvedNostrAccount>({
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) => {

View File

@ -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<ResolvedSignalAccount, SignalProbe> =
hint: "<E.164|uuid:ID|group:ID|signal:group:ID|signal:+E.164>",
},
},
status: {
status: createComputedAccountStatusAdapter<ResolvedSignalAccount, SignalProbe>({
defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID),
collectStatusIssues: (accounts) => collectStatusIssuesFromLastError("signal", accounts),
buildChannelSummary: ({ snapshot }) =>
@ -312,9 +312,16 @@ export const signalPlugin: ChannelPlugin<ResolvedSignalAccount, SignalProbe> =
(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;

View File

@ -343,7 +343,10 @@ const collectSlackSecurityWarnings =
},
});
export const slackPlugin: ChannelPlugin<ResolvedSlackAccount> = createChatChannelPlugin({
export const slackPlugin: ChannelPlugin<ResolvedSlackAccount, SlackProbe> = createChatChannelPlugin<
ResolvedSlackAccount,
SlackProbe
>({
base: {
...createSlackPluginBase({
setupWizard: slackSetupWizard,

View File

@ -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<ReturnType<typeof resolveTlonAccount>>({
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),

View File

@ -166,16 +166,14 @@ export function createComputedAccountStatusAdapter<
params: ComputedAccountStatusAdapterParams<ResolvedAccount, Probe, Audit>,
) => ComputedAccountStatusBase & { extra?: TExtra };
},
): ChannelStatusAdapter<ResolvedAccount> {
): ChannelStatusAdapter<ResolvedAccount, Probe, Audit> {
return {
defaultRuntime: options.defaultRuntime,
buildChannelSummary: options.buildChannelSummary,
probeAccount: options.probeAccount,
formatCapabilitiesProbe:
options.formatCapabilitiesProbe as ChannelStatusAdapter<ResolvedAccount>["formatCapabilitiesProbe"],
auditAccount: options.auditAccount as ChannelStatusAdapter<ResolvedAccount>["auditAccount"],
buildCapabilitiesDiagnostics:
options.buildCapabilitiesDiagnostics as ChannelStatusAdapter<ResolvedAccount>["buildCapabilitiesDiagnostics"],
formatCapabilitiesProbe: options.formatCapabilitiesProbe,
auditAccount: options.auditAccount,
buildCapabilitiesDiagnostics: options.buildCapabilitiesDiagnostics,
logSelfId: options.logSelfId,
resolveAccountState: options.resolveAccountState,
collectStatusIssues: options.collectStatusIssues,