codexmate 0.0.5 → 0.0.6
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/.github/workflows/ci.yml +26 -0
- package/CHANGELOG.md +7 -0
- package/CHANGELOG.zh-CN.md +7 -0
- package/README.md +37 -29
- package/README.zh-CN.md +37 -29
- package/cli.js +1132 -293
- package/package.json +4 -3
- package/tests/e2e/recent-health.e2e.js +136 -135
- package/tests/e2e/run.js +63 -0
- package/web-ui.html +2151 -210
package/cli.js
CHANGED
|
@@ -5,12 +5,14 @@ const os = require('os');
|
|
|
5
5
|
const crypto = require('crypto');
|
|
6
6
|
const toml = require('@iarna/toml');
|
|
7
7
|
const JSON5 = require('json5');
|
|
8
|
+
const zipLib = require('zip-lib');
|
|
8
9
|
const { exec, execSync } = require('child_process');
|
|
9
10
|
const http = require('http');
|
|
10
11
|
const https = require('https');
|
|
11
12
|
const readline = require('readline');
|
|
12
13
|
|
|
13
|
-
const DEFAULT_WEB_PORT = 3737;
|
|
14
|
+
const DEFAULT_WEB_PORT = 3737;
|
|
15
|
+
const DEFAULT_WEB_HOST = '127.0.0.1';
|
|
14
16
|
|
|
15
17
|
// ============================================================================
|
|
16
18
|
// 配置
|
|
@@ -45,15 +47,15 @@ const SESSION_CONTENT_READ_BYTES = SESSION_SUMMARY_READ_BYTES;
|
|
|
45
47
|
const DEFAULT_CONTENT_SCAN_LIMIT = 10;
|
|
46
48
|
const SESSION_SCAN_FACTOR = 4;
|
|
47
49
|
const SESSION_SCAN_MIN_FILES = 800;
|
|
48
|
-
const MAX_SESSION_PATH_LIST_SIZE = 2000;
|
|
49
|
-
const AGENTS_FILE_NAME = 'AGENTS.md';
|
|
50
|
-
const UTF8_BOM = '\ufeff';
|
|
51
|
-
const MODELS_CACHE_TTL_MS = 60 * 1000;
|
|
52
|
-
const MODELS_NEGATIVE_CACHE_TTL_MS = 5 * 1000;
|
|
53
|
-
const MODELS_CACHE_MAX_ENTRIES = 50;
|
|
54
|
-
const MODELS_RESPONSE_MAX_BYTES = 1024 * 1024;
|
|
55
|
-
const MAX_RECENT_CONFIGS = 3;
|
|
56
|
-
const BOOTSTRAP_TEXT_MARKERS = [
|
|
50
|
+
const MAX_SESSION_PATH_LIST_SIZE = 2000;
|
|
51
|
+
const AGENTS_FILE_NAME = 'AGENTS.md';
|
|
52
|
+
const UTF8_BOM = '\ufeff';
|
|
53
|
+
const MODELS_CACHE_TTL_MS = 60 * 1000;
|
|
54
|
+
const MODELS_NEGATIVE_CACHE_TTL_MS = 5 * 1000;
|
|
55
|
+
const MODELS_CACHE_MAX_ENTRIES = 50;
|
|
56
|
+
const MODELS_RESPONSE_MAX_BYTES = 1024 * 1024;
|
|
57
|
+
const MAX_RECENT_CONFIGS = 3;
|
|
58
|
+
const BOOTSTRAP_TEXT_MARKERS = [
|
|
57
59
|
'agents.md instructions',
|
|
58
60
|
'<instructions>',
|
|
59
61
|
'<environment_context>',
|
|
@@ -64,14 +66,26 @@ const BOOTSTRAP_TEXT_MARKERS = [
|
|
|
64
66
|
const HTTP_KEEP_ALIVE_AGENT = new http.Agent({ keepAlive: true });
|
|
65
67
|
const HTTPS_KEEP_ALIVE_AGENT = new https.Agent({ keepAlive: true });
|
|
66
68
|
|
|
67
|
-
function resolveWebPort() {
|
|
68
|
-
const raw = process.env.CODEXMATE_PORT;
|
|
69
|
-
if (!raw) return DEFAULT_WEB_PORT;
|
|
70
|
-
const parsed = parseInt(raw, 10);
|
|
71
|
-
if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_WEB_PORT;
|
|
72
|
-
return parsed;
|
|
73
|
-
}
|
|
74
|
-
|
|
69
|
+
function resolveWebPort() {
|
|
70
|
+
const raw = process.env.CODEXMATE_PORT;
|
|
71
|
+
if (!raw) return DEFAULT_WEB_PORT;
|
|
72
|
+
const parsed = parseInt(raw, 10);
|
|
73
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_WEB_PORT;
|
|
74
|
+
return parsed;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function resolveWebHost(options = {}) {
|
|
78
|
+
const optionHost = typeof options.host === 'string' ? options.host.trim() : '';
|
|
79
|
+
if (optionHost) {
|
|
80
|
+
return optionHost;
|
|
81
|
+
}
|
|
82
|
+
const envHost = typeof process.env.CODEXMATE_HOST === 'string' ? process.env.CODEXMATE_HOST.trim() : '';
|
|
83
|
+
if (envHost) {
|
|
84
|
+
return envHost;
|
|
85
|
+
}
|
|
86
|
+
return DEFAULT_WEB_HOST;
|
|
87
|
+
}
|
|
88
|
+
|
|
75
89
|
const EMPTY_CONFIG_FALLBACK_TEMPLATE = `model = "gpt-5.3-codex"
|
|
76
90
|
model_reasoning_effort = "high"
|
|
77
91
|
disable_response_storage = true
|
|
@@ -609,6 +623,27 @@ function resolveOpenclawWorkspaceDir(config) {
|
|
|
609
623
|
return path.join(OPENCLAW_DIR, resolved);
|
|
610
624
|
}
|
|
611
625
|
|
|
626
|
+
function normalizeOpenclawWorkspaceFileName(input) {
|
|
627
|
+
const raw = typeof input === 'string' ? input.trim() : '';
|
|
628
|
+
if (!raw) {
|
|
629
|
+
return { error: '文件名不能为空' };
|
|
630
|
+
}
|
|
631
|
+
if (raw.includes('\0')) {
|
|
632
|
+
return { error: '文件名非法' };
|
|
633
|
+
}
|
|
634
|
+
if (raw.includes('/') || raw.includes('\\') || raw.includes('..')) {
|
|
635
|
+
return { error: '文件名非法' };
|
|
636
|
+
}
|
|
637
|
+
const baseName = path.basename(raw);
|
|
638
|
+
if (baseName !== raw) {
|
|
639
|
+
return { error: '文件名非法' };
|
|
640
|
+
}
|
|
641
|
+
if (!raw.toLowerCase().endsWith('.md')) {
|
|
642
|
+
return { error: '仅支持 .md 文件' };
|
|
643
|
+
}
|
|
644
|
+
return { ok: true, name: raw };
|
|
645
|
+
}
|
|
646
|
+
|
|
612
647
|
function readOpenclawConfigFile() {
|
|
613
648
|
const filePath = OPENCLAW_CONFIG_FILE;
|
|
614
649
|
if (!fs.existsSync(filePath)) {
|
|
@@ -708,6 +743,81 @@ function applyOpenclawAgentsFile(params = {}) {
|
|
|
708
743
|
};
|
|
709
744
|
}
|
|
710
745
|
|
|
746
|
+
function readOpenclawWorkspaceFile(params = {}) {
|
|
747
|
+
const nameResult = normalizeOpenclawWorkspaceFileName(params.fileName);
|
|
748
|
+
if (nameResult.error) {
|
|
749
|
+
return { error: nameResult.error };
|
|
750
|
+
}
|
|
751
|
+
const workspaceInfo = getOpenclawWorkspaceInfo();
|
|
752
|
+
const baseDir = workspaceInfo.workspaceDir;
|
|
753
|
+
const filePath = path.join(baseDir, nameResult.name);
|
|
754
|
+
|
|
755
|
+
if (!fs.existsSync(baseDir)) {
|
|
756
|
+
return {
|
|
757
|
+
exists: false,
|
|
758
|
+
path: filePath,
|
|
759
|
+
content: '',
|
|
760
|
+
lineEnding: os.EOL === '\r\n' ? '\r\n' : '\n',
|
|
761
|
+
workspaceDir: baseDir,
|
|
762
|
+
configError: workspaceInfo.configError,
|
|
763
|
+
baseDirMissing: true
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
if (!fs.existsSync(filePath)) {
|
|
768
|
+
return {
|
|
769
|
+
exists: false,
|
|
770
|
+
path: filePath,
|
|
771
|
+
content: '',
|
|
772
|
+
lineEnding: os.EOL === '\r\n' ? '\r\n' : '\n',
|
|
773
|
+
workspaceDir: baseDir,
|
|
774
|
+
configError: workspaceInfo.configError
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
try {
|
|
779
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
780
|
+
return {
|
|
781
|
+
exists: true,
|
|
782
|
+
path: filePath,
|
|
783
|
+
content: stripUtf8Bom(raw),
|
|
784
|
+
lineEnding: detectLineEnding(raw),
|
|
785
|
+
workspaceDir: baseDir,
|
|
786
|
+
configError: workspaceInfo.configError
|
|
787
|
+
};
|
|
788
|
+
} catch (e) {
|
|
789
|
+
return { error: `读取 OpenClaw 工作区文件失败: ${e.message}` };
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function applyOpenclawWorkspaceFile(params = {}) {
|
|
794
|
+
const nameResult = normalizeOpenclawWorkspaceFileName(params.fileName);
|
|
795
|
+
if (nameResult.error) {
|
|
796
|
+
return { error: nameResult.error };
|
|
797
|
+
}
|
|
798
|
+
const workspaceInfo = getOpenclawWorkspaceInfo();
|
|
799
|
+
const baseDir = workspaceInfo.workspaceDir;
|
|
800
|
+
ensureDir(baseDir);
|
|
801
|
+
|
|
802
|
+
const content = typeof params.content === 'string' ? params.content : '';
|
|
803
|
+
const lineEnding = params.lineEnding === '\r\n' ? '\r\n' : '\n';
|
|
804
|
+
const normalized = normalizeLineEnding(content, lineEnding);
|
|
805
|
+
const finalContent = ensureUtf8Bom(normalized);
|
|
806
|
+
const filePath = path.join(baseDir, nameResult.name);
|
|
807
|
+
|
|
808
|
+
try {
|
|
809
|
+
fs.writeFileSync(filePath, finalContent, 'utf-8');
|
|
810
|
+
return {
|
|
811
|
+
success: true,
|
|
812
|
+
path: filePath,
|
|
813
|
+
workspaceDir: baseDir,
|
|
814
|
+
configError: workspaceInfo.configError
|
|
815
|
+
};
|
|
816
|
+
} catch (e) {
|
|
817
|
+
return { error: `写入 OpenClaw 工作区文件失败: ${e.message}` };
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
711
821
|
function applyOpenclawConfig(params = {}) {
|
|
712
822
|
const content = typeof params.content === 'string' ? params.content : '';
|
|
713
823
|
const lineEnding = params.lineEnding === '\r\n' ? '\r\n' : '\n';
|
|
@@ -1379,10 +1489,60 @@ async function buildConfigHealthReport(params = {}) {
|
|
|
1379
1489
|
} else {
|
|
1380
1490
|
const timeoutMs = Number.isFinite(params.timeoutMs)
|
|
1381
1491
|
? Math.max(1000, Number(params.timeoutMs))
|
|
1382
|
-
:
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1492
|
+
: undefined;
|
|
1493
|
+
const apiKey = typeof provider.preferred_auth_method === 'string'
|
|
1494
|
+
? provider.preferred_auth_method.trim()
|
|
1495
|
+
: '';
|
|
1496
|
+
const speedResult = await runSpeedTest(baseUrl, apiKey, { timeoutMs });
|
|
1497
|
+
const status = speedResult && typeof speedResult.status === 'number'
|
|
1498
|
+
? speedResult.status
|
|
1499
|
+
: 0;
|
|
1500
|
+
const durationMs = speedResult && typeof speedResult.durationMs === 'number'
|
|
1501
|
+
? speedResult.durationMs
|
|
1502
|
+
: 0;
|
|
1503
|
+
const error = speedResult && speedResult.error ? String(speedResult.error) : '';
|
|
1504
|
+
remote = {
|
|
1505
|
+
type: 'speed-test',
|
|
1506
|
+
url: baseUrl,
|
|
1507
|
+
ok: !!speedResult.ok,
|
|
1508
|
+
status,
|
|
1509
|
+
durationMs,
|
|
1510
|
+
error
|
|
1511
|
+
};
|
|
1512
|
+
|
|
1513
|
+
if (!speedResult.ok) {
|
|
1514
|
+
const errorLower = error.toLowerCase();
|
|
1515
|
+
if (errorLower.includes('timeout')) {
|
|
1516
|
+
issues.push({
|
|
1517
|
+
code: 'remote-speedtest-timeout',
|
|
1518
|
+
message: '远程测速超时',
|
|
1519
|
+
suggestion: '检查网络或 base_url 是否可达'
|
|
1520
|
+
});
|
|
1521
|
+
} else if (errorLower.includes('invalid url')) {
|
|
1522
|
+
issues.push({
|
|
1523
|
+
code: 'remote-speedtest-invalid-url',
|
|
1524
|
+
message: '远程测速失败:base_url 无效',
|
|
1525
|
+
suggestion: '请设置为 http/https 的完整 URL'
|
|
1526
|
+
});
|
|
1527
|
+
} else {
|
|
1528
|
+
issues.push({
|
|
1529
|
+
code: 'remote-speedtest-unreachable',
|
|
1530
|
+
message: `远程测速失败:${error || '无法连接'}`,
|
|
1531
|
+
suggestion: '检查网络或 base_url 是否可用'
|
|
1532
|
+
});
|
|
1533
|
+
}
|
|
1534
|
+
} else if (status === 401 || status === 403) {
|
|
1535
|
+
issues.push({
|
|
1536
|
+
code: 'remote-speedtest-auth-failed',
|
|
1537
|
+
message: '远程测速鉴权失败(401/403)',
|
|
1538
|
+
suggestion: '检查 API Key 或认证方式'
|
|
1539
|
+
});
|
|
1540
|
+
} else if (status >= 400) {
|
|
1541
|
+
issues.push({
|
|
1542
|
+
code: 'remote-speedtest-http-error',
|
|
1543
|
+
message: `远程测速返回异常状态: ${status}`,
|
|
1544
|
+
suggestion: '检查 base_url 或服务状态'
|
|
1545
|
+
});
|
|
1386
1546
|
}
|
|
1387
1547
|
}
|
|
1388
1548
|
}
|
|
@@ -1424,6 +1584,16 @@ function toIsoTime(value, fallback = '') {
|
|
|
1424
1584
|
return date.toISOString();
|
|
1425
1585
|
}
|
|
1426
1586
|
|
|
1587
|
+
function updateLatestIso(currentIso, candidate) {
|
|
1588
|
+
const currentTime = Date.parse(currentIso || '') || 0;
|
|
1589
|
+
const candidateIso = toIsoTime(candidate, '');
|
|
1590
|
+
const candidateTime = Date.parse(candidateIso || '') || 0;
|
|
1591
|
+
if (!candidateTime) {
|
|
1592
|
+
return currentIso;
|
|
1593
|
+
}
|
|
1594
|
+
return candidateTime > currentTime ? candidateIso : currentIso;
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1427
1597
|
function truncateText(text, maxLength = 90) {
|
|
1428
1598
|
if (!text) return '';
|
|
1429
1599
|
const normalized = String(text).replace(/\s+/g, ' ').trim();
|
|
@@ -2297,7 +2467,7 @@ function parseCodexSessionSummary(filePath) {
|
|
|
2297
2467
|
|
|
2298
2468
|
for (const record of records) {
|
|
2299
2469
|
if (record.timestamp) {
|
|
2300
|
-
updatedAt =
|
|
2470
|
+
updatedAt = updateLatestIso(updatedAt, record.timestamp);
|
|
2301
2471
|
}
|
|
2302
2472
|
|
|
2303
2473
|
if (record.type === 'session_meta' && record.payload) {
|
|
@@ -2386,7 +2556,7 @@ function parseClaudeSessionSummary(filePath) {
|
|
|
2386
2556
|
createdAt = toIsoTime(record.timestamp, createdAt);
|
|
2387
2557
|
}
|
|
2388
2558
|
if (record.timestamp) {
|
|
2389
|
-
updatedAt =
|
|
2559
|
+
updatedAt = updateLatestIso(updatedAt, record.timestamp);
|
|
2390
2560
|
}
|
|
2391
2561
|
|
|
2392
2562
|
if (!cwd && record.cwd) {
|
|
@@ -2747,7 +2917,269 @@ function resolveSessionFilePath(source, filePath, sessionId) {
|
|
|
2747
2917
|
return '';
|
|
2748
2918
|
}
|
|
2749
2919
|
|
|
2750
|
-
function
|
|
2920
|
+
function findClaudeSessionIndexPath(sessionFilePath) {
|
|
2921
|
+
const root = getClaudeProjectsDir();
|
|
2922
|
+
if (!root || !sessionFilePath) {
|
|
2923
|
+
return '';
|
|
2924
|
+
}
|
|
2925
|
+
if (!isPathInside(sessionFilePath, root)) {
|
|
2926
|
+
return '';
|
|
2927
|
+
}
|
|
2928
|
+
let current = path.dirname(sessionFilePath);
|
|
2929
|
+
const resolvedRoot = path.resolve(root);
|
|
2930
|
+
while (current && isPathInside(current, resolvedRoot)) {
|
|
2931
|
+
const candidate = path.join(current, 'sessions-index.json');
|
|
2932
|
+
if (fs.existsSync(candidate)) {
|
|
2933
|
+
return candidate;
|
|
2934
|
+
}
|
|
2935
|
+
const parent = path.dirname(current);
|
|
2936
|
+
if (parent === current) {
|
|
2937
|
+
break;
|
|
2938
|
+
}
|
|
2939
|
+
current = parent;
|
|
2940
|
+
}
|
|
2941
|
+
return '';
|
|
2942
|
+
}
|
|
2943
|
+
|
|
2944
|
+
function updateClaudeSessionIndex(indexPath, sessionFilePath, sessionId) {
|
|
2945
|
+
if (!indexPath || !fs.existsSync(indexPath)) {
|
|
2946
|
+
return;
|
|
2947
|
+
}
|
|
2948
|
+
const index = readJsonFile(indexPath, null);
|
|
2949
|
+
if (!index || !Array.isArray(index.entries)) {
|
|
2950
|
+
return;
|
|
2951
|
+
}
|
|
2952
|
+
const resolvedFile = sessionFilePath ? path.resolve(sessionFilePath) : '';
|
|
2953
|
+
const resolvedLower = resolvedFile ? resolvedFile.toLowerCase() : '';
|
|
2954
|
+
const filtered = index.entries.filter((entry) => {
|
|
2955
|
+
if (!entry || typeof entry !== 'object') {
|
|
2956
|
+
return false;
|
|
2957
|
+
}
|
|
2958
|
+
const entrySessionId = typeof entry.sessionId === 'string' ? entry.sessionId : '';
|
|
2959
|
+
if (sessionId && entrySessionId === sessionId) {
|
|
2960
|
+
return false;
|
|
2961
|
+
}
|
|
2962
|
+
if (entry.fullPath) {
|
|
2963
|
+
const expanded = expandHomePath(entry.fullPath);
|
|
2964
|
+
const entryPath = expanded ? path.resolve(expanded) : '';
|
|
2965
|
+
if (entryPath && resolvedLower && entryPath.toLowerCase() === resolvedLower) {
|
|
2966
|
+
return false;
|
|
2967
|
+
}
|
|
2968
|
+
}
|
|
2969
|
+
return true;
|
|
2970
|
+
});
|
|
2971
|
+
if (filtered.length === index.entries.length) {
|
|
2972
|
+
return;
|
|
2973
|
+
}
|
|
2974
|
+
index.entries = filtered;
|
|
2975
|
+
try {
|
|
2976
|
+
fs.writeFileSync(indexPath, JSON.stringify(index, null, 2), 'utf-8');
|
|
2977
|
+
} catch (e) {}
|
|
2978
|
+
}
|
|
2979
|
+
|
|
2980
|
+
async function deleteSessionData(params = {}) {
|
|
2981
|
+
const source = params.source === 'claude' ? 'claude' : (params.source === 'codex' ? 'codex' : '');
|
|
2982
|
+
if (!source) {
|
|
2983
|
+
return { error: 'Invalid source' };
|
|
2984
|
+
}
|
|
2985
|
+
|
|
2986
|
+
const filePath = resolveSessionFilePath(source, params.filePath, params.sessionId);
|
|
2987
|
+
if (!filePath) {
|
|
2988
|
+
return { error: 'Session file not found' };
|
|
2989
|
+
}
|
|
2990
|
+
|
|
2991
|
+
const sessionId = params.sessionId || path.basename(filePath, '.jsonl');
|
|
2992
|
+
try {
|
|
2993
|
+
fs.unlinkSync(filePath);
|
|
2994
|
+
} catch (e) {
|
|
2995
|
+
return { error: `删除会话失败: ${e.message}` };
|
|
2996
|
+
}
|
|
2997
|
+
|
|
2998
|
+
if (source === 'claude') {
|
|
2999
|
+
const indexPath = findClaudeSessionIndexPath(filePath);
|
|
3000
|
+
if (indexPath) {
|
|
3001
|
+
updateClaudeSessionIndex(indexPath, filePath, sessionId);
|
|
3002
|
+
}
|
|
3003
|
+
}
|
|
3004
|
+
|
|
3005
|
+
invalidateSessionListCache();
|
|
3006
|
+
|
|
3007
|
+
return {
|
|
3008
|
+
success: true,
|
|
3009
|
+
source,
|
|
3010
|
+
sessionId,
|
|
3011
|
+
filePath
|
|
3012
|
+
};
|
|
3013
|
+
}
|
|
3014
|
+
|
|
3015
|
+
function generateCloneSessionId() {
|
|
3016
|
+
if (crypto.randomUUID) {
|
|
3017
|
+
return `clone-${crypto.randomUUID()}`;
|
|
3018
|
+
}
|
|
3019
|
+
const timePart = Date.now().toString(36);
|
|
3020
|
+
const randomPart = crypto.randomBytes(8).toString('hex');
|
|
3021
|
+
return `clone-${timePart}-${randomPart}`;
|
|
3022
|
+
}
|
|
3023
|
+
|
|
3024
|
+
function allocateCloneSessionTarget(dirPath) {
|
|
3025
|
+
for (let attempt = 0; attempt < 6; attempt += 1) {
|
|
3026
|
+
const sessionId = generateCloneSessionId();
|
|
3027
|
+
const filePath = path.join(dirPath, `${sessionId}.jsonl`);
|
|
3028
|
+
if (!fs.existsSync(filePath)) {
|
|
3029
|
+
return { sessionId, filePath };
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
3032
|
+
const fallbackId = `clone-${Date.now().toString(36)}-${crypto.randomBytes(8).toString('hex')}`;
|
|
3033
|
+
return { sessionId: fallbackId, filePath: path.join(dirPath, `${fallbackId}.jsonl`) };
|
|
3034
|
+
}
|
|
3035
|
+
|
|
3036
|
+
function parseTimestampMs(value) {
|
|
3037
|
+
if (value === undefined || value === null || value === '') {
|
|
3038
|
+
return null;
|
|
3039
|
+
}
|
|
3040
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
3041
|
+
if (value > 1e12) return value;
|
|
3042
|
+
if (value > 1e9) return value * 1000;
|
|
3043
|
+
return value;
|
|
3044
|
+
}
|
|
3045
|
+
if (typeof value === 'string') {
|
|
3046
|
+
const parsed = Date.parse(value);
|
|
3047
|
+
if (Number.isFinite(parsed)) {
|
|
3048
|
+
return parsed;
|
|
3049
|
+
}
|
|
3050
|
+
const numeric = Number(value);
|
|
3051
|
+
if (Number.isFinite(numeric)) {
|
|
3052
|
+
if (numeric > 1e12) return numeric;
|
|
3053
|
+
if (numeric > 1e9) return numeric * 1000;
|
|
3054
|
+
return numeric;
|
|
3055
|
+
}
|
|
3056
|
+
}
|
|
3057
|
+
return null;
|
|
3058
|
+
}
|
|
3059
|
+
|
|
3060
|
+
async function cloneCodexSession(params = {}) {
|
|
3061
|
+
const source = params.source === 'codex' ? 'codex' : '';
|
|
3062
|
+
if (!source) {
|
|
3063
|
+
return { error: '仅支持 Codex 会话克隆' };
|
|
3064
|
+
}
|
|
3065
|
+
|
|
3066
|
+
const filePath = resolveSessionFilePath(source, params.filePath, params.sessionId);
|
|
3067
|
+
if (!filePath) {
|
|
3068
|
+
return { error: 'Session file not found' };
|
|
3069
|
+
}
|
|
3070
|
+
|
|
3071
|
+
let content = '';
|
|
3072
|
+
try {
|
|
3073
|
+
content = fs.readFileSync(filePath, 'utf-8');
|
|
3074
|
+
} catch (e) {
|
|
3075
|
+
return { error: `读取会话失败: ${e.message}` };
|
|
3076
|
+
}
|
|
3077
|
+
|
|
3078
|
+
if (!content.trim()) {
|
|
3079
|
+
return { error: 'Session file is empty' };
|
|
3080
|
+
}
|
|
3081
|
+
|
|
3082
|
+
const lineEnding = detectLineEnding(content);
|
|
3083
|
+
const rawLines = content.split(/\r?\n/);
|
|
3084
|
+
if (rawLines.length > 0 && rawLines[rawLines.length - 1] === '') {
|
|
3085
|
+
rawLines.pop();
|
|
3086
|
+
}
|
|
3087
|
+
|
|
3088
|
+
let originalSessionId = typeof params.sessionId === 'string' ? params.sessionId.trim() : '';
|
|
3089
|
+
if (!originalSessionId) {
|
|
3090
|
+
originalSessionId = path.basename(filePath, '.jsonl');
|
|
3091
|
+
}
|
|
3092
|
+
let maxTimestampMs = 0;
|
|
3093
|
+
|
|
3094
|
+
for (const line of rawLines) {
|
|
3095
|
+
const trimmed = line.trim();
|
|
3096
|
+
if (!trimmed) continue;
|
|
3097
|
+
try {
|
|
3098
|
+
const record = JSON.parse(trimmed);
|
|
3099
|
+
if (record && record.type === 'session_meta' && record.payload) {
|
|
3100
|
+
if (record.payload.id) {
|
|
3101
|
+
originalSessionId = record.payload.id;
|
|
3102
|
+
}
|
|
3103
|
+
}
|
|
3104
|
+
if (record && record.timestamp !== undefined) {
|
|
3105
|
+
const ts = parseTimestampMs(record.timestamp);
|
|
3106
|
+
if (Number.isFinite(ts) && ts > maxTimestampMs) {
|
|
3107
|
+
maxTimestampMs = ts;
|
|
3108
|
+
}
|
|
3109
|
+
}
|
|
3110
|
+
} catch (e) {}
|
|
3111
|
+
}
|
|
3112
|
+
|
|
3113
|
+
const sessionsDir = getCodexSessionsDir();
|
|
3114
|
+
ensureDir(sessionsDir);
|
|
3115
|
+
const target = allocateCloneSessionTarget(sessionsDir);
|
|
3116
|
+
const newSessionId = target.sessionId;
|
|
3117
|
+
const newFilePath = target.filePath;
|
|
3118
|
+
const offsetMs = maxTimestampMs ? (Date.now() - maxTimestampMs) : 0;
|
|
3119
|
+
const cloneTime = new Date(Date.now() + 1);
|
|
3120
|
+
const cloneIso = cloneTime.toISOString();
|
|
3121
|
+
|
|
3122
|
+
const outputLines = [];
|
|
3123
|
+
for (const line of rawLines) {
|
|
3124
|
+
const trimmed = line.trim();
|
|
3125
|
+
if (!trimmed) {
|
|
3126
|
+
outputLines.push(line);
|
|
3127
|
+
continue;
|
|
3128
|
+
}
|
|
3129
|
+
let record;
|
|
3130
|
+
try {
|
|
3131
|
+
record = JSON.parse(trimmed);
|
|
3132
|
+
} catch (e) {
|
|
3133
|
+
outputLines.push(line);
|
|
3134
|
+
continue;
|
|
3135
|
+
}
|
|
3136
|
+
|
|
3137
|
+
if (originalSessionId && typeof record.sessionId === 'string' && record.sessionId === originalSessionId) {
|
|
3138
|
+
record.sessionId = newSessionId;
|
|
3139
|
+
}
|
|
3140
|
+
if (originalSessionId && typeof record.session_id === 'string' && record.session_id === originalSessionId) {
|
|
3141
|
+
record.session_id = newSessionId;
|
|
3142
|
+
}
|
|
3143
|
+
if (offsetMs && record.timestamp !== undefined) {
|
|
3144
|
+
const ts = parseTimestampMs(record.timestamp);
|
|
3145
|
+
if (Number.isFinite(ts)) {
|
|
3146
|
+
record.timestamp = new Date(ts + offsetMs).toISOString();
|
|
3147
|
+
}
|
|
3148
|
+
}
|
|
3149
|
+
if (record && record.type === 'session_meta' && record.payload && typeof record.payload === 'object') {
|
|
3150
|
+
record.payload = {
|
|
3151
|
+
...record.payload,
|
|
3152
|
+
id: newSessionId,
|
|
3153
|
+
timestamp: cloneIso
|
|
3154
|
+
};
|
|
3155
|
+
record.timestamp = cloneIso;
|
|
3156
|
+
}
|
|
3157
|
+
|
|
3158
|
+
outputLines.push(JSON.stringify(record));
|
|
3159
|
+
}
|
|
3160
|
+
|
|
3161
|
+
const output = outputLines.join(lineEnding) + lineEnding;
|
|
3162
|
+
try {
|
|
3163
|
+
fs.writeFileSync(newFilePath, output, 'utf-8');
|
|
3164
|
+
} catch (e) {
|
|
3165
|
+
return { error: `写入克隆会话失败: ${e.message}` };
|
|
3166
|
+
}
|
|
3167
|
+
try {
|
|
3168
|
+
fs.utimesSync(newFilePath, cloneTime, cloneTime);
|
|
3169
|
+
} catch (e) {}
|
|
3170
|
+
|
|
3171
|
+
invalidateSessionListCache();
|
|
3172
|
+
|
|
3173
|
+
return {
|
|
3174
|
+
success: true,
|
|
3175
|
+
source,
|
|
3176
|
+
sourceLabel: 'Codex',
|
|
3177
|
+
sessionId: newSessionId,
|
|
3178
|
+
filePath: newFilePath
|
|
3179
|
+
};
|
|
3180
|
+
}
|
|
3181
|
+
|
|
3182
|
+
function buildSessionMarkdown(payload) {
|
|
2751
3183
|
const lines = [
|
|
2752
3184
|
'# AI Session Export',
|
|
2753
3185
|
'',
|
|
@@ -2776,24 +3208,75 @@ function buildSessionMarkdown(payload) {
|
|
|
2776
3208
|
lines.push('');
|
|
2777
3209
|
});
|
|
2778
3210
|
|
|
2779
|
-
return lines.join('\n');
|
|
2780
|
-
}
|
|
2781
|
-
|
|
2782
|
-
function
|
|
2783
|
-
if (!
|
|
2784
|
-
return
|
|
2785
|
-
}
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
}
|
|
2795
|
-
|
|
2796
|
-
|
|
3211
|
+
return lines.join('\n');
|
|
3212
|
+
}
|
|
3213
|
+
|
|
3214
|
+
function buildSessionPlainText(messages) {
|
|
3215
|
+
if (!Array.isArray(messages) || messages.length === 0) {
|
|
3216
|
+
return '';
|
|
3217
|
+
}
|
|
3218
|
+
|
|
3219
|
+
const lines = [];
|
|
3220
|
+
messages.forEach((message) => {
|
|
3221
|
+
const role = normalizeRole(message && message.role) || 'unknown';
|
|
3222
|
+
const text = message && typeof message.text === 'string' ? message.text : '';
|
|
3223
|
+
lines.push(role);
|
|
3224
|
+
lines.push(text);
|
|
3225
|
+
lines.push('');
|
|
3226
|
+
});
|
|
3227
|
+
|
|
3228
|
+
while (lines.length > 0 && lines[lines.length - 1] === '') {
|
|
3229
|
+
lines.pop();
|
|
3230
|
+
}
|
|
3231
|
+
|
|
3232
|
+
return lines.join('\n');
|
|
3233
|
+
}
|
|
3234
|
+
|
|
3235
|
+
function parseMaxMessagesValue(value) {
|
|
3236
|
+
if (value === Infinity) {
|
|
3237
|
+
return Infinity;
|
|
3238
|
+
}
|
|
3239
|
+
|
|
3240
|
+
if (typeof value === 'string') {
|
|
3241
|
+
const trimmed = value.trim();
|
|
3242
|
+
if (!trimmed) {
|
|
3243
|
+
return null;
|
|
3244
|
+
}
|
|
3245
|
+
const lower = trimmed.toLowerCase();
|
|
3246
|
+
if (lower === 'all' || lower === 'infinity' || lower === 'inf') {
|
|
3247
|
+
return Infinity;
|
|
3248
|
+
}
|
|
3249
|
+
const parsed = Number(trimmed);
|
|
3250
|
+
if (Number.isFinite(parsed)) {
|
|
3251
|
+
return parsed;
|
|
3252
|
+
}
|
|
3253
|
+
return null;
|
|
3254
|
+
}
|
|
3255
|
+
|
|
3256
|
+
if (Number.isFinite(value)) {
|
|
3257
|
+
return value;
|
|
3258
|
+
}
|
|
3259
|
+
return null;
|
|
3260
|
+
}
|
|
3261
|
+
|
|
3262
|
+
function resolveMaxMessagesValue(value, fallback) {
|
|
3263
|
+
const parsed = parseMaxMessagesValue(value);
|
|
3264
|
+
if (parsed === null) {
|
|
3265
|
+
return fallback;
|
|
3266
|
+
}
|
|
3267
|
+
if (parsed === Infinity) {
|
|
3268
|
+
return Infinity;
|
|
3269
|
+
}
|
|
3270
|
+
return Math.max(1, Math.floor(parsed));
|
|
3271
|
+
}
|
|
3272
|
+
|
|
3273
|
+
function resolveStateMaxMessages(state) {
|
|
3274
|
+
if (!state || typeof state !== 'object') {
|
|
3275
|
+
return MAX_EXPORT_MESSAGES;
|
|
3276
|
+
}
|
|
3277
|
+
|
|
3278
|
+
return resolveMaxMessagesValue(state.maxMessages, MAX_EXPORT_MESSAGES);
|
|
3279
|
+
}
|
|
2797
3280
|
|
|
2798
3281
|
function canAppendMessage(state) {
|
|
2799
3282
|
const maxMessages = resolveStateMaxMessages(state);
|
|
@@ -2830,7 +3313,7 @@ function extractCodexMessageFromRecord(record, state, lineIndex = -1) {
|
|
|
2830
3313
|
}
|
|
2831
3314
|
}
|
|
2832
3315
|
|
|
2833
|
-
function extractClaudeMessageFromRecord(record, state, lineIndex = -1) {
|
|
3316
|
+
function extractClaudeMessageFromRecord(record, state, lineIndex = -1) {
|
|
2834
3317
|
if (record.timestamp) {
|
|
2835
3318
|
state.updatedAt = toIsoTime(record.timestamp, state.updatedAt);
|
|
2836
3319
|
}
|
|
@@ -2855,89 +3338,130 @@ function extractClaudeMessageFromRecord(record, state, lineIndex = -1) {
|
|
|
2855
3338
|
recordLineIndex: Number.isInteger(lineIndex) ? lineIndex : -1
|
|
2856
3339
|
});
|
|
2857
3340
|
}
|
|
2858
|
-
}
|
|
2859
|
-
}
|
|
2860
|
-
|
|
2861
|
-
function
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
}
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
3341
|
+
}
|
|
3342
|
+
}
|
|
3343
|
+
|
|
3344
|
+
function recordHasCodexMessage(record) {
|
|
3345
|
+
if (!record || record.type !== 'response_item' || !record.payload) {
|
|
3346
|
+
return false;
|
|
3347
|
+
}
|
|
3348
|
+
if (record.payload.type !== 'message') {
|
|
3349
|
+
return false;
|
|
3350
|
+
}
|
|
3351
|
+
const role = normalizeRole(record.payload.role);
|
|
3352
|
+
if (role !== 'user' && role !== 'assistant' && role !== 'system') {
|
|
3353
|
+
return false;
|
|
3354
|
+
}
|
|
3355
|
+
const text = extractMessageText(record.payload.content);
|
|
3356
|
+
return !!text;
|
|
3357
|
+
}
|
|
3358
|
+
|
|
3359
|
+
function recordHasClaudeMessage(record) {
|
|
3360
|
+
if (!record) {
|
|
3361
|
+
return false;
|
|
3362
|
+
}
|
|
3363
|
+
const role = normalizeRole(record.type);
|
|
3364
|
+
if (role !== 'user' && role !== 'assistant' && role !== 'system') {
|
|
3365
|
+
return false;
|
|
3366
|
+
}
|
|
3367
|
+
const content = record.message ? record.message.content : '';
|
|
3368
|
+
const text = extractMessageText(content);
|
|
3369
|
+
return !!text;
|
|
3370
|
+
}
|
|
3371
|
+
|
|
3372
|
+
function recordHasMessage(record, source) {
|
|
3373
|
+
return source === 'codex'
|
|
3374
|
+
? recordHasCodexMessage(record)
|
|
3375
|
+
: recordHasClaudeMessage(record);
|
|
3376
|
+
}
|
|
3377
|
+
|
|
3378
|
+
function extractMessagesFromRecords(records, source, options = {}) {
|
|
3379
|
+
const maxMessages = resolveMaxMessagesValue(options.maxMessages, MAX_EXPORT_MESSAGES);
|
|
3380
|
+
const state = {
|
|
3381
|
+
sessionId: '',
|
|
3382
|
+
cwd: '',
|
|
3383
|
+
updatedAt: '',
|
|
3384
|
+
messages: [],
|
|
3385
|
+
maxMessages,
|
|
3386
|
+
truncated: false
|
|
3387
|
+
};
|
|
3388
|
+
|
|
3389
|
+
for (let lineIndex = 0; lineIndex < records.length; lineIndex++) {
|
|
3390
|
+
const record = records[lineIndex];
|
|
3391
|
+
if (source === 'codex') {
|
|
3392
|
+
extractCodexMessageFromRecord(record, state, lineIndex);
|
|
3393
|
+
} else {
|
|
3394
|
+
extractClaudeMessageFromRecord(record, state, lineIndex);
|
|
3395
|
+
}
|
|
3396
|
+
|
|
3397
|
+
if (state.maxMessages !== Infinity && state.messages.length >= state.maxMessages) {
|
|
3398
|
+
for (let i = lineIndex + 1; i < records.length; i++) {
|
|
3399
|
+
if (recordHasMessage(records[i], source)) {
|
|
3400
|
+
state.truncated = true;
|
|
3401
|
+
break;
|
|
3402
|
+
}
|
|
3403
|
+
}
|
|
3404
|
+
break;
|
|
3405
|
+
}
|
|
3406
|
+
}
|
|
3407
|
+
|
|
3408
|
+
return state;
|
|
3409
|
+
}
|
|
3410
|
+
|
|
3411
|
+
async function extractMessagesFromFile(filePath, source, options = {}) {
|
|
3412
|
+
const maxMessages = resolveMaxMessagesValue(options.maxMessages, MAX_EXPORT_MESSAGES);
|
|
3413
|
+
const state = {
|
|
3414
|
+
sessionId: '',
|
|
3415
|
+
cwd: '',
|
|
3416
|
+
updatedAt: '',
|
|
3417
|
+
messages: [],
|
|
3418
|
+
maxMessages,
|
|
3419
|
+
truncated: false
|
|
3420
|
+
};
|
|
3421
|
+
|
|
3422
|
+
let stream;
|
|
3423
|
+
let rl;
|
|
3424
|
+
try {
|
|
3425
|
+
stream = fs.createReadStream(filePath, { encoding: 'utf-8' });
|
|
3426
|
+
rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
3427
|
+
|
|
3428
|
+
let lineIndex = 0;
|
|
3429
|
+
let limitReached = false;
|
|
3430
|
+
for await (const line of rl) {
|
|
3431
|
+
const currentLineIndex = lineIndex;
|
|
3432
|
+
lineIndex += 1;
|
|
2913
3433
|
|
|
2914
3434
|
const trimmed = line.trim();
|
|
2915
3435
|
if (!trimmed) continue;
|
|
2916
3436
|
|
|
2917
3437
|
let record;
|
|
2918
|
-
try {
|
|
2919
|
-
record = JSON.parse(trimmed);
|
|
2920
|
-
} catch (e) {
|
|
2921
|
-
continue;
|
|
2922
|
-
}
|
|
2923
|
-
|
|
2924
|
-
if (
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
}
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
3438
|
+
try {
|
|
3439
|
+
record = JSON.parse(trimmed);
|
|
3440
|
+
} catch (e) {
|
|
3441
|
+
continue;
|
|
3442
|
+
}
|
|
3443
|
+
|
|
3444
|
+
if (limitReached) {
|
|
3445
|
+
if (recordHasMessage(record, source)) {
|
|
3446
|
+
state.truncated = true;
|
|
3447
|
+
break;
|
|
3448
|
+
}
|
|
3449
|
+
continue;
|
|
3450
|
+
}
|
|
3451
|
+
|
|
3452
|
+
if (source === 'codex') {
|
|
3453
|
+
extractCodexMessageFromRecord(record, state, currentLineIndex);
|
|
3454
|
+
} else {
|
|
3455
|
+
extractClaudeMessageFromRecord(record, state, currentLineIndex);
|
|
3456
|
+
}
|
|
3457
|
+
|
|
3458
|
+
if (state.maxMessages !== Infinity && state.messages.length >= state.maxMessages) {
|
|
3459
|
+
limitReached = true;
|
|
3460
|
+
}
|
|
3461
|
+
}
|
|
3462
|
+
} catch (e) {
|
|
3463
|
+
const fallbackRecords = readJsonlRecords(filePath);
|
|
3464
|
+
return extractMessagesFromRecords(fallbackRecords, source, { maxMessages });
|
|
2941
3465
|
} finally {
|
|
2942
3466
|
if (rl) {
|
|
2943
3467
|
try { rl.close(); } catch (e) {}
|
|
@@ -2950,7 +3474,7 @@ async function extractMessagesFromFile(filePath, source, options = {}) {
|
|
|
2950
3474
|
return state;
|
|
2951
3475
|
}
|
|
2952
3476
|
|
|
2953
|
-
async function readSessionDetail(params = {}) {
|
|
3477
|
+
async function readSessionDetail(params = {}) {
|
|
2954
3478
|
const source = params.source === 'claude' ? 'claude' : (params.source === 'codex' ? 'codex' : '');
|
|
2955
3479
|
if (!source) {
|
|
2956
3480
|
return { error: 'Invalid source' };
|
|
@@ -2977,37 +3501,83 @@ async function readSessionDetail(params = {}) {
|
|
|
2977
3501
|
const startIndex = Math.max(0, allMessages.length - messageLimit);
|
|
2978
3502
|
const clippedMessages = allMessages.slice(startIndex);
|
|
2979
3503
|
|
|
2980
|
-
return {
|
|
2981
|
-
source,
|
|
2982
|
-
sourceLabel,
|
|
2983
|
-
sessionId,
|
|
3504
|
+
return {
|
|
3505
|
+
source,
|
|
3506
|
+
sourceLabel,
|
|
3507
|
+
sessionId,
|
|
2984
3508
|
cwd: extracted.cwd || '',
|
|
2985
3509
|
updatedAt: extracted.updatedAt || '',
|
|
2986
3510
|
totalMessages: allMessages.length,
|
|
2987
3511
|
clipped: allMessages.length > clippedMessages.length,
|
|
2988
3512
|
messageLimit,
|
|
2989
3513
|
messages: clippedMessages,
|
|
2990
|
-
filePath
|
|
2991
|
-
};
|
|
2992
|
-
}
|
|
2993
|
-
|
|
2994
|
-
async function
|
|
2995
|
-
const source = params.source === 'claude' ? 'claude' : (params.source === 'codex' ? 'codex' : '');
|
|
2996
|
-
if (!source) {
|
|
2997
|
-
return { error: 'Invalid source' };
|
|
2998
|
-
}
|
|
2999
|
-
|
|
3000
|
-
const filePath = resolveSessionFilePath(source, params.filePath, params.sessionId);
|
|
3001
|
-
if (!filePath) {
|
|
3002
|
-
return { error: 'Session file not found' };
|
|
3003
|
-
}
|
|
3004
|
-
|
|
3005
|
-
let extracted;
|
|
3006
|
-
try {
|
|
3007
|
-
extracted = await extractMessagesFromFile(filePath, source);
|
|
3008
|
-
} catch (e) {
|
|
3009
|
-
extracted = null;
|
|
3010
|
-
}
|
|
3514
|
+
filePath
|
|
3515
|
+
};
|
|
3516
|
+
}
|
|
3517
|
+
|
|
3518
|
+
async function readSessionPlain(params = {}) {
|
|
3519
|
+
const source = params.source === 'claude' ? 'claude' : (params.source === 'codex' ? 'codex' : '');
|
|
3520
|
+
if (!source) {
|
|
3521
|
+
return { error: 'Invalid source' };
|
|
3522
|
+
}
|
|
3523
|
+
|
|
3524
|
+
const filePath = resolveSessionFilePath(source, params.filePath, params.sessionId);
|
|
3525
|
+
if (!filePath) {
|
|
3526
|
+
return { error: 'Session file not found' };
|
|
3527
|
+
}
|
|
3528
|
+
|
|
3529
|
+
let extracted;
|
|
3530
|
+
try {
|
|
3531
|
+
extracted = await extractMessagesFromFile(filePath, source, { maxMessages: Infinity });
|
|
3532
|
+
} catch (e) {
|
|
3533
|
+
extracted = null;
|
|
3534
|
+
}
|
|
3535
|
+
|
|
3536
|
+
if (!extracted) {
|
|
3537
|
+
return { error: 'Failed to parse session file' };
|
|
3538
|
+
}
|
|
3539
|
+
|
|
3540
|
+
if ((!extracted.messages || extracted.messages.length === 0) && !extracted.sessionId && !extracted.cwd) {
|
|
3541
|
+
const fallbackRecords = readJsonlRecords(filePath);
|
|
3542
|
+
if (fallbackRecords.length === 0) {
|
|
3543
|
+
return { error: 'Session file is empty' };
|
|
3544
|
+
}
|
|
3545
|
+
extracted = extractMessagesFromRecords(fallbackRecords, source, { maxMessages: Infinity });
|
|
3546
|
+
}
|
|
3547
|
+
|
|
3548
|
+
const sessionId = extracted.sessionId || params.sessionId || path.basename(filePath, '.jsonl');
|
|
3549
|
+
const sourceLabel = source === 'codex' ? 'Codex' : 'Claude Code';
|
|
3550
|
+
const messages = removeLeadingSystemMessage(Array.isArray(extracted.messages) ? extracted.messages : []);
|
|
3551
|
+
const text = buildSessionPlainText(messages);
|
|
3552
|
+
|
|
3553
|
+
return {
|
|
3554
|
+
source,
|
|
3555
|
+
sourceLabel,
|
|
3556
|
+
sessionId,
|
|
3557
|
+
title: sessionId,
|
|
3558
|
+
filePath,
|
|
3559
|
+
text
|
|
3560
|
+
};
|
|
3561
|
+
}
|
|
3562
|
+
|
|
3563
|
+
async function exportSessionData(params = {}) {
|
|
3564
|
+
const source = params.source === 'claude' ? 'claude' : (params.source === 'codex' ? 'codex' : '');
|
|
3565
|
+
if (!source) {
|
|
3566
|
+
return { error: 'Invalid source' };
|
|
3567
|
+
}
|
|
3568
|
+
|
|
3569
|
+
const maxMessages = resolveMaxMessagesValue(params.maxMessages, MAX_EXPORT_MESSAGES);
|
|
3570
|
+
const filePath = resolveSessionFilePath(source, params.filePath, params.sessionId);
|
|
3571
|
+
if (!filePath) {
|
|
3572
|
+
return { error: 'Session file not found' };
|
|
3573
|
+
}
|
|
3574
|
+
|
|
3575
|
+
let extracted;
|
|
3576
|
+
try {
|
|
3577
|
+
extracted = await extractMessagesFromFile(filePath, source, { maxMessages });
|
|
3578
|
+
} catch (e) {
|
|
3579
|
+
extracted = null;
|
|
3580
|
+
}
|
|
3011
3581
|
|
|
3012
3582
|
if (!extracted) {
|
|
3013
3583
|
return { error: 'Failed to parse session file' };
|
|
@@ -3015,11 +3585,11 @@ async function exportSessionData(params = {}) {
|
|
|
3015
3585
|
|
|
3016
3586
|
if ((!extracted.messages || extracted.messages.length === 0) && !extracted.sessionId && !extracted.cwd) {
|
|
3017
3587
|
const fallbackRecords = readJsonlRecords(filePath);
|
|
3018
|
-
if (fallbackRecords.length === 0) {
|
|
3019
|
-
return { error: 'Session file is empty' };
|
|
3020
|
-
}
|
|
3021
|
-
extracted = extractMessagesFromRecords(fallbackRecords, source);
|
|
3022
|
-
}
|
|
3588
|
+
if (fallbackRecords.length === 0) {
|
|
3589
|
+
return { error: 'Session file is empty' };
|
|
3590
|
+
}
|
|
3591
|
+
extracted = extractMessagesFromRecords(fallbackRecords, source, { maxMessages });
|
|
3592
|
+
}
|
|
3023
3593
|
|
|
3024
3594
|
extracted.messages = removeLeadingSystemMessage(Array.isArray(extracted.messages) ? extracted.messages : []);
|
|
3025
3595
|
|
|
@@ -3031,25 +3601,29 @@ async function exportSessionData(params = {}) {
|
|
|
3031
3601
|
}
|
|
3032
3602
|
|
|
3033
3603
|
const sessionId = extracted.sessionId || params.sessionId || path.basename(filePath, '.jsonl');
|
|
3034
|
-
const safeSessionId = String(sessionId).replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
3035
|
-
const sourceLabel = source === 'codex' ? 'Codex' : 'Claude Code';
|
|
3036
|
-
const
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3604
|
+
const safeSessionId = String(sessionId).replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
3605
|
+
const sourceLabel = source === 'codex' ? 'Codex' : 'Claude Code';
|
|
3606
|
+
const truncated = !!extracted.truncated;
|
|
3607
|
+
const maxMessagesLabel = maxMessages === Infinity ? 'all' : maxMessages;
|
|
3608
|
+
const markdown = buildSessionMarkdown({
|
|
3609
|
+
sourceLabel,
|
|
3610
|
+
sessionId,
|
|
3611
|
+
updatedAt: extracted.updatedAt,
|
|
3612
|
+
cwd: extracted.cwd,
|
|
3041
3613
|
filePath,
|
|
3042
3614
|
messages: extracted.messages
|
|
3043
3615
|
});
|
|
3044
3616
|
|
|
3045
|
-
return {
|
|
3046
|
-
source,
|
|
3047
|
-
sourceLabel,
|
|
3048
|
-
sessionId,
|
|
3049
|
-
fileName: `${source}-session-${safeSessionId}.md`,
|
|
3050
|
-
content: markdown
|
|
3051
|
-
|
|
3052
|
-
|
|
3617
|
+
return {
|
|
3618
|
+
source,
|
|
3619
|
+
sourceLabel,
|
|
3620
|
+
sessionId,
|
|
3621
|
+
fileName: `${source}-session-${safeSessionId}.md`,
|
|
3622
|
+
content: markdown,
|
|
3623
|
+
truncated,
|
|
3624
|
+
maxMessages: maxMessagesLabel
|
|
3625
|
+
};
|
|
3626
|
+
}
|
|
3053
3627
|
|
|
3054
3628
|
function buildExportPayload(includeKeys) {
|
|
3055
3629
|
const { config } = readConfigOrVirtualDefault();
|
|
@@ -3213,7 +3787,7 @@ function resolveSpeedTestTarget(params) {
|
|
|
3213
3787
|
return { error: 'Missing name or url' };
|
|
3214
3788
|
}
|
|
3215
3789
|
|
|
3216
|
-
function runSpeedTest(targetUrl, apiKey) {
|
|
3790
|
+
function runSpeedTest(targetUrl, apiKey, options = {}) {
|
|
3217
3791
|
return new Promise((resolve) => {
|
|
3218
3792
|
let parsed;
|
|
3219
3793
|
try {
|
|
@@ -3222,6 +3796,10 @@ function runSpeedTest(targetUrl, apiKey) {
|
|
|
3222
3796
|
return resolve({ ok: false, error: 'Invalid URL' });
|
|
3223
3797
|
}
|
|
3224
3798
|
|
|
3799
|
+
const timeoutMs = Number.isFinite(options.timeoutMs)
|
|
3800
|
+
? Math.max(1000, Number(options.timeoutMs))
|
|
3801
|
+
: SPEED_TEST_TIMEOUT_MS;
|
|
3802
|
+
|
|
3225
3803
|
const transport = parsed.protocol === 'https:' ? https : http;
|
|
3226
3804
|
const headers = {
|
|
3227
3805
|
'User-Agent': 'codexmate-speed-test',
|
|
@@ -3243,7 +3821,7 @@ function runSpeedTest(targetUrl, apiKey) {
|
|
|
3243
3821
|
});
|
|
3244
3822
|
});
|
|
3245
3823
|
|
|
3246
|
-
req.setTimeout(
|
|
3824
|
+
req.setTimeout(timeoutMs, () => {
|
|
3247
3825
|
req.destroy(new Error('timeout'));
|
|
3248
3826
|
});
|
|
3249
3827
|
|
|
@@ -3938,8 +4516,67 @@ function applyToClaudeSettings(config = {}) {
|
|
|
3938
4516
|
}
|
|
3939
4517
|
}
|
|
3940
4518
|
|
|
3941
|
-
|
|
3942
|
-
|
|
4519
|
+
function commandExists(command, args = '') {
|
|
4520
|
+
try {
|
|
4521
|
+
execSync(`${command} ${args}`, { stdio: 'ignore' });
|
|
4522
|
+
return true;
|
|
4523
|
+
} catch (e) {
|
|
4524
|
+
return false;
|
|
4525
|
+
}
|
|
4526
|
+
}
|
|
4527
|
+
|
|
4528
|
+
const SEVEN_ZIP_PATHS = [
|
|
4529
|
+
'C:\\Program Files\\7-Zip\\7z.exe',
|
|
4530
|
+
'C:\\Program Files (x86)\\7-Zip\\7z.exe',
|
|
4531
|
+
'7z'
|
|
4532
|
+
];
|
|
4533
|
+
|
|
4534
|
+
function findSevenZipExecutable() {
|
|
4535
|
+
for (const candidate of SEVEN_ZIP_PATHS) {
|
|
4536
|
+
try {
|
|
4537
|
+
if (candidate === '7z') {
|
|
4538
|
+
if (commandExists('7z', '--help')) {
|
|
4539
|
+
return '7z';
|
|
4540
|
+
}
|
|
4541
|
+
} else if (fs.existsSync(candidate)) {
|
|
4542
|
+
return candidate;
|
|
4543
|
+
}
|
|
4544
|
+
} catch (e) {}
|
|
4545
|
+
}
|
|
4546
|
+
return null;
|
|
4547
|
+
}
|
|
4548
|
+
|
|
4549
|
+
function resolveZipTool() {
|
|
4550
|
+
const sevenZipExe = findSevenZipExecutable();
|
|
4551
|
+
if (sevenZipExe) {
|
|
4552
|
+
return { type: '7z', cmd: sevenZipExe };
|
|
4553
|
+
}
|
|
4554
|
+
return { type: 'lib', cmd: 'zip-lib' };
|
|
4555
|
+
}
|
|
4556
|
+
|
|
4557
|
+
function resolveUnzipTool() {
|
|
4558
|
+
const sevenZipExe = findSevenZipExecutable();
|
|
4559
|
+
if (sevenZipExe) {
|
|
4560
|
+
return { type: '7z', cmd: sevenZipExe };
|
|
4561
|
+
}
|
|
4562
|
+
return { type: 'lib', cmd: 'zip-lib' };
|
|
4563
|
+
}
|
|
4564
|
+
|
|
4565
|
+
async function zipWithLibrary(absPath, outputPath) {
|
|
4566
|
+
const stat = fs.lstatSync(absPath);
|
|
4567
|
+
if (stat.isDirectory()) {
|
|
4568
|
+
await zipLib.archiveFolder(absPath, outputPath);
|
|
4569
|
+
return;
|
|
4570
|
+
}
|
|
4571
|
+
await zipLib.archiveFile(absPath, outputPath);
|
|
4572
|
+
}
|
|
4573
|
+
|
|
4574
|
+
async function unzipWithLibrary(zipPath, outputDir) {
|
|
4575
|
+
await zipLib.extract(zipPath, outputDir);
|
|
4576
|
+
}
|
|
4577
|
+
|
|
4578
|
+
// 压缩(7-Zip 优先)
|
|
4579
|
+
async function cmdZip(targetPath, options = {}) {
|
|
3943
4580
|
if (!targetPath) {
|
|
3944
4581
|
console.error('用法: codexmate zip <文件或文件夹路径> [--max:压缩级别]');
|
|
3945
4582
|
console.log('\n示例:');
|
|
@@ -3967,58 +4604,43 @@ function cmdZip(targetPath, options = {}) {
|
|
|
3967
4604
|
const outputDir = path.dirname(absPath);
|
|
3968
4605
|
const outputPath = path.join(outputDir, `${baseName}.zip`);
|
|
3969
4606
|
|
|
3970
|
-
|
|
3971
|
-
const sevenZipPaths = [
|
|
3972
|
-
'C:\\Program Files\\7-Zip\\7z.exe',
|
|
3973
|
-
'C:\\Program Files (x86)\\7-Zip\\7z.exe',
|
|
3974
|
-
'7z'
|
|
3975
|
-
];
|
|
3976
|
-
|
|
3977
|
-
let sevenZipExe = null;
|
|
3978
|
-
for (const p of sevenZipPaths) {
|
|
3979
|
-
try {
|
|
3980
|
-
if (p === '7z') {
|
|
3981
|
-
execSync('7z --help', { stdio: 'ignore' });
|
|
3982
|
-
sevenZipExe = '7z';
|
|
3983
|
-
break;
|
|
3984
|
-
} else if (fs.existsSync(p)) {
|
|
3985
|
-
sevenZipExe = p;
|
|
3986
|
-
break;
|
|
3987
|
-
}
|
|
3988
|
-
} catch (e) {}
|
|
3989
|
-
}
|
|
3990
|
-
|
|
3991
|
-
if (!sevenZipExe) {
|
|
3992
|
-
console.error('错误: 未找到 7-Zip,请先安装 7-Zip');
|
|
3993
|
-
console.log('下载地址: https://www.7-zip.org/');
|
|
3994
|
-
process.exit(1);
|
|
3995
|
-
}
|
|
4607
|
+
const zipTool = resolveZipTool();
|
|
3996
4608
|
|
|
3997
4609
|
console.log('\n压缩配置:');
|
|
3998
4610
|
console.log(' 源路径:', absPath);
|
|
3999
4611
|
console.log(' 输出文件:', outputPath);
|
|
4000
4612
|
console.log(' 压缩级别:', compressionLevel);
|
|
4001
|
-
console.log('
|
|
4613
|
+
console.log(' 压缩工具:', zipTool.type === '7z' ? '7-Zip' : 'zip-lib');
|
|
4614
|
+
console.log(' 多线程:', zipTool.type === '7z' ? '启用' : '未启用(JS 库)');
|
|
4615
|
+
if (zipTool.type !== '7z') {
|
|
4616
|
+
console.log(' 提示: JS 库不支持压缩级别,已忽略 --max');
|
|
4617
|
+
}
|
|
4002
4618
|
console.log('\n开始压缩...\n');
|
|
4003
4619
|
|
|
4004
4620
|
try {
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4621
|
+
if (zipTool.type === '7z') {
|
|
4622
|
+
const cmd = `"${zipTool.cmd}" a -tzip -mmt=on -mx=${compressionLevel} "${outputPath}" "${absPath}"`;
|
|
4623
|
+
const result = execSync(cmd, { encoding: 'utf-8', maxBuffer: 50 * 1024 * 1024 });
|
|
4624
|
+
const sizeMatch = result.match(/Archive size:\s*(\d+)\s*bytes/);
|
|
4625
|
+
const filesMatch = result.match(/(\d+)\s*files/);
|
|
4626
|
+
|
|
4627
|
+
console.log('✓ 压缩完成!');
|
|
4628
|
+
console.log(' 输出文件:', outputPath);
|
|
4629
|
+
if (sizeMatch) {
|
|
4630
|
+
const sizeBytes = parseInt(sizeMatch[1]);
|
|
4631
|
+
const sizeMB = (sizeBytes / 1024 / 1024).toFixed(2);
|
|
4632
|
+
console.log(' 压缩大小:', sizeMB, 'MB');
|
|
4633
|
+
}
|
|
4634
|
+
if (filesMatch) {
|
|
4635
|
+
console.log(' 文件数量:', filesMatch[1]);
|
|
4636
|
+
}
|
|
4637
|
+
console.log();
|
|
4638
|
+
return;
|
|
4639
|
+
}
|
|
4011
4640
|
|
|
4641
|
+
await zipWithLibrary(absPath, outputPath);
|
|
4012
4642
|
console.log('✓ 压缩完成!');
|
|
4013
4643
|
console.log(' 输出文件:', outputPath);
|
|
4014
|
-
if (sizeMatch) {
|
|
4015
|
-
const sizeBytes = parseInt(sizeMatch[1]);
|
|
4016
|
-
const sizeMB = (sizeBytes / 1024 / 1024).toFixed(2);
|
|
4017
|
-
console.log(' 压缩大小:', sizeMB, 'MB');
|
|
4018
|
-
}
|
|
4019
|
-
if (filesMatch) {
|
|
4020
|
-
console.log(' 文件数量:', filesMatch[1]);
|
|
4021
|
-
}
|
|
4022
4644
|
console.log();
|
|
4023
4645
|
} catch (e) {
|
|
4024
4646
|
console.error('压缩失败:', e.message);
|
|
@@ -4026,8 +4648,8 @@ function cmdZip(targetPath, options = {}) {
|
|
|
4026
4648
|
}
|
|
4027
4649
|
}
|
|
4028
4650
|
|
|
4029
|
-
//
|
|
4030
|
-
function cmdUnzip(zipPath, outputDir) {
|
|
4651
|
+
// 解压(7-Zip 优先)
|
|
4652
|
+
async function cmdUnzip(zipPath, outputDir) {
|
|
4031
4653
|
if (!zipPath) {
|
|
4032
4654
|
console.error('用法: codexmate unzip <zip文件路径> [输出目录]');
|
|
4033
4655
|
console.log('\n示例:');
|
|
@@ -4053,65 +4675,254 @@ function cmdUnzip(zipPath, outputDir) {
|
|
|
4053
4675
|
const defaultOutputDir = path.join(path.dirname(absZipPath), baseName);
|
|
4054
4676
|
const absOutputDir = outputDir ? path.resolve(outputDir) : defaultOutputDir;
|
|
4055
4677
|
|
|
4056
|
-
|
|
4057
|
-
const sevenZipPaths = [
|
|
4058
|
-
'C:\\Program Files\\7-Zip\\7z.exe',
|
|
4059
|
-
'C:\\Program Files (x86)\\7-Zip\\7z.exe',
|
|
4060
|
-
'7z'
|
|
4061
|
-
];
|
|
4062
|
-
|
|
4063
|
-
let sevenZipExe = null;
|
|
4064
|
-
for (const p of sevenZipPaths) {
|
|
4065
|
-
try {
|
|
4066
|
-
if (p === '7z') {
|
|
4067
|
-
execSync('7z --help', { stdio: 'ignore' });
|
|
4068
|
-
sevenZipExe = '7z';
|
|
4069
|
-
break;
|
|
4070
|
-
} else if (fs.existsSync(p)) {
|
|
4071
|
-
sevenZipExe = p;
|
|
4072
|
-
break;
|
|
4073
|
-
}
|
|
4074
|
-
} catch (e) {}
|
|
4075
|
-
}
|
|
4076
|
-
|
|
4077
|
-
if (!sevenZipExe) {
|
|
4078
|
-
console.error('错误: 未找到 7-Zip,请先安装 7-Zip');
|
|
4079
|
-
console.log('下载地址: https://www.7-zip.org/');
|
|
4080
|
-
process.exit(1);
|
|
4081
|
-
}
|
|
4678
|
+
const unzipTool = resolveUnzipTool();
|
|
4082
4679
|
|
|
4083
4680
|
console.log('\n解压配置:');
|
|
4084
4681
|
console.log(' 源文件:', absZipPath);
|
|
4085
4682
|
console.log(' 输出目录:', absOutputDir);
|
|
4086
|
-
console.log('
|
|
4683
|
+
console.log(' 解压工具:', unzipTool.type === '7z' ? '7-Zip' : 'zip-lib');
|
|
4684
|
+
console.log(' 多线程:', unzipTool.type === '7z' ? '启用' : '未启用(JS 库)');
|
|
4087
4685
|
console.log('\n开始解压...\n');
|
|
4088
4686
|
|
|
4089
4687
|
try {
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4688
|
+
if (unzipTool.type === '7z') {
|
|
4689
|
+
const cmd = `"${unzipTool.cmd}" x -mmt=on -o"${absOutputDir}" "${absZipPath}" -y`;
|
|
4690
|
+
const result = execSync(cmd, { encoding: 'utf-8', maxBuffer: 50 * 1024 * 1024 });
|
|
4691
|
+
const filesMatch = result.match(/(\d+)\s*files/);
|
|
4692
|
+
console.log('✓ 解压完成!');
|
|
4693
|
+
console.log(' 输出目录:', absOutputDir);
|
|
4694
|
+
if (filesMatch) {
|
|
4695
|
+
console.log(' 文件数量:', filesMatch[1]);
|
|
4696
|
+
}
|
|
4697
|
+
console.log();
|
|
4698
|
+
return;
|
|
4699
|
+
}
|
|
4095
4700
|
|
|
4701
|
+
await unzipWithLibrary(absZipPath, absOutputDir);
|
|
4096
4702
|
console.log('✓ 解压完成!');
|
|
4097
4703
|
console.log(' 输出目录:', absOutputDir);
|
|
4098
|
-
if (filesMatch) {
|
|
4099
|
-
console.log(' 文件数量:', filesMatch[1]);
|
|
4100
|
-
}
|
|
4101
4704
|
console.log();
|
|
4102
4705
|
} catch (e) {
|
|
4103
4706
|
console.error('解压失败:', e.message);
|
|
4104
4707
|
process.exit(1);
|
|
4105
|
-
}
|
|
4106
|
-
}
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4708
|
+
}
|
|
4709
|
+
}
|
|
4710
|
+
|
|
4711
|
+
function resolveExportOutputPath(outputPath, defaultFileName) {
|
|
4712
|
+
const fallback = path.resolve(process.cwd(), defaultFileName);
|
|
4713
|
+
if (typeof outputPath !== 'string' || !outputPath.trim()) {
|
|
4714
|
+
return fallback;
|
|
4715
|
+
}
|
|
4716
|
+
|
|
4717
|
+
const trimmed = outputPath.trim();
|
|
4718
|
+
const resolved = path.resolve(trimmed);
|
|
4719
|
+
const hasTrailingSep = /[\\\/]$/.test(trimmed);
|
|
4720
|
+
if (hasTrailingSep) {
|
|
4721
|
+
ensureDir(resolved);
|
|
4722
|
+
return path.join(resolved, defaultFileName);
|
|
4723
|
+
}
|
|
4724
|
+
|
|
4725
|
+
if (fs.existsSync(resolved)) {
|
|
4726
|
+
try {
|
|
4727
|
+
const stat = fs.statSync(resolved);
|
|
4728
|
+
if (stat.isDirectory()) {
|
|
4729
|
+
return path.join(resolved, defaultFileName);
|
|
4730
|
+
}
|
|
4731
|
+
} catch (e) {}
|
|
4732
|
+
}
|
|
4733
|
+
|
|
4734
|
+
return resolved;
|
|
4735
|
+
}
|
|
4736
|
+
|
|
4737
|
+
function printExportSessionUsage() {
|
|
4738
|
+
console.log('\n用法: codexmate export-session --source <codex|claude> (--session-id <ID>|--file <PATH>) [--output <PATH>] [--max-messages <N|all|Infinity>]');
|
|
4739
|
+
console.log('\n示例:');
|
|
4740
|
+
console.log(' codexmate export-session --source codex --session-id 123456');
|
|
4741
|
+
console.log(' codexmate export-session --source claude --file "~/.claude/projects/demo/session.jsonl"');
|
|
4742
|
+
console.log(' codexmate export-session --source codex --session-id 123456 --max-messages=all');
|
|
4743
|
+
}
|
|
4744
|
+
|
|
4745
|
+
function parseExportSessionArgs(args = []) {
|
|
4746
|
+
const options = {
|
|
4747
|
+
source: '',
|
|
4748
|
+
sessionId: '',
|
|
4749
|
+
filePath: '',
|
|
4750
|
+
output: '',
|
|
4751
|
+
maxMessages: undefined
|
|
4752
|
+
};
|
|
4753
|
+
const errors = [];
|
|
4754
|
+
|
|
4755
|
+
for (let i = 0; i < args.length; i++) {
|
|
4756
|
+
const arg = args[i];
|
|
4757
|
+
if (!arg) continue;
|
|
4758
|
+
|
|
4759
|
+
if (arg.startsWith('--source=')) {
|
|
4760
|
+
options.source = arg.slice('--source='.length);
|
|
4761
|
+
continue;
|
|
4762
|
+
}
|
|
4763
|
+
if (arg === '--source') {
|
|
4764
|
+
options.source = args[i + 1] || '';
|
|
4765
|
+
i += 1;
|
|
4766
|
+
continue;
|
|
4767
|
+
}
|
|
4768
|
+
if (arg.startsWith('--session-id=')) {
|
|
4769
|
+
options.sessionId = arg.slice('--session-id='.length);
|
|
4770
|
+
continue;
|
|
4771
|
+
}
|
|
4772
|
+
if (arg === '--session-id') {
|
|
4773
|
+
options.sessionId = args[i + 1] || '';
|
|
4774
|
+
i += 1;
|
|
4775
|
+
continue;
|
|
4776
|
+
}
|
|
4777
|
+
if (arg.startsWith('--file=')) {
|
|
4778
|
+
options.filePath = arg.slice('--file='.length);
|
|
4779
|
+
continue;
|
|
4780
|
+
}
|
|
4781
|
+
if (arg === '--file') {
|
|
4782
|
+
options.filePath = args[i + 1] || '';
|
|
4783
|
+
i += 1;
|
|
4784
|
+
continue;
|
|
4785
|
+
}
|
|
4786
|
+
if (arg.startsWith('--output=')) {
|
|
4787
|
+
options.output = arg.slice('--output='.length);
|
|
4788
|
+
continue;
|
|
4789
|
+
}
|
|
4790
|
+
if (arg === '--output') {
|
|
4791
|
+
options.output = args[i + 1] || '';
|
|
4792
|
+
i += 1;
|
|
4793
|
+
continue;
|
|
4794
|
+
}
|
|
4795
|
+
if (arg.startsWith('--max-messages=')) {
|
|
4796
|
+
options.maxMessages = arg.slice('--max-messages='.length);
|
|
4797
|
+
continue;
|
|
4798
|
+
}
|
|
4799
|
+
if (arg === '--max-messages') {
|
|
4800
|
+
options.maxMessages = args[i + 1] || '';
|
|
4801
|
+
i += 1;
|
|
4802
|
+
continue;
|
|
4803
|
+
}
|
|
4804
|
+
|
|
4805
|
+
errors.push(`未知参数: ${arg}`);
|
|
4806
|
+
}
|
|
4807
|
+
|
|
4808
|
+
const normalizedSource = options.source.trim().toLowerCase();
|
|
4809
|
+
if (normalizedSource && normalizedSource !== 'codex' && normalizedSource !== 'claude') {
|
|
4810
|
+
errors.push('参数 --source 仅支持 codex 或 claude');
|
|
4811
|
+
}
|
|
4812
|
+
options.source = normalizedSource;
|
|
4813
|
+
|
|
4814
|
+
if (!options.source) {
|
|
4815
|
+
errors.push('缺少 --source');
|
|
4816
|
+
}
|
|
4817
|
+
|
|
4818
|
+
if (!options.sessionId && !options.filePath) {
|
|
4819
|
+
errors.push('必须指定 --session-id 或 --file');
|
|
4820
|
+
}
|
|
4821
|
+
|
|
4822
|
+
if (options.maxMessages !== undefined) {
|
|
4823
|
+
const parsed = parseMaxMessagesValue(options.maxMessages);
|
|
4824
|
+
if (parsed === null) {
|
|
4825
|
+
errors.push('参数 --max-messages 无效');
|
|
4826
|
+
} else {
|
|
4827
|
+
options.maxMessages = parsed === Infinity ? Infinity : Math.max(1, Math.floor(parsed));
|
|
4828
|
+
}
|
|
4829
|
+
}
|
|
4830
|
+
|
|
4831
|
+
return {
|
|
4832
|
+
options,
|
|
4833
|
+
error: errors.length > 0 ? errors.join(';') : ''
|
|
4834
|
+
};
|
|
4835
|
+
}
|
|
4836
|
+
|
|
4837
|
+
async function cmdExportSession(args = []) {
|
|
4838
|
+
const parsed = parseExportSessionArgs(args);
|
|
4839
|
+
if (parsed.error) {
|
|
4840
|
+
console.error('错误:', parsed.error);
|
|
4841
|
+
printExportSessionUsage();
|
|
4842
|
+
process.exit(1);
|
|
4843
|
+
}
|
|
4844
|
+
|
|
4845
|
+
const options = parsed.options;
|
|
4846
|
+
const maxMessages = resolveMaxMessagesValue(options.maxMessages, MAX_EXPORT_MESSAGES);
|
|
4847
|
+
let result;
|
|
4848
|
+
try {
|
|
4849
|
+
result = await exportSessionData({
|
|
4850
|
+
source: options.source,
|
|
4851
|
+
sessionId: options.sessionId,
|
|
4852
|
+
filePath: options.filePath,
|
|
4853
|
+
maxMessages
|
|
4854
|
+
});
|
|
4855
|
+
} catch (e) {
|
|
4856
|
+
console.error('导出失败:', e.message || e);
|
|
4857
|
+
process.exit(1);
|
|
4858
|
+
}
|
|
4859
|
+
|
|
4860
|
+
if (result && result.error) {
|
|
4861
|
+
console.error('导出失败:', result.error);
|
|
4862
|
+
process.exit(1);
|
|
4863
|
+
}
|
|
4864
|
+
|
|
4865
|
+
const defaultFileName = (result && result.fileName)
|
|
4866
|
+
? result.fileName
|
|
4867
|
+
: `${options.source}-session-${options.sessionId || Date.now()}.md`;
|
|
4868
|
+
const outputPath = resolveExportOutputPath(options.output, defaultFileName);
|
|
4869
|
+
ensureDir(path.dirname(outputPath));
|
|
4870
|
+
fs.writeFileSync(outputPath, (result && result.content) ? result.content : '', 'utf-8');
|
|
4871
|
+
|
|
4872
|
+
console.log('\n✓ 会话已导出:', outputPath);
|
|
4873
|
+
if (result && result.truncated) {
|
|
4874
|
+
const label = maxMessages === Infinity ? 'all' : maxMessages;
|
|
4875
|
+
console.log(`! 已截断: 仅导出前 ${label} 条消息`);
|
|
4876
|
+
console.log(' 可使用 --max-messages=all 导出完整内容');
|
|
4877
|
+
}
|
|
4878
|
+
console.log();
|
|
4879
|
+
}
|
|
4880
|
+
|
|
4881
|
+
function parseStartOptions(args = []) {
|
|
4882
|
+
const options = { host: '' };
|
|
4883
|
+
if (!Array.isArray(args)) {
|
|
4884
|
+
return options;
|
|
4885
|
+
}
|
|
4886
|
+
|
|
4887
|
+
for (let i = 0; i < args.length; i++) {
|
|
4888
|
+
const arg = args[i];
|
|
4889
|
+
if (!arg) continue;
|
|
4890
|
+
if (arg.startsWith('--host=')) {
|
|
4891
|
+
options.host = arg.slice('--host='.length);
|
|
4892
|
+
continue;
|
|
4893
|
+
}
|
|
4894
|
+
if (arg === '--host') {
|
|
4895
|
+
options.host = args[i + 1] || '';
|
|
4896
|
+
i += 1;
|
|
4897
|
+
}
|
|
4898
|
+
}
|
|
4899
|
+
|
|
4900
|
+
return options;
|
|
4901
|
+
}
|
|
4902
|
+
|
|
4903
|
+
function isAnyAddressHost(host) {
|
|
4904
|
+
return host === '0.0.0.0' || host === '::';
|
|
4905
|
+
}
|
|
4906
|
+
|
|
4907
|
+
function formatHostForUrl(host) {
|
|
4908
|
+
const value = typeof host === 'string' ? host.trim() : '';
|
|
4909
|
+
if (!value) return '';
|
|
4910
|
+
if (value.startsWith('[') && value.endsWith(']')) {
|
|
4911
|
+
return value;
|
|
4912
|
+
}
|
|
4913
|
+
if (value.includes(':')) {
|
|
4914
|
+
return `[${value}]`;
|
|
4915
|
+
}
|
|
4916
|
+
return value;
|
|
4917
|
+
}
|
|
4918
|
+
|
|
4919
|
+
// 打开 Web UI
|
|
4920
|
+
function cmdStart(options = {}) {
|
|
4921
|
+
const htmlPath = path.join(__dirname, 'web-ui.html');
|
|
4922
|
+
if (!fs.existsSync(htmlPath)) {
|
|
4923
|
+
console.error('错误: web-ui.html 不存在');
|
|
4924
|
+
process.exit(1);
|
|
4925
|
+
}
|
|
4115
4926
|
|
|
4116
4927
|
const server = http.createServer((req, res) => {
|
|
4117
4928
|
if (req.url === '/api') {
|
|
@@ -4207,6 +5018,12 @@ function cmdStart() {
|
|
|
4207
5018
|
case 'apply-openclaw-agents-file':
|
|
4208
5019
|
result = applyOpenclawAgentsFile(params || {});
|
|
4209
5020
|
break;
|
|
5021
|
+
case 'get-openclaw-workspace-file':
|
|
5022
|
+
result = readOpenclawWorkspaceFile(params || {});
|
|
5023
|
+
break;
|
|
5024
|
+
case 'apply-openclaw-workspace-file':
|
|
5025
|
+
result = applyOpenclawWorkspaceFile(params || {});
|
|
5026
|
+
break;
|
|
4210
5027
|
case 'switch':
|
|
4211
5028
|
case 'use':
|
|
4212
5029
|
case 'add':
|
|
@@ -4255,12 +5072,21 @@ function cmdStart() {
|
|
|
4255
5072
|
case 'export-session':
|
|
4256
5073
|
result = await exportSessionData(params);
|
|
4257
5074
|
break;
|
|
4258
|
-
case 'session
|
|
4259
|
-
result = await
|
|
5075
|
+
case 'delete-session':
|
|
5076
|
+
result = await deleteSessionData(params || {});
|
|
4260
5077
|
break;
|
|
4261
|
-
|
|
4262
|
-
result =
|
|
4263
|
-
|
|
5078
|
+
case 'clone-session':
|
|
5079
|
+
result = await cloneCodexSession(params || {});
|
|
5080
|
+
break;
|
|
5081
|
+
case 'session-detail':
|
|
5082
|
+
result = await readSessionDetail(params);
|
|
5083
|
+
break;
|
|
5084
|
+
case 'session-plain':
|
|
5085
|
+
result = await readSessionPlain(params);
|
|
5086
|
+
break;
|
|
5087
|
+
default:
|
|
5088
|
+
result = { error: '未知操作' };
|
|
5089
|
+
}
|
|
4264
5090
|
|
|
4265
5091
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
4266
5092
|
res.end(JSON.stringify(result));
|
|
@@ -4276,18 +5102,28 @@ function cmdStart() {
|
|
|
4276
5102
|
}
|
|
4277
5103
|
});
|
|
4278
5104
|
|
|
4279
|
-
const port = resolveWebPort();
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
if (
|
|
4290
|
-
|
|
5105
|
+
const port = resolveWebPort();
|
|
5106
|
+
const host = resolveWebHost(options);
|
|
5107
|
+
const openHost = isAnyAddressHost(host) ? DEFAULT_WEB_HOST : host;
|
|
5108
|
+
const openUrl = `http://${formatHostForUrl(openHost)}:${port}`;
|
|
5109
|
+
server.listen(port, host, () => {
|
|
5110
|
+
console.log('\n✓ Web UI 已启动:', openUrl);
|
|
5111
|
+
if (host && host !== openHost) {
|
|
5112
|
+
console.log(' 监听地址:', host);
|
|
5113
|
+
}
|
|
5114
|
+
console.log(' 按 Ctrl+C 退出\n');
|
|
5115
|
+
if (isAnyAddressHost(host)) {
|
|
5116
|
+
console.warn('! 安全提示: 当前监听所有网卡(无鉴权)。');
|
|
5117
|
+
console.warn(' 建议仅在可信网络使用,或改用 --host 127.0.0.1。');
|
|
5118
|
+
}
|
|
5119
|
+
|
|
5120
|
+
// 打开浏览器
|
|
5121
|
+
const platform = process.platform;
|
|
5122
|
+
let command;
|
|
5123
|
+
const url = openUrl;
|
|
5124
|
+
|
|
5125
|
+
if (platform === 'win32') {
|
|
5126
|
+
command = `start "" "${url}"`;
|
|
4291
5127
|
} else if (platform === 'darwin') {
|
|
4292
5128
|
command = `open "${url}"`;
|
|
4293
5129
|
} else {
|
|
@@ -4324,14 +5160,15 @@ async function main() {
|
|
|
4324
5160
|
console.log(' codexmate use <模型> 切换模型');
|
|
4325
5161
|
console.log(' codexmate add <名称> <URL> [密钥]');
|
|
4326
5162
|
console.log(' codexmate delete <名称> 删除提供商');
|
|
4327
|
-
console.log(' codexmate add-model <模型> 添加模型');
|
|
4328
|
-
console.log(' codexmate delete-model <模型> 删除模型');
|
|
4329
|
-
console.log(' codexmate start
|
|
4330
|
-
console.log(' codexmate
|
|
4331
|
-
console.log(' codexmate
|
|
4332
|
-
console.log('');
|
|
4333
|
-
|
|
4334
|
-
|
|
5163
|
+
console.log(' codexmate add-model <模型> 添加模型');
|
|
5164
|
+
console.log(' codexmate delete-model <模型> 删除模型');
|
|
5165
|
+
console.log(' codexmate start [--host <HOST>] 启动 Web 界面');
|
|
5166
|
+
console.log(' codexmate export-session --source <codex|claude> (--session-id <ID>|--file <PATH>) [--output <PATH>] [--max-messages <N|all|Infinity>]');
|
|
5167
|
+
console.log(' codexmate zip <路径> [--max:级别] 压缩(7-Zip 优先)');
|
|
5168
|
+
console.log(' codexmate unzip <zip文件> [输出目录] 解压(7-Zip 优先)');
|
|
5169
|
+
console.log('');
|
|
5170
|
+
process.exit(0);
|
|
5171
|
+
}
|
|
4335
5172
|
|
|
4336
5173
|
const command = args[0];
|
|
4337
5174
|
|
|
@@ -4344,12 +5181,13 @@ async function main() {
|
|
|
4344
5181
|
case 'use': cmdUseModel(args[1]); break;
|
|
4345
5182
|
case 'add': cmdAdd(args[1], args[2], args[3]); break;
|
|
4346
5183
|
case 'delete': cmdDelete(args[1]); break;
|
|
4347
|
-
case 'add-model': cmdAddModel(args[1]); break;
|
|
4348
|
-
case 'delete-model': cmdDeleteModel(args[1]); break;
|
|
4349
|
-
case 'start': cmdStart(); break;
|
|
4350
|
-
case '
|
|
4351
|
-
|
|
4352
|
-
|
|
5184
|
+
case 'add-model': cmdAddModel(args[1]); break;
|
|
5185
|
+
case 'delete-model': cmdDeleteModel(args[1]); break;
|
|
5186
|
+
case 'start': cmdStart(parseStartOptions(args.slice(1))); break;
|
|
5187
|
+
case 'export-session': await cmdExportSession(args.slice(1)); break;
|
|
5188
|
+
case 'zip': {
|
|
5189
|
+
// 解析 --max:N 参数
|
|
5190
|
+
const zipOptions = {};
|
|
4353
5191
|
let targetPath = null;
|
|
4354
5192
|
for (let i = 1; i < args.length; i++) {
|
|
4355
5193
|
const arg = args[i];
|
|
@@ -4359,10 +5197,10 @@ async function main() {
|
|
|
4359
5197
|
targetPath = arg;
|
|
4360
5198
|
}
|
|
4361
5199
|
}
|
|
4362
|
-
cmdZip(targetPath, zipOptions);
|
|
5200
|
+
await cmdZip(targetPath, zipOptions);
|
|
4363
5201
|
break;
|
|
4364
5202
|
}
|
|
4365
|
-
case 'unzip': cmdUnzip(args[1], args[2]); break;
|
|
5203
|
+
case 'unzip': await cmdUnzip(args[1], args[2]); break;
|
|
4366
5204
|
default:
|
|
4367
5205
|
console.error('错误: 未知命令:', command);
|
|
4368
5206
|
console.log('运行 "codexmate" 查看帮助');
|
|
@@ -4374,3 +5212,4 @@ main().catch((err) => {
|
|
|
4374
5212
|
console.error('错误:', err && err.message ? err.message : err);
|
|
4375
5213
|
process.exit(1);
|
|
4376
5214
|
});
|
|
5215
|
+
|