diff --git a/extensions/bluebubbles/src/channel.ts b/extensions/bluebubbles/src/channel.ts index cdc3a5bc567..33249fcfa9e 100644 --- a/extensions/bluebubbles/src/channel.ts +++ b/extensions/bluebubbles/src/channel.ts @@ -1,7 +1,6 @@ import { formatNormalizedAllowFromEntries } from "openclaw/plugin-sdk/allow-from"; import { - createScopedAccountConfigAccessors, - createScopedChannelConfigBase, + createScopedChannelConfigAdapter, createScopedDmSecurityResolver, } from "openclaw/plugin-sdk/channel-config-helpers"; import { createAccountStatusSink } from "openclaw/plugin-sdk/channel-lifecycle"; @@ -47,8 +46,12 @@ const loadBlueBubblesChannelRuntime = createLazyRuntimeNamedExport( "blueBubblesChannelRuntime", ); -const bluebubblesConfigAccessors = createScopedAccountConfigAccessors({ - resolveAccount: ({ cfg, accountId }) => resolveBlueBubblesAccount({ cfg, accountId }), +const bluebubblesConfigAdapter = createScopedChannelConfigAdapter({ + sectionKey: "bluebubbles", + listAccountIds: listBlueBubblesAccountIds, + resolveAccount: (cfg, accountId) => resolveBlueBubblesAccount({ cfg, accountId }), + defaultAccountId: resolveDefaultBlueBubblesAccountId, + clearBaseFields: ["serverUrl", "password", "name", "webhookPath"], resolveAllowFrom: (account: ResolvedBlueBubblesAccount) => account.config.allowFrom, formatAllowFrom: (allowFrom) => formatNormalizedAllowFromEntries({ @@ -57,14 +60,6 @@ const bluebubblesConfigAccessors = createScopedAccountConfigAccessors({ }), }); -const bluebubblesConfigBase = createScopedChannelConfigBase({ - sectionKey: "bluebubbles", - listAccountIds: listBlueBubblesAccountIds, - resolveAccount: (cfg, accountId) => resolveBlueBubblesAccount({ cfg, accountId }), - defaultAccountId: resolveDefaultBlueBubblesAccountId, - clearBaseFields: ["serverUrl", "password", "name", "webhookPath"], -}); - const resolveBlueBubblesDmPolicy = createScopedDmSecurityResolver({ channelKey: "bluebubbles", resolvePolicy: (account) => account.config.dmPolicy, @@ -115,7 +110,7 @@ export const bluebubblesPlugin: ChannelPlugin = { configSchema: buildChannelConfigSchema(BlueBubblesConfigSchema), setupWizard: blueBubblesSetupWizard, config: { - ...bluebubblesConfigBase, + ...bluebubblesConfigAdapter, isConfigured: (account) => account.configured, describeAccount: (account): ChannelAccountSnapshot => ({ accountId: account.accountId, @@ -124,7 +119,6 @@ export const bluebubblesPlugin: ChannelPlugin = { configured: account.configured, baseUrl: account.baseUrl, }), - ...bluebubblesConfigAccessors, }, actions: bluebubblesMessageActions, security: { diff --git a/extensions/discord/src/channel.ts b/extensions/discord/src/channel.ts index 5116b559b60..1224fc7b37a 100644 --- a/extensions/discord/src/channel.ts +++ b/extensions/discord/src/channel.ts @@ -53,7 +53,7 @@ import { import { getDiscordRuntime } from "./runtime.js"; import { fetchChannelPermissionsDiscord } from "./send.js"; import { discordSetupAdapter } from "./setup-core.js"; -import { createDiscordPluginBase, discordConfigAccessors } from "./shared.js"; +import { createDiscordPluginBase, discordConfigAdapter } from "./shared.js"; import { collectDiscordStatusIssues } from "./status-issues.js"; import { parseDiscordTarget } from "./targets.js"; import { DiscordUiContainer } from "./ui.js"; @@ -307,7 +307,7 @@ export const discordPlugin: ChannelPlugin = { applyConfigEdit: buildAccountScopedAllowlistConfigEditor({ channelId: "discord", normalize: ({ cfg, accountId, values }) => - discordConfigAccessors.formatAllowFrom!({ cfg, accountId, allowFrom: values }), + discordConfigAdapter.formatAllowFrom!({ cfg, accountId, allowFrom: values }), resolvePaths: resolveLegacyDmAllowlistConfigPaths, }), }, diff --git a/extensions/discord/src/runtime-api.ts b/extensions/discord/src/runtime-api.ts index f2676220cdb..2aadbf90b9a 100644 --- a/extensions/discord/src/runtime-api.ts +++ b/extensions/discord/src/runtime-api.ts @@ -23,8 +23,11 @@ export { listDiscordDirectoryPeersFromConfig, } from "./directory-config.js"; export { + createHybridChannelConfigAdapter, + createScopedChannelConfigAdapter, createScopedAccountConfigAccessors, createScopedChannelConfigBase, + createTopLevelChannelConfigAdapter, } from "openclaw/plugin-sdk/channel-config-helpers"; export { createAccountActionGate, diff --git a/extensions/discord/src/shared.ts b/extensions/discord/src/shared.ts index 242d2d163a7..eadb6241899 100644 --- a/extensions/discord/src/shared.ts +++ b/extensions/discord/src/shared.ts @@ -8,8 +8,7 @@ import { type ResolvedDiscordAccount, } from "./accounts.js"; import { - createScopedAccountConfigAccessors, - createScopedChannelConfigBase, + createScopedChannelConfigAdapter, buildChannelConfigSchema, DiscordConfigSchema, getChatChannelMeta, @@ -27,20 +26,16 @@ export const discordSetupWizard = createDiscordSetupWizardProxy( async () => (await loadDiscordChannelRuntime()).discordSetupWizard, ); -export const discordConfigAccessors = createScopedAccountConfigAccessors({ - resolveAccount: ({ cfg, accountId }) => resolveDiscordAccount({ cfg, accountId }), - resolveAllowFrom: (account: ResolvedDiscordAccount) => account.config.dm?.allowFrom, - formatAllowFrom: (allowFrom) => formatAllowFromLowercase({ allowFrom }), - resolveDefaultTo: (account: ResolvedDiscordAccount) => account.config.defaultTo, -}); - -export const discordConfigBase = createScopedChannelConfigBase({ +export const discordConfigAdapter = createScopedChannelConfigAdapter({ sectionKey: DISCORD_CHANNEL, listAccountIds: listDiscordAccountIds, resolveAccount: (cfg, accountId) => resolveDiscordAccount({ cfg, accountId }), inspectAccount: (cfg, accountId) => inspectDiscordAccount({ cfg, accountId }), defaultAccountId: resolveDefaultDiscordAccountId, clearBaseFields: ["token", "name"], + resolveAllowFrom: (account: ResolvedDiscordAccount) => account.config.dm?.allowFrom, + formatAllowFrom: (allowFrom) => formatAllowFromLowercase({ allowFrom }), + resolveDefaultTo: (account: ResolvedDiscordAccount) => account.config.defaultTo, }); export function createDiscordPluginBase(params: { @@ -75,7 +70,7 @@ export function createDiscordPluginBase(params: { reload: { configPrefixes: ["channels.discord"] }, configSchema: buildChannelConfigSchema(DiscordConfigSchema), config: { - ...discordConfigBase, + ...discordConfigAdapter, isConfigured: (account) => Boolean(account.token?.trim()), describeAccount: (account) => ({ accountId: account.accountId, @@ -84,7 +79,6 @@ export function createDiscordPluginBase(params: { configured: Boolean(account.token?.trim()), tokenSource: account.tokenSource, }), - ...discordConfigAccessors, }, setup: params.setup, }) as Pick< diff --git a/extensions/feishu/src/channel.ts b/extensions/feishu/src/channel.ts index d56c80bb482..0aa071e7abd 100644 --- a/extensions/feishu/src/channel.ts +++ b/extensions/feishu/src/channel.ts @@ -1,8 +1,5 @@ import { formatAllowFromLowercase } from "openclaw/plugin-sdk/allow-from"; -import { - createHybridChannelConfigBase, - createScopedAccountConfigAccessors, -} from "openclaw/plugin-sdk/channel-config-helpers"; +import { createHybridChannelConfigAdapter } from "openclaw/plugin-sdk/channel-config-helpers"; import { collectAllowlistProviderRestrictSendersWarnings } from "openclaw/plugin-sdk/channel-policy"; import { createMessageToolCardSchema } from "openclaw/plugin-sdk/channel-runtime"; import type { @@ -130,17 +127,16 @@ function setFeishuNamedAccountEnabled( }; } -const feishuConfigBase = createHybridChannelConfigBase({ +const feishuConfigAdapter = createHybridChannelConfigAdapter< + ResolvedFeishuAccount, + ResolvedFeishuAccount, + ClawdbotConfig +>({ sectionKey: "feishu", listAccountIds: listFeishuAccountIds, resolveAccount: (cfg, accountId) => resolveFeishuAccount({ cfg, accountId }), defaultAccountId: resolveDefaultFeishuAccountId, clearBaseFields: [], -}); - -const feishuConfigAccessors = createScopedAccountConfigAccessors({ - resolveAccount: ({ cfg, accountId }) => - resolveFeishuAccount({ cfg: cfg as ClawdbotConfig, accountId }), resolveAllowFrom: (account) => account.config.allowFrom, formatAllowFrom: (allowFrom) => formatAllowFromLowercase({ allowFrom }), }); @@ -396,7 +392,7 @@ export const feishuPlugin: ChannelPlugin = { reload: { configPrefixes: ["channels.feishu"] }, configSchema: buildChannelConfigSchema(FeishuConfigSchema), config: { - ...feishuConfigBase, + ...feishuConfigAdapter, setAccountEnabled: ({ cfg, accountId, enabled }) => { const isDefault = accountId === DEFAULT_ACCOUNT_ID; if (isDefault) { @@ -454,7 +450,6 @@ export const feishuPlugin: ChannelPlugin = { appId: account.appId, domain: account.domain, }), - ...feishuConfigAccessors, }, actions: { describeMessageTool: describeFeishuMessageTool, diff --git a/extensions/googlechat/runtime-api.ts b/extensions/googlechat/runtime-api.ts index fb71b4196fc..28f7c81c4e9 100644 --- a/extensions/googlechat/runtime-api.ts +++ b/extensions/googlechat/runtime-api.ts @@ -9,8 +9,11 @@ export { readStringParam, } from "../../src/agents/tools/common.js"; export { + createScopedChannelConfigAdapter, createScopedAccountConfigAccessors, createScopedChannelConfigBase, + createTopLevelChannelConfigAdapter, + createHybridChannelConfigAdapter, createScopedDmSecurityResolver, } from "../../src/plugin-sdk/channel-config-helpers.js"; export { diff --git a/extensions/googlechat/src/channel.ts b/extensions/googlechat/src/channel.ts index b82f7635ff1..7cc86e81cda 100644 --- a/extensions/googlechat/src/channel.ts +++ b/extensions/googlechat/src/channel.ts @@ -1,7 +1,6 @@ import { formatNormalizedAllowFromEntries } from "openclaw/plugin-sdk/allow-from"; import { - createScopedAccountConfigAccessors, - createScopedChannelConfigBase, + createScopedChannelConfigAdapter, createScopedDmSecurityResolver, } from "openclaw/plugin-sdk/channel-config-helpers"; import { @@ -61,18 +60,7 @@ const formatAllowFromEntry = (entry: string) => .replace(/^users\//i, "") .toLowerCase(); -const googleChatConfigAccessors = createScopedAccountConfigAccessors({ - resolveAccount: ({ cfg, accountId }) => resolveGoogleChatAccount({ cfg, accountId }), - resolveAllowFrom: (account: ResolvedGoogleChatAccount) => account.config.dm?.allowFrom, - formatAllowFrom: (allowFrom) => - formatNormalizedAllowFromEntries({ - allowFrom, - normalizeEntry: formatAllowFromEntry, - }), - resolveDefaultTo: (account: ResolvedGoogleChatAccount) => account.config.defaultTo, -}); - -const googleChatConfigBase = createScopedChannelConfigBase({ +const googleChatConfigAdapter = createScopedChannelConfigAdapter({ sectionKey: "googlechat", listAccountIds: listGoogleChatAccountIds, resolveAccount: (cfg, accountId) => resolveGoogleChatAccount({ cfg, accountId }), @@ -87,6 +75,13 @@ const googleChatConfigBase = createScopedChannelConfigBase account.config.dm?.allowFrom, + formatAllowFrom: (allowFrom) => + formatNormalizedAllowFromEntries({ + allowFrom, + normalizeEntry: formatAllowFromEntry, + }), + resolveDefaultTo: (account: ResolvedGoogleChatAccount) => account.config.defaultTo, }); const resolveGoogleChatDmPolicy = createScopedDmSecurityResolver({ @@ -146,7 +141,7 @@ export const googlechatPlugin: ChannelPlugin = { reload: { configPrefixes: ["channels.googlechat"] }, configSchema: buildChannelConfigSchema(GoogleChatConfigSchema), config: { - ...googleChatConfigBase, + ...googleChatConfigAdapter, isConfigured: (account) => account.credentialSource !== "none", describeAccount: (account) => ({ accountId: account.accountId, @@ -155,7 +150,6 @@ export const googlechatPlugin: ChannelPlugin = { configured: account.credentialSource !== "none", credentialSource: account.credentialSource, }), - ...googleChatConfigAccessors, }, security: { resolveDmPolicy: resolveGoogleChatDmPolicy, diff --git a/extensions/imessage/src/shared.ts b/extensions/imessage/src/shared.ts index fb2486c69f3..cf3e7b173cf 100644 --- a/extensions/imessage/src/shared.ts +++ b/extensions/imessage/src/shared.ts @@ -1,8 +1,8 @@ import { collectAllowlistProviderRestrictSendersWarnings, - createScopedAccountConfigAccessors, - createScopedChannelConfigBase, + createScopedChannelConfigAdapter, createScopedDmSecurityResolver, + formatTrimmedAllowFromEntries, } from "openclaw/plugin-sdk/channel-config-helpers"; import { createChannelPluginBase } from "openclaw/plugin-sdk/core"; import { @@ -29,19 +29,15 @@ export const imessageSetupWizard = createIMessageSetupWizardProxy( async () => (await loadIMessageChannelRuntime()).imessageSetupWizard, ); -export const imessageConfigAccessors = createScopedAccountConfigAccessors({ - resolveAccount: ({ cfg, accountId }) => resolveIMessageAccount({ cfg, accountId }), - resolveAllowFrom: (account: ResolvedIMessageAccount) => account.config.allowFrom, - formatAllowFrom: (allowFrom) => allowFrom.map((entry) => String(entry).trim()).filter(Boolean), - resolveDefaultTo: (account: ResolvedIMessageAccount) => account.config.defaultTo, -}); - -export const imessageConfigBase = createScopedChannelConfigBase({ +export const imessageConfigAdapter = createScopedChannelConfigAdapter({ sectionKey: IMESSAGE_CHANNEL, listAccountIds: listIMessageAccountIds, resolveAccount: (cfg, accountId) => resolveIMessageAccount({ cfg, accountId }), defaultAccountId: resolveDefaultIMessageAccountId, clearBaseFields: ["cliPath", "dbPath", "service", "region", "name"], + resolveAllowFrom: (account: ResolvedIMessageAccount) => account.config.allowFrom, + formatAllowFrom: (allowFrom) => formatTrimmedAllowFromEntries(allowFrom), + resolveDefaultTo: (account: ResolvedIMessageAccount) => account.config.defaultTo, }); export const imessageResolveDmPolicy = createScopedDmSecurityResolver({ @@ -97,7 +93,7 @@ export function createIMessagePluginBase(params: { reload: { configPrefixes: ["channels.imessage"] }, configSchema: buildChannelConfigSchema(IMessageConfigSchema), config: { - ...imessageConfigBase, + ...imessageConfigAdapter, isConfigured: (account) => account.configured, describeAccount: (account) => ({ accountId: account.accountId, @@ -105,7 +101,6 @@ export function createIMessagePluginBase(params: { enabled: account.enabled, configured: account.configured, }), - ...imessageConfigAccessors, }, security: { resolveDmPolicy: imessageResolveDmPolicy, diff --git a/extensions/irc/src/channel.ts b/extensions/irc/src/channel.ts index 18fa8953045..554a01699ad 100644 --- a/extensions/irc/src/channel.ts +++ b/extensions/irc/src/channel.ts @@ -1,7 +1,6 @@ import { formatNormalizedAllowFromEntries } from "openclaw/plugin-sdk/allow-from"; import { - createScopedAccountConfigAccessors, - createScopedChannelConfigBase, + createScopedChannelConfigAdapter, createScopedDmSecurityResolver, } from "openclaw/plugin-sdk/channel-config-helpers"; import { @@ -51,18 +50,11 @@ function normalizePairingTarget(raw: string): string { return normalized.split(/[!@]/, 1)[0]?.trim() ?? ""; } -const ircConfigAccessors = createScopedAccountConfigAccessors({ - resolveAccount: ({ cfg, accountId }) => resolveIrcAccount({ cfg: cfg as CoreConfig, accountId }), - resolveAllowFrom: (account: ResolvedIrcAccount) => account.config.allowFrom, - formatAllowFrom: (allowFrom) => - formatNormalizedAllowFromEntries({ - allowFrom, - normalizeEntry: normalizeIrcAllowEntry, - }), - resolveDefaultTo: (account: ResolvedIrcAccount) => account.config.defaultTo, -}); - -const ircConfigBase = createScopedChannelConfigBase({ +const ircConfigAdapter = createScopedChannelConfigAdapter< + ResolvedIrcAccount, + ResolvedIrcAccount, + CoreConfig +>({ sectionKey: "irc", listAccountIds: listIrcAccountIds, resolveAccount: (cfg, accountId) => resolveIrcAccount({ cfg, accountId }), @@ -79,6 +71,13 @@ const ircConfigBase = createScopedChannelConfigBase account.config.allowFrom, + formatAllowFrom: (allowFrom) => + formatNormalizedAllowFromEntries({ + allowFrom, + normalizeEntry: normalizeIrcAllowEntry, + }), + resolveDefaultTo: (account: ResolvedIrcAccount) => account.config.defaultTo, }); const resolveIrcDmPolicy = createScopedDmSecurityResolver({ @@ -116,7 +115,7 @@ export const ircPlugin: ChannelPlugin = { reload: { configPrefixes: ["channels.irc"] }, configSchema: buildChannelConfigSchema(IrcConfigSchema), config: { - ...ircConfigBase, + ...ircConfigAdapter, isConfigured: (account) => account.configured, describeAccount: (account) => ({ accountId: account.accountId, @@ -129,7 +128,6 @@ export const ircPlugin: ChannelPlugin = { nick: account.nick, passwordSource: account.passwordSource, }), - ...ircConfigAccessors, }, security: { resolveDmPolicy: resolveIrcDmPolicy, diff --git a/extensions/line/src/channel.setup.ts b/extensions/line/src/channel.setup.ts index 5df541d6286..bae717a205d 100644 --- a/extensions/line/src/channel.setup.ts +++ b/extensions/line/src/channel.setup.ts @@ -2,10 +2,9 @@ import { buildChannelConfigSchema, LineConfigSchema, type ChannelPlugin, - type OpenClawConfig, type ResolvedLineAccount, } from "../api.js"; -import { listLineAccountIds, resolveDefaultLineAccountId, resolveLineAccount } from "../api.js"; +import { lineConfigAdapter } from "./config-adapter.js"; import { lineSetupAdapter } from "./setup-core.js"; import { lineSetupWizard } from "./setup-surface.js"; @@ -20,8 +19,6 @@ const meta = { systemImage: "message.fill", } as const; -const normalizeLineAllowFrom = (entry: string) => entry.replace(/^line:(?:user:)?/i, ""); - export const lineSetupPlugin: ChannelPlugin = { id: "line", meta: { @@ -39,10 +36,7 @@ export const lineSetupPlugin: ChannelPlugin = { reload: { configPrefixes: ["channels.line"] }, configSchema: buildChannelConfigSchema(LineConfigSchema), config: { - listAccountIds: (cfg: OpenClawConfig) => listLineAccountIds(cfg), - resolveAccount: (cfg: OpenClawConfig, accountId?: string | null) => - resolveLineAccount({ cfg, accountId: accountId ?? undefined }), - defaultAccountId: (cfg: OpenClawConfig) => resolveDefaultLineAccountId(cfg), + ...lineConfigAdapter, isConfigured: (account) => Boolean(account.channelAccessToken?.trim() && account.channelSecret?.trim()), describeAccount: (account) => ({ @@ -52,13 +46,6 @@ export const lineSetupPlugin: ChannelPlugin = { configured: Boolean(account.channelAccessToken?.trim() && account.channelSecret?.trim()), tokenSource: account.tokenSource ?? undefined, }), - resolveAllowFrom: ({ cfg, accountId }) => - resolveLineAccount({ cfg, accountId: accountId ?? undefined }).config.allowFrom, - formatAllowFrom: ({ allowFrom }) => - allowFrom - .map((entry) => String(entry).trim()) - .filter(Boolean) - .map((entry) => normalizeLineAllowFrom(entry)), }, setupWizard: lineSetupWizard, setup: lineSetupAdapter, diff --git a/extensions/line/src/channel.ts b/extensions/line/src/channel.ts index 7d01f233371..cd3fab965cc 100644 --- a/extensions/line/src/channel.ts +++ b/extensions/line/src/channel.ts @@ -1,8 +1,4 @@ -import { - createScopedAccountConfigAccessors, - createScopedChannelConfigBase, - createScopedDmSecurityResolver, -} from "openclaw/plugin-sdk/channel-config-helpers"; +import { createScopedDmSecurityResolver } from "openclaw/plugin-sdk/channel-config-helpers"; import { collectAllowlistProviderRestrictSendersWarnings } from "openclaw/plugin-sdk/channel-policy"; import { buildChannelConfigSchema, @@ -14,11 +10,11 @@ import { processLineMessage, type ChannelPlugin, type ChannelStatusIssue, - type OpenClawConfig, type LineConfig, type LineChannelData, type ResolvedLineAccount, } from "../api.js"; +import { lineConfigAdapter } from "./config-adapter.js"; import { resolveLineGroupRequireMention } from "./group-policy.js"; import { getLineRuntime } from "./runtime.js"; import { lineSetupAdapter } from "./setup-core.js"; @@ -36,26 +32,6 @@ const meta = { systemImage: "message.fill", }; -const lineConfigAccessors = createScopedAccountConfigAccessors({ - resolveAccount: ({ cfg, accountId }) => - getLineRuntime().channel.line.resolveLineAccount({ cfg, accountId: accountId ?? undefined }), - resolveAllowFrom: (account: ResolvedLineAccount) => account.config.allowFrom, - formatAllowFrom: (allowFrom) => - allowFrom - .map((entry) => String(entry).trim()) - .filter(Boolean) - .map((entry) => entry.replace(/^line:(?:user:)?/i, "")), -}); - -const lineConfigBase = createScopedChannelConfigBase({ - sectionKey: "line", - listAccountIds: (cfg) => getLineRuntime().channel.line.listLineAccountIds(cfg), - resolveAccount: (cfg, accountId) => - getLineRuntime().channel.line.resolveLineAccount({ cfg, accountId: accountId ?? undefined }), - defaultAccountId: (cfg) => getLineRuntime().channel.line.resolveDefaultLineAccountId(cfg), - clearBaseFields: ["channelSecret", "tokenFile", "secretFile"], -}); - const resolveLineDmPolicy = createScopedDmSecurityResolver({ channelKey: "line", resolvePolicy: (account) => account.config.dmPolicy, @@ -100,7 +76,7 @@ export const linePlugin: ChannelPlugin = { configSchema: buildChannelConfigSchema(LineConfigSchema), setupWizard: lineSetupWizard, config: { - ...lineConfigBase, + ...lineConfigAdapter, isConfigured: (account) => Boolean(account.channelAccessToken?.trim() && account.channelSecret?.trim()), describeAccount: (account) => ({ @@ -110,7 +86,6 @@ export const linePlugin: ChannelPlugin = { configured: Boolean(account.channelAccessToken?.trim() && account.channelSecret?.trim()), tokenSource: account.tokenSource ?? undefined, }), - ...lineConfigAccessors, }, security: { resolveDmPolicy: resolveLineDmPolicy, diff --git a/extensions/line/src/config-adapter.ts b/extensions/line/src/config-adapter.ts new file mode 100644 index 00000000000..118159f16b2 --- /dev/null +++ b/extensions/line/src/config-adapter.ts @@ -0,0 +1,32 @@ +import { createScopedChannelConfigAdapter } from "openclaw/plugin-sdk/channel-config-helpers"; +import type { OpenClawConfig, ResolvedLineAccount } from "../api.js"; +import { getLineRuntime } from "./runtime.js"; + +function resolveLineRuntimeAccount(cfg: OpenClawConfig, accountId?: string | null) { + return getLineRuntime().channel.line.resolveLineAccount({ + cfg, + accountId: accountId ?? undefined, + }); +} + +export function normalizeLineAllowFrom(entry: string): string { + return entry.replace(/^line:(?:user:)?/i, ""); +} + +export const lineConfigAdapter = createScopedChannelConfigAdapter< + ResolvedLineAccount, + ResolvedLineAccount, + OpenClawConfig +>({ + sectionKey: "line", + listAccountIds: (cfg) => getLineRuntime().channel.line.listLineAccountIds(cfg), + resolveAccount: (cfg, accountId) => resolveLineRuntimeAccount(cfg, accountId), + defaultAccountId: (cfg) => getLineRuntime().channel.line.resolveDefaultLineAccountId(cfg), + clearBaseFields: ["channelSecret", "tokenFile", "secretFile"], + resolveAllowFrom: (account) => account.config.allowFrom, + formatAllowFrom: (allowFrom) => + allowFrom + .map((entry) => String(entry).trim()) + .filter(Boolean) + .map(normalizeLineAllowFrom), +}); diff --git a/extensions/matrix/src/channel.ts b/extensions/matrix/src/channel.ts index 7a3f485d21d..aaf18e3f94b 100644 --- a/extensions/matrix/src/channel.ts +++ b/extensions/matrix/src/channel.ts @@ -1,6 +1,5 @@ import { - createScopedAccountConfigAccessors, - createScopedChannelConfigBase, + createScopedChannelConfigAdapter, createScopedDmSecurityResolver, } from "openclaw/plugin-sdk/channel-config-helpers"; import { @@ -69,17 +68,16 @@ function normalizeMatrixMessagingTarget(raw: string): string | undefined { return stripped || undefined; } -const matrixConfigAccessors = createScopedAccountConfigAccessors({ - resolveAccount: ({ cfg, accountId }) => - resolveMatrixAccountConfig({ cfg: cfg as CoreConfig, accountId }), - resolveAllowFrom: (account) => account.dm?.allowFrom, - formatAllowFrom: (allowFrom) => normalizeMatrixAllowList(allowFrom), -}); - -const matrixConfigBase = createScopedChannelConfigBase({ +const matrixConfigAdapter = createScopedChannelConfigAdapter< + ResolvedMatrixAccount, + ReturnType, + CoreConfig +>({ sectionKey: "matrix", listAccountIds: listMatrixAccountIds, resolveAccount: (cfg, accountId) => resolveMatrixAccount({ cfg, accountId }), + resolveAccessorAccount: ({ cfg, accountId }) => + resolveMatrixAccountConfig({ cfg: cfg as CoreConfig, accountId }), defaultAccountId: resolveDefaultMatrixAccountId, clearBaseFields: [ "name", @@ -90,6 +88,8 @@ const matrixConfigBase = createScopedChannelConfigBase account.dm?.allowFrom, + formatAllowFrom: (allowFrom) => normalizeMatrixAllowList(allowFrom), }); const resolveMatrixDmPolicy = createScopedDmSecurityResolver({ @@ -122,7 +122,7 @@ export const matrixPlugin: ChannelPlugin = { reload: { configPrefixes: ["channels.matrix"] }, configSchema: buildChannelConfigSchema(MatrixConfigSchema), config: { - ...matrixConfigBase, + ...matrixConfigAdapter, isConfigured: (account) => account.configured, describeAccount: (account) => ({ accountId: account.accountId, @@ -131,7 +131,6 @@ export const matrixPlugin: ChannelPlugin = { configured: account.configured, baseUrl: account.homeserver, }), - ...matrixConfigAccessors, }, security: { resolveDmPolicy: resolveMatrixDmPolicy, diff --git a/extensions/mattermost/src/channel.ts b/extensions/mattermost/src/channel.ts index 90d24e11406..8c32e068165 100644 --- a/extensions/mattermost/src/channel.ts +++ b/extensions/mattermost/src/channel.ts @@ -1,7 +1,6 @@ import { formatNormalizedAllowFromEntries } from "openclaw/plugin-sdk/allow-from"; import { - createScopedAccountConfigAccessors, - createScopedChannelConfigBase, + createScopedChannelConfigAdapter, createScopedDmSecurityResolver, } from "openclaw/plugin-sdk/channel-config-helpers"; import { collectAllowlistProviderRestrictSendersWarnings } from "openclaw/plugin-sdk/channel-policy"; @@ -248,8 +247,12 @@ function formatAllowEntry(entry: string): string { return trimmed.replace(/^(mattermost|user):/i, "").toLowerCase(); } -const mattermostConfigAccessors = createScopedAccountConfigAccessors({ - resolveAccount: ({ cfg, accountId }) => resolveMattermostAccount({ cfg, accountId }), +const mattermostConfigAdapter = createScopedChannelConfigAdapter({ + sectionKey: "mattermost", + listAccountIds: listMattermostAccountIds, + resolveAccount: (cfg, accountId) => resolveMattermostAccount({ cfg, accountId }), + defaultAccountId: resolveDefaultMattermostAccountId, + clearBaseFields: ["botToken", "baseUrl", "name"], resolveAllowFrom: (account: ResolvedMattermostAccount) => account.config.allowFrom, formatAllowFrom: (allowFrom) => formatNormalizedAllowFromEntries({ @@ -258,14 +261,6 @@ const mattermostConfigAccessors = createScopedAccountConfigAccessors({ }), }); -const mattermostConfigBase = createScopedChannelConfigBase({ - sectionKey: "mattermost", - listAccountIds: listMattermostAccountIds, - resolveAccount: (cfg, accountId) => resolveMattermostAccount({ cfg, accountId }), - defaultAccountId: resolveDefaultMattermostAccountId, - clearBaseFields: ["botToken", "baseUrl", "name"], -}); - const resolveMattermostDmPolicy = createScopedDmSecurityResolver({ channelKey: "mattermost", resolvePolicy: (account) => account.config.dmPolicy, @@ -311,7 +306,7 @@ export const mattermostPlugin: ChannelPlugin = { reload: { configPrefixes: ["channels.mattermost"] }, configSchema: buildChannelConfigSchema(MattermostConfigSchema), config: { - ...mattermostConfigBase, + ...mattermostConfigAdapter, isConfigured: (account) => Boolean(account.botToken && account.baseUrl), describeAccount: (account) => ({ accountId: account.accountId, @@ -321,7 +316,6 @@ export const mattermostPlugin: ChannelPlugin = { botTokenSource: account.botTokenSource, baseUrl: account.baseUrl, }), - ...mattermostConfigAccessors, }, security: { resolveDmPolicy: resolveMattermostDmPolicy, diff --git a/extensions/msteams/src/channel.ts b/extensions/msteams/src/channel.ts index 17c73cf1e61..b1379e311df 100644 --- a/extensions/msteams/src/channel.ts +++ b/extensions/msteams/src/channel.ts @@ -1,8 +1,5 @@ import { formatAllowFromLowercase } from "openclaw/plugin-sdk/allow-from"; -import { - createScopedAccountConfigAccessors, - createTopLevelChannelConfigBase, -} from "openclaw/plugin-sdk/channel-config-helpers"; +import { createTopLevelChannelConfigAdapter } from "openclaw/plugin-sdk/channel-config-helpers"; import { collectAllowlistProviderRestrictSendersWarnings } from "openclaw/plugin-sdk/channel-policy"; import { createMessageToolCardSchema } from "openclaw/plugin-sdk/channel-runtime"; import type { @@ -73,20 +70,20 @@ const resolveMSTeamsChannelConfig = (cfg: OpenClawConfig) => ({ defaultTo: cfg.channels?.msteams?.defaultTo, }); -const msteamsConfigBase = createTopLevelChannelConfigBase({ +const msteamsConfigAdapter = createTopLevelChannelConfigAdapter< + ResolvedMSTeamsAccount, + { + allowFrom?: Array; + defaultTo?: string; + } +>({ sectionKey: "msteams", resolveAccount: (cfg) => ({ accountId: DEFAULT_ACCOUNT_ID, enabled: cfg.channels?.msteams?.enabled !== false, configured: Boolean(resolveMSTeamsCredentials(cfg.channels?.msteams)), }), -}); - -const msteamsConfigAccessors = createScopedAccountConfigAccessors<{ - allowFrom?: Array; - defaultTo?: string; -}>({ - resolveAccount: ({ cfg }) => resolveMSTeamsChannelConfig(cfg), + resolveAccessorAccount: ({ cfg }) => resolveMSTeamsChannelConfig(cfg), resolveAllowFrom: (account) => account.allowFrom, formatAllowFrom: (allowFrom) => formatAllowFromLowercase({ allowFrom }), resolveDefaultTo: (account) => account.defaultTo, @@ -157,14 +154,13 @@ export const msteamsPlugin: ChannelPlugin = { reload: { configPrefixes: ["channels.msteams"] }, configSchema: buildChannelConfigSchema(MSTeamsConfigSchema), config: { - ...msteamsConfigBase, + ...msteamsConfigAdapter, isConfigured: (_account, cfg) => Boolean(resolveMSTeamsCredentials(cfg.channels?.msteams)), describeAccount: (account) => ({ accountId: account.accountId, enabled: account.enabled, configured: account.configured, }), - ...msteamsConfigAccessors, }, security: { collectWarnings: ({ cfg }) => { diff --git a/extensions/nextcloud-talk/src/channel.ts b/extensions/nextcloud-talk/src/channel.ts index d912a6bbf33..ce2f281a3e6 100644 --- a/extensions/nextcloud-talk/src/channel.ts +++ b/extensions/nextcloud-talk/src/channel.ts @@ -1,7 +1,6 @@ import { formatAllowFromLowercase } from "openclaw/plugin-sdk/allow-from"; import { - createScopedAccountConfigAccessors, - createScopedChannelConfigBase, + createScopedChannelConfigAdapter, createScopedDmSecurityResolver, } from "openclaw/plugin-sdk/channel-config-helpers"; import { createAccountStatusSink } from "openclaw/plugin-sdk/channel-lifecycle"; @@ -51,19 +50,8 @@ const meta = { quickstartAllowFrom: true, }; -const nextcloudTalkConfigAccessors = - createScopedAccountConfigAccessors({ - resolveAccount: ({ cfg, accountId }) => - resolveNextcloudTalkAccount({ cfg: cfg as CoreConfig, accountId }), - resolveAllowFrom: (account) => account.config.allowFrom, - formatAllowFrom: (allowFrom) => - formatAllowFromLowercase({ - allowFrom, - stripPrefixRe: /^(nextcloud-talk|nc-talk|nc):/i, - }), - }); - -const nextcloudTalkConfigBase = createScopedChannelConfigBase< +const nextcloudTalkConfigAdapter = createScopedChannelConfigAdapter< + ResolvedNextcloudTalkAccount, ResolvedNextcloudTalkAccount, CoreConfig >({ @@ -72,6 +60,12 @@ const nextcloudTalkConfigBase = createScopedChannelConfigBase< resolveAccount: (cfg, accountId) => resolveNextcloudTalkAccount({ cfg, accountId }), defaultAccountId: resolveDefaultNextcloudTalkAccountId, clearBaseFields: ["botSecret", "botSecretFile", "baseUrl", "name"], + resolveAllowFrom: (account) => account.config.allowFrom, + formatAllowFrom: (allowFrom) => + formatAllowFromLowercase({ + allowFrom, + stripPrefixRe: /^(nextcloud-talk|nc-talk|nc):/i, + }), }); const resolveNextcloudTalkDmPolicy = createScopedDmSecurityResolver({ @@ -105,7 +99,7 @@ export const nextcloudTalkPlugin: ChannelPlugin = reload: { configPrefixes: ["channels.nextcloud-talk"] }, configSchema: buildChannelConfigSchema(NextcloudTalkConfigSchema), config: { - ...nextcloudTalkConfigBase, + ...nextcloudTalkConfigAdapter, isConfigured: (account) => Boolean(account.secret?.trim() && account.baseUrl?.trim()), describeAccount: (account) => ({ accountId: account.accountId, @@ -115,7 +109,6 @@ export const nextcloudTalkPlugin: ChannelPlugin = secretSource: account.secretSource, baseUrl: account.baseUrl ? "[set]" : "[missing]", }), - ...nextcloudTalkConfigAccessors, }, security: { resolveDmPolicy: resolveNextcloudTalkDmPolicy, diff --git a/extensions/nostr/src/channel.ts b/extensions/nostr/src/channel.ts index 7758e1a18ab..63ea3436dab 100644 --- a/extensions/nostr/src/channel.ts +++ b/extensions/nostr/src/channel.ts @@ -1,11 +1,13 @@ -import { createScopedDmSecurityResolver } from "openclaw/plugin-sdk/channel-config-helpers"; +import { + createScopedDmSecurityResolver, + createTopLevelChannelConfigAdapter, +} from "openclaw/plugin-sdk/channel-config-helpers"; import { buildChannelConfigSchema, collectStatusIssuesFromLastError, createDefaultChannelRuntimeState, DEFAULT_ACCOUNT_ID, formatPairingApproveHint, - mapAllowFromEntries, type ChannelPlugin, } from "openclaw/plugin-sdk/nostr"; import { @@ -49,6 +51,39 @@ const resolveNostrDmPolicy = createScopedDmSecurityResolver({ + sectionKey: "nostr", + resolveAccount: (cfg) => resolveNostrAccount({ cfg }), + listAccountIds: listNostrAccountIds, + defaultAccountId: resolveDefaultNostrAccountId, + deleteMode: "clear-fields", + clearBaseFields: [ + "name", + "defaultAccount", + "privateKey", + "relays", + "dmPolicy", + "allowFrom", + "profile", + ], + resolveAllowFrom: (account) => account.config.allowFrom, + formatAllowFrom: (allowFrom) => + allowFrom + .map((entry) => String(entry).trim()) + .filter(Boolean) + .map((entry) => { + if (entry === "*") { + return "*"; + } + try { + return normalizePubkey(entry); + } catch { + return entry; + } + }) + .filter(Boolean), +}); + export const nostrPlugin: ChannelPlugin = { id: "nostr", meta: { @@ -70,9 +105,7 @@ export const nostrPlugin: ChannelPlugin = { setupWizard: nostrSetupWizard, config: { - listAccountIds: (cfg) => listNostrAccountIds(cfg), - resolveAccount: (cfg, accountId) => resolveNostrAccount({ cfg, accountId }), - defaultAccountId: (cfg) => resolveDefaultNostrAccountId(cfg), + ...nostrConfigAdapter, isConfigured: (account) => account.configured, describeAccount: (account) => ({ accountId: account.accountId, @@ -81,23 +114,6 @@ export const nostrPlugin: ChannelPlugin = { configured: account.configured, publicKey: account.publicKey, }), - resolveAllowFrom: ({ cfg, accountId }) => - mapAllowFromEntries(resolveNostrAccount({ cfg, accountId }).config.allowFrom), - formatAllowFrom: ({ allowFrom }) => - allowFrom - .map((entry) => String(entry).trim()) - .filter(Boolean) - .map((entry) => { - if (entry === "*") { - return "*"; - } - try { - return normalizePubkey(entry); - } catch { - return entry; // Keep as-is if normalization fails - } - }) - .filter(Boolean), }, pairing: { diff --git a/extensions/signal/src/channel.ts b/extensions/signal/src/channel.ts index 8552a26c8df..8b8fe842511 100644 --- a/extensions/signal/src/channel.ts +++ b/extensions/signal/src/channel.ts @@ -31,8 +31,8 @@ import { getSignalRuntime } from "./runtime.js"; import { signalSetupAdapter } from "./setup-core.js"; import { collectSignalSecurityWarnings, + signalConfigAdapter, createSignalPluginBase, - signalConfigAccessors, signalResolveDmPolicy, signalSetupWizard, } from "./shared.js"; @@ -290,7 +290,7 @@ export const signalPlugin: ChannelPlugin = { applyConfigEdit: buildAccountScopedAllowlistConfigEditor({ channelId: "signal", normalize: ({ cfg, accountId, values }) => - signalConfigAccessors.formatAllowFrom!({ cfg, accountId, allowFrom: values }), + signalConfigAdapter.formatAllowFrom!({ cfg, accountId, allowFrom: values }), resolvePaths: (scope) => ({ readPaths: [[scope === "dm" ? "allowFrom" : "groupAllowFrom"]], writePath: [scope === "dm" ? "allowFrom" : "groupAllowFrom"], diff --git a/extensions/signal/src/shared.ts b/extensions/signal/src/shared.ts index e9370474de2..c307a51e66c 100644 --- a/extensions/signal/src/shared.ts +++ b/extensions/signal/src/shared.ts @@ -1,7 +1,6 @@ import { collectAllowlistProviderRestrictSendersWarnings, - createScopedAccountConfigAccessors, - createScopedChannelConfigBase, + createScopedChannelConfigAdapter, createScopedDmSecurityResolver, } from "openclaw/plugin-sdk/channel-config-helpers"; import { createChannelPluginBase } from "openclaw/plugin-sdk/core"; @@ -30,8 +29,12 @@ export const signalSetupWizard = createSignalSetupWizardProxy( async () => (await loadSignalChannelRuntime()).signalSetupWizard, ); -export const signalConfigAccessors = createScopedAccountConfigAccessors({ - resolveAccount: ({ cfg, accountId }) => resolveSignalAccount({ cfg, accountId }), +export const signalConfigAdapter = createScopedChannelConfigAdapter({ + sectionKey: SIGNAL_CHANNEL, + listAccountIds: listSignalAccountIds, + resolveAccount: (cfg, accountId) => resolveSignalAccount({ cfg, accountId }), + defaultAccountId: resolveDefaultSignalAccountId, + clearBaseFields: ["account", "httpUrl", "httpHost", "httpPort", "cliPath", "name"], resolveAllowFrom: (account: ResolvedSignalAccount) => account.config.allowFrom, formatAllowFrom: (allowFrom) => allowFrom @@ -42,14 +45,6 @@ export const signalConfigAccessors = createScopedAccountConfigAccessors({ resolveDefaultTo: (account: ResolvedSignalAccount) => account.config.defaultTo, }); -export const signalConfigBase = createScopedChannelConfigBase({ - sectionKey: SIGNAL_CHANNEL, - listAccountIds: listSignalAccountIds, - resolveAccount: (cfg, accountId) => resolveSignalAccount({ cfg, accountId }), - defaultAccountId: resolveDefaultSignalAccountId, - clearBaseFields: ["account", "httpUrl", "httpHost", "httpPort", "cliPath", "name"], -}); - export const signalResolveDmPolicy = createScopedDmSecurityResolver({ channelKey: SIGNAL_CHANNEL, resolvePolicy: (account) => account.config.dmPolicy, @@ -107,7 +102,7 @@ export function createSignalPluginBase(params: { reload: { configPrefixes: ["channels.signal"] }, configSchema: buildChannelConfigSchema(SignalConfigSchema), config: { - ...signalConfigBase, + ...signalConfigAdapter, isConfigured: (account) => account.configured, describeAccount: (account) => ({ accountId: account.accountId, @@ -116,7 +111,6 @@ export function createSignalPluginBase(params: { configured: account.configured, baseUrl: account.baseUrl, }), - ...signalConfigAccessors, }, security: { resolveDmPolicy: signalResolveDmPolicy, diff --git a/extensions/slack/src/channel.ts b/extensions/slack/src/channel.ts index fac44a19770..417f3b9a3b4 100644 --- a/extensions/slack/src/channel.ts +++ b/extensions/slack/src/channel.ts @@ -44,7 +44,7 @@ import { slackSetupWizard } from "./setup-surface.js"; import { createSlackPluginBase, isSlackPluginAccountConfigured, - slackConfigAccessors, + slackConfigAdapter, SLACK_CHANNEL, } from "./shared.js"; import { parseSlackTarget } from "./targets.js"; @@ -352,7 +352,7 @@ export const slackPlugin: ChannelPlugin = { applyConfigEdit: buildAccountScopedAllowlistConfigEditor({ channelId: "slack", normalize: ({ cfg, accountId, values }) => - slackConfigAccessors.formatAllowFrom!({ cfg, accountId, allowFrom: values }), + slackConfigAdapter.formatAllowFrom!({ cfg, accountId, allowFrom: values }), resolvePaths: resolveLegacyDmAllowlistConfigPaths, }), }, diff --git a/extensions/slack/src/shared.ts b/extensions/slack/src/shared.ts index dea94f67d3d..0d7e72a30e1 100644 --- a/extensions/slack/src/shared.ts +++ b/extensions/slack/src/shared.ts @@ -1,8 +1,5 @@ import { formatAllowFromLowercase } from "openclaw/plugin-sdk/allow-from"; -import { - createScopedAccountConfigAccessors, - createScopedChannelConfigBase, -} from "openclaw/plugin-sdk/channel-config-helpers"; +import { createScopedChannelConfigAdapter } from "openclaw/plugin-sdk/channel-config-helpers"; import { createChannelPluginBase } from "openclaw/plugin-sdk/core"; import { formatDocsLink, @@ -145,20 +142,16 @@ export function isSlackSetupAccountConfigured(account: ResolvedSlackAccount): bo return hasConfiguredBotToken && hasConfiguredAppToken; } -export const slackConfigAccessors = createScopedAccountConfigAccessors({ - resolveAccount: ({ cfg, accountId }) => resolveSlackAccount({ cfg, accountId }), - resolveAllowFrom: (account: ResolvedSlackAccount) => account.dm?.allowFrom, - formatAllowFrom: (allowFrom) => formatAllowFromLowercase({ allowFrom }), - resolveDefaultTo: (account: ResolvedSlackAccount) => account.config.defaultTo, -}); - -export const slackConfigBase = createScopedChannelConfigBase({ +export const slackConfigAdapter = createScopedChannelConfigAdapter({ sectionKey: SLACK_CHANNEL, listAccountIds: listSlackAccountIds, resolveAccount: (cfg, accountId) => resolveSlackAccount({ cfg, accountId }), inspectAccount: (cfg, accountId) => inspectSlackAccount({ cfg, accountId }), defaultAccountId: resolveDefaultSlackAccountId, clearBaseFields: ["botToken", "appToken", "name"], + resolveAllowFrom: (account: ResolvedSlackAccount) => account.dm?.allowFrom, + formatAllowFrom: (allowFrom) => formatAllowFromLowercase({ allowFrom }), + resolveDefaultTo: (account: ResolvedSlackAccount) => account.config.defaultTo, }); export function createSlackPluginBase(params: { @@ -208,7 +201,7 @@ export function createSlackPluginBase(params: { reload: { configPrefixes: ["channels.slack"] }, configSchema: buildChannelConfigSchema(SlackConfigSchema), config: { - ...slackConfigBase, + ...slackConfigAdapter, isConfigured: (account) => isSlackPluginAccountConfigured(account), describeAccount: (account) => ({ accountId: account.accountId, @@ -218,7 +211,6 @@ export function createSlackPluginBase(params: { botTokenSource: account.botTokenSource, appTokenSource: account.appTokenSource, }), - ...slackConfigAccessors, }, setup: params.setup, }) as Pick< diff --git a/extensions/synology-chat/src/channel.test.ts b/extensions/synology-chat/src/channel.test.ts index 851b6e92561..3c453d0613a 100644 --- a/extensions/synology-chat/src/channel.test.ts +++ b/extensions/synology-chat/src/channel.test.ts @@ -57,6 +57,16 @@ describe("createSynologyChatPlugin", () => { const plugin = createSynologyChatPlugin(); expect(plugin.config.defaultAccountId?.({})).toBe("default"); }); + + it("formats allowFrom entries through the shared adapter", () => { + const plugin = createSynologyChatPlugin(); + expect( + plugin.config.formatAllowFrom?.({ + cfg: {}, + allowFrom: [" USER1 ", 42], + }), + ).toEqual(["user1", "42"]); + }); }); describe("security", () => { diff --git a/extensions/synology-chat/src/channel.ts b/extensions/synology-chat/src/channel.ts index 3a3cbb99eb2..496b5563857 100644 --- a/extensions/synology-chat/src/channel.ts +++ b/extensions/synology-chat/src/channel.ts @@ -5,7 +5,7 @@ */ import { - createHybridChannelConfigBase, + createHybridChannelConfigAdapter, createScopedDmSecurityResolver, } from "openclaw/plugin-sdk/channel-config-helpers"; import { z } from "zod"; @@ -32,7 +32,7 @@ const resolveSynologyChatDmPolicy = createScopedDmSecurityResolver raw.toLowerCase().trim(), }); -const synologyChatConfigBase = createHybridChannelConfigBase({ +const synologyChatConfigAdapter = createHybridChannelConfigAdapter({ sectionKey: CHANNEL_ID, listAccountIds: (cfg: any) => listAccountIds(cfg), resolveAccount: (cfg: any, accountId?: string | null) => resolveAccount(cfg, accountId), @@ -48,6 +48,9 @@ const synologyChatConfigBase = createHybridChannelConfigBase account.allowedUserIds, + formatAllowFrom: (allowFrom) => + allowFrom.map((entry) => String(entry).trim().toLowerCase()).filter(Boolean), }); function waitUntilAbort(signal?: AbortSignal, onAbort?: () => void): Promise { @@ -100,7 +103,7 @@ export function createSynologyChatPlugin() { setupWizard: synologyChatSetupWizard, config: { - ...synologyChatConfigBase, + ...synologyChatConfigAdapter, }, pairing: { diff --git a/extensions/telegram/src/channel.ts b/extensions/telegram/src/channel.ts index f9946dfa1d6..3313510ad16 100644 --- a/extensions/telegram/src/channel.ts +++ b/extensions/telegram/src/channel.ts @@ -55,7 +55,7 @@ import { createTelegramPluginBase, findTelegramTokenOwnerAccountId, formatDuplicateTelegramTokenReason, - telegramConfigAccessors, + telegramConfigAdapter, } from "./shared.js"; import { collectTelegramStatusIssues } from "./status-issues.js"; import { parseTelegramTarget } from "./targets.js"; @@ -325,7 +325,7 @@ export const telegramPlugin: ChannelPlugin - telegramConfigAccessors.formatAllowFrom!({ cfg, accountId, allowFrom: values }), + telegramConfigAdapter.formatAllowFrom!({ cfg, accountId, allowFrom: values }), resolvePaths: (scope) => ({ readPaths: [[scope === "dm" ? "allowFrom" : "groupAllowFrom"]], writePath: [scope === "dm" ? "allowFrom" : "groupAllowFrom"], diff --git a/extensions/telegram/src/shared.ts b/extensions/telegram/src/shared.ts index e75c17ed7b4..6898870e394 100644 --- a/extensions/telegram/src/shared.ts +++ b/extensions/telegram/src/shared.ts @@ -1,8 +1,5 @@ import { formatAllowFromLowercase } from "openclaw/plugin-sdk/allow-from"; -import { - createScopedAccountConfigAccessors, - createScopedChannelConfigBase, -} from "openclaw/plugin-sdk/channel-config-helpers"; +import { createScopedChannelConfigAdapter } from "openclaw/plugin-sdk/channel-config-helpers"; import { createChannelPluginBase } from "openclaw/plugin-sdk/core"; import { buildChannelConfigSchema, @@ -56,21 +53,17 @@ export function formatDuplicateTelegramTokenReason(params: { ); } -export const telegramConfigAccessors = createScopedAccountConfigAccessors({ - resolveAccount: ({ cfg, accountId }) => resolveTelegramAccount({ cfg, accountId }), - resolveAllowFrom: (account: ResolvedTelegramAccount) => account.config.allowFrom, - formatAllowFrom: (allowFrom) => - formatAllowFromLowercase({ allowFrom, stripPrefixRe: /^(telegram|tg):/i }), - resolveDefaultTo: (account: ResolvedTelegramAccount) => account.config.defaultTo, -}); - -export const telegramConfigBase = createScopedChannelConfigBase({ +export const telegramConfigAdapter = createScopedChannelConfigAdapter({ sectionKey: TELEGRAM_CHANNEL, listAccountIds: listTelegramAccountIds, resolveAccount: (cfg, accountId) => resolveTelegramAccount({ cfg, accountId }), inspectAccount: (cfg, accountId) => inspectTelegramAccount({ cfg, accountId }), defaultAccountId: resolveDefaultTelegramAccountId, clearBaseFields: ["botToken", "tokenFile", "name"], + resolveAllowFrom: (account: ResolvedTelegramAccount) => account.config.allowFrom, + formatAllowFrom: (allowFrom) => + formatAllowFromLowercase({ allowFrom, stripPrefixRe: /^(telegram|tg):/i }), + resolveDefaultTo: (account: ResolvedTelegramAccount) => account.config.defaultTo, }); export function createTelegramPluginBase(params: { @@ -99,7 +92,7 @@ export function createTelegramPluginBase(params: { reload: { configPrefixes: ["channels.telegram"] }, configSchema: buildChannelConfigSchema(TelegramConfigSchema), config: { - ...telegramConfigBase, + ...telegramConfigAdapter, isConfigured: (account, cfg) => { if (!account.token?.trim()) { return false; @@ -131,7 +124,6 @@ export function createTelegramPluginBase(params: { !findTelegramTokenOwnerAccountId({ cfg, accountId: account.accountId }), tokenSource: account.tokenSource, }), - ...telegramConfigAccessors, }, setup: params.setup, }) as Pick< diff --git a/extensions/tlon/src/channel.test.ts b/extensions/tlon/src/channel.test.ts new file mode 100644 index 00000000000..44059ed1617 --- /dev/null +++ b/extensions/tlon/src/channel.test.ts @@ -0,0 +1,32 @@ +import type { OpenClawConfig } from "openclaw/plugin-sdk/tlon"; +import { describe, expect, it } from "vitest"; +import { tlonPlugin } from "./channel.js"; + +describe("tlonPlugin config", () => { + it("formats dm allowlist entries through the shared hybrid adapter", () => { + expect( + tlonPlugin.config.formatAllowFrom?.({ + cfg: {} as OpenClawConfig, + allowFrom: ["zod", " ~nec "], + }), + ).toEqual(["~zod", "~nec"]); + }); + + it("resolves dm allowlist from the default account", () => { + expect( + tlonPlugin.config.resolveAllowFrom?.({ + cfg: { + channels: { + tlon: { + ship: "~sampel-palnet", + url: "https://urbit.example.com", + code: "lidlut-tabwed-pillex-ridrup", + dmAllowlist: ["~zod"], + }, + }, + } as OpenClawConfig, + accountId: "default", + }), + ).toEqual(["~zod"]); + }); +}); diff --git a/extensions/tlon/src/channel.ts b/extensions/tlon/src/channel.ts index 794dbd4f5e0..865ead9ab46 100644 --- a/extensions/tlon/src/channel.ts +++ b/extensions/tlon/src/channel.ts @@ -1,4 +1,4 @@ -import { createHybridChannelConfigBase } from "openclaw/plugin-sdk/channel-config-helpers"; +import { createHybridChannelConfigAdapter } from "openclaw/plugin-sdk/channel-config-helpers"; import type { ChannelAccountSnapshot, ChannelPlugin } from "openclaw/plugin-sdk/channel-runtime"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime"; @@ -39,7 +39,7 @@ const tlonSetupWizardProxy = createTlonSetupWizardBase({ ).tlonSetupWizard.finalize!(params), }) satisfies NonNullable; -const tlonConfigBase = createHybridChannelConfigBase({ +const tlonConfigAdapter = createHybridChannelConfigAdapter({ sectionKey: TLON_CHANNEL_ID, listAccountIds: (cfg: OpenClawConfig) => listTlonAccountIds(cfg), resolveAccount: (cfg: OpenClawConfig, accountId?: string | null) => @@ -47,6 +47,9 @@ const tlonConfigBase = createHybridChannelConfigBase({ defaultAccountId: () => "default", clearBaseFields: ["ship", "code", "url", "name"], preserveSectionOnDefaultDelete: true, + resolveAllowFrom: (account) => account.dmAllowlist, + formatAllowFrom: (allowFrom) => + allowFrom.map((entry) => normalizeShip(String(entry))).filter(Boolean), }); export const tlonPlugin: ChannelPlugin = { @@ -72,7 +75,7 @@ export const tlonPlugin: ChannelPlugin = { reload: { configPrefixes: ["channels.tlon"] }, configSchema: tlonChannelConfigSchema, config: { - ...tlonConfigBase, + ...tlonConfigAdapter, isConfigured: (account) => account.configured, describeAccount: (account) => ({ accountId: account.accountId, diff --git a/extensions/whatsapp/src/shared.ts b/extensions/whatsapp/src/shared.ts index 2854db5d61f..9c3e3d50acf 100644 --- a/extensions/whatsapp/src/shared.ts +++ b/extensions/whatsapp/src/shared.ts @@ -1,8 +1,7 @@ import { collectAllowlistProviderGroupPolicyWarnings, collectOpenGroupPolicyRouteAllowlistWarnings, - createScopedAccountConfigAccessors, - createScopedChannelConfigBase, + createScopedChannelConfigAdapter, createScopedDmSecurityResolver, } from "openclaw/plugin-sdk/channel-config-helpers"; import { createChannelPluginBase } from "openclaw/plugin-sdk/core"; @@ -37,17 +36,13 @@ export const whatsappSetupWizardProxy = createWhatsAppSetupWizardProxy( async () => (await loadWhatsAppChannelRuntime()).whatsappSetupWizard, ); -const whatsappConfigBase = createScopedChannelConfigBase({ +const whatsappConfigAdapter = createScopedChannelConfigAdapter({ sectionKey: WHATSAPP_CHANNEL, listAccountIds: listWhatsAppAccountIds, resolveAccount: (cfg, accountId) => resolveWhatsAppAccount({ cfg, accountId }), defaultAccountId: resolveDefaultWhatsAppAccountId, clearBaseFields: [], allowTopLevel: false, -}); - -const whatsappConfigAccessors = createScopedAccountConfigAccessors({ - resolveAccount: ({ cfg, accountId }) => resolveWhatsAppAccount({ cfg, accountId }), resolveAllowFrom: (account) => account.allowFrom, formatAllowFrom: (allowFrom) => formatWhatsAppConfigAllowFromEntries(allowFrom), resolveDefaultTo: (account) => account.defaultTo, @@ -133,7 +128,7 @@ export function createWhatsAppPluginBase(params: { gatewayMethods: ["web.login.start", "web.login.wait"], configSchema: buildChannelConfigSchema(WhatsAppConfigSchema), config: { - ...whatsappConfigBase, + ...whatsappConfigAdapter, isEnabled: (account, cfg) => account.enabled && cfg.web?.enabled !== false, disabledReason: () => "disabled", isConfigured: params.isConfigured, @@ -147,7 +142,6 @@ export function createWhatsAppPluginBase(params: { dmPolicy: account.dmPolicy, allowFrom: account.allowFrom, }), - ...whatsappConfigAccessors, }, security: { resolveDmPolicy: whatsappResolveDmPolicy, diff --git a/extensions/zalo/src/channel.ts b/extensions/zalo/src/channel.ts index db34bb25400..57f74ca01d2 100644 --- a/extensions/zalo/src/channel.ts +++ b/extensions/zalo/src/channel.ts @@ -1,6 +1,5 @@ import { - createScopedAccountConfigAccessors, - createScopedChannelConfigBase, + createScopedChannelConfigAdapter, createScopedDmSecurityResolver, mapAllowFromEntries, } from "openclaw/plugin-sdk/channel-config-helpers"; @@ -62,19 +61,15 @@ function normalizeZaloMessagingTarget(raw: string): string | undefined { const loadZaloChannelRuntime = createLazyRuntimeModule(() => import("./channel.runtime.js")); -const zaloConfigAccessors = createScopedAccountConfigAccessors({ - resolveAccount: ({ cfg, accountId }) => resolveZaloAccount({ cfg, accountId }), - resolveAllowFrom: (account: ResolvedZaloAccount) => account.config.allowFrom, - formatAllowFrom: (allowFrom) => - formatAllowFromLowercase({ allowFrom, stripPrefixRe: /^(zalo|zl):/i }), -}); - -const zaloConfigBase = createScopedChannelConfigBase({ +const zaloConfigAdapter = createScopedChannelConfigAdapter({ sectionKey: "zalo", listAccountIds: listZaloAccountIds, resolveAccount: (cfg, accountId) => resolveZaloAccount({ cfg, accountId }), defaultAccountId: resolveDefaultZaloAccountId, clearBaseFields: ["botToken", "tokenFile", "name"], + resolveAllowFrom: (account: ResolvedZaloAccount) => account.config.allowFrom, + formatAllowFrom: (allowFrom) => + formatAllowFromLowercase({ allowFrom, stripPrefixRe: /^(zalo|zl):/i }), }); const resolveZaloDmPolicy = createScopedDmSecurityResolver({ @@ -102,7 +97,7 @@ export const zaloPlugin: ChannelPlugin = { reload: { configPrefixes: ["channels.zalo"] }, configSchema: buildChannelConfigSchema(ZaloConfigSchema), config: { - ...zaloConfigBase, + ...zaloConfigAdapter, isConfigured: (account) => Boolean(account.token?.trim()), describeAccount: (account): ChannelAccountSnapshot => ({ accountId: account.accountId, @@ -111,7 +106,6 @@ export const zaloPlugin: ChannelPlugin = { configured: Boolean(account.token?.trim()), tokenSource: account.tokenSource, }), - ...zaloConfigAccessors, }, security: { resolveDmPolicy: resolveZaloDmPolicy, diff --git a/extensions/zalouser/src/shared.ts b/extensions/zalouser/src/shared.ts index bac69441806..c48c80b4903 100644 --- a/extensions/zalouser/src/shared.ts +++ b/extensions/zalouser/src/shared.ts @@ -1,11 +1,6 @@ -import { mapAllowFromEntries } from "openclaw/plugin-sdk/channel-config-helpers"; +import { createScopedChannelConfigAdapter } from "openclaw/plugin-sdk/channel-config-helpers"; import type { ChannelPlugin } from "openclaw/plugin-sdk/zalouser"; -import { - buildChannelConfigSchema, - deleteAccountFromConfigSection, - formatAllowFromLowercase, - setAccountEnabledInConfigSection, -} from "openclaw/plugin-sdk/zalouser"; +import { buildChannelConfigSchema, formatAllowFromLowercase } from "openclaw/plugin-sdk/zalouser"; import { listZalouserAccountIds, resolveDefaultZalouserAccountId, @@ -27,6 +22,27 @@ export const zalouserMeta = { quickstartAllowFrom: false, } satisfies ChannelPlugin["meta"]; +const zalouserConfigAdapter = createScopedChannelConfigAdapter({ + sectionKey: "zalouser", + listAccountIds: listZalouserAccountIds, + resolveAccount: (cfg, accountId) => resolveZalouserAccountSync({ cfg, accountId }), + defaultAccountId: resolveDefaultZalouserAccountId, + clearBaseFields: [ + "profile", + "name", + "dmPolicy", + "allowFrom", + "historyLimit", + "groupAllowFrom", + "groupPolicy", + "groups", + "messagePrefix", + ], + resolveAllowFrom: (account) => account.config.allowFrom, + formatAllowFrom: (allowFrom) => + formatAllowFromLowercase({ allowFrom, stripPrefixRe: /^(zalouser|zlu):/i }), +}); + export function createZalouserPluginBase(params: { setupWizard: NonNullable["setupWizard"]>; setup: NonNullable["setup"]>; @@ -50,34 +66,7 @@ export function createZalouserPluginBase(params: { reload: { configPrefixes: ["channels.zalouser"] }, configSchema: buildChannelConfigSchema(ZalouserConfigSchema), config: { - listAccountIds: (cfg) => listZalouserAccountIds(cfg), - resolveAccount: (cfg, accountId) => resolveZalouserAccountSync({ cfg, accountId }), - defaultAccountId: (cfg) => resolveDefaultZalouserAccountId(cfg), - setAccountEnabled: ({ cfg, accountId, enabled }) => - setAccountEnabledInConfigSection({ - cfg, - sectionKey: "zalouser", - accountId, - enabled, - allowTopLevel: true, - }), - deleteAccount: ({ cfg, accountId }) => - deleteAccountFromConfigSection({ - cfg, - sectionKey: "zalouser", - accountId, - clearBaseFields: [ - "profile", - "name", - "dmPolicy", - "allowFrom", - "historyLimit", - "groupAllowFrom", - "groupPolicy", - "groups", - "messagePrefix", - ], - }), + ...zalouserConfigAdapter, isConfigured: async (account) => await checkZcaAuthenticated(account.profile), describeAccount: (account) => ({ accountId: account.accountId, @@ -85,10 +74,6 @@ export function createZalouserPluginBase(params: { enabled: account.enabled, configured: undefined, }), - resolveAllowFrom: ({ cfg, accountId }) => - mapAllowFromEntries(resolveZalouserAccountSync({ cfg, accountId }).config.allowFrom), - formatAllowFrom: ({ allowFrom }) => - formatAllowFromLowercase({ allowFrom, stripPrefixRe: /^(zalouser|zlu):/i }), }, setup: params.setup, }; diff --git a/src/plugin-sdk/channel-config-helpers.test.ts b/src/plugin-sdk/channel-config-helpers.test.ts index 7753c4c745e..296b8bf9a8e 100644 --- a/src/plugin-sdk/channel-config-helpers.test.ts +++ b/src/plugin-sdk/channel-config-helpers.test.ts @@ -1,8 +1,11 @@ import { describe, expect, it } from "vitest"; import { createScopedAccountConfigAccessors, + createScopedChannelConfigAdapter, createScopedChannelConfigBase, createScopedDmSecurityResolver, + createHybridChannelConfigAdapter, + createTopLevelChannelConfigAdapter, createTopLevelChannelConfigBase, createHybridChannelConfigBase, mapAllowFromEntries, @@ -160,6 +163,41 @@ describe("createScopedChannelConfigBase", () => { }); }); +describe("createScopedChannelConfigAdapter", () => { + it("combines scoped CRUD and allowFrom accessors", () => { + const adapter = createScopedChannelConfigAdapter({ + sectionKey: "demo", + listAccountIds: () => ["default", "alt"], + resolveAccount: (_cfg, accountId) => ({ + accountId: accountId ?? "default", + allowFrom: accountId ? [accountId] : ["fallback"], + defaultTo: " room:123 ", + }), + defaultAccountId: () => "default", + clearBaseFields: ["token"], + resolveAllowFrom: (account) => account.allowFrom, + formatAllowFrom: (allowFrom) => allowFrom.map((entry) => String(entry).toUpperCase()), + resolveDefaultTo: (account) => account.defaultTo, + }); + + expect(adapter.listAccountIds({})).toEqual(["default", "alt"]); + expect(adapter.resolveAccount({}, "alt")).toEqual({ + accountId: "alt", + allowFrom: ["alt"], + defaultTo: " room:123 ", + }); + expect(adapter.resolveAllowFrom?.({ cfg: {}, accountId: "alt" })).toEqual(["alt"]); + expect(adapter.resolveDefaultTo?.({ cfg: {}, accountId: "alt" })).toBe("room:123"); + expect( + adapter.setAccountEnabled!({ + cfg: {}, + accountId: "default", + enabled: true, + }).channels?.demo, + ).toEqual({ enabled: true }); + }); +}); + describe("createScopedDmSecurityResolver", () => { it("builds account-aware DM policy payloads", () => { const resolveDmPolicy = createScopedDmSecurityResolver<{ @@ -232,6 +270,69 @@ describe("createTopLevelChannelConfigBase", () => { }).channels, ).toBeUndefined(); }); + + it("can clear only account-scoped fields while preserving channel settings", () => { + const base = createTopLevelChannelConfigBase({ + sectionKey: "demo", + resolveAccount: () => ({ accountId: "default" }), + deleteMode: "clear-fields", + clearBaseFields: ["token", "allowFrom"], + }); + + expect( + base.deleteAccount!({ + cfg: { + channels: { + demo: { + token: "secret", + allowFrom: ["owner"], + markdown: { tables: false }, + }, + }, + }, + accountId: "default", + }).channels?.demo, + ).toEqual({ + markdown: { tables: false }, + }); + }); +}); + +describe("createTopLevelChannelConfigAdapter", () => { + it("combines top-level CRUD with separate accessor account resolution", () => { + const adapter = createTopLevelChannelConfigAdapter< + { accountId: string; enabled: boolean }, + { allowFrom: string[]; defaultTo: string } + >({ + sectionKey: "demo", + resolveAccount: () => ({ accountId: "default", enabled: true }), + resolveAccessorAccount: () => ({ allowFrom: ["owner"], defaultTo: " chat:123 " }), + deleteMode: "clear-fields", + clearBaseFields: ["token"], + resolveAllowFrom: (account) => account.allowFrom, + formatAllowFrom: (allowFrom) => allowFrom.map((entry) => String(entry)), + resolveDefaultTo: (account) => account.defaultTo, + }); + + expect(adapter.resolveAccount({})).toEqual({ accountId: "default", enabled: true }); + expect(adapter.resolveAllowFrom?.({ cfg: {} })).toEqual(["owner"]); + expect(adapter.resolveDefaultTo?.({ cfg: {} })).toBe("chat:123"); + expect( + adapter.deleteAccount!({ + cfg: { + channels: { + demo: { + token: "secret", + markdown: { tables: false }, + }, + }, + }, + accountId: "default", + }).channels?.demo, + ).toEqual({ + markdown: { tables: false }, + }); + }); }); describe("createHybridChannelConfigBase", () => { @@ -309,3 +410,54 @@ describe("createHybridChannelConfigBase", () => { }); }); }); + +describe("createHybridChannelConfigAdapter", () => { + it("combines hybrid CRUD with allowFrom/defaultTo accessors", () => { + const adapter = createHybridChannelConfigAdapter< + { accountId: string; enabled: boolean }, + { allowFrom: string[]; defaultTo: string } + >({ + sectionKey: "demo", + listAccountIds: () => ["default", "alt"], + resolveAccount: (_cfg, accountId) => ({ + accountId: accountId ?? "default", + enabled: true, + }), + resolveAccessorAccount: ({ accountId }) => ({ + allowFrom: [accountId ?? "default"], + defaultTo: " room:123 ", + }), + defaultAccountId: () => "default", + clearBaseFields: ["token"], + preserveSectionOnDefaultDelete: true, + resolveAllowFrom: (account) => account.allowFrom, + formatAllowFrom: (allowFrom) => allowFrom.map((entry) => String(entry).toUpperCase()), + resolveDefaultTo: (account) => account.defaultTo, + }); + + expect(adapter.resolveAllowFrom?.({ cfg: {}, accountId: "alt" })).toEqual(["alt"]); + expect(adapter.resolveDefaultTo?.({ cfg: {}, accountId: "alt" })).toBe("room:123"); + expect( + adapter.setAccountEnabled!({ + cfg: {}, + accountId: "default", + enabled: true, + }).channels?.demo, + ).toEqual({ enabled: true }); + expect( + adapter.deleteAccount!({ + cfg: { + channels: { + demo: { + token: "secret", + markdown: { tables: false }, + }, + }, + }, + accountId: "default", + }).channels?.demo, + ).toEqual({ + markdown: { tables: false }, + }); + }); +}); diff --git a/src/plugin-sdk/channel-config-helpers.ts b/src/plugin-sdk/channel-config-helpers.ts index af6813e13a1..ee18f8bc9c9 100644 --- a/src/plugin-sdk/channel-config-helpers.ts +++ b/src/plugin-sdk/channel-config-helpers.ts @@ -116,6 +116,59 @@ export function createScopedChannelConfigBase< }; } +/** Build the full shared config adapter for account-scoped channels with allowlist/default target accessors. */ +export function createScopedChannelConfigAdapter< + ResolvedAccount, + AccessorAccount = ResolvedAccount, + Config extends OpenClawConfig = OpenClawConfig, +>(params: { + sectionKey: string; + listAccountIds: (cfg: Config) => string[]; + resolveAccount: (cfg: Config, accountId?: string | null) => ResolvedAccount; + resolveAccessorAccount?: (params: { cfg: Config; accountId?: string | null }) => AccessorAccount; + defaultAccountId: (cfg: Config) => string; + inspectAccount?: (cfg: Config, accountId?: string | null) => unknown; + clearBaseFields: string[]; + allowTopLevel?: boolean; + resolveAllowFrom: (account: AccessorAccount) => Array | null | undefined; + formatAllowFrom: (allowFrom: Array) => string[]; + resolveDefaultTo?: (account: AccessorAccount) => string | number | null | undefined; +}): Pick< + ChannelConfigAdapter, + | "listAccountIds" + | "resolveAccount" + | "inspectAccount" + | "defaultAccountId" + | "setAccountEnabled" + | "deleteAccount" + | "resolveAllowFrom" + | "formatAllowFrom" + | "resolveDefaultTo" +> { + const resolveAccessorAccount = + params.resolveAccessorAccount ?? + (({ cfg, accountId }: { cfg: Config; accountId?: string | null }) => + params.resolveAccount(cfg, accountId) as unknown as AccessorAccount); + + return { + ...createScopedChannelConfigBase({ + sectionKey: params.sectionKey, + listAccountIds: params.listAccountIds, + resolveAccount: params.resolveAccount, + inspectAccount: params.inspectAccount, + defaultAccountId: params.defaultAccountId, + clearBaseFields: params.clearBaseFields, + allowTopLevel: params.allowTopLevel, + }), + ...createScopedAccountConfigAccessors({ + resolveAccount: resolveAccessorAccount, + resolveAllowFrom: params.resolveAllowFrom, + formatAllowFrom: params.formatAllowFrom, + resolveDefaultTo: params.resolveDefaultTo, + }), + }; +} + function setTopLevelChannelEnabledInConfigSection(params: { cfg: Config; sectionKey: string; @@ -219,6 +272,59 @@ export function createTopLevelChannelConfigBase< }; } +/** Build the full shared config adapter for top-level single-account channels with allowlist/default target accessors. */ +export function createTopLevelChannelConfigAdapter< + ResolvedAccount, + AccessorAccount = ResolvedAccount, + Config extends OpenClawConfig = OpenClawConfig, +>(params: { + sectionKey: string; + resolveAccount: (cfg: Config) => ResolvedAccount; + resolveAccessorAccount?: (params: { cfg: Config; accountId?: string | null }) => AccessorAccount; + listAccountIds?: (cfg: Config) => string[]; + defaultAccountId?: (cfg: Config) => string; + inspectAccount?: (cfg: Config) => unknown; + deleteMode?: "remove-section" | "clear-fields"; + clearBaseFields?: string[]; + resolveAllowFrom: (account: AccessorAccount) => Array | null | undefined; + formatAllowFrom: (allowFrom: Array) => string[]; + resolveDefaultTo?: (account: AccessorAccount) => string | number | null | undefined; +}): Pick< + ChannelConfigAdapter, + | "listAccountIds" + | "resolveAccount" + | "inspectAccount" + | "defaultAccountId" + | "setAccountEnabled" + | "deleteAccount" + | "resolveAllowFrom" + | "formatAllowFrom" + | "resolveDefaultTo" +> { + const resolveAccessorAccount = + params.resolveAccessorAccount ?? + (({ cfg }: { cfg: Config; accountId?: string | null }) => + params.resolveAccount(cfg) as unknown as AccessorAccount); + + return { + ...createTopLevelChannelConfigBase({ + sectionKey: params.sectionKey, + resolveAccount: params.resolveAccount, + listAccountIds: params.listAccountIds, + defaultAccountId: params.defaultAccountId, + inspectAccount: params.inspectAccount, + deleteMode: params.deleteMode, + clearBaseFields: params.clearBaseFields, + }), + ...createScopedAccountConfigAccessors({ + resolveAccount: resolveAccessorAccount, + resolveAllowFrom: params.resolveAllowFrom, + formatAllowFrom: params.formatAllowFrom, + resolveDefaultTo: params.resolveDefaultTo, + }), + }; +} + /** Build CRUD/config helpers for channels where the default account lives at channel root and named accounts live under `accounts`. */ export function createHybridChannelConfigBase< ResolvedAccount, @@ -288,6 +394,59 @@ export function createHybridChannelConfigBase< }; } +/** Build the full shared config adapter for hybrid channels with allowlist/default target accessors. */ +export function createHybridChannelConfigAdapter< + ResolvedAccount, + AccessorAccount = ResolvedAccount, + Config extends OpenClawConfig = OpenClawConfig, +>(params: { + sectionKey: string; + listAccountIds: (cfg: Config) => string[]; + resolveAccount: (cfg: Config, accountId?: string | null) => ResolvedAccount; + resolveAccessorAccount?: (params: { cfg: Config; accountId?: string | null }) => AccessorAccount; + defaultAccountId: (cfg: Config) => string; + inspectAccount?: (cfg: Config, accountId?: string | null) => unknown; + clearBaseFields: string[]; + preserveSectionOnDefaultDelete?: boolean; + resolveAllowFrom: (account: AccessorAccount) => Array | null | undefined; + formatAllowFrom: (allowFrom: Array) => string[]; + resolveDefaultTo?: (account: AccessorAccount) => string | number | null | undefined; +}): Pick< + ChannelConfigAdapter, + | "listAccountIds" + | "resolveAccount" + | "inspectAccount" + | "defaultAccountId" + | "setAccountEnabled" + | "deleteAccount" + | "resolveAllowFrom" + | "formatAllowFrom" + | "resolveDefaultTo" +> { + const resolveAccessorAccount = + params.resolveAccessorAccount ?? + (({ cfg, accountId }: { cfg: Config; accountId?: string | null }) => + params.resolveAccount(cfg, accountId) as unknown as AccessorAccount); + + return { + ...createHybridChannelConfigBase({ + sectionKey: params.sectionKey, + listAccountIds: params.listAccountIds, + resolveAccount: params.resolveAccount, + inspectAccount: params.inspectAccount, + defaultAccountId: params.defaultAccountId, + clearBaseFields: params.clearBaseFields, + preserveSectionOnDefaultDelete: params.preserveSectionOnDefaultDelete, + }), + ...createScopedAccountConfigAccessors({ + resolveAccount: resolveAccessorAccount, + resolveAllowFrom: params.resolveAllowFrom, + formatAllowFrom: params.formatAllowFrom, + resolveDefaultTo: params.resolveDefaultTo, + }), + }; +} + /** Convert account-specific DM security fields into the shared runtime policy resolver shape. */ export function createScopedDmSecurityResolver< ResolvedAccount extends { accountId?: string | null }, diff --git a/src/plugin-sdk/compat.ts b/src/plugin-sdk/compat.ts index 2ce0c6a2c3b..9892bbc8fc7 100644 --- a/src/plugin-sdk/compat.ts +++ b/src/plugin-sdk/compat.ts @@ -25,10 +25,13 @@ export { createPluginRuntimeStore } from "./runtime-store.js"; export { KeyedAsyncQueue } from "./keyed-async-queue.js"; export { + createHybridChannelConfigAdapter, createHybridChannelConfigBase, createScopedAccountConfigAccessors, + createScopedChannelConfigAdapter, createScopedChannelConfigBase, createScopedDmSecurityResolver, + createTopLevelChannelConfigAdapter, createTopLevelChannelConfigBase, mapAllowFromEntries, } from "./channel-config-helpers.js"; diff --git a/src/plugin-sdk/subpaths.test.ts b/src/plugin-sdk/subpaths.test.ts index 1b31ed580e4..606e7b623f8 100644 --- a/src/plugin-sdk/subpaths.test.ts +++ b/src/plugin-sdk/subpaths.test.ts @@ -62,6 +62,9 @@ describe("plugin-sdk subpath exports", () => { it("exports compat helpers", () => { expect(typeof compatSdk.emptyPluginConfigSchema).toBe("function"); expect(typeof compatSdk.resolveControlCommandGate).toBe("function"); + expect(typeof compatSdk.createScopedChannelConfigAdapter).toBe("function"); + expect(typeof compatSdk.createTopLevelChannelConfigAdapter).toBe("function"); + expect(typeof compatSdk.createHybridChannelConfigAdapter).toBe("function"); }); it("keeps core focused on generic shared exports", () => {