@agent-wechat/agent-wechat 1.0.0-beta.3 → 1.0.0-beta.5
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 +4 -4
- package/dist/index.js +87 -39
- package/openclaw.plugin.json +2 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ Configure plugin defaults under `plugins.entries.agent-wechat.config` (or the sa
|
|
|
21
21
|
}
|
|
22
22
|
```
|
|
23
23
|
|
|
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
|
|
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
26
|
## Native managed mode
|
|
27
27
|
|
|
@@ -50,15 +50,15 @@ Keep `dataDir` and `consumerStateDir` on separate explicit persistent mounts. Th
|
|
|
50
50
|
|
|
51
51
|
The adapter consumes account-scoped SSE using bearer headers and decimal-string journal cursors. It persists custody before acknowledging a cursor; acknowledgement is independent of unread state. Only explicitly live, incoming, committed resources can enter the model. Supported images, short voice, video, files and eligible stickers pass through bounded media retrieval and hash verification into OpenClaw-managed storage. Pending media retains custody without model dispatch or a read receipt. Expired/unsupported attachments retain an explicit status; raw XML never becomes an attachment. Catch-up/import/raw pending sync batches, unknown origin and outgoing messages do not become automatic model inputs. Group admission uses structured exact self-mention facts; unknown metadata or displayed `@names` never satisfy a mention gate. Contacts and groups use their separate catalogs.
|
|
52
52
|
|
|
53
|
-
The prepared-run callback
|
|
53
|
+
The prepared-run callback records the selected model input before marking its whole thread read. This also clears earlier unread messages in that thread, including messages the model did not receive. The daemon freezes the latest local message boundary, checks the expected version, updates local unread state and submits one native cross-device notification. A lost notification response remains `needs_review` and is never automatically resubmitted. `readPolicy: "never"` disables both local and cross-device marking. Crashes during an unconfirmed dispatch retain `needs_review`; the plugin does not automatically duplicate model turns or uncertain outgoing sends.
|
|
54
54
|
|
|
55
55
|
OpenClaw **2026.10.2** lacks `onPreparedAgentRunStart`, so its released public hooks provide a bounded compatibility path. Before dispatch, the adapter persists an immutable manifest binding the exact new message ID/version/content hash, policy hash, source and supplied asset hashes, and context roles. Text requires the exact current input and host-created message/account/session facts plus a matching `model_call_started`. Media additionally waits for the same run's `agent_end` snapshot and source-turn identity. Image bytes/MIME/length must match the frozen supplied representation; file/video/voice references must appear in the exact current body. Image count, model selection and reply delivery alone cannot mark a message read. Unknown, omitted, rewritten, merged or ambiguous inputs remain unconfirmed.
|
|
56
56
|
|
|
57
|
-
The compatibility path requires the operator's explicit host permission, `plugins.entries.agent-wechat.hooks.allowConversationAccess: true`. It observes only run IDs issued by this adapter and never enables that permission itself. Without the grant, or on an unsupported host, messages remain unread and status reports `handoffUnconfirmed`. The older `onAgentRunStart` also reports direct tools and early lifecycle phases and is never used as proof. `readPolicy: "never"` preserves the handoff receipt while disabling read projection. Late confirmed receipts
|
|
57
|
+
The compatibility path requires the operator's explicit host permission, `plugins.entries.agent-wechat.hooks.allowConversationAccess: true`. It observes only run IDs issued by this adapter and never enables that permission itself. Without the grant, or on an unsupported host, messages remain unread and status reports `handoffUnconfirmed`. The older `onAgentRunStart` also reports direct tools and early lifecycle phases and is never used as proof. `readPolicy: "never"` preserves the handoff receipt while disabling read projection. Late confirmed receipts can mark their thread independently of new SSE traffic. Access and mention gates still precede model dispatch; a rejected message does not itself trigger a read.
|
|
58
58
|
|
|
59
59
|
Images exceeding the host's bounded representation, or formats requiring conversion, receive a separately stored JPEG still preview using the public SDK image processor. The original stays intact; both hashes are recorded and the model input labels the preview. Prepared images fit within 5 MiB and at most 1200 pixels per side (or a smaller configured limit). Files and videos can be supplied as available local references; this does not claim their contents were analyzed. Voice retains its verified file and includes configured transcription output or an explicit unavailable status. Pending, expired, unsupported and failed-integrity media never gains a read receipt from a placeholder.
|
|
60
60
|
|
|
61
|
-
Run `node scripts/native-model-smoke.mjs` and `node scripts/native-media-model-smoke.mjs` after installing workspace dependencies. They use the actual pinned Gateway, a local model and synthetic daemon/files. Checks cover current provider input, image hashes and prepared previews, reference/transcription delivery,
|
|
61
|
+
Run `node scripts/native-model-smoke.mjs` and `node scripts/native-media-model-smoke.mjs` after installing workspace dependencies. They use the actual pinned Gateway, a local model and synthetic daemon/files. Checks cover current provider input, image hashes and prepared previews, reference/transcription delivery, whole-thread read requests, lost-response/reopen recovery, duplicate prevention, omitted images on a text-only model, permission, never-read policy and group routing behavior. Transcription uses the real SDK with an explicitly configured local deterministic CLI; it does not test speech recognition accuracy. Concurrent text inputs were serialized into distinct model turns; merged-input refusal has unit coverage. These checks establish the prepared runtime boundary, not external provider acceptance, semantic analysis or consumption.
|
|
62
62
|
|
|
63
63
|
Native text/media sends retain daemon intent/job identity and caller idempotency. Text waits for accepted submission; media jobs preserve partial and uncertain outcomes rather than resending. Ordinary audio remains a file; only explicit `audioAsVoice` requests independently encoded voice. Sender/group/mention gates run before retrieval/transcription. Verified voice may use the configured OpenClaw transcription runtime after admission. Native QR login uses a durable daemon operation and authenticated PNG retrieval. Logout commits local inhibition before at most one authenticated native logout, reporting the remote outcome separately; saved-device revocation remains unverified. Explicitly confirmed forgetting removes local auth while retaining history; disconnect retains reusable trust.
|
|
64
64
|
|
package/dist/index.js
CHANGED
|
@@ -22,7 +22,7 @@ var nativeConfigSchema = {
|
|
|
22
22
|
}
|
|
23
23
|
}] },
|
|
24
24
|
consumerStateDir: { type: "string" },
|
|
25
|
-
readPolicy: { type: "string", enum: ["never", "on_model_dispatch"] },
|
|
25
|
+
readPolicy: { type: "string", enum: ["never", "on_inbound", "on_model_dispatch"] },
|
|
26
26
|
gatewayFacade: { type: "object", additionalProperties: false, required: ["enabled"], properties: { enabled: { type: "boolean" }, prefix: { type: "string" } } }
|
|
27
27
|
}
|
|
28
28
|
};
|
|
@@ -169,6 +169,7 @@ var NativeWeChatClient = class {
|
|
|
169
169
|
token;
|
|
170
170
|
fetchImpl;
|
|
171
171
|
timeoutMs;
|
|
172
|
+
streamIdleTimeoutMs;
|
|
172
173
|
constructor(options) {
|
|
173
174
|
this.baseUrl = validateNativeBaseUrl(options.baseUrl);
|
|
174
175
|
const nodeEnvironment = globalThis.process?.env;
|
|
@@ -179,6 +180,9 @@ var NativeWeChatClient = class {
|
|
|
179
180
|
this.token = options.token;
|
|
180
181
|
this.fetchImpl = options.fetch ?? fetch;
|
|
181
182
|
this.timeoutMs = options.timeoutMs ?? 3e4;
|
|
183
|
+
this.streamIdleTimeoutMs = options.streamIdleTimeoutMs ?? 45e3;
|
|
184
|
+
if (!Number.isInteger(this.streamIdleTimeoutMs) || this.streamIdleTimeoutMs < 1 || this.streamIdleTimeoutMs > 3e5)
|
|
185
|
+
throw new Error("Invalid native stream idle timeout");
|
|
182
186
|
}
|
|
183
187
|
async response(path, init = {}, stream = false) {
|
|
184
188
|
const headers = new Headers(init.headers);
|
|
@@ -436,16 +440,10 @@ var NativeWeChatClient = class {
|
|
|
436
440
|
}
|
|
437
441
|
return { bytes: await this.readBounded(response, max), contentType: response.headers.get("content-type") ?? "application/octet-stream", etag: response.headers.get("etag"), contentRange: response.headers.get("content-range"), status: response.status };
|
|
438
442
|
}
|
|
439
|
-
markRead(accountId, threadId,
|
|
440
|
-
if (
|
|
441
|
-
throw new Error("Invalid exact-ID read projection");
|
|
442
|
-
return this.request(`v1/accounts/${id(accountId)}/threads/${id(threadId)}/read-state`, { method: "POST", body: JSON.stringify({ messageIds: [...new Set(messageIds)], expectedVersion }) });
|
|
443
|
-
}
|
|
444
|
-
markReadThrough(accountId, threadId, throughMessageId, expectedVersion) {
|
|
445
|
-
id(throughMessageId);
|
|
446
|
-
if (typeof expectedVersion !== "string" || !expectedVersion)
|
|
443
|
+
markRead(accountId, threadId, expectedVersion) {
|
|
444
|
+
if (typeof expectedVersion !== "string" || !expectedVersion || expectedVersion.length > 128)
|
|
447
445
|
throw new Error("Read version is required");
|
|
448
|
-
return this.request(`v1/accounts/${id(accountId)}/threads/${id(threadId)}/read-state`, { method: "POST", body: JSON.stringify({
|
|
446
|
+
return this.request(`v1/accounts/${id(accountId)}/threads/${id(threadId)}/read-state`, { method: "POST", body: JSON.stringify({ expectedVersion }) });
|
|
449
447
|
}
|
|
450
448
|
events(accountId, after = "0", limit = 100) {
|
|
451
449
|
if (!Number.isInteger(limit) || limit < 1 || limit > 200)
|
|
@@ -456,12 +454,27 @@ var NativeWeChatClient = class {
|
|
|
456
454
|
return this.request(`v1/accounts/${id(accountId)}/consumers/${id(consumerId)}/ack`, { method: "POST", body: JSON.stringify({ cursor: decimal(cursor) }) });
|
|
457
455
|
}
|
|
458
456
|
async *watch(accountId, after = "0", signal) {
|
|
459
|
-
const
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
457
|
+
const connection = new AbortController();
|
|
458
|
+
const streamSignal = signal ? AbortSignal.any([signal, connection.signal]) : connection.signal;
|
|
459
|
+
const bounded = async (work, milliseconds, reason) => {
|
|
460
|
+
if (streamSignal.aborted)
|
|
461
|
+
throw streamSignal.reason;
|
|
462
|
+
let timer;
|
|
463
|
+
let onAbort = () => {
|
|
464
|
+
};
|
|
465
|
+
const interrupted = new Promise((_resolve, reject) => {
|
|
466
|
+
onAbort = () => reject(streamSignal.reason ?? new Error("Native SSE cancelled"));
|
|
467
|
+
streamSignal.addEventListener("abort", onAbort, { once: true });
|
|
468
|
+
timer = setTimeout(() => connection.abort(new Error(reason)), milliseconds);
|
|
469
|
+
});
|
|
470
|
+
try {
|
|
471
|
+
return await Promise.race([work(), interrupted]);
|
|
472
|
+
} finally {
|
|
473
|
+
clearTimeout(timer);
|
|
474
|
+
streamSignal.removeEventListener("abort", onAbort);
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
let reader;
|
|
465
478
|
const decoder = new TextDecoder();
|
|
466
479
|
let buffer = "";
|
|
467
480
|
let data = [];
|
|
@@ -469,8 +482,17 @@ var NativeWeChatClient = class {
|
|
|
469
482
|
let eventCursor = "";
|
|
470
483
|
let eventType = "";
|
|
471
484
|
try {
|
|
485
|
+
const response = await bounded(() => this.response(`v1/accounts/${id(accountId)}/events/stream?after=${decimal(after)}`, { signal: streamSignal, headers: { Accept: "text/event-stream" } }, true), this.timeoutMs, "Native SSE connection timeout");
|
|
486
|
+
if (!response.headers.get("content-type")?.startsWith("text/event-stream")) {
|
|
487
|
+
await response.body?.cancel();
|
|
488
|
+
throw new Error("Expected native SSE response");
|
|
489
|
+
}
|
|
490
|
+
reader = response.body?.getReader();
|
|
491
|
+
if (!reader)
|
|
492
|
+
throw new Error("Native SSE has no body");
|
|
472
493
|
while (true) {
|
|
473
|
-
const
|
|
494
|
+
const activeReader = reader;
|
|
495
|
+
const { value, done } = await bounded(() => activeReader.read(), this.streamIdleTimeoutMs, "Native SSE idle timeout");
|
|
474
496
|
if (done)
|
|
475
497
|
break;
|
|
476
498
|
buffer += decoder.decode(value, { stream: true });
|
|
@@ -510,9 +532,12 @@ var NativeWeChatClient = class {
|
|
|
510
532
|
}
|
|
511
533
|
}
|
|
512
534
|
} finally {
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
535
|
+
connection.abort();
|
|
536
|
+
if (reader) {
|
|
537
|
+
await reader.cancel().catch(() => {
|
|
538
|
+
});
|
|
539
|
+
reader.releaseLock();
|
|
540
|
+
}
|
|
516
541
|
}
|
|
517
542
|
}
|
|
518
543
|
};
|
|
@@ -709,7 +734,7 @@ function collectWeChatStatusIssues(accounts) {
|
|
|
709
734
|
accountId: snapshot.accountId,
|
|
710
735
|
kind: "runtime",
|
|
711
736
|
message: "Prepared model handoff was not confirmed by this OpenClaw host; automatic read state remains unchanged.",
|
|
712
|
-
fix: "
|
|
737
|
+
fix: "Select native.readPolicy=on_inbound to mark successfully handled conversations read without model hooks, or use a compatible prepared-run SDK hook."
|
|
713
738
|
});
|
|
714
739
|
}
|
|
715
740
|
if ((snapshot.needsReview ?? 0) > 0) {
|
|
@@ -1333,7 +1358,7 @@ var NativeConsumerState = class {
|
|
|
1333
1358
|
if (message && (!message.id || message.accountId !== this.value.accountId)) throw new Error("Native journal message crosses account boundary");
|
|
1334
1359
|
this.mutate(() => {
|
|
1335
1360
|
if (message && !this.value.items[message.id]) {
|
|
1336
|
-
const completed = Object.entries(this.value.items).filter(([, item]) => !item.readPending && ["completed", "ignored"].includes(item.state));
|
|
1361
|
+
const completed = Object.entries(this.value.items).filter(([, item]) => !item.readPending && item.phoneReadState !== "needs_review" && item.phoneReadState !== "rejected" && ["completed", "ignored"].includes(item.state));
|
|
1337
1362
|
for (const [key] of completed.slice(0, Math.max(0, completed.length - 1e3))) delete this.value.items[key];
|
|
1338
1363
|
if (Object.keys(this.value.items).length >= 5e3) throw new Error("Native custody queue is full; operator review required");
|
|
1339
1364
|
this.value.items[message.id] = { message, origin, state: "received" };
|
|
@@ -1548,14 +1573,16 @@ function nativeWasMentioned(message) {
|
|
|
1548
1573
|
return message.mentions?.state === "known" && message.mentions.self === true;
|
|
1549
1574
|
}
|
|
1550
1575
|
async function projectNativeRead(client, accountId, state) {
|
|
1551
|
-
|
|
1576
|
+
const pending2 = state.items().filter((value) => value.readPending);
|
|
1577
|
+
const threads = [...new Set(pending2.map((item) => item.message.threadId))];
|
|
1578
|
+
for (const threadId of threads) {
|
|
1552
1579
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
1553
1580
|
let cursor;
|
|
1554
1581
|
let thread;
|
|
1555
1582
|
const seen = /* @__PURE__ */ new Set();
|
|
1556
1583
|
for (let pageIndex = 0; pageIndex < 100; pageIndex++) {
|
|
1557
1584
|
const page = await client.threads(accountId, cursor);
|
|
1558
|
-
thread = page.items.find((value) => !isNativeTombstone(value) && value.id ===
|
|
1585
|
+
thread = page.items.find((value) => !isNativeTombstone(value) && value.id === threadId);
|
|
1559
1586
|
if (thread || !page.nextCursor) break;
|
|
1560
1587
|
if (seen.has(page.nextCursor)) throw new Error("Native thread pagination did not advance");
|
|
1561
1588
|
seen.add(page.nextCursor);
|
|
@@ -1563,8 +1590,10 @@ async function projectNativeRead(client, accountId, state) {
|
|
|
1563
1590
|
}
|
|
1564
1591
|
if (!thread) break;
|
|
1565
1592
|
try {
|
|
1566
|
-
await client.markRead(accountId, thread.id,
|
|
1567
|
-
|
|
1593
|
+
const receipt = await client.markRead(accountId, thread.id, thread.readStateVersion);
|
|
1594
|
+
const notification = receipt?.phoneNotification?.state;
|
|
1595
|
+
const phoneReadState = notification === "accepted" || notification === "rejected" || notification === "needs_review" ? notification : void 0;
|
|
1596
|
+
for (const item of pending2.filter((item2) => item2.message.threadId === threadId)) state.update(item.message.id, { readPending: false, phoneReadState });
|
|
1568
1597
|
break;
|
|
1569
1598
|
} catch (error) {
|
|
1570
1599
|
if (error instanceof NativeApiError && error.status === 409 && attempt === 0) continue;
|
|
@@ -1575,6 +1604,7 @@ async function projectNativeRead(client, accountId, state) {
|
|
|
1575
1604
|
}
|
|
1576
1605
|
async function dispatchNativeMessage(params) {
|
|
1577
1606
|
const { client, state, account, message, cfg, core } = params;
|
|
1607
|
+
const readPolicy = account.native?.readPolicy ?? "on_inbound";
|
|
1578
1608
|
const custody = state.get(message.id);
|
|
1579
1609
|
if (!custody || custody.state !== "received") return "needs_review";
|
|
1580
1610
|
if (custody.origin !== "live" || message.direction !== "incoming" || ![1, 3, 34, 43, 47, 49].includes(message.type) || message.type === 1 && !message.body || !message.senderId) {
|
|
@@ -1669,7 +1699,7 @@ async function dispatchNativeMessage(params) {
|
|
|
1669
1699
|
assets: preparedMedia.suppliedAssets ?? []
|
|
1670
1700
|
});
|
|
1671
1701
|
state.update(message.id, { state: "dispatching", dispatchManifest });
|
|
1672
|
-
const callback = message.type === 1 || preparedMedia.attachment?.status === "ready" && preparedMedia.suppliedAssets?.length ? state.preparedCallback([message.id],
|
|
1702
|
+
const callback = readPolicy !== "on_inbound" && (message.type === 1 || preparedMedia.attachment?.status === "ready" && preparedMedia.suppliedAssets?.length) ? state.preparedCallback([message.id], readPolicy) : void 0;
|
|
1673
1703
|
if (callback) trackNativePreparedInput(dispatchManifest, callback);
|
|
1674
1704
|
let failed = false;
|
|
1675
1705
|
let delivery = 0;
|
|
@@ -1691,6 +1721,7 @@ async function dispatchNativeMessage(params) {
|
|
|
1691
1721
|
const key = createHash8("sha256").update(`${account.native.accountId}\0${message.id}\0${delivery++}`).digest("hex");
|
|
1692
1722
|
await submitNativeText(client, account.native.accountId, message.threadId, payload.text, key);
|
|
1693
1723
|
}
|
|
1724
|
+
if (urls.length || payload.text) params.onDelivered?.();
|
|
1694
1725
|
},
|
|
1695
1726
|
onError: () => {
|
|
1696
1727
|
failed = true;
|
|
@@ -1702,8 +1733,12 @@ async function dispatchNativeMessage(params) {
|
|
|
1702
1733
|
// and media-snapshot bridge; unsupported or ambiguous handoffs leave reads unchanged.
|
|
1703
1734
|
replyOptions: { runId: dispatchManifest.runId, onModelSelected, onPreparedAgentRunStart: callback }
|
|
1704
1735
|
});
|
|
1705
|
-
const
|
|
1706
|
-
state.update(message.id, {
|
|
1736
|
+
const readPending = readPolicy === "on_inbound" && !failed && ingress.ingress.admission === "dispatch" ? true : state.get(message.id)?.readPending;
|
|
1737
|
+
state.update(message.id, {
|
|
1738
|
+
state: failed ? "needs_review" : "completed",
|
|
1739
|
+
readPending,
|
|
1740
|
+
handoffUnconfirmed: readPolicy === "on_model_dispatch" && !state.get(message.id)?.runId
|
|
1741
|
+
});
|
|
1707
1742
|
return failed ? "needs_review" : "completed";
|
|
1708
1743
|
} catch {
|
|
1709
1744
|
state.update(message.id, { state: "needs_review" });
|
|
@@ -1737,23 +1772,34 @@ async function startNativeMonitor(opts) {
|
|
|
1737
1772
|
const core = getWeChatRuntime();
|
|
1738
1773
|
let backoff = 250;
|
|
1739
1774
|
let readTask;
|
|
1740
|
-
const flushRead = () =>
|
|
1741
|
-
|
|
1742
|
-
|
|
1775
|
+
const flushRead = () => {
|
|
1776
|
+
if (native.readPolicy === "never") return Promise.resolve();
|
|
1777
|
+
return readTask ??= projectNativeRead(host2.client, native.accountId, state).finally(() => {
|
|
1778
|
+
readTask = void 0;
|
|
1779
|
+
});
|
|
1780
|
+
};
|
|
1743
1781
|
const process2 = async () => {
|
|
1744
1782
|
const cfg = core.config.current();
|
|
1745
1783
|
const account = resolveWeChatAccount(cfg, opts.account.accountId) ?? opts.account;
|
|
1746
1784
|
if (account.backend !== "native" || JSON.stringify(account.native) !== JSON.stringify(native)) throw new Error("Native service configuration changed; restart required before processing");
|
|
1747
1785
|
for (const item of state.items().filter((value) => value.state === "received")) {
|
|
1748
|
-
await dispatchNativeMessage({
|
|
1786
|
+
await dispatchNativeMessage({
|
|
1787
|
+
client: host2.client,
|
|
1788
|
+
state,
|
|
1789
|
+
account,
|
|
1790
|
+
message: item.message,
|
|
1791
|
+
cfg,
|
|
1792
|
+
core,
|
|
1793
|
+
onDelivered: () => opts.setStatus({ accountId: account.accountId, lastOutboundAt: Date.now() })
|
|
1794
|
+
});
|
|
1749
1795
|
}
|
|
1750
1796
|
try {
|
|
1751
1797
|
await flushRead();
|
|
1752
1798
|
} catch {
|
|
1753
|
-
opts.log?.error?.("Native read projection is pending;
|
|
1799
|
+
opts.log?.error?.("Native read projection is pending; thread read receipt retained for retry");
|
|
1754
1800
|
}
|
|
1755
|
-
const needsReview = state.items().filter((value) => value.state === "needs_review").length;
|
|
1756
|
-
const handoffUnconfirmed = native.readPolicy
|
|
1801
|
+
const needsReview = state.items().filter((value) => value.state === "needs_review" || value.phoneReadState === "needs_review" || value.phoneReadState === "rejected").length;
|
|
1802
|
+
const handoffUnconfirmed = native.readPolicy === "on_model_dispatch" && state.items().some((value) => value.handoffUnconfirmed);
|
|
1757
1803
|
const accountStatus = await host2.client.account(native.accountId);
|
|
1758
1804
|
opts.setStatus({
|
|
1759
1805
|
accountId: account.accountId,
|
|
@@ -1769,9 +1815,9 @@ async function startNativeMonitor(opts) {
|
|
|
1769
1815
|
await flushRead();
|
|
1770
1816
|
if (!opts.abortSignal.aborted) opts.setStatus({
|
|
1771
1817
|
accountId: opts.account.accountId,
|
|
1772
|
-
handoffUnconfirmed: native.readPolicy
|
|
1818
|
+
handoffUnconfirmed: native.readPolicy === "on_model_dispatch" && state.items().some((value) => value.handoffUnconfirmed)
|
|
1773
1819
|
});
|
|
1774
|
-
}, () => opts.log?.error?.("Native read projection remains pending;
|
|
1820
|
+
}, () => opts.log?.error?.("Native read projection remains pending; thread read receipts retained"));
|
|
1775
1821
|
try {
|
|
1776
1822
|
while (!opts.abortSignal.aborted) {
|
|
1777
1823
|
try {
|
|
@@ -1785,6 +1831,7 @@ async function startNativeMonitor(opts) {
|
|
|
1785
1831
|
if (message.threadId !== event.data.threadId) throw new Error("Native committed message thread identity mismatch");
|
|
1786
1832
|
}
|
|
1787
1833
|
state.accept(event.cursor, message, typeof event.data.origin === "string" ? event.data.origin : "unknown");
|
|
1834
|
+
if (message?.direction === "incoming" && event.data.origin === "live") opts.setStatus({ accountId: opts.account.accountId, lastInboundAt: Date.now() });
|
|
1788
1835
|
await host2.client.ack(native.accountId, "openclaw", state.cursor);
|
|
1789
1836
|
await process2();
|
|
1790
1837
|
backoff = 250;
|
|
@@ -2322,7 +2369,8 @@ var plugin = {
|
|
|
2322
2369
|
api.registerChannel({ plugin: wechatPlugin });
|
|
2323
2370
|
const stopFacade = selected?.native ? registerNativeFacade(api, selected.native) : () => {
|
|
2324
2371
|
};
|
|
2325
|
-
const stopPreparedBoundary = registerNativePreparedBoundary(api)
|
|
2372
|
+
const stopPreparedBoundary = selected?.native?.readPolicy && selected.native.readPolicy !== "on_inbound" ? registerNativePreparedBoundary(api) : () => {
|
|
2373
|
+
};
|
|
2326
2374
|
api.registerService({
|
|
2327
2375
|
id: "agent-wechat-native",
|
|
2328
2376
|
reload: { configPrefixes: ["plugins.entries.agent-wechat.config", "channels.agent-wechat", "secrets"] },
|
package/openclaw.plugin.json
CHANGED
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.5",
|
|
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.1"
|
|
15
15
|
},
|
|
16
16
|
"openclaw": {
|
|
17
17
|
"extensions": [
|
|
@@ -183,6 +183,7 @@
|
|
|
183
183
|
"type": "string",
|
|
184
184
|
"enum": [
|
|
185
185
|
"never",
|
|
186
|
+
"on_inbound",
|
|
186
187
|
"on_model_dispatch"
|
|
187
188
|
]
|
|
188
189
|
},
|