aicodeswitch 1.10.2 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2 -0
- package/bin/cli.js +7 -9
- package/bin/restart.js +7 -229
- package/bin/restore.js +1 -1
- package/bin/start.js +75 -87
- package/bin/stop.js +77 -14
- package/bin/ui.js +19 -134
- package/bin/update.js +1 -1
- package/bin/utils/get-server.js +58 -0
- package/bin/utils/port-utils.js +118 -0
- package/bin/version.js +1 -1
- package/dist/server/database.js +196 -116
- package/dist/server/main.js +116 -22
- package/dist/server/proxy-server.js +334 -158
- package/dist/server/transformers/claude-openai.js +86 -3
- package/dist/server/transformers/streaming.js +4 -1
- package/dist/server/utils.js +16 -0
- package/dist/ui/assets/index-BLqGemLn.js +423 -0
- package/dist/ui/assets/index-IVPeH7yC.css +1 -0
- package/dist/ui/index.html +2 -2
- package/dist/ui/migration.md +7 -0
- package/package.json +3 -2
- package/public/migration.md +7 -0
- package/dist/ui/assets/index-D6RrKKB5.js +0 -391
- package/dist/ui/assets/index-DU8EG0kT.css +0 -1
|
@@ -48,22 +48,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
48
48
|
exports.ProxyServer = void 0;
|
|
49
49
|
const axios_1 = __importDefault(require("axios"));
|
|
50
50
|
const stream_1 = require("stream");
|
|
51
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
51
52
|
const streaming_1 = require("./transformers/streaming");
|
|
52
53
|
const chunk_collector_1 = require("./transformers/chunk-collector");
|
|
53
54
|
const claude_openai_1 = require("./transformers/claude-openai");
|
|
54
55
|
const SUPPORTED_TARGETS = ['claude-code', 'codex'];
|
|
55
|
-
// 需要排除的路径模式(非业务请求)
|
|
56
|
-
const IGNORED_PATHS = [
|
|
57
|
-
'/favicon.ico',
|
|
58
|
-
'/robots.txt',
|
|
59
|
-
'/sitemap.xml',
|
|
60
|
-
];
|
|
61
|
-
/**
|
|
62
|
-
* 检查路径是否应该被忽略
|
|
63
|
-
*/
|
|
64
|
-
function shouldIgnorePath(path) {
|
|
65
|
-
return IGNORED_PATHS.some(ignored => path === ignored || path.startsWith(ignored + '?'));
|
|
66
|
-
}
|
|
67
56
|
class ProxyServer {
|
|
68
57
|
constructor(dbManager, app) {
|
|
69
58
|
Object.defineProperty(this, "app", {
|
|
@@ -104,125 +93,30 @@ class ProxyServer {
|
|
|
104
93
|
writable: true,
|
|
105
94
|
value: void 0
|
|
106
95
|
});
|
|
96
|
+
// 请求去重缓存:用于防止同一个请求被重复计数(如网络重试)
|
|
97
|
+
// key: requestHash, value: timestamp
|
|
98
|
+
Object.defineProperty(this, "requestDedupeCache", {
|
|
99
|
+
enumerable: true,
|
|
100
|
+
configurable: true,
|
|
101
|
+
writable: true,
|
|
102
|
+
value: new Map()
|
|
103
|
+
});
|
|
104
|
+
Object.defineProperty(this, "DEDUPE_CACHE_TTL", {
|
|
105
|
+
enumerable: true,
|
|
106
|
+
configurable: true,
|
|
107
|
+
writable: true,
|
|
108
|
+
value: 60000
|
|
109
|
+
}); // 去重缓存1分钟过期
|
|
107
110
|
this.dbManager = dbManager;
|
|
108
111
|
this.config = dbManager.getConfig();
|
|
109
112
|
this.app = app;
|
|
110
113
|
}
|
|
111
|
-
|
|
112
|
-
// Access logging middleware
|
|
113
|
-
this.app.use((req, res, next) => __awaiter(this, void 0, void 0, function* () {
|
|
114
|
-
var _a;
|
|
115
|
-
// 忽略非业务请求
|
|
116
|
-
if (shouldIgnorePath(req.path)) {
|
|
117
|
-
return next();
|
|
118
|
-
}
|
|
119
|
-
// Capture client info
|
|
120
|
-
const clientIp = ((_a = req.headers['x-forwarded-for']) === null || _a === void 0 ? void 0 : _a.split(',')[0]) || req.socket.remoteAddress || '';
|
|
121
|
-
const userAgent = req.headers['user-agent'] || '';
|
|
122
|
-
const startTime = Date.now();
|
|
123
|
-
const originalSend = res.send.bind(res);
|
|
124
|
-
const originalJson = res.json.bind(res);
|
|
125
|
-
const accessLog = this.dbManager.addAccessLog({
|
|
126
|
-
timestamp: Date.now(),
|
|
127
|
-
method: req.method,
|
|
128
|
-
path: req.path,
|
|
129
|
-
clientIp,
|
|
130
|
-
userAgent,
|
|
131
|
-
});
|
|
132
|
-
const updateLog = (res, data) => {
|
|
133
|
-
const responseTime = Date.now() - startTime;
|
|
134
|
-
accessLog.then((accessLogId) => {
|
|
135
|
-
const updateData = {
|
|
136
|
-
responseTime,
|
|
137
|
-
};
|
|
138
|
-
let errorMessage = '';
|
|
139
|
-
if (res instanceof Error) {
|
|
140
|
-
updateData.error = res.message;
|
|
141
|
-
errorMessage = res.message;
|
|
142
|
-
}
|
|
143
|
-
else if (res.statusCode >= 400) {
|
|
144
|
-
updateData.statusCode = res.statusCode;
|
|
145
|
-
updateData.error = res.statusMessage;
|
|
146
|
-
// @ts-ignore
|
|
147
|
-
updateData.responseHeaders = this.normalizeResponseHeaders(res.headers || {});
|
|
148
|
-
errorMessage = res.statusMessage;
|
|
149
|
-
}
|
|
150
|
-
else {
|
|
151
|
-
updateData.statusCode = res.statusCode;
|
|
152
|
-
// @ts-ignore
|
|
153
|
-
updateData.responseHeaders = this.normalizeResponseHeaders(res.headers || {});
|
|
154
|
-
updateData.responseBody = data ? JSON.stringify(data) : undefined;
|
|
155
|
-
}
|
|
156
|
-
this.dbManager.updateAccessLog(accessLogId, updateData);
|
|
157
|
-
// 记录错误日志
|
|
158
|
-
if (res instanceof Error || res.statusCode >= 400) {
|
|
159
|
-
this.dbManager.addErrorLog({
|
|
160
|
-
timestamp: Date.now(),
|
|
161
|
-
method: req.method,
|
|
162
|
-
path: req.path,
|
|
163
|
-
statusCode: 503,
|
|
164
|
-
errorMessage,
|
|
165
|
-
requestHeaders: this.normalizeHeaders(req.headers),
|
|
166
|
-
requestBody: req.body ? JSON.stringify(req.body) : undefined,
|
|
167
|
-
responseBody: data ? JSON.stringify(data) : undefined,
|
|
168
|
-
// @ts-ignore
|
|
169
|
-
responseHeaders: res.headers ? this.normalizeResponseHeaders(res.headers) : undefined,
|
|
170
|
-
responseTime,
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
});
|
|
174
|
-
};
|
|
175
|
-
res.send = (data) => {
|
|
176
|
-
res.send = originalSend;
|
|
177
|
-
updateLog(res, data);
|
|
178
|
-
return originalSend(data);
|
|
179
|
-
};
|
|
180
|
-
res.json = (data) => {
|
|
181
|
-
res.json = originalJson;
|
|
182
|
-
updateLog(res, data);
|
|
183
|
-
return originalJson(data);
|
|
184
|
-
};
|
|
185
|
-
res.on('error', (err) => {
|
|
186
|
-
updateLog(err);
|
|
187
|
-
});
|
|
188
|
-
next();
|
|
189
|
-
}));
|
|
190
|
-
// Logging middleware (legacy RequestLog)
|
|
191
|
-
this.app.use((req, res, next) => __awaiter(this, void 0, void 0, function* () {
|
|
192
|
-
const startTime = Date.now();
|
|
193
|
-
const originalSend = res.send.bind(res);
|
|
194
|
-
// 忽略非业务请求,并且只记录支持的编程工具请求
|
|
195
|
-
if (!shouldIgnorePath(req.path) && SUPPORTED_TARGETS.some(target => req.path.startsWith(`/${target}/`))) {
|
|
196
|
-
res.send = (data) => {
|
|
197
|
-
var _a;
|
|
198
|
-
res.send = originalSend;
|
|
199
|
-
if (!res.locals.skipLog && ((_a = this.config) === null || _a === void 0 ? void 0 : _a.enableLogging) && SUPPORTED_TARGETS.some(target => req.path.startsWith(`/${target}/`))) {
|
|
200
|
-
const responseTime = Date.now() - startTime;
|
|
201
|
-
this.dbManager.addLog({
|
|
202
|
-
timestamp: Date.now(),
|
|
203
|
-
method: req.method,
|
|
204
|
-
path: req.path,
|
|
205
|
-
headers: this.normalizeHeaders(req.headers),
|
|
206
|
-
body: req.body ? JSON.stringify(req.body) : undefined,
|
|
207
|
-
statusCode: res.statusCode,
|
|
208
|
-
responseTime,
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
return res.send(data);
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
next();
|
|
215
|
-
}));
|
|
216
|
-
// Fixed route handlers
|
|
217
|
-
this.app.use('/claude-code/', this.createFixedRouteHandler('claude-code'));
|
|
218
|
-
this.app.use('/claude-code', this.createFixedRouteHandler('claude-code'));
|
|
219
|
-
this.app.use('/codex/', this.createFixedRouteHandler('codex'));
|
|
220
|
-
this.app.use('/codex', this.createFixedRouteHandler('codex'));
|
|
114
|
+
initialize() {
|
|
221
115
|
// Dynamic proxy middleware
|
|
222
116
|
this.app.use((req, res, next) => __awaiter(this, void 0, void 0, function* () {
|
|
223
117
|
var _a, _b, _c, _d, _e;
|
|
224
|
-
//
|
|
225
|
-
if (req.path
|
|
118
|
+
// 仅处理支持的目标路径
|
|
119
|
+
if (!SUPPORTED_TARGETS.some(target => req.path.startsWith(`/${target}/`))) {
|
|
226
120
|
return next();
|
|
227
121
|
}
|
|
228
122
|
try {
|
|
@@ -296,7 +190,7 @@ class ProxyServer {
|
|
|
296
190
|
// 所有服务都失败了
|
|
297
191
|
console.error('All services failed');
|
|
298
192
|
// 记录日志
|
|
299
|
-
if (((_d = this.config) === null || _d === void 0 ? void 0 : _d.enableLogging) && SUPPORTED_TARGETS.some(target => req.path.startsWith(`/${target}/`))) {
|
|
193
|
+
if (((_d = this.config) === null || _d === void 0 ? void 0 : _d.enableLogging) !== false && SUPPORTED_TARGETS.some(target => req.path.startsWith(`/${target}/`))) {
|
|
300
194
|
yield this.dbManager.addLog({
|
|
301
195
|
timestamp: Date.now(),
|
|
302
196
|
method: req.method,
|
|
@@ -338,7 +232,7 @@ class ProxyServer {
|
|
|
338
232
|
}
|
|
339
233
|
catch (error) {
|
|
340
234
|
console.error('Proxy error:', error);
|
|
341
|
-
if (((_e = this.config) === null || _e === void 0 ? void 0 : _e.enableLogging) && SUPPORTED_TARGETS.some(target => req.path.startsWith(`/${target}/`))) {
|
|
235
|
+
if (((_e = this.config) === null || _e === void 0 ? void 0 : _e.enableLogging) !== false && SUPPORTED_TARGETS.some(target => req.path.startsWith(`/${target}/`))) {
|
|
342
236
|
yield this.dbManager.addLog({
|
|
343
237
|
timestamp: Date.now(),
|
|
344
238
|
method: req.method,
|
|
@@ -377,6 +271,13 @@ class ProxyServer {
|
|
|
377
271
|
}
|
|
378
272
|
}));
|
|
379
273
|
}
|
|
274
|
+
addProxyRoutes() {
|
|
275
|
+
// Fixed route handlers
|
|
276
|
+
this.app.use('/claude-code/', this.createFixedRouteHandler('claude-code'));
|
|
277
|
+
this.app.use('/claude-code', this.createFixedRouteHandler('claude-code'));
|
|
278
|
+
this.app.use('/codex/', this.createFixedRouteHandler('codex'));
|
|
279
|
+
this.app.use('/codex', this.createFixedRouteHandler('codex'));
|
|
280
|
+
}
|
|
380
281
|
createFixedRouteHandler(targetType) {
|
|
381
282
|
return (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
382
283
|
var _a, _b, _c, _d, _e;
|
|
@@ -459,7 +360,7 @@ class ProxyServer {
|
|
|
459
360
|
// 所有服务都失败了
|
|
460
361
|
console.error('All services failed');
|
|
461
362
|
// 记录日志
|
|
462
|
-
if (((_d = this.config) === null || _d === void 0 ? void 0 : _d.enableLogging) && SUPPORTED_TARGETS.some(target => req.path.startsWith(`/${target}/`))) {
|
|
363
|
+
if (((_d = this.config) === null || _d === void 0 ? void 0 : _d.enableLogging) !== false && SUPPORTED_TARGETS.some(target => req.path.startsWith(`/${target}/`))) {
|
|
463
364
|
yield this.dbManager.addLog({
|
|
464
365
|
timestamp: Date.now(),
|
|
465
366
|
method: req.method,
|
|
@@ -501,7 +402,7 @@ class ProxyServer {
|
|
|
501
402
|
}
|
|
502
403
|
catch (error) {
|
|
503
404
|
console.error(`Fixed route error for ${targetType}:`, error);
|
|
504
|
-
if (((_e = this.config) === null || _e === void 0 ? void 0 : _e.enableLogging) && SUPPORTED_TARGETS.some(target => req.path.startsWith(`/${target}/`))) {
|
|
405
|
+
if (((_e = this.config) === null || _e === void 0 ? void 0 : _e.enableLogging) !== false && SUPPORTED_TARGETS.some(target => req.path.startsWith(`/${target}/`))) {
|
|
505
406
|
yield this.dbManager.addLog({
|
|
506
407
|
timestamp: Date.now(),
|
|
507
408
|
method: req.method,
|
|
@@ -542,24 +443,115 @@ class ProxyServer {
|
|
|
542
443
|
const routeRules = this.dbManager.getRules(routeId);
|
|
543
444
|
return routeRules.sort((a, b) => (b.sortOrder || 0) - (a.sortOrder || 0));
|
|
544
445
|
}
|
|
545
|
-
findMatchingRoute(
|
|
546
|
-
//
|
|
547
|
-
|
|
446
|
+
findMatchingRoute(req) {
|
|
447
|
+
// 根据请求路径确定目标类型
|
|
448
|
+
let targetType;
|
|
449
|
+
if (req.path.startsWith('/claude-code/')) {
|
|
450
|
+
targetType = 'claude-code';
|
|
451
|
+
}
|
|
452
|
+
else if (req.path.startsWith('/codex/')) {
|
|
453
|
+
targetType = 'codex';
|
|
454
|
+
}
|
|
455
|
+
if (!targetType) {
|
|
456
|
+
return undefined;
|
|
457
|
+
}
|
|
458
|
+
// 返回匹配目标类型且处于活跃状态的路由
|
|
548
459
|
const activeRoutes = this.getActiveRoutes();
|
|
549
|
-
return activeRoutes.find(route => route.isActive);
|
|
460
|
+
return activeRoutes.find(route => route.targetType === targetType && route.isActive);
|
|
550
461
|
}
|
|
551
462
|
findRouteByTargetType(targetType) {
|
|
552
463
|
const activeRoutes = this.getActiveRoutes();
|
|
553
464
|
return activeRoutes.find(route => route.targetType === targetType && route.isActive);
|
|
554
465
|
}
|
|
466
|
+
/**
|
|
467
|
+
* 计算请求内容的哈希值,用于去重
|
|
468
|
+
* 基于请求的关键字段生成唯一标识
|
|
469
|
+
*/
|
|
470
|
+
computeRequestHash(req) {
|
|
471
|
+
const body = req.body;
|
|
472
|
+
if (!body)
|
|
473
|
+
return null;
|
|
474
|
+
// 提取关键信息用于哈希
|
|
475
|
+
const messages = body.messages;
|
|
476
|
+
if (!messages || !Array.isArray(messages) || messages.length === 0) {
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
// 只使用最后几条消息的内容来生成哈希(避免整个历史过长)
|
|
480
|
+
const lastMessages = messages.slice(-3).map((msg) => ({
|
|
481
|
+
role: msg.role,
|
|
482
|
+
// 对消息内容进行简化处理,避免token差异导致哈希不同
|
|
483
|
+
content: this.normalizeMessageContent(msg.content)
|
|
484
|
+
}));
|
|
485
|
+
// 包含其他可能影响计费的字段
|
|
486
|
+
const keyFields = {
|
|
487
|
+
messages: lastMessages,
|
|
488
|
+
model: body.model,
|
|
489
|
+
stream: body.stream
|
|
490
|
+
};
|
|
491
|
+
return crypto_1.default.createHash('md5').update(JSON.stringify(keyFields)).digest('hex');
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* 规范化消息内容,去除细微差异
|
|
495
|
+
*/
|
|
496
|
+
normalizeMessageContent(content) {
|
|
497
|
+
if (typeof content === 'string') {
|
|
498
|
+
// 去除首尾空白,限制长度
|
|
499
|
+
return content.trim().slice(0, 500);
|
|
500
|
+
}
|
|
501
|
+
if (Array.isArray(content)) {
|
|
502
|
+
// 对于数组类型内容(如图片+文本),只提取文本部分
|
|
503
|
+
const textParts = content
|
|
504
|
+
.filter((item) => (item === null || item === void 0 ? void 0 : item.type) === 'text')
|
|
505
|
+
.map((item) => { var _a; return ((_a = item.text) === null || _a === void 0 ? void 0 : _a.trim().slice(0, 500)) || ''; })
|
|
506
|
+
.join('|');
|
|
507
|
+
return textParts;
|
|
508
|
+
}
|
|
509
|
+
return String(content || '').slice(0, 500);
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* 检查请求是否已经被处理过(去重)
|
|
513
|
+
*/
|
|
514
|
+
isRequestProcessed(hash) {
|
|
515
|
+
if (!hash)
|
|
516
|
+
return false;
|
|
517
|
+
const timestamp = this.requestDedupeCache.get(hash);
|
|
518
|
+
if (timestamp === undefined) {
|
|
519
|
+
// 未处理过,记录并返回false
|
|
520
|
+
this.requestDedupeCache.set(hash, Date.now());
|
|
521
|
+
return false;
|
|
522
|
+
}
|
|
523
|
+
// 检查是否过期
|
|
524
|
+
const now = Date.now();
|
|
525
|
+
if (now - timestamp > this.DEDUPE_CACHE_TTL) {
|
|
526
|
+
// 缓存已过期,视为新请求
|
|
527
|
+
this.requestDedupeCache.set(hash, now);
|
|
528
|
+
return false;
|
|
529
|
+
}
|
|
530
|
+
// 在缓存期内,视为重复请求
|
|
531
|
+
return true;
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* 清理过期的去重缓存
|
|
535
|
+
*/
|
|
536
|
+
cleanExpiredDedupeCache() {
|
|
537
|
+
const now = Date.now();
|
|
538
|
+
for (const [hash, timestamp] of this.requestDedupeCache.entries()) {
|
|
539
|
+
if (now - timestamp > this.DEDUPE_CACHE_TTL) {
|
|
540
|
+
this.requestDedupeCache.delete(hash);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
555
544
|
/**
|
|
556
545
|
* 根据GLM计费逻辑判断请求是否应该计费
|
|
557
546
|
* 核心规则:
|
|
558
547
|
* 1. 最后一条消息必须是 role: "user"
|
|
559
548
|
* 2. 上一条消息不能是包含 tool_calls 的 assistant 消息(即不是工具回传)
|
|
549
|
+
* 3. 上一条消息应该是 assistant(正常的对话流程),而非连续的 user 消息
|
|
550
|
+
* 4. 避免历史消息重复计数:检查消息序列是否符合正常的对话模式
|
|
560
551
|
*/
|
|
561
|
-
shouldChargeRequest(
|
|
562
|
-
const
|
|
552
|
+
shouldChargeRequest(req) {
|
|
553
|
+
const body = req.body;
|
|
554
|
+
const messages = body === null || body === void 0 ? void 0 : body.messages;
|
|
563
555
|
if (!messages || !Array.isArray(messages) || messages.length === 0) {
|
|
564
556
|
return false;
|
|
565
557
|
}
|
|
@@ -567,13 +559,54 @@ class ProxyServer {
|
|
|
567
559
|
if (lastMessage.role !== 'user') {
|
|
568
560
|
return false;
|
|
569
561
|
}
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
562
|
+
// 规则1:只有一条消息,这是新会话的开始,应该计费
|
|
563
|
+
if (messages.length === 1) {
|
|
564
|
+
return true;
|
|
565
|
+
}
|
|
566
|
+
const previousMessage = messages[messages.length - 2];
|
|
567
|
+
// 规则2:上一条消息是 assistant 且包含 tool_calls,说明这是工具回调,不应计费
|
|
568
|
+
if (previousMessage.role === 'assistant' && previousMessage.tool_calls) {
|
|
569
|
+
return false;
|
|
570
|
+
}
|
|
571
|
+
// 规则3:上一条消息不是 user,说明是正常的 user->assistant->user 流程,应该计费
|
|
572
|
+
if (previousMessage.role !== 'user') {
|
|
573
|
+
return true;
|
|
574
|
+
}
|
|
575
|
+
// 规则4:上一条消息也是 user(连续的 user 消息)
|
|
576
|
+
// 这种情况下需要进一步判断:
|
|
577
|
+
// - 如果倒数第三条是 assistant,可能是用户连续发送的消息,只计最后一条
|
|
578
|
+
// - 检查两条 user 消息的内容是否相同,相同则可能是历史重放
|
|
579
|
+
if (messages.length >= 3) {
|
|
580
|
+
const thirdLastMessage = messages[messages.length - 3];
|
|
581
|
+
// 正常的对话流程: ... assistant, user, user
|
|
582
|
+
// 这种情况说明最后一条 user 消息是在 assistant 之后的新消息,应该计费
|
|
583
|
+
if (thirdLastMessage.role === 'assistant') {
|
|
584
|
+
return true;
|
|
574
585
|
}
|
|
575
586
|
}
|
|
576
|
-
|
|
587
|
+
// 规则5:检查是否有连续的 user 消息内容相同(可能的重复)
|
|
588
|
+
const lastContent = this.normalizeMessageContent(lastMessage.content);
|
|
589
|
+
const prevContent = this.normalizeMessageContent(previousMessage.content);
|
|
590
|
+
if (lastContent === prevContent && lastContent.length > 0) {
|
|
591
|
+
// 两条连续的 user 消息内容相同,可能是重复,不应计费
|
|
592
|
+
return false;
|
|
593
|
+
}
|
|
594
|
+
// 规则6:如果上一条是 user,但没有 assistant 在中间,可能是异常的对话流
|
|
595
|
+
// 为了安全起见,检查再往前是否有 assistant
|
|
596
|
+
for (let i = messages.length - 3; i >= 0; i--) {
|
|
597
|
+
const msg = messages[i];
|
|
598
|
+
if (msg.role === 'assistant') {
|
|
599
|
+
// 找到了最近的 assistant,说明当前是新的对话轮次,应该计费
|
|
600
|
+
return true;
|
|
601
|
+
}
|
|
602
|
+
if (msg.role === 'user') {
|
|
603
|
+
// 还是 user 消息,继续往前找
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
// 其他 role(如 system),继续
|
|
607
|
+
}
|
|
608
|
+
// 没有找到 assistant,可能是异常情况,不计费
|
|
609
|
+
return false;
|
|
577
610
|
}
|
|
578
611
|
/**
|
|
579
612
|
* 从数据库实时获取服务配置
|
|
@@ -955,11 +988,15 @@ class ProxyServer {
|
|
|
955
988
|
addText(body === null || body === void 0 ? void 0 : body.prompt);
|
|
956
989
|
return length;
|
|
957
990
|
}
|
|
991
|
+
/** 判断是否为 Claude 相关类型(使用 x-api-key 认证) */
|
|
958
992
|
isClaudeSource(sourceType) {
|
|
959
|
-
return sourceType === 'claude-chat';
|
|
993
|
+
return sourceType === 'claude-chat' || sourceType === 'claude-code';
|
|
960
994
|
}
|
|
961
995
|
isOpenAIChatSource(sourceType) {
|
|
962
|
-
return sourceType === 'openai-chat' || sourceType === 'deepseek-chat';
|
|
996
|
+
return sourceType === 'openai-chat' || sourceType === 'openai-responses' || sourceType === 'deepseek-reasoning-chat';
|
|
997
|
+
}
|
|
998
|
+
isChatType(sourceType) {
|
|
999
|
+
return sourceType.endsWith('-chat');
|
|
963
1000
|
}
|
|
964
1001
|
/**
|
|
965
1002
|
* 判断模型是否应该使用 max_completion_tokens 字段
|
|
@@ -1018,7 +1055,7 @@ class ProxyServer {
|
|
|
1018
1055
|
const requestedMaxTokens = result[maxTokensFieldName] || result.max_tokens;
|
|
1019
1056
|
// 如果请求中指定了 max_tokens,并且超过配置的限制,则限制为配置的最大值
|
|
1020
1057
|
if (typeof requestedMaxTokens === 'number' && requestedMaxTokens > maxOutputLimit) {
|
|
1021
|
-
console.log(`[Proxy] Limiting ${maxTokensFieldName} from ${requestedMaxTokens} to ${maxOutputLimit} for model ${model} in service ${service.name}`);
|
|
1058
|
+
// console.log(`[Proxy] Limiting ${maxTokensFieldName} from ${requestedMaxTokens} to ${maxOutputLimit} for model ${model} in service ${service.name}`);
|
|
1022
1059
|
result[maxTokensFieldName] = maxOutputLimit;
|
|
1023
1060
|
// 如果使用了 max_completion_tokens,清理旧的 max_tokens 字段
|
|
1024
1061
|
if (maxTokensFieldName === 'max_completion_tokens' && result.max_tokens !== undefined) {
|
|
@@ -1027,7 +1064,7 @@ class ProxyServer {
|
|
|
1027
1064
|
}
|
|
1028
1065
|
else if (requestedMaxTokens === undefined) {
|
|
1029
1066
|
// 如果请求中没有指定 max_tokens,则使用配置的最大值
|
|
1030
|
-
console.log(`[Proxy] Setting ${maxTokensFieldName} to ${maxOutputLimit} for model ${model} in service ${service.name}`);
|
|
1067
|
+
// console.log(`[Proxy] Setting ${maxTokensFieldName} to ${maxOutputLimit} for model ${model} in service ${service.name}`);
|
|
1031
1068
|
result[maxTokensFieldName] = maxOutputLimit;
|
|
1032
1069
|
}
|
|
1033
1070
|
return result;
|
|
@@ -1061,11 +1098,19 @@ class ProxyServer {
|
|
|
1061
1098
|
if (streamRequested) {
|
|
1062
1099
|
headers.accept = 'text/event-stream';
|
|
1063
1100
|
}
|
|
1064
|
-
|
|
1101
|
+
// 确定认证方式:优先使用服务配置的 authType,否则根据 sourceType 自动判断
|
|
1102
|
+
const authType = service.authType || 'auto';
|
|
1103
|
+
const useXApiKey = authType === 'x-api-key' || (authType === 'auto' && this.isClaudeSource(sourceType));
|
|
1104
|
+
if (useXApiKey) {
|
|
1105
|
+
// 使用 x-api-key 认证(适用于 claude-chat, claude-code 及某些需要 x-api-key 的 openai-chat 兼容 API)
|
|
1065
1106
|
headers['x-api-key'] = service.apiKey;
|
|
1066
|
-
|
|
1107
|
+
if (this.isClaudeSource(sourceType) || authType === 'x-api-key') {
|
|
1108
|
+
// 仅在明确配置或 Claude 源时添加 anthropic-version
|
|
1109
|
+
headers['anthropic-version'] = headers['anthropic-version'] || '2023-06-01';
|
|
1110
|
+
}
|
|
1067
1111
|
}
|
|
1068
1112
|
else {
|
|
1113
|
+
// 使用 Authorization 认证(适用于 openai-chat, openai-responses, deepseek-reasoning-chat 等)
|
|
1069
1114
|
delete headers['anthropic-version'];
|
|
1070
1115
|
delete headers['anthropic-beta'];
|
|
1071
1116
|
headers.authorization = `Bearer ${service.apiKey}`;
|
|
@@ -1142,6 +1187,62 @@ class ProxyServer {
|
|
|
1142
1187
|
}
|
|
1143
1188
|
return undefined;
|
|
1144
1189
|
}
|
|
1190
|
+
/**
|
|
1191
|
+
* 从请求中提取 session ID(默认方法)
|
|
1192
|
+
* Claude Code: metadata.user_id
|
|
1193
|
+
* Codex: headers.session_id
|
|
1194
|
+
*/
|
|
1195
|
+
defaultExtractSessionId(request, type) {
|
|
1196
|
+
var _a, _b;
|
|
1197
|
+
if (type === 'claude-code') {
|
|
1198
|
+
// Claude Code 使用 metadata.user_id
|
|
1199
|
+
return ((_b = (_a = request.body) === null || _a === void 0 ? void 0 : _a.metadata) === null || _b === void 0 ? void 0 : _b.user_id) || null;
|
|
1200
|
+
}
|
|
1201
|
+
else if (type === 'codex') {
|
|
1202
|
+
// Codex 使用 headers.session_id
|
|
1203
|
+
const sessionId = request.headers['session_id'];
|
|
1204
|
+
if (typeof sessionId === 'string') {
|
|
1205
|
+
return sessionId;
|
|
1206
|
+
}
|
|
1207
|
+
if (Array.isArray(sessionId)) {
|
|
1208
|
+
return sessionId[0] || null;
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
return null;
|
|
1212
|
+
}
|
|
1213
|
+
/**
|
|
1214
|
+
* 提取会话标题(默认方法)
|
|
1215
|
+
* 对于新会话,尝试从第一条消息的内容中提取标题
|
|
1216
|
+
*/
|
|
1217
|
+
defaultExtractSessionTitle(request, sessionId) {
|
|
1218
|
+
var _a;
|
|
1219
|
+
const existingSession = this.dbManager.getSession(sessionId);
|
|
1220
|
+
if (existingSession) {
|
|
1221
|
+
// 已存在的会话,保持原有标题
|
|
1222
|
+
return existingSession.title;
|
|
1223
|
+
}
|
|
1224
|
+
// 新会话,从消息内容提取标题
|
|
1225
|
+
const messages = (_a = request.body) === null || _a === void 0 ? void 0 : _a.messages;
|
|
1226
|
+
if (Array.isArray(messages) && messages.length > 0) {
|
|
1227
|
+
// 查找第一条 user 消息
|
|
1228
|
+
const firstUserMessage = messages.find((msg) => msg.role === 'user');
|
|
1229
|
+
if (firstUserMessage) {
|
|
1230
|
+
const content = firstUserMessage.content;
|
|
1231
|
+
if (typeof content === 'string') {
|
|
1232
|
+
// 截取前50个字符作为标题
|
|
1233
|
+
return content.slice(0, 50).trim();
|
|
1234
|
+
}
|
|
1235
|
+
else if (Array.isArray(content)) {
|
|
1236
|
+
// 处理结构化内容(如图片+文本)
|
|
1237
|
+
const textBlock = content.find((block) => (block === null || block === void 0 ? void 0 : block.type) === 'text');
|
|
1238
|
+
if (textBlock === null || textBlock === void 0 ? void 0 : textBlock.text) {
|
|
1239
|
+
return textBlock.text.slice(0, 50).trim();
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
return undefined;
|
|
1245
|
+
}
|
|
1145
1246
|
/**
|
|
1146
1247
|
* 根据源工具类型和目标API类型,映射请求路径
|
|
1147
1248
|
* @param sourceTool 源工具类型 (claude-code 或 codex)
|
|
@@ -1195,8 +1296,13 @@ class ProxyServer {
|
|
|
1195
1296
|
let actuallyUsedProxy = false; // 标记是否实际使用了代理
|
|
1196
1297
|
const finalizeLog = (statusCode, error) => __awaiter(this, void 0, void 0, function* () {
|
|
1197
1298
|
var _a, _b;
|
|
1198
|
-
if (logged
|
|
1299
|
+
if (logged)
|
|
1300
|
+
return;
|
|
1301
|
+
// 检查是否启用日志记录(默认启用)
|
|
1302
|
+
const enableLogging = ((_a = this.config) === null || _a === void 0 ? void 0 : _a.enableLogging) !== false; // 默认为 true
|
|
1303
|
+
if (!enableLogging) {
|
|
1199
1304
|
return;
|
|
1305
|
+
}
|
|
1200
1306
|
// 只记录来自编程工具的请求
|
|
1201
1307
|
if (!SUPPORTED_TARGETS.some(target => req.path.startsWith(`/${target}/`))) {
|
|
1202
1308
|
return;
|
|
@@ -1232,6 +1338,26 @@ class ProxyServer {
|
|
|
1232
1338
|
streamChunks: streamChunksForLog,
|
|
1233
1339
|
upstreamRequest: upstreamRequestForLog,
|
|
1234
1340
|
});
|
|
1341
|
+
// Session 索引逻辑
|
|
1342
|
+
const sessionId = this.defaultExtractSessionId(req, targetType);
|
|
1343
|
+
if (sessionId) {
|
|
1344
|
+
const totalTokens = ((usageForLog === null || usageForLog === void 0 ? void 0 : usageForLog.inputTokens) || 0) + ((usageForLog === null || usageForLog === void 0 ? void 0 : usageForLog.outputTokens) || 0) +
|
|
1345
|
+
((usageForLog === null || usageForLog === void 0 ? void 0 : usageForLog.totalTokens) || 0);
|
|
1346
|
+
const sessionTitle = this.defaultExtractSessionTitle(req, sessionId);
|
|
1347
|
+
this.dbManager.upsertSession({
|
|
1348
|
+
id: sessionId,
|
|
1349
|
+
targetType,
|
|
1350
|
+
title: sessionTitle,
|
|
1351
|
+
firstRequestAt: startTime,
|
|
1352
|
+
lastRequestAt: Date.now(),
|
|
1353
|
+
vendorId: service.vendorId,
|
|
1354
|
+
vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
|
|
1355
|
+
serviceId: service.id,
|
|
1356
|
+
serviceName: service.name,
|
|
1357
|
+
model: requestModel || rule.targetModel,
|
|
1358
|
+
totalTokens,
|
|
1359
|
+
});
|
|
1360
|
+
}
|
|
1235
1361
|
// 更新规则的token使用量(只在成功请求时更新)
|
|
1236
1362
|
if (usageForLog && statusCode < 400) {
|
|
1237
1363
|
const totalTokens = (usageForLog.inputTokens || 0) + (usageForLog.outputTokens || 0);
|
|
@@ -1240,8 +1366,17 @@ class ProxyServer {
|
|
|
1240
1366
|
}
|
|
1241
1367
|
}
|
|
1242
1368
|
// 更新规则的请求次数(只在成功请求时更新)
|
|
1243
|
-
if (statusCode < 400 && this.shouldChargeRequest(req
|
|
1244
|
-
|
|
1369
|
+
if (statusCode < 400 && this.shouldChargeRequest(req)) {
|
|
1370
|
+
// 计算请求哈希用于去重
|
|
1371
|
+
const requestHash = this.computeRequestHash(req);
|
|
1372
|
+
// 检查是否是重复请求(如网络重试)
|
|
1373
|
+
if (!this.isRequestProcessed(requestHash)) {
|
|
1374
|
+
this.dbManager.incrementRuleRequestCount(rule.id, 1);
|
|
1375
|
+
}
|
|
1376
|
+
// 定期清理过期缓存
|
|
1377
|
+
if (Math.random() < 0.01) { // 1%概率清理,避免每次都清理
|
|
1378
|
+
this.cleanExpiredDedupeCache();
|
|
1379
|
+
}
|
|
1245
1380
|
}
|
|
1246
1381
|
});
|
|
1247
1382
|
try {
|
|
@@ -1286,7 +1421,7 @@ class ProxyServer {
|
|
|
1286
1421
|
const mappedPath = this.mapRequestPath(route.targetType, sourceType, pathToAppend);
|
|
1287
1422
|
const config = {
|
|
1288
1423
|
method: req.method,
|
|
1289
|
-
url: `${service.apiUrl}${mappedPath}`,
|
|
1424
|
+
url: this.isChatType(sourceType) ? service.apiUrl : `${service.apiUrl}${mappedPath}`,
|
|
1290
1425
|
headers: this.buildUpstreamHeaders(req, service, sourceType, streamRequested),
|
|
1291
1426
|
timeout: rule.timeout || 3000000, // 默认300秒
|
|
1292
1427
|
validateStatus: () => true,
|
|
@@ -1328,13 +1463,16 @@ class ProxyServer {
|
|
|
1328
1463
|
}
|
|
1329
1464
|
}
|
|
1330
1465
|
// 记录实际发出的请求信息作为日志的一部分
|
|
1331
|
-
const actualModel =
|
|
1332
|
-
const maxTokensFieldName = this.getMaxTokensFieldName(actualModel);
|
|
1333
|
-
const actualMaxTokens =
|
|
1466
|
+
// const actualModel = requestBody?.model || '';
|
|
1467
|
+
// const maxTokensFieldName = this.getMaxTokensFieldName(actualModel);
|
|
1468
|
+
// const actualMaxTokens = requestBody?.[maxTokensFieldName] || requestBody?.max_tokens;
|
|
1469
|
+
const upstreamHeaders = this.buildUpstreamHeaders(req, service, sourceType, streamRequested);
|
|
1334
1470
|
upstreamRequestForLog = {
|
|
1335
|
-
url: `${service.apiUrl}${mappedPath}`,
|
|
1336
|
-
model: actualModel,
|
|
1337
|
-
[maxTokensFieldName]: actualMaxTokens,
|
|
1471
|
+
url: this.isChatType(sourceType) ? service.apiUrl : `${service.apiUrl}${mappedPath}`,
|
|
1472
|
+
// model: actualModel,
|
|
1473
|
+
// [maxTokensFieldName]: actualMaxTokens,
|
|
1474
|
+
headers: upstreamHeaders,
|
|
1475
|
+
body: requestBody || undefined,
|
|
1338
1476
|
};
|
|
1339
1477
|
if (actuallyUsedProxy) {
|
|
1340
1478
|
upstreamRequestForLog.useProxy = true;
|
|
@@ -1440,6 +1578,33 @@ class ProxyServer {
|
|
|
1440
1578
|
usageForLog = this.extractTokenUsage(responseData === null || responseData === void 0 ? void 0 : responseData.usage);
|
|
1441
1579
|
// 记录错误响应体
|
|
1442
1580
|
responseBodyForLog = typeof responseData === 'string' ? responseData : JSON.stringify(responseData);
|
|
1581
|
+
// 将 4xx/5xx 错误记录到错误日志
|
|
1582
|
+
// 确保 errorDetail 总是字符串类型
|
|
1583
|
+
let errorDetail;
|
|
1584
|
+
if (typeof (responseData === null || responseData === void 0 ? void 0 : responseData.error) === 'string') {
|
|
1585
|
+
errorDetail = responseData.error;
|
|
1586
|
+
}
|
|
1587
|
+
else if (typeof (responseData === null || responseData === void 0 ? void 0 : responseData.message) === 'string') {
|
|
1588
|
+
errorDetail = responseData.message;
|
|
1589
|
+
}
|
|
1590
|
+
else if (responseData === null || responseData === void 0 ? void 0 : responseData.error) {
|
|
1591
|
+
errorDetail = JSON.stringify(responseData.error);
|
|
1592
|
+
}
|
|
1593
|
+
else {
|
|
1594
|
+
errorDetail = JSON.stringify(responseData);
|
|
1595
|
+
}
|
|
1596
|
+
yield this.dbManager.addErrorLog({
|
|
1597
|
+
timestamp: Date.now(),
|
|
1598
|
+
method: req.method,
|
|
1599
|
+
path: req.path,
|
|
1600
|
+
statusCode: response.status,
|
|
1601
|
+
errorMessage: `Upstream API returned ${response.status}: ${errorDetail}`,
|
|
1602
|
+
errorStack: undefined,
|
|
1603
|
+
requestHeaders: this.normalizeHeaders(req.headers),
|
|
1604
|
+
requestBody: req.body ? JSON.stringify(req.body) : undefined,
|
|
1605
|
+
responseHeaders: responseHeadersForLog,
|
|
1606
|
+
responseBody: responseBodyForLog,
|
|
1607
|
+
});
|
|
1443
1608
|
this.copyResponseHeaders(responseHeaders, res);
|
|
1444
1609
|
if (contentType.includes('application/json')) {
|
|
1445
1610
|
res.status(response.status).json(responseData);
|
|
@@ -1486,7 +1651,18 @@ class ProxyServer {
|
|
|
1486
1651
|
const errorMessage = isTimeout
|
|
1487
1652
|
? 'Request timeout - the upstream API took too long to respond'
|
|
1488
1653
|
: (error.message || 'Internal server error');
|
|
1489
|
-
|
|
1654
|
+
// 将错误记录到错误日志
|
|
1655
|
+
yield this.dbManager.addErrorLog({
|
|
1656
|
+
timestamp: Date.now(),
|
|
1657
|
+
method: req.method,
|
|
1658
|
+
path: req.path,
|
|
1659
|
+
statusCode: isTimeout ? 504 : 500,
|
|
1660
|
+
errorMessage: errorMessage,
|
|
1661
|
+
errorStack: error.stack,
|
|
1662
|
+
requestHeaders: this.normalizeHeaders(req.headers),
|
|
1663
|
+
requestBody: req.body ? JSON.stringify(req.body) : undefined,
|
|
1664
|
+
});
|
|
1665
|
+
yield finalizeLog(isTimeout ? 504 : 500, errorMessage);
|
|
1490
1666
|
// 根据请求类型返回适当格式的错误响应
|
|
1491
1667
|
const streamRequested = this.isStreamRequested(req, req.body || {});
|
|
1492
1668
|
if (route.targetType === 'claude-code') {
|
|
@@ -1554,9 +1730,9 @@ class ProxyServer {
|
|
|
1554
1730
|
this.config = config;
|
|
1555
1731
|
});
|
|
1556
1732
|
}
|
|
1557
|
-
|
|
1733
|
+
registerProxyRoutes() {
|
|
1558
1734
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1559
|
-
this.
|
|
1735
|
+
this.addProxyRoutes();
|
|
1560
1736
|
yield this.reloadRoutes();
|
|
1561
1737
|
});
|
|
1562
1738
|
}
|