@agent-wechat/agent-wechat 1.0.0-beta.12 → 1.0.0-beta.14
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 +8 -0
- package/dist/index.js +238 -187
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -23,6 +23,12 @@ Configure plugin defaults under `plugins.entries.agent-wechat.config` (or the sa
|
|
|
23
23
|
|
|
24
24
|
The external service remains independently owned. Plugin stop closes its stream and never stops/logs out the daemon or cancels its sends. Remote endpoints require verified HTTPS or an encrypted tunnel terminating at loopback. Set `AGENT_WECHAT_TOKEN` in the Gateway process environment, or use a configured OpenClaw secret provider. The plugin resolves `native.token` through the pinned OpenClaw SDK; `native.tokenFile` is removed. Tokens contain 32–1024 printable ASCII bytes without spaces. An explicit unresolved reference fails startup without falling back to another token. The API token needs read/media/send/login for the selected account, plus read-state only when automatic whole-thread read marking is enabled; do not grant admin solely to run the plugin. Normal sender/DM/group/mention policy remains in `channels.agent-wechat`; defaults remain closed.
|
|
25
25
|
|
|
26
|
+
## Login tool
|
|
27
|
+
|
|
28
|
+
`wechat_login` is available to configured agents through OpenClaw's tool policy, including from other channels while WeChat is offline. Use `{"action":"status"}` to inspect the account, `{"action":"start"}` to start or resume login and display the returned QR image, then `{"action":"wait","timeoutMs":30000}` to check scan/approval completion. Repeated starts reuse the pending operation. For a new operation, `force: true` uses QR instead of saved trust. Waits are bounded and report the operation ID and state. Cancelling a tool call leaves the submitted login operation available for a later call.
|
|
29
|
+
|
|
30
|
+
The tool also accepts `disconnect` (retain saved trust), `logout` (durable native logout) and `forget` with `confirm: true` (remove local authentication, retain history). It stays bound to the configured daemon/account. Apply OpenClaw tool allow/deny policies to control which agents can manage login. The tool is directly callable so the daemon's PNG reaches the model as image content.
|
|
31
|
+
|
|
26
32
|
## Native managed mode
|
|
27
33
|
|
|
28
34
|
```json
|
|
@@ -80,6 +86,8 @@ Native text, image, file, video and voice delivery results return actual stored
|
|
|
80
86
|
|
|
81
87
|
Incoming `message.recalled` events are consumed independently of slow media and model work, including recalls of own or unmentioned messages. The consumer persists the event cursor together with a tombstone, removes recalled body/quote content and attachment references from its custody records, and invalidates pending model proofs. Tombstones survive restart and suppress a late original. Attachment retrieval, transcription, model dispatch and later automatic reply delivery recheck the selected messages so late work cannot reinsert recalled input. A quote's unrelated comment remains available after its recalled original is removed.
|
|
82
88
|
|
|
89
|
+
Received quotes use the parsed embedded content delivered by WeChat's server protocol and included in the normal message response's `quote` field. Text and link details render immediately; image metadata can retrieve verified bytes under the reply's own message ID. Other quoted media keeps the current comment and an unavailable attachment status. Local recall suppression and quoted-author admission still apply, including in history tools. A stored original never replaces the embedded excerpt; quote display needs no separate quote endpoint.
|
|
90
|
+
|
|
83
91
|
If a source-local message ID later names a different exact thread/server target, the consumer admits that new message with a separate host turn and reply key. Completion callbacks, media results and read receipts captured for the old target cannot overwrite the new custody record.
|
|
84
92
|
|
|
85
93
|
Stored-history tools return a recalled placeholder; media tools return terminal `status: "recalled"`, `reason: "message_recalled"`, `retryable: false` without attachment paths or image blocks. Recall does not erase an already submitted model input, an existing OpenClaw transcript, a completed action, or attachment bytes retained by OpenClaw's storage policy. An affected in-flight turn is held for review and its later automatic reply is suppressed; the plugin does not claim retroactive transcript correction.
|
package/dist/index.js
CHANGED
|
@@ -192,7 +192,7 @@ var NativeWeChatClient = class {
|
|
|
192
192
|
if (typeof options.token !== "string" || !options.token || /[\r\n]/.test(options.token))
|
|
193
193
|
throw new Error("Native API bearer token is required");
|
|
194
194
|
this.token = options.token;
|
|
195
|
-
this.fetchImpl = options.fetch ?? fetch;
|
|
195
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
196
196
|
this.timeoutMs = options.timeoutMs ?? 3e4;
|
|
197
197
|
this.mediaTimeoutMs = options.mediaTimeoutMs ?? 60 * 6e4;
|
|
198
198
|
if (!Number.isInteger(this.mediaTimeoutMs) || this.mediaTimeoutMs < 1 || this.mediaTimeoutMs > 60 * 6e4)
|
|
@@ -279,9 +279,22 @@ var NativeWeChatClient = class {
|
|
|
279
279
|
throw new Error("Account ID or alias is required");
|
|
280
280
|
return this.request("v1/accounts", { method: "POST", body: JSON.stringify(body) });
|
|
281
281
|
}
|
|
282
|
+
removeAccount(accountId, confirmAccountId) {
|
|
283
|
+
if (accountId !== confirmAccountId)
|
|
284
|
+
throw new Error("Account confirmation does not match");
|
|
285
|
+
return this.request(`v1/accounts/${id(accountId)}`, { method: "DELETE", body: JSON.stringify({ confirmAccountId }) });
|
|
286
|
+
}
|
|
282
287
|
updateAccount(accountId, options) {
|
|
283
288
|
return this.request(`v1/accounts/${id(accountId)}`, { method: "PATCH", body: JSON.stringify(options) });
|
|
284
289
|
}
|
|
290
|
+
accountSettings(accountId, signal) {
|
|
291
|
+
return this.request(`v1/accounts/${id(accountId)}/settings`, { signal });
|
|
292
|
+
}
|
|
293
|
+
updateAccountSettings(accountId, settings, signal) {
|
|
294
|
+
if (!Number.isSafeInteger(settings.autoDownloadMaxBytes) || settings.autoDownloadMaxBytes < 0 || settings.autoDownloadMaxBytes > NATIVE_MAX_SOURCE_BYTES)
|
|
295
|
+
throw new Error("Invalid automatic download limit");
|
|
296
|
+
return this.request(`v1/accounts/${id(accountId)}/settings`, { method: "PATCH", body: JSON.stringify(settings), signal });
|
|
297
|
+
}
|
|
285
298
|
capabilities(accountId) {
|
|
286
299
|
return this.request(`v1/accounts/${id(accountId)}/capabilities`);
|
|
287
300
|
}
|
|
@@ -452,15 +465,6 @@ var NativeWeChatClient = class {
|
|
|
452
465
|
validateMessageRecall(message);
|
|
453
466
|
return message;
|
|
454
467
|
}
|
|
455
|
-
/** Resolve only the stored original in this message's own conversation. */
|
|
456
|
-
async quotedMessage(accountId, messageId) {
|
|
457
|
-
const quote = await this.request(`v1/accounts/${id(accountId)}/messages/${id(messageId)}/quote`);
|
|
458
|
-
if (quote.accountId !== accountId || quote.messageId !== messageId || typeof quote.threadId !== "string" || !["resolved", "embedded", "missing", "invalid", "unsupported", "ambiguous", "recalled"].includes(quote.state) || (quote.state === "resolved" ? !quote.message || quote.message.accountId !== accountId || quote.message.threadId !== quote.threadId || quote.message.id === messageId : quote.message != null))
|
|
459
|
-
throw new Error("Invalid native quote identity");
|
|
460
|
-
if (quote.message)
|
|
461
|
-
validateMessageRecall(quote.message);
|
|
462
|
-
return quote;
|
|
463
|
-
}
|
|
464
468
|
/** Account-wide local search, with the daemon's query-bound opaque cursor. */
|
|
465
469
|
async searchMessages(accountId, options) {
|
|
466
470
|
const result = await this.request(`v1/accounts/${id(accountId)}/messages${listQuery(options)}`);
|
|
@@ -638,7 +642,7 @@ var NativeWeChatClient = class {
|
|
|
638
642
|
ack(accountId, consumerId, cursor) {
|
|
639
643
|
return this.request(`v1/accounts/${id(accountId)}/consumers/${id(consumerId)}/ack`, { method: "POST", body: JSON.stringify({ cursor: decimal(cursor) }) });
|
|
640
644
|
}
|
|
641
|
-
async *watch(accountId, after = "0", signal) {
|
|
645
|
+
async *watch(accountId, after = "0", signal, options = {}) {
|
|
642
646
|
const connection = new AbortController();
|
|
643
647
|
const streamSignal = signal ? AbortSignal.any([signal, connection.signal]) : connection.signal;
|
|
644
648
|
const bounded = async (work, milliseconds, reason) => {
|
|
@@ -680,6 +684,7 @@ var NativeWeChatClient = class {
|
|
|
680
684
|
const { value, done } = await bounded(() => activeReader.read(), this.streamIdleTimeoutMs, "Native SSE idle timeout");
|
|
681
685
|
if (done)
|
|
682
686
|
break;
|
|
687
|
+
options.onActivity?.();
|
|
683
688
|
buffer += decoder.decode(value, { stream: true });
|
|
684
689
|
if (buffer.length > 1024 * 1024)
|
|
685
690
|
throw new Error("Native SSE frame exceeds its bound");
|
|
@@ -1337,13 +1342,13 @@ async function nativeOutboundDelivery(client, accountId, threadId2, messageIds,
|
|
|
1337
1342
|
async function submitNativeText(client, accountId, threadId2, text3, key, options = {}) {
|
|
1338
1343
|
let intent = await client.sendText(accountId, threadId2, text3, key);
|
|
1339
1344
|
const deadline = Date.now() + Math.max(1, Math.min(options.timeoutMs ?? 65e3, 65e3));
|
|
1340
|
-
let
|
|
1345
|
+
let delay2 = Math.max(1, Math.min(options.initialDelayMs ?? 250, 1e3));
|
|
1341
1346
|
while (["queued", "submitting"].includes(intent.state) && Date.now() < deadline) {
|
|
1342
|
-
await new Promise((resolve) => setTimeout(resolve, Math.min(
|
|
1347
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(delay2, deadline - Date.now())));
|
|
1343
1348
|
const remaining = deadline - Date.now();
|
|
1344
1349
|
if (remaining <= 0) break;
|
|
1345
1350
|
intent = await client.send(accountId, intent.id, AbortSignal.timeout(remaining));
|
|
1346
|
-
|
|
1351
|
+
delay2 = Math.min(delay2 * 2, 1e3);
|
|
1347
1352
|
}
|
|
1348
1353
|
if (!["accepted", "sent", "server_accepted"].includes(intent.state)) throw new Error(`Native send ${intent.id} requires inspection (${intent.state}); do not automatically resend`);
|
|
1349
1354
|
return intent;
|
|
@@ -1396,6 +1401,34 @@ function recalledNativeMessage(message, recall) {
|
|
|
1396
1401
|
};
|
|
1397
1402
|
}
|
|
1398
1403
|
|
|
1404
|
+
// src/native-quote.ts
|
|
1405
|
+
function renderEmbeddedQuote(message, permitted = true, imageBody) {
|
|
1406
|
+
const comment = (message.body ?? "").slice(0, 16384);
|
|
1407
|
+
const source = message.quote;
|
|
1408
|
+
const unavailable = (state2) => ({
|
|
1409
|
+
body: `${comment}${comment ? "\n\n" : ""}[Quoted message ${state2}]`,
|
|
1410
|
+
image: false,
|
|
1411
|
+
quote: { state: state2, type: source?.type, senderId: source?.senderId ?? void 0 }
|
|
1412
|
+
});
|
|
1413
|
+
if (!source) return unavailable("unavailable");
|
|
1414
|
+
if (!permitted) return unavailable("not permitted");
|
|
1415
|
+
const state = source.state ?? "missing";
|
|
1416
|
+
if (state === "recalled") return unavailable("recalled");
|
|
1417
|
+
if (!["embedded", "missing", "unsupported"].includes(state) || source.embeddedText != null && (typeof source.embeddedText !== "string" || source.embeddedText.length > 16384) || source.embeddedImage != null && typeof source.embeddedImage !== "boolean") return unavailable("invalid");
|
|
1418
|
+
const label = source.type === 1 ? "text" : source.type === 3 ? "image" : source.type === 34 ? "voice" : source.type === 43 ? "video" : source.type === 49 && source.appMessageType === 5 ? "link" : source.type === 49 && source.appMessageType === 6 ? "file" : "message";
|
|
1419
|
+
const image = state === "embedded" && source.type === 3 && source.embeddedImage === true;
|
|
1420
|
+
if (state === "embedded" && !image && !source.embeddedText) return unavailable("invalid");
|
|
1421
|
+
const content = state === "missing" ? "" : image ? imageBody ?? "[Image metadata embedded in quote; image not retrieved]" : source.embeddedText ?? "";
|
|
1422
|
+
const status = state === "unsupported" ? `[${label} attachment unavailable from embedded quote]` : state === "missing" ? "[Embedded quote content unavailable]" : "";
|
|
1423
|
+
return {
|
|
1424
|
+
body: `${comment}${comment ? "\n\n" : ""}[Quoted ${label} from ${source.senderId ?? "unknown sender"}; embedded in reply]
|
|
1425
|
+
${[content, status].filter(Boolean).join("\n")}
|
|
1426
|
+
[/Quoted message]`,
|
|
1427
|
+
image,
|
|
1428
|
+
quote: { state, type: source.type, senderId: source.senderId ?? void 0 }
|
|
1429
|
+
};
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1399
1432
|
// src/native-media.ts
|
|
1400
1433
|
function kind(type) {
|
|
1401
1434
|
return type === 3 || type === 47 ? "image" : type === 34 ? "audio" : type === 43 ? "video" : type === 49 ? "file" : void 0;
|
|
@@ -1438,11 +1471,11 @@ function mediaSendReceiptIds(job, audioAsVoice) {
|
|
|
1438
1471
|
async function waitNativeMediaSendReceipts(client, accountId, threadId2, initial, audioAsVoice, timeoutMs = 1e4) {
|
|
1439
1472
|
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");
|
|
1440
1473
|
const deadline = Date.now() + Math.max(0, Math.min(timeoutMs, 1e4));
|
|
1441
|
-
let job = initial,
|
|
1474
|
+
let job = initial, delay2 = 100;
|
|
1442
1475
|
while (successfulSendJobs.has(job.state) && !mediaSendReceiptIds(job, audioAsVoice) && Date.now() < deadline) {
|
|
1443
1476
|
const result = mediaSendResult(job);
|
|
1444
1477
|
if (["unknown", "not_applicable"].includes(String(result.profileProjection)) || audioAsVoice && Array.isArray(result.chunks) && result.chunks.some((chunk) => !chunk || chunk.state !== "accepted")) break;
|
|
1445
|
-
await new Promise((resolve) => setTimeout(resolve, Math.min(
|
|
1478
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(delay2, Math.max(0, deadline - Date.now()))));
|
|
1446
1479
|
const remaining = deadline - Date.now();
|
|
1447
1480
|
if (remaining <= 0) break;
|
|
1448
1481
|
let next;
|
|
@@ -1453,7 +1486,7 @@ async function waitNativeMediaSendReceipts(client, accountId, threadId2, initial
|
|
|
1453
1486
|
}
|
|
1454
1487
|
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;
|
|
1455
1488
|
job = next;
|
|
1456
|
-
|
|
1489
|
+
delay2 = Math.min(delay2 * 2, 1e3);
|
|
1457
1490
|
}
|
|
1458
1491
|
return job;
|
|
1459
1492
|
}
|
|
@@ -1463,11 +1496,11 @@ async function waitNativeMediaJob(client, accountId, initial, timeoutMs = 65e3,
|
|
|
1463
1496
|
async function waitNativeMediaStatus(initial, timeoutMs, signal, assertCurrent, readStatus) {
|
|
1464
1497
|
const deadline = Date.now() + timeoutMs;
|
|
1465
1498
|
let job = initial;
|
|
1466
|
-
let
|
|
1499
|
+
let delay2 = 250;
|
|
1467
1500
|
while (!terminalJobs.has(job.state) && Date.now() < deadline) {
|
|
1468
1501
|
signal?.throwIfAborted();
|
|
1469
1502
|
assertCurrent?.();
|
|
1470
|
-
await new Promise((resolve) => setTimeout(resolve, Math.min(
|
|
1503
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(delay2, Math.max(0, deadline - Date.now()))));
|
|
1471
1504
|
const remaining = deadline - Date.now();
|
|
1472
1505
|
if (remaining <= 0) break;
|
|
1473
1506
|
signal?.throwIfAborted();
|
|
@@ -1475,7 +1508,7 @@ async function waitNativeMediaStatus(initial, timeoutMs, signal, assertCurrent,
|
|
|
1475
1508
|
const next = await readStatus(job, signal ? AbortSignal.any([signal, AbortSignal.timeout(remaining)]) : AbortSignal.timeout(remaining));
|
|
1476
1509
|
if (!next) break;
|
|
1477
1510
|
job = next;
|
|
1478
|
-
|
|
1511
|
+
delay2 = Math.min(delay2 * 2, 1e3);
|
|
1479
1512
|
}
|
|
1480
1513
|
return job;
|
|
1481
1514
|
}
|
|
@@ -1551,83 +1584,37 @@ ${prepared.body}`,
|
|
|
1551
1584
|
};
|
|
1552
1585
|
}
|
|
1553
1586
|
async function prepareNativeQuote(params) {
|
|
1554
|
-
const {
|
|
1555
|
-
const
|
|
1587
|
+
const { message } = params;
|
|
1588
|
+
const guard2 = () => {
|
|
1556
1589
|
params.signal?.throwIfAborted();
|
|
1557
1590
|
params.assertCurrent?.();
|
|
1558
1591
|
};
|
|
1559
|
-
|
|
1560
|
-
const
|
|
1561
|
-
const
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
return terminalIdentityErrors.has(error.code) ? { ...unavailable("unavailable"), retryable: false, reason: error.code } : unavailable("unavailable");
|
|
1572
|
-
}
|
|
1573
|
-
guard();
|
|
1574
|
-
if (resolution.accountId !== message.accountId || resolution.messageId !== message.id || resolution.threadId !== message.threadId) throw Error("Native quote identity does not match its message");
|
|
1575
|
-
if (resolution.state === "recalled" || resolution.message && nativeRecallInfo(resolution.message)) return { ...unavailable("recalled"), retryable: false, reason: "message_recalled" };
|
|
1576
|
-
if (!message.quote) return unavailable("invalid");
|
|
1577
|
-
if (resolution.state === "embedded") {
|
|
1578
|
-
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");
|
|
1579
|
-
const author = { senderId: resolution.senderId ?? null, direction: null };
|
|
1580
|
-
if (params.allowQuotedMessage && !await params.allowQuotedMessage(author)) return unavailable("not permitted");
|
|
1581
|
-
guard();
|
|
1582
|
-
const quote2 = { state: "embedded", senderId: resolution.senderId ?? void 0, type: message.quote.type };
|
|
1583
|
-
const prefix2 = `${comment}${comment ? "\n\n" : ""}[Quoted ${message.quote.type === 3 ? "image" : "text"} from ${resolution.senderId ?? "unknown sender"}; embedded in quote, original not stored]
|
|
1584
|
-
`;
|
|
1585
|
-
if (message.quote.type === 1) return { body: `${prefix2}${resolution.embeddedText}
|
|
1586
|
-
[/Quoted message]`, pending: false, quote: quote2 };
|
|
1587
|
-
const beforeMediaAccess2 = async () => {
|
|
1588
|
-
await params.beforeMediaAccess?.();
|
|
1589
|
-
guard();
|
|
1590
|
-
if (params.allowQuotedMessage && !await params.allowQuotedMessage(author)) throw Error("Quoted message is no longer permitted");
|
|
1591
|
-
guard();
|
|
1592
|
-
};
|
|
1593
|
-
const prepared2 = await prepareNativeAttachment({ ...params, message: { ...message, type: 3, appMessageType: void 0, quote: void 0 }, beforeMediaAccess: beforeMediaAccess2 });
|
|
1594
|
-
return {
|
|
1595
|
-
...prepared2,
|
|
1596
|
-
body: `${prefix2}${prepared2.body}
|
|
1597
|
-
[/Quoted message]`,
|
|
1598
|
-
pending: false,
|
|
1599
|
-
quote: quote2,
|
|
1600
|
-
...prepared2.pending ? { attachment: { kind: "image", status: "pending", filename: "quoted-image" } } : {}
|
|
1601
|
-
};
|
|
1602
|
-
}
|
|
1603
|
-
if (resolution.state !== "resolved") {
|
|
1604
|
-
if (!["missing", "invalid", "unsupported", "ambiguous"].includes(resolution.state) || resolution.message != null) throw Error("Invalid native quote resolution");
|
|
1605
|
-
return unavailable(resolution.state);
|
|
1606
|
-
}
|
|
1607
|
-
const original = resolution.message;
|
|
1608
|
-
if (!original || original.accountId !== message.accountId || original.threadId !== message.threadId || original.id === message.id || original.serverId !== message.quote.serverId || original.type !== message.quote.type || ![1, 3].includes(original.type)) throw Error("Native quoted original crosses its message boundary");
|
|
1609
|
-
if (params.allowQuotedMessage && !await params.allowQuotedMessage(original)) return unavailable("not permitted");
|
|
1610
|
-
guard();
|
|
1611
|
-
const quote = { state: "resolved", messageId: original.id, senderId: original.senderId ?? void 0, type: original.type };
|
|
1612
|
-
const prefix = `${comment}${comment ? "\n\n" : ""}[Quoted ${original.type === 3 ? "image" : "text"} from ${original.senderId ?? "unknown sender"}; message ${original.id}]
|
|
1613
|
-
`;
|
|
1614
|
-
if (original.type === 1) return { body: `${prefix}${(original.body ?? "").slice(0, 16384)}
|
|
1615
|
-
[/Quoted message]`, pending: false, quote };
|
|
1592
|
+
guard2();
|
|
1593
|
+
const author = { senderId: message.quote?.senderId ?? null, direction: message.quote?.direction ?? null };
|
|
1594
|
+
const permitted = !message.quote || !params.allowQuotedMessage || await params.allowQuotedMessage(author);
|
|
1595
|
+
guard2();
|
|
1596
|
+
const rendered = renderEmbeddedQuote(message, permitted);
|
|
1597
|
+
if (!rendered.image) return {
|
|
1598
|
+
body: rendered.body,
|
|
1599
|
+
quote: rendered.quote,
|
|
1600
|
+
pending: false,
|
|
1601
|
+
retryable: false,
|
|
1602
|
+
...rendered.quote.state === "recalled" ? { reason: "message_recalled" } : {}
|
|
1603
|
+
};
|
|
1616
1604
|
const beforeMediaAccess = async () => {
|
|
1617
1605
|
await params.beforeMediaAccess?.();
|
|
1618
|
-
|
|
1619
|
-
if (params.allowQuotedMessage && !await params.allowQuotedMessage(
|
|
1620
|
-
|
|
1606
|
+
guard2();
|
|
1607
|
+
if (params.allowQuotedMessage && !await params.allowQuotedMessage(author)) throw Error("Quoted message is no longer permitted");
|
|
1608
|
+
guard2();
|
|
1621
1609
|
};
|
|
1622
|
-
const prepared = await prepareNativeAttachment({ ...params, message:
|
|
1610
|
+
const prepared = await prepareNativeAttachment({ ...params, message: { ...message, type: 3, appMessageType: void 0, quote: void 0, link: void 0 }, beforeMediaAccess });
|
|
1611
|
+
if (prepared.reason === "message_recalled" || terminalIdentityErrors.has(prepared.reason ?? "")) return prepared;
|
|
1623
1612
|
return {
|
|
1624
1613
|
...prepared,
|
|
1625
|
-
body:
|
|
1626
|
-
|
|
1614
|
+
body: renderEmbeddedQuote(message, true, prepared.body).body,
|
|
1615
|
+
quote: rendered.quote,
|
|
1627
1616
|
pending: false,
|
|
1628
|
-
|
|
1629
|
-
...prepared.pending ? { attachment: { kind: "image", status: "pending", filename: "quoted-image" } } : {},
|
|
1630
|
-
suppliedAssets: prepared.suppliedAssets?.map((asset) => ({ ...asset, sourceMessageId: original.id }))
|
|
1617
|
+
...prepared.pending ? { attachment: { kind: "image", status: "pending", filename: "quoted-image" } } : {}
|
|
1631
1618
|
};
|
|
1632
1619
|
}
|
|
1633
1620
|
async function prepareNativeAttachment(params) {
|
|
@@ -1652,16 +1639,16 @@ async function prepareNativeAttachment(params) {
|
|
|
1652
1639
|
async function prepareNativeAttachmentInput(params) {
|
|
1653
1640
|
const { client, account, message, core } = params;
|
|
1654
1641
|
const attachmentKind = kind(message.type);
|
|
1655
|
-
const
|
|
1642
|
+
const guard2 = () => {
|
|
1656
1643
|
params.signal?.throwIfAborted();
|
|
1657
1644
|
params.assertCurrent?.();
|
|
1658
1645
|
};
|
|
1659
|
-
|
|
1646
|
+
guard2();
|
|
1660
1647
|
if (nativeRecallInfo(message)) return { body: "[Message recalled]", pending: false, retryable: false, reason: "message_recalled" };
|
|
1661
1648
|
const authorize = async () => {
|
|
1662
|
-
|
|
1649
|
+
guard2();
|
|
1663
1650
|
await params.beforeMediaAccess?.();
|
|
1664
|
-
|
|
1651
|
+
guard2();
|
|
1665
1652
|
};
|
|
1666
1653
|
if (message.type === 49 && message.appMessageType === 57) return prepareNativeQuote(params);
|
|
1667
1654
|
if (message.type === 49 && message.appMessageType === 19) return { body: message.body?.slice(0, 65536) || "[Forwarded chat history unavailable]", pending: false };
|
|
@@ -1676,7 +1663,7 @@ async function prepareNativeAttachmentInput(params) {
|
|
|
1676
1663
|
const scopedAssets = async () => {
|
|
1677
1664
|
await authorize();
|
|
1678
1665
|
const items = (await client.mediaAssets(accountId, message.id)).items;
|
|
1679
|
-
|
|
1666
|
+
guard2();
|
|
1680
1667
|
if (items.some((asset2) => asset2.accountId !== accountId || asset2.messageId !== message.id)) throw Error("Native attachment identity does not match its message");
|
|
1681
1668
|
return items;
|
|
1682
1669
|
};
|
|
@@ -1698,8 +1685,8 @@ async function prepareNativeAttachmentInput(params) {
|
|
|
1698
1685
|
let transferError;
|
|
1699
1686
|
if (!asset && initialCandidate?.jobId && initialCandidate.state !== "recalled") {
|
|
1700
1687
|
await authorize();
|
|
1701
|
-
job = await incomingMediaJobStatus(client, accountId, initialCandidate.jobId, initialCandidate.id, params.signal,
|
|
1702
|
-
|
|
1688
|
+
job = await incomingMediaJobStatus(client, accountId, initialCandidate.jobId, initialCandidate.id, params.signal, guard2);
|
|
1689
|
+
guard2();
|
|
1703
1690
|
if (!job) return { body: `[${attachmentKind} pending: retrieval is unavailable]`, pending: true, jobId: initialCandidate.jobId, retryable: true };
|
|
1704
1691
|
assets = await scopedAssets();
|
|
1705
1692
|
asset = select(assets, message.type, quality);
|
|
@@ -1710,17 +1697,17 @@ async function prepareNativeAttachmentInput(params) {
|
|
|
1710
1697
|
await authorize();
|
|
1711
1698
|
try {
|
|
1712
1699
|
job = await client.transferMedia(accountId, { messageId: message.id, assetId: null, quality: transferQuality });
|
|
1713
|
-
|
|
1700
|
+
guard2();
|
|
1714
1701
|
job = await waitNativeMediaStatus(
|
|
1715
1702
|
job,
|
|
1716
1703
|
params.waitMs ?? 2e4,
|
|
1717
1704
|
params.signal,
|
|
1718
|
-
|
|
1719
|
-
(known, pollSignal) => incomingMediaJobStatus(client, accountId, known.id, initialCandidate?.id, pollSignal,
|
|
1705
|
+
guard2,
|
|
1706
|
+
(known, pollSignal) => incomingMediaJobStatus(client, accountId, known.id, initialCandidate?.id, pollSignal, guard2)
|
|
1720
1707
|
);
|
|
1721
|
-
|
|
1708
|
+
guard2();
|
|
1722
1709
|
} catch (error) {
|
|
1723
|
-
|
|
1710
|
+
guard2();
|
|
1724
1711
|
if (!(error instanceof NativeApiError) || terminalIdentityErrors.has(error.code)) throw error;
|
|
1725
1712
|
if (error.retryable) return { body: `[${attachmentKind} pending: retrieval is unavailable]`, pending: true, jobId: job?.id, retryable: true };
|
|
1726
1713
|
transferError = error;
|
|
@@ -1766,7 +1753,7 @@ async function prepareNativeAttachmentInput(params) {
|
|
|
1766
1753
|
maxBytes,
|
|
1767
1754
|
expectedBytes: asset.byteLength,
|
|
1768
1755
|
expectedSha256: asset.sha256 ?? "",
|
|
1769
|
-
assertCurrent:
|
|
1756
|
+
assertCurrent: guard2,
|
|
1770
1757
|
saveMediaStream: params.saveMediaStream ?? saveMediaStream
|
|
1771
1758
|
});
|
|
1772
1759
|
} finally {
|
|
@@ -1800,7 +1787,7 @@ async function prepareNativeAttachmentInput(params) {
|
|
|
1800
1787
|
};
|
|
1801
1788
|
}
|
|
1802
1789
|
const content = await client.mediaContent(accountId, asset.id, { maxBytes, signal: params.signal });
|
|
1803
|
-
|
|
1790
|
+
guard2();
|
|
1804
1791
|
const mime = content.contentType.split(";", 1)[0].trim().toLowerCase();
|
|
1805
1792
|
if (!/^[a-z0-9.+-]+\/[a-z0-9.+-]+$/.test(mime)) throw Error("Native attachment MIME type is unsupported");
|
|
1806
1793
|
const hash = createHash4("sha256").update(content.bytes).digest("hex");
|
|
@@ -1826,7 +1813,7 @@ async function prepareNativeAttachmentInput(params) {
|
|
|
1826
1813
|
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");
|
|
1827
1814
|
await authorize();
|
|
1828
1815
|
const derived = await client.mediaContent(accountId, asset.id, { maxBytes: preview.byteLength, representation: "preview", signal: params.signal });
|
|
1829
|
-
|
|
1816
|
+
guard2();
|
|
1830
1817
|
const derivedMime = derived.contentType.split(";", 1)[0].trim().toLowerCase();
|
|
1831
1818
|
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");
|
|
1832
1819
|
imageBytes = Buffer.from(derived.bytes);
|
|
@@ -1851,7 +1838,7 @@ async function prepareNativeAttachmentInput(params) {
|
|
|
1851
1838
|
[Model image: prepared still preview; original retained separately]`;
|
|
1852
1839
|
}
|
|
1853
1840
|
}
|
|
1854
|
-
|
|
1841
|
+
guard2();
|
|
1855
1842
|
return {
|
|
1856
1843
|
body,
|
|
1857
1844
|
attachment,
|
|
@@ -2056,7 +2043,7 @@ var NativeMediaPayloadInspectionError = class extends Error {
|
|
|
2056
2043
|
};
|
|
2057
2044
|
function createWeChatMediaHandlers(send = sendWeChatMedia) {
|
|
2058
2045
|
const key = (ctx, index = 0) => ctx.deliveryQueueId ? createHash6("sha256").update(JSON.stringify(["wechat-media", ctx.deliveryQueueId, ctx.deliveryPartIndex ?? 0, index])).digest("hex") : void 0;
|
|
2059
|
-
const
|
|
2046
|
+
const guard2 = (ctx) => async () => {
|
|
2060
2047
|
ctx.signal?.throwIfAborted();
|
|
2061
2048
|
await ctx.onPlatformSendDispatch?.();
|
|
2062
2049
|
ctx.signal?.throwIfAborted();
|
|
@@ -2065,7 +2052,7 @@ function createWeChatMediaHandlers(send = sendWeChatMedia) {
|
|
|
2065
2052
|
return {
|
|
2066
2053
|
sendMedia: async (ctx) => ({
|
|
2067
2054
|
channel: "agent-wechat",
|
|
2068
|
-
...await send(ctx.cfg, ctx.to, ctx.text, ctx.mediaUrl, ctx.audioAsVoice, key(ctx), { forceDocument: ctx.forceDocument },
|
|
2055
|
+
...await send(ctx.cfg, ctx.to, ctx.text, ctx.mediaUrl, ctx.audioAsVoice, key(ctx), { forceDocument: ctx.forceDocument }, guard2(ctx))
|
|
2069
2056
|
}),
|
|
2070
2057
|
sendPayload: async (ctx) => {
|
|
2071
2058
|
const options = { ...nativePayloadMediaOptions(ctx.payload), forceDocument: ctx.forceDocument };
|
|
@@ -2077,7 +2064,7 @@ function createWeChatMediaHandlers(send = sendWeChatMedia) {
|
|
|
2077
2064
|
for (const [index, mediaUrl] of (urls.length ? urls : [void 0]).entries()) {
|
|
2078
2065
|
let delivered;
|
|
2079
2066
|
try {
|
|
2080
|
-
delivered = await send(ctx.cfg, ctx.to, index === 0 ? ctx.payload.text ?? ctx.text : "", mediaUrl, audioAsVoice, key(ctx, index), options,
|
|
2067
|
+
delivered = await send(ctx.cfg, ctx.to, index === 0 ? ctx.payload.text ?? ctx.text : "", mediaUrl, audioAsVoice, key(ctx, index), options, guard2(ctx));
|
|
2081
2068
|
} catch (error) {
|
|
2082
2069
|
if (!deliveries.length) throw error;
|
|
2083
2070
|
throw new NativeMediaPayloadInspectionError(deliveries, error instanceof NativeMediaJobInspectionError ? { jobId: error.jobId, accountId: error.accountId, threadId: error.threadId, idempotencyKey: error.idempotencyKey } : void 0);
|
|
@@ -2710,17 +2697,17 @@ async function recheckNativeInputs(client, state, messages) {
|
|
|
2710
2697
|
throw new NativeInputInvalidated();
|
|
2711
2698
|
}
|
|
2712
2699
|
if (message.quote) {
|
|
2713
|
-
const quote =
|
|
2714
|
-
if (quote
|
|
2715
|
-
if (quote.state === "recalled" || quote.message && nativeRecallInfo(quote.message)) {
|
|
2700
|
+
const quote = current.quote;
|
|
2701
|
+
if (quote?.state === "recalled") {
|
|
2716
2702
|
const affected = state.acceptRecall(state.cursor, {
|
|
2717
2703
|
threadId: message.threadId,
|
|
2718
2704
|
serverId: message.quote.serverId,
|
|
2719
|
-
observedAt:
|
|
2705
|
+
observedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2720
2706
|
});
|
|
2721
2707
|
invalidateNativePreparedInputs(message.accountId, affected);
|
|
2722
2708
|
throw new NativeInputInvalidated();
|
|
2723
2709
|
}
|
|
2710
|
+
if (!quote || JSON.stringify(quote) !== JSON.stringify(message.quote)) throw Error("Native embedded quote changed; delivery held for review");
|
|
2724
2711
|
}
|
|
2725
2712
|
if (!state.isCurrent(message)) throw new NativeInputInvalidated();
|
|
2726
2713
|
}
|
|
@@ -2877,7 +2864,7 @@ async function dispatchNativeMessageInput(params, revision) {
|
|
|
2877
2864
|
});
|
|
2878
2865
|
assertCurrent();
|
|
2879
2866
|
state.beginDispatch(message.id, dispatchManifest, buffered.map((item) => item.message.id));
|
|
2880
|
-
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
|
|
2867
|
+
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 && [1, 49].includes(preparedMedia.quote.type ?? 0) && ["resolved", "embedded"].includes(preparedMedia.quote.state) || preparedMedia.attachment?.status === "ready" && preparedMedia.suppliedAssets?.length) ? state.preparedCallback([message.id], readPolicy) : void 0;
|
|
2881
2868
|
if (callback) trackNativePreparedInput(dispatchManifest, callback);
|
|
2882
2869
|
const beforeSend = () => recheck([message, ...admitted]);
|
|
2883
2870
|
let failed = false;
|
|
@@ -3077,43 +3064,69 @@ async function startNativeMonitor(opts) {
|
|
|
3077
3064
|
|
|
3078
3065
|
// src/native-login.ts
|
|
3079
3066
|
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
3067
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
3080
3068
|
var operations = /* @__PURE__ */ new Map();
|
|
3081
3069
|
var terminal = /* @__PURE__ */ new Set(["succeeded", "failed", "cancelled", "expired", "rejected", "needs_login"]);
|
|
3082
|
-
|
|
3070
|
+
function guard(access2) {
|
|
3071
|
+
access2.signal?.throwIfAborted();
|
|
3072
|
+
access2.assertCurrent?.();
|
|
3073
|
+
}
|
|
3074
|
+
function validateOperation(operation, accountId, operationId) {
|
|
3075
|
+
if (operation.accountId !== accountId || operation.kind !== "login" || operationId && operation.id !== operationId) throw Error("Native login operation identity does not match its account");
|
|
3076
|
+
}
|
|
3077
|
+
async function nativeLoginStart(account, options = {}, access2 = {}) {
|
|
3078
|
+
guard(access2);
|
|
3083
3079
|
const native = account.native;
|
|
3084
|
-
const { client } = await ensureNativeHost(native);
|
|
3080
|
+
const { client } = await (access2.host ?? ensureNativeHost)(native);
|
|
3081
|
+
guard(access2);
|
|
3082
|
+
const configurationKey = nativeConfigurationKey(native);
|
|
3085
3083
|
const existing = operations.get(account.accountId);
|
|
3086
|
-
if (existing && existing.
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3084
|
+
if (existing && existing.configurationKey === configurationKey) {
|
|
3085
|
+
const refreshed = await client.operation(native.accountId, existing.operation.id);
|
|
3086
|
+
guard(access2);
|
|
3087
|
+
validateOperation(refreshed, native.accountId, existing.operation.id);
|
|
3088
|
+
existing.operation = refreshed;
|
|
3089
|
+
existing.client = client;
|
|
3090
|
+
if (!terminal.has(existing.operation.state)) return nativeLoginWait(account, Math.min(options.timeoutMs ?? 3e4, 3e4), true, access2);
|
|
3091
|
+
}
|
|
3092
|
+
guard(access2);
|
|
3090
3093
|
const operation = await client.login(native.accountId, {
|
|
3091
3094
|
allowQr: true,
|
|
3092
3095
|
forceQr: options.force === true,
|
|
3093
3096
|
timeoutSeconds: Math.max(10, Math.min(900, Math.ceil((options.timeoutMs ?? 3e5) / 1e3))),
|
|
3094
3097
|
computerName: "agent-wechat"
|
|
3095
|
-
});
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
+
}, randomUUID5());
|
|
3099
|
+
validateOperation(operation, native.accountId);
|
|
3100
|
+
operations.set(account.accountId, { client, accountId: native.accountId, configurationKey, operation });
|
|
3101
|
+
guard(access2);
|
|
3102
|
+
return nativeLoginWait(account, Math.min(options.timeoutMs ?? 3e4, 3e4), true, access2);
|
|
3098
3103
|
}
|
|
3099
|
-
async function nativeLoginWait(
|
|
3100
|
-
|
|
3101
|
-
|
|
3104
|
+
async function nativeLoginWait(account, timeoutMs = 3e4, stopAtQr = false, access2 = {}) {
|
|
3105
|
+
guard(access2);
|
|
3106
|
+
const active2 = operations.get(account.accountId);
|
|
3107
|
+
if (!active2 || active2.configurationKey !== nativeConfigurationKey(account.native)) return { connected: false, state: "idle", message: "No native login operation; start login first" };
|
|
3102
3108
|
const deadline = Date.now() + Math.max(1e3, Math.min(timeoutMs, 3e5));
|
|
3103
3109
|
while (Date.now() < deadline) {
|
|
3104
|
-
|
|
3110
|
+
guard(access2);
|
|
3111
|
+
const refreshed = await active2.client.operation(active2.accountId, active2.operation.id);
|
|
3112
|
+
guard(access2);
|
|
3113
|
+
validateOperation(refreshed, active2.accountId, active2.operation.id);
|
|
3114
|
+
active2.operation = refreshed;
|
|
3105
3115
|
const state = active2.operation.state;
|
|
3106
|
-
|
|
3107
|
-
if (
|
|
3116
|
+
const result = { connected: state === "succeeded", state, operationId: active2.operation.id };
|
|
3117
|
+
if (state === "succeeded") return { ...result, message: "Native login succeeded" };
|
|
3118
|
+
if (terminal.has(state)) return { ...result, message: `Native login ${state}` };
|
|
3119
|
+
if (stopAtQr && state === "awaiting_approval") return { ...result, message: "Approve native login on your phone" };
|
|
3108
3120
|
if (stopAtQr && active2.operation.result?.qrAvailable === true) {
|
|
3109
3121
|
const generation = active2.operation.result?.qrGeneration;
|
|
3110
3122
|
const png = await active2.client.qr(active2.accountId, active2.operation.id, typeof generation === "number" ? generation : void 0);
|
|
3111
|
-
|
|
3123
|
+
guard(access2);
|
|
3124
|
+
return { ...result, qrDataUrl: `data:image/png;base64,${Buffer.from(png).toString("base64")}`, message: "Scan the white-background native QR and approve login" };
|
|
3112
3125
|
}
|
|
3113
|
-
|
|
3114
|
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
3126
|
+
await delay(Math.min(500, Math.max(0, deadline - Date.now())), void 0, { signal: access2.signal });
|
|
3115
3127
|
}
|
|
3116
|
-
|
|
3128
|
+
guard(access2);
|
|
3129
|
+
return { connected: false, state: active2.operation.state, operationId: active2.operation.id, message: "Native login remains pending; wait on the same operation rather than starting a replacement" };
|
|
3117
3130
|
}
|
|
3118
3131
|
async function sendNativeText(account, threadId2, text3, key = randomUUID5()) {
|
|
3119
3132
|
const native = account.native;
|
|
@@ -3127,29 +3140,59 @@ async function sendNativeText(account, threadId2, text3, key = randomUUID5()) {
|
|
|
3127
3140
|
{ sendId: send.id, state: send.state, serverIds: send.serverId ? [send.serverId] : void 0 }
|
|
3128
3141
|
);
|
|
3129
3142
|
}
|
|
3130
|
-
function createNativeLoginTool(account) {
|
|
3143
|
+
function createNativeLoginTool(account, dependencies = {}) {
|
|
3144
|
+
const actions = ["status", "start", "wait", "disconnect", "logout", "forget"];
|
|
3145
|
+
const configurationKey = nativeConfigurationKey(account.native);
|
|
3131
3146
|
return {
|
|
3132
3147
|
label: "WeChat Login",
|
|
3133
3148
|
name: "wechat_login",
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3149
|
+
catalogMode: "direct-only",
|
|
3150
|
+
description: "Check WeChat status, start or resume native login, or wait for the same login operation. Show the returned QR image to the user to scan and approve on their phone; do not generate a replacement QR. Repeated start reuses an active operation. For a new operation, force uses QR instead of saved trust. timeoutMs bounds the wait. Disconnect retains saved trust. Logout reports its durable native outcome; it does not promise saved-device revocation. Forget requires confirm=true and removes local authentication while retaining history.",
|
|
3151
|
+
parameters: { type: "object", additionalProperties: false, properties: {
|
|
3152
|
+
action: { type: "string", enum: actions },
|
|
3153
|
+
force: { type: "boolean", description: "With start, use QR instead of saved trust for a new login. An active login is still reused." },
|
|
3154
|
+
timeoutMs: { type: "integer", minimum: 1e3, maximum: 3e5, description: "Wait bound in milliseconds; defaults to 30000. Start returns as soon as a QR is available." },
|
|
3155
|
+
confirm: { type: "boolean", description: "Required true for forget." }
|
|
3156
|
+
}, required: ["action"] },
|
|
3157
|
+
execute: async (_id, params, signal) => {
|
|
3158
|
+
if (!params || typeof params !== "object" || Array.isArray(params)) throw Error("Expected login arguments");
|
|
3137
3159
|
const args = params;
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3160
|
+
if (Object.keys(args).some((key) => !["action", "force", "timeoutMs", "confirm"].includes(key)) || !actions.includes(args.action) || args.force !== void 0 && typeof args.force !== "boolean" || args.confirm !== void 0 && typeof args.confirm !== "boolean" || args.timeoutMs !== void 0 && (!Number.isSafeInteger(args.timeoutMs) || args.timeoutMs < 1e3 || args.timeoutMs > 3e5)) throw Error("Invalid native login arguments");
|
|
3161
|
+
if (args.action === "forget" && args.confirm !== true) throw Error("Forgetting native credentials requires confirm=true");
|
|
3162
|
+
const access2 = { host: dependencies.host, signal, assertCurrent: () => {
|
|
3163
|
+
dependencies.context?.assertInvocationCurrent?.();
|
|
3164
|
+
if (!dependencies.context) return;
|
|
3165
|
+
const context = dependencies.context;
|
|
3166
|
+
const config = context.getRuntimeConfig?.() ?? context.runtimeConfig ?? context.config;
|
|
3167
|
+
const current = config && resolveWeChatAccount(config);
|
|
3168
|
+
if (!current?.enabled || !current.native || nativeConfigurationKey(current.native) !== configurationKey) throw Error("Native login configuration changed; invoke the tool again");
|
|
3169
|
+
} };
|
|
3170
|
+
guard(access2);
|
|
3171
|
+
if (args.action === "start" || args.action === "wait") {
|
|
3172
|
+
const result2 = args.action === "start" ? await nativeLoginStart(account, { force: args.force, timeoutMs: args.timeoutMs }, access2) : await nativeLoginWait(account, args.timeoutMs, false, access2);
|
|
3142
3173
|
const content = [{ type: "text", text: result2.message }];
|
|
3143
3174
|
if (result2.qrDataUrl) content.push({ type: "image", data: result2.qrDataUrl.split(",")[1], mimeType: "image/png" });
|
|
3144
|
-
return { content, details: { connected: result2.connected } };
|
|
3175
|
+
return { content, details: { connected: result2.connected, state: result2.state, operationId: result2.operationId } };
|
|
3145
3176
|
}
|
|
3177
|
+
const native = account.native;
|
|
3178
|
+
const { client } = await (access2.host ?? ensureNativeHost)(native);
|
|
3179
|
+
guard(access2);
|
|
3146
3180
|
const result = args.action === "status" ? await client.account(native.accountId) : args.action === "disconnect" ? await client.disconnect(native.accountId) : args.action === "logout" ? await client.logout(native.accountId, randomUUID5()) : args.action === "forget" && args.confirm === true ? await client.forgetCredentials(native.accountId, randomUUID5(), true) : (() => {
|
|
3147
3181
|
throw new Error("Unknown native login action");
|
|
3148
3182
|
})();
|
|
3183
|
+
if (args.action !== "status") operations.delete(account.accountId);
|
|
3184
|
+
guard(access2);
|
|
3149
3185
|
return { content: [{ type: "text", text: JSON.stringify(result) }], details: result };
|
|
3150
3186
|
}
|
|
3151
3187
|
};
|
|
3152
3188
|
}
|
|
3189
|
+
function registerNativeLoginTool(api) {
|
|
3190
|
+
api.registerTool({ contextVersion: 2, create: (context) => {
|
|
3191
|
+
const config = context.getRuntimeConfig?.() ?? context.runtimeConfig ?? context.config;
|
|
3192
|
+
const account = config && resolveWeChatAccount(config);
|
|
3193
|
+
return account?.enabled && account.native ? [createNativeLoginTool(account, { context })] : [];
|
|
3194
|
+
} }, { names: ["wechat_login"] });
|
|
3195
|
+
}
|
|
3153
3196
|
|
|
3154
3197
|
// src/channel.ts
|
|
3155
3198
|
async function sendWeChatText(cfg, to, text3) {
|
|
@@ -3346,7 +3389,7 @@ var wechatPlugin = {
|
|
|
3346
3389
|
loginWithQrWait: async ({ accountId, timeoutMs }) => {
|
|
3347
3390
|
const account = resolveWeChatAccount(getWeChatRuntime().config.current(), accountId ?? void 0);
|
|
3348
3391
|
if (!account?.native) return { connected: false, message: "No native daemon configured" };
|
|
3349
|
-
return nativeLoginWait(account
|
|
3392
|
+
return nativeLoginWait(account, timeoutMs);
|
|
3350
3393
|
}
|
|
3351
3394
|
},
|
|
3352
3395
|
auth: {
|
|
@@ -3372,10 +3415,6 @@ var wechatPlugin = {
|
|
|
3372
3415
|
linked: runtime2?.linked ?? false
|
|
3373
3416
|
})
|
|
3374
3417
|
},
|
|
3375
|
-
agentTools: (({ cfg }) => {
|
|
3376
|
-
const account = resolveWeChatAccount(cfg);
|
|
3377
|
-
return account?.native ? [createNativeLoginTool(account)] : [];
|
|
3378
|
-
}),
|
|
3379
3418
|
directory: {
|
|
3380
3419
|
self: async ({ cfg }) => {
|
|
3381
3420
|
const account = resolveWeChatAccount(cfg);
|
|
@@ -3600,7 +3639,7 @@ function string(value, name3, max = 4096) {
|
|
|
3600
3639
|
function threadId(value) {
|
|
3601
3640
|
return string(string(value, "thread ID").replace(/^(?:agent-)?wechat:(?:group:)?/i, ""), "thread ID");
|
|
3602
3641
|
}
|
|
3603
|
-
function messageResult(message, retrievalTool = "wechat_media") {
|
|
3642
|
+
function messageResult(message, retrievalTool = "wechat_media", quotePermitted = false) {
|
|
3604
3643
|
if (nativeRecallInfo(message)) return {
|
|
3605
3644
|
id: message.id,
|
|
3606
3645
|
threadId: message.threadId,
|
|
@@ -3615,8 +3654,15 @@ function messageResult(message, retrievalTool = "wechat_media") {
|
|
|
3615
3654
|
const isForward = message.type === 49 && message.appMessageType === 19;
|
|
3616
3655
|
const isLink = message.type === 49 && [3, 4, 5].includes(message.appMessageType ?? 0) && !!message.link;
|
|
3617
3656
|
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";
|
|
3618
|
-
const body = message.type === 1 || isForward || isLink ? message.body ?? "" : isQuote ?
|
|
3619
|
-
|
|
3657
|
+
const body = message.type === 1 || isForward || isLink ? message.body ?? "" : isQuote ? renderEmbeddedQuote(message, quotePermitted).body : `[WeChat ${kind2} message; attachment content has not been retrieved by this tool]`;
|
|
3658
|
+
const quote = message.quote && {
|
|
3659
|
+
serverId: message.quote.serverId,
|
|
3660
|
+
type: message.quote.type,
|
|
3661
|
+
state: quotePermitted ? message.quote.state : "not permitted",
|
|
3662
|
+
senderId: message.quote.senderId,
|
|
3663
|
+
appMessageType: message.quote.appMessageType,
|
|
3664
|
+
embeddedImage: quotePermitted ? message.quote.embeddedImage : void 0
|
|
3665
|
+
};
|
|
3620
3666
|
return {
|
|
3621
3667
|
id: message.id,
|
|
3622
3668
|
threadId: message.threadId,
|
|
@@ -3625,7 +3671,7 @@ function messageResult(message, retrievalTool = "wechat_media") {
|
|
|
3625
3671
|
direction: message.direction,
|
|
3626
3672
|
type: message.type,
|
|
3627
3673
|
appMessageType: message.appMessageType,
|
|
3628
|
-
quote
|
|
3674
|
+
quote,
|
|
3629
3675
|
link: isLink ? message.link : void 0,
|
|
3630
3676
|
createdAt: message.createdAt,
|
|
3631
3677
|
body: body.slice(0, 4e3),
|
|
@@ -3676,7 +3722,7 @@ function createLookupTools(context, dependencies, archive) {
|
|
|
3676
3722
|
parameters: { type: "object", additionalProperties: false, properties, required },
|
|
3677
3723
|
execute: async (_callId, input, signal) => {
|
|
3678
3724
|
const args = object(input);
|
|
3679
|
-
const
|
|
3725
|
+
const guard2 = () => {
|
|
3680
3726
|
signal?.throwIfAborted();
|
|
3681
3727
|
context.assertInvocationCurrent?.();
|
|
3682
3728
|
const cfg2 = currentConfig();
|
|
@@ -3684,7 +3730,7 @@ function createLookupTools(context, dependencies, archive) {
|
|
|
3684
3730
|
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");
|
|
3685
3731
|
return { cfg: cfg2, account: account2, native: account2.native };
|
|
3686
3732
|
};
|
|
3687
|
-
const { cfg, account, native } =
|
|
3733
|
+
const { cfg, account, native } = guard2();
|
|
3688
3734
|
if (Object.keys(args).some((key) => !Object.hasOwn(properties, key))) throw Error(archive ? "Unknown archive lookup argument" : "Unknown lookup argument; WeChat history is restricted to the current conversation");
|
|
3689
3735
|
const limit = args.limit ?? 20;
|
|
3690
3736
|
if (!Number.isInteger(limit) || limit < 1 || limit > 50) throw Error("limit must be 1\u201350");
|
|
@@ -3701,7 +3747,7 @@ function createLookupTools(context, dependencies, archive) {
|
|
|
3701
3747
|
const core = dependencies.core ?? getWeChatRuntime();
|
|
3702
3748
|
const allowed = async (sender) => {
|
|
3703
3749
|
if (archive) return true;
|
|
3704
|
-
const { cfg: cfg2, account: account2 } =
|
|
3750
|
+
const { cfg: cfg2, account: account2 } = guard2();
|
|
3705
3751
|
const result2 = await resolveWeChatIngress({
|
|
3706
3752
|
resolve: core.channel.inbound?.ingress?.resolveStable ?? resolveStableChannelMessageIngress2,
|
|
3707
3753
|
cfg: cfg2,
|
|
@@ -3715,10 +3761,15 @@ function createLookupTools(context, dependencies, archive) {
|
|
|
3715
3761
|
return result2.ingress.admission === "dispatch";
|
|
3716
3762
|
};
|
|
3717
3763
|
if (!await allowed(requesterSenderId)) throw Error("WeChat conversation access is no longer allowed");
|
|
3718
|
-
|
|
3764
|
+
guard2();
|
|
3719
3765
|
const { client } = await (dependencies.host ?? ensureNativeHost)(native);
|
|
3720
|
-
|
|
3766
|
+
guard2();
|
|
3721
3767
|
const accountId = native.accountId;
|
|
3768
|
+
const visibleMessage = async (message) => messageResult(
|
|
3769
|
+
message,
|
|
3770
|
+
archive ? "wechat_archive_media" : "wechat_media",
|
|
3771
|
+
!message.quote || archive || message.quote.direction === "outgoing" || !!message.quote.senderId && await allowed(message.quote.senderId)
|
|
3772
|
+
);
|
|
3722
3773
|
let result;
|
|
3723
3774
|
if (action === "threads") {
|
|
3724
3775
|
const page = await client.threads(accountId, { limit, cursor, ...query ? { query } : {} });
|
|
@@ -3731,21 +3782,20 @@ function createLookupTools(context, dependencies, archive) {
|
|
|
3731
3782
|
result = { ...page, items, scope: "local_profile", note: "Locally stored threads; coverage may be partial. This lookup does not mark threads read." };
|
|
3732
3783
|
} else if (action === "message" || action === "media") {
|
|
3733
3784
|
const message = await client.message(accountId, messageId);
|
|
3734
|
-
|
|
3785
|
+
guard2();
|
|
3735
3786
|
if (!await allowed(requesterSenderId)) throw Error("WeChat conversation access is no longer allowed");
|
|
3736
3787
|
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");
|
|
3737
|
-
|
|
3788
|
+
if (action === "message") result = await visibleMessage(message);
|
|
3738
3789
|
if (action === "media") {
|
|
3739
3790
|
const checkRecall = async () => {
|
|
3740
|
-
|
|
3791
|
+
guard2();
|
|
3741
3792
|
const latest = await client.message(accountId, messageId);
|
|
3742
|
-
|
|
3793
|
+
guard2();
|
|
3743
3794
|
if (latest.id !== message.id || latest.accountId !== accountId || latest.threadId !== message.threadId || latest.serverId !== message.serverId) throw Error("Native media message identity changed");
|
|
3744
3795
|
if (nativeRecallInfo(latest)) throw new NativeApiError(410, "message_recalled", false);
|
|
3745
3796
|
if (message.quote) {
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
if (quote.state === "recalled" || quote.message && nativeRecallInfo(quote.message)) throw new NativeApiError(410, "message_recalled", false);
|
|
3797
|
+
if (latest.quote?.state === "recalled") throw new NativeApiError(410, "message_recalled", false);
|
|
3798
|
+
if (!latest.quote || JSON.stringify(latest.quote) !== JSON.stringify(message.quote)) throw new NativeApiError(409, "media_source_changed", false);
|
|
3749
3799
|
}
|
|
3750
3800
|
};
|
|
3751
3801
|
let prepared;
|
|
@@ -3760,7 +3810,7 @@ function createLookupTools(context, dependencies, archive) {
|
|
|
3760
3810
|
download: args.download !== false,
|
|
3761
3811
|
waitMs: 2e3,
|
|
3762
3812
|
signal,
|
|
3763
|
-
assertCurrent:
|
|
3813
|
+
assertCurrent: guard2,
|
|
3764
3814
|
allowQuotedMessage: async (original) => original.direction === "outgoing" || await allowed(original.senderId ?? void 0),
|
|
3765
3815
|
beforeMediaAccess: async () => {
|
|
3766
3816
|
if (!await allowed(requesterSenderId) || message.direction !== "outgoing" && !await allowed(message.senderId ?? void 0)) throw Error("WeChat conversation access is no longer allowed");
|
|
@@ -3768,7 +3818,7 @@ function createLookupTools(context, dependencies, archive) {
|
|
|
3768
3818
|
}
|
|
3769
3819
|
});
|
|
3770
3820
|
} catch (error) {
|
|
3771
|
-
|
|
3821
|
+
guard2();
|
|
3772
3822
|
if (!(error instanceof NativeApiError)) throw error;
|
|
3773
3823
|
const details2 = {
|
|
3774
3824
|
messageId: message.id,
|
|
@@ -3779,7 +3829,7 @@ function createLookupTools(context, dependencies, archive) {
|
|
|
3779
3829
|
};
|
|
3780
3830
|
return { content: [{ type: "text", text: JSON.stringify(details2) }], details: details2 };
|
|
3781
3831
|
}
|
|
3782
|
-
|
|
3832
|
+
guard2();
|
|
3783
3833
|
if (!await allowed(requesterSenderId) || message.direction !== "outgoing" && !await allowed(message.senderId ?? void 0)) throw Error("WeChat conversation access is no longer allowed");
|
|
3784
3834
|
const details = {
|
|
3785
3835
|
messageId: message.id,
|
|
@@ -3796,7 +3846,7 @@ function createLookupTools(context, dependencies, archive) {
|
|
|
3796
3846
|
};
|
|
3797
3847
|
const image = prepared.suppliedAssets?.find((asset) => asset.role === "model_image");
|
|
3798
3848
|
const output = image ? await imageResultFromFile({ label: "WeChat attachment", path: image.path, extraText: JSON.stringify(details), details }) : { content: [{ type: "text", text: JSON.stringify(details) }], details };
|
|
3799
|
-
|
|
3849
|
+
guard2();
|
|
3800
3850
|
try {
|
|
3801
3851
|
await checkRecall();
|
|
3802
3852
|
} catch (error) {
|
|
@@ -3813,7 +3863,7 @@ function createLookupTools(context, dependencies, archive) {
|
|
|
3813
3863
|
for (const message of page.items) {
|
|
3814
3864
|
if (isNativeTombstone(message)) continue;
|
|
3815
3865
|
if (conversation && message.threadId !== conversation || message.accountId !== accountId) throw Error("WeChat history response crosses a conversation boundary");
|
|
3816
|
-
if (message.direction === "outgoing" || await allowed(message.senderId ?? void 0)) items.push(
|
|
3866
|
+
if (message.direction === "outgoing" || await allowed(message.senderId ?? void 0)) items.push(await visibleMessage(message));
|
|
3817
3867
|
}
|
|
3818
3868
|
result = {
|
|
3819
3869
|
...page,
|
|
@@ -3823,7 +3873,7 @@ function createLookupTools(context, dependencies, archive) {
|
|
|
3823
3873
|
note: archive ? "Account-wide local history may be partial. These reads do not mark threads read or download attachments." : "Local history may be partial. A filtered page can be empty with a nextCursor. These reads do not mark a conversation read or download attachments."
|
|
3824
3874
|
};
|
|
3825
3875
|
}
|
|
3826
|
-
|
|
3876
|
+
guard2();
|
|
3827
3877
|
return { content: [{ type: "text", text: JSON.stringify(result) }], details: result };
|
|
3828
3878
|
}
|
|
3829
3879
|
});
|
|
@@ -3838,13 +3888,13 @@ function createLookupTools(context, dependencies, archive) {
|
|
|
3838
3888
|
tool(archiveNames[1], "search", "Search locally stored messages across every thread in the configured WeChat account, including other senders. Optional threadId narrows the search. Follow nextCursor with the same query and thread selection; no remote backfill or read marking.", { query: querySchema, threadId: messageIdSchema, ...pageSchema }, ["query"]),
|
|
3839
3889
|
tool(archiveNames[2], "history", "Read the locally stored history of any thread in the configured WeChat account, newest first, without sender filtering or read marking. Use wechat_archive_threads to find thread IDs.", { threadId: messageIdSchema, ...pageSchema }, ["threadId"]),
|
|
3840
3890
|
tool(archiveNames[3], "message", "Look up a stored message in any thread of the configured WeChat account by its exact opaque message ID. Use wechat_archive_media to retrieve its attachment.", { messageId: messageIdSchema }, ["messageId"]),
|
|
3841
|
-
tool(archiveNames[4], "media", "Retrieve an attachment or quoted text/image from any thread in the configured WeChat account by its message ID. Use the parent quote message ID for quotes. Returns verified managed file paths and an image preview for vision. Cached bytes first; missing media requests a bounded server-owned download unless download=false. Preserve pending/expired/unavailable results and never automatically retry retryable=false. No sends or read marking.", mediaSchema, ["messageId"])
|
|
3891
|
+
tool(archiveNames[4], "media", "Retrieve an attachment or quoted text/image/link from any thread in the configured WeChat account by its message ID. Use the parent quote message ID for quotes. Returns verified managed file paths and an image preview for vision. Cached bytes first; missing media requests a bounded server-owned download unless download=false. Preserve pending/expired/unavailable results and never automatically retry retryable=false. No sends or read marking.", mediaSchema, ["messageId"])
|
|
3842
3892
|
];
|
|
3843
3893
|
return [
|
|
3844
3894
|
tool(names[0], "history", "Read the current WeChat conversation's locally stored messages, newest first, beyond the recent inbound buffer. Follow nextCursor for older history; no remote history backfill or read marking.", { ...pageSchema }),
|
|
3845
3895
|
tool(names[1], "search", "Search the current WeChat conversation's locally stored message text by case-insensitive substring, beyond buffered messages. Follow nextCursor with the same query in this conversation.", { query: querySchema, ...pageSchema }, ["query"]),
|
|
3846
3896
|
tool(names[2], "message", "Look up a stored message by its exact opaque message ID in the current WeChat conversation. Media is described without downloading it; use wechat_media to retrieve its attachment. Long text is bounded.", { messageId: messageIdSchema }, ["messageId"]),
|
|
3847
|
-
tool(names[3], "media", "Retrieve a message attachment or quoted text/image from the current WeChat conversation. Use the parent quote message ID for quotes. Returns a verified local file path and an image preview for vision when applicable. Uses cached bytes first; download defaults to true and requests a bounded server-owned transfer if missing. Pending results may be checked again; never automatically retry confirmed expiry or retryable=false. Does not send messages or mark the thread read.", mediaSchema, ["messageId"])
|
|
3897
|
+
tool(names[3], "media", "Retrieve a message attachment or quoted text/image/link from the current WeChat conversation. Use the parent quote message ID for quotes. Returns a verified local file path and an image preview for vision when applicable. Uses cached bytes first; download defaults to true and requests a bounded server-owned transfer if missing. Pending results may be checked again; never automatically retry confirmed expiry or retryable=false. Does not send messages or mark the thread read.", mediaSchema, ["messageId"])
|
|
3848
3898
|
];
|
|
3849
3899
|
}
|
|
3850
3900
|
function registerNativeHistoryTools(api) {
|
|
@@ -3910,7 +3960,7 @@ function createNativeShareTools(context, dependencies = {}) {
|
|
|
3910
3960
|
...args.description !== void 0 ? { description: args.description === "" ? "" : text(args.description, "description", 16384, true) } : {},
|
|
3911
3961
|
...args.thumbnailUrl !== void 0 ? { thumbnailUrl: url(args.thumbnailUrl, "thumbnail URL") } : {}
|
|
3912
3962
|
};
|
|
3913
|
-
const
|
|
3963
|
+
const guard2 = () => {
|
|
3914
3964
|
signal?.throwIfAborted();
|
|
3915
3965
|
context.assertInvocationCurrent?.();
|
|
3916
3966
|
const cfg = currentConfig(), account = resolveWeChatAccount(cfg);
|
|
@@ -3919,7 +3969,7 @@ function createNativeShareTools(context, dependencies = {}) {
|
|
|
3919
3969
|
};
|
|
3920
3970
|
const core = dependencies.core ?? getWeChatRuntime();
|
|
3921
3971
|
const authorize = async () => {
|
|
3922
|
-
const { cfg, account } =
|
|
3972
|
+
const { cfg, account } = guard2();
|
|
3923
3973
|
const result = await resolveWeChatIngress({
|
|
3924
3974
|
resolve: core.channel.inbound?.ingress?.resolveStable ?? resolveStableChannelMessageIngress3,
|
|
3925
3975
|
cfg,
|
|
@@ -3930,11 +3980,11 @@ function createNativeShareTools(context, dependencies = {}) {
|
|
|
3930
3980
|
senderId: requester,
|
|
3931
3981
|
readStoreAllowFrom: async () => core.channel.pairing.readAllowFromStore({ channel: "agent-wechat", accountId: account.accountId, env: process.env }).catch(() => [])
|
|
3932
3982
|
});
|
|
3933
|
-
|
|
3983
|
+
guard2();
|
|
3934
3984
|
if (result.ingress.admission !== "dispatch") throw Error("WeChat conversation access is no longer allowed");
|
|
3935
3985
|
};
|
|
3936
3986
|
await authorize();
|
|
3937
|
-
const { native } =
|
|
3987
|
+
const { native } = guard2();
|
|
3938
3988
|
const { client } = await (dependencies.host ?? ensureNativeHost)(native);
|
|
3939
3989
|
await authorize();
|
|
3940
3990
|
let intent;
|
|
@@ -4012,7 +4062,7 @@ function createNativeRecallTools(context, dependencies = {}) {
|
|
|
4012
4062
|
const messageId = text2(input.messageId, "message ID");
|
|
4013
4063
|
const serverId = text2(input.serverId, "server ID");
|
|
4014
4064
|
if (!/^[1-9]\d{0,19}$/.test(serverId) || BigInt(serverId) > 18446744073709551615n) throw Error("Invalid exact server message ID");
|
|
4015
|
-
const
|
|
4065
|
+
const guard2 = () => {
|
|
4016
4066
|
signal?.throwIfAborted();
|
|
4017
4067
|
context.assertInvocationCurrent?.();
|
|
4018
4068
|
const cfg = currentConfig(), account = resolveWeChatAccount(cfg);
|
|
@@ -4020,7 +4070,7 @@ function createNativeRecallTools(context, dependencies = {}) {
|
|
|
4020
4070
|
return { cfg, account, native: account.native };
|
|
4021
4071
|
};
|
|
4022
4072
|
const allowed = async () => {
|
|
4023
|
-
const { cfg, account } =
|
|
4073
|
+
const { cfg, account } = guard2();
|
|
4024
4074
|
const core = dependencies.core ?? getWeChatRuntime();
|
|
4025
4075
|
const ingress = await resolveWeChatIngress({
|
|
4026
4076
|
resolve: core.channel.inbound?.ingress?.resolveStable ?? resolveStableChannelMessageIngress4,
|
|
@@ -4032,20 +4082,20 @@ function createNativeRecallTools(context, dependencies = {}) {
|
|
|
4032
4082
|
senderId: requester,
|
|
4033
4083
|
readStoreAllowFrom: async () => core.channel.pairing.readAllowFromStore({ channel: "agent-wechat", accountId: account.accountId, env: process.env }).catch(() => [])
|
|
4034
4084
|
});
|
|
4035
|
-
|
|
4085
|
+
guard2();
|
|
4036
4086
|
if (ingress.ingress.admission !== "dispatch") throw Error("WeChat conversation access is no longer allowed");
|
|
4037
4087
|
};
|
|
4038
4088
|
await allowed();
|
|
4039
|
-
const { native } =
|
|
4089
|
+
const { native } = guard2();
|
|
4040
4090
|
const { client } = await (dependencies.host ?? ensureNativeHost)(native);
|
|
4041
|
-
|
|
4091
|
+
guard2();
|
|
4042
4092
|
const message = await client.message(accountId, messageId);
|
|
4043
|
-
|
|
4093
|
+
guard2();
|
|
4044
4094
|
const retainedRecall = !!nativeRecallInfo(message);
|
|
4045
4095
|
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");
|
|
4046
4096
|
if (message.serverId !== serverId) throw Error("The exact server identity does not match this receipt; the local message identity may have been reused");
|
|
4047
4097
|
await allowed();
|
|
4048
|
-
|
|
4098
|
+
guard2();
|
|
4049
4099
|
const key = nativeRecallKey(accountId, conversation, messageId, serverId);
|
|
4050
4100
|
let operation;
|
|
4051
4101
|
let operationId;
|
|
@@ -4075,13 +4125,13 @@ function createNativeRecallTools(context, dependencies = {}) {
|
|
|
4075
4125
|
const deadline = Date.now() + Math.max(0, Math.min(dependencies.waitMs ?? 2e3, 1e4));
|
|
4076
4126
|
while (["queued", "running", "submitting"].includes(operation.state) && Date.now() < deadline) {
|
|
4077
4127
|
await new Promise((resolve) => setTimeout(resolve, Math.min(250, deadline - Date.now())));
|
|
4078
|
-
|
|
4128
|
+
guard2();
|
|
4079
4129
|
const remaining = Math.max(1, deadline - Date.now());
|
|
4080
4130
|
const next = await client.operation(accountId, operation.id, signal ? AbortSignal.any([signal, AbortSignal.timeout(remaining)]) : AbortSignal.timeout(remaining));
|
|
4081
4131
|
checkOperation(next);
|
|
4082
4132
|
operation = next;
|
|
4083
4133
|
}
|
|
4084
|
-
|
|
4134
|
+
guard2();
|
|
4085
4135
|
} catch (error) {
|
|
4086
4136
|
const reason = error instanceof NativeApiError ? error.code : "recall_result_unavailable";
|
|
4087
4137
|
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." });
|
|
@@ -4127,7 +4177,7 @@ function createNativeGroupTools(context, dependencies = {}) {
|
|
|
4127
4177
|
const args = input;
|
|
4128
4178
|
const identity = (value) => typeof value === "string" && value.length > 0 && value.length <= 256 && !/[\s\u0000-\u001f\u007f]/u.test(value);
|
|
4129
4179
|
if (!identity(args.groupId) || !args.groupId.endsWith("@chatroom") || args.groupId.length <= 9 || args.limit != null && (!Number.isInteger(args.limit) || Number(args.limit) < 1 || Number(args.limit) > 100) || args.cursor != null && (typeof args.cursor !== "string" || args.cursor.length > 8192) || args.username != null && !identity(args.username) || args.username != null && (args.limit != null || args.cursor != null)) throw Error("Invalid group info selectors");
|
|
4130
|
-
const
|
|
4180
|
+
const guard2 = () => {
|
|
4131
4181
|
signal?.throwIfAborted();
|
|
4132
4182
|
if (context.senderIsOwner !== true) throw Error("OpenClaw owner authorization is required");
|
|
4133
4183
|
context.assertInvocationCurrent();
|
|
@@ -4135,14 +4185,14 @@ function createNativeGroupTools(context, dependencies = {}) {
|
|
|
4135
4185
|
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");
|
|
4136
4186
|
return account.native;
|
|
4137
4187
|
};
|
|
4138
|
-
const native =
|
|
4188
|
+
const native = guard2();
|
|
4139
4189
|
const { client } = await (dependencies.host ?? ensureNativeHost)(native);
|
|
4140
|
-
|
|
4190
|
+
guard2();
|
|
4141
4191
|
const group = await client.group(accountId, args.groupId, signal);
|
|
4142
|
-
|
|
4192
|
+
guard2();
|
|
4143
4193
|
if (group.id !== args.groupId || group.roomId !== args.groupId) throw Error("Group identity is invalid");
|
|
4144
4194
|
const members = typeof args.username === "string" ? await client.groupMember(accountId, args.groupId, args.username, signal) : await client.groupMembers(accountId, args.groupId, { limit: args.limit == null ? 50 : Number(args.limit), cursor: args.cursor }, signal);
|
|
4145
|
-
|
|
4195
|
+
guard2();
|
|
4146
4196
|
const details = { accountId, group, members, source: "cached", membershipAbsence: "unconfirmed" };
|
|
4147
4197
|
return { content: [{ type: "text", text: JSON.stringify(details) }], details };
|
|
4148
4198
|
}
|
|
@@ -4166,6 +4216,7 @@ var plugin = {
|
|
|
4166
4216
|
registerNativeShareTools(api);
|
|
4167
4217
|
registerNativeRecallTools(api);
|
|
4168
4218
|
registerNativeGroupTools(api);
|
|
4219
|
+
registerNativeLoginTool(api);
|
|
4169
4220
|
const stopFacade = selected?.native ? registerNativeFacade(api, selected.native) : () => {
|
|
4170
4221
|
};
|
|
4171
4222
|
const stopPreparedBoundary = selected?.native?.readPolicy && selected.native.readPolicy !== "on_inbound" ? registerNativePreparedBoundary(api) : () => {
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "agent-wechat",
|
|
3
3
|
"contracts": {
|
|
4
|
-
"tools": ["wechat_history", "wechat_search", "wechat_message", "wechat_media", "wechat_archive_threads", "wechat_archive_search", "wechat_archive_history", "wechat_archive_message", "wechat_archive_media", "wechat_recall", "wechat_send_share", "wechat_group_info"]
|
|
4
|
+
"tools": ["wechat_history", "wechat_search", "wechat_message", "wechat_media", "wechat_archive_threads", "wechat_archive_search", "wechat_archive_history", "wechat_archive_message", "wechat_archive_media", "wechat_recall", "wechat_send_share", "wechat_group_info", "wechat_login"]
|
|
5
5
|
},
|
|
6
6
|
"toolMetadata": {
|
|
7
7
|
"wechat_archive_threads": { "optional": true },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-wechat/agent-wechat",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.14",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"esbuild": "^0.25.0",
|
|
12
12
|
"openclaw": "2026.10.2",
|
|
13
13
|
"typescript": "^5.4.5",
|
|
14
|
-
"@agent-wechat/shared": "1.0.0-beta.
|
|
14
|
+
"@agent-wechat/shared": "1.0.0-beta.3"
|
|
15
15
|
},
|
|
16
16
|
"openclaw": {
|
|
17
17
|
"extensions": [
|