refactor(signal): share plugin base config

This commit is contained in:
Peter Steinberger
2026-03-17 03:07:28 +00:00
parent 3cc1c7ba83
commit a8853d23ef
3 changed files with 149 additions and 178 deletions

View File

@@ -1,94 +1,9 @@
import {
buildAccountScopedDmSecurityPolicy,
collectAllowlistProviderRestrictSendersWarnings,
} from "../../../src/plugin-sdk-internal/channel-config.js";
import {
buildChannelConfigSchema,
DEFAULT_ACCOUNT_ID,
deleteAccountFromConfigSection,
getChatChannelMeta,
normalizeE164,
setAccountEnabledInConfigSection,
SignalConfigSchema,
type ChannelPlugin,
} from "../../../src/plugin-sdk-internal/signal.js";
import {
listSignalAccountIds,
resolveDefaultSignalAccountId,
resolveSignalAccount,
type ResolvedSignalAccount,
} from "./accounts.js";
import { signalConfigAccessors, signalSetupWizard } from "./plugin-shared.js";
import { type ChannelPlugin } from "openclaw/plugin-sdk/signal";
import { type ResolvedSignalAccount } from "./accounts.js";
import { signalSetupAdapter } from "./setup-core.js";
import { createSignalPluginBase, signalSetupWizard } from "./shared.js";
export const signalSetupPlugin: ChannelPlugin<ResolvedSignalAccount> = {
id: "signal",
meta: {
...getChatChannelMeta("signal"),
},
export const signalSetupPlugin: ChannelPlugin<ResolvedSignalAccount> = createSignalPluginBase({
setupWizard: signalSetupWizard,
capabilities: {
chatTypes: ["direct", "group"],
media: true,
reactions: true,
},
streaming: {
blockStreamingCoalesceDefaults: { minChars: 1500, idleMs: 1000 },
},
reload: { configPrefixes: ["channels.signal"] },
configSchema: buildChannelConfigSchema(SignalConfigSchema),
config: {
listAccountIds: (cfg) => listSignalAccountIds(cfg),
resolveAccount: (cfg, accountId) => resolveSignalAccount({ cfg, accountId }),
defaultAccountId: (cfg) => resolveDefaultSignalAccountId(cfg),
setAccountEnabled: ({ cfg, accountId, enabled }) =>
setAccountEnabledInConfigSection({
cfg,
sectionKey: "signal",
accountId,
enabled,
allowTopLevel: true,
}),
deleteAccount: ({ cfg, accountId }) =>
deleteAccountFromConfigSection({
cfg,
sectionKey: "signal",
accountId,
clearBaseFields: ["account", "httpUrl", "httpHost", "httpPort", "cliPath", "name"],
}),
isConfigured: (account) => account.configured,
describeAccount: (account) => ({
accountId: account.accountId,
name: account.name,
enabled: account.enabled,
configured: account.configured,
baseUrl: account.baseUrl,
}),
...signalConfigAccessors,
},
security: {
resolveDmPolicy: ({ cfg, accountId, account }) =>
buildAccountScopedDmSecurityPolicy({
cfg,
channelKey: "signal",
accountId,
fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID,
policy: account.config.dmPolicy,
allowFrom: account.config.allowFrom ?? [],
policyPathSuffix: "dmPolicy",
normalizeEntry: (raw) => normalizeE164(raw.replace(/^signal:/i, "").trim()),
}),
collectWarnings: ({ account, cfg }) =>
collectAllowlistProviderRestrictSendersWarnings({
cfg,
providerConfigPresent: cfg.channels?.signal !== undefined,
configuredGroupPolicy: account.config.groupPolicy,
surface: "Signal groups",
openScope: "any member",
groupPolicyPath: "channels.signal.groupPolicy",
groupAllowFromPath: "channels.signal.groupAllowFrom",
mentionGated: false,
}),
},
setup: signalSetupAdapter,
};
});

View File

