@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,496 @@
1
+ import { decryptWecomMediaWithMeta } from "../../media.js";
2
+ import { resolveWecomEgressProxyUrl, resolveWecomMediaDownloadTimeoutMs, resolveWecomMediaMaxBytes, } from "../../config/index.js";
3
+ import { buildInboundBody } from "./message-shape.js";
4
+ const MIME_BY_EXT = {
5
+ txt: "text/plain",
6
+ md: "text/markdown",
7
+ json: "application/json",
8
+ csv: "text/csv",
9
+ html: "text/html",
10
+ pdf: "application/pdf",
11
+ doc: "application/msword",
12
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
13
+ xls: "application/vnd.ms-excel",
14
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
15
+ ppt: "application/vnd.ms-powerpoint",
16
+ pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
17
+ jpg: "image/jpeg",
18
+ jpeg: "image/jpeg",
19
+ png: "image/png",
20
+ gif: "image/gif",
21
+ webp: "image/webp",
22
+ bmp: "image/bmp",
23
+ ogg: "audio/ogg",
24
+ wav: "audio/wav",
25
+ mp3: "audio/mpeg",
26
+ mp4: "video/mp4",
27
+ zip: "application/zip",
28
+ bin: "application/octet-stream",
29
+ };
30
+ const EXT_BY_MIME = {
31
+ ...Object.fromEntries(Object.entries(MIME_BY_EXT).map(([ext, mime]) => [mime, ext])),
32
+ "application/octet-stream": "bin",
33
+ };
34
+ const GENERIC_CONTENT_TYPES = new Set([
35
+ "application/octet-stream",
36
+ "binary/octet-stream",
37
+ "application/download",
38
+ ]);
39
+ function normalizeContentType(raw) {
40
+ const normalized = String(raw ?? "").trim().split(";")[0]?.trim().toLowerCase();
41
+ return normalized || undefined;
42
+ }
43
+ function isGenericContentType(raw) {
44
+ const normalized = normalizeContentType(raw);
45
+ if (!normalized)
46
+ return true;
47
+ return GENERIC_CONTENT_TYPES.has(normalized);
48
+ }
49
+ export function guessContentTypeFromPath(filePath) {
50
+ const ext = filePath.split(".").pop()?.toLowerCase();
51
+ if (!ext)
52
+ return undefined;
53
+ return MIME_BY_EXT[ext];
54
+ }
55
+ function guessExtensionFromContentType(contentType) {
56
+ const normalized = normalizeContentType(contentType);
57
+ if (!normalized)
58
+ return undefined;
59
+ if (normalized === "image/jpeg")
60
+ return "jpg";
61
+ return EXT_BY_MIME[normalized];
62
+ }
63
+ function extractFileNameFromUrl(rawUrl) {
64
+ const s = String(rawUrl ?? "").trim();
65
+ if (!s)
66
+ return undefined;
67
+ try {
68
+ const u = new URL(s);
69
+ const name = decodeURIComponent(u.pathname.split("/").pop() ?? "").trim();
70
+ return name || undefined;
71
+ }
72
+ catch {
73
+ return undefined;
74
+ }
75
+ }
76
+ function sanitizeInboundFilename(raw) {
77
+ const s = String(raw ?? "").trim();
78
+ if (!s)
79
+ return undefined;
80
+ const base = s.split(/[\\/]/).pop()?.trim() ?? "";
81
+ if (!base)
82
+ return undefined;
83
+ const sanitized = base.replace(/[\u0000-\u001f<>:"|?*]/g, "_").trim();
84
+ return sanitized || undefined;
85
+ }
86
+ function hasLikelyExtension(name) {
87
+ if (!name)
88
+ return false;
89
+ return /\.[a-z0-9]{1,16}$/i.test(name);
90
+ }
91
+ function detectMimeFromBuffer(buffer) {
92
+ if (!buffer || buffer.length < 4)
93
+ return undefined;
94
+ if (buffer.length >= 8 &&
95
+ buffer[0] === 0x89 &&
96
+ buffer[1] === 0x50 &&
97
+ buffer[2] === 0x4e &&
98
+ buffer[3] === 0x47 &&
99
+ buffer[4] === 0x0d &&
100
+ buffer[5] === 0x0a &&
101
+ buffer[6] === 0x1a &&
102
+ buffer[7] === 0x0a) {
103
+ return "image/png";
104
+ }
105
+ if (buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) {
106
+ return "image/jpeg";
107
+ }
108
+ if (buffer.subarray(0, 6).toString("ascii") === "GIF87a" || buffer.subarray(0, 6).toString("ascii") === "GIF89a") {
109
+ return "image/gif";
110
+ }
111
+ if (buffer.length >= 12 && buffer.subarray(0, 4).toString("ascii") === "RIFF" && buffer.subarray(8, 12).toString("ascii") === "WEBP") {
112
+ return "image/webp";
113
+ }
114
+ if (buffer[0] === 0x42 && buffer[1] === 0x4d) {
115
+ return "image/bmp";
116
+ }
117
+ if (buffer.subarray(0, 5).toString("ascii") === "%PDF-") {
118
+ return "application/pdf";
119
+ }
120
+ if (buffer.subarray(0, 4).toString("ascii") === "OggS") {
121
+ return "audio/ogg";
122
+ }
123
+ if (buffer.length >= 12 && buffer.subarray(0, 4).toString("ascii") === "RIFF" && buffer.subarray(8, 12).toString("ascii") === "WAVE") {
124
+ return "audio/wav";
125
+ }
126
+ if (buffer.subarray(0, 3).toString("ascii") === "ID3" || (buffer[0] === 0xff && (buffer[1] & 0xe0) === 0xe0)) {
127
+ return "audio/mpeg";
128
+ }
129
+ if (buffer.length >= 12 && buffer.subarray(4, 8).toString("ascii") === "ftyp") {
130
+ return "video/mp4";
131
+ }
132
+ if (buffer.length >= 8 &&
133
+ buffer[0] === 0xd0 &&
134
+ buffer[1] === 0xcf &&
135
+ buffer[2] === 0x11 &&
136
+ buffer[3] === 0xe0 &&
137
+ buffer[4] === 0xa1 &&
138
+ buffer[5] === 0xb1 &&
139
+ buffer[6] === 0x1a &&
140
+ buffer[7] === 0xe1) {
141
+ return "application/msword";
142
+ }
143
+ const zipMagic = (buffer[0] === 0x50 && buffer[1] === 0x4b && buffer[2] === 0x03 && buffer[3] === 0x04) ||
144
+ (buffer[0] === 0x50 && buffer[1] === 0x4b && buffer[2] === 0x05 && buffer[3] === 0x06) ||
145
+ (buffer[0] === 0x50 && buffer[1] === 0x4b && buffer[2] === 0x07 && buffer[3] === 0x08);
146
+ if (zipMagic) {
147
+ const probe = buffer.subarray(0, Math.min(buffer.length, 512 * 1024));
148
+ if (probe.includes(Buffer.from("word/")))
149
+ return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
150
+ if (probe.includes(Buffer.from("xl/")))
151
+ return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
152
+ if (probe.includes(Buffer.from("ppt/")))
153
+ return "application/vnd.openxmlformats-officedocument.presentationml.presentation";
154
+ return "application/zip";
155
+ }
156
+ const sample = buffer.subarray(0, Math.min(buffer.length, 4096));
157
+ let printable = 0;
158
+ for (const b of sample) {
159
+ if (b === 0x00)
160
+ return undefined;
161
+ if (b === 0x09 || b === 0x0a || b === 0x0d || (b >= 0x20 && b <= 0x7e)) {
162
+ printable += 1;
163
+ }
164
+ }
165
+ if (sample.length > 0 && printable / sample.length > 0.95) {
166
+ return "text/plain";
167
+ }
168
+ return undefined;
169
+ }
170
+ function resolveInlineFileName(input) {
171
+ return sanitizeInboundFilename(String(input ?? "").trim());
172
+ }
173
+ function pickBotFileName(msg, item) {
174
+ const fromItem = item
175
+ ? resolveInlineFileName(item?.filename ?? item?.file_name ?? item?.fileName ?? item?.name ?? item?.title)
176
+ : undefined;
177
+ if (fromItem)
178
+ return fromItem;
179
+ return resolveInlineFileName(msg?.file?.filename ??
180
+ msg?.file?.file_name ??
181
+ msg?.file?.fileName ??
182
+ msg?.file?.name ??
183
+ msg?.file?.title ??
184
+ msg?.filename ??
185
+ msg?.fileName ??
186
+ msg?.FileName);
187
+ }
188
+ function inferInboundMediaMeta(params) {
189
+ const headerType = normalizeContentType(params.sourceContentType);
190
+ const magicType = detectMimeFromBuffer(params.buffer);
191
+ const rawUrlName = sanitizeInboundFilename(extractFileNameFromUrl(params.sourceUrl));
192
+ const guessedByUrl = hasLikelyExtension(rawUrlName) ? rawUrlName : undefined;
193
+ const explicitName = sanitizeInboundFilename(params.explicitFilename);
194
+ const sourceName = sanitizeInboundFilename(params.sourceFilename);
195
+ const chosenName = explicitName || sourceName || guessedByUrl;
196
+ const typeByName = chosenName ? guessContentTypeFromPath(chosenName) : undefined;
197
+ let contentType;
198
+ if (params.kind === "image") {
199
+ if (magicType?.startsWith("image/"))
200
+ contentType = magicType;
201
+ else if (headerType?.startsWith("image/"))
202
+ contentType = headerType;
203
+ else if (typeByName?.startsWith("image/"))
204
+ contentType = typeByName;
205
+ else
206
+ contentType = "image/jpeg";
207
+ }
208
+ else {
209
+ contentType = magicType || (!isGenericContentType(headerType) ? headerType : undefined) || typeByName || "application/octet-stream";
210
+ }
211
+ const hasExt = Boolean(chosenName && /\.[a-z0-9]{1,16}$/i.test(chosenName));
212
+ const ext = guessExtensionFromContentType(contentType) || (params.kind === "image" ? "jpg" : "bin");
213
+ const filename = chosenName ? (hasExt ? chosenName : `${chosenName}.${ext}`) : `${params.kind}.${ext}`;
214
+ return { contentType, filename };
215
+ }
216
+ export function looksLikeSendLocalFileIntent(rawBody) {
217
+ const t = rawBody.trim();
218
+ if (!t)
219
+ return false;
220
+ return /(发送|发给|发到|转发|把.*发|把.*发送|帮我发|给我发)/.test(t);
221
+ }
222
+ /**
223
+ * 根据错误信息对媒体下载/解密失败进行分类。
224
+ * 用于区分不同的失败原因:URL 过期、网络超时、文件超大、解密异常。
225
+ */
226
+ function classifyMediaFailure(error) {
227
+ const message = String(error instanceof Error ? error.message : error).toLowerCase();
228
+ // 优先检查 URL 过期或禁止访问(403/401 或签名过期)
229
+ if (message.includes("403") ||
230
+ message.includes("forbidden") ||
231
+ message.includes("expired") ||
232
+ message.includes("signature") ||
233
+ message.includes("status=401")) {
234
+ return "expired_or_forbidden";
235
+ }
236
+ // 检查网络超时(5 分钟 URL 时效窗口内的超时属于此类)
237
+ if (message.includes("timeout") || message.includes("timed out") || message.includes("abort")) {
238
+ return "timeout";
239
+ }
240
+ // 检查文件大小超限
241
+ if (message.includes("maxbytes") ||
242
+ message.includes("exceed") ||
243
+ message.includes("too large") ||
244
+ message.includes("payload too large")) {
245
+ return "size_limit";
246
+ }
247
+ // 其他错误默认归类为解密失败
248
+ return "decrypt";
249
+ }
250
+ /**
251
+ * 从引用消息中选择并提取第一个可用的媒体候选。
252
+ *
253
+ * 优先级规则:
254
+ * 1. quote.image / quote.file / quote.video:单个媒体类型直接提取
255
+ * 2. quote.mixed:从多个 msg_item 中提取第一个 image
256
+ * 3. URI 过期约 5 分钟,必须尽快下载/解密
257
+ *
258
+ * @returns 包含 kind、url、aesKey、filename 的候选项,或 undefined
259
+ */
260
+ function resolveQuoteMediaCandidate(msg) {
261
+ const quote = msg?.quote;
262
+ const quoteType = String(quote?.msgtype ?? "").toLowerCase();
263
+ // 处理单个媒体类型:image、file、video
264
+ if (quoteType === "image" || quoteType === "file" || quoteType === "video") {
265
+ const kind = quoteType;
266
+ const url = String(quote?.[kind]?.url ?? "").trim();
267
+ if (!url)
268
+ return undefined;
269
+ return {
270
+ kind,
271
+ url,
272
+ aesKey: quote?.[kind]?.aeskey,
273
+ explicitFilename: pickBotFileName(msg, quote?.[kind]),
274
+ };
275
+ }
276
+ // 处理混合消息类型:从 msg_item 数组中提取第一个图片
277
+ if (quoteType === "mixed" && Array.isArray(quote?.mixed?.msg_item)) {
278
+ for (const item of quote.mixed.msg_item) {
279
+ const itemType = String(item?.msgtype ?? "").toLowerCase();
280
+ if (itemType !== "image") {
281
+ continue;
282
+ }
283
+ const url = String(item?.image?.url ?? "").trim();
284
+ if (!url) {
285
+ continue;
286
+ }
287
+ return {
288
+ kind: "image",
289
+ url,
290
+ aesKey: item?.image?.aeskey,
291
+ explicitFilename: pickBotFileName(msg, item?.image),
292
+ };
293
+ }
294
+ }
295
+ return undefined;
296
+ }
297
+ export async function processBotInboundMessage(params) {
298
+ const { target, msg, recordOperationalIssue } = params;
299
+ const msgtype = String(msg.msgtype ?? "").toLowerCase();
300
+ const aesKey = target.account.encodingAESKey;
301
+ const maxBytes = resolveWecomMediaMaxBytes(target.config, target.account.accountId);
302
+ const proxyUrl = resolveWecomEgressProxyUrl(target.config);
303
+ const mediaTimeoutMs = resolveWecomMediaDownloadTimeoutMs(target.config);
304
+ const handleMediaFailure = (payload) => {
305
+ const reason = classifyMediaFailure(payload.error);
306
+ const hint = reason === "timeout"
307
+ ? `可调大 channels.wecom.media.downloadTimeoutMs(当前=${mediaTimeoutMs}ms)例如:openclaw config set channels.wecom.media.downloadTimeoutMs 45000`
308
+ : `可调大 channels.wecom.mediaMaxMb(当前=${Math.round(maxBytes / (1024 * 1024))}MB)例如:openclaw config set channels.wecom.mediaMaxMb 50`;
309
+ target.runtime.error?.(`Failed to decrypt ${payload.scope} ${payload.kind}: ${String(payload.error)} reason=${reason}; ${hint}`);
310
+ recordOperationalIssue({
311
+ category: "media-decrypt-failed",
312
+ messageId: msg.msgid ? String(msg.msgid) : undefined,
313
+ summary: `${payload.scope} ${payload.kind} decrypt failed reason=${reason} url=${payload.url}`,
314
+ raw: { transport: "bot-webhook", envelopeType: "json", body: msg },
315
+ error: payload.error instanceof Error ? payload.error.message : String(payload.error),
316
+ });
317
+ const errorMessage = typeof payload.error === "object" && payload.error
318
+ ? `${payload.error.message}${payload.error.cause ? ` (cause: ${String(payload.error.cause)})` : ""}`
319
+ : String(payload.error);
320
+ return { body: `${payload.bodyFallback} (decryption failed: ${errorMessage})` };
321
+ };
322
+ const tryDecryptMedia = async (payload) => {
323
+ const urlHost = (() => { try {
324
+ return new URL(payload.url).hostname;
325
+ }
326
+ catch {
327
+ return "?";
328
+ } })();
329
+ const t0 = Date.now();
330
+ console.log(`[wecom-media] download-start kind=${payload.kind} host=${urlHost} aesKey=${payload.aesKey ? "payload" : "account"} timeoutMs=${mediaTimeoutMs} proxy=${proxyUrl || "none"}`);
331
+ const decrypted = await decryptWecomMediaWithMeta(payload.url, payload.aesKey ?? aesKey, {
332
+ maxBytes,
333
+ http: { proxyUrl, timeoutMs: mediaTimeoutMs },
334
+ });
335
+ console.log(`[wecom-media] download-ok kind=${payload.kind} host=${urlHost} durationMs=${Date.now() - t0} bytes=${decrypted.buffer.length} contentType=${decrypted.sourceContentType ?? "?"}`);
336
+ const inferred = inferInboundMediaMeta({
337
+ kind: payload.kind === "image" ? "image" : "file",
338
+ buffer: decrypted.buffer,
339
+ sourceUrl: decrypted.sourceUrl || payload.url,
340
+ sourceContentType: decrypted.sourceContentType,
341
+ sourceFilename: decrypted.sourceFilename,
342
+ explicitFilename: payload.explicitFilename,
343
+ });
344
+ return {
345
+ buffer: decrypted.buffer,
346
+ contentType: inferred.contentType,
347
+ filename: inferred.filename,
348
+ };
349
+ };
350
+ if (msgtype === "image") {
351
+ const url = String(msg.image?.url ?? "").trim();
352
+ if (url && aesKey) {
353
+ try {
354
+ const media = await tryDecryptMedia({
355
+ kind: "image",
356
+ url,
357
+ explicitFilename: pickBotFileName(msg),
358
+ aesKey: msg.image?.aeskey,
359
+ });
360
+ return { body: "[image]", media };
361
+ }
362
+ catch (err) {
363
+ return handleMediaFailure({
364
+ scope: "inbound",
365
+ kind: "image",
366
+ url,
367
+ error: err,
368
+ bodyFallback: "[image]",
369
+ });
370
+ }
371
+ }
372
+ }
373
+ if (msgtype === "file") {
374
+ const url = String(msg.file?.url ?? "").trim();
375
+ if (url && aesKey) {
376
+ try {
377
+ const media = await tryDecryptMedia({
378
+ kind: "file",
379
+ url,
380
+ explicitFilename: pickBotFileName(msg),
381
+ aesKey: msg.file?.aeskey,
382
+ });
383
+ return { body: "[file]", media };
384
+ }
385
+ catch (err) {
386
+ return handleMediaFailure({
387
+ scope: "inbound",
388
+ kind: "file",
389
+ url,
390
+ error: err,
391
+ bodyFallback: "[file]",
392
+ });
393
+ }
394
+ }
395
+ }
396
+ if (msgtype === "video") {
397
+ const url = String(msg.video?.url ?? "").trim();
398
+ if (url && aesKey) {
399
+ try {
400
+ const media = await tryDecryptMedia({
401
+ kind: "video",
402
+ url,
403
+ explicitFilename: pickBotFileName(msg),
404
+ aesKey: msg.video?.aeskey,
405
+ });
406
+ return { body: "[video]", media };
407
+ }
408
+ catch (err) {
409
+ return handleMediaFailure({
410
+ scope: "inbound",
411
+ kind: "video",
412
+ url,
413
+ error: err,
414
+ bodyFallback: "[video]",
415
+ });
416
+ }
417
+ }
418
+ }
419
+ if (msgtype === "mixed") {
420
+ const items = msg.mixed?.msg_item;
421
+ if (Array.isArray(items)) {
422
+ let foundMedia;
423
+ const bodyParts = [];
424
+ for (const item of items) {
425
+ const t = String(item.msgtype ?? "").toLowerCase();
426
+ if (t === "text") {
427
+ const content = String(item.text?.content ?? "").trim();
428
+ if (content)
429
+ bodyParts.push(content);
430
+ continue;
431
+ }
432
+ if ((t === "image" || t === "file" || t === "video") && !foundMedia && aesKey) {
433
+ const mediaKind = t;
434
+ const url = String(item[mediaKind]?.url ?? "").trim();
435
+ if (url) {
436
+ try {
437
+ foundMedia = await tryDecryptMedia({
438
+ kind: mediaKind,
439
+ url,
440
+ explicitFilename: pickBotFileName(msg, item?.[mediaKind]),
441
+ aesKey: item?.[mediaKind]?.aeskey,
442
+ });
443
+ bodyParts.push(`[${t}]`);
444
+ continue;
445
+ }
446
+ catch (err) {
447
+ const failed = handleMediaFailure({
448
+ scope: "mixed",
449
+ kind: mediaKind,
450
+ url,
451
+ error: err,
452
+ bodyFallback: `[${t}]`,
453
+ });
454
+ bodyParts.push(failed.body);
455
+ continue;
456
+ }
457
+ }
458
+ }
459
+ bodyParts.push(`[${t}]`);
460
+ }
461
+ return { body: bodyParts.join("\n"), media: foundMedia };
462
+ }
463
+ }
464
+ if (msgtype === "text" || msgtype === "voice") {
465
+ const baseBody = buildInboundBody(msg);
466
+ // 新增支持:尝试从引用中提取候选媒体(支持 quote.image/file/video/mixed)
467
+ // 优先级:顶层媒体已在上面处理,如果没有顶层媒体才检查引用
468
+ const candidate = resolveQuoteMediaCandidate(msg);
469
+ if (candidate?.url && aesKey) {
470
+ const qHost = (() => { try {
471
+ return new URL(candidate.url).hostname;
472
+ }
473
+ catch {
474
+ return "?";
475
+ } })();
476
+ console.log(`[wecom-media] quote-candidate msgtype=${msgtype} kind=${candidate.kind} host=${qHost} aesKey=${candidate.aesKey ? "payload" : "account"} msgid=${msg.msgid ?? "?"}`);
477
+ try {
478
+ // 尽快下载并解密媒体(应对 5 分钟 URL 时效窗口)
479
+ const media = await tryDecryptMedia(candidate);
480
+ return { body: baseBody, media };
481
+ }
482
+ catch (err) {
483
+ // 下载或解密失败则降级,保留文本但记录失败原因便于调试
484
+ const failed = handleMediaFailure({
485
+ scope: "quote",
486
+ kind: candidate.kind,
487
+ url: candidate.url,
488
+ error: err,
489
+ bodyFallback: `${baseBody}\n[quote:${candidate.kind}]`,
490
+ });
491
+ return failed;
492
+ }
493
+ }
494
+ }
495
+ return { body: buildInboundBody(msg) };
496
+ }
@@ -0,0 +1,4 @@
1
+ import { resolveDerivedPathSummary } from "../../config/index.js";
2
+ export function resolveBotWebhookPaths(accountId) {
3
+ return resolveDerivedPathSummary(accountId).botWebhook;
4
+ }
@@ -0,0 +1,98 @@
1
+ export function resolveWecomSenderUserId(msg) {
2
+ const direct = msg.from?.userid?.trim();
3
+ if (direct)
4
+ return direct;
5
+ const legacy = String(msg.fromuserid ?? msg.from_userid ?? msg.fromUserId ?? "").trim();
6
+ return legacy || undefined;
7
+ }
8
+ export function shouldProcessBotInboundMessage(msg) {
9
+ const senderUserId = resolveWecomSenderUserId(msg)?.trim();
10
+ if (!senderUserId) {
11
+ return { shouldProcess: false, reason: "missing_sender" };
12
+ }
13
+ if (senderUserId.toLowerCase() === "sys") {
14
+ return { shouldProcess: false, reason: "system_sender" };
15
+ }
16
+ const chatType = String(msg.chattype ?? "").trim().toLowerCase();
17
+ if (chatType === "group") {
18
+ const chatId = msg.chatid?.trim();
19
+ if (!chatId) {
20
+ return { shouldProcess: false, reason: "missing_chatid", senderUserId };
21
+ }
22
+ return { shouldProcess: true, reason: "user_message", senderUserId, chatId };
23
+ }
24
+ return { shouldProcess: true, reason: "user_message", senderUserId, chatId: senderUserId };
25
+ }
26
+ function formatQuote(quote) {
27
+ const type = quote.msgtype ?? "";
28
+ if (type === "text")
29
+ return quote.text?.content || "";
30
+ if (type === "image")
31
+ return `[引用: 图片] ${quote.image?.url || ""}`;
32
+ if (type === "mixed" && quote.mixed?.msg_item) {
33
+ const items = quote.mixed.msg_item
34
+ .map((item) => {
35
+ if (item.msgtype === "text")
36
+ return item.text?.content;
37
+ if (item.msgtype === "image")
38
+ return `[图片] ${item.image?.url || ""}`;
39
+ return "";
40
+ })
41
+ .filter(Boolean)
42
+ .join(" ");
43
+ return `[引用: 图文] ${items}`;
44
+ }
45
+ if (type === "voice")
46
+ return `[引用: 语音] ${quote.voice?.content || ""}`;
47
+ if (type === "file")
48
+ return `[引用: 文件] ${quote.file?.url || ""}`;
49
+ // 新增支持:引用视频类型 - 将在入站正规化中提取媒体并落盘
50
+ if (type === "video")
51
+ return `[引用: 视频] ${quote.video?.url || ""}`;
52
+ return "";
53
+ }
54
+ export function buildInboundBody(msg) {
55
+ let body = "";
56
+ const msgtype = String(msg.msgtype ?? "").toLowerCase();
57
+ if (msgtype === "text")
58
+ body = msg.text?.content || "";
59
+ else if (msgtype === "voice")
60
+ body = msg.voice?.content || "[voice]";
61
+ else if (msgtype === "mixed") {
62
+ const items = msg.mixed?.msg_item;
63
+ if (Array.isArray(items)) {
64
+ body = items
65
+ .map((item) => {
66
+ const t = String(item?.msgtype ?? "").toLowerCase();
67
+ if (t === "text")
68
+ return item?.text?.content || "";
69
+ if (t === "image")
70
+ return `[image] ${item?.image?.url || ""}`;
71
+ return `[${t || "item"}]`;
72
+ })
73
+ .filter(Boolean)
74
+ .join("\n");
75
+ }
76
+ else
77
+ body = "[mixed]";
78
+ }
79
+ else if (msgtype === "image")
80
+ body = `[image] ${msg.image?.url || ""}`;
81
+ else if (msgtype === "file")
82
+ body = `[file] ${msg.file?.url || ""}`;
83
+ else if (msgtype === "video")
84
+ body = `[video] ${msg.video?.url || ""}`;
85
+ else if (msgtype === "event")
86
+ body = `[event] ${msg.event?.eventtype || ""}`;
87
+ else if (msgtype === "stream")
88
+ body = `[stream_refresh] ${msg.stream?.id || ""}`;
89
+ else
90
+ body = msgtype ? `[${msgtype}]` : "";
91
+ const quote = msg.quote;
92
+ if (quote) {
93
+ const quoteText = formatQuote(quote).trim();
94
+ if (quoteText)
95
+ body += `\n\n> ${quoteText}`;
96
+ }
97
+ return body;
98
+ }