@unlinearity/cli-wechat-bridge 1.0.0 → 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.
- package/bin/_run-entry.mjs +2 -8
- package/bin/wechat-bridge-claude.mjs +2 -2
- package/bin/wechat-bridge-codex.mjs +2 -2
- package/bin/wechat-bridge-opencode.mjs +2 -2
- package/bin/wechat-bridge-shell.mjs +2 -2
- package/bin/wechat-bridge.mjs +2 -2
- package/bin/wechat-check-update.mjs +2 -2
- package/bin/wechat-claude-start.mjs +2 -2
- package/bin/wechat-claude.mjs +2 -2
- package/bin/wechat-codex-start.mjs +2 -2
- package/bin/wechat-codex.mjs +2 -2
- package/bin/wechat-opencode-start.mjs +2 -2
- package/bin/wechat-opencode.mjs +2 -2
- package/bin/wechat-setup.mjs +5 -0
- package/dist/bridge/bridge-adapters.claude.js +829 -0
- package/dist/bridge/bridge-adapters.codex.js +2206 -0
- package/dist/bridge/bridge-adapters.core.js +668 -0
- package/dist/bridge/bridge-adapters.js +26 -0
- package/dist/bridge/bridge-adapters.opencode.js +1777 -0
- package/dist/bridge/bridge-adapters.shared.js +1098 -0
- package/dist/bridge/bridge-adapters.shell.js +363 -0
- package/dist/bridge/bridge-controller.js +44 -0
- package/dist/bridge/bridge-final-reply.js +31 -0
- package/dist/bridge/bridge-process-reaper.js +348 -0
- package/dist/bridge/bridge-state.js +356 -0
- package/dist/bridge/bridge-types.js +1 -0
- package/dist/bridge/bridge-utils.js +1150 -0
- package/dist/bridge/claude-hook.js +82 -0
- package/dist/bridge/claude-hooks.js +267 -0
- package/dist/bridge/wechat-bridge.js +904 -0
- package/dist/commands/check-update.js +30 -0
- package/dist/companion/codex-panel-link.js +72 -0
- package/dist/companion/codex-panel.js +179 -0
- package/dist/companion/codex-remote-client.js +117 -0
- package/dist/companion/local-companion-link.js +210 -0
- package/dist/companion/local-companion-start.js +372 -0
- package/dist/companion/local-companion.js +411 -0
- package/dist/media/media-types.js +1 -0
- package/dist/runtime/create-runtime-host.js +12 -0
- package/dist/runtime/legacy-adapter-runtime.js +40 -0
- package/dist/runtime/runtime-types.js +5 -0
- package/dist/utils/version-checker.js +161 -0
- package/dist/wechat/channel-config.js +103 -0
- package/dist/wechat/setup.js +283 -0
- package/dist/wechat/standalone-bot.js +356 -0
- package/dist/wechat/wechat-channel.js +481 -0
- package/dist/wechat/wechat-transport.js +887 -0
- package/package.json +6 -2
- package/src/bridge/bridge-adapters.claude.ts +0 -1047
- package/src/bridge/bridge-adapters.codex.ts +0 -2733
- package/src/bridge/bridge-adapters.core.ts +0 -877
- package/src/bridge/bridge-adapters.opencode.ts +0 -2390
- package/src/bridge/bridge-adapters.shared.ts +0 -1647
- package/src/bridge/bridge-adapters.shell.ts +0 -456
- package/src/bridge/bridge-adapters.ts +0 -30
- package/src/bridge/bridge-controller.ts +0 -58
- package/src/bridge/bridge-final-reply.ts +0 -56
- package/src/bridge/bridge-process-reaper.ts +0 -417
- package/src/bridge/bridge-state.ts +0 -520
- package/src/bridge/bridge-types.ts +0 -218
- package/src/bridge/bridge-utils.ts +0 -1524
- package/src/bridge/claude-hook.ts +0 -92
- package/src/bridge/claude-hooks.ts +0 -376
- package/src/bridge/wechat-bridge.ts +0 -1305
- package/src/commands/check-update.ts +0 -46
- package/src/companion/codex-panel-link.ts +0 -151
- package/src/companion/codex-panel.ts +0 -225
- package/src/companion/codex-remote-client.ts +0 -178
- package/src/companion/local-companion-link.ts +0 -338
- package/src/companion/local-companion-start.ts +0 -520
- package/src/companion/local-companion.ts +0 -532
- package/src/media/media-types.ts +0 -11
- package/src/runtime/create-runtime-host.ts +0 -16
- package/src/runtime/legacy-adapter-runtime.ts +0 -55
- package/src/runtime/runtime-types.ts +0 -51
- package/src/utils/version-checker.ts +0 -197
- package/src/wechat/channel-config.ts +0 -141
- package/src/wechat/setup.ts +0 -232
- package/src/wechat/standalone-bot.ts +0 -483
- package/src/wechat/wechat-channel.ts +0 -608
- package/src/wechat/wechat-transport.ts +0 -1290
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const PROJECT_DIR = path.resolve(MODULE_DIR, "..", "..");
|
|
8
|
+
export const DEFAULT_BASE_URL = process.env.WECHAT_ILINK_BASE_URL?.trim() || "https://ilinkai.weixin.qq.com";
|
|
9
|
+
export const BOT_TYPE = "3";
|
|
10
|
+
export const CHANNEL_DATA_DIR = process.env.CLAUDE_WECHAT_CHANNEL_DATA_DIR?.trim()
|
|
11
|
+
? path.resolve(process.env.CLAUDE_WECHAT_CHANNEL_DATA_DIR.trim())
|
|
12
|
+
: path.join(os.homedir(), ".claude", "channels", "wechat");
|
|
13
|
+
export const GLOBAL_CHANNEL_DATA_DIR = path.join(os.homedir(), ".claude", "channels", "wechat");
|
|
14
|
+
export const CREDENTIALS_FILE = path.join(CHANNEL_DATA_DIR, "account.json");
|
|
15
|
+
export const SYNC_BUF_FILE = path.join(CHANNEL_DATA_DIR, "sync_buf.txt");
|
|
16
|
+
export const CONTEXT_CACHE_FILE = path.join(CHANNEL_DATA_DIR, "context_tokens.json");
|
|
17
|
+
export const BRIDGE_STATE_FILE = path.join(CHANNEL_DATA_DIR, "bridge-state.json");
|
|
18
|
+
export const BRIDGE_LOG_FILE = path.join(CHANNEL_DATA_DIR, "bridge.log");
|
|
19
|
+
export const BRIDGE_LOCK_FILE = path.join(CHANNEL_DATA_DIR, "bridge.lock.json");
|
|
20
|
+
export const CODEX_PANEL_ENDPOINT_FILE = path.join(CHANNEL_DATA_DIR, "codex-panel-endpoint.json");
|
|
21
|
+
export const WORKSPACES_DIR = path.join(CHANNEL_DATA_DIR, "workspaces");
|
|
22
|
+
export const INBOUND_MESSAGE_CLAIMS_DIR = path.join(GLOBAL_CHANNEL_DATA_DIR, "inbound-message-claims");
|
|
23
|
+
const LEGACY_CHANNEL_DATA_DIR = path.join(PROJECT_DIR, "~", ".claude", "channels", "wechat");
|
|
24
|
+
const LEGACY_CREDENTIALS_FILE = path.join(LEGACY_CHANNEL_DATA_DIR, "account.json");
|
|
25
|
+
const LEGACY_SYNC_BUF_FILE = path.join(LEGACY_CHANNEL_DATA_DIR, "sync_buf.txt");
|
|
26
|
+
export function ensureChannelDataDir() {
|
|
27
|
+
fs.mkdirSync(CHANNEL_DATA_DIR, { recursive: true });
|
|
28
|
+
}
|
|
29
|
+
export function normalizeWorkspacePath(cwd) {
|
|
30
|
+
return path.resolve(cwd);
|
|
31
|
+
}
|
|
32
|
+
function buildComparableWorkspacePath(cwd) {
|
|
33
|
+
const normalized = normalizeWorkspacePath(cwd);
|
|
34
|
+
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
35
|
+
}
|
|
36
|
+
function sanitizeWorkspaceSegment(value) {
|
|
37
|
+
const sanitized = value
|
|
38
|
+
.replace(/[^a-zA-Z0-9._-]+/g, "-")
|
|
39
|
+
.replace(/^-+|-+$/g, "")
|
|
40
|
+
.slice(0, 40);
|
|
41
|
+
return sanitized || "workspace";
|
|
42
|
+
}
|
|
43
|
+
export function buildWorkspaceKey(cwd) {
|
|
44
|
+
const normalized = normalizeWorkspacePath(cwd);
|
|
45
|
+
const digest = crypto
|
|
46
|
+
.createHash("sha256")
|
|
47
|
+
.update(buildComparableWorkspacePath(normalized))
|
|
48
|
+
.digest("hex")
|
|
49
|
+
.slice(0, 12);
|
|
50
|
+
const label = sanitizeWorkspaceSegment(path.basename(normalized));
|
|
51
|
+
return `${label}-${digest}`;
|
|
52
|
+
}
|
|
53
|
+
export function getWorkspaceChannelPaths(cwd) {
|
|
54
|
+
const workspaceDir = path.join(WORKSPACES_DIR, buildWorkspaceKey(cwd));
|
|
55
|
+
return {
|
|
56
|
+
workspaceDir,
|
|
57
|
+
stateFile: path.join(workspaceDir, "bridge-state.json"),
|
|
58
|
+
endpointFile: path.join(workspaceDir, "codex-panel-endpoint.json"),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export function ensureWorkspaceChannelDir(cwd) {
|
|
62
|
+
ensureChannelDataDir();
|
|
63
|
+
const paths = getWorkspaceChannelPaths(cwd);
|
|
64
|
+
fs.mkdirSync(paths.workspaceDir, { recursive: true });
|
|
65
|
+
return paths;
|
|
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
|
+
}
|
|
83
|
+
export function migrateLegacyChannelFiles(log) {
|
|
84
|
+
const migrated = [];
|
|
85
|
+
const channelDataDirHadState = channelDataDirHasState();
|
|
86
|
+
if (!fs.existsSync(LEGACY_CREDENTIALS_FILE) &&
|
|
87
|
+
!fs.existsSync(LEGACY_SYNC_BUF_FILE)) {
|
|
88
|
+
return migrated;
|
|
89
|
+
}
|
|
90
|
+
ensureChannelDataDir();
|
|
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
|
+
}
|
|
99
|
+
if (migrated.length && log) {
|
|
100
|
+
log(`Migrated legacy ${migrated.join(" and ")} from ${LEGACY_CHANNEL_DATA_DIR} to ${CHANNEL_DATA_DIR}.`);
|
|
101
|
+
}
|
|
102
|
+
return migrated;
|
|
103
|
+
}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* CLI WeChat Bridge setup.
|
|
4
|
+
*
|
|
5
|
+
* Installed bridge commands run this automatically on first use.
|
|
6
|
+
* To force a relogin manually:
|
|
7
|
+
* wechat-setup
|
|
8
|
+
*/
|
|
9
|
+
import crypto from "node:crypto";
|
|
10
|
+
import fs from "node:fs";
|
|
11
|
+
import readline from "node:readline";
|
|
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() {
|
|
16
|
+
try {
|
|
17
|
+
if (!fs.existsSync(CREDENTIALS_FILE)) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
return JSON.parse(fs.readFileSync(CREDENTIALS_FILE, "utf-8"));
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
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
|
+
}
|
|
85
|
+
async function fetchQRCode(baseUrl) {
|
|
86
|
+
const base = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
87
|
+
const url = `${base}ilink/bot/get_bot_qrcode?bot_type=${BOT_TYPE}`;
|
|
88
|
+
const res = await fetch(url);
|
|
89
|
+
if (!res.ok) {
|
|
90
|
+
throw new Error(`QR fetch failed: ${res.status}`);
|
|
91
|
+
}
|
|
92
|
+
return (await res.json());
|
|
93
|
+
}
|
|
94
|
+
async function pollQRStatus(baseUrl, qrcode) {
|
|
95
|
+
const base = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
96
|
+
const url = `${base}ilink/bot/get_qrcode_status?qrcode=${encodeURIComponent(qrcode)}`;
|
|
97
|
+
const controller = new AbortController();
|
|
98
|
+
const timer = setTimeout(() => controller.abort(), 35_000);
|
|
99
|
+
try {
|
|
100
|
+
const res = await fetch(url, {
|
|
101
|
+
headers: { "iLink-App-ClientVersion": "1" },
|
|
102
|
+
signal: controller.signal,
|
|
103
|
+
});
|
|
104
|
+
clearTimeout(timer);
|
|
105
|
+
if (!res.ok) {
|
|
106
|
+
throw new Error(`QR status failed: ${res.status}`);
|
|
107
|
+
}
|
|
108
|
+
return (await res.json());
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
clearTimeout(timer);
|
|
112
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
113
|
+
return { status: "wait" };
|
|
114
|
+
}
|
|
115
|
+
throw err;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async function askYesNo(prompt) {
|
|
119
|
+
const rl = readline.createInterface({
|
|
120
|
+
input: process.stdin,
|
|
121
|
+
output: process.stdout,
|
|
122
|
+
});
|
|
123
|
+
try {
|
|
124
|
+
const answer = await new Promise((resolve) => {
|
|
125
|
+
rl.question(prompt, resolve);
|
|
126
|
+
});
|
|
127
|
+
return answer.trim().toLowerCase() === "y";
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
rl.close();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async function printQRCode(qrContent, write) {
|
|
134
|
+
try {
|
|
135
|
+
const qrterm = await import("qrcode-terminal");
|
|
136
|
+
await new Promise((resolve) => {
|
|
137
|
+
qrterm.default.generate(qrContent, { small: true }, (qr) => {
|
|
138
|
+
write(`${qr}\n`);
|
|
139
|
+
resolve();
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
write(`Open this QR code URL in a browser: ${qrContent}\n\n`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
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.
|
|
156
|
+
}
|
|
157
|
+
}
|
|
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;
|
|
192
|
+
let scannedPrinted = false;
|
|
193
|
+
while (Date.now() < deadline) {
|
|
194
|
+
const status = await pollQRStatus(baseUrl, qrResp.qrcode);
|
|
195
|
+
switch (status.status) {
|
|
196
|
+
case "wait":
|
|
197
|
+
write(".");
|
|
198
|
+
break;
|
|
199
|
+
case "scaned":
|
|
200
|
+
if (!scannedPrinted) {
|
|
201
|
+
log("\nQR code scanned. Confirm the login in WeChat...");
|
|
202
|
+
scannedPrinted = true;
|
|
203
|
+
}
|
|
204
|
+
break;
|
|
205
|
+
case "expired":
|
|
206
|
+
throw new Error("The QR code expired. Run setup again.");
|
|
207
|
+
case "confirmed": {
|
|
208
|
+
if (!status.ilink_bot_id || !status.bot_token) {
|
|
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.");
|
|
213
|
+
}
|
|
214
|
+
const account = {
|
|
215
|
+
token: status.bot_token,
|
|
216
|
+
baseUrl: status.baseurl || baseUrl,
|
|
217
|
+
accountId: status.ilink_bot_id,
|
|
218
|
+
userId: status.ilink_user_id,
|
|
219
|
+
savedAt: new Date().toISOString(),
|
|
220
|
+
};
|
|
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();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
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
|
+
}
|
|
272
|
+
}
|
|
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
|
+
});
|
|
283
|
+
}
|