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));