@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,4 @@
1
+ /**
2
+ * @deprecated No longer a Zod schema. Kept as a type-only export for backward compatibility.
3
+ */
4
+ export const WecomConfigSchema = undefined;
@@ -2,4 +2,4 @@
2
2
  * Backward-compatible schema export.
3
3
  * Canonical schema lives in `src/config/schema.ts`.
4
4
  */
5
- export { WecomConfigSchema, type WecomConfigInput } from "./config/schema.js";
5
+ export { WecomConfigSchema } from "./config/schema.js";
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Context store for WeCom Bot WS proactive push.
3
+ *
4
+ * Similar to Weixin's contextToken mechanism, we need to track:
5
+ * - Which accountId has active sessions with which peerId
6
+ * - The contextToken for routing outbound messages
7
+ */
8
+ import { randomUUID } from "node:crypto";
9
+ import fs from "node:fs";
10
+ import path from "node:path";
11
+ // Simple logger
12
+ const logger = {
13
+ info: (...args) => console.log('[wecom-context]', ...args),
14
+ warn: (...args) => console.warn('[wecom-context]', ...args),
15
+ debug: (...args) => process.env.DEBUG && console.log('[wecom-context]', ...args),
16
+ };
17
+ // In-memory store: accountId -> peerId -> context info
18
+ const peerContextStore = new Map();
19
+ // Reverse lookup: peerId -> accountId (for routing outbound)
20
+ const peerToAccountMap = new Map();
21
+ const contextTokenToPeerMap = new Map();
22
+ function resolveStateDir() {
23
+ return process.env.OPENCLAW_STATE_DIR || path.join(process.env.HOME || "/tmp", ".openclaw");
24
+ }
25
+ function resolveContextFilePath(accountId) {
26
+ return path.join(resolveStateDir(), "wecom", "context", `${accountId}.json`);
27
+ }
28
+ /** Persist peer contexts for an account to disk */
29
+ function persistContexts(accountId) {
30
+ const peerMap = peerContextStore.get(accountId);
31
+ if (!peerMap)
32
+ return;
33
+ const data = {};
34
+ for (const [peerId, info] of peerMap) {
35
+ data[peerId] = info;
36
+ }
37
+ const filePath = resolveContextFilePath(accountId);
38
+ try {
39
+ const dir = path.dirname(filePath);
40
+ fs.mkdirSync(dir, { recursive: true });
41
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 0), "utf-8");
42
+ }
43
+ catch (err) {
44
+ logger.warn?.(`persistContexts: failed to write ${filePath}: ${String(err)}`);
45
+ }
46
+ }
47
+ function normalizeContextToken(value) {
48
+ const token = typeof value === "string" ? value.trim() : "";
49
+ return token || undefined;
50
+ }
51
+ function normalizePeerKind(value) {
52
+ return value === "group" ? "group" : "direct";
53
+ }
54
+ function normalizeOptional(value) {
55
+ const normalized = typeof value === "string" ? value.trim() : "";
56
+ return normalized || undefined;
57
+ }
58
+ function findStoredPeerContext(accountId, peerId) {
59
+ const peerMap = peerContextStore.get(accountId);
60
+ if (!peerMap)
61
+ return undefined;
62
+ const exact = peerMap.get(peerId);
63
+ if (exact)
64
+ return exact;
65
+ const normalizedPeerId = peerId.trim().toLowerCase();
66
+ for (const [storedPeerId, info] of peerMap) {
67
+ if (storedPeerId.trim().toLowerCase() === normalizedPeerId) {
68
+ return info;
69
+ }
70
+ }
71
+ return undefined;
72
+ }
73
+ function registerPeerContext(accountId, peerId, info) {
74
+ let peerMap = peerContextStore.get(accountId);
75
+ if (!peerMap) {
76
+ peerMap = new Map();
77
+ peerContextStore.set(accountId, peerMap);
78
+ }
79
+ const previous = peerMap.get(peerId);
80
+ if (previous?.contextToken && previous.contextToken !== info.contextToken) {
81
+ contextTokenToPeerMap.delete(previous.contextToken);
82
+ }
83
+ peerMap.set(peerId, info);
84
+ peerToAccountMap.set(peerId, accountId);
85
+ contextTokenToPeerMap.set(info.contextToken, {
86
+ accountId,
87
+ peerId,
88
+ ...info,
89
+ });
90
+ }
91
+ function resolveStoredPeerContext(accountId, peerId, params) {
92
+ const existing = findStoredPeerContext(accountId, peerId);
93
+ return {
94
+ contextToken: normalizeContextToken(params.contextToken) ??
95
+ existing?.contextToken ??
96
+ randomUUID(),
97
+ peerKind: params.peerKind ?? existing?.peerKind ?? "direct",
98
+ lastSeen: params.lastSeen ?? Date.now(),
99
+ ...(normalizeOptional(params.upstreamCorpId) || existing?.upstreamCorpId
100
+ ? { upstreamCorpId: normalizeOptional(params.upstreamCorpId) ?? existing?.upstreamCorpId }
101
+ : {}),
102
+ };
103
+ }
104
+ /** Restore persisted peer contexts for an account */
105
+ export function restorePeerContexts(accountId) {
106
+ const filePath = resolveContextFilePath(accountId);
107
+ try {
108
+ if (!fs.existsSync(filePath))
109
+ return;
110
+ const raw = fs.readFileSync(filePath, "utf-8");
111
+ const data = JSON.parse(raw);
112
+ const peerMap = new Map();
113
+ let count = 0;
114
+ let mutated = false;
115
+ for (const [peerId, info] of Object.entries(data)) {
116
+ const normalized = {
117
+ contextToken: normalizeContextToken(info?.contextToken) ?? randomUUID(),
118
+ peerKind: normalizePeerKind(info?.peerKind),
119
+ lastSeen: typeof info?.lastSeen === "number" && Number.isFinite(info.lastSeen)
120
+ ? info.lastSeen
121
+ : Date.now(),
122
+ ...(normalizeOptional(info?.upstreamCorpId)
123
+ ? { upstreamCorpId: normalizeOptional(info?.upstreamCorpId) }
124
+ : {}),
125
+ };
126
+ peerMap.set(peerId, normalized);
127
+ peerToAccountMap.set(peerId, accountId);
128
+ contextTokenToPeerMap.set(normalized.contextToken, {
129
+ accountId,
130
+ peerId,
131
+ ...normalized,
132
+ });
133
+ if (normalized.contextToken !== info?.contextToken ||
134
+ normalized.peerKind !== info?.peerKind ||
135
+ normalized.lastSeen !== info?.lastSeen ||
136
+ normalized.upstreamCorpId !== normalizeOptional(info?.upstreamCorpId)) {
137
+ mutated = true;
138
+ }
139
+ count++;
140
+ }
141
+ peerContextStore.set(accountId, peerMap);
142
+ if (mutated) {
143
+ persistContexts(accountId);
144
+ }
145
+ logger.info?.(`restorePeerContexts: restored ${count} peers for account=${accountId}`);
146
+ }
147
+ catch (err) {
148
+ logger.warn?.(`restorePeerContexts: failed to read ${filePath}: ${String(err)}`);
149
+ }
150
+ }
151
+ /** Store context for a peer (called on inbound message) */
152
+ export function setPeerContext(accountId, peerId, options) {
153
+ const resolved = resolveStoredPeerContext(accountId, peerId, options ?? {});
154
+ registerPeerContext(accountId, peerId, resolved);
155
+ // Persist to disk (debounced would be better, but simple for now)
156
+ persistContexts(accountId);
157
+ logger.debug?.(`setPeerContext: accountId=${accountId} peerId=${peerId} token=${resolved.contextToken} kind=${resolved.peerKind}`);
158
+ return resolved.contextToken;
159
+ }
160
+ /** Get the accountId that has an active session with a peer */
161
+ export function getAccountIdByPeer(peerId) {
162
+ return peerToAccountMap.get(peerId);
163
+ }
164
+ /** Get the most recent peerId for an account (for proactive push) */
165
+ export function getRecentPeerForAccount(accountId, maxAgeMs = 30 * 60 * 1000) {
166
+ const peerMap = peerContextStore.get(accountId);
167
+ if (!peerMap)
168
+ return undefined;
169
+ let mostRecent;
170
+ for (const [peerId, info] of peerMap) {
171
+ if (Date.now() - info.lastSeen > maxAgeMs)
172
+ continue;
173
+ if (!mostRecent || info.lastSeen > mostRecent.lastSeen) {
174
+ mostRecent = { peerId, lastSeen: info.lastSeen };
175
+ }
176
+ }
177
+ return mostRecent?.peerId;
178
+ }
179
+ /** Get context token for a peer */
180
+ export function getPeerContextToken(accountId, peerId) {
181
+ return findStoredPeerContext(accountId, peerId)?.contextToken;
182
+ }
183
+ export function getPeerUpstreamCorpId(accountId, peerId) {
184
+ return findStoredPeerContext(accountId, peerId)?.upstreamCorpId;
185
+ }
186
+ /** Resolve a peer context from a context token. */
187
+ export function getPeerContextByToken(contextToken) {
188
+ return contextTokenToPeerMap.get(contextToken);
189
+ }
190
+ /** Resolve accountId from a context token. */
191
+ export function getAccountIdByContextToken(contextToken) {
192
+ return contextTokenToPeerMap.get(contextToken)?.accountId;
193
+ }
194
+ /** Check if we have an active session for routing */
195
+ export function hasActiveSession(accountId, peerId, maxAgeMs = 30 * 60 * 1000) {
196
+ const info = findStoredPeerContext(accountId, peerId);
197
+ if (!info)
198
+ return false;
199
+ return Date.now() - info.lastSeen < maxAgeMs;
200
+ }
201
+ /** Clear all contexts for an account */
202
+ export function clearPeerContexts(accountId) {
203
+ const peerMap = peerContextStore.get(accountId);
204
+ if (peerMap) {
205
+ for (const [peerId, info] of peerMap) {
206
+ peerToAccountMap.delete(peerId);
207
+ contextTokenToPeerMap.delete(info.contextToken);
208
+ }
209
+ }
210
+ peerContextStore.delete(accountId);
211
+ const filePath = resolveContextFilePath(accountId);
212
+ try {
213
+ if (fs.existsSync(filePath))
214
+ fs.unlinkSync(filePath);
215
+ }
216
+ catch (err) {
217
+ logger.warn?.(`clearPeerContexts: failed to remove ${filePath}`);
218
+ }
219
+ }
@@ -2,13 +2,12 @@
2
2
  * WeCom AES-256-CBC 加解密核心
