@dingxiang-me/openclaw-wechat 1.7.2 → 2.0.1

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 (65) hide show
  1. package/CHANGELOG.md +160 -0
  2. package/README.en.md +379 -11
  3. package/README.md +620 -12
  4. package/docs/channels/wecom.md +181 -3
  5. package/openclaw.plugin.json +148 -5
  6. package/package.json +9 -5
  7. package/src/core/delivery-router.js +2 -0
  8. package/src/core/stream-manager.js +13 -2
  9. package/src/core.js +96 -6
  10. package/src/wecom/account-config-core.js +2 -0
  11. package/src/wecom/account-config.js +12 -3
  12. package/src/wecom/agent-context.js +7 -1
  13. package/src/wecom/agent-dispatch-executor.js +13 -1
  14. package/src/wecom/agent-dispatch-fallback.js +23 -0
  15. package/src/wecom/agent-inbound-dispatch.js +1 -1
  16. package/src/wecom/agent-inbound-processor.js +33 -2
  17. package/src/wecom/agent-late-reply-runtime.js +31 -1
  18. package/src/wecom/agent-runtime-context.js +3 -0
  19. package/src/wecom/agent-webhook-handler.js +5 -0
  20. package/src/wecom/api-client-core.js +1 -1
  21. package/src/wecom/api-client-send-text.js +43 -20
  22. package/src/wecom/bot-context.js +7 -1
  23. package/src/wecom/bot-dispatch-fallback.js +34 -3
  24. package/src/wecom/bot-dispatch-handlers.js +47 -4
  25. package/src/wecom/bot-inbound-content.js +14 -6
  26. package/src/wecom/bot-inbound-dispatch-runtime.js +10 -0
  27. package/src/wecom/bot-inbound-executor-helpers.js +44 -11
  28. package/src/wecom/bot-inbound-executor.js +40 -0
  29. package/src/wecom/bot-long-connection-manager.js +983 -0
  30. package/src/wecom/bot-reply-runtime.js +36 -6
  31. package/src/wecom/bot-runtime-context.js +3 -0
  32. package/src/wecom/bot-state-store.js +4 -5
  33. package/src/wecom/bot-webhook-dispatch.js +7 -0
  34. package/src/wecom/bot-webhook-handler.js +5 -0
  35. package/src/wecom/callback-health-diagnostics.js +86 -0
  36. package/src/wecom/channel-config-schema.js +242 -0
  37. package/src/wecom/channel-plugin.js +162 -4
  38. package/src/wecom/channel-status-state.js +150 -0
  39. package/src/wecom/command-handlers.js +6 -0
  40. package/src/wecom/command-status-text.js +32 -3
  41. package/src/wecom/doc-client.js +537 -0
  42. package/src/wecom/doc-schema.js +380 -0
  43. package/src/wecom/doc-tool.js +833 -0
  44. package/src/wecom/outbound-active-stream.js +17 -10
  45. package/src/wecom/outbound-delivery.js +46 -0
  46. package/src/wecom/outbound-webhook-sender.js +39 -16
  47. package/src/wecom/plugin-account-policy-services.js +4 -1
  48. package/src/wecom/plugin-composition.js +2 -0
  49. package/src/wecom/plugin-constants.js +1 -1
  50. package/src/wecom/plugin-delivery-inbound-services.js +4 -0
  51. package/src/wecom/plugin-processing-deps.js +5 -0
  52. package/src/wecom/plugin-route-runtime-deps.js +2 -0
  53. package/src/wecom/plugin-services.js +37 -0
  54. package/src/wecom/register-runtime.js +20 -1
  55. package/src/wecom/request-parsers.js +1 -0
  56. package/src/wecom/route-registration.js +4 -1
  57. package/src/wecom/session-reset.js +168 -0
  58. package/src/wecom/target-utils.js +41 -5
  59. package/src/wecom/text-format.js +22 -5
  60. package/src/wecom/text-inbound-scheduler.js +1 -1
  61. package/src/wecom/thinking-parser.js +74 -0
  62. package/src/wecom/voice-transcription-process.js +145 -11
  63. package/src/wecom/voice-transcription.js +14 -2
  64. package/src/wecom/webhook-adapter-normalize.js +29 -0
  65. package/src/wecom/webhook-adapter.js +294 -59
