cli-wechat-bridge 1.1.6 → 1.1.8
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 +454 -475
- package/bin/_run-entry.mjs +23 -5
- package/bin/wecom-check-update.mjs +5 -0
- package/bin/wecom-claude.mjs +5 -0
- package/bin/wecom-codex.mjs +5 -0
- package/bin/wecom-daemon.mjs +5 -0
- package/bin/wecom-opencode.mjs +5 -0
- package/bin/wecom-pi.mjs +5 -0
- package/bin/wecom-setup.mjs +5 -0
- package/dist/bridge/adapter-control.js +58 -0
- package/dist/bridge/bridge-adapters.claude.js +155 -4
- package/dist/bridge/bridge-adapters.codex.js +112 -7
- package/dist/bridge/bridge-adapters.core.js +19 -1
- package/dist/bridge/bridge-adapters.opencode.js +150 -1
- package/dist/bridge/bridge-adapters.pi.js +39 -0
- package/dist/bridge/bridge-process-reaper.js +35 -2
- package/dist/bridge/bridge-state.js +10 -3
- package/dist/bridge/claude-native-controls.js +133 -0
- package/dist/bridge/native-terminal-control.js +253 -0
- package/dist/bridge/wechat-bridge.js +389 -87
- package/dist/channels/wechat/wechat-outbound-queue.js +10 -3
- package/dist/channels/wecom/bridge-final-reply.js +26 -0
- package/dist/channels/wecom/setup-cli.js +6 -0
- package/dist/channels/wecom/setup.js +176 -0
- package/dist/channels/wecom/wecom-channel-port.js +45 -0
- package/dist/channels/wecom/wecom-config.js +66 -0
- package/dist/channels/wecom/wecom-media.js +141 -0
- package/dist/channels/wecom/wecom-message.js +149 -0
- package/dist/channels/wecom/wecom-transport.js +308 -0
- package/dist/companion/codex-remote-client.js +24 -3
- package/dist/companion/codex-visible-client-link.js +101 -0
- package/dist/companion/local-companion-start.js +56 -11
- package/dist/companion/local-companion.js +19 -1
- package/dist/companion/pi-tui-bridge-extension.js +56 -3
- package/dist/core/bridge-control.js +8 -0
- package/dist/core/channel-types.js +3 -1
- package/dist/core/conversation-routing.js +48 -0
- package/dist/core/direct-run.js +17 -0
- package/dist/core/inbound-message-claims.js +61 -0
- package/dist/daemon/daemon-link.js +3 -0
- package/dist/daemon/wechat-daemon.js +335 -60
- package/dist/i18n/messages-en.js +3 -1
- package/dist/i18n/messages-zh.js +3 -1
- package/dist/runtime/legacy-adapter-runtime.js +15 -0
- package/dist/runtime/runtime-types.js +3 -0
- package/dist/utils/doctor.js +30 -10
- package/dist/utils/version-checker.js +11 -11
- package/dist/wechat/setup.js +113 -13
- package/dist/wechat/wechat-transport.js +96 -58
- package/package.json +116 -104
- package/scripts/ensure-node-pty-permissions.mjs +73 -73
package/dist/i18n/messages-en.js
CHANGED
|
@@ -11,7 +11,7 @@ export const messages = {
|
|
|
11
11
|
"bridge.fatalError": "Bridge error: {message}",
|
|
12
12
|
"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
13
|
// === Daemon welcome & control ===
|
|
14
|
-
"daemon.welcome": "WeChat Daemon ready.\nCWD: {cwd}\nActive: {adapter}\n\nCommands: /claude, /codex, /opencode, /pi, /stop, /confirm, /deny, /status\n{bindings}\n\nManage: /bind [emoji] cmd, /unbind [emoji], /bindings",
|
|
14
|
+
"daemon.welcome": "WeChat Daemon ready.\nCWD: {cwd}\nActive: {adapter}\n\nCommands: /claude, /codex, /opencode, /pi, /stop, /confirm, /deny, /status, /model, /plan\n{bindings}\n\nManage: /bind [emoji] cmd, /unbind [emoji], /bindings",
|
|
15
15
|
"daemon.noActiveAdapter": "No active terminal is selected. Send /codex, /claude, /opencode, or /pi to start one.",
|
|
16
16
|
"daemon.switchResult.new": "Started a new visible CLI.",
|
|
17
17
|
"daemon.switchResult.reused": "Reused the existing visible CLI.",
|
|
@@ -59,6 +59,7 @@ export const messages = {
|
|
|
59
59
|
"doctor.label.codePage": "Console code page",
|
|
60
60
|
"doctor.codePage.nonAsciiWarning": "{codePage} (not UTF-8 and the data directory path contains non-ASCII characters; hook scripts from older versions break in this environment — upgrade to the latest release)",
|
|
61
61
|
"doctor.label.connectivity": "WeChat connectivity",
|
|
62
|
+
"doctor.label.wecomConnectivity": "WeCom connectivity",
|
|
62
63
|
"doctor.connectivity.ok": "reachable ({baseUrl})",
|
|
63
64
|
"doctor.connectivity.unreachable": "cannot reach {baseUrl} (check network and firewall)",
|
|
64
65
|
"doctor.connectivity.proxyHint": "cannot reach {baseUrl}. Proxy {proxy} detected, but Node fetch ignores proxy environment variables by default; if your network requires the proxy, set NODE_USE_ENV_PROXY=1 and retry.",
|
|
@@ -82,6 +83,7 @@ export const messages = {
|
|
|
82
83
|
"doctor.dataDirMissing": "(will be created on first run)",
|
|
83
84
|
"doctor.credentialsFound": "found",
|
|
84
85
|
"doctor.credentialsMissing": "not found (run wechat-setup first)",
|
|
86
|
+
"doctor.credentialsMissingWecom": "not found (run wecom-setup first)",
|
|
85
87
|
"doctor.mode.bridge": "standalone bridge",
|
|
86
88
|
"doctor.mode.generic": "generic",
|
|
87
89
|
"doctor.notSpecified": "(not specified)",
|
package/dist/i18n/messages-zh.js
CHANGED
|
@@ -11,7 +11,7 @@ export const messages = {
|
|
|
11
11
|
"bridge.fatalError": "桥接错误: {message}",
|
|
12
12
|
"bridge.backlogIgnored": "已忽略 {count} 条桥接启动前的积压消息(不会自动处理)。\n如果你是在桥接启动后才发送的消息却看到此提示,说明本机系统时钟可能不准(偏差超过 {graceSeconds} 秒),请校准时间后重启桥接。",
|
|
13
13
|
// === Daemon 欢迎 & 控制 ===
|
|
14
|
-
"daemon.welcome": "微信 Daemon 就绪。\n工作目录: {cwd}\n当前: {adapter}\n\n命令: /claude, /codex, /opencode, /pi, /stop, /confirm, /deny, /status\n{bindings}\n\n管理: /bind [表情] 命令, /unbind [表情], /bindings",
|
|
14
|
+
"daemon.welcome": "微信 Daemon 就绪。\n工作目录: {cwd}\n当前: {adapter}\n\n命令: /claude, /codex, /opencode, /pi, /stop, /confirm, /deny, /status, /model, /plan\n{bindings}\n\n管理: /bind [表情] 命令, /unbind [表情], /bindings",
|
|
15
15
|
"daemon.noActiveAdapter": "未选择活跃终端。发送 /codex、/claude、/opencode 或 /pi 启动一个。",
|
|
16
16
|
"daemon.switchResult.new": "已启动新的可见 CLI。",
|
|
17
17
|
"daemon.switchResult.reused": "已复用现有的可见 CLI。",
|
|
@@ -59,6 +59,7 @@ export const messages = {
|
|
|
59
59
|
"doctor.label.codePage": "控制台代码页",
|
|
60
60
|
"doctor.codePage.nonAsciiWarning": "{codePage}(非 UTF-8,且数据目录路径含非 ASCII 字符;旧版本的 hook 脚本在此环境下会乱码失效,请升级到最新版本)",
|
|
61
61
|
"doctor.label.connectivity": "微信服务可达性",
|
|
62
|
+
"doctor.label.wecomConnectivity": "企业微信服务可达性",
|
|
62
63
|
"doctor.connectivity.ok": "可达 ({baseUrl})",
|
|
63
64
|
"doctor.connectivity.unreachable": "无法访问 {baseUrl}(检查网络与防火墙)",
|
|
64
65
|
"doctor.connectivity.proxyHint": "无法访问 {baseUrl}。检测到代理 {proxy},但 Node fetch 默认不使用代理环境变量;若必须经代理出网,请设置 NODE_USE_ENV_PROXY=1 后重试。",
|
|
@@ -82,6 +83,7 @@ export const messages = {
|
|
|
82
83
|
"doctor.dataDirMissing": "(首次运行会创建)",
|
|
83
84
|
"doctor.credentialsFound": "已找到",
|
|
84
85
|
"doctor.credentialsMissing": "未找到 (先运行 wechat-setup)",
|
|
86
|
+
"doctor.credentialsMissingWecom": "未找到 (先运行 wecom-setup)",
|
|
85
87
|
"doctor.mode.bridge": "独立桥接",
|
|
86
88
|
"doctor.mode.generic": "通用",
|
|
87
89
|
"doctor.notSpecified": "(未指定)",
|
|
@@ -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
|
+
}
|
package/dist/utils/doctor.js
CHANGED
|
@@ -74,11 +74,11 @@ function defaultGetWindowsCodePage() {
|
|
|
74
74
|
return null;
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
|
-
async function defaultFetchServerDate() {
|
|
77
|
+
async function defaultFetchServerDate(baseUrl = DEFAULT_BASE_URL) {
|
|
78
78
|
const controller = new AbortController();
|
|
79
79
|
const timer = setTimeout(() => controller.abort(), SERVER_DATE_TIMEOUT_MS);
|
|
80
80
|
try {
|
|
81
|
-
const res = await fetch(
|
|
81
|
+
const res = await fetch(baseUrl, {
|
|
82
82
|
method: "GET",
|
|
83
83
|
signal: controller.signal,
|
|
84
84
|
});
|
|
@@ -179,6 +179,7 @@ function isBridgeAdapterKind(value) {
|
|
|
179
179
|
export function parseDoctorCliArgs(argv, mode = "generic", fallbackCwd = process.cwd()) {
|
|
180
180
|
let cwd = fallbackCwd;
|
|
181
181
|
let adapter;
|
|
182
|
+
let channelId = "wechat";
|
|
182
183
|
for (let i = 0; i < argv.length; i += 1) {
|
|
183
184
|
const arg = argv[i];
|
|
184
185
|
const next = argv[i + 1];
|
|
@@ -195,11 +196,17 @@ export function parseDoctorCliArgs(argv, mode = "generic", fallbackCwd = process
|
|
|
195
196
|
i += 1;
|
|
196
197
|
continue;
|
|
197
198
|
}
|
|
199
|
+
if (arg === "--channel" && (next === "wechat" || next === "wecom")) {
|
|
200
|
+
channelId = next;
|
|
201
|
+
i += 1;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
198
204
|
}
|
|
199
205
|
return {
|
|
200
206
|
mode,
|
|
201
207
|
cwd: path.resolve(cwd),
|
|
202
208
|
adapter,
|
|
209
|
+
...(channelId === "wecom" ? { channelId } : {}),
|
|
203
210
|
};
|
|
204
211
|
}
|
|
205
212
|
function statusTag(status) {
|
|
@@ -582,10 +589,15 @@ export async function buildDoctorReport(options = {}, rawDeps = {}) {
|
|
|
582
589
|
}
|
|
583
590
|
}
|
|
584
591
|
const proxyUrl = pickProxyEnvValue(deps.env);
|
|
585
|
-
const
|
|
592
|
+
const connectivityUrl = doctorOptions.channelId === "wecom"
|
|
593
|
+
? "https://open.work.weixin.qq.com"
|
|
594
|
+
: DEFAULT_BASE_URL;
|
|
595
|
+
const serverDate = await deps.fetchServerDate(connectivityUrl);
|
|
586
596
|
if (serverDate) {
|
|
587
|
-
lines.push(row(STATE_OK, msg(
|
|
588
|
-
|
|
597
|
+
lines.push(row(STATE_OK, msg(doctorOptions.channelId === "wecom"
|
|
598
|
+
? "label.wecomConnectivity"
|
|
599
|
+
: "label.connectivity"), msg("connectivity.ok", {
|
|
600
|
+
baseUrl: connectivityUrl,
|
|
589
601
|
})));
|
|
590
602
|
const skewMs = deps.now() - serverDate.getTime();
|
|
591
603
|
const skewSeconds = Math.round(Math.abs(skewMs) / 1000);
|
|
@@ -595,9 +607,11 @@ export async function buildDoctorReport(options = {}, rawDeps = {}) {
|
|
|
595
607
|
: msg("clock.skewWarning", { skewSeconds })));
|
|
596
608
|
}
|
|
597
609
|
else {
|
|
598
|
-
lines.push(row(STATE_WARN, msg(
|
|
599
|
-
?
|
|
600
|
-
:
|
|
610
|
+
lines.push(row(STATE_WARN, msg(doctorOptions.channelId === "wecom"
|
|
611
|
+
? "label.wecomConnectivity"
|
|
612
|
+
: "label.connectivity"), proxyUrl && deps.env.NODE_USE_ENV_PROXY !== "1"
|
|
613
|
+
? msg("connectivity.proxyHint", { baseUrl: connectivityUrl, proxy: proxyUrl })
|
|
614
|
+
: msg("connectivity.unreachable", { baseUrl: connectivityUrl })));
|
|
601
615
|
}
|
|
602
616
|
try {
|
|
603
617
|
await deps.loadNodePty();
|
|
@@ -612,9 +626,15 @@ export async function buildDoctorReport(options = {}, rawDeps = {}) {
|
|
|
612
626
|
const dataDir = deps.resolveDataDir();
|
|
613
627
|
const dataDirExists = deps.exists(dataDir);
|
|
614
628
|
lines.push(row(dataDirExists ? STATE_OK : STATE_FAIL, msg("label.dataDir"), dataDirExists ? dataDir : `${dataDir} ${msg("dataDirMissing")}`));
|
|
615
|
-
const credFile =
|
|
629
|
+
const credFile = doctorOptions.channelId === "wecom"
|
|
630
|
+
? path.join(dataDir, "wecom", "account.json")
|
|
631
|
+
: path.join(dataDir, "account.json");
|
|
616
632
|
const credExists = deps.exists(credFile);
|
|
617
|
-
lines.push(row(credExists ? STATE_OK : STATE_FAIL, msg("label.credentials"), credExists
|
|
633
|
+
lines.push(row(credExists ? STATE_OK : STATE_FAIL, msg("label.credentials"), credExists
|
|
634
|
+
? msg("credentialsFound")
|
|
635
|
+
: msg(doctorOptions.channelId === "wecom"
|
|
636
|
+
? "credentialsMissingWecom"
|
|
637
|
+
: "credentialsMissing")));
|
|
618
638
|
section(lines, msg("section.runtime"));
|
|
619
639
|
lines.push(fieldRow(msg("label.mode"), formatMode(doctorOptions.mode)));
|
|
620
640
|
lines.push(fieldRow(msg("label.cwd"), doctorOptions.cwd));
|
|
@@ -179,16 +179,16 @@ export async function checkForUpdate(forceCheck = false) {
|
|
|
179
179
|
*/
|
|
180
180
|
export function formatUpdateMessage(versionInfo) {
|
|
181
181
|
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
|
|
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
|
|
193
193
|
`;
|
|
194
194
|
}
|
package/dist/wechat/setup.js
CHANGED
|
@@ -11,6 +11,7 @@ import fs from "node:fs";
|
|
|
11
11
|
import readline from "node:readline";
|
|
12
12
|
import { BOT_TYPE, CONTEXT_CACHE_FILE, CREDENTIALS_FILE, DEFAULT_BASE_URL, ensureChannelDataDir, migrateLegacyChannelFiles, SYNC_BUF_FILE, } from "./channel-config.js";
|
|
13
13
|
import { isWechatSyncSessionTimeout } from "./wechat-transport.js";
|
|
14
|
+
import { isDirectModuleRun } from "../core/direct-run.js";
|
|
14
15
|
const CHANNEL_VERSION = "0.3.0";
|
|
15
16
|
export function loadExistingCredentials() {
|
|
16
17
|
try {
|
|
@@ -130,18 +131,74 @@ async function askYesNo(prompt) {
|
|
|
130
131
|
rl.close();
|
|
131
132
|
}
|
|
132
133
|
}
|
|
133
|
-
|
|
134
|
+
// eslint-disable-next-line no-control-regex
|
|
135
|
+
const ANSI_SGR_RE = /\u001b\[[0-9;]*m/g;
|
|
136
|
+
function resolveLoginTerminal() {
|
|
137
|
+
const columns = process.stdout.columns;
|
|
138
|
+
return {
|
|
139
|
+
platform: process.platform,
|
|
140
|
+
isTTY: process.stdout.isTTY === true,
|
|
141
|
+
columns: typeof columns === "number" && Number.isFinite(columns) && columns > 0
|
|
142
|
+
? columns
|
|
143
|
+
: undefined,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
export function getQRCodeVisibleWidth(qr) {
|
|
147
|
+
return Math.max(0, ...qr
|
|
148
|
+
.replace(/\r/g, "")
|
|
149
|
+
.split("\n")
|
|
150
|
+
.map((line) => Array.from(line.replace(ANSI_SGR_RE, "")).length));
|
|
151
|
+
}
|
|
152
|
+
export function selectQRCodePresentation(params) {
|
|
153
|
+
if (!params.isTTY ||
|
|
154
|
+
typeof params.columns !== "number" ||
|
|
155
|
+
!Number.isFinite(params.columns) ||
|
|
156
|
+
params.columns <= 0 ||
|
|
157
|
+
params.renderedWidth + 1 > params.columns) {
|
|
158
|
+
return "url_only";
|
|
159
|
+
}
|
|
160
|
+
return "qr_with_url";
|
|
161
|
+
}
|
|
162
|
+
export function formatQRCodeLoginInstruction(presentation) {
|
|
163
|
+
return presentation === "qr_with_url"
|
|
164
|
+
? "Scan the QR code above with WeChat, then confirm the login on your phone. If scanning fails, open the browser URL above.\n"
|
|
165
|
+
: "The QR code was not rendered in this terminal. Open the browser URL above, then confirm the login in WeChat.\n";
|
|
166
|
+
}
|
|
167
|
+
async function generateQRCode(qrContent, options) {
|
|
168
|
+
const qrterm = await import("qrcode-terminal");
|
|
169
|
+
return await new Promise((resolve, reject) => {
|
|
170
|
+
try {
|
|
171
|
+
qrterm.default.generate(qrContent, options, resolve);
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
reject(error);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
export async function printQRCode(qrContent, write, terminal = resolveLoginTerminal(), qrMode = "small", qrGenerator = generateQRCode) {
|
|
179
|
+
if (!terminal.isTTY) {
|
|
180
|
+
return "url_only";
|
|
181
|
+
}
|
|
134
182
|
try {
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
qrterm.default.generate(qrContent, { small: true }, (qr) => {
|
|
138
|
-
write(`${qr}\n`);
|
|
139
|
-
resolve();
|
|
140
|
-
});
|
|
183
|
+
const qr = await qrGenerator(qrContent, {
|
|
184
|
+
small: qrMode === "small",
|
|
141
185
|
});
|
|
186
|
+
const presentation = selectQRCodePresentation({
|
|
187
|
+
isTTY: terminal.isTTY,
|
|
188
|
+
columns: terminal.columns,
|
|
189
|
+
renderedWidth: getQRCodeVisibleWidth(qr),
|
|
190
|
+
});
|
|
191
|
+
if (presentation === "qr_with_url") {
|
|
192
|
+
write(`${qr}\n`);
|
|
193
|
+
}
|
|
194
|
+
write(presentation === "qr_with_url"
|
|
195
|
+
? `If scanning fails, open this short-lived WeChat login URL in a browser. Do not share it:\n${qrContent}\n\n`
|
|
196
|
+
: `Open this short-lived WeChat login URL in a browser. Do not share it:\n${qrContent}\n\n`);
|
|
197
|
+
return presentation;
|
|
142
198
|
}
|
|
143
199
|
catch {
|
|
144
|
-
write(`Open this
|
|
200
|
+
write(`The QR code could not be rendered. Open this short-lived WeChat login URL in a browser. Do not share it:\n${qrContent}\n\n`);
|
|
201
|
+
return "url_only";
|
|
145
202
|
}
|
|
146
203
|
}
|
|
147
204
|
function saveCredentials(account) {
|
|
@@ -174,16 +231,54 @@ function printPostLoginHelp(log) {
|
|
|
174
231
|
log("");
|
|
175
232
|
log("Run wechat-setup again any time you need to refresh the login.");
|
|
176
233
|
}
|
|
234
|
+
export function parseWechatSetupCliArgs(argv) {
|
|
235
|
+
let qrMode = "small";
|
|
236
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
237
|
+
const arg = argv[index];
|
|
238
|
+
if (arg === "--help" || arg === "-h") {
|
|
239
|
+
return { help: true, qrMode };
|
|
240
|
+
}
|
|
241
|
+
if (arg === "--qr-mode") {
|
|
242
|
+
const value = argv[index + 1];
|
|
243
|
+
if (value !== "small" && value !== "normal") {
|
|
244
|
+
throw new Error("--qr-mode requires small or normal.");
|
|
245
|
+
}
|
|
246
|
+
qrMode = value;
|
|
247
|
+
index += 1;
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
if (arg === "--qr-mode=small" || arg === "--qr-mode=normal") {
|
|
251
|
+
qrMode = arg.endsWith("=small") ? "small" : "normal";
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
throw new Error(`Unknown wechat-setup option: ${arg}`);
|
|
255
|
+
}
|
|
256
|
+
return { help: false, qrMode };
|
|
257
|
+
}
|
|
258
|
+
export function formatWechatSetupUsage() {
|
|
259
|
+
return [
|
|
260
|
+
"Usage: wechat-setup [--qr-mode <small|normal>]",
|
|
261
|
+
"",
|
|
262
|
+
"Log in to WeChat and save bridge credentials.",
|
|
263
|
+
"QR mode defaults to small; use --qr-mode normal when the terminal renders small QR blocks incorrectly.",
|
|
264
|
+
"",
|
|
265
|
+
].join("\n");
|
|
266
|
+
}
|
|
177
267
|
export async function runWechatLogin(options = {}) {
|
|
178
268
|
const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
|
|
179
269
|
const log = options.log ?? ((message) => console.log(message));
|
|
180
270
|
const write = options.write ?? ((message) => process.stdout.write(message));
|
|
181
271
|
const timeoutMs = options.timeoutMs ?? 480_000;
|
|
182
272
|
const pollIntervalMs = options.pollIntervalMs ?? 1_000;
|
|
273
|
+
const terminal = options.terminal ?? resolveLoginTerminal();
|
|
274
|
+
const qrMode = options.qrMode ?? "small";
|
|
275
|
+
if (!terminal.isTTY) {
|
|
276
|
+
throw new Error("WeChat login requires an interactive terminal. Run wechat-setup directly in PowerShell, Command Prompt, or a terminal window.");
|
|
277
|
+
}
|
|
183
278
|
log("Fetching WeChat login QR code...\n");
|
|
184
279
|
const qrResp = await fetchQRCode(baseUrl);
|
|
185
|
-
await printQRCode(qrResp.qrcode_img_content, write);
|
|
186
|
-
log(
|
|
280
|
+
const presentation = await printQRCode(qrResp.qrcode_img_content, write, terminal, qrMode);
|
|
281
|
+
log(formatQRCodeLoginInstruction(presentation));
|
|
187
282
|
const deadline = Date.now() + timeoutMs;
|
|
188
283
|
let scannedPrinted = false;
|
|
189
284
|
while (Date.now() < deadline) {
|
|
@@ -252,7 +347,12 @@ export async function ensureWechatCredentials(options = {}) {
|
|
|
252
347
|
const login = options.login ?? (() => runWechatLogin(options));
|
|
253
348
|
return login();
|
|
254
349
|
}
|
|
255
|
-
async function main() {
|
|
350
|
+
async function main(argv = process.argv.slice(2)) {
|
|
351
|
+
const cliOptions = parseWechatSetupCliArgs(argv);
|
|
352
|
+
if (cliOptions.help) {
|
|
353
|
+
console.log(formatWechatSetupUsage());
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
256
356
|
migrateLegacyChannelFiles((message) => console.log(message));
|
|
257
357
|
const existing = loadExistingCredentials();
|
|
258
358
|
if (existing) {
|
|
@@ -266,10 +366,10 @@ async function main() {
|
|
|
266
366
|
return;
|
|
267
367
|
}
|
|
268
368
|
}
|
|
269
|
-
await runWechatLogin({ requireUserId: true });
|
|
369
|
+
await runWechatLogin({ requireUserId: true, qrMode: cliOptions.qrMode });
|
|
270
370
|
printPostLoginHelp((message) => console.log(message));
|
|
271
371
|
}
|
|
272
|
-
const isDirectRun =
|
|
372
|
+
const isDirectRun = isDirectModuleRun(import.meta.url, process.argv, import.meta.main);
|
|
273
373
|
if (isDirectRun) {
|
|
274
374
|
main().catch((err) => {
|
|
275
375
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -332,6 +332,15 @@ async function apiFetch(params) {
|
|
|
332
332
|
const url = new URL(params.endpoint, base).toString();
|
|
333
333
|
const controller = new AbortController();
|
|
334
334
|
const timer = setTimeout(() => controller.abort(), params.timeoutMs);
|
|
335
|
+
const abortFromCaller = () => controller.abort(params.signal?.reason);
|
|
336
|
+
if (params.signal) {
|
|
337
|
+
if (params.signal.aborted) {
|
|
338
|
+
abortFromCaller();
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
params.signal.addEventListener("abort", abortFromCaller, { once: true });
|
|
342
|
+
}
|
|
343
|
+
}
|
|
335
344
|
try {
|
|
336
345
|
const res = await fetch(url, {
|
|
337
346
|
method: "POST",
|
|
@@ -353,6 +362,9 @@ async function apiFetch(params) {
|
|
|
353
362
|
clearTimeout(timer);
|
|
354
363
|
throw err;
|
|
355
364
|
}
|
|
365
|
+
finally {
|
|
366
|
+
params.signal?.removeEventListener("abort", abortFromCaller);
|
|
367
|
+
}
|
|
356
368
|
}
|
|
357
369
|
function encryptAesEcb(plaintext, key) {
|
|
358
370
|
const cipher = createCipheriv("aes-128-ecb", key, null);
|
|
@@ -812,6 +824,8 @@ export class WeChatTransport {
|
|
|
812
824
|
recentMessageOrder = [];
|
|
813
825
|
contextTokenCache;
|
|
814
826
|
syncBuffer = "";
|
|
827
|
+
stopped = false;
|
|
828
|
+
activePollControllers = new Set();
|
|
815
829
|
constructor(logger) {
|
|
816
830
|
this.logger = logger;
|
|
817
831
|
migrateLegacyChannelFiles((message) => this.logger.log(message));
|
|
@@ -855,73 +869,96 @@ export class WeChatTransport {
|
|
|
855
869
|
? "Reset sync state and cleared cached context tokens."
|
|
856
870
|
: "Reset sync state.";
|
|
857
871
|
}
|
|
872
|
+
stop() {
|
|
873
|
+
if (this.stopped) {
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
this.stopped = true;
|
|
877
|
+
for (const controller of this.activePollControllers) {
|
|
878
|
+
controller.abort();
|
|
879
|
+
}
|
|
880
|
+
this.activePollControllers.clear();
|
|
881
|
+
}
|
|
858
882
|
async pollMessages(options = {}) {
|
|
883
|
+
if (this.stopped) {
|
|
884
|
+
return { messages: [], ignoredBacklogCount: 0 };
|
|
885
|
+
}
|
|
859
886
|
const timeoutMs = options.timeoutMs ?? DEFAULT_LONG_POLL_TIMEOUT_MS;
|
|
860
887
|
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;
|
|
888
|
+
const controller = new AbortController();
|
|
889
|
+
this.activePollControllers.add(controller);
|
|
890
|
+
try {
|
|
891
|
+
let response = await this.getUpdates(account, timeoutMs, controller.signal);
|
|
892
|
+
if (isWechatSyncSessionTimeout(response) && this.syncBuffer && !this.stopped) {
|
|
893
|
+
this.logger.log("WeChat sync session timed out. Clearing local sync cursor and retrying once.");
|
|
894
|
+
this.clearSyncBuffer();
|
|
895
|
+
response = await this.getUpdates(account, timeoutMs, controller.signal);
|
|
884
896
|
}
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
continue;
|
|
897
|
+
if (this.stopped) {
|
|
898
|
+
return { messages: [], ignoredBacklogCount: 0 };
|
|
888
899
|
}
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
this.cacheContextToken(senderId, rawMessage.context_token);
|
|
900
|
+
if (isWechatSyncSessionTimeout(response)) {
|
|
901
|
+
throw new Error('WeChat session timed out. Run "wechat-setup" to log in again.');
|
|
892
902
|
}
|
|
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;
|
|
903
|
+
const isError = (response.ret !== undefined && response.ret !== 0) ||
|
|
904
|
+
(response.errcode !== undefined && response.errcode !== 0);
|
|
905
|
+
if (isError) {
|
|
906
|
+
throw new Error(`getUpdates failed: ret=${response.ret} errcode=${response.errcode} errmsg=${response.errmsg ?? ""}`);
|
|
903
907
|
}
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
908
|
+
if (response.get_updates_buf) {
|
|
909
|
+
this.syncBuffer = response.get_updates_buf;
|
|
910
|
+
this.saveSyncBuffer(this.syncBuffer);
|
|
907
911
|
}
|
|
908
|
-
|
|
909
|
-
|
|
912
|
+
const messages = [];
|
|
913
|
+
let ignoredBacklogCount = 0;
|
|
914
|
+
for (const rawMessage of response.msgs ?? []) {
|
|
915
|
+
if (rawMessage.message_type !== MSG_TYPE_USER) {
|
|
916
|
+
continue;
|
|
917
|
+
}
|
|
918
|
+
const extracted = extractInboundMessageContent(rawMessage);
|
|
919
|
+
if (!extracted.text && extracted.attachments.length === 0) {
|
|
920
|
+
continue;
|
|
921
|
+
}
|
|
922
|
+
const senderId = rawMessage.from_user_id ?? "unknown";
|
|
923
|
+
if (rawMessage.context_token) {
|
|
924
|
+
this.cacheContextToken(senderId, rawMessage.context_token);
|
|
925
|
+
}
|
|
926
|
+
// Filter pre-start backlog BEFORE claiming/remembering the message so a
|
|
927
|
+
// skipped message is not permanently locked away from other processes.
|
|
928
|
+
// A missing create_time_ms is treated as fresh rather than dropped.
|
|
929
|
+
const createdAtMs = rawMessage.create_time_ms ?? 0;
|
|
930
|
+
if (typeof options.minCreatedAtMs === "number" &&
|
|
931
|
+
Number.isFinite(createdAtMs) &&
|
|
932
|
+
createdAtMs > 0 &&
|
|
933
|
+
createdAtMs < options.minCreatedAtMs) {
|
|
934
|
+
ignoredBacklogCount += 1;
|
|
935
|
+
continue;
|
|
936
|
+
}
|
|
937
|
+
const messageKey = buildMessageKey(rawMessage);
|
|
938
|
+
if (!this.rememberMessage(messageKey)) {
|
|
939
|
+
continue;
|
|
940
|
+
}
|
|
941
|
+
if (!tryClaimInboundMessage(buildScopedMessageClaimKey(account.accountId, messageKey))) {
|
|
942
|
+
continue;
|
|
943
|
+
}
|
|
944
|
+
const { attachments, failureLines } = await this.downloadInboundAttachments(extracted.attachments, rawMessage);
|
|
945
|
+
const text = appendInboundAttachmentFailureText(extracted.text, failureLines);
|
|
946
|
+
messages.push({
|
|
947
|
+
senderId,
|
|
948
|
+
sender: normalizeSender(senderId),
|
|
949
|
+
sessionId: rawMessage.session_id ?? "",
|
|
950
|
+
text,
|
|
951
|
+
attachments,
|
|
952
|
+
contextToken: rawMessage.context_token,
|
|
953
|
+
createdAt: formatTimestamp(rawMessage.create_time_ms),
|
|
954
|
+
createdAtMs,
|
|
955
|
+
});
|
|
910
956
|
}
|
|
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
|
-
});
|
|
957
|
+
return { messages, ignoredBacklogCount };
|
|
958
|
+
}
|
|
959
|
+
finally {
|
|
960
|
+
this.activePollControllers.delete(controller);
|
|
923
961
|
}
|
|
924
|
-
return { messages, ignoredBacklogCount };
|
|
925
962
|
}
|
|
926
963
|
async downloadInboundAttachments(descriptors, rawMessage) {
|
|
927
964
|
const attachments = [];
|
|
@@ -1166,7 +1203,7 @@ export class WeChatTransport {
|
|
|
1166
1203
|
}
|
|
1167
1204
|
return stat;
|
|
1168
1205
|
}
|
|
1169
|
-
async getUpdates(account, timeoutMs) {
|
|
1206
|
+
async getUpdates(account, timeoutMs, signal) {
|
|
1170
1207
|
try {
|
|
1171
1208
|
const raw = await apiFetch({
|
|
1172
1209
|
baseUrl: account.baseUrl,
|
|
@@ -1177,6 +1214,7 @@ export class WeChatTransport {
|
|
|
1177
1214
|
}),
|
|
1178
1215
|
token: account.token,
|
|
1179
1216
|
timeoutMs,
|
|
1217
|
+
signal,
|
|
1180
1218
|
});
|
|
1181
1219
|
return JSON.parse(raw);
|
|
1182
1220
|
}
|