import zipfile from django.test import override_settings from django.urls import reverse from django.core.files.uploadedfile import SimpleUploadedFile from agent_core.results import AgentResult from apps.audit.models import AgentAuditLog from apps.audit.models import NotificationRecord from apps.chat.models import Conversation from apps.chat.services import create_conversation_for_batch from apps.documents.models import ExportedDocument, SubmissionBatch, UploadedDocument def _create_conversation_with_batch(): batch = SubmissionBatch.objects.create( batch_id="SUB-20260604-001", product_name="新型冠状病毒 2019-nCoV 核酸检测试剂盒", workflow_type="registration", conversation_id="conv-001", file_count=2, page_count=12, import_status="completed", ) conversation = Conversation.objects.create( conversation_id="conv-001", title="新型冠状病毒 2019-nCoV 核酸检测试剂盒", product_name=batch.product_name, batch_id=batch.batch_id, task_status="processing", node_results=[ {"label": "资料包导入", "status": "已完成"}, {"label": "目录汇总", "status": "处理中"}, ], ) return batch, conversation def test_chat_post_returns_agent_result_and_audit_log(client, db, monkeypatch): batch, conversation = _create_conversation_with_batch() UploadedDocument.objects.create( batch=batch, scenario_id="document_review", original_name="说明书.md", file_type="md", size=1, status=UploadedDocument.STATUS_INDEXED, ) monkeypatch.setattr( "apps.chat.views.run_agent", lambda *args, **kwargs: AgentResult(answer="模拟回答", status="success"), ) response = client.post( reverse("chat:detail", args=[conversation.conversation_id]), {"message": "如何处理异常?"}, ) assert response.status_code == 200 content = response.content.decode("utf-8") assert "审核智能体" in content assert "模拟回答" in content assert AgentAuditLog.objects.count() == 1 assert AgentAuditLog.objects.get().batch_id == batch.batch_id def test_chat_rejects_empty_message(client, db): _batch, conversation = _create_conversation_with_batch() response = client.post(reverse("chat:detail", args=[conversation.conversation_id]), {"message": ""}) assert response.status_code == 200 assert AgentAuditLog.objects.count() == 0 assert "请输入要咨询的问题" in response.content.decode("utf-8") def test_chat_passes_selected_document_ids_to_agent_core(client, db, monkeypatch): batch, conversation = _create_conversation_with_batch() selected = UploadedDocument.objects.create( batch=batch, scenario_id="document_review", original_name="selected.md", file_type="md", size=1, status=UploadedDocument.STATUS_INDEXED, ) UploadedDocument.objects.create( batch=batch, scenario_id="document_review", original_name="other.md", file_type="md", size=1, status=UploadedDocument.STATUS_INDEXED, ) captured = {} def fake_run_agent(scenario_config, user_input, options=None): captured["options"] = options or {} return AgentResult(answer="ok", status="success") monkeypatch.setattr("apps.chat.views.run_agent", fake_run_agent) response = client.post( reverse("chat:detail", args=[conversation.conversation_id]), {"message": "只查选中文档", "document_ids": [str(selected.id)]}, ) assert response.status_code == 200 assert captured["options"]["document_ids"] == [selected.id] assert captured["options"]["conversation_id"] == conversation.conversation_id assert captured["options"]["batch_id"] == batch.batch_id def test_chat_renders_three_column_workspace_and_node_results(client, db): batch, conversation = _create_conversation_with_batch() UploadedDocument.objects.create( batch=batch, scenario_id="document_review", original_name="说明书.md", file_type="md", size=1, status=UploadedDocument.STATUS_INDEXED, ) response = client.get(reverse("chat:detail", args=[conversation.conversation_id])) content = response.content.decode("utf-8") assert response.status_code == 200 assert "会话历史" in content assert "对话区与节点导航" in content assert "上传区" in content assert "资料包导入 / 已完成" in content assert "目录汇总 / 处理中" in content def test_chat_execution_creates_notification_record_from_agent_result(client, db, monkeypatch): batch, conversation = _create_conversation_with_batch() UploadedDocument.objects.create( batch=batch, scenario_id="document_review", original_name="说明书.md", file_type="md", size=1, status=UploadedDocument.STATUS_INDEXED, ) monkeypatch.setattr( "apps.chat.views.run_agent", lambda *args, **kwargs: AgentResult( answer="执行完成", status="success", notification_payload={ "batch_id": batch.batch_id, "conversation_id": conversation.conversation_id, "product_name": batch.product_name, "notify_reason": "task_completed", "owners": [ { "owner_role": "注册资料负责人", "feishu_user_id": "ou_demo_1", } ], }, ), ) response = client.post( reverse("chat:detail", args=[conversation.conversation_id]), {"message": "执行审核"}, ) assert response.status_code == 200 record = NotificationRecord.objects.get() assert record.notify_reason == "task_completed" assert record.batch_id == batch.batch_id assert record.web_detail_url.endswith(f"/audit/{AgentAuditLog.objects.get().id}/") def test_chat_execution_uses_notification_payload_message_status_and_receipt(client, db, monkeypatch): batch, conversation = _create_conversation_with_batch() UploadedDocument.objects.create( batch=batch, scenario_id="document_review", original_name="说明书.md", file_type="md", size=1, status=UploadedDocument.STATUS_INDEXED, ) monkeypatch.setattr( "apps.chat.views.run_agent", lambda *args, **kwargs: AgentResult( answer="通知已发送", status="success", notification_payload={ "batch_id": batch.batch_id, "conversation_id": conversation.conversation_id, "product_name": batch.product_name, "notify_reason": "task_completed", "message_status": "sent", "web_detail_url": "https://example.com/audit/custom", "receipt": {"message_id": "msg-custom", "status": "sent"}, "owners": [ { "owner_role": "注册资料负责人", "feishu_user_id": "ou_demo_1", } ], }, ), ) response = client.post( reverse("chat:detail", args=[conversation.conversation_id]), {"message": "执行通知任务"}, ) assert response.status_code == 200 record = NotificationRecord.objects.get() assert record.message_status == "sent" assert record.receipt["message_id"] == "msg-custom" def test_chat_execution_creates_failed_notification_record_and_updates_conversation(client, db, monkeypatch): batch, conversation = _create_conversation_with_batch() UploadedDocument.objects.create( batch=batch, scenario_id="document_review", original_name="说明书.md", file_type="md", size=1, status=UploadedDocument.STATUS_INDEXED, ) monkeypatch.setattr( "apps.chat.views.run_agent", lambda *args, **kwargs: AgentResult( answer="执行失败", status="failed", error="规则执行失败", node_results=[ {"code": "package_import", "label": "资料包导入", "status": "已完成"}, {"code": "overview", "label": "目录汇总", "status": "已完成"}, {"code": "risk", "label": "风险预警", "status": "已阻断"}, {"code": "feishu_notify", "label": "飞书通知", "status": "失败"}, ], notification_payload={ "batch_id": batch.batch_id, "conversation_id": conversation.conversation_id, "product_name": batch.product_name, "notify_reason": "task_failed", "owners": [ { "owner_role": "注册申报负责人", "feishu_user_id": "ou_demo_2", } ], }, ), ) response = client.post( reverse("chat:detail", args=[conversation.conversation_id]), {"message": "执行失败任务"}, ) assert response.status_code == 200 record = NotificationRecord.objects.get() conversation.refresh_from_db() assert record.notify_reason == "task_failed" assert record.message_status == "failed" assert record.web_detail_url.endswith(f"/audit/{AgentAuditLog.objects.get().id}/") assert conversation.task_status == "failed" assert conversation.node_results[-1]["label"] == "飞书通知" def test_chat_execution_persists_agent_node_results_to_conversation(client, db, monkeypatch): batch, conversation = _create_conversation_with_batch() UploadedDocument.objects.create( batch=batch, scenario_id="document_review", original_name="说明书.md", file_type="md", size=1, status=UploadedDocument.STATUS_INDEXED, ) monkeypatch.setattr( "apps.chat.views.run_agent", lambda *args, **kwargs: AgentResult( answer="已生成风险结论", status="success", node_results=[ {"code": "package_import", "label": "资料包导入", "status": "已完成"}, {"code": "overview", "label": "目录汇总", "status": "已完成"}, {"code": "completeness", "label": "法规完整性检查", "status": "已完成"}, {"code": "field_extraction", "label": "字段抽取", "status": "已完成"}, {"code": "consistency", "label": "一致性核查", "status": "待复核"}, {"code": "risk", "label": "风险预警", "status": "已阻断", "summary": "存在高风险"}, {"code": "word_export", "label": "Word 回填导出", "status": "待处理"}, {"code": "feishu_notify", "label": "飞书通知", "status": "待处理"}, ], notification_payload={ "batch_id": batch.batch_id, "conversation_id": conversation.conversation_id, "product_name": batch.product_name, "notify_reason": "task_completed", "owners": [], }, ), ) response = client.post( reverse("chat:detail", args=[conversation.conversation_id]), {"message": "执行节点任务"}, ) assert response.status_code == 200 conversation.refresh_from_db() assert len(conversation.node_results) == 8 assert conversation.task_status == "success" assert conversation.latest_summary["answer"] == "已生成风险结论" def test_create_conversation_for_batch_initializes_eight_workflow_nodes(db): conversation = create_conversation_for_batch( "SUB-20260604-001", "新型冠状病毒 2019-nCoV 核酸检测试剂盒", ) labels = [node["label"] for node in conversation.node_results] assert len(labels) == 8 assert labels == [ "资料包导入", "目录汇总", "法规完整性检查", "字段抽取", "一致性核查", "风险预警", "Word 回填导出", "飞书通知", ] def test_chat_page_shows_upload_entry_and_dynamic_context_cards(client, db): batch, conversation = _create_conversation_with_batch() conversation.node_results = [ {"label": "资料包导入", "status": "已完成"}, {"label": "目录汇总", "status": "已完成"}, {"label": "法规完整性检查", "status": "已完成"}, {"label": "字段抽取", "status": "已完成"}, {"label": "一致性核查", "status": "待复核"}, {"label": "风险预警", "status": "已阻断"}, {"label": "Word 回填导出", "status": "待处理"}, {"label": "飞书通知", "status": "待处理"}, ] conversation.save(update_fields=["node_results"]) UploadedDocument.objects.create( batch=batch, scenario_id="document_review", original_name="说明书.md", file_type="md", size=1, status=UploadedDocument.STATUS_INDEXED, ) response = client.get(reverse("chat:detail", args=[conversation.conversation_id])) content = response.content.decode("utf-8") assert response.status_code == 200 assert "继续上传资料" in content assert "最高风险等级" in content assert "是否允许正式导出" in content assert "通知状态" in content assert "飞书通知 / 待处理" in content def test_chat_page_shows_top_context_and_recommended_prompts(client, db): batch, conversation = _create_conversation_with_batch() conversation.task_status = "processing" conversation.node_results = [ {"label": "资料包导入", "status": "已完成"}, {"label": "目录汇总", "status": "已完成"}, {"label": "法规完整性检查", "status": "已完成"}, {"label": "字段抽取", "status": "已完成"}, {"label": "一致性核查", "status": "待复核"}, {"label": "风险预警", "status": "已阻断"}, {"label": "Word 回填导出", "status": "待复核"}, {"label": "飞书通知", "status": "待处理"}, ] conversation.save(update_fields=["task_status", "node_results", "updated_at"]) response = client.get(reverse("chat:detail", args=[conversation.conversation_id])) content = response.content.decode("utf-8") assert response.status_code == 200 assert "顶部对话上下文" in content assert "当前流程类型" in content assert "registration" in content assert "当前审核阶段" in content assert "processing" in content assert "当前最高风险等级" in content assert "推荐提问模板" in content assert "请汇总当前资料包的章节点、页数和目录覆盖情况" in content assert "请给出当前资料包的高风险项、责任人和整改建议" in content def test_chat_page_shows_overview_card_from_conversation_summary(client, db): batch, conversation = _create_conversation_with_batch() conversation.latest_summary = { "structured_output": { "output_type": "registration_overview_report", "batch_id": batch.batch_id, "product_name": batch.product_name, "file_count": 2, "total_page_count": 12, "chapter_summary": [ {"chapter_code": "CH1", "document_count": 2}, ], "documents": [ { "original_name": "说明书.md", "chapter_code": "CH1", "page_count": 12, "document_role": "product_manual", } ], "warnings": ["Word 页数待复核"], } } conversation.save(update_fields=["latest_summary", "updated_at"]) response = client.get(reverse("chat:detail", args=[conversation.conversation_id])) content = response.content.decode("utf-8") assert response.status_code == 200 assert "目录汇总能力卡" in content assert "资料文件数" in content assert "CH1 / 2 份" in content assert "Word 页数待复核" in content def test_chat_page_shows_consistency_card_from_conversation_summary(client, db): batch, conversation = _create_conversation_with_batch() conversation.latest_summary = { "structured_output": { "output_type": "registration_consistency_report", "summary": "检测到跨文档字段冲突。", "conflict_items": [ {"field_name": "产品名称", "issue": "申请表与说明书不一致"}, ], "mixed_document_risks": ["疑似混入其他产品资料"], "risk_level": "high", } } conversation.save(update_fields=["latest_summary", "updated_at"]) response = client.get(reverse("chat:detail", args=[conversation.conversation_id])) content = response.content.decode("utf-8") assert response.status_code == 200 assert "一致性核查能力卡" in content assert "申请表与说明书不一致" in content assert "疑似混入其他产品资料" in content assert "high" in content def test_chat_page_shows_completeness_card_from_conversation_summary(client, db): batch, conversation = _create_conversation_with_batch() conversation.latest_summary = { "structured_output": { "output_type": "registration_completeness_report", "summary": "当前资料包仍缺少关键必交项。", "risk_level": "high", "missing_items": [ { "chapter_code": "CH2", "document_name": "产品技术要求", "reason": "当前资料包未发现对应文件", } ], "misplaced_items": [ { "chapter_code": "CH4", "document_name": "研究资料", "current_location": "CH6", } ], } } conversation.save(update_fields=["latest_summary", "updated_at"]) response = client.get(reverse("chat:detail", args=[conversation.conversation_id])) content = response.content.decode("utf-8") assert response.status_code == 200 assert "完整性检查能力卡" in content assert "当前资料包仍缺少关键必交项" in content assert "产品技术要求" in content assert "CH2" in content assert "当前资料包未发现对应文件" in content assert "研究资料" in content assert "当前归类:CH6" in content def test_chat_page_shows_field_extraction_card_from_conversation_summary(client, db): batch, conversation = _create_conversation_with_batch() conversation.latest_summary = { "structured_output": { "output_type": "registration_field_extraction_report", "summary": "已完成核心注册字段抽取。", "field_items": [ { "field_name": "产品名称", "field_value": batch.product_name, "source_document": "申请表.docx", }, { "field_name": "规格型号", "field_value": "48T/盒", "source_document": "产品列表.xlsx", }, ], "low_confidence_items": [ { "field_name": "储存条件", "field_value": "2-8℃", "source_document": "说明书.docx", } ], } } conversation.save(update_fields=["latest_summary", "updated_at"]) response = client.get(reverse("chat:detail", args=[conversation.conversation_id])) content = response.content.decode("utf-8") assert response.status_code == 200 assert "字段抽取能力卡" in content assert "已完成核心注册字段抽取" in content assert "产品名称" in content assert "48T/盒" in content assert "来源:申请表.docx" in content assert "储存条件" in content assert "来源:说明书.docx" in content def test_chat_page_blocks_formal_export_when_word_export_node_is_blocked(client, db): batch, conversation = _create_conversation_with_batch() conversation.node_results = [ {"label": "资料包导入", "status": "已完成"}, {"label": "目录汇总", "status": "已完成"}, {"label": "法规完整性检查", "status": "已完成"}, {"label": "字段抽取", "status": "已完成"}, {"label": "一致性核查", "status": "已完成"}, {"label": "风险预警", "status": "已完成"}, {"label": "Word 回填导出", "status": "已阻断"}, {"label": "飞书通知", "status": "待处理"}, ] conversation.latest_summary = { "structured_output": { "download_url": "/downloads/export.docx", } } conversation.save(update_fields=["node_results", "latest_summary", "updated_at"]) response = client.get(reverse("chat:detail", args=[conversation.conversation_id])) content = response.content.decode("utf-8") assert response.status_code == 200 assert "是否允许正式导出" in content assert ">否<" in content assert "/downloads/export.docx" in content def test_chat_page_uses_structured_formal_export_flag_when_node_status_is_completed(client, db): batch, conversation = _create_conversation_with_batch() conversation.node_results = [ {"label": "资料包导入", "status": "已完成"}, {"label": "目录汇总", "status": "已完成"}, {"label": "法规完整性检查", "status": "已完成"}, {"label": "字段抽取", "status": "已完成"}, {"label": "一致性核查", "status": "已完成"}, {"label": "风险预警", "status": "已完成"}, {"label": "Word 回填导出", "status": "已完成"}, {"label": "飞书通知", "status": "待处理"}, ] conversation.latest_summary = { "structured_output": { "can_export_formally": False, "download_url": "/downloads/review-only.docx", } } conversation.save(update_fields=["node_results", "latest_summary", "updated_at"]) response = client.get(reverse("chat:detail", args=[conversation.conversation_id])) content = response.content.decode("utf-8") assert response.status_code == 200 assert "是否允许正式导出" in content assert ">否<" in content assert "/downloads/review-only.docx" in content def test_chat_page_shows_word_export_field_table_and_governance_entries(client, db): batch, conversation = _create_conversation_with_batch() conversation.node_results = [ {"label": "资料包导入", "status": "已完成"}, {"label": "目录汇总", "status": "已完成"}, {"label": "法规完整性检查", "status": "已完成"}, {"label": "字段抽取", "status": "已完成"}, {"label": "一致性核查", "status": "已完成"}, {"label": "风险预警", "status": "已阻断"}, {"label": "Word 回填导出", "status": "待复核"}, {"label": "飞书通知", "status": "待处理"}, ] conversation.latest_summary = { "structured_output": { "output_type": "registration_word_export_report", "template_name": "注册证导出模板", "template_version": "V1.0", "export_status": "draft_only", "filled_fields": [ { "placeholder": "{{ product_name }}", "field_name": "产品名称", "field_value": "新型冠状病毒 2019-nCoV 核酸检测试剂盒", "source": "资料包主信息", "fill_status": "filled", "required": True, } ], "blocked_fields": [ { "field_name": "产品名称跨文档不一致", "block_reason": "待人工复核", "risk_source": "registration_risk_report", } ], "download_url": "/media/exports/20260604/SUB-20260604-001-draft.docx", } } conversation.save(update_fields=["node_results", "latest_summary", "updated_at"]) response = client.get(reverse("chat:detail", args=[conversation.conversation_id])) content = response.content.decode("utf-8") assert response.status_code == 200 assert "Word 导出能力卡" in content assert "回填字段表" in content assert "产品名称" in content assert "资料包主信息" in content assert "拦截项区" in content assert "产品名称跨文档不一致" in content assert "维护 Word 模板" in content assert "维护字段映射" in content def test_chat_page_shows_risk_and_notification_cards_from_conversation_summary(client, db): batch, conversation = _create_conversation_with_batch() conversation.node_results = [ {"label": "资料包导入", "status": "已完成"}, {"label": "目录汇总", "status": "已完成"}, {"label": "法规完整性检查", "status": "已完成"}, {"label": "字段抽取", "status": "已完成"}, {"label": "一致性核查", "status": "待复核"}, {"label": "风险预警", "status": "已阻断"}, {"label": "Word 回填导出", "status": "待复核"}, {"label": "飞书通知", "status": "已完成"}, ] conversation.latest_summary = { "structured_output": { "output_type": "registration_risk_report", "summary": "存在高风险项,需人工复核。", "highest_risk_level": "high", "pass_status": "blocked", "manual_review_items": ["CH1.11.5 沟通记录缺失"], "risk_items": [{"title": "产品名称跨文档不一致", "risk_level": "high"}], "owner_roles": [ { "owner_role": "注册资料负责人", "owner_name": "张三", "department": "注册事务部", "chapter_scope": "CH1", "risk_scope": "字段冲突", "feishu_user_id": "ou_demo_1", "feishu_open_id": "on_demo_1", "feishu_name": "张三", "notify_enabled": True, } ], "notify_reason": "task_completed", "mentioned_users": ["ou_demo_1"], "message_status": "sent", "web_detail_url": "https://example.com/audit/1", } } conversation.save(update_fields=["node_results", "latest_summary", "updated_at"]) response = client.get(reverse("chat:detail", args=[conversation.conversation_id])) content = response.content.decode("utf-8") assert response.status_code == 200 assert "风险预警能力卡" in content assert "总风险等级" in content assert "high" in content assert "注册资料负责人" in content assert "注册事务部" in content assert "字段冲突" in content assert "ou_demo_1" in content assert "on_demo_1" in content assert "张三" in content assert "True" in content assert "CH1.11.5 沟通记录缺失" in content assert "飞书通知能力卡" in content assert "task_completed" in content assert "sent" in content def test_chat_upload_keeps_existing_conversation_binding_and_adds_documents(client, db): batch, conversation = _create_conversation_with_batch() existing_document = UploadedDocument.objects.create( batch=batch, scenario_id="document_review", original_name="原说明书.md", file_type="md", size=1, status=UploadedDocument.STATUS_INDEXED, ) existing_count = UploadedDocument.objects.filter(batch=batch).count() upload_file = SimpleUploadedFile( "新增补充资料.txt", "产品名称:新型冠状病毒 2019-nCoV 核酸检测试剂盒".encode("utf-8"), content_type="text/plain", ) response = client.post( reverse("chat:upload-documents", args=[conversation.conversation_id]), {"files": [upload_file]}, follow=True, ) content = response.content.decode("utf-8") batch.refresh_from_db() conversation.refresh_from_db() assert response.status_code == 200 assert SubmissionBatch.objects.count() == 1 assert Conversation.objects.count() == 1 assert conversation.conversation_id == "conv-001" assert batch.conversation_id == conversation.conversation_id assert UploadedDocument.objects.filter(batch=batch).count() == existing_count + 1 assert UploadedDocument.objects.filter(batch=batch, original_name="新增补充资料.txt").exists() assert "新增补充资料.txt" in content assert "已补充到当前资料包" in content @override_settings(MEDIA_URL="/media/") def test_generate_registration_export_creates_real_docx_file(db, tmp_path, settings): from apps.chat.export_service import generate_registration_export settings.MEDIA_ROOT = tmp_path / "uploads" batch, conversation = _create_conversation_with_batch() risk_summary = { "output_type": "registration_risk_report", "summary": "存在高风险项,正式版导出应被阻断,但允许生成草稿。", "highest_risk_level": "high", "pass_status": "blocked", "manual_review_items": ["CH1.11.5 沟通记录待补齐"], "risk_items": [ {"title": "产品名称跨文档不一致", "risk_level": "high"}, ], } report = generate_registration_export( batch=batch, conversation=conversation, upstream_summary=risk_summary, ) export_path = settings.MEDIA_ROOT / report["output_file"]["relative_path"] assert report["output_type"] == "registration_word_export_report" assert report["can_export_formally"] is False assert report["export_status"] == "draft_only" assert report["filled_fields"][0]["field_name"] == "产品名称" assert report["filled_fields"][0]["fill_status"] == "filled" assert report["blocked_fields"][0]["block_reason"] == "待人工复核" assert report["output_file"]["output_version"] == "draft" assert report["output_file"]["generated_at"] assert report["download_url"].startswith("/media/exports/") assert export_path.exists() with zipfile.ZipFile(export_path) as archive: document_xml = archive.read("word/document.xml").decode("utf-8") assert batch.product_name in document_xml assert "产品名称跨文档不一致" in document_xml @override_settings(MEDIA_URL="/media/") def test_chat_export_word_route_persists_real_download_link(client, db, tmp_path, settings): settings.MEDIA_ROOT = tmp_path / "uploads" batch, conversation = _create_conversation_with_batch() conversation.node_results = [ {"label": "资料包导入", "status": "已完成"}, {"label": "目录汇总", "status": "已完成"}, {"label": "法规完整性检查", "status": "已完成"}, {"label": "字段抽取", "status": "已完成"}, {"label": "一致性核查", "status": "已完成"}, {"label": "风险预警", "status": "已阻断"}, {"label": "Word 回填导出", "status": "待处理"}, {"label": "飞书通知", "status": "待处理"}, ] conversation.latest_summary = { "structured_output": { "output_type": "registration_risk_report", "summary": "存在高风险项,允许草稿导出。", "highest_risk_level": "high", "pass_status": "blocked", "manual_review_items": ["CH1.11.5 沟通记录待补齐"], "risk_items": [{"title": "产品名称跨文档不一致", "risk_level": "high"}], } } conversation.save(update_fields=["node_results", "latest_summary", "updated_at"]) response = client.post( reverse("chat:export-word", args=[conversation.conversation_id]), follow=True, ) content = response.content.decode("utf-8") conversation.refresh_from_db() export_report = conversation.latest_summary["structured_output"] export_path = settings.MEDIA_ROOT / export_report["output_file"]["relative_path"] assert response.status_code == 200 assert export_report["output_type"] == "registration_word_export_report" assert export_report["download_url"].startswith("/media/exports/") assert export_path.exists() assert "下载导出文件" in content assert export_report["download_url"] in content assert AgentAuditLog.objects.filter(conversation_id=conversation.conversation_id).count() == 1 assert ExportedDocument.objects.filter(batch=batch, conversation_id=conversation.conversation_id).count() == 1