@wecom/wecom-openclaw-plugin 2026.4.2 → 2026.4.3-beta.0

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 (215) hide show
  1. package/README.md +395 -10
  2. package/dist/index.d.ts +2 -4
  3. package/dist/index.js +58 -0
  4. package/dist/src/accounts.d.ts +57 -0
  5. package/dist/src/accounts.js +222 -0
  6. package/dist/src/agent/api-client.d.ts +94 -0
  7. package/dist/src/agent/api-client.js +292 -0
  8. package/dist/src/agent/handler.d.ts +63 -0
  9. package/dist/src/agent/handler.js +551 -0
  10. package/dist/src/agent/index.d.ts +5 -0
  11. package/dist/src/agent/index.js +5 -0
  12. package/dist/src/agent/webhook.d.ts +24 -0
  13. package/dist/src/agent/webhook.js +228 -0
  14. package/dist/src/agent/xml.d.ts +21 -0
  15. package/dist/src/agent/xml.js +41 -0
  16. package/dist/src/channel.js +579 -0
  17. package/dist/src/chat-queue.d.ts +31 -0
  18. package/dist/src/chat-queue.js +45 -0
  19. package/dist/{esm/types/src → src}/const.d.ts +46 -0
  20. package/dist/{esm/src → src}/const.js +77 -30
  21. package/dist/{esm/src → src}/dm-policy.js +6 -9
  22. package/dist/src/dynamic-agent.d.ts +37 -0
  23. package/dist/src/dynamic-agent.js +60 -0
  24. package/dist/src/dynamic-routing.d.ts +65 -0
  25. package/dist/src/dynamic-routing.js +58 -0
  26. package/dist/{esm/src → src}/group-policy.js +8 -22
  27. package/dist/src/http.d.ts +26 -0
  28. package/dist/src/http.js +93 -0
  29. package/dist/{esm/types/src → src}/interface.d.ts +0 -2
  30. package/dist/src/interface.js +4 -0
  31. package/dist/src/mcp/index.js +6 -0
  32. package/dist/{esm/src → src}/mcp/interceptors/biz-error.js +2 -6
  33. package/dist/{esm/src → src}/mcp/interceptors/index.js +6 -10
  34. package/dist/{esm/src → src}/mcp/interceptors/msg-media.js +6 -10
  35. package/dist/{esm/src → src}/mcp/interceptors/smartpage-create.js +2 -6
  36. package/dist/{esm/src → src}/mcp/interceptors/smartpage-export.js +2 -6
  37. package/dist/src/mcp/interceptors/types.js +7 -0
  38. package/dist/{esm/src → src}/mcp/schema.js +1 -4
  39. package/dist/{esm/src → src}/mcp/tool.js +4 -8
  40. package/dist/{esm/src → src}/mcp/transport.js +15 -16
  41. package/dist/{esm/src → src}/media-handler.js +6 -10
  42. package/dist/{esm/src → src}/media-uploader.js +64 -10
  43. package/dist/{esm/src → src}/message-parser.js +1 -4
  44. package/dist/{esm/types/src → src}/message-sender.d.ts +18 -0
  45. package/dist/{esm/src → src}/message-sender.js +45 -11
  46. package/dist/{esm/src → src}/monitor.js +145 -292
  47. package/dist/{esm/src → src}/onboarding.js +22 -26
  48. package/dist/{esm/types/src → src}/openclaw-compat.d.ts +49 -0
  49. package/dist/{esm/src → src}/openclaw-compat.js +133 -27
  50. package/dist/{esm/src → src}/reqid-store.js +4 -7
  51. package/dist/{esm/src → src}/runtime.js +2 -5
  52. package/dist/src/shared/command-auth.d.ts +23 -0
  53. package/dist/src/shared/command-auth.js +73 -0
  54. package/dist/src/shared/xml-parser.d.ts +46 -0
  55. package/dist/src/shared/xml-parser.js +210 -0
  56. package/dist/{esm/src → src}/state-dir-resolve.js +3 -7
  57. package/dist/{esm/src → src}/state-manager.js +84 -16
  58. package/dist/src/target.d.ts +35 -0
  59. package/dist/src/target.js +67 -0
  60. package/dist/src/template-card-manager.d.ts +55 -0
  61. package/dist/src/template-card-manager.js +205 -0
  62. package/dist/{esm/src → src}/template-card-parser.js +17 -8
  63. package/dist/{esm/src → src}/timeout.js +2 -5
  64. package/dist/src/types/account.d.ts +29 -0
  65. package/dist/src/types/account.js +4 -0
  66. package/dist/src/types/config.d.ts +91 -0
  67. package/dist/src/types/config.js +7 -0
  68. package/dist/src/types/constants.d.ts +42 -0
  69. package/dist/src/types/constants.js +42 -0
  70. package/dist/src/types/index.d.ts +7 -0
  71. package/dist/src/types/index.js +5 -0
  72. package/dist/src/types/message.d.ts +238 -0
  73. package/dist/src/types/message.js +5 -0
  74. package/dist/{esm/types/src → src}/utils.d.ts +41 -0
  75. package/dist/{esm/src → src}/utils.js +24 -10
  76. package/dist/src/version.d.ts +2 -0
  77. package/dist/src/version.js +24 -0
  78. package/dist/src/webhook/command-auth.d.ts +47 -0
  79. package/dist/src/webhook/command-auth.js +96 -0
  80. package/dist/src/webhook/gateway.d.ts +36 -0
  81. package/dist/src/webhook/gateway.js +235 -0
  82. package/dist/src/webhook/handler.d.ts +18 -0
  83. package/dist/src/webhook/handler.js +420 -0
  84. package/dist/src/webhook/helpers.d.ts +156 -0
  85. package/dist/src/webhook/helpers.js +833 -0
  86. package/dist/src/webhook/http.d.ts +26 -0
  87. package/dist/src/webhook/http.js +94 -0
  88. package/dist/src/webhook/index.d.ts +11 -0
  89. package/dist/src/webhook/index.js +14 -0
  90. package/dist/src/webhook/media.d.ts +29 -0
  91. package/dist/src/webhook/media.js +88 -0
  92. package/dist/src/webhook/monitor.d.ts +59 -0
  93. package/dist/src/webhook/monitor.js +1161 -0
  94. package/dist/src/webhook/state.d.ts +220 -0
  95. package/dist/src/webhook/state.js +491 -0
  96. package/dist/src/webhook/target.d.ts +41 -0
  97. package/dist/src/webhook/target.js +132 -0
  98. package/dist/src/webhook/types.d.ts +347 -0
  99. package/dist/src/webhook/types.js +33 -0
  100. package/dist/src/webhook/video-frame.d.ts +13 -0
  101. package/dist/src/webhook/video-frame.js +30 -0
  102. package/package.json +15 -7
  103. package/dist/cjs/index.js +0 -45
  104. package/dist/cjs/index.js.map +0 -1
  105. package/dist/cjs/src/channel.js +0 -339
  106. package/dist/cjs/src/channel.js.map +0 -1
  107. package/dist/cjs/src/const.js +0 -137
  108. package/dist/cjs/src/const.js.map +0 -1
  109. package/dist/cjs/src/dm-policy.js +0 -89
  110. package/dist/cjs/src/dm-policy.js.map +0 -1
  111. package/dist/cjs/src/group-policy.js +0 -133
  112. package/dist/cjs/src/group-policy.js.map +0 -1
  113. package/dist/cjs/src/mcp/interceptors/biz-error.js +0 -70
  114. package/dist/cjs/src/mcp/interceptors/biz-error.js.map +0 -1
  115. package/dist/cjs/src/mcp/interceptors/index.js +0 -73
  116. package/dist/cjs/src/mcp/interceptors/index.js.map +0 -1
  117. package/dist/cjs/src/mcp/interceptors/msg-media.js +0 -159
  118. package/dist/cjs/src/mcp/interceptors/msg-media.js.map +0 -1
  119. package/dist/cjs/src/mcp/interceptors/smartpage-create.js +0 -157
  120. package/dist/cjs/src/mcp/interceptors/smartpage-create.js.map +0 -1
  121. package/dist/cjs/src/mcp/interceptors/smartpage-export.js +0 -93
  122. package/dist/cjs/src/mcp/interceptors/smartpage-export.js.map +0 -1
  123. package/dist/cjs/src/mcp/schema.js +0 -104
  124. package/dist/cjs/src/mcp/schema.js.map +0 -1
  125. package/dist/cjs/src/mcp/tool.js +0 -208
  126. package/dist/cjs/src/mcp/tool.js.map +0 -1
  127. package/dist/cjs/src/mcp/transport.js +0 -417
  128. package/dist/cjs/src/mcp/transport.js.map +0 -1
  129. package/dist/cjs/src/media-handler.js +0 -129
  130. package/dist/cjs/src/media-handler.js.map +0 -1
  131. package/dist/cjs/src/media-uploader.js +0 -284
  132. package/dist/cjs/src/media-uploader.js.map +0 -1
  133. package/dist/cjs/src/message-parser.js +0 -134
  134. package/dist/cjs/src/message-parser.js.map +0 -1
  135. package/dist/cjs/src/message-sender.js +0 -89
  136. package/dist/cjs/src/message-sender.js.map +0 -1
  137. package/dist/cjs/src/monitor.js +0 -991
  138. package/dist/cjs/src/monitor.js.map +0 -1
  139. package/dist/cjs/src/onboarding.js +0 -182
  140. package/dist/cjs/src/onboarding.js.map +0 -1
  141. package/dist/cjs/src/openclaw-compat.js +0 -400
  142. package/dist/cjs/src/openclaw-compat.js.map +0 -1
  143. package/dist/cjs/src/reqid-store.js +0 -88
  144. package/dist/cjs/src/reqid-store.js.map +0 -1
  145. package/dist/cjs/src/runtime.js +0 -16
  146. package/dist/cjs/src/runtime.js.map +0 -1
  147. package/dist/cjs/src/state-dir-resolve.js +0 -35
  148. package/dist/cjs/src/state-dir-resolve.js.map +0 -1
  149. package/dist/cjs/src/state-manager.js +0 -158
  150. package/dist/cjs/src/state-manager.js.map +0 -1
  151. package/dist/cjs/src/template-card-parser.js +0 -641
  152. package/dist/cjs/src/template-card-parser.js.map +0 -1
  153. package/dist/cjs/src/timeout.js +0 -42
  154. package/dist/cjs/src/timeout.js.map +0 -1
  155. package/dist/cjs/src/utils.js +0 -60
  156. package/dist/cjs/src/utils.js.map +0 -1
  157. package/dist/cjs/src/version.js +0 -26
  158. package/dist/cjs/src/version.js.map +0 -1
  159. package/dist/esm/index.js +0 -41
  160. package/dist/esm/index.js.map +0 -1
  161. package/dist/esm/src/channel.js +0 -337
  162. package/dist/esm/src/channel.js.map +0 -1
  163. package/dist/esm/src/const.js.map +0 -1
  164. package/dist/esm/src/dm-policy.js.map +0 -1
  165. package/dist/esm/src/group-policy.js.map +0 -1
  166. package/dist/esm/src/mcp/interceptors/biz-error.js.map +0 -1
  167. package/dist/esm/src/mcp/interceptors/index.js.map +0 -1
  168. package/dist/esm/src/mcp/interceptors/msg-media.js.map +0 -1
  169. package/dist/esm/src/mcp/interceptors/smartpage-create.js.map +0 -1
  170. package/dist/esm/src/mcp/interceptors/smartpage-export.js.map +0 -1
  171. package/dist/esm/src/mcp/schema.js.map +0 -1
  172. package/dist/esm/src/mcp/tool.js.map +0 -1
  173. package/dist/esm/src/mcp/transport.js.map +0 -1
  174. package/dist/esm/src/media-handler.js.map +0 -1
  175. package/dist/esm/src/media-uploader.js.map +0 -1
  176. package/dist/esm/src/message-parser.js.map +0 -1
  177. package/dist/esm/src/message-sender.js.map +0 -1
  178. package/dist/esm/src/monitor.js.map +0 -1
  179. package/dist/esm/src/onboarding.js.map +0 -1
  180. package/dist/esm/src/openclaw-compat.js.map +0 -1
  181. package/dist/esm/src/reqid-store.js.map +0 -1
  182. package/dist/esm/src/runtime.js.map +0 -1
  183. package/dist/esm/src/state-dir-resolve.js.map +0 -1
  184. package/dist/esm/src/state-manager.js.map +0 -1
  185. package/dist/esm/src/template-card-parser.js.map +0 -1
  186. package/dist/esm/src/timeout.js.map +0 -1
  187. package/dist/esm/src/utils.js.map +0 -1
  188. package/dist/esm/src/version.js +0 -23
  189. package/dist/esm/src/version.js.map +0 -1
  190. package/dist/esm/types/index.d.ts +0 -9
  191. package/dist/esm/types/src/version.d.ts +0 -2
  192. /package/dist/{esm/types/src → src}/channel.d.ts +0 -0
  193. /package/dist/{esm/types/src → src}/dm-policy.d.ts +0 -0
  194. /package/dist/{esm/types/src → src}/group-policy.d.ts +0 -0
  195. /package/dist/{esm/types/src → src}/mcp/index.d.ts +0 -0
  196. /package/dist/{esm/types/src → src}/mcp/interceptors/biz-error.d.ts +0 -0
  197. /package/dist/{esm/types/src → src}/mcp/interceptors/index.d.ts +0 -0
  198. /package/dist/{esm/types/src → src}/mcp/interceptors/msg-media.d.ts +0 -0
  199. /package/dist/{esm/types/src → src}/mcp/interceptors/smartpage-create.d.ts +0 -0
  200. /package/dist/{esm/types/src → src}/mcp/interceptors/smartpage-export.d.ts +0 -0
  201. /package/dist/{esm/types/src → src}/mcp/interceptors/types.d.ts +0 -0
  202. /package/dist/{esm/types/src → src}/mcp/schema.d.ts +0 -0
  203. /package/dist/{esm/types/src → src}/mcp/tool.d.ts +0 -0
  204. /package/dist/{esm/types/src → src}/mcp/transport.d.ts +0 -0
  205. /package/dist/{esm/types/src → src}/media-handler.d.ts +0 -0
  206. /package/dist/{esm/types/src → src}/media-uploader.d.ts +0 -0
  207. /package/dist/{esm/types/src → src}/message-parser.d.ts +0 -0
  208. /package/dist/{esm/types/src → src}/monitor.d.ts +0 -0
  209. /package/dist/{esm/types/src → src}/onboarding.d.ts +0 -0
  210. /package/dist/{esm/types/src → src}/reqid-store.d.ts +0 -0
  211. /package/dist/{esm/types/src → src}/runtime.d.ts +0 -0
  212. /package/dist/{esm/types/src → src}/state-dir-resolve.d.ts +0 -0
  213. /package/dist/{esm/types/src → src}/state-manager.d.ts +0 -0
  214. /package/dist/{esm/types/src → src}/template-card-parser.d.ts +0 -0
  215. /package/dist/{esm/types/src → src}/timeout.d.ts +0 -0
