chatccc 0.2.234 → 0.2.235

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.
@@ -0,0 +1,324 @@
1
+ // =============================================================================
2
+ // claude-sdk-installer.ts — Claude Code 引擎(Agent SDK)按需安装器
3
+ // =============================================================================
4
+ // 背景:@anthropic-ai/claude-agent-sdk 通过 optionalDependencies 内置各平台
5
+ // 的原生 CLI 二进制(Windows 上 claude.exe 约 215MB),若作为 chatccc 的硬依赖,
6
+ // 所有用户(包括不用 Claude Code 的)都要白白下载 220MB+。
7
+ //
8
+ // 方案:把 SDK 从 chatccc 的 dependencies 中移除,改为**按需安装**到
9
+ // `~/.chatccc/claude-sdk/`:
10
+ // - 用户未启用 Claude Code → 完全不下载,npm 包体积大幅下降;
11
+ // - 用户在设置页打开 Claude Code 开关时,点「安装引擎」触发后台安装,
12
+ // 带进度条(状态式:检测环境 → 下载中 → 安装中 → 完成/失败)。
13
+ //
14
+ // 设计约束:
15
+ // - **不 import config.ts**(config.ts 顶层有 loadConfig 副作用,web-ui.ts
16
+ // 依赖本模块,间接 import config.ts 会污染依赖 web-ui.ts 的单测)。
17
+ // - 始终使用 SDK 内置的 CLI 二进制(用户拍板档位 b),不依赖用户自装 CLI。
18
+ // - 安装目录可注入(dir 参数),便于单测用临时目录,不碰真实用户目录。
19
+ // =============================================================================
20
+
21
+ import { spawn } from "node:child_process";
22
+ import {
23
+ existsSync,
24
+ mkdirSync,
25
+ readdirSync,
26
+ readFileSync,
27
+ rmSync,
28
+ statSync,
29
+ unlinkSync,
30
+ writeFileSync,
31
+ type Dirent,
32
+ } from "node:fs";
33
+ import { homedir } from "node:os";
34
+ import { join } from "node:path";
35
+ import { pathToFileURL } from "node:url";
36
+
37
+ /** 与当前代码配套的 SDK 版本(package.json 移除依赖后此常量成为唯一版本来源) */
38
+ export const CLAUDE_SDK_VERSION = "0.2.133";
39
+
40
+ /** 按需安装目录:~/.chatccc/claude-sdk */
41
+ export const CLAUDE_SDK_DIR = join(homedir(), ".chatccc", "claude-sdk");
42
+
43
+ const SDK_PKG_REL = join("node_modules", "@anthropic-ai", "claude-agent-sdk", "package.json");
44
+ const SDK_ENTRY_REL = join("node_modules", "@anthropic-ai", "claude-agent-sdk", "sdk.mjs");
45
+ const LOCK_FILE = ".installing";
46
+
47
+ /** 估算的完整安装体积(含内置 CLI 二进制),用于进度条展示 */
48
+ export const CLAUDE_SDK_EXPECTED_BYTES = 240 * 1024 * 1024;
49
+
50
+ export type SdkInstallPhase =
51
+ | "idle"
52
+ | "detecting"
53
+ | "downloading"
54
+ | "installing"
55
+ | "done"
56
+ | "error";
57
+
58
+ export interface SdkInstallProgress {
59
+ phase: SdkInstallPhase;
60
+ /** 0-100 */
61
+ percent: number;
62
+ message: string;
63
+ error?: string;
64
+ }
65
+
66
+ export interface SdkInstallOptions {
67
+ /** 安装目录(测试注入临时目录) */
68
+ dir?: string;
69
+ /** npm 命令(测试注入 fake) */
70
+ npmCommand?: string;
71
+ /** 期望版本(默认 CLAUDE_SDK_VERSION) */
72
+ expectedVersion?: string;
73
+ /** 进度回调 */
74
+ onProgress?: (p: SdkInstallProgress) => void;
75
+ }
76
+
77
+ // ---------------------------------------------------------------------------
78
+ // 状态查询
79
+ // ---------------------------------------------------------------------------
80
+
81
+ /** SDK 入口文件绝对路径(供动态 import) */
82
+ export function getClaudeSdkEntryPath(dir: string = CLAUDE_SDK_DIR): string {
83
+ return join(dir, SDK_ENTRY_REL);
84
+ }
85
+
86
+ /** 是否已安装(node_modules 里存在 SDK package.json 即视为已装) */
87
+ export function isClaudeSdkInstalled(dir: string = CLAUDE_SDK_DIR): boolean {
88
+ return existsSync(join(dir, SDK_PKG_REL));
89
+ }
90
+
91
+ /** 已安装版本号;未安装/解析失败返回 null */
92
+ export function getClaudeSdkInstalledVersion(dir: string = CLAUDE_SDK_DIR): string | null {
93
+ try {
94
+ const raw = readFileSync(join(dir, SDK_PKG_REL), "utf8");
95
+ const pkg = JSON.parse(raw) as { version?: unknown };
96
+ return typeof pkg.version === "string" ? pkg.version : null;
97
+ } catch {
98
+ return null;
99
+ }
100
+ }
101
+
102
+ /** 是否正在安装(锁文件存在且 pid 存活);pid 已死视为过期锁并自动清理 */
103
+ export function isClaudeSdkInstalling(dir: string = CLAUDE_SDK_DIR): boolean {
104
+ const lockPath = join(dir, LOCK_FILE);
105
+ if (!existsSync(lockPath)) return false;
106
+ try {
107
+ const pid = Number.parseInt(readFileSync(lockPath, "utf8").trim(), 10);
108
+ if (Number.isFinite(pid) && pid > 0 && isPidAlive(pid)) return true;
109
+ } catch {
110
+ // 锁文件损坏 → 按过期处理,走清理
111
+ }
112
+ try {
113
+ unlinkSync(lockPath);
114
+ } catch {
115
+ // 清理失败不阻塞判断
116
+ }
117
+ return false;
118
+ }
119
+
120
+ function isPidAlive(pid: number): boolean {
121
+ try {
122
+ process.kill(pid, 0);
123
+ return true;
124
+ } catch (err) {
125
+ return (err as NodeJS.ErrnoException).code === "EPERM";
126
+ }
127
+ }
128
+
129
+ // ---------------------------------------------------------------------------
130
+ // 安装(后台任务 + 进度)
131
+ // ---------------------------------------------------------------------------
132
+
133
+ /** 最近一次安装的进度快照,供 web-ui 轮询 */
134
+ let lastProgress: SdkInstallProgress = { phase: "idle", percent: 0, message: "" };
135
+ let installInProgress = false;
136
+
137
+ export function getLastInstallProgress(): SdkInstallProgress {
138
+ return { ...lastProgress, ...(lastProgress.error ? { error: lastProgress.error } : {}) };
139
+ }
140
+
141
+ export function isInstallRunning(): boolean {
142
+ return installInProgress;
143
+ }
144
+
145
+ function setProgress(p: SdkInstallProgress, onProgress?: (p: SdkInstallProgress) => void): void {
146
+ lastProgress = { ...p };
147
+ onProgress?.(lastProgress);
148
+ }
149
+
150
+ /**
151
+ * 启动按需安装。返回 Promise,安装完成 resolve、失败 reject。
152
+ * 调用方通常不 await(后台执行),通过 onProgress / getLastInstallProgress() 获取进度。
153
+ */
154
+ export async function installClaudeSdk(options: SdkInstallOptions = {}): Promise<void> {
155
+ const dir = options.dir ?? CLAUDE_SDK_DIR;
156
+ const npmCommand = options.npmCommand ?? "npm";
157
+ const expected = options.expectedVersion ?? CLAUDE_SDK_VERSION;
158
+ const onProgress = options.onProgress;
159
+
160
+ if (installInProgress) {
161
+ throw new Error("Claude Code 引擎正在安装中,请等待完成后再试。");
162
+ }
163
+ if (isClaudeSdkInstalling(dir)) {
164
+ throw new Error("检测到另一个安装任务正在进行(锁文件存在),请稍后再试。");
165
+ }
166
+
167
+ const installedVersion = isClaudeSdkInstalled(dir) ? getClaudeSdkInstalledVersion(dir) : null;
168
+ if (installedVersion === expected) {
169
+ setProgress({ phase: "done", percent: 100, message: `已安装 v${installedVersion}` }, onProgress);
170
+ return;
171
+ }
172
+ if (installedVersion !== null && installedVersion !== expected) {
173
+ setProgress(
174
+ {
175
+ phase: "detecting",
176
+ percent: 3,
177
+ message: `已装 v${installedVersion} ≠ 期望 v${expected},开始重装…`,
178
+ },
179
+ onProgress,
180
+ );
181
+ }
182
+
183
+ installInProgress = true;
184
+ mkdirSync(dir, { recursive: true });
185
+ writeFileSync(join(dir, LOCK_FILE), String(process.pid), "utf8");
186
+
187
+ setProgress({ phase: "detecting", percent: 2, message: "检测环境…" }, onProgress);
188
+
189
+ return new Promise<void>((resolve, reject) => {
190
+ const child = spawn(
191
+ npmCommand,
192
+ [
193
+ "install",
194
+ "--prefix",
195
+ dir,
196
+ `@anthropic-ai/claude-agent-sdk@${expected}`,
197
+ "--no-audit",
198
+ "--no-fund",
199
+ "--loglevel=http",
200
+ ],
201
+ {
202
+ stdio: ["ignore", "pipe", "pipe"],
203
+ windowsHide: true,
204
+ // Windows 下 npm 是 npm.cmd,需要 shell 才能解析
205
+ shell: process.platform === "win32",
206
+ },
207
+ );
208
+
209
+ let stderrBuf = "";
210
+ let requestCount = 0;
211
+ let sizeTimer: ReturnType<typeof setInterval> | null = null;
212
+
213
+ const updateBySize = (): void => {
214
+ const size = dirSizeBytes(dir);
215
+ const sizePercent = Math.min((size / CLAUDE_SDK_EXPECTED_BYTES) * 90, 90);
216
+ const requestPercent = Math.min(5 + requestCount * 4, 70);
217
+ const percent = Math.max(sizePercent, requestPercent);
218
+ const mb = (size / 1048576).toFixed(0);
219
+ setProgress(
220
+ {
221
+ phase: "downloading",
222
+ percent: Math.min(percent, 94),
223
+ message: `下载中… 已下载 ${mb} MB / 约 220 MB`,
224
+ },
225
+ onProgress,
226
+ );
227
+ };
228
+
229
+ child.stdout.on("data", (chunk: Buffer) => {
230
+ const text = chunk.toString();
231
+ // npm --loglevel=http 每完成一个请求输出一行 "npm http fetch GET 200 ..."
232
+ requestCount += (text.match(/http fetch GET|GET \d{3}/g) ?? []).length;
233
+ updateBySize();
234
+ });
235
+
236
+ child.stderr.on("data", (chunk: Buffer) => {
237
+ stderrBuf += chunk.toString();
238
+ });
239
+
240
+ child.on("error", (err) => {
241
+ cleanupAfterFailure(dir, err, onProgress, () => {
242
+ installInProgress = false;
243
+ reject(err);
244
+ });
245
+ });
246
+
247
+ child.on("close", (code) => {
248
+ if (sizeTimer) clearInterval(sizeTimer);
249
+ if (code === 0 && isClaudeSdkInstalled(dir)) {
250
+ const version = getClaudeSdkInstalledVersion(dir);
251
+ installInProgress = false;
252
+ try {
253
+ unlinkSync(join(dir, LOCK_FILE));
254
+ } catch {
255
+ // 锁文件不存在可忽略
256
+ }
257
+ setProgress(
258
+ { phase: "done", percent: 100, message: `安装完成(v${version ?? expected})` },
259
+ onProgress,
260
+ );
261
+ resolve();
262
+ } else {
263
+ const tail = stderrBuf.trim().split("\n").slice(-5).join(" | ");
264
+ const detail = code !== 0 ? `npm 退出码 ${code}` : "安装后校验未通过";
265
+ const message = tail ? `${detail}:${tail.slice(0, 400)}` : detail;
266
+ cleanupAfterFailure(dir, new Error(message), onProgress, () => {
267
+ installInProgress = false;
268
+ reject(new Error(message));
269
+ });
270
+ }
271
+ });
272
+
273
+ // 定期按目录体积刷新进度(大文件下载时 stdout 事件稀疏)
274
+ sizeTimer = setInterval(() => {
275
+ if (installInProgress) updateBySize();
276
+ }, 400);
277
+ if (typeof sizeTimer.unref === "function") sizeTimer.unref();
278
+ });
279
+ }
280
+
281
+ function cleanupAfterFailure(
282
+ dir: string,
283
+ err: Error,
284
+ onProgress: ((p: SdkInstallProgress) => void) | undefined,
285
+ after: () => void,
286
+ ): void {
287
+ setProgress(
288
+ { phase: "error", percent: 0, message: "安装失败", error: err.message.slice(0, 500) },
289
+ onProgress,
290
+ );
291
+ try {
292
+ rmSync(dir, { recursive: true, force: true });
293
+ } catch {
294
+ // 清理失败不阻塞
295
+ }
296
+ after();
297
+ }
298
+
299
+ function dirSizeBytes(dir: string): number {
300
+ let total = 0;
301
+ const stack = [dir];
302
+ while (stack.length > 0) {
303
+ const current = stack.pop()!;
304
+ let entries: Dirent[];
305
+ try {
306
+ entries = readdirSync(current, { withFileTypes: true });
307
+ } catch {
308
+ continue;
309
+ }
310
+ for (const entry of entries) {
311
+ const full = join(current, entry.name);
312
+ if (entry.isDirectory()) {
313
+ stack.push(full);
314
+ } else if (entry.isFile()) {
315
+ try {
316
+ total += statSync(full).size;
317
+ } catch {
318
+ // 文件被并发删除可忽略
319
+ }
320
+ }
321
+ }
322
+ }
323
+ return total;
324
+ }
package/src/web-ui.ts CHANGED
@@ -18,6 +18,13 @@ import {
18
18
  createInternalRestartEnv,
19
19
  openWebUiInDefaultBrowser,
20
20
  } from "./startup-lifecycle.ts";
