aicodeswitch 5.1.3 → 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.
@@ -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");
@@ -43,7 +46,7 @@ const upgradeHashFilePath = path_1.default.join(appDir, 'upgrade-hash');
43
46
  if (fs_1.default.existsSync(dotenvPath)) {
44
47
  dotenv_1.default.config({ path: dotenvPath });
45
48
  }
46
- const host = process.env.HOST || '127.0.0.1';
49
+ const host = process.env.HOST || '0.0.0.0';
47
50
  const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 4567;
48
51
  let globalProxyConfig = null;
49
52
  function updateProxyConfig(config) {
@@ -76,6 +79,112 @@ function getProxyAgent() {
76
79
  return null;
77
80
  }
78
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
+ }
79
188
  const app = (0, express_1.default)();
80
189
  app.use((0, cors_1.default)());
81
190
  app.use(express_1.default.json({ limit: 'Infinity' }));
@@ -116,12 +225,11 @@ const isClaudeEffortLevel = (value) => {
116
225
  const isValidAutocompactPct = (v) => {
117
226
  return typeof v === 'number' && Number.isInteger(v) && v >= 1 && v <= 100;
118
227
  };
119
- 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 = {}) {
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 = {}) {
120
229
  var _a;
121
230
  try {
122
231
  const homeDir = os_1.default.homedir();
123
232
  const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 4567;
124
- const config = dbManager.getConfig();
125
233
  // Claude Code settings.json
126
234
  const claudeDir = path_1.default.join(homeDir, '.claude');
127
235
  const claudeSettingsPath = path_1.default.join(claudeDir, 'settings.json');
@@ -168,11 +276,11 @@ const writeClaudeConfig = (dbManager_1, enableAgentTeams_1, enableBypassPermissi
168
276
  }
169
277
  // 构建代理配置
170
278
  const claudeSettingsEnv = {
171
- ANTHROPIC_AUTH_TOKEN: config.apiKey || "api_key",
172
- ANTHROPIC_API_KEY: "",
279
+ ANTHROPIC_AUTH_TOKEN: "api_key",
173
280
  ANTHROPIC_BASE_URL: `http://${host}:${port}/claude-code`,
174
281
  API_TIMEOUT_MS: "3000000",
175
- CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: 1
282
+ CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: 1,
283
+ CLAUDE_CODE_MAX_RETRIES: 3
176
284
  };
177
285
  // 如果启用Agent Teams功能,添加对应的环境变量
178
286
  if (enableAgentTeams) {
@@ -264,11 +372,10 @@ const DEFAULT_CODEX_REASONING_EFFORT = 'high';
264
372
  const isCodexReasoningEffort = (value) => {
265
373
  return typeof value === 'string' && VALID_CODEX_REASONING_EFFORTS.includes(value);
266
374
  };
267
- const writeCodexConfig = (dbManager_1, ...args_1) => __awaiter(void 0, [dbManager_1, ...args_1], void 0, function* (dbManager, modelReasoningEffort = DEFAULT_CODEX_REASONING_EFFORT, codexDefaultModel, options = {}) {
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 = {}) {
268
376
  var _a;
269
377
  try {
270
378
  const homeDir = os_1.default.homedir();
271
- const config = dbManager.getConfig();
272
379
  // Codex config.toml
273
380
  const codexDir = path_1.default.join(homeDir, '.codex');
274
381
  const codexConfigPath = path_1.default.join(codexDir, 'config.toml');
@@ -326,7 +433,9 @@ const writeCodexConfig = (dbManager_1, ...args_1) => __awaiter(void 0, [dbManage
326
433
  aicodeswitch: {
327
434
  name: "aicodeswitch",
328
435
  base_url: `http://${host}:${process.env.PORT ? parseInt(process.env.PORT, 10) : 4567}/codex`,
329
- wire_api: "responses"
436
+ wire_api: "responses",
437
+ stream_max_retries: 3,
438
+ stream_retry_backoff: "fixed"
330
439
  }
331
440
  }
332
441
  };
@@ -356,7 +465,7 @@ const writeCodexConfig = (dbManager_1, ...args_1) => __awaiter(void 0, [dbManage
356
465
  }
357
466
  // 构建代理配置
358
467
  const proxyAuth = {
359
- OPENAI_API_KEY: config.apiKey || "api_key"
468
+ OPENAI_API_KEY: "api_key"
360
469
  };
361
470
  // 使用智能合并
362
471
  const mergedAuth = (0, config_merge_1.mergeJsonConfig)(proxyAuth, currentAuth, config_managed_fields_1.CODEX_AUTH_MANAGED_FIELDS);
@@ -390,6 +499,7 @@ const writeCodexConfig = (dbManager_1, ...args_1) => __awaiter(void 0, [dbManage
390
499
  }
391
500
  });
392
501
  const restoreClaudeConfig = () => __awaiter(void 0, void 0, void 0, function* () {
502
+ var _a, _b;
393
503
  try {
394
504
  const homeDir = os_1.default.homedir();
395
505
  let restoredAnyFile = false;
@@ -410,6 +520,14 @@ const restoreClaudeConfig = () => __awaiter(void 0, void 0, void 0, function* ()
410
520
  console.warn('Failed to parse current settings.json during restore, using empty object:', error);
411
521
  }
412
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
+ }
413
531
  // 生成合并后的配置(备份作为基础,合并当前的非管理字段)
414
532
  const mergedSettings = (0, config_merge_1.mergeJsonConfig)(backupSettings, currentSettings, config_managed_fields_1.CLAUDE_SETTINGS_MANAGED_FIELDS);
415
533
  // 原子性写入合并后的配置
@@ -927,6 +1045,21 @@ const listInstalledSkills = () => {
927
1045
  const registerRoutes = (dbManager, proxyServer) => __awaiter(void 0, void 0, void 0, function* () {
928
1046
  updateProxyConfig(dbManager.getConfig());
929
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
+ });
930
1063
  // 鉴权相关路由 - 公开访问
931
1064
  app.get('/api/auth/status', (_req, res) => {
932
1065
  const response = { enabled: (0, auth_1.isAuthEnabled)() };
@@ -947,10 +1080,10 @@ const registerRoutes = (dbManager, proxyServer) => __awaiter(void 0, void 0, voi
947
1080
  res.status(401).json({ error: 'Invalid auth code' });
948
1081
  }
949
1082
  });
950
- // 鉴权中间件 - 保护所有 /api/* 路由 (除了 /api/auth/*)
1083
+ // 鉴权中间件 - 保护所有 /api/* 路由 (除了 /api/auth/* 和 /api/lan/discover)
951
1084
  app.use('/api', (req, res, next) => {
952
- if (req.path.startsWith('/auth/')) {
953
- next(); // /api/auth/* 路由不需要鉴权
1085
+ if (req.path.startsWith('/auth/') || req.path === '/lan/discover') {
1086
+ next(); // /api/auth/* 和 /api/lan/discover 路由不需要鉴权
954
1087
  }
955
1088
  else {
956
1089
  (0, auth_1.authMiddleware)(req, res, next);
@@ -1137,6 +1270,48 @@ const registerRoutes = (dbManager, proxyServer) => __awaiter(void 0, void 0, voi
1137
1270
  });
1138
1271
  res.json(allStatuses);
1139
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
+ })));
1140
1315
  // 清除规则的错误状态(广播 idle 状态给所有客户端)
1141
1316
  app.post('/api/rules/:id/clear-status', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
1142
1317
  const { id } = req.params;
@@ -1252,9 +1427,236 @@ const registerRoutes = (dbManager, proxyServer) => __awaiter(void 0, void 0, voi
1252
1427
  yield proxyServer.updateConfig(latestConfig);
1253
1428
  updateProxyConfig(latestConfig);
1254
1429
  yield syncConfigsOnGlobalConfigUpdate(dbManager);
1430
+ applyWriteLocalRecords(proxyServer);
1255
1431
  }
1256
1432
  res.json(result);
1257
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
+ })));
1258
1660
  // Skills 管理相关
1259
1661
  app.get('/api/skills/installed', (_req, res) => {
1260
1662
  const skills = listInstalledSkills();
@@ -1602,6 +2004,7 @@ ${instruction}
1602
2004
  ? requestedBypass
1603
2005
  : appConfig.enableBypassPermissionsSupport;
1604
2006
  const result = yield writeClaudeConfig(dbManager, enableAgentTeams, enableBypassPermissionsSupport, undefined, appConfig.claudeDefaultModel, appConfig.autocompactPctOverride);
2007
+ applyWriteLocalRecords(proxyServer);
1605
2008
  res.json(result);
1606
2009
  })));
1607
2010
  app.post('/api/write-config/codex', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
@@ -1613,6 +2016,7 @@ ${instruction}
1613
2016
  ? appConfig.codexModelReasoningEffort
1614
2017
  : DEFAULT_CODEX_REASONING_EFFORT;
1615
2018
  const result = yield writeCodexConfig(dbManager, modelReasoningEffort, appConfig.codexDefaultModel);
2019
+ applyWriteLocalRecords(proxyServer);
1616
2020
  res.json(result);
1617
2021
  })));
