aicodeswitch 3.9.3 → 4.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.
- package/README.md +37 -36
- package/UPGRADE.md +5 -3
- package/bin/restore.js +126 -22
- package/bin/start.js +29 -41
- package/bin/stop.js +3 -3
- package/bin/utils/config-helpers.js +198 -0
- package/dist/server/config-managed-fields.js +69 -0
- package/dist/server/config-merge.js +260 -0
- package/dist/server/database-factory.js +11 -181
- package/dist/server/fs-database.js +211 -31
- package/dist/server/main.js +380 -241
- package/dist/server/original-config-reader.js +154 -88
- package/dist/server/proxy-server.js +993 -385
- package/dist/server/rules-status-service.js +285 -54
- package/dist/server/transformers/chunk-collector.js +26 -4
- package/dist/server/transformers/streaming.js +2334 -280
- package/dist/server/transformers/transformers.js +1765 -0
- package/dist/ui/assets/index-GQBwe1Rm.js +514 -0
- package/dist/ui/index.html +1 -1
- package/package.json +4 -8
- package/schema/claude.schema.md +15 -14
- package/schema/deepseek-chat.schema.md +799 -0
- package/schema/{openai.schema.md → openai-chat-completions.schema.md} +9 -1083
- package/schema/openai-responses.schema.md +226196 -0
- package/schema/stream.md +2592 -0
- package/dist/server/database.js +0 -1609
- package/dist/server/migrate-to-fs.js +0 -353
- package/dist/server/transformers/claude-openai.js +0 -868
- package/dist/server/transformers/gemini.js +0 -625
- package/dist/ui/assets/index-DNtgPQMm.js +0 -511
package/README.md
CHANGED
|
@@ -14,11 +14,11 @@ AI Code Switch 是帮助你在本地管理 AI 编程工具接入大模型的工
|
|
|
14
14
|
|
|
15
15
|
* 可登记任意的大模型API接口服务,让你可以在一个面板管理你的大模型厂商
|
|
16
16
|
* 供应商API一键切换:可以注册多个供应商的多个API接口,当想要切换供应商时,只需要点击一个按钮就可以立即切换
|
|
17
|
-
* 自动配置:无需你手动去修改claude code或codex的系统配置文件,aicodeswitch
|
|
17
|
+
* 自动配置:无需你手动去修改claude code或codex的系统配置文件,aicodeswitch自动帮你修改,你只要启动它,无需配置claude code或codex就可以让它们正常工作
|
|
18
18
|
* 一键配置供应商,省去复杂的供应商配置流程
|
|
19
19
|
* API转流:将兼容openai接口标准的模型,轻松接入到claude code中,支持将符合openai、anthropic、gemini的数据格式转发给claude code或codex
|
|
20
20
|
* 按需代理:根据请求类型,让不同的模型来处理不同的任务,节省成本,基于该特性,可以让glm等原生非多模态模型支持图像识别
|
|
21
|
-
* 智能故障切换:当API服务发生故障时,智能切换到其他API
|
|
21
|
+
* 智能故障切换:当API服务发生故障时,智能切换到其他API服务进行尝试,无需手动切换,特别适用于中转服务商
|
|
22
22
|
* 代理:针对无法直接访问的模型服务,例如地区被ban,支持设置代理来解决
|
|
23
23
|
* tokens超量限制:避免其中一个服务商的用量超过限制,浪费了钱,适合有免费额度的服务商
|
|
24
24
|
* 次数超量限制:次数到达一定量后,切换其他服务商,适合coding plan的服务商
|
|
@@ -29,7 +29,8 @@ AI Code Switch 是帮助你在本地管理 AI 编程工具接入大模型的工
|
|
|
29
29
|
* 导入和导出:一键备份数据,在多太电脑间共享aicodeswitch配置
|
|
30
30
|
* 自定义API Key,支持B/S架构,让aicodeswitch成为在线服务,提供给团队使用
|
|
31
31
|
* 数据完全本地,自主可控
|
|
32
|
-
*
|
|
32
|
+
* 特殊语法:在发送的提示词最前面添加`[!]`来直接切换为高智商模型服务,简单快捷
|
|
33
|
+
* 服务端部署,可随时随地使用
|
|
33
34
|
|
|
34
35
|
## 桌面客户端
|
|
35
36
|
|
|
@@ -79,15 +80,15 @@ http://127.0.0.1:4567
|
|
|
79
80
|
|
|
80
81
|
**配置供应商**
|
|
81
82
|
|
|
82
|
-
*
|
|
83
|
-
*
|
|
83
|
+
* 什么是供应商?
|
|
84
|
+
* 供应商配置有什么用?
|
|
84
85
|
|
|
85
86
|
具体请看下方文档。
|
|
86
87
|
|
|
87
88
|
**路由配置**
|
|
88
89
|
|
|
89
|
-
*
|
|
90
|
-
*
|
|
90
|
+
* 什么是路由?
|
|
91
|
+
* 什么是路由规则?
|
|
91
92
|
|
|
92
93
|
具体请看下方文档。
|
|
93
94
|
|
|
@@ -115,22 +116,22 @@ Codex的配置覆盖逻辑一模一样。
|
|
|
115
116
|
|
|
116
117
|
通过将你所有的AI服务商统一起来管理,可以帮你:
|
|
117
118
|
|
|
118
|
-
1.
|
|
119
|
-
2.
|
|
120
|
-
3.
|
|
121
|
-
4.
|
|
119
|
+
1. 避免频繁修改配置文件,通过aicodeswitch,可以一键切换到不同的供应商的AI服务API
|
|
120
|
+
2. 通过aicodeswitch,将不同供应商的接口数据,转换为工具可以正确使用的接口数据格式,也就是说,你可以将Claude Code接入遵循openai的接口数据协议的其他接口
|
|
121
|
+
3. 避免你忘记曾经注册过那些供应商
|
|
122
|
+
4. 充分榨干不怎么用的供应商的服务,避免充值后不怎么用浪费了
|
|
122
123
|
|
|
123
124
|
### 什么事API服务的“源类型”
|
|
124
125
|
|
|
125
126
|
供应商接口返回的数据格式标准类型,目前支持以下几种:
|
|
126
127
|
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
128
|
+
* OpenAI(Responses API标准)
|
|
129
|
+
* OpenAI Chat (Chat Completions API标准)
|
|
130
|
+
* DeepSeek Chat(reasoning_content标准)
|
|
131
|
+
* Claude
|
|
132
|
+
* Claude Chat
|
|
133
|
+
* Gemini
|
|
134
|
+
* Gemini Chat
|
|
134
135
|
|
|
135
136
|
**有什么用?**
|
|
136
137
|
|
|
@@ -140,7 +141,7 @@ aicodeswitch内部,会根据“源类型”来转换数据。例如,你的
|
|
|
140
141
|
|
|
141
142
|
### 什么是路由?
|
|
142
143
|
|
|
143
|
-
路由是aicodeswitch
|
|
144
|
+
路由是aicodeswitch的核心功能,它负责将不同的对象工具(目前指Claude Code和Codex)的请求,路由到不同的供应商API服务上。
|
|
144
145
|
|
|
145
146
|
### 什么是“客户端工具”?
|
|
146
147
|
|
|
@@ -189,16 +190,16 @@ aicodeswitch内部,会根据“源类型”来转换数据。例如,你的
|
|
|
189
190
|
|
|
190
191
|
**请求日志**:所有 API 请求的详细记录
|
|
191
192
|
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
193
|
+
* 请求来源和目标
|
|
194
|
+
* 请求内容和响应
|
|
195
|
+
* 耗时和状态码
|
|
196
|
+
* 错误信息(如有)
|
|
196
197
|
|
|
197
198
|
**错误日志**:错误和异常记录
|
|
198
199
|
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
200
|
+
* 错误类型
|
|
201
|
+
* 错误详情
|
|
202
|
+
* 发生时间
|
|
202
203
|
|
|
203
204
|
**会话日志**:按照会话session来汇集日志
|
|
204
205
|
|
|
@@ -242,17 +243,17 @@ PORT=4567
|
|
|
242
243
|
|
|
243
244
|
## 我的开源
|
|
244
245
|
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
246
|
+
* [PCM](https://github.com/tangshuang/pcm): 用户意图识别、精准上下文、多线对话的Agent系统
|
|
247
|
+
* [Lan Transfer](https://github.com/tangshuang/lan-transfer): 免费高效的局域网文件互传工具
|
|
248
|
+
* [MCP Bone](https://github.com/tangshuang/mcp-bone): 远程托管的MCP服管理工具
|
|
249
|
+
* [Anys](https://github.com/tangshuang/anys): 免费前端监控kit
|
|
250
|
+
* [WebCut](https://github.com/tangshuang/webcut): 免费开源的网页端视频剪辑UI框架
|
|
251
|
+
* [indb](https://github.com/tangshuang/indb): 网页端轻量kv数据库操作库
|
|
252
|
+
* [Formast](https://github.com/tangshuang/formast): 复杂业务场景下的企业级JSON驱动表单框架
|
|
252
253
|
|
|
253
254
|
## 关联资源
|
|
254
255
|
|
|
255
|
-
*
|
|
256
|
+
* [Claude Code 深度教程](https://claudecode.tangshuang.net): 100%免费的Claude Code入门到精通教程
|
|
256
257
|
|
|
257
258
|
## 支持我
|
|
258
259
|
|
|
@@ -264,8 +265,8 @@ PORT=4567
|
|
|
264
265
|
|
|
265
266
|
此项目采用双许可证模式:
|
|
266
267
|
|
|
267
|
-
*
|
|
268
|
-
*
|
|
268
|
+
* **开源使用**:项目默认采用 GPL 3.0 许可证,允许个人免费使用、修改和分发,但所有衍生品必须开源。
|
|
269
|
+
* **商业使用**:如果您希望商业化使用而不遵守 GPL 条款(例如闭源销售),请联系我们购买单独的商业许可证。
|
|
269
270
|
|
|
270
271
|
## 技术支持
|
|
271
272
|
|
package/UPGRADE.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
本次升级将会有破坏性影响,你需要注意:
|
|
2
2
|
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* 对于API服务,你需要进行检查,OpenAI 类型的服务,请检查 `baseUrl` 是否以 `/v1` 结尾,如果是,请移除。
|
|
4
|
+
* 对codex/claude-code的配置覆盖逻辑,不再与“是否激活路由”绑定,而是在启动服务时写入覆盖并备份,停止服务时恢复备份。
|
|
5
|
+
* 升级后,一旦你启动 AICodeSwitch,就可以统计到所有请求,让你的AI编程统计数据更准确。
|
|
6
|
+
* 由于Codex升级后,数据结构有了较大的变化,导致我们的转流可能存在问题,请为Codex使用复合Responses API标准的接口(注:部分国内厂商实现的responses接口不够完善,部分工具不支持)。
|
|
7
|
+
* 升级后,数据版本会变更,如果重启后没有看到原来的数据,需退回上一个版本,导出数据后重新升级,并导入备份的数据。
|
package/bin/restore.js
CHANGED
|
@@ -4,6 +4,9 @@ const os = require('os');
|
|
|
4
4
|
const chalk = require('chalk');
|
|
5
5
|
const boxen = require('boxen');
|
|
6
6
|
const ora = require('ora');
|
|
7
|
+
const { parseToml, stringifyToml, mergeJsonSettings, mergeTomlSettings, atomicWriteFile } = require('./utils/config-helpers');
|
|
8
|
+
const { isServerRunning, getServerInfo } = require('./utils/get-server');
|
|
9
|
+
const { findPidByPort } = require('./utils/port-utils');
|
|
7
10
|
|
|
8
11
|
// 停用所有激活的路由(直接操作数据库文件)
|
|
9
12
|
const deactivateAllRoutes = () => {
|
|
@@ -55,7 +58,7 @@ const deactivateAllRoutes = () => {
|
|
|
55
58
|
}
|
|
56
59
|
};
|
|
57
60
|
|
|
58
|
-
// 恢复 Claude Code
|
|
61
|
+
// 恢复 Claude Code 配置(使用智能合并)
|
|
59
62
|
const restoreClaudeConfig = () => {
|
|
60
63
|
const results = {
|
|
61
64
|
restored: [],
|
|
@@ -69,13 +72,33 @@ const restoreClaudeConfig = () => {
|
|
|
69
72
|
const claudeSettingsPath = path.join(claudeDir, 'settings.json');
|
|
70
73
|
const claudeSettingsBakPath = path.join(claudeDir, 'settings.json.aicodeswitch_backup');
|
|
71
74
|
|
|
72
|
-
// Restore settings.json
|
|
75
|
+
// Restore settings.json(智能合并)
|
|
73
76
|
if (fs.existsSync(claudeSettingsBakPath)) {
|
|
74
|
-
|
|
75
|
-
fs.
|
|
77
|
+
try {
|
|
78
|
+
const backupSettings = JSON.parse(fs.readFileSync(claudeSettingsBakPath, 'utf-8'));
|
|
79
|
+
let currentSettings = {};
|
|
80
|
+
if (fs.existsSync(claudeSettingsPath)) {
|
|
81
|
+
try {
|
|
82
|
+
currentSettings = JSON.parse(fs.readFileSync(claudeSettingsPath, 'utf-8'));
|
|
83
|
+
} catch (e) {
|
|
84
|
+
// 忽略解析错误
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const mergedSettings = mergeJsonSettings(
|
|
89
|
+
backupSettings,
|
|
90
|
+
currentSettings,
|
|
91
|
+
['env.ANTHROPIC_AUTH_TOKEN', 'env.ANTHROPIC_BASE_URL', 'env.API_TIMEOUT_MS',
|
|
92
|
+
'env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC', 'env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS',
|
|
93
|
+
'permissions', 'skipDangerousModePermissionPrompt']
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
atomicWriteFile(claudeSettingsPath, JSON.stringify(mergedSettings, null, 2));
|
|
97
|
+
fs.unlinkSync(claudeSettingsBakPath);
|
|
98
|
+
results.restored.push('settings.json');
|
|
99
|
+
} catch (error) {
|
|
100
|
+
results.errors.push({ file: 'settings.json', error: error.message });
|
|
76
101
|
}
|
|
77
|
-
fs.renameSync(claudeSettingsBakPath, claudeSettingsPath);
|
|
78
|
-
results.restored.push('settings.json');
|
|
79
102
|
} else {
|
|
80
103
|
results.notFound.push('settings.json.aicodeswitch_backup');
|
|
81
104
|
}
|
|
@@ -88,13 +111,31 @@ const restoreClaudeConfig = () => {
|
|
|
88
111
|
const claudeJsonPath = path.join(homeDir, '.claude.json');
|
|
89
112
|
const claudeJsonBakPath = path.join(homeDir, '.claude.json.aicodeswitch_backup');
|
|
90
113
|
|
|
91
|
-
// Restore .claude.json
|
|
114
|
+
// Restore .claude.json(智能合并)
|
|
92
115
|
if (fs.existsSync(claudeJsonBakPath)) {
|
|
93
|
-
|
|
94
|
-
fs.
|
|
116
|
+
try {
|
|
117
|
+
const backupJson = JSON.parse(fs.readFileSync(claudeJsonBakPath, 'utf-8'));
|
|
118
|
+
let currentJson = {};
|
|
119
|
+
if (fs.existsSync(claudeJsonPath)) {
|
|
120
|
+
try {
|
|
121
|
+
currentJson = JSON.parse(fs.readFileSync(claudeJsonPath, 'utf-8'));
|
|
122
|
+
} catch (e) {
|
|
123
|
+
// 忽略解析错误
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const mergedJson = mergeJsonSettings(
|
|
128
|
+
backupJson,
|
|
129
|
+
currentJson,
|
|
130
|
+
['hasCompletedOnboarding', 'mcpServers']
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
atomicWriteFile(claudeJsonPath, JSON.stringify(mergedJson, null, 2));
|
|
134
|
+
fs.unlinkSync(claudeJsonBakPath);
|
|
135
|
+
results.restored.push('.claude.json');
|
|
136
|
+
} catch (error) {
|
|
137
|
+
results.errors.push({ file: '.claude.json', error: error.message });
|
|
95
138
|
}
|
|
96
|
-
fs.renameSync(claudeJsonBakPath, claudeJsonPath);
|
|
97
|
-
results.restored.push('.claude.json');
|
|
98
139
|
} else {
|
|
99
140
|
results.notFound.push('.claude.json.aicodeswitch_backup');
|
|
100
141
|
}
|
|
@@ -105,7 +146,7 @@ const restoreClaudeConfig = () => {
|
|
|
105
146
|
return results;
|
|
106
147
|
};
|
|
107
148
|
|
|
108
|
-
// 恢复 Codex
|
|
149
|
+
// 恢复 Codex 配置(使用智能合并)
|
|
109
150
|
const restoreCodexConfig = () => {
|
|
110
151
|
const results = {
|
|
111
152
|
restored: [],
|
|
@@ -119,13 +160,32 @@ const restoreCodexConfig = () => {
|
|
|
119
160
|
const codexConfigPath = path.join(codexDir, 'config.toml');
|
|
120
161
|
const codexConfigBakPath = path.join(codexDir, 'config.toml.aicodeswitch_backup');
|
|
121
162
|
|
|
122
|
-
// Restore config.toml
|
|
163
|
+
// Restore config.toml(智能合并)
|
|
123
164
|
if (fs.existsSync(codexConfigBakPath)) {
|
|
124
|
-
|
|
125
|
-
fs.
|
|
165
|
+
try {
|
|
166
|
+
const backupConfig = parseToml(fs.readFileSync(codexConfigBakPath, 'utf-8'));
|
|
167
|
+
let currentConfig = {};
|
|
168
|
+
if (fs.existsSync(codexConfigPath)) {
|
|
169
|
+
try {
|
|
170
|
+
currentConfig = parseToml(fs.readFileSync(codexConfigPath, 'utf-8'));
|
|
171
|
+
} catch (e) {
|
|
172
|
+
// 忽略解析错误
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const mergedConfig = mergeTomlSettings(
|
|
177
|
+
backupConfig,
|
|
178
|
+
currentConfig,
|
|
179
|
+
['model_provider', 'model', 'model_reasoning_effort', 'disable_response_storage',
|
|
180
|
+
'preferred_auth_method', 'requires_openai_auth', 'enableRouteSelection', 'model_providers.aicodeswitch']
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
atomicWriteFile(codexConfigPath, stringifyToml(mergedConfig));
|
|
184
|
+
fs.unlinkSync(codexConfigBakPath);
|
|
185
|
+
results.restored.push('config.toml');
|
|
186
|
+
} catch (error) {
|
|
187
|
+
results.errors.push({ file: 'config.toml', error: error.message });
|
|
126
188
|
}
|
|
127
|
-
fs.renameSync(codexConfigBakPath, codexConfigPath);
|
|
128
|
-
results.restored.push('config.toml');
|
|
129
189
|
} else {
|
|
130
190
|
results.notFound.push('config.toml.aicodeswitch_backup');
|
|
131
191
|
}
|
|
@@ -138,13 +198,31 @@ const restoreCodexConfig = () => {
|
|
|
138
198
|
const codexAuthPath = path.join(homeDir, '.codex', 'auth.json');
|
|
139
199
|
const codexAuthBakPath = path.join(homeDir, '.codex', 'auth.json.aicodeswitch_backup');
|
|
140
200
|
|
|
141
|
-
// Restore auth.json
|
|
201
|
+
// Restore auth.json(智能合并)
|
|
142
202
|
if (fs.existsSync(codexAuthBakPath)) {
|
|
143
|
-
|
|
144
|
-
fs.
|
|
203
|
+
try {
|
|
204
|
+
const backupAuth = JSON.parse(fs.readFileSync(codexAuthBakPath, 'utf-8'));
|
|
205
|
+
let currentAuth = {};
|
|
206
|
+
if (fs.existsSync(codexAuthPath)) {
|
|
207
|
+
try {
|
|
208
|
+
currentAuth = JSON.parse(fs.readFileSync(codexAuthPath, 'utf-8'));
|
|
209
|
+
} catch (e) {
|
|
210
|
+
// 忽略解析错误
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const mergedAuth = mergeJsonSettings(
|
|
215
|
+
backupAuth,
|
|
216
|
+
currentAuth,
|
|
217
|
+
['OPENAI_API_KEY']
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
atomicWriteFile(codexAuthPath, JSON.stringify(mergedAuth, null, 2));
|
|
221
|
+
fs.unlinkSync(codexAuthBakPath);
|
|
222
|
+
results.restored.push('auth.json');
|
|
223
|
+
} catch (error) {
|
|
224
|
+
results.errors.push({ file: 'auth.json', error: error.message });
|
|
145
225
|
}
|
|
146
|
-
fs.renameSync(codexAuthBakPath, codexAuthPath);
|
|
147
|
-
results.restored.push('auth.json');
|
|
148
226
|
} else {
|
|
149
227
|
results.notFound.push('auth.json.aicodeswitch_backup');
|
|
150
228
|
}
|
|
@@ -243,6 +321,32 @@ const restore = async () => {
|
|
|
243
321
|
process.exit(1);
|
|
244
322
|
}
|
|
245
323
|
|
|
324
|
+
// 服务运行中时,禁止执行手动 restore,避免中断当前代理服务
|
|
325
|
+
const { host, port } = getServerInfo();
|
|
326
|
+
const runningPid = await findPidByPort(port);
|
|
327
|
+
const runningByPidFile = isServerRunning();
|
|
328
|
+
|
|
329
|
+
if (runningByPidFile || runningPid) {
|
|
330
|
+
const pidText = runningPid ? `${runningPid}` : 'unknown';
|
|
331
|
+
const message = chalk.yellow.bold('⚠ Restore skipped: server is running\n\n') +
|
|
332
|
+
chalk.white('Detected running server: ') +
|
|
333
|
+
chalk.cyan(`http://${host}:${port}`) +
|
|
334
|
+
chalk.white(` (PID: ${pidText})\n\n`) +
|
|
335
|
+
chalk.white('Please run ') + chalk.cyan.bold('aicos stop') +
|
|
336
|
+
chalk.white(' first.\n') +
|
|
337
|
+
chalk.white('The ') + chalk.cyan.bold('stop') +
|
|
338
|
+
chalk.white(' command will automatically restore configuration files.\n');
|
|
339
|
+
|
|
340
|
+
console.log(boxen(message, {
|
|
341
|
+
padding: 1,
|
|
342
|
+
margin: 1,
|
|
343
|
+
borderStyle: 'round',
|
|
344
|
+
borderColor: 'yellow'
|
|
345
|
+
}));
|
|
346
|
+
console.log('');
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
246
350
|
// 恢复配置
|
|
247
351
|
if (target === 'claude-code' || !target) {
|
|
248
352
|
const spinner = ora({
|
package/bin/start.js
CHANGED
|
@@ -27,39 +27,31 @@ const start = async (options = {}) => {
|
|
|
27
27
|
const { host, port } = getServerInfo();
|
|
28
28
|
if (isServerRunning() || await findPidByPort(port)) {
|
|
29
29
|
if (!silent) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (callback) {
|
|
46
|
-
callback();
|
|
47
|
-
}
|
|
30
|
+
console.log(boxen(
|
|
31
|
+
chalk.yellow.bold('⚠ Server is already running!\n\n') +
|
|
32
|
+
chalk.white('URL: ') + chalk.cyan.bold(`http://${host}:${port}\n\n`) +
|
|
33
|
+
chalk.white('Use ') + chalk.cyan('aicos restart') + chalk.white(' to restart the server.\n'),
|
|
34
|
+
{
|
|
35
|
+
padding: 1,
|
|
36
|
+
margin: 1,
|
|
37
|
+
borderStyle: 'round',
|
|
38
|
+
borderColor: 'yellow'
|
|
39
|
+
}
|
|
40
|
+
));
|
|
41
|
+
console.log('');
|
|
42
|
+
}
|
|
48
43
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
44
|
+
if (callback) {
|
|
45
|
+
callback();
|
|
46
|
+
}
|
|
52
47
|
|
|
53
|
-
|
|
48
|
+
if (!noExit) {
|
|
49
|
+
process.exit(0);
|
|
54
50
|
}
|
|
55
|
-
|
|
56
|
-
if (!noExit) process.exit(0);
|
|
51
|
+
|
|
57
52
|
return true;
|
|
58
53
|
}
|
|
59
54
|
|
|
60
|
-
|
|
61
|
-
// 启动服务器
|
|
62
|
-
|
|
63
55
|
const spinner = ora({
|
|
64
56
|
text: chalk.cyan('Starting AI Code Switch server...'),
|
|
65
57
|
color: 'cyan',
|
|
@@ -79,12 +71,11 @@ const start = async (options = {}) => {
|
|
|
79
71
|
}
|
|
80
72
|
|
|
81
73
|
// 启动服务器进程 - 完全分离
|
|
82
|
-
// 打开日志文件用于输出
|
|
83
74
|
const logFd = fs.openSync(LOG_FILE, 'a');
|
|
84
75
|
|
|
85
76
|
const serverProcess = spawn('node', [serverPath], {
|
|
86
77
|
detached: true,
|
|
87
|
-
stdio: ['ignore', logFd, logFd]
|
|
78
|
+
stdio: ['ignore', logFd, logFd]
|
|
88
79
|
});
|
|
89
80
|
|
|
90
81
|
// 关闭文件描述符(子进程会保持打开)
|
|
@@ -104,13 +95,13 @@ const start = async (options = {}) => {
|
|
|
104
95
|
try {
|
|
105
96
|
const pid = parseInt(fs.readFileSync(PID_FILE, 'utf-8'), 10);
|
|
106
97
|
process.kill(pid, 0);
|
|
98
|
+
|
|
107
99
|
if (!silent) {
|
|
108
100
|
spinner.succeed(chalk.green('Server started successfully!'));
|
|
109
101
|
|
|
110
|
-
const { host, port } = getServerInfo();
|
|
111
|
-
const url = `http://${
|
|
102
|
+
const { host: runningHost, port: runningPort } = getServerInfo();
|
|
103
|
+
const url = `http://${runningHost}:${runningPort}`;
|
|
112
104
|
|
|
113
|
-
// 显示漂亮的启动信息
|
|
114
105
|
console.log(boxen(
|
|
115
106
|
chalk.green.bold('🚀 AI Code Switch Server\n\n') +
|
|
116
107
|
chalk.white('Status: ') + chalk.green.bold('● Running\n') +
|
|
@@ -133,9 +124,7 @@ const start = async (options = {}) => {
|
|
|
133
124
|
console.log('\n');
|
|
134
125
|
}
|
|
135
126
|
|
|
136
|
-
// (callback)
|
|
137
127
|
if (callback) callback();
|
|
138
|
-
// 立即退出,返回控制台
|
|
139
128
|
if (!noExit) process.exit(0);
|
|
140
129
|
return true;
|
|
141
130
|
} catch (err) {
|
|
@@ -144,13 +133,12 @@ const start = async (options = {}) => {
|
|
|
144
133
|
if (!noExit) process.exit(1);
|
|
145
134
|
return false;
|
|
146
135
|
}
|
|
147
|
-
} else {
|
|
148
|
-
spinner.fail(chalk.red('Failed to start server!'));
|
|
149
|
-
console.log(chalk.yellow(`\nCheck logs: ${chalk.cyan(LOG_FILE)}\n`));
|
|
150
|
-
if (!noExit) process.exit(1);
|
|
151
|
-
return false;
|
|
152
136
|
}
|
|
137
|
+
|
|
138
|
+
spinner.fail(chalk.red('Failed to start server!'));
|
|
139
|
+
console.log(chalk.yellow(`\nCheck logs: ${chalk.cyan(LOG_FILE)}\n`));
|
|
140
|
+
if (!noExit) process.exit(1);
|
|
141
|
+
return false;
|
|
153
142
|
};
|
|
154
143
|
|
|
155
|
-
|
|
156
|
-
module.exports = start;
|
|
144
|
+
module.exports = start;
|
package/bin/stop.js
CHANGED
|
@@ -25,7 +25,7 @@ const stop = async (options = {}) => {
|
|
|
25
25
|
console.log('\n' + chalk.gray(`Process found: ${chalk.white(pid)} (${chalk.gray(processInfo)})`));
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
//
|
|
28
|
+
// 尝试终止进程(服务端会在 SIGTERM/SIGINT 时执行配置恢复)
|
|
29
29
|
process.kill(pid, 'SIGTERM');
|
|
30
30
|
|
|
31
31
|
// 等待进程停止
|
|
@@ -58,7 +58,7 @@ const stop = async (options = {}) => {
|
|
|
58
58
|
catch (err) {
|
|
59
59
|
// 进程不存在
|
|
60
60
|
if (err.code === 'ESRCH') {
|
|
61
|
-
|
|
61
|
+
spinner.warn(chalk.yellow(`PID ${pid} not found!`));
|
|
62
62
|
}
|
|
63
63
|
else {
|
|
64
64
|
spinner.fail(chalk.red(`\nError: ${err.message}\n`));
|
|
@@ -115,4 +115,4 @@ const showStoppedMessage = () => {
|
|
|
115
115
|
console.log(chalk.white('Use ') + chalk.cyan('aicos start') + chalk.white(' to start the server again.\n'));
|
|
116
116
|
};
|
|
117
117
|
|
|
118
|
-
module.exports = stop;
|
|
118
|
+
module.exports = stop;
|