@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
@@ -1,394 +0,0 @@
1
- import type { IncomingMessage, ServerResponse } from "node:http";
2
-
3
- import { getWecomRuntime } from "../../runtime.js";
4
- import { decryptWecomEncrypted, verifyWecomSignature } from "../../crypto.js";
5
- import { resolveWecomEgressProxyUrl } from "../../config/index.js";
6
- import { LIMITS, type StreamStore } from "../../monitor/state.js";
7
- import type { StartBotAgentStreamParams } from "../../capability/bot/stream-orchestrator.js";
8
- import type { WecomBotInboundMessage as WecomInboundMessage } from "../../types/index.js";
9
- import type { WecomRuntimeAuditEvent, WecomWebhookTarget } from "../../types/runtime-context.js";
10
- import { wecomFetch } from "../../http.js";
11
- import {
12
- logRouteFailure,
13
- resolveQueryParams,
14
- resolveSignatureParam,
15
- type RouteFailureReason,
16
- writeRouteFailure,
17
- } from "../http/common.js";
18
- import {
19
- buildEncryptedBotWebhookReply,
20
- buildStreamPlaceholderReply,
21
- buildStreamReplyFromState,
22
- buildStreamTextPlaceholderReply,
23
- jsonOk,
24
- parseWecomPlainMessage,
25
- readBotWebhookJsonBody,
26
- resolveBotIdentitySet,
27
- } from "./protocol.js";
28
- import { storeActiveReply } from "./active-reply.js";
29
- import { buildInboundBody, resolveWecomSenderUserId, shouldProcessBotInboundMessage } from "./message-shape.js";
30
-
31
- const ERROR_HELP = "\n\n遇到问题?联系作者: YanHaidao (微信: YanHaidao)";
32
-
33
- type RecordBotOperationalEvent = (
34
- target: Pick<WecomWebhookTarget, "account" | "auditSink">,
35
- event: Omit<WecomRuntimeAuditEvent, "transport">,
36
- ) => void;
37
-
38
- export function createBotWebhookRequestHandler(params: {
39
- streamStore: StreamStore;
40
- logInfo: (target: WecomWebhookTarget, message: string) => void;
41
- logVerbose: (target: WecomWebhookTarget, message: string) => void;
42
- recordBotOperationalEvent: RecordBotOperationalEvent;
43
- startAgentForStream: (params: StartBotAgentStreamParams) => Promise<void>;
44
- }) {
45
- const { streamStore, logInfo, logVerbose, recordBotOperationalEvent, startAgentForStream } = params;
46
-
47
- return async function handleBotWebhookRequest(args: {
48
- req: IncomingMessage;
49
- res: ServerResponse;
50
- path: string;
51
- reqId: string;
52
- targets: WecomWebhookTarget[];
53
- }): Promise<boolean> {
54
- const { req, res, path, reqId, targets } = args;
55
- const query = resolveQueryParams(req);
56
- const timestamp = query.get("timestamp") ?? "";
57
- const nonce = query.get("nonce") ?? "";
58
- const signature = resolveSignatureParam(query);
59
-
60
- if (req.method === "GET") {
61
- const echostr = query.get("echostr") ?? "";
62
- const signatureMatches = targets.filter(
63
- (target) =>
64
- target.account.token &&
65
- verifyWecomSignature({ token: target.account.token, timestamp, nonce, encrypt: echostr, signature }),
66
- );
67
- if (signatureMatches.length !== 1) {
68
- const reason: RouteFailureReason =
69
- signatureMatches.length === 0 ? "wecom_account_not_found" : "wecom_account_conflict";
70
- const candidateIds = (signatureMatches.length > 0 ? signatureMatches : targets).map((target) => target.account.accountId);
71
- logRouteFailure({
72
- reqId,
73
- path,
74
- method: "GET",
75
- reason,
76
- candidateAccountIds: candidateIds,
77
- });
78
- writeRouteFailure(
79
- res,
80
- reason,
81
- reason === "wecom_account_conflict"
82
- ? "Bot callback account conflict: multiple accounts matched signature."
83
- : "Bot callback account not found: signature verification failed.",
84
- );
85
- return true;
86
- }
87
- const target = signatureMatches[0]!;
88
- try {
89
- const plain = decryptWecomEncrypted({
90
- encodingAESKey: target.account.encodingAESKey,
91
- receiveId: target.account.receiveId,
92
- encrypt: echostr,
93
- });
94
- res.statusCode = 200;
95
- res.setHeader("Content-Type", "text/plain; charset=utf-8");
96
- res.end(plain);
97
- return true;
98
- } catch (err) {
99
- res.statusCode = 400;
100
- res.setHeader("Content-Type", "text/plain; charset=utf-8");
101
- res.end(`decrypt failed - 解密失败,请检查 EncodingAESKey${ERROR_HELP}`);
102
- return true;
103
- }
104
- }
105
-
106
- if (req.method !== "POST") return false;
107
-
108
- const body = await readBotWebhookJsonBody(req, 1024 * 1024);
109
- if (!body.ok) {
110
- res.statusCode = 400;
111
- res.end(body.error || "invalid payload");
112
- return true;
113
- }
114
- const record = body.value as any;
115
- const encrypt = String(record?.encrypt ?? record?.Encrypt ?? "");
116
- console.log(
117
- `[wecom] inbound(bot): reqId=${reqId} rawJsonBytes=${Buffer.byteLength(JSON.stringify(record), "utf8")} hasEncrypt=${Boolean(encrypt)} encryptLen=${encrypt.length}`,
118
- );
119
- const signatureMatches = targets.filter(
120
- (target) =>
121
- target.account.token && verifyWecomSignature({ token: target.account.token, timestamp, nonce, encrypt, signature }),
122
- );
123
- if (signatureMatches.length !== 1) {
124
- const reason: RouteFailureReason =
125
- signatureMatches.length === 0 ? "wecom_account_not_found" : "wecom_account_conflict";
126
- const candidateIds = (signatureMatches.length > 0 ? signatureMatches : targets).map((target) => target.account.accountId);
127
- logRouteFailure({
128
- reqId,
129
- path,
130
- method: "POST",
131
- reason,
132
- candidateAccountIds: candidateIds,
133
- });
134
- writeRouteFailure(
135
- res,
136
- reason,
137
- reason === "wecom_account_conflict"
138
- ? "Bot callback account conflict: multiple accounts matched signature."
139
- : "Bot callback account not found: signature verification failed.",
140
- );
141
- return true;
142
- }
143
-
144
- const target = signatureMatches[0]!;
145
- let msg: WecomInboundMessage;
146
- try {
147
- const plain = decryptWecomEncrypted({
148
- encodingAESKey: target.account.encodingAESKey,
149
- receiveId: target.account.receiveId,
150
- encrypt,
151
- });
152
- msg = parseWecomPlainMessage(plain);
153
- } catch {
154
- res.statusCode = 400;
155
- res.setHeader("Content-Type", "text/plain; charset=utf-8");
156
- res.end(`decrypt failed - 解密失败,请检查 EncodingAESKey${ERROR_HELP}`);
157
- return true;
158
- }
159
-
160
- const expected = resolveBotIdentitySet(target);
161
- if (expected.size > 0) {
162
- const inboundAibotId = String((msg as any).aibotid ?? "").trim();
163
- if (!inboundAibotId || !expected.has(inboundAibotId)) {
164
- target.runtime.error?.(
165
- `[wecom] inbound(bot): reqId=${reqId} accountId=${target.account.accountId} aibotid_mismatch expected=${Array.from(expected).join(",")} actual=${inboundAibotId || "N/A"}`,
166
- );
167
- }
168
- }
169
-
170
- logInfo(target, `inbound(bot): reqId=${reqId} selectedAccount=${target.account.accountId} path=${path}`);
171
- target.touchTransportSession?.({ lastInboundAt: Date.now(), running: true });
172
- const msgtype = String(msg.msgtype ?? "").toLowerCase();
173
- const proxyUrl = resolveWecomEgressProxyUrl(target.config);
174
-
175
- if (msgtype === "event") {
176
- const eventtype = String((msg as any).event?.eventtype ?? "").toLowerCase();
177
-
178
- if (eventtype === "template_card_event") {
179
- const msgid = msg.msgid ? String(msg.msgid) : undefined;
180
- if (msgid && streamStore.getStreamByMsgId(msgid)) {
181
- logVerbose(target, `template_card_event: already processed msgid=${msgid}, skipping`);
182
- recordBotOperationalEvent(target, {
183
- category: "duplicate-reply",
184
- messageId: msgid,
185
- summary: `duplicate template card event msgid=${msgid}`,
186
- raw: {
187
- transport: "bot-webhook",
188
- envelopeType: "json",
189
- body: msg,
190
- },
191
- });
192
- jsonOk(res, buildEncryptedBotWebhookReply({ account: target.account, plaintextJson: {}, nonce, timestamp }));
193
- return true;
194
- }
195
-
196
- const cardEvent = (msg as any).event?.template_card_event;
197
- let interactionDesc = `[卡片交互] 按钮: ${cardEvent?.event_key || "unknown"}`;
198
- if (cardEvent?.selected_items?.selected_item?.length) {
199
- const selects = cardEvent.selected_items.selected_item.map(
200
- (i: any) => `${i.question_key}=${i.option_ids?.option_id?.join(",")}`,
201
- );
202
- interactionDesc += ` 选择: ${selects.join("; ")}`;
203
- }
204
- if (cardEvent?.task_id) interactionDesc += ` (任务ID: ${cardEvent.task_id})`;
205
-
206
- jsonOk(res, buildEncryptedBotWebhookReply({ account: target.account, plaintextJson: {}, nonce, timestamp }));
207
-
208
- const streamId = streamStore.createStream({ msgid });
209
- streamStore.markStarted(streamId);
210
- storeActiveReply(streamId, msg.response_url);
211
- const core = getWecomRuntime();
212
- startAgentForStream({
213
- target: { ...target, core },
214
- accountId: target.account.accountId,
215
- msg: { ...msg, msgtype: "text", text: { content: interactionDesc } } as any,
216
- streamId,
217
- }).catch((err) => target.runtime.error?.(`interaction failed: ${String(err)}`));
218
- return true;
219
- }
220
-
221
- if (eventtype === "enter_chat") {
222
- const welcome = target.account.config.welcomeText?.trim();
223
- jsonOk(
224
- res,
225
- buildEncryptedBotWebhookReply({
226
- account: target.account,
227
- plaintextJson: welcome ? { msgtype: "text", text: { content: welcome } } : {},
228
- nonce,
229
- timestamp,
230
- }),
231
- );
232
- return true;
233
- }
234
-
235
- jsonOk(res, buildEncryptedBotWebhookReply({ account: target.account, plaintextJson: {}, nonce, timestamp }));
236
- return true;
237
- }
238
-
239
- if (msgtype === "stream") {
240
- const streamId = String((msg as any).stream?.id ?? "").trim();
241
- const state = streamStore.getStream(streamId);
242
- const reply = state
243
- ? buildStreamReplyFromState(state)
244
- : buildStreamReplyFromState({
245
- streamId: streamId || "unknown",
246
- createdAt: Date.now(),
247
- updatedAt: Date.now(),
248
- started: true,
249
- finished: true,
250
- content: "",
251
- });
252
- jsonOk(res, buildEncryptedBotWebhookReply({ account: target.account, plaintextJson: reply, nonce, timestamp }));
253
- return true;
254
- }
255
-
256
- try {
257
- const decision = shouldProcessBotInboundMessage(msg);
258
- if (!decision.shouldProcess) {
259
- logInfo(
260
- target,
261
- `inbound: skipped msgtype=${msgtype} reason=${decision.reason} chattype=${String(msg.chattype ?? "")} chatid=${String(msg.chatid ?? "")} from=${resolveWecomSenderUserId(msg) || "N/A"}`,
262
- );
263
- jsonOk(res, buildEncryptedBotWebhookReply({ account: target.account, plaintextJson: {}, nonce, timestamp }));
264
- return true;
265
- }
266
-
267
- const userid = decision.senderUserId!;
268
- const chatId = decision.chatId ?? userid;
269
- const conversationKey = `wecom:${target.account.accountId}:${userid}:${chatId}`;
270
- const msgContent = buildInboundBody(msg);
271
-
272
- logInfo(
273
- target,
274
- `inbound: msgtype=${msgtype} chattype=${String(msg.chattype ?? "")} chatid=${String(msg.chatid ?? "")} from=${userid} msgid=${String(msg.msgid ?? "")} hasResponseUrl=${Boolean((msg as any).response_url)}`,
275
- );
276
-
277
- if (msg.msgid) {
278
- const existingStreamId = streamStore.getStreamByMsgId(String(msg.msgid));
279
- if (existingStreamId) {
280
- logInfo(target, `message: 重复的 msgid=${msg.msgid},跳过处理并返回占位符 streamId=${existingStreamId}`);
281
- recordBotOperationalEvent(target, {
282
- category: "duplicate-reply",
283
- messageId: String(msg.msgid),
284
- summary: `duplicate inbound msgid=${String(msg.msgid)} streamId=${existingStreamId}`,
285
- raw: {
286
- transport: "bot-webhook",
287
- envelopeType: "json",
288
- body: msg,
289
- },
290
- });
291
- jsonOk(
292
- res,
293
- buildEncryptedBotWebhookReply({
294
- account: target.account,
295
- plaintextJson: buildStreamPlaceholderReply({
296
- streamId: existingStreamId,
297
- placeholderContent: target.account.config.streamPlaceholderContent,
298
- }),
299
- nonce,
300
- timestamp,
301
- }),
302
- );
303
- return true;
304
- }
305
- }
306
-
307
- const { streamId, status } = streamStore.addPendingMessage({
308
- conversationKey,
309
- target,
310
- msg,
311
- msgContent,
312
- nonce,
313
- timestamp,
314
- debounceMs: (target.account.config as any).debounceMs,
315
- });
316
-
317
- if (msg.response_url) {
318
- storeActiveReply(streamId, msg.response_url, proxyUrl);
319
- }
320
-
321
- const defaultPlaceholder = target.account.config.streamPlaceholderContent;
322
- const queuedPlaceholder = "已收到,已排队处理中...";
323
- const mergedQueuedPlaceholder = "已收到,已合并排队处理中...";
324
-
325
- if (status === "active_new") {
326
- jsonOk(
327
- res,
328
- buildEncryptedBotWebhookReply({
329
- account: target.account,
330
- plaintextJson: buildStreamPlaceholderReply({
331
- streamId,
332
- placeholderContent: defaultPlaceholder,
333
- }),
334
- nonce,
335
- timestamp,
336
- }),
337
- );
338
- return true;
339
- }
340
-
341
- if (status === "queued_new") {
342
- logInfo(target, `queue: 已进入下一批次 streamId=${streamId} msgid=${String(msg.msgid ?? "")}`);
343
- jsonOk(
344
- res,
345
- buildEncryptedBotWebhookReply({
346
- account: target.account,
347
- plaintextJson: buildStreamPlaceholderReply({
348
- streamId,
349
- placeholderContent: queuedPlaceholder,
350
- }),
351
- nonce,
352
- timestamp,
353
- }),
354
- );
355
- return true;
356
- }
357
-
358
- const ackStreamId = streamStore.createStream({ msgid: String(msg.msgid ?? "") || undefined });
359
- streamStore.updateStream(ackStreamId, (s) => {
360
- s.finished = false;
361
- s.started = true;
362
- s.content = mergedQueuedPlaceholder;
363
- });
364
- if (msg.msgid) streamStore.setStreamIdForMsgId(String(msg.msgid), ackStreamId);
365
- streamStore.addAckStreamForBatch({ batchStreamId: streamId, ackStreamId });
366
- logInfo(
367
- target,
368
- `queue: 已合并排队(回执流) ackStreamId=${ackStreamId} mergedIntoStreamId=${streamId} msgid=${String(msg.msgid ?? "")}`,
369
- );
370
- jsonOk(
371
- res,
372
- buildEncryptedBotWebhookReply({
373
- account: target.account,
374
- plaintextJson: buildStreamTextPlaceholderReply({ streamId: ackStreamId, content: mergedQueuedPlaceholder }),
375
- nonce,
376
- timestamp,
377
- }),
378
- );
379
- return true;
380
- } catch (err) {
381
- target.runtime.error?.(`[wecom] Bot message handler crashed: ${String(err)}`);
382
- jsonOk(
383
- res,
384
- buildEncryptedBotWebhookReply({
385
- account: target.account,
386
- plaintextJson: { msgtype: "text", text: { content: "服务内部错误:Bot 处理异常,请稍后重试。" } },
387
- nonce,
388
- timestamp,
389
- }),
390
- );
391
- return true;
392
- }
393
- };
394
- }
@@ -1,23 +0,0 @@
1
- import type { TransportSessionSnapshot } from "../../types/index.js";
2
-
3
- export function createBotWebhookSessionSnapshot(params: {
4
- accountId: string;
5
- running: boolean;
6
- lastInboundAt?: number;
7
- lastOutboundAt?: number;
8
- lastError?: string;
9
- }): TransportSessionSnapshot {
10
- return {
11
- accountId: params.accountId,
12
- transport: "bot-webhook",
13
- running: params.running,
14
- ownerId: `${params.accountId}:bot-webhook`,
15
- connected: params.running,
16
- authenticated: true,
17
- lastConnectedAt: params.running ? Date.now() : undefined,
18
- lastDisconnectedAt: params.running ? undefined : Date.now(),
19
- lastInboundAt: params.lastInboundAt,
20
- lastOutboundAt: params.lastOutboundAt,
21
- lastError: params.lastError,
22
- };
23
- }
@@ -1,96 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
-
3
- import { mapBotWsFrameToInboundEvent } from "./inbound.js";
4
- import type { ResolvedBotAccount } from "../../types/index.js";
5
-
6
- function createBotAccount(): ResolvedBotAccount {
7
- return {
8
- accountId: "haidao",
9
- configured: true,
10
- primaryTransport: "ws",
11
- wsConfigured: true,
12
- webhookConfigured: false,
13
- config: {},
14
- ws: {
15
- botId: "bot-id",
16
- secret: "secret",
17
- },
18
- token: "",
19
- encodingAESKey: "",
20
- receiveId: "",
21
- botId: "bot-id",
22
- secret: "secret",
23
- };
24
- }
25
-
26
- describe("mapBotWsFrameToInboundEvent", () => {
27
- it("includes quote content in text events", () => {
28
- const event = mapBotWsFrameToInboundEvent({
29
- account: createBotAccount(),
30
- frame: {
31
- cmd: "aibot_msg_callback",
32
- headers: { req_id: "req-1" },
33
- body: {
34
- msgid: "msg-1",
35
- msgtype: "text",
36
- chattype: "group",
37
- chatid: "group-1",
38
- from: { userid: "user-1" },
39
- text: { content: "@daodao 这个线索价值" },
40
- quote: {
41
- msgtype: "text",
42
- text: { content: "原始引用内容" },
43
- },
44
- },
45
- },
46
- });
47
-
48
- expect(event.text).toBe("@daodao 这个线索价值\n\n> 原始引用内容");
49
- });
50
-
51
- it("extracts attachments from mixed events", () => {
52
- const event = mapBotWsFrameToInboundEvent({
53
- account: createBotAccount(),
54
- frame: {
55
- cmd: "aibot_msg_callback",
56
- headers: { req_id: "req-2" },
57
- body: {
58
- msgid: "msg-2",
59
- msgtype: "mixed",
60
- chattype: "group",
61
- chatid: "group-2",
62
- from: { userid: "user-2" },
63
- mixed: {
64
- msg_item: [
65
- {
66
- msgtype: "text",
67
- text: { content: "来看看这张图" },
68
- },
69
- {
70
- msgtype: "image",
71
- image: { url: "https://example.com/image.jpg", aeskey: "mock-aes-key" },
72
- },
73
- {
74
- msgtype: "file",
75
- file: { url: "https://example.com/doc.pdf", aeskey: "mock-file-key" },
76
- },
77
- ],
78
- },
79
- },
80
- },
81
- });
82
-
83
- expect(event.attachments).toBeDefined();
84
- expect(event.attachments).toHaveLength(2);
85
- expect(event.attachments![0]).toEqual({
86
- name: "image",
87
- remoteUrl: "https://example.com/image.jpg",
88
- aesKey: "mock-aes-key",
89
- });
90
- expect(event.attachments![1]).toEqual({
91
- name: "file",
92
- remoteUrl: "https://example.com/doc.pdf",
93
- aesKey: "mock-file-key",
94
- });
95
- });
96
- });
@@ -1,116 +0,0 @@
1
- import type { BaseMessage, EventMessage, WsFrame } from "@wecom/aibot-node-sdk";
2
-
3
- import { buildInboundBody } from "../bot-webhook/message-shape.js";
4
- import type {
5
- ResolvedBotAccount,
6
- UnifiedInboundEvent,
7
- WecomBotInboundMessage,
8
- WecomInboundKind,
9
- } from "../../types/index.js";
10
-
11
- function resolveInboundKind(message: BaseMessage | EventMessage): WecomInboundKind {
12
- if (message.msgtype === "event") {
13
- const eventType = String((message as EventMessage).event?.eventtype ?? "").trim();
14
- if (eventType === "enter_chat") return "welcome";
15
- if (eventType === "template_card_event") return "template-card-event";
16
- return "event";
17
- }
18
- switch (message.msgtype) {
19
- case "image":
20
- return "image";
21
- case "file":
22
- return "file";
23
- case "voice":
24
- return "voice";
25
- case "mixed":
26
- return "mixed";
27
- default:
28
- return "text";
29
- }
30
- }
31
-
32
- function resolveEventText(message: BaseMessage | EventMessage, account: ResolvedBotAccount): string {
33
- if (message.msgtype !== "event") {
34
- return buildInboundBody(message as WecomBotInboundMessage);
35
- }
36
-
37
- const event = message as EventMessage;
38
- if (event.event?.eventtype === "enter_chat" && account.config.welcomeText) {
39
- return account.config.welcomeText;
40
- }
41
- return `[event:${String(event.event?.eventtype ?? "unknown")}]`;
42
- }
43
-
44
- export function mapBotWsFrameToInboundEvent(params: {
45
- account: ResolvedBotAccount;
46
- frame: WsFrame<BaseMessage | EventMessage>;
47
- }): UnifiedInboundEvent {
48
- const { account, frame } = params;
49
- const body = frame.body;
50
- if (!body) {
51
- throw new Error("Bot WS frame body is required");
52
- }
53
- const peerKind = body.chattype === "group" ? "group" : "direct";
54
- const senderId = body.from?.userid ?? "unknown";
55
- const peerId = peerKind === "group" ? body.chatid ?? senderId : senderId;
56
- const inboundKind = resolveInboundKind(body);
57
-
58
- let attachments: UnifiedInboundEvent["attachments"];
59
- if (body.msgtype === "image") {
60
- attachments = [{ name: "image", remoteUrl: (body as any).image?.url, aesKey: (body as any).image?.aeskey }];
61
- } else if (body.msgtype === "file") {
62
- attachments = [{ name: "file", remoteUrl: (body as any).file?.url, aesKey: (body as any).file?.aeskey }];
63
- } else if (body.msgtype === "mixed") {
64
- const items = (body as any).mixed?.msg_item;
65
- if (Array.isArray(items)) {
66
- attachments = [];
67
- for (const item of items) {
68
- if (item.msgtype === "image" && item.image?.url) {
69
- attachments.push({ name: "image", remoteUrl: item.image.url, aesKey: item.image.aeskey });
70
- } else if (item.msgtype === "file" && item.file?.url) {
71
- attachments.push({ name: "file", remoteUrl: item.file.url, aesKey: item.file.aeskey });
72
- }
73
- }
74
- if (attachments.length === 0) {
75
- attachments = undefined;
76
- }
77
- }
78
- }
79
-
80
- return {
81
- accountId: account.accountId,
82
- capability: "bot",
83
- transport: "bot-ws",
84
- inboundKind,
85
- messageId: body.msgid,
86
- conversation: {
87
- accountId: account.accountId,
88
- peerKind,
89
- peerId,
90
- senderId,
91
- },
92
- senderName: senderId,
93
- text: resolveEventText(body, account),
94
- timestamp: typeof body.create_time === "number" ? body.create_time : Date.now(),
95
- raw: {
96
- transport: "bot-ws",
97
- command: frame.cmd,
98
- headers: frame.headers,
99
- body,
100
- envelopeType: "ws",
101
- },
102
- replyContext: {
103
- transport: "bot-ws",
104
- accountId: account.accountId,
105
- reqId: frame.headers.req_id,
106
- raw: {
107
- transport: "bot-ws",
108
- command: frame.cmd,
109
- headers: frame.headers,
110
- body,
111
- envelopeType: "ws",
112
- },
113
- },
114
- attachments,
115
- };
116
- }
@@ -1,44 +0,0 @@
1
- import type { WSClient } from "@wecom/aibot-node-sdk";
2
- import { fetchRemoteMedia } from "openclaw/plugin-sdk/media-runtime";
3
- import { beforeEach, describe, expect, it, vi } from "vitest";
4
- import { uploadAndSendBotWsMedia } from "./media.js";
5
-
6
- vi.mock("openclaw/plugin-sdk/media-runtime", () => ({
7
- assertLocalMediaAllowed: vi.fn(),
8
- detectMime: vi.fn(),
9
- fetchRemoteMedia: vi.fn(),
10
- }));
11
-
12
- describe("uploadAndSendBotWsMedia", () => {
13
- const fetchRemoteMediaMock = vi.mocked(fetchRemoteMedia);
14
-
15
- beforeEach(() => {
16
- fetchRemoteMediaMock.mockReset();
17
- fetchRemoteMediaMock.mockResolvedValue({
18
- buffer: Buffer.from("png"),
19
- contentType: "image/png",
20
- fileName: "sample.png",
21
- } as never);
22
- });
23
-
24
- it("passes the configured maxBytes to outbound media loading", async () => {
25
- const wsClient = {
26
- uploadMedia: vi.fn().mockResolvedValue({ media_id: "media-1" }),
27
- sendMediaMessage: vi.fn().mockResolvedValue({ headers: { req_id: "req-1" } }),
28
- } as unknown as WSClient;
29
-
30
- await uploadAndSendBotWsMedia({
31
- wsClient,
32
- chatId: "hidao",
33
- mediaUrl: "https://example.com/sample.png",
34
- maxBytes: 42 * 1024 * 1024,
35
- });
36
-
37
- expect(fetchRemoteMediaMock).toHaveBeenCalledWith(
38
- expect.objectContaining({
39
- url: "https://example.com/sample.png",
40
- maxBytes: 42 * 1024 * 1024,
41
- }),
42
- );
43
- });
44
- });