Compare commits

..

109 Commits

Author SHA1 Message Date
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
271 changed files with 40109 additions and 135 deletions

10
.env
View File

@@ -6,7 +6,9 @@ DJANGO_ALLOWED_HOSTS=*
LLM_PROVIDER=openai_compatible
LLM_API_KEY=sk-pgvkjondmmrlyxmrfhotgpuirgbtgzrpjpweorhwruflxmxw
LLM_BASE_URL=https://api.siliconflow.cn/v1
LLM_MODEL=Qwen/Qwen2.5-7B-Instruct
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
@@ -17,3 +19,9 @@ 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

1
.gitignore vendored
View File

@@ -6,4 +6,5 @@ db.sqlite3
staticfiles/
media/
.pytest_cache/
.tmp/
.idea/

32
PRODUCT.md Normal file
View File

@@ -0,0 +1,32 @@
# Product
## Register
product
## Users
注册资料准备、法规审核和项目管理人员,在资料整理、法规核查、问题整改和申报文件填表过程中使用。
## Product Purpose
DEMO-AGENT 是一个体外诊断试剂注册资料审核工作台。它把上传资料、文件汇总、法规规则核查、RAG 依据检索、风险预警、整改复核和申报表填充组织成可追溯的工作流。
## Brand Personality
克制、可信、清晰。界面应服务审核任务,优先呈现状态、证据和下一步动作。
## Anti-references
避免营销页式大标题、装饰性卡片堆叠、过度动画、过亮的渐变和不必要的视觉噪声。
## Design Principles
- 证据优先:每个结论都应能回到来源文件、规则或检索片段。
- 状态清楚:批次、节点、风险、异常和导出结果要一眼可辨。
- 操作克制:页面提供必要动作,不把审核工作做成复杂后台。
- 复用现有模式:新增页面沿用当前工作台导航、面板、表格和按钮体系。
## Accessibility & Inclusion
默认按 WCAG AA 方向处理对比度、键盘可访问和清晰标签。动效仅用于状态反馈,并尊重减少动态效果需求。

View File

@@ -18,3 +18,60 @@ python manage.py runserver
- 登录页http://127.0.0.1:8000/login/
- 首页http://127.0.0.1:8000/
- 管理后台http://127.0.0.1:8000/admin/
## 文件汇总依赖
自动汇总文件目录与页数功能使用轻量 Python 库读取 PDF、Word、Excel、PowerPoint 文件。
Docker 或生产环境如需处理 `.7z``.rar` 压缩包,还需要安装系统 `7z`/`p7zip`
命令,并确认以下命令可用:
```bash
7z
7z i
```
LibreOffice 不是必需依赖,仅作为未来增强老格式文档解析的可选能力。
上传原始文件、批次工作目录和导出文件默认存储在 Django `MEDIA_ROOT` 下的
`file_summary/users/<user_id>/<conversation_id>/` 或批次 `work_dir` 目录中。生产环境
需要把 `MEDIA_ROOT` 挂载到持久化卷,并纳入备份或归档策略。
## 飞书通知与问答预留
飞书接入使用企业自建应用/智能体的消息 API。敏感信息只允许写入本地 `.env`
或部署环境变量,不要提交真实 App Secret、tenant token、open_id 或 user_id。
常用环境变量:
| 变量名 | 用途 |
| --- | --- |
| `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_idopen_id 为空时使用 |
| `FEISHU_DEFAULT_TARGET_NAME` | 默认接收人展示名,用于记录和页面展示 |
| `FEISHU_TENANT_TOKEN_CACHE_SECONDS` | tenant_access_token 缓存秒数 |
| `PUBLIC_BASE_URL` | 飞书消息中的系统入口根地址,默认 `http://127.0.0.1:8000` |
自动化测试会 mock 飞书 token API 和消息 API不请求真实飞书接口。真实发送只通过
本地手动命令验证:
```bash
python manage.py send_test_feishu_notification --username owner
```
问答预留能力可用本地模拟命令验证:
```bash
python manage.py feishu_question_simulate --username owner "查最新法规核查"
```
集中测试建议在补齐 `.env` 后执行:
```bash
python manage.py check
pytest tests/test_feishu_*.py
pytest tests/test_file_summary_workflow.py tests/test_regulatory_notification.py tests/test_application_form_fill_notification.py
```

View File

