@yanhaidao/wecom 2.4.120 → 2.5.110

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 (323) hide show
  1. package/README.md +4 -5
  2. package/dist/index.js +68 -0
  3. package/dist/src/accounts.js +20 -0
  4. package/dist/src/agent/handler.js +895 -0
  5. package/dist/src/agent/index.js +5 -0
  6. package/dist/src/app/account-runtime.js +216 -0
  7. package/dist/src/app/bootstrap.js +19 -0
  8. package/dist/src/app/index.js +118 -0
  9. package/dist/src/capability/agent/delivery-service.js +63 -0
  10. package/dist/src/capability/agent/fallback-policy.js +6 -0
  11. package/dist/src/capability/agent/ingress-service.js +33 -0
  12. package/dist/src/capability/agent/upstream-delivery-service.js +71 -0
  13. package/dist/src/capability/bot/dispatch-config.js +45 -0
  14. package/dist/src/capability/bot/fallback-delivery.js +147 -0
  15. package/dist/src/capability/bot/local-path-delivery.js +178 -0
  16. package/dist/src/capability/bot/sandbox-media.js +138 -0
  17. package/dist/src/capability/bot/service.js +49 -0
  18. package/dist/src/capability/bot/stream-delivery.js +321 -0
  19. package/dist/src/capability/bot/stream-finalizer.js +81 -0
  20. package/dist/src/capability/bot/stream-orchestrator.js +318 -0
  21. package/dist/src/capability/bot/types.js +1 -0
  22. package/{src/capability/calendar/client.ts → dist/src/capability/calendar/client.js} +118 -241
  23. package/{src/capability/calendar/schema.ts → dist/src/capability/calendar/schema.js} +0 -38
  24. package/dist/src/capability/calendar/tool.js +365 -0
  25. package/dist/src/capability/calendar/types.js +12 -0
  26. package/{src/capability/doc/client.ts → dist/src/capability/doc/client.js} +370 -605
  27. package/{src/capability/doc/schema.ts → dist/src/capability/doc/schema.js} +345 -394
  28. package/dist/src/capability/doc/tool.js +1556 -0
  29. package/dist/src/capability/doc/types.js +113 -0
  30. package/dist/src/capability/mcp/index.js +3 -0
  31. package/dist/src/capability/mcp/schema.js +102 -0
  32. package/dist/src/capability/mcp/tool.js +146 -0
  33. package/dist/src/capability/mcp/transport.js +293 -0
  34. package/dist/src/channel.js +224 -0
  35. package/dist/src/config/accounts.js +236 -0
  36. package/dist/src/config/derived-paths.js +31 -0
  37. package/dist/src/config/index.js +7 -0
  38. package/dist/src/config/media.js +110 -0
  39. package/dist/src/config/network.js +32 -0
  40. package/dist/src/config/routing.js +20 -0
  41. package/dist/src/config/runtime-config.js +25 -0
  42. package/dist/src/config/schema.js +4 -0
  43. package/{src/config-schema.ts → dist/src/config-schema.js} +1 -1
  44. package/dist/src/context-store.js +219 -0
  45. package/{src/crypto/aes.ts → dist/src/crypto/aes.js} +11 -28
  46. package/dist/src/crypto/index.js +9 -0
  47. package/{src/crypto/signature.ts → dist/src/crypto/signature.js} +3 -18
  48. package/{src/crypto/xml.ts → dist/src/crypto/xml.js} +3 -11
  49. package/dist/src/crypto.js +145 -0
  50. package/dist/src/domain/models.js +1 -0
  51. package/dist/src/domain/policies.js +32 -0
  52. package/{src/dynamic-agent.ts → dist/src/dynamic-agent.js} +36 -73
  53. package/dist/src/gateway-monitor.js +139 -0
  54. package/dist/src/http.js +114 -0
  55. package/{src/media.ts → dist/src/media.js} +21 -40
  56. package/dist/src/monitor/limits.js +7 -0
  57. package/dist/src/monitor/state.js +28 -0
  58. package/dist/src/monitor.js +84 -0
  59. package/dist/src/observability/audit-log.js +30 -0
  60. package/dist/src/observability/legacy-operational-event-store.js +22 -0
  61. package/dist/src/observability/raw-envelope-log.js +24 -0
  62. package/dist/src/observability/status-registry.js +9 -0
  63. package/dist/src/observability/transport-session-view.js +14 -0
  64. package/dist/src/onboarding.js +546 -0
  65. package/dist/src/outbound.js +557 -0
  66. package/dist/src/runtime/dispatcher.js +57 -0
  67. package/{src/runtime/index.ts → dist/src/runtime/index.js} +0 -1
  68. package/dist/src/runtime/outbound-intent.js +1 -0
  69. package/dist/src/runtime/reply-orchestrator.js +38 -0
  70. package/dist/src/runtime/routing-bridge.js +26 -0
  71. package/dist/src/runtime/session-manager.js +112 -0
  72. package/dist/src/runtime/source-registry.js +174 -0
  73. package/dist/src/runtime.js +1 -0
  74. package/dist/src/shared/command-auth.js +57 -0
  75. package/{src/shared/index.ts → dist/src/shared/index.js} +0 -1
  76. package/dist/src/shared/media-asset.js +65 -0
  77. package/dist/src/shared/media-service.js +59 -0
  78. package/dist/src/shared/media-types.js +1 -0
  79. package/{src/shared/xml-parser.ts → dist/src/shared/xml-parser.js} +72 -63
  80. package/dist/src/store/active-reply-store.js +41 -0
  81. package/dist/src/store/interfaces.js +1 -0
  82. package/dist/src/store/memory-store.js +33 -0
  83. package/dist/src/store/stream-batch-store.js +319 -0
  84. package/{src/target.ts → dist/src/target.js} +15 -48
  85. package/dist/src/transport/agent-api/client.js +168 -0
  86. package/dist/src/transport/agent-api/core.js +337 -0
  87. package/dist/src/transport/agent-api/delivery.js +28 -0
  88. package/dist/src/transport/agent-api/media-upload.js +4 -0
  89. package/dist/src/transport/agent-api/reply.js +24 -0
  90. package/dist/src/transport/agent-api/upstream-delivery.js +30 -0
  91. package/dist/src/transport/agent-api/upstream-media-upload.js +46 -0
  92. package/dist/src/transport/agent-api/upstream-reply.js +26 -0
  93. package/dist/src/transport/agent-callback/http-handler.js +30 -0
  94. package/dist/src/transport/agent-callback/inbound.js +4 -0
  95. package/dist/src/transport/agent-callback/reply.js +8 -0
  96. package/dist/src/transport/agent-callback/request-handler.js +189 -0
  97. package/dist/src/transport/agent-callback/session.js +15 -0
  98. package/dist/src/transport/bot-webhook/active-reply.js +27 -0
  99. package/dist/src/transport/bot-webhook/http-handler.js +31 -0
  100. package/dist/src/transport/bot-webhook/inbound-normalizer.js +496 -0
  101. package/dist/src/transport/bot-webhook/inbound.js +4 -0
  102. package/dist/src/transport/bot-webhook/message-shape.js +98 -0
  103. package/dist/src/transport/bot-webhook/protocol.js +124 -0
  104. package/dist/src/transport/bot-webhook/reply.js +9 -0
  105. package/dist/src/transport/bot-webhook/request-handler.js +285 -0
  106. package/dist/src/transport/bot-webhook/session.js +15 -0
  107. package/dist/src/transport/bot-ws/inbound.js +147 -0
  108. package/dist/src/transport/bot-ws/media.js +236 -0
  109. package/dist/src/transport/bot-ws/reply.js +310 -0
  110. package/dist/src/transport/bot-ws/sdk-adapter.js +257 -0
  111. package/dist/src/transport/bot-ws/session.js +15 -0
  112. package/dist/src/transport/http/common.js +78 -0
  113. package/dist/src/transport/http/registry.js +71 -0
  114. package/dist/src/transport/http/request-handler.js +51 -0
  115. package/{src/transport/index.ts → dist/src/transport/index.js} +2 -10
  116. package/dist/src/types/account.js +1 -0
  117. package/dist/src/types/config.js +1 -0
  118. package/dist/src/types/constants.js +28 -0
  119. package/dist/src/types/events.js +1 -0
  120. package/dist/src/types/index.js +1 -0
  121. package/dist/src/types/legacy-stream.js +1 -0
  122. package/dist/src/types/message.js +5 -0
  123. package/dist/src/types/runtime-context.js +1 -0
  124. package/dist/src/types/runtime.js +1 -0
  125. package/dist/src/types.js +1 -0
  126. package/dist/src/upstream/index.js +111 -0
  127. package/dist/src/wecom_msg_adapter/markdown_adapter.js +280 -0
  128. package/openclaw.plugin.json +15 -0
  129. package/package.json +18 -1
  130. package/.github/workflows/release.yml +0 -143
  131. package/GOVERNANCE.md +0 -26
  132. package/MENU_EVENT_CONF.md +0 -500
  133. package/MENU_EVENT_PLAN.md +0 -440
  134. package/SKILLS_CAL.md +0 -895
  135. package/SKILLS_DOC.md +0 -2288
  136. package/UPSTREAM_CONFIG.md +0 -170
  137. package/UPSTREAM_PLAN.md +0 -175
  138. package/assets/01.bot-add.png +0 -0
  139. package/assets/01.bot-setp2.png +0 -0
  140. package/assets/01.image.jpg +0 -0
  141. package/assets/02.agent.add.png +0 -0
  142. package/assets/02.agent.api-set.png +0 -0
  143. package/assets/02.image.jpg +0 -0
  144. package/assets/03.agent.page.png +0 -0
  145. package/assets/03.bot.page.png +0 -0
  146. package/assets/link-me.jpg +0 -0
  147. package/assets/register.png +0 -0
  148. package/changelog/v2.2.28.md +0 -70
  149. package/changelog/v2.3.10.md +0 -17
  150. package/changelog/v2.3.11.md +0 -19
  151. package/changelog/v2.3.12.md +0 -25
  152. package/changelog/v2.3.13.md +0 -19
  153. package/changelog/v2.3.14.md +0 -48
  154. package/changelog/v2.3.15.md +0 -15
  155. package/changelog/v2.3.16.md +0 -11
  156. package/changelog/v2.3.18.md +0 -22
  157. package/changelog/v2.3.19.md +0 -73
  158. package/changelog/v2.3.2.md +0 -28
  159. package/changelog/v2.3.26.md +0 -21
  160. package/changelog/v2.3.27.md +0 -33
  161. package/changelog/v2.3.4.md +0 -20
  162. package/changelog/v2.3.9.md +0 -22
  163. package/changelog/v2.4.12.md +0 -37
  164. package/compat-single-account.md +0 -148
  165. package/index.test.ts +0 -38
  166. package/scripts/test-proxy.ts +0 -70
  167. package/scripts/wecom/README.md +0 -123
  168. package/scripts/wecom/menu-click-help.js +0 -59
  169. package/scripts/wecom/menu-click-help.py +0 -55
  170. package/src/accounts.ts +0 -34
  171. package/src/agent/api-client.upload.test.ts +0 -109
  172. package/src/agent/event-router.test.ts +0 -421
  173. package/src/agent/event-router.ts +0 -272
  174. package/src/agent/handler.event-filter.test.ts +0 -135
  175. package/src/agent/handler.ts +0 -1250
  176. package/src/agent/index.ts +0 -12
  177. package/src/agent/script-runner.ts +0 -186
  178. package/src/agent/test-fixtures/invalid-json-script.mjs +0 -1
  179. package/src/agent/test-fixtures/reply-event-script.mjs +0 -29
  180. package/src/agent/test-fixtures/reply-event-script.py +0 -17
  181. package/src/app/account-runtime.ts +0 -276
  182. package/src/app/bootstrap.ts +0 -29
  183. package/src/app/index.ts +0 -192
  184. package/src/capability/agent/delivery-service.ts +0 -87
  185. package/src/capability/agent/fallback-policy.ts +0 -13
  186. package/src/capability/agent/ingress-service.ts +0 -38
  187. package/src/capability/agent/upstream-delivery-service.ts +0 -96
  188. package/src/capability/bot/dispatch-config.ts +0 -47
  189. package/src/capability/bot/fallback-delivery.ts +0 -178
  190. package/src/capability/bot/local-path-delivery.ts +0 -215
  191. package/src/capability/bot/sandbox-media.test.ts +0 -221
  192. package/src/capability/bot/sandbox-media.ts +0 -176
  193. package/src/capability/bot/service.ts +0 -56
  194. package/src/capability/bot/stream-delivery.ts +0 -379
  195. package/src/capability/bot/stream-finalizer.ts +0 -120
  196. package/src/capability/bot/stream-orchestrator.ts +0 -371
  197. package/src/capability/bot/types.ts +0 -8
  198. package/src/capability/calendar/SKILLS_CHECKLIST.md +0 -251
  199. package/src/capability/calendar/tool.ts +0 -417
  200. package/src/capability/calendar/types.ts +0 -309
  201. package/src/capability/doc/tool.ts +0 -1629
  202. package/src/capability/doc/types.ts +0 -792
  203. package/src/capability/mcp/index.ts +0 -10
  204. package/src/capability/mcp/schema.ts +0 -107
  205. package/src/capability/mcp/tool.ts +0 -174
  206. package/src/capability/mcp/transport.ts +0 -394
  207. package/src/channel.config.test.ts +0 -180
  208. package/src/channel.lifecycle.test.ts +0 -255
  209. package/src/channel.meta.test.ts +0 -26
  210. package/src/channel.ts +0 -256
  211. package/src/config/accounts.resolve.test.ts +0 -75
  212. package/src/config/accounts.ts +0 -312
  213. package/src/config/derived-paths.test.ts +0 -111
  214. package/src/config/derived-paths.ts +0 -41
  215. package/src/config/index.ts +0 -22
  216. package/src/config/media.test.ts +0 -113
  217. package/src/config/media.ts +0 -139
  218. package/src/config/network.ts +0 -20
  219. package/src/config/routing.test.ts +0 -88
  220. package/src/config/routing.ts +0 -26
  221. package/src/config/runtime-config.ts +0 -46
  222. package/src/config/schema.ts +0 -144
  223. package/src/context-store.ts +0 -297
  224. package/src/crypto/index.ts +0 -24
  225. package/src/crypto.test.ts +0 -32
  226. package/src/crypto.ts +0 -176
  227. package/src/domain/models.ts +0 -7
  228. package/src/domain/policies.ts +0 -36
  229. package/src/dynamic-agent.account-scope.test.ts +0 -17
  230. package/src/gateway-monitor.ts +0 -181
  231. package/src/http.ts +0 -137
  232. package/src/media.test.ts +0 -82
  233. package/src/monitor/limits.ts +0 -7
  234. package/src/monitor/state.queue.test.ts +0 -185
  235. package/src/monitor/state.ts +0 -34
  236. package/src/monitor.active.test.ts +0 -245
  237. package/src/monitor.inbound-filter.test.ts +0 -63
  238. package/src/monitor.integration.test.ts +0 -208
  239. package/src/monitor.ts +0 -121
  240. package/src/monitor.webhook.test.ts +0 -774
  241. package/src/observability/audit-log.ts +0 -48
  242. package/src/observability/legacy-operational-event-store.ts +0 -36
  243. package/src/observability/raw-envelope-log.ts +0 -28
  244. package/src/observability/status-registry.ts +0 -13
  245. package/src/observability/transport-session-view.ts +0 -14
  246. package/src/onboarding.test.ts +0 -336
  247. package/src/onboarding.ts +0 -704
  248. package/src/outbound.test.ts +0 -1271
  249. package/src/outbound.ts +0 -746
  250. package/src/runtime/dispatcher.ts +0 -71
  251. package/src/runtime/outbound-intent.ts +0 -4
  252. package/src/runtime/reply-orchestrator.test.ts +0 -71
  253. package/src/runtime/reply-orchestrator.ts +0 -67
  254. package/src/runtime/routing-bridge.test.ts +0 -115
  255. package/src/runtime/routing-bridge.ts +0 -44
  256. package/src/runtime/session-manager.test.ts +0 -174
  257. package/src/runtime/session-manager.ts +0 -139
  258. package/src/runtime/source-registry.ts +0 -249
  259. package/src/runtime.ts +0 -14
  260. package/src/shared/command-auth.ts +0 -87
  261. package/src/shared/media-asset.ts +0 -78
  262. package/src/shared/media-service.test.ts +0 -111
  263. package/src/shared/media-service.ts +0 -84
  264. package/src/shared/media-types.ts +0 -5
  265. package/src/shared/xml-parser.test.ts +0 -50
  266. package/src/store/active-reply-store.ts +0 -42
  267. package/src/store/interfaces.ts +0 -11
  268. package/src/store/memory-store.ts +0 -43
  269. package/src/store/stream-batch-store.ts +0 -350
  270. package/src/transport/agent-api/client.ts +0 -277
  271. package/src/transport/agent-api/core.ts +0 -463
  272. package/src/transport/agent-api/delivery.ts +0 -41
  273. package/src/transport/agent-api/media-upload.ts +0 -11
  274. package/src/transport/agent-api/reply.ts +0 -39
  275. package/src/transport/agent-api/upstream-delivery.ts +0 -45
  276. package/src/transport/agent-api/upstream-media-upload.ts +0 -70
  277. package/src/transport/agent-api/upstream-reply.ts +0 -43
  278. package/src/transport/agent-callback/http-handler.ts +0 -47
  279. package/src/transport/agent-callback/inbound.ts +0 -5
  280. package/src/transport/agent-callback/reply.ts +0 -13
  281. package/src/transport/agent-callback/request-handler.ts +0 -244
  282. package/src/transport/agent-callback/session.ts +0 -23
  283. package/src/transport/bot-webhook/active-reply.ts +0 -39
  284. package/src/transport/bot-webhook/http-handler.ts +0 -48
  285. package/src/transport/bot-webhook/inbound-normalizer.ts +0 -371
  286. package/src/transport/bot-webhook/inbound.ts +0 -5
  287. package/src/transport/bot-webhook/message-shape.ts +0 -89
  288. package/src/transport/bot-webhook/protocol.ts +0 -148
  289. package/src/transport/bot-webhook/reply.ts +0 -15
  290. package/src/transport/bot-webhook/request-handler.ts +0 -394
  291. package/src/transport/bot-webhook/session.ts +0 -23
  292. package/src/transport/bot-ws/inbound.test.ts +0 -96
  293. package/src/transport/bot-ws/inbound.ts +0 -116
  294. package/src/transport/bot-ws/media.test.ts +0 -44
  295. package/src/transport/bot-ws/media.ts +0 -321
  296. package/src/transport/bot-ws/reply.test.ts +0 -450
  297. package/src/transport/bot-ws/reply.ts +0 -365
  298. package/src/transport/bot-ws/sdk-adapter.test.ts +0 -187
  299. package/src/transport/bot-ws/sdk-adapter.ts +0 -314
  300. package/src/transport/bot-ws/session.ts +0 -28
  301. package/src/transport/http/common.ts +0 -109
  302. package/src/transport/http/registry.ts +0 -92
  303. package/src/transport/http/request-handler.ts +0 -84
  304. package/src/types/account.ts +0 -72
  305. package/src/types/config.ts +0 -166
  306. package/src/types/constants.ts +0 -31
  307. package/src/types/events.ts +0 -21
  308. package/src/types/global.d.ts +0 -9
  309. package/src/types/index.ts +0 -17
  310. package/src/types/legacy-stream.ts +0 -50
  311. package/src/types/message.ts +0 -187
  312. package/src/types/runtime-context.ts +0 -28
  313. package/src/types/runtime.ts +0 -165
  314. package/src/types.ts +0 -41
  315. package/src/upstream/index.ts +0 -150
  316. package/src/upstream.test.ts +0 -84
  317. package/src/wecom_msg_adapter/markdown_adapter.ts +0 -331
  318. package/tsconfig.json +0 -22
  319. package/vitest.config.ts +0 -26
  320. /package/{src/capability/agent/index.ts → dist/src/capability/agent/index.js} +0 -0
  321. /package/{src/capability/bot/index.ts → dist/src/capability/bot/index.js} +0 -0
  322. /package/{src/capability/calendar/index.ts → dist/src/capability/calendar/index.js} +0 -0
  323. /package/{src/capability/index.ts → dist/src/capability/index.js} +0 -0
