claude-codex-wechat 0.1.19 → 0.1.20
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/dist/server/cli.js +443 -80
- package/dist/web/assets/index-48whrxLU.js +12 -0
- package/dist/web/index.html +1 -1
- package/package.json +2 -1
- package/dist/web/assets/index-DxiX_R2K.js +0 -10
package/dist/server/cli.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
import { createRequire as __cjsCreateRequire } from 'node:module'; const require = __cjsCreateRequire(import.meta.url);
|
|
3
3
|
|
|
4
4
|
// src/cli.ts
|
|
5
|
-
import { existsSync as existsSync10, readFileSync as
|
|
6
|
-
import { dirname as
|
|
7
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { existsSync as existsSync10, readFileSync as readFileSync9 } from "node:fs";
|
|
6
|
+
import { dirname as dirname13, join as join13 } from "node:path";
|
|
7
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
8
8
|
|
|
9
9
|
// src/daemon/bootstrap.ts
|
|
10
10
|
import { networkInterfaces } from "node:os";
|
|
@@ -81,9 +81,24 @@ function normalizeBridgeConfig(raw, env, path) {
|
|
|
81
81
|
wechat: normalizeWechatConfig(record.wechat, env),
|
|
82
82
|
bridge: normalizeBridgeDefaultsConfig(record.bridge),
|
|
83
83
|
tunnel: normalizeTunnelConfig(record.tunnel),
|
|
84
|
+
update: normalizeUpdateStatusConfig(record.update),
|
|
84
85
|
providers: normalizeProvidersConfig(record.providers, env)
|
|
85
86
|
};
|
|
86
87
|
}
|
|
88
|
+
function normalizeUpdateStatusConfig(raw) {
|
|
89
|
+
const record = raw && typeof raw === "object" ? raw : {};
|
|
90
|
+
const currentVersion = typeof record.currentVersion === "string" && record.currentVersion ? record.currentVersion : void 0;
|
|
91
|
+
const latestVersion = typeof record.latestVersion === "string" && record.latestVersion ? record.latestVersion : void 0;
|
|
92
|
+
const lastCheckedAt = typeof record.lastCheckedAt === "number" && Number.isFinite(record.lastCheckedAt) ? record.lastCheckedAt : void 0;
|
|
93
|
+
const updateAvailable = record.updateAvailable === true;
|
|
94
|
+
if (!currentVersion && !latestVersion && lastCheckedAt === void 0 && !updateAvailable) return void 0;
|
|
95
|
+
return {
|
|
96
|
+
...currentVersion ? { currentVersion } : {},
|
|
97
|
+
...latestVersion ? { latestVersion } : {},
|
|
98
|
+
updateAvailable,
|
|
99
|
+
...lastCheckedAt !== void 0 ? { lastCheckedAt } : {}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
87
102
|
function normalizeProvidersConfig(raw, env) {
|
|
88
103
|
const record = raw && typeof raw === "object" ? raw : {};
|
|
89
104
|
const claude = normalizeProviderCommand(record.claude, env.BRIDGE_CLAUDE_COMMAND);
|
|
@@ -164,6 +179,21 @@ async function deleteConfigFile(configPath) {
|
|
|
164
179
|
throw error;
|
|
165
180
|
}
|
|
166
181
|
}
|
|
182
|
+
function persistUpdateStatusToConfigFile(input) {
|
|
183
|
+
const currentConfig = readConfigFileSync(input.configPath);
|
|
184
|
+
const nextConfig = {
|
|
185
|
+
...currentConfig,
|
|
186
|
+
update: {
|
|
187
|
+
...input.status.currentVersion ? { currentVersion: input.status.currentVersion } : {},
|
|
188
|
+
...input.status.latestVersion ? { latestVersion: input.status.latestVersion } : {},
|
|
189
|
+
updateAvailable: input.status.updateAvailable,
|
|
190
|
+
...input.status.lastCheckedAt !== void 0 ? { lastCheckedAt: input.status.lastCheckedAt } : {}
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
mkdirSync(dirname(input.configPath), { recursive: true });
|
|
194
|
+
writeFileSync(input.configPath, `${JSON.stringify(nextConfig, null, 2)}
|
|
195
|
+
`, "utf8");
|
|
196
|
+
}
|
|
167
197
|
async function persistBridgeDefaultsToConfigFile(input) {
|
|
168
198
|
const currentConfig = await readConfigFile(input.configPath);
|
|
169
199
|
const currentBridge = isRecord(currentConfig.bridge) ? currentConfig.bridge : void 0;
|
|
@@ -2319,7 +2349,7 @@ var WeixinOutboundGate = class _WeixinOutboundGate {
|
|
|
2319
2349
|
};
|
|
2320
2350
|
|
|
2321
2351
|
// src/providers/claude-code/claudeStreamingRunner.ts
|
|
2322
|
-
import { spawn as
|
|
2352
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
2323
2353
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
2324
2354
|
|
|
2325
2355
|
// src/shared/platform.ts
|
|
@@ -2384,14 +2414,129 @@ function statePath(filename) {
|
|
|
2384
2414
|
return join8(tmpdir(), filename);
|
|
2385
2415
|
}
|
|
2386
2416
|
|
|
2417
|
+
// src/providers/claude-code/assistantContent.ts
|
|
2418
|
+
function extractAssistantBlocks(message) {
|
|
2419
|
+
if (!message || typeof message !== "object") return [];
|
|
2420
|
+
const content = message.content;
|
|
2421
|
+
if (!Array.isArray(content)) return [];
|
|
2422
|
+
return content.flatMap((item) => {
|
|
2423
|
+
if (!item || typeof item !== "object") return [];
|
|
2424
|
+
const record = item;
|
|
2425
|
+
if (record.type === "text" && typeof record.text === "string") {
|
|
2426
|
+
return [{ type: "text", text: record.text }];
|
|
2427
|
+
}
|
|
2428
|
+
if (record.type === "tool_use" && record.name === "AskUserQuestion") {
|
|
2429
|
+
const rendered = renderAskUserQuestion(record.input);
|
|
2430
|
+
return rendered ? [{ type: "choice", ...rendered }] : [];
|
|
2431
|
+
}
|
|
2432
|
+
return [];
|
|
2433
|
+
});
|
|
2434
|
+
}
|
|
2435
|
+
function renderAskUserQuestion(input) {
|
|
2436
|
+
if (!input || typeof input !== "object") return null;
|
|
2437
|
+
const questions = input.questions;
|
|
2438
|
+
if (!Array.isArray(questions) || questions.length === 0) return null;
|
|
2439
|
+
const singleQuestion = questions.length === 1;
|
|
2440
|
+
const blocks = [];
|
|
2441
|
+
let mappedLabels = [];
|
|
2442
|
+
let mappedMultiSelect = false;
|
|
2443
|
+
for (const raw of questions) {
|
|
2444
|
+
if (!raw || typeof raw !== "object") continue;
|
|
2445
|
+
const q = raw;
|
|
2446
|
+
const question = typeof q.question === "string" ? q.question.trim() : "";
|
|
2447
|
+
if (!question) continue;
|
|
2448
|
+
const multiSelect = q.multiSelect === true;
|
|
2449
|
+
const lines = [`\u2753 ${question}`];
|
|
2450
|
+
const labels = [];
|
|
2451
|
+
const options = Array.isArray(q.options) ? q.options : [];
|
|
2452
|
+
for (const rawOption of options) {
|
|
2453
|
+
if (!rawOption || typeof rawOption !== "object") continue;
|
|
2454
|
+
const option = rawOption;
|
|
2455
|
+
const label = typeof option.label === "string" ? option.label.trim() : "";
|
|
2456
|
+
if (!label) continue;
|
|
2457
|
+
const description = typeof option.description === "string" ? option.description.trim() : "";
|
|
2458
|
+
labels.push(label);
|
|
2459
|
+
const bullet = singleQuestion ? `${labels.length}.` : "\u30FB";
|
|
2460
|
+
lines.push(description ? `${bullet} ${label} \u2014\u2014 ${description}` : `${bullet} ${label}`);
|
|
2461
|
+
}
|
|
2462
|
+
if (!singleQuestion) {
|
|
2463
|
+
lines.push("\uFF08\u56DE\u590D\u5BF9\u5E94\u9009\u9879\u7684\u6587\u5B57\u5373\u53EF\uFF09");
|
|
2464
|
+
} else {
|
|
2465
|
+
lines.push(multiSelect ? "\uFF08\u53EF\u591A\u9009\uFF0C\u56DE\u590D\u5E8F\u53F7\u6216\u9009\u9879\u6587\u5B57\uFF0C\u591A\u4E2A\u7528\u9017\u53F7\u5206\u9694\uFF0C\u5982 1,2\uFF09" : "\uFF08\u56DE\u590D\u5E8F\u53F7\u6216\u9009\u9879\u6587\u5B57\u5373\u53EF\uFF0C\u5982 1\uFF09");
|
|
2466
|
+
}
|
|
2467
|
+
blocks.push(lines.join("\n"));
|
|
2468
|
+
if (singleQuestion) {
|
|
2469
|
+
mappedLabels = labels;
|
|
2470
|
+
mappedMultiSelect = multiSelect;
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2473
|
+
if (blocks.length === 0) return null;
|
|
2474
|
+
return { text: blocks.join("\n\n"), labels: mappedLabels, multiSelect: mappedMultiSelect };
|
|
2475
|
+
}
|
|
2476
|
+
function mapChoiceReply(text, labels, multiSelect) {
|
|
2477
|
+
if (labels.length === 0) return null;
|
|
2478
|
+
const normalized = text.trim().replace(/[0-9]/g, (d) => String.fromCharCode(d.charCodeAt(0) - 65296 + 48));
|
|
2479
|
+
if (!normalized) return null;
|
|
2480
|
+
const tokens = normalized.split(/[\s,,、]+/).filter(Boolean);
|
|
2481
|
+
if (tokens.length === 0) return null;
|
|
2482
|
+
if (!multiSelect && tokens.length > 1) return null;
|
|
2483
|
+
const picked = [];
|
|
2484
|
+
for (const token of tokens) {
|
|
2485
|
+
const digits = token.replace(/[.。))::]+$/, "");
|
|
2486
|
+
if (!/^\d+$/.test(digits)) return null;
|
|
2487
|
+
const index = Number(digits);
|
|
2488
|
+
if (index < 1 || index > labels.length) return null;
|
|
2489
|
+
const label = labels[index - 1];
|
|
2490
|
+
if (!picked.includes(label)) picked.push(label);
|
|
2491
|
+
}
|
|
2492
|
+
return picked.length > 0 ? picked.join(", ") : null;
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2495
|
+
// src/providers/claude-code/bridgeSystemPrompt.ts
|
|
2496
|
+
var BRIDGE_APPEND_SYSTEM_PROMPT = [
|
|
2497
|
+
"\u4F60\u6B63\u8FD0\u884C\u5728\u4E00\u4E2A\u5FAE\u4FE1\u7EAF\u6587\u672C\u6865\u63A5\u73AF\u5883\u4E2D,\u7528\u6237\u53EA\u80FD\u6536\u53D1\u6587\u672C\u6D88\u606F,\u6CA1\u6709\u4EFB\u4F55\u4EA4\u4E92\u5F0F\u5F39\u7A97\u6216\u6309\u94AE\u3002",
|
|
2498
|
+
"\u5F53\u4F60\u9700\u8981\u7528\u6237\u505A\u9009\u62E9\u65F6,\u8BF7\u76F4\u63A5\u5728\u56DE\u590D\u91CC\u7528\u5E26\u5E8F\u53F7\u7684\u5217\u8868\u5217\u51FA\u9009\u9879(\u4F8B\u5982\u300C1. \u9009\u9879A\u300D\u300C2. \u9009\u9879B\u300D),",
|
|
2499
|
+
"\u5E76\u544A\u8BC9\u7528\u6237\u56DE\u590D\u5E8F\u53F7\u6216\u9009\u9879\u6587\u5B57\u5373\u53EF;\u4E0D\u8981\u8C03\u7528 AskUserQuestion \u5DE5\u5177(\u5B83\u5728\u6B64\u73AF\u5883\u4E2D\u65E0\u6CD5\u4EA4\u4E92)\u3002"
|
|
2500
|
+
].join("");
|
|
2501
|
+
|
|
2502
|
+
// src/providers/claude-code/claudeCapabilities.ts
|
|
2503
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
2504
|
+
var cache = /* @__PURE__ */ new Map();
|
|
2505
|
+
var probeAppendSystemPromptSupport = (command) => {
|
|
2506
|
+
const cached = cache.get(command);
|
|
2507
|
+
if (cached) return cached;
|
|
2508
|
+
const result = runHelpProbe(command);
|
|
2509
|
+
cache.set(command, result);
|
|
2510
|
+
return result;
|
|
2511
|
+
};
|
|
2512
|
+
function runHelpProbe(command) {
|
|
2513
|
+
return new Promise((resolve2) => {
|
|
2514
|
+
try {
|
|
2515
|
+
const child = spawn2(command, ["--help"], { stdio: ["ignore", "pipe", "pipe"], shell: useShellForCli() });
|
|
2516
|
+
let output = "";
|
|
2517
|
+
const collect = (data) => {
|
|
2518
|
+
output += String(data);
|
|
2519
|
+
};
|
|
2520
|
+
child.stdout?.on("data", collect);
|
|
2521
|
+
child.stderr?.on("data", collect);
|
|
2522
|
+
child.on("error", () => resolve2(false));
|
|
2523
|
+
child.on("close", () => resolve2(output.includes("--append-system-prompt")));
|
|
2524
|
+
} catch {
|
|
2525
|
+
resolve2(false);
|
|
2526
|
+
}
|
|
2527
|
+
});
|
|
2528
|
+
}
|
|
2529
|
+
|
|
2387
2530
|
// src/providers/claude-code/claudeStreamingRunner.ts
|
|
2388
2531
|
var ClaudeStreamingRunner = class {
|
|
2389
2532
|
sessions = /* @__PURE__ */ new Map();
|
|
2390
2533
|
command;
|
|
2391
2534
|
spawner;
|
|
2535
|
+
capabilityProbe;
|
|
2392
2536
|
constructor(input = {}) {
|
|
2393
2537
|
this.command = input.command ?? "claude";
|
|
2394
2538
|
this.spawner = input.spawner ?? defaultClaudeStreamSpawner;
|
|
2539
|
+
this.capabilityProbe = input.capabilityProbe ?? probeAppendSystemPromptSupport;
|
|
2395
2540
|
}
|
|
2396
2541
|
async startSession(input) {
|
|
2397
2542
|
const resumeId = input.options?.providerSessionId;
|
|
@@ -2412,6 +2557,9 @@ var ClaudeStreamingRunner = class {
|
|
|
2412
2557
|
async *sendMessage(input) {
|
|
2413
2558
|
const session = this.sessions.get(input.bridgeSessionId);
|
|
2414
2559
|
if (!session) throw new Error(`claude_session_not_found:${input.bridgeSessionId}`);
|
|
2560
|
+
if (session.supportsAppendSystemPrompt === void 0) {
|
|
2561
|
+
session.supportsAppendSystemPrompt = await this.capabilityProbe(this.command);
|
|
2562
|
+
}
|
|
2415
2563
|
const handle = this.ensureHandle(session);
|
|
2416
2564
|
session.pendingFollowUps = 0;
|
|
2417
2565
|
handle.write(userEnvelope(input.text));
|
|
@@ -2448,8 +2596,11 @@ var ClaudeStreamingRunner = class {
|
|
|
2448
2596
|
}
|
|
2449
2597
|
if (type === "assistant") {
|
|
2450
2598
|
let emitted = false;
|
|
2451
|
-
for (const
|
|
2452
|
-
yield { type: "text_delta", text };
|
|
2599
|
+
for (const block of extractAssistantBlocks(record.message)) {
|
|
2600
|
+
yield { type: "text_delta", text: block.text };
|
|
2601
|
+
if (block.type === "choice" && block.labels.length > 0) {
|
|
2602
|
+
yield { type: "choice_prompt", labels: block.labels, multiSelect: block.multiSelect };
|
|
2603
|
+
}
|
|
2453
2604
|
emitted = true;
|
|
2454
2605
|
}
|
|
2455
2606
|
if (emitted) yield { type: "message_done" };
|
|
@@ -2510,6 +2661,7 @@ function buildStreamingArgs(session) {
|
|
|
2510
2661
|
"--include-partial-messages",
|
|
2511
2662
|
"--verbose",
|
|
2512
2663
|
"--dangerously-skip-permissions",
|
|
2664
|
+
...session.supportsAppendSystemPrompt ? ["--append-system-prompt", BRIDGE_APPEND_SYSTEM_PROMPT] : [],
|
|
2513
2665
|
"--replay-user-messages"
|
|
2514
2666
|
];
|
|
2515
2667
|
if (session.resumeId) {
|
|
@@ -2528,16 +2680,6 @@ function userEnvelope(text) {
|
|
|
2528
2680
|
return `${JSON.stringify({ type: "user", message: { role: "user", content: text }, parent_tool_use_id: null })}
|
|
2529
2681
|
`;
|
|
2530
2682
|
}
|
|
2531
|
-
function extractAssistantText(message) {
|
|
2532
|
-
if (!message || typeof message !== "object") return [];
|
|
2533
|
-
const content = message.content;
|
|
2534
|
-
if (!Array.isArray(content)) return [];
|
|
2535
|
-
return content.flatMap((item) => {
|
|
2536
|
-
if (!item || typeof item !== "object") return [];
|
|
2537
|
-
const record = item;
|
|
2538
|
-
return record.type === "text" && typeof record.text === "string" ? [record.text] : [];
|
|
2539
|
-
});
|
|
2540
|
-
}
|
|
2541
2683
|
function extractError(record) {
|
|
2542
2684
|
if (typeof record.message === "string") return record.message;
|
|
2543
2685
|
if (record.error && typeof record.error === "object") {
|
|
@@ -2554,7 +2696,7 @@ function parseJsonLine(line) {
|
|
|
2554
2696
|
}
|
|
2555
2697
|
}
|
|
2556
2698
|
function defaultClaudeStreamSpawner(call) {
|
|
2557
|
-
const child =
|
|
2699
|
+
const child = spawn3(call.command, call.args, { cwd: expandTilde(call.cwd) ?? call.cwd, stdio: ["pipe", "pipe", "pipe"], shell: useShellForCli() });
|
|
2558
2700
|
let stderr = "";
|
|
2559
2701
|
let buffer = "";
|
|
2560
2702
|
const queue = [];
|
|
@@ -2653,7 +2795,7 @@ var ClaudeCodeProvider = class {
|
|
|
2653
2795
|
};
|
|
2654
2796
|
|
|
2655
2797
|
// src/providers/codex/codexAppServerClient.ts
|
|
2656
|
-
import { spawn as
|
|
2798
|
+
import { spawn as spawn4 } from "node:child_process";
|
|
2657
2799
|
var CodexAppServerClient = class {
|
|
2658
2800
|
child;
|
|
2659
2801
|
pending = /* @__PURE__ */ new Map();
|
|
@@ -2664,7 +2806,7 @@ var CodexAppServerClient = class {
|
|
|
2664
2806
|
buffer = "";
|
|
2665
2807
|
constructor(input) {
|
|
2666
2808
|
const command = input.command ?? "codex";
|
|
2667
|
-
this.child =
|
|
2809
|
+
this.child = spawn4(command, ["app-server", "--listen", "stdio://"], {
|
|
2668
2810
|
cwd: input.cwd,
|
|
2669
2811
|
stdio: ["pipe", "pipe", "pipe"],
|
|
2670
2812
|
shell: useShellForCli()
|
|
@@ -3071,10 +3213,10 @@ function createDefaultProviders(input = {}) {
|
|
|
3071
3213
|
}
|
|
3072
3214
|
|
|
3073
3215
|
// src/providers/claude-code/claudeDetection.ts
|
|
3074
|
-
import { spawn as
|
|
3216
|
+
import { spawn as spawn5 } from "node:child_process";
|
|
3075
3217
|
async function defaultCommandRunner(command, args) {
|
|
3076
3218
|
return await new Promise((resolve2) => {
|
|
3077
|
-
const child =
|
|
3219
|
+
const child = spawn5(command, args, { stdio: ["ignore", "pipe", "pipe"], shell: useShellForCli() });
|
|
3078
3220
|
let stdout = "";
|
|
3079
3221
|
let stderr = "";
|
|
3080
3222
|
child.stdout.on("data", (chunk) => {
|
|
@@ -3107,10 +3249,10 @@ ${result.stderr}`) };
|
|
|
3107
3249
|
}
|
|
3108
3250
|
|
|
3109
3251
|
// src/providers/codex/codexDetection.ts
|
|
3110
|
-
import { spawn as
|
|
3252
|
+
import { spawn as spawn6 } from "node:child_process";
|
|
3111
3253
|
async function defaultCodexCommandRunner(command, args) {
|
|
3112
3254
|
return await new Promise((resolve2) => {
|
|
3113
|
-
const child =
|
|
3255
|
+
const child = spawn6(command, args, { stdio: ["ignore", "pipe", "pipe"], shell: useShellForCli() });
|
|
3114
3256
|
let stdout = "";
|
|
3115
3257
|
let stderr = "";
|
|
3116
3258
|
child.stdout.on("data", (chunk) => {
|
|
@@ -3377,6 +3519,10 @@ var MessageRouter = class _MessageRouter {
|
|
|
3377
3519
|
opSeq = 0;
|
|
3378
3520
|
latestMutatingSeq = /* @__PURE__ */ new Map();
|
|
3379
3521
|
activeGenerations = /* @__PURE__ */ new Map();
|
|
3522
|
+
// 最近一次 AskUserQuestion 的选项,按 chatId 暂存,并记住是哪个 bridge 会话发出的。
|
|
3523
|
+
// 下一条入站消息若是纯序号,用它翻译成选项文字再转发(消费一次即清除);不是序号、
|
|
3524
|
+
// 或当前会话已变(sessionId 不匹配)则原样透传,避免旧选项串到别的会话。
|
|
3525
|
+
pendingChoices = /* @__PURE__ */ new Map();
|
|
3380
3526
|
get conversation() {
|
|
3381
3527
|
return this.options.conversation;
|
|
3382
3528
|
}
|
|
@@ -3452,12 +3598,24 @@ var MessageRouter = class _MessageRouter {
|
|
|
3452
3598
|
if ((this.latestMutatingSeq.get(message.chatId) ?? 0) > seq) {
|
|
3453
3599
|
return { status: "accepted" };
|
|
3454
3600
|
}
|
|
3455
|
-
|
|
3601
|
+
const chatText = this.resolveChoiceReply(message.chatId, command.text);
|
|
3602
|
+
if (await this.maybeSteer(message.chatId, chatText)) {
|
|
3456
3603
|
return { status: "accepted" };
|
|
3457
3604
|
}
|
|
3458
|
-
await this.runExclusive(() => this.runChatGeneration(message, user,
|
|
3605
|
+
await this.runExclusive(() => this.runChatGeneration(message, user, chatText, seq));
|
|
3459
3606
|
return { status: "accepted" };
|
|
3460
3607
|
}
|
|
3608
|
+
// 消费一次上一轮暂存的 AskUserQuestion 选项:纯序号回复翻译成对应选项文字,
|
|
3609
|
+
// 其它情况原样透传。取用后即清除,避免过期映射误伤后续消息。若暂存所属会话
|
|
3610
|
+
// 与当前会话不一致(用户已 /new、/resume 或切换 provider),也一律不翻译,
|
|
3611
|
+
// 防止旧会话的选项串到新会话。
|
|
3612
|
+
resolveChoiceReply(chatId, text) {
|
|
3613
|
+
const pending = this.pendingChoices.get(chatId);
|
|
3614
|
+
if (!pending) return text;
|
|
3615
|
+
this.pendingChoices.delete(chatId);
|
|
3616
|
+
if (pending.sessionId !== this.conversation.getCurrent()?.id) return text;
|
|
3617
|
+
return mapChoiceReply(text, pending.labels, pending.multiSelect) ?? text;
|
|
3618
|
+
}
|
|
3461
3619
|
// A chat message arriving while a turn is in flight: if the provider supports
|
|
3462
3620
|
// native steer (Codex turn/steer), inject it into the running turn instead of
|
|
3463
3621
|
// queueing a new turn. Otherwise fall back to the serialized lock (queue).
|
|
@@ -3584,6 +3742,9 @@ var MessageRouter = class _MessageRouter {
|
|
|
3584
3742
|
await this.sendToChat({ chatId: message.chatId, kind: "text", text: bufferedText });
|
|
3585
3743
|
bufferedText = "";
|
|
3586
3744
|
}
|
|
3745
|
+
if (event.type === "choice_prompt" && event.labels.length > 0) {
|
|
3746
|
+
this.pendingChoices.set(message.chatId, { sessionId: session.id, labels: event.labels, multiSelect: event.multiSelect });
|
|
3747
|
+
}
|
|
3587
3748
|
if (event.type === "session_state") {
|
|
3588
3749
|
const updated = this.conversation.update({
|
|
3589
3750
|
providerSessionId: event.state.providerSessionId,
|
|
@@ -3655,6 +3816,7 @@ var MessageRouter = class _MessageRouter {
|
|
|
3655
3816
|
return;
|
|
3656
3817
|
}
|
|
3657
3818
|
if (command.kind === "new_session") {
|
|
3819
|
+
this.pendingChoices.delete(chatId);
|
|
3658
3820
|
await this.preemptActiveGeneration(chatId);
|
|
3659
3821
|
const providerId = command.providerId ?? this.conversation.getCurrent()?.providerId ?? this.options.defaults?.defaultProvider ?? "claude-code";
|
|
3660
3822
|
const cwd = command.cwd ?? this.conversation.getCurrent()?.cwd ?? this.options.defaults?.defaultWorkspace ?? defaultWorkspaceDir();
|
|
@@ -3720,6 +3882,7 @@ var MessageRouter = class _MessageRouter {
|
|
|
3720
3882
|
return;
|
|
3721
3883
|
}
|
|
3722
3884
|
if (command.kind === "resume_session") {
|
|
3885
|
+
this.pendingChoices.delete(chatId);
|
|
3723
3886
|
const ref = command.ref.trim();
|
|
3724
3887
|
if (!ref) {
|
|
3725
3888
|
await this.sendToChat({ chatId, kind: "status", text: "\u7528\u6CD5\uFF1A/resume <\u7F16\u53F7>" });
|
|
@@ -3983,6 +4146,7 @@ var BridgeEventHub = class {
|
|
|
3983
4146
|
};
|
|
3984
4147
|
|
|
3985
4148
|
// src/runtime/relayTunnelProvider.ts
|
|
4149
|
+
var WS_OPEN = 1;
|
|
3986
4150
|
var RelayTunnelProvider = class {
|
|
3987
4151
|
constructor(options) {
|
|
3988
4152
|
this.options = options;
|
|
@@ -3997,6 +4161,9 @@ var RelayTunnelProvider = class {
|
|
|
3997
4161
|
stopped = true;
|
|
3998
4162
|
reconnectAttempts = 0;
|
|
3999
4163
|
reconnectTimer = null;
|
|
4164
|
+
// 能力协商结果:仅当服务器在 registered 回包里声明 streaming:true 时才走流式,
|
|
4165
|
+
// 否则退回缓冲(兼容老服务器)。
|
|
4166
|
+
serverSupportsStreaming = false;
|
|
4000
4167
|
static defaultCreateSocket(url) {
|
|
4001
4168
|
const socket = new WebSocket(url);
|
|
4002
4169
|
return {
|
|
@@ -4062,7 +4229,10 @@ var RelayTunnelProvider = class {
|
|
|
4062
4229
|
type: "register",
|
|
4063
4230
|
clientVersion: "0.1.0",
|
|
4064
4231
|
targetBaseUrl: this.options.targetBaseUrl,
|
|
4065
|
-
authToken: this.options.authToken
|
|
4232
|
+
authToken: this.options.authToken,
|
|
4233
|
+
// 声明支持流式转发。新服务器会在 registered 回包里回 streaming:true;
|
|
4234
|
+
// 老服务器忽略此字段,客户端据此自动退回缓冲。
|
|
4235
|
+
supportsStreaming: true
|
|
4066
4236
|
}));
|
|
4067
4237
|
});
|
|
4068
4238
|
socket.on("message", (raw) => {
|
|
@@ -4081,6 +4251,7 @@ var RelayTunnelProvider = class {
|
|
|
4081
4251
|
}
|
|
4082
4252
|
if (payload?.type === "registered") {
|
|
4083
4253
|
this.reconnectAttempts = 0;
|
|
4254
|
+
this.serverSupportsStreaming = payload?.streaming === true;
|
|
4084
4255
|
const publicUrl = typeof payload?.publicUrl === "string" && payload.publicUrl ? payload.publicUrl : typeof payload?.token === "string" ? `${derivePublicBaseUrl(this.options.serverUrl)}/${payload.token}` : void 0;
|
|
4085
4256
|
this.status = {
|
|
4086
4257
|
installed: true,
|
|
@@ -4092,7 +4263,10 @@ var RelayTunnelProvider = class {
|
|
|
4092
4263
|
return;
|
|
4093
4264
|
}
|
|
4094
4265
|
if (payload?.type === "request") {
|
|
4095
|
-
|
|
4266
|
+
const requestId = typeof payload?.requestId === "string" ? payload.requestId : "unknown";
|
|
4267
|
+
void this.handleRequest(payload).catch((error) => {
|
|
4268
|
+
console.warn(`[relay] \u5904\u7406\u8BF7\u6C42 ${requestId} \u5931\u8D25\uFF1A`, error instanceof Error ? error.message : String(error));
|
|
4269
|
+
});
|
|
4096
4270
|
}
|
|
4097
4271
|
});
|
|
4098
4272
|
socket.on("close", () => {
|
|
@@ -4139,22 +4313,91 @@ var RelayTunnelProvider = class {
|
|
|
4139
4313
|
}, delay3);
|
|
4140
4314
|
this.reconnectTimer.unref?.();
|
|
4141
4315
|
}
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4316
|
+
// 只在连接就绪时发送;未就绪(重连中/尚未 OPEN)时静默跳过(属正常态,不记日志、
|
|
4317
|
+
// 不关连接,以免误伤正在进行的重连)。底层 send 真抛错说明 socket 已坏:记一条 warn,
|
|
4318
|
+
// 并主动 close() 让 close 事件驱动重连、由服务端 WS close 清理它那边的挂起请求。
|
|
4319
|
+
safeSend(payload) {
|
|
4320
|
+
const socket = this.socket;
|
|
4321
|
+
if (!socket) return false;
|
|
4322
|
+
if (socket.readyState !== void 0 && socket.readyState !== WS_OPEN) return false;
|
|
4323
|
+
try {
|
|
4324
|
+
socket.send(payload);
|
|
4325
|
+
return true;
|
|
4326
|
+
} catch (error) {
|
|
4327
|
+
console.warn("[relay] \u56DE\u4F20\u53D1\u9001\u5931\u8D25\uFF0C\u56DE\u6536\u8FDE\u63A5\u4EE5\u91CD\u8FDE\uFF1A", error instanceof Error ? error.message : String(error));
|
|
4328
|
+
try {
|
|
4329
|
+
socket.close();
|
|
4330
|
+
} catch {
|
|
4331
|
+
}
|
|
4332
|
+
return false;
|
|
4333
|
+
}
|
|
4334
|
+
}
|
|
4335
|
+
// 尽力给服务端回一个错误响应,让它立即结束这单挂起请求(避免公网调用方干等到 30s 超时)。
|
|
4336
|
+
// 隧道已断时 safeSend 会静默跳过——此时服务端会因 WS close 自行清理,无需在此处理。
|
|
4337
|
+
sendErrorReply(requestId) {
|
|
4338
|
+
this.safeSend(JSON.stringify({
|
|
4151
4339
|
type: "response",
|
|
4152
|
-
requestId
|
|
4153
|
-
status:
|
|
4154
|
-
headers:
|
|
4155
|
-
bodyBase64:
|
|
4340
|
+
requestId,
|
|
4341
|
+
status: 502,
|
|
4342
|
+
headers: { "content-type": "text/plain; charset=utf-8" },
|
|
4343
|
+
bodyBase64: Buffer.from("agent_local_error").toString("base64")
|
|
4156
4344
|
}));
|
|
4157
4345
|
}
|
|
4346
|
+
async handleRequest(payload) {
|
|
4347
|
+
const fetchImpl = this.options.fetchImpl ?? fetch;
|
|
4348
|
+
let response;
|
|
4349
|
+
try {
|
|
4350
|
+
response = await fetchImpl(`${this.options.targetBaseUrl}${payload.path}`, {
|
|
4351
|
+
method: payload.method,
|
|
4352
|
+
headers: payload.headers,
|
|
4353
|
+
body: payload.bodyBase64 ? Buffer.from(payload.bodyBase64, "base64") : void 0
|
|
4354
|
+
});
|
|
4355
|
+
} catch (error) {
|
|
4356
|
+
console.warn(`[relay] \u672C\u5730\u8BF7\u6C42 ${payload.requestId} \u5931\u8D25\uFF1A`, error instanceof Error ? error.message : String(error));
|
|
4357
|
+
this.sendErrorReply(payload.requestId);
|
|
4358
|
+
return;
|
|
4359
|
+
}
|
|
4360
|
+
if (this.serverSupportsStreaming) {
|
|
4361
|
+
await this.streamResponse(payload.requestId, response);
|
|
4362
|
+
return;
|
|
4363
|
+
}
|
|
4364
|
+
try {
|
|
4365
|
+
const bodyBuffer = Buffer.from(await response.arrayBuffer());
|
|
4366
|
+
this.safeSend(JSON.stringify({
|
|
4367
|
+
type: "response",
|
|
4368
|
+
requestId: payload.requestId,
|
|
4369
|
+
status: response.status,
|
|
4370
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
4371
|
+
bodyBase64: bodyBuffer.toString("base64")
|
|
4372
|
+
}));
|
|
4373
|
+
} catch (error) {
|
|
4374
|
+
console.warn(`[relay] \u8BFB\u53D6\u54CD\u5E94\u4F53 ${payload.requestId} \u5931\u8D25\uFF1A`, error instanceof Error ? error.message : String(error));
|
|
4375
|
+
this.sendErrorReply(payload.requestId);
|
|
4376
|
+
}
|
|
4377
|
+
}
|
|
4378
|
+
// 流式回传:把响应拆成 response-start → response-chunk* → response-end 三段边收边发。
|
|
4379
|
+
// 不区分 content-type——服务器按 content-type 决定透传还是收齐重写(html/js/css)。
|
|
4380
|
+
// 对无限流(SSE)这是关键:响应体永不结束,绝不能 await 读完再发。
|
|
4381
|
+
async streamResponse(requestId, response) {
|
|
4382
|
+
const status = response.status;
|
|
4383
|
+
const headers = Object.fromEntries(response.headers.entries());
|
|
4384
|
+
if (!this.safeSend(JSON.stringify({ type: "response-start", requestId, status, headers }))) return;
|
|
4385
|
+
try {
|
|
4386
|
+
const body = response.body;
|
|
4387
|
+
if (body) {
|
|
4388
|
+
for await (const chunk of body) {
|
|
4389
|
+
const ok = this.safeSend(JSON.stringify({
|
|
4390
|
+
type: "response-chunk",
|
|
4391
|
+
requestId,
|
|
4392
|
+
chunkBase64: Buffer.from(chunk).toString("base64")
|
|
4393
|
+
}));
|
|
4394
|
+
if (!ok) return;
|
|
4395
|
+
}
|
|
4396
|
+
}
|
|
4397
|
+
} finally {
|
|
4398
|
+
this.safeSend(JSON.stringify({ type: "response-end", requestId }));
|
|
4399
|
+
}
|
|
4400
|
+
}
|
|
4158
4401
|
};
|
|
4159
4402
|
function derivePublicBaseUrl(serverUrl) {
|
|
4160
4403
|
return serverUrl.replace(/^ws:\/\//, "http://").replace(/^wss:\/\//, "https://").replace(/\/agent\/?$/, "").replace(/\/+$/, "");
|
|
@@ -4208,6 +4451,14 @@ var RelayTunnelRouter = class {
|
|
|
4208
4451
|
};
|
|
4209
4452
|
|
|
4210
4453
|
// src/daemon/server.ts
|
|
4454
|
+
function readUpdateStatusBestEffort(configPath) {
|
|
4455
|
+
if (!configPath) return { ok: false };
|
|
4456
|
+
try {
|
|
4457
|
+
return { ok: true, update: loadBridgeConfig(configPath).update };
|
|
4458
|
+
} catch {
|
|
4459
|
+
return { ok: false };
|
|
4460
|
+
}
|
|
4461
|
+
}
|
|
4211
4462
|
function createDaemonServer(options = {}) {
|
|
4212
4463
|
const app = Fastify({ logger: true });
|
|
4213
4464
|
const events = new BridgeEventHub();
|
|
@@ -4378,16 +4629,21 @@ function createDaemonServer(options = {}) {
|
|
|
4378
4629
|
});
|
|
4379
4630
|
}
|
|
4380
4631
|
registerFsBrowseRoutes({ app });
|
|
4381
|
-
app.get("/api/status", async () =>
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4632
|
+
app.get("/api/status", async () => {
|
|
4633
|
+
const read = readUpdateStatusBestEffort(configPath);
|
|
4634
|
+
const update = read.ok ? read.update ?? null : persistedConfig.update ?? null;
|
|
4635
|
+
return {
|
|
4636
|
+
ok: true,
|
|
4637
|
+
sessions: conversation.getCurrent() ? [conversation.getCurrent()] : [],
|
|
4638
|
+
update,
|
|
4639
|
+
preferredLocalUrl: (() => {
|
|
4640
|
+
const lan = listLanIpv4Addresses()[0];
|
|
4641
|
+
const port = process.env.BRIDGE_PORT ?? 8787;
|
|
4642
|
+
if (lan) return `http://${lan}:${port}`;
|
|
4643
|
+
return `http://127.0.0.1:${port}`;
|
|
4644
|
+
})()
|
|
4645
|
+
};
|
|
4646
|
+
});
|
|
4391
4647
|
app.get("/api/providers/status", async () => providers.getStatus());
|
|
4392
4648
|
app.post("/api/bridge-events", async (request, reply) => {
|
|
4393
4649
|
reply.raw.writeHead(200, {
|
|
@@ -4423,6 +4679,91 @@ function createDaemonServer(options = {}) {
|
|
|
4423
4679
|
return { app, conversation, sessions: conversation, events, activeUserStore, tunnelProvider };
|
|
4424
4680
|
}
|
|
4425
4681
|
|
|
4682
|
+
// src/daemon/updateChecker.ts
|
|
4683
|
+
var DEFAULT_REGISTRY_URL = "https://registry.npmmirror.com/claude-codex-wechat/latest";
|
|
4684
|
+
var DEFAULT_INTERVAL_MS = 60 * 60 * 1e3;
|
|
4685
|
+
var FETCH_TIMEOUT_MS = 5e3;
|
|
4686
|
+
function parseCore(version) {
|
|
4687
|
+
const core = version.trim().replace(/^v/, "").split(/[-+]/, 1)[0];
|
|
4688
|
+
const parts = core.split(".");
|
|
4689
|
+
if (parts.length !== 3) return null;
|
|
4690
|
+
const nums = parts.map((p) => Number(p));
|
|
4691
|
+
if (nums.some((n) => !Number.isInteger(n) || n < 0)) return null;
|
|
4692
|
+
return [nums[0], nums[1], nums[2]];
|
|
4693
|
+
}
|
|
4694
|
+
function compareSemver(a, b) {
|
|
4695
|
+
const ca = parseCore(a);
|
|
4696
|
+
const cb = parseCore(b);
|
|
4697
|
+
if (!ca || !cb) return null;
|
|
4698
|
+
for (let i = 0; i < 3; i += 1) {
|
|
4699
|
+
if (ca[i] > cb[i]) return 1;
|
|
4700
|
+
if (ca[i] < cb[i]) return -1;
|
|
4701
|
+
}
|
|
4702
|
+
return 0;
|
|
4703
|
+
}
|
|
4704
|
+
function isNewerVersion(latest, current) {
|
|
4705
|
+
if (/[-]/.test(latest.trim().replace(/^v/, ""))) return false;
|
|
4706
|
+
return compareSemver(latest, current) === 1;
|
|
4707
|
+
}
|
|
4708
|
+
function createUpdateChecker(opts) {
|
|
4709
|
+
const registryUrl = opts.registryUrl ?? DEFAULT_REGISTRY_URL;
|
|
4710
|
+
const intervalMs = opts.intervalMs ?? DEFAULT_INTERVAL_MS;
|
|
4711
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
4712
|
+
const persist = opts.persist ?? persistUpdateStatusToConfigFile;
|
|
4713
|
+
const now = opts.now ?? (() => Date.now());
|
|
4714
|
+
let timer = null;
|
|
4715
|
+
async function checkOnce() {
|
|
4716
|
+
try {
|
|
4717
|
+
const response = await fetchImpl(registryUrl, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
|
|
4718
|
+
if (!response.ok) return;
|
|
4719
|
+
const data = await response.json();
|
|
4720
|
+
const latest = typeof data?.version === "string" && data.version.trim() ? data.version.trim() : void 0;
|
|
4721
|
+
if (!latest) return;
|
|
4722
|
+
if (compareSemver(latest, opts.currentVersion) === null) return;
|
|
4723
|
+
await Promise.resolve(persist({
|
|
4724
|
+
configPath: opts.configPath,
|
|
4725
|
+
status: {
|
|
4726
|
+
currentVersion: opts.currentVersion,
|
|
4727
|
+
latestVersion: latest,
|
|
4728
|
+
updateAvailable: isNewerVersion(latest, opts.currentVersion),
|
|
4729
|
+
lastCheckedAt: now()
|
|
4730
|
+
}
|
|
4731
|
+
}));
|
|
4732
|
+
} catch {
|
|
4733
|
+
}
|
|
4734
|
+
}
|
|
4735
|
+
return {
|
|
4736
|
+
async start() {
|
|
4737
|
+
await checkOnce();
|
|
4738
|
+
timer = setInterval(() => {
|
|
4739
|
+
void checkOnce();
|
|
4740
|
+
}, intervalMs);
|
|
4741
|
+
timer.unref?.();
|
|
4742
|
+
},
|
|
4743
|
+
stop() {
|
|
4744
|
+
if (timer) {
|
|
4745
|
+
clearInterval(timer);
|
|
4746
|
+
timer = null;
|
|
4747
|
+
}
|
|
4748
|
+
},
|
|
4749
|
+
checkOnce
|
|
4750
|
+
};
|
|
4751
|
+
}
|
|
4752
|
+
|
|
4753
|
+
// src/shared/version.ts
|
|
4754
|
+
import { readFileSync as readFileSync7 } from "node:fs";
|
|
4755
|
+
import { dirname as dirname11, join as join10 } from "node:path";
|
|
4756
|
+
import { fileURLToPath } from "node:url";
|
|
4757
|
+
function readClientVersion() {
|
|
4758
|
+
try {
|
|
4759
|
+
const here2 = dirname11(fileURLToPath(import.meta.url));
|
|
4760
|
+
const pkg = JSON.parse(readFileSync7(join10(here2, "..", "..", "package.json"), "utf8"));
|
|
4761
|
+
return typeof pkg.version === "string" && pkg.version ? pkg.version : "0.0.0";
|
|
4762
|
+
} catch {
|
|
4763
|
+
return "0.0.0";
|
|
4764
|
+
}
|
|
4765
|
+
}
|
|
4766
|
+
|
|
4426
4767
|
// src/daemon/bootstrap.ts
|
|
4427
4768
|
async function startDaemon(options) {
|
|
4428
4769
|
const port = options.port ?? Number(process.env.BRIDGE_PORT ?? 8787);
|
|
@@ -4456,6 +4797,9 @@ async function startDaemon(options) {
|
|
|
4456
4797
|
if (config.tunnel?.relay?.serverUrl && config.tunnel?.relay?.authToken) {
|
|
4457
4798
|
await resolvedTunnelProvider?.start().catch(() => void 0);
|
|
4458
4799
|
}
|
|
4800
|
+
if (options.enableUpdateCheck) {
|
|
4801
|
+
createUpdateChecker({ currentVersion: readClientVersion(), configPath }).start().catch(() => void 0);
|
|
4802
|
+
}
|
|
4459
4803
|
console.log("claude-codex-wechat listening:");
|
|
4460
4804
|
console.log(` Local: http://127.0.0.1:${actualPort}`);
|
|
4461
4805
|
for (const ip of listLanIpv4Addresses()) {
|
|
@@ -4478,8 +4822,8 @@ async function resolveProviderCommands(providers) {
|
|
|
4478
4822
|
}
|
|
4479
4823
|
|
|
4480
4824
|
// src/daemon/staticFrontend.ts
|
|
4481
|
-
import { readFileSync as
|
|
4482
|
-
import { join as
|
|
4825
|
+
import { readFileSync as readFileSync8 } from "node:fs";
|
|
4826
|
+
import { join as join11 } from "node:path";
|
|
4483
4827
|
import fastifyStatic from "@fastify/static";
|
|
4484
4828
|
function attachStaticFrontend(webRoot2) {
|
|
4485
4829
|
return async (app) => {
|
|
@@ -4487,7 +4831,7 @@ function attachStaticFrontend(webRoot2) {
|
|
|
4487
4831
|
root: webRoot2,
|
|
4488
4832
|
wildcard: false
|
|
4489
4833
|
});
|
|
4490
|
-
const indexHtml =
|
|
4834
|
+
const indexHtml = readFileSync8(join11(webRoot2, "index.html"), "utf8");
|
|
4491
4835
|
app.setNotFoundHandler((request, reply) => {
|
|
4492
4836
|
if (request.url.startsWith("/api")) {
|
|
4493
4837
|
reply.code(404).send({ error: "not_found" });
|
|
@@ -4502,17 +4846,17 @@ function attachStaticFrontend(webRoot2) {
|
|
|
4502
4846
|
import { existsSync as existsSync9, statSync, openSync, closeSync, watchFile } from "node:fs";
|
|
4503
4847
|
import { mkdir as mkdir4, readFile as readFile6, rm as rm2, writeFile as writeFile5 } from "node:fs/promises";
|
|
4504
4848
|
import { homedir as homedir8 } from "node:os";
|
|
4505
|
-
import { dirname as
|
|
4849
|
+
import { dirname as dirname12, join as join12 } from "node:path";
|
|
4506
4850
|
import { createReadStream } from "node:fs";
|
|
4507
|
-
import { execFile, spawn as
|
|
4851
|
+
import { execFile, spawn as spawn7 } from "node:child_process";
|
|
4508
4852
|
import { createInterface } from "node:readline";
|
|
4509
4853
|
import { promisify } from "node:util";
|
|
4510
4854
|
var execFileAsync = promisify(execFile);
|
|
4511
4855
|
async function installService(context) {
|
|
4512
4856
|
if (process.platform === "darwin") {
|
|
4513
4857
|
const spec = buildLaunchdSpec(context);
|
|
4514
|
-
await mkdir4(
|
|
4515
|
-
await mkdir4(
|
|
4858
|
+
await mkdir4(dirname12(spec.plistPath), { recursive: true });
|
|
4859
|
+
await mkdir4(dirname12(spec.stdoutPath), { recursive: true });
|
|
4516
4860
|
await writeFile5(spec.plistPath, renderLaunchdPlist(spec), "utf8");
|
|
4517
4861
|
await runLaunchctl(["unload", spec.plistPath]).catch(() => void 0);
|
|
4518
4862
|
await runLaunchctl(["load", spec.plistPath]);
|
|
@@ -4521,8 +4865,8 @@ async function installService(context) {
|
|
|
4521
4865
|
}
|
|
4522
4866
|
if (process.platform === "linux") {
|
|
4523
4867
|
const spec = buildSystemdUserSpec(context);
|
|
4524
|
-
await mkdir4(
|
|
4525
|
-
await mkdir4(
|
|
4868
|
+
await mkdir4(dirname12(spec.unitPath), { recursive: true });
|
|
4869
|
+
await mkdir4(dirname12(spec.stdoutPath), { recursive: true });
|
|
4526
4870
|
await writeFile5(spec.unitPath, renderSystemdUnit(spec), "utf8");
|
|
4527
4871
|
await runSystemctl(["--user", "daemon-reload"]);
|
|
4528
4872
|
await runSystemctl(["--user", "enable", "--now", spec.unitName]);
|
|
@@ -4687,37 +5031,37 @@ async function readServiceStatus(context) {
|
|
|
4687
5031
|
throw new Error("service_status_not_supported_on_this_platform");
|
|
4688
5032
|
}
|
|
4689
5033
|
function buildLaunchdSpec(context) {
|
|
4690
|
-
const stateDir =
|
|
5034
|
+
const stateDir = join12(homedir8(), ".claude-codex-wechat");
|
|
4691
5035
|
return {
|
|
4692
5036
|
label: "com.claude-codex-wechat",
|
|
4693
|
-
plistPath:
|
|
5037
|
+
plistPath: join12(homedir8(), "Library", "LaunchAgents", "com.claude-codex-wechat.plist"),
|
|
4694
5038
|
programArgs: [context.nodePath ?? process.execPath, context.cliEntrypointPath, "__daemon"],
|
|
4695
5039
|
workingDirectory: homedir8(),
|
|
4696
|
-
stdoutPath:
|
|
4697
|
-
stderrPath:
|
|
5040
|
+
stdoutPath: join12(stateDir, "logs", "service.stdout.log"),
|
|
5041
|
+
stderrPath: join12(stateDir, "logs", "service.stderr.log"),
|
|
4698
5042
|
environment: buildServiceEnvironment(context)
|
|
4699
5043
|
};
|
|
4700
5044
|
}
|
|
4701
5045
|
function buildSystemdUserSpec(context) {
|
|
4702
|
-
const stateDir =
|
|
5046
|
+
const stateDir = join12(homedir8(), ".claude-codex-wechat");
|
|
4703
5047
|
return {
|
|
4704
5048
|
unitName: "claude-codex-wechat.service",
|
|
4705
|
-
unitPath:
|
|
5049
|
+
unitPath: join12(homedir8(), ".config", "systemd", "user", "claude-codex-wechat.service"),
|
|
4706
5050
|
execStart: [context.nodePath ?? process.execPath, context.cliEntrypointPath, "__daemon"],
|
|
4707
5051
|
workingDirectory: homedir8(),
|
|
4708
|
-
stdoutPath:
|
|
4709
|
-
stderrPath:
|
|
5052
|
+
stdoutPath: join12(stateDir, "logs", "service.stdout.log"),
|
|
5053
|
+
stderrPath: join12(stateDir, "logs", "service.stderr.log"),
|
|
4710
5054
|
environment: buildServiceEnvironment(context)
|
|
4711
5055
|
};
|
|
4712
5056
|
}
|
|
4713
5057
|
function buildProcessSpec(context) {
|
|
4714
|
-
const stateDir =
|
|
5058
|
+
const stateDir = join12(homedir8(), ".claude-codex-wechat");
|
|
4715
5059
|
return {
|
|
4716
|
-
pidPath:
|
|
5060
|
+
pidPath: join12(stateDir, "service.pid"),
|
|
4717
5061
|
command: [context.nodePath ?? process.execPath, context.cliEntrypointPath, "__daemon"],
|
|
4718
5062
|
workingDirectory: homedir8(),
|
|
4719
|
-
stdoutPath:
|
|
4720
|
-
stderrPath:
|
|
5063
|
+
stdoutPath: join12(stateDir, "logs", "service.stdout.log"),
|
|
5064
|
+
stderrPath: join12(stateDir, "logs", "service.stderr.log"),
|
|
4721
5065
|
environment: buildServiceEnvironment(context)
|
|
4722
5066
|
};
|
|
4723
5067
|
}
|
|
@@ -4838,7 +5182,7 @@ async function readPidFile(pidPath) {
|
|
|
4838
5182
|
}
|
|
4839
5183
|
}
|
|
4840
5184
|
async function writePidFile(pidPath, pid) {
|
|
4841
|
-
await mkdir4(
|
|
5185
|
+
await mkdir4(dirname12(pidPath), { recursive: true });
|
|
4842
5186
|
await writeFile5(pidPath, `${pid}
|
|
4843
5187
|
`, "utf8");
|
|
4844
5188
|
}
|
|
@@ -4853,12 +5197,12 @@ async function killPid(pid) {
|
|
|
4853
5197
|
process.kill(pid, "SIGTERM");
|
|
4854
5198
|
}
|
|
4855
5199
|
async function spawnDetachedDaemon(spec) {
|
|
4856
|
-
await mkdir4(
|
|
5200
|
+
await mkdir4(dirname12(spec.stdoutPath), { recursive: true });
|
|
4857
5201
|
const outFd = openSync(spec.stdoutPath, "a");
|
|
4858
5202
|
const errFd = openSync(spec.stderrPath, "a");
|
|
4859
5203
|
try {
|
|
4860
5204
|
const [command, ...args] = spec.command;
|
|
4861
|
-
const child =
|
|
5205
|
+
const child = spawn7(command, args, {
|
|
4862
5206
|
cwd: spec.workingDirectory,
|
|
4863
5207
|
// Windows 分离进程需继承完整 process.env(SystemRoot/APPDATA/PATHEXT 等),
|
|
4864
5208
|
// 否则子进程及其拉起的 claude/codex 会异常;spec.environment 仅作 BRIDGE_* 等覆盖层。
|
|
@@ -4896,7 +5240,7 @@ ${tail}
|
|
|
4896
5240
|
`);
|
|
4897
5241
|
}
|
|
4898
5242
|
if (process.platform === "linux" || process.platform === "darwin") {
|
|
4899
|
-
const child =
|
|
5243
|
+
const child = spawn7("tail", ["-f", ...existing], { stdio: ["ignore", "pipe", "inherit"] });
|
|
4900
5244
|
child.stdout.on("data", (chunk) => {
|
|
4901
5245
|
process.stdout.write(String(chunk));
|
|
4902
5246
|
});
|
|
@@ -4939,8 +5283,8 @@ function fileSizeHint(path) {
|
|
|
4939
5283
|
}
|
|
4940
5284
|
|
|
4941
5285
|
// src/cli.ts
|
|
4942
|
-
var here =
|
|
4943
|
-
var webRoot =
|
|
5286
|
+
var here = dirname13(fileURLToPath2(import.meta.url));
|
|
5287
|
+
var webRoot = join13(here, "..", "web");
|
|
4944
5288
|
async function main() {
|
|
4945
5289
|
const command = process.argv[2];
|
|
4946
5290
|
switch (command) {
|
|
@@ -5011,6 +5355,7 @@ async function cmdRestart() {
|
|
|
5011
5355
|
async function cmdStatus() {
|
|
5012
5356
|
const status = await readServiceStatus(createServiceContext());
|
|
5013
5357
|
printServiceStatus("service status", status);
|
|
5358
|
+
printUpdateHint();
|
|
5014
5359
|
}
|
|
5015
5360
|
async function cmdLogs() {
|
|
5016
5361
|
console.log(await readServiceLogs(createServiceContext()));
|
|
@@ -5025,7 +5370,8 @@ async function cmdDaemon() {
|
|
|
5025
5370
|
return;
|
|
5026
5371
|
}
|
|
5027
5372
|
await startDaemon({
|
|
5028
|
-
attachFrontend: attachStaticFrontend(webRoot)
|
|
5373
|
+
attachFrontend: attachStaticFrontend(webRoot),
|
|
5374
|
+
enableUpdateCheck: true
|
|
5029
5375
|
});
|
|
5030
5376
|
}
|
|
5031
5377
|
async function cmdDoctor() {
|
|
@@ -5044,6 +5390,23 @@ async function cmdDoctor() {
|
|
|
5044
5390
|
report("\u5FAE\u4FE1 token", wechat?.token ? "\u5DF2\u914D\u7F6E" : "\u672A\u914D\u7F6E");
|
|
5045
5391
|
report("\u5FAE\u4FE1 accountId", wechat?.accountId ? "\u5DF2\u914D\u7F6E" : "\u672A\u914D\u7F6E");
|
|
5046
5392
|
}
|
|
5393
|
+
printUpdateHint();
|
|
5394
|
+
}
|
|
5395
|
+
function printUpdateHint() {
|
|
5396
|
+
const configPath = process.env.BRIDGE_CONFIG ?? defaultConfigPath();
|
|
5397
|
+
if (!existsSync10(configPath)) return;
|
|
5398
|
+
let update;
|
|
5399
|
+
try {
|
|
5400
|
+
update = loadBridgeConfig(configPath).update;
|
|
5401
|
+
} catch {
|
|
5402
|
+
return;
|
|
5403
|
+
}
|
|
5404
|
+
if (!update?.updateAvailable) return;
|
|
5405
|
+
const current = update.currentVersion ?? readClientVersion();
|
|
5406
|
+
console.log(`
|
|
5407
|
+
\u53D1\u73B0\u65B0\u7248 v${update.latestVersion}\uFF08\u5F53\u524D v${current}\uFF09\u3002\u66F4\u65B0\uFF1A`);
|
|
5408
|
+
console.log(" npm install -g claude-codex-wechat@latest --registry=https://registry.npmmirror.com/");
|
|
5409
|
+
console.log(" claude-codex-wechat restart");
|
|
5047
5410
|
}
|
|
5048
5411
|
function cmdPrintConfig() {
|
|
5049
5412
|
const configPath = process.env.BRIDGE_CONFIG ?? defaultConfigPath();
|
|
@@ -5052,7 +5415,7 @@ function cmdPrintConfig() {
|
|
|
5052
5415
|
process.exitCode = 1;
|
|
5053
5416
|
return;
|
|
5054
5417
|
}
|
|
5055
|
-
console.log(
|
|
5418
|
+
console.log(readFileSync9(configPath, "utf8"));
|
|
5056
5419
|
}
|
|
5057
5420
|
async function cmdUninstall() {
|
|
5058
5421
|
const status = await uninstallService(createServiceContext());
|
|
@@ -5060,7 +5423,7 @@ async function cmdUninstall() {
|
|
|
5060
5423
|
}
|
|
5061
5424
|
function createServiceContext() {
|
|
5062
5425
|
return {
|
|
5063
|
-
cliEntrypointPath:
|
|
5426
|
+
cliEntrypointPath: fileURLToPath2(import.meta.url),
|
|
5064
5427
|
nodePath: process.execPath,
|
|
5065
5428
|
configPath: process.env.BRIDGE_CONFIG ?? defaultConfigPath(),
|
|
5066
5429
|
port: Number(process.env.BRIDGE_PORT ?? 8787)
|