codexmate 0.0.12 → 0.0.13

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.
@@ -6,9 +6,10 @@ const crypto = require('crypto');
6
6
  const toml = require('@iarna/toml');
7
7
  const JSON5 = require('json5');
8
8
  const zipLib = require('zip-lib');
9
- const { exec, execSync, spawn } = require('child_process');
9
+ const { exec, execSync, spawn, spawnSync } = require('child_process');
10
10
  const http = require('http');
11
11
  const https = require('https');
12
+ const net = require('net');
12
13
  const readline = require('readline');
13
14
  const {
14
15
  expandHomePath,
@@ -52,6 +53,7 @@ const {
52
53
  parseMaxMessagesValue,
53
54
  resolveMaxMessagesValue
54
55
  } = require('./lib/cli-session-utils');
56
+ const { createMcpStdioServer } = require('./lib/mcp-stdio');
55
57
 
56
58
  const DEFAULT_WEB_PORT = 3737;
57
59
  const DEFAULT_WEB_HOST = '127.0.0.1';
@@ -62,9 +64,12 @@ const DEFAULT_WEB_HOST = '127.0.0.1';
62
64
  const CONFIG_DIR = path.join(os.homedir(), '.codex');
63
65
  const CONFIG_FILE = path.join(CONFIG_DIR, 'config.toml');
64
66
  const AUTH_FILE = path.join(CONFIG_DIR, 'auth.json');
67
+ const AUTH_PROFILES_DIR = path.join(CONFIG_DIR, 'auth-profiles');
68
+ const AUTH_REGISTRY_FILE = path.join(AUTH_PROFILES_DIR, 'registry.json');
65
69
  const MODELS_FILE = path.join(CONFIG_DIR, 'models.json');
66
70
  const CURRENT_MODELS_FILE = path.join(CONFIG_DIR, 'provider-current-models.json');
67
71
  const INIT_MARK_FILE = path.join(CONFIG_DIR, 'codexmate-init.json');
72
+ const BUILTIN_PROXY_SETTINGS_FILE = path.join(CONFIG_DIR, 'codexmate-proxy.json');
68
73
  const CODEX_SESSIONS_DIR = path.join(CONFIG_DIR, 'sessions');
69
74
  const OPENCLAW_DIR = path.join(os.homedir(), '.openclaw');
70
75
  const OPENCLAW_CONFIG_FILE = path.join(OPENCLAW_DIR, 'openclaw.json');
@@ -74,6 +79,7 @@ const CLAUDE_SETTINGS_FILE = path.join(CLAUDE_DIR, 'settings.json');
74
79
  const CLAUDE_PROJECTS_DIR = path.join(os.homedir(), '.claude', 'projects');
75
80
  const RECENT_CONFIGS_FILE = path.join(CONFIG_DIR, 'recent-configs.json');
76
81
  const DEFAULT_CLAUDE_MODEL = 'glm-4.7';
82
+ const CODEX_BACKUP_NAME = 'codex-config';
77
83
 
78
84
  const DEFAULT_MODELS = ['gpt-5.3-codex', 'gpt-5.1-codex-max', 'gpt-4-turbo', 'gpt-4'];
79
85
  const SPEED_TEST_TIMEOUT_MS = 8000;
@@ -97,6 +103,16 @@ const MODELS_NEGATIVE_CACHE_TTL_MS = 5 * 1000;
97
103
  const MODELS_CACHE_MAX_ENTRIES = 50;
98
104
  const MODELS_RESPONSE_MAX_BYTES = 1024 * 1024;
99
105
  const MAX_RECENT_CONFIGS = 3;
106
+ const MAX_UPLOAD_SIZE = 200 * 1024 * 1024;
107
+ const BUILTIN_PROXY_PROVIDER_NAME = 'codexmate-proxy';
108
+ const DEFAULT_BUILTIN_PROXY_SETTINGS = Object.freeze({
109
+ enabled: false,
110
+ host: '127.0.0.1',
111
+ port: 8318,
112
+ provider: '',
113
+ authSource: 'provider',
114
+ timeoutMs: 30000
115
+ });
100
116
  const BOOTSTRAP_TEXT_MARKERS = [
101
117
  'agents.md instructions',
102
118
  '<instructions>',
@@ -104,6 +120,20 @@ const BOOTSTRAP_TEXT_MARKERS = [
104
120
  'you are a coding agent',
105
121
  'codex cli'
106
122
  ];
123
+ const CLI_INSTALL_TARGETS = Object.freeze([
124
+ {
125
+ id: 'claude',
126
+ name: 'Claude Code CLI',
127
+ packageName: '@anthropic-ai/claude-code',
128
+ bins: ['claude']
129
+ },
130
+ {
131
+ id: 'codex',
132
+ name: 'Codex CLI',
133
+ packageName: '@openai/codex',
134
+ bins: ['codex']
135
+ }
136
+ ]);
107
137
 
108
138
  const HTTP_KEEP_ALIVE_AGENT = new http.Agent({ keepAlive: true });
109
139
  const HTTPS_KEEP_ALIVE_AGENT = new https.Agent({ keepAlive: true });
@@ -152,6 +182,29 @@ let g_initNotice = '';
152
182
  let g_sessionListCache = new Map();
153
183
  let g_modelsCache = new Map();
154
184
  let g_modelsInFlight = new Map();
185
+ let g_builtinProxyRuntime = null;
186
+ const DEFAULT_LOCAL_PROVIDER_NAME = 'local';
187
+
188
+ function isBuiltinProxyProvider(providerName) {
189
+ return typeof providerName === 'string' && providerName.trim() === BUILTIN_PROXY_PROVIDER_NAME;
190
+ }
191
+
192
+ function isReservedProviderNameForCreation(providerName) {
193
+ return typeof providerName === 'string'
194
+ && providerName.trim().toLowerCase() === DEFAULT_LOCAL_PROVIDER_NAME;
195
+ }
196
+
197
+ function isDefaultLocalProvider(providerName) {
198
+ return typeof providerName === 'string' && providerName.trim() === DEFAULT_LOCAL_PROVIDER_NAME;
199
+ }
200
+
201
+ function isNonDeletableProvider(providerName) {
202
+ return isBuiltinProxyProvider(providerName) || isDefaultLocalProvider(providerName);
203
+ }
204
+
205
+ function isNonEditableProvider(providerName) {
206
+ return isBuiltinProxyProvider(providerName) || isDefaultLocalProvider(providerName);
207
+ }
155
208
 
156
209
  // ============================================================================
157
210
  // 工具函数
@@ -220,6 +273,310 @@ function updateAuthJson(apiKey) {
220
273
  fs.writeFileSync(AUTH_FILE, JSON.stringify(authData, null, 2), 'utf-8');
221
274
  }
222
275
 
276
+ function isPlainObject(value) {
277
+ return !!value && typeof value === 'object' && !Array.isArray(value);
278
+ }
279
+
280
+ function normalizeAuthProfileName(value) {
281
+ const raw = typeof value === 'string' ? value.trim() : '';
282
+ if (!raw) return '';
283
+ const sanitized = raw
284
+ .replace(/[\\\/:*?"<>|]/g, '-')
285
+ .replace(/\s+/g, '-')
286
+ .replace(/^-+|-+$/g, '')
287
+ .slice(0, 120);
288
+ return sanitized;
289
+ }
290
+
291
+ function normalizeAuthRegistry(raw) {
292
+ const fallback = { version: 1, current: '', items: [] };
293
+ if (!isPlainObject(raw)) return fallback;
294
+ const items = Array.isArray(raw.items)
295
+ ? raw.items.filter(item => isPlainObject(item) && typeof item.name === 'string' && item.name.trim())
296
+ : [];
297
+ return {
298
+ version: 1,
299
+ current: typeof raw.current === 'string' ? raw.current.trim() : '',
300
+ items: items.map((item) => ({
301
+ name: normalizeAuthProfileName(item.name) || item.name.trim(),
302
+ fileName: typeof item.fileName === 'string' ? path.basename(item.fileName) : '',
303
+ type: typeof item.type === 'string' ? item.type : '',
304
+ email: typeof item.email === 'string' ? item.email : '',
305
+ accountId: typeof item.accountId === 'string' ? item.accountId : '',
306
+ expired: typeof item.expired === 'string' ? item.expired : '',
307
+ lastRefresh: typeof item.lastRefresh === 'string' ? item.lastRefresh : '',
308
+ updatedAt: typeof item.updatedAt === 'string' ? item.updatedAt : '',
309
+ importedAt: typeof item.importedAt === 'string' ? item.importedAt : '',
310
+ sourceFile: typeof item.sourceFile === 'string' ? item.sourceFile : ''
311
+ }))
312
+ };
313
+ }
314
+
315
+ function readAuthRegistry() {
316
+ const parsed = readJsonFile(AUTH_REGISTRY_FILE, null);
317
+ return normalizeAuthRegistry(parsed);
318
+ }
319
+
320
+ function writeAuthRegistry(registry) {
321
+ writeJsonAtomic(AUTH_REGISTRY_FILE, normalizeAuthRegistry(registry));
322
+ }
323
+
324
+ function parseAuthProfileJson(rawContent, label = '') {
325
+ let parsed;
326
+ try {
327
+ parsed = JSON.parse(stripUtf8Bom(String(rawContent || '')));
328
+ } catch (e) {
329
+ throw new Error(`认证文件不是有效 JSON${label ? `: ${label}` : ''}`);
330
+ }
331
+ if (!isPlainObject(parsed)) {
332
+ throw new Error('认证文件根节点必须是对象');
333
+ }
334
+ const hasCredential = ['access_token', 'refresh_token', 'id_token', 'OPENAI_API_KEY']
335
+ .some((key) => typeof parsed[key] === 'string' && parsed[key].trim());
336
+ if (!hasCredential) {
337
+ throw new Error('认证文件缺少可用凭据(access_token / refresh_token / id_token / OPENAI_API_KEY)');
338
+ }
339
+ return parsed;
340
+ }
341
+
342
+ function buildAuthProfileSummary(name, payload, fileName = '') {
343
+ const safePayload = isPlainObject(payload) ? payload : {};
344
+ return {
345
+ name,
346
+ fileName: fileName || `${name}.json`,
347
+ type: typeof safePayload.type === 'string' ? safePayload.type : '',
348
+ email: typeof safePayload.email === 'string' ? safePayload.email : '',
349
+ accountId: typeof safePayload.account_id === 'string'
350
+ ? safePayload.account_id
351
+ : (typeof safePayload.accountId === 'string' ? safePayload.accountId : ''),
352
+ expired: typeof safePayload.expired === 'string' ? safePayload.expired : '',
353
+ lastRefresh: typeof safePayload.last_refresh === 'string'
354
+ ? safePayload.last_refresh
355
+ : (typeof safePayload.lastRefresh === 'string' ? safePayload.lastRefresh : ''),
356
+ updatedAt: toIsoTime(Date.now())
357
+ };
358
+ }
359
+
360
+ function getAuthProfileNameFallback(payload, fallbackName = '') {
361
+ const fromPayload = isPlainObject(payload)
362
+ ? (payload.email || payload.account_id || payload.accountId || '')
363
+ : '';
364
+ const fromFallback = typeof fallbackName === 'string' ? fallbackName : '';
365
+ const resolved = normalizeAuthProfileName(fromPayload) || normalizeAuthProfileName(fromFallback);
366
+ if (resolved) return resolved;
367
+ return `auth-${Date.now()}`;
368
+ }
369
+
370
+ function listAuthProfilesInfo() {
371
+ const registry = readAuthRegistry();
372
+ return registry.items.map((item) => ({
373
+ ...item,
374
+ current: item.name === registry.current
375
+ }));
376
+ }
377
+
378
+ function upsertAuthProfile(payload, options = {}) {
379
+ const safePayload = parseAuthProfileJson(JSON.stringify(payload || {}));
380
+ const sourceFile = typeof options.sourceFile === 'string' ? options.sourceFile : '';
381
+ const preferredName = normalizeAuthProfileName(options.name || '');
382
+ const profileName = preferredName || getAuthProfileNameFallback(safePayload, sourceFile);
383
+ const fileName = `${profileName}.json`;
384
+ const profilePath = path.join(AUTH_PROFILES_DIR, fileName);
385
+
386
+ ensureDir(AUTH_PROFILES_DIR);
387
+ writeJsonAtomic(profilePath, safePayload);
388
+
389
+ const registry = readAuthRegistry();
390
+ const meta = buildAuthProfileSummary(profileName, safePayload, fileName);
391
+ meta.importedAt = toIsoTime(Date.now());
392
+ meta.sourceFile = sourceFile || '';
393
+
394
+ const idx = registry.items.findIndex((item) => item.name === profileName);
395
+ if (idx >= 0) {
396
+ registry.items[idx] = {
397
+ ...registry.items[idx],
398
+ ...meta
399
+ };
400
+ } else {
401
+ registry.items.push(meta);
402
+ }
403
+ registry.items.sort((a, b) => a.name.localeCompare(b.name));
404
+
405
+ const shouldActivate = options.activate !== false;
406
+ if (shouldActivate) {
407
+ writeJsonAtomic(AUTH_FILE, safePayload);
408
+ registry.current = profileName;
409
+ }
410
+ writeAuthRegistry(registry);
411
+
412
+ return {
413
+ success: true,
414
+ profile: {
415
+ ...meta,
416
+ current: shouldActivate ? true : registry.current === profileName
417
+ }
418
+ };
419
+ }
420
+
421
+ function importAuthProfileFromFile(filePath, options = {}) {
422
+ const absPath = path.resolve(String(filePath || ''));
423
+ if (!absPath || !fs.existsSync(absPath) || !fs.statSync(absPath).isFile()) {
424
+ throw new Error('认证文件不存在');
425
+ }
426
+ const raw = fs.readFileSync(absPath, 'utf-8');
427
+ const payload = parseAuthProfileJson(raw, path.basename(absPath));
428
+ const fallbackName = path.basename(absPath, path.extname(absPath));
429
+ return upsertAuthProfile(payload, {
430
+ ...options,
431
+ sourceFile: absPath,
432
+ name: options.name || fallbackName
433
+ });
434
+ }
435
+
436
+ function importAuthProfileFromUpload(payload = {}) {
437
+ const fileBase64 = typeof payload.fileBase64 === 'string' ? payload.fileBase64.trim() : '';
438
+ if (!fileBase64) {
439
+ return { error: '缺少认证文件内容' };
440
+ }
441
+ let buffer;
442
+ try {
443
+ buffer = Buffer.from(fileBase64, 'base64');
444
+ } catch (e) {
445
+ return { error: '认证文件不是有效的 base64 编码' };
446
+ }
447
+ if (!buffer || buffer.length === 0) {
448
+ return { error: '认证文件为空' };
449
+ }
450
+ if (buffer.length > 10 * 1024 * 1024) {
451
+ return { error: '认证文件过大(>10MB)' };
452
+ }
453
+
454
+ try {
455
+ const raw = buffer.toString('utf-8');
456
+ const profileData = parseAuthProfileJson(raw, payload.fileName || 'upload.json');
457
+ return upsertAuthProfile(profileData, {
458
+ name: payload.name || path.basename(payload.fileName || '', path.extname(payload.fileName || '')),
459
+ sourceFile: payload.fileName || '',
460
+ activate: payload.activate !== false
461
+ });
462
+ } catch (e) {
463
+ return { error: e.message || '导入认证文件失败' };
464
+ }
465
+ }
466
+
467
+ function switchAuthProfile(name, options = {}) {
468
+ const profileName = normalizeAuthProfileName(name);
469
+ if (!profileName) {
470
+ throw new Error('认证名称不能为空');
471
+ }
472
+ const registry = readAuthRegistry();
473
+ const profile = registry.items.find((item) => item.name === profileName);
474
+ if (!profile) {
475
+ throw new Error(`认证不存在: ${profileName}`);
476
+ }
477
+ const fileName = profile.fileName || `${profileName}.json`;
478
+ const profilePath = path.join(AUTH_PROFILES_DIR, fileName);
479
+ if (!fs.existsSync(profilePath)) {
480
+ throw new Error(`认证文件不存在: ${fileName}`);
481
+ }
482
+ const raw = fs.readFileSync(profilePath, 'utf-8');
483
+ const profileData = parseAuthProfileJson(raw, fileName);
484
+ writeJsonAtomic(AUTH_FILE, profileData);
485
+
486
+ registry.current = profileName;
487
+ const idx = registry.items.findIndex((item) => item.name === profileName);
488
+ if (idx >= 0) {
489
+ registry.items[idx] = {
490
+ ...registry.items[idx],
491
+ updatedAt: toIsoTime(Date.now())
492
+ };
493
+ }
494
+ writeAuthRegistry(registry);
495
+
496
+ if (!options.silent) {
497
+ console.log(`✓ 已切换认证: ${profileName}`);
498
+ if (profile.email) {
499
+ console.log(` 账号: ${profile.email}`);
500
+ }
501
+ console.log();
502
+ }
503
+ return {
504
+ success: true,
505
+ profile: {
506
+ ...profile,
507
+ current: true
508
+ }
509
+ };
510
+ }
511
+
512
+ function deleteAuthProfile(name) {
513
+ const profileName = normalizeAuthProfileName(name);
514
+ if (!profileName) {
515
+ return { error: '认证名称不能为空' };
516
+ }
517
+ const registry = readAuthRegistry();
518
+ const idx = registry.items.findIndex((item) => item.name === profileName);
519
+ if (idx < 0) {
520
+ return { error: '认证不存在' };
521
+ }
522
+ const profile = registry.items[idx];
523
+ const fileName = profile.fileName || `${profileName}.json`;
524
+ const profilePath = path.join(AUTH_PROFILES_DIR, fileName);
525
+
526
+ if (fs.existsSync(profilePath)) {
527
+ try {
528
+ fs.unlinkSync(profilePath);
529
+ } catch (e) {
530
+ return { error: `删除认证文件失败: ${e.message}` };
531
+ }
532
+ }
533
+
534
+ registry.items.splice(idx, 1);
535
+ let switchedTo = '';
536
+ if (registry.current === profileName) {
537
+ if (registry.items.length > 0) {
538
+ const next = registry.items[0];
539
+ try {
540
+ const nextPath = path.join(AUTH_PROFILES_DIR, next.fileName || `${next.name}.json`);
541
+ const raw = fs.readFileSync(nextPath, 'utf-8');
542
+ const nextData = parseAuthProfileJson(raw, next.fileName || `${next.name}.json`);
543
+ writeJsonAtomic(AUTH_FILE, nextData);
544
+ registry.current = next.name;
545
+ switchedTo = next.name;
546
+ } catch (e) {
547
+ registry.current = '';
548
+ }
549
+ } else {
550
+ registry.current = '';
551
+ }
552
+ }
553
+ writeAuthRegistry(registry);
554
+ return {
555
+ success: true,
556
+ switchedTo
557
+ };
558
+ }
559
+
560
+ function resolveAuthTokenFromCurrentProfile() {
561
+ const registry = readAuthRegistry();
562
+ if (!registry.current) return '';
563
+ const profile = registry.items.find((item) => item.name === registry.current);
564
+ if (!profile) return '';
565
+ const filePath = path.join(AUTH_PROFILES_DIR, profile.fileName || `${profile.name}.json`);
566
+ if (!fs.existsSync(filePath)) return '';
567
+ try {
568
+ const raw = fs.readFileSync(filePath, 'utf-8');
569
+ const payload = parseAuthProfileJson(raw, profile.fileName || `${profile.name}.json`);
570
+ if (typeof payload.access_token === 'string' && payload.access_token.trim()) {
571
+ return payload.access_token.trim();
572
+ }
573
+ if (typeof payload.OPENAI_API_KEY === 'string' && payload.OPENAI_API_KEY.trim()) {
574
+ return payload.OPENAI_API_KEY.trim();
575
+ }
576
+ } catch (e) {}
577
+ return '';
578
+ }
579
+
223
580
  function getCodexSessionsDir() {
224
581
  const candidates = [];
225
582
  const envCodexHome = process.env.CODEX_HOME;
@@ -1295,9 +1652,16 @@ function addProviderToConfig(params = {}) {
1295
1652
  const name = typeof params.name === 'string' ? params.name.trim() : '';
1296
1653
  const url = typeof params.url === 'string' ? params.url.trim() : '';
1297
1654
  const key = typeof params.key === 'string' ? params.key.trim() : '';
1655
+ const allowManaged = !!params.allowManaged;
1298
1656
 
1299
1657
  if (!name) return { error: '名称不能为空' };
1300
1658
  if (!url) return { error: 'URL 不能为空' };
1659
+ if (isReservedProviderNameForCreation(name)) {
1660
+ return { error: 'local provider 为系统保留名称,不可新增' };
1661
+ }
1662
+ if (isBuiltinProxyProvider(name) && !allowManaged) {
1663
+ return { error: '本地代理配置为系统内建项,不可手动添加' };
1664
+ }
1301
1665
 
1302
1666
  ensureConfigDir();
1303
1667
 
@@ -1368,14 +1732,21 @@ function updateProviderInConfig(params = {}) {
1368
1732
  const key = params.key !== undefined && params.key !== null
1369
1733
  ? String(params.key).trim()
1370
1734
  : undefined;
1735
+ const allowManaged = !!params.allowManaged;
1371
1736
 
1372
1737
  if (!name) return { error: '名称不能为空' };
1373
1738
  if (!url && key === undefined) {
1374
1739
  return { error: 'URL 或密钥至少填写一项' };
1375
1740
  }
1741
+ if (isNonEditableProvider(name) && !allowManaged) {
1742
+ if (isDefaultLocalProvider(name)) {
1743
+ return { error: 'local provider 为系统保留项,不可编辑' };
1744
+ }
1745
+ return { error: '本地代理配置为系统内建项,不可编辑' };
1746
+ }
1376
1747
 
1377
1748
  try {
1378
- cmdUpdate(name, url || undefined, key, true);
1749
+ cmdUpdate(name, url || undefined, key, true, { allowManaged });
1379
1750
  return { success: true };
1380
1751
  } catch (e) {
1381
1752
  return { error: e.message || '更新失败' };
@@ -1385,6 +1756,12 @@ function updateProviderInConfig(params = {}) {
1385
1756
  function deleteProviderFromConfig(params = {}) {
1386
1757
  const name = typeof params.name === 'string' ? params.name.trim() : '';
1387
1758
  if (!name) return { error: '名称不能为空' };
1759
+ if (isNonDeletableProvider(name)) {
1760
+ if (isDefaultLocalProvider(name)) {
1761
+ return { error: 'local provider 为系统保留项,不可删除' };
1762
+ }
1763
+ return { error: '本地代理配置为系统内建项,不可删除' };
1764
+ }
1388
1765
  if (!fs.existsSync(CONFIG_FILE)) {
1389
1766
  return { error: 'config.toml 不存在' };
1390
1767
  }
@@ -1410,6 +1787,13 @@ function deleteProviderFromConfig(params = {}) {
1410
1787
 
1411
1788
  function performProviderDeletion(name, options = {}) {
1412
1789
  const silent = !!options.silent;
1790
+ if (isNonDeletableProvider(name)) {
1791
+ const msg = isDefaultLocalProvider(name)
1792
+ ? 'local provider 为系统保留项,不可删除'
1793
+ : '本地代理配置为系统内建项,不可删除';
1794
+ if (!silent) console.error('错误:', msg);
1795
+ return { error: msg };
1796
+ }
1413
1797
  const config = options.config || readConfig();
1414
1798
  if (!config.model_providers || !config.model_providers[name]) {
1415
1799
  const msg = '提供商不存在';
@@ -2789,143 +3173,661 @@ function findClaudeSessionIndexPath(sessionFilePath) {
2789
3173
  return '';
2790
3174
  }
2791
3175
 
2792
- function updateClaudeSessionIndex(indexPath, sessionFilePath, sessionId) {
2793
- if (!indexPath || !fs.existsSync(indexPath)) {
2794
- return;
2795
- }
2796
- const index = readJsonFile(indexPath, null);
2797
- if (!index || !Array.isArray(index.entries)) {
2798
- return;
3176
+ function canListenPort(host, port) {
3177
+ return new Promise((resolve) => {
3178
+ const tester = net.createServer();
3179
+ tester.unref();
3180
+ tester.once('error', () => {
3181
+ resolve(false);
3182
+ });
3183
+ tester.once('listening', () => {
3184
+ tester.close(() => resolve(true));
3185
+ });
3186
+ tester.listen(port, host);
3187
+ });
3188
+ }
3189
+
3190
+ async function findAvailablePort(host, startPort, maxAttempts = 20) {
3191
+ const start = parseInt(String(startPort), 10);
3192
+ if (!Number.isFinite(start) || start <= 0) {
3193
+ return 0;
2799
3194
  }
2800
- const resolvedFile = sessionFilePath ? path.resolve(sessionFilePath) : '';
2801
- const resolvedLower = resolvedFile ? resolvedFile.toLowerCase() : '';
2802
- const filtered = index.entries.filter((entry) => {
2803
- if (!entry || typeof entry !== 'object') {
2804
- return false;
2805
- }
2806
- const entrySessionId = typeof entry.sessionId === 'string' ? entry.sessionId : '';
2807
- if (sessionId && entrySessionId === sessionId) {
2808
- return false;
3195
+ const attempts = Number.isFinite(maxAttempts) && maxAttempts > 0 ? maxAttempts : 20;
3196
+ for (let offset = 0; offset < attempts; offset += 1) {
3197
+ const candidate = start + offset;
3198
+ if (candidate > 65535) {
3199
+ break;
2809
3200
  }
2810
- if (entry.fullPath) {
2811
- const expanded = expandHomePath(entry.fullPath);
2812
- const entryPath = expanded ? path.resolve(expanded) : '';
2813
- if (entryPath && resolvedLower && entryPath.toLowerCase() === resolvedLower) {
2814
- return false;
2815
- }
3201
+ // eslint-disable-next-line no-await-in-loop
3202
+ const ok = await canListenPort(host, candidate);
3203
+ if (ok) {
3204
+ return candidate;
2816
3205
  }
2817
- return true;
2818
- });
2819
- if (filtered.length === index.entries.length) {
2820
- return;
2821
3206
  }
2822
- index.entries = filtered;
2823
- try {
2824
- fs.writeFileSync(indexPath, JSON.stringify(index, null, 2), 'utf-8');
2825
- } catch (e) {}
3207
+ return 0;
2826
3208
  }
2827
3209
 
2828
- async function deleteSessionData(params = {}) {
2829
- const source = params.source === 'claude' ? 'claude' : (params.source === 'codex' ? 'codex' : '');
2830
- if (!source) {
2831
- return { error: 'Invalid source' };
2832
- }
3210
+ function normalizeBuiltinProxySettings(raw) {
3211
+ const merged = {
3212
+ ...DEFAULT_BUILTIN_PROXY_SETTINGS,
3213
+ ...(isPlainObject(raw) ? raw : {})
3214
+ };
3215
+ const host = typeof merged.host === 'string' ? merged.host.trim() : '';
3216
+ const port = parseInt(String(merged.port), 10);
3217
+ const provider = typeof merged.provider === 'string' ? merged.provider.trim() : '';
3218
+ const authSourceRaw = typeof merged.authSource === 'string' ? merged.authSource.trim().toLowerCase() : '';
3219
+ const timeoutMs = parseInt(String(merged.timeoutMs), 10);
3220
+ const authSource = authSourceRaw === 'profile' || authSourceRaw === 'none' ? authSourceRaw : 'provider';
2833
3221
 
2834
- const filePath = resolveSessionFilePath(source, params.filePath, params.sessionId);
2835
- if (!filePath) {
2836
- return { error: 'Session file not found' };
2837
- }
3222
+ return {
3223
+ enabled: merged.enabled !== false,
3224
+ host: host || DEFAULT_BUILTIN_PROXY_SETTINGS.host,
3225
+ port: Number.isFinite(port) && port > 0 && port <= 65535 ? port : DEFAULT_BUILTIN_PROXY_SETTINGS.port,
3226
+ provider,
3227
+ authSource,
3228
+ timeoutMs: Number.isFinite(timeoutMs) && timeoutMs >= 1000 ? timeoutMs : DEFAULT_BUILTIN_PROXY_SETTINGS.timeoutMs
3229
+ };
3230
+ }
2838
3231
 
2839
- const sessionId = params.sessionId || path.basename(filePath, '.jsonl');
2840
- try {
2841
- fs.unlinkSync(filePath);
2842
- } catch (e) {
2843
- return { error: `删除会话失败: ${e.message}` };
3232
+ function readBuiltinProxySettings() {
3233
+ const parsed = readJsonFile(BUILTIN_PROXY_SETTINGS_FILE, null);
3234
+ return normalizeBuiltinProxySettings(parsed);
3235
+ }
3236
+
3237
+ function resolveBuiltinProxyProviderName(rawProviderName, providers = {}, preferredProvider = '') {
3238
+ const providerMap = providers && isPlainObject(providers) ? providers : {};
3239
+ const providerNames = Object.keys(providerMap)
3240
+ .filter((name) => name && name !== BUILTIN_PROXY_PROVIDER_NAME);
3241
+ const requested = typeof rawProviderName === 'string' ? rawProviderName.trim() : '';
3242
+ if (requested && requested !== BUILTIN_PROXY_PROVIDER_NAME && providerMap[requested]) {
3243
+ return requested;
2844
3244
  }
3245
+ const preferred = typeof preferredProvider === 'string' ? preferredProvider.trim() : '';
3246
+ if (preferred && preferred !== BUILTIN_PROXY_PROVIDER_NAME && providerMap[preferred]) {
3247
+ return preferred;
3248
+ }
3249
+ return providerNames[0] || '';
3250
+ }
2845
3251
 
2846
- if (source === 'claude') {
2847
- const indexPath = findClaudeSessionIndexPath(filePath);
2848
- if (indexPath) {
2849
- updateClaudeSessionIndex(indexPath, filePath, sessionId);
2850
- }
3252
+ function saveBuiltinProxySettings(payload = {}, options = {}) {
3253
+ const current = readBuiltinProxySettings();
3254
+ const merged = normalizeBuiltinProxySettings({
3255
+ ...current,
3256
+ ...(isPlainObject(payload) ? payload : {})
3257
+ });
3258
+
3259
+ if (!merged.host) {
3260
+ return { error: '代理 host 不能为空' };
3261
+ }
3262
+ if (!Number.isFinite(merged.port) || merged.port <= 0 || merged.port > 65535) {
3263
+ return { error: '代理端口无效(1-65535)' };
2851
3264
  }
2852
3265
 
2853
- invalidateSessionListCache();
3266
+ const { config } = readConfigOrVirtualDefault();
3267
+ const providers = config && isPlainObject(config.model_providers) ? config.model_providers : {};
3268
+ const preferredProvider = typeof config.model_provider === 'string' ? config.model_provider.trim() : '';
3269
+ const finalProvider = resolveBuiltinProxyProviderName(merged.provider, providers, preferredProvider);
3270
+
3271
+ const normalized = {
3272
+ ...merged,
3273
+ provider: finalProvider
3274
+ };
3275
+
3276
+ if (!options.skipWrite) {
3277
+ writeJsonAtomic(BUILTIN_PROXY_SETTINGS_FILE, normalized);
3278
+ }
2854
3279
 
2855
3280
  return {
2856
3281
  success: true,
2857
- source,
2858
- sessionId,
2859
- filePath
3282
+ settings: normalized
2860
3283
  };
2861
3284
  }
2862
3285
 
2863
- function generateCloneSessionId() {
2864
- if (crypto.randomUUID) {
2865
- return `clone-${crypto.randomUUID()}`;
2866
- }
2867
- const timePart = Date.now().toString(36);
2868
- const randomPart = crypto.randomBytes(8).toString('hex');
2869
- return `clone-${timePart}-${randomPart}`;
3286
+ function buildProxyListenUrl(settings) {
3287
+ const host = formatHostForUrl(settings.host || DEFAULT_BUILTIN_PROXY_SETTINGS.host);
3288
+ return `http://${host}:${settings.port}`;
2870
3289
  }
2871
3290
 
2872
- function allocateCloneSessionTarget(dirPath) {
2873
- for (let attempt = 0; attempt < 6; attempt += 1) {
2874
- const sessionId = generateCloneSessionId();
2875
- const filePath = path.join(dirPath, `${sessionId}.jsonl`);
2876
- if (!fs.existsSync(filePath)) {
2877
- return { sessionId, filePath };
2878
- }
3291
+ function hasCodexConfigReadyForProxy() {
3292
+ const result = readConfigOrVirtualDefault();
3293
+ if (!result || result.isVirtual) {
3294
+ return false;
2879
3295
  }
2880
- const fallbackId = `clone-${Date.now().toString(36)}-${crypto.randomBytes(8).toString('hex')}`;
2881
- return { sessionId: fallbackId, filePath: path.join(dirPath, `${fallbackId}.jsonl`) };
3296
+ const config = result.config || {};
3297
+ if (!isPlainObject(config.model_providers)) {
3298
+ return false;
3299
+ }
3300
+ const providerNames = Object.keys(config.model_providers)
3301
+ .filter((name) => name && name !== BUILTIN_PROXY_PROVIDER_NAME);
3302
+ return providerNames.length > 0;
2882
3303
  }
2883
3304
 
2884
- function parseTimestampMs(value) {
2885
- if (value === undefined || value === null || value === '') {
2886
- return null;
3305
+ function resolveBuiltinProxyUpstream(settings) {
3306
+ const { config } = readConfigOrVirtualDefault();
3307
+ const providers = config && isPlainObject(config.model_providers) ? config.model_providers : {};
3308
+ const currentProvider = typeof config.model_provider === 'string' ? config.model_provider.trim() : '';
3309
+ const providerName = resolveBuiltinProxyProviderName(settings.provider, providers, currentProvider);
3310
+ if (!providerName) {
3311
+ return { error: '未找到可用的上游 provider,请先添加 provider' };
2887
3312
  }
2888
- if (typeof value === 'number' && Number.isFinite(value)) {
2889
- if (value > 1e12) return value;
2890
- if (value > 1e9) return value * 1000;
2891
- return value;
3313
+ if (providerName === BUILTIN_PROXY_PROVIDER_NAME) {
3314
+ return { error: `上游 provider 不能是 ${BUILTIN_PROXY_PROVIDER_NAME}` };
2892
3315
  }
2893
- if (typeof value === 'string') {
2894
- const parsed = Date.parse(value);
2895
- if (Number.isFinite(parsed)) {
2896
- return parsed;
2897
- }
2898
- const numeric = Number(value);
2899
- if (Number.isFinite(numeric)) {
2900
- if (numeric > 1e12) return numeric;
2901
- if (numeric > 1e9) return numeric * 1000;
2902
- return numeric;
2903
- }
3316
+ const provider = providers[providerName];
3317
+ if (!provider || !isPlainObject(provider)) {
3318
+ return { error: `上游 provider 不存在: ${providerName}` };
2904
3319
  }
2905
- return null;
2906
- }
2907
3320
 
2908
- async function cloneCodexSession(params = {}) {
2909
- const source = params.source === 'codex' ? 'codex' : '';
2910
- if (!source) {
2911
- return { error: '仅支持 Codex 会话克隆' };
3321
+ const baseUrl = typeof provider.base_url === 'string' ? provider.base_url.trim() : '';
3322
+ if (!baseUrl || !isValidHttpUrl(baseUrl)) {
3323
+ return { error: `上游 provider base_url 无效: ${providerName}` };
2912
3324
  }
2913
3325
 
2914
- const filePath = resolveSessionFilePath(source, params.filePath, params.sessionId);
2915
- if (!filePath) {
2916
- return { error: 'Session file not found' };
3326
+ let token = '';
3327
+ if (settings.authSource === 'profile') {
3328
+ token = resolveAuthTokenFromCurrentProfile();
3329
+ } else if (settings.authSource === 'provider') {
3330
+ token = typeof provider.preferred_auth_method === 'string' ? provider.preferred_auth_method.trim() : '';
3331
+ if (!token) {
3332
+ token = resolveAuthTokenFromCurrentProfile();
3333
+ }
2917
3334
  }
2918
3335
 
2919
- let content = '';
2920
- try {
2921
- content = fs.readFileSync(filePath, 'utf-8');
2922
- } catch (e) {
2923
- return { error: `读取会话失败: ${e.message}` };
3336
+ let authHeader = '';
3337
+ if (token) {
3338
+ authHeader = /^bearer\s+/i.test(token) ? token : `Bearer ${token}`;
2924
3339
  }
2925
3340
 
2926
- if (!content.trim()) {
2927
- return { error: 'Session file is empty' };
2928
- }
3341
+ return {
3342
+ providerName,
3343
+ baseUrl: normalizeBaseUrl(baseUrl),
3344
+ authHeader
3345
+ };
3346
+ }
3347
+
3348
+ function createBuiltinProxyServer(settings, upstream) {
3349
+ const connections = new Set();
3350
+ const timeoutMs = settings.timeoutMs;
3351
+
3352
+ const server = http.createServer((req, res) => {
3353
+ let parsedIncoming;
3354
+ try {
3355
+ parsedIncoming = new URL(req.url || '/', 'http://localhost');
3356
+ } catch (e) {
3357
+ res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
3358
+ res.end(JSON.stringify({ error: 'invalid request path' }));
3359
+ return;
3360
+ }
3361
+
3362
+ const incomingPath = parsedIncoming.pathname || '/';
3363
+ if (incomingPath === '/health' || incomingPath === '/status') {
3364
+ const body = JSON.stringify({
3365
+ ok: true,
3366
+ upstreamProvider: upstream.providerName,
3367
+ upstreamBaseUrl: upstream.baseUrl
3368
+ });
3369
+ res.writeHead(200, {
3370
+ 'Content-Type': 'application/json; charset=utf-8',
3371
+ 'Content-Length': Buffer.byteLength(body, 'utf-8')
3372
+ });
3373
+ res.end(body, 'utf-8');
3374
+ return;
3375
+ }
3376
+
3377
+ if (!(incomingPath === '/v1' || incomingPath.startsWith('/v1/'))) {
3378
+ const body = JSON.stringify({ error: 'proxy only supports /v1/* paths' });
3379
+ res.writeHead(404, {
3380
+ 'Content-Type': 'application/json; charset=utf-8',
3381
+ 'Content-Length': Buffer.byteLength(body, 'utf-8')
3382
+ });
3383
+ res.end(body, 'utf-8');
3384
+ return;
3385
+ }
3386
+
3387
+ const suffix = incomingPath === '/v1'
3388
+ ? ''
3389
+ : incomingPath.replace(/^\/v1\/?/, '');
3390
+ const targetBase = joinApiUrl(upstream.baseUrl, suffix);
3391
+ if (!targetBase) {
3392
+ const body = JSON.stringify({ error: 'failed to build upstream URL' });
3393
+ res.writeHead(500, {
3394
+ 'Content-Type': 'application/json; charset=utf-8',
3395
+ 'Content-Length': Buffer.byteLength(body, 'utf-8')
3396
+ });
3397
+ res.end(body, 'utf-8');
3398
+ return;
3399
+ }
3400
+
3401
+ let targetUrl;
3402
+ try {
3403
+ targetUrl = new URL(targetBase);
3404
+ targetUrl.search = parsedIncoming.search || '';
3405
+ } catch (e) {
3406
+ const body = JSON.stringify({ error: `invalid upstream URL: ${e.message}` });
3407
+ res.writeHead(500, {
3408
+ 'Content-Type': 'application/json; charset=utf-8',
3409
+ 'Content-Length': Buffer.byteLength(body, 'utf-8')
3410
+ });
3411
+ res.end(body, 'utf-8');
3412
+ return;
3413
+ }
3414
+
3415
+ const requestHeaders = { ...req.headers };
3416
+ delete requestHeaders.host;
3417
+ delete requestHeaders.connection;
3418
+ delete requestHeaders['content-length'];
3419
+ if (upstream.authHeader) {
3420
+ requestHeaders.authorization = upstream.authHeader;
3421
+ }
3422
+ requestHeaders['x-codexmate-proxy'] = '1';
3423
+ if (!requestHeaders['x-forwarded-for'] && req.socket && req.socket.remoteAddress) {
3424
+ requestHeaders['x-forwarded-for'] = req.socket.remoteAddress;
3425
+ }
3426
+
3427
+ const transport = targetUrl.protocol === 'https:' ? https : http;
3428
+ const upstreamReq = transport.request({
3429
+ protocol: targetUrl.protocol,
3430
+ hostname: targetUrl.hostname,
3431
+ port: targetUrl.port || (targetUrl.protocol === 'https:' ? 443 : 80),
3432
+ method: req.method || 'GET',
3433
+ path: `${targetUrl.pathname}${targetUrl.search}`,
3434
+ headers: requestHeaders,
3435
+ agent: targetUrl.protocol === 'https:' ? HTTPS_KEEP_ALIVE_AGENT : HTTP_KEEP_ALIVE_AGENT
3436
+ }, (upstreamRes) => {
3437
+ const responseHeaders = { ...upstreamRes.headers };
3438
+ delete responseHeaders.connection;
3439
+ res.writeHead(upstreamRes.statusCode || 502, responseHeaders);
3440
+ upstreamRes.pipe(res);
3441
+ });
3442
+
3443
+ upstreamReq.setTimeout(timeoutMs, () => {
3444
+ upstreamReq.destroy(new Error(`upstream timeout (${timeoutMs}ms)`));
3445
+ });
3446
+
3447
+ upstreamReq.on('error', (err) => {
3448
+ if (res.headersSent) {
3449
+ try { res.destroy(err); } catch (_) {}
3450
+ return;
3451
+ }
3452
+ const body = JSON.stringify({ error: `proxy request failed: ${err.message}` });
3453
+ res.writeHead(502, {
3454
+ 'Content-Type': 'application/json; charset=utf-8',
3455
+ 'Content-Length': Buffer.byteLength(body, 'utf-8')
3456
+ });
3457
+ res.end(body, 'utf-8');
3458
+ });
3459
+
3460
+ req.pipe(upstreamReq);
3461
+ });
3462
+
3463
+ server.on('connection', (socket) => {
3464
+ connections.add(socket);
3465
+ socket.on('close', () => connections.delete(socket));
3466
+ });
3467
+
3468
+ return new Promise((resolve, reject) => {
3469
+ server.once('error', reject);
3470
+ server.listen(settings.port, settings.host, () => {
3471
+ server.removeListener('error', reject);
3472
+ resolve({
3473
+ server,
3474
+ connections,
3475
+ settings,
3476
+ upstream,
3477
+ startedAt: toIsoTime(Date.now()),
3478
+ listenUrl: buildProxyListenUrl(settings)
3479
+ });
3480
+ });
3481
+ });
3482
+ }
3483
+
3484
+ async function startBuiltinProxyRuntime(payload = {}) {
3485
+ if (g_builtinProxyRuntime) {
3486
+ return {
3487
+ error: '内建代理已在运行',
3488
+ runtime: {
3489
+ listenUrl: g_builtinProxyRuntime.listenUrl,
3490
+ upstreamProvider: g_builtinProxyRuntime.upstream.providerName
3491
+ }
3492
+ };
3493
+ }
3494
+
3495
+ const saveResult = saveBuiltinProxySettings(payload);
3496
+ if (saveResult.error) {
3497
+ return { error: saveResult.error };
3498
+ }
3499
+ const settings = saveResult.settings;
3500
+ const upstream = resolveBuiltinProxyUpstream(settings);
3501
+ if (upstream.error) {
3502
+ return { error: upstream.error };
3503
+ }
3504
+
3505
+ try {
3506
+ g_builtinProxyRuntime = await createBuiltinProxyServer(settings, upstream);
3507
+ return {
3508
+ success: true,
3509
+ running: true,
3510
+ listenUrl: g_builtinProxyRuntime.listenUrl,
3511
+ upstreamProvider: upstream.providerName,
3512
+ settings
3513
+ };
3514
+ } catch (e) {
3515
+ return { error: `启动内建代理失败: ${e.message}` };
3516
+ }
3517
+ }
3518
+
3519
+ async function stopBuiltinProxyRuntime() {
3520
+ if (!g_builtinProxyRuntime) {
3521
+ return { success: true, running: false };
3522
+ }
3523
+ const runtime = g_builtinProxyRuntime;
3524
+ g_builtinProxyRuntime = null;
3525
+
3526
+ await new Promise((resolve) => {
3527
+ let settled = false;
3528
+ const finish = () => {
3529
+ if (settled) return;
3530
+ settled = true;
3531
+ resolve();
3532
+ };
3533
+
3534
+ runtime.server.close(() => finish());
3535
+ setTimeout(() => finish(), 1000);
3536
+ });
3537
+
3538
+ for (const socket of runtime.connections) {
3539
+ try { socket.destroy(); } catch (_) {}
3540
+ }
3541
+ runtime.connections.clear();
3542
+
3543
+ return {
3544
+ success: true,
3545
+ running: false
3546
+ };
3547
+ }
3548
+
3549
+ function getBuiltinProxyStatus() {
3550
+ const settings = readBuiltinProxySettings();
3551
+ return {
3552
+ running: !!g_builtinProxyRuntime,
3553
+ settings,
3554
+ runtime: g_builtinProxyRuntime
3555
+ ? {
3556
+ provider: DEFAULT_LOCAL_PROVIDER_NAME,
3557
+ startedAt: g_builtinProxyRuntime.startedAt,
3558
+ listenUrl: g_builtinProxyRuntime.listenUrl,
3559
+ upstreamProvider: g_builtinProxyRuntime.upstream.providerName,
3560
+ upstreamBaseUrl: g_builtinProxyRuntime.upstream.baseUrl
3561
+ }
3562
+ : null
3563
+ };
3564
+ }
3565
+
3566
+ function applyBuiltinProxyProvider(params = {}) {
3567
+ const settings = readBuiltinProxySettings();
3568
+ const hostForUrl = formatHostForUrl(settings.host);
3569
+ const baseUrl = `http://${hostForUrl}:${settings.port}`;
3570
+
3571
+ const { config } = readConfigOrVirtualDefault();
3572
+ const providers = config && isPlainObject(config.model_providers) ? config.model_providers : {};
3573
+ const exists = !!providers[BUILTIN_PROXY_PROVIDER_NAME];
3574
+ const saveResult = exists
3575
+ ? updateProviderInConfig({
3576
+ name: BUILTIN_PROXY_PROVIDER_NAME,
3577
+ url: baseUrl,
3578
+ key: '',
3579
+ allowManaged: true
3580
+ })
3581
+ : addProviderToConfig({
3582
+ name: BUILTIN_PROXY_PROVIDER_NAME,
3583
+ url: baseUrl,
3584
+ key: '',
3585
+ allowManaged: true
3586
+ });
3587
+
3588
+ if (saveResult && saveResult.error) {
3589
+ return saveResult;
3590
+ }
3591
+
3592
+ const switchToProxy = params.switchToProxy !== false;
3593
+ let targetModel = '';
3594
+ if (switchToProxy) {
3595
+ try {
3596
+ targetModel = cmdSwitch(BUILTIN_PROXY_PROVIDER_NAME, true) || '';
3597
+ } catch (e) {
3598
+ return { error: `写入代理 provider 成功,但切换失败: ${e.message}` };
3599
+ }
3600
+ }
3601
+
3602
+ return {
3603
+ success: true,
3604
+ provider: BUILTIN_PROXY_PROVIDER_NAME,
3605
+ baseUrl,
3606
+ switched: switchToProxy,
3607
+ model: targetModel
3608
+ };
3609
+ }
3610
+
3611
+ async function ensureBuiltinProxyForCodexDefault(params = {}) {
3612
+ const payload = isPlainObject(params) ? { ...params } : {};
3613
+ const switchToProxy = payload.switchToProxy !== false;
3614
+ delete payload.switchToProxy;
3615
+ payload.enabled = true;
3616
+
3617
+ const saveResult = saveBuiltinProxySettings(payload);
3618
+ if (saveResult.error) {
3619
+ return { error: saveResult.error };
3620
+ }
3621
+ let nextSettings = saveResult.settings;
3622
+
3623
+ let upstreamResult = resolveBuiltinProxyUpstream(nextSettings);
3624
+ if (upstreamResult.error) {
3625
+ return { error: upstreamResult.error };
3626
+ }
3627
+
3628
+ const runtime = g_builtinProxyRuntime;
3629
+ const shouldRestart = !!runtime && (
3630
+ runtime.settings.host !== nextSettings.host
3631
+ || runtime.settings.port !== nextSettings.port
3632
+ || runtime.settings.authSource !== nextSettings.authSource
3633
+ || runtime.settings.timeoutMs !== nextSettings.timeoutMs
3634
+ || runtime.upstream.providerName !== upstreamResult.providerName
3635
+ || runtime.upstream.baseUrl !== upstreamResult.baseUrl
3636
+ || runtime.upstream.authHeader !== upstreamResult.authHeader
3637
+ );
3638
+
3639
+ if (shouldRestart) {
3640
+ await stopBuiltinProxyRuntime();
3641
+ }
3642
+
3643
+ if (!g_builtinProxyRuntime) {
3644
+ let startRes = await startBuiltinProxyRuntime(nextSettings);
3645
+ if (!startRes.success && /EADDRINUSE/i.test(String(startRes.error || ''))) {
3646
+ const fallbackPort = await findAvailablePort(nextSettings.host, nextSettings.port + 1, 30);
3647
+ if (fallbackPort > 0) {
3648
+ const retrySave = saveBuiltinProxySettings({
3649
+ ...nextSettings,
3650
+ port: fallbackPort,
3651
+ enabled: true
3652
+ });
3653
+ if (retrySave.success) {
3654
+ nextSettings = retrySave.settings;
3655
+ upstreamResult = resolveBuiltinProxyUpstream(nextSettings);
3656
+ if (upstreamResult.error) {
3657
+ return { error: upstreamResult.error };
3658
+ }
3659
+ startRes = await startBuiltinProxyRuntime(nextSettings);
3660
+ }
3661
+ }
3662
+ }
3663
+ if (!startRes.success) {
3664
+ return { error: startRes.error || '启动内建代理失败' };
3665
+ }
3666
+ }
3667
+
3668
+ let applyRes = {
3669
+ success: true,
3670
+ provider: BUILTIN_PROXY_PROVIDER_NAME,
3671
+ baseUrl: buildProxyListenUrl(nextSettings),
3672
+ switched: false,
3673
+ model: ''
3674
+ };
3675
+ if (switchToProxy) {
3676
+ applyRes = applyBuiltinProxyProvider({ switchToProxy: true });
3677
+ if (applyRes.error) {
3678
+ return applyRes;
3679
+ }
3680
+ }
3681
+
3682
+ const status = getBuiltinProxyStatus();
3683
+ return {
3684
+ success: true,
3685
+ provider: applyRes.provider,
3686
+ baseUrl: applyRes.baseUrl,
3687
+ switched: applyRes.switched,
3688
+ model: applyRes.model || '',
3689
+ settings: status.settings,
3690
+ runtime: status.runtime
3691
+ };
3692
+ }
3693
+
3694
+ function updateClaudeSessionIndex(indexPath, sessionFilePath, sessionId) {
3695
+ if (!indexPath || !fs.existsSync(indexPath)) {
3696
+ return;
3697
+ }
3698
+ const index = readJsonFile(indexPath, null);
3699
+ if (!index || !Array.isArray(index.entries)) {
3700
+ return;
3701
+ }
3702
+ const resolvedFile = sessionFilePath ? path.resolve(sessionFilePath) : '';
3703
+ const resolvedLower = resolvedFile ? resolvedFile.toLowerCase() : '';
3704
+ const filtered = index.entries.filter((entry) => {
3705
+ if (!entry || typeof entry !== 'object') {
3706
+ return false;
3707
+ }
3708
+ const entrySessionId = typeof entry.sessionId === 'string' ? entry.sessionId : '';
3709
+ if (sessionId && entrySessionId === sessionId) {
3710
+ return false;
3711
+ }
3712
+ if (entry.fullPath) {
3713
+ const expanded = expandHomePath(entry.fullPath);
3714
+ const entryPath = expanded ? path.resolve(expanded) : '';
3715
+ if (entryPath && resolvedLower && entryPath.toLowerCase() === resolvedLower) {
3716
+ return false;
3717
+ }
3718
+ }
3719
+ return true;
3720
+ });
3721
+ if (filtered.length === index.entries.length) {
3722
+ return;
3723
+ }
3724
+ index.entries = filtered;
3725
+ try {
3726
+ fs.writeFileSync(indexPath, JSON.stringify(index, null, 2), 'utf-8');
3727
+ } catch (e) {}
3728
+ }
3729
+
3730
+ async function deleteSessionData(params = {}) {
3731
+ const source = params.source === 'claude' ? 'claude' : (params.source === 'codex' ? 'codex' : '');
3732
+ if (!source) {
3733
+ return { error: 'Invalid source' };
3734
+ }
3735
+
3736
+ const filePath = resolveSessionFilePath(source, params.filePath, params.sessionId);
3737
+ if (!filePath) {
3738
+ return { error: 'Session file not found' };
3739
+ }
3740
+
3741
+ const sessionId = params.sessionId || path.basename(filePath, '.jsonl');
3742
+ try {
3743
+ fs.unlinkSync(filePath);
3744
+ } catch (e) {
3745
+ return { error: `删除会话失败: ${e.message}` };
3746
+ }
3747
+
3748
+ if (source === 'claude') {
3749
+ const indexPath = findClaudeSessionIndexPath(filePath);
3750
+ if (indexPath) {
3751
+ updateClaudeSessionIndex(indexPath, filePath, sessionId);
3752
+ }
3753
+ }
3754
+
3755
+ invalidateSessionListCache();
3756
+
3757
+ return {
3758
+ success: true,
3759
+ source,
3760
+ sessionId,
3761
+ filePath
3762
+ };
3763
+ }
3764
+
3765
+ function generateCloneSessionId() {
3766
+ if (crypto.randomUUID) {
3767
+ return `clone-${crypto.randomUUID()}`;
3768
+ }
3769
+ const timePart = Date.now().toString(36);
3770
+ const randomPart = crypto.randomBytes(8).toString('hex');
3771
+ return `clone-${timePart}-${randomPart}`;
3772
+ }
3773
+
3774
+ function allocateCloneSessionTarget(dirPath) {
3775
+ for (let attempt = 0; attempt < 6; attempt += 1) {
3776
+ const sessionId = generateCloneSessionId();
3777
+ const filePath = path.join(dirPath, `${sessionId}.jsonl`);
3778
+ if (!fs.existsSync(filePath)) {
3779
+ return { sessionId, filePath };
3780
+ }
3781
+ }
3782
+ const fallbackId = `clone-${Date.now().toString(36)}-${crypto.randomBytes(8).toString('hex')}`;
3783
+ return { sessionId: fallbackId, filePath: path.join(dirPath, `${fallbackId}.jsonl`) };
3784
+ }
3785
+
3786
+ function parseTimestampMs(value) {
3787
+ if (value === undefined || value === null || value === '') {
3788
+ return null;
3789
+ }
3790
+ if (typeof value === 'number' && Number.isFinite(value)) {
3791
+ if (value > 1e12) return value;
3792
+ if (value > 1e9) return value * 1000;
3793
+ return value;
3794
+ }
3795
+ if (typeof value === 'string') {
3796
+ const parsed = Date.parse(value);
3797
+ if (Number.isFinite(parsed)) {
3798
+ return parsed;
3799
+ }
3800
+ const numeric = Number(value);
3801
+ if (Number.isFinite(numeric)) {
3802
+ if (numeric > 1e12) return numeric;
3803
+ if (numeric > 1e9) return numeric * 1000;
3804
+ return numeric;
3805
+ }
3806
+ }
3807
+ return null;
3808
+ }
3809
+
3810
+ async function cloneCodexSession(params = {}) {
3811
+ const source = params.source === 'codex' ? 'codex' : '';
3812
+ if (!source) {
3813
+ return { error: '仅支持 Codex 会话克隆' };
3814
+ }
3815
+
3816
+ const filePath = resolveSessionFilePath(source, params.filePath, params.sessionId);
3817
+ if (!filePath) {
3818
+ return { error: 'Session file not found' };
3819
+ }
3820
+
3821
+ let content = '';
3822
+ try {
3823
+ content = fs.readFileSync(filePath, 'utf-8');
3824
+ } catch (e) {
3825
+ return { error: `读取会话失败: ${e.message}` };
3826
+ }
3827
+
3828
+ if (!content.trim()) {
3829
+ return { error: 'Session file is empty' };
3830
+ }
2929
3831
 
2930
3832
  const lineEnding = detectLineEnding(content);
2931
3833
  const rawLines = content.split(/\r?\n/);
@@ -3440,6 +4342,9 @@ function buildExportPayload(includeKeys) {
3440
4342
  const providers = config.model_providers || {};
3441
4343
  const providerData = {};
3442
4344
  for (const [name, provider] of Object.entries(providers)) {
4345
+ if (isBuiltinProxyProvider(name)) {
4346
+ continue;
4347
+ }
3443
4348
  providerData[name] = {
3444
4349
  baseUrl: provider.base_url || '',
3445
4350
  apiKey: includeKeys ? (provider.preferred_auth_method || '') : null
@@ -3561,6 +4466,9 @@ function importConfigData(payload, options = {}) {
3561
4466
  let updatedProviders = 0;
3562
4467
 
3563
4468
  for (const [name, provider] of Object.entries(normalized.providers)) {
4469
+ if (isBuiltinProxyProvider(name)) {
4470
+ continue;
4471
+ }
3564
4472
  if (existingProviders[name]) {
3565
4473
  if (overwriteProviders) {
3566
4474
  const apiKey = typeof provider.apiKey === 'string' && provider.apiKey
@@ -3592,6 +4500,7 @@ function importConfigData(payload, options = {}) {
3592
4500
  if (applyCurrentModels && normalized.currentModels) {
3593
4501
  const currentModels = readCurrentModels();
3594
4502
  for (const [name, model] of Object.entries(normalized.currentModels)) {
4503
+ if (isBuiltinProxyProvider(name)) continue;
3595
4504
  if (typeof model !== 'string' || !model) continue;
3596
4505
  currentModels[name] = model;
3597
4506
  }
@@ -4108,7 +5017,10 @@ function cmdUseModel(modelName, silent = false) {
4108
5017
 
4109
5018
  // 添加提供商
4110
5019
  function cmdAdd(name, baseUrl, apiKey, silent = false) {
4111
- if (!name || !baseUrl) {
5020
+ const providerName = typeof name === 'string' ? name.trim() : '';
5021
+ const providerBaseUrl = typeof baseUrl === 'string' ? baseUrl.trim() : '';
5022
+
5023
+ if (!providerName || !providerBaseUrl) {
4112
5024
  if (!silent) {
4113
5025
  console.error('用法: codexmate add <名称> <URL> [密钥]');
4114
5026
  console.log('\n示例:');
@@ -4116,17 +5028,21 @@ function cmdAdd(name, baseUrl, apiKey, silent = false) {
4116
5028
  }
4117
5029
  throw new Error('名称和URL必填');
4118
5030
  }
5031
+ if (isReservedProviderNameForCreation(providerName)) {
5032
+ if (!silent) console.error('错误: local provider 为系统保留名称,不可新增');
5033
+ throw new Error('local provider 为系统保留名称,不可新增');
5034
+ }
4119
5035
 
4120
5036
  const config = readConfig();
4121
- if (config.model_providers && config.model_providers[name]) {
4122
- if (!silent) console.error('错误: 提供商已存在:', name);
5037
+ if (config.model_providers && config.model_providers[providerName]) {
5038
+ if (!silent) console.error('错误: 提供商已存在:', providerName);
4123
5039
  throw new Error('提供商已存在');
4124
5040
  }
4125
5041
 
4126
5042
  const newBlock = `
4127
- [model_providers.${name}]
4128
- name = "${name}"
4129
- base_url = "${baseUrl}"
5043
+ [model_providers.${providerName}]
5044
+ name = "${providerName}"
5045
+ base_url = "${providerBaseUrl}"
4130
5046
  wire_api = "responses"
4131
5047
  requires_openai_auth = false
4132
5048
  preferred_auth_method = "${apiKey || ''}"
@@ -4140,14 +5056,14 @@ stream_idle_timeout_ms = 300000
4140
5056
 
4141
5057
  // 初始化当前模型
4142
5058
  const currentModels = readCurrentModels();
4143
- if (!currentModels[name]) {
4144
- currentModels[name] = readModels()[0];
5059
+ if (!currentModels[providerName]) {
5060
+ currentModels[providerName] = readModels()[0];
4145
5061
  writeCurrentModels(currentModels);
4146
5062
  }
4147
5063
 
4148
5064
  if (!silent) {
4149
- console.log('✓ 已添加提供商:', name);
4150
- console.log(' URL:', baseUrl);
5065
+ console.log('✓ 已添加提供商:', providerName);
5066
+ console.log(' URL:', providerBaseUrl);
4151
5067
  console.log();
4152
5068
  }
4153
5069
  }
@@ -4168,11 +5084,19 @@ function cmdDelete(name, silent = false) {
4168
5084
  }
4169
5085
 
4170
5086
  // 更新提供商
4171
- function cmdUpdate(name, baseUrl, apiKey, silent = false) {
5087
+ function cmdUpdate(name, baseUrl, apiKey, silent = false, options = {}) {
5088
+ const allowManaged = !!(options && options.allowManaged);
4172
5089
  if (!name) {
4173
5090
  if (!silent) console.error('错误: 提供商名称必填');
4174
5091
  throw new Error('提供商名称必填');
4175
5092
  }
5093
+ if (isNonEditableProvider(name) && !allowManaged) {
5094
+ const msg = isDefaultLocalProvider(name)
5095
+ ? 'local provider 为系统保留项,不可编辑'
5096
+ : '本地代理配置为系统内建项,不可编辑';
5097
+ if (!silent) console.error(`错误: ${msg}`);
5098
+ throw new Error(msg);
5099
+ }
4176
5100
 
4177
5101
  const config = readConfig();
4178
5102
  if (!config.model_providers || !config.model_providers[name]) {
@@ -4357,29 +5281,248 @@ function applyToClaudeSettings(config = {}) {
4357
5281
  }
4358
5282
  }
4359
5283
 
4360
- function readClaudeSettingsInfo() {
4361
- const readResult = readJsonObjectFromFile(CLAUDE_SETTINGS_FILE, {});
4362
- if (!readResult.ok) {
5284
+ function readClaudeSettingsInfo() {
5285
+ const readResult = readJsonObjectFromFile(CLAUDE_SETTINGS_FILE, {});
5286
+ if (!readResult.ok) {
5287
+ return {
5288
+ error: readResult.error || '读取 Claude 配置失败',
5289
+ exists: !!readResult.exists,
5290
+ targetPath: CLAUDE_SETTINGS_FILE
5291
+ };
5292
+ }
5293
+
5294
+ const settings = readResult.data || {};
5295
+ const env = (settings.env && typeof settings.env === 'object' && !Array.isArray(settings.env))
5296
+ ? settings.env
5297
+ : {};
5298
+
5299
+ return {
5300
+ exists: !!readResult.exists,
5301
+ targetPath: CLAUDE_SETTINGS_FILE,
5302
+ apiKey: typeof env.ANTHROPIC_API_KEY === 'string' ? env.ANTHROPIC_API_KEY : '',
5303
+ baseUrl: typeof env.ANTHROPIC_BASE_URL === 'string' ? env.ANTHROPIC_BASE_URL : '',
5304
+ model: typeof env.ANTHROPIC_MODEL === 'string' ? env.ANTHROPIC_MODEL : '',
5305
+ env
5306
+ };
5307
+ }
5308
+
5309
+ // API: 打包 Claude 配置目录(系统 zip 可用则使用,否则回退 zip-lib)
5310
+ async function prepareClaudeDirDownload() {
5311
+ try {
5312
+ if (!fs.existsSync(CLAUDE_DIR)) {
5313
+ return { error: 'Claude 配置目录不存在', path: CLAUDE_DIR };
5314
+ }
5315
+
5316
+ const tempDir = os.tmpdir();
5317
+ const timestamp = Date.now();
5318
+ const zipFileName = `claude-config-${timestamp}.zip`;
5319
+ const zipFilePath = path.join(tempDir, zipFileName);
5320
+
5321
+ const zipTool = resolveZipTool();
5322
+ if (zipTool.type === 'zip') {
5323
+ const cmd = `"${zipTool.cmd}" -0 -q -r "${zipFilePath}" "${CLAUDE_DIR}"`;
5324
+ execSync(cmd, { stdio: 'ignore' });
5325
+ } else {
5326
+ await zipLib.archiveFolder(CLAUDE_DIR, zipFilePath);
5327
+ }
5328
+
5329
+ return {
5330
+ success: true,
5331
+ downloadPath: zipFilePath,
5332
+ fileName: zipFileName,
5333
+ sourcePath: CLAUDE_DIR
5334
+ };
5335
+ } catch (e) {
5336
+ return { error: `打包失败:${e.message}` };
5337
+ }
5338
+ }
5339
+
5340
+ // API: 打包 Codex 配置目录(同策略)
5341
+ async function prepareCodexDirDownload() {
5342
+ try {
5343
+ if (!fs.existsSync(CONFIG_DIR)) {
5344
+ return { error: 'Codex 配置目录不存在', path: CONFIG_DIR };
5345
+ }
5346
+
5347
+ const tempDir = os.tmpdir();
5348
+ const timestamp = Date.now();
5349
+ const zipFileName = `${CODEX_BACKUP_NAME}-${timestamp}.zip`;
5350
+ const zipFilePath = path.join(tempDir, zipFileName);
5351
+
5352
+ const zipTool = resolveZipTool();
5353
+ if (zipTool.type === 'zip') {
5354
+ const cmd = `"${zipTool.cmd}" -0 -q -r "${zipFilePath}" "${CONFIG_DIR}"`;
5355
+ execSync(cmd, { stdio: 'ignore' });
5356
+ } else {
5357
+ await zipLib.archiveFolder(CONFIG_DIR, zipFilePath);
5358
+ }
5359
+
5360
+ return {
5361
+ success: true,
5362
+ downloadPath: zipFilePath,
5363
+ fileName: zipFileName,
5364
+ sourcePath: CONFIG_DIR
5365
+ };
5366
+ } catch (e) {
5367
+ return { error: `打包失败:${e.message}` };
5368
+ }
5369
+ }
5370
+
5371
+ function copyDirRecursive(srcDir, destDir) {
5372
+ ensureDir(destDir);
5373
+ const entries = fs.readdirSync(srcDir, { withFileTypes: true });
5374
+ for (const entry of entries) {
5375
+ const srcPath = path.join(srcDir, entry.name);
5376
+ const destPath = path.join(destDir, entry.name);
5377
+ if (entry.isDirectory()) {
5378
+ copyDirRecursive(srcPath, destPath);
5379
+ } else if (entry.isSymbolicLink()) {
5380
+ const target = fs.readlinkSync(srcPath);
5381
+ fs.symlinkSync(target, destPath);
5382
+ } else {
5383
+ fs.copyFileSync(srcPath, destPath);
5384
+ }
5385
+ }
5386
+ }
5387
+
5388
+ function writeUploadZip(base64, prefix, originalName = '') {
5389
+ let buffer;
5390
+ try {
5391
+ buffer = Buffer.from(base64 || '', 'base64');
5392
+ } catch (e) {
5393
+ return { error: '备份文件内容不是有效的 base64 编码' };
5394
+ }
5395
+
5396
+ if (!buffer || buffer.length === 0) {
5397
+ return { error: '备份文件为空' };
5398
+ }
5399
+
5400
+ if (buffer.length > MAX_UPLOAD_SIZE) {
5401
+ return { error: `备份文件过大(>${Math.floor(MAX_UPLOAD_SIZE / 1024 / 1024)}MB)` };
5402
+ }
5403
+
5404
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), `${prefix}-`));
5405
+ const fileName = path.basename(originalName && typeof originalName === 'string' ? originalName : `${prefix}.zip`);
5406
+ const zipPath = path.join(tempDir, fileName.toLowerCase().endsWith('.zip') ? fileName : `${fileName}.zip`);
5407
+ fs.writeFileSync(zipPath, buffer);
5408
+ return { tempDir, zipPath };
5409
+ }
5410
+
5411
+ async function extractUploadZip(zipPath, extractDir) {
5412
+ const unzipTool = resolveUnzipTool();
5413
+ ensureDir(extractDir);
5414
+ await unzipWithLibrary(zipPath, extractDir);
5415
+ }
5416
+
5417
+ function findConfigSourceDir(extractedDir, markerDirName, requiredFileName) {
5418
+ const markerPath = path.join(extractedDir, markerDirName);
5419
+ if (fs.existsSync(markerPath) && fs.statSync(markerPath).isDirectory()) {
5420
+ return markerPath;
5421
+ }
5422
+
5423
+ const entries = fs.readdirSync(extractedDir, { withFileTypes: true }).filter((item) => item.isDirectory());
5424
+ if (entries.length === 1) {
5425
+ const onlyDir = path.join(extractedDir, entries[0].name);
5426
+ const nestedMarker = path.join(onlyDir, markerDirName);
5427
+ if (fs.existsSync(nestedMarker) && fs.statSync(nestedMarker).isDirectory()) {
5428
+ return nestedMarker;
5429
+ }
5430
+ if (fs.existsSync(path.join(onlyDir, requiredFileName))) {
5431
+ return onlyDir;
5432
+ }
5433
+ }
5434
+
5435
+ if (fs.existsSync(path.join(extractedDir, requiredFileName))) {
5436
+ return extractedDir;
5437
+ }
5438
+
5439
+ return extractedDir;
5440
+ }
5441
+
5442
+ async function backupDirectoryIfExists(dirPath, prefix) {
5443
+ if (!fs.existsSync(dirPath)) {
5444
+ return { backupPath: '' };
5445
+ }
5446
+
5447
+ const tempDir = os.tmpdir();
5448
+ const timestamp = Date.now();
5449
+ const zipFileName = `${prefix}-${timestamp}.zip`;
5450
+ const zipFilePath = path.join(tempDir, zipFileName);
5451
+ const zipTool = resolveZipTool();
5452
+
5453
+ try {
5454
+ if (zipTool.type === 'zip') {
5455
+ const cmd = `"${zipTool.cmd}" -0 -q -r "${zipFilePath}" "${dirPath}"`;
5456
+ execSync(cmd, { stdio: 'ignore' });
5457
+ } else {
5458
+ await zipLib.archiveFolder(dirPath, zipFilePath);
5459
+ }
5460
+ return { backupPath: zipFilePath, fileName: zipFileName };
5461
+ } catch (e) {
5462
+ return { backupPath: '', warning: `备份失败: ${e.message}` };
5463
+ }
5464
+ }
5465
+
5466
+ async function restoreConfigDirectoryFromUpload(payload, options) {
5467
+ const { targetDir, requiredFileName, markerDirName, tempPrefix, backupPrefix } = options;
5468
+ if (!payload || typeof payload.fileBase64 !== 'string' || !payload.fileBase64.trim()) {
5469
+ return { error: '缺少备份文件内容' };
5470
+ }
5471
+
5472
+ const upload = writeUploadZip(payload.fileBase64, tempPrefix, payload.fileName);
5473
+ if (upload.error) {
5474
+ return { error: upload.error };
5475
+ }
5476
+
5477
+ const tempDir = upload.tempDir;
5478
+ const extractDir = path.join(tempDir, 'extract');
5479
+ let backupPath = '';
5480
+ try {
5481
+ await extractUploadZip(upload.zipPath, extractDir);
5482
+ const sourceDir = findConfigSourceDir(extractDir, markerDirName, requiredFileName);
5483
+ const requiredPath = path.join(sourceDir, requiredFileName);
5484
+ if (!fs.existsSync(requiredPath)) {
5485
+ return { error: `无效备份,缺少 ${requiredFileName}` };
5486
+ }
5487
+
5488
+ const backupResult = await backupDirectoryIfExists(targetDir, backupPrefix);
5489
+ backupPath = backupResult.backupPath || '';
5490
+
5491
+ fs.rmSync(targetDir, { recursive: true, force: true });
5492
+ copyDirRecursive(sourceDir, targetDir);
5493
+
4363
5494
  return {
4364
- error: readResult.error || '读取 Claude 配置失败',
4365
- exists: !!readResult.exists,
4366
- targetPath: CLAUDE_SETTINGS_FILE
5495
+ success: true,
5496
+ targetDir,
5497
+ appliedFrom: payload.fileName || '',
5498
+ backupPath,
5499
+ backupWarning: backupResult.warning || ''
4367
5500
  };
5501
+ } catch (e) {
5502
+ return { error: `导入失败:${e.message}` };
5503
+ } finally {
5504
+ fs.rmSync(tempDir, { recursive: true, force: true });
4368
5505
  }
5506
+ }
4369
5507
 
4370
- const settings = readResult.data || {};
4371
- const env = (settings.env && typeof settings.env === 'object' && !Array.isArray(settings.env))
4372
- ? settings.env
4373
- : {};
5508
+ async function restoreClaudeDir(payload) {
5509
+ return await restoreConfigDirectoryFromUpload(payload, {
5510
+ targetDir: CLAUDE_DIR,
5511
+ requiredFileName: 'settings.json',
5512
+ markerDirName: '.claude',
5513
+ tempPrefix: 'claude-restore',
5514
+ backupPrefix: 'claude-config'
5515
+ });
5516
+ }
4374
5517
 
4375
- return {
4376
- exists: !!readResult.exists,
4377
- targetPath: CLAUDE_SETTINGS_FILE,
4378
- apiKey: typeof env.ANTHROPIC_API_KEY === 'string' ? env.ANTHROPIC_API_KEY : '',
4379
- baseUrl: typeof env.ANTHROPIC_BASE_URL === 'string' ? env.ANTHROPIC_BASE_URL : '',
4380
- model: typeof env.ANTHROPIC_MODEL === 'string' ? env.ANTHROPIC_MODEL : '',
4381
- env
4382
- };
5518
+ async function restoreCodexDir(payload) {
5519
+ return await restoreConfigDirectoryFromUpload(payload, {
5520
+ targetDir: CONFIG_DIR,
5521
+ requiredFileName: 'config.toml',
5522
+ markerDirName: '.codex',
5523
+ tempPrefix: 'codex-restore',
5524
+ backupPrefix: 'codex-config'
5525
+ });
4383
5526
  }
4384
5527
 
4385
5528
  // CLI: 一行写入 Claude Code 配置
@@ -4436,18 +5579,196 @@ function commandExists(command, args = '') {
4436
5579
  }
4437
5580
  }
4438
5581
 
4439
- const SEVEN_ZIP_PATHS = [
4440
- 'C:\\Program Files\\7-Zip\\7z.exe',
4441
- 'C:\\Program Files (x86)\\7-Zip\\7z.exe',
4442
- '7z'
5582
+ function detectPreferredPackageManager() {
5583
+ const userAgent = typeof process.env.npm_config_user_agent === 'string'
5584
+ ? process.env.npm_config_user_agent.trim().toLowerCase()
5585
+ : '';
5586
+ if (userAgent.startsWith('pnpm/')) return 'pnpm';
5587
+ if (userAgent.startsWith('bun/')) return 'bun';
5588
+ if (userAgent.startsWith('npm/')) return 'npm';
5589
+
5590
+ if (commandExists('pnpm', '--version')) return 'pnpm';
5591
+ if (commandExists('bun', '--version')) return 'bun';
5592
+ return 'npm';
5593
+ }
5594
+
5595
+ function resolveCommandPath(command) {
5596
+ if (!command) return '';
5597
+ const locator = process.platform === 'win32' ? 'where' : 'which';
5598
+ try {
5599
+ const probe = spawnSync(locator, [command], {
5600
+ encoding: 'utf8',
5601
+ windowsHide: true,
5602
+ timeout: 2500
5603
+ });
5604
+ if (probe.error || probe.status !== 0) {
5605
+ return '';
5606
+ }
5607
+ const lines = String(probe.stdout || '')
5608
+ .split(/\r?\n/g)
5609
+ .map((line) => line.trim())
5610
+ .filter(Boolean);
5611
+ return lines[0] || '';
5612
+ } catch (e) {
5613
+ return '';
5614
+ }
5615
+ }
5616
+
5617
+ function parseBinaryVersionOutput(text) {
5618
+ const raw = typeof text === 'string' ? text : '';
5619
+ const line = raw
5620
+ .split(/\r?\n/g)
5621
+ .map((item) => item.trim())
5622
+ .find(Boolean) || '';
5623
+ if (!line) return '';
5624
+ return line.length > 120 ? `${line.slice(0, 117)}...` : line;
5625
+ }
5626
+
5627
+ function probeCliBinary(binName) {
5628
+ const attempts = [['--version'], ['-v'], ['version']];
5629
+ let lastError = '';
5630
+
5631
+ for (const args of attempts) {
5632
+ const argString = args.join(' ').trim();
5633
+ const commandLine = argString ? `${binName} ${argString}` : binName;
5634
+ try {
5635
+ const stdout = execSync(commandLine, {
5636
+ encoding: 'utf8',
5637
+ windowsHide: true,
5638
+ timeout: 5000,
5639
+ stdio: ['ignore', 'pipe', 'pipe'],
5640
+ shell: process.platform === 'win32'
5641
+ });
5642
+ const version = parseBinaryVersionOutput(String(stdout || ''));
5643
+ return {
5644
+ installed: true,
5645
+ bin: binName,
5646
+ version: version || 'unknown',
5647
+ path: resolveCommandPath(binName),
5648
+ error: ''
5649
+ };
5650
+ } catch (error) {
5651
+ const err = error || {};
5652
+ const stdout = typeof err.stdout === 'string' ? err.stdout : String(err.stdout || '');
5653
+ const stderr = typeof err.stderr === 'string' ? err.stderr : String(err.stderr || '');
5654
+ const output = `${stdout}\n${stderr}`.trim();
5655
+ const version = parseBinaryVersionOutput(output);
5656
+ const status = Number.isFinite(err.status) ? err.status : null;
5657
+ if (version && status === 0) {
5658
+ return {
5659
+ installed: true,
5660
+ bin: binName,
5661
+ version,
5662
+ path: resolveCommandPath(binName),
5663
+ error: ''
5664
+ };
5665
+ }
5666
+ if (version) {
5667
+ lastError = status !== null
5668
+ ? `${binName} exited with ${status}: ${version}`
5669
+ : `${binName} failed: ${version}`;
5670
+ continue;
5671
+ }
5672
+ const message = err && err.message ? String(err.message) : '';
5673
+ if (message && !/ENOENT/i.test(message)) {
5674
+ lastError = message;
5675
+ }
5676
+ }
5677
+ }
5678
+
5679
+ return {
5680
+ installed: false,
5681
+ bin: binName,
5682
+ version: '',
5683
+ path: '',
5684
+ error: lastError
5685
+ };
5686
+ }
5687
+
5688
+ function resolveInstallCommandsByPackageManager(packageManager) {
5689
+ const normalized = String(packageManager || '').trim().toLowerCase();
5690
+ const manager = normalized === 'pnpm' || normalized === 'bun' || normalized === 'npm'
5691
+ ? normalized
5692
+ : 'npm';
5693
+ const commandsByTarget = {};
5694
+
5695
+ for (const target of CLI_INSTALL_TARGETS) {
5696
+ const pkg = target.packageName;
5697
+ if (manager === 'pnpm') {
5698
+ commandsByTarget[target.id] = {
5699
+ install: `pnpm add -g ${pkg}`,
5700
+ update: `pnpm up -g ${pkg}`,
5701
+ uninstall: `pnpm remove -g ${pkg}`
5702
+ };
5703
+ continue;
5704
+ }
5705
+ if (manager === 'bun') {
5706
+ commandsByTarget[target.id] = {
5707
+ install: `bun add -g ${pkg}`,
5708
+ update: `bun update -g ${pkg}`,
5709
+ uninstall: `bun remove -g ${pkg}`
5710
+ };
5711
+ continue;
5712
+ }
5713
+ commandsByTarget[target.id] = {
5714
+ install: `npm install -g ${pkg}`,
5715
+ update: `npm update -g ${pkg}`,
5716
+ uninstall: `npm uninstall -g ${pkg}`
5717
+ };
5718
+ }
5719
+
5720
+ return {
5721
+ packageManager: manager,
5722
+ commandsByTarget
5723
+ };
5724
+ }
5725
+
5726
+ function buildInstallStatusReport() {
5727
+ const packageManager = detectPreferredPackageManager();
5728
+ const targetReports = CLI_INSTALL_TARGETS.map((target) => {
5729
+ let hit = null;
5730
+ let lastError = '';
5731
+ for (const binName of target.bins) {
5732
+ const probe = probeCliBinary(binName);
5733
+ if (probe.installed) {
5734
+ hit = probe;
5735
+ break;
5736
+ }
5737
+ if (probe.error) {
5738
+ lastError = probe.error;
5739
+ }
5740
+ }
5741
+ return {
5742
+ id: target.id,
5743
+ name: target.name,
5744
+ packageName: target.packageName,
5745
+ installed: !!(hit && hit.installed),
5746
+ bin: hit ? hit.bin : (target.bins[0] || ''),
5747
+ version: hit ? hit.version : '',
5748
+ commandPath: hit ? hit.path : '',
5749
+ error: hit ? '' : lastError
5750
+ };
5751
+ });
5752
+
5753
+ const commandSpec = resolveInstallCommandsByPackageManager(packageManager);
5754
+ return {
5755
+ platform: process.platform,
5756
+ packageManager: commandSpec.packageManager,
5757
+ targets: targetReports,
5758
+ commandsByTarget: commandSpec.commandsByTarget
5759
+ };
5760
+ }
5761
+
5762
+ const ZIP_PATHS = [
5763
+ 'zip'
4443
5764
  ];
4444
5765
 
4445
- function findSevenZipExecutable() {
4446
- for (const candidate of SEVEN_ZIP_PATHS) {
5766
+ function findZipExecutable() {
5767
+ for (const candidate of ZIP_PATHS) {
4447
5768
  try {
4448
- if (candidate === '7z') {
4449
- if (commandExists('7z', '--help')) {
4450
- return '7z';
5769
+ if (candidate === 'zip') {
5770
+ if (commandExists('zip', '--help')) {
5771
+ return 'zip';
4451
5772
  }
4452
5773
  } else if (fs.existsSync(candidate)) {
4453
5774
  return candidate;
@@ -4458,18 +5779,14 @@ function findSevenZipExecutable() {
4458
5779
  }
4459
5780
 
4460
5781
  function resolveZipTool() {
4461
- const sevenZipExe = findSevenZipExecutable();
4462
- if (sevenZipExe) {
4463
- return { type: '7z', cmd: sevenZipExe };
5782
+ const zipExe = findZipExecutable();
5783
+ if (zipExe) {
5784
+ return { type: 'zip', cmd: zipExe };
4464
5785
  }
4465
5786
  return { type: 'lib', cmd: 'zip-lib' };
4466
5787
  }
4467
5788
 
4468
5789
  function resolveUnzipTool() {
4469
- const sevenZipExe = findSevenZipExecutable();
4470
- if (sevenZipExe) {
4471
- return { type: '7z', cmd: sevenZipExe };
4472
- }
4473
5790
  return { type: 'lib', cmd: 'zip-lib' };
4474
5791
  }
4475
5792
 
@@ -4486,7 +5803,7 @@ async function unzipWithLibrary(zipPath, outputDir) {
4486
5803
  await zipLib.extract(zipPath, outputDir);
4487
5804
  }
4488
5805
 
4489
- // 压缩(7-Zip 优先)
5806
+ // 压缩(系统 zip 优先,其次 zip-lib)
4490
5807
  async function cmdZip(targetPath, options = {}) {
4491
5808
  if (!targetPath) {
4492
5809
  console.error('用法: codexmate zip <文件或文件夹路径> [--max:压缩级别]');
@@ -4516,40 +5833,27 @@ async function cmdZip(targetPath, options = {}) {
4516
5833
  const outputPath = path.join(outputDir, `${baseName}.zip`);
4517
5834
 
4518
5835
  const zipTool = resolveZipTool();
5836
+ const useZipCmd = zipTool.type === 'zip';
4519
5837
 
4520
5838
  console.log('\n压缩配置:');
4521
5839
  console.log(' 源路径:', absPath);
4522
5840
  console.log(' 输出文件:', outputPath);
4523
- console.log(' 压缩级别:', compressionLevel);
4524
- console.log(' 压缩工具:', zipTool.type === '7z' ? '7-Zip' : 'zip-lib');
4525
- console.log(' 多线程:', zipTool.type === '7z' ? '启用' : '未启用(JS 库)');
4526
- if (zipTool.type !== '7z') {
4527
- console.log(' 提示: JS 库不支持压缩级别,已忽略 --max');
5841
+ console.log(' 压缩工具:', useZipCmd ? '系统 zip' : 'zip-lib');
5842
+ if (useZipCmd) {
5843
+ console.log(' 压缩级别:', compressionLevel);
5844
+ } else {
5845
+ console.log(' 压缩级别: 固定(zip-lib 不支持 --max,已忽略)');
4528
5846
  }
4529
5847
  console.log('\n开始压缩...\n');
4530
5848
 
4531
5849
  try {
4532
- if (zipTool.type === '7z') {
4533
- const cmd = `"${zipTool.cmd}" a -tzip -mmt=on -mx=${compressionLevel} "${outputPath}" "${absPath}"`;
4534
- const result = execSync(cmd, { encoding: 'utf-8', maxBuffer: 50 * 1024 * 1024 });
4535
- const sizeMatch = result.match(/Archive size:\s*(\d+)\s*bytes/);
4536
- const filesMatch = result.match(/(\d+)\s*files/);
4537
-
4538
- console.log('✓ 压缩完成!');
4539
- console.log(' 输出文件:', outputPath);
4540
- if (sizeMatch) {
4541
- const sizeBytes = parseInt(sizeMatch[1]);
4542
- const sizeMB = (sizeBytes / 1024 / 1024).toFixed(2);
4543
- console.log(' 压缩大小:', sizeMB, 'MB');
4544
- }
4545
- if (filesMatch) {
4546
- console.log(' 文件数量:', filesMatch[1]);
4547
- }
4548
- console.log();
4549
- return;
5850
+ if (useZipCmd) {
5851
+ const cmd = `"${zipTool.cmd}" -${compressionLevel} -q -r "${outputPath}" "${absPath}"`;
5852
+ execSync(cmd, { stdio: 'ignore' });
5853
+ } else {
5854
+ await zipWithLibrary(absPath, outputPath);
4550
5855
  }
4551
5856
 
4552
- await zipWithLibrary(absPath, outputPath);
4553
5857
  console.log('✓ 压缩完成!');
4554
5858
  console.log(' 输出文件:', outputPath);
4555
5859
  console.log();
@@ -4559,7 +5863,7 @@ async function cmdZip(targetPath, options = {}) {
4559
5863
  }
4560
5864
  }
4561
5865
 
4562
- // 解压(7-Zip 优先)
5866
+ // 解压(zip-lib)
4563
5867
  async function cmdUnzip(zipPath, outputDir) {
4564
5868
  if (!zipPath) {
4565
5869
  console.error('用法: codexmate unzip <zip文件路径> [输出目录]');
@@ -4591,24 +5895,10 @@ async function cmdUnzip(zipPath, outputDir) {
4591
5895
  console.log('\n解压配置:');
4592
5896
  console.log(' 源文件:', absZipPath);
4593
5897
  console.log(' 输出目录:', absOutputDir);
4594
- console.log(' 解压工具:', unzipTool.type === '7z' ? '7-Zip' : 'zip-lib');
4595
- console.log(' 多线程:', unzipTool.type === '7z' ? '启用' : '未启用(JS 库)');
5898
+ console.log(' 解压工具:', 'zip-lib');
4596
5899
  console.log('\n开始解压...\n');
4597
5900
 
4598
5901
  try {
4599
- if (unzipTool.type === '7z') {
4600
- const cmd = `"${unzipTool.cmd}" x -mmt=on -o"${absOutputDir}" "${absZipPath}" -y`;
4601
- const result = execSync(cmd, { encoding: 'utf-8', maxBuffer: 50 * 1024 * 1024 });
4602
- const filesMatch = result.match(/(\d+)\s*files/);
4603
- console.log('✓ 解压完成!');
4604
- console.log(' 输出目录:', absOutputDir);
4605
- if (filesMatch) {
4606
- console.log(' 文件数量:', filesMatch[1]);
4607
- }
4608
- console.log();
4609
- return;
4610
- }
4611
-
4612
5902
  await unzipWithLibrary(absZipPath, absOutputDir);
4613
5903
  console.log('✓ 解压完成!');
4614
5904
  console.log(' 输出目录:', absOutputDir);
@@ -4899,6 +6189,9 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
4899
6189
  initNotice: consumeInitNotice()
4900
6190
  };
4901
6191
  break;
6192
+ case 'install-status':
6193
+ result = buildInstallStatusReport();
6194
+ break;
4902
6195
  case 'list':
4903
6196
  const listConfigResult = readConfigOrVirtualDefault();
4904
6197
  const listConfig = listConfigResult.config;
@@ -4911,7 +6204,10 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
4911
6204
  url: p.base_url || '',
4912
6205
  key: maskKey(p.preferred_auth_method || ''),
4913
6206
  hasKey: !!(p.preferred_auth_method && p.preferred_auth_method.trim()),
4914
- current: name === current
6207
+ current: name === current,
6208
+ readOnly: isBuiltinProxyProvider(name),
6209
+ nonDeletable: isNonDeletableProvider(name),
6210
+ nonEditable: isNonEditableProvider(name)
4915
6211
  }))
4916
6212
  };
4917
6213
  break;
@@ -5082,6 +6378,61 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
5082
6378
  case 'session-plain':
5083
6379
  result = await readSessionPlain(params);
5084
6380
  break;
6381
+ case 'download-claude-dir':
6382
+ result = await prepareClaudeDirDownload();
6383
+ break;
6384
+ case 'download-codex-dir':
6385
+ result = await prepareCodexDirDownload();
6386
+ break;
6387
+ case 'restore-claude-dir':
6388
+ result = await restoreClaudeDir(params || {});
6389
+ break;
6390
+ case 'restore-codex-dir':
6391
+ result = await restoreCodexDir(params || {});
6392
+ break;
6393
+ case 'list-auth-profiles':
6394
+ result = {
6395
+ profiles: listAuthProfilesInfo()
6396
+ };
6397
+ break;
6398
+ case 'import-auth-profile':
6399
+ result = importAuthProfileFromUpload(params || {});
6400
+ break;
6401
+ case 'switch-auth-profile':
6402
+ {
6403
+ const profileName = params && typeof params.name === 'string' ? params.name.trim() : '';
6404
+ if (!profileName) {
6405
+ result = { error: '认证名称不能为空' };
6406
+ } else {
6407
+ try {
6408
+ result = switchAuthProfile(profileName, { silent: true });
6409
+ } catch (e) {
6410
+ result = { error: e.message || '切换认证失败' };
6411
+ }
6412
+ }
6413
+ }
6414
+ break;
6415
+ case 'delete-auth-profile':
6416
+ result = deleteAuthProfile(params && params.name ? params.name : '');
6417
+ break;
6418
+ case 'proxy-status':
6419
+ result = getBuiltinProxyStatus();
6420
+ break;
6421
+ case 'proxy-save-config':
6422
+ result = saveBuiltinProxySettings(params || {});
6423
+ break;
6424
+ case 'proxy-start':
6425
+ result = await startBuiltinProxyRuntime(params || {});
6426
+ break;
6427
+ case 'proxy-stop':
6428
+ result = await stopBuiltinProxyRuntime();
6429
+ break;
6430
+ case 'proxy-enable-codex-default':
6431
+ result = await ensureBuiltinProxyForCodexDefault(params || {});
6432
+ break;
6433
+ case 'proxy-apply-provider':
6434
+ result = applyBuiltinProxyProvider(params || {});
6435
+ break;
5085
6436
  default:
5086
6437
  result = { error: '未知操作' };
5087
6438
  }
@@ -5126,6 +6477,34 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
5126
6477
  : 'application/octet-stream';
5127
6478
  res.writeHead(200, { 'Content-Type': mime });
5128
6479
  fs.createReadStream(filePath).pipe(res);
6480
+ } else if (requestPath.startsWith('/download/')) {
6481
+ const fileName = requestPath.slice('/download/'.length);
6482
+ const decodedFileName = decodeURIComponent(fileName);
6483
+ const tempDir = os.tmpdir();
6484
+ const filePath = path.join(tempDir, decodedFileName);
6485
+
6486
+ if (!isPathInside(filePath, tempDir)) {
6487
+ res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' });
6488
+ res.end('Forbidden');
6489
+ return;
6490
+ }
6491
+ if (!fs.existsSync(filePath)) {
6492
+ res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
6493
+ res.end('File Not Found');
6494
+ return;
6495
+ }
6496
+ const stat = fs.statSync(filePath);
6497
+ if (!stat.isFile()) {
6498
+ res.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' });
6499
+ res.end('Not a File');
6500
+ return;
6501
+ }
6502
+ res.writeHead(200, {
6503
+ 'Content-Type': 'application/zip',
6504
+ 'Content-Disposition': `attachment; filename="${path.basename(filePath)}"`,
6505
+ 'Content-Length': stat.size
6506
+ });
6507
+ fs.createReadStream(filePath).pipe(res);
5129
6508
  } else if (requestPath.startsWith('/res/')) {
5130
6509
  const normalized = path.normalize(requestPath).replace(/^([\\.\\/])+/, '');
5131
6510
  const filePath = path.join(__dirname, normalized);
@@ -5229,74 +6608,347 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
5229
6608
 
5230
6609
  // 打开 Web UI
5231
6610
  function cmdStart(options = {}) {
5232
- // Support both new src/ structure and legacy root structure for zero breaking changes
5233
- const webDirLegacy = path.join(__dirname, 'web-ui');
5234
- const webDirSrc = path.join(__dirname, 'src', 'web-ui');
5235
- const webDir = fs.existsSync(webDirSrc) ? webDirSrc : webDirLegacy;
6611
+ const webDir = path.join(__dirname, 'web-ui');
5236
6612
  const newHtmlPath = path.join(webDir, 'index.html');
5237
6613
  const legacyHtmlPath = path.join(__dirname, 'web-ui.html');
5238
- const srcHtmlPath = path.join(__dirname, 'src', 'web-ui.html');
5239
-
5240
- let htmlPath = newHtmlPath;
5241
- if (!fs.existsSync(newHtmlPath)) {
5242
- htmlPath = fs.existsSync(srcHtmlPath) ? srcHtmlPath : legacyHtmlPath;
5243
- }
5244
- const assetsDirLegacy = path.join(__dirname, 'res');
5245
- const assetsDirSrc = path.join(__dirname, 'src', 'res');
5246
- const assetsDir = fs.existsSync(assetsDirSrc) ? assetsDirSrc : assetsDirLegacy;
6614
+ const htmlPath = fs.existsSync(newHtmlPath) ? newHtmlPath : legacyHtmlPath;
6615
+ const assetsDir = path.join(__dirname, 'res');
5247
6616
  if (!fs.existsSync(htmlPath)) {
5248
6617
  console.error('错误: Web UI 页面不存在(尝试路径: web-ui/index.html, web-ui.html)');
5249
6618
  process.exit(1);
5250
6619
  }
5251
6620
 
5252
- const port = resolveWebPort();
5253
- const host = resolveWebHost(options);
5254
-
5255
- let serverHandle = createWebServer({
5256
- htmlPath,
5257
- assetsDir,
5258
- webDir,
5259
- host,
5260
- port,
5261
- openBrowser: true
5262
- });
6621
+ const port = resolveWebPort();
6622
+ const host = resolveWebHost(options);
6623
+
6624
+ let serverHandle = createWebServer({
6625
+ htmlPath,
6626
+ assetsDir,
6627
+ webDir,
6628
+ host,
6629
+ port,
6630
+ openBrowser: true
6631
+ });
6632
+
6633
+ const proxySettings = readBuiltinProxySettings();
6634
+ const shouldAutoStartProxy = proxySettings.enabled || hasCodexConfigReadyForProxy();
6635
+ if (shouldAutoStartProxy) {
6636
+ ensureBuiltinProxyForCodexDefault({
6637
+ ...proxySettings,
6638
+ switchToProxy: false
6639
+ }).then((res) => {
6640
+ if (res && res.success && res.runtime && res.runtime.listenUrl) {
6641
+ const entryProvider = res.runtime.provider || DEFAULT_LOCAL_PROVIDER_NAME;
6642
+ const upstreamLabel = res.runtime.upstreamProvider ? `(上游: ${res.runtime.upstreamProvider})` : '';
6643
+ console.log(`~ 内建代理已启动(${entryProvider}): ${res.runtime.listenUrl}${upstreamLabel}`);
6644
+ } else if (res && res.error) {
6645
+ console.warn(`! 内建代理启动失败: ${res.error}`);
6646
+ }
6647
+ }).catch((err) => {
6648
+ console.warn(`! 内建代理启动失败: ${err && err.message ? err.message : err}`);
6649
+ });
6650
+ }
6651
+
6652
+ const stopWatch = watchPathsForRestart(
6653
+ [webDir, legacyHtmlPath],
6654
+ async (info) => {
6655
+ const fileLabel = info && info.filename ? info.filename : (info && info.target ? path.basename(info.target) : 'unknown');
6656
+ console.log(`\n~ 侦测到前端变更 (${fileLabel}),重启中...`);
6657
+ console.log(' 正在停止旧服务...');
6658
+ try {
6659
+ await serverHandle.stop();
6660
+ console.log(' 旧服务已停止');
6661
+ } catch (e) {
6662
+ console.warn('! 停止旧服务失败:', e.message || e);
6663
+ }
6664
+ await new Promise((resolve) => setTimeout(resolve, 80));
6665
+ try {
6666
+ serverHandle = createWebServer({
6667
+ htmlPath,
6668
+ assetsDir,
6669
+ webDir,
6670
+ host,
6671
+ port,
6672
+ openBrowser: false
6673
+ });
6674
+ console.log('✓ 已重启 Web UI 服务\n');
6675
+ } catch (e) {
6676
+ console.error('! 重启失败:', e.message || e);
6677
+ }
6678
+ }
6679
+ );
6680
+
6681
+ const handleExit = () => {
6682
+ stopWatch();
6683
+ Promise.allSettled([
6684
+ serverHandle.stop(),
6685
+ stopBuiltinProxyRuntime()
6686
+ ]).finally(() => process.exit(0));
6687
+ };
6688
+
6689
+ process.on('SIGINT', handleExit);
6690
+ process.on('SIGTERM', handleExit);
6691
+ }
6692
+
6693
+ function cmdAuth(args = []) {
6694
+ const subcommand = (args[0] || 'list').toLowerCase();
6695
+
6696
+ if (subcommand === 'list') {
6697
+ const profiles = listAuthProfilesInfo();
6698
+ if (profiles.length === 0) {
6699
+ console.log('\n认证列表: (空)\n');
6700
+ return;
6701
+ }
6702
+ console.log('\n认证列表:');
6703
+ profiles.forEach((profile) => {
6704
+ const marker = profile.current ? '●' : ' ';
6705
+ const type = profile.type || 'unknown';
6706
+ const email = profile.email || '(无邮箱)';
6707
+ console.log(` ${marker} ${profile.name} [${type}] ${email}`);
6708
+ });
6709
+ console.log();
6710
+ return;
6711
+ }
6712
+
6713
+ if (subcommand === 'status') {
6714
+ const profiles = listAuthProfilesInfo();
6715
+ const current = profiles.find((item) => item.current);
6716
+ if (!current) {
6717
+ console.log('\n当前认证: 未设置\n');
6718
+ return;
6719
+ }
6720
+ console.log('\n当前认证:');
6721
+ console.log(' 名称:', current.name);
6722
+ console.log(' 类型:', current.type || 'unknown');
6723
+ if (current.email) {
6724
+ console.log(' 账号:', current.email);
6725
+ }
6726
+ if (current.expired) {
6727
+ console.log(' 过期时间:', current.expired);
6728
+ }
6729
+ console.log();
6730
+ return;
6731
+ }
6732
+
6733
+ if (subcommand === 'import' || subcommand === 'upload') {
6734
+ const filePath = args[1];
6735
+ const nameArg = args[2] && !args[2].startsWith('--') ? args[2] : '';
6736
+ const noActivate = args.includes('--no-activate');
6737
+ if (!filePath) {
6738
+ throw new Error('用法: codexmate auth import <json文件路径> [名称] [--no-activate]');
6739
+ }
6740
+ const result = importAuthProfileFromFile(filePath, {
6741
+ name: nameArg,
6742
+ activate: !noActivate
6743
+ });
6744
+ console.log(`✓ 已导入认证: ${result.profile.name}`);
6745
+ if (result.profile.email) {
6746
+ console.log(` 账号: ${result.profile.email}`);
6747
+ }
6748
+ if (!noActivate) {
6749
+ console.log(' 已自动切换为当前认证');
6750
+ }
6751
+ console.log();
6752
+ return;
6753
+ }
6754
+
6755
+ if (subcommand === 'switch' || subcommand === 'use') {
6756
+ const name = args[1];
6757
+ if (!name) {
6758
+ throw new Error('用法: codexmate auth switch <名称>');
6759
+ }
6760
+ switchAuthProfile(name);
6761
+ return;
6762
+ }
6763
+
6764
+ if (subcommand === 'delete' || subcommand === 'remove') {
6765
+ const name = args[1];
6766
+ if (!name) {
6767
+ throw new Error('用法: codexmate auth delete <名称>');
6768
+ }
6769
+ const result = deleteAuthProfile(name);
6770
+ if (result.error) {
6771
+ throw new Error(result.error);
6772
+ }
6773
+ console.log(`✓ 已删除认证: ${name}`);
6774
+ if (result.switchedTo) {
6775
+ console.log(` 已自动切换到: ${result.switchedTo}`);
6776
+ }
6777
+ console.log();
6778
+ return;
6779
+ }
6780
+
6781
+ throw new Error(`未知 auth 子命令: ${subcommand}`);
6782
+ }
6783
+
6784
+ function parseProxyCliOptions(args = []) {
6785
+ const payload = {};
6786
+ for (let i = 0; i < args.length; i += 1) {
6787
+ const arg = args[i];
6788
+ if (arg === '--provider') {
6789
+ payload.provider = args[i + 1] || '';
6790
+ i += 1;
6791
+ continue;
6792
+ }
6793
+ if (arg === '--host') {
6794
+ payload.host = args[i + 1] || '';
6795
+ i += 1;
6796
+ continue;
6797
+ }
6798
+ if (arg === '--port') {
6799
+ const raw = args[i + 1];
6800
+ i += 1;
6801
+ if (raw === undefined) {
6802
+ return { error: '--port 缺少值' };
6803
+ }
6804
+ const port = parseInt(raw, 10);
6805
+ if (!Number.isFinite(port)) {
6806
+ return { error: '--port 必须是数字' };
6807
+ }
6808
+ payload.port = port;
6809
+ continue;
6810
+ }
6811
+ if (arg === '--auth-source') {
6812
+ payload.authSource = args[i + 1] || '';
6813
+ i += 1;
6814
+ continue;
6815
+ }
6816
+ if (arg === '--timeout-ms') {
6817
+ const raw = args[i + 1];
6818
+ i += 1;
6819
+ if (raw === undefined) {
6820
+ return { error: '--timeout-ms 缺少值' };
6821
+ }
6822
+ const timeoutMs = parseInt(raw, 10);
6823
+ if (!Number.isFinite(timeoutMs)) {
6824
+ return { error: '--timeout-ms 必须是数字' };
6825
+ }
6826
+ payload.timeoutMs = timeoutMs;
6827
+ continue;
6828
+ }
6829
+ if (arg === '--enable') {
6830
+ payload.enabled = true;
6831
+ continue;
6832
+ }
6833
+ if (arg === '--disable') {
6834
+ payload.enabled = false;
6835
+ continue;
6836
+ }
6837
+ if (arg === '--no-switch') {
6838
+ payload.switchToProxy = false;
6839
+ continue;
6840
+ }
6841
+ return { error: `未知参数: ${arg}` };
6842
+ }
6843
+ return { payload };
6844
+ }
6845
+
6846
+ async function cmdProxy(args = []) {
6847
+ const subcommand = (args[0] || 'status').toLowerCase();
6848
+ const optionResult = parseProxyCliOptions(args.slice(1));
6849
+ if (optionResult.error) {
6850
+ throw new Error(optionResult.error);
6851
+ }
6852
+ const options = optionResult.payload || {};
6853
+
6854
+ if (subcommand === 'status') {
6855
+ const status = getBuiltinProxyStatus();
6856
+ const settings = status.settings || DEFAULT_BUILTIN_PROXY_SETTINGS;
6857
+ console.log('\n内建代理状态:');
6858
+ console.log(' 运行中:', status.running ? '是' : '否');
6859
+ console.log(' 启用:', settings.enabled ? '是' : '否');
6860
+ console.log(' 监听:', buildProxyListenUrl(settings));
6861
+ console.log(' 上游 provider:', settings.provider || '(自动)');
6862
+ console.log(' 鉴权来源:', settings.authSource);
6863
+ if (status.runtime) {
6864
+ console.log(' 实际上游:', status.runtime.upstreamProvider);
6865
+ console.log(' 启动时间:', status.runtime.startedAt);
6866
+ }
6867
+ console.log();
6868
+ return;
6869
+ }
6870
+
6871
+ if (subcommand === 'set' || subcommand === 'config') {
6872
+ const result = saveBuiltinProxySettings(options);
6873
+ if (result.error) {
6874
+ throw new Error(result.error);
6875
+ }
6876
+ const settings = result.settings;
6877
+ console.log('✓ 内建代理配置已保存');
6878
+ console.log(' 监听:', buildProxyListenUrl(settings));
6879
+ console.log(' 上游 provider:', settings.provider || '(自动)');
6880
+ console.log(' 鉴权来源:', settings.authSource);
6881
+ console.log();
6882
+ return;
6883
+ }
6884
+
6885
+ if (subcommand === 'apply' || subcommand === 'apply-provider') {
6886
+ const result = applyBuiltinProxyProvider({
6887
+ switchToProxy: options.switchToProxy !== false
6888
+ });
6889
+ if (result.error) {
6890
+ throw new Error(result.error);
6891
+ }
6892
+ console.log(`✓ 已写入本地代理 provider: ${result.provider}`);
6893
+ console.log(` URL: ${result.baseUrl}`);
6894
+ if (result.switched) {
6895
+ console.log(` 已切换到 ${result.provider}${result.model ? ` / ${result.model}` : ''}`);
6896
+ }
6897
+ console.log();
6898
+ return;
6899
+ }
5263
6900
 
5264
- const stopWatch = watchPathsForRestart(
5265
- [webDir, path.join(__dirname, 'web-ui.html'), path.join(__dirname, 'src', 'web-ui.html')],
5266
- async (info) => {
5267
- const fileLabel = info && info.filename ? info.filename : (info && info.target ? path.basename(info.target) : 'unknown');
5268
- console.log(`\n~ 侦测到前端变更 (${fileLabel}),重启中...`);
5269
- console.log(' 正在停止旧服务...');
5270
- try {
5271
- await serverHandle.stop();
5272
- console.log(' 旧服务已停止');
5273
- } catch (e) {
5274
- console.warn('! 停止旧服务失败:', e.message || e);
5275
- }
5276
- await new Promise((resolve) => setTimeout(resolve, 80));
5277
- try {
5278
- serverHandle = createWebServer({
5279
- htmlPath,
5280
- assetsDir,
5281
- webDir,
5282
- host,
5283
- port,
5284
- openBrowser: false
5285
- });
5286
- console.log('✓ 已重启 Web UI 服务\n');
5287
- } catch (e) {
5288
- console.error('! 重启失败:', e.message || e);
5289
- }
6901
+ if (subcommand === 'enable' || subcommand === 'default-codex') {
6902
+ const result = await ensureBuiltinProxyForCodexDefault(options);
6903
+ if (result.error) {
6904
+ throw new Error(result.error);
5290
6905
  }
5291
- );
6906
+ const listenUrl = result.runtime && result.runtime.listenUrl
6907
+ ? result.runtime.listenUrl
6908
+ : buildProxyListenUrl(result.settings || DEFAULT_BUILTIN_PROXY_SETTINGS);
6909
+ console.log('✓ 已启用 Codex 内建代理默认模式');
6910
+ console.log(` 监听: ${listenUrl}`);
6911
+ if (result.runtime && result.runtime.upstreamProvider) {
6912
+ console.log(` 上游 provider: ${result.runtime.upstreamProvider}`);
6913
+ }
6914
+ console.log(` 当前 provider: ${result.provider}${result.model ? ` / ${result.model}` : ''}`);
6915
+ console.log();
6916
+ return;
6917
+ }
5292
6918
 
5293
- const handleExit = () => {
5294
- stopWatch();
5295
- serverHandle.stop().then(() => process.exit(0));
5296
- };
6919
+ if (subcommand === 'start') {
6920
+ const result = await startBuiltinProxyRuntime({
6921
+ ...options,
6922
+ enabled: true
6923
+ });
6924
+ if (result.error) {
6925
+ throw new Error(result.error);
6926
+ }
6927
+ console.log(`✓ 内建代理已启动: ${result.listenUrl}`);
6928
+ console.log(` 上游 provider: ${result.upstreamProvider}`);
6929
+ console.log(' 按 Ctrl+C 停止代理\n');
6930
+
6931
+ await new Promise((resolve) => {
6932
+ let stopping = false;
6933
+ const gracefulStop = async () => {
6934
+ if (stopping) return;
6935
+ stopping = true;
6936
+ await stopBuiltinProxyRuntime();
6937
+ resolve();
6938
+ };
6939
+ process.once('SIGINT', gracefulStop);
6940
+ process.once('SIGTERM', gracefulStop);
6941
+ });
6942
+ return;
6943
+ }
5297
6944
 
5298
- process.on('SIGINT', handleExit);
5299
- process.on('SIGTERM', handleExit);
6945
+ if (subcommand === 'stop') {
6946
+ await stopBuiltinProxyRuntime();
6947
+ console.log('✓ 内建代理已停止\n');
6948
+ return;
6949
+ }
6950
+
6951
+ throw new Error(`未知 proxy 子命令: ${subcommand}`);
5300
6952
  }
5301
6953
 
5302
6954
  async function runProxyCommand(displayName, binNames, args = [], installTip = '') {
@@ -5354,6 +7006,16 @@ async function runProxyCommand(displayName, binNames, args = [], installTip = ''
5354
7006
  }
5355
7007
 
5356
7008
  async function cmdCodex(args = []) {
7009
+ const ensureResult = await ensureBuiltinProxyForCodexDefault({});
7010
+ if (!ensureResult || ensureResult.success !== true) {
7011
+ const message = ensureResult && ensureResult.error
7012
+ ? ensureResult.error
7013
+ : '内建代理准备失败';
7014
+ throw new Error(message);
7015
+ }
7016
+ if (ensureResult.runtime && ensureResult.runtime.listenUrl) {
7017
+ console.log(`~ Codex 默认走内建代理: ${ensureResult.runtime.listenUrl}`);
7018
+ }
5357
7019
  return runProxyCommand('Codex', 'codex', args);
5358
7020
  }
5359
7021
 
@@ -5365,16 +7027,783 @@ async function cmdGemini(args = []) {
5365
7027
  return runProxyCommand('Gemini', ['gemini', 'gemini-cli'], args, 'npm install -g @google/gemini-cli');
5366
7028
  }
5367
7029
 
7030
+ function parseMcpOptions(args = []) {
7031
+ const options = {
7032
+ subcommand: 'serve',
7033
+ transport: 'stdio',
7034
+ allowWrite: false,
7035
+ help: false
7036
+ };
7037
+
7038
+ const argv = Array.isArray(args) ? [...args] : [];
7039
+ if (argv.length > 0 && !argv[0].startsWith('-')) {
7040
+ options.subcommand = String(argv.shift() || '').trim().toLowerCase() || 'serve';
7041
+ }
7042
+
7043
+ const envAllowWrite = typeof process.env.CODEXMATE_MCP_ALLOW_WRITE === 'string'
7044
+ && ['1', 'true', 'yes', 'on'].includes(process.env.CODEXMATE_MCP_ALLOW_WRITE.trim().toLowerCase());
7045
+ options.allowWrite = envAllowWrite;
7046
+
7047
+ for (let i = 0; i < argv.length; i++) {
7048
+ const arg = argv[i];
7049
+ if (!arg) continue;
7050
+ if (arg === '--help' || arg === '-h') {
7051
+ options.help = true;
7052
+ continue;
7053
+ }
7054
+ if (arg === '--allow-write' || arg === '--allow-write-tools') {
7055
+ options.allowWrite = true;
7056
+ continue;
7057
+ }
7058
+ if (arg === '--read-only') {
7059
+ options.allowWrite = false;
7060
+ continue;
7061
+ }
7062
+ if (arg.startsWith('--transport=')) {
7063
+ options.transport = arg.slice('--transport='.length).trim().toLowerCase() || options.transport;
7064
+ continue;
7065
+ }
7066
+ if (arg === '--transport') {
7067
+ options.transport = String(argv[i + 1] || '').trim().toLowerCase() || options.transport;
7068
+ i += 1;
7069
+ continue;
7070
+ }
7071
+ }
7072
+
7073
+ return options;
7074
+ }
7075
+
7076
+ function toMcpToolResult(payload) {
7077
+ const structured = payload === undefined
7078
+ ? {}
7079
+ : (payload && typeof payload === 'object' ? payload : { value: payload });
7080
+ const hasError = !!(structured && typeof structured === 'object' && (
7081
+ (typeof structured.error === 'string' && structured.error.trim())
7082
+ || structured.success === false
7083
+ ));
7084
+ const text = JSON.stringify(structured, null, 2);
7085
+ const result = {
7086
+ content: [{ type: 'text', text }],
7087
+ structuredContent: structured
7088
+ };
7089
+ if (hasError) {
7090
+ result.isError = true;
7091
+ }
7092
+ return result;
7093
+ }
7094
+
7095
+ function buildMcpStatusPayload() {
7096
+ const statusConfigResult = readConfigOrVirtualDefault();
7097
+ const config = statusConfigResult.config;
7098
+ const serviceTier = typeof config.service_tier === 'string' ? config.service_tier.trim() : '';
7099
+ const modelReasoningEffort = typeof config.model_reasoning_effort === 'string' ? config.model_reasoning_effort.trim() : '';
7100
+ return {
7101
+ provider: config.model_provider || '未设置',
7102
+ model: config.model || '未设置',
7103
+ serviceTier,
7104
+ modelReasoningEffort,
7105
+ configReady: !statusConfigResult.isVirtual,
7106
+ configNotice: statusConfigResult.reason || '',
7107
+ initNotice: consumeInitNotice()
7108
+ };
7109
+ }
7110
+
7111
+ function buildMcpProviderListPayload() {
7112
+ const listConfigResult = readConfigOrVirtualDefault();
7113
+ const listConfig = listConfigResult.config;
7114
+ const providers = listConfig.model_providers || {};
7115
+ const current = listConfig.model_provider;
7116
+ return {
7117
+ configReady: !listConfigResult.isVirtual,
7118
+ providers: Object.entries(providers).map(([name, p]) => ({
7119
+ name,
7120
+ url: p.base_url || '',
7121
+ key: maskKey(p.preferred_auth_method || ''),
7122
+ hasKey: !!(p.preferred_auth_method && p.preferred_auth_method.trim()),
7123
+ current: name === current,
7124
+ readOnly: isBuiltinProxyProvider(name),
7125
+ nonDeletable: isNonDeletableProvider(name),
7126
+ nonEditable: isNonEditableProvider(name)
7127
+ }))
7128
+ };
7129
+ }
7130
+
7131
+ function buildMcpClaudeSettingsPayload() {
7132
+ const info = readClaudeSettingsInfo();
7133
+ if (!info || typeof info !== 'object') {
7134
+ return { error: '读取 Claude 配置失败' };
7135
+ }
7136
+ if (info.error) {
7137
+ return info;
7138
+ }
7139
+
7140
+ const apiKey = typeof info.apiKey === 'string' ? info.apiKey : '';
7141
+ const baseUrl = typeof info.baseUrl === 'string' ? info.baseUrl : '';
7142
+ const model = typeof info.model === 'string' ? info.model : '';
7143
+ const maskedApiKey = maskKey(apiKey);
7144
+
7145
+ return {
7146
+ exists: !!info.exists,
7147
+ targetPath: info.targetPath || CLAUDE_SETTINGS_FILE,
7148
+ apiKey: maskedApiKey,
7149
+ apiKeyMasked: maskedApiKey,
7150
+ baseUrl,
7151
+ model,
7152
+ env: {
7153
+ ANTHROPIC_API_KEY: maskedApiKey,
7154
+ ANTHROPIC_BASE_URL: baseUrl,
7155
+ ANTHROPIC_MODEL: model
7156
+ },
7157
+ redacted: true
7158
+ };
7159
+ }
7160
+
7161
+ function normalizeMcpSource(value) {
7162
+ const source = typeof value === 'string' ? value.trim().toLowerCase() : '';
7163
+ if (!source) return '';
7164
+ if (source === 'codex' || source === 'claude' || source === 'all') {
7165
+ return source;
7166
+ }
7167
+ return null;
7168
+ }
7169
+
7170
+ function createMcpTools(options = {}) {
7171
+ const allowWrite = !!options.allowWrite;
7172
+ const tools = [];
7173
+
7174
+ const pushTool = (tool) => {
7175
+ if (!tool || typeof tool !== 'object') return;
7176
+ if (!tool.readOnly && !allowWrite) return;
7177
+ tools.push({
7178
+ name: tool.name,
7179
+ description: tool.description,
7180
+ inputSchema: tool.inputSchema || { type: 'object', properties: {}, additionalProperties: false },
7181
+ annotations: {
7182
+ readOnlyHint: !!tool.readOnly
7183
+ },
7184
+ handler: async (args = {}) => {
7185
+ try {
7186
+ const payload = await tool.handler(args || {});
7187
+ return toMcpToolResult(payload);
7188
+ } catch (error) {
7189
+ return toMcpToolResult({
7190
+ error: error && error.message ? error.message : String(error || 'Tool execution failed')
7191
+ });
7192
+ }
7193
+ }
7194
+ });
7195
+ };
7196
+
7197
+ pushTool({
7198
+ name: 'codexmate.status.get',
7199
+ description: 'Get current provider/model status, config readiness and startup notice.',
7200
+ readOnly: true,
7201
+ inputSchema: { type: 'object', properties: {}, additionalProperties: false },
7202
+ handler: async () => buildMcpStatusPayload()
7203
+ });
7204
+
7205
+ pushTool({
7206
+ name: 'codexmate.provider.list',
7207
+ description: 'List configured providers with masked key and active flags.',
7208
+ readOnly: true,
7209
+ inputSchema: { type: 'object', properties: {}, additionalProperties: false },
7210
+ handler: async () => buildMcpProviderListPayload()
7211
+ });
7212
+
7213
+ pushTool({
7214
+ name: 'codexmate.model.list',
7215
+ description: 'List models from a provider. If provider is omitted, use current provider.',
7216
+ readOnly: true,
7217
+ inputSchema: {
7218
+ type: 'object',
7219
+ properties: {
7220
+ provider: { type: 'string' }
7221
+ },
7222
+ additionalProperties: false
7223
+ },
7224
+ handler: async (args = {}) => {
7225
+ const rawProvider = typeof args.provider === 'string' ? args.provider.trim() : '';
7226
+ let providerName = rawProvider;
7227
+ if (!providerName) {
7228
+ const cfg = readConfigOrVirtualDefault().config || {};
7229
+ providerName = typeof cfg.model_provider === 'string' ? cfg.model_provider.trim() : '';
7230
+ }
7231
+ if (!providerName) {
7232
+ return { error: 'Provider name is required' };
7233
+ }
7234
+ const res = await fetchProviderModels(providerName);
7235
+ if (res.error) {
7236
+ return { error: res.error, models: [], source: 'remote' };
7237
+ }
7238
+ if (res.unlimited) {
7239
+ return { models: [], source: 'remote', provider: res.provider || '', unlimited: true };
7240
+ }
7241
+ return { models: res.models || [], source: 'remote', provider: res.provider || '' };
7242
+ }
7243
+ });
7244
+
7245
+ pushTool({
7246
+ name: 'codexmate.config.template.get',
7247
+ description: 'Get Codex config template with optional provider/model/service tier/reasoning effort.',
7248
+ readOnly: true,
7249
+ inputSchema: {
7250
+ type: 'object',
7251
+ properties: {
7252
+ provider: { type: 'string' },
7253
+ model: { type: 'string' },
7254
+ serviceTier: { type: 'string' },
7255
+ reasoningEffort: { type: 'string' }
7256
+ },
7257
+ additionalProperties: false
7258
+ },
7259
+ handler: async (args = {}) => getConfigTemplate(args || {})
7260
+ });
7261
+
7262
+ pushTool({
7263
+ name: 'codexmate.claude.settings.get',
7264
+ description: 'Read Claude settings.json env values managed by codexmate.',
7265
+ readOnly: true,
7266
+ inputSchema: { type: 'object', properties: {}, additionalProperties: false },
7267
+ handler: async () => buildMcpClaudeSettingsPayload()
7268
+ });
7269
+
7270
+ pushTool({
7271
+ name: 'codexmate.openclaw.config.get',
7272
+ description: 'Read OpenClaw config file content and metadata.',
7273
+ readOnly: true,
7274
+ inputSchema: { type: 'object', properties: {}, additionalProperties: false },
7275
+ handler: async () => readOpenclawConfigFile()
7276
+ });
7277
+
7278
+ pushTool({
7279
+ name: 'codexmate.session.list',
7280
+ description: 'List sessions from codex/claude/all with filters.',
7281
+ readOnly: true,
7282
+ inputSchema: {
7283
+ type: 'object',
7284
+ properties: {
7285
+ source: { type: 'string' },
7286
+ pathFilter: { type: 'string' },
7287
+ query: { type: 'string' },
7288
+ roleFilter: { type: 'string' },
7289
+ timeRangePreset: { type: 'string' },
7290
+ limit: { type: 'number' },
7291
+ forceRefresh: { type: 'boolean' },
7292
+ queryMode: { type: 'string' },
7293
+ queryScope: { type: 'string' },
7294
+ contentScanLimit: { type: 'number' }
7295
+ },
7296
+ additionalProperties: false
7297
+ },
7298
+ handler: async (args = {}) => {
7299
+ const input = args && typeof args === 'object' ? args : {};
7300
+ const source = normalizeMcpSource(input.source);
7301
+ if (source === null) {
7302
+ return { error: 'Invalid source. Must be codex, claude, or all' };
7303
+ }
7304
+ const normalizedInput = {
7305
+ ...input,
7306
+ source: source || 'all'
7307
+ };
7308
+ return {
7309
+ sessions: listAllSessions(normalizedInput),
7310
+ source: source || 'all'
7311
+ };
7312
+ }
7313
+ });
7314
+
7315
+ pushTool({
7316
+ name: 'codexmate.session.detail',
7317
+ description: 'Read a session detail by source + sessionId/file.',
7318
+ readOnly: true,
7319
+ inputSchema: {
7320
+ type: 'object',
7321
+ properties: {
7322
+ source: { type: 'string' },
7323
+ sessionId: { type: 'string' },
7324
+ file: { type: 'string' },
7325
+ maxMessages: { type: ['string', 'number'] }
7326
+ },
7327
+ additionalProperties: true
7328
+ },
7329
+ handler: async (args = {}) => readSessionDetail(args || {})
7330
+ });
7331
+
7332
+ pushTool({
7333
+ name: 'codexmate.session.export',
7334
+ description: 'Export session as markdown payload.',
7335
+ readOnly: true,
7336
+ inputSchema: {
7337
+ type: 'object',
7338
+ properties: {
7339
+ source: { type: 'string' },
7340
+ sessionId: { type: 'string' },
7341
+ file: { type: 'string' },
7342
+ maxMessages: { type: ['string', 'number'] }
7343
+ },
7344
+ additionalProperties: true
7345
+ },
7346
+ handler: async (args = {}) => exportSessionData(args || {})
7347
+ });
7348
+
7349
+ pushTool({
7350
+ name: 'codexmate.auth.profile.list',
7351
+ description: 'List codex auth profiles.',
7352
+ readOnly: true,
7353
+ inputSchema: { type: 'object', properties: {}, additionalProperties: false },
7354
+ handler: async () => ({ profiles: listAuthProfilesInfo() })
7355
+ });
7356
+
7357
+ pushTool({
7358
+ name: 'codexmate.proxy.status',
7359
+ description: 'Get builtin proxy runtime status and persisted config.',
7360
+ readOnly: true,
7361
+ inputSchema: { type: 'object', properties: {}, additionalProperties: false },
7362
+ handler: async () => getBuiltinProxyStatus()
7363
+ });
7364
+
7365
+ pushTool({
7366
+ name: 'codexmate.config.template.apply',
7367
+ description: 'Apply Codex TOML template and sync auth/model pointers.',
7368
+ readOnly: false,
7369
+ inputSchema: {
7370
+ type: 'object',
7371
+ properties: {
7372
+ template: { type: 'string' }
7373
+ },
7374
+ required: ['template'],
7375
+ additionalProperties: false
7376
+ },
7377
+ handler: async (args = {}) => applyConfigTemplate(args || {})
7378
+ });
7379
+
7380
+ pushTool({
7381
+ name: 'codexmate.provider.add',
7382
+ description: 'Add provider into config.toml model_providers.',
7383
+ readOnly: false,
7384
+ inputSchema: {
7385
+ type: 'object',
7386
+ properties: {
7387
+ name: { type: 'string' },
7388
+ url: { type: 'string' },
7389
+ key: { type: 'string' }
7390
+ },
7391
+ required: ['name', 'url'],
7392
+ additionalProperties: false
7393
+ },
7394
+ handler: async (args = {}) => addProviderToConfig(args || {})
7395
+ });
7396
+
7397
+ pushTool({
7398
+ name: 'codexmate.provider.update',
7399
+ description: 'Update provider url/key.',
7400
+ readOnly: false,
7401
+ inputSchema: {
7402
+ type: 'object',
7403
+ properties: {
7404
+ name: { type: 'string' },
7405
+ url: { type: 'string' },
7406
+ key: { type: 'string' }
7407
+ },
7408
+ required: ['name'],
7409
+ additionalProperties: false
7410
+ },
7411
+ handler: async (args = {}) => updateProviderInConfig(args || {})
7412
+ });
7413
+
7414
+ pushTool({
7415
+ name: 'codexmate.provider.delete',
7416
+ description: 'Delete provider from config.',
7417
+ readOnly: false,
7418
+ inputSchema: {
7419
+ type: 'object',
7420
+ properties: {
7421
+ name: { type: 'string' }
7422
+ },
7423
+ required: ['name'],
7424
+ additionalProperties: false
7425
+ },
7426
+ handler: async (args = {}) => deleteProviderFromConfig(args || {})
7427
+ });
7428
+
7429
+ pushTool({
7430
+ name: 'codexmate.claude.config.apply',
7431
+ description: 'Apply Claude env config into ~/.claude/settings.json.',
7432
+ readOnly: false,
7433
+ inputSchema: {
7434
+ type: 'object',
7435
+ properties: {
7436
+ apiKey: { type: 'string' },
7437
+ baseUrl: { type: 'string' },
7438
+ model: { type: 'string' }
7439
+ },
7440
+ required: ['apiKey'],
7441
+ additionalProperties: false
7442
+ },
7443
+ handler: async (args = {}) => applyToClaudeSettings(args || {})
7444
+ });
7445
+
7446
+ pushTool({
7447
+ name: 'codexmate.openclaw.config.apply',
7448
+ description: 'Apply OpenClaw config content into ~/.openclaw/openclaw.json.',
7449
+ readOnly: false,
7450
+ inputSchema: {
7451
+ type: 'object',
7452
+ properties: {
7453
+ content: { type: 'string' },
7454
+ lineEnding: { type: 'string' }
7455
+ },
7456
+ required: ['content'],
7457
+ additionalProperties: false
7458
+ },
7459
+ handler: async (args = {}) => applyOpenclawConfig(args || {})
7460
+ });
7461
+
7462
+ pushTool({
7463
+ name: 'codexmate.session.delete',
7464
+ description: 'Delete one session or selected records in a session.',
7465
+ readOnly: false,
7466
+ inputSchema: {
7467
+ type: 'object',
7468
+ properties: {
7469
+ source: { type: 'string' },
7470
+ sessionId: { type: 'string' },
7471
+ file: { type: 'string' },
7472
+ recordLineIndex: { type: 'number' },
7473
+ recordLineIndices: { type: 'array', items: { type: 'number' } }
7474
+ },
7475
+ additionalProperties: true
7476
+ },
7477
+ handler: async (args = {}) => deleteSessionData(args || {})
7478
+ });
7479
+
7480
+ pushTool({
7481
+ name: 'codexmate.auth.profile.switch',
7482
+ description: 'Switch active auth profile by name.',
7483
+ readOnly: false,
7484
+ inputSchema: {
7485
+ type: 'object',
7486
+ properties: {
7487
+ name: { type: 'string' }
7488
+ },
7489
+ required: ['name'],
7490
+ additionalProperties: false
7491
+ },
7492
+ handler: async (args = {}) => {
7493
+ const profileName = typeof args.name === 'string' ? args.name.trim() : '';
7494
+ if (!profileName) return { error: '认证名称不能为空' };
7495
+ try {
7496
+ return switchAuthProfile(profileName, { silent: true });
7497
+ } catch (e) {
7498
+ return { error: e.message || '切换认证失败' };
7499
+ }
7500
+ }
7501
+ });
7502
+
7503
+ pushTool({
7504
+ name: 'codexmate.auth.profile.delete',
7505
+ description: 'Delete an auth profile by name.',
7506
+ readOnly: false,
7507
+ inputSchema: {
7508
+ type: 'object',
7509
+ properties: {
7510
+ name: { type: 'string' }
7511
+ },
7512
+ required: ['name'],
7513
+ additionalProperties: false
7514
+ },
7515
+ handler: async (args = {}) => deleteAuthProfile(typeof args.name === 'string' ? args.name : '')
7516
+ });
7517
+
7518
+ pushTool({
7519
+ name: 'codexmate.proxy.start',
7520
+ description: 'Start builtin proxy runtime with optional overrides.',
7521
+ readOnly: false,
7522
+ inputSchema: {
7523
+ type: 'object',
7524
+ properties: {
7525
+ enabled: { type: 'boolean' },
7526
+ host: { type: 'string' },
7527
+ port: { type: 'number' },
7528
+ provider: { type: 'string' },
7529
+ authSource: { type: 'string' },
7530
+ timeoutMs: { type: 'number' }
7531
+ },
7532
+ additionalProperties: false
7533
+ },
7534
+ handler: async (args = {}) => startBuiltinProxyRuntime(args || {})
7535
+ });
7536
+
7537
+ pushTool({
7538
+ name: 'codexmate.proxy.stop',
7539
+ description: 'Stop builtin proxy runtime.',
7540
+ readOnly: false,
7541
+ inputSchema: { type: 'object', properties: {}, additionalProperties: false },
7542
+ handler: async () => stopBuiltinProxyRuntime()
7543
+ });
7544
+
7545
+ pushTool({
7546
+ name: 'codexmate.proxy.provider.apply',
7547
+ description: 'Apply builtin proxy provider into codex config.',
7548
+ readOnly: false,
7549
+ inputSchema: {
7550
+ type: 'object',
7551
+ properties: {
7552
+ switchToProxy: { type: 'boolean' },
7553
+ provider: { type: 'string' }
7554
+ },
7555
+ additionalProperties: true
7556
+ },
7557
+ handler: async (args = {}) => applyBuiltinProxyProvider(args || {})
7558
+ });
7559
+
7560
+ return tools;
7561
+ }
7562
+
7563
+ function createMcpResources() {
7564
+ return [
7565
+ {
7566
+ uri: 'codexmate://status',
7567
+ name: 'Status',
7568
+ description: 'Current provider/model status snapshot.',
7569
+ mimeType: 'application/json',
7570
+ read: async () => ({
7571
+ contents: [{
7572
+ uri: 'codexmate://status',
7573
+ mimeType: 'application/json',
7574
+ text: JSON.stringify(buildMcpStatusPayload(), null, 2)
7575
+ }]
7576
+ })
7577
+ },
7578
+ {
7579
+ uri: 'codexmate://providers',
7580
+ name: 'Providers',
7581
+ description: 'Configured provider list (masked).',
7582
+ mimeType: 'application/json',
7583
+ read: async () => ({
7584
+ contents: [{
7585
+ uri: 'codexmate://providers',
7586
+ mimeType: 'application/json',
7587
+ text: JSON.stringify(buildMcpProviderListPayload(), null, 2)
7588
+ }]
7589
+ })
7590
+ },
7591
+ {
7592
+ uri: 'codexmate://sessions',
7593
+ name: 'Sessions',
7594
+ description: 'Session listing resource. Query by source/query/pathFilter via URI params.',
7595
+ mimeType: 'application/json',
7596
+ read: async (params = {}) => {
7597
+ const uri = typeof params.uri === 'string' ? params.uri : 'codexmate://sessions';
7598
+ let source = '';
7599
+ let query = '';
7600
+ let pathFilter = '';
7601
+ let roleFilter = '';
7602
+ let timeRangePreset = '';
7603
+ try {
7604
+ const parsed = new URL(uri);
7605
+ source = parsed.searchParams.get('source') || '';
7606
+ query = parsed.searchParams.get('query') || '';
7607
+ pathFilter = parsed.searchParams.get('pathFilter') || '';
7608
+ roleFilter = parsed.searchParams.get('roleFilter') || '';
7609
+ timeRangePreset = parsed.searchParams.get('timeRangePreset') || '';
7610
+ } catch (_) {}
7611
+ const normalizedSource = normalizeMcpSource(source);
7612
+ if (normalizedSource === null) {
7613
+ return {
7614
+ contents: [{
7615
+ uri,
7616
+ mimeType: 'application/json',
7617
+ text: JSON.stringify({ error: 'Invalid source. Must be codex, claude, or all' }, null, 2)
7618
+ }]
7619
+ };
7620
+ }
7621
+ const payload = {
7622
+ source: normalizedSource || 'all',
7623
+ sessions: listAllSessions({
7624
+ source: normalizedSource || 'all',
7625
+ query,
7626
+ pathFilter,
7627
+ roleFilter,
7628
+ timeRangePreset
7629
+ })
7630
+ };
7631
+ return {
7632
+ contents: [{
7633
+ uri,
7634
+ mimeType: 'application/json',
7635
+ text: JSON.stringify(payload, null, 2)
7636
+ }]
7637
+ };
7638
+ }
7639
+ }
7640
+ ];
7641
+ }
7642
+
7643
+ function createMcpPrompts() {
7644
+ return [
7645
+ {
7646
+ name: 'codexmate.diagnose_config',
7647
+ description: 'Generate troubleshooting guidance from current codexmate status/providers.',
7648
+ arguments: [],
7649
+ get: async () => {
7650
+ const status = buildMcpStatusPayload();
7651
+ const providers = buildMcpProviderListPayload();
7652
+ return {
7653
+ messages: [{
7654
+ role: 'user',
7655
+ content: {
7656
+ type: 'text',
7657
+ text: [
7658
+ '请根据以下配置快照进行故障诊断,并给出按优先级排序的修复步骤。',
7659
+ '要求:先给结论,再给操作清单,最后给风险与回滚建议。',
7660
+ '',
7661
+ '[status]',
7662
+ JSON.stringify(status, null, 2),
7663
+ '',
7664
+ '[providers]',
7665
+ JSON.stringify(providers, null, 2)
7666
+ ].join('\n')
7667
+ }
7668
+ }]
7669
+ };
7670
+ }
7671
+ },
7672
+ {
7673
+ name: 'codexmate.switch_provider_safely',
7674
+ description: 'Guide safe provider switch with pre-check and rollback plan.',
7675
+ arguments: [{
7676
+ name: 'provider',
7677
+ description: 'Target provider name',
7678
+ required: true
7679
+ }],
7680
+ get: async (args = {}) => {
7681
+ const provider = typeof args.provider === 'string' ? args.provider.trim() : '';
7682
+ return {
7683
+ messages: [{
7684
+ role: 'user',
7685
+ content: {
7686
+ type: 'text',
7687
+ text: [
7688
+ `请为 provider "${provider || '(missing)'}" 生成安全切换步骤。`,
7689
+ '要求:',
7690
+ '1) 先检查 provider 是否存在与 key 是否可用',
7691
+ '2) 给出切换后验证项(模型拉取/健康检查)',
7692
+ '3) 给出失败时回滚流程(回到旧 provider/model)'
7693
+ ].join('\n')
7694
+ }
7695
+ }]
7696
+ };
7697
+ }
7698
+ },
7699
+ {
7700
+ name: 'codexmate.export_session_for_issue',
7701
+ description: 'Prepare issue report template from a selected session export.',
7702
+ arguments: [{
7703
+ name: 'source',
7704
+ description: 'Session source: codex or claude',
7705
+ required: true
7706
+ }, {
7707
+ name: 'sessionId',
7708
+ description: 'Session id',
7709
+ required: true
7710
+ }],
7711
+ get: async (args = {}) => {
7712
+ const source = typeof args.source === 'string' ? args.source.trim() : '';
7713
+ const sessionId = typeof args.sessionId === 'string' ? args.sessionId.trim() : '';
7714
+ return {
7715
+ messages: [{
7716
+ role: 'user',
7717
+ content: {
7718
+ type: 'text',
7719
+ text: [
7720
+ '请根据会话导出内容生成 issue 报告草稿。',
7721
+ `source: ${source || '(missing)'}`,
7722
+ `sessionId: ${sessionId || '(missing)'}`,
7723
+ '',
7724
+ '报告需包含:问题现象、复现步骤、预期行为、实际行为、可疑配置项。'
7725
+ ].join('\n')
7726
+ }
7727
+ }]
7728
+ };
7729
+ }
7730
+ }
7731
+ ];
7732
+ }
7733
+
7734
+ async function cmdMcp(args = []) {
7735
+ const options = parseMcpOptions(args);
7736
+ if (options.help) {
7737
+ console.log('\n用法: codexmate mcp [serve] [--transport stdio] [--allow-write|--read-only]');
7738
+ console.log(' 默认 transport=stdio,默认 read-only。');
7739
+ console.log(' 设置环境变量 CODEXMATE_MCP_ALLOW_WRITE=1 可默认开启写工具。');
7740
+ console.log();
7741
+ return;
7742
+ }
7743
+
7744
+ if (options.subcommand !== 'serve') {
7745
+ throw new Error(`未知 mcp 子命令: ${options.subcommand}`);
7746
+ }
7747
+ if (options.transport !== 'stdio') {
7748
+ throw new Error(`当前仅支持 stdio 传输,收到: ${options.transport}`);
7749
+ }
7750
+
7751
+ const packageVersion = (() => {
7752
+ try {
7753
+ const pkg = require('./package.json');
7754
+ return pkg && pkg.version ? pkg.version : '0.0.0';
7755
+ } catch (_) {
7756
+ return '0.0.0';
7757
+ }
7758
+ })();
7759
+
7760
+ const server = createMcpStdioServer({
7761
+ protocolVersion: '2025-11-25',
7762
+ serverInfo: {
7763
+ name: 'codexmate-mcp',
7764
+ version: packageVersion
7765
+ },
7766
+ tools: createMcpTools({ allowWrite: options.allowWrite }),
7767
+ resources: createMcpResources(),
7768
+ prompts: createMcpPrompts(),
7769
+ logger: (level, message) => {
7770
+ const label = level === 'error' ? 'ERR' : 'INFO';
7771
+ console.error(`[MCP ${label}] ${message}`);
7772
+ }
7773
+ });
7774
+
7775
+ server.start();
7776
+
7777
+ await new Promise((resolve) => {
7778
+ let done = false;
7779
+ const finish = () => {
7780
+ if (done) return;
7781
+ done = true;
7782
+ server.stop();
7783
+ stopBuiltinProxyRuntime().finally(() => resolve());
7784
+ };
7785
+ process.once('SIGINT', finish);
7786
+ process.once('SIGTERM', finish);
7787
+ process.stdin.once('end', finish);
7788
+ process.stdin.once('close', finish);
7789
+ });
7790
+ }
7791
+
5368
7792
  // ============================================================================
5369
7793
  // 主程序
5370
7794
  // ============================================================================
5371
7795
  async function main() {
7796
+ const args = process.argv.slice(2);
7797
+ const command = args[0];
7798
+ const isMcpCommand = command === 'mcp';
5372
7799
  const bootstrap = ensureManagedConfigBootstrap();
5373
7800
  if (bootstrap && bootstrap.notice) {
5374
- console.log(`\n[Init] ${bootstrap.notice}`);
7801
+ // MCP stdio transport requires stdout to be protocol-clean.
7802
+ if (!isMcpCommand) {
7803
+ console.log(`\n[Init] ${bootstrap.notice}`);
7804
+ }
5375
7805
  }
5376
7806
 
5377
- const args = process.argv.slice(2);
5378
7807
  if (args.length === 0) {
5379
7808
  console.log('\nCodex Mate - Codex 提供商管理工具');
5380
7809
  console.log('\n用法:');
@@ -5389,19 +7818,20 @@ async function main() {
5389
7818
  console.log(' codexmate claude <BaseURL> <API密钥> [模型] 写入 Claude Code 配置');
5390
7819
  console.log(' codexmate add-model <模型> 添加模型');
5391
7820
  console.log(' codexmate delete-model <模型> 删除模型');
7821
+ console.log(' codexmate auth <list|import|switch|delete|status> 认证文件管理');
7822
+ console.log(' codexmate proxy <status|set|apply|enable|start|stop> 内建代理');
5392
7823
  console.log(' codexmate run [--host <HOST>] 启动 Web 界面');
5393
7824
  console.log(' codexmate codex [参数...] 等同于 codex --yolo');
5394
7825
  console.log(' codexmate qwen [参数...] 等同于 qwen --yolo');
5395
7826
  console.log(' codexmate gemini [参数...] 等同于 gemini --yolo');
7827
+ console.log(' codexmate mcp [serve] [--transport stdio] [--allow-write|--read-only]');
5396
7828
  console.log(' codexmate export-session --source <codex|claude> (--session-id <ID>|--file <PATH>) [--output <PATH>] [--max-messages <N|all|Infinity>]');
5397
- console.log(' codexmate zip <路径> [--max:级别] 压缩(7-Zip 优先)');
5398
- console.log(' codexmate unzip <zip文件> [输出目录] 解压(7-Zip 优先)');
7829
+ console.log(' codexmate zip <路径> [--max:级别] 压缩(系统 zip 优先,其次 zip-lib)');
7830
+ console.log(' codexmate unzip <zip文件> [输出目录] 解压(zip-lib)');
5399
7831
  console.log('');
5400
7832
  process.exit(0);
5401
7833
  }
5402
7834
 
5403
- const command = args[0];
5404
-
5405
7835
  switch (command) {
5406
7836
  case 'status': cmdStatus(); break;
5407
7837
  case 'setup': await cmdSetup(); break;
@@ -5414,6 +7844,8 @@ async function main() {
5414
7844
  case 'claude': cmdClaude(args[1], args[2], args[3]); break;
5415
7845
  case 'add-model': cmdAddModel(args[1]); break;
5416
7846
  case 'delete-model': cmdDeleteModel(args[1]); break;
7847
+ case 'auth': cmdAuth(args.slice(1)); break;
7848
+ case 'proxy': await cmdProxy(args.slice(1)); break;
5417
7849
  case 'run': cmdStart(parseStartOptions(args.slice(1))); break;
5418
7850
  case 'start':
5419
7851
  console.error('错误: 命令已更名为 "run",请使用: codexmate run');
@@ -5434,6 +7866,7 @@ async function main() {
5434
7866
  process.exit(exitCode);
5435
7867
  break;
5436
7868
  }
7869
+ case 'mcp': await cmdMcp(args.slice(1)); break;
5437
7870
  case 'export-session': await cmdExportSession(args.slice(1)); break;
5438
7871
  case 'zip': {
5439
7872
  // 解析 --max:N 参数