@@ -92,6 +92,8 @@ USE_TZ = True
STATIC_URL = "static/"
STATICFILES_DIRS = [BASE_DIR / "static"]
MEDIA_ROOT = BASE_DIR / "media"
MEDIA_URL = "media/"
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
@@ -102,3 +104,76 @@ 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,11 +1,15 @@
from django.contrib import admin
from django.contrib.auth.views import LoginView, LogoutView, PasswordChangeView
from django.urls import path
from django.urls import include, path
from review_agent.views import stream_chat, workspace
from review_agent.views import attachment_manager, home_dashboard, knowledge_base_manager, stream_chat, workspace
urlpatterns = [
path("", workspace, name="home"),
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/",

View File

@@ -28,7 +28,7 @@
- 自动识别缺失文件并通知责任人
- 参考法规来源网站:
<https://www.cmde.org.cn/xwdt/zxyw/20210930163300622.html>
<https://www.cmde.org.cn/xwdt/zxyw/20210930163300622.html>
<https://www.nmpa.gov.cn/>

Binary file not shown.

Binary file not shown.

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

@@ -4,7 +4,7 @@
| 项目 | 内容 |
| --- | --- |
| 需求分析文档 | docs/需求分析/1.自动汇总.md |
| 需求分析文档 | docs/1.需求分析/1.自动汇总.md |
| 功能名称 | 自动汇总文件夹文件目录与页数 |
| 所属模块 | 审核智能体 review_agent |
| 设计日期 | 2026-06-05 |
@@ -137,7 +137,7 @@ WorkflowExecutor
| 职责 | 接收对话页上传的压缩包或多个文件,保存原始文件,创建上传批次 |
| 输入 | conversation_id、user_id、uploaded_files |
| 输出 | batch_id、upload_file_ids、upload_type、original_storage_paths |
| 数据写入 | FileSummaryBatch、UploadedSourceFile |
| 数据写入 | FileSummaryBatch、FileAttachment、FileSummaryBatchAttachment |
| 关键规则 | 文件必须绑定当前 Conversation同一对话只使用本对话上传的文件 |
### 4.3 压缩包解压 Skill
@@ -252,9 +252,9 @@ WorkflowExecutor
| started_at | DateTimeField | 开始时间 |
| finished_at | DateTimeField | 完成时间 |
### 5.2 UploadedSourceFile
### 5.2 FileAttachment
上传原始文件记录。
上传原始文件记录。用户上传即存储为 `FileAttachment`,批次启动时再通过 `FileSummaryBatchAttachment` 固化本次使用的附件版本。
| 字段 | 类型 | 说明 |
| --- | --- | --- |
@@ -552,7 +552,7 @@ export_id -> batch -> conversation -> user
| 设计点 | 说明 |
| --- | --- |
| 对话隔离 | 所有批次查询和下载必须校验 conversation.user |
| 防串文件 | 工作流只能读取当前 batch 绑定的 UploadedSourceFile |
| 防串文件 | 工作流只能读取当前 batch 通过 FileSummaryBatchAttachment 绑定的 FileAttachment |
| 解压安全 | 禁止压缩包内路径跳出批次工作目录 |
| 文件执行安全 | 不执行上传文件中的脚本、宏或外部链接 |
| 下载权限 | 下载接口必须验证当前用户拥有批次所属对话 |

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

@@ -4,9 +4,9 @@
| 项目 | 内容 |
| --- | --- |
| 需求分析文档 | docs/需求分析/1.自动汇总.md |
| 功能设计文档 | docs/功能设计/1.自动汇总.md |
| 详细设计文档 | docs/详细设计/1.自动汇总.md |
| 需求分析文档 | docs/1.需求分析/1.自动汇总.md |
| 功能设计文档 | docs/2.功能设计/1.自动汇总.md |
| 详细设计文档 | docs/3.详细设计/1.自动汇总.md |
| 数据库类型 | SQLite / Django ORM |
| 表名前缀 | ra_ |
| 设计日期 | 2026-06-05 |

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

@@ -4,8 +4,8 @@
| 项目 | 内容 |
| --- | --- |
| 需求分析文档 | docs/需求分析/1.自动汇总.md |
| 功能设计文档 | docs/功能设计/1.自动汇总.md |
| 需求分析文档 | docs/1.需求分析/1.自动汇总.md |
| 功能设计文档 | docs/2.功能设计/1.自动汇总.md |
| 功能名称 | 自动汇总文件夹文件目录与页数 |
| 所属模块 | 审核智能体 review_agent |
| 设计日期 | 2026-06-05 |

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

@@ -4,10 +4,10 @@
| 项目 | 内容 |
| --- | --- |
| 需求分析文档 | docs/需求分析/1.自动汇总.md |
| 功能设计文档 | docs/功能设计/1.自动汇总.md |
| 详细设计文档 | docs/详细设计/1.自动汇总.md |
| 数据库设计文档 | docs/数据库设计/1.自动汇总.md |
| 需求分析文档 | docs/1.需求分析/1.自动汇总.md |
| 功能设计文档 | docs/2.功能设计/1.自动汇总.md |
| 详细设计文档 | docs/3.详细设计/1.自动汇总.md |
| 数据库设计文档 | docs/4.数据库设计/1.自动汇总.md |
| 功能名称 | 自动汇总文件夹文件目录与页数 |
| 所属模块 | 审核智能体 review_agent |
| 执行方式 | 单人开发 + Codex 流水线自动化执行 |
@@ -120,7 +120,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/数据库设计/1.自动汇总.md``review_agent/models.py` 新增 7 个 `ra_` 表模型,使用 Django ORM、TextChoices、短表名、索引和唯一约束。 |
| Codex 执行提示 | 请按 `docs/4.数据库设计/1.自动汇总.md``review_agent/models.py` 新增 7 个 `ra_` 表模型,使用 Django ORM、TextChoices、短表名、索引和唯一约束。 |
### FS-P1-002 生成并验证数据库迁移
@@ -616,7 +616,7 @@
后续可直接对 Codex 输入:
```text
请按 docs/开发计划/1.自动汇总.md 执行,从 V2 创建 codex/YYYYMMDD-自动汇总文件目录页数 分支,按 P0 到 P7 顺序开发、验证和阶段提交。每个阶段完成后调用 git-commit-summary 生成提交摘要并本地提交。全部完成后合并回 V2并重新运行总体验收。
请按 docs/5.开发计划/1.自动汇总.md 执行,从 V2 创建 codex/YYYYMMDD-自动汇总文件目录页数 分支,按 P0 到 P7 顺序开发、验证和阶段提交。每个阶段完成后调用 git-commit-summary 生成提交摘要并本地提交。全部完成后合并回 V2并重新运行总体验收。
```
---

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 全量验收。
```

View File

@@ -0,0 +1,632 @@
# 产品关键信息提取与申报文件自动填表开发计划
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 需求分析文档 | docs/1.需求分析/3.产品关键信息提取与申报文件自动填表.md |
| 功能设计文档 | docs/2.功能设计/3.产品关键信息提取与申报文件自动填表.md |
| 详细设计文档 | docs/3.详细设计/3.产品关键信息提取与申报文件自动填表.md |
| 数据库设计文档 | docs/4.数据库设计/3.产品关键信息提取与申报文件自动填表.md |
| 功能名称 | 产品关键信息提取与申报文件自动填表 |
| 所属模块 | 审核智能体 review_agent |
| 执行方式 | 单人开发 + Codex 目标模式自动化执行 |
| 计划日期 | 2026-06-07 |
| 计划版本 | V1.0 |
---
## 一、开发计划目标
本开发计划用于指导 Codex 目标模式按阶段完成“产品关键信息提取与申报文件自动填表”功能开发。该功能作为独立工作流 `application_form_fill` 实现,由用户对话触发,默认复用当前对话最近成功的文件汇总批次;如本次消息带新附件,则先串联文件汇总,再执行自动填表。
本期必须完成:独立填表批次、过程产物、飞书通知记录、模板配置、注册证 `.docx` 模板填充、字段抽取与合并、冲突高亮、追溯清单、Word 下载、自动填表工作流卡片和权限校验。
本期明确不强制完成PDF 转换、字段级数据库表、`.doc` 模板自动转换、完整安全和性能基本原则清单条目拆解。这些事项已进入 `docs/6.待办计划/第二阶段暂缓事项.md`
---
## 二、已确认开发规则
| 规则项 | 内容 |
| --- | --- |
| 工作流类型 | 新增独立 `application_form_fill`,不塞入 `regulatory_review` 工作流 |
| 触发方式 | 用户对话触发,如“帮我填注册证”“给我这个内容对应的表格”“为我该方案生成申报模板” |
| 模板指定 | 用户可指定模板;未指定时按注册类型生成适用模板 |
| 文件来源 | 无新附件时复用当前对话最近成功 `FileSummaryBatch`;有新附件时先自动汇总 |
| 模板配置 | 放在 `review_agent/application_form_fill/templates/application_form_templates_v1.yaml` |
| 字段抽取 | 规则/正则与 LLM 结构化抽取并行,合并处理 |
| 冲突处理 | 说明书优先;冲突字段在 Word 中黄色底色、红色字体 |
| 输出范围 | Demo 主链路优先 Word + Excel/JSON 追溯清单 |
| PDF | 数据结构预留,工作流节点可 skipped不作为本期强验收 |
| 飞书 | 新增自动填表通知记录表,通知失败不阻断下载 |
| 数据库 | 新增三张表;字段级明细表暂缓 |
| Git 提交 | 每个阶段完成并验证通过后提交一次 |
| 测试要求 | 每阶段至少运行对应 pytest前端阶段补卡片和渲染测试 |
---
## 三、总体验收标准
| 类别 | 完成标准 |
| --- | --- |
| 数据库 | `ApplicationFormFillBatch``ApplicationFormFillArtifact``ApplicationFormFillNotificationRecord` 可通过 migration 落库 |
| 导出类型 | `ExportedSummaryFile.ExportType` 支持 `word``pdf`,并兼容既有 markdown/excel/json |
| 模块结构 | 新增 `review_agent/application_form_fill/` 独立模块 |
| 触发 | 用户说“帮我填注册证”等语句可触发 `application_form_fill` |
| 文件来源 | 无新附件时复用最近成功汇总批次;无汇总批次时提示上传资料 |
| 模板配置 | YAML 可加载、校验,并至少配置注册证格式 `.docx` 已识别字段 |
| 字段抽取 | 规则/正则与 LLM 抽取结果均可留底LLM 失败时规则结果可继续 |
| 字段合并 | 说明书优先,冲突字段进入 `conflict_summary` 和追溯清单 |
| Word 填充 | 能按表格行名填入注册证模板字段,缺失字段留空 |
| 冲突高亮 | 冲突字段在 Word 内黄底红字 |
| 追溯清单 | 生成 Excel/JSON记录规则结果、LLM 结果、合并字段、冲突和来源证据 |
| 下载 | 对话框提供填好 Word 和追溯清单下载链接 |
| 工作流卡片 | 前端支持 `application_form_fill` 卡片,展示准备资料、选择模板、复制模板、抽取字段、填写 Word 等节点 |
| 飞书通知 | 填表完成后写通知记录,可 mock失败不阻断文件下载 |
| 权限 | A 对话不能查询或下载 B 对话的填表批次和导出文件 |
| 回归 | 第一批文件汇总、第二批法规核查既有测试不回归 |
---
## 四、阶段总览
| 阶段 | 名称 | 目标 | 阶段验收 |
| --- | --- | --- | --- |
| AFF-0 | 准备与回归 | 创建开发分支,确认现有测试基线 | `python manage.py check` 和关键回归测试通过 |
| AFF-1 | 数据模型与通用导出扩展 | 新增三张表,扩展 word/pdf 导出类型 | migration、模型测试通过 |
| AFF-2 | 模块骨架与模板配置 | 新建独立模块、YAML 配置和配置校验 | 模板配置测试通过 |
| AFF-3 | 触发与工作流骨架 | 对话触发、批次创建、节点事件和状态查询 | 可创建并运行空工作流 |
| AFF-4 | 模板选择与文件来源 | 复用最近汇总批次,支持指定/默认模板选择 | 模板选择和来源批次测试通过 |
| AFF-5 | 字段抽取与合并 | 规则/正则 + LLM 并行抽取、冲突归并和产物留底 | 字段抽取、冲突测试通过 |
| AFF-6 | Word 填充与追溯导出 | 注册证 Word 填充、冲突高亮、Excel/JSON 追溯 | 可下载 Word 和追溯清单 |
| AFF-7 | 飞书通知与对话摘要 | 生成助手摘要、下载链接和通知记录 | 通知、摘要、下载权限测试通过 |
| AFF-8 | 前端卡片与总体验收 | 自动填表工作流卡片、状态恢复、全量回归 | 全量测试通过 |
---
## 五、AFF-0 准备与回归
### AFF-0-001 创建开发分支并确认现状
| 项目 | 内容 |
| --- | --- |
| 任务类型 | Git / 准备 |
| 前置任务 | 无 |
| 涉及文件 | 无固定文件 |
| 目标 | 从当前稳定分支创建 `codex/YYYYMMDD-申报文件自动填表` 开发分支,并确认工作区状态 |
| 开发步骤 | 1. 检查当前分支和 `git status`2. 确认第三批设计文档存在3. 创建开发分支4. 记录已有未提交变更,不得回滚用户变更 |
| 验收标准 | 分支创建成功,工作区变更来源清楚 |
| 验证命令 | `git branch --show-current`; `git status --short` |
| Codex 执行提示 | 请创建第三批自动填表开发分支,检查当前工作区状态和设计文档,不要回滚用户已有变更。 |
### AFF-0-002 运行基线回归
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 测试 / 回归 |
| 前置任务 | AFF-0-001 |
| 涉及文件 | 无固定文件 |
| 目标 | 确认现有文件汇总和法规核查主流程在开发前可用 |
| 开发步骤 | 1. 运行 Django check2. 运行文件汇总测试3. 运行法规核查测试4. 记录失败项并先判断是否为既有问题 |
| 验收标准 | 关键回归测试通过,或记录清楚既有失败和本阶段处理策略 |
| 验证命令 | `python manage.py check`; `pytest tests/test_file_summary_*.py tests/test_regulatory_*.py` |
| Codex 执行提示 | 请在开发前运行 Django check 和文件汇总/法规核查关键测试,确认基线稳定。若存在既有失败,请记录,不要直接改无关代码。 |
---
## 六、AFF-1 数据模型与通用导出扩展
### AFF-1-001 新增自动填表 ORM 模型
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 数据库 / 后端 |
| 前置任务 | AFF-0 |
| 涉及文件 | `review_agent/models.py` |
| 目标 | 新增 `ApplicationFormFillBatch``ApplicationFormFillArtifact``ApplicationFormFillNotificationRecord` |
| 开发步骤 | 1. 定义批次状态枚举2. 定义产物类型枚举3. 定义通知状态和渠道枚举4. 添加外键到 Conversation、User、Message、FileSummaryBatch、RegulatoryReviewBatch5. 添加 JSONField、hash、路径、时间字段6. 添加 `db_table`、索引和唯一约束 |
| 验收标准 | 模型字段、表名、索引与数据库设计一致 |
| 验证命令 | `python manage.py check` |
| Codex 执行提示 | 请按 `docs/4.数据库设计/3.产品关键信息提取与申报文件自动填表.md` 新增自动填表三张表模型,模型集中放在 `review_agent/models.py`。 |
### AFF-1-002 扩展导出类型和权限查询能力
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 数据库 / 下载 |
| 前置任务 | AFF-1-001 |
| 涉及文件 | `review_agent/models.py`、导出下载权限相关视图 |
| 目标 | 为 `ExportedSummaryFile.ExportType` 增加 `word``pdf`,并确保下载权限支持 `application_form_fill` |
| 开发步骤 | 1. 扩展 `ExportType.WORD`2. 扩展 `ExportType.PDF`3. 检查下载接口按 workflow_type 分派权限4. 增加 application_form_fill 反查批次的权限路径 |
| 验收标准 | Word/ PDF 导出记录可创建;填表导出下载权限可追溯到当前用户 |
| 验证命令 | `python manage.py check`; `pytest tests/test_file_summary_views.py -k download` |
| Codex 执行提示 | 请扩展 ExportedSummaryFile 支持 word/pdf并让现有下载接口能通过 workflow_type=application_form_fill 校验填表批次权限。 |
### AFF-1-003 生成迁移并补模型测试
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 数据库 / 测试 |
| 前置任务 | AFF-1-002 |
| 涉及文件 | `review_agent/migrations/``tests/test_application_form_fill_models.py` |
| 目标 | 生成迁移并覆盖新增表的基础约束和权限关系 |
| 开发步骤 | 1. 运行 makemigrations2. 检查 migration 只包含第三批相关变更3. 运行 migrate4. 测试批次创建5. 测试产物 hash 字段6. 测试通知重试字段7. 测试 ExportedSummaryFile word 类型 |
| 验收标准 | migration 可执行,模型测试通过 |
| 验证命令 | `python manage.py makemigrations review_agent`; `python manage.py migrate`; `pytest tests/test_application_form_fill_models.py` |
| Codex 执行提示 | 请为第三批模型生成迁移并新增模型测试,覆盖批次、产物、通知记录和 word/pdf 导出类型。 |
### AFF-1 阶段验证
```bash
python manage.py check
pytest tests/test_application_form_fill_models.py tests/test_file_summary_views.py -k download
```
---
## 七、AFF-2 模块骨架与模板配置
### AFF-2-001 创建 application_form_fill 模块骨架
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 后端 / 模块 |
| 前置任务 | AFF-1 |
| 涉及文件 | `review_agent/application_form_fill/` |
| 目标 | 建立独立模块目录、常量、schemas、storage、workflow、views 和 services 包 |
| 开发步骤 | 1. 创建模块目录2. 创建 `constants.py`3. 创建 `schemas.py`4. 创建 `storage.py`5. 创建 `workflow.py`6. 创建 `views.py`7. 创建 services 子模块8. 创建 templates 和 prompts 目录 |
| 验收标准 | 模块可 import不影响既有应用启动 |
| 验证命令 | `python manage.py check` |
| Codex 执行提示 | 请新增 `review_agent/application_form_fill/` 独立模块骨架先只放常量、schema、空服务和基础 import不要改动法规核查模块。 |
### AFF-2-002 编写模板配置 YAML
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 配置 / 模板 |
| 前置任务 | AFF-2-001 |
| 涉及文件 | `review_agent/application_form_fill/templates/application_form_templates_v1.yaml` |
| 目标 | 建立模板配置,至少覆盖注册证 `.docx` 已识别字段 |
| 开发步骤 | 1. 定义 version2. 定义 source_dir3. 配置 `registration_certificate`4. 配置 `change_registration``.doc` 待转换模板5. 配置 `essential_principles``.doc` 待转换模板6. 为注册证配置注册人名称、注册人住所、生产地址、产品名称、包装规格、主要组成成分、预期用途、储存条件及有效期、附件等字段 |
| 验收标准 | YAML 可解析,注册证字段映射到 table_row |
| 验证命令 | `pytest tests/test_application_form_fill_template_config.py` |
| Codex 执行提示 | 请新增自动填表模板配置 YAML配置路径必须是 `review_agent/application_form_fill/templates/application_form_templates_v1.yaml`,先完整录入注册证表格字段。 |
### AFF-2-003 实现模板配置加载与校验
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 后端 / 配置 |
| 前置任务 | AFF-2-002 |
| 涉及文件 | `review_agent/application_form_fill/services/template_config.py``tests/test_application_form_fill_template_config.py` |
| 目标 | 读取、校验模板配置并计算 hash |
| 开发步骤 | 1. 实现 `load_template_config()`2. 实现 `validate_template_config()`3. 实现 `compute_config_hash()`4. 校验 version、source_dir、templates、code 唯一、source_file 存在、target.type 支持5. 对 `.doc` 待转换模板允许配置存在但标记运行时处理 |
| 验收标准 | 有效配置通过,缺失 source_dir 或重复 code 能被测试捕获 |
| 验证命令 | `pytest tests/test_application_form_fill_template_config.py` |
| Codex 执行提示 | 请实现模板配置加载和校验服务,配置错误必须返回清晰错误列表,不要在 import 时直接崩溃。 |
### AFF-2 阶段验证
```bash
python manage.py check
pytest tests/test_application_form_fill_template_config.py
```
---
## 八、AFF-3 触发与工作流骨架
### AFF-3-001 扩展意图路由
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 后端 / 意图识别 |
| 前置任务 | AFF-2 |
| 涉及文件 | `review_agent/skill_router.py``review_agent/application_form_fill/constants.py``tests/test_application_form_fill_trigger.py` |
| 目标 | 用户话语命中自动填表意图时返回 `application_form_fill` |
| 开发步骤 | 1. 增加触发关键词2. 支持“帮我填注册证”“对应的表格”“生成申报模板”等3. 支持指定模板识别入口4. 保持文件汇总和法规核查路由不回归 |
| 验收标准 | 自动填表语句触发正确,普通对话不误触发 |
| 验证命令 | `pytest tests/test_application_form_fill_trigger.py tests/test_regulatory_workflow.py -k router` |
| Codex 执行提示 | 请扩展现有意图路由,新增 application_form_fill 动作。不要破坏 file_summary 和 regulatory_review 的现有触发。 |
### AFF-3-002 实现批次创建和节点初始化
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 后端 / 工作流 |
| 前置任务 | AFF-3-001 |
| 涉及文件 | `review_agent/application_form_fill/workflow.py``review_agent/application_form_fill/storage.py``tests/test_application_form_fill_workflow.py` |
| 目标 | 创建填表批次、生成工作目录、初始化节点 |
| 开发步骤 | 1. 实现 `build_batch_no()`2. 实现 `build_batch_work_dir()`3. 实现 `create_application_form_fill_batch()`4. 绑定 conversation、user、trigger_message、source_summary_batch5. 初始化 `FORM_FILL_NODE_DEFINITIONS` 节点6. 写 workflow_created 事件 |
| 验收标准 | 批次编号唯一,节点数量正确,工作目录在受控路径 |
| 验证命令 | `pytest tests/test_application_form_fill_workflow.py -k create` |
| Codex 执行提示 | 请实现自动填表批次创建和节点初始化workflow_type 必须写 application_form_fill。 |
### AFF-3-003 实现工作流执行器骨架
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 后端 / 工作流 |
| 前置任务 | AFF-3-002 |
| 涉及文件 | `review_agent/application_form_fill/workflow.py``tests/test_application_form_fill_workflow.py` |
| 目标 | 实现节点串行执行、状态更新、事件推送和 skipped PDF 节点 |
| 开发步骤 | 1. 实现 `FormFillWorkflowExecutor.run()`2. 实现 `_nodes()`3. 实现 `_run_node()`4. 每个节点写 running/success/skipped5. `pdf_convert` 本期标记 skipped6. 失败时写 batch.failed |
| 验收标准 | 空实现节点可完整跑到 successPDF 节点 skipped |
| 验证命令 | `pytest tests/test_application_form_fill_workflow.py -k executor` |
| Codex 执行提示 | 请实现自动填表工作流执行器骨架先让节点状态可完整流转PDF 转换节点本期标记 skipped。 |
### AFF-3-004 接入流式对话启动逻辑
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 后端 / 对话 |
| 前置任务 | AFF-3-003 |
| 涉及文件 | `review_agent/services.py``review_agent/application_form_fill/views.py` |
| 目标 | 用户触发自动填表时启动工作流;有附件时先自动汇总,无附件时使用最近成功汇总批次 |
| 开发步骤 | 1. 在 stream_message 中处理 application_form_fill 路由2. 如本次存在新附件复用文件汇总启动逻辑3. 无新附件时查找最近成功 `FileSummaryBatch`4. 无来源批次时回复请上传资料5. 返回 workflow meta |
| 验收标准 | 对话触发能创建填表批次;无汇总批次时不崩溃 |
| 验证命令 | `pytest tests/test_application_form_fill_workflow.py -k stream` |
| Codex 执行提示 | 请把 application_form_fill 接入现有 stream_message。无附件时复用最近成功汇总批次有新附件时先自动汇总。 |
### AFF-3 阶段验证
```bash
python manage.py check
pytest tests/test_application_form_fill_trigger.py tests/test_application_form_fill_workflow.py
```
---
## 九、AFF-4 模板选择与文件来源
### AFF-4-001 实现模板指定解析
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 后端 / 模板选择 |
| 前置任务 | AFF-3 |
| 涉及文件 | `review_agent/application_form_fill/services/template_select.py``tests/test_application_form_fill_template_select.py` |
| 目标 | 从用户话语中识别指定模板 |
| 开发步骤 | 1. 识别注册证2. 识别变更注册备案文件3. 识别安全和性能基本原则清单4. 识别全部模板5. 未指定返回空数组 |
| 验收标准 | 指定模板语句可返回正确 template_codes |
| 验证命令 | `pytest tests/test_application_form_fill_template_select.py -k requested` |
| Codex 执行提示 | 请实现用户指定模板解析,支持注册证、变更注册备案文件、安全和性能基本原则清单、全部模板。 |
### AFF-4-002 实现注册类型识别和模板选择
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 后端 / 模板选择 |
| 前置任务 | AFF-4-001 |
| 涉及文件 | `review_agent/application_form_fill/services/template_select.py``tests/test_application_form_fill_template_select.py` |
| 目标 | 按用户话语、法规确认条件、文件抽取识别注册类型,并选择模板 |
| 开发步骤 | 1. 用户话语识别首次注册、变更注册、备案2. 从 `source_regulatory_batch.condition_json` 读取 confirmed_conditions3. 从文件抽取候选读取 registration_type4. 未指定模板时首次注册生成注册证 + 基本原则清单5. 变更/备案生成变更文件 + 基本原则清单6. 指定不适用模板允许生成但写 risk_notes |
| 验收标准 | 模板选择规则与功能设计一致 |
| 验证命令 | `pytest tests/test_application_form_fill_template_select.py` |
| Codex 执行提示 | 请实现注册类型识别和默认模板选择优先级是用户话语、已确认法规核查条件、文件抽取、unknown。 |
### AFF-4-003 实现模板复制服务
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 后端 / 模板 |
| 前置任务 | AFF-4-002 |
| 涉及文件 | `review_agent/application_form_fill/services/template_repository.py``review_agent/application_form_fill/storage.py``tests/test_application_form_fill_template_repository.py` |
| 目标 | 将原始模板复制到批次目录,原始模板只读 |
| 开发步骤 | 1. 根据 TemplateSpec 定位 source_file2. 复制到 `work_dir/templates`3. 记录 ApplicationFormFillArtifact(template_copy)4. `.doc` 且无工作模板时返回模板失败不影响其他模板5. 路径必须在受控工作目录内 |
| 验收标准 | 注册证 `.docx` 可复制;原始文件不被修改;产物 hash 写入 |
| 验证命令 | `pytest tests/test_application_form_fill_template_repository.py` |
| Codex 执行提示 | 请实现模板复制服务,只允许复制到批次工作目录,不能直接写原始法规材料目录。 |
### AFF-4 阶段验证
```bash
pytest tests/test_application_form_fill_template_select.py tests/test_application_form_fill_template_repository.py
```
---
## 十、AFF-5 字段抽取与合并
### AFF-5-001 实现规则/正则字段抽取
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 后端 / 字段抽取 |
| 前置任务 | AFF-4 |
| 涉及文件 | `review_agent/application_form_fill/services/field_extract.py``tests/test_application_form_fill_field_extract.py` |
| 目标 | 从说明书、产品技术要求等文本中按标签和章节抽取字段 |
| 开发步骤 | 1. 复用 `regulatory_review.services.text_extract.extract_text`2. 识别文件角色3. 匹配 `字段名:值` 标签行4. 支持多行值拼接5. 保存 source_file、source_role、evidence、confidence、extractor=rule |
| 验收标准 | 能从测试说明书文本抽取产品名称、预期用途、储存条件、有效期、包装规格 |
| 验证命令 | `pytest tests/test_application_form_fill_field_extract.py -k rules` |
| Codex 执行提示 | 请实现自动填表规则/正则字段抽取,优先覆盖注册证模板字段,抽取结果必须包含来源文件、来源角色和证据片段。 |
### AFF-5-002 实现 LLM 结构化抽取封装
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 后端 / LLM |
| 前置任务 | AFF-5-001 |
| 涉及文件 | `review_agent/application_form_fill/services/field_extract.py``review_agent/application_form_fill/prompts/field_extract.md``tests/test_application_form_fill_field_extract.py` |
| 目标 | 调用现有 LLM 能力输出字段 JSON失败时降级 |
| 开发步骤 | 1. 编写字段抽取 prompt2. 输入模板字段、文件上下文和候选文本3. 要求输出 JSON fields/checklist_items4. 解析 JSON5. 捕获超时和解析失败6. 失败返回空 LLM 结果,不阻断规则抽取 |
| 验收标准 | monkeypatch LLM 后可解析结构化字段LLM 异常时工作流继续 |
| 验证命令 | `pytest tests/test_application_form_fill_field_extract.py -k llm` |
| Codex 执行提示 | 请实现 LLM 结构化抽取封装必须可测试、可降级。LLM 输出解析失败不能导致整个填表批次失败。 |
### AFF-5-003 实现并行抽取和产物留底
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 后端 / 字段抽取 |
| 前置任务 | AFF-5-002 |
| 涉及文件 | `review_agent/application_form_fill/services/field_extract.py``review_agent/application_form_fill/storage.py` |
| 目标 | 并行执行规则/正则和 LLM 抽取,并保存 `field_extract_result.json` |
| 开发步骤 | 1. 使用 ThreadPoolExecutor2. 规则和 LLM 两路并行3. 组装 regex_results、llm_results、selected_templates、source_evidence4. 保存 JSON5. 写 ApplicationFormFillArtifact(field_extract_result) |
| 验收标准 | JSON 产物包含两路结果和模板列表 |
| 验证命令 | `pytest tests/test_application_form_fill_field_extract.py -k parallel` |
| Codex 执行提示 | 请实现字段并行抽取和 field_extract_result.json 产物留底LLM 失败时也必须保存规则结果。 |
### AFF-5-004 实现字段合并与冲突检测
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 后端 / 字段合并 |
| 前置任务 | AFF-5-003 |
| 涉及文件 | `review_agent/application_form_fill/services/field_merge.py``tests/test_application_form_fill_field_merge.py` |
| 目标 | 合并规则和 LLM 字段,说明书优先,并生成冲突摘要 |
| 开发步骤 | 1. 实现字段值归一化2. 实现来源优先级排序3. 同字段多值一致时合并4. 不一致时选择最高优先级来源5. 说明书与其他文件冲突时标记 conflict6. 输出 merged_fields 和 conflicts |
| 验收标准 | 说明书优先;冲突字段包含 selected_value、selected_source、conflict_values、handling |
| 验证命令 | `pytest tests/test_application_form_fill_field_merge.py` |
| Codex 执行提示 | 请实现字段合并服务,严格按说明书优先处理冲突,并把冲突列表写成可用于对话摘要和追溯清单的结构。 |
### AFF-5 阶段验证
```bash
pytest tests/test_application_form_fill_field_extract.py tests/test_application_form_fill_field_merge.py
```
---
## 十一、AFF-6 Word 填充与追溯导出
### AFF-6-001 实现 Word 表格行填充
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 后端 / Word |
| 前置任务 | AFF-5 |
| 涉及文件 | `review_agent/application_form_fill/services/word_fill.py``tests/test_application_form_fill_word_fill.py` |
| 目标 | 使用 `python-docx` 按表格行名写入注册证模板 |
| 开发步骤 | 1. 打开 docx 模板副本2. 遍历 tables/rows/cells3. 匹配第一列 row_label4. 写入第二列5. 缺失字段保持空白6. 保存 output_path |
| 验收标准 | 产品名称、包装规格、预期用途等能写入注册证模板对应行 |
| 验证命令 | `pytest tests/test_application_form_fill_word_fill.py -k table` |
| Codex 执行提示 | 请实现 Word 表格行填充服务,先支持注册证模板的两列表格行名匹配。 |
### AFF-6-002 实现冲突高亮
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 后端 / Word |
| 前置任务 | AFF-6-001 |
| 涉及文件 | `review_agent/application_form_fill/services/word_fill.py``tests/test_application_form_fill_word_fill.py` |
| 目标 | 冲突字段在 Word 中黄底红字 |
| 开发步骤 | 1. 对冲突字段写入 run2. 设置字体颜色 `FF0000`3. 设置单元格 shading `FFFF00`4. 非冲突字段保持原样式5. 测试读取 docx XML 验证颜色和底色 |
| 验收标准 | 冲突字段样式可在 docx XML 中验证 |
| 验证命令 | `pytest tests/test_application_form_fill_word_fill.py -k highlight` |
| Codex 执行提示 | 请实现 Word 冲突高亮,冲突字段必须红色字体和黄色底色,测试需检查 docx XML。 |
### AFF-6-003 创建 Word 导出记录
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 后端 / 导出 |
| 前置任务 | AFF-6-002 |
| 涉及文件 | `review_agent/application_form_fill/services/word_fill.py``review_agent/application_form_fill/workflow.py` |
| 目标 | Word 生成后写入 `ExportedSummaryFile(export_type=word)` 和产物记录 |
| 开发步骤 | 1. 按批次号、产品名、模板标签生成文件名2. 保存到 `work_dir/filled`3. 创建 `ApplicationFormFillArtifact(filled_template)`4. 创建 `ExportedSummaryFile`5. 记录模板失败时错误 |
| 验收标准 | 可查询到 word 导出记录和 filled_template 产物 |
| 验证命令 | `pytest tests/test_application_form_fill_word_fill.py -k export` |
| Codex 执行提示 | 请把 Word 填充结果保存为导出文件export_type 使用 wordworkflow_type 使用 application_form_fill。 |
### AFF-6-004 实现追溯清单 Excel/JSON
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 后端 / 导出 |
| 前置任务 | AFF-6-003 |
| 涉及文件 | `review_agent/application_form_fill/services/traceability_export.py``tests/test_application_form_fill_traceability.py` |
| 目标 | 输出字段来源追溯清单和合并结果 JSON |
| 开发步骤 | 1. 生成“字段追溯”Sheet2. 生成“冲突字段”Sheet3. 生成“低置信度条目”Sheet4. 生成“生成结果”Sheet5. 保存 Excel6. 保存 merged_fields.json7. 创建导出和产物记录 |
| 验收标准 | Excel 可打开,包含字段、来源、证据、冲突、处理方式 |
| 验证命令 | `pytest tests/test_application_form_fill_traceability.py` |
| Codex 执行提示 | 请实现字段来源追溯清单导出,必须包含规则/LLM 合并结果、冲突字段和生成结果。 |
### AFF-6 阶段验证
```bash
pytest tests/test_application_form_fill_word_fill.py tests/test_application_form_fill_traceability.py
```
---
## 十二、AFF-7 飞书通知与对话摘要
### AFF-7-001 生成助手 Markdown 摘要
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 后端 / 对话 |
| 前置任务 | AFF-6 |
| 涉及文件 | `review_agent/application_form_fill/services/traceability_export.py``review_agent/application_form_fill/workflow.py` |
| 目标 | 工作流完成后向当前对话写入下载链接和冲突摘要 |
| 开发步骤 | 1. 汇总 Word 导出2. 汇总 PDF 状态为待增强3. 汇总冲突字段4. 添加追溯清单下载链接5. 创建 assistant Message |
| 验收标准 | 对话中出现 Markdown 表格、Word 下载、追溯清单下载和冲突摘要 |
| 验证命令 | `pytest tests/test_application_form_fill_workflow.py -k summary` |
| Codex 执行提示 | 请实现自动填表完成后的助手 Markdown 摘要PDF 本期显示为待增强,不作为失败。 |
### AFF-7-002 实现飞书通知记录和 mock 通知
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 后端 / 通知 |
| 前置任务 | AFF-7-001 |
| 涉及文件 | `review_agent/application_form_fill/services/notifier.py``tests/test_application_form_fill_notification.py` |
| 目标 | 填表完成后记录通知,可 mock 发送,失败不阻断下载 |
| 开发步骤 | 1. 实现 `notify_completion()`2. 默认 channel=mock3. 写 template_codes、export_ids、message_summary4. 支持 send_status success/failed5. 失败时记录 error_message 和 retry_count |
| 验收标准 | 通知记录可查;通知失败不影响批次核心产物 |
| 验证命令 | `pytest tests/test_application_form_fill_notification.py` |
| Codex 执行提示 | 请实现自动填表通知服务,先用 mock 通知记录即可。通知失败不得阻断 Word 下载。 |
### AFF-7-003 完成工作流状态归并
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 后端 / 工作流 |
| 前置任务 | AFF-7-002 |
| 涉及文件 | `review_agent/application_form_fill/workflow.py``tests/test_application_form_fill_workflow.py` |
| 目标 | 根据 Word、追溯清单、通知结果标记 success/partial_success/failed |
| 开发步骤 | 1. 所有目标 Word 成功时 success2. 至少一个 Word 成功但非关键产物失败时 partial_success3. 所有 Word 失败时 failed4. PDF skipped 不导致失败5. 发送 workflow_completed 事件 |
| 验收标准 | 批次状态符合详细设计 |
| 验证命令 | `pytest tests/test_application_form_fill_workflow.py -k status` |
| Codex 执行提示 | 请完成自动填表工作流状态归并PDF skipped 不影响 success通知失败最多导致 partial_success。 |
### AFF-7 阶段验证
```bash
pytest tests/test_application_form_fill_workflow.py tests/test_application_form_fill_notification.py
```
---
## 十三、AFF-8 前端卡片与总体验收
### AFF-8-001 后端状态接口
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 后端 / 接口 |
| 前置任务 | AFF-7 |
| 涉及文件 | `review_agent/application_form_fill/views.py``review_agent/urls.py` 或相关 URL 文件 |
| 目标 | 提供自动填表启动和状态查询接口 |
| 开发步骤 | 1. 新增 start 接口2. 新增 detail/status 接口3. 返回 batch、nodes、conflicts、exports4. 校验 conversation/user 权限5. 接入 URL |
| 验收标准 | 当前用户可查自己的填表批次,不能查他人批次 |
| 验证命令 | `pytest tests/test_application_form_fill_views.py` |
| Codex 执行提示 | 请实现自动填表启动和状态查询接口,所有查询必须校验当前用户权限。 |
### AFF-8-002 前端支持 application_form_fill 卡片
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 前端 / 工作流卡片 |
| 前置任务 | AFF-8-001 |
| 涉及文件 | `static/js/app.js``templates/home.html`、静态 CSS 文件 |
| 目标 | 前端展示自动填表工作流卡片,并根据 SSE 更新节点 |
| 开发步骤 | 1. 解析 workflow_type=application_form_fill2. 定义节点文案3. 创建卡片4. 更新节点状态5. PDF 节点显示待增强/跳过6. 页面刷新后恢复 |
| 验收标准 | 自动填表卡片可显示准备资料、选择模板、复制模板、抽取字段、填写 Word、追溯清单、飞书通知 |
| 验证命令 | `pytest tests/test_application_form_fill_frontend.py` 或现有前端测试命令 |
| Codex 执行提示 | 请在现有工作流卡片逻辑中新增 application_form_fill 类型,展示自动填表节点并支持状态恢复。 |
### AFF-8-003 前端展示结果和下载链接
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 前端 / Markdown |
| 前置任务 | AFF-8-002 |
| 涉及文件 | `static/js/app.js`、模板和 CSS |
| 目标 | 对话框正常展示 Word 下载、追溯清单、冲突摘要 |
| 开发步骤 | 1. 确认助手 Markdown 渲染支持表格2. 验证 Word 下载链接点击3. 验证冲突摘要表格4. PDF 列显示待增强 |
| 验收标准 | 对话结果可读、链接可用、PDF 待增强不被误判为失败 |
| 验证命令 | 前端/Playwright 对应测试 |
| Codex 执行提示 | 请验证并完善自动填表结果展示,确保 Markdown 表格、Word 下载链接、追溯清单链接和冲突摘要正常显示。 |
### AFF-8-004 总体验收与回归
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 验收 / 回归 |
| 前置任务 | AFF-8-003 |
| 涉及文件 | 全项目 |
| 目标 | 运行全量测试,确认前三批能力均不回归 |
| 开发步骤 | 1. 运行 Django check2. 运行自动填表测试3. 运行文件汇总测试4. 运行法规核查测试5. 如可用,运行前端/Playwright 测试6. 检查 git status |
| 验收标准 | 全量测试通过;失败项均有解释;无意外文件变更 |
| 验证命令 | `python manage.py check`; `pytest` |
| Codex 执行提示 | 请执行第三批自动填表总体验收,运行 Django check 和 pytest 全量回归,确认文件汇总与法规核查不回归。 |
### AFF-8 阶段验证
```bash
python manage.py check
pytest
```
---
## 十四、测试分层要求
| 层级 | 验证内容 | 建议文件 |
| --- | --- | --- |
| 模型测试 | 三张新表、word/pdf 导出类型、权限关系 | `tests/test_application_form_fill_models.py` |
| 配置测试 | YAML 加载、模板配置校验、hash | `tests/test_application_form_fill_template_config.py` |
| 选择测试 | 触发语句、指定模板、注册类型优先级、默认模板 | `tests/test_application_form_fill_template_select.py` |
| 抽取测试 | 规则/正则、LLM 降级、并行抽取、字段合并 | `tests/test_application_form_fill_field_extract.py``tests/test_application_form_fill_field_merge.py` |
| Word 测试 | 表格行填充、冲突高亮、导出记录 | `tests/test_application_form_fill_word_fill.py` |
| 导出测试 | 追溯清单 Excel、JSON 产物、下载权限 | `tests/test_application_form_fill_traceability.py``tests/test_application_form_fill_views.py` |
| 工作流测试 | 批次创建、节点流转、状态归并、助手摘要 | `tests/test_application_form_fill_workflow.py` |
| 通知测试 | mock 通知、失败记录、重试字段 | `tests/test_application_form_fill_notification.py` |
| 前端测试 | 卡片节点、PDF 待增强、下载链接、冲突摘要 | `tests/test_application_form_fill_frontend.py` |
---
## 十五、Codex 自动化执行规则
| 规则 | 内容 |
| --- | --- |
| 顺序执行 | 必须从 AFF-0 到 AFF-8 顺序执行,不得跳阶段 |
| TDD | 新行为先写失败测试,再实现 |
| 当前阶段优先 | 某阶段失败时先修复当前阶段,不继续后续阶段 |
| 回归保护 | 文件汇总和法规核查已有测试不得回归 |
| PDF 边界 | PDF 节点本期可 skipped不为 PDF 引入强依赖 |
| 字段表边界 | 不新增字段级数据库表,后续增强已在待办计划 |
| 每阶段验证 | 每阶段完成后运行对应验证命令 |
| 每阶段提交 | 每阶段验证通过后生成提交摘要并本地提交 |
| 不覆盖变更 | 不得回滚或覆盖用户已有未提交变更 |
---
## 十六、推荐目标模式提示词
后续可直接对 Codex 输入:
```text
请按 docs/5.开发计划/3.产品关键信息提取与申报文件自动填表.md 执行第三批开发。
目标:
完成独立 application_form_fill 工作流,通过用户对话触发自动填表,复用当前对话最近成功 FileSummaryBatch支持模板配置、注册证 Word 自动填写、规则/正则与 LLM 并行字段抽取、说明书优先冲突归并、冲突高亮、字段来源追溯清单、Word 下载、自动填表工作流卡片和飞书 mock 通知记录。
执行规则:
1. 创建 codex/YYYYMMDD-申报文件自动填表 分支。
2. 按 AFF-0 到 AFF-8 顺序执行,不跳阶段。
3. 每阶段先写测试,再实现,完成后运行对应验证命令。
4. 不实现字段级数据库表。
5. PDF 转换本期作为 skipped/待增强,不引入强制 LibreOffice 依赖。
6. 模板配置路径必须为 review_agent/application_form_fill/templates/application_form_templates_v1.yaml。
7. Word 模板优先支持注册证格式 docx两个 doc 模板可标记待转换或部分成功。
8. 每阶段验证通过后调用 git-commit-summary 生成提交摘要并本地提交。
9. 最后运行 python manage.py check 和 pytest 全量验收。
```
---
## 十七、待执行前检查清单
| 检查项 | 状态 |
| --- | --- |
| 第三批需求分析、功能设计、详细设计、数据库设计均已存在 | 待执行时确认 |
| 当前分支是否适合创建开发分支 | 待执行时确认 |
| 是否存在用户未提交变更 | 待执行时确认 |
| `python-docx``openpyxl``PyYAML` 是否可用 | 待执行时确认 |
| 现有文件汇总和法规核查测试是否通过 | 待执行时确认 |
| 执行机器是否提供 `git-commit-summary` skill | 待执行时确认 |
| `.doc` 模板和 PDF 转换是否保持在待办边界内 | 待执行时确认 |

View File

@@ -0,0 +1,583 @@
# 飞书通知与问答接入开发计划
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 需求分析文档 | docs/1.需求分析/4.飞书通知与问答接入.md |
| 功能设计文档 | docs/2.功能设计/4.飞书通知与问答接入.md |
| 详细设计文档 | docs/3.详细设计/4.飞书通知与问答接入.md |
| 数据库设计文档 | docs/4.数据库设计/4.飞书通知与问答接入.md |
| 功能名称 | 飞书通知与问答接入 |
| 所属模块 | 审核智能体 review_agent |
| 执行方式 | 单人开发 + Codex 自动化执行 |
| 计划日期 | 2026-06-07 |
| 计划版本 | V1.0 |
---
## Codex 自动执行说明
本文件用于 Codex 自动执行开发任务。执行时必须按阶段顺序推进,不得跳过测试、不得直接请求真实飞书接口作为自动化测试、不得把真实 App Secret 或 token 写入代码库。
执行规则:
| 规则 | 要求 |
| --- | --- |
| 执行顺序 | 必须从 FS-0 到 FS-8 顺序执行,前一阶段验证未通过不得进入下一阶段 |
| TDD | 每个服务、模型、命令和页面展示任务必须先写失败测试,再实现代码,再运行测试确认通过 |
| 外部 API | 自动化测试必须 mock 飞书 token API 和消息 API真实飞书只通过 `send_test_feishu_notification` 手动命令验证 |
| 凭证安全 | 不得提交真实 `FEISHU_APP_ID``FEISHU_APP_SECRET``tenant_access_token`、用户 open_id/user_id |
| 失败处理 | 如测试失败,先定位是否由本阶段改动引起;不得修改无关功能绕过测试 |
| 工作区安全 | 不得回滚用户已有变更;如遇到同文件用户改动,先阅读并兼容 |
| 提交节奏 | 每个阶段完成并通过阶段验证后再提交,提交信息参考“建议提交切分” |
| 实现边界 | 首期只做指定个人账号私聊通知和问答预留不得扩展外部群聊、事件订阅、LLM 问答解析 |
自动执行入口建议:
```text
请按 docs/5.开发计划/4.飞书通知与问答接入.md 从 FS-0 开始逐阶段执行。
每个阶段必须先写测试、运行失败、实现、运行通过,再进入下一阶段。
真实飞书 API 只能通过手动 management command 验证pytest 中必须 mock。
```
---
## 一、开发计划目标
本开发计划用于指导“飞书通知与问答接入”首期开发。首期目标是通过飞书官方智能体/应用机器人接口,把系统中三个工作流的结束结果发送到指定个人飞书账号,并为后续飞书内问答建立可测试的最小服务边界。
本期必须完成:
| 类别 | 内容 |
| --- | --- |
| 真实飞书通知 | 使用 App ID/App Secret 获取 `tenant_access_token`,调用飞书消息 API 发送私聊通知 |
| 指定个人账号 | 通过 `.env` 配置 `FEISHU_DEFAULT_USER_OPEN_ID``FEISHU_DEFAULT_USER_ID`,首期优先发给该账号 |
| 三流程接入 | 自动汇总、法规核查、自动填表三个流程完成后均触发通知 |
| 数据库记录 | 新增统一通知记录表、飞书用户映射表、token 缓存表、问答日志表 |
| 页面展示 | 三个流程结果页或详情区域展示飞书通知状态 |
| 问答预留 | 建表、实现批次摘要查询、简单规则意图解析、本地模拟问答命令 |
| 测试策略 | 关键服务严格 TDD自动化测试 mock 飞书 API真实飞书发送通过 management command 手动验证 |
本期明确不做:
| 类别 | 内容 |
| --- | --- |
| 外部群聊接入 | 暂不向群聊发送通知,不做群内 @ |
| 飞书事件订阅 | 暂不接收飞书回调,不实现真实私聊问答事件入口 |
| 手动重发 | 页面和 Admin 暂不提供重发按钮 |
| 自动后台重试 | 通知失败只记录;成功才判重,失败允许后续再次发送 |
| LLM 问答解析 | 问答预留只做简单规则解析,不接 LLM |
---
## 二、已确认开发规则
| 规则项 | 内容 |
| --- | --- |
| 主接入方式 | 飞书官方智能体/应用机器人消息 API |
| 凭证配置 | `.env` 提供 `FEISHU_APP_ID``FEISHU_APP_SECRET` |
| 接收人配置 | `.env` + Django Admin 都做;首期发送优先使用 `.env` 指定个人账号 |
| 接收人优先级 | `FEISHU_DEFAULT_USER_OPEN_ID` > `FEISHU_DEFAULT_USER_ID` |
| token 缓存 | 数据库缓存 `tenant_access_token` 和过期时间 |
| 通知记录 | 新增统一 `WorkflowNotificationRecord`,三个流程都写入 |
| 判重策略 | 同一批次、同一流程、同一状态,只有成功记录才判重;失败后允许再次发送 |
| 系统链接 | 新增 `PUBLIC_BASE_URL`,默认 `http://127.0.0.1:8000` |
| 页面展示 | 三个流程结果页或详情区域展示通知状态 |
| 真实 API 测试 | 自动化测试全部 mock新增 management command 手动发送真实测试消息 |
| TDD | 每个核心模块先写测试再实现 |
| 环境变量说明 | 写变量名和用途,不写真实值 |
| 阶段提交 | 模型、服务、工作流、页面、命令、测试分阶段提交 |
| 问答预留 | 建 `FeishuQuestionLog`,实现摘要查询、规则解析和本地模拟命令 |
---
## 三、总体验收标准
| 类别 | 完成标准 |
| --- | --- |
| 配置 | `.env` 支持 `FEISHU_APP_ID``FEISHU_APP_SECRET``FEISHU_DEFAULT_USER_OPEN_ID` / `FEISHU_DEFAULT_USER_ID``PUBLIC_BASE_URL` |
| token | 系统可获取、缓存、过期刷新 `tenant_access_token`token API 失败会记录失败通知 |
| 发送 | 手动命令可向指定个人账号发送真实测试消息 |
| 通知 | 三个流程完成后均创建通知记录,并在启用配置时调用飞书消息 API |
| 判重 | 成功记录存在时,同一批次/流程/状态不重复发送;失败记录不阻止再次发送 |
| 失败隔离 | 飞书发送失败不影响业务工作流完成 |
| 页面 | 三个流程结果页或详情区域能看到通知通道、接收人、状态、时间、失败原因 |
| 问答预留 | 本地模拟命令可解析“最新/最近/批次号/工作流关键词”,返回批次摘要并记录日志 |
| 权限 | 普通用户只能查询自己的批次摘要;管理员可查全部 |
| 回归 | 文件汇总、法规核查、自动填表既有测试不回归 |
---
## 四、阶段总览
| 阶段 | 名称 | 目标 | 阶段验收 |
| --- | --- | --- | --- |
| FS-0 | 准备与基线 | 确认文档和测试基线 | `python manage.py check` 与关键现有测试通过 |
| FS-1 | 数据模型与配置 | 新增通知、映射、token、问答日志模型和环境配置 | migration、模型测试通过 |
| FS-2 | 飞书 API 基础服务 | token 获取缓存、接收人解析、消息构造、消息 API client | 服务单测通过,全部 mock 外部 HTTP |
| FS-3 | 通知调度与记录 | 统一通知上下文、判重、成功/失败/disabled 落库 | 通知服务测试通过 |
| FS-4 | 三流程接入 | 自动汇总、法规核查、自动填表完成后触发通知 | 三流程通知集成测试通过 |
| FS-5 | 页面展示 | 批次详情或结果区域展示通知状态 | 页面/视图测试通过 |
| FS-6 | 手动真实测试命令 | management command 发送真实飞书测试消息 | 本地配置后可向个人账号发消息 |
| FS-7 | 问答预留能力 | 批次摘要查询、规则意图解析、模拟问答命令、问答日志 | 问答预留测试通过 |
| FS-8 | 文档与全量回归 | 更新环境变量说明,运行全量相关测试 | 回归通过,计划完成 |
---
## 五、FS-0 准备与基线
### FS-0-001 确认开发文档和当前工作区
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 准备 / Git |
| 前置任务 | 无 |
| 涉及文件 | 文档文件,不改代码 |
| 目标 | 确认需求、功能、数据库、详细设计和开发计划均存在,并记录当前工作区状态 |
| 开发步骤 | 1. 检查 `git status --short`2. 确认四份设计文档与本开发计划存在3. 确认当前未提交变更均为文档或用户已有变更4. 不回滚任何用户变更 |
| 验收标准 | 工作区状态清楚,可进入开发 |
| 验证命令 | `git status --short` |
| Codex 执行提示 | 请先确认飞书接入四份设计文档和开发计划存在,检查工作区状态,不要回滚用户已有变更。 |
### FS-0-002 运行基线测试
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 测试 / 回归 |
| 前置任务 | FS-0-001 |
| 涉及文件 | 无固定文件 |
| 目标 | 确认开发前现有主流程可运行 |
| 开发步骤 | 1. 运行 Django check2. 运行通知相关旧测试3. 运行三个工作流关键测试4. 若失败,判断是否既有问题并记录 |
| 验收标准 | 基线通过,或既有失败已记录且不与本功能冲突 |
| 验证命令 | `python manage.py check`; `pytest tests/test_file_summary_workflow.py tests/test_regulatory_notification.py tests/test_application_form_fill_notification.py` |
| Codex 执行提示 | 请运行 Django check 和现有通知/工作流关键测试,确认开发前基线。 |
---
## 六、FS-1 数据模型与配置
### FS-1-001 新增飞书接入 ORM 模型测试
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 测试 / 数据库 |
| 前置任务 | FS-0 |
| 涉及文件 | `tests/test_feishu_models.py` |
| 目标 | 先写失败测试覆盖飞书用户映射、token 缓存、统一通知记录、问答日志 |
| 开发步骤 | 1. 新增 `test_feishu_user_mapping_preferred_identifier`2. 新增 `test_feishu_access_token_cache_expiry`3. 新增 `test_workflow_notification_success_dedupe_only_success`4. 新增 `test_feishu_question_log_records_summary_without_full_answer` |
| 验收标准 | 新测试因模型不存在而失败 |
| 验证命令 | `pytest tests/test_feishu_models.py -q` |
| Codex 执行提示 | 请先为飞书相关模型写失败测试,覆盖接收人标识优先级、数据库 token 缓存、成功判重和问答日志摘要。 |
### FS-1-002 新增模型
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 数据库 / 后端 |
| 前置任务 | FS-1-001 |
| 涉及文件 | `review_agent/models.py` |
| 目标 | 新增 `FeishuUserMapping``FeishuAccessTokenCache``WorkflowNotificationRecord``FeishuQuestionLog` |
| 开发步骤 | 1. `FeishuUserMapping` 关联系统用户,支持 open_id、user_id、mobile、is_active2. `FeishuAccessTokenCache` 保存 token、expires_at、app_id_hash、error_message3. `WorkflowNotificationRecord` 保存 workflow_type、batch_id、batch_no、status、channel、target、send_status、summary、error、sent_at4. `FeishuQuestionLog` 保存问题、意图、查询对象、回答摘要、权限结果和状态5. 添加索引和模型方法 |
| 验收标准 | `python manage.py check` 通过 |
| 验证命令 | `python manage.py check` |
| Codex 执行提示 | 请按数据库设计新增四个模型。注意 token 需要数据库缓存,通知判重只对 success 生效。 |
### FS-1-003 生成迁移并通过模型测试
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 数据库 / 测试 |
| 前置任务 | FS-1-002 |
| 涉及文件 | `review_agent/migrations/``tests/test_feishu_models.py` |
| 目标 | 生成 migration模型测试全部通过 |
| 开发步骤 | 1. 运行 makemigrations2. 检查 migration 只包含飞书相关表3. 运行 migrate4. 运行模型测试 |
| 验收标准 | migration 可执行,模型测试通过 |
| 验证命令 | `python manage.py makemigrations review_agent`; `python manage.py migrate`; `pytest tests/test_feishu_models.py -q` |
| Codex 执行提示 | 请生成飞书相关模型迁移并运行模型测试。 |
### FS-1-004 注册 Admin 和配置项
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 后台 / 配置 |
| 前置任务 | FS-1-003 |
| 涉及文件 | `review_agent/admin.py``config/settings.py``.env.example` 或 README |
| 目标 | Admin 可维护用户映射settings 暴露飞书配置;文档只写变量名不写真实值 |
| 开发步骤 | 1. 注册 `FeishuUserMapping``WorkflowNotificationRecord``FeishuAccessTokenCache``FeishuQuestionLog`2. settings 读取 `FEISHU_NOTIFY_ENABLED``FEISHU_APP_ID``FEISHU_APP_SECRET``FEISHU_DEFAULT_USER_OPEN_ID``FEISHU_DEFAULT_USER_ID``FEISHU_DEFAULT_TARGET_NAME``PUBLIC_BASE_URL`3. 默认 `PUBLIC_BASE_URL=http://127.0.0.1:8000`4. 在说明文件中加入变量名和用途 |
| 验收标准 | Django check 通过Admin 列表可展示字段 |
| 验证命令 | `python manage.py check` |
| Codex 执行提示 | 请注册飞书相关模型到 Admin并新增环境变量配置说明不要写入真实凭证。 |
### FS-1 阶段验证
```bash
python manage.py check
pytest tests/test_feishu_models.py -q
```
---
## 七、FS-2 飞书 API 基础服务
### FS-2-001 token 服务 TDD
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 测试 / 服务 |
| 前置任务 | FS-1 |
| 涉及文件 | `tests/test_feishu_api_services.py` |
| 目标 | 先写 token 获取、缓存、过期刷新、失败记录测试 |
| 开发步骤 | 1. mock 飞书 token HTTP 成功2. 测试首次获取后写数据库缓存3. 测试未过期时不再请求 HTTP4. 测试过期后重新请求5. 测试 token API 失败返回错误对象 |
| 验收标准 | 测试先失败 |
| 验证命令 | `pytest tests/test_feishu_api_services.py -k token -q` |
| Codex 执行提示 | 请先写飞书 tenant_access_token 服务测试,外部 HTTP 必须 mock。 |
### FS-2-002 实现 token 服务
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 后端 / 服务 |
| 前置任务 | FS-2-001 |
| 涉及文件 | `review_agent/notifications/feishu_token.py` |
| 目标 | 使用 App ID/App Secret 获取并数据库缓存 `tenant_access_token` |
| 开发步骤 | 1. 定义 `FeishuTokenResult`2. 检查配置缺失3. 查询未过期数据库缓存4. 调用 token API5. 保存 token 和 expires_at6. token 失败时返回错误,不抛出到业务流程 |
| 验收标准 | token 服务测试通过 |
| 验证命令 | `pytest tests/test_feishu_api_services.py -k token -q` |
| Codex 执行提示 | 请实现 token 服务,缓存放数据库,不打印 App Secret 和 token。 |
### FS-2-003 接收人解析和消息构造 TDD
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 测试 / 服务 |
| 前置任务 | FS-2-002 |
| 涉及文件 | `tests/test_feishu_api_services.py` |
| 目标 | 测试指定个人接收人优先级、配置缺失、富文本消息摘要 |
| 开发步骤 | 1. 测试 `FEISHU_DEFAULT_USER_OPEN_ID` 优先2. 测试无 open_id 时使用 `FEISHU_DEFAULT_USER_ID`3. 测试均缺失返回 `recipient_missing`4. 测试消息包含流程、批次、状态、摘要、链接和发起人 |
| 验收标准 | 测试先失败 |
| 验证命令 | `pytest tests/test_feishu_api_services.py -k 'recipient or message' -q` |
| Codex 执行提示 | 请先写接收人解析和富文本消息构造测试。 |
### FS-2-004 实现接收人解析和消息构造
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 后端 / 服务 |
| 前置任务 | FS-2-003 |
| 涉及文件 | `review_agent/notifications/recipient.py``review_agent/notifications/message_builder.py``review_agent/notifications/context.py` |
| 目标 | 生成统一通知上下文、指定个人接收人和飞书富文本 payload |
| 开发步骤 | 1. 定义 `NotificationContext`2. 定义 `ResolvedFeishuTarget`3. 实现 `resolve_configured_personal_recipient()`4. 实现 `build_feishu_post_message()`5. 实现 `build_message_summary()`6. 链接使用 `PUBLIC_BASE_URL` |
| 验收标准 | 接收人和消息构造测试通过 |
| 验证命令 | `pytest tests/test_feishu_api_services.py -k 'recipient or message' -q` |
| Codex 执行提示 | 请实现通知上下文、接收人解析和飞书富文本消息构造。首期不需要群 @。 |
### FS-2-005 消息 API client TDD 与实现
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 后端 / 测试 |
| 前置任务 | FS-2-004 |
| 涉及文件 | `tests/test_feishu_api_services.py``review_agent/notifications/feishu_message_api.py` |
| 目标 | mock 飞书消息 API完成成功、超时、错误码、token 失效重试一次 |
| 开发步骤 | 1. 写成功测试,断言请求携带 Authorization2. 写非 0 code 测试3. 写超时测试4. 写 token 失效后刷新 token 并同步重试一次测试5. 实现 `send_personal_message()` |
| 验收标准 | 消息 API client 测试通过 |
| 验证命令 | `pytest tests/test_feishu_api_services.py -q` |
| Codex 执行提示 | 请用 mock HTTP 实现飞书消息 API client。自动化测试不得请求真实飞书。 |
### FS-2 阶段验证
```bash
pytest tests/test_feishu_api_services.py -q
```
---
## 八、FS-3 通知调度与记录
### FS-3-001 通知记录服务 TDD
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 测试 / 服务 |
| 前置任务 | FS-2 |
| 涉及文件 | `tests/test_feishu_notification_dispatcher.py` |
| 目标 | 先写通知调度、成功判重、失败允许再次发送、disabled 记录测试 |
| 开发步骤 | 1. 测试通知关闭写 disabled2. 测试发送成功写 success3. 测试已有 success 时不再调用 API4. 测试已有 failed 时允许再次调用 API5. 测试 token 失败写 failed |
| 验收标准 | 测试先失败 |
| 验证命令 | `pytest tests/test_feishu_notification_dispatcher.py -q` |
| Codex 执行提示 | 请先写统一通知调度测试,重点覆盖成功判重和失败可重试。 |
### FS-3-002 实现通知记录和 dispatcher
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 后端 / 服务 |
| 前置任务 | FS-3-001 |
| 涉及文件 | `review_agent/notifications/records.py``review_agent/notifications/dispatcher.py` |
| 目标 | 实现统一通知调度入口 |
| 开发步骤 | 1. 实现 `already_successfully_sent(dedupe_key)`2. 实现 disabled、success、failed 记录创建3. 实现 `dispatch_workflow_notification(context)`4. 捕获服务异常并写 failed5. 不让异常冒泡阻断工作流 |
| 验收标准 | dispatcher 测试通过 |
| 验证命令 | `pytest tests/test_feishu_notification_dispatcher.py -q` |
| Codex 执行提示 | 请实现统一通知调度和记录落库。注意 success 才判重failed 不判重。 |
### FS-3 阶段验证
```bash
pytest tests/test_feishu_notification_dispatcher.py -q
```
---
## 九、FS-4 三流程接入
### FS-4-001 工作流 adapter TDD
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 测试 / 集成 |
| 前置任务 | FS-3 |
| 涉及文件 | `tests/test_feishu_workflow_adapters.py` |
| 目标 | 测试自动汇总、法规核查、自动填表三类批次能生成正确通知上下文 |
| 开发步骤 | 1. 构造 `FileSummaryBatch` 和 items断言文件摘要2. 构造 `RegulatoryReviewBatch` 和 issues断言风险摘要3. 构造 `ApplicationFormFillBatch` 和 exports断言导出/冲突摘要4. 断言 result_url 使用 `PUBLIC_BASE_URL` |
| 验收标准 | 测试先失败 |
| 验证命令 | `pytest tests/test_feishu_workflow_adapters.py -q` |
| Codex 执行提示 | 请先写三个工作流 adapter 的测试。 |
### FS-4-002 实现工作流 adapters
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 后端 / 服务 |
| 前置任务 | FS-4-001 |
| 涉及文件 | `review_agent/notifications/workflow_adapters.py` |
| 目标 | 三个工作流批次转换为 `NotificationContext` |
| 开发步骤 | 1. 实现 `build_file_summary_context()`2. 实现 `build_regulatory_review_context()`3. 实现 `build_application_form_fill_context()`4. 控制摘要长度5. 处理 partial_success 和 failed |
| 验收标准 | adapter 测试通过 |
| 验证命令 | `pytest tests/test_feishu_workflow_adapters.py -q` |
| Codex 执行提示 | 请实现三个工作流通知上下文 adapter。 |
### FS-4-003 接入三个工作流完成节点
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 后端 / 工作流 |
| 前置任务 | FS-4-002 |
| 涉及文件 | `review_agent/file_summary/workflow.py``review_agent/regulatory_review/workflow.py``review_agent/application_form_fill/workflow.py` |
| 目标 | 三个工作流完成后调用通知 dispatcher |
| 开发步骤 | 1. 自动汇总成功/失败状态落定后触发通知2. 法规核查报告和风险落库后触发通知3. 自动填表 notify 节点改为统一通知服务4. 捕获通知异常并记录非阻断错误5. 保留现有 mock 测试兼容 |
| 验收标准 | 三流程通知集成测试通过 |
| 验证命令 | `pytest tests/test_file_summary_workflow.py tests/test_regulatory_notification.py tests/test_application_form_fill_notification.py` |
| Codex 执行提示 | 请把统一通知服务接入三个工作流完成节点,通知失败不得影响业务状态。 |
### FS-4 阶段验证
```bash
pytest tests/test_feishu_workflow_adapters.py tests/test_file_summary_workflow.py tests/test_regulatory_notification.py tests/test_application_form_fill_notification.py
```
---
## 十、FS-5 页面展示
### FS-5-001 通知状态展示测试
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 测试 / 前端 |
| 前置任务 | FS-4 |
| 涉及文件 | `tests/test_file_summary_frontend.py``tests/test_regulatory_frontend.py``tests/test_application_form_fill_frontend.py` |
| 目标 | 测试三个流程页面或结果区域展示飞书通知状态 |
| 开发步骤 | 1. 准备 success 通知记录断言页面出现“飞书通知已发送”2. 准备 failed 记录断言出现失败原因3. 无记录时展示“暂无飞书通知记录”或不破坏页面 |
| 验收标准 | 测试先失败 |
| 验证命令 | `pytest tests/test_file_summary_frontend.py tests/test_regulatory_frontend.py tests/test_application_form_fill_frontend.py -k feishu` |
| Codex 执行提示 | 请先写三个流程通知状态展示测试。 |
### FS-5-002 实现通知状态 presenter 和页面展示
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 后端 / 前端 |
| 前置任务 | FS-5-001 |
| 涉及文件 | `review_agent/notifications/presenter.py``review_agent/*/views.py``templates/home.html` 或相关模板 |
| 目标 | 页面展示通知状态、接收人、发送时间、失败原因 |
| 开发步骤 | 1. 实现 `get_notification_records(workflow_type, batch_id)`2. 在三个流程视图上下文中加入通知记录3. 模板展示最近一条通知状态4. 保持页面无记录时兼容 |
| 验收标准 | 页面展示测试通过 |
| 验证命令 | `pytest tests/test_file_summary_frontend.py tests/test_regulatory_frontend.py tests/test_application_form_fill_frontend.py -k feishu` |
| Codex 执行提示 | 请实现通知状态 presenter并在三个流程结果页展示最近飞书通知状态。 |
### FS-5 阶段验证
```bash
pytest tests/test_file_summary_frontend.py tests/test_regulatory_frontend.py tests/test_application_form_fill_frontend.py -k feishu
```
---
## 十一、FS-6 手动真实测试命令
### FS-6-001 测试命令 TDD
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 测试 / 命令 |
| 前置任务 | FS-5 |
| 涉及文件 | `tests/test_feishu_management_commands.py` |
| 目标 | 测试 management command 构造测试通知并调用 dispatcher |
| 开发步骤 | 1. mock dispatcher2. 执行 `send_test_feishu_notification --username owner`3. 断言构造测试上下文4. 测试缺少用户时报错 |
| 验收标准 | 测试先失败 |
| 验证命令 | `pytest tests/test_feishu_management_commands.py -q` |
| Codex 执行提示 | 请先写真实飞书测试命令的自动化测试dispatcher 要 mock。 |
### FS-6-002 实现发送测试消息命令
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 运维 / 命令 |
| 前置任务 | FS-6-001 |
| 涉及文件 | `review_agent/management/commands/send_test_feishu_notification.py` |
| 目标 | 本地可手动向指定个人飞书账号发送真实测试消息 |
| 开发步骤 | 1. 支持 `--username`2. 构造 workflow_type=`manual_test``NotificationContext`3. 调用 dispatcher4. 输出 send_status、target、error_message5. 不打印 token 和 App Secret |
| 验收标准 | 命令测试通过;本地配置真实凭证后可手动验证 |
| 验证命令 | `pytest tests/test_feishu_management_commands.py -q`; `python manage.py send_test_feishu_notification --username owner` |
| Codex 执行提示 | 请实现发送测试飞书通知的 management command。自动测试 mock dispatcher真实发送只手动运行。 |
### FS-6 阶段验证
```bash
pytest tests/test_feishu_management_commands.py -q
```
手动验证命令:
```bash
python manage.py send_test_feishu_notification --username owner
```
---
## 十二、FS-7 问答预留能力
### FS-7-001 批次摘要查询服务 TDD
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 测试 / 服务 |
| 前置任务 | FS-6 |
| 涉及文件 | `tests/test_feishu_question_reserved.py` |
| 目标 | 测试按批次号、latest、工作流类型查询三个流程摘要 |
| 开发步骤 | 1. 普通用户查询自己的最新法规核查批次2. 普通用户不能查询他人批次3. 管理员可查全部4. 按批次号精确查询5. 返回状态、摘要、链接 |
| 验收标准 | 测试先失败 |
| 验证命令 | `pytest tests/test_feishu_question_reserved.py -k query -q` |
| Codex 执行提示 | 请先写飞书问答预留的批次摘要查询测试。 |
### FS-7-002 实现批次摘要查询服务
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 后端 / 服务 |
| 前置任务 | FS-7-001 |
| 涉及文件 | `review_agent/feishu_questions/query.py``review_agent/feishu_questions/permissions.py` |
| 目标 | 支持三个工作流的摘要查询和权限过滤 |
| 开发步骤 | 1. 实现管理员/普通用户权限过滤2. 实现 batch_no 查询3. 实现 latest 查询4. 实现 workflow_type 关键词映射5. 返回统一摘要 dict |
| 验收标准 | 查询服务测试通过 |
| 验证命令 | `pytest tests/test_feishu_question_reserved.py -k query -q` |
| Codex 执行提示 | 请实现问答预留查询服务,普通用户只能查自己的批次,管理员可查全部。 |
### FS-7-003 简单意图解析和日志 TDD
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 测试 / 服务 |
| 前置任务 | FS-7-002 |
| 涉及文件 | `tests/test_feishu_question_reserved.py` |
| 目标 | 测试规则解析“最新/最近/批次号/工作流关键词”,并记录问答日志 |
| 开发步骤 | 1. 识别 `RR-``AFF-``FS-` 批次号2. 识别“最新/最近”3. 识别“法规核查/自动填表/自动汇总”4. 记录 `FeishuQuestionLog`,不保存完整回答正文 |
| 验收标准 | 测试先失败 |
| 验证命令 | `pytest tests/test_feishu_question_reserved.py -k 'intent or log' -q` |
| Codex 执行提示 | 请先写简单规则意图解析和问答日志测试,不接 LLM。 |
### FS-7-004 实现意图解析、问答服务和模拟命令
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 后端 / 命令 |
| 前置任务 | FS-7-003 |
| 涉及文件 | `review_agent/feishu_questions/intent.py``review_agent/feishu_questions/service.py``review_agent/management/commands/feishu_question_simulate.py` |
| 目标 | 本地模拟飞书问答输入,返回批次摘要并记录日志 |
| 开发步骤 | 1. 实现 `parse_question_intent(text)`2. 实现 `answer_question(user, text)`3. 写入 `FeishuQuestionLog`4. 实现命令 `python manage.py feishu_question_simulate --username owner "查最新法规核查"`5. 输出回答摘要 |
| 验收标准 | 问答预留测试和命令测试通过 |
| 验证命令 | `pytest tests/test_feishu_question_reserved.py -q`; `python manage.py feishu_question_simulate --username owner "查最新法规核查"` |
| Codex 执行提示 | 请实现飞书问答预留的规则解析、服务和本地模拟命令。 |
### FS-7 阶段验证
```bash
pytest tests/test_feishu_question_reserved.py -q
python manage.py feishu_question_simulate --username owner "查最新法规核查"
```
---
## 十三、FS-8 文档与全量回归
### FS-8-001 更新配置说明
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 文档 / 配置 |
| 前置任务 | FS-7 |
| 涉及文件 | `README.md``.env.example` 或项目配置说明文档 |
| 目标 | 说明飞书相关环境变量和手动测试命令 |
| 开发步骤 | 1. 写明变量名和用途2. 标注不要提交真实 App Secret3. 写明 `send_test_feishu_notification` 用法4. 写明自动化测试不请求真实飞书 |
| 验收标准 | 配置说明清楚,无真实密钥 |
| 验证命令 | 手动检查文档 |
| Codex 执行提示 | 请补充飞书配置说明,只写变量名和用途,不写真实值。 |
### FS-8-002 全量相关测试
| 项目 | 内容 |
| --- | --- |
| 任务类型 | 测试 / 回归 |
| 前置任务 | FS-8-001 |
| 涉及文件 | 无固定文件 |
| 目标 | 运行飞书新增测试和三个工作流关键回归 |
| 开发步骤 | 1. 运行 Django check2. 运行飞书新增测试3. 运行三个工作流关键测试4. 修复与本功能相关失败5. 记录无法处理的既有失败 |
| 验收标准 | 新增测试通过,关键回归通过 |
| 验证命令 | `python manage.py check`; `pytest tests/test_feishu_*.py tests/test_file_summary_workflow.py tests/test_regulatory_notification.py tests/test_application_form_fill_notification.py` |
| Codex 执行提示 | 请运行飞书新增测试和三个工作流关键回归,确保首期飞书接入不破坏既有功能。 |
### FS-8 阶段验证
```bash
python manage.py check
pytest tests/test_feishu_*.py tests/test_file_summary_workflow.py tests/test_regulatory_notification.py tests/test_application_form_fill_notification.py
```
---
## 十四、建议提交切分
| 提交 | 建议提交信息 | 包含内容 |
| --- | --- | --- |
| 1 | `feat: add feishu notification data models` | 模型、迁移、Admin、配置项 |
| 2 | `feat: add feishu api notification services` | token、接收人、消息构造、消息 API client |
| 3 | `feat: add workflow notification dispatcher` | dispatcher、记录判重、三流程 adapter |
| 4 | `feat: wire feishu notifications into workflows` | 三个工作流接入 |
| 5 | `feat: show feishu notification status` | 页面展示 |
| 6 | `feat: add feishu notification test command` | 真实发送测试命令 |
| 7 | `feat: add feishu question preview services` | 问答预留查询、解析、日志、模拟命令 |
| 8 | `docs: document feishu configuration` | 配置说明和回归修正 |
---
## 十五、风险与处理策略
| 风险 | 影响 | 策略 |
| --- | --- | --- |
| 飞书应用权限不足 | 消息 API 返回无权限 | 手动测试命令先验证;错误码入库展示 |
| open_id/user_id 不正确 | 个人账号收不到消息 | 接收人配置缺失或错误时记录 failed命令输出错误 |
| token 缓存过期处理不当 | 偶发发送失败 | token 失效时刷新并允许消息 API 同步重试一次 |
| 三流程状态差异 | 通知触发点不一致 | 用 adapter 隔离各流程摘要生成 |
| 页面展示影响既有模板 | 前端回归失败 | 使用小型通知状态区块,无记录时不改变主流程展示 |
| 问答预留过度设计 | 影响首期交付 | 只做规则解析和摘要查询,不接事件订阅、不接 LLM |

View File

@@ -0,0 +1,622 @@
# 第1章监管信息材料包生成开发计划
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 需求分析文档 | docs/1.需求分析/5.第1章监管信息材料包生成.md |
| 功能设计文档 | docs/2.功能设计/5.第1章监管信息材料包生成.md |
| 数据库设计文档 | docs/3.数据库设计/5.第1章监管信息材料包生成.md |
| 详细设计文档 | docs/4.详细设计/5.第1章监管信息材料包生成.md |
| 参考开发计划 | docs/5.开发计划/3.产品关键信息提取与申报文件自动填表.md |
| 功能名称 | 第1章监管信息材料包生成 |
| 工作流编码 | regulatory_info_package |
| 批次号规则 | RIP-YYYYMMDDHHMMSS-abcdef |
| 计划日期 | 2026-06-10 |
| 计划版本 | V1.0 |
---
## 一、开发计划目标
本开发计划面向 Codex 执行,目标是把 `regulatory_info_package` 独立工作流按可验证、可回滚、可阶段验收的方式落地。计划以现有自动填表工作流 `application_form_fill` 为主要参考,但保持独立模块、独立批次、独立产物、独立通知和独立前端卡片。
现状裁决:当前最新代码中尚未存在 `regulatory_info_package` 正式工作流,本计划按“新建正式材料包工作流”执行;不得把该功能并入或改造 `application_form_fill`
开发完成后用户可在对话中上传或指定产品说明书并通过“根据说明书生成第1章监管信息”触发工作流。系统基于 `docs/0.原始材料/第1章 监管信息` 样例模板生成 7 个监管信息文件,以 `第1章 监管信息(预生成版).zip` 作为首位下载入口,同时提供单文件和追溯 Excel 辅助下载。
---
## 二、已确认开发规则
| 规则 | 内容 |
| --- | --- |
| 工作流独立 | 新增 `workflow_type=regulatory_info_package`,不并入 `application_form_fill` |
| 模块独立 | 新增 `review_agent/regulatory_info_package/`,服务与自动填表平级 |
| 模型集中 | Django 模型继续放在 `review_agent/models.py` |
| 节点幂等 | RIP 节点必须基于 `workflow_type + workflow_batch_id + node_code` 做幂等创建或数据库唯一约束 |
| 单说明书输入 | 用户消息指定文件名优先,其次 active 附件,再兼容最近成功文件汇总 |
| 多候选处理 | 不做选择弹窗,通过对话反问用户确认说明书文件名 |
| 模板固定 | 固定处理第1章监管信息 7 个模板 |
| 模板字段化 | 优先把模板整理为 Agent/代码可识别的字段模板,使用内容控件 Tag 或稳定占位符;代码只填字段,不依赖手工改格式 |
| 抽取策略 | 规则抽取和 LLM 抽取并行LLM 最多重试 3 次,失败后规则结果继续 |
| 文档生成 | 工作流节点串行,`generate_docs` 节点内部每个文档独立线程处理 |
| `.doc` 策略 | CH1.9 能力驱动:探测到 Word COM/UNO 时优先原生 `.doc`,无原生能力时明确记录并允许 `.docx` 兜底 |
| zip 策略 | zip 只包含成功或兜底成功文件,失败文件不进入 zip |
| 高亮策略 | 缺失项 `/` 黄底LLM-only 黄底;冲突黄底红字 |
| 追溯策略 | 用户下载 ExcelJSON 只写后台 logs 目录 |
| 前端策略 | 只做最小接入,不单独建设新页面或独立样式体系 |
| TDD | 新行为先写失败测试,再实现 |
| Git 提交 | 每阶段验证通过后生成提交摘要;是否本地提交由用户确认 |
| 用户变更保护 | 不回滚、不覆盖用户已有未提交变更 |
---
## 三、规范依据与裁决
| 规范来源 | 命中内容 | 本计划裁决 |
| --- | --- | --- |
| GYRX 后端开发规范 | 接口响应、日志、增量规范 | 状态接口、下载权限、异常降级和日志留痕按现有 Django 模式实现 |
| GYRX 前端开发规范 | 样式复用、组件接入、下载图标建议 | 复用现有对话页和工作流卡片样式,必要时只补少量语义化样式 |
| 既有自动填表开发计划 | 阶段拆分、测试先行、每阶段验证 | 本计划沿用阶段结构和 Codex 执行提示粒度 |
| 第1章监管信息详细设计 | 独立模块、7 模板、doc 兜底、zip 首位 | 作为本计划最高优先级依据 |
未发现规范冲突。项目专项设计优先于通用规范。
---
## 四、总体验收标准
| 类别 | 完成标准 |
| --- | --- |
| 触发 | 固定提示词和 LLM 路由均可触发 `regulatory_info_package` |
| 输入选择 | 能按用户指定文件名、active 附件、最近文件汇总选择说明书;多候选可反问 |
| 批次 | 能创建 `RegulatoryInfoPackageBatch`,节点和事件可查询 |
| 模板 | 能加载并校验 7 个模板配置,模板复制只写批次目录 |
| 抽取 | 规则抽取可独立跑通LLM 失败不阻断主链路 |
| 合并 | missing、llm_only、conflict 均有可追溯结构和高亮决策 |
| docx 生成 | 6 个 `.docx` 文件能按模板生成并保留基本版式 |
| doc 处理 | CH1.9 优先 `.doc` 原生处理,失败时 `.docx` 兜底,状态可见 |
| ZIP | `第1章 监管信息(预生成版).zip` 排在助手回显首位,只包含成功/兜底成功文件 |
| 单文件 | 成功文件有辅助下载,失败文件显示原因且无下载链接 |
| 追溯 | 用户可下载 `traceability.xlsx`JSON 写入 `logs/` |
| 前端 | 对话快捷入口、工作流卡片、状态轮询和下载列表正常 |
| 权限 | 非批次所属用户不能下载 RIP 产物 |
| 回归 | `python manage.py check` 和相关 pytest 通过,既有文件汇总/自动填表/法规核查不回归 |
---
## 五、阶段总览
| 阶段 | 名称 | 目标 | 阶段验收 |
| --- | --- | --- | --- |
| RIP-0 | 准备与基线回归 | 创建开发分支,确认依赖和既有测试状态 | 基线命令结果已记录 |
| RIP-1 | 数据模型与导出扩展 | 新增三张模型,扩展 zip 下载能力 | migration、模型和下载权限测试通过 |
| RIP-2 | 模块骨架与模板配置 | 新建模块、schema、YAML 配置和存储服务 | 配置加载和路径安全测试通过 |
| RIP-3 | 触发与工作流骨架 | 接入路由、批次创建、节点流转和状态接口 | 可创建并运行空工作流 |
| RIP-4 | 输入选择与说明书解析 | 选择说明书,解析 docx 段落、章节和表格 | 输入选择和说明书解析测试通过 |
| RIP-5 | 字段抽取与合并 | 规则 + LLM 并行抽取、重试、合并和高亮决策 | 抽取、重试、冲突合并测试通过 |
| RIP-6 | DOCX 文档生成 | 实现 6 个 docx 模板生成、产品列表重建和高亮 | docx 生成和 XML 高亮测试通过 |
| RIP-7 | CH1.9 DOC 适配 | 实现 `.doc` 原生适配探测和 `.docx` 兜底 | doc 兜底、失败隔离测试通过 |
| RIP-8 | 追溯、ZIP 与下载权限 | 生成 Excel、logs JSON、ZIP 和导出记录 | ZIP 内容、追溯、权限测试通过 |
| RIP-9 | 摘要、通知与状态归并 | 生成助手摘要,写通知记录,落定批次状态 | partial_success 等状态测试通过 |
| RIP-10 | 前端接入与总体验收 | 接入快捷入口、卡片、状态轮询和下载展示 | 前端回归和全量后端测试通过 |
---
## 六、RIP-0 准备与基线回归
### RIP-0-001 创建开发分支并确认工作区
| 项 | 内容 |
| --- | --- |
| 目标 | 创建本功能开发分支,确认当前工作区已有变更 |
| 修改范围 | Git 分支,不修改业务代码 |
| 验收标准 | 分支名符合 `codex/` 前缀;记录已有未提交变更,不回滚用户变更 |
| Codex 执行提示 | 请创建 `codex/regulatory-info-package` 开发分支,运行 `git status --short`,确认设计文档和目录重排状态,不要回滚无关变更。 |
### RIP-0-002 确认依赖与基线测试
| 项 | 内容 |
| --- | --- |
| 目标 | 确认 Django、python-docx、openpyxl、PyYAML、可选 Word COM 环境状态 |
| 修改范围 | 不修改业务代码 |
| 验收标准 | `python manage.py check` 可执行;关键依赖可 import既有失败需记录 |
| Codex 执行提示 | 请运行 Django check 和关键回归测试,确认依赖可用。若发现既有失败,只记录并继续按计划隔离,不改无关代码。 |
### RIP-0 阶段验证
```bash
python manage.py check
pytest tests/test_file_summary_views.py -k download
```
---
## 七、RIP-1 数据模型与导出扩展
### RIP-1-001 新增监管信息材料包 ORM 模型
| 项 | 内容 |
| --- | --- |
| 目标 | 新增 `RegulatoryInfoPackageBatch``RegulatoryInfoPackageArtifact``RegulatoryInfoPackageNotificationRecord` |
| 修改范围 | `review_agent/models.py` |
| 验收标准 | 字段、枚举、索引、软删除、关联关系符合数据库设计 |
| Codex 执行提示 | 请按 `docs/3.数据库设计/5.第1章监管信息材料包生成.md` 新增三张模型,模型集中放在 `review_agent/models.py`,不要新增字段级数据库表。 |
### RIP-1-002 扩展导出类型和下载 MIME
| 项 | 内容 |
| --- | --- |
| 目标 | `ExportedSummaryFile.ExportType` 增加 `zip`,下载 MIME 支持 `.zip``.doc``.docx` |
| 修改范围 | `review_agent/models.py``review_agent/file_summary/views.py` |
| 验收标准 | zip 可下载doc/docx MIME 正确;原有导出不回归 |
| Codex 执行提示 | 请扩展 `ExportedSummaryFile` 导出类型,并在下载接口按 workflow_type 和文件后缀处理权限与 content type。 |
### RIP-1-003 生成迁移并补模型测试
| 项 | 内容 |
| --- | --- |
| 目标 | 生成数据库迁移并覆盖基础模型行为 |
| 修改范围 | `review_agent/migrations/``tests/` |
| 验收标准 | migration 可应用模型测试覆盖批次号、状态、artifact、通知、zip export type |
| Codex 执行提示 | 请生成迁移并新增 `tests/test_regulatory_info_package_models.py`,优先覆盖模型字段默认值、导出类型,以及 `WorkflowNodeRun` 在 RIP 批次下的幂等/唯一节点创建。 |
### RIP-1 阶段验证
```bash
python manage.py check
pytest tests/test_regulatory_info_package_models.py tests/test_file_summary_views.py -k download
```
---
## 八、RIP-2 模块骨架与模板配置
### RIP-2-001 创建 regulatory_info_package 模块骨架
| 项 | 内容 |
| --- | --- |
| 目标 | 新增独立模块目录和基础文件 |
| 修改范围 | `review_agent/regulatory_info_package/` |
| 验收标准 | 模块可 import不影响现有 `application_form_fill` |
| Codex 执行提示 | 请创建详细设计中的模块骨架先放常量、schema、storage、events、workflow 空实现和 service 包,不提前写复杂业务。 |
### RIP-2-002 编写模板配置 YAML
| 项 | 内容 |
| --- | --- |
| 目标 | 配置 7 个样例模板、输出文件名、策略、字段 Tag/占位符映射和 `.doc` 标记 |
| 修改范围 | `review_agent/regulatory_info_package/templates/regulatory_info_package_templates_v1.yaml` |
| 验收标准 | 7 个模板完整zip 名称为 `第1章 监管信息(预生成版).zip`;字段映射优先使用内容控件 Tag 或稳定占位符 |
| Codex 执行提示 | 请按详细设计录入模板配置source_dir 指向样例目录,字段 targets 优先写 content_control_tag 或 placeholderCH1.9 声明 `prefer_legacy_doc_native: true` 且允许 docx fallback。 |
### RIP-2-003 实现配置加载、模板仓库和存储目录
| 项 | 内容 |
| --- | --- |
| 目标 | 实现 YAML 加载校验、模板复制、批次目录创建、路径安全检查 |
| 修改范围 | `template_config.py``template_repository.py``storage.py` |
| 验收标准 | 配置错误可返回清晰错误;模板只复制到批次目录;不写原始材料目录;能审计模板是否包含所需 Tag/占位符 |
| Codex 执行提示 | 请实现配置加载、模板复制和模板字段审计服务,所有路径必须校验位于批次工作目录内,原始模板目录只读。 |
### RIP-2-004 模板字段化整理与审计
| 项 | 内容 |
| --- | --- |
| 目标 | 将样例模板升级为代码友好的字段模板,不手工改生成文件格式 |
| 修改范围 | `docs/0.原始材料/第1章 监管信息` 的模板副本或 `review_agent/regulatory_info_package/templates/field_manifest.yaml` |
| 验收标准 | CH1.4 关键字段、复选框、声明类产品名/申请人位置有稳定 Tag 或占位符;审计缺失字段时测试失败 |
| Codex 执行提示 | 请优先使用 Word 内容控件 Tag若暂不具备内容控件编辑能力则使用不会影响版式的稳定占位符并在配置中记录字段与目标位置。 |
### RIP-2 阶段验证
```bash
python manage.py check
pytest tests/test_regulatory_info_package_template_config.py
```
---
## 九、RIP-3 触发与工作流骨架
### RIP-3-001 扩展意图路由
| 项 | 内容 |
| --- | --- |
| 目标 | 新增 `regulatory_info_package` action支持固定关键词和 LLM 路由 |
| 修改范围 | `review_agent/skill_router.py` |
| 验收标准 | 固定提示词直接命中LLM action 列表包含本工作流;原路由不回归 |
| Codex 执行提示 | 请扩展意图路由,新增 `starts_regulatory_info_package` 标记,避免破坏 file_summary、regulatory_review 和 application_form_fill。 |
### RIP-3-002 实现批次创建和节点初始化
| 项 | 内容 |
| --- | --- |
| 目标 | 创建批次、生成节点、记录事件 |
| 修改范围 | `workflow.py``events.py``constants.py` |
| 验收标准 | 可创建 `RIP-...` 批次;节点按定义初始化;事件可查询 |
| Codex 执行提示 | 请实现批次创建和节点初始化workflow_type 必须写 `regulatory_info_package`。 |
### RIP-3-003 实现执行器骨架和状态接口
| 项 | 内容 |
| --- | --- |
| 目标 | 工作流节点可完整流转status 接口可返回批次、节点、导出和风险信息 |
| 修改范围 | `workflow.py``views.py``urls.py` 或现有 URL 注册文件 |
| 验收标准 | 空工作流可从 pending 到 completed状态接口校验用户权限 |
| Codex 执行提示 | 请先实现可运行的空工作流骨架,业务节点可以临时 no-op但状态流转和权限必须真实。 |
### RIP-3-004 接入对话启动逻辑
| 项 | 内容 |
| --- | --- |
| 目标 | `stream_message` 能启动本工作流或返回说明书反问 |
| 修改范围 | `review_agent/services.py` |
| 验收标准 | 触发后发送 `workflow_started`;无输入或多候选时不误启动 |
| Codex 执行提示 | 请在 `stream_message` 增加 regulatory_info_package 分支,先调用输入选择服务,再决定启动、提示上传或反问。 |
### RIP-3 阶段验证
```bash
python manage.py check
pytest tests/test_regulatory_info_package_trigger.py tests/test_regulatory_info_package_workflow.py tests/test_regulatory_info_package_views.py
```
---
## 十、RIP-4 输入选择与说明书解析
### RIP-4-001 实现说明书输入选择
| 项 | 内容 |
| --- | --- |
| 目标 | 按用户消息、active 附件、最近汇总批次选择说明书 |
| 修改范围 | `services/input_select.py` |
| 验收标准 | 文件名模糊匹配、唯一 docx、多个说明书、无说明书均有明确结果 |
| Codex 执行提示 | 请实现 `select_instruction_input`,多候选返回 waiting_user 语义,由对话反问用户确认具体文件名。 |
### RIP-4-002 实现说明书 docx 解析
| 项 | 内容 |
| --- | --- |
| 目标 | 读取说明书段落、章节、表格、组成成分表和 front_text |
| 修改范围 | `services/instruction_extract.py` |
| 验收标准 | 能解析 `目标产品说明书.docx` 的产品名称、章节和主要表格结构 |
| Codex 执行提示 | 请使用结构化 Word 解析能力,不用脆弱的纯字符串拼接;解析结果写入可序列化 schema。 |
### RIP-4-003 写入说明书抽取日志产物
| 项 | 内容 |
| --- | --- |
| 目标 | 保存 `logs/instruction_extract.json` 并创建 artifact |
| 修改范围 | `workflow.py``storage.py``instruction_extract.py` |
| 验收标准 | JSON 只在后台 logs 目录,不进入用户下载列表 |
| Codex 执行提示 | 请在 text_extract 节点保存说明书抽取 JSONartifact 可记录,但不要创建 ExportedSummaryFile。 |
### RIP-4 阶段验证
```bash
pytest tests/test_regulatory_info_package_input_select.py tests/test_regulatory_info_package_instruction_extract.py
```
---
## 十一、RIP-5 字段抽取与合并
### RIP-5-001 实现规则字段抽取
| 项 | 内容 |
| --- | --- |
| 目标 | 从说明书章节和表格中抽取产品名称、包装规格、预期用途、组成、储存条件、样本类型、适用仪器、标准号等 |
| 修改范围 | `services/field_extract.py` |
| 验收标准 | 不依赖 LLM 时可抽取关键字段并支撑 demo |
| Codex 执行提示 | 请优先实现规则抽取,抽取结果包含 value、evidence、confidence 和 source。 |
### RIP-5-002 实现 LLM 抽取封装和三次重试
| 项 | 内容 |
| --- | --- |
| 目标 | LLM 结构化抽取,失败最多重试 3 次,失败后不阻断 |
| 修改范围 | `services/field_extract.py``prompts/field_extract.md` |
| 验收标准 | 0s/1s/2s 重试;解析失败可记录错误;规则结果继续 |
| Codex 执行提示 | 请封装 LLM 调用为可 mock 的函数,测试中不要真实调用外部模型。 |
### RIP-5-003 实现规则与 LLM 并行抽取
| 项 | 内容 |
| --- | --- |
| 目标 | 使用线程并行执行规则抽取和 LLM 抽取 |
| 修改范围 | `services/field_extract.py` |
| 验收标准 | 任一分支失败不影响另一分支结果;输出 `field_extract_result.json` |
| Codex 执行提示 | 请使用 `ThreadPoolExecutor(max_workers=2)`,不要在子线程直接写数据库。 |
### RIP-5-004 实现字段合并和高亮决策
| 项 | 内容 |
| --- | --- |
| 目标 | 输出 missing、llm_only、conflict 和最终写入值 |
| 修改范围 | `services/field_merge.py` |
| 验收标准 | 全缺失写 `/` 黄底LLM-only 黄底;冲突黄底红字;合并结果可追溯 |
| Codex 执行提示 | 请实现 `MergedField` 结构,合并结果写 `logs/merged_fields.json`,并同步批次摘要字段。 |
### RIP-5 阶段验证
```bash
pytest tests/test_regulatory_info_package_field_extract.py tests/test_regulatory_info_package_field_merge.py
```
---
## 十二、RIP-6 DOCX 文档生成
### RIP-6-001 实现 DocxDocumentAdapter
| 项 | 内容 |
| --- | --- |
| 目标 | 支持段落/表格替换、表格单元格填充、黄色底色、红字 |
| 修改范围 | `services/document_writer.py``services/docx_document.py` |
| 验收标准 | 可处理 run 拆分;测试可检查 docx XML 高亮和红字 |
| Codex 执行提示 | 请优先支持本模板需要的替换和表格填充场景,复杂通用 Word 引擎不要过度设计。 |
### RIP-6-002 实现 6 个 DOCX 文件生成策略
| 项 | 内容 |
| --- | --- |
| 目标 | 生成 CH1.2、CH1.4、CH1.5、CH1.11.1、CH1.11.5、CH1.11.6 |
| 修改范围 | `services/package_generate.py``services/standard_candidates.py` |
| 验收标准 | 6 个 docx 文件可生成;缺失/LLM-only/冲突样式正确 |
| Codex 执行提示 | 请先完成 docx 主链路。CH1.5 产品列表必须转成样例表头:包装规格、货号、组成、组分、主要组成成分、规格/数量,其中货号 `/` 黄底。 |
### RIP-6-003 实现 generate_docs 内部并发
| 项 | 内容 |
| --- | --- |
| 目标 | 每个文档独立线程生成,主线程统一写 artifact/export |
| 修改范围 | `services/package_generate.py``workflow.py` |
| 验收标准 | 单个文件失败不影响其他文件;返回 `GeneratedFileResult` 列表 |
| Codex 执行提示 | 请使用独立模板副本,子线程不要写数据库;所有异常转成文件级 failed 状态。 |
### RIP-6 阶段验证
```bash
pytest tests/test_regulatory_info_package_docx_writer.py tests/test_regulatory_info_package_package_generate.py
```
---
## 十三、RIP-7 CH1.9 DOC 适配
### RIP-7-001 实现 LegacyDocDocumentAdapter 能力探测
| 项 | 内容 |
| --- | --- |
| 目标 | 探测 Word COM、LibreOffice UNO 或可用兜底能力 |
| 修改范围 | `services/legacy_doc_document.py` |
| 验收标准 | 当前环境无原生能力时返回清晰 capability不崩溃测试不要求本机必须安装 Word 或 LibreOffice |
| Codex 执行提示 | 请先实现能力探测和接口骨架Windows Word COM/LibreOffice UNO 可作为原生能力;不可用时明确进入 docx 兜底。 |
### RIP-7-002 实现 CH1.9 原生写入与 docx 兜底
| 项 | 内容 |
| --- | --- |
| 目标 | CH1.9 优先 `.doc` 输出,失败时生成同语义 `.docx` |
| 修改范围 | `legacy_doc_document.py``package_generate.py` |
| 验收标准 | 有原生能力时原生成功状态 success无原生能力或原生失败但兜底成功时状态 fallback_success两者失败不进入 zip |
| Codex 执行提示 | 请把能力探测、原生失败和兜底失败都写入 `adapter_summary``risk_notes`,不要静默转换。 |
### RIP-7-003 补充 doc 适配器测试
| 项 | 内容 |
| --- | --- |
| 目标 | 覆盖 capability、兜底成功、失败隔离 |
| 修改范围 | `tests/test_regulatory_info_package_legacy_doc.py` |
| 验收标准 | 测试不依赖本机必须安装 Word用 mock 覆盖原生成功/失败 |
| Codex 执行提示 | 请用 mock 模拟 Word COM 可用和不可用场景,保证 CI 或本地无 Word 时测试仍稳定。 |
### RIP-7 阶段验证
```bash
pytest tests/test_regulatory_info_package_legacy_doc.py tests/test_regulatory_info_package_package_generate.py
```
---
## 十四、RIP-8 追溯、ZIP 与下载权限
### RIP-8-001 实现追溯 Excel 和后台 JSON
| 项 | 内容 |
| --- | --- |
| 目标 | 生成 `exports/traceability.xlsx``logs/traceability.json` |
| 修改范围 | `services/traceability_export.py` |
| 验收标准 | Excel 可下载JSON 不进入用户下载列表 |
| Codex 执行提示 | 请用 openpyxl 生成 Excel字段包含 target_file、target_field、final_value、extraction_source、evidence、highlight_reason、needs_review。 |
### RIP-8-002 实现 zip 打包
| 项 | 内容 |
| --- | --- |
| 目标 | 生成 `第1章 监管信息(预生成版).zip` |
| 修改范围 | `services/zip_export.py` |
| 验收标准 | zip 只包含 success/fallback_success 文件;失败文件不入包 |
| Codex 执行提示 | 请用 Python 标准库 `zipfile` 打包zip 中保留最终输出文件名。CH1.9 兜底成功时放入 `.docx` 文件。 |
### RIP-8-003 创建导出记录和下载权限
| 项 | 内容 |
| --- | --- |
| 目标 | zip、单文件、Excel 均写 `ExportedSummaryFile`;下载接口校验用户权限 |
| 修改范围 | `file_summary/views.py``storage.py``zip_export.py` |
| 验收标准 | 非批次用户不能下载zip 在 exports 返回顺序中排首位 |
| Codex 执行提示 | 请按 `workflow_type=regulatory_info_package` 反查批次所属 conversation/user软删除批次不可下载。 |
### RIP-8 阶段验证
```bash
pytest tests/test_regulatory_info_package_traceability.py tests/test_regulatory_info_package_zip.py tests/test_regulatory_info_package_views.py
```
---
## 十五、RIP-9 摘要、通知与状态归并
### RIP-9-001 实现助手 Markdown 摘要
| 项 | 内容 |
| --- | --- |
| 目标 | 完成后返回 zip 首位、单文件列表、失败原因、待确认摘要 |
| 修改范围 | `services/summary.py``workflow.py` |
| 验收标准 | zip 链接在回复首位;失败文件显示原因且无下载;待确认数量准确 |
| Codex 执行提示 | 请严格按详细设计生成助手摘要partial_success 时也要展示可下载 zip 和失败文件原因。 |
### RIP-9-002 实现通知记录和统一通知接入
| 项 | 内容 |
| --- | --- |
| 目标 | 写 `RegulatoryInfoPackageNotificationRecord`,调用统一通知服务 |
| 修改范围 | `services/notifier.py``workflow.py` |
| 验收标准 | 通知失败不阻断下载;失败写 `risk_notes` |
| Codex 执行提示 | 请复用已有通知模式,先保证本地测试可 mock不要让外部通知失败影响批次主状态。 |
### RIP-9-003 完成状态归并
| 项 | 内容 |
| --- | --- |
| 目标 | 根据生成结果、zip、追溯、通知落定 success/partial_success/failed/waiting_user |
| 修改范围 | `workflow.py` |
| 验收标准 | 7 文件成功为 success部分文件失败但有 zip 为 partial_success全部失败为 failed |
| Codex 执行提示 | 请把状态归并集中在一个函数,测试覆盖 docx 兜底、zip 失败、通知失败、产品名缺失。 |
### RIP-9 阶段验证
```bash
pytest tests/test_regulatory_info_package_workflow.py tests/test_regulatory_info_package_notification.py
```
---
## 十六、RIP-10 前端接入与总体验收
### RIP-10-001 增加对话快捷入口
| 项 | 内容 |
| --- | --- |
| 目标 | 对话框底部增加“第1章监管信息”快捷提示 |
| 修改范围 | `templates/home.html` |
| 验收标准 | 点击后填入或发送 `根据说明书生成第1章监管信息` |
| Codex 执行提示 | 请复用现有 tool-chip 样式,不单独创建新前端样式文件,除非现有结构无法展示。 |
### RIP-10-002 工作流卡片和状态轮询支持
| 项 | 内容 |
| --- | --- |
| 目标 | 前端识别 `regulatory_info_package`,使用新 status URL 轮询 |
| 修改范围 | `static/js/app.js``templates/home.html` |
| 验收标准 | 卡片能展示节点、状态、风险和导出列表;终态识别 success/partial_success/failed/waiting_user |
| Codex 执行提示 | 请在现有工作流卡片逻辑中增量接入,不复制一套新卡片实现。 |
### RIP-10-003 下载展示和失败文件展示
| 项 | 内容 |
| --- | --- |
| 目标 | zip 首位展示,单文件辅助下载,失败文件展示原因 |
| 修改范围 | `static/js/app.js` |
| 验收标准 | exports 返回顺序被保留失败文件无下载按钮traceability.xlsx 可下载 |
| Codex 执行提示 | 请以后端 exports 顺序为准,不新增 `is_primary` 字段zip 已由后端排首位。 |
### RIP-10-004 总体验收与回归
| 项 | 内容 |
| --- | --- |
| 目标 | 全链路验证和回归保护 |
| 修改范围 | 测试、必要的 bug fix |
| 验收标准 | Django check、RIP 测试、关键既有测试通过;能用样例说明书生成材料包 |
| Codex 执行提示 | 请用 `docs/0.原始材料/目标产品说明书.docx` 做端到端验证,确认 zip、单文件、Excel、logs 和摘要均符合设计。 |
### RIP-10 阶段验证
```bash
python manage.py check
pytest tests/test_regulatory_info_package_frontend.py
pytest tests/test_regulatory_info_package_models.py tests/test_regulatory_info_package_trigger.py tests/test_regulatory_info_package_input_select.py tests/test_regulatory_info_package_template_config.py tests/test_regulatory_info_package_instruction_extract.py tests/test_regulatory_info_package_field_extract.py tests/test_regulatory_info_package_field_merge.py tests/test_regulatory_info_package_docx_writer.py tests/test_regulatory_info_package_legacy_doc.py tests/test_regulatory_info_package_package_generate.py tests/test_regulatory_info_package_traceability.py tests/test_regulatory_info_package_zip.py tests/test_regulatory_info_package_workflow.py tests/test_regulatory_info_package_views.py tests/test_regulatory_info_package_notification.py
```
---
## 十七、测试分层要求
| 测试层 | 覆盖内容 | 建议文件 |
| --- | --- | --- |
| 模型测试 | 批次、产物、通知、zip 导出类型 | `tests/test_regulatory_info_package_models.py` |
| 路由测试 | 固定关键词、LLM action、对话启动分支 | `tests/test_regulatory_info_package_trigger.py` |
| 输入测试 | 文件名匹配、active 附件、多候选反问 | `tests/test_regulatory_info_package_input_select.py` |
| 配置测试 | YAML 加载、模板缺失、code 唯一 | `tests/test_regulatory_info_package_template_config.py` |
| 解析测试 | 说明书章节、表格、组成成分表 | `tests/test_regulatory_info_package_instruction_extract.py` |
| 抽取测试 | 规则抽取、LLM 重试、失败降级 | `tests/test_regulatory_info_package_field_extract.py` |
| 合并测试 | missing、llm_only、conflict | `tests/test_regulatory_info_package_field_merge.py` |
| 文档测试 | docx 替换、表格、高亮、红字 | `tests/test_regulatory_info_package_docx_writer.py` |
| doc 测试 | adapter 探测、docx 兜底、失败状态 | `tests/test_regulatory_info_package_legacy_doc.py` |
| 生成测试 | 7 文件并发生成、异常隔离 | `tests/test_regulatory_info_package_package_generate.py` |
| 追溯测试 | Excel 下载、logs JSON | `tests/test_regulatory_info_package_traceability.py` |
| zip 测试 | 只打包 success/fallback_success | `tests/test_regulatory_info_package_zip.py` |
| 工作流测试 | 节点流转、状态归并、partial_success | `tests/test_regulatory_info_package_workflow.py` |
| 接口测试 | start/status/download 权限 | `tests/test_regulatory_info_package_views.py` |
| 通知测试 | 通知记录、通知失败降级 | `tests/test_regulatory_info_package_notification.py` |
| 前端测试 | chip、卡片、状态 URL、下载展示 | `tests/test_regulatory_info_package_frontend.py` |
---
## 十八、Codex 自动化执行规则
| 规则 | 内容 |
| --- | --- |
| 顺序执行 | 必须从 RIP-0 到 RIP-10 顺序执行,不得跳阶段 |
| 阶段聚焦 | 当前阶段失败时先修复当前阶段,不继续后续阶段 |
| TDD | 新行为先写失败测试,再实现 |
| 小步修改 | 每次只修改当前阶段相关文件,避免顺手重构 |
| 用户变更保护 | 不得回滚或覆盖用户已有未提交变更 |
| 过程日志 | 每阶段记录关键命令结果和既有失败 |
| 阶段验证 | 每阶段完成后运行对应验证命令 |
| 阶段提交 | 每阶段验证通过后生成提交摘要;是否执行 `git commit` 由用户确认 |
| 回归保护 | 文件汇总、法规核查、自动填表现有测试不得回归 |
| doc 风险隔离 | `.doc` 原生能力不可用或原生处理失败不得阻断其他 6 个 docx 文件生成 |
| 外部依赖隔离 | LLM、通知、Word COM 均需可 mock测试不依赖真实外部服务 |
| 下载安全 | 所有导出下载必须通过所属用户权限校验 |
---
## 十九、推荐目标模式提示词
后续可直接对 Codex 输入:
```text
请按 docs/5.开发计划/5.第1章监管信息材料包生成.md 执行开发。
执行要求:
1. 严格按 RIP-0 到 RIP-10 顺序推进,不跳阶段。
2. 每阶段先读对应需求、功能、数据库、详细设计文档。
3. 每阶段先写或补充测试,再实现代码。
4. 每阶段只修改当前阶段相关文件,不做无关重构。
5. 不回滚、不覆盖用户已有未提交变更。
6. LLM、通知、Word COM 等外部能力必须可 mock。
7. 每阶段完成后运行该阶段验证命令。
8. 验证通过后生成提交摘要,是否本地提交等待用户确认。
9. 最后使用 docs/0.原始材料/目标产品说明书.docx 做端到端验收。
```
---
## 二十、待执行前检查清单
| 检查项 | 状态 |
| --- | --- |
| 需求分析、功能设计、数据库设计、详细设计均已存在 | 待执行时确认 |
| 当前分支是否适合创建开发分支 | 待执行时确认 |
| 是否存在用户未提交变更 | 待执行时确认 |
| `python-docx``openpyxl``PyYAML` 是否可用 | 待执行时确认 |
| Word COM 或 LibreOffice UNO 是否可用 | 待执行时确认,非阻塞 |
| 目标说明书 `docs/0.原始材料/目标产品说明书.docx` 是否存在 | 待执行时确认 |
| 样例模板目录 `docs/0.原始材料/第1章 监管信息` 是否完整 | 待执行时确认 |
| 现有文件汇总、法规核查、自动填表测试是否通过 | 待执行时确认 |

View File

@@ -0,0 +1,53 @@
# 第二阶段暂缓事项待办表
## 一、待办原则
以下事项不进入第二阶段第一批或第二批落地范围。完成 Demo 主任务后,再根据展示效果和剩余时间决定是否进入第三阶段。
---
## 二、第三阶段第一批建议事项
| 编号 | 待办项 | 来源 | 建议优先级 | 说明 |
| --- | --- | --- | --- | --- |
| TODO-3-001 | 真实飞书 CLI/API 接入 | 第二阶段通知能力 | P0 | 替换第二阶段 mock 通知,支持真实发送 |
| TODO-3-002 | 用户与飞书账号映射 | 第二阶段通知能力 | P0 | 维护 Django User 到飞书 open_id、手机号或邮箱的映射 |
| TODO-3-003 | 飞书通知模板和失败重试完善 | 第二阶段通知能力 | P0 | 支持风险摘要、报告链接、重试、失败告警 |
| TODO-3-004 | 飞书通知权限和脱敏策略 | 第二阶段通知能力 | P1 | 通知中不暴露完整敏感文件内容 |
---
## 三、规则管理后续事项
| 编号 | 待办项 | 来源 | 建议优先级 | 说明 |
| --- | --- | --- | --- | --- |
| TODO-RULE-001 | 规则管理前端 | YAML + DB 规则版本 | P1 | 展示 YAML 与数据库 hash 差异,支持人工确认导入 |
| TODO-RULE-002 | 规则导入审批流 | 合规追溯 | P1 | 规则版本变更需要审批和留痕 |
| TODO-RULE-003 | 规则/RAG 状态管理页 | RAG 运维 | P1 | 展示规则版本、YAML hash、Chroma 索引版本、索引状态和重建提示 |
| TODO-RULE-004 | RAG 索引重建前端入口 | RAG 运维 | P1 | 前端触发或提示重建法规 RAG 索引 |
| TODO-RULE-005 | 官网法规定期更新 | 原始需求法规来源 | P2 | 后续从 NMPA/CMDE 官网定期抓取或人工导入 |
---
## 四、原始需求 3 后续事项
| 编号 | 待办项 | 来源 | 建议优先级 | 说明 |
| --- | --- | --- | --- | --- |
| TODO-FILL-001 | 字段级数据库表 | 第三批自动填表数据库设计 | P1 | 后续新增 `ApplicationFormFillField`,支持字段级查询、人工修改、审计和统计 |
| TODO-FILL-002 | PDF 转换与版式 QA | 第三批自动填表详细设计 | P1 | 使用 LibreOffice/soffice 将填好的 Word 转 PDF并增加页数非 0、逐页截图或版式差异检查 |
| TODO-FILL-003 | `.doc` 模板预转换管理 | 第三批自动填表模板处理 | P1 | 将变更注册(备案)文件和安全和性能基本原则清单预转换为 `.docx` 工作模板,并人工确认版式 |
| TODO-FILL-004 | 安全和性能基本原则清单完整条目拆解 | 第三批自动填表模板配置 | P1 | 拆解清单条目编号、原则内容、适用性栏、证据栏和证明文件位置栏,写入 YAML 配置 |
| TODO-FILL-005 | 填写前后差异报告 | 自动填写风控 | P1 | 输出写入前后 diff供人工复核 |
| TODO-FILL-006 | 自动填写审批确认 | 自动填写风控 | P1 | 文件写操作前支持人工确认或二次审批 |
---
## 五、其他增强事项
| 编号 | 待办项 | 来源 | 建议优先级 | 说明 |
| --- | --- | --- | --- | --- |
| TODO-EXT-001 | 无汇总批次时自动串联文件汇总 | 第二阶段启动方式 | P2 | 当前口径为提示用户先自动汇总,暂不自动串联 |
| TODO-EXT-002 | 文件夹上传增强 | 第一阶段边界 | P2 | 浏览器 `webkitdirectory` 或目录上传能力 |
| TODO-EXT-003 | Office 精确分页 | 第一阶段边界 | P2 | 引入 LibreOffice headless 转 PDF 后统计页数 |
| TODO-EXT-004 | OCR 文本抽取 | 章节/一致性核查增强 | P2 | 支持扫描件和图片型 PDF |
| TODO-EXT-005 | 独立 Chroma Server 部署 | RAG 运维增强 | P2 | 当前第二阶段使用本地持久化 ChromaDB后续可演进为独立服务 |

View File

@@ -0,0 +1,311 @@
# 架构搭建思路汇报稿(基于 Demo 版)
## 一、设计路径:先锁规格,再实现代码
各位老师好,我本次 Demo 搭建的是一个面向体外诊断试剂注册资料准备与审核的智能体原型。
这次开发没有直接从代码开始而是采用“文档先行、规格锁定、再实现代码”的路径。原因是注册资料审核不是一个简单问答场景它涉及文件解析、法规规则、RAG 依据、工作流状态、导出文件、人工确认和整改闭环。如果一开始就写代码,很容易出现功能能跑但边界不清、结果不可追溯、后续难维护的问题。
所以整体设计路径分为四步:
```text
需求拆解
-> 生成需求分析、功能设计、详细设计、数据库设计和开发计划
-> 用文档锁定实现规格
-> 按规格实现 Django 代码、工作流、前端页面和测试
```
当前仓库中可以看到完整的规格文档链路:
| 阶段 | 产物 | 作用 |
| --- | --- | --- |
| 需求分析 | `docs/1.需求分析` | 明确业务目标、用户动作、输入输出和异常场景 |
| 功能设计 | `docs/2.功能设计` | 把需求拆成文件汇总、法规核查、自动填表、飞书通知等模块 |
| 详细设计 | `docs/3.详细设计` | 锁定工作流节点、字段结构、状态流转和服务边界 |
| 数据库设计 | `docs/4.数据库设计` | 锁定批次、附件、节点、风险项、导出文件等模型 |
| 开发计划 | `docs/5.开发计划` | 将实现拆成可验证的开发任务和前端线框图 |
因此,这个 Demo 的核心不是“让大模型临时回答一个问题”,而是先用文档定义清楚系统应该如何工作,再把这些规格落实到代码、数据库、前端和测试中。最终形成的是一个可追溯、可复核、可继续扩展的审核工作台。
## 二、系统定位和 Demo 目标
这个 Demo 的目标不是简单做文件上传、文件解析或法规问答,而是把注册资料审核中几个高频、耗时、容易出错的环节串成一个智能工作流,包括:
```text
资料上传
-> 文件目录和页数汇总
-> NMPA 法规完整性核查
-> 法规依据 RAG 检索
-> 产品关键信息抽取
-> 一致性核查和风险预警
-> 申报文件自动填表
-> 报告导出和整改复核
```
从产品形态上看,它更像是一个“注册资料审核工作台”。用户上传一批申报资料后,系统先把资料包结构化,再按法规规则做核查,然后输出风险清单、整改建议、证据来源和导出文件。后续还可以继续复用抽取到的产品信息,自动填入申报模板。
## 三、技术栈和总体架构
本 Demo 采用轻量、可本地运行、便于测试和可解释的技术栈。
| 层级 | 技术/工具 | 作用 |
| --- | --- | --- |
| Web 框架 | Django | 路由、视图、模板、认证、ORM 和后台能力 |
| 数据库 | SQLite / Django ORM | Demo 阶段保存会话、附件、批次、节点、风险项和导出文件 |
| 前端 | Django Template + 原生 JS + CSS | 实现首页工作台、审核智能体、知识库管理、附件管理和流式对话 |
| 文件解析 | `pypdf``python-docx``python-pptx``openpyxl``xlrd``py7zr``zipfile` | 解析 PDF、Word、PPT、Excel、压缩包和旧 Office 文件 |
| 规则配置 | YAML | 维护 NMPA 体外诊断试剂注册资料核查规则 |
| RAG | ChromaDB + embedding provider | 构建法规材料向量索引,检索法规依据片段 |
| LLM | SiliconFlow / 可配置大模型接口 | 做意图路由、低置信度抽取、自然语言总结和辅助复核 |
| 流式交互 | SSE | 将工作流启动、节点进度和模型回复实时推给前端 |
| 自动化验证 | pytest + Django test client | 验证路由、页面、模型、工作流和导出结果 |
整体架构可以概括为:
```text
用户界面
-> Django 视图层
-> 对话服务和 Skill 路由器
-> 文件汇总 / 法规核查 / 自动填表工作流
-> ORM 状态记录和导出文件
-> RAG/LLM/规则服务
-> 前端工作流卡片和报告下载
```
这里的关键设计原则是规则判断要稳定RAG 负责补证据LLM 做辅助,不把高风险合规结论完全交给大模型自由发挥。
## 四、对话流程:先识别意图,再决定 RAG 或工作流
审核智能体页面不是单纯把用户输入直接发给大模型,而是有一层对话编排流程。
一次用户消息进入系统后,大致会经历以下步骤:
```text
用户输入
-> 保存用户消息
-> Skill Router 判断意图
-> 根据意图选择普通问答、附件读取或工作流
-> 必要时先检查附件和前置批次
-> 启动对应工作流或执行 RAG 问答
-> 保存助手回复和工作流事件
-> 前端通过 SSE 展示增量内容和节点状态
```
当前路由动作包括:
| action | 场景 | 后续动作 |
| --- | --- | --- |
| `normal_chat` | 普通法规问答或项目问答 | 先检索知识库,再把 RAG 片段放入大模型上下文 |
| `attachment_reader` | 用户要求阅读、提取、总结上传附件 | 调用附件读取 Skill返回文件内容摘要 |
| `file_summary` | 用户要求汇总文件目录、页数、清单 | 启动文件汇总工作流 |
| `regulatory_review` | 用户要求法规核查、完整性核查、风险预警、整改建议 | 必要时先生成文件汇总批次,再启动法规核查工作流 |
| `application_form_fill` | 用户要求申报文件填表、模板填充、安全和性能清单 | 必要时先生成文件汇总批次,再启动自动填表工作流 |
也就是说,普通问题是“先 RAG再回答”工作流问题是“先路由再检查前置条件再启动工作流”。例如用户问“注册检验报告要求是什么”系统会走 RAG 问答;用户说“请对当前资料做法规核查”,系统会进入法规核查工作流。
## 五、Skill 调用方式:路由器统一调度工具能力
Demo 中的 Skill 不是一个单独页面,而是对话服务后面的工具调用机制。用户不需要手动选择复杂功能,系统会根据用户话语和当前附件状态判断是否调用某个 Skill 或工作流。
当前实现中,`review_agent/skill_router.py` 负责意图路由。它采用两层判断:
```text
确定性规则预判
-> LLM 路由判断
-> 规则兜底
```
第一层是确定性规则。例如用户输入中包含“法规核查”“NMPA 核查”“风险预警”“自动填表”“申报模板”等明确关键词,系统可以直接判断要启动对应工作流。这样可以避免每次都依赖大模型判断。
第二层是 LLM 路由。系统会把用户消息和当前 active 附件列表发给路由模型,让模型只输出结构化 JSON
```json
{
"action": "regulatory_review",
"confidence": 0.9,
"reason": "用户要求对当前注册资料进行法规完整性核查"
}
```
第三层是规则兜底。如果 LLM 不可用、配置缺失或返回异常,系统会退回关键词和附件状态判断,保证 Demo 在本地环境也能稳定运行。
这个设计的好处是:用户体验上像是在和一个智能体对话,技术实现上则是由路由器把对话分发到不同工具、不同工作流和不同数据服务。
## 六、RAG 方式:法规依据和用户知识库共同参与
RAG 在 Demo 中有两类来源:
| 来源 | 说明 |
| --- | --- |
| 内置法规材料 | 来自 `docs/0.原始材料` 和 NMPA 相关法规文件,用于法规依据检索 |
| 用户管理知识库 | 由用户在“知识库管理”页面上传,可作为当前账号所有对话的补充知识 |
法规材料会被切分为文本块,写入 ChromaDB 向量库。每个 chunk 保留来源文件、chunk 编号、文本片段和元数据。embedding 支持真实语义 embedding也支持 deterministic/local embedding后者主要用于测试和 dry run。
RAG 在系统中的定位有两种:
### 1. 普通问答中的 RAG
如果用户提出普通问题,系统会先检索知识库,把命中的法规片段或用户知识库片段拼入上下文,再调用大模型回答。这样回答不会只依赖模型记忆,而是带有本地法规材料和用户资料依据。
```text
用户问题
-> 知识库检索
-> 过滤和排序相关片段
-> 组装为知识上下文
-> 调用 LLM 生成回答
```
### 2. 工作流中的 RAG
在法规核查工作流里RAG 不直接决定是否合规而是为规则判断补充法规依据。例如结构化规则已经判断“缺少注册检验报告”RAG 再检索相关法规要求,给出来源文件和依据片段。
这种方式避免了“让大模型自由判断合规”的不稳定性,同时让报告具备可解释依据。
## 七、三条核心工作流
当前 Demo 拆成三条主链路:文件汇总、法规核查、自动填表。
### 1. 文件汇总链路
对应模块:`review_agent/file_summary`
```text
文件上传
-> 附件固化
-> 压缩包解压
-> 文件扫描
-> 页数统计
-> 产品名识别
-> Markdown/Excel 报告输出
```
这个链路负责把原始资料包转换成结构化文件清单。系统会生成 `FileSummaryBatch``FileSummaryItem`,后续法规核查和自动填表都复用这套文件清单,不再重复扫描资料。
输出字段包括序号、目录层级、文件名、文件类型、页数、相对路径、统计状态、重试次数和异常说明。
### 2. 法规核查链路
对应模块:`review_agent/regulatory_review`
```text
准备资料
-> 适用条件确认
-> 规则范围裁剪
-> 完整性核查
-> 文本抽取
-> 章节核查
-> 一致性核查
-> RAG 法规依据补充
-> 风险评估
-> 报告输出
-> 整改复核
```
这条链路使用 `review_agent/regulatory_review/rules/nmpa_ivd_registration_v1.yaml` 作为结构化规则文件。规则中配置了附件 4 的资料要求,包括监管信息、综述资料、非临床资料、临床评价资料、说明书和标签样稿、质量管理体系文件等。
系统会检查是否缺少关键资料,例如注册申请表、符合性声明、产品技术要求、注册检验报告、说明书、标签样稿、临床评价资料和质量管理体系文件。缺失项会转成 `RegulatoryIssue`,并按阻断项、高风险、中风险、低风险和提示项分级。
### 3. 自动填表链路
对应模块:`review_agent/application_form_fill`
```text
准备资料
-> 模板选择
-> 模板复制
-> 字段抽取
-> 冲突归并
-> Word 填写
-> 追溯清单导出
-> 结果通知
```
这条链路会复用前面抽取到的产品信息,自动选择申报模板,并将字段填入 Word 模板。对于冲突字段Demo 中采用明确的归并策略,同时在结果中保留冲突摘要和来源追溯。
## 八、页面和数据工作台
前端目前包括四个主要页面:
| 页面 | URL | 作用 |
| --- | --- | --- |
| 首页工作台 | `/` | 展示对话、附件、知识库、批次状态和最近处理记录 |
| 审核智能体 | `/chat/` | 对话、上传附件、启动工作流、查看节点进度 |
| 知识库管理 | `/knowledge-base/` | 管理用户上传知识库、查看内置法规材料和索引状态 |
| 附件管理 | `/attachments/` | 管理不同对话下的上传附件、版本、启用状态和下载 |
首页工作台重点不是营销展示,而是运行态数据,包括:
```text
对话总数
附件总数
知识库材料数
执行中批次
已处理批次
成功批次
等待确认批次
失败批次
最近处理记录
```
知识库材料中同时统计用户管理文档和内置法规材料,避免把“知识库”误解成只包含用户上传文件。
## 九、过程留痕和可追溯设计
审核类系统不能只输出一个结论,还必须说明结论从哪里来。因此 Demo 对关键过程都做了结构化留痕。
| 过程 | 留痕内容 |
| --- | --- |
| 对话 | 用户消息、助手消息、会话标题、更新时间 |
| 附件 | 原始文件名、版本号、启用状态、存储路径、文件大小 |
| 文件汇总 | 批次号、文件明细、页数、统计状态、异常说明 |
| 工作流节点 | 节点编码、节点名称、进度、状态、错误信息 |
| 法规核查 | 规则编码、缺失项、风险等级、证据、整改建议 |
| RAG 检索 | 来源文件、片段文本、相似度、chunk 元数据 |
| 自动填表 | 字段来源、冲突摘要、模板选择、追溯清单 |
| 导出文件 | Markdown、Excel、JSON、Word 等结果文件 |
这保证了 Demo 输出的结果不是一次性回答,而是可以复核、下载、整改和继续追踪的过程资产。
## 十、Demo 可展示结果
本次 Demo 可以展示以下核心结果:
### 1. 文件目录汇总表
用户上传注册资料文件夹、散装文件或压缩包后,系统自动完成附件固化、解压、扫描和页数统计,最终生成 Markdown 汇总报告和 Excel 明细表。
### 2. 法规完整性报告
系统基于文件汇总结果和 NMPA 规则库做完整性核查,输出 Markdown 法规核查报告、Excel 问题清单和 JSON 结构化结果包。
### 3. 产品关键信息提取对照表
系统从说明书、产品技术要求、注册检验报告、申请表等文件中抽取产品名称、型号规格、预期用途、管理类别、分类编码、注册类型和临床评价路径,并保留来源文件和证据片段。
### 4. 风险预警列表
系统把完整性缺失、章节异常、字段冲突、文本抽取失败、页数不可确定、通知失败等问题统一沉淀为风险项,并按阻断项、高风险、中风险、低风险和提示项分级。
### 5. 申报文件自动填表结果
系统根据资料内容和适用条件选择模板,自动填充 Word 文件,并导出字段追溯清单,说明每个字段来自哪个文件、哪个证据片段。
## 十一、总结
整体来看,本 Demo 的架构搭建思路可以概括为:
```text
先用文档锁定规格
再用规则结构化审核逻辑
再用 RAG 补充法规依据
再用 Skill Router 调度工具和工作流
再用 ORM 和导出文件沉淀过程资产
最后通过工作台页面呈现状态和结果
```
它体现的是一个“资料输入、规则判断、证据追溯、风险输出、整改闭环”的智能体原型。
当前 Demo 已经完成了首页工作台、审核智能体对话、附件管理、知识库管理、文件汇总、法规核查、RAG 依据检索、风险预警、报告导出和自动填表主链路。后续如果继续增强,可以重点补充 OCR、扫描件识别、复杂 PDF 版式解析、规则后台维护、人工确认界面、飞书真实消息闭环,以及更完整的多智能体编排能力。
最终希望这个智能体能够从一个 Demo 原型,逐步演进为注册资料准备和审核过程中的智能协作平台。

3
pytest.ini Normal file
View File

@@ -0,0 +1,3 @@
[pytest]
DJANGO_SETTINGS_MODULE = config.settings
python_files = tests.py test_*.py *_tests.py

View File

@@ -1 +1,12 @@
Django>=5.0,<6.0
pypdf>=5.0
python-docx>=1.1
python-pptx>=1.0
openpyxl>=3.1
xlrd>=2.0
olefile>=0.47
py7zr>=0.21
playwright>=1.60
PyYAML>=6.0
chromadb>=0.5
httpx>=0.27

74
review_agent/admin.py Normal file
View File

@@ -0,0 +1,74 @@
from django.contrib import admin
from review_agent.models import (
FeishuAccessTokenCache,
FeishuQuestionLog,
FeishuUserMapping,
WorkflowNotificationRecord,
)
@admin.register(FeishuUserMapping)
class FeishuUserMappingAdmin(admin.ModelAdmin):
list_display = (
"system_user",
"feishu_display_name",
"feishu_open_id",
"feishu_user_id",
"feishu_mobile",
"is_active",
"updated_at",
)
list_filter = ("is_active",)
search_fields = (
"system_user__username",
"feishu_display_name",
"feishu_open_id",
"feishu_user_id",
"feishu_mobile",
)
readonly_fields = ("created_at", "updated_at")
@admin.register(FeishuAccessTokenCache)
class FeishuAccessTokenCacheAdmin(admin.ModelAdmin):
list_display = ("app_id_hash", "expires_at", "updated_at", "has_error")
search_fields = ("app_id_hash", "error_message")
readonly_fields = ("created_at", "updated_at")
@admin.display(boolean=True, description="有错误")
def has_error(self, obj: FeishuAccessTokenCache) -> bool:
return bool(obj.error_message)
@admin.register(WorkflowNotificationRecord)
class WorkflowNotificationRecordAdmin(admin.ModelAdmin):
list_display = (
"workflow_type",
"workflow_batch_no",
"workflow_status",
"channel",
"send_status",
"target",
"sent_at",
"created_at",
)
list_filter = ("workflow_type", "channel", "send_status", "workflow_status")
search_fields = ("workflow_batch_no", "dedupe_key", "target", "error_message")
readonly_fields = ("created_at", "updated_at")
@admin.register(FeishuQuestionLog)
class FeishuQuestionLogAdmin(admin.ModelAdmin):
list_display = (
"system_user",
"source_type",
"intent",
"permission_result",
"status",
"processed_at",
"created_at",
)
list_filter = ("source_type", "intent", "permission_result", "status")
search_fields = ("system_user__username", "question_text", "answer_summary", "message_id")
readonly_fields = ("created_at",)

View File

@@ -0,0 +1 @@
"""Application form auto-fill workflow package."""

View File

@@ -0,0 +1,36 @@
WORKFLOW_TYPE = "application_form_fill"
TEMPLATE_REGISTRATION_CERTIFICATE = "registration_certificate"
TEMPLATE_CHANGE_REGISTRATION = "change_registration"
TEMPLATE_ESSENTIAL_PRINCIPLES = "essential_principles"
DEFAULT_OUTPUT_TYPES = ["word", "excel", "json"]
FORM_FILL_TRIGGER_KEYWORDS = [
"填注册证",
"对应的表格",
"生成申报模板",
"安全和性能基本原则清单",
"填到申报模板",
"自动填表",
"生成表格",
"申报文件模板",
"申报文件填表",
"产品关键信息",
"字段来源追溯清单",
"注册证 word",
]
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"),
]

