aicodeswitch 5.2.12 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,8 +4,9 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>AI Code Switch</title>
7
- <script type="module" crossorigin src="./assets/index-BFVjD9Y2.js"></script>
8
- <link rel="stylesheet" crossorigin href="./assets/index-Dm34-4zP.css">
7
+ <script type="module" crossorigin src="./assets/index-QZBX5HHX.js"></script>
8
+ <link rel="modulepreload" crossorigin href="./assets/three-CFpmPosW.js">
9
+ <link rel="stylesheet" crossorigin href="./assets/index-CQVhBlBB.css">
9
10
  </head>
10
11
  <body>
11
12
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aicodeswitch",
3
- "version": "5.2.12",
3
+ "version": "6.0.0",
4
4
  "description": "A tool to help you manage AI programming tools to access large language models locally. It allows your Claude Code, Codex and other tools to no longer be limited to official models.",
5
5
  "author": "tangshuang",
6
6
  "license": "GPL-3.0",
@@ -35,7 +35,7 @@
35
35
  "aicodeswitch": "./bin/cli.js"
36
36
  },
37
37
  "scripts": {
38
- "dev": "concurrently \"npm run dev:server\" \"npm run dev:ui\"",
38
+ "dev": "node scripts/dev.js",
39
39
  "dev:ui": "vite",
40
40
  "dev:server": "tsx --tsconfig=tsconfig.server.json src/server/main.ts",
41
41
  "dev:server:watch": "tsx watch --tsconfig=tsconfig.server.json --clear-screen=false --watch-kill-signal=SIGINT src/server/main.ts",
@@ -80,6 +80,7 @@
80
80
  "@types/node": "^20.11.5",
81
81
  "@types/react": "^18.2.43",
82
82
  "@types/react-dom": "^18.2.17",
83
+ "@types/three": "^0.165.0",
83
84
  "@typescript-eslint/eslint-plugin": "^6.14.0",
84
85
  "@typescript-eslint/parser": "^6.14.0",
85
86
  "@vitejs/plugin-react": "^4.2.1",
@@ -96,6 +97,7 @@
96
97
  "react-router-dom": "^6.21.3",
97
98
  "recharts": "^3.7.0",
98
99
  "standard-version": "^9.5.0",
100
+ "three": "^0.165.0",
99
101
  "tsx": "^4.7.0",
100
102
  "typescript": "^5.2.2",
101
103
  "vite": "^5.0.8"
package/scripts/dev.js ADDED
@@ -0,0 +1,283 @@
1
+ /**
2
+ * 开发模式启动器(替代 concurrently)
3
+ *
4
+ * 目标:
5
+ * 1. 先启动 server,轮询 /health 确认服务可用后,再启动 UI(避免 UI 起来后代理目标尚未就绪)。
6
+ * 2. Ctrl+C 后「同步阻塞」到服务子进程真正退出(即服务端打印 Server stopped. 并
7
+ * process.exit 之后)再退出父进程,从而避免「终端提示符已回但端口 4567 尚未释放」
8
+ * 导致的快速重启 EADDRINUSE 冲突。
9
+ *
10
+ * 原理:Node 无法真正同步阻塞事件循环,但只要本进程(前台进程组长)不调用
11
+ * process.exit,终端就不会回到提示符。因此我们在信号处理器里 await 子进程的
12
+ * 'exit' 事件,等价于「等到 Server stopped. 出现」。
13
+ *
14
+ * 直接运行:node scripts/dev.js(package.json 的 dev 脚本)
15
+ */
16
+
17
+ const { spawn } = require('child_process');
18
+ const path = require('path');
19
+ const readline = require('readline');
20
+ const http = require('http');
21
+
22
+ // 可选彩色前缀;非 TTY 时 chalk 自动失活,无副作用
23
+ let chalk;
24
+ try {
25
+ // eslint-disable-next-line global-require
26
+ chalk = require('chalk');
27
+ } catch {
28
+ chalk = {
29
+ cyan: (s) => s,
30
+ magenta: (s) => s,
31
+ gray: (s) => s,
32
+ yellow: (s) => s,
33
+ };
34
+ }
35
+
36
+ const ROOT = path.resolve(__dirname, '..');
37
+
38
+ // 等待子进程优雅退出的硬超时(秒)。需大于服务端 server.close 的 5s 上限 + 配置恢复余量。
39
+ const SHUTDOWN_TIMEOUT_MS = 15000;
40
+
41
+ // 服务端健康检查配置:轮询 /health 直到服务可用再启动 UI。
42
+ const SERVER_PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 4567;
43
+ const SERVER_HOST = '127.0.0.1';
44
+ const HEALTH_PATH = '/health';
45
+ const HEALTH_POLL_INTERVAL_MS = 300;
46
+ const HEALTH_WAIT_TIMEOUT_MS = 30000;
47
+
48
+ const SERVER_LABEL = chalk.cyan('[server]');
49
+ const UI_LABEL = chalk.magenta('[ui]');
50
+ const DEV_LABEL = chalk.gray('[dev]');
51
+
52
+ /**
53
+ * 探测服务端 /health 是否可用(返回 2xx 即视为就绪)。
54
+ * @returns {Promise<boolean>}
55
+ */
56
+ function checkServerHealth() {
57
+ return new Promise((resolve) => {
58
+ const req = http.get(
59
+ {
60
+ host: SERVER_HOST,
61
+ port: SERVER_PORT,
62
+ path: HEALTH_PATH,
63
+ timeout: 1500,
64
+ },
65
+ (res) => {
66
+ res.resume();
67
+ resolve(res.statusCode !== undefined && res.statusCode >= 200 && res.statusCode < 300);
68
+ },
69
+ );
70
+ req.on('error', () => resolve(false));
71
+ req.on('timeout', () => { req.destroy(); resolve(false); });
72
+ });
73
+ }
74
+
75
+ /**
76
+ * 轮询等待服务端可用。期间若 serverChild 已退出,立即返回 false。
77
+ * @param {import('child_process').ChildProcess} serverChild
78
+ * @returns {Promise<boolean>} 服务是否在超时内就绪
79
+ */
80
+ async function waitForServerReady(serverChild) {
81
+ const deadline = Date.now() + HEALTH_WAIT_TIMEOUT_MS;
82
+ while (Date.now() < deadline) {
83
+ // 服务进程已退出则无需再等
84
+ if (serverChild.exitCode !== null || serverChild.signalCode) return false;
85
+ if (await checkServerHealth()) return true; // eslint-disable-line no-await-in-loop
86
+ await new Promise((r) => setTimeout(r, HEALTH_POLL_INTERVAL_MS)); // eslint-disable-line no-await-in-loop
87
+ }
88
+ return false;
89
+ }
90
+
91
+ /**
92
+ * 给子进程的 stdout/stderr 加行级前缀后写出。
93
+ * @param {import('child_process').ChildProcess} child
94
+ * @param {string} label
95
+ */
96
+ function prefixStream(child, label) {
97
+ for (const streamName of ['stdout', 'stderr']) {
98
+ const stream = child[streamName];
99
+ if (!stream) continue;
100
+ const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
101
+ rl.on('line', (line) => {
102
+ process.stdout.write(`${label} ${line}\n`);
103
+ });
104
+ }
105
+ }
106
+
107
+ const IS_WIN = process.platform === 'win32';
108
+
109
+ /**
110
+ * 进程组是否仍存活(组内还有任意进程)。
111
+ * 子进程以 detached:true 启动后,其 pid == pgid,故 -pid 可探测整组。
112
+ * 关键点:tsx 会 fork——serverChild 只是 tsx 启动器(launcher),真正运行
113
+ * main.ts 的 node 进程、以及 esbuild 服务进程都是它的子孙,与启动器同组。
114
+ * 启动器的 'exit'/'close' 在它自身一退出就触发(Ctrl+C 时几乎立刻),
115
+ * 而真正的服务进程仍在做数秒优雅关闭并随后打印 "Server stopped."。
116
+ * 因此「等启动器退出」≠「等服务停止」。改用「轮询整组存活」:只要组内
117
+ * 还有任何进程(真正的 node 服务),就算未停止;等整组清空才等价于
118
+ * 「Server stopped. 已出现并 process.exit」。
119
+ */
120
+ function groupAlive(pid) {
121
+ if (pid == null) return false;
122
+ try {
123
+ process.kill(-pid, 0); // 信号 0 = 探测存在性;-pid = 整个进程组
124
+ return true;
125
+ } catch {
126
+ return false;
127
+ }
128
+ }
129
+
130
+ /**
131
+ * 向子进程所在进程组发信号(覆盖 launcher + 真正服务 + esbuild)。
132
+ * Windows 无进程组语义,退化为直接 kill 直接子进程。
133
+ */
134
+ function signalGroup(child, signal) {
135
+ if (!child || child.exitCode !== null || child.signalCode) return;
136
+ const pid = child.pid;
137
+ if (IS_WIN || pid == null) {
138
+ try { child.kill(signal); } catch { /* ignore */ }
139
+ return;
140
+ }
141
+ try {
142
+ process.kill(-pid, signal);
143
+ } catch {
144
+ try { child.kill(signal); } catch { /* ignore */ }
145
+ }
146
+ }
147
+
148
+ /**
149
+ * 等待整个进程组清空(POSIX),Windows 退化为等待直接子进程 'close'。
150
+ * 不带内部超时——超时由调用方用 SIGKILL 清组后,本函数下一拍(≤100ms)即感知并 resolve。
151
+ * @returns {Promise<number>} 直接子进程的 exit code(仅用于异常码上报)
152
+ */
153
+ function waitForGroupExit(child) {
154
+ return new Promise((resolve) => {
155
+ const pid = child.pid;
156
+ if (IS_WIN || pid == null) {
157
+ if (child.exitCode !== null || child.signalCode) { resolve(child.exitCode ?? 0); return; }
158
+ child.once('close', (code, signal) => resolve(code ?? (signal ? 128 + 1 : 0)));
159
+ return;
160
+ }
161
+ if (!groupAlive(pid)) { resolve(child.exitCode ?? 0); return; }
162
+ const iv = setInterval(() => {
163
+ if (!groupAlive(pid)) {
164
+ clearInterval(iv);
165
+ resolve(child.exitCode ?? 0);
166
+ }
167
+ }, 100);
168
+ });
169
+ }
170
+
171
+ async function main() {
172
+ let exitCode = 0;
173
+ let shuttingDown = false;
174
+ let uiChild = null;
175
+
176
+ // 直接调本地 bin,去掉 `npm run` 额外进程层。
177
+ // POSIX 不开 shell(避免中间 /bin/sh 抢先于 SIGINT 退出);Windows 的 .cmd 仍需 shell。
178
+ // detached:true:把子进程放进它自己的进程组(POSIX setsid),这样我们可以用
179
+ // process.kill(-pid, sig) 给整组发信号、用 groupAlive(-pid) 探测整组存活。
180
+ // 这一点对本脚本的核心目标至关重要——见 waitForGroupExit 注释:tsx 会 fork,
181
+ // serverChild 只是启动器,真正的 node 服务是它的孙进程,只有「整组存活探测」
182
+ // 才能把退出阻塞到真正的 "Server stopped."。
183
+ const USE_SHELL = IS_WIN;
184
+ const serverChild = spawn(
185
+ 'tsx',
186
+ ['--tsconfig=tsconfig.server.json', 'src/server/main.ts'],
187
+ { cwd: ROOT, shell: USE_SHELL, detached: !IS_WIN, stdio: ['inherit', 'pipe', 'pipe'] },
188
+ );
189
+ prefixStream(serverChild, SERVER_LABEL);
190
+
191
+ // 在启动 UI 之前注册信号处理与 server 退出级联,确保等待健康期间 Ctrl+C 也能正确清理
192
+ process.on('SIGINT', () => { void stop('Ctrl+C'); });
193
+ process.on('SIGTERM', () => { void stop('SIGTERM'); });
194
+
195
+ serverChild.once('exit', (code) => {
196
+ if (!shuttingDown) {
197
+ process.stderr.write(
198
+ `${DEV_LABEL} 服务进程已退出${code ? `(code=${code})` : ''},停止 UI 进程。\n`,
199
+ );
200
+ if (code && code !== 0) exitCode = code;
201
+ void stop();
202
+ }
203
+ });
204
+
205
+ // 1) 先等待服务端 /health 可用,再启动 UI,避免 UI 启动时代理目标尚未就绪
206
+ process.stdout.write(
207
+ `${DEV_LABEL} 等待服务端就绪(http://${SERVER_HOST}:${SERVER_PORT}${HEALTH_PATH})...\n`,
208
+ );
209
+ const ready = await waitForServerReady(serverChild);
210
+ if (!ready) {
211
+ process.stderr.write(
212
+ `${DEV_LABEL} 服务端在 ${HEALTH_WAIT_TIMEOUT_MS / 1000}s 内未就绪,放弃启动 UI。\n`,
213
+ );
214
+ // 走正常停止流程:清理可能残留的服务子进程后退出
215
+ await stop();
216
+ return;
217
+ }
218
+ process.stdout.write(`${DEV_LABEL} 服务端已就绪,启动 UI...\n`);
219
+
220
+ // 2) 启动 UI(同样放进独立进程组,便于整组信号/清理,避免 vite 的 esbuild 子进程残留)
221
+ uiChild = spawn('vite', [], {
222
+ cwd: ROOT,
223
+ shell: USE_SHELL,
224
+ detached: !IS_WIN,
225
+ stdio: ['inherit', 'pipe', 'pipe'],
226
+ });
227
+ prefixStream(uiChild, UI_LABEL);
228
+
229
+ uiChild.once('exit', (code) => {
230
+ if (!shuttingDown) {
231
+ process.stderr.write(
232
+ `${DEV_LABEL} UI 进程已退出${code ? `(code=${code})` : ''},停止服务进程。\n`,
233
+ );
234
+ if (code && code !== 0) exitCode = code;
235
+ void stop();
236
+ }
237
+ });
238
+
239
+ /**
240
+ * 停止流程:向子进程转发信号,等待服务子进程(长时序)与 UI 子进程(若已启动)都退出。
241
+ * 关键:不在此处立即 process.exit——父进程存活 = 终端提示符不回。
242
+ */
243
+ async function stop(reason) {
244
+ if (shuttingDown) return;
245
+ shuttingDown = true;
246
+
247
+ if (reason) {
248
+ process.stdout.write(
249
+ `${DEV_LABEL} ${chalk.yellow(reason)},等待服务完全停止后再退出...\n`,
250
+ );
251
+ }
252
+
253
+ // 子进程各自在独立进程组里,Ctrl+C 不会直达它们,必须由父进程显式给整组发 SIGINT,
254
+ // 真正的服务进程才会进入优雅关闭(恢复配置 + server.close + 打印 "Server stopped.")。
255
+ signalGroup(serverChild, 'SIGINT');
256
+ signalGroup(uiChild, 'SIGINT');
257
+
258
+ let timedOut = false;
259
+ const timer = setTimeout(() => {
260
+ timedOut = true;
261
+ process.stderr.write(
262
+ `${DEV_LABEL} 等待超时(${SHUTDOWN_TIMEOUT_MS / 1000}s),强制终止残留子进程。\n`,
263
+ );
264
+ signalGroup(serverChild, 'SIGKILL');
265
+ signalGroup(uiChild, 'SIGKILL');
266
+ }, SHUTDOWN_TIMEOUT_MS);
267
+
268
+ // 阻塞到「整组清空」:等真正的 node 服务进程也退出(而不仅是 tsx 启动器)。
269
+ const exits = [waitForGroupExit(serverChild)];
270
+ if (uiChild) exits.push(waitForGroupExit(uiChild));
271
+ const [serverCode, uiCode] = await Promise.all(exits);
272
+
273
+ clearTimeout(timer);
274
+
275
+ if (timedOut) exitCode = 1;
276
+ else if (serverCode && serverCode !== 0) exitCode = serverCode;
277
+ else if (uiCode && uiCode !== 0) exitCode = uiCode;
278
+
279
+ process.exit(exitCode);
280
+ }
281
+ }
282
+
283
+ main();
@@ -1,203 +0,0 @@
1
- "use strict";
2
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
- return new (P || (P = Promise))(function (resolve, reject) {
5
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
- step((generator = generator.apply(thisArg, _arguments || [])).next());
9
- });
10
- };
11
- var __importDefault = (this && this.__importDefault) || function (mod) {
12
- return (mod && mod.__esModule) ? mod : { "default": mod };
13
- };
14
- Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.getToolsInstallationStatus = getToolsInstallationStatus;
16
- exports.installTool = installTool;
17
- const child_process_1 = require("child_process");
18
- const os_1 = __importDefault(require("os"));
19
- /**
20
- * 检测工具是否已安装
21
- */
22
- function checkToolInstalled(toolName) {
23
- return __awaiter(this, void 0, void 0, function* () {
24
- console.log(`[ToolsService] 开始检测工具: ${toolName}`);
25
- return new Promise((resolve) => {
26
- var _a, _b;
27
- const command = toolName === 'claude-code' ? 'claude' : 'codex';
28
- console.log(`[ToolsService] 执行命令: ${command} --version`);
29
- const child = (0, child_process_1.spawn)(command, ['--version'], {
30
- shell: true,
31
- stdio: ['ignore', 'pipe', 'pipe'],
32
- windowsHide: true, // 隐藏 Windows 命令行窗口,避免检测时闪窗
33
- });
34
- let stdout = '';
35
- let stderr = '';
36
- (_a = child.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (data) => {
37
- stdout += data.toString();
38
- console.log(`[ToolsService] ${toolName} stdout:`, data.toString());
39
- });
40
- (_b = child.stderr) === null || _b === void 0 ? void 0 : _b.on('data', (data) => {
41
- stderr += data.toString();
42
- console.log(`[ToolsService] ${toolName} stderr:`, data.toString());
43
- });
44
- child.on('close', (code) => {
45
- console.log(`[ToolsService] ${toolName} 进程退出,退出码: ${code}`);
46
- if (code === 0) {
47
- // 尝试从输出中提取版本号
48
- const versionMatch = stdout.match(/(\d+\.\d+\.\d+)/) || stderr.match(/(\d+\.\d+\.\d+)/);
49
- resolve({
50
- installed: true,
51
- version: versionMatch ? versionMatch[1] : 'unknown',
52
- });
53
- }
54
- else {
55
- resolve({ installed: false });
56
- }
57
- });
58
- child.on('error', (err) => {
59
- console.log(`[ToolsService] ${toolName} 检测出错:`, err);
60
- resolve({ installed: false });
61
- });
62
- // 10秒超时
63
- setTimeout(() => {
64
- console.log(`[ToolsService] ${toolName} 检测超时`);
65
- child.kill();
66
- resolve({ installed: false });
67
- }, 10000);
68
- });
69
- });
70
- }
71
- /**
72
- * 获取工具的安装命令
73
- */
74
- function getInstallCommand(toolName) {
75
- const platform = os_1.default.platform();
76
- const tool = toolName === 'claude-code' ? '@anthropic-ai/claude-code' : '@openai/codex';
77
- if (platform === 'win32') {
78
- return `npm install -g ${tool}`;
79
- }
80
- else {
81
- // macOS 和 Linux 需要 sudo
82
- return `sudo npm install -g ${tool}`;
83
- }
84
- }
85
- /**
86
- * 检测所有工具的安装状态
87
- */
88
- function getToolsInstallationStatus() {
89
- return __awaiter(this, void 0, void 0, function* () {
90
- const [claudeCodeStatus, codexStatus] = yield Promise.all([
91
- checkToolInstalled('claude-code'),
92
- checkToolInstalled('codex'),
93
- ]);
94
- return {
95
- claudeCode: Object.assign(Object.assign({}, claudeCodeStatus), { installCommand: claudeCodeStatus.installed ? undefined : getInstallCommand('claude-code') }),
96
- codex: Object.assign(Object.assign({}, codexStatus), { installCommand: codexStatus.installed ? undefined : getInstallCommand('codex') }),
97
- };
98
- });
99
- }
100
- /**
101
- * 执行工具安装
102
- */
103
- function installTool(toolName, callbacks) {
104
- var _a, _b;
105
- const command = getInstallCommand(toolName);
106
- const platform = os_1.default.platform();
107
- console.log(`[ToolsService] ========== 开始安装 ${toolName} ==========`);
108
- console.log(`[ToolsService] 操作系统: ${platform}`);
109
- console.log(`[ToolsService] 安装命令: ${command}`);
110
- // 立即发送启动消息,让前端快速进入 terminal 界面
111
- callbacks.onData(`\n========== 开始安装 ${toolName} ==========\n`);
112
- callbacks.onData(`操作系统: ${platform}\n`);
113
- callbacks.onData(`执行命令: ${command}\n`);
114
- callbacks.onData(`进程启动中...\n\n`);
115
- // 在 macOS/Linux 上,需要使用 sudo 并且可能需要输入密码
116
- // 在 Windows 上直接使用 cmd 执行 npm 命令
117
- let child;
118
- if (platform === 'win32') {
119
- // Windows: 使用 cmd.exe 执行命令
120
- console.log(`[ToolsService] Windows 环境,使用 cmd.exe`);
121
- child = (0, child_process_1.spawn)('cmd.exe', ['/c', command], {
122
- stdio: ['pipe', 'pipe', 'pipe'],
123
- shell: false,
124
- windowsHide: true, // 隐藏命令行窗口
125
- });
126
- }
127
- else {
128
- // Unix: 使用 sh 执行命令
129
- console.log(`[ToolsService] Unix 环境,使用 sh`);
130
- child = (0, child_process_1.spawn)('sh', ['-c', command], {
131
- stdio: ['pipe', 'pipe', 'pipe'],
132
- shell: false,
133
- });
134
- }
135
- console.log(`[ToolsService] 子进程 PID: ${child.pid}`);
136
- // 立即发送进程信息
137
- callbacks.onData(`子进程已创建 (PID: ${child.pid})\n`);
138
- callbacks.onData(`等待 npm 输出...\n\n`);
139
- // 设置超时:30分钟后自动终止
140
- const timeout = 30 * 60 * 1000;
141
- const timeoutHandle = setTimeout(() => {
142
- console.error(`[ToolsService] ${toolName} 安装超时 (${timeout}ms),终止进程`);
143
- child.kill('SIGTERM');
144
- callbacks.onError(`\n安装超时 (${timeout / 60000} 分钟),请检查网络连接或手动执行命令:\n${command}\n`);
145
- }, timeout);
146
- let hasReceivedData = false;
147
- (_a = child.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (data) => {
148
- const output = data.toString();
149
- if (!hasReceivedData) {
150
- console.log(`[ToolsService] ${toolName} 首次收到 stdout 数据`);
151
- hasReceivedData = true;
152
- }
153
- console.log(`[ToolsService] ${toolName} stdout:`, output.slice(0, 200)); // 只记录前200字符
154
- callbacks.onData(output);
155
- });
156
- (_b = child.stderr) === null || _b === void 0 ? void 0 : _b.on('data', (data) => {
157
- const output = data.toString();
158
- if (!hasReceivedData) {
159
- console.log(`[ToolsService] ${toolName} 首次收到 stderr 数据`);
160
- hasReceivedData = true;
161
- }
162
- console.log(`[ToolsService] ${toolName} stderr:`, output.slice(0, 200)); // 只记录前200字符
163
- callbacks.onError(output);
164
- });
165
- child.on('close', (code) => {
166
- clearTimeout(timeoutHandle);
167
- console.log(`[ToolsService] ${toolName} 安装进程关闭,退出码: ${code}`);
168
- if (code === 0) {
169
- callbacks.onData(`\n========== 安装完成 ==========\n`);
170
- }
171
- else if (code === null) {
172
- callbacks.onError(`\n========== 安装被终止 ==========\n`);
173
- }
174
- else {
175
- callbacks.onError(`\n========== 安装失败 (退出码: ${code}) ==========\n`);
176
- }
177
- callbacks.onClose(code);
178
- });
179
- child.on('error', (err) => {
180
- clearTimeout(timeoutHandle);
181
- console.error(`[ToolsService] ${toolName} 安装进程错误:`, err);
182
- callbacks.onError(`启动安装进程失败: ${err.message}\n`);
183
- callbacks.onError(`错误详情: ${err}\n`);
184
- callbacks.onClose(null);
185
- });
186
- // 处理进程退出信号
187
- child.on('exit', (_code, signal) => {
188
- clearTimeout(timeoutHandle);
189
- if (signal) {
190
- console.log(`[ToolsService] ${toolName} 进程被信号终止: ${signal}`);
191
- callbacks.onError(`\n安装进程被信号终止: ${signal}\n`);
192
- }
193
- });
194
- // 如果10秒后还没有收到任何数据,发送提示
195
- setTimeout(() => {
196
- if (!hasReceivedData) {
197
- console.log(`[ToolsService] ${toolName} 10秒内未收到输出,发送等待提示`);
198
- callbacks.onData(`[提示] 正在连接 npm 服务器,这可能需要一些时间...\n`);
199
- callbacks.onData(`[提示] 如果持续无响应,请检查网络连接或 npm 配置\n\n`);
200
- }
201
- }, 10000);
202
- return child;
203
- }
@@ -1,148 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.ToolInstallationWS = void 0;
7
- exports.createToolInstallationWSServer = createToolInstallationWSServer;
8
- // @ts-ignore - ws 类型声明可能需要手动安装 @types/ws
9
- const ws_1 = require("ws");
10
- const tools_service_1 = require("./tools-service");
11
- const os_1 = __importDefault(require("os"));
12
- /**
13
- * WebSocket 连接管理
14
- */
15
- class ToolInstallationWS {
16
- constructor(ws, req) {
17
- Object.defineProperty(this, "ws", {
18
- enumerable: true,
19
- configurable: true,
20
- writable: true,
21
- value: void 0
22
- });
23
- Object.defineProperty(this, "childProcess", {
24
- enumerable: true,
25
- configurable: true,
26
- writable: true,
27
- value: null
28
- });
29
- this.ws = ws;
30
- console.log(`[WS] 新的 WebSocket 连接: ${req.socket.remoteAddress}`);
31
- this.ws.on('message', (data) => {
32
- try {
33
- const message = JSON.parse(data.toString());
34
- console.log(`[WS] 收到消息:`, message.type);
35
- if (message.type === 'input') {
36
- // 用户输入(如密码),发送到子进程的 stdin
37
- if (this.childProcess && this.childProcess.stdin) {
38
- console.log(`[WS] 发送用户输入到子进程:`, message.data.slice(0, 10));
39
- this.childProcess.stdin.write(message.data);
40
- }
41
- else {
42
- console.warn(`[WS] 子进程不存在或无 stdin,无法发送输入`);
43
- }
44
- }
45
- }
46
- catch (err) {
47
- console.error(`[WS] 解析消息失败:`, err);
48
- }
49
- });
50
- this.ws.on('close', () => {
51
- console.log(`[WS] WebSocket 连接关闭`);
52
- this.cleanup();
53
- });
54
- this.ws.on('error', (err) => {
55
- console.error(`[WS] WebSocket 错误:`, err);
56
- this.cleanup();
57
- });
58
- }
59
- /**
60
- * 开始安装工具
61
- */
62
- startInstallation(toolName) {
63
- console.log(`[WS] 开始安装 ${toolName}`);
64
- const callbacks = {
65
- onData: (data) => {
66
- this.sendMessage({ type: 'stdout', data });
67
- },
68
- onError: (data) => {
69
- this.sendMessage({ type: 'stderr', data });
70
- },
71
- onClose: (code) => {
72
- const success = code === 0;
73
- this.sendMessage({ type: 'close', data: { code, success } });
74
- // 延迟关闭连接,让客户端收到最终消息
75
- setTimeout(() => {
76
- this.ws.close();
77
- }, 1000);
78
- },
79
- };
80
- // 启动安装进程
81
- this.childProcess = (0, tools_service_1.installTool)(toolName, callbacks);
82
- // 发送启动信息
83
- const platform = os_1.default.platform();
84
- const command = platform === 'win32'
85
- ? `npm install -g ${toolName === 'claude-code' ? '@anthropic-ai/claude-code' : '@openai/codex'}`
86
- : `sudo npm install -g ${toolName === 'claude-code' ? '@anthropic-ai/claude-code' : '@openai/codex'}`;
87
- this.sendMessage({
88
- type: 'start',
89
- data: {
90
- tool: toolName,
91
- os: platform,
92
- command,
93
- pid: this.childProcess.pid || 0,
94
- },
95
- });
96
- }
97
- /**
98
- * 发送消息到客户端
99
- */
100
- sendMessage(message) {
101
- if (this.ws.readyState === ws_1.WebSocket.OPEN) {
102
- this.ws.send(JSON.stringify(message));
103
- }
104
- }
105
- /**
106
- * 清理资源
107
- */
108
- cleanup() {
109
- if (this.childProcess) {
110
- console.log(`[WS] 清理资源,终止子进程`);
111
- // 给进程5秒时间正常退出
112
- setTimeout(() => {
113
- if (this.childProcess && !this.childProcess.killed) {
114
- this.childProcess.kill('SIGTERM');
115
- }
116
- }, 5000);
117
- }
118
- }
119
- }
120
- exports.ToolInstallationWS = ToolInstallationWS;
121
- /**
122
- * 创建 WebSocket 服务器
123
- */
124
- function createToolInstallationWSServer() {
125
- const wss = new ws_1.WebSocketServer({ noServer: true });
126
- wss.on('connection', (ws, req) => {
127
- const wsHandler = new ToolInstallationWS(ws, req);
128
- // 监听第一次消息,获取要安装的工具名称
129
- ws.once('message', (data) => {
130
- try {
131
- const message = JSON.parse(data.toString());
132
- if (message.type === 'install' && message.tool) {
133
- wsHandler.startInstallation(message.tool);
134
- }
135
- else {
136
- ws.send(JSON.stringify({ type: 'error', data: '无效的请求' }));
137
- ws.close();
138
- }
139
- }
140
- catch (error) {
141
- console.error(`[WS] 解析安装请求失败:`, error);
142
- ws.send(JSON.stringify({ type: 'error', data: '解析请求失败' }));
143
- ws.close();
144
- }
145
- });
146
- });
147
- return wss;
148
- }