zkn пре 2 дана
родитељ
комит
68aacd586c

+ 2 - 2
.gitignore

@@ -52,5 +52,5 @@ shudao-go-backend/views/index.html
 .roo
 
 .npm-cache
-shudao-main/docs/
-shudao-main/shudao-vue-frontend/.playwright-cli/
+docs/
+shudao-vue-frontend/.playwright-cli/

+ 61 - 0
shudao-chat-py/tests/test_report_compat_proxy_body.py

@@ -0,0 +1,61 @@
+import json
+import importlib.util
+import unittest
+from pathlib import Path
+from types import SimpleNamespace
+
+from models.report import ReportCompleteFlowRequest
+
+
+REPORT_COMPAT_PATH = Path(__file__).resolve().parents[1] / "routers" / "report_compat.py"
+spec = importlib.util.spec_from_file_location("report_compat_under_test", REPORT_COMPAT_PATH)
+report_compat = importlib.util.module_from_spec(spec)
+spec.loader.exec_module(report_compat)
+
+_build_aichat_complete_flow_body = report_compat._build_aichat_complete_flow_body
+
+
+def _fake_request(user_id: int = 70430):
+    return SimpleNamespace(
+        state=SimpleNamespace(
+            user=SimpleNamespace(user_id=user_id)
+        )
+    )
+
+
+class ReportCompatProxyBodyTest(unittest.TestCase):
+    def test_new_conversation_zero_is_forwarded_to_aichat(self):
+        request_data = ReportCompleteFlowRequest(
+            user_question="专业问题",
+            ai_conversation_id=0,
+        )
+
+        body = _build_aichat_complete_flow_body(request_data, _fake_request())
+        payload = json.loads(body.decode("utf-8"))
+
+        self.assertEqual(payload["ai_conversation_id"], 0)
+
+    def test_missing_conversation_id_is_forwarded_as_zero(self):
+        request_data = ReportCompleteFlowRequest(
+            user_question="专业问题",
+        )
+
+        body = _build_aichat_complete_flow_body(request_data, _fake_request())
+        payload = json.loads(body.decode("utf-8"))
+
+        self.assertEqual(payload["ai_conversation_id"], 0)
+
+    def test_existing_conversation_id_is_preserved(self):
+        request_data = ReportCompleteFlowRequest(
+            user_question="继续追问",
+            ai_conversation_id=11226,
+        )
+
+        body = _build_aichat_complete_flow_body(request_data, _fake_request())
+        payload = json.loads(body.decode("utf-8"))
+
+        self.assertEqual(payload["ai_conversation_id"], 11226)
+
+
+if __name__ == "__main__":
+    unittest.main()

+ 54 - 0
shudao-vue-frontend/src/utils/chatHistoryPersistence.js

@@ -0,0 +1,54 @@
+export const hydratePersistedReports = (reports) => {
+  if (!Array.isArray(reports)) {
+    return []
+  }
+
+  return reports.map((report) => {
+    if (!report || report.type === 'category_title') {
+      return report
+    }
+
+    if (!report._fullContent) {
+      return report
+    }
+
+    return {
+      ...report,
+      report: {
+        display_name: report._fullContent.display_name || report.report?.display_name || '',
+        summary: report._fullContent.summary || report.report?.summary || '',
+        analysis: report._fullContent.analysis || report.report?.analysis || '',
+        clauses: report._fullContent.clauses || report.report?.clauses || ''
+      }
+    }
+  })
+}
+
+export const normalizeReportsForPersistence = (reports) => {
+  return hydratePersistedReports(reports)
+}
+
+export const buildPersistedAIMessageContent = (message) => {
+  if (!message) {
+    return ''
+  }
+
+  const reports = normalizeReportsForPersistence(message.reports || [])
+  const webSearchRaw = message.webSearchRaw || null
+  const webSearchSummary = message._fullWebSearchSummary || message.webSearchSummary || null
+  const summary = message.summary || message._fullSummary || ''
+  const directContent = message.content || ''
+  const hasStructuredPayload = reports.length > 0 || webSearchRaw || webSearchSummary
+
+  if (hasStructuredPayload || (!directContent && summary)) {
+    return JSON.stringify({
+      reports,
+      webSearchRaw,
+      webSearchSummary,
+      hasWebSearchResults: message.hasWebSearchResults || false,
+      summary
+    })
+  }
+
+  return directContent || summary
+}

+ 112 - 0
shudao-vue-frontend/src/utils/chatHistoryPersistence.test.js

@@ -0,0 +1,112 @@
+import { describe, expect, it } from 'vitest'
+
+import {
+  buildPersistedAIMessageContent,
+  hydratePersistedReports,
+  normalizeReportsForPersistence
+} from './chatHistoryPersistence'
+
+describe('chatHistoryPersistence', () => {
+  it('fills report fields from _fullContent before persistence', () => {
+    const reports = [
+      {
+        file_index: 1,
+        status: 'completed',
+        report: {
+          display_name: '',
+          summary: '',
+          analysis: '',
+          clauses: ''
+        },
+        _fullContent: {
+          display_name: '桥梁施工规范.pdf',
+          summary: '完整摘要',
+          analysis: '完整分析',
+          clauses: '完整条款'
+        }
+      }
+    ]
+
+    expect(normalizeReportsForPersistence(reports)).toEqual([
+      expect.objectContaining({
+        report: {
+          display_name: '桥梁施工规范.pdf',
+          summary: '完整摘要',
+          analysis: '完整分析',
+          clauses: '完整条款'
+        }
+      })
+    ])
+  })
+
+  it('repairs persisted reports when history reloads from older incomplete content', () => {
+    const reports = [
+      {
+        file_index: 2,
+        status: 'completed',
+        report: {
+          display_name: '',
+          summary: '',
+          analysis: '',
+          clauses: ''
+        },
+        _fullContent: {
+          display_name: '混凝土养护说明.pdf',
+          summary: '已保存的完整摘要',
+          analysis: '已保存的完整分析',
+          clauses: ''
+        }
+      },
+      {
+        type: 'category_title',
+        category: '国家规范',
+        number: '一',
+        count: 1
+      }
+    ]
+
+    expect(hydratePersistedReports(reports)).toEqual([
+      expect.objectContaining({
+        report: {
+          display_name: '混凝土养护说明.pdf',
+          summary: '已保存的完整摘要',
+          analysis: '已保存的完整分析',
+          clauses: ''
+        }
+      }),
+      expect.objectContaining({
+        type: 'category_title',
+        category: '国家规范'
+      })
+    ])
+  })
+
+  it('builds structured content for professional replies before the stream finishes', () => {
+    const content = buildPersistedAIMessageContent({
+      reports: [],
+      summary: '',
+      _fullSummary: '已识别到专业问题,正在分析相关规范。',
+      webSearchRaw: null,
+      webSearchSummary: null,
+      hasWebSearchResults: false,
+      content: ''
+    })
+
+    expect(JSON.parse(content)).toEqual({
+      reports: [],
+      webSearchRaw: null,
+      webSearchSummary: null,
+      hasWebSearchResults: false,
+      summary: '已识别到专业问题,正在分析相关规范。'
+    })
+  })
+
+  it('returns plain text for direct AI answers without structured report data', () => {
+    expect(buildPersistedAIMessageContent({
+      reports: [],
+      content: '你好,我在。',
+      summary: '',
+      _fullSummary: ''
+    })).toBe('你好,我在。')
+  })
+})