aicodeswitch 1.4.1 → 1.5.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.
@@ -0,0 +1,514 @@
1
+ import express, { Request, Response, NextFunction } from 'express';
2
+ import cors from 'cors';
3
+ import dotenv from 'dotenv';
4
+ import fs from 'fs';
5
+ import path from 'path';
6
+ import { DatabaseManager } from './database';
7
+ import { ProxyServer } from './proxy-server';
8
+ import type { AppConfig, LoginRequest, LoginResponse, AuthStatus } from '../types';
9
+ import os from 'os';
10
+ import { isAuthEnabled, verifyAuthCode, generateToken, authMiddleware } from './auth';
11
+ import { checkVersionUpdate } from './version-check';
12
+
13
+ const dotenvPath = path.resolve(os.homedir(), '.aicodeswitch/aicodeswitch.conf');
14
+ if (fs.existsSync(dotenvPath)) {
15
+ dotenv.config({ path: dotenvPath });
16
+ }
17
+
18
+ const host = process.env.HOST || '127.0.0.1';
19
+ const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 4567;
20
+ const dataDir = process.env.DATA_DIR ? path.resolve(process.cwd(), process.env.DATA_DIR) : path.join(os.homedir(), '.aicodeswitch/data');
21
+
22
+ const app = express();
23
+ app.use(cors());
24
+ app.use(express.json({ limit: '10mb' }));
25
+ app.use(express.urlencoded({ extended: true }));
26
+
27
+ const asyncHandler =
28
+ (handler: (req: Request, res: Response, next: NextFunction) => Promise<void> | void) =>
29
+ (req: Request, res: Response, next: NextFunction) => {
30
+ Promise.resolve(handler(req, res, next)).catch(next);
31
+ };
32
+
33
+ const writeClaudeConfig = async (dbManager: DatabaseManager): Promise<boolean> => {
34
+ try {
35
+ const homeDir = os.homedir();
36
+ const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 4567;
37
+ const config = dbManager.getConfig();
38
+
39
+ // Claude Code settings.json
40
+ const claudeDir = path.join(homeDir, '.claude');
41
+ const claudeSettingsPath = path.join(claudeDir, 'settings.json');
42
+ const claudeSettingsBakPath = path.join(claudeDir, 'settings.json.bak');
43
+ const claudeJsonBakPath = path.join(homeDir, '.claude.json.bak');
44
+
45
+ // Check if any backup file already exists
46
+ if (fs.existsSync(claudeSettingsBakPath) || fs.existsSync(claudeJsonBakPath)) {
47
+ console.error('Claude backup files already exist, refusing to overwrite');
48
+ return false;
49
+ }
50
+
51
+ if (fs.existsSync(claudeSettingsPath)) {
52
+ fs.renameSync(claudeSettingsPath, claudeSettingsBakPath);
53
+ }
54
+
55
+ if (!fs.existsSync(claudeDir)) {
56
+ fs.mkdirSync(claudeDir, { recursive: true });
57
+ }
58
+
59
+ const claudeSettings = {
60
+ env: {
61
+ ANTHROPIC_AUTH_TOKEN: config.apiKey || "api_key",
62
+ ANTHROPIC_BASE_URL: `http://${host}:${port}/claude-code`,
63
+ API_TIMEOUT_MS: "3000000"
64
+ }
65
+ };
66
+
67
+ fs.writeFileSync(claudeSettingsPath, JSON.stringify(claudeSettings, null, 2));
68
+
69
+ // Claude Code .claude.json
70
+ const claudeJsonPath = path.join(homeDir, '.claude.json');
71
+
72
+ if (fs.existsSync(claudeJsonPath)) {
73
+ fs.renameSync(claudeJsonPath, claudeJsonBakPath);
74
+ }
75
+
76
+ let claudeJson: any = {};
77
+ if (fs.existsSync(claudeJsonPath)) {
78
+ claudeJson = JSON.parse(fs.readFileSync(claudeJsonPath, 'utf8'));
79
+ }
80
+ claudeJson.hasCompletedOnboarding = true;
81
+
82
+ fs.writeFileSync(claudeJsonPath, JSON.stringify(claudeJson, null, 2));
83
+
84
+ return true;
85
+ } catch (error) {
86
+ console.error('Failed to write Claude config files:', error);
87
+ return false;
88
+ }
89
+ };
90
+
91
+ const writeCodexConfig = async (dbManager: DatabaseManager): Promise<boolean> => {
92
+ try {
93
+ const homeDir = os.homedir();
94
+ const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 4567;
95
+ const config = dbManager.getConfig();
96
+
97
+ // Codex config.toml
98
+ const codexDir = path.join(homeDir, '.codex');
99
+ const codexConfigPath = path.join(codexDir, 'config.toml');
100
+ const codexConfigBakPath = path.join(codexDir, 'config.toml.bak');
101
+ const codexAuthBakPath = path.join(codexDir, 'auth.json.bak');
102
+
103
+ // Check if any backup file already exists
104
+ if (fs.existsSync(codexConfigBakPath) || fs.existsSync(codexAuthBakPath)) {
105
+ console.error('Codex backup files already exist, refusing to overwrite');
106
+ return false;
107
+ }
108
+
109
+ if (fs.existsSync(codexConfigPath)) {
110
+ fs.renameSync(codexConfigPath, codexConfigBakPath);
111
+ }
112
+
113
+ if (!fs.existsSync(codexDir)) {
114
+ fs.mkdirSync(codexDir, { recursive: true });
115
+ }
116
+
117
+ const codexConfig = `model_provider = "aicodeswitch"
118
+ model = "gpt-5.1-codex"
119
+ model_reasoning_effort = "high"
120
+ disable_response_storage = true
121
+
122
+
123
+ [model_providers.aicodeswitch]
124
+ name = "aicodeswitch"
125
+ base_url = "http://${host}:${port}/codex"
126
+ wire_api = "responses"
127
+ requires_openai_auth = true
128
+ `;
129
+
130
+ fs.writeFileSync(codexConfigPath, codexConfig);
131
+
132
+ // Codex auth.json
133
+ const codexAuthPath = path.join(codexDir, 'auth.json');
134
+
135
+ if (fs.existsSync(codexAuthPath)) {
136
+ fs.renameSync(codexAuthPath, codexAuthBakPath);
137
+ }
138
+
139
+ const codexAuth = {
140
+ OPENAI_API_KEY: config.apiKey || "api_key"
141
+ };
142
+
143
+ fs.writeFileSync(codexAuthPath, JSON.stringify(codexAuth, null, 2));
144
+
145
+ return true;
146
+ } catch (error) {
147
+ console.error('Failed to write Codex config files:', error);
148
+ return false;
149
+ }
150
+ };
151
+
152
+ const restoreClaudeConfig = async (): Promise<boolean> => {
153
+ try {
154
+ const homeDir = os.homedir();
155
+
156
+ // Restore Claude Code settings.json
157
+ const claudeDir = path.join(homeDir, '.claude');
158
+ const claudeSettingsPath = path.join(claudeDir, 'settings.json');
159
+ const claudeSettingsBakPath = path.join(claudeDir, 'settings.json.bak');
160
+
161
+ if (fs.existsSync(claudeSettingsBakPath)) {
162
+ if (fs.existsSync(claudeSettingsPath)) {
163
+ fs.unlinkSync(claudeSettingsPath);
164
+ }
165
+ fs.renameSync(claudeSettingsBakPath, claudeSettingsPath);
166
+ }
167
+
168
+ // Restore Claude Code .claude.json
169
+ const claudeJsonPath = path.join(homeDir, '.claude.json');
170
+ const claudeJsonBakPath = path.join(homeDir, '.claude.json.bak');
171
+
172
+ if (fs.existsSync(claudeJsonBakPath)) {
173
+ if (fs.existsSync(claudeJsonPath)) {
174
+ fs.unlinkSync(claudeJsonPath);
175
+ }
176
+ fs.renameSync(claudeJsonBakPath, claudeJsonPath);
177
+ }
178
+
179
+ return true;
180
+ } catch (error) {
181
+ console.error('Failed to restore Claude config files:', error);
182
+ return false;
183
+ }
184
+ };
185
+
186
+ const restoreCodexConfig = async (): Promise<boolean> => {
187
+ try {
188
+ const homeDir = os.homedir();
189
+
190
+ // Restore Codex config.toml
191
+ const codexDir = path.join(homeDir, '.codex');
192
+ const codexConfigPath = path.join(codexDir, 'config.toml');
193
+ const codexConfigBakPath = path.join(codexDir, 'config.toml.bak');
194
+
195
+ if (fs.existsSync(codexConfigBakPath)) {
196
+ if (fs.existsSync(codexConfigPath)) {
197
+ fs.unlinkSync(codexConfigPath);
198
+ }
199
+ fs.renameSync(codexConfigBakPath, codexConfigPath);
200
+ }
201
+
202
+ // Restore Codex auth.json
203
+ const codexAuthPath = path.join(codexDir, 'auth.json');
204
+ const codexAuthBakPath = path.join(codexDir, 'auth.json.bak');
205
+
206
+ if (fs.existsSync(codexAuthBakPath)) {
207
+ if (fs.existsSync(codexAuthPath)) {
208
+ fs.unlinkSync(codexAuthPath);
209
+ }
210
+ fs.renameSync(codexAuthBakPath, codexAuthPath);
211
+ }
212
+
213
+ return true;
214
+ } catch (error) {
215
+ console.error('Failed to restore Codex config files:', error);
216
+ return false;
217
+ }
218
+ };
219
+
220
+ const checkClaudeBackupExists = (): boolean => {
221
+ try {
222
+ const homeDir = os.homedir();
223
+ const claudeSettingsBakPath = path.join(homeDir, '.claude', 'settings.json.bak');
224
+ const claudeJsonBakPath = path.join(homeDir, '.claude.json.bak');
225
+
226
+ return fs.existsSync(claudeSettingsBakPath) || fs.existsSync(claudeJsonBakPath);
227
+ } catch (error) {
228
+ console.error('Failed to check Claude backup files:', error);
229
+ return false;
230
+ }
231
+ };
232
+
233
+ const checkCodexBackupExists = (): boolean => {
234
+ try {
235
+ const homeDir = os.homedir();
236
+ const codexConfigBakPath = path.join(homeDir, '.codex', 'config.toml.bak');
237
+ const codexAuthBakPath = path.join(homeDir, '.codex', 'auth.json.bak');
238
+
239
+ return fs.existsSync(codexConfigBakPath) || fs.existsSync(codexAuthBakPath);
240
+ } catch (error) {
241
+ console.error('Failed to check Codex backup files:', error);
242
+ return false;
243
+ }
244
+ };
245
+
246
+ const registerRoutes = (dbManager: DatabaseManager, proxyServer: ProxyServer) => {
247
+ app.get('/health', (_req, res) => res.json({ status: 'ok' }));
248
+
249
+ // 鉴权相关路由 - 公开访问
250
+ app.get('/api/auth/status', (_req, res) => {
251
+ const response: AuthStatus = { enabled: isAuthEnabled() };
252
+ res.json(response);
253
+ });
254
+
255
+ app.post('/api/auth/login', (req, res) => {
256
+ const { authCode } = req.body as LoginRequest;
257
+
258
+ if (!authCode) {
259
+ res.status(400).json({ error: 'Auth code is required' });
260
+ return;
261
+ }
262
+
263
+ if (verifyAuthCode(authCode)) {
264
+ const token = generateToken();
265
+ const response: LoginResponse = { token };
266
+ res.json(response);
267
+ } else {
268
+ res.status(401).json({ error: 'Invalid auth code' });
269
+ }
270
+ });
271
+
272
+ // 鉴权中间件 - 保护所有 /api/* 路由 (除了 /api/auth/*)
273
+ app.use('/api', (req, res, next) => {
274
+ if (req.path.startsWith('/auth/')) {
275
+ next(); // /api/auth/* 路由不需要鉴权
276
+ } else {
277
+ authMiddleware(req, res, next);
278
+ }
279
+ });
280
+
281
+ app.get('/api/vendors', (_req, res) => res.json(dbManager.getVendors()));
282
+ app.post('/api/vendors', (req, res) => res.json(dbManager.createVendor(req.body)));
283
+ app.put('/api/vendors/:id', (req, res) => res.json(dbManager.updateVendor(req.params.id, req.body)));
284
+ app.delete('/api/vendors/:id', (req, res) => res.json(dbManager.deleteVendor(req.params.id)));
285
+
286
+ app.get('/api/services', (req, res) => {
287
+ const vendorId = typeof req.query.vendorId === 'string' ? req.query.vendorId : undefined;
288
+ res.json(dbManager.getAPIServices(vendorId));
289
+ });
290
+ app.post('/api/services', (req, res) => res.json(dbManager.createAPIService(req.body)));
291
+ app.put('/api/services/:id', (req, res) => res.json(dbManager.updateAPIService(req.params.id, req.body)));
292
+ app.delete('/api/services/:id', (req, res) => res.json(dbManager.deleteAPIService(req.params.id)));
293
+
294
+ app.get('/api/routes', (_req, res) => res.json(dbManager.getRoutes()));
295
+ app.post('/api/routes', (req, res) => res.json(dbManager.createRoute(req.body)));
296
+ app.put('/api/routes/:id', (req, res) => res.json(dbManager.updateRoute(req.params.id, req.body)));
297
+ app.delete('/api/routes/:id', (req, res) => res.json(dbManager.deleteRoute(req.params.id)));
298
+ app.post(
299
+ '/api/routes/:id/activate',
300
+ asyncHandler(async (req, res) => {
301
+ const result = dbManager.activateRoute(req.params.id);
302
+ if (result) {
303
+ await proxyServer.reloadRoutes();
304
+ }
305
+ res.json(result);
306
+ })
307
+ );
308
+
309
+ app.post(
310
+ '/api/routes/:id/deactivate',
311
+ asyncHandler(async (req, res) => {
312
+ const result = dbManager.deactivateRoute(req.params.id);
313
+ if (result) {
314
+ await proxyServer.reloadRoutes();
315
+ }
316
+ res.json(result);
317
+ })
318
+ );
319
+
320
+ app.get('/api/rules', (req, res) => {
321
+ const routeId = typeof req.query.routeId === 'string' ? req.query.routeId : undefined;
322
+ res.json(dbManager.getRules(routeId));
323
+ });
324
+ app.post('/api/rules', (req, res) => res.json(dbManager.createRule(req.body)));
325
+ app.put('/api/rules/:id', (req, res) => res.json(dbManager.updateRule(req.params.id, req.body)));
326
+ app.delete('/api/rules/:id', (req, res) => res.json(dbManager.deleteRule(req.params.id)));
327
+
328
+ app.get(
329
+ '/api/logs',
330
+ asyncHandler(async (req, res) => {
331
+ const rawLimit = typeof req.query.limit === 'string' ? parseInt(req.query.limit, 10) : NaN;
332
+ const rawOffset = typeof req.query.offset === 'string' ? parseInt(req.query.offset, 10) : NaN;
333
+ const limit = Number.isFinite(rawLimit) ? rawLimit : 100;
334
+ const offset = Number.isFinite(rawOffset) ? rawOffset : 0;
335
+ const logs = await dbManager.getLogs(limit, offset);
336
+ res.json(logs);
337
+ })
338
+ );
339
+ app.delete(
340
+ '/api/logs',
341
+ asyncHandler(async (_req, res) => {
342
+ await dbManager.clearLogs();
343
+ res.json(true);
344
+ })
345
+ );
346
+
347
+ app.get(
348
+ '/api/access-logs',
349
+ asyncHandler(async (req, res) => {
350
+ const rawLimit = typeof req.query.limit === 'string' ? parseInt(req.query.limit, 10) : NaN;
351
+ const rawOffset = typeof req.query.offset === 'string' ? parseInt(req.query.offset, 10) : NaN;
352
+ const limit = Number.isFinite(rawLimit) ? rawLimit : 100;
353
+ const offset = Number.isFinite(rawOffset) ? rawOffset : 0;
354
+ const logs = await dbManager.getAccessLogs(limit, offset);
355
+ res.json(logs);
356
+ })
357
+ );
358
+ app.delete(
359
+ '/api/access-logs',
360
+ asyncHandler(async (_req, res) => {
361
+ await dbManager.clearAccessLogs();
362
+ res.json(true);
363
+ })
364
+ );
365
+
366
+ app.get(
367
+ '/api/error-logs',
368
+ asyncHandler(async (req, res) => {
369
+ const rawLimit = typeof req.query.limit === 'string' ? parseInt(req.query.limit, 10) : NaN;
370
+ const rawOffset = typeof req.query.offset === 'string' ? parseInt(req.query.offset, 10) : NaN;
371
+ const limit = Number.isFinite(rawLimit) ? rawLimit : 100;
372
+ const offset = Number.isFinite(rawOffset) ? rawOffset : 0;
373
+ const logs = await dbManager.getErrorLogs(limit, offset);
374
+ res.json(logs);
375
+ })
376
+ );
377
+ app.delete(
378
+ '/api/error-logs',
379
+ asyncHandler(async (_req, res) => {
380
+ await dbManager.clearErrorLogs();
381
+ res.json(true);
382
+ })
383
+ );
384
+
385
+ app.get('/api/config', (_req, res) => res.json(dbManager.getConfig()));
386
+ app.put(
387
+ '/api/config',
388
+ asyncHandler(async (req, res) => {
389
+ const config = req.body as AppConfig;
390
+ const result = dbManager.updateConfig(config);
391
+ if (result) {
392
+ await proxyServer.updateConfig(config);
393
+ }
394
+ res.json(result);
395
+ })
396
+ );
397
+
398
+ app.post(
399
+ '/api/write-config/claude',
400
+ asyncHandler(async (_req, res) => {
401
+ const result = await writeClaudeConfig(dbManager);
402
+ res.json(result);
403
+ })
404
+ );
405
+
406
+ app.post(
407
+ '/api/write-config/codex',
408
+ asyncHandler(async (_req, res) => {
409
+ const result = await writeCodexConfig(dbManager);
410
+ res.json(result);
411
+ })
412
+ );
413
+
414
+ app.post(
415
+ '/api/restore-config/claude',
416
+ asyncHandler(async (_req, res) => {
417
+ const result = await restoreClaudeConfig();
418
+ res.json(result);
419
+ })
420
+ );
421
+
422
+ app.post(
423
+ '/api/restore-config/codex',
424
+ asyncHandler(async (_req, res) => {
425
+ const result = await restoreCodexConfig();
426
+ res.json(result);
427
+ })
428
+ );
429
+
430
+ app.get('/api/check-backup/claude', (_req, res) => {
431
+ res.json({ exists: checkClaudeBackupExists() });
432
+ });
433
+
434
+ app.get('/api/check-backup/codex', (_req, res) => {
435
+ res.json({ exists: checkCodexBackupExists() });
436
+ });
437
+
438
+ app.post(
439
+ '/api/export',
440
+ asyncHandler(async (req, res) => {
441
+ const { password } = req.body as { password: string };
442
+ const data = await dbManager.exportData(password);
443
+ res.json({ data });
444
+ })
445
+ );
446
+ app.post(
447
+ '/api/import',
448
+ asyncHandler(async (req, res) => {
449
+ const { encryptedData, password } = req.body as { encryptedData: string; password: string };
450
+ const result = await dbManager.importData(encryptedData, password);
451
+ if (result) {
452
+ await proxyServer.reloadRoutes();
453
+ }
454
+ res.json(result);
455
+ })
456
+ );
457
+
458
+ app.get(
459
+ '/api/version/check',
460
+ asyncHandler(async (_req, res) => {
461
+ const versionInfo = await checkVersionUpdate();
462
+ res.json(versionInfo);
463
+ })
464
+ );
465
+
466
+ app.get(
467
+ '/api/statistics',
468
+ asyncHandler(async (req, res) => {
469
+ const days = typeof req.query.days === 'string' ? parseInt(req.query.days, 10) : 30;
470
+ const stats = await dbManager.getStatistics(days);
471
+ res.json(stats);
472
+ })
473
+ );
474
+
475
+ app.use(express.static(path.resolve(__dirname, '../ui')));
476
+ };
477
+
478
+ const start = async () => {
479
+ fs.mkdirSync(dataDir, { recursive: true });
480
+
481
+ const dbManager = new DatabaseManager(dataDir);
482
+ await dbManager.initialize();
483
+
484
+ const proxyServer = new ProxyServer(dbManager, app);
485
+
486
+ // Register admin routes first
487
+ registerRoutes(dbManager, proxyServer);
488
+
489
+ // Initialize proxy server and register proxy routes last
490
+ await proxyServer.initialize();
491
+
492
+ const adminServer = app.listen(port, host, () => {
493
+ console.log(`Admin server running on http://${host}:${port}`);
494
+ });
495
+
496
+ const shutdown = async () => {
497
+ console.log('Shutting down server...');
498
+ dbManager.close();
499
+ adminServer.close(() => process.exit(0));
500
+ };
501
+
502
+ process.on('SIGINT', shutdown);
503
+ process.on('SIGTERM', shutdown);
504
+ };
505
+
506
+ app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
507
+ console.error(err);
508
+ res.status(500).json({ error: err.message || 'Internal server error' });
509
+ });
510
+
511
+ start().catch((error) => {
512
+ console.error('Failed to start server:', error);
513
+ process.exit(1);
514
+ });