aicodeswitch 2.0.9 → 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 +4 -0
- package/CLAUDE.md +14 -2
- package/dist/server/database.js +38 -6
- package/dist/server/main.js +39 -0
- package/dist/server/proxy-server.js +79 -6
- package/dist/server/rules-status-service.js +197 -0
- package/dist/server/tools-service.js +202 -0
- package/dist/server/transformers/claude-openai.js +5 -2
- package/dist/server/websocket-service.js +148 -0
- package/dist/ui/assets/index-BC_wSFXP.js +452 -0
- package/dist/ui/assets/index-DQ2LJr-O.css +1 -0
- package/dist/ui/index.html +2 -2
- package/package.json +3 -2
- package/dist/ui/assets/index-BOlCGnMv.css +0 -1
- package/dist/ui/assets/index-d74w3Uye.js +0 -441
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
### 2.0.11 (2026-02-03)
|
|
6
|
+
|
|
7
|
+
### 2.0.10 (2026-02-03)
|
|
8
|
+
|
|
5
9
|
### 2.0.9 (2026-02-02)
|
|
6
10
|
|
|
7
11
|
### 2.0.8 (2026-02-02)
|
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
|
|
@@ -163,7 +163,18 @@ aicos version # Show current version information
|
|
|
163
163
|
### Logging
|
|
164
164
|
- Request logs: Detailed API call records with token usage
|
|
165
165
|
- Access logs: System access records
|
|
166
|
-
- Error logs: Error and exception records
|
|
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
|
|
170
|
+
- **Session Management**:
|
|
171
|
+
- Tracks user sessions based on session ID (Claude Code: `metadata.user_id`, Codex: `headers.session_id`)
|
|
172
|
+
- Auto-generates session title from first user message content:
|
|
173
|
+
- Extracts text from first user message
|
|
174
|
+
- Cleans up whitespace and newlines
|
|
175
|
+
- Intelligently truncates at word boundaries (max 100 chars)
|
|
176
|
+
- Adds "..." for truncated titles
|
|
177
|
+
- Records first request time, last request time, request count, and total tokens per session
|
|
167
178
|
|
|
168
179
|
### Usage Limits Auto-Sync
|
|
169
180
|
- **Service-Level Limits**: API services can have token and request count limits configured
|
|
@@ -203,3 +214,4 @@ aicos version # Show current version information
|
|
|
203
214
|
* 所有对话请使用中文。生成代码中的文案及相关注释根据代码原本的语言生成。
|
|
204
215
|
* 在服务端,直接使用 __dirname 来获取当前目录,不要使用 process.cwd()
|
|
205
216
|
* 每次有新的变化时,你需要更新 CLAUDE.md 来让文档保持最新。
|
|
217
|
+
* 禁止在项目中使用依赖GPU的css样式处理。
|
package/dist/server/database.js
CHANGED
|
@@ -264,6 +264,13 @@ class DatabaseManager {
|
|
|
264
264
|
this.db.exec('ALTER TABLE api_services ADD COLUMN auth_type TEXT DEFAULT NULL;');
|
|
265
265
|
console.log('[DB] Migration completed: auth_type column added');
|
|
266
266
|
}
|
|
267
|
+
// 检查rules表是否有is_disabled字段(临时屏蔽功能)
|
|
268
|
+
const hasIsDisabled = rulesColumns.some((col) => col.name === 'is_disabled');
|
|
269
|
+
if (!hasIsDisabled) {
|
|
270
|
+
console.log('[DB] Running migration: Adding is_disabled column to rules table');
|
|
271
|
+
this.db.exec('ALTER TABLE rules ADD COLUMN is_disabled INTEGER DEFAULT 0;');
|
|
272
|
+
console.log('[DB] Migration completed: is_disabled column added');
|
|
273
|
+
}
|
|
267
274
|
});
|
|
268
275
|
}
|
|
269
276
|
migrateMaxOutputTokensToModelLimits() {
|
|
@@ -378,10 +385,13 @@ class DatabaseManager {
|
|
|
378
385
|
total_tokens_used INTEGER DEFAULT 0,
|
|
379
386
|
reset_interval INTEGER,
|
|
380
387
|
last_reset_at INTEGER,
|
|
388
|
+
token_reset_base_time INTEGER,
|
|
381
389
|
request_count_limit INTEGER,
|
|
382
390
|
total_requests_used INTEGER DEFAULT 0,
|
|
383
391
|
request_reset_interval INTEGER,
|
|
384
392
|
request_last_reset_at INTEGER,
|
|
393
|
+
request_reset_base_time INTEGER,
|
|
394
|
+
is_disabled INTEGER DEFAULT 0,
|
|
385
395
|
created_at INTEGER NOT NULL,
|
|
386
396
|
updated_at INTEGER NOT NULL,
|
|
387
397
|
FOREIGN KEY (route_id) REFERENCES routes(id) ON DELETE CASCADE,
|
|
@@ -631,6 +641,7 @@ class DatabaseManager {
|
|
|
631
641
|
requestResetInterval: row.request_reset_interval,
|
|
632
642
|
requestLastResetAt: row.request_last_reset_at,
|
|
633
643
|
requestResetBaseTime: row.request_reset_base_time,
|
|
644
|
+
isDisabled: row.is_disabled === 1,
|
|
634
645
|
createdAt: row.created_at,
|
|
635
646
|
updatedAt: row.updated_at,
|
|
636
647
|
}));
|
|
@@ -658,6 +669,7 @@ class DatabaseManager {
|
|
|
658
669
|
requestResetInterval: row.request_reset_interval,
|
|
659
670
|
requestLastResetAt: row.request_last_reset_at,
|
|
660
671
|
requestResetBaseTime: row.request_reset_base_time,
|
|
672
|
+
isDisabled: row.is_disabled === 1,
|
|
661
673
|
createdAt: row.created_at,
|
|
662
674
|
updatedAt: row.updated_at,
|
|
663
675
|
};
|
|
@@ -666,21 +678,41 @@ class DatabaseManager {
|
|
|
666
678
|
const id = crypto_1.default.randomUUID();
|
|
667
679
|
const now = Date.now();
|
|
668
680
|
this.db
|
|
669
|
-
.prepare('INSERT INTO rules (id, route_id, content_type, target_service_id, target_model, replaced_model, sort_order, timeout, token_limit, total_tokens_used, reset_interval, last_reset_at, token_reset_base_time, request_count_limit, total_requests_used, request_reset_interval, request_last_reset_at, request_reset_base_time, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
|
|
670
|
-
.run(id, route.routeId, route.contentType, route.targetServiceId, route.targetModel || null, route.replacedModel || null, route.sortOrder || 0, route.timeout || null, route.tokenLimit || null, route.totalTokensUsed || 0, route.resetInterval || null, route.lastResetAt || null, route.tokenResetBaseTime || null, route.requestCountLimit || null, route.totalRequestsUsed || 0, route.requestResetInterval || null, route.requestLastResetAt || null, route.requestResetBaseTime || null, now, now);
|
|
681
|
+
.prepare('INSERT INTO rules (id, route_id, content_type, target_service_id, target_model, replaced_model, sort_order, timeout, token_limit, total_tokens_used, reset_interval, last_reset_at, token_reset_base_time, request_count_limit, total_requests_used, request_reset_interval, request_last_reset_at, request_reset_base_time, is_disabled, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
|
|
682
|
+
.run(id, route.routeId, route.contentType, route.targetServiceId, route.targetModel || null, route.replacedModel || null, route.sortOrder || 0, route.timeout || null, route.tokenLimit || null, route.totalTokensUsed || 0, route.resetInterval || null, route.lastResetAt || null, route.tokenResetBaseTime || null, route.requestCountLimit || null, route.totalRequestsUsed || 0, route.requestResetInterval || null, route.requestLastResetAt || null, route.requestResetBaseTime || null, route.isDisabled ? 1 : 0, now, now);
|
|
671
683
|
return Object.assign(Object.assign({}, route), { id, createdAt: now, updatedAt: now });
|
|
672
684
|
}
|
|
673
685
|
updateRule(id, route) {
|
|
674
686
|
const now = Date.now();
|
|
675
687
|
const result = this.db
|
|
676
|
-
.prepare('UPDATE rules SET content_type = ?, target_service_id = ?, target_model = ?, replaced_model = ?, sort_order = ?, timeout = ?, token_limit = ?, reset_interval = ?, token_reset_base_time = ?, request_count_limit = ?, request_reset_interval = ?, request_reset_base_time = ?, updated_at = ? WHERE id = ?')
|
|
677
|
-
.run(route.contentType, route.targetServiceId, route.targetModel || null, route.replacedModel || null, route.sortOrder || 0, route.timeout !== undefined ? route.timeout : null, route.tokenLimit !== undefined ? route.tokenLimit : null, route.resetInterval !== undefined ? route.resetInterval : null, route.tokenResetBaseTime !== undefined ? route.tokenResetBaseTime : null, route.requestCountLimit !== undefined ? route.requestCountLimit : null, route.requestResetInterval !== undefined ? route.requestResetInterval : null, route.requestResetBaseTime !== undefined ? route.requestResetBaseTime : null, now, id);
|
|
688
|
+
.prepare('UPDATE rules SET content_type = ?, target_service_id = ?, target_model = ?, replaced_model = ?, sort_order = ?, timeout = ?, token_limit = ?, reset_interval = ?, token_reset_base_time = ?, request_count_limit = ?, request_reset_interval = ?, request_reset_base_time = ?, is_disabled = ?, updated_at = ? WHERE id = ?')
|
|
689
|
+
.run(route.contentType, route.targetServiceId, route.targetModel || null, route.replacedModel || null, route.sortOrder || 0, route.timeout !== undefined ? route.timeout : null, route.tokenLimit !== undefined ? route.tokenLimit : null, route.resetInterval !== undefined ? route.resetInterval : null, route.tokenResetBaseTime !== undefined ? route.tokenResetBaseTime : null, route.requestCountLimit !== undefined ? route.requestCountLimit : null, route.requestResetInterval !== undefined ? route.requestResetInterval : null, route.requestResetBaseTime !== undefined ? route.requestResetBaseTime : null, route.isDisabled !== undefined ? (route.isDisabled ? 1 : 0) : null, now, id);
|
|
678
690
|
return result.changes > 0;
|
|
679
691
|
}
|
|
680
692
|
deleteRule(id) {
|
|
681
693
|
const result = this.db.prepare('DELETE FROM rules WHERE id = ?').run(id);
|
|
682
694
|
return result.changes > 0;
|
|
683
695
|
}
|
|
696
|
+
/**
|
|
697
|
+
* 切换规则的临时屏蔽状态
|
|
698
|
+
* @param ruleId 规则ID
|
|
699
|
+
* @returns 是否成功
|
|
700
|
+
*/
|
|
701
|
+
toggleRuleDisabled(ruleId) {
|
|
702
|
+
const rule = this.getRule(ruleId);
|
|
703
|
+
if (!rule) {
|
|
704
|
+
return { success: false, isDisabled: false };
|
|
705
|
+
}
|
|
706
|
+
const now = Date.now();
|
|
707
|
+
const newDisabledState = !rule.isDisabled;
|
|
708
|
+
const result = this.db
|
|
709
|
+
.prepare('UPDATE rules SET is_disabled = ?, updated_at = ? WHERE id = ?')
|
|
710
|
+
.run(newDisabledState ? 1 : 0, now, ruleId);
|
|
711
|
+
return {
|
|
712
|
+
success: result.changes > 0,
|
|
713
|
+
isDisabled: newDisabledState
|
|
714
|
+
};
|
|
715
|
+
}
|
|
684
716
|
/**
|
|
685
717
|
* 增加规则的token使用量
|
|
686
718
|
* @param ruleId 规则ID
|
|
@@ -1134,8 +1166,8 @@ class DatabaseManager {
|
|
|
1134
1166
|
// Import rules
|
|
1135
1167
|
for (const rule of importData.rules) {
|
|
1136
1168
|
this.db
|
|
1137
|
-
.prepare('INSERT INTO rules (id, route_id, content_type, target_service_id, target_model, replaced_model, sort_order, timeout, token_limit, total_tokens_used, reset_interval, last_reset_at, token_reset_base_time, request_count_limit, total_requests_used, request_reset_interval, request_last_reset_at, request_reset_base_time, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
|
|
1138
|
-
.run(rule.id, rule.routeId, rule.contentType || 'default', rule.targetServiceId, rule.targetModel || null, rule.replacedModel || null, rule.sortOrder || 0, rule.timeout || null, rule.tokenLimit || null, rule.totalTokensUsed || 0, rule.resetInterval || null, rule.lastResetAt || null, rule.tokenResetBaseTime || null, rule.requestCountLimit || null, rule.totalRequestsUsed || 0, rule.requestResetInterval || null, rule.requestLastResetAt || null, rule.requestResetBaseTime || null, rule.createdAt, rule.updatedAt);
|
|
1169
|
+
.prepare('INSERT INTO rules (id, route_id, content_type, target_service_id, target_model, replaced_model, sort_order, timeout, token_limit, total_tokens_used, reset_interval, last_reset_at, token_reset_base_time, request_count_limit, total_requests_used, request_reset_interval, request_last_reset_at, request_reset_base_time, is_disabled, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
|
|
1170
|
+
.run(rule.id, rule.routeId, rule.contentType || 'default', rule.targetServiceId, rule.targetModel || null, rule.replacedModel || null, rule.sortOrder || 0, rule.timeout || null, rule.tokenLimit || null, rule.totalTokensUsed || 0, rule.resetInterval || null, rule.lastResetAt || null, rule.tokenResetBaseTime || null, rule.requestCountLimit || null, rule.totalRequestsUsed || 0, rule.requestResetInterval || null, rule.requestLastResetAt || null, rule.requestResetBaseTime || null, rule.isDisabled ? 1 : 0, rule.createdAt, rule.updatedAt);
|
|
1139
1171
|
}
|
|
1140
1172
|
// Update config
|
|
1141
1173
|
this.updateConfig(importData.config);
|
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');
|
|
@@ -760,6 +763,7 @@ const registerRoutes = (dbManager, proxyServer) => {
|
|
|
760
763
|
app.delete('/api/rules/:id', (req, res) => res.json(dbManager.deleteRule(req.params.id)));
|
|
761
764
|
app.put('/api/rules/:id/reset-tokens', (req, res) => res.json(dbManager.resetRuleTokenUsage(req.params.id)));
|
|
762
765
|
app.put('/api/rules/:id/reset-requests', (req, res) => res.json(dbManager.resetRuleRequestCount(req.params.id)));
|
|
766
|
+
app.put('/api/rules/:id/toggle-disable', (req, res) => res.json(dbManager.toggleRuleDisabled(req.params.id)));
|
|
763
767
|
// 解除规则的黑名单状态
|
|
764
768
|
app.put('/api/rules/:id/clear-blacklist', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
765
769
|
const { id } = req.params;
|
|
@@ -1355,6 +1359,19 @@ ${instruction}
|
|
|
1355
1359
|
res.json({ success: false });
|
|
1356
1360
|
}
|
|
1357
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
|
+
})));
|
|
1358
1375
|
};
|
|
1359
1376
|
const start = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
1360
1377
|
fs_1.default.mkdirSync(dataDir, { recursive: true });
|
|
@@ -1376,6 +1393,28 @@ const start = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
1376
1393
|
const server = app.listen(port, host, () => {
|
|
1377
1394
|
console.log(`Admin server running on http://${host}:${port}`);
|
|
1378
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`);
|
|
1379
1418
|
const shutdown = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
1380
1419
|
console.log('Shutting down server...');
|
|
1381
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 {
|
|
@@ -627,6 +628,10 @@ class ProxyServer {
|
|
|
627
628
|
const rules = this.getRulesByRouteId(routeId);
|
|
628
629
|
if (!rules || rules.length === 0)
|
|
629
630
|
return undefined;
|
|
631
|
+
// 过滤掉被屏蔽的规则
|
|
632
|
+
const enabledRules = rules.filter(rule => !rule.isDisabled);
|
|
633
|
+
if (enabledRules.length === 0)
|
|
634
|
+
return undefined;
|
|
630
635
|
const body = req.body;
|
|
631
636
|
const requestModel = body === null || body === void 0 ? void 0 : body.model;
|
|
632
637
|
// 1. 首先查找 model-mapping 类型的规则,按 sortOrder 降序匹配
|
|
@@ -704,22 +709,26 @@ class ProxyServer {
|
|
|
704
709
|
const rules = this.getRulesByRouteId(routeId);
|
|
705
710
|
if (!rules || rules.length === 0)
|
|
706
711
|
return [];
|
|
712
|
+
// 过滤掉被屏蔽的规则
|
|
713
|
+
const enabledRules = rules.filter(rule => !rule.isDisabled);
|
|
714
|
+
if (enabledRules.length === 0)
|
|
715
|
+
return [];
|
|
707
716
|
const body = req.body;
|
|
708
717
|
const requestModel = body === null || body === void 0 ? void 0 : body.model;
|
|
709
718
|
const candidates = [];
|
|
710
719
|
// 1. Model mapping rules
|
|
711
720
|
if (requestModel) {
|
|
712
|
-
const modelMappingRules =
|
|
721
|
+
const modelMappingRules = enabledRules.filter(rule => rule.contentType === 'model-mapping' &&
|
|
713
722
|
rule.replacedModel &&
|
|
714
723
|
requestModel.includes(rule.replacedModel));
|
|
715
724
|
candidates.push(...modelMappingRules);
|
|
716
725
|
}
|
|
717
726
|
// 2. Content type specific rules
|
|
718
727
|
const contentType = this.determineContentType(req);
|
|
719
|
-
const contentTypeRules =
|
|
728
|
+
const contentTypeRules = enabledRules.filter(rule => rule.contentType === contentType);
|
|
720
729
|
candidates.push(...contentTypeRules);
|
|
721
730
|
// 3. Default rules
|
|
722
|
-
const defaultRules =
|
|
731
|
+
const defaultRules = enabledRules.filter(rule => rule.contentType === 'default');
|
|
723
732
|
candidates.push(...defaultRules);
|
|
724
733
|
// 4. 检查并重置到期的规则
|
|
725
734
|
candidates.forEach(rule => {
|
|
@@ -754,6 +763,10 @@ class ProxyServer {
|
|
|
754
763
|
const body = req.body;
|
|
755
764
|
if (!body)
|
|
756
765
|
return 'default';
|
|
766
|
+
// 检查是否为 count_tokens 请求(后台类型)
|
|
767
|
+
if (req.path.includes('/count_tokens')) {
|
|
768
|
+
return 'background';
|
|
769
|
+
}
|
|
757
770
|
const explicitType = this.getExplicitContentType(req, body);
|
|
758
771
|
if (explicitType) {
|
|
759
772
|
return explicitType;
|
|
@@ -882,6 +895,17 @@ class ProxyServer {
|
|
|
882
895
|
}
|
|
883
896
|
hasBackgroundSignal(body) {
|
|
884
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
|
+
// 检测其他后台信号
|
|
885
909
|
const candidates = [
|
|
886
910
|
body === null || body === void 0 ? void 0 : body.background,
|
|
887
911
|
(_a = body === null || body === void 0 ? void 0 : body.metadata) === null || _a === void 0 ? void 0 : _a.background,
|
|
@@ -1239,6 +1263,7 @@ class ProxyServer {
|
|
|
1239
1263
|
/**
|
|
1240
1264
|
* 提取会话标题(默认方法)
|
|
1241
1265
|
* 对于新会话,尝试从第一条消息的内容中提取标题
|
|
1266
|
+
* 优化:使用第一条用户消息的完整内容,并智能截取
|
|
1242
1267
|
*/
|
|
1243
1268
|
defaultExtractSessionTitle(request, sessionId) {
|
|
1244
1269
|
var _a;
|
|
@@ -1254,21 +1279,51 @@ class ProxyServer {
|
|
|
1254
1279
|
const firstUserMessage = messages.find((msg) => msg.role === 'user');
|
|
1255
1280
|
if (firstUserMessage) {
|
|
1256
1281
|
const content = firstUserMessage.content;
|
|
1282
|
+
let rawText = '';
|
|
1257
1283
|
if (typeof content === 'string') {
|
|
1258
|
-
|
|
1259
|
-
return content.slice(0, 50).trim();
|
|
1284
|
+
rawText = content;
|
|
1260
1285
|
}
|
|
1261
1286
|
else if (Array.isArray(content)) {
|
|
1262
1287
|
// 处理结构化内容(如图片+文本)
|
|
1263
1288
|
const textBlock = content.find((block) => (block === null || block === void 0 ? void 0 : block.type) === 'text');
|
|
1264
1289
|
if (textBlock === null || textBlock === void 0 ? void 0 : textBlock.text) {
|
|
1265
|
-
|
|
1290
|
+
rawText = textBlock.text;
|
|
1266
1291
|
}
|
|
1267
1292
|
}
|
|
1293
|
+
if (rawText) {
|
|
1294
|
+
return this.formatSessionTitle(rawText);
|
|
1295
|
+
}
|
|
1268
1296
|
}
|
|
1269
1297
|
}
|
|
1270
1298
|
return undefined;
|
|
1271
1299
|
}
|
|
1300
|
+
/**
|
|
1301
|
+
* 格式化会话标题
|
|
1302
|
+
* - 去除多余空白和换行符
|
|
1303
|
+
* - 智能截取,在单词边界处截断
|
|
1304
|
+
* - 限制最大长度为100个字符
|
|
1305
|
+
*/
|
|
1306
|
+
formatSessionTitle(text) {
|
|
1307
|
+
// 去除多余空白和换行符,替换为单个空格
|
|
1308
|
+
let formatted = text
|
|
1309
|
+
.replace(/\s+/g, ' ') // 多个空白字符替换为单个空格
|
|
1310
|
+
.replace(/[\r\n]+/g, ' ') // 换行符替换为空格
|
|
1311
|
+
.trim();
|
|
1312
|
+
// 限制最大长度
|
|
1313
|
+
const maxLength = 100;
|
|
1314
|
+
if (formatted.length <= maxLength) {
|
|
1315
|
+
return formatted;
|
|
1316
|
+
}
|
|
1317
|
+
// 在单词边界处截断
|
|
1318
|
+
let truncated = formatted.slice(0, maxLength);
|
|
1319
|
+
const lastSpaceIndex = truncated.lastIndexOf(' ');
|
|
1320
|
+
if (lastSpaceIndex > maxLength * 0.7) {
|
|
1321
|
+
// 如果最后一个空格位置在长度的70%之后,在空格处截断
|
|
1322
|
+
truncated = truncated.slice(0, lastSpaceIndex);
|
|
1323
|
+
}
|
|
1324
|
+
// 添加省略号
|
|
1325
|
+
return truncated.trim() + '...';
|
|
1326
|
+
}
|
|
1272
1327
|
/**
|
|
1273
1328
|
* 根据源工具类型和目标API类型,映射请求路径
|
|
1274
1329
|
* @param sourceTool 源工具类型 (claude-code 或 codex)
|
|
@@ -1320,6 +1375,8 @@ class ProxyServer {
|
|
|
1320
1375
|
let streamChunksForLog;
|
|
1321
1376
|
let upstreamRequestForLog;
|
|
1322
1377
|
let actuallyUsedProxy = false; // 标记是否实际使用了代理
|
|
1378
|
+
// 标记规则正在使用
|
|
1379
|
+
rules_status_service_1.rulesStatusBroadcaster.markRuleInUse(route.id, rule.id);
|
|
1323
1380
|
const finalizeLog = (statusCode, error) => __awaiter(this, void 0, void 0, function* () {
|
|
1324
1381
|
var _a, _b;
|
|
1325
1382
|
if (logged)
|
|
@@ -1389,6 +1446,11 @@ class ProxyServer {
|
|
|
1389
1446
|
const totalTokens = (usageForLog.inputTokens || 0) + (usageForLog.outputTokens || 0);
|
|
1390
1447
|
if (totalTokens > 0) {
|
|
1391
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
|
+
}
|
|
1392
1454
|
}
|
|
1393
1455
|
}
|
|
1394
1456
|
// 更新规则的请求次数(只在成功请求时更新)
|
|
@@ -1398,6 +1460,11 @@ class ProxyServer {
|
|
|
1398
1460
|
// 检查是否是重复请求(如网络重试)
|
|
1399
1461
|
if (!this.isRequestProcessed(requestHash)) {
|
|
1400
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
|
+
}
|
|
1401
1468
|
}
|
|
1402
1469
|
// 定期清理过期缓存
|
|
1403
1470
|
if (Math.random() < 0.01) { // 1%概率清理,避免每次都清理
|
|
@@ -1533,6 +1600,7 @@ class ProxyServer {
|
|
|
1533
1600
|
}
|
|
1534
1601
|
// 收集stream chunks(每个chunk是一个完整的SSE事件)
|
|
1535
1602
|
streamChunksForLog = eventCollector.getChunks();
|
|
1603
|
+
console.log('[Proxy] Stream request finished, collected chunks:', (streamChunksForLog === null || streamChunksForLog === void 0 ? void 0 : streamChunksForLog.length) || 0);
|
|
1536
1604
|
void finalizeLog(res.statusCode);
|
|
1537
1605
|
});
|
|
1538
1606
|
// 监听 res 的错误事件
|
|
@@ -1553,6 +1621,7 @@ class ProxyServer {
|
|
|
1553
1621
|
errorStack: error.stack,
|
|
1554
1622
|
requestHeaders: this.normalizeHeaders(req.headers),
|
|
1555
1623
|
requestBody: req.body ? JSON.stringify(req.body) : undefined,
|
|
1624
|
+
upstreamRequest: upstreamRequestForLog,
|
|
1556
1625
|
});
|
|
1557
1626
|
}
|
|
1558
1627
|
catch (logError) {
|
|
@@ -1602,6 +1671,7 @@ class ProxyServer {
|
|
|
1602
1671
|
}
|
|
1603
1672
|
}
|
|
1604
1673
|
streamChunksForLog = eventCollector.getChunks();
|
|
1674
|
+
console.log('[Proxy] Codex stream request finished, collected chunks:', (streamChunksForLog === null || streamChunksForLog === void 0 ? void 0 : streamChunksForLog.length) || 0);
|
|
1605
1675
|
void finalizeLog(res.statusCode);
|
|
1606
1676
|
});
|
|
1607
1677
|
// 监听 res 的错误事件
|
|
@@ -1622,6 +1692,7 @@ class ProxyServer {
|
|
|
1622
1692
|
errorStack: error.stack,
|
|
1623
1693
|
requestHeaders: this.normalizeHeaders(req.headers),
|
|
1624
1694
|
requestBody: req.body ? JSON.stringify(req.body) : undefined,
|
|
1695
|
+
upstreamRequest: upstreamRequestForLog,
|
|
1625
1696
|
});
|
|
1626
1697
|
}
|
|
1627
1698
|
catch (logError) {
|
|
@@ -1676,6 +1747,7 @@ class ProxyServer {
|
|
|
1676
1747
|
errorStack: error.stack,
|
|
1677
1748
|
requestHeaders: this.normalizeHeaders(req.headers),
|
|
1678
1749
|
requestBody: req.body ? JSON.stringify(req.body) : undefined,
|
|
1750
|
+
upstreamRequest: upstreamRequestForLog,
|
|
1679
1751
|
});
|
|
1680
1752
|
}
|
|
1681
1753
|
catch (logError) {
|
|
@@ -1751,6 +1823,7 @@ class ProxyServer {
|
|
|
1751
1823
|
usageForLog = this.extractTokenUsage(responseData === null || responseData === void 0 ? void 0 : responseData.usage);
|
|
1752
1824
|
// 记录原始响应体
|
|
1753
1825
|
responseBodyForLog = typeof responseData === 'string' ? responseData : JSON.stringify(responseData);
|
|
1826
|
+
console.log('[Proxy] Non-stream response logged, body length:', (responseBodyForLog === null || responseBodyForLog === void 0 ? void 0 : responseBodyForLog.length) || 0);
|
|
1754
1827
|
this.copyResponseHeaders(responseHeaders, res);
|
|
1755
1828
|
if (contentType.includes('application/json')) {
|
|
1756
1829
|
res.status(response.status).json(responseData);
|
|
@@ -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
|
+
}
|