UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

456 lines (455 loc) 16.4 kB
import { a as normalizeLowercaseStringOrEmpty, d as normalizeStringifiedEntries } from "./string-coerce-mnp54Vah.js"; import { l as normalizeE164 } from "./utils-CCC-BEJH.js"; import { t as DEFAULT_ACCOUNT_ID } from "./account-id-Df9e41E6.js"; import { a as chunkText, c as resolveTextChunkLimit } from "./chunk-Cpb8JO1x.js"; import { n as resolveOutboundSendDep } from "./send-deps-DjbvQHZ4.js"; import { s as createScopedChannelConfigAdapter, t as adaptScopedAccountAccessor } from "./channel-config-helpers-lj5quus3.js"; import { t as buildOutboundBaseSessionKey } from "./base-session-key-BpTC-eng.js"; import { a as resolveChannelMediaMaxBytes } from "./media-runtime-fgpmX6eL.js"; import "./string-coerce-runtime-CEGJWkQ_.js"; import "./text-utility-runtime-BZ3jGr-e.js"; import { n as describeAccountSnapshot } from "./account-helpers-pcH3lGFY.js"; import { c as getChatChannelMeta, i as createChatChannelPlugin, r as createChannelPluginBase } from "./core-DSxVv-v1.js"; import "./channel-core-IukkNCd-.js"; import "./routing-CQ63ICPX.js"; import { t as resolveMarkdownTableMode } from "./markdown-tables-LfSv7QSx.js"; import { r as createRestrictSendersChannelSecurity } from "./channel-policy-CIW58SA5.js"; import "./reply-chunking-C9pgDFVG.js"; import { T as defineChannelMessageAdapter } from "./channel-outbound-B3_Zy-kG.js"; import "./markdown-table-runtime-j_C4iTF_.js"; import { n as buildDmGroupAccountAllowlistAdapter } from "./allowlist-config-edit-dX8vJeki.js"; import { t as PAIRING_APPROVED_MESSAGE } from "./pairing-message-DNhqI-OE.js"; import { c as collectStatusIssuesFromLastError, d as createDefaultChannelRuntimeState, n as buildBaseChannelStatusSummary, u as createComputedAccountStatusAdapter } from "./status-helpers-BevxX6Pu.js"; import "./channel-status-DxlXr0Xh.js"; import { i as createPairingPrefixStripper } from "./channel-pairing-Bk6_JhTm.js"; import { n as attachChannelToResults, t as attachChannelToResult } from "./channel-send-result-Dn_C6AJS.js"; import { i as resolveSignalAccount, n as listSignalAccountIds, r as resolveDefaultSignalAccountId } from "./accounts-ms-4J9iY.js"; import { c as resolveSignalSender, d as normalizeSignalMessagingTarget, l as looksLikeUuid, o as resolveSignalPeerId, s as resolveSignalRecipient, u as looksLikeSignalTargetId } from "./identity-IGz-X4Li.js"; import { n as markdownToSignalTextChunks } from "./format-B-0MMYcV.js"; import { n as shouldSuppressLocalSignalExecApprovalPrompt, r as signalApprovalCapability } from "./approval-native-DiTxhcNB.js"; import { n as resolveSignalReactionLevel, t as signalMessageActions } from "./message-actions-DetJujbK.js"; import { n as createSignalSetupWizardProxy, s as signalSetupAdapter } from "./setup-core-DU4Kigam.js"; import { t as SignalChannelConfigSchema } from "./config-schema-CZNPGE3A.js"; //#region extensions/signal/src/outbound-session.ts function resolveSignalOutboundTarget(target) { const stripped = target.replace(/^signal:/i, "").trim(); const lowered = normalizeLowercaseStringOrEmpty(stripped); if (lowered.startsWith("group:")) { const groupId = stripped.slice(6).trim(); if (!groupId) return null; return { peer: { kind: "group", id: groupId }, chatType: "group", from: `group:${groupId}`, to: `group:${groupId}` }; } let recipient = stripped.trim(); if (lowered.startsWith("username:")) recipient = stripped.slice(9).trim(); else if (lowered.startsWith("u:")) recipient = stripped.slice(2).trim(); if (!recipient) return null; const uuidCandidate = normalizeLowercaseStringOrEmpty(recipient).startsWith("uuid:") ? recipient.slice(5) : recipient; const sender = resolveSignalSender({ sourceUuid: looksLikeUuid(uuidCandidate) ? uuidCandidate : null, sourceNumber: looksLikeUuid(uuidCandidate) ? null : recipient }); const peerId = sender ? resolveSignalPeerId(sender) : recipient; const displayRecipient = sender ? resolveSignalRecipient(sender) : recipient; return { peer: { kind: "direct", id: peerId }, chatType: "direct", from: `signal:${displayRecipient}`, to: `signal:${displayRecipient}` }; } //#endregion //#region extensions/signal/src/shared.ts const SIGNAL_CHANNEL = "signal"; async function loadSignalChannelRuntime() { return await import("./channel.runtime-Cr6I93IS.js"); } const signalSetupWizard = createSignalSetupWizardProxy(async () => (await loadSignalChannelRuntime()).signalSetupWizard); const signalConfigAdapter = createScopedChannelConfigAdapter({ sectionKey: SIGNAL_CHANNEL, listAccountIds: (cfg) => listSignalAccountIds(cfg), resolveAccount: adaptScopedAccountAccessor((params) => resolveSignalAccount(params)), defaultAccountId: (cfg) => resolveDefaultSignalAccountId(cfg), clearBaseFields: [ "account", "configPath", "httpUrl", "httpHost", "httpPort", "cliPath", "name" ], resolveAllowFrom: (account) => account.config.allowFrom, formatAllowFrom: (allowFrom) => normalizeStringifiedEntries(allowFrom).map((entry) => entry === "*" ? "*" : normalizeE164(entry.replace(/^signal:/i, ""))).filter(Boolean), resolveDefaultTo: (account) => account.config.defaultTo }); const signalSecurityAdapter = createRestrictSendersChannelSecurity({ channelKey: SIGNAL_CHANNEL, resolveDmPolicy: (account) => account.config.dmPolicy, resolveDmAllowFrom: (account) => account.config.allowFrom, resolveGroupPolicy: (account) => account.config.groupPolicy, surface: "Signal groups", openScope: "any member", groupPolicyPath: "channels.signal.groupPolicy", groupAllowFromPath: "channels.signal.groupAllowFrom", mentionGated: false, policyPathSuffix: "dmPolicy", normalizeDmEntry: (raw) => normalizeE164(raw.replace(/^signal:/i, "").trim()) }); function createSignalPluginBase(params) { return { ...createChannelPluginBase({ id: SIGNAL_CHANNEL, meta: { ...getChatChannelMeta(SIGNAL_CHANNEL) }, setupWizard: params.setupWizard, capabilities: { chatTypes: ["direct", "group"], media: true, reactions: true }, streaming: { blockStreamingCoalesceDefaults: { minChars: 1500, idleMs: 1e3 } }, reload: { configPrefixes: ["channels.signal"] }, configSchema: SignalChannelConfigSchema, config: { ...signalConfigAdapter, isConfigured: (account) => account.configured, describeAccount: (account) => describeAccountSnapshot({ account, configured: account.configured, extra: { baseUrl: account.baseUrl } }) }, security: signalSecurityAdapter, setup: params.setup }), messaging: { defaultMarkdownTableMode: "bullets" } }; } //#endregion //#region extensions/signal/src/channel.ts let signalMonitorModulePromise = null; let signalProbeModulePromise = null; let signalSendRuntimePromise = null; async function loadSignalMonitorModule() { signalMonitorModulePromise ??= import("./monitor-BnKYyNGH.js"); return await signalMonitorModulePromise; } async function loadSignalProbeModule() { signalProbeModulePromise ??= import("./probe-Cu3k5GNq.js"); return await signalProbeModulePromise; } async function loadSignalSendRuntime() { signalSendRuntimePromise ??= import("./send.runtime.js"); return await signalSendRuntimePromise; } async function resolveSignalSendContext(params) { return { send: resolveOutboundSendDep(params.deps, "signal") ?? (await loadSignalSendRuntime()).sendMessageSignal, maxBytes: resolveChannelMediaMaxBytes({ cfg: params.cfg, resolveChannelLimitMb: ({ cfg, accountId }) => cfg.channels?.signal?.accounts?.[accountId]?.mediaMaxMb ?? cfg.channels?.signal?.mediaMaxMb, accountId: params.accountId }) }; } async function sendSignalOutbound(params) { const { send, maxBytes } = await resolveSignalSendContext(params); return await send(params.to, params.text, { cfg: params.cfg, ...params.mediaUrl ? { mediaUrl: params.mediaUrl } : {}, ...params.mediaLocalRoots?.length ? { mediaLocalRoots: params.mediaLocalRoots } : {}, ...params.mediaReadFile ? { mediaReadFile: params.mediaReadFile } : {}, maxBytes, accountId: params.accountId ?? void 0 }); } const signalMessageAdapter = defineChannelMessageAdapter({ id: "signal", durableFinal: { capabilities: { text: true, media: true } }, send: { text: async (ctx) => await sendSignalOutbound({ cfg: ctx.cfg, to: ctx.to, text: ctx.text, accountId: ctx.accountId ?? void 0, deps: ctx.deps }), media: async (ctx) => await sendSignalOutbound({ cfg: ctx.cfg, to: ctx.to, text: ctx.text, mediaUrl: ctx.mediaUrl, mediaLocalRoots: ctx.mediaLocalRoots, mediaReadFile: ctx.mediaReadFile, accountId: ctx.accountId ?? void 0, deps: ctx.deps }) } }); function inferSignalTargetChatType(rawTo) { let to = rawTo.trim(); if (!to) return; if (/^signal:/i.test(to)) to = to.replace(/^signal:/i, "").trim(); if (!to) return; const lower = normalizeLowercaseStringOrEmpty(to); if (lower.startsWith("group:")) return "group"; if (lower.startsWith("username:") || lower.startsWith("u:")) return "direct"; return "direct"; } function buildSignalBaseSessionKey(params) { return buildOutboundBaseSessionKey({ ...params, channel: "signal" }); } function resolveSignalOutboundSessionRoute(params) { const resolved = resolveSignalOutboundTarget(params.target); if (!resolved) return null; const baseSessionKey = buildSignalBaseSessionKey({ cfg: params.cfg, agentId: params.agentId, accountId: params.accountId, peer: resolved.peer }); return { sessionKey: baseSessionKey, baseSessionKey, ...resolved }; } async function sendFormattedSignalText(ctx) { const { send, maxBytes } = await resolveSignalSendContext({ cfg: ctx.cfg, accountId: ctx.accountId ?? void 0, deps: ctx.deps }); const limit = resolveTextChunkLimit(ctx.cfg, "signal", ctx.accountId ?? void 0, { fallbackLimit: 4e3 }); const tableMode = resolveMarkdownTableMode({ cfg: ctx.cfg, channel: "signal", accountId: ctx.accountId ?? void 0 }); let chunks = limit === void 0 ? markdownToSignalTextChunks(ctx.text, Number.POSITIVE_INFINITY, { tableMode }) : markdownToSignalTextChunks(ctx.text, limit, { tableMode }); if (chunks.length === 0 && ctx.text) chunks = [{ text: ctx.text, styles: [] }]; const results = []; for (const chunk of chunks) { ctx.abortSignal?.throwIfAborted(); const result = await send(ctx.to, chunk.text, { cfg: ctx.cfg, maxBytes, accountId: ctx.accountId ?? void 0, textMode: "plain", textStyles: chunk.styles }); results.push(result); } return attachChannelToResults("signal", results); } async function sendFormattedSignalMedia(ctx) { ctx.abortSignal?.throwIfAborted(); const { send, maxBytes } = await resolveSignalSendContext({ cfg: ctx.cfg, accountId: ctx.accountId ?? void 0, deps: ctx.deps }); const tableMode = resolveMarkdownTableMode({ cfg: ctx.cfg, channel: "signal", accountId: ctx.accountId ?? void 0 }); const formatted = markdownToSignalTextChunks(ctx.text, Number.POSITIVE_INFINITY, { tableMode })[0] ?? { text: ctx.text, styles: [] }; return attachChannelToResult("signal", await send(ctx.to, formatted.text, { cfg: ctx.cfg, mediaUrl: ctx.mediaUrl, mediaLocalRoots: ctx.mediaLocalRoots, ...ctx.mediaReadFile ? { mediaReadFile: ctx.mediaReadFile } : {}, maxBytes, accountId: ctx.accountId ?? void 0, textMode: "plain", textStyles: formatted.styles })); } const signalPlugin = createChatChannelPlugin({ base: { ...createSignalPluginBase({ setupWizard: signalSetupWizard, setup: signalSetupAdapter }), actions: signalMessageActions, approvalCapability: signalApprovalCapability, allowlist: buildDmGroupAccountAllowlistAdapter({ channelId: "signal", resolveAccount: resolveSignalAccount, normalize: ({ cfg, accountId, values }) => signalConfigAdapter.formatAllowFrom({ cfg, accountId, allowFrom: values }), resolveDmAllowFrom: (account) => account.config.allowFrom, resolveGroupAllowFrom: (account) => account.config.groupAllowFrom, resolveDmPolicy: (account) => account.config.dmPolicy, resolveGroupPolicy: (account) => account.config.groupPolicy }), agentPrompt: { reactionGuidance: ({ cfg, accountId }) => { const level = resolveSignalReactionLevel({ cfg, accountId: accountId ?? void 0 }).agentReactionGuidance; return level ? { level, channelLabel: "Signal" } : void 0; } }, messaging: { targetPrefixes: ["signal"], normalizeTarget: normalizeSignalMessagingTarget, inferTargetChatType: ({ to }) => inferSignalTargetChatType(to), resolveOutboundSessionRoute: (params) => resolveSignalOutboundSessionRoute(params), targetResolver: { looksLikeId: looksLikeSignalTargetId, hint: "<E.164|uuid:ID|group:ID|signal:group:ID|signal:+E.164>" } }, heartbeat: { sendTyping: async ({ cfg, to, accountId }) => { await (await loadSignalSendRuntime()).sendTypingSignal(to, { cfg, ...accountId ? { accountId } : {} }); }, clearTyping: async ({ cfg, to, accountId }) => { await (await loadSignalSendRuntime()).sendTypingSignal(to, { cfg, ...accountId ? { accountId } : {}, stop: true }); } }, status: createComputedAccountStatusAdapter({ defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID), collectStatusIssues: (accounts) => collectStatusIssuesFromLastError("signal", accounts), buildChannelSummary: ({ snapshot }) => buildBaseChannelStatusSummary(snapshot, { baseUrl: snapshot.baseUrl ?? null, probe: snapshot.probe, lastProbeAt: snapshot.lastProbeAt ?? null }), probeAccount: async ({ account, timeoutMs }) => { const baseUrl = account.baseUrl; const { probeSignal } = await loadSignalProbeModule(); return await probeSignal(baseUrl, timeoutMs, { apiMode: account.config?.apiMode ?? "auto" }); }, formatCapabilitiesProbe: ({ probe }) => probe?.version ? [{ text: `Signal daemon: ${probe.version}` }] : [], 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; ctx.setStatus({ accountId: account.accountId, baseUrl: account.baseUrl }); ctx.log?.info(`[${account.accountId}] starting provider (${account.baseUrl})`); const { monitorSignalProvider } = await loadSignalMonitorModule(); return await monitorSignalProvider({ accountId: account.accountId, config: ctx.cfg, runtime: ctx.runtime, channelRuntime: ctx.channelRuntime, abortSignal: ctx.abortSignal, mediaMaxMb: account.config.mediaMaxMb }); } }, message: signalMessageAdapter }, pairing: { text: { idLabel: "signalNumber", message: PAIRING_APPROVED_MESSAGE, normalizeAllowEntry: createPairingPrefixStripper(/^signal:/i), notify: async ({ cfg, id, message }) => { await (await loadSignalSendRuntime()).sendMessageSignal(id, message, { cfg }); } } }, security: signalSecurityAdapter, outbound: { base: { deliveryMode: "direct", chunker: chunkText, chunkerMode: "text", textChunkLimit: 4e3, shouldSuppressLocalPayloadPrompt: ({ cfg, accountId, payload, hint }) => shouldSuppressLocalSignalExecApprovalPrompt({ cfg, accountId, payload, hint }), sendFormattedText: async ({ cfg, to, text, accountId, deps, abortSignal }) => await sendFormattedSignalText({ cfg, to, text, accountId, deps, abortSignal }), sendFormattedMedia: async ({ cfg, to, text, mediaUrl, mediaLocalRoots, mediaReadFile, accountId, deps, abortSignal }) => await sendFormattedSignalMedia({ cfg, to, text, mediaUrl, mediaLocalRoots, mediaReadFile, accountId, deps, abortSignal }) }, attachedResults: { channel: "signal", sendText: async ({ cfg, to, text, accountId, deps }) => await sendSignalOutbound({ cfg, to, text, accountId: accountId ?? void 0, deps }), sendMedia: async ({ cfg, to, text, mediaUrl, mediaLocalRoots, mediaReadFile, accountId, deps }) => await sendSignalOutbound({ cfg, to, text, mediaUrl, mediaLocalRoots, mediaReadFile, accountId: accountId ?? void 0, deps }) } } }); //#endregion export { resolveSignalOutboundTarget as i, createSignalPluginBase as n, signalSetupWizard as r, signalPlugin as t };