View File

@@ -0,0 +1,27 @@
from __future__ import annotations
from review_agent.application_form_fill.constants import WORKFLOW_TYPE
from review_agent.models import ApplicationFormFillBatch, WorkflowEvent
def record_event(
batch: ApplicationFormFillBatch,
event_type: str,
payload: dict | None = None,
) -> WorkflowEvent:
return WorkflowEvent.objects.create(
workflow_type=WORKFLOW_TYPE,
workflow_batch_id=batch.pk,
conversation=batch.conversation,
event_type=event_type,
payload=payload or {},
)
def serialize_event(event: WorkflowEvent) -> dict[str, object]:
return {
"id": event.pk,
"event_type": event.event_type,
"payload": event.payload,
"created_at": event.created_at.isoformat(),
}

View File

@@ -0,0 +1,23 @@
你是医疗器械体外诊断试剂申报资料字段抽取助手。
请只输出 JSON 对象,不要输出 Markdown。结构如下
{
"fields": [
{
"key": "product_name",
"label": "产品名称",
"value": "字段值",
"source_file": "来源文件名",
"source_role": "说明书",
"evidence": "原文证据",
"confidence": 0.8
}
],
"checklist_items": []
}
要求:
- 只抽取输入模板字段中出现的信息。
- 字段值必须来自资料原文,不要编造。
- 找不到时不要输出该字段。

