Fix session file and elapsed replay state
This commit is contained in:
@@ -16,15 +16,24 @@ export async function GET(req: Request) {
|
||||
|
||||
const url = new URL(req.url);
|
||||
const requestedPath = url.searchParams.get("path");
|
||||
const sessionId = url.searchParams.get("session_id");
|
||||
const sessionId = normalizeSessionId(url.searchParams.get("session_id"));
|
||||
if (!requestedPath && sessionId) {
|
||||
return Response.json({
|
||||
session_id: sessionId,
|
||||
input: await listSessionFiles(account.id, sessionId, "input"),
|
||||
output: await listSessionFiles(account.id, sessionId, "output"),
|
||||
});
|
||||
}
|
||||
if (!requestedPath) {
|
||||
return Response.json({ error: "缺少文件路径或会话 ID" }, { status: 400 });
|
||||
const latestSessionId = await findLatestSessionId(account.id);
|
||||
if (!latestSessionId) {
|
||||
return Response.json({ session_id: null, input: [], output: [] });
|
||||
}
|
||||
return Response.json({
|
||||
session_id: latestSessionId,
|
||||
input: await listSessionFiles(account.id, latestSessionId, "input"),
|
||||
output: await listSessionFiles(account.id, latestSessionId, "output"),
|
||||
});
|
||||
}
|
||||
|
||||
const accountRoot = path.resolve(accountBaseRoot(account.id));
|
||||
@@ -87,6 +96,37 @@ async function listSessionFiles(
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeSessionId(value: string | null) {
|
||||
const trimmed = value?.trim();
|
||||
return trimmed || null;
|
||||
}
|
||||
|
||||
async function findLatestSessionId(accountId: string) {
|
||||
const sessionsRoot = path.join(accountBaseRoot(accountId), "sessions");
|
||||
try {
|
||||
const entries = await readdir(sessionsRoot, { withFileTypes: true });
|
||||
const candidates = await Promise.all(
|
||||
entries
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.map(async (entry) => {
|
||||
const sessionDir = path.join(sessionsRoot, entry.name);
|
||||
const sessionFile = path.join(sessionDir, "session.json");
|
||||
try {
|
||||
const sessionStat = await stat(sessionFile);
|
||||
return { id: entry.name, modifiedAt: sessionStat.mtimeMs };
|
||||
} catch {
|
||||
const dirStat = await stat(sessionDir);
|
||||
return { id: entry.name, modifiedAt: dirStat.mtimeMs };
|
||||
}
|
||||
}),
|
||||
);
|
||||
candidates.sort((a, b) => b.modifiedAt - a.modifiedAt);
|
||||
return candidates.at(0)?.id ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function contentTypeFor(filePath: string) {
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
if (ext === ".json") return "application/json; charset=utf-8";
|
||||
|
||||
@@ -222,6 +222,7 @@ type SessionFile = {
|
||||
};
|
||||
|
||||
type SessionFilesPayload = {
|
||||
session_id?: string | null;
|
||||
input?: SessionFile[];
|
||||
output?: SessionFile[];
|
||||
error?: string;
|
||||
@@ -243,24 +244,37 @@ function SessionFilesPanel({
|
||||
const activeSessionId = normalizeSessionId(
|
||||
sessionId ?? window.localStorage.getItem("claw.activeSessionId"),
|
||||
);
|
||||
if (!activeSessionId) {
|
||||
setPayload({ input: [], output: [] });
|
||||
setStatus("当前还没有可用会话。");
|
||||
return;
|
||||
}
|
||||
window.localStorage.setItem("claw.activeSessionId", activeSessionId);
|
||||
setStatus("正在读取聊天中的文件...");
|
||||
try {
|
||||
const url = new URL("/api/claw/files", window.location.origin);
|
||||
url.searchParams.set("session_id", activeSessionId);
|
||||
const response = await fetch(url, { cache: "no-store" });
|
||||
const nextPayload = (await response.json()) as SessionFilesPayload;
|
||||
let { response, payload: nextPayload } =
|
||||
await fetchSessionFiles(activeSessionId);
|
||||
if (
|
||||
activeSessionId &&
|
||||
response.ok &&
|
||||
sessionFilesCount(nextPayload) === 0
|
||||
) {
|
||||
const latest = await fetchSessionFiles(null);
|
||||
if (
|
||||
latest.response.ok &&
|
||||
latest.payload.session_id &&
|
||||
latest.payload.session_id !== activeSessionId
|
||||
) {
|
||||
response = latest.response;
|
||||
nextPayload = latest.payload;
|
||||
}
|
||||
}
|
||||
if (cancelled) return;
|
||||
if (!response.ok) {
|
||||
setPayload({ input: [], output: [], error: nextPayload.error });
|
||||
setStatus(nextPayload.error ?? "读取文件失败");
|
||||
return;
|
||||
}
|
||||
if (nextPayload.session_id) {
|
||||
window.localStorage.setItem(
|
||||
"claw.activeSessionId",
|
||||
nextPayload.session_id,
|
||||
);
|
||||
}
|
||||
setPayload(nextPayload);
|
||||
setStatus("");
|
||||
} catch (err) {
|
||||
@@ -322,6 +336,18 @@ function SessionFilesPanel({
|
||||
);
|
||||
}
|
||||
|
||||
async function fetchSessionFiles(sessionId: string | null) {
|
||||
const url = new URL("/api/claw/files", window.location.origin);
|
||||
if (sessionId) url.searchParams.set("session_id", sessionId);
|
||||
const response = await fetch(url, { cache: "no-store" });
|
||||
const payload = (await response.json()) as SessionFilesPayload;
|
||||
return { response, payload };
|
||||
}
|
||||
|
||||
function sessionFilesCount(payload: SessionFilesPayload) {
|
||||
return (payload.input?.length ?? 0) + (payload.output?.length ?? 0);
|
||||
}
|
||||
|
||||
function normalizeSessionId(value: unknown) {
|
||||
if (typeof value !== "string") return null;
|
||||
const trimmed = value.trim();
|
||||
|
||||
@@ -414,6 +414,10 @@ export function toReplayRepository(
|
||||
message.role !== "assistant" ||
|
||||
!toolParts.length ||
|
||||
index === lastAssistantContentIndex;
|
||||
const elapsedMs =
|
||||
message.role === "assistant"
|
||||
? resolveReplayElapsedMs(replayMessages, index)
|
||||
: undefined;
|
||||
const parts = (
|
||||
message.role === "assistant" && toolParts.length
|
||||
? shouldShowAssistantText
|
||||
@@ -429,7 +433,7 @@ export function toReplayRepository(
|
||||
? {
|
||||
metadata: {
|
||||
sessionId: session.session_id ?? fallbackSessionId,
|
||||
elapsedMs: normalizeElapsedMs(message.metadata?.elapsed_ms),
|
||||
elapsedMs,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
@@ -449,7 +453,7 @@ export function toReplayRepository(
|
||||
steps: [],
|
||||
custom: {
|
||||
sessionId: session.session_id ?? fallbackSessionId,
|
||||
elapsedMs: normalizeElapsedMs(message.metadata?.elapsed_ms),
|
||||
elapsedMs,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -708,6 +712,22 @@ function normalizeElapsedMs(value: unknown) {
|
||||
return Math.max(0, Math.round(value));
|
||||
}
|
||||
|
||||
function resolveReplayElapsedMs(
|
||||
messages: readonly ClawStoredMessage[],
|
||||
index: number,
|
||||
) {
|
||||
const current = normalizeElapsedMs(messages[index]?.metadata?.elapsed_ms);
|
||||
if (current !== undefined) return current;
|
||||
for (let i = index + 1; i < messages.length; i += 1) {
|
||||
const message = messages[i];
|
||||
if (message?.role === "user") return undefined;
|
||||
if (message?.role !== "assistant") continue;
|
||||
const elapsedMs = normalizeElapsedMs(message.metadata?.elapsed_ms);
|
||||
if (elapsedMs !== undefined) return elapsedMs;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function findLastAssistantContentIndex(messages: readonly ClawStoredMessage[]) {
|
||||
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
||||
const message = messages[index];
|
||||
|
||||
@@ -1320,7 +1320,7 @@ const ActivityChainSummary: FC<{
|
||||
? Math.max(0, Math.round(value))
|
||||
: null;
|
||||
});
|
||||
const [elapsedMs, setElapsedMs] = useState(storedElapsedMs ?? 0);
|
||||
const [elapsedMs, setElapsedMs] = useState<number | null>(storedElapsedMs);
|
||||
const label = useAuiState((s) => {
|
||||
const message = s.message;
|
||||
const status =
|
||||
@@ -1338,7 +1338,7 @@ const ActivityChainSummary: FC<{
|
||||
|
||||
useEffect(() => {
|
||||
if (!active) {
|
||||
if (storedElapsedMs !== null) setElapsedMs(storedElapsedMs);
|
||||
setElapsedMs(storedElapsedMs);
|
||||
return;
|
||||
}
|
||||
const startedAt =
|
||||
@@ -1392,7 +1392,7 @@ function findNumberInActivityParts(
|
||||
function summarizeActivityChainLabel(
|
||||
content: readonly ActivitySummaryPart[],
|
||||
status: string | undefined,
|
||||
elapsedMs = 0,
|
||||
elapsedMs: number | null = null,
|
||||
) {
|
||||
const toolParts = content.filter((part) => part.type === "tool-call");
|
||||
const runningTool = toolParts.findLast((part) => part.result === undefined);
|
||||
@@ -1412,7 +1412,7 @@ function summarizeActivityChainLabel(
|
||||
(isGenericActivityLine(lastReasoningLine) ? "" : lastReasoningLine);
|
||||
|
||||
if (status === "running") {
|
||||
const elapsed = `用时 ${formatActivityDuration(elapsedMs)}`;
|
||||
const elapsed = `用时 ${formatActivityDuration(elapsedMs ?? 0)}`;
|
||||
if (activeDetail) {
|
||||
return `${compactActivityLabel(activeDetail, 15)},${elapsed}`;
|
||||
}
|
||||
@@ -1424,8 +1424,10 @@ function summarizeActivityChainLabel(
|
||||
15,
|
||||
);
|
||||
const finalDuration = extractDurationLabel(lastReasoningLine);
|
||||
const elapsed = finalDuration || formatActivityDuration(elapsedMs);
|
||||
return `${label},用时 ${elapsed}`;
|
||||
const elapsed =
|
||||
finalDuration ||
|
||||
(elapsedMs === null ? "" : formatActivityDuration(elapsedMs));
|
||||
return elapsed ? `${label},用时 ${elapsed}` : label;
|
||||
}
|
||||
|
||||
function getToolStageNote(value: unknown) {
|
||||
|
||||
Reference in New Issue
Block a user