@agent-wechat/agent-wechat 1.0.0-beta.12 → 1.0.0-beta.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/dist/index.js +222 -185
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
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
|
@@ -452,15 +452,6 @@ var NativeWeChatClient = class {
|
|
|
452
452
|
validateMessageRecall(message);
|
|
453
453
|
return message;
|
|
454
454
|
}
|
|
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
455
|
/** Account-wide local search, with the daemon's query-bound opaque cursor. */
|
|
465
456
|
async searchMessages(accountId, options) {
|
|
466
457
|
const result = await this.request(`v1/accounts/${id(accountId)}/messages${listQuery(options)}`);
|
|
@@ -1337,13 +1328,13 @@ async function nativeOutboundDelivery(client, accountId, threadId2, messageIds,
|
|
|
1337
1328
|
async function submitNativeText(client, accountId, threadId2, text3, key, options = {}) {
|
|
1338
1329
|
let intent = await client.sendText(accountId, threadId2, text3, key);
|
|
1339
1330
|
const deadline = Date.now() + Math.max(1, Math.min(options.timeoutMs ?? 65e3, 65e3));
|
|
1340
|
-
let
|
|
1331
|
+
let delay2 = Math.max(1, Math.min(options.initialDelayMs ?? 250, 1e3));
|
|
1341
1332
|
while (["queued", "submitting"].includes(intent.state) && Date.now() < deadline) {
|
|
1342
|
-
await new Promise((resolve) => setTimeout(resolve, Math.min(
|
|
1333
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(delay2, deadline - Date.now())));
|
|
1343
1334
|
const remaining = deadline - Date.now();
|
|
1344
1335
|
if (remaining <= 0) break;
|
|
1345
1336
|
intent = await client.send(accountId, intent.id, AbortSignal.timeout(remaining));
|
|
1346
|
-
|
|
1337
|
+
delay2 = Math.min(delay2 * 2, 1e3);
|
|
1347
1338
|
}
|
|
1348
1339
|
if (!["accepted", "sent", "server_accepted"].includes(intent.state)) throw new Error(`Native send ${intent.id} requires inspection (${intent.state}); do not automatically resend`);
|
|
1349
1340
|
return intent;
|
|
@@ -1396,6 +1387,34 @@ function recalledNativeMessage(message, recall) {
|
|
|
1396
1387
|
};
|
|
1397
1388
|
}
|
|
1398
1389
|
|
|
1390
|
+
// src/native-quote.ts
|
|
1391
|
+
function renderEmbeddedQuote(message, permitted = true, imageBody) {
|
|
1392
|
+
const comment = (message.body ?? "").slice(0, 16384);
|
|
1393
|
+
const source = message.quote;
|
|
1394
|
+
const unavailable = (state2) => ({
|
|
1395
|
+
body: `${comment}${comment ? "\n\n" : ""}[Quoted message ${state2}]`,
|
|
1396
|
+
image: false,
|
|
1397
|
+
quote: { state: state2, type: source?.type, senderId: source?.senderId ?? void 0 }
|
|
1398
|
+
});
|
|
1399
|
+
if (!source) return unavailable("unavailable");
|
|
1400
|
+
if (!permitted) return unavailable("not permitted");
|
|
1401
|
+
const state = source.state ?? "missing";
|
|
1402
|
+
if (state === "recalled") return unavailable("recalled");
|
|
1403
|
+
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");
|
|
1404
|
+
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";
|
|
1405
|
+
const image = state === "embedded" && source.type === 3 && source.embeddedImage === true;
|
|
1406
|
+
if (state === "embedded" && !image && !source.embeddedText) return unavailable("invalid");
|
|
1407
|
+
const content = state === "missing" ? "" : image ? imageBody ?? "[Image metadata embedded in quote; image not retrieved]" : source.embeddedText ?? "";
|
|
1408
|
+
const status = state === "unsupported" ? `[${label} attachment unavailable from embedded quote]` : state === "missing" ? "[Embedded quote content unavailable]" : "";
|
|
1409
|
+
return {
|
|
1410
|
+
body: `${comment}${comment ? "\n\n" : ""}[Quoted ${label} from ${source.senderId ?? "unknown sender"}; embedded in reply]
|
|
1411
|
+
${[content, status].filter(Boolean).join("\n")}
|
|
1412
|
+
[/Quoted message]`,
|
|
1413
|
+
image,
|
|
1414
|
+
quote: { state, type: source.type, senderId: source.senderId ?? void 0 }
|
|
1415
|
+
};
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1399
1418
|
// src/native-media.ts
|
|
1400
1419
|
function kind(type) {
|
|
1401
1420
|
return type === 3 || type === 47 ? "image" : type === 34 ? "audio" : type === 43 ? "video" : type === 49 ? "file" : void 0;
|
|
@@ -1438,11 +1457,11 @@ function mediaSendReceiptIds(job, audioAsVoice) {
|
|
|
1438
1457
|
async function waitNativeMediaSendReceipts(client, accountId, threadId2, initial, audioAsVoice, timeoutMs = 1e4) {
|
|
1439
1458
|
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
1459
|
const deadline = Date.now() + Math.max(0, Math.min(timeoutMs, 1e4));
|
|
1441
|
-
let job = initial,
|
|
1460
|
+
let job = initial, delay2 = 100;
|
|
1442
1461
|
while (successfulSendJobs.has(job.state) && !mediaSendReceiptIds(job, audioAsVoice) && Date.now() < deadline) {
|
|
1443
1462
|
const result = mediaSendResult(job);
|
|
1444
1463
|
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(
|
|
1464
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(delay2, Math.max(0, deadline - Date.now()))));
|
|
1446
1465
|
const remaining = deadline - Date.now();
|
|
1447
1466
|
if (remaining <= 0) break;
|
|
1448
1467
|
let next;
|
|
@@ -1453,7 +1472,7 @@ async function waitNativeMediaSendReceipts(client, accountId, threadId2, initial
|
|
|
1453
1472
|
}
|
|
1454
1473
|
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
1474
|
job = next;
|
|
1456
|
-
|
|
1475
|
+
delay2 = Math.min(delay2 * 2, 1e3);
|
|
1457
1476
|
}
|
|
1458
1477
|
return job;
|
|
1459
1478
|
}
|
|
@@ -1463,11 +1482,11 @@ async function waitNativeMediaJob(client, accountId, initial, timeoutMs = 65e3,
|
|
|
1463
1482
|
async function waitNativeMediaStatus(initial, timeoutMs, signal, assertCurrent, readStatus) {
|
|
1464
1483
|
const deadline = Date.now() + timeoutMs;
|
|
1465
1484
|
let job = initial;
|
|
1466
|
-
let
|
|
1485
|
+
let delay2 = 250;
|
|
1467
1486
|
while (!terminalJobs.has(job.state) && Date.now() < deadline) {
|
|
1468
1487
|
signal?.throwIfAborted();
|
|
1469
1488
|
assertCurrent?.();
|
|
1470
|
-
await new Promise((resolve) => setTimeout(resolve, Math.min(
|
|
1489
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(delay2, Math.max(0, deadline - Date.now()))));
|
|
1471
1490
|
const remaining = deadline - Date.now();
|
|
1472
1491
|
if (remaining <= 0) break;
|
|
1473
1492
|
signal?.throwIfAborted();
|
|
@@ -1475,7 +1494,7 @@ async function waitNativeMediaStatus(initial, timeoutMs, signal, assertCurrent,
|
|
|
1475
1494
|
const next = await readStatus(job, signal ? AbortSignal.any([signal, AbortSignal.timeout(remaining)]) : AbortSignal.timeout(remaining));
|
|
1476
1495
|
if (!next) break;
|
|
1477
1496
|
job = next;
|
|
1478
|
-
|
|
1497
|
+
delay2 = Math.min(delay2 * 2, 1e3);
|
|
1479
1498
|
}
|
|
1480
1499
|
return job;
|
|
1481
1500
|
}
|
|
@@ -1551,83 +1570,37 @@ ${prepared.body}`,
|
|
|
1551
1570
|
};
|
|
1552
1571
|
}
|
|
1553
1572
|
async function prepareNativeQuote(params) {
|
|
1554
|
-
const {
|
|
1555
|
-
const
|
|
1573
|
+
const { message } = params;
|
|
1574
|
+
const guard2 = () => {
|
|
1556
1575
|
params.signal?.throwIfAborted();
|
|
1557
1576
|
params.assertCurrent?.();
|
|
1558
1577
|
};
|
|
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 };
|
|
1578
|
+
guard2();
|
|
1579
|
+
const author = { senderId: message.quote?.senderId ?? null, direction: message.quote?.direction ?? null };
|
|
1580
|
+
const permitted = !message.quote || !params.allowQuotedMessage || await params.allowQuotedMessage(author);
|
|
1581
|
+
guard2();
|
|
1582
|
+
const rendered = renderEmbeddedQuote(message, permitted);
|
|
1583
|
+
if (!rendered.image) return {
|
|
1584
|
+
body: rendered.body,
|
|
1585
|
+
quote: rendered.quote,
|
|
1586
|
+
pending: false,
|
|
1587
|
+
retryable: false,
|
|
1588
|
+
...rendered.quote.state === "recalled" ? { reason: "message_recalled" } : {}
|
|
1589
|
+
};
|
|
1616
1590
|
const beforeMediaAccess = async () => {
|
|
1617
1591
|
await params.beforeMediaAccess?.();
|
|
1618
|
-
|
|
1619
|
-
if (params.allowQuotedMessage && !await params.allowQuotedMessage(
|
|
1620
|
-
|
|
1592
|
+
guard2();
|
|
1593
|
+
if (params.allowQuotedMessage && !await params.allowQuotedMessage(author)) throw Error("Quoted message is no longer permitted");
|
|
1594
|
+
guard2();
|
|
1621
1595
|
};
|
|
1622
|
-
const prepared = await prepareNativeAttachment({ ...params, message:
|
|
1596
|
+
const prepared = await prepareNativeAttachment({ ...params, message: { ...message, type: 3, appMessageType: void 0, quote: void 0, link: void 0 }, beforeMediaAccess });
|
|
1597
|
+
if (prepared.reason === "message_recalled" || terminalIdentityErrors.has(prepared.reason ?? "")) return prepared;
|
|
1623
1598
|
return {
|
|
1624
1599
|
...prepared,
|
|
1625
|
-
body:
|
|
1626
|
-
|
|
1600
|
+
body: renderEmbeddedQuote(message, true, prepared.body).body,
|
|
1601
|
+
quote: rendered.quote,
|
|
1627
1602
|
pending: false,
|
|
1628
|
-
|
|
1629
|
-
...prepared.pending ? { attachment: { kind: "image", status: "pending", filename: "quoted-image" } } : {},
|
|
1630
|
-
suppliedAssets: prepared.suppliedAssets?.map((asset) => ({ ...asset, sourceMessageId: original.id }))
|
|
1603
|
+
...prepared.pending ? { attachment: { kind: "image", status: "pending", filename: "quoted-image" } } : {}
|
|
1631
1604
|
};
|
|
1632
1605
|
}
|
|
1633
1606
|
async function prepareNativeAttachment(params) {
|
|
@@ -1652,16 +1625,16 @@ async function prepareNativeAttachment(params) {
|
|
|
1652
1625
|
async function prepareNativeAttachmentInput(params) {
|
|
1653
1626
|
const { client, account, message, core } = params;
|
|
1654
1627
|
const attachmentKind = kind(message.type);
|
|
1655
|
-
const
|
|
1628
|
+
const guard2 = () => {
|
|
1656
1629
|
params.signal?.throwIfAborted();
|
|
1657
1630
|
params.assertCurrent?.();
|
|
1658
1631
|
};
|
|
1659
|
-
|
|
1632
|
+
guard2();
|
|
1660
1633
|
if (nativeRecallInfo(message)) return { body: "[Message recalled]", pending: false, retryable: false, reason: "message_recalled" };
|
|
1661
1634
|
const authorize = async () => {
|
|
1662
|
-
|
|
1635
|
+
guard2();
|
|
1663
1636
|
await params.beforeMediaAccess?.();
|
|
1664
|
-
|
|
1637
|
+
guard2();
|
|
1665
1638
|
};
|
|
1666
1639
|
if (message.type === 49 && message.appMessageType === 57) return prepareNativeQuote(params);
|
|
1667
1640
|
if (message.type === 49 && message.appMessageType === 19) return { body: message.body?.slice(0, 65536) || "[Forwarded chat history unavailable]", pending: false };
|
|
@@ -1676,7 +1649,7 @@ async function prepareNativeAttachmentInput(params) {
|
|
|
1676
1649
|
const scopedAssets = async () => {
|
|
1677
1650
|
await authorize();
|
|
1678
1651
|
const items = (await client.mediaAssets(accountId, message.id)).items;
|
|
1679
|
-
|
|
1652
|
+
guard2();
|
|
1680
1653
|
if (items.some((asset2) => asset2.accountId !== accountId || asset2.messageId !== message.id)) throw Error("Native attachment identity does not match its message");
|
|
1681
1654
|
return items;
|
|
1682
1655
|
};
|
|
@@ -1698,8 +1671,8 @@ async function prepareNativeAttachmentInput(params) {
|
|
|
1698
1671
|
let transferError;
|
|
1699
1672
|
if (!asset && initialCandidate?.jobId && initialCandidate.state !== "recalled") {
|
|
1700
1673
|
await authorize();
|
|
1701
|
-
job = await incomingMediaJobStatus(client, accountId, initialCandidate.jobId, initialCandidate.id, params.signal,
|
|
1702
|
-
|
|
1674
|
+
job = await incomingMediaJobStatus(client, accountId, initialCandidate.jobId, initialCandidate.id, params.signal, guard2);
|
|
1675
|
+
guard2();
|
|
1703
1676
|
if (!job) return { body: `[${attachmentKind} pending: retrieval is unavailable]`, pending: true, jobId: initialCandidate.jobId, retryable: true };
|
|
1704
1677
|
assets = await scopedAssets();
|
|
1705
1678
|
asset = select(assets, message.type, quality);
|
|
@@ -1710,17 +1683,17 @@ async function prepareNativeAttachmentInput(params) {
|
|
|
1710
1683
|
await authorize();
|
|
1711
1684
|
try {
|
|
1712
1685
|
job = await client.transferMedia(accountId, { messageId: message.id, assetId: null, quality: transferQuality });
|
|
1713
|
-
|
|
1686
|
+
guard2();
|
|
1714
1687
|
job = await waitNativeMediaStatus(
|
|
1715
1688
|
job,
|
|
1716
1689
|
params.waitMs ?? 2e4,
|
|
1717
1690
|
params.signal,
|
|
1718
|
-
|
|
1719
|
-
(known, pollSignal) => incomingMediaJobStatus(client, accountId, known.id, initialCandidate?.id, pollSignal,
|
|
1691
|
+
guard2,
|
|
1692
|
+
(known, pollSignal) => incomingMediaJobStatus(client, accountId, known.id, initialCandidate?.id, pollSignal, guard2)
|
|
1720
1693
|
);
|
|
1721
|
-
|
|
1694
|
+
guard2();
|
|
1722
1695
|
} catch (error) {
|
|
1723
|
-
|
|
1696
|
+
guard2();
|
|
1724
1697
|
if (!(error instanceof NativeApiError) || terminalIdentityErrors.has(error.code)) throw error;
|
|
1725
1698
|
if (error.retryable) return { body: `[${attachmentKind} pending: retrieval is unavailable]`, pending: true, jobId: job?.id, retryable: true };
|
|
1726
1699
|
transferError = error;
|
|
@@ -1766,7 +1739,7 @@ async function prepareNativeAttachmentInput(params) {
|
|
|
1766
1739
|
maxBytes,
|
|
1767
1740
|
expectedBytes: asset.byteLength,
|
|
1768
1741
|
expectedSha256: asset.sha256 ?? "",
|
|
1769
|
-
assertCurrent:
|
|
1742
|
+
assertCurrent: guard2,
|
|
1770
1743
|
saveMediaStream: params.saveMediaStream ?? saveMediaStream
|
|
1771
1744
|
});
|
|
1772
1745
|
} finally {
|
|
@@ -1800,7 +1773,7 @@ async function prepareNativeAttachmentInput(params) {
|
|
|
1800
1773
|
};
|
|
1801
1774
|
}
|
|
1802
1775
|
const content = await client.mediaContent(accountId, asset.id, { maxBytes, signal: params.signal });
|
|
1803
|
-
|
|
1776
|
+
guard2();
|
|
1804
1777
|
const mime = content.contentType.split(";", 1)[0].trim().toLowerCase();
|
|
1805
1778
|
if (!/^[a-z0-9.+-]+\/[a-z0-9.+-]+$/.test(mime)) throw Error("Native attachment MIME type is unsupported");
|
|
1806
1779
|
const hash = createHash4("sha256").update(content.bytes).digest("hex");
|
|
@@ -1826,7 +1799,7 @@ async function prepareNativeAttachmentInput(params) {
|
|
|
1826
1799
|
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
1800
|
await authorize();
|
|
1828
1801
|
const derived = await client.mediaContent(accountId, asset.id, { maxBytes: preview.byteLength, representation: "preview", signal: params.signal });
|
|
1829
|
-
|
|
1802
|
+
guard2();
|
|
1830
1803
|
const derivedMime = derived.contentType.split(";", 1)[0].trim().toLowerCase();
|
|
1831
1804
|
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
1805
|
imageBytes = Buffer.from(derived.bytes);
|
|
@@ -1851,7 +1824,7 @@ async function prepareNativeAttachmentInput(params) {
|
|
|
1851
1824
|
[Model image: prepared still preview; original retained separately]`;
|
|
1852
1825
|
}
|
|
1853
1826
|
}
|
|
1854
|
-
|
|
1827
|
+
guard2();
|
|
1855
1828
|
return {
|
|
1856
1829
|
body,
|
|
1857
1830
|
attachment,
|
|
@@ -2056,7 +2029,7 @@ var NativeMediaPayloadInspectionError = class extends Error {
|
|
|
2056
2029
|
};
|
|
2057
2030
|
function createWeChatMediaHandlers(send = sendWeChatMedia) {
|
|
2058
2031
|
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
|
|
2032
|
+
const guard2 = (ctx) => async () => {
|
|
2060
2033
|
ctx.signal?.throwIfAborted();
|
|
2061
2034
|
await ctx.onPlatformSendDispatch?.();
|
|
2062
2035
|
ctx.signal?.throwIfAborted();
|
|
@@ -2065,7 +2038,7 @@ function createWeChatMediaHandlers(send = sendWeChatMedia) {
|
|
|
2065
2038
|
return {
|
|
2066
2039
|
sendMedia: async (ctx) => ({
|
|
2067
2040
|
channel: "agent-wechat",
|
|
2068
|
-
...await send(ctx.cfg, ctx.to, ctx.text, ctx.mediaUrl, ctx.audioAsVoice, key(ctx), { forceDocument: ctx.forceDocument },
|
|
2041
|
+
...await send(ctx.cfg, ctx.to, ctx.text, ctx.mediaUrl, ctx.audioAsVoice, key(ctx), { forceDocument: ctx.forceDocument }, guard2(ctx))
|
|
2069
2042
|
}),
|
|
2070
2043
|
sendPayload: async (ctx) => {
|
|
2071
2044
|
const options = { ...nativePayloadMediaOptions(ctx.payload), forceDocument: ctx.forceDocument };
|
|
@@ -2077,7 +2050,7 @@ function createWeChatMediaHandlers(send = sendWeChatMedia) {
|
|
|
2077
2050
|
for (const [index, mediaUrl] of (urls.length ? urls : [void 0]).entries()) {
|
|
2078
2051
|
let delivered;
|
|
2079
2052
|
try {
|
|
2080
|
-
delivered = await send(ctx.cfg, ctx.to, index === 0 ? ctx.payload.text ?? ctx.text : "", mediaUrl, audioAsVoice, key(ctx, index), options,
|
|
2053
|
+
delivered = await send(ctx.cfg, ctx.to, index === 0 ? ctx.payload.text ?? ctx.text : "", mediaUrl, audioAsVoice, key(ctx, index), options, guard2(ctx));
|
|
2081
2054
|
} catch (error) {
|
|
2082
2055
|
if (!deliveries.length) throw error;
|
|
2083
2056
|
throw new NativeMediaPayloadInspectionError(deliveries, error instanceof NativeMediaJobInspectionError ? { jobId: error.jobId, accountId: error.accountId, threadId: error.threadId, idempotencyKey: error.idempotencyKey } : void 0);
|
|
@@ -2710,17 +2683,17 @@ async function recheckNativeInputs(client, state, messages) {
|
|
|
2710
2683
|
throw new NativeInputInvalidated();
|
|
2711
2684
|
}
|
|
2712
2685
|
if (message.quote) {
|
|
2713
|
-
const quote =
|
|
2714
|
-
if (quote
|
|
2715
|
-
if (quote.state === "recalled" || quote.message && nativeRecallInfo(quote.message)) {
|
|
2686
|
+
const quote = current.quote;
|
|
2687
|
+
if (quote?.state === "recalled") {
|
|
2716
2688
|
const affected = state.acceptRecall(state.cursor, {
|
|
2717
2689
|
threadId: message.threadId,
|
|
2718
2690
|
serverId: message.quote.serverId,
|
|
2719
|
-
observedAt:
|
|
2691
|
+
observedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2720
2692
|
});
|
|
2721
2693
|
invalidateNativePreparedInputs(message.accountId, affected);
|
|
2722
2694
|
throw new NativeInputInvalidated();
|
|
2723
2695
|
}
|
|
2696
|
+
if (!quote || JSON.stringify(quote) !== JSON.stringify(message.quote)) throw Error("Native embedded quote changed; delivery held for review");
|
|
2724
2697
|
}
|
|
2725
2698
|
if (!state.isCurrent(message)) throw new NativeInputInvalidated();
|
|
2726
2699
|
}
|
|
@@ -2877,7 +2850,7 @@ async function dispatchNativeMessageInput(params, revision) {
|
|
|
2877
2850
|
});
|
|
2878
2851
|
assertCurrent();
|
|
2879
2852
|
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
|
|
2853
|
+
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
2854
|
if (callback) trackNativePreparedInput(dispatchManifest, callback);
|
|
2882
2855
|
const beforeSend = () => recheck([message, ...admitted]);
|
|
2883
2856
|
let failed = false;
|
|
@@ -3077,43 +3050,69 @@ async function startNativeMonitor(opts) {
|
|
|
3077
3050
|
|
|
3078
3051
|
// src/native-login.ts
|
|
3079
3052
|
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
3053
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
3080
3054
|
var operations = /* @__PURE__ */ new Map();
|
|
3081
3055
|
var terminal = /* @__PURE__ */ new Set(["succeeded", "failed", "cancelled", "expired", "rejected", "needs_login"]);
|
|
3082
|
-
|
|
3056
|
+
function guard(access2) {
|
|
3057
|
+
access2.signal?.throwIfAborted();
|
|
3058
|
+
access2.assertCurrent?.();
|
|
3059
|
+
}
|
|
3060
|
+
function validateOperation(operation, accountId, operationId) {
|
|
3061
|
+
if (operation.accountId !== accountId || operation.kind !== "login" || operationId && operation.id !== operationId) throw Error("Native login operation identity does not match its account");
|
|
3062
|
+
}
|
|
3063
|
+
async function nativeLoginStart(account, options = {}, access2 = {}) {
|
|
3064
|
+
guard(access2);
|
|
3083
3065
|
const native = account.native;
|
|
3084
|
-
const { client } = await ensureNativeHost(native);
|
|
3066
|
+
const { client } = await (access2.host ?? ensureNativeHost)(native);
|
|
3067
|
+
guard(access2);
|
|
3068
|
+
const configurationKey = nativeConfigurationKey(native);
|
|
3085
3069
|
const existing = operations.get(account.accountId);
|
|
3086
|
-
if (existing && existing.
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3070
|
+
if (existing && existing.configurationKey === configurationKey) {
|
|
3071
|
+
const refreshed = await client.operation(native.accountId, existing.operation.id);
|
|
3072
|
+
guard(access2);
|
|
3073
|
+
validateOperation(refreshed, native.accountId, existing.operation.id);
|
|
3074
|
+
existing.operation = refreshed;
|
|
3075
|
+
existing.client = client;
|
|
3076
|
+
if (!terminal.has(existing.operation.state)) return nativeLoginWait(account, Math.min(options.timeoutMs ?? 3e4, 3e4), true, access2);
|
|
3077
|
+
}
|
|
3078
|
+
guard(access2);
|
|
3090
3079
|
const operation = await client.login(native.accountId, {
|
|
3091
3080
|
allowQr: true,
|
|
3092
3081
|
forceQr: options.force === true,
|
|
3093
3082
|
timeoutSeconds: Math.max(10, Math.min(900, Math.ceil((options.timeoutMs ?? 3e5) / 1e3))),
|
|
3094
3083
|
computerName: "agent-wechat"
|
|
3095
|
-
});
|
|
3096
|
-
|
|
3097
|
-
|
|
3084
|
+
}, randomUUID5());
|
|
3085
|
+
validateOperation(operation, native.accountId);
|
|
3086
|
+
operations.set(account.accountId, { client, accountId: native.accountId, configurationKey, operation });
|
|
3087
|
+
guard(access2);
|
|
3088
|
+
return nativeLoginWait(account, Math.min(options.timeoutMs ?? 3e4, 3e4), true, access2);
|
|
3098
3089
|
}
|
|
3099
|
-
async function nativeLoginWait(
|
|
3100
|
-
|
|
3101
|
-
|
|
3090
|
+
async function nativeLoginWait(account, timeoutMs = 3e4, stopAtQr = false, access2 = {}) {
|
|
3091
|
+
guard(access2);
|
|
3092
|
+
const active2 = operations.get(account.accountId);
|
|
3093
|
+
if (!active2 || active2.configurationKey !== nativeConfigurationKey(account.native)) return { connected: false, state: "idle", message: "No native login operation; start login first" };
|
|
3102
3094
|
const deadline = Date.now() + Math.max(1e3, Math.min(timeoutMs, 3e5));
|
|
3103
3095
|
while (Date.now() < deadline) {
|
|
3104
|
-
|
|
3096
|
+
guard(access2);
|
|
3097
|
+
const refreshed = await active2.client.operation(active2.accountId, active2.operation.id);
|
|
3098
|
+
guard(access2);
|
|
3099
|
+
validateOperation(refreshed, active2.accountId, active2.operation.id);
|
|
3100
|
+
active2.operation = refreshed;
|
|
3105
3101
|
const state = active2.operation.state;
|
|
3106
|
-
|
|
3107
|
-
if (
|
|
3102
|
+
const result = { connected: state === "succeeded", state, operationId: active2.operation.id };
|
|
3103
|
+
if (state === "succeeded") return { ...result, message: "Native login succeeded" };
|
|
3104
|
+
if (terminal.has(state)) return { ...result, message: `Native login ${state}` };
|
|
3105
|
+
if (stopAtQr && state === "awaiting_approval") return { ...result, message: "Approve native login on your phone" };
|
|
3108
3106
|
if (stopAtQr && active2.operation.result?.qrAvailable === true) {
|
|
3109
3107
|
const generation = active2.operation.result?.qrGeneration;
|
|
3110
3108
|
const png = await active2.client.qr(active2.accountId, active2.operation.id, typeof generation === "number" ? generation : void 0);
|
|
3111
|
-
|
|
3109
|
+
guard(access2);
|
|
3110
|
+
return { ...result, qrDataUrl: `data:image/png;base64,${Buffer.from(png).toString("base64")}`, message: "Scan the white-background native QR and approve login" };
|
|
3112
3111
|
}
|
|
3113
|
-
|
|
3114
|
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
3112
|
+
await delay(Math.min(500, Math.max(0, deadline - Date.now())), void 0, { signal: access2.signal });
|
|
3115
3113
|
}
|
|
3116
|
-
|
|
3114
|
+
guard(access2);
|
|
3115
|
+
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
3116
|
}
|
|
3118
3117
|
async function sendNativeText(account, threadId2, text3, key = randomUUID5()) {
|
|
3119
3118
|
const native = account.native;
|
|
@@ -3127,29 +3126,59 @@ async function sendNativeText(account, threadId2, text3, key = randomUUID5()) {
|
|
|
3127
3126
|
{ sendId: send.id, state: send.state, serverIds: send.serverId ? [send.serverId] : void 0 }
|
|
3128
3127
|
);
|
|
3129
3128
|
}
|
|
3130
|
-
function createNativeLoginTool(account) {
|
|
3129
|
+
function createNativeLoginTool(account, dependencies = {}) {
|
|
3130
|
+
const actions = ["status", "start", "wait", "disconnect", "logout", "forget"];
|
|
3131
|
+
const configurationKey = nativeConfigurationKey(account.native);
|
|
3131
3132
|
return {
|
|
3132
3133
|
label: "WeChat Login",
|
|
3133
3134
|
name: "wechat_login",
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3135
|
+
catalogMode: "direct-only",
|
|
3136
|
+
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.",
|
|
3137
|
+
parameters: { type: "object", additionalProperties: false, properties: {
|
|
3138
|
+
action: { type: "string", enum: actions },
|
|
3139
|
+
force: { type: "boolean", description: "With start, use QR instead of saved trust for a new login. An active login is still reused." },
|
|
3140
|
+
timeoutMs: { type: "integer", minimum: 1e3, maximum: 3e5, description: "Wait bound in milliseconds; defaults to 30000. Start returns as soon as a QR is available." },
|
|
3141
|
+
confirm: { type: "boolean", description: "Required true for forget." }
|
|
3142
|
+
}, required: ["action"] },
|
|
3143
|
+
execute: async (_id, params, signal) => {
|
|
3144
|
+
if (!params || typeof params !== "object" || Array.isArray(params)) throw Error("Expected login arguments");
|
|
3137
3145
|
const args = params;
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3146
|
+
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");
|
|
3147
|
+
if (args.action === "forget" && args.confirm !== true) throw Error("Forgetting native credentials requires confirm=true");
|
|
3148
|
+
const access2 = { host: dependencies.host, signal, assertCurrent: () => {
|
|
3149
|
+
dependencies.context?.assertInvocationCurrent?.();
|
|
3150
|
+
if (!dependencies.context) return;
|
|
3151
|
+
const context = dependencies.context;
|
|
3152
|
+
const config = context.getRuntimeConfig?.() ?? context.runtimeConfig ?? context.config;
|
|
3153
|
+
const current = config && resolveWeChatAccount(config);
|
|
3154
|
+
if (!current?.enabled || !current.native || nativeConfigurationKey(current.native) !== configurationKey) throw Error("Native login configuration changed; invoke the tool again");
|
|
3155
|
+
} };
|
|
3156
|
+
guard(access2);
|
|
3157
|
+
if (args.action === "start" || args.action === "wait") {
|
|
3158
|
+
const result2 = args.action === "start" ? await nativeLoginStart(account, { force: args.force, timeoutMs: args.timeoutMs }, access2) : await nativeLoginWait(account, args.timeoutMs, false, access2);
|
|
3142
3159
|
const content = [{ type: "text", text: result2.message }];
|
|
3143
3160
|
if (result2.qrDataUrl) content.push({ type: "image", data: result2.qrDataUrl.split(",")[1], mimeType: "image/png" });
|
|
3144
|
-
return { content, details: { connected: result2.connected } };
|
|
3161
|
+
return { content, details: { connected: result2.connected, state: result2.state, operationId: result2.operationId } };
|
|
3145
3162
|
}
|
|
3163
|
+
const native = account.native;
|
|
3164
|
+
const { client } = await (access2.host ?? ensureNativeHost)(native);
|
|
3165
|
+
guard(access2);
|
|
3146
3166
|
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
3167
|
throw new Error("Unknown native login action");
|
|
3148
3168
|
})();
|
|
3169
|
+
if (args.action !== "status") operations.delete(account.accountId);
|
|
3170
|
+
guard(access2);
|
|
3149
3171
|
return { content: [{ type: "text", text: JSON.stringify(result) }], details: result };
|
|
3150
3172
|
}
|
|
3151
3173
|
};
|
|
3152
3174
|
}
|
|
3175
|
+
function registerNativeLoginTool(api) {
|
|
3176
|
+
api.registerTool({ contextVersion: 2, create: (context) => {
|
|
3177
|
+
const config = context.getRuntimeConfig?.() ?? context.runtimeConfig ?? context.config;
|
|
3178
|
+
const account = config && resolveWeChatAccount(config);
|
|
3179
|
+
return account?.enabled && account.native ? [createNativeLoginTool(account, { context })] : [];
|
|
3180
|
+
} }, { names: ["wechat_login"] });
|
|
3181
|
+
}
|
|
3153
3182
|
|
|
3154
3183
|
// src/channel.ts
|
|
3155
3184
|
async function sendWeChatText(cfg, to, text3) {
|
|
@@ -3346,7 +3375,7 @@ var wechatPlugin = {
|
|
|
3346
3375
|
loginWithQrWait: async ({ accountId, timeoutMs }) => {
|
|
3347
3376
|
const account = resolveWeChatAccount(getWeChatRuntime().config.current(), accountId ?? void 0);
|
|
3348
3377
|
if (!account?.native) return { connected: false, message: "No native daemon configured" };
|
|
3349
|
-
return nativeLoginWait(account
|
|
3378
|
+
return nativeLoginWait(account, timeoutMs);
|
|
3350
3379
|
}
|
|
3351
3380
|
},
|
|
3352
3381
|
auth: {
|
|
@@ -3372,10 +3401,6 @@ var wechatPlugin = {
|
|
|
3372
3401
|
linked: runtime2?.linked ?? false
|
|
3373
3402
|
})
|
|
3374
3403
|
},
|
|
3375
|
-
agentTools: (({ cfg }) => {
|
|
3376
|
-
const account = resolveWeChatAccount(cfg);
|
|
3377
|
-
return account?.native ? [createNativeLoginTool(account)] : [];
|
|
3378
|
-
}),
|
|
3379
3404
|
directory: {
|
|
3380
3405
|
self: async ({ cfg }) => {
|
|
3381
3406
|
const account = resolveWeChatAccount(cfg);
|
|
@@ -3600,7 +3625,7 @@ function string(value, name3, max = 4096) {
|
|
|
3600
3625
|
function threadId(value) {
|
|
3601
3626
|
return string(string(value, "thread ID").replace(/^(?:agent-)?wechat:(?:group:)?/i, ""), "thread ID");
|
|
3602
3627
|
}
|
|
3603
|
-
function messageResult(message, retrievalTool = "wechat_media") {
|
|
3628
|
+
function messageResult(message, retrievalTool = "wechat_media", quotePermitted = false) {
|
|
3604
3629
|
if (nativeRecallInfo(message)) return {
|
|
3605
3630
|
id: message.id,
|
|
3606
3631
|
threadId: message.threadId,
|
|
@@ -3615,8 +3640,15 @@ function messageResult(message, retrievalTool = "wechat_media") {
|
|
|
3615
3640
|
const isForward = message.type === 49 && message.appMessageType === 19;
|
|
3616
3641
|
const isLink = message.type === 49 && [3, 4, 5].includes(message.appMessageType ?? 0) && !!message.link;
|
|
3617
3642
|
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
|
-
|
|
3643
|
+
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]`;
|
|
3644
|
+
const quote = message.quote && {
|
|
3645
|
+
serverId: message.quote.serverId,
|
|
3646
|
+
type: message.quote.type,
|
|
3647
|
+
state: quotePermitted ? message.quote.state : "not permitted",
|
|
3648
|
+
senderId: message.quote.senderId,
|
|
3649
|
+
appMessageType: message.quote.appMessageType,
|
|
3650
|
+
embeddedImage: quotePermitted ? message.quote.embeddedImage : void 0
|
|
3651
|
+
};
|
|
3620
3652
|
return {
|
|
3621
3653
|
id: message.id,
|
|
3622
3654
|
threadId: message.threadId,
|
|
@@ -3625,7 +3657,7 @@ function messageResult(message, retrievalTool = "wechat_media") {
|
|
|
3625
3657
|
direction: message.direction,
|
|
3626
3658
|
type: message.type,
|
|
3627
3659
|
appMessageType: message.appMessageType,
|
|
3628
|
-
quote
|
|
3660
|
+
quote,
|
|
3629
3661
|
link: isLink ? message.link : void 0,
|
|
3630
3662
|
createdAt: message.createdAt,
|
|
3631
3663
|
body: body.slice(0, 4e3),
|
|
@@ -3676,7 +3708,7 @@ function createLookupTools(context, dependencies, archive) {
|
|
|
3676
3708
|
parameters: { type: "object", additionalProperties: false, properties, required },
|
|
3677
3709
|
execute: async (_callId, input, signal) => {
|
|
3678
3710
|
const args = object(input);
|
|
3679
|
-
const
|
|
3711
|
+
const guard2 = () => {
|
|
3680
3712
|
signal?.throwIfAborted();
|
|
3681
3713
|
context.assertInvocationCurrent?.();
|
|
3682
3714
|
const cfg2 = currentConfig();
|
|
@@ -3684,7 +3716,7 @@ function createLookupTools(context, dependencies, archive) {
|
|
|
3684
3716
|
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
3717
|
return { cfg: cfg2, account: account2, native: account2.native };
|
|
3686
3718
|
};
|
|
3687
|
-
const { cfg, account, native } =
|
|
3719
|
+
const { cfg, account, native } = guard2();
|
|
3688
3720
|
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
3721
|
const limit = args.limit ?? 20;
|
|
3690
3722
|
if (!Number.isInteger(limit) || limit < 1 || limit > 50) throw Error("limit must be 1\u201350");
|
|
@@ -3701,7 +3733,7 @@ function createLookupTools(context, dependencies, archive) {
|
|
|
3701
3733
|
const core = dependencies.core ?? getWeChatRuntime();
|
|
3702
3734
|
const allowed = async (sender) => {
|
|
3703
3735
|
if (archive) return true;
|
|
3704
|
-
const { cfg: cfg2, account: account2 } =
|
|
3736
|
+
const { cfg: cfg2, account: account2 } = guard2();
|
|
3705
3737
|
const result2 = await resolveWeChatIngress({
|
|
3706
3738
|
resolve: core.channel.inbound?.ingress?.resolveStable ?? resolveStableChannelMessageIngress2,
|
|
3707
3739
|
cfg: cfg2,
|
|
@@ -3715,10 +3747,15 @@ function createLookupTools(context, dependencies, archive) {
|
|
|
3715
3747
|
return result2.ingress.admission === "dispatch";
|
|
3716
3748
|
};
|
|
3717
3749
|
if (!await allowed(requesterSenderId)) throw Error("WeChat conversation access is no longer allowed");
|
|
3718
|
-
|
|
3750
|
+
guard2();
|
|
3719
3751
|
const { client } = await (dependencies.host ?? ensureNativeHost)(native);
|
|
3720
|
-
|
|
3752
|
+
guard2();
|
|
3721
3753
|
const accountId = native.accountId;
|
|
3754
|
+
const visibleMessage = async (message) => messageResult(
|
|
3755
|
+
message,
|
|
3756
|
+
archive ? "wechat_archive_media" : "wechat_media",
|
|
3757
|
+
!message.quote || archive || message.quote.direction === "outgoing" || !!message.quote.senderId && await allowed(message.quote.senderId)
|
|
3758
|
+
);
|
|
3722
3759
|
let result;
|
|
3723
3760
|
if (action === "threads") {
|
|
3724
3761
|
const page = await client.threads(accountId, { limit, cursor, ...query ? { query } : {} });
|
|
@@ -3731,21 +3768,20 @@ function createLookupTools(context, dependencies, archive) {
|
|
|
3731
3768
|
result = { ...page, items, scope: "local_profile", note: "Locally stored threads; coverage may be partial. This lookup does not mark threads read." };
|
|
3732
3769
|
} else if (action === "message" || action === "media") {
|
|
3733
3770
|
const message = await client.message(accountId, messageId);
|
|
3734
|
-
|
|
3771
|
+
guard2();
|
|
3735
3772
|
if (!await allowed(requesterSenderId)) throw Error("WeChat conversation access is no longer allowed");
|
|
3736
3773
|
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
|
-
|
|
3774
|
+
if (action === "message") result = await visibleMessage(message);
|
|
3738
3775
|
if (action === "media") {
|
|
3739
3776
|
const checkRecall = async () => {
|
|
3740
|
-
|
|
3777
|
+
guard2();
|
|
3741
3778
|
const latest = await client.message(accountId, messageId);
|
|
3742
|
-
|
|
3779
|
+
guard2();
|
|
3743
3780
|
if (latest.id !== message.id || latest.accountId !== accountId || latest.threadId !== message.threadId || latest.serverId !== message.serverId) throw Error("Native media message identity changed");
|
|
3744
3781
|
if (nativeRecallInfo(latest)) throw new NativeApiError(410, "message_recalled", false);
|
|
3745
3782
|
if (message.quote) {
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
if (quote.state === "recalled" || quote.message && nativeRecallInfo(quote.message)) throw new NativeApiError(410, "message_recalled", false);
|
|
3783
|
+
if (latest.quote?.state === "recalled") throw new NativeApiError(410, "message_recalled", false);
|
|
3784
|
+
if (!latest.quote || JSON.stringify(latest.quote) !== JSON.stringify(message.quote)) throw new NativeApiError(409, "media_source_changed", false);
|
|
3749
3785
|
}
|
|
3750
3786
|
};
|
|
3751
3787
|
let prepared;
|
|
@@ -3760,7 +3796,7 @@ function createLookupTools(context, dependencies, archive) {
|
|
|
3760
3796
|
download: args.download !== false,
|
|
3761
3797
|
waitMs: 2e3,
|
|
3762
3798
|
signal,
|
|
3763
|
-
assertCurrent:
|
|
3799
|
+
assertCurrent: guard2,
|
|
3764
3800
|
allowQuotedMessage: async (original) => original.direction === "outgoing" || await allowed(original.senderId ?? void 0),
|
|
3765
3801
|
beforeMediaAccess: async () => {
|
|
3766
3802
|
if (!await allowed(requesterSenderId) || message.direction !== "outgoing" && !await allowed(message.senderId ?? void 0)) throw Error("WeChat conversation access is no longer allowed");
|
|
@@ -3768,7 +3804,7 @@ function createLookupTools(context, dependencies, archive) {
|
|
|
3768
3804
|
}
|
|
3769
3805
|
});
|
|
3770
3806
|
} catch (error) {
|
|
3771
|
-
|
|
3807
|
+
guard2();
|
|
3772
3808
|
if (!(error instanceof NativeApiError)) throw error;
|
|
3773
3809
|
const details2 = {
|
|
3774
3810
|
messageId: message.id,
|
|
@@ -3779,7 +3815,7 @@ function createLookupTools(context, dependencies, archive) {
|
|
|
3779
3815
|
};
|
|
3780
3816
|
return { content: [{ type: "text", text: JSON.stringify(details2) }], details: details2 };
|
|
3781
3817
|
}
|
|
3782
|
-
|
|
3818
|
+
guard2();
|
|
3783
3819
|
if (!await allowed(requesterSenderId) || message.direction !== "outgoing" && !await allowed(message.senderId ?? void 0)) throw Error("WeChat conversation access is no longer allowed");
|
|
3784
3820
|
const details = {
|
|
3785
3821
|
messageId: message.id,
|
|
@@ -3796,7 +3832,7 @@ function createLookupTools(context, dependencies, archive) {
|
|
|
3796
3832
|
};
|
|
3797
3833
|
const image = prepared.suppliedAssets?.find((asset) => asset.role === "model_image");
|
|
3798
3834
|
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
|
-
|
|
3835
|
+
guard2();
|
|
3800
3836
|
try {
|
|
3801
3837
|
await checkRecall();
|
|
3802
3838
|
} catch (error) {
|
|
@@ -3813,7 +3849,7 @@ function createLookupTools(context, dependencies, archive) {
|
|
|
3813
3849
|
for (const message of page.items) {
|
|
3814
3850
|
if (isNativeTombstone(message)) continue;
|
|
3815
3851
|
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(
|
|
3852
|
+
if (message.direction === "outgoing" || await allowed(message.senderId ?? void 0)) items.push(await visibleMessage(message));
|
|
3817
3853
|
}
|
|
3818
3854
|
result = {
|
|
3819
3855
|
...page,
|
|
@@ -3823,7 +3859,7 @@ function createLookupTools(context, dependencies, archive) {
|
|
|
3823
3859
|
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
3860
|
};
|
|
3825
3861
|
}
|
|
3826
|
-
|
|
3862
|
+
guard2();
|
|
3827
3863
|
return { content: [{ type: "text", text: JSON.stringify(result) }], details: result };
|
|
3828
3864
|
}
|
|
3829
3865
|
});
|
|
@@ -3838,13 +3874,13 @@ function createLookupTools(context, dependencies, archive) {
|
|
|
3838
3874
|
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
3875
|
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
3876
|
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"])
|
|
3877
|
+
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
3878
|
];
|
|
3843
3879
|
return [
|
|
3844
3880
|
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
3881
|
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
3882
|
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"])
|
|
3883
|
+
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
3884
|
];
|
|
3849
3885
|
}
|
|
3850
3886
|
function registerNativeHistoryTools(api) {
|
|
@@ -3910,7 +3946,7 @@ function createNativeShareTools(context, dependencies = {}) {
|
|
|
3910
3946
|
...args.description !== void 0 ? { description: args.description === "" ? "" : text(args.description, "description", 16384, true) } : {},
|
|
3911
3947
|
...args.thumbnailUrl !== void 0 ? { thumbnailUrl: url(args.thumbnailUrl, "thumbnail URL") } : {}
|
|
3912
3948
|
};
|
|
3913
|
-
const
|
|
3949
|
+
const guard2 = () => {
|
|
3914
3950
|
signal?.throwIfAborted();
|
|
3915
3951
|
context.assertInvocationCurrent?.();
|
|
3916
3952
|
const cfg = currentConfig(), account = resolveWeChatAccount(cfg);
|
|
@@ -3919,7 +3955,7 @@ function createNativeShareTools(context, dependencies = {}) {
|
|
|
3919
3955
|
};
|
|
3920
3956
|
const core = dependencies.core ?? getWeChatRuntime();
|
|
3921
3957
|
const authorize = async () => {
|
|
3922
|
-
const { cfg, account } =
|
|
3958
|
+
const { cfg, account } = guard2();
|
|
3923
3959
|
const result = await resolveWeChatIngress({
|
|
3924
3960
|
resolve: core.channel.inbound?.ingress?.resolveStable ?? resolveStableChannelMessageIngress3,
|
|
3925
3961
|
cfg,
|
|
@@ -3930,11 +3966,11 @@ function createNativeShareTools(context, dependencies = {}) {
|
|
|
3930
3966
|
senderId: requester,
|
|
3931
3967
|
readStoreAllowFrom: async () => core.channel.pairing.readAllowFromStore({ channel: "agent-wechat", accountId: account.accountId, env: process.env }).catch(() => [])
|
|
3932
3968
|
});
|
|
3933
|
-
|
|
3969
|
+
guard2();
|
|
3934
3970
|
if (result.ingress.admission !== "dispatch") throw Error("WeChat conversation access is no longer allowed");
|
|
3935
3971
|
};
|
|
3936
3972
|
await authorize();
|
|
3937
|
-
const { native } =
|
|
3973
|
+
const { native } = guard2();
|
|
3938
3974
|
const { client } = await (dependencies.host ?? ensureNativeHost)(native);
|
|
3939
3975
|
await authorize();
|
|
3940
3976
|
let intent;
|
|
@@ -4012,7 +4048,7 @@ function createNativeRecallTools(context, dependencies = {}) {
|
|
|
4012
4048
|
const messageId = text2(input.messageId, "message ID");
|
|
4013
4049
|
const serverId = text2(input.serverId, "server ID");
|
|
4014
4050
|
if (!/^[1-9]\d{0,19}$/.test(serverId) || BigInt(serverId) > 18446744073709551615n) throw Error("Invalid exact server message ID");
|
|
4015
|
-
const
|
|
4051
|
+
const guard2 = () => {
|
|
4016
4052
|
signal?.throwIfAborted();
|
|
4017
4053
|
context.assertInvocationCurrent?.();
|
|
4018
4054
|
const cfg = currentConfig(), account = resolveWeChatAccount(cfg);
|
|
@@ -4020,7 +4056,7 @@ function createNativeRecallTools(context, dependencies = {}) {
|
|
|
4020
4056
|
return { cfg, account, native: account.native };
|
|
4021
4057
|
};
|
|
4022
4058
|
const allowed = async () => {
|
|
4023
|
-
const { cfg, account } =
|
|
4059
|
+
const { cfg, account } = guard2();
|
|
4024
4060
|
const core = dependencies.core ?? getWeChatRuntime();
|
|
4025
4061
|
const ingress = await resolveWeChatIngress({
|
|
4026
4062
|
resolve: core.channel.inbound?.ingress?.resolveStable ?? resolveStableChannelMessageIngress4,
|
|
@@ -4032,20 +4068,20 @@ function createNativeRecallTools(context, dependencies = {}) {
|
|
|
4032
4068
|
senderId: requester,
|
|
4033
4069
|
readStoreAllowFrom: async () => core.channel.pairing.readAllowFromStore({ channel: "agent-wechat", accountId: account.accountId, env: process.env }).catch(() => [])
|
|
4034
4070
|
});
|
|
4035
|
-
|
|
4071
|
+
guard2();
|
|
4036
4072
|
if (ingress.ingress.admission !== "dispatch") throw Error("WeChat conversation access is no longer allowed");
|
|
4037
4073
|
};
|
|
4038
4074
|
await allowed();
|
|
4039
|
-
const { native } =
|
|
4075
|
+
const { native } = guard2();
|
|
4040
4076
|
const { client } = await (dependencies.host ?? ensureNativeHost)(native);
|
|
4041
|
-
|
|
4077
|
+
guard2();
|
|
4042
4078
|
const message = await client.message(accountId, messageId);
|
|
4043
|
-
|
|
4079
|
+
guard2();
|
|
4044
4080
|
const retainedRecall = !!nativeRecallInfo(message);
|
|
4045
4081
|
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
4082
|
if (message.serverId !== serverId) throw Error("The exact server identity does not match this receipt; the local message identity may have been reused");
|
|
4047
4083
|
await allowed();
|
|
4048
|
-
|
|
4084
|
+
guard2();
|
|
4049
4085
|
const key = nativeRecallKey(accountId, conversation, messageId, serverId);
|
|
4050
4086
|
let operation;
|
|
4051
4087
|
let operationId;
|
|
@@ -4075,13 +4111,13 @@ function createNativeRecallTools(context, dependencies = {}) {
|
|
|
4075
4111
|
const deadline = Date.now() + Math.max(0, Math.min(dependencies.waitMs ?? 2e3, 1e4));
|
|
4076
4112
|
while (["queued", "running", "submitting"].includes(operation.state) && Date.now() < deadline) {
|
|
4077
4113
|
await new Promise((resolve) => setTimeout(resolve, Math.min(250, deadline - Date.now())));
|
|
4078
|
-
|
|
4114
|
+
guard2();
|
|
4079
4115
|
const remaining = Math.max(1, deadline - Date.now());
|
|
4080
4116
|
const next = await client.operation(accountId, operation.id, signal ? AbortSignal.any([signal, AbortSignal.timeout(remaining)]) : AbortSignal.timeout(remaining));
|
|
4081
4117
|
checkOperation(next);
|
|
4082
4118
|
operation = next;
|
|
4083
4119
|
}
|
|
4084
|
-
|
|
4120
|
+
guard2();
|
|
4085
4121
|
} catch (error) {
|
|
4086
4122
|
const reason = error instanceof NativeApiError ? error.code : "recall_result_unavailable";
|
|
4087
4123
|
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 +4163,7 @@ function createNativeGroupTools(context, dependencies = {}) {
|
|
|
4127
4163
|
const args = input;
|
|
4128
4164
|
const identity = (value) => typeof value === "string" && value.length > 0 && value.length <= 256 && !/[\s\u0000-\u001f\u007f]/u.test(value);
|
|
4129
4165
|
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
|
|
4166
|
+
const guard2 = () => {
|
|
4131
4167
|
signal?.throwIfAborted();
|
|
4132
4168
|
if (context.senderIsOwner !== true) throw Error("OpenClaw owner authorization is required");
|
|
4133
4169
|
context.assertInvocationCurrent();
|
|
@@ -4135,14 +4171,14 @@ function createNativeGroupTools(context, dependencies = {}) {
|
|
|
4135
4171
|
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
4172
|
return account.native;
|
|
4137
4173
|
};
|
|
4138
|
-
const native =
|
|
4174
|
+
const native = guard2();
|
|
4139
4175
|
const { client } = await (dependencies.host ?? ensureNativeHost)(native);
|
|
4140
|
-
|
|
4176
|
+
guard2();
|
|
4141
4177
|
const group = await client.group(accountId, args.groupId, signal);
|
|
4142
|
-
|
|
4178
|
+
guard2();
|
|
4143
4179
|
if (group.id !== args.groupId || group.roomId !== args.groupId) throw Error("Group identity is invalid");
|
|
4144
4180
|
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
|
-
|
|
4181
|
+
guard2();
|
|
4146
4182
|
const details = { accountId, group, members, source: "cached", membershipAbsence: "unconfirmed" };
|
|
4147
4183
|
return { content: [{ type: "text", text: JSON.stringify(details) }], details };
|
|
4148
4184
|
}
|
|
@@ -4166,6 +4202,7 @@ var plugin = {
|
|
|
4166
4202
|
registerNativeShareTools(api);
|
|
4167
4203
|
registerNativeRecallTools(api);
|
|
4168
4204
|
registerNativeGroupTools(api);
|
|
4205
|
+
registerNativeLoginTool(api);
|
|
4169
4206
|
const stopFacade = selected?.native ? registerNativeFacade(api, selected.native) : () => {
|
|
4170
4207
|
};
|
|
4171
4208
|
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 },
|