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.
Files changed (32) hide show
  1. package/README.md +1 -0
  2. package/bin/restore.js +14 -7
  3. package/bin/utils/managed-fields.js +62 -0
  4. package/dist/server/access-keys/index.js +173 -0
  5. package/dist/server/access-keys/key-logger.js +358 -0
  6. package/dist/server/access-keys/key-resolver.js +51 -0
  7. package/dist/server/access-keys/key-session-tracker.js +217 -0
  8. package/dist/server/access-keys/manager.js +206 -0
  9. package/dist/server/access-keys/policy-manager.js +144 -0
  10. package/dist/server/access-keys/quota-checker.js +197 -0
  11. package/dist/server/access-keys/usage-tracker.js +279 -0
  12. package/dist/server/auth.js +16 -4
  13. package/dist/server/coding-plan-headers.js +121 -0
  14. package/dist/server/config-managed-fields.js +2 -0
  15. package/dist/server/conversions/index.js +8 -0
  16. package/dist/server/conversions/utils/tool-result.js +35 -0
  17. package/dist/server/fs-database.js +72 -1
  18. package/dist/server/main.js +1162 -13
  19. package/dist/server/proxy-server.js +662 -128
  20. package/dist/server/rules-status-service.js +32 -3
  21. package/dist/server/session-launcher.js +282 -0
  22. package/dist/server/session-migration.js +419 -0
  23. package/dist/server/transformers/chunk-collector.js +28 -1
  24. package/dist/server/transformers/model-rewrite-transform.js +128 -0
  25. package/dist/ui/assets/claude-XtpLmGtF.webp +0 -0
  26. package/dist/ui/assets/index-Cws89pD2.js +828 -0
  27. package/dist/ui/assets/index-CzfKxImD.css +1 -0
  28. package/dist/ui/assets/openai-CPEiZpaN.webp +0 -0
  29. package/dist/ui/index.html +2 -2
  30. package/package.json +1 -1
  31. package/dist/ui/assets/index-BHR12ImE.css +0 -1
  32. package/dist/ui/assets/index-CumAhpXg.js +0 -517
@@ -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 || '127.0.0.1';
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 = (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 = {}) {
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: config.apiKey || "api_key",
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 = (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 = {}) {
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: config.apiKey || "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());