UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

595 lines (594 loc) 22.6 kB
import { a as normalizeLowercaseStringOrEmpty, s as normalizeOptionalLowercaseString } from "./string-coerce-mnp54Vah.js"; import { n as normalizeAccountId, t as DEFAULT_ACCOUNT_ID } from "./account-id-Df9e41E6.js"; import { b as sendPayloadWithChunkedTextAndMedia, l as isNumericTargetId } from "./reply-payload-D-VMfpYI.js"; import { r as createLazyRuntimeModule } from "./lazy-runtime-D-7_JraP.js"; import { l as createScopedDmSecurityResolver } from "./channel-config-helpers-lj5quus3.js"; import "./string-coerce-runtime-CEGJWkQ_.js"; import { t as chunkTextForOutbound } from "./text-chunking-D45TeeEs.js"; import { n as createStaticReplyToModeResolver } from "./threading-helpers-CMXJIj4M.js"; import { i as createChatChannelPlugin, t as buildChannelOutboundSessionRoute } from "./core-DSxVv-v1.js"; import "./channel-core-IukkNCd-.js"; import { t as createAccountStatusSink } from "./channel-lifecycle.core-Bfh0_sXw.js"; import { n as isDangerousNameMatchingEnabled } from "./dangerous-name-matching-D4QRC91G.js"; import { T as defineChannelMessageAdapter } from "./channel-outbound-B3_Zy-kG.js"; import "./conversation-runtime-VjM1-D7a.js"; import { d as createDefaultChannelRuntimeState, l as createAsyncComputedAccountStatusAdapter } from "./status-helpers-BevxX6Pu.js"; import { a as coerceStatusIssueAccountId, d as readStatusIssueFields, n as buildPassiveProbedChannelStatusSummary } from "./extension-shared-B8fkO3TV.js"; import { i as createPairingPrefixStripper } from "./channel-pairing-Bk6_JhTm.js"; import { a as createEmptyChannelResult, o as createRawChannelSendResultAdapter } from "./channel-send-result-Dn_C6AJS.js"; import { a as listZalouserAccountIds, i as checkZcaAuthenticated, n as zalouserSetupAdapter, o as resolveDefaultZalouserAccountId, r as writeQrDataUrlToTempFile, s as resolveZalouserAccountSync, t as createZalouserSetupWizardProxy } from "./setup-core-kty7gX4X.js"; import { t as createZalouserPluginBase } from "./shared-Bc_78ryu.js"; import { a as resolveZalouserReactionMessageIds, o as buildZalouserGroupCandidates, s as findZalouserGroupEntry, t as getZalouserRuntime } from "./runtime-CGCemWjT.js"; //#region extensions/zalouser/src/session-route.ts function stripZalouserTargetPrefix(raw) { return raw.trim().replace(/^(zalouser|zlu):/i, "").trim(); } function normalizeZalouserTarget(raw) { const trimmed = stripZalouserTargetPrefix(raw); if (!trimmed) return; const lower = normalizeLowercaseStringOrEmpty(trimmed); if (lower.startsWith("group:")) { const id = trimmed.slice(6).trim(); return id ? `group:${id}` : void 0; } if (lower.startsWith("g:")) { const id = trimmed.slice(2).trim(); return id ? `group:${id}` : void 0; } if (lower.startsWith("user:")) { const id = trimmed.slice(5).trim(); return id ? `user:${id}` : void 0; } if (lower.startsWith("dm:")) { const id = trimmed.slice(3).trim(); return id ? `user:${id}` : void 0; } if (lower.startsWith("u:")) { const id = trimmed.slice(2).trim(); return id ? `user:${id}` : void 0; } if (/^g-\S+$/i.test(trimmed)) return `group:${trimmed}`; if (/^u-\S+$/i.test(trimmed)) return `user:${trimmed}`; return trimmed; } function parseZalouserOutboundTarget(raw) { const normalized = normalizeZalouserTarget(raw); if (!normalized) throw new Error("Zalouser target is required"); const lowered = normalizeLowercaseStringOrEmpty(normalized); if (lowered.startsWith("group:")) { const threadId = normalized.slice(6).trim(); if (!threadId) throw new Error("Zalouser group target is missing group id"); return { threadId, isGroup: true }; } if (lowered.startsWith("user:")) { const threadId = normalized.slice(5).trim(); if (!threadId) throw new Error("Zalouser user target is missing user id"); return { threadId, isGroup: false }; } return { threadId: normalized, isGroup: false }; } function parseZalouserDirectoryGroupId(raw) { const normalized = normalizeZalouserTarget(raw); if (!normalized) throw new Error("Zalouser group target is required"); const lowered = normalizeLowercaseStringOrEmpty(normalized); if (lowered.startsWith("group:")) { const groupId = normalized.slice(6).trim(); if (!groupId) throw new Error("Zalouser group target is missing group id"); return groupId; } if (lowered.startsWith("user:")) throw new Error("Zalouser group members lookup requires a group target (group:<id>)"); return normalized; } function resolveZalouserOutboundSessionRoute(params) { const normalized = normalizeZalouserTarget(params.target); if (!normalized) return null; const isGroup = (normalizeOptionalLowercaseString(normalized) ?? "").startsWith("group:"); const peerId = normalized.replace(/^(group|user):/i, "").trim(); return buildChannelOutboundSessionRoute({ cfg: params.cfg, agentId: params.agentId, channel: "zalouser", accountId: params.accountId, peer: { kind: isGroup ? "group" : "direct", id: peerId }, chatType: isGroup ? "group" : "direct", from: isGroup ? `zalouser:group:${peerId}` : `zalouser:${peerId}`, to: `zalouser:${peerId}` }); } //#endregion //#region extensions/zalouser/src/channel.adapters.ts const loadZalouserChannelRuntime$1 = createLazyRuntimeModule(() => import("./channel.runtime-Bg5zyba1.js")); const ZALOUSER_TEXT_CHUNK_LIMIT = 2e3; function resolveZalouserQrProfile(accountId) { const normalized = normalizeAccountId(accountId); if (!normalized || normalized === "default") return process.env.ZALOUSER_PROFILE?.trim() || process.env.ZCA_PROFILE?.trim() || "default"; return normalized; } function resolveZalouserOutboundChunkMode(cfg, accountId) { return getZalouserRuntime().channel.text.resolveChunkMode(cfg, "zalouser", accountId); } function resolveZalouserOutboundTextChunkLimit(cfg, accountId) { return getZalouserRuntime().channel.text.resolveTextChunkLimit(cfg, "zalouser", accountId, { fallbackLimit: ZALOUSER_TEXT_CHUNK_LIMIT }); } function resolveZalouserGroupPolicyEntry(params) { const account = resolveZalouserAccountSync({ cfg: params.cfg, accountId: params.accountId ?? void 0 }); return findZalouserGroupEntry(account.config.groups ?? {}, buildZalouserGroupCandidates({ groupId: params.groupId, groupChannel: params.groupChannel, includeWildcard: true, allowNameMatching: isDangerousNameMatchingEnabled(account.config) })); } function resolveZalouserGroupToolPolicy(params) { return resolveZalouserGroupPolicyEntry(params)?.tools; } function resolveZalouserRequireMention(params) { const entry = resolveZalouserGroupPolicyEntry(params); if (typeof entry?.requireMention === "boolean") return entry.requireMention; return true; } async function sendZalouserTextFromContext({ to, text, accountId, cfg }) { const { sendMessageZalouser } = await loadZalouserChannelRuntime$1(); const account = resolveZalouserAccountSync({ cfg, accountId }); const target = parseZalouserOutboundTarget(to); return await sendMessageZalouser(target.threadId, text, { profile: account.profile, isGroup: target.isGroup, textMode: "markdown", textChunkMode: resolveZalouserOutboundChunkMode(cfg, account.accountId), textChunkLimit: resolveZalouserOutboundTextChunkLimit(cfg, account.accountId) }); } async function sendZalouserMediaFromContext({ to, text, mediaUrl, accountId, cfg, mediaLocalRoots, mediaReadFile }) { const { sendMessageZalouser } = await loadZalouserChannelRuntime$1(); const account = resolveZalouserAccountSync({ cfg, accountId }); const target = parseZalouserOutboundTarget(to); return await sendMessageZalouser(target.threadId, text, { profile: account.profile, isGroup: target.isGroup, mediaUrl, mediaLocalRoots, mediaReadFile, textMode: "markdown", textChunkMode: resolveZalouserOutboundChunkMode(cfg, account.accountId), textChunkLimit: resolveZalouserOutboundTextChunkLimit(cfg, account.accountId) }); } const zalouserRawSendResultAdapter = createRawChannelSendResultAdapter({ channel: "zalouser", sendText: sendZalouserTextFromContext, sendMedia: sendZalouserMediaFromContext }); const zalouserMessageAdapter = defineChannelMessageAdapter({ id: "zalouser", durableFinal: { capabilities: { text: true, media: true, messageSendingHooks: true } }, send: { text: sendZalouserTextFromContext, media: sendZalouserMediaFromContext } }); const resolveZalouserDmPolicy = createScopedDmSecurityResolver({ channelKey: "zalouser", resolvePolicy: (account) => account.config.dmPolicy, resolveAllowFrom: (account) => account.config.allowFrom, policyPathSuffix: "dmPolicy", normalizeEntry: (raw) => raw.trim().replace(/^(zalouser|zlu):/i, "") }); const zalouserGroupsAdapter = { resolveRequireMention: resolveZalouserRequireMention, resolveToolPolicy: resolveZalouserGroupToolPolicy }; const zalouserMessageActions = { describeMessageTool: ({ cfg, accountId }) => { if ((accountId ? [resolveZalouserAccountSync({ cfg, accountId })].filter((account) => account.enabled) : listZalouserAccountIds(cfg).map((resolvedAccountId) => resolveZalouserAccountSync({ cfg, accountId: resolvedAccountId })).filter((account) => account.enabled)).length === 0) return null; return { actions: ["react"] }; }, supportsAction: ({ action }) => action === "react", handleAction: async ({ action, params, cfg, accountId, toolContext }) => { if (action !== "react") throw new Error(`Zalouser action ${action} not supported`); const { sendReactionZalouser } = await loadZalouserChannelRuntime$1(); const account = resolveZalouserAccountSync({ cfg, accountId }); const threadId = (typeof params.threadId === "string" ? params.threadId.trim() : "") || (typeof params.to === "string" ? params.to.trim() : "") || (typeof params.chatId === "string" ? params.chatId.trim() : "") || (toolContext?.currentChannelId?.trim() ?? ""); if (!threadId) throw new Error("Zalouser react requires threadId (or to/chatId)."); const emoji = typeof params.emoji === "string" ? params.emoji.trim() : ""; if (!emoji) throw new Error("Zalouser react requires emoji."); const ids = resolveZalouserReactionMessageIds({ messageId: typeof params.messageId === "string" ? params.messageId : void 0, cliMsgId: typeof params.cliMsgId === "string" ? params.cliMsgId : void 0, currentMessageId: toolContext?.currentMessageId }); if (!ids) throw new Error("Zalouser react requires messageId + cliMsgId (or a current message context id)."); const result = await sendReactionZalouser({ profile: account.profile, threadId, isGroup: params.isGroup === true, msgId: ids.msgId, cliMsgId: ids.cliMsgId, emoji, remove: params.remove === true }); if (!result.ok) throw new Error(result.error || "Failed to react on Zalo message"); return { content: [{ type: "text", text: params.remove === true ? `Removed reaction ${emoji} from ${ids.msgId}` : `Reacted ${emoji} on ${ids.msgId}` }], details: { messageId: ids.msgId, cliMsgId: ids.cliMsgId, threadId } }; } }; const zalouserResolverAdapter = { resolveTargets: async ({ cfg, accountId, inputs, kind, runtime }) => { const results = []; for (const input of inputs) { const trimmed = input.trim(); if (!trimmed) { results.push({ input, resolved: false, note: "empty input" }); continue; } if (/^\d+$/.test(trimmed)) { results.push({ input, resolved: true, id: trimmed }); continue; } try { const runtimeModule = await loadZalouserChannelRuntime$1(); const account = resolveZalouserAccountSync({ cfg, accountId: accountId ?? resolveDefaultZalouserAccountId(cfg) }); if (kind === "user") { const friends = await runtimeModule.listZaloFriendsMatching(account.profile, trimmed); const best = friends[0]; results.push({ input, resolved: Boolean(best?.userId), id: best?.userId, name: best?.displayName, note: friends.length > 1 ? "multiple matches; chose first" : void 0 }); } else { const groups = await runtimeModule.listZaloGroupsMatching(account.profile, trimmed); const best = groups.find((group) => normalizeLowercaseStringOrEmpty(group.name) === normalizeLowercaseStringOrEmpty(trimmed)) ?? groups[0]; results.push({ input, resolved: Boolean(best?.groupId), id: best?.groupId, name: best?.name, note: groups.length > 1 ? "multiple matches; chose first" : void 0 }); } } catch (err) { runtime.error?.(`zalouser resolve failed: ${String(err)}`); results.push({ input, resolved: false, note: "lookup failed" }); } } return results; } }; const zalouserAuthAdapter = { login: async ({ cfg, accountId, runtime }) => { const { startZaloQrLogin, waitForZaloQrLogin } = await loadZalouserChannelRuntime$1(); const account = resolveZalouserAccountSync({ cfg, accountId: accountId ?? resolveDefaultZalouserAccountId(cfg) }); runtime.log(`Generating QR login for Zalo Personal (account: ${account.accountId}, profile: ${account.profile})...`); const started = await startZaloQrLogin({ profile: account.profile, timeoutMs: 35e3 }); if (!started.qrDataUrl) throw new Error(started.message || "Failed to start QR login"); const qrPath = await writeQrDataUrlToTempFile(started.qrDataUrl, account.profile); if (qrPath) runtime.log(`Scan QR image: ${qrPath}`); else runtime.log("QR generated but could not be written to a temp file."); const waited = await waitForZaloQrLogin({ profile: account.profile, timeoutMs: 18e4 }); if (!waited.connected) throw new Error(waited.message || "Zalouser login failed"); runtime.log(waited.message); } }; const zalouserSecurityAdapter = { resolveDmPolicy: resolveZalouserDmPolicy, collectAuditFindings: async (params) => (await loadZalouserChannelRuntime$1()).collectZalouserSecurityAuditFindings(params) }; const zalouserThreadingAdapter = { resolveReplyToMode: createStaticReplyToModeResolver("off") }; const zalouserPairingTextAdapter = { idLabel: "zalouserUserId", message: "Your pairing request has been approved.", normalizeAllowEntry: createPairingPrefixStripper(/^(zalouser|zlu):/i), notify: async ({ cfg, id, message }) => { const { sendMessageZalouser } = await loadZalouserChannelRuntime$1(); const account = resolveZalouserAccountSync({ cfg }); if (!await checkZcaAuthenticated(account.profile)) throw new Error("Zalouser not authenticated"); await sendMessageZalouser(id, message, { profile: account.profile }); } }; const zalouserOutboundAdapter = { deliveryMode: "direct", chunker: chunkTextForOutbound, chunkerMode: "markdown", sendPayload: async (ctx) => await sendPayloadWithChunkedTextAndMedia({ ctx, sendText: (nextCtx) => zalouserRawSendResultAdapter.sendText(nextCtx), sendMedia: (nextCtx) => zalouserRawSendResultAdapter.sendMedia(nextCtx), emptyResult: createEmptyChannelResult("zalouser") }), ...zalouserRawSendResultAdapter }; const zalouserMessagingAdapter = { targetPrefixes: ["zalouser", "zlu"], normalizeTarget: (raw) => normalizeZalouserTarget(raw), resolveOutboundSessionRoute: (params) => resolveZalouserOutboundSessionRoute(params), targetResolver: { looksLikeId: (raw) => { const normalized = normalizeZalouserTarget(raw); if (!normalized) return false; if (/^group:[^\s]+$/i.test(normalized) || /^user:[^\s]+$/i.test(normalized)) return true; return isNumericTargetId(normalized); }, hint: "<user:id|group:id>" } }; //#endregion //#region extensions/zalouser/src/directory.ts function mapUser$1(params) { return { kind: "user", id: params.id, name: params.name ?? void 0, avatarUrl: params.avatarUrl ?? void 0, raw: params.raw }; } async function listZalouserDirectoryGroupMembers(params, deps) { const account = resolveZalouserAccountSync({ cfg: params.cfg, accountId: params.accountId }); const normalizedGroupId = parseZalouserDirectoryGroupId(params.groupId); const rows = (await deps.listZaloGroupMembers(account.profile, normalizedGroupId)).map((member) => mapUser$1({ id: member.userId, name: member.displayName, avatarUrl: member.avatar ?? null, raw: member })); return typeof params.limit === "number" && params.limit > 0 ? rows.slice(0, params.limit) : rows; } //#endregion //#region extensions/zalouser/src/status-issues.ts const ZALOUSER_STATUS_FIELDS = [ "accountId", "enabled", "configured", "dmPolicy", "lastError" ]; function collectZalouserStatusIssues(accounts) { const issues = []; for (const entry of accounts) { const account = readStatusIssueFields(entry, ZALOUSER_STATUS_FIELDS); if (!account) continue; const accountId = coerceStatusIssueAccountId(account.accountId) ?? "default"; if (!(account.enabled !== false)) continue; if (!(account.configured === true)) { issues.push({ channel: "zalouser", accountId, kind: "auth", message: "Not authenticated (no saved Zalo session).", fix: "Run: openclaw channels login --channel zalouser" }); continue; } if (account.dmPolicy === "open") issues.push({ channel: "zalouser", accountId, kind: "config", message: "Zalo Personal dmPolicy is \"open\", allowing any user to message the bot without pairing.", fix: "Set channels.zalouser.dmPolicy to \"pairing\" or \"allowlist\" to restrict access." }); } return issues; } //#endregion //#region extensions/zalouser/src/channel.ts const loadZalouserChannelRuntime = createLazyRuntimeModule(() => import("./channel.runtime-Bg5zyba1.js")); const zalouserSetupWizardProxy = createZalouserSetupWizardProxy(async () => (await import("./setup-surface-DzsG9CCm.js")).zalouserSetupWizard); function mapUser(params) { return { kind: "user", id: params.id, name: params.name ?? void 0, avatarUrl: params.avatarUrl ?? void 0, raw: params.raw }; } function mapGroup(params) { return { kind: "group", id: params.id, name: params.name ?? void 0, raw: params.raw }; } const zalouserPlugin = createChatChannelPlugin({ base: { ...createZalouserPluginBase({ setupWizard: zalouserSetupWizardProxy, setup: zalouserSetupAdapter }), groups: zalouserGroupsAdapter, actions: zalouserMessageActions, messaging: zalouserMessagingAdapter, directory: { self: async ({ cfg, accountId }) => { const { getZaloUserInfo } = await loadZalouserChannelRuntime(); const parsed = await getZaloUserInfo(resolveZalouserAccountSync({ cfg, accountId }).profile); if (!parsed?.userId) return null; return mapUser({ id: parsed.userId, name: parsed.displayName ?? null, avatarUrl: parsed.avatar ?? null, raw: parsed }); }, listPeers: async ({ cfg, accountId, query, limit }) => { const { listZaloFriendsMatching } = await loadZalouserChannelRuntime(); const rows = (await listZaloFriendsMatching(resolveZalouserAccountSync({ cfg, accountId }).profile, query)).map((friend) => mapUser({ id: friend.userId, name: friend.displayName ?? null, avatarUrl: friend.avatar ?? null, raw: friend })); return typeof limit === "number" && limit > 0 ? rows.slice(0, limit) : rows; }, listGroups: async ({ cfg, accountId, query, limit }) => { const { listZaloGroupsMatching } = await loadZalouserChannelRuntime(); const rows = (await listZaloGroupsMatching(resolveZalouserAccountSync({ cfg, accountId }).profile, query)).map((group) => mapGroup({ id: `group:${group.groupId}`, name: group.name ?? null, raw: group })); return typeof limit === "number" && limit > 0 ? rows.slice(0, limit) : rows; }, listGroupMembers: async ({ cfg, accountId, groupId, limit }) => { const { listZaloGroupMembers } = await loadZalouserChannelRuntime(); return await listZalouserDirectoryGroupMembers({ cfg, accountId: accountId ?? void 0, groupId, limit: limit ?? void 0 }, { listZaloGroupMembers }); } }, resolver: zalouserResolverAdapter, auth: zalouserAuthAdapter, message: zalouserMessageAdapter, status: createAsyncComputedAccountStatusAdapter({ defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID), collectStatusIssues: collectZalouserStatusIssues, buildChannelSummary: ({ snapshot }) => buildPassiveProbedChannelStatusSummary(snapshot), probeAccount: async ({ account, timeoutMs }) => (await loadZalouserChannelRuntime()).probeZalouser(account.profile, timeoutMs), resolveAccountSnapshot: async ({ account, runtime }) => { const configured = await checkZcaAuthenticated(account.profile); return { accountId: account.accountId, name: account.name, enabled: account.enabled, configured, extra: { dmPolicy: account.config.dmPolicy ?? "pairing", lastError: configured ? runtime?.lastError ?? null : runtime?.lastError ?? "not authenticated" } }; } }), gateway: { startAccount: async (ctx) => { const { getZaloUserInfo } = await loadZalouserChannelRuntime(); const account = ctx.account; let userLabel = ""; try { const userInfo = await getZaloUserInfo(account.profile); if (userInfo?.displayName) userLabel = ` (${userInfo.displayName})`; ctx.setStatus({ accountId: account.accountId, profile: userInfo }); } catch {} const statusSink = createAccountStatusSink({ accountId: ctx.accountId, setStatus: ctx.setStatus }); ctx.log?.info(`[${account.accountId}] starting zalouser provider${userLabel}`); const { monitorZalouserProvider } = await import("./monitor-CQbwKpeC.js"); return monitorZalouserProvider({ account, config: ctx.cfg, runtime: ctx.runtime, abortSignal: ctx.abortSignal, statusSink }); }, loginWithQrStart: async (params) => { const { startZaloQrLogin } = await loadZalouserChannelRuntime(); return await startZaloQrLogin({ profile: resolveZalouserQrProfile(params.accountId), force: params.force, timeoutMs: params.timeoutMs }); }, loginWithQrWait: async (params) => { const { waitForZaloQrLogin } = await loadZalouserChannelRuntime(); return await waitForZaloQrLogin({ profile: resolveZalouserQrProfile(params.accountId), timeoutMs: params.timeoutMs }); }, logoutAccount: async (ctx) => await (await loadZalouserChannelRuntime()).logoutZaloProfile(ctx.account.profile || resolveZalouserQrProfile(ctx.accountId)) } }, security: zalouserSecurityAdapter, threading: zalouserThreadingAdapter, pairing: { text: zalouserPairingTextAdapter }, outbound: zalouserOutboundAdapter }); //#endregion export { parseZalouserOutboundTarget as n, zalouserPlugin as t };