@agent-wechat/agent-wechat 1.0.0-beta.7 → 1.0.0-beta.9
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 +22 -0
- package/dist/index.js +950 -182
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -110,7 +110,7 @@ import { constants } from "node:fs";
|
|
|
110
110
|
import { access, lstat, readFile, realpath } from "node:fs/promises";
|
|
111
111
|
import { spawn, execFile } from "node:child_process";
|
|
112
112
|
import { promisify } from "node:util";
|
|
113
|
-
import { delimiter, dirname, isAbsolute, join } from "node:path";
|
|
113
|
+
import { delimiter, dirname, isAbsolute, join, toNamespacedPath } from "node:path";
|
|
114
114
|
import { createConnection } from "node:net";
|
|
115
115
|
|
|
116
116
|
// ../shared/dist/native.generated.js
|
|
@@ -146,13 +146,13 @@ var NativeApiError = class extends Error {
|
|
|
146
146
|
}
|
|
147
147
|
};
|
|
148
148
|
function validateNativeBaseUrl(input) {
|
|
149
|
-
const
|
|
150
|
-
const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(
|
|
151
|
-
if (
|
|
149
|
+
const url2 = new URL(input);
|
|
150
|
+
const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(url2.hostname.toLowerCase());
|
|
151
|
+
if (url2.username || url2.password || url2.search || url2.hash || !["http:", "https:"].includes(url2.protocol) || url2.protocol === "http:" && !loopback)
|
|
152
152
|
throw new Error("Native API requires verified HTTPS or a loopback HTTP endpoint, without embedded credentials");
|
|
153
|
-
if (!
|
|
154
|
-
|
|
155
|
-
return
|
|
153
|
+
if (!url2.pathname.endsWith("/"))
|
|
154
|
+
url2.pathname += "/";
|
|
155
|
+
return url2;
|
|
156
156
|
}
|
|
157
157
|
var id = (value) => {
|
|
158
158
|
if (typeof value !== "string" || !value || value === "." || value === ".." || value.length > 4096 || /[\u0000-\u001f\u007f]/.test(value))
|
|
@@ -283,8 +283,24 @@ var NativeWeChatClient = class {
|
|
|
283
283
|
throw new Error("Invalid login idempotency key");
|
|
284
284
|
return this.request(`v1/accounts/${id(accountId)}/auth/login`, { method: "POST", headers: key ? { "Idempotency-Key": key } : void 0, body: JSON.stringify(options) });
|
|
285
285
|
}
|
|
286
|
-
operation(accountId, operationId) {
|
|
287
|
-
return this.request(`v1/accounts/${id(accountId)}/operations/${id(operationId)}
|
|
286
|
+
operation(accountId, operationId, signal) {
|
|
287
|
+
return this.request(`v1/accounts/${id(accountId)}/operations/${id(operationId)}`, { signal });
|
|
288
|
+
}
|
|
289
|
+
/** Submit once with a stable key; inspect the returned operation after uncertainty. */
|
|
290
|
+
async recallMessage(accountId, messageId, serverId, idempotencyKey, signal) {
|
|
291
|
+
if (typeof serverId !== "string" || !/^[1-9]\d{0,19}$/.test(serverId) || BigInt(serverId) > 18446744073709551615n)
|
|
292
|
+
throw new Error("Invalid exact recall server ID");
|
|
293
|
+
if (typeof idempotencyKey !== "string" || !idempotencyKey || idempotencyKey.length > 200 || /[\r\n]/.test(idempotencyKey))
|
|
294
|
+
throw new Error("Invalid recall idempotency key");
|
|
295
|
+
const operation = await this.request(`v1/accounts/${id(accountId)}/messages/${id(messageId)}/recall`, {
|
|
296
|
+
method: "POST",
|
|
297
|
+
headers: { "Idempotency-Key": idempotencyKey },
|
|
298
|
+
body: JSON.stringify({ serverId }),
|
|
299
|
+
signal
|
|
300
|
+
});
|
|
301
|
+
if (!operation || operation.accountId !== accountId || operation.kind !== "message_recall" || typeof operation.id !== "string" || !operation.id || typeof operation.state !== "string")
|
|
302
|
+
throw new Error("Native recall operation identity is invalid");
|
|
303
|
+
return operation;
|
|
288
304
|
}
|
|
289
305
|
cancelOperation(accountId, operationId) {
|
|
290
306
|
return this.request(`v1/accounts/${id(accountId)}/operations/${id(operationId)}`, { method: "DELETE" });
|
|
@@ -395,14 +411,19 @@ var NativeWeChatClient = class {
|
|
|
395
411
|
}
|
|
396
412
|
return result;
|
|
397
413
|
}
|
|
398
|
-
sendText(accountId, threadId2,
|
|
414
|
+
sendText(accountId, threadId2, text3, idempotencyKey) {
|
|
399
415
|
if (!idempotencyKey || /[\r\n]/.test(idempotencyKey))
|
|
400
416
|
throw new Error("An idempotency key is required");
|
|
401
|
-
return this.request(`v1/accounts/${id(accountId)}/sends`, { method: "POST", headers: { "Idempotency-Key": idempotencyKey }, body: JSON.stringify({ threadId: threadId2, text }) });
|
|
417
|
+
return this.request(`v1/accounts/${id(accountId)}/sends`, { method: "POST", headers: { "Idempotency-Key": idempotencyKey }, body: JSON.stringify({ threadId: threadId2, text: text3 }) });
|
|
402
418
|
}
|
|
403
419
|
send(accountId, sendId, signal) {
|
|
404
420
|
return this.request(`v1/accounts/${id(accountId)}/sends/${id(sendId)}`, { signal });
|
|
405
421
|
}
|
|
422
|
+
sendShare(accountId, input, key, signal) {
|
|
423
|
+
if (!key || key.length > 200 || /[\u0000-\u001f\u007f]/.test(key))
|
|
424
|
+
throw new Error("An idempotency key is required");
|
|
425
|
+
return this.request(`v1/accounts/${id(accountId)}/sends`, { method: "POST", headers: { "Idempotency-Key": key }, body: JSON.stringify(input), signal });
|
|
426
|
+
}
|
|
406
427
|
sendByKey(accountId, key) {
|
|
407
428
|
if (!key || key.length > 200)
|
|
408
429
|
throw new Error("Invalid idempotency key");
|
|
@@ -411,11 +432,11 @@ var NativeWeChatClient = class {
|
|
|
411
432
|
cancelSend(accountId, sendId) {
|
|
412
433
|
return this.request(`v1/accounts/${id(accountId)}/sends/${id(sendId)}/cancel`, { method: "POST", body: "{}" });
|
|
413
434
|
}
|
|
414
|
-
async upload(accountId, bytes,
|
|
415
|
-
if (!bytes.length || bytes.length > NATIVE_MAX_SOURCE_BYTES || !
|
|
435
|
+
async upload(accountId, bytes, name2, contentType = "application/octet-stream", signal) {
|
|
436
|
+
if (!bytes.length || bytes.length > NATIVE_MAX_SOURCE_BYTES || !name2 || name2.length > 255 || /[\r\n/\\]/.test(name2))
|
|
416
437
|
throw new Error("Invalid bounded media upload");
|
|
417
438
|
const form = new FormData();
|
|
418
|
-
form.set("file", new Blob([new Uint8Array(bytes).buffer], { type: contentType }),
|
|
439
|
+
form.set("file", new Blob([new Uint8Array(bytes).buffer], { type: contentType }), name2);
|
|
419
440
|
return this.request(`v1/accounts/${id(accountId)}/uploads`, { method: "POST", body: form, signal });
|
|
420
441
|
}
|
|
421
442
|
sendMedia(accountId, input, key) {
|
|
@@ -448,15 +469,16 @@ var NativeWeChatClient = class {
|
|
|
448
469
|
}
|
|
449
470
|
async mediaContent(accountId, assetId, options = {}) {
|
|
450
471
|
const max = options.maxBytes ?? 128 * 1024 * 1024;
|
|
451
|
-
if (!Number.isInteger(max) || max < 1 || max > 128 * 1024 * 1024 || options.range && !/^bytes=(?:\d+-\d*|-\d+)$/.test(options.range))
|
|
472
|
+
if (!Number.isInteger(max) || max < 1 || max > 128 * 1024 * 1024 || options.range && !/^bytes=(?:\d+-\d*|-\d+)$/.test(options.range) || options.representation != null && !["original", "preview"].includes(options.representation))
|
|
452
473
|
throw new Error("Invalid bounded content request");
|
|
453
|
-
const
|
|
474
|
+
const query = options.representation === "preview" ? "?representation=preview" : "";
|
|
475
|
+
const response = await this.response(`v1/accounts/${id(accountId)}/media/${id(assetId)}/content${query}`, { signal: options.signal, headers: options.range ? { Range: options.range } : {} });
|
|
454
476
|
const length = response.headers.get("content-length");
|
|
455
477
|
if (length && (!/^\d+$/.test(length) || Number(length) > max)) {
|
|
456
478
|
await response.body?.cancel();
|
|
457
479
|
throw new Error("Native media response exceeds its bound");
|
|
458
480
|
}
|
|
459
|
-
return { bytes: await this.readBounded(response, max), contentType: response.headers.get("content-type") ?? "application/octet-stream", etag: response.headers.get("etag"), contentRange: response.headers.get("content-range"), status: response.status };
|
|
481
|
+
return { bytes: await this.readBounded(response, max), contentType: response.headers.get("content-type") ?? "application/octet-stream", etag: response.headers.get("etag"), contentRange: response.headers.get("content-range"), sourceSha256: response.headers.get("x-source-sha256"), status: response.status };
|
|
460
482
|
}
|
|
461
483
|
markRead(accountId, threadId2, expectedVersion) {
|
|
462
484
|
if (typeof expectedVersion !== "string" || !expectedVersion || expectedVersion.length > 128)
|
|
@@ -576,11 +598,22 @@ function requireNativeIdentity(value) {
|
|
|
576
598
|
}
|
|
577
599
|
}
|
|
578
600
|
function nativeChildEnvironment(token, environment = process.env) {
|
|
579
|
-
return Object.fromEntries(["PATH", "HOME", "XDG_CONFIG_HOME", "XDG_DATA_HOME", "TMPDIR", "TMP", "TEMP", "LANG", "LC_ALL", "TZ", "SSL_CERT_FILE", "SSL_CERT_DIR"].filter((
|
|
601
|
+
return Object.fromEntries(["PATH", "HOME", "USERPROFILE", "APPDATA", "LOCALAPPDATA", "SystemRoot", "WINDIR", "XDG_CONFIG_HOME", "XDG_DATA_HOME", "TMPDIR", "TMP", "TEMP", "LANG", "LC_ALL", "TZ", "SSL_CERT_FILE", "SSL_CERT_DIR"].filter((name2) => environment[name2] !== void 0 && environment[name2] !== token).map((name2) => [name2, environment[name2]]));
|
|
580
602
|
}
|
|
581
603
|
function nativeHostTarget(platform = process.platform, arch = process.arch) {
|
|
582
|
-
|
|
583
|
-
|
|
604
|
+
const cpu = arch === "arm64" ? "aarch64" : "x86_64";
|
|
605
|
+
const suffix = { linux: "unknown-linux-gnu", darwin: "apple-darwin", win32: "pc-windows-msvc" }[platform];
|
|
606
|
+
if (!suffix || !["x64", "arm64"].includes(arch) || platform === "darwin" && arch !== "arm64") throw new Error("native_binary_incompatible: unsupported host platform; use external mode for remote daemons");
|
|
607
|
+
return { platform, arch, rustTarget: `${cpu}-${suffix}`, key: `${platform}-${arch}${platform === "linux" ? "-gnu" : ""}`, executable: platform === "win32" ? "wx.exe" : "wx" };
|
|
608
|
+
}
|
|
609
|
+
function nativeExecutableFormat(bytes, platform = process.platform) {
|
|
610
|
+
if (platform === "linux") return bytes.subarray(0, 4).equals(Buffer.from([127, 69, 76, 70]));
|
|
611
|
+
if (platform === "darwin") return bytes.subarray(0, 4).equals(Buffer.from([207, 250, 237, 254]));
|
|
612
|
+
if (platform === "win32" && bytes.length >= 64 && bytes[0] === 77 && bytes[1] === 90) {
|
|
613
|
+
const offset = bytes.readUInt32LE(60);
|
|
614
|
+
return offset >= 64 && offset <= bytes.length - 4 && bytes.subarray(offset, offset + 4).equals(Buffer.from([80, 69, 0, 0]));
|
|
615
|
+
}
|
|
616
|
+
return false;
|
|
584
617
|
}
|
|
585
618
|
async function resolveNativeExecutable(explicit, path = process.env.PATH ?? "", environment = nativeChildEnvironment("")) {
|
|
586
619
|
const target = nativeHostTarget();
|
|
@@ -589,40 +622,48 @@ async function resolveNativeExecutable(explicit, path = process.env.PATH ?? "",
|
|
|
589
622
|
if (!candidate) {
|
|
590
623
|
for (const directory of path.split(delimiter).filter(isAbsolute)) {
|
|
591
624
|
try {
|
|
592
|
-
await access(join(directory,
|
|
593
|
-
candidate = join(directory,
|
|
625
|
+
await access(join(directory, target.executable), constants.X_OK);
|
|
626
|
+
candidate = join(directory, target.executable);
|
|
594
627
|
break;
|
|
595
628
|
} catch {
|
|
596
629
|
}
|
|
630
|
+
if (process.platform === "win32") {
|
|
631
|
+
const payload = join(directory, "node_modules", "@agent-wechat", "cli", "scripts", "native-launcher.mjs");
|
|
632
|
+
try {
|
|
633
|
+
await access(payload);
|
|
634
|
+
candidate = payload;
|
|
635
|
+
break;
|
|
636
|
+
} catch {
|
|
637
|
+
}
|
|
638
|
+
}
|
|
597
639
|
}
|
|
598
640
|
}
|
|
599
641
|
if (!candidate) throw new Error("native_binary_missing: install native wx separately or configure its absolute path");
|
|
600
642
|
candidate = await realpath(candidate);
|
|
601
|
-
if (candidate.endsWith("/scripts/native-launcher.mjs")) {
|
|
643
|
+
if (candidate.replaceAll("\\", "/").endsWith("/scripts/native-launcher.mjs")) {
|
|
602
644
|
const root = dirname(dirname(candidate));
|
|
603
645
|
const pkg = JSON.parse(await readFile(join(root, "package.json"), "utf8"));
|
|
604
646
|
if (pkg.name !== "@agent-wechat/cli" || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(pkg.version)) throw new Error("Unsupported native CLI launcher");
|
|
605
647
|
const manifest = JSON.parse(await readFile(join(root, "native-release.json"), "utf8"));
|
|
606
|
-
const asset = manifest.assets?.find((value) => value.rustTarget === target.rustTarget && value.platform ===
|
|
648
|
+
const asset = manifest.assets?.find((value) => value.rustTarget === target.rustTarget && value.platform === target.platform && value.arch === target.arch && (target.platform !== "linux" || value.libc === "gnu"));
|
|
607
649
|
if (manifest.version !== pkg.version || !asset || !/^[a-f0-9]{64}$/.test(asset.sha256)) throw new Error("Native CLI manifest is incompatible");
|
|
608
|
-
candidate = await realpath(join(root, "native", pkg.version,
|
|
650
|
+
candidate = await realpath(join(root, "native", pkg.version, target.key, target.executable));
|
|
609
651
|
const bytes = await readFile(candidate);
|
|
610
652
|
if (bytes.length !== asset.bytes || createHash2("sha256").update(bytes).digest("hex") !== asset.sha256) throw new Error("Installed native executable checksum mismatch");
|
|
611
653
|
}
|
|
612
654
|
const stat = await lstat(candidate);
|
|
613
655
|
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error("Native executable must be a regular file");
|
|
614
656
|
await access(candidate, constants.X_OK);
|
|
615
|
-
|
|
616
|
-
if (!magic.equals(Buffer.from([127, 69, 76, 70]))) throw new Error("native_binary_required: configure the actual native ELF executable, not an unverified process wrapper");
|
|
657
|
+
if (!nativeExecutableFormat(await readFile(candidate))) throw new Error("native_binary_required: configure the actual native executable for this OS");
|
|
617
658
|
const { stdout } = await exec(candidate, ["--json", "version"], { env: environment, timeout: 5e3, maxBuffer: 65536 });
|
|
618
659
|
const version = JSON.parse(stdout);
|
|
619
660
|
requireNativeIdentity(version);
|
|
620
661
|
if (version.target !== target.rustTarget) throw new Error("native_binary_incompatible: executable architecture differs from this host");
|
|
621
662
|
return candidate;
|
|
622
663
|
}
|
|
623
|
-
async function portOccupied(
|
|
664
|
+
async function portOccupied(url2) {
|
|
624
665
|
return new Promise((resolve, reject) => {
|
|
625
|
-
const socket = createConnection({ host:
|
|
666
|
+
const socket = createConnection({ host: url2.hostname.replace(/^\[|\]$/g, ""), port: Number(url2.port || (url2.protocol === "https:" ? 443 : 80)) });
|
|
626
667
|
socket.setTimeout(1e3);
|
|
627
668
|
socket.once("connect", () => {
|
|
628
669
|
socket.destroy();
|
|
@@ -639,7 +680,7 @@ async function portOccupied(url) {
|
|
|
639
680
|
});
|
|
640
681
|
});
|
|
641
682
|
}
|
|
642
|
-
async function stopOwnedChild(child) {
|
|
683
|
+
async function stopOwnedChild(child, graceful) {
|
|
643
684
|
if (!child.pid || child.exitCode !== null || child.signalCode !== null) return;
|
|
644
685
|
await new Promise((resolve) => {
|
|
645
686
|
const kill = setTimeout(() => {
|
|
@@ -649,7 +690,10 @@ async function stopOwnedChild(child) {
|
|
|
649
690
|
clearTimeout(kill);
|
|
650
691
|
resolve();
|
|
651
692
|
});
|
|
652
|
-
|
|
693
|
+
if (graceful) void graceful().catch(() => {
|
|
694
|
+
if (child.exitCode === null && child.signalCode === null) child.kill("SIGTERM");
|
|
695
|
+
});
|
|
696
|
+
else child.kill("SIGTERM");
|
|
653
697
|
});
|
|
654
698
|
}
|
|
655
699
|
async function startNativeHost(config) {
|
|
@@ -664,11 +708,11 @@ async function startNativeHost(config) {
|
|
|
664
708
|
});
|
|
665
709
|
}
|
|
666
710
|
if (config.mode !== "managed" || config.endpoint || !config.dataDir || !isAbsolute(config.dataDir) || config.serverConfig && !isAbsolute(config.serverConfig)) throw new Error("Managed native mode requires absolute dataDir/config paths and no remote endpoint");
|
|
667
|
-
const
|
|
668
|
-
if (await portOccupied(
|
|
711
|
+
const url2 = validateNativeBaseUrl(`http://${config.listen ?? "127.0.0.1:6174"}`);
|
|
712
|
+
if (await portOccupied(url2)) throw new Error("native_owner_conflict: configured port is already owned; use external mode for an independently operated daemon");
|
|
669
713
|
const executable = await resolveNativeExecutable(config.executable, void 0, nativeChildEnvironment(token));
|
|
670
|
-
const client = new NativeWeChatClient({ baseUrl:
|
|
671
|
-
const args = ["serve", "--data-dir", config.dataDir, "--token-stdin", "--listen",
|
|
714
|
+
const client = new NativeWeChatClient({ baseUrl: url2.href, token, timeoutMs: 1e3 });
|
|
715
|
+
const args = ["serve", "--data-dir", config.dataDir, "--token-stdin", "--listen", url2.host];
|
|
672
716
|
if (config.serverConfig) args.push("--server-config", config.serverConfig);
|
|
673
717
|
const child = spawn(executable, args, { env: nativeChildEnvironment(token), stdio: ["pipe", "ignore", "pipe"], shell: false });
|
|
674
718
|
child.stderr?.on("data", () => {
|
|
@@ -691,10 +735,21 @@ async function startNativeHost(config) {
|
|
|
691
735
|
if (exited) throw new Error("Native owned child exited before readiness");
|
|
692
736
|
try {
|
|
693
737
|
const status = await client.status();
|
|
694
|
-
const
|
|
738
|
+
const canonical = await realpath(config.dataDir);
|
|
739
|
+
const dataDir = process.platform === "win32" ? toNamespacedPath(canonical) : canonical;
|
|
695
740
|
if (exited || status.version.backend !== "native" || status.version.apiVersion !== 1 || status.pid !== child.pid || status.dataDir !== dataDir || typeof status.serverId !== "string" || typeof status.ownershipGeneration !== "string") throw new Error("native_owner_conflict: readiness identity does not match the owned process/store");
|
|
696
741
|
requireNativeIdentity(status.version);
|
|
697
|
-
return host(new NativeWeChatClient({ baseUrl:
|
|
742
|
+
return host(new NativeWeChatClient({ baseUrl: url2.href, token }), token, () => stopOwnedChild(child, async () => {
|
|
743
|
+
const response = await fetch(new URL("v1/daemon/shutdown", url2), {
|
|
744
|
+
method: "POST",
|
|
745
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
746
|
+
body: JSON.stringify({ pid: status.pid, serverId: status.serverId, ownershipGeneration: status.ownershipGeneration }),
|
|
747
|
+
redirect: "error",
|
|
748
|
+
signal: AbortSignal.timeout(3e3)
|
|
749
|
+
});
|
|
750
|
+
if (!response.ok) throw new Error("Owned daemon refused graceful shutdown");
|
|
751
|
+
await response.body?.cancel();
|
|
752
|
+
}));
|
|
698
753
|
} catch (error) {
|
|
699
754
|
if (String(error).includes("native_owner_conflict")) throw error;
|
|
700
755
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
@@ -1023,25 +1078,25 @@ function appendReference(body, reference) {
|
|
|
1023
1078
|
return body ? `${body}
|
|
1024
1079
|
${reference}` : reference;
|
|
1025
1080
|
}
|
|
1026
|
-
function safeDisplayName(
|
|
1027
|
-
return
|
|
1081
|
+
function safeDisplayName(name2) {
|
|
1082
|
+
return name2.replace(/[\r\n\0]/g, " ").trim() || "attachment";
|
|
1028
1083
|
}
|
|
1029
1084
|
function renderAttachmentBody(originalBody, attachment, exposeNonVisualPath = true) {
|
|
1030
1085
|
if (!attachment) return originalBody;
|
|
1031
|
-
const
|
|
1086
|
+
const name2 = safeDisplayName(attachment.filename);
|
|
1032
1087
|
if (attachment.status !== "ready" || !attachment.path) {
|
|
1033
1088
|
const label = attachment.kind === "file" ? "File" : attachment.kind === "image" ? "Image" : attachment.kind === "audio" ? "Voice message" : "Video";
|
|
1034
1089
|
if (attachment.status === "pending") {
|
|
1035
|
-
return appendReference(originalBody, `[${label} pending: ${
|
|
1090
|
+
return appendReference(originalBody, `[${label} pending: ${name2}]`);
|
|
1036
1091
|
}
|
|
1037
1092
|
if (attachment.status === "expired") {
|
|
1038
|
-
return appendReference(originalBody, `[${label} expired: ${
|
|
1093
|
+
return appendReference(originalBody, `[${label} expired: ${name2}; no cached copy available. Ask the sender to resend.]`);
|
|
1039
1094
|
}
|
|
1040
|
-
return appendReference(originalBody, `[${label} unavailable: ${
|
|
1095
|
+
return appendReference(originalBody, `[${label} unavailable: ${name2} (${attachment.status})]`);
|
|
1041
1096
|
}
|
|
1042
1097
|
if (attachment.kind === "file") {
|
|
1043
|
-
const prefix = originalBody.trim() ===
|
|
1044
|
-
return appendReference(prefix, `[File: ${
|
|
1098
|
+
const prefix = originalBody.trim() === name2 ? "" : originalBody;
|
|
1099
|
+
return appendReference(prefix, `[File: ${name2}]
|
|
1045
1100
|
[Local file: <${attachment.path}>]`);
|
|
1046
1101
|
}
|
|
1047
1102
|
if (exposeNonVisualPath) {
|
|
@@ -1060,11 +1115,117 @@ function renderAttachmentBody(originalBody, attachment, exposeNonVisualPath = tr
|
|
|
1060
1115
|
return body;
|
|
1061
1116
|
}
|
|
1062
1117
|
|
|
1118
|
+
// src/native-send.ts
|
|
1119
|
+
async function nativeOutboundDelivery(client, accountId, threadId2, messageIds, source) {
|
|
1120
|
+
const receipts = [];
|
|
1121
|
+
try {
|
|
1122
|
+
if (!Array.isArray(messageIds) || !messageIds.length || messageIds.length > 256 || messageIds.some((id2) => typeof id2 !== "string" || !id2 || id2.length > 4096) || new Set(messageIds).size !== messageIds.length) throw Error("Message receipts unavailable");
|
|
1123
|
+
for (const [index, messageId] of messageIds.entries()) {
|
|
1124
|
+
const message = await client.message(accountId, messageId);
|
|
1125
|
+
if (message.id !== messageId || message.accountId !== accountId || message.threadId !== threadId2 || message.direction !== "outgoing" || typeof message.serverId !== "string" || !/^[1-9]\d{0,19}$/.test(message.serverId) || BigInt(message.serverId) > 18446744073709551615n || source.serverIds?.[index] != null && source.serverIds[index] !== message.serverId) throw Error("Message receipt identity mismatch");
|
|
1126
|
+
receipts.push({ messageId, serverId: message.serverId, accountId, threadId: threadId2 });
|
|
1127
|
+
}
|
|
1128
|
+
} catch {
|
|
1129
|
+
return { messageId: "", meta: {
|
|
1130
|
+
accountId,
|
|
1131
|
+
threadId: threadId2,
|
|
1132
|
+
...source,
|
|
1133
|
+
messageIds: receipts,
|
|
1134
|
+
outcome: "unknown",
|
|
1135
|
+
receiptState: "needs_review",
|
|
1136
|
+
retryable: false,
|
|
1137
|
+
note: "Send submitted. Inspect the durable send/job and retained receipts; do not resend automatically."
|
|
1138
|
+
} };
|
|
1139
|
+
}
|
|
1140
|
+
return { messageId: receipts[0].messageId, meta: {
|
|
1141
|
+
accountId,
|
|
1142
|
+
threadId: threadId2,
|
|
1143
|
+
serverId: receipts[0].serverId,
|
|
1144
|
+
outcome: "sent",
|
|
1145
|
+
...source,
|
|
1146
|
+
messageIds: receipts,
|
|
1147
|
+
receiptState: "verified"
|
|
1148
|
+
} };
|
|
1149
|
+
}
|
|
1150
|
+
async function submitNativeText(client, accountId, threadId2, text3, key, options = {}) {
|
|
1151
|
+
let intent = await client.sendText(accountId, threadId2, text3, key);
|
|
1152
|
+
const deadline = Date.now() + Math.max(1, Math.min(options.timeoutMs ?? 65e3, 65e3));
|
|
1153
|
+
let delay = Math.max(1, Math.min(options.initialDelayMs ?? 250, 1e3));
|
|
1154
|
+
while (["queued", "submitting"].includes(intent.state) && Date.now() < deadline) {
|
|
1155
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(delay, deadline - Date.now())));
|
|
1156
|
+
const remaining = deadline - Date.now();
|
|
1157
|
+
if (remaining <= 0) break;
|
|
1158
|
+
intent = await client.send(accountId, intent.id, AbortSignal.timeout(remaining));
|
|
1159
|
+
delay = Math.min(delay * 2, 1e3);
|
|
1160
|
+
}
|
|
1161
|
+
if (!["accepted", "sent", "server_accepted"].includes(intent.state)) throw new Error(`Native send ${intent.id} requires inspection (${intent.state}); do not automatically resend`);
|
|
1162
|
+
return intent;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// src/native-recall-state.ts
|
|
1166
|
+
function nativeRecallInfo(message) {
|
|
1167
|
+
return message.recall;
|
|
1168
|
+
}
|
|
1169
|
+
function nativeRecallIdentity(threadId2, serverId) {
|
|
1170
|
+
return JSON.stringify([threadId2, serverId]);
|
|
1171
|
+
}
|
|
1172
|
+
function validateNativeRecall(value) {
|
|
1173
|
+
if (!value || typeof value.threadId !== "string" || !value.threadId || value.threadId.length > 4096 || typeof value.serverId !== "string" || !/^[1-9]\d{0,19}$/.test(value.serverId) || BigInt(value.serverId) > 18446744073709551615n || typeof value.observedAt !== "string" || value.observedAt.length > 128 || !Number.isFinite(Date.parse(value.observedAt)) || value.recalledAt !== void 0 && (typeof value.recalledAt !== "string" || value.recalledAt.length > 128 || !Number.isFinite(Date.parse(value.recalledAt))) || value.messageId !== void 0 && (typeof value.messageId !== "string" || !value.messageId || value.messageId.length > 4096)) throw Error("Invalid native recall observation; cursor retained");
|
|
1174
|
+
}
|
|
1175
|
+
function recalledNativeMessage(message, recall) {
|
|
1176
|
+
return {
|
|
1177
|
+
...message,
|
|
1178
|
+
type: 1e4,
|
|
1179
|
+
body: "[Message recalled]",
|
|
1180
|
+
quote: void 0,
|
|
1181
|
+
link: void 0,
|
|
1182
|
+
media: [],
|
|
1183
|
+
mentions: { state: "unknown", userIds: [], self: null, all: null, source: null },
|
|
1184
|
+
recall: { observedAt: recall.observedAt, ...recall.recalledAt ? { recalledAt: recall.recalledAt } : {} }
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1063
1188
|
// src/native-media.ts
|
|
1064
1189
|
function kind(type) {
|
|
1065
1190
|
return type === 3 || type === 47 ? "image" : type === 34 ? "audio" : type === 43 ? "video" : type === 49 ? "file" : void 0;
|
|
1066
1191
|
}
|
|
1067
|
-
var terminalJobs = /* @__PURE__ */ new Set(["accepted", "completed", "succeeded", "available", "failed", "cancelled", "expired", "needs_review"]);
|
|
1192
|
+
var terminalJobs = /* @__PURE__ */ new Set(["accepted", "completed", "succeeded", "available", "failed", "partial", "cancelled", "expired", "needs_review"]);
|
|
1193
|
+
var successfulSendJobs = /* @__PURE__ */ new Set(["accepted", "completed", "succeeded"]);
|
|
1194
|
+
var terminalIdentityErrors = /* @__PURE__ */ new Set(["media_identity_unavailable", "media_source_changed"]);
|
|
1195
|
+
function mediaSendResult(job) {
|
|
1196
|
+
return job.result && typeof job.result === "object" && !Array.isArray(job.result) ? job.result : {};
|
|
1197
|
+
}
|
|
1198
|
+
function mediaSendReceiptIds(job, audioAsVoice) {
|
|
1199
|
+
const result = mediaSendResult(job), ids = result.messageIds;
|
|
1200
|
+
if (!successfulSendJobs.has(job.state) || result.profileProjection === "unknown" || !Array.isArray(ids) || !ids.length || ids.length > 256 || ids.some((id2) => typeof id2 !== "string" || !id2 || id2.length > 4096) || new Set(ids).size !== ids.length) return void 0;
|
|
1201
|
+
if (audioAsVoice) {
|
|
1202
|
+
const chunks = result.chunks;
|
|
1203
|
+
if (!Array.isArray(chunks) || !chunks.length || chunks.length !== ids.length || chunks.some((chunk) => !chunk || chunk.state !== "accepted" || typeof chunk.serverMessageId !== "string")) return void 0;
|
|
1204
|
+
} else if (ids.length !== 1) return void 0;
|
|
1205
|
+
return ids;
|
|
1206
|
+
}
|
|
1207
|
+
async function waitNativeMediaSendReceipts(client, accountId, threadId2, initial, audioAsVoice, timeoutMs = 1e4) {
|
|
1208
|
+
if (initial.accountId !== accountId || initial.threadId !== threadId2 || initial.kind !== "send") throw Error("Native media receipt job identity mismatch; inspect the submitted job and do not resend");
|
|
1209
|
+
const deadline = Date.now() + Math.max(0, Math.min(timeoutMs, 1e4));
|
|
1210
|
+
let job = initial, delay = 100;
|
|
1211
|
+
while (successfulSendJobs.has(job.state) && !mediaSendReceiptIds(job, audioAsVoice) && Date.now() < deadline) {
|
|
1212
|
+
const result = mediaSendResult(job);
|
|
1213
|
+
if (["unknown", "not_applicable"].includes(String(result.profileProjection)) || audioAsVoice && Array.isArray(result.chunks) && result.chunks.some((chunk) => !chunk || chunk.state !== "accepted")) break;
|
|
1214
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(delay, Math.max(0, deadline - Date.now()))));
|
|
1215
|
+
const remaining = deadline - Date.now();
|
|
1216
|
+
if (remaining <= 0) break;
|
|
1217
|
+
let next;
|
|
1218
|
+
try {
|
|
1219
|
+
next = await client.job(accountId, initial.id, AbortSignal.timeout(remaining));
|
|
1220
|
+
} catch {
|
|
1221
|
+
break;
|
|
1222
|
+
}
|
|
1223
|
+
if (next.id !== initial.id || next.accountId !== accountId || next.threadId !== threadId2 || next.kind !== "send" || next.sourceId !== initial.sourceId || initial.serverMessageId != null && next.serverMessageId !== initial.serverMessageId) break;
|
|
1224
|
+
job = next;
|
|
1225
|
+
delay = Math.min(delay * 2, 1e3);
|
|
1226
|
+
}
|
|
1227
|
+
return job;
|
|
1228
|
+
}
|
|
1068
1229
|
async function waitNativeMediaJob(client, accountId, initial, timeoutMs = 65e3, signal, assertCurrent) {
|
|
1069
1230
|
const deadline = Date.now() + timeoutMs;
|
|
1070
1231
|
let job = initial;
|
|
@@ -1102,6 +1263,43 @@ async function prepareImageRepresentation(bytes, mime, cfg) {
|
|
|
1102
1263
|
}
|
|
1103
1264
|
throw Error("Native model image representation exceeds its bound");
|
|
1104
1265
|
}
|
|
1266
|
+
async function prepareNativeLink(params) {
|
|
1267
|
+
const { message } = params;
|
|
1268
|
+
const link = message.link;
|
|
1269
|
+
const body = [`[Link]${link.title ? ` ${link.title}` : ""}`, link.description, link.url].filter(Boolean).join("\n").slice(0, 65536);
|
|
1270
|
+
const unavailable = (status, reason) => ({
|
|
1271
|
+
body: `${body}
|
|
1272
|
+
[Link preview ${status}]`,
|
|
1273
|
+
pending: false,
|
|
1274
|
+
retryable: false,
|
|
1275
|
+
reason,
|
|
1276
|
+
attachment: { kind: "image", status, filename: "link-preview" }
|
|
1277
|
+
});
|
|
1278
|
+
if (params.quality && !["best", "thumbnail"].includes(params.quality)) return unavailable("unsupported", "unsupported_media_variant");
|
|
1279
|
+
let prepared;
|
|
1280
|
+
try {
|
|
1281
|
+
prepared = await prepareNativeAttachment({
|
|
1282
|
+
...params,
|
|
1283
|
+
quality: "thumbnail",
|
|
1284
|
+
message: { ...message, type: 3, appMessageType: void 0, quote: void 0, link: void 0 }
|
|
1285
|
+
});
|
|
1286
|
+
} catch (error) {
|
|
1287
|
+
params.signal?.throwIfAborted();
|
|
1288
|
+
params.assertCurrent?.();
|
|
1289
|
+
if (!(error instanceof NativeApiError)) throw error;
|
|
1290
|
+
if (terminalIdentityErrors.has(error.code) || error.code === "message_recalled") throw error;
|
|
1291
|
+
return { ...unavailable("unavailable", error.code), retryable: error.retryable };
|
|
1292
|
+
}
|
|
1293
|
+
if (prepared.reason === "message_recalled" || terminalIdentityErrors.has(prepared.reason ?? "")) return prepared;
|
|
1294
|
+
return {
|
|
1295
|
+
...prepared,
|
|
1296
|
+
body: `${body}
|
|
1297
|
+
[Link preview]
|
|
1298
|
+
${prepared.body}`,
|
|
1299
|
+
pending: false,
|
|
1300
|
+
...prepared.pending ? { attachment: { kind: "image", status: "pending", filename: "link-preview" } } : {}
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1105
1303
|
async function prepareNativeQuote(params) {
|
|
1106
1304
|
const { client, account, message } = params;
|
|
1107
1305
|
const guard = () => {
|
|
@@ -1111,7 +1309,6 @@ async function prepareNativeQuote(params) {
|
|
|
1111
1309
|
guard();
|
|
1112
1310
|
const comment = (message.body ?? "").slice(0, 16384);
|
|
1113
1311
|
const unavailable = (state) => ({ body: `${comment}${comment ? "\n\n" : ""}[Quoted message ${state}; original lookup is confined to this chat]`, pending: false, retryable: state === "unavailable", quote: { state } });
|
|
1114
|
-
if (!message.quote) return unavailable("invalid");
|
|
1115
1312
|
await params.beforeMediaAccess?.();
|
|
1116
1313
|
guard();
|
|
1117
1314
|
let resolution;
|
|
@@ -1120,10 +1317,13 @@ async function prepareNativeQuote(params) {
|
|
|
1120
1317
|
} catch (error) {
|
|
1121
1318
|
guard();
|
|
1122
1319
|
if (!(error instanceof NativeApiError)) throw error;
|
|
1123
|
-
return unavailable("
|
|
1320
|
+
if (error.code === "message_recalled") return { ...unavailable("recalled"), retryable: false, reason: "message_recalled" };
|
|
1321
|
+
return terminalIdentityErrors.has(error.code) ? { ...unavailable("unavailable"), retryable: false, reason: error.code } : unavailable("unavailable");
|
|
1124
1322
|
}
|
|
1125
1323
|
guard();
|
|
1126
1324
|
if (resolution.accountId !== message.accountId || resolution.messageId !== message.id || resolution.threadId !== message.threadId) throw Error("Native quote identity does not match its message");
|
|
1325
|
+
if (resolution.state === "recalled" || resolution.message && nativeRecallInfo(resolution.message)) return { ...unavailable("recalled"), retryable: false, reason: "message_recalled" };
|
|
1326
|
+
if (!message.quote) return unavailable("invalid");
|
|
1127
1327
|
if (resolution.state === "embedded") {
|
|
1128
1328
|
if (resolution.message != null || (message.quote.type === 1 ? typeof resolution.embeddedText !== "string" || resolution.embeddedText.length > 16384 : message.quote.type !== 3 || resolution.embeddedImage !== true)) throw Error("Invalid embedded quote resolution");
|
|
1129
1329
|
const author = { senderId: resolution.senderId ?? null, direction: null };
|
|
@@ -1181,6 +1381,25 @@ async function prepareNativeQuote(params) {
|
|
|
1181
1381
|
};
|
|
1182
1382
|
}
|
|
1183
1383
|
async function prepareNativeAttachment(params) {
|
|
1384
|
+
try {
|
|
1385
|
+
return await prepareNativeAttachmentInput(params);
|
|
1386
|
+
} catch (error) {
|
|
1387
|
+
params.signal?.throwIfAborted();
|
|
1388
|
+
params.assertCurrent?.();
|
|
1389
|
+
if (!(error instanceof NativeApiError) || !terminalIdentityErrors.has(error.code)) throw error;
|
|
1390
|
+
if (params.message.type === 49 && params.message.appMessageType === 57) return {
|
|
1391
|
+
body: `${(params.message.body ?? "").slice(0, 16384)}
|
|
1392
|
+
[Quoted message unavailable: stored source identity could not be verified]`,
|
|
1393
|
+
quote: { state: "unavailable" },
|
|
1394
|
+
pending: false,
|
|
1395
|
+
retryable: false,
|
|
1396
|
+
reason: error.code
|
|
1397
|
+
};
|
|
1398
|
+
const attachment = { kind: kind(params.message.type) ?? "file", filename: "attachment", status: "unavailable" };
|
|
1399
|
+
return { body: renderAttachmentBody("", attachment), attachment, pending: false, retryable: false, reason: error.code };
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
async function prepareNativeAttachmentInput(params) {
|
|
1184
1403
|
const { client, account, message, core } = params;
|
|
1185
1404
|
const attachmentKind = kind(message.type);
|
|
1186
1405
|
const guard = () => {
|
|
@@ -1188,6 +1407,7 @@ async function prepareNativeAttachment(params) {
|
|
|
1188
1407
|
params.assertCurrent?.();
|
|
1189
1408
|
};
|
|
1190
1409
|
guard();
|
|
1410
|
+
if (nativeRecallInfo(message)) return { body: "[Message recalled]", pending: false, retryable: false, reason: "message_recalled" };
|
|
1191
1411
|
const authorize = async () => {
|
|
1192
1412
|
guard();
|
|
1193
1413
|
await params.beforeMediaAccess?.();
|
|
@@ -1195,6 +1415,7 @@ async function prepareNativeAttachment(params) {
|
|
|
1195
1415
|
};
|
|
1196
1416
|
if (message.type === 49 && message.appMessageType === 57) return prepareNativeQuote(params);
|
|
1197
1417
|
if (message.type === 49 && message.appMessageType === 19) return { body: message.body?.slice(0, 65536) || "[Forwarded chat history unavailable]", pending: false };
|
|
1418
|
+
if (message.type === 49 && [3, 4, 5].includes(message.appMessageType ?? 0) && message.link) return prepareNativeLink(params);
|
|
1198
1419
|
if (message.type === 49 && message.appMessageType != null && message.appMessageType !== 6) return { body: `[Unsupported WeChat app message subtype ${message.appMessageType}]`, pending: false, retryable: false };
|
|
1199
1420
|
if (!attachmentKind) return { body: message.type === 1 ? message.body ?? "" : `[Unsupported WeChat message type ${message.type}]`, pending: false };
|
|
1200
1421
|
const accountId = account.native.accountId;
|
|
@@ -1222,34 +1443,38 @@ async function prepareNativeAttachment(params) {
|
|
|
1222
1443
|
}
|
|
1223
1444
|
let job;
|
|
1224
1445
|
let transferError;
|
|
1225
|
-
if (!asset && initialCandidate?.jobId) {
|
|
1446
|
+
if (!asset && initialCandidate?.jobId && initialCandidate.state !== "recalled") {
|
|
1226
1447
|
await authorize();
|
|
1227
1448
|
job = await client.job(accountId, initialCandidate.jobId, params.signal);
|
|
1228
1449
|
guard();
|
|
1229
1450
|
if (job.accountId !== accountId || job.kind !== "download" || job.assetId != null && job.assetId !== initialCandidate.id) throw Error("Native download job identity does not match its attachment");
|
|
1230
1451
|
assets = await scopedAssets();
|
|
1231
1452
|
asset = select(assets, message.type, quality);
|
|
1453
|
+
if (!asset && transferCandidate()?.state === "recalled") return { body: "[Message recalled]", pending: false, retryable: false, reason: "message_recalled" };
|
|
1232
1454
|
if (!asset && ["queued", "running"].includes(job.state)) return { body: `[${attachmentKind} pending]`, pending: true, jobId: job.id, retryable: true };
|
|
1233
1455
|
}
|
|
1234
|
-
if (!asset && !job && params.download !== false && !["expired", "unsupported"].includes(transferCandidate()?.state ?? "")) {
|
|
1456
|
+
if (!asset && !job && params.download !== false && !["expired", "unsupported", "recalled"].includes(transferCandidate()?.state ?? "")) {
|
|
1457
|
+
await authorize();
|
|
1235
1458
|
try {
|
|
1236
|
-
await authorize();
|
|
1237
1459
|
job = await client.transferMedia(accountId, { messageId: message.id, assetId: null, quality: transferQuality });
|
|
1238
1460
|
guard();
|
|
1239
1461
|
job = await waitNativeMediaJob(client, accountId, job, params.waitMs ?? 2e4, params.signal, guard);
|
|
1240
1462
|
guard();
|
|
1241
1463
|
} catch (error) {
|
|
1242
1464
|
guard();
|
|
1243
|
-
if (!(error instanceof NativeApiError) || error.
|
|
1465
|
+
if (!(error instanceof NativeApiError) || terminalIdentityErrors.has(error.code)) throw error;
|
|
1466
|
+
if (error.retryable) return { body: `[${attachmentKind} pending: retrieval is unavailable]`, pending: true, jobId: job?.id, retryable: true };
|
|
1244
1467
|
transferError = error;
|
|
1245
1468
|
}
|
|
1246
1469
|
assets = await scopedAssets();
|
|
1247
1470
|
asset = select(assets, message.type, quality);
|
|
1471
|
+
if (!asset && transferCandidate()?.state === "recalled") return { body: "[Message recalled]", pending: false, retryable: false, reason: "message_recalled" };
|
|
1248
1472
|
if (!asset && job && ["queued", "running"].includes(job.state)) return { body: `[${attachmentKind} pending]`, pending: true, jobId: job.id, retryable: true };
|
|
1249
1473
|
}
|
|
1250
1474
|
const candidate = transferCandidate();
|
|
1251
1475
|
const filename = basename(asset?.filename ?? candidate?.filename ?? `message-${createHash4("sha256").update(message.id).digest("hex").slice(0, 12)}.bin`).replace(/[\r\n\0]/g, "_");
|
|
1252
1476
|
if (!asset) {
|
|
1477
|
+
if (candidate?.state === "recalled" || transferError?.code === "message_recalled") return { body: "[Message recalled]", pending: false, retryable: false, reason: "message_recalled" };
|
|
1253
1478
|
const state = candidate?.state === "expired" ? "expired" : candidate?.state === "unsupported" ? "unsupported" : "unavailable";
|
|
1254
1479
|
const attachment2 = { kind: attachmentKind, filename, status: state, expiresAt: candidate?.expiresAt ?? void 0 };
|
|
1255
1480
|
return {
|
|
@@ -1285,13 +1510,27 @@ async function prepareNativeAttachment(params) {
|
|
|
1285
1510
|
};
|
|
1286
1511
|
let suppliedPath = saved.path, suppliedMime = mime, suppliedHash = hash, suppliedLength = content.bytes.length;
|
|
1287
1512
|
let body = renderAttachmentBody("", attachment);
|
|
1288
|
-
if (attachmentKind === "image" && params.cfg) {
|
|
1289
|
-
|
|
1290
|
-
|
|
1513
|
+
if (attachmentKind === "image" && (params.cfg || mime === "image/x-wxgf")) {
|
|
1514
|
+
let imageBytes = Buffer.from(content.bytes), imageMime = mime;
|
|
1515
|
+
const wxgf = mime === "image/x-wxgf";
|
|
1516
|
+
if (wxgf) {
|
|
1517
|
+
const preview = asset.preview;
|
|
1518
|
+
if (!preview || preview.sourceSha256 !== hash || !/^[a-f0-9]{64}$/.test(preview.sha256) || !Number.isInteger(preview.byteLength) || preview.byteLength <= 0 || preview.byteLength > 16 * 1024 * 1024 || !["image/jpeg", "image/png"].includes(preview.contentType)) throw Error("Native WXGF preview metadata did not verify");
|
|
1519
|
+
await authorize();
|
|
1520
|
+
const derived = await client.mediaContent(accountId, asset.id, { maxBytes: preview.byteLength, representation: "preview", signal: params.signal });
|
|
1521
|
+
guard();
|
|
1522
|
+
const derivedMime = derived.contentType.split(";", 1)[0].trim().toLowerCase();
|
|
1523
|
+
if (derived.sourceSha256 !== hash || derivedMime !== preview.contentType || derived.bytes.length !== preview.byteLength || createHash4("sha256").update(derived.bytes).digest("hex") !== preview.sha256) throw Error("Native WXGF preview integrity did not verify");
|
|
1524
|
+
imageBytes = Buffer.from(derived.bytes);
|
|
1525
|
+
imageMime = derivedMime;
|
|
1526
|
+
}
|
|
1527
|
+
const representation = await prepareImageRepresentation(imageBytes, imageMime, params.cfg);
|
|
1528
|
+
if (wxgf || representation.preview) {
|
|
1529
|
+
const extension = representation.mime === "image/png" ? "png" : "jpg";
|
|
1291
1530
|
const normalized = await saveManagedAttachment({
|
|
1292
1531
|
data: representation.bytes.toString("base64"),
|
|
1293
1532
|
mime: representation.mime,
|
|
1294
|
-
filename: `${filename}.model
|
|
1533
|
+
filename: `${filename}.model.${extension}`,
|
|
1295
1534
|
maxBytes: 5 * 1024 * 1024,
|
|
1296
1535
|
saveMediaBuffer: core.channel.media.saveMediaBuffer
|
|
1297
1536
|
});
|
|
@@ -1299,7 +1538,8 @@ async function prepareNativeAttachment(params) {
|
|
|
1299
1538
|
suppliedMime = representation.mime;
|
|
1300
1539
|
suppliedHash = normalized.sha256;
|
|
1301
1540
|
suppliedLength = normalized.size;
|
|
1302
|
-
body += `
|
|
1541
|
+
body += wxgf ? `
|
|
1542
|
+
[Model image: WXGF still preview; original retained separately; animation and transparency are not represented]` : `
|
|
1303
1543
|
[Model image: prepared still preview; original retained separately]`;
|
|
1304
1544
|
}
|
|
1305
1545
|
}
|
|
@@ -1330,7 +1570,7 @@ async function prepareNativeAttachment(params) {
|
|
|
1330
1570
|
pending: false
|
|
1331
1571
|
};
|
|
1332
1572
|
}
|
|
1333
|
-
async function sendNativeAttachment(client, account, threadId2, mediaUrl, audioAsVoice = false, key = randomUUID()) {
|
|
1573
|
+
async function sendNativeAttachment(client, account, threadId2, mediaUrl, audioAsVoice = false, key = randomUUID(), beforeSend) {
|
|
1334
1574
|
const max = Math.min(account.mediaMaxMb * 1024 * 1024, NATIVE_MAX_SOURCE_BYTES);
|
|
1335
1575
|
let bytes;
|
|
1336
1576
|
let filename;
|
|
@@ -1345,7 +1585,7 @@ async function sendNativeAttachment(client, account, threadId2, mediaUrl, audioA
|
|
|
1345
1585
|
}
|
|
1346
1586
|
const reader = response.body?.getReader();
|
|
1347
1587
|
if (!reader) throw new Error("Native outbound source has no bytes");
|
|
1348
|
-
const
|
|
1588
|
+
const chunks2 = [];
|
|
1349
1589
|
let size = 0;
|
|
1350
1590
|
try {
|
|
1351
1591
|
while (true) {
|
|
@@ -1353,7 +1593,7 @@ async function sendNativeAttachment(client, account, threadId2, mediaUrl, audioA
|
|
|
1353
1593
|
if (done) break;
|
|
1354
1594
|
size += value.length;
|
|
1355
1595
|
if (size > max) throw new Error("Native outbound source exceeds configured size");
|
|
1356
|
-
|
|
1596
|
+
chunks2.push(value);
|
|
1357
1597
|
}
|
|
1358
1598
|
} catch (error) {
|
|
1359
1599
|
await reader.cancel();
|
|
@@ -1363,7 +1603,7 @@ async function sendNativeAttachment(client, account, threadId2, mediaUrl, audioA
|
|
|
1363
1603
|
}
|
|
1364
1604
|
bytes = new Uint8Array(size);
|
|
1365
1605
|
let offset = 0;
|
|
1366
|
-
for (const chunk of
|
|
1606
|
+
for (const chunk of chunks2) {
|
|
1367
1607
|
bytes.set(chunk, offset);
|
|
1368
1608
|
offset += chunk.length;
|
|
1369
1609
|
}
|
|
@@ -1375,7 +1615,7 @@ async function sendNativeAttachment(client, account, threadId2, mediaUrl, audioA
|
|
|
1375
1615
|
try {
|
|
1376
1616
|
const stat = await file.stat();
|
|
1377
1617
|
if (!stat.isFile() || stat.size === 0 || stat.size > max) throw new Error("Native outbound source must be a bounded regular file");
|
|
1378
|
-
const
|
|
1618
|
+
const chunks2 = [];
|
|
1379
1619
|
let size = 0;
|
|
1380
1620
|
while (true) {
|
|
1381
1621
|
const chunk = Buffer.alloc(Math.min(65536, max + 1 - size));
|
|
@@ -1383,9 +1623,9 @@ async function sendNativeAttachment(client, account, threadId2, mediaUrl, audioA
|
|
|
1383
1623
|
if (!bytesRead) break;
|
|
1384
1624
|
size += bytesRead;
|
|
1385
1625
|
if (size > max) throw new Error("Native outbound source exceeds configured size");
|
|
1386
|
-
|
|
1626
|
+
chunks2.push(chunk.subarray(0, bytesRead));
|
|
1387
1627
|
}
|
|
1388
|
-
bytes = Buffer.concat(
|
|
1628
|
+
bytes = Buffer.concat(chunks2, size);
|
|
1389
1629
|
} finally {
|
|
1390
1630
|
await file.close();
|
|
1391
1631
|
}
|
|
@@ -1394,43 +1634,39 @@ async function sendNativeAttachment(client, account, threadId2, mediaUrl, audioA
|
|
|
1394
1634
|
mime = [".png", ".jpg", ".jpeg", ".gif", ".webp"].includes(extension) ? `image/${extension === ".jpg" ? "jpeg" : extension.slice(1)}` : extension === ".mp4" ? "video/mp4" : [".wav", ".mp3", ".ogg", ".m4a", ".flac", ".aac"].includes(extension) ? `audio/${extension.slice(1)}` : "application/octet-stream";
|
|
1395
1635
|
}
|
|
1396
1636
|
if (audioAsVoice && !mime.startsWith("audio/")) throw new Error("Voice sending requires an explicit audio attachment");
|
|
1637
|
+
await beforeSend?.();
|
|
1397
1638
|
const accountId = account.native.accountId;
|
|
1398
1639
|
const source = await client.upload(accountId, bytes, filename, mime);
|
|
1399
1640
|
const input = { threadId: threadId2, kind: audioAsVoice ? "voice" : mime.startsWith("image/") ? "image" : mime.startsWith("video/") ? "video" : "file", sourceId: source.id, standardSourceId: null, thumbnailSourceId: null, playSeconds: null, durationMs: null };
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
}
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
const deadline = Date.now() + Math.max(1, Math.min(options.timeoutMs ?? 65e3, 65e3));
|
|
1409
|
-
let delay = Math.max(1, Math.min(options.initialDelayMs ?? 250, 1e3));
|
|
1410
|
-
while (["queued", "submitting"].includes(intent.state) && Date.now() < deadline) {
|
|
1411
|
-
await new Promise((resolve) => setTimeout(resolve, Math.min(delay, deadline - Date.now())));
|
|
1412
|
-
const remaining = deadline - Date.now();
|
|
1413
|
-
if (remaining <= 0) break;
|
|
1414
|
-
intent = await client.send(accountId, intent.id, AbortSignal.timeout(remaining));
|
|
1415
|
-
delay = Math.min(delay * 2, 1e3);
|
|
1416
|
-
}
|
|
1417
|
-
if (!["accepted", "sent", "server_accepted"].includes(intent.state)) throw new Error(`Native send ${intent.id} requires inspection (${intent.state}); do not automatically resend`);
|
|
1418
|
-
return intent;
|
|
1641
|
+
await beforeSend?.();
|
|
1642
|
+
let job = await waitNativeMediaJob(client, accountId, await client.sendMedia(accountId, input, key), audioAsVoice ? 25 * 6e4 : 65e3);
|
|
1643
|
+
if (successfulSendJobs.has(job.state)) job = await waitNativeMediaSendReceipts(client, accountId, threadId2, job, audioAsVoice);
|
|
1644
|
+
if (!successfulSendJobs.has(job.state)) throw new Error(`Native media job ${job.id} requires inspection (${job.state}); retain completed items and do not automatically resend`);
|
|
1645
|
+
const result = mediaSendResult(job);
|
|
1646
|
+
const chunks = Array.isArray(result.chunks) ? result.chunks.filter((chunk) => chunk && typeof chunk === "object") : [];
|
|
1647
|
+
const serverIds = audioAsVoice ? chunks.filter((chunk) => chunk.state === "accepted" && typeof chunk.serverMessageId === "string").map((chunk) => chunk.serverMessageId) : job.serverMessageId ? [job.serverMessageId] : void 0;
|
|
1648
|
+
return nativeOutboundDelivery(client, accountId, threadId2, mediaSendReceiptIds(job, audioAsVoice), { jobId: job.id, state: job.state, serverIds });
|
|
1419
1649
|
}
|
|
1420
1650
|
|
|
1421
1651
|
// src/outbound-media.ts
|
|
1422
|
-
async function sendWeChatMedia(cfg, to,
|
|
1652
|
+
async function sendWeChatMedia(cfg, to, text3, mediaUrl, audioAsVoice = false, idempotencyKey) {
|
|
1423
1653
|
const account = resolveWeChatAccount(cfg);
|
|
1424
1654
|
if (!account?.native) throw new Error("No native daemon configured");
|
|
1425
1655
|
const { client } = await ensureNativeHost(account.native);
|
|
1426
1656
|
const key = idempotencyKey ?? randomUUID2();
|
|
1427
1657
|
if (!mediaUrl) {
|
|
1428
1658
|
if (audioAsVoice) throw new Error("Voice sending requires an audio attachment");
|
|
1429
|
-
const send = await submitNativeText(client, account.native.accountId, to,
|
|
1430
|
-
return
|
|
1659
|
+
const send = await submitNativeText(client, account.native.accountId, to, text3, key);
|
|
1660
|
+
return nativeOutboundDelivery(
|
|
1661
|
+
client,
|
|
1662
|
+
account.native.accountId,
|
|
1663
|
+
to,
|
|
1664
|
+
send.localMessageId ? [send.localMessageId] : [],
|
|
1665
|
+
{ sendId: send.id, state: send.state, serverIds: send.serverId ? [send.serverId] : void 0 }
|
|
1666
|
+
);
|
|
1431
1667
|
}
|
|
1432
1668
|
const result = await sendNativeAttachment(client, account, to, mediaUrl, audioAsVoice, key);
|
|
1433
|
-
if (
|
|
1669
|
+
if (text3) await submitNativeText(client, account.native.accountId, to, text3, createHash5("sha256").update(`${key}\0caption`).digest("hex"));
|
|
1434
1670
|
return result;
|
|
1435
1671
|
}
|
|
1436
1672
|
|
|
@@ -1458,9 +1694,16 @@ var NativeConsumerState = class {
|
|
|
1458
1694
|
for (const [id2, item] of Object.entries(parsed.items)) {
|
|
1459
1695
|
if (item.message?.id !== id2 || item.message.accountId !== accountId || typeof item.message.threadId !== "string" || !["received", "dispatching", "handed_off", "completed", "ignored", "needs_review"].includes(item.state)) throw new Error("Invalid native custody record; state was preserved");
|
|
1460
1696
|
if (item.cursor != null && !/^\d+$/.test(item.cursor) || item.groupContext != null && typeof item.groupContext !== "boolean") throw new Error("Invalid native group context record; state was preserved");
|
|
1697
|
+
if (item.revision != null && (typeof item.revision !== "string" || !item.revision || item.revision.length > 64)) throw Error("Invalid native custody revision; state was preserved");
|
|
1698
|
+
item.revision ??= randomUUID3();
|
|
1461
1699
|
if (item.groupContext && (!item.cursor || item.origin !== "live" || item.state !== "ignored" || item.message.direction !== "incoming" || !item.message.threadId.endsWith("@chatroom"))) throw new Error("Invalid native buffered group context; state was preserved");
|
|
1462
1700
|
if (item.state === "dispatching") item.state = "needs_review";
|
|
1463
1701
|
}
|
|
1702
|
+
if (parsed.recalls != null && (!parsed.recalls || typeof parsed.recalls !== "object" || Array.isArray(parsed.recalls))) throw Error("Invalid native recall state; state was preserved");
|
|
1703
|
+
for (const [key, recall] of Object.entries(parsed.recalls ?? {})) {
|
|
1704
|
+
validateNativeRecall(recall);
|
|
1705
|
+
if (key !== nativeRecallIdentity(recall.threadId, recall.serverId)) throw Error("Invalid native recall identity; state was preserved");
|
|
1706
|
+
}
|
|
1464
1707
|
this.value = parsed;
|
|
1465
1708
|
this.persist();
|
|
1466
1709
|
}
|
|
@@ -1468,12 +1711,22 @@ var NativeConsumerState = class {
|
|
|
1468
1711
|
get cursor() {
|
|
1469
1712
|
return this.value.cursor;
|
|
1470
1713
|
}
|
|
1714
|
+
get accountId() {
|
|
1715
|
+
return this.value.accountId;
|
|
1716
|
+
}
|
|
1471
1717
|
items() {
|
|
1472
1718
|
return Object.values(this.value.items);
|
|
1473
1719
|
}
|
|
1474
1720
|
get(messageId) {
|
|
1475
1721
|
return this.value.items[messageId];
|
|
1476
1722
|
}
|
|
1723
|
+
isRecalled(message) {
|
|
1724
|
+
return !!nativeRecallInfo(message) || !!(message.serverId && this.value.recalls?.[nativeRecallIdentity(message.threadId, message.serverId)]);
|
|
1725
|
+
}
|
|
1726
|
+
isCurrent(message, revision) {
|
|
1727
|
+
const current = this.get(message.id);
|
|
1728
|
+
return !!current && (revision === void 0 || current.revision === revision) && !this.isRecalled(message) && JSON.stringify(current.message) === JSON.stringify(message);
|
|
1729
|
+
}
|
|
1477
1730
|
persist() {
|
|
1478
1731
|
const directory = dirname2(this.file);
|
|
1479
1732
|
mkdirSync(directory, { recursive: true, mode: 448 });
|
|
@@ -1517,42 +1770,129 @@ var NativeConsumerState = class {
|
|
|
1517
1770
|
accept(cursor, message, origin = "unknown") {
|
|
1518
1771
|
if (!/^\d+$/.test(cursor) || BigInt(cursor) < BigInt(this.value.cursor)) throw new Error("Native journal cursor cannot move backward");
|
|
1519
1772
|
if (message && (!message.id || message.accountId !== this.value.accountId)) throw new Error("Native journal message crosses account boundary");
|
|
1773
|
+
let replaced = false;
|
|
1520
1774
|
this.mutate(() => {
|
|
1521
|
-
if (message &&
|
|
1775
|
+
if (message?.serverId && nativeRecallInfo(message)) this.rememberRecall({ messageId: message.id, threadId: message.threadId, serverId: message.serverId, ...nativeRecallInfo(message) });
|
|
1776
|
+
if (message?.serverId) {
|
|
1777
|
+
const recall = this.value.recalls?.[nativeRecallIdentity(message.threadId, message.serverId)];
|
|
1778
|
+
if (recall) message = recalledNativeMessage(message, recall);
|
|
1779
|
+
}
|
|
1780
|
+
const existing = message ? this.value.items[message.id] : void 0;
|
|
1781
|
+
replaced = !!message && !nativeRecallInfo(message) && !!existing && (existing.message.threadId !== message.threadId || existing.message.serverId !== message.serverId);
|
|
1782
|
+
if (message && (!existing || replaced)) {
|
|
1522
1783
|
const completed = Object.entries(this.value.items).filter(([, item]) => !item.readPending && !item.groupContext && item.phoneReadState !== "needs_review" && item.phoneReadState !== "rejected" && ["completed", "ignored"].includes(item.state));
|
|
1523
1784
|
for (const [key] of completed.slice(0, Math.max(0, completed.length - 1e3))) delete this.value.items[key];
|
|
1524
|
-
if (Object.keys(this.value.items).length >= 5e3) throw new Error("Native custody queue is full; operator review required");
|
|
1525
|
-
|
|
1785
|
+
if (!existing && Object.keys(this.value.items).length >= 5e3) throw new Error("Native custody queue is full; operator review required");
|
|
1786
|
+
if (replaced) {
|
|
1787
|
+
for (const item of this.items()) {
|
|
1788
|
+
const manifest = item.dispatchManifest;
|
|
1789
|
+
if (!manifest || ![
|
|
1790
|
+
...manifest.messageIds,
|
|
1791
|
+
...(manifest.historyInputs ?? []).map((input) => input.id),
|
|
1792
|
+
...manifest.assets.flatMap((asset) => asset.sourceMessageId ? [asset.sourceMessageId] : [])
|
|
1793
|
+
].includes(message.id)) continue;
|
|
1794
|
+
if (["dispatching", "handed_off"].includes(item.state)) item.state = "needs_review";
|
|
1795
|
+
item.dispatchManifest = void 0;
|
|
1796
|
+
item.manifestHash = void 0;
|
|
1797
|
+
item.readPending = false;
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
this.value.items[message.id] = { message, revision: randomUUID3(), origin, state: nativeRecallInfo(message) ? "ignored" : "received", cursor };
|
|
1526
1801
|
}
|
|
1527
1802
|
this.value.cursor = cursor;
|
|
1528
1803
|
});
|
|
1804
|
+
return replaced;
|
|
1805
|
+
}
|
|
1806
|
+
/** Persist invalidation and the event cursor together, before consumer ACK. */
|
|
1807
|
+
acceptRecall(cursor, recall) {
|
|
1808
|
+
validateNativeRecall(recall);
|
|
1809
|
+
if (!/^\d+$/.test(cursor) || BigInt(cursor) < BigInt(this.value.cursor)) throw Error("Native journal cursor cannot move backward");
|
|
1810
|
+
let affected = [];
|
|
1811
|
+
this.mutate(() => {
|
|
1812
|
+
affected = this.rememberRecall(recall);
|
|
1813
|
+
this.value.cursor = cursor;
|
|
1814
|
+
});
|
|
1815
|
+
return affected;
|
|
1529
1816
|
}
|
|
1530
|
-
|
|
1817
|
+
rememberRecall(recall) {
|
|
1818
|
+
const hinted = recall.messageId ? this.get(recall.messageId)?.message : void 0;
|
|
1819
|
+
if (hinted && (hinted.threadId !== recall.threadId || hinted.serverId !== recall.serverId)) {
|
|
1820
|
+
const { messageId: _stale, ...exact } = recall;
|
|
1821
|
+
recall = exact;
|
|
1822
|
+
}
|
|
1823
|
+
const key = nativeRecallIdentity(recall.threadId, recall.serverId);
|
|
1824
|
+
this.value.recalls ??= {};
|
|
1825
|
+
this.value.recalls[key] ??= {
|
|
1826
|
+
threadId: recall.threadId,
|
|
1827
|
+
serverId: recall.serverId,
|
|
1828
|
+
observedAt: recall.observedAt,
|
|
1829
|
+
...recall.messageId ? { messageId: recall.messageId } : {},
|
|
1830
|
+
...recall.recalledAt ? { recalledAt: recall.recalledAt } : {}
|
|
1831
|
+
};
|
|
1832
|
+
const affected = /* @__PURE__ */ new Set();
|
|
1833
|
+
for (const item of this.items()) {
|
|
1834
|
+
if (item.message.threadId !== recall.threadId) continue;
|
|
1835
|
+
const target = item.message.serverId === recall.serverId;
|
|
1836
|
+
const quote = item.message.quote?.serverId === recall.serverId;
|
|
1837
|
+
if (!target && !quote) continue;
|
|
1838
|
+
affected.add(item.message.id);
|
|
1839
|
+
item.message = target ? recalledNativeMessage(item.message, recall) : { ...item.message, quote: void 0 };
|
|
1840
|
+
if (target) item.groupContext = false;
|
|
1841
|
+
item.readPending = false;
|
|
1842
|
+
if (["dispatching", "handed_off"].includes(item.state)) item.state = "needs_review";
|
|
1843
|
+
else if (target && item.state === "received") item.state = "ignored";
|
|
1844
|
+
item.invalidatedByRecall = true;
|
|
1845
|
+
item.dispatchManifest = void 0;
|
|
1846
|
+
item.manifestHash = void 0;
|
|
1847
|
+
}
|
|
1848
|
+
for (const item of this.items()) {
|
|
1849
|
+
const manifest = item.dispatchManifest;
|
|
1850
|
+
if (item.message.threadId !== recall.threadId || !manifest) continue;
|
|
1851
|
+
const references = [
|
|
1852
|
+
...manifest.messageIds,
|
|
1853
|
+
...(manifest.historyInputs ?? []).map((input) => input.id),
|
|
1854
|
+
...manifest.assets.flatMap((asset) => asset.sourceMessageId ? [asset.sourceMessageId] : [])
|
|
1855
|
+
];
|
|
1856
|
+
if (!references.some((id2) => affected.has(id2) || id2 === recall.messageId)) continue;
|
|
1857
|
+
affected.add(item.message.id);
|
|
1858
|
+
item.invalidatedByRecall = true;
|
|
1859
|
+
item.readPending = false;
|
|
1860
|
+
if (["dispatching", "handed_off"].includes(item.state)) item.state = "needs_review";
|
|
1861
|
+
item.dispatchManifest = void 0;
|
|
1862
|
+
item.manifestHash = void 0;
|
|
1863
|
+
}
|
|
1864
|
+
return [...affected];
|
|
1865
|
+
}
|
|
1866
|
+
update(messageId, update, revision) {
|
|
1531
1867
|
const item = this.value.items[messageId];
|
|
1868
|
+
if (revision !== void 0 && item?.revision !== revision) return false;
|
|
1532
1869
|
if (!item) throw new Error("Unknown native custody message");
|
|
1533
1870
|
this.mutate(() => Object.assign(this.value.items[messageId], update));
|
|
1871
|
+
return true;
|
|
1534
1872
|
}
|
|
1535
1873
|
groupHistory(threadId2, beforeId, limit) {
|
|
1536
1874
|
const before = this.value.items[beforeId]?.cursor;
|
|
1537
1875
|
if (!before || limit <= 0) return [];
|
|
1538
1876
|
return this.items().filter((item) => item.groupContext && item.state === "ignored" && item.origin === "live" && item.message.threadId === threadId2 && item.cursor && BigInt(item.cursor) < BigInt(before)).sort((a, b) => BigInt(a.cursor) < BigInt(b.cursor) ? -1 : BigInt(a.cursor) > BigInt(b.cursor) ? 1 : 0).slice(-limit);
|
|
1539
1877
|
}
|
|
1540
|
-
bufferGroupMessage(messageId, limit) {
|
|
1878
|
+
bufferGroupMessage(messageId, limit, revision) {
|
|
1541
1879
|
const item = this.value.items[messageId];
|
|
1880
|
+
if (revision !== void 0 && item?.revision !== revision) return false;
|
|
1542
1881
|
if (!item || item.state !== "received" || item.origin !== "live" || item.message.direction !== "incoming" || !item.cursor || !item.message.threadId.endsWith("@chatroom") || !Number.isInteger(limit) || limit < 0 || limit > 200) throw new Error("Invalid native group history admission");
|
|
1543
1882
|
this.mutate(() => {
|
|
1544
1883
|
Object.assign(this.value.items[messageId], { state: "ignored", groupContext: limit > 0 });
|
|
1545
1884
|
const history = this.items().filter((value) => value.groupContext).sort((a, b) => BigInt(a.cursor) < BigInt(b.cursor) ? -1 : 1);
|
|
1546
|
-
const
|
|
1547
|
-
for (const value of
|
|
1885
|
+
const thread3 = history.filter((value) => value.message.threadId === item.message.threadId);
|
|
1886
|
+
for (const value of thread3.slice(0, Math.max(0, thread3.length - limit))) value.groupContext = false;
|
|
1548
1887
|
const retained = history.filter((value) => value.groupContext);
|
|
1549
1888
|
for (const value of retained.slice(0, Math.max(0, retained.length - 1e3))) value.groupContext = false;
|
|
1550
1889
|
});
|
|
1890
|
+
return true;
|
|
1551
1891
|
}
|
|
1552
1892
|
beginDispatch(messageId, dispatchManifest, historyIds = []) {
|
|
1553
|
-
if (this.value.items[messageId]?.state !== "received" || historyIds.some((id2) => !this.value.items[id2]?.groupContext || this.value.items[id2].message.threadId !== this.value.items[messageId].message.threadId)) throw new Error("Invalid native context dispatch custody");
|
|
1893
|
+
if (this.value.items[messageId]?.state !== "received" || this.isRecalled(this.value.items[messageId].message) || historyIds.some((id2) => !this.value.items[id2]?.groupContext || this.value.items[id2].message.threadId !== this.value.items[messageId].message.threadId)) throw new Error("Invalid native context dispatch custody");
|
|
1554
1894
|
this.mutate(() => {
|
|
1555
|
-
Object.assign(this.value.items[messageId], { state: "dispatching", dispatchManifest });
|
|
1895
|
+
Object.assign(this.value.items[messageId], { state: "dispatching", dispatchManifest, invalidatedByRecall: false });
|
|
1556
1896
|
for (const id2 of historyIds) this.value.items[id2].groupContext = false;
|
|
1557
1897
|
});
|
|
1558
1898
|
}
|
|
@@ -1565,6 +1905,7 @@ var NativeConsumerState = class {
|
|
|
1565
1905
|
if (!item || item.state !== "dispatching") throw new Error("Prepared input manifest is not in dispatch custody");
|
|
1566
1906
|
return item;
|
|
1567
1907
|
});
|
|
1908
|
+
const revisions = items.map((item) => item.revision);
|
|
1568
1909
|
if (new Set(items.map((item) => item.message.threadId)).size !== 1) throw new Error("Prepared input manifest crosses threads");
|
|
1569
1910
|
const manifestHash = createHash6("sha256").update(JSON.stringify({
|
|
1570
1911
|
accountId: this.value.accountId,
|
|
@@ -1575,8 +1916,15 @@ var NativeConsumerState = class {
|
|
|
1575
1916
|
const runId = typeof args[0] === "string" ? args[0] : args[0]?.runId;
|
|
1576
1917
|
if (typeof runId !== "string" || !runId || runId.length > 256) throw new Error("Prepared run callback has no valid runId");
|
|
1577
1918
|
this.mutate(() => {
|
|
1578
|
-
|
|
1919
|
+
const currentHash = createHash6("sha256").update(JSON.stringify({
|
|
1920
|
+
accountId: this.value.accountId,
|
|
1921
|
+
messageIds: exact,
|
|
1922
|
+
dispatchManifests: exact.map((id2) => this.value.items[id2]?.dispatchManifest ?? null)
|
|
1923
|
+
})).digest("hex");
|
|
1924
|
+
for (const [index, id2] of exact.entries()) {
|
|
1579
1925
|
const item = this.value.items[id2];
|
|
1926
|
+
if (!item || item.invalidatedByRecall || this.isRecalled(item.message)) throw Error("Native prepared input was invalidated by a recall");
|
|
1927
|
+
if (item.revision !== revisions[index] || currentHash !== manifestHash) throw Error("Native prepared input source identity changed");
|
|
1580
1928
|
if (item.runId && item.runId !== runId) throw new Error("Native prepared manifest was assigned to another run; operator review required");
|
|
1581
1929
|
Object.assign(item, { state: "handed_off", runId, manifestHash, readPending: readPolicy === "on_model_dispatch", handoffUnconfirmed: false });
|
|
1582
1930
|
}
|
|
@@ -1595,6 +1943,9 @@ var digest = (value) => createHash7("sha256").update(value).digest("hex");
|
|
|
1595
1943
|
function prune() {
|
|
1596
1944
|
for (const [id2, item] of pending) if (item.expires < Date.now()) pending.delete(id2);
|
|
1597
1945
|
}
|
|
1946
|
+
function nativeSourceMessageSid(message) {
|
|
1947
|
+
return `wechat-native:${message.accountId}:${message.id}:${digest(JSON.stringify([message.threadId, message.serverId ?? null]))}`;
|
|
1948
|
+
}
|
|
1598
1949
|
function nativeDispatchManifest(params) {
|
|
1599
1950
|
const history = params.history ?? [];
|
|
1600
1951
|
if (history.length > 200 || history.some((message) => message.accountId !== params.message.accountId || message.threadId !== params.message.threadId || message.id === params.message.id)) throw Error("Invalid native history manifest");
|
|
@@ -1602,7 +1953,7 @@ function nativeDispatchManifest(params) {
|
|
|
1602
1953
|
if (typeof asset.id !== "string" || !asset.id || asset.id.length > 2048 || !/^[a-f0-9]{64}$/.test(asset.sha256) || !Number.isSafeInteger(asset.byteLength) || asset.byteLength < 1 || asset.byteLength > 128 * 1024 * 1024 || typeof asset.path !== "string" || !isAbsolute3(asset.path) || asset.path.length > 4096 || typeof asset.mime !== "string" || asset.mime.length > 128 || !/^[a-z0-9.+-]+\/[a-z0-9.+-]+$/.test(asset.mime) || !["image", "audio", "video", "file"].includes(asset.kind) || !["model_image", "file_reference"].includes(asset.role) || asset.role === "model_image" && (asset.kind !== "image" || !asset.mime.startsWith("image/") || !asset.runtimeMedia) || !["trigger", "history"].includes(asset.contextRole) || asset.sourceMessageId != null && (typeof asset.sourceMessageId !== "string" || !asset.sourceMessageId || asset.sourceMessageId.length > 8192 || ![params.message, ...history].some((message) => message.id === asset.messageId && message.type === 49 && message.appMessageType === 57 && message.quote?.type === 3)) || asset.contextRole === "history" && !history.some((message) => message.id === asset.messageId)) throw Error("Invalid native supplied asset manifest");
|
|
1603
1954
|
return Object.freeze({ ...asset });
|
|
1604
1955
|
}));
|
|
1605
|
-
const messageSid =
|
|
1956
|
+
const messageSid = nativeSourceMessageSid(params.message);
|
|
1606
1957
|
const manifest = {
|
|
1607
1958
|
schemaVersion: 2,
|
|
1608
1959
|
runId: randomUUID4(),
|
|
@@ -1627,14 +1978,15 @@ function nativeDispatchManifest(params) {
|
|
|
1627
1978
|
params.message.mentions,
|
|
1628
1979
|
params.message.serverId,
|
|
1629
1980
|
params.message.appMessageType,
|
|
1630
|
-
params.message.quote
|
|
1981
|
+
params.message.quote,
|
|
1982
|
+
params.message.link
|
|
1631
1983
|
])),
|
|
1632
1984
|
role: "trigger"
|
|
1633
1985
|
})]),
|
|
1634
1986
|
...history.length ? { historyInputs: Object.freeze(history.map((message) => Object.freeze({
|
|
1635
1987
|
id: message.id,
|
|
1636
1988
|
version: message.version ?? "",
|
|
1637
|
-
contentSha256: digest(JSON.stringify([message.id, message.version, message.senderId, message.type, message.body, message.mentions, message.serverId, message.appMessageType, message.quote])),
|
|
1989
|
+
contentSha256: digest(JSON.stringify([message.id, message.version, message.senderId, message.type, message.body, message.mentions, message.serverId, message.appMessageType, message.quote, message.link])),
|
|
1638
1990
|
role: "history"
|
|
1639
1991
|
}))) } : {},
|
|
1640
1992
|
assets,
|
|
@@ -1650,30 +2002,40 @@ function trackNativePreparedInput(manifest, record) {
|
|
|
1650
2002
|
if (!enabled || pending.size >= 1024) return;
|
|
1651
2003
|
pending.set(manifest.runId, { manifest, record, expires: Date.now() + 15 * 60 * 1e3 });
|
|
1652
2004
|
}
|
|
2005
|
+
function invalidateNativePreparedInputs(accountId, messageIds) {
|
|
2006
|
+
const ids = new Set(messageIds);
|
|
2007
|
+
for (const [runId, item] of pending) {
|
|
2008
|
+
if (item.manifest.accountId === accountId && [
|
|
2009
|
+
...item.manifest.messageIds,
|
|
2010
|
+
...(item.manifest.historyInputs ?? []).map((input) => input.id),
|
|
2011
|
+
...item.manifest.assets.flatMap((asset) => asset.sourceMessageId ? [asset.sourceMessageId] : [])
|
|
2012
|
+
].some((id2) => ids.has(id2))) pending.delete(runId);
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
1653
2015
|
function promptMatches(prompt, manifest) {
|
|
1654
2016
|
if (typeof prompt !== "string" || Buffer.byteLength(prompt) > 1024 * 1024) return false;
|
|
1655
|
-
const
|
|
1656
|
-
const candidates = [
|
|
2017
|
+
const text3 = prompt.replace(/^\[[^\]\r\n]{1,96}\] /, "");
|
|
2018
|
+
const candidates = [text3];
|
|
1657
2019
|
const sender = `Conversation info: \u27E6openclaw:ctx\u27E7
|
|
1658
2020
|
\`\`\`json
|
|
1659
2021
|
${JSON.stringify({ sender: { id: manifest.senderId } })}
|
|
1660
2022
|
\`\`\`
|
|
1661
2023
|
|
|
1662
2024
|
`;
|
|
1663
|
-
if (manifest.threadId.endsWith("@chatroom") &&
|
|
1664
|
-
return candidates.some((
|
|
2025
|
+
if (manifest.threadId.endsWith("@chatroom") && text3.startsWith(sender)) candidates.push(text3.slice(sender.length));
|
|
2026
|
+
return candidates.some((text4) => normalizedPromptMatches(text4, manifest));
|
|
1665
2027
|
}
|
|
1666
|
-
function normalizedPromptMatches(
|
|
2028
|
+
function normalizedPromptMatches(text3, manifest) {
|
|
1667
2029
|
const runtimeAssets = manifest.assets.filter((asset) => asset.runtimeMedia);
|
|
1668
2030
|
if (runtimeAssets.length) {
|
|
1669
2031
|
if (runtimeAssets.length !== 1) return false;
|
|
1670
2032
|
const asset = runtimeAssets[0];
|
|
1671
|
-
const header = /^\[media attached: ([^\r\n]+) \(([^)\r\n]+)\) \| (media:\/\/inbound\/[^\]\r\n]+)\]\n/.exec(
|
|
2033
|
+
const header = /^\[media attached: ([^\r\n]+) \(([^)\r\n]+)\) \| (media:\/\/inbound\/[^\]\r\n]+)\]\n/.exec(text3);
|
|
1672
2034
|
if (!header || !isAbsolute3(header[1]) || basename2(header[1]) !== `input-${basename2(asset.path)}` || header[2] !== asset.mime || header[3] !== `media://inbound/${basename2(asset.path)}`) return false;
|
|
1673
|
-
|
|
1674
|
-
if (asset.kind === "video")
|
|
2035
|
+
text3 = text3.slice(header[0].length);
|
|
2036
|
+
if (asset.kind === "video") text3 = text3.replace(/\n\n\[Video attachment not analyzed: video understanding is disabled\]$/, "");
|
|
1675
2037
|
}
|
|
1676
|
-
return digest(
|
|
2038
|
+
return digest(text3) === manifest.inputBodySha256;
|
|
1677
2039
|
}
|
|
1678
2040
|
function currentMessageFacts(messages) {
|
|
1679
2041
|
if (!Array.isArray(messages) || messages.length > 1e3) return [];
|
|
@@ -1734,8 +2096,8 @@ function observeNativeAgentEnd(event, ctx) {
|
|
|
1734
2096
|
const blocks = typeof user.content === "string" ? [{ type: "text", text: user.content }] : user.content;
|
|
1735
2097
|
if (!Array.isArray(blocks) || blocks.length > 32) return;
|
|
1736
2098
|
if (blocks.some((block) => !["text", "image"].includes(block?.type) || block.type === "text" && typeof block.text !== "string")) return;
|
|
1737
|
-
const
|
|
1738
|
-
if (!promptMatches(
|
|
2099
|
+
const text3 = blocks.filter((block) => block?.type === "text").map((block) => block.text).join("\n");
|
|
2100
|
+
if (!promptMatches(text3, m)) return;
|
|
1739
2101
|
const images = blocks.filter((block) => block?.type === "image");
|
|
1740
2102
|
const expectedImages = m.assets.filter((asset) => asset.role === "model_image");
|
|
1741
2103
|
if (images.length !== expectedImages.length) return;
|
|
@@ -1745,7 +2107,7 @@ function observeNativeAgentEnd(event, ctx) {
|
|
|
1745
2107
|
const bytes = Buffer.from(image.data, "base64");
|
|
1746
2108
|
if (bytes.length !== asset.byteLength || digest(bytes) !== asset.sha256) return;
|
|
1747
2109
|
}
|
|
1748
|
-
if (m.assets.some((asset) => asset.role === "file_reference" && !
|
|
2110
|
+
if (m.assets.some((asset) => asset.role === "file_reference" && !text3.includes(asset.path))) return;
|
|
1749
2111
|
item.record(m.runId);
|
|
1750
2112
|
pending.delete(m.runId);
|
|
1751
2113
|
}
|
|
@@ -1860,22 +2222,22 @@ async function projectNativeRead(client, accountId, state) {
|
|
|
1860
2222
|
for (const threadId2 of threads) {
|
|
1861
2223
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
1862
2224
|
let cursor;
|
|
1863
|
-
let
|
|
2225
|
+
let thread3;
|
|
1864
2226
|
const seen = /* @__PURE__ */ new Set();
|
|
1865
2227
|
for (let pageIndex = 0; pageIndex < 100; pageIndex++) {
|
|
1866
2228
|
const page = await client.threads(accountId, cursor);
|
|
1867
|
-
|
|
1868
|
-
if (
|
|
2229
|
+
thread3 = page.items.find((value) => !isNativeTombstone(value) && value.id === threadId2);
|
|
2230
|
+
if (thread3 || !page.nextCursor) break;
|
|
1869
2231
|
if (seen.has(page.nextCursor)) throw new Error("Native thread pagination did not advance");
|
|
1870
2232
|
seen.add(page.nextCursor);
|
|
1871
2233
|
cursor = page.nextCursor;
|
|
1872
2234
|
}
|
|
1873
|
-
if (!
|
|
2235
|
+
if (!thread3) break;
|
|
1874
2236
|
try {
|
|
1875
|
-
const receipt = await client.markRead(accountId,
|
|
2237
|
+
const receipt = await client.markRead(accountId, thread3.id, thread3.readStateVersion);
|
|
1876
2238
|
const notification = receipt?.phoneNotification?.state;
|
|
1877
2239
|
const phoneReadState = notification === "accepted" || notification === "rejected" || notification === "needs_review" ? notification : void 0;
|
|
1878
|
-
for (const item of pending2.filter((item2) => item2.message.threadId === threadId2)) state.update(item.message.id, { readPending: false, phoneReadState });
|
|
2240
|
+
for (const item of pending2.filter((item2) => item2.message.threadId === threadId2)) state.update(item.message.id, { readPending: false, phoneReadState }, item.revision);
|
|
1879
2241
|
break;
|
|
1880
2242
|
} catch (error) {
|
|
1881
2243
|
if (error instanceof NativeApiError && error.status === 409 && attempt === 0) continue;
|
|
@@ -1884,13 +2246,72 @@ async function projectNativeRead(client, accountId, state) {
|
|
|
1884
2246
|
}
|
|
1885
2247
|
}
|
|
1886
2248
|
}
|
|
2249
|
+
var NativeInputInvalidated = class extends Error {
|
|
2250
|
+
};
|
|
2251
|
+
async function acceptNativeEvent(client, accountId, state, event) {
|
|
2252
|
+
if (state.accountId !== accountId) throw Error("Native event crosses its custody account boundary");
|
|
2253
|
+
if (event.kind === "message.recalled") {
|
|
2254
|
+
const affected = state.acceptRecall(event.cursor, event.data);
|
|
2255
|
+
invalidateNativePreparedInputs(accountId, affected);
|
|
2256
|
+
return void 0;
|
|
2257
|
+
}
|
|
2258
|
+
let message;
|
|
2259
|
+
if (event.kind === "message.committed") {
|
|
2260
|
+
if (typeof event.data.messageId !== "string") throw Error("Native committed message identity is missing; cursor retained");
|
|
2261
|
+
message = await client.message(accountId, event.data.messageId);
|
|
2262
|
+
if (message.id !== event.data.messageId || message.accountId !== accountId || message.threadId !== event.data.threadId) throw Error("Native committed message identity mismatch");
|
|
2263
|
+
}
|
|
2264
|
+
const replaced = state.accept(event.cursor, message, typeof event.data.origin === "string" ? event.data.origin : "unknown");
|
|
2265
|
+
if (replaced && message) invalidateNativePreparedInputs(accountId, [message.id]);
|
|
2266
|
+
return message;
|
|
2267
|
+
}
|
|
2268
|
+
async function recheckNativeInputs(client, state, messages) {
|
|
2269
|
+
for (const message of messages) {
|
|
2270
|
+
if (!state.isCurrent(message)) throw new NativeInputInvalidated();
|
|
2271
|
+
const current = await client.message(message.accountId, message.id);
|
|
2272
|
+
if (current.accountId !== message.accountId || current.threadId !== message.threadId || current.id !== message.id || current.serverId !== message.serverId) throw Error("Native input identity changed; delivery held for review");
|
|
2273
|
+
const recall = nativeRecallInfo(current);
|
|
2274
|
+
if (recall && current.serverId) {
|
|
2275
|
+
const affected = state.acceptRecall(state.cursor, { ...recall, messageId: current.id, threadId: current.threadId, serverId: current.serverId });
|
|
2276
|
+
invalidateNativePreparedInputs(message.accountId, [current.id, ...affected]);
|
|
2277
|
+
throw new NativeInputInvalidated();
|
|
2278
|
+
}
|
|
2279
|
+
if (message.quote) {
|
|
2280
|
+
const quote = await client.quotedMessage(message.accountId, message.id);
|
|
2281
|
+
if (quote.accountId !== message.accountId || quote.threadId !== message.threadId || quote.messageId !== message.id) throw Error("Native quote identity changed");
|
|
2282
|
+
if (quote.state === "recalled" || quote.message && nativeRecallInfo(quote.message)) {
|
|
2283
|
+
const affected = state.acceptRecall(state.cursor, {
|
|
2284
|
+
threadId: message.threadId,
|
|
2285
|
+
serverId: message.quote.serverId,
|
|
2286
|
+
observedAt: quote.message && nativeRecallInfo(quote.message)?.observedAt || (/* @__PURE__ */ new Date()).toISOString()
|
|
2287
|
+
});
|
|
2288
|
+
invalidateNativePreparedInputs(message.accountId, affected);
|
|
2289
|
+
throw new NativeInputInvalidated();
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
if (!state.isCurrent(message)) throw new NativeInputInvalidated();
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
1887
2295
|
async function dispatchNativeMessage(params) {
|
|
2296
|
+
const revision = params.state.get(params.message.id)?.revision;
|
|
2297
|
+
try {
|
|
2298
|
+
return await dispatchNativeMessageInput(params, revision);
|
|
2299
|
+
} catch (error) {
|
|
2300
|
+
const item = params.state.get(params.message.id);
|
|
2301
|
+
if (params.state.isRecalled(params.message)) return "ignored";
|
|
2302
|
+
if (error instanceof NativeInputInvalidated && item?.state === "received") return "pending";
|
|
2303
|
+
if (item) params.state.update(params.message.id, { state: "needs_review" }, revision);
|
|
2304
|
+
return "needs_review";
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
async function dispatchNativeMessageInput(params, revision) {
|
|
1888
2308
|
const { client, state, account, message, cfg, core } = params;
|
|
1889
2309
|
const readPolicy = account.native?.readPolicy ?? "on_inbound";
|
|
1890
2310
|
const custody = state.get(message.id);
|
|
1891
|
-
if (
|
|
2311
|
+
if (state.isRecalled(message)) return "ignored";
|
|
2312
|
+
if (!custody || custody.state !== "received" || !state.isCurrent(message, revision)) return "needs_review";
|
|
1892
2313
|
if (custody.origin !== "live" || message.direction !== "incoming" || ![1, 3, 34, 43, 47, 49].includes(message.type) || message.type === 1 && !message.body || !message.senderId) {
|
|
1893
|
-
state.update(message.id, { state: "ignored" });
|
|
2314
|
+
state.update(message.id, { state: "ignored" }, revision);
|
|
1894
2315
|
return "ignored";
|
|
1895
2316
|
}
|
|
1896
2317
|
const isGroup = message.threadId.endsWith("@chatroom");
|
|
@@ -1925,32 +2346,50 @@ async function dispatchNativeMessage(params) {
|
|
|
1925
2346
|
const unauthorizedDirectCommand = !isGroup && ingress.ingress.reasonCode === "control_command_unauthorized";
|
|
1926
2347
|
if (ingress.ingress.admission !== "dispatch" && !unauthorizedDirectCommand) {
|
|
1927
2348
|
if (isGroup && policy.requireMention && !wasMentioned && !hasControlCommand && (await senderAdmission(message.senderId)).ingress.admission === "dispatch") {
|
|
1928
|
-
state.bufferGroupMessage(message.id, nativeGroupHistoryLimit(cfg));
|
|
2349
|
+
state.bufferGroupMessage(message.id, nativeGroupHistoryLimit(cfg), revision);
|
|
1929
2350
|
return "ignored";
|
|
1930
2351
|
}
|
|
1931
|
-
state.update(message.id, { state: "ignored" });
|
|
2352
|
+
state.update(message.id, { state: "ignored" }, revision);
|
|
1932
2353
|
return "ignored";
|
|
1933
2354
|
}
|
|
1934
2355
|
let preparedMedia;
|
|
1935
2356
|
const allowQuotedMessage = async (original) => original.direction === "outgoing" || !!original.senderId && (await senderAdmission(original.senderId)).ingress.admission === "dispatch";
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
2357
|
+
const assertCurrent = () => {
|
|
2358
|
+
params.signal?.throwIfAborted();
|
|
2359
|
+
if (!state.isCurrent(message, revision)) throw new NativeInputInvalidated();
|
|
2360
|
+
};
|
|
2361
|
+
const recheck = async (inputs) => {
|
|
2362
|
+
assertCurrent();
|
|
2363
|
+
await recheckNativeInputs(client, state, inputs);
|
|
2364
|
+
assertCurrent();
|
|
2365
|
+
};
|
|
2366
|
+
await recheck([message]);
|
|
2367
|
+
preparedMedia = await (params.prepareAttachment ?? prepareNativeAttachment)({ client, account, message, core, cfg, allowQuotedMessage, assertCurrent });
|
|
1942
2368
|
if (preparedMedia.pending) return "pending";
|
|
1943
2369
|
preparedMedia = await transcribeNativeAttachment(preparedMedia, core, cfg);
|
|
2370
|
+
await recheck([message]);
|
|
1944
2371
|
const buffered = isGroup && !hasControlCommand ? state.groupHistory(message.threadId, message.id, nativeGroupHistoryLimit(cfg)) : [];
|
|
1945
2372
|
const admitted = [];
|
|
1946
2373
|
for (const item of buffered) {
|
|
1947
2374
|
if (item.message.senderId && (await senderAdmission(item.message.senderId)).ingress.admission === "dispatch") admitted.push(item.message);
|
|
1948
2375
|
}
|
|
1949
2376
|
const history = await prepareNativeHistory(admitted, async (historical) => transcribeNativeAttachment(
|
|
1950
|
-
await (params.prepareAttachment ?? prepareNativeAttachment)({
|
|
2377
|
+
await (params.prepareAttachment ?? prepareNativeAttachment)({
|
|
2378
|
+
client,
|
|
2379
|
+
account,
|
|
2380
|
+
message: historical,
|
|
2381
|
+
core,
|
|
2382
|
+
cfg,
|
|
2383
|
+
allowQuotedMessage,
|
|
2384
|
+
assertCurrent: () => {
|
|
2385
|
+
assertCurrent();
|
|
2386
|
+
if (!state.isCurrent(historical)) throw new NativeInputInvalidated();
|
|
2387
|
+
}
|
|
2388
|
+
}),
|
|
1951
2389
|
core,
|
|
1952
2390
|
cfg
|
|
1953
2391
|
));
|
|
2392
|
+
await recheck([message, ...admitted]);
|
|
1954
2393
|
const context = buildNativeContext(history, message, preparedMedia);
|
|
1955
2394
|
const inputBody = context.inputBody;
|
|
1956
2395
|
const route = core.channel.routing.resolveAgentRoute({
|
|
@@ -1980,7 +2419,7 @@ async function dispatchNativeMessage(params) {
|
|
|
1980
2419
|
SenderId: message.senderId,
|
|
1981
2420
|
Provider: "agent-wechat",
|
|
1982
2421
|
Surface: "agent-wechat",
|
|
1983
|
-
MessageSid:
|
|
2422
|
+
MessageSid: nativeSourceMessageSid(message),
|
|
1984
2423
|
WasMentioned: isGroup ? wasMentioned : void 0,
|
|
1985
2424
|
Attachments: context.attachments,
|
|
1986
2425
|
InboundHistory: context.history,
|
|
@@ -1990,6 +2429,7 @@ async function dispatchNativeMessage(params) {
|
|
|
1990
2429
|
...context.mediaPath ? { MediaPath: context.mediaPath, MediaUrl: context.mediaPath, MediaType: context.mediaMime } : {}
|
|
1991
2430
|
});
|
|
1992
2431
|
await core.channel.session.recordInboundSession({ storePath, sessionKey: route.sessionKey, ctx });
|
|
2432
|
+
await recheck([message, ...admitted]);
|
|
1993
2433
|
const pipeline = (params.createPipeline ?? createChannelMessageReplyPipeline)({ cfg, agentId: route.agentId, channel: "agent-wechat", accountId: account.accountId });
|
|
1994
2434
|
const { onModelSelected, ...prefixOptions } = pipeline;
|
|
1995
2435
|
const dispatchManifest = nativeDispatchManifest({
|
|
@@ -2002,9 +2442,11 @@ async function dispatchNativeMessage(params) {
|
|
|
2002
2442
|
assets: context.assets,
|
|
2003
2443
|
history: admitted
|
|
2004
2444
|
});
|
|
2445
|
+
assertCurrent();
|
|
2005
2446
|
state.beginDispatch(message.id, dispatchManifest, buffered.map((item) => item.message.id));
|
|
2006
|
-
const callback = readPolicy !== "on_inbound" && (message.type === 1 || message.type === 49 && message.appMessageType === 19 && !!message.body || preparedMedia.quote?.type === 1 && ["resolved", "embedded"].includes(preparedMedia.quote.state) || preparedMedia.attachment?.status === "ready" && preparedMedia.suppliedAssets?.length) ? state.preparedCallback([message.id], readPolicy) : void 0;
|
|
2447
|
+
const callback = readPolicy !== "on_inbound" && (message.type === 1 || message.type === 49 && message.appMessageType === 19 && !!message.body || message.type === 49 && [3, 4, 5].includes(message.appMessageType ?? 0) && !!message.link || preparedMedia.quote?.type === 1 && ["resolved", "embedded"].includes(preparedMedia.quote.state) || preparedMedia.attachment?.status === "ready" && preparedMedia.suppliedAssets?.length) ? state.preparedCallback([message.id], readPolicy) : void 0;
|
|
2007
2448
|
if (callback) trackNativePreparedInput(dispatchManifest, callback);
|
|
2449
|
+
const beforeSend = () => recheck([message, ...admitted]);
|
|
2008
2450
|
let failed = false;
|
|
2009
2451
|
let delivery = 0;
|
|
2010
2452
|
try {
|
|
@@ -2014,15 +2456,19 @@ async function dispatchNativeMessage(params) {
|
|
|
2014
2456
|
dispatcherOptions: {
|
|
2015
2457
|
...prefixOptions,
|
|
2016
2458
|
deliver: async (payload) => {
|
|
2459
|
+
params.signal?.throwIfAborted();
|
|
2460
|
+
if (state.get(message.id)?.invalidatedByRecall) return;
|
|
2461
|
+
await beforeSend();
|
|
2017
2462
|
const urls = payload.mediaUrls?.length ? payload.mediaUrls : payload.mediaUrl ? [payload.mediaUrl] : [];
|
|
2018
2463
|
if (urls.length > 16) throw new Error("Native reply media count exceeds its bound");
|
|
2019
2464
|
if (payload.audioAsVoice && !urls.length) throw new Error("Native voice reply requires an audio attachment");
|
|
2020
|
-
for (const
|
|
2021
|
-
const key = createHash8("sha256").update(`${
|
|
2022
|
-
await sendNativeAttachment(client, account, message.threadId,
|
|
2465
|
+
for (const url2 of urls) {
|
|
2466
|
+
const key = createHash8("sha256").update(`${dispatchManifest.messageSid}\0${delivery++}`).digest("hex");
|
|
2467
|
+
await sendNativeAttachment(client, account, message.threadId, url2, payload.audioAsVoice === true, key, beforeSend);
|
|
2023
2468
|
}
|
|
2024
2469
|
if (payload.text) {
|
|
2025
|
-
|
|
2470
|
+
await beforeSend();
|
|
2471
|
+
const key = createHash8("sha256").update(`${dispatchManifest.messageSid}\0${delivery++}`).digest("hex");
|
|
2026
2472
|
await submitNativeText(client, account.native.accountId, message.threadId, payload.text, key);
|
|
2027
2473
|
}
|
|
2028
2474
|
if (urls.length || payload.text) params.onDelivered?.();
|
|
@@ -2037,18 +2483,34 @@ async function dispatchNativeMessage(params) {
|
|
|
2037
2483
|
// and media-snapshot bridge; unsupported or ambiguous handoffs leave reads unchanged.
|
|
2038
2484
|
replyOptions: { runId: dispatchManifest.runId, onModelSelected, onPreparedAgentRunStart: callback }
|
|
2039
2485
|
});
|
|
2486
|
+
params.signal?.throwIfAborted();
|
|
2487
|
+
if (state.get(message.id)?.revision !== revision || state.get(message.id)?.dispatchManifest?.runId !== dispatchManifest.runId) return state.isRecalled(message) ? "ignored" : "needs_review";
|
|
2488
|
+
if (state.get(message.id)?.invalidatedByRecall) return state.isRecalled(message) ? "ignored" : "needs_review";
|
|
2040
2489
|
const readPending = readPolicy === "on_inbound" && !failed && ingress.ingress.admission === "dispatch" ? true : state.get(message.id)?.readPending;
|
|
2041
2490
|
state.update(message.id, {
|
|
2042
2491
|
state: failed ? "needs_review" : "completed",
|
|
2043
2492
|
readPending,
|
|
2044
2493
|
handoffUnconfirmed: readPolicy === "on_model_dispatch" && !state.get(message.id)?.runId
|
|
2045
|
-
});
|
|
2494
|
+
}, revision);
|
|
2046
2495
|
return failed ? "needs_review" : "completed";
|
|
2047
2496
|
} catch {
|
|
2048
|
-
state.
|
|
2497
|
+
if (state.isRecalled(message)) return "ignored";
|
|
2498
|
+
state.update(message.id, { state: "needs_review" }, revision);
|
|
2049
2499
|
return "needs_review";
|
|
2050
2500
|
}
|
|
2051
2501
|
}
|
|
2502
|
+
async function synchronizeNativeEvents(client, accountId, state) {
|
|
2503
|
+
for (let index = 0; index < 100; index++) {
|
|
2504
|
+
const page = await client.events(accountId, state.cursor, 200);
|
|
2505
|
+
if (!page.items.length) return;
|
|
2506
|
+
const before = state.cursor;
|
|
2507
|
+
for (const event of page.items) await acceptNativeEvent(client, accountId, state, event);
|
|
2508
|
+
if (state.cursor === before) throw Error("Native recovery event cursor did not advance");
|
|
2509
|
+
await client.ack(accountId, "openclaw", state.cursor);
|
|
2510
|
+
if (page.items.length < 200) return;
|
|
2511
|
+
}
|
|
2512
|
+
throw Error("Native recovery backlog remains; no model input was dispatched");
|
|
2513
|
+
}
|
|
2052
2514
|
function startNativeReadPump(task, failed, intervalMs = 5e3) {
|
|
2053
2515
|
let active2 = false, stopped = false;
|
|
2054
2516
|
const timer = setInterval(() => {
|
|
@@ -2094,6 +2556,7 @@ async function startNativeMonitor(opts) {
|
|
|
2094
2556
|
message: item.message,
|
|
2095
2557
|
cfg,
|
|
2096
2558
|
core,
|
|
2559
|
+
signal: opts.abortSignal,
|
|
2097
2560
|
onDelivered: () => opts.setStatus({ accountId: account.accountId, lastOutboundAt: Date.now() })
|
|
2098
2561
|
});
|
|
2099
2562
|
}
|
|
@@ -2114,9 +2577,24 @@ async function startNativeMonitor(opts) {
|
|
|
2114
2577
|
handoffUnconfirmed
|
|
2115
2578
|
});
|
|
2116
2579
|
};
|
|
2117
|
-
|
|
2580
|
+
let processing;
|
|
2581
|
+
let processAgain = false;
|
|
2582
|
+
let intakeReady = false;
|
|
2583
|
+
const scheduleProcess = () => {
|
|
2584
|
+
processAgain = true;
|
|
2585
|
+
if (processing || opts.abortSignal.aborted || !intakeReady) return;
|
|
2586
|
+
processing = (async () => {
|
|
2587
|
+
do {
|
|
2588
|
+
processAgain = false;
|
|
2589
|
+
await process2();
|
|
2590
|
+
} while (processAgain && !opts.abortSignal.aborted);
|
|
2591
|
+
})().catch(() => opts.log?.error?.("Native input processing was interrupted; durable custody was retained")).finally(() => {
|
|
2592
|
+
processing = void 0;
|
|
2593
|
+
});
|
|
2594
|
+
};
|
|
2118
2595
|
const stopReadPump = startNativeReadPump(async () => {
|
|
2119
2596
|
await flushRead();
|
|
2597
|
+
scheduleProcess();
|
|
2120
2598
|
if (!opts.abortSignal.aborted) opts.setStatus({
|
|
2121
2599
|
accountId: opts.account.accountId,
|
|
2122
2600
|
handoffUnconfirmed: native.readPolicy === "on_model_dispatch" && state.items().some((value) => value.handoffUnconfirmed)
|
|
@@ -2126,18 +2604,15 @@ async function startNativeMonitor(opts) {
|
|
|
2126
2604
|
while (!opts.abortSignal.aborted) {
|
|
2127
2605
|
try {
|
|
2128
2606
|
if (state.cursor !== "0") await host2.client.ack(native.accountId, "openclaw", state.cursor);
|
|
2129
|
-
|
|
2607
|
+
intakeReady = false;
|
|
2608
|
+
await synchronizeNativeEvents(host2.client, native.accountId, state);
|
|
2609
|
+
intakeReady = true;
|
|
2610
|
+
scheduleProcess();
|
|
2130
2611
|
for await (const event of host2.client.watch(native.accountId, state.cursor, opts.abortSignal)) {
|
|
2131
|
-
|
|
2132
|
-
if (event.
|
|
2133
|
-
if (typeof event.data.messageId !== "string") throw new Error("Native committed message identity is missing; cursor retained");
|
|
2134
|
-
message = await host2.client.message(native.accountId, event.data.messageId);
|
|
2135
|
-
if (message.threadId !== event.data.threadId) throw new Error("Native committed message thread identity mismatch");
|
|
2136
|
-
}
|
|
2137
|
-
state.accept(event.cursor, message, typeof event.data.origin === "string" ? event.data.origin : "unknown");
|
|
2138
|
-
if (message?.direction === "incoming" && event.data.origin === "live") opts.setStatus({ accountId: opts.account.accountId, lastInboundAt: Date.now() });
|
|
2612
|
+
const message = await acceptNativeEvent(host2.client, native.accountId, state, event);
|
|
2613
|
+
if (message?.direction === "incoming" && event.data.origin === "live" && !nativeRecallInfo(message)) opts.setStatus({ accountId: opts.account.accountId, lastInboundAt: Date.now() });
|
|
2139
2614
|
await host2.client.ack(native.accountId, "openclaw", state.cursor);
|
|
2140
|
-
|
|
2615
|
+
scheduleProcess();
|
|
2141
2616
|
backoff = 250;
|
|
2142
2617
|
}
|
|
2143
2618
|
if (!opts.abortSignal.aborted) throw new Error("Native event stream closed");
|
|
@@ -2207,11 +2682,17 @@ async function nativeLoginWait(accountId, timeoutMs = 3e4, stopAtQr = false) {
|
|
|
2207
2682
|
}
|
|
2208
2683
|
return { connected: false, message: "Native login remains pending; wait on the same operation rather than starting a replacement" };
|
|
2209
2684
|
}
|
|
2210
|
-
async function sendNativeText(account, threadId2,
|
|
2685
|
+
async function sendNativeText(account, threadId2, text3, key = randomUUID5()) {
|
|
2211
2686
|
const native = account.native;
|
|
2212
2687
|
const { client } = await ensureNativeHost(native);
|
|
2213
|
-
const send = await submitNativeText(client, native.accountId, threadId2,
|
|
2214
|
-
return
|
|
2688
|
+
const send = await submitNativeText(client, native.accountId, threadId2, text3, key);
|
|
2689
|
+
return nativeOutboundDelivery(
|
|
2690
|
+
client,
|
|
2691
|
+
native.accountId,
|
|
2692
|
+
threadId2,
|
|
2693
|
+
send.localMessageId ? [send.localMessageId] : [],
|
|
2694
|
+
{ sendId: send.id, state: send.state, serverIds: send.serverId ? [send.serverId] : void 0 }
|
|
2695
|
+
);
|
|
2215
2696
|
}
|
|
2216
2697
|
function createNativeLoginTool(account) {
|
|
2217
2698
|
return {
|
|
@@ -2238,10 +2719,10 @@ function createNativeLoginTool(account) {
|
|
|
2238
2719
|
}
|
|
2239
2720
|
|
|
2240
2721
|
// src/channel.ts
|
|
2241
|
-
async function sendWeChatText(cfg, to,
|
|
2722
|
+
async function sendWeChatText(cfg, to, text3) {
|
|
2242
2723
|
const account = resolveWeChatAccount(cfg);
|
|
2243
2724
|
if (!account?.serverUrl) throw new Error("No native daemon configured");
|
|
2244
|
-
return sendNativeText(account, to,
|
|
2725
|
+
return sendNativeText(account, to, text3);
|
|
2245
2726
|
}
|
|
2246
2727
|
var wechatMessageAdapter = createChannelMessageAdapterFromOutbound({
|
|
2247
2728
|
id: "agent-wechat",
|
|
@@ -2251,13 +2732,13 @@ var wechatMessageAdapter = createChannelMessageAdapterFromOutbound({
|
|
|
2251
2732
|
messageSendingHooks: true
|
|
2252
2733
|
},
|
|
2253
2734
|
outbound: {
|
|
2254
|
-
sendText: async ({ cfg, to, text }) => ({
|
|
2735
|
+
sendText: async ({ cfg, to, text: text3 }) => ({
|
|
2255
2736
|
channel: "agent-wechat",
|
|
2256
|
-
|
|
2737
|
+
...await sendWeChatText(cfg, to, text3)
|
|
2257
2738
|
}),
|
|
2258
|
-
sendMedia: async ({ cfg, to, text, mediaUrl, audioAsVoice }) => ({
|
|
2739
|
+
sendMedia: async ({ cfg, to, text: text3, mediaUrl, audioAsVoice }) => ({
|
|
2259
2740
|
channel: "agent-wechat",
|
|
2260
|
-
|
|
2741
|
+
...await sendWeChatMedia(cfg, to, text3, mediaUrl, audioAsVoice)
|
|
2261
2742
|
})
|
|
2262
2743
|
}
|
|
2263
2744
|
});
|
|
@@ -2380,7 +2861,7 @@ var wechatPlugin = {
|
|
|
2380
2861
|
},
|
|
2381
2862
|
// ---- Mention adapter ----
|
|
2382
2863
|
mentions: {
|
|
2383
|
-
stripMentions: ({ text, ctx }) => normalizeWeChatCommandBody(
|
|
2864
|
+
stripMentions: ({ text: text3, ctx }) => normalizeWeChatCommandBody(text3, {
|
|
2384
2865
|
isGroup: ctx.ChatType === "group",
|
|
2385
2866
|
wasMentioned: ctx.WasMentioned === true
|
|
2386
2867
|
})
|
|
@@ -2399,13 +2880,13 @@ var wechatPlugin = {
|
|
|
2399
2880
|
// ---- Outbound adapter for OpenClaw hosts using the outbound hook ----
|
|
2400
2881
|
outbound: {
|
|
2401
2882
|
deliveryMode: "direct",
|
|
2402
|
-
sendText: async ({ cfg, to, text }) => ({
|
|
2883
|
+
sendText: async ({ cfg, to, text: text3 }) => ({
|
|
2403
2884
|
channel: "agent-wechat",
|
|
2404
|
-
|
|
2885
|
+
...await sendWeChatText(cfg, to, text3)
|
|
2405
2886
|
}),
|
|
2406
|
-
sendMedia: async ({ cfg, to, text, mediaUrl, audioAsVoice }) => ({
|
|
2887
|
+
sendMedia: async ({ cfg, to, text: text3, mediaUrl, audioAsVoice }) => ({
|
|
2407
2888
|
channel: "agent-wechat",
|
|
2408
|
-
|
|
2889
|
+
...await sendWeChatMedia(cfg, to, text3, mediaUrl, audioAsVoice)
|
|
2409
2890
|
})
|
|
2410
2891
|
},
|
|
2411
2892
|
message: wechatMessageAdapter,
|
|
@@ -2586,9 +3067,9 @@ function createNativeFacade(config, dependencies = {}) {
|
|
|
2586
3067
|
try {
|
|
2587
3068
|
const host2 = dependencies.endpoint ? void 0 : await ensureNativeHost(config);
|
|
2588
3069
|
const headers = new Headers();
|
|
2589
|
-
for (const
|
|
2590
|
-
const value = req.headers[
|
|
2591
|
-
if (typeof value === "string") headers.set(
|
|
3070
|
+
for (const name2 of ["content-type", "accept", "range", "if-none-match", "if-range", "last-event-id", "idempotency-key"]) {
|
|
3071
|
+
const value = req.headers[name2];
|
|
3072
|
+
if (typeof value === "string") headers.set(name2, value);
|
|
2592
3073
|
}
|
|
2593
3074
|
const length = req.headers["content-length"];
|
|
2594
3075
|
if (length && (!/^\d+$/.test(length) || Number(length) > MAX_BODY)) {
|
|
@@ -2612,9 +3093,9 @@ function createNativeFacade(config, dependencies = {}) {
|
|
|
2612
3093
|
response = await (dependencies.fetch ?? fetch)(new URL(relative, dependencies.endpoint), init);
|
|
2613
3094
|
}
|
|
2614
3095
|
res.statusCode = response.status;
|
|
2615
|
-
for (const
|
|
2616
|
-
const value = response.headers.get(
|
|
2617
|
-
if (value) res.setHeader(
|
|
3096
|
+
for (const name2 of ["content-type", "content-length", "content-range", "accept-ranges", "etag", "cache-control", "content-disposition", "x-content-type-options"]) {
|
|
3097
|
+
const value = response.headers.get(name2);
|
|
3098
|
+
if (value) res.setHeader(name2, value);
|
|
2618
3099
|
}
|
|
2619
3100
|
res.setHeader("x-content-type-options", "nosniff");
|
|
2620
3101
|
const reader = response.body?.getReader();
|
|
@@ -2673,18 +3154,29 @@ function object(value) {
|
|
|
2673
3154
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw Error("Expected tool arguments");
|
|
2674
3155
|
return value;
|
|
2675
3156
|
}
|
|
2676
|
-
function string(value,
|
|
2677
|
-
if (typeof value !== "string" || !value.trim() || value.length > max || /[\u0000-\u001f\u007f]/.test(value)) throw Error(`Invalid ${
|
|
3157
|
+
function string(value, name2, max = 4096) {
|
|
3158
|
+
if (typeof value !== "string" || !value.trim() || value.length > max || /[\u0000-\u001f\u007f]/.test(value)) throw Error(`Invalid ${name2}`);
|
|
2678
3159
|
return value.trim();
|
|
2679
3160
|
}
|
|
2680
3161
|
function threadId(value) {
|
|
2681
3162
|
return string(string(value, "thread ID").replace(/^(?:agent-)?wechat:(?:group:)?/i, ""), "thread ID");
|
|
2682
3163
|
}
|
|
2683
3164
|
function messageResult(message, retrievalTool = "wechat_media") {
|
|
3165
|
+
if (nativeRecallInfo(message)) return {
|
|
3166
|
+
id: message.id,
|
|
3167
|
+
threadId: message.threadId,
|
|
3168
|
+
serverId: message.serverId,
|
|
3169
|
+
direction: message.direction,
|
|
3170
|
+
type: 1e4,
|
|
3171
|
+
body: "[Message recalled]",
|
|
3172
|
+
status: "recalled",
|
|
3173
|
+
recall: nativeRecallInfo(message)
|
|
3174
|
+
};
|
|
2684
3175
|
const isQuote = message.type === 49 && message.appMessageType === 57;
|
|
2685
3176
|
const isForward = message.type === 49 && message.appMessageType === 19;
|
|
2686
|
-
const
|
|
2687
|
-
const
|
|
3177
|
+
const isLink = message.type === 49 && [3, 4, 5].includes(message.appMessageType ?? 0) && !!message.link;
|
|
3178
|
+
const kind2 = isQuote ? "quote" : isForward ? "forwarded_history" : isLink ? "link_preview" : message.type === 3 ? "image" : message.type === 47 ? "sticker" : message.type === 34 ? "voice" : message.type === 43 ? "video" : message.type === 49 && (message.appMessageType == null || message.appMessageType === 6) ? "file" : "unsupported";
|
|
3179
|
+
const body = message.type === 1 || isForward || isLink ? message.body ?? "" : isQuote ? `${message.body ?? ""}
|
|
2688
3180
|
[Quoted original not retrieved; use ${retrievalTool} with this message ID]` : `[WeChat ${kind2} message; attachment content has not been retrieved by this tool]`;
|
|
2689
3181
|
return {
|
|
2690
3182
|
id: message.id,
|
|
@@ -2695,6 +3187,7 @@ function messageResult(message, retrievalTool = "wechat_media") {
|
|
|
2695
3187
|
type: message.type,
|
|
2696
3188
|
appMessageType: message.appMessageType,
|
|
2697
3189
|
quote: message.quote,
|
|
3190
|
+
link: isLink ? message.link : void 0,
|
|
2698
3191
|
createdAt: message.createdAt,
|
|
2699
3192
|
body: body.slice(0, 4e3),
|
|
2700
3193
|
bodyTruncated: body.length > 4e3,
|
|
@@ -2712,6 +3205,7 @@ function createLookupTools(context, dependencies, archive) {
|
|
|
2712
3205
|
if (!currentConfig()) return [];
|
|
2713
3206
|
const selected = resolveWeChatAccount(currentConfig());
|
|
2714
3207
|
if (!selected?.native || !selected.enabled) return [];
|
|
3208
|
+
const configurationKey = nativeConfigurationKey(selected.native);
|
|
2715
3209
|
if (!archive && context.messageChannel !== "agent-wechat") return [];
|
|
2716
3210
|
if (!archive && context.agentAccountId && context.agentAccountId !== selected.accountId) return [];
|
|
2717
3211
|
if (!archive && context.deliveryContext?.channel === "agent-wechat" && context.deliveryContext.accountId && context.deliveryContext.accountId !== selected.accountId) return [];
|
|
@@ -2733,9 +3227,9 @@ function createLookupTools(context, dependencies, archive) {
|
|
|
2733
3227
|
if (!currentThread) return [];
|
|
2734
3228
|
}
|
|
2735
3229
|
const selectedId = selected.native.accountId;
|
|
2736
|
-
const tool = (
|
|
2737
|
-
name,
|
|
2738
|
-
label:
|
|
3230
|
+
const tool = (name2, action, description, properties, required = []) => ({
|
|
3231
|
+
name: name2,
|
|
3232
|
+
label: name2.replaceAll("_", " "),
|
|
2739
3233
|
description,
|
|
2740
3234
|
// The pinned host's tool_call wrapper JSON-serializes image blocks. Its
|
|
2741
3235
|
// public direct-only surface preserves actual image content for vision.
|
|
@@ -2748,7 +3242,7 @@ function createLookupTools(context, dependencies, archive) {
|
|
|
2748
3242
|
context.assertInvocationCurrent?.();
|
|
2749
3243
|
const cfg2 = currentConfig();
|
|
2750
3244
|
const account2 = resolveWeChatAccount(cfg2);
|
|
2751
|
-
if (!account2?.enabled || !account2.native || account2.native.accountId !== selectedId || account2.accountId !== selected.accountId) throw Error("WeChat account configuration changed; start a new turn");
|
|
3245
|
+
if (!account2?.enabled || !account2.native || account2.native.accountId !== selectedId || account2.accountId !== selected.accountId || nativeConfigurationKey(account2.native) !== configurationKey) throw Error("WeChat account configuration changed; start a new turn");
|
|
2752
3246
|
return { cfg: cfg2, account: account2, native: account2.native };
|
|
2753
3247
|
};
|
|
2754
3248
|
const { cfg, account, native } = guard();
|
|
@@ -2790,10 +3284,10 @@ function createLookupTools(context, dependencies, archive) {
|
|
|
2790
3284
|
if (action === "threads") {
|
|
2791
3285
|
const page = await client.threads(accountId, { limit, cursor, ...query ? { query } : {} });
|
|
2792
3286
|
const items = [];
|
|
2793
|
-
for (const
|
|
2794
|
-
if (isNativeTombstone(
|
|
2795
|
-
if (
|
|
2796
|
-
items.push({ id:
|
|
3287
|
+
for (const thread3 of page.items) {
|
|
3288
|
+
if (isNativeTombstone(thread3)) continue;
|
|
3289
|
+
if (thread3.accountId !== accountId) throw Error("Thread response crosses an account boundary");
|
|
3290
|
+
items.push({ id: thread3.id, peerId: thread3.peerId, kind: thread3.kind, displayName: thread3.displayName, unreadCount: thread3.unreadCount });
|
|
2797
3291
|
}
|
|
2798
3292
|
result = { ...page, items, scope: "local_profile", note: "Locally stored threads; coverage may be partial. This lookup does not mark threads read." };
|
|
2799
3293
|
} else if (action === "message" || action === "media") {
|
|
@@ -2803,6 +3297,18 @@ function createLookupTools(context, dependencies, archive) {
|
|
|
2803
3297
|
if (message.id !== messageId || message.accountId !== accountId || !archive && message.threadId !== conversation || message.direction !== "outgoing" && !await allowed(message.senderId ?? void 0)) throw Error("Message is outside the permitted conversation or sender policy");
|
|
2804
3298
|
result = messageResult(message, archive ? "wechat_archive_media" : "wechat_media");
|
|
2805
3299
|
if (action === "media") {
|
|
3300
|
+
const checkRecall = async () => {
|
|
3301
|
+
guard();
|
|
3302
|
+
const latest = await client.message(accountId, messageId);
|
|
3303
|
+
guard();
|
|
3304
|
+
if (latest.id !== message.id || latest.accountId !== accountId || latest.threadId !== message.threadId || latest.serverId !== message.serverId) throw Error("Native media message identity changed");
|
|
3305
|
+
if (nativeRecallInfo(latest)) throw new NativeApiError(410, "message_recalled", false);
|
|
3306
|
+
if (message.quote) {
|
|
3307
|
+
const quote = await client.quotedMessage(accountId, message.id);
|
|
3308
|
+
guard();
|
|
3309
|
+
if (quote.state === "recalled" || quote.message && nativeRecallInfo(quote.message)) throw new NativeApiError(410, "message_recalled", false);
|
|
3310
|
+
}
|
|
3311
|
+
};
|
|
2806
3312
|
let prepared;
|
|
2807
3313
|
try {
|
|
2808
3314
|
prepared = await prepareNativeAttachment({
|
|
@@ -2819,6 +3325,7 @@ function createLookupTools(context, dependencies, archive) {
|
|
|
2819
3325
|
allowQuotedMessage: async (original) => original.direction === "outgoing" || await allowed(original.senderId ?? void 0),
|
|
2820
3326
|
beforeMediaAccess: async () => {
|
|
2821
3327
|
if (!await allowed(requesterSenderId) || message.direction !== "outgoing" && !await allowed(message.senderId ?? void 0)) throw Error("WeChat conversation access is no longer allowed");
|
|
3328
|
+
await checkRecall();
|
|
2822
3329
|
}
|
|
2823
3330
|
});
|
|
2824
3331
|
} catch (error) {
|
|
@@ -2827,7 +3334,7 @@ function createLookupTools(context, dependencies, archive) {
|
|
|
2827
3334
|
const details2 = {
|
|
2828
3335
|
messageId: message.id,
|
|
2829
3336
|
threadId: message.threadId,
|
|
2830
|
-
status: error.retryable ? "pending" : ["unsupported_profile", "unsupported_media", "unsupported_media_variant"].includes(error.code) ? "unsupported" : "unavailable",
|
|
3337
|
+
status: error.code === "message_recalled" ? "recalled" : error.retryable ? "pending" : ["unsupported_profile", "unsupported_media", "unsupported_media_variant"].includes(error.code) ? "unsupported" : "unavailable",
|
|
2831
3338
|
retryable: error.retryable,
|
|
2832
3339
|
reason: error.code
|
|
2833
3340
|
};
|
|
@@ -2838,9 +3345,10 @@ function createLookupTools(context, dependencies, archive) {
|
|
|
2838
3345
|
const details = {
|
|
2839
3346
|
messageId: message.id,
|
|
2840
3347
|
threadId: message.threadId,
|
|
2841
|
-
status: prepared.pending ? "pending" : prepared.attachment?.status ?? prepared.quote?.state ?? "unsupported",
|
|
3348
|
+
status: prepared.reason === "message_recalled" ? "recalled" : prepared.pending ? "pending" : prepared.attachment?.status ?? prepared.quote?.state ?? "unsupported",
|
|
2842
3349
|
attachment: prepared.attachment,
|
|
2843
3350
|
quote: prepared.quote,
|
|
3351
|
+
link: ["message_recalled", "media_identity_unavailable", "media_source_changed"].includes(prepared.reason ?? "") ? void 0 : message.link,
|
|
2844
3352
|
variant: prepared.variant,
|
|
2845
3353
|
jobId: prepared.jobId,
|
|
2846
3354
|
retryable: prepared.retryable ?? false,
|
|
@@ -2850,6 +3358,13 @@ function createLookupTools(context, dependencies, archive) {
|
|
|
2850
3358
|
const image = prepared.suppliedAssets?.find((asset) => asset.role === "model_image");
|
|
2851
3359
|
const output = image ? await imageResultFromFile({ label: "WeChat attachment", path: image.path, extraText: JSON.stringify(details), details }) : { content: [{ type: "text", text: JSON.stringify(details) }], details };
|
|
2852
3360
|
guard();
|
|
3361
|
+
try {
|
|
3362
|
+
await checkRecall();
|
|
3363
|
+
} catch (error) {
|
|
3364
|
+
if (!(error instanceof NativeApiError) || error.code !== "message_recalled") throw error;
|
|
3365
|
+
const details2 = { messageId: message.id, threadId: message.threadId, status: "recalled", reason: "message_recalled", retryable: false };
|
|
3366
|
+
return { content: [{ type: "text", text: JSON.stringify(details2) }], details: details2 };
|
|
3367
|
+
}
|
|
2853
3368
|
return output;
|
|
2854
3369
|
}
|
|
2855
3370
|
} else {
|
|
@@ -2898,6 +3413,257 @@ function registerNativeHistoryTools(api) {
|
|
|
2898
3413
|
api.registerTool({ contextVersion: 2, create: (context) => createNativeArchiveTools(context) }, { names: archiveNames, optional: true });
|
|
2899
3414
|
}
|
|
2900
3415
|
|
|
3416
|
+
// src/native-share-tool.ts
|
|
3417
|
+
import { resolveStableChannelMessageIngress as resolveStableChannelMessageIngress3 } from "openclaw/plugin-sdk/channel-ingress-runtime";
|
|
3418
|
+
var name = "wechat_send_share";
|
|
3419
|
+
function text(value, label, max, multiline = false) {
|
|
3420
|
+
if (typeof value !== "string" || !value.trim() || Buffer.byteLength(value) > max || (multiline ? /[\u0000-\u0008\u000b-\u001f\u007f]/ : /[\u0000-\u001f\u007f]/).test(value)) throw Error(`Invalid ${label}`);
|
|
3421
|
+
return value;
|
|
3422
|
+
}
|
|
3423
|
+
function thread(value) {
|
|
3424
|
+
return text(text(value, "current conversation", 256).replace(/^(?:agent-)?wechat:(?:group:)?/i, "").trim(), "current conversation", 256);
|
|
3425
|
+
}
|
|
3426
|
+
function url(value, label) {
|
|
3427
|
+
const raw = text(value, label, 8192);
|
|
3428
|
+
const parsed = new URL(raw);
|
|
3429
|
+
if (!["https:", "http:"].includes(parsed.protocol) || !parsed.hostname || parsed.username || parsed.password) throw Error(`Invalid ${label}`);
|
|
3430
|
+
return raw;
|
|
3431
|
+
}
|
|
3432
|
+
function createNativeShareTools(context, dependencies = {}) {
|
|
3433
|
+
const currentConfig = () => context.getRuntimeConfig?.() ?? context.runtimeConfig ?? context.config;
|
|
3434
|
+
if (!currentConfig()) return [];
|
|
3435
|
+
const selected = resolveWeChatAccount(currentConfig());
|
|
3436
|
+
if (!selected?.enabled || !selected.native || context.messageChannel !== "agent-wechat" || context.agentAccountId && context.agentAccountId !== selected.accountId || context.deliveryContext?.channel === "agent-wechat" && context.deliveryContext.accountId && context.deliveryContext.accountId !== selected.accountId) return [];
|
|
3437
|
+
let conversation, requester;
|
|
3438
|
+
try {
|
|
3439
|
+
const native = context.nativeChannelId ? thread(context.nativeChannelId) : void 0;
|
|
3440
|
+
const delivery = context.deliveryContext?.channel === "agent-wechat" && context.deliveryContext.to ? thread(context.deliveryContext.to) : void 0;
|
|
3441
|
+
if (native && delivery && native !== delivery) return [];
|
|
3442
|
+
conversation = native ?? delivery;
|
|
3443
|
+
if (!conversation) return [];
|
|
3444
|
+
requester = text(context.requesterSenderId, "requester sender", 256);
|
|
3445
|
+
} catch {
|
|
3446
|
+
return [];
|
|
3447
|
+
}
|
|
3448
|
+
const configurationKey = nativeConfigurationKey(selected.native);
|
|
3449
|
+
const fields = ["title", "url", "description", "thumbnailUrl", "idempotencyKey"];
|
|
3450
|
+
return [{
|
|
3451
|
+
name,
|
|
3452
|
+
label: "Send WeChat share",
|
|
3453
|
+
description: "Send a URL share card to the current WeChat conversation, with an explicit title, URL, optional description and public thumbnail URL. Does not fetch or summarize the linked page. Use one idempotencyKey per intended share and reuse it after timeouts; never retry an uncertain send with a new key. Returns a durable send ID and state. Accepted means server acceptance; recipient rendering requires verification.",
|
|
3454
|
+
parameters: { type: "object", additionalProperties: false, required: ["title", "url", "idempotencyKey"], properties: {
|
|
3455
|
+
title: { type: "string", minLength: 1, maxLength: 16384 },
|
|
3456
|
+
url: { type: "string", minLength: 1, maxLength: 8192 },
|
|
3457
|
+
description: { type: "string", maxLength: 16384 },
|
|
3458
|
+
thumbnailUrl: { type: "string", minLength: 1, maxLength: 8192, description: "Public HTTP(S) image URL advertised as the card thumbnail; no upload or page scraping." },
|
|
3459
|
+
idempotencyKey: { type: "string", minLength: 1, maxLength: 200, description: "Stable unique key for this logical share. Reuse the same key and identical fields when inspecting/retrying this request." }
|
|
3460
|
+
} },
|
|
3461
|
+
execute: async (_callId, input, signal) => {
|
|
3462
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) throw Error("Expected share arguments");
|
|
3463
|
+
const args = input;
|
|
3464
|
+
if (Object.keys(args).some((key2) => !fields.includes(key2))) throw Error("Share sending is restricted to the current conversation");
|
|
3465
|
+
const key = text(args.idempotencyKey, "idempotency key", 200);
|
|
3466
|
+
const request = {
|
|
3467
|
+
kind: "share",
|
|
3468
|
+
threadId: conversation,
|
|
3469
|
+
title: text(args.title, "title", 16384, true),
|
|
3470
|
+
url: url(args.url, "URL"),
|
|
3471
|
+
...args.description !== void 0 ? { description: args.description === "" ? "" : text(args.description, "description", 16384, true) } : {},
|
|
3472
|
+
...args.thumbnailUrl !== void 0 ? { thumbnailUrl: url(args.thumbnailUrl, "thumbnail URL") } : {}
|
|
3473
|
+
};
|
|
3474
|
+
const guard = () => {
|
|
3475
|
+
signal?.throwIfAborted();
|
|
3476
|
+
context.assertInvocationCurrent?.();
|
|
3477
|
+
const cfg = currentConfig(), account = resolveWeChatAccount(cfg);
|
|
3478
|
+
if (!account?.enabled || !account.native || account.accountId !== selected.accountId || account.native.accountId !== selected.native.accountId || nativeConfigurationKey(account.native) !== configurationKey) throw Error("WeChat account configuration changed; start a new turn");
|
|
3479
|
+
return { cfg, account, native: account.native };
|
|
3480
|
+
};
|
|
3481
|
+
const core = dependencies.core ?? getWeChatRuntime();
|
|
3482
|
+
const authorize = async () => {
|
|
3483
|
+
const { cfg, account } = guard();
|
|
3484
|
+
const result = await resolveWeChatIngress({
|
|
3485
|
+
resolve: core.channel.inbound?.ingress?.resolveStable ?? resolveStableChannelMessageIngress3,
|
|
3486
|
+
cfg,
|
|
3487
|
+
accountId: account.accountId,
|
|
3488
|
+
policy: resolveWeChatPolicyContext({ account, cfg, chatId: conversation }),
|
|
3489
|
+
isGroup: conversation.endsWith("@chatroom"),
|
|
3490
|
+
chatId: conversation,
|
|
3491
|
+
senderId: requester,
|
|
3492
|
+
readStoreAllowFrom: async () => core.channel.pairing.readAllowFromStore({ channel: "agent-wechat", accountId: account.accountId, env: process.env }).catch(() => [])
|
|
3493
|
+
});
|
|
3494
|
+
guard();
|
|
3495
|
+
if (result.ingress.admission !== "dispatch") throw Error("WeChat conversation access is no longer allowed");
|
|
3496
|
+
};
|
|
3497
|
+
await authorize();
|
|
3498
|
+
const { native } = guard();
|
|
3499
|
+
const { client } = await (dependencies.host ?? ensureNativeHost)(native);
|
|
3500
|
+
await authorize();
|
|
3501
|
+
let intent;
|
|
3502
|
+
try {
|
|
3503
|
+
intent = await client.sendShare(native.accountId, request, key, signal);
|
|
3504
|
+
} catch (error) {
|
|
3505
|
+
if (!(error instanceof NativeApiError) || error.status >= 500 || error.code === "timeout") {
|
|
3506
|
+
const details2 = { threadId: conversation, idempotencyKey: key, state: "submission_unknown", note: "Inspect the send by this idempotency key. Do not resend with a new key." };
|
|
3507
|
+
return { content: [{ type: "text", text: JSON.stringify(details2) }], details: details2, isError: true };
|
|
3508
|
+
}
|
|
3509
|
+
throw error;
|
|
3510
|
+
}
|
|
3511
|
+
if (intent.accountId !== native.accountId || intent.destination !== conversation || !intent.id) throw Error("Share receipt crosses an account or conversation boundary; inspect the original idempotency key before another send");
|
|
3512
|
+
const delivery = intent.state === "accepted" && intent.localMessageId && intent.serverId ? await nativeOutboundDelivery(client, native.accountId, conversation, [intent.localMessageId], { sendId: intent.id, state: intent.state, serverIds: [intent.serverId] }) : void 0;
|
|
3513
|
+
const details = {
|
|
3514
|
+
id: intent.id,
|
|
3515
|
+
threadId: conversation,
|
|
3516
|
+
idempotencyKey: key,
|
|
3517
|
+
state: intent.state,
|
|
3518
|
+
serverId: intent.serverId,
|
|
3519
|
+
messageId: delivery?.messageId || void 0,
|
|
3520
|
+
receiptState: delivery?.meta.receiptState,
|
|
3521
|
+
reason: intent.errorCode,
|
|
3522
|
+
note: ["accepted", "sent", "server_accepted"].includes(intent.state) ? "Server accepted; recipient rendering is not confirmed." : "Inspect this send ID or reuse the identical request/key. Do not send again with a new key."
|
|
3523
|
+
};
|
|
3524
|
+
return { content: [{ type: "text", text: JSON.stringify(details) }], details, isError: ["failed", "needs_review", "cancelled"].includes(intent.state) };
|
|
3525
|
+
}
|
|
3526
|
+
}];
|
|
3527
|
+
}
|
|
3528
|
+
function registerNativeShareTools(api) {
|
|
3529
|
+
api.registerTool({ contextVersion: 2, create: (context) => createNativeShareTools(context) }, { names: [name] });
|
|
3530
|
+
}
|
|
3531
|
+
|
|
3532
|
+
// src/native-recall-tools.ts
|
|
3533
|
+
import { createHash as createHash9 } from "node:crypto";
|
|
3534
|
+
import { resolveStableChannelMessageIngress as resolveStableChannelMessageIngress4 } from "openclaw/plugin-sdk/channel-ingress-runtime";
|
|
3535
|
+
var names2 = ["wechat_recall"];
|
|
3536
|
+
function text2(value, label) {
|
|
3537
|
+
if (typeof value !== "string" || !value.trim() || value.length > 4096 || /[\u0000-\u001f\u007f]/.test(value)) throw Error(`Invalid ${label}`);
|
|
3538
|
+
return value.trim();
|
|
3539
|
+
}
|
|
3540
|
+
function thread2(value) {
|
|
3541
|
+
return text2(text2(value, "conversation").replace(/^(?:agent-)?wechat:(?:group:)?/i, ""), "conversation");
|
|
3542
|
+
}
|
|
3543
|
+
function nativeRecallKey(accountId, threadId2, messageId, serverId) {
|
|
3544
|
+
return `openclaw-recall-${createHash9("sha256").update(JSON.stringify([accountId, threadId2, messageId, serverId])).digest("hex")}`;
|
|
3545
|
+
}
|
|
3546
|
+
function createNativeRecallTools(context, dependencies = {}) {
|
|
3547
|
+
const currentConfig = () => context.getRuntimeConfig?.() ?? context.runtimeConfig ?? context.config;
|
|
3548
|
+
if (!currentConfig()) return [];
|
|
3549
|
+
const selected = resolveWeChatAccount(currentConfig());
|
|
3550
|
+
if (!selected?.enabled || !selected.native || context.messageChannel !== "agent-wechat" || context.agentAccountId && context.agentAccountId !== selected.accountId || context.deliveryContext?.channel === "agent-wechat" && context.deliveryContext.accountId && context.deliveryContext.accountId !== selected.accountId) return [];
|
|
3551
|
+
let conversation, requester;
|
|
3552
|
+
try {
|
|
3553
|
+
const nativeThread = context.nativeChannelId ? thread2(context.nativeChannelId) : void 0;
|
|
3554
|
+
const delivered = context.deliveryContext?.channel === "agent-wechat" && context.deliveryContext.to ? thread2(context.deliveryContext.to) : void 0;
|
|
3555
|
+
if (nativeThread && delivered && nativeThread !== delivered) return [];
|
|
3556
|
+
conversation = text2(nativeThread ?? delivered, "conversation");
|
|
3557
|
+
requester = text2(context.requesterSenderId, "requester");
|
|
3558
|
+
} catch {
|
|
3559
|
+
return [];
|
|
3560
|
+
}
|
|
3561
|
+
const accountId = selected.native.accountId;
|
|
3562
|
+
const configurationKey = nativeConfigurationKey(selected.native);
|
|
3563
|
+
return [{
|
|
3564
|
+
name: names2[0],
|
|
3565
|
+
label: "WeChat Recall",
|
|
3566
|
+
description: "Recall one own sent message in the current WeChat conversation by its exact opaque message ID from a verified send receipt or wechat_history, or inspect a retained recalled placeholder without another native mutation. The daemon checks eligibility. Returns the durable operation and confirmed, rejected, pending or uncertain result. Never invent IDs or resubmit an uncertain action with a new key; prior model input and completed actions cannot be erased.",
|
|
3567
|
+
parameters: { type: "object", additionalProperties: false, properties: {
|
|
3568
|
+
messageId: { type: "string", minLength: 1, maxLength: 4096, description: "Exact stored message ID in this conversation; send/job/operation IDs are not message IDs." },
|
|
3569
|
+
serverId: { type: "string", pattern: "^[1-9][0-9]{0,19}$", description: "Exact decimal server ID from the same verified receipt or history entry. The stored local message ID alone may be reused." }
|
|
3570
|
+
}, required: ["messageId", "serverId"] },
|
|
3571
|
+
execute: async (_callId, input, signal) => {
|
|
3572
|
+
if (!input || typeof input !== "object" || Array.isArray(input) || Object.keys(input).some((key2) => !["messageId", "serverId"].includes(key2))) throw Error("Recall is restricted to a message ID in the current conversation");
|
|
3573
|
+
const messageId = text2(input.messageId, "message ID");
|
|
3574
|
+
const serverId = text2(input.serverId, "server ID");
|
|
3575
|
+
if (!/^[1-9]\d{0,19}$/.test(serverId) || BigInt(serverId) > 18446744073709551615n) throw Error("Invalid exact server message ID");
|
|
3576
|
+
const guard = () => {
|
|
3577
|
+
signal?.throwIfAborted();
|
|
3578
|
+
context.assertInvocationCurrent?.();
|
|
3579
|
+
const cfg = currentConfig(), account = resolveWeChatAccount(cfg);
|
|
3580
|
+
if (!account?.enabled || !account.native || account.accountId !== selected.accountId || account.native.accountId !== accountId || nativeConfigurationKey(account.native) !== configurationKey) throw Error("WeChat account configuration changed; start a new turn");
|
|
3581
|
+
return { cfg, account, native: account.native };
|
|
3582
|
+
};
|
|
3583
|
+
const allowed = async () => {
|
|
3584
|
+
const { cfg, account } = guard();
|
|
3585
|
+
const core = dependencies.core ?? getWeChatRuntime();
|
|
3586
|
+
const ingress = await resolveWeChatIngress({
|
|
3587
|
+
resolve: core.channel.inbound?.ingress?.resolveStable ?? resolveStableChannelMessageIngress4,
|
|
3588
|
+
cfg,
|
|
3589
|
+
accountId: account.accountId,
|
|
3590
|
+
policy: resolveWeChatPolicyContext({ account, cfg, chatId: conversation }),
|
|
3591
|
+
isGroup: conversation.endsWith("@chatroom"),
|
|
3592
|
+
chatId: conversation,
|
|
3593
|
+
senderId: requester,
|
|
3594
|
+
readStoreAllowFrom: async () => core.channel.pairing.readAllowFromStore({ channel: "agent-wechat", accountId: account.accountId, env: process.env }).catch(() => [])
|
|
3595
|
+
});
|
|
3596
|
+
guard();
|
|
3597
|
+
if (ingress.ingress.admission !== "dispatch") throw Error("WeChat conversation access is no longer allowed");
|
|
3598
|
+
};
|
|
3599
|
+
await allowed();
|
|
3600
|
+
const { native } = guard();
|
|
3601
|
+
const { client } = await (dependencies.host ?? ensureNativeHost)(native);
|
|
3602
|
+
guard();
|
|
3603
|
+
const message = await client.message(accountId, messageId);
|
|
3604
|
+
guard();
|
|
3605
|
+
const retainedRecall = !!nativeRecallInfo(message);
|
|
3606
|
+
if (message.id !== messageId || message.accountId !== accountId || message.threadId !== conversation || message.direction !== "outgoing" && !retainedRecall) throw Error("Recall target is outside this conversation or was not sent by this account");
|
|
3607
|
+
if (message.serverId !== serverId) throw Error("The exact server identity does not match this receipt; the local message identity may have been reused");
|
|
3608
|
+
await allowed();
|
|
3609
|
+
guard();
|
|
3610
|
+
const key = nativeRecallKey(accountId, conversation, messageId, serverId);
|
|
3611
|
+
let operation;
|
|
3612
|
+
let operationId;
|
|
3613
|
+
const result = (status2, extra = {}) => {
|
|
3614
|
+
const details = {
|
|
3615
|
+
messageId,
|
|
3616
|
+
serverId,
|
|
3617
|
+
threadId: conversation,
|
|
3618
|
+
accountId,
|
|
3619
|
+
action: retainedRecall ? "inspect_recalled" : "recall",
|
|
3620
|
+
idempotencyKey: key,
|
|
3621
|
+
operationId,
|
|
3622
|
+
operationState: operation?.state,
|
|
3623
|
+
status: status2,
|
|
3624
|
+
retryable: false,
|
|
3625
|
+
...extra
|
|
3626
|
+
};
|
|
3627
|
+
return { content: [{ type: "text", text: JSON.stringify(details) }], details };
|
|
3628
|
+
};
|
|
3629
|
+
try {
|
|
3630
|
+
operation = await client.recallMessage(accountId, messageId, serverId, key, signal);
|
|
3631
|
+
const checkOperation = (candidate) => {
|
|
3632
|
+
if (!candidate || typeof candidate.id !== "string" || !candidate.id || operationId && candidate.id !== operationId || candidate.accountId !== accountId || candidate.kind !== "message_recall") throw Error("Recall operation identity is invalid");
|
|
3633
|
+
};
|
|
3634
|
+
checkOperation(operation);
|
|
3635
|
+
operationId = operation.id;
|
|
3636
|
+
const deadline = Date.now() + Math.max(0, Math.min(dependencies.waitMs ?? 2e3, 1e4));
|
|
3637
|
+
while (["queued", "running", "submitting"].includes(operation.state) && Date.now() < deadline) {
|
|
3638
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(250, deadline - Date.now())));
|
|
3639
|
+
guard();
|
|
3640
|
+
const remaining = Math.max(1, deadline - Date.now());
|
|
3641
|
+
const next = await client.operation(accountId, operation.id, signal ? AbortSignal.any([signal, AbortSignal.timeout(remaining)]) : AbortSignal.timeout(remaining));
|
|
3642
|
+
checkOperation(next);
|
|
3643
|
+
operation = next;
|
|
3644
|
+
}
|
|
3645
|
+
guard();
|
|
3646
|
+
} catch (error) {
|
|
3647
|
+
const reason = error instanceof NativeApiError ? error.code : "recall_result_unavailable";
|
|
3648
|
+
return result("needs_review", { reason, note: "Inspect the existing operation or repeat only this same message with the same idempotency key. No automatic second submission was made." });
|
|
3649
|
+
}
|
|
3650
|
+
const receipt = operation.result && typeof operation.result === "object" ? operation.result : {};
|
|
3651
|
+
if (receipt.messageId !== void 0 && receipt.messageId !== messageId || receipt.threadId !== void 0 && receipt.threadId !== conversation || receipt.serverId !== void 0 && receipt.serverId !== serverId) throw Error("Recall operation target identity is invalid");
|
|
3652
|
+
const exactReceipt = receipt.messageId === messageId && receipt.threadId === conversation && receipt.serverId === serverId;
|
|
3653
|
+
const status = operation.state === "succeeded" && exactReceipt && ["recalled", "already_recalled"].includes(String(receipt.status)) ? String(receipt.status) : ["queued", "running", "submitting"].includes(operation.state) ? "pending" : operation.state === "failed" && exactReceipt && ["not_self", "not_eligible", "unavailable", "not_found"].includes(String(receipt.status)) ? String(receipt.status) : "needs_review";
|
|
3654
|
+
const boundedState = (value) => typeof value === "string" && /^[a-z0-9_]{1,80}$/.test(value) ? value : void 0;
|
|
3655
|
+
return result(status, {
|
|
3656
|
+
remoteOutcome: boundedState(receipt.remoteOutcome),
|
|
3657
|
+
profileProjection: boundedState(receipt.profileProjection),
|
|
3658
|
+
note: status === "pending" || status === "needs_review" ? "Inspect this durable operation; do not automatically submit a replacement recall." : void 0
|
|
3659
|
+
});
|
|
3660
|
+
}
|
|
3661
|
+
}];
|
|
3662
|
+
}
|
|
3663
|
+
function registerNativeRecallTools(api) {
|
|
3664
|
+
api.registerTool({ contextVersion: 2, create: (context) => createNativeRecallTools(context) }, { names: names2 });
|
|
3665
|
+
}
|
|
3666
|
+
|
|
2901
3667
|
// index.ts
|
|
2902
3668
|
var plugin = {
|
|
2903
3669
|
id: "agent-wechat",
|
|
@@ -2909,6 +3675,8 @@ var plugin = {
|
|
|
2909
3675
|
const selected = resolveWeChatAccount(api.config);
|
|
2910
3676
|
api.registerChannel({ plugin: wechatPlugin });
|
|
2911
3677
|
registerNativeHistoryTools(api);
|
|
3678
|
+
registerNativeShareTools(api);
|
|
3679
|
+
registerNativeRecallTools(api);
|
|
2912
3680
|
const stopFacade = selected?.native ? registerNativeFacade(api, selected.native) : () => {
|
|
2913
3681
|
};
|
|
2914
3682
|
const stopPreparedBoundary = selected?.native?.readPolicy && selected.native.readPolicy !== "on_inbound" ? registerNativePreparedBoundary(api) : () => {
|