gewe-openclaw 2026.3.13 → 2026.3.23

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 (44) hide show
  1. package/README.md +455 -3
  2. package/index.ts +39 -1
  3. package/package.json +12 -1
  4. package/skills/gewe-agent-tools/SKILL.md +113 -0
  5. package/skills/gewe-channel-rules/SKILL.md +7 -0
  6. package/src/accounts.ts +51 -5
  7. package/src/api-tools.ts +1264 -0
  8. package/src/api.ts +37 -2
  9. package/src/binary-command.ts +65 -0
  10. package/src/channel-actions.ts +536 -0
  11. package/src/channel-allowlist.ts +150 -0
  12. package/src/channel-directory.ts +419 -0
  13. package/src/channel-status.ts +186 -0
  14. package/src/channel.ts +155 -58
  15. package/src/config-edit.ts +94 -0
  16. package/src/config-schema.ts +78 -3
  17. package/src/contacts-api.ts +113 -0
  18. package/src/delivery.ts +502 -62
  19. package/src/directory-cache.ts +164 -0
  20. package/src/gewe-account-api.ts +27 -0
  21. package/src/group-allowlist-tool.ts +242 -0
  22. package/src/group-binding-tool.ts +154 -0
  23. package/src/group-binding.ts +405 -0
  24. package/src/groups-api.ts +146 -0
  25. package/src/inbound-batch.ts +5 -2
  26. package/src/inbound.ts +248 -41
  27. package/src/media-server.ts +73 -93
  28. package/src/moments-api.ts +138 -0
  29. package/src/monitor.ts +81 -24
  30. package/src/onboarding.ts +9 -4
  31. package/src/openclaw-compat.ts +1070 -0
  32. package/src/pairing-store.ts +478 -0
  33. package/src/personal-api.ts +45 -0
  34. package/src/policy.ts +130 -22
  35. package/src/quote-context-cache.ts +97 -0
  36. package/src/reply-options.ts +101 -2
  37. package/src/s3.ts +1 -1
  38. package/src/send.ts +235 -16
  39. package/src/setup-wizard-types.ts +162 -0
  40. package/src/setup-wizard.ts +464 -0
  41. package/src/silk.ts +2 -1
  42. package/src/state-paths.ts +55 -14
  43. package/src/types.ts +66 -7
  44. package/src/xml.ts +158 -0
package/src/delivery.ts CHANGED
@@ -1,26 +1,85 @@
1
1
  import fs from "node:fs/promises";
2
+ import crypto from "node:crypto";
2
3
  import os from "node:os";
3
4
  import path from "node:path";
4
5
  import { fileURLToPath } from "node:url";
5
6
 
6
- import type { OpenClawConfig, ReplyPayload } from "openclaw/plugin-sdk";
7
- import { extractOriginalFilename, extensionForMime } from "openclaw/plugin-sdk";
7
+ import {
8
+ extensionForMime,
9
+ extractOriginalFilename,
10
+ type OpenClawConfig,
11
+ type ReplyPayload,
12
+ } from "./openclaw-compat.js";
13
+ import { runBinaryCommand, type BinaryCommandResult } from "./binary-command.js";
8
14
  import { CHANNEL_ID } from "./constants.js";
9
15
  import { getGeweRuntime } from "./runtime.js";
10
16
  import { resolveS3Config, uploadToS3 } from "./s3.js";
11
- import { ensureRustSilkBinary } from "./silk.js";
17
+ import { buildRustSilkEncodeArgs, ensureRustSilkBinary } from "./silk.js";
12
18
  import {
19
+ forwardFileGewe,
20
+ forwardImageGewe,
21
+ forwardLinkGewe,
22
+ forwardMiniAppGewe,
23
+ forwardVideoGewe,
24
+ sendAppMsgGewe,
25
+ sendEmojiGewe,
13
26
  sendFileGewe,
14
27
  sendImageGewe,
15
28
  sendLinkGewe,
29
+ sendMiniAppGewe,
30
+ sendNameCardGewe,
16
31
  sendTextGewe,
17
32
  sendVideoGewe,
18
33
  sendVoiceGewe,
34
+ revokeMessageGewe,
19
35
  } from "./send.js";
