@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,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
+ }