View File

@@ -0,0 +1,58 @@
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
from review_agent.models import ApplicationFormFillBatch, ExportedSummaryFile, FileSummaryBatch, RegulatoryReviewBatch
@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]] = field(default_factory=list)
@dataclass(frozen=True)
class ExtractedField:
key: str
label: str
value: str
source_file: str
source_role: str
evidence: str
extractor: str
confidence: float
@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)
@dataclass
class FormFillContext:
batch: ApplicationFormFillBatch
source_summary_batch: FileSummaryBatch
source_regulatory_batch: RegulatoryReviewBatch | None
template_config: dict[str, Any] = field(default_factory=dict)
selected_templates: list[TemplateSpec] = field(default_factory=list)
document_texts: dict[str, str] = field(default_factory=dict)
regex_results: dict[str, Any] = field(default_factory=dict)
llm_results: dict[str, Any] = field(default_factory=dict)
merged_fields: dict[str, MergedField] = field(default_factory=dict)
checklist_items: dict[str, Any] = field(default_factory=dict)
conflicts: list[dict[str, Any]] = field(default_factory=list)
exports: list[ExportedSummaryFile] = field(default_factory=list)

View File

@@ -0,0 +1 @@
"""Application form auto-fill services."""

View File

@@ -0,0 +1,278 @@
from __future__ import annotations
import json
import re
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
from typing import Any
from django.conf import settings
from review_agent.application_form_fill.schemas import ExtractedField, TemplateSpec
from review_agent.application_form_fill.storage import create_artifact_for_file, ensure_batch_subdir
from review_agent.llm import generate_completion
from review_agent.models import ApplicationFormFillArtifact, ApplicationFormFillBatch, FileSummaryBatch
from review_agent.regulatory_review.services.text_extract import extract_text
FIELD_ALIASES = {
"product_name": ["产品名称"],
"applicant_name": ["注册人名称", "申请人名称", "生产企业名称"],
"applicant_address": ["注册人住所", "申请人住所", "生产企业住所"],
"manufacturer_address": ["生产地址", "生产企业地址", "生产场所"],
"agent_name": ["代理人名称", "生产企业名称", "注册人名称", "申请人名称"],
"agent_address": ["代理人住所", "生产企业住所", "注册人住所", "申请人住所"],
"package_specification": ["包装规格", "规格"],
"main_components": ["主要组成成分", "主要组成", "组成成分"],
"intended_use": ["预期用途"],
"storage_condition_and_validity": ["产品储存条件及有效期", "储存条件及有效期", "储存条件", "有效期"],
}
STATIC_STOP_LABELS = [
"申请人",
"国家药品监督管理局",
"填表说明",
"",
"保证书",
"应附资料",
"优先通道申请",
"分类编码",
"医疗器械唯一标识",
"注册产品目前是否",
"临床评价路径",
"临床试验",
"其他需要说明的问题",
"国家药监局器审中心医疗器械",
]
def collect_document_texts(summary_batch: FileSummaryBatch) -> dict[str, str]:
texts: dict[str, str] = {}
for item in summary_batch.items.order_by("file_index"):
path = Path(item.storage_path)
if not path.is_absolute():
path = Path(settings.MEDIA_ROOT) / item.storage_path
if not path.exists():
continue
result = extract_text(path)
if result.status == "success" and result.text:
texts[item.file_name] = result.text
return texts
def extract_by_rules(texts: dict[str, str], specs: list[TemplateSpec]) -> dict[str, Any]:
fields: list[dict[str, Any]] = []
field_defs = _field_defs(specs)
labels = _all_field_labels(field_defs)
for file_name, text in texts.items():
source_role = detect_source_role(file_name, text)
for field in field_defs:
value, evidence = _extract_field_value(text, field, labels)
if not value:
continue
fields.append(
ExtractedField(
key=field["key"],
label=field["label"],
value=value,
source_file=file_name,
source_role=source_role,
evidence=evidence,
extractor="rule",
confidence=0.75 if source_role == "说明书" else 0.65,
).__dict__
)
return {"fields": fields, "checklist_items": []}
def extract_by_llm(texts: dict[str, str], specs: list[TemplateSpec]) -> dict[str, Any]:
try:
raw = generate_completion(
[
{"role": "system", "content": _prompt_text()},
{"role": "user", "content": _build_llm_user_prompt(texts, specs)},
],
temperature=0.0,
)
payload = _parse_json_object(raw)
except Exception as exc:
return {"fields": [], "checklist_items": [], "error_message": str(exc)}
fields = []
allowed_keys = {field["key"] for field in _field_defs(specs)}
for item in payload.get("fields") or []:
if not isinstance(item, dict) or item.get("key") not in allowed_keys or not item.get("value"):
continue
fields.append(
{
"key": str(item.get("key") or ""),
"label": str(item.get("label") or item.get("key") or ""),
"value": str(item.get("value") or "").strip(),
"source_file": str(item.get("source_file") or ""),
"source_role": str(item.get("source_role") or detect_source_role(str(item.get("source_file") or ""), "")),
"evidence": str(item.get("evidence") or "").strip(),
"extractor": "llm",
"confidence": _float_confidence(item.get("confidence"), default=0.7),
}
)
return {"fields": fields, "checklist_items": payload.get("checklist_items") or []}
def run_parallel_extract(texts: dict[str, str], specs: list[TemplateSpec]) -> dict[str, Any]:
with ThreadPoolExecutor(max_workers=2) as executor:
rule_future = executor.submit(extract_by_rules, texts, specs)
llm_future = executor.submit(extract_by_llm, texts, specs)
regex_results = rule_future.result()
llm_results = llm_future.result()
return {
"regex_results": regex_results,
"llm_results": llm_results,
"selected_templates": [spec.code for spec in specs],
"source_evidence": [{"source_file": name, "char_count": len(text)} for name, text in texts.items()],
}
def save_field_extract_result(batch: ApplicationFormFillBatch, payload: dict[str, Any]) -> ApplicationFormFillArtifact:
target_dir = ensure_batch_subdir(batch, "exports")
path = target_dir / "field_extract_result.json"
path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
return create_artifact_for_file(
batch,
path=path,
artifact_type=ApplicationFormFillArtifact.ArtifactType.FIELD_EXTRACT_RESULT,
file_format=ApplicationFormFillArtifact.FileFormat.JSON,
name="field_extract_result",
metadata={"artifact": "field_extract_result"},
created_by_node="field_extract",
)
def detect_source_role(file_name: str, text: str = "") -> str:
target = f"{file_name}\n{text[:200]}"
if "说明书" in target:
return "说明书"
if "产品技术要求" in target:
return "产品技术要求"
if "注册检验" in target or "检测报告" in target:
return "注册检验报告"
if "性能研究" in target:
return "性能研究资料"
if "申请表" in target:
return "申请表"
return "其他注册资料"
def _field_defs(specs: list[TemplateSpec]) -> list[dict[str, str]]:
fields: list[dict[str, str]] = []
for spec in specs:
for field in spec.fields:
key = str(field.get("key") or "")
label = str(field.get("label") or "")
if key and label:
fields.append({"key": key, "label": label})
return fields
def _extract_field_value(text: str, field: dict[str, str], labels: list[str]) -> tuple[str, str]:
aliases = _field_aliases(field)
for label in aliases:
value, evidence = _extract_colon_label_value(text, label, labels + aliases)
if value:
return value, evidence
value, evidence = _extract_bracket_section_value(text, label)
if value:
return value, evidence
return "", ""
def _field_aliases(field: dict[str, str]) -> list[str]:
aliases = [field["label"]]
aliases.extend(FIELD_ALIASES.get(field["key"], []))
result: list[str] = []
for alias in aliases:
normalized = str(alias or "").strip()
if normalized and normalized not in result:
result.append(normalized)
return result
def _all_field_labels(fields: list[dict[str, str]]) -> list[str]:
labels: list[str] = list(STATIC_STOP_LABELS)
for field in fields:
for label in _field_aliases(field):
if label not in labels:
labels.append(label)
return labels
def _extract_label_value(text: str, label: str, labels: list[str]) -> tuple[str, str]:
return _extract_colon_label_value(text, label, labels)
def _extract_colon_label_value(text: str, label: str, labels: list[str]) -> tuple[str, str]:
escaped_labels = "|".join(re.escape(item) for item in labels if item != label)
stop_pattern = rf"(?=\n\s*(?:{escaped_labels})(?:\s*[:]|\s*$))" if escaped_labels else r"(?=\Z)"
pattern = re.compile(rf"{re.escape(label)}\s*[:]\s*(.+?)(?:{stop_pattern}|\Z)", re.S)
match = pattern.search(text or "")
if not match:
return "", ""
raw = match.group(1).strip()
value = re.sub(r"\n{2,}.*\Z", "", raw, flags=re.S).strip()
value = "\n".join(line.strip() for line in value.splitlines() if line.strip())
evidence = f"{label}{value}"[:300]
return value, evidence
def _extract_bracket_section_value(text: str, label: str) -> tuple[str, str]:
heading_pattern = rf"^\s*[【\[]\s*{re.escape(label)}\s*[】\]]\s*$"
lines = (text or "").splitlines()
for index, line in enumerate(lines):
if not re.match(heading_pattern, line.strip()):
continue
value_parts: list[str] = []
for next_line in lines[index + 1 :]:
normalized = next_line.strip()
if not normalized:
continue
if _looks_like_bracket_heading(normalized):
break
value_parts.append(normalized)
value = "\n".join(value_parts).strip()
if value:
return value, f"{label}\n{value}"[:300]
return "", ""
def _looks_like_bracket_heading(line: str) -> bool:
return bool(re.match(r"^\s*[【\[].{1,40}[】\]]\s*$", line))
def _prompt_text() -> str:
path = Path(__file__).resolve().parents[1] / "prompts" / "field_extract.md"
return path.read_text(encoding="utf-8")
def _build_llm_user_prompt(texts: dict[str, str], specs: list[TemplateSpec]) -> str:
fields = [{"key": field["key"], "label": field["label"]} for field in _field_defs(specs)]
documents = [{"source_file": name, "text": text[:4000]} for name, text in texts.items()]
return json.dumps({"fields": fields, "documents": documents}, ensure_ascii=False)
def _parse_json_object(raw: str) -> dict[str, Any]:
text = (raw or "").strip()
if text.startswith("```"):
text = text.strip("`").strip()
if text.lower().startswith("json"):
text = text[4:].strip()
start = text.find("{")
end = text.rfind("}")
if start == -1 or end == -1 or end < start:
raise json.JSONDecodeError("未找到 JSON 对象", text, 0)
return json.loads(text[start : end + 1])
def _float_confidence(value, *, default: float) -> float:
try:
return float(value)
except (TypeError, ValueError):
return default

