codexmate 0.0.10 → 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.
Files changed (50) hide show
  1. package/README.md +52 -12
  2. package/README.zh-CN.md +52 -12
  3. package/cli.js +3491 -563
  4. package/{CHANGELOG.md → doc/CHANGELOG.md} +6 -0
  5. package/{CHANGELOG.zh-CN.md → doc/CHANGELOG.zh-CN.md} +6 -0
  6. package/lib/mcp-stdio.js +440 -0
  7. package/package.json +22 -2
  8. package/res/logo.png +0 -0
  9. package/web-ui/app.js +1171 -149
  10. package/web-ui/index.html +1605 -0
  11. package/web-ui/logic.mjs +21 -21
  12. package/web-ui/styles.css +3213 -0
  13. package/web-ui.html +7 -3967
  14. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -27
  15. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -17
  16. package/.github/workflows/ci.yml +0 -26
  17. package/.github/workflows/release.yml +0 -159
  18. package/.planning/.fix-attempts +0 -1
  19. package/.planning/.lock +0 -6
  20. package/.planning/.verify-cache.json +0 -14
  21. package/.planning/CHECKPOINT.json +0 -46
  22. package/.planning/DESIGN.md +0 -26
  23. package/.planning/HISTORY.json +0 -124
  24. package/.planning/PLAN.md +0 -69
  25. package/.planning/REVIEW.md +0 -41
  26. package/.planning/STATE.md +0 -12
  27. package/.planning/STATS.json +0 -13
  28. package/.planning/VERIFICATION.md +0 -70
  29. package/.planning/daude-code-plan.md +0 -51
  30. package/.planning/research/architecture.md +0 -32
  31. package/.planning/research/conventions.md +0 -36
  32. package/.planning/task_1-REVIEW.md +0 -29
  33. package/.planning/task_1-SUMMARY.md +0 -32
  34. package/.planning/task_2-REVIEW.md +0 -24
  35. package/.planning/task_2-SUMMARY.md +0 -37
  36. package/.planning/task_3-REVIEW.md +0 -25
  37. package/.planning/task_3-SUMMARY.md +0 -31
  38. package/cmd/publish-npm.cmd +0 -65
  39. package/tests/e2e/helpers.js +0 -214
  40. package/tests/e2e/recent-health.e2e.js +0 -142
  41. package/tests/e2e/run.js +0 -154
  42. package/tests/e2e/test-claude.js +0 -21
  43. package/tests/e2e/test-config.js +0 -124
  44. package/tests/e2e/test-health-speed.js +0 -79
  45. package/tests/e2e/test-openclaw.js +0 -47
  46. package/tests/e2e/test-session-search.js +0 -114
  47. package/tests/e2e/test-sessions.js +0 -69
  48. package/tests/e2e/test-setup.js +0 -159
  49. package/tests/unit/run.mjs +0 -29
  50. package/tests/unit/web-ui-logic.test.mjs +0 -186
package/cli.js CHANGED
@@ -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;
@@ -85,9 +91,9 @@ const MAX_SESSION_DETAIL_MESSAGES = 1000;
85
91
  const SESSION_TITLE_READ_BYTES = 64 * 1024;
86
92
  const CODEXMATE_MANAGED_MARKER = '# codexmate-managed: true';
87
93
  const SESSION_LIST_CACHE_TTL_MS = 4000;
88
- const SESSION_SUMMARY_READ_BYTES = 256 * 1024;
89
- const SESSION_CONTENT_READ_BYTES = SESSION_SUMMARY_READ_BYTES;
90
- const DEFAULT_CONTENT_SCAN_LIMIT = 50;
94
+ const SESSION_SUMMARY_READ_BYTES = 256 * 1024;
95
+ const SESSION_CONTENT_READ_BYTES = SESSION_SUMMARY_READ_BYTES;
96
+ const DEFAULT_CONTENT_SCAN_LIMIT = 50;
91
97
  const SESSION_SCAN_FACTOR = 4;
92
98
  const SESSION_SCAN_MIN_FILES = 800;
93
99
  const MAX_SESSION_PATH_LIST_SIZE = 2000;
@@ -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;
@@ -1203,6 +1560,21 @@ function applyServiceTierToTemplate(template, serviceTier) {
1203
1560
  return `service_tier = "fast"\n${content}`;
1204
1561
  }
1205
1562
 