@@ -0,0 +1,124 @@
1
+ import { LIMITS } from "../../monitor/state.js";
2
+ import { computeWecomMsgSignature, encryptWecomPlaintext } from "../../crypto.js";
3
+ function truncateUtf8Bytes(text, maxBytes) {
4
+ const buf = Buffer.from(text, "utf8");
5
+ if (buf.length <= maxBytes)
6
+ return text;
7
+ const slice = buf.subarray(buf.length - maxBytes);
8
+ return slice.toString("utf8");
9
+ }
10
+ export function jsonOk(res, body) {
11
+ res.statusCode = 200;
12
+ res.setHeader("Content-Type", "text/plain; charset=utf-8");
13
+ res.end(JSON.stringify(body));
14
+ }
15
+ export async function readBotWebhookJsonBody(req, maxBytes) {
16
+ const chunks = [];
17
+ let total = 0;
18
+ return await new Promise((resolve) => {
19
+ req.on("data", (chunk) => {
20
+ total += chunk.length;
21
+ if (total > maxBytes) {
22
+ resolve({ ok: false, error: "payload too large" });
23
+ req.destroy();
24
+ return;
25
+ }
26
+ chunks.push(chunk);
27
+ });
28
+ req.on("end", () => {
29
+ try {
30
+ const raw = Buffer.concat(chunks).toString("utf8");
31
+ if (!raw.trim()) {
32
+ resolve({ ok: false, error: "empty payload" });
33
+ return;
34
+ }
35
+ resolve({ ok: true, value: JSON.parse(raw) });
36
+ }
37
+ catch (err) {
38
+ resolve({ ok: false, error: err instanceof Error ? err.message : String(err) });
39
+ }
40
+ });
41
+ req.on("error", (err) => {
42
+ resolve({ ok: false, error: err instanceof Error ? err.message : String(err) });
43
+ });
44
+ });
45
+ }
46
+ export function buildEncryptedBotWebhookReply(params) {
47
+ const plaintext = JSON.stringify(params.plaintextJson ?? {});
48
+ const encrypt = encryptWecomPlaintext({
49
+ encodingAESKey: params.account.encodingAESKey ?? "",
50
+ receiveId: params.account.receiveId ?? "",
51
+ plaintext,
52
+ });
53
+ const msgsignature = computeWecomMsgSignature({
54
+ token: params.account.token ?? "",
55
+ timestamp: params.timestamp,
56
+ nonce: params.nonce,
57
+ encrypt,
58
+ });
59
+ return {
60
+ encrypt,
61
+ msgsignature,
62
+ timestamp: params.timestamp,
63
+ nonce: params.nonce,
64
+ };
65
+ }
66
+ export function resolveBotIdentitySet(target) {
67
+ const ids = new Set();
68
+ const single = target.account.config.aibotid?.trim();
69
+ if (single)
70
+ ids.add(single);
71
+ for (const botId of target.account.config.botIds ?? []) {
72
+ const normalized = String(botId ?? "").trim();
73
+ if (normalized)
74
+ ids.add(normalized);
75
+ }
76
+ return ids;
77
+ }
78
+ export function buildStreamPlaceholderReply(params) {
79
+ const content = params.placeholderContent?.trim() || "1";
80
+ return {
81
+ msgtype: "stream",
82
+ stream: {
83
+ id: params.streamId,
84
+ finish: false,
85
+ content,
86
+ },
87
+ };
88
+ }
89
+ export function buildStreamTextPlaceholderReply(params) {
90
+ return {
91
+ msgtype: "stream",
92
+ stream: {
93
+ id: params.streamId,
94
+ finish: false,
95
+ content: params.content.trim() || "1",
96
+ },
97
+ };
98
+ }
99
+ export function buildStreamReplyFromState(state) {
100
+ const content = truncateUtf8Bytes(state.content, LIMITS.STREAM_MAX_BYTES);
101
+ return {
102
+ msgtype: "stream",
103
+ stream: {
104
+ id: state.streamId,
105
+ finish: state.finished,
106
+ content,
107
+ ...(state.finished && state.images?.length
108
+ ? {
109
+ msg_item: state.images.map((img) => ({
110
+ msgtype: "image",
111
+ image: { base64: img.base64, md5: img.md5 },
112
+ })),
113
+ }
114
+ : {}),
115
+ },
116
+ };
117
+ }
118
+ export function parseWecomPlainMessage(raw) {
119
+ const parsed = JSON.parse(raw);
120
+ if (!parsed || typeof parsed !== "object") {
121
+ return {};
122
+ }
123
+ return parsed;
124
+ }
@@ -0,0 +1,9 @@
1
+ export function createBotWebhookReplyContext(params) {
2
+ return {
3
+ transport: "bot-webhook",
4
+ accountId: params.accountId,
5
+ responseUrl: params.responseUrl,
6
+ passiveWindowMs: 5_000,
7
+ raw: params.raw,
8
+ };
9
+ }
@@ -0,0 +1,285 @@
1
+ import { getWecomRuntime } from "../../runtime.js";
2
+ import { decryptWecomEncrypted, verifyWecomSignature } from "../../crypto.js";
3
+ import { resolveWecomEgressProxyUrl } from "../../config/index.js";
4
+ import { logRouteFailure, resolveQueryParams, resolveSignatureParam, writeRouteFailure, } from "../http/common.js";
5
+ import { buildEncryptedBotWebhookReply, buildStreamPlaceholderReply, buildStreamReplyFromState, buildStreamTextPlaceholderReply, jsonOk, parseWecomPlainMessage, readBotWebhookJsonBody, resolveBotIdentitySet, } from "./protocol.js";
6
+ import { storeActiveReply } from "./active-reply.js";
7
+ import { buildInboundBody, resolveWecomSenderUserId, shouldProcessBotInboundMessage } from "./message-shape.js";
8
+ const ERROR_HELP = "\n\n遇到问题?联系作者: YanHaidao (微信: YanHaidao)";
9
+ export function createBotWebhookRequestHandler(params) {
10
+ const { streamStore, logInfo, logVerbose, recordBotOperationalEvent, startAgentForStream } = params;
11
+ return async function handleBotWebhookRequest(args) {
12
+ const { req, res, path, reqId, targets } = args;
13
+ const query = resolveQueryParams(req);
14
+ const timestamp = query.get("timestamp") ?? "";
15
+ const nonce = query.get("nonce") ?? "";
16
+ const signature = resolveSignatureParam(query);
17
+ if (req.method === "GET") {
18
+ const echostr = query.get("echostr") ?? "";
19
+ const signatureMatches = targets.filter((target) => target.account.token &&
20
+ verifyWecomSignature({ token: target.account.token, timestamp, nonce, encrypt: echostr, signature }));
21
+ if (signatureMatches.length !== 1) {
22
+ const reason = signatureMatches.length === 0 ? "wecom_account_not_found" : "wecom_account_conflict";
23
+ const candidateIds = (signatureMatches.length > 0 ? signatureMatches : targets).map((target) => target.account.accountId);
24
+ logRouteFailure({
25
+ reqId,
26
+ path,
27
+ method: "GET",
28
+ reason,
29
+ candidateAccountIds: candidateIds,
30
+ });
31
+ writeRouteFailure(res, reason, reason === "wecom_account_conflict"
32
+ ? "Bot callback account conflict: multiple accounts matched signature."
33
+ : "Bot callback account not found: signature verification failed.");
34
+ return true;
35
+ }
36
+ const target = signatureMatches[0];
37
+ try {
38
+ const plain = decryptWecomEncrypted({
39
+ encodingAESKey: target.account.encodingAESKey,
40
+ receiveId: target.account.receiveId,
41
+ encrypt: echostr,
42
+ });
43
+ res.statusCode = 200;
44
+ res.setHeader("Content-Type", "text/plain; charset=utf-8");
45
+ res.end(plain);
46
+ return true;
47
+ }
48
+ catch (err) {
49
+ res.statusCode = 400;
50
+ res.setHeader("Content-Type", "text/plain; charset=utf-8");
51
+ res.end(`decrypt failed - 解密失败,请检查 EncodingAESKey${ERROR_HELP}`);
52
+ return true;
53
+ }
54
+ }
55
+ if (req.method !== "POST")
56
+ return false;
57
+ const body = await readBotWebhookJsonBody(req, 1024 * 1024);
58
+ if (!body.ok) {
59
+ res.statusCode = 400;
60
+ res.end(body.error || "invalid payload");
61
+ return true;
62
+ }
63
+ const record = body.value;
64
+ const encrypt = String(record?.encrypt ?? record?.Encrypt ?? "");
65
+ console.log(`[wecom] inbound(bot): reqId=${reqId} rawJsonBytes=${Buffer.byteLength(JSON.stringify(record), "utf8")} hasEncrypt=${Boolean(encrypt)} encryptLen=${encrypt.length}`);
66
+ const signatureMatches = targets.filter((target) => target.account.token && verifyWecomSignature({ token: target.account.token, timestamp, nonce, encrypt, signature }));
67
+ if (signatureMatches.length !== 1) {
68
+ const reason = signatureMatches.length === 0 ? "wecom_account_not_found" : "wecom_account_conflict";
69
+ const candidateIds = (signatureMatches.length > 0 ? signatureMatches : targets).map((target) => target.account.accountId);
70
+ logRouteFailure({
71
+ reqId,
72
+ path,
73
+ method: "POST",
74
+ reason,
75
+ candidateAccountIds: candidateIds,
76
+ });
77
+ writeRouteFailure(res, reason, reason === "wecom_account_conflict"
78
+ ? "Bot callback account conflict: multiple accounts matched signature."
79
+ : "Bot callback account not found: signature verification failed.");
80
+ return true;
81
+ }
82
+ const target = signatureMatches[0];
83
+ let msg;
84
+ try {
85
+ const plain = decryptWecomEncrypted({
86
+ encodingAESKey: target.account.encodingAESKey,
87
+ receiveId: target.account.receiveId,
88
+ encrypt,
89
+ });
90
+ msg = parseWecomPlainMessage(plain);
91
+ }
92
+ catch {
93
+ res.statusCode = 400;
94
+ res.setHeader("Content-Type", "text/plain; charset=utf-8");
95
+ res.end(`decrypt failed - 解密失败,请检查 EncodingAESKey${ERROR_HELP}`);
96
+ return true;
97
+ }
98
+ const expected = resolveBotIdentitySet(target);
99
+ if (expected.size > 0) {
100
+ const inboundAibotId = String(msg.aibotid ?? "").trim();
101
+ if (!inboundAibotId || !expected.has(inboundAibotId)) {
102
+ target.runtime.error?.(`[wecom] inbound(bot): reqId=${reqId} accountId=${target.account.accountId} aibotid_mismatch expected=${Array.from(expected).join(",")} actual=${inboundAibotId || "N/A"}`);
103
+ }
104
+ }
105
+ logInfo(target, `inbound(bot): reqId=${reqId} selectedAccount=${target.account.accountId} path=${path}`);
106
+ target.touchTransportSession?.({ lastInboundAt: Date.now(), running: true });
107
+ const msgtype = String(msg.msgtype ?? "").toLowerCase();
108
+ const proxyUrl = resolveWecomEgressProxyUrl(target.config);
109
+ if (msgtype === "event") {
110
+ const eventtype = String(msg.event?.eventtype ?? "").toLowerCase();
111
+ if (eventtype === "template_card_event") {
112
+ const msgid = msg.msgid ? String(msg.msgid) : undefined;
113
+ if (msgid && streamStore.getStreamByMsgId(msgid)) {
114
+ logVerbose(target, `template_card_event: already processed msgid=${msgid}, skipping`);
115
+ recordBotOperationalEvent(target, {
116
+ category: "duplicate-reply",
117
+ messageId: msgid,
118
+ summary: `duplicate template card event msgid=${msgid}`,
119
+ raw: {
120
+ transport: "bot-webhook",
121
+ envelopeType: "json",
122
+ body: msg,
123
+ },
124
+ });
125
+ jsonOk(res, buildEncryptedBotWebhookReply({ account: target.account, plaintextJson: {}, nonce, timestamp }));
126
+ return true;
127
+ }
128
+ const cardEvent = msg.event?.template_card_event;
129
+ let interactionDesc = `[卡片交互] 按钮: ${cardEvent?.event_key || "unknown"}`;
130
+ if (cardEvent?.selected_items?.selected_item?.length) {
131
+ const selects = cardEvent.selected_items.selected_item.map((i) => `${i.question_key}=${i.option_ids?.option_id?.join(",")}`);
132
+ interactionDesc += ` 选择: ${selects.join("; ")}`;
133
+ }
134
+ if (cardEvent?.task_id)
135
+ interactionDesc += ` (任务ID: ${cardEvent.task_id})`;
136
+ jsonOk(res, buildEncryptedBotWebhookReply({ account: target.account, plaintextJson: {}, nonce, timestamp }));
137
+ const streamId = streamStore.createStream({ msgid });
138
+ streamStore.markStarted(streamId);
139
+ storeActiveReply(streamId, msg.response_url);
140
+ const core = getWecomRuntime();
141
+ startAgentForStream({
142
+ target: { ...target, core },
143
+ accountId: target.account.accountId,
144
+ msg: { ...msg, msgtype: "text", text: { content: interactionDesc } },
145
+ streamId,
146
+ }).catch((err) => target.runtime.error?.(`interaction failed: ${String(err)}`));
147
+ return true;
148
+ }
149
+ if (eventtype === "enter_chat") {
150
+ const welcome = target.account.config.welcomeText?.trim();
151
+ jsonOk(res, buildEncryptedBotWebhookReply({
152
+ account: target.account,
153
+ plaintextJson: welcome ? { msgtype: "text", text: { content: welcome } } : {},
154
+ nonce,
155
+ timestamp,
156
+ }));
157
+ return true;
158
+ }
159
+ jsonOk(res, buildEncryptedBotWebhookReply({ account: target.account, plaintextJson: {}, nonce, timestamp }));
160
+ return true;
161
+ }
162
+ if (msgtype === "stream") {
163
+ const streamId = String(msg.stream?.id ?? "").trim();
164
+ const state = streamStore.getStream(streamId);
165
+ const reply = state
166
+ ? buildStreamReplyFromState(state)
167
+ : buildStreamReplyFromState({
168
+ streamId: streamId || "unknown",
169
+ createdAt: Date.now(),
170
+ updatedAt: Date.now(),
171
+ started: true,
172
+ finished: true,
173
+ content: "",
174
+ });
175
+ jsonOk(res, buildEncryptedBotWebhookReply({ account: target.account, plaintextJson: reply, nonce, timestamp }));
176
+ return true;
177
+ }
178
+ try {
179
+ const decision = shouldProcessBotInboundMessage(msg);
180
+ if (!decision.shouldProcess) {
181
+ logInfo(target, `inbound: skipped msgtype=${msgtype} reason=${decision.reason} chattype=${String(msg.chattype ?? "")} chatid=${String(msg.chatid ?? "")} from=${resolveWecomSenderUserId(msg) || "N/A"}`);
182
+ jsonOk(res, buildEncryptedBotWebhookReply({ account: target.account, plaintextJson: {}, nonce, timestamp }));
183
+ return true;
184
+ }
185
+ const userid = decision.senderUserId;
186
+ const chatId = decision.chatId ?? userid;
187
+ const conversationKey = `wecom:${target.account.accountId}:${userid}:${chatId}`;
188
+ const msgContent = buildInboundBody(msg);
189
+ logInfo(target, `inbound: msgtype=${msgtype} chattype=${String(msg.chattype ?? "")} chatid=${String(msg.chatid ?? "")} from=${userid} msgid=${String(msg.msgid ?? "")} hasResponseUrl=${Boolean(msg.response_url)}`);
190
+ if (msg.msgid) {
191
+ const existingStreamId = streamStore.getStreamByMsgId(String(msg.msgid));
192
+ if (existingStreamId) {
193
+ logInfo(target, `message: 重复的 msgid=${msg.msgid},跳过处理并返回占位符 streamId=${existingStreamId}`);
194
+ recordBotOperationalEvent(target, {
195
+ category: "duplicate-reply",
196
+ messageId: String(msg.msgid),
197
+ summary: `duplicate inbound msgid=${String(msg.msgid)} streamId=${existingStreamId}`,
198
+ raw: {
199
+ transport: "bot-webhook",
200
+ envelopeType: "json",
201
+ body: msg,
202
+ },
203
+ });
204
+ jsonOk(res, buildEncryptedBotWebhookReply({
205
+ account: target.account,
206
+ plaintextJson: buildStreamPlaceholderReply({
207
+ streamId: existingStreamId,
208
+ placeholderContent: target.account.config.streamPlaceholderContent,
209
+ }),
210
+ nonce,
211
+ timestamp,
212
+ }));
213
+ return true;
214
+ }
215
+ }
216
+ const { streamId, status } = streamStore.addPendingMessage({
217
+ conversationKey,
218
+ target,
219
+ msg,
220
+ msgContent,
221
+ nonce,
222
+ timestamp,
223
+ debounceMs: target.account.config.debounceMs,
224
+ });
225
+ if (msg.response_url) {
226
+ storeActiveReply(streamId, msg.response_url, proxyUrl);
227
+ }
228
+ const defaultPlaceholder = target.account.config.streamPlaceholderContent;
229
+ const queuedPlaceholder = "已收到,已排队处理中...";
230
+ const mergedQueuedPlaceholder = "已收到,已合并排队处理中...";
231
+ if (status === "active_new") {
232
+ jsonOk(res, buildEncryptedBotWebhookReply({
233
+ account: target.account,
234
+ plaintextJson: buildStreamPlaceholderReply({
235
+ streamId,
236
+ placeholderContent: defaultPlaceholder,
237
+ }),
238
+ nonce,
239
+ timestamp,
240
+ }));
241
+ return true;
242
+ }
243
+ if (status === "queued_new") {
244
+ logInfo(target, `queue: 已进入下一批次 streamId=${streamId} msgid=${String(msg.msgid ?? "")}`);
245
+ jsonOk(res, buildEncryptedBotWebhookReply({
246
+ account: target.account,
247
+ plaintextJson: buildStreamPlaceholderReply({
248
+ streamId,
249
+ placeholderContent: queuedPlaceholder,
250
+ }),
251
+ nonce,
252
+ timestamp,
253
+ }));
254
+ return true;
255
+ }
256
+ const ackStreamId = streamStore.createStream({ msgid: String(msg.msgid ?? "") || undefined });
257
+ streamStore.updateStream(ackStreamId, (s) => {
258
+ s.finished = false;
259
+ s.started = true;
260
+ s.content = mergedQueuedPlaceholder;
261
+ });
262
+ if (msg.msgid)
263
+ streamStore.setStreamIdForMsgId(String(msg.msgid), ackStreamId);
264
+ streamStore.addAckStreamForBatch({ batchStreamId: streamId, ackStreamId });
265
+ logInfo(target, `queue: 已合并排队(回执流) ackStreamId=${ackStreamId} mergedIntoStreamId=${streamId} msgid=${String(msg.msgid ?? "")}`);
266
+ jsonOk(res, buildEncryptedBotWebhookReply({
267
+ account: target.account,
268
+ plaintextJson: buildStreamTextPlaceholderReply({ streamId: ackStreamId, content: mergedQueuedPlaceholder }),
269
+ nonce,
270
+ timestamp,
271
+ }));
272
+ return true;
273
+ }
274
+ catch (err) {
275
+ target.runtime.error?.(`[wecom] Bot message handler crashed: ${String(err)}`);
276
+ jsonOk(res, buildEncryptedBotWebhookReply({
277
+ account: target.account,
278
+ plaintextJson: { msgtype: "text", text: { content: "服务内部错误:Bot 处理异常,请稍后重试。" } },
279
+ nonce,
280
+ timestamp,
281
+ }));
282
+ return true;
283
+ }
284
+ };
285
+ }
@@ -0,0 +1,15 @@
1
+ export function createBotWebhookSessionSnapshot(params) {
2
+ return {
3
+ accountId: params.accountId,
4
+ transport: "bot-webhook",
5
+ running: params.running,
6
+ ownerId: `${params.accountId}:bot-webhook`,
7
+ connected: params.running,
8
+ authenticated: true,
9
+ lastConnectedAt: params.running ? Date.now() : undefined,
10
+ lastDisconnectedAt: params.running ? undefined : Date.now(),
11
+ lastInboundAt: params.lastInboundAt,
12
+ lastOutboundAt: params.lastOutboundAt,
13
+ lastError: params.lastError,
14
+ };
15
+ }
@@ -0,0 +1,147 @@
1
+ import { buildInboundBody } from "../bot-webhook/message-shape.js";
2
+ function resolveInboundKind(message) {
3
+ if (message.msgtype === "event") {
4
+ const eventType = String(message.event?.eventtype ?? "").trim();
5
+ if (eventType === "enter_chat")
6
+ return "welcome";
7
+ if (eventType === "template_card_event")
8
+ return "template-card-event";
9
+ return "event";
10
+ }
11
+ switch (message.msgtype) {
12
+ case "image":
13
+ return "image";
14
+ case "file":
15
+ return "file";
16
+ case "voice":
17
+ return "voice";
18
+ case "video":
19
+ return "video";
20
+ case "mixed":
21
+ return "mixed";
22
+ default:
23
+ return "text";
24
+ }
25
+ }
26
+ function pushAttachment(list, name, remoteUrl, aesKey) {
27
+ if (!remoteUrl) {
28
+ return;
29
+ }
30
+ list.push({ name, remoteUrl, aesKey });
31
+ }
32
+ function resolveEventText(message, account) {
33
+ if (message.msgtype !== "event") {
34
+ return buildInboundBody(message);
35
+ }
36
+ const event = message;
37
+ if (event.event?.eventtype === "enter_chat" && account.config.welcomeText) {
38
+ return account.config.welcomeText;
39
+ }
40
+ return `[event:${String(event.event?.eventtype ?? "unknown")}]`;
41
+ }
42
+ export function mapBotWsFrameToInboundEvent(params) {
43
+ const { account, frame } = params;
44
+ const body = frame.body;
45
+ if (!body) {
46
+ throw new Error("Bot WS frame body is required");
47
+ }
48
+ const peerKind = body.chattype === "group" ? "group" : "direct";
49
+ const senderId = body.from?.userid ?? "unknown";
50
+ const peerId = peerKind === "group" ? body.chatid ?? senderId : senderId;
51
+ const inboundKind = resolveInboundKind(body);
52
+ let attachments;
53
+ const collected = [];
54
+ if (body.msgtype === "image") {
55
+ pushAttachment(collected, "image", body.image?.url, body.image?.aeskey);
56
+ }
57
+ else if (body.msgtype === "file") {
58
+ pushAttachment(collected, "file", body.file?.url, body.file?.aeskey);
59
+ }
60
+ else if (body.msgtype === "video") {
61
+ pushAttachment(collected, "video", body.video?.url, body.video?.aeskey);
62
+ }
63
+ else if (body.msgtype === "mixed") {
64
+ const items = body.mixed?.msg_item;
65
+ if (Array.isArray(items)) {
66
+ for (const item of items) {
67
+ const itemType = String(item.msgtype ?? "").toLowerCase();
68
+ if (itemType === "image") {
69
+ pushAttachment(collected, "image", item.image?.url, item.image?.aeskey);
70
+ }
71
+ else if (itemType === "file") {
72
+ pushAttachment(collected, "file", item.file?.url, item.file?.aeskey);
73
+ }
74
+ else if (itemType === "video") {
75
+ pushAttachment(collected, "video", item.video?.url, item.video?.aeskey);
76
+ }
77
+ }
78
+ }
79
+ }
80
+ // 新增支持:如果没有顶层媒体,尝试从引用中提取媒体附件
81
+ // 优先级:quote.image/file/video 优先,其次 quote.mixed 中第一个图片
82
+ if (collected.length === 0) {
83
+ const quote = body.quote;
84
+ if (quote) {
85
+ const quoteType = String(quote.msgtype ?? "").toLowerCase();
86
+ // 处理单个媒体类型的引用
87
+ if (quoteType === "image") {
88
+ pushAttachment(collected, "image", quote.image?.url, quote.image?.aeskey);
89
+ }
90
+ else if (quoteType === "file") {
91
+ pushAttachment(collected, "file", quote.file?.url, quote.file?.aeskey);
92
+ }
93
+ else if (quoteType === "video") {
94
+ pushAttachment(collected, "video", quote.video?.url, quote.video?.aeskey);
95
+ }
96
+ // 处理图文混合类型:只提取第一个图片以保持与 webhook 一致
97
+ else if (quoteType === "mixed" && Array.isArray(quote.mixed?.msg_item)) {
98
+ for (const item of quote.mixed.msg_item) {
99
+ const itemType = String(item.msgtype ?? "").toLowerCase();
100
+ if (itemType === "image") {
101
+ pushAttachment(collected, "image", item.image?.url, item.image?.aeskey);
102
+ break;
103
+ }
104
+ }
105
+ }
106
+ }
107
+ }
108
+ if (collected.length > 0) {
109
+ attachments = collected;
110
+ }
111
+ return {
112
+ accountId: account.accountId,
113
+ capability: "bot",
114
+ transport: "bot-ws",
115
+ inboundKind,
116
+ messageId: body.msgid,
117
+ conversation: {
118
+ accountId: account.accountId,
119
+ peerKind,
120
+ peerId,
121
+ senderId,
122
+ },
123
+ senderName: senderId,
124
+ text: resolveEventText(body, account),
125
+ timestamp: typeof body.create_time === "number" ? body.create_time : Date.now(),
126
+ raw: {
127
+ transport: "bot-ws",
128
+ command: frame.cmd,
129
+ headers: frame.headers,
130
+ body,
131
+ envelopeType: "ws",
132
+ },
133
+ replyContext: {
134
+ transport: "bot-ws",
135
+ accountId: account.accountId,
136
+ reqId: frame.headers.req_id,
137
+ raw: {
138
+ transport: "bot-ws",
139
+ command: frame.cmd,
140
+ headers: frame.headers,
141
+ body,
142
+ envelopeType: "ws",
143
+ },
144
+ },
145
+ ...(attachments && { attachments }),
146
+ };
147
+ }