aicodeswitch 1.10.1 → 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 +4 -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 +480 -129
- package/dist/server/main.js +164 -22
- package/dist/server/proxy-server.js +417 -120
- 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-BdKga7KO.js +0 -391
- package/dist/ui/assets/index-Dat4drax.css +0 -1
|
@@ -48,6 +48,7 @@ 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");
|
|
@@ -92,94 +93,30 @@ class ProxyServer {
|
|
|
92
93
|
writable: true,
|
|
93
94
|
value: void 0
|
|
94
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分钟过期
|
|
95
110
|
this.dbManager = dbManager;
|
|
96
111
|
this.config = dbManager.getConfig();
|
|
97
112
|
this.app = app;
|
|
98
113
|
}
|
|
99
|
-
|
|
100
|
-
// Access logging middleware
|
|
101
|
-
this.app.use((req, res, next) => __awaiter(this, void 0, void 0, function* () {
|
|
102
|
-
var _a;
|
|
103
|
-
// Capture client info
|
|
104
|
-
const clientIp = ((_a = req.headers['x-forwarded-for']) === null || _a === void 0 ? void 0 : _a.split(',')[0]) || req.socket.remoteAddress || '';
|
|
105
|
-
const userAgent = req.headers['user-agent'] || '';
|
|
106
|
-
const startTime = Date.now();
|
|
107
|
-
const originalSend = res.send.bind(res);
|
|
108
|
-
const originalJson = res.json.bind(res);
|
|
109
|
-
const accessLog = this.dbManager.addAccessLog({
|
|
110
|
-
timestamp: Date.now(),
|
|
111
|
-
method: req.method,
|
|
112
|
-
path: req.path,
|
|
113
|
-
clientIp,
|
|
114
|
-
userAgent,
|
|
115
|
-
});
|
|
116
|
-
res.send = (data) => {
|
|
117
|
-
res.send = originalSend;
|
|
118
|
-
const responseTime = Date.now() - startTime;
|
|
119
|
-
accessLog.then((accessLogId) => {
|
|
120
|
-
this.dbManager.updateAccessLog(accessLogId, {
|
|
121
|
-
responseTime,
|
|
122
|
-
statusCode: res.statusCode,
|
|
123
|
-
});
|
|
124
|
-
});
|
|
125
|
-
return originalSend(data);
|
|
126
|
-
};
|
|
127
|
-
res.json = (data) => {
|
|
128
|
-
res.json = originalJson;
|
|
129
|
-
const responseTime = Date.now() - startTime;
|
|
130
|
-
accessLog.then((accessLogId) => {
|
|
131
|
-
this.dbManager.updateAccessLog(accessLogId, {
|
|
132
|
-
responseTime,
|
|
133
|
-
statusCode: res.statusCode,
|
|
134
|
-
});
|
|
135
|
-
});
|
|
136
|
-
return originalJson(data);
|
|
137
|
-
};
|
|
138
|
-
res.on('error', (err) => {
|
|
139
|
-
accessLog.then((accessLogId) => {
|
|
140
|
-
this.dbManager.updateAccessLog(accessLogId, {
|
|
141
|
-
statusCode: res.statusCode,
|
|
142
|
-
error: err.message,
|
|
143
|
-
});
|
|
144
|
-
});
|
|
145
|
-
});
|
|
146
|
-
next();
|
|
147
|
-
}));
|
|
148
|
-
// Logging middleware (legacy RequestLog)
|
|
149
|
-
this.app.use((req, res, next) => __awaiter(this, void 0, void 0, function* () {
|
|
150
|
-
const startTime = Date.now();
|
|
151
|
-
const originalSend = res.send.bind(res);
|
|
152
|
-
if (SUPPORTED_TARGETS.some(target => req.path.startsWith(`/${target}/`))) {
|
|
153
|
-
res.send = (data) => {
|
|
154
|
-
var _a;
|
|
155
|
-
res.send = originalSend;
|
|
156
|
-
if (!res.locals.skipLog && ((_a = this.config) === null || _a === void 0 ? void 0 : _a.enableLogging) && SUPPORTED_TARGETS.some(target => req.path.startsWith(`/${target}/`))) {
|
|
157
|
-
const responseTime = Date.now() - startTime;
|
|
158
|
-
this.dbManager.addLog({
|
|
159
|
-
timestamp: Date.now(),
|
|
160
|
-
method: req.method,
|
|
161
|
-
path: req.path,
|
|
162
|
-
headers: this.normalizeHeaders(req.headers),
|
|
163
|
-
body: req.body ? JSON.stringify(req.body) : undefined,
|
|
164
|
-
statusCode: res.statusCode,
|
|
165
|
-
responseTime,
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
return res.send(data);
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
next();
|
|
172
|
-
}));
|
|
173
|
-
// Fixed route handlers
|
|
174
|
-
this.app.use('/claude-code/', this.createFixedRouteHandler('claude-code'));
|
|
175
|
-
this.app.use('/claude-code', this.createFixedRouteHandler('claude-code'));
|
|
176
|
-
this.app.use('/codex/', this.createFixedRouteHandler('codex'));
|
|
177
|
-
this.app.use('/codex', this.createFixedRouteHandler('codex'));
|
|
114
|
+
initialize() {
|
|
178
115
|
// Dynamic proxy middleware
|
|
179
116
|
this.app.use((req, res, next) => __awaiter(this, void 0, void 0, function* () {
|
|
180
|
-
var _a, _b, _c, _d;
|
|
181
|
-
//
|
|
182
|
-
if (req.path
|
|
117
|
+
var _a, _b, _c, _d, _e;
|
|
118
|
+
// 仅处理支持的目标路径
|
|
119
|
+
if (!SUPPORTED_TARGETS.some(target => req.path.startsWith(`/${target}/`))) {
|
|
183
120
|
return next();
|
|
184
121
|
}
|
|
185
122
|
try {
|
|
@@ -227,11 +164,24 @@ class ProxyServer {
|
|
|
227
164
|
catch (error) {
|
|
228
165
|
console.error(`Service ${service.name} failed:`, error.message);
|
|
229
166
|
lastError = error;
|
|
230
|
-
//
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
167
|
+
// 检测是否是 timeout 错误
|
|
168
|
+
const isTimeout = error.code === 'ECONNABORTED' ||
|
|
169
|
+
((_b = error.message) === null || _b === void 0 ? void 0 : _b.toLowerCase().includes('timeout')) ||
|
|
170
|
+
(error.errno && error.errno === 'ETIMEDOUT');
|
|
171
|
+
// 判断错误类型并加入黑名单
|
|
172
|
+
if (isTimeout) {
|
|
173
|
+
// Timeout错误,加入黑名单
|
|
174
|
+
yield this.dbManager.addToBlacklist(service.id, route.id, rule.contentType, 'Request timeout - the upstream API took too long to respond', undefined, // timeout没有HTTP状态码
|
|
175
|
+
'timeout');
|
|
176
|
+
console.log(`Service ${service.name} added to blacklist due to timeout (${route.id}:${rule.contentType}:${service.id})`);
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
// HTTP错误,检查状态码
|
|
180
|
+
const statusCode = ((_c = error.response) === null || _c === void 0 ? void 0 : _c.status) || 500;
|
|
181
|
+
if (statusCode >= 400) {
|
|
182
|
+
yield this.dbManager.addToBlacklist(service.id, route.id, rule.contentType, error.message, statusCode, 'http');
|
|
183
|
+
console.log(`Service ${service.name} added to blacklist due to HTTP error ${statusCode} (${route.id}:${rule.contentType}:${service.id})`);
|
|
184
|
+
}
|
|
235
185
|
}
|
|
236
186
|
// 继续尝试下一个服务
|
|
237
187
|
continue;
|
|
@@ -240,7 +190,7 @@ class ProxyServer {
|
|
|
240
190
|
// 所有服务都失败了
|
|
241
191
|
console.error('All services failed');
|
|
242
192
|
// 记录日志
|
|
243
|
-
if (((
|
|
193
|
+
if (((_d = this.config) === null || _d === void 0 ? void 0 : _d.enableLogging) !== false && SUPPORTED_TARGETS.some(target => req.path.startsWith(`/${target}/`))) {
|
|
244
194
|
yield this.dbManager.addLog({
|
|
245
195
|
timestamp: Date.now(),
|
|
246
196
|
method: req.method,
|
|
@@ -282,7 +232,7 @@ class ProxyServer {
|
|
|
282
232
|
}
|
|
283
233
|
catch (error) {
|
|
284
234
|
console.error('Proxy error:', error);
|
|
285
|
-
if (((
|
|
235
|
+
if (((_e = this.config) === null || _e === void 0 ? void 0 : _e.enableLogging) !== false && SUPPORTED_TARGETS.some(target => req.path.startsWith(`/${target}/`))) {
|
|
286
236
|
yield this.dbManager.addLog({
|
|
287
237
|
timestamp: Date.now(),
|
|
288
238
|
method: req.method,
|
|
@@ -321,9 +271,16 @@ class ProxyServer {
|
|
|
321
271
|
}
|
|
322
272
|
}));
|
|
323
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
|
+
}
|
|
324
281
|
createFixedRouteHandler(targetType) {
|
|
325
282
|
return (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
326
|
-
var _a, _b, _c, _d;
|
|
283
|
+
var _a, _b, _c, _d, _e;
|
|
327
284
|
try {
|
|
328
285
|
// 检查API Key验证
|
|
329
286
|
if (this.config.apiKey) {
|
|
@@ -377,11 +334,24 @@ class ProxyServer {
|
|
|
377
334
|
catch (error) {
|
|
378
335
|
console.error(`Service ${service.name} failed:`, error.message);
|
|
379
336
|
lastError = error;
|
|
380
|
-
//
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
337
|
+
// 检测是否是 timeout 错误
|
|
338
|
+
const isTimeout = error.code === 'ECONNABORTED' ||
|
|
339
|
+
((_b = error.message) === null || _b === void 0 ? void 0 : _b.toLowerCase().includes('timeout')) ||
|
|
340
|
+
(error.errno && error.errno === 'ETIMEDOUT');
|
|
341
|
+
// 判断错误类型并加入黑名单
|
|
342
|
+
if (isTimeout) {
|
|
343
|
+
// Timeout错误,加入黑名单
|
|
344
|
+
yield this.dbManager.addToBlacklist(service.id, route.id, rule.contentType, 'Request timeout - the upstream API took too long to respond', undefined, // timeout没有HTTP状态码
|
|
345
|
+
'timeout');
|
|
346
|
+
console.log(`Service ${service.name} added to blacklist due to timeout (${route.id}:${rule.contentType}:${service.id})`);
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
// HTTP错误,检查状态码
|
|
350
|
+
const statusCode = ((_c = error.response) === null || _c === void 0 ? void 0 : _c.status) || 500;
|
|
351
|
+
if (statusCode >= 400) {
|
|
352
|
+
yield this.dbManager.addToBlacklist(service.id, route.id, rule.contentType, error.message, statusCode, 'http');
|
|
353
|
+
console.log(`Service ${service.name} added to blacklist due to HTTP error ${statusCode} (${route.id}:${rule.contentType}:${service.id})`);
|
|
354
|
+
}
|
|
385
355
|
}
|
|
386
356
|
// 继续尝试下一个服务
|
|
387
357
|
continue;
|
|
@@ -390,7 +360,7 @@ class ProxyServer {
|
|
|
390
360
|
// 所有服务都失败了
|
|
391
361
|
console.error('All services failed');
|
|
392
362
|
// 记录日志
|
|
393
|
-
if (((
|
|
363
|
+
if (((_d = this.config) === null || _d === void 0 ? void 0 : _d.enableLogging) !== false && SUPPORTED_TARGETS.some(target => req.path.startsWith(`/${target}/`))) {
|
|
394
364
|
yield this.dbManager.addLog({
|
|
395
365
|
timestamp: Date.now(),
|
|
396
366
|
method: req.method,
|
|
@@ -432,7 +402,7 @@ class ProxyServer {
|
|
|
432
402
|
}
|
|
433
403
|
catch (error) {
|
|
434
404
|
console.error(`Fixed route error for ${targetType}:`, error);
|
|
435
|
-
if (((
|
|
405
|
+
if (((_e = this.config) === null || _e === void 0 ? void 0 : _e.enableLogging) !== false && SUPPORTED_TARGETS.some(target => req.path.startsWith(`/${target}/`))) {
|
|
436
406
|
yield this.dbManager.addLog({
|
|
437
407
|
timestamp: Date.now(),
|
|
438
408
|
method: req.method,
|
|
@@ -473,16 +443,171 @@ class ProxyServer {
|
|
|
473
443
|
const routeRules = this.dbManager.getRules(routeId);
|
|
474
444
|
return routeRules.sort((a, b) => (b.sortOrder || 0) - (a.sortOrder || 0));
|
|
475
445
|
}
|
|
476
|
-
findMatchingRoute(
|
|
477
|
-
//
|
|
478
|
-
|
|
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
|
+
// 返回匹配目标类型且处于活跃状态的路由
|
|
479
459
|
const activeRoutes = this.getActiveRoutes();
|
|
480
|
-
return activeRoutes.find(route => route.isActive);
|
|
460
|
+
return activeRoutes.find(route => route.targetType === targetType && route.isActive);
|
|
481
461
|
}
|
|
482
462
|
findRouteByTargetType(targetType) {
|
|
483
463
|
const activeRoutes = this.getActiveRoutes();
|
|
484
464
|
return activeRoutes.find(route => route.targetType === targetType && route.isActive);
|
|
485
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
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* 根据GLM计费逻辑判断请求是否应该计费
|
|
546
|
+
* 核心规则:
|
|
547
|
+
* 1. 最后一条消息必须是 role: "user"
|
|
548
|
+
* 2. 上一条消息不能是包含 tool_calls 的 assistant 消息(即不是工具回传)
|
|
549
|
+
* 3. 上一条消息应该是 assistant(正常的对话流程),而非连续的 user 消息
|
|
550
|
+
* 4. 避免历史消息重复计数:检查消息序列是否符合正常的对话模式
|
|
551
|
+
*/
|
|
552
|
+
shouldChargeRequest(req) {
|
|
553
|
+
const body = req.body;
|
|
554
|
+
const messages = body === null || body === void 0 ? void 0 : body.messages;
|
|
555
|
+
if (!messages || !Array.isArray(messages) || messages.length === 0) {
|
|
556
|
+
return false;
|
|
557
|
+
}
|
|
558
|
+
const lastMessage = messages[messages.length - 1];
|
|
559
|
+
if (lastMessage.role !== 'user') {
|
|
560
|
+
return false;
|
|
561
|
+
}
|
|
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;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
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;
|
|
610
|
+
}
|
|
486
611
|
/**
|
|
487
612
|
* 从数据库实时获取服务配置
|
|
488
613
|
* @param serviceId 服务ID
|
|
@@ -517,10 +642,15 @@ class ProxyServer {
|
|
|
517
642
|
}
|
|
518
643
|
// 检查并重置到期的规则
|
|
519
644
|
this.dbManager.checkAndResetRuleIfNeeded(rule.id);
|
|
645
|
+
this.dbManager.checkAndResetRequestCountIfNeeded(rule.id);
|
|
520
646
|
// 检查token限制
|
|
521
647
|
if (rule.tokenLimit && rule.totalTokensUsed !== undefined && rule.totalTokensUsed >= rule.tokenLimit) {
|
|
522
648
|
continue; // 跳过超限规则
|
|
523
649
|
}
|
|
650
|
+
// 检查请求次数限制
|
|
651
|
+
if (rule.requestCountLimit && rule.totalRequestsUsed !== undefined && rule.totalRequestsUsed >= rule.requestCountLimit) {
|
|
652
|
+
continue; // 跳过超限规则
|
|
653
|
+
}
|
|
524
654
|
return rule;
|
|
525
655
|
}
|
|
526
656
|
}
|
|
@@ -535,10 +665,15 @@ class ProxyServer {
|
|
|
535
665
|
}
|
|
536
666
|
// 检查并重置到期的规则
|
|
537
667
|
this.dbManager.checkAndResetRuleIfNeeded(rule.id);
|
|
668
|
+
this.dbManager.checkAndResetRequestCountIfNeeded(rule.id);
|
|
538
669
|
// 检查token限制
|
|
539
670
|
if (rule.tokenLimit && rule.totalTokensUsed !== undefined && rule.totalTokensUsed >= rule.tokenLimit) {
|
|
540
671
|
continue; // 跳过超限规则
|
|
541
672
|
}
|
|
673
|
+
// 检查请求次数限制
|
|
674
|
+
if (rule.requestCountLimit && rule.totalRequestsUsed !== undefined && rule.totalRequestsUsed >= rule.requestCountLimit) {
|
|
675
|
+
continue; // 跳过超限规则
|
|
676
|
+
}
|
|
542
677
|
return rule;
|
|
543
678
|
}
|
|
544
679
|
// 3. 最后返回 default 规则
|
|
@@ -551,10 +686,15 @@ class ProxyServer {
|
|
|
551
686
|
}
|
|
552
687
|
// 检查并重置到期的规则
|
|
553
688
|
this.dbManager.checkAndResetRuleIfNeeded(rule.id);
|
|
689
|
+
this.dbManager.checkAndResetRequestCountIfNeeded(rule.id);
|
|
554
690
|
// 检查token限制
|
|
555
691
|
if (rule.tokenLimit && rule.totalTokensUsed !== undefined && rule.totalTokensUsed >= rule.tokenLimit) {
|
|
556
692
|
continue; // 跳过超限规则
|
|
557
693
|
}
|
|
694
|
+
// 检查请求次数限制
|
|
695
|
+
if (rule.requestCountLimit && rule.totalRequestsUsed !== undefined && rule.totalRequestsUsed >= rule.requestCountLimit) {
|
|
696
|
+
continue; // 跳过超限规则
|
|
697
|
+
}
|
|
558
698
|
return rule;
|
|
559
699
|
}
|
|
560
700
|
return undefined;
|
|
@@ -584,12 +724,22 @@ class ProxyServer {
|
|
|
584
724
|
// 4. 检查并重置到期的规则
|
|
585
725
|
candidates.forEach(rule => {
|
|
586
726
|
this.dbManager.checkAndResetRuleIfNeeded(rule.id);
|
|
727
|
+
this.dbManager.checkAndResetRequestCountIfNeeded(rule.id);
|
|
587
728
|
});
|
|
588
|
-
// 5.
|
|
729
|
+
// 5. 过滤掉超过限制的规则(仅在有多个候选规则时)
|
|
589
730
|
if (candidates.length > 1) {
|
|
590
731
|
const filteredCandidates = candidates.filter(rule => {
|
|
732
|
+
// 检查token限制
|
|
591
733
|
if (rule.tokenLimit && rule.totalTokensUsed !== undefined) {
|
|
592
|
-
|
|
734
|
+
if (rule.totalTokensUsed >= rule.tokenLimit) {
|
|
735
|
+
return false;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
// 检查请求次数限制
|
|
739
|
+
if (rule.requestCountLimit && rule.totalRequestsUsed !== undefined) {
|
|
740
|
+
if (rule.totalRequestsUsed >= rule.requestCountLimit) {
|
|
741
|
+
return false;
|
|
742
|
+
}
|
|
593
743
|
}
|
|
594
744
|
return true; // 没有设置限制的规则总是可用
|
|
595
745
|
});
|
|
@@ -838,11 +988,15 @@ class ProxyServer {
|
|
|
838
988
|
addText(body === null || body === void 0 ? void 0 : body.prompt);
|
|
839
989
|
return length;
|
|
840
990
|
}
|
|
991
|
+
/** 判断是否为 Claude 相关类型(使用 x-api-key 认证) */
|
|
841
992
|
isClaudeSource(sourceType) {
|
|
842
|
-
return sourceType === 'claude-chat';
|
|
993
|
+
return sourceType === 'claude-chat' || sourceType === 'claude-code';
|
|
843
994
|
}
|
|
844
995
|
isOpenAIChatSource(sourceType) {
|
|
845
|
-
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');
|
|
846
1000
|
}
|
|
847
1001
|
/**
|
|
848
1002
|
* 判断模型是否应该使用 max_completion_tokens 字段
|
|
@@ -901,7 +1055,7 @@ class ProxyServer {
|
|
|
901
1055
|
const requestedMaxTokens = result[maxTokensFieldName] || result.max_tokens;
|
|
902
1056
|
// 如果请求中指定了 max_tokens,并且超过配置的限制,则限制为配置的最大值
|
|
903
1057
|
if (typeof requestedMaxTokens === 'number' && requestedMaxTokens > maxOutputLimit) {
|
|
904
|
-
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}`);
|
|
905
1059
|
result[maxTokensFieldName] = maxOutputLimit;
|
|
906
1060
|
// 如果使用了 max_completion_tokens,清理旧的 max_tokens 字段
|
|
907
1061
|
if (maxTokensFieldName === 'max_completion_tokens' && result.max_tokens !== undefined) {
|
|
@@ -910,7 +1064,7 @@ class ProxyServer {
|
|
|
910
1064
|
}
|
|
911
1065
|
else if (requestedMaxTokens === undefined) {
|
|
912
1066
|
// 如果请求中没有指定 max_tokens,则使用配置的最大值
|
|
913
|
-
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}`);
|
|
914
1068
|
result[maxTokensFieldName] = maxOutputLimit;
|
|
915
1069
|
}
|
|
916
1070
|
return result;
|
|
@@ -944,11 +1098,19 @@ class ProxyServer {
|
|
|
944
1098
|
if (streamRequested) {
|
|
945
1099
|
headers.accept = 'text/event-stream';
|
|
946
1100
|
}
|
|
947
|
-
|
|
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)
|
|
948
1106
|
headers['x-api-key'] = service.apiKey;
|
|
949
|
-
|
|
1107
|
+
if (this.isClaudeSource(sourceType) || authType === 'x-api-key') {
|
|
1108
|
+
// 仅在明确配置或 Claude 源时添加 anthropic-version
|
|
1109
|
+
headers['anthropic-version'] = headers['anthropic-version'] || '2023-06-01';
|
|
1110
|
+
}
|
|
950
1111
|
}
|
|
951
1112
|
else {
|
|
1113
|
+
// 使用 Authorization 认证(适用于 openai-chat, openai-responses, deepseek-reasoning-chat 等)
|
|
952
1114
|
delete headers['anthropic-version'];
|
|
953
1115
|
delete headers['anthropic-beta'];
|
|
954
1116
|
headers.authorization = `Bearer ${service.apiKey}`;
|
|
@@ -1025,6 +1187,62 @@ class ProxyServer {
|
|
|
1025
1187
|
}
|
|
1026
1188
|
return undefined;
|
|
1027
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
|
+
}
|
|
1028
1246
|
/**
|
|
1029
1247
|
* 根据源工具类型和目标API类型,映射请求路径
|
|
1030
1248
|
* @param sourceTool 源工具类型 (claude-code 或 codex)
|
|
@@ -1078,8 +1296,13 @@ class ProxyServer {
|
|
|
1078
1296
|
let actuallyUsedProxy = false; // 标记是否实际使用了代理
|
|
1079
1297
|
const finalizeLog = (statusCode, error) => __awaiter(this, void 0, void 0, function* () {
|
|
1080
1298
|
var _a, _b;
|
|
1081
|
-
if (logged
|
|
1299
|
+
if (logged)
|
|
1082
1300
|
return;
|
|
1301
|
+
// 检查是否启用日志记录(默认启用)
|
|
1302
|
+
const enableLogging = ((_a = this.config) === null || _a === void 0 ? void 0 : _a.enableLogging) !== false; // 默认为 true
|
|
1303
|
+
if (!enableLogging) {
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1083
1306
|
// 只记录来自编程工具的请求
|
|
1084
1307
|
if (!SUPPORTED_TARGETS.some(target => req.path.startsWith(`/${target}/`))) {
|
|
1085
1308
|
return;
|
|
@@ -1115,6 +1338,26 @@ class ProxyServer {
|
|
|
1115
1338
|
streamChunks: streamChunksForLog,
|
|
1116
1339
|
upstreamRequest: upstreamRequestForLog,
|
|
1117
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
|
+
}
|
|
1118
1361
|
// 更新规则的token使用量(只在成功请求时更新)
|
|
1119
1362
|
if (usageForLog && statusCode < 400) {
|
|
1120
1363
|
const totalTokens = (usageForLog.inputTokens || 0) + (usageForLog.outputTokens || 0);
|
|
@@ -1122,6 +1365,19 @@ class ProxyServer {
|
|
|
1122
1365
|
this.dbManager.incrementRuleTokenUsage(rule.id, totalTokens);
|
|
1123
1366
|
}
|
|
1124
1367
|
}
|
|
1368
|
+
// 更新规则的请求次数(只在成功请求时更新)
|
|
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
|
+
}
|
|
1380
|
+
}
|
|
1125
1381
|
});
|
|
1126
1382
|
try {
|
|
1127
1383
|
if (targetType === 'claude-code') {
|
|
@@ -1165,7 +1421,7 @@ class ProxyServer {
|
|
|
1165
1421
|
const mappedPath = this.mapRequestPath(route.targetType, sourceType, pathToAppend);
|
|
1166
1422
|
const config = {
|
|
1167
1423
|
method: req.method,
|
|
1168
|
-
url: `${service.apiUrl}${mappedPath}`,
|
|
1424
|
+
url: this.isChatType(sourceType) ? service.apiUrl : `${service.apiUrl}${mappedPath}`,
|
|
1169
1425
|
headers: this.buildUpstreamHeaders(req, service, sourceType, streamRequested),
|
|
1170
1426
|
timeout: rule.timeout || 3000000, // 默认300秒
|
|
1171
1427
|
validateStatus: () => true,
|
|
@@ -1207,13 +1463,16 @@ class ProxyServer {
|
|
|
1207
1463
|
}
|
|
1208
1464
|
}
|
|
1209
1465
|
// 记录实际发出的请求信息作为日志的一部分
|
|
1210
|
-
const actualModel =
|
|
1211
|
-
const maxTokensFieldName = this.getMaxTokensFieldName(actualModel);
|
|
1212
|
-
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);
|
|
1213
1470
|
upstreamRequestForLog = {
|
|
1214
|
-
url: `${service.apiUrl}${mappedPath}`,
|
|
1215
|
-
model: actualModel,
|
|
1216
|
-
[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,
|
|
1217
1476
|
};
|
|
1218
1477
|
if (actuallyUsedProxy) {
|
|
1219
1478
|
upstreamRequestForLog.useProxy = true;
|
|
@@ -1319,6 +1578,33 @@ class ProxyServer {
|
|
|
1319
1578
|
usageForLog = this.extractTokenUsage(responseData === null || responseData === void 0 ? void 0 : responseData.usage);
|
|
1320
1579
|
// 记录错误响应体
|
|
1321
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
|
+
});
|
|
1322
1608
|
this.copyResponseHeaders(responseHeaders, res);
|
|
1323
1609
|
if (contentType.includes('application/json')) {
|
|
1324
1610
|
res.status(response.status).json(responseData);
|
|
@@ -1365,7 +1651,18 @@ class ProxyServer {
|
|
|
1365
1651
|
const errorMessage = isTimeout
|
|
1366
1652
|
? 'Request timeout - the upstream API took too long to respond'
|
|
1367
1653
|
: (error.message || 'Internal server error');
|
|
1368
|
-
|
|
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);
|
|
1369
1666
|
// 根据请求类型返回适当格式的错误响应
|
|
1370
1667
|
const streamRequested = this.isStreamRequested(req, req.body || {});
|
|
1371
1668
|
if (route.targetType === 'claude-code') {
|
|
@@ -1433,9 +1730,9 @@ class ProxyServer {
|
|
|
1433
1730
|
this.config = config;
|
|
1434
1731
|
});
|
|
1435
1732
|
}
|
|
1436
|
-
|
|
1733
|
+
registerProxyRoutes() {
|
|
1437
1734
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1438
|
-
this.
|
|
1735
|
+
this.addProxyRoutes();
|
|
1439
1736
|
yield this.reloadRoutes();
|
|
1440
1737
|
});
|
|
1441
1738
|
}
|