21
+ import {
22
+ getClaudeSdkInstalledVersion,
23
+ getLastInstallProgress,
24
+ installClaudeSdk,
25
+ isClaudeSdkInstalled,
26
+ isInstallRunning,
27
+ } from "./claude-sdk-installer.ts";
21
28
 
22
29
  const __dirname = dirname(fileURLToPath(import.meta.url));
23
30
  const PROJECT_ROOT = join(__dirname, "..");
@@ -629,6 +636,31 @@ async function handleForgetIlink(_req: IncomingMessage, res: ServerResponse): Pr
629
636
  }
630
637
  }
631
638
 
639
+ async function handleClaudeSdkStatus(_req: IncomingMessage, res: ServerResponse): Promise<void> {
640
+ jsonReply(res, 200, {
641
+ installed: isClaudeSdkInstalled(),
642
+ version: getClaudeSdkInstalledVersion(),
643
+ running: isInstallRunning(),
644
+ progress: getLastInstallProgress(),
645
+ });
646
+ }
647
+
648
+ async function handleClaudeSdkInstall(_req: IncomingMessage, res: ServerResponse): Promise<void> {
649
+ if (isInstallRunning()) {
650
+ jsonReply(res, 200, { ok: true, alreadyRunning: true });
651
+ return;
652
+ }
653
+ try {
654
+ // 后台执行,不阻塞响应;前端通过 /api/claude-sdk/status 轮询进度
655
+ installClaudeSdk().catch((err: unknown) => {
656
+ console.error(`[WEB-UI] Claude SDK 安装失败: ${(err as Error).message}`);
657
+ });
658
+ jsonReply(res, 200, { ok: true });
659
+ } catch (err) {
660
+ jsonReply(res, 500, { ok: false, error: (err as Error).message });
661
+ }
662
+ }
663
+
632
664
  // ---------------------------------------------------------------------------
