@unlinearity/cli-wechat-bridge 1.0.1 → 1.0.2
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.
|
@@ -4,7 +4,7 @@ import { resolveDefaultAdapterCommand, } from "./bridge-adapters.js";
|
|
|
4
4
|
import { delay } from "./bridge-adapters.shared.js";
|
|
5
5
|
import { BridgeController } from "./bridge-controller.js";
|
|
6
6
|
import { forwardWechatFinalReply } from "./bridge-final-reply.js";
|
|
7
|
-
import {
|
|
7
|
+
import { ensureWechatCredentials } from "../wechat/setup.js";
|
|
8
8
|
import { BridgeStateStore } from "./bridge-state.js";
|
|
9
9
|
import { reapOrphanedOpencodeProcesses, reapPeerBridgeProcesses } from "./bridge-process-reaper.js";
|
|
10
10
|
import { createRuntimeHost } from "../runtime/create-runtime-host.js";
|
|
@@ -196,8 +196,17 @@ function printUsageAndExit() {
|
|
|
196
196
|
process.exit(0);
|
|
197
197
|
}
|
|
198
198
|
async function main() {
|
|
199
|
-
|
|
200
|
-
|
|
199
|
+
const options = parseCliArgs(process.argv.slice(2));
|
|
200
|
+
const credentials = await ensureWechatCredentials({
|
|
201
|
+
requireUserId: true,
|
|
202
|
+
validateExisting: true,
|
|
203
|
+
log,
|
|
204
|
+
});
|
|
205
|
+
if (!credentials.userId) {
|
|
206
|
+
throw new Error("Saved WeChat credentials are missing userId.");
|
|
207
|
+
}
|
|
208
|
+
const transport = new WeChatTransport({ log, logError });
|
|
209
|
+
// 非阻塞地检查更新(不影响启动速度,也避免首次登录时打断二维码输出)
|
|
201
210
|
setTimeout(async () => {
|
|
202
211
|
try {
|
|
203
212
|
const versionInfo = await checkForUpdate();
|
|
@@ -209,15 +218,6 @@ async function main() {
|
|
|
209
218
|
// 静默失败,不影响正常使用
|
|
210
219
|
}
|
|
211
220
|
}, 3000); // 延迟3秒,确保不影响启动
|
|
212
|
-
const options = parseCliArgs(process.argv.slice(2));
|
|
213
|
-
const transport = new WeChatTransport({ log, logError });
|
|
214
|
-
const credentials = transport.getCredentials();
|
|
215
|
-
if (!credentials) {
|
|
216
|
-
throw new Error('No saved WeChat credentials found. Run "bun run setup" first.');
|
|
217
|
-
}
|
|
218
|
-
if (!credentials.userId) {
|
|
219
|
-
throw new Error('Saved WeChat credentials are missing userId. Run "bun run setup" again.');
|
|
220
|
-
}
|
|
221
221
|
const stateStore = new BridgeStateStore({
|
|
222
222
|
...options,
|
|
223
223
|
authorizedUserId: credentials.userId,
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import fs from "node:fs";
|
|
3
2
|
import net from "node:net";
|
|
4
3
|
import path from "node:path";
|
|
5
4
|
import { spawn } from "node:child_process";
|
|
6
5
|
import { fileURLToPath } from "node:url";
|
|
7
|
-
import { BRIDGE_LOG_FILE
|
|
6
|
+
import { BRIDGE_LOG_FILE } from "../wechat/channel-config.js";
|
|
7
|
+
import { ensureWechatCredentials } from "../wechat/setup.js";
|
|
8
8
|
import { readBridgeLockFile, shouldAutoReclaimBridgeLock, } from "../bridge/bridge-state.js";
|
|
9
9
|
import { clearLocalCompanionOccupancy, clearLocalCompanionEndpoint, readLocalCompanionEndpoint, } from "./local-companion-link.js";
|
|
10
10
|
import { runCodexRemoteClient } from "./codex-remote-client.js";
|
|
@@ -54,9 +54,13 @@ export function decideLaunchAction(input) {
|
|
|
54
54
|
failureMessage: formatSwitchFailureMessage(input.runningLock.cwd),
|
|
55
55
|
};
|
|
56
56
|
}
|
|
57
|
-
if (input.endpoint
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
if (!input.endpoint || !input.endpointIsReachable) {
|
|
58
|
+
return {
|
|
59
|
+
kind: "restart_unhealthy",
|
|
60
|
+
message: formatRestartUnhealthyMessage(input.requestedCwd),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
if (input.companionIsAlive &&
|
|
60
64
|
(input.endpoint.companionStatus === "stopped" ||
|
|
61
65
|
input.endpoint.companionStatus === "error")) {
|
|
62
66
|
return {
|
|
@@ -328,12 +332,16 @@ export async function runVisibleClient(options, runners = {}) {
|
|
|
328
332
|
cliArgs: options.cliArgs,
|
|
329
333
|
});
|
|
330
334
|
}
|
|
335
|
+
export async function ensureCompanionStartWechatCredentials(adapter, ensureCredentials = ensureWechatCredentials) {
|
|
336
|
+
await ensureCredentials({
|
|
337
|
+
requireUserId: true,
|
|
338
|
+
validateExisting: true,
|
|
339
|
+
log: (message) => log(adapter, message),
|
|
340
|
+
});
|
|
341
|
+
}
|
|
331
342
|
export async function runLocalCompanionStart(argv = process.argv.slice(2)) {
|
|
332
343
|
const options = parseCliArgs(argv);
|
|
333
|
-
|
|
334
|
-
if (!fs.existsSync(CREDENTIALS_FILE)) {
|
|
335
|
-
throw new Error(`Missing WeChat credentials. Run "bun run setup" first. (${CREDENTIALS_FILE})`);
|
|
336
|
-
}
|
|
344
|
+
await ensureCompanionStartWechatCredentials(options.adapter);
|
|
337
345
|
const ready = await ensureBridgeReady(options);
|
|
338
346
|
if (!ready.shouldOpenVisibleClient) {
|
|
339
347
|
return 0;
|
|
@@ -64,22 +64,38 @@ export function ensureWorkspaceChannelDir(cwd) {
|
|
|
64
64
|
fs.mkdirSync(paths.workspaceDir, { recursive: true });
|
|
65
65
|
return paths;
|
|
66
66
|
}
|
|
67
|
+
function channelDataDirHasState() {
|
|
68
|
+
try {
|
|
69
|
+
if (!fs.existsSync(CHANNEL_DATA_DIR)) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
return fs.readdirSync(CHANNEL_DATA_DIR).length > 0;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export function shouldMigrateLegacyCredentials(params) {
|
|
79
|
+
return (params.legacyCredentialsExists &&
|
|
80
|
+
!params.credentialsExists &&
|
|
81
|
+
!params.channelDataDirHadState);
|
|
82
|
+
}
|
|
67
83
|
export function migrateLegacyChannelFiles(log) {
|
|
68
84
|
const migrated = [];
|
|
85
|
+
const channelDataDirHadState = channelDataDirHasState();
|
|
69
86
|
if (!fs.existsSync(LEGACY_CREDENTIALS_FILE) &&
|
|
70
87
|
!fs.existsSync(LEGACY_SYNC_BUF_FILE)) {
|
|
71
88
|
return migrated;
|
|
72
89
|
}
|
|
73
90
|
ensureChannelDataDir();
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
copyIfNeeded(LEGACY_SYNC_BUF_FILE, SYNC_BUF_FILE, "sync state");
|
|
91
|
+
if (shouldMigrateLegacyCredentials({
|
|
92
|
+
channelDataDirHadState,
|
|
93
|
+
credentialsExists: fs.existsSync(CREDENTIALS_FILE),
|
|
94
|
+
legacyCredentialsExists: fs.existsSync(LEGACY_CREDENTIALS_FILE),
|
|
95
|
+
})) {
|
|
96
|
+
fs.copyFileSync(LEGACY_CREDENTIALS_FILE, CREDENTIALS_FILE);
|
|
97
|
+
migrated.push("credentials");
|
|
98
|
+
}
|
|
83
99
|
if (migrated.length && log) {
|
|
84
100
|
log(`Migrated legacy ${migrated.join(" and ")} from ${LEGACY_CHANNEL_DATA_DIR} to ${CHANNEL_DATA_DIR}.`);
|
|
85
101
|
}
|
package/dist/wechat/setup.js
CHANGED
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* CLI WeChat Bridge setup.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Installed bridge commands run this automatically on first use.
|
|
6
|
+
* To force a relogin manually:
|
|
7
|
+
* wechat-setup
|
|
7
8
|
*/
|
|
9
|
+
import crypto from "node:crypto";
|
|
8
10
|
import fs from "node:fs";
|
|
9
11
|
import readline from "node:readline";
|
|
10
|
-
import { BOT_TYPE, CREDENTIALS_FILE, DEFAULT_BASE_URL, ensureChannelDataDir, migrateLegacyChannelFiles, } from "./channel-config.js";
|
|
11
|
-
|
|
12
|
+
import { BOT_TYPE, CONTEXT_CACHE_FILE, CREDENTIALS_FILE, DEFAULT_BASE_URL, ensureChannelDataDir, migrateLegacyChannelFiles, SYNC_BUF_FILE, } from "./channel-config.js";
|
|
13
|
+
import { isWechatSyncSessionTimeout } from "./wechat-transport.js";
|
|
14
|
+
const CHANNEL_VERSION = "0.3.0";
|
|
15
|
+
export function loadExistingCredentials() {
|
|
12
16
|
try {
|
|
13
17
|
if (!fs.existsSync(CREDENTIALS_FILE)) {
|
|
14
18
|
return null;
|
|
@@ -19,6 +23,65 @@ function loadExistingCredentials() {
|
|
|
19
23
|
return null;
|
|
20
24
|
}
|
|
21
25
|
}
|
|
26
|
+
export function getWechatLoginRequiredReason(account, options = {}) {
|
|
27
|
+
if (!account) {
|
|
28
|
+
return "No saved WeChat credentials found.";
|
|
29
|
+
}
|
|
30
|
+
if (options.requireUserId && !account.userId) {
|
|
31
|
+
return "Saved WeChat credentials are missing userId.";
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
function randomWechatUin() {
|
|
36
|
+
const uint32 = crypto.randomBytes(4).readUInt32BE(0);
|
|
37
|
+
return Buffer.from(String(uint32), "utf-8").toString("base64");
|
|
38
|
+
}
|
|
39
|
+
export async function getStoredCredentialsInvalidReason(account, options = {}) {
|
|
40
|
+
const baseUrl = account.baseUrl || DEFAULT_BASE_URL;
|
|
41
|
+
const base = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
42
|
+
const url = `${base}ilink/bot/getupdates`;
|
|
43
|
+
const body = JSON.stringify({
|
|
44
|
+
get_updates_buf: "",
|
|
45
|
+
base_info: { channel_version: CHANNEL_VERSION },
|
|
46
|
+
});
|
|
47
|
+
const controller = new AbortController();
|
|
48
|
+
const timer = setTimeout(() => controller.abort(), options.timeoutMs ?? 5_000);
|
|
49
|
+
try {
|
|
50
|
+
const res = await fetch(url, {
|
|
51
|
+
method: "POST",
|
|
52
|
+
headers: {
|
|
53
|
+
"Content-Type": "application/json",
|
|
54
|
+
"Content-Length": String(Buffer.byteLength(body, "utf-8")),
|
|
55
|
+
AuthorizationType: "ilink_bot_token",
|
|
56
|
+
Authorization: `Bearer ${account.token}`,
|
|
57
|
+
"X-WECHAT-UIN": randomWechatUin(),
|
|
58
|
+
},
|
|
59
|
+
body,
|
|
60
|
+
signal: controller.signal,
|
|
61
|
+
});
|
|
62
|
+
const text = await res.text();
|
|
63
|
+
if (res.status === 401 || res.status === 403) {
|
|
64
|
+
return "Saved WeChat credentials were rejected by the server.";
|
|
65
|
+
}
|
|
66
|
+
if (!res.ok) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
const response = JSON.parse(text);
|
|
70
|
+
if (isWechatSyncSessionTimeout(response)) {
|
|
71
|
+
return "Saved WeChat login has expired.";
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
clearTimeout(timer);
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
22
85
|
async function fetchQRCode(baseUrl) {
|
|
23
86
|
const base = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
24
87
|
const url = `${base}ilink/bot/get_bot_qrcode?bot_type=${BOT_TYPE}`;
|
|
@@ -67,118 +130,154 @@ async function askYesNo(prompt) {
|
|
|
67
130
|
rl.close();
|
|
68
131
|
}
|
|
69
132
|
}
|
|
70
|
-
async function printQRCode(qrContent) {
|
|
133
|
+
async function printQRCode(qrContent, write) {
|
|
71
134
|
try {
|
|
72
135
|
const qrterm = await import("qrcode-terminal");
|
|
73
136
|
await new Promise((resolve) => {
|
|
74
137
|
qrterm.default.generate(qrContent, { small: true }, (qr) => {
|
|
75
|
-
|
|
138
|
+
write(`${qr}\n`);
|
|
76
139
|
resolve();
|
|
77
140
|
});
|
|
78
141
|
});
|
|
79
142
|
}
|
|
80
143
|
catch {
|
|
81
|
-
|
|
144
|
+
write(`Open this QR code URL in a browser: ${qrContent}\n\n`);
|
|
82
145
|
}
|
|
83
146
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (!shouldRelogin) {
|
|
94
|
-
console.log("Keeping existing credentials.");
|
|
95
|
-
return;
|
|
147
|
+
function saveCredentials(account) {
|
|
148
|
+
ensureChannelDataDir();
|
|
149
|
+
fs.writeFileSync(CREDENTIALS_FILE, JSON.stringify(account, null, 2), "utf-8");
|
|
150
|
+
for (const staleStateFile of [SYNC_BUF_FILE, CONTEXT_CACHE_FILE]) {
|
|
151
|
+
try {
|
|
152
|
+
fs.rmSync(staleStateFile, { force: true });
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
// Best effort cleanup.
|
|
96
156
|
}
|
|
97
157
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
158
|
+
try {
|
|
159
|
+
fs.chmodSync(CREDENTIALS_FILE, 0o600);
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
// Best effort on Windows.
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function printPostLoginHelp(log) {
|
|
166
|
+
log("This WeChat account is now authorized for the bridge.");
|
|
167
|
+
log("");
|
|
168
|
+
log("Start from any project directory with one of:");
|
|
169
|
+
log(" wechat-codex-start");
|
|
170
|
+
log(" wechat-claude-start");
|
|
171
|
+
log(" wechat-opencode-start");
|
|
172
|
+
log(" wechat-bridge-shell");
|
|
173
|
+
log("");
|
|
174
|
+
log("Manual two-terminal mode is also available:");
|
|
175
|
+
log(" wechat-bridge-codex + wechat-codex");
|
|
176
|
+
log(" wechat-bridge-claude + wechat-claude");
|
|
177
|
+
log(" wechat-bridge-opencode + wechat-opencode");
|
|
178
|
+
log("");
|
|
179
|
+
log("Run wechat-setup again any time you need to refresh the login.");
|
|
180
|
+
}
|
|
181
|
+
export async function runWechatLogin(options = {}) {
|
|
182
|
+
const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
|
|
183
|
+
const log = options.log ?? ((message) => console.log(message));
|
|
184
|
+
const write = options.write ?? ((message) => process.stdout.write(message));
|
|
185
|
+
const timeoutMs = options.timeoutMs ?? 480_000;
|
|
186
|
+
const pollIntervalMs = options.pollIntervalMs ?? 1_000;
|
|
187
|
+
log("Fetching WeChat login QR code...\n");
|
|
188
|
+
const qrResp = await fetchQRCode(baseUrl);
|
|
189
|
+
await printQRCode(qrResp.qrcode_img_content, write);
|
|
190
|
+
log("Scan the QR code above with WeChat, then confirm the login on your phone.\n");
|
|
191
|
+
const deadline = Date.now() + timeoutMs;
|
|
103
192
|
let scannedPrinted = false;
|
|
104
193
|
while (Date.now() < deadline) {
|
|
105
|
-
const status = await pollQRStatus(
|
|
194
|
+
const status = await pollQRStatus(baseUrl, qrResp.qrcode);
|
|
106
195
|
switch (status.status) {
|
|
107
196
|
case "wait":
|
|
108
|
-
|
|
197
|
+
write(".");
|
|
109
198
|
break;
|
|
110
199
|
case "scaned":
|
|
111
200
|
if (!scannedPrinted) {
|
|
112
|
-
|
|
201
|
+
log("\nQR code scanned. Confirm the login in WeChat...");
|
|
113
202
|
scannedPrinted = true;
|
|
114
203
|
}
|
|
115
204
|
break;
|
|
116
205
|
case "expired":
|
|
117
|
-
|
|
118
|
-
process.exit(1);
|
|
119
|
-
break;
|
|
206
|
+
throw new Error("The QR code expired. Run setup again.");
|
|
120
207
|
case "confirmed": {
|
|
121
208
|
if (!status.ilink_bot_id || !status.bot_token) {
|
|
122
|
-
|
|
123
|
-
|
|
209
|
+
throw new Error("Login failed: missing bot credentials from server.");
|
|
210
|
+
}
|
|
211
|
+
if (options.requireUserId && !status.ilink_user_id) {
|
|
212
|
+
throw new Error("Login failed: missing WeChat userId from server.");
|
|
124
213
|
}
|
|
125
214
|
const account = {
|
|
126
215
|
token: status.bot_token,
|
|
127
|
-
baseUrl: status.baseurl ||
|
|
216
|
+
baseUrl: status.baseurl || baseUrl,
|
|
128
217
|
accountId: status.ilink_bot_id,
|
|
129
218
|
userId: status.ilink_user_id,
|
|
130
219
|
savedAt: new Date().toISOString(),
|
|
131
220
|
};
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
console.log(" bun run codex:panel");
|
|
163
|
-
console.log(" bun run bridge:opencode");
|
|
164
|
-
console.log(" bun run opencode:panel");
|
|
165
|
-
console.log(" bun run opencode:start");
|
|
166
|
-
console.log(" bun run bridge:claude");
|
|
167
|
-
console.log(" bun run claude:companion");
|
|
168
|
-
console.log(" bun run bridge:shell");
|
|
169
|
-
console.log();
|
|
170
|
-
console.log("Legacy MCP server entrypoint:");
|
|
171
|
-
console.log(" bun run start");
|
|
172
|
-
return;
|
|
221
|
+
saveCredentials(account);
|
|
222
|
+
log("\nWeChat login completed.");
|
|
223
|
+
log(`Account ID: ${account.accountId}`);
|
|
224
|
+
log(`User ID: ${account.userId ?? "(unknown)"}`);
|
|
225
|
+
log(`Credentials saved to: ${CREDENTIALS_FILE}`);
|
|
226
|
+
log("");
|
|
227
|
+
return account;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
231
|
+
}
|
|
232
|
+
throw new Error("Login timed out. Run setup again.");
|
|
233
|
+
}
|
|
234
|
+
export async function ensureWechatCredentials(options = {}) {
|
|
235
|
+
const log = options.log ?? ((message) => console.log(message));
|
|
236
|
+
migrateLegacyChannelFiles(log);
|
|
237
|
+
const existing = loadExistingCredentials();
|
|
238
|
+
const loginReason = getWechatLoginRequiredReason(existing, {
|
|
239
|
+
requireUserId: options.requireUserId,
|
|
240
|
+
});
|
|
241
|
+
if (!loginReason) {
|
|
242
|
+
const account = existing;
|
|
243
|
+
if (options.validateExisting) {
|
|
244
|
+
const invalidReason = await getStoredCredentialsInvalidReason(account, {
|
|
245
|
+
timeoutMs: options.validationTimeoutMs,
|
|
246
|
+
});
|
|
247
|
+
if (invalidReason) {
|
|
248
|
+
log(`${invalidReason} Starting WeChat login...`);
|
|
249
|
+
const login = options.login ?? (() => runWechatLogin(options));
|
|
250
|
+
return login();
|
|
173
251
|
}
|
|
174
252
|
}
|
|
175
|
-
|
|
253
|
+
return account;
|
|
254
|
+
}
|
|
255
|
+
log(`${loginReason} Starting WeChat login...`);
|
|
256
|
+
const login = options.login ?? (() => runWechatLogin(options));
|
|
257
|
+
return login();
|
|
258
|
+
}
|
|
259
|
+
async function main() {
|
|
260
|
+
migrateLegacyChannelFiles((message) => console.log(message));
|
|
261
|
+
const existing = loadExistingCredentials();
|
|
262
|
+
if (existing) {
|
|
263
|
+
console.log(`Found saved account: ${existing.accountId}`);
|
|
264
|
+
console.log(`Saved at: ${existing.savedAt}`);
|
|
265
|
+
console.log(`Credentials file: ${CREDENTIALS_FILE}`);
|
|
266
|
+
console.log();
|
|
267
|
+
const shouldRelogin = await askYesNo("Log in again? (y/N) ");
|
|
268
|
+
if (!shouldRelogin) {
|
|
269
|
+
console.log("Keeping existing credentials.");
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
176
272
|
}
|
|
177
|
-
|
|
178
|
-
|
|
273
|
+
await runWechatLogin({ requireUserId: true });
|
|
274
|
+
printPostLoginHelp((message) => console.log(message));
|
|
275
|
+
}
|
|
276
|
+
const isDirectRun = Boolean(import.meta.main);
|
|
277
|
+
if (isDirectRun) {
|
|
278
|
+
main().catch((err) => {
|
|
279
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
280
|
+
console.error(`Error: ${message}`);
|
|
281
|
+
process.exit(1);
|
|
282
|
+
});
|
|
179
283
|
}
|
|
180
|
-
main().catch((err) => {
|
|
181
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
182
|
-
console.error(`Error: ${message}`);
|
|
183
|
-
process.exit(1);
|
|
184
|
-
});
|
|
@@ -12,6 +12,7 @@ const SEND_TIMEOUT_MS = 15_000;
|
|
|
12
12
|
const CDN_MAX_RETRIES = 3;
|
|
13
13
|
const ERROR_CAUSE_DEPTH_LIMIT = 4;
|
|
14
14
|
const INBOUND_MESSAGE_CLAIM_TTL_MS = 10 * 60 * 1000;
|
|
15
|
+
const SYNC_SESSION_TIMEOUT_ERRCODE = -14;
|
|
15
16
|
const MSG_TYPE_USER = 1;
|
|
16
17
|
const MSG_TYPE_BOT = 2;
|
|
17
18
|
const MSG_ITEM_TEXT = 1;
|
|
@@ -24,6 +25,10 @@ const UPLOAD_MEDIA_TYPE_IMAGE = 1;
|
|
|
24
25
|
const UPLOAD_MEDIA_TYPE_VIDEO = 2;
|
|
25
26
|
const UPLOAD_MEDIA_TYPE_FILE = 3;
|
|
26
27
|
const UPLOAD_MEDIA_TYPE_VOICE = 4;
|
|
28
|
+
export function isWechatSyncSessionTimeout(response) {
|
|
29
|
+
return (response.errcode === SYNC_SESSION_TIMEOUT_ERRCODE &&
|
|
30
|
+
/session timeout/i.test(response.errmsg ?? ""));
|
|
31
|
+
}
|
|
27
32
|
const DEFAULT_MEDIA_UPLOAD_LIMIT_MB = {
|
|
28
33
|
image: 20,
|
|
29
34
|
file: 50,
|
|
@@ -199,6 +204,10 @@ export function describeWechatTransportError(error) {
|
|
|
199
204
|
}
|
|
200
205
|
export function classifyWechatTransportError(error) {
|
|
201
206
|
if (error instanceof Error) {
|
|
207
|
+
if (/WeChat session timed out/i.test(error.message) ||
|
|
208
|
+
/errcode=-14\b.*session timeout/i.test(error.message)) {
|
|
209
|
+
return { kind: "auth", retryable: false };
|
|
210
|
+
}
|
|
202
211
|
if (error.name === "AbortError") {
|
|
203
212
|
return { kind: "timeout", retryable: true };
|
|
204
213
|
}
|
|
@@ -539,7 +548,15 @@ export class WeChatTransport {
|
|
|
539
548
|
async pollMessages(options = {}) {
|
|
540
549
|
const timeoutMs = options.timeoutMs ?? DEFAULT_LONG_POLL_TIMEOUT_MS;
|
|
541
550
|
const account = this.requireAccount();
|
|
542
|
-
|
|
551
|
+
let response = await this.getUpdates(account, timeoutMs);
|
|
552
|
+
if (isWechatSyncSessionTimeout(response) && this.syncBuffer) {
|
|
553
|
+
this.logger.log("WeChat sync session timed out. Clearing local sync cursor and retrying once.");
|
|
554
|
+
this.clearSyncBuffer();
|
|
555
|
+
response = await this.getUpdates(account, timeoutMs);
|
|
556
|
+
}
|
|
557
|
+
if (isWechatSyncSessionTimeout(response)) {
|
|
558
|
+
throw new Error('WeChat session timed out. Run "wechat-setup" to log in again.');
|
|
559
|
+
}
|
|
543
560
|
const isError = (response.ret !== undefined && response.ret !== 0) ||
|
|
544
561
|
(response.errcode !== undefined && response.errcode !== 0);
|
|
545
562
|
if (isError) {
|
|
@@ -689,7 +706,7 @@ export class WeChatTransport {
|
|
|
689
706
|
requireAccount() {
|
|
690
707
|
const account = this.getCredentials();
|
|
691
708
|
if (!account) {
|
|
692
|
-
throw new Error(`No saved WeChat credentials found.
|
|
709
|
+
throw new Error(`No saved WeChat credentials found. Start a bridge command in a terminal to log in automatically, or run "wechat-setup". Expected file: ${CREDENTIALS_FILE}`);
|
|
693
710
|
}
|
|
694
711
|
return account;
|
|
695
712
|
}
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unlinearity/cli-wechat-bridge",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "CLI WeChat Bridge for Codex, Claude Code, OpenCode, and PowerShell over WeChat",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"files": [
|
|
7
|
-
"bin/",
|
|
8
|
-
"dist/",
|
|
9
|
-
"README.md",
|
|
10
|
-
"LICENSE.txt"
|
|
11
|
-
],
|
|
6
|
+
"files": [
|
|
7
|
+
"bin/",
|
|
8
|
+
"dist/",
|
|
9
|
+
"README.md",
|
|
10
|
+
"LICENSE.txt"
|
|
11
|
+
],
|
|
12
12
|
"keywords": [
|
|
13
13
|
"wechat",
|
|
14
14
|
"cli",
|
|
@@ -43,13 +43,14 @@
|
|
|
43
43
|
"wechat-bridge-opencode": "bin/wechat-bridge-opencode.mjs",
|
|
44
44
|
"wechat-opencode": "bin/wechat-opencode.mjs",
|
|
45
45
|
"wechat-opencode-start": "bin/wechat-opencode-start.mjs",
|
|
46
|
+
"wechat-setup": "bin/wechat-setup.mjs",
|
|
46
47
|
"wechat-check-update": "bin/wechat-check-update.mjs"
|
|
47
48
|
},
|
|
48
|
-
"scripts": {
|
|
49
|
-
"clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
|
|
50
|
-
"build": "npm run clean && tsc -p tsconfig.build.json",
|
|
51
|
-
"prepack": "npm run build",
|
|
52
|
-
"setup": "node --no-warnings --experimental-strip-types src/wechat/setup.ts",
|
|
49
|
+
"scripts": {
|
|
50
|
+
"clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
|
|
51
|
+
"build": "npm run clean && tsc -p tsconfig.build.json",
|
|
52
|
+
"prepack": "npm run build",
|
|
53
|
+
"setup": "node --no-warnings --experimental-strip-types src/wechat/setup.ts",
|
|
53
54
|
"start": "node --no-warnings --experimental-strip-types src/wechat/wechat-channel.ts",
|
|
54
55
|
"check": "node --no-warnings --experimental-strip-types src/wechat/wechat-channel.ts --check",
|
|
55
56
|
"bridge": "node --no-warnings --experimental-strip-types src/bridge/wechat-bridge.ts",
|
|
@@ -82,8 +83,8 @@
|
|
|
82
83
|
"@types/bun": "latest",
|
|
83
84
|
"typescript": "^6.0.2"
|
|
84
85
|
},
|
|
85
|
-
"engines": {
|
|
86
|
-
"bun": ">=1.0.0",
|
|
87
|
-
"node": ">=24.0.0"
|
|
88
|
-
}
|
|
89
|
-
}
|
|
86
|
+
"engines": {
|
|
87
|
+
"bun": ">=1.0.0",
|
|
88
|
+
"node": ">=24.0.0"
|
|
89
|
+
}
|
|
90
|
+
}
|