aicodeswitch 2.0.8 → 2.0.10
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 +16 -1
- package/bin/stop.js +1 -2
- package/dist/server/database.js +77 -6
- package/dist/server/main.js +13 -2
- package/dist/server/proxy-server.js +162 -23
- package/dist/server/transformers/chunk-collector.js +103 -18
- package/dist/server/transformers/claude-openai.js +5 -2
- package/dist/server/transformers/streaming.js +220 -113
- package/dist/ui/assets/index-BYeFnkER.js +431 -0
- package/dist/ui/assets/index-DQ2LJr-O.css +1 -0
- package/dist/ui/index.html +2 -2
- package/package.json +1 -1
- package/dist/ui/assets/index-BOY_bl12.js +0 -441
- package/dist/ui/assets/index-BOlCGnMv.css +0 -1
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.10 (2026-02-03)
|
|
6
|
+
|
|
7
|
+
### 2.0.9 (2026-02-02)
|
|
8
|
+
|
|
5
9
|
### 2.0.8 (2026-02-02)
|
|
6
10
|
|
|
7
11
|
### 2.0.7 (2026-02-02)
|
package/CLAUDE.md
CHANGED
|
@@ -163,7 +163,21 @@ 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
|
+
- **Session Management**:
|
|
168
|
+
- Tracks user sessions based on session ID (Claude Code: `metadata.user_id`, Codex: `headers.session_id`)
|
|
169
|
+
- Auto-generates session title from first user message content:
|
|
170
|
+
- Extracts text from first user message
|
|
171
|
+
- Cleans up whitespace and newlines
|
|
172
|
+
- Intelligently truncates at word boundaries (max 100 chars)
|
|
173
|
+
- Adds "..." for truncated titles
|
|
174
|
+
- Records first request time, last request time, request count, and total tokens per session
|
|
175
|
+
|
|
176
|
+
### Usage Limits Auto-Sync
|
|
177
|
+
- **Service-Level Limits**: API services can have token and request count limits configured
|
|
178
|
+
- **Auto-Sync to Rules**: When an API service's usage limits are modified, all rules using that service are automatically updated with the new limits
|
|
179
|
+
- **Inheritance Detection**: When editing a rule, the system detects if the rule's limits match the service's limits and displays them as "inherited" (read-only)
|
|
180
|
+
- **Manual Override**: Rules can be configured with custom limits that differ from the service defaults
|
|
167
181
|
|
|
168
182
|
## Development Tips
|
|
169
183
|
|
|
@@ -197,3 +211,4 @@ aicos version # Show current version information
|
|
|
197
211
|
* 所有对话请使用中文。生成代码中的文案及相关注释根据代码原本的语言生成。
|
|
198
212
|
* 在服务端,直接使用 __dirname 来获取当前目录,不要使用 process.cwd()
|
|
199
213
|
* 每次有新的变化时,你需要更新 CLAUDE.md 来让文档保持最新。
|
|
214
|
+
* 禁止在项目中使用依赖GPU的css样式处理。
|
package/bin/stop.js
CHANGED
|
@@ -18,9 +18,8 @@ const stop = async (options = {}) => {
|
|
|
18
18
|
|
|
19
19
|
// 第一步:如果 PID 文件存在,优先通过 PID 文件停止服务器
|
|
20
20
|
if (fs.existsSync(PID_FILE)) {
|
|
21
|
+
const pid = parseInt(fs.readFileSync(PID_FILE, 'utf-8'), 10);
|
|
21
22
|
try {
|
|
22
|
-
const pid = parseInt(fs.readFileSync(PID_FILE, 'utf-8'), 10);
|
|
23
|
-
|
|
24
23
|
const processInfo = await getProcessInfo(pid);
|
|
25
24
|
if (!silent) {
|
|
26
25
|
console.log('\n' + chalk.gray(`Process found: ${chalk.white(pid)} (${chalk.gray(processInfo)})`));
|
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,
|
|
@@ -515,8 +525,47 @@ class DatabaseManager {
|
|
|
515
525
|
if (result.changes > 0 && process.env.NODE_ENV === 'development') {
|
|
516
526
|
console.log(`[DB] Updated service ${id}: ${service.name} -> ${service.apiUrl}`);
|
|
517
527
|
}
|
|
528
|
+
// 如果更新成功,检查是否需要同步更新关联规则的超量限制
|
|
529
|
+
if (result.changes > 0) {
|
|
530
|
+
this.syncRulesWithServiceLimits(id, service);
|
|
531
|
+
}
|
|
518
532
|
return result.changes > 0;
|
|
519
533
|
}
|
|
534
|
+
/**
|
|
535
|
+
* 同步更新使用该服务的规则的超量限制
|
|
536
|
+
* 当API服务的超量限制修改时,自动更新所有使用该服务的规则
|
|
537
|
+
*/
|
|
538
|
+
syncRulesWithServiceLimits(serviceId, service) {
|
|
539
|
+
// 获取所有使用该服务的规则
|
|
540
|
+
const rules = this.db.prepare('SELECT id FROM rules WHERE target_service_id = ?').all(serviceId);
|
|
541
|
+
if (rules.length === 0) {
|
|
542
|
+
return; // 没有规则使用此服务,无需同步
|
|
543
|
+
}
|
|
544
|
+
const now = Date.now();
|
|
545
|
+
const ruleIds = rules.map(r => r.id);
|
|
546
|
+
// Token超量限制同步
|
|
547
|
+
if (service.enableTokenLimit !== undefined || service.tokenLimit !== undefined ||
|
|
548
|
+
service.tokenResetInterval !== undefined || service.tokenResetBaseTime !== undefined) {
|
|
549
|
+
// 获取当前服务的最新配置
|
|
550
|
+
const currentService = this.db.prepare('SELECT enable_token_limit, token_limit, token_reset_interval, token_reset_base_time FROM api_services WHERE id = ?').get(serviceId);
|
|
551
|
+
if (currentService && currentService.enable_token_limit === 1) {
|
|
552
|
+
// 启用了Token超量限制,同步到所有规则
|
|
553
|
+
this.db.prepare('UPDATE rules SET token_limit = ?, reset_interval = ?, token_reset_base_time = ?, updated_at = ? WHERE target_service_id = ?').run(currentService.token_limit, currentService.token_reset_interval, currentService.token_reset_base_time, now, serviceId);
|
|
554
|
+
console.log(`[DB] Synced token limits for ${ruleIds.length} rule(s) using service ${serviceId}`);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
// 请求次数超量限制同步
|
|
558
|
+
if (service.enableRequestLimit !== undefined || service.requestCountLimit !== undefined ||
|
|
559
|
+
service.requestResetInterval !== undefined || service.requestResetBaseTime !== undefined) {
|
|
560
|
+
// 获取当前服务的最新配置
|
|
561
|
+
const currentService = this.db.prepare('SELECT enable_request_limit, request_count_limit, request_reset_interval, request_reset_base_time FROM api_services WHERE id = ?').get(serviceId);
|
|
562
|
+
if (currentService && currentService.enable_request_limit === 1) {
|
|
563
|
+
// 启用了请求次数超量限制,同步到所有规则
|
|
564
|
+
this.db.prepare('UPDATE rules SET request_count_limit = ?, request_reset_interval = ?, request_reset_base_time = ?, updated_at = ? WHERE target_service_id = ?').run(currentService.request_count_limit, currentService.request_reset_interval, currentService.request_reset_base_time, now, serviceId);
|
|
565
|
+
console.log(`[DB] Synced request count limits for ${ruleIds.length} rule(s) using service ${serviceId}`);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
520
569
|
deleteAPIService(id) {
|
|
521
570
|
const result = this.db.prepare('DELETE FROM api_services WHERE id = ?').run(id);
|
|
522
571
|
return result.changes > 0;
|
|
@@ -592,6 +641,7 @@ class DatabaseManager {
|
|
|
592
641
|
requestResetInterval: row.request_reset_interval,
|
|
593
642
|
requestLastResetAt: row.request_last_reset_at,
|
|
594
643
|
requestResetBaseTime: row.request_reset_base_time,
|
|
644
|
+
isDisabled: row.is_disabled === 1,
|
|
595
645
|
createdAt: row.created_at,
|
|
596
646
|
updatedAt: row.updated_at,
|
|
597
647
|
}));
|
|
@@ -619,6 +669,7 @@ class DatabaseManager {
|
|
|
619
669
|
requestResetInterval: row.request_reset_interval,
|
|
620
670
|
requestLastResetAt: row.request_last_reset_at,
|
|
621
671
|
requestResetBaseTime: row.request_reset_base_time,
|
|
672
|
+
isDisabled: row.is_disabled === 1,
|
|
622
673
|
createdAt: row.created_at,
|
|
623
674
|
updatedAt: row.updated_at,
|
|
624
675
|
};
|
|
@@ -627,21 +678,41 @@ class DatabaseManager {
|
|
|
627
678
|
const id = crypto_1.default.randomUUID();
|
|
628
679
|
const now = Date.now();
|
|
629
680
|
this.db
|
|
630
|
-
.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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
|
|
631
|
-
.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);
|
|
632
683
|
return Object.assign(Object.assign({}, route), { id, createdAt: now, updatedAt: now });
|
|
633
684
|
}
|
|
634
685
|
updateRule(id, route) {
|
|
635
686
|
const now = Date.now();
|
|
636
687
|
const result = this.db
|
|
637
|
-
.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 = ?')
|
|
638
|
-
.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);
|
|
639
690
|
return result.changes > 0;
|
|
640
691
|
}
|
|
641
692
|
deleteRule(id) {
|
|
642
693
|
const result = this.db.prepare('DELETE FROM rules WHERE id = ?').run(id);
|
|
643
694
|
return result.changes > 0;
|
|
644
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
|
+
}
|
|
645
716
|
/**
|
|
646
717
|
* 增加规则的token使用量
|
|
647
718
|
* @param ruleId 规则ID
|
|
@@ -1095,8 +1166,8 @@ class DatabaseManager {
|
|
|
1095
1166
|
// Import rules
|
|
1096
1167
|
for (const rule of importData.rules) {
|
|
1097
1168
|
this.db
|
|
1098
|
-
.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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
|
|
1099
|
-
.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);
|
|
1100
1171
|
}
|
|
1101
1172
|
// Update config
|
|
1102
1173
|
this.updateConfig(importData.config);
|
package/dist/server/main.js
CHANGED
|
@@ -69,8 +69,8 @@ function getProxyAgent() {
|
|
|
69
69
|
}
|
|
70
70
|
const app = (0, express_1.default)();
|
|
71
71
|
app.use((0, cors_1.default)());
|
|
72
|
-
app.use(express_1.default.json({ limit: '
|
|
73
|
-
app.use(express_1.default.urlencoded({ extended: true }));
|
|
72
|
+
app.use(express_1.default.json({ limit: 'Infinity' }));
|
|
73
|
+
app.use(express_1.default.urlencoded({ extended: true, limit: 'Infinity' }));
|
|
74
74
|
const asyncHandler = (handler) => (req, res, next) => {
|
|
75
75
|
Promise.resolve(handler(req, res, next)).catch(next);
|
|
76
76
|
};
|
|
@@ -760,6 +760,7 @@ const registerRoutes = (dbManager, proxyServer) => {
|
|
|
760
760
|
app.delete('/api/rules/:id', (req, res) => res.json(dbManager.deleteRule(req.params.id)));
|
|
761
761
|
app.put('/api/rules/:id/reset-tokens', (req, res) => res.json(dbManager.resetRuleTokenUsage(req.params.id)));
|
|
762
762
|
app.put('/api/rules/:id/reset-requests', (req, res) => res.json(dbManager.resetRuleRequestCount(req.params.id)));
|
|
763
|
+
app.put('/api/rules/:id/toggle-disable', (req, res) => res.json(dbManager.toggleRuleDisabled(req.params.id)));
|
|
763
764
|
// 解除规则的黑名单状态
|
|
764
765
|
app.put('/api/rules/:id/clear-blacklist', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
765
766
|
const { id } = req.params;
|
|
@@ -1391,6 +1392,16 @@ app.use((err, _req, res, _next) => {
|
|
|
1391
1392
|
console.error(err);
|
|
1392
1393
|
res.status(500).json({ error: err.message || 'Internal server error' });
|
|
1393
1394
|
});
|
|
1395
|
+
// 全局未捕获异常处理 - 防止服务崩溃
|
|
1396
|
+
process.on('uncaughtException', (error) => {
|
|
1397
|
+
console.error('[Uncaught Exception] 服务遇到未捕获的异常:', error);
|
|
1398
|
+
console.error('[Uncaught Exception] 堆栈信息:', error.stack);
|
|
1399
|
+
// 不退出进程,继续运行
|
|
1400
|
+
});
|
|
1401
|
+
process.on('unhandledRejection', (reason) => {
|
|
1402
|
+
console.error('[Unhandled Rejection] 服务遇到未处理的 Promise 拒绝:', reason);
|
|
1403
|
+
// 不退出进程,继续运行
|
|
1404
|
+
});
|
|
1394
1405
|
start().catch((error) => {
|
|
1395
1406
|
console.error('Failed to start server:', error);
|
|
1396
1407
|
process.exit(1);
|
|
@@ -627,6 +627,10 @@ class ProxyServer {
|
|
|
627
627
|
const rules = this.getRulesByRouteId(routeId);
|
|
628
628
|
if (!rules || rules.length === 0)
|
|
629
629
|
return undefined;
|
|
630
|
+
// 过滤掉被屏蔽的规则
|
|
631
|
+
const enabledRules = rules.filter(rule => !rule.isDisabled);
|
|
632
|
+
if (enabledRules.length === 0)
|
|
633
|
+
return undefined;
|
|
630
634
|
const body = req.body;
|
|
631
635
|
const requestModel = body === null || body === void 0 ? void 0 : body.model;
|
|
632
636
|
// 1. 首先查找 model-mapping 类型的规则,按 sortOrder 降序匹配
|
|
@@ -643,8 +647,8 @@ class ProxyServer {
|
|
|
643
647
|
// 检查并重置到期的规则
|
|
644
648
|
this.dbManager.checkAndResetRuleIfNeeded(rule.id);
|
|
645
649
|
this.dbManager.checkAndResetRequestCountIfNeeded(rule.id);
|
|
646
|
-
// 检查token
|
|
647
|
-
if (rule.tokenLimit && rule.totalTokensUsed !== undefined && rule.totalTokensUsed >= rule.tokenLimit) {
|
|
650
|
+
// 检查token限制(tokenLimit单位是k,需要乘以1000转换为实际token数)
|
|
651
|
+
if (rule.tokenLimit && rule.totalTokensUsed !== undefined && rule.totalTokensUsed >= rule.tokenLimit * 1000) {
|
|
648
652
|
continue; // 跳过超限规则
|
|
649
653
|
}
|
|
650
654
|
// 检查请求次数限制
|
|
@@ -666,8 +670,8 @@ class ProxyServer {
|
|
|
666
670
|
// 检查并重置到期的规则
|
|
667
671
|
this.dbManager.checkAndResetRuleIfNeeded(rule.id);
|
|
668
672
|
this.dbManager.checkAndResetRequestCountIfNeeded(rule.id);
|
|
669
|
-
// 检查token
|
|
670
|
-
if (rule.tokenLimit && rule.totalTokensUsed !== undefined && rule.totalTokensUsed >= rule.tokenLimit) {
|
|
673
|
+
// 检查token限制(tokenLimit单位是k,需要乘以1000转换为实际token数)
|
|
674
|
+
if (rule.tokenLimit && rule.totalTokensUsed !== undefined && rule.totalTokensUsed >= rule.tokenLimit * 1000) {
|
|
671
675
|
continue; // 跳过超限规则
|
|
672
676
|
}
|
|
673
677
|
// 检查请求次数限制
|
|
@@ -687,8 +691,8 @@ class ProxyServer {
|
|
|
687
691
|
// 检查并重置到期的规则
|
|
688
692
|
this.dbManager.checkAndResetRuleIfNeeded(rule.id);
|
|
689
693
|
this.dbManager.checkAndResetRequestCountIfNeeded(rule.id);
|
|
690
|
-
// 检查token
|
|
691
|
-
if (rule.tokenLimit && rule.totalTokensUsed !== undefined && rule.totalTokensUsed >= rule.tokenLimit) {
|
|
694
|
+
// 检查token限制(tokenLimit单位是k,需要乘以1000转换为实际token数)
|
|
695
|
+
if (rule.tokenLimit && rule.totalTokensUsed !== undefined && rule.totalTokensUsed >= rule.tokenLimit * 1000) {
|
|
692
696
|
continue; // 跳过超限规则
|
|
693
697
|
}
|
|
694
698
|
// 检查请求次数限制
|
|
@@ -704,22 +708,26 @@ class ProxyServer {
|
|
|
704
708
|
const rules = this.getRulesByRouteId(routeId);
|
|
705
709
|
if (!rules || rules.length === 0)
|
|
706
710
|
return [];
|
|
711
|
+
// 过滤掉被屏蔽的规则
|
|
712
|
+
const enabledRules = rules.filter(rule => !rule.isDisabled);
|
|
713
|
+
if (enabledRules.length === 0)
|
|
714
|
+
return [];
|
|
707
715
|
const body = req.body;
|
|
708
716
|
const requestModel = body === null || body === void 0 ? void 0 : body.model;
|
|
709
717
|
const candidates = [];
|
|
710
718
|
// 1. Model mapping rules
|
|
711
719
|
if (requestModel) {
|
|
712
|
-
const modelMappingRules =
|
|
720
|
+
const modelMappingRules = enabledRules.filter(rule => rule.contentType === 'model-mapping' &&
|
|
713
721
|
rule.replacedModel &&
|
|
714
722
|
requestModel.includes(rule.replacedModel));
|
|
715
723
|
candidates.push(...modelMappingRules);
|
|
716
724
|
}
|
|
717
725
|
// 2. Content type specific rules
|
|
718
726
|
const contentType = this.determineContentType(req);
|
|
719
|
-
const contentTypeRules =
|
|
727
|
+
const contentTypeRules = enabledRules.filter(rule => rule.contentType === contentType);
|
|
720
728
|
candidates.push(...contentTypeRules);
|
|
721
729
|
// 3. Default rules
|
|
722
|
-
const defaultRules =
|
|
730
|
+
const defaultRules = enabledRules.filter(rule => rule.contentType === 'default');
|
|
723
731
|
candidates.push(...defaultRules);
|
|
724
732
|
// 4. 检查并重置到期的规则
|
|
725
733
|
candidates.forEach(rule => {
|
|
@@ -729,9 +737,9 @@ class ProxyServer {
|
|
|
729
737
|
// 5. 过滤掉超过限制的规则(仅在有多个候选规则时)
|
|
730
738
|
if (candidates.length > 1) {
|
|
731
739
|
const filteredCandidates = candidates.filter(rule => {
|
|
732
|
-
// 检查token
|
|
740
|
+
// 检查token限制(tokenLimit单位是k,需要乘以1000转换为实际token数)
|
|
733
741
|
if (rule.tokenLimit && rule.totalTokensUsed !== undefined) {
|
|
734
|
-
if (rule.totalTokensUsed >= rule.tokenLimit) {
|
|
742
|
+
if (rule.totalTokensUsed >= rule.tokenLimit * 1000) {
|
|
735
743
|
return false;
|
|
736
744
|
}
|
|
737
745
|
}
|
|
@@ -1239,6 +1247,7 @@ class ProxyServer {
|
|
|
1239
1247
|
/**
|
|
1240
1248
|
* 提取会话标题(默认方法)
|
|
1241
1249
|
* 对于新会话,尝试从第一条消息的内容中提取标题
|
|
1250
|
+
* 优化:使用第一条用户消息的完整内容,并智能截取
|
|
1242
1251
|
*/
|
|
1243
1252
|
defaultExtractSessionTitle(request, sessionId) {
|
|
1244
1253
|
var _a;
|
|
@@ -1254,21 +1263,51 @@ class ProxyServer {
|
|
|
1254
1263
|
const firstUserMessage = messages.find((msg) => msg.role === 'user');
|
|
1255
1264
|
if (firstUserMessage) {
|
|
1256
1265
|
const content = firstUserMessage.content;
|
|
1266
|
+
let rawText = '';
|
|
1257
1267
|
if (typeof content === 'string') {
|
|
1258
|
-
|
|
1259
|
-
return content.slice(0, 50).trim();
|
|
1268
|
+
rawText = content;
|
|
1260
1269
|
}
|
|
1261
1270
|
else if (Array.isArray(content)) {
|
|
1262
1271
|
// 处理结构化内容(如图片+文本)
|
|
1263
1272
|
const textBlock = content.find((block) => (block === null || block === void 0 ? void 0 : block.type) === 'text');
|
|
1264
1273
|
if (textBlock === null || textBlock === void 0 ? void 0 : textBlock.text) {
|
|
1265
|
-
|
|
1274
|
+
rawText = textBlock.text;
|
|
1266
1275
|
}
|
|
1267
1276
|
}
|
|
1277
|
+
if (rawText) {
|
|
1278
|
+
return this.formatSessionTitle(rawText);
|
|
1279
|
+
}
|
|
1268
1280
|
}
|
|
1269
1281
|
}
|
|
1270
1282
|
return undefined;
|
|
1271
1283
|
}
|
|
1284
|
+
/**
|
|
1285
|
+
* 格式化会话标题
|
|
1286
|
+
* - 去除多余空白和换行符
|
|
1287
|
+
* - 智能截取,在单词边界处截断
|
|
1288
|
+
* - 限制最大长度为100个字符
|
|
1289
|
+
*/
|
|
1290
|
+
formatSessionTitle(text) {
|
|
1291
|
+
// 去除多余空白和换行符,替换为单个空格
|
|
1292
|
+
let formatted = text
|
|
1293
|
+
.replace(/\s+/g, ' ') // 多个空白字符替换为单个空格
|
|
1294
|
+
.replace(/[\r\n]+/g, ' ') // 换行符替换为空格
|
|
1295
|
+
.trim();
|
|
1296
|
+
// 限制最大长度
|
|
1297
|
+
const maxLength = 100;
|
|
1298
|
+
if (formatted.length <= maxLength) {
|
|
1299
|
+
return formatted;
|
|
1300
|
+
}
|
|
1301
|
+
// 在单词边界处截断
|
|
1302
|
+
let truncated = formatted.slice(0, maxLength);
|
|
1303
|
+
const lastSpaceIndex = truncated.lastIndexOf(' ');
|
|
1304
|
+
if (lastSpaceIndex > maxLength * 0.7) {
|
|
1305
|
+
// 如果最后一个空格位置在长度的70%之后,在空格处截断
|
|
1306
|
+
truncated = truncated.slice(0, lastSpaceIndex);
|
|
1307
|
+
}
|
|
1308
|
+
// 添加省略号
|
|
1309
|
+
return truncated.trim() + '...';
|
|
1310
|
+
}
|
|
1272
1311
|
/**
|
|
1273
1312
|
* 根据源工具类型和目标API类型,映射请求路径
|
|
1274
1313
|
* @param sourceTool 源工具类型 (claude-code 或 codex)
|
|
@@ -1533,13 +1572,53 @@ class ProxyServer {
|
|
|
1533
1572
|
}
|
|
1534
1573
|
// 收集stream chunks(每个chunk是一个完整的SSE事件)
|
|
1535
1574
|
streamChunksForLog = eventCollector.getChunks();
|
|
1575
|
+
console.log('[Proxy] Stream request finished, collected chunks:', (streamChunksForLog === null || streamChunksForLog === void 0 ? void 0 : streamChunksForLog.length) || 0);
|
|
1536
1576
|
void finalizeLog(res.statusCode);
|
|
1537
1577
|
});
|
|
1538
|
-
|
|
1578
|
+
// 监听 res 的错误事件
|
|
1579
|
+
res.on('error', (err) => {
|
|
1580
|
+
console.error('[Proxy] Response stream error:', err);
|
|
1581
|
+
});
|
|
1582
|
+
(0, stream_1.pipeline)(response.data, parser, eventCollector, converter, serializer, res, (error) => __awaiter(this, void 0, void 0, function* () {
|
|
1539
1583
|
if (error) {
|
|
1540
|
-
|
|
1584
|
+
console.error('[Proxy] Pipeline error for claude-code:', error);
|
|
1585
|
+
// 记录到错误日志
|
|
1586
|
+
try {
|
|
1587
|
+
yield this.dbManager.addErrorLog({
|
|
1588
|
+
timestamp: Date.now(),
|
|
1589
|
+
method: req.method,
|
|
1590
|
+
path: req.path,
|
|
1591
|
+
statusCode: 500,
|
|
1592
|
+
errorMessage: error.message || 'Stream processing error',
|
|
1593
|
+
errorStack: error.stack,
|
|
1594
|
+
requestHeaders: this.normalizeHeaders(req.headers),
|
|
1595
|
+
requestBody: req.body ? JSON.stringify(req.body) : undefined,
|
|
1596
|
+
upstreamRequest: upstreamRequestForLog,
|
|
1597
|
+
});
|
|
1598
|
+
}
|
|
1599
|
+
catch (logError) {
|
|
1600
|
+
console.error('[Proxy] Failed to log error:', logError);
|
|
1601
|
+
}
|
|
1602
|
+
// 尝试向客户端发送错误事件
|
|
1603
|
+
try {
|
|
1604
|
+
if (!res.writableEnded) {
|
|
1605
|
+
const errorEvent = `event: error\ndata: ${JSON.stringify({
|
|
1606
|
+
type: 'error',
|
|
1607
|
+
error: {
|
|
1608
|
+
type: 'api_error',
|
|
1609
|
+
message: 'Stream processing error occurred'
|
|
1610
|
+
}
|
|
1611
|
+
})}\n\n`;
|
|
1612
|
+
res.write(errorEvent);
|
|
1613
|
+
res.end();
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
catch (writeError) {
|
|
1617
|
+
console.error('[Proxy] Failed to send error event:', writeError);
|
|
1618
|
+
}
|
|
1619
|
+
yield finalizeLog(500, error.message);
|
|
1541
1620
|
}
|
|
1542
|
-
});
|
|
1621
|
+
}));
|
|
1543
1622
|
return;
|
|
1544
1623
|
}
|
|
1545
1624
|
if (targetType === 'codex' && this.isClaudeSource(sourceType)) {
|
|
@@ -1564,21 +1643,62 @@ class ProxyServer {
|
|
|
1564
1643
|
}
|
|
1565
1644
|
}
|
|
1566
1645
|
streamChunksForLog = eventCollector.getChunks();
|
|
1646
|
+
console.log('[Proxy] Codex stream request finished, collected chunks:', (streamChunksForLog === null || streamChunksForLog === void 0 ? void 0 : streamChunksForLog.length) || 0);
|
|
1567
1647
|
void finalizeLog(res.statusCode);
|
|
1568
1648
|
});
|
|
1569
|
-
|
|
1649
|
+
// 监听 res 的错误事件
|
|
1650
|
+
res.on('error', (err) => {
|
|
1651
|
+
console.error('[Proxy] Response stream error:', err);
|
|
1652
|
+
});
|
|
1653
|
+
(0, stream_1.pipeline)(response.data, parser, eventCollector, converter, serializer, res, (error) => __awaiter(this, void 0, void 0, function* () {
|
|
1570
1654
|
if (error) {
|
|
1571
|
-
|
|
1655
|
+
console.error('[Proxy] Pipeline error for codex:', error);
|
|
1656
|
+
// 记录到错误日志
|
|
1657
|
+
try {
|
|
1658
|
+
yield this.dbManager.addErrorLog({
|
|
1659
|
+
timestamp: Date.now(),
|
|
1660
|
+
method: req.method,
|
|
1661
|
+
path: req.path,
|
|
1662
|
+
statusCode: 500,
|
|
1663
|
+
errorMessage: error.message || 'Stream processing error',
|
|
1664
|
+
errorStack: error.stack,
|
|
1665
|
+
requestHeaders: this.normalizeHeaders(req.headers),
|
|
1666
|
+
requestBody: req.body ? JSON.stringify(req.body) : undefined,
|
|
1667
|
+
upstreamRequest: upstreamRequestForLog,
|
|
1668
|
+
});
|
|
1669
|
+
}
|
|
1670
|
+
catch (logError) {
|
|
1671
|
+
console.error('[Proxy] Failed to log error:', logError);
|
|
1672
|
+
}
|
|
1673
|
+
// 尝试向客户端发送错误事件
|
|
1674
|
+
try {
|
|
1675
|
+
if (!res.writableEnded) {
|
|
1676
|
+
const errorEvent = `data: ${JSON.stringify({
|
|
1677
|
+
error: 'Stream processing error occurred'
|
|
1678
|
+
})}\n\n`;
|
|
1679
|
+
res.write(errorEvent);
|
|
1680
|
+
res.end();
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
catch (writeError) {
|
|
1684
|
+
console.error('[Proxy] Failed to send error event:', writeError);
|
|
1685
|
+
}
|
|
1686
|
+
yield finalizeLog(500, error.message);
|
|
1572
1687
|
}
|
|
1573
|
-
});
|
|
1688
|
+
}));
|
|
1574
1689
|
return;
|
|
1575
1690
|
}
|
|
1576
1691
|
// 默认stream处理(无转换)
|
|
1577
1692
|
const eventCollector = new chunk_collector_1.SSEEventCollectorTransform();
|
|
1578
1693
|
responseHeadersForLog = this.normalizeResponseHeaders(responseHeaders);
|
|
1579
1694
|
this.copyResponseHeaders(responseHeaders, res);
|
|
1695
|
+
// 监听 res 的错误事件
|
|
1696
|
+
res.on('error', (err) => {
|
|
1697
|
+
console.error('[Proxy] Response stream error:', err);
|
|
1698
|
+
});
|
|
1580
1699
|
res.on('finish', () => {
|
|
1581
1700
|
streamChunksForLog = eventCollector.getChunks();
|
|
1701
|
+
console.log('[Proxy] Default stream request finished, collected chunks:', (streamChunksForLog === null || streamChunksForLog === void 0 ? void 0 : streamChunksForLog.length) || 0);
|
|
1582
1702
|
// 尝试从event collector中提取usage信息
|
|
1583
1703
|
const extractedUsage = eventCollector.extractUsage();
|
|
1584
1704
|
if (extractedUsage) {
|
|
@@ -1586,11 +1706,29 @@ class ProxyServer {
|
|
|
1586
1706
|
}
|
|
1587
1707
|
void finalizeLog(res.statusCode);
|
|
1588
1708
|
});
|
|
1589
|
-
(0, stream_1.pipeline)(response.data, eventCollector, res, (error) => {
|
|
1709
|
+
(0, stream_1.pipeline)(response.data, eventCollector, res, (error) => __awaiter(this, void 0, void 0, function* () {
|
|
1590
1710
|
if (error) {
|
|
1591
|
-
|
|
1711
|
+
console.error('[Proxy] Pipeline error (default stream):', error);
|
|
1712
|
+
// 记录到错误日志
|
|
1713
|
+
try {
|
|
1714
|
+
yield this.dbManager.addErrorLog({
|
|
1715
|
+
timestamp: Date.now(),
|
|
1716
|
+
method: req.method,
|
|
1717
|
+
path: req.path,
|
|
1718
|
+
statusCode: 500,
|
|
1719
|
+
errorMessage: error.message || 'Stream processing error',
|
|
1720
|
+
errorStack: error.stack,
|
|
1721
|
+
requestHeaders: this.normalizeHeaders(req.headers),
|
|
1722
|
+
requestBody: req.body ? JSON.stringify(req.body) : undefined,
|
|
1723
|
+
upstreamRequest: upstreamRequestForLog,
|
|
1724
|
+
});
|
|
1725
|
+
}
|
|
1726
|
+
catch (logError) {
|
|
1727
|
+
console.error('[Proxy] Failed to log error:', logError);
|
|
1728
|
+
}
|
|
1729
|
+
yield finalizeLog(500, error.message);
|
|
1592
1730
|
}
|
|
1593
|
-
});
|
|
1731
|
+
}));
|
|
1594
1732
|
return;
|
|
1595
1733
|
}
|
|
1596
1734
|
let responseData = response.data;
|
|
@@ -1658,6 +1796,7 @@ class ProxyServer {
|
|
|
1658
1796
|
usageForLog = this.extractTokenUsage(responseData === null || responseData === void 0 ? void 0 : responseData.usage);
|
|
1659
1797
|
// 记录原始响应体
|
|
1660
1798
|
responseBodyForLog = typeof responseData === 'string' ? responseData : JSON.stringify(responseData);
|
|
1799
|
+
console.log('[Proxy] Non-stream response logged, body length:', (responseBodyForLog === null || responseBodyForLog === void 0 ? void 0 : responseBodyForLog.length) || 0);
|
|
1661
1800
|
this.copyResponseHeaders(responseHeaders, res);
|
|
1662
1801
|
if (contentType.includes('application/json')) {
|
|
1663
1802
|
res.status(response.status).json(responseData);
|