docs(docs): 调整数据库与详细设计目录编号

This commit is contained in:
zhiye.sun
2026-06-10 15:15:02 +08:00
parent db0e94cf26
commit a060c23ba7
8 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,930 @@
# 自动汇总文件夹文件目录与页数流程详细设计
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 需求分析文档 | docs/1.需求分析/1.自动汇总.md |
| 功能设计文档 | docs/2.功能设计/1.自动汇总.md |
| 功能名称 | 自动汇总文件夹文件目录与页数 |
| 所属模块 | 审核智能体 review_agent |
| 设计日期 | 2026-06-05 |
| 设计版本 | V1.0 |
---
## 一、详细设计目标
本详细设计用于指导“自动汇总文件夹文件目录与页数”功能开发落地覆盖代码目录、数据模型、接口契约、后台工作流、Skill 拆分、轻量依赖、前端三栏布局、SSE 实时状态、异常重试和测试用例。
核心约束:
| 约束 | 说明 |
| --- | --- |
| 对话绑定 | 上传文件与当前 Conversation 绑定,一个对话对应一套文件,不能串文件 |
| 上传即存储 | 用户拖拽或选择文件后立即保存,但不启动工作流 |
| 提示词触发 | 用户发送消息后,根据提示词判断是否启动自动汇总工作流 |
| 后台异步 | 工作流后台执行,右侧第三栏工作流卡片实时更新 |
| 轻量依赖 | 优先使用 Python 内部库和轻量第三方库,不强依赖 LibreOffice |
| 老格式支持 | doc、xls、ppt 进入处理流程,能读到页数则统计,读不到则记录异常 |
| 结果存档 | 批次、文件、节点、事件、明细、导出文件全部入库 |
---
## 二、代码结构设计
### 2.1 目录结构
在现有 `review_agent` 应用内按模块重新划分文件处理能力。Django 模型仍集中放在 `review_agent/models.py`,其余代码放入 `review_agent/file_summary/`
```text
review_agent/
models.py
urls.py
views.py
services.py
file_summary/
__init__.py
constants.py
schemas.py
storage.py
workflow.py
events.py
urls.py
views.py
services/
__init__.py
archive.py
inventory.py
page_count.py
product_detect.py
report.py
export_excel.py
workflow_trigger.py
skills/
__init__.py
base.py
registry.py
upload_intake.py
archive_extract.py
file_inventory.py
document_page_count.py
product_detect.py
summary_report.py
excel_export.py
```
### 2.2 文件职责
| 文件 | 职责 |
| --- | --- |
| review_agent/models.py | 集中定义 Conversation、Message、文件汇总相关模型 |
| file_summary/constants.py | 状态、节点、文件类型、事件类型常量 |
| file_summary/schemas.py | dataclass 入参出参结构,避免业务层直接传散乱 dict |
| file_summary/storage.py | 上传文件、工作目录、导出文件路径生成与保存 |
| file_summary/workflow.py | WorkflowExecutor串行执行节点图 |
| file_summary/events.py | 工作流事件持久化与 SSE 格式化 |
| file_summary/views.py | 上传暂存、启动工作流、状态查询、SSE、下载接口 |
| services/archive.py | 压缩包识别、zip/7z/rar 解压 |
| services/inventory.py | 文件遍历与清单生成 |
| services/page_count.py | 文件页数统计与 3 次重试 |
| services/product_detect.py | 产品名识别 |
| services/report.py | Markdown 报告和对话简表生成 |
| services/export_excel.py | Excel 文件导出 |
| services/workflow_trigger.py | 根据提示词判断是否触发自动汇总工作流 |
| skills/base.py | Skill 基类与统一返回结构 |
| skills/registry.py | Skill 注册与按需加载 |
| skills/*.py | 各工作流节点对应 Skill |
---
## 三、依赖设计
### 3.1 requirements 建议
```text
Django==5.2.14
pypdf
python-docx
python-pptx
openpyxl
xlrd
olefile
py7zr
```
### 3.2 格式处理策略
| 格式 | 处理库 | 统计口径 | 失败策略 |
| --- | --- | --- | --- |
| pdf | pypdf | PDF 页面数 | 重试 3 次,仍失败记录异常 |
| docx | python-docx | 优先读取内置页数属性 | 读不到记录“页数不可确定” |
| doc | olefile | 读取 OLE 元数据页数 | 读不到记录“页数不可确定” |
| pptx | python-pptx | 幻灯片数量 | 重试 3 次,仍失败记录异常 |
| ppt | olefile | 读取 OLE 元数据页数/幻灯片数 | 读不到记录“页数不可确定” |
| xlsx | openpyxl | 工作表数量 | 重试 3 次,仍失败记录异常 |
| xls | xlrd | 工作表数量 | 重试 3 次,仍失败记录异常 |
### 3.3 压缩包处理策略
| 格式 | 处理方式 | 说明 |
| --- | --- | --- |
| zip | Python 标准库 zipfile | 必须支持 |
| 7z | py7zr | 必须支持 |
| rar | 优先系统 7z 命令 | Docker 镜像需安装 7-Zip/p7zip |
### 3.4 Docker 部署说明
Demo 运行不强依赖 LibreOffice。若未来要求 doc/docx/ppt/pptx 页数与 Office 打开后的分页完全一致,可在 Docker 镜像中额外安装 LibreOffice headless再通过“转换 PDF 后统计页数”的增强策略实现。
RAR 解压如需稳定支持Docker 镜像需要安装 7-Zip/p7zip并确保 `7z` 命令在 PATH 中可调用。
---
## 四、数据模型详细设计
模型集中放在 `review_agent/models.py`,按“会话模型”和“文件汇总模型”分段。
### 4.1 FileAttachment
用户上传即存储的文件记录。此时尚未启动工作流。
| 字段 | 类型 | 约束 | 说明 |
| --- | --- | --- | --- |
| id | BigAutoField | PK | 主键 |
| conversation | ForeignKey(Conversation) | CASCADE, db_index | 绑定对话 |
| user | ForeignKey(User) | CASCADE, db_index | 上传用户 |
| original_name | CharField(255) | required | 原始文件名 |
| storage_path | CharField(500) | required | 本地保存路径 |
| file_size | BigIntegerField | default=0 | 文件大小 |
| content_type | CharField(120) | blank | MIME 类型 |
| upload_status | CharField(20) | choices | uploaded、bound、deleted |
| created_at | DateTimeField | auto_now_add | 上传时间 |
索引:
```text
(conversation, created_at)
(user, created_at)
```
### 4.2 FileSummaryBatch
一次自动汇总工作流批次。
| 字段 | 类型 | 约束 | 说明 |
| --- | --- | --- | --- |
| id | BigAutoField | PK | 主键 |
| conversation | ForeignKey(Conversation) | CASCADE, db_index | 绑定对话 |
| user | ForeignKey(User) | CASCADE, db_index | 执行用户 |
| trigger_message | ForeignKey(Message) | SET_NULL, null | 触发工作流的用户消息 |
| batch_no | CharField(64) | unique | 批次编号 |
| product_name | CharField(200) | blank | 产品名称 |
| status | CharField(20) | choices | pending、running、success、failed |
| total_files | IntegerField | default=0 | 文件总数 |
| supported_files | IntegerField | default=0 | 支持统计数 |
| success_files | IntegerField | default=0 | 成功数 |
| failed_files | IntegerField | default=0 | 失败数 |
| unsupported_files | IntegerField | default=0 | 不支持数 |
| uncertain_files | IntegerField | default=0 | 页数不可确定数 |
| total_pages | IntegerField | default=0 | 总页数 |
| work_dir | CharField(500) | blank | 工作目录 |
| error_message | TextField | blank | 批次错误 |
| created_at | DateTimeField | auto_now_add | 创建时间 |
| started_at | DateTimeField | null | 开始时间 |
| finished_at | DateTimeField | null | 结束时间 |
### 4.3 FileSummaryBatchAttachment
批次与上传文件的绑定表,确保工作流只读取本批次文件。
| 字段 | 类型 | 约束 | 说明 |
| --- | --- | --- | --- |
| id | BigAutoField | PK | 主键 |
| batch | ForeignKey(FileSummaryBatch) | CASCADE | 批次 |
| attachment | ForeignKey(FileAttachment) | CASCADE | 上传文件 |
| created_at | DateTimeField | auto_now_add | 绑定时间 |
唯一约束:
```text
unique(batch, attachment)
```
### 4.4 FileSummaryItem
文件明细记录。
| 字段 | 类型 | 约束 | 说明 |
| --- | --- | --- | --- |
| id | BigAutoField | PK | 主键 |
| batch | ForeignKey(FileSummaryBatch) | CASCADE, db_index | 所属批次 |
| file_index | IntegerField | required | 文件序号 |
| directory_level | CharField(300) | blank | 目录层级 |
| file_name | CharField(255) | required | 文件名 |
| file_type | CharField(20) | required | 扩展名 |
| relative_path | CharField(500) | required | 相对路径 |
| storage_path | CharField(500) | required | 实际处理路径 |
| page_count | IntegerField | null | 页数 |
| statistics_status | CharField(20) | choices | success、failed、unsupported、uncertain、skipped |
| retry_count | IntegerField | default=0 | 重试次数 |
| error_message | TextField | blank | 异常说明 |
| created_at | DateTimeField | auto_now_add | 创建时间 |
| updated_at | DateTimeField | auto_now | 更新时间 |
唯一约束:
```text
unique(batch, relative_path)
```
### 4.5 WorkflowNodeRun
工作流节点状态记录。
| 字段 | 类型 | 约束 | 说明 |
| --- | --- | --- | --- |
| id | BigAutoField | PK | 主键 |
| batch | ForeignKey(FileSummaryBatch) | CASCADE, db_index | 批次 |
| node_code | CharField(40) | required | 节点编码 |
| node_name | CharField(80) | required | 节点名称 |
| status | CharField(20) | choices | pending、running、retrying、success、failed、skipped |
| progress | IntegerField | default=0 | 进度百分比 |
| message | TextField | blank | 节点说明 |
| started_at | DateTimeField | null | 开始时间 |
| finished_at | DateTimeField | null | 完成时间 |
唯一约束:
```text
unique(batch, node_code)
```
### 4.6 WorkflowEvent
SSE 事件持久化记录,用于页面刷新后恢复和调试。
| 字段 | 类型 | 约束 | 说明 |
| --- | --- | --- | --- |
| id | BigAutoField | PK | 主键 |
| batch | ForeignKey(FileSummaryBatch) | CASCADE, db_index | 批次 |
| event_type | CharField(40) | required | 事件类型 |
| payload | JSONField | default=dict | 事件载荷 |
| created_at | DateTimeField | auto_now_add | 创建时间 |
### 4.7 ExportedSummaryFile
导出文件记录。
| 字段 | 类型 | 约束 | 说明 |
| --- | --- | --- | --- |
| id | BigAutoField | PK | 主键 |
| batch | ForeignKey(FileSummaryBatch) | CASCADE, db_index | 批次 |
| export_type | CharField(20) | choices | markdown、excel |
| file_name | CharField(255) | required | 文件名 |
| storage_path | CharField(500) | required | 保存路径 |
| status | CharField(20) | choices | success、failed |
| error_message | TextField | blank | 异常 |
| created_at | DateTimeField | auto_now_add | 生成时间 |
下载链接运行时根据 `export_id` 生成,不建议长期存储静态 URL。
---
## 五、常量与状态设计
### 5.1 支持格式
```python
SUPPORTED_PAGE_TYPES = {"pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx"}
ARCHIVE_TYPES = {"zip", "7z", "rar"}
```
### 5.2 工作流节点
```python
WORKFLOW_NODES = [
("upload", "上传中"),
("extract", "解压中"),
("inventory", "扫描中"),
("page_count", "解析页数中"),
("product_detect", "识别产品名中"),
("report", "输出 Markdown 中"),
("excel_export", "输出 Excel 中"),
("completed", "已完成"),
]
```
### 5.3 触发词规则
`workflow_trigger.py` 先用规则判断,后续可升级为 LLM 意图识别。
```python
SUMMARY_TRIGGER_KEYWORDS = [
"自动汇总",
"文件目录",
"页数",
"统计文件",
"汇总目录",
"目录与页数",
]
```
规则:
| 条件 | 结果 |
| --- | --- |
| 当前对话存在未绑定或最近上传文件,且提示词命中关键词 | 启动自动汇总工作流 |
| 未命中关键词 | 走普通 LLM 对话 |
| 命中关键词但没有上传文件 | AI 回复提示“请先上传文件或压缩包” |
---
## 六、服务与方法签名
### 6.1 storage.py
```python
def save_attachment(conversation, user, uploaded_file) -> FileAttachment:
"""保存上传文件并绑定当前对话。"""
def build_batch_work_dir(batch: FileSummaryBatch) -> Path:
"""生成批次工作目录。"""
def build_export_path(batch: FileSummaryBatch, suffix: str) -> Path:
"""生成导出文件路径。"""
```
存储目录:
```text
media/review_agent/
user_{user_id}/
conversation_{conversation_id}/
attachments/
batches/
batch_{batch_id}/
input/
extracted/
exports/
```
### 6.2 archive.py
```python
def is_archive(path: Path) -> bool:
"""判断是否压缩包。"""
def extract_archive(source: Path, target_dir: Path) -> list[Path]:
"""解压 zip、7z、rar返回解压后的文件路径列表。"""
def extract_zip(source: Path, target_dir: Path) -> list[Path]:
"""使用 zipfile 解压。"""
def extract_7z(source: Path, target_dir: Path) -> list[Path]:
"""使用 py7zr 解压。"""
def extract_rar(source: Path, target_dir: Path) -> list[Path]:
"""优先调用系统 7z 命令解压 rar。"""
```
安全规则:
| 规则 | 说明 |
| --- | --- |
| 路径穿越检查 | 解压后的最终路径必须仍在 target_dir 内 |
| 文件名清理 | 保留原名,但禁止绝对路径和上级目录跳转 |
| 解压失败 | 抛出 ArchiveExtractError批次失败 |
### 6.3 inventory.py
```python
def scan_files(batch: FileSummaryBatch, roots: list[Path]) -> list[FileSummaryItem]:
"""扫描目录或散装文件,创建 FileSummaryItem。"""
def build_directory_level(relative_path: Path) -> str:
"""根据相对路径生成目录层级。"""
def normalize_file_type(path: Path) -> str:
"""返回小写扩展名,不含点。"""
```
### 6.4 page_count.py
```python
def count_pages(item: FileSummaryItem) -> PageCountResult:
"""根据文件类型分发页数统计。"""
def count_pages_with_retry(item: FileSummaryItem, max_retry: int = 3) -> PageCountResult:
"""失败最多重试 3 次。"""
def count_pdf(path: Path) -> int:
"""使用 pypdf 统计 PDF 页数。"""
def count_docx(path: Path) -> PageCountResult:
"""使用 python-docx 读取内置页数属性。"""
def count_doc(path: Path) -> PageCountResult:
"""使用 olefile 读取老 doc 的 OLE 元数据页数。"""
def count_xlsx(path: Path) -> int:
"""使用 openpyxl 统计工作表数量。"""
def count_xls(path: Path) -> int:
"""使用 xlrd 统计工作表数量。"""
def count_pptx(path: Path) -> int:
"""使用 python-pptx 统计幻灯片数量。"""
def count_ppt(path: Path) -> PageCountResult:
"""使用 olefile 读取老 ppt 的 OLE 元数据页数或幻灯片数。"""
```
`PageCountResult`
```python
@dataclass
class PageCountResult:
status: str
page_count: int | None = None
error_message: str = ""
```
状态规则:
| 情况 | status | page_count |
| --- | --- | --- |
| 成功读取页数 | success | 整数 |
| 不支持类型 | unsupported | None |
| 文件可读但页数无元数据 | uncertain | None |
| 解析异常且重试失败 | failed | None |
### 6.5 product_detect.py
```python
def detect_product_name(batch: FileSummaryBatch) -> ProductDetectResult:
"""从目录名、文件名和少量元数据中识别产品名。"""
def update_conversation_title(batch: FileSummaryBatch, product_name: str) -> None:
"""按规则更新对话标题。"""
```
产品名识别优先级:
| 优先级 | 来源 |
| --- | --- |
| 1 | 顶层目录名 |
| 2 | 文件名中包含“产品”“试剂盒”“说明书”等关键词的片段 |
| 3 | docx 文档属性 title |
| 4 | PDF 元数据 title |
### 6.6 report.py
```python
def build_summary_stats(batch: FileSummaryBatch) -> dict:
"""汇总统计数据。"""
def build_chat_markdown(batch: FileSummaryBatch) -> str:
"""生成对话框展示 Markdown 简表。"""
def build_full_markdown_report(batch: FileSummaryBatch) -> str:
"""生成完整 Markdown 报告。"""
def save_markdown_report(batch: FileSummaryBatch) -> ExportedSummaryFile:
"""保存 Markdown 报告并创建导出记录。"""
```
### 6.7 export_excel.py
```python
def build_excel_workbook(batch: FileSummaryBatch) -> Workbook:
"""构建 Excel Workbook。"""
def save_excel(batch: FileSummaryBatch) -> ExportedSummaryFile:
"""保存 Excel 并创建导出记录。"""
```
工作表:
| Sheet | 字段 |
| --- | --- |
| 汇总信息 | 批次编号、产品名、文件总数、成功数、失败数、不可确定数、总页数 |
| 文件明细 | 序号、目录层级、文件名、类型、页数、相对路径、状态、重试次数、异常说明 |
---
## 七、Skill 详细设计
### 7.1 BaseSkill
```python
class BaseSkill:
name: str
node_code: str
def run(self, context: WorkflowContext) -> SkillResult:
raise NotImplementedError
```
`WorkflowContext`
```python
@dataclass
class WorkflowContext:
batch_id: int
conversation_id: int
user_id: int
message_id: int | None = None
```
`SkillResult`
```python
@dataclass
class SkillResult:
success: bool
message: str = ""
data: dict = field(default_factory=dict)
```
### 7.2 Skill 列表
| Skill 类名 | 节点 | 调用服务 |
| --- | --- | --- |
| UploadIntakeSkill | upload | storage.py |
| ArchiveExtractSkill | extract | archive.py |
| FileInventorySkill | inventory | inventory.py |
| DocumentPageCountSkill | page_count | page_count.py |
| ProductDetectSkill | product_detect | product_detect.py |
| SummaryReportSkill | report | report.py |
| ExcelExportSkill | excel_export | export_excel.py |
---
## 八、工作流执行器详细设计
### 8.1 执行入口
```python
def start_file_summary_workflow(batch_id: int) -> None:
thread = threading.Thread(
target=WorkflowExecutor().run,
args=(batch_id,),
daemon=True,
)
thread.start()
```
### 8.2 执行伪代码
```python
class WorkflowExecutor:
def run(self, batch_id: int) -> None:
batch = FileSummaryBatch.objects.get(pk=batch_id)
self.mark_batch_running(batch)
self.emit("workflow_started", batch, {"batch_id": batch.id})
try:
for node_code in self.resolve_nodes(batch):
self.run_node(batch, node_code)
self.mark_batch_success(batch)
self.emit("workflow_completed", batch, self.build_completed_payload(batch))
except Exception as exc:
self.mark_batch_failed(batch, str(exc))
self.emit("workflow_failed", batch, {"message": str(exc)})
```
### 8.3 节点跳过规则
| 节点 | 跳过条件 |
| --- | --- |
| extract | 当前批次没有压缩包 |
| product_detect | 没有任何可用于识别的文件名、目录名或元数据 |
---
## 九、接口详细设计
### 9.1 上传暂存接口
```text
POST /api/review-agent/conversations/{conversation_id}/attachments/
Content-Type: multipart/form-data
```
请求:
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| files[] | File[] | 是 | 一个或多个文件 |
响应:
```json
{
"attachments": [
{
"id": 101,
"original_name": "注册资料.zip",
"file_size": 204800,
"upload_status": "uploaded"
}
]
}
```
权限:
```text
conversation.user 必须等于 request.user
```
### 9.2 发送消息并按需触发工作流
沿用现有 `POST /chat/stream/` SSE 能力,在 `stream_chat` 中增加判断:
```text
用户发送 prompt
-> 保存 Message
-> 判断 prompt 是否命中自动汇总工作流
-> 命中则创建 FileSummaryBatch 并启动后台工作流
-> SSE 返回 workflow_meta
-> 未命中则走原 LLM 流式回复
```
新增 SSE meta
```json
{
"conversation_id": 1,
"title": "新对话",
"workflow": {
"type": "file_summary",
"batch_id": 12,
"status": "running"
}
}
```
### 9.3 查询批次状态
```text
GET /api/review-agent/file-summary/{batch_id}/
```
响应:
```json
{
"batch": {
"id": 12,
"batch_no": "FS202606050001",
"status": "running",
"product_name": "",
"total_files": 24,
"success_files": 10,
"failed_files": 1,
"uncertain_files": 2,
"total_pages": 180
},
"nodes": [
{
"node_code": "page_count",
"node_name": "解析页数中",
"status": "running",
"progress": 45,
"message": "正在解析 11/24"
}
],
"exports": []
}
```
### 9.4 工作流事件流
```text
GET /api/review-agent/file-summary/{batch_id}/events/?after={event_id}
```
响应类型:`text/event-stream`
事件:
```text
event: node_progress
data: {"event_id": 301, "batch_id": 12, "node_code": "page_count", "status": "running", "progress": 45, "message": "正在解析 11/24"}
```
### 9.5 下载导出文件
```text
GET /api/review-agent/file-summary/exports/{export_id}/download/
```
权限:
```text
ExportedSummaryFile -> batch -> conversation -> user 必须为当前用户
```
---
## 十、前端详细设计
### 10.1 三栏布局
页面调整为三栏:
| 区域 | 内容 |
| --- | --- |
| 左侧栏 | 对话历史 |
| 中间栏 | 聊天消息、输入框 |
| 右侧栏上半部分 | 拖拽式文件导入区 |
| 右侧栏下半部分 | 工作流卡片列表 |
HTML 结构建议:
```html
<main class="workspace three-column">
<aside class="sidebar"></aside>
<section class="chat-shell"></section>
<aside class="workflow-panel">
<section class="upload-dropzone" id="uploadDropzone"></section>
<section class="workflow-card-list" id="workflowCardList"></section>
</aside>
</main>
```
### 10.2 上传交互
JS 方法:
```javascript
function bindUploadDropzone()
function uploadConversationFiles(files)
function renderAttachmentList(attachments)
```
流程:
```text
用户拖拽或选择文件
-> POST attachments 接口
-> 保存成功后右侧上传区展示文件名
-> 不启动工作流
-> 用户发送提示词
-> 命中工作流后创建工作流卡片
```
### 10.3 工作流卡片
JS 方法:
```javascript
function createWorkflowCard(batch)
function updateWorkflowNode(batchId, nodePayload)
function markWorkflowCompleted(batchId, payload)
function markWorkflowFailed(batchId, payload)
function connectWorkflowEvents(batchId)
function restoreWorkflowCards()
```
卡片结构:
```html
<article class="workflow-card" data-batch-id="12">
<header>
<strong>文件目录与页数汇总</strong>
<span class="workflow-status">运行中</span>
</header>
<ol class="workflow-nodes">
<li data-node-code="upload">上传中</li>
<li data-node-code="extract">解压中</li>
<li data-node-code="inventory">扫描中</li>
<li data-node-code="page_count">解析页数中</li>
<li data-node-code="product_detect">识别产品名中</li>
<li data-node-code="report">输出 Markdown 中</li>
<li data-node-code="excel_export">输出 Excel 中</li>
</ol>
</article>
```
### 10.4 Markdown 渲染
现有消息使用 `nl2br`,无法正常渲染 Markdown 表格。需要改造:
| 消息类型 | 渲染策略 |
| --- | --- |
| 普通用户消息 | escapeHtml + nl2br |
| 普通助手消息 | 安全 Markdown 渲染 |
| 文件汇总结果 | 安全 Markdown 渲染,允许 table、a、strong、code |
可选方案:
| 方案 | 说明 |
| --- | --- |
| 前端 marked + DOMPurify | 渲染体验好,但增加前端依赖 |
| 后端 markdown + bleach | 后端输出安全 HTML前端直接展示 |
Demo 建议使用前端 `marked` + `DOMPurify` CDN 或本地静态文件。
---
## 十一、对话标题更新设计
产品名识别成功后更新标题:
```python
def update_conversation_title(batch, product_name):
conversation = batch.conversation
if conversation.title.startswith("新对话"):
conversation.title = f"{product_name}-文件汇总"[:120]
conversation.save(update_fields=["title", "updated_at"])
```
规则:
| 场景 | 处理 |
| --- | --- |
| 新对话默认标题 | 更新为产品名 |
| 用户已有自定义标题 | 不覆盖 |
| 产品名为空 | 不更新 |
---
## 十二、测试设计
### 12.1 单元测试
| 用例 | 目标 |
| --- | --- |
| test_trigger_keywords | 提示词命中时触发自动汇总 |
| test_save_attachment_binds_conversation | 上传文件绑定当前对话 |
| test_zip_extract_safe_path | zip 解压禁止路径穿越 |
| test_scan_files_builds_relative_path | 扫描生成正确相对路径 |
| test_count_pdf_pages | PDF 页数统计 |
| test_count_xlsx_sheets | xlsx 工作表数量统计 |
| test_count_pptx_slides | pptx 幻灯片数量统计 |
| test_retry_three_times | 单文件失败重试 3 次 |
| test_uncertain_old_doc | 老 doc 元数据缺失时标记 uncertain |
### 12.2 接口测试
| 用例 | 目标 |
| --- | --- |
| test_upload_attachment_api | 上传接口返回 attachment_id |
| test_upload_permission_denied | 不能向他人对话上传文件 |
| test_stream_triggers_workflow | 发送命中提示词后返回 workflow meta |
| test_batch_status_permission | 不能查询他人批次 |
| test_export_download_permission | 不能下载他人导出文件 |
### 12.3 集成测试
| 用例 | 目标 |
| --- | --- |
| test_file_summary_zip_workflow | zip 上传后完整工作流成功 |
| test_file_summary_multi_file_workflow | 多文件上传后完整工作流成功 |
| test_single_file_failure_not_blocking | 单文件失败不阻断批次 |
| test_workflow_events_created | 节点事件按顺序写入数据库 |
| test_markdown_and_excel_exports | Markdown 与 Excel 文件生成成功 |
### 12.4 前端验证
| 用例 | 目标 |
| --- | --- |
| 拖拽上传 | 右侧上传区展示文件列表 |
| 提示词触发 | 发送“自动汇总文件目录与页数”后创建工作流卡片 |
| 状态实时更新 | SSE 事件驱动节点状态变化 |
| 页面刷新恢复 | 刷新后右侧卡片恢复当前批次状态 |
| Markdown 表格 | 对话消息中表格和下载链接正常显示 |
---
## 十三、开发顺序
1. 增加依赖与模型字段,生成迁移。
2. 实现文件上传暂存接口和存储目录策略。
3. 实现 workflow_trigger根据提示词决定是否启动工作流。
4. 实现 SkillRegistry、WorkflowExecutor 和 WorkflowEvent。
5. 实现压缩包解压、文件扫描、页数统计服务。
6. 实现 Markdown 报告与 Excel 导出。
7. 改造前端三栏布局、拖拽上传区和工作流卡片。
8. 增加 Markdown 渲染能力。
9. 补齐权限测试、工作流测试和前端手工验证。
---
## 十四、参考依据
本设计采用轻量 Python 库优先方案,依据如下:
| 能力 | 依据 |
| --- | --- |
| PDF 页数 | pypdf 的 PdfReader 可读取 pages |
| docx 元数据 | python-docx 支持 core properties |
| pptx 幻灯片 | python-pptx 可读取 presentation slides |
| xlsx 工作表 | openpyxl 可读取 workbook worksheets |
| xls 工作表 | xlrd 支持读取历史 xls 工作簿 |
| 老 Office 元数据 | olefile 可读取 OLE2 复合文档结构 |
| 7z 解压 | py7zr 支持 7z 压缩格式处理 |
| rar 解压 | rarfile 通常依赖外部 unrar/unar/7z 工具,故本设计优先系统 7z |

View File

@@ -0,0 +1,666 @@
# NMPA 注册资料法规核查与整改闭环工作流详细设计
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 需求分析文档 | docs/1.需求分析/2.NMPA注册资料法规核查与整改闭环.md |
| 功能设计文档 | docs/2.功能设计/2.NMPA注册资料法规核查与整改闭环.md |
| 数据库设计文档 | docs/4.数据库设计/2.NMPA注册资料法规核查与整改闭环.md |
| 依赖详细设计 | docs/3.详细设计/1.自动汇总.md |
| 功能名称 | NMPA 注册资料法规核查与整改闭环 |
| 所属模块 | 审核智能体 review_agent |
| 设计日期 | 2026-06-06 |
| 设计版本 | V1.0 |
---
## 一、详细设计目标
本详细设计用于指导“NMPA 注册资料法规核查与整改闭环”功能开发落地,覆盖代码结构、通用工作流改造、法规核查执行器、规则/RAG/LLM 调用边界、服务拆分、接口契约、前端交互、飞书 CLI 通知、过程产物留底、异常重试和测试建议。
核心约束:
| 约束 | 说明 |
| --- | --- |
| 复用自动汇总 | 不重复实现上传、解压、扫描和页数统计,法规核查基于 `FileSummaryBatch` 执行 |
| 独立工作流 | 法规核查有独立 `RegulatoryReviewBatch` 和卡片,事件机制与文件汇总共用 |
| 通用事件模型 | `WorkflowNodeRun``WorkflowEvent``ExportedSummaryFile` 增加 workflow_type 和 workflow_batch_id |
| 异步执行 | 启动接口立即返回 batch_id后台执行并通过 SSE 更新卡片 |
| 暂停恢复 | 遇到 waiting_user 时后台任务结束,用户确认后重新唤起执行器继续 |
| 规则优先 | 结构化规则负责合规判断RAG 只补充依据LLM 只用于低置信度字段抽取和建议润色 |
| 过程留底 | 文本抽取、RAG 结果、LLM 输出、通知和复核记录均生成过程产物 |
---
## 二、代码结构设计
### 2.1 目录结构
`review_agent` 应用内新增 `regulatory_review/` 模块。法规核查与文件汇总并列,通过共享工作流事件和导出服务协同。`review_agent/workflow/` 是对模块 1 中 `file_summary/events.py`、节点状态和导出记录能力的通用化抽取,不是为法规核查重建一套并行事件体系。
```text
review_agent/
models.py
urls.py
views.py
file_summary/
...
workflow/
__init__.py
constants.py
events.py
node_runs.py
exports.py
regulatory_review/
__init__.py
constants.py
schemas.py
urls.py
views.py
workflow.py
storage.py
services/
__init__.py
rule_loader.py
rag_citation.py
info_extract.py
text_extract.py
completeness_check.py
structure_check.py
consistency_check.py
risk_assess.py
export.py
feishu_notifier.py
rectification_review.py
condition_parser.py
rules/
nmpa_ivd_registration_v1.yaml
prompts/
condition_parse.md
field_extract.md
suggestion_polish.md
```
### 2.2 文件职责
| 文件 | 职责 |
| --- | --- |
| workflow/constants.py | 通用 workflow_type、节点状态、事件类型 |
| workflow/events.py | 通用 SSE 事件持久化和格式化 |
| workflow/node_runs.py | 通用节点状态创建、更新和恢复 |
| workflow/exports.py | 通用导出记录和下载权限校验 |
| regulatory_review/constants.py | 法规核查节点、风险等级、问题状态常量 |
| regulatory_review/schemas.py | RegulatoryContext、NodeResult、Finding 等 dataclass |
| regulatory_review/workflow.py | RegulatoryWorkflowExecutor负责编排节点和暂停恢复 |
| regulatory_review/storage.py | 法规核查过程产物路径、hash、文件保存 |
| services/rule_loader.py | 加载规则版本、校验 hash、裁剪适用规则 |
| services/rag_citation.py | 基于 findings 批量检索法规依据 |
| services/info_extract.py | 从文件清单和文本片段抽取适用条件候选值 |
| services/condition_parser.py | 将用户自然语言确认解析为结构化字段 |
| services/text_extract.py | 统一抽取关键文件文本并缓存为 JSON 产物 |
| services/completeness_check.py | 完整性核查,生成 findings |
| services/structure_check.py | 章节结构核查,生成 findings |
| services/consistency_check.py | 跨文件一致性核查,生成 findings |
| services/risk_assess.py | 去重、风险分级、RAG 依据引用、写入 RegulatoryIssue |
| services/export.py | 生成最终报告和过程产物,支持重试 |
| services/feishu_notifier.py | 通过飞书 CLI 发送通知,支持 3 次重试 |
| services/rectification_review.py | 补充资料后的问题复核和状态更新 |
---
## 三、通用工作流改造
### 3.1 WorkflowNodeRun 改造
现有节点状态表需要兼容多类工作流。
| 字段 | 处理 |
| --- | --- |
| batch_id | 保留,兼容文件汇总旧逻辑 |
| workflow_type | 新增file_summary、regulatory_review |
| workflow_batch_id | 新增,保存对应工作流批次 ID |
| node_group | 新增,可选,用于法规核查卡片主节点聚合 |
唯一约束调整为:
```text
unique(workflow_type, workflow_batch_id, node_code)
```
文件汇总旧逻辑写入时同步设置:
```text
workflow_type = file_summary
workflow_batch_id = file_summary_batch.id
batch_id = file_summary_batch.id
```
### 3.2 WorkflowEvent 改造
事件表同样新增:
| 字段 | 说明 |
| --- | --- |
| workflow_type | file_summary、regulatory_review |
| workflow_batch_id | 对应工作流批次 ID |
| conversation_id | 冗余记录对话 ID便于 SSE 查询 |
SSE 查询时按 `conversation_id` 获取多个工作流事件,前端根据 `workflow_type + workflow_batch_id` 更新对应卡片。
### 3.3 ExportedSummaryFile 改造
最终下载文件表通用化:
| 字段 | 说明 |
| --- | --- |
| workflow_type | file_summary、regulatory_review |
| workflow_batch_id | 对应工作流批次 ID |
| export_category | summary_report、risk_report、excel_list、json_package |
法规核查最终 Markdown、Excel、JSON 结果包进入 `ExportedSummaryFile`;过程产物进入 `RegulatoryArtifact`
---
## 四、核心数据结构
### 4.1 RegulatoryContext
节点间传递统一上下文,避免每个服务重复组装状态。
```python
@dataclass
class RegulatoryContext:
regulatory_batch: RegulatoryReviewBatch
file_summary_batch: FileSummaryBatch | None
rule_version: RegulatoryRuleVersion | None
rules: dict[str, Any]
scoped_rules: list[dict[str, Any]]
conditions: dict[str, Any]
file_items: list[FileSummaryItem]
text_artifacts: dict[str, Any]
findings: list["Finding"]
issues: list[RegulatoryIssue]
artifacts: list[RegulatoryArtifact]
reference_only: bool = False
```
### 4.2 NodeResult
每个节点统一返回 `NodeResult`
```python
@dataclass
class NodeResult:
status: str
message: str = ""
payload: dict[str, Any] = field(default_factory=dict)
findings: list["Finding"] = field(default_factory=list)
artifacts: list[RegulatoryArtifact] = field(default_factory=list)
next_node: str | None = None
```
### 4.3 Finding
核查服务只返回 findings不直接写 `RegulatoryIssue`。Issue 由 `RiskAssessService` 统一去重、分级和落库。
```python
@dataclass
class Finding:
finding_key: str
issue_type: str
initial_risk_level: str
title: str
description: str
rule_id: str | None = None
file_item_id: int | None = None
file_path: str | None = None
page_no: int | None = None
field_name: str | None = None
evidence: dict[str, Any] = field(default_factory=dict)
suggestion_template: str | None = None
source_node: str | None = None
```
---
## 五、工作流执行设计
### 5.1 启动流程
```text
POST /regulatory-review/start/
-> 创建 RegulatoryReviewBatch(status=pending)
-> 查找当前对话最近一次 success FileSummaryBatch
-> 如有则绑定并异步启动法规核查
-> 如无则创建 FileSummaryBatch 并启动自动汇总
-> 自动汇总 success 后回填 file_summary_batch_id
-> 继续法规核查 prepare 节点
```
如果用户明确说“重新核查最新上传资料”,系统强制创建新的 `FileSummaryBatch`,再创建新的 `RegulatoryReviewBatch`
### 5.2 暂停与恢复
当适用条件缺失或解析冲突时:
```text
RegulatoryWorkflowExecutor
-> 写入 condition_confirm 节点 status=waiting_user
-> RegulatoryReviewBatch.status=waiting_user
-> 发送 workflow SSE
-> 后台任务结束
```
用户确认后:
```text
POST /regulatory-review/{batch_id}/confirm-condition/
-> LLM 解析自然语言为结构化 JSON
-> 字段校验器校验必填字段
-> 如仍缺失,继续追问并保持 waiting_user
-> 如完整,写入 batch 核心字段和 condition_json
-> 重新唤起 RegulatoryWorkflowExecutor从 rule_scope 节点继续
```
### 5.3 节点调度
```text
prepare
-> info_extract
-> condition_confirm 或 rule_scope
-> rule_scope
-> completeness_check
-> text_extract
-> 并行执行 structure_check 和 consistency_check
-> risk_assess
-> report_export
-> notify
-> completed
```
章节核查和一致性核查通过后台线程池并行:
```python
with ThreadPoolExecutor(max_workers=2) as pool:
structure_future = pool.submit(structure_service.run, context)
consistency_future = pool.submit(consistency_service.run, context)
```
### 5.4 关键节点
关键节点失败时终止批次:
| 节点 | 失败处理 |
| --- | --- |
| prepare | 无法绑定文件汇总批次,批次 failed |
| rule_scope | 规则 hash 不一致,批次 failed规则加载失败可降级 reference_only |
| report_export | 最终报告重试失败,批次 failed |
非关键节点失败时生成 `Finding``RegulatoryIssue`,工作流尽量继续:
| 节点 | 失败处理 |
| --- | --- |
| text_extract | 对相关文件生成待确认 finding |
| structure_check | 生成章节核查失败 finding |
| consistency_check | 生成一致性待确认 finding |
| notify | 写通知失败记录,批次可 partial_success |
---
## 六、规则、RAG 与 LLM 设计
### 6.1 RuleLoader
流程:
```text
读取当前 active RegulatoryRuleVersion
-> 读取 rule_file_path
-> 计算文件 hash
-> 与 rule_file_hash 比对
-> hash 一致则解析规则
-> 按适用条件裁剪 scoped_rules
```
处理策略:
| 场景 | 处理 |
| --- | --- |
| 规则文件 hash 不一致 | 停止执行并标记 failed |
| 规则文件不存在或解析失败 | 降级 RAG 辅助核查batch.status=reference_only |
| RAG 索引版本缺失 | 记录提示项,但规则核查可继续 |
### 6.2 RagCitationService
RAG 在 `RiskAssessService` 阶段批量调用,而不是每个核查节点实时调用。
输入:
| 字段 | 说明 |
| --- | --- |
| findings | 所有核查 findings |
| rule_version | 当前法规规则版本 |
| scoped_rules | 本次适用规则 |
输出:
| 字段 | 说明 |
| --- | --- |
| citations_by_finding | finding_key 到法规依据列表的映射 |
| rag_result_json | RAG 检索结果过程产物 |
### 6.3 LLM 调用边界
| 场景 | 是否调用 LLM | 说明 |
| --- | --- | --- |
| 自然语言适用条件解析 | 是 | 解析为结构化 JSON再由字段校验器校验 |
| 低置信度字段抽取 | 是 | 规则/正则失败或置信度低时调用 |
| 整改建议润色 | 是 | 规则模板生成标准动作LLM 润色表达 |
| 风险等级判断 | 否 | 风险等级由规则和 RiskAssess 决定 |
| 法规结论判断 | 否 | 合规判断不交给 LLM |
LLM 抽取结果需写入过程产物,可使用 `llm_extract_json` 或并入 `text_extract_json`
---
## 七、服务详细设计
### 7.1 RegulatoryWorkflowExecutor
| 方法 | 说明 |
| --- | --- |
| start(batch_id) | 创建后台任务并返回 |
| run(batch_id, start_node=None) | 运行法规核查节点 |
| build_context(batch_id) | 组装 RegulatoryContext |
| run_node(node_code, context) | 执行单个节点并处理 NodeResult |
| run_parallel_checks(context) | 并行执行章节和一致性核查 |
| pause_for_user(batch, node_code, message) | 写 waiting_user 状态并结束任务 |
| complete(batch) | 标记批次完成 |
| fail(batch, error) | 标记批次失败 |
### 7.2 ConditionParserService
| 方法 | 说明 |
| --- | --- |
| parse(raw_user_input, previous_conditions) | 使用 LLM 解析自然语言 |
| validate(parsed_json) | 校验产品类别、注册类型、临床路径、产品名称、型号规格、预期用途 |
| merge(batch, parsed_json) | 写入批次字段和 condition_json |
### 7.3 RiskAssessService
| 方法 | 说明 |
| --- | --- |
| deduplicate(findings) | 按 finding_key、rule_id、file_item_id 去重 |
| attach_citations(findings) | 批量调用 RAG 获取法规依据 |
| resolve_risk(finding) | 统一风险等级,处理升级/降级 |
| generate_suggestion(finding) | 规则模板 + LLM 润色 |
| create_issues(batch, findings) | 统一写入 RegulatoryIssue |
| build_risk_summary(batch) | 写入 risk_summary_json |
### 7.4 RegulatoryExportService
| 方法 | 说明 |
| --- | --- |
| export_final_markdown(batch) | 生成最终 Markdown 核查报告 |
| export_final_excel(batch) | 生成 Excel 缺失清单 |
| export_json_package(batch) | 生成结构化 JSON 结果包 |
| create_artifact(batch, artifact_type, path) | 写 RegulatoryArtifact 并计算 hash |
| create_export_record(batch, path, category) | 写 ExportedSummaryFile |
| retry_export(fn, max_retry=3) | 导出失败重试 |
重试策略:
| 产物 | 重试后仍失败 |
| --- | --- |
| 最终 Markdown/Excel/JSON | 批次 failed |
| 非关键过程产物 | 批次 partial_success |
### 7.5 FeishuNotifier
调用方式必须使用参数数组,不拼接 shell 字符串。
```python
subprocess.run(
[cli_path, "send", "--user", feishu_user_id, "--message", message],
check=True,
capture_output=True,
text=True,
)
```
处理策略:
| 场景 | 处理 |
| --- | --- |
| 用户无 feishu_user_id | 写通知失败记录,不阻断 |
| CLI 执行失败 | 最多重试 3 次 |
| 仍失败 | send_status=failed批次可 partial_success |
| 成功 | 写 external_message_id 和 sent_at |
通知内容包含系统内风险报告链接,不附原始文件。
---
## 八、接口详细设计
### 8.1 发起法规核查
| 项目 | 内容 |
| --- | --- |
| URL | POST /api/review-agent/regulatory-review/start/ |
| 请求 | conversation_id、file_summary_batch_id 可选、force_resummary 可选 |
| 响应 | regulatory_batch_id、workflow_type、status |
响应示例:
```json
{
"regulatory_batch_id": 2001,
"workflow_type": "regulatory_review",
"status": "pending"
}
```
### 8.2 确认适用条件
| 项目 | 内容 |
| --- | --- |
| URL | POST /api/review-agent/regulatory-review/{batch_id}/confirm-condition/ |
| 请求 | raw_user_input、可选结构化字段 |
| 响应 | status、missing_fields、next_question |
如果解析完整:
```json
{
"status": "accepted",
"next_node": "rule_scope"
}
```
如果仍缺失:
```json
{
"status": "need_more_info",
"missing_fields": ["clinical_evaluation_path"],
"next_question": "请确认临床评价路径:临床试验、免临床,还是同品种比对?"
}
```
### 8.3 查询状态
| 项目 | 内容 |
| --- | --- |
| URL | GET /api/review-agent/regulatory-review/{batch_id}/ |
| 响应 | 批次、节点、风险摘要、导出文件、过程产物 |
### 8.4 发起整改复核
| 项目 | 内容 |
| --- | --- |
| URL | POST /api/review-agent/regulatory-review/{batch_id}/rectify-review/ |
| 请求 | issue_ids、file_summary_batch_id 或 uploaded_file_ids |
| 响应 | review_status、updated_issues、review_artifact_id |
补充文件必须复用自动汇总上传与汇总能力。上传后先生成新的 `FileSummaryBatch`,再由 `RectificationReviewService` 对原批次问题执行复核。复核不创建新的 `RegulatoryReviewBatch`
---
## 九、前端与对话交互
### 9.1 工作流卡片
| 设计点 | 说明 |
| --- | --- |
| 卡片切换 | 多工作流卡片使用轮播切换 |
| 卡片识别 | 使用 workflow_type + workflow_batch_id |
| 状态来源 | SSE workflow 事件 |
| 法规卡片 | 展示主节点和可展开子节点 |
| waiting_user | 卡片显示等待确认,对话框给出选择和追问 |
### 9.2 自然语言确认
对话框中用户可以用自然语言确认,例如:
```text
按体外诊断试剂首次注册处理,临床评价路径走同品种比对,产品名称是 XXX型号规格是 YYY预期用途是 ZZZ。
```
后端解析并校验后继续工作流。原始输入写入 `condition_json.raw_user_input`
### 9.3 整改复核触发
Demo 阶段通过对话指令触发:
```text
我已补充注册检验报告,请复核阻断项。
```
系统识别后调用复核接口,要求用户上传补充文件或选择已上传文件。
---
## 十、过程产物与报告
### 10.1 文件命名
过程产物和最终报告采用固定模板:
```text
{batch_no}_{artifact_type}.{ext}
```
示例:
```text
RRB202606060001_rule_matrix.xlsx
RRB202606060001_risk_list.json
RRB202606060001_final_report.md
```
### 10.2 文件保存
路径:
```text
media/regulatory_review/{user_id}/{conversation_id}/{batch_id}/
```
所有 `RegulatoryArtifact` 必须计算 SHA-256 hash。
### 10.3 报告内容
最终 Markdown 报告包含:
| 模块 | 说明 |
| --- | --- |
| 核查概览 | 批次、规则版本、RAG 版本、上传人 |
| 适用条件 | 系统抽取和用户确认结果 |
| 风险清单 | 五级风险、状态、责任人、建议 |
| 法规核查矩阵 | 应有文件、实际文件、缺失情况 |
| 章节核查结果 | 缺失章节、异常章节 |
| 一致性核查结果 | 字段冲突和来源文件 |
| 飞书通知记录 | 发送对象、状态、失败原因 |
| 整改复核记录 | 复核方式、复核结果、关闭确认 |
---
## 十一、异常与重试
| 场景 | 处理 |
| --- | --- |
| 无成功 FileSummaryBatch | 自动启动文件汇总,成功后继续 |
| 文件汇总失败 | 法规核查批次 failed |
| 规则 hash 不一致 | 法规核查批次 failed |
| 规则加载失败 | 降级 reference_only仅输出参考性结果 |
| 用户确认信息缺失 | waiting_user追问缺失字段 |
| 文本抽取失败 | 生成待确认 finding继续后续节点 |
| 章节或一致性节点失败 | 生成对应 issue继续风险汇总 |
| RAG 检索无结果 | 规则问题仍输出,依据标记原文待补充 |
| LLM 调用失败 | 回退规则/正则结果,低置信度项待确认 |
| 飞书失败 | 重试 3 次,仍失败写通知失败记录 |
| 最终报告导出失败 | 重试 3 次,仍失败 batch failed |
| 非关键产物导出失败 | 重试 3 次,仍失败 batch partial_success |
---
## 十二、测试建议
### 12.1 单元测试
| 模块 | 测试点 |
| --- | --- |
| RuleLoader | hash 校验、规则解析、规则裁剪、加载失败降级 |
| ConditionParserService | 自然语言解析、缺失字段追问、原始输入留痕 |
| TextExtractService | 首页文本、章节文本、抽取失败产物 |
| CompletenessCheckService | 文件名/目录名/首页内容三层匹配 |
| StructureCheckService | 必需章节缺失识别 |
| ConsistencyCheckService | 字段冲突、低置信度 LLM 辅助 |
| RiskAssessService | findings 去重、风险升级/降级、Issue 落库 |
| RegulatoryExportService | 文件命名、hash、导出重试 |
| FeishuNotifier | 参数数组调用、3 次重试、失败记录 |
### 12.2 集成测试
| 场景 | 验证 |
| --- | --- |
| 已有汇总批次发起核查 | 默认复用最近 success 批次 |
| 无汇总批次发起核查 | 自动串联文件汇总后继续 |
| waiting_user 暂停恢复 | 用户确认后从 rule_scope 继续 |
| 章节和一致性并行 | 两个节点均完成后进入 risk_assess |
| 规则加载失败 | batch.status=reference_only |
| 飞书失败 | 不阻断报告,通知记录 failed |
| 补充文件复核 | 新 FileSummaryBatch 生成,原 Issue 状态更新 |
### 12.3 验收测试
| 序号 | 验收项 | 标准 |
| --- | --- | --- |
| 1 | 多工作流卡片 | 文件汇总和法规核查卡片可切换且状态独立 |
| 2 | 条件确认 | 用户自然语言确认后能结构化入库 |
| 3 | 完整性核查 | 能识别缺失注册检验报告等问题 |
| 4 | 章节核查 | 能识别关键章节缺失 |
| 5 | 一致性核查 | 能识别产品名称、型号规格、预期用途冲突 |
| 6 | 风险报告 | 输出 Markdown、Excel、JSON 结果包 |
| 7 | 飞书通知 | 阻断项、高风险、中风险能 @ 上传人 |
| 8 | 过程留底 | RAG、文本抽取、通知、复核均有 artifact |
| 9 | 整改复核 | 补充文件后原 Issue 可进入复核通过或复核不通过 |
---
## 十三、实施顺序建议
结合当前优先级,建议先打通 RAG 和 LLM 能力,再落完整工作流:
1. 构建本地法规材料 RAG 索引,并实现 `RagCitationService`
2. 实现适用条件解析和低置信度字段抽取的 LLM 调用封装。
3. 完成数据库模型和通用 workflow/export 表改造。
4. 实现 `RuleLoader` 与规则 hash 校验。
5. 实现 `RegulatoryWorkflowExecutor``RegulatoryContext``NodeResult`
6. 实现完整性、文本抽取、章节核查、一致性核查和风险归并。
7. 实现报告导出、过程产物 hash 和导出重试。
8. 接入飞书 CLI 通知和 3 次重试。
9. 改造前端多工作流卡片和适用条件确认交互。
10. 实现整改复核和 Issue 状态流转。

View File

@@ -0,0 +1,790 @@
# 产品关键信息提取与申报文件自动填表详细设计
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 需求分析文档 | docs/1.需求分析/3.产品关键信息提取与申报文件自动填表.md |
| 功能设计文档 | docs/2.功能设计/3.产品关键信息提取与申报文件自动填表.md |
| 数据库设计文档 | docs/4.数据库设计/3.产品关键信息提取与申报文件自动填表.md |
| 依赖详细设计 | docs/3.详细设计/1.自动汇总.mddocs/3.详细设计/2.NMPA注册资料法规核查与整改闭环.md |
| 功能名称 | 产品关键信息提取与申报文件自动填表 |
| 所属模块 | 审核智能体 review_agent |
| 设计日期 | 2026-06-07 |
| 设计版本 | V1.0 |
---
## 一、详细设计目标
本详细设计用于指导“产品关键信息提取与申报文件自动填表”功能开发落地覆盖代码结构、数据库模型、模板配置、独立工作流、字段抽取、字段合并、Word 模板填充、追溯清单导出、飞书通知、接口契约、前端卡片、异常降级和测试建议。
核心约束:
| 约束 | 说明 |
| --- | --- |
| 独立工作流 | 使用 `workflow_type=application_form_fill`,拥有独立批次和卡片 |
| 对话触发 | 由用户自然语言触发,可指定模板;未指定时按注册类型选择适用模板 |
| 文件来源复用 | 默认使用当前对话最近成功的 `FileSummaryBatch`;本次带附件时先执行自动汇总 |
| 模板配置驱动 | 模板路径、字段映射、适用条件写入 `application_form_fill/templates/application_form_templates_v1.yaml` |
| Word 优先 | Demo 阶段主链路只要求生成 Word 和追溯清单 |
| PDF 待办 | PDF 转换节点保留,但本期可标记 skipped 并写入待办计划 |
| 抽取并行 | 规则/正则抽取与 LLM 结构化抽取并行执行,再统一合并 |
| 冲突可见 | 说明书优先;冲突字段写入 Word 时黄底红字,并在对话框展示摘要 |
| 过程留底 | 规则抽取、LLM 抽取、合并结果、冲突和追溯清单均保存产物 |
| 飞书通知 | 填表完成后通知上传人,通知失败不阻断下载 |
---
## 二、代码结构设计
### 2.1 目录结构
第三批独立为 `review_agent/application_form_fill/` 模块。Django 模型仍集中在 `review_agent/models.py`,业务服务放入独立模块。
```text
review_agent/
models.py
services.py
skill_router.py
application_form_fill/
__init__.py
constants.py
schemas.py
storage.py
workflow.py
views.py
services/
__init__.py
template_config.py
template_select.py
template_repository.py
field_extract.py
field_merge.py
word_fill.py
traceability_export.py
notifier.py
templates/
application_form_templates_v1.yaml
prompts/
field_extract.md
checklist_extract.md
```
### 2.2 文件职责
| 文件 | 职责 |
| --- | --- |
| application_form_fill/constants.py | 工作流节点、模板编码、状态、输出类型常量 |
| application_form_fill/schemas.py | FormFillContext、TemplateSpec、ExtractedField、MergedField 等 dataclass |
| application_form_fill/storage.py | 批次工作目录、模板副本、产物保存、hash 计算 |
| application_form_fill/workflow.py | FormFillWorkflowExecutor串行执行独立填表工作流 |
| application_form_fill/views.py | 启动、状态查询、后续可选下载或重试接口 |
| services/template_config.py | 读取和校验 YAML 模板配置 |
| services/template_select.py | 解析用户指定模板、识别注册类型、选择模板 |
| services/template_repository.py | 定位原始模板、复制模板、`.doc``.docx` 预留 |
| services/field_extract.py | 规则/正则与 LLM 并行字段抽取 |
| services/field_merge.py | 字段归一化、来源排序、冲突识别、最终字段输出 |
| services/word_fill.py | 使用 `python-docx` 写入 Word 表格、段落和高亮 |
| services/traceability_export.py | 生成 Excel/JSON 追溯清单,创建导出记录 |
| services/notifier.py | 包装飞书通知,生成通知记录 |
| prompts/field_extract.md | LLM 字段抽取提示词 |
| prompts/checklist_extract.md | 安全和性能基本原则清单条目判断提示词 |
---
## 三、依赖设计
### 3.1 Python 依赖
| 依赖 | 用途 | 当前项目情况 |
| --- | --- | --- |
| Django | Web、ORM、权限 | 已使用 |
| python-docx | Word 模板读取、表格填充、字体和底色设置 | 已在项目依赖链中使用 |
| openpyxl | 字段来源追溯清单 Excel 导出 | 已使用 |
| PyYAML | YAML 模板配置读取 | 已用于法规规则 |
| pypdf / python-pptx | 文本抽取链路复用 | 已使用 |
| LibreOffice/soffice | `.doc``.docx`、PDF 转换 | 本期非强依赖,后续待办 |
### 3.2 技术边界
| 能力 | 本期实现 | 后续增强 |
| --- | --- | --- |
| `.docx` 模板填充 | 必须支持 | 支持内容控件、复杂 OOXML patch |
| `.doc` 模板处理 | 可通过预转换模板或标记失败 | 自动 LibreOffice 转换 |
| PDF 转换 | 可跳过并提示待生成 | LibreOffice 转 PDF + 视觉 QA |
| 字段级入库 | 不做 | 新增字段明细表和在线编辑 |
| LLM 抽取 | 输出 JSON 并留底 | 增加置信度校准和人工确认 |
---
## 四、数据模型详细设计
模型放在 `review_agent/models.py`
### 4.1 ApplicationFormFillBatch
```python
class ApplicationFormFillBatch(models.Model):
class Status(models.TextChoices):
PENDING = "pending", "待执行"
RUNNING = "running", "执行中"
WAITING_USER = "waiting_user", "等待用户"
SUCCESS = "success", "成功"
PARTIAL_SUCCESS = "partial_success", "部分成功"
FAILED = "failed", "失败"
CANCELLED = "cancelled", "已取消"
```
关键字段:
| 字段 | 说明 |
| --- | --- |
| conversation | 绑定对话 |
| user | 发起用户 |
| trigger_message | 触发消息 |
| source_summary_batch | 文件来源批次 |
| source_regulatory_batch | 可选法规核查批次 |
| batch_no | `AFF-YYYYMMDDHHMMSS-abcdef` |
| requested_templates | 用户指定模板 |
| selected_templates | 实际生成模板 |
| output_types | 本次请求输出类型Demo 默认 `["word", "excel", "json"]` |
| registration_type | 识别出的注册类型 |
| registration_type_source | 注册类型来源 |
| product_name | 产品名称 |
| conflict_summary | 冲突摘要 |
| risk_notes | 不适用模板、PDF 待生成等提示 |
| template_config_version | 模板配置版本 |
| template_config_hash | 模板配置 hash |
| work_dir | 批次工作目录 |
### 4.2 ApplicationFormFillArtifact
用于保存过程产物和模板副本元数据。
```python
class ApplicationFormFillArtifact(models.Model):
class ArtifactType(models.TextChoices):
TEMPLATE_COPY = "template_copy", "模板副本"
FIELD_EXTRACT_RESULT = "field_extract_result", "字段抽取结果"
MERGED_FIELDS = "merged_fields", "字段合并结果"
TRACEABILITY = "traceability", "追溯清单"
FILLED_TEMPLATE = "filled_template", "已填模板"
NOTIFICATION_RECORD = "notification_record", "通知记录"
```
### 4.3 ApplicationFormFillNotificationRecord
通知记录字段与第二批法规通知风格一致,支持重试:
| 字段 | 说明 |
| --- | --- |
| batch | 自动填表批次 |
| recipient | 通知对象 |
| channel | feishu_cli、feishu_api、mock |
| template_codes | 涉及模板 |
| export_ids | 关联下载文件 |
| message_summary | 通知摘要 |
| send_status | pending、success、failed |
| retry_count | 重试次数 |
| external_message_id | 飞书外部消息 ID |
| error_message | 失败原因 |
| sent_at | 发送成功时间 |
### 4.4 ExportedSummaryFile 扩展
`ExportedSummaryFile.ExportType` 增加:
```python
WORD = "word", "Word"
PDF = "pdf", "PDF"
```
填表导出记录使用:
| 字段 | 值 |
| --- | --- |
| workflow_type | application_form_fill |
| workflow_batch_id | ApplicationFormFillBatch.id |
| export_category | filled_template、traceability、extract_result |
| export_type | word、excel、json、pdf |
---
## 五、常量设计
### 5.1 工作流节点
```python
FORM_FILL_NODE_DEFINITIONS = [
("prepare", "准备资料", "form_fill"),
("template_select", "选择模板", "form_fill"),
("template_copy", "复制模板", "form_fill"),
("field_extract", "抽取字段", "form_fill"),
("conflict_merge", "冲突归并", "form_fill"),
("word_fill", "填写 Word", "form_fill"),
("pdf_convert", "转换 PDF", "form_fill"),
("trace_export", "追溯清单", "form_fill"),
("output_export", "输出下载", "form_fill"),
("notify", "飞书通知", "form_fill"),
("completed", "完成", "completed"),
]
```
### 5.2 模板编码
```python
TEMPLATE_REGISTRATION_CERTIFICATE = "registration_certificate"
TEMPLATE_CHANGE_REGISTRATION = "change_registration"
TEMPLATE_ESSENTIAL_PRINCIPLES = "essential_principles"
```
### 5.3 触发关键词
```python
FORM_FILL_TRIGGER_KEYWORDS = [
"填注册证",
"对应的表格",
"生成申报模板",
"安全和性能基本原则清单",
"填到申报模板",
"自动填表",
"生成表格",
]
```
---
## 六、核心数据结构
### 6.1 FormFillContext
```python
@dataclass
class FormFillContext:
batch: ApplicationFormFillBatch
source_summary_batch: FileSummaryBatch
source_regulatory_batch: RegulatoryReviewBatch | None
template_config: dict[str, Any]
selected_templates: list["TemplateSpec"]
document_texts: dict[str, str]
regex_results: dict[str, Any]
llm_results: dict[str, Any]
merged_fields: dict[str, "MergedField"]
checklist_items: dict[str, Any]
conflicts: list[dict[str, Any]]
exports: list[ExportedSummaryFile]
```
### 6.2 TemplateSpec
```python
@dataclass(frozen=True)
class TemplateSpec:
code: str
name: str
source_file: str
output_label: str
applies_when: dict[str, Any]
file_format: str
fields: list[dict[str, Any]]
checklist_items: list[dict[str, Any]]
```
### 6.3 ExtractedField
```python
@dataclass(frozen=True)
class ExtractedField:
key: str
label: str
value: str
source_file: str
source_role: str
evidence: str
extractor: str
confidence: float
```
### 6.4 MergedField
```python
@dataclass(frozen=True)
class MergedField:
key: str
label: str
value: str
source_file: str
evidence: str
confidence: float
has_conflict: bool = False
conflict_values: list[dict[str, Any]] = field(default_factory=list)
```
---
## 七、模板配置详细设计
### 7.1 配置路径
```text
review_agent/application_form_fill/templates/application_form_templates_v1.yaml
```
### 7.2 初始配置示例
```yaml
version: application_form_templates_v1
source_dir: docs/0.原始材料/关于公布体外诊断试剂注册申报资料要求和批准证明文件格式的公告
templates:
- code: registration_certificate
name: 中华人民共和国医疗器械注册证(体外诊断试剂)(格式)
source_file: 中华人民共和国医疗器械注册证(体外诊断试剂)(格式).docx
output_label: 注册证格式
applies_when:
registration_type: ["首次注册"]
file_format: docx
fields:
- key: applicant_name
label: 注册人名称
target:
type: table_row
row_label: 注册人名称
source_roles: ["申请表", "说明书", "企业信息"]
- key: product_name
label: 产品名称
target:
type: table_row
row_label: 产品名称
source_roles: ["说明书", "产品技术要求", "注册检验报告"]
- key: intended_use
label: 预期用途
target:
type: table_row
row_label: 预期用途
source_roles: ["说明书", "临床评价资料", "产品技术要求"]
```
### 7.3 配置校验
`TemplateConfigService` 启动时校验:
| 校验项 | 失败处理 |
| --- | --- |
| version 存在 | 批次 failed |
| source_dir 存在 | 批次 failed |
| templates 非空 | 批次 failed |
| code 唯一 | 批次 failed |
| source_file 存在 | 对应模板不可用 |
| target.type 支持 | 对应字段跳过并记录 |
---
## 八、服务详细设计
### 8.1 TemplateConfigService
```python
def load_template_config() -> dict:
"""读取 YAML 模板配置。"""
def validate_template_config(config: dict) -> list[str]:
"""返回配置错误列表。"""
def compute_config_hash(path: Path) -> str:
"""计算模板配置 SHA-256。"""
```
### 8.2 TemplateSelectionService
```python
def parse_requested_templates(message: str) -> list[str]:
"""从用户话语中识别指定模板。"""
def detect_registration_type(batch: ApplicationFormFillBatch, message: str) -> tuple[str, str]:
"""按用户话语、法规核查批次、文件抽取结果识别注册类型及来源。"""
def select_templates(
config: dict,
requested_templates: list[str],
registration_type: str,
) -> tuple[list[TemplateSpec], list[dict]]:
"""输出模板列表和风险提示。"""
```
注册类型优先级:
```text
用户话语明确指定
-> source_regulatory_batch.condition_json / confirmed_conditions
-> source_summary_batch 文件内容抽取候选
-> unknown
```
### 8.3 TemplateRepository
```python
def resolve_source_template(spec: TemplateSpec) -> Path:
"""返回原始模板路径或预转换工作模板路径。"""
def copy_template_to_batch(spec: TemplateSpec, batch: ApplicationFormFillBatch) -> Path:
"""复制模板到批次 work_dir/templates。"""
def convert_doc_to_docx(source: Path, target_dir: Path) -> Path:
"""P1 能力:使用 soffice 转 docx。"""
```
`.doc` 模板本期处理:
| 场景 | 处理 |
| --- | --- |
| 存在 working_template docx | 使用工作模板 |
| 仅有 `.doc` 且无 soffice | 对应模板失败,其他模板继续 |
| 具备 soffice | 转换为 `.docx` 后继续 |
### 8.4 FieldExtractionService
```python
def collect_document_texts(summary_batch: FileSummaryBatch) -> dict[str, str]:
"""复用 text_extract 读取文件文本。"""
def extract_by_rules(texts: dict[str, str], specs: list[TemplateSpec]) -> dict:
"""规则/正则抽取字段。"""
def extract_by_llm(texts: dict[str, str], specs: list[TemplateSpec]) -> dict:
"""LLM 结构化抽取字段。"""
def run_parallel_extract(texts: dict[str, str], specs: list[TemplateSpec]) -> tuple[dict, dict]:
"""并行执行规则/正则与 LLM 抽取。"""
```
并行实现可使用 `ThreadPoolExecutor(max_workers=2)`。LLM 超时或失败时,保留规则/正则结果继续。
### 8.5 FieldMergeService
```python
def normalize_field_value(value: str) -> str:
"""字段值归一化。"""
def rank_source(source_role: str, source_file: str) -> int:
"""说明书优先,其次产品技术要求、检测报告、性能研究等。"""
def merge_fields(regex_results: dict, llm_results: dict) -> tuple[dict[str, MergedField], list[dict]]:
"""合并字段并输出冲突。"""
```
来源优先级:
| 排名 | 来源 |
| --- | --- |
| 1 | 说明书 |
| 2 | 产品技术要求 |
| 3 | 注册检验报告/检测报告 |
| 4 | 性能研究资料 |
| 5 | 其他注册资料 |
### 8.6 WordTemplateFillService
```python
def fill_template(
template_path: Path,
output_path: Path,
spec: TemplateSpec,
fields: dict[str, MergedField],
checklist_items: dict[str, Any],
) -> Path:
"""填充 Word 模板并保存。"""
def fill_table_row(document: Document, row_label: str, value: str, conflict: bool) -> bool:
"""根据表格行首字段名定位并填入第二列。"""
def replace_placeholders(document: Document, fields: dict[str, MergedField]) -> None:
"""替换段落中的 {{field_key}}。"""
def apply_conflict_style(cell_or_run) -> None:
"""应用黄色底色和红色字体。"""
```
冲突样式:
| 样式 | 说明 |
| --- | --- |
| 字体颜色 | 红色 `FF0000` |
| 底色 | 黄色 `FFFF00` |
| 适用范围 | 单元格或字段值 run |
### 8.7 TraceabilityExportService
```python
def build_traceability_workbook(batch, merged_fields, conflicts, specs) -> Workbook:
"""生成追溯清单 Excel。"""
def save_traceability_excel(batch, workbook) -> ExportedSummaryFile:
"""保存 Excel 并写导出记录。"""
def save_extract_json(batch, payload: dict) -> ApplicationFormFillArtifact:
"""保存字段抽取 JSON 过程产物。"""
```
Excel Sheet
| Sheet | 内容 |
| --- | --- |
| 字段追溯 | 模板、字段、填入值、来源文件、证据、冲突状态 |
| 冲突字段 | 字段、采用值、冲突值、处理方式 |
| 低置信度条目 | 安全和性能基本原则清单候选判断 |
| 生成结果 | 模板文件、Word 状态、PDF 状态、错误说明 |
### 8.8 FormFillNotifier
```python
def notify_completion(batch: ApplicationFormFillBatch, exports: list[ExportedSummaryFile]) -> ApplicationFormFillNotificationRecord:
"""发送填表完成通知。"""
```
通知摘要包含:
| 内容 | 说明 |
| --- | --- |
| 批次号 | 填表批次 |
| 产品名称 | 如已识别 |
| 生成模板 | 模板名称列表 |
| 冲突数量 | 提示需下载核对 |
| 下载提示 | 提示回到系统对话下载,不直接暴露敏感全文 |
---
## 九、工作流执行器详细设计
### 9.1 启动入口
```python
def start_application_form_fill_workflow(batch: ApplicationFormFillBatch, *, async_run: bool = True) -> None:
executor = FormFillWorkflowExecutor(batch)
if async_run:
Thread(target=executor.run, daemon=True).start()
else:
executor.run()
```
### 9.2 执行伪代码
```python
class FormFillWorkflowExecutor:
def run(self) -> None:
self.mark_batch_running()
try:
for node in self.nodes():
if node.status == "success":
continue
self.run_node(node)
self.complete_or_partial()
except WorkflowPausedForUser:
self.mark_waiting_user()
except Exception as exc:
self.mark_failed(exc)
```
### 9.3 节点处理要点
| 节点 | 处理 |
| --- | --- |
| prepare | 校验 `source_summary_batch` 成功且属于当前对话 |
| template_select | 读取 YAML、识别注册类型、选择模板 |
| template_copy | 复制模板到 `work_dir/templates` |
| field_extract | 抽取文本,规则/正则与 LLM 并行,保存 JSON |
| conflict_merge | 合并字段,写 `conflict_summary` |
| word_fill | 逐模板生成 Word`ExportedSummaryFile(word)` |
| pdf_convert | 本期 skipped`risk_notes` |
| trace_export | 生成追溯 Excel 和 JSON |
| output_export | 生成 AI 对话 Markdown 摘要 |
| notify | 写飞书通知记录,失败不阻断 |
| completed | 标记 success 或 partial_success |
### 9.4 批次状态决策
| 条件 | 状态 |
| --- | --- |
| 所有目标 Word 均成功,追溯清单成功,通知成功或跳过 | success |
| 至少一个 Word 成功但部分模板、追溯清单、PDF 或通知失败 | partial_success |
| 所有目标 Word 均失败 | failed |
| 无来源文件汇总批次 | waiting_user |
---
## 十、接口详细设计
### 10.1 发起自动填表
```text
POST /api/review-agent/application-form-fill/start/
```
请求:
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| conversation_id | integer | 是 | 当前对话 |
| message_id | integer | 否 | 触发消息 |
| file_summary_batch_id | integer | 否 | 指定文件来源批次 |
| template_codes | array | 否 | 指定模板 |
| output_types | array | 否 | 输出类型,默认 word、excel、json |
响应:
```json
{
"batch_id": 3001,
"workflow_type": "application_form_fill",
"status": "pending",
"selected_templates": ["registration_certificate", "essential_principles"]
}
```
### 10.2 查询状态
```text
GET /api/review-agent/application-form-fill/{batch_id}/
```
响应:
```json
{
"batch": {
"id": 3001,
"batch_no": "AFF-20260607153000-a1b2c3",
"status": "success",
"product_name": "甲胎蛋白检测试剂盒",
"selected_templates": ["registration_certificate"]
},
"nodes": [],
"conflicts": [],
"exports": []
}
```
### 10.3 下载文件
继续复用既有导出下载接口:
```text
GET /api/review-agent/file-summary/exports/{export_id}/download/
```
下载权限通过 `workflow_type=application_form_fill``workflow_batch_id` 反查填表批次。
---
## 十一、前端详细设计
### 11.1 工作流卡片
新增卡片类型 `application_form_fill`
| 节点 | 展示 |
| --- | --- |
| prepare | 准备资料 |
| template_select | 选择模板 |
| template_copy | 复制模板 |
| field_extract | 抽取字段 |
| conflict_merge | 冲突归并 |
| word_fill | 填写 Word |
| pdf_convert | 转换 PDF |
| trace_export | 追溯清单 |
| output_export | 输出下载 |
| notify | 飞书通知 |
| completed | 已完成 |
PDF 本期显示为“已跳过/待增强”,不显示为失败。
### 11.2 AI 回复摘要
```markdown
已生成申报模板自动填表文件。
| 文件 | Word | PDF |
| --- | --- | --- |
| 注册证格式 | 下载 | 待增强 |
| 安全和性能基本原则清单 | 下载 | 待增强 |
| 冲突字段 | 采用值 | 冲突来源 | 处理 |
| --- | --- | --- | --- |
| 储存条件 | 2-8℃保存 | 产品技术要求:-20℃保存 | 已按说明书填入,并在模板中高亮 |
[下载字段来源追溯清单](download-url)
```
---
## 十二、异常与降级
| 场景 | 处理 |
| --- | --- |
| 无成功汇总批次 | 批次 waiting_user对话提示上传资料 |
| 模板配置不存在 | 批次 failed |
| 指定模板不存在 | 忽略无效模板并提示;若无有效模板则 failed |
| `.doc` 模板无可用工作模板 | 该模板失败,其他模板继续 |
| 文本抽取失败 | 对应文件跳过,记录在追溯清单 |
| LLM 抽取失败 | 使用规则/正则结果继续 |
| 字段缺失 | Word 留空 |
| 字段冲突 | 说明书优先并高亮 |
| 追溯清单失败 | Word 成功时批次 partial_success |
| 飞书通知失败 | 批次 partial_success 或 success取决于核心产物是否成功 |
| PDF 未实现 | 节点 skipped写入待增强提示 |
---
## 十三、测试设计
### 13.1 单元测试
| 用例 | 目标 |
| --- | --- |
| test_form_fill_trigger_keywords | 触发语句识别为自动填表 |
| test_template_config_loads | YAML 配置可加载并校验 |
| test_select_default_templates_initial_registration | 首次注册默认选择注册证和基本原则清单 |
| test_select_user_requested_mismatch | 用户指定不适用模板仍允许生成并提示 |
| test_field_merge_prefers_instructions | 说明书字段优先 |
| test_field_merge_marks_conflict | 冲突字段进入 conflict_summary |
| test_word_fill_table_row | 能按表格行名写入 Word |
| test_word_fill_conflict_highlight | 冲突字段黄底红字 |
| test_traceability_excel | 追溯清单包含字段、来源和冲突 |
| test_notify_records_failure | 飞书失败写通知记录但不阻断 |
### 13.2 集成测试
| 场景 | 验证 |
| --- | --- |
| 最近汇总批次触发填表 | 无附件时复用最近 success `FileSummaryBatch` |
| 新附件触发填表 | 先自动汇总再启动填表 |
| 注册证模板填充 | 生成 Word 导出文件 |
| LLM 失败降级 | LLM 超时后规则抽取仍可生成 Word |
| 部分模板失败 | 至少一个 Word 成功时批次 partial_success |
| 权限隔离 | 不能查询或下载他人填表批次产物 |
### 13.3 前端验证
| 场景 | 验证 |
| --- | --- |
| 自动填表卡片 | 节点状态随 SSE 更新 |
| 指定模板展示 | 卡片展示本次选择模板 |
| PDF 跳过显示 | PDF 节点显示待增强而非失败 |
| 下载链接 | Word 和追溯清单链接可点击下载 |
| 冲突摘要 | 冲突字段表格正常渲染 |
---
## 十四、实施顺序建议
1. 修改功能设计中的模板配置路径为 `review_agent/application_form_fill/templates/application_form_templates_v1.yaml`
2. 新增数据库模型和 `ExportedSummaryFile.ExportType` 扩展。
3. 新增 `application_form_fill` 模块目录和常量、schemas、storage。
4. 新增模板配置 YAML先录入注册证 `.docx` 的已识别字段。
5. 实现模板选择、模板复制和 Word 表格行填充。
6. 实现规则/正则字段抽取和 LLM 抽取降级。
7. 实现字段合并、冲突高亮和追溯清单。
8. 实现工作流执行器、节点事件和状态接口。
9. 改造路由和前端工作流卡片。
10. 接入飞书通知记录。
11. 将字段级数据库表和 PDF 转换写入待办计划。

View File

@@ -0,0 +1,604 @@
# 飞书通知与问答接入详细设计
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 需求分析文档 | docs/1.需求分析/4.飞书通知与问答接入.md |
| 功能设计文档 | docs/2.功能设计/4.飞书通知与问答接入.md |
| 数据库设计文档 | docs/4.数据库设计/4.飞书通知与问答接入.md |
| 所属模块 | 审核智能体 review_agent |
| 设计日期 | 2026-06-07 |
| 设计版本 | V1.0 |
---
## 一、实现目标
首期实现一个统一飞书通知能力,使自动汇总、法规核查、自动填表三个工作流在完成、部分成功或失败时,通过飞书官方智能体/应用机器人消息 API 向指定个人账号发送富文本私聊通知。通知失败不阻断主流程,发送结果落库并在批次详情页展示。
同时预留飞书私聊问答所需的用户映射、查询服务、权限过滤和问答日志模型,但不实现飞书事件订阅回调。
---
## 二、推荐文件结构
| 文件 | 类型 | 责任 |
| --- | --- | --- |
| `review_agent/models.py` | 修改 | 新增 `FeishuUserMapping``WorkflowNotificationRecord``FeishuQuestionLog` |
| `review_agent/admin.py` | 修改/新增 | 注册飞书用户映射和通知记录后台 |
| `review_agent/notifications/__init__.py` | 新增 | 通知模块包 |
| `review_agent/notifications/context.py` | 新增 | 定义统一通知上下文 dataclass |
| `review_agent/notifications/recipient.py` | 新增 | 解析首期指定个人接收人;后续扩展为按系统用户映射解析 |
| `review_agent/notifications/message_builder.py` | 新增 | 构造飞书富文本 payload 和摘要 |
| `review_agent/notifications/feishu_token.py` | 新增 | 使用 App ID/App Secret 获取并缓存 tenant_access_token |
| `review_agent/notifications/feishu_message_api.py` | 新增 | 调用飞书发送消息 API、处理响应解析 |
| `review_agent/notifications/records.py` | 新增 | 判重和通知记录落库 |
| `review_agent/notifications/dispatcher.py` | 新增 | 对外统一发送入口 |
| `review_agent/notifications/workflow_adapters.py` | 新增 | 三个工作流批次到通知上下文的适配 |
| `review_agent/feishu_questions/query.py` | 新增 | 后续问答预留:批次摘要查询 |
| `review_agent/feishu_questions/permissions.py` | 新增 | 后续问答预留:权限过滤 |
| `tests/test_feishu_notification.py` | 新增 | 飞书通知单元测试 |
| `tests/test_feishu_question_reserved.py` | 新增 | 问答预留服务测试 |
---
## 三、数据结构设计
### 3.1 NotificationContext
```python
from dataclasses import dataclass, field
from typing import Any
@dataclass(frozen=True)
class NotificationContext:
workflow_type: str
workflow_batch_id: int
workflow_batch_no: str
workflow_status: str
title: str
trigger_user_id: int
trigger_username: str
result_url: str
summary_lines: list[str] = field(default_factory=list)
next_action: str = ""
metadata: dict[str, Any] = field(default_factory=dict)
@property
def dedupe_key(self) -> str:
return f"{self.workflow_type}:{self.workflow_batch_id}:{self.workflow_status}"
```
### 3.2 ResolvedFeishuTarget
```python
from dataclasses import dataclass
@dataclass(frozen=True)
class ResolvedFeishuTarget:
mapping_id: int | None
display_name: str
identifier_type: str
identifier_value: str
masked_identifier: str
missing: bool = False
```
identifier_type 取值:
| 值 | 说明 |
| --- | --- |
| open_id | 使用飞书 open_id |
| user_id | 使用飞书 user_id |
| mobile | 使用手机号,后续按发起人私聊时使用 |
| missing | 未配置映射 |
---
## 四、模型详细设计
### 4.1 FeishuUserMapping
字段见数据库设计。模型需提供方法:
```python
def preferred_identifier(self) -> tuple[str, str]:
if self.feishu_open_id:
return "open_id", self.feishu_open_id
if self.feishu_user_id:
return "user_id", self.feishu_user_id
if self.feishu_mobile:
return "mobile", self.feishu_mobile
return "missing", ""
```
`clean()` 校验:
```python
def clean(self):
if not (self.feishu_open_id or self.feishu_user_id or self.feishu_mobile):
raise ValidationError("feishu_open_id、feishu_user_id、feishu_mobile 至少填写一个")
```
### 4.2 WorkflowNotificationRecord
字段见数据库设计。建议方法:
```python
@classmethod
def already_sent(cls, dedupe_key: str) -> bool:
return cls.objects.filter(dedupe_key=dedupe_key, send_status=cls.SendStatus.SUCCESS).exists()
```
注意:若使用唯一约束限制 `dedupe_key`,重复触发时可以直接返回已有记录;若希望保留 skipped_duplicate 记录,则不能对 dedupe_key 做全局唯一,只能用查询判重。本项目需求是“只发一次”,更推荐保留唯一成功意图,重复触发返回已有记录或创建 skipped 记录需在实现计划中二选一。为了 SQLite 简化,首期建议不创建 skipped 记录,直接返回已有成功记录。
---
## 五、核心服务详细设计
### 5.1 workflow_adapters.py
职责:把不同批次对象转换为 `NotificationContext`
函数:
```python
def build_file_summary_context(batch: FileSummaryBatch) -> NotificationContext: ...
def build_regulatory_review_context(batch: RegulatoryReviewBatch) -> NotificationContext: ...
def build_application_form_fill_context(batch: ApplicationFormFillBatch) -> NotificationContext: ...
```
自动汇总摘要:
| 字段 | 计算方式 |
| --- | --- |
| 文件总数 | `batch.items.count()` |
| 成功解析数 | 解析状态为 success 的 item 数 |
| 异常数 | failed、skipped、unsupported 等状态数量 |
| 导出文件数 | `ExportedSummaryFile` 中 workflow_type=file_summary 或 batch 关联文件数 |
法规核查摘要:
| 字段 | 计算方式 |
| --- | --- |
| 风险总数 | `batch.issues.count()` |
| 阻断项 | severity=blocking |
| 高风险 | severity=high |
| 中风险 | severity=medium |
自动填表摘要:
| 字段 | 计算方式 |
| --- | --- |
| 模板数 | `len(batch.selected_templates)` |
| 导出文件数 | 对应 `ExportedSummaryFile` 数量 |
| 冲突字段数 | `len(batch.conflict_summary or [])` |
| 失败原因 | `batch.error_message` 或节点错误摘要 |
### 5.2 recipient.py
职责:首期根据环境变量解析指定个人接收人;后续可扩展为根据系统用户解析飞书目标。
伪代码:
```python
def resolve_feishu_target(user: User) -> ResolvedFeishuTarget:
if settings.FEISHU_DEFAULT_USER_OPEN_ID:
return ResolvedFeishuTarget(
mapping_id=None,
display_name=getattr(settings, "FEISHU_DEFAULT_TARGET_NAME", "指定个人账号"),
identifier_type="open_id",
identifier_value=settings.FEISHU_DEFAULT_USER_OPEN_ID,
masked_identifier=mask_identifier(settings.FEISHU_DEFAULT_USER_OPEN_ID),
missing=False,
)
if settings.FEISHU_DEFAULT_USER_ID:
return ResolvedFeishuTarget(
mapping_id=None,
display_name=getattr(settings, "FEISHU_DEFAULT_TARGET_NAME", "指定个人账号"),
identifier_type="user_id",
identifier_value=settings.FEISHU_DEFAULT_USER_ID,
masked_identifier=mask_identifier(settings.FEISHU_DEFAULT_USER_ID),
missing=False,
)
return ResolvedFeishuTarget(
mapping_id=None,
display_name=user.get_username(),
identifier_type="missing",
identifier_value="",
masked_identifier="",
missing=True,
)
def resolve_feishu_target_by_user_mapping(user: User) -> ResolvedFeishuTarget:
mapping = (
FeishuUserMapping.objects
.filter(system_user=user, is_active=True)
.first()
)
if mapping is None:
return ResolvedFeishuTarget(
mapping_id=None,
display_name=user.get_username(),
identifier_type="missing",
identifier_value="",
masked_identifier="",
missing=True,
)
identifier_type, identifier_value = mapping.preferred_identifier()
return ResolvedFeishuTarget(
mapping_id=mapping.pk,
display_name=mapping.feishu_display_name or user.get_username(),
identifier_type=identifier_type,
identifier_value=identifier_value,
masked_identifier=mask_identifier(identifier_value),
missing=identifier_type == "missing",
)
```
脱敏规则:
| 类型 | 规则 |
| --- | --- |
| mobile | 保留前三位和后四位,如 `138****1234` |
| open_id/user_id | 保留前 6 位和后 4 位 |
| missing | 空字符串 |
首期调度器使用 `resolve_feishu_target()``resolve_feishu_target_by_user_mapping()` 作为后续“按发起人私聊”能力预留。
### 5.3 message_builder.py
职责:构造富文本 payload 和入库摘要。
函数:
```python
def build_feishu_post_message(
context: NotificationContext,
target: ResolvedFeishuTarget,
) -> dict: ...
def build_message_summary(
context: NotificationContext,
target: ResolvedFeishuTarget,
) -> str: ...
```
富文本规则:
| 场景 | 规则 |
| --- | --- |
| 有映射 | 加入 `at` 标签 |
| 无映射 | 不加入 `at` 标签,增加映射缺失提示 |
| 失败状态 | 标题和下一步动作突出失败原因摘要 |
| 摘要过长 | 每条摘要最多 120 字,总摘要最多 800 字 |
| 链接 | 使用本地地址拼接,后续再切换域名配置 |
### 5.4 feishu_token.py
职责:使用 App ID/App Secret 获取并缓存 `tenant_access_token`
函数:
```python
def get_tenant_access_token() -> FeishuTokenResult: ...
def refresh_tenant_access_token() -> FeishuTokenResult: ...
```
结果结构:
```python
@dataclass(frozen=True)
class FeishuTokenResult:
ok: bool
tenant_access_token: str
expire_seconds: int
code: str
message: str
```
处理规则:
| 场景 | 处理 |
| --- | --- |
| App ID/App Secret 缺失 | 返回 failed错误码 config_missing |
| 缓存 token 未过期 | 直接返回缓存 token |
| token 过期或不存在 | 调用飞书 token API 重新获取 |
| token API 返回失败 | 返回 failed记录 code/message |
| HTTP 超时 | 返回 failed错误码 timeout |
### 5.5 feishu_message_api.py
职责:调用飞书发送消息 API。
函数:
```python
def send_personal_message(
*,
tenant_access_token: str,
receive_id_type: str,
receive_id: str,
payload: dict,
) -> FeishuMessageApiResult: ...
```
结果结构:
```python
@dataclass(frozen=True)
class FeishuMessageApiResult:
ok: bool
status_code: int | None
code: str
message: str
duration_ms: int
message_id: str = ""
```
异常处理:
| 异常 | 处理 |
| --- | --- |
| 指定接收人缺失 | 返回 failed错误码 recipient_missing |
| tenant_access_token 缺失 | 返回 failed错误码 token_missing |
| HTTP 超时 | 返回 failed错误码 timeout |
| 非 2xx | 返回 failed记录 status_code |
| 飞书返回 code 非 0 | 返回 failed记录 code/message |
| token 失效 | 刷新 token 后允许同步重试一次消息 API |
### 5.6 records.py
职责:判重和落库。
流程:
```text
输入 NotificationContext
-> 查询 dedupe_key 是否已有 success
-> 若有,返回已有记录,不发送
-> 若未启用真实飞书,创建 disabled/mock 记录
-> 若发送成功,创建 success 记录
-> 若发送失败,创建 failed 记录
```
字段写入规则:
| 字段 | 来源 |
| --- | --- |
| workflow_type | context.workflow_type |
| workflow_batch_id | context.workflow_batch_id |
| workflow_batch_no | context.workflow_batch_no |
| workflow_status | context.workflow_status |
| dedupe_key | context.dedupe_key |
| trigger_user_id | context.trigger_user_id |
| feishu_mapping_id | target.mapping_id |
| at_identifier_type | target.identifier_type |
| at_identifier_masked | target.masked_identifier |
| message_summary | `build_message_summary()` |
### 5.7 dispatcher.py
对外入口:
```python
def dispatch_workflow_notification(context: NotificationContext) -> WorkflowNotificationRecord:
if WorkflowNotificationRecord.already_sent(context.dedupe_key):
return WorkflowNotificationRecord.objects.get(
dedupe_key=context.dedupe_key,
send_status=WorkflowNotificationRecord.SendStatus.SUCCESS,
)
user = User.objects.get(pk=context.trigger_user_id)
target = resolve_feishu_target(user)
message = build_feishu_post_message(context, target)
summary = build_message_summary(context, target)
if not settings.FEISHU_NOTIFY_ENABLED:
return create_disabled_record(context, target, summary)
token_result = get_tenant_access_token()
if not token_result.ok:
return create_failed_record(context, target, summary, token_result)
result = send_personal_message(
tenant_access_token=token_result.tenant_access_token,
receive_id_type=target.identifier_type,
receive_id=target.identifier_value,
payload=message,
)
if result.ok:
return create_success_record(context, target, summary, result)
return create_failed_record(context, target, summary, result)
```
---
## 六、工作流接入点
| 工作流 | 推荐接入位置 |
| --- | --- |
| 自动汇总 | 文件汇总批次状态写为 success/partial_success/failed 后 |
| 法规核查 | 报告导出和风险项保存后;替换或并行现有 `create_mock_notifications` |
| 自动填表 | `notify` 节点中替换或扩展现有 `notify_completion` |
接入原则:
| 原则 | 说明 |
| --- | --- |
| 通知异常捕获 | 工作流调用通知服务时捕获异常并记录 non_blocking_errors |
| 不回滚业务结果 | 通知失败不修改业务批次成功状态 |
| 单点适配 | 工作流只负责生成或传入批次,摘要由 adapter 负责 |
---
## 七、批次详情展示设计
### 7.1 后端上下文
为批次详情页提供:
```python
def get_notification_records(workflow_type: str, batch_id: int) -> QuerySet:
return WorkflowNotificationRecord.objects.filter(
workflow_type=workflow_type,
workflow_batch_id=batch_id,
).order_by("-created_at")
```
### 7.2 页面展示规则
| 状态 | 展示 |
| --- | --- |
| success | “飞书通知已发送”,展示 sent_at |
| failed | “飞书通知失败”,展示 error_message |
| disabled | “飞书通知未启用” |
| 无记录 | “暂无通知记录” |
三个工作流结果页可复用同一 partial 模板或上下文字段。
---
## 八、问答预留详细设计
### 8.1 批次摘要查询服务
预留函数:
```python
def query_batch_summary(
user: User,
*,
workflow_type: str | None = None,
batch_no: str | None = None,
latest: bool = False,
) -> dict:
...
```
权限规则:
| 用户 | 可查范围 |
| --- | --- |
| 管理员 | 全部批次 |
| 普通用户 | `batch.user == user` 的批次 |
| 未绑定用户 | 不可查 |
查询对象:
| 类型 | 说明 |
| --- | --- |
| 明确批次号 | 精确匹配 batch_no |
| 最近/最新 | 在有权限范围内按 created_at/finished_at 倒序取第一条 |
| 工作流类型 | file_summary、regulatory_review、application_form_fill |
### 8.2 问答日志服务
预留函数:
```python
def record_feishu_question_log(
*,
user: User | None,
mapping: FeishuUserMapping | None,
source_type: str,
question_text: str,
intent: str,
query_object: dict,
answer_summary: str,
permission_result: str,
status: str,
error_message: str = "",
) -> FeishuQuestionLog:
...
```
首期不需要接飞书事件,但测试可直接调用该服务,确认日志字段与权限规则可用。
---
## 九、测试设计
### 9.1 单元测试
| 测试文件 | 用例 |
| --- | --- |
| `tests/test_feishu_notification.py` | tenant_access_token 获取和缓存 |
| `tests/test_feishu_notification.py` | 指定个人接收人优先级 open_id > user_id |
| `tests/test_feishu_notification.py` | 指定接收人缺失时写 failed 记录 |
| `tests/test_feishu_notification.py` | 真实通知关闭时写 disabled/mock 记录 |
| `tests/test_feishu_notification.py` | 消息 API 成功写 success 记录 |
| `tests/test_feishu_notification.py` | token 获取失败写 failed 记录 |
| `tests/test_feishu_notification.py` | 消息 API 超时写 failed 记录 |
| `tests/test_feishu_notification.py` | 同一 dedupe_key 不重复发送 |
| `tests/test_feishu_question_reserved.py` | 管理员可查询全部批次摘要 |
| `tests/test_feishu_question_reserved.py` | 普通用户只能查询自己的批次 |
| `tests/test_feishu_question_reserved.py` | 问答日志不保存完整回答正文 |
### 9.2 集成测试
| 场景 | 验证 |
| --- | --- |
| 自动汇总完成 | 生成通知上下文并写记录 |
| 法规核查完成 | 风险摘要正确 |
| 自动填表完成 | 导出和冲突摘要正确 |
| 批次详情页 | 展示通知状态和失败原因 |
### 9.3 外部飞书测试
真实飞书 API 测试不进入默认 CI。建议提供手动命令或 Django management command
```text
python manage.py send_test_feishu_notification --username owner
```
该命令只在本地配置 `FEISHU_NOTIFY_ENABLED=true``FEISHU_APP_ID``FEISHU_APP_SECRET``FEISHU_DEFAULT_USER_OPEN_ID``FEISHU_DEFAULT_USER_ID` 后使用。
---
## 十、异常处理
| 异常 | 处理 |
| --- | --- |
| 指定接收人缺失 | 不发送真实消息,记录 recipient_missing |
| App ID/App Secret 未配置 | 写 failed 或 disabled 记录,不发送 |
| tenant_access_token 获取失败 | 写 failed记录 token API 错误 |
| 指定接收人 open_id/user_id 未配置 | 写 failed错误码 recipient_missing |
| HTTP 超时 | 写 failed错误码 timeout |
| 飞书返回错误 | 写 failed记录 code/message |
| 通知记录唯一冲突 | 查询已有记录并返回,不重复发送 |
| 批次链接生成失败 | 发送无链接摘要,记录 warning 到 message_summary |
---
## 十一、日志与安全
| 项 | 要求 |
| --- | --- |
| 日志脱敏 | 不打印 App Secret、tenant_access_token、完整手机号 |
| 入库脱敏 | 通知记录只保存脱敏接收人标识 |
| payload | 不保存完整富文本 payload |
| 错误信息 | 保存飞书错误摘要,避免保存敏感请求头 |
| 问答日志 | 保存问题、意图、对象和回答摘要,不保存完整回答 |
---
## 十二、实施顺序建议
| 顺序 | 内容 |
| --- | --- |
| 1 | 新增模型、迁移和 Admin |
| 2 | 实现用户映射解析和脱敏 |
| 3 | 实现飞书富文本构造 |
| 4 | 实现 tenant_access_token 获取与缓存 |
| 5 | 实现飞书消息 API 发送客户端 |
| 6 | 实现通知记录判重和落库 |
| 7 | 实现三个工作流 adapter |
| 8 | 接入三个工作流完成节点 |
| 9 | 批次详情页展示通知状态 |
| 10 | 实现问答预留查询服务和日志服务 |
| 11 | 补齐单元测试和集成测试 |