codexmate 0.0.10 → 0.0.12

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 (63) hide show
  1. package/README.md +29 -11
  2. package/README.zh-CN.md +29 -11
  3. package/package.json +53 -36
  4. package/res/logo.png +0 -0
  5. package/{cli.js → src/cli.js} +822 -327
  6. package/src/lib/cli-file-utils.js +151 -0
  7. package/src/lib/cli-models-utils.js +152 -0
  8. package/src/lib/cli-network-utils.js +148 -0
  9. package/src/lib/cli-session-utils.js +121 -0
  10. package/src/lib/cli-utils.js +139 -0
  11. package/src/res/json5.min.js +1 -0
  12. package/src/res/logo.png +0 -0
  13. package/src/res/screenshot.png +0 -0
  14. package/src/res/vue.global.js +18552 -0
  15. package/src/web-ui/app.js +2970 -0
  16. package/src/web-ui/index.html +1310 -0
  17. package/src/web-ui/logic.mjs +157 -0
  18. package/src/web-ui/styles.css +2868 -0
  19. package/src/web-ui.html +17 -0
  20. package/web-ui/app.js +273 -144
  21. package/web-ui/index.html +1310 -0
  22. package/web-ui/logic.mjs +21 -21
  23. package/web-ui/styles.css +2868 -0
  24. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -27
  25. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -17
  26. package/.github/workflows/ci.yml +0 -26
  27. package/.github/workflows/release.yml +0 -159
  28. package/.planning/.fix-attempts +0 -1
  29. package/.planning/.lock +0 -6
  30. package/.planning/.verify-cache.json +0 -14
  31. package/.planning/CHECKPOINT.json +0 -46
  32. package/.planning/DESIGN.md +0 -26
  33. package/.planning/HISTORY.json +0 -124
  34. package/.planning/PLAN.md +0 -69
  35. package/.planning/REVIEW.md +0 -41
  36. package/.planning/STATE.md +0 -12
  37. package/.planning/STATS.json +0 -13
  38. package/.planning/VERIFICATION.md +0 -70
  39. package/.planning/daude-code-plan.md +0 -51
  40. package/.planning/research/architecture.md +0 -32
  41. package/.planning/research/conventions.md +0 -36
  42. package/.planning/task_1-REVIEW.md +0 -29
  43. package/.planning/task_1-SUMMARY.md +0 -32
  44. package/.planning/task_2-REVIEW.md +0 -24
  45. package/.planning/task_2-SUMMARY.md +0 -37
  46. package/.planning/task_3-REVIEW.md +0 -25
  47. package/.planning/task_3-SUMMARY.md +0 -31
  48. package/cmd/publish-npm.cmd +0 -65
  49. package/tests/e2e/helpers.js +0 -214
  50. package/tests/e2e/recent-health.e2e.js +0 -142
  51. package/tests/e2e/run.js +0 -154
  52. package/tests/e2e/test-claude.js +0 -21
  53. package/tests/e2e/test-config.js +0 -124
  54. package/tests/e2e/test-health-speed.js +0 -79
  55. package/tests/e2e/test-openclaw.js +0 -47
  56. package/tests/e2e/test-session-search.js +0 -114
  57. package/tests/e2e/test-sessions.js +0 -69
  58. package/tests/e2e/test-setup.js +0 -159
  59. package/tests/unit/run.mjs +0 -29
  60. package/tests/unit/web-ui-logic.test.mjs +0 -186
  61. package/web-ui.html +0 -3977
  62. /package/{CHANGELOG.md → doc/CHANGELOG.md} +0 -0
  63. /package/{CHANGELOG.zh-CN.md → doc/CHANGELOG.zh-CN.md} +0 -0
@@ -85,9 +85,9 @@ const MAX_SESSION_DETAIL_MESSAGES = 1000;
85
85
  const SESSION_TITLE_READ_BYTES = 64 * 1024;
86
86
  const CODEXMATE_MANAGED_MARKER = '# codexmate-managed: true';
87
87
  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;
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;
91
91
  const SESSION_SCAN_FACTOR = 4;
92
92
  const SESSION_SCAN_MIN_FILES = 800;
93
93
  const MAX_SESSION_PATH_LIST_SIZE = 2000;
@@ -1203,6 +1203,21 @@ function applyServiceTierToTemplate(template, serviceTier) {
1203
1203
  return `service_tier = "fast"\n${content}`;
1204
1204
  }
1205
1205
 