3
3
  * Bot 和 Agent 模式共用
4
4
  */
5
-
6
5
  import crypto from "node:crypto";
7
6
  import { CRYPTO } from "../types/constants.js";
8
-
9
- export function decodeEncodingAESKey(encodingAESKey: string): Buffer {
7
+ export function decodeEncodingAESKey(encodingAESKey) {
10
8
  const trimmed = encodingAESKey.trim();
11
- if (!trimmed) throw new Error("encodingAESKey missing");
9
+ if (!trimmed)
10
+ throw new Error("encodingAESKey missing");
12
11
  const withPadding = trimmed.endsWith("=") ? trimmed : `${trimmed}=`;
13
12
  const key = Buffer.from(withPadding, "base64");
14
13
  if (key.length !== CRYPTO.AES_KEY_LENGTH) {
@@ -16,17 +15,16 @@ export function decodeEncodingAESKey(encodingAESKey: string): Buffer {
16
15
  }
17
16
  return key;
18
17
  }
19
-
20
- function pkcs7Pad(buf: Buffer, blockSize: number): Buffer {
18
+ function pkcs7Pad(buf, blockSize) {
21
19
  const mod = buf.length % blockSize;
22
20
  const pad = mod === 0 ? blockSize : blockSize - mod;
23
21
  const padByte = Buffer.from([pad]);
24
- return Buffer.concat([buf, Buffer.alloc(pad, padByte[0]!)]);
22
+ return Buffer.concat([buf, Buffer.alloc(pad, padByte[0])]);
25
23
  }
26
-
27
- export function pkcs7Unpad(buf: Buffer, blockSize: number): Buffer {
28
- if (buf.length === 0) throw new Error("invalid pkcs7 payload");
29
- const pad = buf[buf.length - 1]!;
24
+ export function pkcs7Unpad(buf, blockSize) {
25
+ if (buf.length === 0)
26
+ throw new Error("invalid pkcs7 payload");
27
+ const pad = buf[buf.length - 1];
30
28
  if (pad < 1 || pad > blockSize) {
31
29
  throw new Error("invalid pkcs7 padding");
32
30
  }
@@ -40,15 +38,10 @@ export function pkcs7Unpad(buf: Buffer, blockSize: number): Buffer {
40
38
  }
41
39
  return buf.subarray(0, buf.length - pad);
42
40
  }
43
-
44
41
  /**
45
42
  * 解密 WeCom 加密消息
46
43
  */
47
- export function decryptWecomEncrypted(params: {
48
- encodingAESKey: string;
49
- receiveId?: string;
50
- encrypt: string;
51
- }): string {
44
+ export function decryptWecomEncrypted(params) {
52
45
  const aesKey = decodeEncodingAESKey(params.encodingAESKey);
53
46
  const iv = aesKey.subarray(0, 16);
54
47
  const decipher = crypto.createDecipheriv("aes-256-cbc", aesKey, iv);
@@ -58,11 +51,9 @@ export function decryptWecomEncrypted(params: {
58
51
  decipher.final(),
59
52
  ]);
60
53
  const decrypted = pkcs7Unpad(decryptedPadded, CRYPTO.PKCS7_BLOCK_SIZE);
61
-
62
54
  if (decrypted.length < 20) {
63
55
  throw new Error(`invalid payload (expected >=20 bytes, got ${decrypted.length})`);
64
56
  }
65
-
66
57
  // 16 bytes random + 4 bytes length + msg + receiveId
67
58
  const msgLen = decrypted.readUInt32BE(16);
68
59
  const msgStart = 20;
@@ -71,7 +62,6 @@ export function decryptWecomEncrypted(params: {
71
62
  throw new Error(`invalid msg length (msgEnd=${msgEnd}, total=${decrypted.length})`);
72
63
  }
73
64
  const msg = decrypted.subarray(msgStart, msgEnd).toString("utf8");
74
-
75
65
  const receiveId = params.receiveId ?? "";
76
66
  if (receiveId) {
77
67
  const trailing = decrypted.subarray(msgEnd).toString("utf8");
@@ -79,18 +69,12 @@ export function decryptWecomEncrypted(params: {
79
69
  throw new Error(`receiveId mismatch (expected "${receiveId}", got "${trailing}")`);
80
70
  }
81
71
  }
82
-
83
72
  return msg;
84
73
  }
85
-
86
74
  /**
87
75
  * 加密明文为 WeCom 格式
88
76
  */
89
- export function encryptWecomPlaintext(params: {
90
- encodingAESKey: string;
91
- receiveId?: string;
92
- plaintext: string;
93
- }): string {
77
+ export function encryptWecomPlaintext(params) {
94
78
  const aesKey = decodeEncodingAESKey(params.encodingAESKey);
95
79
  const iv = aesKey.subarray(0, 16);
96
80
  const random16 = crypto.randomBytes(16);
@@ -98,7 +82,6 @@ export function encryptWecomPlaintext(params: {
98
82
  const msgLen = Buffer.alloc(4);
99
83
  msgLen.writeUInt32BE(msg.length, 0);
100
84
  const receiveId = Buffer.from(params.receiveId ?? "", "utf8");
101
-
102
85
  const raw = Buffer.concat([random16, msgLen, msg, receiveId]);
103
86
  const padded = pkcs7Pad(raw, CRYPTO.PKCS7_BLOCK_SIZE);
104
87
  const cipher = crypto.createCipheriv("aes-256-cbc", aesKey, iv);
@@ -0,0 +1,9 @@
1
+ /**
2
+ * WeCom 加解密模块导出
3
+ */
4
+ // AES 加解密
5
+ export { decodeEncodingAESKey, pkcs7Unpad, decryptWecomEncrypted, encryptWecomPlaintext, } from "./aes.js";
6
+ // 签名验证
7
+ export { computeWecomMsgSignature, verifyWecomSignature, } from "./signature.js";
8
+ // XML 辅助
9
+ export { extractEncryptFromXml, extractToUserNameFromXml, buildEncryptedXmlResponse, } from "./xml.js";
@@ -1,38 +1,23 @@
1
1
  /**
2
2
  * WeCom 签名计算与验证
3
3
  */
4
-
5
4
  import crypto from "node:crypto";
6
-
7
- function sha1Hex(input: string): string {
5
+ function sha1Hex(input) {
8
6
  return crypto.createHash("sha1").update(input).digest("hex");
9
7
  }
10
-
11
8
  /**
12
9
  * 计算 WeCom 消息签名
13
10
  */
14
- export function computeWecomMsgSignature(params: {
15
- token: string;
16
- timestamp: string;
17
- nonce: string;
18
- encrypt: string;
19
- }): string {
11
+ export function computeWecomMsgSignature(params) {
20
12
  const parts = [params.token, params.timestamp, params.nonce, params.encrypt]
21
13
  .map((v) => String(v ?? ""))
22
14
  .sort();
23
15
  return sha1Hex(parts.join(""));
24
16
  }
25
-
26
17
  /**
27
18
  * 验证 WeCom 消息签名
28
19
  */
29
- export function verifyWecomSignature(params: {
30
- token: string;
31
- timestamp: string;
32
- nonce: string;
33
- encrypt: string;
34
- signature: string;
35
- }): boolean {
20
+ export function verifyWecomSignature(params) {
36
21
  const expected = computeWecomMsgSignature({
37
22
  token: params.token,
38
23
  timestamp: params.timestamp,
@@ -2,11 +2,10 @@
2
2
  * WeCom XML 加解密辅助函数
3
3
  * 用于 Agent 模式处理 XML 格式回调
4
4
  */
5
-
6
5
  /**
7
6
  * 从 XML 密文中提取 Encrypt 字段
8
7
  */
9
- export function extractEncryptFromXml(xml: string): string {
8
+ export function extractEncryptFromXml(xml) {
10
9
  const match = /<Encrypt><!\[CDATA\[(.*?)\]\]><\/Encrypt>/s.exec(xml);
11
10
  if (!match?.[1]) {
12
11
  // 尝试不带 CDATA 的格式
@@ -18,11 +17,10 @@ export function extractEncryptFromXml(xml: string): string {
18
17
  }
19
18
  return match[1];
20
19
  }
21
-
22
20
  /**
23
21
  * 从 XML 中提取 ToUserName (CorpID)
24
22
  */
25
- export function extractToUserNameFromXml(xml: string): string {
23
+ export function extractToUserNameFromXml(xml) {
26
24
  const match = /<ToUserName><!\[CDATA\[(.*?)\]\]><\/ToUserName>/s.exec(xml);
27
25
  if (!match?.[1]) {
28
26
  const altMatch = /<ToUserName>(.*?)<\/ToUserName>/s.exec(xml);
@@ -30,16 +28,10 @@ export function extractToUserNameFromXml(xml: string): string {
30
28
  }
31
29
  return match[1];
32
30
  }
33
-
34
31
  /**
35
32
  * 构建加密 XML 响应
36
33
  */
37
- export function buildEncryptedXmlResponse(params: {
38
- encrypt: string;
39
- signature: string;
40
- timestamp: string;
41
- nonce: string;
42
- }): string {
34
+ export function buildEncryptedXmlResponse(params) {
43
35
  return `<xml>
44
36
  <Encrypt><![CDATA[${params.encrypt}]]></Encrypt>
45
37
  <MsgSignature><![CDATA[${params.signature}]]></MsgSignature>
@@ -0,0 +1,145 @@
1
+ import crypto from "node:crypto";
2
+ /**
3
+ * **decodeEncodingAESKey (解码 AES Key)**
4
+ *
5
+ * 将企业微信配置的 Base64 编码的 AES Key 解码为 Buffer。
6
+ * 包含补全 Padding 和长度校验 (必须32字节)。
7
+ */
8
+ export function decodeEncodingAESKey(encodingAESKey) {
9
+ const trimmed = encodingAESKey.trim();
10
+ if (!trimmed)
11
+ throw new Error("encodingAESKey missing");
12
+ const withPadding = trimmed.endsWith("=") ? trimmed : `${trimmed}=`;
13
+ const key = Buffer.from(withPadding, "base64");
14
+ if (key.length !== 32) {
15
+ throw new Error(`invalid encodingAESKey (expected 32 bytes after base64 decode, got ${key.length})`);
16
+ }
17
+ return key;
18
+ }
19
+ // WeCom uses PKCS#7 padding with a block size of 32 bytes (not AES's 16-byte block).
20
+ // This is compatible with AES-CBC as 32 is a multiple of 16, but it requires manual padding/unpadding.
21
+ export const WECOM_PKCS7_BLOCK_SIZE = 32;
22
+ function pkcs7Pad(buf, blockSize) {
23
+ const mod = buf.length % blockSize;
24
+ const pad = mod === 0 ? blockSize : blockSize - mod;
25
+ const padByte = Buffer.from([pad]);
26
+ return Buffer.concat([buf, Buffer.alloc(pad, padByte[0])]);
27
+ }
28
+ /**
29
+ * **pkcs7Unpad (去除 PKCS#7 填充)**
30
+ *
31
+ * 移除 AES 解密后的 PKCS#7 填充字节。
32
+ * 包含填充合法性校验。
33
+ */
34
+ export function pkcs7Unpad(buf, blockSize) {
35
+ if (buf.length === 0)
36
+ throw new Error("invalid pkcs7 payload");
37
+ const pad = buf[buf.length - 1];
38
+ if (pad < 1 || pad > blockSize) {
39
+ throw new Error("invalid pkcs7 padding");
40
+ }
41
+ if (pad > buf.length) {
42
+ throw new Error("invalid pkcs7 payload");
43
+ }
44
+ // Best-effort validation (all padding bytes equal).
45
+ for (let i = 0; i < pad; i += 1) {
46
+ if (buf[buf.length - 1 - i] !== pad) {
47
+ throw new Error("invalid pkcs7 padding");
48
+ }
49
+ }
50
+ return buf.subarray(0, buf.length - pad);
51
+ }
52
+ function sha1Hex(input) {
53
+ return crypto.createHash("sha1").update(input).digest("hex");
54
+ }
55
+ /**
56
+ * **computeWecomMsgSignature (计算消息签名)**
57
+ *
58
+ * 算法:sha1(sort(token, timestamp, nonce, encrypt_msg))
59
+ */
60
+ export function computeWecomMsgSignature(params) {
61
+ const parts = [params.token, params.timestamp, params.nonce, params.encrypt]
62
+ .map((v) => String(v ?? ""))
63
+ .sort();
64
+ return sha1Hex(parts.join(""));
65
+ }
66
+ /**
67
+ * **verifyWecomSignature (验证消息签名)**
68
+ *
69
+ * 比较计算出的签名与企业微信传入的签名是否一致。
70
+ */
71
+ export function verifyWecomSignature(params) {
72
+ const expected = computeWecomMsgSignature({
73
+ token: params.token,
74
+ timestamp: params.timestamp,
75
+ nonce: params.nonce,
76
+ encrypt: params.encrypt,
77
+ });
78
+ return expected === params.signature;
79
+ }
80
+ /**
81
+ * **decryptWecomEncrypted (解密企业微信消息)**
82
+ *
83
+ * 将企业微信的 AES 加密包解密为明文。
84
+ * 流程:
85
+ * 1. Base64 解码 AESKey 并获取 IV (前16字节)。
86
+ * 2. AES-CBC 解密。
87
+ * 3. 去除 PKCS#7 填充。
88
+ * 4. 拆解协议包结构: [16字节随机串][4字节长度][消息体][接收者ID]。
89
+ * 5. 校验接收者ID (ReceiveId)。
90
+ */
91
+ export function decryptWecomEncrypted(params) {
92
+ const aesKey = decodeEncodingAESKey(params.encodingAESKey);
93
+ const iv = aesKey.subarray(0, 16);
94
+ const decipher = crypto.createDecipheriv("aes-256-cbc", aesKey, iv);
95
+ decipher.setAutoPadding(false);
96
+ const decryptedPadded = Buffer.concat([
97
+ decipher.update(Buffer.from(params.encrypt, "base64")),
98
+ decipher.final(),
99
+ ]);
100
+ const decrypted = pkcs7Unpad(decryptedPadded, WECOM_PKCS7_BLOCK_SIZE);
101
+ if (decrypted.length < 20) {
102
+ throw new Error(`invalid decrypted payload (expected at least 20 bytes, got ${decrypted.length})`);
103
+ }
104
+ // 16 bytes random + 4 bytes network-order length + msg + receiveId (optional)
105
+ const msgLen = decrypted.readUInt32BE(16);
106
+ const msgStart = 20;
107
+ const msgEnd = msgStart + msgLen;
108
+ if (msgEnd > decrypted.length) {
109
+ throw new Error(`invalid decrypted msg length (msgEnd=${msgEnd}, payloadLength=${decrypted.length})`);
110
+ }
111
+ const msg = decrypted.subarray(msgStart, msgEnd).toString("utf8");
112
+ const receiveId = params.receiveId ?? "";
113
+ if (receiveId) {
114
+ const trailing = decrypted.subarray(msgEnd).toString("utf8");
115
+ if (trailing !== receiveId) {
116
+ throw new Error(`receiveId mismatch (expected "${receiveId}", got "${trailing}")`);
117
+ }
118
+ }
119
+ return msg;
120
+ }
121
+ /**
122
+ * **encryptWecomPlaintext (加密回复消息)**
123
+ *
124
+ * 将明文消息打包为企业微信的加密格式。
125
+ * 流程:
126
+ * 1. 构造协议包: [16字节随机串][4字节长度][消息体][接收者ID]。
127
+ * 2. PKCS#7 填充。
128
+ * 3. AES-CBC 加密。
129
+ * 4. 转 Base64。
130
+ */
131
+ export function encryptWecomPlaintext(params) {
132
+ const aesKey = decodeEncodingAESKey(params.encodingAESKey);
133
+ const iv = aesKey.subarray(0, 16);
134
+ const random16 = crypto.randomBytes(16);
135
+ const msg = Buffer.from(params.plaintext ?? "", "utf8");
136
+ const msgLen = Buffer.alloc(4);
137
+ msgLen.writeUInt32BE(msg.length, 0);
138
+ const receiveId = Buffer.from(params.receiveId ?? "", "utf8");
139
+ const raw = Buffer.concat([random16, msgLen, msg, receiveId]);
140
+ const padded = pkcs7Pad(raw, WECOM_PKCS7_BLOCK_SIZE);
141
+ const cipher = crypto.createCipheriv("aes-256-cbc", aesKey, iv);
142
+ cipher.setAutoPadding(false);
143
+ const encrypted = Buffer.concat([cipher.update(padded), cipher.final()]);
144
+ return encrypted.toString("base64");
145
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,32 @@
1
+ export function assertBotPrimaryTransport(account) {
2
+ if (account.primaryTransport === "ws" && !account.wsConfigured) {
3
+ throw new Error(`WeCom bot account "${account.accountId}" is missing bot.ws credentials.`);
4
+ }
5
+ if (account.primaryTransport === "webhook" && !account.webhookConfigured) {
6
+ throw new Error(`WeCom bot account "${account.accountId}" is missing bot.webhook credentials.`);
7
+ }
8
+ }
9
+ export function buildDedupKey(event) {
10
+ return `${event.accountId}:${event.transport}:${event.messageId}`;
11
+ }
12
+ export function resolveConversationKey(event) {
13
+ const conversation = event.conversation;
14
+ return [event.accountId, conversation.peerKind, conversation.peerId, conversation.senderId].join(":");
15
+ }
16
+ export function normalizeWecomAllowFromEntry(raw) {
17
+ return raw
18
+ .trim()
19
+ .toLowerCase()
20
+ .replace(/^wecom:/, "")
21
+ .replace(/^user:/, "")
22
+ .replace(/^userid:/, "");
23
+ }
24
+ export function isWecomSenderAllowed(senderUserId, allowFrom) {
25
+ const list = allowFrom.map((entry) => normalizeWecomAllowFromEntry(entry)).filter(Boolean);
26
+ if (list.includes("*"))
27
+ return true;
28
+ const normalizedSender = normalizeWecomAllowFromEntry(senderUserId);
29
+ if (!normalizedSender)
30
+ return false;
31
+ return list.includes(normalizedSender);
32
+ }