@unlinearity/cli-wechat-bridge 1.0.9 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -0
- package/dist/bridge/bridge-adapters.claude.js +186 -5
- package/dist/bridge/bridge-adapters.codex.js +58 -24
- package/dist/bridge/bridge-adapters.core.js +39 -5
- package/dist/bridge/bridge-adapters.opencode.js +6 -2
- package/dist/bridge/bridge-adapters.shared.js +104 -1
- package/dist/bridge/bridge-adapters.shell.js +1 -1
- package/dist/bridge/bridge-utils.js +36 -12
- package/dist/bridge/claude-hooks.js +9 -16
- package/dist/bridge/wechat-bridge.js +64 -10
- package/dist/companion/local-companion.js +1 -0
- package/dist/daemon/emoji-bindings.js +31 -0
- package/dist/daemon/wechat-daemon.js +61 -75
- package/dist/i18n/index.js +32 -0
- package/dist/i18n/messages-en.js +125 -0
- package/dist/i18n/messages-zh.js +125 -0
- package/dist/runtime/legacy-adapter-runtime.js +3 -0
- package/dist/utils/doctor.js +540 -0
- package/dist/wechat/channel-config.js +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import net from "node:net";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { normalizeBridgeLockPayload, shouldAutoReclaimBridgeLock, } from "../bridge/bridge-state.js";
|
|
7
|
+
import { readLocalCompanionEndpoint as readWorkspaceEndpoint, } from "../companion/local-companion-link.js";
|
|
8
|
+
import { isDaemonEndpointAlive, } from "../daemon/daemon-link.js";
|
|
9
|
+
import { t } from "../i18n/index.js";
|
|
10
|
+
import { resolveChannelDataDir } from "../wechat/channel-config.js";
|
|
11
|
+
const STATE_OK = "[ok]";
|
|
12
|
+
const STATE_WARN = "[warn]";
|
|
13
|
+
const STATE_FAIL = "[fail]";
|
|
14
|
+
const RUNTIME_ENDPOINT_TIMEOUT_MS = 500;
|
|
15
|
+
const DAEMON_ENDPOINT_TIMEOUT_MS = 500;
|
|
16
|
+
function msg(key, params) {
|
|
17
|
+
return t(`doctor.${key}`, params);
|
|
18
|
+
}
|
|
19
|
+
function section(lines, title) {
|
|
20
|
+
if (lines.length > 0 && lines.at(-1) !== "") {
|
|
21
|
+
lines.push("");
|
|
22
|
+
}
|
|
23
|
+
lines.push(title);
|
|
24
|
+
}
|
|
25
|
+
function row(status, label, value) {
|
|
26
|
+
return ` ${status} ${label} ${value}`;
|
|
27
|
+
}
|
|
28
|
+
function fieldRow(label, value) {
|
|
29
|
+
return ` ${label} ${value}`;
|
|
30
|
+
}
|
|
31
|
+
function detail(value) {
|
|
32
|
+
return ` ${value}`;
|
|
33
|
+
}
|
|
34
|
+
function detailField(label, value) {
|
|
35
|
+
return detail(`${label} ${value}`);
|
|
36
|
+
}
|
|
37
|
+
function findExecutable(name) {
|
|
38
|
+
const cmd = process.platform === "win32" ? "where" : "which";
|
|
39
|
+
try {
|
|
40
|
+
return execFileSync(cmd, [name], {
|
|
41
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
42
|
+
encoding: "utf8",
|
|
43
|
+
}).trim().split(/\r?\n/)[0] ?? null;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function getPlatformPtyFix(platform) {
|
|
50
|
+
switch (platform) {
|
|
51
|
+
case "linux":
|
|
52
|
+
return "sudo apt install build-essential python3 && npm install -g cli-wechat-bridge@latest";
|
|
53
|
+
case "darwin":
|
|
54
|
+
return "xcode-select --install && npm rebuild node-pty";
|
|
55
|
+
case "win32":
|
|
56
|
+
return "npm rebuild node-pty (also ensure Visual C++ Redistributable is installed)";
|
|
57
|
+
default:
|
|
58
|
+
return "npm rebuild node-pty";
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function defaultIsProcessAlive(pid) {
|
|
62
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
process.kill(pid, 0);
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function defaultIsTcpPortReachable(port, timeoutMs) {
|
|
74
|
+
if (!Number.isInteger(port) || port <= 0) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
return await new Promise((resolve) => {
|
|
78
|
+
const socket = net.connect({
|
|
79
|
+
host: "127.0.0.1",
|
|
80
|
+
port,
|
|
81
|
+
});
|
|
82
|
+
let done = false;
|
|
83
|
+
const finish = (result) => {
|
|
84
|
+
if (done) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
done = true;
|
|
88
|
+
socket.destroy();
|
|
89
|
+
resolve(result);
|
|
90
|
+
};
|
|
91
|
+
socket.setTimeout(timeoutMs);
|
|
92
|
+
socket.once("connect", () => finish(true));
|
|
93
|
+
socket.once("timeout", () => finish(false));
|
|
94
|
+
socket.once("error", () => finish(false));
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
function resolveDeps(deps = {}) {
|
|
98
|
+
return {
|
|
99
|
+
platform: deps.platform ?? process.platform,
|
|
100
|
+
arch: deps.arch ?? process.arch,
|
|
101
|
+
nodeVersion: deps.nodeVersion ?? process.version,
|
|
102
|
+
osRelease: deps.osRelease ?? os.release,
|
|
103
|
+
findExecutable: deps.findExecutable ?? findExecutable,
|
|
104
|
+
loadNodePty: deps.loadNodePty ??
|
|
105
|
+
(async () => {
|
|
106
|
+
await import("node-pty");
|
|
107
|
+
}),
|
|
108
|
+
resolveDataDir: deps.resolveDataDir ?? resolveChannelDataDir,
|
|
109
|
+
exists: deps.exists ?? fs.existsSync,
|
|
110
|
+
readTextFile: deps.readTextFile ??
|
|
111
|
+
((filePath) => fs.readFileSync(filePath, "utf8")),
|
|
112
|
+
isProcessAlive: deps.isProcessAlive ?? defaultIsProcessAlive,
|
|
113
|
+
isTcpPortReachable: deps.isTcpPortReachable ?? defaultIsTcpPortReachable,
|
|
114
|
+
isDaemonAlive: deps.isDaemonAlive ??
|
|
115
|
+
((endpoint) => isDaemonEndpointAlive(endpoint, { timeoutMs: DAEMON_ENDPOINT_TIMEOUT_MS })),
|
|
116
|
+
readLocalCompanionEndpoint: deps.readLocalCompanionEndpoint ??
|
|
117
|
+
((cwd, adapter) => readWorkspaceEndpoint(cwd, adapter ? { adapter } : {})),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function isBridgeAdapterKind(value) {
|
|
121
|
+
return (value === "codex" ||
|
|
122
|
+
value === "claude" ||
|
|
123
|
+
value === "opencode" ||
|
|
124
|
+
value === "shell");
|
|
125
|
+
}
|
|
126
|
+
export function parseDoctorCliArgs(argv, mode = "generic", fallbackCwd = process.cwd()) {
|
|
127
|
+
let cwd = fallbackCwd;
|
|
128
|
+
let adapter;
|
|
129
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
130
|
+
const arg = argv[i];
|
|
131
|
+
const next = argv[i + 1];
|
|
132
|
+
if (!arg) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (arg === "--cwd" && next) {
|
|
136
|
+
cwd = path.resolve(next);
|
|
137
|
+
i += 1;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (arg === "--adapter" && isBridgeAdapterKind(next)) {
|
|
141
|
+
adapter = next;
|
|
142
|
+
i += 1;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
mode,
|
|
148
|
+
cwd: path.resolve(cwd),
|
|
149
|
+
adapter,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function statusTag(status) {
|
|
153
|
+
if (status === "ok") {
|
|
154
|
+
return STATE_OK;
|
|
155
|
+
}
|
|
156
|
+
if (status === "warn") {
|
|
157
|
+
return STATE_WARN;
|
|
158
|
+
}
|
|
159
|
+
return STATE_FAIL;
|
|
160
|
+
}
|
|
161
|
+
function shortError(error) {
|
|
162
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
163
|
+
return message.length > 100 ? `${message.slice(0, 100)}...` : message;
|
|
164
|
+
}
|
|
165
|
+
function sameWorkspacePath(left, right) {
|
|
166
|
+
const normalizedLeft = path.resolve(left);
|
|
167
|
+
const normalizedRight = path.resolve(right);
|
|
168
|
+
return process.platform === "win32"
|
|
169
|
+
? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase()
|
|
170
|
+
: normalizedLeft === normalizedRight;
|
|
171
|
+
}
|
|
172
|
+
function normalizeDaemonEndpoint(value) {
|
|
173
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
const record = value;
|
|
177
|
+
if (typeof record.protocolVersion !== "number" ||
|
|
178
|
+
typeof record.pid !== "number" ||
|
|
179
|
+
typeof record.port !== "number" ||
|
|
180
|
+
typeof record.token !== "string" ||
|
|
181
|
+
typeof record.cwd !== "string" ||
|
|
182
|
+
typeof record.startedAt !== "string") {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
protocolVersion: record.protocolVersion,
|
|
187
|
+
pid: record.pid,
|
|
188
|
+
port: record.port,
|
|
189
|
+
token: record.token,
|
|
190
|
+
cwd: record.cwd,
|
|
191
|
+
startedAt: record.startedAt,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function readNormalizedJsonFile(filePath, deps, normalize) {
|
|
195
|
+
if (!deps.exists(filePath)) {
|
|
196
|
+
return { kind: "missing", filePath };
|
|
197
|
+
}
|
|
198
|
+
try {
|
|
199
|
+
const parsed = JSON.parse(deps.readTextFile(filePath));
|
|
200
|
+
const normalized = normalize(parsed);
|
|
201
|
+
if (!normalized) {
|
|
202
|
+
return {
|
|
203
|
+
kind: "invalid",
|
|
204
|
+
filePath,
|
|
205
|
+
error: msg("invalidShape"),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
kind: "ok",
|
|
210
|
+
filePath,
|
|
211
|
+
value: normalized,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
return {
|
|
216
|
+
kind: "invalid",
|
|
217
|
+
filePath,
|
|
218
|
+
error: shortError(error),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function formatMode(mode) {
|
|
223
|
+
if (mode === "bridge") {
|
|
224
|
+
return msg("mode.bridge");
|
|
225
|
+
}
|
|
226
|
+
if (mode === "daemon") {
|
|
227
|
+
return "wechat-daemon";
|
|
228
|
+
}
|
|
229
|
+
return msg("mode.generic");
|
|
230
|
+
}
|
|
231
|
+
function formatAlive(value) {
|
|
232
|
+
if (value === undefined) {
|
|
233
|
+
return msg("unknown");
|
|
234
|
+
}
|
|
235
|
+
return value ? msg("yes") : msg("no");
|
|
236
|
+
}
|
|
237
|
+
function appendLockDetailLines(lines, lock) {
|
|
238
|
+
lines.push(detailField(msg("lock.detail.pid"), String(lock.pid)));
|
|
239
|
+
lines.push(detailField(msg("lock.detail.adapter"), lock.adapter));
|
|
240
|
+
lines.push(detailField(msg("lock.detail.cwd"), lock.cwd));
|
|
241
|
+
lines.push(detailField(msg("lock.detail.lifecycle"), lock.lifecycle));
|
|
242
|
+
lines.push(detailField(msg("lock.detail.startedAt"), lock.startedAt));
|
|
243
|
+
}
|
|
244
|
+
async function appendDaemonLines(lines, options, dataDir, deps) {
|
|
245
|
+
const endpointFile = path.join(dataDir, "daemon-endpoint.json");
|
|
246
|
+
const endpointResult = readNormalizedJsonFile(endpointFile, deps, normalizeDaemonEndpoint);
|
|
247
|
+
const label = msg("label.daemon");
|
|
248
|
+
if (endpointResult.kind === "missing") {
|
|
249
|
+
lines.push(row(STATE_OK, label, msg("none")));
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (endpointResult.kind === "invalid") {
|
|
253
|
+
lines.push(row(STATE_WARN, label, msg("daemon.invalid", {
|
|
254
|
+
error: endpointResult.error,
|
|
255
|
+
})));
|
|
256
|
+
lines.push(detail(msg("file", { file: endpointResult.filePath })));
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const endpoint = endpointResult.value;
|
|
260
|
+
const pidAlive = deps.isProcessAlive(endpoint.pid);
|
|
261
|
+
const daemonAlive = pidAlive ? await deps.isDaemonAlive(endpoint) : false;
|
|
262
|
+
if (!daemonAlive) {
|
|
263
|
+
lines.push(row(STATE_WARN, label, msg("daemon.stale", {
|
|
264
|
+
pid: endpoint.pid,
|
|
265
|
+
cwd: endpoint.cwd,
|
|
266
|
+
port: endpoint.port,
|
|
267
|
+
})));
|
|
268
|
+
lines.push(detail(msg("daemon.staleStartup")));
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const status = options.mode === "bridge" ? "fail" : "warn";
|
|
272
|
+
lines.push(row(statusTag(status), label, msg("daemon.live", {
|
|
273
|
+
pid: endpoint.pid,
|
|
274
|
+
cwd: endpoint.cwd,
|
|
275
|
+
port: endpoint.port,
|
|
276
|
+
})));
|
|
277
|
+
if (options.mode === "bridge") {
|
|
278
|
+
lines.push(detail(msg("daemon.liveBridgeStartup")));
|
|
279
|
+
}
|
|
280
|
+
else if (options.mode === "daemon") {
|
|
281
|
+
lines.push(detail(msg("daemon.liveDaemonStartup")));
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function appendBridgeLockLines(lines, options, dataDir, deps) {
|
|
285
|
+
const lockFile = path.join(dataDir, "bridge.lock.json");
|
|
286
|
+
const lockResult = readNormalizedJsonFile(lockFile, deps, normalizeBridgeLockPayload);
|
|
287
|
+
const label = msg("label.bridgeLock");
|
|
288
|
+
if (lockResult.kind === "missing") {
|
|
289
|
+
lines.push(row(STATE_OK, label, msg("none")));
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
if (lockResult.kind === "invalid") {
|
|
293
|
+
lines.push(row(STATE_WARN, label, msg("lock.invalid", {
|
|
294
|
+
error: lockResult.error,
|
|
295
|
+
})));
|
|
296
|
+
lines.push(detail(msg("file", { file: lockResult.filePath })));
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
const lock = lockResult.value;
|
|
300
|
+
const lockAlive = deps.isProcessAlive(lock.pid);
|
|
301
|
+
const parentAlive = lock.parentPid > 1 ? deps.isProcessAlive(lock.parentPid) : undefined;
|
|
302
|
+
const reclaimable = lockAlive
|
|
303
|
+
? shouldAutoReclaimBridgeLock(lock, deps.isProcessAlive)
|
|
304
|
+
: false;
|
|
305
|
+
if (!lockAlive) {
|
|
306
|
+
lines.push(row(STATE_WARN, label, msg("lock.stale")));
|
|
307
|
+
appendLockDetailLines(lines, lock);
|
|
308
|
+
lines.push(detailField(msg("lock.detail.startup"), options.mode === "daemon"
|
|
309
|
+
? msg("lock.staleDaemonStartup")
|
|
310
|
+
: msg("lock.staleBridgeStartup")));
|
|
311
|
+
}
|
|
312
|
+
else if (reclaimable) {
|
|
313
|
+
lines.push(row(STATE_WARN, label, msg("lock.reclaimable")));
|
|
314
|
+
appendLockDetailLines(lines, lock);
|
|
315
|
+
lines.push(detailField(msg("lock.detail.parent"), msg("lock.parent", {
|
|
316
|
+
pid: lock.parentPid,
|
|
317
|
+
alive: formatAlive(parentAlive),
|
|
318
|
+
})));
|
|
319
|
+
lines.push(detailField(msg("lock.detail.startup"), options.mode === "daemon"
|
|
320
|
+
? msg("lock.reclaimableDaemonStartup")
|
|
321
|
+
: msg("lock.reclaimableBridgeStartup")));
|
|
322
|
+
}
|
|
323
|
+
else if (options.mode === "daemon") {
|
|
324
|
+
lines.push(row(STATE_WARN, label, msg("lock.live")));
|
|
325
|
+
appendLockDetailLines(lines, lock);
|
|
326
|
+
lines.push(detailField(msg("lock.detail.startup"), msg("lock.liveDaemonStartup")));
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
lines.push(row(STATE_FAIL, label, msg("lock.live")));
|
|
330
|
+
appendLockDetailLines(lines, lock);
|
|
331
|
+
lines.push(detailField(msg("lock.detail.startup"), msg("lock.liveBridgeStartup")));
|
|
332
|
+
}
|
|
333
|
+
if (!sameWorkspacePath(lock.cwd, options.cwd)) {
|
|
334
|
+
lines.push(detailField(msg("lock.detail.note"), msg("lock.cwdDifferent", { cwd: options.cwd })));
|
|
335
|
+
}
|
|
336
|
+
if (options.adapter && lock.adapter !== options.adapter) {
|
|
337
|
+
lines.push(detailField(msg("lock.detail.note"), msg("lock.adapterDifferent", {
|
|
338
|
+
lockAdapter: lock.adapter,
|
|
339
|
+
requestedAdapter: options.adapter,
|
|
340
|
+
})));
|
|
341
|
+
}
|
|
342
|
+
return lock;
|
|
343
|
+
}
|
|
344
|
+
function chooseEndpointAdapters(options, lock) {
|
|
345
|
+
if (options.adapter) {
|
|
346
|
+
return [options.adapter];
|
|
347
|
+
}
|
|
348
|
+
if (lock) {
|
|
349
|
+
return [lock.adapter];
|
|
350
|
+
}
|
|
351
|
+
return options.mode === "daemon"
|
|
352
|
+
? ["codex", "claude", "opencode"]
|
|
353
|
+
: ["codex"];
|
|
354
|
+
}
|
|
355
|
+
function chooseCliChecks(options) {
|
|
356
|
+
if (options.mode === "bridge") {
|
|
357
|
+
switch (options.adapter) {
|
|
358
|
+
case "codex":
|
|
359
|
+
return [{ name: "codex", label: "Codex CLI", optional: false }];
|
|
360
|
+
case "claude":
|
|
361
|
+
return [{ name: "claude", label: "Claude CLI", optional: false }];
|
|
362
|
+
case "opencode":
|
|
363
|
+
return [{ name: "opencode", label: "OpenCode", optional: false }];
|
|
364
|
+
case "shell":
|
|
365
|
+
return [];
|
|
366
|
+
default:
|
|
367
|
+
return [];
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return [
|
|
371
|
+
{ name: "codex", label: "Codex CLI", optional: true },
|
|
372
|
+
{ name: "claude", label: "Claude CLI", optional: true },
|
|
373
|
+
{ name: "opencode", label: "OpenCode", optional: true },
|
|
374
|
+
];
|
|
375
|
+
}
|
|
376
|
+
function appendCliLines(lines, options, deps) {
|
|
377
|
+
const clis = chooseCliChecks(options);
|
|
378
|
+
if (options.mode === "bridge" && options.adapter === "shell") {
|
|
379
|
+
section(lines, msg("section.adapterCli"));
|
|
380
|
+
lines.push(row(STATE_OK, msg("label.shell"), msg("adapterCli.shellNative")));
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
if (clis.length === 0) {
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
section(lines, msg("section.adapterCli"));
|
|
387
|
+
for (const cli of clis) {
|
|
388
|
+
const loc = deps.findExecutable(cli.name);
|
|
389
|
+
if (loc) {
|
|
390
|
+
lines.push(row(STATE_OK, cli.label, loc));
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
lines.push(row(STATE_FAIL, cli.label, msg("adapterCli.notFound", {
|
|
394
|
+
optional: cli.optional ? ` ${msg("optional")}` : "",
|
|
395
|
+
})));
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
async function appendEndpointSummary(lines, label, endpoint, options, lock, deps) {
|
|
400
|
+
if (!endpoint) {
|
|
401
|
+
lines.push(row(STATE_OK, label, msg("none")));
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
const port = endpoint.serverPort ?? endpoint.port;
|
|
405
|
+
const reachable = await deps.isTcpPortReachable(port, RUNTIME_ENDPOINT_TIMEOUT_MS);
|
|
406
|
+
const bridgeOwnerAlive = endpoint.bridgeOwnerPid
|
|
407
|
+
? deps.isProcessAlive(endpoint.bridgeOwnerPid)
|
|
408
|
+
: undefined;
|
|
409
|
+
const companionAlive = endpoint.companionPid
|
|
410
|
+
? deps.isProcessAlive(endpoint.companionPid)
|
|
411
|
+
: undefined;
|
|
412
|
+
const issues = [];
|
|
413
|
+
if (options.adapter && endpoint.kind !== options.adapter) {
|
|
414
|
+
issues.push(msg("endpoint.issue.kind", {
|
|
415
|
+
kind: endpoint.kind,
|
|
416
|
+
adapter: options.adapter,
|
|
417
|
+
}));
|
|
418
|
+
}
|
|
419
|
+
if (!sameWorkspacePath(endpoint.cwd, options.cwd)) {
|
|
420
|
+
issues.push(msg("endpoint.issue.cwd", { cwd: options.cwd }));
|
|
421
|
+
}
|
|
422
|
+
if (!reachable) {
|
|
423
|
+
issues.push(msg("endpoint.issue.port", { port }));
|
|
424
|
+
}
|
|
425
|
+
if (bridgeOwnerAlive === false) {
|
|
426
|
+
issues.push(msg("endpoint.issue.ownerDead", {
|
|
427
|
+
pid: endpoint.bridgeOwnerPid ?? msg("none"),
|
|
428
|
+
}));
|
|
429
|
+
}
|
|
430
|
+
if (companionAlive === false) {
|
|
431
|
+
issues.push(msg("endpoint.issue.companionDead", {
|
|
432
|
+
pid: endpoint.companionPid ?? msg("none"),
|
|
433
|
+
}));
|
|
434
|
+
}
|
|
435
|
+
if (endpoint.companionStatus === "stopped" ||
|
|
436
|
+
endpoint.companionStatus === "error") {
|
|
437
|
+
issues.push(msg("endpoint.issue.workerStatus", {
|
|
438
|
+
status: endpoint.companionStatus,
|
|
439
|
+
}));
|
|
440
|
+
}
|
|
441
|
+
if (lock &&
|
|
442
|
+
endpoint.kind === lock.adapter &&
|
|
443
|
+
sameWorkspacePath(endpoint.cwd, lock.cwd) &&
|
|
444
|
+
endpoint.instanceId !== lock.instanceId) {
|
|
445
|
+
issues.push(msg("endpoint.issue.instanceMismatch"));
|
|
446
|
+
}
|
|
447
|
+
const status = issues.length > 0 ? "warn" : "ok";
|
|
448
|
+
lines.push(row(statusTag(status), label, msg("endpoint.summary", {
|
|
449
|
+
instanceId: endpoint.instanceId,
|
|
450
|
+
kind: endpoint.kind,
|
|
451
|
+
port,
|
|
452
|
+
reachable: reachable ? msg("yes") : msg("no"),
|
|
453
|
+
})));
|
|
454
|
+
lines.push(detail(msg("endpoint.owner", {
|
|
455
|
+
ownerPid: endpoint.bridgeOwnerPid ?? msg("none"),
|
|
456
|
+
ownerAlive: formatAlive(bridgeOwnerAlive),
|
|
457
|
+
companionPid: endpoint.companionPid ?? msg("none"),
|
|
458
|
+
companionAlive: formatAlive(companionAlive),
|
|
459
|
+
status: endpoint.companionStatus ?? msg("unknown"),
|
|
460
|
+
})));
|
|
461
|
+
for (const issue of issues) {
|
|
462
|
+
lines.push(detail(msg("issue", { issue })));
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
async function appendWorkspaceEndpointLines(lines, options, lock, deps) {
|
|
466
|
+
const adapters = chooseEndpointAdapters(options, lock);
|
|
467
|
+
const seenEndpoints = new Set();
|
|
468
|
+
for (const adapter of adapters) {
|
|
469
|
+
const endpoint = deps.readLocalCompanionEndpoint(options.cwd, adapter);
|
|
470
|
+
if (endpoint) {
|
|
471
|
+
seenEndpoints.add(`${endpoint.kind}:${endpoint.instanceId}:${endpoint.port}`);
|
|
472
|
+
}
|
|
473
|
+
await appendEndpointSummary(lines, msg("label.workspaceEndpoint", { adapter }), endpoint, { ...options, adapter }, lock, deps);
|
|
474
|
+
}
|
|
475
|
+
const legacyEndpoint = deps.readLocalCompanionEndpoint(options.cwd);
|
|
476
|
+
const legacyLabel = msg("label.legacyEndpoint");
|
|
477
|
+
if (!legacyEndpoint) {
|
|
478
|
+
lines.push(row(STATE_OK, legacyLabel, msg("none")));
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
const legacyKey = `${legacyEndpoint.kind}:${legacyEndpoint.instanceId}:${legacyEndpoint.port}`;
|
|
482
|
+
if (seenEndpoints.has(legacyKey)) {
|
|
483
|
+
lines.push(row(STATE_OK, legacyLabel, msg("endpoint.legacySame")));
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
await appendEndpointSummary(lines, legacyLabel, legacyEndpoint, options, lock, deps);
|
|
487
|
+
}
|
|
488
|
+
export async function buildDoctorReport(options = {}, rawDeps = {}) {
|
|
489
|
+
const deps = resolveDeps(rawDeps);
|
|
490
|
+
const parsed = parseDoctorCliArgs(options.argv ?? process.argv.slice(2), options.mode ?? "generic", options.cwd ?? process.cwd());
|
|
491
|
+
const doctorOptions = {
|
|
492
|
+
...parsed,
|
|
493
|
+
cwd: path.resolve(options.cwd ?? parsed.cwd),
|
|
494
|
+
adapter: options.adapter ?? parsed.adapter,
|
|
495
|
+
};
|
|
496
|
+
const lines = [];
|
|
497
|
+
lines.push(msg("title"));
|
|
498
|
+
section(lines, msg("section.environment"));
|
|
499
|
+
lines.push(row(STATE_OK, msg("label.node"), deps.nodeVersion));
|
|
500
|
+
lines.push(row(STATE_OK, msg("label.platform"), `${deps.platform}-${deps.arch}`));
|
|
501
|
+
if (deps.platform === "win32") {
|
|
502
|
+
const release = deps.osRelease();
|
|
503
|
+
const build = parseInt(release.split(".").pop() ?? "0", 10);
|
|
504
|
+
const ok = build >= 18309;
|
|
505
|
+
lines.push(row(ok ? STATE_OK : STATE_FAIL, msg("label.winBuild"), ok ? String(build) : `${build} ${msg("winBuildConpty")}`));
|
|
506
|
+
}
|
|
507
|
+
try {
|
|
508
|
+
await deps.loadNodePty();
|
|
509
|
+
lines.push(row(STATE_OK, msg("label.nodePty"), msg("nodePty.loaded")));
|
|
510
|
+
}
|
|
511
|
+
catch (error) {
|
|
512
|
+
lines.push(row(STATE_FAIL, msg("label.nodePty"), shortError(error)));
|
|
513
|
+
lines.push(detail(msg("nodePty.fix", { fix: getPlatformPtyFix(deps.platform) })));
|
|
514
|
+
}
|
|
515
|
+
appendCliLines(lines, doctorOptions, deps);
|
|
516
|
+
section(lines, msg("section.data"));
|
|
517
|
+
const dataDir = deps.resolveDataDir();
|
|
518
|
+
const dataDirExists = deps.exists(dataDir);
|
|
519
|
+
lines.push(row(dataDirExists ? STATE_OK : STATE_FAIL, msg("label.dataDir"), dataDirExists ? dataDir : `${dataDir} ${msg("dataDirMissing")}`));
|
|
520
|
+
const credFile = path.join(dataDir, "account.json");
|
|
521
|
+
const credExists = deps.exists(credFile);
|
|
522
|
+
lines.push(row(credExists ? STATE_OK : STATE_FAIL, msg("label.credentials"), credExists ? msg("credentialsFound") : msg("credentialsMissing")));
|
|
523
|
+
section(lines, msg("section.runtime"));
|
|
524
|
+
lines.push(fieldRow(msg("label.mode"), formatMode(doctorOptions.mode)));
|
|
525
|
+
lines.push(fieldRow(msg("label.cwd"), doctorOptions.cwd));
|
|
526
|
+
lines.push(fieldRow(msg("label.adapter"), doctorOptions.adapter ?? msg("notSpecified")));
|
|
527
|
+
lines.push("");
|
|
528
|
+
await appendDaemonLines(lines, doctorOptions, dataDir, deps);
|
|
529
|
+
const lock = appendBridgeLockLines(lines, doctorOptions, dataDir, deps);
|
|
530
|
+
await appendWorkspaceEndpointLines(lines, doctorOptions, lock, deps);
|
|
531
|
+
lines.push("");
|
|
532
|
+
return lines;
|
|
533
|
+
}
|
|
534
|
+
export async function runDoctorCheck(argv = process.argv.slice(2), options = {}) {
|
|
535
|
+
const lines = await buildDoctorReport({
|
|
536
|
+
argv,
|
|
537
|
+
mode: options.mode ?? "generic",
|
|
538
|
+
});
|
|
539
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
540
|
+
}
|
|
@@ -3,6 +3,7 @@ import fs from "node:fs";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { initLocaleFromEnv } from "../i18n/index.js";
|
|
6
7
|
const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
7
8
|
const PROJECT_DIR = path.resolve(MODULE_DIR, "..", "..");
|
|
8
9
|
export const DEFAULT_BASE_URL = process.env.WECHAT_ILINK_BASE_URL?.trim() || "https://ilinkai.weixin.qq.com";
|
|
@@ -24,6 +25,7 @@ export const WORKSPACES_DIR = path.join(CHANNEL_DATA_DIR, "workspaces");
|
|
|
24
25
|
export const INBOUND_MESSAGE_CLAIMS_DIR = path.join(CHANNEL_DATA_DIR, "inbound-message-claims");
|
|
25
26
|
export const INBOUND_ATTACHMENTS_DIR = path.join(CHANNEL_DATA_DIR, "inbound-attachments");
|
|
26
27
|
export const EMOJI_BINDINGS_FILE = path.join(CHANNEL_DATA_DIR, "emoji-bindings.json");
|
|
28
|
+
initLocaleFromEnv();
|
|
27
29
|
const LEGACY_GLOBAL_CHANNEL_DATA_DIR = path.join(os.homedir(), ".claude", "channels", "wechat");
|
|
28
30
|
const LEGACY_REPO_CHANNEL_DATA_DIR = path.join(PROJECT_DIR, "~", ".claude", "channels", "wechat");
|
|
29
31
|
const LEGACY_ENV_CHANNEL_DATA_DIR = process.env.CLAUDE_WECHAT_CHANNEL_DATA_DIR?.trim()
|