aicodeswitch 1.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 +176 -0
- package/bin/cli.js +29 -0
- package/bin/restart.js +232 -0
- package/bin/start.js +166 -0
- package/bin/stop.js +90 -0
- package/dist/server/auth.js +77 -0
- package/dist/server/database.js +480 -0
- package/dist/server/main.js +382 -0
- package/dist/server/proxy-server.js +889 -0
- package/dist/server/transformers/chunk-collector.js +39 -0
- package/dist/server/transformers/claude-openai.js +231 -0
- package/dist/server/transformers/openai-responses.js +392 -0
- package/dist/server/transformers/streaming.js +888 -0
- package/dist/types/index.js +2 -0
- package/dist/ui/assets/index-BN77E7-U.js +259 -0
- package/dist/ui/assets/index-CaNSVfpD.css +1 -0
- package/dist/ui/index.html +13 -0
- package/package.json +59 -0
package/README.md
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# AI Code Switch
|
|
2
|
+
|
|
3
|
+
## 简介
|
|
4
|
+
|
|
5
|
+
AI Code Switch 是帮助你在本地管理 AI 编程工具接入大模型的工具。
|
|
6
|
+
它可以让你的 Claude Code、Codex 等工具不再局限于官方模型。
|
|
7
|
+
|
|
8
|
+
**而且它尽可能简单的帮你解决这件事。**
|
|
9
|
+
|
|
10
|
+
## 安装
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
npm install -g aicodeswitch
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## 使用方法
|
|
17
|
+
|
|
18
|
+
**启动服务**
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
aicos start
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**进入管理界面**
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
http://127.0.0.1:4567
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**配置供应商**
|
|
31
|
+
|
|
32
|
+
- 什么是供应商?
|
|
33
|
+
- 供应商配置有什么用?
|
|
34
|
+
|
|
35
|
+
具体请看下方文档。
|
|
36
|
+
|
|
37
|
+
**路由配置**
|
|
38
|
+
|
|
39
|
+
- 什么是路由?
|
|
40
|
+
- 什么是路由规则?
|
|
41
|
+
|
|
42
|
+
具体请看下方文档。
|
|
43
|
+
|
|
44
|
+
**覆盖配置文件**
|
|
45
|
+
|
|
46
|
+
在aicodeswitch中,点击“写入Claude Code配置”按钮,它会修改Claude Code的配置文件,让Claude Code开始使用aicocdeswitch提供的模型API,而非直接连到官网的模型API。
|
|
47
|
+
|
|
48
|
+
你不用太担心,你可以在写入后,点击“恢复Claude Code配置”按钮,将Claude Code的配置文件恢复到原始状态。
|
|
49
|
+
|
|
50
|
+
Codex的配置覆盖逻辑一模一样。
|
|
51
|
+
|
|
52
|
+
**设置**
|
|
53
|
+
|
|
54
|
+
你可以在设置页面,对aicodedeswitch进行配置。
|
|
55
|
+
|
|
56
|
+
也可以导出配置数据,转移到其他电脑上导入。
|
|
57
|
+
|
|
58
|
+
## 配置供应商
|
|
59
|
+
|
|
60
|
+
### 什么是供应商?
|
|
61
|
+
|
|
62
|
+
所谓供应商,就是提供AI服务的上游服务商。可以是OpenAI、Claude、DeepSeek、GLM 官方服务,也可以是其他中转服务商。
|
|
63
|
+
|
|
64
|
+
### 供应商配置有什么用?
|
|
65
|
+
|
|
66
|
+
通过将你所有的AI服务商统一起来管理,可以帮你:
|
|
67
|
+
|
|
68
|
+
1. 避免频繁修改配置文件,通过aicodeswitch,可以一键切换到不同的供应商的AI服务API
|
|
69
|
+
2. 通过aicodeswitch,将不同供应商的接口数据,转换为工具可以正确使用的接口数据格式,也就是说,你可以将Claude Code接入遵循openai的接口数据协议的其他接口
|
|
70
|
+
3. 避免你忘记曾经注册过那些供应商
|
|
71
|
+
4. 充分榨干不怎么用的供应商的服务,避免充值后不怎么用浪费了
|
|
72
|
+
|
|
73
|
+
### 什么事API服务的“源类型”
|
|
74
|
+
|
|
75
|
+
供应商接口返回的数据格式标准类型,目前支持以下几种:
|
|
76
|
+
- OpenAI Chat
|
|
77
|
+
- OpenAI Code
|
|
78
|
+
- OpenAI Responses
|
|
79
|
+
- Claude Chat
|
|
80
|
+
- Claude Code
|
|
81
|
+
- DeepSeek Chat
|
|
82
|
+
|
|
83
|
+
**有什么用?**
|
|
84
|
+
|
|
85
|
+
aicodeswitch内部,会根据“源类型”来转换数据。例如,你的供应商API服务接口是OpenAI Chat的数据格式,而你在路由中配置的“路由对象“是Claude Code,那么就意味着,这个供应商API的数据,需要经过转换之后才能被Claude Code正确使用。
|
|
86
|
+
|
|
87
|
+
## 路由管理
|
|
88
|
+
|
|
89
|
+
### 什么是路由?
|
|
90
|
+
|
|
91
|
+
路由是aicodeswitch的核心功能,它负责将不同的对象(目前指Claude Code和Codex)的请求,路由到不同的供应商API服务上。
|
|
92
|
+
|
|
93
|
+
### 什么是“路由对象”?
|
|
94
|
+
|
|
95
|
+
目前指Claude Code或Codex。
|
|
96
|
+
|
|
97
|
+
### 什么是“路由规则”?
|
|
98
|
+
|
|
99
|
+
以Claude Code为例,它的请求实际上并非铁板一块,可以被分为多种。比如它的深度思考、长文对话、图片理解等等,都是可以独立对待的。
|
|
100
|
+
|
|
101
|
+
路由规则的目的,就是让你的工具发出的请求,可以根据这个区分,发送给不同的服务商来处理。例如,你默认使用glm-4.7作为写代码的模型,但是,你可以把图片识别的请求发给doubao-code来进行,因为doubao-code的图片识别平均价格可以更低。同样的道理,不同目标的请求可以通过不同的规则来处理,以提升编程的质量和效果。
|
|
102
|
+
|
|
103
|
+
目前,我仅提供了几个比较容易区分的规则,以后,还会添加更多的规则。
|
|
104
|
+
|
|
105
|
+
### 激活路由
|
|
106
|
+
|
|
107
|
+
我们可以为Claude Code添加多个路由,但是,我们必须激活一个路由,才能开始使用。
|
|
108
|
+
而且,所有以Claude Code为对象的路由,在同一时间,只有一个可以被激活。
|
|
109
|
+
|
|
110
|
+
### 切换路由
|
|
111
|
+
|
|
112
|
+
你可以根据你的实际情况来实时切换路由,比如,你可以在发现自己的某个服务商处的余额较少时,立即切换到另外一个服务商。
|
|
113
|
+
|
|
114
|
+
另外,我还在考虑增加一些自动化切换到逻辑,比如,当上游服务商接口报错时,立即切换到另外一个服务商。
|
|
115
|
+
|
|
116
|
+
### 3. 请求日志
|
|
117
|
+
|
|
118
|
+
在**请求日志**页面,您可以查看:
|
|
119
|
+
|
|
120
|
+
- **请求日志**:所有 API 请求的详细记录
|
|
121
|
+
- 请求来源和目标
|
|
122
|
+
- 请求内容和响应
|
|
123
|
+
- 耗时和状态码
|
|
124
|
+
- 错误信息(如有)
|
|
125
|
+
|
|
126
|
+
- **访问日志**:系统访问记录
|
|
127
|
+
- 访问时间
|
|
128
|
+
- 请求路径
|
|
129
|
+
- HTTP 方法
|
|
130
|
+
|
|
131
|
+
- **错误日志**:错误和异常记录
|
|
132
|
+
- 错误类型
|
|
133
|
+
- 错误详情
|
|
134
|
+
- 发生时间
|
|
135
|
+
|
|
136
|
+
**日志筛选**
|
|
137
|
+
|
|
138
|
+
根据提供的选项进行筛选。
|
|
139
|
+
|
|
140
|
+
## 配置文件
|
|
141
|
+
|
|
142
|
+
作为 CLI 工具,你可以在 ~/.aicodeswitch/ 目录下找到工具的相关文件。里面有一个 aicodeswitch.conf 文件,可以进行配置。
|
|
143
|
+
目前仅支持以下配置:
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
# aicodeswitch的服务IP
|
|
147
|
+
HOST=127.0.0.1
|
|
148
|
+
# aicodeswitch的服务端口
|
|
149
|
+
PORT=4567
|
|
150
|
+
|
|
151
|
+
# 如果提供AUTH,你无法直接登录用户界面,必须输入AUTH的值才能进入,相当于是一个登陆鉴权
|
|
152
|
+
# 如果你在自己的服务器上使用,通过远程接入接口时,就必须提供这个值
|
|
153
|
+
# AUTH=
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## 常见问题
|
|
157
|
+
|
|
158
|
+
### 1. 如何切换供应商?
|
|
159
|
+
|
|
160
|
+
在路由管理页面修改规则的目标供应商,或调整优先级即可。
|
|
161
|
+
|
|
162
|
+
### 2. 如何查看失败的请求?
|
|
163
|
+
|
|
164
|
+
在请求日志页面,筛选状态码不为 200 的记录。
|
|
165
|
+
|
|
166
|
+
### 3. 如何备份配置?
|
|
167
|
+
|
|
168
|
+
在系统设置页面使用**导出配置**功能,然后将提供的数据保存到本地文件中。
|
|
169
|
+
|
|
170
|
+
### 4. 如何设置日志保留时间?
|
|
171
|
+
|
|
172
|
+
在系统设置页面修改**日志保留天数**配置。
|
|
173
|
+
|
|
174
|
+
## 技术支持
|
|
175
|
+
|
|
176
|
+
如有问题或建议,请访问项目 [GitHub 仓库](https://github.com/tangshuang/aicodeswitch/issues)提交 Issue。
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const args = process.argv.slice(2);
|
|
4
|
+
const command = args[0];
|
|
5
|
+
|
|
6
|
+
const commands = {
|
|
7
|
+
start: () => require('./start'),
|
|
8
|
+
stop: () => require('./stop'),
|
|
9
|
+
restart: () => require('./restart'),
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
if (!command || !commands[command]) {
|
|
13
|
+
console.log(`
|
|
14
|
+
Usage: aicos <command>
|
|
15
|
+
|
|
16
|
+
Commands:
|
|
17
|
+
start Start the AI Code Switch server
|
|
18
|
+
stop Stop the AI Code Switch server
|
|
19
|
+
restart Restart the AI Code Switch server
|
|
20
|
+
|
|
21
|
+
Example:
|
|
22
|
+
aicos start
|
|
23
|
+
aicos stop
|
|
24
|
+
aicos restart
|
|
25
|
+
`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
commands[command]();
|
package/bin/restart.js
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
const { spawn } = require('child_process');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const chalk = require('chalk');
|
|
6
|
+
const boxen = require('boxen');
|
|
7
|
+
const ora = require('ora');
|
|
8
|
+
|
|
9
|
+
const PID_FILE = path.join(os.homedir(), '.aicodeswitch', 'server.pid');
|
|
10
|
+
const LOG_FILE = path.join(os.homedir(), '.aicodeswitch', 'server.log');
|
|
11
|
+
|
|
12
|
+
// 确保目录存在
|
|
13
|
+
const ensureDir = (filePath) => {
|
|
14
|
+
const dir = path.dirname(filePath);
|
|
15
|
+
if (!fs.existsSync(dir)) {
|
|
16
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const isServerRunning = () => {
|
|
21
|
+
if (!fs.existsSync(PID_FILE)) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const pid = parseInt(fs.readFileSync(PID_FILE, 'utf-8'), 10);
|
|
27
|
+
// 检查进程是否存在
|
|
28
|
+
process.kill(pid, 0);
|
|
29
|
+
return true;
|
|
30
|
+
} catch (err) {
|
|
31
|
+
// 进程不存在,删除过期的 PID 文件
|
|
32
|
+
fs.unlinkSync(PID_FILE);
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const getServerInfo = () => {
|
|
38
|
+
// 尝试多个可能的配置文件位置
|
|
39
|
+
const possiblePaths = [
|
|
40
|
+
path.join(os.homedir(), '.aicodeswitch', '.env'),
|
|
41
|
+
path.join(os.homedir(), '.aicodeswitch', 'aicodeswitch.conf')
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
let host = '127.0.0.1';
|
|
45
|
+
let port = 4567;
|
|
46
|
+
|
|
47
|
+
for (const dotenvPath of possiblePaths) {
|
|
48
|
+
if (fs.existsSync(dotenvPath)) {
|
|
49
|
+
const content = fs.readFileSync(dotenvPath, 'utf-8');
|
|
50
|
+
const hostMatch = content.match(/HOST=(.+)/);
|
|
51
|
+
const portMatch = content.match(/PORT=(.+)/);
|
|
52
|
+
|
|
53
|
+
if (hostMatch) host = hostMatch[1].trim();
|
|
54
|
+
if (portMatch) port = parseInt(portMatch[1].trim(), 10);
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return { host, port };
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const stopServer = async () => {
|
|
63
|
+
if (!fs.existsSync(PID_FILE)) {
|
|
64
|
+
return true; // 服务未运行,视为停止成功
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const spinner = ora({
|
|
68
|
+
text: chalk.cyan('Stopping server...'),
|
|
69
|
+
color: 'cyan'
|
|
70
|
+
}).start();
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const pid = parseInt(fs.readFileSync(PID_FILE, 'utf-8'), 10);
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
process.kill(pid, 'SIGTERM');
|
|
77
|
+
|
|
78
|
+
// 等待进程停止
|
|
79
|
+
let attempts = 0;
|
|
80
|
+
const maxAttempts = 10;
|
|
81
|
+
|
|
82
|
+
await new Promise((resolve) => {
|
|
83
|
+
const checkStopped = setInterval(() => {
|
|
84
|
+
attempts++;
|
|
85
|
+
try {
|
|
86
|
+
process.kill(pid, 0);
|
|
87
|
+
if (attempts >= maxAttempts) {
|
|
88
|
+
clearInterval(checkStopped);
|
|
89
|
+
// 强制终止
|
|
90
|
+
try {
|
|
91
|
+
process.kill(pid, 'SIGKILL');
|
|
92
|
+
} catch (e) {
|
|
93
|
+
// 进程可能已经停止
|
|
94
|
+
}
|
|
95
|
+
resolve();
|
|
96
|
+
}
|
|
97
|
+
} catch (err) {
|
|
98
|
+
// 进程已停止
|
|
99
|
+
clearInterval(checkStopped);
|
|
100
|
+
resolve();
|
|
101
|
+
}
|
|
102
|
+
}, 200);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
spinner.succeed(chalk.green('Server stopped'));
|
|
106
|
+
fs.unlinkSync(PID_FILE);
|
|
107
|
+
return true;
|
|
108
|
+
} catch (err) {
|
|
109
|
+
// 进程不存在
|
|
110
|
+
spinner.succeed(chalk.green('Server stopped'));
|
|
111
|
+
fs.unlinkSync(PID_FILE);
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
} catch (err) {
|
|
115
|
+
spinner.fail(chalk.red('Failed to stop server'));
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const startServer = async () => {
|
|
121
|
+
const spinner = ora({
|
|
122
|
+
text: chalk.cyan('Starting server...'),
|
|
123
|
+
color: 'cyan'
|
|
124
|
+
}).start();
|
|
125
|
+
|
|
126
|
+
ensureDir(PID_FILE);
|
|
127
|
+
ensureDir(LOG_FILE);
|
|
128
|
+
|
|
129
|
+
// 找到 main.js 的路径
|
|
130
|
+
const serverPath = path.join(__dirname, '..', 'dist', 'server', 'main.js');
|
|
131
|
+
|
|
132
|
+
if (!fs.existsSync(serverPath)) {
|
|
133
|
+
spinner.fail(chalk.red('Server file not found!'));
|
|
134
|
+
console.log(chalk.yellow(`\nPlease run ${chalk.cyan('npm run build')} first.\n`));
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 启动服务器进程 - 完全分离
|
|
139
|
+
// 打开日志文件用于输出
|
|
140
|
+
const logFd = fs.openSync(LOG_FILE, 'a');
|
|
141
|
+
|
|
142
|
+
const serverProcess = spawn('node', [serverPath], {
|
|
143
|
+
detached: true,
|
|
144
|
+
stdio: ['ignore', logFd, logFd] // 使用文件描述符
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// 关闭文件描述符(子进程会保持打开)
|
|
148
|
+
fs.closeSync(logFd);
|
|
149
|
+
|
|
150
|
+
// 保存 PID
|
|
151
|
+
fs.writeFileSync(PID_FILE, serverProcess.pid.toString());
|
|
152
|
+
|
|
153
|
+
// 分离进程,让父进程可以退出
|
|
154
|
+
serverProcess.unref();
|
|
155
|
+
|
|
156
|
+
// 等待服务器启动
|
|
157
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
158
|
+
|
|
159
|
+
// 检查服务器是否成功启动
|
|
160
|
+
if (fs.existsSync(PID_FILE)) {
|
|
161
|
+
try {
|
|
162
|
+
const pid = parseInt(fs.readFileSync(PID_FILE, 'utf-8'), 10);
|
|
163
|
+
process.kill(pid, 0);
|
|
164
|
+
spinner.succeed(chalk.green('Server started'));
|
|
165
|
+
return true;
|
|
166
|
+
} catch (err) {
|
|
167
|
+
spinner.fail(chalk.red('Failed to start server'));
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
spinner.fail(chalk.red('Failed to start server'));
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const restart = async () => {
|
|
177
|
+
console.log('\n');
|
|
178
|
+
|
|
179
|
+
const wasRunning = isServerRunning();
|
|
180
|
+
|
|
181
|
+
if (wasRunning) {
|
|
182
|
+
console.log(chalk.cyan('🔄 Restarting AI Code Switch server...\n'));
|
|
183
|
+
|
|
184
|
+
// 停止服务器
|
|
185
|
+
const stopped = await stopServer();
|
|
186
|
+
if (!stopped) {
|
|
187
|
+
console.log(chalk.red('\nFailed to stop server. Restart aborted.\n'));
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// 等待一下确保端口释放
|
|
192
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
193
|
+
} else {
|
|
194
|
+
console.log(chalk.cyan('Starting AI Code Switch server...\n'));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 启动服务器
|
|
198
|
+
const started = await startServer();
|
|
199
|
+
|
|
200
|
+
if (!started) {
|
|
201
|
+
console.log(chalk.yellow(`\nCheck logs: ${chalk.cyan(LOG_FILE)}\n`));
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const { host, port } = getServerInfo();
|
|
206
|
+
const url = `http://${host}:${port}`;
|
|
207
|
+
|
|
208
|
+
// 显示漂亮的启动信息
|
|
209
|
+
console.log(boxen(
|
|
210
|
+
chalk.green.bold('🚀 AI Code Switch Server\n\n') +
|
|
211
|
+
chalk.white('Status: ') + chalk.green.bold('● Running\n') +
|
|
212
|
+
chalk.white('URL: ') + chalk.cyan.bold(url) + '\n' +
|
|
213
|
+
chalk.white('Logs: ') + chalk.gray(LOG_FILE) + '\n\n' +
|
|
214
|
+
chalk.gray('Server has been ' + (wasRunning ? 'restarted' : 'started') + ' successfully'),
|
|
215
|
+
{
|
|
216
|
+
padding: 1,
|
|
217
|
+
margin: 1,
|
|
218
|
+
borderStyle: 'double',
|
|
219
|
+
borderColor: 'green'
|
|
220
|
+
}
|
|
221
|
+
));
|
|
222
|
+
|
|
223
|
+
console.log(chalk.cyan('💡 Tips:\n'));
|
|
224
|
+
console.log(chalk.white(' • Open browser: ') + chalk.cyan(url));
|
|
225
|
+
console.log(chalk.white(' • View logs: ') + chalk.gray(`tail -f ${LOG_FILE}`));
|
|
226
|
+
console.log(chalk.white(' • Stop server: ') + chalk.yellow('aicos stop'));
|
|
227
|
+
console.log('\n');
|
|
228
|
+
|
|
229
|
+
process.exit(0);
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
module.exports = restart();
|
package/bin/start.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
const { spawn } = require('child_process');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const chalk = require('chalk');
|
|
6
|
+
const boxen = require('boxen');
|
|
7
|
+
const ora = require('ora');
|
|
8
|
+
|
|
9
|
+
const PID_FILE = path.join(os.homedir(), '.aicodeswitch', 'server.pid');
|
|
10
|
+
const LOG_FILE = path.join(os.homedir(), '.aicodeswitch', 'server.log');
|
|
11
|
+
|
|
12
|
+
// 确保目录存在
|
|
13
|
+
const ensureDir = (filePath) => {
|
|
14
|
+
const dir = path.dirname(filePath);
|
|
15
|
+
if (!fs.existsSync(dir)) {
|
|
16
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const isServerRunning = () => {
|
|
21
|
+
if (!fs.existsSync(PID_FILE)) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const pid = parseInt(fs.readFileSync(PID_FILE, 'utf-8'), 10);
|
|
27
|
+
// 检查进程是否存在
|
|
28
|
+
process.kill(pid, 0);
|
|
29
|
+
return true;
|
|
30
|
+
} catch (err) {
|
|
31
|
+
// 进程不存在,删除过期的 PID 文件
|
|
32
|
+
fs.unlinkSync(PID_FILE);
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const getServerInfo = () => {
|
|
38
|
+
// 尝试多个可能的配置文件位置
|
|
39
|
+
const possiblePaths = [
|
|
40
|
+
path.join(os.homedir(), '.aicodeswitch', '.env'),
|
|
41
|
+
path.join(os.homedir(), '.aicodeswitch', 'aicodeswitch.conf')
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
let host = '127.0.0.1';
|
|
45
|
+
let port = 4567;
|
|
46
|
+
|
|
47
|
+
for (const dotenvPath of possiblePaths) {
|
|
48
|
+
if (fs.existsSync(dotenvPath)) {
|
|
49
|
+
const content = fs.readFileSync(dotenvPath, 'utf-8');
|
|
50
|
+
const hostMatch = content.match(/HOST=(.+)/);
|
|
51
|
+
const portMatch = content.match(/PORT=(.+)/);
|
|
52
|
+
|
|
53
|
+
if (hostMatch) host = hostMatch[1].trim();
|
|
54
|
+
if (portMatch) port = parseInt(portMatch[1].trim(), 10);
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return { host, port };
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const start = async () => {
|
|
63
|
+
console.log('\n');
|
|
64
|
+
|
|
65
|
+
// 检查是否已经运行
|
|
66
|
+
if (isServerRunning()) {
|
|
67
|
+
const { host, port } = getServerInfo();
|
|
68
|
+
console.log(boxen(
|
|
69
|
+
chalk.yellow.bold('⚠ Server is already running!\n\n') +
|
|
70
|
+
chalk.white(`URL: `) + chalk.cyan.bold(`http://${host}:${port}`),
|
|
71
|
+
{
|
|
72
|
+
padding: 1,
|
|
73
|
+
margin: 1,
|
|
74
|
+
borderStyle: 'round',
|
|
75
|
+
borderColor: 'yellow'
|
|
76
|
+
}
|
|
77
|
+
));
|
|
78
|
+
console.log('');
|
|
79
|
+
process.exit(0);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const spinner = ora({
|
|
83
|
+
text: chalk.cyan('Starting AI Code Switch server...'),
|
|
84
|
+
color: 'cyan'
|
|
85
|
+
}).start();
|
|
86
|
+
|
|
87
|
+
ensureDir(PID_FILE);
|
|
88
|
+
ensureDir(LOG_FILE);
|
|
89
|
+
|
|
90
|
+
// 找到 main.js 的路径
|
|
91
|
+
const serverPath = path.join(__dirname, '..', 'dist', 'server', 'main.js');
|
|
92
|
+
|
|
93
|
+
if (!fs.existsSync(serverPath)) {
|
|
94
|
+
spinner.fail(chalk.red('Server file not found!'));
|
|
95
|
+
console.log(chalk.yellow(`\nPlease run ${chalk.cyan('npm run build')} first.\n`));
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 启动服务器进程 - 完全分离
|
|
100
|
+
// 打开日志文件用于输出
|
|
101
|
+
const logFd = fs.openSync(LOG_FILE, 'a');
|
|
102
|
+
|
|
103
|
+
const serverProcess = spawn('node', [serverPath], {
|
|
104
|
+
detached: true,
|
|
105
|
+
stdio: ['ignore', logFd, logFd] // 使用文件描述符
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// 关闭文件描述符(子进程会保持打开)
|
|
109
|
+
fs.closeSync(logFd);
|
|
110
|
+
|
|
111
|
+
// 保存 PID
|
|
112
|
+
fs.writeFileSync(PID_FILE, serverProcess.pid.toString());
|
|
113
|
+
|
|
114
|
+
// 分离进程,让父进程可以退出
|
|
115
|
+
serverProcess.unref();
|
|
116
|
+
|
|
117
|
+
// 等待服务器启动
|
|
118
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
119
|
+
|
|
120
|
+
// 检查服务器是否成功启动
|
|
121
|
+
if (fs.existsSync(PID_FILE)) {
|
|
122
|
+
try {
|
|
123
|
+
const pid = parseInt(fs.readFileSync(PID_FILE, 'utf-8'), 10);
|
|
124
|
+
process.kill(pid, 0);
|
|
125
|
+
spinner.succeed(chalk.green('Server started successfully!'));
|
|
126
|
+
|
|
127
|
+
const { host, port } = getServerInfo();
|
|
128
|
+
const url = `http://${host}:${port}`;
|
|
129
|
+
|
|
130
|
+
// 显示漂亮的启动信息
|
|
131
|
+
console.log(boxen(
|
|
132
|
+
chalk.green.bold('🚀 AI Code Switch Server\n\n') +
|
|
133
|
+
chalk.white('Status: ') + chalk.green.bold('● Running\n') +
|
|
134
|
+
chalk.white('URL: ') + chalk.cyan.bold(url) + '\n' +
|
|
135
|
+
chalk.white('PID: ') + chalk.yellow(pid) + '\n' +
|
|
136
|
+
chalk.white('Logs: ') + chalk.gray(LOG_FILE) + '\n\n' +
|
|
137
|
+
chalk.gray('Open the URL in your browser to access the dashboard'),
|
|
138
|
+
{
|
|
139
|
+
padding: 1,
|
|
140
|
+
margin: 1,
|
|
141
|
+
borderStyle: 'double',
|
|
142
|
+
borderColor: 'green'
|
|
143
|
+
}
|
|
144
|
+
));
|
|
145
|
+
|
|
146
|
+
console.log(chalk.cyan('💡 Tips:\n'));
|
|
147
|
+
console.log(chalk.white(' • Open browser: ') + chalk.cyan(url));
|
|
148
|
+
console.log(chalk.white(' • View logs: ') + chalk.gray(`tail -f ${LOG_FILE}`));
|
|
149
|
+
console.log(chalk.white(' • Stop server: ') + chalk.yellow('aicos stop'));
|
|
150
|
+
console.log('\n');
|
|
151
|
+
|
|
152
|
+
// 立即退出,返回控制台
|
|
153
|
+
process.exit(0);
|
|
154
|
+
} catch (err) {
|
|
155
|
+
spinner.fail(chalk.red('Failed to start server!'));
|
|
156
|
+
console.log(chalk.yellow(`\nCheck logs: ${chalk.cyan(LOG_FILE)}\n`));
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
} else {
|
|
160
|
+
spinner.fail(chalk.red('Failed to start server!'));
|
|
161
|
+
console.log(chalk.yellow(`\nCheck logs: ${chalk.cyan(LOG_FILE)}\n`));
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
module.exports = start();
|
package/bin/stop.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
const boxen = require('boxen');
|
|
6
|
+
const ora = require('ora');
|
|
7
|
+
|
|
8
|
+
const PID_FILE = path.join(os.homedir(), '.aicodeswitch', 'server.pid');
|
|
9
|
+
|
|
10
|
+
const stop = () => {
|
|
11
|
+
console.log('\n');
|
|
12
|
+
|
|
13
|
+
if (!fs.existsSync(PID_FILE)) {
|
|
14
|
+
console.log(boxen(
|
|
15
|
+
chalk.yellow.bold('⚠ Server is not running'),
|
|
16
|
+
{
|
|
17
|
+
padding: 1,
|
|
18
|
+
margin: 1,
|
|
19
|
+
borderStyle: 'round',
|
|
20
|
+
borderColor: 'yellow'
|
|
21
|
+
}
|
|
22
|
+
));
|
|
23
|
+
console.log('\n');
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const spinner = ora({
|
|
28
|
+
text: chalk.cyan('Stopping server...'),
|
|
29
|
+
color: 'cyan'
|
|
30
|
+
}).start();
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const pid = parseInt(fs.readFileSync(PID_FILE, 'utf-8'), 10);
|
|
34
|
+
|
|
35
|
+
// 尝试终止进程
|
|
36
|
+
try {
|
|
37
|
+
process.kill(pid, 'SIGTERM');
|
|
38
|
+
|
|
39
|
+
// 等待进程停止
|
|
40
|
+
let attempts = 0;
|
|
41
|
+
const maxAttempts = 10;
|
|
42
|
+
|
|
43
|
+
const checkStopped = setInterval(() => {
|
|
44
|
+
attempts++;
|
|
45
|
+
try {
|
|
46
|
+
process.kill(pid, 0);
|
|
47
|
+
if (attempts >= maxAttempts) {
|
|
48
|
+
clearInterval(checkStopped);
|
|
49
|
+
// 强制终止
|
|
50
|
+
process.kill(pid, 'SIGKILL');
|
|
51
|
+
spinner.warn(chalk.yellow('Server forcefully stopped'));
|
|
52
|
+
fs.unlinkSync(PID_FILE);
|
|
53
|
+
showStoppedMessage();
|
|
54
|
+
}
|
|
55
|
+
} catch (err) {
|
|
56
|
+
// 进程已停止
|
|
57
|
+
clearInterval(checkStopped);
|
|
58
|
+
spinner.succeed(chalk.green('Server stopped successfully'));
|
|
59
|
+
fs.unlinkSync(PID_FILE);
|
|
60
|
+
showStoppedMessage();
|
|
61
|
+
}
|
|
62
|
+
}, 200);
|
|
63
|
+
|
|
64
|
+
} catch (err) {
|
|
65
|
+
// 进程不存在
|
|
66
|
+
spinner.warn(chalk.yellow('Process not found'));
|
|
67
|
+
fs.unlinkSync(PID_FILE);
|
|
68
|
+
showStoppedMessage();
|
|
69
|
+
}
|
|
70
|
+
} catch (err) {
|
|
71
|
+
spinner.fail(chalk.red('Failed to stop server'));
|
|
72
|
+
console.log(chalk.red(`\nError: ${err.message}\n`));
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const showStoppedMessage = () => {
|
|
77
|
+
console.log(boxen(
|
|
78
|
+
chalk.gray('AI Code Switch Server\n\n') +
|
|
79
|
+
chalk.white('Status: ') + chalk.red('● Stopped'),
|
|
80
|
+
{
|
|
81
|
+
padding: 1,
|
|
82
|
+
margin: 1,
|
|
83
|
+
borderStyle: 'round',
|
|
84
|
+
borderColor: 'gray'
|
|
85
|
+
}
|
|
86
|
+
));
|
|
87
|
+
console.log(chalk.white('Use ') + chalk.cyan('aicos start') + chalk.white(' to start the server again.\n'));
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
module.exports = stop();
|