codexmate 0.0.16 → 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 -24
- package/README.md +42 -24
- package/cli.js +1457 -157
- package/lib/text-diff.js +303 -0
- package/package.json +2 -2
- package/web-ui/app.js +1946 -247
- package/web-ui/index.html +321 -78
- package/web-ui/logic.mjs +503 -13
- package/web-ui/modules/skills.methods.mjs +7 -1
- package/web-ui/session-helpers.mjs +350 -0
- package/web-ui/styles.css +679 -31
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) {
|
|
@@ -5086,143 +5606,798 @@ function applyBuiltinProxyProvider(params = {}) {
|
|
|
5086
5606
|
if (saveResult && saveResult.error) {
|
|
5087
5607
|
return saveResult;
|
|
5088
5608
|
}
|
|
5089
|
-
|
|
5090
|
-
const switchToProxy = params.switchToProxy !== false;
|
|
5091
|
-
let targetModel = '';
|
|
5092
|
-
if (switchToProxy) {
|
|
5093
|
-
try {
|
|
5094
|
-
targetModel = cmdSwitch(BUILTIN_PROXY_PROVIDER_NAME, true) || '';
|
|
5095
|
-
} catch (e) {
|
|
5096
|
-
return { error: `写入代理 provider 成功,但切换失败: ${e.message}` };
|
|
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;
|
|
6079
|
+
}
|
|
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
|
}
|
|
@@ -5891,8 +7076,15 @@ function buildProviderSharePayload(params = {}) {
|
|
|
5891
7076
|
|
|
5892
7077
|
const baseUrl = typeof provider.base_url === 'string' ? provider.base_url.trim() : '';
|
|
5893
7078
|
const apiKey = typeof provider.preferred_auth_method === 'string'
|
|
5894
|
-
? provider.preferred_auth_method
|
|
7079
|
+
? provider.preferred_auth_method.trim()
|
|
7080
|
+
: '';
|
|
7081
|
+
const currentModels = readCurrentModels();
|
|
7082
|
+
const savedModel = currentModels && typeof currentModels[name] === 'string'
|
|
7083
|
+
? currentModels[name].trim()
|
|
5895
7084
|
: '';
|
|
7085
|
+
const activeProvider = typeof config.model_provider === 'string' ? config.model_provider.trim() : '';
|
|
7086
|
+
const activeModel = typeof config.model === 'string' ? config.model.trim() : '';
|
|
7087
|
+
const model = savedModel || (activeProvider === name ? activeModel : '');
|
|
5896
7088
|
|
|
5897
7089
|
if (!baseUrl) {
|
|
5898
7090
|
return { error: `提供商 ${name} 缺少 base_url` };
|
|
@@ -5902,7 +7094,8 @@ function buildProviderSharePayload(params = {}) {
|
|
|
5902
7094
|
payload: {
|
|
5903
7095
|
name,
|
|
5904
7096
|
baseUrl,
|
|
5905
|
-
apiKey
|
|
7097
|
+
apiKey,
|
|
7098
|
+
model
|
|
5906
7099
|
}
|
|
5907
7100
|
};
|
|
5908
7101
|
}
|
|
@@ -6929,6 +8122,8 @@ function readClaudeSettingsInfo() {
|
|
|
6929
8122
|
exists: !!readResult.exists,
|
|
6930
8123
|
targetPath: CLAUDE_SETTINGS_FILE,
|
|
6931
8124
|
apiKey: typeof env.ANTHROPIC_API_KEY === 'string' ? env.ANTHROPIC_API_KEY : '',
|
|
8125
|
+
authToken: typeof env.ANTHROPIC_AUTH_TOKEN === 'string' ? env.ANTHROPIC_AUTH_TOKEN : '',
|
|
8126
|
+
useKey: typeof env.CLAUDE_CODE_USE_KEY === 'string' ? env.CLAUDE_CODE_USE_KEY : '',
|
|
6932
8127
|
baseUrl: typeof env.ANTHROPIC_BASE_URL === 'string' ? env.ANTHROPIC_BASE_URL : '',
|
|
6933
8128
|
model: typeof env.ANTHROPIC_MODEL === 'string' ? env.ANTHROPIC_MODEL : '',
|
|
6934
8129
|
env
|
|
@@ -8473,8 +9668,23 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
8473
9668
|
}
|
|
8474
9669
|
if (requestPath === '/api') {
|
|
8475
9670
|
let body = '';
|
|
8476
|
-
|
|
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
|
+
});
|
|
8477
9686
|
req.on('end', async () => {
|
|
9687
|
+
if (bodyTooLarge) return;
|
|
8478
9688
|
try {
|
|
8479
9689
|
const { action, params } = JSON.parse(body);
|
|
8480
9690
|
let result;
|
|
@@ -8582,6 +9792,9 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
8582
9792
|
case 'apply-agents-file':
|
|
8583
9793
|
result = applyAgentsFile(params || {});
|
|
8584
9794
|
break;
|
|
9795
|
+
case 'preview-agents-diff':
|
|
9796
|
+
result = buildAgentsDiff(params || {});
|
|
9797
|
+
break;
|
|
8585
9798
|
case 'list-codex-skills':
|
|
8586
9799
|
result = listCodexSkills();
|
|
8587
9800
|
break;
|
|
@@ -8669,7 +9882,7 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
8669
9882
|
result = { error: 'Invalid source. Must be codex, claude, or all' };
|
|
8670
9883
|
} else {
|
|
8671
9884
|
result = {
|
|
8672
|
-
sessions:
|
|
9885
|
+
sessions: await listAllSessionsData(params),
|
|
8673
9886
|
source: source || 'all'
|
|
8674
9887
|
};
|
|
8675
9888
|
}
|
|
@@ -8687,6 +9900,18 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
8687
9900
|
}
|
|
8688
9901
|
}
|
|
8689
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;
|
|
8690
9915
|
case 'export-session':
|
|
8691
9916
|
result = await exportSessionData(params);
|
|
8692
9917
|
break;
|
|
@@ -8985,6 +10210,69 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
8985
10210
|
return { server, stop };
|
|
8986
10211
|
}
|
|
8987
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
|
+
|
|
8988
10276
|
// 打开 Web UI
|
|
8989
10277
|
function cmdStart(options = {}) {
|
|
8990
10278
|
const webDir = path.join(__dirname, 'web-ui');
|
|
@@ -9028,32 +10316,28 @@ function cmdStart(options = {}) {
|
|
|
9028
10316
|
});
|
|
9029
10317
|
}
|
|
9030
10318
|
|
|
9031
|
-
const
|
|
9032
|
-
[webDir, legacyHtmlPath],
|
|
9033
|
-
async (info) => {
|
|
10319
|
+
const requestWebUiRestart = createSerializedWebUiRestartHandler(async (info) => {
|
|
9034
10320
|
const fileLabel = info && info.filename ? info.filename : (info && info.target ? path.basename(info.target) : 'unknown');
|
|
9035
10321
|
console.log(`\n~ 侦测到前端变更 (${fileLabel}),重启中...`);
|
|
9036
|
-
|
|
9037
|
-
|
|
9038
|
-
|
|
9039
|
-
console.log(' 旧服务已停止');
|
|
9040
|
-
} catch (e) {
|
|
9041
|
-
console.warn('! 停止旧服务失败:', e.message || e);
|
|
9042
|
-
}
|
|
9043
|
-
await new Promise((resolve) => setTimeout(resolve, 80));
|
|
9044
|
-
try {
|
|
9045
|
-
serverHandle = createWebServer({
|
|
10322
|
+
serverHandle = await restartWebUiServerAfterFrontendChange({
|
|
10323
|
+
serverHandle,
|
|
10324
|
+
serverOptions: {
|
|
9046
10325
|
htmlPath,
|
|
9047
10326
|
assetsDir,
|
|
9048
10327
|
webDir,
|
|
9049
10328
|
host,
|
|
9050
10329
|
port,
|
|
9051
10330
|
openBrowser: false
|
|
9052
|
-
}
|
|
9053
|
-
|
|
9054
|
-
|
|
9055
|
-
|
|
9056
|
-
|
|
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
|
+
});
|
|
9057
10341
|
}
|
|
9058
10342
|
);
|
|
9059
10343
|
|
|
@@ -10379,7 +11663,7 @@ function createWorkflowToolCatalog() {
|
|
|
10379
11663
|
}
|
|
10380
11664
|
return {
|
|
10381
11665
|
source: source || 'all',
|
|
10382
|
-
sessions:
|
|
11666
|
+
sessions: await listAllSessionsData({
|
|
10383
11667
|
...args,
|
|
10384
11668
|
source: source || 'all'
|
|
10385
11669
|
})
|
|
@@ -10746,7 +12030,7 @@ function createMcpTools(options = {}) {
|
|
|
10746
12030
|
source: source || 'all'
|
|
10747
12031
|
};
|
|
10748
12032
|
return {
|
|
10749
|
-
sessions:
|
|
12033
|
+
sessions: await listAllSessionsData(normalizedInput),
|
|
10750
12034
|
source: source || 'all'
|
|
10751
12035
|
};
|
|
10752
12036
|
}
|
|
@@ -10982,18 +12266,34 @@ function createMcpTools(options = {}) {
|
|
|
10982
12266
|
handler: async (args = {}) => applyOpenclawConfig(args || {})
|
|
10983
12267
|
});
|
|
10984
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
|
+
|
|
10985
12286
|
pushTool({
|
|
10986
12287
|
name: 'codexmate.session.delete',
|
|
10987
|
-
description: '
|
|
12288
|
+
description: 'Permanently delete one entire session file.',
|
|
10988
12289
|
readOnly: false,
|
|
10989
12290
|
inputSchema: {
|
|
10990
12291
|
type: 'object',
|
|
10991
12292
|
properties: {
|
|
10992
12293
|
source: { type: 'string' },
|
|
10993
12294
|
sessionId: { type: 'string' },
|
|
10994
|
-
|
|
10995
|
-
|
|
10996
|
-
recordLineIndices: { type: 'array', items: { type: 'number' } }
|
|
12295
|
+
filePath: { type: 'string' },
|
|
12296
|
+
file: { type: 'string' }
|
|
10997
12297
|
},
|
|
10998
12298
|
additionalProperties: true
|
|
10999
12299
|
},
|
|
@@ -11143,7 +12443,7 @@ function createMcpResources() {
|
|
|
11143
12443
|
}
|
|
11144
12444
|
const payload = {
|
|
11145
12445
|
source: normalizedSource || 'all',
|
|
11146
|
-
sessions:
|
|
12446
|
+
sessions: await listAllSessionsData({
|
|
11147
12447
|
source: normalizedSource || 'all',
|
|
11148
12448
|
query,
|
|
11149
12449
|
pathFilter,
|