aicodeswitch 2.0.10 → 2.0.11
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/CHANGELOG.md +2 -0
- package/CLAUDE.md +4 -1
- package/dist/server/main.js +38 -0
- package/dist/server/proxy-server.js +28 -1
- package/dist/server/rules-status-service.js +197 -0
- package/dist/server/tools-service.js +202 -0
- package/dist/server/websocket-service.js +148 -0
- package/dist/ui/assets/index-BC_wSFXP.js +452 -0
- package/dist/ui/index.html +1 -1
- package/package.json +3 -2
- package/dist/ui/assets/index-BYeFnkER.js +0 -431
package/CHANGELOG.md
CHANGED
package/CLAUDE.md
CHANGED
|
@@ -139,7 +139,7 @@ aicos version # Show current version information
|
|
|
139
139
|
- `image-understanding`: Requests with image content
|
|
140
140
|
- `thinking`: Requests with reasoning/thinking signals
|
|
141
141
|
- `long-context`: Requests with large context (≥12000 chars or ≥8000 max tokens)
|
|
142
|
-
- `background`: Background/priority requests
|
|
142
|
+
- `background`: Background/priority requests, including `/count_tokens` endpoint requests and token counting requests with `{"role": "user", "content": "count"}`
|
|
143
143
|
- `default`: All other requests
|
|
144
144
|
|
|
145
145
|
### Request Transformation
|
|
@@ -164,6 +164,9 @@ aicos version # Show current version information
|
|
|
164
164
|
- Request logs: Detailed API call records with token usage
|
|
165
165
|
- Access logs: System access records
|
|
166
166
|
- Error logs: Error and exception records (includes upstream request information when available)
|
|
167
|
+
- **Data Sanitization**:
|
|
168
|
+
- Sensitive authentication fields (api_key, authorization, password, secret, etc.) are automatically masked in the UI
|
|
169
|
+
- Technical fields like `max_tokens`, `input_tokens`, `output_tokens` are NOT masked - they are legitimate API parameters
|
|
167
170
|
- **Session Management**:
|
|
168
171
|
- Tracks user sessions based on session ID (Claude Code: `metadata.user_id`, Codex: `headers.session_id`)
|
|
169
172
|
- Auto-generates session title from first user message content:
|
package/dist/server/main.js
CHANGED
|
@@ -25,6 +25,9 @@ const os_1 = __importDefault(require("os"));
|
|
|
25
25
|
const auth_1 = require("./auth");
|
|
26
26
|
const version_check_1 = require("./version-check");
|
|
27
27
|
const utils_1 = require("./utils");
|
|
28
|
+
const tools_service_1 = require("./tools-service");
|
|
29
|
+
const websocket_service_1 = require("./websocket-service");
|
|
30
|
+
const rules_status_service_1 = require("./rules-status-service");
|
|
28
31
|
const config_metadata_1 = require("./config-metadata");
|
|
29
32
|
const config_1 = require("./config");
|
|
30
33
|
const appDir = path_1.default.join(os_1.default.homedir(), '.aicodeswitch');
|
|
@@ -1356,6 +1359,19 @@ ${instruction}
|
|
|
1356
1359
|
res.json({ success: false });
|
|
1357
1360
|
}
|
|
1358
1361
|
})));
|
|
1362
|
+
// 工具安装检测相关路由
|
|
1363
|
+
app.get('/api/tools/status', asyncHandler((_req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1364
|
+
console.log('[API] GET /api/tools/status - 获取工具安装状态');
|
|
1365
|
+
try {
|
|
1366
|
+
const status = yield (0, tools_service_1.getToolsInstallationStatus)();
|
|
1367
|
+
console.log('[API] 工具安装状态:', status);
|
|
1368
|
+
res.json(status);
|
|
1369
|
+
}
|
|
1370
|
+
catch (error) {
|
|
1371
|
+
console.error('[API] 获取工具状态失败:', error);
|
|
1372
|
+
res.status(500).json({ error: '获取工具状态失败' });
|
|
1373
|
+
}
|
|
1374
|
+
})));
|
|
1359
1375
|
};
|
|
1360
1376
|
const start = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
1361
1377
|
fs_1.default.mkdirSync(dataDir, { recursive: true });
|
|
@@ -1377,6 +1393,28 @@ const start = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
1377
1393
|
const server = app.listen(port, host, () => {
|
|
1378
1394
|
console.log(`Admin server running on http://${host}:${port}`);
|
|
1379
1395
|
});
|
|
1396
|
+
// 创建 WebSocket 服务器用于工具安装
|
|
1397
|
+
const toolInstallWss = (0, websocket_service_1.createToolInstallationWSServer)();
|
|
1398
|
+
// 创建 WebSocket 服务器用于规则状态
|
|
1399
|
+
const rulesStatusWss = (0, rules_status_service_1.createRulesStatusWSServer)();
|
|
1400
|
+
// 将 WebSocket 服务器附加到 HTTP 服务器
|
|
1401
|
+
server.on('upgrade', (request, socket, head) => {
|
|
1402
|
+
if (request.url === '/api/tools/install') {
|
|
1403
|
+
toolInstallWss.handleUpgrade(request, socket, head, (ws) => {
|
|
1404
|
+
toolInstallWss.emit('connection', ws, request);
|
|
1405
|
+
});
|
|
1406
|
+
}
|
|
1407
|
+
else if (request.url === '/api/rules/status') {
|
|
1408
|
+
rulesStatusWss.handleUpgrade(request, socket, head, (ws) => {
|
|
1409
|
+
rulesStatusWss.emit('connection', ws, request);
|
|
1410
|
+
});
|
|
1411
|
+
}
|
|
1412
|
+
else {
|
|
1413
|
+
socket.destroy();
|
|
1414
|
+
}
|
|
1415
|
+
});
|
|
1416
|
+
console.log(`WebSocket server for tool installation attached to ws://${host}:${port}/api/tools/install`);
|
|
1417
|
+
console.log(`WebSocket server for rules status attached to ws://${host}:${port}/api/rules/status`);
|
|
1380
1418
|
const shutdown = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
1381
1419
|
console.log('Shutting down server...');
|
|
1382
1420
|
dbManager.close();
|
|
@@ -51,6 +51,7 @@ const stream_1 = require("stream");
|
|
|
51
51
|
const crypto_1 = __importDefault(require("crypto"));
|
|
52
52
|
const streaming_1 = require("./transformers/streaming");
|
|
53
53
|
const chunk_collector_1 = require("./transformers/chunk-collector");
|
|
54
|
+
const rules_status_service_1 = require("./rules-status-service");
|
|
54
55
|
const claude_openai_1 = require("./transformers/claude-openai");
|
|
55
56
|
const SUPPORTED_TARGETS = ['claude-code', 'codex'];
|
|
56
57
|
class ProxyServer {
|
|
@@ -762,6 +763,10 @@ class ProxyServer {
|
|
|
762
763
|
const body = req.body;
|
|
763
764
|
if (!body)
|
|
764
765
|
return 'default';
|
|
766
|
+
// 检查是否为 count_tokens 请求(后台类型)
|
|
767
|
+
if (req.path.includes('/count_tokens')) {
|
|
768
|
+
return 'background';
|
|
769
|
+
}
|
|
765
770
|
const explicitType = this.getExplicitContentType(req, body);
|
|
766
771
|
if (explicitType) {
|
|
767
772
|
return explicitType;
|
|
@@ -890,6 +895,17 @@ class ProxyServer {
|
|
|
890
895
|
}
|
|
891
896
|
hasBackgroundSignal(body) {
|
|
892
897
|
var _a, _b, _c;
|
|
898
|
+
// 检测 count tokens 请求:messages 只有一条,role 为 "user",content 为 "count"
|
|
899
|
+
const messages = body === null || body === void 0 ? void 0 : body.messages;
|
|
900
|
+
if (Array.isArray(messages) && messages.length === 1) {
|
|
901
|
+
const firstMessage = messages[0];
|
|
902
|
+
if ((firstMessage === null || firstMessage === void 0 ? void 0 : firstMessage.role) === 'user' &&
|
|
903
|
+
((firstMessage === null || firstMessage === void 0 ? void 0 : firstMessage.content) === 'count' ||
|
|
904
|
+
(typeof (firstMessage === null || firstMessage === void 0 ? void 0 : firstMessage.content) === 'string' && firstMessage.content.trim() === 'count'))) {
|
|
905
|
+
return true;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
// 检测其他后台信号
|
|
893
909
|
const candidates = [
|
|
894
910
|
body === null || body === void 0 ? void 0 : body.background,
|
|
895
911
|
(_a = body === null || body === void 0 ? void 0 : body.metadata) === null || _a === void 0 ? void 0 : _a.background,
|
|
@@ -1359,6 +1375,8 @@ class ProxyServer {
|
|
|
1359
1375
|
let streamChunksForLog;
|
|
1360
1376
|
let upstreamRequestForLog;
|
|
1361
1377
|
let actuallyUsedProxy = false; // 标记是否实际使用了代理
|
|
1378
|
+
// 标记规则正在使用
|
|
1379
|
+
rules_status_service_1.rulesStatusBroadcaster.markRuleInUse(route.id, rule.id);
|
|
1362
1380
|
const finalizeLog = (statusCode, error) => __awaiter(this, void 0, void 0, function* () {
|
|
1363
1381
|
var _a, _b;
|
|
1364
1382
|
if (logged)
|
|
@@ -1428,6 +1446,11 @@ class ProxyServer {
|
|
|
1428
1446
|
const totalTokens = (usageForLog.inputTokens || 0) + (usageForLog.outputTokens || 0);
|
|
1429
1447
|
if (totalTokens > 0) {
|
|
1430
1448
|
this.dbManager.incrementRuleTokenUsage(rule.id, totalTokens);
|
|
1449
|
+
// 获取更新后的规则数据并广播
|
|
1450
|
+
const updatedRule = this.dbManager.getRule(rule.id);
|
|
1451
|
+
if (updatedRule) {
|
|
1452
|
+
rules_status_service_1.rulesStatusBroadcaster.broadcastUsageUpdate(rule.id, updatedRule.totalTokensUsed || 0, updatedRule.totalRequestsUsed || 0);
|
|
1453
|
+
}
|
|
1431
1454
|
}
|
|
1432
1455
|
}
|
|
1433
1456
|
// 更新规则的请求次数(只在成功请求时更新)
|
|
@@ -1437,6 +1460,11 @@ class ProxyServer {
|
|
|
1437
1460
|
// 检查是否是重复请求(如网络重试)
|
|
1438
1461
|
if (!this.isRequestProcessed(requestHash)) {
|
|
1439
1462
|
this.dbManager.incrementRuleRequestCount(rule.id, 1);
|
|
1463
|
+
// 获取更新后的规则数据并广播
|
|
1464
|
+
const updatedRule = this.dbManager.getRule(rule.id);
|
|
1465
|
+
if (updatedRule) {
|
|
1466
|
+
rules_status_service_1.rulesStatusBroadcaster.broadcastUsageUpdate(rule.id, updatedRule.totalTokensUsed || 0, updatedRule.totalRequestsUsed || 0);
|
|
1467
|
+
}
|
|
1440
1468
|
}
|
|
1441
1469
|
// 定期清理过期缓存
|
|
1442
1470
|
if (Math.random() < 0.01) { // 1%概率清理,避免每次都清理
|
|
@@ -1698,7 +1726,6 @@ class ProxyServer {
|
|
|
1698
1726
|
});
|
|
1699
1727
|
res.on('finish', () => {
|
|
1700
1728
|
streamChunksForLog = eventCollector.getChunks();
|
|
1701
|
-
console.log('[Proxy] Default stream request finished, collected chunks:', (streamChunksForLog === null || streamChunksForLog === void 0 ? void 0 : streamChunksForLog.length) || 0);
|
|
1702
1729
|
// 尝试从event collector中提取usage信息
|
|
1703
1730
|
const extractedUsage = eventCollector.extractUsage();
|
|
1704
1731
|
if (extractedUsage) {
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RulesStatusWS = exports.rulesStatusBroadcaster = exports.RulesStatusBroadcaster = void 0;
|
|
4
|
+
exports.createRulesStatusWSServer = createRulesStatusWSServer;
|
|
5
|
+
// @ts-ignore - ws 类型声明可能需要手动安装 @types/ws
|
|
6
|
+
const ws_1 = require("ws");
|
|
7
|
+
/**
|
|
8
|
+
* 规则状态 WebSocket 连接管理
|
|
9
|
+
*/
|
|
10
|
+
class RulesStatusWS {
|
|
11
|
+
constructor(ws, req) {
|
|
12
|
+
Object.defineProperty(this, "ws", {
|
|
13
|
+
enumerable: true,
|
|
14
|
+
configurable: true,
|
|
15
|
+
writable: true,
|
|
16
|
+
value: void 0
|
|
17
|
+
});
|
|
18
|
+
this.ws = ws;
|
|
19
|
+
console.log(`[RulesStatusWS] 新的 WebSocket 连接: ${req.socket.remoteAddress}`);
|
|
20
|
+
this.ws.on('close', () => {
|
|
21
|
+
console.log(`[RulesStatusWS] WebSocket 连接关闭`);
|
|
22
|
+
});
|
|
23
|
+
this.ws.on('error', (err) => {
|
|
24
|
+
console.error(`[RulesStatusWS] WebSocket 错误:`, err);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* 发送规则状态消息到客户端
|
|
29
|
+
*/
|
|
30
|
+
sendStatus(message) {
|
|
31
|
+
if (this.ws.readyState === ws_1.WebSocket.OPEN) {
|
|
32
|
+
this.ws.send(JSON.stringify(message));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
exports.RulesStatusWS = RulesStatusWS;
|
|
37
|
+
/**
|
|
38
|
+
* 规则状态广播服务
|
|
39
|
+
* 管理所有连接的客户端,负责广播规则使用状态
|
|
40
|
+
*/
|
|
41
|
+
class RulesStatusBroadcaster {
|
|
42
|
+
constructor() {
|
|
43
|
+
Object.defineProperty(this, "clients", {
|
|
44
|
+
enumerable: true,
|
|
45
|
+
configurable: true,
|
|
46
|
+
writable: true,
|
|
47
|
+
value: new Set()
|
|
48
|
+
});
|
|
49
|
+
Object.defineProperty(this, "activeRules", {
|
|
50
|
+
enumerable: true,
|
|
51
|
+
configurable: true,
|
|
52
|
+
writable: true,
|
|
53
|
+
value: new Map()
|
|
54
|
+
}); // routeId -> Set of ruleIds
|
|
55
|
+
Object.defineProperty(this, "ruleTimeouts", {
|
|
56
|
+
enumerable: true,
|
|
57
|
+
configurable: true,
|
|
58
|
+
writable: true,
|
|
59
|
+
value: new Map()
|
|
60
|
+
});
|
|
61
|
+
Object.defineProperty(this, "INACTIVITY_TIMEOUT", {
|
|
62
|
+
enumerable: true,
|
|
63
|
+
configurable: true,
|
|
64
|
+
writable: true,
|
|
65
|
+
value: 30000
|
|
66
|
+
}); // 30秒无活动后标记为空闲
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* 添加客户端
|
|
70
|
+
*/
|
|
71
|
+
addClient(client) {
|
|
72
|
+
this.clients.add(client);
|
|
73
|
+
console.log(`[RulesStatusBroadcaster] 客户端已连接,当前客户端数: ${this.clients.size}`);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* 移除客户端
|
|
77
|
+
*/
|
|
78
|
+
removeClient(client) {
|
|
79
|
+
this.clients.delete(client);
|
|
80
|
+
console.log(`[RulesStatusBroadcaster] 客户端已断开,当前客户端数: ${this.clients.size}`);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* 标记规则正在使用
|
|
84
|
+
*/
|
|
85
|
+
markRuleInUse(routeId, ruleId) {
|
|
86
|
+
// 添加到活动规则集合
|
|
87
|
+
if (!this.activeRules.has(routeId)) {
|
|
88
|
+
this.activeRules.set(routeId, new Set());
|
|
89
|
+
}
|
|
90
|
+
this.activeRules.get(routeId).add(ruleId);
|
|
91
|
+
// 清除之前的超时定时器
|
|
92
|
+
const timeoutKey = `${routeId}:${ruleId}`;
|
|
93
|
+
const existingTimeout = this.ruleTimeouts.get(timeoutKey);
|
|
94
|
+
if (existingTimeout) {
|
|
95
|
+
clearTimeout(existingTimeout);
|
|
96
|
+
}
|
|
97
|
+
// 广播状态
|
|
98
|
+
this.broadcastStatus({
|
|
99
|
+
type: 'rule_status',
|
|
100
|
+
data: {
|
|
101
|
+
ruleId,
|
|
102
|
+
status: 'in_use',
|
|
103
|
+
timestamp: Date.now(),
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
// 设置超时定时器,如果30秒内没有新活动则标记为空闲
|
|
107
|
+
const timeout = setTimeout(() => {
|
|
108
|
+
this.markRuleIdle(routeId, ruleId);
|
|
109
|
+
}, this.INACTIVITY_TIMEOUT);
|
|
110
|
+
this.ruleTimeouts.set(timeoutKey, timeout);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* 标记规则空闲
|
|
114
|
+
*/
|
|
115
|
+
markRuleIdle(routeId, ruleId) {
|
|
116
|
+
const timeoutKey = `${routeId}:${ruleId}`;
|
|
117
|
+
// 清除超时定时器
|
|
118
|
+
const existingTimeout = this.ruleTimeouts.get(timeoutKey);
|
|
119
|
+
if (existingTimeout) {
|
|
120
|
+
clearTimeout(existingTimeout);
|
|
121
|
+
this.ruleTimeouts.delete(timeoutKey);
|
|
122
|
+
}
|
|
123
|
+
// 从活动规则集合中移除
|
|
124
|
+
if (this.activeRules.has(routeId)) {
|
|
125
|
+
this.activeRules.get(routeId).delete(ruleId);
|
|
126
|
+
if (this.activeRules.get(routeId).size === 0) {
|
|
127
|
+
this.activeRules.delete(routeId);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// 广播空闲状态
|
|
131
|
+
this.broadcastStatus({
|
|
132
|
+
type: 'rule_status',
|
|
133
|
+
data: {
|
|
134
|
+
ruleId,
|
|
135
|
+
status: 'idle',
|
|
136
|
+
timestamp: Date.now(),
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* 广播规则使用量更新
|
|
142
|
+
*/
|
|
143
|
+
broadcastUsageUpdate(ruleId, totalTokensUsed, totalRequestsUsed) {
|
|
144
|
+
this.broadcastStatus({
|
|
145
|
+
type: 'rule_status',
|
|
146
|
+
data: {
|
|
147
|
+
ruleId,
|
|
148
|
+
status: 'in_use',
|
|
149
|
+
totalTokensUsed,
|
|
150
|
+
totalRequestsUsed,
|
|
151
|
+
timestamp: Date.now(),
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* 广播消息到所有客户端
|
|
157
|
+
*/
|
|
158
|
+
broadcastStatus(message) {
|
|
159
|
+
const deadClients = [];
|
|
160
|
+
this.clients.forEach((client) => {
|
|
161
|
+
try {
|
|
162
|
+
client.sendStatus(message);
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
console.error('[RulesStatusBroadcaster] 发送消息失败:', error);
|
|
166
|
+
deadClients.push(client);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
// 清理断开的客户端
|
|
170
|
+
deadClients.forEach((client) => {
|
|
171
|
+
this.removeClient(client);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* 获取当前活动的规则列表
|
|
176
|
+
*/
|
|
177
|
+
getActiveRules() {
|
|
178
|
+
return new Map(this.activeRules);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
exports.RulesStatusBroadcaster = RulesStatusBroadcaster;
|
|
182
|
+
// 全局单例
|
|
183
|
+
exports.rulesStatusBroadcaster = new RulesStatusBroadcaster();
|
|
184
|
+
/**
|
|
185
|
+
* 创建 WebSocket 服务器用于规则状态
|
|
186
|
+
*/
|
|
187
|
+
function createRulesStatusWSServer() {
|
|
188
|
+
const wss = new ws_1.WebSocketServer({ noServer: true });
|
|
189
|
+
wss.on('connection', (ws, req) => {
|
|
190
|
+
const wsHandler = new RulesStatusWS(ws, req);
|
|
191
|
+
exports.rulesStatusBroadcaster.addClient(wsHandler);
|
|
192
|
+
ws.on('close', () => {
|
|
193
|
+
exports.rulesStatusBroadcaster.removeClient(wsHandler);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
return wss;
|
|
197
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
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
|
+
});
|
|
33
|
+
let stdout = '';
|
|
34
|
+
let stderr = '';
|
|
35
|
+
(_a = child.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (data) => {
|
|
36
|
+
stdout += data.toString();
|
|
37
|
+
console.log(`[ToolsService] ${toolName} stdout:`, data.toString());
|
|
38
|
+
});
|
|
39
|
+
(_b = child.stderr) === null || _b === void 0 ? void 0 : _b.on('data', (data) => {
|
|
40
|
+
stderr += data.toString();
|
|
41
|
+
console.log(`[ToolsService] ${toolName} stderr:`, data.toString());
|
|
42
|
+
});
|
|
43
|
+
child.on('close', (code) => {
|
|
44
|
+
console.log(`[ToolsService] ${toolName} 进程退出,退出码: ${code}`);
|
|
45
|
+
if (code === 0) {
|
|
46
|
+
// 尝试从输出中提取版本号
|
|
47
|
+
const versionMatch = stdout.match(/(\d+\.\d+\.\d+)/) || stderr.match(/(\d+\.\d+\.\d+)/);
|
|
48
|
+
resolve({
|
|
49
|
+
installed: true,
|
|
50
|
+
version: versionMatch ? versionMatch[1] : 'unknown',
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
resolve({ installed: false });
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
child.on('error', (err) => {
|
|
58
|
+
console.log(`[ToolsService] ${toolName} 检测出错:`, err);
|
|
59
|
+
resolve({ installed: false });
|
|
60
|
+
});
|
|
61
|
+
// 10秒超时
|
|
62
|
+
setTimeout(() => {
|
|
63
|
+
console.log(`[ToolsService] ${toolName} 检测超时`);
|
|
64
|
+
child.kill();
|
|
65
|
+
resolve({ installed: false });
|
|
66
|
+
}, 10000);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* 获取工具的安装命令
|
|
72
|
+
*/
|
|
73
|
+
function getInstallCommand(toolName) {
|
|
74
|
+
const platform = os_1.default.platform();
|
|
75
|
+
const tool = toolName === 'claude-code' ? '@anthropic-ai/claude-code' : '@openai/codex';
|
|
76
|
+
if (platform === 'win32') {
|
|
77
|
+
return `npm install -g ${tool}`;
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
// macOS 和 Linux 需要 sudo
|
|
81
|
+
return `sudo npm install -g ${tool}`;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* 检测所有工具的安装状态
|
|
86
|
+
*/
|
|
87
|
+
function getToolsInstallationStatus() {
|
|
88
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
89
|
+
const [claudeCodeStatus, codexStatus] = yield Promise.all([
|
|
90
|
+
checkToolInstalled('claude-code'),
|
|
91
|
+
checkToolInstalled('codex'),
|
|
92
|
+
]);
|
|
93
|
+
return {
|
|
94
|
+
claudeCode: Object.assign(Object.assign({}, claudeCodeStatus), { installCommand: claudeCodeStatus.installed ? undefined : getInstallCommand('claude-code') }),
|
|
95
|
+
codex: Object.assign(Object.assign({}, codexStatus), { installCommand: codexStatus.installed ? undefined : getInstallCommand('codex') }),
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* 执行工具安装
|
|
101
|
+
*/
|
|
102
|
+
function installTool(toolName, callbacks) {
|
|
103
|
+
var _a, _b;
|
|
104
|
+
const command = getInstallCommand(toolName);
|
|
105
|
+
const platform = os_1.default.platform();
|
|
106
|
+
console.log(`[ToolsService] ========== 开始安装 ${toolName} ==========`);
|
|
107
|
+
console.log(`[ToolsService] 操作系统: ${platform}`);
|
|
108
|
+
console.log(`[ToolsService] 安装命令: ${command}`);
|
|
109
|
+
// 立即发送启动消息,让前端快速进入 terminal 界面
|
|
110
|
+
callbacks.onData(`\n========== 开始安装 ${toolName} ==========\n`);
|
|
111
|
+
callbacks.onData(`操作系统: ${platform}\n`);
|
|
112
|
+
callbacks.onData(`执行命令: ${command}\n`);
|
|
113
|
+
callbacks.onData(`进程启动中...\n\n`);
|
|
114
|
+
// 在 macOS/Linux 上,需要使用 sudo 并且可能需要输入密码
|
|
115
|
+
// 在 Windows 上直接使用 cmd 执行 npm 命令
|
|
116
|
+
let child;
|
|
117
|
+
if (platform === 'win32') {
|
|
118
|
+
// Windows: 使用 cmd.exe 执行命令
|
|
119
|
+
console.log(`[ToolsService] Windows 环境,使用 cmd.exe`);
|
|
120
|
+
child = (0, child_process_1.spawn)('cmd.exe', ['/c', command], {
|
|
121
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
122
|
+
shell: false,
|
|
123
|
+
windowsHide: true, // 隐藏命令行窗口
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
// Unix: 使用 sh 执行命令
|
|
128
|
+
console.log(`[ToolsService] Unix 环境,使用 sh`);
|
|
129
|
+
child = (0, child_process_1.spawn)('sh', ['-c', command], {
|
|
130
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
131
|
+
shell: false,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
console.log(`[ToolsService] 子进程 PID: ${child.pid}`);
|
|
135
|
+
// 立即发送进程信息
|
|
136
|
+
callbacks.onData(`子进程已创建 (PID: ${child.pid})\n`);
|
|
137
|
+
callbacks.onData(`等待 npm 输出...\n\n`);
|
|
138
|
+
// 设置超时:30分钟后自动终止
|
|
139
|
+
const timeout = 30 * 60 * 1000;
|
|
140
|
+
const timeoutHandle = setTimeout(() => {
|
|
141
|
+
console.error(`[ToolsService] ${toolName} 安装超时 (${timeout}ms),终止进程`);
|
|
142
|
+
child.kill('SIGTERM');
|
|
143
|
+
callbacks.onError(`\n安装超时 (${timeout / 60000} 分钟),请检查网络连接或手动执行命令:\n${command}\n`);
|
|
144
|
+
}, timeout);
|
|
145
|
+
let hasReceivedData = false;
|
|
146
|
+
(_a = child.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (data) => {
|
|
147
|
+
const output = data.toString();
|
|
148
|
+
if (!hasReceivedData) {
|
|
149
|
+
console.log(`[ToolsService] ${toolName} 首次收到 stdout 数据`);
|
|
150
|
+
hasReceivedData = true;
|
|
151
|
+
}
|
|
152
|
+
console.log(`[ToolsService] ${toolName} stdout:`, output.slice(0, 200)); // 只记录前200字符
|
|
153
|
+
callbacks.onData(output);
|
|
154
|
+
});
|
|
155
|
+
(_b = child.stderr) === null || _b === void 0 ? void 0 : _b.on('data', (data) => {
|
|
156
|
+
const output = data.toString();
|
|
157
|
+
if (!hasReceivedData) {
|
|
158
|
+
console.log(`[ToolsService] ${toolName} 首次收到 stderr 数据`);
|
|
159
|
+
hasReceivedData = true;
|
|
160
|
+
}
|
|
161
|
+
console.log(`[ToolsService] ${toolName} stderr:`, output.slice(0, 200)); // 只记录前200字符
|
|
162
|
+
callbacks.onError(output);
|
|
163
|
+
});
|
|
164
|
+
child.on('close', (code) => {
|
|
165
|
+
clearTimeout(timeoutHandle);
|
|
166
|
+
console.log(`[ToolsService] ${toolName} 安装进程关闭,退出码: ${code}`);
|
|
167
|
+
if (code === 0) {
|
|
168
|
+
callbacks.onData(`\n========== 安装完成 ==========\n`);
|
|
169
|
+
}
|
|
170
|
+
else if (code === null) {
|
|
171
|
+
callbacks.onError(`\n========== 安装被终止 ==========\n`);
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
callbacks.onError(`\n========== 安装失败 (退出码: ${code}) ==========\n`);
|
|
175
|
+
}
|
|
176
|
+
callbacks.onClose(code);
|
|
177
|
+
});
|
|
178
|
+
child.on('error', (err) => {
|
|
179
|
+
clearTimeout(timeoutHandle);
|
|
180
|
+
console.error(`[ToolsService] ${toolName} 安装进程错误:`, err);
|
|
181
|
+
callbacks.onError(`启动安装进程失败: ${err.message}\n`);
|
|
182
|
+
callbacks.onError(`错误详情: ${err}\n`);
|
|
183
|
+
callbacks.onClose(null);
|
|
184
|
+
});
|
|
185
|
+
// 处理进程退出信号
|
|
186
|
+
child.on('exit', (_code, signal) => {
|
|
187
|
+
clearTimeout(timeoutHandle);
|
|
188
|
+
if (signal) {
|
|
189
|
+
console.log(`[ToolsService] ${toolName} 进程被信号终止: ${signal}`);
|
|
190
|
+
callbacks.onError(`\n安装进程被信号终止: ${signal}\n`);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
// 如果10秒后还没有收到任何数据,发送提示
|
|
194
|
+
setTimeout(() => {
|
|
195
|
+
if (!hasReceivedData) {
|
|
196
|
+
console.log(`[ToolsService] ${toolName} 10秒内未收到输出,发送等待提示`);
|
|
197
|
+
callbacks.onData(`[提示] 正在连接 npm 服务器,这可能需要一些时间...\n`);
|
|
198
|
+
callbacks.onData(`[提示] 如果持续无响应,请检查网络连接或 npm 配置\n\n`);
|
|
199
|
+
}
|
|
200
|
+
}, 10000);
|
|
201
|
+
return child;
|
|
202
|
+
}
|