View File

@@ -0,0 +1,110 @@
from __future__ import annotations
import re
from typing import Any
from review_agent.application_form_fill.schemas import MergedField
SOURCE_PRIORITY = {
"说明书": 1,
"产品技术要求": 2,
"注册检验报告": 3,
"检测报告": 3,
"性能研究资料": 4,
"其他注册资料": 5,
}
def normalize_field_value(value: str) -> str:
return re.sub(r"\s+", "", str(value or "")).strip().lower()
def rank_source(source_role: str, source_file: str = "") -> int:
target = f"{source_role}\n{source_file}"
for keyword, rank in SOURCE_PRIORITY.items():
if keyword in target:
return rank
return 9
def merge_fields(regex_results: dict[str, Any], llm_results: dict[str, Any]) -> tuple[dict[str, MergedField], list[dict]]:
grouped: dict[str, list[dict[str, Any]]] = {}
for item in list(regex_results.get("fields") or []) + list(llm_results.get("fields") or []):
key = str(item.get("key") or "")
value = str(item.get("value") or "").strip()
if not key or not value:
continue
grouped.setdefault(key, []).append(item)
merged: dict[str, MergedField] = {}
conflicts: list[dict] = []
for key, candidates in grouped.items():
selected = sorted(
candidates,
key=lambda item: (
rank_source(str(item.get("source_role") or ""), str(item.get("source_file") or "")),
-float(item.get("confidence") or 0),
),
)[0]
distinct = _distinct_values(candidates)
has_conflict = len(distinct) > 1
conflict_values = [
{
"value": item.get("value"),
"source_file": item.get("source_file", ""),
"source_role": item.get("source_role", ""),
"evidence": item.get("evidence", ""),
}
for item in candidates
if normalize_field_value(str(item.get("value") or "")) != normalize_field_value(str(selected.get("value") or ""))
]
merged_field = MergedField(
key=key,
label=str(selected.get("label") or key),
value=str(selected.get("value") or ""),
source_file=str(selected.get("source_file") or ""),
evidence=str(selected.get("evidence") or ""),
confidence=float(selected.get("confidence") or 0),
has_conflict=has_conflict,
conflict_values=conflict_values,
)
merged[key] = merged_field
if has_conflict:
conflicts.append(
{
"field_key": key,
"field_label": merged_field.label,
"selected_value": merged_field.value,
"selected_source": merged_field.source_file,
"conflict_values": conflict_values,
"handling": "说明书优先,模板内黄底红字高亮" if rank_source(merged_field.source_file, merged_field.source_file) == 1 else "按来源优先级采用最高优先级字段",
}
)
_apply_agent_company_fallbacks(merged)
return merged, conflicts
def _distinct_values(candidates: list[dict[str, Any]]) -> set[str]:
return {normalize_field_value(str(item.get("value") or "")) for item in candidates if item.get("value")}
def _apply_agent_company_fallbacks(merged: dict[str, MergedField]) -> None:
fallback_pairs = {
"agent_name": ("applicant_name", "代理人名称"),
"agent_address": ("applicant_address", "代理人住所"),
}
for target_key, (source_key, target_label) in fallback_pairs.items():
if target_key in merged or source_key not in merged:
continue
source = merged[source_key]
merged[target_key] = MergedField(
key=target_key,
label=target_label,
value=source.value,
source_file=source.source_file,
evidence=source.evidence,
confidence=source.confidence,
has_conflict=source.has_conflict,
conflict_values=source.conflict_values,
)

View File

@@ -0,0 +1,55 @@
from __future__ import annotations
from django.utils import timezone
from review_agent.models import (
ApplicationFormFillBatch,
ApplicationFormFillNotificationRecord,
ExportedSummaryFile,
)
from review_agent.notifications.dispatcher import dispatch_workflow_notification
from review_agent.notifications.workflow_adapters import build_application_form_fill_context
def notify_completion(
batch: ApplicationFormFillBatch,
exports: list[ExportedSummaryFile],
*,
fail: bool = False,
) -> ApplicationFormFillNotificationRecord:
export_ids = [export.pk for export in exports]
message_summary = (
f"自动填表批次 {batch.batch_no} 已完成,"
f"模板 {', '.join(batch.selected_templates or []) or '未识别'}"
f"冲突字段 {len(batch.conflict_summary or [])} 个。"
)
if fail:
return ApplicationFormFillNotificationRecord.objects.create(
batch=batch,
recipient=batch.user,
channel=ApplicationFormFillNotificationRecord.Channel.MOCK,
template_codes=batch.selected_templates,
export_ids=export_ids,
message_summary=message_summary,
send_status=ApplicationFormFillNotificationRecord.SendStatus.FAILED,
retry_count=1,
error_message="mock notification failed",
)
unified_error = ""
try:
unified_record = dispatch_workflow_notification(build_application_form_fill_context(batch))
if unified_record.send_status == unified_record.SendStatus.FAILED:
unified_error = unified_record.error_message
except Exception as exc:
unified_error = str(exc)
return ApplicationFormFillNotificationRecord.objects.create(
batch=batch,
recipient=batch.user,
channel=ApplicationFormFillNotificationRecord.Channel.MOCK,
template_codes=batch.selected_templates,
export_ids=export_ids,
message_summary=message_summary,
send_status=ApplicationFormFillNotificationRecord.SendStatus.SUCCESS,
error_message=unified_error,
sent_at=timezone.now(),
)

View File

@@ -0,0 +1,43 @@
from __future__ import annotations
from review_agent.models import ApplicationFormFillBatch, ExportedSummaryFile
def build_assistant_summary(batch: ApplicationFormFillBatch, exports: list[ExportedSummaryFile]) -> str:
word_exports = [export for export in exports if export.export_type == ExportedSummaryFile.ExportType.WORD]
trace_exports = [
export
for export in exports
if export.export_type in {ExportedSummaryFile.ExportType.EXCEL, ExportedSummaryFile.ExportType.JSON}
]
lines = ["已生成申报模板自动填表文件。", "", "| 文件 | Word | PDF |", "| --- | --- | --- |"]
if word_exports:
for export in word_exports:
lines.append(f"| {export.file_name} | [下载](/api/review-agent/file-summary/exports/{export.pk}/download/) | 待增强 |")
else:
lines.append("| 自动填表结果 | 未生成 | 待增强 |")
conflicts = batch.conflict_summary or []
if conflicts:
lines.extend(["", "| 冲突字段 | 采用值 | 冲突来源 | 处理 |", "| --- | --- | --- | --- |"])
for item in conflicts:
conflict_sources = "".join(
f"{_compact_table_text(value.get('source_file', ''))}{_compact_table_text(value.get('value', ''))}"
for value in item.get("conflict_values", [])
)
lines.append(
f"| {_compact_table_text(item.get('field_label', item.get('field_key', '')))} | {_compact_table_text(item.get('selected_value', ''))} | {_compact_table_text(conflict_sources or '-')} | {_compact_table_text(item.get('handling', ''))} |"
)
if trace_exports:
lines.append("")
for export in trace_exports:
lines.append(f"[下载{export.file_name}](/api/review-agent/file-summary/exports/{export.pk}/download/)")
return "\n".join(lines).strip()
def _compact_table_text(value: object, *, limit: int = 80) -> str:
text = " ".join(str(value or "").replace("|", " ").split())
if len(text) <= limit:
return text
return f"{text[:limit]}..."

View File

@@ -0,0 +1,96 @@
from __future__ import annotations
import hashlib
from pathlib import Path
from typing import Any
import yaml
from django.conf import settings
DEFAULT_CONFIG_PATH = (
Path(settings.BASE_DIR)
/ "review_agent"
/ "application_form_fill"
/ "templates"
/ "application_form_templates_v1.yaml"
)
SUPPORTED_TARGET_TYPES = {"table_row", "placeholder"}
SUPPORTED_FILE_FORMATS = {"doc", "docx"}
def load_template_config(path: str | Path | None = None) -> dict[str, Any]:
config_path = Path(path) if path else DEFAULT_CONFIG_PATH
with config_path.open("r", encoding="utf-8") as handle:
payload = yaml.safe_load(handle) or {}
return payload
def compute_config_hash(path: str | Path | None = None) -> str:
config_path = Path(path) if path else DEFAULT_CONFIG_PATH
digest = hashlib.sha256()
with config_path.open("rb") as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def validate_template_config(config: dict[str, Any], *, base_dir: str | Path | None = None) -> list[str]:
errors: list[str] = []
root = Path(base_dir) if base_dir else Path(settings.BASE_DIR)
version = config.get("version")
if not version:
errors.append("模板配置缺少 version。")
source_dir_value = config.get("source_dir")
source_dir = root / source_dir_value if source_dir_value else None
if not source_dir_value:
errors.append("模板配置缺少 source_dir。")
elif not source_dir.exists():
errors.append(f"模板 source_dir 不存在:{source_dir_value}")
templates = config.get("templates")
if not isinstance(templates, list) or not templates:
errors.append("模板配置必须包含非空 templates 列表。")
return errors
seen_codes: set[str] = set()
for index, template in enumerate(templates, start=1):
if not isinstance(template, dict):
errors.append(f"{index} 个模板配置必须是对象。")
continue
code = str(template.get("code") or "").strip()
if not code:
errors.append(f"{index} 个模板缺少 code。")
elif code in seen_codes:
errors.append(f"模板 code 重复:{code}")
seen_codes.add(code)
file_format = str(template.get("file_format") or "").strip().lower()
if file_format not in SUPPORTED_FILE_FORMATS:
errors.append(f"模板 {code or index} 的 file_format 不支持:{file_format or ''}")
source_file = str(template.get("source_file") or "").strip()
if not source_file:
errors.append(f"模板 {code or index} 缺少 source_file。")
elif source_dir and source_dir.exists() and not (source_dir / source_file).exists():
errors.append(f"模板 {code or index} 的 source_file 不存在:{source_file}")
fields = template.get("fields") or []
if not isinstance(fields, list):
errors.append(f"模板 {code or index} 的 fields 必须是列表。")
continue
for field_index, field in enumerate(fields, start=1):
target = field.get("target") if isinstance(field, dict) else None
target_type = str((target or {}).get("type") or "").strip()
if target_type not in SUPPORTED_TARGET_TYPES:
errors.append(
f"模板 {code or index}{field_index} 个字段 target.type 不支持:{target_type or ''}"
)
return errors
def template_specs(config: dict[str, Any]) -> list[dict[str, Any]]:
return list(config.get("templates") or [])

View File

@@ -0,0 +1,57 @@
from __future__ import annotations
import shutil
from pathlib import Path
from typing import Any
from django.conf import settings
from review_agent.application_form_fill.schemas import TemplateSpec
from review_agent.application_form_fill.storage import create_artifact_for_file, ensure_batch_subdir
from review_agent.models import ApplicationFormFillArtifact, ApplicationFormFillBatch
class TemplateUnavailableError(Exception):
pass
def resolve_source_template(spec: TemplateSpec, config: dict[str, Any]) -> Path:
source_dir = Path(settings.BASE_DIR) / str(config.get("source_dir") or "")
working_template = getattr(spec, "working_template", "") or ""
if spec.file_format == "doc" and working_template:
candidate = source_dir / working_template
else:
candidate = source_dir / spec.source_file
if not candidate.exists():
raise TemplateUnavailableError(f"模板文件不存在:{spec.source_file}")
if spec.file_format == "doc" and candidate.suffix.lower() == ".doc":
raise TemplateUnavailableError(f"模板 {spec.code} 为 .doc当前阶段需预转换为 .docx 后使用。")
return candidate
def copy_template_to_batch(
spec: TemplateSpec,
batch: ApplicationFormFillBatch,
config: dict[str, Any],
) -> ApplicationFormFillArtifact:
source = resolve_source_template(spec, config)
target_dir = ensure_batch_subdir(batch, "templates")
target = target_dir / f"{spec.code}.source{source.suffix.lower()}"
shutil.copy2(source, target)
_ensure_under(target, Path(batch.work_dir))
return create_artifact_for_file(
batch,
path=target,
artifact_type=ApplicationFormFillArtifact.ArtifactType.TEMPLATE_COPY,
file_format=source.suffix.lower().lstrip(".") or spec.file_format,
name=spec.name,
metadata={"template_code": spec.code, "source_file": spec.source_file},
created_by_node="template_copy",
)
def _ensure_under(path: Path, root: Path) -> None:
resolved_path = path.resolve()
resolved_root = root.resolve()
if resolved_path != resolved_root and resolved_root not in resolved_path.parents:
raise ValueError(f"模板复制目标不在批次工作目录内:{path}")

View File

@@ -0,0 +1,158 @@
from __future__ import annotations
from typing import Any
from review_agent.application_form_fill.constants import (
TEMPLATE_CHANGE_REGISTRATION,
TEMPLATE_ESSENTIAL_PRINCIPLES,
TEMPLATE_REGISTRATION_CERTIFICATE,
)
from review_agent.application_form_fill.schemas import TemplateSpec
from review_agent.models import ApplicationFormFillBatch
ALL_TEMPLATE_CODES = [
TEMPLATE_REGISTRATION_CERTIFICATE,
TEMPLATE_CHANGE_REGISTRATION,
TEMPLATE_ESSENTIAL_PRINCIPLES,
]
def parse_requested_templates(message: str) -> list[str]:
normalized = (message or "").lower()
if any(keyword in normalized for keyword in ["全部模板", "所有模板", "全套模板", "全部表格", "所有表格"]):
return ALL_TEMPLATE_CODES.copy()
requested: list[str] = []
if "注册证" in normalized and "变更注册" not in normalized and "变更 注册" not in normalized:
requested.append(TEMPLATE_REGISTRATION_CERTIFICATE)
if any(keyword in normalized for keyword in ["变更注册", "变更 注册", "变更备案", "备案文件"]):
requested.append(TEMPLATE_CHANGE_REGISTRATION)
if any(keyword in normalized for keyword in ["安全和性能基本原则", "基本原则清单", "原则清单"]):
requested.append(TEMPLATE_ESSENTIAL_PRINCIPLES)
return _dedupe(requested)
def detect_registration_type(
*,
batch: ApplicationFormFillBatch | None = None,
message: str = "",
file_candidates: dict[str, Any] | None = None,
) -> tuple[str, str]:
user_value = _registration_type_from_text(message)
if user_value:
return user_value, ApplicationFormFillBatch.RegistrationTypeSource.USER_MESSAGE
regulatory_value = _registration_type_from_regulatory_batch(batch)
if regulatory_value:
return regulatory_value, ApplicationFormFillBatch.RegistrationTypeSource.REGULATORY_BATCH
file_value = _registration_type_from_candidates(file_candidates or {})
if file_value:
return file_value, ApplicationFormFillBatch.RegistrationTypeSource.FILE_EXTRACT
return "unknown", ApplicationFormFillBatch.RegistrationTypeSource.UNKNOWN
def select_templates(
config: dict[str, Any],
requested_templates: list[str],
registration_type: str,
) -> tuple[list[TemplateSpec], list[dict[str, str]]]:
template_map = {item.get("code"): item for item in config.get("templates") or []}
risk_notes: list[dict[str, str]] = []
if requested_templates:
selected_codes = _dedupe(requested_templates)
elif registration_type in {"变更注册", "备案"}:
selected_codes = [TEMPLATE_CHANGE_REGISTRATION, TEMPLATE_ESSENTIAL_PRINCIPLES]
else:
selected_codes = [TEMPLATE_REGISTRATION_CERTIFICATE, TEMPLATE_ESSENTIAL_PRINCIPLES]
specs: list[TemplateSpec] = []
for code in selected_codes:
raw = template_map.get(code)
if not raw:
risk_notes.append({"type": "unknown_template", "message": f"模板不存在:{code}"})
continue
spec = _to_template_spec(raw)
if requested_templates and not _template_applies(spec, registration_type):
risk_notes.append(
{
"type": "template_registration_mismatch",
"message": f"用户指定模板 {spec.name} 与注册类型 {registration_type or 'unknown'} 可能不匹配,仍按指定生成。",
}
)
specs.append(spec)
return specs, risk_notes
def _to_template_spec(raw: dict[str, Any]) -> TemplateSpec:
return TemplateSpec(
code=str(raw.get("code") or ""),
name=str(raw.get("name") or ""),
source_file=str(raw.get("source_file") or ""),
output_label=str(raw.get("output_label") or raw.get("name") or ""),
applies_when=dict(raw.get("applies_when") or {}),
file_format=str(raw.get("file_format") or ""),
fields=list(raw.get("fields") or []),
checklist_items=list(raw.get("checklist_items") or []),
)
def _template_applies(spec: TemplateSpec, registration_type: str) -> bool:
allowed = spec.applies_when.get("registration_type") or []
if not allowed:
return True
return registration_type in allowed or (registration_type == "unknown" and "unknown" in allowed)
def _registration_type_from_text(message: str) -> str:
normalized = (message or "").lower()
if any(keyword in normalized for keyword in ["首次注册", "初次注册", "新注册"]):
return "首次注册"
if "变更注册" in normalized:
return "变更注册"
if "备案" in normalized:
return "备案"
return ""
def _registration_type_from_regulatory_batch(batch: ApplicationFormFillBatch | None) -> str:
if not batch or not batch.source_regulatory_batch_id:
return ""
condition_json = batch.source_regulatory_batch.condition_json or {}
confirmed = condition_json.get("confirmed_conditions") or {}
candidates = condition_json.get("candidates") or {}
for payload in [confirmed, condition_json, candidates.get("registration_type") or {}]:
if isinstance(payload, dict):
value = payload.get("registration_type") or payload.get("suggested") or payload.get("value")
normalized = _normalize_registration_type(value)
if normalized:
return normalized
return ""
def _registration_type_from_candidates(candidates: dict[str, Any]) -> str:
value = candidates.get("registration_type") or candidates.get("suggested")
if isinstance(value, dict):
value = value.get("value") or value.get("suggested")
return _normalize_registration_type(value)
def _normalize_registration_type(value: Any) -> str:
text = str(value or "")
if "首次" in text or "初次" in text:
return "首次注册"
if "变更" in text:
return "变更注册"
if "备案" in text:
return "备案"
return ""
def _dedupe(values: list[str]) -> list[str]:
result: list[str] = []
for value in values:
if value and value not in result:
result.append(value)
return result

View File

@@ -0,0 +1,145 @@
from __future__ import annotations
import json
from dataclasses import asdict
from pathlib import Path
from typing import Any
from openpyxl import Workbook
from review_agent.application_form_fill.constants import WORKFLOW_TYPE
from review_agent.application_form_fill.schemas import MergedField, TemplateSpec
from review_agent.application_form_fill.storage import create_artifact_for_file, ensure_batch_subdir
from review_agent.models import ApplicationFormFillArtifact, ApplicationFormFillBatch, ExportedSummaryFile
def build_traceability_workbook(
batch: ApplicationFormFillBatch,
merged_fields: dict[str, MergedField],
conflicts: list[dict[str, Any]],
specs: list[TemplateSpec],
generation_results: list[dict[str, Any]] | None = None,
) -> Workbook:
workbook = Workbook()
field_sheet = workbook.active
field_sheet.title = "字段追溯"
field_sheet.append(["模板", "字段", "填入值", "来源文件", "证据", "冲突状态"])
template_names = {field.get("key"): spec.output_label for spec in specs for field in spec.fields}
for key, field in merged_fields.items():
field_sheet.append(
[
template_names.get(key, ""),
field.label,
field.value,
field.source_file,
field.evidence,
"冲突" if field.has_conflict else "一致",
]
)
conflict_sheet = workbook.create_sheet("冲突字段")
conflict_sheet.append(["字段", "采用值", "冲突值", "冲突来源", "处理方式"])
for conflict in conflicts:
conflict_values = conflict.get("conflict_values") or []
if not conflict_values:
conflict_sheet.append(
[
conflict.get("field_label", ""),
conflict.get("selected_value", ""),
"",
"",
conflict.get("handling", ""),
]
)
continue
for value in conflict_values:
conflict_sheet.append(
[
conflict.get("field_label", ""),
conflict.get("selected_value", ""),
value.get("value", ""),
value.get("source_file", ""),
conflict.get("handling", ""),
]
)
low_confidence_sheet = workbook.create_sheet("低置信度条目")
low_confidence_sheet.append(["字段", "填入值", "置信度", "来源文件"])
for field in merged_fields.values():
if field.confidence < 0.6:
low_confidence_sheet.append([field.label, field.value, field.confidence, field.source_file])
result_sheet = workbook.create_sheet("生成结果")
result_sheet.append(["模板", "Word状态", "PDF状态", "错误说明"])
for result in generation_results or []:
result_sheet.append(
[
result.get("template_label", ""),
result.get("word_status", ""),
result.get("pdf_status", "待增强"),
result.get("error_message", ""),
]
)
if not generation_results:
for spec in specs:
result_sheet.append([spec.output_label, "待生成", "待增强", ""])
return workbook
def save_traceability_exports(
batch: ApplicationFormFillBatch,
merged_fields: dict[str, MergedField],
conflicts: list[dict[str, Any]],
specs: list[TemplateSpec],
generation_results: list[dict[str, Any]] | None = None,
) -> list[ExportedSummaryFile]:
target_dir = ensure_batch_subdir(batch, "exports")
workbook = build_traceability_workbook(batch, merged_fields, conflicts, specs, generation_results)
excel_path = target_dir / f"{batch.batch_no}-字段来源追溯清单.xlsx"
workbook.save(excel_path)
create_artifact_for_file(
batch,
path=excel_path,
artifact_type=ApplicationFormFillArtifact.ArtifactType.TRACEABILITY,
file_format=ApplicationFormFillArtifact.FileFormat.EXCEL,
name="字段来源追溯清单",
metadata={"conflict_count": len(conflicts)},
created_by_node="trace_export",
)
excel_export = ExportedSummaryFile.objects.create(
batch=batch.source_summary_batch,
workflow_type=WORKFLOW_TYPE,
workflow_batch_id=batch.pk,
export_category="traceability",
export_type=ExportedSummaryFile.ExportType.EXCEL,
file_name=excel_path.name,
storage_path=str(excel_path),
)
json_path = target_dir / "merged_fields.json"
payload = {
"batch_no": batch.batch_no,
"merged_fields": {key: asdict(value) for key, value in merged_fields.items()},
"conflicts": conflicts,
"generation_results": generation_results or [],
}
json_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
create_artifact_for_file(
batch,
path=json_path,
artifact_type=ApplicationFormFillArtifact.ArtifactType.MERGED_FIELDS,
file_format=ApplicationFormFillArtifact.FileFormat.JSON,
name="merged_fields",
metadata={"conflict_count": len(conflicts)},
created_by_node="trace_export",
)
json_export = ExportedSummaryFile.objects.create(
batch=batch.source_summary_batch,
workflow_type=WORKFLOW_TYPE,
workflow_batch_id=batch.pk,
export_category="traceability",
export_type=ExportedSummaryFile.ExportType.JSON,
file_name=json_path.name,
storage_path=str(json_path),
)
return [excel_export, json_export]

View File

@@ -0,0 +1,141 @@
from __future__ import annotations
import re
from pathlib import Path
from docx import Document
from docx.oxml import OxmlElement
from docx.oxml.ns import qn
from docx.shared import RGBColor
from review_agent.application_form_fill.constants import WORKFLOW_TYPE
from review_agent.application_form_fill.schemas import MergedField, TemplateSpec
from review_agent.application_form_fill.storage import create_artifact_for_file, ensure_batch_subdir
from review_agent.models import ApplicationFormFillArtifact, ApplicationFormFillBatch, ExportedSummaryFile
def fill_template(
template_path: str | Path,
output_path: str | Path,
spec: TemplateSpec,
fields: dict[str, MergedField],
conflicts: list[dict] | None = None,
) -> Path:
document = Document(str(template_path))
remove_fill_instructions(document)
conflict_keys = {item.get("field_key") for item in conflicts or []}
for field_config in spec.fields:
target = field_config.get("target") or {}
if target.get("type") != "table_row":
continue
key = field_config.get("key")
field = fields.get(key)
if not field:
continue
fill_table_row(
document,
str(target.get("row_label") or field_config.get("label") or ""),
field.value,
conflict=key in conflict_keys or field.has_conflict,
)
output = Path(output_path)
output.parent.mkdir(parents=True, exist_ok=True)
document.save(str(output))
return output
def remove_fill_instructions(document: Document) -> None:
removing = False
for paragraph in list(document.paragraphs):
text = _normalize_label(paragraph.text)
if text == "填表说明":
removing = True
if removing:
_remove_paragraph(paragraph)
continue
if text.startswith("注填表前") and "填表说明" in text:
_remove_paragraph(paragraph)
for table in document.tables:
for row in list(table.rows):
row_text = _normalize_label("".join(cell.text for cell in row.cells))
if row_text == "填表说明" or row_text.startswith("注填表前"):
_remove_row(row)
def fill_table_row(document: Document, row_label: str, value: str, *, conflict: bool = False) -> bool:
normalized_label = _normalize_label(row_label)
for table in document.tables:
for row in table.rows:
if len(row.cells) < 2:
continue
if _normalize_label(row.cells[0].text) != normalized_label:
continue
target = row.cells[1]
target.text = ""
paragraph = target.paragraphs[0]
run = paragraph.add_run(value)
if conflict:
run.font.color.rgb = RGBColor(0xFF, 0x00, 0x00)
apply_cell_shading(target, "FFFF00")
return True
return False
def apply_cell_shading(cell, fill: str) -> None:
tc_pr = cell._tc.get_or_add_tcPr()
shading = tc_pr.find(qn("w:shd"))
if shading is None:
shading = OxmlElement("w:shd")
tc_pr.append(shading)
shading.set(qn("w:fill"), fill)
def _remove_paragraph(paragraph) -> None:
element = paragraph._element
element.getparent().remove(element)
def _remove_row(row) -> None:
row._tr.getparent().remove(row._tr)
def create_word_export(
batch: ApplicationFormFillBatch,
spec: TemplateSpec,
template_path: str | Path,
fields: dict[str, MergedField],
conflicts: list[dict] | None = None,
) -> ExportedSummaryFile:
target_dir = ensure_batch_subdir(batch, "filled")
product_name = _safe_filename(batch.product_name or fields.get("product_name", MergedField("product_name", "产品名称", "", "", "", 0)).value or "未识别产品")
output_path = target_dir / f"{batch.batch_no}-{product_name}-{_safe_filename(spec.output_label)}.docx"
fill_template(template_path, output_path, spec, fields, conflicts)
create_artifact_for_file(
batch,
path=output_path,
artifact_type=ApplicationFormFillArtifact.ArtifactType.FILLED_TEMPLATE,
file_format=ApplicationFormFillArtifact.FileFormat.DOCX,
name=spec.output_label,
metadata={"template_code": spec.code, "conflict_count": len(conflicts or [])},
created_by_node="word_fill",
)
return ExportedSummaryFile.objects.create(
batch=batch.source_summary_batch,
workflow_type=WORKFLOW_TYPE,
workflow_batch_id=batch.pk,
export_category="filled_template",
export_type=ExportedSummaryFile.ExportType.WORD,
file_name=output_path.name,
storage_path=str(output_path),
)
def _normalize_label(value: str) -> str:
return re.sub(r"\s+", "", value or "").replace("", "").replace(":", "")
def _safe_filename(value: str) -> str:
text = re.sub(r"[\x00-\x1f\x7f]+", "", value or "")
text = re.sub(r'[\\/:*?"<>|]+', "_", text)
return text.strip()[:80] or "output"

View File

@@ -0,0 +1,55 @@
from __future__ import annotations
import hashlib
from pathlib import Path
from django.conf import settings
from review_agent.models import ApplicationFormFillArtifact, ApplicationFormFillBatch
def build_batch_work_dir(batch: ApplicationFormFillBatch | None = None, *, batch_no: str = "") -> Path:
if batch:
return Path(settings.MEDIA_ROOT) / "application_form_fill" / str(batch.user_id) / str(batch.conversation_id) / batch.batch_no
return Path(settings.MEDIA_ROOT) / "application_form_fill" / batch_no
def compute_file_sha256(path: str | Path) -> str:
file_path = Path(path)
digest = hashlib.sha256()
with file_path.open("rb") as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def ensure_batch_subdir(batch: ApplicationFormFillBatch, name: str) -> Path:
root = Path(batch.work_dir) if batch.work_dir else build_batch_work_dir(batch)
target = root / Path(name).name
target.mkdir(parents=True, exist_ok=True)
return target
def create_artifact_for_file(
batch: ApplicationFormFillBatch,
*,
path: str | Path,
artifact_type: str,
file_format: str,
name: str = "",
metadata: dict | None = None,
created_by_node: str = "",
) -> ApplicationFormFillArtifact:
file_path = Path(path)
return ApplicationFormFillArtifact.objects.create(
batch=batch,
artifact_type=artifact_type,
file_format=file_format,
name=name or file_path.stem,
file_name=file_path.name,
storage_path=str(file_path),
file_size=file_path.stat().st_size if file_path.exists() else 0,
content_hash=compute_file_sha256(file_path) if file_path.exists() else "",
metadata=metadata or {},
created_by_node=created_by_node,
)

View File

@@ -0,0 +1,130 @@
version: application_form_templates_v1
source_dir: docs/0.原始材料/关于公布体外诊断试剂注册申报资料要求和批准证明文件格式的公告
templates:
- code: registration_certificate
name: 中华人民共和国医疗器械注册证(体外诊断试剂)(格式)
source_file: 中华人民共和国医疗器械注册证(体外诊断试剂)(格式).docx
output_label: 注册证格式
applies_when:
registration_type:
- 首次注册
- unknown
file_format: docx
fields:
- key: applicant_name
label: 注册人名称
target:
type: table_row
row_label: 注册人名称
source_roles:
- 申请表
- 说明书
- 企业信息
- key: applicant_address
label: 注册人住所
target:
type: table_row
row_label: 注册人住所
source_roles:
- 申请表
- 企业信息
- key: manufacturer_address
label: 生产地址
target:
type: table_row
row_label: 生产地址
source_roles:
- 申请表
- 质量管理体系文件
- key: agent_name
label: 代理人名称
target:
type: table_row
row_label: 代理人名称
source_roles:
- 说明书
- 企业信息
- 申请表
- key: agent_address
label: 代理人住所
target:
type: table_row
row_label: 代理人住所
source_roles:
- 说明书
- 企业信息
- 申请表
- key: product_name
label: 产品名称
target:
type: table_row
row_label: 产品名称
source_roles:
- 说明书
- 产品技术要求
- 注册检验报告
- key: package_specification
label: 包装规格
target:
type: table_row
row_label: 包装规格
source_roles:
- 说明书
- 产品技术要求
- key: main_components
label: 主要组成成分
target:
type: table_row
row_label: 主要组成成分
source_roles:
- 说明书
- 产品技术要求
- key: intended_use
label: 预期用途
target:
type: table_row
row_label: 预期用途
source_roles:
- 说明书
- 临床评价资料
- 产品技术要求
- key: storage_condition_and_validity
label: 产品储存条件及有效期
target:
type: table_row
row_label: 产品储存条件及有效期
source_roles:
- 说明书
- 产品技术要求
- 稳定性研究资料
- key: attachments
label: 附件
target:
type: table_row
row_label: 附件
source_roles:
- 注册申报资料
- 说明书
- code: change_registration
name: 中华人民共和国医疗器械变更注册(备案)文件(体外诊断试剂)(格式)
source_file: 中华人民共和国医疗器械变更注册(备案)文件(体外诊断试剂)(格式).doc
output_label: 变更注册备案文件
applies_when:
registration_type:
- 变更注册
- 备案
file_format: doc
fields: []
- code: essential_principles
name: 体外诊断试剂安全和性能基本原则清单
source_file: 体外诊断试剂安全和性能基本原则清单.doc
output_label: 安全和性能基本原则清单
applies_when:
registration_type:
- 首次注册
- 变更注册
- 备案
- unknown
file_format: doc
fields: []
checklist_items: []

