Hydrate terminal drawer with current log history

This commit is contained in:
npc0-hue
2026-09-09 15:02:39 +08:00
parent f88123e185
commit 892c6d843f
2 changed files with 57 additions and 1 deletions
@@ -95,6 +95,9 @@ export function ServerManagementTerminalDrawer({ open, serverId, serverName, onC
const serverTimeRef = useRef<string | undefined>(undefined);
const initialHistoryPendingRef = useRef(false);
const liveSessionRef = useRef<string | null | undefined>(undefined);
const liveStreamsRef = useRef<LogStreamResponse[]>([]);
const hydratedLiveStreamKeysRef = useRef<Set<string>>(new Set());
const liveHistoryRequestRef = useRef(0);
const historyRequestRef = useRef(0);
const appendLines = useCallback((incoming: TerminalLine[]) => {
@@ -128,6 +131,9 @@ export function ServerManagementTerminalDrawer({ open, serverId, serverName, onC
setHistoryLines({ status: "idle" });
setSelectedHistoryStreamId("");
liveSessionRef.current = undefined;
liveStreamsRef.current = [];
hydratedLiveStreamKeysRef.current = new Set();
liveHistoryRequestRef.current += 1;
serverTimeRef.current = undefined;
historyRequestRef.current += 1;
initialHistoryPendingRef.current = true;
@@ -145,6 +151,29 @@ export function ServerManagementTerminalDrawer({ open, serverId, serverName, onC
return () => window.cancelAnimationFrame(frame);
}, [lines, open]);
const hydrateCurrentSessionHistory = useCallback((sessionId: string | null | undefined) => {
if (!sessionId) return;
const streamsToHydrate = liveStreamsRef.current.filter((stream) => eventBelongsToLiveSession(stream.logSessionId, sessionId) && stream.latestSeq > 0 && !hydratedLiveStreamKeysRef.current.has(liveHistoryStreamKey(sessionId, stream)));
if (streamsToHydrate.length === 0) return;
const requestId = liveHistoryRequestRef.current + 1;
liveHistoryRequestRef.current = requestId;
for (const stream of streamsToHydrate) hydratedLiveStreamKeysRef.current.add(liveHistoryStreamKey(sessionId, stream));
void Promise.all(streamsToHydrate.map(async (stream) => {
const response = await platformApiClient.queryLogStream({ logStreamId: stream.id, afterSeq: Math.max(0, stream.latestSeq - terminalHistoryWindow), limit: terminalHistoryWindow });
return response.entries.map((entry) => terminalLineFromLog(stream, entry));
})).then((lineGroups) => {
if (liveHistoryRequestRef.current !== requestId || liveSessionRef.current !== sessionId) return;
const historicalLines = lineGroups.flat();
if (historicalLines.length === 0) return;
appendLines(historicalLines);
lockTerminalFollow();
}).catch(() => {
if (liveHistoryRequestRef.current !== requestId || liveSessionRef.current !== sessionId) return;
appendLines([terminalSystemLine("warn", "当前会话历史读取失败,继续等待实时输出。", "SYSTEM", `session-history-failed-${sessionId}`, serverTimeRef.current)]);
lockTerminalFollow();
});
}, [appendLines, lockTerminalFollow]);
useEffect(() => {
if (!open) return undefined;
let ready = false;
@@ -159,6 +188,9 @@ export function ServerManagementTerminalDrawer({ open, serverId, serverName, onC
liveSessionRef.current = nextSessionId;
setLiveSessionId(nextSessionId);
if (previousSessionId === nextSessionId) return;
liveStreamsRef.current = [];
hydratedLiveStreamKeysRef.current = new Set();
liveHistoryRequestRef.current += 1;
setStreams({ status: "ready", data: [] });
setLines(nextSessionId
? [terminalSystemLine("info", previousSessionId === undefined ? "已跟随当前受管进程输出会话。" : "Run 已切换到新的受管进程输出会话。", "SYSTEM", `session-${nextSessionId}`, serverTimeRef.current)]
@@ -169,11 +201,13 @@ export function ServerManagementTerminalDrawer({ open, serverId, serverName, onC
const stream = parseLogStreamEvent(event);
if (!stream || !eventBelongsToLiveSession(stream.logSessionId, liveSessionRef.current)) return;
ready = true;
liveStreamsRef.current = mergeLogStreams(liveStreamsRef.current, stream);
setStreams((current) => ({ status: "ready", data: mergeLogStreams(current.status === "ready" ? current.data : [], stream) }));
});
events.addEventListener("ready", () => {
ready = true;
setStreams((current) => current.status === "ready" ? current : { status: "ready", data: [] });
hydrateCurrentSessionHistory(liveSessionRef.current);
lockTerminalFollow();
});
events.addEventListener("log", (event) => {
@@ -181,6 +215,7 @@ export function ServerManagementTerminalDrawer({ open, serverId, serverName, onC
if (!payload || !eventBelongsToLiveSession(payload.logSessionId, liveSessionRef.current)) return;
ready = true;
const stream = streamFromServerLogEvent(payload);
liveStreamsRef.current = mergeLogStreams(liveStreamsRef.current, stream);
setStreams((current) => ({ status: "ready", data: mergeLogStreams(current.status === "ready" ? current.data : [], stream) }));
appendLines([terminalLineFromLog(stream, payload.entry)]);
});
@@ -188,7 +223,7 @@ export function ServerManagementTerminalDrawer({ open, serverId, serverName, onC
if (!ready) setStreams({ status: "error", reason: "实时日志推送连接失败" });
};
return () => events.close();
}, [appendLines, lockTerminalFollow, open, serverId]);
}, [appendLines, hydrateCurrentSessionHistory, lockTerminalFollow, open, serverId]);
useEffect(() => {
if (!open || !historyOpen) return;
@@ -381,6 +416,10 @@ function eventBelongsToLiveSession(eventSessionId: string | undefined, liveSessi
return Boolean(normalizedEventSessionId && normalizedEventSessionId === liveSessionId);
}
function liveHistoryStreamKey(sessionId: string, stream: LogStreamResponse): string {
return `${sessionId}:${stream.id}`;
}
function mergeTerminalLines(current: TerminalLine[], incoming: TerminalLine[]): TerminalLine[] {
const seen = new Set(current.map((line) => line.id));
const merged = [...current];