@@ -1,284 +0,0 @@
1
- 'use strict';
2
-
3
- var openclawCompat = require('./openclaw-compat.js');
4
- var _const = require('./const.js');
5
-
6
- /**
7
- * 企业微信出站媒体上传工具模块
8
- *
9
- * 负责:
10
- * - 从 mediaUrl 加载文件 buffer(远程 URL 或本地路径均支持)
11
- * - 检测 MIME 类型并映射为企微媒体类型
12
- * - 文件大小检查与降级策略
13
- */
14
- // ============================================================================
15
- // MIME → 企微媒体类型映射
16
- // ============================================================================
17
- /**
18
- * 根据 MIME 类型检测企微媒体类型
19
- *
20
- * @param mimeType - MIME 类型字符串
21
- * @returns 企微媒体类型
22
- */
23
- function detectWeComMediaType(mimeType) {
24
- const mime = mimeType.toLowerCase();
25
- // 图片类型
26
- if (mime.startsWith("image/")) {
27
- return "image";
28
- }
29
- // 视频类型
30
- if (mime.startsWith("video/")) {
31
- return "video";
32
- }
33
- // 语音类型
34
- if (mime.startsWith("audio/") ||
35
- mime === "application/ogg" // OGG 音频容器
36
- ) {
37
- return "voice";
38
- }
39
- // 其他类型默认为文件
40
- return "file";
41
- }
42
- // ============================================================================
43
- // 媒体文件加载
44
- // ============================================================================
45
- /**
46
- * 从 mediaUrl 加载媒体文件
47
- *
48
- * 支持远程 URL(http/https)和本地路径(file:// 或绝对路径),
49
- * 利用 openclaw plugin-sdk 的 loadOutboundMediaFromUrl 统一处理。
50
- *
51
- * @param mediaUrl - 媒体文件的 URL 或本地路径
52
- * @param mediaLocalRoots - 允许读取本地文件的安全白名单目录
53
- * @returns 解析后的媒体文件信息
54
- */
55
- async function resolveMediaFile(mediaUrl, mediaLocalRoots) {
56
- // 使用兼容层加载媒体文件(优先 SDK,不可用时 fallback)
57
- // 传入足够大的 maxBytes,由我们自己在后续步骤做大小检查
58
- const result = await openclawCompat.loadOutboundMediaFromUrl(mediaUrl, {
59
- maxBytes: _const.ABSOLUTE_MAX_BYTES,
60
- mediaLocalRoots,
61
- });
62
- if (!result.buffer || result.buffer.length === 0) {
63
- throw new Error(`Failed to load media from ${mediaUrl}: empty buffer`);
64
- }
65
- // 检测真实 MIME 类型
66
- let contentType = result.contentType || "application/octet-stream";
67
- // 如果没有返回准确的 contentType,尝试通过 buffer 魔术字节检测
68
- if (contentType === "application/octet-stream" ||
69
- contentType === "text/plain") {
70
- const detected = await openclawCompat.detectMime(result.buffer);
71
- if (detected) {
72
- contentType = detected;
73
- }
74
- }
75
- // 提取文件名
76
- const fileName = extractFileName(mediaUrl, result.fileName, contentType);
77
- return {
78
- buffer: result.buffer,
79
- contentType,
80
- fileName,
81
- };
82
- }
83
- // ============================================================================
84
- // 文件大小检查与降级
85
- // ============================================================================
86
- /** 企微语音消息仅支持 AMR 格式 */
87
- const VOICE_SUPPORTED_MIMES = new Set(["audio/amr"]);
88
- /**
89
- * 检查文件大小并执行降级策略
90
- *
91
- * 降级规则:
92
- * - voice 非 AMR 格式 → 降级为 file(企微后台仅支持 AMR)
93
- * - image 超过 10MB → 降级为 file
94
- * - video 超过 10MB → 降级为 file
95
- * - voice 超过 2MB → 降级为 file
96
- * - file 超过 20MB → 拒绝发送
97
- *
98
- * @param fileSize - 文件大小(字节)
99
- * @param detectedType - 检测到的企微媒体类型
100
- * @param contentType - 文件的 MIME 类型(用于语音格式校验)
101
- * @returns 大小检查结果
102
- */
103
- function applyFileSizeLimits(fileSize, detectedType, contentType) {
104
- const fileSizeMB = (fileSize / (1024 * 1024)).toFixed(2);
105
- // 先检查绝对上限(20MB)
106
- if (fileSize > _const.ABSOLUTE_MAX_BYTES) {
107
- return {
108
- finalType: detectedType,
109
- shouldReject: true,
110
- rejectReason: `文件大小 ${fileSizeMB}MB 超过了企业微信允许的最大限制 20MB,无法发送。请尝试压缩文件或减小文件大小。`,
111
- downgraded: false,
112
- };
113
- }
114
- // 按类型检查大小限制
115
- switch (detectedType) {
116
- case "image":
117
- if (fileSize > _const.IMAGE_MAX_BYTES) {
118
- return {
119
- finalType: "file",
120
- shouldReject: false,
121
- downgraded: true,
122
- downgradeNote: `图片大小 ${fileSizeMB}MB 超过 10MB 限制,已转为文件格式发送`,
123
- };
124
- }
125
- break;
126
- case "video":
127
- if (fileSize > _const.VIDEO_MAX_BYTES) {
128
- return {
129
- finalType: "file",
130
- shouldReject: false,
131
- downgraded: true,
132
- downgradeNote: `视频大小 ${fileSizeMB}MB 超过 10MB 限制,已转为文件格式发送`,
133
- };
134
- }
135
- break;
136
- case "voice":
137
- // 企微语音消息仅支持 AMR 格式,非 AMR 一律降级为文件
138
- if (contentType && !VOICE_SUPPORTED_MIMES.has(contentType.toLowerCase())) {
139
- return {
140
- finalType: "file",
141
- shouldReject: false,
142
- downgraded: true,
143
- downgradeNote: `语音格式 ${contentType} 不支持,企微仅支持 AMR 格式,已转为文件格式发送`,
144
- };
145
- }
146
- if (fileSize > _const.VOICE_MAX_BYTES) {
147
- return {
148
- finalType: "file",
149
- shouldReject: false,
150
- downgraded: true,
151
- downgradeNote: `语音大小 ${fileSizeMB}MB 超过 2MB 限制,已转为文件格式发送`,
152
- };
153
- }
154
- break;
155
- }
156
- // 无需降级
157
- return {
158
- finalType: detectedType,
159
- shouldReject: false,
160
- downgraded: false,
161
- };
162
- }
163
- // ============================================================================
164
- // 辅助函数
165
- // ============================================================================
166
- /**
167
- * 从 URL/路径中提取文件名
168
- */
169
- function extractFileName(mediaUrl, providedFileName, contentType) {
170
- // 优先使用提供的文件名
171
- if (providedFileName) {
172
- return providedFileName;
173
- }
174
- // 尝试从 URL 中提取
175
- try {
176
- const urlObj = new URL(mediaUrl, "file://");
177
- const pathParts = urlObj.pathname.split("/");
178
- const lastPart = pathParts[pathParts.length - 1];
179
- if (lastPart && lastPart.includes(".")) {
180
- return decodeURIComponent(lastPart);
181
- }
182
- }
183
- catch {
184
- // 尝试作为普通路径处理
185
- const parts = mediaUrl.split("/");
186
- const lastPart = parts[parts.length - 1];
187
- if (lastPart && lastPart.includes(".")) {
188
- return lastPart;
189
- }
190
- }
191
- // 使用 MIME 类型生成默认文件名
192
- const ext = mimeToExtension(contentType || "application/octet-stream");
193
- return `media_${Date.now()}${ext}`;
194
- }
195
- /**
196
- * MIME 类型转文件扩展名
197
- */
198
- function mimeToExtension(mime) {
199
- const map = {
200
- "image/jpeg": ".jpg",
201
- "image/png": ".png",
202
- "image/gif": ".gif",
203
- "image/webp": ".webp",
204
- "image/bmp": ".bmp",
205
- "image/svg+xml": ".svg",
206
- "video/mp4": ".mp4",
207
- "video/quicktime": ".mov",
208
- "video/x-msvideo": ".avi",
209
- "video/webm": ".webm",
210
- "audio/mpeg": ".mp3",
211
- "audio/ogg": ".ogg",
212
- "audio/wav": ".wav",
213
- "audio/amr": ".amr",
214
- "audio/aac": ".aac",
215
- "application/pdf": ".pdf",
216
- "application/zip": ".zip",
217
- "application/msword": ".doc",
218
- "application/vnd.openxmlformats-officedocument.wordprocessingml.document": ".docx",
219
- "application/vnd.ms-excel": ".xls",
220
- "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": ".xlsx",
221
- "text/plain": ".txt",
222
- };
223
- return map[mime] || ".bin";
224
- }
225
- /**
226
- * 公共媒体上传+发送流程
227
- *
228
- * 统一处理:resolveMediaFile → detectType → sizeCheck → uploadMedia → sendMediaMessage
229
- * 媒体消息统一走 aibot_send_msg 主动发送,避免多文件场景下 reqId 只能用一次的问题。
230
- * channel.ts 的 sendMedia 和 monitor.ts 的 deliver 回调都使用此函数。
231
- */
232
- async function uploadAndSendMedia(options) {
233
- const { wsClient, mediaUrl, chatId, mediaLocalRoots, log, errorLog } = options;
234
- try {
235
- // 1. 加载媒体文件
236
- log?.(`[wecom] Uploading media: url=${mediaUrl}`);
237
- const media = await resolveMediaFile(mediaUrl, mediaLocalRoots);
238
- // 2. 检测企微媒体类型
239
- const detectedType = detectWeComMediaType(media.contentType);
240
- // 3. 文件大小检查与降级策略
241
- const sizeCheck = applyFileSizeLimits(media.buffer.length, detectedType, media.contentType);
242
- if (sizeCheck.shouldReject) {
243
- errorLog?.(`[wecom] Media rejected: ${sizeCheck.rejectReason}`);
244
- return {
245
- ok: false,
246
- rejected: true,
247
- rejectReason: sizeCheck.rejectReason,
248
- finalType: sizeCheck.finalType,
249
- };
250
- }
251
- const finalType = sizeCheck.finalType;
252
- // 4. 分片上传获取 media_id
253
- const uploadResult = await wsClient.uploadMedia(media.buffer, {
254
- type: finalType,
255
- filename: media.fileName,
256
- });
257
- log?.(`[wecom] Media uploaded: media_id=${uploadResult.media_id}, type=${finalType}`);
258
- // 5. 统一通过 aibot_send_msg 主动发送媒体消息
259
- const result = await wsClient.sendMediaMessage(chatId, finalType, uploadResult.media_id);
260
- const messageId = result?.headers?.req_id ?? `wecom-media-${Date.now()}`;
261
- log?.(`[wecom] Media sent via sendMediaMessage: chatId=${chatId}, type=${finalType}`);
262
- return {
263
- ok: true,
264
- messageId,
265
- finalType,
266
- downgraded: sizeCheck.downgraded,
267
- downgradeNote: sizeCheck.downgradeNote,
268
- };
269
- }
270
- catch (err) {
271
- const errMsg = String(err);
272
- errorLog?.(`[wecom] Failed to upload/send media: url=${mediaUrl}, error=${errMsg}`);
273
- return {
274
- ok: false,
275
- error: errMsg,
276
- };
277
- }
278
- }
279
-
280
- exports.applyFileSizeLimits = applyFileSizeLimits;
281
- exports.detectWeComMediaType = detectWeComMediaType;
282
- exports.resolveMediaFile = resolveMediaFile;
283
- exports.uploadAndSendMedia = uploadAndSendMedia;
284
- //# sourceMappingURL=media-uploader.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"media-uploader.js","sources":["../../../../../src/media-uploader.ts"],"sourcesContent":["/**\n * 企业微信出站媒体上传工具模块\n *\n * 负责:\n * - 从 mediaUrl 加载文件 buffer(远程 URL 或本地路径均支持)\n * - 检测 MIME 类型并映射为企微媒体类型\n * - 文件大小检查与降级策略\n */\n\nimport type { WeComMediaType, WSClient, WsFrameHeaders } from \"@wecom/aibot-node-sdk\";\nimport {\n loadOutboundMediaFromUrl,\n detectMime,\n type WebMediaResult,\n} from \"./openclaw-compat.js\";\nimport {\n IMAGE_MAX_BYTES,\n VIDEO_MAX_BYTES,\n VOICE_MAX_BYTES,\n FILE_MAX_BYTES,\n ABSOLUTE_MAX_BYTES,\n} from \"./const.js\";\n\n// ============================================================================\n// 类型定义\n// ============================================================================\n\n/** 媒体文件解析结果 */\nexport interface ResolvedMedia {\n /** 文件数据 */\n buffer: Buffer;\n /** 检测到的 MIME 类型 */\n contentType: string;\n /** 文件名(从 URL 提取或默认生成) */\n fileName: string;\n}\n\n/** 文件大小检查结果 */\nexport interface FileSizeCheckResult {\n /** 最终确定的企微媒体类型(可能被降级) */\n finalType: WeComMediaType;\n /** 是否需要拒绝(超过绝对限制) */\n shouldReject: boolean;\n /** 拒绝原因(仅 shouldReject=true 时有值) */\n rejectReason?: string;\n /** 是否发生了降级 */\n downgraded: boolean;\n /** 降级说明(仅 downgraded=true 时有值) */\n downgradeNote?: string;\n}\n\n// ============================================================================\n// MIME → 企微媒体类型映射\n// ============================================================================\n\n/**\n * 根据 MIME 类型检测企微媒体类型\n *\n * @param mimeType - MIME 类型字符串\n * @returns 企微媒体类型\n */\nexport function detectWeComMediaType(mimeType: string): WeComMediaType {\n const mime = mimeType.toLowerCase();\n\n // 图片类型\n if (mime.startsWith(\"image/\")) {\n return \"image\";\n }\n\n // 视频类型\n if (mime.startsWith(\"video/\")) {\n return \"video\";\n }\n\n // 语音类型\n if (\n mime.startsWith(\"audio/\") ||\n mime === \"application/ogg\" // OGG 音频容器\n ) {\n return \"voice\";\n }\n\n // 其他类型默认为文件\n return \"file\";\n}\n\n// ============================================================================\n// 媒体文件加载\n// ============================================================================\n\n/**\n * 从 mediaUrl 加载媒体文件\n *\n * 支持远程 URL(http/https)和本地路径(file:// 或绝对路径),\n * 利用 openclaw plugin-sdk 的 loadOutboundMediaFromUrl 统一处理。\n *\n * @param mediaUrl - 媒体文件的 URL 或本地路径\n * @param mediaLocalRoots - 允许读取本地文件的安全白名单目录\n * @returns 解析后的媒体文件信息\n */\nexport async function resolveMediaFile(\n mediaUrl: string,\n mediaLocalRoots?: readonly string[],\n): Promise<ResolvedMedia> {\n // 使用兼容层加载媒体文件(优先 SDK,不可用时 fallback)\n // 传入足够大的 maxBytes,由我们自己在后续步骤做大小检查\n const result: WebMediaResult = await loadOutboundMediaFromUrl(mediaUrl, {\n maxBytes: ABSOLUTE_MAX_BYTES,\n mediaLocalRoots,\n });\n\n if (!result.buffer || result.buffer.length === 0) {\n throw new Error(`Failed to load media from ${mediaUrl}: empty buffer`);\n }\n\n // 检测真实 MIME 类型\n let contentType = result.contentType || \"application/octet-stream\";\n\n // 如果没有返回准确的 contentType,尝试通过 buffer 魔术字节检测\n if (\n contentType === \"application/octet-stream\" ||\n contentType === \"text/plain\"\n ) {\n const detected = await detectMime(result.buffer);\n if (detected) {\n contentType = detected;\n }\n }\n\n // 提取文件名\n const fileName = extractFileName(mediaUrl, result.fileName, contentType);\n\n return {\n buffer: result.buffer,\n contentType,\n fileName,\n };\n}\n\n// ============================================================================\n// 文件大小检查与降级\n// ============================================================================\n\n/** 企微语音消息仅支持 AMR 格式 */\nconst VOICE_SUPPORTED_MIMES = new Set([\"audio/amr\"]);\n\n/**\n * 检查文件大小并执行降级策略\n *\n * 降级规则:\n * - voice 非 AMR 格式 → 降级为 file(企微后台仅支持 AMR)\n * - image 超过 10MB → 降级为 file\n * - video 超过 10MB → 降级为 file\n * - voice 超过 2MB → 降级为 file\n * - file 超过 20MB → 拒绝发送\n *\n * @param fileSize - 文件大小(字节)\n * @param detectedType - 检测到的企微媒体类型\n * @param contentType - 文件的 MIME 类型(用于语音格式校验)\n * @returns 大小检查结果\n */\nexport function applyFileSizeLimits(\n fileSize: number,\n detectedType: WeComMediaType,\n contentType?: string,\n): FileSizeCheckResult {\n const fileSizeMB = (fileSize / (1024 * 1024)).toFixed(2);\n\n // 先检查绝对上限(20MB)\n if (fileSize > ABSOLUTE_MAX_BYTES) {\n return {\n finalType: detectedType,\n shouldReject: true,\n rejectReason: `文件大小 ${fileSizeMB}MB 超过了企业微信允许的最大限制 20MB,无法发送。请尝试压缩文件或减小文件大小。`,\n downgraded: false,\n };\n }\n\n // 按类型检查大小限制\n switch (detectedType) {\n case \"image\":\n if (fileSize > IMAGE_MAX_BYTES) {\n return {\n finalType: \"file\",\n shouldReject: false,\n downgraded: true,\n downgradeNote: `图片大小 ${fileSizeMB}MB 超过 10MB 限制,已转为文件格式发送`,\n };\n }\n break;\n\n case \"video\":\n if (fileSize > VIDEO_MAX_BYTES) {\n return {\n finalType: \"file\",\n shouldReject: false,\n downgraded: true,\n downgradeNote: `视频大小 ${fileSizeMB}MB 超过 10MB 限制,已转为文件格式发送`,\n };\n }\n break;\n\n case \"voice\":\n // 企微语音消息仅支持 AMR 格式,非 AMR 一律降级为文件\n if (contentType && !VOICE_SUPPORTED_MIMES.has(contentType.toLowerCase())) {\n return {\n finalType: \"file\",\n shouldReject: false,\n downgraded: true,\n downgradeNote: `语音格式 ${contentType} 不支持,企微仅支持 AMR 格式,已转为文件格式发送`,\n };\n }\n if (fileSize > VOICE_MAX_BYTES) {\n return {\n finalType: \"file\",\n shouldReject: false,\n downgraded: true,\n downgradeNote: `语音大小 ${fileSizeMB}MB 超过 2MB 限制,已转为文件格式发送`,\n };\n }\n break;\n\n case \"file\":\n // file 类型在绝对上限内即可\n break;\n }\n\n // 无需降级\n return {\n finalType: detectedType,\n shouldReject: false,\n downgraded: false,\n };\n}\n\n// ============================================================================\n// 辅助函数\n// ============================================================================\n\n/**\n * 从 URL/路径中提取文件名\n */\nfunction extractFileName(\n mediaUrl: string,\n providedFileName?: string,\n contentType?: string,\n): string {\n // 优先使用提供的文件名\n if (providedFileName) {\n return providedFileName;\n }\n\n // 尝试从 URL 中提取\n try {\n const urlObj = new URL(mediaUrl, \"file://\");\n const pathParts = urlObj.pathname.split(\"/\");\n const lastPart = pathParts[pathParts.length - 1];\n if (lastPart && lastPart.includes(\".\")) {\n return decodeURIComponent(lastPart);\n }\n } catch {\n // 尝试作为普通路径处理\n const parts = mediaUrl.split(\"/\");\n const lastPart = parts[parts.length - 1];\n if (lastPart && lastPart.includes(\".\")) {\n return lastPart;\n }\n }\n\n // 使用 MIME 类型生成默认文件名\n const ext = mimeToExtension(contentType || \"application/octet-stream\");\n return `media_${Date.now()}${ext}`;\n}\n\n/**\n * MIME 类型转文件扩展名\n */\nfunction mimeToExtension(mime: string): string {\n const map: Record<string, string> = {\n \"image/jpeg\": \".jpg\",\n \"image/png\": \".png\",\n \"image/gif\": \".gif\",\n \"image/webp\": \".webp\",\n \"image/bmp\": \".bmp\",\n \"image/svg+xml\": \".svg\",\n \"video/mp4\": \".mp4\",\n \"video/quicktime\": \".mov\",\n \"video/x-msvideo\": \".avi\",\n \"video/webm\": \".webm\",\n \"audio/mpeg\": \".mp3\",\n \"audio/ogg\": \".ogg\",\n \"audio/wav\": \".wav\",\n \"audio/amr\": \".amr\",\n \"audio/aac\": \".aac\",\n \"application/pdf\": \".pdf\",\n \"application/zip\": \".zip\",\n \"application/msword\": \".doc\",\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\":\n \".docx\",\n \"application/vnd.ms-excel\": \".xls\",\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\":\n \".xlsx\",\n \"text/plain\": \".txt\",\n };\n return map[mime] || \".bin\";\n}\n\n// ============================================================================\n// 公共媒体上传+发送流程\n// ============================================================================\n\n/** uploadAndSendMedia 的参数 */\nexport interface UploadAndSendMediaOptions {\n /** WSClient 实例 */\n wsClient: WSClient;\n /** 媒体文件的 URL 或本地路径 */\n mediaUrl: string;\n /** 目标会话 ID(用于 aibot_send_msg 主动发送) */\n chatId: string;\n /** 允许读取本地文件的安全白名单目录 */\n mediaLocalRoots?: readonly string[];\n /** 日志函数 */\n log?: (...args: any[]) => void;\n /** 错误日志函数 */\n errorLog?: (...args: any[]) => void;\n}\n\n/** uploadAndSendMedia 的返回结果 */\nexport interface UploadAndSendMediaResult {\n /** 是否发送成功 */\n ok: boolean;\n /** 发送后返回的 messageId */\n messageId?: string;\n /** 最终的企微媒体类型 */\n finalType?: WeComMediaType;\n /** 是否被拒绝(文件过大) */\n rejected?: boolean;\n /** 拒绝原因 */\n rejectReason?: string;\n /** 是否发生了降级 */\n downgraded?: boolean;\n /** 降级说明 */\n downgradeNote?: string;\n /** 错误信息 */\n error?: string;\n}\n\n/**\n * 公共媒体上传+发送流程\n *\n * 统一处理:resolveMediaFile → detectType → sizeCheck → uploadMedia → sendMediaMessage\n * 媒体消息统一走 aibot_send_msg 主动发送,避免多文件场景下 reqId 只能用一次的问题。\n * channel.ts 的 sendMedia 和 monitor.ts 的 deliver 回调都使用此函数。\n */\nexport async function uploadAndSendMedia(\n options: UploadAndSendMediaOptions,\n): Promise<UploadAndSendMediaResult> {\n const { wsClient, mediaUrl, chatId, mediaLocalRoots, log, errorLog } = options;\n\n try {\n // 1. 加载媒体文件\n log?.(`[wecom] Uploading media: url=${mediaUrl}`);\n const media = await resolveMediaFile(mediaUrl, mediaLocalRoots);\n\n // 2. 检测企微媒体类型\n const detectedType = detectWeComMediaType(media.contentType);\n\n // 3. 文件大小检查与降级策略\n const sizeCheck = applyFileSizeLimits(media.buffer.length, detectedType, media.contentType);\n\n if (sizeCheck.shouldReject) {\n errorLog?.(`[wecom] Media rejected: ${sizeCheck.rejectReason}`);\n return {\n ok: false,\n rejected: true,\n rejectReason: sizeCheck.rejectReason,\n finalType: sizeCheck.finalType,\n };\n }\n\n const finalType = sizeCheck.finalType;\n\n // 4. 分片上传获取 media_id\n const uploadResult = await wsClient.uploadMedia(media.buffer, {\n type: finalType,\n filename: media.fileName,\n });\n log?.(`[wecom] Media uploaded: media_id=${uploadResult.media_id}, type=${finalType}`);\n\n // 5. 统一通过 aibot_send_msg 主动发送媒体消息\n const result = await wsClient.sendMediaMessage(chatId, finalType, uploadResult.media_id);\n const messageId = result?.headers?.req_id ?? `wecom-media-${Date.now()}`;\n log?.(`[wecom] Media sent via sendMediaMessage: chatId=${chatId}, type=${finalType}`);\n\n return {\n ok: true,\n messageId,\n finalType,\n downgraded: sizeCheck.downgraded,\n downgradeNote: sizeCheck.downgradeNote,\n };\n } catch (err) {\n const errMsg = String(err);\n errorLog?.(`[wecom] Failed to upload/send media: url=${mediaUrl}, error=${errMsg}`);\n return {\n ok: false,\n error: errMsg,\n };\n }\n}\n\n// ============================================================================\n// 被动回复媒体上传+发送流程\n// ============================================================================\n\n/** uploadAndReplyMedia 的参数 */\nexport interface UploadAndReplyMediaOptions {\n /** WSClient 实例 */\n wsClient: WSClient;\n /** 媒体文件的 URL 或本地路径 */\n mediaUrl: string;\n /** 原始 WebSocket 帧(用于 aibot_respond_msg 被动回复,携带 req_id) */\n frame: WsFrameHeaders;\n /** 允许读取本地文件的安全白名单目录 */\n mediaLocalRoots?: readonly string[];\n /** 日志函数 */\n log?: (...args: any[]) => void;\n /** 错误日志函数 */\n errorLog?: (...args: any[]) => void;\n}\n\n/**\n * 被动回复媒体上传+发送流程\n *\n * 统一处理:resolveMediaFile → detectType → sizeCheck → uploadMedia → replyMedia\n * 通过 aibot_respond_msg 被动回复通道发送媒体消息,可以覆盖之前的 THINKING_MESSAGE。\n *\n * 适用场景:回包只有媒体没有文本时,第一个媒体文件用此方法发送以清理 thinking 状态。\n */\nexport async function uploadAndReplyMedia(\n options: UploadAndReplyMediaOptions,\n): Promise<UploadAndSendMediaResult> {\n const { wsClient, mediaUrl, frame, mediaLocalRoots, log, errorLog } = options;\n\n try {\n // 1. 加载媒体文件\n log?.(`[wecom] Uploading media (reply mode): url=${mediaUrl}`);\n const media = await resolveMediaFile(mediaUrl, mediaLocalRoots);\n\n // 2. 检测企微媒体类型\n const detectedType = detectWeComMediaType(media.contentType);\n\n // 3. 文件大小检查与降级策略\n const sizeCheck = applyFileSizeLimits(media.buffer.length, detectedType, media.contentType);\n\n if (sizeCheck.shouldReject) {\n errorLog?.(`[wecom] Media rejected: ${sizeCheck.rejectReason}`);\n return {\n ok: false,\n rejected: true,\n rejectReason: sizeCheck.rejectReason,\n finalType: sizeCheck.finalType,\n };\n }\n\n const finalType = sizeCheck.finalType;\n\n // 4. 分片上传获取 media_id\n const uploadResult = await wsClient.uploadMedia(media.buffer, {\n type: finalType,\n filename: media.fileName,\n });\n log?.(`[wecom] Media uploaded: media_id=${uploadResult.media_id}, type=${finalType}`);\n\n // 5. 通过 aibot_respond_msg 被动回复发送媒体消息(会覆盖 THINKING_MESSAGE)\n const result = await wsClient.replyMedia(frame, finalType, uploadResult.media_id);\n const messageId = result?.headers?.req_id ?? `wecom-reply-media-${Date.now()}`;\n log?.(`[wecom] Media sent via replyMedia (passive reply): type=${finalType}`);\n\n return {\n ok: true,\n messageId,\n finalType,\n downgraded: sizeCheck.downgraded,\n downgradeNote: sizeCheck.downgradeNote,\n };\n } catch (err) {\n const errMsg = String(err);\n errorLog?.(`[wecom] Failed to upload/reply media: url=${mediaUrl}, error=${errMsg}`);\n return {\n ok: false,\n error: errMsg,\n };\n }\n}\n"],"names":["loadOutboundMediaFromUrl","ABSOLUTE_MAX_BYTES","detectMime","IMAGE_MAX_BYTES","VIDEO_MAX_BYTES","VOICE_MAX_BYTES"],"mappings":"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"}
@@ -1,134 +0,0 @@
1
- 'use strict';
2
-
3
- /**
4
- * 企业微信消息内容解析模块
5
- *
6
- * 负责从 WsFrame 中提取文本、图片、引用等内容
7
- */
8
- // ============================================================================
9
- // 解析函数
10
- // ============================================================================
11
- /**
12
- * 将模板卡片事件回调格式化为可继续路由给大模型的文本。
13
- *
14
- * 这样后续 Agent 可以直接从 question_key / option_id 中理解用户的真实选择。
15
- */
16
- function buildTemplateCardEventText(body) {
17
- const templateCardEvent = body.event?.template_card_event;
18
- if (body.msgtype !== "event" ||
19
- body.event?.eventtype !== "template_card_event" ||
20
- !templateCardEvent) {
21
- return undefined;
22
- }
23
- const selectedItems = templateCardEvent.selected_items?.selected_item ?? [];
24
- const selectedLines = selectedItems.map((item) => {
25
- const questionKey = item.question_key?.trim() || "unknown_question";
26
- const optionIds = item.option_ids?.option_id?.filter(Boolean) ?? [];
27
- return `- ${questionKey}: ${optionIds.length > 0 ? optionIds.join(", ") : "(未选择)"}`;
28
- });
29
- const senderUserId = body.from?.userid || "";
30
- const senderCorpId = body.from?.corpid || "";
31
- const chatId = body.chatid || senderUserId;
32
- return [
33
- "[企业微信模板卡片回调]",
34
- `event_type(事件类型): template_card_event`,
35
- body.msgid ? `msgid(消息 id): ${body.msgid}` : undefined,
36
- body.aibotid ? `aibotid(机器人 id): ${body.aibotid}` : undefined,
37
- body.chattype ? `chat_type(会话类型): ${body.chattype}` : undefined,
38
- chatId ? `chat_id(会话 id): ${chatId}` : undefined,
39
- senderCorpId ? `from.corpid(企业 id): ${senderCorpId}` : undefined,
40
- senderUserId ? `from.userid(发送人 id): ${senderUserId}` : undefined,
41
- senderUserId ? `sender_userid(发送人 id): ${senderUserId}` : undefined,
42
- templateCardEvent.card_type ? `card_type(卡片类型): ${templateCardEvent.card_type}` : undefined,
43
- templateCardEvent.event_key ? `event_key(事件 key): ${templateCardEvent.event_key}` : undefined,
44
- templateCardEvent.task_id ? `task_id(任务 id): ${templateCardEvent.task_id}` : undefined,
45
- selectedLines.length > 0 ? "selected_items(选择项):" : "selected_items(选择项): []",
46
- ...selectedLines,
47
- ]
48
- .filter((line) => Boolean(line))
49
- .join("\n");
50
- }
51
- /**
52
- * 解析消息内容(支持单条消息、图文混排、事件回调和引用消息)
53
- * @returns 提取的文本数组、图片URL数组和引用消息内容
54
- */
55
- function parseMessageContent(body) {
56
- const textParts = [];
57
- const imageUrls = [];
58
- const imageAesKeys = new Map();
59
- const fileUrls = [];
60
- const fileAesKeys = new Map();
61
- let quoteContent;
62
- // 处理模板卡片事件回调
63
- if (body.msgtype === "event") {
64
- const eventText = buildTemplateCardEventText(body);
65
- if (eventText) {
66
- textParts.push(eventText);
67
- }
68
- return { textParts, imageUrls, imageAesKeys, fileUrls, fileAesKeys, quoteContent };
69
- }
70
- // 处理图文混排消息
71
- if (body.msgtype === "mixed" && body.mixed?.msg_item) {
72
- for (const item of body.mixed.msg_item) {
73
- if (item.msgtype === "text" && item.text?.content) {
74
- textParts.push(item.text.content);
75
- }
76
- else if (item.msgtype === "image" && item.image?.url) {
77
- imageUrls.push(item.image.url);
78
- if (item.image.aeskey) {
79
- imageAesKeys.set(item.image.url, item.image.aeskey);
80
- }
81
- }
82
- }
83
- }
84
- else {
85
- // 处理单条消息
86
- if (body.text?.content) {
87
- textParts.push(body.text.content);
88
- }
89
- // 处理语音消息(语音转文字后的文本内容)
90
- if (body.msgtype === "voice" && body.voice?.content) {
91
- textParts.push(body.voice.content);
92
- }
93
- if (body.image?.url) {
94
- imageUrls.push(body.image.url);
95
- if (body.image.aeskey) {
96
- imageAesKeys.set(body.image.url, body.image.aeskey);
97
- }
98
- }
99
- // 处理文件消息
100
- if (body.msgtype === "file" && body.file?.url) {
101
- fileUrls.push(body.file.url);
102
- if (body.file.aeskey) {
103
- fileAesKeys.set(body.file.url, body.file.aeskey);
104
- }
105
- }
106
- }
107
- // 处理引用消息
108
- if (body.quote) {
109
- if (body.quote.msgtype === "text" && body.quote.text?.content) {
110
- quoteContent = body.quote.text.content;
111
- }
112
- else if (body.quote.msgtype === "voice" && body.quote.voice?.content) {
113
- quoteContent = body.quote.voice.content;
114
- }
115
- else if (body.quote.msgtype === "image" && body.quote.image?.url) {
116
- // 引用的图片消息:将图片 URL 加入下载列表
117
- imageUrls.push(body.quote.image.url);
118
- if (body.quote.image.aeskey) {
119
- imageAesKeys.set(body.quote.image.url, body.quote.image.aeskey);
120
- }
121
- }
122
- else if (body.quote.msgtype === "file" && body.quote.file?.url) {
123
- // 引用的文件消息:将文件 URL 加入下载列表
124
- fileUrls.push(body.quote.file.url);
125
- if (body.quote.file.aeskey) {
126
- fileAesKeys.set(body.quote.file.url, body.quote.file.aeskey);
127
- }
128
- }
129
- }
130
- return { textParts, imageUrls, imageAesKeys, fileUrls, fileAesKeys, quoteContent };
131
- }
132
-
133
- exports.parseMessageContent = parseMessageContent;
134
- //# sourceMappingURL=message-parser.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"message-parser.js","sources":["../../../../../src/message-parser.ts"],"sourcesContent":["/**\n * 企业微信消息内容解析模块\n *\n * 负责从 WsFrame 中提取文本、图片、引用等内容\n */\n\n// ============================================================================\n// 消息体类型(来自 SDK WsFrame.body)\n// ============================================================================\n\nexport interface MessageBody {\n msgid: string;\n aibotid?: string;\n chatid?: string;\n chattype: \"single\" | \"group\";\n from: {\n corpid?: string;\n userid: string;\n };\n response_url?: string;\n msgtype: string;\n text?: {\n content: string;\n };\n image?: {\n url?: string;\n aeskey?: string;\n };\n voice?: {\n content?: string;\n };\n mixed?: {\n msg_item: Array<{\n msgtype: \"text\" | \"image\";\n text?: { content: string };\n image?: { url?: string; aeskey?: string };\n }>;\n };\n file?: {\n url?: string;\n aeskey?: string;\n };\n quote?: {\n msgtype: string;\n text?: { content: string };\n voice?: { content: string };\n image?: { url?: string; aeskey?: string };\n file?: { url?: string; aeskey?: string };\n };\n event?: {\n eventtype?: string;\n template_card_event?: {\n card_type?: string;\n event_key?: string;\n task_id?: string;\n selected_items?: {\n selected_item?: Array<{\n question_key?: string;\n option_ids?: {\n option_id?: string[];\n };\n }>;\n };\n };\n };\n}\n\n// ============================================================================\n// 解析结果类型\n// ============================================================================\n\nexport interface ParsedMessageContent {\n textParts: string[];\n imageUrls: string[];\n imageAesKeys: Map<string, string>;\n fileUrls: string[];\n fileAesKeys: Map<string, string>;\n quoteContent: string | undefined;\n}\n\n// ============================================================================\n// 解析函数\n// ============================================================================\n\n/**\n * 将模板卡片事件回调格式化为可继续路由给大模型的文本。\n *\n * 这样后续 Agent 可以直接从 question_key / option_id 中理解用户的真实选择。\n */\nfunction buildTemplateCardEventText(body: MessageBody): string | undefined {\n const templateCardEvent = body.event?.template_card_event;\n if (\n body.msgtype !== \"event\" ||\n body.event?.eventtype !== \"template_card_event\" ||\n !templateCardEvent\n ) {\n return undefined;\n }\n\n const selectedItems = templateCardEvent.selected_items?.selected_item ?? [];\n const selectedLines = selectedItems.map((item) => {\n const questionKey = item.question_key?.trim() || \"unknown_question\";\n const optionIds = item.option_ids?.option_id?.filter(Boolean) ?? [];\n return `- ${questionKey}: ${optionIds.length > 0 ? optionIds.join(\", \") : \"(未选择)\"}`;\n });\n\n const senderUserId = body.from?.userid || \"\";\n const senderCorpId = body.from?.corpid || \"\";\n const chatId = body.chatid || senderUserId;\n\n return [\n \"[企业微信模板卡片回调]\",\n `event_type(事件类型): template_card_event`,\n body.msgid ? `msgid(消息 id): ${body.msgid}` : undefined,\n body.aibotid ? `aibotid(机器人 id): ${body.aibotid}` : undefined,\n body.chattype ? `chat_type(会话类型): ${body.chattype}` : undefined,\n chatId ? `chat_id(会话 id): ${chatId}` : undefined,\n senderCorpId ? `from.corpid(企业 id): ${senderCorpId}` : undefined,\n senderUserId ? `from.userid(发送人 id): ${senderUserId}` : undefined,\n senderUserId ? `sender_userid(发送人 id): ${senderUserId}` : undefined,\n templateCardEvent.card_type ? `card_type(卡片类型): ${templateCardEvent.card_type}` : undefined,\n templateCardEvent.event_key ? `event_key(事件 key): ${templateCardEvent.event_key}` : undefined,\n templateCardEvent.task_id ? `task_id(任务 id): ${templateCardEvent.task_id}` : undefined,\n selectedLines.length > 0 ? \"selected_items(选择项):\" : \"selected_items(选择项): []\",\n ...selectedLines,\n ]\n .filter((line): line is string => Boolean(line))\n .join(\"\\n\");\n}\n\n/**\n * 解析消息内容(支持单条消息、图文混排、事件回调和引用消息)\n * @returns 提取的文本数组、图片URL数组和引用消息内容\n */\nexport function parseMessageContent(body: MessageBody): ParsedMessageContent {\n const textParts: string[] = [];\n const imageUrls: string[] = [];\n const imageAesKeys = new Map<string, string>();\n const fileUrls: string[] = [];\n const fileAesKeys = new Map<string, string>();\n let quoteContent: string | undefined;\n\n // 处理模板卡片事件回调\n \n if (body.msgtype === \"event\") {\n const eventText = buildTemplateCardEventText(body);\n if (eventText) {\n textParts.push(eventText);\n }\n return { textParts, imageUrls, imageAesKeys, fileUrls, fileAesKeys, quoteContent };\n }\n\n // 处理图文混排消息\n if (body.msgtype === \"mixed\" && body.mixed?.msg_item) {\n for (const item of body.mixed.msg_item) {\n if (item.msgtype === \"text\" && item.text?.content) {\n textParts.push(item.text.content);\n } else if (item.msgtype === \"image\" && item.image?.url) {\n imageUrls.push(item.image.url);\n if (item.image.aeskey) {\n imageAesKeys.set(item.image.url, item.image.aeskey);\n }\n }\n }\n } else {\n // 处理单条消息\n if (body.text?.content) {\n textParts.push(body.text.content);\n }\n // 处理语音消息(语音转文字后的文本内容)\n if (body.msgtype === \"voice\" && body.voice?.content) {\n textParts.push(body.voice.content);\n }\n if (body.image?.url) {\n imageUrls.push(body.image.url);\n if (body.image.aeskey) {\n imageAesKeys.set(body.image.url, body.image.aeskey);\n }\n }\n // 处理文件消息\n if (body.msgtype === \"file\" && body.file?.url) {\n fileUrls.push(body.file.url);\n if (body.file.aeskey) {\n fileAesKeys.set(body.file.url, body.file.aeskey);\n }\n }\n }\n\n // 处理引用消息\n if (body.quote) {\n if (body.quote.msgtype === \"text\" && body.quote.text?.content) {\n quoteContent = body.quote.text.content;\n } else if (body.quote.msgtype === \"voice\" && body.quote.voice?.content) {\n quoteContent = body.quote.voice.content;\n } else if (body.quote.msgtype === \"image\" && body.quote.image?.url) {\n // 引用的图片消息:将图片 URL 加入下载列表\n imageUrls.push(body.quote.image.url);\n if (body.quote.image.aeskey) {\n imageAesKeys.set(body.quote.image.url, body.quote.image.aeskey);\n }\n } else if (body.quote.msgtype === \"file\" && body.quote.file?.url) {\n // 引用的文件消息:将文件 URL 加入下载列表\n fileUrls.push(body.quote.file.url);\n if (body.quote.file.aeskey) {\n fileAesKeys.set(body.quote.file.url, body.quote.file.aeskey);\n }\n }\n }\n\n return { textParts, imageUrls, imageAesKeys, fileUrls, fileAesKeys, quoteContent };\n}\n"],"names":[],"mappings":"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"}
@@ -1,89 +0,0 @@
1
- 'use strict';
2
-
3
- var aibotNodeSdk = require('@wecom/aibot-node-sdk');
4
- var _const = require('./const.js');
5
- var timeout = require('./timeout.js');
6
-
7
- /**
8
- * 企业微信消息发送模块
9
- *
10
- * 负责通过 WSClient 发送回复消息,包含超时保护
11
- */
12
- // ============================================================================
13
- // 流式过期错误(errcode 846608)
14
- // ============================================================================
15
- /** 流式回复超时错误码(>6分钟未更新,服务端拒绝继续流式更新) */
16
- const STREAM_EXPIRED_ERRCODE = 846608;
17
- /**
18
- * 流式回复过期错误
19
- * 当服务端返回 errcode=846608 时抛出,表示流式消息已超过6分钟无法更新,
20
- * 调用方需降级为主动发送(sendMessage)方式回复。
21
- */
22
- class StreamExpiredError extends Error {
23
- constructor(message) {
24
- super(message ?? `Stream message update expired (errcode=${STREAM_EXPIRED_ERRCODE})`);
25
- this.errcode = STREAM_EXPIRED_ERRCODE;
26
- this.name = "StreamExpiredError";
27
- }
28
- }
29
- // ============================================================================
30
- // 消息发送
31
- // ============================================================================
32
- /**
33
- * 发送企业微信回复消息
34
- * 供 monitor 内部和 channel outbound 使用
35
- *
36
- * @returns messageId (streamId)
37
- */
38
- async function sendWeComReply(params) {
39
- const { wsClient, frame, text, runtime, finish = true, streamId: existingStreamId } = params;
40
- if (!text) {
41
- return "";
42
- }
43
- const streamId = existingStreamId || aibotNodeSdk.generateReqId("stream");
44
- if (!wsClient.isConnected) {
45
- runtime.error?.(`[wecom] WSClient not connected, cannot send reply`);
46
- throw new Error("WSClient not connected");
47
- }
48
- const body = frame.body;
49
- // 事件回调(aibot_event_callback)没有可用于 replyStream 的有效 req_id,
50
- // 对该场景改用主动发送 sendMessage,避免 846605 invalid req_id。
51
- if (body.msgtype === "event") {
52
- // 中间帧(thinking / 流式增量)直接跳过,仅在最终帧主动发一次文本。
53
- if (!finish) {
54
- runtime.log?.(`[plugin -> server] skip non-final stream for event callback, streamId=${streamId}`);
55
- return streamId;
56
- }
57
- const chatId = body.chatid || body.from?.userid;
58
- if (!chatId) {
59
- throw new Error("Missing chatId for event callback reply");
60
- }
61
- await timeout.withTimeout(wsClient.sendMessage(chatId, {
62
- msgtype: "markdown",
63
- markdown: { content: text },
64
- }), _const.REPLY_SEND_TIMEOUT_MS, `Event reply send timed out (streamId=${streamId})`);
65
- runtime.log?.(`[plugin -> server] event-active-send chatId=${chatId}, finish=${finish}`);
66
- return streamId;
67
- }
68
- // 非事件消息,继续使用 replyStream(被动回复)
69
- // 使用 SDK 的 replyStream 方法发送消息,带超时保护
70
- try {
71
- await timeout.withTimeout(wsClient.replyStream(frame, streamId, text, finish), _const.REPLY_SEND_TIMEOUT_MS, `Reply send timed out (streamId=${streamId})`);
72
- }
73
- catch (err) {
74
- // 服务端返回 846608:流式消息超过6分钟无法更新,需降级为主动发送
75
- const errMsg = err?.errmsg || err?.message || String(err);
76
- if (err?.errcode === STREAM_EXPIRED_ERRCODE ||
77
- errMsg.includes(String(STREAM_EXPIRED_ERRCODE))) {
78
- throw new StreamExpiredError(errMsg);
79
- }
80
- throw err;
81
- }
82
- runtime.log?.(`[plugin -> server] streamId=${streamId}, finish=${finish}`);
83
- return streamId;
84
- }
85
-
86
- exports.STREAM_EXPIRED_ERRCODE = STREAM_EXPIRED_ERRCODE;
87
- exports.StreamExpiredError = StreamExpiredError;
88
- exports.sendWeComReply = sendWeComReply;
89
- //# sourceMappingURL=message-sender.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"message-sender.js","sources":["../../../../../src/message-sender.ts"],"sourcesContent":["/**\n * 企业微信消息发送模块\n *\n * 负责通过 WSClient 发送回复消息,包含超时保护\n */\n\nimport type { RuntimeEnv } from \"openclaw/plugin-sdk/runtime-env\";\nimport { type WSClient, type WsFrame, generateReqId } from \"@wecom/aibot-node-sdk\";\nimport { REPLY_SEND_TIMEOUT_MS } from \"./const.js\";\nimport { withTimeout } from \"./timeout.js\";\n\n// ============================================================================\n// 流式过期错误(errcode 846608)\n// ============================================================================\n\n/** 流式回复超时错误码(>6分钟未更新,服务端拒绝继续流式更新) */\nexport const STREAM_EXPIRED_ERRCODE = 846608;\n\n/**\n * 流式回复过期错误\n * 当服务端返回 errcode=846608 时抛出,表示流式消息已超过6分钟无法更新,\n * 调用方需降级为主动发送(sendMessage)方式回复。\n */\nexport class StreamExpiredError extends Error {\n readonly errcode = STREAM_EXPIRED_ERRCODE;\n constructor(message?: string) {\n super(message ?? `Stream message update expired (errcode=${STREAM_EXPIRED_ERRCODE})`);\n this.name = \"StreamExpiredError\";\n }\n}\n\n// ============================================================================\n// 消息发送\n// ============================================================================\n\n/**\n * 发送企业微信回复消息\n * 供 monitor 内部和 channel outbound 使用\n *\n * @returns messageId (streamId)\n */\nexport async function sendWeComReply(params: {\n wsClient: WSClient;\n frame: WsFrame;\n text?: string;\n runtime: RuntimeEnv;\n /** 是否为流式回复的最终消息,默认为 true */\n finish?: boolean;\n /** 指定 streamId,用于流式回复时保持相同的 streamId */\n streamId?: string;\n}): Promise<string> {\n const { wsClient, frame, text, runtime, finish = true, streamId: existingStreamId } = params;\n\n if (!text) {\n return \"\";\n }\n\n const streamId = existingStreamId || generateReqId(\"stream\");\n\n if (!wsClient.isConnected) {\n runtime.error?.(`[wecom] WSClient not connected, cannot send reply`);\n throw new Error(\"WSClient not connected\");\n }\n\n const body = frame.body as {\n msgtype?: string;\n chatid?: string;\n from?: {\n userid?: string;\n };\n };\n\n // 事件回调(aibot_event_callback)没有可用于 replyStream 的有效 req_id,\n // 对该场景改用主动发送 sendMessage,避免 846605 invalid req_id。\n if (body.msgtype === \"event\") {\n // 中间帧(thinking / 流式增量)直接跳过,仅在最终帧主动发一次文本。\n if (!finish) {\n runtime.log?.(`[plugin -> server] skip non-final stream for event callback, streamId=${streamId}`);\n return streamId;\n }\n\n const chatId = body.chatid || body.from?.userid;\n if (!chatId) {\n throw new Error(\"Missing chatId for event callback reply\");\n }\n\n await withTimeout(\n wsClient.sendMessage(chatId, {\n msgtype: \"markdown\",\n markdown: { content: text },\n }),\n REPLY_SEND_TIMEOUT_MS,\n `Event reply send timed out (streamId=${streamId})`,\n );\n runtime.log?.(`[plugin -> server] event-active-send chatId=${chatId}, finish=${finish}`);\n return streamId;\n }\n\n // 非事件消息,继续使用 replyStream(被动回复)\n // 使用 SDK 的 replyStream 方法发送消息,带超时保护\n try {\n await withTimeout(\n wsClient.replyStream(frame, streamId, text, finish),\n REPLY_SEND_TIMEOUT_MS,\n `Reply send timed out (streamId=${streamId})`,\n );\n } catch (err: any) {\n // 服务端返回 846608:流式消息超过6分钟无法更新,需降级为主动发送\n const errMsg = err?.errmsg || err?.message || String(err);\n if (\n err?.errcode === STREAM_EXPIRED_ERRCODE ||\n errMsg.includes(String(STREAM_EXPIRED_ERRCODE))\n ) {\n throw new StreamExpiredError(errMsg);\n }\n throw err;\n }\n runtime.log?.(`[plugin -> server] streamId=${streamId}, finish=${finish}`);\n\n return streamId;\n}\n"],"names":["generateReqId","withTimeout","REPLY_SEND_TIMEOUT_MS"],"mappings":";;;;;;AAAA;;;;AAIG;AAOH;AACA;AACA;AAEA;AACO,MAAM,sBAAsB,GAAG;AAEtC;;;;AAIG;AACG,MAAO,kBAAmB,SAAQ,KAAK,CAAA;AAE3C,IAAA,WAAA,CAAY,OAAgB,EAAA;AAC1B,QAAA,KAAK,CAAC,OAAO,IAAI,0CAA0C,sBAAsB,CAAA,CAAA,CAAG,CAAC;QAF9E,IAAA,CAAA,OAAO,GAAG,sBAAsB;AAGvC,QAAA,IAAI,CAAC,IAAI,GAAG,oBAAoB;IAClC;AACD;AAED;AACA;AACA;AAEA;;;;;AAKG;AACI,eAAe,cAAc,CAAC,MASpC,EAAA;AACC,IAAA,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,EAAE,QAAQ,EAAE,gBAAgB,EAAE,GAAG,MAAM;IAE5F,IAAI,CAAC,IAAI,EAAE;AACT,QAAA,OAAO,EAAE;IACX;IAEA,MAAM,QAAQ,GAAG,gBAAgB,IAAIA,0BAAa,CAAC,QAAQ,CAAC;AAE5D,IAAA,IAAI,CAAC,QAAQ,CAAC,WAAW,EAAE;AACzB,QAAA,OAAO,CAAC,KAAK,GAAG,CAAA,iDAAA,CAAmD,CAAC;AACpE,QAAA,MAAM,IAAI,KAAK,CAAC,wBAAwB,CAAC;IAC3C;AAEA,IAAA,MAAM,IAAI,GAAG,KAAK,CAAC,IAMlB;;;AAID,IAAA,IAAI,IAAI,CAAC,OAAO,KAAK,OAAO,EAAE;;QAE5B,IAAI,CAAC,MAAM,EAAE;YACX,OAAO,CAAC,GAAG,GAAG,yEAAyE,QAAQ,CAAA,CAAE,CAAC;AAClG,YAAA,OAAO,QAAQ;QACjB;QAEA,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,IAAI,EAAE,MAAM;QAC/C,IAAI,CAAC,MAAM,EAAE;AACX,YAAA,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC;QAC5D;AAEA,QAAA,MAAMC,mBAAW,CACf,QAAQ,CAAC,WAAW,CAAC,MAAM,EAAE;AAC3B,YAAA,OAAO,EAAE,UAAU;AACnB,YAAA,QAAQ,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE;AAC5B,SAAA,CAAC,EACFC,4BAAqB,EACrB,wCAAwC,QAAQ,CAAA,CAAA,CAAG,CACpD;QACD,OAAO,CAAC,GAAG,GAAG,CAAA,4CAAA,EAA+C,MAAM,CAAA,SAAA,EAAY,MAAM,CAAA,CAAE,CAAC;AACxF,QAAA,OAAO,QAAQ;IACjB;;;AAIA,IAAA,IAAI;QACF,MAAMD,mBAAW,CACf,QAAQ,CAAC,WAAW,CAAC,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,CAAC,EACnDC,4BAAqB,EACrB,CAAA,+BAAA,EAAkC,QAAQ,CAAA,CAAA,CAAG,CAC9C;IACH;IAAE,OAAO,GAAQ,EAAE;;AAEjB,QAAA,MAAM,MAAM,GAAG,GAAG,EAAE,MAAM,IAAI,GAAG,EAAE,OAAO,IAAI,MAAM,CAAC,GAAG,CAAC;AACzD,QAAA,IACE,GAAG,EAAE,OAAO,KAAK,sBAAsB;YACvC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,sBAAsB,CAAC,CAAC,EAC/C;AACA,YAAA,MAAM,IAAI,kBAAkB,CAAC,MAAM,CAAC;QACtC;AACA,QAAA,MAAM,GAAG;IACX;IACA,OAAO,CAAC,GAAG,GAAG,CAAA,4BAAA,EAA+B,QAAQ,CAAA,SAAA,EAAY,MAAM,CAAA,CAAE,CAAC;AAE1E,IAAA,OAAO,QAAQ;AACjB;;;;;;"}