@marshulll/openclaw-wecom 0.1.11 → 0.1.13

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marshulll/openclaw-wecom",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "type": "module",
5
5
  "description": "OpenClaw WeCom channel plugin (intelligent bot + internal app)",
6
6
  "author": "OpenClaw",
@@ -1,6 +1,6 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  import { XMLParser } from "fast-xml-parser";
3
- import { mkdir, readdir, rm, stat, unlink, writeFile } from "node:fs/promises";
3
+ import { mkdir, readdir, rm, stat, writeFile } from "node:fs/promises";
4
4
  import { tmpdir } from "node:os";
5
5
  import { join } from "node:path";
6
6
 
@@ -171,8 +171,13 @@ async function startAgentForApp(params: {
171
171
  chatId?: string;
172
172
  isGroup: boolean;
173
173
  messageText: string;
174
+ media?: {
175
+ type: "image" | "voice" | "video" | "file";
176
+ path: string;
177
+ url?: string;
178
+ } | null;
174
179
  }): Promise<void> {
175
- const { target, fromUser, chatId, isGroup, messageText } = params;
180
+ const { target, fromUser, chatId, isGroup, messageText, media } = params;
176
181
  const core = getWecomRuntime();
177
182
  const config = target.config;
178
183
  const account = target.account;
@@ -221,6 +226,14 @@ async function startAgentForApp(params: {
221
226
  OriginatingTo: `wecom:${peerId}`,
222
227
  });
223
228
 
229
+ if (media?.path) {
230
+ ctxPayload.MediaPath = media.path;
231
+ ctxPayload.MediaType = media.type;
232
+ if (media.url) {
233
+ ctxPayload.MediaUrl = media.url;
234
+ }
235
+ }
236
+
224
237
  await core.channel.session.recordInboundSession({
225
238
  storePath,
226
239
  sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
@@ -318,7 +331,7 @@ async function processAppMessage(params: {
318
331
  if (!fromUser) return;
319
332
 
320
333
  let messageText = "";
321
- let tempImagePath: string | null = null;
334
+ let mediaContext: { type: "image" | "voice" | "video" | "file"; path: string; url?: string } | null = null;
322
335
 
323
336
  if (msgType === "text") {
324
337
  messageText = String(msgObj?.Content ?? "");
@@ -337,17 +350,19 @@ async function processAppMessage(params: {
337
350
  if (maxBytes && media.buffer.length > maxBytes) {
338
351
  messageText = "[语音消息过大,未处理]\n\n请发送更短的语音消息。";
339
352
  } else {
340
- const ext = resolveExtFromContentType(media.contentType, "amr");
341
- const tempDir = resolveMediaTempDir(target);
342
- await mkdir(tempDir, { recursive: true });
343
- await cleanupMediaDir(
344
- tempDir,
345
- target.account.config.media?.retentionHours,
346
- target.account.config.media?.cleanupOnStart,
347
- );
348
- const tempVoicePath = join(tempDir, `voice-${Date.now()}-${Math.random().toString(36).slice(2)}.${ext}`);
349
- await writeFile(tempVoicePath, media.buffer);
350
- messageText = `[用户发送了一条语音消息,已保存到: ${tempVoicePath}]\n\n请根据语音内容回复用户。`;
353
+ const ext = resolveExtFromContentType(media.contentType, "amr");
354
+ const tempDir = resolveMediaTempDir(target);
355
+ await mkdir(tempDir, { recursive: true });
356
+ await cleanupMediaDir(
357
+ tempDir,
358
+ target.account.config.media?.retentionHours,
359
+ target.account.config.media?.cleanupOnStart,
360
+ );
361
+ const tempVoicePath = join(tempDir, `voice-${Date.now()}-${Math.random().toString(36).slice(2)}.${ext}`);
362
+ await writeFile(tempVoicePath, media.buffer);
363
+ mediaContext = { type: "voice", path: tempVoicePath };
364
+ logVerbose(target, `app voice saved (${media.buffer.length} bytes): ${tempVoicePath}`);
365
+ messageText = `[用户发送了一条语音消息]\n\n请根据语音内容回复用户。`;
351
366
  }
352
367
  } catch (err) {
353
368
  target.runtime.error?.(`wecom app voice download failed: ${String(err)}`);
@@ -380,17 +395,19 @@ async function processAppMessage(params: {
380
395
  if (maxBytes && buffer.length > maxBytes) {
381
396
  messageText = "[图片过大,未处理]\n\n请发送更小的图片。";
382
397
  } else {
383
- const ext = resolveExtFromContentType(contentType, "jpg");
384
- const tempDir = resolveMediaTempDir(target);
385
- await mkdir(tempDir, { recursive: true });
386
- await cleanupMediaDir(
387
- tempDir,
388
- target.account.config.media?.retentionHours,
389
- target.account.config.media?.cleanupOnStart,
390
- );
391
- tempImagePath = join(tempDir, `image-${Date.now()}-${Math.random().toString(36).slice(2)}.${ext}`);
392
- await writeFile(tempImagePath, buffer);
393
- messageText = `[用户发送了一张图片,已保存到: ${tempImagePath}]\n\n请根据图片内容回复用户。`;
398
+ const ext = resolveExtFromContentType(contentType, "jpg");
399
+ const tempDir = resolveMediaTempDir(target);
400
+ await mkdir(tempDir, { recursive: true });
401
+ await cleanupMediaDir(
402
+ tempDir,
403
+ target.account.config.media?.retentionHours,
404
+ target.account.config.media?.cleanupOnStart,
405
+ );
406
+ const tempImagePath = join(tempDir, `image-${Date.now()}-${Math.random().toString(36).slice(2)}.${ext}`);
407
+ await writeFile(tempImagePath, buffer);
408
+ mediaContext = { type: "image", path: tempImagePath, url: picUrl || undefined };
409
+ logVerbose(target, `app image saved (${buffer.length} bytes): ${tempImagePath}`);
410
+ messageText = "[用户发送了一张图片]\n\n请根据图片内容回复用户。";
394
411
  }
395
412
  } else {
396
413
  messageText = "[用户发送了一张图片,但下载失败]\n\n请告诉用户图片处理暂时不可用。";
@@ -417,17 +434,19 @@ async function processAppMessage(params: {
417
434
  if (maxBytes && media.buffer.length > maxBytes) {
418
435
  messageText = "[视频过大,未处理]\n\n请发送更小的视频。";
419
436
  } else {
420
- const ext = resolveExtFromContentType(media.contentType, "mp4");
421
- const tempDir = resolveMediaTempDir(target);
422
- await mkdir(tempDir, { recursive: true });
423
- await cleanupMediaDir(
424
- tempDir,
425
- target.account.config.media?.retentionHours,
426
- target.account.config.media?.cleanupOnStart,
427
- );
428
- const tempVideoPath = join(tempDir, `video-${Date.now()}-${Math.random().toString(36).slice(2)}.${ext}`);
429
- await writeFile(tempVideoPath, media.buffer);
430
- messageText = `[用户发送了一个视频文件,已保存到: ${tempVideoPath}]\n\n请根据视频内容回复用户。`;
437
+ const ext = resolveExtFromContentType(media.contentType, "mp4");
438
+ const tempDir = resolveMediaTempDir(target);
439
+ await mkdir(tempDir, { recursive: true });
440
+ await cleanupMediaDir(
441
+ tempDir,
442
+ target.account.config.media?.retentionHours,
443
+ target.account.config.media?.cleanupOnStart,
444
+ );
445
+ const tempVideoPath = join(tempDir, `video-${Date.now()}-${Math.random().toString(36).slice(2)}.${ext}`);
446
+ await writeFile(tempVideoPath, media.buffer);
447
+ mediaContext = { type: "video", path: tempVideoPath };
448
+ logVerbose(target, `app video saved (${media.buffer.length} bytes): ${tempVideoPath}`);
449
+ messageText = "[用户发送了一个视频文件]\n\n请根据视频内容回复用户。";
431
450
  }
432
451
  } catch (err) {
433
452
  target.runtime.error?.(`wecom app video download failed: ${String(err)}`);
@@ -446,18 +465,20 @@ async function processAppMessage(params: {
446
465
  if (maxBytes && media.buffer.length > maxBytes) {
447
466
  messageText = "[文件过大,未处理]\n\n请发送更小的文件。";
448
467
  } else {
449
- const ext = fileName.includes(".") ? fileName.split(".").pop() : resolveExtFromContentType(media.contentType, "bin");
450
- const tempDir = resolveMediaTempDir(target);
451
- await mkdir(tempDir, { recursive: true });
452
- await cleanupMediaDir(
453
- tempDir,
454
- target.account.config.media?.retentionHours,
455
- target.account.config.media?.cleanupOnStart,
456
- );
457
- const safeName = sanitizeFilename(fileName, `file-${Date.now()}.${ext}`);
458
- const tempFilePath = join(tempDir, safeName);
459
- await writeFile(tempFilePath, media.buffer);
460
- messageText = `[用户发送了一个文件: ${safeName},已保存到: ${tempFilePath}]\n\n请根据文件内容回复用户。`;
468
+ const ext = fileName.includes(".") ? fileName.split(".").pop() : resolveExtFromContentType(media.contentType, "bin");
469
+ const tempDir = resolveMediaTempDir(target);
470
+ await mkdir(tempDir, { recursive: true });
471
+ await cleanupMediaDir(
472
+ tempDir,
473
+ target.account.config.media?.retentionHours,
474
+ target.account.config.media?.cleanupOnStart,
475
+ );
476
+ const safeName = sanitizeFilename(fileName, `file-${Date.now()}.${ext}`);
477
+ const tempFilePath = join(tempDir, safeName);
478
+ await writeFile(tempFilePath, media.buffer);
479
+ mediaContext = { type: "file", path: tempFilePath };
480
+ logVerbose(target, `app file saved (${media.buffer.length} bytes): ${tempFilePath}`);
481
+ messageText = `[用户发送了一个文件: ${safeName}]\n\n请根据文件内容回复用户。`;
461
482
  }
462
483
  } catch (err) {
463
484
  target.runtime.error?.(`wecom app file download failed: ${String(err)}`);
@@ -490,6 +511,7 @@ async function processAppMessage(params: {
490
511
  chatId,
491
512
  isGroup,
492
513
  messageText,
514
+ media: mediaContext,
493
515
  });
494
516
  } catch (err) {
495
517
  target.runtime.error?.(`wecom app agent failed: ${String(err)}`);
@@ -503,10 +525,6 @@ async function processAppMessage(params: {
503
525
  } catch {
504
526
  // ignore
505
527
  }
506
- } finally {
507
- if (tempImagePath) {
508
- unlink(tempImagePath).catch(() => {});
509
- }
510
528
  }
511
529
  }
512
530
 
@@ -753,6 +753,10 @@ export async function handleWecomBotWebhook(params: {
753
753
  pruneStreams();
754
754
 
755
755
  const { req, res, targets } = params;
756
+ const botTargets = targets.filter((candidate) => shouldHandleBot(candidate.account));
757
+ if (botTargets.length === 0) {
758
+ return false;
759
+ }
756
760
  const query = resolveQueryParams(req);
757
761
  const timestamp = query.get("timestamp") ?? "";
758
762
  const nonce = query.get("nonce") ?? "";
@@ -770,7 +774,7 @@ export async function handleWecomBotWebhook(params: {
770
774
  return false;
771
775
  }
772
776
 
773
- const target = targets.find((candidate) => {
777
+ const target = botTargets.find((candidate) => {
774
778
  if (!shouldHandleBot(candidate.account)) return false;
775
779
  if (!candidate.account.configured || !candidate.account.token) return false;
776
780
  const ok = verifyWecomSignature({
@@ -823,9 +827,9 @@ export async function handleWecomBotWebhook(params: {
823
827
  return true;
824
828
  }
825
829
 
826
- const target = targets.find((candidate) => {
827
- if (!shouldHandleBot(candidate.account)) return false;
828
- if (!candidate.account.token) return false;
830
+ const target = botTargets.find((candidate) => {
831
+ if (!shouldHandleBot(candidate.account)) return false;
832
+ if (!candidate.account.token) return false;
829
833
  const ok = verifyWecomSignature({
830
834
  token: candidate.account.token,
831
835
  timestamp,