fix(ai): 修复分析结果 JSON 嵌套 bug
- replay_cached 直接回放纯文本,不再包装 JSON 壳 - complete_analysis 跳过已完成的记录,防止缓存命中时覆写 - 前端 AnalysisContent 增加 extractPlainText 递归解析 JSON
This commit is contained in:
@@ -60,9 +60,23 @@ const SUGGESTION_STATUS_CONFIG: Record<string, { color: string; text: string }>
|
|||||||
// 分析结果渲染(Markdown 风格)
|
// 分析结果渲染(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 }) {
|
function AnalysisContent({ content, isDark }: { content: string; isDark: boolean }) {
|
||||||
|
const text = extractPlainText(content);
|
||||||
// 简单的 Markdown 风格渲染
|
// 简单的 Markdown 风格渲染
|
||||||
const lines = content.split('\n');
|
const lines = text.split('\n');
|
||||||
const rendered = lines.map((line, i) => {
|
const rendered = lines.map((line, i) => {
|
||||||
// 标题行
|
// 标题行
|
||||||
if (line.startsWith('### ')) {
|
if (line.startsWith('### ')) {
|
||||||
|
|||||||
@@ -128,20 +128,14 @@ impl AnalysisService {
|
|||||||
Ok((stream, analysis_id, provider_name))
|
Ok((stream, analysis_id, provider_name))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 将缓存结果构造为一次性 Stream(模拟 SSE 单条返回)
|
/// 将缓存结果构造为一次性 Stream(直接回放纯文本,不额外包装 JSON)
|
||||||
fn replay_cached(
|
fn replay_cached(
|
||||||
&self,
|
&self,
|
||||||
content: String,
|
content: String,
|
||||||
metadata: serde_json::Value,
|
_metadata: serde_json::Value,
|
||||||
) -> Pin<Box<dyn Stream<Item = AiResult<String>> + Send>> {
|
) -> Pin<Box<dyn Stream<Item = AiResult<String>> + Send>> {
|
||||||
use futures::stream;
|
use futures::stream;
|
||||||
let payload = serde_json::json!({
|
Box::pin(stream::once(async move { Ok(content) }))
|
||||||
"content": content,
|
|
||||||
"metadata": metadata,
|
|
||||||
"cached": true,
|
|
||||||
});
|
|
||||||
let chunk = serde_json::to_string(&payload).unwrap_or_default();
|
|
||||||
Box::pin(stream::once(async move { Ok(chunk) }))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 更新分析记录为完成
|
/// 更新分析记录为完成
|
||||||
@@ -156,6 +150,11 @@ impl AnalysisService {
|
|||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| AiError::AnalysisNotFound(analysis_id.to_string()))?;
|
.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();
|
let mut active: ai_analysis::ActiveModel = entity.into();
|
||||||
active.status = Set("completed".into());
|
active.status = Set("completed".into());
|
||||||
active.result_content = Set(Some(content));
|
active.result_content = Set(Some(content));
|
||||||
|
|||||||
Reference in New Issue
Block a user