633
665
  // HTML page (embedded template)
634
666
  // ---------------------------------------------------------------------------
@@ -854,6 +886,52 @@ header .badge{font-size:13px;padding:4px 12px;border-radius:12px;font-weight:500
854
886
 
855
887
  <div class="agent-cards">
856
888
 
889
+ <!-- CCC Agent 卡片(置顶:ChatCCC 内置 Agent,开箱即用) -->
890
+ <div class="agent-card" id="agent-card-ccc">
891
+ <div class="agent-card-header">
892
+ <input type="checkbox" class="agent-toggle" id="agent-enable-ccc" onchange="onAgentToggle('ccc', this.checked)">
893
+ <div class="meta">
894
+ <div class="name">CCC Agent</div>
895
+ <div class="desc">ChatCCC 内置 Agent<br>OpenAI 兼容 API(不限于 DeepSeek)</div>
896
+ </div>
897
+ </div>
898
+ <label class="agent-default-row">
899
+ <input type="checkbox" id="agent-default-ccc" onchange="onDefaultAgentToggle('ccc', this.checked)">
900
+ 设为默认 Agent
901
+ </label>
902
+ <fieldset class="agent-body" id="agent-body-ccc" disabled>
903
+ <div class="form-group">
904
+ <label>API Key</label>
905
+ <input type="password" id="field-CHATCCC_CCC_API_KEY" placeholder="OpenAI 兼容 API Key(如 DeepSeek)">
906
+ </div>
907
+ <div class="form-group">
908
+ <label>Base URL</label>
909
+ <input type="text" id="field-CHATCCC_CCC_BASE_URL" placeholder="https://api.deepseek.com/v1(可填任意 OpenAI 兼容端点)">
910
+ </div>
911
+ <div class="form-group">
912
+ <label>模型</label>
913
+ <input type="text" id="field-CHATCCC_CCC_MODEL" placeholder="deepseek-v4-pro">
914
+ </div>
915
+ <div class="form-group">
916
+ <label>备选模型(选填)</label>
917
+ <input type="text" id="field-CHATCCC_CCC_ALTERNATIVE_MODEL" placeholder="加入 /model 列表,便于会话内切换">
918
+ </div>
919
+ <div class="form-group">
920
+ <label>Effort(推理强度,选填)</label>
921
+ <select id="field-CHATCCC_CCC_EFFORT">
922
+ <option value="">(留空/默认,服务端 medium)</option>
923
+ <option value="none">none - 直接作答,最省 token</option>
924
+ <option value="minimal">minimal</option>
925
+ <option value="low">low</option>
926
+ <option value="medium">medium</option>
927
+ <option value="high">high</option>
928
+ <option value="xhigh">xhigh</option>
929
+ <option value="max">max - 最强推理</option>
930
+ </select>
931
+ </div>
932
+ </fieldset>
933
+ </div>
934
+
857
935
  <!-- Claude 卡片 -->
858
936
  <div class="agent-card" id="agent-card-claude">
859
937
  <div class="agent-card-header">
@@ -888,6 +966,18 @@ header .badge{font-size:13px;padding:4px 12px;border-radius:12px;font-weight:500
888
966
  <label>Base URL(选填)</label>
889
967
  <input type="text" id="field-CHATCCC_ANTHROPIC_BASE_URL" placeholder="留空使用默认端点">
890
968
  </div>
969
+ <div class="form-group" style="border-top:1px solid #e2e8f0;padding-top:12px;margin-top:4px">
970
+ <label>Claude Code 引擎(Agent SDK)</label>
971
+ <div id="claude-engine-status" style="font-size:13px;color:#64748b;margin-bottom:8px">检测中...</div>
972
+ <div id="claude-engine-progress-wrap" style="display:none;margin-bottom:8px">
973
+ <div style="background:#e2e8f0;border-radius:6px;height:8px;overflow:hidden">
974
+ <div id="claude-engine-progress-bar" style="width:0%;height:100%;background:#3b82f6;transition:width .3s"></div>
975
+ </div>
976
+ <div id="claude-engine-progress-text" style="font-size:12px;color:#64748b;margin-top:4px"></div>
977
+ </div>
978
+ <button class="btn btn-outline" id="claude-engine-install-btn" onclick="installClaudeEngine()">安装引擎(约 220MB)</button>
979
+ <div class="hint" style="margin-top:6px">ChatCCC 通过 Claude Agent SDK 调用 Claude Code;SDK 引擎按需下载到本机(仅启用 Claude Code 时需要),安装期间请保持网络畅通。</div>
980
+ </div>
891
981
  </fieldset>
892
982
  </div>
893
983
 
@@ -974,52 +1064,6 @@ header .badge{font-size:13px;padding:4px 12px;border-radius:12px;font-weight:500
974
1064
  </fieldset>
975
1065
  </div>
976
1066
 
977
- <!-- CCC Agent 卡片 -->
978
- <div class="agent-card" id="agent-card-ccc">
979
- <div class="agent-card-header">
980
- <input type="checkbox" class="agent-toggle" id="agent-enable-ccc" onchange="onAgentToggle('ccc', this.checked)">
981
- <div class="meta">
982
- <div class="name">CCC Agent</div>
983
- <div class="desc">ChatCCC 内置 Agent<br>使用 DeepSeek 兼容 API</div>
984
- </div>
985
- </div>
986
- <label class="agent-default-row">
987
- <input type="checkbox" id="agent-default-ccc" onchange="onDefaultAgentToggle('ccc', this.checked)">
988
- 设为默认 Agent
989
- </label>
990
- <fieldset class="agent-body" id="agent-body-ccc" disabled>
991
- <div class="form-group">
992
- <label>API Key</label>
993
- <input type="password" id="field-CHATCCC_CCC_API_KEY" placeholder="DeepSeek 兼容 API Key">
994
- </div>
995
- <div class="form-group">
996
- <label>Base URL</label>
997
- <input type="text" id="field-CHATCCC_CCC_BASE_URL" placeholder="https://api.deepseek.com/v1">
998
- </div>
999
- <div class="form-group">
1000
- <label>模型</label>
1001
- <input type="text" id="field-CHATCCC_CCC_MODEL" placeholder="deepseek-v4-pro">
1002
- </div>
1003
- <div class="form-group">
1004
- <label>备选模型(选填)</label>
1005
- <input type="text" id="field-CHATCCC_CCC_ALTERNATIVE_MODEL" placeholder="加入 /model 列表,便于会话内切换">
1006
- </div>
1007
- <div class="form-group">
1008
- <label>Effort(推理强度,选填)</label>
1009
- <select id="field-CHATCCC_CCC_EFFORT">
1010
- <option value="">(留空/默认,服务端 medium)</option>
1011
- <option value="none">none - 直接作答,最省 token</option>
1012
- <option value="minimal">minimal</option>
1013
- <option value="low">low</option>
1014
- <option value="medium">medium</option>
1015
- <option value="high">high</option>
1016
- <option value="xhigh">xhigh</option>
1017
- <option value="max">max - 最强推理</option>
1018
- </select>
1019
- </div>
1020
- </fieldset>
1021
- </div>
1022
-
1023
1067
  </div>
1024
1068
 
1025
1069
  <div class="btn-group" style="justify-content:space-between">
@@ -2182,8 +2226,87 @@ function validateCli(tool) {
2182
2226
  });
2183
2227
  }
2184
2228
 
2229
+ // ---- Claude Code 引擎(Agent SDK)按需安装 ----
2230
+ var claudeEnginePollTimer = null;
2231
+
2232
+ function claudeEngineEl(id) { return document.getElementById(id); }
2233
+
2234
+ function claudeEngineRenderStatus(s) {
2235
+ var el = claudeEngineEl('claude-engine-status');
2236
+ if (!el) return;
2237
+ var phase = s.phase;
2238
+ var text = s.message || '';
2239
+ var color = '#64748b';
2240
+ if (phase === 'done') color = '#16a34a';
2241
+ else if (phase === 'error') color = '#ef4444';
2242
+ else if (phase === 'downloading' || phase === 'installing') color = '#3b82f6';
2243
+ el.innerHTML = '<span style="color:' + color + '">' + (text ? text : '未知状态') + '</span>';
2244
+ if (s.error) el.innerHTML += '<br><span style="color:#ef4444;font-size:12px">' + s.error + '</span>';
2245
+ }
2246
+
2247
+ function claudeEngineRenderProgress(p) {
2248
+ var wrap = claudeEngineEl('claude-engine-progress-wrap');
2249
+ if (!wrap) return;
2250
+ if (p && (p.phase === 'downloading' || p.phase === 'installing' || p.phase === 'detecting')) {
2251
+ wrap.style.display = 'block';
2252
+ claudeEngineEl('claude-engine-progress-bar').style.width = (p.percent || 0) + '%';
2253
+ claudeEngineEl('claude-engine-progress-text').textContent = p.message || '';
2254
+ } else {
2255
+ wrap.style.display = 'none';
2256
+ }
2257
+ }
2258
+
2259
+ function claudeEngineRefreshStatus() {
2260
+ api('/api/claude-sdk/status', 'GET').then(function(s){
2261
+ if (!s || !s.phase) return;
2262
+ claudeEngineRenderStatus(s);
2263
+ claudeEngineRenderProgress(s);
2264
+ var btn = claudeEngineEl('claude-engine-install-btn');
2265
+ if (btn) {
2266
+ if (s.phase === 'done' || s.installed) {
2267
+ btn.textContent = '重新安装引擎';
2268
+ btn.disabled = false;
2269
+ } else if (s.phase === 'downloading' || s.phase === 'installing' || s.phase === 'detecting') {
2270
+ btn.textContent = '安装中…';
2271
+ btn.disabled = true;
2272
+ } else {
2273
+ btn.textContent = '安装引擎(约 220MB)';
2274
+ btn.disabled = false;
2275
+ }
2276
+ }
2277
+ if (s.phase === 'downloading' || s.phase === 'installing' || s.phase === 'detecting') {
2278
+ claudeEnginePollTimer = setTimeout(claudeEngineRefreshStatus, 600);
2279
+ } else if (claudeEnginePollTimer) {
2280
+ clearTimeout(claudeEnginePollTimer);
2281
+ claudeEnginePollTimer = null;
2282
+ }
2283
+ }).catch(function(){
2284
+ // 网络/服务异常时停止轮询,避免无限重试
2285
+ if (claudeEnginePollTimer) { clearTimeout(claudeEnginePollTimer); claudeEnginePollTimer = null; }
2286
+ });
2287
+ }
2288
+
2289
+ function installClaudeEngine() {
2290
+ var btn = claudeEngineEl('claude-engine-install-btn');
2291
+ if (btn) btn.disabled = true;
2292
+ api('/api/claude-sdk/install', 'POST').then(function(r){
2293
+ if (r.ok) {
2294
+ claudeEngineRefreshStatus();
2295
+ } else {
2296
+ claudeEngineRenderStatus({ phase: 'error', message: '启动安装失败', error: r.error || '' });
2297
+ if (btn) btn.disabled = false;
2298
+ }
2299
+ }).catch(function(e){
2300
+ claudeEngineRenderStatus({ phase: 'error', message: '请求失败', error: String(e) });
2301
+ if (btn) btn.disabled = false;
2302
+ });
2303
+ }
2304
+
2185
2305
  // ---- Start ----
2186
2306
  init();
2307
+
2308
+ // 进入 dashboard/向导后初始化 Claude 引擎状态(页面元素此时已存在)
2309
+ setTimeout(claudeEngineRefreshStatus, 300);
2187
2310
  </script>
2188
2311
  </body>
2189
2312
  </html>`;
@@ -2209,6 +2332,8 @@ async function handleRequest(req: IncomingMessage, res: ServerResponse): Promise
2209
2332
  if (url === "/api/restart" && method === "POST") return handleRestartService(req, res);
2210
2333
  if (url === "/api/validate" && method === "POST") return handleValidate(req, res);
2211
2334
  if (url === "/api/ilink/forget" && method === "POST") return handleForgetIlink(req, res);
2335
+ if (url === "/api/claude-sdk/status" && method === "GET") return handleClaudeSdkStatus(req, res);
2336
+ if (url === "/api/claude-sdk/install" && method === "POST") return handleClaudeSdkInstall(req, res);
2212
2337
 
2213
2338
  if (method === "GET" && (pathname === "/agent-team" || pathname === "/agent-team/")) {
2214
2339
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });