Compare commits

119 Commits
dev ... master

Author SHA1 Message Date
8f7d1482ed docs: 更新V2项目与agent协作文档 2026-06-11 00:20:40 +08:00
1d0b5338a8 chore(master): 对齐V2忽略规则 2026-06-11 00:15:56 +08:00
bb2fbe272f chore(master): 保留V2环境配置文件 2026-06-11 00:15:20 +08:00
ccd6e8ef4d chore(master): 清理V2合并后的旧版遗留文件 2026-06-11 00:09:54 +08:00
64d09ec30f merge: 合并V2到master
# Conflicts:
#	.gitignore
#	README.md
#	config/asgi.py
#	config/settings.py
#	config/urls.py
#	config/wsgi.py
#	manage.py
#	requirements.txt
#	templates/base.html
#	tests/conftest.py
2026-06-11 00:08:00 +08:00
7def60f1b6 merge: 合并监管信息材料包最新代码到V2 2026-06-10 23:58:24 +08:00
9c6cad481c test(regulatory-info-package): 补充模板生成回归覆盖 2026-06-10 23:56:51 +08:00
1bf8634373 feat(regulatory-info-package): 完善目录页码与组成成分填充 2026-06-10 23:56:40 +08:00
3bcf9647a1 docs(regulatory-info-package): 更新材料包生成设计决策 2026-06-10 23:56:20 +08:00
cf4f4456c4 fix(regulatory-info-package): 使用干净字段模板生成材料包 2026-06-10 20:23:06 +08:00
b728703e67 fix(regulatory-info-package): 完成后追加下载摘要 2026-06-10 19:56:50 +08:00
6d4b519f83 test(regulatory-info-package): 覆盖材料包主链路 2026-06-10 19:50:22 +08:00
dcd829e821 feat(regulatory-info-package): 接入对话和前端卡片 2026-06-10 19:50:03 +08:00
dac8ce3c14 feat(regulatory-info-package): 实现材料包生成工作流 2026-06-10 19:49:44 +08:00
f0286264e2 feat(regulatory-info-package): 增加材料包数据模型 2026-06-10 19:49:25 +08:00
zhiye.sun
a060c23ba7 docs(docs): 调整数据库与详细设计目录编号 2026-06-10 15:15:02 +08:00
zhiye.sun
db0e94cf26 docs(regulatory-info-package): 补充第1章监管信息开发计划 2026-06-10 14:59:24 +08:00
zhiye.sun
dce7045a46 docs(regulatory-info-package): 新增第1章监管信息设计文档 2026-06-10 14:57:40 +08:00
8548b6d2b4 docs:原始材料内容补充 2026-06-10 08:41:50 +08:00
26e675e5d3 fix(chat): 拦截无依据的非业务问题 2026-06-09 08:23:08 +08:00
42187bf8e9 fix(knowledge-base): 停用文档时同步清理索引 2026-06-09 08:22:57 +08:00
18548eb78f fix(file-summary): 修复删除对话时受保护批次阻塞 2026-06-09 08:22:45 +08:00
2b5093040d fix(kb): 完善知识库入库和重建索引 2026-06-08 23:45:34 +08:00
d8cd95e590 docs(report): 更新架构汇报材料 2026-06-08 23:45:06 +08:00
681cb03eb9 chore(config): 切换演示模型配置 2026-06-08 23:44:50 +08:00
ccfa43645e feat(dashboard): 增加首页工作台并调整聊天入口 2026-06-08 22:25:16 +08:00
ef0a9ee13e feat(conversations): 支持删除对话并优化侧栏 2026-06-08 21:39:38 +08:00
2244b69d62 feat(chat): 接入全局知识库上下文 2026-06-08 21:38:12 +08:00
5ecf78c5d6 feat(knowledge-base): 增加全局知识库管理 2026-06-08 21:37:32 +08:00
e6fa738fd5 docs: 补充产品说明和汇报材料 2026-06-08 21:35:13 +08:00
1f56247978 test(feishu): 修正 token 刷新测试 mock 2026-06-07 22:43:40 +08:00
90144c42ac chore(feishu): 提交飞书接入文档和本地配置 2026-06-07 22:36:04 +08:00
f23e403eb8 docs: document feishu configuration 2026-06-07 22:17:40 +08:00
bd9b2e872e feat: add feishu question preview services 2026-06-07 22:16:36 +08:00
be7fbab0a0 feat: add feishu notification test command 2026-06-07 22:14:51 +08:00
1a1b3ee9d4 feat: show feishu notification status 2026-06-07 22:13:56 +08:00
cbc7493df8 feat: wire feishu notifications into workflows 2026-06-07 22:09:47 +08:00
820069f558 feat: add workflow notification dispatcher 2026-06-07 22:07:00 +08:00
bdc1d58c22 feat: add feishu api notification services 2026-06-07 22:05:20 +08:00
da81ce24d0 feat: add feishu notification data models 2026-06-07 22:03:05 +08:00
003ff59268 fix(application-form-fill): 过滤申请表噪声冲突内容 2026-06-07 20:34:24 +08:00
d640ced748 fix(application-form-fill): 清理填表说明并收窄按钮话术 2026-06-07 20:26:32 +08:00
30bdcdbc9c fix(application-form-fill): 代理人字段暂用生产企业信息 2026-06-07 20:19:52 +08:00
57f9181d58 fix(application-form-fill): 新附件先汇总再填表 2026-06-07 20:15:08 +08:00
0ccd69d3f4 fix(application-form-fill): 抽取说明书章节和表格字段 2026-06-07 20:14:53 +08:00
13b543c99d fix(application-form-fill): 清洗填表Word文件名 2026-06-07 20:14:37 +08:00
ac5cf8bf7e fix(application-form-fill): 优先路由填表提示并支持rar预览 2026-06-07 20:14:23 +08:00
82c33e513f feat(frontend): 启用工作流快捷提示按钮 2026-06-07 20:14:04 +08:00
228f30a697 feat(application-form-fill): 合并自动填表完整链路 2026-06-07 18:45:15 +08:00
4ac9c04dbf feat(application-form-fill): 展示自动填表工作流卡片 2026-06-07 18:43:39 +08:00
9be10ef990 feat(application-form-fill): 串联填表工作流产物输出 2026-06-07 18:40:04 +08:00
f35a3ba9b4 feat(application-form-fill): 生成填表 Word 和追溯清单 2026-06-07 18:33:59 +08:00
a48f778e09 feat(application-form-fill): 实现字段抽取与冲突合并 2026-06-07 18:31:34 +08:00
72890783b3 feat(application-form-fill): 实现自动填表模板选择 2026-06-07 18:28:48 +08:00
8694f2d43e feat(application-form-fill): 接入自动填表工作流触发 2026-06-07 18:26:37 +08:00
e48d44f832 feat(application-form-fill): 新增模板配置骨架 2026-06-07 18:23:06 +08:00
74cbe349a8 feat(application-form-fill): 新增自动填表批次模型 2026-06-07 18:20:14 +08:00
f48277aea7 chore(application-form-fill): 记录自动填表开发基线 2026-06-07 18:16:45 +08:00
56225f40d9 docs(application-form-fill): 完善申报文件自动填表设计文档 2026-06-07 17:05:08 +08:00
3e8720e521 feat(regulatory): 按实际处理数量更新节点进度 2026-06-07 13:32:06 +08:00
32d258bb75 feat(regulatory): 输出法规核查过程日志 2026-06-07 13:23:55 +08:00
0f9fb980f2 fix(regulatory): 为LLM复核超时增加重试 2026-06-07 13:03:24 +08:00
9e27c4c684 fix(regulatory): 修复无标签文档适用条件回显 2026-06-07 12:29:22 +08:00
1b4a10b5ba fix(regulatory): 自动执行法规核查前置汇总 2026-06-07 12:17:20 +08:00
911e5378e8 fix(regulatory): 修复条件确认实时轮询和重复提示 2026-06-07 12:09:02 +08:00
8f16675a92 feat(regulatory): 为核查流程增加LLM复核记录 2026-06-07 11:52:54 +08:00
945669b9c2 feat(regulatory): 增加条件字段LLM复核 2026-06-07 11:46:55 +08:00
a34684e490 fix(regulatory): 修复换行产品名称提取不全 2026-06-07 11:30:48 +08:00
72f18167c5 fix(regulatory): 轮询时加载条件确认卡 2026-06-07 11:27:12 +08:00
b8d711729d feat(regulatory): 支持按附件4章节核查 2026-06-07 11:17:57 +08:00
f46d9c5be6 fix(regulatory): 缺失问题标题显示章节序号 2026-06-07 11:12:19 +08:00
462d3ec5f5 fix(regulatory): 优先从附件字段识别适用条件 2026-06-07 10:40:05 +08:00
12b476a8ef fix(regulatory): 将条件确认移入对话区 2026-06-07 10:37:12 +08:00
48d94884b9 docs(regulatory): 补充第二批附件4开发计划 2026-06-07 10:31:39 +08:00
4e46f27c28 feat(regulatory): 完善复核前端入口 2026-06-07 09:40:18 +08:00
d39e3fe2d5 feat(regulatory): 增加mock通知留痕 2026-06-07 09:35:24 +08:00
d88d642f6a feat(regulatory): 增加整改复核闭环 2026-06-07 09:32:39 +08:00
1bdc7322cf feat(regulatory): 对齐附件4目录核查规则 2026-06-07 09:27:42 +08:00
bbd2d3532a feat(regulatory): 增加适用条件确认暂停恢复 2026-06-07 09:19:31 +08:00
bd805203f1 feat(regulatory): 展示法规核查工作流卡片 2026-06-07 00:43:18 +08:00
4c28466fe4 feat(regulatory): 增加风险归并与核查报告导出 2026-06-07 00:39:33 +08:00
ec89e62661 feat(regulatory): 增加法规核查基础服务 2026-06-07 00:36:18 +08:00
44d31d2a14 feat(regulatory): 接入法规核查触发与工作流骨架 2026-06-07 00:34:12 +08:00
26490f7c46 feat(regulatory): 增加本地法规RAG索引检索 2026-06-07 00:30:53 +08:00
2a4dd6cfab feat(regulatory): 增加法规规则版本检查 2026-06-07 00:26:19 +08:00
f52dcc197d feat(regulatory): 新增法规核查模型与工作流通用字段 2026-06-07 00:23:58 +08:00
665403735a chore: 确认法规核查基线回归通过 2026-06-07 00:20:16 +08:00
f179749cfb docs(todo): 补充第二阶段暂缓事项 2026-06-07 00:11:24 +08:00
e58da66853 docs(regulatory-review): 拆分法规核查开发计划 2026-06-07 00:11:01 +08:00
df3f393dd2 feat(attachments): 新增附件管理页面 2026-06-06 22:45:48 +08:00
0fca20756b feat(attachments): 补充附件管理接口 2026-06-06 22:45:14 +08:00
3c6ec67371 fix(file-summary): 调整工作流批次轮播展示 2026-06-06 22:20:26 +08:00
7e561ea213 fix(file-summary): 同步压缩包工作流状态与结果刷新 2026-06-06 19:45:49 +08:00
daa0642142 fix(agent): 增强 LLM 流式回复兜底 2026-06-06 19:45:13 +08:00
c78ff3a1fd fix(file-summary): 避免无效 Office 文件触发 COM 统计 2026-06-06 19:44:42 +08:00
460d418921 fix(file-summary): 补强 Office 页数统计 2026-06-06 17:57:08 +08:00
54c37edf19 fix(frontend): 修复会话补充与工作流刷新 2026-06-06 17:56:54 +08:00
fa77c68d77 feat(agent): 增加 LLM 路由与诊断日志 2026-06-06 17:56:41 +08:00
47b5ad1054 feat(attachments): 增加附件阅读解析能力 2026-06-06 16:37:54 +08:00
fd88ff4652 fix(frontend): 调整审核页布局与报告渲染 2026-06-06 16:37:40 +08:00
b1a336d019 fix(file-summary): 修复报告导出下载 2026-06-06 16:37:29 +08:00
311eb1b129 test(file-summary): 增加 Playwright 端到端测试 2026-06-06 10:32:18 +08:00
77db0d978a merge: 自动汇总文件目录页数 2026-06-06 10:28:14 +08:00
684682f86d docs(file-summary): 补充部署存储说明 2026-06-06 10:27:23 +08:00
a917a18ca1 feat(file-summary): 增加前端汇总面板 2026-06-06 10:25:11 +08:00
61bd31790b feat(file-summary): 生成汇总报告和导出下载 2026-06-06 01:22:49 +08:00
18d045d487 feat(file-summary): 实现文件处理技能链路 2026-06-06 01:20:26 +08:00
51e7c0c007 feat(file-summary): 接入文件汇总工作流触发 2026-06-06 01:16:22 +08:00
eb87d9040d feat(file-summary): 实现对话附件上传接口 2026-06-06 01:13:23 +08:00
855afcdee3 feat(file-summary): 添加文件汇总数据模型 2026-06-06 01:11:11 +08:00
b96ab1303a docs(file-summary): 对齐文档路径和附件模型命名 2026-06-06 00:56:41 +08:00
ce574048a4 docs(regulatory-review): 增加法规核查设计文档 2026-06-06 00:55:54 +08:00
f1534b6165 docs(requirements): 增加法规核查整改闭环需求 2026-06-05 23:40:18 +08:00
b7a3d512c0 docs(materials): 整理文档目录并补充法规材料 2026-06-05 23:39:38 +08:00
zhiye.sun
38529393cc docs(file-summary): 完善自动汇总流程设计与开发计划 2026-06-05 16:48:31 +08:00
933799a882 chore(config): 纳入本地硅基流动环境配置 2026-06-05 00:13:20 +08:00
a0e5e4c301 docs(source): 补充试题原始材料转换稿 2026-06-05 00:12:47 +08:00
7ab5aad938 feat(chat): 支持流式回复与用户节点导航 2026-06-05 00:11:53 +08:00
84e045f5ab feat(demo): 初始化审核智能体演示基线 2026-06-04 23:42:37 +08:00
364 changed files with 45409 additions and 7961 deletions

27
.env Normal file
View File

@@ -0,0 +1,27 @@
DJANGO_SECRET_KEY=replace-with-a-local-secret-key
DJANGO_DEBUG=true
DJANGO_ALLOWED_HOSTS=*
# SiliconFlow OpenAI-compatible API
LLM_PROVIDER=openai_compatible
LLM_API_KEY=sk-pgvkjondmmrlyxmrfhotgpuirgbtgzrpjpweorhwruflxmxw
LLM_BASE_URL=https://api.siliconflow.cn/v1
LLM_MODEL=deepseek-ai/DeepSeek-V4-Pro
SILICONFLOW_EMBEDDING_MODEL=Qwen/Qwen3-Embedding-8B
SILICONFLOW_EMBEDDING_DIMENSIONS=4096
# SiliconFlow embedding model for RAG
EMBEDDING_API_KEY=sk-pgvkjondmmrlyxmrfhotgpuirgbtgzrpjpweorhwruflxmxw
EMBEDDING_BASE_URL=https://api.siliconflow.cn/v1
EMBEDDING_MODEL=BAAI/bge-m3
SCENARIO_CONFIG_DIR=configs
GOVERNANCE_CONFIG_PATH=configs/governance.yaml
UPLOAD_ROOT=data/uploads
CHROMA_PATH=data/chroma
FEISHU_NOTIFY_ENABLED=true
FEISHU_APP_ID=cli_aaafcc59f4b85bc2
FEISHU_APP_SECRET=OO8GKpjqTO3bHAUwCiSmRgW4FqsNB5Qa
FEISHU_DEFAULT_USER_OPEN_ID=ou_a6015773781a117eb7d8995efa5e4590
FEISHU_DEFAULT_TARGET_NAME=bruce
PUBLIC_BASE_URL=http://127.0.0.1:8000

View File

@@ -1,18 +0,0 @@
DJANGO_SECRET_KEY=replace-with-a-local-secret-key
DJANGO_DEBUG=true
DJANGO_ALLOWED_HOSTS=*
# OpenAI-compatible LLM API
LLM_API_KEY=your_llm_api_key
LLM_BASE_URL=https://api.openai.com/v1
LLM_MODEL=gpt-4.1-mini
# Embedding model for RAG
# Leave EMBEDDING_API_KEY empty to reuse LLM_API_KEY if desired.
EMBEDDING_API_KEY=
EMBEDDING_BASE_URL=
EMBEDDING_MODEL=text-embedding-3-small
SCENARIO_CONFIG_DIR=configs
UPLOAD_ROOT=data/uploads
CHROMA_PATH=data/chroma

35
.gitignore vendored
View File

@@ -1,37 +1,10 @@
# IDE
.idea/
.vscode/
# Python
.venv/
__pycache__/
*.py[cod]
*.pyo
*.pyd
.Python
# Virtual environments
.venv/
venv/
env/
# Django / local data
*.sqlite3
db.sqlite3
data/uploads/
data/chroma/
media/
staticfiles/
# Environment variables
.env
.env.local
# Test / coverage
media/
.pytest_cache/
.coverage
htmlcov/
# OS
.DS_Store
Thumbs.db
.tmp/
.idea/

199
AGENTS.md
View File

@@ -1,172 +1,65 @@
# AGENTS.md
# Agent Collaboration Guide
本文档约定本项目后续由人或编码 Agent 协作开发时需要遵守的边界、风格和实现顺序。
This guide is for Codex or other coding agents working in this repository.
## 项目定位
## Project Summary
Universal Agent Demo Framework 是一个用于复试展示的通用 AI Agent Demo 框架。
DEMO-AGENT V2 is a Django application for IVD registration document review. The main app is `review_agent`, with workflow modules for file summaries, regulatory review, application form filling, regulatory information package generation, knowledge-base management, and Feishu notification/question handling.
优先目标:
The current `master` branch is intended to match `V2`.
- 快速适配未知复试题。
- 保证本地可运行。
- 保证代码结构清楚,方便讲解。
- 避免为了平台完整性牺牲改题速度。
## Important Paths
## 架构原则
| Path | Purpose |
| --- | --- |
| `config/settings.py` | Django settings and environment loading |
| `config/urls.py` | Page routes and included API routes |
| `review_agent/models.py` | Shared Django models |
| `review_agent/urls.py` | Review-agent API routes |
| `review_agent/file_summary/` | Attachment handling, file inventory, page count, exports |
| `review_agent/regulatory_review/` | NMPA review workflow, rules, RAG, risk and issue review |
| `review_agent/application_form_fill/` | Application form field extraction and Word filling |
| `review_agent/regulatory_info_package/` | Chapter 1 regulatory information package generation |
| `review_agent/notifications/` | Notification dispatch and Feishu adapters |
| `templates/` | Django templates |
| `static/` | Frontend CSS and JavaScript |
| `docs/` | Requirements, designs, plans, source materials |
| `tests/` | pytest suite |
采用:
## Development Rules
```text
Django 单体 + 独立 Agent Core + Docker Compose
```
- Prefer the existing Django patterns in `review_agent` before introducing new abstractions.
- Keep workflow modules independent. Do not fold regulatory package, application form fill, or regulatory review logic into unrelated modules.
- Preserve user data and generated artifacts. Do not delete `media/`, `.tmp/`, `db.sqlite3`, or `.env` unless explicitly asked.
- Treat `.env` as environment-specific configuration. It is currently tracked because this project needs a complete V2 state, but do not print secret values in logs or docs.
- For Word/PDF/Excel handling, use structured libraries already in the project instead of ad hoc text parsing when possible.
- For frontend work, keep the current workbench style: restrained, task-focused, evidence-first, and consistent with existing templates and CSS.
核心边界:
- Django 负责页面、数据库、文件上传、审计日志和后台管理。
- Agent Core 负责 RAG、Prompt、工具调用、模型适配和结构化输出。
- Django View 不直接写大模型调用、向量检索和工具执行细节。
- Agent Core 不依赖 Django View。
## 模块边界
### config
负责 Django 项目配置、URL 总入口、环境变量、静态资源、上传路径和部署配置。
### apps.scenarios
负责场景列表、场景配置读取、场景元信息展示。
### apps.documents
负责文件上传、文件记录、文件状态和触发 RAG 入库。
### apps.chat
负责对话页面、用户输入表单、调用 Agent Core 和展示结果。
### apps.audit
负责审计日志模型、日志写入服务、日志列表和详情页。
### agent_core
负责 Agent 编排、RAG、工具注册、LLM Provider、结构化输出和 Adapter 扩展。
## 开发顺序
建议按以下顺序推进:
1. 创建 Django 项目骨架。
2. 完成 Config 模块。
3. 完成 Scenarios 模块,先展示 5 个场景。
4. 完成 Agent Core 最小闭环,先返回模拟结果。
5. 完成 Chat 页面,打通对话链路。
6. 完成 Audit 模块,记录每次对话。
7. 完成 Documents 模块,支持上传文件。
8. 完成 RAG 入库和检索。
9. 完成内置工具系统。
10. 补 Docker Compose 一键启动。
当前仓库状态说明:
- Django 单体骨架已完成。
- 5 个预置场景 YAML 已接通首页和对话页。
- Agent Core 已具备 Prompt 编排、结构化解析、工具注册和 RAG fallback / Chroma 双路径。
- Chat、Documents、Audit 页面已经可以形成完整演示闭环。
- 全量测试已覆盖主要模块行为,并默认隔离真实 LLM 网络调用。
## 编码约定
- Python 代码优先保持简单、直观、可讲解。
- 不为了抽象而抽象。
- View 只做请求处理和页面渲染,复杂逻辑放到 `services.py``agent_core`
- 配置化优先,业务场景不要写死在代码中。
- 工具函数必须通过 Tool Registry 注册。
- 模型调用必须通过 LLM Provider不允许散落在业务代码中。
- 审计日志要记录成功和失败两种情况。
- 不在日志中保存 API Key、密钥或敏感环境变量。
- 新增或重构模块时,优先补清晰的中文注释,说明职责边界、输入输出和设计取舍。
- 页面模板优先直接表达业务信息,不在模板中堆积复杂逻辑判断。
- 测试优先覆盖服务层和核心编排逻辑,再由页面测试补齐关键展示行为。
## 文档约定
需求文档放在:
```text
docs/
```
需求分析文档放在:
```text
docs/需求分析/
```
设计文档放在:
```text
docs/设计文档/
```
场景配置放在:
```text
configs/
```
重要设计变更需要同步更新:
- `README.md`
- `docs/需求分析/1.V1总需求文档.md`
- 相关模块需求文档
- `AGENTS.md` 中的协作边界与当前实现状态
推荐同步文档的场景:
- 新增用户可见页面或流程。
- 调整环境变量、生效方式或部署命令。
- 修改 Agent Core 的输入输出合约。
- 新增工具、审计字段或场景配置字段。
## 测试与验证约定
每个阶段至少验证:
- Django 可以启动。
- 首页可以访问。
- 场景列表可显示。
- 对话流程可执行。
- 出错时页面有清晰提示。
- 审计日志能记录。
- Docker Compose 可以启动。
当前默认验证命令:
## Common Commands
```bash
pytest
python manage.py check
docker compose config
python manage.py migrate
python manage.py runserver
pytest
pytest tests -k regulatory_info_package
pytest tests/test_feishu_*.py
```
补充约定:
## Verification Notes
- 若本地 `.env` 存在真实模型密钥,测试仍应保持可离线执行。
- 每完成一项功能或一轮重构后,应先跑相关测试,再跑全量测试或核心回归测试。
- 完成改动后,按逻辑分组使用 Conventional Commit 风格提交到本地。
Before claiming a code change is complete, run at least the narrow test set for the touched workflow. For broad changes, run `python manage.py check` and `pytest`.
## 不优先做的事项
Known current state:
第一版不要优先做:
- `python manage.py check` passes.
- `pytest tests -k regulatory_info_package` passes.
- Full `pytest` may still include a few historical failures unrelated to the latest regulatory-info-package merge; report exact failures if they remain.
- React / Vue 前端。
- 多租户。
- 复杂 RBAC。
- 完整工作流引擎。
- 深度 Dify 集成。
- 微服务拆分。
- 分布式任务队列。
## Git Notes
- Check `git status --short --branch` before editing.
- Do not reset or revert user changes unless explicitly asked.
- Keep commits grouped by logical concern: docs, feature behavior, tests, cleanup.
- When merging `V2` and `master`, remember these histories were unrelated before the merge. Prefer preserving the V2 tree when the goal is to keep `master` as the complete V2 state.
这些内容可以作为后续增强,不应影响 V1 快速成型。

View File

@@ -1,15 +0,0 @@
FROM python:3.13-slim
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
WORKDIR /app
COPY requirements.txt /app/
RUN pip install --no-cache-dir -r requirements.txt
COPY . /app/
EXPOSE 8000
CMD ["sh", "-c", "python manage.py migrate && python manage.py runserver 0.0.0.0:8000"]

55
PRODUCT.md Normal file
View File

@@ -0,0 +1,55 @@
# Product
## Product Name
DEMO-AGENT V2
## Users
注册资料准备人员、法规审核人员、项目管理人员和演示评审人员。用户通常需要在资料量大、文件格式复杂、法规要求多、证据链容易断裂的情况下快速完成资料整理、核查、整改和申报文件准备。
## Product Purpose
DEMO-AGENT V2 是一个体外诊断试剂注册资料审核工作台。它把上传资料、文件汇总、法规规则核查、RAG 依据检索、风险预警、整改复核、申报表填充和第 1 章监管信息材料包生成组织成可追溯的工作流。
产品目标不是替代法规负责人作最终判断,而是把机械整理、跨文件检索、字段预填、问题归类和证据追溯做扎实,让负责人把精力放在判断和确认上。
## Core Workflows
| 工作流 | 目标产物 |
| --- | --- |
| 文件汇总 | 文件目录、页数、类型、批次状态、Markdown/Excel 导出 |
| 法规核查 | 缺失项、风险项、一致性问题、整改建议、复核记录 |
| 知识库管理 | 用户资料索引、内置法规资料检索、引用片段 |
| 申报表填充 | 预填申报表、字段来源、冲突和缺失提示 |
| 第 1 章监管信息材料包 | CH1.2、CH1.4、CH1.5、CH1.11 等 docx 文件和 zip |
| 飞书通知与问答 | 批次完成通知、问题模拟查询、系统入口链接 |
## Brand Personality
克制、可信、清晰。界面应服务审核任务,优先呈现状态、证据和下一步动作。
## Anti-References
避免营销页式大标题、装饰性卡片堆叠、过度动画、过亮渐变和不必要的视觉噪声。不要把审核工作台做成展示型官网,也不要隐藏关键状态或证据来源。
## Design Principles
- 证据优先:每个结论都应能回到来源文件、规则或检索片段。
- 状态清楚:批次、节点、风险、异常和导出结果要一眼可辨。
- 操作克制:页面提供必要动作,不把审核工作做成复杂后台。
- 人工确认:系统负责预处理和提示,法规负责人保留最终确认权。
- 可追溯:导出文件、消息、节点事件和问题状态都应能回到批次。
- 复用现有模式:新增页面沿用当前工作台导航、面板、表格和按钮体系。
## Accessibility & Inclusion
默认按 WCAG AA 方向处理对比度、键盘可访问和清晰标签。动效仅用于状态反馈,并尊重减少动态效果需求。
## Operational Boundaries
- `.env` 可用于本地和演示环境,但包含密钥时应限制分发范围。
- LLM、飞书、Word COM、7z、RAG 索引等外部能力必须允许 mock 或降级。
- 生成的申报和监管信息文件是预生成结果,需要人工复核后再用于正式申报。
- 默认存储使用 SQLite 和本地 `media/`,生产环境应迁移到持久化卷和受控备份。

309
README.md
View File

@@ -1,248 +1,123 @@
# Universal Agent Demo Framework
# DEMO-AGENT V2
用于复试展示的通用 AI Agent Demo 框架
DEMO-AGENT V2 是一个面向体外诊断试剂注册资料准备与审核的 Django 工作台。系统把资料上传、文件目录汇总、法规核查、知识库检索、风险提示、整改复核、申报表自动填充和第 1 章监管信息材料包生成组织到同一个可追溯的审核流程中
项目目标不是提前猜中某一个具体业务题,而是先准备一个可快速改题的基础平台。拿到复试题目后,可以通过修改场景配置、上传知识库、补充少量工具函数,快速完成一个可演示的企业业务 Agent
当前 `master` 已与 `V2` 内容对齐,是项目主线
## 核心理念
## 核心能力
| 能力 | 说明 |
| --- | --- |
| 审核工作台 | 登录后进入首页,查看对话、附件、知识库、批次和处理状态 |
| 对话式工作流 | 在 `/chat/` 中围绕当前对话上传资料、触发汇总、法规核查和生成任务 |
| 文件汇总 | 读取 PDF、Word、Excel、PowerPoint、压缩包等资料生成目录、页数、类型和导出结果 |
| NMPA 法规核查 | 基于规则、文本抽取、RAG 检索和 LLM 复核生成问题、风险和整改建议 |
| 知识库管理 | 上传管理资料、重建索引、检索引用片段,并过滤已停用或删除文档 |
| 申报表填充 | 从说明书和资料中抽取关键字段,生成预填申报表和追溯结果 |
| 第 1 章监管信息材料包 | 生成 CH1.2、CH1.4、CH1.5、CH1.11 等监管信息文件和 zip 产物 |
| 飞书通知与问答 | 支持企业自建应用消息通知,并预留飞书问答模拟命令 |
## 页面入口
| 页面 | 路径 |
| --- | --- |
| 登录页 | `http://127.0.0.1:8000/login/` |
| 首页 | `http://127.0.0.1:8000/` |
| 审核智能体 | `http://127.0.0.1:8000/chat/` |
| 知识库管理 | `http://127.0.0.1:8000/knowledge-base/` |
| 附件管理 | `http://127.0.0.1:8000/attachments/` |
| 管理后台 | `http://127.0.0.1:8000/admin/` |
## 项目结构
```text
业务 Agent = 场景配置 + 知识库 + 工具集 + 输出模板 + 审计日志 + 模型适配器
config/ Django 配置和总路由
review_agent/ 核心业务应用
application_form_fill/ 申报表自动填充
file_summary/ 文件汇总、附件和导出
regulatory_review/ 法规核查与整改复核
regulatory_info_package/ 第 1 章监管信息材料包生成
notifications/ 飞书通知和消息适配
feishu_questions/ 飞书问答预留能力
static/ 前端脚本和样式
templates/ Django 模板
docs/ 需求、设计、开发计划和原始材料
tests/ pytest 测试
```
## 技术路线
V1 采用:
- Django 单体应用
- 独立 Agent Core 模块
- SQLite
- Chroma
- Django Templates
- Docker Compose
- OpenAI API 兼容的 LLM 与 Embedding 接口
默认不强依赖 Dify。系统预留 Adapter 设计,后续可以接入 Dify、OpenAI Agents SDK 或其他 Agent 编排平台。
## 适用复试题型
| 题型 | 推荐场景模板 |
|---|---|
| SOP 问答 | `knowledge_qa` |
| 制度问答 | `knowledge_qa` |
| 文档审核 | `document_review` |
| 客服工单 | `ticket_assistant` |
| 质量异常分析 | `quality_analysis` |
| 财务审核 | `risk_audit` |
| 采购审核 | `risk_audit` |
| 合同风险分析 | `document_review``risk_audit` |
## 模块划分
```text
config
apps.scenarios
apps.documents
apps.chat
apps.audit
agent_core
```
职责边界:
- Django Apps 负责页面、数据、文件、日志等企业应用外壳。
- Agent Core 负责 RAG、工具调用、模型适配、结构化输出和 Agent 编排。
- RAG、工具调用和模型调用不直接写进 Django View。
## 推荐项目结构
```text
universal-agent-demo/
manage.py
requirements.txt
Dockerfile
docker-compose.yml
.env.example
README.md
AGENTS.md
config/
apps/
scenarios/
documents/
chat/
audit/
agent_core/
rag/
tools/
schemas/
configs/
knowledge_qa.yaml
document_review.yaml
ticket_assistant.yaml
quality_analysis.yaml
risk_audit.yaml
data/
uploads/
chroma/
docs/
```
## V1 功能范围
V1 需要完成:
- 场景列表。
- Agent 对话页。
- 文件上传。
- 文档入库。
- RAG 检索。
- 内置工具调用。
- 结构化输出展示。
- 审计日志。
- 模型 API 可配置。
- Docker 一键启动。
当前代码基线已经落地的能力:
- 首页支持展示场景摘要、适用题型、RAG 状态、工具数量。
- 非法 YAML 场景配置会被自动跳过,并在首页展示错误摘要。
- 对话页支持问题输入、文档范围选择、结构化结果、引用片段、工具调用和审计入口展示。
- 文档页支持上传、列表查看、手动入库、失败原因提示和重试。
- 审计页支持列表摘要、按场景筛选、详情查看、原始输出展示和敏感信息脱敏。
- Agent Core 已具备 Prompt 编排、OpenAI 兼容 Provider、结构化输出解析、RAG 检索和工具注册机制。
- 测试环境默认固定使用 Mock Provider避免误调用本地真实模型配置。
V1 暂不重点做:
- 多租户。
- 复杂权限。
- 完整工作流引擎。
- 前后端分离。
- 深度 Dify 集成。
- 生产级高并发优化。
## 复试改题流程
拿到题目后:
1. 判断题目属于哪类模板。
2. 复制最接近的 YAML 场景配置。
3. 修改 Agent 角色、目标、指令和输出模板。
4. 上传题目材料。
5. 如需业务计算,新增一个工具函数。
6. 用 2 到 3 个问题测试效果。
7. 演示场景配置、知识库引用、工具调用、结构化输出和审计日志。
## 当前页面概览
当前项目包含以下主要页面:
| 页面 | 路径 | 当前能力 |
|---|---|---|
| 场景首页 | `/` | 展示场景名称、描述、适用题型、RAG 状态、工具数和配置异常摘要 |
| 对话页 | `/chat/<scenario_id>/` | 输入问题、勾选已入库文档、查看结构化结果、引用片段、工具调用和审计入口 |
| 文档列表页 | `/documents/` | 查看文档状态、错误信息、上传时间并手动触发入库 |
| 文档上传页 | `/documents/upload/` | 选择场景并上传 `.txt``.md``.pdf``.docx` 文件 |
| 审计列表页 | `/audit/` | 查看执行摘要并按场景筛选 |
| 审计详情页 | `/audit/<log_id>/` | 查看输入、最终回答、结构化输出、引用、工具调用、原始输出和错误信息 |
## 计划启动方式
本地启动:
```bash
pip install -r requirements.txt
python manage.py migrate
python manage.py runserver
```
Docker 启动:
```bash
docker compose up --build
```
当前文档目标已统一为完整 V1 闭环:真实 Chroma RAG、OpenAI 兼容 LLM、OpenAI 兼容 Embedding、工具注册和审计日志。开发阶段可以用测试桩验证页面和边界但不作为 V1 验收结果。
推荐首次启动步骤:
## 本地运行
```bash
python -m venv .venv
.venv\Scripts\activate
pip install -r requirements.txt
python manage.py migrate
python manage.py createsuperuser
python manage.py runserver
```
## 环境变量
项目会自动读取仓库根目录 `.env`。当前仓库保留了 V2 的 `.env` 文件;后续如果要面向外部协作,请先确认其中没有不应公开的密钥。
项目当前通过 `os.environ` 读取配置,核心变量如下:
## 常用环境变量
```env
DJANGO_SECRET_KEY=replace-with-a-local-secret-key
DJANGO_DEBUG=true
DJANGO_ALLOWED_HOSTS=*
| 变量名 | 用途 |
| --- | --- |
| `DJANGO_SECRET_KEY` | Django secret key |
| `DJANGO_DEBUG` | 是否开启调试模式 |
| `DJANGO_ALLOWED_HOSTS` | 允许访问的主机列表 |
| `LLM_PROVIDER` | LLM provider 选择 |
| `LLM_API_KEY` | LLM API key |
| `LLM_BASE_URL` | OpenAI 兼容 LLM API 地址 |
| `LLM_MODEL` | 默认对话/抽取模型 |
| `SILICONFLOW_API_KEY` | SiliconFlow API key默认可复用 `LLM_API_KEY` |
| `SILICONFLOW_EMBEDDING_MODEL` | 法规 RAG 使用的 embedding 模型 |
| `SILICONFLOW_EMBEDDING_DIMENSIONS` | embedding 维度 |
| `REGULATORY_RAG_CHROMA_PATH` | 法规 RAG Chroma 存储路径 |
| `REGULATORY_RAG_COLLECTION` | 法规 RAG collection 名称 |
| `FEISHU_NOTIFY_ENABLED` | 是否启用真实飞书通知 |
| `FEISHU_APP_ID` | 飞书应用 App ID |
| `FEISHU_APP_SECRET` | 飞书应用 App Secret |
| `FEISHU_DEFAULT_USER_OPEN_ID` | 默认飞书接收人 open_id |
| `PUBLIC_BASE_URL` | 飞书消息中的系统入口根地址 |
LLM_API_KEY=your_llm_api_key
LLM_BASE_URL=https://api.openai.com/v1
LLM_MODEL=gpt-4.1-mini
## 外部依赖
EMBEDDING_API_KEY=
EMBEDDING_BASE_URL=
EMBEDDING_MODEL=text-embedding-3-small
Python 依赖见 `requirements.txt`,主要包括:
SCENARIO_CONFIG_DIR=configs
UPLOAD_ROOT=data/uploads
CHROMA_PATH=data/chroma
```
- Django
- PyYAML
- httpx
- chromadb
- pypdf
- python-docx
- python-pptx
- openpyxl / xlrd
- py7zr
- playwright
说明:
文件汇总支持 `.7z``.rar` 时,运行环境还需要可用的 `7z`/`p7zip` 命令。LibreOffice 不是必需依赖,仅作为后续增强老格式文档处理能力的可选项。
- `EMBEDDING_API_KEY` 为空时,代码会自动复用 `LLM_API_KEY`
- `EMBEDDING_BASE_URL` 为空时,代码会自动复用 `LLM_BASE_URL`
- `.env.example` 只作为模板,不应填写真实密钥并提交到仓库。
- 当前代码会在 Django settings 初始化时自动加载根目录 `.env`,本地 `python manage.py runserver``pytest` 和 Docker Compose 可以复用同一套配置。
- Docker Compose 当前在 `docker-compose.yml` 中通过 `env_file` 读取 `.env`
常见做法:
- 本地开发:复制 `.env.example``.env`,填入真实参数后运行。
- Docker 演示:确认 `.env` 已配置后,再执行 `docker compose up --build`
## 测试与验证
当前项目已经补有较完整的模块级测试,覆盖:
- 场景配置读取、非法配置容错和首页展示。
- 对话提交、文档范围传递、结构化结果展示。
- 文档上传、文本抽取、入库成功与失败提示。
- 审计日志落库、筛选、原始输出展示和 API Key 脱敏。
- Agent Core 的 Prompt 编排、结构化解析、RAG fallback 检索。
- Tool Registry 和内置工具行为。
- LLM / Embedding Provider 的配置与请求构造。
常用验证命令:
## 常用命令
```bash
pytest
python manage.py check
docker compose config
pytest
pytest tests -k regulatory_info_package
pytest tests/test_feishu_*.py
python manage.py send_test_feishu_notification --username owner
python manage.py feishu_question_simulate --username owner "查最新法规核查"
```
说明:
- 测试环境默认通过 `tests/conftest.py` 固定 `LLM_PROVIDER=mock`,避免回归测试误走真实网络请求。
- 当前本地 `.env` 可能包含真实模型配置,但不会影响自动化测试稳定性。
已知情况:当前全量 `pytest` 中仍有少量历史测试与当前页面/LLM 调用策略不完全一致;监管信息材料包主链路测试已通过。
## 文档入口
- [V1 总需求文档](docs/需求分析/1.V1总需求文档.md)
- [模块需求文档索引](docs/需求分析/2.模块需求索引.md)
- [智能体总体设计](docs/设计文档/1.智能体总体设计.md)
- [设计文档索引](docs/设计文档/0.设计文档索引.md)
- [协作与编码约定](AGENTS.md)
- [产品说明](PRODUCT.md)
- [Agent 协作约定](AGENTS.md)
- [docs 文档索引](docs/README.md)
- [需求分析](docs/1.需求分析)
- [功能设计](docs/2.功能设计)
- [数据库设计](docs/3.数据库设计)
- [详细设计](docs/4.详细设计)
- [开发计划](docs/5.开发计划)

View File

@@ -1,201 +0,0 @@
from dataclasses import dataclass
import json
import os
from urllib.error import URLError
from urllib.request import Request, urlopen
class LLMConfigurationError(ValueError):
"""LLM 调用缺少关键配置时抛出的业务异常。"""
class EmbeddingConfigurationError(ValueError):
"""Embedding 调用缺少关键配置时抛出的业务异常。"""
@dataclass
class LLMResponse:
"""
统一的模型响应对象。
Agent Core 的 Orchestrator 只依赖这一个结构,而不直接感知底层供应商差异。
"""
content: str = ""
model_name: str = ""
success: bool = True
error: Exception | None = None
class MockLLMProvider:
"""
本地和测试默认使用的 Mock Provider。
设计目标不是拟真对话,而是提供一个稳定、可断言、可结构化解析的响应,
让前后端在未接入真实模型时也能完整演示链路。
"""
def __init__(self, model_name: str = "mock-model"):
self.model_name = model_name or "mock-model"
def generate(self, messages: list[dict], response_format: dict | None = None) -> LLMResponse:
user_content = _find_last_user_message(messages)
return LLMResponse(
content=json.dumps(
{
"answer": f"模拟回答:{user_content}",
"confidence": "medium",
"references": [],
},
ensure_ascii=False,
),
model_name=self.model_name,
success=True,
)
class OpenAICompatibleProvider:
"""调用 OpenAI Chat Completions 兼容接口的 Provider。"""
def __init__(self, api_key: str, base_url: str, model_name: str):
self.api_key = api_key
self.base_url = base_url
self.model_name = model_name
def generate(self, messages: list[dict], response_format: dict | None = None) -> LLMResponse:
if not self.api_key:
return LLMResponse(
model_name=self.model_name,
success=False,
error=LLMConfigurationError("LLM_API_KEY 未配置,无法调用 OpenAI 兼容模型接口"),
)
payload = {
"model": self.model_name,
"messages": messages,
}
if response_format:
payload["response_format"] = response_format
try:
data = _post_json(
base_url=self.base_url,
endpoint="chat/completions",
api_key=self.api_key,
payload=payload,
)
choice = data.get("choices", [{}])[0]
content = choice.get("message", {}).get("content", "")
return LLMResponse(
content=content,
model_name=data.get("model", self.model_name),
success=True,
)
except Exception as exc:
return LLMResponse(model_name=self.model_name, success=False, error=exc)
class OpenAICompatibleEmbeddingProvider:
"""调用 OpenAI Embeddings 兼容接口的 Provider。"""
def __init__(self, api_key: str, base_url: str, model_name: str):
self.api_key = api_key
self.base_url = base_url
self.model_name = model_name
def embed_texts(self, texts: list[str]) -> list[list[float]]:
if not self.api_key:
raise EmbeddingConfigurationError("EMBEDDING_API_KEY 未配置,无法调用 OpenAI 兼容 Embedding 接口")
data = _post_json(
base_url=self.base_url,
endpoint="embeddings",
api_key=self.api_key,
payload={"model": self.model_name, "input": texts},
)
return [item.get("embedding", []) for item in data.get("data", [])]
def create_llm_provider(config: dict | None = None):
"""
根据配置创建 LLM Provider。
默认策略:
- 明确指定 `LLM_PROVIDER=mock` 时使用 Mock
- 未指定但存在 `LLM_API_KEY` 时默认走 OpenAI 兼容接口
- 否则回退到 Mock保证页面仍可闭环
"""
config = config or {}
provider_name = _resolve_provider_name(config)
model_name = config.get("LLM_MODEL", "mock-model")
if provider_name == "mock":
return MockLLMProvider(model_name=model_name)
return OpenAICompatibleProvider(
api_key=config.get("LLM_API_KEY", ""),
base_url=config.get("LLM_BASE_URL", "https://api.openai.com/v1"),
model_name=model_name,
)
def create_embedding_provider(config: dict | None = None):
"""
创建 Embedding Provider。
当未单独配置 Embedding Key 或 Base URL 时,会自动复用 LLM 配置,
以减少复试演示时的环境变量负担。
"""
config = config or {}
return OpenAICompatibleEmbeddingProvider(
api_key=config.get("EMBEDDING_API_KEY", config.get("LLM_API_KEY", "")),
base_url=config.get("EMBEDDING_BASE_URL", config.get("LLM_BASE_URL", "https://api.openai.com/v1")),
model_name=config.get("EMBEDDING_MODEL", "text-embedding-3-small"),
)
def get_runtime_llm_config(overrides: dict | None = None) -> dict:
"""
从环境变量读取运行时配置。
Agent Core 通过这一层读取模型配置,避免直接依赖 Django settings
这样本模块在独立脚本、测试和 Django 环境中都可复用。
"""
config = {
"LLM_PROVIDER": os.environ.get("LLM_PROVIDER", ""),
"LLM_API_KEY": os.environ.get("LLM_API_KEY", ""),
"LLM_BASE_URL": os.environ.get("LLM_BASE_URL", "https://api.openai.com/v1"),
"LLM_MODEL": os.environ.get("LLM_MODEL", "mock-model"),
}
if overrides:
config.update(overrides)
return config
def _resolve_provider_name(config: dict) -> str:
"""统一推导当前应启用的 Provider 名称。"""
provider_name = config.get("LLM_PROVIDER")
if provider_name:
return provider_name
return "openai_compatible" if config.get("LLM_API_KEY") else "mock"
def _find_last_user_message(messages: list[dict]) -> str:
"""从消息列表中提取最后一条用户输入,用于 Mock Provider 回显。"""
for message in reversed(messages):
if message.get("role") == "user":
return message.get("content", "")
return ""
def _post_json(base_url: str, endpoint: str, api_key: str, payload: dict) -> dict:
"""向 OpenAI 兼容接口发送 JSON POST 请求并解析响应。"""
url = f"{base_url.rstrip('/')}/{endpoint}"
request = Request(
url,
data=json.dumps(payload, ensure_ascii=False).encode("utf-8"),
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
},
method="POST",
)
try:
with urlopen(request, timeout=60) as response:
return json.loads(response.read().decode("utf-8"))
except URLError as exc:
raise RuntimeError(f"OpenAI 兼容接口调用失败:{exc}") from exc

View File

@@ -1,153 +0,0 @@
import json
import time
from .llm_provider import create_llm_provider, get_runtime_llm_config
from .results import AgentResult
from .structured_output import (
build_response_schema_hint,
extract_answer_from_structured_output,
parse_structured_output,
)
from .tool_registry import run_declared_tools
from .rag.retriever import retrieve
def run_agent(scenario_config: dict, user_input: str, options: dict | None = None) -> AgentResult:
"""
执行当前场景的最小 Agent 闭环。
处理顺序保持和设计文档一致:
1. 读取场景配置
2. 执行 RAG 检索
3. 执行声明式工具
4. 构造 Prompt 并调用 LLM
5. 解析结构化结果
6. 统一返回 AgentResult
"""
started_at = time.perf_counter()
options = options or {}
output_type = scenario_config.get("output", {}).get("type", "general_answer")
references = _collect_references(scenario_config=scenario_config, user_input=user_input, options=options)
tool_calls = run_declared_tools(scenario_config.get("tools", []), user_input)
messages = build_messages(
scenario_config=scenario_config,
user_input=user_input,
references=references,
tool_calls=tool_calls,
)
provider = options.get("llm_provider") or create_llm_provider(
get_runtime_llm_config(options.get("llm_config"))
)
llm_response = provider.generate(
messages,
response_format=build_response_schema_hint(output_type),
)
latency_ms = int((time.perf_counter() - started_at) * 1000)
if not llm_response.success:
return AgentResult(
answer="模型调用失败,请检查配置或稍后重试。",
structured_output={},
references=references,
tool_calls=tool_calls,
raw_output="",
model_name=llm_response.model_name or "unknown-model",
latency_ms=latency_ms,
status="failed",
error=str(llm_response.error or "未知模型错误"),
)
structured_output, _ = parse_structured_output(llm_response.content, output_type)
answer = extract_answer_from_structured_output(structured_output, llm_response.content)
return AgentResult(
answer=answer,
structured_output=structured_output,
references=references,
tool_calls=tool_calls,
raw_output=llm_response.content,
model_name=llm_response.model_name or "unknown-model",
latency_ms=latency_ms,
status="success",
)
def build_messages(
scenario_config: dict,
user_input: str,
references: list[dict],
tool_calls: list[dict],
) -> list[dict]:
"""将场景配置、检索结果和工具结果整合为最小可解释 Prompt。"""
agent_config = scenario_config.get("agent", {})
system_message = "\n".join(
[
f"你当前扮演的角色:{agent_config.get('role', '通用业务助手')}",
f"当前任务目标:{agent_config.get('goal', '根据输入生成结构化结果')}",
"执行要求:",
_format_instructions(agent_config.get("instructions", [])),
f"输出类型:{scenario_config.get('output', {}).get('type', 'general_answer')}",
"请优先输出 JSON 对象,字段必须贴近约定输出结构。",
]
)
context_message = "\n".join(
[
f"当前场景:{scenario_config.get('name', '未命名场景')}",
_format_references(references),
_format_tool_calls(tool_calls),
]
)
return [
{"role": "system", "content": system_message},
{"role": "assistant", "content": context_message},
{"role": "user", "content": user_input},
]
def _collect_references(scenario_config: dict, user_input: str, options: dict) -> list[dict]:
"""按场景配置执行检索,并保持无 RAG 场景也能正常返回空列表。"""
rag_config = scenario_config.get("rag", {})
if not rag_config.get("enabled"):
return []
return retrieve(
scenario_id=scenario_config.get("id", ""),
query=user_input,
collection=rag_config.get("collection", scenario_config.get("id", "")),
top_k=rag_config.get("top_k", 5),
document_ids=options.get("document_ids"),
store_path=options.get("rag_store_path"),
)
def _format_instructions(instructions: list[str]) -> str:
if not instructions:
return "1. 结合知识库和工具结果回答。\n2. 信息不足时明确说明。"
return "\n".join(f"{index}. {item}" for index, item in enumerate(instructions, start=1))
def _format_references(references: list[dict]) -> str:
if not references:
return "知识库引用:当前没有检索到可用片段。"
lines = ["知识库引用:"]
for index, reference in enumerate(references, start=1):
lines.append(
f"{index}. 来源={reference.get('source', '未知来源')} 内容={reference.get('content', '')}"
)
return "\n".join(lines)
def _format_tool_calls(tool_calls: list[dict]) -> str:
if not tool_calls:
return "工具结果:当前场景未声明工具或无需调用工具。"
lines = ["工具结果:"]
for index, tool_call in enumerate(tool_calls, start=1):
if tool_call.get("success"):
lines.append(
f"{index}. 工具={tool_call.get('tool_name')} 结果={json.dumps(tool_call.get('result', {}), ensure_ascii=False)}"
)
else:
lines.append(
f"{index}. 工具={tool_call.get('tool_name')} 失败={tool_call.get('error', '未知错误')}"
)
return "\n".join(lines)

View File

@@ -1,104 +0,0 @@
from pathlib import Path
from django.conf import settings
from agent_core.llm_provider import create_embedding_provider
def _client(path: str | Path | None = None):
"""按给定路径初始化 Chroma 持久化客户端。"""
import chromadb
resolved_path = str(path or settings.CHROMA_PATH)
return chromadb.PersistentClient(path=resolved_path)
def _embedding_provider():
"""从 Django settings 构造 Embedding Provider避免在业务层散落配置读取。"""
return create_embedding_provider(
{
"EMBEDDING_API_KEY": settings.EMBEDDING_API_KEY,
"EMBEDDING_BASE_URL": settings.EMBEDDING_BASE_URL,
"EMBEDDING_MODEL": settings.EMBEDDING_MODEL,
}
)
def upsert_chunks(
collection: str,
chunks: list[dict],
store_path: str | Path | None = None,
) -> None:
"""
将 chunk 写入 Chroma。
同一 document_id 重新入库前会先删除旧记录,保证一次文档只有一份有效向量数据。
"""
client = _client(store_path)
chroma_collection = client.get_or_create_collection(collection)
document_ids = {chunk["document_id"] for chunk in chunks if chunk.get("document_id") is not None}
for document_id in document_ids:
chroma_collection.delete(where={"document_id": document_id})
texts = [chunk["content"] for chunk in chunks]
embeddings = _embedding_provider().embed_texts(texts)
chroma_collection.upsert(
ids=[chunk["chunk_id"] for chunk in chunks],
documents=texts,
embeddings=embeddings,
metadatas=[
{
"scenario_id": chunk["scenario_id"],
"document_id": chunk["document_id"],
"source": chunk["source"],
"chunk_id": chunk["chunk_id"],
"created_at": chunk["created_at"],
}
for chunk in chunks
],
)
def query_chunks(
scenario_id: str,
query: str,
collection: str,
top_k: int = 5,
document_ids: list[int] | None = None,
store_path: str | Path | None = None,
) -> list[dict]:
"""执行向量检索,并把 Chroma 原始结果转换为统一引用结构。"""
client = _client(store_path)
chroma_collection = client.get_or_create_collection(collection)
where: dict = {"scenario_id": scenario_id}
if document_ids:
where = {
"$and": [
{"scenario_id": scenario_id},
{"document_id": {"$in": document_ids}},
]
}
embedding = _embedding_provider().embed_texts([query])[0]
result = chroma_collection.query(
query_embeddings=[embedding],
n_results=top_k,
where=where,
include=["documents", "metadatas", "distances"],
)
chunks = []
documents = result.get("documents", [[]])[0]
metadatas = result.get("metadatas", [[]])[0]
distances = result.get("distances", [[]])[0]
for content, metadata, distance in zip(documents, metadatas, distances):
chunks.append(
{
"scenario_id": metadata.get("scenario_id"),
"document_id": metadata.get("document_id"),
"collection": collection,
"source": metadata.get("source"),
"chunk_id": metadata.get("chunk_id"),
"content": content,
"created_at": metadata.get("created_at"),
"score": round(1 / (1 + float(distance)), 4),
}
)
return chunks

View File

@@ -1,171 +0,0 @@
import importlib.util
import json
import re
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from django.conf import settings
from .chroma_store import upsert_chunks
@dataclass
class IngestResult:
"""RAG 入库统一返回结构,供 Documents 模块稳定消费。"""
success: bool
chunks_count: int = 0
error: str = ""
def ingest_document(
scenario_id: str,
source_file: str,
text: str,
collection: str,
document_id: int | None = None,
store_path: str | Path | None = None,
) -> IngestResult:
"""
将单个文档文本切分后写入知识库。
运行策略:
- 如果显式传入 `store_path`,说明当前是测试或降级模式,走本地 JSON 存储。
- 如果未传入且环境可用 chromadb则走真实 Chroma 持久化。
"""
if not text.strip():
return IngestResult(success=False, error="文档内容为空")
if _should_use_chroma(store_path):
return _ingest_chroma_document(
document_id=document_id,
scenario_id=scenario_id,
source_file=source_file,
text=text,
collection=collection,
)
resolved_store_path = Path(store_path) if store_path else _default_store_path()
chunks = _build_chunks(
scenario_id=scenario_id,
source_file=source_file,
text=text,
collection=collection,
document_id=document_id,
chunk_id_prefix=source_file,
)
persisted_chunks = _filter_out_same_document_chunks(
_load_store(resolved_store_path),
scenario_id=scenario_id,
collection=collection,
document_id=document_id,
)
_save_store(resolved_store_path, [*persisted_chunks, *chunks])
return IngestResult(success=True, chunks_count=len(chunks))
def _should_use_chroma(store_path: str | Path | None) -> bool:
"""只在未指定测试存储路径且安装 chromadb 时启用真实向量库。"""
return store_path is None and importlib.util.find_spec("chromadb") is not None
def _default_store_path() -> Path:
return Path(settings.CHROMA_PATH) / "rag_store.json"
def _load_store(store_path: Path) -> list[dict]:
if not store_path.exists():
return []
with store_path.open("r", encoding="utf-8") as file:
return json.load(file)
def _save_store(store_path: Path, chunks: list[dict]) -> None:
store_path.parent.mkdir(parents=True, exist_ok=True)
with store_path.open("w", encoding="utf-8") as file:
json.dump(chunks, file, ensure_ascii=False, indent=2)
def _split_text(text: str, chunk_size: int = 800, overlap: int = 120) -> list[str]:
"""
使用固定窗口 + overlap 切分文本。
该策略简单但稳定,便于解释:
- chunk_size 控制每个片段最大长度
- overlap 保证相邻片段共享上下文,降低边界信息丢失
"""
normalized = re.sub(r"\s+", " ", text).strip()
if not normalized:
return []
chunks = []
start = 0
while start < len(normalized):
end = start + chunk_size
chunks.append(normalized[start:end])
if end >= len(normalized):
break
start = max(end - overlap, start + 1)
return chunks
def _build_chunks(
scenario_id: str,
source_file: str,
text: str,
collection: str,
document_id: int | None,
chunk_id_prefix: str,
) -> list[dict]:
"""把原始文本切分并封装为统一 chunk 结构。"""
created_at = datetime.now(timezone.utc).isoformat()
return [
{
"scenario_id": scenario_id,
"document_id": document_id,
"collection": collection,
"source": source_file,
"chunk_id": f"{scenario_id}:{chunk_id_prefix}:{index}",
"content": chunk_text,
"created_at": created_at,
}
for index, chunk_text in enumerate(_split_text(text), start=1)
]
def _filter_out_same_document_chunks(
chunks: list[dict],
scenario_id: str,
collection: str,
document_id: int | None,
) -> list[dict]:
"""重新入库同一 document_id 时,先删除旧 chunk避免重复检索。"""
return [
chunk
for chunk in chunks
if not (
chunk.get("document_id") == document_id
and chunk.get("scenario_id") == scenario_id
and chunk.get("collection") == collection
)
]
def _ingest_chroma_document(
document_id: int | None,
scenario_id: str,
source_file: str,
text: str,
collection: str,
) -> IngestResult:
"""真实 Chroma 模式的入库分支。"""
chunks = _build_chunks(
scenario_id=scenario_id,
source_file=source_file,
text=text,
collection=collection,
document_id=document_id,
chunk_id_prefix=str(document_id or source_file),
)
try:
upsert_chunks(collection=collection, chunks=chunks)
except Exception as exc:
return IngestResult(success=False, error=str(exc))
return IngestResult(success=True, chunks_count=len(chunks))

View File

@@ -1,105 +0,0 @@
import importlib.util
import json
import re
from pathlib import Path
from django.conf import settings
from .chroma_store import query_chunks
def retrieve(
scenario_id: str,
query: str,
collection: str,
top_k: int = 5,
document_ids: list[int] | None = None,
store_path: str | Path | None = None,
) -> list[dict]:
"""
统一对外提供检索入口。
与 ingest_document 保持一致:
- 真实运行优先走 Chroma
- 测试或降级模式走本地 JSON + 轻量文本打分
"""
if _should_use_chroma(store_path):
return query_chunks(
scenario_id=scenario_id,
query=query,
collection=collection,
top_k=top_k,
document_ids=document_ids,
)
resolved_store_path = Path(store_path) if store_path else _default_store_path()
query_tokens = _tokens(query)
allowed_document_ids = set(document_ids or [])
scored_chunks = []
for chunk in _load_store(resolved_store_path):
if not _matches_scope(
chunk=chunk,
scenario_id=scenario_id,
collection=collection,
allowed_document_ids=allowed_document_ids,
):
continue
score = _score(query_tokens, chunk.get("content", ""))
if score <= 0:
continue
scored_chunks.append({**chunk, "score": score})
return sorted(scored_chunks, key=lambda item: item["score"], reverse=True)[:top_k]
def _should_use_chroma(store_path: str | Path | None) -> bool:
return store_path is None and importlib.util.find_spec("chromadb") is not None
def _default_store_path() -> Path:
return Path(settings.CHROMA_PATH) / "rag_store.json"
def _load_store(store_path: Path) -> list[dict]:
if not store_path.exists():
return []
with store_path.open("r", encoding="utf-8") as file:
return json.load(file)
def _matches_scope(
chunk: dict,
scenario_id: str,
collection: str,
allowed_document_ids: set[int],
) -> bool:
"""先按场景、collection 和可选文档范围过滤,再进行相关性打分。"""
if chunk.get("scenario_id") != scenario_id:
return False
if chunk.get("collection") != collection:
return False
if allowed_document_ids and chunk.get("document_id") not in allowed_document_ids:
return False
return True
def _tokens(text: str) -> set[str]:
"""
兼容中英文的轻量分词策略。
该分词仅用于 fallback 模式,不替代真实向量检索:
- 英文/数字按词提取
- 中文按连续词片段和单字同时保留,提升短查询命中率
"""
lowered = text.lower()
ascii_tokens = set(re.findall(r"[a-z0-9_]+", lowered))
cjk_tokens = set(re.findall(r"[\u4e00-\u9fff]{2,}", lowered))
chars = {char for char in lowered if "\u4e00" <= char <= "\u9fff"}
return ascii_tokens | cjk_tokens | chars
def _score(query_tokens: set[str], content: str) -> float:
"""使用交集占比计算一个便于排序的简化相关性分数。"""
content_tokens = _tokens(content)
if not query_tokens or not content_tokens:
return 0.0
overlap = query_tokens & content_tokens
return round(len(overlap) / len(query_tokens), 4)

View File

@@ -1,22 +0,0 @@
from dataclasses import dataclass, field
@dataclass
class AgentResult:
"""
Agent Core 对 Django 层暴露的统一结果对象。
任何底层编排实现都必须返回这一结构,确保:
- Chat 页面有稳定字段可展示
- Audit 模块有稳定字段可落库
- 未来替换编排引擎时不影响 Django 业务层
"""
answer: str = ""
structured_output: dict = field(default_factory=dict)
references: list = field(default_factory=list)
tool_calls: list = field(default_factory=list)
raw_output: str = ""
model_name: str = "mock-model"
latency_ms: int = 0
status: str = "success"
error: str = ""

View File

@@ -1,7 +0,0 @@
SUPPORTED_OUTPUT_TYPES = {
"general_answer",
"document_review_report",
"ticket_response",
"quality_report",
"risk_audit_report",
}

View File

@@ -1,142 +0,0 @@
import json
from .schemas.outputs import SUPPORTED_OUTPUT_TYPES
# 按输出类型声明页面和审计日志真正需要消费的结构化字段。
# 这里不追求复杂 schema 框架,优先保证字段稳定、可读、易讲解。
OUTPUT_FIELD_TEMPLATES = {
"general_answer": {
"answer": "",
"confidence": "medium",
"references": [],
},
"document_review_report": {
"summary": "",
"issues": [],
"risk_level": "medium",
"suggestions": [],
"missing_items": [],
"references": [],
},
"ticket_response": {
"reply": "",
"category": "general",
"priority": "medium",
"suggested_action": "",
"need_human_review": False,
},
"quality_report": {
"summary": "",
"possible_causes": [],
"evidence": [],
"risk_level": "medium",
"suggested_actions": [],
"references": [],
},
"risk_audit_report": {
"summary": "",
"risk_points": [],
"risk_level": "medium",
"suggestions": [],
"references": [],
},
}
def build_response_schema_hint(output_type: str) -> dict:
"""返回给 LLM 的结构化提示,帮助模型尽量输出稳定 JSON。"""
normalized_output_type = normalize_output_type(output_type)
return {
"output_type": normalized_output_type,
"fields": list(OUTPUT_FIELD_TEMPLATES[normalized_output_type].keys()),
}
def normalize_output_type(output_type: str) -> str:
"""对外部配置做轻量归一化,避免拼写差异导致解析分支混乱。"""
if output_type in SUPPORTED_OUTPUT_TYPES:
return output_type
return "general_answer"
def parse_structured_output(raw_content: str, output_type: str) -> tuple[dict, str]:
"""
优先将模型输出解析为 JSON。
返回值:
- structured_output: 页面和审计日志可直接消费的标准结构
- parse_mode: `json` 表示成功解析,`fallback` 表示降级处理
"""
normalized_output_type = normalize_output_type(output_type)
parsed = _try_parse_json_object(raw_content)
if parsed is None:
return build_fallback_structured_output(
output_type=normalized_output_type,
raw_content=raw_content,
), "fallback"
template = {
"output_type": normalized_output_type,
"parse_mode": "json",
}
template.update(OUTPUT_FIELD_TEMPLATES[normalized_output_type])
template.update(parsed)
return template, "json"
def build_fallback_structured_output(output_type: str, raw_content: str) -> dict:
"""当模型没有输出合法 JSON 时,仍然构造一个稳定的展示结构。"""
normalized_output_type = normalize_output_type(output_type)
structured_output = {
"output_type": normalized_output_type,
"parse_mode": "fallback",
}
structured_output.update(OUTPUT_FIELD_TEMPLATES[normalized_output_type])
if normalized_output_type == "general_answer":
structured_output["answer"] = raw_content
return structured_output
if normalized_output_type == "document_review_report":
structured_output["summary"] = raw_content
return structured_output
if normalized_output_type == "ticket_response":
structured_output["reply"] = raw_content
return structured_output
if normalized_output_type == "quality_report":
structured_output["summary"] = raw_content
return structured_output
structured_output["summary"] = raw_content
return structured_output
def extract_answer_from_structured_output(structured_output: dict, raw_content: str) -> str:
"""从结构化结果里提取页面主回答,保证不同输出类型有统一入口。"""
for field_name in ("answer", "reply", "summary"):
value = structured_output.get(field_name)
if isinstance(value, str) and value.strip():
return value.strip()
return raw_content.strip()
def _try_parse_json_object(raw_content: str) -> dict | None:
"""支持纯 JSON 或被 Markdown 代码块包裹的 JSON。"""
content = raw_content.strip()
if not content:
return None
candidates = [content]
if content.startswith("```"):
stripped = content.strip("`").strip()
if stripped.lower().startswith("json"):
stripped = stripped[4:].strip()
candidates.append(stripped)
for candidate in candidates:
try:
parsed = json.loads(candidate)
except json.JSONDecodeError:
continue
if isinstance(parsed, dict):
return parsed
return None

View File

@@ -1,70 +0,0 @@
from collections.abc import Callable
from .tools.builtin_tools import BUILTIN_TOOLS
class ToolRegistry:
"""
统一管理工具注册、查询和执行。
设计目标:
- 让 Orchestrator 只关心“声明了哪些工具”,不关心工具如何存放。
- 固化统一的工具调用结果结构,便于页面展示和审计日志保存。
- 后续新增业务工具时,只需要注册函数,不必改调用协议。
"""
def __init__(self, initial_tools: dict[str, Callable] | None = None):
self._tools: dict[str, Callable] = dict(initial_tools or {})
def register(self, tool_name: str, tool_func: Callable) -> None:
"""注册一个可通过名称调用的工具函数。"""
self._tools[tool_name] = tool_func
def get(self, tool_name: str) -> Callable | None:
"""按名称返回工具函数;未注册时返回 None。"""
return self._tools.get(tool_name)
def run(self, tool_name: str, **kwargs) -> dict:
"""
执行单个工具,并返回统一结果结构。
统一返回值是审计日志、页面展示和后续 Agent 编排共享的协议。
即使工具不存在或执行失败,也返回可消费的失败结果,而不是抛异常。
"""
tool = self.get(tool_name)
if tool is None:
return {
"tool_name": tool_name,
"success": False,
"arguments": kwargs,
"result": {},
"error": "工具未注册",
}
try:
return {
"tool_name": tool_name,
"success": True,
"arguments": kwargs,
"result": tool(**kwargs),
"error": "",
}
except Exception as exc:
return {
"tool_name": tool_name,
"success": False,
"arguments": kwargs,
"result": {},
"error": str(exc),
}
# 默认注册表承载项目内置工具,便于当前 V1 直接复用。
DEFAULT_TOOL_REGISTRY = ToolRegistry(BUILTIN_TOOLS)
def run_declared_tools(tool_names: list[str], user_input: str) -> list[dict]:
"""按场景声明顺序执行工具,保证结果顺序与配置顺序一致。"""
return [
DEFAULT_TOOL_REGISTRY.run(tool_name, user_input=user_input)
for tool_name in tool_names
]

View File

@@ -1,124 +0,0 @@
import re
def calculate_rate(user_input: str) -> dict:
"""
从自然语言中提取两个数值并计算比例。
V1 目标不是构建复杂公式引擎,而是提供一个可演示的“业务工具”示例:
只要输入中出现两个数字,就将其解释为“已完成值 / 总数”。
"""
numbers = [float(item) for item in re.findall(r"\d+(?:\.\d+)?", user_input)]
if len(numbers) < 2:
return {
"success": False,
"rate": 0.0,
"numerator": 0.0,
"denominator": 0.0,
"note": "未能从输入中提取两个数字,无法计算比例。",
}
numerator, denominator = numbers[0], numbers[1]
if denominator == 0:
return {
"success": False,
"rate": 0.0,
"numerator": numerator,
"denominator": denominator,
"note": "分母为 0无法计算比例。",
}
return {
"success": True,
"numerator": numerator,
"denominator": denominator,
"rate": round(numerator / denominator, 4),
"note": "已按输入中的前两个数字完成比例计算。",
}
def query_demo_records(user_input: str) -> dict:
"""
查询示例业务记录。
该工具依赖 Audit 模块中的 DemoBusinessRecord 演示表,用于证明
“场景 + 结构化数据 + 工具调用”可以组成更可信的业务 Agent。
"""
try:
from apps.audit.models import DemoBusinessRecord
except Exception as exc:
return {"records": [], "error": str(exc)}
queryset = DemoBusinessRecord.objects.all()
tokens = {token.strip().lower() for token in user_input.split() if token.strip()}
scenario_ids = set(queryset.values_list("scenario_id", flat=True))
record_types = set(queryset.values_list("record_type", flat=True))
matched_scenario_ids = scenario_ids & tokens
matched_record_types = record_types & tokens
if matched_scenario_ids:
queryset = queryset.filter(scenario_id__in=matched_scenario_ids)
if matched_record_types:
queryset = queryset.filter(record_type__in=matched_record_types)
records = [
{
"id": record.id,
"scenario_id": record.scenario_id,
"record_type": record.record_type,
"title": record.title,
"payload": record.payload,
}
for record in queryset[:20]
]
return {"records": records}
def check_required_fields(user_input: str) -> dict:
"""
检查输入中声明的必填项是否全部出现。
约定格式示例:
“请检查必填项:合同编号、供应商、金额。当前只提供了合同编号和金额。”
"""
required_match = re.search(r"必填项[:](.+?)(?:。|\.)", user_input)
provided_match = re.search(r"(?:当前|已|仅)?提供了(.+?)(?:。|\.)", user_input)
required_fields = _split_cn_items(required_match.group(1) if required_match else "")
provided_fields = set(_split_cn_items(provided_match.group(1) if provided_match else ""))
missing_fields = [field for field in required_fields if field not in provided_fields]
return {
"required_fields": required_fields,
"provided_fields": list(provided_fields),
"missing_fields": missing_fields,
"note": "已根据输入中的“必填项/提供了”描述完成检查。",
}
def generate_action_items(user_input: str) -> dict:
"""
生成最小可执行行动项。
该工具主要用于演示“模型回答之外,还可以得到结构化待办建议”。
"""
return {
"items": [
"先确认问题背景和适用场景。",
f"围绕当前问题继续核实:{user_input}",
"根据知识库和审计结果安排下一步处理动作。",
]
}
def _split_cn_items(raw_text: str) -> list[str]:
"""将中文顿号、逗号和连接词分隔的字段串切分为列表。"""
normalized = (
raw_text.replace("", "")
.replace("以及", "")
.replace(",", "")
.replace("", "")
)
return [item.strip(" 。.") for item in normalized.split("") if item.strip(" 。.")]
BUILTIN_TOOLS = {
"calculate_rate": calculate_rate,
"query_demo_records": query_demo_records,
"check_required_fields": check_required_fields,
"generate_action_items": generate_action_items,
}

View File

@@ -1 +0,0 @@

View File

@@ -1,19 +0,0 @@
from django.contrib import admin
from .models import AgentAuditLog, DemoBusinessRecord
@admin.register(AgentAuditLog)
class AgentAuditLogAdmin(admin.ModelAdmin):
"""便于在 Django Admin 中快速查看一次 Agent 执行的关键信息。"""
list_display = ("id", "scenario_name", "status", "model_name", "latency_ms", "created_at")
list_filter = ("status", "scenario_id")
search_fields = ("scenario_id", "scenario_name", "user_input", "final_answer")
@admin.register(DemoBusinessRecord)
class DemoBusinessRecordAdmin(admin.ModelAdmin):
"""管理工具查询依赖的示例业务记录。"""
list_display = ("id", "title", "scenario_id", "record_type", "created_at")
list_filter = ("scenario_id", "record_type")
search_fields = ("title", "scenario_id", "record_type")

View File

@@ -1,7 +0,0 @@
from django.apps import AppConfig
class AuditConfig(AppConfig):
"""Audit 模块应用配置。"""
default_auto_field = "django.db.models.BigAutoField"
name = "apps.audit"

View File

@@ -1,46 +0,0 @@
# Generated by Django 5.2.14 on 2026-05-29 13:39
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name="AgentAuditLog",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("scenario_id", models.CharField(db_index=True, max_length=100)),
("scenario_name", models.CharField(blank=True, max_length=200)),
("user_input", models.TextField()),
("retrieved_chunks", models.JSONField(blank=True, default=list)),
("tool_calls", models.JSONField(blank=True, default=list)),
("structured_output", models.JSONField(blank=True, default=dict)),
("final_answer", models.TextField(blank=True)),
("raw_output", models.TextField(blank=True)),
("model_name", models.CharField(blank=True, max_length=100)),
("latency_ms", models.PositiveIntegerField(default=0)),
(
"status",
models.CharField(db_index=True, default="success", max_length=20),
),
("error_message", models.TextField(blank=True)),
("created_at", models.DateTimeField(auto_now_add=True, db_index=True)),
],
options={
"ordering": ["-created_at"],
},
),
]

View File

@@ -1,35 +0,0 @@
# Generated for V1 demo business records.
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("audit", "0001_initial"),
]
operations = [
migrations.CreateModel(
name="DemoBusinessRecord",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("scenario_id", models.CharField(db_index=True, max_length=100)),
("record_type", models.CharField(db_index=True, max_length=100)),
("title", models.CharField(max_length=255)),
("payload", models.JSONField(blank=True, default=dict)),
("created_at", models.DateTimeField(auto_now_add=True)),
],
options={
"ordering": ["-created_at"],
},
),
]

View File

@@ -1,68 +0,0 @@
from django.db import models
class AgentAuditLog(models.Model):
"""
保存一次 Agent 执行的完整审计快照。
该模型是“系统可解释性”的核心:
- 对话页负责触发执行
- Agent Core 负责生成结果
- Audit 模型负责长期保存输入、引用、工具调用和模型输出
"""
# 审计状态需要同时服务数据库检索和前端展示。
STATUS_SUCCESS = "success"
STATUS_FAILED = "failed"
scenario_id = models.CharField(max_length=100, db_index=True)
scenario_name = models.CharField(max_length=200, blank=True)
user_input = models.TextField()
retrieved_chunks = models.JSONField(default=list, blank=True)
tool_calls = models.JSONField(default=list, blank=True)
structured_output = models.JSONField(default=dict, blank=True)
final_answer = models.TextField(blank=True)
raw_output = models.TextField(blank=True)
model_name = models.CharField(max_length=100, blank=True)
latency_ms = models.PositiveIntegerField(default=0)
status = models.CharField(max_length=20, default=STATUS_SUCCESS, db_index=True)
error_message = models.TextField(blank=True)
created_at = models.DateTimeField(auto_now_add=True, db_index=True)
class Meta:
ordering = ["-created_at"]
def __str__(self) -> str:
return f"{self.scenario_name or self.scenario_id} #{self.pk}"
def get_status_display_text(self) -> str:
"""返回更适合页面展示的中文状态。"""
return {
self.STATUS_SUCCESS: "执行成功",
self.STATUS_FAILED: "执行失败",
}.get(self.status, self.status)
def get_user_input_summary(self, max_length: int = 28) -> str:
"""在列表页展示用户输入摘要,避免长文本撑破表格。"""
if len(self.user_input) <= max_length:
return self.user_input
return f"{self.user_input[:max_length]}..."
class DemoBusinessRecord(models.Model):
"""
演示用业务记录表。
该表不直接参与页面主流程,而是供内置工具 `query_demo_records`
查询,证明 Agent 除知识库外也可以结合结构化业务数据。
"""
scenario_id = models.CharField(max_length=100, db_index=True)
record_type = models.CharField(max_length=100, db_index=True)
title = models.CharField(max_length=255)
payload = models.JSONField(default=dict, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ["-created_at"]
def __str__(self) -> str:
return self.title

View File

@@ -1,57 +0,0 @@
from agent_core.results import AgentResult
from .models import AgentAuditLog
def create_audit_log(
scenario_id: str,
scenario_name: str,
user_input: str,
agent_result: AgentResult,
) -> AgentAuditLog:
"""
将一次 Agent 执行结果落库为审计日志。
设计原则:
- 成功与失败都必须记录,方便复盘整条执行链路
- 敏感信息在写库前先脱敏,避免误存 API Key
- 对前端和 Django Model 统一输出稳定字段
"""
return AgentAuditLog.objects.create(
scenario_id=scenario_id,
scenario_name=scenario_name,
user_input=user_input,
retrieved_chunks=agent_result.references,
tool_calls=agent_result.tool_calls,
structured_output=agent_result.structured_output,
final_answer=agent_result.answer,
raw_output=agent_result.raw_output,
model_name=agent_result.model_name,
latency_ms=max(agent_result.latency_ms, 0),
status=agent_result.status,
error_message=mask_sensitive_text(agent_result.error),
)
def mask_sensitive_text(value: str) -> str:
"""
对错误文本中的敏感配置进行脱敏。
当前至少处理:
- `LLM_API_KEY=...`
- `EMBEDDING_API_KEY=...`
"""
masked = value
for marker in ("LLM_API_KEY=", "EMBEDDING_API_KEY="):
masked = _mask_token_after_marker(masked, marker)
return masked
def _mask_token_after_marker(value: str, marker: str) -> str:
"""将 marker 后紧跟的 token 替换为脱敏占位符。"""
if marker not in value:
return value
prefix, _, suffix = value.partition(marker)
secret, separator, rest = suffix.partition(" ")
masked_secret = "sk-***" if secret.startswith("sk-") else "***"
return f"{prefix}{marker}{masked_secret}{separator}{rest}"

View File

@@ -1,12 +0,0 @@
from django.urls import path
from . import views
app_name = "audit"
# V1 的审计功能由列表页和详情页组成,暂不拆分页或复杂筛选接口。
urlpatterns = [
path("", views.log_list, name="list"),
path("<int:log_id>/", views.log_detail, name="detail"),
]

View File

@@ -1,26 +0,0 @@
from django.shortcuts import get_object_or_404, render
from .models import AgentAuditLog
def log_list(request):
# 列表页支持按场景筛选,方便演示时快速定位同一类场景的执行记录。
scenario_id = (request.GET.get("scenario_id") or "").strip()
logs = AgentAuditLog.objects.all()
if scenario_id:
logs = logs.filter(scenario_id=scenario_id)
return render(
request,
"audit/log_list.html",
{
"logs": logs,
"selected_scenario_id": scenario_id,
},
)
def log_detail(request, log_id: int):
# 详情页只负责按主键加载审计快照并渲染;
# 所有脱敏和字段映射都应在服务层完成。
audit_log = get_object_or_404(AgentAuditLog, pk=log_id)
return render(request, "audit/log_detail.html", {"log": audit_log})

View File

@@ -1 +0,0 @@

View File

@@ -1,7 +0,0 @@
from django.apps import AppConfig
class ChatConfig(AppConfig):
"""Chat 模块应用配置。"""
default_auto_field = "django.db.models.BigAutoField"
name = "apps.chat"

View File

@@ -1,40 +0,0 @@
from django import forms
class ChatForm(forms.Form):
# 该表单只负责收集用户问题和可选文档范围,
# 不承载任何 Agent 业务逻辑,便于在 View 层保持轻量。
message = forms.CharField(
label="问题",
max_length=4000,
error_messages={
"required": "请输入要咨询的问题。",
"max_length": "问题过长,请控制在 4000 字以内。",
},
widget=forms.Textarea(
attrs={
"rows": 8,
"placeholder": "例如:请结合已上传 SOP分析当前异常的原因、风险等级和建议动作。",
}
),
)
document_ids = forms.MultipleChoiceField(
label="文档范围",
required=False,
choices=(),
widget=forms.CheckboxSelectMultiple,
error_messages={"invalid_choice": "请选择当前场景下已入库的文档。"},
)
def __init__(self, *args, documents=None, **kwargs):
super().__init__(*args, **kwargs)
documents = documents or []
# 仅允许选择当前场景且已完成入库的文档,
# 避免前端把无效文件范围传入 Agent Core。
self.fields["document_ids"].choices = [
(str(document.id), document.original_name) for document in documents
]
def clean_document_ids(self):
# View 与 Agent Core 都使用整型文档 ID统一在表单层完成转换。
return [int(document_id) for document_id in self.cleaned_data.get("document_ids", [])]

View File

@@ -1,11 +0,0 @@
from django.urls import path
from . import views
app_name = "chat"
# 当前 V1 仅保留一个场景对话入口,场景详情合并在对话页中展示。
urlpatterns = [
path("<str:scenario_id>/", views.index, name="index"),
]

View File

@@ -1,60 +0,0 @@
from django.shortcuts import render
from agent_core.orchestrator import run_agent
from agent_core.results import AgentResult
from apps.audit.services import create_audit_log
from apps.documents.models import UploadedDocument
from apps.scenarios.services import ScenarioNotFound, get_scenario
from .forms import ChatForm
def index(request, scenario_id: str):
# View 只负责请求编排、表单校验和模板渲染。
# 具体 Agent 执行、审计写入和文档筛选规则分别交给独立模块处理。
try:
scenario = get_scenario(scenario_id)
except ScenarioNotFound:
return render(
request,
"chat/index.html",
{
"scenario": None,
"form": ChatForm(),
"error": "场景不存在,请返回首页检查配置。",
},
status=404,
)
result = None
audit_log = None
documents = UploadedDocument.objects.filter(
scenario_id=scenario["id"],
status=UploadedDocument.STATUS_INDEXED,
)
form = ChatForm(request.POST or None, documents=documents)
if request.method == "POST" and form.is_valid():
message = form.cleaned_data["message"]
try:
# 只把必要的运行选项传给 Agent Core避免在 View 中散落模型细节。
result = run_agent(
scenario,
message,
options={"document_ids": form.cleaned_data["document_ids"]},
)
except Exception as exc:
result = AgentResult(status="failed", error=str(exc), answer="")
audit_log = create_audit_log(scenario["id"], scenario["name"], message, result)
return render(
request,
"chat/index.html",
{
"scenario": scenario,
"form": form,
"documents": documents,
"document_count": documents.count(),
"result": result,
"audit_log": audit_log,
},
)

View File

@@ -1 +0,0 @@

View File

@@ -1,11 +0,0 @@
from django.contrib import admin
from .models import UploadedDocument
@admin.register(UploadedDocument)
class UploadedDocumentAdmin(admin.ModelAdmin):
"""管理上传文档及其入库状态,便于后台排查问题。"""
list_display = ("id", "original_name", "scenario_id", "file_type", "status", "created_at")
list_filter = ("status", "scenario_id", "file_type")
search_fields = ("original_name", "scenario_id")

View File

@@ -1,7 +0,0 @@
from django.apps import AppConfig
class DocumentsConfig(AppConfig):
"""Documents 模块应用配置。"""
default_auto_field = "django.db.models.BigAutoField"
name = "apps.documents"

View File

@@ -1,37 +0,0 @@
from pathlib import Path
from django import forms
from apps.scenarios.services import ScenarioNotFound, get_scenario
from apps.scenarios.services import list_scenarios
SUPPORTED_EXTENSIONS = {".txt", ".md", ".pdf", ".docx"}
class DocumentUploadForm(forms.Form):
# 使用 ChoiceField 让表单自己维护场景选项,
# 这样模板、校验和后续扩展都能围绕一个入口完成。
scenario_id = forms.ChoiceField(label="场景", choices=())
file = forms.FileField(label="文件")
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["scenario_id"].choices = [
(scenario["id"], scenario["name"])
for scenario in list_scenarios()
]
def clean_scenario_id(self):
scenario_id = self.cleaned_data["scenario_id"]
try:
get_scenario(scenario_id)
except ScenarioNotFound as exc:
raise forms.ValidationError("场景不存在") from exc
return scenario_id
def clean_file(self):
uploaded_file = self.cleaned_data["file"]
extension = Path(uploaded_file.name).suffix.lower()
if extension not in SUPPORTED_EXTENSIONS:
raise forms.ValidationError("仅支持 .txt、.md、.pdf 和 .docx 文件")
return uploaded_file

View File

@@ -1,42 +0,0 @@
# Generated by Django 5.2.14 on 2026-05-29 13:41
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name="UploadedDocument",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("scenario_id", models.CharField(db_index=True, max_length=100)),
("original_name", models.CharField(max_length=255)),
("file", models.FileField(upload_to="documents/%Y%m%d/")),
("file_type", models.CharField(max_length=20)),
("size", models.PositiveIntegerField(default=0)),
(
"status",
models.CharField(db_index=True, default="uploaded", max_length=20),
),
("error_message", models.TextField(blank=True)),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
],
options={
"ordering": ["-created_at"],
},
),
]

View File

@@ -1,38 +0,0 @@
from django.db import models
class UploadedDocument(models.Model):
"""
保存用户上传文档的元数据和入库状态。
设计上只记录“文件属于哪个场景、当前是否已入库、失败原因是什么”,
不把 RAG 细节耦合进模型层。
"""
# 文档状态用于驱动前端提示和后续可操作项。
STATUS_UPLOADED = "uploaded"
STATUS_INDEXED = "indexed"
STATUS_FAILED = "failed"
scenario_id = models.CharField(max_length=100, db_index=True)
original_name = models.CharField(max_length=255)
file = models.FileField(upload_to="documents/%Y%m%d/")
file_type = models.CharField(max_length=20)
size = models.PositiveIntegerField(default=0)
status = models.CharField(max_length=20, default=STATUS_UPLOADED, db_index=True)
error_message = models.TextField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ["-created_at"]
def __str__(self) -> str:
return self.original_name
def get_status_display_text(self) -> str:
"""为模板提供更适合演示的中文状态文案。"""
return {
self.STATUS_UPLOADED: "已上传,待入库",
self.STATUS_INDEXED: "已入库,可检索",
self.STATUS_FAILED: "入库失败",
}.get(self.status, self.status)

View File

@@ -1,127 +0,0 @@
from pathlib import Path
import re
import xml.etree.ElementTree as ET
from zipfile import BadZipFile, ZipFile
from agent_core.rag.ingest import ingest_document
from .models import UploadedDocument
def create_uploaded_document(scenario_id: str, uploaded_file) -> UploadedDocument:
"""
保存上传文件的元数据记录。
Documents 模块只记录文件与场景关系、原始名称、类型和大小,
真正的入库动作由用户后续主动触发,避免上传阶段就耦合 RAG 流程。
"""
extension = _detect_extension(uploaded_file.name)
return UploadedDocument.objects.create(
scenario_id=scenario_id,
original_name=uploaded_file.name,
file=uploaded_file,
file_type=extension,
size=uploaded_file.size,
status=UploadedDocument.STATUS_UPLOADED,
)
def extract_text(document: UploadedDocument) -> str:
"""
根据文档类型选择合适的文本抽取策略。
V1 的目标是“可演示且稳定”,因此:
- `.txt` / `.md` 直接按文本读取
- `.pdf` 优先走 pypdf失败时回退为二进制容错读取
- `.docx` 优先解析 Word XML失败时回退为二进制容错读取
"""
path = Path(document.file.path)
extension = f".{document.file_type.lower().lstrip('.')}"
if extension == ".pdf":
return _extract_pdf_text(path)
if extension == ".docx":
return _extract_docx_text(path)
return _read_text_file(path)
def index_document(document: UploadedDocument) -> UploadedDocument:
"""
触发单个文档入库,并把成功/失败状态回写到 UploadedDocument。
这里故意不抛业务异常给 View
View 层只需要知道“最终状态是什么”,而错误信息统一落到模型字段中,
便于页面重试和演示。
"""
try:
text = extract_text(document)
ingest_result = ingest_document(
document_id=document.id,
scenario_id=document.scenario_id,
source_file=document.original_name,
text=text,
collection=document.scenario_id,
)
_apply_ingest_result(document, ingest_result.success, ingest_result.error)
except Exception as exc:
_apply_ingest_result(document, success=False, error=str(exc))
document.save(update_fields=["status", "error_message", "updated_at"])
return document
def _apply_ingest_result(document: UploadedDocument, success: bool, error: str = "") -> None:
"""把入库结果映射为 UploadedDocument 的稳定状态字段。"""
if success:
document.status = UploadedDocument.STATUS_INDEXED
document.error_message = ""
return
document.status = UploadedDocument.STATUS_FAILED
document.error_message = error
def _detect_extension(file_name: str) -> str:
"""统一将扩展名转成小写且去掉前导点,便于模型字段存储。"""
return Path(file_name).suffix.lower().lstrip(".")
def _read_text_file(path: Path) -> str:
"""优先按 UTF-8 读取;失败时回退到系统默认编码。"""
try:
return path.read_text(encoding="utf-8")
except UnicodeDecodeError:
return path.read_text()
def _extract_pdf_text(path: Path) -> str:
"""优先使用 pypdf 抽取 PDF 文本,失败时回退到容错方案。"""
try:
import pypdf
reader = pypdf.PdfReader(str(path))
return "\n".join(page.extract_text() or "" for page in reader.pages)
except Exception:
return _read_binary_text_fallback(path)
def _extract_docx_text(path: Path) -> str:
"""提取 Word XML 中的可见文字内容,不追求保留样式。"""
try:
with ZipFile(path) as archive:
document_xml = archive.read("word/document.xml")
root = ET.fromstring(document_xml)
namespace = {"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"}
texts = [node.text for node in root.findall(".//w:t", namespace) if node.text]
return "\n".join(texts)
except (BadZipFile, KeyError, ET.ParseError):
return _read_binary_text_fallback(path)
def _read_binary_text_fallback(path: Path) -> str:
"""
当结构化抽取失败时,退回到“尽可能保留纯文本”的保底方案。
该方案不保证版式,但足以支撑 V1 入库和演示。
"""
data = path.read_bytes()
text = data.decode("utf-8", errors="ignore")
text = re.sub(r"[\x00-\x08\x0b\x0c\x0e-\x1f]+", " ", text)
return text.strip()

View File

@@ -1,13 +0,0 @@
from django.urls import path
from . import views
app_name = "documents"
# 文档模块对外暴露三个基础动作:列表、上传、手动入库。
urlpatterns = [
path("", views.document_list, name="list"),
path("upload/", views.upload, name="upload"),
path("<int:document_id>/index/", views.index, name="index"),
]

View File

@@ -1,43 +0,0 @@
from django.contrib import messages
from django.shortcuts import get_object_or_404, redirect, render
from django.views.decorators.http import require_POST
from apps.scenarios.services import list_scenarios
from .forms import DocumentUploadForm
from .models import UploadedDocument
from .services import create_uploaded_document, index_document
def document_list(request):
# 列表页只负责展示文档元数据和可执行操作,不处理入库细节。
documents = UploadedDocument.objects.all()
return render(request, "documents/document_list.html", {"documents": documents})
def upload(request):
# 上传成功后仅保存文件和元数据,是否入库由用户显式触发。
if request.method == "POST":
form = DocumentUploadForm(request.POST, request.FILES)
if form.is_valid():
create_uploaded_document(form.cleaned_data["scenario_id"], form.cleaned_data["file"])
messages.success(request, "文件已上传,可继续执行入库。")
return redirect("documents:list")
else:
form = DocumentUploadForm()
return render(
request,
"documents/upload.html",
{"form": form, "scenarios": list_scenarios()},
)
@require_POST
def index(request, document_id: int):
document = get_object_or_404(UploadedDocument, pk=document_id)
document = index_document(document)
if document.status == UploadedDocument.STATUS_INDEXED:
messages.success(request, "文档入库成功,当前文档已可参与检索。")
else:
messages.error(request, "文档入库失败,请检查错误原因后重试。")
return redirect("documents:list")

View File

@@ -1 +0,0 @@

View File

@@ -1,7 +0,0 @@
from django.apps import AppConfig
class ScenariosConfig(AppConfig):
"""Scenarios 模块应用配置。"""
default_auto_field = "django.db.models.BigAutoField"
name = "apps.scenarios"

View File

@@ -1,110 +0,0 @@
from pathlib import Path
import yaml
from django.conf import settings
REQUIRED_FIELDS = [
("id",),
("name",),
("description",),
("agent", "role"),
("agent", "goal"),
("agent", "instructions"),
("rag", "enabled"),
("tools",),
("output", "type"),
("audit", "enabled"),
]
class ScenarioNotFound(KeyError):
pass
class ScenarioValidationError(ValueError):
pass
def _get_nested(config: dict, path: tuple[str, ...]):
value = config
for key in path:
if not isinstance(value, dict) or key not in value:
raise ScenarioValidationError("缺失必填字段: " + ".".join(path))
value = value[key]
return value
def validate_scenario(config: dict) -> dict:
# 仅校验真正影响运行闭环的必填字段;
# 页面展示字段允许缺失,并在归一化阶段补默认值。
for field_path in REQUIRED_FIELDS:
_get_nested(config, field_path)
return normalize_scenario(config)
def normalize_scenario(config: dict) -> dict:
"""补齐页面和其它模块常用的派生字段,避免模板层重复判断。"""
normalized = dict(config)
normalized["applicable_questions"] = list(config.get("applicable_questions") or [])
normalized["rag"] = dict(config.get("rag", {}))
normalized["rag"]["enabled"] = bool(normalized["rag"].get("enabled"))
normalized["tools"] = list(config.get("tools") or [])
normalized["tool_count"] = len(normalized["tools"])
normalized["is_enabled"] = True
return normalized
def _scenario_files() -> list[Path]:
config_dir = Path(settings.SCENARIO_CONFIG_DIR)
if not config_dir.exists():
return []
return sorted([*config_dir.glob("*.yaml"), *config_dir.glob("*.yml")])
def _read_yaml_file(path: Path) -> dict:
with path.open("r", encoding="utf-8") as file:
return yaml.safe_load(file) or {}
def _collect_scenario_load_result() -> tuple[list[dict], list[dict]]:
"""
统一读取配置目录中的所有场景文件。
返回值:
- scenarios: 校验通过的场景列表
- issues: 非法 YAML / 缺字段等错误摘要,供首页展示
"""
scenarios = []
issues = []
for path in _scenario_files():
try:
config = _read_yaml_file(path)
scenarios.append(validate_scenario(config))
except (yaml.YAMLError, ScenarioValidationError) as exc:
issues.append(
{
"file_name": path.name,
"message": str(exc),
}
)
return scenarios, issues
def list_scenarios() -> list[dict]:
# 首页每次读取最新 YAML便于复试现场快速改题。
scenarios, _issues = _collect_scenario_load_result()
return scenarios
def list_scenario_issues() -> list[dict]:
"""返回配置异常摘要,便于页面明确提示而不是直接 500。"""
_scenarios, issues = _collect_scenario_load_result()
return issues
def get_scenario(scenario_id: str) -> dict:
for scenario in list_scenarios():
if scenario["id"] == scenario_id:
return scenario
raise ScenarioNotFound(f"场景不存在: {scenario_id}")

View File

@@ -1,10 +0,0 @@
from django.urls import path
from . import views
app_name = "scenarios"
urlpatterns = [
path("", views.index, name="index"),
]

View File

@@ -1,16 +0,0 @@
from django.shortcuts import render
from .services import list_scenario_issues, list_scenarios
def index(request):
# 首页只消费服务层给出的场景摘要和错误摘要,
# 不自行读取 YAML更不在 View 里做字段拼装。
return render(
request,
"scenarios/index.html",
{
"scenarios": list_scenarios(),
"scenario_issues": list_scenario_issues(),
},
)

View File

@@ -1,8 +1,8 @@
"""ASGI config for the project."""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
application = get_asgi_application()

View File

@@ -1,46 +1,33 @@
import os
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
def load_dotenv(dotenv_path: Path) -> None:
"""
读取根目录 `.env` 并注入进程环境。
def load_env_file(file_path: Path) -> None:
"""Loads a simple KEY=VALUE .env file into process env without extra deps."""
这里使用极简解析逻辑,目的是减少额外依赖,
同时让本地 `runserver`、`pytest` 与 Docker Compose 共用一套配置文件。
"""
if not dotenv_path.exists():
if not file_path.exists():
return
for raw_line in dotenv_path.read_text(encoding="utf-8").splitlines():
for raw_line in file_path.read_text(encoding="utf-8").splitlines():
line = raw_line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
key = key.strip()
value = value.strip().strip('"').strip("'")
os.environ.setdefault(key, value)
os.environ.setdefault(key.strip(), value.strip())
load_dotenv(BASE_DIR / ".env")
load_env_file(BASE_DIR / ".env")
def env_bool(name: str, default: bool = False) -> bool:
"""将常见的字符串布尔值转换为 Python bool。"""
value = os.environ.get(name)
if value is None:
return default
return value.lower() in {"1", "true", "yes", "on"}
# Django 核心运行参数。
SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY", "dev-secret-key")
DEBUG = env_bool("DJANGO_DEBUG", True)
SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY", "django-insecure-v2-local-development-key")
DEBUG = os.environ.get("DJANGO_DEBUG", "true").lower() == "true"
ALLOWED_HOSTS = [
host.strip()
for host in os.environ.get("DJANGO_ALLOWED_HOSTS", "*").split(",")
for host in os.environ.get(
"DJANGO_ALLOWED_HOSTS",
"127.0.0.1,localhost,testserver",
).split(",")
if host.strip()
]
@@ -51,10 +38,7 @@ INSTALLED_APPS = [
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"apps.scenarios",
"apps.documents",
"apps.chat",
"apps.audit",
"review_agent",
]
MIDDLEWARE = [
@@ -76,6 +60,7 @@ TEMPLATES = [
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
@@ -86,14 +71,20 @@ TEMPLATES = [
WSGI_APPLICATION = "config.wsgi.application"
# V1 默认使用 SQLite确保本地演示零外部依赖。
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "data" / "db.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
}
}
AUTH_PASSWORD_VALIDATORS = [
{"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
]
LANGUAGE_CODE = "zh-hans"
TIME_ZONE = "Asia/Shanghai"
USE_I18N = True
@@ -101,21 +92,88 @@ USE_TZ = True
STATIC_URL = "static/"
STATICFILES_DIRS = [BASE_DIR / "static"]
MEDIA_ROOT = BASE_DIR / "media"
MEDIA_URL = "media/"
# 上传根目录可通过环境变量覆盖,便于 Docker 挂载到持久化目录。
MEDIA_ROOT = Path(os.environ.get("UPLOAD_ROOT", BASE_DIR / "data" / "uploads"))
# 配置目录和 Chroma 数据目录都允许外部覆盖,方便复试现场快速切换。
SCENARIO_CONFIG_DIR = Path(os.environ.get("SCENARIO_CONFIG_DIR", BASE_DIR / "configs"))
CHROMA_PATH = Path(os.environ.get("CHROMA_PATH", BASE_DIR / "data" / "chroma"))
# LLM 与 Embedding 默认遵循“尽量少配置也能跑”的策略:
# Embedding 未单独配置时自动复用 LLM 的 Key 和 Base URL。
LLM_API_KEY = os.environ.get("LLM_API_KEY", "")
LLM_BASE_URL = os.environ.get("LLM_BASE_URL", "https://api.openai.com/v1")
LLM_MODEL = os.environ.get("LLM_MODEL", "gpt-4.1-mini")
EMBEDDING_API_KEY = os.environ.get("EMBEDDING_API_KEY", LLM_API_KEY)
EMBEDDING_BASE_URL = os.environ.get("EMBEDDING_BASE_URL", LLM_BASE_URL)
EMBEDDING_MODEL = os.environ.get("EMBEDDING_MODEL", "text-embedding-3-small")
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
LOGIN_URL = "login"
LOGIN_REDIRECT_URL = "home"
LOGOUT_REDIRECT_URL = "login"
LLM_API_KEY = os.environ.get("LLM_API_KEY", "")
LLM_BASE_URL = os.environ.get("LLM_BASE_URL", "https://api.siliconflow.cn/v1")
LLM_MODEL = os.environ.get("LLM_MODEL", "")
REGULATORY_RAG_PROVIDER = os.environ.get("REGULATORY_RAG_PROVIDER", "siliconflow")
REGULATORY_RAG_CHROMA_PATH = os.environ.get(
"REGULATORY_RAG_CHROMA_PATH",
str(MEDIA_ROOT / "regulatory_review" / "rag" / "chroma"),
)
REGULATORY_RAG_COLLECTION = os.environ.get(
"REGULATORY_RAG_COLLECTION",
"nmpa_ivd_registration_v1",
)
REGULATORY_REVIEW_ASYNC = os.environ.get("REGULATORY_REVIEW_ASYNC", "true").lower() == "true"
REGULATORY_LLM_REVIEW_MAX_ATTEMPTS = int(os.environ.get("REGULATORY_LLM_REVIEW_MAX_ATTEMPTS", "3"))
REGULATORY_LLM_REVIEW_RETRY_DELAY_SECONDS = float(os.environ.get("REGULATORY_LLM_REVIEW_RETRY_DELAY_SECONDS", "0.5"))
REGULATORY_LLM_REVIEW_TIMEOUT_SECONDS = float(os.environ.get("REGULATORY_LLM_REVIEW_TIMEOUT_SECONDS", "15"))
SILICONFLOW_BASE_URL = os.environ.get("SILICONFLOW_BASE_URL", "https://api.siliconflow.cn/v1")
SILICONFLOW_API_KEY = os.environ.get("SILICONFLOW_API_KEY", LLM_API_KEY)
SILICONFLOW_EMBEDDING_MODEL = os.environ.get(
"SILICONFLOW_EMBEDDING_MODEL",
"Qwen/Qwen3-Embedding-4B",
)
SILICONFLOW_EMBEDDING_DIMENSIONS = int(os.environ.get("SILICONFLOW_EMBEDDING_DIMENSIONS", "1024"))
FEISHU_NOTIFY_ENABLED = os.environ.get("FEISHU_NOTIFY_ENABLED", "false").lower() == "true"
FEISHU_NOTIFY_CHANNEL = os.environ.get("FEISHU_NOTIFY_CHANNEL", "feishu_api")
FEISHU_APP_ID = os.environ.get("FEISHU_APP_ID", "")
FEISHU_APP_SECRET = os.environ.get("FEISHU_APP_SECRET", "")
FEISHU_DEFAULT_USER_OPEN_ID = os.environ.get("FEISHU_DEFAULT_USER_OPEN_ID", "")
FEISHU_DEFAULT_USER_ID = os.environ.get("FEISHU_DEFAULT_USER_ID", "")
FEISHU_DEFAULT_TARGET_NAME = os.environ.get("FEISHU_DEFAULT_TARGET_NAME", "默认飞书接收人")
FEISHU_TENANT_TOKEN_CACHE_SECONDS = int(os.environ.get("FEISHU_TENANT_TOKEN_CACHE_SECONDS", "6600"))
FEISHU_TOKEN_API_URL = os.environ.get(
"FEISHU_TOKEN_API_URL",
"https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal",
)
FEISHU_MESSAGE_API_URL = os.environ.get(
"FEISHU_MESSAGE_API_URL",
"https://open.feishu.cn/open-apis/im/v1/messages",
)
PUBLIC_BASE_URL = os.environ.get("PUBLIC_BASE_URL", "http://127.0.0.1:8000")
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"filters": {
"suppress_workflow_status_poll": {
"()": "review_agent.logging_filters.SuppressWorkflowStatusPollFilter",
},
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "verbose",
"filters": ["suppress_workflow_status_poll"],
},
},
"formatters": {
"verbose": {
"format": "%(asctime)s %(levelname)s %(name)s %(message)s",
},
},
"loggers": {
"review_agent": {
"handlers": ["console"],
"level": os.environ.get("REVIEW_AGENT_LOG_LEVEL", "INFO"),
"propagate": True,
},
"django.server": {
"handlers": ["console"],
"level": "INFO",
"propagate": False,
},
},
}

View File

@@ -1,18 +1,32 @@
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.contrib.auth.views import LoginView, LogoutView, PasswordChangeView
from django.urls import include, path
from review_agent.views import attachment_manager, home_dashboard, knowledge_base_manager, stream_chat, workspace
# 总路由只承担模块装配职责,不在这里写业务逻辑。
urlpatterns = [
path("", home_dashboard, name="home"),
path("chat/", workspace, name="chat"),
path("knowledge-base/", knowledge_base_manager, name="knowledge_base_manager"),
path("attachments/", attachment_manager, name="attachment_manager"),
path("", include("review_agent.urls")),
path("chat/stream/", stream_chat, name="chat_stream"),
path(
"login/",
LoginView.as_view(
template_name="registration/login.html",
redirect_authenticated_user=True,
),
name="login",
),
path("logout/", LogoutView.as_view(), name="logout"),
path(
"password/change/",
PasswordChangeView.as_view(
template_name="registration/password_change.html",
success_url="/",
),
name="password_change",
),
path("admin/", admin.site.urls),
path("", include("apps.scenarios.urls")),
path("chat/", include("apps.chat.urls")),
path("documents/", include("apps.documents.urls")),
path("audit/", include("apps.audit.urls")),
]
if settings.DEBUG:
# 开发环境下直接通过 Django 提供上传文件访问能力,便于本地演示。
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View File

@@ -1,8 +1,8 @@
"""WSGI config for the project."""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
application = get_wsgi_application()

View File

@@ -1,22 +0,0 @@
id: document_review
name: 文档审核助手
description: 检查合同、制度或 SOP 中的风险点和缺失项
applicable_questions:
- 合同审核
- 制度审核
agent:
role: 文档审核专家
goal: 根据审核规则和知识库内容输出结构化审核意见
instructions:
- 不确定的问题必须标记为需人工复核
- 输出必须包含风险等级和修改建议
rag:
enabled: true
collection: document_review
top_k: 5
tools:
- check_required_fields
output:
type: document_review_report
audit:
enabled: true

View File

@@ -1,22 +0,0 @@
id: knowledge_qa
name: 知识库问答助手
description: 用于 SOP、制度、客服知识库和内部文档问答
applicable_questions:
- SOP 问答
- 制度问答
agent:
role: 知识库问答专家
goal: 基于知识库内容回答用户问题
instructions:
- 回答必须优先基于检索内容
- 不确定时说明缺失信息
rag:
enabled: true
collection: knowledge_qa
top_k: 5
tools:
- generate_action_items
output:
type: general_answer
audit:
enabled: true

View File

@@ -1,23 +0,0 @@
id: quality_analysis
name: 质量异常分析助手
description: 用于分析生产质量异常、检索 SOP、生成处理建议
applicable_questions:
- 质量异常分析
- 缺陷原因定位
agent:
role: 质量管理专家
goal: 根据用户问题、知识库和工具结果,输出可执行的质量分析报告
instructions:
- 回答必须基于知识库或工具结果
- 涉及质量风险时给出风险等级
rag:
enabled: true
collection: quality_analysis
top_k: 5
tools:
- query_demo_records
- calculate_rate
output:
type: quality_report
audit:
enabled: true

View File

@@ -1,24 +0,0 @@
id: risk_audit
name: 风险审核助手
description: 用于财务、采购、报销和合同风险审核
applicable_questions:
- 财务审核
- 采购审核
- 合同风险分析
agent:
role: 风险审核专家
goal: 识别业务材料中的风险点并给出审核建议
instructions:
- 风险点必须说明依据
- 缺失材料要单独列出
rag:
enabled: true
collection: risk_audit
top_k: 5
tools:
- check_required_fields
- calculate_rate
output:
type: risk_audit_report
audit:
enabled: true

View File

@@ -1,23 +0,0 @@
id: ticket_assistant
name: 工单处理助手
description: 用于客服、售后和运维工单的分类与回复建议
applicable_questions:
- 客服工单
- 售后工单
agent:
role: 工单处理专家
goal: 判断工单类别、优先级并生成处理建议
instructions:
- 需要人工处理时明确标记
- 回复建议要简洁可执行
rag:
enabled: true
collection: ticket_assistant
top_k: 5
tools:
- query_demo_records
- generate_action_items
output:
type: ticket_response
audit:
enabled: true

View File

@@ -1,10 +0,0 @@
services:
web:
build: .
env_file:
- .env
ports:
- "8000:8000"
volumes:
- ./data:/app/data
- ./configs:/app/configs

View File

@@ -0,0 +1,62 @@
**试剂盒临床注册文件准备与审核智能体搭建**
**一、背景**
卡尤迪生物研发团队在推进NMPA国家药品监督管理局注册申报时需准备大量合规性文件包括产品技术要求、说明书、检测报告、临床评估资料等。
公司计划组建AI Agent新团队目标为"试剂盒NMPA注册文件准备与审核智能体",实现文件目录自动汇总、法规完整性检查、关键信息自动提取与填写、缺失文件预警、文档一致性核查,提升注册效率并降低合规风险。
**二、任务目标**
请你作为 AI Agent 工程师候选人,设计并实现(或详细描述)一个智能体,能够:
1. 自动汇总注册申报文件夹中的所有文件及页数
2. 对照 NMPA 法规要求核查文件完整性并预警缺失
3. 提取产品关键信息并自动填写至申报文件
4. 核查文档结构与信息一致性
5. 输出合规风险预警与处理建议
**三、具体要求如下**
**1. 自动汇总文件夹文件目录与页数。**
文件目录参考附件。
**2. 按照NMPA现行法规要求核查文件完整性。**
- 对照NMPA法规检查所需文件是否齐全如注册申报资料基本要求、产品技术要求、注册检验报告等
- 自动识别缺失文件并通知责任人
- 参考法规来源网站:
<https://www.cmde.org.cn/xwdt/zxyw/20210930163300622.html>
<https://www.nmpa.gov.cn/>
**3. 从产品文件中提取关键信息并自动填写至目标文件。**
- 自动提取:产品名称、检测靶标、适用范围、储存条件、性能指标等核心信息
- 将提取信息自动填入注册申报表格或对照清单
**4. 核查文档结构、信息一致性与章节规范性。**
- 检测章节是否完整(如分析灵敏度、特异性、重复性等必检项目)
- 不同文档间同一信息是否一致(如产品名称、规格型号等)
- 格式是否符合NMPA要求的规范章节结构
**5. 提供合规风险预警与处理建议。**
例如:"文件X缺少临床评估报告请补充"或"产品Y说明书与检测报告中的适用范围描述不一致请核对"
**附加要求【在复试时陈述,需结合 Demo 演示】**
**1. 架构搭建思路(基于 Demo 版)**
- 展示Demo运行结果文件目录汇总表、法规完整性报告、信息提取对照表、异常预警列表
- 结合你实现的Demo说明智能体的整体工作流文件扫描 → 目录汇总 → 法规匹配 → 信息提取 → 一致性核查 → 风险预警)
- 展示Demo中实际调用的关键工具/库(如 pdfplumber / PyMuPDF、正则表达式、规则引擎、向量检索等并分析选用理由
- 简述Demo中如何体现文件完整性检测、信息一致性核查、法规条款匹配等难点规则的处理
**2. 基于 Demo 版的迭代规划**
- 说明当前Demo实现了哪些核心功能哪些是模拟数据/简化逻辑
- 下一版本最想增加的一个功能以及需要投入的技术资源(如 NMPA 官网 API 对接、文件版本管理、多语言支持等),并说明为什么优先做它

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,328 @@
# 自动汇总文件夹文件目录与页数流程需求分析
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 原始材料 | docs/原始材料/【模拟题二】试剂盒临床注册文件准备与审核Agent.docx |
| 功能主题 | 自动汇总注册申报资料文件目录与页数 |
| 分析日期 | 2026-06-05 |
| 分析版本 | V1.0 |
---
## 一、需求背景
试剂盒 NMPA 注册申报过程中,研发或注册人员需要准备大量文件,包括产品技术要求、说明书、检测报告、临床评估资料等。原始材料中明确提出智能体需要具备“自动汇总注册申报文件夹中的所有文件及页数”的能力,作为后续法规完整性检查、缺失文件预警、信息提取和一致性核查的基础数据来源。
本功能目标是:用户在 AI 对话框上传注册申报资料压缩包、文件夹或多个散装文件后,系统自动扫描资料结构,统计各文件的目录层级、文件类型、页数、路径和处理状态,生成 Markdown 汇总报告与 Excel 文件,并在 AI 对话框中展示简表和下载链接,同时将汇总结果存入数据库。
---
## 二、需求范围
### 2.1 本期范围
| 序号 | 范围项 | 说明 |
| --- | --- | --- |
| 1 | 文件上传入口 | 用户通过现有 AI 对话框上传压缩包、文件夹或多个散装文件 |
| 2 | 文件解析 | 系统识别上传内容并展开为待扫描文件清单 |
| 3 | 目录汇总 | 保留原始目录层级,散装文件归入默认上传批次目录 |
| 4 | 页数统计 | 默认支持 pdf、doc、docx、xls、xlsx、ppt、pptx |
| 5 | 结果展示 | AI 对话框中展示 Markdown 简表 |
| 6 | 文件导出 | 生成 Markdown 报告与 Excel 汇总表,并在对话框提供下载链接 |
| 7 | 数据存档 | 上传批次、文件明细、页数、异常状态、导出文件信息写入数据库 |
### 2.2 非本期范围
| 序号 | 范围项 | 说明 |
| --- | --- | --- |
| 1 | NMPA 法规完整性核查 | 依赖目录汇总结果,属于后续流程 |
| 2 | 产品关键信息抽取 | 依赖具体文档内容解析,属于后续流程 |
| 3 | 文档一致性核查 | 依赖信息抽取结果,属于后续流程 |
| 4 | 合规风险预警 | 本功能仅输出文件扫描异常,不输出法规合规风险 |
---
## 三、用户角色与使用场景
| 角色 | 诉求 | 典型场景 |
| --- | --- | --- |
| 注册人员 | 快速了解申报资料是否完整、页数是否可统计 | 上传供应商或研发团队整理好的注册资料压缩包 |
| 审核人员 | 获得可复核的文件目录和页数清单 | 在审核前获取 Markdown 简表和 Excel 明细 |
| 系统管理员 | 追踪上传批次和处理异常 | 查看数据库存档,定位文件解析失败原因 |
---
## 四、业务流程分析
### 4.1 主流程
```text
用户进入 AI 对话框
-> 上传压缩包、文件夹或多个散装文件
-> 用户发送“自动汇总文件目录与页数”类指令
-> 系统创建上传批次
-> 系统保存原始上传文件
-> 系统识别上传类型
-> 如为压缩包则解压,如为文件夹或散装文件则直接扫描
-> 系统遍历文件并识别目录层级
-> 系统过滤支持的文件类型
-> 系统计数每个支持文件的页数
-> 系统记录不支持或统计失败的文件异常
-> 系统生成 Markdown 报告与 Excel 文件
-> 系统在 AI 对话框展示 Markdown 简表和下载链接
-> 系统将批次和明细数据写入数据库
-> 结束
```
### 4.2 分支流程
| 分支场景 | 流程说明 |
| --- | --- |
| 上传压缩包 | 系统校验压缩包格式,解压至临时工作目录,按解压后的目录结构统计 |
| 上传文件夹 | 系统保留文件夹层级,扫描所有子目录文件 |
| 上传多个散装文件 | 系统生成默认批次目录,将所有文件视为同一层级 |
| 存在不支持类型 | 文件进入明细表,页数为空,统计状态为“不支持”,异常说明记录文件类型 |
| 单个文件页数统计失败 | 不中断整个批次,统计状态为“失败”,异常说明记录失败原因 |
| 重名文件 | 以完整相对路径区分;散装文件重名时使用系统保存路径或自动重命名策略避免覆盖 |
### 4.3 异常流程
| 异常场景 | 处理方式 |
| --- | --- |
| 上传文件为空 | 对话框提示“未检测到可处理文件”,不生成报告 |
| 压缩包损坏或无法解压 | 批次状态标记为失败,对话框展示错误原因 |
| 文件被加密或受保护 | 明细记录该文件,页数统计失败,异常说明标记“文件加密或无法读取” |
| 文件过大或处理超时 | 明细记录超时状态,批次可继续处理其他文件 |
| Excel/Markdown 导出失败 | 对话框提示导出失败,数据库保留扫描明细与失败原因 |
---
## 五、功能模块梳理
### 5.1 核心功能
| 序号 | 功能名称 | 功能描述 | 优先级 |
| --- | --- | --- | --- |
| 1 | 上传资料接收 | 支持用户在 AI 对话框上传压缩包、文件夹或多个散装文件 | P0 |
| 2 | 上传批次管理 | 为每次汇总创建批次编号,记录用户、时间、来源会话和处理状态 | P0 |
| 3 | 压缩包解压 | 支持压缩包上传后的安全解压和目录结构保留 | P0 |
| 4 | 文件遍历扫描 | 递归扫描上传范围内所有文件,生成相对路径和目录层级 | P0 |
| 5 | 文件类型识别 | 根据扩展名和必要的文件头信息识别 pdf、doc、docx、xls、xlsx、ppt、pptx | P0 |
| 6 | 页数统计 | 对支持文件统计页数或页签/幻灯片数量 | P0 |
| 7 | Markdown 简表展示 | 在 AI 对话框展示可解析的 Markdown 表格摘要 | P0 |
| 8 | Markdown 报告导出 | 生成完整 Markdown 汇总报告 | P0 |
| 9 | Excel 明细导出 | 生成 Excel 文件,包含所有文件明细和统计状态 | P0 |
| 10 | 数据库存档 | 保存批次、文件明细、统计结果、异常说明和导出文件记录 | P0 |
### 5.2 辅助功能
| 序号 | 功能名称 | 功能描述 | 优先级 |
| --- | --- | --- | --- |
| 1 | 下载链接生成 | 在 AI 回复中提供 Markdown 报告和 Excel 文件下载链接 | P0 |
| 2 | 处理进度提示 | 对较大批次展示“上传中、解析中、统计中、导出中、已完成”等状态 | P1 |
| 3 | 文件过滤规则 | 支持过滤系统隐藏文件、临时文件和空文件 | P1 |
| 4 | 统计摘要 | 输出文件总数、支持文件数、统计成功数、失败数、总页数 | P1 |
| 5 | 历史记录查询 | 可通过对话记录或批次记录回看历史汇总结果 | P1 |
### 5.3 隐含功能
| 序号 | 功能名称 | 功能描述 | 来源说明 |
| --- | --- | --- | --- |
| 1 | AI 对话框附件上传能力 | 现有对话框需要支持上传附件并关联会话消息 | 用户要求“在 AI 对话框中提供下载连接” |
| 2 | Markdown 渲染能力 | 现有对话框需要能解析表格、链接等 Markdown 内容 | 用户要求“对话框能正常解析 md 格式” |
| 3 | 文件访问权限控制 | 下载链接只能允许当前用户或授权用户访问 | 涉及注册资料敏感文件 |
| 4 | 临时文件清理 | 解压目录和处理中间文件需要定期清理 | 压缩包和大文件处理会产生临时文件 |
---
## 六、数据实体分析
### 6.1 实体列表
| 序号 | 实体名称 | 字段说明 | 关联实体 |
| --- | --- | --- | --- |
| 1 | 汇总批次 | 批次编号、用户、会话、上传类型、文件数量、总页数、状态、创建时间、完成时间 | 会话、文件明细、导出文件 |
| 2 | 文件明细 | 文件序号、目录层级、文件名、文件类型、页数、路径、统计状态、异常说明 | 汇总批次 |
| 3 | 导出文件 | 文件类型、文件名、保存路径、下载地址、生成状态、生成时间 | 汇总批次 |
| 4 | 上传原始文件 | 原始文件名、保存路径、大小、文件类型、上传时间 | 汇总批次 |
### 6.2 文件明细字段
| 字段 | 类型建议 | 必填 | 说明 |
| --- | --- | --- | --- |
| file_index | Integer | 是 | 文件序号,按目录遍历顺序生成 |
| directory_level | String | 是 | 目录层级如“1/2/3”或“注册资料/产品技术要求” |
| file_name | String | 是 | 文件名,不含目录 |
| file_type | String | 是 | 文件扩展名或识别后的类型 |
| page_count | Integer | 否 | 页数,统计失败或不支持时为空 |
| relative_path | String | 是 | 相对上传根目录的路径 |
| statistics_status | String | 是 | 成功、失败、不支持、跳过 |
| error_message | Text | 否 | 异常说明 |
### 6.3 实体关系
```text
会话 1:N 汇总批次
汇总批次 1:N 上传原始文件
汇总批次 1:N 文件明细
汇总批次 1:N 导出文件
```
---
## 七、输出要求
### 7.1 AI 对话框简表
AI 回复内容必须使用前端可解析的 Markdown 格式,至少包含统计摘要、文件简表和下载链接。
示例:
```markdown
已完成文件目录与页数汇总。
| 指标 | 数量 |
| --- | --- |
| 文件总数 | 24 |
| 统计成功 | 21 |
| 统计失败 | 2 |
| 不支持 | 1 |
| 总页数 | 386 |
| 序号 | 目录层级 | 文件名 | 类型 | 页数 | 状态 | 异常说明 |
| --- | --- | --- | --- | --- | --- | --- |
| 1 | 注册资料/说明书 | 说明书.docx | docx | 12 | 成功 | |
| 2 | 注册资料/检测报告 | 检测报告.pdf | pdf | 38 | 成功 | |
[下载 Markdown 报告](download-url)
[下载 Excel 明细](download-url)
```
### 7.2 Markdown 报告
Markdown 报告应包含:
| 模块 | 内容 |
| --- | --- |
| 汇总信息 | 批次编号、上传用户、上传时间、处理完成时间、上传类型 |
| 统计摘要 | 文件总数、支持文件数、成功数、失败数、不支持数、总页数 |
| 文件明细 | 文件序号、目录层级、文件名、文件类型、页数、路径、统计状态、异常说明 |
| 异常清单 | 统计失败、不支持、解压失败、加密文件、超时文件 |
| 处理说明 | 支持范围、页数统计口径、待确认事项 |
### 7.3 Excel 导出
Excel 至少包含两个工作表:
| 工作表 | 内容 |
| --- | --- |
| 汇总信息 | 批次信息、统计摘要 |
| 文件明细 | 文件序号、目录层级、文件名、文件类型、页数、路径、统计状态、异常说明 |
---
## 八、页数统计口径
| 文件类型 | 统计口径 | 备注 |
| --- | --- | --- |
| pdf | PDF 页面数量 | 可使用 pdfplumber 或 PyMuPDF |
| doc | Word 文档页数 | 可通过 LibreOffice 转 PDF 后统计,或读取文档属性;实现方案待技术验证 |
| docx | Word 文档页数 | 可通过 LibreOffice 转 PDF 后统计,或读取文档属性;实现方案待技术验证 |
| xls | Excel 工作表打印页数或页签数量 | 具体口径待确认 |
| xlsx | Excel 工作表打印页数或页签数量 | 具体口径待确认 |
| ppt | 幻灯片数量 | 可转换或读取演示文稿结构 |
| pptx | 幻灯片数量 | 可读取演示文稿结构 |
> 待确认Excel 的“页数”是否按打印分页统计,还是按工作表数量统计。建议 Demo 阶段先按工作表数量统计,并在报告中明确口径。
---
## 九、非功能性需求
### 9.1 性能要求
| 项目 | 要求 |
| --- | --- |
| 小批次文件 | 50 个文件以内,应在 30 秒内完成汇总 |
| 中等批次文件 | 200 个文件以内,应支持异步处理或进度提示 |
| 大文件处理 | 单文件超时不影响其他文件统计 |
### 9.2 安全要求
| 项目 | 要求 |
| --- | --- |
| 上传安全 | 限制可处理类型,避免执行上传文件中的脚本或宏 |
| 解压安全 | 防止路径穿越,限制解压目录在系统工作目录内 |
| 下载权限 | 下载链接需要校验登录用户和会话权限 |
| 数据隔离 | 不同用户上传文件和统计结果不可互相访问 |
| 敏感资料保护 | 原始上传文件、报告和 Excel 文件应存储在受控目录 |
### 9.3 可用性要求
| 项目 | 要求 |
| --- | --- |
| 对话展示 | Markdown 表格、链接在现有 AI 对话框中可正常渲染 |
| 失败可见 | 失败文件需要展示具体原因,便于用户补传或修复 |
| 可追溯 | 每份导出报告能关联到具体上传批次和会话 |
---
## 十、疑问点与待确认事项
### 10.1 功能疑问
| 序号 | 疑问内容 | 建议 | 状态 |
| --- | --- | --- | --- |
| 1 | “文件目录参考附件”尚未上传,无法确认标准目录样例 | 附件上传后补充目录样例和字段映射 | 待确认 |
| 2 | 文件夹上传在浏览器端是否采用目录上传能力 | 前端可使用 webkitdirectory 或改为要求用户上传压缩包 | 待确认 |
| 3 | 散装文件是否需要用户手动指定归属目录 | Demo 阶段统一归入“散装上传”目录 | 待确认 |
### 10.2 业务规则疑问
| 序号 | 疑问内容 | 建议 | 状态 |
| --- | --- | --- | --- |
| 1 | Excel 页数统计口径不明确 | Demo 阶段按工作表数量,正式版可按打印分页 | 待确认 |
| 2 | doc/docx 页数是否必须与 Word 打开后的实际页数完全一致 | 建议通过转换 PDF 后统计,提高一致性 | 待确认 |
| 3 | 是否需要按 NMPA 申报资料目录自动排序 | 本功能先按上传目录顺序,后续法规完整性检查再做标准目录匹配 | 待确认 |
### 10.3 技术方案疑问
| 序号 | 疑问内容 | 可选方案 | 状态 |
| --- | --- | --- | --- |
| 1 | Office 文档页数统计依赖 | LibreOffice 转 PDF、文档属性读取、商业 Office 自动化 | 待确认 |
| 2 | 大文件处理模式 | 同步处理、后台任务、队列任务 | 待确认 |
| 3 | Markdown 渲染方案 | 前端引入 Markdown 渲染库,或后端转换为安全 HTML | 待确认 |
### 10.4 数据定义疑问
| 序号 | 疑问内容 | 建议 | 状态 |
| --- | --- | --- | --- |
| 1 | 导出文件保存周期未明确 | Demo 阶段永久保存,正式版增加过期清理策略 | 待确认 |
| 2 | 原始上传文件是否长期保留 | 建议至少保留到关联审核流程结束 | 待确认 |
| 3 | 下载链接是否需要一次性或有效期控制 | 注册资料敏感,建议正式版增加有效期和权限校验 | 待确认 |
---
## 十一、验收标准
| 序号 | 验收项 | 验收标准 |
| --- | --- | --- |
| 1 | 压缩包上传汇总 | 上传包含多层目录的压缩包后,系统能保留层级并统计支持文件页数 |
| 2 | 散装文件上传汇总 | 上传多个散装文件后,系统能生成同一批次汇总结果 |
| 3 | Markdown 展示 | AI 对话框能展示摘要表和文件简表,表格格式正常 |
| 4 | 下载链接 | AI 回复中提供 Markdown 报告和 Excel 明细下载链接,点击可下载 |
| 5 | 数据存档 | 数据库中能查询到批次记录、文件明细和导出文件记录 |
| 6 | 异常不中断 | 单个文件失败时不影响其他文件统计,失败原因可见 |
| 7 | 支持类型覆盖 | pdf、doc、docx、xls、xlsx、ppt、pptx 均能进入处理流程 |
---
## 十二、下一步建议
1. 上传“文件目录参考附件”,补充标准目录样例和演示数据。
2. 明确 Excel 页数统计口径,决定 Demo 版是否按工作表数量统计。
3. 设计数据库表结构,包括汇总批次、文件明细、上传原始文件和导出文件。
4. 改造 AI 对话框增加附件上传、Markdown 渲染和下载链接展示能力。
5. 实现文件扫描与页数统计服务,优先打通 pdf、docx、xlsx、pptx 的 Demo 流程。

View File

@@ -0,0 +1,330 @@
# NMPA 注册资料法规核查与整改闭环工作流需求分析
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 原始材料 | docs/原始材料/【模拟题二】试剂盒临床注册文件准备与审核Agent.docx |
| 法规资料来源 | docs/原始材料/关于公布体外诊断试剂注册申报资料要求和批准证明文件格式的公告 |
| 参考来源 URL | https://www.cmde.org.cn/xwdt/zxyw/20210930163300622.html |
| 备用法规入口 | https://www.nmpa.gov.cn/ |
| 工作流名称 | NMPA 注册资料法规核查与整改闭环工作流 |
| 分析日期 | 2026-06-05 |
| 分析版本 | V1.0 |
---
## 一、需求背景
试剂盒及医疗器械 NMPA 注册申报资料准备过程中,注册人员不仅需要确认申报资料文件是否齐全,还需要进一步检查关键文件章节结构是否符合要求、不同文件中的核心信息是否一致,并对发现的问题形成风险预警和整改闭环。
原始任务中的第 2、4、5 条能力本质上属于同一条法规核查工作流:
| 原任务编号 | 能力要求 | 在本工作流中的定位 |
| --- | --- | --- |
| 2 | 按照 NMPA 现行法规要求核查文件完整性 | 法规资料完整性核查 |
| 4 | 核查文档结构、信息一致性与章节规范性 | 章节结构核查与跨文件一致性核查 |
| 5 | 提供合规风险预警与处理建议 | 风险分级、整改建议、通知与复核闭环 |
本工作流目标是:基于上一阶段生成的文件目录与页数汇总结果,结合本地法规资料包中的 CMDE 公告要求,对上传申报资料执行法规适用条件确认、资料完整性核查、章节结构核查、跨文件一致性核查,输出风险清单、法规核查矩阵、整改建议,并通过系统内提示和飞书通知责任人,支持补充资料后的复核与关闭。
---
## 二、需求范围
### 2.1 本期范围
| 序号 | 范围项 | 说明 |
| --- | --- | --- |
| 1 | 法规资料来源 | Demo 阶段暂按本地 CMDE 公告资料包执行,不做实时网页检索 |
| 2 | 适用品类 | 建立医疗器械/体外诊断试剂通用核查框架,优先覆盖体外诊断试剂注册申报资料 |
| 3 | 产品信息提取 | 从上传资料中提取产品类别、注册类型、临床评价路径、产品名称、预期用途、检测靶标、样本类型、储存条件等信息 |
| 4 | 适用条件确认 | 系统先自动提取适用条件,再由用户确认后执行完整核查 |
| 5 | 法规资料完整性核查 | 对照法规清单检查必需文件、条件性文件、建议文件是否存在 |
| 6 | 文件项级完整性核查 | 不仅判断文件类别是否存在,还检查关键子项、章节或字段是否齐全 |
| 7 | 章节结构核查 | 检查产品技术要求、说明书、检验报告等关键文件是否包含法规要求章节 |
| 8 | 跨文件一致性核查 | 检查产品名称、型号规格、适用范围、样本类型等核心信息在不同文件中是否一致 |
| 9 | 风险分级 | 按阻断项、高风险、中风险、低风险、提示项五级统一排序 |
| 10 | 整改建议 | 先按规则模板生成标准建议,再由 AI 整理为易读表达 |
| 11 | 责任人通知 | 默认按上传人识别责任人;上传人是当前用户时飞书通知 @ 当前用户 |
| 12 | 整改复核 | 支持重新上传整包复核,也支持仅上传缺失文件合并到原批次后复核 |
| 13 | 输出留痕 | 输出对话框摘要、Markdown 报告、Excel 缺失清单、飞书通知记录 |
### 2.2 非本期范围
| 序号 | 范围项 | 说明 |
| --- | --- | --- |
| 1 | 官网法规实时更新 | 本期不实时抓取 NMPA/CMDE 官网,后续可扩展为内置清单加定期更新 |
| 2 | 全品类法规覆盖 | 本期建立通用框架,但规则内容优先围绕体外诊断试剂注册资料 |
| 3 | 自动关闭所有风险 | 待确认项和复核通过关闭前需要人工确认 |
| 4 | 企业级责任矩阵 | 本期默认按上传人通知,后续再支持按项目、部门、文件类别分配责任人 |
---
## 三、用户角色与使用场景
| 角色 | 诉求 | 典型场景 |
| --- | --- | --- |
| 注册人员 | 快速发现缺失资料、结构不规范和核心信息冲突 | 上传注册申报资料包后发起法规核查工作流 |
| 审核人员 | 获得可追溯的法规核查矩阵和风险清单 | 在正式申报前复核资料是否达到提交条件 |
| 资料上传人 | 接收缺失项和风险项通知,并完成补充整改 | 系统发现阻断项、高风险或中风险后通过飞书 @ 上传人 |
| 系统管理员 | 维护法规规则、通知配置和核查记录 | 查看核查批次、规则版本、飞书发送记录和复核状态 |
---
## 四、工作流主流程
### 4.1 主流程
```text
用户在 AI 对话框上传申报资料
-> 系统执行文件清单汇总与页数统计
-> 用户发起“NMPA 注册资料法规核查与整改闭环”工作流
-> 系统读取本地法规资料包和内置法规清单
-> 系统从上传资料中提取产品类别、注册类型、临床评价路径和产品关键信息
-> 系统在对话框展示适用条件识别结果,请用户确认或修正
-> 用户确认适用条件
-> 系统裁剪本次适用的法规核查清单
-> 系统并行执行法规资料完整性核查、章节结构核查、跨文件一致性核查
-> 系统汇总问题并按风险等级排序
-> 系统生成整改建议、法规依据和证据说明
-> 系统在对话框展示风险摘要
-> 系统生成 Markdown 报告、Excel 缺失清单和飞书通知记录
-> 对阻断项、高风险、中风险发送飞书通知并 @ 上传人
-> 上传人补充资料后发起整包重核或缺失文件补传复核
-> 系统复核整改项
-> 待确认项和复核通过关闭前由用户人工确认
-> 风险项关闭或驳回
```
### 4.2 工作流节点
| 节点编码 | 节点名称 | 输入 | 输出 |
| --- | --- | --- | --- |
| inventory | 文件清单汇总 | 上传资料、目录扫描结果 | 文件清单、页数、解析状态 |
| info_extract | 产品信息提取 | 文件名、目录名、文档首页或前几页内容 | 产品类别、注册类型、临床评价路径、产品关键信息 |
| condition_confirm | 适用条件确认 | 系统提取结果 | 用户确认后的适用条件 |
| rule_scope | 法规清单裁剪 | 法规规则库、适用条件 | 本次适用的核查清单 |
| completeness_check | 法规资料完整性核查 | 文件清单、法规核查清单 | 缺失文件、条件性缺失、待确认项 |
| structure_check | 章节结构核查 | 关键文件文本、章节模板 | 缺失章节、异常章节、格式问题 |
| consistency_check | 跨文件一致性核查 | 关键字段抽取结果 | 信息冲突项、证据文件、冲突字段 |
| risk_assess | 风险分级与建议生成 | 所有核查问题 | 风险等级、法规依据、整改建议 |
| notify | 系统提示与飞书通知 | 风险清单、责任人 | 对话提示、飞书通知记录 |
| rectify_review | 整改复核 | 补充资料、原风险项 | 复核结论、状态变更 |
---
## 五、核心核查规则
### 5.1 法规资料完整性核查
系统对照法规清单检查本次申报应提交的文件是否齐全。Demo 阶段法规依据暂按本地 CMDE 公告资料包维护,后续可扩展为可配置规则库。
| 检查项 | 检查说明 |
| --- | --- |
| 必需文件 | 缺失后直接影响申报资料完整性的文件,如注册申报资料基本要求、产品技术要求、说明书、注册检验报告等 |
| 条件性文件 | 是否必需取决于产品类别、注册类型、临床评价路径等适用条件 |
| 建议文件 | 有助于完善资料但不一定构成阻断的问题 |
| 文件项级子项 | 检查关键文件内部是否存在必需章节、附件、签章页、结论页或关键字段 |
### 5.2 文件匹配规则
系统采用三层匹配识别文件是否满足法规文件项:
| 匹配层级 | 说明 | 示例 |
| --- | --- | --- |
| 文件名匹配 | 根据文件名关键词识别文件类别 | 文件名包含“产品技术要求” |
| 目录名匹配 | 文件名不明确时参考所在目录 | 文件位于“注册检验/报告”目录 |
| 首页内容匹配 | 文件名和目录名不足以判断时读取首页或前几页文本辅助识别 | 首页标题包含“医疗器械注册检验报告” |
匹配结果需要记录命中证据,包括命中的文件路径、关键词、页码或文本片段,便于人工复核。
### 5.3 章节结构核查
系统对关键文件执行章节结构检查,判断是否存在法规或模板要求的章节。
| 文件类别 | 章节核查示例 |
| --- | --- |
| 产品技术要求 | 检查性能指标、检验方法、术语、附录等章节 |
| 说明书 | 检查产品名称、包装规格、预期用途、检验原理、主要组成成分、储存条件、有效期、样本要求、检验方法、阳性判断值或参考区间等章节 |
| 注册检验报告 | 检查封面、样品信息、检验依据、检验项目、检验结论、签章页等内容 |
| 临床评价资料 | 检查临床评价路径、数据来源、评价结论、适用性说明等内容 |
### 5.4 跨文件一致性核查
系统抽取不同文件中的核心字段并进行一致性比对。
| 字段 | 典型来源文件 | 不一致示例 |
| --- | --- | --- |
| 产品名称 | 申请表、说明书、产品技术要求、检验报告 | 说明书和检验报告中的产品名称不一致 |
| 型号规格 | 说明书、产品技术要求、检验报告 | 规格型号缺失或描述不一致 |
| 预期用途 | 说明书、临床评价资料、注册申报表 | 适用人群或检测目的描述冲突 |
| 检测靶标 | 说明书、产品技术要求、性能研究资料 | 靶标名称、缩写或组合不一致 |
| 样本类型 | 说明书、检验报告、性能研究资料 | 样本类型范围不一致 |
| 储存条件 | 说明书、标签、稳定性研究资料 | 温度条件或有效期描述不一致 |
---
## 六、风险等级与通知策略
### 6.1 风险等级
| 风险等级 | 定义 | 示例 |
| --- | --- | --- |
| 阻断项 | 直接影响资料能否进入有效申报或审核的严重问题 | 缺少法规必需文件;关键文件损坏、加密或页数为 0产品名称/型号规格在关键文件中严重冲突 |
| 高风险 | 可能导致注册审评补正或重大整改的问题 | 关键章节缺失;注册检验报告缺少结论页;临床评价路径资料不完整 |
| 中风险 | 需要补充说明或修改,但不一定阻断整体资料审核的问题 | 个别关键字段缺失;章节标题不规范;证据页码不明确 |
| 低风险 | 对申报完整性影响较小,但建议修正的问题 | 文件命名不规范;目录层级不清晰 |
| 提示项 | 系统无法充分判断或建议人工关注的问题 | 条件性文件是否适用待人工确认 |
### 6.2 通知策略
| 风险等级 | 系统内提示 | 飞书通知 |
| --- | --- | --- |
| 阻断项 | 是 | 是,@ 上传人 |
| 高风险 | 是 | 是,@ 上传人 |
| 中风险 | 是 | 是,@ 上传人 |
| 低风险 | 是 | 否 |
| 提示项 | 是 | 否 |
责任人识别规则:本期默认按上传人识别责任人;上传人是当前用户时,系统内提示归属当前用户,飞书通知中 @ 当前用户。后续可扩展为项目责任人表、文件类别责任矩阵或目录归属规则。
---
## 七、整改建议与闭环状态
### 7.1 整改建议生成
整改建议采用“规则模板 + AI 润色”的方式生成:
| 步骤 | 说明 |
| --- | --- |
| 规则模板 | 根据问题类型、法规依据、风险等级生成标准整改动作 |
| AI 整理 | 将标准动作整理为适合对话框和飞书通知阅读的表达 |
| 人工可复核 | 输出中保留法规依据、证据文件、命中规则和建议动作 |
示例:
```text
问题:缺少注册检验报告。
风险等级:阻断项。
整改建议:请补充与本产品一致的注册检验报告,并确保报告包含样品信息、检验依据、检验项目、检验结论和签章页。
责任人:上传人。
通知方式:系统内提示 + 飞书 @ 上传人。
```
### 7.2 问题状态流转
| 状态 | 含义 | 触发方式 |
| --- | --- | --- |
| 待确认 | 系统发现条件性问题或判断依据不足 | 系统自动生成,等待用户确认 |
| 待处理 | 问题已确认,需要补充或修改资料 | 用户确认或系统自动判定 |
| 已补充 | 责任人已上传补充资料或替换文件 | 用户上传资料后自动进入 |
| 复核通过 | 系统复核后判断问题已解决 | 系统自动判断,关闭前需人工确认 |
| 复核不通过 | 系统复核后判断问题仍存在 | 系统自动判断 |
| 已关闭 | 用户确认问题已解决并关闭 | 人工确认 |
### 7.3 复核方式
| 复核方式 | 适用场景 |
| --- | --- |
| 重新上传整包复核 | 资料包整体结构有较大调整,或需要重新生成完整核查报告 |
| 仅上传缺失文件复核 | 只补充少量缺失文件或替换单个问题文件 |
待确认项和复核通过关闭前需要人工确认;其他状态可由系统根据核查结果自动流转。
---
## 八、输出要求
### 8.1 AI 对话框摘要
对话框摘要应优先展示风险分布和需要处理的事项。
```markdown
已完成 NMPA 注册资料法规核查。
| 风险等级 | 数量 |
| --- | --- |
| 阻断项 | 2 |
| 高风险 | 3 |
| 中风险 | 5 |
| 低风险 | 4 |
| 提示项 | 2 |
| 风险等级 | 问题 | 责任人 | 建议 |
| --- | --- | --- | --- |
| 阻断项 | 缺少注册检验报告 | 上传人 | 请补充注册检验报告并重新复核 |
| 高风险 | 说明书缺少储存条件章节 | 上传人 | 请补充储存条件和有效期说明 |
[下载 Markdown 核查报告](download-url)
[下载 Excel 缺失清单](download-url)
```
### 8.2 Markdown 核查报告
Markdown 报告至少包含:
| 模块 | 内容 |
| --- | --- |
| 核查概览 | 批次编号、上传人、法规资料来源、规则版本、核查时间 |
| 适用条件 | 产品类别、注册类型、临床评价路径、产品关键信息及用户确认记录 |
| 风险清单 | 风险等级、问题描述、法规依据、证据、责任人、整改建议、状态 |
| 法规核查矩阵 | 法规文件项、是否适用、应提交资料、匹配文件、缺失情况、结论 |
| 章节结构核查 | 文件类别、章节要求、识别章节、缺失章节、结论 |
| 一致性核查 | 字段名称、来源文件、字段值、冲突说明、结论 |
| 飞书通知记录 | 通知时间、通知对象、通知等级、发送状态 |
| 复核记录 | 补充资料、复核时间、复核结果、关闭确认人 |
### 8.3 Excel 缺失清单
Excel 至少包含以下工作表:
| 工作表 | 内容 |
| --- | --- |
| 风险清单 | 所有风险项、等级、责任人、状态、建议 |
| 法规核查矩阵 | 应有文件、实际匹配文件、缺失情况、法规依据 |
| 章节结构问题 | 缺失章节、异常章节、文件路径、页码或证据 |
| 一致性问题 | 冲突字段、来源文件、字段值、风险等级 |
| 通知记录 | 飞书发送对象、发送时间、发送状态、通知内容摘要 |
---
## 九、非功能性需求
### 9.1 可追溯性
| 要求 | 说明 |
| --- | --- |
| 规则版本留痕 | 每次核查记录使用的法规资料来源和规则版本 |
| 证据留痕 | 每个问题记录命中文件、路径、页码或文本片段 |
| 通知留痕 | 飞书通知需要记录发送对象、发送状态、发送时间和内容摘要 |
| 状态留痕 | 问题状态变化需要记录操作人、操作时间和变化原因 |
### 9.2 安全要求
| 要求 | 说明 |
| --- | --- |
| 文件访问控制 | 核查报告、Excel 清单和原始资料仅授权用户可访问 |
| 敏感信息保护 | 飞书通知只展示必要摘要,不直接暴露完整敏感文件内容 |
| 人工确认 | 条件适用性和风险关闭前需人工确认,避免系统误判 |
### 9.3 性能要求
| 场景 | 要求 |
| --- | --- |
| 小批次资料 | 50 个文件以内应在 1 分钟内完成初步风险摘要 |
| 中等批次资料 | 200 个文件以内支持后台异步处理和进度提示 |
| 单文件解析失败 | 不阻断整个批次,记录异常并继续其他文件核查 |
---
## 十、待后续确认事项
| 序号 | 待确认项 | 当前建议 |
| --- | --- | --- |
| 1 | 飞书集成方式 | Demo 阶段可使用飞书 CLI、Webhook 或类似命令行工具发送通知 |
| 2 | 当前用户与飞书账号映射 | 需要维护用户账号与飞书 open_id、手机号或邮箱的映射关系 |
| 3 | 法规清单结构 | 功能设计阶段需将本地 CMDE 公告拆解为结构化规则表 |
| 4 | 条件性文件适用规则 | 先由系统提取并让用户确认,后续逐步自动化 |
| 5 | 跨文件一致性字段范围 | Demo 阶段优先覆盖产品名称、型号规格、预期用途、样本类型、储存条件 |

View File

@@ -0,0 +1,394 @@
# 产品关键信息提取与申报文件自动填表需求分析
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 原始材料 | docs/原始材料/【模拟题二】试剂盒临床注册文件准备与审核Agent.docx |
| 法规模板来源 | docs/原始材料/关于公布体外诊断试剂注册申报资料要求和批准证明文件格式的公告 |
| 功能主题 | 从产品文件中提取关键信息并自动填写至指定申报模板 |
| 分析日期 | 2026-06-07 |
| 分析版本 | V1.0 |
---
## 一、需求背景
试剂盒及体外诊断试剂注册申报过程中,注册人员需要将同一批产品关键信息重复填写到注册证格式文件、变更注册或备案文件、安全和性能基本原则清单等申报材料中。人工复制粘贴容易出现字段遗漏、表述不一致、来源不可追溯和模板误改等问题。
原始任务中的第 3 条能力要求系统能够“从产品文件中提取关键信息并自动填写至目标文件”。本功能目标是:系统基于用户上传的产品说明书、产品技术要求、检测报告、性能研究资料等文件,自动抽取产品名称、检测靶标、适用范围、储存条件、性能指标等核心信息,复制指定法规模板生成可填写副本,将抽取结果写入模板,并输出 Word 与 PDF 两种下载文件。
本功能是前两批能力的后续增强依赖第一批文件汇总结果定位产品文件复用第二批文本抽取、适用条件确认和一致性核查能力同时新增“模板识别、字段映射、模板填充、冲突高亮、PDF 转换、来源追溯”能力。
---
## 二、需求范围
### 2.1 本期范围
| 序号 | 范围项 | 说明 |
| --- | --- | --- |
| 1 | 目标模板复制 | 从原始法规资料中复制指定模板,不覆盖原始文件 |
| 2 | 注册类型选择 | 首次注册填写注册证格式;变更注册或备案填写变更注册(备案)文件格式 |
| 3 | 安全和性能基本原则清单填写 | 无论首次注册或变更注册,均生成并填写安全和性能基本原则清单 |
| 4 | 产品信息提取 | 从产品说明书、产品技术要求、检测报告、性能研究资料等文件中抽取模板所需字段 |
| 5 | 模板字段识别 | 读取目标模板中的表格、段落、占位栏位和清单条目,建立字段映射 |
| 6 | 自动填入模板 | 将抽取字段写入模板副本,缺失字段保持留空 |
| 7 | 冲突标记 | 同一字段在多个文件中不一致时,按说明书为准填写,并在模板中黄色底色、红色字体标记 |
| 8 | 冲突摘要展示 | AI 对话框展示冲突字段、采用值、冲突来源和待用户下载确认提示 |
| 9 | Word 导出 | 输出填好的 `.docx` 或可编辑 Word 文件 |
| 10 | PDF 导出 | 将填好的 Word 转换为 PDF尽量保持原 Word 模板版式一致,可用于正式提交前预览 |
| 11 | 来源追溯 | 允许额外输出字段来源追溯清单,记录字段来源文件、文本片段、冲突状态和填入目标 |
### 2.2 非本期范围
| 序号 | 范围项 | 说明 |
| --- | --- | --- |
| 1 | 直接覆盖原始法规模板 | 原始材料只作为模板来源,不允许被改写 |
| 2 | 自动代替人工最终确认 | 系统生成带标记文件,用户自行下载核对确认 |
| 3 | 在线提交 NMPA 系统 | 本期只生成申报文件,不对接外部申报系统 |
| 4 | 全部法规表单覆盖 | 本期仅覆盖用户指定的三个目标模板 |
| 5 | 复杂版式人工校订 | 系统尽量保持模板版式,复杂错位仍需人工最终复核 |
---
## 三、目标模板
本期一共处理三个目标模板。用户此前重复提到“体外诊断试剂安全和性能基本原则清单”,经确认属于误填,实际只有一个该清单模板。
| 序号 | 模板名称 | 原始文件 | 使用条件 | 输出要求 |
| --- | --- | --- | --- | --- |
| 1 | 中华人民共和国医疗器械注册证(体外诊断试剂)(格式) | 中华人民共和国医疗器械注册证(体外诊断试剂)(格式).docx | 首次注册 | Word + PDF |
| 2 | 中华人民共和国医疗器械变更注册(备案)文件(体外诊断试剂)(格式) | 中华人民共和国医疗器械变更注册(备案)文件(体外诊断试剂)(格式).doc | 变更注册或备案 | Word + PDF |
| 3 | 体外诊断试剂安全和性能基本原则清单 | 体外诊断试剂安全和性能基本原则清单.doc | 首次注册、变更注册、备案均适用 | Word + PDF |
### 3.1 已识别注册证模板字段
`中华人民共和国医疗器械注册证(体外诊断试剂)(格式).docx` 中已识别到以下表格栏目:
| 字段 | 填写规则 |
| --- | --- |
| 注册人名称 | 从申请人、注册人、企业信息类文件中抽取 |
| 注册人住所 | 从申请人、注册人、企业信息类文件中抽取 |
| 生产地址 | 从注册资料、说明书、质量体系或生产信息文件中抽取 |
| 代理人名称 | 进口体外诊断试剂适用,境内产品可留空 |
| 代理人住所 | 进口体外诊断试剂适用,境内产品可留空 |
| 产品名称 | 优先取说明书字段 |
| 包装规格 | 对应型号规格、包装规格 |
| 主要组成成分 | 优先取说明书和产品技术要求 |
| 预期用途 | 对应适用范围、预期用途 |
| 产品储存条件及有效期 | 对应储存条件、有效期 |
| 附件 | 默认包含产品技术要求、说明书,可根据实际文件匹配补充 |
| 其他内容 | 未识别或需人工确认时留空 |
| 备注 | 未识别或需人工确认时留空 |
### 3.2 模板解析约束
变更注册(备案)文件格式和安全和性能基本原则清单当前为 `.doc` 格式。系统实施时需要支持以下任一方案:
| 方案 | 说明 |
| --- | --- |
| LibreOffice 转换 | 使用 LibreOffice/soffice 将 `.doc` 转为 `.docx` 后识别和填写 |
| 预转换模板 | 项目内预先保存经人工确认的 `.docx` 模板副本 |
| OOXML/COM 方案 | 在 Windows 环境通过 Office 自动化读取和转换模板 |
无论采用哪种方式转换后的模板必须保留原文件表格结构、分页、字体和版式PDF 导出需以填好的 Word 为来源。
---
## 四、用户角色与使用场景
| 角色 | 诉求 | 典型场景 |
| --- | --- | --- |
| 注册人员 | 减少重复填表,提高字段一致性 | 上传注册资料包后生成已填注册证格式和基本原则清单 |
| 变更注册负责人 | 根据变更类型生成变更注册或备案文件 | 上传变更资料后生成已填变更注册(备案)文件 |
| 审核人员 | 快速定位字段来源和冲突 | 下载带冲突高亮的 Word/PDF并查看 AI 对话框冲突摘要 |
| 系统管理员 | 维护模板版本和转换能力 | 更新法规模板、检查 PDF 转换服务和导出记录 |
---
## 五、业务流程分析
### 5.1 主流程
```text
用户上传产品注册资料
-> 系统执行文件目录与页数汇总
-> 系统执行法规核查前置文本抽取
-> 系统识别注册类型:首次注册、变更注册或备案
-> 系统选择本次适用目标模板
-> 系统复制原始模板到批次工作目录
-> 系统读取目标模板栏目和清单条目
-> 系统从产品文件中抽取模板所需字段
-> 系统按字段优先级合并抽取结果
-> 如字段存在跨文件冲突,系统按说明书为准填入,并做黄色底色、红色字体标记
-> 缺失字段保持留空
-> 系统逐条判断安全和性能基本原则清单的适用性、符合性证据和证明文件位置
-> 系统生成已填 Word 文件
-> 系统将已填 Word 转换为 PDF
-> 系统生成来源追溯清单
-> AI 对话框展示生成结果、冲突字段摘要和下载链接
-> 用户下载 Word/PDF 自行确认
```
### 5.2 注册类型分支
| 注册类型 | 生成文件 |
| --- | --- |
| 首次注册 | 注册证格式 Word/PDF安全和性能基本原则清单 Word/PDF |
| 变更注册 | 变更注册(备案)文件 Word/PDF安全和性能基本原则清单 Word/PDF |
| 备案 | 变更注册(备案)文件 Word/PDF安全和性能基本原则清单 Word/PDF |
| 注册类型无法识别 | AI 对话框提示待确认;默认不生成注册证或变更文件,只可生成带待确认标记的草稿版本 |
### 5.3 异常流程
| 异常场景 | 处理方式 |
| --- | --- |
| 模板文件不存在 | 批次标记失败,对话框提示缺少目标模板 |
| `.doc` 模板无法转换 | 对应模板导出失败,其他模板继续生成 |
| 字段未提取到 | 目标栏位留空,来源追溯清单记录为空 |
| 字段冲突 | 按说明书为准填入,模板内高亮标记,对话框展示冲突摘要 |
| PDF 转换失败 | 保留 Word 下载,提示 PDF 生成失败原因 |
| 模板版式明显错位 | 标记为需人工复核,不阻断 Word 文件下载 |
---
## 六、信息提取与字段规则
### 6.1 字段范围
字段范围不固定写死应以三个目标模板的实际栏目和清单条目为准动态建立。Demo 阶段优先覆盖以下字段:
| 字段 | 说明 |
| --- | --- |
| 产品名称 | 产品标准名称 |
| 检测靶标 | 被检测物、基因、抗原、抗体、病原体或生物标志物 |
| 适用范围/预期用途 | 适用人群、样本类型、检测目的、临床用途 |
| 储存条件 | 温度、避光、防潮等保存条件 |
| 性能指标 | 分析灵敏度、特异性、重复性、准确度、检出限等 |
| 型号规格/包装规格 | 规格型号、包装规格、人份数或测试数 |
| 样本类型 | 血清、血浆、全血、咽拭子等 |
| 有效期 | 产品有效期或稳定性期限 |
| 主要组成成分 | 试剂、校准品、质控品、耗材等组成 |
| 检验原理 | 反应原理、方法学或检测平台 |
| 注册人/申请人 | 注册申请主体 |
| 生产地址 | 生产场所地址 |
### 6.2 来源文件优先级
| 优先级 | 文件类型 | 说明 |
| --- | --- | --- |
| 1 | 说明书 | 字段冲突时默认以说明书为准 |
| 2 | 产品技术要求 | 用于补充性能指标、检验方法、组成成分等字段 |
| 3 | 注册检验报告/检测报告 | 用于补充性能指标、样本信息、检验依据和结论 |
| 4 | 性能研究资料 | 用于补充安全和性能基本原则清单证据 |
| 5 | 其他注册资料 | 用于补充申请人、生产地址、附件清单等信息 |
### 6.3 冲突处理规则
| 场景 | 处理方式 |
| --- | --- |
| 说明书与其他文件字段不一致 | 按说明书值填入模板 |
| 多个非说明书文件不一致,说明书缺失 | 目标字段留空或取最高优先级来源,具体规则由实现阶段配置 |
| 字段被高亮标记 | 黄色底色、红色字体,提示用户下载后确认 |
| AI 对话框展示 | 展示字段名、采用值、冲突值、来源文件和目标模板 |
---
## 七、安全和性能基本原则清单填写规则
安全和性能基本原则清单不只填写基础产品信息,还需要根据产品文件内容逐条判断清单条目的适用性、符合性证据和证明文件位置。
| 填写项 | 规则 |
| --- | --- |
| 适用/不适用 | 根据产品特性、检测方法、样本类型、是否含仪器/软件/灭菌/生物材料等信息判断 |
| 符合性说明 | 从产品技术要求、说明书、风险管理、性能研究、稳定性研究等文件中提取证据摘要 |
| 证明文件位置 | 填写证据文件名、章节、页码或可定位文本片段 |
| 无法判断 | 留空或标记待人工确认,来源追溯清单记录原因 |
| 冲突证据 | 如不同文件对同一条款适用性或证据描述冲突,保留高亮并在对话框列出 |
逐条判断结果需要可追溯,不能只输出“适用”或“不适用”结论。
---
## 八、输出要求
### 8.1 文件命名
文件命名规则:
```text
批次号-产品名称-注册证格式.docx
批次号-产品名称-注册证格式.pdf
批次号-产品名称-变更注册备案文件.docx
批次号-产品名称-变更注册备案文件.pdf
批次号-产品名称-安全和性能基本原则清单.docx
批次号-产品名称-安全和性能基本原则清单.pdf
批次号-产品名称-字段来源追溯清单.xlsx
```
产品名称为空时,可使用 `未识别产品名称` 作为文件名占位。
### 8.2 AI 对话框摘要
AI 对话框应展示生成结果、下载链接和冲突字段摘要。
```markdown
已生成申报模板自动填表文件。
| 文件 | Word | PDF |
| --- | --- | --- |
| 注册证格式 | 下载 | 下载 |
| 安全和性能基本原则清单 | 下载 | 下载 |
| 冲突字段 | 采用值 | 冲突来源 | 处理 |
| --- | --- | --- | --- |
| 储存条件 | 2-8℃保存 | 产品技术要求:-20℃保存 | 已按说明书填入,并在模板中高亮 |
```
### 8.3 Word 输出
| 要求 | 说明 |
| --- | --- |
| 模板副本 | 从原始法规模板复制生成,不覆盖原始文件 |
| 版式保持 | 保留原模板表格、段落、分页、字体和标题结构 |
| 冲突高亮 | 黄色底色、红色字体 |
| 缺失字段 | 留空,不填“待补充” |
| 可编辑 | 用户可下载后继续人工修改 |
### 8.4 PDF 输出
| 要求 | 说明 |
| --- | --- |
| 来源 | 由填好的 Word 转换生成 |
| 版式 | 尽量与原 Word 模板一致 |
| 用途 | 可作为正式提交前预览 |
| 失败处理 | PDF 失败不影响 Word 下载 |
### 8.5 来源追溯清单
来源追溯清单允许额外生成,建议至少包含:
| 字段 | 说明 |
| --- | --- |
| 目标模板 | 字段填入哪个模板 |
| 目标栏位/条目 | 字段对应的表格栏位或清单条目 |
| 填入值 | 实际写入模板的值 |
| 来源文件 | 取值来源文件 |
| 来源片段 | 支撑取值的文本片段 |
| 是否冲突 | 是/否 |
| 冲突值 | 其他文件中的不同值 |
| 处理方式 | 采用说明书、留空、高亮、待人工确认等 |
---
## 九、功能模块梳理
| 序号 | 功能名称 | 功能描述 | 优先级 |
| --- | --- | --- | --- |
| 1 | 模板管理 | 维护三个目标模板路径、版本和适用注册类型 | P0 |
| 2 | 模板副本生成 | 将原始模板复制到批次工作目录 | P0 |
| 3 | 模板结构识别 | 识别模板中的表格字段、段落占位、清单条目 | P0 |
| 4 | 产品字段抽取 | 从上传文件中抽取模板所需产品字段 | P0 |
| 5 | 字段合并与冲突检测 | 按说明书优先级合并字段,并识别跨文件冲突 | P0 |
| 6 | Word 模板填充 | 将字段写入 Word 模板副本 | P0 |
| 7 | 冲突高亮 | 对冲突字段应用黄色底色和红色字体 | P0 |
| 8 | 基本原则逐条判断 | 判断安全和性能条目的适用性、符合性证据和证明文件位置 | P0 |
| 9 | PDF 转换 | 将填好的 Word 转为 PDF | P0 |
| 10 | 下载链接生成 | 在 AI 对话框提供 Word/PDF 下载链接 | P0 |
| 11 | 来源追溯清单导出 | 输出字段来源、冲突和填入目标 | P1 |
| 12 | 版式 QA | 对 Word/PDF 版式进行自动或人工可见检查 | P1 |
---
## 十、数据实体分析
| 实体名称 | 字段说明 | 关联实体 |
| --- | --- | --- |
| 自动填表批次 | 批次编号、用户、会话、注册类型、产品名称、状态、错误信息、创建时间、完成时间 | 文件汇总批次、法规核查批次 |
| 模板副本 | 模板名称、模板类型、原始模板路径、副本路径、模板版本、适用条件 | 自动填表批次 |
| 提取字段 | 字段名、填入值、来源文件、来源片段、来源优先级、是否冲突、冲突详情 | 自动填表批次 |
| 填表结果文件 | 文件类型、文件名、Word 路径、PDF 路径、下载状态 | 自动填表批次 |
| 清单条目判断 | 条目编号、条目内容、适用性、符合性证据、证明文件位置、判断来源 | 自动填表批次 |
---
## 十一、非功能性需求
### 11.1 可追溯性
| 要求 | 说明 |
| --- | --- |
| 字段来源可追溯 | 每个填入字段应能追溯到来源文件和文本片段 |
| 模板版本可追溯 | 每次生成记录原始模板文件名、版本和路径 |
| 冲突处理可追溯 | 冲突字段记录采用值、冲突值和处理规则 |
| 输出文件可追溯 | Word/PDF 文件关联批次、用户和会话 |
### 11.2 安全要求
| 要求 | 说明 |
| --- | --- |
| 原始模板保护 | 不允许覆盖或修改原始法规资料目录中的模板 |
| 下载权限 | Word/PDF/追溯清单仅允许当前会话授权用户下载 |
| 敏感信息保护 | 对话框只展示必要冲突摘要,不展示大段敏感原文 |
| 文件隔离 | 不同用户、不同批次的模板副本和导出文件隔离存储 |
### 11.3 版式要求
| 要求 | 说明 |
| --- | --- |
| Word 版式 | 尽量保持原模板表格、字体、分页和段落结构 |
| PDF 版式 | 与填好后的 Word 一致,可用于正式提交前预览 |
| 高亮可见 | 冲突字段在 Word 和 PDF 中均应能被用户识别 |
| 缺失字段不污染模板 | 未提取字段留空,不填入系统提示语 |
### 11.4 性能要求
| 场景 | 要求 |
| --- | --- |
| 小批次资料 | 50 个文件以内,应在 1 分钟内完成字段抽取和模板生成 |
| 中等批次资料 | 200 个文件以内支持后台异步处理和进度提示 |
| 单个模板失败 | 不影响其他适用模板生成 |
| 单个字段失败 | 不影响整份模板生成,字段留空并记录原因 |
---
## 十二、待后续确认事项
| 序号 | 待确认项 | 当前建议 |
| --- | --- | --- |
| 1 | `.doc` 模板转换方案 | 优先使用 LibreOffice/soffice 转 docx无法部署时预置人工确认版 docx 模板 |
| 2 | 变更注册(备案)文件字段清单 | 需在模板可解析后补充字段映射 |
| 3 | 安全和性能基本原则清单条目结构 | 需在模板可解析后拆解条目编号、要求、适用性和证据栏 |
| 4 | 说明书识别规则 | 需明确如何从上传资料中判定哪份文件是说明书 |
| 5 | PDF 转换质量标准 | 需明确是否要求逐页渲染检查、页数一致和关键表格不跨页错位 |
| 6 | 注册类型无法识别时是否允许生成草稿 | 建议允许生成安全和性能基本原则清单,注册证或变更文件等待确认 |
---
## 十三、验收标准
| 序号 | 验收项 | 验收标准 |
| --- | --- | --- |
| 1 | 模板复制 | 系统生成模板副本,不修改原始法规模板 |
| 2 | 首次注册文件选择 | 首次注册场景生成注册证格式和安全和性能基本原则清单 |
| 3 | 变更注册/备案文件选择 | 变更注册或备案场景生成变更注册(备案)文件和安全和性能基本原则清单 |
| 4 | 字段自动填写 | 产品名称、预期用途、储存条件、包装规格等字段能自动写入对应栏目 |
| 5 | 缺失字段留空 | 未提取到的字段保持空白 |
| 6 | 冲突字段高亮 | 字段冲突时按说明书值填入,并在 Word/PDF 中黄色底色、红色字体标记 |
| 7 | 冲突摘要展示 | AI 对话框展示冲突字段、采用值、冲突来源和处理方式 |
| 8 | 基本原则清单判断 | 系统能逐条输出适用/不适用、符合性证据和证明文件位置 |
| 9 | Word 下载 | 对话框提供填好后的 Word 下载链接 |
| 10 | PDF 下载 | 对话框提供由 Word 转换生成的 PDF 下载链接 |
| 11 | 来源追溯 | 可导出字段来源追溯清单,记录字段来源和冲突情况 |
| 12 | 异常不中断 | 单个字段、单个模板或 PDF 转换失败时,其他结果仍可正常输出 |
---
## 十四、下一步建议
1. 将两个 `.doc` 原始模板转换为可解析的 `.docx` 工作模板,并人工确认版式无明显变化。
2. 拆解三个模板的字段、表格和清单条目,形成模板字段映射配置。
3. 扩展产品信息抽取字段,优先覆盖注册证模板已识别字段和安全和性能基本原则清单证据字段。
4. 设计冲突高亮写入规则,确保 Word 与 PDF 中均可见。
5. 接入 Word 到 PDF 转换能力,并建立页数、版式和关键表格的转换质量检查。

View File

@@ -0,0 +1,527 @@
# 飞书通知与问答接入需求分析
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 功能主题 | 飞书通知链路与飞书内问答能力接入 |
| 关联工作流 | 自动汇总、NMPA 注册资料法规核查与整改闭环、产品关键信息提取与申报文件自动填表 |
| 分析日期 | 2026-06-07 |
| 分析版本 | V1.0 |
| 短期目标 | 流程结束后同步飞书提醒 |
| 终极目标 | 用户可在飞书内向 Agent 提问并获得基于系统数据的回答 |
---
## 一、需求背景
当前系统已经具备多个注册资料处理工作流,包括文件自动汇总、法规核查与整改闭环、产品关键信息提取与申报文件自动填表。系统内已经存在模拟通知记录和通知节点,但尚未接入真实飞书发送链路。
在实际业务协作中,注册人员、审核人员和整改负责人往往以飞书群或飞书私聊作为日常沟通入口。如果工作流只在系统页面内展示结果,用户需要主动返回系统查看状态,容易造成流程完成后无人跟进、整改项遗漏、生成文件未及时下载等问题。
因此需要引入飞书接入能力,分阶段实现:
1. 流程结束后自动向飞书发送提醒,完成从“系统内闭环”到“协作通知闭环”的升级。
2. 后续支持用户在飞书内与 Agent 对话,查询批次状态、风险项、生成文件、整改建议等信息。
---
## 二、接入方案调研摘要
### 2.1 主方案:飞书官方智能体/应用机器人 + 消息 API
飞书开放平台支持创建飞书智能体应用或企业自建应用机器人。系统通过 `App ID``App Secret` 获取 `tenant_access_token`,再调用飞书消息 API 向固定群发送流程完成提醒;后续通过事件订阅接收用户私聊机器人或群内 @ 机器人的消息,实现飞书内问答。
该方案同时覆盖短期“流程结束后提醒”和终极“飞书内问答”,避免先接自定义 Webhook、后续再迁移到应用机器人的重复建设。
核心能力:
| 能力 | 用途 |
| --- | --- |
| 飞书智能体/应用机器人 | 允许 Agent 以机器人身份进入飞书 |
| tenant_access_token | 使用 App ID、App Secret 换取应用访问令牌 |
| 发送消息 API | 主动向用户或群聊发送文本、富文本、卡片、文件等消息 |
| 事件订阅 | 接收用户私聊机器人或群里 @ 机器人的消息 |
| 权限配置 | 申请发送消息、接收消息、读取用户或群组信息等权限 |
### 2.2 备选方案:飞书自定义机器人 Webhook
飞书自定义机器人 Webhook 适合只做固定群主动推送,但不适合飞书内问答、私聊回复和统一身份权限管理。本项目不将 Webhook 作为主接入方案,仅作为后续极简部署或故障降级备选。
### 2.3 参考官方文档
| 主题 | 参考地址 |
| --- | --- |
| 一键创建飞书智能体应用 | https://open.feishu.cn/document/mcp_open_tools/integrating-agents-with-feishu/overview |
| 机器人概述 | https://open.feishu.cn/document/client-docs/bot-v3/bot-overview?lang=zh-CN |
| 自建应用获取 tenant_access_token | https://open.feishu.cn/document/server-docs/authentication-management/access-token/tenant_access_token_internal?lang=zh-CN |
| 发送消息 API | https://open.feishu.cn/document/server-docs/im-v1/message/create?lang=zh-CN |
| 事件订阅概述 | https://open.feishu.cn/document/server-docs/event-subscription-guide/overview?lang=zh-CN |
---
## 三、总体目标
### 3.1 短期目标:流程结束后同步提醒到指定个人账号
当系统中的工作流执行结束后自动通过飞书智能体向指定个人账号发送一条结构化私聊提醒。Demo 阶段先与当前系统负责人账号单独对接,不接入外部群聊。提醒内容应帮助用户快速判断:
| 信息项 | 说明 |
| --- | --- |
| 哪个流程完成 | 例如自动汇总、法规核查、自动填表 |
| 哪个批次完成 | 展示批次编号、会话标题或上传文件摘要 |
| 当前状态 | 成功、部分成功、失败、需人工确认 |
| 核心结果 | 风险数量、阻断项数量、生成文件数量、冲突字段数量等 |
| 下一步动作 | 查看报告、下载文件、处理整改项、回到系统确认 |
| 系统入口 | 提供可点击链接,跳转到对应批次或会话页面 |
| 被提醒人 | 首期固定发送给已配置的个人飞书账号 |
### 3.2 中期目标:按流程和责任人分发
在个人账号通知跑通后,逐步支持更精细的通知策略:
| 通知策略 | 说明 |
| --- | --- |
| 按发起人私聊 | 根据系统用户映射发送给流程发起人 |
| 按工作流分群 | 不同工作流通知到不同群 |
| 按项目分群 | 同一项目或产品线通知到指定群 |
| 按负责人私聊 | 将待处理事项发送给上传人、审核人或整改负责人 |
| 风险分级通知 | 阻断项和高风险立即通知,低风险可汇总通知 |
### 3.3 终极目标:飞书内问答
用户可以在飞书内向 Agent 提问,系统根据用户消息识别意图,查询本地业务数据和已生成结果,返回回答。
示例问题:
| 问题 | 预期回答 |
| --- | --- |
| “最近一个法规核查批次结果怎么样?” | 返回最近批次状态、风险数量和报告入口 |
| “RR-20260607-001 有哪些阻断项?” | 返回阻断项标题、法规依据、整改建议 |
| “自动填表生成的 Word 在哪里?” | 返回生成文件列表和下载入口 |
| “这个批次还缺哪些资料?” | 返回缺失文件清单和对应建议 |
| “帮我解释第 3 个风险项” | 返回风险说明、证据文件、整改建议和注意事项 |
---
## 四、需求范围
### 4.1 本期范围
| 序号 | 范围项 | 说明 |
| --- | --- | --- |
| 1 | 真实飞书通知通道 | 接入飞书官方智能体/应用机器人消息 API |
| 2 | 通知开关 | 通过环境变量控制是否启用真实飞书通知 |
| 3 | 保留 mock 通道 | 默认可回退到 mock不影响本地开发和自动化测试 |
| 4 | 工作流完成通知 | 流程成功、部分成功或失败后发送飞书提醒 |
| 5 | 通知记录落库 | 记录通道、目标、发送状态、发送时间、错误信息和原始 payload |
| 6 | 失败不阻断主流程 | 飞书发送失败只记录错误,不让业务工作流失败,首期不自动重试 |
| 7 | 消息模板 | 输出清晰的富文本消息,包含批次、状态、摘要和系统链接 |
| 8 | 安全配置 | App ID、App Secret、事件订阅密钥等敏感配置不得写入代码库 |
| 9 | 基础测试 | 覆盖成功、失败、未启用、配置缺失、发送超时等场景 |
### 4.2 非本期范围
| 序号 | 范围项 | 说明 |
| --- | --- | --- |
| 1 | 飞书内问答完整实现 | 本期只为后续问答预留架构,不直接实现复杂对话 |
| 2 | 飞书审批流 | 不接入飞书审批或表单能力 |
| 3 | 飞书文档写入 | 不自动创建或更新飞书文档 |
| 4 | 企业级组织架构同步 | 不做通讯录全量同步 |
| 5 | 多租户飞书应用管理 | Demo 阶段只考虑单企业或单环境配置 |
| 6 | 复杂交互式卡片操作 | 本期优先文本或简单卡片,不实现按钮回调闭环 |
---
## 五、用户角色与使用场景
| 角色 | 诉求 | 典型场景 |
| --- | --- | --- |
| 注册人员 | 及时知道批次完成并下载结果 | 自动汇总或自动填表完成后在飞书收到提醒 |
| 审核人员 | 快速查看法规核查风险摘要 | 法规核查结束后查看阻断项和高风险数量 |
| 整改负责人 | 及时处理缺失资料和风险项 | 飞书提醒中看到整改入口和主要问题 |
| 系统管理员 | 维护通知配置并排查发送失败 | 查看通知记录、错误信息和配置状态 |
| 后续飞书用户 | 不打开系统也能查询结果 | 在飞书中向机器人提问批次状态或风险项 |
---
## 六、业务流程
### 6.1 短期通知流程
```text
用户发起业务工作流
-> 系统执行自动汇总、法规核查或自动填表
-> 工作流进入完成、部分成功或失败状态
-> 系统生成通知摘要
-> 系统判断飞书真实通知是否启用
-> 未启用:写入 mock 通知记录
-> 已启用:使用 App ID/App Secret 获取或复用 tenant_access_token
-> 调用飞书消息 API 向指定个人账号发送富文本消息
-> 发送成功:写入成功通知记录和 sent_at
-> 发送失败:写入失败通知记录、错误信息和重试次数
-> 主工作流继续完成,不因通知失败回滚业务结果
```
### 6.2 终极问答流程
```text
用户在飞书私聊机器人或群里 @ 机器人提问
-> 飞书通过事件订阅将消息推送到系统回调地址
-> 系统校验事件来源和签名
-> 系统解析用户身份、会话位置和消息内容
-> 系统执行意图识别
-> 系统根据意图查询批次、文件、报告、风险项或生成结果
-> 系统组织回答内容
-> 系统通过飞书消息 API 回复用户或群聊
-> 系统记录问答日志、引用数据和错误信息
```
---
## 七、功能需求
### 7.1 通知触发点
| 工作流 | 触发节点 | 通知时机 | 初始优先级 |
| --- | --- | --- | --- |
| 自动汇总 | 文件汇总完成 | 成功、部分成功、失败 | 高 |
| 法规核查与整改闭环 | 风险分级和报告生成后 | 成功、部分成功、失败;阻断项和高风险优先展示 | 高 |
| 自动填表 | Word/PDF 和追溯清单生成后 | 成功、部分成功、失败 | 高 |
首期三个业务工作流均接入飞书通知:自动汇总、法规核查与整改闭环、产品关键信息提取与申报文件自动填表。
### 7.2 通知内容模板
通知消息首期采用富文本格式,需支持换行和重点信息突出展示。通知消息应至少包含:
| 字段 | 说明 |
| --- | --- |
| 标题 | 工作流名称 + 状态 |
| 批次编号 | 例如 RR-NOTIFY、AFF-NOTIFY |
| 发起人 | 当前系统用户 |
| 完成时间 | 工作流完成时间 |
| 结果摘要 | 风险数量、文件数量、导出文件数量、冲突字段数量等 |
| 下一步 | 查看报告、下载结果、处理整改项、重新复核 |
| 系统链接 | 首期使用本地地址拼接系统内批次或会话页面链接,例如 `http://127.0.0.1:8000/...` |
发送策略:
| 策略项 | 要求 |
| --- | --- |
| 通知状态 | 成功、部分成功、失败均发送飞书通知 |
| 重复发送 | 同一批次、同一工作流、同一状态只发送一次,避免重复点击或重复运行造成刷屏 |
| 失败重试 | 首期不自动重试,只记录失败状态和错误信息 |
| 主流程影响 | 通知失败不阻断业务工作流完成 |
消息内容粒度:
| 粒度项 | 要求 |
| --- | --- |
| 基础信息 | 工作流名称、状态、批次编号、发起人、完成时间 |
| 结果摘要 | 自动汇总展示文件数和异常数;法规核查展示风险总数、阻断项、高风险数量;自动填表展示导出文件数、冲突字段数和失败原因概述 |
| 详细清单 | 首期不在飞书私聊中展开完整风险项、缺失项或文件明细,避免消息过长 |
| 系统入口 | 首期使用本地地址拼接系统内批次或会话链接,部署后再升级为可配置外部域名 |
### 7.3 通知状态记录
通知发送后必须落库,便于排查和审计。
| 字段 | 说明 |
| --- | --- |
| channel | mock、feishu_api 等 |
| target | 指定个人 open_id、user_id 等 |
| status | pending、sent、failed 或 success、failed |
| payload | 发送内容摘要和业务上下文 |
| external_message_id | 飞书返回的消息 IDWebhook 无返回时可为空 |
| error_message | 失败原因 |
| retry_count | 重试次数 |
| sent_at | 成功发送时间 |
### 7.4 配置需求
环境变量建议:
| 配置项 | 说明 |
| --- | --- |
| FEISHU_NOTIFY_ENABLED | 是否启用真实飞书通知 |
| FEISHU_NOTIFY_CHANNEL | 通知通道,首期为 feishu_api |
| FEISHU_APP_ID | 飞书智能体/企业自建应用 App ID |
| FEISHU_APP_SECRET | 飞书智能体/企业自建应用 App Secret |
| FEISHU_DEFAULT_USER_OPEN_ID | 首期指定接收人的飞书 open_id |
| FEISHU_DEFAULT_USER_ID | 首期指定接收人的飞书 user_id可作为 open_id 的备选 |
| FEISHU_DEFAULT_TARGET_NAME | 默认通知目标名称,用于记录展示 |
| FEISHU_TENANT_TOKEN_CACHE_SECONDS | tenant_access_token 本地缓存秒数 |
| FEISHU_EVENT_VERIFY_TOKEN | 事件订阅校验 Token后续问答使用 |
| FEISHU_EVENT_ENCRYPT_KEY | 事件订阅加密 Key后续问答使用 |
敏感配置不得提交到代码库,只能通过本地 `.env`、部署环境变量或密钥管理系统注入。
首期配置维护方式:
| 配置类型 | 维护方式 | 说明 |
| --- | --- | --- |
| 飞书 App ID | 环境变量 | 属于敏感信息,不进入数据库和代码库 |
| 飞书 App Secret | 环境变量 | 属于敏感信息,不进入数据库和代码库 |
| 指定接收人 open_id/user_id | 环境变量 | 首期固定发送到一个个人账号 |
| 通知开关 | 环境变量 | 便于本地、测试、部署环境切换 |
| 系统用户与飞书用户映射 | Django Admin | 便于非开发人员维护发起人和飞书用户标识 |
### 7.5 系统用户与飞书用户映射
首期采用手工配置表维护系统用户与飞书用户之间的映射关系。系统在发送固定群通知时,根据批次 `user` 字段找到流程发起人或上传人,再从映射表中读取可用于飞书 @ 的用户标识。
建议字段:
| 字段 | 说明 |
| --- | --- |
| system_username | 系统登录用户名 |
| system_user_id | 系统用户 ID可选 |
| feishu_display_name | 飞书展示名称,便于管理员识别 |
| feishu_mobile | 飞书手机号,可选 |
| feishu_open_id | 飞书 open_id可选 |
| feishu_user_id | 飞书 user_id可选 |
| is_active | 是否启用该映射 |
| remark | 备注 |
首期实现时,系统优先将通知发送给环境变量中配置的指定个人账号。用户映射表仍保留,用于后续从“固定个人账号”升级为“按流程发起人私聊”。若指定接收人未配置,系统不发送真实飞书消息,只记录配置缺失失败。
当同一个系统用户配置了多个飞书标识时,首期按以下优先级选择 @ 标识:
```text
feishu_open_id -> feishu_user_id -> feishu_mobile
```
### 7.6 通知记录展示
首期需要在对应批次详情页展示通知状态,帮助用户和管理员判断飞书提醒是否已发送。
| 展示项 | 说明 |
| --- | --- |
| 通知通道 | mock、feishu_api 等 |
| 通知目标 | 指定个人账号名称或配置名称 |
| 接收人 | 首期指定接收人;后续可展示发起人/上传人的飞书展示名称 |
| 发送状态 | 成功、失败、待发送或未启用 |
| 发送时间 | 成功发送时间 |
| 失败原因 | 发送失败或配置异常时展示摘要 |
---
## 八、飞书内问答需求预留
### 8.1 问答入口
| 入口 | 说明 |
| --- | --- |
| 私聊机器人 | 首期入口,用户直接向机器人询问自己的批次、文件和报告 |
| 群聊 @ 机器人 | 群内成员 @ 机器人询问某个批次或风险项 |
| 通知消息引用 | 用户收到通知后,基于批次编号继续提问 |
### 8.2 问答能力边界
第一阶段飞书问答不应直接执行高风险写操作,只提供查询和解释:
| 能力 | 是否纳入首期问答 |
| --- | --- |
| 查询批次状态 | 是 |
| 查询风险项摘要 | 是 |
| 查询缺失项摘要 | 是 |
| 查询生成文件摘要 | 是 |
| 解释整改建议 | 否,作为后续增强 |
| 重新发起工作流 | 否 |
| 删除文件或记录 | 否 |
| 自动关闭风险项 | 否 |
| 修改申报文件 | 否 |
### 8.3 权限原则
飞书内问答必须解决用户身份和数据权限问题:
| 场景 | 要求 |
| --- | --- |
| 私聊查询 | 普通用户只能查询自己发起或上传的批次;管理员可以查询全部批次 |
| 群内查询 | 只返回适合在群内公开的信息,敏感文件链接需谨慎 |
| 未绑定用户 | 提示先完成系统用户与飞书用户绑定 |
| 无权限数据 | 返回无权限提示,不泄露批次是否存在以外的敏感信息 |
### 8.4 首期问答交互规则
首期私聊问答支持两类批次定位方式:
| 方式 | 示例 | 说明 |
| --- | --- | --- |
| 明确批次号 | “查 RR-20260607-001” | 系统按批次编号精确查询 |
| 自然指代 | “查最近一个法规核查批次”“最新自动填表结果怎么样” | 系统在用户可访问范围内查找最近批次 |
问答回复规则:
| 规则 | 要求 |
| --- | --- |
| 链接返回 | 只有用户具备对应批次访问权限时才返回系统链接 |
| 无权限结果 | 提示无权限或无法访问,不返回敏感摘要和链接 |
| 回答粒度 | 返回批次状态、风险摘要、缺失摘要、导出摘要和下一步建议 |
| 日志留痕 | 记录用户问题、识别意图、查询对象、回答摘要、错误信息和处理时间,不保存完整回答正文 |
---
## 九、异常与安全要求
| 场景 | 处理方式 |
| --- | --- |
| App ID/App Secret 或指定接收人未配置 | 自动回退 mock 或只记录未发送状态 |
| tenant_access_token 获取失败 | 记录失败,不发送消息,不阻断主流程 |
| 飞书接口超时 | 记录失败,不阻断主流程 |
| 飞书返回错误 | 记录错误码和错误信息,便于排查 |
| 消息过长 | 自动截断摘要,系统链接保留完整结果;首期不发送详细风险项或缺失项清单 |
| 重复触发 | 同一批次、同一工作流、同一状态只发送一次 |
| 敏感信息 | 通知正文避免包含完整文件内容、密钥、个人敏感信息 |
| 外部链接 | 首期使用本地地址;部署环境应升级为可信域名配置 |
| 回调伪造 | 后续事件订阅必须校验来源、签名、Token 或加密参数 |
---
## 十、验收标准
### 10.1 短期通知验收
| 序号 | 验收项 | 标准 |
| --- | --- | --- |
| 1 | 配置关闭 | 未启用飞书通知时,工作流仍可正常完成并记录 mock 通知 |
| 2 | 配置开启 | 配置 App ID、App Secret 和指定个人 open_id/user_id 后,流程完成会向个人飞书账号发送提醒 |
| 3 | 成功记录 | 发送成功后通知记录状态为成功,并记录发送时间 |
| 4 | 失败记录 | token 获取失败、消息 API 错误、超时或配置错误时记录失败原因 |
| 5 | 不阻断主流程 | 通知失败不会导致工作流失败 |
| 6 | 内容完整 | 飞书消息包含工作流、批次、状态、摘要和系统入口 |
| 7 | 自动化测试 | 有单元测试覆盖通知构造、发送成功、发送失败、配置关闭 |
| 8 | token 管理 | 系统能获取并缓存 tenant_access_tokentoken 失效后可重新获取 |
| 9 | 后台映射 | 管理员可在 Django Admin 维护系统用户与飞书用户映射 |
### 10.2 终极问答验收
| 序号 | 验收项 | 标准 |
| --- | --- | --- |
| 1 | 消息接收 | 系统能接收飞书私聊或群 @ 机器人消息 |
| 2 | 身份识别 | 能识别飞书用户并关联系统用户 |
| 3 | 意图识别 | 能区分批次查询、风险查询、文件查询、解释类问题 |
| 4 | 权限控制 | 普通用户只能查询自己发起或上传的批次;管理员可查询全部批次 |
| 5 | 消息回复 | 系统能通过飞书消息 API 回复用户 |
| 6 | 日志留痕 | 用户问题、意图、查询对象、回答摘要和错误信息可追溯,不保存完整回答正文 |
| 7 | 批次定位 | 支持明确批次号和“最近一个/最新批次”等自然说法 |
| 8 | 链接控制 | 只有用户有权限访问时才返回系统链接 |
---
## 十一、阶段规划
### 阶段一:指定个人账号完成提醒
目标:使用飞书官方智能体/应用机器人消息 API 将流程完成提醒发送到指定个人账号。Demo 阶段先与当前系统负责人账号单独对接,暂不接入外部群聊。
建议内容:
| 内容 | 说明 |
| --- | --- |
| 通知通道抽象 | 将 mock 和 feishu_api 封装为可切换通道 |
| 消息模板 | 输出流程完成摘要 |
| 指定接收人 | 根据环境变量配置的 open_id/user_id 发送给指定个人账号 |
| token 管理 | 使用 App ID/App Secret 获取并缓存 tenant_access_token |
| 消息 API | 使用指定个人 open_id/user_id 调用飞书发送消息 API |
| 通知记录 | 发送结果落库 |
| 配置开关 | 环境变量控制启用与否 |
| 测试覆盖 | 不依赖真实飞书也能测试发送逻辑 |
| 批次详情展示 | 在批次详情页展示通知状态和失败原因 |
### 阶段一附加:飞书问答预留
目标:在不实现飞书事件回调和私聊问答的前提下,为后续问答 MVP 预留必要的数据结构、服务边界和权限规则。
建议内容:
| 内容 | 说明 |
| --- | --- |
| 用户映射复用 | 飞书用户映射模型同时服务 @ 通知和后续私聊身份识别 |
| 查询服务边界 | 预留按批次号、最近批次、工作流类型查询结果摘要的服务接口 |
| 权限过滤规则 | 查询服务内置管理员全查、普通用户查自己批次的权限规则 |
| 问答日志模型预留 | 可先设计模型或接口,不要求首期接收飞书消息 |
### 阶段二:按流程或项目分群
目标:支持不同流程、项目或业务线配置不同飞书目标。
建议内容:
| 内容 | 说明 |
| --- | --- |
| 通知路由 | 根据 workflow_type、project、batch 等选择目标 |
| 通知策略 | 风险等级、完成状态、失败状态决定是否通知 |
| 消息降噪 | 避免同一批次重复刷屏 |
### 阶段三:事件订阅与私聊问答
建议内容:
| 内容 | 说明 |
| --- | --- |
| 事件回调 | 接收飞书私聊消息事件 |
| 用户绑定 | 使用飞书 open_id/user_id 映射系统用户 |
| 问答处理 | 查询批次状态、风险摘要、缺失摘要和导出摘要 |
| 回复消息 | 继续使用消息 API 回复用户 |
### 阶段四:飞书内问答
目标:通过事件订阅接收用户消息,并调用系统 Agent 能力回答问题。
建议内容:
| 内容 | 说明 |
| --- | --- |
| 事件回调 | 接收私聊和群 @ 消息 |
| 意图识别 | 解析查询对象和问题类型 |
| 数据查询 | 查询批次、风险、文件、报告和通知记录 |
| 回答生成 | 返回简洁、可追溯、带链接的回答 |
| 安全审计 | 记录问答日志和权限判断 |
---
## 十二、待确认问题
| 编号 | 问题 | 推荐选项 |
| --- | --- | --- |
| Q1 | 短期通知发到哪里?固定飞书群、按业务群区分、还是按个人私聊? | 已调整:先发送到指定个人账号,暂不接入外部群聊 |
| Q2 | 首期接入哪些工作流?自动填表、法规核查、自动汇总是否都通知? | 已确认:三个流程都通知 |
| Q3 | 通知格式用普通文本、富文本还是飞书消息卡片? | 已确认:首期使用富文本 |
| Q4 | 系统链接使用本地地址还是部署域名? | Demo 本地,部署后改域名 |
| Q5 | 是否需要 @ 指定人员? | 已调整:首期为个人私聊通知,不需要群内 @ |
| Q6 | 是否需要失败重试? | 已确认:首期不自动重试,只记录失败 |
| Q7 | 飞书内问答优先支持私聊还是群 @ | 先私聊,后群 @ |
---
## 十三、已确认决策
| 编号 | 决策 | 影响 |
| --- | --- | --- |
| D1 | 短期通知发送到指定个人账号,暂不接入外部群聊 | 首期需要配置个人 open_id/user_id后续再扩展群聊、按发起人私聊和责任矩阵 |
| D2 | 首期接收人为配置中的固定个人账号 | 通知服务不再依赖批次 `user` 解析接收人;批次 `user` 仍用于摘要展示和后续按发起人私聊 |
| D3 | 首期采用手工配置表维护系统用户与飞书用户映射 | 避免首期被通讯录权限、用户自动绑定和开放平台审核阻塞;后续可升级为自动绑定 |
| D4 | 首期三个流程均发送飞书完成通知 | 自动汇总、法规核查、自动填表都需要接入统一通知服务;消息发送到指定个人账号 |
| D5 | 首期通知格式采用飞书富文本 | 消息构造需支持富文本结构、换行、重点字段和 @ 用户标签;暂不实现消息卡片按钮 |
| D6 | 成功、部分成功、失败三类状态均发送通知 | 消息模板需要按状态展示不同摘要和下一步动作 |
| D7 | 同一批次、同一工作流、同一状态只发送一次 | 通知记录需要保存可判重的业务键,发送前先检查历史成功或已发送记录 |
| D8 | 首期飞书发送失败不自动重试 | 通知失败只落库并暴露错误信息,不引入异步重试队列 |
| D9 | 飞书消息链接首期使用本地地址 | 满足本机 Demo部署环境后续升级为可信域名配置 |
| D10 | 飞书消息采用摘要级内容粒度 | 私聊通知展示核心结果摘要和入口链接,不展开完整风险项、缺失项或文件明细 |
| D11 | 指定个人接收人未配置时不发送真实飞书消息 | 记录配置缺失失败或回退 mock用户映射缺失不影响首期固定个人通知 |
| D12 | 通知记录只保存发送摘要,不保存完整富文本 payload | 降低记录冗余和敏感信息留存风险;排查时依赖摘要、状态、错误信息和业务上下文 |
| D13 | App ID、App Secret、指定个人 open_id/user_id 等敏感配置通过环境变量维护,用户映射通过 Django Admin 维护 | 兼顾安全性和运维便利性;用户映射服务于后续按发起人私聊和问答身份识别 |
| D14 | 首期使用 tenant_access_token + 飞书消息 API 发送通知 | 通知客户端需要实现 token 获取、缓存、失效重取和消息 API 错误处理 |
| D15 | 飞书内问答首期入口为私聊机器人 | 优先解决个人查询场景,降低群聊权限泄露风险 |
| D16 | 飞书内问答首期回答批次状态、风险摘要、缺失摘要和导出摘要 | 不在首期做具体风险解释和复杂整改建议生成 |
| D17 | 私聊问答支持明确批次号和“最近/最新”自然说法 | 问答解析需要支持批次编号识别和按工作流类型查询最近可访问批次 |
| D18 | 问答权限为管理员可查全部,普通用户只能查自己发起或上传的批次 | 需要识别系统管理员身份,并在查询层统一做权限过滤 |
| D19 | 问答回复仅在用户有权限时返回系统链接 | 链接生成必须在权限校验之后执行 |
| D20 | 问答日志记录问题、意图、查询对象和回答摘要,不保存完整回答 | 兼顾审计排查与敏感信息最小留存 |
| D21 | 首期实现指定个人私聊通知,并预留飞书问答数据模型和服务边界 | 不在首期实现飞书事件回调和交互式问答,降低一次性交付风险 |
| D22 | 批次详情页需要展示通知状态 | 用户无需进入数据库或 Admin 即可确认飞书提醒是否发送成功 |
| D23 | 多个飞书标识的 @ 优先级为 `open_id > user_id > mobile` | 优先使用稳定标识,手机号作为兜底 |
| D24 | 本需求文档版本升级为 V1.0 | 当前决策已足够进入功能设计阶段 |

View File

@@ -0,0 +1,450 @@
# 第1章监管信息材料包生成需求分析
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 原始输入 | docs/0.原始材料/目标产品说明书.docx |
| 样例模板 | docs/0.原始材料/第1章 监管信息 |
| 法规材料 | docs/0.原始材料/关于公布体外诊断试剂注册申报资料要求和批准证明文件格式的公告 |
| 功能主题 | 从产品说明书生成第1章监管信息材料包 |
| 工作流名称 | 第1章监管信息材料包生成 |
| 工作流编码 | regulatory_info_package |
| 批次号规则 | RIP-YYYYMMDDHHMMSS-abcdef |
| 分析日期 | 2026-06-10 |
| 分析版本 | V1.0 |
---
## 一、需求背景
体外诊断试剂注册申报资料中第1章监管信息包含监管信息目录、申请表、产品列表、申报前沟通说明、符合标准清单、真实性声明和符合性声明等材料。注册人员通常需要根据产品说明书、企业信息和法规要求手工整理这些文件容易出现产品名称、包装规格、组成成分、预期用途等字段重复录入、漏填、格式不一致和待补信息不醒目的问题。
本需求新增独立工作流:用户上传或选择一个产品说明书后,系统以既有 `第1章 监管信息` 样例文件作为模板抽取说明书中的产品关键信息生成一套类似样例目录的第1章监管信息材料包。生成结果以 zip 压缩包作为主下载入口,同时保留单文件辅助下载。
该工作流可以复用现有自动填表工作流中已拆分出的字段抽取、LLM 调用、Word 写入、导出下载、批次事件和通知能力,但不并入 `application_form_fill`,而是作为独立工作流建设。
---
## 二、需求范围
### 2.1 本期范围
| 序号 | 范围项 | 说明 |
| --- | --- | --- |
| 1 | 独立工作流 | 新增 `regulatory_info_package`,不复用 `application_form_fill` 的 workflow_type |
| 2 | 单说明书输入 | 本期只支持一个产品说明书作为主输入 |
| 3 | 模板复用 | 以 `docs/0.原始材料/第1章 监管信息` 下的样例文件作为生成模板 |
| 4 | 固定输出文件 | 固定生成 7 个第1章监管信息文件 |
| 5 | 代码抽取与 LLM 抽取并行 | 规则/代码抽取与 LLM 结构化抽取并行处理,合并后写入模板 |
| 6 | 尽量多填 | 对说明书中可识别的产品名称、包装规格、预期用途、组成成分、储存条件、适用仪器、样本类型、检测靶标等字段尽量填入 |
| 7 | 缺失项标记 | 系统新填入的缺失项使用 `/`,并设置黄色底色提醒负责人补充 |
| 8 | LLM-only 标记 | 代码抽取未取到但 LLM 抽取到的字段,也需要在输出文件中高亮提示人工复核 |
| 9 | 模板字段化 | 优先将样例模板整理为 Agent/代码可识别字段模板,使用内容控件 Tag 或稳定占位符,代码只填内容不手改格式 |
| 10 | doc 能力增强 | `.doc` 文档按能力驱动处理:有原生能力时优先原生写入,无原生能力时明确记录并允许 `.docx` 兜底,不静默输出未改写文件 |
| 11 | zip 主输出 | 生成 `第1章 监管信息(预生成版).zip` 作为主下载入口,单文件作为辅助下载 |
| 12 | 对话唤起提示 | 在对话框底部增加本工作流的唤起提示词 |
| 13 | LLM 意图判断 | 触发判断不能只依赖固定关键词,需要引入 LLM 判断用户是否要生成第1章监管信息材料包 |
### 2.2 非本期范围
| 序号 | 范围项 | 说明 |
| --- | --- | --- |
| 1 | 多资料综合生成 | 本期不从产品技术要求、检验报告、企业证照等多文件综合生成 |
| 2 | 人工在线编辑 | 本期只生成文件并标记待确认项,不提供网页内字段编辑 |
| 3 | 自动保证法规最终准确 | 标准清单、分类编码、管理类别等无法从说明书确认的信息仍需负责人确认 |
| 4 | 自动提交监管系统 | 本期只生成申报材料包,不对接外部申报平台 |
| 5 | 版式人工校订替代 | 系统尽量保持模板版式,但最终提交前仍需人工核对 |
---
## 三、输入与触发
### 3.1 输入文件规则
| 场景 | 处理规则 |
| --- | --- |
| 用户上传一个 `.docx` 说明书 | 直接作为本次输入 |
| 用户上传多个文件 | 优先选择文件名包含“说明书”的 `.docx` |
| 多个说明书候选 | 工作流进入待确认状态,提示用户选择 |
| 未找到说明书 | 提示用户上传产品说明书 |
| 非 `.docx` 说明书 | 本期可提示格式不支持,后续扩展 `.doc`、PDF 或 OCR |
### 3.2 对话触发规则
固定提示词需要支持:
| 触发表达 | 触发结果 |
| --- | --- |
| 根据说明书生成第1章监管信息 | 启动第1章监管信息材料包生成 |
| 生成监管信息材料包 | 启动第1章监管信息材料包生成 |
| 从说明书生成第1章材料 | 启动第1章监管信息材料包生成 |
除固定表达外,系统需要引入 LLM 意图判断。当用户自然语言表达包含“根据说明书”“第1章”“监管信息”“材料包”“申请表/产品列表/声明”等意图组合时LLM 可判断为 `regulatory_info_package`。规则命中优先,规则未命中时再进入 LLM 路由,避免只靠固定模板。
### 3.3 对话框底部唤起提示
对话框底部快捷提示词新增:
```text
根据说明书生成第1章监管信息
```
后续可追加:
```text
生成监管信息材料包
从说明书生成第1章材料
```
---
## 四、输出文件范围
本期固定生成与样例目录一致的 7 个文件:
| 序号 | 输出文件 | 模板来源 | 生成规则 |
| --- | --- | --- | --- |
| 1 | CH1.2 监管信息目录.docx | 样例 `CH1.2 监管信息目录.docx` | 替换产品名称,目录结构和页码沿用样例 |
| 2 | CH1.4 申请表.docx | 样例 `CH1.4 申请表.docx` | 尽量填入说明书字段,未知项填 `/` 并黄底 |
| 3 | CH1.5 产品列表.docx | 样例 `CH1.5 产品列表.docx` | 按样例表头重建产品列表,货号留空并黄底 |
| 4 | CH1.9 产品申报前沟通的说明.doc | 样例 `CH1.9 产品申报前沟通的说明.doc` | `.doc` 应支持与 `.docx` 等价替换能力 |
| 5 | CH1.11.1 符合标准的清单.docx | 样例 `CH1.11.1 符合标准的清单.docx` | 从说明书和 RAG/法规知识库提取或推荐标准,非明确项需高亮待确认 |
| 6 | CH1.11.5 真实性声明.docx | 样例 `CH1.11.5 真实性声明.docx` | 保留样例正文结构,替换产品名称,公司名位置黄底 `/` |
| 7 | CH1.11.6 符合性声明.docx | 样例 `CH1.11.6 符合性声明.docx` | 保留样例正文结构,替换产品名称,公司名位置黄底 `/` |
### 4.1 下载形态
| 输出类型 | 要求 |
| --- | --- |
| zip 主入口 | 生成 `第1章 监管信息(预生成版).zip`,只包含成功或兜底成功的文件 |
| 单文件下载 | 每个生成文件均可作为辅助下载项展示 |
| 追溯清单 | 建议生成 JSON/Excel记录字段来源、抽取方式、高亮原因和待确认项 |
---
## 五、字段抽取与填写规则
### 5.1 抽取字段范围
系统应从说明书中尽量抽取以下字段:
| 字段 | 示例来源 |
| --- | --- |
| 产品名称 | `【产品名称】` |
| 包装规格 | `【包装规格】` |
| 预期用途 | `【预期用途】` |
| 检测原理/方法原理 | `【检测原理】` |
| 主要组成成分 | `【主要组成成分】` 及其下方表格 |
| 储存条件及有效期 | `【储存条件及有效期】` |
| 样本类型 | `【样本要求】` 中的适用样本类型 |
| 检测靶标 | 预期用途或检测原理中的基因、病原体、抗原、抗体等 |
| 适用仪器 | `【适用仪器】` |
| 检验方法 | `【检验方法】` |
| 生产日期和使用期限描述 | 储存条件章节 |
字段抽取采用规则/代码抽取与 LLM 结构化抽取并行模式:
```text
读取说明书
-> 规则/代码抽取
-> LLM 结构化抽取
-> 字段合并
-> 标记字段来源和置信度
-> 写入模板
```
### 5.2 合并与高亮规则
| 场景 | 处理规则 |
| --- | --- |
| 代码抽取和 LLM 都命中且结果一致 | 正常写入,不强制高亮 |
| 代码抽取和 LLM 都命中但结果不一致 | 优先按规则配置选择,写入值高亮并进入追溯清单 |
| 代码抽取未命中LLM 命中 | 写入 LLM 值,并高亮提示人工复核 |
| 代码抽取命中LLM 未命中 | 正常写入,追溯记录代码抽取来源 |
| 两者均未命中 | 写入 `/` 并设置黄色底色 |
| 企业信息缺失 | 写入 `/` 并设置黄色底色 |
高亮含义:
| 高亮类型 | 视觉要求 | 含义 |
| --- | --- | --- |
| 缺失项高亮 | 黄色底色 | 说明书无法提供,负责人需填写 |
| LLM-only 高亮 | 黄色底色,可在追溯清单标记 `llm_only` | 代码未抽到,仅 LLM 推断,需要复核 |
| 冲突高亮 | 黄色底色,可配合红色字体 | 规则结果与 LLM 结果不一致 |
仅标记系统新填入的缺失项或需复核项。样例模板中原本存在的 `/` 不统一高亮,避免整份文件过度标记。
---
## 六、各文件生成规则
### 6.1 CH1.2 监管信息目录
| 项目 | 规则 |
| --- | --- |
| 产品名称 | 替换为说明书抽取的产品名称 |
| 目录条目 | 沿用样例目录结构 |
| 适用情况 | 沿用样例 |
| 资料名称 | 沿用样例 |
| 页码 | 沿用样例页码 |
### 6.2 CH1.4 申请表
| 字段类型 | 规则 |
| --- | --- |
| 产品名称 | 从说明书抽取 |
| 包装规格 | 从说明书抽取 |
| 主要组成成分 | 优先使用说明书组成成分摘要或附件提示 |
| 预期用途 | 从说明书抽取 |
| 产品储存条件及有效期 | 从说明书抽取 |
| 方法原理 | 从说明书检测原理抽取 |
| 产品类别 | 缺失,填 `/` 并黄底 |
| 分类编码 | 缺失,填 `/` 并黄底 |
| 临床评价路径 | 缺失,填 `/` 并黄底 |
| 申请人信息 | 缺失,填 `/` 并黄底 |
| 联系人、法定代表人、邮箱、组织机构代码 | 缺失,填 `/` 并黄底 |
| 生产地址 | 缺失,填 `/` 并黄底 |
管理类别、分类编码、临床评价路径、UDI、国家标准品/强制标准等不得根据经验自动下结论,全部按待确认处理。
### 6.3 CH1.5 产品列表
产品列表需要转成样例表头:
| 包装规格 | 货号 | 组成 | 组分 | 主要组成成分 | 规格/数量 |
| --- | --- | --- | --- | --- | --- |
生成规则:
| 字段 | 规则 |
| --- | --- |
| 包装规格 | 从说明书组成成分表的规格列或包装规格章节抽取 |
| 货号 | 说明书未提供,填 `/` 并黄底 |
| 组成 | 根据组分名称推断为反应液、质控品、处理液、增强剂等;无法判断则填 `/` 并黄底 |
| 组分 | 使用说明书表格中的组分名称 |
| 主要组成成分 | 使用说明书表格中的主要组成成分 |
| 规格/数量 | 使用说明书表格中的对应规格数量 |
目标产品说明书中存在规格A大包装、规格A分管包装、规格B大管包装等多个组成表系统应尽量展开为多行产品列表。
### 6.4 CH1.9 产品申报前沟通的说明
`CH1.9` 当前为 `.doc` 格式。本工作流要求 `.doc` 文档具备与 `.docx` 等价的原始功能,即模板复制、文本定位、字段替换、高亮标记、导出和打包均应支持 `.doc`
实现上不应只把转换作为唯一方案。可选技术路径包括:
| 路径 | 说明 |
| --- | --- |
| 原生 `.doc` 处理 | 优先探索可直接读取和写入 `.doc` 的库、COM 或二进制文档处理能力 |
| Office/COM 自动化 | Windows 环境下通过 Word COM 直接打开 `.doc` 并原格式写入保存 |
| LibreOffice UNO/API | 通过 LibreOffice API 直接处理旧版 Word而不只作为离线预转换 |
| 转换兜底 | 当原生处理不可用时,可作为兜底手段,但不能作为需求定义中的唯一能力 |
如运行环境不具备 `.doc` 写入能力,工作流应明确失败原因或降级提示,不应静默输出未改写文件。
### 6.5 CH1.11.1 符合标准的清单
生成规则:
| 来源 | 处理方式 |
| --- | --- |
| 说明书明确出现的标准号 | 可直接写入,并记录来源片段 |
| RAG/法规知识库命中的候选标准 | 可作为候选写入或追溯提示,但需高亮待确认 |
| 样例中的标准清单 | 不可无条件沿用 |
| 无法确认的标准 | 填 `/` 并黄底 |
法规材料目录中存在 `医疗器械注册申报资料和批准证明文件格式要求(体外诊断试剂).doc``体外诊断试剂注册申报资料要求及说明.doc``体外诊断试剂安全和性能基本原则清单.doc` 等材料。其中安全和性能基本原则清单属于第3章非临床资料不直接等同于 `CH1.11.1 符合标准的清单`。系统应优先查询已上传 RAG/法规知识库来确认标准清单要求;未命中时不得强行套用样例标准。
### 6.6 CH1.11.5 真实性声明
| 项目 | 规则 |
| --- | --- |
| 正文结构 | 保留样例结构 |
| 产品名称 | 替换为说明书抽取的产品名称 |
| 公司名/申请人 | 填 `/` 并黄底 |
| 日期 | 使用当天日期 |
| 材料列表 | 沿用样例材料列表 |
### 6.7 CH1.11.6 符合性声明
| 项目 | 规则 |
| --- | --- |
| 正文结构 | 保留样例结构 |
| 产品名称 | 替换为说明书抽取的产品名称 |
| 公司名/申请人 | 填 `/` 并黄底 |
| 日期 | 使用当天日期 |
---
## 七、工作流设计
### 7.1 主流程
```text
用户上传或选择产品说明书
-> 用户触发“根据说明书生成第1章监管信息”
-> 系统通过规则和 LLM 判断工作流意图
-> 创建 regulatory_info_package 批次
-> 校验输入说明书
-> 复制第1章监管信息样例模板到批次目录
-> 抽取说明书文本、段落和表格
-> 规则/代码抽取字段
-> LLM 结构化抽取字段
-> 合并字段并识别缺失、LLM-only 和冲突项
-> 生成 7 个目标文件
-> 对缺失项、LLM-only 项和冲突项进行高亮
-> 生成追溯清单
-> 打包第1章监管信息 zip
-> 写入导出记录
-> 对话框展示 zip 主下载入口、单文件下载和待确认摘要
```
### 7.2 节点建议
| 节点编码 | 节点名称 | 成功条件 |
| --- | --- | --- |
| prepare | 准备资料 | 找到唯一说明书输入 |
| template_copy | 复制模板 | 7 个样例模板复制到批次目录 |
| text_extract | 抽取说明书 | 提取说明书段落和表格 |
| field_extract | 抽取字段 | 规则和 LLM 抽取结果均留底 |
| field_merge | 合并字段 | 输出最终字段、缺失项、LLM-only 项和冲突项 |
| generate_docs | 生成材料 | 7 个文件生成完成 |
| highlight_review_items | 标记待确认 | 缺失项、LLM-only、冲突项完成高亮 |
| trace_export | 追溯清单 | 生成 JSON/Excel 追溯清单 |
| zip_export | 打包下载 | 生成 `第1章 监管信息(预生成版).zip` |
| completed | 完成 | 更新批次状态并返回下载摘要 |
### 7.3 状态建议
| 状态 | 含义 |
| --- | --- |
| pending | 已创建,等待执行 |
| running | 执行中 |
| waiting_user | 多个说明书或缺少说明书,等待用户确认 |
| success | zip 和必要单文件生成成功 |
| partial_success | zip 已生成,但部分 `.doc`、追溯清单或高亮处理失败 |
| failed | 关键文件均未生成 |
---
## 八、数据与产物
### 8.1 批次数据
建议新增独立批次模型或等价数据结构,记录:
| 字段 | 说明 |
| --- | --- |
| batch_no | RIP 批次号 |
| workflow_type | regulatory_info_package |
| conversation | 所属对话 |
| user | 发起用户 |
| trigger_message | 触发消息 |
| source_instruction_file | 输入说明书 |
| product_name | 抽取到的产品名称 |
| status | 批次状态 |
| work_dir | 批次工作目录 |
| missing_fields | 缺失字段清单 |
| llm_only_fields | 仅 LLM 命中的字段 |
| conflict_fields | 冲突字段 |
| risk_notes | `.doc` 处理、标准清单待确认等风险提示 |
### 8.2 追溯清单
追溯清单至少记录:
| 字段 | 说明 |
| --- | --- |
| target_file | 目标文件 |
| target_field | 目标字段 |
| final_value | 写入值 |
| extraction_source | rule、llm、missing、rag_candidate |
| evidence | 来源片段 |
| highlight_reason | missing、llm_only、conflict、rag_candidate |
| needs_review | 是否需要负责人确认 |
---
## 九、界面与交互
### 9.1 对话回复
工作流完成后,对话框展示:
| 信息 | 说明 |
| --- | --- |
| 批次号 | RIP 批次号 |
| 产品名称 | 抽取到的产品名称 |
| 主下载 | `第1章 监管信息(预生成版).zip` |
| 单文件下载 | 7 个文件列表 |
| 待确认摘要 | 缺失字段数、LLM-only 字段数、冲突字段数 |
| `.doc` 状态 | CH1.9 是否成功完成 `.doc` 写入 |
| 标准清单提示 | 标准来源和待确认说明 |
### 9.2 工作流卡片
前端需新增 `regulatory_info_package` 工作流卡片,展示节点状态和导出结果。对话框底部新增快捷唤起提示词:
```text
根据说明书生成第1章监管信息
```
---
## 十、异常与降级
| 异常场景 | 处理方式 |
| --- | --- |
| 未上传说明书 | 提示用户上传产品说明书 |
| 多个说明书候选 | 进入 waiting_user提示选择 |
| 产品名称未抽到 | 目标文件产品名位置填 `/` 并黄底 |
| 企业信息缺失 | 相关位置填 `/` 并黄底 |
| LLM 调用失败 | 使用规则抽取结果继续生成,并记录风险提示 |
| 规则抽取失败 | 使用 LLM 结果继续生成LLM-only 字段高亮 |
| RAG/法规知识库不可用 | 标准清单不自动套用样例,写入 `/` 并黄底 |
| `.doc` 原生处理失败 | 批次标记 partial_success 或 failed明确提示 CH1.9 处理失败原因 |
| zip 打包失败 | 保留单文件下载,并提示压缩包生成失败 |
---
## 十一、验收标准
| 序号 | 验收项 | 标准 |
| --- | --- | --- |
| 1 | 触发识别 | 用户输入“根据说明书生成第1章监管信息”可启动 `regulatory_info_package` |
| 2 | LLM 路由 | 非固定话术但语义明确时,可由 LLM 判断进入本工作流 |
| 3 | 输入选择 | 单说明书可直接执行,多说明书进入待确认 |
| 4 | 输出文件 | 生成 7 个与样例同名或同语义的第1章文件 |
| 5 | zip 下载 | 生成 `第1章 监管信息(预生成版).zip` 作为主下载入口 |
| 6 | 单文件下载 | 7 个生成文件均可单独下载 |
| 7 | 产品名称替换 | 目录、申请表、声明类文件中的产品名称替换为说明书产品名称 |
| 8 | 产品列表 | CH1.5 使用样例表头展开说明书组成成分,货号填 `/` 并黄底 |
| 9 | 缺失项高亮 | 系统新填入的 `/` 均有黄色底色 |
| 10 | LLM-only 高亮 | 代码未抽到但 LLM 抽到的字段在文件中高亮 |
| 11 | 标准清单 | 不无条件沿用样例标准;无法确认时填 `/` 并黄底 |
| 12 | 日期 | 声明类文件日期使用当天日期 |
| 13 | `.doc` 支持 | CH1.9 `.doc` 具备与 `.docx` 等价的处理能力,失败时明确提示 |
| 14 | 追溯清单 | 输出字段来源、抽取方式和高亮原因 |
| 15 | 权限隔离 | 用户只能访问自己对话下的批次和导出文件 |
---
## 十二、已确认结论
| 编号 | 结论 |
| --- | --- |
| D1 | 输出范围固定为样例第1章监管信息目录下的 7 个文件 |
| D2 | 样例文件作为模板使用,不只是效果参考 |
| D3 | 企业信息、申请人信息缺失时不沿用样例公司,填 `/` 并黄底 |
| D4 | 管理类别、分类编码、临床评价路径等无法从说明书确认的信息填 `/` 并黄底 |
| D5 | 产品列表货号留空,填 `/` 并黄底 |
| D6 | 标准清单不得无条件沿用样例,优先从说明书和 RAG/法规知识库确认 |
| D7 | 声明日期使用当天日期 |
| D8 | 新建独立工作流,可复用原自动填表工作流拆出的 skill/service |
| D9 | 需求分析文档新增为 `docs/1.需求分析/5.第1章监管信息材料包生成.md` |
| D10 | zip 作为主入口,单文件作为辅助下载 |
| D11 | 对话框底部增加工作流唤起提示词 |
| D12 | 模板优先字段化,使用内容控件 Tag 或稳定占位符服务 Agent/代码填充,行标签定位仅作为兜底 |
| D13 | `.doc` 要按能力驱动实现与 `.docx` 等价能力;原生能力不可用时允许 `.docx` 兜底并明确提示 |
| D14 | 触发判断需要引入 LLM不只依赖固定关键词 |

View File

@@ -0,0 +1,597 @@
# 自动汇总文件夹文件目录与页数流程功能设计
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 需求分析文档 | docs/1.需求分析/1.自动汇总.md |
| 功能名称 | 自动汇总文件夹文件目录与页数 |
| 所属模块 | 审核智能体 review_agent |
| 设计日期 | 2026-06-05 |
| 设计版本 | V1.0 |
---
## 一、设计目标
本功能面向试剂盒 NMPA 注册申报资料审核场景,支持用户在 AI 对话页上传压缩包或多个文件由后台异步执行文件汇总工作流自动完成解压、文件清单扫描、页数统计、产品名识别、Markdown 报告生成和 Excel 导出。
前台 AI 对话页需要展示一个工作流卡片,实时呈现“上传中、解压中、扫描中、解析中、识别中、输出中、完成/失败”等节点状态工作流完成后AI 对话框展示 Markdown 简表,并提供 Markdown 报告和 Excel 明细下载链接。上传文件、批次、节点状态、文件明细和导出结果均需要与当前对话绑定,一个对话对应一套文件,不能跨对话串用。
---
## 二、总体架构
### 2.1 架构原则
| 原则 | 说明 |
| --- | --- |
| 对话绑定 | 上传文件、处理批次、结果文件均绑定当前 Conversation |
| 按需加载 | 将文件处理流程拆分为多个可单独执行的 Skill按工作流节点调用 |
| 后台异步 | 用户提交后后台执行工作流,前台通过 SSE 接收状态事件 |
| 失败隔离 | 解压失败导致批次失败;单文件解析失败最多重试 3 次后记录异常并继续 |
| 可迁移 MCP | Demo 阶段使用项目内 Skill 注册与调用,后续可迁移为 MCP Tool |
| 可追溯 | 每个节点状态、文件统计结果、导出文件均持久化入库 |
### 2.2 逻辑架构
```mermaid
flowchart TD
A["AI 对话页"] --> B["上传接收接口"]
B --> C["工作流任务表"]
C --> D["后台工作流执行器"]
D --> E["Skill 注册表"]
E --> F1["上传接收 Skill"]
E --> F2["压缩包解压 Skill"]
E --> F3["文件清单扫描 Skill"]
E --> F4["文档页数统计 Skill"]
E --> F5["产品信息识别 Skill"]
E --> F6["汇总报告生成 Skill"]
E --> F7["Excel 导出 Skill"]
D --> G["数据库存档"]
D --> H["导出文件存储"]
D --> I["SSE 状态事件"]
I --> A
```
### 2.3 技术选型
| 设计项 | Demo 方案 | 后续演进 |
| --- | --- | --- |
| 工作流编排 | 项目内 LangGraph 风格节点图执行器 | 接入 LangGraph |
| Skill 形态 | Python 类或函数注册表,按节点名称动态调用 | 封装为 MCP Tool |
| 后台任务 | Django 后台线程 + 数据库状态 | Celery/RQ + Redis |
| 实时更新 | 沿用现有 SSE 流式能力,新增 workflow 事件 | 独立任务事件通道 |
| 文件存储 | 本地 media 目录 | 对象存储或加密文件服务 |
| Markdown 渲染 | 前端引入安全 Markdown 渲染 | 统一富文本渲染组件 |
---
## 三、工作流设计
### 3.1 节点图
```mermaid
flowchart LR
N1["上传中"] --> N2{"是否压缩包"}
N2 -->|"是"| N3["解压中"]
N2 -->|"否"| N4["扫描中"]
N3 --> N4
N4 --> N5["解析页数中"]
N5 --> N6["识别产品名中"]
N6 --> N7["输出中"]
N7 --> N8["完成"]
N3 -->|"解压失败"| F["失败"]
N7 -->|"导出失败"| F
```
### 3.2 节点定义
| 节点编码 | 节点名称 | 触发 Skill | 成功条件 | 失败处理 |
| --- | --- | --- | --- | --- |
| upload | 上传中 | 上传接收 Skill | 原始文件保存成功,批次创建成功 | 批次失败 |
| extract | 解压中 | 压缩包解压 Skill | zip/rar/7z 等压缩包解压成功 | 批次失败 |
| inventory | 扫描中 | 文件清单扫描 Skill | 生成文件清单 | 批次失败或空文件提示 |
| page_count | 解析页数中 | 文档页数统计 Skill | 支持类型完成页数统计或异常记录 | 单文件失败不阻断 |
| product_detect | 识别产品名中 | 产品信息识别 Skill | 识别到产品名或返回空 | 不阻断 |
| report | 输出中 | 汇总报告生成 Skill | Markdown 报告与对话简表生成成功 | 批次失败 |
| export | 输出中 | Excel 导出 Skill | Excel 明细生成成功 | 批次失败或记录导出异常 |
| completed | 完成 | 工作流执行器 | 所有必需产物完成 | 写入完成状态 |
### 3.3 状态机
| 状态 | 含义 |
| --- | --- |
| pending | 已创建,等待执行 |
| running | 执行中 |
| retrying | 单文件解析失败,正在重试 |
| success | 节点执行成功 |
| failed | 节点或批次失败 |
| skipped | 当前节点不需要执行,例如非压缩包跳过解压 |
---
## 四、Skill 设计
### 4.1 Skill 注册与调用
Demo 阶段在项目内定义 Skill 注册表,工作流执行器根据节点编码按需加载并执行对应 Skill。
```text
WorkflowExecutor
-> 根据当前节点读取 Skill 名称
-> 从 SkillRegistry 获取 Skill
-> 执行 skill.run(context)
-> 写入节点状态与结果
-> 发送 SSE 状态事件
-> 进入下一节点
```
后续 MCP 化时,每个 Skill 可映射为独立 MCP Tool输入输出保持稳定 JSON 契约。
### 4.2 上传接收 Skill
| 项目 | 说明 |
| --- | --- |
| 中文名称 | 上传接收 Skill |
| 职责 | 接收对话页上传的压缩包或多个文件,保存原始文件,创建上传批次 |
| 输入 | conversation_id、user_id、uploaded_files |
| 输出 | batch_id、upload_file_ids、upload_type、original_storage_paths |
| 数据写入 | FileSummaryBatch、FileAttachment、FileSummaryBatchAttachment |
| 关键规则 | 文件必须绑定当前 Conversation同一对话只使用本对话上传的文件 |
### 4.3 压缩包解压 Skill
| 项目 | 说明 |
| --- | --- |
| 中文名称 | 压缩包解压 Skill |
| 职责 | 识别并解压 zip、rar、7z 等常见压缩包,保留目录结构 |
| 输入 | batch_id、source_file_path |
| 输出 | extract_root、extracted_file_count |
| 数据写入 | WorkflowNodeRun、批次工作目录 |
| 关键规则 | 防止路径穿越;解压目录必须限定在批次工作目录内;解压失败批次失败 |
### 4.4 文件清单扫描 Skill
| 项目 | 说明 |
| --- | --- |
| 中文名称 | 文件清单扫描 Skill |
| 职责 | 遍历解压目录或散装文件目录,生成文件清单 |
| 输入 | batch_id、scan_root |
| 输出 | inventory_items |
| 数据写入 | FileSummaryItem |
| 关键规则 | 保留目录层级;散装文件归入同一批次根目录;隐藏文件和空文件可标记跳过 |
### 4.5 文档页数统计 Skill
| 项目 | 说明 |
| --- | --- |
| 中文名称 | 文档页数统计 Skill |
| 职责 | 对支持类型统计页数或数量 |
| 输入 | batch_id、FileSummaryItem 列表 |
| 输出 | page_count、statistics_status、error_message |
| 数据写入 | FileSummaryItem |
| 关键规则 | 支持 pdf、doc、docx、xls、xlsx、ppt、pptx单文件失败最多重试 3 次,仍失败则记录异常并继续 |
页数统计口径:
| 文件类型 | 统计口径 |
| --- | --- |
| pdf | PDF 页面数量 |
| doc/docx | 优先转 PDF 后统计页面数量 |
| xls/xlsx | Demo 阶段按工作表数量统计 |
| ppt/pptx | 按幻灯片数量统计 |
### 4.6 产品信息识别 Skill
| 项目 | 说明 |
| --- | --- |
| 中文名称 | 产品信息识别 Skill |
| 职责 | 尝试识别产品名称,并用于更新对话标题 |
| 输入 | batch_id、文件名、目录名、可读取文本片段 |
| 输出 | product_name、confidence、evidence |
| 数据写入 | FileSummaryBatch.product_name、Conversation.title |
| 关键规则 | 优先从文件名和目录名识别;其次读取文档首页或关键文本;识别失败不阻断流程 |
会话标题规则:
| 场景 | 标题处理 |
| --- | --- |
| 识别到产品名 | 更新为“产品名-文件汇总” |
| 未识别产品名 | 保持原对话标题 |
| 用户已手动命名 | 可保留用户标题,产品名写入批次信息 |
### 4.7 汇总报告生成 Skill
| 项目 | 说明 |
| --- | --- |
| 中文名称 | 汇总报告生成 Skill |
| 职责 | 生成完整 Markdown 报告和对话框展示简表 |
| 输入 | batch_id、统计摘要、文件明细、异常清单 |
| 输出 | markdown_report_path、assistant_markdown_summary |
| 数据写入 | ExportedSummaryFile、Message |
| 关键规则 | Markdown 简表需要适合前端对话框渲染;完整报告包含全部文件明细 |
### 4.8 Excel 导出 Skill
| 项目 | 说明 |
| --- | --- |
| 中文名称 | Excel 导出 Skill |
| 职责 | 生成 Excel 汇总文件 |
| 输入 | batch_id、统计摘要、文件明细 |
| 输出 | excel_path、download_url |
| 数据写入 | ExportedSummaryFile |
| 关键规则 | 至少包含“汇总信息”“文件明细”两个 Sheet |
---
## 五、数据模型设计
### 5.1 FileSummaryBatch
文件汇总批次,表示一次对话内的文件汇总任务。
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| id | BigAutoField | 主键 |
| conversation | ForeignKey(Conversation) | 绑定对话 |
| user | ForeignKey(User) | 上传用户 |
| batch_no | CharField | 批次编号 |
| product_name | CharField | 识别出的产品名,可为空 |
| upload_type | CharField | archive、multi_file |
| status | CharField | pending、running、success、failed |
| total_files | Integer | 文件总数 |
| supported_files | Integer | 支持统计的文件数 |
| success_files | Integer | 统计成功数 |
| failed_files | Integer | 统计失败数 |
| unsupported_files | Integer | 不支持文件数 |
| total_pages | Integer | 总页数 |
| work_dir | CharField | 批次工作目录 |
| error_message | TextField | 批次异常说明 |
| created_at | DateTimeField | 创建时间 |
| started_at | DateTimeField | 开始时间 |
| finished_at | DateTimeField | 完成时间 |
### 5.2 FileAttachment
上传原始文件记录。用户上传即存储为 `FileAttachment`,批次启动时再通过 `FileSummaryBatchAttachment` 固化本次使用的附件版本。
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| id | BigAutoField | 主键 |
| batch | ForeignKey(FileSummaryBatch) | 所属批次 |
| original_name | CharField | 原始文件名 |
| storage_path | CharField | 保存路径 |
| file_size | BigInteger | 文件大小 |
| content_type | CharField | MIME 类型 |
| created_at | DateTimeField | 上传时间 |
### 5.3 FileSummaryItem
文件明细记录。
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| id | BigAutoField | 主键 |
| batch | ForeignKey(FileSummaryBatch) | 所属批次 |
| file_index | Integer | 文件序号 |
| directory_level | CharField | 目录层级 |
| file_name | CharField | 文件名 |
| file_type | CharField | 文件类型 |
| relative_path | CharField | 相对路径 |
| storage_path | CharField | 实际处理路径 |
| page_count | Integer | 页数,可为空 |
| statistics_status | CharField | success、failed、unsupported、skipped |
| retry_count | Integer | 页数统计重试次数 |
| error_message | TextField | 异常说明 |
| created_at | DateTimeField | 创建时间 |
| updated_at | DateTimeField | 更新时间 |
### 5.4 WorkflowNodeRun
工作流节点运行记录。
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| id | BigAutoField | 主键 |
| batch | ForeignKey(FileSummaryBatch) | 所属批次 |
| node_code | CharField | 节点编码 |
| node_name | CharField | 节点名称 |
| status | CharField | pending、running、retrying、success、failed、skipped |
| progress | Integer | 进度百分比 |
| message | TextField | 节点提示 |
| started_at | DateTimeField | 开始时间 |
| finished_at | DateTimeField | 完成时间 |
### 5.5 ExportedSummaryFile
导出文件记录。
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| id | BigAutoField | 主键 |
| batch | ForeignKey(FileSummaryBatch) | 所属批次 |
| export_type | CharField | markdown、excel |
| file_name | CharField | 文件名 |
| storage_path | CharField | 保存路径 |
| download_url | CharField | 下载链接 |
| status | CharField | success、failed |
| error_message | TextField | 导出异常说明 |
| created_at | DateTimeField | 生成时间 |
---
## 六、后端接口设计
### 6.1 上传并启动工作流
| 项目 | 内容 |
| --- | --- |
| URL | POST /api/review-agent/conversations/{conversation_id}/file-summary/start/ |
| 认证 | 登录用户 |
| 请求类型 | multipart/form-data |
| 请求参数 | files[]、prompt |
| 响应 | batch_id、status、workflow_nodes |
处理逻辑:
```text
校验 conversation 属于当前用户
-> 保存上传文件
-> 创建 FileSummaryBatch
-> 创建 WorkflowNodeRun 初始节点
-> 启动后台线程执行工作流
-> 返回 batch_id 和初始节点状态
```
### 6.2 工作流 SSE 事件流
| 项目 | 内容 |
| --- | --- |
| URL | GET /api/review-agent/file-summary/{batch_id}/events/ |
| 认证 | 登录用户 |
| 响应类型 | text/event-stream |
事件类型:
| 事件 | 说明 |
| --- | --- |
| workflow_started | 工作流开始 |
| node_started | 节点开始 |
| node_progress | 节点进度更新 |
| node_retrying | 单文件解析重试 |
| node_completed | 节点完成 |
| node_failed | 节点失败 |
| workflow_completed | 工作流完成 |
| workflow_failed | 工作流失败 |
示例:
```json
{
"batch_id": 12,
"node_code": "page_count",
"node_name": "解析页数中",
"status": "retrying",
"progress": 42,
"message": "检测报告.pdf 解析失败,正在第 2 次重试"
}
```
### 6.3 查询批次状态
| 项目 | 内容 |
| --- | --- |
| URL | GET /api/review-agent/file-summary/{batch_id}/ |
| 认证 | 登录用户 |
| 响应 | 批次摘要、节点状态、文件简表、导出文件链接 |
用途:
| 场景 | 说明 |
| --- | --- |
| 页面刷新恢复 | 前端重新加载后恢复工作流卡片状态 |
| 历史记录查看 | 从会话历史进入后展示已完成汇总结果 |
### 6.4 下载导出文件
| 项目 | 内容 |
| --- | --- |
| URL | GET /api/review-agent/file-summary/exports/{export_id}/download/ |
| 认证 | 登录用户 |
| 响应 | 文件流 |
权限规则:
```text
export_id -> batch -> conversation -> user
必须等于当前登录用户,才允许下载。
```
---
## 七、前端设计
### 7.1 AI 对话页改造
现有 `templates/home.html``static/js/app.js` 需要增强:
| 改造点 | 说明 |
| --- | --- |
| 附件选择 | 在输入框旁增加文件上传按钮,支持压缩包和多个文件 |
| 工作流卡片 | 用户提交后在对话流中插入工作流状态卡片 |
| SSE 监听 | 监听后台节点事件,实时更新卡片节点状态 |
| Markdown 渲染 | AI 回复支持 Markdown 表格和下载链接渲染 |
| 状态恢复 | 页面刷新后查询批次状态,恢复工作流卡片 |
### 7.2 工作流卡片
卡片包含节点列表:
| 节点 | 前台展示文案 |
| --- | --- |
| upload | 上传中 |
| extract | 解压中 |
| inventory | 扫描中 |
| page_count | 解析页数中 |
| product_detect | 识别产品名中 |
| report/export | 输出中 |
| completed | 已完成 |
节点状态样式:
| 状态 | 展示 |
| --- | --- |
| pending | 灰色等待 |
| running | 高亮进行中 |
| retrying | 黄色重试中 |
| success | 绿色完成 |
| failed | 红色失败 |
| skipped | 灰色跳过 |
### 7.3 对话框结果展示
工作流完成后AI 对话框新增助手消息,内容为 Markdown
```markdown
已完成文件目录与页数汇总。
| 指标 | 数量 |
| --- | --- |
| 文件总数 | 24 |
| 统计成功 | 21 |
| 统计失败 | 2 |
| 不支持 | 1 |
| 总页数 | 386 |
| 序号 | 目录层级 | 文件名 | 类型 | 页数 | 状态 | 异常说明 |
| --- | --- | --- | --- | --- | --- | --- |
| 1 | 注册资料/说明书 | 说明书.docx | docx | 12 | 成功 | |
| 2 | 注册资料/检测报告 | 检测报告.pdf | pdf | 38 | 成功 | |
[下载 Markdown 报告](download-url)
[下载 Excel 明细](download-url)
```
---
## 八、后台服务设计
### 8.1 WorkflowExecutor
负责批次级工作流编排。
| 方法 | 说明 |
| --- | --- |
| start(batch_id) | 启动后台任务 |
| run(batch_id) | 串行执行节点图 |
| run_node(node_code, context) | 执行单个节点 |
| emit_event(batch_id, event_type, payload) | 写入并推送事件 |
| complete(batch_id) | 完成批次 |
| fail(batch_id, error) | 标记批次失败 |
### 8.2 SkillRegistry
负责 Skill 注册与按需加载。
| 方法 | 说明 |
| --- | --- |
| register(name, skill_cls) | 注册 Skill |
| get(name) | 获取 Skill |
| run(name, context) | 执行 Skill |
### 8.3 PageCountService
负责具体文件页数统计。
| 方法 | 说明 |
| --- | --- |
| count_pdf(path) | 统计 PDF 页面数 |
| count_word(path) | doc/docx 转 PDF 后统计页面数 |
| count_excel(path) | 统计工作表数量 |
| count_ppt(path) | 统计幻灯片数量 |
| count_with_retry(item, max_retry=3) | 单文件重试统计 |
### 8.4 ExportService
负责 Markdown 和 Excel 导出。
| 方法 | 说明 |
| --- | --- |
| build_markdown_report(batch) | 生成完整 Markdown 报告 |
| build_chat_summary(batch) | 生成对话简表 |
| build_excel(batch) | 生成 Excel 明细 |
| create_download_record(batch, path, type) | 创建下载记录 |
---
## 九、异常与重试设计
### 9.1 批次级失败
| 场景 | 处理 |
| --- | --- |
| 上传保存失败 | 批次不创建或标记失败 |
| 压缩包无法解压 | 批次失败,工作流终止 |
| 文件清单为空 | 批次失败,提示未检测到可处理文件 |
| 报告导出失败 | 批次失败或标记导出异常 |
### 9.2 文件级失败
| 场景 | 处理 |
| --- | --- |
| 单文件页数解析失败 | 最多重试 3 次 |
| 重试仍失败 | statistics_status=failed记录异常说明继续处理其他文件 |
| 不支持类型 | statistics_status=unsupported不重试 |
| 加密或损坏文件 | statistics_status=failed记录“文件加密或损坏” |
---
## 十、安全设计
| 设计点 | 说明 |
| --- | --- |
| 对话隔离 | 所有批次查询和下载必须校验 conversation.user |
| 防串文件 | 工作流只能读取当前 batch 通过 FileSummaryBatchAttachment 绑定的 FileAttachment |
| 解压安全 | 禁止压缩包内路径跳出批次工作目录 |
| 文件执行安全 | 不执行上传文件中的脚本、宏或外部链接 |
| 下载权限 | 下载接口必须验证当前用户拥有批次所属对话 |
| 存储隔离 | 按 user_id/conversation_id/batch_id 建立存储目录 |
---
## 十一、验收设计
| 序号 | 验收项 | 验收标准 |
| --- | --- | --- |
| 1 | 对话绑定 | A 对话上传的文件不会出现在 B 对话的汇总结果中 |
| 2 | 压缩包处理 | 支持 zip、rar、7z 常见压缩包解压并保留目录结构 |
| 3 | 多文件处理 | 支持一次上传多个散装文件并生成同一批次结果 |
| 4 | 工作流卡片 | 前台能实时展示上传中、解压中、扫描中、解析中、输出中、完成状态 |
| 5 | 解析重试 | 单文件解析失败最多重试 3 次,失败后记录异常并继续 |
| 6 | Markdown 展示 | 对话框能正确渲染 Markdown 表格和下载链接 |
| 7 | 导出下载 | Markdown 报告和 Excel 明细可通过对话框链接下载 |
| 8 | 数据存档 | 数据库保留批次、上传文件、节点状态、文件明细、导出文件记录 |
| 9 | 标题更新 | 识别到产品名后,可将会话标题更新为“产品名-文件汇总” |
---
## 十二、待确认事项
| 序号 | 问题 | 当前建议 | 状态 |
| --- | --- | --- | --- |
| 1 | 是否接入真实 LangGraph 依赖 | Demo 先按 LangGraph 节点图思想自实现轻量编排器 | 待确认 |
| 2 | rar/7z 解压依赖 | 可选 py7zr、rarfile、系统 7z 命令 | 待技术验证 |
| 3 | doc/docx 转 PDF 依赖 | 建议使用 LibreOffice headless | 待技术验证 |
| 4 | 用户手动命名对话时是否允许覆盖 | 建议不覆盖,仅写入产品名字段 | 待确认 |
| 5 | 后台任务是否需要取消能力 | Demo 可不做,正式版建议支持取消 | 待确认 |
---
## 十三、实施建议
1. 先补充数据模型和迁移,建立批次、文件明细、节点状态和导出文件表。
2. 增加上传并启动工作流接口,确保文件和当前对话强绑定。
3. 实现轻量 WorkflowExecutor 和 SkillRegistry先完成 zip、pdf、xlsx、pptx 的主链路。
4. 改造前端对话框,增加附件上传、工作流卡片和 Markdown 渲染。
5. 补齐 doc/docx、rar、7z 等依赖能力,再完善异常重试和下载权限测试。

View File

@@ -0,0 +1,730 @@
# NMPA 注册资料法规核查与整改闭环工作流功能设计
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 需求分析文档 | docs/1.需求分析/2.NMPA注册资料法规核查与整改闭环.md |
| 依赖功能设计 | docs/2.功能设计/1.自动汇总.md |
| 功能名称 | NMPA 注册资料法规核查与整改闭环 |
| 所属模块 | 审核智能体 review_agent |
| 设计日期 | 2026-06-06 |
| 设计版本 | V1.0 |
---
## 一、设计目标
本功能在“自动汇总文件夹文件目录与页数流程”基础上扩展,不重复实现上传、解压、文件扫描、页数统计、基础导出和 SSE 推送能力。法规核查工作流复用已有 `FileSummaryBatch``FileSummaryItem` 作为资料清单输入新增法规规则库、RAG 法规依据索引、法规核查批次、风险问题、过程产物、飞书通知和整改复核能力。
工作流支持两种启动方式:用户可以在已有文件汇总批次完成后发起法规核查;也可以直接在上传资料后发起法规核查,系统内部先执行自动汇总,再串联执行法规核查。若同一对话已存在最近一次成功的文件汇总批次,默认复用该批次。
前端需要新增独立的法规核查工作流卡片。一个对话内可能同时存在“文件汇总”和“法规核查”等多个工作流卡片,卡片区域采用类似轮播图的切换方式展示当前活跃卡片和历史卡片。底层 SSE 事件机制复用现有 `workflow` 事件,通过 `workflow_type` 区分 `file_summary``regulatory_review`
---
## 二、与自动汇总功能的关系
### 2.1 复用边界
| 能力 | 处理方式 | 说明 |
| --- | --- | --- |
| 上传接收 | 复用 | 沿用 `FileAttachment``FileSummaryBatchAttachment` 和上传接收接口 |
| 压缩包解压 | 复用 | 沿用自动汇总的解压 Skill 和工作目录规则 |
| 文件清单扫描 | 复用 | 以 `FileSummaryItem` 作为法规核查文件清单 |
| 页数统计 | 复用 | 法规核查直接读取页数和解析状态 |
| 基础节点状态 | 复用 | 沿用 `WorkflowNodeRun` 事件模型,新增 workflow_type |
| Markdown/Excel 下载 | 部分复用 | 最终报告进入 `ExportedSummaryFile`,过程产物进入 `RegulatoryArtifact` |
| 产品名识别 | 不扩展 | 原 `产品信息识别 Skill` 继续只服务自动汇总标题识别 |
### 2.2 新增边界
| 能力 | 说明 |
| --- | --- |
| 法规适用信息抽取 | 新增 `RegulatoryInfoExtract Skill`,抽取注册类型、临床评价路径、产品关键信息 |
| 适用条件确认 | 通过 AI 对话选择框让用户确认或自由补充 |
| 规则文件与 RAG | 结构化规则文件负责判断RAG 负责法规依据引用和解释 |
| 法规核查批次 | 新增 `RegulatoryReviewBatch`,关联 `FileSummaryBatch` |
| 风险问题与整改状态 | 新增 `RegulatoryIssue`,记录问题、风险、证据、责任人、状态 |
| 过程产物留底 | 新增 `RegulatoryArtifact`,保存条件确认、核查矩阵、风险清单、复核记录等 |
| 飞书通知 | 新增 `FeishuNotifier` 抽象接口Demo 实现接飞书 CLI |
---
## 三、总体架构
### 3.1 架构原则
| 原则 | 说明 |
| --- | --- |
| 依赖汇总批次 | 法规核查必须绑定一个 `FileSummaryBatch`,不能跨对话读取文件 |
| 工作流独立 | 法规核查拥有独立卡片、批次和节点,但事件通道可复用 |
| 规则优先 | 合规判断以结构化规则文件为准RAG 只做法规依据检索和解释增强 |
| 人工确认 | 适用条件缺失时停在待确认,复核通过关闭前需要人工确认 |
| 过程留底 | 所有关键过程文档都要留底,便于复核和 Demo 展示 |
| 通知可替换 | 飞书发送通过接口抽象Demo 接 CLI后续可替换为 Webhook/API |
### 3.2 逻辑架构
```mermaid
flowchart TD
A["AI 对话页"] --> B["工作流卡片轮播区"]
A --> C["法规核查启动接口"]
C --> D{"是否已有成功 FileSummaryBatch"}
D -->|"有"| E["复用最近成功汇总批次"]
D -->|"无"| F["串联执行自动汇总工作流"]
F --> E
E --> G["RegulatoryReviewBatch"]
G --> H["RegulatoryWorkflowExecutor"]
H --> I["SkillRegistry"]
I --> I1["RegulatoryInfoExtract Skill"]
I --> I2["TextExtract Skill"]
I --> I3["CompletenessCheck Skill"]
I --> I4["StructureCheck Skill"]
I --> I5["ConsistencyCheck Skill"]
I --> I6["RiskAssess Skill"]
I --> I7["RegulatoryReportExport Skill"]
H --> J["结构化规则文件"]
H --> K["本地法规 RAG 索引"]
H --> L["RegulatoryIssue"]
H --> M["RegulatoryArtifact"]
H --> N["FeishuNotifier CLI"]
H --> O["workflow SSE 事件"]
O --> B
```
### 3.3 技术选型
| 设计项 | Demo 方案 | 后续演进 |
| --- | --- | --- |
| 工作流编排 | 复用轻量 WorkflowExecutor 思路,新增 RegulatoryWorkflowExecutor | 接入 LangGraph 子图 |
| 事件机制 | 复用 `workflow` SSE新增 `workflow_type=regulatory_review` | 独立工作流事件中心 |
| 规则存储 | 项目内 JSON/YAML 规则文件 | 规则管理后台 + 数据库版本表 |
| 法规依据检索 | 本地 CMDE 文档构建 RAG 索引 | 法规资料定期更新和重建索引 |
| 文本抽取 | 新增统一 TextExtract Skill | 建立文档文本缓存和 OCR 能力 |
| 飞书通知 | `FeishuNotifier` 接飞书 CLI可直接测试发送 | 飞书开放平台 Webhook/API |
| 过程产物 | Markdown、Excel、JSON 留底 | 对象存储 + 证据快照管理 |
---
## 四、工作流设计
### 4.1 启动方式
| 场景 | 处理方式 |
| --- | --- |
| 已有成功文件汇总批次 | 默认复用当前对话最近一次成功 `FileSummaryBatch` |
| 无成功文件汇总批次 | 系统先串联执行自动汇总,再执行法规核查 |
| 用户修改适用条件后重核 | 创建新的 `RegulatoryReviewBatch`,保留旧批次记录 |
| 用户补充缺失文件复核 | 通过对话指令上传补充文件,合并到原问题上下文后复核 |
### 4.2 节点图
```mermaid
flowchart LR
N1["准备资料"] --> N2["识别信息"]
N2 --> N3{"适用条件是否完整"}
N3 -->|"否"| W["待用户确认"]
W --> N4["裁剪规则"]
N3 -->|"是"| N4
N4 --> N5["完整性核查"]
N5 --> N6["文本抽取"]
N6 --> N7["章节核查"]
N6 --> N8["一致性核查"]
N7 --> N9["风险分级"]
N8 --> N9
N9 --> N10["报告导出"]
N10 --> N11["飞书通知"]
N11 --> N12["待整改复核"]
N12 --> N13["完成"]
N4 -->|"规则加载失败"| R["RAG 辅助核查"]
R --> N9
```
### 4.3 主节点与子节点
法规核查卡片展示主节点,主节点可展开查看子节点。
| 主节点 | 子节点 | 说明 |
| --- | --- | --- |
| 准备资料 | 复用批次、检查文件清单、读取规则版本 | 绑定 `FileSummaryBatch` |
| 识别信息 | 产品信息抽取、适用条件识别 | 生成用户确认项 |
| 确认条件 | 对话选择框确认、自由补充 | 卡片只展示等待状态 |
| 法规核查 | 规则裁剪、完整性核查、文本抽取、章节核查、一致性核查 | 完整性先跑,章节和一致性并行 |
| 风险输出 | 风险分级、建议生成、RAG 依据引用、报告导出 | 生成问题和过程产物 |
| 通知复核 | 飞书通知、补充资料、整改复核、关闭确认 | 支持后续闭环 |
### 4.4 节点定义
| 节点编码 | 节点名称 | 触发 Skill/服务 | 成功条件 | 失败或暂停处理 |
| --- | --- | --- | --- | --- |
| prepare | 准备资料 | RegulatoryWorkflowExecutor | 绑定成功的 `FileSummaryBatch` | 无汇总批次则串联自动汇总 |
| info_extract | 识别信息 | RegulatoryInfoExtract Skill | 输出适用条件候选值 | 缺少关键条件则进入待确认 |
| condition_confirm | 确认条件 | Conversation Interaction | 用户确认产品类别、注册类型、临床路径等 | 暂停等待用户输入 |
| rule_scope | 裁剪规则 | RuleLoader | 生成本次适用规则清单 | 规则加载失败则降级 RAG 辅助核查 |
| completeness_check | 完整性核查 | CompletenessCheck Skill | 输出缺失文件和文件项问题 | 单项失败记录待确认 |
| text_extract | 文本抽取 | TextExtract Skill | 抽取关键文件文本和首页内容 | 单文件失败记录问题并继续 |
| structure_check | 章节核查 | StructureCheck Skill | 输出章节缺失和格式问题 | 与一致性核查并行 |
| consistency_check | 一致性核查 | ConsistencyCheck Skill | 输出字段冲突问题 | 低置信度字段可用 LLM 辅助 |
| risk_assess | 风险分级 | RiskAssess Skill | 归并问题、生成风险等级和建议 | 无 RAG 依据时仍输出规则问题 |
| report_export | 报告导出 | RegulatoryReportExport Skill | 生成 Markdown、Excel、JSON 产物 | 导出失败记录批次失败 |
| notify | 飞书通知 | FeishuNotifier | 阻断项、高风险、中风险完成通知 | CLI 失败写入通知失败记录 |
| rectify_review | 整改复核 | RectificationReview Skill | 输出复核通过/不通过 | 关闭前等待人工确认 |
---
## 五、规则库与 RAG 设计
### 5.1 双层法规能力
| 层级 | 职责 | 不承担的职责 |
| --- | --- | --- |
| 结构化规则库 | 判断文件项、章节项、关键字段、一致性字段、风险等级和整改模板 | 不负责自由解释法规 |
| RAG 法规依据索引 | 从本地 CMDE 原文材料检索法规依据片段、来源文件和引用说明 | 不直接决定合规结论 |
### 5.2 规则文件结构
Demo 阶段规则采用项目内 JSON/YAML 文件维护,建议路径:
```text
review_agent/rules/nmpa_ivd_registration_v1.yaml
```
规则文件需要包含版本信息:
| 字段 | 说明 |
| --- | --- |
| version | 规则版本,如 nmpa_ivd_2021_v1 |
| source_url | https://www.cmde.org.cn/xwdt/zxyw/20210930163300622.html |
| source_path | 本地 CMDE 法规材料路径 |
| effective_date | 规则生效日期或公告发布日期 |
| rag_index_version | 对应 RAG 索引版本 |
规则项最小结构:
```yaml
version: nmpa_ivd_2021_v1
source_url: https://www.cmde.org.cn/xwdt/zxyw/20210930163300622.html
source_path: docs/0.原始材料/关于公布体外诊断试剂注册申报资料要求和批准证明文件格式的公告
effective_date: "2021-09-30"
file_items:
- rule_id: ivd_registration_test_report
title: 注册检验报告
required_type: required
applies_when:
product_category: in_vitro_diagnostic
registration_type: initial_registration
match_keywords:
file_name: ["注册检验报告", "检验报告"]
directory: ["注册检验", "检测报告"]
first_pages: ["医疗器械注册检验报告", "检验结论"]
required_sections: ["样品信息", "检验依据", "检验项目", "检验结论", "签章"]
required_fields: ["产品名称", "型号规格", "样本类型"]
consistency_fields: ["产品名称", "型号规格"]
default_risk_level: blocking
suggestion_template: 请补充与本产品一致的注册检验报告,并确保报告包含样品信息、检验依据、检验项目、检验结论和签章页。
```
### 5.3 规则加载策略
| 场景 | 处理方式 |
| --- | --- |
| 规则文件正常加载 | 按结构化规则执行核查RAG 补充法规依据 |
| 规则文件加载失败 | 降级为 RAG 辅助核查,报告明确标记“仅供参考,不输出正式合规结论” |
| 规则命中但 RAG 无依据 | 仍输出问题,法规依据标记“规则库依据,原文待补充” |
| 规则版本与 RAG 版本不一致 | 批次标记提示项,允许继续但报告记录版本差异 |
### 5.4 RAG 索引设计
| 项目 | 说明 |
| --- | --- |
| 资料来源 | 本地 CMDE 公告目录下的 doc/docx 文档 |
| 索引粒度 | 按标题、段落、表格行或文件项说明切分 |
| 元数据 | source_file、section_title、page_or_row、rule_version、source_url |
| 输出 | matched_snippets、source_file、score、citation_text |
| 用途 | 风险报告中的法规依据、AI 对话解释、飞书通知简要依据 |
---
## 六、Skill 设计
### 6.1 RegulatoryInfoExtract Skill
| 项目 | 说明 |
| --- | --- |
| 中文名称 | 法规适用信息抽取 Skill |
| 职责 | 从 `FileSummaryItem`、文件名、目录名和文本片段中抽取法规适用条件 |
| 输入 | regulatory_batch_id、file_summary_batch_id、file_items |
| 输出 | 产品类别、注册类型、临床评价路径、产品名称、型号规格、预期用途、置信度、证据 |
| 关键规则 | 不修改自动汇总的产品名识别 Skill缺少关键条件时暂停等待用户确认 |
用户确认字段:
| 字段 | 是否必填 | 说明 |
| --- | --- | --- |
| 产品类别 | 是 | 医疗器械/体外诊断试剂等 |
| 注册类型 | 是 | 首次注册、变更注册、延续注册等 |
| 临床评价路径 | 是 | 临床试验、免临床、同品种比对等 |
| 产品名称 | 是 | 用于一致性核查 |
| 型号规格 | 是 | 用于一致性核查 |
| 预期用途 | 是 | 用于规则裁剪和一致性核查 |
### 6.2 TextExtract Skill
| 项目 | 说明 |
| --- | --- |
| 中文名称 | 文本抽取 Skill |
| 职责 | 按需抽取关键文件首页、前几页、章节文本和字段候选值 |
| 输入 | regulatory_batch_id、file_item_ids、extract_scope |
| 输出 | text_blocks、first_page_text、section_candidates、field_candidates |
| 数据写入 | RegulatoryArtifactartifact_type=text_extract_json |
| 关键规则 | 统一抽取,避免完整性、章节、一致性节点重复读取文件 |
### 6.3 CompletenessCheck Skill
| 项目 | 说明 |
| --- | --- |
| 中文名称 | 法规资料完整性核查 Skill |
| 职责 | 对照适用规则清单检查文件项和文件项子项是否存在 |
| 输入 | regulatory_batch_id、file_summary_items、scoped_rules |
| 输出 | missing_items、matched_items、pending_confirm_items |
| 关键规则 | 文件匹配采用文件名、目录名、首页内容三层匹配,记录命中证据 |
### 6.4 StructureCheck Skill
| 项目 | 说明 |
| --- | --- |
| 中文名称 | 章节结构核查 Skill |
| 职责 | 检查关键文件是否包含规则要求章节 |
| 输入 | regulatory_batch_id、text_blocks、required_sections |
| 输出 | missing_sections、abnormal_sections、evidence |
| 关键规则 | 章节缺失按规则初始等级输出,由 RiskAssess 统一归并 |
### 6.5 ConsistencyCheck Skill
| 项目 | 说明 |
| --- | --- |
| 中文名称 | 跨文件一致性核查 Skill |
| 职责 | 抽取并比对产品名称、型号规格、预期用途等核心字段 |
| 输入 | regulatory_batch_id、text_blocks、consistency_fields |
| 输出 | field_values、conflicts、confidence |
| 关键规则 | 规则/正则优先,失败或置信度低时调用 LLM 辅助抽取结构化 JSON |
### 6.6 RiskAssess Skill
| 项目 | 说明 |
| --- | --- |
| 中文名称 | 风险分级与整改建议 Skill |
| 职责 | 归并核查问题,统一风险等级,生成整改建议和法规依据 |
| 输入 | all_check_findings、rules、rag_results |
| 输出 | RegulatoryIssue 列表、risk_summary、suggestions |
| 关键规则 | 核查节点提供初始等级RiskAssess 负责去重、合并、升级或降级 |
### 6.7 RegulatoryReportExport Skill
| 项目 | 说明 |
| --- | --- |
| 中文名称 | 法规核查报告导出 Skill |
| 职责 | 生成最终报告和过程产物 |
| 输入 | regulatory_batch_id、issues、artifacts、notification_records |
| 输出 | Markdown 报告、Excel 清单、JSON 产物、下载链接 |
| 关键规则 | 最终报告进入 `ExportedSummaryFile`,过程产物进入 `RegulatoryArtifact` |
### 6.8 FeishuNotifier
| 项目 | 说明 |
| --- | --- |
| 中文名称 | 飞书通知适配器 |
| 职责 | 对阻断项、高风险、中风险发送飞书通知并 @ 上传人 |
| 输入 | recipient、risk_summary、message_markdown |
| 输出 | send_status、external_message_id、error_message |
| Demo 实现 | 抽象接口接飞书 CLI并支持直接测试发送 |
| 后续演进 | 替换为飞书 Webhook/API |
---
## 七、数据模型设计
### 7.1 RegulatoryReviewBatch
法规核查批次,表示一次法规核查工作流执行。
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| id | BigAutoField | 主键 |
| conversation | ForeignKey(Conversation) | 绑定对话 |
| user | ForeignKey(User) | 发起用户 |
| file_summary_batch | ForeignKey(FileSummaryBatch) | 关联文件汇总批次 |
| batch_no | CharField | 法规核查批次编号 |
| status | CharField | pending、running、waiting_user、success、failed、reference_only、partial_success、cancelled |
| rule_version | CharField | 使用的结构化规则版本 |
| rule_source_url | URLField | 法规来源 URL |
| rule_source_path | CharField | 本地法规资料路径 |
| rag_index_version | CharField | RAG 索引版本 |
| product_category | CharField | 用户确认后的产品类别 |
| registration_type | CharField | 用户确认后的注册类型 |
| clinical_evaluation_path | CharField | 用户确认后的临床评价路径 |
| product_name | CharField | 产品名称 |
| model_specification | CharField | 型号规格 |
| intended_use | TextField | 预期用途 |
| risk_summary_json | JSONField | 风险数量摘要 |
| error_message | TextField | 异常说明 |
| created_at | DateTimeField | 创建时间 |
| started_at | DateTimeField | 开始时间 |
| finished_at | DateTimeField | 完成时间 |
### 7.2 RegulatoryIssue
法规核查问题和整改状态实体。
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| id | BigAutoField | 主键 |
| batch | ForeignKey(RegulatoryReviewBatch) | 所属法规核查批次 |
| issue_code | CharField | 问题编码 |
| issue_type | CharField | completeness、structure、consistency、notification、review |
| risk_level | CharField | blocking、high、medium、low、info |
| title | CharField | 问题标题 |
| description | TextField | 问题描述 |
| rule_id | CharField | 命中的规则 ID |
| regulation_basis | TextField | 法规依据或规则依据 |
| evidence_json | JSONField | 文件路径、页码、文本片段、字段值等证据 |
| suggestion | TextField | 整改建议 |
| owner | ForeignKey(User) | 默认上传人 |
| status | CharField | 待确认、待处理、已补充、复核通过、复核不通过、已关闭 |
| confirmed_by | ForeignKey(User) | 确认人,可为空 |
| closed_by | ForeignKey(User) | 关闭人,可为空 |
| created_at | DateTimeField | 创建时间 |
| updated_at | DateTimeField | 更新时间 |
### 7.3 RegulatoryArtifact
法规核查过程产物留底实体。
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| id | BigAutoField | 主键 |
| batch | ForeignKey(RegulatoryReviewBatch) | 所属法规核查批次 |
| artifact_type | CharField | condition_record、rule_matrix、risk_list、text_extract_json、rag_result_json、notification_record、review_record |
| file_format | CharField | markdown、excel、json |
| file_name | CharField | 文件名 |
| storage_path | CharField | 存储路径 |
| summary | TextField | 产物摘要 |
| created_at | DateTimeField | 创建时间 |
### 7.4 RegulatoryNotificationRecord
飞书通知记录。
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| id | BigAutoField | 主键 |
| batch | ForeignKey(RegulatoryReviewBatch) | 所属法规核查批次 |
| recipient | ForeignKey(User) | 通知对象 |
| channel | CharField | feishu_cli、feishu_api、mock |
| risk_levels | JSONField | 本次通知包含的风险等级 |
| message_summary | TextField | 通知摘要 |
| send_status | CharField | pending、success、failed |
| external_message_id | CharField | 外部消息 ID可为空 |
| error_message | TextField | 失败原因 |
| sent_at | DateTimeField | 发送时间 |
### 7.5 与既有模型关系
```text
Conversation 1:N FileSummaryBatch
FileSummaryBatch 1:N FileSummaryItem
FileSummaryBatch 1:N RegulatoryReviewBatch
RegulatoryReviewBatch 1:N RegulatoryIssue
RegulatoryReviewBatch 1:N RegulatoryArtifact
RegulatoryReviewBatch 1:N RegulatoryNotificationRecord
RegulatoryReviewBatch 1:N ExportedSummaryFile
```
---
## 八、接口设计
### 8.1 发起法规核查
| 项目 | 内容 |
| --- | --- |
| URL | POST /api/review-agent/regulatory-review/start/ |
| 认证 | 登录用户 |
| 请求 | conversation_id、file_summary_batch_id 可选、force_resummary 可选 |
| 响应 | regulatory_batch_id、workflow_type、status |
处理规则:
| 场景 | 说明 |
| --- | --- |
| 传入 file_summary_batch_id | 校验该批次属于当前对话和用户 |
| 未传入 file_summary_batch_id | 默认查找当前对话最近一次成功汇总批次 |
| 无成功汇总批次 | 自动启动文件汇总工作流,完成后继续法规核查 |
### 8.2 提交适用条件确认
| 项目 | 内容 |
| --- | --- |
| URL | POST /api/review-agent/regulatory-review/{batch_id}/confirm-condition/ |
| 认证 | 登录用户 |
| 请求 | product_category、registration_type、clinical_evaluation_path、product_name、model_specification、intended_use |
| 响应 | batch_id、status、next_node |
说明:对话选择框负责收集用户确认结果,接口只接收结构化确认值。用户修改已确认条件时创建新的 `RegulatoryReviewBatch`
### 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、uploaded_files 可选、review_mode |
| 响应 | review_record_id、status、updated_issues |
Demo 阶段主要通过对话指令触发,卡片入口作为设计预留。
### 8.5 下载法规核查文件
| 项目 | 内容 |
| --- | --- |
| URL | GET /api/review-agent/regulatory-review/artifacts/{artifact_id}/download/ |
| 认证 | 登录用户 |
| 响应 | 文件流 |
权限规则:
```text
artifact_id -> regulatory_batch -> conversation -> user
必须等于当前登录用户,才允许下载。
```
---
## 九、前端设计
### 9.1 多工作流卡片轮播
AI 对话页顶部或对话流内的工作流区域支持多个工作流卡片。
| 设计点 | 说明 |
| --- | --- |
| 展示方式 | 顶部只显示当前活跃卡片,通过左右箭头或点位切换历史卡片 |
| 卡片类型 | file_summary、regulatory_review |
| 事件更新 | 统一监听 `workflow` SSE根据 workflow_type 和 batch_id 更新对应卡片 |
| 卡片职责 | 展示工作流状态,不承载适用条件编辑表单 |
| 历史恢复 | 页面刷新后按对话查询工作流批次并恢复卡片列表 |
### 9.2 法规核查卡片
卡片主节点:
| 主节点 | 展示文案 |
| --- | --- |
| prepare | 准备资料 |
| info_extract | 识别信息 |
| condition_confirm | 确认条件 |
| regulatory_check | 法规核查 |
| risk_output | 风险输出 |
| notify_review | 通知复核 |
| completed | 已完成 |
节点可展开展示子节点,例如法规核查下展开“规则裁剪、完整性核查、文本抽取、章节核查、一致性核查”。
### 9.3 适用条件确认交互
适用条件确认采用 AI 对话选择框,不放在卡片内。交互形式参考计划模式:系统给出识别结果、推荐选项和自由输入能力。
确认字段:
| 字段 | 交互方式 |
| --- | --- |
| 产品类别 | 选项 + 自由输入 |
| 注册类型 | 选项 + 自由输入 |
| 临床评价路径 | 选项 + 自由输入 |
| 产品名称 | 文本确认 |
| 型号规格 | 文本确认 |
| 预期用途 | 文本确认 |
### 9.4 对话框结果展示
工作流完成后新增助手消息,优先展示风险摘要、待处理问题和下载链接。
```markdown
已完成 NMPA 注册资料法规核查。
| 风险等级 | 数量 |
| --- | --- |
| 阻断项 | 2 |
| 高风险 | 1 |
| 中风险 | 3 |
| 低风险 | 4 |
| 提示项 | 2 |
| 等级 | 问题 | 状态 | 建议 |
| --- | --- | --- | --- |
| 阻断项 | 缺少注册检验报告 | 待处理 | 请补充注册检验报告并发起复核 |
[下载 Markdown 核查报告](download-url)
[下载 Excel 缺失清单](download-url)
[下载过程产物 JSON](download-url)
```
---
## 十、事件设计
### 10.1 SSE 事件结构
复用现有 `workflow` 事件,新增字段区分工作流。
```json
{
"event": "workflow",
"workflow_type": "regulatory_review",
"batch_id": 2001,
"conversation_id": 1001,
"node_code": "structure_check",
"node_group": "regulatory_check",
"status": "running",
"message": "正在核查关键文件章节结构",
"progress": 62,
"payload": {
"risk_summary": {
"blocking": 1,
"high": 2
}
}
}
```
### 10.2 状态扩展
| 状态 | 含义 |
| --- | --- |
| pending | 已创建,等待执行 |
| running | 执行中 |
| waiting_user | 等待用户确认适用条件 |
| success | 节点成功 |
| failed | 节点失败 |
| reference_only | 规则库不可用,降级为 RAG 辅助核查 |
| partial_success | 部分节点、通知或非关键过程产物失败,但已输出主要结果 |
| cancelled | 用户或系统取消执行 |
| skipped | 当前节点跳过 |
---
## 十一、输出与留底设计
### 11.1 最终下载文件
最终面向用户下载的报告沿用 `ExportedSummaryFile`
| 文件 | 说明 |
| --- | --- |
| Markdown 核查报告 | 面向人工阅读的完整法规核查报告 |
| Excel 缺失清单 | 面向整改跟踪的风险和缺失清单 |
| JSON 结果包 | 面向后续复核和系统处理的结构化结果 |
### 11.2 过程产物
过程产物进入 `RegulatoryArtifact`
| 产物类型 | 格式 | 说明 |
| --- | --- | --- |
| condition_record | markdown/json | 适用条件识别和用户确认记录 |
| rule_matrix | excel/json | 法规核查矩阵 |
| risk_list | markdown/json | 风险清单和等级归并结果 |
| text_extract_json | json | 关键文件文本抽取结果 |
| rag_result_json | json | RAG 检索依据和引用片段 |
| notification_record | markdown/json | 飞书通知记录 |
| review_record | markdown/json | 整改复核记录 |
---
## 十二、异常与降级设计
| 场景 | 处理 |
| --- | --- |
| 无成功文件汇总批次 | 自动串联执行文件汇总;汇总失败则法规核查不启动 |
| 规则文件加载失败 | 降级为 RAG 辅助核查,标记 `reference_only`,报告声明仅供参考 |
| RAG 检索不到依据 | 规则命中的问题仍输出,依据标记“规则库依据,原文待补充” |
| 关键适用条件缺失 | 工作流进入 `waiting_user`,用户确认后继续 |
| 文本抽取失败 | 记录文件级问题,相关章节或一致性结果标记待确认 |
| LLM 字段抽取失败 | 回退规则/正则结果,低置信度字段进入待确认 |
| 飞书 CLI 发送失败 | 记录通知失败,不阻断报告生成 |
| 过程产物导出失败 | 批次标记失败或部分失败,错误信息写入批次 |
---
## 十三、安全设计
| 设计点 | 说明 |
| --- | --- |
| 对话隔离 | RegulatoryReviewBatch 必须绑定当前 Conversation |
| 文件访问 | 只能读取关联 FileSummaryBatch 下的 FileSummaryItem |
| 下载权限 | 导出文件和过程产物下载必须校验 conversation.user |
| 飞书脱敏 | 飞书通知只展示风险摘要和必要文件名,不直接发送敏感全文 |
| 证据留痕 | 证据片段写入受控存储,不暴露给无权限用户 |
| CLI 安全 | 飞书 CLI 参数使用结构化调用,避免拼接执行用户输入 |
---
## 十四、验收设计
| 序号 | 验收项 | 验收标准 |
| --- | --- | --- |
| 1 | 汇总复用 | 已有成功文件汇总批次时,法规核查默认复用最近批次 |
| 2 | 串联启动 | 无成功汇总批次时,可先自动汇总再执行法规核查 |
| 3 | 多卡片切换 | 同一对话存在多个工作流时,可通过轮播切换卡片 |
| 4 | 适用条件确认 | 系统能识别条件并通过对话选择框让用户确认 |
| 5 | 规则与 RAG | 结构化规则负责判断RAG 能补充法规依据 |
| 6 | 完整性核查 | 能识别缺失文件和文件项级缺失 |
| 7 | 章节核查 | 能识别关键文件章节缺失或异常 |
| 8 | 一致性核查 | 能识别产品名称、型号规格、预期用途等字段冲突 |
| 9 | 风险分级 | 问题能归并为阻断项、高、中、低、提示项 |
| 10 | 飞书通知 | 阻断项、高风险、中风险能通过飞书 CLI @ 上传人 |
| 11 | 过程留底 | 条件确认、核查矩阵、风险清单、RAG 结果、通知记录、复核记录均有产物 |
| 12 | 整改复核 | 用户通过对话指令上传补充资料后,可重新复核问题状态 |
| 13 | 权限隔离 | A 对话的法规核查结果和过程产物不能被 B 对话访问 |
---
## 十五、实施建议
1. 先实现 `RegulatoryReviewBatch``RegulatoryIssue``RegulatoryArtifact``RegulatoryNotificationRecord` 数据模型。
2. 增加规则文件加载器和一版 `nmpa_ivd_registration_v1` 结构化规则。
3. 构建本地 CMDE 法规材料 RAG 索引,确保能按规则项检索依据。
4. 实现法规核查工作流主链路:准备资料、信息抽取、条件确认、规则裁剪、完整性核查。
5. 补齐 `TextExtract`、章节核查、一致性核查、风险归并和报告导出。
6. 接入 `FeishuNotifier` CLI 实现并提供直接测试命令。
7. 改造前端工作流卡片,支持 `workflow_type` 和轮播切换。
8. 最后完善整改复核、过程产物下载和权限校验。
---
## 十六、待确认事项
| 序号 | 问题 | 当前建议 | 状态 |
| --- | --- | --- | --- |
| 1 | 规则文件格式使用 YAML 还是 JSON | 建议 YAML便于人工维护和注释 | 待确认 |
| 2 | 本地 RAG 使用哪种向量库 | 可复用项目依赖中的 ChromaDB | 待技术验证 |
| 3 | 飞书 CLI 具体命令格式 | 需要结合本机飞书 CLI 或企业内部工具确认 | 待确认 |
| 4 | 对话选择框前端能力 | 参考计划模式实现选项 + 自由输入 | 待技术验证 |
| 5 | LLM 抽取是否需要人工确认阈值 | 建议低于置信度阈值进入待确认 | 待确认 |

View File

@@ -0,0 +1,816 @@
# 产品关键信息提取与申报文件自动填表功能设计
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 需求分析文档 | docs/1.需求分析/3.产品关键信息提取与申报文件自动填表.md |
| 依赖功能设计 | docs/2.功能设计/1.自动汇总.mddocs/2.功能设计/2.NMPA注册资料法规核查与整改闭环.md |
| 功能名称 | 产品关键信息提取与申报文件自动填表 |
| 所属模块 | 审核智能体 review_agent |
| 设计日期 | 2026-06-07 |
| 设计版本 | V1.0 |
---
## 一、设计目标
本功能作为独立工作流 `application_form_fill` 建设,由用户在 AI 对话中触发,例如“帮我填注册证”“给我这个内容对应的表格”“为我该方案生成申报模板”“生成安全和性能基本原则清单”“把产品信息填到申报模板里”等。用户可以明确指定目标模板;未指定时,系统根据识别出的注册类型生成当前注册类型适用的全部模板。
本功能复用第一批文件汇总结果作为文件来源复用第二批法规核查中的文本抽取、适用条件识别、LLM 调用、飞书通知和导出下载能力,但拥有独立批次、独立工作流卡片和独立过程产物。系统复制原始法规模板到批次工作目录,不覆盖原始文件;随后按模板配置识别应填字段,使用规则/正则抽取与 LLM 结构化抽取并行处理,合并字段、识别冲突、写入 Word 模板,并在 AI 对话框和飞书通知中提示生成结果与冲突摘要。
Demo 阶段优先保证 Word 模板自动填写和下载。PDF 转换作为待办增强项:功能设计保留 PDF 导出节点和数据结构,实施时可先返回 Word 与追溯清单,并在待办清单记录 PDF 转换能力。
---
## 二、与既有功能的关系
### 2.1 复用边界
| 能力 | 处理方式 | 现有代码/模型 |
| --- | --- | --- |
| 对话与用户权限 | 复用 | `Conversation``Message` |
| 附件上传与文件绑定 | 复用 | `FileAttachment``FileSummaryBatchAttachment` |
| 文件汇总与页数统计 | 复用 | `FileSummaryBatch``FileSummaryItem``file_summary.workflow` |
| 文本抽取 | 复用并扩展 | `regulatory_review/services/text_extract.py``rag_index.py` |
| 适用条件候选 | 复用并扩展 | `regulatory_review/services/info_extract.py` |
| LLM 调用 | 复用 | `review_agent/llm.py``regulatory_review/services/llm_review.py` |
| 导出记录与下载 | 扩展复用 | `ExportedSummaryFile` |
| 过程产物 | 复用 | `RegulatoryArtifact` 或新增填表过程产物 |
| 飞书通知 | 复用并扩展 | `regulatory_review/services/feishu_notifier.py` |
| SSE 工作流事件 | 复用 | `WorkflowNodeRun``WorkflowEvent` |
### 2.2 新增边界
| 能力 | 说明 |
| --- | --- |
| 独立填表批次 | 新增 `ApplicationFormFillBatch`,不强绑法规核查批次 |
| 模板配置 | 新增 YAML 配置,维护模板路径、适用条件、字段映射和输出规则 |
| 模板选择 | 根据用户指定模板和注册类型选择生成范围 |
| 规则/正则与 LLM 并行抽取 | 两路抽取并行执行,最后统一合并 |
| 字段冲突归并 | 按来源文件优先级处理,说明书优先;冲突字段高亮 |
| Word 模板填充 | 使用 `python-docx``.docx` 表格、段落和占位字段写入 |
| `.doc` 模板转换 | 使用 LibreOffice/soffice 或预转换 `.docx` 模板 |
| 字段来源追溯 | 输出 Excel/JSON 追溯清单,记录抽取、合并和冲突证据 |
---
## 三、总体架构
### 3.1 架构原则
| 原则 | 说明 |
| --- | --- |
| 独立工作流 | 填表流程拥有独立批次、节点和卡片workflow_type 为 `application_form_fill` |
| 复用文件汇总 | 填表不重新实现上传扫描,默认使用当前对话最近成功的 `FileSummaryBatch` |
| 用户指令优先 | 用户明确指定模板或注册类型时,优先使用用户指令 |
| 配置驱动 | 模板路径、字段映射、适用条件和输出规则写入 YAML 配置 |
| Word 优先 | Demo 阶段优先生成可编辑 WordPDF 作为增强项进入待办 |
| 可追溯 | 规则抽取、LLM 抽取、合并结果、冲突列表和来源证据均留底 |
| 失败隔离 | 单字段、单模板或 PDF 转换失败不影响其他模板输出 |
| 通知可控 | 填表完成后可通过飞书通知上传人,通知内容只包含摘要和下载提示 |
### 3.2 逻辑架构
```mermaid
flowchart TD
A["AI 对话页"] --> B["意图识别 application_form_fill"]
B --> C{"本次消息是否带附件"}
C -->|"是"| D["先执行文件汇总工作流"]
C -->|"否"| E["查找最近成功 FileSummaryBatch"]
D --> E
E --> F["ApplicationFormFillBatch"]
F --> G["FormFillWorkflowExecutor"]
G --> H["模板配置 YAML"]
G --> I["模板选择服务"]
G --> J["文本抽取服务"]
J --> K1["规则/正则抽取"]
J --> K2["LLM 结构化抽取"]
K1 --> L["字段合并与冲突归并"]
K2 --> L
L --> M["Word 模板填充服务"]
M --> N["追溯清单导出"]
M --> O["PDF 转换服务 P1"]
N --> P["ExportedSummaryFile"]
O --> P
G --> Q["WorkflowEvent/SSE"]
Q --> R["自动填表工作流卡片"]
G --> S["FeishuNotifier"]
S --> T["上传人通知"]
```
### 3.3 技术选型
| 设计项 | Demo 方案 | 后续演进 |
| --- | --- | --- |
| Web 框架 | Django沿用当前 `review_agent` 应用 | 保持 Django必要时拆分独立 app |
| 工作流编排 | 新增轻量 `FormFillWorkflowExecutor` | 接入 LangGraph 子图 |
| 后台执行 | Django 后台线程,沿用现有工作流方式 | Celery/RQ + Redis |
| 工作流状态 | `WorkflowNodeRun` + `WorkflowEvent`,新增 workflow_type | 独立工作流事件中心 |
| 模板配置 | YAML建议路径 `review_agent/application_form_fill/templates/application_form_templates_v1.yaml` | 数据库模板管理后台 |
| Word 处理 | `python-docx` 写入 `.docx` 表格和段落,高亮冲突字段 | OOXML 精细 patch、内容控件 SDT |
| `.doc` 转换 | LibreOffice/soffice headless 转 `.docx`;无法部署时预置 `.docx` 工作模板 | 模板入库前统一转换和人工校验 |
| PDF 导出 | P1 待办LibreOffice/soffice headless 转 PDF | 逐页渲染 QA、版式差异检测 |
| Excel 追溯清单 | `openpyxl` | 增加多 Sheet 审核视图 |
| 文本抽取 | 复用 `text_extract.py``rag_index.py` | OCR、文档文本缓存 |
| 字段抽取 | 规则/正则与 LLM 结构化抽取并行,合并后输出 | 可配置抽取器和置信度模型 |
| 飞书通知 | 复用 `FeishuNotifier`Demo 可 mock 或 CLI | 飞书 Webhook/API |
---
## 四、触发与模板选择设计
### 4.1 意图识别
填表工作流通过用户对话触发。意图识别可先采用关键词规则,必要时调用现有 LLM 路由能力。
| 触发表达 | 触发结果 |
| --- | --- |
| 帮我填注册证 | 触发填表,指定注册证格式 |
| 给我这个内容对应的表格 | 触发填表,未指定模板 |
| 为我该方案生成申报模板 | 触发填表,未指定模板 |
| 生成安全和性能基本原则清单 | 触发填表,指定安全和性能基本原则清单 |
| 把产品信息填到申报模板里 | 触发填表,未指定模板 |
| 只生成变更注册备案文件 | 触发填表,指定变更注册(备案)文件 |
### 4.2 文件来源选择
| 场景 | 处理方式 |
| --- | --- |
| 本次消息带新附件 | 先自动执行文件汇总,汇总成功后启动填表 |
| 本次消息无附件 | 默认使用当前对话最近一次成功 `FileSummaryBatch` |
| 无成功汇总批次 | 对话框提示用户先上传资料或补充附件 |
| 用户明确指定历史批次 | 校验批次属于当前对话和当前用户后使用 |
### 4.3 注册类型识别优先级
注册类型用于决定默认生成哪些模板。优先级如下:
```text
用户话语明确指定
-> 当前对话已确认的法规核查条件
-> 上传文件内容抽取结果
-> 无法识别
```
### 4.4 模板选择规则
| 场景 | 生成模板 |
| --- | --- |
| 用户未指定模板,注册类型为首次注册 | 注册证格式;安全和性能基本原则清单 |
| 用户未指定模板,注册类型为变更注册或备案 | 变更注册(备案)文件;安全和性能基本原则清单 |
| 用户未指定模板,注册类型无法识别 | 安全和性能基本原则清单;注册证/变更文件进入待确认提示 |
| 用户明确指定模板且与注册类型一致 | 只生成用户指定模板 |
| 用户明确指定模板但与注册类型不一致 | 允许生成,并在摘要和追溯清单提示“与识别注册类型不一致,需人工确认” |
| 用户指定“全部模板” | 生成三个目标模板,并提示用户核对注册类型适用性 |
---
## 五、工作流设计
### 5.1 节点图
```mermaid
flowchart LR
N1["准备资料"] --> N2["选择模板"]
N2 --> N3["复制模板"]
N3 --> N4["抽取字段"]
N4 --> N5["冲突归并"]
N5 --> N6["填写 Word"]
N6 --> N7["转换 PDF P1"]
N6 --> N8["追溯清单"]
N7 --> N9["输出下载"]
N8 --> N9
N9 --> N10["飞书通知"]
N10 --> N11["完成"]
```
### 5.2 节点定义
| 节点编码 | 节点名称 | 触发服务 | 成功条件 | 失败处理 |
| --- | --- | --- | --- | --- |
| prepare | 准备资料 | `FormFillWorkflowExecutor` | 找到或生成成功的 `FileSummaryBatch` | 无文件汇总则暂停提示上传 |
| template_select | 选择模板 | `TemplateSelectionService` | 输出本次目标模板列表 | 无适用模板则失败 |
| template_copy | 复制模板 | `TemplateRepository` | 模板副本进入批次工作目录 | 单模板失败不影响其他模板 |
| field_extract | 抽取字段 | `FieldExtractionService` | 规则/正则与 LLM 结果留底 | 单文件失败记录并继续 |
| conflict_merge | 冲突归并 | `FieldMergeService` | 输出最终字段和冲突列表 | 无字段时仍生成空模板 |
| word_fill | 填写 Word | `WordTemplateFillService` | 生成填好后的 Word 文件 | 单模板失败记录失败 |
| pdf_convert | 转换 PDF | `PdfConversionService` | P1生成 PDF 文件 | PDF 失败标记 partial_success |
| trace_export | 追溯清单 | `TraceabilityExportService` | 生成 Excel/JSON 追溯清单 | 失败不影响 Word |
| output_export | 输出下载 | `FormFillExportService` | 写入 `ExportedSummaryFile` 并生成下载链接 | 关键 Word 失败则批次失败 |
| notify | 飞书通知 | `FeishuNotifier` | 通知上传人生成完成 | 通知失败不影响下载 |
| completed | 完成 | 工作流执行器 | 更新批次状态和对话消息 | - |
### 5.3 状态设计
| 状态 | 含义 |
| --- | --- |
| pending | 已创建,等待执行 |
| running | 执行中 |
| waiting_user | 缺少文件或关键条件,等待用户补充 |
| success | Word 和必要追溯产物生成成功 |
| partial_success | Word 已生成但部分模板、PDF、追溯清单或通知失败 |
| failed | 所有目标 Word 模板均生成失败 |
| skipped | 当前节点不适用,例如 Demo 阶段跳过 PDF |
---
## 六、模板配置设计
### 6.1 配置文件路径
建议新增:
```text
review_agent/application_form_fill/templates/application_form_templates_v1.yaml
```
### 6.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: product_name
label: 产品名称
target:
type: table_row
row_label: 产品名称
sources: ["说明书", "产品技术要求", "注册检验报告"]
- key: package_specification
label: 包装规格
target:
type: table_row
row_label: 包装规格
sources: ["说明书", "产品技术要求"]
```
### 6.3 模板配置项
| 配置项 | 说明 |
| --- | --- |
| code | 模板编码,用于用户指定和导出分类 |
| name | 模板中文名称 |
| source_file | 原始模板文件名 |
| working_template | 可选,预转换 `.docx` 工作模板 |
| output_label | 文件命名中的模板标签 |
| applies_when | 默认适用注册类型 |
| fields | 字段映射列表 |
| checklist_items | 安全和性能基本原则清单条目映射 |
| conversion | `.doc``.docx` 和 PDF 的转换策略 |
### 6.4 已知模板字段
注册证格式当前已从 `.docx` 表格识别到以下字段:注册人名称、注册人住所、生产地址、代理人名称、代理人住所、产品名称、包装规格、主要组成成分、预期用途、产品储存条件及有效期、附件、其他内容、备注。
变更注册(备案)文件和安全和性能基本原则清单当前为 `.doc`,实施前需通过 LibreOffice/soffice 转换或预置人工确认版 `.docx` 工作模板,再补齐字段映射。
---
## 七、字段抽取与合并设计
### 7.1 三层提取链路
```text
模板字段配置
-> 文档字段候选提取
-> 规则/正则抽取与 LLM 结构化抽取并行
-> 字段归一化
-> 来源优先级合并
-> 冲突识别
-> 最终字段包
```
### 7.2 规则/正则抽取
| 能力 | 说明 |
| --- | --- |
| 标签字段识别 | 识别 `产品名称:``预期用途:``储存条件:` 等标签行 |
| 表格字段识别 | 从 Word/Excel 表格中识别左侧字段名、右侧字段值 |
| 章节范围识别 | 从说明书、产品技术要求中按章节提取连续文本 |
| 文件类型识别 | 根据文件名、目录名和首页标题判断说明书、产品技术要求、检验报告 |
| 证据片段截取 | 保存字段前后上下文,用于追溯清单 |
### 7.3 LLM 结构化抽取
LLM 输入为模板字段清单、文件上下文和候选文本片段,输出严格 JSON
```json
{
"fields": [
{
"key": "storage_condition",
"label": "产品储存条件及有效期",
"value": "2-8℃保存有效期12个月",
"source_file": "说明书.docx",
"evidence": "产品储存条件2-8℃保存...",
"confidence": 0.86
}
],
"checklist_items": [
{
"item_code": "A1",
"applicability": "适用",
"compliance_evidence": "产品技术要求中规定了性能指标和检验方法",
"proof_location": "产品技术要求.docx 第2章"
}
]
}
```
### 7.4 并行合并规则
| 场景 | 处理规则 |
| --- | --- |
| 规则和 LLM 值一致 | 合并为同一字段,提高置信度 |
| 规则和 LLM 值不一致,但来源文件不同 | 按来源文件优先级处理,说明书优先 |
| 规则和 LLM 值不一致,来源文件相同 | 标记冲突,模板中高亮 |
| 说明书与其他文件冲突 | 采用说明书值,黄色底色、红色字体标记 |
| 说明书缺失,多个来源冲突 | 取最高优先级文件值并标记冲突;无法判断则留空 |
| 字段缺失 | 模板留空,追溯清单记录未提取 |
### 7.5 过程产物留底
字段抽取结果保存为 `field_extract_result.json`,至少包含:
| 内容 | 说明 |
| --- | --- |
| regex_results | 规则/正则抽取结果 |
| llm_results | LLM 结构化抽取结果 |
| merged_fields | 合并后的最终字段 |
| conflicts | 冲突字段列表 |
| source_evidence | 来源文件和文本片段 |
| selected_templates | 本次选择的模板 |
---
## 八、安全和性能基本原则清单设计
### 8.1 判断策略
安全和性能基本原则清单采用“候选判断 + 高置信度写入”策略。
| 步骤 | 说明 |
| --- | --- |
| 条目拆解 | 从模板配置中读取条目编号、原则内容、适用性栏、证据栏、证明文件位置栏 |
| 候选判断 | 规则和 LLM 均可给出适用/不适用候选 |
| 证据匹配 | 从产品技术要求、说明书、性能研究、稳定性研究、风险管理资料中匹配证明文件 |
| 高置信度写入 | 仅将高置信度判断写入 Word |
| 低置信度留空 | 证据不足或判断不一致时 Word 留空,追溯清单记录候选判断 |
| 冲突提示 | 冲突条目在对话框和追溯清单中提示,不强行填入 |
### 8.2 输出字段
| 字段 | 说明 |
| --- | --- |
| 条目编号 | 基本原则清单中的条目编码 |
| 条目内容 | 原始原则或要求 |
| 适用性 | 适用/不适用,低置信度留空 |
| 符合性证据 | 高置信度证据摘要 |
| 证明文件位置 | 文件名、章节、页码或文本定位 |
| 置信度 | 用于判断是否写入 Word |
| 候选来源 | 规则、LLM 或两者一致 |
---
## 九、Word 与 PDF 生成设计
### 9.1 Word 模板填充
| 能力 | 说明 |
| --- | --- |
| 模板副本 | 原始模板复制到批次工作目录后再写入 |
| 表格行填充 | 根据行首字段名定位目标单元格 |
| 段落占位填充 | 支持 `{{field_key}}` 等占位符 |
| 清单条目填充 | 按条目编号和配置列写入适用性、证据和证明位置 |
| 冲突高亮 | 冲突字段使用黄色底色和红色字体 |
| 缺失字段 | 保持空白,不写“待补充” |
| 版式保持 | 尽量不改变表格结构、分页和字体 |
### 9.2 PDF 转换
PDF 转换作为 P1 待办增强项设计:
| 阶段 | 处理 |
| --- | --- |
| Demo 主链路 | 优先生成 Word不因 PDF 能力缺失阻断工作流 |
| P1 增强 | 使用 LibreOffice/soffice headless 将 Word 转为 PDF |
| 失败处理 | Word 已生成但 PDF 失败时,批次状态为 `partial_success` |
| QA 增强 | 后续增加 PDF 页数非 0、逐页截图或版式差异检查 |
---
## 十、输出与下载设计
### 10.1 输出文件
| 文件 | Demo 阶段 | P1/P2 |
| --- | --- | --- |
| 填好后的 Word | 必须生成 | 持续支持 |
| PDF 预览 | 待办增强 | LibreOffice 转换生成 |
| 字段来源追溯清单 Excel | 允许生成,建议实现 | 增加多 Sheet |
| 字段抽取 JSON | 过程产物留底 | 支持下载或调试查看 |
### 10.2 文件命名
```text
批次号-产品名称-注册证格式.docx
批次号-产品名称-注册证格式.pdf
批次号-产品名称-变更注册备案文件.docx
批次号-产品名称-变更注册备案文件.pdf
批次号-产品名称-安全和性能基本原则清单.docx
批次号-产品名称-安全和性能基本原则清单.pdf
批次号-产品名称-字段来源追溯清单.xlsx
```
### 10.3 ExportedSummaryFile 扩展
继续复用 `ExportedSummaryFile`,但需要扩展 `ExportType`
| export_type | 说明 |
| --- | --- |
| markdown | 既有 Markdown 报告 |
| excel | Excel 追溯清单 |
| json | 字段抽取 JSON 或结果包 |
| word | 填好的 Word 文件,新增 |
| pdf | Word 转换后的 PDF新增 |
填表工作流导出记录建议:
| 字段 | 值 |
| --- | --- |
| workflow_type | `application_form_fill` |
| workflow_batch_id | `ApplicationFormFillBatch.id` |
| export_category | `filled_template``traceability``extract_result` |
| export_type | `word``pdf``excel``json` |
导出服务入参应包含目标输出类型列表,例如:
```json
{
"output_types": ["word", "pdf", "excel"],
"template_codes": ["registration_certificate", "essential_principles"]
}
```
系统根据入参决定生成哪些类型的内容。
---
## 十一、数据模型设计
### 11.1 ApplicationFormFillBatch
新增自动填表批次表。
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| id | BigAutoField | 主键 |
| conversation | ForeignKey(Conversation) | 绑定对话 |
| user | ForeignKey(User) | 发起用户 |
| source_summary_batch | ForeignKey(FileSummaryBatch) | 文件来源批次 |
| source_regulatory_batch | ForeignKey(RegulatoryReviewBatch, null=True) | 可选,复用已确认法规条件 |
| batch_no | CharField | 填表批次号,如 AFF-YYYYMMDDHHMMSS |
| status | CharField | pending、running、waiting_user、success、partial_success、failed |
| trigger_message | ForeignKey(Message, null=True) | 触发消息 |
| requested_templates | JSONField | 用户指定模板 |
| selected_templates | JSONField | 实际生成模板 |
| output_types | JSONField | 请求输出类型,如 word、pdf、excel |
| registration_type | CharField | 注册类型 |
| product_name | CharField | 产品名称 |
| conflict_summary | JSONField | 冲突摘要 |
| risk_notes | JSONField | 不适用模板、低置信度等提示 |
| work_dir | CharField | 批次工作目录 |
| error_message | TextField | 异常说明 |
| created_at | DateTimeField | 创建时间 |
| started_at | DateTimeField | 开始时间 |
| finished_at | DateTimeField | 完成时间 |
### 11.2 ApplicationFormFillArtifact
可新增独立过程产物表,也可复用 `RegulatoryArtifact`。考虑到这是独立工作流,建议新增轻量产物表,结构与 `RegulatoryArtifact` 保持一致。
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| id | BigAutoField | 主键 |
| batch | ForeignKey(ApplicationFormFillBatch) | 所属填表批次 |
| artifact_type | CharField | template_copy、field_extract_result、merged_fields、traceability、notification_record |
| file_format | CharField | json、excel、docx、pdf |
| name | CharField | 产物名称 |
| storage_path | CharField | 存储路径 |
| metadata | JSONField | 模板编码、输出类型、生成状态等 |
| content_hash | CharField | 文件 hash |
| created_at | DateTimeField | 创建时间 |
### 11.3 与既有模型关系
```text
Conversation 1:N ApplicationFormFillBatch
FileSummaryBatch 1:N ApplicationFormFillBatch
RegulatoryReviewBatch 0:N ApplicationFormFillBatch
ApplicationFormFillBatch 1:N ApplicationFormFillArtifact
ApplicationFormFillBatch 1:N WorkflowNodeRun
ApplicationFormFillBatch 1:N ExportedSummaryFile
```
---
## 十二、后端服务设计
### 12.1 FormFillWorkflowExecutor
| 方法 | 说明 |
| --- | --- |
| run(batch) | 串行执行自动填表节点 |
| run_node(node) | 执行单节点并记录进度 |
| resolve_source_summary_batch() | 根据本次附件或最近成功批次确定来源 |
| emit_event() | 写入 `WorkflowEvent` |
| complete_or_partial() | 根据 Word/PDF/通知结果更新批次状态 |
### 12.2 TemplateSelectionService
| 方法 | 说明 |
| --- | --- |
| parse_requested_templates(message) | 从用户话语中识别指定模板 |
| detect_registration_type() | 按用户话语、法规确认条件、文件抽取识别注册类型 |
| select_templates() | 根据注册类型和用户指令输出模板列表 |
### 12.3 TemplateRepository
| 方法 | 说明 |
| --- | --- |
| load_config() | 读取 YAML 模板配置 |
| resolve_source_template(code) | 找到原始模板或预转换模板 |
| copy_to_work_dir(code, batch) | 复制模板到批次目录 |
| convert_doc_to_docx(path) | `.doc``.docx` |
### 12.4 FieldExtractionService
| 方法 | 说明 |
| --- | --- |
| extract_by_rules(texts, template_fields) | 规则/正则抽取 |
| extract_by_llm(texts, template_fields) | LLM 结构化抽取 |
| run_parallel() | 并行执行两路抽取 |
| save_extract_artifact() | 保存 `field_extract_result.json` |
### 12.5 FieldMergeService
| 方法 | 说明 |
| --- | --- |
| normalize_fields() | 字段名、单位、空白和同义词归一 |
| rank_sources() | 按说明书、产品技术要求、检验报告等来源排序 |
| merge() | 输出最终字段 |
| detect_conflicts() | 输出冲突列表和高亮标记 |
### 12.6 WordTemplateFillService
| 方法 | 说明 |
| --- | --- |
| fill_table_rows() | 根据行名定位表格单元格并写入 |
| fill_placeholders() | 替换段落占位符 |
| fill_checklist_items() | 写入安全和性能基本原则清单 |
| apply_conflict_highlight() | 黄底红字标记冲突字段 |
| save_docx() | 保存填好后的 Word |
### 12.7 TraceabilityExportService
| 方法 | 说明 |
| --- | --- |
| build_excel() | 生成字段来源追溯清单 |
| build_json() | 生成结构化追溯 JSON |
| create_export_records() | 写入 `ExportedSummaryFile` |
### 12.8 FormFillNotifier
复用或包装 `FeishuNotifier`
| 通知场景 | 说明 |
| --- | --- |
| 填表成功 | 通知上传人文件已生成 |
| 部分成功 | 通知 Word 已生成,但 PDF/部分模板失败 |
| 冲突字段存在 | 通知中提示存在冲突字段,需下载核对 |
| 失败 | 可选通知失败原因Demo 可只在对话框展示 |
---
## 十三、接口设计
### 13.1 发起自动填表
| 项目 | 内容 |
| --- | --- |
| URL | POST /api/review-agent/application-form-fill/start/ |
| 认证 | 登录用户 |
| 请求 | conversation_id、message_id、file_summary_batch_id 可选、template_codes 可选、output_types 可选 |
| 响应 | batch_id、workflow_type、status、selected_templates |
处理规则:
```text
校验 conversation 属于当前用户
-> 如本次消息带附件,先执行文件汇总
-> 否则查找当前对话最近成功 FileSummaryBatch
-> 创建 ApplicationFormFillBatch
-> 初始化 WorkflowNodeRun
-> 启动 FormFillWorkflowExecutor
-> 返回工作流卡片初始状态
```
### 13.2 查询自动填表状态
| 项目 | 内容 |
| --- | --- |
| URL | GET /api/review-agent/application-form-fill/{batch_id}/ |
| 认证 | 登录用户 |
| 响应 | 批次状态、节点状态、选择模板、冲突摘要、导出文件 |
### 13.3 下载导出文件
继续复用:
| 项目 | 内容 |
| --- | --- |
| URL | GET /api/review-agent/file-summary/exports/{export_id}/download/ |
| 认证 | 登录用户 |
| 响应 | 文件流 |
权限规则:
```text
export_id -> workflow_type/workflow_batch_id -> ApplicationFormFillBatch -> conversation -> user
必须等于当前登录用户,才允许下载。
```
---
## 十四、前端设计
### 14.1 自动填表工作流卡片
前端新增独立卡片类型 `application_form_fill`,展示节点:
| 节点 | 展示文案 |
| --- | --- |
| prepare | 准备资料 |
| template_select | 选择模板 |
| template_copy | 复制模板 |
| field_extract | 抽取字段 |
| conflict_merge | 冲突归并 |
| word_fill | 填写 Word |
| pdf_convert | 转换 PDF |
| output_export | 输出下载 |
| notify | 飞书通知 |
| completed | 已完成 |
### 14.2 对话框结果展示
工作流完成后AI 对话框展示 Markdown 摘要:
```markdown
已生成申报模板自动填表文件。
| 文件 | Word | PDF |
| --- | --- | --- |
| 注册证格式 | 下载 | 待生成 |
| 安全和性能基本原则清单 | 下载 | 待生成 |
| 冲突字段 | 采用值 | 冲突来源 | 处理 |
| --- | --- | --- | --- |
| 储存条件 | 2-8℃保存 | 产品技术要求:-20℃保存 | 已按说明书填入,并在模板中高亮 |
[下载字段来源追溯清单](download-url)
```
### 14.3 指定模板交互
用户可以通过自然语言指定模板。前端无需额外表单,后端意图识别后在卡片中展示本次选择模板。
---
## 十五、事件设计
### 15.1 SSE 事件结构
```json
{
"event": "workflow",
"workflow_type": "application_form_fill",
"batch_id": 3001,
"conversation_id": 1001,
"node_code": "field_extract",
"node_group": "form_fill",
"status": "running",
"progress": 55,
"message": "正在并行抽取模板字段",
"payload": {
"selected_templates": ["registration_certificate", "essential_principles"],
"processed_files": 8,
"total_files": 20
}
}
```
### 15.2 节点进度
| 节点 | 进度口径 |
| --- | --- |
| 准备资料 | 是否找到来源批次 |
| 选择模板 | 模板数量 |
| 复制模板 | 已复制模板数/总模板数 |
| 抽取字段 | 已处理文件数/总文件数 |
| 冲突归并 | 字段数量和冲突数量 |
| 填写 Word | 已生成 Word 数/目标 Word 数 |
| 转换 PDF | 已生成 PDF 数/目标 PDF 数 |
| 输出下载 | 已创建下载记录数 |
| 飞书通知 | 通知状态 |
---
## 十六、异常与降级设计
| 场景 | 处理 |
| --- | --- |
| 无成功文件汇总批次 | 进入 waiting_user提示上传资料 |
| 新附件汇总失败 | 填表工作流不启动或标记失败 |
| 用户指定不适用模板 | 允许生成,摘要提示需人工确认 |
| `.doc` 转换失败 | 该模板失败,其他模板继续 |
| 单字段缺失 | Word 留空,追溯清单记录未提取 |
| 规则和 LLM 冲突 | 按来源优先级合并,冲突高亮 |
| 所有 Word 生成失败 | 批次 failed |
| 部分 Word 生成失败 | 批次 partial_success |
| PDF 转换失败 | 批次 partial_success保留 Word 下载 |
| 飞书通知失败 | 不影响文件下载,记录通知失败 |
---
## 十七、安全设计
| 设计点 | 说明 |
| --- | --- |
| 原始模板保护 | 只读原始模板,所有写入发生在批次工作目录副本 |
| 对话隔离 | 填表批次必须绑定当前 Conversation |
| 文件读取权限 | 只能读取关联 `FileSummaryBatch` 下的文件 |
| 下载权限 | 根据 workflow_type 和 workflow_batch_id 校验当前用户 |
| LLM 输入控制 | 只传必要文本片段和字段上下文,避免发送整包敏感资料 |
| 飞书脱敏 | 通知仅包含生成状态、模板名称、冲突数量和系统内下载提示 |
| 命令调用安全 | LibreOffice/飞书 CLI 使用结构化参数,不拼接用户输入 |
---
## 十八、验收设计
| 序号 | 验收项 | 验收标准 |
| --- | --- | --- |
| 1 | 意图触发 | 用户说“帮我填注册证”等语句可触发 `application_form_fill` |
| 2 | 指定模板 | 用户指定模板时只生成指定模板 |
| 3 | 默认模板 | 未指定模板时按注册类型生成适用的全部模板 |
| 4 | 新附件串联 | 本次消息带附件时先自动汇总,再执行填表 |
| 5 | 最近批次复用 | 无附件时复用当前对话最近成功文件汇总批次 |
| 6 | 工作流卡片 | 前端展示准备资料、选择模板、复制模板、抽取字段、填写 Word 等节点 |
| 7 | 字段并行抽取 | 规则/正则和 LLM 抽取结果均进入过程产物 |
| 8 | 冲突归并 | 说明书优先,冲突字段在 Word 中黄底红字 |
| 9 | 缺失字段 | 未提取字段在 Word 中留空 |
| 10 | 基本原则清单 | 高置信度条目写入,低置信度候选留在追溯清单 |
| 11 | Word 下载 | 对话框提供填好后的 Word 下载链接 |
| 12 | PDF 待办 | Demo 阶段 PDF 可展示为待生成,不阻断 Word |
| 13 | 追溯清单 | 生成字段来源追溯清单包含规则、LLM、合并和冲突信息 |
| 14 | 飞书通知 | 填表完成后可通知上传人,失败不影响下载 |
| 15 | 权限隔离 | A 对话生成的 Word/追溯清单不能被 B 对话访问 |
---
## 十九、实施建议
1. 新增 `ApplicationFormFillBatch``ApplicationFormFillArtifact` 数据模型,扩展 `ExportedSummaryFile.ExportType` 支持 `word``pdf`
2. 新增模板配置 `application_form_templates_v1.yaml`,先录入注册证格式 `.docx` 的已识别字段。
3. 将两个 `.doc` 模板转换为 `.docx` 工作模板,或在配置中标记为待转换模板。
4. 实现 `TemplateSelectionService`,支持用户指定模板、注册类型识别和默认模板选择。
5. 实现规则/正则与 LLM 并行字段抽取,并保存 `field_extract_result.json`
6. 实现 `FieldMergeService`,按说明书优先规则处理冲突。
7. 实现 `WordTemplateFillService`,优先支持表格行填充和冲突高亮。
8. 实现追溯清单 Excel 导出和 Word 下载记录。
9. 改造前端工作流卡片,新增 `application_form_fill` 类型。
10. 接入飞书通知摘要。
11. 将 PDF 转换、逐页版式 QA 和更完整的 `.doc` 模板转换纳入后续待办。
---
## 二十、待办与待确认事项
| 序号 | 项目 | 当前建议 |
| --- | --- | --- |
| 1 | PDF 转换 | 放入待办Demo 优先 Word 下载 |
| 2 | `.doc` 模板转换 | 优先 LibreOffice/soffice不可用时预置 `.docx` 工作模板 |
| 3 | 安全和性能基本原则清单条目拆解 | 需转换模板后补齐 YAML 条目配置 |
| 4 | LLM 结构化抽取提示词 | 需约束输出 JSON schema 和置信度 |
| 5 | 飞书通知渠道 | Demo 可 mock 或 CLI正式版接 Webhook/API |
| 6 | 低置信度阈值 | 建议功能实现阶段先配置为 0.75 |
| 7 | 版式验证 | P1 增加 PDF 页数检查和逐页截图 QA |

View File

@@ -0,0 +1,292 @@
# 飞书通知与问答接入功能设计
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 需求分析文档 | docs/1.需求分析/4.飞书通知与问答接入.md |
| 依赖功能设计 | docs/2.功能设计/1.自动汇总.mddocs/2.功能设计/2.NMPA注册资料法规核查与整改闭环.mddocs/2.功能设计/3.产品关键信息提取与申报文件自动填表.md |
| 功能名称 | 飞书通知与问答接入 |
| 所属模块 | 审核智能体 review_agent |
| 设计日期 | 2026-06-07 |
| 设计版本 | V1.0 |
---
## 一、设计目标
本功能用于将系统内工作流结果通过飞书官方智能体/应用机器人同步到指定个人账号并为后续飞书内问答能力预留数据模型和服务边界。首期实现重点是自动汇总、NMPA 注册资料法规核查与整改闭环、产品关键信息提取与申报文件自动填表三个流程结束后,使用 App ID/App Secret 获取 `tenant_access_token`,调用飞书消息 API 向指定个人账号发送富文本私聊提醒。
首期不实现飞书事件订阅回调和私聊问答,但需要在设计上预留用户映射、查询服务、权限过滤和问答日志能力,保证后续可以平滑扩展到“用户在飞书私聊机器人中查询批次状态、风险摘要、缺失摘要和导出摘要”。
---
## 二、设计范围
### 2.1 本期范围
| 序号 | 范围项 | 说明 |
| --- | --- | --- |
| 1 | 指定个人通知 | 通过飞书官方智能体/应用机器人消息 API 向一个指定个人账号发送通知 |
| 2 | 发起人展示 | 消息正文展示批次发起人或上传人,不做群内 @ |
| 3 | 三流程接入 | 自动汇总、法规核查、自动填表均发送完成通知 |
| 4 | 富文本消息 | 使用飞书富文本格式展示标题、批次、状态、摘要、链接和发起人 |
| 5 | token 管理 | 使用 App ID/App Secret 获取并缓存 tenant_access_token |
| 6 | 通知判重 | 同一批次、同一工作流、同一状态只发送一次 |
| 7 | 通知记录 | 保存摘要、通道、目标、状态、失败原因、发送时间等信息 |
| 8 | 批次详情展示 | 在对应批次详情页展示通知状态和失败原因 |
| 9 | 用户映射管理 | 通过 Django Admin 手工维护系统用户与飞书用户标识,服务后续按发起人私聊和问答身份识别 |
| 10 | 问答预留 | 预留飞书用户映射、查询服务、权限规则和问答日志模型 |
### 2.2 非本期范围
| 序号 | 范围项 | 说明 |
| --- | --- | --- |
| 1 | 飞书私聊问答回调 | 不实现事件订阅接口和问答回复处理 |
| 2 | 群聊 @ 机器人问答 | 不接收群消息,不处理群内权限问题 |
| 3 | 飞书事件订阅回调 | 首期不接收私聊或群聊消息事件 |
| 4 | 复杂消息卡片 | 不做交互式卡片按钮和回调 |
| 5 | 自动后台重试 | 飞书发送失败只记录,不自动重试 |
| 6 | 飞书通讯录同步 | 不自动拉取用户,首期手工维护映射 |
---
## 三、与既有功能的关系
| 既有能力 | 处理方式 | 说明 |
| --- | --- | --- |
| 自动汇总工作流 | 接入通知 | 文件汇总完成后生成摘要通知 |
| 法规核查工作流 | 替换/扩展 mock 通知 | 风险分级和报告生成后发送摘要通知 |
| 自动填表工作流 | 扩展现有 notifier | Word/追溯清单生成后发送摘要通知 |
| 通知记录模型 | 统一扩展 | 现有法规和填表通知记录已存在,本设计建议抽象统一通知服务 |
| 工作流事件 | 复用 | 通知发送结果可作为节点事件或批次附属信息展示 |
| Django Admin | 扩展 | 新增飞书用户映射管理入口 |
---
## 四、总体架构
### 4.1 逻辑架构
```mermaid
flowchart TD
A["业务工作流完成"] --> B["NotificationDispatcher"]
B --> C["WorkflowNotificationBuilder"]
C --> D["ConfiguredPersonalRecipientResolver"]
D --> E["RichTextMessageBuilder"]
E --> F{"通知是否已发送"}
F -->|"已发送"| G["写入/返回重复跳过结果"]
F -->|"未发送"| H{"飞书通知是否启用"}
H -->|"否"| I["写入 mock/未启用记录"]
H -->|"是"| J["FeishuTokenProvider"]
J --> K["获取/复用 tenant_access_token"]
K --> L["FeishuMessageApiClient"]
L --> X["POST /im/v1/messages"]
X --> M["保存通知记录"]
M --> N["批次详情页展示"]
O["后续飞书私聊消息"] -.预留.-> P["FeishuQuestionService"]
P -.预留.-> Q["BatchSummaryQueryService"]
Q -.预留.-> R["权限过滤"]
P -.预留.-> S["FeishuQuestionLog"]
```
### 4.2 模块划分
| 模块 | 责任 |
| --- | --- |
| `notification_dispatcher` | 工作流完成后统一调度通知发送 |
| `workflow_notification_builder` | 将不同工作流批次转换为统一通知上下文 |
| `feishu_recipient_resolver` | 首期读取配置中的个人 open_id/user_id后续支持按系统用户映射解析 |
| `feishu_message_builder` | 构造飞书富文本消息体 |
| `feishu_token_provider` | 使用 App ID/App Secret 获取并缓存 tenant_access_token |
| `feishu_message_api_client` | 调用飞书发送消息 API、处理超时和响应解析 |
| `notification_record_service` | 判重、保存成功/失败/未启用记录 |
| `batch_notification_presenter` | 为批次详情页输出通知状态 |
| `feishu_question_service` | 后续问答预留,解析问题并查询摘要 |
| `batch_summary_query_service` | 后续问答预留,按权限查询批次摘要 |
---
## 五、通知业务流程
### 5.1 主流程
```text
业务工作流进入 success、partial_success 或 failed
-> 工作流调用统一通知服务
-> 通知服务生成 workflow_type、batch_id、status 组成的判重键
-> 检查是否已有同一判重键的成功通知
-> 若已有成功通知,跳过发送并返回 skipped
-> 读取批次、用户、摘要、结果链接
-> 读取配置中的个人 open_id/user_id 作为接收人
-> 构造富文本消息,正文展示批次发起人或上传人
-> 判断 FEISHU_NOTIFY_ENABLED
-> 未启用时写入 mock/disabled 记录
-> 已启用时获取或复用 tenant_access_token
-> 调用飞书消息 API 向指定个人 open_id/user_id 发送消息
-> 发送成功写入 sent/success 记录
-> 发送失败写入 failed 记录,记录错误信息
-> 业务工作流不因通知失败而失败
```
### 5.2 三类工作流通知摘要
| 工作流 | workflow_type | 摘要字段 | 下一步 |
| --- | --- | --- | --- |
| 自动汇总 | `file_summary` | 文件总数、成功解析数、失败/跳过数、导出文件数 | 查看汇总结果或下载 Excel |
| 法规核查 | `regulatory_review` | 风险总数、阻断项数、高风险数、中风险数、报告导出状态 | 查看风险报告和整改建议 |
| 自动填表 | `application_form_fill` | 选中模板数、导出文件数、冲突字段数、失败原因概述 | 下载 Word/追溯清单并人工确认 |
### 5.3 通知状态
| 状态 | 含义 | 是否阻断主流程 |
| --- | --- | --- |
| pending | 已创建记录但未发送 | 否 |
| sent/success | 已成功发送到飞书 | 否 |
| failed | 发送失败或配置异常 | 否 |
| skipped_duplicate | 已存在同一批次、同一流程、同一状态通知 | 否 |
| disabled/mock | 真实通知未启用,记录为模拟或未启用 | 否 |
---
## 六、飞书富文本设计
### 6.1 消息结构
飞书富文本消息建议使用 `post` 类型。首期内容只放摘要,不展开完整风险项和缺失项。
```json
{
"msg_type": "post",
"content": {
"post": {
"zh_cn": {
"title": "自动填表流程已完成",
"content": [
[
{"tag": "text", "text": "状态:成功\n"},
{"tag": "text", "text": "批次AFF-20260607-001\n"},
{"tag": "text", "text": "发起人owner\n"}
],
[
{"tag": "text", "text": "摘要:生成 2 个文件,冲突字段 1 个。\n"},
{"tag": "a", "text": "查看系统结果", "href": "http://127.0.0.1:8000/..."}
]
]
}
}
}
}
```
### 6.2 接收人标识优先级
首期接收人来自环境变量配置。若同时配置多个飞书标识,按以下优先级选取:
```text
FEISHU_DEFAULT_USER_OPEN_ID -> FEISHU_DEFAULT_USER_ID
```
若无可用接收人标识,系统不发送真实飞书消息,并记录配置缺失失败。
用户映射表仍保留,用于后续从“固定个人账号”升级为“按发起人私聊”。
### 6.3 系统链接
首期使用本地地址,例如:
```text
http://127.0.0.1:8000/
```
批次详情链接由各工作流已有页面路由或详情接口拼接。部署环境后续再升级为可信域名配置。
---
## 七、配置设计
| 配置项 | 来源 | 是否敏感 | 说明 |
| --- | --- | --- | --- |
| FEISHU_NOTIFY_ENABLED | 环境变量 | 否 | 是否启用真实飞书通知 |
| FEISHU_NOTIFY_CHANNEL | 环境变量 | 否 | 首期为 `feishu_api` |
| FEISHU_APP_ID | 环境变量 | 是 | 飞书智能体/企业自建应用 App ID |
| FEISHU_APP_SECRET | 环境变量 | 是 | 飞书智能体/企业自建应用 App Secret |
| FEISHU_DEFAULT_USER_OPEN_ID | 环境变量 | 否 | 首期指定接收人的飞书 open_id |
| FEISHU_DEFAULT_USER_ID | 环境变量 | 否 | 首期指定接收人的飞书 user_id |
| FEISHU_DEFAULT_TARGET_NAME | 环境变量 | 否 | 固定群展示名称 |
| FEISHU_TENANT_TOKEN_CACHE_SECONDS | 环境变量 | 否 | tenant_access_token 本地缓存秒数 |
| FEISHU_REQUEST_TIMEOUT_SECONDS | 环境变量 | 否 | 默认 5 秒 |
| 系统用户与飞书用户映射 | Django Admin | 部分敏感 | open_id、user_id、mobile |
---
## 八、页面设计
### 8.1 Django Admin
新增飞书用户映射管理:
| 字段 | 列表展示 | 可搜索 | 可过滤 |
| --- | --- | --- | --- |
| system_user | 是 | username | 是 |
| feishu_display_name | 是 | 是 | 否 |
| feishu_open_id | 否 | 是 | 否 |
| feishu_user_id | 否 | 是 | 否 |
| feishu_mobile | 否 | 是 | 否 |
| is_active | 是 | 否 | 是 |
### 8.2 批次详情页
三个流程对应的批次详情或结果区域展示通知状态:
| 展示项 | 说明 |
| --- | --- |
| 通知通道 | mock、feishu_api |
| 通知目标 | 指定个人账号名称或配置名称 |
| 接收人 | 指定个人账号;后续可展示发起人/上传人的飞书展示名 |
| 发送状态 | 成功、失败、未启用、重复跳过 |
| 发送时间 | 成功发送时间 |
| 失败原因 | 配置错误、超时、飞书返回错误等摘要 |
---
## 九、飞书问答预留设计
### 9.1 首期预留能力
| 能力 | 设计说明 |
| --- | --- |
| 用户映射复用 | 后续私聊事件中的飞书用户 ID 可通过映射表关联系统用户 |
| 批次查询服务 | 预留按批次号、工作流类型、最近批次查询摘要的服务 |
| 权限过滤 | 普通用户只查自己发起或上传的批次;管理员可查全部 |
| 问答日志 | 预留日志表或服务接口,记录问题、意图、查询对象和回答摘要 |
### 9.2 后续问答能力边界
| 问题类型 | 首期问答 MVP 是否支持 |
| --- | --- |
| 查最近批次状态 | 是 |
| 查指定批次状态 | 是 |
| 查风险摘要 | 是 |
| 查缺失摘要 | 是 |
| 查导出摘要 | 是 |
| 解释具体整改建议 | 否 |
| 重新发起工作流 | 否 |
---
## 十、验收标准
| 序号 | 验收项 | 标准 |
| --- | --- | --- |
| 1 | 三流程通知 | 自动汇总、法规核查、自动填表完成后均调用统一通知服务 |
| 2 | 个人账号发送 | 配置 App ID、App Secret 和指定个人 open_id/user_id 后,个人飞书账号能收到富文本通知 |
| 3 | 发起人展示 | 消息正文能展示流程发起人或上传人 |
| 4 | 接收人缺失 | 指定接收人缺失时不发送真实消息,并记录配置错误 |
| 5 | token 管理 | 系统能获取并缓存 tenant_access_tokentoken 失效后可重新获取 |
| 6 | 判重 | 同一批次、同一流程、同一状态不会重复发送成功通知 |
| 7 | 失败不阻断 | 飞书接口失败时主工作流仍完成 |
| 8 | 记录落库 | 成功、失败、未启用、重复跳过均可追溯 |
| 9 | 页面展示 | 批次详情页展示通知状态和失败原因 |
| 10 | 问答预留 | 用户映射、查询服务边界和日志设计可支持后续私聊问答 |

View File

@@ -0,0 +1,873 @@
# 第1章监管信息材料包生成功能设计
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 需求分析文档 | docs/1.需求分析/5.第1章监管信息材料包生成.md |
| 参考功能设计 | docs/2.功能设计/3.产品关键信息提取与申报文件自动填表.md |
| 功能名称 | 第1章监管信息材料包生成 |
| 工作流编码 | regulatory_info_package |
| 所属模块 | 审核智能体 review_agent |
| 设计日期 | 2026-06-10 |
| 设计版本 | V1.0 |
---
## 一、设计目标
新增独立工作流 `regulatory_info_package`用于根据产品说明书生成第1章监管信息材料包。用户在对话中上传或选择一个产品说明书发送“根据说明书生成第1章监管信息”等指令后系统复制 `docs/0.原始材料/第1章 监管信息` 下的 7 个样例模板抽取说明书中的产品关键信息生成一套新的第1章监管信息文件并打包为 `第1章 监管信息(预生成版).zip` 作为主下载入口。
本功能与 `application_form_fill` 平级,不复用其 workflow_type 和批次表但复用其已形成的服务思想和部分可拆能力包括字段抽取、LLM 调用、Word 写入、追溯清单、导出下载、通知、工作流事件和前端卡片。
本期重点实现:
| 目标 | 说明 |
| --- | --- |
| 独立工作流 | 新增 `regulatory_info_package` 批次、节点和卡片 |
| 单说明书输入 | 直接从当前对话 active 附件中选择唯一说明书;兼容最近成功文件汇总批次 |
| 模板驱动 | 通过 YAML 配置维护 7 个模板、字段映射和生成策略 |
| 模板字段化 | 优先使用 Word 内容控件 Tag 或稳定占位符,让代码只写字段值,最大限度保留原格式 |
| 规则 + LLM 并行抽取 | 代码抽取与 LLM 抽取并行,合并后写入模板 |
| 待确认高亮 | 系统新填入的 `/`、LLM-only 字段、冲突字段均高亮 |
| `.doc` 等价处理 | 设计 `LegacyWordDocumentService`,按能力驱动提供与 `.docx` 一致的文档操作接口;原生能力不可用时明确兜底 |
| zip 主输出 | 扩展 `ExportedSummaryFile.ExportType.ZIP`,统一下载权限 |
| LLM 意图路由 | 扩展路由 action支持固定话术和 LLM 语义判断 |
---
## 二、规范依据与裁决
| 规范来源 | 命中内容 | 设计处理 |
| --- | --- | --- |
| GYRX 后端开发规范 | 服务层职责清晰、接口响应统一、记录必要日志 | Django 项目沿用现有 JsonResponse/SSE 模式;服务拆入独立模块,记录批次与节点日志 |
| GYRX 前端开发规范 | 前端样式复用、交互一致、下载图标语义 | 当前项目为 Django 模板 + 原生 JS按现有工具 chip、工作流卡片和下载链接风格扩展 |
| 既有自动填表设计 | 独立工作流、YAML 配置、字段抽取、追溯清单、导出记录 | 复用模式,不复用批次表和 workflow_type |
| 需求分析确认 | `.doc` 不只依赖转换、zip 主入口、LLM-only 高亮 | 在服务抽象和验收标准中作为强约束 |
冲突裁决GYRX 规范中部分 Java/Spring 约束不适用于当前 Django 项目,按当前项目既有 Django 架构落地;通用原则如服务拆分、日志、权限和前端交互一致性继续采用。
---
## 三、与既有功能关系
### 3.1 复用边界
| 能力 | 处理方式 | 现有代码/模块 |
| --- | --- | --- |
| 对话与消息 | 复用 | `Conversation``Message``stream_message` |
| 附件上传 | 复用 | `FileAttachment``file_summary.storage` |
| 文件汇总结果 | 兼容复用 | `FileSummaryBatch``FileSummaryItem` |
| 文本抽取 | 复用并扩展 | `regulatory_review/services/text_extract.py``rag_index.py` |
| LLM 调用 | 复用 | `review_agent/llm.py` |
| 知识库搜索 | 复用系统现有能力 | `knowledge_base.py`、法规 RAG 相关服务 |
| 导出下载 | 扩展复用 | `ExportedSummaryFile``file_summary.views.export_download` |
| 工作流事件 | 复用 | `WorkflowNodeRun``WorkflowEvent` |
| 通知 | 复用统一通知链路 | `review_agent.notifications` |
| 前端卡片 | 扩展复用 | `templates/home.html``static/js/app.js` |
### 3.2 新增边界
| 能力 | 说明 |
| --- | --- |
| 独立批次 | 新增 `RegulatoryInfoPackageBatch`,批次号 `RIP-...` |
| 独立产物 | 新增 `RegulatoryInfoPackageArtifact` 记录模板副本、抽取结果、生成文件、zip 和追溯清单 |
| 独立通知记录 | 新增 `RegulatoryInfoPackageNotificationRecord`,结构与自动填表通知保持一致 |
| 模板配置 | 新增 `regulatory_info_package_templates_v1.yaml` |
| 说明书选择 | 新增输入选择服务,优先从 active 附件选择,兼容文件汇总批次 |
| 材料包生成 | 新增 7 个文件的生成策略和 zip 打包服务 |
| `.doc` 适配 | 新增旧版 Word 文档适配层 |
---
## 四、总体架构
### 4.1 目录结构
新增模块:
```text
review_agent/
regulatory_info_package/
__init__.py
constants.py
schemas.py
storage.py
events.py
workflow.py
views.py
services/
__init__.py
input_select.py
template_config.py
template_repository.py
instruction_extract.py
field_extract.py
field_merge.py
standard_candidates.py
document_writer.py
docx_document.py
legacy_doc_document.py
package_generate.py
traceability_export.py
zip_export.py
summary.py
notifier.py
templates/
regulatory_info_package_templates_v1.yaml
prompts/
field_extract.md
router_intent.md
standard_candidate.md
```
### 4.2 逻辑架构
```mermaid
flowchart TD
A["AI 对话页"] --> B["意图路由"]
B --> C{"action = regulatory_info_package"}
C --> D["RegulatoryInfoPackageBatch"]
D --> E["RegulatoryInfoPackageWorkflowExecutor"]
E --> F["输入说明书选择"]
E --> G["模板配置 YAML"]
F --> H["说明书文本与表格抽取"]
H --> I1["规则/代码抽取"]
H --> I2["LLM 结构化抽取"]
I1 --> J["字段合并与高亮决策"]
I2 --> J
J --> K["标准候选服务"]
J --> L["材料包生成服务"]
K --> L
L --> M1["DOCX 文档适配器"]
L --> M2["Legacy DOC 文档适配器"]
M1 --> N["7 个目标文件"]
M2 --> N
N --> O["追溯清单"]
N --> P["ZIP 打包"]
O --> Q["ExportedSummaryFile"]
P --> Q
E --> R["WorkflowEvent/SSE"]
E --> S["通知服务"]
```
### 4.3 技术选型
| 设计项 | 本期方案 | 说明 |
| --- | --- | --- |
| Web 框架 | Django | 沿用当前项目 |
| 工作流执行 | 轻量 Executor + 后台线程 | 与文件汇总、法规核查、自动填表一致 |
| 工作流状态 | `WorkflowNodeRun``WorkflowEvent` | 使用 `workflow_type=regulatory_info_package` |
| 模板配置 | YAML | 便于维护 7 个模板和字段映射 |
| `.docx` 操作 | `python-docx` | 表格、段落、run、底色和字体可控 |
| `.doc` 操作 | 适配器抽象 | Python 标准库不支持 `.doc` 二进制 Word 写入;设计为 COM/UNO/第三方库适配器,能力不可用时使用可追溯的 `.docx` 兜底 |
| zip 打包 | Python `zipfile` 标准库 | 标准库可满足打包需求 |
| Excel 追溯 | `openpyxl` | 复用现有依赖 |
| LLM | `review_agent.llm.generate_completion` | 统一模型调用 |
| 知识库 | 系统现有知识库/RAG | 不新增单独 RAG 模块 |
关于 `.doc`Python 自带库不能实现类似 Apache POI HWPF 的 Word 97-2003 二进制文档完整读写。项目依赖中有 `olefile`,可读取 OLE 复合文档结构,但不足以可靠修改 Word 文本、表格和样式。因此设计上必须使用文档适配器屏蔽实现差异,底层可选 Word COM、LibreOffice UNO、专用第三方库或受控转换兜底。
---
## 五、触发与路由设计
### 5.1 action 扩展
`skill_router.py` 扩展:
| 项 | 设计 |
| --- | --- |
| 新 action | `regulatory_info_package` |
| 新属性 | `starts_regulatory_info_package` |
| ROUTE_ACTIONS | 增加 `regulatory_info_package` |
| LLM prompt | 描述该 action 用于“根据说明书生成第1章监管信息、监管信息材料包、申请表/产品列表/声明材料包” |
### 5.2 固定规则
规则预判关键词:
```python
REGULATORY_INFO_PACKAGE_TRIGGER_KEYWORDS = [
"根据说明书生成第1章监管信息",
"生成监管信息材料包",
"从说明书生成第1章材料",
"第1章监管信息",
"监管信息材料包",
]
```
规则命中时直接进入本工作流。规则未命中时,继续走 LLM 路由判断,避免自然表达漏触发。
### 5.3 对话启动
`review_agent/services.py::stream_message` 增加分支:
```text
if route.starts_regulatory_info_package:
-> 选择说明书输入
-> 创建 RegulatoryInfoPackageBatch
-> start_regulatory_info_package_workflow
-> SSE workflow_started
-> 回复“已启动第1章监管信息材料包生成工作流批次号RIP-...”
```
如果没有 active 附件,也没有可复用的最近文件汇总批次,则回复“请先上传产品说明书”。
如果存在多个候选说明书且用户消息无法唯一命中文件名,则不展示选择弹窗,由对话反问用户确认具体文件名后再启动工作流。
---
## 六、输入选择设计
### 6.1 选择优先级
| 优先级 | 来源 | 规则 |
| --- | --- | --- |
| 1 | 用户消息指定文件名 | 按 active 附件名或可复用文件名模糊匹配,唯一命中则使用 |
| 2 | 当前对话 active 附件 | 文件名包含“说明书”且扩展名为 `.docx` |
| 3 | 当前对话 active 附件 | 唯一 `.docx` 文件 |
| 4 | 最近成功 `FileSummaryBatch.items` | 文件名包含“说明书”且扩展名为 `.docx` |
| 5 | 无法唯一选择 | 对话反问用户确认使用哪个说明书;必要时批次进入 `waiting_user` |
本期直接输入只支持 `.docx` 产品说明书。`.doc`、PDF、扫描件说明书作为后续扩展但输出模板中的 `.doc` 必须支持。
### 6.2 输入绑定
批次记录:
| 字段 | 来源 |
| --- | --- |
| source_attachment | 直接选择的 FileAttachment |
| source_summary_batch | 可选,来自最近成功文件汇总 |
| source_summary_item | 可选,来自汇总条目 |
| source_file_name | 原始说明书文件名 |
| source_storage_path | 说明书存储路径 |
---
## 七、模板配置设计
配置路径:
```text
review_agent/regulatory_info_package/templates/regulatory_info_package_templates_v1.yaml
```
配置结构:
```yaml
version: regulatory_info_package_templates_v1
source_dir: docs/0.原始材料/第1章 监管信息
output_zip_name: 第1章 监管信息(预生成版).zip
templates:
- code: ch1_2_directory
output_name: CH1.2 监管信息目录.docx
source_file: CH1.2 监管信息目录.docx
file_format: docx
strategy: directory
include_in_zip: true
fields:
- key: product_name
targets:
- type: paragraph_contains_replace
match: 呼吸道合胞病毒、肺炎支原体核酸检测试剂盒荧光PCR法
- code: ch1_4_application_form
output_name: CH1.4 申请表.docx
source_file: CH1.4 申请表.docx
file_format: docx
strategy: application_form
include_in_zip: true
- code: ch1_9_pre_submission
output_name: CH1.9 产品申报前沟通的说明.doc
source_file: CH1.9 产品申报前沟通的说明.doc
file_format: doc
strategy: pre_submission
prefer_legacy_doc_native: true
allow_docx_fallback: true
include_in_zip: true
```
字段映射优先级:
| 目标类型 | 说明 |
| --- | --- |
| content_control_tag | 正式模板优先,代码按 Word 内容控件 Tag 写入 |
| placeholder | 过渡方案,替换稳定占位符并保留原 run/段落格式 |
| table_row_label | 未字段化模板的兜底方案,必须保留原单元格格式 |
### 7.1 配置项说明
| 配置项 | 说明 |
| --- | --- |
| version | 配置版本,写入批次 |
| source_dir | 样例模板目录 |
| output_zip_name | zip 主输出文件名 |
| templates | 7 个目标模板 |
| code | 模板编码 |
| output_name | 生成文件名 |
| source_file | 样例文件 |
| file_format | docx/doc |
| strategy | 生成策略 |
| include_in_zip | 是否进入 zip |
| fields | 字段映射与替换目标 |
| prefer_legacy_doc_native | `.doc` 是否优先尝试原生处理能力 |
| allow_docx_fallback | 原生 `.doc` 能力不可用或失败时是否允许 `.docx` 兜底 |
---
## 八、字段抽取设计
### 8.1 说明书解析
`instruction_extract.py` 输出:
| 数据 | 说明 |
| --- | --- |
| paragraphs | 按顺序提取段落 |
| sections | 按 `【章节名】` 切分 |
| tables | 提取表格二维数据 |
| component_tables | 识别主要组成成分表 |
| front_text | 前 4000 字,供 LLM 使用 |
### 8.2 规则抽取
规则抽取覆盖:
| 字段 | 规则 |
| --- | --- |
| product_name | `【产品名称】` 下一段 |
| package_specification | `【包装规格】` 到下一章节 |
| intended_use | `【预期用途】` 到下一章节 |
| detection_principle | `【检测原理】` 到下一章节 |
| main_components | `【主要组成成分】` 表格摘要 |
| storage_condition_and_validity | `【储存条件及有效期】` 到下一章节 |
| sample_type | `样本要求` 中“适用样本类型” |
| detection_targets | 从预期用途/检测原理中抽取基因、病原体、靶标 |
| applicable_instruments | `【适用仪器】` 到下一章节 |
| test_method | `【检验方法】` 摘要 |
| standards | 正则抽取 `GB/T``YY/T``YY``GB` 等标准号 |
### 8.3 LLM 抽取
LLM prompt 要求只输出 JSON
```json
{
"fields": [
{
"key": "product_name",
"label": "产品名称",
"value": "...",
"evidence": "...",
"confidence": 0.9
}
],
"product_list_rows": [
{
"package_specification": "...",
"composition": "...",
"component_name": "...",
"main_component": "...",
"quantity": "..."
}
],
"standards": []
}
```
LLM 不允许填企业信息、分类编码、管理类别、临床评价路径等说明书无法证明的内容。
### 8.4 字段合并
`field_merge.py` 输出 `MergedField`
| 字段 | 说明 |
| --- | --- |
| key | 字段编码 |
| label | 中文名 |
| value | 最终写入值 |
| source | rule、llm、missing、conflict |
| evidence | 来源片段 |
| confidence | 置信度 |
| highlight_reason | none、missing、llm_only、conflict、rag_candidate |
| needs_review | 是否需人工复核 |
合并规则:
| 场景 | 处理 |
| --- | --- |
| rule 与 LLM 一致 | 采用值,不高亮 |
| rule 与 LLM 不一致 | 采用规则优先或配置优先,标记 conflict |
| rule 缺失、LLM 命中 | 采用 LLM 值,标记 llm_only |
| 全部缺失 | 写 `/`,标记 missing |
---
## 九、文档生成设计
### 9.1 文档适配器接口
`document_writer.py` 定义统一接口:
```python
class DocumentAdapter:
def replace_text(self, old: str, new: str, *, highlight: bool = False) -> int: ...
def fill_table_cell(self, row_label: str, value: str, *, highlight: bool = False) -> bool: ...
def replace_table(self, marker: str, rows: list[dict], *, highlight_columns: list[str] = None) -> bool: ...
def highlight_value(self, value: str, reason: str) -> int: ...
def save(self, path: Path) -> Path: ...
```
`.docx` 使用 `DocxDocumentAdapter``.doc` 使用 `LegacyDocDocumentAdapter`
### 9.2 `.docx` 处理
能力:
| 能力 | 实现 |
| --- | --- |
| 段落替换 | 遍历 paragraph runs |
| 表格行填充 | 按首列 label 定位 |
| 单元格高亮 | `w:shd` 黄色底色 |
| 字体颜色 | 冲突项可红色字体 |
| 产品列表重建 | 清空目标表格数据行后追加 |
| 声明日期替换 | 按日期正则或段落末尾替换 |
### 9.3 `.doc` 处理
设计 `LegacyDocDocumentAdapter`,对外提供与 `.docx` 一致能力。底层按可用性选择适配器:
| 适配器 | 定位 |
| --- | --- |
| `WordComDocAdapter` | Windows + Microsoft Word 环境下优先,直接打开 `.doc`、查找替换、设置高亮并保存 `.doc` |
| `LibreOfficeUnoDocAdapter` | LibreOffice UNO/API 环境下使用,直接操作文档模型 |
| `OleDocReadOnlyAdapter` | 仅可读取时用于诊断,不满足写入验收 |
| `ConversionFallbackAdapter` | 兜底路径,可转换为 `.docx` 后处理,但不能作为唯一实现 |
功能设计约束:
| 约束 | 说明 |
| --- | --- |
| 不静默降级 | `.doc` 原生写入失败时必须记录适配器失败原因,随后尝试 `.docx` 兜底;兜底仍失败时该文件失败并触发 partial_success |
| 不只靠转换 | 转换可作为兜底,但设计主路径必须是文档适配器 |
| 能力探测 | 启动时或节点执行时检测适配器可用性 |
| 追溯记录 | 写入 `.doc` 的适配器类型和失败信息写入 artifact metadata |
### 9.4 7 个文件生成策略
| 模板 | 策略服务 | 关键动作 |
| --- | --- | --- |
| CH1.2 监管信息目录 | `generate_directory_doc` | 替换产品名称;页码沿用样例 |
| CH1.4 申请表 | `generate_application_form_doc` | 填表格行;缺失字段 `/` 黄底 |
| CH1.5 产品列表 | `generate_product_list_doc` | 使用样例表头重建产品列表;货号 `/` 黄底 |
| CH1.9 申报前沟通说明 | `generate_pre_submission_doc` | `.doc` 原生替换产品名和公司名;原生失败则输出 `.docx` 兜底文件;两者均失败才不进入 zip |
| CH1.11.1 符合标准清单 | `generate_standard_list_doc` | 说明书标准号直接写;候选/缺失高亮 |
| CH1.11.5 真实性声明 | `generate_authenticity_statement_doc` | 保留正文,替换产品名,公司名 `/` 黄底,日期当天 |
| CH1.11.6 符合性声明 | `generate_compliance_statement_doc` | 保留正文,替换产品名,公司名 `/` 黄底,日期当天 |
`generate_docs` 节点内部允许多线程并发处理 7 个目标文件。每个文档使用独立模板副本,子线程只返回生成结果,数据库 artifact/export 记录由主线程统一写入,避免并发写库和共享文件冲突。
---
## 十、标准清单设计
系统中已有知识库/RAG 能力,不新增单独 RAG 模块。本功能只新增 `standard_candidates.py` 作为业务服务,调用既有知识库搜索能力。
处理规则:
| 来源 | 处理 |
| --- | --- |
| 说明书明确标准号 | 写入标准清单,记录 `source=instruction` |
| 知识库候选标准 | 可写入候选区或追溯清单,标记 `rag_candidate` 并高亮 |
| 无命中 | 写 `/` 并黄底 |
| 样例标准 | 不无条件沿用 |
查询建议:
```text
体外诊断试剂 核酸扩增 检测试剂 标准 清单
新型冠状病毒 2019-nCoV 核酸检测试剂盒 荧光PCR 标准
```
---
## 十一、zip 与导出设计
### 11.1 ExportType 扩展
`ExportedSummaryFile.ExportType` 增加:
```python
ZIP = "zip", "ZIP"
```
下载 content type 增加:
```python
"zip": "application/zip"
```
### 11.2 导出记录
| 文件 | export_category | export_type |
| --- | --- | --- |
| 第1章 监管信息(预生成版).zip | regulatory_info_package | zip |
| 7 个生成文件 | generated_document | word 或 legacy_word |
| 追溯清单 Excel | traceability | excel |
追溯 JSON 和抽取过程 JSON 只保存到后台 `logs/` 目录和 artifact 记录,不作为用户下载入口。用户侧只提供追溯 Excel 下载。
如果不新增 `legacy_word` export_type`.doc` 也可暂用 `word`,通过文件扩展名和 content type 判断下载 MIME。功能设计建议新增 content type 映射时按扩展名兜底,避免 `.doc` 被当作 `.docx`
### 11.3 权限
`file_summary.views._export_for_user` 增加:
```text
if exported.workflow_type == "regulatory_info_package":
查询 RegulatoryInfoPackageBatch
校验 conversation__user == request.user 且 is_deleted=False
```
---
## 十二、数据模型设计
### 12.1 RegulatoryInfoPackageBatch
```python
class RegulatoryInfoPackageBatch(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 | FK Conversation | 所属对话 |
| user | FK User | 发起用户 |
| trigger_message | FK Message | 触发消息 |
| source_attachment | FK FileAttachment | 直接选中的说明书附件 |
| source_summary_batch | FK FileSummaryBatch | 可选文件汇总批次 |
| source_summary_item_id | PositiveBigIntegerField | 可选汇总条目 ID |
| batch_no | CharField unique | RIP 批次号 |
| status | CharField | 状态 |
| source_file_name | CharField | 说明书原文件名 |
| source_storage_path | CharField | 说明书路径 |
| product_name | CharField | 抽取产品名 |
| output_zip_name | CharField | zip 文件名 |
| generated_files | JSONField | 7 个文件状态 |
| missing_fields | JSONField | 缺失项 |
| llm_only_fields | JSONField | LLM-only 项 |
| conflict_fields | JSONField | 冲突项 |
| risk_notes | JSONField | 风险提示 |
| template_config_version | CharField | 配置版本 |
| template_config_hash | CharField | 配置 hash |
| adapter_summary | JSONField | `.doc`/`.docx` 适配器信息 |
| work_dir | CharField | 工作目录 |
| error_message | TextField | 错误信息 |
| started_at/finished_at | DateTimeField | 执行时间 |
| is_deleted | BooleanField | 软删除 |
索引:
| 索引 | 字段 |
| --- | --- |
| idx_ra_rip_batch_conv_status | conversation, status |
| idx_ra_rip_batch_user_created | user, created_at |
| idx_ra_rip_batch_attachment | source_attachment |
| idx_ra_rip_batch_summary | source_summary_batch |
### 12.2 RegulatoryInfoPackageArtifact
产物类型:
| 类型 | 说明 |
| --- | --- |
| template_copy | 模板副本 |
| instruction_extract | 说明书抽取结果 |
| field_extract_result | 字段抽取结果 |
| merged_fields | 合并字段 |
| generated_document | 生成文件 |
| traceability | 追溯清单 |
| zip_package | zip 包 |
| notification_record | 通知记录 |
字段与 `ApplicationFormFillArtifact` 保持一致:`batch``artifact_type``file_format``name``file_name``storage_path``file_size``content_hash``metadata``created_by_node``is_deleted`
`file_format` 增加 `DOC``ZIP`
### 12.3 RegulatoryInfoPackageNotificationRecord
结构对齐 `ApplicationFormFillNotificationRecord`
| 字段 | 说明 |
| --- | --- |
| batch | 所属 RIP 批次 |
| recipient | 通知对象 |
| channel | feishu_cli、feishu_api、mock |
| export_ids | 导出 ID |
| message_summary | 通知摘要 |
| send_status | pending、success、failed |
| retry_count | 重试次数 |
| external_message_id | 外部消息 ID |
| error_message | 错误 |
| sent_at | 发送时间 |
---
## 十三、工作流设计
### 13.1 节点定义
| 节点编码 | 节点名称 | 触发服务 | 成功条件 | 失败处理 |
| --- | --- | --- | --- | --- |
| prepare | 准备资料 | `RegulatoryInfoPackageWorkflowExecutor` | 找到唯一说明书 | 缺失或多候选进入 waiting_user |
| template_copy | 复制模板 | `TemplateRepository` | 7 个模板进入批次目录 | 缺关键模板则 failed |
| text_extract | 抽取说明书 | `InstructionExtractService` | 提取文本、章节和表格 | 失败则 failed |
| field_extract | 抽取字段 | `FieldExtractionService` | 规则/LLM 结果留底 | LLM 失败可继续 |
| field_merge | 合并字段 | `FieldMergeService` | 输出 merged_fields | 无产品名仍继续,产品名 `/` |
| generate_docs | 生成材料 | `PackageGenerateService` | 生成 7 个文件 | 单文件失败可 partial_success |
| highlight_review_items | 标记待确认 | 文档适配器 | 缺失/LLM-only/冲突完成高亮 | 失败则对应文件失败 |
| trace_export | 追溯清单 | `TraceabilityExportService` | 生成 Excel/JSON | 不阻断 zip |
| zip_export | 打包下载 | `ZipExportService` | 生成 zip 并创建导出记录 | zip 失败则保留单文件 |
| notify | 通知 | `Notifier` | 写通知记录 | 不阻断下载 |
| completed | 完成 | Executor | 状态落定、摘要写入对话 | - |
### 13.2 状态落定
| 结果 | 批次状态 |
| --- | --- |
| 7 个文件、zip、追溯清单均成功 | success |
| zip 成功但部分单文件/追溯/通知失败 | partial_success |
| 单文件成功但 zip 失败 | partial_success |
| 关键输入或模板缺失 | failed 或 waiting_user |
| 所有目标文件生成失败 | failed |
---
## 十四、接口设计
### 14.1 URL
```text
GET /api/review-agent/regulatory-info-package/health/
POST /api/review-agent/regulatory-info-package/start/
GET /api/review-agent/regulatory-info-package/<batch_id>/status/
POST /api/review-agent/regulatory-info-package/<batch_id>/select-input/
```
### 14.2 start
请求:
```json
{
"conversation_id": 1,
"attachment_id": 10,
"file_summary_batch_id": 20,
"source_summary_item_id": 30
}
```
响应:
```json
{
"batch_id": 1,
"workflow_type": "regulatory_info_package",
"batch_no": "RIP-20260610153000-abcdef",
"status": "pending"
}
```
### 14.3 status
响应包含:
| 字段 | 说明 |
| --- | --- |
| batch | 批次基础信息、产品名、缺失数、LLM-only 数、冲突数 |
| nodes | 工作流节点 |
| generated_files | 7 个文件状态 |
| exports | zip、单文件、追溯清单下载 |
| missing_fields | 缺失项摘要 |
| llm_only_fields | LLM-only 摘要 |
| conflict_fields | 冲突摘要 |
| risk_notes | 风险提示 |
| notifications | 通知记录 |
---
## 十五、前端设计
### 15.1 对话框底部快捷提示
`templates/home.html` 增加 tool chip
```text
根据说明书生成第1章监管信息
```
点击后填入 prompt不自动发送保持现有交互一致。
### 15.2 工作流卡片
`build_workflow_cards()` 增加 RIP 批次,前端复用现有卡片样式,展示:
| 信息 | 说明 |
| --- | --- |
| 批次号 | RIP-... |
| 状态 | pending/running/success/partial_success/failed |
| 风险摘要 | 缺失字段 N、LLM复核 N、提示 N |
| 节点 | RIP 节点 |
### 15.3 状态轮询
`summaryPanel` 增加:
```html
data-regulatory-info-package-status-url-template="/api/review-agent/regulatory-info-package/__batch_id__/status/"
```
`static/js/app.js` 在工作流类型判断中增加 `regulatory_info_package`
### 15.4 结果展示
状态 payload 中 `exports` 按类别展示:
| 类别 | 展示 |
| --- | --- |
| zip | 主下载按钮 |
| generated_document | 单文件下载列表 |
| traceability | 追溯清单下载 |
---
## 十六、通知设计
复用统一通知服务,新增 `build_regulatory_info_package_context(batch)`
| 摘要项 | 说明 |
| --- | --- |
| 工作流 | 第1章监管信息材料包生成 |
| 批次号 | RIP-... |
| 产品名称 | 抽取产品名 |
| 导出文件 | zip + 单文件数量 |
| 待确认 | 缺失项、LLM-only、冲突项数量 |
| 下载提示 | 进入系统下载 zip |
通知失败不影响下载。
---
## 十七、异常与降级
| 异常 | 处理 |
| --- | --- |
| 未找到说明书 | 返回提示,不创建或创建 waiting_user 批次 |
| 多说明书候选 | waiting_user等待选择 |
| YAML 配置错误 | failed提示配置错误 |
| 样例模板缺失 | failed列出缺失模板 |
| LLM 失败 | 使用规则抽取继续,写 risk_notes |
| 规则抽取为空 | 使用 LLM-only 继续并高亮 |
| 知识库不可用 | 标准清单填 `/` 并高亮,写 risk_notes |
| `.doc` 适配器不可用 | CH1.9 失败,批次 partial_success 或 failed明确原因 |
| zip 打包失败 | 保留单文件下载,状态 partial_success |
| 下载文件不存在 | 返回 404记录日志 |
---
## 十八、安全与权限
| 控制点 | 设计 |
| --- | --- |
| 批次访问 | `conversation__user == request.user` |
| 附件访问 | 附件必须属于当前对话和当前用户 |
| 汇总批次访问 | 批次必须属于当前对话和当前用户 |
| 导出下载 | `workflow_type=regulatory_info_package` 时反查 RIP 批次 |
| 工作目录 | `media/regulatory_info_package/{user_id}/{conversation_id}/{batch_no}` |
| 路径安全 | 所有复制/输出路径必须校验位于批次工作目录内 |
| 原始模板保护 | 只读复制,不允许覆盖 `docs/0.原始材料` |
---
## 十九、测试设计
| 测试文件 | 覆盖 |
| --- | --- |
| `tests/test_regulatory_info_package_models.py` | 批次、产物、通知、zip 导出类型 |
| `tests/test_regulatory_info_package_trigger.py` | 固定规则与 LLM 路由 |
| `tests/test_regulatory_info_package_input_select.py` | 说明书选择、多候选 waiting_user |
| `tests/test_regulatory_info_package_template_config.py` | YAML 加载、模板存在性校验 |
| `tests/test_regulatory_info_package_field_extract.py` | 说明书字段、表格、标准号抽取 |
| `tests/test_regulatory_info_package_field_merge.py` | missing、llm_only、conflict 高亮决策 |
| `tests/test_regulatory_info_package_docx_writer.py` | docx 替换、表格填充、黄底 |
| `tests/test_regulatory_info_package_legacy_doc.py` | `.doc` 适配器能力探测和失败提示 |
| `tests/test_regulatory_info_package_zip.py` | zip 只包含 success/fallback_success 文件 |
| `tests/test_regulatory_info_package_workflow.py` | 工作流节点和状态落定 |
| `tests/test_regulatory_info_package_views.py` | start/status/权限 |
| `tests/test_regulatory_info_package_frontend.py` | 卡片、快捷提示、状态 URL |
回归测试:
```bash
python manage.py check
pytest tests/test_application_form_fill_*.py tests/test_file_summary_views.py tests/test_regulatory_*tests.py
```
实际执行时按项目现有测试命名拆分运行。
---
## 二十、实施顺序建议
| 阶段 | 内容 |
| --- | --- |
| RIP-1 | 模型、迁移、ExportType.ZIP、下载权限 |
| RIP-2 | 模块骨架、YAML 配置、输入说明书选择 |
| RIP-3 | 路由 action、对话启动、工作流节点 |
| RIP-4 | 说明书文本/表格抽取、规则 + LLM 字段抽取 |
| RIP-5 | docx 文档生成、黄底高亮、产品列表重建 |
| RIP-6 | `.doc` 适配器、CH1.9 处理能力 |
| RIP-7 | 追溯清单、zip 导出、助手摘要 |
| RIP-8 | 前端卡片、快捷提示、状态轮询 |
| RIP-9 | 通知、权限、全量回归 |
---
## 二十一、待确认与风险
| 风险 | 说明 | 建议 |
| --- | --- | --- |
| `.doc` 原生写入难度 | Python 标准库不支持 Word `.doc` 完整写入 | 优先调研 Word COM 或 LibreOffice UNO无原生能力时允许可追溯 `.docx` 兜底 |
| 模板字段化工作量 | 需要先把样例模板整理为代码可识别字段 | 优先覆盖 CH1.4、CH1.5 和声明类关键字段;缺少 Tag 时通过模板审计提前暴露 |
| 样例模板文本碎片 | Word run 拆分可能导致简单字符串替换失败 | 文档写入服务需支持跨 run 替换 |
| 产品列表结构复杂 | 说明书表格可能存在合并单元格和多规格 | 先覆盖目标说明书结构,再扩展通用表格归一化 |
| 标准清单准确性 | 说明书未必包含标准号,知识库候选不能直接作为结论 | 候选全部高亮并进入追溯清单 |
| LLM-only 风险 | LLM 推断可能过度补全 | 写入但高亮,追溯清单标记需复核 |
---
## 二十二、设计结论
| 编号 | 结论 |
| --- | --- |
| D1 | 功能设计文档新增为 `docs/2.功能设计/5.第1章监管信息材料包生成.md` |
| D2 | 新增独立模块 `review_agent/regulatory_info_package/` |
| D3 | 新建独立批次、产物、通知三张表 |
| D4 | 输入选择以 active 附件为主,兼容最近成功文件汇总批次 |
| D5 | `ExportedSummaryFile.ExportType` 扩展 `zip` |
| D6 | 采用 YAML 配置驱动 7 个模板 |
| D7 | 模板字段优先使用内容控件 Tag 或稳定占位符,行标签定位仅作为兜底 |
| D8 | `.doc` 通过 `LegacyWordDocumentService` 适配器实现与 `.docx` 等价接口,原生能力不可用时允许可追溯兜底 |
| D9 | 标准候选复用系统已有知识库/RAG不新增独立 RAG |
| D10 | 前端只扩展现有对话页、工作流卡片、快捷提示和状态轮询 |
| D11 | 本轮先产出功能设计;数据库设计先在本文档中给出,后续可拆成正式数据库设计文档 |

View File

@@ -0,0 +1,651 @@
# 自动汇总文件夹文件目录与页数流程数据库设计
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 需求分析文档 | docs/1.需求分析/1.自动汇总.md |
| 功能设计文档 | docs/2.功能设计/1.自动汇总.md |
| 详细设计文档 | docs/3.详细设计/1.自动汇总.md |
| 数据库类型 | SQLite / Django ORM |
| 表名前缀 | ra_ |
| 设计日期 | 2026-06-05 |
| 设计版本 | V1.0 |
---
## 一、设计原则
| 原则 | 说明 |
| --- | --- |
| ORM 优先 | 当前项目使用 Django实际落地以 Django Model 与 migration 为准 |
| SQLite 兼容 | 字段类型、索引和约束优先保证 SQLite 可运行 |
| 短表名前缀 | 使用 `ra_` 作为审核智能体文件汇总相关表前缀 |
| 不建枚举表 | 状态枚举使用 Django `TextChoices`,数据库存储字符串 |
| 对话隔离 | 所有附件、批次、导出文件均可追溯到 Conversation 和 User |
| 多版本附件 | 同一对话同名附件允许多次上传,以版本号区分 |
| 批次固化 | 每次汇总批次通过中间表绑定本次使用的附件版本,防止串文件 |
| 事件留痕 | 保留 WorkflowEvent用于 SSE 断线续传、页面刷新恢复和排查问题 |
---
## 二、ER 图
```mermaid
erDiagram
AUTH_USER ||--o{ CONVERSATION : owns
CONVERSATION ||--o{ MESSAGE : contains
CONVERSATION ||--o{ RA_FILE_ATTACHMENT : has
CONVERSATION ||--o{ RA_FILE_SUMMARY_BATCH : has
AUTH_USER ||--o{ RA_FILE_ATTACHMENT : uploads
AUTH_USER ||--o{ RA_FILE_SUMMARY_BATCH : runs
MESSAGE ||--o{ RA_FILE_SUMMARY_BATCH : triggers
RA_FILE_SUMMARY_BATCH ||--o{ RA_FILE_SUMMARY_BATCH_ATTACHMENT : binds
RA_FILE_ATTACHMENT ||--o{ RA_FILE_SUMMARY_BATCH_ATTACHMENT : selected
RA_FILE_SUMMARY_BATCH ||--o{ RA_FILE_SUMMARY_ITEM : produces
RA_FILE_SUMMARY_BATCH ||--o{ RA_WORKFLOW_NODE_RUN : tracks
RA_FILE_SUMMARY_BATCH ||--o{ RA_WORKFLOW_EVENT : emits
RA_FILE_SUMMARY_BATCH ||--o{ RA_EXPORTED_SUMMARY_FILE : exports
```
---
## 三、表结构设计
### 3.1 ra_file_attachment
用户在对话右侧上传区上传后的附件记录。上传即存储,不代表已启动工作流。
| 字段名 | Django 类型 | SQLite 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| id | BigAutoField | integer | 是 | 主键 |
| conversation_id | ForeignKey | bigint | 是 | 绑定对话 |
| user_id | ForeignKey | bigint | 是 | 上传用户 |
| original_name | CharField(255) | varchar(255) | 是 | 原始文件名 |
| version_no | PositiveIntegerField | integer | 是 | 同一对话同名文件版本号,从 1 递增 |
| is_active | BooleanField | bool | 是 | 是否当前默认版本 |
| storage_path | CharField(500) | varchar(500) | 是 | 文件存储路径 |
| file_size | BigIntegerField | bigint | 是 | 文件大小 |
| content_type | CharField(120) | varchar(120) | 否 | MIME 类型 |
| upload_status | CharField(20) | varchar(20) | 是 | uploaded、bound、deleted |
| created_at | DateTimeField | datetime | 是 | 上传时间 |
唯一约束:
| 约束名 | 字段 |
| --- | --- |
| uq_ra_attachment_conv_name_version | conversation_id, original_name, version_no |
索引:
| 索引名 | 字段 | 说明 |
| --- | --- | --- |
| idx_ra_attachment_conv_created | conversation_id, created_at | 查询对话附件列表 |
| idx_ra_attachment_user_created | user_id, created_at | 查询用户上传记录 |
| idx_ra_attachment_active | conversation_id, original_name, is_active | 查询当前默认版本 |
---
### 3.2 ra_file_summary_batch
一次文件目录与页数汇总工作流批次。
| 字段名 | Django 类型 | SQLite 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| id | BigAutoField | integer | 是 | 主键 |
| conversation_id | ForeignKey | bigint | 是 | 绑定对话 |
| user_id | ForeignKey | bigint | 是 | 执行用户 |
| trigger_message_id | ForeignKey | bigint | 否 | 触发工作流的用户消息 |
| batch_no | CharField(64) | varchar(64) | 是 | 批次编号,唯一 |
| product_name | CharField(200) | varchar(200) | 否 | 识别出的产品名称 |
| status | CharField(20) | varchar(20) | 是 | pending、running、success、failed |
| total_files | IntegerField | integer | 是 | 文件总数 |
| supported_files | IntegerField | integer | 是 | 支持统计文件数 |
| success_files | IntegerField | integer | 是 | 统计成功文件数 |
| failed_files | IntegerField | integer | 是 | 统计失败文件数 |
| unsupported_files | IntegerField | integer | 是 | 不支持文件数 |
| uncertain_files | IntegerField | integer | 是 | 页数不可确定文件数 |
| total_pages | IntegerField | integer | 是 | 总页数 |
| work_dir | CharField(500) | varchar(500) | 否 | 批次工作目录 |
| error_message | TextField | text | 否 | 批次异常说明 |
| created_at | DateTimeField | datetime | 是 | 创建时间 |
| started_at | DateTimeField | datetime | 否 | 开始时间 |
| finished_at | DateTimeField | datetime | 否 | 完成时间 |
唯一约束:
| 约束名 | 字段 |
| --- | --- |
| uq_ra_batch_no | batch_no |
索引:
| 索引名 | 字段 | 说明 |
| --- | --- | --- |
| idx_ra_batch_conv_created | conversation_id, created_at | 查询对话下批次 |
| idx_ra_batch_user_created | user_id, created_at | 查询用户批次 |
| idx_ra_batch_status | status, created_at | 查询执行中或失败批次 |
---
### 3.3 ra_file_summary_batch_attachment
批次与附件版本绑定表。一个对话可多次上传同名附件,批次必须固化本次使用的附件版本。
| 字段名 | Django 类型 | SQLite 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| id | BigAutoField | integer | 是 | 主键 |
| batch_id | ForeignKey | bigint | 是 | 汇总批次 |
| attachment_id | ForeignKey | bigint | 是 | 本次使用的附件版本 |
| source_role | CharField(20) | varchar(20) | 是 | archive、multi_file |
| created_at | DateTimeField | datetime | 是 | 绑定时间 |
唯一约束:
| 约束名 | 字段 |
| --- | --- |
| uq_ra_batch_attachment | batch_id, attachment_id |
索引:
| 索引名 | 字段 | 说明 |
| --- | --- | --- |
| idx_ra_batch_attachment_batch | batch_id, created_at | 查询批次附件 |
| idx_ra_batch_attachment_attachment | attachment_id | 查询附件被哪些批次使用 |
---
### 3.4 ra_file_summary_item
文件明细表,记录扫描到的每个文件及页数统计结果。
| 字段名 | Django 类型 | SQLite 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| id | BigAutoField | integer | 是 | 主键 |
| batch_id | ForeignKey | bigint | 是 | 所属批次 |
| file_index | PositiveIntegerField | integer | 是 | 文件序号 |
| directory_level | CharField(300) | varchar(300) | 否 | 目录层级 |
| file_name | CharField(255) | varchar(255) | 是 | 文件名 |
| file_type | CharField(20) | varchar(20) | 是 | 文件类型 |
| relative_path | CharField(500) | varchar(500) | 是 | 相对路径,用于展示和导出 |
| storage_path | CharField(500) | varchar(500) | 是 | 实际处理路径 |
| page_count | IntegerField | integer | 否 | 页数,失败或不可确定时为空 |
| statistics_status | CharField(20) | varchar(20) | 是 | success、failed、unsupported、uncertain、skipped |
| retry_count | PositiveIntegerField | integer | 是 | 页数统计重试次数 |
| error_message | TextField | text | 否 | 异常说明 |
| created_at | DateTimeField | datetime | 是 | 创建时间 |
| updated_at | DateTimeField | datetime | 是 | 更新时间 |
唯一约束:
| 约束名 | 字段 |
| --- | --- |
| uq_ra_item_batch_relative_path | batch_id, relative_path |
索引:
| 索引名 | 字段 | 说明 |
| --- | --- | --- |
| idx_ra_item_batch_index | batch_id, file_index | 按序展示文件明细 |
| idx_ra_item_batch_status | batch_id, statistics_status | 查询失败/不可确定文件 |
| idx_ra_item_batch_type | batch_id, file_type | 按类型统计 |
---
### 3.5 ra_workflow_node_run
工作流节点运行状态表,用于右侧工作流卡片状态恢复。
| 字段名 | Django 类型 | SQLite 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| id | BigAutoField | integer | 是 | 主键 |
| batch_id | ForeignKey | bigint | 是 | 所属批次 |
| node_code | CharField(40) | varchar(40) | 是 | 节点编码 |
| node_name | CharField(80) | varchar(80) | 是 | 节点名称 |
| status | CharField(20) | varchar(20) | 是 | pending、running、retrying、success、failed、skipped |
| progress | PositiveIntegerField | integer | 是 | 进度百分比0-100 |
| message | TextField | text | 否 | 节点提示 |
| started_at | DateTimeField | datetime | 否 | 开始时间 |
| finished_at | DateTimeField | datetime | 否 | 完成时间 |
唯一约束:
| 约束名 | 字段 |
| --- | --- |
| uq_ra_node_batch_code | batch_id, node_code |
索引:
| 索引名 | 字段 | 说明 |
| --- | --- | --- |
| idx_ra_node_batch_status | batch_id, status | 查询批次节点状态 |
---
### 3.6 ra_workflow_event
工作流事件表,用于 SSE 事件持久化、断线续传和调试。
| 字段名 | Django 类型 | SQLite 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| id | BigAutoField | integer | 是 | 主键,同时可作为 event_id |
| batch_id | ForeignKey | bigint | 是 | 所属批次 |
| event_type | CharField(40) | varchar(40) | 是 | workflow_started、node_progress 等 |
| payload | JSONField | text/json | 是 | 事件载荷 |
| created_at | DateTimeField | datetime | 是 | 事件时间 |
索引:
| 索引名 | 字段 | 说明 |
| --- | --- | --- |
| idx_ra_event_batch_id | batch_id, id | SSE after 续传 |
| idx_ra_event_batch_created | batch_id, created_at | 按时间查询事件 |
---
### 3.7 ra_exported_summary_file
导出文件记录表。下载链接运行时根据 export_id 生成。
| 字段名 | Django 类型 | SQLite 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| id | BigAutoField | integer | 是 | 主键 |
| batch_id | ForeignKey | bigint | 是 | 所属批次 |
| export_type | CharField(20) | varchar(20) | 是 | markdown、excel |
| file_name | CharField(255) | varchar(255) | 是 | 导出文件名 |
| storage_path | CharField(500) | varchar(500) | 是 | 保存路径 |
| status | CharField(20) | varchar(20) | 是 | success、failed |
| error_message | TextField | text | 否 | 导出异常说明 |
| created_at | DateTimeField | datetime | 是 | 生成时间 |
索引:
| 索引名 | 字段 | 说明 |
| --- | --- | --- |
| idx_ra_export_batch_type | batch_id, export_type | 查询批次导出文件 |
| idx_ra_export_batch_created | batch_id, created_at | 按生成时间查询 |
---
## 四、枚举设计
本功能不建立枚举表,枚举通过 Django `TextChoices` 定义,数据库存储字符串。
### 4.1 附件状态 upload_status
| 值 | 中文 | 说明 |
| --- | --- | --- |
| uploaded | 已上传 | 上传完成,尚未绑定批次 |
| bound | 已绑定 | 已被某个批次使用 |
| deleted | 已删除 | 用户逻辑删除,不再作为默认候选 |
### 4.2 批次状态 batch.status
| 值 | 中文 | 说明 |
| --- | --- | --- |
| pending | 待执行 | 批次已创建 |
| running | 执行中 | 后台工作流运行中 |
| success | 成功 | 工作流完成 |
| failed | 失败 | 批次级失败 |
### 4.3 节点状态 node.status
| 值 | 中文 | 说明 |
| --- | --- | --- |
| pending | 等待中 | 节点未开始 |
| running | 执行中 | 节点正在执行 |
| retrying | 重试中 | 单文件解析失败后重试 |
| success | 成功 | 节点执行成功 |
| failed | 失败 | 节点失败 |
| skipped | 跳过 | 当前批次不需要执行该节点 |
### 4.4 文件统计状态 statistics_status
| 值 | 中文 | 说明 |
| --- | --- | --- |
| success | 成功 | 页数统计成功 |
| failed | 失败 | 重试后仍失败 |
| unsupported | 不支持 | 文件类型不在支持范围 |
| uncertain | 不确定 | 文件可读,但无可靠页数元数据 |
| skipped | 跳过 | 空文件、隐藏文件或规则跳过 |
### 4.5 导出类型 export_type
| 值 | 中文 | 说明 |
| --- | --- | --- |
| markdown | Markdown | Markdown 汇总报告 |
| excel | Excel | Excel 明细文件 |
### 4.6 导出状态 export.status
| 值 | 中文 | 说明 |
| --- | --- | --- |
| success | 成功 | 导出文件生成成功 |
| failed | 失败 | 导出失败 |
---
## 五、关系与业务规则
### 5.1 对话与附件
```text
Conversation 1:N ra_file_attachment
```
规则:
| 规则 | 说明 |
| --- | --- |
| 上传即存储 | 用户上传后立即创建 FileAttachment |
| 对话隔离 | 附件只能被同一 Conversation 下的批次使用 |
| 多版本 | 同一 conversation + original_name 可存在多个 version_no |
| 默认版本 | is_active=true 的记录作为默认候选版本 |
| 逻辑删除 | 删除附件时设置 upload_status=deleted不立即物理删除 |
### 5.2 对话与批次
```text
Conversation 1:N ra_file_summary_batch
```
规则:
| 规则 | 说明 |
| --- | --- |
| 多次汇总 | 同一对话允许多次触发自动汇总 |
| 提示词触发 | 批次由用户消息触发,可关联 trigger_message_id |
| 批次固化 | 批次启动时固化本次使用的附件版本 |
### 5.3 批次与附件版本
```text
ra_file_summary_batch N:M ra_file_attachment
```
通过 `ra_file_summary_batch_attachment` 实现。
规则:
| 规则 | 说明 |
| --- | --- |
| 不串文件 | 工作流只能读取中间表绑定的附件 |
| 保留历史 | 即使附件后续上传新版本,历史批次仍指向旧版本 |
| 版本选择 | 用户未选择时默认使用同名文件的最新 active 版本 |
### 5.4 批次与文件明细
```text
ra_file_summary_batch 1:N ra_file_summary_item
```
规则:
| 规则 | 说明 |
| --- | --- |
| 相对路径唯一 | 同一批次下 relative_path 唯一 |
| 处理路径保留 | relative_path 用于展示storage_path 用于后台处理 |
| 单文件失败不阻断 | 文件解析失败记录 failed批次继续处理其他文件 |
---
## 六、索引设计汇总
| 表 | 索引/约束 | 字段 | 用途 |
| --- | --- | --- | --- |
| ra_file_attachment | uq_ra_attachment_conv_name_version | conversation_id, original_name, version_no | 同名附件版本唯一 |
| ra_file_attachment | idx_ra_attachment_conv_created | conversation_id, created_at | 对话附件列表 |
| ra_file_attachment | idx_ra_attachment_user_created | user_id, created_at | 用户上传记录 |
| ra_file_attachment | idx_ra_attachment_active | conversation_id, original_name, is_active | 默认版本查询 |
| ra_file_summary_batch | uq_ra_batch_no | batch_no | 批次编号唯一 |
| ra_file_summary_batch | idx_ra_batch_conv_created | conversation_id, created_at | 对话批次列表 |
| ra_file_summary_batch | idx_ra_batch_user_created | user_id, created_at | 用户批次列表 |
| ra_file_summary_batch | idx_ra_batch_status | status, created_at | 查询运行中/失败批次 |
| ra_file_summary_batch_attachment | uq_ra_batch_attachment | batch_id, attachment_id | 批次附件唯一 |
| ra_file_summary_item | uq_ra_item_batch_relative_path | batch_id, relative_path | 批次内文件唯一 |
| ra_file_summary_item | idx_ra_item_batch_index | batch_id, file_index | 文件明细排序 |
| ra_file_summary_item | idx_ra_item_batch_status | batch_id, statistics_status | 查询异常文件 |
| ra_workflow_node_run | uq_ra_node_batch_code | batch_id, node_code | 每批次每节点唯一 |
| ra_workflow_event | idx_ra_event_batch_id | batch_id, id | SSE 断点续传 |
| ra_exported_summary_file | idx_ra_export_batch_type | batch_id, export_type | 查询导出文件 |
---
## 七、SQLite 参考 DDL
> 说明:以下 DDL 为设计参考,实际落地以 Django migration 为准。
```sql
CREATE TABLE ra_file_attachment (
id INTEGER PRIMARY KEY AUTOINCREMENT,
conversation_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
original_name VARCHAR(255) NOT NULL,
version_no INTEGER NOT NULL DEFAULT 1,
is_active BOOLEAN NOT NULL DEFAULT 1,
storage_path VARCHAR(500) NOT NULL,
file_size BIGINT NOT NULL DEFAULT 0,
content_type VARCHAR(120) NOT NULL DEFAULT '',
upload_status VARCHAR(20) NOT NULL DEFAULT 'uploaded',
created_at DATETIME NOT NULL,
UNIQUE (conversation_id, original_name, version_no)
);
CREATE INDEX idx_ra_attachment_conv_created
ON ra_file_attachment (conversation_id, created_at);
CREATE INDEX idx_ra_attachment_user_created
ON ra_file_attachment (user_id, created_at);
CREATE INDEX idx_ra_attachment_active
ON ra_file_attachment (conversation_id, original_name, is_active);
```
```sql
CREATE TABLE ra_file_summary_batch (
id INTEGER PRIMARY KEY AUTOINCREMENT,
conversation_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
trigger_message_id BIGINT NULL,
batch_no VARCHAR(64) NOT NULL UNIQUE,
product_name VARCHAR(200) NOT NULL DEFAULT '',
status VARCHAR(20) NOT NULL DEFAULT 'pending',
total_files INTEGER NOT NULL DEFAULT 0,
supported_files INTEGER NOT NULL DEFAULT 0,
success_files INTEGER NOT NULL DEFAULT 0,
failed_files INTEGER NOT NULL DEFAULT 0,
unsupported_files INTEGER NOT NULL DEFAULT 0,
uncertain_files INTEGER NOT NULL DEFAULT 0,
total_pages INTEGER NOT NULL DEFAULT 0,
work_dir VARCHAR(500) NOT NULL DEFAULT '',
error_message TEXT NOT NULL DEFAULT '',
created_at DATETIME NOT NULL,
started_at DATETIME NULL,
finished_at DATETIME NULL
);
CREATE INDEX idx_ra_batch_conv_created
ON ra_file_summary_batch (conversation_id, created_at);
CREATE INDEX idx_ra_batch_user_created
ON ra_file_summary_batch (user_id, created_at);
CREATE INDEX idx_ra_batch_status
ON ra_file_summary_batch (status, created_at);
```
```sql
CREATE TABLE ra_file_summary_batch_attachment (
id INTEGER PRIMARY KEY AUTOINCREMENT,
batch_id BIGINT NOT NULL,
attachment_id BIGINT NOT NULL,
source_role VARCHAR(20) NOT NULL DEFAULT 'multi_file',
created_at DATETIME NOT NULL,
UNIQUE (batch_id, attachment_id)
);
CREATE INDEX idx_ra_batch_attachment_batch
ON ra_file_summary_batch_attachment (batch_id, created_at);
CREATE INDEX idx_ra_batch_attachment_attachment
ON ra_file_summary_batch_attachment (attachment_id);
```
```sql
CREATE TABLE ra_file_summary_item (
id INTEGER PRIMARY KEY AUTOINCREMENT,
batch_id BIGINT NOT NULL,
file_index INTEGER NOT NULL,
directory_level VARCHAR(300) NOT NULL DEFAULT '',
file_name VARCHAR(255) NOT NULL,
file_type VARCHAR(20) NOT NULL,
relative_path VARCHAR(500) NOT NULL,
storage_path VARCHAR(500) NOT NULL,
page_count INTEGER NULL,
statistics_status VARCHAR(20) NOT NULL DEFAULT 'skipped',
retry_count INTEGER NOT NULL DEFAULT 0,
error_message TEXT NOT NULL DEFAULT '',
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
UNIQUE (batch_id, relative_path)
);
CREATE INDEX idx_ra_item_batch_index
ON ra_file_summary_item (batch_id, file_index);
CREATE INDEX idx_ra_item_batch_status
ON ra_file_summary_item (batch_id, statistics_status);
CREATE INDEX idx_ra_item_batch_type
ON ra_file_summary_item (batch_id, file_type);
```
```sql
CREATE TABLE ra_workflow_node_run (
id INTEGER PRIMARY KEY AUTOINCREMENT,
batch_id BIGINT NOT NULL,
node_code VARCHAR(40) NOT NULL,
node_name VARCHAR(80) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'pending',
progress INTEGER NOT NULL DEFAULT 0,
message TEXT NOT NULL DEFAULT '',
started_at DATETIME NULL,
finished_at DATETIME NULL,
UNIQUE (batch_id, node_code)
);
CREATE INDEX idx_ra_node_batch_status
ON ra_workflow_node_run (batch_id, status);
```
```sql
CREATE TABLE ra_workflow_event (
id INTEGER PRIMARY KEY AUTOINCREMENT,
batch_id BIGINT NOT NULL,
event_type VARCHAR(40) NOT NULL,
payload TEXT NOT NULL DEFAULT '{}',
created_at DATETIME NOT NULL
);
CREATE INDEX idx_ra_event_batch_id
ON ra_workflow_event (batch_id, id);
CREATE INDEX idx_ra_event_batch_created
ON ra_workflow_event (batch_id, created_at);
```
```sql
CREATE TABLE ra_exported_summary_file (
id INTEGER PRIMARY KEY AUTOINCREMENT,
batch_id BIGINT NOT NULL,
export_type VARCHAR(20) NOT NULL,
file_name VARCHAR(255) NOT NULL,
storage_path VARCHAR(500) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'success',
error_message TEXT NOT NULL DEFAULT '',
created_at DATETIME NOT NULL
);
CREATE INDEX idx_ra_export_batch_type
ON ra_exported_summary_file (batch_id, export_type);
CREATE INDEX idx_ra_export_batch_created
ON ra_exported_summary_file (batch_id, created_at);
```
---
## 八、Django ORM 落地注意事项
### 8.1 db_table
每个模型通过 `class Meta: db_table = "ra_xxx"` 固定表名,避免 Django 默认生成较长表名。
### 8.2 JSONField
`WorkflowEvent.payload` 使用 Django `models.JSONField(default=dict)`。SQLite 下实际以文本形式存储Django 负责序列化与反序列化。
### 8.3 版本号生成
同一对话同名文件上传时:
```text
version_no = max(existing version_no) + 1
```
若新版本设为默认版本,需要将旧版本 `is_active` 更新为 false。
### 8.4 逻辑删除
附件删除时:
```text
upload_status = deleted
is_active = false
```
历史批次仍可通过中间表追溯该附件。
### 8.5 批次选择附件
用户发送提示词触发工作流时:
| 场景 | 处理 |
| --- | --- |
| 用户显式选择附件版本 | 使用所选 attachment_id |
| 用户未选择版本 | 使用当前对话下 is_active=true 且未删除的附件 |
| 存在多个同名 active 异常 | 取 created_at 最新,并记录待修复数据异常 |
---
## 九、数据保留策略
| 数据 | Demo 策略 | 正式部署建议 |
| --- | --- | --- |
| 上传附件记录 | 永久保留 | 随会话归档周期清理 |
| 上传原始文件 | 永久保留 | 可按用户/项目配置保留期限 |
| 汇总批次 | 永久保留 | 保留用于审计追溯 |
| 文件明细 | 永久保留 | 保留用于历史报告复现 |
| 工作流事件 | 永久保留 | 可定期清理已完成批次的事件 |
| 导出文件 | 永久保留 | 可设置下载有效期或归档 |
---
## 十、待确认事项
| 序号 | 问题 | 当前设计 | 状态 |
| --- | --- | --- | --- |
| 1 | 正式部署是否从 SQLite 迁移到 PostgreSQL/MySQL | 当前按 SQLite/Django ORM 设计,保留 ORM 兼容性 | 待后续确认 |
| 2 | 同名附件 active 是否允许多个 | 设计上不允许,代码更新时应关闭旧 active | 待开发实现 |
| 3 | 文件物理删除时机 | Demo 不物理删除 | 待后续确认 |
---
## 十一、开发顺序建议
1.`review_agent/models.py` 中新增上述 7 个模型。
2. 为状态字段定义 Django `TextChoices`
3. 配置 `db_table``indexes``constraints`
4. 执行 `python manage.py makemigrations review_agent` 生成迁移。
5. 执行 `python manage.py migrate` 验证 SQLite 可落表。
6. 编写模型级测试,覆盖同名附件版本、批次附件绑定、唯一约束和权限查询。

View File

@@ -0,0 +1,485 @@
# NMPA 注册资料法规核查与整改闭环工作流数据库设计
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 需求分析文档 | docs/1.需求分析/2.NMPA注册资料法规核查与整改闭环.md |
| 功能设计文档 | docs/2.功能设计/2.NMPA注册资料法规核查与整改闭环.md |
| 数据库类型 | SQLite / Django ORM |
| 表名前缀 | ra_ |
| 设计日期 | 2026-06-06 |
| 设计版本 | V1.0 |
---
## 一、设计原则
| 原则 | 说明 |
| --- | --- |
| 复用汇总批次 | 法规核查不重复保存文件清单,必须关联既有 `ra_file_summary_batch` |
| 独立核查批次 | 同一个文件汇总批次可以产生多次法规核查批次,适用条件变更时创建新批次 |
| 规则版本入库 | 结构化规则版本进入数据库便于追溯规则文件、RAG 索引和启用状态 |
| RAG 不单独建表 | RAG 索引信息挂在规则版本和核查批次字段中,不新增索引表 |
| 枚举存值 | 数据库存英文枚举 value前端或服务层映射为中文展示 |
| 关键字段独立 | 常用查询字段独立存储,其余过程上下文进入 JSON 或文件产物 |
| 大文本不入库 | 过程产物只在数据库保存路径、摘要和 hash大文本内容写入文件 |
| 软删除优先 | 法规核查相关数据采用软删除/归档策略,便于审计和恢复 |
| 过程产物留底 | 条件确认、核查矩阵、风险清单、RAG 结果、通知记录、复核记录均需留底 |
---
## 二、ER 图
```mermaid
erDiagram
AUTH_USER ||--o{ CONVERSATION : owns
CONVERSATION ||--o{ RA_FILE_SUMMARY_BATCH : has
RA_FILE_SUMMARY_BATCH ||--o{ RA_FILE_SUMMARY_ITEM : produces
RA_FILE_SUMMARY_BATCH ||--o{ RA_REGULATORY_REVIEW_BATCH : reviews
AUTH_USER ||--o{ RA_REGULATORY_REVIEW_BATCH : runs
AUTH_USER ||--o{ RA_REGULATORY_ISSUE : owns
RA_REGULATORY_RULE_VERSION ||--o{ RA_REGULATORY_REVIEW_BATCH : used_by
RA_REGULATORY_REVIEW_BATCH ||--o{ RA_REGULATORY_ISSUE : produces
RA_REGULATORY_REVIEW_BATCH ||--o{ RA_REGULATORY_ARTIFACT : keeps
RA_REGULATORY_REVIEW_BATCH ||--o{ RA_REGULATORY_NOTIFICATION_RECORD : sends
RA_REGULATORY_REVIEW_BATCH ||--o{ RA_EXPORTED_SUMMARY_FILE : exports
RA_REGULATORY_REVIEW_BATCH ||--o{ RA_WORKFLOW_NODE_RUN : tracks
RA_REGULATORY_REVIEW_BATCH ||--o{ RA_WORKFLOW_EVENT : emits
```
说明:`ra_workflow_node_run``ra_workflow_event` 在第一阶段设计中属于文件汇总批次节点记录表。法规核查工作流复用同一套事件机制,采用 `workflow_type``workflow_batch_id` 兼容多工作流;原 `batch_id` 保留用于兼容文件汇总旧逻辑。
---
## 三、表结构设计
### 3.1 ra_regulatory_rule_version
法规结构化规则版本表。规则文件仍以 YAML/JSON 文件形式维护,数据库记录版本元数据、文件 hash、RAG 索引版本和启用状态。
| 字段名 | Django 类型 | SQLite 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| id | BigAutoField | integer | 是 | 主键 |
| version | CharField(80) | varchar(80) | 是 | 规则版本,如 nmpa_ivd_2021_v1 |
| source_url | URLField(500) | varchar(500) | 是 | 法规来源 URL |
| source_path | CharField(500) | varchar(500) | 是 | 本地法规资料路径 |
| effective_date | DateField | date | 否 | 规则生效日期或公告日期 |
| rule_file_path | CharField(500) | varchar(500) | 是 | 结构化规则文件路径 |
| rule_file_hash | CharField(128) | varchar(128) | 是 | 规则文件 hash |
| rag_index_version | CharField(80) | varchar(80) | 否 | RAG 索引版本 |
| rag_index_path | CharField(500) | varchar(500) | 否 | RAG 索引存储路径 |
| is_active | BooleanField | bool | 是 | 是否当前启用版本 |
| created_by_id | ForeignKey(User) | bigint | 否 | 创建人 |
| activated_at | DateTimeField | datetime | 否 | 启用时间 |
| description | TextField | text | 否 | 版本说明 |
| created_at | DateTimeField | datetime | 是 | 创建时间 |
| updated_at | DateTimeField | datetime | 是 | 更新时间 |
| is_deleted | BooleanField | bool | 是 | 软删除标记 |
唯一约束:
| 约束名 | 字段 |
| --- | --- |
| uq_ra_reg_rule_version | version |
索引:
| 索引名 | 字段 | 说明 |
| --- | --- | --- |
| idx_ra_reg_rule_active | is_active, is_deleted | 查询当前启用规则 |
| idx_ra_reg_rule_effective | effective_date | 按生效日期追溯 |
| idx_ra_reg_rule_created | created_at | 查看规则版本历史 |
---
### 3.2 ra_regulatory_review_batch
法规核查批次表。一次法规核查工作流对应一条记录。同一个 `ra_file_summary_batch` 可关联多个法规核查批次,用于适用条件变更或重新核查。
| 字段名 | Django 类型 | SQLite 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| id | BigAutoField | integer | 是 | 主键 |
| conversation_id | ForeignKey | bigint | 是 | 绑定对话 |
| user_id | ForeignKey | bigint | 是 | 发起用户 |
| file_summary_batch_id | ForeignKey | bigint | 是 | 关联文件汇总批次 |
| rule_version_id | ForeignKey | bigint | 否 | 使用的规则版本 |
| batch_no | CharField(64) | varchar(64) | 是 | 法规核查批次编号,唯一 |
| status | CharField(30) | varchar(30) | 是 | pending、running、waiting_user、success、failed、reference_only、partial_success、cancelled |
| product_category | CharField(80) | varchar(80) | 否 | 产品类别 |
| registration_type | CharField(80) | varchar(80) | 否 | 注册类型 |
| clinical_evaluation_path | CharField(120) | varchar(120) | 否 | 临床评价路径 |
| product_name | CharField(200) | varchar(200) | 否 | 产品名称 |
| model_specification | CharField(200) | varchar(200) | 否 | 型号规格 |
| intended_use | TextField | text | 否 | 预期用途 |
| condition_json | JSONField | text/json | 否 | 其他适用条件、用户确认记录和抽取置信度 |
| rule_version_value | CharField(80) | varchar(80) | 否 | 冗余记录规则版本值,便于历史追溯 |
| rule_source_url | URLField(500) | varchar(500) | 否 | 冗余记录法规来源 URL |
| rule_source_path | CharField(500) | varchar(500) | 否 | 冗余记录本地法规资料路径 |
| rag_index_version | CharField(80) | varchar(80) | 否 | 本次使用的 RAG 索引版本 |
| risk_summary_json | JSONField | text/json | 否 | 风险数量摘要 |
| artifact_root | CharField(500) | varchar(500) | 否 | 本批次过程产物根目录 |
| error_message | TextField | text | 否 | 批次异常说明 |
| created_at | DateTimeField | datetime | 是 | 创建时间 |
| started_at | DateTimeField | datetime | 否 | 开始时间 |
| finished_at | DateTimeField | datetime | 否 | 完成时间 |
| archived_at | DateTimeField | datetime | 否 | 归档时间 |
| is_deleted | BooleanField | bool | 是 | 软删除标记 |
唯一约束:
| 约束名 | 字段 |
| --- | --- |
| uq_ra_reg_batch_no | batch_no |
索引:
| 索引名 | 字段 | 说明 |
| --- | --- | --- |
| idx_ra_reg_batch_conv_status | conversation_id, status | 查询对话下法规核查批次状态 |
| idx_ra_reg_batch_summary | file_summary_batch_id | 根据文件汇总批次查询法规核查历史 |
| idx_ra_reg_batch_created | created_at | 按创建时间查询 |
| idx_ra_reg_batch_rule | rule_version_value | 规则版本追溯 |
| idx_ra_reg_batch_user_created | user_id, created_at | 查询用户发起记录 |
---
### 3.3 ra_regulatory_issue
法规核查问题表,记录完整性、章节结构、一致性、通知、复核等业务问题及整改状态。
| 字段名 | Django 类型 | SQLite 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| id | BigAutoField | integer | 是 | 主键 |
| batch_id | ForeignKey | bigint | 是 | 所属法规核查批次 |
| owner_id | ForeignKey(User) | bigint | 否 | 责任人,默认上传人 |
| issue_code | CharField(100) | varchar(100) | 是 | 问题编码 |
| issue_type | CharField(40) | varchar(40) | 是 | completeness、structure、consistency、notification、review |
| risk_level | CharField(20) | varchar(20) | 是 | blocking、high、medium、low、info |
| status | CharField(30) | varchar(30) | 是 | pending_confirm、pending_fix、fixed、review_passed、review_failed、closed |
| title | CharField(255) | varchar(255) | 是 | 问题标题 |
| description | TextField | text | 否 | 问题描述 |
| rule_id | CharField(120) | varchar(120) | 否 | 命中的规则 ID |
| regulation_basis | TextField | text | 否 | 法规依据或规则依据 |
| file_item_id | ForeignKey(FileSummaryItem) | bigint | 否 | 关联文件明细,可为空 |
| file_path | CharField(500) | varchar(500) | 否 | 常用证据文件路径 |
| page_no | PositiveIntegerField | integer | 否 | 常用证据页码 |
| field_name | CharField(120) | varchar(120) | 否 | 一致性或字段问题名称 |
| evidence_json | JSONField | text/json | 否 | 证据详情如文本片段、多个来源值、RAG 引用等 |
| suggestion | TextField | text | 否 | 整改建议 |
| source_node | CharField(60) | varchar(60) | 否 | 产生问题的工作流节点 |
| confirmed_by_id | ForeignKey(User) | bigint | 否 | 确认人 |
| confirmed_at | DateTimeField | datetime | 否 | 确认时间 |
| closed_by_id | ForeignKey(User) | bigint | 否 | 关闭人 |
| closed_at | DateTimeField | datetime | 否 | 关闭时间 |
| created_at | DateTimeField | datetime | 是 | 创建时间 |
| updated_at | DateTimeField | datetime | 是 | 更新时间 |
| is_deleted | BooleanField | bool | 是 | 软删除标记 |
唯一约束:
| 约束名 | 字段 |
| --- | --- |
| uq_ra_reg_issue_batch_code | batch_id, issue_code |
索引:
| 索引名 | 字段 | 说明 |
| --- | --- | --- |
| idx_ra_reg_issue_batch | batch_id, created_at | 查询批次问题 |
| idx_ra_reg_issue_risk_status | risk_level, status | 风险列表和整改状态筛选 |
| idx_ra_reg_issue_owner_status | owner_id, status | 责任人待办 |
| idx_ra_reg_issue_rule | rule_id | 规则问题追溯 |
| idx_ra_reg_issue_file | file_item_id | 关联文件问题 |
| idx_ra_reg_issue_field | field_name | 字段一致性问题查询 |
---
### 3.4 ra_regulatory_artifact
法规核查过程产物表。只保存文件元数据,不保存大文本全文。文件内容写入受控存储目录,`file_hash` 必填。
| 字段名 | Django 类型 | SQLite 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| id | BigAutoField | integer | 是 | 主键 |
| batch_id | ForeignKey | bigint | 是 | 所属法规核查批次 |
| artifact_type | CharField(60) | varchar(60) | 是 | condition_record、rule_matrix、risk_list、text_extract_json、rag_result_json、notification_record、review_record |
| file_format | CharField(20) | varchar(20) | 是 | markdown、excel、json |
| file_name | CharField(255) | varchar(255) | 是 | 文件名 |
| storage_path | CharField(500) | varchar(500) | 是 | 存储路径 |
| file_size | BigIntegerField | bigint | 是 | 文件大小 |
| file_hash | CharField(128) | varchar(128) | 是 | 文件 hash用于校验留底文件未被篡改 |
| summary | TextField | text | 否 | 产物摘要 |
| created_by_node | CharField(60) | varchar(60) | 否 | 产生该产物的工作流节点 |
| created_at | DateTimeField | datetime | 是 | 创建时间 |
| is_deleted | BooleanField | bool | 是 | 软删除标记 |
索引:
| 索引名 | 字段 | 说明 |
| --- | --- | --- |
| idx_ra_reg_artifact_batch_type | batch_id, artifact_type | 查询批次过程产物 |
| idx_ra_reg_artifact_format | file_format | 按格式查询 |
| idx_ra_reg_artifact_created | created_at | 按时间追溯 |
---
### 3.5 ra_regulatory_notification_record
法规核查通知记录表,记录飞书 CLI 发送结果。飞书失败不阻断工作流,但需要留痕。
| 字段名 | Django 类型 | SQLite 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| id | BigAutoField | integer | 是 | 主键 |
| batch_id | ForeignKey | bigint | 是 | 所属法规核查批次 |
| recipient_id | ForeignKey(User) | bigint | 是 | 通知对象 |
| channel | CharField(30) | varchar(30) | 是 | feishu_cli、feishu_api、mock |
| risk_levels | JSONField | text/json | 是 | 本次通知包含的风险等级 |
| issue_ids | JSONField | text/json | 是 | 本次通知关联的问题 ID 列表 |
| message_summary | TextField | text | 是 | 通知内容摘要 |
| send_status | CharField(20) | varchar(20) | 是 | pending、success、failed |
| retry_count | PositiveIntegerField | integer | 是 | 已重试次数,最多 3 次 |
| external_message_id | CharField(120) | varchar(120) | 否 | 飞书外部消息 ID |
| error_message | TextField | text | 否 | 失败原因 |
| sent_at | DateTimeField | datetime | 否 | 发送成功时间 |
| created_at | DateTimeField | datetime | 是 | 创建时间 |
| updated_at | DateTimeField | datetime | 是 | 更新时间 |
| is_deleted | BooleanField | bool | 是 | 软删除标记 |
索引:
| 索引名 | 字段 | 说明 |
| --- | --- | --- |
| idx_ra_reg_notify_batch | batch_id, created_at | 查询批次通知记录 |
| idx_ra_reg_notify_recipient | recipient_id, send_status | 查询用户通知状态 |
| idx_ra_reg_notify_status | send_status, retry_count | 查询待重试通知 |
---
## 四、枚举设计
### 4.1 RegulatoryReviewBatch.status
| value | 中文展示 | 说明 |
| --- | --- | --- |
| pending | 待执行 | 已创建,等待执行 |
| running | 执行中 | 工作流正在执行 |
| waiting_user | 等待用户 | 等待用户确认适用条件或关闭复核 |
| success | 已完成 | 核查完成且无关键失败 |
| failed | 失败 | 关键节点失败,无法输出有效结果 |
| reference_only | 仅供参考 | 规则文件加载失败,降级为 RAG 辅助核查 |
| partial_success | 部分完成 | 部分节点或通知失败,但已输出主要结果 |
| cancelled | 已取消 | 用户或系统取消执行 |
### 4.2 RegulatoryIssue.status
| value | 中文展示 | 说明 |
| --- | --- | --- |
| pending_confirm | 待确认 | 条件性问题或低置信度问题等待人工确认 |
| pending_fix | 待处理 | 已确认需要补充或整改 |
| fixed | 已补充 | 用户已上传补充资料或声明已处理 |
| review_passed | 复核通过 | 系统复核通过,关闭前仍需人工确认 |
| review_failed | 复核不通过 | 系统复核后问题仍存在 |
| closed | 已关闭 | 用户确认问题解决并关闭 |
### 4.3 RegulatoryIssue.risk_level
| value | 中文展示 | 说明 |
| --- | --- | --- |
| blocking | 阻断项 | 直接影响资料能否进入有效申报或审核 |
| high | 高风险 | 可能导致注册审评补正或重大整改 |
| medium | 中风险 | 需要补充说明或修改 |
| low | 低风险 | 建议修正但影响较小 |
| info | 提示项 | 系统无法充分判断或建议人工关注 |
### 4.4 其他枚举
| 字段 | value |
| --- | --- |
| issue_type | completeness、structure、consistency、notification、review |
| artifact_type | condition_record、rule_matrix、risk_list、text_extract_json、rag_result_json、notification_record、review_record |
| file_format | markdown、excel、json |
| send_status | pending、success、failed |
| channel | feishu_cli、feishu_api、mock |
---
## 五、软删除与归档策略
| 对象 | 策略 |
| --- | --- |
| RegulatoryRuleVersion | 使用 `is_deleted` 软删除;已被批次引用的版本不允许物理删除 |
| RegulatoryReviewBatch | 使用 `is_deleted``archived_at` 归档;归档后默认不在对话主列表展示 |
| RegulatoryIssue | 使用 `is_deleted` 软删除;删除时保留批次摘要和过程产物 |
| RegulatoryArtifact | 使用 `is_deleted` 软删除;正式环境可配合对象存储生命周期归档 |
| RegulatoryNotificationRecord | 使用 `is_deleted` 软删除;保留通知失败原因和重试次数 |
删除 Conversation 时,本期不建议物理级联法规核查数据。应先标记相关批次归档或删除,再由后台清理任务处理文件和产物。
---
## 六、过程产物存储设计
### 6.1 存储目录
法规核查过程产物使用独立目录,按用户、对话、法规核查批次隔离:
```text
media/regulatory_review/{user_id}/{conversation_id}/{batch_id}/
```
示例:
```text
media/regulatory_review/12/1001/2001/
condition_record.md
condition_record.json
rule_matrix.xlsx
risk_list.md
risk_list.json
text_extract.json
rag_result.json
notification_record.md
review_record.json
```
### 6.2 文件 hash
`ra_regulatory_artifact.file_hash` 必填。建议使用 SHA-256。
| 场景 | 处理 |
| --- | --- |
| 文件生成成功 | 计算 hash 后写入记录 |
| hash 计算失败 | 产物生成视为失败,节点进入 partial_success 或 failed |
| 下载文件 | 可选重新计算 hash 校验 |
---
## 七、JSON 字段结构建议
### 7.1 condition_json
```json
{
"extracted": {
"product_category": {"value": "in_vitro_diagnostic", "confidence": 0.92},
"registration_type": {"value": "initial_registration", "confidence": 0.76}
},
"confirmed": {
"confirmed_by": 1,
"confirmed_at": "2026-06-06T00:00:00+08:00",
"source": "dialog_choice"
},
"raw_user_input": "按体外诊断试剂首次注册处理"
}
```
### 7.2 risk_summary_json
```json
{
"blocking": 2,
"high": 1,
"medium": 3,
"low": 4,
"info": 2,
"notified": {
"feishu": 6
}
}
```
### 7.3 evidence_json
```json
{
"matched_rule": {
"rule_id": "ivd_registration_test_report",
"rule_title": "注册检验报告"
},
"matched_files": [
{
"file_item_id": 33,
"relative_path": "注册检验/检验报告.pdf",
"matched_by": "directory_keyword"
}
],
"rag_citations": [
{
"source_file": "体外诊断试剂注册申报资料要求及说明.doc",
"section_title": "注册申报资料要求",
"snippet": "..."
}
]
}
```
---
## 八、与现有表的改造建议
### 8.1 ra_workflow_node_run
第一阶段设计中该表通过 `batch_id` 直接关联文件汇总批次。法规核查复用同一套工作流状态机制,采用通用工作流引用:
| 字段 | 说明 |
| --- | --- |
| workflow_type | 新增,用于区分 file_summary 和 regulatory_review |
| workflow_batch_id | 新增,记录对应工作流批次 ID |
| batch_id | 保留,兼容文件汇总旧逻辑 |
### 8.2 ra_workflow_event
同样增加 `workflow_type``workflow_batch_id`,使 SSE 能同时服务文件汇总和法规核查卡片。
### 8.3 ra_exported_summary_file
最终法规核查报告复用导出文件表。现有 `batch_id` 关联文件汇总批次,需要通用化:
| 字段 | 说明 |
| --- | --- |
| workflow_type | 新增,用于区分 file_summary 和 regulatory_review |
| workflow_batch_id | 新增,记录对应工作流批次 ID |
| batch_id | 保留,兼容文件汇总旧逻辑 |
| export_category | 新增,用于区分 summary_report、risk_report、excel_list、json_package |
最终法规核查报告进入 `ExportedSummaryFile`,过程产物进入 `RegulatoryArtifact`
---
## 九、Django Model 命名建议
| 表名 | Model 名称 |
| --- | --- |
| ra_regulatory_rule_version | RegulatoryRuleVersion |
| ra_regulatory_review_batch | RegulatoryReviewBatch |
| ra_regulatory_issue | RegulatoryIssue |
| ra_regulatory_artifact | RegulatoryArtifact |
| ra_regulatory_notification_record | RegulatoryNotificationRecord |
---
## 十、验收检查点
| 序号 | 检查项 | 验收标准 |
| --- | --- | --- |
| 1 | 规则版本可追溯 | 每个法规核查批次能查到 rule_version、source_path、rule_file_hash 和 rag_index_version |
| 2 | 批次可多次核查 | 同一个 FileSummaryBatch 可创建多个 RegulatoryReviewBatch |
| 3 | 软删除可用 | 归档或删除法规核查批次后,默认列表不展示但历史可追溯 |
| 4 | 问题可筛选 | 可按 risk_level、status、owner 查询待处理问题 |
| 5 | 证据可追溯 | Issue 可查到 file_path、page_no、field_name 和 evidence_json |
| 6 | 产物可校验 | 每个 RegulatoryArtifact 都有 file_hash |
| 7 | 飞书可重试 | NotificationRecord 可记录 retry_count、send_status 和失败原因 |
| 8 | 权限可追溯 | 所有法规核查数据可通过 batch -> conversation -> user 校验访问权限 |
---
## 十一、后续实现注意事项
| 序号 | 问题 | 当前建议 |
| --- | --- | --- |
| 1 | WorkflowNodeRun/Event 通用化 | 已确定新增 workflow_type 和 workflow_batch_id保留 batch_id 兼容文件汇总 |
| 2 | ExportedSummaryFile 通用化 | 已确定新增 workflow_type、workflow_batch_id 和 export_category |
| 3 | RegulatoryArtifact 下载接口 | 按 batch -> conversation -> user 校验权限 |
| 4 | 飞书用户映射 | 暂通过 User 扩展字段或配置表映射飞书 CLI 可识别账号 |
| 5 | 规则文件 hash 计算时机 | 规则导入或激活时计算并写入 RegulatoryRuleVersion |

View File

@@ -0,0 +1,433 @@
# 产品关键信息提取与申报文件自动填表数据库设计
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 需求分析文档 | docs/1.需求分析/3.产品关键信息提取与申报文件自动填表.md |
| 功能设计文档 | docs/2.功能设计/3.产品关键信息提取与申报文件自动填表.md |
| 数据库类型 | SQLite / Django ORM |
| 表名前缀 | ra_ |
| 设计日期 | 2026-06-07 |
| 设计版本 | V1.0 |
---
## 一、设计原则
| 原则 | 说明 |
| --- | --- |
| 独立填表批次 | 自动填表作为独立工作流,使用独立批次表,不强绑法规核查批次 |
| 复用文件来源 | 填表批次必须关联一个成功的 `FileSummaryBatch`,不重复保存文件清单 |
| 可选复用法规条件 | 如当前对话已有已确认法规核查批次,可通过可空外键复用注册类型等条件 |
| 导出记录复用 | Word、Excel、JSON、PDF 等下载文件继续进入 `ExportedSummaryFile` |
| 过程产物独立 | 自动填表过程产物单独建表,避免和法规核查 `RegulatoryArtifact` 混用 |
| 通知记录独立 | 自动填表飞书通知单独建表,字段风格与法规通知记录保持一致 |
| 大文本不入库 | 字段抽取 JSON、追溯清单和模板副本保存为文件数据库仅保存路径、hash 和摘要 |
| 字段明细暂不入库 | 本期不新增字段级明细表;字段结果保存在 JSON/Excel 产物与批次摘要中 |
| SQLite 兼容 | 字段类型、索引和约束优先保证当前 SQLite + Django ORM 可运行 |
---
## 二、ER 图
```mermaid
erDiagram
AUTH_USER ||--o{ CONVERSATION : owns
CONVERSATION ||--o{ RA_FILE_SUMMARY_BATCH : has
RA_FILE_SUMMARY_BATCH ||--o{ RA_FILE_SUMMARY_ITEM : produces
RA_FILE_SUMMARY_BATCH ||--o{ RA_APPLICATION_FORM_FILL_BATCH : feeds
RA_REGULATORY_REVIEW_BATCH ||--o{ RA_APPLICATION_FORM_FILL_BATCH : optionally_confirms
AUTH_USER ||--o{ RA_APPLICATION_FORM_FILL_BATCH : runs
CONVERSATION ||--o{ RA_APPLICATION_FORM_FILL_BATCH : has
MESSAGE ||--o{ RA_APPLICATION_FORM_FILL_BATCH : triggers
RA_APPLICATION_FORM_FILL_BATCH ||--o{ RA_APPLICATION_FORM_FILL_ARTIFACT : keeps
RA_APPLICATION_FORM_FILL_BATCH ||--o{ RA_APPLICATION_FORM_FILL_NOTIFICATION_RECORD : sends
RA_APPLICATION_FORM_FILL_BATCH ||--o{ RA_EXPORTED_SUMMARY_FILE : exports
RA_APPLICATION_FORM_FILL_BATCH ||--o{ RA_WORKFLOW_NODE_RUN : tracks
RA_APPLICATION_FORM_FILL_BATCH ||--o{ RA_WORKFLOW_EVENT : emits
```
说明:`ra_workflow_node_run``ra_workflow_event``ra_exported_summary_file` 已在第二批中被通用化,通过 `workflow_type``workflow_batch_id` 支持多工作流。本功能使用 `workflow_type=application_form_fill`
---
## 三、表结构设计
### 3.1 ra_application_form_fill_batch
一次自动填表工作流批次。该表记录本次触发来源、选择模板、输出类型、注册类型、产品名称、冲突摘要、工作目录和状态。
| 字段名 | Django 类型 | SQLite 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| id | BigAutoField | integer | 是 | 主键 |
| conversation_id | ForeignKey | bigint | 是 | 绑定对话 |
| user_id | ForeignKey | bigint | 是 | 发起用户 |
| trigger_message_id | ForeignKey | bigint | 否 | 触发填表工作流的用户消息 |
| source_summary_batch_id | ForeignKey | bigint | 是 | 文件来源汇总批次 |
| source_regulatory_batch_id | ForeignKey | bigint | 否 | 可选,复用已确认法规核查批次条件 |
| batch_no | CharField(64) | varchar(64) | 是 | 填表批次编号,唯一 |
| status | CharField(30) | varchar(30) | 是 | pending、running、waiting_user、success、partial_success、failed、cancelled |
| requested_templates | JSONField | text/json | 是 | 用户指定模板编码列表;未指定为空数组 |
| selected_templates | JSONField | text/json | 是 | 系统实际选择模板编码列表 |
| output_types | JSONField | text/json | 是 | 请求输出类型,如 word、excel、json、pdf |
| registration_type | CharField(80) | varchar(80) | 否 | 识别出的注册类型 |
| registration_type_source | CharField(40) | varchar(40) | 否 | user_message、regulatory_batch、file_extract、unknown |
| product_name | CharField(200) | varchar(200) | 否 | 产品名称 |
| conflict_summary | JSONField | text/json | 是 | 冲突字段摘要 |
| risk_notes | JSONField | text/json | 是 | 不适用模板、低置信度、PDF 待生成等提示 |
| template_config_version | CharField(80) | varchar(80) | 否 | 模板配置版本 |
| template_config_hash | CharField(128) | varchar(128) | 否 | 模板配置文件 hash |
| work_dir | CharField(500) | varchar(500) | 否 | 批次工作目录 |
| error_message | TextField | text | 否 | 批次异常说明 |
| created_at | DateTimeField | datetime | 是 | 创建时间 |
| started_at | DateTimeField | datetime | 否 | 开始时间 |
| finished_at | DateTimeField | datetime | 否 | 完成时间 |
| archived_at | DateTimeField | datetime | 否 | 归档时间 |
| is_deleted | BooleanField | bool | 是 | 软删除标记 |
唯一约束:
| 约束名 | 字段 |
| --- | --- |
| uq_ra_aff_batch_no | batch_no |
索引:
| 索引名 | 字段 | 说明 |
| --- | --- | --- |
| idx_ra_aff_batch_conv_status | conversation_id, status | 查询对话下填表批次状态 |
| idx_ra_aff_batch_summary | source_summary_batch_id | 根据文件汇总批次查询填表历史 |
| idx_ra_aff_batch_regulatory | source_regulatory_batch_id | 根据法规核查批次查询关联填表历史 |
| idx_ra_aff_batch_user_created | user_id, created_at | 查询用户发起记录 |
| idx_ra_aff_batch_created | created_at | 按创建时间查询 |
---
### 3.2 ra_application_form_fill_artifact
自动填表过程产物表。仅保存文件元数据,不保存字段抽取大 JSON 的全文。
| 字段名 | Django 类型 | SQLite 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| id | BigAutoField | integer | 是 | 主键 |
| batch_id | ForeignKey | bigint | 是 | 所属自动填表批次 |
| artifact_type | CharField(60) | varchar(60) | 是 | template_copy、field_extract_result、merged_fields、traceability、filled_template、notification_record |
| file_format | CharField(20) | varchar(20) | 是 | json、excel、docx、pdf、markdown |
| name | CharField(160) | varchar(160) | 是 | 产物名称 |
| file_name | CharField(255) | varchar(255) | 是 | 文件名 |
| storage_path | CharField(500) | varchar(500) | 是 | 存储路径 |
| file_size | BigIntegerField | bigint | 是 | 文件大小 |
| content_hash | CharField(128) | varchar(128) | 是 | 文件 SHA-256 hash |
| metadata | JSONField | text/json | 是 | 模板编码、输出类型、生成状态、错误摘要等 |
| created_by_node | CharField(60) | varchar(60) | 否 | 产生该产物的节点 |
| created_at | DateTimeField | datetime | 是 | 创建时间 |
| is_deleted | BooleanField | bool | 是 | 软删除标记 |
索引:
| 索引名 | 字段 | 说明 |
| --- | --- | --- |
| idx_ra_aff_artifact_batch_type | batch_id, artifact_type | 查询批次过程产物 |
| idx_ra_aff_artifact_format | file_format | 按文件格式查询 |
| idx_ra_aff_artifact_created | created_at | 按时间追溯 |
---
### 3.3 ra_application_form_fill_notification_record
自动填表飞书通知记录表。通知失败不阻断文件下载,但需要留痕和支持后续重试。
| 字段名 | Django 类型 | SQLite 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| id | BigAutoField | integer | 是 | 主键 |
| batch_id | ForeignKey | bigint | 是 | 所属自动填表批次 |
| recipient_id | ForeignKey(User) | bigint | 是 | 通知对象,默认上传人/发起人 |
| channel | CharField(30) | varchar(30) | 是 | feishu_cli、feishu_api、mock |
| template_codes | JSONField | text/json | 是 | 本次通知涉及模板 |
| export_ids | JSONField | text/json | 是 | 本次通知关联导出文件 ID |
| message_summary | TextField | text | 是 | 通知摘要 |
| send_status | CharField(20) | varchar(20) | 是 | pending、success、failed |
| retry_count | PositiveIntegerField | integer | 是 | 已重试次数 |
| external_message_id | CharField(120) | varchar(120) | 否 | 飞书外部消息 ID |
| error_message | TextField | text | 否 | 失败原因 |
| sent_at | DateTimeField | datetime | 否 | 发送成功时间 |
| created_at | DateTimeField | datetime | 是 | 创建时间 |
| updated_at | DateTimeField | datetime | 是 | 更新时间 |
| is_deleted | BooleanField | bool | 是 | 软删除标记 |
索引:
| 索引名 | 字段 | 说明 |
| --- | --- | --- |
| idx_ra_aff_notify_batch | batch_id, created_at | 查询批次通知记录 |
| idx_ra_aff_notify_recipient | recipient_id, send_status | 查询用户通知状态 |
| idx_ra_aff_notify_status | send_status, retry_count | 查询待重试通知 |
---
## 四、既有表扩展
### 4.1 ra_exported_summary_file
继续复用导出文件表,需扩展导出类型。
| 字段/枚举 | 处理 |
| --- | --- |
| export_type | 增加 `word``pdf` |
| workflow_type | 使用 `application_form_fill` |
| workflow_batch_id | 记录 `ApplicationFormFillBatch.id` |
| export_category | 使用 `filled_template``traceability``extract_result` |
导出类型枚举:
| value | 中文展示 | 说明 |
| --- | --- | --- |
| markdown | Markdown | 既有报告 |
| excel | Excel | 追溯清单 |
| json | JSON | 字段抽取结果包 |
| word | Word | 填好的 Word 模板 |
| pdf | PDF | Word 转换后的 PDFP1 预留 |
### 4.2 ra_workflow_node_run
本功能使用通用工作流字段:
| 字段 | 值 |
| --- | --- |
| workflow_type | application_form_fill |
| workflow_batch_id | ApplicationFormFillBatch.id |
| node_group | form_fill |
| batch_id | 可为空或兼容性填充 source_summary_batch_id |
### 4.3 ra_workflow_event
本功能事件写入:
| 字段 | 值 |
| --- | --- |
| workflow_type | application_form_fill |
| workflow_batch_id | ApplicationFormFillBatch.id |
| conversation_id | 当前对话 ID |
| payload | 节点状态、模板列表、冲突数量、导出文件等 |
---
## 五、枚举设计
### 5.1 ApplicationFormFillBatch.status
| value | 中文展示 | 说明 |
| --- | --- | --- |
| pending | 待执行 | 批次已创建,等待执行 |
| running | 执行中 | 工作流正在执行 |
| waiting_user | 等待用户 | 缺少文件汇总批次或关键条件 |
| success | 成功 | Word 和必要追溯产物生成成功 |
| partial_success | 部分成功 | 部分模板、PDF、追溯清单或通知失败 |
| failed | 失败 | 所有目标 Word 模板均生成失败 |
| cancelled | 已取消 | 用户或系统取消执行 |
### 5.2 artifact_type
| value | 说明 |
| --- | --- |
| template_copy | 模板副本 |
| field_extract_result | 规则/正则与 LLM 抽取原始结果 |
| merged_fields | 合并后的最终字段和冲突 |
| traceability | 字段来源追溯清单 |
| filled_template | 已填写模板 |
| notification_record | 通知记录产物 |
### 5.3 registration_type_source
| value | 说明 |
| --- | --- |
| user_message | 用户话语明确指定 |
| regulatory_batch | 复用已确认法规核查条件 |
| file_extract | 从文件内容抽取 |
| unknown | 未识别 |
### 5.4 通知枚举
| 字段 | value |
| --- | --- |
| channel | feishu_cli、feishu_api、mock |
| send_status | pending、success、failed |
---
## 六、JSON 字段结构建议
### 6.1 requested_templates / selected_templates
```json
["registration_certificate", "essential_principles"]
```
### 6.2 output_types
```json
["word", "excel", "json"]
```
PDF 作为 P1 预留,可在后续加入:
```json
["word", "pdf", "excel", "json"]
```
### 6.3 conflict_summary
```json
[
{
"field_key": "storage_condition",
"field_label": "产品储存条件及有效期",
"selected_value": "2-8℃保存有效期12个月",
"selected_source": "说明书.docx",
"conflict_values": [
{
"value": "-20℃保存",
"source_file": "产品技术要求.docx",
"evidence": "储存条件:-20℃保存"
}
],
"handling": "说明书优先,模板内黄底红字高亮"
}
]
```
### 6.4 risk_notes
```json
[
{
"type": "template_registration_mismatch",
"message": "用户指定变更注册(备案)文件,但系统识别注册类型为首次注册,需人工确认。"
},
{
"type": "pdf_pending",
"message": "PDF 转换为后续增强项,本次优先生成 Word。"
}
]
```
### 6.5 artifact.metadata
```json
{
"template_code": "registration_certificate",
"output_type": "word",
"node_code": "word_fill",
"status": "success",
"conflict_count": 2
}
```
---
## 七、存储路径设计
自动填表工作目录按用户、对话和批次隔离:
```text
media/application_form_fill/{user_id}/{conversation_id}/{batch_no}/
```
目录结构:
```text
media/application_form_fill/12/1001/AFF-20260607153000-a1b2c3/
templates/
registration_certificate.source.docx
essential_principles.source.docx
filled/
AFF-20260607153000-a1b2c3-甲胎蛋白检测试剂盒-注册证格式.docx
exports/
AFF-20260607153000-a1b2c3-甲胎蛋白检测试剂盒-字段来源追溯清单.xlsx
field_extract_result.json
merged_fields.json
notifications/
notification_record.json
```
所有产物写入 `ApplicationFormFillArtifact` 时必须记录 SHA-256 hash。
---
## 八、权限与查询规则
### 8.1 批次访问权限
```text
ApplicationFormFillBatch -> conversation -> user
必须等于当前 request.user
```
### 8.2 导出下载权限
```text
ExportedSummaryFile.workflow_type == application_form_fill
-> workflow_batch_id
-> ApplicationFormFillBatch.conversation.user
```
`workflow_type=file_summary``regulatory_review`,仍按既有逻辑校验。
### 8.3 文件读取权限
自动填表只能读取 `source_summary_batch.items` 对应的文件,不允许从其他对话或其他批次随意读取文件。
---
## 九、字段级数据库表暂缓说明
本期不新增 `ApplicationFormFillField` 字段级明细表。原因:
| 原因 | 说明 |
| --- | --- |
| Demo 主链路更轻 | 字段结果以 JSON 和 Excel 追溯清单即可满足下载复核 |
| 避免过早建模 | 字段结构依赖模板配置和后续人工修改交互,暂不固化表结构 |
| 查询需求有限 | 本期主要按批次下载文件,不做字段级统计和在线编辑 |
后续如需要在线确认、人工修改、字段级审计或批量统计,再新增字段级表。该事项写入 `docs/6.待办计划/第二阶段暂缓事项.md`
---
## 十、Django Model 命名建议
| 表名 | Model 名称 |
| --- | --- |
| ra_application_form_fill_batch | ApplicationFormFillBatch |
| ra_application_form_fill_artifact | ApplicationFormFillArtifact |
| ra_application_form_fill_notification_record | ApplicationFormFillNotificationRecord |
建议模型仍集中放在 `review_agent/models.py`,与前两批现有模型保持一致;业务逻辑放在 `review_agent/application_form_fill/`
---
## 十一、验收检查点
| 序号 | 检查项 | 验收标准 |
| --- | --- | --- |
| 1 | 独立批次 | 触发填表后生成 `ApplicationFormFillBatch` |
| 2 | 文件来源 | 每个填表批次都关联一个成功的 `FileSummaryBatch` |
| 3 | 可选法规条件 | 如有关联法规核查批次,可记录 `source_regulatory_batch` |
| 4 | 过程产物 | 字段抽取 JSON、合并结果、追溯清单、模板副本均可留底 |
| 5 | 导出复用 | 填好的 Word 和追溯清单进入 `ExportedSummaryFile` |
| 6 | 导出类型 | `ExportedSummaryFile.ExportType` 支持 `word``pdf` |
| 7 | 通知记录 | 飞书通知记录能保存状态、重试次数、失败原因 |
| 8 | 权限隔离 | A 对话的填表批次和导出文件不能被 B 对话访问 |
| 9 | 字段表暂缓 | 字段级结果不入库,但能从 JSON/Excel 追溯产物复核 |
---
## 十二、开发顺序建议
1. 扩展 `ExportedSummaryFile.ExportType`,增加 `word``pdf`
2. 新增 `ApplicationFormFillBatch``ApplicationFormFillArtifact``ApplicationFormFillNotificationRecord`
3. 为新增状态字段定义 Django `TextChoices`
4. 配置表名、索引和唯一约束。
5. 执行 `python manage.py makemigrations review_agent``python manage.py migrate`
6. 编写模型测试,覆盖批次创建、产物 hash、通知重试字段、导出权限查询。
7. 将字段级数据库表和 PDF 转换能力写入待办计划。

View File

@@ -0,0 +1,302 @@
# 飞书通知与问答接入数据库设计
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 需求分析文档 | docs/1.需求分析/4.飞书通知与问答接入.md |
| 功能设计文档 | docs/2.功能设计/4.飞书通知与问答接入.md |
| 数据库类型 | SQLite / Django ORM |
| 表名前缀 | ra_ |
| 设计日期 | 2026-06-07 |
| 设计版本 | V1.0 |
---
## 一、设计原则
| 原则 | 说明 |
| --- | --- |
| 统一通知抽象 | 三个工作流共用统一通知服务和通用通知记录,减少重复实现 |
| 兼容现有表 | 现有法规通知、填表通知可保留;新增通用表作为后续统一入口 |
| 可判重 | 通知记录必须支持同一批次、同一流程、同一状态只发送一次 |
| 摘要入库 | 只保存发送摘要、状态、错误,不保存完整富文本 payload |
| 映射可维护 | 系统用户与飞书用户映射独立建表,通过 Django Admin 维护 |
| 问答可扩展 | 预留问答日志表,首期可不接事件回调 |
| SQLite 兼容 | 使用 Django ORM 常规字段,避免数据库特有能力 |
---
## 二、ER 图
```mermaid
erDiagram
AUTH_USER ||--o{ RA_FEISHU_USER_MAPPING : maps
AUTH_USER ||--o{ RA_WORKFLOW_NOTIFICATION_RECORD : triggers
RA_FEISHU_USER_MAPPING ||--o{ RA_WORKFLOW_NOTIFICATION_RECORD : resolves
AUTH_USER ||--o{ RA_FEISHU_QUESTION_LOG : asks
RA_WORKFLOW_NOTIFICATION_RECORD {
bigint id
string workflow_type
bigint workflow_batch_id
string workflow_status
string dedupe_key
string channel
string target
string send_status
}
RA_FEISHU_USER_MAPPING {
bigint id
bigint system_user_id
string feishu_open_id
string feishu_user_id
string feishu_mobile
boolean is_active
}
RA_FEISHU_QUESTION_LOG {
bigint id
bigint system_user_id
string feishu_open_id
string intent
string query_object
string status
}
```
---
## 三、表结构设计
### 3.1 ra_feishu_user_mapping
系统用户与飞书用户标识映射表。首期通知发送给环境变量中配置的指定个人账号,本表通过 Django Admin 手工维护,用于后续按发起人私聊通知和飞书私聊问答身份识别。
| 字段名 | Django 类型 | SQLite 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| id | BigAutoField | integer | 是 | 主键 |
| system_user_id | ForeignKey | bigint | 是 | 关联 Django 用户 |
| feishu_display_name | CharField(120) | varchar(120) | 否 | 飞书展示名,便于后台识别 |
| feishu_open_id | CharField(120) | varchar(120) | 否 | 飞书 open_id优先用于 @ |
| feishu_user_id | CharField(120) | varchar(120) | 否 | 飞书 user_id第二优先级 |
| feishu_mobile | CharField(40) | varchar(40) | 否 | 飞书手机号,兜底 |
| is_active | BooleanField | bool | 是 | 是否启用 |
| remark | CharField(255) | varchar(255) | 否 | 备注 |
| created_at | DateTimeField | datetime | 是 | 创建时间 |
| updated_at | DateTimeField | datetime | 是 | 更新时间 |
约束:
| 约束名 | 字段 | 说明 |
| --- | --- | --- |
| uq_ra_feishu_mapping_user | system_user_id | 一个系统用户首期只维护一条启用映射 |
索引:
| 索引名 | 字段 | 说明 |
| --- | --- | --- |
| idx_ra_feishu_mapping_active | is_active | 后台筛选启用映射 |
| idx_ra_feishu_mapping_open | feishu_open_id | 后续私聊事件反查用户 |
| idx_ra_feishu_mapping_userid | feishu_user_id | 后续私聊事件反查用户 |
| idx_ra_feishu_mapping_mobile | feishu_mobile | 手机号兜底查询 |
校验规则:
| 规则 | 说明 |
| --- | --- |
| 至少一个飞书标识 | `feishu_open_id``feishu_user_id``feishu_mobile` 至少填写一个 |
| @ 优先级 | `feishu_open_id -> feishu_user_id -> feishu_mobile` |
---
### 3.2 ra_workflow_notification_record
通用工作流通知记录表。用于记录自动汇总、法规核查、自动填表的飞书通知发送结果。现有专项通知表可继续保留,后续逐步收敛到本表。
| 字段名 | Django 类型 | SQLite 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| id | BigAutoField | integer | 是 | 主键 |
| workflow_type | CharField(40) | varchar(40) | 是 | file_summary、regulatory_review、application_form_fill |
| workflow_batch_id | PositiveBigIntegerField | bigint | 是 | 对应工作流批次 ID |
| workflow_batch_no | CharField(80) | varchar(80) | 是 | 批次编号冗余,便于展示 |
| workflow_status | CharField(40) | varchar(40) | 是 | success、partial_success、failed 等 |
| dedupe_key | CharField(160) | varchar(160) | 是 | 判重键 |
| trigger_user_id | ForeignKey | bigint | 是 | 发起人或上传人 |
| feishu_mapping_id | ForeignKey | bigint | 否 | 命中的飞书用户映射 |
| channel | CharField(40) | varchar(40) | 是 | mock、feishu_api、disabled |
| target | CharField(160) | varchar(160) | 否 | 指定个人账号名称、open_id、user_id 或目标标识 |
| at_display_name | CharField(120) | varchar(120) | 否 | 被 @ 人展示名 |
| at_identifier_type | CharField(30) | varchar(30) | 否 | open_id、user_id、mobile、missing |
| at_identifier_masked | CharField(120) | varchar(120) | 否 | 脱敏后的 @ 标识 |
| send_status | CharField(30) | varchar(30) | 是 | pending、success、failed、skipped_duplicate、disabled |
| message_title | CharField(200) | varchar(200) | 是 | 通知标题 |
| message_summary | TextField | text | 否 | 发送摘要,不保存完整 payload |
| result_url | CharField(500) | varchar(500) | 否 | 系统结果入口 |
| external_message_id | CharField(120) | varchar(120) | 否 | Webhook 一般为空API 发送时保存 |
| error_code | CharField(80) | varchar(80) | 否 | 飞书或客户端错误码 |
| error_message | TextField | text | 否 | 失败原因 |
| request_duration_ms | PositiveIntegerField | integer | 否 | HTTP 请求耗时 |
| sent_at | DateTimeField | datetime | 否 | 成功发送时间 |
| created_at | DateTimeField | datetime | 是 | 创建时间 |
| updated_at | DateTimeField | datetime | 是 | 更新时间 |
唯一约束:
| 约束名 | 字段 | 说明 |
| --- | --- | --- |
| uq_ra_notify_dedupe_key | dedupe_key | 同一批次、流程、状态只保留一个成功发送意图 |
索引:
| 索引名 | 字段 | 说明 |
| --- | --- | --- |
| idx_ra_notify_workflow | workflow_type, workflow_batch_id | 批次详情页查询通知 |
| idx_ra_notify_user_created | trigger_user_id, created_at | 用户通知历史 |
| idx_ra_notify_status | send_status, created_at | 排查失败通知 |
| idx_ra_notify_batch_no | workflow_batch_no | 按批次编号检索 |
dedupe_key 生成规则:
```text
{workflow_type}:{workflow_batch_id}:{workflow_status}
```
---
### 3.3 ra_feishu_question_log
飞书问答日志预留表。首期可创建表但不接入事件回调;后续私聊问答 MVP 使用该表记录问题、意图、查询对象、回答摘要和错误信息。
| 字段名 | Django 类型 | SQLite 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| id | BigAutoField | integer | 是 | 主键 |
| system_user_id | ForeignKey | bigint | 否 | 识别出的系统用户 |
| feishu_mapping_id | ForeignKey | bigint | 否 | 命中的飞书映射 |
| feishu_open_id | CharField(120) | varchar(120) | 否 | 事件中的 open_id |
| feishu_user_id | CharField(120) | varchar(120) | 否 | 事件中的 user_id |
| source_type | CharField(30) | varchar(30) | 是 | private_chat、group_mention |
| message_id | CharField(120) | varchar(120) | 否 | 飞书消息 ID |
| question_text | TextField | text | 是 | 用户原始问题 |
| intent | CharField(60) | varchar(60) | 否 | batch_status、risk_summary、export_summary 等 |
| query_object | JSONField | text/json | 是 | 批次号、工作流类型、最近批次等查询对象 |
| answer_summary | TextField | text | 否 | 回答摘要,不保存完整回答正文 |
| permission_result | CharField(40) | varchar(40) | 否 | allowed、denied、unbound |
| status | CharField(30) | varchar(30) | 是 | success、failed、ignored |
| error_message | TextField | text | 否 | 异常说明 |
| processed_at | DateTimeField | datetime | 否 | 处理完成时间 |
| created_at | DateTimeField | datetime | 是 | 创建时间 |
索引:
| 索引名 | 字段 | 说明 |
| --- | --- | --- |
| idx_ra_feishu_q_user_created | system_user_id, created_at | 用户问答历史 |
| idx_ra_feishu_q_intent | intent, created_at | 按意图分析 |
| idx_ra_feishu_q_status | status, created_at | 排查失败问答 |
| idx_ra_feishu_q_message | message_id | 消息幂等 |
---
## 四、状态枚举
### 4.1 WorkflowNotificationRecord.channel
| 值 | 说明 |
| --- | --- |
| mock | 模拟通知 |
| disabled | 真实通知未启用 |
| feishu_api | 飞书官方智能体/企业自建应用消息 API |
| feishu_webhook | 备选自定义机器人 Webhook非首期主方案 |
### 4.2 WorkflowNotificationRecord.send_status
| 值 | 说明 |
| --- | --- |
| pending | 待发送 |
| success | 发送成功 |
| failed | 发送失败 |
| skipped_duplicate | 重复通知跳过 |
| disabled | 未启用真实发送 |
### 4.3 FeishuQuestionLog.intent
| 值 | 说明 |
| --- | --- |
| batch_status | 查询批次状态 |
| risk_summary | 查询风险摘要 |
| missing_summary | 查询缺失摘要 |
| export_summary | 查询导出摘要 |
| unknown | 未识别 |
---
## 五、与现有表的兼容关系
| 现有表 | 处理建议 |
| --- | --- |
| `ra_regulatory_notification_record` | 保留现有数据;法规核查真实飞书通知可新增写入通用表,后续再决定是否迁移 |
| `ra_application_form_fill_notification_record` | 保留现有数据;自动填表通知状态展示可优先读通用表,兼容旧表 |
| `ra_exported_summary_file` | 通知摘要中的导出文件数量来自该表 |
| `ra_workflow_event` | 可记录通知节点事件,但不替代通知记录表 |
| `auth_user` | 飞书映射通过外键关联系统用户 |
---
## 六、数据脱敏与安全
| 数据 | 入库策略 |
| --- | --- |
| App ID | 不入库,只在环境变量中维护 |
| App Secret | 不入库,只在环境变量中维护 |
| tenant_access_token | 不持久化入库,仅允许进程内短期缓存 |
| 富文本完整 payload | 不入库 |
| 手机号 | 映射表保存原值;通知记录只保存脱敏值 |
| open_id/user_id | 映射表保存原值;通知记录保存脱敏值 |
| 用户问题 | 问答日志保存原始问题,用于审计;不保存完整回答正文 |
---
## 七、迁移计划
| 步骤 | 说明 |
| --- | --- |
| 1 | 新增 `FeishuUserMapping` 模型和迁移 |
| 2 | 新增 `WorkflowNotificationRecord` 模型和迁移 |
| 3 | 新增 `FeishuQuestionLog` 预留模型和迁移 |
| 4 | 注册 Django Admin 管理入口 |
| 5 | 批次详情页查询通用通知记录展示 |
| 6 | 保留现有专项通知表,不做破坏性迁移 |
---
## 八、验收 SQL 示例
查询某个批次通知状态:
```sql
SELECT workflow_type, workflow_batch_no, workflow_status, channel, send_status, sent_at, error_message
FROM ra_workflow_notification_record
WHERE workflow_type = 'application_form_fill'
AND workflow_batch_no = 'AFF-20260607-001'
ORDER BY created_at DESC;
```
查询未配置飞书映射的失败或降级通知:
```sql
SELECT workflow_type, workflow_batch_no, trigger_user_id, send_status, message_summary
FROM ra_workflow_notification_record
WHERE at_identifier_type = 'missing'
ORDER BY created_at DESC;
```
查询飞书用户映射:
```sql
SELECT u.username, m.feishu_display_name, m.feishu_open_id, m.feishu_user_id, m.feishu_mobile, m.is_active
FROM ra_feishu_user_mapping m
JOIN auth_user u ON u.id = m.system_user_id
ORDER BY u.username;
```

View File

@@ -0,0 +1,590 @@
# 第1章监管信息材料包生成数据库设计
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 需求分析文档 | docs/1.需求分析/5.第1章监管信息材料包生成.md |
| 功能设计文档 | docs/2.功能设计/5.第1章监管信息材料包生成.md |
| 数据库类型 | SQLite / Django ORM |
| 表名前缀 | ra_ |
| 工作流编码 | regulatory_info_package |
| 设计日期 | 2026-06-10 |
| 设计版本 | V1.0 |
---
## 一、设计原则
| 原则 | 说明 |
| --- | --- |
| 独立工作流批次 | 第1章监管信息材料包生成使用独立批次表不复用自动填表批次 |
| 附件优先 | 输入说明书优先绑定 `FileAttachment`,兼容最近成功 `FileSummaryBatch``FileSummaryItem` |
| 过程产物文件化 | 大 JSON、追溯清单、模板副本、生成文件和 zip 均保存为文件数据库只保存路径、hash、摘要 |
| 导出记录复用 | zip、单文件、追溯清单继续写入 `ExportedSummaryFile`,统一下载权限 |
| 工作流通用表复用 | 节点状态和 SSE 事件复用 `WorkflowNodeRun``WorkflowEvent` |
| 通知独立留痕 | 新增专项通知记录表,结构与自动填表通知记录保持一致 |
| SQLite 兼容 | 使用 Django ORM 常规字段和 JSONField避免数据库特定语法 |
| 原始模板保护 | 数据库只记录批次工作目录产物,不记录对原始模板的写操作 |
---
## 二、ER 图
```mermaid
erDiagram
AUTH_USER ||--o{ CONVERSATION : owns
CONVERSATION ||--o{ MESSAGE : contains
CONVERSATION ||--o{ RA_FILE_ATTACHMENT : has
CONVERSATION ||--o{ RA_REGULATORY_INFO_PACKAGE_BATCH : has
AUTH_USER ||--o{ RA_REGULATORY_INFO_PACKAGE_BATCH : runs
MESSAGE ||--o{ RA_REGULATORY_INFO_PACKAGE_BATCH : triggers
RA_FILE_ATTACHMENT ||--o{ RA_REGULATORY_INFO_PACKAGE_BATCH : provides_instruction
RA_FILE_SUMMARY_BATCH ||--o{ RA_REGULATORY_INFO_PACKAGE_BATCH : optionally_feeds
RA_REGULATORY_INFO_PACKAGE_BATCH ||--o{ RA_REGULATORY_INFO_PACKAGE_ARTIFACT : keeps
RA_REGULATORY_INFO_PACKAGE_BATCH ||--o{ RA_REGULATORY_INFO_PACKAGE_NOTIFICATION_RECORD : sends
RA_REGULATORY_INFO_PACKAGE_BATCH ||--o{ RA_EXPORTED_SUMMARY_FILE : exports
RA_REGULATORY_INFO_PACKAGE_BATCH ||--o{ RA_WORKFLOW_NODE_RUN : tracks
RA_REGULATORY_INFO_PACKAGE_BATCH ||--o{ RA_WORKFLOW_EVENT : emits
```
说明:`ra_workflow_node_run``ra_workflow_event``ra_exported_summary_file` 通过 `workflow_type``workflow_batch_id` 支持多工作流。本功能统一使用 `workflow_type=regulatory_info_package`
现状补充:当前通用节点表已有 `batch + node_code` 唯一约束主要服务文件汇总批次。RIP 批次不应强依赖 `FileSummaryBatch.batch`,因此实现时必须为 `workflow_type + workflow_batch_id + node_code` 增加数据库唯一约束,或在创建节点时使用同等幂等逻辑,避免同一 RIP 批次重复初始化节点。
---
## 三、表结构设计
### 3.1 ra_regulatory_info_package_batch
一次第1章监管信息材料包生成工作流批次。记录触发来源、输入说明书、产品名称、生成状态、待确认摘要、zip 名称、配置版本和工作目录。
| 字段名 | Django 类型 | SQLite 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| id | BigAutoField | integer | 是 | 主键 |
| conversation_id | ForeignKey | bigint | 是 | 所属对话 |
| user_id | ForeignKey | bigint | 是 | 发起用户 |
| trigger_message_id | ForeignKey | bigint | 否 | 触发本工作流的用户消息 |
| source_attachment_id | ForeignKey | bigint | 否 | 直接选中的说明书附件 |
| source_summary_batch_id | ForeignKey | bigint | 否 | 可选,最近成功文件汇总批次 |
| source_summary_item_id | PositiveBigIntegerField | integer | 否 | 可选,文件汇总条目 ID |
| batch_no | CharField(64) | varchar(64) | 是 | 批次编号,格式 `RIP-YYYYMMDDHHMMSS-abcdef`,唯一 |
| status | CharField(30) | varchar(30) | 是 | pending、running、waiting_user、success、partial_success、failed、cancelled |
| source_file_name | CharField(255) | varchar(255) | 否 | 说明书原文件名 |
| source_storage_path | CharField(500) | varchar(500) | 否 | 说明书存储路径 |
| product_name | CharField(200) | varchar(200) | 否 | 抽取到的产品名称 |
| output_zip_name | CharField(255) | varchar(255) | 否 | 主输出 zip 文件名,默认 `第1章 监管信息(预生成版).zip` |
| generated_files | JSONField | text/json | 是 | 7 个文件生成状态摘要 |
| missing_fields | JSONField | text/json | 是 | 缺失并填 `/` 的字段 |
| llm_only_fields | JSONField | text/json | 是 | 仅 LLM 命中的字段 |
| conflict_fields | JSONField | text/json | 是 | 规则和 LLM 冲突字段 |
| risk_notes | JSONField | text/json | 是 | `.doc` 适配器、知识库不可用、zip 失败等提示 |
| template_config_version | CharField(80) | varchar(80) | 否 | 模板配置版本 |
| template_config_hash | CharField(128) | varchar(128) | 否 | 模板配置 hash |
| adapter_summary | JSONField | text/json | 是 | docx/doc 适配器使用情况 |
| work_dir | CharField(500) | varchar(500) | 否 | 批次工作目录 |
| error_message | TextField | text | 否 | 批次异常说明 |
| created_at | DateTimeField | datetime | 是 | 创建时间 |
| started_at | DateTimeField | datetime | 否 | 开始时间 |
| finished_at | DateTimeField | datetime | 否 | 完成时间 |
| archived_at | DateTimeField | datetime | 否 | 归档时间 |
| is_deleted | BooleanField | bool | 是 | 软删除标记 |
唯一约束:
| 约束名 | 字段 |
| --- | --- |
| uq_ra_rip_batch_no | batch_no |
索引:
| 索引名 | 字段 | 说明 |
| --- | --- | --- |
| idx_ra_rip_batch_conv_status | conversation_id, status | 查询对话下材料包批次状态 |
| idx_ra_rip_batch_user_created | user_id, created_at | 查询用户发起历史 |
| idx_ra_rip_batch_attachment | source_attachment_id | 查询某说明书附件生成历史 |
| idx_ra_rip_batch_summary | source_summary_batch_id | 查询文件汇总关联的材料包批次 |
| idx_ra_rip_batch_created | created_at | 后台按时间排查 |
---
### 3.2 ra_regulatory_info_package_artifact
第1章监管信息材料包生成过程产物表。仅保存文件元数据不保存大文本正文。
| 字段名 | Django 类型 | SQLite 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| id | BigAutoField | integer | 是 | 主键 |
| batch_id | ForeignKey | bigint | 是 | 所属材料包批次 |
| artifact_type | CharField(60) | varchar(60) | 是 | template_copy、instruction_extract、field_extract_result、merged_fields、generated_document、traceability、zip_package、notification_record |
| file_format | CharField(20) | varchar(20) | 是 | json、excel、docx、doc、zip、markdown |
| name | CharField(160) | varchar(160) | 是 | 产物名称 |
| file_name | CharField(255) | varchar(255) | 是 | 文件名 |
| storage_path | CharField(500) | varchar(500) | 是 | 文件存储路径 |
| file_size | BigIntegerField | bigint | 是 | 文件大小 |
| content_hash | CharField(128) | varchar(128) | 否 | 文件 SHA-256 hash |
| metadata | JSONField | text/json | 是 | 模板编码、生成状态、高亮数量、适配器、错误摘要等 |
| created_by_node | CharField(60) | varchar(60) | 否 | 生成该产物的工作流节点 |
| created_at | DateTimeField | datetime | 是 | 创建时间 |
| is_deleted | BooleanField | bool | 是 | 软删除标记 |
索引:
| 索引名 | 字段 | 说明 |
| --- | --- | --- |
| idx_ra_rip_artifact_batch_type | batch_id, artifact_type | 查询批次过程产物 |
| idx_ra_rip_artifact_format | file_format | 按文件格式查询 |
| idx_ra_rip_artifact_created | created_at | 按时间追溯 |
---
### 3.3 ra_regulatory_info_package_notification_record
第1章监管信息材料包生成通知记录表。通知失败不阻断下载但需要留痕和支持后续重试。
| 字段名 | Django 类型 | SQLite 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| id | BigAutoField | integer | 是 | 主键 |
| batch_id | ForeignKey | bigint | 是 | 所属材料包批次 |
| recipient_id | ForeignKey(User) | bigint | 是 | 通知对象,默认发起人 |
| channel | CharField(30) | varchar(30) | 是 | feishu_cli、feishu_api、mock |
| export_ids | JSONField | text/json | 是 | 本次通知关联导出文件 ID |
| message_summary | TextField | text | 是 | 通知摘要 |
| send_status | CharField(20) | varchar(20) | 是 | pending、success、failed |
| retry_count | PositiveIntegerField | integer | 是 | 已重试次数 |
| external_message_id | CharField(120) | varchar(120) | 否 | 飞书外部消息 ID |
| error_message | TextField | text | 否 | 失败原因 |
| sent_at | DateTimeField | datetime | 否 | 发送成功时间 |
| created_at | DateTimeField | datetime | 是 | 创建时间 |
| updated_at | DateTimeField | datetime | 是 | 更新时间 |
| is_deleted | BooleanField | bool | 是 | 软删除标记 |
索引:
| 索引名 | 字段 | 说明 |
| --- | --- | --- |
| idx_ra_rip_notify_batch | batch_id, created_at | 查询批次通知 |
| idx_ra_rip_notify_recipient | recipient_id, send_status | 查询用户通知状态 |
| idx_ra_rip_notify_status | send_status, retry_count | 查询待重试通知 |
---
## 四、既有表扩展
### 4.1 ra_exported_summary_file
继续复用导出文件表,新增 zip 导出类型,并支持 `regulatory_info_package` 权限反查。
| 字段/枚举 | 处理 |
| --- | --- |
| export_type | 增加 `zip` |
| workflow_type | 使用 `regulatory_info_package` |
| workflow_batch_id | 记录 `RegulatoryInfoPackageBatch.id` |
| export_category | 使用 `regulatory_info_package``generated_document``traceability` |
导出类型枚举:
| value | 中文展示 | 说明 |
| --- | --- | --- |
| markdown | Markdown | 既有报告 |
| excel | Excel | 追溯清单 |
| json | JSON | 抽取结果、合并字段 |
| word | Word | 生成的 Word 文件,包含 `.docx` 和可下载 `.doc` |
| pdf | PDF | 既有预留 |
| zip | ZIP | 第1章监管信息材料包主下载 |
下载 MIME 规则:
| 条件 | content_type |
| --- | --- |
| export_type=zip | application/zip |
| export_type=word 且文件名后缀 `.doc` | application/msword |
| export_type=word 且文件名后缀 `.docx` | application/vnd.openxmlformats-officedocument.wordprocessingml.document |
### 4.2 ra_workflow_node_run
本功能使用通用工作流节点表:
| 字段 | 值 |
| --- | --- |
| workflow_type | regulatory_info_package |
| workflow_batch_id | RegulatoryInfoPackageBatch.id |
| node_group | regulatory_info_package |
| batch_id | 可为空;如为兼容旧查询,不建议绑定文件汇总批次 |
幂等约束建议:
| 约束/策略 | 字段 | 说明 |
| --- | --- | --- |
| uq_ra_node_workflow_batch_code | workflow_type, workflow_batch_id, node_code | 推荐新增数据库唯一约束,防止同一 RIP 批次重复节点 |
| get_or_create 幂等 | workflow_type, workflow_batch_id, node_code | 若暂不改通用表约束,节点初始化必须使用该组合做代码层幂等 |
建议新增节点:
```text
prepare, template_copy, text_extract, field_extract, field_merge,
generate_docs, highlight_review_items, trace_export, zip_export, notify, completed
```
### 4.3 ra_workflow_event
本功能事件写入:
| 字段 | 值 |
| --- | --- |
| workflow_type | regulatory_info_package |
| workflow_batch_id | RegulatoryInfoPackageBatch.id |
| conversation_id | 当前对话 ID |
| payload | 节点状态、文件生成状态、导出 ID、待确认摘要等 |
---
## 五、枚举设计
### 5.1 RegulatoryInfoPackageBatch.status
| value | 中文展示 | 说明 |
| --- | --- | --- |
| pending | 待执行 | 批次已创建,等待执行 |
| running | 执行中 | 工作流正在执行 |
| waiting_user | 等待用户 | 未找到唯一说明书,需要用户选择 |
| success | 成功 | 7 个文件、zip 和必要追溯产物生成成功 |
| partial_success | 部分成功 | zip 或主要文件已生成,但部分单文件、`.doc` 原生处理、`.docx` 兜底、追溯或通知存在失败 |
| failed | 失败 | 关键输入、模板或全部目标文件生成失败 |
| cancelled | 已取消 | 用户或系统取消执行 |
### 5.2 RegulatoryInfoPackageArtifact.artifact_type
| value | 说明 |
| --- | --- |
| template_copy | 模板副本 |
| instruction_extract | 说明书文本、章节、表格抽取结果 |
| field_extract_result | 规则与 LLM 抽取原始结果 |
| merged_fields | 合并字段、高亮决策、标准候选 |
| generated_document | 生成后的单个目标文件 |
| traceability | 追溯清单 |
| zip_package | 主下载 zip 包 |
| notification_record | 通知记录产物 |
### 5.3 RegulatoryInfoPackageArtifact.file_format
| value | 说明 |
| --- | --- |
| json | JSON 产物 |
| excel | Excel 追溯清单 |
| docx | Word OpenXML 文件 |
| doc | Word 97-2003 文件 |
| zip | 压缩包 |
| markdown | Markdown 摘要或报告 |
### 5.4 通知枚举
| 字段 | value |
| --- | --- |
| channel | feishu_cli、feishu_api、mock |
| send_status | pending、success、failed |
---
## 六、JSON 字段结构
### 6.1 generated_files
```json
[
{
"template_code": "ch1_4_application_form",
"file_name": "CH1.4 申请表.docx",
"status": "success",
"artifact_id": 12,
"export_id": 34,
"highlight_count": 8,
"missing_count": 5,
"llm_only_count": 2,
"error_message": ""
}
]
```
### 6.2 missing_fields
```json
[
{
"target_file": "CH1.4 申请表.docx",
"field_key": "applicant_name",
"field_label": "申请人名称",
"final_value": "/",
"highlight_reason": "missing",
"needs_review": true
}
]
```
### 6.3 llm_only_fields
```json
[
{
"target_file": "CH1.4 申请表.docx",
"field_key": "detection_targets",
"field_label": "检测靶标",
"final_value": "ORF1ab、N基因",
"evidence": "预期用途和检测原理章节",
"highlight_reason": "llm_only",
"needs_review": true
}
]
```
### 6.4 conflict_fields
```json
[
{
"field_key": "package_specification",
"field_label": "包装规格",
"rule_value": "规格A24人份/盒、48人份/盒、96人份/盒",
"llm_value": "规格A、规格B均为24/48/96人份",
"selected_value": "规格A24人份/盒、48人份/盒、96人份/盒",
"handling": "规则优先,写入值高亮并进入追溯清单"
}
]
```
### 6.5 risk_notes
```json
[
{
"type": "legacy_doc_adapter_unavailable",
"message": "CH1.9 为 .doc 文件,当前环境未检测到可写入适配器。",
"template_code": "ch1_9_pre_submission"
},
{
"type": "knowledge_base_unavailable",
"message": "标准清单知识库查询不可用,未自动写入候选标准。"
}
]
```
### 6.6 adapter_summary
```json
{
"docx": {
"adapter": "DocxDocumentAdapter",
"status": "available"
},
"doc": {
"adapter": "WordComDocAdapter",
"status": "available",
"fallback_used": false
}
}
```
### 6.7 artifact.metadata
```json
{
"template_code": "ch1_5_product_list",
"strategy": "product_list",
"source_template": "CH1.5 产品列表.docx",
"generated_status": "success",
"highlight_count": 12,
"missing_count": 6,
"llm_only_count": 1,
"adapter": "DocxDocumentAdapter",
"created_by_node": "generate_docs"
}
```
---
## 七、存储路径设计
批次目录:
```text
media/regulatory_info_package/{user_id}/{conversation_id}/{batch_no}/
```
目录结构:
```text
media/regulatory_info_package/12/1001/RIP-20260610153000-abcdef/
templates/
ch1_2_directory.source.docx
ch1_9_pre_submission.source.doc
extracted/
instruction_extract.json
field_extract_result.json
merged_fields.json
generated/
CH1.2 监管信息目录.docx
CH1.4 申请表.docx
CH1.5 产品列表.docx
CH1.9 产品申报前沟通的说明.doc
CH1.11.1 符合标准的清单.docx
CH1.11.5 真实性声明.docx
CH1.11.6 符合性声明.docx
exports/
traceability.xlsx
第1章 监管信息(预生成版).zip
logs/
instruction_extract.json
field_extract_result.json
merged_fields.json
traceability.json
doc_adapter_result.json
```
路径安全要求:
| 要求 | 说明 |
| --- | --- |
| 输出目录校验 | 所有输出路径必须位于当前批次 `work_dir` 下 |
| 原始模板只读 | 不允许覆盖 `docs/0.原始材料` |
| 导出路径 | `ExportedSummaryFile.storage_path` 保存实际文件路径,下载时校验权限 |
---
## 八、权限关系
### 8.1 批次权限
```text
RegulatoryInfoPackageBatch.conversation.user_id == request.user.id
```
### 8.2 输入附件权限
```text
FileAttachment.conversation_id == batch.conversation_id
FileAttachment.user_id == batch.user_id
FileAttachment.upload_status != deleted
```
### 8.3 导出下载权限
`ExportedSummaryFile` 下载时按 `workflow_type` 分支:
```text
workflow_type == "regulatory_info_package"
-> workflow_batch_id 反查 RegulatoryInfoPackageBatch
-> conversation__user == request.user
-> is_deleted == false
```
---
## 九、迁移设计
建议新增一个迁移文件,包含:
| 变更 | 说明 |
| --- | --- |
| 新增 `RegulatoryInfoPackageBatch` | 批次表 |
| 新增 `RegulatoryInfoPackageArtifact` | 产物表 |
| 新增 `RegulatoryInfoPackageNotificationRecord` | 通知记录表 |
| 扩展 `ExportedSummaryFile.ExportType` | 增加 `zip` 枚举 |
Django 模型建议仍集中放在 `review_agent/models.py`,业务逻辑放入 `review_agent/regulatory_info_package/`
---
## 十、DDL 参考
以下 DDL 为 SQLite / Django ORM 参考,实际以 migration 生成为准。
```sql
CREATE TABLE ra_regulatory_info_package_batch (
id integer NOT NULL PRIMARY KEY AUTOINCREMENT,
conversation_id bigint NOT NULL REFERENCES review_agent_conversation(id),
user_id bigint NOT NULL REFERENCES auth_user(id),
trigger_message_id bigint NULL REFERENCES review_agent_message(id),
source_attachment_id bigint NULL REFERENCES ra_file_attachment(id),
source_summary_batch_id bigint NULL REFERENCES ra_file_summary_batch(id),
source_summary_item_id integer NULL,
batch_no varchar(64) NOT NULL UNIQUE,
status varchar(30) NOT NULL,
source_file_name varchar(255) NOT NULL DEFAULT '',
source_storage_path varchar(500) NOT NULL DEFAULT '',
product_name varchar(200) NOT NULL DEFAULT '',
output_zip_name varchar(255) NOT NULL DEFAULT '',
generated_files text NOT NULL DEFAULT '[]',
missing_fields text NOT NULL DEFAULT '[]',
llm_only_fields text NOT NULL DEFAULT '[]',
conflict_fields text NOT NULL DEFAULT '[]',
risk_notes text NOT NULL DEFAULT '[]',
template_config_version varchar(80) NOT NULL DEFAULT '',
template_config_hash varchar(128) NOT NULL DEFAULT '',
adapter_summary text NOT NULL DEFAULT '{}',
work_dir varchar(500) NOT NULL DEFAULT '',
error_message text NOT NULL DEFAULT '',
created_at datetime NOT NULL,
started_at datetime NULL,
finished_at datetime NULL,
archived_at datetime NULL,
is_deleted bool NOT NULL DEFAULT 0
);
CREATE INDEX idx_ra_rip_batch_conv_status
ON ra_regulatory_info_package_batch(conversation_id, status);
CREATE INDEX idx_ra_rip_batch_user_created
ON ra_regulatory_info_package_batch(user_id, created_at);
CREATE INDEX idx_ra_rip_batch_attachment
ON ra_regulatory_info_package_batch(source_attachment_id);
CREATE INDEX idx_ra_rip_batch_summary
ON ra_regulatory_info_package_batch(source_summary_batch_id);
CREATE INDEX idx_ra_rip_batch_created
ON ra_regulatory_info_package_batch(created_at);
```
---
## 十一、实现注意事项
| 注意事项 | 说明 |
| --- | --- |
| JSONField 默认值 | 使用 `default=list``default=dict`,禁止使用可变对象字面量 |
| 外键删除策略 | conversation/user 使用 CASCADE输入附件和文件汇总批次建议 PROTECT 或 SET_NULL避免历史批次断链 |
| `source_summary_item_id` | 当前没有强制外键到 `FileSummaryItem`,可先保存 ID后续需要强约束时再改 FK |
| 工作流节点幂等 | RIP 节点不得只依赖 `WorkflowNodeRun.batch + node_code` 唯一约束;必须使用 `workflow_type + workflow_batch_id + node_code` 保证幂等 |
| `.doc` 失败记录 | `.doc` 原生适配器不可用或执行失败时必须写入 `risk_notes` 和 artifact metadata`.docx` 兜底成功则 generated_files 状态为 `fallback_success` |
| zip 主入口 | zip 导出记录的 `export_category` 固定为 `regulatory_info_package` |
| 单文件下载 | 7 个生成文件也写入 `ExportedSummaryFile`,作为辅助下载 |
| 软删除 | 批次和产物使用 `is_deleted`,下载权限需过滤软删除批次 |
---
## 十二、验收标准
| 序号 | 验收项 | 标准 |
| --- | --- | --- |
| 1 | 模型创建 | 三张 RIP 专项表可通过 migration 创建 |
| 2 | 批次编号 | `batch_no` 唯一,符合 `RIP-...` 格式 |
| 3 | 附件关联 | 批次可绑定直接说明书附件 |
| 4 | 汇总兼容 | 批次可选绑定 `FileSummaryBatch``source_summary_item_id` |
| 5 | 产物留痕 | 模板副本、抽取结果、生成文件、zip、追溯清单均可写 artifact |
| 6 | zip 导出 | `ExportedSummaryFile` 支持 `export_type=zip` |
| 7 | 下载权限 | 非批次所属用户不能下载 RIP 导出 |
| 8 | 节点事件 | `WorkflowNodeRun``WorkflowEvent` 可通过 `workflow_type=regulatory_info_package` 查询 |
| 9 | 节点幂等 | 同一 `workflow_type + workflow_batch_id + node_code` 不会重复创建节点 |
| 10 | 通知记录 | 通知成功、失败和重试次数可落库 |
| 11 | JSON 摘要 | 缺失项、LLM-only、冲突项、风险提示结构符合本文约定 |
---
## 十三、规范依据与裁决
| 规范来源 | 命中规则 | 本设计裁决 |
| --- | --- | --- |
| GYRX 数据库设计流程 | 项目规范优先,未命中时回退基线规范 | 当前项目为 Django/SQLite沿用既有数据库设计文档风格 |
| 既有自动填表数据库设计 | 独立批次、产物、通知三表;大 JSON 文件化;通用导出表复用 | 本功能按同样模式新增 RIP 三表 |
| 自动汇总数据库设计 | 对话隔离、多版本附件、工作流事件留痕 | 输入附件和批次权限沿用该关系 |
| 飞书通知数据库设计 | 通知摘要入库、失败不阻断主流程 | RIP 通知表结构与自动填表通知对齐 |
冲突裁决:技能规范中的低代码/Java 表达不适用于当前 Django 项目,数据库设计以当前项目 ORM、SQLite 兼容和既有 `ra_` 表风格为准。

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 | 补齐单元测试和集成测试 |

View File

@@ -0,0 +1,963 @@
# 第1章监管信息材料包生成详细设计
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 需求分析文档 | docs/1.需求分析/5.第1章监管信息材料包生成.md |
| 功能设计文档 | docs/2.功能设计/5.第1章监管信息材料包生成.md |
| 数据库设计文档 | docs/3.数据库设计/5.第1章监管信息材料包生成.md |
| 参考详细设计 | docs/4.详细设计/3.产品关键信息提取与申报文件自动填表.md |
| 功能名称 | 第1章监管信息材料包生成 |
| 工作流编码 | regulatory_info_package |
| 所属模块 | 审核智能体 review_agent |
| 设计日期 | 2026-06-10 |
| 设计版本 | V1.0 |
---
## 一、详细设计目标
本详细设计用于指导 `regulatory_info_package` 独立工作流开发落地。系统根据用户上传或指定的产品说明书,抽取产品关键信息,基于 `docs/0.原始材料/第1章 监管信息` 下的样例模板生成第1章监管信息材料包并以 `第1章 监管信息(预生成版).zip` 作为对话摘要首位下载入口。
核心约束:
| 约束 | 说明 |
| --- | --- |
| 独立工作流 | 使用 `workflow_type=regulatory_info_package`,拥有独立批次、产物、通知和卡片 |
| 独立模块 | 新增 `review_agent/regulatory_info_package/`,与 `application_form_fill` 平级 |
| 模型集中 | Django 模型仍集中放在 `review_agent/models.py` |
| 节点幂等 | `WorkflowNodeRun` 必须按 `workflow_type + workflow_batch_id + node_code` 幂等创建或加唯一约束 |
| 输入优先级 | 用户消息指定文件名优先;其次 active 附件;再兼容最近成功文件汇总 |
| 模板固定 | 固定处理第1章监管信息 7 个模板 |
| 模板字段化 | 生成逻辑优先写 Word 内容控件 Tag 或稳定占位符,不以手工调整表格格式为前提 |
| 规则优先可演示 | 规则抽取可独立跑通LLM 失败最多重试 3 次,失败后继续 |
| 文档并发生成 | 工作流整体串行,`generate_docs` 节点内部每个文档可独立线程并发处理 |
| `.doc` 兜底 | 能力驱动:有 Word COM/UNO 时优先原生 `.doc`;无原生能力或原生失败时允许生成 `.docx` 兜底文件 |
| zip 只含成功文件 | zip 只打包成功或兜底成功的文件;失败文件不进入 zip |
| 高亮规则 | 缺失和 LLM-only 黄底;冲突黄底红字 |
| 追溯输出 | 用户下载 ExcelJSON 仅保存到后台 logs 目录 |
| 前端最小接入 | 不做多说明书选择 UI不确定时通过对话反问 |
---
## 二、代码结构设计
### 2.1 目录结构
```text
review_agent/
models.py
services.py
skill_router.py
regulatory_info_package/
__init__.py
constants.py
schemas.py
storage.py
events.py
workflow.py
views.py
services/
__init__.py
input_select.py
template_config.py
template_repository.py
instruction_extract.py
field_extract.py
field_merge.py
standard_candidates.py
document_writer.py
docx_document.py
legacy_doc_document.py
package_generate.py
traceability_export.py
zip_export.py
summary.py
notifier.py
templates/
regulatory_info_package_templates_v1.yaml
prompts/
field_extract.md
```
### 2.2 文件职责
| 文件 | 职责 |
| --- | --- |
| constants.py | 工作流编码、节点定义、触发关键词、模板编码、状态常量 |
| schemas.py | dataclass 数据结构,如 `TemplateSpec``InstructionExtractResult``MergedField``GeneratedFileResult` |
| storage.py | 批次目录、子目录、hash、产物创建、路径安全校验 |
| events.py | 记录与序列化 `WorkflowEvent` |
| workflow.py | `RegulatoryInfoPackageWorkflowExecutor`、批次创建、工作流启动 |
| views.py | health、start、status、select-input 接口 |
| input_select.py | 根据用户消息、active 附件、文件汇总选择说明书 |
| template_config.py | YAML 加载、校验、hash |
| template_repository.py | 定位样例模板、复制到批次目录、审计字段 Tag/占位符 |
| instruction_extract.py | 说明书段落、章节、表格和组成成分表解析 |
| field_extract.py | 规则抽取与 LLM 抽取并行执行LLM 最多 3 次重试 |
| field_merge.py | 合并字段输出缺失、LLM-only、冲突和高亮决策 |
| standard_candidates.py | 从说明书抽标准号,调用现有知识库搜索候选 |
| document_writer.py | 文档适配器接口与通用高亮策略 |
| docx_document.py | `DocxDocumentAdapter`,处理 `.docx` |
| legacy_doc_document.py | `LegacyDocDocumentAdapter`,处理 `.doc` 原生写入与 `.docx` 兜底 |
| package_generate.py | 7 个文档生成策略,多线程生成文件 |
| traceability_export.py | 生成 `exports/traceability.xlsx``logs/traceability.json` |
| zip_export.py | 生成主下载 zip只包含成功文件 |
| summary.py | 构造助手回显zip 链接排首位 |
| notifier.py | 写专项通知记录,并调用统一通知服务 |
---
## 三、数据模型详细设计
模型放在 `review_agent/models.py`
### 3.1 RegulatoryInfoPackageBatch
```python
class RegulatoryInfoPackageBatch(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_attachment | 直接选中的说明书附件,可空 |
| source_summary_batch | 兼容文件汇总批次,可空 |
| source_summary_item_id | 文件汇总条目 ID可空 |
| batch_no | `RIP-YYYYMMDDHHMMSS-abcdef` |
| source_file_name | 说明书原文件名 |
| source_storage_path | 说明书存储路径 |
| product_name | 抽取产品名称 |
| output_zip_name | `第1章 监管信息(预生成版).zip` |
| generated_files | 7 个文件状态 |
| missing_fields | 缺失字段 |
| llm_only_fields | LLM-only 字段 |
| conflict_fields | 冲突字段 |
| risk_notes | 风险和降级提示 |
| adapter_summary | doc/docx 适配器实际执行摘要 |
| template_config_version/hash | 模板配置版本和 hash |
| work_dir | 批次工作目录 |
| is_deleted | 软删除 |
### 3.2 RegulatoryInfoPackageArtifact
```python
class RegulatoryInfoPackageArtifact(models.Model):
class ArtifactType(models.TextChoices):
TEMPLATE_COPY = "template_copy", "模板副本"
INSTRUCTION_EXTRACT = "instruction_extract", "说明书抽取结果"
FIELD_EXTRACT_RESULT = "field_extract_result", "字段抽取结果"
MERGED_FIELDS = "merged_fields", "合并字段"
GENERATED_DOCUMENT = "generated_document", "生成文件"
TRACEABILITY = "traceability", "追溯清单"
ZIP_PACKAGE = "zip_package", "ZIP包"
NOTIFICATION_RECORD = "notification_record", "通知记录"
```
`file_format` 包含:`json``excel``docx``doc``zip``markdown`
### 3.3 RegulatoryInfoPackageNotificationRecord
字段对齐自动填表通知记录:`batch``recipient``channel``export_ids``message_summary``send_status``retry_count``external_message_id``error_message``sent_at``is_deleted`
### 3.4 ExportedSummaryFile 扩展
`ExportedSummaryFile.ExportType` 增加:
```python
ZIP = "zip", "ZIP"
```
下载 MIME 按扩展名兜底:
| 条件 | MIME |
| --- | --- |
| zip | application/zip |
| .doc | application/msword |
| .docx | application/vnd.openxmlformats-officedocument.wordprocessingml.document |
---
## 四、常量设计
### 4.1 工作流常量
```python
WORKFLOW_TYPE = "regulatory_info_package"
DEFAULT_ZIP_NAME = "第1章 监管信息(预生成版).zip"
REGULATORY_INFO_PACKAGE_NODE_DEFINITIONS = [
("prepare", "准备资料", "regulatory_info_package"),
("template_copy", "复制模板", "regulatory_info_package"),
("text_extract", "抽取说明书", "regulatory_info_package"),
("field_extract", "抽取字段", "regulatory_info_package"),
("field_merge", "合并字段", "regulatory_info_package"),
("generate_docs", "生成材料", "regulatory_info_package"),
("highlight_review_items", "标记待确认", "regulatory_info_package"),
("trace_export", "追溯清单", "regulatory_info_package"),
("zip_export", "打包下载", "regulatory_info_package"),
("notify", "通知", "regulatory_info_package"),
("completed", "完成", "completed"),
]
```
### 4.2 触发关键词
```python
REGULATORY_INFO_PACKAGE_TRIGGER_KEYWORDS = [
"根据说明书生成第1章监管信息",
"生成监管信息材料包",
"从说明书生成第1章材料",
"第1章监管信息",
"监管信息材料包",
]
```
### 4.3 文件状态
```python
GENERATED_FILE_SUCCESS = "success"
GENERATED_FILE_FALLBACK_SUCCESS = "fallback_success"
GENERATED_FILE_FAILED = "failed"
GENERATED_FILE_SKIPPED = "skipped"
```
---
## 五、核心数据结构
### 5.1 TemplateSpec
```python
@dataclass(frozen=True)
class TemplateSpec:
code: str
output_name: str
source_file: str
file_format: str
strategy: str
include_in_zip: bool
prefer_legacy_doc_native: bool = False
allow_docx_fallback: bool = True
fields: list[dict[str, Any]] = field(default_factory=list)
```
### 5.2 InstructionExtractResult
```python
@dataclass
class InstructionExtractResult:
source_file_name: str
paragraphs: list[str]
sections: dict[str, str]
tables: list[list[list[str]]]
component_tables: list["ComponentTable"]
front_text: str
```
### 5.3 ProductListRow
```python
@dataclass
class ProductListRow:
package_specification: str
item_no: str
composition: str
component_name: str
main_component: str
quantity: str
source_table_title: str
needs_review_fields: list[str] = field(default_factory=list)
```
其中 `item_no` 对应货号,本期固定 `/` 并黄底。
### 5.4 MergedField
```python
@dataclass
class MergedField:
key: str
label: str
value: str
source: str
evidence: str
confidence: float
highlight_reason: str = "none"
needs_review: bool = False
rule_value: str = ""
llm_value: str = ""
```
### 5.5 GeneratedFileResult
```python
@dataclass
class GeneratedFileResult:
template_code: str
file_name: str
requested_format: str
actual_format: str
status: str
path: str = ""
artifact_id: int | None = None
export_id: int | None = None
highlight_count: int = 0
missing_count: int = 0
llm_only_count: int = 0
error_message: str = ""
```
---
## 六、存储目录设计
```text
media/regulatory_info_package/{user_id}/{conversation_id}/{batch_no}/
templates/
logs/
instruction_extract.json
field_extract_result.json
merged_fields.json
doc_adapter_result.json
traceability.json
generated/
CH1.2 监管信息目录.docx
CH1.4 申请表.docx
CH1.5 产品列表.docx
CH1.9 产品申报前沟通的说明.docx
CH1.11.1 符合标准的清单.docx
CH1.11.5 真实性声明.docx
CH1.11.6 符合性声明.docx
exports/
traceability.xlsx
第1章 监管信息(预生成版).zip
```
说明:
| 目录 | 说明 |
| --- | --- |
| templates | 模板副本 |
| logs | 后台 JSON 产物,不作为用户主下载 |
| generated | 生成成功或兜底成功的单文件 |
| exports | 用户可下载的追溯 Excel 和 zip |
---
## 七、输入选择详细设计
### 7.1 选择优先级
`input_select.py` 的选择顺序:
1. 用户消息显式指定文件名时,按 active 附件名模糊匹配。
2. 当前对话 active 附件中文件名包含“说明书”的 `.docx`
3. 当前对话 active 附件中唯一 `.docx`
4. 最近成功 `FileSummaryBatch.items` 中包含“说明书”的 `.docx`
5. 多候选或无候选时返回 `InputSelectionResult(status="waiting_user")`
### 7.2 多候选处理
本期不新增在线选择弹窗。多候选时:
| 场景 | 处理 |
| --- | --- |
| 用户消息可模糊匹配唯一附件 | 直接选择 |
| 多个候选且无法确定 | 对话反问用户确认哪个说明书 |
| 无说明书 | 提示上传产品说明书 |
反问示例:
```text
我找到多个说明书候选请回复要使用的文件名A.docx、B.docx。
```
---
## 八、模板配置详细设计
配置路径:
```text
review_agent/regulatory_info_package/templates/regulatory_info_package_templates_v1.yaml
```
必须包含 7 个模板:
| code | source_file | strategy |
| --- | --- | --- |
| ch1_2_directory | CH1.2 监管信息目录.docx | directory |
| ch1_4_application_form | CH1.4 申请表.docx | application_form |
| ch1_5_product_list | CH1.5 产品列表.docx | product_list |
| ch1_9_pre_submission | CH1.9 产品申报前沟通的说明.doc | pre_submission |
| ch1_11_1_standard_list | CH1.11.1 符合标准的清单.docx | standard_list |
| ch1_11_5_authenticity | CH1.11.5 真实性声明.docx | authenticity_statement |
| ch1_11_6_compliance | CH1.11.6 符合性声明.docx | compliance_statement |
校验规则:
| 校验 | 说明 |
| --- | --- |
| version 必填 | 写入批次 |
| source_dir 存在 | 指向样例目录 |
| code 唯一 | 防止覆盖产物 |
| source_file 存在 | 缺失则配置错误 |
| strategy 合法 | 必须命中生成策略 |
| doc 模板标记 | `.doc` 模板需声明 `prefer_legacy_doc_native`,并配置允许 `.docx` 兜底 |
### 8.1 模板字段化约定
为避免生成时破坏 Word 表格、复选框、字号、缩进和合并单元格,本工作流优先使用字段化模板:
| 方式 | 使用场景 | 说明 |
| --- | --- | --- |
| Word 内容控件 Tag | 正式模板优先 | 在 Word 中为产品名、申请人、复选框、日期、说明文字等填写区设置稳定 Tag代码按 Tag 写入 |
| 稳定占位符 | 过渡方案 | 使用 `{{ product_name }}` 等不会影响版式的占位符,代码替换占位符所在 run |
| 行标签定位 | 兜底方案 | 仅用于未字段化的旧模板,必须保留原单元格、段落和 run 格式 |
模板配置中的字段目标优先级:
```yaml
targets:
- type: content_control_tag
tag: product_name
- type: placeholder
marker: "{{ product_name }}"
- type: table_row_label
label: 产品名称
```
模板加载时必须执行字段审计:关键字段缺少 Tag/占位符时给出清晰错误或降级说明;不得静默使用会破坏格式的整格重建策略。
---
## 九、字段抽取详细设计
### 9.1 规则抽取
规则抽取必须独立可用,覆盖:
| 字段 | 规则 |
| --- | --- |
| product_name | `【产品名称】` 下一段 |
| package_specification | `【包装规格】` 至下一章节 |
| intended_use | `【预期用途】` 至下一章节 |
| detection_principle | `【检测原理】` 至下一章节 |
| main_components | `【主要组成成分】` 下方表格摘要 |
| storage_condition_and_validity | `【储存条件及有效期】` 至下一章节 |
| sample_type | 样本要求章节中的“适用样本类型” |
| detection_targets | 预期用途/检测原理中的基因、病原体、靶标 |
| applicable_instruments | `【适用仪器】` 至下一章节 |
| test_method | `【检验方法】` 摘要 |
| standards | 正则抽取标准号 |
### 9.2 LLM 抽取与重试
`field_extract.py` 并行执行规则抽取和 LLM 抽取:
```text
ThreadPoolExecutor(max_workers=2)
-> rule_extract()
-> llm_extract_with_retry(max_attempts=3)
```
LLM 重试策略:
| 次数 | 间隔 |
| --- | --- |
| 第 1 次 | 立即 |
| 第 2 次 | 等待 1 秒 |
| 第 3 次 | 等待 2 秒 |
三次失败后:
| 产物 | 处理 |
| --- | --- |
| risk_notes | 增加 `llm_extract_failed` |
| logs/field_extract_result.json | 记录每次错误摘要 |
| 工作流 | 继续使用规则结果 |
LLM 不允许填企业信息、分类编码、管理类别、临床评价路径等说明书无法证明的内容。
### 9.3 字段合并
| 场景 | 写入值 | 高亮 | needs_review |
| --- | --- | --- | --- |
| rule 与 LLM 一致 | rule/LLM 值 | 否 | 否 |
| rule 与 LLM 冲突 | 规则优先或配置优先 | 黄底红字 | 是 |
| rule 缺失、LLM 命中 | LLM 值 | 黄底 | 是 |
| 全部缺失 | `/` | 黄底 | 是 |
---
## 十、文档适配器详细设计
### 10.1 统一接口
```python
class DocumentAdapter(Protocol):
def replace_text(self, old: str, new: str, *, highlight: bool = False, conflict: bool = False) -> int: ...
def fill_table_cell(self, row_label: str, value: str, *, highlight: bool = False, conflict: bool = False) -> bool: ...
def replace_table(self, marker: str, rows: list[ProductListRow], *, highlight_columns: list[str] | None = None) -> bool: ...
def save(self, path: Path) -> Path: ...
```
高亮规则:
| 类型 | 视觉 |
| --- | --- |
| missing | 黄色底色 |
| llm_only | 黄色底色 |
| conflict | 黄色底色 + 红色字体 |
### 10.2 DocxDocumentAdapter
实现能力:
| 方法 | 说明 |
| --- | --- |
| replace_text | 支持段落与表格中的文本替换,需处理 run 拆分 |
| fill_content_control | 按内容控件 Tag 填写文本、日期或复选框 |
| replace_placeholder | 按稳定占位符替换文本,保留占位符所在 run/段落格式 |
| fill_table_cell | 按行标签定位目标单元格,仅作为未字段化模板的兜底 |
| replace_table | 重建 CH1.5 产品列表表格 |
| apply_highlight | 使用 `w:shd` 设置黄色底色 |
| apply_conflict_style | 黄色底色 + 红字 |
### 10.3 LegacyDocDocumentAdapter
接口:
```python
class AdapterCapability:
adapter_name: str
supports_native_doc_write: bool
supports_docx_fallback: bool
status: str
error_message: str = ""
class LegacyDocDocumentAdapter:
@staticmethod
def detect_available_adapter() -> AdapterCapability: ...
```
执行顺序:
1. 执行能力探测Word COM、LibreOffice UNO 或其他可写 `.doc` 能力。
2. 有原生能力时优先尝试原生打开 `.doc` 并保存 `.doc`
3. 无原生能力或原生失败时,尝试生成同语义 `.docx` 兜底文件,再交给 `DocxDocumentAdapter`
4. 兜底成功时,输出 `CH1.9 产品申报前沟通的说明.docx`,状态为 `fallback_success`
5. 原生和兜底均失败时,该文件状态为 `failed`,不进入 zip。
兜底成功 `adapter_summary.doc`
```json
{
"requested_format": "doc",
"actual_format": "docx",
"adapter": "ConversionFallbackAdapter",
"status": "fallback_success"
}
```
---
## 十一、材料生成详细设计
### 11.1 generate_docs 节点并发
工作流节点仍串行执行,但 `generate_docs` 内部并发生成单文件:
```python
with ThreadPoolExecutor(max_workers=min(7, len(specs))) as executor:
futures = [executor.submit(generate_one_document, spec, context) for spec in specs]
```
并发注意事项:
| 注意事项 | 说明 |
| --- | --- |
| 每个文档使用独立模板副本 | 避免并发写同一文件 |
| 共享字段只读 | `merged_fields``product_list_rows` 不在子线程修改 |
| 数据库写入集中处理 | 子线程返回 `GeneratedFileResult`,主线程统一写 artifact/export |
| 异常隔离 | 单文件失败不影响其他文件 |
### 11.2 7 个生成策略
| 模板 | 输出规则 |
| --- | --- |
| CH1.2 | 替换产品名;页码沿用样例 |
| CH1.4 | 填产品名、包装规格、预期用途、组成、储存有效期、方法原理;企业/分类等缺失项 `/` 黄底 |
| CH1.5 | 按样例表头重建,货号 `/` 黄底 |
| CH1.9 | 优先 `.doc` 原生写入;失败则 `.docx` 兜底;兜底失败则不输出 |
| CH1.11.1 | 说明书标准号直接写;知识库候选只作为待确认高亮/追溯 |
| CH1.11.5 | 保留正文,替换产品名,公司名 `/` 黄底,日期当天 |
| CH1.11.6 | 保留正文,替换产品名,公司名 `/` 黄底,日期当天 |
### 11.3 产品名缺失
规则和 LLM 都抽不到产品名称时:
| 项 | 处理 |
| --- | --- |
| 文件内容 | 产品名位置写 `/` 并黄底 |
| 批次状态 | 至少 `partial_success` |
| zip | 仍生成,包含成功文件 |
| 摘要 | 明确提示产品名称待确认 |
---
## 十二、追溯与 zip 设计
### 12.1 追溯 Excel
用户可下载:
```text
exports/traceability.xlsx
```
创建导出记录:
```text
export_category = traceability
export_type = excel
```
字段:
| 字段 | 说明 |
| --- | --- |
| target_file | 目标文件 |
| target_field | 目标字段 |
| final_value | 写入值 |
| extraction_source | rule、llm、missing、knowledge_candidate |
| evidence | 来源片段 |
| highlight_reason | missing、llm_only、conflict、rag_candidate |
| needs_review | 是否需复核 |
### 12.2 后台 JSON
JSON 产物仅写入 `logs/`,按需从后台查看:
```text
logs/instruction_extract.json
logs/field_extract_result.json
logs/merged_fields.json
logs/traceability.json
logs/doc_adapter_result.json
```
这些 JSON 产物写入 `RegulatoryInfoPackageArtifact`,但不作为用户主下载。
### 12.3 zip 打包
zip 文件名:
```text
第1章 监管信息(预生成版).zip
```
规则:
| 场景 | 是否进入 zip |
| --- | --- |
| 文件状态 `success` | 是 |
| 文件状态 `fallback_success` | 是 |
| 文件状态 `failed` | 否 |
| 文件状态 `skipped` | 否 |
`CH1.9 .doc` 兜底 `.docx` 成功zip 中放入:
```text
CH1.9 产品申报前沟通的说明.docx
```
---
## 十三、工作流详细设计
### 13.1 批次创建
```python
def create_regulatory_info_package_batch(
*,
conversation: Conversation,
user,
trigger_message: Message | None = None,
source_attachment: FileAttachment | None = None,
source_summary_batch: FileSummaryBatch | None = None,
source_summary_item_id: int | None = None,
) -> RegulatoryInfoPackageBatch:
```
创建后初始化 `REGULATORY_INFO_PACKAGE_NODE_DEFINITIONS`
### 13.2 执行器
```python
class RegulatoryInfoPackageWorkflowExecutor:
def run(self) -> None: ...
def _nodes(self): ...
def _run_node(self, node: WorkflowNodeRun) -> None: ...
def _execute_node(self, node: WorkflowNodeRun) -> None: ...
```
节点执行:
| 节点 | 关键动作 |
| --- | --- |
| prepare | 确认说明书,或 waiting_user |
| template_copy | 复制 7 个模板 |
| template_audit | 审计模板字段 Tag/占位符,记录缺失和降级策略 |
| text_extract | 抽取说明书章节和表格 |
| field_extract | 规则 + LLM 并行抽取 |
| field_merge | 合并字段、高亮决策 |
| generate_docs | 多线程生成单文件 |
| highlight_review_items | 若生成策略已完成高亮,该节点记录确认结果即可 |
| trace_export | 写 Excel 和 logs JSON |
| zip_export | 打包成功/兜底成功文件 |
| notify | 写专项通知并调用统一通知 |
| completed | 写助手摘要 |
### 13.3 状态落定
| 条件 | 状态 |
| --- | --- |
| zip 成功且 7 个文件均 success/fallback_success | success |
| zip 成功但有 failed/skipped | partial_success |
| zip 失败但至少一个单文件成功 | partial_success |
| 全部文件失败或关键输入缺失 | failed |
| 多说明书候选等待确认 | waiting_user |
---
## 十四、路由与接口详细设计
### 14.1 skill_router.py
增加:
| 项 | 内容 |
| --- | --- |
| ROUTE_ACTIONS | 加入 `regulatory_info_package` |
| SkillRoute 属性 | `starts_regulatory_info_package` |
| deterministic route | 命中触发关键词直接返回 |
| LLM prompt | action 列表加入 `regulatory_info_package` |
### 14.2 services.py
`stream_message` 增加分支:
1. 调用 `select_instruction_input(conversation, content)`
2. 若多候选,回复反问,不启动工作流。
3. 若无候选,回复请上传说明书。
4. 若唯一候选,创建批次并启动工作流。
5. SSE 发送 `workflow_started`
### 14.3 views.py
接口:
```text
GET /api/review-agent/regulatory-info-package/health/
POST /api/review-agent/regulatory-info-package/start/
GET /api/review-agent/regulatory-info-package/<batch_id>/status/
POST /api/review-agent/regulatory-info-package/<batch_id>/select-input/
```
`status` 返回:
| 字段 | 说明 |
| --- | --- |
| batch | 状态、产品名、缺失/LLM-only/冲突数量 |
| nodes | 节点状态 |
| generated_files | 7 个文件成功/失败/兜底状态 |
| exports | zip、单文件、Excel 下载 |
| risk_notes | 风险提示 |
| notifications | 通知 |
zip 不需要 `is_primary` 字段,前端或摘要按返回顺序把 zip 放首位。
---
## 十五、助手摘要设计
完成消息结构:
```markdown
已生成第1章监管信息材料包。
批次号RIP-...
产品名称:...
状态success / partial_success
主下载:[第1章 监管信息(预生成版).zip](...)
| 文件 | 状态 | 下载/原因 |
| --- | --- | --- |
| CH1.2 监管信息目录.docx | 成功 | 下载 |
| CH1.9 产品申报前沟通的说明.docx | 兜底成功 | 下载 |
| CH1.11.1 符合标准的清单.docx | 失败 | 失败原因 |
待确认:缺失项 X 个LLM复核项 Y 个,冲突项 Z 个。
```
要求:
| 要求 | 说明 |
| --- | --- |
| zip 首位 | zip 链接必须在单文件列表之前 |
| 失败可见 | 失败文件展示状态和原因,无下载链接 |
| 兜底提示 | `.doc -> .docx` 时显示“兜底成功” |
| 待确认摘要 | 展示 missing、llm_only、conflict 数量 |
---
## 十六、前端详细设计
### 16.1 模板
`templates/home.html` 增加工具 chip
```html
<button
class="tool-chip"
type="button"
data-prompt-template="根据说明书生成第1章监管信息"
>第1章监管信息</button>
```
`summaryPanel` 增加:
```html
data-regulatory-info-package-status-url-template="/api/review-agent/regulatory-info-package/__batch_id__/status/"
```
### 16.2 app.js
增加:
| 位置 | 处理 |
| --- | --- |
| workflow type 判断 | 支持 `regulatory_info_package` |
| 状态 URL 选择 | 使用 `data-regulatory-info-package-status-url-template` |
| 终态判断 | success、partial_success、failed、waiting_user |
| 导出展示 | 直接按 exports 返回顺序展示zip 在后端排首位 |
### 16.3 不做选择 UI
多说明书候选时,本期不做弹窗。通过对话反问用户确认文件名。
---
## 十七、导出下载权限
`file_summary.views._export_for_user` 增加:
```python
if exported.workflow_type == "regulatory_info_package":
allowed = RegulatoryInfoPackageBatch.objects.filter(
pk=exported.workflow_batch_id,
conversation__user=user,
is_deleted=False,
).exists()
return exported if allowed else None
```
下载 content type 增加 zip 和 `.doc` 后缀判断。
---
## 十八、通知详细设计
`notifier.py`
```python
def notify_completion(batch: RegulatoryInfoPackageBatch, exports: list[ExportedSummaryFile]) -> RegulatoryInfoPackageNotificationRecord:
```
处理:
| 步骤 | 说明 |
| --- | --- |
| 创建专项通知记录 | 写 `RegulatoryInfoPackageNotificationRecord` |
| 调用统一通知 | `dispatch_workflow_notification(build_regulatory_info_package_context(batch))` |
| 捕获异常 | 通知失败写记录和 risk_notes不影响批次下载 |
---
## 十九、测试详细设计
| 测试文件 | 覆盖 |
| --- | --- |
| test_regulatory_info_package_models.py | 三张表、zip export type、基础关联 |
| test_regulatory_info_package_trigger.py | 固定关键词与 LLM action |
| test_regulatory_info_package_input_select.py | 文件名模糊匹配、active 附件、多候选反问 |
| test_regulatory_info_package_template_config.py | YAML 加载、模板缺失、code 唯一 |
| test_regulatory_info_package_instruction_extract.py | 说明书章节和组成表抽取 |
| test_regulatory_info_package_field_extract.py | 规则抽取、LLM 三次重试、失败降级 |
| test_regulatory_info_package_field_merge.py | missing、llm_only、conflict |
| test_regulatory_info_package_docx_writer.py | 替换、表格填充、黄底、红字 |
| test_regulatory_info_package_legacy_doc.py | adapter 探测、docx 兜底、失败状态 |
| test_regulatory_info_package_package_generate.py | 7 文件生成结果、多线程异常隔离 |
| test_regulatory_info_package_traceability.py | Excel 追溯和 logs JSON |
| test_regulatory_info_package_zip.py | zip 只包含 success/fallback_success |
| test_regulatory_info_package_workflow.py | 节点流转、partial_success、waiting_user |
| test_regulatory_info_package_views.py | start/status/download 权限 |
| test_regulatory_info_package_frontend.py | chip、卡片、状态 URL |
---
## 二十、异常处理矩阵
| 异常 | 批次状态 | 处理 |
| --- | --- | --- |
| 无说明书 | waiting_user 或不创建批次 | 提示上传说明书 |
| 多候选无法匹配 | waiting_user 或不创建批次 | 反问确认文件名 |
| 模板缺失 | failed | 列出缺失模板 |
| 规则抽取失败 | partial_success/continue | 使用 LLM 结果 |
| LLM 三次失败 | continue | 使用规则结果,写 risk_notes |
| 产品名缺失 | partial_success | 写 `/` 黄底,继续生成 zip |
| 单个 docx 文件生成失败 | partial_success | 不进入 zip摘要展示失败 |
| CH1.9 doc 原生失败但 docx 兜底成功 | success/partial_success | 状态 fallback_success进入 zip |
| CH1.9 doc 和 docx 兜底均失败 | partial_success | 不进入 zip摘要展示失败 |
| traceability.xlsx 失败 | partial_success | 不阻断 zip |
| zip 失败 | partial_success | 保留单文件下载 |
| 通知失败 | 不影响主状态 | 写通知失败和 risk_notes |
---
## 二十一、设计结论
| 编号 | 结论 |
| --- | --- |
| D1 | 详细设计文档路径为 `docs/4.详细设计/5.第1章监管信息材料包生成.md` |
| D2 | 模型集中在 `review_agent/models.py`,业务模块为 `review_agent/regulatory_info_package/` |
| D3 | `.doc` 采用能力驱动策略:探测 Word COM/UNO 等原生能力,有能力时优先原生处理 |
| D4 | `.doc` 无原生能力或原生失败时允许 `.docx` 兜底;兜底文件名为 `CH1.9 产品申报前沟通的说明.docx` |
| D5 | zip 只包含成功或兜底成功文件,失败文件不进入 zip |
| D6 | LLM 最多重试 3 次,失败后使用规则结果继续 |
| D7 | 缺失和 LLM-only 黄底,冲突黄底红字 |
| D8 | 产品列表使用 `ProductListRow`,货号固定 `/` 黄底 |
| D9 | 标准清单只复用现有知识库能力,不新增独立 RAG 流程 |
| D10 | 前端最小接入,不做说明书选择弹窗 |
| D11 | 追溯 Excel 可下载JSON 只放后台 logs |
| D12 | 本期不新增字段级数据库表 |
| D13 | 工作流串行,文档生成节点内部可多线程 |
| D14 | 模板优先字段化,正式填充路径使用内容控件 Tag 或稳定占位符,行标签定位仅作为兜底 |
| D15 | 本轮只产出详细设计,不写代码、不生成迁移 |

View File

@@ -0,0 +1,74 @@
# 自动汇总前端线框图
## 评审目标
在实现三栏页面前,先确认审核智能体工作台的信息架构、右侧文件汇总面板、工作流状态展示和移动端降级方式。
## 桌面端布局
```mermaid
flowchart LR
A["左栏:会话列表<br/>新对话 / 搜索 / 历史会话"] --> B["中栏:聊天区<br/>顶部导航 / 消息流 / 输入框"]
B --> C["右栏:文件汇总面板"]
C --> C1["上半区:上传区<br/>拖拽上传 / 选择文件 / 上传状态"]
C --> C2["中段:当前对话附件<br/>文件名 / 版本 / 大小 / 状态 / 删除"]
C --> C3["下半区:工作流卡片<br/>批次号 / 节点进度 / 下载入口"]
```
## 右侧面板结构
```mermaid
flowchart TB
P["文件汇总面板"] --> U["上传拖拽区"]
U --> U0["无附件:提示上传文件或压缩包"]
U --> U1["上传中:显示文件名和处理中状态"]
U --> U2["上传失败:展示错误并允许重试"]
P --> L["附件列表"]
L --> L1["active 版本优先展示"]
L --> L2["历史版本保留展示"]
L --> L3["逻辑删除后从默认候选移除"]
P --> W["工作流卡片列表"]
W --> W1["运行中:节点逐项更新"]
W --> W2["成功:展示 Markdown/Excel 下载"]
W --> W3["失败:展示失败节点和错误说明"]
```
## 工作流状态流转
```mermaid
stateDiagram-v2
[*] --> Pending: 用户上传附件
Pending --> Running: 发送自动汇总提示词
Running --> Extracting: 固化附件
Extracting --> Scanning: 解压完成或跳过
Scanning --> Counting: 生成文件清单
Counting --> Detecting: 页数统计完成
Detecting --> Reporting: 产品名识别完成
Reporting --> Success: 生成报告与下载
Running --> Failed: 批次级异常
Extracting --> Failed: 解压安全检查失败
Reporting --> Failed: 报告生成失败
Success --> Restored: 刷新页面后状态恢复
Failed --> Restored: 刷新页面后状态恢复
```
## 移动端布局
```mermaid
flowchart TB
M["移动端工作台"] --> T["顶部:侧栏按钮 / 当前页面 / 用户菜单"]
T --> Chat["聊天区优先展示"]
Chat --> Composer["底部输入框"]
T --> Drawer["会话侧栏抽屉"]
Chat --> Panel["文件汇总面板下移或折叠"]
Panel --> Upload["上传区"]
Panel --> Workflow["工作流卡片"]
```
## 关键评审点
- 桌面端保持左侧会话、中间聊天、右侧文件汇总三栏,不改变现有聊天主路径。
- 右侧面板上半部分用于上传和附件列表,下半部分用于批次工作流卡片。
- 工作流卡片节点顺序固定为:附件固化、压缩包解压、文件扫描、页数统计、产品识别、报告输出、完成。
- 助手消息中的文件汇总结果使用安全 Markdown 渲染,用户消息仍按纯文本转义。
- 移动端优先保证聊天可用,文件汇总面板折叠或下移,不能遮挡输入框。

View File

@@ -0,0 +1,634 @@
# 自动汇总文件夹文件目录与页数流程开发计划
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 需求分析文档 | docs/1.需求分析/1.自动汇总.md |
| 功能设计文档 | docs/2.功能设计/1.自动汇总.md |
| 详细设计文档 | docs/3.详细设计/1.自动汇总.md |
| 数据库设计文档 | docs/4.数据库设计/1.自动汇总.md |
| 功能名称 | 自动汇总文件夹文件目录与页数 |
| 所属模块 | 审核智能体 review_agent |
| 执行方式 | 单人开发 + Codex 流水线自动化执行 |
| 计划日期 | 2026-06-05 |
| 计划版本 | V1.0 |
---
## 一、开发计划目标
本开发计划用于指导 Codex 按阶段自动完成“自动汇总文件夹文件目录与页数”功能开发。任务拆分按可交付阶段组织,每个任务都需要具备明确目标、涉及文件、前置依赖、开发步骤、验收标准、验证命令和 Codex 执行提示。
本功能不按 MVP 缩减范围,必须按需求分析、功能设计、详细设计、数据库设计中的全部范围完成。
---
## 二、已确认开发规则
| 规则项 | 内容 |
| --- | --- |
| 拆分方式 | 按可交付阶段拆分 |
| 任务粒度 | 每个任务写到可直接交给 Codex 执行 |
| 执行对象 | 一个开发者使用 Codex 流水线自动化执行 |
| 单任务范围 | 尽量控制在 1 到 3 类文件 |
| Codex 提示 | 每个任务都提供“Codex 执行提示” |
| 功能范围 | 必须完成全部需求,不允许降级为最小闭环 |
| 前端验证 | 使用 Playwright 做真实浏览器端到端测试 |
| 测试数据 | 测试代码中可动态创建登录用户和临时文件 |
| Git 提交 | 每个阶段完成并验证通过后提交一次 |
| 提交摘要 | 使用执行机器上的 `git-commit-summary` skill |
| 分支规则 | 从 `V2` 创建日期 + 中文功能名分支,完成后合并回 `V2` |
---
## 三、总体验收标准
| 类别 | 完成标准 |
| --- | --- |
| 数据库 | 7 张 `ra_` 表全部通过 Django migration 落库,约束、索引、枚举齐全 |
| 上传 | 当前对话右侧上传区支持多文件和压缩包上传,上传即存储,附件不跨对话 |
| 触发 | 用户发送命中提示词后才启动自动汇总工作流,普通对话不误触发 |
| 工作流 | 后台异步执行,节点状态可实时更新,事件可持久化和恢复 |
| 解压 | 支持 zip、7z、rar解压安全检查必须完成 |
| 统计 | 支持 pdf、doc、docx、xls、xlsx、ppt、pptx失败重试 3 次,失败不阻断批次 |
| 输出 | 生成 Markdown 报告、Excel 明细,对话框展示 Markdown 简表和下载链接 |
| 前端 | 三栏布局、上方拖拽上传、下方工作流卡片、Markdown 表格渲染正常 |
| 存档 | 批次、附件、文件明细、节点、事件、导出文件全部入库 |
| 标题 | 识别到产品名后按规则更新对话标题 |
| 权限 | 上传、查询、下载都校验当前用户和当前对话 |
| 测试 | 单元、接口、集成、Playwright 端到端测试全部覆盖 |
| 部署 | requirements 可安装Docker 部署说明包含 7z/p7ziprar/7z 解压验证通过 |
---
## 四、阶段总览
| 阶段 | 名称 | 目标 | 阶段验收 |
| --- | --- | --- | --- |
| P0 | 流水线准备 | 建立开发分支,确认依赖、规范和现状 | 分支创建完成,开发前检查通过 |
| P1 | 数据模型与迁移 | 完成 7 张 ra_ 表 ORM 与 migration | SQLite 可建表,模型约束正确 |
| P2 | 上传与对话绑定 | 实现上传即存储、同名版本和附件权限 | 上传接口可用,附件不跨对话 |
| P3 | 工作流触发与后台执行 | 实现提示词触发、批次创建、后台节点执行和事件持久化 | 命中提示词可启动工作流,状态可查询 |
| P4 | Skill 与文件处理能力 | 实现解压、扫描、页数统计、重试和产品名识别 | 支持格式全部进入处理流程 |
| P5 | 报告生成与下载 | 实现 Markdown 报告、Excel 导出、下载权限和助手消息 | 可下载报告,数据库留痕完整 |
| P6 | 前端三栏与工作流卡片 | 实现右侧上传区、工作流卡片、SSE 更新和 Markdown 渲染 | Playwright 验证前端主流程 |
| P7 | 测试、部署与总体验收 | 补齐自动化测试、端到端测试、Docker 说明和最终合并 | 全部测试通过,合并回 V2 |
---
## 五、P0 流水线准备
### FS-P0-001 创建开发分支并检查现状
| 项目 | 内容 |
| --- | --- |
| 任务类型 | Git / 准备 |
| 前置任务 | 无 |
| 涉及文件 | 无固定文件 |
| 目标 | 从 `V2` 分支创建日期 + 中文功能名开发分支,并确认工作区状态 |
| 开发步骤 | 1. 切换到 `V2`2. 拉取或确认本地最新状态3. 创建 `codex/YYYYMMDD-自动汇总文件目录页数` 分支4. 检查 `git status`5. 确认已有设计文档存在 |
| 验收标准 | 开发分支创建成功;工作区变更来源清楚;不会覆盖用户已有未提交改动 |
| 验证命令 | `git branch --show-current`; `git status --short` |
| Codex 执行提示 | 请从 `V2` 创建 `codex/YYYYMMDD-自动汇总文件目录页数` 开发分支,检查当前工作区状态,不要回滚用户已有变更。 |
### FS-P0-002 补充依赖清单与部署前置说明
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 依赖 / 部署准备 |
| 前置任务 | FS-P0-001 |
| 涉及文件 | `requirements.txt`、部署说明文档、前端静态资源引入位置 |
| 目标 | 增加文件解析与导出所需 Python 依赖,并说明 rar/7z 的系统依赖 |
| 开发步骤 | 1. 在 `requirements.txt` 增加 `pypdf``python-docx``python-pptx``openpyxl``xlrd``olefile``py7zr`2. 在前端任务中明确 `marked + DOMPurify` 通过模板或静态资源引入3. 在部署说明中写明 Docker 需要安装 7z/p7zip4. 明确不强制依赖 LibreOffice |
| 验收标准 | Python 依赖可安装;部署说明明确 rar 依赖系统 7z/p7zip未引入 LibreOffice 强依赖 |
| 验证命令 | `pip install -r requirements.txt` |
| Codex 执行提示 | 请按详细设计补充轻量依赖,并在部署说明中写清 Docker 需安装 7z/p7zip 支持 rar/7z禁止把 LibreOffice 作为必需依赖。 |
---
## 六、P1 数据模型与迁移
### FS-P1-001 新增文件汇总 ORM 模型
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 数据库 / 后端 |
| 前置任务 | P0 |
| 涉及文件 | `review_agent/models.py` |
| 目标 | 新增文件汇总相关 7 个模型和状态枚举 |
| 开发步骤 | 1. 定义 `FileAttachment`2. 定义 `FileSummaryBatch`3. 定义 `FileSummaryBatchAttachment`4. 定义 `FileSummaryItem`5. 定义 `WorkflowNodeRun`6. 定义 `WorkflowEvent`7. 定义 `ExportedSummaryFile`8. 使用 Django `TextChoices` 管理枚举 |
| 验收标准 | 模型字段、关联、默认值、`db_table``indexes``constraints` 与数据库设计一致 |
| 验证命令 | `python manage.py check` |
| Codex 执行提示 | 请按 `docs/4.数据库设计/1.自动汇总.md``review_agent/models.py` 新增 7 个 `ra_` 表模型,使用 Django ORM、TextChoices、短表名、索引和唯一约束。 |
### FS-P1-002 生成并验证数据库迁移
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 数据库 / 迁移 |
| 前置任务 | FS-P1-001 |
| 涉及文件 | `review_agent/migrations/` |
| 目标 | 生成 migration 并验证 SQLite 可落表 |
| 开发步骤 | 1. 执行 `makemigrations`2. 检查 migration 是否只包含本功能相关模型3. 执行 `migrate`4. 检查表结构和索引 |
| 验收标准 | migration 可执行SQLite 中生成 7 张 `ra_` 表;约束和索引生效 |
| 验证命令 | `python manage.py makemigrations review_agent`; `python manage.py migrate`; `python manage.py check` |
| Codex 执行提示 | 请为文件汇总模型生成 Django migration 并执行迁移验证,确保 SQLite 下 7 张 `ra_` 表均可创建。 |
### FS-P1-003 增加模型级测试
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 测试 / 数据库 |
| 前置任务 | FS-P1-002 |
| 涉及文件 | `tests/test_file_summary_models.py` |
| 目标 | 覆盖附件版本、批次绑定、唯一约束和权限查询基础逻辑 |
| 开发步骤 | 1. 测试同一对话同名附件版本号递增2. 测试 active 版本切换3. 测试批次绑定附件唯一4. 测试同批次 relative_path 唯一5. 测试导出文件能追溯到用户和对话 |
| 验收标准 | 模型测试全部通过,关键约束失败时能暴露错误 |
| 验证命令 | `pytest tests/test_file_summary_models.py` |
| Codex 执行提示 | 请新增模型级测试,覆盖文件汇总表的版本、绑定、唯一约束和对话隔离规则。 |
### P1 阶段提交规则
| 项目 | 内容 |
| --- | --- |
| 阶段验证 | `python manage.py check`; `pytest tests/test_file_summary_models.py` |
| 提交动作 | 调用 `git-commit-summary` 生成提交摘要并提交 |
| Codex 执行提示 | P1 验证通过后,请调用 `git-commit-summary` 分析本阶段变更,并按其输出提交到当前开发分支。 |
---
## 七、P2 上传与对话绑定
### FS-P2-001 实现附件存储服务
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 后端 / 存储 |
| 前置任务 | P1 |
| 涉及文件 | `review_agent/file_summary/storage.py``review_agent/file_summary/constants.py` |
| 目标 | 实现上传文件保存、版本号生成、存储目录生成和逻辑删除基础能力 |
| 开发步骤 | 1. 创建 `file_summary` 目录2. 实现按 `user/conversation/attachments` 保存文件3. 实现同名附件版本递增4. 新版本设为 active 并关闭旧 active5. 实现路径安全处理 |
| 验收标准 | 上传文件保存到受控目录;附件记录绑定当前用户和对话;同名多版本不覆盖 |
| 验证命令 | `pytest tests/test_file_summary_storage.py` |
| Codex 执行提示 | 请实现文件汇总附件存储服务,保证上传即存储、同名多版本、当前对话绑定和路径安全。 |
### FS-P2-002 实现附件上传接口
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 后端 / 接口 |
| 前置任务 | FS-P2-001 |
| 涉及文件 | `review_agent/file_summary/views.py``review_agent/file_summary/urls.py``config/urls.py` |
| 目标 | 新增对话附件上传接口,支持多文件和压缩包上传 |
| 开发步骤 | 1. 新增 `POST /api/review-agent/conversations/{conversation_id}/attachments/`2. 校验 conversation 属于 request.user3. 保存多个文件4. 返回 attachment 列表5. 接入 URL |
| 验收标准 | 当前用户只能向自己的对话上传;接口返回附件 ID、文件名、大小、版本和状态 |
| 验证命令 | `pytest tests/test_file_summary_views.py -k upload` |
| Codex 执行提示 | 请新增对话附件上传 API支持一次上传多个文件所有附件必须绑定当前 Conversation禁止跨用户上传。 |
### FS-P2-003 实现附件列表和删除接口
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 后端 / 接口 |
| 前置任务 | FS-P2-002 |
| 涉及文件 | `review_agent/file_summary/views.py``review_agent/file_summary/urls.py` |
| 目标 | 支持前端右侧上传区展示当前对话附件,并允许逻辑删除 |
| 开发步骤 | 1. 新增当前对话附件列表接口2. 返回 active 和历史版本信息3. 新增附件逻辑删除接口4. 删除时设置 `upload_status=deleted``is_active=false` |
| 验收标准 | 附件列表只返回当前对话文件;逻辑删除不影响历史批次追溯 |
| 验证命令 | `pytest tests/test_file_summary_views.py -k attachment` |
| Codex 执行提示 | 请实现当前对话附件列表和逻辑删除接口,支持同名版本展示,删除不得物理移除历史批次需要的文件。 |
### P2 阶段提交规则
| 项目 | 内容 |
| --- | --- |
| 阶段验证 | `pytest tests/test_file_summary_storage.py tests/test_file_summary_views.py -k "upload or attachment"` |
| 提交动作 | 调用 `git-commit-summary` 生成提交摘要并提交 |
| Codex 执行提示 | P2 验证通过后,请调用 `git-commit-summary` 分析本阶段变更,并按其输出提交到当前开发分支。 |
---
## 八、P3 工作流触发与后台执行
### FS-P3-001 实现提示词触发判断
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 后端 / 意图识别 |
| 前置任务 | P2 |
| 涉及文件 | `review_agent/file_summary/services/workflow_trigger.py``review_agent/services.py` |
| 目标 | 根据提示词决定是否启动自动汇总工作流 |
| 开发步骤 | 1. 定义触发关键词2. 判断当前对话是否存在可用 active 附件3. 命中时返回 workflow 类型4. 未命中走普通 LLM5. 命中但无附件时返回提示 |
| 验收标准 | “自动汇总”“文件目录”“页数”等关键词可触发;普通对话不误触发 |
| 验证命令 | `pytest tests/test_file_summary_trigger.py` |
| Codex 执行提示 | 请实现自动汇总工作流触发判断,只有当前对话存在可用附件且提示词命中关键词时才启动工作流。 |
### FS-P3-002 实现批次创建与附件固化
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 后端 / 工作流 |
| 前置任务 | FS-P3-001 |
| 涉及文件 | `review_agent/file_summary/workflow.py``review_agent/file_summary/storage.py` |
| 目标 | 用户消息触发时创建 FileSummaryBatch并固化本次使用的附件版本 |
| 开发步骤 | 1. 创建批次编号2. 创建 `FileSummaryBatch`3. 绑定 active 附件到中间表4. 标记附件为 bound5. 创建初始节点记录 |
| 验收标准 | 同一对话可多次汇总;历史批次绑定历史附件版本;不会读取其他对话文件 |
| 验证命令 | `pytest tests/test_file_summary_workflow.py -k batch` |
| Codex 执行提示 | 请实现批次创建和附件版本固化,确保每次汇总只读取本批次绑定的附件。 |
### FS-P3-003 实现 WorkflowEvent 与 SSE 事件查询
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 后端 / SSE |
| 前置任务 | FS-P3-002 |
| 涉及文件 | `review_agent/file_summary/events.py``review_agent/file_summary/views.py` |
| 目标 | 持久化工作流事件,并支持前端按 batch 监听和断点续传 |
| 开发步骤 | 1. 实现事件写入2. 实现 SSE 格式化3. 新增 `GET /api/review-agent/file-summary/{batch_id}/events/?after=`4. 新增批次状态查询接口5. 校验用户权限 |
| 验收标准 | 节点事件可入库SSE 可返回事件流;页面刷新可通过状态接口恢复 |
| 验证命令 | `pytest tests/test_file_summary_views.py -k "event or status"` |
| Codex 执行提示 | 请实现工作流事件持久化、事件 SSE 接口和批次状态查询接口,所有查询必须校验当前用户权限。 |
### FS-P3-004 实现轻量后台工作流执行器
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 后端 / 工作流 |
| 前置任务 | FS-P3-003 |
| 涉及文件 | `review_agent/file_summary/workflow.py``review_agent/file_summary/skills/` |
| 目标 | 实现串行节点图执行器,后台异步执行并更新节点状态 |
| 开发步骤 | 1. 定义节点顺序2. 实现后台线程启动3. 实现节点开始、成功、失败、跳过状态4. 每个节点写入 WorkflowNodeRun5. 每个节点发送 WorkflowEvent |
| 验收标准 | 命中提示词后可后台创建并推进节点;节点状态可查询;异常能标记批次失败 |
| 验证命令 | `pytest tests/test_file_summary_workflow.py -k executor` |
| Codex 执行提示 | 请实现轻量 WorkflowExecutor按节点图异步执行文件汇总流程实时写入节点状态和事件。 |
### FS-P3-005 接入现有流式聊天接口
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 后端 / 对话 |
| 前置任务 | FS-P3-004 |
| 涉及文件 | `review_agent/services.py``review_agent/views.py` |
| 目标 | 在现有 `stream_chat` 流程中按需启动自动汇总工作流 |
| 开发步骤 | 1. 用户消息入库后判断触发2. 命中时创建批次并启动后台3. SSE meta 返回 workflow 信息4. 对话中返回“已启动工作流”类助手消息或后续由报告生成写入结果5. 未命中时保持原 LLM 流式逻辑 |
| 验收标准 | 普通聊天不受影响;自动汇总触发后前端可拿到 batch_id无附件时提示用户先上传 |
| 验证命令 | `pytest tests/test_chat.py tests/test_file_summary_workflow.py -k trigger` |
| Codex 执行提示 | 请把自动汇总触发接入现有流式聊天接口,保证普通 LLM 对话兼容,命中工作流时返回 workflow meta。 |
### P3 阶段提交规则
| 项目 | 内容 |
| --- | --- |
| 阶段验证 | `pytest tests/test_file_summary_trigger.py tests/test_file_summary_workflow.py tests/test_file_summary_views.py` |
| 提交动作 | 调用 `git-commit-summary` 生成提交摘要并提交 |
| Codex 执行提示 | P3 验证通过后,请调用 `git-commit-summary` 分析本阶段变更,并按其输出提交到当前开发分支。 |
---
## 九、P4 Skill 与文件处理能力
### FS-P4-001 实现 Skill 基类与注册表
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 后端 / Skill |
| 前置任务 | P3 |
| 涉及文件 | `review_agent/file_summary/skills/base.py``review_agent/file_summary/skills/registry.py``review_agent/file_summary/schemas.py` |
| 目标 | 建立项目内 Skill 注册与调用机制,后续可迁移 MCP |
| 开发步骤 | 1. 定义 `WorkflowContext`2. 定义 `SkillResult`3. 定义 `BaseSkill`4. 实现 `SkillRegistry`5. 支持按名称获取和执行 Skill |
| 验收标准 | 工作流执行器通过注册表调用 SkillSkill 输入输出保持 JSON 友好 |
| 验证命令 | `pytest tests/test_file_summary_skills.py -k registry` |
| Codex 执行提示 | 请实现文件汇总 Skill 基类、上下文、统一返回结构和注册表,使工作流节点能按需加载 Skill。 |
### FS-P4-002 实现压缩包解压 Skill
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 后端 / 文件处理 |
| 前置任务 | FS-P4-001 |
| 涉及文件 | `review_agent/file_summary/services/archive.py``review_agent/file_summary/skills/archive_extract.py` |
| 目标 | 支持 zip、7z、rar 解压,并完成路径穿越防护 |
| 开发步骤 | 1. 实现压缩包识别2. 使用 `zipfile` 解压 zip3. 使用 `py7zr` 解压 7z4. 使用系统 `7z` 解压 rar5. 检查解压目标路径必须在批次工作目录内6. 解压失败标记批次失败 |
| 验收标准 | zip、7z、rar 均进入解压流程;恶意路径压缩包被拒绝;解压目录保留层级 |
| 验证命令 | `pytest tests/test_file_summary_archive.py` |
| Codex 执行提示 | 请实现压缩包解压服务和 Skill必须支持 zip、7z、rar并对所有解压路径做 target_dir 内部校验。 |
### FS-P4-003 实现文件清单扫描 Skill
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 后端 / 文件处理 |
| 前置任务 | FS-P4-002 |
| 涉及文件 | `review_agent/file_summary/services/inventory.py``review_agent/file_summary/skills/file_inventory.py` |
| 目标 | 扫描解压目录或散装文件,生成 FileSummaryItem 明细 |
| 开发步骤 | 1. 识别扫描根目录2. 递归遍历文件3. 生成相对路径4. 生成目录层级5. 标记支持、不支持、空文件或跳过状态6. 按目录顺序生成 file_index |
| 验收标准 | 文件明细保留目录层级散装文件进入同一批次根relative_path 唯一 |
| 验证命令 | `pytest tests/test_file_summary_inventory.py` |
| Codex 执行提示 | 请实现文件清单扫描服务和 Skill保留目录层级生成文件序号、相对路径、文件类型和初始统计状态。 |
### FS-P4-004 实现页数统计服务
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 后端 / 文件解析 |
| 前置任务 | FS-P4-003 |
| 涉及文件 | `review_agent/file_summary/services/page_count.py` |
| 目标 | 支持 pdf、doc、docx、xls、xlsx、ppt、pptx 页数或数量统计 |
| 开发步骤 | 1. pdf 使用 `pypdf` 统计页面2. docx 使用 `python-docx` 读取内置页数属性3. doc 使用 `olefile` 读取 OLE 元数据4. xlsx 使用 `openpyxl` 统计工作表5. xls 使用 `xlrd` 统计工作表6. pptx 使用 `python-pptx` 统计幻灯片7. ppt 使用 `olefile` 读取元数据8. 无可靠页数时标记 uncertain |
| 验收标准 | 7 类格式全部有处理分支;读不到页数不崩溃;状态区分 success、failed、unsupported、uncertain |
| 验证命令 | `pytest tests/test_file_summary_page_count.py` |
| Codex 执行提示 | 请实现页数统计服务,覆盖 pdf/doc/docx/xls/xlsx/ppt/pptx老格式读不到可靠页数时标记 uncertain不允许中断批次。 |
### FS-P4-005 实现页数统计 Skill 与三次重试
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 后端 / Skill |
| 前置任务 | FS-P4-004 |
| 涉及文件 | `review_agent/file_summary/skills/document_page_count.py``review_agent/file_summary/services/page_count.py` |
| 目标 | 对每个支持文件执行页数统计,失败最多重试 3 次 |
| 开发步骤 | 1. 遍历 FileSummaryItem2. 支持类型调用 page_count 服务3. 失败重试 3 次4. 更新 retry_count、statistics_status、page_count、error_message5. 更新节点进度事件6. 汇总批次数量 |
| 验收标准 | 单文件失败不阻断其他文件;重试事件可记录;批次统计字段更新正确 |
| 验证命令 | `pytest tests/test_file_summary_page_count.py -k retry` |
| Codex 执行提示 | 请实现文档页数统计 Skill对单文件解析失败最多重试 3 次,仍失败则记录异常并继续处理其他文件。 |
### FS-P4-006 实现产品名识别 Skill 与会话标题更新
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 后端 / 识别 |
| 前置任务 | FS-P4-005 |
| 涉及文件 | `review_agent/file_summary/services/product_detect.py``review_agent/file_summary/skills/product_detect.py` |
| 目标 | 从目录名、文件名和少量元数据中识别产品名,并按规则更新对话标题 |
| 开发步骤 | 1. 优先使用顶层目录名2. 从含“产品”“试剂盒”“说明书”等关键词的文件名提取3. 尝试读取 docx/PDF 元数据 title4. 写入 batch.product_name5. 默认标题时更新 Conversation.title6. 用户自定义标题不覆盖 |
| 验收标准 | 识别失败不阻断;识别成功后批次记录产品名;默认对话标题可更新为“产品名-文件汇总” |
| 验证命令 | `pytest tests/test_file_summary_product_detect.py` |
| Codex 执行提示 | 请实现产品名识别 Skill从目录名、文件名和轻量元数据识别产品名识别成功后按规则更新批次和对话标题。 |
### P4 阶段提交规则
| 项目 | 内容 |
| --- | --- |
| 阶段验证 | `pytest tests/test_file_summary_skills.py tests/test_file_summary_archive.py tests/test_file_summary_inventory.py tests/test_file_summary_page_count.py tests/test_file_summary_product_detect.py` |
| 提交动作 | 调用 `git-commit-summary` 生成提交摘要并提交 |
| Codex 执行提示 | P4 验证通过后,请调用 `git-commit-summary` 分析本阶段变更,并按其输出提交到当前开发分支。 |
---
## 十、P5 报告生成与下载
### FS-P5-001 实现 Markdown 报告生成
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 后端 / 报告 |
| 前置任务 | P4 |
| 涉及文件 | `review_agent/file_summary/services/report.py``review_agent/file_summary/skills/summary_report.py` |
| 目标 | 生成完整 Markdown 报告和对话框展示简表 |
| 开发步骤 | 1. 构建统计摘要2. 构建对话简表3. 构建完整 Markdown 报告4. 保存到批次 exports 目录5. 创建 ExportedSummaryFile6. 生成助手消息内容 |
| 验收标准 | Markdown 包含汇总信息、统计摘要、文件明细、异常清单、处理说明和下载链接占位 |
| 验证命令 | `pytest tests/test_file_summary_report.py -k markdown` |
| Codex 执行提示 | 请实现 Markdown 报告生成 Skill完整报告和对话简表必须包含文件序号、目录层级、文件名、类型、页数、路径、状态、异常说明。 |
### FS-P5-002 实现 Excel 导出
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 后端 / 导出 |
| 前置任务 | FS-P5-001 |
| 涉及文件 | `review_agent/file_summary/services/export_excel.py``review_agent/file_summary/skills/excel_export.py` |
| 目标 | 生成 Excel 明细文件 |
| 开发步骤 | 1. 使用 `openpyxl` 创建 Workbook2. 创建“汇总信息”Sheet3. 创建“文件明细”Sheet4. 写入状态、重试次数和异常说明5. 保存到 exports 目录6. 创建 ExportedSummaryFile |
| 验收标准 | Excel 可打开;至少包含两个工作表;字段与需求一致 |
| 验证命令 | `pytest tests/test_file_summary_report.py -k excel` |
| Codex 执行提示 | 请实现 Excel 导出 Skill生成包含“汇总信息”和“文件明细”两个 Sheet 的汇总文件。 |
### FS-P5-003 实现导出下载接口
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 后端 / 下载 |
| 前置任务 | FS-P5-002 |
| 涉及文件 | `review_agent/file_summary/views.py``review_agent/file_summary/urls.py` |
| 目标 | 提供 Markdown 和 Excel 文件下载,并校验权限 |
| 开发步骤 | 1. 新增 `GET /api/review-agent/file-summary/exports/{export_id}/download/`2. 校验 export -> batch -> conversation -> user3. 返回文件流4. 设置合适文件名5. 文件不存在时返回错误 |
| 验收标准 | 当前用户可下载自己的导出文件;不能下载其他用户文件;下载链接可用于 Markdown |
| 验证命令 | `pytest tests/test_file_summary_views.py -k download` |
| Codex 执行提示 | 请实现导出文件下载接口,下载权限必须沿 export -> batch -> conversation -> user 校验。 |
### FS-P5-004 完成报告 Skill 与工作流衔接
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 后端 / 工作流 |
| 前置任务 | FS-P5-003 |
| 涉及文件 | `review_agent/file_summary/workflow.py``review_agent/file_summary/services/report.py` |
| 目标 | 工作流完成后写入助手消息,展示 Markdown 简表和真实下载链接 |
| 开发步骤 | 1. 报告和 Excel 导出完成后生成下载 URL2. 替换对话简表中的下载链接3. 创建 assistant Message4. 标记 batch success5. 发送 workflow_completed 事件 |
| 验收标准 | 工作流完成后对话中出现 Markdown 简表;下载链接可点击;批次状态成功 |
| 验证命令 | `pytest tests/test_file_summary_workflow.py -k report` |
| Codex 执行提示 | 请把 Markdown 报告、Excel 导出和工作流完成逻辑串起来,完成后向当前对话写入助手消息。 |
### P5 阶段提交规则
| 项目 | 内容 |
| --- | --- |
| 阶段验证 | `pytest tests/test_file_summary_report.py tests/test_file_summary_views.py -k download tests/test_file_summary_workflow.py -k report` |
| 提交动作 | 调用 `git-commit-summary` 生成提交摘要并提交 |
| Codex 执行提示 | P5 验证通过后,请调用 `git-commit-summary` 分析本阶段变更,并按其输出提交到当前开发分支。 |
---
## 十一、P6 前端三栏与工作流卡片
### FS-P6-001 改造页面为三栏布局
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 前端 / 布局 |
| 前置任务 | P5 |
| 涉及文件 | `templates/home.html`、实际静态 CSS 文件 |
| 目标 | 在现有对话页增加右侧第三栏,上半部分上传区,下半部分工作流卡片 |
| 开发步骤 | 1. 确认真实静态样式文件路径2. 调整 workspace 结构3. 增加 `workflow-panel`4. 增加 `upload-dropzone`5. 增加 `workflow-card-list`6. 保证桌面和移动端不遮挡 |
| 验收标准 | 页面显示左侧会话、中间聊天、右侧上传/工作流三栏;移动端布局可用 |
| 验证命令 | `pytest tests/test_file_summary_e2e.py -k layout` 或 Playwright 对应命令 |
| Codex 执行提示 | 请把审核智能体页面改造成三栏布局,右侧上半部分为拖拽上传区,下半部分为工作流卡片列表,并保持现有聊天能力可用。 |
### FS-P6-002 实现前端附件上传交互
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 前端 / 上传 |
| 前置任务 | FS-P6-001 |
| 涉及文件 | `static/js/app.js``templates/home.html`、实际静态 CSS 文件 |
| 目标 | 支持拖拽或选择多个文件上传,上传成功后展示附件列表 |
| 开发步骤 | 1. 绑定 dropzone2. 支持点击选择文件3. 调用附件上传 API4. 展示文件名、版本、大小和状态5. 上传失败展示错误 |
| 验收标准 | 上传即存储;前端展示当前对话附件;切换对话不串附件 |
| 验证命令 | Playwright 上传测试 |
| Codex 执行提示 | 请实现右侧上传区前端交互,支持拖拽和选择多个文件,调用附件上传接口并展示当前对话附件列表。 |
### FS-P6-003 实现工作流卡片与 SSE 更新
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 前端 / 工作流 |
| 前置任务 | FS-P6-002 |
| 涉及文件 | `static/js/app.js`、实际静态 CSS 文件 |
| 目标 | 在发送提示词触发工作流后创建卡片,并根据 SSE 更新节点状态 |
| 开发步骤 | 1. 解析 chat stream 中的 workflow meta2. 创建 workflow card3. 连接 batch events SSE4. 更新节点 pending/running/retrying/success/failed/skipped5. workflow_completed 后更新完成状态6. 页面刷新后通过状态接口恢复 |
| 验收标准 | 工作流节点实时更新;刷新页面可恢复;失败状态可见 |
| 验证命令 | Playwright 工作流卡片测试 |
| Codex 执行提示 | 请实现工作流卡片前端逻辑,接收 workflow meta 后连接事件流,实时更新上传、解压、扫描、解析、识别、输出、完成等节点状态。 |
### FS-P6-004 实现 Markdown 安全渲染
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 前端 / 渲染 |
| 前置任务 | FS-P6-003 |
| 涉及文件 | `templates/home.html``static/js/app.js`、静态依赖文件或 CDN 引入 |
| 目标 | 让助手消息支持 Markdown 表格和下载链接渲染 |
| 开发步骤 | 1. 引入 `marked + DOMPurify`2. 普通用户消息保持 escape3. 助手消息使用安全 Markdown 渲染4. 历史消息渲染兼容5. 下载链接可点击 |
| 验收标准 | Markdown 表格渲染为 HTML table链接渲染为 a 标签;无明显 XSS 风险 |
| 验证命令 | Playwright Markdown 渲染测试 |
| Codex 执行提示 | 请引入 marked + DOMPurify 实现助手消息安全 Markdown 渲染,确保文件汇总结果表格和下载链接正常显示。 |
### FS-P6-005 实现 Playwright 端到端测试
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 前端 / E2E |
| 前置任务 | FS-P6-004 |
| 涉及文件 | Playwright 测试文件、测试配置 |
| 目标 | 使用真实浏览器覆盖上传、触发、卡片、渲染、下载和恢复 |
| 开发步骤 | 1. 创建测试用户2. 登录系统3. 打开审核智能体页面4. 上传动态生成的测试文件5. 发送“自动汇总文件目录与页数”6. 等待工作流卡片完成7. 验证 Markdown table 和下载链接8. 刷新后验证卡片恢复9. 验证越权访问失败 |
| 验收标准 | Playwright 端到端测试通过;关键页面截图可生成;失败时能定位到具体断言 |
| 验证命令 | Playwright 对应执行命令 |
| Codex 执行提示 | 请使用 Playwright 增加真实浏览器端到端测试,从登录、上传、发送提示词一直验证到报告渲染、下载和刷新恢复。 |
### P6 阶段提交规则
| 项目 | 内容 |
| --- | --- |
| 阶段验证 | Playwright 端到端测试 + 相关后端接口测试 |
| 提交动作 | 调用 `git-commit-summary` 生成提交摘要并提交 |
| Codex 执行提示 | P6 验证通过后,请调用 `git-commit-summary` 分析本阶段变更,并按其输出提交到当前开发分支。 |
---
## 十二、P7 测试、部署与总体验收
### FS-P7-001 补齐后端测试矩阵
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 测试 / 后端 |
| 前置任务 | P6 |
| 涉及文件 | `tests/test_file_summary_*.py` |
| 目标 | 覆盖单元、接口、工作流集成和权限隔离 |
| 开发步骤 | 1. 覆盖触发词2. 覆盖附件版本3. 覆盖解压安全4. 覆盖文件扫描5. 覆盖页数统计6. 覆盖报告导出7. 覆盖下载权限8. 覆盖完整工作流 |
| 验收标准 | 后端文件汇总测试全部通过;失败场景覆盖充分 |
| 验证命令 | `pytest tests/test_file_summary_*.py` |
| Codex 执行提示 | 请补齐文件汇总后端测试矩阵,覆盖单元、接口、工作流集成和权限隔离。 |
### FS-P7-002 补充部署与 Docker 说明
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 部署 / 文档 |
| 前置任务 | FS-P7-001 |
| 涉及文件 | README 或部署说明文档 |
| 目标 | 写明生产或 Docker 部署时的依赖安装和验证方式 |
| 开发步骤 | 1. 写明 Python 依赖安装2. 写明 7z/p7zip 安装3. 写明 rar/7z 验证命令4. 写明 LibreOffice 非必需、仅未来增强使用5. 写明 media 文件存储目录 |
| 验收标准 | 部署说明可指导在 Docker 中启用 rar/7z 解压;依赖边界清楚 |
| 验证命令 | `python manage.py check` |
| Codex 执行提示 | 请补充部署说明,明确 Docker 环境需要安装 7z/p7zip 支持 rar/7zLibreOffice 不是必需依赖。 |
### FS-P7-003 执行总体验收
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 验收 / 流水线 |
| 前置任务 | FS-P7-002 |
| 涉及文件 | 无固定文件 |
| 目标 | 运行全部测试和端到端验证,确认功能完整 |
| 开发步骤 | 1. 运行 Django check2. 运行全量 pytest3. 运行 Playwright E2E4. 手工或自动验证下载文件可打开5. 检查数据库记录6. 检查 git status |
| 验收标准 | 总体验收标准全部满足;没有未解释的失败测试;没有意外文件变更 |
| 验证命令 | `python manage.py check`; `pytest`; Playwright 对应命令 |
| Codex 执行提示 | 请执行文件汇总功能总体验收,运行后端全量测试和 Playwright 端到端测试,确认所有验收标准已满足。 |
### FS-P7-004 合并回 V2 分支
| 项目 | 内容 |
| --- | --- |
| 任务类型 | Git / 收尾 |
| 前置任务 | FS-P7-003 |
| 涉及文件 | 无固定文件 |
| 目标 | 将开发分支合并回 `V2`,并在合并后再次运行总体验收 |
| 开发步骤 | 1. P7 通过后调用 `git-commit-summary` 提交阶段变更2. 切换到 `V2`3. 合并开发分支4. 解决冲突但不得覆盖用户变更5. 合并后运行总体验收6. 保留最终 git status |
| 验收标准 | 开发分支成功合并到 `V2`;合并后测试通过;本地 Git 历史包含阶段提交 |
| 验证命令 | `git branch --show-current`; `git status --short`; `python manage.py check`; `pytest`; Playwright 对应命令 |
| Codex 执行提示 | 请在全部阶段完成后提交 P7 变更,切回 `V2` 并合并开发分支,合并后重新运行总体验收。 |
### P7 阶段提交规则
| 项目 | 内容 |
| --- | --- |
| 阶段验证 | `python manage.py check`; `pytest`; Playwright 端到端测试 |
| 提交动作 | 调用 `git-commit-summary` 生成提交摘要并提交 |
| 合并动作 | 所有阶段提交完成后合并回 `V2` |
| Codex 执行提示 | P7 验证通过后,请调用 `git-commit-summary` 提交本阶段变更,然后合并回 `V2` 并再次总体验收。 |
---
## 十三、测试分层要求
| 层级 | 验证内容 | 建议文件 |
| --- | --- | --- |
| 单元测试 | 触发词、附件版本、解压安全、文件扫描、页数统计、报告生成 | `tests/test_file_summary_*.py` |
| 接口测试 | 上传接口、批次状态接口、事件接口、下载接口、权限隔离 | `tests/test_file_summary_views.py` |
| 工作流集成测试 | 上传附件后发送提示词,完整执行到生成 Markdown/Excel | `tests/test_file_summary_workflow.py` |
| Playwright E2E | 登录、上传、触发、卡片更新、Markdown 渲染、下载、刷新恢复 | Playwright 测试文件 |
| 部署验证 | requirements 安装成功Docker 中 7z/p7zip 可用rar/7z 解压可跑通 | 部署说明和验证命令 |
说明:测试样例文件不单独拆任务,可在测试代码中动态生成临时 pdf、docx、xlsx、pptx、zip、7z、rar、损坏文件或不可读文件。
---
## 十四、Codex 自动化执行规则
| 规则 | 内容 |
| --- | --- |
| 顺序执行 | 必须从 P0 到 P7 顺序执行,不得跳阶段 |
| 当前阶段优先 | 某阶段测试失败时,必须先修复当前阶段,不得继续后续阶段 |
| 连续失败处理 | 同一阶段连续 3 次失败时,记录阻塞原因、已尝试方案和下一步建议 |
| 每任务验证 | 每个任务完成后运行对应验证命令或说明无法运行原因 |
| 每阶段提交 | 每个阶段全部任务完成并验证通过后,调用 `git-commit-summary` 后本地提交 |
| 前端强验证 | P6 完成后必须运行 Playwright 端到端测试和截图/断言验证 |
| 不覆盖变更 | 不得回滚或覆盖用户已有未提交变更 |
| 合并收尾 | 全部完成后必须合并回 `V2` 并再次总体验收 |
---
## 十五、推荐一键执行提示词
后续可直接对 Codex 输入:
```text
请按 docs/5.开发计划/1.自动汇总.md 执行,从 V2 创建 codex/YYYYMMDD-自动汇总文件目录页数 分支,按 P0 到 P7 顺序开发、验证和阶段提交。每个阶段完成后调用 git-commit-summary 生成提交摘要并本地提交。全部完成后合并回 V2并重新运行总体验收。
```
---
## 十六、待执行前检查清单
| 检查项 | 状态 |
| --- | --- |
| 需求分析、功能设计、详细设计、数据库设计均已存在 | 待执行时确认 |
| 当前分支是否为 `V2` | 待执行时确认 |
| 是否存在用户未提交变更 | 待执行时确认 |
| Python 依赖是否可安装 | 待执行时确认 |
| Playwright 或对应 MCP/Skill 是否可用 | 待执行时确认 |
| 执行机器是否提供 `git-commit-summary` skill | 待执行时确认 |
| Docker 环境是否可安装 7z/p7zip | 待执行时确认 |

View File

@@ -0,0 +1,415 @@
# NMPA 注册资料法规核查与整改闭环开发计划(第一批:主链路)
## 一、已确认口径
| 问题 | 结论 |
| --- | --- |
| 第二阶段覆盖范围 | 覆盖原始需求 2、4、5法规完整性核查、章节/一致性核查、风险预警与整改建议 |
| 原始需求 3 | 本阶段只做核查所需的信息抽取,不做自动填写目标文件 |
| 执行策略 | 第二阶段拆成两次 Codex 目标执行;第一批先打通 Demo 主链路 |
| 启动方式 | 用户对话提示词触发法规核查工作流,不做上传后自动核查 |
| 汇总批次 | 默认复用当前对话最近一次成功 `FileSummaryBatch`,不自动串联文件汇总 |
| 规则来源 | Demo 先用本地 YAML数据库记录规则版本、路径、hash、RAG 索引信息 |
| 规则差异 | 自动检测 YAML 与数据库记录差异,提示人工确认更新;第一批不做规则管理前端 |
| RAG | 必须使用向量库;默认 ChromaDB |
| Embedding | Provider 可配置Demo 默认 SiliconFlow `Qwen/Qwen3-Embedding-4B` |
| 法规材料 | 先索引 `docs/0.原始材料/关于公布体外诊断试剂注册申报资料要求和批准证明文件格式的公告` |
| 法规文档抽取 | 允许使用 LibreOffice headless 转换本地法规 `.doc` 材料;该依赖只服务 RAG 建库,不改变第一阶段页数统计口径 |
| ChromaDB 运行方式 | 第一批采用本地持久化模式,不单独启动 Chroma Server |
| 飞书 | 第一批不接真实飞书;暂缓项写入待办计划 |
---
## 二、第一批目标
第一批只追求“可运行、可演示、可追溯”的法规核查主链路:
```text
已有文件汇总批次
-> 用户提示词触发法规核查
-> 读取本地 YAML 规则
-> 检查规则版本和 RAG 索引状态
-> 使用 ChromaDB 检索法规依据
-> 完整性核查
-> 基础章节核查
-> 基础一致性核查
-> 风险分级和整改建议
-> 生成对话摘要、Markdown 报告、Excel 清单、JSON 结果包
-> 前端展示法规核查工作流卡片
```
第一批完成后Demo 应能展示:
| 展示项 | 内容 |
| --- | --- |
| 法规依据 | RAG 返回本地法规材料来源和片段 |
| 完整性问题 | 如缺少注册检验报告、临床评价资料等 |
| 章节问题 | 如说明书缺少储存条件、有效期、样本要求等章节 |
| 一致性问题 | 如产品名称、型号规格、预期用途在不同文件中不一致 |
| 风险清单 | blocking/high/medium/low/info 五级 |
| 报告下载 | Markdown、Excel、JSON |
---
## 三、阶段拆分
| 阶段 | 名称 | 目标 | 验收 |
| --- | --- | --- | --- |
| RR1-0 | 准备与回归 | 确认第一阶段稳定,创建开发分支 | `pytest` 通过 |
| RR1-1 | 模型与兼容改造 | 新增法规核查模型,兼容工作流/导出通用字段 | migration 和模型测试通过 |
| RR1-2 | YAML 规则与版本记录 | 建立 Demo 规则文件、规则版本表、hash 差异检测 | 能识别 YAML 与 DB 差异 |
| RR1-3 | RAG 索引与检索 | 用 ChromaDB + SiliconFlow embedding 构建本地法规索引 | 能检索法规依据 |
| RR1-4 | 触发与工作流骨架 | 对话提示词触发法规核查,复用最近成功汇总批次 | 能创建并运行法规核查批次 |
| RR1-5 | 核查服务 | 完整性、基础章节、基础一致性核查 | 生成 findings |
| RR1-6 | 风险与导出 | 风险归并、Issue 落库、报告导出 | 生成助手摘要和下载文件 |
| RR1-7 | 前端与验收 | 法规核查卡片、状态恢复、Markdown 结果展示 | 全量测试通过 |
---
## 四、RR1-0 准备与回归
### 任务
| 编号 | 内容 |
| --- | --- |
| RR1-0-001 | 从当前稳定分支创建 `codex/YYYYMMDD-NMPA法规核查主链路` |
| RR1-0-002 | 运行 `python manage.py check``pytest` |
| RR1-0-003 | 记录第一阶段边界文件夹上传不作为强验收、RAR 依赖 7z、Office 页数口径可不精确 |
### 验证命令
```bash
python manage.py check
pytest
git status --short
```
### Codex 执行提示
```text
请创建第二阶段第一批开发分支,先确认第一阶段文件汇总功能全量测试通过。本阶段不要修改业务代码,只做环境和边界确认。
```
---
## 五、RR1-1 模型与兼容改造
### 任务
| 编号 | 内容 | 文件 |
| --- | --- | --- |
| RR1-1-001 | 新增法规核查模型和枚举 | `review_agent/models.py` |
| RR1-1-002 | 给 `WorkflowNodeRun` 增加 `workflow_type``workflow_batch_id``node_group` | `review_agent/models.py` |
| RR1-1-003 | 给 `WorkflowEvent` 增加 `workflow_type``workflow_batch_id``conversation_id` | `review_agent/models.py` |
| RR1-1-004 | 给 `ExportedSummaryFile` 增加 `workflow_type``workflow_batch_id``export_category` | `review_agent/models.py` |
| RR1-1-005 | 保持第一阶段文件汇总写入兼容 | `review_agent/file_summary/*` |
| RR1-1-006 | 生成 migration 并补模型测试 | `review_agent/migrations/``tests/test_regulatory_models.py` |
### 新增模型
| 模型 | 说明 |
| --- | --- |
| `RegulatoryRuleVersion` | 规则版本、YAML 路径、文件 hash、RAG 索引版本 |
| `RegulatoryReviewBatch` | 法规核查批次 |
| `RegulatoryIssue` | 风险问题和整改状态 |
| `RegulatoryArtifact` | 过程产物 |
| `RegulatoryNotificationRecord` | mock 通知预留记录,第一批可只建表不接真实通知 |
### 验证命令
```bash
python manage.py makemigrations review_agent
python manage.py migrate
python manage.py check
pytest tests/test_regulatory_models.py tests/test_file_summary_workflow.py tests/test_file_summary_views.py
```
### Codex 执行提示
```text
请新增法规核查相关模型,并轻量通用化现有工作流节点、事件和导出文件表。必须保持第一阶段文件汇总测试通过,不要重写第一阶段工作流。
```
---
## 六、RR1-2 YAML 规则与版本记录
### 任务
| 编号 | 内容 | 文件 |
| --- | --- | --- |
| RR1-2-001 | 新建法规核查模块目录 | `review_agent/regulatory_review/` |
| RR1-2-002 | 编写 Demo YAML 规则 | `review_agent/regulatory_review/rules/nmpa_ivd_registration_v1.yaml` |
| RR1-2-003 | 实现规则 hash 计算和版本记录 | `services/rule_loader.py` |
| RR1-2-004 | 实现 YAML 与 DB 差异检测 | `services/rule_loader.py` |
| RR1-2-005 | 增加规则版本初始化/检查管理命令 | `management/commands/regulatory_rules_check.py` |
| RR1-2-006 | 增加测试 | `tests/test_regulatory_rule_loader.py` |
### Demo 规则至少覆盖
| 文件项 | 类型 | 风险 |
| --- | --- | --- |
| 产品技术要求 | required | blocking |
| 说明书 | required | high |
| 注册检验报告 | required | blocking |
| 临床评价资料 | conditional | high |
| 安全和性能基本原则清单 | recommended | medium |
YAML 规则内容需参考本地法规资料目录:
```text
docs/0.原始材料/关于公布体外诊断试剂注册申报资料要求和批准证明文件格式的公告
```
### 验证命令
```bash
pytest tests/test_regulatory_rule_loader.py
python manage.py regulatory_rules_check
```
### Codex 执行提示
```text
请建立 Demo 版 NMPA IVD 注册资料 YAML 规则库,并实现规则版本、文件 hash 和数据库记录差异检测。发现 YAML 与 DB hash 不一致时只提示需要更新,不自动覆盖。
```
---
## 七、RR1-3 RAG 索引与检索
### 任务
| 编号 | 内容 | 文件 |
| --- | --- | --- |
| RR1-3-001 | 增加依赖 `chromadb` 和必要 HTTP 客户端 | `requirements.txt` |
| RR1-3-002 | 实现 embedding provider 抽象 | `services/rag_embedding.py` |
| RR1-3-003 | 实现 SiliconFlow embedding provider | `services/rag_embedding.py` |
| RR1-3-004 | 实现法规文档文本抽取和切块 | `services/rag_index.py` |
| RR1-3-005 | 实现 ChromaDB 持久化索引构建命令 | `management/commands/regulatory_rag_build.py` |
| RR1-3-006 | 实现 RAG 引用检索服务 | `services/rag_citation.py` |
| RR1-3-007 | 增加测试 | `tests/test_regulatory_rag.py` |
### 配置
| 配置项 | 默认 |
| --- | --- |
| `REGULATORY_RAG_PROVIDER` | `siliconflow` |
| `REGULATORY_RAG_CHROMA_PATH` | `media/regulatory_review/rag/chroma/` |
| `SILICONFLOW_BASE_URL` | `https://api.siliconflow.cn/v1` |
| `SILICONFLOW_API_KEY` | 从环境变量读取 |
| `SILICONFLOW_EMBEDDING_MODEL` | `Qwen/Qwen3-Embedding-4B` |
| `SILICONFLOW_EMBEDDING_DIMENSIONS` | `1024` |
| `REGULATORY_RAG_COLLECTION` | `nmpa_ivd_registration_v1` |
SiliconFlow Embedding API 参考:
```text
https://docs.siliconflow.com/en/api-reference/embeddings/create-embeddings
```
### 规则
| 场景 | 处理 |
| --- | --- |
| RAG 索引不存在 | 核查时提示先构建索引,不在核查中临时构建 |
| Embedding API 不可用 | 构建命令失败,核查不启动 |
| RAG 无命中 | 规则问题仍输出,法规依据标记“原文依据待补充” |
| 本地法规 `.doc` 无法直接抽取 | 允许通过 LibreOffice headless 转换后抽取Docker 部署说明需写明可选安装方式 |
| ChromaDB 存储 | 使用本地持久化目录Docker 部署时通过 volume 挂载保留索引 |
### 验证命令
```bash
python manage.py regulatory_rag_build
pytest tests/test_regulatory_rag.py
```
### Codex 执行提示
```text
请实现基于 ChromaDB 的本地法规 RAG。Embedding Provider 要可配置Demo 默认使用 SiliconFlow Qwen/Qwen3-Embedding-4B。ChromaDB 使用本地持久化目录,不单独启动服务。法规 `.doc` 材料允许用 LibreOffice headless 转换后抽取。核查流程只检查索引可用性,不临时构建索引。
```
---
## 八、RR1-4 触发与工作流骨架
### 任务
| 编号 | 内容 | 文件 |
| --- | --- | --- |
| RR1-4-001 | 实现法规核查提示词路由 | `review_agent/skill_router.py` |
| RR1-4-002 | 实现法规核查批次创建 | `regulatory_review/workflow.py` |
| RR1-4-003 | 默认查找当前对话最近成功 `FileSummaryBatch` | `workflow.py` |
| RR1-4-004 | 无成功汇总批次时提示用户先执行自动汇总 | `services.py` |
| RR1-4-005 | 实现启动、状态、事件接口 | `regulatory_review/views.py``urls.py` |
| RR1-4-006 | 接入项目 URL | `config/urls.py``review_agent/urls.py` |
| RR1-4-007 | 增加测试 | `tests/test_regulatory_workflow.py``tests/test_regulatory_views.py` |
### 第一批节点
```text
prepare
-> rule_scope
-> completeness_check
-> text_extract
-> structure_check
-> consistency_check
-> risk_assess
-> report_export
-> completed
```
### 验证命令
```bash
pytest tests/test_regulatory_workflow.py tests/test_regulatory_views.py
pytest tests/test_file_summary_trigger.py tests/test_llm_streaming.py
```
### Codex 执行提示
```text
请实现法规核查提示词触发和工作流骨架。用户说“法规核查、NMPA核查、完整性核查、风险预警”等意图时启动 regulatory_review默认复用当前对话最近成功 FileSummaryBatch没有成功汇总批次时提示先自动汇总。
```
---
## 九、RR1-5 核查服务
### 任务
| 编号 | 内容 | 文件 |
| --- | --- | --- |
| RR1-5-001 | 实现统一 Finding dataclass | `regulatory_review/schemas.py` |
| RR1-5-002 | 完整性核查:文件名、目录名、首页文本匹配 | `services/completeness_check.py` |
| RR1-5-003 | 文本抽取docx/pdf/xlsx/pptx/txt/md 基础文本 | `services/text_extract.py` |
| RR1-5-004 | 基础章节核查:按规则关键词判断章节是否存在 | `services/structure_check.py` |
| RR1-5-005 | 基础一致性核查:产品名称、型号规格、预期用途 | `services/consistency_check.py` |
| RR1-5-006 | 过程产物保存和 hash | `storage.py` |
| RR1-5-007 | 增加测试 | `tests/test_regulatory_completeness.py``tests/test_regulatory_text_extract.py``tests/test_regulatory_structure.py``tests/test_regulatory_consistency.py` |
### Demo 验收样例
测试或演示资料中至少构造:
| 条件 | 预期 |
| --- | --- |
| 有说明书 | 可匹配说明书规则 |
| 有产品技术要求 | 可匹配产品技术要求规则 |
| 缺少注册检验报告 | 生成 blocking 问题 |
| 说明书缺少储存条件章节 | 生成 high 或 medium 问题 |
| 产品名称在两个文件中不一致 | 生成 consistency 问题 |
### 验证命令
```bash
pytest tests/test_regulatory_completeness.py tests/test_regulatory_text_extract.py tests/test_regulatory_structure.py tests/test_regulatory_consistency.py
```
### Codex 执行提示
```text
请实现完整性核查、文本抽取、基础章节核查和基础一致性核查。所有核查服务只返回 Finding不直接创建 RegulatoryIssue。
```
---
## 十、RR1-6 风险与导出
### 任务
| 编号 | 内容 | 文件 |
| --- | --- | --- |
| RR1-6-001 | Findings 去重和风险归并 | `services/risk_assess.py` |
| RR1-6-002 | RAG 引用挂载到问题证据 | `services/risk_assess.py``services/rag_citation.py` |
| RR1-6-003 | 创建 `RegulatoryIssue` | `services/risk_assess.py` |
| RR1-6-004 | 生成 Markdown 核查报告 | `services/export.py` |
| RR1-6-005 | 生成 Excel 缺失清单 | `services/export.py` |
| RR1-6-006 | 生成 JSON 结果包 | `services/export.py` |
| RR1-6-007 | 工作流完成后写入助手消息 | `workflow.py` |
| RR1-6-008 | 增加测试 | `tests/test_regulatory_risk_assess.py``tests/test_regulatory_export.py` |
### 对话摘要
助手消息至少包含:
```markdown
已完成 NMPA 注册资料法规核查。
| 风险等级 | 数量 |
| --- | --- |
| 阻断项 | 1 |
| 高风险 | 1 |
| 等级 | 问题 | 状态 | 建议 |
| --- | --- | --- | --- |
| 阻断项 | 缺少注册检验报告 | 待处理 | 请补充注册检验报告并复核 |
[下载 Markdown 核查报告](...)
[下载 Excel 缺失清单](...)
[下载 JSON 结果包](...)
```
### 验证命令
```bash
pytest tests/test_regulatory_risk_assess.py tests/test_regulatory_export.py tests/test_regulatory_workflow.py
```
### Codex 执行提示
```text
请实现风险归并、RAG 法规依据挂载、Issue 落库和最终报告导出。工作流完成后必须向当前对话写入 Markdown 摘要和下载链接。
```
---
## 十一、RR1-7 前端与总体验收
### 任务
| 编号 | 内容 | 文件 |
| --- | --- | --- |
| RR1-7-001 | 工作流卡片支持 `regulatory_review` 类型 | `templates/home.html``static/js/app.js` |
| RR1-7-002 | 卡片使用 `workflow_type + workflow_batch_id` 区分 | `static/js/app.js` |
| RR1-7-003 | 显示法规核查节点和风险摘要 | `templates/home.html``static/js/app.js` |
| RR1-7-004 | 页面刷新恢复法规核查卡片 | `views.py``static/js/app.js` |
| RR1-7-005 | 补前端测试 | `tests/test_regulatory_frontend.py` |
| RR1-7-006 | 全量回归 | 全项目 |
### 验证命令
```bash
python manage.py check
pytest
```
如浏览器可用,再运行 Playwright 端到端验证。
### Codex 执行提示
```text
请在现有工作流卡片轮播基础上支持 regulatory_review 类型,展示法规核查节点、风险摘要和完成状态。最后运行 python manage.py check 和 pytest 全量验收。
```
---
## 十二、第一批 Codex 目标模式提示词
```text
请按 docs/5.开发计划/2.NMPA注册资料法规核查与整改闭环-第一批主链路.md 执行第二阶段第一批开发。
目标:
完成 NMPA 法规核查主链路,复用当前对话最近成功 FileSummaryBatch通过用户提示词触发 regulatory_review 工作流,实现 YAML 规则、ChromaDB + SiliconFlow Embedding RAG、完整性核查、基础章节核查、基础一致性核查、风险分级、Markdown/Excel/JSON 报告和前端法规核查卡片。
执行规则:
1. 创建 codex/YYYYMMDD-NMPA法规核查主链路 分支。
2. 按 RR1-0 到 RR1-7 顺序执行,不跳阶段。
3. 每阶段完成后运行对应验证命令。
4. 第一阶段文件汇总测试不得回归。
5. 不自动串联文件汇总;没有成功汇总批次时提示用户先自动汇总。
6. 不接真实飞书,不做规则管理前端,不做自动填写目标文件。
7. 最后运行 python manage.py check 和 pytest 全量验收。
```

View File

@@ -0,0 +1,304 @@
# NMPA 注册资料法规核查与整改闭环开发计划(第二批:完整闭环补齐)
## 一、第二批目标
第二批在第一批主链路通过后执行,补齐完整整改闭环和交互能力:
```text
适用条件对话选择框
-> waiting_user 暂停恢复
-> 附件 4 申报资料目录规则对齐
-> 整包复核
-> 缺失项复核
-> mock 通知留痕
-> 更完整的过程产物
-> 更强的前端交互和验收测试
```
飞书真实 CLI/API、规则管理前端、自动填写目标文件不在第二批落地进入 `docs/6.待办计划/第二阶段暂缓事项.md`
---
## 二、阶段总览
| 阶段 | 名称 | 目标 | 验收 |
| --- | --- | --- | --- |
| RR2-1 | 适用条件确认 | 对话选择框确认产品类别、注册类型、临床评价路径等 | waiting_user 可暂停恢复 |
| RR2-2 | 附件 4 规则对齐与核查能力增强 | 按《体外诊断试剂注册申报资料要求及说明》扩展完整目录规则、章节、一致性、RAG 引用和文本抽取范围 | 能识别附件 4 一级/二级目录缺失和关键字段问题 |
| RR2-3 | 整包复核 | 基于新的汇总批次创建新的法规核查批次 | 可追溯来源批次 |
| RR2-4 | 缺失项复核 | 针对原 Issue 执行复核并更新状态 | 生成 review_record |
| RR2-5 | mock 通知留痕 | 对 blocking/high/medium 写 mock 通知记录 | 报告展示通知记录 |
| RR2-6 | 前端和总体验收 | 条件选择框、复核入口、通知/复核记录展示 | 全量测试通过 |
---
## 三、RR2-1 适用条件确认
### 任务
| 编号 | 内容 | 文件 |
| --- | --- | --- |
| RR2-1-001 | 实现适用条件候选识别 | `services/info_extract.py` |
| RR2-1-002 | 工作流支持 `waiting_user` 暂停 | `regulatory_review/workflow.py` |
| RR2-1-003 | 实现条件确认接口 | `regulatory_review/views.py` |
| RR2-1-004 | 实现对话选择框 UI | `templates/home.html``static/js/app.js` |
| RR2-1-005 | 确认后从 `rule_scope` 或下一节点恢复 | `workflow.py` |
| RR2-1-006 | 增加测试 | `tests/test_regulatory_condition.py``tests/test_regulatory_frontend.py` |
### 确认字段
以下选项来自既有第二阶段功能/详细设计:`RegulatoryInfoExtract` 输出产品类别、注册类型、临床评价路径,功能设计中明确注册类型包括“首次注册、变更注册、延续注册等”,临床评价路径包括“临床试验、免临床、同品种比对等”。因此 Demo 版按下表实现。
| 字段 | 交互 |
| --- | --- |
| 产品类别 | 体外诊断试剂 / 医疗器械 / 其他 |
| 注册类型 | 首次注册 / 变更注册 / 延续注册 |
| 临床评价路径 | 临床试验 / 免临床 / 同品种比对 / 待确认 |
| 产品名称 | 文本输入 |
| 型号规格 | 文本输入 |
| 预期用途 | 文本输入 |
### 验证命令
```bash
pytest tests/test_regulatory_condition.py tests/test_regulatory_frontend.py tests/test_regulatory_workflow.py
```
### Codex 执行提示
```text
请实现法规适用条件候选识别、waiting_user 暂停恢复和对话选择框确认。用户确认前工作流不得继续执行规则裁剪。
```
---
## 四、RR2-2 附件 4 规则对齐与核查能力增强
### 新增口径:附件 4 必须结构化入规则库
第一批主链路已经可以演示,但现有 Demo YAML 只覆盖 5 类规则:产品技术要求、说明书、注册检验报告、临床评价资料、安全和性能基本原则清单。经人工确认,第一批链路可通过;但与附件《体外诊断试剂注册申报资料要求及说明》相比,规则覆盖仍不完整。第二批 RR2-2 必须将附件 4 的申报资料目录结构补入规则库,并作为完整性和章节核查的主要依据。
附件来源:
```text
docs/0.原始材料/附件 4 体外诊断试剂注册申报资料要求及说明.doc
```
如附件仍为旧版 `.doc`,允许在开发阶段通过 Pandoc、LibreOffice headless、Word COM 或受控脚本转换为 `.docx`/`.txt` 中间产物;中间产物只用于规则抽取和测试夹具,不改变第一阶段文件页数统计口径。
### 附件 4 目录覆盖范围
第二批 Demo 规则至少覆盖以下一级和二级标题。规则应支持“章节目录”类目录项、资料文件项、条件适用项和推荐项的区分。
| 一级目录 | 二级目录/资料项 |
| --- | --- |
| 1. 监管信息 | 1.1 章节目录、1.2 申请表、1.3 术语/缩写词列表、1.4 产品列表、1.5 关联文件、1.6 申报前与监管机构的联系情况和沟通记录、1.7 符合性声明 |
| 2. 综述资料 | 2.1 章节目录、2.2 概述、2.3 产品描述、2.4 预期用途、2.5 申报产品上市历史、2.6 其他需说明的内容 |
| 3. 非临床资料 | 3.1 章节目录、3.2 产品风险管理资料、3.3 体外诊断试剂安全和性能基本原则清单、3.4 产品技术要求及检验报告、3.5 分析性能研究、3.6 稳定性研究、3.7 阳性判断值或参考区间研究、3.8 其他资料 |
| 4. 临床评价资料 | 4.1 章节目录、4.2 临床评价资料 |
| 5. 产品说明书和标签样稿 | 5.1 章节目录、5.2 产品说明书、5.3 标签样稿、5.4 其他资料 |
| 6. 质量管理体系文件 | 6.1 综述、6.2 章节目录、6.3 生产制造信息、6.4 质量管理体系程序、6.5 管理职责程序、6.6 资源管理程序、6.7 产品实现程序、6.8 质量管理体系的测量/分析和改进程序、6.9 其他质量体系程序信息、6.10 质量管理体系核查文件 |
### 规则分级默认值
| 规则类型 | 默认风险 | 说明 |
| --- | --- | --- |
| 一级目录整体缺失 | high | 如缺少“监管信息”“综述资料”“非临床资料”等完整章节 |
| 关键法定资料缺失 | blocking | 申请表、符合性声明、产品技术要求及检验报告等 |
| 关键技术/评价资料缺失 | high | 产品风险管理资料、分析性能研究、稳定性研究、临床评价资料、产品说明书、标签样稿等 |
| 条件适用资料缺失 | medium/high | 如上市历史、申报前沟通记录、其他资料;需结合 RR2-1 适用条件判断 |
| 章节目录缺失 | medium | 各一级目录下的章节目录缺失,影响资料可追溯性 |
### 与现有第一批链路的差异修正
| 当前能力 | 第二批修正 |
| --- | --- |
| 完整性核查只按文件名和相对路径匹配 | 增加目录名、首页文本/前若干页文本、章节标题候选匹配 |
| YAML 只覆盖 5 个 Demo 条目 | 扩展为附件 4 一级/二级目录规则,保留第一批 5 条并映射到附件 4 对应章节 |
| 章节核查只检查说明书储存条件/有效期/样本要求 | 改为同时检查申报资料目录结构和说明书内部关键章节 |
| RAG 可能跳过 `.doc` 材料 | 附件 4 必须可被转换或抽取,构建 RAG 前输出可读文本抽取状态 |
| 一致性只检查产品名称、型号规格、预期用途 | 保留这三项,并增加管理类别、分类编码、注册类型、临床评价路径等候选字段 |
### 任务
| 编号 | 内容 | 文件 |
| --- | --- | --- |
| RR2-2-001 | 将附件 4 `.doc` 抽取为可测试的结构化目录夹具 | `tests/fixtures/regulatory/attachment4_outline.json` 或同等 fixture |
| RR2-2-002 | 扩展 YAML 规则,覆盖附件 4 一级/二级目录、别名、适用条件、风险等级和整改建议 | `rules/nmpa_ivd_registration_v1.yaml` |
| RR2-2-003 | 增强规则加载校验,确保附件 4 必填目录项都有规则 ID、关键词、风险等级和 citation_query | `services/rule_loader.py` |
| RR2-2-004 | 增强完整性核查,支持文件名、目录名、首页文本/前若干页文本、章节标题候选匹配 | `services/completeness_check.py``services/text_extract.py` |
| RR2-2-005 | 增强文本抽取,缓存章节候选、字段候选、首页文本和抽取状态 | `services/text_extract.py``storage.py` |
| RR2-2-006 | 增强章节核查,支持附件 4 目录层级、别名、近似标题和证据片段 | `services/structure_check.py` |
| RR2-2-007 | 增强一致性核查,支持产品名称、型号规格、预期用途、管理类别、分类编码、注册类型、临床评价路径等来源值 | `services/consistency_check.py` |
| RR2-2-008 | RAG 引用写入 `rag_result_json` 过程产物,并记录附件 4 文本抽取/索引状态 | `services/rag_citation.py``storage.py` |
| RR2-2-009 | 增加附件 4 对齐测试 | `tests/test_regulatory_rule_loader.py``tests/test_regulatory_completeness.py``tests/test_regulatory_structure.py``tests/test_regulatory_consistency.py``tests/test_regulatory_rag.py` |
### 验收样例
| 样例条件 | 预期 |
| --- | --- |
| 文件包缺少“监管信息/申请表” | 生成 blocking 或 high 问题,并引用附件 4 监管信息要求 |
| 文件包缺少“产品风险管理资料” | 生成 high 问题category 为 completeness |
| 文件包缺少“分析性能研究”或“稳定性研究” | 生成 high 问题,给出补充研究资料建议 |
| 文件包有产品技术要求但无检验报告 | 生成 blocking 问题,规则映射到 3.4 |
| 文件包有产品说明书但无标签样稿 | 生成 high 问题,规则映射到 5.3 |
| 文件包缺少质量管理体系文件 | 生成 high 问题,规则映射到第 6 章 |
| 附件 4 `.doc` 未能抽取 | RAG 构建命令失败或明确报告附件 4 抽取失败,不允许静默跳过该核心材料 |
### 验证命令
```bash
pytest tests/test_regulatory_rule_loader.py tests/test_regulatory_completeness.py tests/test_regulatory_structure.py tests/test_regulatory_consistency.py tests/test_regulatory_rag.py
```
### Codex 执行提示
```text
请先将附件 4《体外诊断试剂注册申报资料要求及说明》结构化为规则覆盖清单再增强 YAML、完整性核查、章节核查、一致性核查和 RAG 过程产物。第二批必须覆盖附件 4 的 1-6 章一级目录和主要二级目录;证据必须包含文件路径、命中片段、字段名或规则 ID便于人工复核。附件 4 作为核心法规材料,不允许在 RAG 构建中静默跳过。
```
---
## 五、RR2-3 整包复核
### 口径
整包复核不是修改原法规核查批次,而是基于新的成功 `FileSummaryBatch` 创建新的 `RegulatoryReviewBatch`。新批次记录来源批次信息,用于报告中展示“复核来源”。
复核入口不新增独立页面。前端通过法规核查工作流卡片展示复核入口,用户点击后由 AI 在对话区发起确认与引导。
### 任务
| 编号 | 内容 | 文件 |
| --- | --- | --- |
| RR2-3-001 | 新增整包复核启动接口 | `regulatory_review/views.py` |
| RR2-3-002 | 支持指定新的 `file_summary_batch_id` | `workflow.py` |
| RR2-3-003 | 记录 source/regenerated_from 信息 | `RegulatoryReviewBatch.condition_json` 或独立字段 |
| RR2-3-004 | 报告展示整包复核来源 | `services/export.py` |
| RR2-3-005 | 增加测试 | `tests/test_regulatory_rectification.py` |
### 验证命令
```bash
pytest tests/test_regulatory_rectification.py tests/test_regulatory_workflow.py
```
### Codex 执行提示
```text
请实现整包复核:用户完成新的文件汇总后,可基于新 FileSummaryBatch 创建新的 RegulatoryReviewBatch并在报告中追溯原核查批次。
```
---
## 六、RR2-4 缺失项复核
### 口径
缺失项复核针对原 `RegulatoryIssue` 更新状态,不新建完整法规核查批次。系统可读取补充文件对应的新 `FileSummaryBatch`,只对指定问题重新匹配相关规则。
缺失项复核同样不新增独立页面。卡片只展示入口和状态,具体确认动作通过 AI 对话完成,例如确认复核哪些问题、使用哪个补充文件汇总批次。
### 任务
| 编号 | 内容 | 文件 |
| --- | --- | --- |
| RR2-4-001 | 实现缺失项复核服务 | `services/rectification_review.py` |
| RR2-4-002 | 支持 issue_ids + file_summary_batch_id 输入 | `views.py` |
| RR2-4-003 | 复核通过更新 `review_passed`,不通过更新 `review_failed` | `services/rectification_review.py` |
| RR2-4-004 | 生成 `review_record` 过程产物 | `storage.py` |
| RR2-4-005 | 报告展示复核记录 | `services/export.py` |
| RR2-4-006 | 增加测试 | `tests/test_regulatory_rectification.py` |
### 验证命令
```bash
pytest tests/test_regulatory_rectification.py
```
### Codex 执行提示
```text
请实现缺失项复核。复核不重新跑完整法规核查工作流,只针对指定 RegulatoryIssue 和补充文件汇总批次更新问题状态,并生成 review_record 产物。
```
---
## 七、RR2-5 mock 通知留痕
### 口径
真实飞书暂缓。第二批只在 blocking/high/medium 风险项出现时创建 `RegulatoryNotificationRecord(channel=mock)`,用于报告留痕和第三阶段接入。
### 任务
| 编号 | 内容 | 文件 |
| --- | --- | --- |
| RR2-5-001 | 实现 mock notifier | `services/feishu_notifier.py` |
| RR2-5-002 | 风险等级 blocking/high/medium 写通知记录 | `workflow.py` |
| RR2-5-003 | 通知记录进入 Markdown/Excel/JSON 报告 | `services/export.py` |
| RR2-5-004 | 增加测试 | `tests/test_regulatory_notification.py` |
### 验证命令
```bash
pytest tests/test_regulatory_notification.py tests/test_regulatory_export.py
```
### Codex 执行提示
```text
请实现 mock 通知留痕。不要接真实飞书 CLI/API只为阻断项、高风险、中风险写 RegulatoryNotificationRecord并在报告中展示。
```
---
## 八、RR2-6 前端和总体验收
### 任务
| 编号 | 内容 | 文件 |
| --- | --- | --- |
| RR2-6-001 | 前端显示条件确认卡片 | `templates/home.html``static/js/app.js` |
| RR2-6-002 | 前端通过工作流卡片展示整包复核入口,并由 AI 对话确认 | `static/js/app.js` |
| RR2-6-003 | 前端通过工作流卡片展示缺失项复核入口,并由 AI 对话确认 | `static/js/app.js` |
| RR2-6-004 | 卡片展示通知和复核摘要 | `templates/home.html``static/js/app.js` |
| RR2-6-005 | 补 Playwright 或前端测试 | `tests/test_regulatory_frontend.py` |
| RR2-6-006 | 全量回归 | 全项目 |
### 验证命令
```bash
python manage.py check
pytest
```
### Codex 执行提示
```text
请完善法规核查前端交互包含条件选择框、卡片式整包复核入口、卡片式缺失项复核入口、AI 对话确认、mock 通知和复核记录展示。不要新增独立复核页面。最后运行 python manage.py check 和 pytest 全量验收。
```
---
## 九、第二批 Codex 目标模式提示词
```text
请按 docs/5.开发计划/2.NMPA注册资料法规核查与整改闭环-第二批完整闭环.md 执行第二阶段第二批开发。
前提:
第一批主链路已经完成并通过全量测试。
目标:
补齐法规核查完整整改闭环包括适用条件对话选择框、waiting_user 暂停恢复、附件 4 申报资料目录规则对齐、整包复核、缺失项复核、mock 通知留痕、增强章节/一致性核查和前端交互。
执行规则:
1. 从第一批完成后的稳定分支创建 codex/YYYYMMDD-NMPA法规核查完整闭环 分支。
2. 按 RR2-1 到 RR2-6 顺序执行。
3. 每阶段完成后运行对应验证命令。
4. RR2-2 必须覆盖附件 4 的 1-6 章一级目录和主要二级目录,不能只保留第一批 5 条 Demo 规则。
5. 不接真实飞书 CLI/API。
6. 不做规则管理前端。
7. 不做自动填写目标文件。
8. 最后运行 python manage.py check 和 pytest 全量验收。
```

Some files were not shown because too many files have changed in this diff Show More