codexmate 0.0.17 → 0.0.18
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.en.md +42 -23
- package/README.md +42 -23
- package/cli.js +1447 -157
- package/lib/text-diff.js +303 -0
- package/package.json +1 -1
- package/web-ui/app.js +1728 -202
- package/web-ui/index.html +306 -77
- package/web-ui/logic.mjs +390 -0
- package/web-ui/modules/skills.methods.mjs +7 -1
- package/web-ui/session-helpers.mjs +350 -0
- package/web-ui/styles.css +481 -11
package/cli.js
CHANGED
|
@@ -38,6 +38,7 @@ const {
|
|
|
38
38
|
writeJsonAtomic,
|
|
39
39
|
formatTimestampForFileName
|
|
40
40
|
} = require('./lib/cli-file-utils');
|
|
41
|
+
const { buildLineDiff } = require('./lib/text-diff');
|
|
41
42
|
const {
|
|
42
43
|
extractModelNames,
|
|
43
44
|
hasModelsListPayload,
|
|
@@ -78,6 +79,9 @@ const CURRENT_MODELS_FILE = path.join(CONFIG_DIR, 'provider-current-models.json'
|
|
|
78
79
|
const INIT_MARK_FILE = path.join(CONFIG_DIR, 'codexmate-init.json');
|
|
79
80
|
const BUILTIN_PROXY_SETTINGS_FILE = path.join(CONFIG_DIR, 'codexmate-proxy.json');
|
|
80
81
|
const CODEX_SESSIONS_DIR = path.join(CONFIG_DIR, 'sessions');
|
|
82
|
+
const SESSION_TRASH_DIR = path.join(CONFIG_DIR, 'codexmate-session-trash');
|
|
83
|
+
const SESSION_TRASH_FILES_DIR = path.join(SESSION_TRASH_DIR, 'files');
|
|
84
|
+
const SESSION_TRASH_INDEX_FILE = path.join(SESSION_TRASH_DIR, 'index.json');
|
|
81
85
|
const OPENCLAW_DIR = path.join(os.homedir(), '.openclaw');
|
|
82
86
|
const OPENCLAW_CONFIG_FILE = path.join(OPENCLAW_DIR, 'openclaw.json');
|
|
83
87
|
const OPENCLAW_WORKSPACE_DIR = path.join(OPENCLAW_DIR, 'workspace');
|
|
@@ -94,6 +98,7 @@ const DEFAULT_MODELS = ['gpt-5.3-codex', 'gpt-5.1-codex-max', 'gpt-4-turbo', 'gp
|
|
|
94
98
|
const SPEED_TEST_TIMEOUT_MS = 8000;
|
|
95
99
|
const HEALTH_CHECK_TIMEOUT_MS = 6000;
|
|
96
100
|
const MAX_SESSION_LIST_SIZE = 300;
|
|
101
|
+
const MAX_SESSION_TRASH_LIST_SIZE = 500;
|
|
97
102
|
const MAX_EXPORT_MESSAGES = 1000;
|
|
98
103
|
const DEFAULT_SESSION_DETAIL_MESSAGES = 300;
|
|
99
104
|
const MAX_SESSION_DETAIL_MESSAGES = 1000;
|
|
@@ -102,6 +107,7 @@ const CODEXMATE_MANAGED_MARKER = '# codexmate-managed: true';
|
|
|
102
107
|
const SESSION_LIST_CACHE_TTL_MS = 4000;
|
|
103
108
|
const SESSION_SUMMARY_READ_BYTES = 256 * 1024;
|
|
104
109
|
const SESSION_CONTENT_READ_BYTES = SESSION_SUMMARY_READ_BYTES;
|
|
110
|
+
const EXACT_MESSAGE_COUNT_CACHE_MAX_ENTRIES = 800;
|
|
105
111
|
const DEFAULT_CONTENT_SCAN_LIMIT = 50;
|
|
106
112
|
const SESSION_SCAN_FACTOR = 4;
|
|
107
113
|
const SESSION_SCAN_MIN_FILES = 800;
|
|
@@ -121,6 +127,7 @@ const MODELS_RESPONSE_MAX_BYTES = 1024 * 1024;
|
|
|
121
127
|
const MAX_RECENT_CONFIGS = 3;
|
|
122
128
|
const MAX_UPLOAD_SIZE = 200 * 1024 * 1024;
|
|
123
129
|
const MAX_SKILLS_ZIP_UPLOAD_SIZE = 20 * 1024 * 1024;
|
|
130
|
+
const MAX_API_BODY_SIZE = 4 * 1024 * 1024;
|
|
124
131
|
const MAX_SKILLS_ZIP_ENTRY_COUNT = 2000;
|
|
125
132
|
const MAX_SKILLS_ZIP_UNCOMPRESSED_BYTES = 512 * 1024 * 1024;
|
|
126
133
|
const DEFAULT_EXTRACT_SUFFIXES = Object.freeze(['.json']);
|
|
@@ -202,6 +209,7 @@ stream_idle_timeout_ms = 300000
|
|
|
202
209
|
|
|
203
210
|
let g_initNotice = '';
|
|
204
211
|
let g_sessionListCache = new Map();
|
|
212
|
+
let g_exactMessageCountCache = new Map();
|
|
205
213
|
let g_modelsCache = new Map();
|
|
206
214
|
let g_modelsInFlight = new Map();
|
|
207
215
|
let g_builtinProxyRuntime = null;
|
|
@@ -2218,6 +2226,15 @@ function readAgentsFile(params = {}) {
|
|
|
2218
2226
|
};
|
|
2219
2227
|
}
|
|
2220
2228
|
|
|
2229
|
+
if (params.metaOnly) {
|
|
2230
|
+
return {
|
|
2231
|
+
exists: true,
|
|
2232
|
+
path: filePath,
|
|
2233
|
+
content: '',
|
|
2234
|
+
lineEnding: os.EOL === '\r\n' ? '\r\n' : '\n'
|
|
2235
|
+
};
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2221
2238
|
try {
|
|
2222
2239
|
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
2223
2240
|
return {
|
|
@@ -2251,6 +2268,48 @@ function applyAgentsFile(params = {}) {
|
|
|
2251
2268
|
}
|
|
2252
2269
|
}
|
|
2253
2270
|
|
|
2271
|
+
function normalizeDiffText(input) {
|
|
2272
|
+
const safe = typeof input === 'string' ? input : '';
|
|
2273
|
+
return normalizeLineEnding(stripUtf8Bom(safe), '\n');
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2276
|
+
function buildAgentsDiff(params = {}) {
|
|
2277
|
+
const hasBaseContent = typeof params.baseContent === 'string';
|
|
2278
|
+
const contextRaw = typeof params.context === 'string' ? params.context.trim() : '';
|
|
2279
|
+
const context = contextRaw || 'codex';
|
|
2280
|
+
const metaOnly = hasBaseContent;
|
|
2281
|
+
let readResult;
|
|
2282
|
+
if (context === 'openclaw') {
|
|
2283
|
+
readResult = readOpenclawAgentsFile({ metaOnly });
|
|
2284
|
+
} else if (context === 'openclaw-workspace') {
|
|
2285
|
+
readResult = readOpenclawWorkspaceFile({ ...params, metaOnly });
|
|
2286
|
+
} else if (context === 'codex') {
|
|
2287
|
+
readResult = readAgentsFile({ ...params, metaOnly });
|
|
2288
|
+
} else {
|
|
2289
|
+
return { error: `Unsupported agents diff context: ${context}` };
|
|
2290
|
+
}
|
|
2291
|
+
if (readResult && readResult.error) {
|
|
2292
|
+
return { error: readResult.error };
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
const beforeText = normalizeDiffText(
|
|
2296
|
+
hasBaseContent ? params.baseContent : (readResult && readResult.content ? readResult.content : '')
|
|
2297
|
+
);
|
|
2298
|
+
const afterText = normalizeDiffText(params.content);
|
|
2299
|
+
const diff = buildLineDiff(beforeText, afterText);
|
|
2300
|
+
const hasChanges = diff.truncated ? beforeText !== afterText : (diff.stats.added > 0 || diff.stats.removed > 0);
|
|
2301
|
+
return {
|
|
2302
|
+
diff: {
|
|
2303
|
+
...diff,
|
|
2304
|
+
hasChanges
|
|
2305
|
+
},
|
|
2306
|
+
path: readResult && readResult.path ? readResult.path : '',
|
|
2307
|
+
exists: !!(readResult && readResult.exists),
|
|
2308
|
+
context,
|
|
2309
|
+
configError: readResult && readResult.configError ? readResult.configError : ''
|
|
2310
|
+
};
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2254
2313
|
function resolveOpenclawWorkspaceDir(config) {
|
|
2255
2314
|
const workspace = config
|
|
2256
2315
|
&& config.agents
|
|
@@ -2348,7 +2407,7 @@ function getOpenclawWorkspaceInfo() {
|
|
|
2348
2407
|
};
|
|
2349
2408
|
}
|
|
2350
2409
|
|
|
2351
|
-
function readOpenclawAgentsFile() {
|
|
2410
|
+
function readOpenclawAgentsFile(params = {}) {
|
|
2352
2411
|
const workspaceInfo = getOpenclawWorkspaceInfo();
|
|
2353
2412
|
const baseDir = workspaceInfo.workspaceDir;
|
|
2354
2413
|
const filePath = path.join(baseDir, AGENTS_FILE_NAME);
|
|
@@ -2365,7 +2424,7 @@ function readOpenclawAgentsFile() {
|
|
|
2365
2424
|
};
|
|
2366
2425
|
}
|
|
2367
2426
|
|
|
2368
|
-
const readResult = readAgentsFile({ baseDir });
|
|
2427
|
+
const readResult = readAgentsFile({ baseDir, metaOnly: !!params.metaOnly });
|
|
2369
2428
|
return {
|
|
2370
2429
|
...readResult,
|
|
2371
2430
|
workspaceDir: baseDir,
|
|
@@ -2420,6 +2479,17 @@ function readOpenclawWorkspaceFile(params = {}) {
|
|
|
2420
2479
|
};
|
|
2421
2480
|
}
|
|
2422
2481
|
|
|
2482
|
+
if (params.metaOnly) {
|
|
2483
|
+
return {
|
|
2484
|
+
exists: true,
|
|
2485
|
+
path: filePath,
|
|
2486
|
+
content: '',
|
|
2487
|
+
lineEnding: os.EOL === '\r\n' ? '\r\n' : '\n',
|
|
2488
|
+
workspaceDir: baseDir,
|
|
2489
|
+
configError: workspaceInfo.configError
|
|
2490
|
+
};
|
|
2491
|
+
}
|
|
2492
|
+
|
|
2423
2493
|
try {
|
|
2424
2494
|
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
2425
2495
|
return {
|
|
@@ -3656,6 +3726,102 @@ function parseJsonlHeadRecords(filePath, maxBytes = SESSION_SUMMARY_READ_BYTES)
|
|
|
3656
3726
|
return parseJsonlContent(headText);
|
|
3657
3727
|
}
|
|
3658
3728
|
|
|
3729
|
+
function buildClaudeStoredIndexMessageCount(messageCount) {
|
|
3730
|
+
const safeCount = Number.isFinite(Number(messageCount))
|
|
3731
|
+
? Math.max(0, Math.floor(Number(messageCount)))
|
|
3732
|
+
: 0;
|
|
3733
|
+
return safeCount + 1;
|
|
3734
|
+
}
|
|
3735
|
+
|
|
3736
|
+
function getFileStatSafe(filePath) {
|
|
3737
|
+
try {
|
|
3738
|
+
return fs.statSync(filePath);
|
|
3739
|
+
} catch (e) {
|
|
3740
|
+
return null;
|
|
3741
|
+
}
|
|
3742
|
+
}
|
|
3743
|
+
|
|
3744
|
+
function getFileMtimeMs(filePath, stat = null) {
|
|
3745
|
+
const fileStat = stat || getFileStatSafe(filePath);
|
|
3746
|
+
if (!fileStat || !Number.isFinite(Number(fileStat.mtimeMs))) {
|
|
3747
|
+
return 0;
|
|
3748
|
+
}
|
|
3749
|
+
return Math.max(0, Math.floor(Number(fileStat.mtimeMs)));
|
|
3750
|
+
}
|
|
3751
|
+
|
|
3752
|
+
function isSessionSummaryMessageCountExact(stat, maxBytes = SESSION_SUMMARY_READ_BYTES) {
|
|
3753
|
+
if (!stat || !Number.isFinite(Number(stat.size))) {
|
|
3754
|
+
return false;
|
|
3755
|
+
}
|
|
3756
|
+
return Number(stat.size) <= maxBytes;
|
|
3757
|
+
}
|
|
3758
|
+
|
|
3759
|
+
function buildExactMessageCountCacheKey(filePath, source, stat = null) {
|
|
3760
|
+
const validSource = source === 'claude' ? 'claude' : (source === 'codex' ? 'codex' : '');
|
|
3761
|
+
if (!validSource || !filePath) {
|
|
3762
|
+
return '';
|
|
3763
|
+
}
|
|
3764
|
+
const mtimeMs = getFileMtimeMs(filePath, stat);
|
|
3765
|
+
if (!mtimeMs) {
|
|
3766
|
+
return '';
|
|
3767
|
+
}
|
|
3768
|
+
return `${validSource}:${path.resolve(filePath)}:${mtimeMs}`;
|
|
3769
|
+
}
|
|
3770
|
+
|
|
3771
|
+
function readExactMessageCountCache(filePath, source, stat = null) {
|
|
3772
|
+
const cacheKey = buildExactMessageCountCacheKey(filePath, source, stat);
|
|
3773
|
+
if (!cacheKey) {
|
|
3774
|
+
return null;
|
|
3775
|
+
}
|
|
3776
|
+
if (!g_exactMessageCountCache.has(cacheKey)) {
|
|
3777
|
+
return null;
|
|
3778
|
+
}
|
|
3779
|
+
const cached = g_exactMessageCountCache.get(cacheKey);
|
|
3780
|
+
g_exactMessageCountCache.delete(cacheKey);
|
|
3781
|
+
g_exactMessageCountCache.set(cacheKey, cached);
|
|
3782
|
+
return Number.isFinite(Number(cached)) ? Math.max(0, Math.floor(Number(cached))) : null;
|
|
3783
|
+
}
|
|
3784
|
+
|
|
3785
|
+
function writeExactMessageCountCache(filePath, source, messageCount, stat = null) {
|
|
3786
|
+
const cacheKey = buildExactMessageCountCacheKey(filePath, source, stat);
|
|
3787
|
+
const safeCount = Number.isFinite(Number(messageCount))
|
|
3788
|
+
? Math.max(0, Math.floor(Number(messageCount)))
|
|
3789
|
+
: null;
|
|
3790
|
+
if (!cacheKey || safeCount === null) {
|
|
3791
|
+
return;
|
|
3792
|
+
}
|
|
3793
|
+
if (g_exactMessageCountCache.has(cacheKey)) {
|
|
3794
|
+
g_exactMessageCountCache.delete(cacheKey);
|
|
3795
|
+
}
|
|
3796
|
+
g_exactMessageCountCache.set(cacheKey, safeCount);
|
|
3797
|
+
if (g_exactMessageCountCache.size <= EXACT_MESSAGE_COUNT_CACHE_MAX_ENTRIES) {
|
|
3798
|
+
return;
|
|
3799
|
+
}
|
|
3800
|
+
const firstKey = g_exactMessageCountCache.keys().next().value;
|
|
3801
|
+
if (firstKey) {
|
|
3802
|
+
g_exactMessageCountCache.delete(firstKey);
|
|
3803
|
+
}
|
|
3804
|
+
}
|
|
3805
|
+
|
|
3806
|
+
async function mapWithConcurrency(items, concurrency, mapper) {
|
|
3807
|
+
const list = Array.isArray(items) ? items : [];
|
|
3808
|
+
if (list.length === 0) {
|
|
3809
|
+
return [];
|
|
3810
|
+
}
|
|
3811
|
+
const safeConcurrency = Math.max(1, Math.min(Math.floor(Number(concurrency)) || 1, list.length));
|
|
3812
|
+
const results = new Array(list.length);
|
|
3813
|
+
let nextIndex = 0;
|
|
3814
|
+
const workers = Array.from({ length: safeConcurrency }, async () => {
|
|
3815
|
+
while (nextIndex < list.length) {
|
|
3816
|
+
const currentIndex = nextIndex;
|
|
3817
|
+
nextIndex += 1;
|
|
3818
|
+
results[currentIndex] = await mapper(list[currentIndex], currentIndex);
|
|
3819
|
+
}
|
|
3820
|
+
});
|
|
3821
|
+
await Promise.all(workers);
|
|
3822
|
+
return results.filter((item) => item !== undefined);
|
|
3823
|
+
}
|
|
3824
|
+
|
|
3659
3825
|
function isBootstrapLikeText(text) {
|
|
3660
3826
|
if (!text || typeof text !== 'string') {
|
|
3661
3827
|
return false;
|
|
@@ -3723,6 +3889,307 @@ function countConversationMessagesInRecords(records, source) {
|
|
|
3723
3889
|
return removeLeadingSystemMessage(messages).length;
|
|
3724
3890
|
}
|
|
3725
3891
|
|
|
3892
|
+
async function countConversationMessagesInFile(filePath, source) {
|
|
3893
|
+
const fileStat = getFileStatSafe(filePath);
|
|
3894
|
+
const cached = readExactMessageCountCache(filePath, source, fileStat);
|
|
3895
|
+
if (cached !== null) {
|
|
3896
|
+
return cached;
|
|
3897
|
+
}
|
|
3898
|
+
|
|
3899
|
+
let stream;
|
|
3900
|
+
let rl;
|
|
3901
|
+
let messageCount = 0;
|
|
3902
|
+
let leadingSystem = true;
|
|
3903
|
+
|
|
3904
|
+
try {
|
|
3905
|
+
stream = fs.createReadStream(filePath, { encoding: 'utf-8' });
|
|
3906
|
+
rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
3907
|
+
|
|
3908
|
+
for await (const line of rl) {
|
|
3909
|
+
const trimmed = line.trim();
|
|
3910
|
+
if (!trimmed) continue;
|
|
3911
|
+
|
|
3912
|
+
let record;
|
|
3913
|
+
try {
|
|
3914
|
+
record = JSON.parse(trimmed);
|
|
3915
|
+
} catch (e) {
|
|
3916
|
+
continue;
|
|
3917
|
+
}
|
|
3918
|
+
|
|
3919
|
+
let role = '';
|
|
3920
|
+
let text = '';
|
|
3921
|
+
if (source === 'codex') {
|
|
3922
|
+
if (record.type === 'response_item' && record.payload && record.payload.type === 'message') {
|
|
3923
|
+
role = normalizeRole(record.payload.role);
|
|
3924
|
+
text = extractMessageText(record.payload.content);
|
|
3925
|
+
}
|
|
3926
|
+
} else {
|
|
3927
|
+
role = normalizeRole(record.type);
|
|
3928
|
+
if (role === 'assistant' || role === 'user' || role === 'system') {
|
|
3929
|
+
const content = record.message ? record.message.content : '';
|
|
3930
|
+
text = extractMessageText(content);
|
|
3931
|
+
} else {
|
|
3932
|
+
role = '';
|
|
3933
|
+
}
|
|
3934
|
+
}
|
|
3935
|
+
if (!role) {
|
|
3936
|
+
continue;
|
|
3937
|
+
}
|
|
3938
|
+
|
|
3939
|
+
const hasText = text.length > 0;
|
|
3940
|
+
if (leadingSystem && (role === 'system' || (hasText && isBootstrapLikeText(text)))) {
|
|
3941
|
+
continue;
|
|
3942
|
+
}
|
|
3943
|
+
|
|
3944
|
+
leadingSystem = false;
|
|
3945
|
+
messageCount += 1;
|
|
3946
|
+
}
|
|
3947
|
+
const safeCount = Math.max(0, messageCount);
|
|
3948
|
+
writeExactMessageCountCache(filePath, source, safeCount, fileStat);
|
|
3949
|
+
return safeCount;
|
|
3950
|
+
} catch (e) {
|
|
3951
|
+
const safeCount = countConversationMessagesInRecords(readJsonlRecords(filePath), source);
|
|
3952
|
+
writeExactMessageCountCache(filePath, source, safeCount, fileStat);
|
|
3953
|
+
return safeCount;
|
|
3954
|
+
} finally {
|
|
3955
|
+
if (rl) {
|
|
3956
|
+
try { rl.close(); } catch (e) {}
|
|
3957
|
+
}
|
|
3958
|
+
if (stream && !stream.destroyed && stream.destroy) {
|
|
3959
|
+
try { stream.destroy(); } catch (e) {}
|
|
3960
|
+
}
|
|
3961
|
+
}
|
|
3962
|
+
}
|
|
3963
|
+
|
|
3964
|
+
function appendSessionDetailTailMessage(state, record, source, lineIndex = -1) {
|
|
3965
|
+
if (!state || typeof state !== 'object') {
|
|
3966
|
+
return;
|
|
3967
|
+
}
|
|
3968
|
+
|
|
3969
|
+
const message = extractMessageFromRecord(record, source);
|
|
3970
|
+
if (!message) {
|
|
3971
|
+
return;
|
|
3972
|
+
}
|
|
3973
|
+
|
|
3974
|
+
const role = normalizeRole(message.role);
|
|
3975
|
+
const text = typeof message.text === 'string' ? message.text : '';
|
|
3976
|
+
if (!role || !text) {
|
|
3977
|
+
return;
|
|
3978
|
+
}
|
|
3979
|
+
|
|
3980
|
+
if (state.leadingSystem && (role === 'system' || isBootstrapLikeText(text))) {
|
|
3981
|
+
return;
|
|
3982
|
+
}
|
|
3983
|
+
|
|
3984
|
+
state.leadingSystem = false;
|
|
3985
|
+
state.totalMessages += 1;
|
|
3986
|
+
if (!Number.isFinite(state.tailLimit) || state.tailLimit <= 0) {
|
|
3987
|
+
return;
|
|
3988
|
+
}
|
|
3989
|
+
|
|
3990
|
+
if (state.messages.length >= state.tailLimit) {
|
|
3991
|
+
state.messages.shift();
|
|
3992
|
+
}
|
|
3993
|
+
state.messages.push({
|
|
3994
|
+
role,
|
|
3995
|
+
text,
|
|
3996
|
+
timestamp: toIsoTime(record && record.timestamp, ''),
|
|
3997
|
+
recordLineIndex: Number.isInteger(lineIndex) ? lineIndex : -1
|
|
3998
|
+
});
|
|
3999
|
+
}
|
|
4000
|
+
|
|
4001
|
+
function applySessionDetailRecordMetadata(record, source, state) {
|
|
4002
|
+
if (!state || typeof state !== 'object' || !record) {
|
|
4003
|
+
return;
|
|
4004
|
+
}
|
|
4005
|
+
|
|
4006
|
+
if (record.timestamp) {
|
|
4007
|
+
state.updatedAt = toIsoTime(record.timestamp, state.updatedAt);
|
|
4008
|
+
}
|
|
4009
|
+
|
|
4010
|
+
if (source === 'codex') {
|
|
4011
|
+
if (record.type === 'session_meta' && record.payload) {
|
|
4012
|
+
state.sessionId = record.payload.id || state.sessionId;
|
|
4013
|
+
state.cwd = record.payload.cwd || state.cwd;
|
|
4014
|
+
}
|
|
4015
|
+
return;
|
|
4016
|
+
}
|
|
4017
|
+
|
|
4018
|
+
if (!state.sessionId && record.sessionId) {
|
|
4019
|
+
state.sessionId = record.sessionId;
|
|
4020
|
+
}
|
|
4021
|
+
if (!state.cwd && record.cwd) {
|
|
4022
|
+
state.cwd = record.cwd;
|
|
4023
|
+
}
|
|
4024
|
+
}
|
|
4025
|
+
|
|
4026
|
+
function extractSessionDetailPreviewFromRecords(records, source, messageLimit) {
|
|
4027
|
+
const safeMessageLimit = Number.isFinite(Number(messageLimit))
|
|
4028
|
+
? Math.max(1, Math.floor(Number(messageLimit)))
|
|
4029
|
+
: DEFAULT_SESSION_DETAIL_MESSAGES;
|
|
4030
|
+
const state = {
|
|
4031
|
+
sessionId: '',
|
|
4032
|
+
cwd: '',
|
|
4033
|
+
updatedAt: '',
|
|
4034
|
+
messages: [],
|
|
4035
|
+
tailLimit: safeMessageLimit,
|
|
4036
|
+
totalMessages: 0,
|
|
4037
|
+
leadingSystem: true
|
|
4038
|
+
};
|
|
4039
|
+
|
|
4040
|
+
for (let lineIndex = 0; lineIndex < records.length; lineIndex++) {
|
|
4041
|
+
const record = records[lineIndex];
|
|
4042
|
+
applySessionDetailRecordMetadata(record, source, state);
|
|
4043
|
+
appendSessionDetailTailMessage(state, record, source, lineIndex);
|
|
4044
|
+
}
|
|
4045
|
+
|
|
4046
|
+
return state;
|
|
4047
|
+
}
|
|
4048
|
+
|
|
4049
|
+
async function extractSessionDetailPreviewFromFile(filePath, source, messageLimit) {
|
|
4050
|
+
const safeMessageLimit = Number.isFinite(Number(messageLimit))
|
|
4051
|
+
? Math.max(1, Math.floor(Number(messageLimit)))
|
|
4052
|
+
: DEFAULT_SESSION_DETAIL_MESSAGES;
|
|
4053
|
+
const state = {
|
|
4054
|
+
sessionId: '',
|
|
4055
|
+
cwd: '',
|
|
4056
|
+
updatedAt: '',
|
|
4057
|
+
messages: [],
|
|
4058
|
+
tailLimit: safeMessageLimit,
|
|
4059
|
+
totalMessages: 0,
|
|
4060
|
+
leadingSystem: true
|
|
4061
|
+
};
|
|
4062
|
+
|
|
4063
|
+
let stream;
|
|
4064
|
+
let rl;
|
|
4065
|
+
try {
|
|
4066
|
+
stream = fs.createReadStream(filePath, { encoding: 'utf-8' });
|
|
4067
|
+
rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
4068
|
+
|
|
4069
|
+
let lineIndex = 0;
|
|
4070
|
+
for await (const line of rl) {
|
|
4071
|
+
const currentLineIndex = lineIndex;
|
|
4072
|
+
lineIndex += 1;
|
|
4073
|
+
|
|
4074
|
+
const trimmed = line.trim();
|
|
4075
|
+
if (!trimmed) {
|
|
4076
|
+
continue;
|
|
4077
|
+
}
|
|
4078
|
+
|
|
4079
|
+
let record;
|
|
4080
|
+
try {
|
|
4081
|
+
record = JSON.parse(trimmed);
|
|
4082
|
+
} catch (e) {
|
|
4083
|
+
continue;
|
|
4084
|
+
}
|
|
4085
|
+
|
|
4086
|
+
applySessionDetailRecordMetadata(record, source, state);
|
|
4087
|
+
appendSessionDetailTailMessage(state, record, source, currentLineIndex);
|
|
4088
|
+
}
|
|
4089
|
+
return state;
|
|
4090
|
+
} catch (e) {
|
|
4091
|
+
return extractSessionDetailPreviewFromRecords(readJsonlRecords(filePath), source, safeMessageLimit);
|
|
4092
|
+
} finally {
|
|
4093
|
+
if (rl) {
|
|
4094
|
+
try { rl.close(); } catch (e) {}
|
|
4095
|
+
}
|
|
4096
|
+
if (stream && !stream.destroyed && stream.destroy) {
|
|
4097
|
+
try { stream.destroy(); } catch (e) {}
|
|
4098
|
+
}
|
|
4099
|
+
}
|
|
4100
|
+
}
|
|
4101
|
+
|
|
4102
|
+
async function resolveSessionTrashEntryExactMessageCount(entry) {
|
|
4103
|
+
const normalizedEntry = normalizeSessionTrashEntry(entry);
|
|
4104
|
+
if (!normalizedEntry) {
|
|
4105
|
+
return null;
|
|
4106
|
+
}
|
|
4107
|
+
const trashFilePath = resolveSessionTrashFilePath(normalizedEntry);
|
|
4108
|
+
if (!trashFilePath || !fs.existsSync(trashFilePath)) {
|
|
4109
|
+
return normalizedEntry;
|
|
4110
|
+
}
|
|
4111
|
+
const trashFileStat = getFileStatSafe(trashFilePath);
|
|
4112
|
+
const trashFileMtimeMs = getFileMtimeMs(trashFilePath, trashFileStat);
|
|
4113
|
+
if (
|
|
4114
|
+
Number.isFinite(Number(normalizedEntry.messageCount))
|
|
4115
|
+
&& normalizedEntry.messageCount >= 0
|
|
4116
|
+
&& trashFileMtimeMs > 0
|
|
4117
|
+
&& normalizedEntry.messageCountMtimeMs === trashFileMtimeMs
|
|
4118
|
+
) {
|
|
4119
|
+
return normalizedEntry;
|
|
4120
|
+
}
|
|
4121
|
+
|
|
4122
|
+
const exactMessageCount = await countConversationMessagesInFile(trashFilePath, normalizedEntry.source);
|
|
4123
|
+
if (!Number.isFinite(Number(exactMessageCount))) {
|
|
4124
|
+
return normalizedEntry;
|
|
4125
|
+
}
|
|
4126
|
+
|
|
4127
|
+
const safeMessageCount = Math.max(0, Math.floor(Number(exactMessageCount)));
|
|
4128
|
+
if (
|
|
4129
|
+
normalizedEntry.messageCount === safeMessageCount
|
|
4130
|
+
&& normalizedEntry.messageCountMtimeMs === trashFileMtimeMs
|
|
4131
|
+
) {
|
|
4132
|
+
return normalizedEntry;
|
|
4133
|
+
}
|
|
4134
|
+
|
|
4135
|
+
return {
|
|
4136
|
+
...normalizedEntry,
|
|
4137
|
+
messageCount: safeMessageCount,
|
|
4138
|
+
messageCountMtimeMs: trashFileMtimeMs
|
|
4139
|
+
};
|
|
4140
|
+
}
|
|
4141
|
+
|
|
4142
|
+
async function hydrateSessionTrashEntries(entries, options = {}) {
|
|
4143
|
+
const source = options.source === 'claude' ? 'claude' : (options.source === 'codex' ? 'codex' : 'all');
|
|
4144
|
+
const hydratedEntries = await mapWithConcurrency(Array.isArray(entries) ? entries : [], 8, async (entry) => {
|
|
4145
|
+
const normalizedEntry = normalizeSessionTrashEntry(entry);
|
|
4146
|
+
if (!normalizedEntry) {
|
|
4147
|
+
return undefined;
|
|
4148
|
+
}
|
|
4149
|
+
return await resolveSessionTrashEntryExactMessageCount(normalizedEntry);
|
|
4150
|
+
});
|
|
4151
|
+
|
|
4152
|
+
if (source === 'codex' || source === 'claude') {
|
|
4153
|
+
return hydratedEntries.filter((entry) => entry.source === source);
|
|
4154
|
+
}
|
|
4155
|
+
return hydratedEntries;
|
|
4156
|
+
}
|
|
4157
|
+
|
|
4158
|
+
async function hydrateSessionItemsExactMessageCount(items) {
|
|
4159
|
+
return await mapWithConcurrency(Array.isArray(items) ? items : [], 8, async (item) => {
|
|
4160
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) {
|
|
4161
|
+
return undefined;
|
|
4162
|
+
}
|
|
4163
|
+
if (item.__messageCountExact === true) {
|
|
4164
|
+
return item;
|
|
4165
|
+
}
|
|
4166
|
+
const source = item.source === 'claude' ? 'claude' : (item.source === 'codex' ? 'codex' : '');
|
|
4167
|
+
const filePath = typeof item.filePath === 'string' ? item.filePath : '';
|
|
4168
|
+
if (!source || !filePath || !fs.existsSync(filePath)) {
|
|
4169
|
+
return item;
|
|
4170
|
+
}
|
|
4171
|
+
|
|
4172
|
+
const exactMessageCount = await countConversationMessagesInFile(filePath, source);
|
|
4173
|
+
if (!Number.isFinite(Number(exactMessageCount))) {
|
|
4174
|
+
return item;
|
|
4175
|
+
}
|
|
4176
|
+
|
|
4177
|
+
const safeMessageCount = Math.max(0, Math.floor(Number(exactMessageCount)));
|
|
4178
|
+
if (Number(item.messageCount) === safeMessageCount) {
|
|
4179
|
+
return {
|
|
4180
|
+
...item,
|
|
4181
|
+
__messageCountExact: true
|
|
4182
|
+
};
|
|
4183
|
+
}
|
|
4184
|
+
|
|
4185
|
+
return {
|
|
4186
|
+
...item,
|
|
4187
|
+
messageCount: safeMessageCount,
|
|
4188
|
+
__messageCountExact: true
|
|
4189
|
+
};
|
|
4190
|
+
});
|
|
4191
|
+
}
|
|
4192
|
+
|
|
3726
4193
|
function sortSessionsByUpdatedAt(items) {
|
|
3727
4194
|
items.sort((a, b) => {
|
|
3728
4195
|
const aTime = Date.parse(a.updatedAt || '') || 0;
|
|
@@ -4240,6 +4707,7 @@ function parseCodexSessionSummary(filePath) {
|
|
|
4240
4707
|
createdAt,
|
|
4241
4708
|
updatedAt,
|
|
4242
4709
|
messageCount,
|
|
4710
|
+
__messageCountExact: isSessionSummaryMessageCountExact(stat),
|
|
4243
4711
|
filePath,
|
|
4244
4712
|
keywords: [],
|
|
4245
4713
|
capabilities: {}
|
|
@@ -4329,6 +4797,7 @@ function parseClaudeSessionSummary(filePath) {
|
|
|
4329
4797
|
createdAt,
|
|
4330
4798
|
updatedAt,
|
|
4331
4799
|
messageCount,
|
|
4800
|
+
__messageCountExact: isSessionSummaryMessageCountExact(stat),
|
|
4332
4801
|
filePath,
|
|
4333
4802
|
keywords: [],
|
|
4334
4803
|
capabilities: { code: true }
|
|
@@ -4425,7 +4894,8 @@ function listClaudeSessions(limit, options = {}) {
|
|
|
4425
4894
|
}
|
|
4426
4895
|
filePath = filePath ? path.resolve(filePath) : '';
|
|
4427
4896
|
|
|
4428
|
-
|
|
4897
|
+
const fileStat = getFileStatSafe(filePath);
|
|
4898
|
+
if (!fileStat) {
|
|
4429
4899
|
continue;
|
|
4430
4900
|
}
|
|
4431
4901
|
|
|
@@ -4434,7 +4904,7 @@ function listClaudeSessions(limit, options = {}) {
|
|
|
4434
4904
|
let title = truncateText(entry.summary || entry.firstPrompt || sessionId, 120);
|
|
4435
4905
|
let messageCount = Number.isFinite(entry.messageCount) ? Math.max(0, entry.messageCount - 1) : 0;
|
|
4436
4906
|
|
|
4437
|
-
const quickRecords = parseJsonlHeadRecords(filePath,
|
|
4907
|
+
const quickRecords = parseJsonlHeadRecords(filePath, SESSION_SUMMARY_READ_BYTES);
|
|
4438
4908
|
if (quickRecords.length > 0) {
|
|
4439
4909
|
const filteredCount = countConversationMessagesInRecords(quickRecords, 'claude');
|
|
4440
4910
|
if (filteredCount > 0 || messageCount === 0) {
|
|
@@ -4472,6 +4942,7 @@ function listClaudeSessions(limit, options = {}) {
|
|
|
4472
4942
|
createdAt,
|
|
4473
4943
|
updatedAt,
|
|
4474
4944
|
messageCount,
|
|
4945
|
+
__messageCountExact: quickRecords.length > 0 && isSessionSummaryMessageCountExact(fileStat),
|
|
4475
4946
|
filePath,
|
|
4476
4947
|
keywords,
|
|
4477
4948
|
capabilities
|
|
@@ -4566,8 +5037,44 @@ function listAllSessions(params = {}) {
|
|
|
4566
5037
|
return result;
|
|
4567
5038
|
}
|
|
4568
5039
|
|
|
4569
|
-
function
|
|
4570
|
-
const source =
|
|
5040
|
+
async function listAllSessionsData(params = {}) {
|
|
5041
|
+
const source = params.source === 'codex' || params.source === 'claude'
|
|
5042
|
+
? params.source
|
|
5043
|
+
: 'all';
|
|
5044
|
+
const rawLimit = Number(params.limit);
|
|
5045
|
+
const limit = Number.isFinite(rawLimit)
|
|
5046
|
+
? Math.max(1, Math.min(rawLimit, MAX_SESSION_LIST_SIZE))
|
|
5047
|
+
: 120;
|
|
5048
|
+
const forceRefresh = !!params.forceRefresh;
|
|
5049
|
+
const normalizedPathFilter = normalizeSessionPathFilter(params.pathFilter);
|
|
5050
|
+
const queryTokens = expandSessionQueryTokens(normalizeQueryTokens(params.query));
|
|
5051
|
+
const hasQuery = queryTokens.length > 0;
|
|
5052
|
+
const cacheKey = hasQuery ? '' : `exact:${source}:${limit}:${normalizedPathFilter}`;
|
|
5053
|
+
if (!hasQuery) {
|
|
5054
|
+
const cached = getSessionListCache(cacheKey, forceRefresh);
|
|
5055
|
+
if (cached) {
|
|
5056
|
+
return cached;
|
|
5057
|
+
}
|
|
5058
|
+
}
|
|
5059
|
+
|
|
5060
|
+
const sessions = listAllSessions(params);
|
|
5061
|
+
const hydratedSessions = await hydrateSessionItemsExactMessageCount(sessions);
|
|
5062
|
+
const result = hydratedSessions.map((item) => {
|
|
5063
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) {
|
|
5064
|
+
return item;
|
|
5065
|
+
}
|
|
5066
|
+
const normalized = { ...item };
|
|
5067
|
+
delete normalized.__messageCountExact;
|
|
5068
|
+
return normalized;
|
|
5069
|
+
});
|
|
5070
|
+
if (!hasQuery) {
|
|
5071
|
+
setSessionListCache(cacheKey, result);
|
|
5072
|
+
}
|
|
5073
|
+
return result;
|
|
5074
|
+
}
|
|
5075
|
+
|
|
5076
|
+
function listSessionPaths(params = {}) {
|
|
5077
|
+
const source = typeof params.source === 'string' ? params.source.trim().toLowerCase() : '';
|
|
4571
5078
|
if (source && source !== 'codex' && source !== 'claude' && source !== 'all') {
|
|
4572
5079
|
return [];
|
|
4573
5080
|
}
|
|
@@ -4647,6 +5154,19 @@ function resolveSessionFilePath(source, filePath, sessionId) {
|
|
|
4647
5154
|
return '';
|
|
4648
5155
|
}
|
|
4649
5156
|
|
|
5157
|
+
function getSessionFileArg(params = {}) {
|
|
5158
|
+
if (!params || typeof params !== 'object') {
|
|
5159
|
+
return '';
|
|
5160
|
+
}
|
|
5161
|
+
if (typeof params.filePath === 'string' && params.filePath.trim()) {
|
|
5162
|
+
return params.filePath.trim();
|
|
5163
|
+
}
|
|
5164
|
+
if (typeof params.file === 'string' && params.file.trim()) {
|
|
5165
|
+
return params.file.trim();
|
|
5166
|
+
}
|
|
5167
|
+
return '';
|
|
5168
|
+
}
|
|
5169
|
+
|
|
4650
5170
|
function findClaudeSessionIndexPath(sessionFilePath) {
|
|
4651
5171
|
const root = getClaudeProjectsDir();
|
|
4652
5172
|
if (!root || !sessionFilePath) {
|
|
@@ -5083,146 +5603,801 @@ function applyBuiltinProxyProvider(params = {}) {
|
|
|
5083
5603
|
allowManaged: true
|
|
5084
5604
|
});
|
|
5085
5605
|
|
|
5086
|
-
if (saveResult && saveResult.error) {
|
|
5087
|
-
return saveResult;
|
|
5606
|
+
if (saveResult && saveResult.error) {
|
|
5607
|
+
return saveResult;
|
|
5608
|
+
}
|
|
5609
|
+
|
|
5610
|
+
const switchToProxy = params.switchToProxy !== false;
|
|
5611
|
+
let targetModel = '';
|
|
5612
|
+
if (switchToProxy) {
|
|
5613
|
+
try {
|
|
5614
|
+
targetModel = cmdSwitch(BUILTIN_PROXY_PROVIDER_NAME, true) || '';
|
|
5615
|
+
} catch (e) {
|
|
5616
|
+
return { error: `写入代理 provider 成功,但切换失败: ${e.message}` };
|
|
5617
|
+
}
|
|
5618
|
+
}
|
|
5619
|
+
|
|
5620
|
+
return {
|
|
5621
|
+
success: true,
|
|
5622
|
+
provider: BUILTIN_PROXY_PROVIDER_NAME,
|
|
5623
|
+
baseUrl,
|
|
5624
|
+
switched: switchToProxy,
|
|
5625
|
+
model: targetModel
|
|
5626
|
+
};
|
|
5627
|
+
}
|
|
5628
|
+
|
|
5629
|
+
async function ensureBuiltinProxyForCodexDefault(params = {}) {
|
|
5630
|
+
const payload = isPlainObject(params) ? { ...params } : {};
|
|
5631
|
+
const switchToProxy = payload.switchToProxy !== false;
|
|
5632
|
+
delete payload.switchToProxy;
|
|
5633
|
+
payload.enabled = true;
|
|
5634
|
+
|
|
5635
|
+
const saveResult = saveBuiltinProxySettings(payload);
|
|
5636
|
+
if (saveResult.error) {
|
|
5637
|
+
return { error: saveResult.error };
|
|
5638
|
+
}
|
|
5639
|
+
let nextSettings = saveResult.settings;
|
|
5640
|
+
|
|
5641
|
+
let upstreamResult = resolveBuiltinProxyUpstream(nextSettings);
|
|
5642
|
+
if (upstreamResult.error) {
|
|
5643
|
+
return { error: upstreamResult.error };
|
|
5644
|
+
}
|
|
5645
|
+
|
|
5646
|
+
const runtime = g_builtinProxyRuntime;
|
|
5647
|
+
const shouldRestart = !!runtime && (
|
|
5648
|
+
runtime.settings.host !== nextSettings.host
|
|
5649
|
+
|| runtime.settings.port !== nextSettings.port
|
|
5650
|
+
|| runtime.settings.authSource !== nextSettings.authSource
|
|
5651
|
+
|| runtime.settings.timeoutMs !== nextSettings.timeoutMs
|
|
5652
|
+
|| runtime.upstream.providerName !== upstreamResult.providerName
|
|
5653
|
+
|| runtime.upstream.baseUrl !== upstreamResult.baseUrl
|
|
5654
|
+
|| runtime.upstream.authHeader !== upstreamResult.authHeader
|
|
5655
|
+
);
|
|
5656
|
+
|
|
5657
|
+
if (shouldRestart) {
|
|
5658
|
+
await stopBuiltinProxyRuntime();
|
|
5659
|
+
}
|
|
5660
|
+
|
|
5661
|
+
if (!g_builtinProxyRuntime) {
|
|
5662
|
+
let startRes = await startBuiltinProxyRuntime(nextSettings);
|
|
5663
|
+
if (!startRes.success && /EADDRINUSE/i.test(String(startRes.error || ''))) {
|
|
5664
|
+
const fallbackPort = await findAvailablePort(nextSettings.host, nextSettings.port + 1, 30);
|
|
5665
|
+
if (fallbackPort > 0) {
|
|
5666
|
+
const retrySave = saveBuiltinProxySettings({
|
|
5667
|
+
...nextSettings,
|
|
5668
|
+
port: fallbackPort,
|
|
5669
|
+
enabled: true
|
|
5670
|
+
});
|
|
5671
|
+
if (retrySave.success) {
|
|
5672
|
+
nextSettings = retrySave.settings;
|
|
5673
|
+
upstreamResult = resolveBuiltinProxyUpstream(nextSettings);
|
|
5674
|
+
if (upstreamResult.error) {
|
|
5675
|
+
return { error: upstreamResult.error };
|
|
5676
|
+
}
|
|
5677
|
+
startRes = await startBuiltinProxyRuntime(nextSettings);
|
|
5678
|
+
}
|
|
5679
|
+
}
|
|
5680
|
+
}
|
|
5681
|
+
if (!startRes.success) {
|
|
5682
|
+
return { error: startRes.error || '启动内建代理失败' };
|
|
5683
|
+
}
|
|
5684
|
+
}
|
|
5685
|
+
|
|
5686
|
+
let applyRes = {
|
|
5687
|
+
success: true,
|
|
5688
|
+
provider: BUILTIN_PROXY_PROVIDER_NAME,
|
|
5689
|
+
baseUrl: buildProxyListenUrl(nextSettings),
|
|
5690
|
+
switched: false,
|
|
5691
|
+
model: ''
|
|
5692
|
+
};
|
|
5693
|
+
if (switchToProxy) {
|
|
5694
|
+
applyRes = applyBuiltinProxyProvider({ switchToProxy: true });
|
|
5695
|
+
if (applyRes.error) {
|
|
5696
|
+
return applyRes;
|
|
5697
|
+
}
|
|
5698
|
+
}
|
|
5699
|
+
|
|
5700
|
+
const status = getBuiltinProxyStatus();
|
|
5701
|
+
return {
|
|
5702
|
+
success: true,
|
|
5703
|
+
provider: applyRes.provider,
|
|
5704
|
+
baseUrl: applyRes.baseUrl,
|
|
5705
|
+
switched: applyRes.switched,
|
|
5706
|
+
model: applyRes.model || '',
|
|
5707
|
+
settings: status.settings,
|
|
5708
|
+
runtime: status.runtime
|
|
5709
|
+
};
|
|
5710
|
+
}
|
|
5711
|
+
|
|
5712
|
+
function removeClaudeSessionIndexEntry(indexPath, sessionFilePath, sessionId) {
|
|
5713
|
+
if (!indexPath || !fs.existsSync(indexPath)) {
|
|
5714
|
+
return { removed: false, entry: null };
|
|
5715
|
+
}
|
|
5716
|
+
const index = readJsonFile(indexPath, null);
|
|
5717
|
+
if (!index || !Array.isArray(index.entries)) {
|
|
5718
|
+
return { removed: false, entry: null };
|
|
5719
|
+
}
|
|
5720
|
+
const ignoreCase = process.platform === 'win32';
|
|
5721
|
+
const resolvedFile = sessionFilePath
|
|
5722
|
+
? normalizePathForCompare(sessionFilePath, { ignoreCase })
|
|
5723
|
+
: '';
|
|
5724
|
+
let removedEntry = null;
|
|
5725
|
+
const filtered = index.entries.filter((entry) => {
|
|
5726
|
+
if (!entry || typeof entry !== 'object') {
|
|
5727
|
+
return false;
|
|
5728
|
+
}
|
|
5729
|
+
if (entry.fullPath) {
|
|
5730
|
+
const expanded = expandHomePath(entry.fullPath);
|
|
5731
|
+
const entryPath = expanded
|
|
5732
|
+
? normalizePathForCompare(expanded, { ignoreCase })
|
|
5733
|
+
: '';
|
|
5734
|
+
if (entryPath && resolvedFile && entryPath === resolvedFile) {
|
|
5735
|
+
if (!removedEntry) {
|
|
5736
|
+
removedEntry = entry;
|
|
5737
|
+
}
|
|
5738
|
+
return false;
|
|
5739
|
+
}
|
|
5740
|
+
}
|
|
5741
|
+
const entrySessionId = typeof entry.sessionId === 'string' ? entry.sessionId : '';
|
|
5742
|
+
if (!resolvedFile && sessionId && entrySessionId === sessionId) {
|
|
5743
|
+
if (!removedEntry) {
|
|
5744
|
+
removedEntry = entry;
|
|
5745
|
+
}
|
|
5746
|
+
return false;
|
|
5747
|
+
}
|
|
5748
|
+
return true;
|
|
5749
|
+
});
|
|
5750
|
+
if (filtered.length === index.entries.length) {
|
|
5751
|
+
return { removed: false, entry: null };
|
|
5752
|
+
}
|
|
5753
|
+
index.entries = filtered;
|
|
5754
|
+
writeJsonAtomic(indexPath, index);
|
|
5755
|
+
return {
|
|
5756
|
+
removed: true,
|
|
5757
|
+
entry: removedEntry && typeof removedEntry === 'object'
|
|
5758
|
+
? JSON.parse(JSON.stringify(removedEntry))
|
|
5759
|
+
: null
|
|
5760
|
+
};
|
|
5761
|
+
}
|
|
5762
|
+
|
|
5763
|
+
function moveFileSync(sourcePath, targetPath) {
|
|
5764
|
+
ensureDir(path.dirname(targetPath));
|
|
5765
|
+
try {
|
|
5766
|
+
fs.renameSync(sourcePath, targetPath);
|
|
5767
|
+
return;
|
|
5768
|
+
} catch (error) {
|
|
5769
|
+
if (!error || error.code !== 'EXDEV') {
|
|
5770
|
+
throw error;
|
|
5771
|
+
}
|
|
5772
|
+
}
|
|
5773
|
+
|
|
5774
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
5775
|
+
try {
|
|
5776
|
+
fs.unlinkSync(sourcePath);
|
|
5777
|
+
} catch (error) {
|
|
5778
|
+
try {
|
|
5779
|
+
fs.unlinkSync(targetPath);
|
|
5780
|
+
} catch (_) {}
|
|
5781
|
+
throw error;
|
|
5782
|
+
}
|
|
5783
|
+
}
|
|
5784
|
+
|
|
5785
|
+
function buildSessionSummaryFallback(source, filePath, sessionId = '') {
|
|
5786
|
+
const resolvedSessionId = sessionId || path.basename(filePath, '.jsonl');
|
|
5787
|
+
const sourceLabel = source === 'claude' ? 'Claude Code' : 'Codex';
|
|
5788
|
+
return {
|
|
5789
|
+
source,
|
|
5790
|
+
sourceLabel,
|
|
5791
|
+
provider: source === 'claude' ? 'claude' : 'codex',
|
|
5792
|
+
sessionId: resolvedSessionId,
|
|
5793
|
+
title: resolvedSessionId,
|
|
5794
|
+
cwd: '',
|
|
5795
|
+
createdAt: '',
|
|
5796
|
+
updatedAt: '',
|
|
5797
|
+
messageCount: 0,
|
|
5798
|
+
filePath,
|
|
5799
|
+
keywords: [],
|
|
5800
|
+
capabilities: source === 'claude' ? { code: true } : {}
|
|
5801
|
+
};
|
|
5802
|
+
}
|
|
5803
|
+
|
|
5804
|
+
function generateSessionTrashId() {
|
|
5805
|
+
if (crypto.randomUUID) {
|
|
5806
|
+
return `trash-${crypto.randomUUID()}`;
|
|
5807
|
+
}
|
|
5808
|
+
return `trash-${Date.now().toString(36)}-${crypto.randomBytes(8).toString('hex')}`;
|
|
5809
|
+
}
|
|
5810
|
+
|
|
5811
|
+
function allocateSessionTrashTarget() {
|
|
5812
|
+
ensureDir(SESSION_TRASH_FILES_DIR);
|
|
5813
|
+
for (let attempt = 0; attempt < 6; attempt += 1) {
|
|
5814
|
+
const trashId = generateSessionTrashId();
|
|
5815
|
+
const trashFileName = `${trashId}.jsonl`;
|
|
5816
|
+
const trashFilePath = path.join(SESSION_TRASH_FILES_DIR, trashFileName);
|
|
5817
|
+
if (!fs.existsSync(trashFilePath)) {
|
|
5818
|
+
return { trashId, trashFileName, trashFilePath };
|
|
5819
|
+
}
|
|
5820
|
+
}
|
|
5821
|
+
const fallbackId = `trash-${Date.now().toString(36)}-${crypto.randomBytes(8).toString('hex')}`;
|
|
5822
|
+
return {
|
|
5823
|
+
trashId: fallbackId,
|
|
5824
|
+
trashFileName: `${fallbackId}.jsonl`,
|
|
5825
|
+
trashFilePath: path.join(SESSION_TRASH_FILES_DIR, `${fallbackId}.jsonl`)
|
|
5826
|
+
};
|
|
5827
|
+
}
|
|
5828
|
+
|
|
5829
|
+
function normalizeSessionTrashEntry(entry) {
|
|
5830
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
5831
|
+
return null;
|
|
5832
|
+
}
|
|
5833
|
+
const source = entry.source === 'claude' ? 'claude' : (entry.source === 'codex' ? 'codex' : '');
|
|
5834
|
+
const trashId = typeof entry.trashId === 'string' ? entry.trashId.trim() : '';
|
|
5835
|
+
if (!source || !trashId || trashId.includes('/') || trashId.includes('\\') || trashId.includes('\0')) {
|
|
5836
|
+
return null;
|
|
5837
|
+
}
|
|
5838
|
+
const sessionId = typeof entry.sessionId === 'string' ? entry.sessionId.trim() : '';
|
|
5839
|
+
const trashFileNameRaw = typeof entry.trashFileName === 'string' ? entry.trashFileName.trim() : '';
|
|
5840
|
+
const trashFileName = path.basename(trashFileNameRaw || `${trashId}.jsonl`);
|
|
5841
|
+
if (!trashFileName || trashFileName === '.' || trashFileName === '..' || trashFileName.includes('\0')) {
|
|
5842
|
+
return null;
|
|
5843
|
+
}
|
|
5844
|
+
return {
|
|
5845
|
+
trashId,
|
|
5846
|
+
trashFileName,
|
|
5847
|
+
source,
|
|
5848
|
+
sourceLabel: source === 'claude' ? 'Claude Code' : 'Codex',
|
|
5849
|
+
sessionId: sessionId || trashId,
|
|
5850
|
+
title: typeof entry.title === 'string' && entry.title.trim() ? entry.title.trim() : (sessionId || trashId),
|
|
5851
|
+
cwd: typeof entry.cwd === 'string' ? entry.cwd : '',
|
|
5852
|
+
createdAt: typeof entry.createdAt === 'string' ? entry.createdAt : '',
|
|
5853
|
+
updatedAt: typeof entry.updatedAt === 'string' ? entry.updatedAt : '',
|
|
5854
|
+
deletedAt: typeof entry.deletedAt === 'string' ? entry.deletedAt : '',
|
|
5855
|
+
messageCount: Number.isFinite(Number(entry.messageCount))
|
|
5856
|
+
? Math.max(0, Math.floor(Number(entry.messageCount)))
|
|
5857
|
+
: 0,
|
|
5858
|
+
messageCountMtimeMs: Number.isFinite(Number(entry.messageCountMtimeMs))
|
|
5859
|
+
? Math.max(0, Math.floor(Number(entry.messageCountMtimeMs)))
|
|
5860
|
+
: 0,
|
|
5861
|
+
originalFilePath: typeof entry.originalFilePath === 'string' ? entry.originalFilePath : '',
|
|
5862
|
+
provider: typeof entry.provider === 'string' && entry.provider.trim()
|
|
5863
|
+
? entry.provider.trim()
|
|
5864
|
+
: (source === 'claude' ? 'claude' : 'codex'),
|
|
5865
|
+
keywords: normalizeKeywords(entry.keywords),
|
|
5866
|
+
capabilities: normalizeCapabilities(entry.capabilities),
|
|
5867
|
+
claudeIndexPath: typeof entry.claudeIndexPath === 'string' ? entry.claudeIndexPath : '',
|
|
5868
|
+
claudeIndexEntry: entry.claudeIndexEntry && typeof entry.claudeIndexEntry === 'object' && !Array.isArray(entry.claudeIndexEntry)
|
|
5869
|
+
? entry.claudeIndexEntry
|
|
5870
|
+
: null
|
|
5871
|
+
};
|
|
5872
|
+
}
|
|
5873
|
+
|
|
5874
|
+
function resolveSessionTrashFilePath(entry) {
|
|
5875
|
+
const normalized = normalizeSessionTrashEntry(entry);
|
|
5876
|
+
if (!normalized) {
|
|
5877
|
+
return '';
|
|
5878
|
+
}
|
|
5879
|
+
const filePath = path.join(SESSION_TRASH_FILES_DIR, normalized.trashFileName);
|
|
5880
|
+
return isPathInside(filePath, SESSION_TRASH_FILES_DIR) ? filePath : '';
|
|
5881
|
+
}
|
|
5882
|
+
|
|
5883
|
+
function writeSessionTrashEntries(entries) {
|
|
5884
|
+
writeJsonAtomic(SESSION_TRASH_INDEX_FILE, {
|
|
5885
|
+
version: 1,
|
|
5886
|
+
updatedAt: new Date().toISOString(),
|
|
5887
|
+
entries
|
|
5888
|
+
});
|
|
5889
|
+
}
|
|
5890
|
+
|
|
5891
|
+
function readSessionTrashEntries(options = {}) {
|
|
5892
|
+
const cleanup = options.cleanup !== false;
|
|
5893
|
+
const parsed = readJsonFile(SESSION_TRASH_INDEX_FILE, null);
|
|
5894
|
+
if (!parsed || !Array.isArray(parsed.entries)) {
|
|
5895
|
+
return [];
|
|
5896
|
+
}
|
|
5897
|
+
|
|
5898
|
+
const normalizedEntries = [];
|
|
5899
|
+
let dirty = false;
|
|
5900
|
+
for (const rawEntry of parsed.entries) {
|
|
5901
|
+
const entry = normalizeSessionTrashEntry(rawEntry);
|
|
5902
|
+
if (!entry) {
|
|
5903
|
+
dirty = true;
|
|
5904
|
+
continue;
|
|
5905
|
+
}
|
|
5906
|
+
const trashFilePath = resolveSessionTrashFilePath(entry);
|
|
5907
|
+
if (!trashFilePath || !fs.existsSync(trashFilePath)) {
|
|
5908
|
+
dirty = true;
|
|
5909
|
+
continue;
|
|
5910
|
+
}
|
|
5911
|
+
normalizedEntries.push(entry);
|
|
5912
|
+
}
|
|
5913
|
+
|
|
5914
|
+
if (dirty && cleanup) {
|
|
5915
|
+
writeSessionTrashEntries(normalizedEntries);
|
|
5916
|
+
}
|
|
5917
|
+
|
|
5918
|
+
return normalizedEntries;
|
|
5919
|
+
}
|
|
5920
|
+
|
|
5921
|
+
function buildSessionTrashEntry(summary, options = {}) {
|
|
5922
|
+
const source = options.source === 'claude' ? 'claude' : 'codex';
|
|
5923
|
+
const sessionId = options.sessionId || summary.sessionId || path.basename(options.originalFilePath || summary.filePath || '', '.jsonl');
|
|
5924
|
+
const claudeIndexEntry = options.claudeIndexEntry && typeof options.claudeIndexEntry === 'object' && !Array.isArray(options.claudeIndexEntry)
|
|
5925
|
+
? options.claudeIndexEntry
|
|
5926
|
+
: null;
|
|
5927
|
+
const deletedAt = typeof options.deletedAt === 'string' && options.deletedAt
|
|
5928
|
+
? options.deletedAt
|
|
5929
|
+
: new Date().toISOString();
|
|
5930
|
+
const sourceLabel = source === 'claude' ? 'Claude Code' : 'Codex';
|
|
5931
|
+
const fallbackTitle = truncateText(
|
|
5932
|
+
(claudeIndexEntry && (claudeIndexEntry.summary || claudeIndexEntry.firstPrompt)) || sessionId,
|
|
5933
|
+
120
|
|
5934
|
+
);
|
|
5935
|
+
const rawFallbackMessageCount = claudeIndexEntry && claudeIndexEntry.messageCount;
|
|
5936
|
+
const fallbackMessageCount = Number.isFinite(Number(rawFallbackMessageCount))
|
|
5937
|
+
? Math.max(0, Number(rawFallbackMessageCount))
|
|
5938
|
+
: 0;
|
|
5939
|
+
const resolvedMessageCount = Number.isFinite(Number(summary && summary.messageCount))
|
|
5940
|
+
? Math.max(0, Math.floor(Number(summary.messageCount)))
|
|
5941
|
+
: fallbackMessageCount;
|
|
5942
|
+
const messageCountMtimeMs = getFileMtimeMs(options.trashFilePath);
|
|
5943
|
+
const normalizedClaudeKeywords = claudeIndexEntry && Array.isArray(claudeIndexEntry.keywords)
|
|
5944
|
+
? normalizeKeywords(claudeIndexEntry.keywords)
|
|
5945
|
+
: [];
|
|
5946
|
+
const normalizedClaudeCapabilities = claudeIndexEntry
|
|
5947
|
+
? normalizeCapabilities(claudeIndexEntry.capabilities)
|
|
5948
|
+
: {};
|
|
5949
|
+
const normalizedSummaryKeywords = normalizeKeywords(summary.keywords);
|
|
5950
|
+
const normalizedSummaryCapabilities = normalizeCapabilities(summary.capabilities);
|
|
5951
|
+
return {
|
|
5952
|
+
trashId: options.trashId,
|
|
5953
|
+
trashFileName: options.trashFileName,
|
|
5954
|
+
source,
|
|
5955
|
+
sourceLabel,
|
|
5956
|
+
sessionId,
|
|
5957
|
+
title: summary.title || fallbackTitle || sessionId,
|
|
5958
|
+
cwd: summary.cwd || (claudeIndexEntry && typeof claudeIndexEntry.projectPath === 'string' ? claudeIndexEntry.projectPath : ''),
|
|
5959
|
+
createdAt: summary.createdAt || toIsoTime(claudeIndexEntry && claudeIndexEntry.created, ''),
|
|
5960
|
+
updatedAt: summary.updatedAt || toIsoTime(claudeIndexEntry && (claudeIndexEntry.modified || claudeIndexEntry.fileMtime), ''),
|
|
5961
|
+
deletedAt,
|
|
5962
|
+
messageCount: resolvedMessageCount,
|
|
5963
|
+
messageCountMtimeMs,
|
|
5964
|
+
originalFilePath: options.originalFilePath || summary.filePath || '',
|
|
5965
|
+
provider: (claudeIndexEntry && typeof claudeIndexEntry.provider === 'string' && claudeIndexEntry.provider.trim())
|
|
5966
|
+
? claudeIndexEntry.provider.trim()
|
|
5967
|
+
: (summary.provider || (source === 'claude' ? 'claude' : 'codex')),
|
|
5968
|
+
keywords: normalizedClaudeKeywords.length > 0 ? normalizedClaudeKeywords : normalizedSummaryKeywords,
|
|
5969
|
+
capabilities: Object.keys(normalizedClaudeCapabilities).length > 0
|
|
5970
|
+
? normalizedClaudeCapabilities
|
|
5971
|
+
: normalizedSummaryCapabilities,
|
|
5972
|
+
claudeIndexPath: typeof options.claudeIndexPath === 'string' ? options.claudeIndexPath : '',
|
|
5973
|
+
claudeIndexEntry
|
|
5974
|
+
};
|
|
5975
|
+
}
|
|
5976
|
+
|
|
5977
|
+
function resolveSessionRestoreTarget(entry) {
|
|
5978
|
+
const normalized = normalizeSessionTrashEntry(entry);
|
|
5979
|
+
if (!normalized) {
|
|
5980
|
+
return '';
|
|
5981
|
+
}
|
|
5982
|
+
const root = normalized.source === 'claude' ? getClaudeProjectsDir() : getCodexSessionsDir();
|
|
5983
|
+
const originalFilePath = typeof normalized.originalFilePath === 'string' ? normalized.originalFilePath.trim() : '';
|
|
5984
|
+
if (!root || !originalFilePath) {
|
|
5985
|
+
return '';
|
|
5986
|
+
}
|
|
5987
|
+
const expanded = expandHomePath(originalFilePath);
|
|
5988
|
+
const resolved = expanded ? path.resolve(expanded) : '';
|
|
5989
|
+
if (!resolved || !isPathInside(resolved, root)) {
|
|
5990
|
+
return '';
|
|
5991
|
+
}
|
|
5992
|
+
return resolved;
|
|
5993
|
+
}
|
|
5994
|
+
|
|
5995
|
+
function resolveClaudeSessionRestoreIndexPath(entry, targetFilePath) {
|
|
5996
|
+
const fallbackIndexPath = findClaudeSessionIndexPath(targetFilePath) || path.join(path.dirname(targetFilePath), 'sessions-index.json');
|
|
5997
|
+
const fallbackResolved = fallbackIndexPath ? path.resolve(fallbackIndexPath) : '';
|
|
5998
|
+
const candidateRaw = entry && typeof entry.claudeIndexPath === 'string' ? entry.claudeIndexPath.trim() : '';
|
|
5999
|
+
if (!candidateRaw) {
|
|
6000
|
+
return fallbackResolved;
|
|
6001
|
+
}
|
|
6002
|
+
const claudeProjectsDir = getClaudeProjectsDir();
|
|
6003
|
+
if (!claudeProjectsDir) {
|
|
6004
|
+
return fallbackResolved;
|
|
6005
|
+
}
|
|
6006
|
+
const candidateIndexPath = path.resolve(candidateRaw);
|
|
6007
|
+
if (path.basename(candidateIndexPath).toLowerCase() !== 'sessions-index.json') {
|
|
6008
|
+
return fallbackResolved;
|
|
6009
|
+
}
|
|
6010
|
+
if (!isPathInside(candidateIndexPath, claudeProjectsDir)) {
|
|
6011
|
+
return fallbackResolved;
|
|
6012
|
+
}
|
|
6013
|
+
if (!isPathInside(targetFilePath, path.dirname(candidateIndexPath))) {
|
|
6014
|
+
return fallbackResolved;
|
|
6015
|
+
}
|
|
6016
|
+
return candidateIndexPath;
|
|
6017
|
+
}
|
|
6018
|
+
|
|
6019
|
+
function buildClaudeSessionIndexEntry(entry, sessionFilePath) {
|
|
6020
|
+
const normalized = normalizeSessionTrashEntry(entry);
|
|
6021
|
+
const stored = normalized && normalized.claudeIndexEntry && typeof normalized.claudeIndexEntry === 'object'
|
|
6022
|
+
? JSON.parse(JSON.stringify(normalized.claudeIndexEntry))
|
|
6023
|
+
: {};
|
|
6024
|
+
const storedCapabilities = stored && stored.capabilities && typeof stored.capabilities === 'object' && !Array.isArray(stored.capabilities)
|
|
6025
|
+
? stored.capabilities
|
|
6026
|
+
: null;
|
|
6027
|
+
const storedKeywords = Array.isArray(stored && stored.keywords)
|
|
6028
|
+
? stored.keywords
|
|
6029
|
+
: null;
|
|
6030
|
+
const normalizedMessageCount = Number(normalized && normalized.messageCount);
|
|
6031
|
+
const storedMessageCount = Number(stored && stored.messageCount);
|
|
6032
|
+
let modifiedAt = '';
|
|
6033
|
+
try {
|
|
6034
|
+
modifiedAt = fs.statSync(sessionFilePath).mtime.toISOString();
|
|
6035
|
+
} catch (e) {
|
|
6036
|
+
modifiedAt = normalized && normalized.updatedAt ? normalized.updatedAt : new Date().toISOString();
|
|
6037
|
+
}
|
|
6038
|
+
const projectDir = path.dirname(sessionFilePath);
|
|
6039
|
+
return {
|
|
6040
|
+
...stored,
|
|
6041
|
+
sessionId: normalized.sessionId,
|
|
6042
|
+
fullPath: sessionFilePath,
|
|
6043
|
+
projectPath: (stored && typeof stored.projectPath === 'string' && stored.projectPath.trim())
|
|
6044
|
+
? stored.projectPath.trim()
|
|
6045
|
+
: projectDir,
|
|
6046
|
+
created: (stored && typeof stored.created === 'string' && stored.created.trim())
|
|
6047
|
+
? stored.created.trim()
|
|
6048
|
+
: (normalized.createdAt || modifiedAt),
|
|
6049
|
+
modified: modifiedAt,
|
|
6050
|
+
summary: (stored && typeof stored.summary === 'string' && stored.summary.trim())
|
|
6051
|
+
? stored.summary.trim()
|
|
6052
|
+
: (normalized.title || normalized.sessionId),
|
|
6053
|
+
provider: (stored && typeof stored.provider === 'string' && stored.provider.trim())
|
|
6054
|
+
? stored.provider.trim()
|
|
6055
|
+
: (normalized.provider || 'claude'),
|
|
6056
|
+
capabilities: normalizeCapabilities(
|
|
6057
|
+
storedCapabilities && Object.keys(storedCapabilities).length > 0
|
|
6058
|
+
? storedCapabilities
|
|
6059
|
+
: normalized.capabilities
|
|
6060
|
+
),
|
|
6061
|
+
keywords: normalizeKeywords(
|
|
6062
|
+
storedKeywords && storedKeywords.length > 0
|
|
6063
|
+
? storedKeywords
|
|
6064
|
+
: normalized.keywords
|
|
6065
|
+
),
|
|
6066
|
+
messageCount: Number.isFinite(normalizedMessageCount)
|
|
6067
|
+
? buildClaudeStoredIndexMessageCount(normalizedMessageCount)
|
|
6068
|
+
: (
|
|
6069
|
+
Number.isFinite(storedMessageCount)
|
|
6070
|
+
? Math.max(0, Math.floor(storedMessageCount))
|
|
6071
|
+
: buildClaudeStoredIndexMessageCount(normalized && normalized.messageCount)
|
|
6072
|
+
)
|
|
6073
|
+
};
|
|
6074
|
+
}
|
|
6075
|
+
|
|
6076
|
+
function upsertClaudeSessionIndexEntry(indexPath, sessionFilePath, entry) {
|
|
6077
|
+
if (!indexPath) {
|
|
6078
|
+
return;
|
|
5088
6079
|
}
|
|
5089
|
-
|
|
5090
|
-
const
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
6080
|
+
const parsed = readJsonFile(indexPath, null);
|
|
6081
|
+
const index = parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
6082
|
+
? parsed
|
|
6083
|
+
: {};
|
|
6084
|
+
const entries = Array.isArray(index.entries) ? index.entries : [];
|
|
6085
|
+
const ignoreCase = process.platform === 'win32';
|
|
6086
|
+
const resolvedFile = normalizePathForCompare(sessionFilePath, { ignoreCase });
|
|
6087
|
+
const normalizedEntry = normalizeSessionTrashEntry(entry);
|
|
6088
|
+
const filtered = entries.filter((item) => {
|
|
6089
|
+
if (!item || typeof item !== 'object') {
|
|
6090
|
+
return false;
|
|
6091
|
+
}
|
|
6092
|
+
if (typeof item.fullPath === 'string' && item.fullPath) {
|
|
6093
|
+
const expanded = expandHomePath(item.fullPath);
|
|
6094
|
+
const itemPath = expanded
|
|
6095
|
+
? normalizePathForCompare(expanded, { ignoreCase })
|
|
6096
|
+
: '';
|
|
6097
|
+
if (itemPath && itemPath === resolvedFile) {
|
|
6098
|
+
return false;
|
|
6099
|
+
}
|
|
6100
|
+
}
|
|
6101
|
+
const itemSessionId = typeof item.sessionId === 'string' ? item.sessionId : '';
|
|
6102
|
+
if (!resolvedFile && normalizedEntry.sessionId && itemSessionId === normalizedEntry.sessionId) {
|
|
6103
|
+
return false;
|
|
5097
6104
|
}
|
|
6105
|
+
return true;
|
|
6106
|
+
});
|
|
6107
|
+
filtered.unshift(buildClaudeSessionIndexEntry(normalizedEntry, sessionFilePath));
|
|
6108
|
+
index.entries = filtered;
|
|
6109
|
+
if (!index.originalPath) {
|
|
6110
|
+
index.originalPath = path.dirname(indexPath);
|
|
5098
6111
|
}
|
|
6112
|
+
writeJsonAtomic(indexPath, index);
|
|
6113
|
+
}
|
|
5099
6114
|
|
|
6115
|
+
async function listSessionTrashItems(params = {}) {
|
|
6116
|
+
const source = params.source === 'claude' ? 'claude' : (params.source === 'codex' ? 'codex' : 'all');
|
|
6117
|
+
const countOnly = params.countOnly === true;
|
|
6118
|
+
const rawLimit = Number(params.limit);
|
|
6119
|
+
const limit = Number.isFinite(rawLimit)
|
|
6120
|
+
? Math.max(1, Math.min(rawLimit, MAX_SESSION_TRASH_LIST_SIZE))
|
|
6121
|
+
: 200;
|
|
6122
|
+
const allEntries = readSessionTrashEntries();
|
|
6123
|
+
let items = source === 'codex' || source === 'claude'
|
|
6124
|
+
? allEntries.filter((entry) => entry.source === source)
|
|
6125
|
+
: allEntries.slice();
|
|
6126
|
+
items.sort((a, b) => {
|
|
6127
|
+
const aTime = Date.parse(a.deletedAt || a.updatedAt || '') || 0;
|
|
6128
|
+
const bTime = Date.parse(b.deletedAt || b.updatedAt || '') || 0;
|
|
6129
|
+
return bTime - aTime;
|
|
6130
|
+
});
|
|
6131
|
+
const totalCount = items.length;
|
|
6132
|
+
if (countOnly) {
|
|
6133
|
+
return {
|
|
6134
|
+
totalCount,
|
|
6135
|
+
items: []
|
|
6136
|
+
};
|
|
6137
|
+
}
|
|
6138
|
+
const visibleEntries = items.slice(0, limit);
|
|
6139
|
+
const hydratedVisibleEntries = await hydrateSessionTrashEntries(visibleEntries, { source });
|
|
6140
|
+
const updatedEntriesById = new Map();
|
|
6141
|
+
for (let index = 0; index < visibleEntries.length; index += 1) {
|
|
6142
|
+
const originalEntry = visibleEntries[index];
|
|
6143
|
+
const hydratedEntry = hydratedVisibleEntries[index];
|
|
6144
|
+
if (!originalEntry || !hydratedEntry) {
|
|
6145
|
+
continue;
|
|
6146
|
+
}
|
|
6147
|
+
if (
|
|
6148
|
+
originalEntry.messageCount !== hydratedEntry.messageCount
|
|
6149
|
+
|| originalEntry.messageCountMtimeMs !== hydratedEntry.messageCountMtimeMs
|
|
6150
|
+
) {
|
|
6151
|
+
updatedEntriesById.set(originalEntry.trashId, hydratedEntry);
|
|
6152
|
+
}
|
|
6153
|
+
}
|
|
6154
|
+
if (updatedEntriesById.size > 0) {
|
|
6155
|
+
const latestEntries = readSessionTrashEntries({ cleanup: false });
|
|
6156
|
+
writeSessionTrashEntries(latestEntries.map((entry) => updatedEntriesById.get(entry.trashId) || entry));
|
|
6157
|
+
}
|
|
5100
6158
|
return {
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
6159
|
+
totalCount,
|
|
6160
|
+
items: hydratedVisibleEntries.map((item) => ({
|
|
6161
|
+
...item,
|
|
6162
|
+
trashFilePath: resolveSessionTrashFilePath(item)
|
|
6163
|
+
}))
|
|
5106
6164
|
};
|
|
5107
6165
|
}
|
|
5108
6166
|
|
|
5109
|
-
async function
|
|
5110
|
-
const
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
payload.enabled = true;
|
|
5114
|
-
|
|
5115
|
-
const saveResult = saveBuiltinProxySettings(payload);
|
|
5116
|
-
if (saveResult.error) {
|
|
5117
|
-
return { error: saveResult.error };
|
|
6167
|
+
async function restoreSessionTrashItem(params = {}) {
|
|
6168
|
+
const trashId = typeof params.trashId === 'string' ? params.trashId.trim() : '';
|
|
6169
|
+
if (!trashId) {
|
|
6170
|
+
return { error: '请先选择要恢复的回收站记录' };
|
|
5118
6171
|
}
|
|
5119
|
-
let nextSettings = saveResult.settings;
|
|
5120
6172
|
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
6173
|
+
const entries = readSessionTrashEntries();
|
|
6174
|
+
const entry = entries.find((item) => item.trashId === trashId);
|
|
6175
|
+
if (!entry) {
|
|
6176
|
+
return { error: '回收站记录不存在' };
|
|
6177
|
+
}
|
|
6178
|
+
const hydratedEntry = await resolveSessionTrashEntryExactMessageCount(entry);
|
|
6179
|
+
if (!hydratedEntry) {
|
|
6180
|
+
return { error: '回收站记录不存在' };
|
|
5124
6181
|
}
|
|
5125
6182
|
|
|
5126
|
-
const
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|| runtime.settings.authSource !== nextSettings.authSource
|
|
5131
|
-
|| runtime.settings.timeoutMs !== nextSettings.timeoutMs
|
|
5132
|
-
|| runtime.upstream.providerName !== upstreamResult.providerName
|
|
5133
|
-
|| runtime.upstream.baseUrl !== upstreamResult.baseUrl
|
|
5134
|
-
|| runtime.upstream.authHeader !== upstreamResult.authHeader
|
|
5135
|
-
);
|
|
6183
|
+
const trashFilePath = resolveSessionTrashFilePath(hydratedEntry);
|
|
6184
|
+
if (!trashFilePath || !fs.existsSync(trashFilePath)) {
|
|
6185
|
+
return { error: '回收站文件不存在' };
|
|
6186
|
+
}
|
|
5136
6187
|
|
|
5137
|
-
|
|
5138
|
-
|
|
6188
|
+
const targetFilePath = resolveSessionRestoreTarget(hydratedEntry);
|
|
6189
|
+
if (!targetFilePath) {
|
|
6190
|
+
return { error: '原始会话路径非法,无法恢复' };
|
|
6191
|
+
}
|
|
6192
|
+
if (fs.existsSync(targetFilePath)) {
|
|
6193
|
+
return { error: '原始会话路径已存在同名文件,请先手动处理冲突' };
|
|
5139
6194
|
}
|
|
5140
6195
|
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
6196
|
+
let claudeIndexPath = '';
|
|
6197
|
+
try {
|
|
6198
|
+
const latestEntries = readSessionTrashEntries({ cleanup: false });
|
|
6199
|
+
const latestEntry = latestEntries.find((item) => item && item.trashId === trashId);
|
|
6200
|
+
if (!latestEntry) {
|
|
6201
|
+
return { error: '回收站记录不存在' };
|
|
6202
|
+
}
|
|
6203
|
+
const remainingEntries = latestEntries.filter((item) => item.trashId !== trashId);
|
|
6204
|
+
moveFileSync(trashFilePath, targetFilePath);
|
|
6205
|
+
if (hydratedEntry.source === 'claude') {
|
|
6206
|
+
claudeIndexPath = resolveClaudeSessionRestoreIndexPath(hydratedEntry, targetFilePath);
|
|
6207
|
+
upsertClaudeSessionIndexEntry(claudeIndexPath, targetFilePath, hydratedEntry);
|
|
6208
|
+
}
|
|
6209
|
+
writeSessionTrashEntries(remainingEntries);
|
|
6210
|
+
} catch (e) {
|
|
6211
|
+
let rollbackSucceeded = false;
|
|
6212
|
+
if (fs.existsSync(targetFilePath) && !fs.existsSync(trashFilePath)) {
|
|
6213
|
+
try {
|
|
6214
|
+
moveFileSync(targetFilePath, trashFilePath);
|
|
6215
|
+
rollbackSucceeded = true;
|
|
6216
|
+
} catch (_) {}
|
|
5160
6217
|
}
|
|
5161
|
-
if (
|
|
5162
|
-
|
|
6218
|
+
if (rollbackSucceeded && entry.source === 'claude' && claudeIndexPath && fs.existsSync(claudeIndexPath)) {
|
|
6219
|
+
try {
|
|
6220
|
+
removeClaudeSessionIndexEntry(claudeIndexPath, targetFilePath, entry.sessionId);
|
|
6221
|
+
} catch (_) {}
|
|
5163
6222
|
}
|
|
6223
|
+
return { error: `恢复会话失败: ${e.message}` };
|
|
5164
6224
|
}
|
|
5165
6225
|
|
|
5166
|
-
|
|
6226
|
+
invalidateSessionListCache();
|
|
6227
|
+
|
|
6228
|
+
return {
|
|
5167
6229
|
success: true,
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
|
|
6230
|
+
restored: true,
|
|
6231
|
+
trashId,
|
|
6232
|
+
source: entry.source,
|
|
6233
|
+
sessionId: entry.sessionId,
|
|
6234
|
+
filePath: targetFilePath
|
|
5172
6235
|
};
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
6236
|
+
}
|
|
6237
|
+
|
|
6238
|
+
async function purgeSessionTrashItems(params = {}) {
|
|
6239
|
+
const entries = readSessionTrashEntries();
|
|
6240
|
+
if (entries.length === 0) {
|
|
6241
|
+
return { success: true, purged: [], count: 0 };
|
|
6242
|
+
}
|
|
6243
|
+
|
|
6244
|
+
const all = params.all === true;
|
|
6245
|
+
const trashIds = Array.isArray(params.trashIds)
|
|
6246
|
+
? params.trashIds
|
|
6247
|
+
.map((item) => (typeof item === 'string' ? item.trim() : ''))
|
|
6248
|
+
.filter(Boolean)
|
|
6249
|
+
: [];
|
|
6250
|
+
const singleTrashId = typeof params.trashId === 'string' ? params.trashId.trim() : '';
|
|
6251
|
+
const targetIds = all
|
|
6252
|
+
? new Set(entries.map((item) => item.trashId))
|
|
6253
|
+
: new Set(singleTrashId ? [singleTrashId, ...trashIds] : trashIds);
|
|
6254
|
+
|
|
6255
|
+
if (targetIds.size === 0) {
|
|
6256
|
+
return { error: '请先选择要彻底删除的回收站记录' };
|
|
6257
|
+
}
|
|
6258
|
+
|
|
6259
|
+
const purged = [];
|
|
6260
|
+
const remaining = [];
|
|
6261
|
+
let purgeError = null;
|
|
6262
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
6263
|
+
const entry = entries[index];
|
|
6264
|
+
if (!targetIds.has(entry.trashId)) {
|
|
6265
|
+
remaining.push(entry);
|
|
6266
|
+
continue;
|
|
6267
|
+
}
|
|
6268
|
+
const trashFilePath = resolveSessionTrashFilePath(entry);
|
|
6269
|
+
if (trashFilePath && fs.existsSync(trashFilePath)) {
|
|
6270
|
+
try {
|
|
6271
|
+
fs.unlinkSync(trashFilePath);
|
|
6272
|
+
} catch (e) {
|
|
6273
|
+
if (!purgeError) purgeError = e;
|
|
6274
|
+
remaining.push(entry);
|
|
6275
|
+
continue;
|
|
6276
|
+
}
|
|
5177
6277
|
}
|
|
6278
|
+
purged.push({
|
|
6279
|
+
trashId: entry.trashId,
|
|
6280
|
+
source: entry.source,
|
|
6281
|
+
sessionId: entry.sessionId
|
|
6282
|
+
});
|
|
6283
|
+
}
|
|
6284
|
+
|
|
6285
|
+
try {
|
|
6286
|
+
writeSessionTrashEntries(remaining);
|
|
6287
|
+
} catch (e) {
|
|
6288
|
+
return { error: `回收站索引更新失败: ${e.message}` };
|
|
6289
|
+
}
|
|
6290
|
+
|
|
6291
|
+
if (purgeError) {
|
|
6292
|
+
return { error: `彻底删除失败: ${purgeError.message}` };
|
|
5178
6293
|
}
|
|
5179
6294
|
|
|
5180
|
-
const status = getBuiltinProxyStatus();
|
|
5181
6295
|
return {
|
|
5182
6296
|
success: true,
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
switched: applyRes.switched,
|
|
5186
|
-
model: applyRes.model || '',
|
|
5187
|
-
settings: status.settings,
|
|
5188
|
-
runtime: status.runtime
|
|
6297
|
+
purged,
|
|
6298
|
+
count: purged.length
|
|
5189
6299
|
};
|
|
5190
6300
|
}
|
|
5191
6301
|
|
|
5192
|
-
function
|
|
5193
|
-
|
|
5194
|
-
|
|
6302
|
+
async function trashSessionData(params = {}) {
|
|
6303
|
+
const source = params.source === 'claude' ? 'claude' : (params.source === 'codex' ? 'codex' : '');
|
|
6304
|
+
if (!source) {
|
|
6305
|
+
return { error: 'Invalid source' };
|
|
5195
6306
|
}
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
6307
|
+
|
|
6308
|
+
const filePath = resolveSessionFilePath(source, getSessionFileArg(params), params.sessionId);
|
|
6309
|
+
if (!filePath) {
|
|
6310
|
+
return { error: 'Session file not found' };
|
|
5199
6311
|
}
|
|
5200
|
-
|
|
5201
|
-
const
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
6312
|
+
|
|
6313
|
+
const summary = (source === 'claude' ? parseClaudeSessionSummary(filePath) : parseCodexSessionSummary(filePath))
|
|
6314
|
+
|| buildSessionSummaryFallback(source, filePath, params.sessionId);
|
|
6315
|
+
const exactMessageCount = await countConversationMessagesInFile(filePath, source);
|
|
6316
|
+
if (Number.isFinite(Number(exactMessageCount))) {
|
|
6317
|
+
summary.messageCount = Math.max(0, Math.floor(Number(exactMessageCount)));
|
|
6318
|
+
}
|
|
6319
|
+
const sessionId = summary.sessionId || params.sessionId || path.basename(filePath, '.jsonl');
|
|
6320
|
+
const { trashId, trashFileName, trashFilePath } = allocateSessionTrashTarget();
|
|
6321
|
+
const deletedAt = new Date().toISOString();
|
|
6322
|
+
const claudeIndexPath = source === 'claude' ? findClaudeSessionIndexPath(filePath) : '';
|
|
6323
|
+
let removedClaudeIndexEntry = null;
|
|
6324
|
+
|
|
6325
|
+
try {
|
|
6326
|
+
moveFileSync(filePath, trashFilePath);
|
|
6327
|
+
} catch (e) {
|
|
6328
|
+
return { error: `移入回收站失败: ${e.message}` };
|
|
6329
|
+
}
|
|
6330
|
+
|
|
6331
|
+
try {
|
|
6332
|
+
if (source === 'claude' && claudeIndexPath) {
|
|
6333
|
+
const removal = removeClaudeSessionIndexEntry(claudeIndexPath, filePath, sessionId);
|
|
6334
|
+
removedClaudeIndexEntry = removal && removal.entry ? removal.entry : null;
|
|
6335
|
+
}
|
|
6336
|
+
const entry = buildSessionTrashEntry(summary, {
|
|
6337
|
+
trashId,
|
|
6338
|
+
trashFileName,
|
|
6339
|
+
trashFilePath,
|
|
6340
|
+
source,
|
|
6341
|
+
sessionId,
|
|
6342
|
+
deletedAt,
|
|
6343
|
+
originalFilePath: filePath,
|
|
6344
|
+
claudeIndexPath,
|
|
6345
|
+
claudeIndexEntry: removedClaudeIndexEntry
|
|
6346
|
+
});
|
|
6347
|
+
const entries = readSessionTrashEntries({ cleanup: false });
|
|
6348
|
+
const totalCount = entries.length + 1;
|
|
6349
|
+
const nextEntries = [entry, ...entries].slice(0, MAX_SESSION_TRASH_LIST_SIZE);
|
|
6350
|
+
writeSessionTrashEntries(nextEntries);
|
|
6351
|
+
summary.totalCount = Math.min(totalCount, MAX_SESSION_TRASH_LIST_SIZE);
|
|
6352
|
+
} catch (e) {
|
|
6353
|
+
let rollbackSucceeded = false;
|
|
6354
|
+
if (fs.existsSync(trashFilePath) && !fs.existsSync(filePath)) {
|
|
6355
|
+
try {
|
|
6356
|
+
moveFileSync(trashFilePath, filePath);
|
|
6357
|
+
rollbackSucceeded = true;
|
|
6358
|
+
} catch (_) {}
|
|
5205
6359
|
}
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
6360
|
+
if (rollbackSucceeded && source === 'claude' && claudeIndexPath && removedClaudeIndexEntry) {
|
|
6361
|
+
try {
|
|
6362
|
+
upsertClaudeSessionIndexEntry(claudeIndexPath, filePath, {
|
|
6363
|
+
source,
|
|
6364
|
+
sessionId,
|
|
6365
|
+
title: summary.title,
|
|
6366
|
+
messageCount: summary.messageCount,
|
|
6367
|
+
capabilities: summary.capabilities,
|
|
6368
|
+
keywords: summary.keywords,
|
|
6369
|
+
updatedAt: summary.updatedAt,
|
|
6370
|
+
createdAt: summary.createdAt,
|
|
6371
|
+
claudeIndexEntry: removedClaudeIndexEntry,
|
|
6372
|
+
originalFilePath: filePath,
|
|
6373
|
+
trashId,
|
|
6374
|
+
trashFileName
|
|
6375
|
+
});
|
|
6376
|
+
} catch (_) {}
|
|
5209
6377
|
}
|
|
5210
|
-
if (
|
|
5211
|
-
|
|
5212
|
-
const entryPath = expanded ? path.resolve(expanded) : '';
|
|
5213
|
-
if (entryPath && resolvedLower && entryPath.toLowerCase() === resolvedLower) {
|
|
5214
|
-
return false;
|
|
5215
|
-
}
|
|
6378
|
+
if (!rollbackSucceeded && fs.existsSync(trashFilePath)) {
|
|
6379
|
+
try { fs.unlinkSync(trashFilePath); } catch (_) {}
|
|
5216
6380
|
}
|
|
5217
|
-
return
|
|
5218
|
-
});
|
|
5219
|
-
if (filtered.length === index.entries.length) {
|
|
5220
|
-
return;
|
|
6381
|
+
return { error: `移入回收站失败: ${e.message}` };
|
|
5221
6382
|
}
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
6383
|
+
|
|
6384
|
+
invalidateSessionListCache();
|
|
6385
|
+
|
|
6386
|
+
return {
|
|
6387
|
+
success: true,
|
|
6388
|
+
source,
|
|
6389
|
+
sessionId,
|
|
6390
|
+
filePath,
|
|
6391
|
+
trashed: true,
|
|
6392
|
+
trashId,
|
|
6393
|
+
deletedAt,
|
|
6394
|
+
totalCount: Number.isFinite(Number(summary && summary.totalCount))
|
|
6395
|
+
? Math.max(0, Math.floor(Number(summary.totalCount)))
|
|
6396
|
+
: undefined,
|
|
6397
|
+
messageCount: Number.isFinite(Number(summary && summary.messageCount))
|
|
6398
|
+
? Math.max(0, Math.floor(Number(summary.messageCount)))
|
|
6399
|
+
: 0
|
|
6400
|
+
};
|
|
5226
6401
|
}
|
|
5227
6402
|
|
|
5228
6403
|
async function deleteSessionData(params = {}) {
|
|
@@ -5231,14 +6406,16 @@ async function deleteSessionData(params = {}) {
|
|
|
5231
6406
|
return { error: 'Invalid source' };
|
|
5232
6407
|
}
|
|
5233
6408
|
|
|
5234
|
-
const filePath = resolveSessionFilePath(source, params
|
|
6409
|
+
const filePath = resolveSessionFilePath(source, getSessionFileArg(params), params.sessionId);
|
|
5235
6410
|
if (!filePath) {
|
|
5236
6411
|
return { error: 'Session file not found' };
|
|
5237
6412
|
}
|
|
5238
6413
|
|
|
5239
6414
|
const sessionId = params.sessionId || path.basename(filePath, '.jsonl');
|
|
6415
|
+
let fileDeleted = false;
|
|
5240
6416
|
try {
|
|
5241
6417
|
fs.unlinkSync(filePath);
|
|
6418
|
+
fileDeleted = true;
|
|
5242
6419
|
} catch (e) {
|
|
5243
6420
|
return { error: `删除会话失败: ${e.message}` };
|
|
5244
6421
|
}
|
|
@@ -5246,7 +6423,14 @@ async function deleteSessionData(params = {}) {
|
|
|
5246
6423
|
if (source === 'claude') {
|
|
5247
6424
|
const indexPath = findClaudeSessionIndexPath(filePath);
|
|
5248
6425
|
if (indexPath) {
|
|
5249
|
-
|
|
6426
|
+
try {
|
|
6427
|
+
removeClaudeSessionIndexEntry(indexPath, filePath, sessionId);
|
|
6428
|
+
} catch (e) {
|
|
6429
|
+
console.warn('删除会话索引失败:', e && e.message ? e.message : e);
|
|
6430
|
+
if (!fileDeleted) {
|
|
6431
|
+
return { error: `删除会话失败: ${e.message || e}` };
|
|
6432
|
+
}
|
|
6433
|
+
}
|
|
5250
6434
|
}
|
|
5251
6435
|
}
|
|
5252
6436
|
|
|
@@ -5256,7 +6440,8 @@ async function deleteSessionData(params = {}) {
|
|
|
5256
6440
|
success: true,
|
|
5257
6441
|
source,
|
|
5258
6442
|
sessionId,
|
|
5259
|
-
filePath
|
|
6443
|
+
filePath,
|
|
6444
|
+
deleted: true
|
|
5260
6445
|
};
|
|
5261
6446
|
}
|
|
5262
6447
|
|
|
@@ -5311,7 +6496,7 @@ async function cloneCodexSession(params = {}) {
|
|
|
5311
6496
|
return { error: '仅支持 Codex 会话克隆' };
|
|
5312
6497
|
}
|
|
5313
6498
|
|
|
5314
|
-
const filePath = resolveSessionFilePath(source, params
|
|
6499
|
+
const filePath = resolveSessionFilePath(source, getSessionFileArg(params), params.sessionId);
|
|
5315
6500
|
if (!filePath) {
|
|
5316
6501
|
return { error: 'Session file not found' };
|
|
5317
6502
|
}
|
|
@@ -5690,26 +6875,26 @@ async function readSessionDetail(params = {}) {
|
|
|
5690
6875
|
return { error: 'Invalid source' };
|
|
5691
6876
|
}
|
|
5692
6877
|
|
|
5693
|
-
const filePath = resolveSessionFilePath(source, params
|
|
6878
|
+
const filePath = resolveSessionFilePath(source, getSessionFileArg(params), params.sessionId);
|
|
5694
6879
|
if (!filePath) {
|
|
5695
6880
|
return { error: 'Session file not found' };
|
|
5696
6881
|
}
|
|
5697
6882
|
|
|
5698
|
-
const
|
|
6883
|
+
const rawMaxMessages = Number(params.maxMessages);
|
|
6884
|
+
const rawLimit = Number.isFinite(rawMaxMessages) ? rawMaxMessages : Number(params.messageLimit);
|
|
5699
6885
|
const messageLimit = Number.isFinite(rawLimit)
|
|
5700
6886
|
? Math.max(1, Math.min(rawLimit, MAX_SESSION_DETAIL_MESSAGES))
|
|
5701
6887
|
: DEFAULT_SESSION_DETAIL_MESSAGES;
|
|
5702
6888
|
|
|
5703
|
-
const extracted = await
|
|
6889
|
+
const extracted = await extractSessionDetailPreviewFromFile(filePath, source, messageLimit);
|
|
5704
6890
|
const sessionId = extracted.sessionId || params.sessionId || path.basename(filePath, '.jsonl');
|
|
5705
6891
|
const sourceLabel = source === 'codex' ? 'Codex' : 'Claude Code';
|
|
5706
|
-
const
|
|
5707
|
-
|
|
5708
|
-
|
|
5709
|
-
|
|
5710
|
-
|
|
5711
|
-
|
|
5712
|
-
const clippedMessages = allMessages.slice(startIndex);
|
|
6892
|
+
const clippedMessages = Array.isArray(extracted.messages) ? extracted.messages : [];
|
|
6893
|
+
const startIndex = Math.max(0, extracted.totalMessages - clippedMessages.length);
|
|
6894
|
+
const indexedMessages = clippedMessages.map((message, messageIndex) => ({
|
|
6895
|
+
...message,
|
|
6896
|
+
messageIndex: startIndex + messageIndex
|
|
6897
|
+
}));
|
|
5713
6898
|
|
|
5714
6899
|
return {
|
|
5715
6900
|
source,
|
|
@@ -5717,10 +6902,10 @@ async function readSessionDetail(params = {}) {
|
|
|
5717
6902
|
sessionId,
|
|
5718
6903
|
cwd: extracted.cwd || '',
|
|
5719
6904
|
updatedAt: extracted.updatedAt || '',
|
|
5720
|
-
totalMessages:
|
|
5721
|
-
clipped:
|
|
6905
|
+
totalMessages: extracted.totalMessages,
|
|
6906
|
+
clipped: extracted.totalMessages > indexedMessages.length,
|
|
5722
6907
|
messageLimit,
|
|
5723
|
-
messages:
|
|
6908
|
+
messages: indexedMessages,
|
|
5724
6909
|
filePath
|
|
5725
6910
|
};
|
|
5726
6911
|
}
|
|
@@ -5731,7 +6916,7 @@ async function readSessionPlain(params = {}) {
|
|
|
5731
6916
|
return { error: 'Invalid source' };
|
|
5732
6917
|
}
|
|
5733
6918
|
|
|
5734
|
-
const filePath = resolveSessionFilePath(source, params
|
|
6919
|
+
const filePath = resolveSessionFilePath(source, getSessionFileArg(params), params.sessionId);
|
|
5735
6920
|
if (!filePath) {
|
|
5736
6921
|
return { error: 'Session file not found' };
|
|
5737
6922
|
}
|
|
@@ -5777,7 +6962,7 @@ async function exportSessionData(params = {}) {
|
|
|
5777
6962
|
}
|
|
5778
6963
|
|
|
5779
6964
|
const maxMessages = resolveMaxMessagesValue(params.maxMessages, MAX_EXPORT_MESSAGES);
|
|
5780
|
-
const filePath = resolveSessionFilePath(source, params
|
|
6965
|
+
const filePath = resolveSessionFilePath(source, getSessionFileArg(params), params.sessionId);
|
|
5781
6966
|
if (!filePath) {
|
|
5782
6967
|
return { error: 'Session file not found' };
|
|
5783
6968
|
}
|
|
@@ -8483,8 +9668,23 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
8483
9668
|
}
|
|
8484
9669
|
if (requestPath === '/api') {
|
|
8485
9670
|
let body = '';
|
|
8486
|
-
|
|
9671
|
+
let bodySize = 0;
|
|
9672
|
+
let bodyTooLarge = false;
|
|
9673
|
+
req.on('data', chunk => {
|
|
9674
|
+
if (bodyTooLarge) return;
|
|
9675
|
+
bodySize += chunk.length;
|
|
9676
|
+
if (bodySize > MAX_API_BODY_SIZE) {
|
|
9677
|
+
bodyTooLarge = true;
|
|
9678
|
+
writeJsonResponse(res, 413, {
|
|
9679
|
+
error: `请求体过大(>${Math.floor(MAX_API_BODY_SIZE / 1024 / 1024)}MB)`
|
|
9680
|
+
});
|
|
9681
|
+
req.destroy();
|
|
9682
|
+
return;
|
|
9683
|
+
}
|
|
9684
|
+
body += chunk;
|
|
9685
|
+
});
|
|
8487
9686
|
req.on('end', async () => {
|
|
9687
|
+
if (bodyTooLarge) return;
|
|
8488
9688
|
try {
|
|
8489
9689
|
const { action, params } = JSON.parse(body);
|
|
8490
9690
|
let result;
|
|
@@ -8592,6 +9792,9 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
8592
9792
|
case 'apply-agents-file':
|
|
8593
9793
|
result = applyAgentsFile(params || {});
|
|
8594
9794
|
break;
|
|
9795
|
+
case 'preview-agents-diff':
|
|
9796
|
+
result = buildAgentsDiff(params || {});
|
|
9797
|
+
break;
|
|
8595
9798
|
case 'list-codex-skills':
|
|
8596
9799
|
result = listCodexSkills();
|
|
8597
9800
|
break;
|
|
@@ -8679,7 +9882,7 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
8679
9882
|
result = { error: 'Invalid source. Must be codex, claude, or all' };
|
|
8680
9883
|
} else {
|
|
8681
9884
|
result = {
|
|
8682
|
-
sessions:
|
|
9885
|
+
sessions: await listAllSessionsData(params),
|
|
8683
9886
|
source: source || 'all'
|
|
8684
9887
|
};
|
|
8685
9888
|
}
|
|
@@ -8697,6 +9900,18 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
8697
9900
|
}
|
|
8698
9901
|
}
|
|
8699
9902
|
break;
|
|
9903
|
+
case 'list-session-trash':
|
|
9904
|
+
result = await listSessionTrashItems(params || {});
|
|
9905
|
+
break;
|
|
9906
|
+
case 'restore-session-trash':
|
|
9907
|
+
result = await restoreSessionTrashItem(params || {});
|
|
9908
|
+
break;
|
|
9909
|
+
case 'purge-session-trash':
|
|
9910
|
+
result = await purgeSessionTrashItems(params || {});
|
|
9911
|
+
break;
|
|
9912
|
+
case 'trash-session':
|
|
9913
|
+
result = await trashSessionData(params || {});
|
|
9914
|
+
break;
|
|
8700
9915
|
case 'export-session':
|
|
8701
9916
|
result = await exportSessionData(params);
|
|
8702
9917
|
break;
|
|
@@ -8995,6 +10210,69 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
8995
10210
|
return { server, stop };
|
|
8996
10211
|
}
|
|
8997
10212
|
|
|
10213
|
+
// Region markers are used by unit tests that extract these helpers directly.
|
|
10214
|
+
// #region createSerializedWebUiRestartHandler
|
|
10215
|
+
function createSerializedWebUiRestartHandler(runRestart) {
|
|
10216
|
+
let restartQueued = false;
|
|
10217
|
+
let latestRestartInfo = null;
|
|
10218
|
+
let restartInFlight = null;
|
|
10219
|
+
|
|
10220
|
+
const drainRestartQueue = async () => {
|
|
10221
|
+
try {
|
|
10222
|
+
while (restartQueued) {
|
|
10223
|
+
restartQueued = false;
|
|
10224
|
+
await runRestart(latestRestartInfo);
|
|
10225
|
+
}
|
|
10226
|
+
} finally {
|
|
10227
|
+
restartInFlight = null;
|
|
10228
|
+
if (restartQueued) {
|
|
10229
|
+
restartInFlight = drainRestartQueue();
|
|
10230
|
+
return restartInFlight;
|
|
10231
|
+
}
|
|
10232
|
+
}
|
|
10233
|
+
};
|
|
10234
|
+
|
|
10235
|
+
return (info) => {
|
|
10236
|
+
latestRestartInfo = info;
|
|
10237
|
+
restartQueued = true;
|
|
10238
|
+
if (!restartInFlight) {
|
|
10239
|
+
restartInFlight = drainRestartQueue();
|
|
10240
|
+
}
|
|
10241
|
+
return restartInFlight;
|
|
10242
|
+
};
|
|
10243
|
+
}
|
|
10244
|
+
// #endregion createSerializedWebUiRestartHandler
|
|
10245
|
+
|
|
10246
|
+
// #region restartWebUiServerAfterFrontendChange
|
|
10247
|
+
async function restartWebUiServerAfterFrontendChange({
|
|
10248
|
+
serverHandle,
|
|
10249
|
+
serverOptions,
|
|
10250
|
+
createServer = createWebServer,
|
|
10251
|
+
delayMs = 3000,
|
|
10252
|
+
wait = setTimeout,
|
|
10253
|
+
logger = console
|
|
10254
|
+
}) {
|
|
10255
|
+
logger.log(' 正在停止旧服务...');
|
|
10256
|
+
try {
|
|
10257
|
+
await serverHandle.stop();
|
|
10258
|
+
logger.log(' 旧服务已停止');
|
|
10259
|
+
} catch (e) {
|
|
10260
|
+
logger.warn('! 停止旧服务失败:', e.message || e);
|
|
10261
|
+
}
|
|
10262
|
+
|
|
10263
|
+
await new Promise((resolve) => wait(resolve, delayMs));
|
|
10264
|
+
|
|
10265
|
+
try {
|
|
10266
|
+
const nextServerHandle = await createServer(serverOptions);
|
|
10267
|
+
logger.log('✓ 已重启 Web UI 服务\n');
|
|
10268
|
+
return nextServerHandle;
|
|
10269
|
+
} catch (e) {
|
|
10270
|
+
logger.error('! 重启失败:', e.message || e);
|
|
10271
|
+
return serverHandle;
|
|
10272
|
+
}
|
|
10273
|
+
}
|
|
10274
|
+
// #endregion restartWebUiServerAfterFrontendChange
|
|
10275
|
+
|
|
8998
10276
|
// 打开 Web UI
|
|
8999
10277
|
function cmdStart(options = {}) {
|
|
9000
10278
|
const webDir = path.join(__dirname, 'web-ui');
|
|
@@ -9038,32 +10316,28 @@ function cmdStart(options = {}) {
|
|
|
9038
10316
|
});
|
|
9039
10317
|
}
|
|
9040
10318
|
|
|
9041
|
-
const
|
|
9042
|
-
[webDir, legacyHtmlPath],
|
|
9043
|
-
async (info) => {
|
|
10319
|
+
const requestWebUiRestart = createSerializedWebUiRestartHandler(async (info) => {
|
|
9044
10320
|
const fileLabel = info && info.filename ? info.filename : (info && info.target ? path.basename(info.target) : 'unknown');
|
|
9045
10321
|
console.log(`\n~ 侦测到前端变更 (${fileLabel}),重启中...`);
|
|
9046
|
-
|
|
9047
|
-
|
|
9048
|
-
|
|
9049
|
-
console.log(' 旧服务已停止');
|
|
9050
|
-
} catch (e) {
|
|
9051
|
-
console.warn('! 停止旧服务失败:', e.message || e);
|
|
9052
|
-
}
|
|
9053
|
-
await new Promise((resolve) => setTimeout(resolve, 80));
|
|
9054
|
-
try {
|
|
9055
|
-
serverHandle = createWebServer({
|
|
10322
|
+
serverHandle = await restartWebUiServerAfterFrontendChange({
|
|
10323
|
+
serverHandle,
|
|
10324
|
+
serverOptions: {
|
|
9056
10325
|
htmlPath,
|
|
9057
10326
|
assetsDir,
|
|
9058
10327
|
webDir,
|
|
9059
10328
|
host,
|
|
9060
10329
|
port,
|
|
9061
10330
|
openBrowser: false
|
|
9062
|
-
}
|
|
9063
|
-
|
|
9064
|
-
|
|
9065
|
-
|
|
9066
|
-
|
|
10331
|
+
}
|
|
10332
|
+
});
|
|
10333
|
+
});
|
|
10334
|
+
|
|
10335
|
+
const stopWatch = watchPathsForRestart(
|
|
10336
|
+
[webDir, legacyHtmlPath],
|
|
10337
|
+
(info) => {
|
|
10338
|
+
void requestWebUiRestart(info).catch((err) => {
|
|
10339
|
+
console.error('! 重启 Web UI 失败:', err && err.message ? err.message : err);
|
|
10340
|
+
});
|
|
9067
10341
|
}
|
|
9068
10342
|
);
|
|
9069
10343
|
|
|
@@ -10389,7 +11663,7 @@ function createWorkflowToolCatalog() {
|
|
|
10389
11663
|
}
|
|
10390
11664
|
return {
|
|
10391
11665
|
source: source || 'all',
|
|
10392
|
-
sessions:
|
|
11666
|
+
sessions: await listAllSessionsData({
|
|
10393
11667
|
...args,
|
|
10394
11668
|
source: source || 'all'
|
|
10395
11669
|
})
|
|
@@ -10756,7 +12030,7 @@ function createMcpTools(options = {}) {
|
|
|
10756
12030
|
source: source || 'all'
|
|
10757
12031
|
};
|
|
10758
12032
|
return {
|
|
10759
|
-
sessions:
|
|
12033
|
+
sessions: await listAllSessionsData(normalizedInput),
|
|
10760
12034
|
source: source || 'all'
|
|
10761
12035
|
};
|
|
10762
12036
|
}
|
|
@@ -10992,18 +12266,34 @@ function createMcpTools(options = {}) {
|
|
|
10992
12266
|
handler: async (args = {}) => applyOpenclawConfig(args || {})
|
|
10993
12267
|
});
|
|
10994
12268
|
|
|
12269
|
+
pushTool({
|
|
12270
|
+
name: 'codexmate.session.trash',
|
|
12271
|
+
description: 'Move one entire session file into session trash.',
|
|
12272
|
+
readOnly: false,
|
|
12273
|
+
inputSchema: {
|
|
12274
|
+
type: 'object',
|
|
12275
|
+
properties: {
|
|
12276
|
+
source: { type: 'string' },
|
|
12277
|
+
sessionId: { type: 'string' },
|
|
12278
|
+
filePath: { type: 'string' },
|
|
12279
|
+
file: { type: 'string' }
|
|
12280
|
+
},
|
|
12281
|
+
additionalProperties: true
|
|
12282
|
+
},
|
|
12283
|
+
handler: async (args = {}) => trashSessionData(args || {})
|
|
12284
|
+
});
|
|
12285
|
+
|
|
10995
12286
|
pushTool({
|
|
10996
12287
|
name: 'codexmate.session.delete',
|
|
10997
|
-
description: '
|
|
12288
|
+
description: 'Permanently delete one entire session file.',
|
|
10998
12289
|
readOnly: false,
|
|
10999
12290
|
inputSchema: {
|
|
11000
12291
|
type: 'object',
|
|
11001
12292
|
properties: {
|
|
11002
12293
|
source: { type: 'string' },
|
|
11003
12294
|
sessionId: { type: 'string' },
|
|
11004
|
-
|
|
11005
|
-
|
|
11006
|
-
recordLineIndices: { type: 'array', items: { type: 'number' } }
|
|
12295
|
+
filePath: { type: 'string' },
|
|
12296
|
+
file: { type: 'string' }
|
|
11007
12297
|
},
|
|
11008
12298
|
additionalProperties: true
|
|
11009
12299
|
},
|
|
@@ -11153,7 +12443,7 @@ function createMcpResources() {
|
|
|
11153
12443
|
}
|
|
11154
12444
|
const payload = {
|
|
11155
12445
|
source: normalizedSource || 'all',
|
|
11156
|
-
sessions:
|
|
12446
|
+
sessions: await listAllSessionsData({
|
|
11157
12447
|
source: normalizedSource || 'all',
|
|
11158
12448
|
query,
|
|
11159
12449
|
pathFilter,
|