View File

@@ -0,0 +1,131 @@
import json
from django.contrib.auth.decorators import login_required
from django.conf import settings
from django.http import Http404, JsonResponse
from django.views.decorators.http import require_http_methods
from review_agent.application_form_fill.workflow import (
create_application_form_fill_batch,
find_latest_successful_summary_batch,
start_application_form_fill_workflow,
)
from review_agent.models import ApplicationFormFillBatch, Conversation, ExportedSummaryFile, FileSummaryBatch, WorkflowNodeRun
from review_agent.notifications.presenter import serialize_notification_records
@require_http_methods(["GET"])
def health(request):
return JsonResponse({"workflow_type": "application_form_fill", "status": "available"})
@login_required
@require_http_methods(["POST"])
def start(request):
try:
payload = json.loads(request.body.decode("utf-8") or "{}")
except json.JSONDecodeError:
return JsonResponse({"error": "JSON 格式错误。"}, status=400)
conversation = Conversation.objects.filter(pk=payload.get("conversation_id"), user=request.user).first()
if not conversation:
raise Http404("对话不存在。")
summary_batch = None
if payload.get("file_summary_batch_id"):
summary_batch = FileSummaryBatch.objects.filter(
pk=payload.get("file_summary_batch_id"),
conversation=conversation,
user=request.user,
status=FileSummaryBatch.Status.SUCCESS,
).first()
if summary_batch is None:
summary_batch = find_latest_successful_summary_batch(conversation)
if summary_batch is None:
return JsonResponse({"error": "请先上传资料并完成文件汇总。"}, status=400)
batch = create_application_form_fill_batch(
conversation=conversation,
user=request.user,
source_summary_batch=summary_batch,
requested_templates=payload.get("template_codes") or [],
output_types=payload.get("output_types") or None,
)
start_application_form_fill_workflow(batch, async_run=getattr(settings, "APPLICATION_FORM_FILL_ASYNC", True))
return JsonResponse(
{
"batch_id": batch.pk,
"workflow_type": "application_form_fill",
"status": batch.status,
"selected_templates": batch.selected_templates,
}
)
@login_required
@require_http_methods(["GET"])
def batch_status(request, batch_id: int):
batch = ApplicationFormFillBatch.objects.filter(
pk=batch_id,
conversation__user=request.user,
is_deleted=False,
).first()
if not batch:
raise Http404("填表批次不存在。")
exports = ExportedSummaryFile.objects.filter(
workflow_type="application_form_fill",
workflow_batch_id=batch.pk,
).order_by("id")
notifications = serialize_notification_records("application_form_fill", batch.pk)
return JsonResponse(
{
"batch": {
"id": batch.pk,
"workflow_type": "application_form_fill",
"batch_no": batch.batch_no,
"status": batch.status,
"product_name": batch.product_name,
"selected_templates": batch.selected_templates,
"conflict_count": len(batch.conflict_summary or []),
"risk_summary_text": _risk_summary_text(batch),
"error_message": batch.error_message,
},
"nodes": [
{
"node_code": node.node_code,
"node_name": node.node_name,
"status": node.status,
"progress": node.progress,
"message": node.message,
}
for node in WorkflowNodeRun.objects.filter(
workflow_type="application_form_fill",
workflow_batch_id=batch.pk,
).order_by("id")
],
"conflicts": batch.conflict_summary or [],
"exports": [
{
"id": export.pk,
"export_type": export.export_type,
"export_category": export.export_category,
"file_name": export.file_name,
"download_url": f"/api/review-agent/file-summary/exports/{export.pk}/download/",
}
for export in exports
],
"notifications": notifications,
"latest_notification": notifications[0] if notifications else None,
}
)
def _risk_summary_text(batch: ApplicationFormFillBatch) -> str:
parts = []
if batch.selected_templates:
parts.append("模板 " + "".join(batch.selected_templates))
if batch.conflict_summary:
parts.append(f"冲突字段 {len(batch.conflict_summary)}")
if batch.risk_notes:
parts.append(f"提示 {len(batch.risk_notes)}")
return " · ".join(parts)

View File

@@ -0,0 +1,328 @@
from __future__ import annotations
import logging
from threading import Thread
from uuid import uuid4
from django.conf import settings
from django.db import transaction
from django.utils import timezone
from review_agent.application_form_fill.constants import DEFAULT_OUTPUT_TYPES, FORM_FILL_NODE_DEFINITIONS, WORKFLOW_TYPE
from review_agent.application_form_fill.events import record_event
from review_agent.application_form_fill.services.field_extract import (
collect_document_texts,
run_parallel_extract,
save_field_extract_result,
)
from review_agent.application_form_fill.services.field_merge import merge_fields
from review_agent.application_form_fill.services.notifier import notify_completion
from review_agent.application_form_fill.services.summary import build_assistant_summary
from review_agent.application_form_fill.services.template_config import (
compute_config_hash,
load_template_config,
validate_template_config,
)
from review_agent.application_form_fill.services.template_repository import (
TemplateUnavailableError,
copy_template_to_batch,
)
from review_agent.application_form_fill.services.template_select import (
detect_registration_type,
parse_requested_templates,
select_templates,
)
from review_agent.application_form_fill.services.traceability_export import save_traceability_exports
from review_agent.application_form_fill.services.word_fill import create_word_export
from review_agent.application_form_fill.schemas import MergedField, TemplateSpec
from review_agent.application_form_fill.storage import build_batch_work_dir
from review_agent.models import ApplicationFormFillBatch, Conversation, FileSummaryBatch, Message, WorkflowNodeRun
logger = logging.getLogger("review_agent.application_form_fill.workflow")
def build_batch_no() -> str:
return f"AFF-{timezone.localtime().strftime('%Y%m%d%H%M%S')}-{uuid4().hex[:6]}"
def find_latest_successful_summary_batch(conversation: Conversation) -> FileSummaryBatch | None:
return (
FileSummaryBatch.objects.filter(
conversation=conversation,
status=FileSummaryBatch.Status.SUCCESS,
)
.order_by("-finished_at", "-created_at", "-id")
.first()
)
@transaction.atomic
def create_application_form_fill_batch(
*,
conversation: Conversation,
user,
source_summary_batch: FileSummaryBatch,
trigger_message: Message | None = None,
requested_templates: list[str] | None = None,
output_types: list[str] | None = None,
) -> ApplicationFormFillBatch:
batch_no = build_batch_no()
work_dir = build_batch_work_dir(batch_no=batch_no)
work_dir.mkdir(parents=True, exist_ok=True)
batch = ApplicationFormFillBatch.objects.create(
conversation=conversation,
user=user,
trigger_message=trigger_message,
source_summary_batch=source_summary_batch,
batch_no=batch_no,
requested_templates=requested_templates or [],
output_types=output_types or DEFAULT_OUTPUT_TYPES,
work_dir=str(work_dir),
)
for code, name, group in FORM_FILL_NODE_DEFINITIONS:
WorkflowNodeRun.objects.create(
workflow_type=WORKFLOW_TYPE,
workflow_batch_id=batch.pk,
node_group=group,
node_code=code,
node_name=name,
)
record_event(batch, "workflow_created", {"batch_id": batch.pk, "batch_no": batch.batch_no})
return batch
class FormFillWorkflowExecutor:
"""Runs the auto-fill workflow skeleton; later stages fill node bodies."""
def __init__(self, batch: ApplicationFormFillBatch):
self.batch = batch
self.template_config: dict = {}
self.selected_templates: list[TemplateSpec] = []
self.template_paths: dict[str, str] = {}
self.document_texts: dict[str, str] = {}
self.extract_payload: dict = {}
self.merged_fields: dict[str, MergedField] = {}
self.conflicts: list[dict] = []
self.exports = []
self.generation_results: list[dict] = []
self.non_blocking_errors: list[str] = []
def run(self) -> None:
logger.info("自动填表工作流开始 batch_no=%s batch_id=%s", self.batch.batch_no, self.batch.pk)
self.batch.status = ApplicationFormFillBatch.Status.RUNNING
self.batch.started_at = timezone.now()
self.batch.save(update_fields=["status", "started_at"])
record_event(self.batch, "workflow_started", {"batch_id": self.batch.pk})
try:
for node in self._nodes():
if node.status in {WorkflowNodeRun.Status.SUCCESS, WorkflowNodeRun.Status.SKIPPED}:
continue
self._run_node(node)
except Exception as exc:
logger.exception("Application form fill workflow failed", extra={"batch_id": self.batch.pk})
self.batch.status = ApplicationFormFillBatch.Status.FAILED
self.batch.error_message = str(exc)
self.batch.finished_at = timezone.now()
self.batch.save(update_fields=["status", "error_message", "finished_at"])
record_event(self.batch, "workflow_failed", {"message": str(exc)})
return
self.batch.refresh_from_db()
if self.batch.status != ApplicationFormFillBatch.Status.PARTIAL_SUCCESS:
self.batch.status = ApplicationFormFillBatch.Status.SUCCESS
self.batch.finished_at = timezone.now()
self.batch.save(update_fields=["status", "finished_at"])
record_event(self.batch, "workflow_completed", {"batch_id": self.batch.pk})
logger.info("自动填表工作流完成 batch_no=%s", self.batch.batch_no)
def _nodes(self):
return WorkflowNodeRun.objects.filter(
workflow_type=WORKFLOW_TYPE,
workflow_batch_id=self.batch.pk,
).order_by("id")
def _run_node(self, node: WorkflowNodeRun) -> None:
node.status = WorkflowNodeRun.Status.RUNNING
node.progress = 10
node.started_at = timezone.now()
node.message = f"{node.node_name}处理中"
node.save(update_fields=["status", "progress", "started_at", "message"])
record_event(
self.batch,
"node_progress",
{"node_code": node.node_code, "status": node.status, "progress": node.progress, "message": node.message},
)
if node.node_code == "pdf_convert":
self._append_risk_note(
{
"type": "pdf_pending",
"message": "PDF 转换为后续增强项,本次优先生成 Word。",
}
)
node.status = WorkflowNodeRun.Status.SKIPPED
node.progress = 100
node.finished_at = timezone.now()
node.message = "PDF 转换为后续增强项,本次跳过"
node.save(update_fields=["status", "progress", "finished_at", "message"])
record_event(
self.batch,
"node_progress",
{"node_code": node.node_code, "status": node.status, "progress": node.progress, "message": node.message},
)
return
self._execute_node(node)
node.status = WorkflowNodeRun.Status.SUCCESS
node.progress = 100
node.finished_at = timezone.now()
node.message = f"{node.node_name}完成"
node.save(update_fields=["status", "progress", "finished_at", "message"])
record_event(
self.batch,
"node_progress",
{"node_code": node.node_code, "status": node.status, "progress": node.progress, "message": node.message},
)
def _execute_node(self, node: WorkflowNodeRun) -> None:
if node.node_code == "prepare":
if self.batch.source_summary_batch.status != FileSummaryBatch.Status.SUCCESS:
raise ValueError("自动填表需要成功的文件汇总批次。")
return
if node.node_code == "template_select":
self.template_config = load_template_config()
errors = validate_template_config(self.template_config)
if errors:
raise ValueError("".join(errors))
requested = parse_requested_templates(self.batch.trigger_message.content if self.batch.trigger_message else "")
registration_type, source = detect_registration_type(batch=self.batch, message=self.batch.trigger_message.content if self.batch.trigger_message else "")
specs, risk_notes = select_templates(self.template_config, requested, registration_type)
if not specs:
raise ValueError("未选择到可用申报模板。")
self.selected_templates = specs
self.batch.requested_templates = requested
self.batch.selected_templates = [spec.code for spec in specs]
self.batch.registration_type = registration_type
self.batch.registration_type_source = source
self.batch.template_config_version = str(self.template_config.get("version") or "")
self.batch.template_config_hash = compute_config_hash()
self.batch.risk_notes = list(self.batch.risk_notes or []) + risk_notes
self.batch.save(
update_fields=[
"requested_templates",
"selected_templates",
"registration_type",
"registration_type_source",
"template_config_version",
"template_config_hash",
"risk_notes",
]
)
return
if node.node_code == "template_copy":
for spec in self.selected_templates:
try:
artifact = copy_template_to_batch(spec, self.batch, self.template_config)
self.template_paths[spec.code] = artifact.storage_path
except TemplateUnavailableError as exc:
self.non_blocking_errors.append(str(exc))
self._append_risk_note({"type": "template_unavailable", "message": str(exc), "template_code": spec.code})
if not self.template_paths:
raise ValueError("没有可用的 Word 模板副本。")
return
if node.node_code == "field_extract":
self.document_texts = collect_document_texts(self.batch.source_summary_batch)
self.extract_payload = run_parallel_extract(self.document_texts, self.selected_templates)
save_field_extract_result(self.batch, self.extract_payload)
return
if node.node_code == "conflict_merge":
self.merged_fields, self.conflicts = merge_fields(
self.extract_payload.get("regex_results") or {},
self.extract_payload.get("llm_results") or {},
)
product = self.merged_fields.get("product_name")
if product and product.value:
self.batch.product_name = product.value
self.batch.conflict_summary = self.conflicts
self.batch.save(update_fields=["product_name", "conflict_summary"])
return
if node.node_code == "word_fill":
for spec in self.selected_templates:
template_path = self.template_paths.get(spec.code)
if not template_path:
self.generation_results.append(
{
"template_code": spec.code,
"template_label": spec.output_label,
"word_status": "failed",
"pdf_status": "待增强",
"error_message": "模板不可用",
}
)
continue
export = create_word_export(self.batch, spec, template_path, self.merged_fields, self.conflicts)
self.exports.append(export)
self.generation_results.append(
{
"template_code": spec.code,
"template_label": spec.output_label,
"word_status": "success",
"pdf_status": "待增强",
"error_message": "",
}
)
if not any(item["word_status"] == "success" for item in self.generation_results):
raise ValueError("所有目标 Word 模板均生成失败。")
return
if node.node_code == "trace_export":
self.exports.extend(
save_traceability_exports(
self.batch,
self.merged_fields,
self.conflicts,
self.selected_templates,
self.generation_results,
)
)
return
if node.node_code == "output_export":
Message.objects.create(
conversation=self.batch.conversation,
role=Message.Role.ASSISTANT,
content=build_assistant_summary(self.batch, self.exports),
)
return
if node.node_code == "notify":
notification = notify_completion(
self.batch,
self.exports,
fail=getattr(settings, "APPLICATION_FORM_FILL_MOCK_NOTIFY_FAIL", False),
)
if notification.send_status == notification.SendStatus.FAILED:
self.non_blocking_errors.append(notification.error_message or "通知失败")
return
if node.node_code == "completed":
self._mark_final_status()
def _mark_final_status(self) -> None:
failed_word = any(item.get("word_status") == "failed" for item in self.generation_results)
if self.non_blocking_errors or failed_word:
self.batch.status = ApplicationFormFillBatch.Status.PARTIAL_SUCCESS
else:
self.batch.status = ApplicationFormFillBatch.Status.SUCCESS
self.batch.save(update_fields=["status"])
def _append_risk_note(self, note: dict) -> None:
self.batch.risk_notes = list(self.batch.risk_notes or []) + [note]
self.batch.save(update_fields=["risk_notes"])
def start_application_form_fill_workflow(batch: ApplicationFormFillBatch, *, async_run: bool = True) -> None:
executor = FormFillWorkflowExecutor(batch)
if not async_run:
executor.run()
return
Thread(target=executor.run, daemon=True).start()

View File

@@ -0,0 +1 @@
"""Reserved Feishu question services."""

View File

@@ -0,0 +1,43 @@
from __future__ import annotations
import re
WORKFLOW_KEYWORDS = {
"regulatory_review": ("法规核查", "风险", "整改", "RR-"),
"application_form_fill": ("自动填表", "填表", "申报文件", "AFF-"),
"file_summary": ("自动汇总", "文件汇总", "目录", "页数", "FS-"),
}
def parse_question_intent(text: str) -> dict[str, object]:
normalized = (text or "").strip()
batch_no = _extract_batch_no(normalized)
workflow_type = _detect_workflow_type(normalized, batch_no)
latest = bool(re.search(r"(最新|最近|上一个|最后一个)", normalized))
intent = "batch_status" if batch_no or latest else "unknown"
if workflow_type == "regulatory_review" and any(keyword in normalized for keyword in ["风险", "阻断", "整改"]):
intent = "risk_summary"
if workflow_type == "application_form_fill" and any(keyword in normalized for keyword in ["导出", "文件", "word", "Word"]):
intent = "export_summary"
if workflow_type == "file_summary" and any(keyword in normalized for keyword in ["缺失", "目录", "页数"]):
intent = "missing_summary"
return {
"intent": intent,
"workflow_type": workflow_type,
"batch_no": batch_no,
"latest": latest or not batch_no,
}
def _extract_batch_no(text: str) -> str:
match = re.search(r"\b(?:RR|AFF|FS)-[A-Za-z0-9-]+", text, flags=re.IGNORECASE)
return match.group(0).upper() if match else ""
def _detect_workflow_type(text: str, batch_no: str = "") -> str:
source = f"{text} {batch_no}"
for workflow_type, keywords in WORKFLOW_KEYWORDS.items():
if any(keyword in source for keyword in keywords):
return workflow_type
return ""

View File

@@ -0,0 +1,9 @@
from __future__ import annotations
def can_access_batch(user, batch) -> bool:
if not user or not getattr(user, "is_authenticated", False):
return False
if getattr(user, "is_staff", False) or getattr(user, "is_superuser", False):
return True
return getattr(batch, "user_id", None) == user.pk

View File

@@ -0,0 +1,85 @@
from __future__ import annotations
from review_agent.models import ApplicationFormFillBatch, ExportedSummaryFile, FileSummaryBatch, RegulatoryReviewBatch
from .permissions import can_access_batch
WORKFLOW_MODELS = {
"file_summary": FileSummaryBatch,
"regulatory_review": RegulatoryReviewBatch,
"application_form_fill": ApplicationFormFillBatch,
}
def query_batch_summary(user, *, workflow_type: str | None = None, batch_no: str | None = None, latest: bool = False) -> dict:
candidates = _candidate_batches(workflow_type)
if batch_no:
for current_workflow_type, model in candidates:
batch = model.objects.filter(batch_no=batch_no).first()
if batch:
return _serialize_allowed_batch(user, current_workflow_type, batch)
return {"ok": False, "permission_result": "not_found", "answer_summary": "未找到对应批次。"}
if latest:
for current_workflow_type, model in candidates:
queryset = model.objects.all().order_by("-finished_at", "-created_at", "-id")
for batch in queryset:
if can_access_batch(user, batch):
return _serialize_batch(current_workflow_type, batch, permission_result="allowed")
return {"ok": False, "permission_result": "not_found", "answer_summary": "未找到可访问的批次。"}
return {"ok": False, "permission_result": "not_found", "answer_summary": "请提供批次号,或询问最新/最近批次。"}
def _candidate_batches(workflow_type: str | None):
if workflow_type and workflow_type in WORKFLOW_MODELS:
return [(workflow_type, WORKFLOW_MODELS[workflow_type])]
return list(WORKFLOW_MODELS.items())
def _serialize_allowed_batch(user, workflow_type: str, batch) -> dict:
if not can_access_batch(user, batch):
return {"ok": False, "permission_result": "denied", "answer_summary": "无权限访问该批次。"}
return _serialize_batch(workflow_type, batch, permission_result="allowed")
def _serialize_batch(workflow_type: str, batch, *, permission_result: str) -> dict:
summary = _summary_for_batch(workflow_type, batch)
result_url = _result_url(workflow_type, batch.pk)
answer = f"{batch.batch_no} 状态 {batch.status}{summary}"
return {
"ok": True,
"permission_result": permission_result,
"workflow_type": workflow_type,
"batch_id": batch.pk,
"batch_no": batch.batch_no,
"status": batch.status,
"summary": summary,
"result_url": result_url,
"answer_summary": answer,
}
def _summary_for_batch(workflow_type: str, batch) -> str:
if workflow_type == "file_summary":
return f"文件 {batch.total_files} 个,成功 {batch.success_files} 个,失败 {batch.failed_files} 个。"
if workflow_type == "regulatory_review":
risk = batch.risk_summary or {}
return f"阻断项 {int(risk.get('blocking') or 0)} 个,高风险 {int(risk.get('high') or 0)} 个。"
if workflow_type == "application_form_fill":
export_count = ExportedSummaryFile.objects.filter(
workflow_type="application_form_fill",
workflow_batch_id=batch.pk,
).count()
return f"导出文件 {export_count} 个,冲突字段 {len(batch.conflict_summary or [])} 个。"
return ""
def _result_url(workflow_type: str, batch_id: int) -> str:
paths = {
"file_summary": f"/api/review-agent/file-summary/{batch_id}/status/",
"regulatory_review": f"/api/review-agent/regulatory-review/{batch_id}/status/",
"application_form_fill": f"/api/review-agent/application-form-fill/{batch_id}/status/",
}
return paths.get(workflow_type, "/")

View File

@@ -0,0 +1,37 @@
from __future__ import annotations
from django.utils import timezone
from review_agent.models import FeishuQuestionLog
from .intent import parse_question_intent
from .query import query_batch_summary
def answer_question(user, text: str, *, source_type: str = FeishuQuestionLog.SourceType.SIMULATE) -> dict:
parsed = parse_question_intent(text)
result = query_batch_summary(
user,
workflow_type=parsed.get("workflow_type") or None,
batch_no=parsed.get("batch_no") or None,
latest=bool(parsed.get("latest")),
)
status = FeishuQuestionLog.Status.SUCCESS if result.get("ok") else FeishuQuestionLog.Status.FAILED
answer_summary = str(result.get("answer_summary") or "")
log = FeishuQuestionLog.objects.create(
system_user=user if getattr(user, "is_authenticated", False) else None,
source_type=source_type,
question_text=text,
intent=str(parsed.get("intent") or "unknown"),
query_object={
"workflow_type": parsed.get("workflow_type") or "",
"batch_no": parsed.get("batch_no") or "",
"latest": bool(parsed.get("latest")),
},
answer_summary=answer_summary[:500],
permission_result=str(result.get("permission_result") or ""),
status=status,
error_message="" if result.get("ok") else answer_summary,
processed_at=timezone.now(),
)
return {**result, "intent": parsed.get("intent"), "log_id": log.pk}

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,4 @@
from pathlib import Path
ATTACHMENT_ROOT = Path("file_summary") / "users"

View File

@@ -0,0 +1,23 @@
from __future__ import annotations
from review_agent.models import FileSummaryBatch, WorkflowEvent
def record_event(batch: FileSummaryBatch, event_type: str, payload: dict | None = None) -> WorkflowEvent:
return WorkflowEvent.objects.create(
batch=batch,
workflow_type="file_summary",
workflow_batch_id=batch.pk,
conversation=batch.conversation,
event_type=event_type,
payload=payload or {},
)
def serialize_event(event: WorkflowEvent) -> dict[str, object]:
return {
"id": event.pk,
"event_type": event.event_type,
"payload": event.payload,
"created_at": event.created_at.isoformat(),
}

View File

@@ -0,0 +1,12 @@
from __future__ import annotations
from pathlib import Path
from django.conf import settings
def resolve_storage_path(storage_path: str) -> Path:
path = Path(storage_path)
if path.is_absolute():
return path
return Path(settings.MEDIA_ROOT) / path

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,125 @@
from __future__ import annotations
import logging
import subprocess
from pathlib import Path
from zipfile import ZipFile
import py7zr
ARCHIVE_EXTENSIONS = {"zip", "7z", "rar"}
logger = logging.getLogger("review_agent.file_summary.services.archive")
def _ensure_inside_target(path: Path, target_dir: Path) -> None:
target = target_dir.resolve()
resolved = path.resolve()
if target != resolved and target not in resolved.parents:
raise ValueError("解压路径必须位于批次工作目录内。")
def _safe_member_path(target_dir: Path, member_name: str) -> Path:
destination = target_dir / member_name
_ensure_inside_target(destination, target_dir)
return destination
def extract_archive(archive_path: str | Path, target_dir: str | Path) -> list[Path]:
archive_path = Path(archive_path)
target_dir = Path(target_dir)
target_dir.mkdir(parents=True, exist_ok=True)
ext = archive_path.suffix.lower().lstrip(".")
if ext not in ARCHIVE_EXTENSIONS:
return []
if ext == "zip":
return _extract_zip(archive_path, target_dir)
if ext == "7z":
return _extract_7z(archive_path, target_dir)
return _extract_rar(archive_path, target_dir)
def _extract_zip(archive_path: Path, target_dir: Path) -> list[Path]:
extracted: list[Path] = []
with ZipFile(archive_path) as archive:
for member in archive.infolist():
destination = _safe_member_path(target_dir, member.filename)
if member.is_dir():
destination.mkdir(parents=True, exist_ok=True)
continue
destination.parent.mkdir(parents=True, exist_ok=True)
with archive.open(member) as source, destination.open("wb") as target:
target.write(source.read())
extracted.append(destination)
return extracted
def _extract_7z(archive_path: Path, target_dir: Path) -> list[Path]:
with py7zr.SevenZipFile(archive_path, mode="r") as archive:
names = archive.getnames()
for name in names:
_safe_member_path(target_dir, name)
archive.extractall(path=target_dir)
return [target_dir / name for name in names if (target_dir / name).is_file()]
def _extract_rar(archive_path: Path, target_dir: Path) -> list[Path]:
try:
extracted = _extract_rar_with_libarchive(archive_path, target_dir)
except Exception as exc:
logger.warning(
"RAR libarchive extract failed, falling back to 7z",
extra={"archive_path": str(archive_path), "target_dir": str(target_dir), "error": str(exc)},
)
else:
if extracted:
return extracted
logger.info(
"RAR libarchive extract produced no files, falling back to 7z",
extra={"archive_path": str(archive_path), "target_dir": str(target_dir)},
)
return _extract_rar_with_7z(archive_path, target_dir)
def _extract_rar_with_libarchive(archive_path: Path, target_dir: Path) -> list[Path]:
try:
import libarchive
except ImportError as exc:
raise RuntimeError("未安装 libarchive跳过 Python RAR 解压。") from exc
extracted: list[Path] = []
with libarchive.file_reader(str(archive_path)) as entries:
for entry in entries:
destination = _safe_member_path(target_dir, entry.pathname)
if entry.isdir:
destination.mkdir(parents=True, exist_ok=True)
continue
if not entry.isfile:
logger.info(
"RAR libarchive skipped non-regular entry",
extra={"archive_path": str(archive_path), "entry": entry.pathname},
)
continue
destination.parent.mkdir(parents=True, exist_ok=True)
with destination.open("wb") as target:
for block in entry.get_blocks():
target.write(block)
extracted.append(destination)
return extracted
def _extract_rar_with_7z(archive_path: Path, target_dir: Path) -> list[Path]:
result = subprocess.run(
["7z", "x", f"-o{target_dir}", str(archive_path), "-y"],
check=False,
capture_output=True,
text=True,
)
if result.returncode != 0:
raise RuntimeError(result.stderr or result.stdout or "rar 解压失败")
extracted = [path for path in target_dir.rglob("*") if path.is_file()]
for path in extracted:
_ensure_inside_target(path, target_dir)
return extracted

View File

