aicodeswitch 5.1.2 → 5.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/bin/restore.js +14 -7
- package/bin/utils/managed-fields.js +62 -0
- package/dist/server/access-keys/index.js +173 -0
- package/dist/server/access-keys/key-logger.js +358 -0
- package/dist/server/access-keys/key-resolver.js +51 -0
- package/dist/server/access-keys/key-session-tracker.js +217 -0
- package/dist/server/access-keys/manager.js +206 -0
- package/dist/server/access-keys/policy-manager.js +144 -0
- package/dist/server/access-keys/quota-checker.js +197 -0
- package/dist/server/access-keys/usage-tracker.js +279 -0
- package/dist/server/auth.js +16 -4
- package/dist/server/coding-plan-headers.js +121 -0
- package/dist/server/config-managed-fields.js +2 -0
- package/dist/server/conversions/index.js +8 -0
- package/dist/server/conversions/utils/tool-result.js +35 -0
- package/dist/server/fs-database.js +72 -1
- package/dist/server/main.js +1162 -13
- package/dist/server/proxy-server.js +662 -128
- package/dist/server/rules-status-service.js +32 -3
- package/dist/server/session-launcher.js +282 -0
- package/dist/server/session-migration.js +419 -0
- package/dist/server/transformers/chunk-collector.js +28 -1
- package/dist/server/transformers/model-rewrite-transform.js +128 -0
- package/dist/ui/assets/claude-XtpLmGtF.webp +0 -0
- package/dist/ui/assets/index-Cws89pD2.js +828 -0
- package/dist/ui/assets/index-CzfKxImD.css +1 -0
- package/dist/ui/assets/openai-CPEiZpaN.webp +0 -0
- package/dist/ui/index.html +2 -2
- package/package.json +1 -1
- package/dist/ui/assets/index-BHR12ImE.css +0 -1
- package/dist/ui/assets/index-CumAhpXg.js +0 -517
package/dist/server/main.js
CHANGED
|
@@ -21,6 +21,9 @@ const crypto_1 = require("crypto");
|
|
|
21
21
|
const https_proxy_agent_1 = require("https-proxy-agent");
|
|
22
22
|
const database_factory_1 = require("./database-factory");
|
|
23
23
|
const proxy_server_1 = require("./proxy-server");
|
|
24
|
+
const index_1 = require("./access-keys/index");
|
|
25
|
+
const manager_1 = require("./access-keys/manager");
|
|
26
|
+
const policy_manager_1 = require("./access-keys/policy-manager");
|
|
24
27
|
const os_1 = __importDefault(require("os"));
|
|
25
28
|
const auth_1 = require("./auth");
|
|
26
29
|
const version_check_1 = require("./version-check");
|
|
@@ -33,6 +36,8 @@ const config_metadata_1 = require("./config-metadata");
|
|
|
33
36
|
const config_merge_1 = require("./config-merge");
|
|
34
37
|
const config_managed_fields_1 = require("./config-managed-fields");
|
|
35
38
|
const config_1 = require("./config");
|
|
39
|
+
const session_migration_1 = require("./session-migration");
|
|
40
|
+
const session_launcher_1 = require("./session-launcher");
|
|
36
41
|
const appDir = path_1.default.join(os_1.default.homedir(), '.aicodeswitch');
|
|
37
42
|
const legacyDataDir = path_1.default.join(appDir, 'data');
|
|
38
43
|
const dataDir = path_1.default.join(appDir, 'fs-db');
|
|
@@ -41,7 +46,7 @@ const upgradeHashFilePath = path_1.default.join(appDir, 'upgrade-hash');
|
|
|
41
46
|
if (fs_1.default.existsSync(dotenvPath)) {
|
|
42
47
|
dotenv_1.default.config({ path: dotenvPath });
|
|
43
48
|
}
|
|
44
|
-
const host = process.env.HOST || '
|
|
49
|
+
const host = process.env.HOST || '0.0.0.0';
|
|
45
50
|
const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 4567;
|
|
46
51
|
let globalProxyConfig = null;
|
|
47
52
|
function updateProxyConfig(config) {
|
|
@@ -74,6 +79,112 @@ function getProxyAgent() {
|
|
|
74
79
|
return null;
|
|
75
80
|
}
|
|
76
81
|
}
|
|
82
|
+
// ============================================================================
|
|
83
|
+
// 写入本地记录持久化
|
|
84
|
+
// ============================================================================
|
|
85
|
+
const getWriteLocalRecordsFile = () => path_1.default.join(dataDir, 'write-local-records.json');
|
|
86
|
+
function loadWriteLocalRecords() {
|
|
87
|
+
try {
|
|
88
|
+
const filePath = getWriteLocalRecordsFile();
|
|
89
|
+
if (fs_1.default.existsSync(filePath)) {
|
|
90
|
+
return JSON.parse(fs_1.default.readFileSync(filePath, 'utf-8'));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch ( /* ignore */_a) { /* ignore */ }
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
function saveWriteLocalRecords(records) {
|
|
97
|
+
const filePath = getWriteLocalRecordsFile();
|
|
98
|
+
if (!fs_1.default.existsSync(dataDir)) {
|
|
99
|
+
fs_1.default.mkdirSync(dataDir, { recursive: true });
|
|
100
|
+
}
|
|
101
|
+
(0, config_merge_1.atomicWriteFile)(filePath, JSON.stringify(records, null, 2));
|
|
102
|
+
}
|
|
103
|
+
function addWriteLocalRecord(accessKeyId, targets) {
|
|
104
|
+
const records = loadWriteLocalRecords();
|
|
105
|
+
const existing = records.find(r => r.accessKeyId === accessKeyId);
|
|
106
|
+
if (existing) {
|
|
107
|
+
for (const t of targets) {
|
|
108
|
+
if (!existing.targets.includes(t))
|
|
109
|
+
existing.targets.push(t);
|
|
110
|
+
}
|
|
111
|
+
existing.timestamp = Date.now();
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
records.push({ accessKeyId, targets: [...targets], timestamp: Date.now() });
|
|
115
|
+
}
|
|
116
|
+
saveWriteLocalRecords(records);
|
|
117
|
+
}
|
|
118
|
+
function removeWriteLocalRecords(accessKeyId) {
|
|
119
|
+
const records = loadWriteLocalRecords().filter(r => r.accessKeyId !== accessKeyId);
|
|
120
|
+
saveWriteLocalRecords(records);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* 从持久化记录中恢复已写入本地的 AccessKey
|
|
124
|
+
* 每次代理配置写入后调用,确保 AccessKey 不会被占位符覆盖
|
|
125
|
+
*/
|
|
126
|
+
function applyWriteLocalRecords(proxyServer) {
|
|
127
|
+
const accessKeyModule = proxyServer.getAccessKeyModule();
|
|
128
|
+
if (!accessKeyModule)
|
|
129
|
+
return;
|
|
130
|
+
const records = loadWriteLocalRecords();
|
|
131
|
+
if (records.length === 0)
|
|
132
|
+
return;
|
|
133
|
+
let changed = false;
|
|
134
|
+
const homeDir = os_1.default.homedir();
|
|
135
|
+
const remainingRecords = records.filter(record => {
|
|
136
|
+
const key = accessKeyModule.keyManager.get(record.accessKeyId);
|
|
137
|
+
if (!key || key.status !== 'active') {
|
|
138
|
+
changed = true;
|
|
139
|
+
return false; // 密钥已删除或停用,移除记录
|
|
140
|
+
}
|
|
141
|
+
for (const target of record.targets) {
|
|
142
|
+
try {
|
|
143
|
+
if (target === 'claude-code') {
|
|
144
|
+
const claudeDir = path_1.default.join(homeDir, '.claude');
|
|
145
|
+
const settingsPath = path_1.default.join(claudeDir, 'settings.json');
|
|
146
|
+
if (!fs_1.default.existsSync(claudeDir)) {
|
|
147
|
+
fs_1.default.mkdirSync(claudeDir, { recursive: true });
|
|
148
|
+
}
|
|
149
|
+
let settings = {};
|
|
150
|
+
if (fs_1.default.existsSync(settingsPath)) {
|
|
151
|
+
try {
|
|
152
|
+
settings = JSON.parse(fs_1.default.readFileSync(settingsPath, 'utf-8'));
|
|
153
|
+
}
|
|
154
|
+
catch ( /* ignore */_a) { /* ignore */ }
|
|
155
|
+
}
|
|
156
|
+
if (!settings.env)
|
|
157
|
+
settings.env = {};
|
|
158
|
+
settings.env.ANTHROPIC_AUTH_TOKEN = key.apiKey;
|
|
159
|
+
(0, config_merge_1.atomicWriteFile)(settingsPath, JSON.stringify(settings, null, 2));
|
|
160
|
+
}
|
|
161
|
+
else if (target === 'codex') {
|
|
162
|
+
const codexDir = path_1.default.join(homeDir, '.codex');
|
|
163
|
+
const authPath = path_1.default.join(codexDir, 'auth.json');
|
|
164
|
+
if (!fs_1.default.existsSync(codexDir)) {
|
|
165
|
+
fs_1.default.mkdirSync(codexDir, { recursive: true });
|
|
166
|
+
}
|
|
167
|
+
let auth = {};
|
|
168
|
+
if (fs_1.default.existsSync(authPath)) {
|
|
169
|
+
try {
|
|
170
|
+
auth = JSON.parse(fs_1.default.readFileSync(authPath, 'utf-8'));
|
|
171
|
+
}
|
|
172
|
+
catch ( /* ignore */_b) { /* ignore */ }
|
|
173
|
+
}
|
|
174
|
+
auth.OPENAI_API_KEY = key.apiKey;
|
|
175
|
+
(0, config_merge_1.atomicWriteFile)(authPath, JSON.stringify(auth, null, 2));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
console.error(`[WriteLocal] Failed to apply key ${record.accessKeyId} to ${target}:`, error);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return true;
|
|
183
|
+
});
|
|
184
|
+
if (changed) {
|
|
185
|
+
saveWriteLocalRecords(remainingRecords);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
77
188
|
const app = (0, express_1.default)();
|
|
78
189
|
app.use((0, cors_1.default)());
|
|
79
190
|
app.use(express_1.default.json({ limit: 'Infinity' }));
|
|
@@ -114,12 +225,11 @@ const isClaudeEffortLevel = (value) => {
|
|
|
114
225
|
const isValidAutocompactPct = (v) => {
|
|
115
226
|
return typeof v === 'number' && Number.isInteger(v) && v >= 1 && v <= 100;
|
|
116
227
|
};
|
|
117
|
-
const writeClaudeConfig = (
|
|
228
|
+
const writeClaudeConfig = (_dbManager_1, enableAgentTeams_1, enableBypassPermissionsSupport_1, effortLevel_1, defaultModel_1, autocompactPctOverride_1, ...args_1) => __awaiter(void 0, [_dbManager_1, enableAgentTeams_1, enableBypassPermissionsSupport_1, effortLevel_1, defaultModel_1, autocompactPctOverride_1, ...args_1], void 0, function* (_dbManager, enableAgentTeams, enableBypassPermissionsSupport, effortLevel, defaultModel, autocompactPctOverride, options = {}) {
|
|
118
229
|
var _a;
|
|
119
230
|
try {
|
|
120
231
|
const homeDir = os_1.default.homedir();
|
|
121
232
|
const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 4567;
|
|
122
|
-
const config = dbManager.getConfig();
|
|
123
233
|
// Claude Code settings.json
|
|
124
234
|
const claudeDir = path_1.default.join(homeDir, '.claude');
|
|
125
235
|
const claudeSettingsPath = path_1.default.join(claudeDir, 'settings.json');
|
|
@@ -166,11 +276,11 @@ const writeClaudeConfig = (dbManager_1, enableAgentTeams_1, enableBypassPermissi
|
|
|
166
276
|
}
|
|
167
277
|
// 构建代理配置
|
|
168
278
|
const claudeSettingsEnv = {
|
|
169
|
-
ANTHROPIC_AUTH_TOKEN:
|
|
170
|
-
ANTHROPIC_API_KEY: "",
|
|
279
|
+
ANTHROPIC_AUTH_TOKEN: "api_key",
|
|
171
280
|
ANTHROPIC_BASE_URL: `http://${host}:${port}/claude-code`,
|
|
172
281
|
API_TIMEOUT_MS: "3000000",
|
|
173
|
-
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: 1
|
|
282
|
+
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: 1,
|
|
283
|
+
CLAUDE_CODE_MAX_RETRIES: 3
|
|
174
284
|
};
|
|
175
285
|
// 如果启用Agent Teams功能,添加对应的环境变量
|
|
176
286
|
if (enableAgentTeams) {
|
|
@@ -262,11 +372,10 @@ const DEFAULT_CODEX_REASONING_EFFORT = 'high';
|
|
|
262
372
|
const isCodexReasoningEffort = (value) => {
|
|
263
373
|
return typeof value === 'string' && VALID_CODEX_REASONING_EFFORTS.includes(value);
|
|
264
374
|
};
|
|
265
|
-
const writeCodexConfig = (
|
|
375
|
+
const writeCodexConfig = (_dbManager_1, ...args_1) => __awaiter(void 0, [_dbManager_1, ...args_1], void 0, function* (_dbManager, modelReasoningEffort = DEFAULT_CODEX_REASONING_EFFORT, codexDefaultModel, options = {}) {
|
|
266
376
|
var _a;
|
|
267
377
|
try {
|
|
268
378
|
const homeDir = os_1.default.homedir();
|
|
269
|
-
const config = dbManager.getConfig();
|
|
270
379
|
// Codex config.toml
|
|
271
380
|
const codexDir = path_1.default.join(homeDir, '.codex');
|
|
272
381
|
const codexConfigPath = path_1.default.join(codexDir, 'config.toml');
|
|
@@ -324,7 +433,9 @@ const writeCodexConfig = (dbManager_1, ...args_1) => __awaiter(void 0, [dbManage
|
|
|
324
433
|
aicodeswitch: {
|
|
325
434
|
name: "aicodeswitch",
|
|
326
435
|
base_url: `http://${host}:${process.env.PORT ? parseInt(process.env.PORT, 10) : 4567}/codex`,
|
|
327
|
-
wire_api: "responses"
|
|
436
|
+
wire_api: "responses",
|
|
437
|
+
stream_max_retries: 3,
|
|
438
|
+
stream_retry_backoff: "fixed"
|
|
328
439
|
}
|
|
329
440
|
}
|
|
330
441
|
};
|
|
@@ -354,7 +465,7 @@ const writeCodexConfig = (dbManager_1, ...args_1) => __awaiter(void 0, [dbManage
|
|
|
354
465
|
}
|
|
355
466
|
// 构建代理配置
|
|
356
467
|
const proxyAuth = {
|
|
357
|
-
OPENAI_API_KEY:
|
|
468
|
+
OPENAI_API_KEY: "api_key"
|
|
358
469
|
};
|
|
359
470
|
// 使用智能合并
|
|
360
471
|
const mergedAuth = (0, config_merge_1.mergeJsonConfig)(proxyAuth, currentAuth, config_managed_fields_1.CODEX_AUTH_MANAGED_FIELDS);
|
|
@@ -388,6 +499,7 @@ const writeCodexConfig = (dbManager_1, ...args_1) => __awaiter(void 0, [dbManage
|
|
|
388
499
|
}
|
|
389
500
|
});
|
|
390
501
|
const restoreClaudeConfig = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
502
|
+
var _a, _b;
|
|
391
503
|
try {
|
|
392
504
|
const homeDir = os_1.default.homedir();
|
|
393
505
|
let restoredAnyFile = false;
|
|
@@ -408,6 +520,14 @@ const restoreClaudeConfig = () => __awaiter(void 0, void 0, void 0, function* ()
|
|
|
408
520
|
console.warn('Failed to parse current settings.json during restore, using empty object:', error);
|
|
409
521
|
}
|
|
410
522
|
}
|
|
523
|
+
// 防御性清理:移除 currentSettings 中 ANTHROPIC_API_KEY 的空值,
|
|
524
|
+
// 防止旧版本代理写入的空值覆盖 backup 中的真实 Key
|
|
525
|
+
if (((_a = currentSettings === null || currentSettings === void 0 ? void 0 : currentSettings.env) === null || _a === void 0 ? void 0 : _a.ANTHROPIC_API_KEY) === '' && ((_b = backupSettings === null || backupSettings === void 0 ? void 0 : backupSettings.env) === null || _b === void 0 ? void 0 : _b.ANTHROPIC_API_KEY)) {
|
|
526
|
+
delete currentSettings.env.ANTHROPIC_API_KEY;
|
|
527
|
+
if (Object.keys(currentSettings.env).length === 0) {
|
|
528
|
+
delete currentSettings.env;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
411
531
|
// 生成合并后的配置(备份作为基础,合并当前的非管理字段)
|
|
412
532
|
const mergedSettings = (0, config_merge_1.mergeJsonConfig)(backupSettings, currentSettings, config_managed_fields_1.CLAUDE_SETTINGS_MANAGED_FIELDS);
|
|
413
533
|
// 原子性写入合并后的配置
|
|
@@ -925,6 +1045,21 @@ const listInstalledSkills = () => {
|
|
|
925
1045
|
const registerRoutes = (dbManager, proxyServer) => __awaiter(void 0, void 0, void 0, function* () {
|
|
926
1046
|
updateProxyConfig(dbManager.getConfig());
|
|
927
1047
|
app.get('/health', (_req, res) => res.json({ status: 'ok' }));
|
|
1048
|
+
// 局域网访问控制中间件:当 enableLanDiscovery 关闭时,仅允许本机访问 /api/* 路由
|
|
1049
|
+
app.use('/api', (req, res, next) => {
|
|
1050
|
+
const config = dbManager.getConfig();
|
|
1051
|
+
if (config.enableLanDiscovery) {
|
|
1052
|
+
return next();
|
|
1053
|
+
}
|
|
1054
|
+
const clientIp = req.ip || req.socket.remoteAddress || '';
|
|
1055
|
+
// 规范化 IPv4-mapped IPv6 地址 (::ffff:127.0.0.1 -> 127.0.0.1)
|
|
1056
|
+
const normalizedIp = clientIp.replace(/^::ffff:/, '');
|
|
1057
|
+
if (normalizedIp === '127.0.0.1' || normalizedIp === '::1' || normalizedIp === 'localhost') {
|
|
1058
|
+
return next();
|
|
1059
|
+
}
|
|
1060
|
+
console.warn(`[LAN Guard] 拒绝非本机访问: ${clientIp} -> ${req.method} ${req.path}`);
|
|
1061
|
+
res.status(403).json({ error: 'LAN access is disabled. Only local access is allowed.' });
|
|
1062
|
+
});
|
|
928
1063
|
// 鉴权相关路由 - 公开访问
|
|
929
1064
|
app.get('/api/auth/status', (_req, res) => {
|
|
930
1065
|
const response = { enabled: (0, auth_1.isAuthEnabled)() };
|
|
@@ -945,10 +1080,10 @@ const registerRoutes = (dbManager, proxyServer) => __awaiter(void 0, void 0, voi
|
|
|
945
1080
|
res.status(401).json({ error: 'Invalid auth code' });
|
|
946
1081
|
}
|
|
947
1082
|
});
|
|
948
|
-
// 鉴权中间件 - 保护所有 /api/* 路由 (除了 /api/auth/*)
|
|
1083
|
+
// 鉴权中间件 - 保护所有 /api/* 路由 (除了 /api/auth/* 和 /api/lan/discover)
|
|
949
1084
|
app.use('/api', (req, res, next) => {
|
|
950
|
-
if (req.path.startsWith('/auth/')) {
|
|
951
|
-
next(); // /api/auth/* 路由不需要鉴权
|
|
1085
|
+
if (req.path.startsWith('/auth/') || req.path === '/lan/discover') {
|
|
1086
|
+
next(); // /api/auth/* 和 /api/lan/discover 路由不需要鉴权
|
|
952
1087
|
}
|
|
953
1088
|
else {
|
|
954
1089
|
(0, auth_1.authMiddleware)(req, res, next);
|
|
@@ -1135,6 +1270,48 @@ const registerRoutes = (dbManager, proxyServer) => __awaiter(void 0, void 0, voi
|
|
|
1135
1270
|
});
|
|
1136
1271
|
res.json(allStatuses);
|
|
1137
1272
|
})));
|
|
1273
|
+
// SSE 端点:实时推送规则状态变更
|
|
1274
|
+
app.get('/api/rules/status/stream', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1275
|
+
// 设置 SSE 响应头
|
|
1276
|
+
res.writeHead(200, {
|
|
1277
|
+
'Content-Type': 'text/event-stream',
|
|
1278
|
+
'Cache-Control': 'no-cache',
|
|
1279
|
+
'Connection': 'keep-alive',
|
|
1280
|
+
'X-Accel-Buffering': 'no', // 防止 nginx 等代理缓冲
|
|
1281
|
+
});
|
|
1282
|
+
// 连接时立即发送完整快照(init 事件)
|
|
1283
|
+
const allRules = dbManager.getRules();
|
|
1284
|
+
const statusMap = rules_status_service_1.rulesStatusBroadcaster.getAllRuleStatuses();
|
|
1285
|
+
const statusMapByRuleId = new Map(statusMap.map(status => [status.ruleId, status]));
|
|
1286
|
+
const allStatuses = allRules.map(rule => {
|
|
1287
|
+
const existingStatus = statusMapByRuleId.get(rule.id);
|
|
1288
|
+
if (existingStatus) {
|
|
1289
|
+
return existingStatus;
|
|
1290
|
+
}
|
|
1291
|
+
else {
|
|
1292
|
+
return {
|
|
1293
|
+
ruleId: rule.id,
|
|
1294
|
+
status: 'idle',
|
|
1295
|
+
timestamp: Date.now(),
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
});
|
|
1299
|
+
res.write(`data: ${JSON.stringify({ type: 'init', statuses: allStatuses })}\n\n`);
|
|
1300
|
+
// 监听状态变更,推送增量更新
|
|
1301
|
+
const onChange = (data) => {
|
|
1302
|
+
res.write(`data: ${JSON.stringify({ type: 'update', status: data })}\n\n`);
|
|
1303
|
+
};
|
|
1304
|
+
rules_status_service_1.rulesStatusBroadcaster.on('statusChanged', onChange);
|
|
1305
|
+
// 3 秒心跳,用于客户端检测连接存活状态
|
|
1306
|
+
const heartbeat = setInterval(() => {
|
|
1307
|
+
res.write(`data: ${JSON.stringify({ type: 'heartbeat', timestamp: Date.now() })}\n\n`);
|
|
1308
|
+
}, 3000);
|
|
1309
|
+
// 客户端断开时清理
|
|
1310
|
+
req.on('close', () => {
|
|
1311
|
+
rules_status_service_1.rulesStatusBroadcaster.off('statusChanged', onChange);
|
|
1312
|
+
clearInterval(heartbeat);
|
|
1313
|
+
});
|
|
1314
|
+
})));
|
|
1138
1315
|
// 清除规则的错误状态(广播 idle 状态给所有客户端)
|
|
1139
1316
|
app.post('/api/rules/:id/clear-status', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1140
1317
|
const { id } = req.params;
|
|
@@ -1250,9 +1427,236 @@ const registerRoutes = (dbManager, proxyServer) => __awaiter(void 0, void 0, voi
|
|
|
1250
1427
|
yield proxyServer.updateConfig(latestConfig);
|
|
1251
1428
|
updateProxyConfig(latestConfig);
|
|
1252
1429
|
yield syncConfigsOnGlobalConfigUpdate(dbManager);
|
|
1430
|
+
applyWriteLocalRecords(proxyServer);
|
|
1253
1431
|
}
|
|
1254
1432
|
res.json(result);
|
|
1255
1433
|
})));
|
|
1434
|
+
// ===================== 局域网同步相关 =====================
|
|
1435
|
+
// GET /api/lan/discover - 远端节点暴露配置数据(不受鉴权保护,由开关控制)
|
|
1436
|
+
app.get('/api/lan/discover', (_req, res) => {
|
|
1437
|
+
const config = dbManager.getConfig();
|
|
1438
|
+
if (!config.enableLanDiscovery) {
|
|
1439
|
+
res.status(404).json({ error: 'Not found' });
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1442
|
+
try {
|
|
1443
|
+
// 收集 Skills
|
|
1444
|
+
const installedSkills = listInstalledSkills();
|
|
1445
|
+
const skills = [];
|
|
1446
|
+
for (const skill of installedSkills) {
|
|
1447
|
+
const skillItem = {
|
|
1448
|
+
name: skill.name,
|
|
1449
|
+
description: skill.description,
|
|
1450
|
+
targets: skill.targets,
|
|
1451
|
+
githubUrl: skill.githubUrl,
|
|
1452
|
+
skillPath: skill.skillPath,
|
|
1453
|
+
};
|
|
1454
|
+
// 尝试读取 SKILL.md 内容
|
|
1455
|
+
const skillDir = path_1.default.join(getCentralSkillsDir(), skill.id);
|
|
1456
|
+
const skillMdPath = path_1.default.join(skillDir, 'SKILL.md');
|
|
1457
|
+
if (fs_1.default.existsSync(skillMdPath)) {
|
|
1458
|
+
try {
|
|
1459
|
+
skillItem.instruction = fs_1.default.readFileSync(skillMdPath, 'utf-8');
|
|
1460
|
+
}
|
|
1461
|
+
catch ( /* ignore */_a) { /* ignore */ }
|
|
1462
|
+
}
|
|
1463
|
+
skills.push(skillItem);
|
|
1464
|
+
}
|
|
1465
|
+
// 收集 MCPs(脱敏 headers 和 env 中的敏感信息)
|
|
1466
|
+
const SENSITIVE_KEY_PATTERN = /api_key|apikey|access_token|accesstoken|auth|key|token|secret|password|private_key|privatekey|credentials/i;
|
|
1467
|
+
const allMcps = dbManager.getMCPs();
|
|
1468
|
+
const mcps = allMcps.map(mcp => {
|
|
1469
|
+
const sanitized = {
|
|
1470
|
+
name: mcp.name,
|
|
1471
|
+
description: mcp.description,
|
|
1472
|
+
type: mcp.type,
|
|
1473
|
+
command: mcp.command,
|
|
1474
|
+
args: mcp.args,
|
|
1475
|
+
targets: mcp.targets,
|
|
1476
|
+
};
|
|
1477
|
+
if (mcp.url)
|
|
1478
|
+
sanitized.url = mcp.url;
|
|
1479
|
+
// 脱敏 env
|
|
1480
|
+
if (mcp.env) {
|
|
1481
|
+
const sanitizedEnv = {};
|
|
1482
|
+
for (const [key, value] of Object.entries(mcp.env)) {
|
|
1483
|
+
if (SENSITIVE_KEY_PATTERN.test(key)) {
|
|
1484
|
+
sanitizedEnv[key] = '***';
|
|
1485
|
+
}
|
|
1486
|
+
else {
|
|
1487
|
+
sanitizedEnv[key] = value;
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
sanitized.env = sanitizedEnv;
|
|
1491
|
+
}
|
|
1492
|
+
// 脱敏 headers
|
|
1493
|
+
if (mcp.headers) {
|
|
1494
|
+
const sanitizedHeaders = {};
|
|
1495
|
+
for (const [key, value] of Object.entries(mcp.headers)) {
|
|
1496
|
+
if (SENSITIVE_KEY_PATTERN.test(key)) {
|
|
1497
|
+
sanitizedHeaders[key] = '***';
|
|
1498
|
+
}
|
|
1499
|
+
else {
|
|
1500
|
+
sanitizedHeaders[key] = value;
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
sanitized.headers = sanitizedHeaders;
|
|
1504
|
+
}
|
|
1505
|
+
return sanitized;
|
|
1506
|
+
});
|
|
1507
|
+
// 读取版本号
|
|
1508
|
+
let version = 'unknown';
|
|
1509
|
+
try {
|
|
1510
|
+
const pkgPath = path_1.default.join(__dirname, '..', 'package.json');
|
|
1511
|
+
const pkg = JSON.parse(fs_1.default.readFileSync(pkgPath, 'utf-8'));
|
|
1512
|
+
version = pkg.version || 'unknown';
|
|
1513
|
+
}
|
|
1514
|
+
catch ( /* ignore */_b) { /* ignore */ }
|
|
1515
|
+
res.json({
|
|
1516
|
+
node: {
|
|
1517
|
+
name: os_1.default.hostname(),
|
|
1518
|
+
version,
|
|
1519
|
+
port: process.env.PORT ? parseInt(process.env.PORT) : 4567,
|
|
1520
|
+
},
|
|
1521
|
+
skills,
|
|
1522
|
+
mcps,
|
|
1523
|
+
});
|
|
1524
|
+
}
|
|
1525
|
+
catch (error) {
|
|
1526
|
+
console.error('[LAN Discover] 错误:', error);
|
|
1527
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
1528
|
+
}
|
|
1529
|
+
});
|
|
1530
|
+
// GET /api/lan/scan - 获取本机局域网 IP 信息
|
|
1531
|
+
app.get('/api/lan/scan', (_req, res) => {
|
|
1532
|
+
const interfaces = os_1.default.networkInterfaces();
|
|
1533
|
+
const port = process.env.PORT ? parseInt(process.env.PORT) : 4567;
|
|
1534
|
+
// 收集所有非内部 IPv4 网络接口
|
|
1535
|
+
const networkInterfaces = [];
|
|
1536
|
+
let localIp = '127.0.0.1';
|
|
1537
|
+
let subnet = '127.0.0';
|
|
1538
|
+
for (const [ifaceName, entries] of Object.entries(interfaces)) {
|
|
1539
|
+
if (!entries)
|
|
1540
|
+
continue;
|
|
1541
|
+
for (const entry of entries) {
|
|
1542
|
+
if (entry.family === 'IPv4' && !entry.internal) {
|
|
1543
|
+
const entrySubnet = entry.address.split('.').slice(0, 3).join('.');
|
|
1544
|
+
networkInterfaces.push({
|
|
1545
|
+
name: ifaceName,
|
|
1546
|
+
address: entry.address,
|
|
1547
|
+
subnet: entrySubnet,
|
|
1548
|
+
netmask: entry.netmask,
|
|
1549
|
+
});
|
|
1550
|
+
// 兼容旧逻辑:取第一个非内部接口作为默认值
|
|
1551
|
+
if (localIp === '127.0.0.1') {
|
|
1552
|
+
localIp = entry.address;
|
|
1553
|
+
subnet = entrySubnet;
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
res.json({ localIp, subnet, port, networkInterfaces });
|
|
1559
|
+
});
|
|
1560
|
+
// POST /api/lan/sync - 执行同步写入
|
|
1561
|
+
app.post('/api/lan/sync', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1562
|
+
const { remoteNode, skills, mcps, vendor } = req.body;
|
|
1563
|
+
const result = {
|
|
1564
|
+
skillsImported: 0,
|
|
1565
|
+
mcpsImported: 0,
|
|
1566
|
+
vendorCreated: false,
|
|
1567
|
+
vendorName: '',
|
|
1568
|
+
servicesCreated: 0,
|
|
1569
|
+
};
|
|
1570
|
+
// 1. 同步 Skills
|
|
1571
|
+
const centralDir = getCentralSkillsDir();
|
|
1572
|
+
if (!fs_1.default.existsSync(centralDir)) {
|
|
1573
|
+
fs_1.default.mkdirSync(centralDir, { recursive: true });
|
|
1574
|
+
}
|
|
1575
|
+
const existingSkills = listInstalledSkills();
|
|
1576
|
+
const existingSkillNames = new Set(existingSkills.map(s => s.name));
|
|
1577
|
+
for (const skill of skills) {
|
|
1578
|
+
if (existingSkillNames.has(skill.name))
|
|
1579
|
+
continue; // 防御性跳过
|
|
1580
|
+
const dirName = sanitizeDirName(skill.name);
|
|
1581
|
+
const skillDir = path_1.default.join(centralDir, dirName);
|
|
1582
|
+
if (!fs_1.default.existsSync(skillDir)) {
|
|
1583
|
+
fs_1.default.mkdirSync(skillDir, { recursive: true });
|
|
1584
|
+
}
|
|
1585
|
+
// 写入 skill.json
|
|
1586
|
+
const skillJson = {
|
|
1587
|
+
id: dirName,
|
|
1588
|
+
name: skill.name,
|
|
1589
|
+
description: skill.description || '',
|
|
1590
|
+
targets: skill.targets || [],
|
|
1591
|
+
enabledTargets: [], // 不自动选中编程工具,由用户自行配置
|
|
1592
|
+
githubUrl: skill.githubUrl,
|
|
1593
|
+
skillPath: skill.skillPath,
|
|
1594
|
+
installedAt: Date.now(),
|
|
1595
|
+
};
|
|
1596
|
+
fs_1.default.writeFileSync(path_1.default.join(skillDir, 'skill.json'), JSON.stringify(skillJson, null, 2));
|
|
1597
|
+
// 写入 SKILL.md(如果有 instruction)
|
|
1598
|
+
if (skill.instruction) {
|
|
1599
|
+
fs_1.default.writeFileSync(path_1.default.join(skillDir, 'SKILL.md'), skill.instruction);
|
|
1600
|
+
}
|
|
1601
|
+
result.skillsImported++;
|
|
1602
|
+
}
|
|
1603
|
+
// 2. 同步 MCPs
|
|
1604
|
+
const existingMcps = dbManager.getMCPs();
|
|
1605
|
+
const existingMcpNames = new Set(existingMcps.map(m => m.name));
|
|
1606
|
+
for (const mcp of mcps) {
|
|
1607
|
+
if (existingMcpNames.has(mcp.name))
|
|
1608
|
+
continue; // 防御性跳过
|
|
1609
|
+
yield dbManager.createMCP({
|
|
1610
|
+
name: mcp.name,
|
|
1611
|
+
description: mcp.description,
|
|
1612
|
+
type: mcp.type,
|
|
1613
|
+
command: mcp.command,
|
|
1614
|
+
args: mcp.args,
|
|
1615
|
+
url: mcp.url,
|
|
1616
|
+
headers: mcp.headers,
|
|
1617
|
+
env: mcp.env,
|
|
1618
|
+
targets: [], // 不自动选中编程工具,由用户自行配置
|
|
1619
|
+
});
|
|
1620
|
+
result.mcpsImported++;
|
|
1621
|
+
}
|
|
1622
|
+
// 3. 可选:创建供应商(将远端节点的代理路径映射为固定 API 服务)
|
|
1623
|
+
if (vendor.enabled) {
|
|
1624
|
+
const vendorName = `${remoteNode.name}@${remoteNode.ip}`;
|
|
1625
|
+
const remoteBaseUrl = `http://${remoteNode.ip}:${remoteNode.port}`;
|
|
1626
|
+
// 固定的代理路径到 API 服务映射
|
|
1627
|
+
// 规则:Claude/Responses/Gemini 标准接口只填 baseurl,Chat Completions 需完整路径
|
|
1628
|
+
const LAN_PROXY_SERVICES = [
|
|
1629
|
+
{ name: 'Claude Code', sourceType: 'claude', apiUrl: `${remoteBaseUrl}/claude-code` },
|
|
1630
|
+
{ name: 'Codex', sourceType: 'openai', apiUrl: `${remoteBaseUrl}/codex` },
|
|
1631
|
+
{ name: 'Claude 标准接口', sourceType: 'claude', apiUrl: remoteBaseUrl },
|
|
1632
|
+
{ name: 'Responses 标准接口', sourceType: 'openai', apiUrl: remoteBaseUrl },
|
|
1633
|
+
{ name: 'Chat Completions 标准接口', sourceType: 'openai-chat', apiUrl: `${remoteBaseUrl}/v1/chat/completions` },
|
|
1634
|
+
{ name: 'Gemini 标准接口', sourceType: 'gemini', apiUrl: remoteBaseUrl },
|
|
1635
|
+
];
|
|
1636
|
+
// 检查供应商是否已存在
|
|
1637
|
+
const existingVendor = dbManager.getVendors().find(v => v.name === vendorName);
|
|
1638
|
+
if (!existingVendor) {
|
|
1639
|
+
const services = LAN_PROXY_SERVICES.map(svc => ({
|
|
1640
|
+
name: svc.name,
|
|
1641
|
+
apiUrl: svc.apiUrl,
|
|
1642
|
+
apiKey: vendor.apiKey || '',
|
|
1643
|
+
sourceType: svc.sourceType,
|
|
1644
|
+
enableProxy: false,
|
|
1645
|
+
enableCodingPlan: false,
|
|
1646
|
+
}));
|
|
1647
|
+
yield dbManager.createVendor({
|
|
1648
|
+
name: vendorName,
|
|
1649
|
+
description: `从局域网节点 ${remoteNode.ip}:${remoteNode.port} 同步`,
|
|
1650
|
+
apiBaseUrl: remoteBaseUrl,
|
|
1651
|
+
services: services,
|
|
1652
|
+
});
|
|
1653
|
+
result.vendorCreated = true;
|
|
1654
|
+
result.vendorName = vendorName;
|
|
1655
|
+
result.servicesCreated = services.length;
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
res.json({ success: true, result });
|
|
1659
|
+
})));
|
|
1256
1660
|
// Skills 管理相关
|
|
1257
1661
|
app.get('/api/skills/installed', (_req, res) => {
|
|
1258
1662
|
const skills = listInstalledSkills();
|
|
@@ -1600,6 +2004,7 @@ ${instruction}
|
|
|
1600
2004
|
? requestedBypass
|
|
1601
2005
|
: appConfig.enableBypassPermissionsSupport;
|
|
1602
2006
|
const result = yield writeClaudeConfig(dbManager, enableAgentTeams, enableBypassPermissionsSupport, undefined, appConfig.claudeDefaultModel, appConfig.autocompactPctOverride);
|
|
2007
|
+
applyWriteLocalRecords(proxyServer);
|
|
1603
2008
|
res.json(result);
|
|
1604
2009
|
})));
|
|
1605
2010
|
app.post('/api/write-config/codex', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
@@ -1611,6 +2016,7 @@ ${instruction}
|
|
|
1611
2016
|
? appConfig.codexModelReasoningEffort
|
|
1612
2017
|
: DEFAULT_CODEX_REASONING_EFFORT;
|
|
1613
2018
|
const result = yield writeCodexConfig(dbManager, modelReasoningEffort, appConfig.codexDefaultModel);
|
|
2019
|
+
applyWriteLocalRecords(proxyServer);
|
|
1614
2020
|
res.json(result);
|
|
1615
2021
|
})));
|
|
1616
2022
|
// 兼容接口:更新全局 Agent Teams 配置
|
|
@@ -1623,6 +2029,7 @@ ${instruction}
|
|
|
1623
2029
|
yield proxyServer.updateConfig(latestConfig);
|
|
1624
2030
|
updateProxyConfig(latestConfig);
|
|
1625
2031
|
yield syncConfigsOnGlobalConfigUpdate(dbManager);
|
|
2032
|
+
applyWriteLocalRecords(proxyServer);
|
|
1626
2033
|
}
|
|
1627
2034
|
res.json(result);
|
|
1628
2035
|
})));
|
|
@@ -1636,6 +2043,7 @@ ${instruction}
|
|
|
1636
2043
|
yield proxyServer.updateConfig(latestConfig);
|
|
1637
2044
|
updateProxyConfig(latestConfig);
|
|
1638
2045
|
yield syncConfigsOnGlobalConfigUpdate(dbManager);
|
|
2046
|
+
applyWriteLocalRecords(proxyServer);
|
|
1639
2047
|
}
|
|
1640
2048
|
res.json(result);
|
|
1641
2049
|
})));
|
|
@@ -1652,6 +2060,7 @@ ${instruction}
|
|
|
1652
2060
|
yield proxyServer.updateConfig(latestConfig);
|
|
1653
2061
|
updateProxyConfig(latestConfig);
|
|
1654
2062
|
yield syncConfigsOnGlobalConfigUpdate(dbManager);
|
|
2063
|
+
applyWriteLocalRecords(proxyServer);
|
|
1655
2064
|
}
|
|
1656
2065
|
res.json(result);
|
|
1657
2066
|
})));
|
|
@@ -1770,6 +2179,150 @@ ${instruction}
|
|
|
1770
2179
|
dbManager.clearSessions();
|
|
1771
2180
|
res.json(true);
|
|
1772
2181
|
})));
|
|
2182
|
+
// ─── Session Route Binding API ───
|
|
2183
|
+
app.put('/api/sessions/:id/bind-route', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2184
|
+
const sessionId = req.params.id;
|
|
2185
|
+
const { routeId } = req.body || {};
|
|
2186
|
+
if (!routeId) {
|
|
2187
|
+
res.status(400).json({ success: false, error: 'routeId is required' });
|
|
2188
|
+
return;
|
|
2189
|
+
}
|
|
2190
|
+
const session = dbManager.getSession(sessionId);
|
|
2191
|
+
if (!session) {
|
|
2192
|
+
res.status(404).json({ success: false, error: 'Session not found' });
|
|
2193
|
+
return;
|
|
2194
|
+
}
|
|
2195
|
+
const updatedSession = yield dbManager.bindSessionRoute(sessionId, routeId);
|
|
2196
|
+
if (!updatedSession) {
|
|
2197
|
+
res.status(400).json({ success: false, error: 'Route not found' });
|
|
2198
|
+
return;
|
|
2199
|
+
}
|
|
2200
|
+
res.json({ success: true, session: updatedSession });
|
|
2201
|
+
})));
|
|
2202
|
+
app.delete('/api/sessions/:id/bind-route', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2203
|
+
const sessionId = req.params.id;
|
|
2204
|
+
const session = dbManager.getSession(sessionId);
|
|
2205
|
+
if (!session) {
|
|
2206
|
+
res.status(404).json({ success: false, error: 'Session not found' });
|
|
2207
|
+
return;
|
|
2208
|
+
}
|
|
2209
|
+
const result = yield dbManager.unbindSessionRoute(sessionId);
|
|
2210
|
+
res.json({ success: result });
|
|
2211
|
+
})));
|
|
2212
|
+
app.get('/api/routes/:id/bound-sessions', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2213
|
+
const routeId = req.params.id;
|
|
2214
|
+
const route = dbManager.getRoute(routeId);
|
|
2215
|
+
if (!route) {
|
|
2216
|
+
res.status(404).json({ error: 'Route not found' });
|
|
2217
|
+
return;
|
|
2218
|
+
}
|
|
2219
|
+
const sessions = dbManager.getBoundSessions(routeId);
|
|
2220
|
+
res.json({
|
|
2221
|
+
routeId,
|
|
2222
|
+
sessions: sessions.map(s => ({
|
|
2223
|
+
id: s.id,
|
|
2224
|
+
title: s.title,
|
|
2225
|
+
targetType: s.targetType,
|
|
2226
|
+
requestCount: s.requestCount,
|
|
2227
|
+
totalTokens: s.totalTokens,
|
|
2228
|
+
lastRequestAt: s.lastRequestAt,
|
|
2229
|
+
})),
|
|
2230
|
+
});
|
|
2231
|
+
})));
|
|
2232
|
+
// ─── Session Migration API ───
|
|
2233
|
+
app.post('/api/sessions/:id/migration-preview', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2234
|
+
var _a;
|
|
2235
|
+
const sessionId = req.params.id;
|
|
2236
|
+
const { targetTool, includeThinking, includeToolCalls, maxRounds } = req.body || {};
|
|
2237
|
+
if (!targetTool || !['claude-code', 'codex'].includes(targetTool)) {
|
|
2238
|
+
res.status(400).json({ error: 'Invalid targetTool. Must be "claude-code" or "codex".' });
|
|
2239
|
+
return;
|
|
2240
|
+
}
|
|
2241
|
+
try {
|
|
2242
|
+
const content = yield (0, session_migration_1.extractSessionContent)(dbManager, sessionId, {
|
|
2243
|
+
sourceSessionId: sessionId,
|
|
2244
|
+
targetTool,
|
|
2245
|
+
includeThinking: includeThinking === true,
|
|
2246
|
+
includeToolCalls: includeToolCalls !== false,
|
|
2247
|
+
maxRounds: typeof maxRounds === 'number' ? maxRounds : 0,
|
|
2248
|
+
});
|
|
2249
|
+
const preview = (0, session_migration_1.previewMigration)(content, targetTool);
|
|
2250
|
+
res.json(preview);
|
|
2251
|
+
}
|
|
2252
|
+
catch (err) {
|
|
2253
|
+
if ((_a = err.message) === null || _a === void 0 ? void 0 : _a.includes('not found')) {
|
|
2254
|
+
res.status(404).json({ error: err.message });
|
|
2255
|
+
}
|
|
2256
|
+
else {
|
|
2257
|
+
res.status(500).json({ error: err.message || 'Migration preview failed' });
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
})));
|
|
2261
|
+
app.post('/api/sessions/:id/migrate', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2262
|
+
var _a;
|
|
2263
|
+
const sessionId = req.params.id;
|
|
2264
|
+
const { targetTool, includeThinking, includeToolCalls, maxRounds, editedPrompt } = req.body || {};
|
|
2265
|
+
if (!targetTool || !['claude-code', 'codex'].includes(targetTool)) {
|
|
2266
|
+
res.status(400).json({ error: 'Invalid targetTool. Must be "claude-code" or "codex".' });
|
|
2267
|
+
return;
|
|
2268
|
+
}
|
|
2269
|
+
try {
|
|
2270
|
+
const content = yield (0, session_migration_1.extractSessionContent)(dbManager, sessionId, {
|
|
2271
|
+
sourceSessionId: sessionId,
|
|
2272
|
+
targetTool,
|
|
2273
|
+
includeThinking: includeThinking === true,
|
|
2274
|
+
includeToolCalls: includeToolCalls !== false,
|
|
2275
|
+
maxRounds: typeof maxRounds === 'number' ? maxRounds : 0,
|
|
2276
|
+
});
|
|
2277
|
+
const result = (0, session_migration_1.migrateSession)(content, targetTool, editedPrompt);
|
|
2278
|
+
res.json(result);
|
|
2279
|
+
}
|
|
2280
|
+
catch (err) {
|
|
2281
|
+
if ((_a = err.message) === null || _a === void 0 ? void 0 : _a.includes('not found')) {
|
|
2282
|
+
res.status(404).json({ error: err.message });
|
|
2283
|
+
}
|
|
2284
|
+
else {
|
|
2285
|
+
res.status(500).json({ error: err.message || 'Migration failed' });
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
})));
|
|
2289
|
+
app.post('/api/sessions/:id/migrate-launch', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2290
|
+
var _a;
|
|
2291
|
+
const sessionId = req.params.id;
|
|
2292
|
+
const { targetTool, includeThinking, includeToolCalls, maxRounds } = req.body || {};
|
|
2293
|
+
if (!targetTool || !['claude-code', 'codex'].includes(targetTool)) {
|
|
2294
|
+
res.status(400).json({ error: 'Invalid targetTool. Must be "claude-code" or "codex".' });
|
|
2295
|
+
return;
|
|
2296
|
+
}
|
|
2297
|
+
try {
|
|
2298
|
+
// First extract content and generate prompt
|
|
2299
|
+
const content = yield (0, session_migration_1.extractSessionContent)(dbManager, sessionId, {
|
|
2300
|
+
sourceSessionId: sessionId,
|
|
2301
|
+
targetTool,
|
|
2302
|
+
includeThinking: includeThinking === true,
|
|
2303
|
+
includeToolCalls: includeToolCalls !== false,
|
|
2304
|
+
maxRounds: typeof maxRounds === 'number' ? maxRounds : 0,
|
|
2305
|
+
});
|
|
2306
|
+
const { prompt } = (0, session_migration_1.migrateSession)(content, targetTool);
|
|
2307
|
+
// Resolve the project directory from session metadata
|
|
2308
|
+
const projectDir = (0, session_launcher_1.resolveProjectDir)(sessionId, content.sourceTool);
|
|
2309
|
+
// Write prompt to temp file
|
|
2310
|
+
const tempFilePath = (0, session_launcher_1.writePromptToTempFile)(prompt, sessionId);
|
|
2311
|
+
// Try to launch the target tool with the resolved project directory
|
|
2312
|
+
const result = yield (0, session_launcher_1.launchTargetWithFallback)(targetTool, tempFilePath, prompt, projectDir || undefined);
|
|
2313
|
+
// Schedule cleanup
|
|
2314
|
+
(0, session_launcher_1.cleanupTempFile)(tempFilePath);
|
|
2315
|
+
res.json(result);
|
|
2316
|
+
}
|
|
2317
|
+
catch (err) {
|
|
2318
|
+
if ((_a = err.message) === null || _a === void 0 ? void 0 : _a.includes('not found')) {
|
|
2319
|
+
res.status(404).json({ error: err.message });
|
|
2320
|
+
}
|
|
2321
|
+
else {
|
|
2322
|
+
res.status(500).json({ error: err.message || 'Launch migration failed' });
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
})));
|
|
1773
2326
|
app.get('/api/docs/recommend-vendors', asyncHandler((_req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1774
2327
|
const resp = yield fetch('https://unpkg.com/aicodeswitch/docs/vendors-recommand.md');
|
|
1775
2328
|
if (!resp.ok) {
|
|
@@ -1918,6 +2471,572 @@ ${instruction}
|
|
|
1918
2471
|
}
|
|
1919
2472
|
res.json(result);
|
|
1920
2473
|
})));
|
|
2474
|
+
// ============================================================
|
|
2475
|
+
// AccessKey 接入密钥 API
|
|
2476
|
+
// ============================================================
|
|
2477
|
+
// 获取密钥列表(支持分页和筛选)
|
|
2478
|
+
app.get('/api/access-keys', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2479
|
+
const accessKeyModule = proxyServer.getAccessKeyModule();
|
|
2480
|
+
if (!accessKeyModule) {
|
|
2481
|
+
res.json({ data: [], total: 0, page: 1, pageSize: 20 });
|
|
2482
|
+
return;
|
|
2483
|
+
}
|
|
2484
|
+
const page = Math.max(1, parseInt(req.query.page) || 1);
|
|
2485
|
+
const pageSize = Math.min(100, Math.max(1, parseInt(req.query.pageSize) || 20));
|
|
2486
|
+
const status = req.query.status;
|
|
2487
|
+
const policyId = req.query.policyId;
|
|
2488
|
+
const search = req.query.search;
|
|
2489
|
+
let keys = accessKeyModule.keyManager.list({ status, policyId, search });
|
|
2490
|
+
// 附加策略名称和用量信息
|
|
2491
|
+
const result = keys.map(key => {
|
|
2492
|
+
const policy = key.policyId ? accessKeyModule.policyManager.get(key.policyId) : null;
|
|
2493
|
+
return Object.assign(Object.assign({}, key), { apiKey: manager_1.AccessKeyManager.maskApiKey(key.apiKey), policyName: policy === null || policy === void 0 ? void 0 : policy.name });
|
|
2494
|
+
});
|
|
2495
|
+
const total = result.length;
|
|
2496
|
+
const start = (page - 1) * pageSize;
|
|
2497
|
+
const paged = result.slice(start, start + pageSize);
|
|
2498
|
+
res.json({ data: paged, total, page, pageSize });
|
|
2499
|
+
})));
|
|
2500
|
+
// 创建密钥
|
|
2501
|
+
app.post('/api/access-keys', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2502
|
+
const accessKeyModule = proxyServer.getAccessKeyModule();
|
|
2503
|
+
if (!accessKeyModule) {
|
|
2504
|
+
res.status(500).json({ error: 'AccessKey 功能未启用' });
|
|
2505
|
+
return;
|
|
2506
|
+
}
|
|
2507
|
+
const { name, remark, policyId } = req.body;
|
|
2508
|
+
if (!name || !name.trim()) {
|
|
2509
|
+
res.status(400).json({ error: '名称不能为空' });
|
|
2510
|
+
return;
|
|
2511
|
+
}
|
|
2512
|
+
const result = accessKeyModule.keyManager.create({ name: name.trim(), remark, policyId });
|
|
2513
|
+
yield accessKeyModule.save();
|
|
2514
|
+
res.json({
|
|
2515
|
+
key: Object.assign(Object.assign({}, result.key), { apiKey: manager_1.AccessKeyManager.maskApiKey(result.key.apiKey) }),
|
|
2516
|
+
apiKey: result.apiKey,
|
|
2517
|
+
});
|
|
2518
|
+
})));
|
|
2519
|
+
// 获取密钥详情
|
|
2520
|
+
app.get('/api/access-keys/:id', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2521
|
+
const accessKeyModule = proxyServer.getAccessKeyModule();
|
|
2522
|
+
if (!accessKeyModule) {
|
|
2523
|
+
res.status(404).json({ error: 'AccessKey 功能未启用' });
|
|
2524
|
+
return;
|
|
2525
|
+
}
|
|
2526
|
+
const key = accessKeyModule.keyManager.get(req.params.id);
|
|
2527
|
+
if (!key) {
|
|
2528
|
+
res.status(404).json({ error: '密钥不存在' });
|
|
2529
|
+
return;
|
|
2530
|
+
}
|
|
2531
|
+
const policy = key.policyId ? accessKeyModule.policyManager.get(key.policyId) : null;
|
|
2532
|
+
res.json(Object.assign(Object.assign({}, key), { apiKey: manager_1.AccessKeyManager.maskApiKey(key.apiKey), policyName: policy === null || policy === void 0 ? void 0 : policy.name }));
|
|
2533
|
+
})));
|
|
2534
|
+
// 编辑密钥
|
|
2535
|
+
app.put('/api/access-keys/:id', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2536
|
+
const accessKeyModule = proxyServer.getAccessKeyModule();
|
|
2537
|
+
if (!accessKeyModule) {
|
|
2538
|
+
res.status(500).json({ error: 'AccessKey 功能未启用' });
|
|
2539
|
+
return;
|
|
2540
|
+
}
|
|
2541
|
+
const result = accessKeyModule.keyManager.update(req.params.id, req.body);
|
|
2542
|
+
if (!result) {
|
|
2543
|
+
res.status(404).json({ error: '密钥不存在' });
|
|
2544
|
+
return;
|
|
2545
|
+
}
|
|
2546
|
+
yield accessKeyModule.save();
|
|
2547
|
+
res.json(true);
|
|
2548
|
+
})));
|
|
2549
|
+
// 删除密钥
|
|
2550
|
+
app.delete('/api/access-keys/:id', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2551
|
+
const accessKeyModule = proxyServer.getAccessKeyModule();
|
|
2552
|
+
if (!accessKeyModule) {
|
|
2553
|
+
res.status(500).json({ error: 'AccessKey 功能未启用' });
|
|
2554
|
+
return;
|
|
2555
|
+
}
|
|
2556
|
+
const result = accessKeyModule.keyManager.delete(req.params.id);
|
|
2557
|
+
yield accessKeyModule.save();
|
|
2558
|
+
removeWriteLocalRecords(req.params.id);
|
|
2559
|
+
res.json(result);
|
|
2560
|
+
})));
|
|
2561
|
+
// 重新生成 API Key
|
|
2562
|
+
app.post('/api/access-keys/:id/regenerate', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2563
|
+
const accessKeyModule = proxyServer.getAccessKeyModule();
|
|
2564
|
+
if (!accessKeyModule) {
|
|
2565
|
+
res.status(500).json({ error: 'AccessKey 功能未启用' });
|
|
2566
|
+
return;
|
|
2567
|
+
}
|
|
2568
|
+
const result = accessKeyModule.keyManager.regenerate(req.params.id);
|
|
2569
|
+
if (!result) {
|
|
2570
|
+
res.status(404).json({ error: '密钥不存在' });
|
|
2571
|
+
return;
|
|
2572
|
+
}
|
|
2573
|
+
yield accessKeyModule.save();
|
|
2574
|
+
// 如果该密钥有写入本地记录,重新生成后自动重写
|
|
2575
|
+
applyWriteLocalRecords(proxyServer);
|
|
2576
|
+
res.json({ apiKey: result.apiKey });
|
|
2577
|
+
})));
|
|
2578
|
+
// 批量更新状态
|
|
2579
|
+
app.put('/api/access-keys/batch/status', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2580
|
+
const accessKeyModule = proxyServer.getAccessKeyModule();
|
|
2581
|
+
if (!accessKeyModule) {
|
|
2582
|
+
res.status(500).json({ error: 'AccessKey 功能未启用' });
|
|
2583
|
+
return;
|
|
2584
|
+
}
|
|
2585
|
+
const { keyIds, status } = req.body;
|
|
2586
|
+
if (!Array.isArray(keyIds) || !status) {
|
|
2587
|
+
res.status(400).json({ error: '参数错误' });
|
|
2588
|
+
return;
|
|
2589
|
+
}
|
|
2590
|
+
const count = accessKeyModule.keyManager.batchUpdateStatus(keyIds, status);
|
|
2591
|
+
yield accessKeyModule.save();
|
|
2592
|
+
res.json({ count });
|
|
2593
|
+
})));
|
|
2594
|
+
// 批量绑定策略
|
|
2595
|
+
app.put('/api/access-keys/batch/policy', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2596
|
+
const accessKeyModule = proxyServer.getAccessKeyModule();
|
|
2597
|
+
if (!accessKeyModule) {
|
|
2598
|
+
res.status(500).json({ error: 'AccessKey 功能未启用' });
|
|
2599
|
+
return;
|
|
2600
|
+
}
|
|
2601
|
+
const { keyIds, policyId } = req.body;
|
|
2602
|
+
if (!Array.isArray(keyIds) || !policyId) {
|
|
2603
|
+
res.status(400).json({ error: '参数错误' });
|
|
2604
|
+
return;
|
|
2605
|
+
}
|
|
2606
|
+
const count = accessKeyModule.keyManager.batchBindPolicy(keyIds, policyId);
|
|
2607
|
+
yield accessKeyModule.save();
|
|
2608
|
+
res.json({ count });
|
|
2609
|
+
})));
|
|
2610
|
+
// 批量删除
|
|
2611
|
+
app.delete('/api/access-keys/batch', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2612
|
+
const accessKeyModule = proxyServer.getAccessKeyModule();
|
|
2613
|
+
if (!accessKeyModule) {
|
|
2614
|
+
res.status(500).json({ error: 'AccessKey 功能未启用' });
|
|
2615
|
+
return;
|
|
2616
|
+
}
|
|
2617
|
+
const { keyIds } = req.body;
|
|
2618
|
+
if (!Array.isArray(keyIds)) {
|
|
2619
|
+
res.status(400).json({ error: '参数错误' });
|
|
2620
|
+
return;
|
|
2621
|
+
}
|
|
2622
|
+
const count = accessKeyModule.keyManager.batchDelete(keyIds);
|
|
2623
|
+
yield accessKeyModule.save();
|
|
2624
|
+
for (const keyId of keyIds)
|
|
2625
|
+
removeWriteLocalRecords(keyId);
|
|
2626
|
+
res.json({ count });
|
|
2627
|
+
})));
|
|
2628
|
+
// Key 用量统计
|
|
2629
|
+
app.get('/api/access-keys/:id/usage', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2630
|
+
const accessKeyModule = proxyServer.getAccessKeyModule();
|
|
2631
|
+
if (!accessKeyModule) {
|
|
2632
|
+
res.status(404).json({ error: 'AccessKey 功能未启用' });
|
|
2633
|
+
return;
|
|
2634
|
+
}
|
|
2635
|
+
const usage = yield accessKeyModule.usageTracker.getUsage(req.params.id);
|
|
2636
|
+
res.json(usage);
|
|
2637
|
+
})));
|
|
2638
|
+
// Key 用量趋势
|
|
2639
|
+
app.get('/api/access-keys/:id/usage/trend', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2640
|
+
const accessKeyModule = proxyServer.getAccessKeyModule();
|
|
2641
|
+
if (!accessKeyModule) {
|
|
2642
|
+
res.status(404).json({ error: 'AccessKey 功能未启用' });
|
|
2643
|
+
return;
|
|
2644
|
+
}
|
|
2645
|
+
const days = parseInt(req.query.days) || 30;
|
|
2646
|
+
const trend = yield accessKeyModule.usageTracker.getTrend(req.params.id, days);
|
|
2647
|
+
res.json(trend);
|
|
2648
|
+
})));
|
|
2649
|
+
// Key 日志
|
|
2650
|
+
app.get('/api/access-keys/:id/logs', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2651
|
+
const accessKeyModule = proxyServer.getAccessKeyModule();
|
|
2652
|
+
if (!accessKeyModule) {
|
|
2653
|
+
res.status(404).json({ error: 'AccessKey 功能未启用' });
|
|
2654
|
+
return;
|
|
2655
|
+
}
|
|
2656
|
+
const page = Math.max(1, parseInt(req.query.page) || 1);
|
|
2657
|
+
const pageSize = Math.min(100, Math.max(1, parseInt(req.query.pageSize) || 50));
|
|
2658
|
+
const startDate = req.query.startDate;
|
|
2659
|
+
const endDate = req.query.endDate;
|
|
2660
|
+
const contentType = req.query.contentType;
|
|
2661
|
+
const search = req.query.search;
|
|
2662
|
+
const result = yield accessKeyModule.keyLogger.getLogs(req.params.id, { page, pageSize, startDate, endDate, contentType, search });
|
|
2663
|
+
res.json(result);
|
|
2664
|
+
})));
|
|
2665
|
+
// ========== AccessKey 会话 API ==========
|
|
2666
|
+
// 获取密钥的会话列表
|
|
2667
|
+
app.get('/api/access-keys/:id/sessions', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2668
|
+
const accessKeyModule = proxyServer.getAccessKeyModule();
|
|
2669
|
+
if (!accessKeyModule) {
|
|
2670
|
+
res.status(404).json({ error: 'AccessKey 功能未启用' });
|
|
2671
|
+
return;
|
|
2672
|
+
}
|
|
2673
|
+
const page = Math.max(1, parseInt(req.query.page) || 1);
|
|
2674
|
+
const pageSize = Math.min(100, Math.max(1, parseInt(req.query.pageSize) || 20));
|
|
2675
|
+
const targetType = req.query.targetType;
|
|
2676
|
+
const search = req.query.search;
|
|
2677
|
+
const result = yield accessKeyModule.keySessionTracker.getSessions(req.params.id, {
|
|
2678
|
+
page, pageSize, targetType, search,
|
|
2679
|
+
});
|
|
2680
|
+
res.json(result);
|
|
2681
|
+
})));
|
|
2682
|
+
// 获取密钥的会话总数
|
|
2683
|
+
app.get('/api/access-keys/:id/sessions/count', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2684
|
+
const accessKeyModule = proxyServer.getAccessKeyModule();
|
|
2685
|
+
if (!accessKeyModule) {
|
|
2686
|
+
res.status(404).json({ error: 'AccessKey 功能未启用' });
|
|
2687
|
+
return;
|
|
2688
|
+
}
|
|
2689
|
+
const targetType = req.query.targetType;
|
|
2690
|
+
const count = yield accessKeyModule.keySessionTracker.getSessionsCount(req.params.id, targetType);
|
|
2691
|
+
res.json({ count });
|
|
2692
|
+
})));
|
|
2693
|
+
// 获取密钥的单个会话
|
|
2694
|
+
app.get('/api/access-keys/:id/sessions/:sessionId', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2695
|
+
const accessKeyModule = proxyServer.getAccessKeyModule();
|
|
2696
|
+
if (!accessKeyModule) {
|
|
2697
|
+
res.status(404).json({ error: 'AccessKey 功能未启用' });
|
|
2698
|
+
return;
|
|
2699
|
+
}
|
|
2700
|
+
const session = yield accessKeyModule.keySessionTracker.getSession(req.params.id, req.params.sessionId);
|
|
2701
|
+
if (!session) {
|
|
2702
|
+
res.status(404).json({ error: '会话不存在' });
|
|
2703
|
+
return;
|
|
2704
|
+
}
|
|
2705
|
+
res.json(session);
|
|
2706
|
+
})));
|
|
2707
|
+
// 获取密钥会话的日志
|
|
2708
|
+
app.get('/api/access-keys/:id/sessions/:sessionId/logs', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2709
|
+
const accessKeyModule = proxyServer.getAccessKeyModule();
|
|
2710
|
+
if (!accessKeyModule) {
|
|
2711
|
+
res.status(404).json({ error: 'AccessKey 功能未启用' });
|
|
2712
|
+
return;
|
|
2713
|
+
}
|
|
2714
|
+
const limit = Math.min(10000, Math.max(1, parseInt(req.query.limit) || 10000));
|
|
2715
|
+
const logs = yield accessKeyModule.keyLogger.getLogsBySessionId(req.params.id, req.params.sessionId, limit);
|
|
2716
|
+
res.json(logs);
|
|
2717
|
+
})));
|
|
2718
|
+
// 删除密钥的单个会话
|
|
2719
|
+
app.delete('/api/access-keys/:id/sessions/:sessionId', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2720
|
+
const accessKeyModule = proxyServer.getAccessKeyModule();
|
|
2721
|
+
if (!accessKeyModule) {
|
|
2722
|
+
res.status(404).json({ error: 'AccessKey 功能未启用' });
|
|
2723
|
+
return;
|
|
2724
|
+
}
|
|
2725
|
+
const deleted = yield accessKeyModule.keySessionTracker.deleteSession(req.params.id, req.params.sessionId);
|
|
2726
|
+
if (!deleted) {
|
|
2727
|
+
res.status(404).json({ error: '会话不存在' });
|
|
2728
|
+
return;
|
|
2729
|
+
}
|
|
2730
|
+
res.json({ success: true });
|
|
2731
|
+
})));
|
|
2732
|
+
// 清空密钥的所有会话
|
|
2733
|
+
app.delete('/api/access-keys/:id/sessions', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2734
|
+
const accessKeyModule = proxyServer.getAccessKeyModule();
|
|
2735
|
+
if (!accessKeyModule) {
|
|
2736
|
+
res.status(404).json({ error: 'AccessKey 功能未启用' });
|
|
2737
|
+
return;
|
|
2738
|
+
}
|
|
2739
|
+
yield accessKeyModule.keySessionTracker.clearSessions(req.params.id);
|
|
2740
|
+
res.json({ success: true });
|
|
2741
|
+
})));
|
|
2742
|
+
// Key 接入指引
|
|
2743
|
+
app.get('/api/access-keys/:id/guide', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2744
|
+
const accessKeyModule = proxyServer.getAccessKeyModule();
|
|
2745
|
+
if (!accessKeyModule) {
|
|
2746
|
+
res.status(404).json({ error: 'AccessKey 功能未启用' });
|
|
2747
|
+
return;
|
|
2748
|
+
}
|
|
2749
|
+
const key = accessKeyModule.keyManager.get(req.params.id);
|
|
2750
|
+
if (!key) {
|
|
2751
|
+
res.status(404).json({ error: '密钥不存在' });
|
|
2752
|
+
return;
|
|
2753
|
+
}
|
|
2754
|
+
const host = req.query.host || req.hostname || 'localhost';
|
|
2755
|
+
const port = req.query.port || (req.socket.localAddress ? String(req.socket.localPort) : '4567');
|
|
2756
|
+
const baseUrl = `http://${host}:${port}`;
|
|
2757
|
+
const maskedKey = manager_1.AccessKeyManager.maskApiKey(key.apiKey);
|
|
2758
|
+
res.json({
|
|
2759
|
+
claudeCode: {
|
|
2760
|
+
description: 'Claude Code 接入',
|
|
2761
|
+
envVars: {
|
|
2762
|
+
ANTHROPIC_BASE_URL: `${baseUrl}/claude-code`,
|
|
2763
|
+
ANTHROPIC_AUTH_TOKEN: maskedKey,
|
|
2764
|
+
},
|
|
2765
|
+
},
|
|
2766
|
+
codex: {
|
|
2767
|
+
description: 'Codex 接入',
|
|
2768
|
+
envVars: {
|
|
2769
|
+
OPENAI_API_KEY: maskedKey,
|
|
2770
|
+
OPENAI_BASE_URL: `${baseUrl}/codex`,
|
|
2771
|
+
},
|
|
2772
|
+
},
|
|
2773
|
+
openai: {
|
|
2774
|
+
description: 'OpenAI 兼容工具 (Cursor / Continue 等)',
|
|
2775
|
+
envVars: {
|
|
2776
|
+
OPENAI_API_KEY: maskedKey,
|
|
2777
|
+
OPENAI_BASE_URL: `${baseUrl}/v1`,
|
|
2778
|
+
},
|
|
2779
|
+
},
|
|
2780
|
+
});
|
|
2781
|
+
})));
|
|
2782
|
+
// 写入本地配置(将 AccessKey 写入 Claude Code / Codex 配置文件)
|
|
2783
|
+
app.post('/api/access-keys/:id/write-local', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2784
|
+
const accessKeyModule = proxyServer.getAccessKeyModule();
|
|
2785
|
+
if (!accessKeyModule) {
|
|
2786
|
+
res.status(400).json({ error: 'AccessKey 功能未启用' });
|
|
2787
|
+
return;
|
|
2788
|
+
}
|
|
2789
|
+
const key = accessKeyModule.keyManager.get(req.params.id);
|
|
2790
|
+
if (!key) {
|
|
2791
|
+
res.status(404).json({ error: '密钥不存在' });
|
|
2792
|
+
return;
|
|
2793
|
+
}
|
|
2794
|
+
const targets = req.body.targets || [];
|
|
2795
|
+
if (targets.length === 0) {
|
|
2796
|
+
res.status(400).json({ error: '请选择至少一个目标' });
|
|
2797
|
+
return;
|
|
2798
|
+
}
|
|
2799
|
+
const homeDir = os_1.default.homedir();
|
|
2800
|
+
const results = {};
|
|
2801
|
+
for (const target of targets) {
|
|
2802
|
+
try {
|
|
2803
|
+
if (target === 'claude-code') {
|
|
2804
|
+
// 写入 ~/.claude/settings.json 的 env.ANTHROPIC_AUTH_TOKEN
|
|
2805
|
+
const claudeDir = path_1.default.join(homeDir, '.claude');
|
|
2806
|
+
const settingsPath = path_1.default.join(claudeDir, 'settings.json');
|
|
2807
|
+
if (!fs_1.default.existsSync(claudeDir)) {
|
|
2808
|
+
fs_1.default.mkdirSync(claudeDir, { recursive: true });
|
|
2809
|
+
}
|
|
2810
|
+
let settings = {};
|
|
2811
|
+
if (fs_1.default.existsSync(settingsPath)) {
|
|
2812
|
+
try {
|
|
2813
|
+
settings = JSON.parse(fs_1.default.readFileSync(settingsPath, 'utf-8'));
|
|
2814
|
+
}
|
|
2815
|
+
catch ( /* ignore */_a) { /* ignore */ }
|
|
2816
|
+
}
|
|
2817
|
+
if (!settings.env)
|
|
2818
|
+
settings.env = {};
|
|
2819
|
+
settings.env.ANTHROPIC_AUTH_TOKEN = key.apiKey;
|
|
2820
|
+
(0, config_merge_1.atomicWriteFile)(settingsPath, JSON.stringify(settings, null, 2));
|
|
2821
|
+
results['claude-code'] = true;
|
|
2822
|
+
}
|
|
2823
|
+
else if (target === 'codex') {
|
|
2824
|
+
// 写入 ~/.codex/auth.json 的 OPENAI_API_KEY
|
|
2825
|
+
const codexDir = path_1.default.join(homeDir, '.codex');
|
|
2826
|
+
const authPath = path_1.default.join(codexDir, 'auth.json');
|
|
2827
|
+
if (!fs_1.default.existsSync(codexDir)) {
|
|
2828
|
+
fs_1.default.mkdirSync(codexDir, { recursive: true });
|
|
2829
|
+
}
|
|
2830
|
+
let auth = {};
|
|
2831
|
+
if (fs_1.default.existsSync(authPath)) {
|
|
2832
|
+
try {
|
|
2833
|
+
auth = JSON.parse(fs_1.default.readFileSync(authPath, 'utf-8'));
|
|
2834
|
+
}
|
|
2835
|
+
catch ( /* ignore */_b) { /* ignore */ }
|
|
2836
|
+
}
|
|
2837
|
+
auth.OPENAI_API_KEY = key.apiKey;
|
|
2838
|
+
(0, config_merge_1.atomicWriteFile)(authPath, JSON.stringify(auth, null, 2));
|
|
2839
|
+
results['codex'] = true;
|
|
2840
|
+
}
|
|
2841
|
+
}
|
|
2842
|
+
catch (error) {
|
|
2843
|
+
console.error(`Failed to write local config for ${target}:`, error);
|
|
2844
|
+
results[target] = false;
|
|
2845
|
+
}
|
|
2846
|
+
}
|
|
2847
|
+
// 持久化写入本地记录,确保服务重启后自动恢复
|
|
2848
|
+
const successTargets = Object.entries(results)
|
|
2849
|
+
.filter(([, v]) => v === true)
|
|
2850
|
+
.map(([k]) => k);
|
|
2851
|
+
if (successTargets.length > 0) {
|
|
2852
|
+
addWriteLocalRecord(key.id, successTargets);
|
|
2853
|
+
}
|
|
2854
|
+
res.json({ success: true, results });
|
|
2855
|
+
})));
|
|
2856
|
+
// 查询写入本地记录(供 UI 显示标注)
|
|
2857
|
+
app.get('/api/write-local-records', asyncHandler((_req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2858
|
+
res.json(loadWriteLocalRecords());
|
|
2859
|
+
})));
|
|
2860
|
+
// ============================================================
|
|
2861
|
+
// Policy 策略 API
|
|
2862
|
+
// ============================================================
|
|
2863
|
+
// 策略列表
|
|
2864
|
+
app.get('/api/policies', asyncHandler((_req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2865
|
+
const accessKeyModule = proxyServer.getAccessKeyModule();
|
|
2866
|
+
if (!accessKeyModule) {
|
|
2867
|
+
res.json([]);
|
|
2868
|
+
return;
|
|
2869
|
+
}
|
|
2870
|
+
const policies = accessKeyModule.policyManager.list();
|
|
2871
|
+
const result = policies.map(p => (Object.assign(Object.assign({}, p), { keyCount: accessKeyModule.keyManager.countByPolicyId(p.id) })));
|
|
2872
|
+
res.json(result);
|
|
2873
|
+
})));
|
|
2874
|
+
// 创建策略
|
|
2875
|
+
app.post('/api/policies', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2876
|
+
const accessKeyModule = proxyServer.getAccessKeyModule();
|
|
2877
|
+
if (!accessKeyModule) {
|
|
2878
|
+
res.status(500).json({ error: 'AccessKey 功能未启用' });
|
|
2879
|
+
return;
|
|
2880
|
+
}
|
|
2881
|
+
const policy = accessKeyModule.policyManager.create(req.body);
|
|
2882
|
+
yield accessKeyModule.save();
|
|
2883
|
+
res.json(policy);
|
|
2884
|
+
})));
|
|
2885
|
+
// 策略模板(必须在 /:id 之前注册,避免被当作 id 参数匹配)
|
|
2886
|
+
app.get('/api/policies/templates', asyncHandler((_req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2887
|
+
res.json(policy_manager_1.PolicyManager.getTemplates());
|
|
2888
|
+
})));
|
|
2889
|
+
// 策略详情
|
|
2890
|
+
app.get('/api/policies/:id', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2891
|
+
const accessKeyModule = proxyServer.getAccessKeyModule();
|
|
2892
|
+
if (!accessKeyModule) {
|
|
2893
|
+
res.status(404).json({ error: 'AccessKey 功能未启用' });
|
|
2894
|
+
return;
|
|
2895
|
+
}
|
|
2896
|
+
const policy = accessKeyModule.policyManager.get(req.params.id);
|
|
2897
|
+
if (!policy) {
|
|
2898
|
+
res.status(404).json({ error: '策略不存在' });
|
|
2899
|
+
return;
|
|
2900
|
+
}
|
|
2901
|
+
res.json(policy);
|
|
2902
|
+
})));
|
|
2903
|
+
// 编辑策略
|
|
2904
|
+
app.put('/api/policies/:id', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2905
|
+
const accessKeyModule = proxyServer.getAccessKeyModule();
|
|
2906
|
+
if (!accessKeyModule) {
|
|
2907
|
+
res.status(500).json({ error: 'AccessKey 功能未启用' });
|
|
2908
|
+
return;
|
|
2909
|
+
}
|
|
2910
|
+
const result = accessKeyModule.policyManager.update(req.params.id, req.body);
|
|
2911
|
+
if (!result) {
|
|
2912
|
+
res.status(404).json({ error: '策略不存在' });
|
|
2913
|
+
return;
|
|
2914
|
+
}
|
|
2915
|
+
yield accessKeyModule.save();
|
|
2916
|
+
res.json(true);
|
|
2917
|
+
})));
|
|
2918
|
+
// 删除策略
|
|
2919
|
+
app.delete('/api/policies/:id', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2920
|
+
const accessKeyModule = proxyServer.getAccessKeyModule();
|
|
2921
|
+
if (!accessKeyModule) {
|
|
2922
|
+
res.status(500).json({ error: 'AccessKey 功能未启用' });
|
|
2923
|
+
return;
|
|
2924
|
+
}
|
|
2925
|
+
const keyCount = accessKeyModule.keyManager.countByPolicyId(req.params.id);
|
|
2926
|
+
if (keyCount > 0) {
|
|
2927
|
+
res.status(400).json({ error: `有 ${keyCount} 个密钥正在使用此策略,请先解除绑定` });
|
|
2928
|
+
return;
|
|
2929
|
+
}
|
|
2930
|
+
const result = accessKeyModule.policyManager.delete(req.params.id);
|
|
2931
|
+
yield accessKeyModule.save();
|
|
2932
|
+
res.json(result);
|
|
2933
|
+
})));
|
|
2934
|
+
// 复制策略
|
|
2935
|
+
app.post('/api/policies/:id/duplicate', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2936
|
+
const accessKeyModule = proxyServer.getAccessKeyModule();
|
|
2937
|
+
if (!accessKeyModule) {
|
|
2938
|
+
res.status(500).json({ error: 'AccessKey 功能未启用' });
|
|
2939
|
+
return;
|
|
2940
|
+
}
|
|
2941
|
+
const result = accessKeyModule.policyManager.duplicate(req.params.id);
|
|
2942
|
+
if (!result) {
|
|
2943
|
+
res.status(404).json({ error: '策略不存在' });
|
|
2944
|
+
return;
|
|
2945
|
+
}
|
|
2946
|
+
yield accessKeyModule.save();
|
|
2947
|
+
res.json(result);
|
|
2948
|
+
})));
|
|
2949
|
+
// 使用策略的密钥列表
|
|
2950
|
+
app.get('/api/policies/:id/keys', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2951
|
+
const accessKeyModule = proxyServer.getAccessKeyModule();
|
|
2952
|
+
if (!accessKeyModule) {
|
|
2953
|
+
res.json([]);
|
|
2954
|
+
return;
|
|
2955
|
+
}
|
|
2956
|
+
const keys = accessKeyModule.keyManager.listByPolicyId(req.params.id);
|
|
2957
|
+
res.json(keys.map(k => (Object.assign(Object.assign({}, k), { apiKey: manager_1.AccessKeyManager.maskApiKey(k.apiKey) }))));
|
|
2958
|
+
})));
|
|
2959
|
+
// ============================================================
|
|
2960
|
+
// AccessKey 全局统计 API
|
|
2961
|
+
// ============================================================
|
|
2962
|
+
// Key 用量排行
|
|
2963
|
+
app.get('/api/statistics/access-keys', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2964
|
+
const accessKeyModule = proxyServer.getAccessKeyModule();
|
|
2965
|
+
if (!accessKeyModule) {
|
|
2966
|
+
res.json([]);
|
|
2967
|
+
return;
|
|
2968
|
+
}
|
|
2969
|
+
const keys = accessKeyModule.keyManager.list();
|
|
2970
|
+
const result = yield Promise.all(keys.map((k) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2971
|
+
const usage = yield accessKeyModule.usageTracker.getUsage(k.id);
|
|
2972
|
+
return {
|
|
2973
|
+
keyId: k.id,
|
|
2974
|
+
keyName: k.name,
|
|
2975
|
+
totalTokens: usage.lifetime.totalTokens,
|
|
2976
|
+
totalRequests: usage.lifetime.totalRequests,
|
|
2977
|
+
lastActiveAt: k.lastActiveAt,
|
|
2978
|
+
};
|
|
2979
|
+
})));
|
|
2980
|
+
// 排序
|
|
2981
|
+
const sortBy = req.query.sortBy || 'totalTokens';
|
|
2982
|
+
const order = req.query.order || 'desc';
|
|
2983
|
+
result.sort((a, b) => {
|
|
2984
|
+
const va = a[sortBy] || 0;
|
|
2985
|
+
const vb = b[sortBy] || 0;
|
|
2986
|
+
return order === 'desc' ? vb - va : va - vb;
|
|
2987
|
+
});
|
|
2988
|
+
const limit = Math.min(100, parseInt(req.query.limit) || 20);
|
|
2989
|
+
res.json(result.slice(0, limit));
|
|
2990
|
+
})));
|
|
2991
|
+
// 配额告警
|
|
2992
|
+
app.get('/api/statistics/quota-alerts', asyncHandler((_req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2993
|
+
const accessKeyModule = proxyServer.getAccessKeyModule();
|
|
2994
|
+
if (!accessKeyModule) {
|
|
2995
|
+
res.json([]);
|
|
2996
|
+
return;
|
|
2997
|
+
}
|
|
2998
|
+
const keys = accessKeyModule.keyManager.list({ status: 'active' });
|
|
2999
|
+
const alerts = [];
|
|
3000
|
+
for (const key of keys) {
|
|
3001
|
+
if (!key.policyId)
|
|
3002
|
+
continue;
|
|
3003
|
+
const policy = accessKeyModule.policyManager.get(key.policyId);
|
|
3004
|
+
if (!policy)
|
|
3005
|
+
continue;
|
|
3006
|
+
const usage = yield accessKeyModule.usageTracker.getUsage(key.id);
|
|
3007
|
+
// 检查各维度
|
|
3008
|
+
const checks = [];
|
|
3009
|
+
if (policy.dailyTokenLimit) {
|
|
3010
|
+
checks.push({ dimension: 'dailyTokenLimit', usage: usage.periods.daily.tokens, limit: policy.dailyTokenLimit * 1000 });
|
|
3011
|
+
}
|
|
3012
|
+
if (policy.monthlyTokenLimit) {
|
|
3013
|
+
checks.push({ dimension: 'monthlyTokenLimit', usage: usage.periods.monthly.tokens, limit: policy.monthlyTokenLimit * 1000 });
|
|
3014
|
+
}
|
|
3015
|
+
if (policy.dailyRequestLimit) {
|
|
3016
|
+
checks.push({ dimension: 'dailyRequestLimit', usage: usage.periods.daily.requests, limit: policy.dailyRequestLimit });
|
|
3017
|
+
}
|
|
3018
|
+
if (policy.monthlyRequestLimit) {
|
|
3019
|
+
checks.push({ dimension: 'monthlyRequestLimit', usage: usage.periods.monthly.requests, limit: policy.monthlyRequestLimit });
|
|
3020
|
+
}
|
|
3021
|
+
for (const check of checks) {
|
|
3022
|
+
if (check.limit <= 0)
|
|
3023
|
+
continue;
|
|
3024
|
+
const pct = Math.round((check.usage / check.limit) * 100);
|
|
3025
|
+
if (pct >= 80) {
|
|
3026
|
+
alerts.push({
|
|
3027
|
+
keyId: key.id,
|
|
3028
|
+
keyName: key.name,
|
|
3029
|
+
dimension: check.dimension,
|
|
3030
|
+
usage: check.usage,
|
|
3031
|
+
limit: check.limit,
|
|
3032
|
+
percentage: pct,
|
|
3033
|
+
level: pct >= 100 ? 'exceeded' : pct >= 95 ? 'critical' : 'warning',
|
|
3034
|
+
});
|
|
3035
|
+
}
|
|
3036
|
+
}
|
|
3037
|
+
}
|
|
3038
|
+
res.json(alerts);
|
|
3039
|
+
})));
|
|
1921
3040
|
// 写入MCP配置到Claude Code或Codex的全局配置文件
|
|
1922
3041
|
const writeMCPConfig = (targetType) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1923
3042
|
try {
|
|
@@ -2148,7 +3267,28 @@ const start = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
2148
3267
|
catch (error) {
|
|
2149
3268
|
console.error('[Server] Tool config sync failed:', error);
|
|
2150
3269
|
}
|
|
3270
|
+
// 清理旧的迁移临时文件
|
|
3271
|
+
try {
|
|
3272
|
+
(0, session_launcher_1.cleanupOldTempFiles)();
|
|
3273
|
+
}
|
|
3274
|
+
catch ( /* ignore */_a) { /* ignore */ }
|
|
2151
3275
|
const proxyServer = new proxy_server_1.ProxyServer(dbManager, app);
|
|
3276
|
+
// Initialize AccessKey module
|
|
3277
|
+
const accessKeyModule = new index_1.AccessKeyModule(dataDir);
|
|
3278
|
+
try {
|
|
3279
|
+
yield accessKeyModule.initialize();
|
|
3280
|
+
proxyServer.setAccessKeyModule(accessKeyModule);
|
|
3281
|
+
}
|
|
3282
|
+
catch (error) {
|
|
3283
|
+
console.error('[Server] AccessKey module initialization failed:', error);
|
|
3284
|
+
}
|
|
3285
|
+
// 恢复已写入本地的 AccessKey(在代理配置写入之后、AccessKey 模块初始化之后)
|
|
3286
|
+
try {
|
|
3287
|
+
applyWriteLocalRecords(proxyServer);
|
|
3288
|
+
}
|
|
3289
|
+
catch (error) {
|
|
3290
|
+
console.error('[Server] Failed to apply write-local records:', error);
|
|
3291
|
+
}
|
|
2152
3292
|
// Initialize proxy server and register proxy routes last
|
|
2153
3293
|
proxyServer.initialize();
|
|
2154
3294
|
// Register admin routes first
|
|
@@ -2223,7 +3363,16 @@ const start = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
2223
3363
|
catch (error) {
|
|
2224
3364
|
console.error('[Shutdown ...] Failed to restore Codex config:', error);
|
|
2225
3365
|
}
|
|
3366
|
+
// Shutdown AccessKey module
|
|
3367
|
+
try {
|
|
3368
|
+
yield accessKeyModule.shutdown();
|
|
3369
|
+
}
|
|
3370
|
+
catch (error) {
|
|
3371
|
+
console.error('[Shutdown ...] AccessKey module shutdown failed:', error);
|
|
3372
|
+
}
|
|
2226
3373
|
dbManager.close();
|
|
3374
|
+
// 清理规则状态广播器(关闭 SSE 连接)
|
|
3375
|
+
rules_status_service_1.rulesStatusBroadcaster.destroy();
|
|
2227
3376
|
yield Promise.race([
|
|
2228
3377
|
new Promise((resolve) => {
|
|
2229
3378
|
server.close(() => resolve());
|