1563
+ function applyReasoningEffortToTemplate(template, reasoningEffort) {
1564
+ let content = typeof template === 'string' ? template : '';
1565
+ const effort = typeof reasoningEffort === 'string' ? reasoningEffort.trim().toLowerCase() : '';
1566
+ if (!effort) {
1567
+ return content;
1568
+ }
1569
+
1570
+ content = content.replace(/^\s*model_reasoning_effort\s*=\s*["'][^"']*["']\s*\n?/gmi, '');
1571
+ if (effort === 'high' || effort === 'xhigh') {
1572
+ content = content.replace(/^\s*\n*/, '');
1573
+ return `model_reasoning_effort = "${effort}"\n${content}`;
1574
+ }
1575
+ return content;
1576
+ }
1577
+
1206
1578
  function getConfigTemplate(params = {}) {
1207
1579
  let content = EMPTY_CONFIG_FALLBACK_TEMPLATE;
1208
1580
  if (fs.existsSync(CONFIG_FILE)) {
@@ -1219,6 +1591,9 @@ function getConfigTemplate(params = {}) {
1219
1591
  if (typeof params.serviceTier === 'string') {
1220
1592
  template = applyServiceTierToTemplate(template, params.serviceTier);
1221
1593
  }
1594
+ if (typeof params.reasoningEffort === 'string') {
1595
+ template = applyReasoningEffortToTemplate(template, params.reasoningEffort);
1596
+ }
1222
1597
  return {
1223
1598
  template
1224
1599
  };
@@ -1273,6 +1648,248 @@ function applyConfigTemplate(params = {}) {
1273
1648
  return { success: true };
1274
1649
  }
1275
1650
 
1651
+ function addProviderToConfig(params = {}) {
1652
+ const name = typeof params.name === 'string' ? params.name.trim() : '';
1653
+ const url = typeof params.url === 'string' ? params.url.trim() : '';
1654
+ const key = typeof params.key === 'string' ? params.key.trim() : '';
1655
+ const allowManaged = !!params.allowManaged;
1656
+
1657
+ if (!name) return { error: '名称不能为空' };
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
+ }
1665
+
1666
+ ensureConfigDir();
1667
+
1668
+ let content = '';
1669
+ if (fs.existsSync(CONFIG_FILE)) {
1670
+ try {
1671
+ content = fs.readFileSync(CONFIG_FILE, 'utf-8');
1672
+ } catch (e) {
1673
+ return { error: `读取 config.toml 失败: ${e.message}` };
1674
+ }
1675
+ } else {
1676
+ content = EMPTY_CONFIG_FALLBACK_TEMPLATE;
1677
+ }
1678
+
1679
+ if (!content || !content.trim()) {
1680
+ content = EMPTY_CONFIG_FALLBACK_TEMPLATE;
1681
+ }
1682
+
1683
+ let parsed;
1684
+ try {
1685
+ parsed = toml.parse(content);
1686
+ } catch (e) {
1687
+ return { error: `config.toml 解析失败: ${e.message}` };
1688
+ }
1689
+
1690
+ if (!parsed.model_providers || typeof parsed.model_providers !== 'object') {
1691
+ parsed.model_providers = {};
1692
+ }
1693
+
1694
+ if (parsed.model_providers[name]) {
1695
+ return { error: '提供商已存在' };
1696
+ }
1697
+
1698
+ const escapeTomlString = (value) => String(value || '')
1699
+ .replace(/\\/g, '\\\\')
1700
+ .replace(/"/g, '\\"');
1701
+
1702
+ const lineEnding = content.includes('\r\n') ? '\r\n' : '\n';
1703
+ const safeName = escapeTomlString(name);
1704
+ const safeUrl = escapeTomlString(url);
1705
+ const safeKey = escapeTomlString(key);
1706
+ const block = [
1707
+ `[model_providers.${safeName}]`,
1708
+ `name = "${safeName}"`,
1709
+ `base_url = "${safeUrl}"`,
1710
+ `wire_api = "responses"`,
1711
+ `requires_openai_auth = false`,
1712
+ `preferred_auth_method = "${safeKey}"`,
1713
+ `request_max_retries = 4`,
1714
+ `stream_max_retries = 10`,
1715
+ `stream_idle_timeout_ms = 300000`
1716
+ ].join(lineEnding);
1717
+
1718
+ const newContent = content.trimEnd() + lineEnding + lineEnding + block + lineEnding;
1719
+
1720
+ try {
1721
+ writeConfig(newContent);
1722
+ } catch (e) {
1723
+ return { error: `写入配置失败: ${e.message}` };
1724
+ }
1725
+
1726
+ return { success: true };
1727
+ }
1728
+
1729
+ function updateProviderInConfig(params = {}) {
1730
+ const name = typeof params.name === 'string' ? params.name.trim() : '';
1731
+ const url = typeof params.url === 'string' ? params.url.trim() : '';
1732
+ const key = params.key !== undefined && params.key !== null
1733
+ ? String(params.key).trim()
1734
+ : undefined;
1735
+ const allowManaged = !!params.allowManaged;
1736
+
1737
+ if (!name) return { error: '名称不能为空' };
1738
+ if (!url && key === undefined) {
1739
+ return { error: 'URL 或密钥至少填写一项' };
1740
+ }
1741
+ if (isNonEditableProvider(name) && !allowManaged) {
1742
+ if (isDefaultLocalProvider(name)) {
1743
+ return { error: 'local provider 为系统保留项,不可编辑' };
1744
+ }
1745
+ return { error: '本地代理配置为系统内建项,不可编辑' };
1746
+ }
1747
+
1748
+ try {
1749
+ cmdUpdate(name, url || undefined, key, true, { allowManaged });
1750
+ return { success: true };
1751
+ } catch (e) {
1752
+ return { error: e.message || '更新失败' };
1753
+ }
1754
+ }
1755
+
1756
+ function deleteProviderFromConfig(params = {}) {
1757
+ const name = typeof params.name === 'string' ? params.name.trim() : '';
1758
+ if (!name) return { error: '名称不能为空' };
1759
+ if (isNonDeletableProvider(name)) {
1760
+ if (isDefaultLocalProvider(name)) {
1761
+ return { error: 'local provider 为系统保留项,不可删除' };
1762
+ }
1763
+ return { error: '本地代理配置为系统内建项,不可删除' };
1764
+ }
1765
+ if (!fs.existsSync(CONFIG_FILE)) {
1766
+ return { error: 'config.toml 不存在' };
1767
+ }
1768
+
1769
+ let config;
1770
+ try {
1771
+ config = readConfig();
1772
+ } catch (e) {
1773
+ return { error: `读取配置失败: ${e.message}` };
1774
+ }
1775
+
1776
+ const result = performProviderDeletion(name, { silent: true, config });
1777
+ if (result.error) {
1778
+ return { error: result.error };
1779
+ }
1780
+ return {
1781
+ success: true,
1782
+ switched: !!result.switched,
1783
+ provider: result.provider || '',
1784
+ model: result.model || ''
1785
+ };
1786
+ }
1787
+
1788
+ function performProviderDeletion(name, options = {}) {
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
+ }
1797
+ const config = options.config || readConfig();
1798
+ if (!config.model_providers || !config.model_providers[name]) {
1799
+ const msg = '提供商不存在';
1800
+ if (!silent) console.error('错误:', msg, name);
1801
+ return { error: msg };
1802
+ }
1803
+
1804
+ const content = fs.readFileSync(CONFIG_FILE, 'utf-8');
1805
+ const lineEnding = content.includes('\r\n') ? '\r\n' : '\n';
1806
+ const hasBom = content.charCodeAt(0) === 0xFEFF;
1807
+ const safeName = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1808
+ const sectionRegex = new RegExp(`\\[\\s*model_providers\\s*\\.\\s*(?:"${safeName}"|'${safeName}'|${safeName})\\s*\\]`);
1809
+
1810
+ const remainingProviders = Object.keys(config.model_providers || {}).filter(item => item !== name);
1811
+ if (remainingProviders.length === 0) {
1812
+ const msg = '删除后将没有可用提供商';
1813
+ if (!silent) console.error('错误:', msg);
1814
+ return { error: msg };
1815
+ }
1816
+
1817
+ const currentProvider = typeof config.model_provider === 'string' ? config.model_provider.trim() : '';
1818
+ const currentModels = readCurrentModels();
1819
+ const models = readModels();
1820
+ const result = { success: true, switched: false, provider: '', model: '' };
1821
+
1822
+ if (currentModels[name]) {
1823
+ delete currentModels[name];
1824
+ }
1825
+
1826
+ let fallbackProvider = currentProvider;
1827
+ let fallbackModel = typeof config.model === 'string' ? config.model.trim() : '';
1828
+ if (currentProvider === name) {
1829
+ fallbackProvider = remainingProviders[0];
1830
+ fallbackModel = currentModels[fallbackProvider]
1831
+ || (Array.isArray(models) && models.length > 0 ? models[0] : (DEFAULT_MODELS[0] || ''));
1832
+ result.switched = true;
1833
+ result.provider = fallbackProvider;
1834
+ result.model = fallbackModel;
1835
+ }
1836
+
1837
+ const upsertTopLevel = (text, key, value) => {
1838
+ if (!value && value !== '') return text;
1839
+ const regex = new RegExp(`^\\s*${key}\\s*=.*$`, 'm');
1840
+ if (regex.test(text)) {
1841
+ return text.replace(regex, `${key} = "${value}"`);
1842
+ }
1843
+ return `${key} = "${value}"${lineEnding}${text}`;
1844
+ };
1845
+
1846
+ let updatedContent = null;
1847
+ const match = content.match(sectionRegex);
1848
+ if (match) {
1849
+ const startIdx = match.index;
1850
+ const rest = content.slice(startIdx + match[0].length);
1851
+ const nextIdx = rest.indexOf('[');
1852
+ const endIdx = nextIdx === -1 ? content.length : (startIdx + match[0].length + nextIdx);
1853
+
1854
+ const removedContent = (content.slice(0, startIdx) + content.slice(endIdx))
1855
+ .replace(/\n{3,}/g, lineEnding + lineEnding);
1856
+
1857
+ updatedContent = removedContent;
1858
+ }
1859
+
1860
+ if (updatedContent) {
1861
+ if (result.switched) {
1862
+ updatedContent = upsertTopLevel(updatedContent, 'model_provider', fallbackProvider);
1863
+ updatedContent = upsertTopLevel(updatedContent, 'model', fallbackModel);
1864
+ currentModels[fallbackProvider] = fallbackModel;
1865
+ }
1866
+ } else {
1867
+ // 回退:重建 TOML,保持行尾风格
1868
+ const rebuilt = JSON.parse(JSON.stringify(config));
1869
+ delete rebuilt.model_providers[name];
1870
+ if (result.switched) {
1871
+ rebuilt.model_provider = fallbackProvider;
1872
+ rebuilt.model = fallbackModel;
1873
+ currentModels[fallbackProvider] = fallbackModel;
1874
+ }
1875
+ const hasMarker = content.includes(CODEXMATE_MANAGED_MARKER);
1876
+ let rebuiltToml = toml.stringify(rebuilt).trimEnd();
1877
+ rebuiltToml = rebuiltToml.replace(/\n/g, lineEnding);
1878
+ if (hasMarker && !rebuiltToml.includes(CODEXMATE_MANAGED_MARKER)) {
1879
+ rebuiltToml = `${CODEXMATE_MANAGED_MARKER}${lineEnding}${rebuiltToml}`;
1880
+ }
1881
+ updatedContent = rebuiltToml + lineEnding;
1882
+ if (hasBom && updatedContent.charCodeAt(0) !== 0xFEFF) {
1883
+ updatedContent = '\uFEFF' + updatedContent;
1884
+ }
1885
+ }
1886
+
1887
+ writeCurrentModels(currentModels);
1888
+ writeConfig(updatedContent.trimEnd() + lineEnding);
1889
+
1890
+ return result;
1891
+ }
1892
+
1276
1893
  function ensureSupportFiles(defaultProvider, defaultModel) {
1277
1894
  if (!fs.existsSync(MODELS_FILE)) {
1278
1895
  writeModels([...DEFAULT_MODELS]);
@@ -1385,6 +2002,30 @@ function ensureManagedConfigBootstrap() {
1385
2002
  return { notice: g_initNotice };
1386
2003
  }
1387
2004
 
2005
+ function resetConfigToDefault() {
2006
+ ensureConfigDir();
2007
+ const initializedAt = new Date().toISOString();
2008
+ const defaultProvider = 'openai';
2009
+ const defaultModel = DEFAULT_MODELS[0] || 'gpt-4';
2010
+
2011
+ let backupFile = '';
2012
+ if (fs.existsSync(CONFIG_FILE)) {
2013
+ backupFile = `config.toml.reset-${formatTimestampForFileName(initializedAt)}.bak`;
2014
+ fs.copyFileSync(CONFIG_FILE, path.join(CONFIG_DIR, backupFile));
2015
+ }
2016
+
2017
+ writeConfig(buildDefaultConfigContent(initializedAt));
2018
+ ensureSupportFiles(defaultProvider, defaultModel);
2019
+ writeInitMark({
2020
+ version: 1,
2021
+ initializedAt,
2022
+ mode: 'manual-reset',
2023
+ backupFile
2024
+ });
2025
+
2026
+ return { success: true, backupFile };
2027
+ }
2028
+
1388
2029
  function consumeInitNotice() {
1389
2030
  const notice = g_initNotice;
1390
2031
  g_initNotice = '';
@@ -1530,17 +2171,17 @@ function isBootstrapLikeText(text) {
1530
2171
  return BOOTSTRAP_TEXT_MARKERS.some(marker => normalized.includes(marker));
1531
2172
  }
1532
2173
 
1533
- function removeLeadingSystemMessage(messages) {
1534
- if (!Array.isArray(messages) || messages.length === 0) {
1535
- return [];
1536
- }
1537
-
1538
- let startIndex = 0;
1539
- while (startIndex < messages.length) {
1540
- const item = messages[startIndex];
1541
- const role = item ? normalizeRole(item.role) : '';
1542
- const text = item && typeof item.text === 'string' ? item.text : '';
1543
- const isSystemRole = role === 'system';
2174
+ function removeLeadingSystemMessage(messages) {
2175
+ if (!Array.isArray(messages) || messages.length === 0) {
2176
+ return [];
2177
+ }
2178
+
2179
+ let startIndex = 0;
2180
+ while (startIndex < messages.length) {
2181
+ const item = messages[startIndex];
2182
+ const role = item ? normalizeRole(item.role) : '';
2183
+ const text = item && typeof item.text === 'string' ? item.text : '';
2184
+ const isSystemRole = role === 'system';
1544
2185
  const isBootstrapText = isBootstrapLikeText(text);
1545
2186
  if (!item || isSystemRole || isBootstrapText) {
1546
2187
  startIndex += 1;
@@ -1627,102 +2268,119 @@ function matchesSessionPathFilter(session, normalizedFilter) {
1627
2268
  return cwd.includes(normalizedFilter);
1628
2269
  }
1629
2270
 
1630
- function normalizeQueryTokens(query) {
1631
- if (typeof query !== 'string') {
1632
- return [];
1633
- }
1634
- return query
1635
- .split(/\s+/)
1636
- .map(item => item.trim())
1637
- .map(item => item.toLowerCase())
1638
- .filter(Boolean);
1639
- }
1640
-
1641
- function expandSessionQueryTokens(tokens) {
1642
- const base = Array.isArray(tokens) ? tokens.map(t => String(t || '').toLowerCase()).filter(Boolean) : [];
1643
- const result = [];
1644
- const seen = new Set();
1645
- let hasClaudeAlias = false;
1646
- let hasDaudeAlias = false;
1647
-
1648
- for (const token of base) {
1649
- if (/^claude[-_ ]?code$/.test(token) || token === 'claudecode') {
1650
- hasClaudeAlias = true;
1651
- continue;
1652
- }
1653
- if (/^daude[-_ ]?code$/.test(token) || token === 'daudecode') {
1654
- hasDaudeAlias = true;
1655
- continue;
1656
- }
1657
- if (!seen.has(token)) {
1658
- seen.add(token);
1659
- result.push(token);
1660
- }
1661
- }
1662
-
1663
- const push = (token) => {
1664
- const normalized = String(token || '').toLowerCase();
1665
- if (!normalized || seen.has(normalized)) return;
1666
- seen.add(normalized);
1667
- result.push(normalized);
1668
- };
1669
-
1670
- if (hasClaudeAlias) {
1671
- push('claude');
1672
- push('code');
1673
- }
1674
- if (hasDaudeAlias) {
1675
- push('daude');
1676
- push('code');
1677
- }
1678
-
1679
- return result;
1680
- }
1681
-
1682
- function normalizeKeywords(value) {
1683
- if (!Array.isArray(value)) {
1684
- return [];
1685
- }
1686
- const seen = new Set();
1687
- const result = [];
1688
- for (const item of value) {
1689
- const normalized = typeof item === 'string' ? item.trim() : String(item || '').trim();
1690
- if (!normalized) continue;
1691
- const lower = normalized.toLowerCase();
1692
- if (seen.has(lower)) continue;
1693
- seen.add(lower);
1694
- result.push(normalized);
1695
- }
1696
- return result;
1697
- }
1698
-
1699
- function normalizeCapabilities(value) {
1700
- const result = {};
1701
- if (!value || typeof value !== 'object') {
1702
- return result;
1703
- }
1704
- if (value.code === true) {
1705
- result.code = true;
1706
- }
1707
- return result;
1708
- }
1709
-
1710
- function normalizeQueryMode(mode) {
1711
- return mode === 'or' ? 'or' : 'and';
1712
- }
1713
-
1714
- function normalizeQueryScope(scope) {
1715
- if (scope === 'content' || scope === 'all' || scope === 'summary') {
1716
- return scope;
2271
+ function normalizeQueryTokens(query) {
2272
+ if (typeof query !== 'string') {
2273
+ return [];
1717
2274
  }
1718
- return 'summary';
2275
+ return query
2276
+ .split(/\s+/)
2277
+ .map(item => item.trim())
2278
+ .map(item => item.toLowerCase())
2279
+ .filter(Boolean);
1719
2280
  }
1720
2281
 
1721
- function normalizeRoleFilter(roleFilter) {
1722
- if (roleFilter === 'all' || roleFilter === undefined || roleFilter === null) {
1723
- return 'all';
1724
- }
1725
- const normalized = normalizeRole(String(roleFilter));
2282
+ function expandSessionQueryTokens(tokens) {
2283
+ const base = Array.isArray(tokens) ? tokens.map(t => String(t || '').toLowerCase()).filter(Boolean) : [];
2284
+ const result = [];
2285
+ const seen = new Set();
2286
+ let hasClaudeAlias = false;
2287
+ let hasDaudeAlias = false;
2288
+
2289
+ // First pass: detect multi-token aliases (e.g., "claude code", "daude code")
2290
+ for (let i = 0; i < base.length; i++) {
2291
+ const token = base[i];
2292
+ const nextToken = base[i + 1] || '';
2293
+
2294
+ // Check for "claude code" pattern (two separate tokens)
2295
+ if (token === 'claude' && nextToken === 'code') {
2296
+ hasClaudeAlias = true;
2297
+ i++; // Skip next token
2298
+ continue;
2299
+ }
2300
+ // Check for "daude code" pattern (two separate tokens)
2301
+ if (token === 'daude' && nextToken === 'code') {
2302
+ hasDaudeAlias = true;
2303
+ i++; // Skip next token
2304
+ continue;
2305
+ }
2306
+ // Check for combined patterns (e.g., "claude-code", "claude_code", "claudecode")
2307
+ if (/^claude[-_ ]?code$/.test(token) || token === 'claudecode') {
2308
+ hasClaudeAlias = true;
2309
+ continue;
2310
+ }
2311
+ if (/^daude[-_ ]?code$/.test(token) || token === 'daudecode') {
2312
+ hasDaudeAlias = true;
2313
+ continue;
2314
+ }
2315
+ if (!seen.has(token)) {
2316
+ seen.add(token);
2317
+ result.push(token);
2318
+ }
2319
+ }
2320
+
2321
+ const push = (token) => {
2322
+ const normalized = String(token || '').toLowerCase();
2323
+ if (!normalized || seen.has(normalized)) return;
2324
+ seen.add(normalized);
2325
+ result.push(normalized);
2326
+ };
2327
+
2328
+ if (hasClaudeAlias) {
2329
+ push('claude');
2330
+ push('code');
2331
+ }
2332
+ if (hasDaudeAlias) {
2333
+ push('daude');
2334
+ push('code');
2335
+ }
2336
+
2337
+ return result;
2338
+ }
2339
+
2340
+ function normalizeKeywords(value) {
2341
+ if (!Array.isArray(value)) {
2342
+ return [];
2343
+ }
2344
+ const seen = new Set();
2345
+ const result = [];
2346
+ for (const item of value) {
2347
+ const normalized = typeof item === 'string' ? item.trim() : String(item || '').trim();
2348
+ if (!normalized) continue;
2349
+ const lower = normalized.toLowerCase();
2350
+ if (seen.has(lower)) continue;
2351
+ seen.add(lower);
2352
+ result.push(normalized);
2353
+ }
2354
+ return result;
2355
+ }
2356
+
2357
+ function normalizeCapabilities(value) {
2358
+ const result = {};
2359
+ if (!value || typeof value !== 'object') {
2360
+ return result;
2361
+ }
2362
+ if (value.code === true) {
2363
+ result.code = true;
2364
+ }
2365
+ return result;
2366
+ }
2367
+
2368
+ function normalizeQueryMode(mode) {
2369
+ return mode === 'or' ? 'or' : 'and';
2370
+ }
2371
+
2372
+ function normalizeQueryScope(scope) {
2373
+ if (scope === 'content' || scope === 'all' || scope === 'summary') {
2374
+ return scope;
2375
+ }
2376
+ return 'summary';
2377
+ }
2378
+
2379
+ function normalizeRoleFilter(roleFilter) {
2380
+ if (roleFilter === 'all' || roleFilter === undefined || roleFilter === null) {
2381
+ return 'all';
2382
+ }
2383
+ const normalized = normalizeRole(String(roleFilter));
1726
2384
  return normalized || 'all';
1727
2385
  }
1728
2386
 
@@ -1740,22 +2398,22 @@ function matchTokensInText(text, tokens, mode = 'and') {
1740
2398
  return tokens.every(token => haystack.includes(token));
1741
2399
  }
1742
2400
 
1743
- function buildSessionSummaryText(session) {
1744
- if (!session) {
1745
- return '';
1746
- }
1747
- const keywords = Array.isArray(session.keywords) ? session.keywords.join(' ') : '';
1748
- const provider = typeof session.provider === 'string' ? session.provider : '';
1749
- return [
1750
- session.title,
1751
- session.sessionId,
1752
- session.cwd,
1753
- session.filePath,
1754
- session.sourceLabel,
1755
- provider,
1756
- keywords
1757
- ].filter(Boolean).join(' ');
1758
- }
2401
+ function buildSessionSummaryText(session) {
2402
+ if (!session) {
2403
+ return '';
2404
+ }
2405
+ const keywords = Array.isArray(session.keywords) ? session.keywords.join(' ') : '';
2406
+ const provider = typeof session.provider === 'string' ? session.provider : '';
2407
+ return [
2408
+ session.title,
2409
+ session.sessionId,
2410
+ session.cwd,
2411
+ session.filePath,
2412
+ session.sourceLabel,
2413
+ provider,
2414
+ keywords
2415
+ ].filter(Boolean).join(' ');
2416
+ }
1759
2417
 
1760
2418
  function extractMessageFromRecord(record, source) {
1761
2419
  if (!record) {
@@ -1865,39 +2523,39 @@ function applySessionQueryFilter(sessions, options = {}) {
1865
2523
  ? Math.max(1024, Number(options.contentScanBytes))
1866
2524
  : SESSION_CONTENT_READ_BYTES;
1867
2525
 
1868
- let scanned = 0;
1869
- const results = [];
1870
-
1871
- for (const session of sessions) {
1872
- if (scope === 'content' && scanned >= contentScanLimit) {
2526
+ let scanned = 0;
2527
+ const results = [];
2528
+
2529
+ for (const session of sessions) {
2530
+ if (scope === 'content' && scanned >= contentScanLimit) {
1873
2531
  break;
1874
2532
  }
1875
-
1876
- const summaryText = buildSessionSummaryText(session);
1877
- const summaryHit = scope !== 'content' && matchTokensInText(summaryText, tokens, mode);
1878
- let contentHit = false;
1879
- let contentInfo = null;
1880
-
1881
- const shouldScanContent = scope === 'content' || scope === 'all' || !summaryHit;
1882
- if (shouldScanContent && scanned < contentScanLimit) {
1883
- scanned += 1;
1884
- contentInfo = scanSessionContentForQuery(session, tokens, {
1885
- mode,
1886
- roleFilter,
1887
- maxBytes: contentScanBytes,
1888
- maxMatches: 1,
1889
- snippetLimit: 2
1890
- });
1891
- contentHit = contentInfo.hit;
1892
- }
1893
-
1894
- const hit = scope === 'summary'
1895
- ? summaryHit
1896
- : (scope === 'content' ? contentHit : (summaryHit || contentHit));
1897
-
1898
- if (!hit) {
1899
- continue;
1900
- }
2533
+
2534
+ const summaryText = buildSessionSummaryText(session);
2535
+ const summaryHit = scope !== 'content' && matchTokensInText(summaryText, tokens, mode);
2536
+ let contentHit = false;
2537
+ let contentInfo = null;
2538
+
2539
+ const shouldScanContent = scope === 'content' || scope === 'all' || !summaryHit;
2540
+ if (shouldScanContent && scanned < contentScanLimit) {
2541
+ scanned += 1;
2542
+ contentInfo = scanSessionContentForQuery(session, tokens, {
2543
+ mode,
2544
+ roleFilter,
2545
+ maxBytes: contentScanBytes,
2546
+ maxMatches: 1,
2547
+ snippetLimit: 2
2548
+ });
2549
+ contentHit = contentInfo.hit;
2550
+ }
2551
+
2552
+ const hit = scope === 'summary'
2553
+ ? summaryHit
2554
+ : (scope === 'content' ? contentHit : (summaryHit || contentHit));
2555
+
2556
+ if (!hit) {
2557
+ continue;
2558
+ }
1901
2559
 
1902
2560
  const matchInfo = contentInfo && contentInfo.hit
1903
2561
  ? contentInfo
@@ -2072,26 +2730,26 @@ function parseCodexSessionSummary(filePath) {
2072
2730
  }
2073
2731
  }
2074
2732
 
2075
- messageCount = Math.max(0, messageCount);
2076
-
2077
- return {
2078
- source: 'codex',
2079
- sourceLabel: 'Codex',
2080
- provider: 'codex',
2081
- sessionId,
2082
- title: firstPrompt || sessionId,
2083
- cwd,
2084
- createdAt,
2085
- updatedAt,
2086
- messageCount,
2087
- filePath,
2088
- keywords: [],
2089
- capabilities: {}
2090
- };
2091
- }
2092
-
2093
- function parseClaudeSessionSummary(filePath) {
2094
- const records = parseJsonlHeadRecords(filePath);
2733
+ messageCount = Math.max(0, messageCount);
2734
+
2735
+ return {
2736
+ source: 'codex',
2737
+ sourceLabel: 'Codex',
2738
+ provider: 'codex',
2739
+ sessionId,
2740
+ title: firstPrompt || sessionId,
2741
+ cwd,
2742
+ createdAt,
2743
+ updatedAt,
2744
+ messageCount,
2745
+ filePath,
2746
+ keywords: [],
2747
+ capabilities: {}
2748
+ };
2749
+ }
2750
+
2751
+ function parseClaudeSessionSummary(filePath) {
2752
+ const records = parseJsonlHeadRecords(filePath);
2095
2753
  if (records.length === 0) {
2096
2754
  return null;
2097
2755
  }
@@ -2161,23 +2819,23 @@ function parseClaudeSessionSummary(filePath) {
2161
2819
  }
2162
2820
  }
2163
2821
 
2164
- messageCount = Math.max(0, messageCount);
2165
-
2166
- return {
2167
- source: 'claude',
2168
- sourceLabel: 'Claude Code',
2169
- provider: 'claude',
2170
- sessionId,
2171
- title: firstPrompt || sessionId,
2172
- cwd,
2173
- createdAt,
2174
- updatedAt,
2175
- messageCount,
2176
- filePath,
2177
- keywords: [],
2178
- capabilities: { code: true }
2179
- };
2180
- }
2822
+ messageCount = Math.max(0, messageCount);
2823
+
2824
+ return {
2825
+ source: 'claude',
2826
+ sourceLabel: 'Claude Code',
2827
+ provider: 'claude',
2828
+ sessionId,
2829
+ title: firstPrompt || sessionId,
2830
+ cwd,
2831
+ createdAt,
2832
+ updatedAt,
2833
+ messageCount,
2834
+ filePath,
2835
+ keywords: [],
2836
+ capabilities: { code: true }
2837
+ };
2838
+ }
2181
2839
 
2182
2840
  function listCodexSessions(limit, options = {}) {
2183
2841
  const codexSessionsDir = getCodexSessionsDir();
@@ -2278,12 +2936,12 @@ function listClaudeSessions(limit, options = {}) {
2278
2936
  let title = truncateText(entry.summary || entry.firstPrompt || sessionId, 120);
2279
2937
  let messageCount = Number.isFinite(entry.messageCount) ? Math.max(0, entry.messageCount - 1) : 0;
2280
2938
 
2281
- const quickRecords = parseJsonlHeadRecords(filePath, SESSION_TITLE_READ_BYTES);
2282
- if (quickRecords.length > 0) {
2283
- const filteredCount = countConversationMessagesInRecords(quickRecords, 'claude');
2284
- if (filteredCount > 0 || messageCount === 0) {
2285
- messageCount = filteredCount;
2286
- }
2939
+ const quickRecords = parseJsonlHeadRecords(filePath, SESSION_TITLE_READ_BYTES);
2940
+ if (quickRecords.length > 0) {
2941
+ const filteredCount = countConversationMessagesInRecords(quickRecords, 'claude');
2942
+ if (filteredCount > 0 || messageCount === 0) {
2943
+ messageCount = filteredCount;
2944
+ }
2287
2945
 
2288
2946
  const quickMessages = [];
2289
2947
  for (const record of quickRecords) {
@@ -2292,38 +2950,38 @@ function listClaudeSessions(limit, options = {}) {
2292
2950
  const content = record.message ? record.message.content : '';
2293
2951
  quickMessages.push({ role, text: extractMessageText(content) });
2294
2952
  }
2295
- }
2296
- const filteredQuickMessages = removeLeadingSystemMessage(quickMessages);
2297
- const firstUser = filteredQuickMessages.find(item => item.role === 'user' && item.text);
2298
- if (firstUser) {
2299
- title = truncateText(firstUser.text, 120);
2300
- }
2301
- }
2302
-
2303
- const provider = typeof entry.provider === 'string' && entry.provider.trim()
2304
- ? entry.provider.trim()
2305
- : 'claude';
2306
- const keywords = normalizeKeywords(entry.keywords);
2307
- const capabilities = normalizeCapabilities(entry.capabilities);
2308
-
2309
- sessions.push({
2310
- source: 'claude',
2311
- sourceLabel: 'Claude Code',
2312
- provider,
2313
- sessionId,
2314
- title,
2315
- cwd: entry.projectPath || index.originalPath || '',
2316
- createdAt,
2317
- updatedAt,
2318
- messageCount,
2319
- filePath,
2320
- keywords,
2321
- capabilities
2322
- });
2323
-
2324
- if (sessions.length >= targetCount) {
2325
- break;
2326
- }
2953
+ }
2954
+ const filteredQuickMessages = removeLeadingSystemMessage(quickMessages);
2955
+ const firstUser = filteredQuickMessages.find(item => item.role === 'user' && item.text);
2956
+ if (firstUser) {
2957
+ title = truncateText(firstUser.text, 120);
2958
+ }
2959
+ }
2960
+
2961
+ const provider = typeof entry.provider === 'string' && entry.provider.trim()
2962
+ ? entry.provider.trim()
2963
+ : 'claude';
2964
+ const keywords = normalizeKeywords(entry.keywords);
2965
+ const capabilities = normalizeCapabilities(entry.capabilities);
2966
+
2967
+ sessions.push({
2968
+ source: 'claude',
2969
+ sourceLabel: 'Claude Code',
2970
+ provider,
2971
+ sessionId,
2972
+ title,
2973
+ cwd: entry.projectPath || index.originalPath || '',
2974
+ createdAt,
2975
+ updatedAt,
2976
+ messageCount,
2977
+ filePath,
2978
+ keywords,
2979
+ capabilities
2980
+ });
2981
+
2982
+ if (sessions.length >= targetCount) {
2983
+ break;
2984
+ }
2327
2985
  }
2328
2986
 
2329
2987
  if (sessions.length >= targetCount) {
@@ -2356,15 +3014,15 @@ function listAllSessions(params = {}) {
2356
3014
  const source = params.source === 'codex' || params.source === 'claude'
2357
3015
  ? params.source
2358
3016
  : 'all';
2359
- const rawLimit = Number(params.limit);
2360
- const limit = Number.isFinite(rawLimit)
2361
- ? Math.max(1, Math.min(rawLimit, MAX_SESSION_LIST_SIZE))
2362
- : 120;
2363
- const forceRefresh = !!params.forceRefresh;
2364
- const normalizedPathFilter = normalizeSessionPathFilter(params.pathFilter);
2365
- const hasPathFilter = !!normalizedPathFilter;
2366
- const queryTokens = expandSessionQueryTokens(normalizeQueryTokens(params.query));
2367
- const hasQuery = queryTokens.length > 0;
3017
+ const rawLimit = Number(params.limit);
3018
+ const limit = Number.isFinite(rawLimit)
3019
+ ? Math.max(1, Math.min(rawLimit, MAX_SESSION_LIST_SIZE))
3020
+ : 120;
3021
+ const forceRefresh = !!params.forceRefresh;
3022
+ const normalizedPathFilter = normalizeSessionPathFilter(params.pathFilter);
3023
+ const hasPathFilter = !!normalizedPathFilter;
3024
+ const queryTokens = expandSessionQueryTokens(normalizeQueryTokens(params.query));
3025
+ const hasQuery = queryTokens.length > 0;
2368
3026
  const cacheKey = hasQuery ? '' : `${source}:${limit}:${normalizedPathFilter}`;
2369
3027
  if (!hasQuery) {
2370
3028
  const cached = getSessionListCache(cacheKey, forceRefresh);
@@ -2381,16 +3039,16 @@ function listAllSessions(params = {}) {
2381
3039
  : {};
2382
3040
 
2383
3041
  let sessions = [];
2384
- if (source === 'all' || source === 'codex') {
2385
- sessions = sessions.concat(listCodexSessions(limit, scanOptions));
2386
- }
2387
- if (source === 'all' || source === 'claude') {
2388
- sessions = sessions.concat(listClaudeSessions(limit, scanOptions));
2389
- }
2390
-
2391
- if (hasPathFilter) {
2392
- sessions = sessions.filter(item => matchesSessionPathFilter(item, normalizedPathFilter));
2393
- }
3042
+ if (source === 'all' || source === 'codex') {
3043
+ sessions = sessions.concat(listCodexSessions(limit, scanOptions));
3044
+ }
3045
+ if (source === 'all' || source === 'claude') {
3046
+ sessions = sessions.concat(listClaudeSessions(limit, scanOptions));
3047
+ }
3048
+
3049
+ if (hasPathFilter) {
3050
+ sessions = sessions.filter(item => matchesSessionPathFilter(item, normalizedPathFilter));
3051
+ }
2394
3052
 
2395
3053
  let result = sessions;
2396
3054
  if (hasQuery) {
@@ -2411,15 +3069,17 @@ function listAllSessions(params = {}) {
2411
3069
  }
2412
3070
 
2413
3071
  function listSessionPaths(params = {}) {
2414
- const source = params.source === 'codex' || params.source === 'claude'
2415
- ? params.source
2416
- : 'all';
3072
+ const source = typeof params.source === 'string' ? params.source.trim().toLowerCase() : '';
3073
+ if (source && source !== 'codex' && source !== 'claude' && source !== 'all') {
3074
+ return [];
3075
+ }
3076
+ const validSource = source === 'codex' || source === 'claude' ? source : 'all';
2417
3077
  const rawLimit = Number(params.limit);
2418
3078
  const limit = Number.isFinite(rawLimit)
2419
3079
  ? Math.max(1, Math.min(rawLimit, MAX_SESSION_PATH_LIST_SIZE))
2420
3080
  : 500;
2421
3081
  const forceRefresh = !!params.forceRefresh;
2422
- const cacheKey = `paths:${source}:${limit}`;
3082
+ const cacheKey = `paths:${validSource}:${limit}`;
2423
3083
  const cached = getSessionListCache(cacheKey, forceRefresh);
2424
3084
  if (cached) {
2425
3085
  return cached;
@@ -2433,10 +3093,10 @@ function listSessionPaths(params = {}) {
2433
3093
  };
2434
3094
 
2435
3095
  let sessions = [];
2436
- if (source === 'all' || source === 'codex') {
3096
+ if (validSource === 'all' || validSource === 'codex') {
2437
3097
  sessions = sessions.concat(listCodexSessions(gatherLimit, scanOptions));
2438
3098
  }
2439
- if (source === 'all' || source === 'claude') {
3099
+ if (validSource === 'all' || validSource === 'claude') {
2440
3100
  sessions = sessions.concat(listClaudeSessions(gatherLimit, scanOptions));
2441
3101
  }
2442
3102
 
@@ -2477,15 +3137,15 @@ function resolveSessionFilePath(source, filePath, sessionId) {
2477
3137
  }
2478
3138
  }
2479
3139
 
2480
- if (typeof sessionId === 'string' && sessionId.trim()) {
2481
- const targetId = sessionId.trim().toLowerCase();
2482
- const files = collectJsonlFiles(root, 5000);
2483
- const matchedFile = files.find(item => path.basename(item, '.jsonl').toLowerCase() === targetId);
2484
- if (matchedFile && fs.existsSync(matchedFile)) {
2485
- return matchedFile;
2486
- }
2487
- }
2488
-
3140
+ if (typeof sessionId === 'string' && sessionId.trim()) {
3141
+ const targetId = sessionId.trim().toLowerCase();
3142
+ const files = collectJsonlFiles(root, 5000);
3143
+ const matchedFile = files.find(item => path.basename(item, '.jsonl').toLowerCase() === targetId);
3144
+ if (matchedFile && fs.existsSync(matchedFile)) {
3145
+ return matchedFile;
3146
+ }
3147
+ }
3148
+
2489
3149
  return '';
2490
3150
  }
2491
3151
 
@@ -2513,155 +3173,673 @@ function findClaudeSessionIndexPath(sessionFilePath) {
2513
3173
  return '';
2514
3174
  }
2515
3175
 
2516
- function updateClaudeSessionIndex(indexPath, sessionFilePath, sessionId) {
2517
- if (!indexPath || !fs.existsSync(indexPath)) {
2518
- return;
2519
- }
2520
- const index = readJsonFile(indexPath, null);
2521
- if (!index || !Array.isArray(index.entries)) {
2522
- 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;
2523
3194
  }
2524
- const resolvedFile = sessionFilePath ? path.resolve(sessionFilePath) : '';
2525
- const resolvedLower = resolvedFile ? resolvedFile.toLowerCase() : '';
2526
- const filtered = index.entries.filter((entry) => {
2527
- if (!entry || typeof entry !== 'object') {
2528
- return false;
2529
- }
2530
- const entrySessionId = typeof entry.sessionId === 'string' ? entry.sessionId : '';
2531
- if (sessionId && entrySessionId === sessionId) {
2532
- 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;
2533
3200
  }
2534
- if (entry.fullPath) {
2535
- const expanded = expandHomePath(entry.fullPath);
2536
- const entryPath = expanded ? path.resolve(expanded) : '';
2537
- if (entryPath && resolvedLower && entryPath.toLowerCase() === resolvedLower) {
2538
- return false;
2539
- }
3201
+ // eslint-disable-next-line no-await-in-loop
3202
+ const ok = await canListenPort(host, candidate);
3203
+ if (ok) {
3204
+ return candidate;
2540
3205
  }
2541
- return true;
2542
- });
2543
- if (filtered.length === index.entries.length) {
2544
- return;
2545
3206
  }
2546
- index.entries = filtered;
2547
- try {
2548
- fs.writeFileSync(indexPath, JSON.stringify(index, null, 2), 'utf-8');
2549
- } catch (e) {}
3207
+ return 0;
2550
3208
  }
2551
3209
 
2552
- async function deleteSessionData(params = {}) {
2553
- const source = params.source === 'claude' ? 'claude' : (params.source === 'codex' ? 'codex' : '');
2554
- if (!source) {
2555
- return { error: 'Invalid source' };
2556
- }
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';
2557
3221
 
2558
- const filePath = resolveSessionFilePath(source, params.filePath, params.sessionId);
2559
- if (!filePath) {
2560
- return { error: 'Session file not found' };
2561
- }
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
+ }
2562
3231
 
2563
- const sessionId = params.sessionId || path.basename(filePath, '.jsonl');
2564
- try {
2565
- fs.unlinkSync(filePath);
2566
- } catch (e) {
2567
- 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;
3244
+ }
3245
+ const preferred = typeof preferredProvider === 'string' ? preferredProvider.trim() : '';
3246
+ if (preferred && preferred !== BUILTIN_PROXY_PROVIDER_NAME && providerMap[preferred]) {
3247
+ return preferred;
2568
3248
  }
3249
+ return providerNames[0] || '';
3250
+ }
2569
3251
 
2570
- if (source === 'claude') {
2571
- const indexPath = findClaudeSessionIndexPath(filePath);
2572
- if (indexPath) {
2573
- updateClaudeSessionIndex(indexPath, filePath, sessionId);
2574
- }
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)' };
2575
3264
  }
2576
3265
 
2577
- 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
+ }
2578
3279
 
2579
3280
  return {
2580
3281
  success: true,
2581
- source,
2582
- sessionId,
2583
- filePath
3282
+ settings: normalized
2584
3283
  };
2585
3284
  }
2586
3285
 
2587
- function generateCloneSessionId() {
2588
- if (crypto.randomUUID) {
2589
- return `clone-${crypto.randomUUID()}`;
2590
- }
2591
- const timePart = Date.now().toString(36);
2592
- const randomPart = crypto.randomBytes(8).toString('hex');
2593
- 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}`;
2594
3289
  }
2595
3290
 
2596
- function allocateCloneSessionTarget(dirPath) {
2597
- for (let attempt = 0; attempt < 6; attempt += 1) {
2598
- const sessionId = generateCloneSessionId();
2599
- const filePath = path.join(dirPath, `${sessionId}.jsonl`);
2600
- if (!fs.existsSync(filePath)) {
2601
- return { sessionId, filePath };
2602
- }
3291
+ function hasCodexConfigReadyForProxy() {
3292
+ const result = readConfigOrVirtualDefault();
3293
+ if (!result || result.isVirtual) {
3294
+ return false;
2603
3295
  }
2604
- const fallbackId = `clone-${Date.now().toString(36)}-${crypto.randomBytes(8).toString('hex')}`;
2605
- 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;
2606
3303
  }
2607
3304
 
2608
- function parseTimestampMs(value) {
2609
- if (value === undefined || value === null || value === '') {
2610
- return null;
2611
- }
2612
- if (typeof value === 'number' && Number.isFinite(value)) {
2613
- if (value > 1e12) return value;
2614
- if (value > 1e9) return value * 1000;
2615
- return value;
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' };
2616
3312
  }
2617
- if (typeof value === 'string') {
2618
- const parsed = Date.parse(value);
2619
- if (Number.isFinite(parsed)) {
2620
- return parsed;
2621
- }
2622
- const numeric = Number(value);
2623
- if (Number.isFinite(numeric)) {
2624
- if (numeric > 1e12) return numeric;
2625
- if (numeric > 1e9) return numeric * 1000;
2626
- return numeric;
2627
- }
3313
+ if (providerName === BUILTIN_PROXY_PROVIDER_NAME) {
3314
+ return { error: `上游 provider 不能是 ${BUILTIN_PROXY_PROVIDER_NAME}` };
2628
3315
  }
2629
- return null;
2630
- }
2631
-
2632
- async function cloneCodexSession(params = {}) {
2633
- const source = params.source === 'codex' ? 'codex' : '';
2634
- if (!source) {
2635
- return { error: '仅支持 Codex 会话克隆' };
3316
+ const provider = providers[providerName];
3317
+ if (!provider || !isPlainObject(provider)) {
3318
+ return { error: `上游 provider 不存在: ${providerName}` };
2636
3319
  }
2637
3320
 
2638
- const filePath = resolveSessionFilePath(source, params.filePath, params.sessionId);
2639
- if (!filePath) {
2640
- return { error: 'Session file not found' };
3321
+ const baseUrl = typeof provider.base_url === 'string' ? provider.base_url.trim() : '';
3322
+ if (!baseUrl || !isValidHttpUrl(baseUrl)) {
3323
+ return { error: `上游 provider base_url 无效: ${providerName}` };
2641
3324
  }
2642
3325
 
2643
- let content = '';
2644
- try {
2645
- content = fs.readFileSync(filePath, 'utf-8');
2646
- } catch (e) {
2647
- return { error: `读取会话失败: ${e.message}` };
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
+ }
2648
3334
  }
2649
3335
 
2650
- if (!content.trim()) {
2651
- return { error: 'Session file is empty' };
3336
+ let authHeader = '';
3337
+ if (token) {
3338
+ authHeader = /^bearer\s+/i.test(token) ? token : `Bearer ${token}`;
2652
3339
  }
2653
3340
 
2654
- const lineEnding = detectLineEnding(content);
2655
- const rawLines = content.split(/\r?\n/);
2656
- if (rawLines.length > 0 && rawLines[rawLines.length - 1] === '') {
2657
- rawLines.pop();
2658
- }
3341
+ return {
3342
+ providerName,
3343
+ baseUrl: normalizeBaseUrl(baseUrl),
3344
+ authHeader
3345
+ };
3346
+ }
2659
3347
 
2660
- let originalSessionId = typeof params.sessionId === 'string' ? params.sessionId.trim() : '';
2661
- if (!originalSessionId) {
2662
- originalSessionId = path.basename(filePath, '.jsonl');
2663
- }
2664
- let maxTimestampMs = 0;
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
+ }
3831
+
3832
+ const lineEnding = detectLineEnding(content);
3833
+ const rawLines = content.split(/\r?\n/);
3834
+ if (rawLines.length > 0 && rawLines[rawLines.length - 1] === '') {
3835
+ rawLines.pop();
3836
+ }
3837
+
3838
+ let originalSessionId = typeof params.sessionId === 'string' ? params.sessionId.trim() : '';
3839
+ if (!originalSessionId) {
3840
+ originalSessionId = path.basename(filePath, '.jsonl');
3841
+ }
3842
+ let maxTimestampMs = 0;
2665
3843
 
2666
3844
  for (const line of rawLines) {
2667
3845
  const trimmed = line.trim();
@@ -3164,6 +4342,9 @@ function buildExportPayload(includeKeys) {
3164
4342
  const providers = config.model_providers || {};
3165
4343
  const providerData = {};
3166
4344
  for (const [name, provider] of Object.entries(providers)) {
4345
+ if (isBuiltinProxyProvider(name)) {
4346
+ continue;
4347
+ }
3167
4348
  providerData[name] = {
3168
4349
  baseUrl: provider.base_url || '',
3169
4350
  apiKey: includeKeys ? (provider.preferred_auth_method || '') : null
@@ -3256,6 +4437,10 @@ function normalizeImportPayload(payload) {
3256
4437
  }
3257
4438
  }
3258
4439
 
4440
+ if (Object.keys(providers).length === 0 && (!payload.models || payload.models.length === 0)) {
4441
+ return { error: 'Invalid import payload' };
4442
+ }
4443
+
3259
4444
  return {
3260
4445
  providers,
3261
4446
  models: Array.isArray(payload.models) ? payload.models : [],
@@ -3281,6 +4466,9 @@ function importConfigData(payload, options = {}) {
3281
4466
  let updatedProviders = 0;
3282
4467
 
3283
4468
  for (const [name, provider] of Object.entries(normalized.providers)) {
4469
+ if (isBuiltinProxyProvider(name)) {
4470
+ continue;
4471
+ }
3284
4472
  if (existingProviders[name]) {
3285
4473
  if (overwriteProviders) {
3286
4474
  const apiKey = typeof provider.apiKey === 'string' && provider.apiKey
@@ -3312,6 +4500,7 @@ function importConfigData(payload, options = {}) {
3312
4500
  if (applyCurrentModels && normalized.currentModels) {
3313
4501
  const currentModels = readCurrentModels();
3314
4502
  for (const [name, model] of Object.entries(normalized.currentModels)) {
4503
+ if (isBuiltinProxyProvider(name)) continue;
3315
4504
  if (typeof model !== 'string' || !model) continue;
3316
4505
  currentModels[name] = model;
3317
4506
  }
@@ -3828,7 +5017,10 @@ function cmdUseModel(modelName, silent = false) {
3828
5017
 
3829
5018
  // 添加提供商
3830
5019
  function cmdAdd(name, baseUrl, apiKey, silent = false) {
3831
- if (!name || !baseUrl) {
5020
+ const providerName = typeof name === 'string' ? name.trim() : '';
5021
+ const providerBaseUrl = typeof baseUrl === 'string' ? baseUrl.trim() : '';
5022
+
5023
+ if (!providerName || !providerBaseUrl) {
3832
5024
  if (!silent) {
3833
5025
  console.error('用法: codexmate add <名称> <URL> [密钥]');
3834
5026
  console.log('\n示例:');
@@ -3836,17 +5028,21 @@ function cmdAdd(name, baseUrl, apiKey, silent = false) {
3836
5028
  }
3837
5029
  throw new Error('名称和URL必填');
3838
5030
  }
5031
+ if (isReservedProviderNameForCreation(providerName)) {
5032
+ if (!silent) console.error('错误: local provider 为系统保留名称,不可新增');
5033
+ throw new Error('local provider 为系统保留名称,不可新增');
5034
+ }
3839
5035
 
3840
5036
  const config = readConfig();
3841
- if (config.model_providers && config.model_providers[name]) {
3842
- if (!silent) console.error('错误: 提供商已存在:', name);
5037
+ if (config.model_providers && config.model_providers[providerName]) {
5038
+ if (!silent) console.error('错误: 提供商已存在:', providerName);
3843
5039
  throw new Error('提供商已存在');
3844
5040
  }
3845
5041
 
3846
5042
  const newBlock = `
3847
- [model_providers.${name}]
3848
- name = "${name}"
3849
- base_url = "${baseUrl}"
5043
+ [model_providers.${providerName}]
5044
+ name = "${providerName}"
5045
+ base_url = "${providerBaseUrl}"
3850
5046
  wire_api = "responses"
3851
5047
  requires_openai_auth = false
3852
5048
  preferred_auth_method = "${apiKey || ''}"
@@ -3860,60 +5056,47 @@ stream_idle_timeout_ms = 300000
3860
5056
 
3861
5057
  // 初始化当前模型
3862
5058
  const currentModels = readCurrentModels();
3863
- if (!currentModels[name]) {
3864
- currentModels[name] = readModels()[0];
5059
+ if (!currentModels[providerName]) {
5060
+ currentModels[providerName] = readModels()[0];
3865
5061
  writeCurrentModels(currentModels);
3866
5062
  }
3867
5063
 
3868
5064
  if (!silent) {
3869
- console.log('✓ 已添加提供商:', name);
3870
- console.log(' URL:', baseUrl);
5065
+ console.log('✓ 已添加提供商:', providerName);
5066
+ console.log(' URL:', providerBaseUrl);
3871
5067
  console.log();
3872
5068
  }
3873
5069
  }
3874
5070
 
3875
5071
  // 删除提供商
3876
5072
  function cmdDelete(name, silent = false) {
3877
- const config = readConfig();
3878
- if (!config.model_providers || !config.model_providers[name]) {
3879
- if (!silent) console.error('错误: 提供商不存在:', name);
3880
- throw new Error('提供商不存在');
3881
- }
3882
-
3883
- const content = fs.readFileSync(CONFIG_FILE, 'utf-8');
3884
- const safeName = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
3885
- const sectionRegex = new RegExp(`\\[\\s*model_providers\\s*\\.\\s*${safeName}\\s*\\]`);
3886
- const match = content.match(sectionRegex);
3887
- if (!match) {
3888
- if (!silent) console.error('错误: 无法找到提供商配置块');
3889
- throw new Error('无法找到提供商配置块');
5073
+ const res = performProviderDeletion(name, { silent });
5074
+ if (res.error) {
5075
+ throw new Error(res.error);
3890
5076
  }
3891
-
3892
- const startIdx = match.index;
3893
- const rest = content.slice(startIdx + match[0].length);
3894
- const nextIdx = rest.indexOf('[');
3895
- const endIdx = nextIdx === -1 ? content.length : (startIdx + match[0].length + nextIdx);
3896
-
3897
- const newContent = content.slice(0, startIdx) + content.slice(endIdx);
3898
- writeConfig(newContent.trim());
3899
-
3900
- // 删除当前模型记录
3901
- const currentModels = readCurrentModels();
3902
- delete currentModels[name];
3903
- writeCurrentModels(currentModels);
3904
-
3905
5077
  if (!silent) {
3906
5078
  console.log('✓ 已删除提供商:', name);
5079
+ if (res.switched && res.provider) {
5080
+ console.log(` 已自动切换到 provider: ${res.provider},model: ${res.model || '(未设置)'}`);
5081
+ }
3907
5082
  console.log();
3908
5083
  }
3909
5084
  }
3910
5085
 
3911
5086
  // 更新提供商
3912
- function cmdUpdate(name, baseUrl, apiKey, silent = false) {
5087
+ function cmdUpdate(name, baseUrl, apiKey, silent = false, options = {}) {
5088
+ const allowManaged = !!(options && options.allowManaged);
3913
5089
  if (!name) {
3914
5090
  if (!silent) console.error('错误: 提供商名称必填');
3915
5091
  throw new Error('提供商名称必填');
3916
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
+ }
3917
5100
 
3918
5101
  const config = readConfig();
3919
5102
  if (!config.model_providers || !config.model_providers[name]) {
@@ -4104,7 +5287,7 @@ function readClaudeSettingsInfo() {
4104
5287
  return {
4105
5288
  error: readResult.error || '读取 Claude 配置失败',
4106
5289
  exists: !!readResult.exists,
4107
- path: CLAUDE_SETTINGS_FILE
5290
+ targetPath: CLAUDE_SETTINGS_FILE
4108
5291
  };
4109
5292
  }
4110
5293
 
@@ -4115,7 +5298,7 @@ function readClaudeSettingsInfo() {
4115
5298
 
4116
5299
  return {
4117
5300
  exists: !!readResult.exists,
4118
- path: CLAUDE_SETTINGS_FILE,
5301
+ targetPath: CLAUDE_SETTINGS_FILE,
4119
5302
  apiKey: typeof env.ANTHROPIC_API_KEY === 'string' ? env.ANTHROPIC_API_KEY : '',
4120
5303
  baseUrl: typeof env.ANTHROPIC_BASE_URL === 'string' ? env.ANTHROPIC_BASE_URL : '',
4121
5304
  model: typeof env.ANTHROPIC_MODEL === 'string' ? env.ANTHROPIC_MODEL : '',
@@ -4123,6 +5306,225 @@ function readClaudeSettingsInfo() {
4123
5306
  };
4124
5307
  }
4125
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
+
5494
+ return {
5495
+ success: true,
5496
+ targetDir,
5497
+ appliedFrom: payload.fileName || '',
5498
+ backupPath,
5499
+ backupWarning: backupResult.warning || ''
5500
+ };
5501
+ } catch (e) {
5502
+ return { error: `导入失败:${e.message}` };
5503
+ } finally {
5504
+ fs.rmSync(tempDir, { recursive: true, force: true });
5505
+ }
5506
+ }
5507
+
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
+ }
5517
+
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
+ });
5526
+ }
5527
+
4126
5528
  // CLI: 一行写入 Claude Code 配置
4127
5529
  function cmdClaude(baseUrl, apiKey, model, silent = false) {
4128
5530
  const normalizedBaseUrl = typeof baseUrl === 'string' ? baseUrl.trim() : '';
@@ -4152,43 +5554,221 @@ function cmdClaude(baseUrl, apiKey, model, silent = false) {
4152
5554
  throw new Error(message);
4153
5555
  }
4154
5556
 
4155
- if (!silent) {
4156
- console.log('✓ 已写入 Claude Code 配置');
4157
- console.log(' Base URL:', normalizedBaseUrl);
4158
- console.log(' 模型:', normalizedModel);
4159
- if (result.targetPath) {
4160
- console.log(' 目标文件:', result.targetPath);
5557
+ if (!silent) {
5558
+ console.log('✓ 已写入 Claude Code 配置');
5559
+ console.log(' Base URL:', normalizedBaseUrl);
5560
+ console.log(' 模型:', normalizedModel);
5561
+ if (result.targetPath) {
5562
+ console.log(' 目标文件:', result.targetPath);
5563
+ }
5564
+ if (result.backupPath) {
5565
+ console.log(' 已自动备份:', result.backupPath);
5566
+ }
5567
+ console.log();
5568
+ }
5569
+
5570
+ return result;
5571
+ }
5572
+
5573
+ function commandExists(command, args = '') {
5574
+ try {
5575
+ execSync(`${command} ${args}`, { stdio: 'ignore', shell: process.platform === 'win32' });
5576
+ return true;
5577
+ } catch (e) {
5578
+ return false;
5579
+ }
5580
+ }
5581
+
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;
4161
5704
  }
4162
- if (result.backupPath) {
4163
- console.log(' 已自动备份:', result.backupPath);
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;
4164
5712
  }
4165
- console.log();
5713
+ commandsByTarget[target.id] = {
5714
+ install: `npm install -g ${pkg}`,
5715
+ update: `npm update -g ${pkg}`,
5716
+ uninstall: `npm uninstall -g ${pkg}`
5717
+ };
4166
5718
  }
4167
5719
 
4168
- return result;
5720
+ return {
5721
+ packageManager: manager,
5722
+ commandsByTarget
5723
+ };
4169
5724
  }
4170
5725
 
4171
- function commandExists(command, args = '') {
4172
- try {
4173
- execSync(`${command} ${args}`, { stdio: 'ignore' });
4174
- return true;
4175
- } catch (e) {
4176
- return false;
4177
- }
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
+ };
4178
5760
  }
4179
5761
 
4180
- const SEVEN_ZIP_PATHS = [
4181
- 'C:\\Program Files\\7-Zip\\7z.exe',
4182
- 'C:\\Program Files (x86)\\7-Zip\\7z.exe',
4183
- '7z'
5762
+ const ZIP_PATHS = [
5763
+ 'zip'
4184
5764
  ];
4185
5765
 
4186
- function findSevenZipExecutable() {
4187
- for (const candidate of SEVEN_ZIP_PATHS) {
5766
+ function findZipExecutable() {
5767
+ for (const candidate of ZIP_PATHS) {
4188
5768
  try {
4189
- if (candidate === '7z') {
4190
- if (commandExists('7z', '--help')) {
4191
- return '7z';
5769
+ if (candidate === 'zip') {
5770
+ if (commandExists('zip', '--help')) {
5771
+ return 'zip';
4192
5772
  }
4193
5773
  } else if (fs.existsSync(candidate)) {
4194
5774
  return candidate;
@@ -4199,18 +5779,14 @@ function findSevenZipExecutable() {
4199
5779
  }
4200
5780
 
4201
5781
  function resolveZipTool() {
4202
- const sevenZipExe = findSevenZipExecutable();
4203
- if (sevenZipExe) {
4204
- return { type: '7z', cmd: sevenZipExe };
5782
+ const zipExe = findZipExecutable();
5783
+ if (zipExe) {
5784
+ return { type: 'zip', cmd: zipExe };
4205
5785
  }
4206
5786
  return { type: 'lib', cmd: 'zip-lib' };
4207
5787
  }
4208
5788
 
4209
5789
  function resolveUnzipTool() {
4210
- const sevenZipExe = findSevenZipExecutable();
4211
- if (sevenZipExe) {
4212
- return { type: '7z', cmd: sevenZipExe };
4213
- }
4214
5790
  return { type: 'lib', cmd: 'zip-lib' };
4215
5791
  }
4216
5792
 
@@ -4227,7 +5803,7 @@ async function unzipWithLibrary(zipPath, outputDir) {
4227
5803
  await zipLib.extract(zipPath, outputDir);
4228
5804
  }
4229
5805
 
4230
- // 压缩(7-Zip 优先)
5806
+ // 压缩(系统 zip 优先,其次 zip-lib)
4231
5807
  async function cmdZip(targetPath, options = {}) {
4232
5808
  if (!targetPath) {
4233
5809
  console.error('用法: codexmate zip <文件或文件夹路径> [--max:压缩级别]');
@@ -4257,40 +5833,27 @@ async function cmdZip(targetPath, options = {}) {
4257
5833
  const outputPath = path.join(outputDir, `${baseName}.zip`);
4258
5834
 
4259
5835
  const zipTool = resolveZipTool();
5836
+ const useZipCmd = zipTool.type === 'zip';
4260
5837
 
4261
5838
  console.log('\n压缩配置:');
4262
5839
  console.log(' 源路径:', absPath);
4263
5840
  console.log(' 输出文件:', outputPath);
4264
- console.log(' 压缩级别:', compressionLevel);
4265
- console.log(' 压缩工具:', zipTool.type === '7z' ? '7-Zip' : 'zip-lib');
4266
- console.log(' 多线程:', zipTool.type === '7z' ? '启用' : '未启用(JS 库)');
4267
- if (zipTool.type !== '7z') {
4268
- 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,已忽略)');
4269
5846
  }
4270
5847
  console.log('\n开始压缩...\n');
4271
5848
 
4272
5849
  try {
4273
- if (zipTool.type === '7z') {
4274
- const cmd = `"${zipTool.cmd}" a -tzip -mmt=on -mx=${compressionLevel} "${outputPath}" "${absPath}"`;
4275
- const result = execSync(cmd, { encoding: 'utf-8', maxBuffer: 50 * 1024 * 1024 });
4276
- const sizeMatch = result.match(/Archive size:\s*(\d+)\s*bytes/);
4277
- const filesMatch = result.match(/(\d+)\s*files/);
4278
-
4279
- console.log('✓ 压缩完成!');
4280
- console.log(' 输出文件:', outputPath);
4281
- if (sizeMatch) {
4282
- const sizeBytes = parseInt(sizeMatch[1]);
4283
- const sizeMB = (sizeBytes / 1024 / 1024).toFixed(2);
4284
- console.log(' 压缩大小:', sizeMB, 'MB');
4285
- }
4286
- if (filesMatch) {
4287
- console.log(' 文件数量:', filesMatch[1]);
4288
- }
4289
- console.log();
4290
- 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);
4291
5855
  }
4292
5856
 
4293
- await zipWithLibrary(absPath, outputPath);
4294
5857
  console.log('✓ 压缩完成!');
4295
5858
  console.log(' 输出文件:', outputPath);
4296
5859
  console.log();
@@ -4300,7 +5863,7 @@ async function cmdZip(targetPath, options = {}) {
4300
5863
  }
4301
5864
  }
4302
5865
 
4303
- // 解压(7-Zip 优先)
5866
+ // 解压(zip-lib)
4304
5867
  async function cmdUnzip(zipPath, outputDir) {
4305
5868
  if (!zipPath) {
4306
5869
  console.error('用法: codexmate unzip <zip文件路径> [输出目录]');
@@ -4332,24 +5895,10 @@ async function cmdUnzip(zipPath, outputDir) {
4332
5895
  console.log('\n解压配置:');
4333
5896
  console.log(' 源文件:', absZipPath);
4334
5897
  console.log(' 输出目录:', absOutputDir);
4335
- console.log(' 解压工具:', unzipTool.type === '7z' ? '7-Zip' : 'zip-lib');
4336
- console.log(' 多线程:', unzipTool.type === '7z' ? '启用' : '未启用(JS 库)');
5898
+ console.log(' 解压工具:', 'zip-lib');
4337
5899
  console.log('\n开始解压...\n');
4338
5900
 
4339
5901
  try {
4340
- if (unzipTool.type === '7z') {
4341
- const cmd = `"${unzipTool.cmd}" x -mmt=on -o"${absOutputDir}" "${absZipPath}" -y`;
4342
- const result = execSync(cmd, { encoding: 'utf-8', maxBuffer: 50 * 1024 * 1024 });
4343
- const filesMatch = result.match(/(\d+)\s*files/);
4344
- console.log('✓ 解压完成!');
4345
- console.log(' 输出目录:', absOutputDir);
4346
- if (filesMatch) {
4347
- console.log(' 文件数量:', filesMatch[1]);
4348
- }
4349
- console.log();
4350
- return;
4351
- }
4352
-
4353
5902
  await unzipWithLibrary(absZipPath, absOutputDir);
4354
5903
  console.log('✓ 解压完成!');
4355
5904
  console.log(' 输出目录:', absOutputDir);
@@ -4568,16 +6117,52 @@ function formatHostForUrl(host) {
4568
6117
  return value;
4569
6118
  }
4570
6119
 
4571
- // 打开 Web UI
4572
- function cmdStart(options = {}) {
4573
- const htmlPath = path.join(__dirname, 'web-ui.html');
4574
- const assetsDir = path.join(__dirname, 'res');
4575
- const webDir = path.join(__dirname, 'web-ui');
4576
- if (!fs.existsSync(htmlPath)) {
4577
- console.error('错误: web-ui.html 不存在');
4578
- process.exit(1);
6120
+ function watchPathsForRestart(targets, onChange) {
6121
+ const disposers = [];
6122
+ const debounceMs = 300;
6123
+ let timer = null;
6124
+
6125
+ const trigger = (info) => {
6126
+ if (timer) clearTimeout(timer);
6127
+ timer = setTimeout(() => {
6128
+ timer = null;
6129
+ onChange(info);
6130
+ }, debounceMs);
6131
+ };
6132
+
6133
+ const addWatcher = (target, recursive) => {
6134
+ if (!fs.existsSync(target)) return;
6135
+ try {
6136
+ const watcher = fs.watch(target, { recursive }, (eventType, filename) => {
6137
+ if (!filename) return;
6138
+ const lower = filename.toLowerCase();
6139
+ if (!(/\.(html|js|mjs|css)$/.test(lower))) return;
6140
+ trigger({ target, eventType, filename });
6141
+ });
6142
+ disposers.push(() => watcher.close());
6143
+ return true;
6144
+ } catch (e) {
6145
+ return false;
6146
+ }
6147
+ };
6148
+
6149
+ for (const target of targets) {
6150
+ const ok = addWatcher(target, true);
6151
+ if (!ok) {
6152
+ addWatcher(target, false);
6153
+ }
4579
6154
  }
4580
6155
 
6156
+ return () => {
6157
+ for (const dispose of disposers) {
6158
+ try { dispose(); } catch (_) {}
6159
+ }
6160
+ };
6161
+ }
6162
+
6163
+ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser }) {
6164
+ const connections = new Set();
6165
+
4581
6166
  const server = http.createServer((req, res) => {
4582
6167
  const requestPath = (req.url || '/').split('?')[0];
4583
6168
  if (requestPath === '/api') {
@@ -4593,15 +6178,20 @@ function cmdStart(options = {}) {
4593
6178
  const statusConfigResult = readConfigOrVirtualDefault();
4594
6179
  const config = statusConfigResult.config;
4595
6180
  const serviceTier = typeof config.service_tier === 'string' ? config.service_tier.trim() : '';
6181
+ const modelReasoningEffort = typeof config.model_reasoning_effort === 'string' ? config.model_reasoning_effort.trim() : '';
4596
6182
  result = {
4597
6183
  provider: config.model_provider || '未设置',
4598
6184
  model: config.model || '未设置',
4599
6185
  serviceTier,
6186
+ modelReasoningEffort,
4600
6187
  configReady: !statusConfigResult.isVirtual,
4601
6188
  configNotice: statusConfigResult.reason || '',
4602
6189
  initNotice: consumeInitNotice()
4603
6190
  };
4604
6191
  break;
6192
+ case 'install-status':
6193
+ result = buildInstallStatusReport();
6194
+ break;
4605
6195
  case 'list':
4606
6196
  const listConfigResult = readConfigOrVirtualDefault();
4607
6197
  const listConfig = listConfigResult.config;
@@ -4614,20 +6204,27 @@ function cmdStart(options = {}) {
4614
6204
  url: p.base_url || '',
4615
6205
  key: maskKey(p.preferred_auth_method || ''),
4616
6206
  hasKey: !!(p.preferred_auth_method && p.preferred_auth_method.trim()),
4617
- current: name === current
6207
+ current: name === current,
6208
+ readOnly: isBuiltinProxyProvider(name),
6209
+ nonDeletable: isNonDeletableProvider(name),
6210
+ nonEditable: isNonEditableProvider(name)
4618
6211
  }))
4619
6212
  };
4620
6213
  break;
4621
6214
  case 'models':
4622
6215
  {
4623
6216
  const providerName = params && typeof params.provider === 'string' ? params.provider : '';
4624
- const res = await fetchProviderModels(providerName);
4625
- if (res.error) {
4626
- result = { error: res.error, models: [], source: 'remote' };
4627
- } else if (res.unlimited) {
4628
- result = { models: [], source: 'remote', provider: res.provider || '', unlimited: true };
6217
+ if (!providerName) {
6218
+ result = { error: 'Provider name is required' };
4629
6219
  } else {
4630
- result = { models: res.models || [], source: 'remote', provider: res.provider || '' };
6220
+ const res = await fetchProviderModels(providerName);
6221
+ if (res.error) {
6222
+ result = { error: res.error, models: [], source: 'remote' };
6223
+ } else if (res.unlimited) {
6224
+ result = { models: [], source: 'remote', provider: res.provider || '', unlimited: true };
6225
+ } else {
6226
+ result = { models: res.models || [], source: 'remote', provider: res.provider || '' };
6227
+ }
4631
6228
  }
4632
6229
  }
4633
6230
  break;
@@ -4635,13 +6232,17 @@ function cmdStart(options = {}) {
4635
6232
  {
4636
6233
  const baseUrl = params && typeof params.baseUrl === 'string' ? params.baseUrl : '';
4637
6234
  const apiKey = params && typeof params.apiKey === 'string' ? params.apiKey : '';
4638
- const res = await fetchModelsFromBaseUrl(baseUrl, apiKey);
4639
- if (res.error) {
4640
- result = { error: res.error, models: [], source: 'remote' };
4641
- } else if (res.unlimited) {
4642
- result = { models: [], source: 'remote', unlimited: true };
6235
+ if (!baseUrl) {
6236
+ result = { error: 'Base URL is required' };
4643
6237
  } else {
4644
- result = { models: res.models || [], source: 'remote' };
6238
+ const res = await fetchModelsFromBaseUrl(baseUrl, apiKey);
6239
+ if (res.error) {
6240
+ result = { error: res.error, models: [], source: 'remote' };
6241
+ } else if (res.unlimited) {
6242
+ result = { models: [], source: 'remote', unlimited: true };
6243
+ } else {
6244
+ result = { models: res.models || [], source: 'remote' };
6245
+ }
4645
6246
  }
4646
6247
  }
4647
6248
  break;
@@ -4651,6 +6252,15 @@ function cmdStart(options = {}) {
4651
6252
  case 'apply-config-template':
4652
6253
  result = applyConfigTemplate(params || {});
4653
6254
  break;
6255
+ case 'add-provider':
6256
+ result = addProviderToConfig(params || {});
6257
+ break;
6258
+ case 'update-provider':
6259
+ result = updateProviderInConfig(params || {});
6260
+ break;
6261
+ case 'delete-provider':
6262
+ result = deleteProviderFromConfig(params || {});
6263
+ break;
4654
6264
  case 'get-recent-configs':
4655
6265
  result = { items: readRecentConfigs() };
4656
6266
  break;
@@ -4669,6 +6279,9 @@ function cmdStart(options = {}) {
4669
6279
  case 'apply-openclaw-config':
4670
6280
  result = applyOpenclawConfig(params || {});
4671
6281
  break;
6282
+ case 'reset-config':
6283
+ result = resetConfigToDefault();
6284
+ break;
4672
6285
  case 'get-openclaw-agents-file':
4673
6286
  result = readOpenclawAgentsFile();
4674
6287
  break;
@@ -4726,14 +6339,29 @@ function cmdStart(options = {}) {
4726
6339
  break;
4727
6340
  }
4728
6341
  case 'list-sessions':
4729
- result = {
4730
- sessions: listAllSessions(params)
4731
- };
6342
+ {
6343
+ const source = typeof params.source === 'string' ? params.source.trim().toLowerCase() : '';
6344
+ if (source && source !== 'codex' && source !== 'claude' && source !== 'all') {
6345
+ result = { error: 'Invalid source. Must be codex, claude, or all' };
6346
+ } else {
6347
+ result = {
6348
+ sessions: listAllSessions(params),
6349
+ source: source || 'all'
6350
+ };
6351
+ }
6352
+ }
4732
6353
  break;
4733
6354
  case 'list-session-paths':
4734
- result = {
4735
- paths: listSessionPaths(params)
4736
- };
6355
+ {
6356
+ const source = typeof params.source === 'string' ? params.source.trim().toLowerCase() : '';
6357
+ if (source && source !== 'codex' && source !== 'claude' && source !== 'all') {
6358
+ result = { error: 'Invalid source. Must be codex, claude, or all' };
6359
+ } else {
6360
+ result = {
6361
+ paths: listSessionPaths(params)
6362
+ };
6363
+ }
6364
+ }
4737
6365
  break;
4738
6366
  case 'export-session':
4739
6367
  result = await exportSessionData(params);
@@ -4750,15 +6378,78 @@ function cmdStart(options = {}) {
4750
6378
  case 'session-plain':
4751
6379
  result = await readSessionPlain(params);
4752
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;
4753
6436
  default:
4754
6437
  result = { error: '未知操作' };
4755
6438
  }
4756
6439
 
4757
- res.writeHead(200, { 'Content-Type': 'application/json' });
4758
- res.end(JSON.stringify(result));
6440
+ const responseBody = JSON.stringify(result, null, 2);
6441
+ res.writeHead(200, {
6442
+ 'Content-Type': 'application/json; charset=utf-8',
6443
+ 'Content-Length': Buffer.byteLength(responseBody, 'utf-8')
6444
+ });
6445
+ res.end(responseBody, 'utf-8');
4759
6446
  } catch (e) {
4760
- res.writeHead(500, { 'Content-Type': 'application/json' });
4761
- res.end(JSON.stringify({ error: e.message }));
6447
+ const errorBody = JSON.stringify({ error: e.message }, null, 2);
6448
+ res.writeHead(500, {
6449
+ 'Content-Type': 'application/json; charset=utf-8',
6450
+ 'Content-Length': Buffer.byteLength(errorBody, 'utf-8')
6451
+ });
6452
+ res.end(errorBody, 'utf-8');
4762
6453
  }
4763
6454
  });
4764
6455
  } else if (requestPath.startsWith('/web-ui/')) {
@@ -4777,6 +6468,8 @@ function cmdStart(options = {}) {
4777
6468
  const ext = path.extname(filePath).toLowerCase();
4778
6469
  const mime = ext === '.js' || ext === '.mjs'
4779
6470
  ? 'application/javascript; charset=utf-8'
6471
+ : ext === '.html'
6472
+ ? 'text/html; charset=utf-8'
4780
6473
  : ext === '.css'
4781
6474
  ? 'text/css; charset=utf-8'
4782
6475
  : ext === '.json'
@@ -4784,6 +6477,34 @@ function cmdStart(options = {}) {
4784
6477
  : 'application/octet-stream';
4785
6478
  res.writeHead(200, { 'Content-Type': mime });
4786
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);
4787
6508
  } else if (requestPath.startsWith('/res/')) {
4788
6509
  const normalized = path.normalize(requestPath).replace(/^([\\.\\/])+/, '');
4789
6510
  const filePath = path.join(__dirname, normalized);
@@ -4800,6 +6521,8 @@ function cmdStart(options = {}) {
4800
6521
  const ext = path.extname(filePath).toLowerCase();
4801
6522
  const mime = ext === '.js'
4802
6523
  ? 'application/javascript; charset=utf-8'
6524
+ : ext === '.html'
6525
+ ? 'text/html; charset=utf-8'
4803
6526
  : ext === '.json'
4804
6527
  ? 'application/json; charset=utf-8'
4805
6528
  : 'application/octet-stream';
@@ -4810,58 +6533,458 @@ function cmdStart(options = {}) {
4810
6533
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
4811
6534
  res.end(html);
4812
6535
  }
4813
- });
6536
+ });
6537
+
6538
+ server.on('connection', (socket) => {
6539
+ connections.add(socket);
6540
+ socket.on('close', () => connections.delete(socket));
6541
+ });
6542
+
6543
+ server.once('error', (err) => {
6544
+ if (err && err.code === 'EADDRINUSE') {
6545
+ console.error(`! 启动失败: 端口 ${port} 已被占用,可能有残留的 codexmate run 实例。`);
6546
+ console.error(' 请先停止旧实例或更换端口后重试。');
6547
+ } else {
6548
+ console.error('! 启动 Web UI 失败:', err && err.message ? err.message : err);
6549
+ }
6550
+ process.exit(1);
6551
+ });
6552
+
6553
+ const openHost = isAnyAddressHost(host) ? DEFAULT_WEB_HOST : host;
6554
+ const openUrl = `http://${formatHostForUrl(openHost)}:${port}`;
6555
+ server.listen(port, host, () => {
6556
+ console.log('\n✓ Web UI 已启动:', openUrl);
6557
+ if (host && host !== openHost) {
6558
+ console.log(' 监听地址:', host);
6559
+ }
6560
+ console.log(' 按 Ctrl+C 退出\n');
6561
+ if (isAnyAddressHost(host)) {
6562
+ console.warn('! 安全提示: 当前监听所有网卡(无鉴权)。');
6563
+ console.warn(' 建议仅在可信网络使用,或改用 --host 127.0.0.1。');
6564
+ }
6565
+
6566
+ if (!process.env.CODEXMATE_NO_BROWSER && openBrowser) {
6567
+ const platform = process.platform;
6568
+ let command;
6569
+ const url = openUrl;
6570
+
6571
+ if (platform === 'win32') {
6572
+ command = `start \"\" \"${url}\"`;
6573
+ } else if (platform === 'darwin') {
6574
+ command = `open \"${url}\"`;
6575
+ } else {
6576
+ command = `xdg-open \"${url}\"`;
6577
+ }
6578
+
6579
+ exec(command, (error) => {
6580
+ if (error) console.warn('无法自动打开浏览器,请手动访问:', url);
6581
+ });
6582
+ }
6583
+ });
6584
+
6585
+ const stop = () => new Promise((resolve) => {
6586
+ let done = false;
6587
+ const finish = () => {
6588
+ if (done) return;
6589
+ done = true;
6590
+ for (const socket of connections) {
6591
+ try { socket.destroy(); } catch (_) {}
6592
+ }
6593
+ connections.clear();
6594
+ resolve();
6595
+ };
6596
+
6597
+ if (!server.listening) {
6598
+ finish();
6599
+ return;
6600
+ }
6601
+
6602
+ server.close(() => finish());
6603
+ setTimeout(() => finish(), 800);
6604
+ });
6605
+
6606
+ return { server, stop };
6607
+ }
6608
+
6609
+ // 打开 Web UI
6610
+ function cmdStart(options = {}) {
6611
+ const webDir = path.join(__dirname, 'web-ui');
6612
+ const newHtmlPath = path.join(webDir, 'index.html');
6613
+ const legacyHtmlPath = path.join(__dirname, 'web-ui.html');
6614
+ const htmlPath = fs.existsSync(newHtmlPath) ? newHtmlPath : legacyHtmlPath;
6615
+ const assetsDir = path.join(__dirname, 'res');
6616
+ if (!fs.existsSync(htmlPath)) {
6617
+ console.error('错误: Web UI 页面不存在(尝试路径: web-ui/index.html, web-ui.html)');
6618
+ process.exit(1);
6619
+ }
6620
+
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
+ }
4814
6845
 
4815
- const port = resolveWebPort();
4816
- const host = resolveWebHost(options);
4817
- const openHost = isAnyAddressHost(host) ? DEFAULT_WEB_HOST : host;
4818
- const openUrl = `http://${formatHostForUrl(openHost)}:${port}`;
4819
- server.listen(port, host, () => {
4820
- console.log('\n✓ Web UI 已启动:', openUrl);
4821
- if (host && host !== openHost) {
4822
- console.log(' 监听地址:', host);
4823
- }
4824
- console.log(' 按 Ctrl+C 退出\n');
4825
- if (isAnyAddressHost(host)) {
4826
- console.warn('! 安全提示: 当前监听所有网卡(无鉴权)。');
4827
- console.warn(' 建议仅在可信网络使用,或改用 --host 127.0.0.1。');
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);
4828
6866
  }
6867
+ console.log();
6868
+ return;
6869
+ }
4829
6870
 
4830
- // 打开浏览器
4831
- const platform = process.platform;
4832
- let command;
4833
- const url = openUrl;
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
+ }
4834
6884
 
4835
- if (platform === 'win32') {
4836
- command = `start "" "${url}"`;
4837
- } else if (platform === 'darwin') {
4838
- command = `open "${url}"`;
4839
- } else {
4840
- command = `xdg-open "${url}"`;
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}` : ''}`);
4841
6896
  }
6897
+ console.log();
6898
+ return;
6899
+ }
4842
6900
 
4843
- const disableBrowser = process.env.CODEXMATE_NO_BROWSER === '1';
4844
- if (!disableBrowser) {
4845
- exec(command, (error) => {
4846
- if (error) console.warn('无法自动打开浏览器,请手动访问:', url);
4847
- });
6901
+ if (subcommand === 'enable' || subcommand === 'default-codex') {
6902
+ const result = await ensureBuiltinProxyForCodexDefault(options);
6903
+ if (result.error) {
6904
+ throw new Error(result.error);
4848
6905
  }
4849
- });
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
+ }
6918
+
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
+ }
6944
+
6945
+ if (subcommand === 'stop') {
6946
+ await stopBuiltinProxyRuntime();
6947
+ console.log('✓ 内建代理已停止\n');
6948
+ return;
6949
+ }
6950
+
6951
+ throw new Error(`未知 proxy 子命令: ${subcommand}`);
4850
6952
  }
4851
6953
 
4852
- async function cmdCodex(args = []) {
6954
+ async function runProxyCommand(displayName, binNames, args = [], installTip = '') {
4853
6955
  const extraArgs = Array.isArray(args) ? args.filter(arg => arg !== undefined) : [];
4854
6956
  const hasYolo = extraArgs.includes('--yolo');
4855
6957
  const finalArgs = hasYolo ? extraArgs : ['--yolo', ...extraArgs];
4856
6958
 
6959
+ const names = Array.isArray(binNames) ? binNames : [binNames];
6960
+ let selectedBin = names[0];
6961
+ let exists = false;
6962
+
6963
+ // Detect if any of the bin names exist
6964
+ for (const name of names) {
6965
+ if (commandExists(name, '--version')) {
6966
+ selectedBin = name;
6967
+ exists = true;
6968
+ break;
6969
+ }
6970
+ }
6971
+
6972
+ if (!exists) {
6973
+ let msg = `无法启动 ${displayName},请确认已安装并在 PATH 中。`;
6974
+ if (installTip) {
6975
+ msg += `\n安装建议: ${installTip}`;
6976
+ }
6977
+ throw new Error(msg);
6978
+ }
6979
+
4857
6980
  return new Promise((resolve, reject) => {
4858
- const child = spawn('codex', finalArgs, {
6981
+ const child = spawn(selectedBin, finalArgs, {
4859
6982
  stdio: 'inherit',
4860
6983
  shell: process.platform === 'win32'
4861
6984
  });
4862
6985
 
4863
6986
  child.on('error', (err) => {
4864
- reject(new Error(`无法启动 codex,请确认已安装并在 PATH 中: ${err.message}`));
6987
+ reject(new Error(`运行 ${selectedBin} 失败: ${err.message}`));
4865
6988
  });
4866
6989
 
4867
6990
  child.on('exit', (code, signal) => {
@@ -4882,16 +7005,805 @@ async function cmdCodex(args = []) {
4882
7005
  });
4883
7006
  }
4884
7007
 
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
+ }
7019
+ return runProxyCommand('Codex', 'codex', args);
7020
+ }
7021
+
7022
+ async function cmdQwen(args = []) {
7023
+ return runProxyCommand('Qwen', ['qwen', 'qwen-code'], args, 'npm install -g @qwen-code/qwen-code');
7024
+ }
7025
+
7026
+ async function cmdGemini(args = []) {
7027
+ return runProxyCommand('Gemini', ['gemini', 'gemini-cli'], args, 'npm install -g @google/gemini-cli');
7028
+ }
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
+
4885
7792
  // ============================================================================
4886
7793
  // 主程序
4887
7794
  // ============================================================================
4888
7795
  async function main() {
7796
+ const args = process.argv.slice(2);
7797
+ const command = args[0];
7798
+ const isMcpCommand = command === 'mcp';
4889
7799
  const bootstrap = ensureManagedConfigBootstrap();
4890
7800
  if (bootstrap && bootstrap.notice) {
4891
- 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
+ }
4892
7805
  }
4893
7806
 
4894
- const args = process.argv.slice(2);
4895
7807
  if (args.length === 0) {
4896
7808
  console.log('\nCodex Mate - Codex 提供商管理工具');
4897
7809
  console.log('\n用法:');
@@ -4906,17 +7818,20 @@ async function main() {
4906
7818
  console.log(' codexmate claude <BaseURL> <API密钥> [模型] 写入 Claude Code 配置');
4907
7819
  console.log(' codexmate add-model <模型> 添加模型');
4908
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> 内建代理');
4909
7823
  console.log(' codexmate run [--host <HOST>] 启动 Web 界面');
4910
7824
  console.log(' codexmate codex [参数...] 等同于 codex --yolo');
7825
+ console.log(' codexmate qwen [参数...] 等同于 qwen --yolo');
7826
+ console.log(' codexmate gemini [参数...] 等同于 gemini --yolo');
7827
+ console.log(' codexmate mcp [serve] [--transport stdio] [--allow-write|--read-only]');
4911
7828
  console.log(' codexmate export-session --source <codex|claude> (--session-id <ID>|--file <PATH>) [--output <PATH>] [--max-messages <N|all|Infinity>]');
4912
- console.log(' codexmate zip <路径> [--max:级别] 压缩(7-Zip 优先)');
4913
- console.log(' codexmate unzip <zip文件> [输出目录] 解压(7-Zip 优先)');
7829
+ console.log(' codexmate zip <路径> [--max:级别] 压缩(系统 zip 优先,其次 zip-lib)');
7830
+ console.log(' codexmate unzip <zip文件> [输出目录] 解压(zip-lib)');
4914
7831
  console.log('');
4915
7832
  process.exit(0);
4916
7833
  }
4917
7834
 
4918
- const command = args[0];
4919
-
4920
7835
  switch (command) {
4921
7836
  case 'status': cmdStatus(); break;
4922
7837
  case 'setup': await cmdSetup(); break;
@@ -4929,6 +7844,8 @@ async function main() {
4929
7844
  case 'claude': cmdClaude(args[1], args[2], args[3]); break;
4930
7845
  case 'add-model': cmdAddModel(args[1]); break;
4931
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;
4932
7849
  case 'run': cmdStart(parseStartOptions(args.slice(1))); break;
4933
7850
  case 'start':
4934
7851
  console.error('错误: 命令已更名为 "run",请使用: codexmate run');
@@ -4939,6 +7856,17 @@ async function main() {
4939
7856
  process.exit(exitCode);
4940
7857
  break;
4941
7858
  }
7859
+ case 'qwen': {
7860
+ const exitCode = await cmdQwen(args.slice(1));
7861
+ process.exit(exitCode);
7862
+ break;
7863
+ }
7864
+ case 'gemini': {
7865
+ const exitCode = await cmdGemini(args.slice(1));
7866
+ process.exit(exitCode);
7867
+ break;
7868
+ }
7869
+ case 'mcp': await cmdMcp(args.slice(1)); break;
4942
7870
  case 'export-session': await cmdExportSession(args.slice(1)); break;
4943
7871
  case 'zip': {
4944
7872
  // 解析 --max:N 参数