修复历史消息回看并重新打包

This commit is contained in:
shiyue
2026-09-11 09:29:53 +08:00
parent 6fff9023b2
commit b8fcc12229
18 changed files with 494 additions and 73 deletions

122
server.js
View File

@@ -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;
});