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.
@@ -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
- setupMiddleware() {
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(_req) {
546
- // Find active route based on targetType - for now, return the first active route
547
- // This can be extended later based on specific routing logic
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(requestBody) {
562
- const messages = requestBody === null || requestBody === void 0 ? void 0 : requestBody.messages;
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
- if (messages.length > 1) {
571
- const previousMessage = messages[messages.length - 2];
572
- if (previousMessage.role === 'assistant' && previousMessage.tool_calls) {
573
- return false;
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
- return true;
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
- if (this.isClaudeSource(sourceType)) {
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
- headers['anthropic-version'] = headers['anthropic-version'] || '2023-06-01';
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 || !((_a = this.config) === null || _a === void 0 ? void 0 : _a.enableLogging))
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.body)) {
1244
- this.dbManager.incrementRuleRequestCount(rule.id, 1);
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 = (requestBody === null || requestBody === void 0 ? void 0 : requestBody.model) || '';
1332
- const maxTokensFieldName = this.getMaxTokensFieldName(actualModel);
1333
- const actualMaxTokens = (requestBody === null || requestBody === void 0 ? void 0 : requestBody[maxTokensFieldName]) || (requestBody === null || requestBody === void 0 ? void 0 : requestBody.max_tokens);
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
- yield finalizeLog(500, errorMessage);
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
- initialize() {
1733
+ registerProxyRoutes() {
1558
1734
  return __awaiter(this, void 0, void 0, function* () {
1559
- this.setupMiddleware();
1735
+ this.addProxyRoutes();
1560
1736
  yield this.reloadRoutes();
1561
1737
  });
1562
1738
  }