"use strict";Object.defineProperty(exports, "__esModule", { value: true });exports.wecomOutbound = void 0; var _apiClient = require("./agent/api-client.js"); var _index = require("./config/index.js"); var _runtime = require("./runtime.js"); var _wsAdapter = require("./ws-adapter.js"); var _target = require("./target.js");function _interopRequireWildcard(e, t) {if ("function" == typeof WeakMap) var r = new WeakMap(),n = new WeakMap();return (_interopRequireWildcard = function (e, t) {if (!t && e && e.__esModule) return e;var o,i,f = { __proto__: null, default: e };if (null === e || "object" != typeof e && "function" != typeof e) return f;if (o = t ? n : r) {if (o.has(e)) return o.get(e);o.set(e, f);}for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]);return f;})(e, t);} function resolveAgentConfigOrThrow(params) { const resolvedAccounts = (0, _index.resolveWecomAccounts)(params.cfg); const conflictAccountId = params.accountId?.trim() || resolvedAccounts.defaultAccountId; const conflict = (0, _index.resolveWecomAccountConflict)({ cfg: params.cfg, accountId: conflictAccountId }); if (conflict) { throw new Error(conflict.message); } const requestedAccountId = params.accountId?.trim(); if (requestedAccountId) { if (!resolvedAccounts.accounts[requestedAccountId]) { throw new Error( `WeCom outbound account "${requestedAccountId}" not found. Configure channels.wecom.accounts.${requestedAccountId} or use an existing accountId.` ); } } const account = (0, _index.resolveWecomAccount)({ cfg: params.cfg, accountId: params.accountId }).agent; if (!account?.configured) { throw new Error( `WeCom outbound requires Agent mode for account=${params.accountId ?? "default"}. Configure channels.wecom.accounts..agent (or legacy channels.wecom.agent).` ); } if (typeof account.agentId !== "number" || !Number.isFinite(account.agentId)) { throw new Error( `WeCom outbound requires channels.wecom.accounts..agent.agentId (or legacy channels.wecom.agent.agentId) for account=${params.accountId ?? account.accountId}.` ); } // 注意:不要在日志里输出 corpSecret 等敏感信息 console.log(`[wecom-outbound] Using agent config: accountId=${account.accountId}, corpId=${account.corpId}, agentId=${account.agentId}`); return account; } const wecomOutbound = exports.wecomOutbound = { deliveryMode: "direct", chunkerMode: "text", textChunkLimit: 20480, chunker: (text, limit) => { try { return (0, _runtime.getWecomRuntime)().channel.text.chunkText(text, limit); } catch { return [text]; } }, sendText: async ({ cfg, to, text, accountId }) => { // signal removed - not supported in current SDK // ── Bot WebSocket outbound 独立路径 ── // WS Bot 完全独立收发,不与 Agent 组成双模,失败时直接抛错而非 fallthrough const resolvedAccount = (0, _index.resolveWecomAccount)({ cfg, accountId }); const botAccount = resolvedAccount.bot; if (botAccount?.connectionMode === 'websocket' && botAccount.configured) { const wsClient = (0, _wsAdapter.getWsClient)(botAccount.accountId); const wsTarget = (0, _target.resolveWecomTarget)(to); const chatid = wsTarget?.touser || wsTarget?.chatid; // 如果目标是 Agent 会话(wecom-agent:),跳过 WS Bot,走 Agent outbound const rawTo = typeof to === "string" ? to.trim().toLowerCase() : ""; if (!rawTo.startsWith("wecom-agent:")) { if (!wsClient?.isConnected) { console.log(`[wecom-outbound] Bot WS 未连接,等待重连... (accountId=${botAccount.accountId})`); const reconnected = await (0, _wsAdapter.waitForWsConnection)(botAccount.accountId, 10_000); if (!reconnected) { throw new Error(`[wecom-outbound] Bot WS 等待重连超时,无法发送消息 (accountId=${botAccount.accountId})`); } } if (!chatid) { throw new Error(`[wecom-outbound] Bot WS 无法解析目标 chatid (to=${String(to)})`); } // 重连后重新获取 client(可能是新实例) const activeClient = (0, _wsAdapter.getWsClient)(botAccount.accountId); if (!activeClient?.isConnected) { throw new Error(`[wecom-outbound] Bot WS 重连后仍不可用 (accountId=${botAccount.accountId})`); } await activeClient.sendMessage(chatid, { msgtype: 'markdown', markdown: { content: text } }); console.log(`[wecom-outbound] Sent text via Bot WS to chatid=${chatid} (len=${text.length})`); return { channel: "wecom", messageId: `ws-bot-${Date.now()}`, timestamp: Date.now() }; } } // ── Agent outbound(Webhook Bot 双模 / Agent 独立)── const agent = resolveAgentConfigOrThrow({ cfg, accountId }); const target = (0, _target.resolveWecomTarget)(to); if (!target) { throw new Error("WeCom outbound requires a target (userid, partyid, tagid or chatid)."); } // 体验优化:/new /reset 的“New session started”回执在 OpenClaw 核心里是英文固定文案, // 且通过 routeReply 走 wecom outbound(Agent 主动发送)。 // 在 WeCom“双模式”场景下,这会造成: // - 用户在 Bot 会话发 /new,但却收到一条 Agent 私信回执(双重回复/错会话)。 // 因此: // - Bot 会话目标:抑制该回执(Bot 会话里由 wecom 插件补中文回执)。 // - Agent 会话目标(wecom-agent:):允许发送,但改写成中文。 let outgoingText = text; const trimmed = String(outgoingText ?? "").trim(); const rawTo = typeof to === "string" ? to.trim().toLowerCase() : ""; const isAgentSessionTarget = rawTo.startsWith("wecom-agent:"); const looksLikeNewSessionAck = /new session started/i.test(trimmed) && /model:/i.test(trimmed); if (looksLikeNewSessionAck) { if (!isAgentSessionTarget) { console.log(`[wecom-outbound] Suppressed command ack to avoid Bot/Agent double-reply (len=${trimmed.length})`); return { channel: "wecom", messageId: `suppressed-${Date.now()}`, timestamp: Date.now() }; } const modelLabel = (() => { const m = trimmed.match(/model:\s*([^\n()]+)\s*/i); return m?.[1]?.trim(); })(); const rewritten = modelLabel ? `✅ 已开启新会话(模型:${modelLabel})` : "✅ 已开启新会话。"; console.log(`[wecom-outbound] Rewrote command ack for agent session (len=${rewritten.length})`); outgoingText = rewritten; } const { touser, toparty, totag, chatid } = target; if (chatid) { throw new Error( `企业微信(WeCom)Agent 主动发送不支持向群 chatId 发送(chatId=${chatid})。` + `该路径在实际环境中经常失败(例如 86008:无权限访问该会话/会话由其他应用创建)。` + `请改为发送给用户(userid / user:xxx),或由 Bot 模式在群内交付。` ); } console.log(`[wecom-outbound] Sending text to target=${JSON.stringify(target)} (len=${outgoingText.length})`); try { await (0, _apiClient.sendText)({ agent, toUser: touser, toParty: toparty, toTag: totag, chatId: chatid, text: outgoingText }); console.log(`[wecom-outbound] Successfully sent text to ${JSON.stringify(target)}`); } catch (err) { console.error(`[wecom-outbound] Failed to send text to ${JSON.stringify(target)}:`, err); throw err; } return { channel: "wecom", messageId: `agent-${Date.now()}`, timestamp: Date.now() }; }, sendMedia: async ({ cfg, to, text, mediaUrl, accountId }) => { // signal removed - not supported in current SDK const agent = resolveAgentConfigOrThrow({ cfg, accountId }); const target = (0, _target.resolveWecomTarget)(to); if (!target) { throw new Error("WeCom outbound requires a target (userid, partyid, tagid or chatid)."); } if (target.chatid) { throw new Error( `企业微信(WeCom)Agent 主动发送不支持向群 chatId 发送(chatId=${target.chatid})。` + `该路径在实际环境中经常失败(例如 86008:无权限访问该会话/会话由其他应用创建)。` + `请改为发送给用户(userid / user:xxx),或由 Bot 模式在群内交付。` ); } if (!mediaUrl) { throw new Error("WeCom outbound requires mediaUrl."); } let buffer; let contentType; let filename; // 判断是 URL 还是本地文件路径 const isRemoteUrl = /^https?:\/\//i.test(mediaUrl); if (isRemoteUrl) { const res = await fetch(mediaUrl, { signal: AbortSignal.timeout(30000) }); if (!res.ok) { throw new Error(`Failed to download media: ${res.status}`); } buffer = Buffer.from(await res.arrayBuffer()); contentType = res.headers.get("content-type") || "application/octet-stream"; const urlPath = new URL(mediaUrl).pathname; filename = urlPath.split("/").pop() || "media"; } else { // 本地文件路径 const fs = await Promise.resolve().then(() => jitiImport("node:fs/promises").then((m) => _interopRequireWildcard(m))); const path = await Promise.resolve().then(() => jitiImport("node:path").then((m) => _interopRequireWildcard(m))); buffer = await fs.readFile(mediaUrl); filename = path.basename(mediaUrl); // 根据扩展名推断 content-type const ext = path.extname(mediaUrl).slice(1).toLowerCase(); const mimeTypes = { jpg: "image/jpeg", jpeg: "image/jpeg", png: "image/png", gif: "image/gif", webp: "image/webp", bmp: "image/bmp", mp3: "audio/mpeg", wav: "audio/wav", amr: "audio/amr", mp4: "video/mp4", pdf: "application/pdf", doc: "application/msword", docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", xls: "application/vnd.ms-excel", xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ppt: "application/vnd.ms-powerpoint", pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation", txt: "text/plain", csv: "text/csv", tsv: "text/tab-separated-values", md: "text/markdown", json: "application/json", xml: "application/xml", yaml: "application/yaml", yml: "application/yaml", zip: "application/zip", rar: "application/vnd.rar", "7z": "application/x-7z-compressed", tar: "application/x-tar", gz: "application/gzip", tgz: "application/gzip", rtf: "application/rtf", odt: "application/vnd.oasis.opendocument.text" }; contentType = mimeTypes[ext] || "application/octet-stream"; console.log(`[wecom-outbound] Reading local file: ${mediaUrl}, ext=${ext}, contentType=${contentType}`); } let mediaType = "file"; if (contentType.startsWith("image/")) mediaType = "image";else if (contentType.startsWith("audio/")) mediaType = "voice";else if (contentType.startsWith("video/")) mediaType = "video"; const mediaId = await (0, _apiClient.uploadMedia)({ agent, type: mediaType, buffer, filename }); const { touser, toparty, totag, chatid } = target; console.log(`[wecom-outbound] Sending media (${mediaType}) to ${JSON.stringify(target)} (mediaId=${mediaId})`); try { await (0, _apiClient.sendMedia)({ agent, toUser: touser, toParty: toparty, toTag: totag, chatId: chatid, mediaId, mediaType, ...(mediaType === "video" && text?.trim() ? { title: text.trim().slice(0, 64), description: text.trim().slice(0, 512) } : {}) }); console.log(`[wecom-outbound] Successfully sent media to ${JSON.stringify(target)}`); } catch (err) { console.error(`[wecom-outbound] Failed to send media to ${JSON.stringify(target)}:`, err); throw err; } return { channel: "wecom", messageId: `agent-media-${Date.now()}`, timestamp: Date.now() }; } }; /* v9-d3d1b8f40ab07be3 */