cli-wechat-bridge 1.1.7 → 1.2.0
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/LICENSE.txt +661 -661
- package/README.md +461 -434
- package/bin/_run-entry.mjs +29 -6
- package/bin/wechat-check-update.mjs +6 -1
- package/bin/wechat-claude.mjs +6 -1
- package/bin/wechat-codex.mjs +6 -1
- package/bin/wechat-daemon.mjs +6 -1
- package/bin/wechat-opencode.mjs +6 -1
- package/bin/wechat-pi.mjs +6 -1
- package/bin/wechat-setup.mjs +6 -1
- package/bin/wecom-check-update.mjs +6 -1
- package/bin/wecom-claude.mjs +6 -1
- package/bin/wecom-codex.mjs +6 -1
- package/bin/wecom-daemon.mjs +6 -1
- package/bin/wecom-opencode.mjs +6 -1
- package/bin/wecom-pi.mjs +6 -1
- package/bin/wecom-setup.mjs +6 -1
- package/dist/bridge/adapter-control.js +58 -0
- package/dist/bridge/bridge-adapters.claude.js +35 -1
- package/dist/bridge/bridge-adapters.codex.js +118 -7
- package/dist/bridge/bridge-adapters.core.js +25 -1
- package/dist/bridge/bridge-adapters.opencode.js +156 -1
- package/dist/bridge/bridge-adapters.pi.js +45 -0
- package/dist/bridge/bridge-adapters.shared.js +6 -0
- package/dist/bridge/bridge-controller.js +6 -0
- package/dist/bridge/bridge-final-reply.js +6 -0
- package/dist/bridge/bridge-process-reaper.js +6 -0
- package/dist/bridge/bridge-state.js +6 -0
- package/dist/bridge/bridge-utils.js +6 -31
- package/dist/bridge/claude-hook.js +6 -0
- package/dist/bridge/claude-native-controls.js +133 -0
- package/dist/bridge/native-terminal-control.js +259 -0
- package/dist/bridge/wechat-bridge.js +250 -274
- package/dist/bridge/wechat-forwarding.js +6 -0
- package/dist/bridge/wechat-outbound-queue.js +6 -0
- package/dist/channels/wechat/bridge-final-reply.js +13 -22
- package/dist/channels/wechat/index.js +6 -0
- package/dist/channels/wechat/wechat-driver.js +191 -0
- package/dist/channels/wechat/wechat-forwarding.js +18 -0
- package/dist/channels/wechat/wechat-outbound-queue.js +39 -0
- package/dist/channels/wecom/bridge-final-reply.js +6 -0
- package/dist/channels/wecom/setup-cli.js +6 -0
- package/dist/channels/wecom/setup.js +6 -0
- package/dist/channels/wecom/wecom-config.js +6 -0
- package/dist/channels/wecom/wecom-driver.js +106 -0
- package/dist/channels/wecom/wecom-media.js +6 -0
- package/dist/channels/wecom/wecom-transport.js +6 -0
- package/dist/commands/check-update.js +6 -0
- package/dist/companion/codex-remote-client.js +28 -2
- package/dist/companion/codex-visible-client-link.js +107 -0
- package/dist/companion/local-companion-link.js +6 -0
- package/dist/companion/local-companion-start.js +13 -2
- package/dist/companion/local-companion.js +23 -0
- package/dist/companion/opencode-tui-bridge-plugin.js +6 -0
- package/dist/companion/pi-tui-bridge-extension.js +62 -3
- package/dist/core/bridge-control.js +10 -0
- package/dist/core/bridge-defer.js +25 -0
- package/dist/core/channel-driver.js +1 -0
- package/dist/core/conversation-routing.js +58 -0
- package/dist/core/direct-run.js +6 -0
- package/dist/core/inbound-message-claims.js +6 -0
- package/dist/core/outbound-target.js +32 -0
- package/dist/core/text-utils.js +6 -0
- package/dist/core/turn-coordinator.js +85 -0
- package/dist/daemon/daemon-link.js +6 -0
- package/dist/daemon/emoji-bindings.js +6 -0
- package/dist/daemon/wechat-daemon.js +529 -205
- package/dist/i18n/index.js +6 -0
- package/dist/i18n/messages-en.js +7 -1
- package/dist/i18n/messages-zh.js +7 -1
- package/dist/runtime/create-runtime-host.js +6 -0
- package/dist/runtime/legacy-adapter-runtime.js +15 -0
- package/dist/runtime/runtime-types.js +3 -0
- package/dist/utils/atomic-file.js +6 -0
- package/dist/utils/doctor.js +6 -0
- package/dist/utils/version-checker.js +17 -11
- package/dist/wechat/channel-config.js +6 -0
- package/dist/wechat/setup.js +6 -0
- package/dist/wechat/wechat-channel.js +6 -0
- package/dist/wechat/wechat-transport.js +169 -58
- package/package.json +108 -106
- package/scripts/ensure-node-pty-permissions.mjs +73 -73
package/dist/i18n/index.js
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Copyright (c) 2026 UNLINEARITY <unlinearity@gmail.com>
|
|
3
|
+
// Source: https://github.com/UNLINEARITY/CLI-WeChat-Bridge
|
|
4
|
+
// This file is part of CLI-WeChat-Bridge. Modifications and derivative works
|
|
5
|
+
// must be released under AGPL-3.0-or-later with full source code; see
|
|
6
|
+
// LICENSE.txt. Network services built on it must offer source to users.
|
|
1
7
|
import { messages as zhMessages } from "./messages-zh.js";
|
|
2
8
|
import { messages as enMessages } from "./messages-en.js";
|
|
3
9
|
const catalogs = {
|
package/dist/i18n/messages-en.js
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Copyright (c) 2026 UNLINEARITY <unlinearity@gmail.com>
|
|
3
|
+
// Source: https://github.com/UNLINEARITY/CLI-WeChat-Bridge
|
|
4
|
+
// This file is part of CLI-WeChat-Bridge. Modifications and derivative works
|
|
5
|
+
// must be released under AGPL-3.0-or-later with full source code; see
|
|
6
|
+
// LICENSE.txt. Network services built on it must offer source to users.
|
|
1
7
|
export const messages = {
|
|
2
8
|
// === Bridge welcome & control ===
|
|
3
9
|
"bridge.welcome": "WeChat Bridge ready ({adapter}).\nCWD: {cwd}",
|
|
@@ -11,7 +17,7 @@ export const messages = {
|
|
|
11
17
|
"bridge.fatalError": "Bridge error: {message}",
|
|
12
18
|
"bridge.backlogIgnored": "Ignored {count} backlog message(s) sent before the bridge started (they will not be processed).\nIf you actually sent them after the bridge started, your system clock is likely skewed by more than {graceSeconds}s — sync the clock and restart the bridge.",
|
|
13
19
|
// === Daemon welcome & control ===
|
|
14
|
-
"daemon.welcome": "WeChat Daemon ready.\nCWD: {cwd}\nActive: {adapter}\n\nCommands: /claude, /codex, /opencode, /pi, /stop, /confirm, /deny, /status
|
|
20
|
+
"daemon.welcome": "WeChat Daemon ready.\nCWD: {cwd}\nActive: {adapter}\n\nCommands: /claude, /codex, /opencode, /pi, /stop, /confirm, /deny, /status, /model, /plan, /all <prompt>\n{bindings}\n\nManage: /bind [emoji] cmd, /unbind [emoji], /bindings",
|
|
15
21
|
"daemon.noActiveAdapter": "No active terminal is selected. Send /codex, /claude, /opencode, or /pi to start one.",
|
|
16
22
|
"daemon.switchResult.new": "Started a new visible CLI.",
|
|
17
23
|
"daemon.switchResult.reused": "Reused the existing visible CLI.",
|
package/dist/i18n/messages-zh.js
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Copyright (c) 2026 UNLINEARITY <unlinearity@gmail.com>
|
|
3
|
+
// Source: https://github.com/UNLINEARITY/CLI-WeChat-Bridge
|
|
4
|
+
// This file is part of CLI-WeChat-Bridge. Modifications and derivative works
|
|
5
|
+
// must be released under AGPL-3.0-or-later with full source code; see
|
|
6
|
+
// LICENSE.txt. Network services built on it must offer source to users.
|
|
1
7
|
export const messages = {
|
|
2
8
|
// === 桥接欢迎 & 控制 ===
|
|
3
9
|
"bridge.welcome": "微信桥接就绪 ({adapter})。\n工作目录: {cwd}",
|
|
@@ -11,7 +17,7 @@ export const messages = {
|
|
|
11
17
|
"bridge.fatalError": "桥接错误: {message}",
|
|
12
18
|
"bridge.backlogIgnored": "已忽略 {count} 条桥接启动前的积压消息(不会自动处理)。\n如果你是在桥接启动后才发送的消息却看到此提示,说明本机系统时钟可能不准(偏差超过 {graceSeconds} 秒),请校准时间后重启桥接。",
|
|
13
19
|
// === Daemon 欢迎 & 控制 ===
|
|
14
|
-
"daemon.welcome": "微信 Daemon 就绪。\n工作目录: {cwd}\n当前: {adapter}\n\n命令: /claude, /codex, /opencode, /pi, /stop, /confirm, /deny, /status
|
|
20
|
+
"daemon.welcome": "微信 Daemon 就绪。\n工作目录: {cwd}\n当前: {adapter}\n\n命令: /claude, /codex, /opencode, /pi, /stop, /confirm, /deny, /status, /model, /plan, /all <prompt>\n{bindings}\n\n管理: /bind [表情] 命令, /unbind [表情], /bindings",
|
|
15
21
|
"daemon.noActiveAdapter": "未选择活跃终端。发送 /codex、/claude、/opencode 或 /pi 启动一个。",
|
|
16
22
|
"daemon.switchResult.new": "已启动新的可见 CLI。",
|
|
17
23
|
"daemon.switchResult.reused": "已复用现有的可见 CLI。",
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Copyright (c) 2026 UNLINEARITY <unlinearity@gmail.com>
|
|
3
|
+
// Source: https://github.com/UNLINEARITY/CLI-WeChat-Bridge
|
|
4
|
+
// This file is part of CLI-WeChat-Bridge. Modifications and derivative works
|
|
5
|
+
// must be released under AGPL-3.0-or-later with full source code; see
|
|
6
|
+
// LICENSE.txt. Network services built on it must offer source to users.
|
|
1
7
|
import { createBridgeAdapter } from "../bridge/bridge-adapters.js";
|
|
2
8
|
import { CodexPtyAdapter } from "../bridge/bridge-adapters.codex.js";
|
|
3
9
|
import { LegacyAdapterRuntime } from "./legacy-adapter-runtime.js";
|
|
@@ -25,6 +25,21 @@ export class LegacyAdapterRuntime {
|
|
|
25
25
|
}
|
|
26
26
|
await this.adapter.createSession();
|
|
27
27
|
}
|
|
28
|
+
async listModels() {
|
|
29
|
+
if (!this.adapter.listModels)
|
|
30
|
+
throw new Error("Model listing is not supported by this companion.");
|
|
31
|
+
return this.adapter.listModels();
|
|
32
|
+
}
|
|
33
|
+
async selectModel(modelId) {
|
|
34
|
+
if (!this.adapter.selectModel)
|
|
35
|
+
throw new Error("Model selection is not supported by this companion.");
|
|
36
|
+
return this.adapter.selectModel(modelId);
|
|
37
|
+
}
|
|
38
|
+
async setPlanMode(enabled) {
|
|
39
|
+
if (!this.adapter.setPlanMode)
|
|
40
|
+
throw new Error("Plan mode is not supported by this companion.");
|
|
41
|
+
return this.adapter.setPlanMode(enabled);
|
|
42
|
+
}
|
|
28
43
|
async interrupt() {
|
|
29
44
|
return await this.adapter.interrupt();
|
|
30
45
|
}
|
|
@@ -3,3 +3,6 @@ export const CODEX_REMOTE_AUTH_TOKEN_ENV = "WECHAT_BRIDGE_CODEX_REMOTE_AUTH_TOKE
|
|
|
3
3
|
export function hasLocalClientEndpointProvider(runtime) {
|
|
4
4
|
return typeof runtime.getLocalClientEndpoint === "function";
|
|
5
5
|
}
|
|
6
|
+
export function hasVisibleClientSessionPreparer(runtime) {
|
|
7
|
+
return typeof runtime.prepareVisibleClientSession === "function";
|
|
8
|
+
}
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Copyright (c) 2026 UNLINEARITY <unlinearity@gmail.com>
|
|
3
|
+
// Source: https://github.com/UNLINEARITY/CLI-WeChat-Bridge
|
|
4
|
+
// This file is part of CLI-WeChat-Bridge. Modifications and derivative works
|
|
5
|
+
// must be released under AGPL-3.0-or-later with full source code; see
|
|
6
|
+
// LICENSE.txt. Network services built on it must offer source to users.
|
|
1
7
|
import fs from "node:fs";
|
|
2
8
|
import { ensureChannelDataDir } from "../wechat/channel-config.js";
|
|
3
9
|
/**
|
package/dist/utils/doctor.js
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Copyright (c) 2026 UNLINEARITY <unlinearity@gmail.com>
|
|
3
|
+
// Source: https://github.com/UNLINEARITY/CLI-WeChat-Bridge
|
|
4
|
+
// This file is part of CLI-WeChat-Bridge. Modifications and derivative works
|
|
5
|
+
// must be released under AGPL-3.0-or-later with full source code; see
|
|
6
|
+
// LICENSE.txt. Network services built on it must offer source to users.
|
|
1
7
|
import { execFileSync } from "node:child_process";
|
|
2
8
|
import fs from "node:fs";
|
|
3
9
|
import net from "node:net";
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Copyright (c) 2026 UNLINEARITY <unlinearity@gmail.com>
|
|
3
|
+
// Source: https://github.com/UNLINEARITY/CLI-WeChat-Bridge
|
|
4
|
+
// This file is part of CLI-WeChat-Bridge. Modifications and derivative works
|
|
5
|
+
// must be released under AGPL-3.0-or-later with full source code; see
|
|
6
|
+
// LICENSE.txt. Network services built on it must offer source to users.
|
|
1
7
|
import fs from "node:fs";
|
|
2
8
|
import path from "node:path";
|
|
3
9
|
import { CHANNEL_DATA_DIR, ensureChannelDataDir } from "../wechat/channel-config.js";
|
|
@@ -179,16 +185,16 @@ export async function checkForUpdate(forceCheck = false) {
|
|
|
179
185
|
*/
|
|
180
186
|
export function formatUpdateMessage(versionInfo) {
|
|
181
187
|
const { current, latest } = versionInfo;
|
|
182
|
-
return `
|
|
183
|
-
[Update Available] Version ${latest} is available (current: ${current})
|
|
184
|
-
|
|
185
|
-
Update instructions:
|
|
186
|
-
cd CLI-WeChat-Bridge
|
|
187
|
-
git pull
|
|
188
|
-
bun install
|
|
189
|
-
npm install -g .
|
|
190
|
-
|
|
191
|
-
For more information:
|
|
192
|
-
https://github.com/UNLINEARITY/CLI-WeChat-Bridge/releases
|
|
188
|
+
return `
|
|
189
|
+
[Update Available] Version ${latest} is available (current: ${current})
|
|
190
|
+
|
|
191
|
+
Update instructions:
|
|
192
|
+
cd CLI-WeChat-Bridge
|
|
193
|
+
git pull
|
|
194
|
+
bun install
|
|
195
|
+
npm install -g .
|
|
196
|
+
|
|
197
|
+
For more information:
|
|
198
|
+
https://github.com/UNLINEARITY/CLI-WeChat-Bridge/releases
|
|
193
199
|
`;
|
|
194
200
|
}
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Copyright (c) 2026 UNLINEARITY <unlinearity@gmail.com>
|
|
3
|
+
// Source: https://github.com/UNLINEARITY/CLI-WeChat-Bridge
|
|
4
|
+
// This file is part of CLI-WeChat-Bridge. Modifications and derivative works
|
|
5
|
+
// must be released under AGPL-3.0-or-later with full source code; see
|
|
6
|
+
// LICENSE.txt. Network services built on it must offer source to users.
|
|
1
7
|
import crypto from "node:crypto";
|
|
2
8
|
import fs from "node:fs";
|
|
3
9
|
import os from "node:os";
|
package/dist/wechat/setup.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
// Copyright (c) 2026 UNLINEARITY <unlinearity@gmail.com>
|
|
4
|
+
// Source: https://github.com/UNLINEARITY/CLI-WeChat-Bridge
|
|
5
|
+
// This file is part of CLI-WeChat-Bridge. Modifications and derivative works
|
|
6
|
+
// must be released under AGPL-3.0-or-later with full source code; see
|
|
7
|
+
// LICENSE.txt. Network services built on it must offer source to users.
|
|
2
8
|
/**
|
|
3
9
|
* CLI WeChat Bridge setup.
|
|
4
10
|
*
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
// Copyright (c) 2026 UNLINEARITY <unlinearity@gmail.com>
|
|
4
|
+
// Source: https://github.com/UNLINEARITY/CLI-WeChat-Bridge
|
|
5
|
+
// This file is part of CLI-WeChat-Bridge. Modifications and derivative works
|
|
6
|
+
// must be released under AGPL-3.0-or-later with full source code; see
|
|
7
|
+
// LICENSE.txt. Network services built on it must offer source to users.
|
|
2
8
|
/**
|
|
3
9
|
* WeChat MCP server.
|
|
4
10
|
*
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Copyright (c) 2026 UNLINEARITY <unlinearity@gmail.com>
|
|
3
|
+
// Source: https://github.com/UNLINEARITY/CLI-WeChat-Bridge
|
|
4
|
+
// This file is part of CLI-WeChat-Bridge. Modifications and derivative works
|
|
5
|
+
// must be released under AGPL-3.0-or-later with full source code; see
|
|
6
|
+
// LICENSE.txt. Network services built on it must offer source to users.
|
|
1
7
|
import crypto from "node:crypto";
|
|
2
8
|
import { createCipheriv, createDecipheriv } from "node:crypto";
|
|
3
9
|
import fs from "node:fs";
|
|
@@ -332,6 +338,15 @@ async function apiFetch(params) {
|
|
|
332
338
|
const url = new URL(params.endpoint, base).toString();
|
|
333
339
|
const controller = new AbortController();
|
|
334
340
|
const timer = setTimeout(() => controller.abort(), params.timeoutMs);
|
|
341
|
+
const abortFromCaller = () => controller.abort(params.signal?.reason);
|
|
342
|
+
if (params.signal) {
|
|
343
|
+
if (params.signal.aborted) {
|
|
344
|
+
abortFromCaller();
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
params.signal.addEventListener("abort", abortFromCaller, { once: true });
|
|
348
|
+
}
|
|
349
|
+
}
|
|
335
350
|
try {
|
|
336
351
|
const res = await fetch(url, {
|
|
337
352
|
method: "POST",
|
|
@@ -353,6 +368,9 @@ async function apiFetch(params) {
|
|
|
353
368
|
clearTimeout(timer);
|
|
354
369
|
throw err;
|
|
355
370
|
}
|
|
371
|
+
finally {
|
|
372
|
+
params.signal?.removeEventListener("abort", abortFromCaller);
|
|
373
|
+
}
|
|
356
374
|
}
|
|
357
375
|
function encryptAesEcb(plaintext, key) {
|
|
358
376
|
const cipher = createCipheriv("aes-128-ecb", key, null);
|
|
@@ -806,12 +824,15 @@ function buildInboundAttachmentFilePath(params) {
|
|
|
806
824
|
const uniquePrefix = `${timestamp.replace(/[:.]/g, "-")}-${crypto.randomBytes(4).toString("hex")}`;
|
|
807
825
|
return path.join(directory, `${uniquePrefix}-${safeFileName}`);
|
|
808
826
|
}
|
|
827
|
+
const WECHAT_TYPING_TIMEOUT_MS = 8_000;
|
|
809
828
|
export class WeChatTransport {
|
|
810
829
|
logger;
|
|
811
830
|
recentMessageKeys = new Set();
|
|
812
831
|
recentMessageOrder = [];
|
|
813
832
|
contextTokenCache;
|
|
814
833
|
syncBuffer = "";
|
|
834
|
+
stopped = false;
|
|
835
|
+
activePollControllers = new Set();
|
|
815
836
|
constructor(logger) {
|
|
816
837
|
this.logger = logger;
|
|
817
838
|
migrateLegacyChannelFiles((message) => this.logger.log(message));
|
|
@@ -855,73 +876,96 @@ export class WeChatTransport {
|
|
|
855
876
|
? "Reset sync state and cleared cached context tokens."
|
|
856
877
|
: "Reset sync state.";
|
|
857
878
|
}
|
|
879
|
+
stop() {
|
|
880
|
+
if (this.stopped) {
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
this.stopped = true;
|
|
884
|
+
for (const controller of this.activePollControllers) {
|
|
885
|
+
controller.abort();
|
|
886
|
+
}
|
|
887
|
+
this.activePollControllers.clear();
|
|
888
|
+
}
|
|
858
889
|
async pollMessages(options = {}) {
|
|
890
|
+
if (this.stopped) {
|
|
891
|
+
return { messages: [], ignoredBacklogCount: 0 };
|
|
892
|
+
}
|
|
859
893
|
const timeoutMs = options.timeoutMs ?? DEFAULT_LONG_POLL_TIMEOUT_MS;
|
|
860
894
|
const account = this.requireAccount();
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
this.
|
|
865
|
-
response
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
}
|
|
870
|
-
const isError = (response.ret !== undefined && response.ret !== 0) ||
|
|
871
|
-
(response.errcode !== undefined && response.errcode !== 0);
|
|
872
|
-
if (isError) {
|
|
873
|
-
throw new Error(`getUpdates failed: ret=${response.ret} errcode=${response.errcode} errmsg=${response.errmsg ?? ""}`);
|
|
874
|
-
}
|
|
875
|
-
if (response.get_updates_buf) {
|
|
876
|
-
this.syncBuffer = response.get_updates_buf;
|
|
877
|
-
this.saveSyncBuffer(this.syncBuffer);
|
|
878
|
-
}
|
|
879
|
-
const messages = [];
|
|
880
|
-
let ignoredBacklogCount = 0;
|
|
881
|
-
for (const rawMessage of response.msgs ?? []) {
|
|
882
|
-
if (rawMessage.message_type !== MSG_TYPE_USER) {
|
|
883
|
-
continue;
|
|
895
|
+
const controller = new AbortController();
|
|
896
|
+
this.activePollControllers.add(controller);
|
|
897
|
+
try {
|
|
898
|
+
let response = await this.getUpdates(account, timeoutMs, controller.signal);
|
|
899
|
+
if (isWechatSyncSessionTimeout(response) && this.syncBuffer && !this.stopped) {
|
|
900
|
+
this.logger.log("WeChat sync session timed out. Clearing local sync cursor and retrying once.");
|
|
901
|
+
this.clearSyncBuffer();
|
|
902
|
+
response = await this.getUpdates(account, timeoutMs, controller.signal);
|
|
884
903
|
}
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
continue;
|
|
904
|
+
if (this.stopped) {
|
|
905
|
+
return { messages: [], ignoredBacklogCount: 0 };
|
|
888
906
|
}
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
this.cacheContextToken(senderId, rawMessage.context_token);
|
|
907
|
+
if (isWechatSyncSessionTimeout(response)) {
|
|
908
|
+
throw new Error('WeChat session timed out. Run "wechat-setup" to log in again.');
|
|
892
909
|
}
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
if (typeof options.minCreatedAtMs === "number" &&
|
|
898
|
-
Number.isFinite(createdAtMs) &&
|
|
899
|
-
createdAtMs > 0 &&
|
|
900
|
-
createdAtMs < options.minCreatedAtMs) {
|
|
901
|
-
ignoredBacklogCount += 1;
|
|
902
|
-
continue;
|
|
910
|
+
const isError = (response.ret !== undefined && response.ret !== 0) ||
|
|
911
|
+
(response.errcode !== undefined && response.errcode !== 0);
|
|
912
|
+
if (isError) {
|
|
913
|
+
throw new Error(`getUpdates failed: ret=${response.ret} errcode=${response.errcode} errmsg=${response.errmsg ?? ""}`);
|
|
903
914
|
}
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
915
|
+
if (response.get_updates_buf) {
|
|
916
|
+
this.syncBuffer = response.get_updates_buf;
|
|
917
|
+
this.saveSyncBuffer(this.syncBuffer);
|
|
907
918
|
}
|
|
908
|
-
|
|
909
|
-
|
|
919
|
+
const messages = [];
|
|
920
|
+
let ignoredBacklogCount = 0;
|
|
921
|
+
for (const rawMessage of response.msgs ?? []) {
|
|
922
|
+
if (rawMessage.message_type !== MSG_TYPE_USER) {
|
|
923
|
+
continue;
|
|
924
|
+
}
|
|
925
|
+
const extracted = extractInboundMessageContent(rawMessage);
|
|
926
|
+
if (!extracted.text && extracted.attachments.length === 0) {
|
|
927
|
+
continue;
|
|
928
|
+
}
|
|
929
|
+
const senderId = rawMessage.from_user_id ?? "unknown";
|
|
930
|
+
if (rawMessage.context_token) {
|
|
931
|
+
this.cacheContextToken(senderId, rawMessage.context_token);
|
|
932
|
+
}
|
|
933
|
+
// Filter pre-start backlog BEFORE claiming/remembering the message so a
|
|
934
|
+
// skipped message is not permanently locked away from other processes.
|
|
935
|
+
// A missing create_time_ms is treated as fresh rather than dropped.
|
|
936
|
+
const createdAtMs = rawMessage.create_time_ms ?? 0;
|
|
937
|
+
if (typeof options.minCreatedAtMs === "number" &&
|
|
938
|
+
Number.isFinite(createdAtMs) &&
|
|
939
|
+
createdAtMs > 0 &&
|
|
940
|
+
createdAtMs < options.minCreatedAtMs) {
|
|
941
|
+
ignoredBacklogCount += 1;
|
|
942
|
+
continue;
|
|
943
|
+
}
|
|
944
|
+
const messageKey = buildMessageKey(rawMessage);
|
|
945
|
+
if (!this.rememberMessage(messageKey)) {
|
|
946
|
+
continue;
|
|
947
|
+
}
|
|
948
|
+
if (!tryClaimInboundMessage(buildScopedMessageClaimKey(account.accountId, messageKey))) {
|
|
949
|
+
continue;
|
|
950
|
+
}
|
|
951
|
+
const { attachments, failureLines } = await this.downloadInboundAttachments(extracted.attachments, rawMessage);
|
|
952
|
+
const text = appendInboundAttachmentFailureText(extracted.text, failureLines);
|
|
953
|
+
messages.push({
|
|
954
|
+
senderId,
|
|
955
|
+
sender: normalizeSender(senderId),
|
|
956
|
+
sessionId: rawMessage.session_id ?? "",
|
|
957
|
+
text,
|
|
958
|
+
attachments,
|
|
959
|
+
contextToken: rawMessage.context_token,
|
|
960
|
+
createdAt: formatTimestamp(rawMessage.create_time_ms),
|
|
961
|
+
createdAtMs,
|
|
962
|
+
});
|
|
910
963
|
}
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
sender: normalizeSender(senderId),
|
|
916
|
-
sessionId: rawMessage.session_id ?? "",
|
|
917
|
-
text,
|
|
918
|
-
attachments,
|
|
919
|
-
contextToken: rawMessage.context_token,
|
|
920
|
-
createdAt: formatTimestamp(rawMessage.create_time_ms),
|
|
921
|
-
createdAtMs,
|
|
922
|
-
});
|
|
964
|
+
return { messages, ignoredBacklogCount };
|
|
965
|
+
}
|
|
966
|
+
finally {
|
|
967
|
+
this.activePollControllers.delete(controller);
|
|
923
968
|
}
|
|
924
|
-
return { messages, ignoredBacklogCount };
|
|
925
969
|
}
|
|
926
970
|
async downloadInboundAttachments(descriptors, rawMessage) {
|
|
927
971
|
const attachments = [];
|
|
@@ -963,6 +1007,72 @@ export class WeChatTransport {
|
|
|
963
1007
|
}
|
|
964
1008
|
return { attachments, failureLines };
|
|
965
1009
|
}
|
|
1010
|
+
/** Typing status values for {@link sendTyping}. 1 = typing, 2 = cancel. */
|
|
1011
|
+
static TYPING_STATUS = { TYPING: 1, CANCEL: 2 };
|
|
1012
|
+
/**
|
|
1013
|
+
* Fetch the per-user typing ticket from ilink/bot/getconfig.
|
|
1014
|
+
* Returns "" when the endpoint fails or reports no ticket.
|
|
1015
|
+
*/
|
|
1016
|
+
async fetchTypingTicket(recipientId) {
|
|
1017
|
+
const account = this.requireAccount();
|
|
1018
|
+
const resolvedRecipientId = recipientId?.trim() ||
|
|
1019
|
+
[...this.contextTokenCache.keys()].at(-1);
|
|
1020
|
+
if (!resolvedRecipientId) {
|
|
1021
|
+
return "";
|
|
1022
|
+
}
|
|
1023
|
+
try {
|
|
1024
|
+
const raw = await apiFetch({
|
|
1025
|
+
baseUrl: account.baseUrl,
|
|
1026
|
+
endpoint: "ilink/bot/getconfig",
|
|
1027
|
+
body: JSON.stringify({
|
|
1028
|
+
ilink_user_id: resolvedRecipientId,
|
|
1029
|
+
...(this.contextTokenCache.has(resolvedRecipientId)
|
|
1030
|
+
? { context_token: this.contextTokenCache.get(resolvedRecipientId) }
|
|
1031
|
+
: {}),
|
|
1032
|
+
}),
|
|
1033
|
+
token: account.token,
|
|
1034
|
+
timeoutMs: WECHAT_TYPING_TIMEOUT_MS,
|
|
1035
|
+
});
|
|
1036
|
+
const resp = JSON.parse(raw);
|
|
1037
|
+
return resp.ret === 0 ? (resp.typing_ticket ?? "") : "";
|
|
1038
|
+
}
|
|
1039
|
+
catch {
|
|
1040
|
+
return "";
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
1044
|
+
* Send or cancel a "typing…" indicator. Failures are logged only; the
|
|
1045
|
+
* indicator is best-effort and never blocks message flow.
|
|
1046
|
+
*/
|
|
1047
|
+
async sendTyping(recipientId, typingTicket, status) {
|
|
1048
|
+
if (!typingTicket) {
|
|
1049
|
+
return false;
|
|
1050
|
+
}
|
|
1051
|
+
const account = this.requireAccount();
|
|
1052
|
+
try {
|
|
1053
|
+
const raw = await apiFetch({
|
|
1054
|
+
baseUrl: account.baseUrl,
|
|
1055
|
+
endpoint: "ilink/bot/sendtyping",
|
|
1056
|
+
body: JSON.stringify({
|
|
1057
|
+
ilink_user_id: recipientId,
|
|
1058
|
+
typing_ticket: typingTicket,
|
|
1059
|
+
status,
|
|
1060
|
+
}),
|
|
1061
|
+
token: account.token,
|
|
1062
|
+
timeoutMs: WECHAT_TYPING_TIMEOUT_MS,
|
|
1063
|
+
});
|
|
1064
|
+
const resp = JSON.parse(raw);
|
|
1065
|
+
if (resp.ret !== undefined && resp.ret !== 0) {
|
|
1066
|
+
this.logger.logError(`sendTyping ret=${resp.ret} errmsg=${resp.errmsg ?? "(none)"}`);
|
|
1067
|
+
return false;
|
|
1068
|
+
}
|
|
1069
|
+
return true;
|
|
1070
|
+
}
|
|
1071
|
+
catch (error) {
|
|
1072
|
+
this.logger.logError(`sendTyping failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1073
|
+
return false;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
966
1076
|
async sendText(senderId, text) {
|
|
967
1077
|
const trimmed = text.trim();
|
|
968
1078
|
if (!trimmed) {
|
|
@@ -1166,7 +1276,7 @@ export class WeChatTransport {
|
|
|
1166
1276
|
}
|
|
1167
1277
|
return stat;
|
|
1168
1278
|
}
|
|
1169
|
-
async getUpdates(account, timeoutMs) {
|
|
1279
|
+
async getUpdates(account, timeoutMs, signal) {
|
|
1170
1280
|
try {
|
|
1171
1281
|
const raw = await apiFetch({
|
|
1172
1282
|
baseUrl: account.baseUrl,
|
|
@@ -1177,6 +1287,7 @@ export class WeChatTransport {
|
|
|
1177
1287
|
}),
|
|
1178
1288
|
token: account.token,
|
|
1179
1289
|
timeoutMs,
|
|
1290
|
+
signal,
|
|
1180
1291
|
});
|
|
1181
1292
|
return JSON.parse(raw);
|
|
1182
1293
|
}
|