@yanhaidao/wecom 2.4.160 → 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 (313) hide show
  1. package/dist/index.js +68 -0
  2. package/dist/src/accounts.js +20 -0
  3. package/dist/src/agent/handler.js +895 -0
  4. package/dist/src/agent/index.js +5 -0
  5. package/dist/src/app/account-runtime.js +216 -0
  6. package/dist/src/app/bootstrap.js +19 -0
  7. package/dist/src/app/index.js +118 -0
  8. package/dist/src/capability/agent/delivery-service.js +63 -0
  9. package/dist/src/capability/agent/fallback-policy.js +6 -0
  10. package/dist/src/capability/agent/ingress-service.js +33 -0
  11. package/dist/src/capability/agent/upstream-delivery-service.js +71 -0
  12. package/dist/src/capability/bot/dispatch-config.js +45 -0
  13. package/dist/src/capability/bot/fallback-delivery.js +147 -0
  14. package/dist/src/capability/bot/local-path-delivery.js +178 -0
  15. package/dist/src/capability/bot/sandbox-media.js +138 -0
  16. package/dist/src/capability/bot/service.js +49 -0
  17. package/dist/src/capability/bot/stream-delivery.js +321 -0
  18. package/dist/src/capability/bot/stream-finalizer.js +81 -0
  19. package/dist/src/capability/bot/stream-orchestrator.js +318 -0
  20. package/dist/src/capability/bot/types.js +1 -0
  21. package/{src/capability/calendar/client.ts → dist/src/capability/calendar/client.js} +118 -241
  22. package/{src/capability/calendar/schema.ts → dist/src/capability/calendar/schema.js} +0 -38
  23. package/dist/src/capability/calendar/tool.js +365 -0
  24. package/dist/src/capability/calendar/types.js +12 -0
  25. package/{src/capability/doc/client.ts → dist/src/capability/doc/client.js} +370 -605
  26. package/{src/capability/doc/schema.ts → dist/src/capability/doc/schema.js} +345 -394
  27. package/dist/src/capability/doc/tool.js +1556 -0
  28. package/dist/src/capability/doc/types.js +113 -0
  29. package/dist/src/capability/mcp/index.js +3 -0
  30. package/dist/src/capability/mcp/schema.js +102 -0
  31. package/dist/src/capability/mcp/tool.js +146 -0
  32. package/dist/src/capability/mcp/transport.js +293 -0
  33. package/dist/src/channel.js +224 -0
  34. package/dist/src/config/accounts.js +236 -0
  35. package/dist/src/config/derived-paths.js +31 -0
  36. package/dist/src/config/index.js +7 -0
  37. package/dist/src/config/media.js +110 -0
  38. package/dist/src/config/network.js +32 -0
  39. package/dist/src/config/routing.js +20 -0
  40. package/dist/src/config/runtime-config.js +25 -0
  41. package/dist/src/config/schema.js +4 -0
  42. package/{src/config-schema.ts → dist/src/config-schema.js} +1 -1
  43. package/dist/src/context-store.js +219 -0
  44. package/{src/crypto/aes.ts → dist/src/crypto/aes.js} +11 -28
  45. package/dist/src/crypto/index.js +9 -0
  46. package/{src/crypto/signature.ts → dist/src/crypto/signature.js} +3 -18
  47. package/{src/crypto/xml.ts → dist/src/crypto/xml.js} +3 -11
  48. package/dist/src/crypto.js +145 -0
  49. package/dist/src/domain/models.js +1 -0
  50. package/dist/src/domain/policies.js +32 -0
  51. package/{src/dynamic-agent.ts → dist/src/dynamic-agent.js} +36 -73
  52. package/dist/src/gateway-monitor.js +139 -0
  53. package/dist/src/http.js +114 -0
  54. package/{src/media.ts → dist/src/media.js} +21 -40
  55. package/dist/src/monitor/limits.js +7 -0
  56. package/dist/src/monitor/state.js +28 -0
  57. package/dist/src/monitor.js +84 -0
  58. package/dist/src/observability/audit-log.js +30 -0
  59. package/dist/src/observability/legacy-operational-event-store.js +22 -0
  60. package/dist/src/observability/raw-envelope-log.js +24 -0
  61. package/dist/src/observability/status-registry.js +9 -0
  62. package/dist/src/observability/transport-session-view.js +14 -0
  63. package/dist/src/onboarding.js +546 -0
  64. package/dist/src/outbound.js +557 -0
  65. package/dist/src/runtime/dispatcher.js +57 -0
  66. package/{src/runtime/index.ts → dist/src/runtime/index.js} +0 -1
  67. package/dist/src/runtime/outbound-intent.js +1 -0
  68. package/dist/src/runtime/reply-orchestrator.js +38 -0
  69. package/dist/src/runtime/routing-bridge.js +26 -0
  70. package/dist/src/runtime/session-manager.js +112 -0
  71. package/dist/src/runtime/source-registry.js +174 -0
  72. package/dist/src/runtime.js +1 -0
  73. package/dist/src/shared/command-auth.js +57 -0
  74. package/{src/shared/index.ts → dist/src/shared/index.js} +0 -1
  75. package/dist/src/shared/media-asset.js +65 -0
  76. package/dist/src/shared/media-service.js +59 -0
  77. package/dist/src/shared/media-types.js +1 -0
  78. package/{src/shared/xml-parser.ts → dist/src/shared/xml-parser.js} +72 -63
  79. package/dist/src/store/active-reply-store.js +41 -0
  80. package/dist/src/store/interfaces.js +1 -0
  81. package/dist/src/store/memory-store.js +33 -0
  82. package/dist/src/store/stream-batch-store.js +319 -0
  83. package/{src/target.ts → dist/src/target.js} +15 -48
  84. package/dist/src/transport/agent-api/client.js +168 -0
  85. package/dist/src/transport/agent-api/core.js +337 -0
  86. package/dist/src/transport/agent-api/delivery.js +28 -0
  87. package/dist/src/transport/agent-api/media-upload.js +4 -0
  88. package/dist/src/transport/agent-api/reply.js +24 -0
  89. package/dist/src/transport/agent-api/upstream-delivery.js +30 -0
  90. package/dist/src/transport/agent-api/upstream-media-upload.js +46 -0
  91. package/dist/src/transport/agent-api/upstream-reply.js +26 -0
  92. package/dist/src/transport/agent-callback/http-handler.js +30 -0
  93. package/dist/src/transport/agent-callback/inbound.js +4 -0
  94. package/dist/src/transport/agent-callback/reply.js +8 -0
  95. package/dist/src/transport/agent-callback/request-handler.js +189 -0
  96. package/dist/src/transport/agent-callback/session.js +15 -0
  97. package/dist/src/transport/bot-webhook/active-reply.js +27 -0
  98. package/dist/src/transport/bot-webhook/http-handler.js +31 -0
  99. package/dist/src/transport/bot-webhook/inbound-normalizer.js +496 -0
  100. package/dist/src/transport/bot-webhook/inbound.js +4 -0
  101. package/dist/src/transport/bot-webhook/message-shape.js +98 -0
  102. package/dist/src/transport/bot-webhook/protocol.js +124 -0
  103. package/dist/src/transport/bot-webhook/reply.js +9 -0
  104. package/dist/src/transport/bot-webhook/request-handler.js +285 -0
  105. package/dist/src/transport/bot-webhook/session.js +15 -0
  106. package/dist/src/transport/bot-ws/inbound.js +147 -0
  107. package/dist/src/transport/bot-ws/media.js +236 -0
  108. package/dist/src/transport/bot-ws/reply.js +310 -0
  109. package/dist/src/transport/bot-ws/sdk-adapter.js +257 -0
  110. package/dist/src/transport/bot-ws/session.js +15 -0
  111. package/dist/src/transport/http/common.js +78 -0
  112. package/dist/src/transport/http/registry.js +71 -0
  113. package/dist/src/transport/http/request-handler.js +51 -0
  114. package/{src/transport/index.ts → dist/src/transport/index.js} +2 -10
  115. package/dist/src/types/account.js +1 -0
  116. package/dist/src/types/config.js +1 -0
  117. package/dist/src/types/constants.js +28 -0
  118. package/dist/src/types/events.js +1 -0
  119. package/dist/src/types/index.js +1 -0
  120. package/dist/src/types/legacy-stream.js +1 -0
  121. package/dist/src/types/message.js +5 -0
  122. package/dist/src/types/runtime-context.js +1 -0
  123. package/dist/src/types/runtime.js +1 -0
  124. package/dist/src/types.js +1 -0
  125. package/dist/src/upstream/index.js +111 -0
  126. package/dist/src/wecom_msg_adapter/markdown_adapter.js +280 -0
  127. package/openclaw.plugin.json +15 -0
  128. package/package.json +18 -1
  129. package/.github/workflows/release.yml +0 -143
  130. package/GOVERNANCE.md +0 -26
  131. package/SKILLS_CAL.md +0 -895
  132. package/SKILLS_DOC.md +0 -2288
  133. package/UPSTREAM_CONFIG.md +0 -170
  134. package/UPSTREAM_PLAN.md +0 -175
  135. package/assets/01.bot-add.png +0 -0
  136. package/assets/01.bot-setp2.png +0 -0
  137. package/assets/01.image.jpg +0 -0
  138. package/assets/02.agent.add.png +0 -0
  139. package/assets/02.agent.api-set.png +0 -0
  140. package/assets/02.image.jpg +0 -0
  141. package/assets/03.agent.page.png +0 -0
  142. package/assets/03.bot.page.png +0 -0
  143. package/assets/link-me.jpg +0 -0
  144. package/assets/register.png +0 -0
  145. package/changelog/v2.2.28.md +0 -70
  146. package/changelog/v2.3.10.md +0 -17
  147. package/changelog/v2.3.11.md +0 -19
  148. package/changelog/v2.3.12.md +0 -25
  149. package/changelog/v2.3.13.md +0 -19
  150. package/changelog/v2.3.14.md +0 -48
  151. package/changelog/v2.3.15.md +0 -15
  152. package/changelog/v2.3.16.md +0 -11
  153. package/changelog/v2.3.18.md +0 -22
  154. package/changelog/v2.3.19.md +0 -73
  155. package/changelog/v2.3.2.md +0 -28
  156. package/changelog/v2.3.26.md +0 -21
  157. package/changelog/v2.3.27.md +0 -33
  158. package/changelog/v2.3.4.md +0 -20
  159. package/changelog/v2.3.9.md +0 -22
  160. package/changelog/v2.4.12.md +0 -37
  161. package/changelog/v2.4.16.md +0 -19
  162. package/compat-single-account.md +0 -148
  163. package/index.test.ts +0 -38
  164. package/scripts/test-proxy.ts +0 -70
  165. package/src/accounts.ts +0 -34
  166. package/src/agent/api-client.upload.test.ts +0 -109
  167. package/src/agent/handler.event-filter.test.ts +0 -100
  168. package/src/agent/handler.ts +0 -1105
  169. package/src/agent/index.ts +0 -12
  170. package/src/app/account-runtime.ts +0 -276
  171. package/src/app/bootstrap.ts +0 -29
  172. package/src/app/index.ts +0 -192
  173. package/src/capability/agent/delivery-service.ts +0 -87
  174. package/src/capability/agent/fallback-policy.ts +0 -13
  175. package/src/capability/agent/ingress-service.ts +0 -38
  176. package/src/capability/agent/upstream-delivery-service.ts +0 -96
  177. package/src/capability/bot/dispatch-config.ts +0 -47
  178. package/src/capability/bot/fallback-delivery.ts +0 -178
  179. package/src/capability/bot/local-path-delivery.ts +0 -215
  180. package/src/capability/bot/sandbox-media.test.ts +0 -221
  181. package/src/capability/bot/sandbox-media.ts +0 -176
  182. package/src/capability/bot/service.ts +0 -56
  183. package/src/capability/bot/stream-delivery.ts +0 -379
  184. package/src/capability/bot/stream-finalizer.ts +0 -120
  185. package/src/capability/bot/stream-orchestrator.ts +0 -371
  186. package/src/capability/bot/types.ts +0 -8
  187. package/src/capability/calendar/SKILLS_CHECKLIST.md +0 -251
  188. package/src/capability/calendar/tool.ts +0 -417
  189. package/src/capability/calendar/types.ts +0 -309
  190. package/src/capability/doc/tool.ts +0 -1629
  191. package/src/capability/doc/types.ts +0 -792
  192. package/src/capability/mcp/index.ts +0 -10
  193. package/src/capability/mcp/schema.ts +0 -107
  194. package/src/capability/mcp/tool.ts +0 -174
  195. package/src/capability/mcp/transport.ts +0 -394
  196. package/src/channel.config.test.ts +0 -147
  197. package/src/channel.lifecycle.test.ts +0 -255
  198. package/src/channel.meta.test.ts +0 -26
  199. package/src/channel.ts +0 -256
  200. package/src/config/accounts.resolve.test.ts +0 -75
  201. package/src/config/accounts.ts +0 -296
  202. package/src/config/derived-paths.test.ts +0 -111
  203. package/src/config/derived-paths.ts +0 -41
  204. package/src/config/index.ts +0 -26
  205. package/src/config/media.test.ts +0 -113
  206. package/src/config/media.ts +0 -139
  207. package/src/config/network.ts +0 -53
  208. package/src/config/routing.test.ts +0 -88
  209. package/src/config/routing.ts +0 -26
  210. package/src/config/runtime-config.ts +0 -46
  211. package/src/config/schema.ts +0 -90
  212. package/src/context-store.ts +0 -297
  213. package/src/crypto/index.ts +0 -24
  214. package/src/crypto.test.ts +0 -32
  215. package/src/crypto.ts +0 -176
  216. package/src/domain/models.ts +0 -7
  217. package/src/domain/policies.ts +0 -36
  218. package/src/dynamic-agent.account-scope.test.ts +0 -17
  219. package/src/gateway-monitor.ts +0 -181
  220. package/src/http.ts +0 -145
  221. package/src/media.test.ts +0 -82
  222. package/src/monitor/limits.ts +0 -7
  223. package/src/monitor/state.queue.test.ts +0 -185
  224. package/src/monitor/state.ts +0 -34
  225. package/src/monitor.active.test.ts +0 -245
  226. package/src/monitor.inbound-filter.test.ts +0 -63
  227. package/src/monitor.integration.test.ts +0 -208
  228. package/src/monitor.ts +0 -121
  229. package/src/monitor.webhook.test.ts +0 -774
  230. package/src/observability/audit-log.ts +0 -48
  231. package/src/observability/legacy-operational-event-store.ts +0 -36
  232. package/src/observability/raw-envelope-log.ts +0 -28
  233. package/src/observability/status-registry.ts +0 -13
  234. package/src/observability/transport-session-view.ts +0 -14
  235. package/src/onboarding.test.ts +0 -336
  236. package/src/onboarding.ts +0 -704
  237. package/src/outbound.test.ts +0 -1271
  238. package/src/outbound.ts +0 -746
  239. package/src/runtime/dispatcher.ts +0 -71
  240. package/src/runtime/outbound-intent.ts +0 -4
  241. package/src/runtime/reply-orchestrator.test.ts +0 -71
  242. package/src/runtime/reply-orchestrator.ts +0 -67
  243. package/src/runtime/routing-bridge.test.ts +0 -115
  244. package/src/runtime/routing-bridge.ts +0 -44
  245. package/src/runtime/session-manager.test.ts +0 -174
  246. package/src/runtime/session-manager.ts +0 -139
  247. package/src/runtime/source-registry.ts +0 -249
  248. package/src/runtime.ts +0 -14
  249. package/src/shared/command-auth.ts +0 -87
  250. package/src/shared/media-asset.ts +0 -78
  251. package/src/shared/media-service.test.ts +0 -111
  252. package/src/shared/media-service.ts +0 -84
  253. package/src/shared/media-types.ts +0 -5
  254. package/src/shared/xml-parser.test.ts +0 -50
  255. package/src/store/active-reply-store.ts +0 -42
  256. package/src/store/interfaces.ts +0 -11
  257. package/src/store/memory-store.ts +0 -43
  258. package/src/store/stream-batch-store.ts +0 -350
  259. package/src/transport/agent-api/client.ts +0 -277
  260. package/src/transport/agent-api/core.ts +0 -463
  261. package/src/transport/agent-api/delivery.ts +0 -41
  262. package/src/transport/agent-api/media-upload.ts +0 -11
  263. package/src/transport/agent-api/reply.ts +0 -39
  264. package/src/transport/agent-api/upstream-delivery.ts +0 -45
  265. package/src/transport/agent-api/upstream-media-upload.ts +0 -70
  266. package/src/transport/agent-api/upstream-reply.ts +0 -43
  267. package/src/transport/agent-callback/http-handler.ts +0 -47
  268. package/src/transport/agent-callback/inbound.ts +0 -5
  269. package/src/transport/agent-callback/reply.ts +0 -13
  270. package/src/transport/agent-callback/request-handler.ts +0 -244
  271. package/src/transport/agent-callback/session.ts +0 -23
  272. package/src/transport/bot-webhook/active-reply.ts +0 -39
  273. package/src/transport/bot-webhook/http-handler.ts +0 -48
  274. package/src/transport/bot-webhook/inbound-normalizer.test.ts +0 -433
  275. package/src/transport/bot-webhook/inbound-normalizer.ts +0 -558
  276. package/src/transport/bot-webhook/inbound.ts +0 -5
  277. package/src/transport/bot-webhook/message-shape.ts +0 -92
  278. package/src/transport/bot-webhook/protocol.ts +0 -148
  279. package/src/transport/bot-webhook/reply.ts +0 -15
  280. package/src/transport/bot-webhook/request-handler.ts +0 -394
  281. package/src/transport/bot-webhook/session.ts +0 -23
  282. package/src/transport/bot-ws/inbound.test.ts +0 -290
  283. package/src/transport/bot-ws/inbound.ts +0 -163
  284. package/src/transport/bot-ws/media.test.ts +0 -44
  285. package/src/transport/bot-ws/media.ts +0 -321
  286. package/src/transport/bot-ws/reply.test.ts +0 -450
  287. package/src/transport/bot-ws/reply.ts +0 -365
  288. package/src/transport/bot-ws/sdk-adapter.test.ts +0 -187
  289. package/src/transport/bot-ws/sdk-adapter.ts +0 -314
  290. package/src/transport/bot-ws/session.ts +0 -28
  291. package/src/transport/http/common.ts +0 -109
  292. package/src/transport/http/registry.ts +0 -92
  293. package/src/transport/http/request-handler.ts +0 -84
  294. package/src/types/account.ts +0 -70
  295. package/src/types/config.ts +0 -114
  296. package/src/types/constants.ts +0 -31
  297. package/src/types/events.ts +0 -21
  298. package/src/types/global.d.ts +0 -9
  299. package/src/types/index.ts +0 -17
  300. package/src/types/legacy-stream.ts +0 -50
  301. package/src/types/message.ts +0 -189
  302. package/src/types/runtime-context.ts +0 -28
  303. package/src/types/runtime.ts +0 -165
  304. package/src/types.ts +0 -41
  305. package/src/upstream/index.ts +0 -150
  306. package/src/upstream.test.ts +0 -84
  307. package/src/wecom_msg_adapter/markdown_adapter.ts +0 -331
  308. package/tsconfig.json +0 -22
  309. package/vitest.config.ts +0 -26
  310. /package/{src/capability/agent/index.ts → dist/src/capability/agent/index.js} +0 -0
  311. /package/{src/capability/bot/index.ts → dist/src/capability/bot/index.js} +0 -0
  312. /package/{src/capability/calendar/index.ts → dist/src/capability/calendar/index.js} +0 -0
  313. /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
+ }