36
+ import { recallGeweQuoteReplyContext } from "./quote-context-cache.js";
20
37
  import type { GeweSendResult, ResolvedGeweAccount } from "./types.js";
21
38
 
22
39
  type GeweChannelData = {
23
40
  ats?: string;
41
+ appMsg?: {
42
+ appmsg: string;
43
+ };
44
+ quoteReply?: {
45
+ svrid?: string | number;
46
+ title?: string;
47
+ atWxid?: string;
48
+ partialText?: {
49
+ text?: string;
50
+ start?: string;
51
+ end?: string;
52
+ startIndex?: string | number;
53
+ endIndex?: string | number;
54
+ quoteMd5?: string;
55
+ };
56
+ };
57
+ emoji?: {
58
+ emojiMd5: string;
59
+ emojiSize: number;
60
+ };
61
+ nameCard?: {
62
+ nickName: string;
63
+ nameCardWxid: string;
64
+ };
65
+ miniApp?: {
66
+ miniAppId: string;
67
+ displayName: string;
68
+ pagePath: string;
69
+ coverImgUrl: string;
70
+ title: string;
71
+ userName: string;
72
+ };
73
+ revoke?: {
74
+ msgId: string | number;
75
+ newMsgId: string | number;
76
+ createTime: string | number;
77
+ };
78
+ forward?: {
79
+ kind: "image" | "video" | "file" | "link" | "miniApp";
80
+ xml: string;
81
+ coverImgUrl?: string;
82
+ };
24
83
  link?: {
25
84
  title: string;
26
85
  desc: string;
@@ -265,46 +324,115 @@ function resolveSilkArgs(params: {
265
324
  return next;
266
325
  }
267
326
 
268
- async function convertAudioToSilk(params: {
269
- account: ResolvedGeweAccount;
270
- sourcePath: string;
271
- }): Promise<{ buffer: Buffer; durationMs: number } | null> {
272
- const core = getGeweRuntime();
273
- const logger = core.logging.getChildLogger({ channel: CHANNEL_ID, module: "voice" });
274
- if (params.account.config.voiceAutoConvert === false) return null;
327
+ function trimPcmBuffer(params: {
328
+ buffer: Buffer;
329
+ sampleRate: number;
330
+ }): { buffer: Buffer; durationMs: number } {
331
+ let pcmBuffer = params.buffer;
332
+ const frameSamples = params.sampleRate % 50 === 0 ? params.sampleRate / 50 : 0; // 20ms frames
333
+ const frameBytes = frameSamples > 0 ? frameSamples * PCM_BYTES_PER_SAMPLE : 0;
334
+ if (frameBytes > 0 && pcmBuffer.length % frameBytes !== 0) {
335
+ const trimmedSize = pcmBuffer.length - (pcmBuffer.length % frameBytes);
336
+ if (trimmedSize <= 0) {
337
+ throw new Error("ffmpeg produced empty PCM after frame trim");
338
+ }
339
+ pcmBuffer = Buffer.from(pcmBuffer.subarray(0, trimmedSize));
340
+ }
341
+ if (!pcmBuffer.length) {
342
+ throw new Error("ffmpeg produced empty PCM");
343
+ }
344
+ return {
345
+ buffer: pcmBuffer,
346
+ durationMs: Math.max(
347
+ 1,
348
+ Math.round((pcmBuffer.length / (params.sampleRate * PCM_BYTES_PER_SAMPLE)) * 1000),
349
+ ),
350
+ };
351
+ }
275
352
 
276
- const sampleRate = resolveVoiceSampleRate(params.account);
277
- const ffmpegPath = params.account.config.voiceFfmpegPath?.trim() || DEFAULT_VOICE_FFMPEG;
278
- const fallbackArgs = [
279
- ["-i", "{input}", "-o", "{output}", "-rate", "{sampleRate}"],
280
- ["{input}", "{output}", "-rate", "{sampleRate}"],
281
- ["{input}", "{output}", "{sampleRate}"],
282
- ["{input}", "{output}"],
283
- ];
284
- const rustArgs = [
285
- "encode",
353
+ function formatBinaryCommandFailure(params: {
354
+ label: string;
355
+ result: BinaryCommandResult;
356
+ }): string {
357
+ if (params.result.timedOut) {
358
+ return `${params.label} timed out after ${DEFAULT_VOICE_TIMEOUT_MS}ms`;
359
+ }
360
+ const detail = params.result.stderr.trim();
361
+ if (detail) return detail;
362
+ if (params.result.signal) return `signal ${params.result.signal}`;
363
+ return `exit code ${params.result.code ?? "?"}`;
364
+ }
365
+
366
+ async function convertAudioToSilkViaPipe(params: {
367
+ sourcePath: string;
368
+ ffmpegPath: string;
369
+ silkPath: string;
370
+ argTemplates: string[][];
371
+ sampleRate: number;
372
+ }): Promise<{ buffer: Buffer; durationMs: number }> {
373
+ const ffmpegArgs = [
374
+ "-y",
286
375
  "-i",
287
- "{input}",
288
- "-o",
289
- "{output}",
290
- "--sample-rate",
291
- "{sampleRate}",
292
- "--tencent",
293
- "--quiet",
376
+ params.sourcePath,
377
+ "-ac",
378
+ "1",
379
+ "-ar",
380
+ String(params.sampleRate),
381
+ "-f",
382
+ "s16le",
383
+ "pipe:1",
294
384
  ];
295
- const customPath = params.account.config.voiceSilkPath?.trim();
296
- const customArgs =
297
- params.account.config.voiceSilkArgs?.length ? [params.account.config.voiceSilkArgs] : [];
298
- let silkPath = customPath || DEFAULT_VOICE_SILK;
299
- let argTemplates = customArgs.length ? customArgs : fallbackArgs;
300
- if (!customPath) {
301
- const rustSilk = await ensureRustSilkBinary(params.account);
302
- if (rustSilk) {
303
- silkPath = rustSilk;
304
- argTemplates = [rustArgs];
385
+ const ffmpegResult = await runBinaryCommand({
386
+ argv: [params.ffmpegPath, ...ffmpegArgs],
387
+ timeoutMs: DEFAULT_VOICE_TIMEOUT_MS,
388
+ });
389
+ if (ffmpegResult.code !== 0) {
390
+ throw new Error(
391
+ `ffmpeg pipe failed: ${formatBinaryCommandFailure({
392
+ label: "ffmpeg",
393
+ result: ffmpegResult,
394
+ })}`,
395
+ );
396
+ }
397
+
398
+ const trimmedPcm = trimPcmBuffer({
399
+ buffer: ffmpegResult.stdout,
400
+ sampleRate: params.sampleRate,
401
+ });
402
+
403
+ let lastError: string | null = null;
404
+ for (const template of params.argTemplates) {
405
+ const args = resolveSilkArgs({
406
+ template,
407
+ input: "-",
408
+ output: "-",
409
+ sampleRate: params.sampleRate,
410
+ });
411
+ const result = await runBinaryCommand({
412
+ argv: [params.silkPath, ...args],
413
+ timeoutMs: DEFAULT_VOICE_TIMEOUT_MS,
414
+ input: trimmedPcm.buffer,
415
+ });
416
+ if (result.code === 0 && result.stdout.length > 0) {
417
+ return { buffer: result.stdout, durationMs: trimmedPcm.durationMs };
305
418
  }
419
+ lastError =
420
+ result.code === 0
421
+ ? "encoder produced empty stdout"
422
+ : formatBinaryCommandFailure({ label: "silk", result });
306
423
  }
307
424
 
425
+ throw new Error(`silk encoder pipe failed (${params.silkPath}): ${lastError ?? "unknown error"}`);
426
+ }
427
+
428
+ async function convertAudioToSilkViaFiles(params: {
429
+ sourcePath: string;
430
+ ffmpegPath: string;
431
+ silkPath: string;
432
+ argTemplates: string[][];
433
+ sampleRate: number;
434
+ }): Promise<{ buffer: Buffer; durationMs: number }> {
435
+ const core = getGeweRuntime();
308
436
  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-gewe-voice-"));
309
437
  const pcmPath = path.join(tmpDir, "voice.pcm");
310
438
  const silkOutPath = path.join(tmpDir, "voice.silk");
@@ -317,13 +445,13 @@ async function convertAudioToSilk(params: {
317
445
  "-ac",
318
446
  "1",
319
447
  "-ar",
320
- String(sampleRate),
448
+ String(params.sampleRate),
321
449
  "-f",
322
450
  "s16le",
323
451
  pcmPath,
324
452
  ];
325
453
  const ffmpegResult = await core.system.runCommandWithTimeout(
326
- [ffmpegPath, ...ffmpegArgs],
454
+ [params.ffmpegPath, ...ffmpegArgs],
327
455
  { timeoutMs: DEFAULT_VOICE_TIMEOUT_MS },
328
456
  );
329
457
  if (ffmpegResult.code !== 0) {
@@ -332,33 +460,22 @@ async function convertAudioToSilk(params: {
332
460
  );
333
461
  }
334
462
 
335
- let pcmStat = await fs.stat(pcmPath);
336
- const frameSamples = sampleRate % 50 === 0 ? sampleRate / 50 : 0; // 20ms frames
337
- const frameBytes = frameSamples > 0 ? frameSamples * PCM_BYTES_PER_SAMPLE : 0;
338
- if (frameBytes > 0 && pcmStat.size % frameBytes !== 0) {
339
- const trimmedSize = pcmStat.size - (pcmStat.size % frameBytes);
340
- if (trimmedSize <= 0) {
341
- throw new Error("ffmpeg produced empty PCM after frame trim");
342
- }
343
- await fs.truncate(pcmPath, trimmedSize);
344
- pcmStat = await fs.stat(pcmPath);
345
- }
346
-
347
- const durationMs = Math.max(
348
- 1,
349
- Math.round((pcmStat.size / (sampleRate * PCM_BYTES_PER_SAMPLE)) * 1000),
350
- );
463
+ const trimmedPcm = trimPcmBuffer({
464
+ buffer: await fs.readFile(pcmPath),
465
+ sampleRate: params.sampleRate,
466
+ });
467
+ await fs.writeFile(pcmPath, trimmedPcm.buffer);
351
468
 
352
469
  let encoded = false;
353
470
  let lastError: string | null = null;
354
- for (const template of argTemplates) {
471
+ for (const template of params.argTemplates) {
355
472
  const args = resolveSilkArgs({
356
473
  template,
357
474
  input: pcmPath,
358
475
  output: silkOutPath,
359
- sampleRate,
476
+ sampleRate: params.sampleRate,
360
477
  });
361
- const result = await core.system.runCommandWithTimeout([silkPath, ...args], {
478
+ const result = await core.system.runCommandWithTimeout([params.silkPath, ...args], {
362
479
  timeoutMs: DEFAULT_VOICE_TIMEOUT_MS,
363
480
  });
364
481
  if (result.code === 0) {
@@ -372,7 +489,7 @@ async function convertAudioToSilk(params: {
372
489
  }
373
490
  if (!encoded) {
374
491
  throw new Error(
375
- `silk encoder failed (${silkPath}): ${lastError ?? "unknown error"}`,
492
+ `silk encoder failed (${params.silkPath}): ${lastError ?? "unknown error"}`,
376
493
  );
377
494
  }
378
495
 
@@ -381,12 +498,71 @@ async function convertAudioToSilk(params: {
381
498
  throw new Error("silk encoder produced empty output");
382
499
  }
383
500
 
384
- return { buffer, durationMs };
501
+ return { buffer, durationMs: trimmedPcm.durationMs };
502
+ } finally {
503
+ await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
504
+ }
505
+ }
506
+
507
+ export async function convertAudioToSilk(params: {
508
+ account: ResolvedGeweAccount;
509
+ sourcePath: string;
510
+ }): Promise<{ buffer: Buffer; durationMs: number } | null> {
511
+ const core = getGeweRuntime();
512
+ const logger = core.logging.getChildLogger({ channel: CHANNEL_ID, module: "voice" });
513
+ if (params.account.config.voiceAutoConvert === false) return null;
514
+
515
+ const sampleRate = resolveVoiceSampleRate(params.account);
516
+ const ffmpegPath = params.account.config.voiceFfmpegPath?.trim() || DEFAULT_VOICE_FFMPEG;
517
+ const fallbackArgs = [
518
+ ["-i", "{input}", "-o", "{output}", "-rate", "{sampleRate}"],
519
+ ["{input}", "{output}", "-rate", "{sampleRate}"],
520
+ ["{input}", "{output}", "{sampleRate}"],
521
+ ["{input}", "{output}"],
522
+ ];
523
+ const rustArgs = buildRustSilkEncodeArgs({
524
+ input: "{input}",
525
+ output: "{output}",
526
+ sampleRate,
527
+ });
528
+ const customPath = params.account.config.voiceSilkPath?.trim();
529
+ const customArgs =
530
+ params.account.config.voiceSilkArgs?.length ? [params.account.config.voiceSilkArgs] : [];
531
+ let silkPath = customPath || DEFAULT_VOICE_SILK;
532
+ let argTemplates = customArgs.length ? customArgs : fallbackArgs;
533
+ if (!customPath) {
534
+ const rustSilk = await ensureRustSilkBinary(params.account);
535
+ if (rustSilk) {
536
+ silkPath = rustSilk;
537
+ argTemplates = [rustArgs];
538
+ }
539
+ }
540
+
541
+ try {
542
+ if (params.account.config.voiceSilkPipe === true) {
543
+ try {
544
+ return await convertAudioToSilkViaPipe({
545
+ sourcePath: params.sourcePath,
546
+ ffmpegPath,
547
+ silkPath,
548
+ argTemplates,
549
+ sampleRate,
550
+ });
551
+ } catch (err) {
552
+ logger.warn?.(`gewe voice pipe convert failed, falling back to temp files: ${String(err)}`);
553
+ }
554
+ }
555
+
556
+ return await convertAudioToSilkViaFiles({
557
+ sourcePath: params.sourcePath,
558
+ ffmpegPath,
559
+ silkPath,
560
+ argTemplates,
561
+ sampleRate,
562
+ });
385
563
  } catch (err) {
386
564
  logger.warn?.(`gewe voice convert failed: ${String(err)}`);
387
565
  return null;
388
- } finally {
389
- await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
390
566
  }
391
567
  }
392
568
 
@@ -551,6 +727,69 @@ function normalizeMediaToken(raw: string): string {
551
727
  return value;
552
728
  }
553
729
 
730
+ function escapeXmlText(value: string): string {
731
+ return value
732
+ .replace(/&/g, "&amp;")
733
+ .replace(/</g, "&lt;")
734
+ .replace(/>/g, "&gt;")
735
+ .replace(/"/g, "&quot;")
736
+ .replace(/'/g, "&#39;");
737
+ }
738
+
739
+ function md5Hex(value: string): string {
740
+ return crypto.createHash("md5").update(value, "utf8").digest("hex");
741
+ }
742
+
743
+ function buildPartialQuoteXml(params?: {
744
+ text?: string;
745
+ start?: string;
746
+ end?: string;
747
+ startIndex?: string | number;
748
+ endIndex?: string | number;
749
+ quoteMd5?: string;
750
+ }): string {
751
+ if (!params) return "";
752
+ const text = params.text?.trim();
753
+ const start = (params.start?.trim() || text?.slice(0, 1) || "").trim();
754
+ const end = (params.end?.trim() || text?.slice(-1) || "").trim();
755
+ const quoteMd5 = (params.quoteMd5?.trim() || (text ? md5Hex(text) : "")).toLowerCase();
756
+ if (!start || !end || !quoteMd5) return "";
757
+
758
+ const startIndex =
759
+ params.startIndex != null && String(params.startIndex).trim()
760
+ ? String(params.startIndex).trim()
761
+ : "0";
762
+ const endIndex =
763
+ params.endIndex != null && String(params.endIndex).trim()
764
+ ? String(params.endIndex).trim()
765
+ : "0";
766
+
767
+ return `<partialtext><start>${escapeXmlText(start)}</start><end>${escapeXmlText(end)}</end><startindex>${escapeXmlText(startIndex)}</startindex><endindex>${escapeXmlText(endIndex)}</endindex><quotemd5>${escapeXmlText(quoteMd5)}</quotemd5></partialtext>`;
768
+ }
769
+
770
+ function buildQuoteReplyAppMsg(params: {
771
+ title: string;
772
+ svrid: string;
773
+ atWxid?: string;
774
+ partialText?: {
775
+ text?: string;
776
+ start?: string;
777
+ end?: string;
778
+ startIndex?: string | number;
779
+ endIndex?: string | number;
780
+ quoteMd5?: string;
781
+ };
782
+ }): string {
783
+ const safeTitle = escapeXmlText(params.title.trim() || "引用回复");
784
+ const safeSvrid = escapeXmlText(params.svrid.trim());
785
+ const safeAtWxid = params.atWxid?.trim() ? escapeXmlText(params.atWxid.trim()) : undefined;
786
+ const encodedMsgSource = safeAtWxid
787
+ ? `&lt;msgsource&gt;&lt;atuserlist&gt;${safeAtWxid}&lt;/atuserlist&gt;&lt;/msgsource&gt;`
788
+ : "";
789
+ const partialTextXml = buildPartialQuoteXml(params.partialText);
790
+ return `<appmsg><title>${safeTitle}</title><type>57</type><refermsg>${partialTextXml}<svrid>${safeSvrid}</svrid>${safeAtWxid ? `<msgsource>${encodedMsgSource}</msgsource>` : ""}</refermsg></appmsg>`;
791
+ }
792
+
554
793
  async function stageMedia(params: {
555
794
  account: ResolvedGeweAccount;
556
795
  cfg: OpenClawConfig;
@@ -714,6 +953,188 @@ export async function deliverGewePayload(params: {
714
953
  const mediaUrl =
715
954
  payload.mediaUrl?.trim() || payload.mediaUrls?.[0]?.trim() || "";
716
955
  const normalizedMediaUrl = normalizeMediaToken(mediaUrl);
956
+ const autoQuoteContext =
957
+ trimmedText && payload.replyToId?.trim() && !mediaUrl
958
+ ? recallGeweQuoteReplyContext({
959
+ accountId: account.accountId,
960
+ messageId: payload.replyToId,
961
+ })
962
+ : null;
963
+ const autoQuoteReplyEnabled = account.config.autoQuoteReply !== false;
964
+
965
+ if (geweData?.appMsg?.appmsg?.trim()) {
966
+ const result = await sendAppMsgGewe({
967
+ account,
968
+ toWxid,
969
+ appmsg: geweData.appMsg.appmsg.trim(),
970
+ });
971
+ core.channel.activity.record({
972
+ channel: CHANNEL_ID,
973
+ accountId: account.accountId,
974
+ direction: "outbound",
975
+ });
976
+ statusSink?.({ lastOutboundAt: Date.now() });
977
+ return result;
978
+ }
979
+
980
+ const quoteReplySvrid =
981
+ geweData?.quoteReply?.svrid != null
982
+ ? String(geweData.quoteReply.svrid).trim()
983
+ : payload.replyToId?.trim() || "";
984
+ const quoteReplyTitle = geweData?.quoteReply?.title?.trim() || trimmedText;
985
+ if (quoteReplySvrid && quoteReplyTitle && geweData?.quoteReply) {
986
+ const result = await sendAppMsgGewe({
987
+ account,
988
+ toWxid,
989
+ appmsg: buildQuoteReplyAppMsg({
990
+ svrid: quoteReplySvrid,
991
+ title: quoteReplyTitle,
992
+ atWxid: geweData.quoteReply.atWxid?.trim(),
993
+ partialText: geweData.quoteReply.partialText,
994
+ }),
995
+ });
996
+ core.channel.activity.record({
997
+ channel: CHANNEL_ID,
998
+ accountId: account.accountId,
999
+ direction: "outbound",
1000
+ });
1001
+ statusSink?.({ lastOutboundAt: Date.now() });
1002
+ return result;
1003
+ }
1004
+
1005
+ if (geweData?.emoji?.emojiMd5?.trim() && typeof geweData.emoji.emojiSize === "number") {
1006
+ const result = await sendEmojiGewe({
1007
+ account,
1008
+ toWxid,
1009
+ emojiMd5: geweData.emoji.emojiMd5.trim(),
1010
+ emojiSize: Math.floor(geweData.emoji.emojiSize),
1011
+ });
1012
+ core.channel.activity.record({
1013
+ channel: CHANNEL_ID,
1014
+ accountId: account.accountId,
1015
+ direction: "outbound",
1016
+ });
1017
+ statusSink?.({ lastOutboundAt: Date.now() });
1018
+ return result;
1019
+ }
1020
+
1021
+ if (geweData?.nameCard?.nickName?.trim() && geweData.nameCard.nameCardWxid?.trim()) {
1022
+ const result = await sendNameCardGewe({
1023
+ account,
1024
+ toWxid,
1025
+ nickName: geweData.nameCard.nickName.trim(),
1026
+ nameCardWxid: geweData.nameCard.nameCardWxid.trim(),
1027
+ });
1028
+ core.channel.activity.record({
1029
+ channel: CHANNEL_ID,
1030
+ accountId: account.accountId,
1031
+ direction: "outbound",
1032
+ });
1033
+ statusSink?.({ lastOutboundAt: Date.now() });
1034
+ return result;
1035
+ }
1036
+
1037
+ if (
1038
+ geweData?.miniApp?.miniAppId?.trim() &&
1039
+ geweData.miniApp.displayName?.trim() &&
1040
+ geweData.miniApp.pagePath?.trim() &&
1041
+ geweData.miniApp.coverImgUrl?.trim() &&
1042
+ geweData.miniApp.title?.trim() &&
1043
+ geweData.miniApp.userName?.trim()
1044
+ ) {
1045
+ const result = await sendMiniAppGewe({
1046
+ account,
1047
+ toWxid,
1048
+ miniAppId: geweData.miniApp.miniAppId.trim(),
1049
+ displayName: geweData.miniApp.displayName.trim(),
1050
+ pagePath: geweData.miniApp.pagePath.trim(),
1051
+ coverImgUrl: geweData.miniApp.coverImgUrl.trim(),
1052
+ title: geweData.miniApp.title.trim(),
1053
+ userName: geweData.miniApp.userName.trim(),
1054
+ });
1055
+ core.channel.activity.record({
1056
+ channel: CHANNEL_ID,
1057
+ accountId: account.accountId,
1058
+ direction: "outbound",
1059
+ });
1060
+ statusSink?.({ lastOutboundAt: Date.now() });
1061
+ return result;
1062
+ }
1063
+
1064
+ if (
1065
+ geweData?.revoke?.msgId != null &&
1066
+ geweData.revoke.newMsgId != null &&
1067
+ geweData.revoke.createTime != null
1068
+ ) {
1069
+ const result = await revokeMessageGewe({
1070
+ account,
1071
+ toWxid,
1072
+ msgId: String(geweData.revoke.msgId).trim(),
1073
+ newMsgId: String(geweData.revoke.newMsgId).trim(),
1074
+ createTime: String(geweData.revoke.createTime).trim(),
1075
+ });
1076
+ core.channel.activity.record({
1077
+ channel: CHANNEL_ID,
1078
+ accountId: account.accountId,
1079
+ direction: "outbound",
1080
+ });
1081
+ statusSink?.({ lastOutboundAt: Date.now() });
1082
+ return result;
1083
+ }
1084
+
1085
+ if (geweData?.forward?.kind && geweData.forward.xml?.trim()) {
1086
+ let result: GeweSendResult | null = null;
1087
+ switch (geweData.forward.kind) {
1088
+ case "image":
1089
+ result = await forwardImageGewe({
1090
+ account,
1091
+ toWxid,
1092
+ xml: geweData.forward.xml.trim(),
1093
+ });
1094
+ break;
1095
+ case "video":
1096
+ result = await forwardVideoGewe({
1097
+ account,
1098
+ toWxid,
1099
+ xml: geweData.forward.xml.trim(),
1100
+ });
1101
+ break;
1102
+ case "file":
1103
+ result = await forwardFileGewe({
1104
+ account,
1105
+ toWxid,
1106
+ xml: geweData.forward.xml.trim(),
1107
+ });
1108
+ break;
1109
+ case "link":
1110
+ result = await forwardLinkGewe({
1111
+ account,
1112
+ toWxid,
1113
+ xml: geweData.forward.xml.trim(),
1114
+ });
1115
+ break;
1116
+ case "miniApp":
1117
+ if (!geweData.forward.coverImgUrl?.trim()) {
1118
+ break;
1119
+ }
1120
+ result = await forwardMiniAppGewe({
1121
+ account,
1122
+ toWxid,
1123
+ xml: geweData.forward.xml.trim(),
1124
+ coverImgUrl: geweData.forward.coverImgUrl.trim(),
1125
+ });
1126
+ break;
1127
+ }
1128
+ if (result) {
1129
+ core.channel.activity.record({
1130
+ channel: CHANNEL_ID,
1131
+ accountId: account.accountId,
1132
+ direction: "outbound",
1133
+ });
1134
+ statusSink?.({ lastOutboundAt: Date.now() });
1135
+ return result;
1136
+ }
1137
+ }
717
1138
 
718
1139
  if (geweData?.link) {
719
1140
  const link = geweData.link;
@@ -738,6 +1159,25 @@ export async function deliverGewePayload(params: {
738
1159
  return result;
739
1160
  }
740
1161
 
1162
+ if (autoQuoteReplyEnabled && trimmedText && payload.replyToId?.trim() && !mediaUrl) {
1163
+ const result = await sendAppMsgGewe({
1164
+ account,
1165
+ toWxid,
1166
+ appmsg: buildQuoteReplyAppMsg({
1167
+ svrid: autoQuoteContext?.svrid ?? payload.replyToId.trim(),
1168
+ title: trimmedText,
1169
+ partialText: autoQuoteContext?.partialText,
1170
+ }),
1171
+ });
1172
+ core.channel.activity.record({
1173
+ channel: CHANNEL_ID,
1174
+ accountId: account.accountId,
1175
+ direction: "outbound",
1176
+ });
1177
+ statusSink?.({ lastOutboundAt: Date.now() });
1178
+ return result;
1179
+ }
1180
+
741
1181
  if (mediaUrl) {
742
1182
  const audioAsVoice = payload.audioAsVoice === true;
743
1183
  const forceFile = geweData?.forceFile === true;