UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

609 lines (608 loc) 24.7 kB
import { a as normalizeLowercaseStringOrEmpty, c as normalizeOptionalString } from "./string-coerce-mnp54Vah.js"; import { l as normalizeE164 } from "./utils-CCC-BEJH.js"; import { n as normalizeAccountId } from "./account-id-Df9e41E6.js"; import { c as kindFromMime } from "./mime-C8mVE2Bw.js"; import { t as createMessageReceiptFromOutboundResults } from "./receipt-B3SXxhHV.js"; import { n as resolveOutboundAttachmentFromUrl } from "./outbound-attachment-DeL7GCBY.js"; import "./media-runtime-fgpmX6eL.js"; import "./string-coerce-runtime-CEGJWkQ_.js"; import "./text-utility-runtime-BZ3jGr-e.js"; import "./routing-CQ63ICPX.js"; import { t as resolveMarkdownTableMode } from "./markdown-tables-LfSv7QSx.js"; import { t as createPluginRuntimeStore } from "./runtime-store-uAKGMqTs.js"; import { t as matchesApprovalRequestFilters } from "./approval-request-filters-W2XYHus7.js"; import "./approval-client-runtime-iV-RyHdv.js"; import { c as listApprovalReactionBindings, d as resolveApprovalReactionTarget, r as buildApprovalReactionHint, s as createApprovalReactionTargetStore } from "./approval-reaction-runtime-BQQ1cR5g.js"; import { t as requireRuntimeConfig } from "./plugin-config-runtime-CpE6DYgJ.js"; import "./channel-outbound-B3_Zy-kG.js"; import "./markdown-table-runtime-j_C4iTF_.js"; import { i as resolveSignalAccount } from "./accounts-ms-4J9iY.js"; import { d as normalizeSignalMessagingTarget, l as looksLikeUuid } from "./identity-IGz-X4Li.js"; import { i as signalApprovalAuth, r as getSignalApprovalApprovers, t as markdownToSignalText } from "./format-B-0MMYcV.js"; import { n as signalRpcRequest } from "./client-adapter-Bk8RFbS1.js"; import { t as resolveSignalRpcContext } from "./rpc-context-Bh6DQHqc.js"; //#region extensions/signal/src/runtime.ts const { setRuntime: setSignalRuntime, getRuntime: getSignalRuntime, tryGetRuntime: getOptionalSignalRuntime, clearRuntime: clearSignalRuntime } = createPluginRuntimeStore({ pluginId: "signal", errorMessage: "Signal runtime not initialized" }); //#endregion //#region extensions/signal/src/approval-reactions.ts const PERSISTENT_NAMESPACE = "signal.approval-reactions"; const PERSISTENT_MAX_ENTRIES = 1e3; const DEFAULT_REACTION_TARGET_TTL_MS = 1440 * 60 * 1e3; let resolverRuntimePromise; const signalApprovalReactionTargets = createApprovalReactionTargetStore({ namespace: PERSISTENT_NAMESPACE, maxEntries: PERSISTENT_MAX_ENTRIES, defaultTtlMs: DEFAULT_REACTION_TARGET_TTL_MS, openStore: (storeParams) => getOptionalSignalRuntime()?.state.openKeyedStore(storeParams), logPersistentError: reportPersistentApprovalReactionError, readPersistedTarget }); function loadApprovalResolver() { resolverRuntimePromise ??= import("./approval-resolver-Ble8PYgo.js"); return resolverRuntimePromise; } function resolveApprovalKindFromId(approvalId) { return approvalId.startsWith("plugin:") ? "plugin" : "exec"; } function resolveApprovalForwardingConfig(params) { return params.approvalKind === "plugin" ? params.cfg.approvals?.plugin : params.cfg.approvals?.exec; } function normalizeApprovalForwardingMode(mode) { return mode ?? "session"; } function approvalModeIncludesSession(mode) { return mode === "session" || mode === "both"; } function approvalModeIncludesTargets(mode) { return mode === "targets" || mode === "both"; } function matchesSignalApprovalReactionFilters(params) { return matchesApprovalRequestFilters({ request: { agentId: params.route.agentId, sessionKey: params.route.sessionKey }, agentFilter: params.config.agentFilter, sessionFilter: params.config.sessionFilter, fallbackAgentIdFromSessionKey: true }); } function targetAccountMatches(params) { const configuredAccountId = normalizeOptionalString(params.configuredAccountId); if (!configuredAccountId) return true; const routeAccountId = normalizeOptionalString(params.routeAccountId); return Boolean(routeAccountId && normalizeAccountId(routeAccountId) === normalizeAccountId(configuredAccountId)); } function hasMatchingSignalApprovalReactionTarget(params) { return (params.config.targets ?? []).some((target) => { if (normalizeLowercaseStringOrEmpty(target.channel) !== "signal") return false; const configuredTo = normalizeSignalMessagingTarget(target.to); if (!configuredTo || configuredTo !== params.route.to) return false; return targetAccountMatches({ routeAccountId: params.route.accountId, configuredAccountId: target.accountId }); }); } function isSignalApprovalReactionRouteStillEnabled(params) { const config = resolveApprovalForwardingConfig({ cfg: params.cfg, approvalKind: params.target.approvalKind }); if (!config?.enabled) return false; const mode = normalizeApprovalForwardingMode(config.mode); if (params.target.route.deliveryMode === "target") return approvalModeIncludesTargets(mode) && matchesSignalApprovalReactionFilters({ config, route: params.target.route }) && hasMatchingSignalApprovalReactionTarget({ config, route: params.target.route }); if (!approvalModeIncludesSession(mode)) return false; return matchesSignalApprovalReactionFilters({ config, route: params.target.route }); } function resolveSignalApprovalConversationKey(to) { return normalizeSignalMessagingTarget(to) ?? null; } function normalizeSignalApprovalTargetAuthorKey(value) { const normalized = normalizeOptionalString(value); if (!normalized) return null; const withoutSignalPrefix = normalized.replace(/^signal:/i, "").trim(); if (normalizeLowercaseStringOrEmpty(withoutSignalPrefix).startsWith("uuid:")) { const uuid = withoutSignalPrefix.slice(5).trim().toLowerCase(); return uuid ? `uuid:${uuid}` : null; } if (looksLikeUuid(withoutSignalPrefix)) return `uuid:${withoutSignalPrefix.toLowerCase()}`; return normalizeE164(withoutSignalPrefix); } function resolveSignalApprovalTargetAuthorKeys(params) { const targetAuthorUuid = normalizeOptionalString(params.targetAuthorUuid); const keys = [targetAuthorUuid ? `uuid:${targetAuthorUuid.replace(/^uuid:/i, "").trim().toLowerCase()}` : null, params.targetAuthor ? normalizeSignalApprovalTargetAuthorKey(params.targetAuthor) : null].filter((key) => Boolean(key)); return Array.from(new Set(keys)); } function buildReactionTargetKey(params) { const accountId = params.accountId.trim(); const conversationKey = params.conversationKey.trim(); const messageId = params.messageId.trim(); if (!accountId || !conversationKey || !messageId || messageId === "unknown") return null; return `${accountId}:${conversationKey}:${messageId}`; } function reportPersistentApprovalReactionError(error) { try { getOptionalSignalRuntime()?.logging.getChildLogger({ plugin: "signal", feature: "approval-reaction-state" }).warn("Signal persistent approval reaction state failed", { error: String(error) }); } catch {} } function readPersistedTarget(target) { const value = target; if (!value || typeof value.approvalId !== "string" || value.approvalKind !== "exec" && value.approvalKind !== "plugin" || !value.route || value.route.deliveryMode !== "session" && value.route.deliveryMode !== "target" || !Array.isArray(value.targetAuthorKeys) || !Array.isArray(value.allowedDecisions)) return null; const targetRouteTo = value.route.deliveryMode === "target" && typeof value.route.to === "string" ? normalizeSignalMessagingTarget(value.route.to) : null; if (value.route.deliveryMode === "target" && !targetRouteTo) return null; const route = value.route.deliveryMode === "target" ? { deliveryMode: "target", to: targetRouteTo, ...typeof value.route.accountId === "string" ? { accountId: value.route.accountId } : {}, ...typeof value.route.agentId === "string" ? { agentId: value.route.agentId } : {}, ...typeof value.route.sessionKey === "string" ? { sessionKey: value.route.sessionKey } : {} } : { deliveryMode: "session", ...typeof value.route.agentId === "string" ? { agentId: value.route.agentId } : {}, ...typeof value.route.sessionKey === "string" ? { sessionKey: value.route.sessionKey } : {} }; return { approvalId: value.approvalId, approvalKind: value.approvalKind, allowedDecisions: value.allowedDecisions, targetAuthorKeys: value.targetAuthorKeys, route }; } function listSignalApprovalReactionBindings(allowedDecisions) { return listApprovalReactionBindings({ allowedDecisions }); } function buildSignalApprovalReactionHint(allowedDecisions) { return buildApprovalReactionHint({ allowedDecisions }); } function insertSignalApprovalReactionHintNearHeader(params) { const lines = params.text.split(/\r?\n/); const idLineIndex = lines.findIndex((line) => /^ID:\s*\S+/.test(line.trim())); if (idLineIndex >= 0) { const before = lines.slice(0, idLineIndex + 1).join("\n"); const after = lines.slice(idLineIndex + 1).join("\n").replace(/^\n+/, ""); return after ? `${before}\n\n${params.hint}\n\n${after}` : `${before}\n\n${params.hint}`; } return `${params.hint}\n\n${params.text}`; } function addSignalApprovalReactionHintToText(params) { if (/(^|\n)React with:\s*(\n|$)/i.test(params.text)) return params.text; const hint = buildSignalApprovalReactionHint(params.allowedDecisions); return hint ? insertSignalApprovalReactionHintNearHeader({ text: params.text, hint }) : params.text; } function normalizeApprovalDecision(value) { const normalized = value.trim().toLowerCase(); if (normalized === "always") return "allow-always"; if (normalized === "allow-once" || normalized === "allow-always" || normalized === "deny") return normalized; return null; } function extractSignalApprovalPromptBinding(text) { const allowedDecisions = []; let approvalId = ""; for (const line of text.split(/\r?\n/)) { const match = line.match(/\/approve(?:@[^\s]+)?\s+([A-Za-z0-9][A-Za-z0-9._:-]*)\s+(.+)$/i); if (!match) continue; if (approvalId && match[1] !== approvalId) continue; approvalId ||= match[1]; for (const decisionText of match[2].split(/[\s|,]+/)) { const decision = normalizeApprovalDecision(decisionText); if (decision && !allowedDecisions.includes(decision)) allowedDecisions.push(decision); } } return approvalId && allowedDecisions.length > 0 ? { approvalId, allowedDecisions } : null; } function buildTargetRoute(params) { const to = normalizeSignalMessagingTarget(params.to); if (!to) return null; const route = { deliveryMode: "target", to, ...normalizeOptionalString(params.accountId) ? { accountId: normalizeOptionalString(params.accountId) } : {}, ...normalizeOptionalString(params.agentId) ? { agentId: normalizeOptionalString(params.agentId) } : {}, ...normalizeOptionalString(params.sessionKey) ? { sessionKey: normalizeOptionalString(params.sessionKey) } : {} }; return isSignalApprovalReactionRouteStillEnabled({ cfg: params.cfg, target: { approvalKind: resolveApprovalKindFromId(params.approvalId), route } }) ? route : null; } function shouldAppendSignalApprovalReactionHintForOutboundMessage(params) { const binding = extractSignalApprovalPromptBinding(params.text); if (!binding) return false; if (resolveSignalApprovalTargetAuthorKeys(params).length === 0) return false; if (!hasSignalApprovalReactionApprovers({ cfg: params.cfg, accountId: params.accountId })) return false; return Boolean(buildTargetRoute({ cfg: params.cfg, accountId: params.accountId, to: params.to, approvalId: binding.approvalId, agentId: params.agentId, sessionKey: params.sessionKey })); } function appendSignalApprovalReactionHintForOutboundMessage(params) { const binding = extractSignalApprovalPromptBinding(params.text); if (!binding || !shouldAppendSignalApprovalReactionHintForOutboundMessage({ ...params, text: params.text })) return params.text; return addSignalApprovalReactionHintToText({ text: params.text, allowedDecisions: binding.allowedDecisions }); } function hasSignalApprovalReactionApprovers(params) { return getSignalApprovalApprovers(params).length > 0; } function registerSignalApprovalReactionTarget(params) { const key = buildReactionTargetKey(params); const approvalId = params.approvalId.trim(); const targetAuthorKeys = Array.from(new Set(params.targetAuthorKeys.map((entry) => normalizeSignalApprovalTargetAuthorKey(entry)).filter((entry) => Boolean(entry)))); const allowedDecisions = listSignalApprovalReactionBindings(params.allowedDecisions).map((binding) => binding.decision); if (!params.routeAllowed || !key || !approvalId || allowedDecisions.length === 0) return null; if (targetAuthorKeys.length === 0) return null; const route = params.route.deliveryMode === "target" ? { deliveryMode: "target", to: params.route.to, ...normalizeOptionalString(params.route.accountId) ? { accountId: normalizeOptionalString(params.route.accountId) } : {}, ...normalizeOptionalString(params.route.agentId) ? { agentId: normalizeOptionalString(params.route.agentId) } : {}, ...normalizeOptionalString(params.route.sessionKey) ? { sessionKey: normalizeOptionalString(params.route.sessionKey) } : {} } : { deliveryMode: "session", ...normalizeOptionalString(params.route.agentId) ? { agentId: normalizeOptionalString(params.route.agentId) } : {}, ...normalizeOptionalString(params.route.sessionKey) ? { sessionKey: normalizeOptionalString(params.route.sessionKey) } : {} }; const target = { approvalId, approvalKind: resolveApprovalKindFromId(approvalId), allowedDecisions, targetAuthorKeys, route }; signalApprovalReactionTargets.register(key, target, { ttlMs: params.ttlMs }); return target; } function registerSignalApprovalReactionTargetForOutboundMessage(params) { const binding = extractSignalApprovalPromptBinding(params.text); if (!binding) return false; const conversationKey = resolveSignalApprovalConversationKey(params.to); if (!conversationKey) return false; const route = buildTargetRoute({ cfg: params.cfg, accountId: params.accountId, to: params.to, approvalId: binding.approvalId, agentId: params.agentId, sessionKey: params.sessionKey }); if (!route) return false; return Boolean(registerSignalApprovalReactionTarget({ accountId: params.accountId, conversationKey, messageId: params.messageId, approvalId: binding.approvalId, allowedDecisions: binding.allowedDecisions, targetAuthorKeys: resolveSignalApprovalTargetAuthorKeys(params), route, routeAllowed: true, ttlMs: params.ttlMs })); } function unregisterSignalApprovalReactionTarget(params) { const key = buildReactionTargetKey(params); if (!key) return; signalApprovalReactionTargets.delete(key); } function resolveTarget(params) { const target = params.target; if (!target) return null; if (params.targetAuthorKeys.length === 0 || !params.targetAuthorKeys.some((key) => target.targetAuthorKeys.includes(key))) return null; const resolved = resolveApprovalReactionTarget({ target, reactionKey: params.reactionKey }); if (!resolved?.route) return null; return { approvalId: resolved.approvalId, approvalKind: resolved.approvalKind, decision: resolved.decision, route: resolved.route }; } async function resolveSignalApprovalReactionTargetWithPersistence(params) { const key = buildReactionTargetKey(params); if (!key) return null; const targetAuthorKeys = resolveSignalApprovalTargetAuthorKeys(params); if (targetAuthorKeys.length === 0) return null; return resolveTarget({ target: await signalApprovalReactionTargets.lookup(key), reactionKey: params.reactionKey, targetAuthorKeys }); } async function maybeResolveSignalApprovalReaction(params) { const target = await resolveSignalApprovalReactionTargetWithPersistence({ accountId: params.accountId, conversationKey: params.conversationKey, messageId: params.messageId, reactionKey: params.reactionKey, targetAuthor: params.targetAuthor, targetAuthorUuid: params.targetAuthorUuid }); if (!target) return false; if (!isSignalApprovalReactionRouteStillEnabled({ cfg: params.cfg, target })) { params.logVerboseMessage?.(`signal: approval reaction denied id=${target.approvalId}; approval route is no longer enabled`); return true; } const actorId = params.actorId?.trim(); if (!actorId) { params.logVerboseMessage?.(`signal: approval reaction ignored for ${target.approvalId}; missing actor identity`); return true; } if (getSignalApprovalApprovers({ cfg: params.cfg, accountId: params.accountId }).length === 0) { params.logVerboseMessage?.(`signal: approval reaction denied id=${target.approvalId}; reactions require explicit approvers`); return true; } if (!signalApprovalAuth.authorizeActorAction({ cfg: params.cfg, accountId: params.accountId, senderId: actorId, action: "approve", approvalKind: target.approvalKind }).authorized) { params.logVerboseMessage?.(`signal: approval reaction denied id=${target.approvalId} sender=${actorId}`); return true; } const { isApprovalNotFoundError, resolveSignalApproval } = await loadApprovalResolver(); try { await resolveSignalApproval({ cfg: params.cfg, approvalId: target.approvalId, decision: target.decision, senderId: actorId, gatewayUrl: params.gatewayUrl }); params.logVerboseMessage?.(`signal: approval reaction resolved id=${target.approvalId} sender=${actorId} decision=${target.decision}`); return true; } catch (error) { if (isApprovalNotFoundError(error)) { unregisterSignalApprovalReactionTarget({ accountId: params.accountId, conversationKey: params.conversationKey, messageId: params.messageId }); params.logVerboseMessage?.(`signal: approval reaction ignored for expired approval id=${target.approvalId} sender=${actorId}`); return true; } params.logVerboseMessage?.(`signal: approval reaction failed id=${target.approvalId} sender=${actorId}: ${String(error)}`); return true; } } //#endregion //#region extensions/signal/src/send.ts async function resolveSignalRpcAccountInfo(opts) { if (opts.baseUrl?.trim() && opts.account?.trim()) return; if (!opts.cfg) throw new Error("Signal RPC account resolution requires a resolved runtime config. Load and resolve config at the command or gateway boundary, then pass cfg through the runtime path."); return resolveSignalAccount({ cfg: requireRuntimeConfig(opts.cfg, "Signal RPC account resolution"), accountId: opts.accountId }); } function parseTarget(raw) { let value = raw.trim(); if (!value) throw new Error("Signal recipient is required"); if (normalizeLowercaseStringOrEmpty(value).startsWith("signal:")) value = value.slice(7).trim(); const normalized = normalizeLowercaseStringOrEmpty(value); if (normalized.startsWith("group:")) return { type: "group", groupId: value.slice(6).trim() }; if (normalized.startsWith("username:")) return { type: "username", username: value.slice(9).trim() }; if (normalized.startsWith("u:")) return { type: "username", username: value.trim() }; return { type: "recipient", recipient: value }; } function buildTargetParams(target, allow) { if (target.type === "recipient") { if (!allow.recipient) return null; return { recipient: [target.recipient] }; } if (target.type === "group") { if (!allow.group) return null; return { groupId: target.groupId }; } if (target.type === "username") { if (!allow.username) return null; return { username: [target.username] }; } return null; } function createSignalSendReceipt(params) { const messageId = params.messageId.trim(); const results = messageId && messageId !== "unknown" ? [{ channel: "signal", messageId, meta: { targetType: params.target.type } }] : []; if (results[0]) { if (params.timestamp != null) results[0].timestamp = params.timestamp; if (params.target.type === "group") results[0].chatId = params.target.groupId; else if (params.target.type === "recipient") results[0].toJid = params.target.recipient; else results[0].toJid = params.target.username; } return createMessageReceiptFromOutboundResults({ results, kind: params.kind }); } async function sendMessageSignal(to, text, opts) { const cfg = requireRuntimeConfig(opts.cfg, "Signal send"); const apiMode = cfg.channels?.signal?.apiMode; const accountInfo = resolveSignalAccount({ cfg, accountId: opts.accountId }); const { baseUrl, account } = resolveSignalRpcContext(opts, accountInfo); const target = parseTarget(to); const outboundText = appendSignalApprovalReactionHintForOutboundMessage({ cfg, accountId: accountInfo.accountId, to, text: text ?? "", targetAuthor: account }); let message = outboundText; let messageFromPlaceholder = false; let textStyles = []; const textMode = opts.textMode ?? "markdown"; const maxBytes = (() => { if (typeof opts.maxBytes === "number") return opts.maxBytes; if (typeof accountInfo.config.mediaMaxMb === "number") return accountInfo.config.mediaMaxMb * 1024 * 1024; if (typeof cfg.agents?.defaults?.mediaMaxMb === "number") return cfg.agents.defaults.mediaMaxMb * 1024 * 1024; return 8 * 1024 * 1024; })(); let attachments; if (opts.mediaUrl?.trim()) { const resolved = await resolveOutboundAttachmentFromUrl(opts.mediaUrl.trim(), maxBytes, { mediaAccess: opts.mediaAccess, localRoots: opts.mediaLocalRoots, readFile: opts.mediaReadFile }); attachments = [resolved.path]; const kind = kindFromMime(resolved.contentType ?? void 0); if (!message && kind) { message = kind === "image" ? "<media:image>" : `<media:${kind}>`; messageFromPlaceholder = true; } } if (message.trim() && !messageFromPlaceholder) if (textMode === "plain") textStyles = opts.textStyles ?? []; else { const tableMode = resolveMarkdownTableMode({ cfg, channel: "signal", accountId: accountInfo.accountId }); const formatted = markdownToSignalText(message, { tableMode }); message = formatted.text; textStyles = formatted.styles; } if (!message.trim() && (!attachments || attachments.length === 0)) throw new Error("Signal send requires text or media"); const params = { message }; if (textStyles.length > 0) params["text-style"] = textStyles.map((style) => `${style.start}:${style.length}:${style.style}`); if (account) params.account = account; if (attachments && attachments.length > 0) params.attachments = attachments; const targetParams = buildTargetParams(target, { recipient: true, group: true, username: true }); if (!targetParams) throw new Error("Signal recipient is required"); Object.assign(params, targetParams); const timestamp = (await signalRpcRequest("send", params, { baseUrl, timeoutMs: opts.timeoutMs, apiMode }))?.timestamp; const messageId = timestamp ? String(timestamp) : "unknown"; registerSignalApprovalReactionTargetForOutboundMessage({ cfg, accountId: accountInfo.accountId, to, messageId, text: outboundText, targetAuthor: account }); return { messageId, timestamp, receipt: createSignalSendReceipt({ messageId, target, kind: attachments && attachments.length > 0 ? "media" : "text", ...timestamp != null ? { timestamp } : {} }) }; } async function sendTypingSignal(to, opts) { const accountInfo = await resolveSignalRpcAccountInfo(opts); const cfg = requireRuntimeConfig(opts.cfg, "Signal typing"); const { baseUrl, account } = resolveSignalRpcContext(opts, accountInfo); const targetParams = buildTargetParams(parseTarget(to), { recipient: true, group: true }); if (!targetParams) return false; const params = { ...targetParams }; if (account) params.account = account; if (opts.stop) params.stop = true; await signalRpcRequest("sendTyping", params, { baseUrl, timeoutMs: opts.timeoutMs, apiMode: cfg.channels?.signal?.apiMode }); return true; } async function sendReadReceiptSignal(to, targetTimestamp, opts) { if (!Number.isFinite(targetTimestamp) || targetTimestamp <= 0) return false; const accountInfo = await resolveSignalRpcAccountInfo(opts); const cfg = requireRuntimeConfig(opts.cfg, "Signal read receipt"); const { baseUrl, account } = resolveSignalRpcContext(opts, accountInfo); const targetParams = buildTargetParams(parseTarget(to), { recipient: true }); if (!targetParams) return false; const params = { ...targetParams, targetTimestamp, type: opts.type ?? "read" }; if (account) params.account = account; await signalRpcRequest("sendReceipt", params, { baseUrl, timeoutMs: opts.timeoutMs, apiMode: cfg.channels?.signal?.apiMode }); return true; } //#endregion export { maybeResolveSignalApprovalReaction as a, resolveSignalApprovalTargetAuthorKeys as c, hasSignalApprovalReactionApprovers as i, unregisterSignalApprovalReactionTarget as l, sendReadReceiptSignal as n, registerSignalApprovalReactionTarget as o, sendTypingSignal as r, resolveSignalApprovalConversationKey as s, sendMessageSignal as t, setSignalRuntime as u };