@@ -1,31 +1,21 @@
import { resolveTextChunkLimit } from "../../../src/auto-reply/chunk.js";
import { resolveMarkdownTableMode } from "../../../src/config/markdown-tables.js";
import { resolveOutboundSendDep } from "../../../src/infra/outbound/send-deps.js";
import {
buildAccountScopedAllowlistConfigEditor,
buildAccountScopedDmSecurityPolicy,
collectAllowlistProviderRestrictSendersWarnings,
} from "../../../src/plugin-sdk-internal/channel-config.js";
import { buildAgentSessionKey, type RoutePeer } from "../../../src/plugin-sdk-internal/core.js";
import { buildAccountScopedAllowlistConfigEditor } from "openclaw/plugin-sdk/compat";
import { buildAgentSessionKey, type RoutePeer } from "openclaw/plugin-sdk/core";
import {
buildBaseAccountStatusSnapshot,
buildBaseChannelStatusSummary,
buildChannelConfigSchema,
collectStatusIssuesFromLastError,
createDefaultChannelRuntimeState,
DEFAULT_ACCOUNT_ID,
deleteAccountFromConfigSection,
getChatChannelMeta,
looksLikeSignalTargetId,
normalizeE164,
normalizeSignalMessagingTarget,
PAIRING_APPROVED_MESSAGE,
resolveChannelMediaMaxBytes,
setAccountEnabledInConfigSection,
SignalConfigSchema,
type ChannelMessageActionAdapter,
type ChannelPlugin,
} from "../../../src/plugin-sdk-internal/signal.js";
} from "openclaw/plugin-sdk/signal";
import { resolveTextChunkLimit } from "../../../src/auto-reply/chunk.js";
import { resolveMarkdownTableMode } from "../../../src/config/markdown-tables.js";
import { resolveOutboundSendDep } from "../../../src/infra/outbound/send-deps.js";
import {
listSignalAccountIds,
resolveDefaultSignalAccountId,
@@ -39,10 +29,10 @@ import {
resolveSignalRecipient,
resolveSignalSender,
} from "./identity.js";
import { signalConfigAccessors, signalSetupWizard } from "./plugin-shared.js";
import type { SignalProbe } from "./probe.js";
import { getSignalRuntime } from "./runtime.js";
import { signalSetupAdapter } from "./setup-core.js";
import { createSignalPluginBase, signalConfigAccessors, signalSetupWizard } from "./shared.js";
const signalMessageActions: ChannelMessageActionAdapter = {
listActions: (ctx) => getSignalRuntime().channel.signal.messageActions?.listActions?.(ctx) ?? [],
@@ -292,11 +282,10 @@ async function sendFormattedSignalMedia(ctx: {
}
export const signalPlugin: ChannelPlugin<ResolvedSignalAccount> = {
id: "signal",
meta: {
...getChatChannelMeta("signal"),
},
setupWizard: signalSetupWizard,
...createSignalPluginBase({
setupWizard: signalSetupWizard,
setup: signalSetupAdapter,
}),
pairing: {
idLabel: "signalNumber",
normalizeAllowEntry: (entry) => entry.replace(/^signal:/i, ""),
@@ -304,46 +293,7 @@ export const signalPlugin: ChannelPlugin<ResolvedSignalAccount> = {
await getSignalRuntime().channel.signal.sendMessageSignal(id, PAIRING_APPROVED_MESSAGE);
},
},
capabilities: {
chatTypes: ["direct", "group"],
media: true,
reactions: true,
},
actions: signalMessageActions,
streaming: {
blockStreamingCoalesceDefaults: { minChars: 1500, idleMs: 1000 },
},
reload: { configPrefixes: ["channels.signal"] },
configSchema: buildChannelConfigSchema(SignalConfigSchema),
config: {
listAccountIds: (cfg) => listSignalAccountIds(cfg),
resolveAccount: (cfg, accountId) => resolveSignalAccount({ cfg, accountId }),
defaultAccountId: (cfg) => resolveDefaultSignalAccountId(cfg),
setAccountEnabled: ({ cfg, accountId, enabled }) =>
setAccountEnabledInConfigSection({
cfg,
sectionKey: "signal",
accountId,
enabled,
allowTopLevel: true,
}),
deleteAccount: ({ cfg, accountId }) =>
deleteAccountFromConfigSection({
cfg,
sectionKey: "signal",
accountId,
clearBaseFields: ["account", "httpUrl", "httpHost", "httpPort", "cliPath", "name"],
}),
isConfigured: (account) => account.configured,
describeAccount: (account) => ({
accountId: account.accountId,
name: account.name,
enabled: account.enabled,
configured: account.configured,
baseUrl: account.baseUrl,
}),
...signalConfigAccessors,
},
allowlist: {
supportsScope: ({ scope }) => scope === "dm" || scope === "group" || scope === "all",
readConfig: ({ cfg, accountId }) => {
@@ -365,32 +315,6 @@ export const signalPlugin: ChannelPlugin<ResolvedSignalAccount> = {
}),
}),
},
security: {
resolveDmPolicy: ({ cfg, accountId, account }) => {
return buildAccountScopedDmSecurityPolicy({
cfg,
channelKey: "signal",
accountId,
fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID,
policy: account.config.dmPolicy,
allowFrom: account.config.allowFrom ?? [],
policyPathSuffix: "dmPolicy",
normalizeEntry: (raw) => normalizeE164(raw.replace(/^signal:/i, "").trim()),
});
},
collectWarnings: ({ account, cfg }) => {
return collectAllowlistProviderRestrictSendersWarnings({
cfg,
providerConfigPresent: cfg.channels?.signal !== undefined,
configuredGroupPolicy: account.config.groupPolicy,
surface: "Signal groups",
openScope: "any member",
groupPolicyPath: "channels.signal.groupPolicy",
groupAllowFromPath: "channels.signal.groupAllowFrom",
mentionGated: false,
});
},
},
messaging: {
normalizeTarget: normalizeSignalMessagingTarget,
parseExplicitTarget: ({ raw }) => parseSignalExplicitTarget(raw),
@@ -401,7 +325,6 @@ export const signalPlugin: ChannelPlugin<ResolvedSignalAccount> = {
hint: "<E.164|uuid:ID|group:ID|signal:group:ID|signal:+E.164>",
},
},
setup: signalSetupAdapter,
outbound: {
deliveryMode: "direct",
chunker: (text, limit) => getSignalRuntime().channel.text.chunkText(text, limit),

View File

@@ -0,0 +1,133 @@
import {
buildAccountScopedDmSecurityPolicy,
collectAllowlistProviderRestrictSendersWarnings,
createScopedAccountConfigAccessors,
} from "openclaw/plugin-sdk/compat";
import {
buildChannelConfigSchema,
DEFAULT_ACCOUNT_ID,
deleteAccountFromConfigSection,
getChatChannelMeta,
normalizeE164,
setAccountEnabledInConfigSection,
SignalConfigSchema,
type ChannelPlugin,
} from "openclaw/plugin-sdk/signal";
import {
listSignalAccountIds,
resolveDefaultSignalAccountId,
resolveSignalAccount,
type ResolvedSignalAccount,
} from "./accounts.js";
import { createSignalSetupWizardProxy } from "./setup-core.js";
export const SIGNAL_CHANNEL = "signal" as const;
async function loadSignalChannelRuntime() {
return await import("./channel.runtime.js");
}
export const signalSetupWizard = createSignalSetupWizardProxy(async () => ({
signalSetupWizard: (await loadSignalChannelRuntime()).signalSetupWizard,
}));
export const signalConfigAccessors = createScopedAccountConfigAccessors({
resolveAccount: ({ cfg, accountId }) => resolveSignalAccount({ cfg, accountId }),
resolveAllowFrom: (account: ResolvedSignalAccount) => account.config.allowFrom,
formatAllowFrom: (allowFrom) =>
allowFrom
.map((entry) => String(entry).trim())
.filter(Boolean)
.map((entry) => (entry === "*" ? "*" : normalizeE164(entry.replace(/^signal:/i, ""))))
.filter(Boolean),
resolveDefaultTo: (account: ResolvedSignalAccount) => account.config.defaultTo,
});
export function createSignalPluginBase(params: {
setupWizard?: NonNullable<ChannelPlugin<ResolvedSignalAccount>["setupWizard"]>;
setup: NonNullable<ChannelPlugin<ResolvedSignalAccount>["setup"]>;
}): Pick<
ChannelPlugin<ResolvedSignalAccount>,
| "id"
| "meta"
| "setupWizard"
| "capabilities"
| "streaming"
| "reload"
| "configSchema"
| "config"
| "security"
| "setup"
> {
return {
id: SIGNAL_CHANNEL,
meta: {
...getChatChannelMeta(SIGNAL_CHANNEL),
},
setupWizard: params.setupWizard,
capabilities: {
chatTypes: ["direct", "group"],
media: true,
reactions: true,
},
streaming: {
blockStreamingCoalesceDefaults: { minChars: 1500, idleMs: 1000 },
},
reload: { configPrefixes: ["channels.signal"] },
configSchema: buildChannelConfigSchema(SignalConfigSchema),
config: {
listAccountIds: (cfg) => listSignalAccountIds(cfg),
resolveAccount: (cfg, accountId) => resolveSignalAccount({ cfg, accountId }),
defaultAccountId: (cfg) => resolveDefaultSignalAccountId(cfg),
setAccountEnabled: ({ cfg, accountId, enabled }) =>
setAccountEnabledInConfigSection({
cfg,
sectionKey: SIGNAL_CHANNEL,
accountId,
enabled,
allowTopLevel: true,
}),
deleteAccount: ({ cfg, accountId }) =>
deleteAccountFromConfigSection({
cfg,
sectionKey: SIGNAL_CHANNEL,
accountId,
clearBaseFields: ["account", "httpUrl", "httpHost", "httpPort", "cliPath", "name"],
}),
isConfigured: (account) => account.configured,
describeAccount: (account) => ({
accountId: account.accountId,
name: account.name,
enabled: account.enabled,
configured: account.configured,
baseUrl: account.baseUrl,
}),
...signalConfigAccessors,
},
security: {
resolveDmPolicy: ({ cfg, accountId, account }) =>
buildAccountScopedDmSecurityPolicy({
cfg,
channelKey: SIGNAL_CHANNEL,
accountId,
fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID,
policy: account.config.dmPolicy,
allowFrom: account.config.allowFrom ?? [],
policyPathSuffix: "dmPolicy",
normalizeEntry: (raw) => normalizeE164(raw.replace(/^signal:/i, "").trim()),
}),
collectWarnings: ({ account, cfg }) =>
collectAllowlistProviderRestrictSendersWarnings({
cfg,
providerConfigPresent: cfg.channels?.signal !== undefined,
configuredGroupPolicy: account.config.groupPolicy,
surface: "Signal groups",
openScope: "any member",
groupPolicyPath: "channels.signal.groupPolicy",
groupAllowFromPath: "channels.signal.groupAllowFrom",
mentionGated: false,
}),
},
setup: params.setup,
};
}