@@ -1,4 +1,5 @@
1
1
  import {
2
+ collectWecomBotImageEntries,
2
3
  collectWecomBotImageUrls,
3
4
  dedupeUrlList,
4
5
  normalizeLowerToken,
@@ -7,7 +8,202 @@ import {
7
8
  normalizeWecomBotOutboundMediaUrls,
8
9
  } from "./webhook-adapter-normalize.js";
9
10
 
10
- export { collectWecomBotImageUrls, normalizeWecomBotOutboundMediaUrls };
11
+ export { collectWecomBotImageEntries, collectWecomBotImageUrls, normalizeWecomBotOutboundMediaUrls };
12
+
13
+ function dedupeMediaEntries(entries) {
14
+ const seen = new Map();
15
+ for (const rawEntry of Array.isArray(entries) ? entries : []) {
16
+ if (!rawEntry || typeof rawEntry !== "object") continue;
17
+ const url = normalizeToken(rawEntry.url);
18
+ if (!url) continue;
19
+ const aesKey = normalizeToken(rawEntry.aesKey);
20
+ const existing = seen.get(url);
21
+ if (!existing) {
22
+ seen.set(url, { url, aesKey });
23
+ continue;
24
+ }
25
+ if (!existing.aesKey && aesKey) {
26
+ seen.set(url, { url, aesKey });
27
+ }
28
+ }
29
+ return Array.from(seen.values());
30
+ }
31
+
32
+ function pickNestedValue(source, paths = []) {
33
+ if (!source || typeof source !== "object") return undefined;
34
+ for (const rawPath of Array.isArray(paths) ? paths : []) {
35
+ const segments = Array.isArray(rawPath)
36
+ ? rawPath
37
+ : String(rawPath ?? "")
38
+ .split(".")
39
+ .map((part) => String(part ?? "").trim())
40
+ .filter(Boolean);
41
+ if (segments.length === 0) continue;
42
+ let current = source;
43
+ let found = true;
44
+ for (const segment of segments) {
45
+ if (!current || typeof current !== "object" || !Object.hasOwn(current, segment)) {
46
+ found = false;
47
+ break;
48
+ }
49
+ current = current[segment];
50
+ }
51
+ if (found && current != null) {
52
+ return current;
53
+ }
54
+ }
55
+ return undefined;
56
+ }
57
+
58
+ function collectWecomBotMixedItems(payload) {
59
+ const directItems = pickNestedValue(payload, [
60
+ ["mixed", "msg_item"],
61
+ ["mixed", "msgItem"],
62
+ ["mixed", "items"],
63
+ ["mixed", "msgItems"],
64
+ ["msg_item"],
65
+ ["msgItem"],
66
+ ["items"],
67
+ ["attachments"],
68
+ ["attachment"],
69
+ ["message", "attachments"],
70
+ ["message", "items"],
71
+ ["message", "msg_item"],
72
+ ["message", "msgItem"],
73
+ ]);
74
+ if (Array.isArray(directItems)) {
75
+ return directItems.filter((item) => item && typeof item === "object");
76
+ }
77
+ return [];
78
+ }
79
+
80
+ function inferWecomBotItemType(item) {
81
+ const explicitType = normalizeLowerToken(
82
+ item?.msgtype ||
83
+ item?.msg_type ||
84
+ item?.msgType ||
85
+ item?.type ||
86
+ item?.message_type ||
87
+ item?.messageType ||
88
+ item?.kind,
89
+ );
90
+ if (explicitType) return explicitType;
91
+ if (item?.text || item?.content_type === "text" || item?.contentType === "text") return "text";
92
+ if (item?.image || item?.pic_url || item?.image_url || item?.imageUrl) return "image";
93
+ if (item?.voice || item?.voice_url || item?.voiceUrl || item?.audio || item?.audio_url || item?.audioUrl) return "voice";
94
+ if (
95
+ item?.file ||
96
+ item?.file_url ||
97
+ item?.fileUrl ||
98
+ item?.download_url ||
99
+ item?.downloadUrl ||
100
+ item?.filename ||
101
+ item?.file_name
102
+ ) {
103
+ return "file";
104
+ }
105
+ if (item?.link || item?.url) return "link";
106
+ if (item?.location || item?.latitude || item?.longitude) return "location";
107
+ return "";
108
+ }
109
+
110
+ function normalizeWecomBotMediaPayload(payload) {
111
+ if (!payload || typeof payload !== "object") return null;
112
+ const nestedMessage = pickNestedValue(payload, [["message"], ["msg"], ["data"], ["event", "message"]]);
113
+ const candidates = [payload];
114
+ if (nestedMessage && typeof nestedMessage === "object" && nestedMessage !== payload) {
115
+ candidates.unshift(nestedMessage);
116
+ }
117
+
118
+ for (const candidate of candidates) {
119
+ const msgType = normalizeLowerToken(
120
+ pickNestedValue(candidate, [
121
+ ["msgtype"],
122
+ ["msg_type"],
123
+ ["msgType"],
124
+ ["message_type"],
125
+ ["messageType"],
126
+ ["type"],
127
+ ]),
128
+ );
129
+ if (!msgType) continue;
130
+ return {
131
+ source: candidate,
132
+ msgType,
133
+ };
134
+ }
135
+ return null;
136
+ }
137
+
138
+ function extractWecomBotTextContent(textLike, fallbackContent = "") {
139
+ return normalizeToken(
140
+ textLike?.content ||
141
+ textLike?.text ||
142
+ textLike?.body ||
143
+ textLike?.message ||
144
+ fallbackContent,
145
+ );
146
+ }
147
+
148
+ function extractWecomBotVoicePayload(voiceLike, fallback = {}) {
149
+ const source = voiceLike && typeof voiceLike === "object" ? voiceLike : fallback;
150
+ return {
151
+ url: normalizeToken(
152
+ source?.url ||
153
+ source?.media_url ||
154
+ source?.mediaUrl ||
155
+ source?.download_url ||
156
+ source?.downloadUrl ||
157
+ source?.file_url ||
158
+ source?.fileUrl ||
159
+ source?.voice_url ||
160
+ source?.voiceUrl ||
161
+ source?.audio_url ||
162
+ source?.audioUrl,
163
+ ),
164
+ mediaId: normalizeToken(
165
+ source?.media_id ||
166
+ source?.mediaid ||
167
+ source?.mediaId ||
168
+ source?.id,
169
+ ),
170
+ contentType: normalizeToken(
171
+ source?.content_type ||
172
+ source?.contentType ||
173
+ source?.mime_type ||
174
+ source?.mimeType ||
175
+ source?.format,
176
+ ),
177
+ text: extractWecomBotTextContent(source, source?.content),
178
+ };
179
+ }
180
+
181
+ function extractWecomBotFilePayload(fileLike, fallback = {}) {
182
+ const source = fileLike && typeof fileLike === "object" ? fileLike : fallback;
183
+ return {
184
+ url: normalizeToken(
185
+ source?.url ||
186
+ source?.download_url ||
187
+ source?.downloadUrl ||
188
+ source?.media_url ||
189
+ source?.mediaUrl ||
190
+ source?.file_url ||
191
+ source?.fileUrl,
192
+ ),
193
+ name: normalizeToken(
194
+ source?.name ||
195
+ source?.filename ||
196
+ source?.file_name ||
197
+ source?.fileName ||
198
+ source?.title,
199
+ ),
200
+ aesKey: normalizeToken(
201
+ source?.aeskey ||
202
+ source?.aes_key ||
203
+ source?.aesKey,
204
+ ),
205
+ };
206
+ }
11
207
 
12
208
  export function buildWecomBotMixedPayload({ text = "", mediaUrl, mediaUrls } = {}) {
13
209
  const normalizedText = normalizeToken(text);
@@ -46,81 +242,113 @@ export function buildWecomBotMixedPayload({ text = "", mediaUrl, mediaUrls } = {
46
242
 
47
243
  export function parseWecomBotInboundMessage(payload) {
48
244
  if (!payload || typeof payload !== "object") return null;
49
- const msgType = normalizeLowerToken(payload.msgtype);
50
- if (!msgType) return null;
51
- const feedbackId = normalizeToken(payload?.feedback?.id || payload?.stream?.feedback?.id);
245
+ const normalizedPayload = normalizeWecomBotMediaPayload(payload);
246
+ if (!normalizedPayload) return null;
247
+ const source = normalizedPayload.source;
248
+ const msgType = normalizedPayload.msgType;
249
+ const feedbackId = normalizeToken(
250
+ pickNestedValue(source, [["feedback", "id"], ["stream", "feedback", "id"], ["stream", "feedbackId"], ["feedbackId"]]),
251
+ );
52
252
  if (msgType === "stream") {
53
253
  return {
54
254
  kind: "stream-refresh",
55
- streamId: normalizeToken(payload?.stream?.id),
255
+ streamId: normalizeToken(pickNestedValue(source, [["stream", "id"], ["streamId"]])),
56
256
  feedbackId,
57
257
  };
58
258
  }
59
259
 
60
- const msgId = normalizeToken(payload.msgid) || `wecom-bot-${Date.now()}`;
61
- const fromUser = normalizeToken(payload?.from?.userid);
62
- const chatType = normalizeLowerToken(payload.chattype || "single") || "single";
63
- const chatId = normalizeToken(payload.chatid);
64
- const responseUrl = normalizeToken(payload.response_url);
65
- const quote = normalizeQuotePayload(payload.quote);
260
+ const msgId =
261
+ normalizeToken(
262
+ pickNestedValue(source, [["msgid"], ["msg_id"], ["msgId"], ["message_id"], ["messageId"], ["id"]]),
263
+ ) || `wecom-bot-${Date.now()}`;
264
+ const fromUser = normalizeToken(
265
+ pickNestedValue(source, [
266
+ ["from", "userid"],
267
+ ["from", "user_id"],
268
+ ["from", "userId"],
269
+ ["sender", "userid"],
270
+ ["sender", "user_id"],
271
+ ["sender", "userId"],
272
+ ["sender", "id"],
273
+ ["userid"],
274
+ ["user_id"],
275
+ ["userId"],
276
+ ]),
277
+ );
278
+ const chatType =
279
+ normalizeLowerToken(
280
+ pickNestedValue(source, [["chattype"], ["chat_type"], ["chatType"]]) || "single",
281
+ ) || "single";
282
+ const chatId = normalizeToken(
283
+ pickNestedValue(source, [["chatid"], ["chat_id"], ["chatId"], ["conversation_id"], ["conversationId"]]),
284
+ );
285
+ const responseUrl = normalizeToken(
286
+ pickNestedValue(source, [["response_url"], ["responseUrl"], ["reply_url"], ["replyUrl"]]),
287
+ );
288
+ const quote = normalizeQuotePayload(pickNestedValue(source, [["quote"], ["quoted_message"], ["quotedMessage"]]));
66
289
  let content = "";
290
+ const imageEntries = [];
67
291
  const imageUrls = [];
68
292
  let fileUrl = "";
69
293
  let fileName = "";
294
+ let fileAesKey = "";
70
295
  let voiceUrl = "";
71
296
  let voiceMediaId = "";
72
297
  let voiceContentType = "";
73
298
 
74
299
  if (msgType === "text") {
75
- content = normalizeToken(payload?.text?.content);
300
+ content = extractWecomBotTextContent(
301
+ pickNestedValue(source, [["text"], ["message", "text"]]),
302
+ pickNestedValue(source, [["content"], ["text_content"], ["textContent"]]),
303
+ );
76
304
  } else if (msgType === "voice") {
77
- content = normalizeToken(payload?.voice?.content);
78
- voiceUrl = normalizeToken(
79
- payload?.voice?.url ||
80
- payload?.voice?.media_url ||
81
- payload?.voice?.download_url ||
82
- payload?.voice?.file_url,
305
+ const voicePayload = extractWecomBotVoicePayload(
306
+ pickNestedValue(source, [["voice"], ["audio"], ["message", "voice"], ["message", "audio"]]),
307
+ source,
83
308
  );
84
- voiceMediaId = normalizeToken(payload?.voice?.media_id || payload?.voice?.mediaid || payload?.voice?.id);
85
- voiceContentType = normalizeToken(payload?.voice?.content_type || payload?.voice?.mime_type || payload?.voice?.format);
309
+ content = voicePayload.text;
310
+ voiceUrl = voicePayload.url;
311
+ voiceMediaId = voicePayload.mediaId;
312
+ voiceContentType = voicePayload.contentType;
86
313
  } else if (msgType === "link") {
87
- const title = normalizeToken(payload?.link?.title);
88
- const description = normalizeToken(payload?.link?.description);
89
- const url = normalizeToken(payload?.link?.url);
314
+ const linkPayload = pickNestedValue(source, [["link"], ["message", "link"]]) || source;
315
+ const title = normalizeToken(linkPayload?.title);
316
+ const description = normalizeToken(linkPayload?.description);
317
+ const url = normalizeToken(linkPayload?.url);
90
318
  content = [title ? `[链接] ${title}` : "", description, url].filter(Boolean).join("\n").trim();
91
319
  } else if (msgType === "location") {
92
- const latitude = normalizeToken(payload?.location?.latitude);
93
- const longitude = normalizeToken(payload?.location?.longitude);
94
- const name = normalizeToken(payload?.location?.name || payload?.location?.label);
320
+ const locationPayload = pickNestedValue(source, [["location"], ["message", "location"]]) || source;
321
+ const latitude = normalizeToken(locationPayload?.latitude);
322
+ const longitude = normalizeToken(locationPayload?.longitude);
323
+ const name = normalizeToken(locationPayload?.name || locationPayload?.label);
95
324
  content = name ? `[位置] ${name} (${latitude}, ${longitude})` : `[位置] ${latitude}, ${longitude}`;
96
325
  } else if (msgType === "image") {
97
- imageUrls.push(...collectWecomBotImageUrls(payload?.image));
326
+ const topLevelImageEntries = collectWecomBotImageEntries(
327
+ pickNestedValue(source, [["image"], ["message", "image"]]) || source,
328
+ );
329
+ imageEntries.push(...topLevelImageEntries);
330
+ imageUrls.push(...topLevelImageEntries.map((entry) => entry.url));
98
331
  content = "[图片]";
99
332
  } else if (msgType === "mixed") {
100
- const items = Array.isArray(payload?.mixed?.msg_item) ? payload.mixed.msg_item : [];
333
+ const items = collectWecomBotMixedItems(source);
101
334
  const parts = [];
102
335
  for (const item of items) {
103
- const itemType = normalizeLowerToken(item?.msgtype);
336
+ const itemType = inferWecomBotItemType(item);
104
337
  if (itemType === "text") {
105
- const text = normalizeToken(item?.text?.content);
338
+ const text = extractWecomBotTextContent(item?.text, item?.content);
106
339
  if (text) parts.push(text);
107
340
  } else if (itemType === "image") {
108
- const itemImageUrls = collectWecomBotImageUrls(item?.image);
109
- if (itemImageUrls.length > 0) {
110
- imageUrls.push(...itemImageUrls);
341
+ const itemImageEntries = collectWecomBotImageEntries(item?.image || item);
342
+ if (itemImageEntries.length > 0) {
343
+ imageEntries.push(...itemImageEntries);
344
+ imageUrls.push(...itemImageEntries.map((entry) => entry.url));
111
345
  parts.push("[图片]");
112
346
  }
113
347
  } else if (itemType === "voice") {
114
- const itemVoiceUrl = normalizeToken(
115
- item?.voice?.url ||
116
- item?.voice?.media_url ||
117
- item?.voice?.download_url ||
118
- item?.voice?.file_url,
119
- );
120
- const itemVoiceMediaId = normalizeToken(item?.voice?.media_id || item?.voice?.mediaid || item?.voice?.id);
121
- const itemVoiceContentType = normalizeToken(
122
- item?.voice?.content_type || item?.voice?.mime_type || item?.voice?.format,
123
- );
348
+ const voicePayload = extractWecomBotVoicePayload(item?.voice || item?.audio, item);
349
+ const itemVoiceUrl = voicePayload.url;
350
+ const itemVoiceMediaId = voicePayload.mediaId;
351
+ const itemVoiceContentType = voicePayload.contentType;
124
352
  if (itemVoiceUrl) {
125
353
  voiceUrl = voiceUrl || itemVoiceUrl;
126
354
  voiceMediaId = voiceMediaId || itemVoiceMediaId;
@@ -128,29 +356,27 @@ export function parseWecomBotInboundMessage(payload) {
128
356
  parts.push("[语音]");
129
357
  }
130
358
  } else if (itemType === "file") {
131
- const itemFileUrl = normalizeToken(
132
- item?.file?.url ||
133
- item?.file?.download_url ||
134
- item?.file?.media_url ||
135
- item?.file?.file_url,
136
- );
137
- const itemFileName = normalizeToken(item?.file?.name || item?.file?.filename);
359
+ const filePayload = extractWecomBotFilePayload(item?.file, item);
360
+ const itemFileUrl = filePayload.url;
361
+ const itemFileName = filePayload.name;
362
+ const itemFileAesKey = filePayload.aesKey;
138
363
  if (itemFileUrl || itemFileName) {
139
364
  fileUrl = fileUrl || itemFileUrl;
140
365
  fileName = fileName || itemFileName;
366
+ fileAesKey = fileAesKey || itemFileAesKey;
141
367
  const displayName = itemFileName || itemFileUrl || "附件";
142
368
  parts.push(`[文件] ${displayName}`);
143
369
  }
144
370
  } else if (itemType === "link") {
145
- const title = normalizeToken(item?.link?.title);
146
- const description = normalizeToken(item?.link?.description);
147
- const url = normalizeToken(item?.link?.url);
371
+ const title = normalizeToken(item?.link?.title || item?.title);
372
+ const description = normalizeToken(item?.link?.description || item?.description);
373
+ const url = normalizeToken(item?.link?.url || item?.url);
148
374
  const linkText = [title ? `[链接] ${title}` : "", description, url].filter(Boolean).join("\n").trim();
149
375
  if (linkText) parts.push(linkText);
150
376
  } else if (itemType === "location") {
151
- const latitude = normalizeToken(item?.location?.latitude);
152
- const longitude = normalizeToken(item?.location?.longitude);
153
- const name = normalizeToken(item?.location?.name || item?.location?.label);
377
+ const latitude = normalizeToken(item?.location?.latitude || item?.latitude);
378
+ const longitude = normalizeToken(item?.location?.longitude || item?.longitude);
379
+ const name = normalizeToken(item?.location?.name || item?.location?.label || item?.name || item?.label);
154
380
  const locationText = name ? `[位置] ${name} (${latitude}, ${longitude})` : `[位置] ${latitude}, ${longitude}`;
155
381
  if (locationText.trim() !== "[位置] ,") {
156
382
  parts.push(locationText);
@@ -159,14 +385,21 @@ export function parseWecomBotInboundMessage(payload) {
159
385
  }
160
386
  content = parts.join("\n").trim();
161
387
  } else if (msgType === "file") {
162
- fileUrl = normalizeToken(payload?.file?.url);
163
- fileName = normalizeToken(payload?.file?.name || payload?.file?.filename);
388
+ const filePayload = extractWecomBotFilePayload(
389
+ pickNestedValue(source, [["file"], ["message", "file"], ["attachment"], ["document"]]),
390
+ source,
391
+ );
392
+ fileUrl = filePayload.url;
393
+ fileName = filePayload.name;
394
+ fileAesKey = filePayload.aesKey;
164
395
  const displayName = fileName || fileUrl || "附件";
165
396
  content = `[文件] ${displayName}`;
166
397
  } else if (msgType === "event") {
167
398
  return {
168
399
  kind: "event",
169
- eventType: normalizeToken(payload?.event?.event_type || payload?.event),
400
+ eventType: normalizeToken(
401
+ pickNestedValue(source, [["event", "event_type"], ["event", "eventType"], ["event"], ["event_type"], ["eventType"]]),
402
+ ),
170
403
  fromUser,
171
404
  };
172
405
  } else {
@@ -197,8 +430,10 @@ export function parseWecomBotInboundMessage(payload) {
197
430
  responseUrl,
198
431
  content,
199
432
  imageUrls: dedupeUrlList(imageUrls),
433
+ imageEntries: dedupeMediaEntries(imageEntries),
200
434
  fileUrl,
201
435
  fileName,
436
+ fileAesKey,
202
437
  voiceUrl,
203
438
  voiceMediaId,
204
439
  voiceContentType,