aicodeswitch 5.2.12 → 5.2.13

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,8 @@
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-CJMAVkIN.js"></script>
8
+ <link rel="stylesheet" crossorigin href="./assets/index-4liE4bV8.css">
9
9
  </head>
10
10
  <body>
11
11
  <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": "5.2.13",
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",
package/scripts/dev.js ADDED
@@ -0,0 +1,237 @@
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
+ /**
108
+ * 等待子进程 exit(触发于 process.exit 或被信号杀死)。
109
+ * @returns {Promise<number>} exit code 或 signal 字符串
110
+ */
111
+ function waitForExit(child) {
112
+ return new Promise((resolve) => {
113
+ if (child.exitCode !== null || child.signalCode) {
114
+ resolve(child.exitCode ?? 0);
115
+ return;
116
+ }
117
+ child.once('exit', (code, signal) => {
118
+ resolve(code ?? (signal ? 128 + 1 : 0));
119
+ });
120
+ });
121
+ }
122
+
123
+ function safeKill(child, signal) {
124
+ try {
125
+ if (child && child.exitCode === null && !child.signalCode) {
126
+ child.kill(signal);
127
+ }
128
+ } catch {
129
+ /* ignore */
130
+ }
131
+ }
132
+
133
+ async function main() {
134
+ let exitCode = 0;
135
+ let shuttingDown = false;
136
+ let uiChild = null;
137
+
138
+ // 直接调本地 bin,去掉 `npm run` 额外进程层
139
+ // shell:true 让 Windows 能找到 .cmd 版 bin
140
+ const serverChild = spawn(
141
+ 'tsx',
142
+ ['--tsconfig=tsconfig.server.json', 'src/server/main.ts'],
143
+ { cwd: ROOT, shell: true, stdio: ['inherit', 'pipe', 'pipe'] },
144
+ );
145
+ prefixStream(serverChild, SERVER_LABEL);
146
+
147
+ // 在启动 UI 之前注册信号处理与 server 退出级联,确保等待健康期间 Ctrl+C 也能正确清理
148
+ process.on('SIGINT', () => { void stop('Ctrl+C'); });
149
+ process.on('SIGTERM', () => { void stop('SIGTERM'); });
150
+
151
+ serverChild.once('exit', (code) => {
152
+ if (!shuttingDown) {
153
+ process.stderr.write(
154
+ `${DEV_LABEL} 服务进程已退出${code ? `(code=${code})` : ''},停止 UI 进程。\n`,
155
+ );
156
+ if (code && code !== 0) exitCode = code;
157
+ void stop();
158
+ }
159
+ });
160
+
161
+ // 1) 先等待服务端 /health 可用,再启动 UI,避免 UI 启动时代理目标尚未就绪
162
+ process.stdout.write(
163
+ `${DEV_LABEL} 等待服务端就绪(http://${SERVER_HOST}:${SERVER_PORT}${HEALTH_PATH})...\n`,
164
+ );
165
+ const ready = await waitForServerReady(serverChild);
166
+ if (!ready) {
167
+ process.stderr.write(
168
+ `${DEV_LABEL} 服务端在 ${HEALTH_WAIT_TIMEOUT_MS / 1000}s 内未就绪,放弃启动 UI。\n`,
169
+ );
170
+ // 走正常停止流程:清理可能残留的服务子进程后退出
171
+ await stop();
172
+ return;
173
+ }
174
+ process.stdout.write(`${DEV_LABEL} 服务端已就绪,启动 UI...\n`);
175
+
176
+ // 2) 启动 UI
177
+ uiChild = spawn('vite', [], {
178
+ cwd: ROOT,
179
+ shell: true,
180
+ stdio: ['inherit', 'pipe', 'pipe'],
181
+ });
182
+ prefixStream(uiChild, UI_LABEL);
183
+
184
+ uiChild.once('exit', (code) => {
185
+ if (!shuttingDown) {
186
+ process.stderr.write(
187
+ `${DEV_LABEL} UI 进程已退出${code ? `(code=${code})` : ''},停止服务进程。\n`,
188
+ );
189
+ if (code && code !== 0) exitCode = code;
190
+ void stop();
191
+ }
192
+ });
193
+
194
+ /**
195
+ * 停止流程:向子进程转发信号,等待服务子进程(长时序)与 UI 子进程(若已启动)都退出。
196
+ * 关键:不在此处立即 process.exit——父进程存活 = 终端提示符不回。
197
+ */
198
+ async function stop(reason) {
199
+ if (shuttingDown) return;
200
+ shuttingDown = true;
201
+
202
+ if (reason) {
203
+ process.stdout.write(
204
+ `${DEV_LABEL} ${chalk.yellow(reason)},等待服务完全停止后再退出...\n`,
205
+ );
206
+ }
207
+
208
+ // 子进程随进程组已直接收到信号,这里再显式转发一次(幂等,无副作用)
209
+ safeKill(serverChild, 'SIGINT');
210
+ safeKill(uiChild, 'SIGINT');
211
+
212
+ let timedOut = false;
213
+ const timer = setTimeout(() => {
214
+ timedOut = true;
215
+ process.stderr.write(
216
+ `${DEV_LABEL} 等待超时(${SHUTDOWN_TIMEOUT_MS / 1000}s),强制终止残留子进程。\n`,
217
+ );
218
+ safeKill(serverChild, 'SIGKILL');
219
+ safeKill(uiChild, 'SIGKILL');
220
+ }, SHUTDOWN_TIMEOUT_MS);
221
+
222
+ // 服务是长时序(配置恢复 + server.close 最多 5s),UI 通常已先退出
223
+ const exits = [waitForExit(serverChild)];
224
+ if (uiChild) exits.push(waitForExit(uiChild));
225
+ const [serverCode, uiCode] = await Promise.all(exits);
226
+
227
+ clearTimeout(timer);
228
+
229
+ if (timedOut) exitCode = 1;
230
+ else if (serverCode && serverCode !== 0) exitCode = serverCode;
231
+ else if (uiCode && uiCode !== 0) exitCode = uiCode;
232
+
233
+ process.exit(exitCode);
234
+ }
235
+ }
236
+
237
+ main();