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.
- package/README.md +455 -3
- package/index.ts +39 -1
- package/package.json +12 -1
- package/skills/gewe-agent-tools/SKILL.md +113 -0
- package/skills/gewe-channel-rules/SKILL.md +7 -0
- package/src/accounts.ts +51 -5
- package/src/api-tools.ts +1264 -0
- package/src/api.ts +37 -2
- package/src/binary-command.ts +65 -0
- package/src/channel-actions.ts +536 -0
- package/src/channel-allowlist.ts +150 -0
- package/src/channel-directory.ts +419 -0
- package/src/channel-status.ts +186 -0
- package/src/channel.ts +155 -58
- package/src/config-edit.ts +94 -0
- package/src/config-schema.ts +78 -3
- package/src/contacts-api.ts +113 -0
- package/src/delivery.ts +502 -62
- package/src/directory-cache.ts +164 -0
- package/src/gewe-account-api.ts +27 -0
- package/src/group-allowlist-tool.ts +242 -0
- package/src/group-binding-tool.ts +154 -0
- package/src/group-binding.ts +405 -0
- package/src/groups-api.ts +146 -0
- package/src/inbound-batch.ts +5 -2
- package/src/inbound.ts +248 -41
- package/src/media-server.ts +73 -93
- package/src/moments-api.ts +138 -0
- package/src/monitor.ts +81 -24
- package/src/onboarding.ts +9 -4
- package/src/openclaw-compat.ts +1070 -0
- package/src/pairing-store.ts +478 -0
- package/src/personal-api.ts +45 -0
- package/src/policy.ts +130 -22
- package/src/quote-context-cache.ts +97 -0
- package/src/reply-options.ts +101 -2
- package/src/s3.ts +1 -1
- package/src/send.ts +235 -16
- package/src/setup-wizard-types.ts +162 -0
- package/src/setup-wizard.ts +464 -0
- package/src/silk.ts +2 -1
- package/src/state-paths.ts +55 -14
- package/src/types.ts +66 -7
- 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
|
|
7
|
-
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
}):
|
|
272
|
-
|
|
273
|
-
const
|
|
274
|
-
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
288
|
-
"-
|
|
289
|
-
"
|
|
290
|
-
"
|
|
291
|
-
|
|
292
|
-
"
|
|
293
|
-
"
|
|
376
|
+
params.sourcePath,
|
|
377
|
+
"-ac",
|
|
378
|
+
"1",
|
|
379
|
+
"-ar",
|
|
380
|
+
String(params.sampleRate),
|
|
381
|
+
"-f",
|
|
382
|
+
"s16le",
|
|
383
|
+
"pipe:1",
|
|
294
384
|
];
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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, "&")
|
|
733
|
+
.replace(/</g, "<")
|
|
734
|
+
.replace(/>/g, ">")
|
|
735
|
+
.replace(/"/g, """)
|
|
736
|
+
.replace(/'/g, "'");
|
|
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
|
+
? `<msgsource><atuserlist>${safeAtWxid}</atuserlist></msgsource>`
|
|
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;
|