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