修复历史消息回看并重新打包
This commit is contained in:
122
server.js
122
server.js
@@ -4119,6 +4119,90 @@ function handleDirectoryPickerListApi(req, res, url) {
|
||||
|
||||
const INITIAL_HISTORY_COUNT = 12;
|
||||
const HISTORY_CHUNK_SIZE = 24;
|
||||
const SESSION_HISTORY_CACHE_LIMIT = 12;
|
||||
const sessionHistoryCache = new Map();
|
||||
|
||||
function isPersistenceNoticeMessage(message) {
|
||||
return !!(message && typeof message === 'object' && message.ccwebPersistenceNotice === true);
|
||||
}
|
||||
|
||||
function sessionHistoryCacheKey(session) {
|
||||
if (!session) return '';
|
||||
if (isCodexLikeSession(session)) return `${getSessionAgent(session)}:${session.codexAppThreadId || session.codexThreadId || ''}`;
|
||||
return `claude:${session.claudeSessionId || ''}`;
|
||||
}
|
||||
|
||||
function cacheSessionHistory(key, value) {
|
||||
if (!key || !value?.filePath || !Array.isArray(value.messages)) return value;
|
||||
sessionHistoryCache.delete(key);
|
||||
sessionHistoryCache.set(key, value);
|
||||
while (sessionHistoryCache.size > SESSION_HISTORY_CACHE_LIMIT) {
|
||||
const oldestKey = sessionHistoryCache.keys().next().value;
|
||||
if (!oldestKey) break;
|
||||
sessionHistoryCache.delete(oldestKey);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function loadNativeSessionHistory(session) {
|
||||
const key = sessionHistoryCacheKey(session);
|
||||
if (!key || (!isCodexLikeSession(session) && !session.claudeSessionId)) return null;
|
||||
|
||||
let filePath = null;
|
||||
if (isCodexLikeSession(session)) {
|
||||
const threadId = session.codexAppThreadId || session.codexThreadId || '';
|
||||
if (!threadId) return null;
|
||||
const candidates = getCodexRolloutFiles().filter((candidate) => (
|
||||
path.basename(candidate).includes(threadId)
|
||||
));
|
||||
filePath = candidates[0] || null;
|
||||
} else {
|
||||
filePath = resolveClaudeSessionLocalMeta(session.claudeSessionId)?.filePath || null;
|
||||
}
|
||||
if (!filePath || !fs.existsSync(filePath)) return null;
|
||||
|
||||
let stat;
|
||||
try {
|
||||
stat = fs.statSync(filePath);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
const cached = sessionHistoryCache.get(key);
|
||||
if (cached && cached.filePath === filePath && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = isCodexLikeSession(session)
|
||||
? parseCodexRolloutFile(filePath)
|
||||
: { messages: parseJsonlToMessages(fs.readFileSync(filePath, 'utf8').split('\n')) };
|
||||
const messages = Array.isArray(parsed?.messages) ? parsed.messages : [];
|
||||
if (messages.length === 0) return null;
|
||||
return cacheSessionHistory(key, { filePath, mtimeMs: stat.mtimeMs, size: stat.size, messages });
|
||||
} catch (error) {
|
||||
plog('WARN', 'session_native_history_load_failed', {
|
||||
sessionId: String(session.id || '').slice(0, 8),
|
||||
agent: getSessionAgent(session),
|
||||
error: error?.message || String(error || ''),
|
||||
});
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveSessionHistory(session) {
|
||||
const persisted = Array.isArray(session?.messages) ? session.messages : [];
|
||||
const persistedMessages = persisted.filter((message) => !isPersistenceNoticeMessage(message));
|
||||
const notice = persisted.some(isPersistenceNoticeMessage);
|
||||
if (!notice) {
|
||||
return { messages: persisted, source: 'snapshot', available: true, recoverable: false };
|
||||
}
|
||||
|
||||
const native = loadNativeSessionHistory(session);
|
||||
if (native?.messages?.length > persistedMessages.length) {
|
||||
return { messages: native.messages, source: 'native', available: true, recoverable: true };
|
||||
}
|
||||
return { messages: persisted, source: 'snapshot', available: false, recoverable: false };
|
||||
}
|
||||
|
||||
function normalizeAgent(agent) {
|
||||
return VALID_AGENTS.has(agent) ? agent : 'claude';
|
||||
@@ -4911,7 +4995,7 @@ function sanitizeMessagesForPersist(messages, limits = {}) {
|
||||
if (list.length > selected.length) {
|
||||
output.unshift({
|
||||
role: 'system',
|
||||
content: `历史消息过多,cc-web 已只保留最近 ${selected.length} 条用于本地展示,省略 ${list.length - selected.length} 条旧消息。`,
|
||||
content: `历史消息过多,cc-web 本地快照只保留最近 ${selected.length} 条;点击顶部“查看更早消息”可从原始会话记录加载省略的 ${list.length - selected.length} 条旧消息。`,
|
||||
timestamp: new Date().toISOString(),
|
||||
ccwebPersistenceNotice: true,
|
||||
});
|
||||
@@ -4996,7 +5080,7 @@ function buildSessionJsonForPersist(session) {
|
||||
...session,
|
||||
messages: [{
|
||||
role: 'system',
|
||||
content: '当前会话历史过大,cc-web 已跳过本地历史明细写入,仅保留会话元数据以保护服务稳定性。',
|
||||
content: `当前会话历史过大,cc-web 未将完整历史写入本地快照;如果原始会话记录仍在,可点击顶部“查看更早消息”尝试恢复。`,
|
||||
timestamp: new Date().toISOString(),
|
||||
ccwebPersistenceNotice: true,
|
||||
}],
|
||||
@@ -10589,14 +10673,17 @@ function handleLoadHistoryPage(ws, msg = {}) {
|
||||
message: 'Session not found',
|
||||
}, msg));
|
||||
}
|
||||
const list = sanitizeMessagesForTransport(session.messages);
|
||||
const history = typeof resolveSessionHistory === 'function'
|
||||
? resolveSessionHistory(session)
|
||||
: { messages: session.messages || [], source: 'snapshot', available: true };
|
||||
const list = sanitizeMessagesForTransport(history.messages);
|
||||
const requestedBefore = Number.parseInt(String(msg.before || ''), 10);
|
||||
const before = Number.isFinite(requestedBefore)
|
||||
? Math.max(0, Math.min(list.length, requestedBefore))
|
||||
: Math.max(0, list.length - INITIAL_HISTORY_COUNT);
|
||||
const end = before;
|
||||
const start = Math.max(0, end - HISTORY_CHUNK_SIZE);
|
||||
wsSend(ws, {
|
||||
wsSend(ws, attachClientRequestId({
|
||||
type: 'session_history_chunk',
|
||||
sessionId: session.id,
|
||||
messages: list.slice(start, end),
|
||||
@@ -10604,7 +10691,10 @@ function handleLoadHistoryPage(ws, msg = {}) {
|
||||
historyCursor: start,
|
||||
historyBaseIndex: start,
|
||||
historyTruncated: start > 0,
|
||||
});
|
||||
historyTotal: list.length,
|
||||
historyAvailable: history.available,
|
||||
historySource: history.source,
|
||||
}, msg));
|
||||
}
|
||||
|
||||
function attachActiveRuntimeToWs(ws, sessionId, source = {}) {
|
||||
@@ -10708,16 +10798,19 @@ function handleLoadSession(ws, msg) {
|
||||
const requestedTargetMessageIndex = Number.parseInt(String(
|
||||
typeof msg === 'object' ? msg?.targetMessageIndex ?? '' : '',
|
||||
), 10);
|
||||
const history = typeof resolveSessionHistory === 'function'
|
||||
? resolveSessionHistory(refreshedSession)
|
||||
: { messages: refreshedSession.messages || [], source: 'snapshot', available: true };
|
||||
const targetMessageIndex = Number.isFinite(requestedTargetMessageIndex)
|
||||
? Math.max(0, Math.min(refreshedSession.messages.length - 1, requestedTargetMessageIndex))
|
||||
? Math.max(0, Math.min(history.messages.length - 1, requestedTargetMessageIndex))
|
||||
: null;
|
||||
const recentHistoryBaseIndex = Math.max(0, refreshedSession.messages.length - INITIAL_HISTORY_COUNT);
|
||||
const recentHistoryBaseIndex = Math.max(0, history.messages.length - INITIAL_HISTORY_COUNT);
|
||||
const targetPrefetchChunks = targetMessageIndex !== null && targetMessageIndex < recentHistoryBaseIndex
|
||||
? Math.ceil((recentHistoryBaseIndex - targetMessageIndex) / HISTORY_CHUNK_SIZE)
|
||||
: 0;
|
||||
const { recentMessages, olderChunks, historyRemaining, historyBuffered } = splitHistoryMessages(
|
||||
refreshedSession.messages,
|
||||
{ prefetchChunks: Math.max(HISTORY_PREFETCH_CHUNKS, targetPrefetchChunks) },
|
||||
history.messages,
|
||||
{ prefetchChunks: targetMessageIndex === null ? 0 : Math.max(HISTORY_PREFETCH_CHUNKS, targetPrefetchChunks) },
|
||||
);
|
||||
const effectiveCwd = refreshedSession.cwd || activeProcesses.get(sessionId)?.cwd || activeCodexAppTurns.get(sessionId)?.cwd || null;
|
||||
const waitState = crossConversationWaitState(sessionId);
|
||||
@@ -10750,12 +10843,14 @@ function handleLoadSession(ws, msg) {
|
||||
cwd: effectiveCwd,
|
||||
totalCost: session.totalCost || 0,
|
||||
totalUsage: session.totalUsage || null,
|
||||
historyTotal: refreshedSession.messages.length,
|
||||
historyTotal: history.messages.length,
|
||||
historyBuffered,
|
||||
historyCursor: historyRemaining,
|
||||
historyBaseIndex: Math.max(0, refreshedSession.messages.length - recentMessages.length),
|
||||
historyBaseIndex: Math.max(0, history.messages.length - recentMessages.length),
|
||||
historyTruncated: historyRemaining > 0,
|
||||
historyPending: olderChunks.length > 0,
|
||||
historyAvailable: history.available,
|
||||
historySource: history.source,
|
||||
updated: refreshedSession.updated,
|
||||
isRunning: isSessionRunning(sessionId),
|
||||
waitingOnChildren: waitState.waitingOnChildren,
|
||||
@@ -10769,7 +10864,7 @@ function handleLoadSession(ws, msg) {
|
||||
}, msg));
|
||||
|
||||
if (olderChunks.length > 0) {
|
||||
let chunkEnd = Math.max(0, refreshedSession.messages.length - recentMessages.length);
|
||||
let chunkEnd = Math.max(0, history.messages.length - recentMessages.length);
|
||||
olderChunks.forEach((chunk, index) => {
|
||||
const chunkStart = Math.max(0, chunkEnd - chunk.length);
|
||||
wsSend(ws, attachClientRequestId({
|
||||
@@ -10780,6 +10875,9 @@ function handleLoadSession(ws, msg) {
|
||||
historyCursor: index === olderChunks.length - 1 ? historyRemaining : null,
|
||||
historyBaseIndex: chunkStart,
|
||||
historyTruncated: historyRemaining > 0,
|
||||
historyTotal: history.messages.length,
|
||||
historyAvailable: history.available,
|
||||
historySource: history.source,
|
||||
}, msg));
|
||||
chunkEnd = chunkStart;
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user