fix: preserve restored session markdown
This commit is contained in:
@@ -1181,7 +1181,13 @@ test("cloud api repairs conversation snapshot when persisted result advances las
|
||||
const aliceCreate = await postJson(port, "/v1/admin/users", { username: "alice-issue842-snapshot", password: "alice-pass" }, adminLogin.cookie);
|
||||
assert.equal(aliceCreate.status, 201);
|
||||
const aliceLogin = await postJson(port, "/auth/login", { username: "alice-issue842-snapshot", password: "alice-pass" });
|
||||
const finalText = "持久化验收OK。";
|
||||
const finalText = [
|
||||
"持久化验收OK。",
|
||||
"",
|
||||
"| Skill | 状态 |",
|
||||
"|---|---|",
|
||||
"| hwlab-agent-runtime | 可用 |"
|
||||
].join("\n");
|
||||
codeAgentChatResults.set("trc_issue842_persisted_final", {
|
||||
status: "completed",
|
||||
traceId: "trc_issue842_persisted_final",
|
||||
@@ -1221,6 +1227,7 @@ test("cloud api repairs conversation snapshot when persisted result advances las
|
||||
assert.equal(listed.lastTraceId, "trc_issue842_persisted_final");
|
||||
assert.equal(listed.messages.at(-1).traceId, "trc_issue842_persisted_final");
|
||||
assert.equal(listed.messages.at(-1).text, finalText);
|
||||
assert.match(listed.messages.at(-1).text, /\n\|---\|---\|\n/u);
|
||||
assert.equal(listed.messages.at(-1).status, "idle");
|
||||
|
||||
const direct = await getJson(port, "/v1/agent/conversations/cnv_issue842_snapshot", aliceLogin.cookie);
|
||||
@@ -1239,6 +1246,7 @@ test("cloud api repairs conversation snapshot when persisted result advances las
|
||||
assert.equal(selected.status, 200);
|
||||
assert.equal(selected.body.workspace.selectedConversation.messages.at(-1).traceId, "trc_issue842_persisted_final");
|
||||
assert.equal(selected.body.workspace.selectedConversation.messages.at(-1).text, finalText);
|
||||
assert.match(selected.body.workspace.selectedConversation.messages.at(-1).text, /\n\|---\|---\|\n/u);
|
||||
} finally {
|
||||
await new Promise((resolve, reject) => {
|
||||
server.close((error) => (error ? reject(error) : resolve()));
|
||||
|
||||
@@ -2447,9 +2447,15 @@ function conversationNeedsTerminalRepair(conversation, traceId) {
|
||||
const latestAgent = latestAgentConversationMessage(messages);
|
||||
if (!traceMessage) return Boolean(latestAgent && traceId === safeTraceIdLocal(conversation.lastTraceId ?? conversation.session?.lastTraceId ?? conversation.snapshot?.lastTraceId));
|
||||
if (latestAgent?.traceId && latestAgent.traceId !== traceId && traceId === safeTraceIdLocal(conversation.lastTraceId ?? conversation.session?.lastTraceId ?? conversation.snapshot?.lastTraceId)) return true;
|
||||
if (markdownTextLooksFlattened(traceMessage.text ?? traceMessage.content ?? traceMessage.message) && traceId === safeTraceIdLocal(conversation.lastTraceId ?? conversation.session?.lastTraceId ?? conversation.snapshot?.lastTraceId)) return true;
|
||||
const messageStatus = textOr(traceMessage.status, "").toLowerCase();
|
||||
return ["", "running", "busy", "pending", "active"].includes(messageStatus);
|
||||
}
|
||||
function markdownTextLooksFlattened(value) {
|
||||
const text = conversationTextRaw(value).replace(/\r\n?/gu, "\n").trim();
|
||||
if (!text || text.includes("\n")) return false;
|
||||
return /\|\s*:?-{3,}:?\s*\|/u.test(text) || /```[\s\S]+```/u.test(text);
|
||||
}
|
||||
function terminalConversationRepairTraceId(conversation) {
|
||||
if (!conversation) return "";
|
||||
const lastTraceId = safeTraceIdLocal(conversation.lastTraceId ?? conversation.session?.lastTraceId ?? conversation.snapshot?.lastTraceId);
|
||||
@@ -2734,7 +2740,7 @@ function firstConversationText(values = [], maxBytes = 12000) {
|
||||
return "";
|
||||
}
|
||||
function conversationText(value, maxBytes = 12000) {
|
||||
const extracted = conversationTextRaw(value).replace(/\s+/gu, " ").trim();
|
||||
const extracted = conversationTextRaw(value).replace(/\r\n?/gu, "\n").trim();
|
||||
if (!extracted || extracted === "[object Object]") return "";
|
||||
return boundedText(extracted, maxBytes);
|
||||
}
|
||||
|
||||
@@ -1629,7 +1629,7 @@ async function sessionSwitchCommand(context: any) {
|
||||
})
|
||||
});
|
||||
if (responseSucceeded(response) && response.body?.workspace) await saveWorkspaceState(context, response.body.workspace);
|
||||
return responsePayload("client.session.switch", response, context, { route: route("POST", pathName), workspace: normalizeWorkbenchWorkspace(response.body?.workspace), current: sessionSummary(response.body?.workspace?.selectedConversation), body: responseBodyForCli(response.body, context.parsed) });
|
||||
return responsePayload("client.session.switch", response, context, { route: route("POST", pathName), workspace: normalizeWorkbenchWorkspace(response.body?.workspace), current: sessionSummary(response.body?.workspace?.selectedConversation), selectedConversation: finalResponseConversationSummary(response.body?.workspace?.selectedConversation), body: responseBodyForCli(response.body, context.parsed) });
|
||||
}
|
||||
|
||||
async function sessionDeleteCommand(context: any) {
|
||||
@@ -3209,7 +3209,8 @@ function finalResponseConversationSummary(conversation: any) {
|
||||
status: text(latest.status),
|
||||
traceId: text(latest.traceId),
|
||||
updatedAt: text(latest.updatedAt),
|
||||
textPreview: preview(latest.text, 240)
|
||||
textPreview: preview(latest.text, 240),
|
||||
markdown: markdownTextSummary(latest.text)
|
||||
}) : undefined
|
||||
});
|
||||
}
|
||||
@@ -3223,7 +3224,25 @@ function finalResponseResultSummary(body: any) {
|
||||
sessionId: text(body?.sessionId),
|
||||
threadId: text(body?.threadId ?? body?.session?.threadId ?? body?.providerTrace?.threadId),
|
||||
assistantTextPreview: preview(resultText, 240),
|
||||
assistantTextChars: resultText.length || undefined
|
||||
assistantTextChars: resultText.length || undefined,
|
||||
markdown: markdownTextSummary(resultText)
|
||||
});
|
||||
}
|
||||
|
||||
function markdownTextSummary(value: unknown) {
|
||||
const source = messageTextValue(value);
|
||||
if (!source) return undefined;
|
||||
const newlineCount = (source.match(/\n/gu) ?? []).length;
|
||||
const hasTableSeparator = /\|\s*:?-{3,}:?\s*\|/u.test(source);
|
||||
const hasFence = /```/u.test(source);
|
||||
const likelyFlattened = newlineCount === 0 && (hasTableSeparator || /```[\s\S]+```/u.test(source));
|
||||
return clean({
|
||||
textChars: source.length,
|
||||
newlineCount,
|
||||
hasTablePipe: /\|/u.test(source) || undefined,
|
||||
hasTableSeparator: hasTableSeparator || undefined,
|
||||
hasFence: hasFence || undefined,
|
||||
likelyFlattened: likelyFlattened || undefined
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3259,6 +3278,9 @@ function finalResponseValidation({ conversationId, projectId, inspectConversatio
|
||||
const inspectText = text(inspectedLatest?.text);
|
||||
const listText = text(listedLatest?.text);
|
||||
const resultText = assistantText(resultBody) || "";
|
||||
const inspectMarkdown = markdownTextSummary(inspectText) ?? {};
|
||||
const listMarkdown = markdownTextSummary(listText) ?? {};
|
||||
const resultMarkdown = markdownTextSummary(resultText) ?? {};
|
||||
const failures = [
|
||||
!inspectConversation ? "inspect-conversation-missing" : "",
|
||||
!listedConversation ? "list-conversation-missing" : "",
|
||||
@@ -3269,7 +3291,9 @@ function finalResponseValidation({ conversationId, projectId, inspectConversatio
|
||||
inspectedLatest && text(inspectedLatest.traceId) !== traceId ? "inspect-latest-agent-trace-mismatch" : "",
|
||||
listedLatest && text(listedLatest.traceId) !== traceId ? "list-latest-agent-trace-mismatch" : "",
|
||||
inspectText && resultText && inspectText !== resultText ? "inspect-text-result-mismatch" : "",
|
||||
listText && resultText && listText !== resultText ? "list-text-result-mismatch" : ""
|
||||
listText && resultText && listText !== resultText ? "list-text-result-mismatch" : "",
|
||||
inspectMarkdown.likelyFlattened === true && resultMarkdown.newlineCount > 0 ? "inspect-markdown-flattened" : "",
|
||||
listMarkdown.likelyFlattened === true && resultMarkdown.newlineCount > 0 ? "list-markdown-flattened" : ""
|
||||
].filter(Boolean);
|
||||
return {
|
||||
ok: failures.length === 0,
|
||||
@@ -3289,7 +3313,10 @@ function finalResponseValidation({ conversationId, projectId, inspectConversatio
|
||||
inspectLatestAgentTraceMatches: inspectedLatest ? text(inspectedLatest.traceId) === traceId : false,
|
||||
listLatestAgentTraceMatches: listedLatest ? text(listedLatest.traceId) === traceId : false,
|
||||
inspectTextMatchesResult: Boolean(inspectText && resultText && inspectText === resultText),
|
||||
listTextMatchesResult: Boolean(listText && resultText && listText === resultText)
|
||||
listTextMatchesResult: Boolean(listText && resultText && listText === resultText),
|
||||
inspectMarkdownNotFlattened: inspectMarkdown.likelyFlattened !== true,
|
||||
listMarkdownNotFlattened: listMarkdown.likelyFlattened !== true,
|
||||
resultMarkdownHasStructure: resultMarkdown.newlineCount > 0 || resultMarkdown.hasTableSeparator === true || resultMarkdown.hasFence === true
|
||||
},
|
||||
repair: clean({
|
||||
inspectSnapshotSource: text(inspectConversation?.snapshot?.source),
|
||||
@@ -3301,6 +3328,11 @@ function finalResponseValidation({ conversationId, projectId, inspectConversatio
|
||||
listText: preview(listText, 200),
|
||||
resultText: preview(resultText, 200)
|
||||
},
|
||||
markdown: {
|
||||
inspect: inspectMarkdown,
|
||||
list: listMarkdown,
|
||||
result: resultMarkdown
|
||||
},
|
||||
failures,
|
||||
nextCommands: [
|
||||
`hwlab-cli client session inspect ${conversationId} --full`,
|
||||
|
||||
@@ -285,7 +285,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
}
|
||||
|
||||
function completeTrace(traceId: string, result: AgentChatResultResponse): void {
|
||||
const text = firstNonEmptyString(result.assistantText, typeof result.reply === "string" ? result.reply : result.reply?.content, result.text, result.summary) ?? "Code Agent 已完成,但没有返回可展示的 final response。";
|
||||
const text = firstNonEmptyString(result.assistantText, finalResponseText(result.finalResponse), typeof result.reply === "string" ? result.reply : result.reply?.content, result.text, result.summary) ?? "Code Agent 已完成,但没有返回可展示的 final response。";
|
||||
messages.value = messages.value.map((message) => {
|
||||
if (message.traceId !== traceId || message.role !== "agent") return message;
|
||||
const runnerTrace = result.runnerTrace ? mergeRunnerTrace(message.runnerTrace, result.runnerTrace) : message.runnerTrace ?? null;
|
||||
@@ -352,14 +352,36 @@ function optimisticWorkspaceSelection(current: WorkspaceRecord, conversation: Co
|
||||
}
|
||||
|
||||
function messagesFromConversation(conversation: ConversationRecord): ChatMessage[] {
|
||||
return (conversation.messages ?? []).map((message) => ({ ...message, id: message.id ?? nextProtocolId("msg"), title: message.title ?? (message.role === "user" ? "用户" : "Code Agent"), createdAt: message.createdAt ?? new Date().toISOString(), status: message.status ?? "source" }));
|
||||
return (conversation.messages ?? []).map(normalizeChatMessage);
|
||||
}
|
||||
|
||||
function messagesFromWorkspace(workspace: WorkspaceRecord | null): ChatMessage[] {
|
||||
const selected = workspace?.selectedConversation?.messages;
|
||||
const embedded = workspace?.workspace?.messages;
|
||||
const source = Array.isArray(selected) ? selected : Array.isArray(embedded) ? embedded : [];
|
||||
return source.map((message) => ({ ...message, id: message.id ?? nextProtocolId("msg"), title: message.title ?? (message.role === "user" ? "用户" : "Code Agent"), createdAt: message.createdAt ?? new Date().toISOString(), status: message.status ?? "source" }));
|
||||
return source.map(normalizeChatMessage);
|
||||
}
|
||||
|
||||
function normalizeChatMessage(message: ChatMessage): ChatMessage {
|
||||
const text = firstNonEmptyString(message.text, messageText((message as Record<string, unknown>).content), messageText((message as Record<string, unknown>).message), finalResponseText((message as Record<string, unknown>).finalResponse)) ?? "";
|
||||
return { ...message, text, id: message.id ?? nextProtocolId("msg"), title: message.title ?? (message.role === "user" ? "用户" : "Code Agent"), createdAt: message.createdAt ?? new Date().toISOString(), status: message.status ?? "source" };
|
||||
}
|
||||
|
||||
function finalResponseText(value: unknown): string | null {
|
||||
if (!value || typeof value !== "object") return null;
|
||||
const record = value as Record<string, unknown>;
|
||||
return firstNonEmptyString(messageText(record.text), messageText(record.content), messageText(record.message));
|
||||
}
|
||||
|
||||
function messageText(value: unknown): string | null {
|
||||
if (typeof value === "string") return value.trim() || null;
|
||||
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
||||
if (Array.isArray(value)) return value.map(messageText).filter((item): item is string => Boolean(item)).join("\n") || null;
|
||||
if (value && typeof value === "object") {
|
||||
const record = value as Record<string, unknown>;
|
||||
return firstNonEmptyString(messageText(record.text), messageText(record.content), messageText(record.message), messageText(record.summary), messageText(record.preview), messageText(record.title));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function liveCall<T>(label: string, call: () => Promise<ApiResult<T>>): Promise<ApiResult<T>> {
|
||||
|
||||
Reference in New Issue
Block a user