1206
+ function applyReasoningEffortToTemplate(template, reasoningEffort) {
1207
+ let content = typeof template === 'string' ? template : '';
1208
+ const effort = typeof reasoningEffort === 'string' ? reasoningEffort.trim().toLowerCase() : '';
1209
+ if (!effort) {
1210
+ return content;
1211
+ }
1212
+
1213
+ content = content.replace(/^\s*model_reasoning_effort\s*=\s*["'][^"']*["']\s*\n?/gmi, '');
1214
+ if (effort === 'high' || effort === 'xhigh') {
1215
+ content = content.replace(/^\s*\n*/, '');
1216
+ return `model_reasoning_effort = "${effort}"\n${content}`;
1217
+ }
1218
+ return content;
1219
+ }
1220
+
1206
1221
  function getConfigTemplate(params = {}) {
1207
1222
  let content = EMPTY_CONFIG_FALLBACK_TEMPLATE;
1208
1223
  if (fs.existsSync(CONFIG_FILE)) {
@@ -1219,6 +1234,9 @@ function getConfigTemplate(params = {}) {
1219
1234
  if (typeof params.serviceTier === 'string') {
1220
1235
  template = applyServiceTierToTemplate(template, params.serviceTier);
1221
1236
  }
1237
+ if (typeof params.reasoningEffort === 'string') {
1238
+ template = applyReasoningEffortToTemplate(template, params.reasoningEffort);
1239
+ }
1222
1240
  return {
1223
1241
  template
1224
1242
  };
@@ -1273,6 +1291,221 @@ function applyConfigTemplate(params = {}) {
1273
1291
  return { success: true };
1274
1292
  }
1275
1293
 
1294
+ function addProviderToConfig(params = {}) {
1295
+ const name = typeof params.name === 'string' ? params.name.trim() : '';
1296
+ const url = typeof params.url === 'string' ? params.url.trim() : '';
1297
+ const key = typeof params.key === 'string' ? params.key.trim() : '';
1298
+
1299
+ if (!name) return { error: '名称不能为空' };
1300
+ if (!url) return { error: 'URL 不能为空' };
1301
+
1302
+ ensureConfigDir();
1303
+
1304
+ let content = '';
1305
+ if (fs.existsSync(CONFIG_FILE)) {
1306
+ try {
1307
+ content = fs.readFileSync(CONFIG_FILE, 'utf-8');
1308
+ } catch (e) {
1309
+ return { error: `读取 config.toml 失败: ${e.message}` };
1310
+ }
1311
+ } else {
1312
+ content = EMPTY_CONFIG_FALLBACK_TEMPLATE;
1313
+ }
1314
+
1315
+ if (!content || !content.trim()) {
1316
+ content = EMPTY_CONFIG_FALLBACK_TEMPLATE;
1317
+ }
1318
+
1319
+ let parsed;
1320
+ try {
1321
+ parsed = toml.parse(content);
1322
+ } catch (e) {
1323
+ return { error: `config.toml 解析失败: ${e.message}` };
1324
+ }
1325
+
1326
+ if (!parsed.model_providers || typeof parsed.model_providers !== 'object') {
1327
+ parsed.model_providers = {};
1328
+ }
1329
+
1330
+ if (parsed.model_providers[name]) {
1331
+ return { error: '提供商已存在' };
1332
+ }
1333
+
1334
+ const escapeTomlString = (value) => String(value || '')
1335
+ .replace(/\\/g, '\\\\')
1336
+ .replace(/"/g, '\\"');
1337
+
1338
+ const lineEnding = content.includes('\r\n') ? '\r\n' : '\n';
1339
+ const safeName = escapeTomlString(name);
1340
+ const safeUrl = escapeTomlString(url);
1341
+ const safeKey = escapeTomlString(key);
1342
+ const block = [
1343
+ `[model_providers.${safeName}]`,
1344
+ `name = "${safeName}"`,
1345
+ `base_url = "${safeUrl}"`,
1346
+ `wire_api = "responses"`,
1347
+ `requires_openai_auth = false`,
1348
+ `preferred_auth_method = "${safeKey}"`,
1349
+ `request_max_retries = 4`,
1350
+ `stream_max_retries = 10`,
1351
+ `stream_idle_timeout_ms = 300000`
1352
+ ].join(lineEnding);
1353
+
1354
+ const newContent = content.trimEnd() + lineEnding + lineEnding + block + lineEnding;
1355
+
1356
+ try {
1357
+ writeConfig(newContent);
1358
+ } catch (e) {
1359
+ return { error: `写入配置失败: ${e.message}` };
1360
+ }
1361
+
1362
+ return { success: true };
1363
+ }
1364
+
1365
+ function updateProviderInConfig(params = {}) {
1366
+ const name = typeof params.name === 'string' ? params.name.trim() : '';
1367
+ const url = typeof params.url === 'string' ? params.url.trim() : '';
1368
+ const key = params.key !== undefined && params.key !== null
1369
+ ? String(params.key).trim()
1370
+ : undefined;
1371
+
1372
+ if (!name) return { error: '名称不能为空' };
1373
+ if (!url && key === undefined) {
1374
+ return { error: 'URL 或密钥至少填写一项' };
1375
+ }
1376
+
1377
+ try {
1378
+ cmdUpdate(name, url || undefined, key, true);
1379
+ return { success: true };
1380
+ } catch (e) {
1381
+ return { error: e.message || '更新失败' };
1382
+ }
1383
+ }
1384
+
1385
+ function deleteProviderFromConfig(params = {}) {
1386
+ const name = typeof params.name === 'string' ? params.name.trim() : '';
1387
+ if (!name) return { error: '名称不能为空' };
1388
+ if (!fs.existsSync(CONFIG_FILE)) {
1389
+ return { error: 'config.toml 不存在' };
1390
+ }
1391
+
1392
+ let config;
1393
+ try {
1394
+ config = readConfig();
1395
+ } catch (e) {
1396
+ return { error: `读取配置失败: ${e.message}` };
1397
+ }
1398
+
1399
+ const result = performProviderDeletion(name, { silent: true, config });
1400
+ if (result.error) {
1401
+ return { error: result.error };
1402
+ }
1403
+ return {
1404
+ success: true,
1405
+ switched: !!result.switched,
1406
+ provider: result.provider || '',
1407
+ model: result.model || ''
1408
+ };
1409
+ }
1410
+
1411
+ function performProviderDeletion(name, options = {}) {
1412
+ const silent = !!options.silent;
1413
+ const config = options.config || readConfig();
1414
+ if (!config.model_providers || !config.model_providers[name]) {
1415
+ const msg = '提供商不存在';
1416
+ if (!silent) console.error('错误:', msg, name);
1417
+ return { error: msg };
1418
+ }
1419
+
1420
+ const content = fs.readFileSync(CONFIG_FILE, 'utf-8');
1421
+ const lineEnding = content.includes('\r\n') ? '\r\n' : '\n';
1422
+ const hasBom = content.charCodeAt(0) === 0xFEFF;
1423
+ const safeName = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1424
+ const sectionRegex = new RegExp(`\\[\\s*model_providers\\s*\\.\\s*(?:"${safeName}"|'${safeName}'|${safeName})\\s*\\]`);
1425
+
1426
+ const remainingProviders = Object.keys(config.model_providers || {}).filter(item => item !== name);
1427
+ if (remainingProviders.length === 0) {
1428
+ const msg = '删除后将没有可用提供商';
1429
+ if (!silent) console.error('错误:', msg);
1430
+ return { error: msg };
1431
+ }
1432
+
1433
+ const currentProvider = typeof config.model_provider === 'string' ? config.model_provider.trim() : '';
1434
+ const currentModels = readCurrentModels();
1435
+ const models = readModels();
1436
+ const result = { success: true, switched: false, provider: '', model: '' };
1437
+
1438
+ if (currentModels[name]) {
1439
+ delete currentModels[name];
1440
+ }
1441
+
1442
+ let fallbackProvider = currentProvider;
1443
+ let fallbackModel = typeof config.model === 'string' ? config.model.trim() : '';
1444
+ if (currentProvider === name) {
1445
+ fallbackProvider = remainingProviders[0];
1446
+ fallbackModel = currentModels[fallbackProvider]
1447
+ || (Array.isArray(models) && models.length > 0 ? models[0] : (DEFAULT_MODELS[0] || ''));
1448
+ result.switched = true;
1449
+ result.provider = fallbackProvider;
1450
+ result.model = fallbackModel;
1451
+ }
1452
+
1453
+ const upsertTopLevel = (text, key, value) => {
1454
+ if (!value && value !== '') return text;
1455
+ const regex = new RegExp(`^\\s*${key}\\s*=.*$`, 'm');
1456
+ if (regex.test(text)) {
1457
+ return text.replace(regex, `${key} = "${value}"`);
1458
+ }
1459
+ return `${key} = "${value}"${lineEnding}${text}`;
1460
+ };
1461
+
1462
+ let updatedContent = null;
1463
+ const match = content.match(sectionRegex);
1464
+ if (match) {
1465
+ const startIdx = match.index;
1466
+ const rest = content.slice(startIdx + match[0].length);
1467
+ const nextIdx = rest.indexOf('[');
1468
+ const endIdx = nextIdx === -1 ? content.length : (startIdx + match[0].length + nextIdx);
1469
+
1470
+ const removedContent = (content.slice(0, startIdx) + content.slice(endIdx))
1471
+ .replace(/\n{3,}/g, lineEnding + lineEnding);
1472
+
1473
+ updatedContent = removedContent;
1474
+ }
1475
+
1476
+ if (updatedContent) {
1477
+ if (result.switched) {
1478
+ updatedContent = upsertTopLevel(updatedContent, 'model_provider', fallbackProvider);
1479
+ updatedContent = upsertTopLevel(updatedContent, 'model', fallbackModel);
1480
+ currentModels[fallbackProvider] = fallbackModel;
1481
+ }
1482
+ } else {
1483
+ // 回退:重建 TOML,保持行尾风格
1484
+ const rebuilt = JSON.parse(JSON.stringify(config));
1485
+ delete rebuilt.model_providers[name];
1486
+ if (result.switched) {
1487
+ rebuilt.model_provider = fallbackProvider;
1488
+ rebuilt.model = fallbackModel;
1489
+ currentModels[fallbackProvider] = fallbackModel;
1490
+ }
1491
+ const hasMarker = content.includes(CODEXMATE_MANAGED_MARKER);
1492
+ let rebuiltToml = toml.stringify(rebuilt).trimEnd();
1493
+ rebuiltToml = rebuiltToml.replace(/\n/g, lineEnding);
1494
+ if (hasMarker && !rebuiltToml.includes(CODEXMATE_MANAGED_MARKER)) {
1495
+ rebuiltToml = `${CODEXMATE_MANAGED_MARKER}${lineEnding}${rebuiltToml}`;
1496
+ }
1497
+ updatedContent = rebuiltToml + lineEnding;
1498
+ if (hasBom && updatedContent.charCodeAt(0) !== 0xFEFF) {
1499
+ updatedContent = '\uFEFF' + updatedContent;
1500
+ }
1501
+ }
1502
+
1503
+ writeCurrentModels(currentModels);
1504
+ writeConfig(updatedContent.trimEnd() + lineEnding);
1505
+
1506
+ return result;
1507
+ }
1508
+
1276
1509
  function ensureSupportFiles(defaultProvider, defaultModel) {
1277
1510
  if (!fs.existsSync(MODELS_FILE)) {
1278
1511
  writeModels([...DEFAULT_MODELS]);
@@ -1385,6 +1618,30 @@ function ensureManagedConfigBootstrap() {
1385
1618
  return { notice: g_initNotice };
1386
1619
  }
1387
1620
 
1621
+ function resetConfigToDefault() {
1622
+ ensureConfigDir();
1623
+ const initializedAt = new Date().toISOString();
1624
+ const defaultProvider = 'openai';
1625
+ const defaultModel = DEFAULT_MODELS[0] || 'gpt-4';
1626
+
1627
+ let backupFile = '';
1628
+ if (fs.existsSync(CONFIG_FILE)) {
1629
+ backupFile = `config.toml.reset-${formatTimestampForFileName(initializedAt)}.bak`;
1630
+ fs.copyFileSync(CONFIG_FILE, path.join(CONFIG_DIR, backupFile));
1631
+ }
1632
+
1633
+ writeConfig(buildDefaultConfigContent(initializedAt));
1634
+ ensureSupportFiles(defaultProvider, defaultModel);
1635
+ writeInitMark({
1636
+ version: 1,
1637
+ initializedAt,
1638
+ mode: 'manual-reset',
1639
+ backupFile
1640
+ });
1641
+
1642
+ return { success: true, backupFile };
1643
+ }
1644
+
1388
1645
  function consumeInitNotice() {
1389
1646
  const notice = g_initNotice;
1390
1647
  g_initNotice = '';
@@ -1530,17 +1787,17 @@ function isBootstrapLikeText(text) {
1530
1787
  return BOOTSTRAP_TEXT_MARKERS.some(marker => normalized.includes(marker));
1531
1788
  }
1532
1789
 
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';
1790
+ function removeLeadingSystemMessage(messages) {
1791
+ if (!Array.isArray(messages) || messages.length === 0) {
1792
+ return [];
1793
+ }
1794
+
1795
+ let startIndex = 0;
1796
+ while (startIndex < messages.length) {
1797
+ const item = messages[startIndex];
1798
+ const role = item ? normalizeRole(item.role) : '';
1799
+ const text = item && typeof item.text === 'string' ? item.text : '';
1800
+ const isSystemRole = role === 'system';
1544
1801
  const isBootstrapText = isBootstrapLikeText(text);
1545
1802
  if (!item || isSystemRole || isBootstrapText) {
1546
1803
  startIndex += 1;
@@ -1627,85 +1884,102 @@ function matchesSessionPathFilter(session, normalizedFilter) {
1627
1884
  return cwd.includes(normalizedFilter);
1628
1885
  }
1629
1886
 
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
- }
1887
+ function normalizeQueryTokens(query) {
1888
+ if (typeof query !== 'string') {
1889
+ return [];
1890
+ }
1891
+ return query
1892
+ .split(/\s+/)
1893
+ .map(item => item.trim())
1894
+ .map(item => item.toLowerCase())
1895
+ .filter(Boolean);
1896
+ }
1897
+
1898
+ function expandSessionQueryTokens(tokens) {
1899
+ const base = Array.isArray(tokens) ? tokens.map(t => String(t || '').toLowerCase()).filter(Boolean) : [];
1900
+ const result = [];
1901
+ const seen = new Set();
1902
+ let hasClaudeAlias = false;
1903
+ let hasDaudeAlias = false;
1904
+
1905
+ // First pass: detect multi-token aliases (e.g., "claude code", "daude code")
1906
+ for (let i = 0; i < base.length; i++) {
1907
+ const token = base[i];
1908
+ const nextToken = base[i + 1] || '';
1909
+
1910
+ // Check for "claude code" pattern (two separate tokens)
1911
+ if (token === 'claude' && nextToken === 'code') {
1912
+ hasClaudeAlias = true;
1913
+ i++; // Skip next token
1914
+ continue;
1915
+ }
1916
+ // Check for "daude code" pattern (two separate tokens)
1917
+ if (token === 'daude' && nextToken === 'code') {
1918
+ hasDaudeAlias = true;
1919
+ i++; // Skip next token
1920
+ continue;
1921
+ }
1922
+ // Check for combined patterns (e.g., "claude-code", "claude_code", "claudecode")
1923
+ if (/^claude[-_ ]?code$/.test(token) || token === 'claudecode') {
1924
+ hasClaudeAlias = true;
1925
+ continue;
1926
+ }
1927
+ if (/^daude[-_ ]?code$/.test(token) || token === 'daudecode') {
1928
+ hasDaudeAlias = true;
1929
+ continue;
1930
+ }
1931
+ if (!seen.has(token)) {
1932
+ seen.add(token);
1933
+ result.push(token);
1934
+ }
1935
+ }
1936
+
1937
+ const push = (token) => {
1938
+ const normalized = String(token || '').toLowerCase();
1939
+ if (!normalized || seen.has(normalized)) return;
1940
+ seen.add(normalized);
1941
+ result.push(normalized);
1942
+ };
1943
+
1944
+ if (hasClaudeAlias) {
1945
+ push('claude');
1946
+ push('code');
1947
+ }
1948
+ if (hasDaudeAlias) {
1949
+ push('daude');
1950
+ push('code');
1951
+ }
1952
+
1953
+ return result;
1954
+ }
1955
+
1956
+ function normalizeKeywords(value) {
1957
+ if (!Array.isArray(value)) {
1958
+ return [];
1959
+ }
1960
+ const seen = new Set();
1961
+ const result = [];
1962
+ for (const item of value) {
1963
+ const normalized = typeof item === 'string' ? item.trim() : String(item || '').trim();
1964
+ if (!normalized) continue;
1965
+ const lower = normalized.toLowerCase();
1966
+ if (seen.has(lower)) continue;
1967
+ seen.add(lower);
1968
+ result.push(normalized);
1969
+ }
1970
+ return result;
1971
+ }
1972
+
1973
+ function normalizeCapabilities(value) {
1974
+ const result = {};
1975
+ if (!value || typeof value !== 'object') {
1976
+ return result;
1977
+ }
1978
+ if (value.code === true) {
1979
+ result.code = true;
1980
+ }
1981
+ return result;
1982
+ }
1709
1983
 
1710
1984
  function normalizeQueryMode(mode) {
1711
1985
  return mode === 'or' ? 'or' : 'and';
@@ -1740,22 +2014,22 @@ function matchTokensInText(text, tokens, mode = 'and') {
1740
2014
  return tokens.every(token => haystack.includes(token));
1741
2015
  }
1742
2016
 
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
- }
2017
+ function buildSessionSummaryText(session) {
2018
+ if (!session) {
2019
+ return '';
2020
+ }
2021
+ const keywords = Array.isArray(session.keywords) ? session.keywords.join(' ') : '';
2022
+ const provider = typeof session.provider === 'string' ? session.provider : '';
2023
+ return [
2024
+ session.title,
2025
+ session.sessionId,
2026
+ session.cwd,
2027
+ session.filePath,
2028
+ session.sourceLabel,
2029
+ provider,
2030
+ keywords
2031
+ ].filter(Boolean).join(' ');
2032
+ }
1759
2033
 
1760
2034
  function extractMessageFromRecord(record, source) {
1761
2035
  if (!record) {
@@ -1865,39 +2139,39 @@ function applySessionQueryFilter(sessions, options = {}) {
1865
2139
  ? Math.max(1024, Number(options.contentScanBytes))
1866
2140
  : SESSION_CONTENT_READ_BYTES;
1867
2141
 
1868
- let scanned = 0;
1869
- const results = [];
1870
-
1871
- for (const session of sessions) {
1872
- if (scope === 'content' && scanned >= contentScanLimit) {
2142
+ let scanned = 0;
2143
+ const results = [];
2144
+
2145
+ for (const session of sessions) {
2146
+ if (scope === 'content' && scanned >= contentScanLimit) {
1873
2147
  break;
1874
2148
  }
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
- }
2149
+
2150
+ const summaryText = buildSessionSummaryText(session);
2151
+ const summaryHit = scope !== 'content' && matchTokensInText(summaryText, tokens, mode);
2152
+ let contentHit = false;
2153
+ let contentInfo = null;
2154
+
2155
+ const shouldScanContent = scope === 'content' || scope === 'all' || !summaryHit;
2156
+ if (shouldScanContent && scanned < contentScanLimit) {
2157
+ scanned += 1;
2158
+ contentInfo = scanSessionContentForQuery(session, tokens, {
2159
+ mode,
2160
+ roleFilter,
2161
+ maxBytes: contentScanBytes,
2162
+ maxMatches: 1,
2163
+ snippetLimit: 2
2164
+ });
2165
+ contentHit = contentInfo.hit;
2166
+ }
2167
+
2168
+ const hit = scope === 'summary'
2169
+ ? summaryHit
2170
+ : (scope === 'content' ? contentHit : (summaryHit || contentHit));
2171
+
2172
+ if (!hit) {
2173
+ continue;
2174
+ }
1901
2175
 
1902
2176
  const matchInfo = contentInfo && contentInfo.hit
1903
2177
  ? contentInfo
@@ -2072,26 +2346,26 @@ function parseCodexSessionSummary(filePath) {
2072
2346
  }
2073
2347
  }
2074
2348
 
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);
2349
+ messageCount = Math.max(0, messageCount);
2350
+
2351
+ return {
2352
+ source: 'codex',
2353
+ sourceLabel: 'Codex',
2354
+ provider: 'codex',
2355
+ sessionId,
2356
+ title: firstPrompt || sessionId,
2357
+ cwd,
2358
+ createdAt,
2359
+ updatedAt,
2360
+ messageCount,
2361
+ filePath,
2362
+ keywords: [],
2363
+ capabilities: {}
2364
+ };
2365
+ }
2366
+
2367
+ function parseClaudeSessionSummary(filePath) {
2368
+ const records = parseJsonlHeadRecords(filePath);
2095
2369
  if (records.length === 0) {
2096
2370
  return null;
2097
2371
  }
@@ -2161,23 +2435,23 @@ function parseClaudeSessionSummary(filePath) {
2161
2435
  }
2162
2436
  }
2163
2437
 
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
- }
2438
+ messageCount = Math.max(0, messageCount);
2439
+
2440
+ return {
2441
+ source: 'claude',
2442
+ sourceLabel: 'Claude Code',
2443
+ provider: 'claude',
2444
+ sessionId,
2445
+ title: firstPrompt || sessionId,
2446
+ cwd,
2447
+ createdAt,
2448
+ updatedAt,
2449
+ messageCount,
2450
+ filePath,
2451
+ keywords: [],
2452
+ capabilities: { code: true }
2453
+ };
2454
+ }
2181
2455
 
2182
2456
  function listCodexSessions(limit, options = {}) {
2183
2457
  const codexSessionsDir = getCodexSessionsDir();
@@ -2278,12 +2552,12 @@ function listClaudeSessions(limit, options = {}) {
2278
2552
  let title = truncateText(entry.summary || entry.firstPrompt || sessionId, 120);
2279
2553
  let messageCount = Number.isFinite(entry.messageCount) ? Math.max(0, entry.messageCount - 1) : 0;
2280
2554
 
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
- }
2555
+ const quickRecords = parseJsonlHeadRecords(filePath, SESSION_TITLE_READ_BYTES);
2556
+ if (quickRecords.length > 0) {
2557
+ const filteredCount = countConversationMessagesInRecords(quickRecords, 'claude');
2558
+ if (filteredCount > 0 || messageCount === 0) {
2559
+ messageCount = filteredCount;
2560
+ }
2287
2561
 
2288
2562
  const quickMessages = [];
2289
2563
  for (const record of quickRecords) {
@@ -2292,38 +2566,38 @@ function listClaudeSessions(limit, options = {}) {
2292
2566
  const content = record.message ? record.message.content : '';
2293
2567
  quickMessages.push({ role, text: extractMessageText(content) });
2294
2568
  }
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
- }
2569
+ }
2570
+ const filteredQuickMessages = removeLeadingSystemMessage(quickMessages);
2571
+ const firstUser = filteredQuickMessages.find(item => item.role === 'user' && item.text);
2572
+ if (firstUser) {
2573
+ title = truncateText(firstUser.text, 120);
2574
+ }
2575
+ }
2576
+
2577
+ const provider = typeof entry.provider === 'string' && entry.provider.trim()
2578
+ ? entry.provider.trim()
2579
+ : 'claude';
2580
+ const keywords = normalizeKeywords(entry.keywords);
2581
+ const capabilities = normalizeCapabilities(entry.capabilities);
2582
+
2583
+ sessions.push({
2584
+ source: 'claude',
2585
+ sourceLabel: 'Claude Code',
2586
+ provider,
2587
+ sessionId,
2588
+ title,
2589
+ cwd: entry.projectPath || index.originalPath || '',
2590
+ createdAt,
2591
+ updatedAt,
2592
+ messageCount,
2593
+ filePath,
2594
+ keywords,
2595
+ capabilities
2596
+ });
2597
+
2598
+ if (sessions.length >= targetCount) {
2599
+ break;
2600
+ }
2327
2601
  }
2328
2602
 
2329
2603
  if (sessions.length >= targetCount) {
@@ -2356,15 +2630,15 @@ function listAllSessions(params = {}) {
2356
2630
  const source = params.source === 'codex' || params.source === 'claude'
2357
2631
  ? params.source
2358
2632
  : '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;
2633
+ const rawLimit = Number(params.limit);
2634
+ const limit = Number.isFinite(rawLimit)
2635
+ ? Math.max(1, Math.min(rawLimit, MAX_SESSION_LIST_SIZE))
2636
+ : 120;
2637
+ const forceRefresh = !!params.forceRefresh;
2638
+ const normalizedPathFilter = normalizeSessionPathFilter(params.pathFilter);
2639
+ const hasPathFilter = !!normalizedPathFilter;
2640
+ const queryTokens = expandSessionQueryTokens(normalizeQueryTokens(params.query));
2641
+ const hasQuery = queryTokens.length > 0;
2368
2642
  const cacheKey = hasQuery ? '' : `${source}:${limit}:${normalizedPathFilter}`;
2369
2643
  if (!hasQuery) {
2370
2644
  const cached = getSessionListCache(cacheKey, forceRefresh);
@@ -2381,16 +2655,16 @@ function listAllSessions(params = {}) {
2381
2655
  : {};
2382
2656
 
2383
2657
  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
- }
2658
+ if (source === 'all' || source === 'codex') {
2659
+ sessions = sessions.concat(listCodexSessions(limit, scanOptions));
2660
+ }
2661
+ if (source === 'all' || source === 'claude') {
2662
+ sessions = sessions.concat(listClaudeSessions(limit, scanOptions));
2663
+ }
2664
+
2665
+ if (hasPathFilter) {
2666
+ sessions = sessions.filter(item => matchesSessionPathFilter(item, normalizedPathFilter));
2667
+ }
2394
2668
 
2395
2669
  let result = sessions;
2396
2670
  if (hasQuery) {
@@ -2411,15 +2685,17 @@ function listAllSessions(params = {}) {
2411
2685
  }
2412
2686
 
2413
2687
  function listSessionPaths(params = {}) {
2414
- const source = params.source === 'codex' || params.source === 'claude'
2415
- ? params.source
2416
- : 'all';
2688
+ const source = typeof params.source === 'string' ? params.source.trim().toLowerCase() : '';
2689
+ if (source && source !== 'codex' && source !== 'claude' && source !== 'all') {
2690
+ return [];
2691
+ }
2692
+ const validSource = source === 'codex' || source === 'claude' ? source : 'all';
2417
2693
  const rawLimit = Number(params.limit);
2418
2694
  const limit = Number.isFinite(rawLimit)
2419
2695
  ? Math.max(1, Math.min(rawLimit, MAX_SESSION_PATH_LIST_SIZE))
2420
2696
  : 500;
2421
2697
  const forceRefresh = !!params.forceRefresh;
2422
- const cacheKey = `paths:${source}:${limit}`;
2698
+ const cacheKey = `paths:${validSource}:${limit}`;
2423
2699
  const cached = getSessionListCache(cacheKey, forceRefresh);
2424
2700
  if (cached) {
2425
2701
  return cached;
@@ -2433,10 +2709,10 @@ function listSessionPaths(params = {}) {
2433
2709
  };
2434
2710
 
2435
2711
  let sessions = [];
2436
- if (source === 'all' || source === 'codex') {
2712
+ if (validSource === 'all' || validSource === 'codex') {
2437
2713
  sessions = sessions.concat(listCodexSessions(gatherLimit, scanOptions));
2438
2714
  }
2439
- if (source === 'all' || source === 'claude') {
2715
+ if (validSource === 'all' || validSource === 'claude') {
2440
2716
  sessions = sessions.concat(listClaudeSessions(gatherLimit, scanOptions));
2441
2717
  }
2442
2718
 
@@ -2477,15 +2753,15 @@ function resolveSessionFilePath(source, filePath, sessionId) {
2477
2753
  }
2478
2754
  }
2479
2755
 
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
-
2756
+ if (typeof sessionId === 'string' && sessionId.trim()) {
2757
+ const targetId = sessionId.trim().toLowerCase();
2758
+ const files = collectJsonlFiles(root, 5000);
2759
+ const matchedFile = files.find(item => path.basename(item, '.jsonl').toLowerCase() === targetId);
2760
+ if (matchedFile && fs.existsSync(matchedFile)) {
2761
+ return matchedFile;
2762
+ }
2763
+ }
2764
+
2489
2765
  return '';
2490
2766
  }
2491
2767
 
@@ -3256,6 +3532,10 @@ function normalizeImportPayload(payload) {
3256
3532
  }
3257
3533
  }
3258
3534
 
3535
+ if (Object.keys(providers).length === 0 && (!payload.models || payload.models.length === 0)) {
3536
+ return { error: 'Invalid import payload' };
3537
+ }
3538
+
3259
3539
  return {
3260
3540
  providers,
3261
3541
  models: Array.isArray(payload.models) ? payload.models : [],
@@ -3874,36 +4154,15 @@ stream_idle_timeout_ms = 300000
3874
4154
 
3875
4155
  // 删除提供商
3876
4156
  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('无法找到提供商配置块');
4157
+ const res = performProviderDeletion(name, { silent });
4158
+ if (res.error) {
4159
+ throw new Error(res.error);
3890
4160
  }
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
4161
  if (!silent) {
3906
4162
  console.log('✓ 已删除提供商:', name);
4163
+ if (res.switched && res.provider) {
4164
+ console.log(` 已自动切换到 provider: ${res.provider},model: ${res.model || '(未设置)'}`);
4165
+ }
3907
4166
  console.log();
3908
4167
  }
3909
4168
  }
@@ -4104,7 +4363,7 @@ function readClaudeSettingsInfo() {
4104
4363
  return {
4105
4364
  error: readResult.error || '读取 Claude 配置失败',
4106
4365
  exists: !!readResult.exists,
4107
- path: CLAUDE_SETTINGS_FILE
4366
+ targetPath: CLAUDE_SETTINGS_FILE
4108
4367
  };
4109
4368
  }
4110
4369
 
@@ -4115,7 +4374,7 @@ function readClaudeSettingsInfo() {
4115
4374
 
4116
4375
  return {
4117
4376
  exists: !!readResult.exists,
4118
- path: CLAUDE_SETTINGS_FILE,
4377
+ targetPath: CLAUDE_SETTINGS_FILE,
4119
4378
  apiKey: typeof env.ANTHROPIC_API_KEY === 'string' ? env.ANTHROPIC_API_KEY : '',
4120
4379
  baseUrl: typeof env.ANTHROPIC_BASE_URL === 'string' ? env.ANTHROPIC_BASE_URL : '',
4121
4380
  model: typeof env.ANTHROPIC_MODEL === 'string' ? env.ANTHROPIC_MODEL : '',
@@ -4170,7 +4429,7 @@ function cmdClaude(baseUrl, apiKey, model, silent = false) {
4170
4429
 
4171
4430
  function commandExists(command, args = '') {
4172
4431
  try {
4173
- execSync(`${command} ${args}`, { stdio: 'ignore' });
4432
+ execSync(`${command} ${args}`, { stdio: 'ignore', shell: process.platform === 'win32' });
4174
4433
  return true;
4175
4434
  } catch (e) {
4176
4435
  return false;
@@ -4568,16 +4827,52 @@ function formatHostForUrl(host) {
4568
4827
  return value;
4569
4828
  }
4570
4829
 
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);
4830
+ function watchPathsForRestart(targets, onChange) {
4831
+ const disposers = [];
4832
+ const debounceMs = 300;
4833
+ let timer = null;
4834
+
4835
+ const trigger = (info) => {
4836
+ if (timer) clearTimeout(timer);
4837
+ timer = setTimeout(() => {
4838
+ timer = null;
4839
+ onChange(info);
4840
+ }, debounceMs);
4841
+ };
4842
+
4843
+ const addWatcher = (target, recursive) => {
4844
+ if (!fs.existsSync(target)) return;
4845
+ try {
4846
+ const watcher = fs.watch(target, { recursive }, (eventType, filename) => {
4847
+ if (!filename) return;
4848
+ const lower = filename.toLowerCase();
4849
+ if (!(/\.(html|js|mjs|css)$/.test(lower))) return;
4850
+ trigger({ target, eventType, filename });
4851
+ });
4852
+ disposers.push(() => watcher.close());
4853
+ return true;
4854
+ } catch (e) {
4855
+ return false;
4856
+ }
4857
+ };
4858
+
4859
+ for (const target of targets) {
4860
+ const ok = addWatcher(target, true);
4861
+ if (!ok) {
4862
+ addWatcher(target, false);
4863
+ }
4579
4864
  }
4580
4865
 
4866
+ return () => {
4867
+ for (const dispose of disposers) {
4868
+ try { dispose(); } catch (_) {}
4869
+ }
4870
+ };
4871
+ }
4872
+
4873
+ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser }) {
4874
+ const connections = new Set();
4875
+
4581
4876
  const server = http.createServer((req, res) => {
4582
4877
  const requestPath = (req.url || '/').split('?')[0];
4583
4878
  if (requestPath === '/api') {
@@ -4593,10 +4888,12 @@ function cmdStart(options = {}) {
4593
4888
  const statusConfigResult = readConfigOrVirtualDefault();
4594
4889
  const config = statusConfigResult.config;
4595
4890
  const serviceTier = typeof config.service_tier === 'string' ? config.service_tier.trim() : '';
4891
+ const modelReasoningEffort = typeof config.model_reasoning_effort === 'string' ? config.model_reasoning_effort.trim() : '';
4596
4892
  result = {
4597
4893
  provider: config.model_provider || '未设置',
4598
4894
  model: config.model || '未设置',
4599
4895
  serviceTier,
4896
+ modelReasoningEffort,
4600
4897
  configReady: !statusConfigResult.isVirtual,
4601
4898
  configNotice: statusConfigResult.reason || '',
4602
4899
  initNotice: consumeInitNotice()
@@ -4621,13 +4918,17 @@ function cmdStart(options = {}) {
4621
4918
  case 'models':
4622
4919
  {
4623
4920
  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 };
4921
+ if (!providerName) {
4922
+ result = { error: 'Provider name is required' };
4629
4923
  } else {
4630
- result = { models: res.models || [], source: 'remote', provider: res.provider || '' };
4924
+ const res = await fetchProviderModels(providerName);
4925
+ if (res.error) {
4926
+ result = { error: res.error, models: [], source: 'remote' };
4927
+ } else if (res.unlimited) {
4928
+ result = { models: [], source: 'remote', provider: res.provider || '', unlimited: true };
4929
+ } else {
4930
+ result = { models: res.models || [], source: 'remote', provider: res.provider || '' };
4931
+ }
4631
4932
  }
4632
4933
  }
4633
4934
  break;
@@ -4635,13 +4936,17 @@ function cmdStart(options = {}) {
4635
4936
  {
4636
4937
  const baseUrl = params && typeof params.baseUrl === 'string' ? params.baseUrl : '';
4637
4938
  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 };
4939
+ if (!baseUrl) {
4940
+ result = { error: 'Base URL is required' };
4643
4941
  } else {
4644
- result = { models: res.models || [], source: 'remote' };
4942
+ const res = await fetchModelsFromBaseUrl(baseUrl, apiKey);
4943
+ if (res.error) {
4944
+ result = { error: res.error, models: [], source: 'remote' };
4945
+ } else if (res.unlimited) {
4946
+ result = { models: [], source: 'remote', unlimited: true };
4947
+ } else {
4948
+ result = { models: res.models || [], source: 'remote' };
4949
+ }
4645
4950
  }
4646
4951
  }
4647
4952
  break;
@@ -4651,6 +4956,15 @@ function cmdStart(options = {}) {
4651
4956
  case 'apply-config-template':
4652
4957
  result = applyConfigTemplate(params || {});
4653
4958
  break;
4959
+ case 'add-provider':
4960
+ result = addProviderToConfig(params || {});
4961
+ break;
4962
+ case 'update-provider':
4963
+ result = updateProviderInConfig(params || {});
4964
+ break;
4965
+ case 'delete-provider':
4966
+ result = deleteProviderFromConfig(params || {});
4967
+ break;
4654
4968
  case 'get-recent-configs':
4655
4969
  result = { items: readRecentConfigs() };
4656
4970
  break;
@@ -4669,6 +4983,9 @@ function cmdStart(options = {}) {
4669
4983
  case 'apply-openclaw-config':
4670
4984
  result = applyOpenclawConfig(params || {});
4671
4985
  break;
4986
+ case 'reset-config':
4987
+ result = resetConfigToDefault();
4988
+ break;
4672
4989
  case 'get-openclaw-agents-file':
4673
4990
  result = readOpenclawAgentsFile();
4674
4991
  break;
@@ -4726,14 +5043,29 @@ function cmdStart(options = {}) {
4726
5043
  break;
4727
5044
  }
4728
5045
  case 'list-sessions':
4729
- result = {
4730
- sessions: listAllSessions(params)
4731
- };
5046
+ {
5047
+ const source = typeof params.source === 'string' ? params.source.trim().toLowerCase() : '';
5048
+ if (source && source !== 'codex' && source !== 'claude' && source !== 'all') {
5049
+ result = { error: 'Invalid source. Must be codex, claude, or all' };
5050
+ } else {
5051
+ result = {
5052
+ sessions: listAllSessions(params),
5053
+ source: source || 'all'
5054
+ };
5055
+ }
5056
+ }
4732
5057
  break;
4733
5058
  case 'list-session-paths':
4734
- result = {
4735
- paths: listSessionPaths(params)
4736
- };
5059
+ {
5060
+ const source = typeof params.source === 'string' ? params.source.trim().toLowerCase() : '';
5061
+ if (source && source !== 'codex' && source !== 'claude' && source !== 'all') {
5062
+ result = { error: 'Invalid source. Must be codex, claude, or all' };
5063
+ } else {
5064
+ result = {
5065
+ paths: listSessionPaths(params)
5066
+ };
5067
+ }
5068
+ }
4737
5069
  break;
4738
5070
  case 'export-session':
4739
5071
  result = await exportSessionData(params);
@@ -4754,11 +5086,19 @@ function cmdStart(options = {}) {
4754
5086
  result = { error: '未知操作' };
4755
5087
  }
4756
5088
 
4757
- res.writeHead(200, { 'Content-Type': 'application/json' });
4758
- res.end(JSON.stringify(result));
5089
+ const responseBody = JSON.stringify(result, null, 2);
5090
+ res.writeHead(200, {
5091
+ 'Content-Type': 'application/json; charset=utf-8',
5092
+ 'Content-Length': Buffer.byteLength(responseBody, 'utf-8')
5093
+ });
5094
+ res.end(responseBody, 'utf-8');
4759
5095
  } catch (e) {
4760
- res.writeHead(500, { 'Content-Type': 'application/json' });
4761
- res.end(JSON.stringify({ error: e.message }));
5096
+ const errorBody = JSON.stringify({ error: e.message }, null, 2);
5097
+ res.writeHead(500, {
5098
+ 'Content-Type': 'application/json; charset=utf-8',
5099
+ 'Content-Length': Buffer.byteLength(errorBody, 'utf-8')
5100
+ });
5101
+ res.end(errorBody, 'utf-8');
4762
5102
  }
4763
5103
  });
4764
5104
  } else if (requestPath.startsWith('/web-ui/')) {
@@ -4777,6 +5117,8 @@ function cmdStart(options = {}) {
4777
5117
  const ext = path.extname(filePath).toLowerCase();
4778
5118
  const mime = ext === '.js' || ext === '.mjs'
4779
5119
  ? 'application/javascript; charset=utf-8'
5120
+ : ext === '.html'
5121
+ ? 'text/html; charset=utf-8'
4780
5122
  : ext === '.css'
4781
5123
  ? 'text/css; charset=utf-8'
4782
5124
  : ext === '.json'
@@ -4800,6 +5142,8 @@ function cmdStart(options = {}) {
4800
5142
  const ext = path.extname(filePath).toLowerCase();
4801
5143
  const mime = ext === '.js'
4802
5144
  ? 'application/javascript; charset=utf-8'
5145
+ : ext === '.html'
5146
+ ? 'text/html; charset=utf-8'
4803
5147
  : ext === '.json'
4804
5148
  ? 'application/json; charset=utf-8'
4805
5149
  : 'application/octet-stream';
@@ -4812,8 +5156,21 @@ function cmdStart(options = {}) {
4812
5156
  }
4813
5157
  });
4814
5158
 
4815
- const port = resolveWebPort();
4816
- const host = resolveWebHost(options);
5159
+ server.on('connection', (socket) => {
5160
+ connections.add(socket);
5161
+ socket.on('close', () => connections.delete(socket));
5162
+ });
5163
+
5164
+ server.once('error', (err) => {
5165
+ if (err && err.code === 'EADDRINUSE') {
5166
+ console.error(`! 启动失败: 端口 ${port} 已被占用,可能有残留的 codexmate run 实例。`);
5167
+ console.error(' 请先停止旧实例或更换端口后重试。');
5168
+ } else {
5169
+ console.error('! 启动 Web UI 失败:', err && err.message ? err.message : err);
5170
+ }
5171
+ process.exit(1);
5172
+ });
5173
+
4817
5174
  const openHost = isAnyAddressHost(host) ? DEFAULT_WEB_HOST : host;
4818
5175
  const openUrl = `http://${formatHostForUrl(openHost)}:${port}`;
4819
5176
  server.listen(port, host, () => {
@@ -4827,41 +5184,155 @@ function cmdStart(options = {}) {
4827
5184
  console.warn(' 建议仅在可信网络使用,或改用 --host 127.0.0.1。');
4828
5185
  }
4829
5186
 
4830
- // 打开浏览器
4831
- const platform = process.platform;
4832
- let command;
4833
- const url = openUrl;
5187
+ if (!process.env.CODEXMATE_NO_BROWSER && openBrowser) {
5188
+ const platform = process.platform;
5189
+ let command;
5190
+ const url = openUrl;
4834
5191
 
4835
- if (platform === 'win32') {
4836
- command = `start "" "${url}"`;
4837
- } else if (platform === 'darwin') {
4838
- command = `open "${url}"`;
4839
- } else {
4840
- command = `xdg-open "${url}"`;
4841
- }
5192
+ if (platform === 'win32') {
5193
+ command = `start \"\" \"${url}\"`;
5194
+ } else if (platform === 'darwin') {
5195
+ command = `open \"${url}\"`;
5196
+ } else {
5197
+ command = `xdg-open \"${url}\"`;
5198
+ }
4842
5199
 
4843
- const disableBrowser = process.env.CODEXMATE_NO_BROWSER === '1';
4844
- if (!disableBrowser) {
4845
5200
  exec(command, (error) => {
4846
5201
  if (error) console.warn('无法自动打开浏览器,请手动访问:', url);
4847
5202
  });
4848
5203
  }
4849
5204
  });
5205
+
5206
+ const stop = () => new Promise((resolve) => {
5207
+ let done = false;
5208
+ const finish = () => {
5209
+ if (done) return;
5210
+ done = true;
5211
+ for (const socket of connections) {
5212
+ try { socket.destroy(); } catch (_) {}
5213
+ }
5214
+ connections.clear();
5215
+ resolve();
5216
+ };
5217
+
5218
+ if (!server.listening) {
5219
+ finish();
5220
+ return;
5221
+ }
5222
+
5223
+ server.close(() => finish());
5224
+ setTimeout(() => finish(), 800);
5225
+ });
5226
+
5227
+ return { server, stop };
4850
5228
  }
4851
5229
 
4852
- async function cmdCodex(args = []) {
5230
+ // 打开 Web UI
5231
+ function cmdStart(options = {}) {
5232
+ // Support both new src/ structure and legacy root structure for zero breaking changes
5233
+ const webDirLegacy = path.join(__dirname, 'web-ui');
5234
+ const webDirSrc = path.join(__dirname, 'src', 'web-ui');
5235
+ const webDir = fs.existsSync(webDirSrc) ? webDirSrc : webDirLegacy;
5236
+ const newHtmlPath = path.join(webDir, 'index.html');
5237
+ const legacyHtmlPath = path.join(__dirname, 'web-ui.html');
5238
+ const srcHtmlPath = path.join(__dirname, 'src', 'web-ui.html');
5239
+
5240
+ let htmlPath = newHtmlPath;
5241
+ if (!fs.existsSync(newHtmlPath)) {
5242
+ htmlPath = fs.existsSync(srcHtmlPath) ? srcHtmlPath : legacyHtmlPath;
5243
+ }
5244
+ const assetsDirLegacy = path.join(__dirname, 'res');
5245
+ const assetsDirSrc = path.join(__dirname, 'src', 'res');
5246
+ const assetsDir = fs.existsSync(assetsDirSrc) ? assetsDirSrc : assetsDirLegacy;
5247
+ if (!fs.existsSync(htmlPath)) {
5248
+ console.error('错误: Web UI 页面不存在(尝试路径: web-ui/index.html, web-ui.html)');
5249
+ process.exit(1);
5250
+ }
5251
+
5252
+ const port = resolveWebPort();
5253
+ const host = resolveWebHost(options);
5254
+
5255
+ let serverHandle = createWebServer({
5256
+ htmlPath,
5257
+ assetsDir,
5258
+ webDir,
5259
+ host,
5260
+ port,
5261
+ openBrowser: true
5262
+ });
5263
+
5264
+ const stopWatch = watchPathsForRestart(
5265
+ [webDir, path.join(__dirname, 'web-ui.html'), path.join(__dirname, 'src', 'web-ui.html')],
5266
+ async (info) => {
5267
+ const fileLabel = info && info.filename ? info.filename : (info && info.target ? path.basename(info.target) : 'unknown');
5268
+ console.log(`\n~ 侦测到前端变更 (${fileLabel}),重启中...`);
5269
+ console.log(' 正在停止旧服务...');
5270
+ try {
5271
+ await serverHandle.stop();
5272
+ console.log(' 旧服务已停止');
5273
+ } catch (e) {
5274
+ console.warn('! 停止旧服务失败:', e.message || e);
5275
+ }
5276
+ await new Promise((resolve) => setTimeout(resolve, 80));
5277
+ try {
5278
+ serverHandle = createWebServer({
5279
+ htmlPath,
5280
+ assetsDir,
5281
+ webDir,
5282
+ host,
5283
+ port,
5284
+ openBrowser: false
5285
+ });
5286
+ console.log('✓ 已重启 Web UI 服务\n');
5287
+ } catch (e) {
5288
+ console.error('! 重启失败:', e.message || e);
5289
+ }
5290
+ }
5291
+ );
5292
+
5293
+ const handleExit = () => {
5294
+ stopWatch();
5295
+ serverHandle.stop().then(() => process.exit(0));
5296
+ };
5297
+
5298
+ process.on('SIGINT', handleExit);
5299
+ process.on('SIGTERM', handleExit);
5300
+ }
5301
+
5302
+ async function runProxyCommand(displayName, binNames, args = [], installTip = '') {
4853
5303
  const extraArgs = Array.isArray(args) ? args.filter(arg => arg !== undefined) : [];
4854
5304
  const hasYolo = extraArgs.includes('--yolo');
4855
5305
  const finalArgs = hasYolo ? extraArgs : ['--yolo', ...extraArgs];
4856
5306
 
5307
+ const names = Array.isArray(binNames) ? binNames : [binNames];
5308
+ let selectedBin = names[0];
5309
+ let exists = false;
5310
+
5311
+ // Detect if any of the bin names exist
5312
+ for (const name of names) {
5313
+ if (commandExists(name, '--version')) {
5314
+ selectedBin = name;
5315
+ exists = true;
5316
+ break;
5317
+ }
5318
+ }
5319
+
5320
+ if (!exists) {
5321
+ let msg = `无法启动 ${displayName},请确认已安装并在 PATH 中。`;
5322
+ if (installTip) {
5323
+ msg += `\n安装建议: ${installTip}`;
5324
+ }
5325
+ throw new Error(msg);
5326
+ }
5327
+
4857
5328
  return new Promise((resolve, reject) => {
4858
- const child = spawn('codex', finalArgs, {
5329
+ const child = spawn(selectedBin, finalArgs, {
4859
5330
  stdio: 'inherit',
4860
5331
  shell: process.platform === 'win32'
4861
5332
  });
4862
5333
 
4863
5334
  child.on('error', (err) => {
4864
- reject(new Error(`无法启动 codex,请确认已安装并在 PATH 中: ${err.message}`));
5335
+ reject(new Error(`运行 ${selectedBin} 失败: ${err.message}`));
4865
5336
  });
4866
5337
 
4867
5338
  child.on('exit', (code, signal) => {
@@ -4882,6 +5353,18 @@ async function cmdCodex(args = []) {
4882
5353
  });
4883
5354
  }
4884
5355
 
5356
+ async function cmdCodex(args = []) {
5357
+ return runProxyCommand('Codex', 'codex', args);
5358
+ }
5359
+
5360
+ async function cmdQwen(args = []) {
5361
+ return runProxyCommand('Qwen', ['qwen', 'qwen-code'], args, 'npm install -g @qwen-code/qwen-code');
5362
+ }
5363
+
5364
+ async function cmdGemini(args = []) {
5365
+ return runProxyCommand('Gemini', ['gemini', 'gemini-cli'], args, 'npm install -g @google/gemini-cli');
5366
+ }
5367
+
4885
5368
  // ============================================================================
4886
5369
  // 主程序
4887
5370
  // ============================================================================
@@ -4908,6 +5391,8 @@ async function main() {
4908
5391
  console.log(' codexmate delete-model <模型> 删除模型');
4909
5392
  console.log(' codexmate run [--host <HOST>] 启动 Web 界面');
4910
5393
  console.log(' codexmate codex [参数...] 等同于 codex --yolo');
5394
+ console.log(' codexmate qwen [参数...] 等同于 qwen --yolo');
5395
+ console.log(' codexmate gemini [参数...] 等同于 gemini --yolo');
4911
5396
  console.log(' codexmate export-session --source <codex|claude> (--session-id <ID>|--file <PATH>) [--output <PATH>] [--max-messages <N|all|Infinity>]');
4912
5397
  console.log(' codexmate zip <路径> [--max:级别] 压缩(7-Zip 优先)');
4913
5398
  console.log(' codexmate unzip <zip文件> [输出目录] 解压(7-Zip 优先)');
@@ -4939,6 +5424,16 @@ async function main() {
4939
5424
  process.exit(exitCode);
4940
5425
  break;
4941
5426
  }
5427
+ case 'qwen': {
5428
+ const exitCode = await cmdQwen(args.slice(1));
5429
+ process.exit(exitCode);
5430
+ break;
5431
+ }
5432
+ case 'gemini': {
5433
+ const exitCode = await cmdGemini(args.slice(1));
5434
+ process.exit(exitCode);
5435
+ break;
5436
+ }
4942
5437
  case 'export-session': await cmdExportSession(args.slice(1)); break;
4943
5438
  case 'zip': {
4944
5439
  // 解析 --max:N 参数