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 url = new URL(req.url);
|
||||||
const requestedPath = url.searchParams.get("path");
|
const requestedPath = url.searchParams.get("path");
|
||||||
const sessionId = url.searchParams.get("session_id");
|
const sessionId = normalizeSessionId(url.searchParams.get("session_id"));
|
||||||
if (!requestedPath && sessionId) {
|
if (!requestedPath && sessionId) {
|
||||||
return Response.json({
|
return Response.json({
|
||||||
|
session_id: sessionId,
|
||||||
input: await listSessionFiles(account.id, sessionId, "input"),
|
input: await listSessionFiles(account.id, sessionId, "input"),
|
||||||
output: await listSessionFiles(account.id, sessionId, "output"),
|
output: await listSessionFiles(account.id, sessionId, "output"),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (!requestedPath) {
|
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));
|
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) {
|
function contentTypeFor(filePath: string) {
|
||||||
const ext = path.extname(filePath).toLowerCase();
|
const ext = path.extname(filePath).toLowerCase();
|
||||||
if (ext === ".json") return "application/json; charset=utf-8";
|
if (ext === ".json") return "application/json; charset=utf-8";
|
||||||
|
|||||||
@@ -222,6 +222,7 @@ type SessionFile = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type SessionFilesPayload = {
|
type SessionFilesPayload = {
|
||||||
|
session_id?: string | null;
|
||||||
input?: SessionFile[];
|
input?: SessionFile[];
|
||||||
output?: SessionFile[];
|
output?: SessionFile[];
|
||||||
error?: string;
|
error?: string;
|
||||||
@@ -243,24 +244,37 @@ function SessionFilesPanel({
|
|||||||
const activeSessionId = normalizeSessionId(
|
const activeSessionId = normalizeSessionId(
|
||||||
sessionId ?? window.localStorage.getItem("claw.activeSessionId"),
|
sessionId ?? window.localStorage.getItem("claw.activeSessionId"),
|
||||||
);
|
);
|
||||||
if (!activeSessionId) {
|
|
||||||
setPayload({ input: [], output: [] });
|
|
||||||
setStatus("当前还没有可用会话。");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
window.localStorage.setItem("claw.activeSessionId", activeSessionId);
|
|
||||||
setStatus("正在读取聊天中的文件...");
|
setStatus("正在读取聊天中的文件...");
|
||||||
try {
|
try {
|
||||||
const url = new URL("/api/claw/files", window.location.origin);
|
let { response, payload: nextPayload } =
|
||||||
url.searchParams.set("session_id", activeSessionId);
|
await fetchSessionFiles(activeSessionId);
|
||||||
const response = await fetch(url, { cache: "no-store" });
|
if (
|
||||||
const nextPayload = (await response.json()) as SessionFilesPayload;
|
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 (cancelled) return;
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
setPayload({ input: [], output: [], error: nextPayload.error });
|
setPayload({ input: [], output: [], error: nextPayload.error });
|
||||||
setStatus(nextPayload.error ?? "读取文件失败");
|
setStatus(nextPayload.error ?? "读取文件失败");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (nextPayload.session_id) {
|
||||||
|
window.localStorage.setItem(
|
||||||
|
"claw.activeSessionId",
|
||||||
|
nextPayload.session_id,
|
||||||
|
);
|
||||||
|
}
|
||||||
setPayload(nextPayload);
|
setPayload(nextPayload);
|
||||||
setStatus("");
|
setStatus("");
|
||||||
} catch (err) {
|
} 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) {
|
function normalizeSessionId(value: unknown) {
|
||||||
if (typeof value !== "string") return null;
|
if (typeof value !== "string") return null;
|
||||||
const trimmed = value.trim();
|
const trimmed = value.trim();
|
||||||
|
|||||||
@@ -414,6 +414,10 @@ export function toReplayRepository(
|
|||||||
message.role !== "assistant" ||
|
message.role !== "assistant" ||
|
||||||
!toolParts.length ||
|
!toolParts.length ||
|
||||||
index === lastAssistantContentIndex;
|
index === lastAssistantContentIndex;
|
||||||
|
const elapsedMs =
|
||||||
|
message.role === "assistant"
|
||||||
|
? resolveReplayElapsedMs(replayMessages, index)
|
||||||
|
: undefined;
|
||||||
const parts = (
|
const parts = (
|
||||||
message.role === "assistant" && toolParts.length
|
message.role === "assistant" && toolParts.length
|
||||||
? shouldShowAssistantText
|
? shouldShowAssistantText
|
||||||
@@ -429,7 +433,7 @@ export function toReplayRepository(
|
|||||||
? {
|
? {
|
||||||
metadata: {
|
metadata: {
|
||||||
sessionId: session.session_id ?? fallbackSessionId,
|
sessionId: session.session_id ?? fallbackSessionId,
|
||||||
elapsedMs: normalizeElapsedMs(message.metadata?.elapsed_ms),
|
elapsedMs,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
@@ -449,7 +453,7 @@ export function toReplayRepository(
|
|||||||
steps: [],
|
steps: [],
|
||||||
custom: {
|
custom: {
|
||||||
sessionId: session.session_id ?? fallbackSessionId,
|
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));
|
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[]) {
|
function findLastAssistantContentIndex(messages: readonly ClawStoredMessage[]) {
|
||||||
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
||||||
const message = messages[index];
|
const message = messages[index];
|
||||||
|
|||||||
@@ -1320,7 +1320,7 @@ const ActivityChainSummary: FC<{
|
|||||||
? Math.max(0, Math.round(value))
|
? Math.max(0, Math.round(value))
|
||||||
: null;
|
: null;
|
||||||
});
|
});
|
||||||
const [elapsedMs, setElapsedMs] = useState(storedElapsedMs ?? 0);
|
const [elapsedMs, setElapsedMs] = useState<number | null>(storedElapsedMs);
|
||||||
const label = useAuiState((s) => {
|
const label = useAuiState((s) => {
|
||||||
const message = s.message;
|
const message = s.message;
|
||||||
const status =
|
const status =
|
||||||
@@ -1338,7 +1338,7 @@ const ActivityChainSummary: FC<{
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!active) {
|
if (!active) {
|
||||||
if (storedElapsedMs !== null) setElapsedMs(storedElapsedMs);
|
setElapsedMs(storedElapsedMs);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const startedAt =
|
const startedAt =
|
||||||
@@ -1392,7 +1392,7 @@ function findNumberInActivityParts(
|
|||||||
function summarizeActivityChainLabel(
|
function summarizeActivityChainLabel(
|
||||||
content: readonly ActivitySummaryPart[],
|
content: readonly ActivitySummaryPart[],
|
||||||
status: string | undefined,
|
status: string | undefined,
|
||||||
elapsedMs = 0,
|
elapsedMs: number | null = null,
|
||||||
) {
|
) {
|
||||||
const toolParts = content.filter((part) => part.type === "tool-call");
|
const toolParts = content.filter((part) => part.type === "tool-call");
|
||||||
const runningTool = toolParts.findLast((part) => part.result === undefined);
|
const runningTool = toolParts.findLast((part) => part.result === undefined);
|
||||||
@@ -1412,7 +1412,7 @@ function summarizeActivityChainLabel(
|
|||||||
(isGenericActivityLine(lastReasoningLine) ? "" : lastReasoningLine);
|
(isGenericActivityLine(lastReasoningLine) ? "" : lastReasoningLine);
|
||||||
|
|
||||||
if (status === "running") {
|
if (status === "running") {
|
||||||
const elapsed = `用时 ${formatActivityDuration(elapsedMs)}`;
|
const elapsed = `用时 ${formatActivityDuration(elapsedMs ?? 0)}`;
|
||||||
if (activeDetail) {
|
if (activeDetail) {
|
||||||
return `${compactActivityLabel(activeDetail, 15)},${elapsed}`;
|
return `${compactActivityLabel(activeDetail, 15)},${elapsed}`;
|
||||||
}
|
}
|
||||||
@@ -1424,8 +1424,10 @@ function summarizeActivityChainLabel(
|
|||||||
15,
|
15,
|
||||||
);
|
);
|
||||||
const finalDuration = extractDurationLabel(lastReasoningLine);
|
const finalDuration = extractDurationLabel(lastReasoningLine);
|
||||||
const elapsed = finalDuration || formatActivityDuration(elapsedMs);
|
const elapsed =
|
||||||
return `${label},用时 ${elapsed}`;
|
finalDuration ||
|
||||||
|
(elapsedMs === null ? "" : formatActivityDuration(elapsedMs));
|
||||||
|
return elapsed ? `${label},用时 ${elapsed}` : label;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getToolStageNote(value: unknown) {
|
function getToolStageNote(value: unknown) {
|
||||||
|
|||||||
Reference in New Issue
Block a user