diff --git a/frontend/src/run/AssistantPanel.tsx b/frontend/src/run/AssistantPanel.tsx index c61a377e..da5b94a7 100644 --- a/frontend/src/run/AssistantPanel.tsx +++ b/frontend/src/run/AssistantPanel.tsx @@ -16,7 +16,7 @@ import { formatCost, formatLapse, formatOffset, formatSpan, formatTokens, shortI import { statusLabel } from '../lib/status' import { CHAT_BLUEPRINT_TEXT, runClassLabel, SQL_CHECK_TEXT } from '../lib/terms' import { sortSqlChecks } from '../lib/sqlcheck' -import type { RunUsage, ToolChange } from '../types' +import type { ConversationTurn, RunUsage, ToolChange } from '../types' import { AssistantStream, type StreamTurn } from './AssistantStream' import { Composer } from './Composer' import { SqlCheckBody } from './SqlChecks' @@ -220,6 +220,21 @@ function copilotTurn( } } +/** + * 之前那几轮的报错:落库时记了原因(meta.cause,助手本轮未完成的那种)就连「怎么办」和诊断一起摊开, + * 和这一轮当场看到的一样;老轮次只有一句话 + */ +const pastError = (t: ConversationTurn): StreamTurn['error'] => { + const cause = t.meta?.cause + if (!t.error) return undefined + if (!cause || typeof cause !== 'object' || cause.kind !== 'copilot') return t.error + return { + error: t.error, + ...(typeof cause.hint === 'string' && cause.hint ? { hint: cause.hint } : {}), + ...(typeof cause.detail === 'string' && cause.detail ? { detail: cause.detail } : {}), + } +} + /** 这一轮变少了的工具,分「没让删」和「按要求」两组:和改图回执(toast)同一条规则 */ const toolLossOf = (out: CopilotOutcome) => splitShrunk(out.toolChanges, out.dropped.map((w) => ({ node_id: w.nodeId, field: w.field }))) @@ -254,7 +269,7 @@ function ChatView({ hasRun, onOpenRun }: { hasRun: boolean; onOpenRun: () => voi status: t.status === 'error' ? '本轮未能完成' : t.graph ? '已修改画布' : '已回答', steps: [], output: t.answer ? { 回答: t.answer } : t.explanation ? { 说明: t.explanation } : null, - error: t.status === 'error' ? t.error || undefined : undefined, + error: t.status === 'error' ? pastError(t) : undefined, })), [earlier]) const shown = showPast ? [...pastTurns, ...turns] : turns const streamRef = useRef(null) diff --git a/frontend/src/store/studio.ts b/frontend/src/store/studio.ts index 78350ec4..ec90d9d3 100644 --- a/frontend/src/store/studio.ts +++ b/frontend/src/store/studio.ts @@ -484,13 +484,20 @@ const memoryOf = (past: ConversationTurn[]): StudioState['copilotMemory'] => ({ past, total: past.length, turns: Math.min(past.length, COPILOT_HISTORY_TURNS), }) +/** + * 画布这一轮没完成时记下的原因:同问数据一轮的 meta.cause(backend/app/chat/record.py 的 kind=copilot)。 + * 以前只记 error 那一句,诊断(模型交回了什么)没落库,事后查不了为什么失败 + */ +type CanvasCause = { kind: 'copilot'; message: string; hint?: string; detail?: string; code?: string + diagnostics?: Record } + /** 把画布上这一轮记进会话,下一条指令才接得上 */ function recordCanvasTurn( conversationId: string | null, instruction: string, patch: { graph?: any; explanation?: string; answer?: string; status?: 'done' | 'error'; error?: string - /** 这一轮的查看记录(look §2.5.8):服务端下一轮据此写「上一轮查看过」 */ - meta?: { looks: LookEvent[] } }, + /** 这一轮的查看记录(look §2.5.8):服务端下一轮据此写「上一轮查看过」;没完成的另记原因 */ + meta?: { looks?: LookEvent[]; cause?: CanvasCause } }, set?: (fn: (s: StudioState) => Partial) => void, ): void { if (!conversationId) return @@ -2148,7 +2155,15 @@ export const useStudio = create((set, get) => ({ finish({ explanation: '', error: message, errorHint: typeof op.hint === 'string' ? op.hint : '', errorDetail: typeof op.detail === 'string' ? op.detail : '' }) - recordCanvasTurn(get().copilotConversationId, instruction, { status: 'error', error: message, ...looksMeta() }, set) + const cause: CanvasCause = { + kind: 'copilot', message, + ...(typeof op.hint === 'string' && op.hint ? { hint: op.hint } : {}), + ...(typeof op.detail === 'string' && op.detail ? { detail: op.detail } : {}), + ...(typeof op.code === 'string' && op.code ? { code: op.code } : {}), + ...(op.diagnostics && typeof op.diagnostics === 'object' ? { diagnostics: op.diagnostics } : {}), + } + recordCanvasTurn(get().copilotConversationId, instruction, + { status: 'error', error: message, meta: { ...looksMeta().meta, cause } }, set) break } }