@wecom/wecom-openclaw-plugin 2026.3.31-beta.0 → 2026.4.1-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/README.md +4 -251
  2. package/dist/cjs/index.js +45 -0
  3. package/dist/cjs/index.js.map +1 -0
  4. package/dist/cjs/src/channel.js +339 -0
  5. package/dist/cjs/src/channel.js.map +1 -0
  6. package/dist/cjs/src/const.js +137 -0
  7. package/dist/cjs/src/const.js.map +1 -0
  8. package/dist/cjs/src/dm-policy.js +89 -0
  9. package/dist/cjs/src/dm-policy.js.map +1 -0
  10. package/dist/cjs/src/group-policy.js +133 -0
  11. package/dist/cjs/src/group-policy.js.map +1 -0
  12. package/dist/cjs/src/mcp/interceptors/biz-error.js +70 -0
  13. package/dist/cjs/src/mcp/interceptors/biz-error.js.map +1 -0
  14. package/dist/cjs/src/mcp/interceptors/index.js +73 -0
  15. package/dist/cjs/src/mcp/interceptors/index.js.map +1 -0
  16. package/dist/cjs/src/mcp/interceptors/msg-media.js +159 -0
  17. package/dist/cjs/src/mcp/interceptors/msg-media.js.map +1 -0
  18. package/dist/cjs/src/mcp/interceptors/smartpage-create.js +157 -0
  19. package/dist/cjs/src/mcp/interceptors/smartpage-create.js.map +1 -0
  20. package/dist/cjs/src/mcp/interceptors/smartpage-export.js +93 -0
  21. package/dist/cjs/src/mcp/interceptors/smartpage-export.js.map +1 -0
  22. package/dist/cjs/src/mcp/schema.js +104 -0
  23. package/dist/cjs/src/mcp/schema.js.map +1 -0
  24. package/dist/cjs/src/mcp/tool.js +208 -0
  25. package/dist/cjs/src/mcp/tool.js.map +1 -0
  26. package/dist/cjs/src/mcp/transport.js +417 -0
  27. package/dist/cjs/src/mcp/transport.js.map +1 -0
  28. package/dist/cjs/src/media-handler.js +129 -0
  29. package/dist/cjs/src/media-handler.js.map +1 -0
  30. package/dist/cjs/src/media-uploader.js +284 -0
  31. package/dist/cjs/src/media-uploader.js.map +1 -0
  32. package/dist/cjs/src/message-parser.js +134 -0
  33. package/dist/cjs/src/message-parser.js.map +1 -0
  34. package/dist/cjs/src/message-sender.js +89 -0
  35. package/dist/cjs/src/message-sender.js.map +1 -0
  36. package/dist/cjs/src/monitor.js +991 -0
  37. package/dist/cjs/src/monitor.js.map +1 -0
  38. package/dist/cjs/src/onboarding.js +182 -0
  39. package/dist/cjs/src/onboarding.js.map +1 -0
  40. package/dist/cjs/src/openclaw-compat.js +400 -0
  41. package/dist/cjs/src/openclaw-compat.js.map +1 -0
  42. package/dist/cjs/src/reqid-store.js +88 -0
  43. package/dist/cjs/src/reqid-store.js.map +1 -0
  44. package/dist/cjs/src/runtime.js +16 -0
  45. package/dist/cjs/src/runtime.js.map +1 -0
  46. package/dist/cjs/src/state-dir-resolve.js +35 -0
  47. package/dist/cjs/src/state-dir-resolve.js.map +1 -0
  48. package/dist/cjs/src/state-manager.js +158 -0
  49. package/dist/cjs/src/state-manager.js.map +1 -0
  50. package/dist/cjs/src/template-card-parser.js +641 -0
  51. package/dist/cjs/src/template-card-parser.js.map +1 -0
  52. package/dist/cjs/src/timeout.js +42 -0
  53. package/dist/cjs/src/timeout.js.map +1 -0
  54. package/dist/cjs/src/utils.js +60 -0
  55. package/dist/cjs/src/utils.js.map +1 -0
  56. package/dist/cjs/src/version.js +26 -0
  57. package/dist/cjs/src/version.js.map +1 -0
  58. package/dist/esm/index.js +41 -0
  59. package/dist/esm/index.js.map +1 -0
  60. package/dist/esm/src/channel.js +337 -0
  61. package/dist/esm/src/channel.js.map +1 -0
  62. package/dist/{src → esm/src}/const.js +30 -77
  63. package/dist/esm/src/const.js.map +1 -0
  64. package/dist/{src → esm/src}/dm-policy.js +9 -6
  65. package/dist/esm/src/dm-policy.js.map +1 -0
  66. package/dist/{src → esm/src}/group-policy.js +7 -3
  67. package/dist/esm/src/group-policy.js.map +1 -0
  68. package/dist/{src → esm/src}/mcp/interceptors/biz-error.js +6 -2
  69. package/dist/esm/src/mcp/interceptors/biz-error.js.map +1 -0
  70. package/dist/{src → esm/src}/mcp/interceptors/index.js +10 -6
  71. package/dist/esm/src/mcp/interceptors/index.js.map +1 -0
  72. package/dist/{src → esm/src}/mcp/interceptors/msg-media.js +10 -6
  73. package/dist/esm/src/mcp/interceptors/msg-media.js.map +1 -0
  74. package/dist/{src → esm/src}/mcp/interceptors/smartpage-create.js +6 -2
  75. package/dist/esm/src/mcp/interceptors/smartpage-create.js.map +1 -0
  76. package/dist/{src → esm/src}/mcp/interceptors/smartpage-export.js +6 -2
  77. package/dist/esm/src/mcp/interceptors/smartpage-export.js.map +1 -0
  78. package/dist/{src → esm/src}/mcp/schema.js +4 -1
  79. package/dist/esm/src/mcp/schema.js.map +1 -0
  80. package/dist/{src → esm/src}/mcp/tool.js +8 -4
  81. package/dist/esm/src/mcp/tool.js.map +1 -0
  82. package/dist/{src → esm/src}/mcp/transport.js +16 -15
  83. package/dist/esm/src/mcp/transport.js.map +1 -0
  84. package/dist/{src → esm/src}/media-handler.js +10 -6
  85. package/dist/esm/src/media-handler.js.map +1 -0
  86. package/dist/{src → esm/src}/media-uploader.js +10 -64
  87. package/dist/esm/src/media-uploader.js.map +1 -0
  88. package/dist/{src → esm/src}/message-parser.js +4 -1
  89. package/dist/esm/src/message-parser.js.map +1 -0
  90. package/dist/{src → esm/src}/message-sender.js +11 -46
  91. package/dist/esm/src/message-sender.js.map +1 -0
  92. package/dist/{src → esm/src}/monitor.js +282 -151
  93. package/dist/esm/src/monitor.js.map +1 -0
  94. package/dist/{src → esm/src}/onboarding.js +26 -22
  95. package/dist/esm/src/onboarding.js.map +1 -0
  96. package/dist/{src → esm/src}/openclaw-compat.js +27 -143
  97. package/dist/esm/src/openclaw-compat.js.map +1 -0
  98. package/dist/{src → esm/src}/reqid-store.js +7 -4
  99. package/dist/esm/src/reqid-store.js.map +1 -0
  100. package/dist/{src → esm/src}/runtime.js +5 -2
  101. package/dist/esm/src/runtime.js.map +1 -0
  102. package/dist/esm/src/state-dir-resolve.js +13 -0
  103. package/dist/esm/src/state-dir-resolve.js.map +1 -0
  104. package/dist/{src → esm/src}/state-manager.js +16 -84
  105. package/dist/esm/src/state-manager.js.map +1 -0
  106. package/dist/{src → esm/src}/template-card-parser.js +8 -17
  107. package/dist/esm/src/template-card-parser.js.map +1 -0
  108. package/dist/{src → esm/src}/timeout.js +5 -2
  109. package/dist/esm/src/timeout.js.map +1 -0
  110. package/dist/{src → esm/src}/utils.js +10 -24
  111. package/dist/esm/src/utils.js.map +1 -0
  112. package/dist/esm/src/version.js +23 -0
  113. package/dist/esm/src/version.js.map +1 -0
  114. package/dist/esm/types/index.d.ts +9 -0
  115. package/dist/{src → esm/types/src}/const.d.ts +0 -46
  116. package/dist/{src → esm/types/src}/interface.d.ts +2 -0
  117. package/dist/{src → esm/types/src}/message-sender.d.ts +0 -18
  118. package/dist/{src → esm/types/src}/openclaw-compat.d.ts +0 -49
  119. package/dist/esm/types/src/state-dir-resolve.d.ts +2 -0
  120. package/dist/{src → esm/types/src}/utils.d.ts +0 -39
  121. package/dist/esm/types/src/version.d.ts +2 -0
  122. package/dist/index.d.ts +4 -2
  123. package/package.json +7 -15
  124. package/dist/index.js +0 -113
  125. package/dist/src/accounts.d.ts +0 -57
  126. package/dist/src/accounts.js +0 -222
  127. package/dist/src/agent/api-client.d.ts +0 -94
  128. package/dist/src/agent/api-client.js +0 -292
  129. package/dist/src/agent/handler.d.ts +0 -63
  130. package/dist/src/agent/handler.js +0 -532
  131. package/dist/src/agent/index.d.ts +0 -5
  132. package/dist/src/agent/index.js +0 -5
  133. package/dist/src/agent/webhook.d.ts +0 -23
  134. package/dist/src/agent/webhook.js +0 -226
  135. package/dist/src/agent/xml.d.ts +0 -21
  136. package/dist/src/agent/xml.js +0 -41
  137. package/dist/src/channel.js +0 -564
  138. package/dist/src/chat-queue.d.ts +0 -31
  139. package/dist/src/chat-queue.js +0 -45
  140. package/dist/src/http.d.ts +0 -26
  141. package/dist/src/http.js +0 -93
  142. package/dist/src/interface.js +0 -4
  143. package/dist/src/mcp/index.js +0 -6
  144. package/dist/src/mcp/interceptors/types.js +0 -7
  145. package/dist/src/shared/command-auth.d.ts +0 -23
  146. package/dist/src/shared/command-auth.js +0 -73
  147. package/dist/src/shared/xml-parser.d.ts +0 -46
  148. package/dist/src/shared/xml-parser.js +0 -210
  149. package/dist/src/target.d.ts +0 -35
  150. package/dist/src/target.js +0 -67
  151. package/dist/src/template-card-manager.d.ts +0 -55
  152. package/dist/src/template-card-manager.js +0 -205
  153. package/dist/src/types/account.d.ts +0 -29
  154. package/dist/src/types/account.js +0 -4
  155. package/dist/src/types/config.d.ts +0 -80
  156. package/dist/src/types/config.js +0 -7
  157. package/dist/src/types/constants.d.ts +0 -42
  158. package/dist/src/types/constants.js +0 -42
  159. package/dist/src/types/index.d.ts +0 -7
  160. package/dist/src/types/index.js +0 -5
  161. package/dist/src/types/message.d.ts +0 -238
  162. package/dist/src/types/message.js +0 -5
  163. package/dist/src/version.d.ts +0 -2
  164. package/dist/src/version.js +0 -24
  165. package/dist/src/webhook/command-auth.d.ts +0 -47
  166. package/dist/src/webhook/command-auth.js +0 -96
  167. package/dist/src/webhook/gateway.d.ts +0 -36
  168. package/dist/src/webhook/gateway.js +0 -227
  169. package/dist/src/webhook/handler.d.ts +0 -18
  170. package/dist/src/webhook/handler.js +0 -420
  171. package/dist/src/webhook/helpers.d.ts +0 -156
  172. package/dist/src/webhook/helpers.js +0 -833
  173. package/dist/src/webhook/http.d.ts +0 -26
  174. package/dist/src/webhook/http.js +0 -94
  175. package/dist/src/webhook/index.d.ts +0 -11
  176. package/dist/src/webhook/index.js +0 -14
  177. package/dist/src/webhook/media.d.ts +0 -29
  178. package/dist/src/webhook/media.js +0 -88
  179. package/dist/src/webhook/monitor.d.ts +0 -59
  180. package/dist/src/webhook/monitor.js +0 -1147
  181. package/dist/src/webhook/state.d.ts +0 -220
  182. package/dist/src/webhook/state.js +0 -491
  183. package/dist/src/webhook/target.d.ts +0 -41
  184. package/dist/src/webhook/target.js +0 -132
  185. package/dist/src/webhook/types.d.ts +0 -347
  186. package/dist/src/webhook/types.js +0 -33
  187. /package/dist/{src → esm/types/src}/channel.d.ts +0 -0
  188. /package/dist/{src → esm/types/src}/dm-policy.d.ts +0 -0
  189. /package/dist/{src → esm/types/src}/group-policy.d.ts +0 -0
  190. /package/dist/{src → esm/types/src}/mcp/index.d.ts +0 -0
  191. /package/dist/{src → esm/types/src}/mcp/interceptors/biz-error.d.ts +0 -0
  192. /package/dist/{src → esm/types/src}/mcp/interceptors/index.d.ts +0 -0
  193. /package/dist/{src → esm/types/src}/mcp/interceptors/msg-media.d.ts +0 -0
  194. /package/dist/{src → esm/types/src}/mcp/interceptors/smartpage-create.d.ts +0 -0
  195. /package/dist/{src → esm/types/src}/mcp/interceptors/smartpage-export.d.ts +0 -0
  196. /package/dist/{src → esm/types/src}/mcp/interceptors/types.d.ts +0 -0
  197. /package/dist/{src → esm/types/src}/mcp/schema.d.ts +0 -0
  198. /package/dist/{src → esm/types/src}/mcp/tool.d.ts +0 -0
  199. /package/dist/{src → esm/types/src}/mcp/transport.d.ts +0 -0
  200. /package/dist/{src → esm/types/src}/media-handler.d.ts +0 -0
  201. /package/dist/{src → esm/types/src}/media-uploader.d.ts +0 -0
  202. /package/dist/{src → esm/types/src}/message-parser.d.ts +0 -0
  203. /package/dist/{src → esm/types/src}/monitor.d.ts +0 -0
  204. /package/dist/{src → esm/types/src}/onboarding.d.ts +0 -0
  205. /package/dist/{src → esm/types/src}/reqid-store.d.ts +0 -0
  206. /package/dist/{src → esm/types/src}/runtime.d.ts +0 -0
  207. /package/dist/{src → esm/types/src}/state-manager.d.ts +0 -0
  208. /package/dist/{src → esm/types/src}/template-card-parser.d.ts +0 -0
  209. /package/dist/{src → esm/types/src}/timeout.d.ts +0 -0
