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.
- package/README.md +29 -11
- package/README.zh-CN.md +29 -11
- package/package.json +53 -36
- package/res/logo.png +0 -0
- package/{cli.js → src/cli.js} +822 -327
- package/src/lib/cli-file-utils.js +151 -0
- package/src/lib/cli-models-utils.js +152 -0
- package/src/lib/cli-network-utils.js +148 -0
- package/src/lib/cli-session-utils.js +121 -0
- package/src/lib/cli-utils.js +139 -0
- package/src/res/json5.min.js +1 -0
- package/src/res/logo.png +0 -0
- package/src/res/screenshot.png +0 -0
- package/src/res/vue.global.js +18552 -0
- package/src/web-ui/app.js +2970 -0
- package/src/web-ui/index.html +1310 -0
- package/src/web-ui/logic.mjs +157 -0
- package/src/web-ui/styles.css +2868 -0
- package/src/web-ui.html +17 -0
- package/web-ui/app.js +273 -144
- package/web-ui/index.html +1310 -0
- package/web-ui/logic.mjs +21 -21
- package/web-ui/styles.css +2868 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -27
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -17
- package/.github/workflows/ci.yml +0 -26
- package/.github/workflows/release.yml +0 -159
- package/.planning/.fix-attempts +0 -1
- package/.planning/.lock +0 -6
- package/.planning/.verify-cache.json +0 -14
- package/.planning/CHECKPOINT.json +0 -46
- package/.planning/DESIGN.md +0 -26
- package/.planning/HISTORY.json +0 -124
- package/.planning/PLAN.md +0 -69
- package/.planning/REVIEW.md +0 -41
- package/.planning/STATE.md +0 -12
- package/.planning/STATS.json +0 -13
- package/.planning/VERIFICATION.md +0 -70
- package/.planning/daude-code-plan.md +0 -51
- package/.planning/research/architecture.md +0 -32
- package/.planning/research/conventions.md +0 -36
- package/.planning/task_1-REVIEW.md +0 -29
- package/.planning/task_1-SUMMARY.md +0 -32
- package/.planning/task_2-REVIEW.md +0 -24
- package/.planning/task_2-SUMMARY.md +0 -37
- package/.planning/task_3-REVIEW.md +0 -25
- package/.planning/task_3-SUMMARY.md +0 -31
- package/cmd/publish-npm.cmd +0 -65
- package/tests/e2e/helpers.js +0 -214
- package/tests/e2e/recent-health.e2e.js +0 -142
- package/tests/e2e/run.js +0 -154
- package/tests/e2e/test-claude.js +0 -21
- package/tests/e2e/test-config.js +0 -124
- package/tests/e2e/test-health-speed.js +0 -79
- package/tests/e2e/test-openclaw.js +0 -47
- package/tests/e2e/test-session-search.js +0 -114
- package/tests/e2e/test-sessions.js +0 -69
- package/tests/e2e/test-setup.js +0 -159
- package/tests/unit/run.mjs +0 -29
- package/tests/unit/web-ui-logic.test.mjs +0 -186
- package/web-ui.html +0 -3977
- /package/{CHANGELOG.md → doc/CHANGELOG.md} +0 -0
- /package/{CHANGELOG.zh-CN.md → doc/CHANGELOG.zh-CN.md} +0 -0
package/{cli.js → src/cli.js}
RENAMED
|
@@ -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
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
}
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
return result;
|
|
1697
|
-
}
|
|
1698
|
-
|
|
1699
|
-
function
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
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 === '
|
|
2415
|
-
|
|
2416
|
-
|
|
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:${
|
|
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 (
|
|
2712
|
+
if (validSource === 'all' || validSource === 'codex') {
|
|
2437
2713
|
sessions = sessions.concat(listCodexSessions(gatherLimit, scanOptions));
|
|
2438
2714
|
}
|
|
2439
|
-
if (
|
|
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
|
|
3878
|
-
if (
|
|
3879
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4572
|
-
|
|
4573
|
-
const
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
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
|
-
|
|
4625
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4639
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4730
|
-
|
|
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
|
-
|
|
4735
|
-
|
|
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
|
-
|
|
4758
|
-
res.
|
|
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
|
-
|
|
4761
|
-
res.
|
|
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
|
-
|
|
4816
|
-
|
|
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
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
5187
|
+
if (!process.env.CODEXMATE_NO_BROWSER && openBrowser) {
|
|
5188
|
+
const platform = process.platform;
|
|
5189
|
+
let command;
|
|
5190
|
+
const url = openUrl;
|
|
4834
5191
|
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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 参数
|