1618
2022
  // 兼容接口:更新全局 Agent Teams 配置
@@ -1625,6 +2029,7 @@ ${instruction}
1625
2029
  yield proxyServer.updateConfig(latestConfig);
1626
2030
  updateProxyConfig(latestConfig);
1627
2031
  yield syncConfigsOnGlobalConfigUpdate(dbManager);
2032
+ applyWriteLocalRecords(proxyServer);
1628
2033
  }
1629
2034
  res.json(result);
1630
2035
  })));
@@ -1638,6 +2043,7 @@ ${instruction}
1638
2043
  yield proxyServer.updateConfig(latestConfig);
1639
2044
  updateProxyConfig(latestConfig);
1640
2045
  yield syncConfigsOnGlobalConfigUpdate(dbManager);
2046
+ applyWriteLocalRecords(proxyServer);
1641
2047
  }
1642
2048
  res.json(result);
1643
2049
  })));
@@ -1654,6 +2060,7 @@ ${instruction}
1654
2060
  yield proxyServer.updateConfig(latestConfig);
1655
2061
  updateProxyConfig(latestConfig);
1656
2062
  yield syncConfigsOnGlobalConfigUpdate(dbManager);
2063
+ applyWriteLocalRecords(proxyServer);
1657
2064
  }
1658
2065
  res.json(result);