@@ -0,0 +1,261 @@
from __future__ import annotations
import csv
import logging
from tempfile import TemporaryDirectory
from dataclasses import asdict, dataclass, field
from pathlib import Path
from django.conf import settings
from review_agent.models import FileAttachment
from review_agent.file_summary.services.archive import ARCHIVE_EXTENSIONS, extract_archive
TEXT_EXTENSIONS = {"txt", "md", "csv", "json", "log"}
SUPPORTED_EXTENSIONS = TEXT_EXTENSIONS | {"pdf", "docx", "xlsx", "pptx"} | ARCHIVE_EXTENSIONS
MAX_PREVIEW_CHARS = 3000
MAX_ROWS_PER_SHEET = 20
logger = logging.getLogger("review_agent.file_summary.attachment_reader")
@dataclass(frozen=True)
class AttachmentReadResult:
status: str
filename: str
file_type: str
file_size: int
preview_text: str = ""
sections: list[dict[str, object]] = field(default_factory=list)
error_message: str = ""
def to_dict(self) -> dict[str, object]:
return asdict(self)
def read_attachment_details(attachment: FileAttachment) -> AttachmentReadResult:
file_path = _attachment_absolute_path(attachment)
file_type = Path(attachment.original_name).suffix.lower().lstrip(".")
logger.info(
"Attachment read started",
extra={
"attachment_id": attachment.pk,
"conversation_id": attachment.conversation_id,
"original_name": attachment.original_name,
"file_type": file_type,
"storage_path": attachment.storage_path,
"resolved_path": str(file_path),
},
)
if not file_path.exists():
logger.warning(
"Attachment read missing file",
extra={"attachment_id": attachment.pk, "resolved_path": str(file_path)},
)
return _failed(attachment, file_type, "附件文件不存在。")
if file_type not in SUPPORTED_EXTENSIONS:
logger.warning(
"Attachment read unsupported type",
extra={"attachment_id": attachment.pk, "file_type": file_type},
)
return _failed(attachment, file_type, f"暂不支持解析 .{file_type or 'unknown'} 文件。", "unsupported")
try:
if file_type == "pdf":
sections = _read_pdf(file_path)
elif file_type == "docx":
sections = _read_docx(file_path)
elif file_type == "xlsx":
sections = _read_xlsx(file_path)
elif file_type == "pptx":
sections = _read_pptx(file_path)
elif file_type == "csv":
sections = _read_csv(file_path)
elif file_type in ARCHIVE_EXTENSIONS:
sections = _read_archive(file_path)
else:
sections = _read_text(file_path)
except Exception as exc:
logger.exception(
"Attachment read failed",
extra={"attachment_id": attachment.pk, "file_type": file_type, "error": str(exc)},
)
return _failed(attachment, file_type, str(exc))
preview = _build_preview(sections)
logger.info(
"Attachment read finished",
extra={
"attachment_id": attachment.pk,
"section_count": len(sections),
"preview_length": len(preview),
},
)
return AttachmentReadResult(
status="success",
filename=attachment.original_name,
file_type=file_type,
file_size=attachment.file_size,
preview_text=preview[:MAX_PREVIEW_CHARS],
sections=sections,
)
def _attachment_absolute_path(attachment: FileAttachment) -> Path:
path = Path(attachment.storage_path)
if path.is_absolute():
return path
return Path(settings.MEDIA_ROOT) / path
def _failed(
attachment: FileAttachment,
file_type: str,
message: str,
status: str = "failed",
) -> AttachmentReadResult:
return AttachmentReadResult(
status=status,
filename=attachment.original_name,
file_type=file_type,
file_size=attachment.file_size,
error_message=message,
)
def _read_text(path: Path) -> list[dict[str, object]]:
text = path.read_text(encoding="utf-8", errors="replace")
return [{"type": "text", "name": path.name, "text": text[:MAX_PREVIEW_CHARS]}]
def _read_csv(path: Path) -> list[dict[str, object]]:
with path.open("r", encoding="utf-8-sig", errors="replace", newline="") as handle:
rows = [[str(cell) for cell in row] for row in csv.reader(handle)]
return [
{
"type": "table",
"name": path.name,
"row_count": len(rows),
"rows": rows[:MAX_ROWS_PER_SHEET],
}
]
def _read_pdf(path: Path) -> list[dict[str, object]]:
from pypdf import PdfReader
reader = PdfReader(str(path))
pages = []
for index, page in enumerate(reader.pages, start=1):
text = page.extract_text() or ""
pages.append({"type": "page", "name": f"{index}", "text": text})
return pages
def _read_docx(path: Path) -> list[dict[str, object]]:
from docx import Document
document = Document(str(path))
paragraphs = [item.text.strip() for item in document.paragraphs if item.text.strip()]
sections: list[dict[str, object]] = [
{"type": "text", "name": "正文", "text": "\n".join(paragraphs)}
]
for index, table in enumerate(document.tables, start=1):
rows = [[cell.text.strip() for cell in row.cells] for row in table.rows]
sections.append(
{
"type": "table",
"name": f"表格 {index}",
"row_count": len(rows),
"rows": rows[:MAX_ROWS_PER_SHEET],
}
)
return sections
def _read_xlsx(path: Path) -> list[dict[str, object]]:
from openpyxl import load_workbook
workbook = load_workbook(str(path), read_only=True, data_only=True)
sections = []
for sheet in workbook.worksheets:
rows = []
for row in sheet.iter_rows(max_row=MAX_ROWS_PER_SHEET, values_only=True):
rows.append(["" if cell is None else str(cell) for cell in row])
sections.append(
{
"type": "sheet",
"name": sheet.title,
"row_count": sheet.max_row,
"column_count": sheet.max_column,
"rows": rows,
}
)
workbook.close()
return sections
def _read_pptx(path: Path) -> list[dict[str, object]]:
from pptx import Presentation
presentation = Presentation(str(path))
sections = []
for index, slide in enumerate(presentation.slides, start=1):
texts = []
for shape in slide.shapes:
if hasattr(shape, "text") and shape.text.strip():
texts.append(shape.text.strip())
sections.append({"type": "slide", "name": f"幻灯片 {index}", "text": "\n".join(texts)})
return sections
def _read_archive(path: Path) -> list[dict[str, object]]:
sections: list[dict[str, object]] = []
with TemporaryDirectory(prefix="attachment-reader-") as temp_dir:
extracted = extract_archive(path, Path(temp_dir))
if not extracted:
return [{"type": "archive", "name": path.name, "text": "压缩包未解出任何可读取文件。"}]
for item in extracted:
file_type = item.suffix.lower().lstrip(".")
if file_type not in SUPPORTED_EXTENSIONS or file_type in ARCHIVE_EXTENSIONS:
sections.append(
{
"type": "file",
"name": item.name,
"text": f"暂不支持预览压缩包内的 .{file_type or 'unknown'} 文件。",
}
)
continue
for section in _read_supported_file(item, file_type):
section = dict(section)
section["name"] = f"{item.name} / {section.get('name', item.name)}"
sections.append(section)
return sections
def _read_supported_file(path: Path, file_type: str) -> list[dict[str, object]]:
if file_type == "pdf":
return _read_pdf(path)
if file_type == "docx":
return _read_docx(path)
if file_type == "xlsx":
return _read_xlsx(path)
if file_type == "pptx":
return _read_pptx(path)
if file_type == "csv":
return _read_csv(path)
return _read_text(path)
def _build_preview(sections: list[dict[str, object]]) -> str:
parts: list[str] = []
for section in sections:
if "text" in section and section["text"]:
parts.append(str(section["text"]))
rows = section.get("rows")
if rows:
parts.extend(" | ".join(str(cell) for cell in row) for row in rows[:5])
return "\n".join(part for part in parts if part).strip()

View File

@@ -0,0 +1,68 @@
from __future__ import annotations
import logging
from pathlib import Path
from django.conf import settings
from openpyxl import Workbook
from review_agent.models import ExportedSummaryFile, FileSummaryBatch
logger = logging.getLogger("review_agent.file_summary.export_excel")
def _exports_dir(batch: FileSummaryBatch) -> Path:
root = Path(batch.work_dir) if batch.work_dir else Path(settings.MEDIA_ROOT) / "file_summary" / batch.batch_no
export_dir = root / "exports"
export_dir.mkdir(parents=True, exist_ok=True)
return export_dir
def generate_excel_export(batch: FileSummaryBatch) -> ExportedSummaryFile:
logger.info("Excel export generation started", extra={"batch_id": batch.pk})
workbook = Workbook()
summary = workbook.active
summary.title = "汇总信息"
summary.append(["批次号", batch.batch_no])
summary.append(["产品名称", batch.product_name or "-"])
summary.append(["文件总数", batch.total_files])
summary.append(["统计成功", batch.success_files])
summary.append(["统计失败", batch.failed_files])
summary.append(["不支持", batch.unsupported_files])
summary.append(["不确定", batch.uncertain_files])
summary.append(["总页数", batch.total_pages])
detail = workbook.create_sheet("文件明细")
detail.append(["序号", "目录层级", "文件名", "类型", "页数", "路径", "状态", "重试次数", "异常说明"])
for item in batch.items.order_by("file_index"):
detail.append(
[
item.file_index,
item.directory_level,
item.file_name,
item.file_type,
item.page_count,
item.relative_path,
item.statistics_status,
item.retry_count,
item.error_message,
]
)
path = _exports_dir(batch) / f"{batch.batch_no}-summary.xlsx"
workbook.save(path)
exported = ExportedSummaryFile.objects.create(
batch=batch,
workflow_type="file_summary",
workflow_batch_id=batch.pk,
export_category="summary",
export_type=ExportedSummaryFile.ExportType.EXCEL,
file_name=path.name,
storage_path=str(path),
)
logger.info(
"Excel export generation finished",
extra={"batch_id": batch.pk, "export_id": exported.pk, "path": str(path)},
)
return exported

View File

@@ -0,0 +1,49 @@
from __future__ import annotations
from pathlib import Path
from review_agent.models import FileSummaryBatch, FileSummaryItem
SUPPORTED_EXTENSIONS = {"pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx"}
def _directory_level(relative_path: Path) -> str:
if len(relative_path.parts) <= 1:
return ""
return "/".join(relative_path.parts[:-1])
def scan_files_to_items(*, batch: FileSummaryBatch, roots: list[Path]) -> list[FileSummaryItem]:
files: list[tuple[Path, Path]] = []
for root in roots:
root = Path(root)
if root.is_file():
files.append((root.parent, root))
continue
for path in sorted(item for item in root.rglob("*") if item.is_file()):
if path.name.startswith(".") or path.stat().st_size == 0:
continue
files.append((root, path))
created: list[FileSummaryItem] = []
for index, (root, path) in enumerate(files, start=1):
relative = path.relative_to(root).as_posix()
file_type = path.suffix.lower().lstrip(".")
item = FileSummaryItem.objects.create(
batch=batch,
file_index=index,
directory_level=_directory_level(Path(relative)),
file_name=path.name,
file_type=file_type,
relative_path=relative,
storage_path=str(path),
statistics_status=FileSummaryItem.StatisticsStatus.SKIPPED,
)
created.append(item)
batch.total_files = len(created)
batch.supported_files = sum(1 for item in created if item.file_type in SUPPORTED_EXTENSIONS)
batch.unsupported_files = len(created) - batch.supported_files
batch.save(update_fields=["total_files", "supported_files", "unsupported_files"])
return created

View File

@@ -0,0 +1,282 @@
from __future__ import annotations
import logging
from dataclasses import dataclass
from pathlib import Path
from xml.etree import ElementTree
from zipfile import ZipFile, is_zipfile
SUPPORTED_EXTENSIONS = {"pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx"}
logger = logging.getLogger("review_agent.file_summary.page_count")
@dataclass(frozen=True)
class PageCountResult:
status: str
page_count: int | None = None
error_message: str = ""
def count_document_pages(path: str | Path) -> PageCountResult:
file_path = Path(path)
ext = file_path.suffix.lower().lstrip(".")
if ext not in SUPPORTED_EXTENSIONS:
return PageCountResult(status="unsupported")
try:
if ext == "pdf":
from pypdf import PdfReader
return PageCountResult(status="success", page_count=len(PdfReader(str(file_path)).pages))
if ext == "docx":
pages = _count_docx_pages_from_extended_properties(file_path)
if pages:
return PageCountResult(status="success", page_count=pages)
pages = _count_word_pages_with_com(file_path) if _can_try_com_fallback(file_path, ext) else None
if pages:
return PageCountResult(status="success", page_count=pages)
return PageCountResult(status="uncertain")
if ext == "xlsx":
pages = _count_xlsx_sheets(file_path) or (
_count_excel_sheets_with_com(file_path) if _can_try_com_fallback(file_path, ext) else None
)
if pages:
return PageCountResult(status="success", page_count=pages)
return PageCountResult(status="uncertain")
if ext == "xls":
pages = _count_xls_sheets(file_path) or (
_count_excel_sheets_with_com(file_path) if _can_try_com_fallback(file_path, ext) else None
)
if pages:
return PageCountResult(status="success", page_count=pages)
return PageCountResult(status="uncertain")
if ext == "pptx":
pages = _count_pptx_slides(file_path) or (
_count_powerpoint_slides_with_com(file_path) if _can_try_com_fallback(file_path, ext) else None
)
if pages:
return PageCountResult(status="success", page_count=pages)
return PageCountResult(status="uncertain")
if ext == "doc":
pages = _count_word_pages_with_com(file_path) if _can_try_com_fallback(file_path, ext) else None
if pages:
return PageCountResult(status="success", page_count=pages)
return _ole_uncertain_or_failed(file_path)
if ext == "ppt":
pages = _count_powerpoint_slides_with_com(file_path) if _can_try_com_fallback(file_path, ext) else None
if pages:
return PageCountResult(status="success", page_count=pages)
return _ole_uncertain_or_failed(file_path)
except Exception as exc:
return PageCountResult(status="failed", error_message=str(exc))
return PageCountResult(status="uncertain")
def _count_docx_pages_from_extended_properties(path: Path) -> int | None:
try:
with ZipFile(path) as archive:
app_entries = [
item for item in archive.infolist() if item.filename == "docProps/app.xml"
]
if not app_entries:
return None
content = archive.read(app_entries[-1]).decode("utf-8", errors="replace")
except Exception as exc:
logger.warning("DOCX extended properties read failed", extra={"path": str(path), "error": str(exc)})
return None
try:
root = ElementTree.fromstring(content)
except ElementTree.ParseError as exc:
logger.warning("DOCX extended properties parse failed", extra={"path": str(path), "error": str(exc)})
return None
pages_node = root.find("{http://schemas.openxmlformats.org/officeDocument/2006/extended-properties}Pages")
if pages_node is None or not pages_node.text:
return None
return _positive_int(pages_node.text)
def _count_xlsx_sheets(path: Path) -> int | None:
try:
from openpyxl import load_workbook
workbook = load_workbook(str(path), read_only=True, data_only=True)
try:
return _positive_int(len(workbook.sheetnames))
finally:
workbook.close()
except Exception as exc:
logger.warning("XLSX sheet count failed", extra={"path": str(path), "error": str(exc)})
return None
def _count_xls_sheets(path: Path) -> int | None:
try:
import xlrd
workbook = xlrd.open_workbook(str(path), on_demand=True)
try:
return _positive_int(workbook.nsheets)
finally:
workbook.release_resources()
except Exception as exc:
logger.warning("XLS sheet count failed", extra={"path": str(path), "error": str(exc)})
return None
def _count_pptx_slides(path: Path) -> int | None:
try:
from pptx import Presentation
return _positive_int(len(Presentation(str(path)).slides))
except Exception as exc:
logger.warning("PPTX slide count failed", extra={"path": str(path), "error": str(exc)})
return None
def _ole_uncertain_or_failed(path: Path) -> PageCountResult:
try:
import olefile
if olefile.isOleFile(str(path)):
return PageCountResult(status="uncertain")
return PageCountResult(status="failed", error_message="不是有效的 OLE 文件。")
except Exception as exc:
logger.warning("OLE validation failed", extra={"path": str(path), "error": str(exc)})
return PageCountResult(status="uncertain")
def _can_try_com_fallback(path: Path, ext: str) -> bool:
if ext in {"docx", "xlsx", "pptx"}:
return is_zipfile(path)
if ext in {"doc", "xls", "ppt"}:
try:
import olefile
return olefile.isOleFile(str(path))
except Exception as exc:
logger.warning("OLE signature check failed", extra={"path": str(path), "error": str(exc)})
return False
return False
def _count_word_pages_with_com(path: Path) -> int | None:
try:
import pythoncom
import win32com.client
except Exception as exc:
logger.info("Word COM page count unavailable", extra={"path": str(path), "error": str(exc)})
return None
word = None
document = None
pythoncom.CoInitialize()
try:
word = win32com.client.DispatchEx("Word.Application")
word.Visible = False
word.DisplayAlerts = 0
document = word.Documents.Open(
str(path.resolve()),
ReadOnly=True,
AddToRecentFiles=False,
ConfirmConversions=False,
)
document.Repaginate()
return _positive_int(document.ComputeStatistics(2))
except Exception as exc:
logger.warning("Word COM page count failed", extra={"path": str(path), "error": str(exc)})
return None
finally:
try:
if document is not None:
document.Close(False)
except Exception as exc:
logger.debug("Word document close failed", extra={"path": str(path), "error": str(exc)})
try:
if word is not None:
word.Quit()
except Exception as exc:
logger.debug("Word application quit failed", extra={"path": str(path), "error": str(exc)})
pythoncom.CoUninitialize()
def _count_powerpoint_slides_with_com(path: Path) -> int | None:
try:
import pythoncom
import win32com.client
except Exception as exc:
logger.info("PowerPoint COM slide count unavailable", extra={"path": str(path), "error": str(exc)})
return None
powerpoint = None
presentation = None
pythoncom.CoInitialize()
try:
powerpoint = win32com.client.DispatchEx("PowerPoint.Application")
presentation = powerpoint.Presentations.Open(
str(path.resolve()),
ReadOnly=True,
Untitled=False,
WithWindow=False,
)
return _positive_int(presentation.Slides.Count)
except Exception as exc:
logger.warning("PowerPoint COM slide count failed", extra={"path": str(path), "error": str(exc)})
return None
finally:
try:
if presentation is not None:
presentation.Close()
except Exception as exc:
logger.debug("PowerPoint presentation close failed", extra={"path": str(path), "error": str(exc)})
try:
if powerpoint is not None:
powerpoint.Quit()
except Exception as exc:
logger.debug("PowerPoint application quit failed", extra={"path": str(path), "error": str(exc)})
pythoncom.CoUninitialize()
def _count_excel_sheets_with_com(path: Path) -> int | None:
try:
import pythoncom
import win32com.client
except Exception as exc:
logger.info("Excel COM sheet count unavailable", extra={"path": str(path), "error": str(exc)})
return None
excel = None
workbook = None
pythoncom.CoInitialize()
try:
excel = win32com.client.DispatchEx("Excel.Application")
excel.Visible = False
excel.DisplayAlerts = False
workbook = excel.Workbooks.Open(str(path.resolve()), ReadOnly=True)
return _positive_int(workbook.Worksheets.Count)
except Exception as exc:
logger.warning("Excel COM sheet count failed", extra={"path": str(path), "error": str(exc)})
return None
finally:
try:
if workbook is not None:
workbook.Close(False)
except Exception as exc:
logger.debug("Excel workbook close failed", extra={"path": str(path), "error": str(exc)})
try:
if excel is not None:
excel.Quit()
except Exception as exc:
logger.debug("Excel application quit failed", extra={"path": str(path), "error": str(exc)})
pythoncom.CoUninitialize()
def _positive_int(value) -> int | None:
try:
number = int(value)
except (TypeError, ValueError):
return None
return number if number > 0 else None

View File

@@ -0,0 +1,31 @@
from __future__ import annotations
from pathlib import Path
from review_agent.models import FileSummaryBatch
def detect_product_name(batch: FileSummaryBatch) -> str:
product_name = ""
for item in batch.items.order_by("file_index"):
parts = Path(item.relative_path).parts
if len(parts) > 1:
product_name = parts[0]
break
name = Path(item.file_name).stem
for keyword in ("产品", "试剂盒", "说明书"):
if keyword in name:
product_name = name
break
if product_name:
break
if not product_name:
return ""
batch.product_name = product_name
batch.save(update_fields=["product_name"])
if batch.conversation.title.startswith("新对话"):
batch.conversation.title = f"{product_name}-文件汇总"
batch.conversation.save(update_fields=["title", "updated_at"])
return product_name

View File

@@ -0,0 +1,79 @@
from __future__ import annotations
import logging
from pathlib import Path
from django.conf import settings
from review_agent.models import ExportedSummaryFile, FileSummaryBatch
logger = logging.getLogger("review_agent.file_summary.report")
def _exports_dir(batch: FileSummaryBatch) -> Path:
root = Path(batch.work_dir) if batch.work_dir else Path(settings.MEDIA_ROOT) / "file_summary" / batch.batch_no
export_dir = root / "exports"
export_dir.mkdir(parents=True, exist_ok=True)
return export_dir
def build_summary_table(batch: FileSummaryBatch) -> str:
lines = [
"| 序号 | 目录层级 | 文件名 | 类型 | 页数 | 状态 | 异常说明 |",
"| --- | --- | --- | --- | --- | --- | --- |",
]
for item in batch.items.order_by("file_index"):
lines.append(
"| {index} | {directory} | {name} | {file_type} | {pages} | {status} | {error} |".format(
index=item.file_index,
directory=item.directory_level or "-",
name=item.file_name,
file_type=item.file_type,
pages=item.page_count if item.page_count is not None else "-",
status=item.statistics_status,
error=item.error_message or "-",
)
)
return "\n".join(lines)
def build_markdown_report(batch: FileSummaryBatch) -> str:
return "\n\n".join(
[
f"# 文件目录与页数汇总报告\n\n批次号:{batch.batch_no}",
(
"## 汇总信息\n\n"
f"- 产品名称:{batch.product_name or '-'}\n"
f"- 文件总数:{batch.total_files}\n"
f"- 统计成功:{batch.success_files}\n"
f"- 统计失败:{batch.failed_files}\n"
f"- 不支持:{batch.unsupported_files}\n"
f"- 不确定:{batch.uncertain_files}\n"
f"- 总页数:{batch.total_pages}"
),
"## 文件明细\n\n" + build_summary_table(batch),
"## 处理说明\n\n单文件失败不会阻断批次,失败与不确定文件已在明细中标注。",
]
)
def generate_markdown_report(batch: FileSummaryBatch) -> tuple[ExportedSummaryFile, str]:
logger.info("Markdown report generation started", extra={"batch_id": batch.pk})
content = build_markdown_report(batch)
path = _exports_dir(batch) / f"{batch.batch_no}-summary.md"
path.write_text(content, encoding="utf-8")
exported = ExportedSummaryFile.objects.create(
batch=batch,
workflow_type="file_summary",
workflow_batch_id=batch.pk,
export_category="summary",
export_type=ExportedSummaryFile.ExportType.MARKDOWN,
file_name=path.name,
storage_path=str(path),
)
logger.info(
"Markdown report generation finished",
extra={"batch_id": batch.pk, "export_id": exported.pk, "path": str(path)},
)
return exported, build_summary_table(batch)

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,69 @@
from __future__ import annotations
import logging
from pathlib import Path
import re
from review_agent.models import FileSummaryBatchAttachment
from ..paths import resolve_storage_path
from ..services.archive import ARCHIVE_EXTENSIONS, extract_archive
from .base import BaseSkill, SkillResult, WorkflowContext
logger = logging.getLogger("review_agent.file_summary.skills.archive_extract")
def _safe_archive_dir_name(binding: FileSummaryBatchAttachment) -> str:
stem = Path(binding.attachment.original_name).stem or "archive"
safe_stem = re.sub(r"[^A-Za-z0-9._-]+", "_", stem).strip("._") or "archive"
return f"{binding.attachment_id}_{safe_stem}"
class ArchiveExtractSkill(BaseSkill):
name = "archive_extract"
def run(self, context: WorkflowContext) -> SkillResult:
extracted_count = 0
if not context.batch.work_dir:
message = "批次工作目录为空,无法解压压缩包。"
logger.error(
"Archive extract failed without work dir",
extra={"batch_id": context.batch.pk, "batch_no": context.batch.batch_no},
)
return SkillResult(success=False, message=message, data={"extracted_count": 0})
target_root = Path(context.batch.work_dir)
archive_count = 0
for binding in FileSummaryBatchAttachment.objects.filter(batch=context.batch):
path = resolve_storage_path(binding.attachment.storage_path)
if path.suffix.lower().lstrip(".") not in ARCHIVE_EXTENSIONS:
continue
archive_count += 1
target_dir = target_root / "extracted" / _safe_archive_dir_name(binding)
logger.info(
"Archive extract started",
extra={
"batch_id": context.batch.pk,
"attachment_id": binding.attachment_id,
"path": str(path),
"target_dir": str(target_dir),
},
)
extracted_count += len(extract_archive(path, target_dir))
if archive_count and extracted_count == 0:
message = "压缩包未解出任何可扫描文件,请检查压缩包内容或格式。"
logger.warning(
"Archive extract produced no files",
extra={"batch_id": context.batch.pk, "archive_count": archive_count},
)
return SkillResult(success=False, message=message, data={"extracted_count": 0})
logger.info(
"Archive extract finished",
extra={
"batch_id": context.batch.pk,
"archive_count": archive_count,
"extracted_count": extracted_count,
},
)
return SkillResult(success=True, data={"extracted_count": extracted_count})

View File

@@ -0,0 +1,52 @@
from __future__ import annotations
import logging
from collections.abc import Iterable
from review_agent.models import FileAttachment
from ..services.attachment_reader import read_attachment_details
from .base import BaseSkill, SkillResult, WorkflowContext
logger = logging.getLogger("review_agent.file_summary.skills.attachment_reader")
class AttachmentReaderSkill(BaseSkill):
name = "attachment_reader"
def run(self, context: WorkflowContext) -> SkillResult:
attachments = FileAttachment.objects.filter(
conversation=context.batch.conversation,
is_active=True,
).exclude(upload_status=FileAttachment.UploadStatus.DELETED)
return self.run_for_attachments(attachments)
def run_for_attachments(self, attachments: Iterable[FileAttachment]) -> SkillResult:
attachment_list = list(attachments)
logger.info(
"Attachment reader skill started",
extra={
"attachment_count": len(attachment_list),
"attachment_ids": [attachment.pk for attachment in attachment_list],
},
)
results = [read_attachment_details(attachment).to_dict() for attachment in attachment_list]
if not results:
logger.warning("Attachment reader skill found no attachments")
return SkillResult(success=False, message="当前对话没有可读取的附件。")
has_success = any(item["status"] == "success" for item in results)
logger.info(
"Attachment reader skill finished",
extra={
"success": has_success,
"success_count": sum(1 for item in results if item["status"] == "success"),
"failed_count": sum(1 for item in results if item["status"] != "success"),
},
)
return SkillResult(
success=has_success,
data={"attachments": results},
message="附件解析完成。" if has_success else "附件解析失败。",
)

View File

@@ -0,0 +1,24 @@
from __future__ import annotations
from dataclasses import dataclass, field
from review_agent.models import FileSummaryBatch
@dataclass(frozen=True)
class WorkflowContext:
batch: FileSummaryBatch
@dataclass
class SkillResult:
success: bool
data: dict = field(default_factory=dict)
message: str = ""
class BaseSkill:
name = ""
def run(self, context: WorkflowContext) -> SkillResult:
raise NotImplementedError

View File

@@ -0,0 +1,108 @@
from __future__ import annotations
import logging
from review_agent.models import FileSummaryItem
from ..services.page_count import SUPPORTED_EXTENSIONS, count_document_pages
from .base import BaseSkill, SkillResult, WorkflowContext
logger = logging.getLogger("review_agent.file_summary.skills.document_page_count")
class DocumentPageCountSkill(BaseSkill):
name = "document_page_count"
def run(self, context: WorkflowContext) -> SkillResult:
success_files = failed_files = unsupported_files = uncertain_files = total_pages = 0
logger.info("Document page count started", extra={"batch_id": context.batch.pk})
for item in context.batch.items.order_by("file_index"):
if item.file_type not in SUPPORTED_EXTENSIONS:
item.statistics_status = FileSummaryItem.StatisticsStatus.UNSUPPORTED
unsupported_files += 1
item.save(update_fields=["statistics_status", "updated_at"])
logger.info(
"Document page count unsupported",
extra={
"batch_id": context.batch.pk,
"item_id": item.pk,
"file_type": item.file_type,
"file_name": item.file_name,
},
)
continue
result = None
for attempt in range(1, 4):
logger.info(
"Document page count attempt",
extra={
"batch_id": context.batch.pk,
"item_id": item.pk,
"attempt": attempt,
"storage_path": item.storage_path,
},
)
result = count_document_pages(item.storage_path)
item.retry_count = attempt - 1
if result.status != "failed":
break
item.statistics_status = result.status
item.page_count = result.page_count
item.error_message = result.error_message
item.save(
update_fields=[
"statistics_status",
"page_count",
"retry_count",
"error_message",
"updated_at",
]
)
if result.status == FileSummaryItem.StatisticsStatus.SUCCESS:
success_files += 1
total_pages += result.page_count or 0
elif result.status == FileSummaryItem.StatisticsStatus.UNCERTAIN:
uncertain_files += 1
elif result.status == FileSummaryItem.StatisticsStatus.UNSUPPORTED:
unsupported_files += 1
else:
failed_files += 1
logger.warning(
"Document page count failed",
extra={
"batch_id": context.batch.pk,
"item_id": item.pk,
"file_name": item.file_name,
"error": result.error_message,
},
)
context.batch.success_files = success_files
context.batch.failed_files = failed_files
context.batch.unsupported_files = unsupported_files
context.batch.uncertain_files = uncertain_files
context.batch.total_pages = total_pages
context.batch.save(
update_fields=[
"success_files",
"failed_files",
"unsupported_files",
"uncertain_files",
"total_pages",
]
)
logger.info(
"Document page count finished",
extra={
"batch_id": context.batch.pk,
"success_files": success_files,
"failed_files": failed_files,
"unsupported_files": unsupported_files,
"uncertain_files": uncertain_files,
"total_pages": total_pages,
},
)
return SkillResult(success=True)

View File

@@ -0,0 +1,69 @@
from __future__ import annotations
import logging
from pathlib import Path
import re
from review_agent.models import FileSummaryBatchAttachment
from ..paths import resolve_storage_path
from ..services.archive import ARCHIVE_EXTENSIONS
from ..services.inventory import scan_files_to_items
from .base import BaseSkill, SkillResult, WorkflowContext
logger = logging.getLogger("review_agent.file_summary.skills.file_inventory")
def _safe_archive_dir_name(binding: FileSummaryBatchAttachment) -> str:
stem = Path(binding.attachment.original_name).stem or "archive"
safe_stem = re.sub(r"[^A-Za-z0-9._-]+", "_", stem).strip("._") or "archive"
return f"{binding.attachment_id}_{safe_stem}"
class FileInventorySkill(BaseSkill):
name = "file_inventory"
def run(self, context: WorkflowContext) -> SkillResult:
roots: list[Path] = []
missing_extract_roots: list[str] = []
for binding in FileSummaryBatchAttachment.objects.filter(batch=context.batch):
original_path = resolve_storage_path(binding.attachment.storage_path)
is_archive = original_path.suffix.lower().lstrip(".") in ARCHIVE_EXTENSIONS
if not is_archive:
roots.append(original_path)
continue
extracted_root = (
Path(context.batch.work_dir)
/ "extracted"
/ _safe_archive_dir_name(binding)
)
if extracted_root.exists():
roots.append(extracted_root)
else:
missing_extract_roots.append(str(extracted_root))
if missing_extract_roots:
message = "压缩包解压目录不存在,无法扫描解压后的文件。"
logger.warning(
"File inventory missing extracted roots",
extra={
"batch_id": context.batch.pk,
"missing_extract_roots": missing_extract_roots,
},
)
return SkillResult(success=False, message=message)
logger.info(
"File inventory started",
extra={
"batch_id": context.batch.pk,
"root_count": len(roots),
"roots": [str(root) for root in roots],
},
)
items = scan_files_to_items(batch=context.batch, roots=roots)
logger.info(
"File inventory finished",
extra={"batch_id": context.batch.pk, "total_files": len(items)},
)
return SkillResult(success=True, data={"total_files": len(items)})

View File

@@ -0,0 +1,22 @@
from __future__ import annotations
import logging
from ..services.product_detect import detect_product_name
from .base import BaseSkill, SkillResult, WorkflowContext
logger = logging.getLogger("review_agent.file_summary.skills.product_detect")
class ProductDetectSkill(BaseSkill):
name = "product_detect"
def run(self, context: WorkflowContext) -> SkillResult:
logger.info("Product detect started", extra={"batch_id": context.batch.pk})
product_name = detect_product_name(context.batch)
logger.info(
"Product detect finished",
extra={"batch_id": context.batch.pk, "product_name": product_name},
)
return SkillResult(success=True, data={"product_name": product_name})

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