aicodeswitch 1.7.1 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/server/database.js +223 -18
- package/dist/server/main.js +1 -0
- package/dist/server/proxy-server.js +145 -12
- package/dist/ui/assets/index-CvBDcTGi.js +391 -0
- package/dist/ui/assets/index-vy5mPtJs.css +1 -0
- package/dist/ui/index.html +2 -2
- package/package.json +1 -1
- package/dist/ui/assets/index-Bfc2kKan.css +0 -1
- package/dist/ui/assets/index-DGODOljs.js +0 -391
package/README.md
CHANGED
|
@@ -97,7 +97,7 @@ Codex的配置覆盖逻辑一模一样。
|
|
|
97
97
|
|
|
98
98
|
**有什么用?**
|
|
99
99
|
|
|
100
|
-
aicodeswitch内部,会根据“源类型”来转换数据。例如,你的供应商API服务接口是OpenAI Chat
|
|
100
|
+
aicodeswitch内部,会根据“源类型”来转换数据。例如,你的供应商API服务接口是OpenAI Chat的数据格式,而你在路由中配置的“客户端工具“是Claude Code,那么就意味着,这个供应商API的数据,需要经过转换之后才能被Claude Code正确使用。
|
|
101
101
|
|
|
102
102
|
## 路由管理
|
|
103
103
|
|
|
@@ -105,7 +105,7 @@ aicodeswitch内部,会根据“源类型”来转换数据。例如,你的
|
|
|
105
105
|
|
|
106
106
|
路由是aicodeswitch的核心功能,它负责将不同的对象(目前指Claude Code和Codex)的请求,路由到不同的供应商API服务上。
|
|
107
107
|
|
|
108
|
-
###
|
|
108
|
+
### 什么是“客户端工具”?
|
|
109
109
|
|
|
110
110
|
目前指Claude Code或Codex。
|
|
111
111
|
|
package/dist/server/database.js
CHANGED
|
@@ -58,6 +58,16 @@ class DatabaseManager {
|
|
|
58
58
|
value: void 0
|
|
59
59
|
});
|
|
60
60
|
this.db = new better_sqlite3_1.default(path_1.default.join(dataPath, 'app.db'));
|
|
61
|
+
// 启用外键约束(SQLite 默认禁用,必须手动启用才能使 ON DELETE CASCADE 生效)
|
|
62
|
+
this.db.pragma('foreign_keys = ON');
|
|
63
|
+
// 配置数据库以确保实时读取最新数据
|
|
64
|
+
// WAL 模式 + normal 同步模式: 提供最佳的并发性和实时性
|
|
65
|
+
this.db.pragma('journal_mode = WAL');
|
|
66
|
+
this.db.pragma('synchronous = NORMAL');
|
|
67
|
+
// 确保读取操作不会看到旧的数据快照
|
|
68
|
+
// 在 WAL 模式下,默认情况下读取操作不会阻塞写入操作
|
|
69
|
+
// 设置 read_uncommitted = 0 确保读取最新提交的数据
|
|
70
|
+
this.db.pragma('read_uncommitted = 0');
|
|
61
71
|
this.logDb = new level_1.Level(path_1.default.join(dataPath, 'logs'), { valueEncoding: 'json' });
|
|
62
72
|
this.accessLogDb = new level_1.Level(path_1.default.join(dataPath, 'access-logs'), { valueEncoding: 'json' });
|
|
63
73
|
this.errorLogDb = new level_1.Level(path_1.default.join(dataPath, 'error-logs'), { valueEncoding: 'json' });
|
|
@@ -66,9 +76,137 @@ class DatabaseManager {
|
|
|
66
76
|
initialize() {
|
|
67
77
|
return __awaiter(this, void 0, void 0, function* () {
|
|
68
78
|
this.createTables();
|
|
79
|
+
yield this.runMigrations();
|
|
69
80
|
yield this.ensureDefaultConfig();
|
|
70
81
|
});
|
|
71
82
|
}
|
|
83
|
+
runMigrations() {
|
|
84
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
85
|
+
const columns = this.db.pragma('table_info(api_services)');
|
|
86
|
+
// 检查是否有旧的 max_output_tokens 字段(单值版本)
|
|
87
|
+
const hasOldMaxOutputTokens = columns.some((col) => col.name === 'max_output_tokens');
|
|
88
|
+
// 检查是否已经有新的 model_limits 字段(JSON 版本)
|
|
89
|
+
const hasModelLimits = columns.some((col) => col.name === 'model_limits');
|
|
90
|
+
if (!hasModelLimits) {
|
|
91
|
+
if (hasOldMaxOutputTokens) {
|
|
92
|
+
// 如果有旧字段,先删除旧字段(SQLite 不支持 ALTER TABLE DROP COLUMN,需要重建表)
|
|
93
|
+
console.log('[DB] Running migration: Replacing max_output_tokens with model_limits');
|
|
94
|
+
yield this.migrateMaxOutputTokensToModelLimits();
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
// 直接添加新字段
|
|
98
|
+
console.log('[DB] Running migration: Adding model_limits column to api_services table');
|
|
99
|
+
this.db.exec('ALTER TABLE api_services ADD COLUMN model_limits TEXT;');
|
|
100
|
+
console.log('[DB] Migration completed: model_limits column added');
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// 检查rules表是否有token相关字段
|
|
104
|
+
const rulesColumns = this.db.pragma('table_info(rules)');
|
|
105
|
+
const hasTokenLimit = rulesColumns.some((col) => col.name === 'token_limit');
|
|
106
|
+
const hasTotalTokensUsed = rulesColumns.some((col) => col.name === 'total_tokens_used');
|
|
107
|
+
const hasResetInterval = rulesColumns.some((col) => col.name === 'reset_interval');
|
|
108
|
+
const hasLastResetAt = rulesColumns.some((col) => col.name === 'last_reset_at');
|
|
109
|
+
if (!hasTokenLimit) {
|
|
110
|
+
console.log('[DB] Running migration: Adding token_limit column to rules table');
|
|
111
|
+
this.db.exec('ALTER TABLE rules ADD COLUMN token_limit INTEGER;');
|
|
112
|
+
console.log('[DB] Migration completed: token_limit column added');
|
|
113
|
+
}
|
|
114
|
+
if (!hasTotalTokensUsed) {
|
|
115
|
+
console.log('[DB] Running migration: Adding total_tokens_used column to rules table');
|
|
116
|
+
this.db.exec('ALTER TABLE rules ADD COLUMN total_tokens_used INTEGER DEFAULT 0;');
|
|
117
|
+
console.log('[DB] Migration completed: total_tokens_used column added');
|
|
118
|
+
}
|
|
119
|
+
if (!hasResetInterval) {
|
|
120
|
+
console.log('[DB] Running migration: Adding reset_interval column to rules table');
|
|
121
|
+
this.db.exec('ALTER TABLE rules ADD COLUMN reset_interval INTEGER;');
|
|
122
|
+
console.log('[DB] Migration completed: reset_interval column added');
|
|
123
|
+
}
|
|
124
|
+
if (!hasLastResetAt) {
|
|
125
|
+
console.log('[DB] Running migration: Adding last_reset_at column to rules table');
|
|
126
|
+
this.db.exec('ALTER TABLE rules ADD COLUMN last_reset_at INTEGER;');
|
|
127
|
+
console.log('[DB] Migration completed: last_reset_at column added');
|
|
128
|
+
}
|
|
129
|
+
// 检查rules表是否有timeout字段
|
|
130
|
+
const hasRuleTimeout = rulesColumns.some((col) => col.name === 'timeout');
|
|
131
|
+
if (!hasRuleTimeout) {
|
|
132
|
+
console.log('[DB] Running migration: Adding timeout column to rules table');
|
|
133
|
+
this.db.exec('ALTER TABLE rules ADD COLUMN timeout INTEGER;');
|
|
134
|
+
console.log('[DB] Migration completed: timeout column added to rules');
|
|
135
|
+
}
|
|
136
|
+
// 检查api_services表是否有timeout字段,如果有则移除
|
|
137
|
+
const hasServiceTimeout = columns.some((col) => col.name === 'timeout');
|
|
138
|
+
if (hasServiceTimeout) {
|
|
139
|
+
console.log('[DB] Running migration: Removing timeout column from api_services table');
|
|
140
|
+
yield this.migrateRemoveServiceTimeout();
|
|
141
|
+
console.log('[DB] Migration completed: timeout column removed from api_services');
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
migrateMaxOutputTokensToModelLimits() {
|
|
146
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
147
|
+
// SQLite 不支持直接删除列,需要重建表
|
|
148
|
+
// 先临时禁用外键约束
|
|
149
|
+
this.db.pragma('foreign_keys = OFF');
|
|
150
|
+
this.db.exec(`
|
|
151
|
+
CREATE TABLE api_services_new (
|
|
152
|
+
id TEXT PRIMARY KEY,
|
|
153
|
+
vendor_id TEXT NOT NULL,
|
|
154
|
+
name TEXT NOT NULL,
|
|
155
|
+
api_url TEXT NOT NULL,
|
|
156
|
+
api_key TEXT NOT NULL,
|
|
157
|
+
timeout INTEGER,
|
|
158
|
+
source_type TEXT,
|
|
159
|
+
supported_models TEXT,
|
|
160
|
+
model_limits TEXT,
|
|
161
|
+
created_at INTEGER NOT NULL,
|
|
162
|
+
updated_at INTEGER NOT NULL,
|
|
163
|
+
FOREIGN KEY (vendor_id) REFERENCES vendors(id) ON DELETE CASCADE
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
INSERT INTO api_services_new
|
|
167
|
+
SELECT
|
|
168
|
+
id, vendor_id, name, api_url, api_key, timeout, source_type, supported_models,
|
|
169
|
+
NULL, -- model_limits 设为 NULL,旧数据需要手动配置
|
|
170
|
+
created_at, updated_at
|
|
171
|
+
FROM api_services;
|
|
172
|
+
|
|
173
|
+
DROP TABLE api_services;
|
|
174
|
+
ALTER TABLE api_services_new RENAME TO api_services;
|
|
175
|
+
`);
|
|
176
|
+
// 重新启用外键约束
|
|
177
|
+
this.db.pragma('foreign_keys = ON');
|
|
178
|
+
console.log('[DB] Migration completed: Replaced max_output_tokens with model_limits');
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
migrateRemoveServiceTimeout() {
|
|
182
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
183
|
+
// SQLite 不支持直接删除列,需要重建表
|
|
184
|
+
this.db.pragma('foreign_keys = OFF');
|
|
185
|
+
this.db.exec(`
|
|
186
|
+
CREATE TABLE api_services_new (
|
|
187
|
+
id TEXT PRIMARY KEY,
|
|
188
|
+
vendor_id TEXT NOT NULL,
|
|
189
|
+
name TEXT NOT NULL,
|
|
190
|
+
api_url TEXT NOT NULL,
|
|
191
|
+
api_key TEXT NOT NULL,
|
|
192
|
+
source_type TEXT,
|
|
193
|
+
supported_models TEXT,
|
|
194
|
+
model_limits TEXT,
|
|
195
|
+
created_at INTEGER NOT NULL,
|
|
196
|
+
updated_at INTEGER NOT NULL,
|
|
197
|
+
FOREIGN KEY (vendor_id) REFERENCES vendors(id) ON DELETE CASCADE
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
INSERT INTO api_services_new (id, vendor_id, name, api_url, api_key, source_type, supported_models, model_limits, created_at, updated_at)
|
|
201
|
+
SELECT id, vendor_id, name, api_url, api_key, source_type, supported_models, model_limits, created_at, updated_at
|
|
202
|
+
FROM api_services;
|
|
203
|
+
|
|
204
|
+
DROP TABLE api_services;
|
|
205
|
+
ALTER TABLE api_services_new RENAME TO api_services;
|
|
206
|
+
`);
|
|
207
|
+
this.db.pragma('foreign_keys = ON');
|
|
208
|
+
});
|
|
209
|
+
}
|
|
72
210
|
createTables() {
|
|
73
211
|
this.db.exec(`
|
|
74
212
|
CREATE TABLE IF NOT EXISTS vendors (
|
|
@@ -85,7 +223,6 @@ class DatabaseManager {
|
|
|
85
223
|
name TEXT NOT NULL,
|
|
86
224
|
api_url TEXT NOT NULL,
|
|
87
225
|
api_key TEXT NOT NULL,
|
|
88
|
-
timeout INTEGER,
|
|
89
226
|
source_type TEXT,
|
|
90
227
|
supported_models TEXT,
|
|
91
228
|
created_at INTEGER NOT NULL,
|
|
@@ -111,6 +248,11 @@ class DatabaseManager {
|
|
|
111
248
|
target_model TEXT,
|
|
112
249
|
replaced_model TEXT,
|
|
113
250
|
sort_order INTEGER DEFAULT 0,
|
|
251
|
+
timeout INTEGER,
|
|
252
|
+
token_limit INTEGER,
|
|
253
|
+
total_tokens_used INTEGER DEFAULT 0,
|
|
254
|
+
reset_interval INTEGER,
|
|
255
|
+
last_reset_at INTEGER,
|
|
114
256
|
created_at INTEGER NOT NULL,
|
|
115
257
|
updated_at INTEGER NOT NULL,
|
|
116
258
|
FOREIGN KEY (route_id) REFERENCES routes(id) ON DELETE CASCADE,
|
|
@@ -129,7 +271,7 @@ class DatabaseManager {
|
|
|
129
271
|
if (!config) {
|
|
130
272
|
const defaultConfig = {
|
|
131
273
|
enableLogging: true,
|
|
132
|
-
logRetentionDays:
|
|
274
|
+
logRetentionDays: 30,
|
|
133
275
|
maxLogSize: 1000,
|
|
134
276
|
apiKey: '',
|
|
135
277
|
enableFailover: true, // 默认启用智能故障切换
|
|
@@ -170,37 +312,48 @@ class DatabaseManager {
|
|
|
170
312
|
}
|
|
171
313
|
// API Service operations
|
|
172
314
|
getAPIServices(vendorId) {
|
|
315
|
+
// 每次都重新准备语句以确保获取最新数据
|
|
173
316
|
const query = vendorId
|
|
174
317
|
? 'SELECT * FROM api_services WHERE vendor_id = ? ORDER BY created_at DESC'
|
|
175
318
|
: 'SELECT * FROM api_services ORDER BY created_at DESC';
|
|
176
|
-
|
|
177
|
-
const
|
|
178
|
-
|
|
319
|
+
// 不缓存 prepared statement,每次重新创建以确保读取最新数据
|
|
320
|
+
const stmt = this.db.prepare(query);
|
|
321
|
+
const rows = vendorId ? stmt.all(vendorId) : stmt.all();
|
|
322
|
+
const services = rows.map((row) => ({
|
|
179
323
|
id: row.id,
|
|
180
324
|
vendorId: row.vendor_id,
|
|
181
325
|
name: row.name,
|
|
182
326
|
apiUrl: row.api_url,
|
|
183
327
|
apiKey: row.api_key,
|
|
184
|
-
timeout: row.timeout,
|
|
185
328
|
sourceType: row.source_type,
|
|
186
329
|
supportedModels: row.supported_models ? row.supported_models.split(',').map((model) => model.trim()).filter((model) => model.length > 0) : undefined,
|
|
330
|
+
modelLimits: row.model_limits ? JSON.parse(row.model_limits) : undefined,
|
|
187
331
|
createdAt: row.created_at,
|
|
188
332
|
updatedAt: row.updated_at,
|
|
189
333
|
}));
|
|
334
|
+
// 调试日志: 记录读取的服务信息
|
|
335
|
+
if (process.env.NODE_ENV === 'development' && services.length > 0) {
|
|
336
|
+
console.log(`[DB] Read ${services.length} services from database, first service: ${services[0].name} -> ${services[0].apiUrl}`);
|
|
337
|
+
}
|
|
338
|
+
return services;
|
|
190
339
|
}
|
|
191
340
|
createAPIService(service) {
|
|
192
341
|
const id = crypto_1.default.randomUUID();
|
|
193
342
|
const now = Date.now();
|
|
194
343
|
this.db
|
|
195
|
-
.prepare('INSERT INTO api_services (id, vendor_id, name, api_url, api_key,
|
|
196
|
-
.run(id, service.vendorId, service.name, service.apiUrl, service.apiKey, service.
|
|
344
|
+
.prepare('INSERT INTO api_services (id, vendor_id, name, api_url, api_key, source_type, supported_models, model_limits, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
|
|
345
|
+
.run(id, service.vendorId, service.name, service.apiUrl, service.apiKey, service.sourceType || null, service.supportedModels ? service.supportedModels.join(',') : null, service.modelLimits ? JSON.stringify(service.modelLimits) : null, now, now);
|
|
197
346
|
return Object.assign(Object.assign({}, service), { id, createdAt: now, updatedAt: now });
|
|
198
347
|
}
|
|
199
348
|
updateAPIService(id, service) {
|
|
200
349
|
const now = Date.now();
|
|
201
350
|
const result = this.db
|
|
202
|
-
.prepare('UPDATE api_services SET name = ?, api_url = ?, api_key = ?,
|
|
203
|
-
.run(service.name, service.apiUrl, service.apiKey, service.
|
|
351
|
+
.prepare('UPDATE api_services SET name = ?, api_url = ?, api_key = ?, source_type = ?, supported_models = ?, model_limits = ?, updated_at = ? WHERE id = ?')
|
|
352
|
+
.run(service.name, service.apiUrl, service.apiKey, service.sourceType || null, service.supportedModels ? service.supportedModels.join(',') : null, service.modelLimits ? JSON.stringify(service.modelLimits) : null, now, id);
|
|
353
|
+
// 调试日志: 记录更新操作
|
|
354
|
+
if (result.changes > 0 && process.env.NODE_ENV === 'development') {
|
|
355
|
+
console.log(`[DB] Updated service ${id}: ${service.name} -> ${service.apiUrl}`);
|
|
356
|
+
}
|
|
204
357
|
return result.changes > 0;
|
|
205
358
|
}
|
|
206
359
|
deleteAPIService(id) {
|
|
@@ -267,6 +420,11 @@ class DatabaseManager {
|
|
|
267
420
|
targetModel: row.target_model,
|
|
268
421
|
replacedModel: row.replaced_model,
|
|
269
422
|
sortOrder: row.sort_order,
|
|
423
|
+
timeout: row.timeout,
|
|
424
|
+
tokenLimit: row.token_limit,
|
|
425
|
+
totalTokensUsed: row.total_tokens_used,
|
|
426
|
+
resetInterval: row.reset_interval,
|
|
427
|
+
lastResetAt: row.last_reset_at,
|
|
270
428
|
createdAt: row.created_at,
|
|
271
429
|
updatedAt: row.updated_at,
|
|
272
430
|
}));
|
|
@@ -275,21 +433,68 @@ class DatabaseManager {
|
|
|
275
433
|
const id = crypto_1.default.randomUUID();
|
|
276
434
|
const now = Date.now();
|
|
277
435
|
this.db
|
|
278
|
-
.prepare('INSERT INTO rules (id, route_id, content_type, target_service_id, target_model, replaced_model, sort_order, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)')
|
|
279
|
-
.run(id, route.routeId, route.contentType, route.targetServiceId, route.targetModel || null, route.replacedModel || null, route.sortOrder || 0, now, now);
|
|
436
|
+
.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, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
|
|
437
|
+
.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, now, now);
|
|
280
438
|
return Object.assign(Object.assign({}, route), { id, createdAt: now, updatedAt: now });
|
|
281
439
|
}
|
|
282
440
|
updateRule(id, route) {
|
|
283
441
|
const now = Date.now();
|
|
284
442
|
const result = this.db
|
|
285
|
-
.prepare('UPDATE rules SET content_type = ?, target_service_id = ?, target_model = ?, replaced_model = ?, sort_order = ?, updated_at = ? WHERE id = ?')
|
|
286
|
-
.run(route.contentType, route.targetServiceId, route.targetModel || null, route.replacedModel || null, route.sortOrder || 0, now, id);
|
|
443
|
+
.prepare('UPDATE rules SET content_type = ?, target_service_id = ?, target_model = ?, replaced_model = ?, sort_order = ?, timeout = ?, token_limit = ?, reset_interval = ?, updated_at = ? WHERE id = ?')
|
|
444
|
+
.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, now, id);
|
|
287
445
|
return result.changes > 0;
|
|
288
446
|
}
|
|
289
447
|
deleteRule(id) {
|
|
290
448
|
const result = this.db.prepare('DELETE FROM rules WHERE id = ?').run(id);
|
|
291
449
|
return result.changes > 0;
|
|
292
450
|
}
|
|
451
|
+
/**
|
|
452
|
+
* 增加规则的token使用量
|
|
453
|
+
* @param ruleId 规则ID
|
|
454
|
+
* @param tokensUsed 使用的token数量
|
|
455
|
+
* @returns 是否成功
|
|
456
|
+
*/
|
|
457
|
+
incrementRuleTokenUsage(ruleId, tokensUsed) {
|
|
458
|
+
const result = this.db
|
|
459
|
+
.prepare('UPDATE rules SET total_tokens_used = total_tokens_used + ? WHERE id = ?')
|
|
460
|
+
.run(tokensUsed, ruleId);
|
|
461
|
+
return result.changes > 0;
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* 重置规则的token使用量
|
|
465
|
+
* @param ruleId 规则ID
|
|
466
|
+
* @returns 是否成功
|
|
467
|
+
*/
|
|
468
|
+
resetRuleTokenUsage(ruleId) {
|
|
469
|
+
const now = Date.now();
|
|
470
|
+
const result = this.db
|
|
471
|
+
.prepare('UPDATE rules SET total_tokens_used = 0, last_reset_at = ? WHERE id = ?')
|
|
472
|
+
.run(now, ruleId);
|
|
473
|
+
return result.changes > 0;
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* 检查并重置到期的规则
|
|
477
|
+
* 如果规则设置了reset_interval且已经到了重置时间,则自动重置token使用量
|
|
478
|
+
* @param ruleId 规则ID
|
|
479
|
+
* @returns 是否进行了重置
|
|
480
|
+
*/
|
|
481
|
+
checkAndResetRuleIfNeeded(ruleId) {
|
|
482
|
+
const rule = this.db
|
|
483
|
+
.prepare('SELECT reset_interval, last_reset_at FROM rules WHERE id = ?')
|
|
484
|
+
.get(ruleId);
|
|
485
|
+
if (!rule || !rule.reset_interval) {
|
|
486
|
+
return false; // 没有设置重置间隔
|
|
487
|
+
}
|
|
488
|
+
const now = Date.now();
|
|
489
|
+
const resetIntervalMs = rule.reset_interval * 60 * 60 * 1000; // 小时转毫秒
|
|
490
|
+
const lastResetAt = rule.last_reset_at || 0;
|
|
491
|
+
// 检查是否已经到了重置时间
|
|
492
|
+
if (now - lastResetAt >= resetIntervalMs) {
|
|
493
|
+
this.resetRuleTokenUsage(ruleId);
|
|
494
|
+
return true;
|
|
495
|
+
}
|
|
496
|
+
return false;
|
|
497
|
+
}
|
|
293
498
|
// Log operations
|
|
294
499
|
addLog(log) {
|
|
295
500
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -544,8 +749,8 @@ class DatabaseManager {
|
|
|
544
749
|
// Import API services
|
|
545
750
|
for (const service of importData.apiServices) {
|
|
546
751
|
this.db
|
|
547
|
-
.prepare('INSERT INTO api_services (id, vendor_id, name, api_url, api_key,
|
|
548
|
-
.run(service.id, service.vendorId, service.name, service.apiUrl, service.apiKey, service.
|
|
752
|
+
.prepare('INSERT INTO api_services (id, vendor_id, name, api_url, api_key, source_type, supported_models, model_limits, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
|
|
753
|
+
.run(service.id, service.vendorId, service.name, service.apiUrl, service.apiKey, service.sourceType || null, service.supportedModels ? service.supportedModels.join(',') : null, service.modelLimits ? JSON.stringify(service.modelLimits) : null, service.createdAt, service.updatedAt);
|
|
549
754
|
}
|
|
550
755
|
// Import routes
|
|
551
756
|
for (const route of importData.routes) {
|
|
@@ -556,8 +761,8 @@ class DatabaseManager {
|
|
|
556
761
|
// Import rules
|
|
557
762
|
for (const rule of importData.rules) {
|
|
558
763
|
this.db
|
|
559
|
-
.prepare('INSERT INTO rules (id, route_id, content_type, target_service_id, target_model, replaced_model, sort_order, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)')
|
|
560
|
-
.run(rule.id, rule.routeId, rule.contentType || 'default', rule.targetServiceId, rule.targetModel || null, rule.replacedModel || null, rule.sortOrder || 0, rule.createdAt, rule.updatedAt);
|
|
764
|
+
.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, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
|
|
765
|
+
.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.createdAt, rule.updatedAt);
|
|
561
766
|
}
|
|
562
767
|
// Update config
|
|
563
768
|
this.updateConfig(importData.config);
|
package/dist/server/main.js
CHANGED
|
@@ -282,6 +282,7 @@ const registerRoutes = (dbManager, proxyServer) => {
|
|
|
282
282
|
app.post('/api/rules', (req, res) => res.json(dbManager.createRule(req.body)));
|
|
283
283
|
app.put('/api/rules/:id', (req, res) => res.json(dbManager.updateRule(req.params.id, req.body)));
|
|
284
284
|
app.delete('/api/rules/:id', (req, res) => res.json(dbManager.deleteRule(req.params.id)));
|
|
285
|
+
app.put('/api/rules/:id/reset-tokens', (req, res) => res.json(dbManager.resetRuleTokenUsage(req.params.id)));
|
|
285
286
|
app.get('/api/logs', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
286
287
|
const rawLimit = typeof req.query.limit === 'string' ? parseInt(req.query.limit, 10) : NaN;
|
|
287
288
|
const rawOffset = typeof req.query.offset === 'string' ? parseInt(req.query.offset, 10) : NaN;
|
|
@@ -457,7 +457,12 @@ class ProxyServer {
|
|
|
457
457
|
*/
|
|
458
458
|
getServiceById(serviceId) {
|
|
459
459
|
const allServices = this.dbManager.getAPIServices();
|
|
460
|
-
|
|
460
|
+
const service = allServices.find(s => s.id === serviceId);
|
|
461
|
+
// 调试日志: 记录获取的服务信息
|
|
462
|
+
if (process.env.NODE_ENV === 'development' && service) {
|
|
463
|
+
console.log(`[Proxy] getServiceById(${serviceId}): ${service.name} -> ${service.apiUrl}`);
|
|
464
|
+
}
|
|
465
|
+
return service;
|
|
461
466
|
}
|
|
462
467
|
findMatchingRule(routeId, req) {
|
|
463
468
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -471,32 +476,53 @@ class ProxyServer {
|
|
|
471
476
|
const modelMappingRules = rules.filter(rule => rule.contentType === 'model-mapping' &&
|
|
472
477
|
rule.replacedModel &&
|
|
473
478
|
requestModel.includes(rule.replacedModel));
|
|
474
|
-
//
|
|
479
|
+
// 过滤黑名单和token限制
|
|
475
480
|
for (const rule of modelMappingRules) {
|
|
476
481
|
const isBlacklisted = yield this.dbManager.isServiceBlacklisted(rule.targetServiceId, routeId, rule.contentType);
|
|
477
|
-
if (
|
|
478
|
-
|
|
482
|
+
if (isBlacklisted) {
|
|
483
|
+
continue;
|
|
479
484
|
}
|
|
485
|
+
// 检查并重置到期的规则
|
|
486
|
+
this.dbManager.checkAndResetRuleIfNeeded(rule.id);
|
|
487
|
+
// 检查token限制
|
|
488
|
+
if (rule.tokenLimit && rule.totalTokensUsed !== undefined && rule.totalTokensUsed >= rule.tokenLimit) {
|
|
489
|
+
continue; // 跳过超限规则
|
|
490
|
+
}
|
|
491
|
+
return rule;
|
|
480
492
|
}
|
|
481
493
|
}
|
|
482
494
|
// 2. 查找其他内容类型的规则
|
|
483
495
|
const contentType = this.determineContentType(req);
|
|
484
496
|
const contentTypeRules = rules.filter(rule => rule.contentType === contentType);
|
|
485
|
-
//
|
|
497
|
+
// 过滤黑名单和token限制
|
|
486
498
|
for (const rule of contentTypeRules) {
|
|
487
499
|
const isBlacklisted = yield this.dbManager.isServiceBlacklisted(rule.targetServiceId, routeId, contentType);
|
|
488
|
-
if (
|
|
489
|
-
|
|
500
|
+
if (isBlacklisted) {
|
|
501
|
+
continue;
|
|
490
502
|
}
|
|
503
|
+
// 检查并重置到期的规则
|
|
504
|
+
this.dbManager.checkAndResetRuleIfNeeded(rule.id);
|
|
505
|
+
// 检查token限制
|
|
506
|
+
if (rule.tokenLimit && rule.totalTokensUsed !== undefined && rule.totalTokensUsed >= rule.tokenLimit) {
|
|
507
|
+
continue; // 跳过超限规则
|
|
508
|
+
}
|
|
509
|
+
return rule;
|
|
491
510
|
}
|
|
492
511
|
// 3. 最后返回 default 规则
|
|
493
512
|
const defaultRules = rules.filter(rule => rule.contentType === 'default');
|
|
494
|
-
//
|
|
513
|
+
// 过滤黑名单和token限制
|
|
495
514
|
for (const rule of defaultRules) {
|
|
496
515
|
const isBlacklisted = yield this.dbManager.isServiceBlacklisted(rule.targetServiceId, routeId, 'default');
|
|
497
|
-
if (
|
|
498
|
-
|
|
516
|
+
if (isBlacklisted) {
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
// 检查并重置到期的规则
|
|
520
|
+
this.dbManager.checkAndResetRuleIfNeeded(rule.id);
|
|
521
|
+
// 检查token限制
|
|
522
|
+
if (rule.tokenLimit && rule.totalTokensUsed !== undefined && rule.totalTokensUsed >= rule.tokenLimit) {
|
|
523
|
+
continue; // 跳过超限规则
|
|
499
524
|
}
|
|
525
|
+
return rule;
|
|
500
526
|
}
|
|
501
527
|
return undefined;
|
|
502
528
|
});
|
|
@@ -522,6 +548,23 @@ class ProxyServer {
|
|
|
522
548
|
// 3. Default rules
|
|
523
549
|
const defaultRules = rules.filter(rule => rule.contentType === 'default');
|
|
524
550
|
candidates.push(...defaultRules);
|
|
551
|
+
// 4. 检查并重置到期的规则
|
|
552
|
+
candidates.forEach(rule => {
|
|
553
|
+
this.dbManager.checkAndResetRuleIfNeeded(rule.id);
|
|
554
|
+
});
|
|
555
|
+
// 5. 过滤掉超过token限制的规则(仅在有多个候选规则时)
|
|
556
|
+
if (candidates.length > 1) {
|
|
557
|
+
const filteredCandidates = candidates.filter(rule => {
|
|
558
|
+
if (rule.tokenLimit && rule.totalTokensUsed !== undefined) {
|
|
559
|
+
return rule.totalTokensUsed < rule.tokenLimit;
|
|
560
|
+
}
|
|
561
|
+
return true; // 没有设置限制的规则总是可用
|
|
562
|
+
});
|
|
563
|
+
// 如果过滤后还有规则,使用过滤后的结果
|
|
564
|
+
if (filteredCandidates.length > 0) {
|
|
565
|
+
return filteredCandidates;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
525
568
|
return candidates;
|
|
526
569
|
}
|
|
527
570
|
determineContentType(req) {
|
|
@@ -768,6 +811,77 @@ class ProxyServer {
|
|
|
768
811
|
isOpenAIChatSource(sourceType) {
|
|
769
812
|
return sourceType === 'openai-chat' || sourceType === 'deepseek-chat';
|
|
770
813
|
}
|
|
814
|
+
/**
|
|
815
|
+
* 判断模型是否应该使用 max_completion_tokens 字段
|
|
816
|
+
* GPT 的新模型(如 o1 系列)使用 max_completion_tokens
|
|
817
|
+
*/
|
|
818
|
+
shouldUseMaxCompletionTokens(model) {
|
|
819
|
+
if (!model)
|
|
820
|
+
return false;
|
|
821
|
+
const lowerModel = model.toLowerCase();
|
|
822
|
+
// o1 系列模型使用 max_completion_tokens
|
|
823
|
+
return lowerModel.includes('o1-') ||
|
|
824
|
+
lowerModel.startsWith('o1') ||
|
|
825
|
+
lowerModel.includes('gpt-4.1') ||
|
|
826
|
+
lowerModel.includes('gpt-4o') ||
|
|
827
|
+
lowerModel.startsWith('chatgpt-');
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* 获取 max tokens 字段的名称
|
|
831
|
+
*/
|
|
832
|
+
getMaxTokensFieldName(model) {
|
|
833
|
+
return this.shouldUseMaxCompletionTokens(model) ? 'max_completion_tokens' : 'max_tokens';
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* 应用 max_output_tokens 限制
|
|
837
|
+
* 根据服务的 modelLimits 配置,对具体模型应用 max_tokens/max_completion_tokens 限制
|
|
838
|
+
*/
|
|
839
|
+
applyMaxOutputTokensLimit(body, service) {
|
|
840
|
+
if (!service.modelLimits || !body || typeof body !== 'object') {
|
|
841
|
+
return body;
|
|
842
|
+
}
|
|
843
|
+
const result = Object.assign({}, body);
|
|
844
|
+
const model = result.model;
|
|
845
|
+
if (!model) {
|
|
846
|
+
return body;
|
|
847
|
+
}
|
|
848
|
+
// 查找该模型的限制配置
|
|
849
|
+
// 支持精确匹配和前缀匹配(例如:gpt-4 可以匹配 gpt-4-turbo)
|
|
850
|
+
let maxOutputLimit;
|
|
851
|
+
// 1. 先尝试精确匹配
|
|
852
|
+
if (typeof service.modelLimits[model] === 'number') {
|
|
853
|
+
maxOutputLimit = service.modelLimits[model];
|
|
854
|
+
}
|
|
855
|
+
else {
|
|
856
|
+
// 2. 尝试前缀匹配(查找配置中以模型名开头的项)
|
|
857
|
+
const matchedKey = Object.keys(service.modelLimits).find(key => model.startsWith(key) || key.startsWith(model));
|
|
858
|
+
if (matchedKey && typeof service.modelLimits[matchedKey] === 'number') {
|
|
859
|
+
maxOutputLimit = service.modelLimits[matchedKey];
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
if (maxOutputLimit === undefined) {
|
|
863
|
+
// 没有找到配置,直接透传
|
|
864
|
+
return body;
|
|
865
|
+
}
|
|
866
|
+
const maxTokensFieldName = this.getMaxTokensFieldName(model);
|
|
867
|
+
// 获取请求中的 max_tokens 或 max_completion_tokens 值
|
|
868
|
+
const requestedMaxTokens = result[maxTokensFieldName] || result.max_tokens;
|
|
869
|
+
// 如果请求中指定了 max_tokens,并且超过配置的限制,则限制为配置的最大值
|
|
870
|
+
if (typeof requestedMaxTokens === 'number' && requestedMaxTokens > maxOutputLimit) {
|
|
871
|
+
console.log(`[Proxy] Limiting ${maxTokensFieldName} from ${requestedMaxTokens} to ${maxOutputLimit} for model ${model} in service ${service.name}`);
|
|
872
|
+
result[maxTokensFieldName] = maxOutputLimit;
|
|
873
|
+
// 如果使用了 max_completion_tokens,清理旧的 max_tokens 字段
|
|
874
|
+
if (maxTokensFieldName === 'max_completion_tokens' && result.max_tokens !== undefined) {
|
|
875
|
+
delete result.max_tokens;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
else if (requestedMaxTokens === undefined) {
|
|
879
|
+
// 如果请求中没有指定 max_tokens,则使用配置的最大值
|
|
880
|
+
console.log(`[Proxy] Setting ${maxTokensFieldName} to ${maxOutputLimit} for model ${model} in service ${service.name}`);
|
|
881
|
+
result[maxTokensFieldName] = maxOutputLimit;
|
|
882
|
+
}
|
|
883
|
+
return result;
|
|
884
|
+
}
|
|
771
885
|
applyModelOverride(body, rule) {
|
|
772
886
|
// 如果 targetModel 为空或不存在,保留原始 model(透传)
|
|
773
887
|
if (!rule.targetModel)
|
|
@@ -932,6 +1046,10 @@ class ProxyServer {
|
|
|
932
1046
|
var _a, _b;
|
|
933
1047
|
if (logged || !((_a = this.config) === null || _a === void 0 ? void 0 : _a.enableLogging))
|
|
934
1048
|
return;
|
|
1049
|
+
// 只记录来自编程工具的请求
|
|
1050
|
+
if (!SUPPORTED_TARGETS.some(target => req.path.startsWith(`/${target}/`))) {
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
935
1053
|
logged = true;
|
|
936
1054
|
// 获取供应商信息
|
|
937
1055
|
const vendors = this.dbManager.getVendors();
|
|
@@ -950,6 +1068,7 @@ class ProxyServer {
|
|
|
950
1068
|
usage: usageForLog,
|
|
951
1069
|
error,
|
|
952
1070
|
// 新增字段
|
|
1071
|
+
ruleId: rule.id,
|
|
953
1072
|
targetType,
|
|
954
1073
|
targetServiceId: service.id,
|
|
955
1074
|
targetServiceName: service.name,
|
|
@@ -962,6 +1081,13 @@ class ProxyServer {
|
|
|
962
1081
|
streamChunks: streamChunksForLog,
|
|
963
1082
|
upstreamRequest: upstreamRequestForLog,
|
|
964
1083
|
});
|
|
1084
|
+
// 更新规则的token使用量(只在成功请求时更新)
|
|
1085
|
+
if (usageForLog && statusCode < 400) {
|
|
1086
|
+
const totalTokens = (usageForLog.inputTokens || 0) + (usageForLog.outputTokens || 0);
|
|
1087
|
+
if (totalTokens > 0) {
|
|
1088
|
+
this.dbManager.incrementRuleTokenUsage(rule.id, totalTokens);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
965
1091
|
});
|
|
966
1092
|
try {
|
|
967
1093
|
if (targetType === 'claude-code') {
|
|
@@ -990,6 +1116,8 @@ class ProxyServer {
|
|
|
990
1116
|
return;
|
|
991
1117
|
}
|
|
992
1118
|
}
|
|
1119
|
+
// 应用 max_output_tokens 限制
|
|
1120
|
+
requestBody = this.applyMaxOutputTokensLimit(requestBody, service);
|
|
993
1121
|
const streamRequested = this.isStreamRequested(req, requestBody);
|
|
994
1122
|
// Build the full URL by appending the request path to the service API URL
|
|
995
1123
|
let pathToAppend = req.path;
|
|
@@ -1005,7 +1133,7 @@ class ProxyServer {
|
|
|
1005
1133
|
method: req.method,
|
|
1006
1134
|
url: `${service.apiUrl}${mappedPath}`,
|
|
1007
1135
|
headers: this.buildUpstreamHeaders(req, service, sourceType, streamRequested),
|
|
1008
|
-
timeout:
|
|
1136
|
+
timeout: rule.timeout || 3000000, // 默认300秒
|
|
1009
1137
|
validateStatus: () => true,
|
|
1010
1138
|
responseType: streamRequested ? 'stream' : 'json',
|
|
1011
1139
|
};
|
|
@@ -1016,9 +1144,14 @@ class ProxyServer {
|
|
|
1016
1144
|
config.data = requestBody;
|
|
1017
1145
|
}
|
|
1018
1146
|
// 记录实际发出的请求信息作为日志的一部分
|
|
1147
|
+
const actualModel = (requestBody === null || requestBody === void 0 ? void 0 : requestBody.model) || '';
|
|
1148
|
+
const maxTokensFieldName = this.getMaxTokensFieldName(actualModel);
|
|
1149
|
+
const actualMaxTokens = (requestBody === null || requestBody === void 0 ? void 0 : requestBody[maxTokensFieldName]) || (requestBody === null || requestBody === void 0 ? void 0 : requestBody.max_tokens);
|
|
1019
1150
|
upstreamRequestForLog = {
|
|
1020
1151
|
url: `${service.apiUrl}${mappedPath}`,
|
|
1021
|
-
model:
|
|
1152
|
+
model: actualModel,
|
|
1153
|
+
maxTokens: actualMaxTokens,
|
|
1154
|
+
maxTokensField: maxTokensFieldName,
|
|
1022
1155
|
};
|
|
1023
1156
|
const response = yield (0, axios_1.default)(config);
|
|
1024
1157
|
const responseHeaders = response.headers || {};
|