aicodeswitch 3.6.2 → 3.9.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/dist/server/main.js +134 -20
- package/dist/server/original-config-reader.js +181 -0
- package/dist/server/proxy-server.js +134 -1
- package/dist/server/transformers/claude-openai.js +2 -2
- package/dist/ui/assets/index-guA3KrZ3.js +511 -0
- package/dist/ui/index.html +1 -1
- package/package.json +1 -1
- package/dist/ui/assets/index-BtoE4g4L.js +0 -511
package/dist/server/main.js
CHANGED
|
@@ -104,7 +104,7 @@ const asyncHandler = (handler) => (req, res, next) => {
|
|
|
104
104
|
next(err);
|
|
105
105
|
});
|
|
106
106
|
};
|
|
107
|
-
const writeClaudeConfig = (dbManager, enableAgentTeams) => __awaiter(void 0, void 0, void 0, function* () {
|
|
107
|
+
const writeClaudeConfig = (dbManager, enableAgentTeams, enableBypassPermissionsSupport) => __awaiter(void 0, void 0, void 0, function* () {
|
|
108
108
|
try {
|
|
109
109
|
const homeDir = os_1.default.homedir();
|
|
110
110
|
const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 4567;
|
|
@@ -153,6 +153,13 @@ const writeClaudeConfig = (dbManager, enableAgentTeams) => __awaiter(void 0, voi
|
|
|
153
153
|
const claudeSettings = {
|
|
154
154
|
env: claudeSettingsEnv
|
|
155
155
|
};
|
|
156
|
+
// 如果开启对bypassPermissions的支持,添加对应的配置项
|
|
157
|
+
if (enableBypassPermissionsSupport) {
|
|
158
|
+
claudeSettings.permissions = {
|
|
159
|
+
defaultMode: "bypassPermissions"
|
|
160
|
+
};
|
|
161
|
+
claudeSettings.skipDangerousModePermissionPrompt = true;
|
|
162
|
+
}
|
|
156
163
|
fs_1.default.writeFileSync(claudeSettingsPath, JSON.stringify(claudeSettings, null, 2));
|
|
157
164
|
// Claude Code .claude.json
|
|
158
165
|
const claudeJsonPath = path_1.default.join(homeDir, '.claude.json');
|
|
@@ -243,10 +250,80 @@ const updateClaudeAgentTeamsConfig = (enableAgentTeams) => __awaiter(void 0, voi
|
|
|
243
250
|
return false;
|
|
244
251
|
}
|
|
245
252
|
});
|
|
246
|
-
|
|
253
|
+
/**
|
|
254
|
+
* 更新Claude Code配置中的bypassPermissions支持设置
|
|
255
|
+
* 此函数假设配置文件已经被代理覆盖,直接修改配置而不重新备份
|
|
256
|
+
*/
|
|
257
|
+
const updateClaudeBypassPermissionsSupportConfig = (enableBypassPermissionsSupport) => __awaiter(void 0, void 0, void 0, function* () {
|
|
258
|
+
try {
|
|
259
|
+
const homeDir = os_1.default.homedir();
|
|
260
|
+
const claudeSettingsPath = path_1.default.join(homeDir, '.claude/settings.json');
|
|
261
|
+
// 检查配置文件是否存在
|
|
262
|
+
if (!fs_1.default.existsSync(claudeSettingsPath)) {
|
|
263
|
+
console.error('Claude settings.json does not exist');
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
// 读取当前配置
|
|
267
|
+
const currentContent = fs_1.default.readFileSync(claudeSettingsPath, 'utf-8');
|
|
268
|
+
const currentConfig = JSON.parse(currentContent);
|
|
269
|
+
// 检查是否是代理配置
|
|
270
|
+
const configStatus = (0, config_metadata_1.checkClaudeConfigStatus)();
|
|
271
|
+
if (!configStatus.isOverwritten) {
|
|
272
|
+
console.error('Claude config is not overwritten by proxy. Please activate a route first.');
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
// 更新或删除bypassPermissions支持配置项
|
|
276
|
+
if (enableBypassPermissionsSupport) {
|
|
277
|
+
currentConfig.permissions = {
|
|
278
|
+
defaultMode: "bypassPermissions"
|
|
279
|
+
};
|
|
280
|
+
currentConfig.skipDangerousModePermissionPrompt = true;
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
delete currentConfig.permissions;
|
|
284
|
+
delete currentConfig.skipDangerousModePermissionPrompt;
|
|
285
|
+
}
|
|
286
|
+
// 写入更新后的配置
|
|
287
|
+
fs_1.default.writeFileSync(claudeSettingsPath, JSON.stringify(currentConfig, null, 2));
|
|
288
|
+
// 更新元数据中的当前配置hash
|
|
289
|
+
const metadata = (0, config_metadata_1.loadMetadata)('claude');
|
|
290
|
+
if (metadata && metadata.files[0]) {
|
|
291
|
+
metadata.files[0].currentHash = (0, crypto_1.createHash)('sha256')
|
|
292
|
+
.update(fs_1.default.readFileSync(claudeSettingsPath, 'utf-8'))
|
|
293
|
+
.digest('hex');
|
|
294
|
+
metadata.timestamp = Date.now();
|
|
295
|
+
(0, config_metadata_1.saveMetadata)(metadata);
|
|
296
|
+
}
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
console.error('Failed to update Claude bypassPermissions support config:', error);
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
const VALID_CODEX_REASONING_EFFORTS = ['low', 'medium', 'high'];
|
|
305
|
+
const DEFAULT_CODEX_REASONING_EFFORT = 'high';
|
|
306
|
+
const isCodexReasoningEffort = (value) => {
|
|
307
|
+
return typeof value === 'string' && VALID_CODEX_REASONING_EFFORTS.includes(value);
|
|
308
|
+
};
|
|
309
|
+
const buildCodexConfigToml = (modelReasoningEffort) => {
|
|
310
|
+
const localPort = process.env.PORT ? parseInt(process.env.PORT, 10) : 4567;
|
|
311
|
+
return `model_provider = "aicodeswitch"
|
|
312
|
+
model = "gpt-5.1-codex"
|
|
313
|
+
model_reasoning_effort = "${modelReasoningEffort}"
|
|
314
|
+
disable_response_storage = true
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
[model_providers.aicodeswitch]
|
|
318
|
+
name = "aicodeswitch"
|
|
319
|
+
base_url = "http://${host}:${localPort}/codex"
|
|
320
|
+
wire_api = "responses"
|
|
321
|
+
requires_openai_auth = true
|
|
322
|
+
`;
|
|
323
|
+
};
|
|
324
|
+
const writeCodexConfig = (dbManager_1, ...args_1) => __awaiter(void 0, [dbManager_1, ...args_1], void 0, function* (dbManager, modelReasoningEffort = DEFAULT_CODEX_REASONING_EFFORT) {
|
|
247
325
|
try {
|
|
248
326
|
const homeDir = os_1.default.homedir();
|
|
249
|
-
const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 4567;
|
|
250
327
|
const config = dbManager.getConfig();
|
|
251
328
|
// Codex config.toml
|
|
252
329
|
const codexDir = path_1.default.join(homeDir, '.codex');
|
|
@@ -277,19 +354,7 @@ const writeCodexConfig = (dbManager) => __awaiter(void 0, void 0, void 0, functi
|
|
|
277
354
|
if (!fs_1.default.existsSync(codexDir)) {
|
|
278
355
|
fs_1.default.mkdirSync(codexDir, { recursive: true });
|
|
279
356
|
}
|
|
280
|
-
|
|
281
|
-
model = "gpt-5.1-codex"
|
|
282
|
-
model_reasoning_effort = "high"
|
|
283
|
-
disable_response_storage = true
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
[model_providers.aicodeswitch]
|
|
287
|
-
name = "aicodeswitch"
|
|
288
|
-
base_url = "http://${host}:${port}/codex"
|
|
289
|
-
wire_api = "responses"
|
|
290
|
-
requires_openai_auth = true
|
|
291
|
-
`;
|
|
292
|
-
fs_1.default.writeFileSync(codexConfigPath, codexConfig);
|
|
357
|
+
fs_1.default.writeFileSync(codexConfigPath, buildCodexConfigToml(modelReasoningEffort));
|
|
293
358
|
// Codex auth.json
|
|
294
359
|
const codexAuthPath = path_1.default.join(codexDir, 'auth.json');
|
|
295
360
|
// 同样处理 auth.json 的备份
|
|
@@ -308,7 +373,7 @@ requires_openai_auth = true
|
|
|
308
373
|
configType: 'codex',
|
|
309
374
|
timestamp: Date.now(),
|
|
310
375
|
originalHash: originalConfigHash,
|
|
311
|
-
proxyMarker: `http://${host}:${
|
|
376
|
+
proxyMarker: `http://${host}:${process.env.PORT ? parseInt(process.env.PORT, 10) : 4567}/codex`,
|
|
312
377
|
files: [
|
|
313
378
|
{
|
|
314
379
|
originalPath: codexConfigPath,
|
|
@@ -329,6 +394,35 @@ requires_openai_auth = true
|
|
|
329
394
|
return false;
|
|
330
395
|
}
|
|
331
396
|
});
|
|
397
|
+
const updateCodexReasoningEffortConfig = (modelReasoningEffort) => __awaiter(void 0, void 0, void 0, function* () {
|
|
398
|
+
try {
|
|
399
|
+
const homeDir = os_1.default.homedir();
|
|
400
|
+
const codexConfigPath = path_1.default.join(homeDir, '.codex/config.toml');
|
|
401
|
+
if (!fs_1.default.existsSync(codexConfigPath)) {
|
|
402
|
+
console.error('Codex config.toml does not exist');
|
|
403
|
+
return false;
|
|
404
|
+
}
|
|
405
|
+
const configStatus = (0, config_metadata_1.checkCodexConfigStatus)();
|
|
406
|
+
if (!configStatus.isOverwritten) {
|
|
407
|
+
console.error('Codex config is not overwritten by proxy. Please activate a route first.');
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
410
|
+
fs_1.default.writeFileSync(codexConfigPath, buildCodexConfigToml(modelReasoningEffort));
|
|
411
|
+
const metadata = (0, config_metadata_1.loadMetadata)('codex');
|
|
412
|
+
if (metadata && metadata.files[0]) {
|
|
413
|
+
metadata.files[0].currentHash = (0, crypto_1.createHash)('sha256')
|
|
414
|
+
.update(fs_1.default.readFileSync(codexConfigPath, 'utf-8'))
|
|
415
|
+
.digest('hex');
|
|
416
|
+
metadata.timestamp = Date.now();
|
|
417
|
+
(0, config_metadata_1.saveMetadata)(metadata);
|
|
418
|
+
}
|
|
419
|
+
return true;
|
|
420
|
+
}
|
|
421
|
+
catch (error) {
|
|
422
|
+
console.error('Failed to update Codex reasoning effort config:', error);
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
425
|
+
});
|
|
332
426
|
const restoreClaudeConfig = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
333
427
|
try {
|
|
334
428
|
const homeDir = os_1.default.homedir();
|
|
@@ -1381,11 +1475,16 @@ ${instruction}
|
|
|
1381
1475
|
})));
|
|
1382
1476
|
app.post('/api/write-config/claude', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1383
1477
|
const enableAgentTeams = req.body.enableAgentTeams;
|
|
1384
|
-
const
|
|
1478
|
+
const enableBypassPermissionsSupport = req.body.enableBypassPermissionsSupport;
|
|
1479
|
+
const result = yield writeClaudeConfig(dbManager, enableAgentTeams, enableBypassPermissionsSupport);
|
|
1385
1480
|
res.json(result);
|
|
1386
1481
|
})));
|
|
1387
|
-
app.post('/api/write-config/codex', asyncHandler((
|
|
1388
|
-
const
|
|
1482
|
+
app.post('/api/write-config/codex', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1483
|
+
const requestedEffort = req.body.modelReasoningEffort;
|
|
1484
|
+
const modelReasoningEffort = isCodexReasoningEffort(requestedEffort)
|
|
1485
|
+
? requestedEffort
|
|
1486
|
+
: DEFAULT_CODEX_REASONING_EFFORT;
|
|
1487
|
+
const result = yield writeCodexConfig(dbManager, modelReasoningEffort);
|
|
1389
1488
|
res.json(result);
|
|
1390
1489
|
})));
|
|
1391
1490
|
// 更新Claude Code配置中的Agent Teams设置(当路由已激活时)
|
|
@@ -1394,6 +1493,21 @@ ${instruction}
|
|
|
1394
1493
|
const result = yield updateClaudeAgentTeamsConfig(enableAgentTeams);
|
|
1395
1494
|
res.json(result);
|
|
1396
1495
|
})));
|
|
1496
|
+
// 更新Claude Code配置中的bypassPermissions支持设置(当路由已激活时)
|
|
1497
|
+
app.post('/api/update-claude-bypass-permissions-support', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1498
|
+
const { enableBypassPermissionsSupport } = req.body;
|
|
1499
|
+
const result = yield updateClaudeBypassPermissionsSupportConfig(enableBypassPermissionsSupport);
|
|
1500
|
+
res.json(result);
|
|
1501
|
+
})));
|
|
1502
|
+
app.post('/api/update-codex-reasoning-effort', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1503
|
+
const requestedEffort = req.body.modelReasoningEffort;
|
|
1504
|
+
if (!isCodexReasoningEffort(requestedEffort)) {
|
|
1505
|
+
res.status(400).json({ error: 'Invalid modelReasoningEffort' });
|
|
1506
|
+
return;
|
|
1507
|
+
}
|
|
1508
|
+
const result = yield updateCodexReasoningEffortConfig(requestedEffort);
|
|
1509
|
+
res.json(result);
|
|
1510
|
+
})));
|
|
1397
1511
|
app.post('/api/restore-config/claude', asyncHandler((_req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1398
1512
|
const result = yield restoreClaudeConfig();
|
|
1399
1513
|
res.json(result);
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.readOriginalConfig = exports.readCodexOriginalConfig = exports.readClaudeOriginalConfig = void 0;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const os_1 = __importDefault(require("os"));
|
|
10
|
+
const types_1 = require("../types");
|
|
11
|
+
/**
|
|
12
|
+
* TOML 解析器(简单实现,仅用于解析 Codex config.toml)
|
|
13
|
+
*/
|
|
14
|
+
const parseToml = (content) => {
|
|
15
|
+
const result = {};
|
|
16
|
+
let currentSection = result;
|
|
17
|
+
const lines = content.split('\n');
|
|
18
|
+
for (const line of lines) {
|
|
19
|
+
const trimmed = line.trim();
|
|
20
|
+
// 跳过空行和注释
|
|
21
|
+
if (!trimmed || trimmed.startsWith('#')) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
// 检查是否是 section
|
|
25
|
+
const sectionMatch = trimmed.match(/^\[([^\]]+)\]$/);
|
|
26
|
+
if (sectionMatch) {
|
|
27
|
+
const sectionPath = sectionMatch[1].split('.');
|
|
28
|
+
currentSection = result;
|
|
29
|
+
for (const key of sectionPath) {
|
|
30
|
+
if (!currentSection[key]) {
|
|
31
|
+
currentSection[key] = {};
|
|
32
|
+
}
|
|
33
|
+
currentSection = currentSection[key];
|
|
34
|
+
}
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
// 解析键值对
|
|
38
|
+
const kvMatch = trimmed.match(/^([^=]+)=(.*)$/);
|
|
39
|
+
if (kvMatch) {
|
|
40
|
+
const key = kvMatch[1].trim();
|
|
41
|
+
let value = kvMatch[2].trim();
|
|
42
|
+
// 移除引号
|
|
43
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
44
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
45
|
+
value = value.slice(1, -1);
|
|
46
|
+
}
|
|
47
|
+
// 尝试转换为布尔值
|
|
48
|
+
if (value === 'true') {
|
|
49
|
+
value = true;
|
|
50
|
+
}
|
|
51
|
+
else if (value === 'false') {
|
|
52
|
+
value = false;
|
|
53
|
+
}
|
|
54
|
+
currentSection[key] = value;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return result;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* 读取 Claude Code 原始配置
|
|
61
|
+
* 从备份文件或当前配置文件中读取(如果未激活路由)
|
|
62
|
+
*/
|
|
63
|
+
const readClaudeOriginalConfig = () => {
|
|
64
|
+
var _a, _b, _c;
|
|
65
|
+
try {
|
|
66
|
+
const homeDir = os_1.default.homedir();
|
|
67
|
+
const settingsPath = path_1.default.join(homeDir, '.claude/settings.json');
|
|
68
|
+
const settingsBakPath = path_1.default.join(homeDir, '.claude/settings.json.aicodeswitch_backup');
|
|
69
|
+
// 优先读取备份文件(原始配置)
|
|
70
|
+
let configPath = settingsBakPath;
|
|
71
|
+
if (!fs_1.default.existsSync(configPath)) {
|
|
72
|
+
// 如果没有备份,尝试读取当前配置
|
|
73
|
+
configPath = settingsPath;
|
|
74
|
+
if (!fs_1.default.existsSync(configPath)) {
|
|
75
|
+
console.log('No Claude config file found');
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const content = fs_1.default.readFileSync(configPath, 'utf-8');
|
|
80
|
+
const config = JSON.parse(content);
|
|
81
|
+
// 提取配置信息
|
|
82
|
+
const baseUrl = ((_a = config.env) === null || _a === void 0 ? void 0 : _a.ANTHROPIC_BASE_URL) || 'https://api.anthropic.com';
|
|
83
|
+
const apiKey = ((_b = config.env) === null || _b === void 0 ? void 0 : _b.ANTHROPIC_AUTH_TOKEN) || ((_c = config.env) === null || _c === void 0 ? void 0 : _c.ANTHROPIC_API_KEY) || '';
|
|
84
|
+
if (!apiKey) {
|
|
85
|
+
console.log('No API key found in Claude config');
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
apiUrl: baseUrl,
|
|
90
|
+
apiKey: apiKey,
|
|
91
|
+
authType: types_1.AuthType.AUTH_TOKEN,
|
|
92
|
+
sourceType: 'claude',
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
console.error('Failed to read Claude original config:', error);
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
exports.readClaudeOriginalConfig = readClaudeOriginalConfig;
|
|
101
|
+
/**
|
|
102
|
+
* 读取 Codex 原始配置
|
|
103
|
+
* 从备份文件或当前配置文件中读取(如果未激活路由)
|
|
104
|
+
*/
|
|
105
|
+
const readCodexOriginalConfig = () => {
|
|
106
|
+
try {
|
|
107
|
+
const homeDir = os_1.default.homedir();
|
|
108
|
+
const configPath = path_1.default.join(homeDir, '.codex/config.toml');
|
|
109
|
+
const configBakPath = path_1.default.join(homeDir, '.codex/config.toml.aicodeswitch_backup');
|
|
110
|
+
const authPath = path_1.default.join(homeDir, '.codex/auth.json');
|
|
111
|
+
// 优先读取备份文件(原始配置)
|
|
112
|
+
let tomlPath = configBakPath;
|
|
113
|
+
if (!fs_1.default.existsSync(tomlPath)) {
|
|
114
|
+
// 如果没有备份,尝试读取当前配置
|
|
115
|
+
tomlPath = configPath;
|
|
116
|
+
if (!fs_1.default.existsSync(tomlPath)) {
|
|
117
|
+
console.log('No Codex config file found');
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const tomlContent = fs_1.default.readFileSync(tomlPath, 'utf-8');
|
|
122
|
+
const config = parseToml(tomlContent);
|
|
123
|
+
// 提取 base_url
|
|
124
|
+
let baseUrl = '';
|
|
125
|
+
let model = config.model;
|
|
126
|
+
// 从 model_providers 中查找配置
|
|
127
|
+
if (config.model_providers) {
|
|
128
|
+
const providerName = config.model_provider;
|
|
129
|
+
if (providerName && config.model_providers[providerName]) {
|
|
130
|
+
baseUrl = config.model_providers[providerName].base_url;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (!baseUrl) {
|
|
134
|
+
console.log('No base_url found in Codex config');
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
// 读取 API key(从 auth.json)
|
|
138
|
+
let apiKey = '';
|
|
139
|
+
if (fs_1.default.existsSync(authPath)) {
|
|
140
|
+
try {
|
|
141
|
+
const authContent = fs_1.default.readFileSync(authPath, 'utf-8');
|
|
142
|
+
const authConfig = JSON.parse(authContent);
|
|
143
|
+
// Codex 的 auth.json 可能包含多个 provider 的 key
|
|
144
|
+
// 尝试读取常见的字段
|
|
145
|
+
apiKey = authConfig.api_key || authConfig.openai_api_key || authConfig.key || '';
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
console.error('Failed to read Codex auth.json:', error);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (!apiKey) {
|
|
152
|
+
console.log('No API key found in Codex auth.json');
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
apiUrl: baseUrl,
|
|
157
|
+
apiKey: apiKey,
|
|
158
|
+
authType: types_1.AuthType.API_KEY,
|
|
159
|
+
sourceType: 'openai',
|
|
160
|
+
model: model,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
console.error('Failed to read Codex original config:', error);
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
exports.readCodexOriginalConfig = readCodexOriginalConfig;
|
|
169
|
+
/**
|
|
170
|
+
* 根据目标类型读取原始配置
|
|
171
|
+
*/
|
|
172
|
+
const readOriginalConfig = (targetType) => {
|
|
173
|
+
if (targetType === 'claude-code') {
|
|
174
|
+
return (0, exports.readClaudeOriginalConfig)();
|
|
175
|
+
}
|
|
176
|
+
else if (targetType === 'codex') {
|
|
177
|
+
return (0, exports.readCodexOriginalConfig)();
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
};
|
|
181
|
+
exports.readOriginalConfig = readOriginalConfig;
|
|
@@ -57,6 +57,7 @@ const gemini_1 = require("./transformers/gemini");
|
|
|
57
57
|
const types_1 = require("../types");
|
|
58
58
|
const mcp_image_handler_1 = require("./mcp-image-handler");
|
|
59
59
|
const type_migration_1 = require("./type-migration");
|
|
60
|
+
const original_config_reader_1 = require("./original-config-reader");
|
|
60
61
|
const SUPPORTED_TARGETS = ['claude-code', 'codex'];
|
|
61
62
|
class ProxyServer {
|
|
62
63
|
constructor(dbManager, app) {
|
|
@@ -135,7 +136,13 @@ class ProxyServer {
|
|
|
135
136
|
try {
|
|
136
137
|
const route = this.findMatchingRoute(req);
|
|
137
138
|
if (!route) {
|
|
138
|
-
|
|
139
|
+
// 没有找到激活的路由,尝试使用原始配置
|
|
140
|
+
const fallbackResult = yield this.handleFallbackToOriginalConfig(req, res);
|
|
141
|
+
if (fallbackResult) {
|
|
142
|
+
return; // 成功使用原始配置处理请求
|
|
143
|
+
}
|
|
144
|
+
// 如果原始配置也不可用,返回错误
|
|
145
|
+
return res.status(404).json({ error: 'No matching route found and no original config available' });
|
|
139
146
|
}
|
|
140
147
|
// 高智商命令检测和会话状态管理
|
|
141
148
|
const highIqCommand = this.detectHighIqCommand(req.body);
|
|
@@ -646,6 +653,110 @@ class ProxyServer {
|
|
|
646
653
|
const activeRoutes = this.getActiveRoutes();
|
|
647
654
|
return activeRoutes.find(route => route.targetType === targetType && route.isActive);
|
|
648
655
|
}
|
|
656
|
+
/**
|
|
657
|
+
* 当没有激活的路由时,fallback 到原始配置
|
|
658
|
+
* @returns true 表示成功处理,false 表示无法处理
|
|
659
|
+
*/
|
|
660
|
+
handleFallbackToOriginalConfig(req, res) {
|
|
661
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
662
|
+
// 确定目标类型
|
|
663
|
+
let targetType;
|
|
664
|
+
if (req.path.startsWith('/claude-code/')) {
|
|
665
|
+
targetType = 'claude-code';
|
|
666
|
+
}
|
|
667
|
+
else if (req.path.startsWith('/codex/')) {
|
|
668
|
+
targetType = 'codex';
|
|
669
|
+
}
|
|
670
|
+
if (!targetType) {
|
|
671
|
+
return false;
|
|
672
|
+
}
|
|
673
|
+
// 读取原始配置
|
|
674
|
+
const originalConfig = (0, original_config_reader_1.readOriginalConfig)(targetType);
|
|
675
|
+
if (!originalConfig) {
|
|
676
|
+
console.log(`[FALLBACK] No original config available for ${targetType}`);
|
|
677
|
+
return false;
|
|
678
|
+
}
|
|
679
|
+
// 检查原始配置的 API URL 是否指向本系统(避免死循环)
|
|
680
|
+
if (this.isLocalProxyUrl(originalConfig.apiUrl, targetType)) {
|
|
681
|
+
console.error(`[FALLBACK] Original config points to local proxy, rejecting to avoid loop: ${originalConfig.apiUrl}`);
|
|
682
|
+
return false;
|
|
683
|
+
}
|
|
684
|
+
console.log(`[FALLBACK] Using original config for ${targetType}: ${originalConfig.apiUrl}`);
|
|
685
|
+
try {
|
|
686
|
+
// 创建临时的路由对象(用于传递给 proxyRequest)
|
|
687
|
+
const tempRoute = {
|
|
688
|
+
id: 'fallback-route',
|
|
689
|
+
name: 'Fallback to Original Config',
|
|
690
|
+
targetType: targetType,
|
|
691
|
+
isActive: true,
|
|
692
|
+
createdAt: Date.now(),
|
|
693
|
+
updatedAt: Date.now(),
|
|
694
|
+
};
|
|
695
|
+
// 创建临时的规则对象
|
|
696
|
+
const tempRule = {
|
|
697
|
+
id: 'fallback-rule',
|
|
698
|
+
routeId: 'fallback-route',
|
|
699
|
+
contentType: 'default',
|
|
700
|
+
targetServiceId: 'fallback-service',
|
|
701
|
+
createdAt: Date.now(),
|
|
702
|
+
updatedAt: Date.now(),
|
|
703
|
+
};
|
|
704
|
+
// 创建临时的服务对象
|
|
705
|
+
const tempService = {
|
|
706
|
+
id: 'fallback-service',
|
|
707
|
+
name: 'Original Config',
|
|
708
|
+
apiUrl: originalConfig.apiUrl,
|
|
709
|
+
apiKey: originalConfig.apiKey,
|
|
710
|
+
authType: originalConfig.authType,
|
|
711
|
+
sourceType: originalConfig.sourceType || (targetType === 'claude-code' ? 'claude' : 'openai'),
|
|
712
|
+
createdAt: Date.now(),
|
|
713
|
+
updatedAt: Date.now(),
|
|
714
|
+
};
|
|
715
|
+
// 调用 proxyRequest 处理请求,并标记使用原始配置
|
|
716
|
+
yield this.proxyRequest(req, res, tempRoute, tempRule, tempService, {
|
|
717
|
+
useOriginalConfig: true,
|
|
718
|
+
});
|
|
719
|
+
return true;
|
|
720
|
+
}
|
|
721
|
+
catch (error) {
|
|
722
|
+
console.error('[FALLBACK] Failed to use original config:', error);
|
|
723
|
+
return false;
|
|
724
|
+
}
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* 检查 API URL 是否指向本系统的代理服务
|
|
729
|
+
* 用于避免 fallback 时的死循环
|
|
730
|
+
*/
|
|
731
|
+
isLocalProxyUrl(apiUrl, targetType) {
|
|
732
|
+
try {
|
|
733
|
+
const url = new URL(apiUrl);
|
|
734
|
+
// 检查是否是 localhost 或 127.0.0.1
|
|
735
|
+
const isLocalhost = url.hostname === 'localhost' ||
|
|
736
|
+
url.hostname === '127.0.0.1' ||
|
|
737
|
+
url.hostname === '::1' ||
|
|
738
|
+
url.hostname === '0.0.0.0';
|
|
739
|
+
if (!isLocalhost) {
|
|
740
|
+
return false;
|
|
741
|
+
}
|
|
742
|
+
// 检查端口是否是本系统的端口(默认 4567)
|
|
743
|
+
const serverPort = process.env.PORT ? parseInt(process.env.PORT, 10) : 4567;
|
|
744
|
+
const urlPort = url.port ? parseInt(url.port, 10) : (url.protocol === 'https:' ? 443 : 80);
|
|
745
|
+
const isSamePort = urlPort === serverPort;
|
|
746
|
+
if (!isSamePort) {
|
|
747
|
+
return false;
|
|
748
|
+
}
|
|
749
|
+
// 检查路径是否包含本系统的代理路径
|
|
750
|
+
const proxyPath = `/${targetType}`;
|
|
751
|
+
const hasProxyPath = url.pathname.startsWith(proxyPath) ||
|
|
752
|
+
url.pathname === proxyPath;
|
|
753
|
+
return hasProxyPath;
|
|
754
|
+
}
|
|
755
|
+
catch (error) {
|
|
756
|
+
// URL 解析失败,认为不是本地代理 URL
|
|
757
|
+
return false;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
649
760
|
findRouteByTargetType(targetType) {
|
|
650
761
|
const activeRoutes = this.getActiveRoutes();
|
|
651
762
|
return activeRoutes.find(route => route.targetType === targetType && route.isActive);
|
|
@@ -1551,6 +1662,22 @@ class ProxyServer {
|
|
|
1551
1662
|
isChatType(sourceType) {
|
|
1552
1663
|
return sourceType.endsWith('-chat') || sourceType === 'gemini';
|
|
1553
1664
|
}
|
|
1665
|
+
/**
|
|
1666
|
+
* 构建 OpenAI Responses 类型的完整 URL
|
|
1667
|
+
* - baseUrl 以 /v{number} 结尾时,直接拼接请求路径
|
|
1668
|
+
* - baseUrl 不带版本时,自动补 /v1 再拼接请求路径
|
|
1669
|
+
* - 兼容请求路径本身已携带版本前缀(如 /v1/responses)场景
|
|
1670
|
+
*/
|
|
1671
|
+
buildOpenAIResponsesUrl(baseUrl, mappedPath) {
|
|
1672
|
+
const trimmedBase = baseUrl.trim().replace(/\/+$/, '');
|
|
1673
|
+
const normalizedPath = mappedPath.startsWith('/') || mappedPath === '' ? mappedPath : `/${mappedPath}`;
|
|
1674
|
+
const baseHasVersionSuffix = /\/v\d+$/i.test(trimmedBase);
|
|
1675
|
+
const pathHasVersionPrefix = /^\/v\d+(?:\/|$)/i.test(normalizedPath);
|
|
1676
|
+
if (baseHasVersionSuffix || pathHasVersionPrefix) {
|
|
1677
|
+
return `${trimmedBase}${normalizedPath}`;
|
|
1678
|
+
}
|
|
1679
|
+
return `${trimmedBase}/v1${normalizedPath}`;
|
|
1680
|
+
}
|
|
1554
1681
|
/**
|
|
1555
1682
|
* 构建 Gemini API 的完整 URL
|
|
1556
1683
|
* 用户只填写 base 地址(如 https://generativelanguage.googleapis.com)
|
|
@@ -1938,6 +2065,7 @@ class ProxyServer {
|
|
|
1938
2065
|
const targetType = route.targetType;
|
|
1939
2066
|
const failoverEnabled = (options === null || options === void 0 ? void 0 : options.failoverEnabled) === true;
|
|
1940
2067
|
const forwardedToServiceName = options === null || options === void 0 ? void 0 : options.forwardedToServiceName;
|
|
2068
|
+
const useOriginalConfig = (options === null || options === void 0 ? void 0 : options.useOriginalConfig) === true;
|
|
1941
2069
|
let requestBody = req.body || {};
|
|
1942
2070
|
let usageForLog;
|
|
1943
2071
|
let logged = false;
|
|
@@ -2106,6 +2234,7 @@ class ProxyServer {
|
|
|
2106
2234
|
vendorId: service.vendorId,
|
|
2107
2235
|
vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
|
|
2108
2236
|
requestModel,
|
|
2237
|
+
tags: useOriginalConfig ? ['使用原始配置'] : undefined,
|
|
2109
2238
|
responseHeaders: responseHeadersForLog,
|
|
2110
2239
|
responseBody: responseBodyForLog,
|
|
2111
2240
|
streamChunks: streamChunksForLog,
|
|
@@ -2282,6 +2411,10 @@ class ProxyServer {
|
|
|
2282
2411
|
const model = requestBody.model || rule.targetModel || 'gemini-pro';
|
|
2283
2412
|
upstreamUrl = this.buildGeminiUrl(service.apiUrl, model, streamRequested);
|
|
2284
2413
|
}
|
|
2414
|
+
else if (sourceType === 'openai') {
|
|
2415
|
+
// OpenAI Responses 兼容模式:自动处理 baseUrl 是否包含 /v{number}
|
|
2416
|
+
upstreamUrl = this.buildOpenAIResponsesUrl(service.apiUrl, mappedPath);
|
|
2417
|
+
}
|
|
2285
2418
|
else if (this.isChatType(sourceType) || this.isGeminiChatSource(sourceType)) {
|
|
2286
2419
|
// Chat 类型(包括 gemini-chat)直接使用用户配置的完整 URL
|
|
2287
2420
|
upstreamUrl = service.apiUrl;
|
|
@@ -446,11 +446,11 @@ const transformClaudeRequestToOpenAIChat = (body, targetModel) => {
|
|
|
446
446
|
openaiBody.thinking = { type: claudeThinking.type };
|
|
447
447
|
}
|
|
448
448
|
// 为 OpenAI Responses API 添加 reasoning 配置
|
|
449
|
-
// 映射关系:enabled->medium, disabled->
|
|
449
|
+
// 映射关系:enabled->medium, disabled->low, auto->low
|
|
450
450
|
if (claudeThinking.type) {
|
|
451
451
|
const effortMap = {
|
|
452
452
|
'enabled': 'medium',
|
|
453
|
-
'disabled': '
|
|
453
|
+
'disabled': 'low',
|
|
454
454
|
'auto': 'low'
|
|
455
455
|
};
|
|
456
456
|
openaiBody.reasoning = {
|