1659
2066
  })));
@@ -2064,6 +2471,572 @@ ${instruction}
2064
2471
  }
2065
2472
  res.json(result);
2066
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
+ })));
2067
3040
  // 写入MCP配置到Claude Code或Codex的全局配置文件
2068
3041
  const writeMCPConfig = (targetType) => __awaiter(void 0, void 0, void 0, function* () {
2069
3042
  try {
@@ -2300,6 +3273,22 @@ const start = () => __awaiter(void 0, void 0, void 0, function* () {
2300
3273
  }
2301
3274
  catch ( /* ignore */_a) { /* ignore */ }
2302
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
+ }
2303
3292
  // Initialize proxy server and register proxy routes last
2304
3293
  proxyServer.initialize();
2305
3294
  // Register admin routes first
@@ -2374,7 +3363,16 @@ const start = () => __awaiter(void 0, void 0, void 0, function* () {
2374
3363
  catch (error) {
2375
3364
  console.error('[Shutdown ...] Failed to restore Codex config:', error);
2376
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
+ }
2377
3373
  dbManager.close();
3374
+ // 清理规则状态广播器(关闭 SSE 连接)
3375
+ rules_status_service_1.rulesStatusBroadcaster.destroy();
2378
3376
  yield Promise.race([
2379
3377
  new Promise((resolve) => {
2380
3378
  server.close(() => resolve());