@@ -1,420 +0,0 @@
1
- /**
2
- * Webhook HTTP 请求处理
3
- *
4
- * 从 @mocrane/wecom monitor.ts handleWecomWebhookRequest 部分迁移 + 重构。
5
- * 负责:
6
- * 1. GET/POST 请求分流
7
- * 2. 签名验证(调用 crypto 模块)
8
- * 3. 消息解密
9
- * 4. 按消息类型分发到 monitor 层
10
- */
11
- import crypto from "node:crypto";
12
- import { getRegisteredTargets, getWebhookTargetsMap, parseWebhookPath } from "./target.js";
13
- import { resolveWecomEgressProxyUrl } from "../utils.js";
14
- import { handleInboundMessage, handleStreamRefresh, handleEnterChat, handleTemplateCardEvent, } from "./monitor.js";
15
- import { hasActiveTargets } from "./target.js";
16
- import { resolveWecomSenderUserId, } from "./helpers.js";
17
- import { WecomCrypto } from "@wecom/aibot-node-sdk";
18
- // ============================================================================
19
- // 辅助函数
20
- // ============================================================================
21
- /** 解析 URL 查询参数 */
22
- function parseQuery(url) {
23
- const idx = url.indexOf("?");
24
- if (idx < 0)
25
- return {};
26
- const params = new URLSearchParams(url.slice(idx + 1));
27
- const result = {};
28
- for (const [key, value] of params) {
29
- result[key] = value;
30
- }
31
- return result;
32
- }
33
- /**
34
- * 从查询参数中提取签名字段
35
- *
36
- * 企微不同场景下签名参数名不一致,按优先级依次尝试:
37
- * msg_signature → msgsignature → signature
38
- */
39
- function resolveSignatureParam(query) {
40
- return query.msg_signature ?? query.msgsignature ?? query.signature ?? "";
41
- }
42
- /**
43
- * 判断入站消息是否应该被处理(对齐原版 shouldProcessBotInboundMessage)
44
- *
45
- * 仅允许"真实用户消息"进入 Bot 会话:
46
- * - 发送者缺失 → 丢弃(避免 unknown 会话串会话)
47
- * - 发送者是 sys → 丢弃(避免系统回调触发 AI 自动回复)
48
- * - 群消息缺失 chatid → 丢弃(避免 group:unknown 串群)
49
- */
50
- function shouldProcessBotInboundMessage(msg) {
51
- const senderUserId = resolveWecomSenderUserId(msg)?.trim();
52
- if (!senderUserId) {
53
- return { shouldProcess: false, reason: "missing_sender" };
54
- }
55
- if (senderUserId.toLowerCase() === "sys") {
56
- return { shouldProcess: false, reason: "system_sender" };
57
- }
58
- // 企微 Bot 回调中 chattype 是扁平字段(非嵌套在 chat_info 内)
59
- const chatType = String(msg.chattype ?? "").trim().toLowerCase();
60
- if (chatType === "group") {
61
- const chatId = msg.chatid?.trim();
62
- if (!chatId) {
63
- return { shouldProcess: false, reason: "missing_chatid", senderUserId };
64
- }
65
- return { shouldProcess: true, reason: "user_message", senderUserId, chatId };
66
- }
67
- return { shouldProcess: true, reason: "user_message", senderUserId, chatId: senderUserId };
68
- }
69
- /**
70
- * 从 Target 配置中提取预期的 Bot Identity 集合
71
- *
72
- * 用于 aibotid 校验:即使签名匹配,也要确认消息来自预期的 Bot。
73
- *
74
- * 配置来源(对齐用户 YAML 配置):
75
- * - 单账号模式:channels.wecom.botId
76
- * - 多账号模式:channels.wecom.accounts.xxx.botId
77
- *
78
- * 解析后的 botId 已在 account.botId 中,直接读取即可。
79
- * 同时兼容 config 中可能存在的 aibotid(原版字段名)。
80
- */
81
- function resolveBotIdentitySet(target) {
82
- const ids = new Set();
83
- // account.botId — 从 YAML 配置中解析出的 botId(单账号/多账号均可)
84
- const botId = target.account.botId?.trim();
85
- if (botId)
86
- ids.add(botId);
87
- // config.botId — 与 account.botId 相同来源(兜底)
88
- const configBotId = target.account.config.botId?.trim();
89
- if (configBotId)
90
- ids.add(configBotId);
91
- return ids;
92
- }
93
- /** POST body 最大允许字节数 (1 MB) */
94
- const MAX_BODY_BYTES = 1024 * 1024;
95
- /**
96
- * 读取 HTTP 请求 body(带大小限制保护)
97
- *
98
- * 超过 maxBytes 时会主动销毁请求并拒绝,防止大包攻击。
99
- */
100
- function readBody(req, maxBytes = MAX_BODY_BYTES) {
101
- return new Promise((resolve) => {
102
- const chunks = [];
103
- let total = 0;
104
- req.on("data", (chunk) => {
105
- total += chunk.length;
106
- if (total > maxBytes) {
107
- resolve({ ok: false, error: "payload too large" });
108
- req.destroy();
109
- return;
110
- }
111
- chunks.push(chunk);
112
- });
113
- req.on("end", () => {
114
- const raw = Buffer.concat(chunks).toString("utf8");
115
- if (!raw.trim()) {
116
- resolve({ ok: false, error: "empty payload" });
117
- return;
118
- }
119
- resolve({ ok: true, value: raw });
120
- });
121
- req.on("error", (err) => {
122
- resolve({ ok: false, error: err instanceof Error ? err.message : String(err) });
123
- });
124
- });
125
- }
126
- /** 构造加密 JSON 响应(返回对象,不做 stringify) */
127
- function encryptResponse(target, responseData, timestamp, nonce) {
128
- const plaintext = JSON.stringify(responseData);
129
- const wc = new WecomCrypto(target.account.token, target.account.encodingAESKey, target.account.receiveId);
130
- const { encrypt, signature } = wc.encrypt(plaintext, timestamp, nonce);
131
- return { encrypt, msgsignature: signature, timestamp, nonce };
132
- }
133
- /** 发送 JSON 响应 (Content-Type: application/json) */
134
- function sendJson(res, statusCode, data) {
135
- res.writeHead(statusCode, { "Content-Type": "application/json" });
136
- res.end(JSON.stringify(data));
137
- }
138
- /**
139
- * 发送加密回复响应 (Content-Type: text/plain)
140
- *
141
- * 企微官方参考实现要求加密 JSON 以 text/plain 返回。
142
- */
143
- function sendEncryptedReply(res, data) {
144
- res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
145
- res.end(JSON.stringify(data));
146
- }
147
- /** 发送纯文本响应 */
148
- function sendText(res, statusCode, text) {
149
- res.writeHead(statusCode, { "Content-Type": "text/plain charset=utf-8" });
150
- res.end(text);
151
- }
152
- // ============================================================================
153
- // 路径解析
154
- // ============================================================================
155
- /**
156
- * 标准化 Webhook 路径(不含 query string)
157
- */
158
- function normalizeRequestPath(url) {
159
- const idx = url.indexOf("?");
160
- const pathname = idx >= 0 ? url.slice(0, idx) : url;
161
- const trimmed = pathname.trim();
162
- if (!trimmed || trimmed === "/")
163
- return "/";
164
- const withSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
165
- if (withSlash.length > 1 && withSlash.endsWith("/"))
166
- return withSlash.slice(0, -1);
167
- return withSlash;
168
- }
169
- /** 按 accountId 去重 Target 列表(同一 account 注册多条路径时只保留第一个) */
170
- function deduplicateByAccountId(targets) {
171
- const seen = new Set();
172
- const result = [];
173
- for (const target of targets) {
174
- if (!seen.has(target.account.accountId)) {
175
- seen.add(target.account.accountId);
176
- result.push(target);
177
- }
178
- }
179
- return result;
180
- }
181
- /**
182
- * 从已注册的 Target 中匹配签名
183
- *
184
- * 匹配策略:
185
- * 1. 如果路径中有 accountId,优先精确匹配
186
- * 2. 用 filter 收集所有签名匹配的 Target
187
- * 3. 检查冲突:0 个 = not_found,1 个 = matched,>1 个 = conflict
188
- *
189
- * 与原版保持一致:检查 target.account.token 存在性,防止空 token 的误匹配。
190
- */
191
- function findMatchingTarget(requestPath, signature, timestamp, nonce, encrypt, pathAccountId) {
192
- // 收集所有候选 Target(路径匹配 + 全局兜底)
193
- const targetsMap = getWebhookTargetsMap();
194
- const normalizedPath = normalizeRequestPath(requestPath);
195
- const pathTargets = targetsMap.get(normalizedPath);
196
- // 如果路径中有 accountId,优先精确匹配
197
- if (pathAccountId && pathTargets) {
198
- const byAccountId = pathTargets.find((t) => t.account.accountId === pathAccountId);
199
- if (byAccountId?.account?.token) {
200
- const wc = new WecomCrypto(byAccountId.account.token, byAccountId.account.encodingAESKey, byAccountId.account.receiveId);
201
- const ok = wc.verifySignature(signature, timestamp, nonce, encrypt);
202
- if (ok)
203
- return { status: "matched", target: byAccountId };
204
- }
205
- }
206
- // 收集候选列表(路径匹配优先,否则全局遍历)
207
- const candidates = (pathTargets && pathTargets.length > 0)
208
- ? pathTargets
209
- : getRegisteredTargets();
210
- // filter 语义:收集所有签名匹配的 Target
211
- const signatureMatches = candidates.filter((target) => {
212
- if (!target?.account?.token)
213
- return false;
214
- const wc = new WecomCrypto(target.account.token, target.account.encodingAESKey, target.account.receiveId);
215
- return wc.verifySignature(signature, timestamp, nonce, encrypt);
216
- });
217
- // 按 accountId 去重(同一 account 注册多条路径时,不应被误判为冲突)
218
- const uniqueMatches = deduplicateByAccountId(signatureMatches);
219
- if (uniqueMatches.length === 1) {
220
- return { status: "matched", target: uniqueMatches[0] };
221
- }
222
- const candidateAccountIds = (uniqueMatches.length > 0 ? uniqueMatches : candidates)
223
- .map((t) => t.account.accountId);
224
- if (uniqueMatches.length === 0) {
225
- return { status: "not_found", candidateAccountIds };
226
- }
227
- // uniqueMatches.length > 1 → 多账号冲突
228
- return { status: "conflict", candidateAccountIds };
229
- }
230
- // ============================================================================
231
- // 主入口
232
- // ============================================================================
233
- /**
234
- * Webhook HTTP 请求总入口
235
- *
236
- * 处理企微 Bot Webhook 的 GET(URL 验证)和 POST(消息回调)请求。
237
- * 返回 true 表示已处理,false 表示不匹配(交给其他 handler)。
238
- */
239
- export async function handleWecomWebhookRequest(req, res) {
240
- // ── 入站诊断日志(不输出敏感参数内容,仅输出是否存在)──────────────
241
- const reqId = crypto.randomUUID().slice(0, 8);
242
- const url = req.url ?? "/";
243
- const method = (req.method ?? "GET").toUpperCase();
244
- const remote = req.socket?.remoteAddress ?? "unknown";
245
- const ua = String(req.headers["user-agent"] ?? "");
246
- const cl = String(req.headers["content-length"] ?? "");
247
- const query = parseQuery(url);
248
- const hasTimestamp = Boolean(query.timestamp);
249
- const hasNonce = Boolean(query.nonce);
250
- const hasEchostr = Boolean(query.echostr);
251
- const signature = resolveSignatureParam(query);
252
- const hasSig = Boolean(signature);
253
- console.log(`[wecom] inbound(http): reqId=${reqId} path=${url.split("?")[0]} method=${method} remote=${remote} ua=${ua ? `"${ua}"` : "N/A"} contentLength=${cl || "N/A"} query={timestamp:${hasTimestamp},nonce:${hasNonce},echostr:${hasEchostr},signature:${hasSig}}`);
254
- if (!hasActiveTargets()) {
255
- console.log(`[wecom] inbound(http): reqId=${reqId} skipped — no active targets`);
256
- return false; // 无注册 Target,不处理
257
- }
258
- const pathAccountId = parseWebhookPath(url);
259
- // ── GET 请求:URL 验证 ──────────────────────────────────────────
260
- if (method === "GET") {
261
- const { timestamp, nonce, echostr } = query;
262
- const msgSignature = resolveSignatureParam(query);
263
- if (!msgSignature || !timestamp || !nonce || !echostr) {
264
- sendText(res, 400, "missing required query parameters");
265
- return true;
266
- }
267
- const matchResult = findMatchingTarget(url, msgSignature, timestamp, nonce, echostr, pathAccountId);
268
- if (matchResult.status !== "matched") {
269
- console.log(`[wecom] inbound(http): reqId=${reqId} GET route_failure reason=${matchResult.status} candidates=[${matchResult.candidateAccountIds.join(",")}]`);
270
- sendText(res, 403, "signature verification failed");
271
- return true;
272
- }
273
- const target = matchResult.target;
274
- target.runtime.log?.(`[webhook] GET URL 验证成功 (account=${target.account.accountId})`);
275
- try {
276
- const wc = new WecomCrypto(target.account.token, target.account.encodingAESKey, target.account.receiveId);
277
- const plaintext = wc.decrypt(echostr);
278
- sendText(res, 200, plaintext);
279
- }
280
- catch (err) {
281
- target.runtime.log?.(`[webhook] echostr 解密失败: ${err instanceof Error ? err.message : String(err)}`);
282
- sendText(res, 403, "decryption failed");
283
- }
284
- return true;
285
- }
286
- // ── POST 请求:消息回调 ──────────────────────────────────────────
287
- if (method === "POST") {
288
- const { timestamp, nonce } = query;
289
- const msgSignature = resolveSignatureParam(query);
290
- if (!msgSignature || !timestamp || !nonce) {
291
- sendJson(res, 400, { error: "missing required query parameters" });
292
- return true;
293
- }
294
- const bodyResult = await readBody(req);
295
- if (!bodyResult.ok) {
296
- sendJson(res, 400, { error: bodyResult.error });
297
- return true;
298
- }
299
- const bodyStr = bodyResult.value;
300
- let body;
301
- try {
302
- body = JSON.parse(bodyStr);
303
- }
304
- catch {
305
- sendJson(res, 400, { error: "invalid JSON body" });
306
- return true;
307
- }
308
- // 兼容 encrypt / Encrypt 两种大小写(企微不同场景下字段名不一致)
309
- const encrypt = String(body.encrypt ?? body.Encrypt ?? "");
310
- // POST body 诊断日志(不输出 encrypt 内容)
311
- console.log(`[wecom] inbound(bot): reqId=${reqId} rawJsonBytes=${Buffer.byteLength(bodyStr, "utf8")} hasEncrypt=${Boolean(encrypt)} encryptLen=${encrypt.length}`);
312
- if (!encrypt) {
313
- sendJson(res, 400, { error: "missing encrypt field" });
314
- return true;
315
- }
316
- // 多账号签名匹配
317
- const matchResult = findMatchingTarget(url, msgSignature, timestamp, nonce, encrypt, pathAccountId);
318
- if (matchResult.status !== "matched") {
319
- const reason = matchResult.status === "conflict"
320
- ? "wecom_account_conflict"
321
- : "wecom_account_not_found";
322
- const detail = matchResult.status === "conflict"
323
- ? "Bot callback account conflict: multiple accounts matched signature."
324
- : "Bot callback account not found: signature verification failed.";
325
- console.log(`[wecom] inbound(bot): reqId=${reqId} route_failure reason=${reason} path=${url.split("?")[0]} candidates=[${matchResult.candidateAccountIds.join(",")}]`);
326
- sendText(res, 403, detail);
327
- return true;
328
- }
329
- const target = matchResult.target;
330
- target.runtime.log?.(`[webhook] POST 签名验证成功 (account=${target.account.accountId})`);
331
- // 更新状态:最后接收消息时间
332
- target.statusSink?.({ lastInboundAt: Date.now() });
333
- // 消息解密
334
- let message;
335
- try {
336
- const wc = new WecomCrypto(target.account.token, target.account.encodingAESKey, target.account.receiveId);
337
- const plaintext = wc.decrypt(encrypt);
338
- message = JSON.parse(plaintext);
339
- }
340
- catch (err) {
341
- target.runtime.log?.(`[webhook] 消息解密失败: ${err instanceof Error ? err.message : String(err)}`);
342
- // 解密失败返回 400 + 可读错误信息(与原版一致,方便排查 EncodingAESKey 配置错误)
343
- sendText(res, 400, "decrypt failed - 解密失败,请检查 EncodingAESKey");
344
- return true;
345
- }
346
- // aibotid 身份校验(安全兜底:即使签名匹配,也校验消息中的 aibotid)
347
- const expectedBotIds = resolveBotIdentitySet(target);
348
- if (expectedBotIds.size > 0) {
349
- const inboundAibotId = String(message.aibotid ?? "").trim();
350
- if (!inboundAibotId || !expectedBotIds.has(inboundAibotId)) {
351
- target.runtime.error?.(`[webhook] aibotid_mismatch: accountId=${target.account.accountId} expected=${Array.from(expectedBotIds).join(",")} actual=${inboundAibotId || "N/A"}`);
352
- }
353
- }
354
- target.runtime.log?.(`[webhook] 收到消息 (type=${message.msgtype}, msgid=${message.msgid ?? "N/A"}, account=${target.account.accountId})`);
355
- // 获取出口代理 URL(对齐原版 resolveWecomEgressProxyUrl)
356
- const proxyUrl = resolveWecomEgressProxyUrl(target.config);
357
- // 按消息类型分发
358
- try {
359
- const responseData = await dispatchMessage(target, message, timestamp, nonce, proxyUrl);
360
- if (responseData) {
361
- const encrypted = encryptResponse(target, responseData, timestamp, nonce);
362
- sendEncryptedReply(res, encrypted);
363
- }
364
- else {
365
- // 空响应也使用加密包装
366
- const encrypted = encryptResponse(target, {}, timestamp, nonce);
367
- sendEncryptedReply(res, encrypted);
368
- }
369
- }
370
- catch (err) {
371
- target.runtime.error?.(`[webhook] 消息处理异常: ${err instanceof Error ? err.message : String(err)}`);
372
- // 对齐原版:尽量返回 200 避免企微重试风暴,同时给一个可见的错误文本
373
- const errorResponse = {
374
- msgtype: "text",
375
- text: { content: "服务内部错误:Bot 处理异常,请稍后重试。" },
376
- };
377
- const encrypted = encryptResponse(target, errorResponse, timestamp, nonce);
378
- sendEncryptedReply(res, encrypted);
379
- }
380
- return true;
381
- }
382
- return false;
383
- }
384
- // ============================================================================
385
- // 消息分发
386
- // ============================================================================
387
- /**
388
- * 根据消息类型分发到对应的处理函数
389
- */
390
- async function dispatchMessage(target, message, timestamp, nonce, proxyUrl) {
391
- const msgtype = message.msgtype;
392
- // stream_refresh 轮询
393
- if (msgtype === "stream") {
394
- return handleStreamRefresh(target, message);
395
- }
396
- // 事件处理
397
- if (msgtype === "event") {
398
- const eventType = String(message.event?.eventtype ?? "").toLowerCase();
399
- if (eventType === "enter_chat") {
400
- return handleEnterChat(target, message);
401
- }
402
- if (eventType === "template_card_event") {
403
- return handleTemplateCardEvent(target, message, timestamp, nonce, proxyUrl);
404
- }
405
- target.runtime.log?.(`[webhook] 未处理的事件类型: ${eventType}`);
406
- return null;
407
- }
408
- // 普通消息(text / image / file / voice / video / mixed)
409
- if (["text", "image", "file", "voice", "video", "mixed"].includes(msgtype)) {
410
- // 过滤非真实用户消息(与原版 shouldProcessBotInboundMessage 对齐)
411
- const filterResult = shouldProcessBotInboundMessage(message);
412
- if (!filterResult.shouldProcess) {
413
- target.runtime.log?.(`[webhook] 消息过滤: msgtype=${msgtype} reason=${filterResult.reason} from=${resolveWecomSenderUserId(message) ?? "N/A"} chatType=${String(message.chattype ?? "N/A")}`);
414
- return null;
415
- }
416
- return handleInboundMessage(target, message, timestamp, nonce, proxyUrl, filterResult);
417
- }
418
- target.runtime.log?.(`[webhook] 未知消息类型: ${msgtype}`);
419
- return null;
420
- }
@@ -1,156 +0,0 @@
1
- /**
2
- * Webhook 辅助函数
3
- *
4
- * 从 @mocrane/wecom monitor.ts 迁移的辅助工具函数集合。
5
- * 包含:文本截断、兜底提示构建、本机路径提取、MIME 推断等。
6
- */
7
- import type { OpenClawConfig } from "openclaw/plugin-sdk";
8
- import type { StreamState, WecomWebhookTarget, WebhookInboundMessage, WebhookInboundQuote } from "./types.js";
9
- /** DM 文本最大字节数上限 */
10
- export declare const STREAM_MAX_DM_BYTES = 200000;
11
- /** MIME 扩展名映射表 */
12
- export declare const MIME_BY_EXT: Record<string, string>;
13
- /**
14
- * UTF-8 字节截断(保留尾部,截断头部)
15
- *
16
- * 对齐原版 truncateUtf8Bytes:保留最后 maxBytes 字节。
17
- */
18
- export declare function truncateUtf8Bytes(text: string, maxBytes: number): string;
19
- /**
20
- * 追加 DM 兜底内容(对齐原版 appendDmContent)
21
- *
22
- * 每次 deliver 时都追加到 dmContent(不受 STREAM_MAX_BYTES 限制,有 DM 上限保护)
23
- */
24
- export declare function appendDmContent(state: StreamState, text: string): void;
25
- /**
26
- * 构建兜底提示文本(对齐原版 buildFallbackPrompt)
27
- */
28
- export declare function buildFallbackPrompt(params: {
29
- kind: "media" | "timeout" | "error";
30
- agentConfigured: boolean;
31
- userId?: string;
32
- filename?: string;
33
- chatType?: "group" | "direct";
34
- }): string;
35
- /**
36
- * 从文本中提取本机文件路径(对齐原版 extractLocalFilePathsFromText)
37
- */
38
- export declare function extractLocalFilePathsFromText(text: string): string[];
39
- /**
40
- * 从文本中提取本机图片路径(对齐原版 extractLocalImagePathsFromText)
41
- *
42
- * 仅提取 text 中存在且也出现在 mustAlsoAppearIn 中的路径(安全:防止泄漏)
43
- */
44
- export declare function extractLocalImagePathsFromText(params: {
45
- text: string;
46
- mustAlsoAppearIn: string;
47
- }): string[];
48
- /**
49
- * 判断文本是否包含"发送本机文件"的意图(对齐原版 looksLikeSendLocalFileIntent)
50
- */
51
- export declare function looksLikeSendLocalFileIntent(rawBody: string): boolean;
52
- /**
53
- * 计算 taskKey(对齐原版 computeTaskKey)
54
- */
55
- export declare function computeTaskKey(target: WecomWebhookTarget, msg: WebhookInboundMessage): string | undefined;
56
- /**
57
- * 检查 Agent 凭证是否已配置(对齐原版 resolveAgentAccountOrUndefined 的简化版)
58
- *
59
- * 在 webhook 模式下,Agent 凭证直接来自 target.account,不需要复杂的解析
60
- */
61
- export declare function isAgentConfigured(target: WecomWebhookTarget): boolean;
62
- /**
63
- * 从路径猜测 content-type
64
- */
65
- export declare function guessContentTypeFromPath(filePath: string): string | undefined;
66
- /**
67
- * 从 StreamState 构建最终流式回复(对齐原版 buildStreamReplyFromState)
68
- *
69
- * 包含 images/msg_item,对 content 做 truncateUtf8Bytes。
70
- */
71
- export declare function buildStreamReplyFromState(state: StreamState, maxBytes: number): Record<string, unknown>;
72
- /**
73
- * 计算 MD5
74
- */
75
- export declare function computeMd5(data: Buffer | string): string;
76
- /**
77
- * 解析媒体最大字节数(对齐原版 resolveWecomMediaMaxBytes)
78
- */
79
- export declare function resolveWecomMediaMaxBytes(cfg: OpenClawConfig): number;
80
- /** 入站消息解析结果(对齐原版 InboundResult) */
81
- export type InboundResult = {
82
- body: string;
83
- media?: {
84
- buffer: Buffer;
85
- contentType: string;
86
- filename: string;
87
- };
88
- };
89
- /**
90
- * 处理接收消息(对齐原版 processInboundMessage)
91
- *
92
- * 解析企业微信传入的消息体:
93
- * 1. 识别媒体消息(Image/File/Video/Mixed)
94
- * 2. 如果存在媒体文件,调用 media.ts 进行解密和下载
95
- * 3. 通过 inferInboundMediaMeta 精确推断 MIME 和文件名
96
- * 4. 构造统一的 InboundResult 供后续 Agent 处理
97
- *
98
- * @param target Webhook 目标配置
99
- * @param msg 企业微信原始消息对象
100
- */
101
- export declare function processInboundMessage(target: WecomWebhookTarget, msg: WebhookInboundMessage): Promise<InboundResult>;
102
- /**
103
- * 构建 Agent 调度所需的 config(对齐原版 cfgForDispatch 逻辑)
104
- *
105
- * 关键修改:
106
- * - tools.deny += "message"(防止 Agent 绕过 Bot 交付)
107
- * - blockStreamingChunk / blockStreamingCoalesce 使用更小的阈值
108
- */
109
- export declare function buildCfgForDispatch(config: OpenClawConfig): OpenClawConfig;
110
- /**
111
- * 解析企微 Bot 回调中的发送者 userid(对齐原版 resolveWecomSenderUserId)
112
- *
113
- * 优先级:from.userid → fromuserid → from_userid → fromUserId
114
- */
115
- export declare function resolveWecomSenderUserId(msg: WebhookInboundMessage): string | undefined;
116
- /**
117
- * 构造入站消息文本内容(对齐原版 buildInboundBody)
118
- *
119
- * 根据消息类型提取文本表示:
120
- * - text → text.content
121
- * - voice → voice.content 或 "[voice]"
122
- * - image → "[image] {url}"
123
- * - file → "[file] {url}"
124
- * - video → "[video] {url}"
125
- * - mixed → 逐项提取拼接
126
- * - event → "[event] {eventtype}"
127
- * - stream → "[stream_refresh] {id}"
128
- *
129
- * 如果消息包含 quote(引用),追加引用内容。
130
- */
131
- export declare function buildInboundBody(msg: WebhookInboundMessage): string;
132
- /**
133
- * 格式化引用消息文本(对齐原版 formatQuote)
134
- */
135
- export declare function formatQuote(quote: WebhookInboundQuote): string;
136
- /** 检查消息是否有媒体内容 */
137
- export declare function hasMedia(message: WebhookInboundMessage): boolean;
138
- /**
139
- * 构造占位符响应(对齐原版 buildStreamPlaceholderReply)
140
- *
141
- * 用于 active_new / queued_new 场景:finish=false,显示占位符文本。
142
- * 原版规范:第一次回复内容为 "1" 作为最小占位符。
143
- */
144
- export declare function buildStreamPlaceholderReply(streamId: string, placeholderContent?: string): Record<string, unknown>;
145
- /**
146
- * 构造文本占位符响应(对齐原版 buildStreamTextPlaceholderReply)
147
- *
148
- * 用于 merged 场景:finish=false,显示自定义提示(如"已合并排队处理中...")。
149
- */
150
- export declare function buildStreamTextPlaceholderReply(streamId: string, content: string): Record<string, unknown>;
151
- /**
152
- * 构造流式响应(从 StreamState 构建)
153
- *
154
- * 用于 stream_refresh 和 msgid 去重场景:返回当前累积内容 + finish 标记。
155
- */
156
- export declare function buildStreamResponse(stream: StreamState): Record<string, unknown>;