From 1f91dcc5ccc2529b92186fdcbb6194068315c96e Mon Sep 17 00:00:00 2001 From: iven Date: Tue, 5 May 2026 19:45:36 +0800 Subject: [PATCH] =?UTF-8?q?fix(ai):=20=E4=BF=AE=E5=A4=8D=E5=88=86=E6=9E=90?= =?UTF-8?q?=E7=BB=93=E6=9E=9C=20JSON=20=E5=B5=8C=E5=A5=97=20bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - replay_cached 直接回放纯文本,不再包装 JSON 壳 - complete_analysis 跳过已完成的记录,防止缓存命中时覆写 - 前端 AnalysisContent 增加 extractPlainText 递归解析 JSON --- apps/web/src/pages/health/AiAnalysisList.tsx | 16 +++++++++++++++- crates/erp-ai/src/service/analysis.rs | 17 ++++++++--------- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/apps/web/src/pages/health/AiAnalysisList.tsx b/apps/web/src/pages/health/AiAnalysisList.tsx index f1dc522..f023ef5 100644 --- a/apps/web/src/pages/health/AiAnalysisList.tsx +++ b/apps/web/src/pages/health/AiAnalysisList.tsx @@ -60,9 +60,23 @@ const SUGGESTION_STATUS_CONFIG: Record // 分析结果渲染(Markdown 风格) // --------------------------------------------------------------------------- +/** 递归提取 JSON 嵌套中的实际文本内容 */ +function extractPlainText(raw: string): string { + try { + const parsed = JSON.parse(raw); + if (typeof parsed === 'object' && parsed !== null && typeof parsed.content === 'string') { + return extractPlainText(parsed.content); + } + return raw; + } catch { + return raw; + } +} + function AnalysisContent({ content, isDark }: { content: string; isDark: boolean }) { + const text = extractPlainText(content); // 简单的 Markdown 风格渲染 - const lines = content.split('\n'); + const lines = text.split('\n'); const rendered = lines.map((line, i) => { // 标题行 if (line.startsWith('### ')) { diff --git a/crates/erp-ai/src/service/analysis.rs b/crates/erp-ai/src/service/analysis.rs index c352f0b..5516c0a 100644 --- a/crates/erp-ai/src/service/analysis.rs +++ b/crates/erp-ai/src/service/analysis.rs @@ -128,20 +128,14 @@ impl AnalysisService { Ok((stream, analysis_id, provider_name)) } - /// 将缓存结果构造为一次性 Stream(模拟 SSE 单条返回) + /// 将缓存结果构造为一次性 Stream(直接回放纯文本,不额外包装 JSON) fn replay_cached( &self, content: String, - metadata: serde_json::Value, + _metadata: serde_json::Value, ) -> Pin> + Send>> { use futures::stream; - let payload = serde_json::json!({ - "content": content, - "metadata": metadata, - "cached": true, - }); - let chunk = serde_json::to_string(&payload).unwrap_or_default(); - Box::pin(stream::once(async move { Ok(chunk) })) + Box::pin(stream::once(async move { Ok(content) })) } /// 更新分析记录为完成 @@ -156,6 +150,11 @@ impl AnalysisService { .await? .ok_or_else(|| AiError::AnalysisNotFound(analysis_id.to_string()))?; + // 缓存回放时记录已是 completed,跳过重复更新 + if entity.status == "completed" { + tracing::debug!(analysis = %analysis_id, "分析已完成,跳过重复 complete"); + return Ok(()); + } let mut active: ai_analysis::ActiveModel = entity.into(); active.status = Set("completed".into()); active.result_content = Set(Some(content));