Compare commits

...

96 Commits

Author SHA1 Message Date
1f5f0a968b fix(chat): remove workspace hero card 2026-06-04 23:05:13 +08:00
f2c1e3cfa1 docs(config): add siliconflow demo env template 2026-06-04 22:56:38 +08:00
5a6e7698e4 fix(chat): simplify header cards and llm fallback 2026-06-04 22:42:39 +08:00
fecaee0b03 feat(chat): refine workspace chat layout 2026-06-04 22:30:37 +08:00
efb06519d8 feat(chat): allow knowledge chat before upload 2026-06-04 22:16:54 +08:00
zhiye.sun
1d8a526770 docs(demo-agent): 同步当前实现状态与协作约定 2026-06-04 16:38:18 +08:00
5aa7b5f3d0 fix(routing): 默认进入审核聊天工作台 2026-06-04 08:47:27 +08:00
e701b4502e refactor: 下沉 Word 导出执行链到 chat 服务层 2026-06-04 04:44:47 +08:00
47f887647b test: 补强审核智能体关键上下文字段证据 2026-06-04 04:41:09 +08:00
1b6a09f786 test: 补强资料包关联会话跳转证据 2026-06-04 04:38:33 +08:00
80dc10ce6d refactor: 下沉资料包列表上下文到 documents 服务层 2026-06-04 04:36:41 +08:00
a49524fd93 refactor: 下沉处理历史筛选到 audit 服务层 2026-06-04 04:34:20 +08:00
1b6c54fe78 refactor: 下沉会话执行编排到 chat 服务层 2026-06-04 04:31:55 +08:00
de2bd2956f style: 清理平台页面旧工作台叙事 2026-06-04 04:28:36 +08:00
b30ba19dcc feat: 收口审核指挥台旧入口到 Agent 原型页 2026-06-04 04:26:15 +08:00
961615b88c style: 收口审核指挥台四入口主导航 2026-06-04 04:24:32 +08:00
3ffb6f23b0 style: 统一审核指挥台四入口标签口径 2026-06-04 04:20:19 +08:00
1e08f24cd5 fix: 修复审核指挥台智能审核入口路由 2026-06-04 04:17:00 +08:00
47ca116937 style: 统一飞书通知节点状态语义 2026-06-04 04:11:00 +08:00
87f674cece style: 统一审核智能体状态展示口径 2026-06-04 04:00:44 +08:00
8ec254c393 style: 统一处理历史状态展示口径 2026-06-04 03:57:03 +08:00
7c0dfe14d5 feat: 动态生成资料包异常提示 2026-06-04 03:50:12 +08:00
dc86fc0e58 feat: 收口通知原因语义与留痕校验 2026-06-04 03:44:04 +08:00
742d5e9a42 feat: 增强会话历史风险与绑定状态展示 2026-06-04 03:38:51 +08:00
9bca08001f feat: 补齐DOCX精确页数识别与待复核策略 2026-06-04 03:35:34 +08:00
e9cf964a3f docs: 同步四入口产品形态与知识库入口口径 2026-06-04 03:30:03 +08:00
5d1598f5c1 style: 统一四入口导航当前态与首页入口命名 2026-06-04 03:27:16 +08:00
7b5d968fb4 style: 收口首页四入口主叙事与场景配置层级 2026-06-04 03:23:50 +08:00
e3a54b874d feat: 增强知识库治理台动作总览与通知策略展示 2026-06-04 03:19:50 +08:00
b2af88f870 feat: 增强资料包页导出回看与处理链路展示 2026-06-04 03:15:56 +08:00
5a4a176108 feat: 增强处理历史页指标与上下文跳转 2026-06-04 03:13:28 +08:00
0f989f4c95 feat: 补齐风险卡责任人飞书字段展示 2026-06-04 03:09:13 +08:00
b8501a680d feat: 增强审核智能体页完整性与字段抽取能力卡展示 2026-06-04 03:07:34 +08:00
af36ec460d feat: 增强审核智能体页目录与一致性能力卡展示 2026-06-04 03:03:37 +08:00
a7cee4aa27 feat: 增强审核智能体页顶部上下文与提问模板 2026-06-04 02:59:17 +08:00
5fdcc31c74 feat: 增强审核智能体页风险与通知能力卡展示 2026-06-04 02:54:39 +08:00
20f3883b8c feat: 增强审核智能体页Word导出能力卡展示 2026-06-04 02:49:27 +08:00
08dcb62834 feat: 补齐Word导出报告结构化字段口径 2026-06-04 02:44:50 +08:00
ab3d520642 feat: 持久化资料包导出记录与列表摘要 2026-06-04 02:41:49 +08:00
0e49466746 feat: 增强处理历史导出记录摘要展示 2026-06-04 02:35:23 +08:00
e9b3f13eec feat: 增强知识库治理台前台入口与维护导航 2026-06-04 02:33:17 +08:00
0250bd360a feat: 打通Word导出文件生成与下载闭环 2026-06-04 02:28:42 +08:00
e81f0f891e feat: 明确导出报告正式导出口径 2026-06-04 02:18:36 +08:00
5808dd794d feat: 支持治理配置通过admin维护 2026-06-04 02:14:07 +08:00
0d4e02b9dc feat: 对齐导出节点口径与下载信息展示 2026-06-04 02:07:58 +08:00
007bf43e15 feat: 支持rar资料包导入 2026-06-04 02:04:27 +08:00
3280186625 feat: 打通通知回执与消息状态留痕 2026-06-04 02:00:41 +08:00
a663543b37 feat: 增强处理历史详情导出与通知回执展示 2026-06-04 01:57:21 +08:00
d2a4907561 feat: 增强处理历史资料规模与会话状态展示 2026-06-04 01:54:11 +08:00
96f710ea13 feat: 支持会话内补传资料并保持绑定 2026-06-04 01:51:48 +08:00
1e18fd2be9 feat: 统一治理配置与通知责任人映射 2026-06-04 01:45:27 +08:00
4914ee3a75 feat: 收口知识库治理配置与模板映射 2026-06-04 01:42:12 +08:00
0b7322aa65 feat: 增强资料包压缩导入异常提示 2026-06-04 01:37:11 +08:00
e7e3202714 feat: 支持处理历史按风险状态筛选 2026-06-04 01:32:35 +08:00
73c6336600 feat: 支持7z资料包导入 2026-06-04 01:28:28 +08:00
24446658ad feat: 增强处理历史风险与通知状态展示 2026-06-04 01:21:02 +08:00
72409e9652 feat: 打通通知回链与历史节点回看 2026-06-04 01:15:44 +08:00
11caa6c908 feat: 增强注册审核节点式结果输出 2026-06-04 01:10:40 +08:00
aa0a24fe5a feat: 支持资料包多文件与zip导入 2026-06-04 01:07:15 +08:00
2b40ddc487 feat: 持久化会话节点结果与失败通知留痕 2026-06-04 01:02:06 +08:00
b8381b3ba1 style: 对齐审核智能体节点与信息卡原型 2026-06-04 00:58:11 +08:00
cac9a4aaeb feat: 收口知识库治理入口与配置口径 2026-06-04 00:54:51 +08:00
77d9420d43 feat: 重构处理历史与通知留痕追踪 2026-06-04 00:49:33 +08:00
d0841e533f feat: 重构资料包模型与会话绑定主链路 2026-06-04 00:43:13 +08:00
ddf5e7d15c docs(清理): 移除废弃的superpowers过程文档 2026-06-04 00:23:37 +08:00
7f96e94c21 docs(设计对齐): 统一飞书责任人与通知口径 2026-06-04 00:16:42 +08:00
89ea02ee33 docs(原型设计): 统一分页节点视图表述 2026-06-04 00:14:17 +08:00
70d58c62fe docs(原型设计): 重构Agent化原型文档主线 2026-06-04 00:11:33 +08:00
b489dc1b37 docs(需求分析): 同步Agent原型与资料包会话绑定 2026-06-04 00:07:20 +08:00
fe7f1e8855 docs(详细设计): 按Agent原型重写流程设计 2026-06-03 23:58:42 +08:00
ea9ad57a5f docs(需求分析): 按Agent原型重写核心需求 2026-06-03 23:55:27 +08:00
7836690303 feat(原型设计): 关联资料包与对话记录 2026-06-03 23:48:53 +08:00
aa5d4d77f8 refactor(原型设计): 调整用户信息展示位置 2026-06-03 23:44:40 +08:00
12a92ad278 refactor(原型设计): 调整导航并合并法规依据入口 2026-06-03 23:43:16 +08:00
134e0fb5ff feat(原型设计): 精简导航并补充资料与法规页 2026-06-03 23:32:56 +08:00
f53d5a1902 refactor(原型设计): 简化顶部导航结构 2026-06-03 23:28:26 +08:00
c303a2fcc6 style(原型设计): 调整为蓝白助手工作台风格 2026-06-03 23:24:16 +08:00
e75f0a0356 refactor(原型设计): 重构为Agent对话优先原型 2026-06-03 23:15:24 +08:00
3da774e537 feat(原型设计): 新增知识库管理与流程状态卡片 2026-06-03 23:00:22 +08:00
54fc1baa4c feat(原型设计): 增强强Agent对话工作台原型 2026-06-03 22:31:23 +08:00
5a137b5b45 fix(原型设计): 修复演示站内容空白问题 2026-06-03 22:01:00 +08:00
08251ae5e6 docs(原型设计): 同步原型交付入口索引 2026-06-03 21:44:31 +08:00
cace6cb941 feat(原型设计): 新增注册审核演示站 HTML 原型 2026-06-03 21:41:27 +08:00
78b841131b docs(原型设计): 补充注册审核平台分页原型方案 2026-06-03 21:34:36 +08:00
7a60af0485 docs(详细设计): 新增飞书通知设计 2026-06-03 21:15:34 +08:00
cc200a32c4 docs(详细设计): 新增Word回填导出设计 2026-06-03 21:12:04 +08:00
2876a1b028 docs(详细设计): 新增风险预警设计 2026-06-03 21:08:15 +08:00
0e49eea683 docs(详细设计): 新增一致性核查设计 2026-06-03 21:04:54 +08:00
4208f29d77 docs(详细设计): 新增字段抽取与字段池设计 2026-06-03 21:00:28 +08:00
759939b446 docs(详细设计): 新增法规完整性检查设计 2026-06-03 20:55:38 +08:00
18428e75fd docs(详细设计): 新增资料包导入与目录汇总设计 2026-06-03 20:50:27 +08:00
zhiye.sun
11c20593d5 docs(requirements): 明确核心信息自动回填目标 2026-06-03 14:13:53 +08:00
zhiye.sun
56a332a7dd docs(requirements): 固化资料包解析确认口径 2026-06-03 14:10:20 +08:00
zhiye.sun
5125f79037 feat(platform-ui): 新增审核指挥台V2原型 2026-06-03 14:01:23 +08:00
zhiye.sun
d670c51d43 chore(registration): 更新注册审核场景输出契约 2026-06-03 14:00:58 +08:00
zhiye.sun
4017151218 docs(requirements): 梳理注册资料审核Agent需求 2026-06-03 14:00:33 +08:00
142 changed files with 18537 additions and 2821 deletions

21
.env.siliconflow.example Normal file
View File

@@ -0,0 +1,21 @@
DJANGO_SECRET_KEY=replace-with-a-local-secret-key
DJANGO_DEBUG=true
DJANGO_ALLOWED_HOSTS=*
# SiliconFlow OpenAI-compatible API
# Fill these two keys manually before demo.
LLM_PROVIDER=openai_compatible
LLM_API_KEY=your_siliconflow_api_key
LLM_BASE_URL=https://api.siliconflow.cn/v1
LLM_MODEL=Qwen/Qwen2.5-7B-Instruct
# SiliconFlow embedding model for RAG.
# You can reuse the same SiliconFlow key here.
EMBEDDING_API_KEY=your_siliconflow_api_key
EMBEDDING_BASE_URL=https://api.siliconflow.cn/v1
EMBEDDING_MODEL=BAAI/bge-m3
SCENARIO_CONFIG_DIR=configs
GOVERNANCE_CONFIG_PATH=configs/governance.yaml
UPLOAD_ROOT=data/uploads
CHROMA_PATH=data/chroma

111
AGENTS.md
View File

@@ -1,6 +1,6 @@
# AGENTS.md # AGENTS.md
本文档约定本项目后续由人或编码 Agent 协作开发时需要遵守的边界、风格实现顺序。 本文档约定本项目后续由人或编码 Agent 协作开发时需要遵守的边界、风格实现顺序和文档同步要求
## 项目定位 ## 项目定位
@@ -13,8 +13,8 @@
优先目标: 优先目标:
- 围绕 NMPA 体外诊断试剂注册申报资料场景完成可演示闭环。 - 围绕 NMPA 体外诊断试剂注册申报资料场景完成可演示闭环。
- 保证本地可运行。 - 保证本地可运行、可测试、可讲解
- 保证代码结构清楚,方便讲解 - 保证代码结构清楚,业务流程能从页面、服务层、Agent Core 和审计日志串起来
- 允许在保留主架构边界前提下进行大幅度业务重构。 - 允许在保留主架构边界前提下进行大幅度业务重构。
## 架构原则 ## 架构原则
@@ -27,59 +27,68 @@ Django 单体 + 独立 Agent Core + Docker Compose
核心边界: 核心边界:
- Django 负责页面、数据库、文件上传、审计日志和后台管理。 - Django 负责页面、数据库、文件上传、导出文件、审计日志、通知留痕和后台管理。
- Agent Core 负责 RAG、Prompt、工具调用、模型适配和结构化输出。 - Agent Core 负责 RAG、Prompt、工具调用、治理配置、模型适配和结构化输出。
- Django View 不直接写大模型调用、向量检索和工具执行细节。 - Django View 不直接写大模型调用、向量检索和工具执行细节。
- Agent Core 不依赖 Django View。 - Agent Core 不依赖 Django View。
- 业务流程优先放在各模块 `services.py`View 只负责请求处理、消息提示和模板渲染。
## 模块边界 ## 模块边界
### config ### config
负责 Django 项目配置、URL 总入口、环境变量、静态资源、上传路径和部署配置。 负责 Django 项目配置、URL 总入口、环境变量、静态资源、上传路径、Chroma 路径和部署配置。
### apps.scenarios ### apps.scenarios
负责注册审核任务列表、任务配置读取、任务元信息展示。 负责注册审核任务配置读取、场景元信息展示和非法 YAML 配置容错
### apps.documents ### apps.documents
负责注册资料上传文件记录、章节点归类、页数与文本处理状态和触发 RAG 入库。 负责资料包导入、上传文件记录、压缩包展开、文本抽取、章节点归类、页数统计、资料包搜索、异常提示、触发 RAG 入库和导出记录维护
### apps.chat ### apps.chat
负责审核工作台、用户输入表单、调用 Agent Core展示结构化审核结果。 负责审核工作台、会话列表、用户输入表单、文档范围选择、调用 Agent Core展示结构化审核结果、补传资料和触发 Word 导出
### apps.audit ### apps.audit
负责审计日志模型、日志写入服务、日志列表和详情页,以及审核留痕展示。 负责审计日志模型、日志写入服务、通知留痕、处理历史列表和详情页,以及审核留痕展示。
### apps.platform_ui
负责知识库治理台、MCP 中心、Skill 工作室和审核指挥台等演示型平台页面。该模块可以展示治理对象和 mock 业务态势,但不要把主业务执行逻辑写进这里。
### agent_core ### agent_core
负责注册审核 Agent 编排、RAG、工具注册、规则执行、LLM Provider 和结构化输出。 负责注册审核 Agent 编排、RAG、工具注册、治理配置读取、LLM / Embedding Provider 和结构化输出。
## 开发顺序 ## 当前实现状态
建议按以下顺序推进: - Django 单体骨架已完成,根路径 `/` 默认进入审核智能体。
- 当前主入口为 `审核智能体 / 资料包 / 知识库治理台 / 处理历史`,底层场景列表保留在 `/scenarios/`
1. 创建 Django 项目骨架 - 通用场景 YAML、Chat、Documents、Audit、Platform UI 和 Agent Core 已具备可重构基础
2. 完成 Config 模块 - 资料包导入支持 PDF、DOCX、MD、TXT、ZIP、7Z、RAR
3. 完成 Scenarios 模块,先展示 5 个场景 - 资料包会自动绑定会话,标题优先使用解析出的产品名称
4. 完成 Agent Core 最小闭环,先返回模拟结果 - 审核智能体允许在未上传资料包时直接发起知识库问答,会话保持未绑定资料包状态并走 RAG 检索链路
5. 完成 Chat 页面,打通对话链路 - Agent Core 已具备 Prompt 编排、结构化解析、工具注册、RAG fallback / Chroma 双路径和 OpenAI 兼容 Provider
6. 完成 Audit 模块,记录每次对话 - Word 导出已支持生成最小 `.docx`,并按风险状态形成正式版或草稿版
7. 完成 Documents 模块,支持上传文件 - 飞书通知当前为离线通知留痕,不直接发送真实飞书消息
8. 完成 RAG 入库和检索。
9. 完成内置工具系统。
10. 补 Docker Compose 一键启动。
当前仓库状态说明:
- Django 单体骨架已完成。
- 通用场景 YAML、Chat、Documents、Audit 和 Agent Core 已具备可重构基础。
- Agent Core 已具备 Prompt 编排、结构化解析、工具注册和 RAG fallback / Chroma 双路径。
- 全量测试已覆盖主要模块行为,并默认隔离真实 LLM 网络调用。 - 全量测试已覆盖主要模块行为,并默认隔离真实 LLM 网络调用。
- 当前需求文档已按真实笔试题重写到 `docs/需求分析/` - 当前需求文档已按真实笔试题重写到 `docs/需求分析/`
- 当前详细设计文档放在 `docs/详细设计/`,原型资料放在 `docs/原型设计/`
## 推荐开发顺序
后续新增或重构功能时,建议按以下顺序推进:
1. 先确认需求文档、详细设计或当前页面是否需要同步调整。
2. 补或调整服务层测试、Agent Core 测试或页面关键展示测试。
3. 在对应模块的 `services.py``agent_core` 中实现核心逻辑。
4. View 只接入服务层结果,模板只做直接展示。
5. 若涉及用户可见入口同步更新模板、README 和相关需求/设计文档。
6. 运行相关测试,再运行核心回归验证。
7. 按逻辑分组使用 Conventional Commit 风格提交到本地。
## 编码约定 ## 编码约定
@@ -89,6 +98,7 @@ Django 单体 + 独立 Agent Core + Docker Compose
- 配置化优先,业务场景不要写死在代码中。 - 配置化优先,业务场景不要写死在代码中。
- 工具函数必须通过 Tool Registry 注册。 - 工具函数必须通过 Tool Registry 注册。
- 模型调用必须通过 LLM Provider不允许散落在业务代码中。 - 模型调用必须通过 LLM Provider不允许散落在业务代码中。
- RAG 入库、检索和 Embedding 逻辑必须留在 Agent Core 或 Documents 服务边界内。
- 审计日志要记录成功和失败两种情况。 - 审计日志要记录成功和失败两种情况。
- 不在日志中保存 API Key、密钥或敏感环境变量。 - 不在日志中保存 API Key、密钥或敏感环境变量。
- 新增或重构模块时,优先补清晰的中文注释,说明职责边界、输入输出和设计取舍。 - 新增或重构模块时,优先补清晰的中文注释,说明职责边界、输入输出和设计取舍。
@@ -99,20 +109,20 @@ Django 单体 + 独立 Agent Core + Docker Compose
需求文档放在: 需求文档放在:
```text
docs/
```
需求分析文档放在:
```text ```text
docs/需求分析/ docs/需求分析/
``` ```
设计文档放在: 详细设计文档放在:
```text ```text
docs/设计文档/ docs/详细设计/
```
原型设计文档放在:
```text
docs/原型设计/
``` ```
场景配置放在: 场景配置放在:
@@ -126,26 +136,28 @@ configs/
- `README.md` - `README.md`
- `docs/需求分析/1.V1总需求文档.md` - `docs/需求分析/1.V1总需求文档.md`
- 相关模块需求文档 - 相关模块需求文档
- 相关详细设计文档
- `AGENTS.md` 中的协作边界与当前实现状态 - `AGENTS.md` 中的协作边界与当前实现状态
推荐同步文档的场景: 推荐同步文档的场景:
- 新增用户可见页面或流程。 - 新增用户可见页面或流程。
- 调整环境变量、生效方式或部署命令。 - 调整根路径、URL、环境变量、生效方式或部署命令。
- 修改 Agent Core 的输入输出合约 - 修改资料包、会话、审计、通知或导出模型字段
- 新增工具、审计字段或场景配置字段 - 修改 Agent Core 的输入输出合约、结构化输出类型或节点状态口径
- 新增工具、治理配置字段、场景配置字段或模板映射字段。
- 改变测试隔离策略、真实模型调用策略或 Docker 启动方式。
## 测试与验证约定 ## 测试与验证约定
每个阶段至少验证: 每个阶段至少验证:
- Django 可以启动。 - Django 可以启动`python manage.py check` 通过
- 首页可以访问。 - 根路径和审核智能体页面可以访问。
- 场景列表可显示 - 资料包导入流程可执行
- 对话流程可执行。 - 对话流程可执行,出错时页面有清晰提示
- 出错时页面有清晰提示 - 审计日志能记录成功和失败
- 审计日志能记录 - Docker Compose 配置有效
- Docker Compose 可以启动。
当前默认验证命令: 当前默认验证命令:
@@ -159,6 +171,8 @@ docker compose config
- 若本地 `.env` 存在真实模型密钥,测试仍应保持可离线执行。 - 若本地 `.env` 存在真实模型密钥,测试仍应保持可离线执行。
- 每完成一项功能或一轮重构后,应先跑相关测试,再跑全量测试或核心回归测试。 - 每完成一项功能或一轮重构后,应先跑相关测试,再跑全量测试或核心回归测试。
- 涉及页面结构时,至少补或更新对应页面测试。
- 涉及导出文件时,需要验证导出记录和下载路径。
- 完成改动后,按逻辑分组使用 Conventional Commit 风格提交到本地。 - 完成改动后,按逻辑分组使用 Conventional Commit 风格提交到本地。
## 不优先做的事项 ## 不优先做的事项
@@ -172,5 +186,6 @@ docker compose config
- 深度 Dify 集成。 - 深度 Dify 集成。
- 微服务拆分。 - 微服务拆分。
- 分布式任务队列。 - 分布式任务队列。
- 真实飞书发送链路。
这些内容可以作为后续增强,不应影响 V1 快速成型。 这些内容可以作为后续增强,不应影响 V1 快速成型。

266
README.md
View File

@@ -2,12 +2,12 @@
用于复试展示的体外诊断试剂注册申报资料准备与审核系统。 用于复试展示的体外诊断试剂注册申报资料准备与审核系统。
当前项目已根据真实笔试题重构目标定位,重点服务于 NMPA 境内第三类体外诊断试剂注册申报场景,覆盖资料整理、目录汇总、法规完整性检查、关键信息抽取、跨文档一致性核查、风险预警和审计留痕 项目已真实笔试题收口为 NMPA 境内第三类体外诊断试剂注册申报资料场景,重点演示“资料包导入 -> 审核智能体执行 -> 结构化结果 -> Word 导出 -> 通知与审计留痕”的本地闭环
## 核心理念 ## 核心理念
```text ```text
注册审核 Agent = 任务配置 + 资料 + 法规规则 + 工具集 + 输出模板 + 审计日志 + 模型适配器 注册审核 Agent = 任务配置 + 资料 + 法规/业务知识库 + 工具集 + 输出模板 + 审计日志 + 模型适配器
``` ```
## 技术路线 ## 技术路线
@@ -17,23 +17,40 @@ V1 采用:
- Django 单体应用 - Django 单体应用
- 独立 Agent Core 模块 - 独立 Agent Core 模块
- SQLite - SQLite
- Chroma - Chroma / fallback 检索双路径
- Django Templates - Django Templates
- Docker Compose - Docker Compose
- OpenAI API 兼容的 LLM 与 Embedding 接口 - OpenAI API 兼容的 LLM 与 Embedding 接口
默认不强依赖 Dify。系统留 Adapter 设计,后续可接入 Dify、OpenAI Agents SDK 或其他 Agent 编排平台。 默认不强依赖 Dify。系统 Provider / Adapter 边界,后续可接入 Dify、OpenAI Agents SDK 或其他 Agent 编排平台。
## 当前业务主线 ## 当前业务主线
当前系统围绕以下注册申报审核闭环展开: 1. 进入审核智能体后,可以先不上传资料,直接通过对话查询法规和业务知识库。
2. 导入注册资料包,支持单文件、多文件和压缩包。
3. 解析文件元数据、页数、章节点和产品名称。
4. 自动创建资料包批次,并绑定审核会话。
5. 在审核智能体工作台选择文档范围并发起目录汇总、完整性检查、字段抽取、一致性核查或风险报告。
6. Agent Core 按场景配置执行 RAG 检索、工具调用、Prompt 编排、LLM 调用和结构化输出解析。
7. 会话页展示节点状态、能力卡、风险摘要、通知信息和导出入口。
8. Word 回填导出生成可下载 `.docx`,并记录到资料包和处理历史。
9. 审计模块保存成功与失败两类执行快照,并沉淀飞书通知留痕。
1. 导入注册资料。 ## 当前产品入口
2. 汇总文件目录与页数。
3. 对照法规要求检查完整性 当前根路径 `/` 会直接进入审核智能体工作台,便于复试演示聚焦主链路
4. 抽取产品关键信息。
5. 核查跨文档一致性。 | 页面 | 路径 | 当前能力 |
6. 输出风险预警与处理建议。 |---|---|---|
| 审核智能体 | `/``/chat/``/chat/<conversation_id>/` | 无资料包知识库问答、会话驱动审核、文档范围选择、节点式结果、能力卡、补传资料、Word 导出、通知与审计回看 |
| 资料包 | `/documents/``/documents/upload/` | 导入资料包、搜索产品或批次、查看解析状态、异常提示、最近导出和处理链路 |
| 处理历史 | `/audit/``/audit/<log_id>/` | 按批次、产品、风险状态、通知状态回看执行快照、原始输出、导出摘要和通知回执 |
| 知识库治理台 | `/platform/knowledge-base/` | 查看法规规则包、RAG 文档源、切片、字段 Schema、Word 模板、责任人映射和飞书配置 |
| MCP 中心演示页 | `/platform/mcp-center/` | 展示外部连接器治理视图 |
| Skill 工作室演示页 | `/platform/skills/` | 展示审核 Skill 编排和发布状态 |
| 审核指挥台 | `/platform/command-center-v2/` | 面向讲解的大屏式审核流程与风险状态视图 |
| 底层场景列表 | `/scenarios/` | 展示 YAML 场景配置和非法配置错误摘要 |
| Django Admin | `/admin/` | 维护后台模型数据 |
## 模块划分 ## 模块划分
@@ -43,19 +60,26 @@ apps.scenarios
apps.documents apps.documents
apps.chat apps.chat
apps.audit apps.audit
apps.platform_ui
agent_core agent_core
``` ```
职责边界: 职责边界:
- Django Apps 负责页面、数据、文件、日志等企业应用外壳 - `config` 负责 Django 配置、路由入口、环境变量、静态资源和上传路径
- Agent Core 负责 RAG、工具调用、模型适配、结构化输出和 Agent 编排 - `apps.scenarios` 负责读取 YAML 场景配置,非法配置可被跳过并展示错误摘要
- RAG、工具调用和模型调用不直接写进 Django View - `apps.documents` 负责资料包、上传文件、章节点识别、页数统计、文本抽取、RAG 入库触发和导出记录
- `apps.chat` 负责审核工作台、会话绑定、用户输入、调用 Agent Core、补传资料和 Word 导出编排。
- `apps.audit` 负责审计日志、通知留痕、处理历史列表和详情回看。
- `apps.platform_ui` 负责知识库治理台、MCP 中心、Skill 工作室和指挥台等演示型治理页面。
- `agent_core` 负责 RAG、工具注册、治理配置、LLM Provider、Prompt 编排和结构化输出。
## 推荐项目结构 约束RAG、工具调用和模型调用不直接写进 Django ViewView 只做请求处理和页面渲染,复杂业务逻辑放到 `services.py``agent_core`
## 项目结构
```text ```text
universal-agent-demo/ DEMO-AGENT/
manage.py manage.py
requirements.txt requirements.txt
Dockerfile Dockerfile
@@ -66,102 +90,62 @@ universal-agent-demo/
config/ config/
apps/ apps/
scenarios/
documents/
chat/
audit/ audit/
chat/
documents/
platform_ui/
scenarios/
agent_core/ agent_core/
rag/ rag/
tools/
schemas/ schemas/
tools/
configs/ configs/
registration_overview.yaml document_review.yaml
registration_completeness_check.yaml governance.yaml
registration_field_extraction.yaml knowledge_qa.yaml
registration_consistency_review.yaml quality_analysis.yaml
registration_risk_report.yaml risk_audit.yaml
ticket_assistant.yaml
data/ data/
uploads/ uploads/
chroma/ chroma/
db.sqlite3
docs/ docs/
需求分析/
详细设计/
原型设计/
原始材料/
templates/
tests/
``` ```
## V1 功能范围 ## 已落地能力
V1 需要完成: - 根路径已重定向到审核智能体,降低演示入口复杂度。
- 审核工作台允许未上传资料时直接发起知识库问答,后续再通过右侧上传区导入资料包。
- 资料包导入支持 PDF、DOCX、MD、TXT、ZIP、7Z、RAR压缩包内仅导入支持格式其他文件会生成提示。
- 导入时会创建 `SubmissionBatch``UploadedDocument` 和绑定的 `Conversation`
- 文档解析覆盖文本抽取、PDF 页数统计、DOCX 页数元数据读取、章节点识别、文档角色识别和人工复核标记。
- 审核工作台支持会话历史、资料范围选择、预设问题、节点状态、结构化能力卡、补传资料、Word 导出和通知回看。
- Agent Core 已具备 Prompt 编排、OpenAI 兼容 Provider、结构化输出解析、RAG 检索、工具注册和治理配置读取。
- Word 导出会生成最小可下载 `.docx`,按风险状态区分正式版或草稿版,并写入导出记录。
- 审计日志记录输入、检索片段、工具调用、结构化输出、原始输出、模型名、耗时、状态和错误信息。
- 飞书通知首版为离线留痕,不直接依赖真实飞书网络发送;支持 `task_completed``task_failed` 两类原因。
- 知识库治理台展示法规规则、RAG 源、切片、字段 Schema、Word 模板、责任人映射和飞书配置。
- 自动化测试默认使用 Mock Provider避免本地真实模型密钥导致测试走网络。
- 注册审核任务列表。 ## 启动方式
- 审核工作台。
- 资料上传与管理。
- 文档解析与入库。
- 目录与页数汇总。
- 法规完整性检查。
- 关键信息抽取与回填预览。
- 一致性核查。
- 风险预警与审计日志。
- 模型 API 可配置。
- Docker 一键启动。
当前代码基线已经落地的通用能力 推荐首次本地启动
- 首页支持展示场景摘要、RAG 状态、工具数量。
- 非法 YAML 场景配置会被自动跳过,并在首页展示错误摘要。
- 对话页支持问题输入、文档范围选择、结构化结果、引用片段、工具调用和审计入口展示。
- 文档页支持上传、列表查看、手动入库、失败原因提示和重试。
- 审计页支持列表摘要、按场景筛选、详情查看、原始输出展示和敏感信息脱敏。
- Agent Core 已具备 Prompt 编排、OpenAI 兼容 Provider、结构化输出解析、RAG 检索和工具注册机制。
- 测试环境默认固定使用 Mock Provider避免误调用本地真实模型配置。
## 本轮需求文档
本轮已按模块重写需求分析,详见:
- [V1 总需求文档](F:\PyCharm\DEMO-AGENT\docs\需求分析\1.V1总需求文档.md)
- [需求重构总览与待确认事项](F:\PyCharm\DEMO-AGENT\docs\需求分析\0.需求重构总览与待确认事项.md)
V1 暂不重点做:
- 多租户。
- 复杂权限。
- 完整工作流引擎。
- 前后端分离。
- 深度 Dify 集成。
- 生产级高并发优化。
## 复试改题流程
拿到题目后:
1. 判断题目属于哪类模板。
2. 复制最接近的 YAML 场景配置。
3. 修改 Agent 角色、目标、指令和输出模板。
4. 上传题目材料。
5. 如需业务计算,新增一个工具函数。
6. 用 2 到 3 个问题测试效果。
7. 演示场景配置、知识库引用、工具调用、结构化输出和审计日志。
## 当前页面概览
当前项目包含以下主要页面:
| 页面 | 路径 | 当前能力 |
|---|---|---|
| 场景首页 | `/` | 展示场景名称、描述、适用题型、RAG 状态、工具数和配置异常摘要 |
| 对话页 | `/chat/<scenario_id>/` | 输入问题、勾选已入库文档、查看结构化结果、引用片段、工具调用和审计入口 |
| 文档列表页 | `/documents/` | 查看文档状态、错误信息、上传时间并手动触发入库 |
| 文档上传页 | `/documents/upload/` | 选择场景并上传 `.txt``.md``.pdf``.docx` 文件 |
| 审计列表页 | `/audit/` | 查看执行摘要并按场景筛选 |
| 审计详情页 | `/audit/<log_id>/` | 查看输入、最终回答、结构化输出、引用、工具调用、原始输出和错误信息 |
## 计划启动方式
本地启动:
```bash ```bash
python -m venv .venv
.venv\Scripts\activate
pip install -r requirements.txt pip install -r requirements.txt
python manage.py migrate python manage.py migrate
python manage.py runserver python manage.py runserver
@@ -173,21 +157,12 @@ Docker 启动:
docker compose up --build docker compose up --build
``` ```
当前文档目标已统一为完整 V1 闭环:真实 Chroma RAG、OpenAI 兼容 LLM、OpenAI 兼容 Embedding、工具注册和审计日志。开发阶段可以用测试桩验证页面和边界但不作为 V1 验收结果 Docker Compose 会读取根目录 `.env`,并挂载 `./data``./configs`
推荐首次启动步骤:
```bash
python -m venv .venv
.venv\Scripts\activate
pip install -r requirements.txt
python manage.py migrate
python manage.py runserver
```
## 环境变量 ## 环境变量
项目当前通过 `os.environ` 读取配置,核心变量如下: 项目通过根目录 `.env` 和系统环境变量读取配置。`.env.example` 只作为模板,不应提交真实密钥。
若复试演示使用硅基流动,可复制 `.env.siliconflow.example``.env`,再手动填入 `LLM_API_KEY``EMBEDDING_API_KEY`
```env ```env
DJANGO_SECRET_KEY=replace-with-a-local-secret-key DJANGO_SECRET_KEY=replace-with-a-local-secret-key
@@ -203,35 +178,21 @@ EMBEDDING_BASE_URL=
EMBEDDING_MODEL=text-embedding-3-small EMBEDDING_MODEL=text-embedding-3-small
SCENARIO_CONFIG_DIR=configs SCENARIO_CONFIG_DIR=configs
GOVERNANCE_CONFIG_PATH=configs/governance.yaml
UPLOAD_ROOT=data/uploads UPLOAD_ROOT=data/uploads
CHROMA_PATH=data/chroma CHROMA_PATH=data/chroma
``` ```
说明: 说明:
- `EMBEDDING_API_KEY` 为空时,代码会自动复用 `LLM_API_KEY` - `EMBEDDING_API_KEY` 为空时自动复用 `LLM_API_KEY`
- `EMBEDDING_BASE_URL` 为空时,代码会自动复用 `LLM_BASE_URL` - `EMBEDDING_BASE_URL` 为空时自动复用 `LLM_BASE_URL`
- `.env.example` 只作为模板,不应填写真实密钥并提交到仓库 - `.env.siliconflow.example` 内置硅基流动 `base_url`、Qwen 对话模型和 `BAAI/bge-m3` Embedding 配置
- 当前代码会在 Django settings 初始化时自动加载根目录 `.env`,本地 `python manage.py runserver``pytest` 和 Docker Compose 可以复用同一套配置 - Django settings 初始化时自动加载根目录 `.env`
- Docker Compose 当前在 `docker-compose.yml` 中通过 `env_file` 读取 `.env` - 测试环境会在 `tests/conftest.py` 中固定 Mock Provider避免误调用真实 LLM
常见做法:
- 本地开发:复制 `.env.example``.env`,填入真实参数后运行。
- Docker 演示:确认 `.env` 已配置后,再执行 `docker compose up --build`
## 测试与验证 ## 测试与验证
当前项目已经补有较完整的模块级测试,覆盖:
- 场景配置读取、非法配置容错和首页展示。
- 对话提交、文档范围传递、结构化结果展示。
- 文档上传、文本抽取、入库成功与失败提示。
- 审计日志落库、筛选、原始输出展示和 API Key 脱敏。
- Agent Core 的 Prompt 编排、结构化解析、RAG fallback 检索。
- Tool Registry 和内置工具行为。
- LLM / Embedding Provider 的配置与请求构造。
常用验证命令: 常用验证命令:
```bash ```bash
@@ -240,15 +201,60 @@ python manage.py check
docker compose config docker compose config
``` ```
说明 当前测试覆盖
- 测试环境默认通过 `tests/conftest.py` 固定 `LLM_PROVIDER=mock`,避免回归测试误走真实网络请求 - 项目配置、根路由和核心页面可访问性
- 当前本地 `.env` 可能包含真实模型配置,但不会影响自动化测试稳定性 - 场景配置读取、非法 YAML 容错和场景列表展示
- 资料包导入、压缩包展开、文档解析、入库状态和异常提示。
- 会话创建、对话提交、文档范围传递、结构化结果展示和 Word 导出。
- 审计日志落库、筛选、详情展示、通知留痕和敏感信息脱敏。
- Agent Core 的 Prompt 编排、结构化解析、RAG fallback、工具注册、LLM / Embedding Provider 请求构造。
- 平台治理页、指挥台、知识库、MCP 中心和 Skill 工作室展示。
## 文档入口 ## 文档入口
- [V1 总需求文档](docs/需求分析/1.V1总需求文档.md) - [V1 总需求文档](docs/需求分析/1.V1总需求文档.md)
- [模块需求文档索引](docs/需求分析/2.模块需求索引.md) - [需求重构总览与待确认事项](docs/需求分析/0.需求重构总览与待确认事项.md)
- [智能体总体设计](docs/设计文档/1.智能体总体设计.md) - [Config 模块需求分析](docs/需求分析/1.config模块需求分析.md)
- [设计文档索引](docs/设计文档/0.设计文档索引.md) - [Scenarios 模块需求分析](docs/需求分析/2.scenarios模块需求分析.md)
- [Documents 模块需求分析](docs/需求分析/3.documents模块需求分析.md)
- [Chat 模块需求分析](docs/需求分析/4.chat模块需求分析.md)
- [Audit 模块需求分析](docs/需求分析/5.audit模块需求分析.md)
- [Agent Core 模块需求分析](docs/需求分析/6.agent_core模块需求分析.md)
- [业务确认问答清单](docs/需求分析/9.业务确认问答清单.md)
- [资料包导入与目录汇总详细设计](docs/详细设计/1.资料包导入与目录汇总.md)
- [法规完整性检查详细设计](docs/详细设计/2.法规完整性检查.md)
- [字段抽取与统一字段池详细设计](docs/详细设计/3.字段抽取与统一字段池.md)
- [一致性核查详细设计](docs/详细设计/4.一致性核查.md)
- [风险预警详细设计](docs/详细设计/5.风险预警.md)
- [Word 回填导出详细设计](docs/详细设计/6.Word回填导出.md)
- [飞书通知详细设计](docs/详细设计/7.飞书通知.md)
- [注册审核平台整体原型设计](docs/原型设计/1.整体原型设计.md)
- [单文件演示站 HTML](docs/原型设计/registration-prototype-demo.html)
- [协作与编码约定](AGENTS.md) - [协作与编码约定](AGENTS.md)
## 复试改题流程
拿到新题目后:
1. 判断资料包、规则依据和核心审核链路。
2. 调整最接近的 YAML 场景配置,优先从 `configs/document_review.yaml` 入手。
3. 修改 Agent 角色、目标、指令和输出模板。
4. 上传题目材料并生成资料包。
5. 确认产品名称解析、资料包绑定和会话标题是否正确。
6. 如需业务计算,新增工具函数并通过 Tool Registry 注册。
7. 用 2 到 3 个预设问题测试目录汇总、完整性检查、字段抽取和风险报告。
8. 演示节点结果、知识库引用、结构化输出、Word 导出、通知留痕和审计日志。
## V1 不优先做
- React / Vue 前端。
- 多租户。
- 复杂 RBAC。
- 完整工作流引擎。
- 深度 Dify 集成。
- 微服务拆分。
- 分布式任务队列。
- 真实飞书发送链路。
这些内容可以作为后续增强,不应影响 V1 快速成型。

142
agent_core/governance.py Normal file
View File

@@ -0,0 +1,142 @@
from pathlib import Path
import yaml
from django.conf import settings
from django.db.utils import OperationalError, ProgrammingError
def governance_defaults() -> dict:
return {
"owner_mappings": [
{
"owner_role": "注册资料负责人",
"owner_name": "张三",
"department": "注册事务部",
"chapter_scope": "CH1",
"risk_scope": "字段冲突 / 缺失项",
"feishu_user_id": "ou_demo_1",
"feishu_open_id": "on_demo_1",
"feishu_name": "张三",
"notify_enabled": "",
},
{
"owner_role": "注册申报负责人",
"owner_name": "李四",
"department": "临床注册组",
"chapter_scope": "CH2-CH6",
"risk_scope": "完整性风险 / 导出阻断",
"feishu_user_id": "ou_demo_2",
"feishu_open_id": "on_demo_2",
"feishu_name": "李四",
"notify_enabled": "",
},
],
"feishu_configs": [
{
"config_name": "注册审核完成通知",
"notify_reason": "task_completed",
"channel": "群机器人",
"message_template": "审核完成摘要 + @处理人",
"status": "启用",
},
{
"config_name": "注册审核异常通知",
"notify_reason": "task_failed",
"channel": "群机器人",
"message_template": "异常摘要 + @处理人",
"status": "启用",
},
],
"template_mappings": [
{
"template_name": "注册证导出模板",
"output_type": "registration_word_export_report",
"version": "V1.0",
"placeholder_count": 18,
"status": "启用",
"field_mapping_summary": "产品名称 / 注册人 / 适用机型 / 储存条件",
},
{
"template_name": "风险摘要导出模板",
"output_type": "registration_word_export_report",
"version": "V0.9",
"placeholder_count": 10,
"status": "待校验",
"field_mapping_summary": "风险等级 / 批次号 / 责任人 / 证据摘要",
},
],
}
def read_governance_yaml() -> dict:
raw_path = getattr(settings, "GOVERNANCE_CONFIG_PATH", "")
if not raw_path:
return {}
config_path = Path(raw_path)
if not config_path.exists() or not config_path.is_file():
return {}
with config_path.open("r", encoding="utf-8") as file:
return yaml.safe_load(file) or {}
def load_governance_config() -> dict:
defaults = governance_defaults()
config = read_governance_yaml()
db_config = load_governance_config_from_db()
for key, default_value in defaults.items():
configured_value = db_config.get(key) or config.get(key)
if isinstance(default_value, list) and configured_value:
defaults[key] = configured_value
return defaults
def load_governance_config_from_db() -> dict:
try:
from apps.platform_ui.models import FeishuNotifyConfig, OwnerMapping, WordTemplateMapping
except Exception:
return {}
try:
owner_mappings = [
{
"owner_role": item.owner_role,
"owner_name": item.owner_name,
"department": item.department,
"chapter_scope": item.chapter_scope,
"risk_scope": item.risk_scope,
"feishu_user_id": item.feishu_user_id,
"feishu_open_id": item.feishu_open_id,
"feishu_name": item.feishu_name,
"notify_enabled": "" if item.notify_enabled else "",
}
for item in OwnerMapping.objects.filter(is_active=True)
]
feishu_configs = [
{
"config_name": item.config_name,
"notify_reason": item.notify_reason,
"channel": item.channel,
"message_template": item.message_template,
"status": item.status,
}
for item in FeishuNotifyConfig.objects.filter(is_active=True)
]
template_mappings = [
{
"template_name": item.template_name,
"output_type": item.output_type,
"version": item.version,
"placeholder_count": item.placeholder_count,
"status": item.status,
"field_mapping_summary": item.field_mapping_summary,
}
for item in WordTemplateMapping.objects.filter(is_active=True)
]
except (OperationalError, ProgrammingError, RuntimeError):
return {}
return {
"owner_mappings": owner_mappings,
"feishu_configs": feishu_configs,
"template_mappings": template_mappings,
}

View File

@@ -1,7 +1,7 @@
from dataclasses import dataclass from dataclasses import dataclass
import json import json
import os import os
from urllib.error import URLError from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen from urllib.request import Request, urlopen
@@ -74,6 +74,7 @@ class OpenAICompatibleProvider:
} }
if response_format: if response_format:
payload["response_format"] = response_format payload["response_format"] = response_format
try:
try: try:
data = _post_json( data = _post_json(
base_url=self.base_url, base_url=self.base_url,
@@ -81,6 +82,21 @@ class OpenAICompatibleProvider:
api_key=self.api_key, api_key=self.api_key,
payload=payload, payload=payload,
) )
except RuntimeError as exc:
# 部分 OpenAI 兼容供应商或模型不支持 response_format。
# 保留结构化优先,遇到 400 时退回普通对话,避免演示链路被接口能力差异阻断。
if not response_format or "HTTP Error 400" not in str(exc):
raise
fallback_payload = {
"model": self.model_name,
"messages": messages,
}
data = _post_json(
base_url=self.base_url,
endpoint="chat/completions",
api_key=self.api_key,
payload=fallback_payload,
)
choice = data.get("choices", [{}])[0] choice = data.get("choices", [{}])[0]
content = choice.get("message", {}).get("content", "") content = choice.get("message", {}).get("content", "")
return LLMResponse( return LLMResponse(
@@ -197,5 +213,11 @@ def _post_json(base_url: str, endpoint: str, api_key: str, payload: dict) -> dic
try: try:
with urlopen(request, timeout=60) as response: with urlopen(request, timeout=60) as response:
return json.loads(response.read().decode("utf-8")) return json.loads(response.read().decode("utf-8"))
except HTTPError as exc:
error_body = exc.read().decode("utf-8", errors="ignore")
error_detail = f"{exc}"
if error_body:
error_detail = f"{error_detail} {error_body}"
raise RuntimeError(f"OpenAI 兼容接口调用失败:{error_detail}") from exc
except URLError as exc: except URLError as exc:
raise RuntimeError(f"OpenAI 兼容接口调用失败:{exc}") from exc raise RuntimeError(f"OpenAI 兼容接口调用失败:{exc}") from exc

View File

@@ -1,6 +1,7 @@
import json import json
import time import time
from .governance import load_governance_config
from .llm_provider import create_llm_provider, get_runtime_llm_config from .llm_provider import create_llm_provider, get_runtime_llm_config
from .results import AgentResult from .results import AgentResult
from .structured_output import ( from .structured_output import (
@@ -57,6 +58,14 @@ def run_agent(scenario_config: dict, user_input: str, options: dict | None = Non
latency_ms=latency_ms, latency_ms=latency_ms,
status="failed", status="failed",
error=str(llm_response.error or "未知模型错误"), error=str(llm_response.error or "未知模型错误"),
conversation_id=str(options.get("conversation_id", "")),
batch_id=str(options.get("batch_id", "")),
product_name=str(options.get("product_name", "")),
notification_payload=_build_notification_payload(
{"notify_reason": "task_failed", "owner_roles": []},
options=options,
status="failed",
),
) )
structured_output, _ = parse_structured_output(llm_response.content, output_type) structured_output, _ = parse_structured_output(llm_response.content, output_type)
@@ -70,6 +79,11 @@ def run_agent(scenario_config: dict, user_input: str, options: dict | None = Non
model_name=llm_response.model_name or "unknown-model", model_name=llm_response.model_name or "unknown-model",
latency_ms=latency_ms, latency_ms=latency_ms,
status="success", status="success",
conversation_id=str(options.get("conversation_id", "")),
batch_id=str(options.get("batch_id", "")),
product_name=str(options.get("product_name", "")),
node_results=_build_node_results(output_type, structured_output),
notification_payload=_build_notification_payload(structured_output, options=options, status="success"),
) )
@@ -151,3 +165,103 @@ def _format_tool_calls(tool_calls: list[dict]) -> str:
f"{index}. 工具={tool_call.get('tool_name')} 失败={tool_call.get('error', '未知错误')}" f"{index}. 工具={tool_call.get('tool_name')} 失败={tool_call.get('error', '未知错误')}"
) )
return "\n".join(lines) return "\n".join(lines)
def _build_node_results(output_type: str, structured_output: dict) -> list[dict]:
if output_type.startswith("registration_") or output_type == "feishu_notification_report":
return _build_registration_node_results(output_type, structured_output)
return [
{
"code": output_type,
"label": output_type,
"status": "已完成",
"summary": structured_output.get("summary") or structured_output.get("answer", ""),
}
]
def _build_notification_payload(structured_output: dict, options: dict, status: str) -> dict:
notify_reason = _normalize_notify_reason(
structured_output.get("notify_reason"),
status=status,
)
owners = structured_output.get("owner_roles") or []
if not owners:
owners = load_governance_config()["owner_mappings"]
return {
"batch_id": str(options.get("batch_id", "")),
"conversation_id": str(options.get("conversation_id", "")),
"product_name": str(options.get("product_name", "")),
"notify_reason": notify_reason,
"owners": owners,
"mentioned_users": structured_output.get("mentioned_users") or [],
"message_status": structured_output.get("message_status")
or ("sent" if status == "success" else "failed"),
"web_detail_url": structured_output.get("web_detail_url", ""),
"receipt": structured_output.get("receipt") or {},
"status": status,
}
def _normalize_notify_reason(notify_reason: str | None, *, status: str) -> str:
"""
将通知原因收口到 Demo 固定支持的两类语义。
"""
if notify_reason in {"task_completed", "task_failed"}:
return notify_reason
return "task_completed" if status == "success" else "task_failed"
def _build_registration_node_results(output_type: str, structured_output: dict) -> list[dict]:
nodes = [
{"code": "package_import", "label": "资料包导入", "status": "已完成"},
{"code": "overview", "label": "目录汇总", "status": "待处理"},
{"code": "completeness", "label": "法规完整性检查", "status": "待处理"},
{"code": "field_extraction", "label": "字段抽取", "status": "待处理"},
{"code": "consistency", "label": "一致性核查", "status": "待处理"},
{"code": "risk", "label": "风险预警", "status": "待处理"},
{"code": "word_export", "label": "Word 回填导出", "status": "待处理"},
{"code": "feishu_notify", "label": "飞书通知", "status": "待处理"},
]
progression_map = {
"registration_overview_report": 1,
"registration_completeness_report": 2,
"registration_field_extraction_report": 3,
"registration_consistency_report": 4,
"registration_risk_report": 5,
"registration_word_export_report": 6,
"feishu_notification_report": 7,
}
completed_index = progression_map.get(output_type, 0)
for index in range(1, completed_index + 1):
nodes[index]["status"] = "已完成"
if output_type == "registration_risk_report":
pass_status = structured_output.get("pass_status", "")
if pass_status in {"blocked", "failed"}:
nodes[5]["status"] = "已阻断"
elif pass_status in {"review_required", "manual_review"}:
nodes[5]["status"] = "待复核"
else:
nodes[5]["status"] = "已完成"
return nodes
if output_type == "registration_word_export_report":
export_status = structured_output.get("export_status", "")
if export_status in {"blocked", "draft_only"}:
nodes[6]["status"] = "已阻断" if export_status == "blocked" else "待复核"
else:
nodes[6]["status"] = "已完成"
return nodes
if output_type == "feishu_notification_report":
message_status = structured_output.get("message_status", "")
if message_status in {"failed", "error"}:
nodes[7]["status"] = "失败"
elif message_status in {"sent", "success"}:
nodes[7]["status"] = "已发送"
else:
nodes[7]["status"] = "待处理"
return nodes
return nodes

View File

@@ -20,3 +20,8 @@ class AgentResult:
latency_ms: int = 0 latency_ms: int = 0
status: str = "success" status: str = "success"
error: str = "" error: str = ""
conversation_id: str = ""
batch_id: str = ""
product_name: str = ""
node_results: list = field(default_factory=list)
notification_payload: dict = field(default_factory=dict)

View File

@@ -1,6 +1,13 @@
SUPPORTED_OUTPUT_TYPES = { SUPPORTED_OUTPUT_TYPES = {
"general_answer", "general_answer",
"document_review_report", "document_review_report",
"registration_overview_report",
"registration_completeness_report",
"registration_field_extraction_report",
"registration_consistency_report",
"registration_risk_report",
"registration_word_export_report",
"feishu_notification_report",
"ticket_response", "ticket_response",
"quality_report", "quality_report",
"risk_audit_report", "risk_audit_report",

View File

@@ -41,6 +41,61 @@ OUTPUT_FIELD_TEMPLATES = {
"suggestions": [], "suggestions": [],
"references": [], "references": [],
}, },
"registration_overview_report": {
"batch_id": "",
"product_name": "",
"file_count": 0,
"total_page_count": 0,
"chapter_summary": [],
"documents": [],
"warnings": [],
},
"registration_completeness_report": {
"summary": "",
"missing_items": [],
"misplaced_items": [],
"risk_level": "medium",
"references": [],
},
"registration_field_extraction_report": {
"summary": "",
"field_items": [],
"low_confidence_items": [],
"references": [],
},
"registration_consistency_report": {
"summary": "",
"conflict_items": [],
"mixed_document_risks": [],
"risk_level": "medium",
"references": [],
},
"registration_risk_report": {
"summary": "",
"risk_items": [],
"highest_risk_level": "medium",
"pass_status": "review_required",
"manual_review_items": [],
"owner_roles": [],
"suggestions": [],
"notify_reason": "task_completed",
},
"registration_word_export_report": {
"summary": "",
"export_status": "draft_only",
"can_export_formally": False,
"blocked_items": [],
"download_url": "",
},
"feishu_notification_report": {
"batch_id": "",
"conversation_id": "",
"notify_reason": "task_completed",
"mentioned_users": [],
"message_status": "pending",
"web_detail_url": "",
"receipt": {},
},
} }

View File

@@ -0,0 +1,62 @@
# Generated by Django 5.2.14 on 2026-06-03 16:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("audit", "0002_demobusinessrecord"),
]
operations = [
migrations.CreateModel(
name="NotificationRecord",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("batch_id", models.CharField(db_index=True, max_length=64)),
("conversation_id", models.CharField(db_index=True, max_length=64)),
(
"product_name",
models.CharField(blank=True, db_index=True, max_length=255),
),
("trigger_source", models.CharField(blank=True, max_length=64)),
("notify_reason", models.CharField(db_index=True, max_length=32)),
("owner_role", models.CharField(blank=True, max_length=100)),
("feishu_user_id", models.CharField(blank=True, max_length=100)),
(
"message_status",
models.CharField(db_index=True, default="pending", max_length=32),
),
("web_detail_url", models.URLField(blank=True)),
("receipt", models.JSONField(blank=True, default=dict)),
("created_at", models.DateTimeField(auto_now_add=True, db_index=True)),
],
options={
"ordering": ["-created_at"],
},
),
migrations.AddField(
model_name="agentauditlog",
name="batch_id",
field=models.CharField(blank=True, db_index=True, max_length=64),
),
migrations.AddField(
model_name="agentauditlog",
name="conversation_id",
field=models.CharField(blank=True, db_index=True, max_length=64),
),
migrations.AddField(
model_name="agentauditlog",
name="product_name",
field=models.CharField(blank=True, db_index=True, max_length=255),
),
]

View File

@@ -16,6 +16,9 @@ class AgentAuditLog(models.Model):
scenario_id = models.CharField(max_length=100, db_index=True) scenario_id = models.CharField(max_length=100, db_index=True)
scenario_name = models.CharField(max_length=200, blank=True) scenario_name = models.CharField(max_length=200, blank=True)
batch_id = models.CharField(max_length=64, blank=True, db_index=True)
conversation_id = models.CharField(max_length=64, blank=True, db_index=True)
product_name = models.CharField(max_length=255, blank=True, db_index=True)
user_input = models.TextField() user_input = models.TextField()
retrieved_chunks = models.JSONField(default=list, blank=True) retrieved_chunks = models.JSONField(default=list, blank=True)
tool_calls = models.JSONField(default=list, blank=True) tool_calls = models.JSONField(default=list, blank=True)
@@ -66,3 +69,41 @@ class DemoBusinessRecord(models.Model):
def __str__(self) -> str: def __str__(self) -> str:
return self.title return self.title
class NotificationRecord(models.Model):
"""
飞书通知留痕。
首版只保存离线通知载荷与结果状态,不直接依赖真实飞书网络。
"""
STATUS_PENDING = "pending"
STATUS_SENT = "sent"
STATUS_FAILED = "failed"
batch_id = models.CharField(max_length=64, db_index=True)
conversation_id = models.CharField(max_length=64, db_index=True)
product_name = models.CharField(max_length=255, blank=True, db_index=True)
trigger_source = models.CharField(max_length=64, blank=True)
notify_reason = models.CharField(max_length=32, db_index=True)
owner_role = models.CharField(max_length=100, blank=True)
feishu_user_id = models.CharField(max_length=100, blank=True)
message_status = models.CharField(max_length=32, default=STATUS_PENDING, db_index=True)
web_detail_url = models.URLField(blank=True)
receipt = models.JSONField(default=dict, blank=True)
created_at = models.DateTimeField(auto_now_add=True, db_index=True)
class Meta:
ordering = ["-created_at"]
def __str__(self) -> str:
return f"{self.notify_reason}:{self.batch_id}"
def get_message_status_display_text(self) -> str:
"""返回通知状态的中文展示文案。"""
return {
self.STATUS_PENDING: "处理中",
self.STATUS_SENT: "已发送",
self.STATUS_FAILED: "失败",
}.get(self.message_status, self.message_status)

View File

@@ -1,6 +1,41 @@
from agent_core.results import AgentResult from agent_core.results import AgentResult
from apps.chat.models import Conversation
from apps.documents.models import SubmissionBatch
from .models import AgentAuditLog from .models import AgentAuditLog, NotificationRecord
SUPPORTED_NOTIFY_REASONS = {"task_completed", "task_failed"}
RISK_STATUS_DISPLAY = {
"high": "已阻断",
"medium": "待复核",
"low": "已完成",
"failed": "失败",
"processing": "处理中",
"pending": "处理中",
}
EXPORT_STATUS_DISPLAY = {
"completed": "已完成",
"draft_only": "待复核",
"review_required": "待复核",
"manual_review": "待复核",
"blocked": "已阻断",
"failed": "失败",
"processing": "处理中",
"pending": "处理中",
}
CONVERSATION_STATUS_DISPLAY = {
"success": "已完成",
"completed": "已完成",
"review_required": "待复核",
"blocked": "已阻断",
"failed": "失败",
"processing": "处理中",
"pending": "处理中",
}
def create_audit_log( def create_audit_log(
@@ -8,6 +43,9 @@ def create_audit_log(
scenario_name: str, scenario_name: str,
user_input: str, user_input: str,
agent_result: AgentResult, agent_result: AgentResult,
batch_id: str = "",
conversation_id: str = "",
product_name: str = "",
) -> AgentAuditLog: ) -> AgentAuditLog:
""" """
将一次 Agent 执行结果落库为审计日志。 将一次 Agent 执行结果落库为审计日志。
@@ -20,6 +58,9 @@ def create_audit_log(
return AgentAuditLog.objects.create( return AgentAuditLog.objects.create(
scenario_id=scenario_id, scenario_id=scenario_id,
scenario_name=scenario_name, scenario_name=scenario_name,
batch_id=batch_id,
conversation_id=conversation_id,
product_name=product_name,
user_input=user_input, user_input=user_input,
retrieved_chunks=agent_result.references, retrieved_chunks=agent_result.references,
tool_calls=agent_result.tool_calls, tool_calls=agent_result.tool_calls,
@@ -55,3 +96,227 @@ def _mask_token_after_marker(value: str, marker: str) -> str:
secret, separator, rest = suffix.partition(" ") secret, separator, rest = suffix.partition(" ")
masked_secret = "sk-***" if secret.startswith("sk-") else "***" masked_secret = "sk-***" if secret.startswith("sk-") else "***"
return f"{prefix}{marker}{masked_secret}{separator}{rest}" return f"{prefix}{marker}{masked_secret}{separator}{rest}"
def create_notification_record(
*,
batch_id: str,
conversation_id: str,
product_name: str,
trigger_source: str,
notify_reason: str,
owner_role: str,
feishu_user_id: str,
message_status: str,
web_detail_url: str,
receipt: dict,
) -> NotificationRecord:
"""
保存通知留痕。
V1 先把通知载荷和结果状态稳定落库,
真实飞书发送可在后续阶段接入。
"""
if notify_reason not in SUPPORTED_NOTIFY_REASONS:
raise ValueError(f"notify_reason 不受支持:{notify_reason}")
return NotificationRecord.objects.create(
batch_id=batch_id,
conversation_id=conversation_id,
product_name=product_name,
trigger_source=trigger_source,
notify_reason=notify_reason,
owner_role=owner_role,
feishu_user_id=feishu_user_id,
message_status=message_status,
web_detail_url=web_detail_url,
receipt=receipt,
)
def build_history_list_context(
*,
scenario_id: str = "",
keyword: str = "",
notify_status: str = "",
risk_status: str = "",
) -> dict:
"""
组装处理历史列表页所需的筛选结果与展示上下文。
View 只负责读取 query params筛选逻辑和列表聚合统一在服务层完成。
"""
logs = AgentAuditLog.objects.all()
if scenario_id:
logs = logs.filter(scenario_id=scenario_id)
if keyword:
logs = logs.filter(product_name__icontains=keyword) | logs.filter(batch_id__icontains=keyword)
if notify_status:
matched_pairs = list(
NotificationRecord.objects.filter(message_status=notify_status).values_list(
"batch_id",
"conversation_id",
)
)
logs = [
log
for log in logs
if (log.batch_id, log.conversation_id) in matched_pairs
]
if risk_status:
logs = [
log
for log in logs
if (log.structured_output or {}).get("highest_risk_level") == risk_status
or (log.structured_output or {}).get("risk_level") == risk_status
]
history_rows = build_history_rows(logs)
return {
"history_rows": history_rows,
"history_metrics": build_history_metrics(history_rows),
"selected_scenario_id": scenario_id,
"keyword": keyword,
"notify_status": notify_status,
"risk_status": risk_status,
}
def build_history_rows(logs) -> list[dict]:
"""
为处理历史列表补齐风险状态和通知状态。
View 只负责收集筛选条件,列表展示所需的聚合字段统一在服务层完成。
"""
notification_map = {
(item.batch_id, item.conversation_id): item
for item in NotificationRecord.objects.order_by("-created_at")
}
batch_map = {
item.batch_id: item
for item in SubmissionBatch.objects.filter(
batch_id__in=[log.batch_id for log in logs if log.batch_id]
)
}
conversation_map = {
item.conversation_id: item
for item in Conversation.objects.filter(
conversation_id__in=[log.conversation_id for log in logs if log.conversation_id]
)
}
rows = []
for log in logs:
notification = notification_map.get((log.batch_id, log.conversation_id))
batch = batch_map.get(log.batch_id)
conversation = conversation_map.get(log.conversation_id)
structured_output = log.structured_output or {}
rows.append(
{
"log": log,
"batch": batch,
"conversation": conversation,
"batch_scale": f"{batch.file_count} 份 / {batch.page_count}" if batch else "-",
"batch_status": batch.get_import_status_display_text() if batch else "-",
"conversation_status": _get_conversation_status_display_text(
conversation.task_status if conversation else "-"
),
"risk_status": _get_risk_status_display_text(
structured_output.get("highest_risk_level")
or structured_output.get("risk_level")
or "-"
),
"notify_status": notification.get_message_status_display_text() if notification else "-",
"notify_reason": notification.notify_reason if notification else "-",
}
)
return rows
def build_history_metrics(history_rows: list[dict]) -> list[dict]:
"""
为处理历史页生成顶部指标卡。
口径保持前台可讲解:
- 处理任务数:当前筛选结果中的执行记录数
- 成功执行:状态为 success 的记录数
- 通知已发送:通知状态为 sent 的记录数
- 高风险阻断:风险等级为 high 的记录数
"""
total_count = len(history_rows)
success_count = sum(1 for row in history_rows if row["log"].status == "success")
notify_sent_count = sum(1 for row in history_rows if row.get("notify_status") == "已发送")
blocked_count = sum(1 for row in history_rows if row.get("risk_status") == "已阻断")
return [
{"label": "处理任务数", "value": total_count, "note": "按当前筛选条件回看执行留痕。"},
{"label": "成功执行", "value": success_count, "note": "执行完成并写入审计快照。"},
{"label": "通知已发送", "value": notify_sent_count, "note": "已生成已发送状态的通知留痕。"},
{"label": "高风险阻断", "value": blocked_count, "note": "当前风险状态为已阻断的处理记录。"},
]
def build_detail_summary(log: AgentAuditLog, conversation, notifications) -> dict:
"""
组装处理历史详情页的导出摘要与通知回执信息。
详情页模板只负责展示,字段拼装与优先级判断统一放在服务层。
"""
structured_output = log.structured_output or {}
output_file = structured_output.get("output_file") or {}
export_node = None
if conversation and conversation.node_results:
export_node = next(
(node for node in conversation.node_results if node.get("label") == "Word 回填导出"),
None,
)
latest_notification = notifications.first() if hasattr(notifications, "first") else None
return {
"export_status": _get_export_status_display_text(
structured_output.get("export_status") or (export_node or {}).get("status", "-")
),
"download_url": structured_output.get("download_url", ""),
"output_file_name": output_file.get("file_name", ""),
"output_file_relative_path": output_file.get("relative_path", ""),
"export_mode": output_file.get("export_mode", ""),
"template_name": structured_output.get("template_name", ""),
"template_version": structured_output.get("template_version", ""),
"draft_export_status": _get_export_status_display_text(
structured_output.get("draft_export_status", "")
),
"formal_export_status": _get_export_status_display_text(
structured_output.get("formal_export_status", "")
),
"blocked_items": structured_output.get("blocked_items") or [],
"notification_receipt": latest_notification.receipt if latest_notification else {},
}
def normalize_conversation_node_results(node_results: list[dict] | None) -> list[dict]:
"""
统一处理历史详情页的节点状态展示口径。
兼容历史上遗留的“飞书通知 / 已完成”节点状态,
页面展示时统一映射为“已发送”。
"""
normalized = []
for node in node_results or []:
item = dict(node)
if item.get("label") == "飞书通知":
status = item.get("status", "")
if status in {"已完成", "success", "sent"}:
item["status"] = "已发送"
elif status in {"failed", "error"}:
item["status"] = "失败"
elif status in {"pending", "processing"}:
item["status"] = "待处理"
normalized.append(item)
return normalized
def _get_risk_status_display_text(status: str) -> str:
return RISK_STATUS_DISPLAY.get(status, status or "-")
def _get_export_status_display_text(status: str) -> str:
return EXPORT_STATUS_DISPLAY.get(status, status or "-")
def _get_conversation_status_display_text(status: str) -> str:
return CONVERSATION_STATUS_DISPLAY.get(status, status or "-")

View File

@@ -1,26 +1,45 @@
from django.shortcuts import get_object_or_404, render from django.shortcuts import get_object_or_404, render
from .models import AgentAuditLog from .models import AgentAuditLog, NotificationRecord
from apps.chat.models import Conversation
from .services import (
build_detail_summary,
build_history_list_context,
normalize_conversation_node_results,
)
def log_list(request): def log_list(request):
# 列表页支持按场景筛选,方便演示时快速定位同一类场景的执行记录 # 处理历史页支持按批次、产品和状态筛选
scenario_id = (request.GET.get("scenario_id") or "").strip() context = build_history_list_context(
logs = AgentAuditLog.objects.all() scenario_id=(request.GET.get("scenario_id") or "").strip(),
if scenario_id: keyword=(request.GET.get("keyword") or "").strip(),
logs = logs.filter(scenario_id=scenario_id) notify_status=(request.GET.get("notify_status") or "").strip(),
return render( risk_status=(request.GET.get("risk_status") or "").strip(),
request,
"audit/log_list.html",
{
"logs": logs,
"selected_scenario_id": scenario_id,
},
) )
return render(request, "audit/log_list.html", context)
def log_detail(request, log_id: int): def log_detail(request, log_id: int):
# 详情页只负责按主键加载审计快照并渲染; # 详情页只负责按主键加载审计快照并渲染;
# 所有脱敏和字段映射都应在服务层完成。 # 所有脱敏和字段映射都应在服务层完成。
audit_log = get_object_or_404(AgentAuditLog, pk=log_id) audit_log = get_object_or_404(AgentAuditLog, pk=log_id)
return render(request, "audit/log_detail.html", {"log": audit_log}) notifications = NotificationRecord.objects.filter(
conversation_id=audit_log.conversation_id,
batch_id=audit_log.batch_id,
)
conversation = Conversation.objects.filter(conversation_id=audit_log.conversation_id).first()
detail_summary = build_detail_summary(audit_log, conversation, notifications)
return render(
request,
"audit/log_detail.html",
{
"log": audit_log,
"notifications": notifications,
"conversation": conversation,
"conversation_node_results": normalize_conversation_node_results(
conversation.node_results if conversation else []
),
"detail_summary": detail_summary,
},
)

316
apps/chat/export_service.py Normal file
View File

@@ -0,0 +1,316 @@
from __future__ import annotations
from datetime import date
from pathlib import Path
from xml.sax.saxutils import escape
import zipfile
from django.conf import settings
from django.utils import timezone
from agent_core.governance import load_governance_config
from apps.documents.services import create_export_record
def generate_registration_export(*, batch, conversation, upstream_summary: dict | None = None) -> dict:
"""
基于当前会话上下文生成最小可下载的 Word 导出结果。
这里故意保持为 Django 服务层职责:
- 根据风险结果判断正式版/草稿版
- 选择治理台中启用的模板摘要
- 生成离线可演示的 `.docx` 文件与下载链接
"""
upstream_summary = upstream_summary or {}
template_mapping = _resolve_template_mapping()
blocked_items = _collect_blocked_items(upstream_summary)
can_export_formally = _can_export_formally(upstream_summary, blocked_items)
export_mode = "formal" if can_export_formally else "draft"
export_status = "completed" if can_export_formally else "draft_only"
relative_path = _build_relative_export_path(batch.batch_id, export_mode)
absolute_path = Path(settings.MEDIA_ROOT) / relative_path
absolute_path.parent.mkdir(parents=True, exist_ok=True)
fillable_items = _build_fillable_items(batch, conversation)
filled_fields = _build_filled_fields(fillable_items)
blocked_fields = _build_blocked_fields(blocked_items)
_write_minimal_docx(
absolute_path,
product_name=batch.product_name or conversation.product_name or "未命名资料包",
batch_id=batch.batch_id,
export_mode=export_mode,
summary=upstream_summary.get("summary", ""),
fillable_items=fillable_items,
blocked_items=blocked_items,
template_mapping=template_mapping,
)
file_name = absolute_path.name
download_url = f"/{settings.MEDIA_URL.strip('/')}/{relative_path.as_posix()}"
summary = (
"已生成正式版导出文件。"
if can_export_formally
else "已生成草稿导出文件,正式版仍被风险项阻断。"
)
report = {
"output_type": "registration_word_export_report",
"summary": summary,
"template_name": template_mapping["template_name"],
"template_version": template_mapping["version"],
"export_status": export_status,
"draft_export_status": "completed",
"formal_export_status": "completed" if can_export_formally else "blocked",
"can_export_formally": can_export_formally,
"fillable_items": fillable_items,
"filled_fields": filled_fields,
"fillable_field_count": len(fillable_items),
"filled_field_count": len(filled_fields),
"blocked_items": blocked_items,
"blocked_fields": blocked_fields,
"blocked_field_count": len(blocked_fields),
"manual_review_field_count": len(blocked_fields),
"layout_check_status": "passed",
"download_url": download_url,
"output_file": {
"file_name": file_name,
"relative_path": relative_path.as_posix(),
"absolute_path": str(absolute_path),
"export_mode": export_mode,
"output_version": export_mode,
"generated_at": timezone.now().isoformat(),
},
}
create_export_record(
batch=batch,
conversation_id=conversation.conversation_id,
product_name=batch.product_name or conversation.product_name,
template_name=report["template_name"],
template_version=report["template_version"],
export_mode=export_mode,
output_type=report["output_type"],
file_name=file_name,
relative_path=relative_path.as_posix(),
download_url=download_url,
)
return report
def update_conversation_with_export_report(conversation, export_report: dict) -> None:
latest_summary = dict(conversation.latest_summary or {})
previous_structured_output = latest_summary.get("structured_output") or {}
if previous_structured_output.get("output_type") != "registration_word_export_report":
latest_summary["upstream_structured_output"] = previous_structured_output
latest_summary["structured_output"] = export_report
latest_summary["answer"] = export_report.get("summary", "")
latest_summary["status"] = "success"
conversation.latest_summary = latest_summary
conversation.node_results = _update_word_export_node(conversation.node_results, export_report)
conversation.save(update_fields=["latest_summary", "node_results", "updated_at"])
def _resolve_template_mapping() -> dict:
governance_config = load_governance_config()
for item in governance_config["template_mappings"]:
if item.get("status") == "启用":
return item
return {
"template_name": "注册证导出模板",
"version": "V1.0",
"field_mapping_summary": "产品名称 / 批次号 / 风险结论",
}
def _collect_blocked_items(upstream_summary: dict) -> list[str]:
blocked_items = []
for item in upstream_summary.get("manual_review_items") or []:
if isinstance(item, str) and item.strip():
blocked_items.append(item.strip())
for item in upstream_summary.get("risk_items") or []:
if isinstance(item, dict):
title = (item.get("title") or item.get("issue") or "").strip()
if title:
blocked_items.append(title)
unique_items = []
for item in blocked_items:
if item not in unique_items:
unique_items.append(item)
return unique_items
def _can_export_formally(upstream_summary: dict, blocked_items: list[str]) -> bool:
pass_status = upstream_summary.get("pass_status")
if pass_status in {"blocked", "failed", "review_required", "manual_review"}:
return False
highest_risk_level = str(upstream_summary.get("highest_risk_level", "")).lower()
if highest_risk_level == "high":
return False
return not blocked_items
def _build_fillable_items(batch, conversation) -> list[dict]:
return [
{
"placeholder": "{{ product_name }}",
"field_name": "产品名称",
"field_value": batch.product_name,
"source": "资料包主信息",
"fill_status": "filled",
"required": True,
},
{
"placeholder": "{{ batch_id }}",
"field_name": "批次号",
"field_value": batch.batch_id,
"source": "资料包主信息",
"fill_status": "filled",
"required": True,
},
{
"placeholder": "{{ conversation_id }}",
"field_name": "会话编号",
"field_value": conversation.conversation_id,
"source": "会话主信息",
"fill_status": "filled",
"required": True,
},
{
"placeholder": "{{ file_count }}",
"field_name": "文件数",
"field_value": str(batch.file_count),
"source": "资料包统计",
"fill_status": "filled",
"required": False,
},
{
"placeholder": "{{ page_count }}",
"field_name": "页数",
"field_value": str(batch.page_count),
"source": "资料包统计",
"fill_status": "filled",
"required": False,
},
]
def _build_filled_fields(fillable_items: list[dict]) -> list[dict]:
return [
{
"placeholder": item["placeholder"],
"field_name": item["field_name"],
"field_value": item["field_value"],
"source": item["source"],
"fill_status": item["fill_status"],
"required": item["required"],
}
for item in fillable_items
]
def _build_blocked_fields(blocked_items: list[str]) -> list[dict]:
return [
{
"field_name": item,
"block_reason": "待人工复核",
"risk_source": "registration_risk_report",
}
for item in blocked_items
]
def _build_relative_export_path(batch_id: str, export_mode: str) -> Path:
file_name = f"{batch_id}-{export_mode}.docx"
return Path("exports") / date.today().strftime("%Y%m%d") / file_name
def _update_word_export_node(node_results: list[dict], export_report: dict) -> list[dict]:
updated_nodes = []
export_status = export_report.get("export_status")
node_status = "已完成" if export_status == "completed" else "待复核"
for node in node_results or []:
current = dict(node)
if current.get("label") == "Word 回填导出":
current["status"] = node_status
current["summary"] = export_report.get("summary", "")
updated_nodes.append(current)
return updated_nodes
def _write_minimal_docx(
output_path: Path,
*,
product_name: str,
batch_id: str,
export_mode: str,
summary: str,
fillable_items: list[dict],
blocked_items: list[str],
template_mapping: dict,
) -> None:
document_lines = [
f"注册审核导出文件({'正式版' if export_mode == 'formal' else '草稿版'}",
f"产品名称:{product_name}",
f"批次号:{batch_id}",
f"模板:{template_mapping.get('template_name', '')} {template_mapping.get('version', '')}".strip(),
f"风险摘要:{summary or ''}",
"回填字段:",
]
document_lines.extend(
f"- {item['field_name']}{item['field_value']}" for item in fillable_items
)
if blocked_items:
document_lines.append("阻断项:")
document_lines.extend(f"- {item}" for item in blocked_items)
else:
document_lines.append("阻断项:无")
document_xml = _build_document_xml(document_lines)
with zipfile.ZipFile(output_path, "w", compression=zipfile.ZIP_DEFLATED) as archive:
archive.writestr("[Content_Types].xml", _content_types_xml())
archive.writestr("_rels/.rels", _root_rels_xml())
archive.writestr("word/document.xml", document_xml)
archive.writestr("word/_rels/document.xml.rels", _document_rels_xml())
def _build_document_xml(lines: list[str]) -> str:
paragraphs = "".join(
f"<w:p><w:r><w:t xml:space='preserve'>{escape(line)}</w:t></w:r></w:p>" for line in lines
)
return (
"<?xml version='1.0' encoding='UTF-8' standalone='yes'?>"
"<w:document xmlns:w='http://schemas.openxmlformats.org/wordprocessingml/2006/main'>"
"<w:body>"
f"{paragraphs}"
"<w:sectPr/>"
"</w:body>"
"</w:document>"
)
def _content_types_xml() -> str:
return (
"<?xml version='1.0' encoding='UTF-8'?>"
"<Types xmlns='http://schemas.openxmlformats.org/package/2006/content-types'>"
"<Default Extension='rels' ContentType='application/vnd.openxmlformats-package.relationships+xml'/>"
"<Default Extension='xml' ContentType='application/xml'/>"
"<Override PartName='/word/document.xml' "
"ContentType='application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml'/>"
"</Types>"
)
def _root_rels_xml() -> str:
return (
"<?xml version='1.0' encoding='UTF-8'?>"
"<Relationships xmlns='http://schemas.openxmlformats.org/package/2006/relationships'>"
"<Relationship Id='rId1' "
"Type='http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument' "
"Target='word/document.xml'/>"
"</Relationships>"
)
def _document_rels_xml() -> str:
return (
"<?xml version='1.0' encoding='UTF-8'?>"
"<Relationships xmlns='http://schemas.openxmlformats.org/package/2006/relationships'/>"
)

View File

@@ -1,4 +1,7 @@
from django import forms from django import forms
from pathlib import Path
from apps.documents.forms import MultipleFileField, SUPPORTED_EXTENSIONS
class ChatForm(forms.Form): class ChatForm(forms.Form):
@@ -38,3 +41,23 @@ class ChatForm(forms.Form):
def clean_document_ids(self): def clean_document_ids(self):
# View 与 Agent Core 都使用整型文档 ID统一在表单层完成转换。 # View 与 Agent Core 都使用整型文档 ID统一在表单层完成转换。
return [int(document_id) for document_id in self.cleaned_data.get("document_ids", [])] return [int(document_id) for document_id in self.cleaned_data.get("document_ids", [])]
class ConversationUploadForm(forms.Form):
# 会话右侧上传区只负责继续补传资料,不修改会话绑定关系。
files = MultipleFileField(label="补充文件或资料包", required=False)
file = forms.FileField(label="兼容单文件上传", required=False)
def clean(self):
cleaned_data = super().clean()
files = list(cleaned_data.get("files") or [])
file = cleaned_data.get("file")
if file:
files.append(file)
if not files:
raise forms.ValidationError("请至少上传一个文件或资料包。")
for uploaded_file in files:
if Path(uploaded_file.name).suffix.lower() not in SUPPORTED_EXTENSIONS:
raise forms.ValidationError("仅支持 .txt、.md、.pdf、.docx、.zip 和 .7z 文件")
cleaned_data["uploaded_files"] = files
return cleaned_data

View File

@@ -0,0 +1,52 @@
# Generated by Django 5.2.14 on 2026-06-03 16:39
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name="Conversation",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"conversation_id",
models.CharField(db_index=True, max_length=64, unique=True),
),
("title", models.CharField(max_length=255)),
(
"product_name",
models.CharField(blank=True, db_index=True, max_length=255),
),
(
"batch_id",
models.CharField(blank=True, db_index=True, max_length=64),
),
(
"task_status",
models.CharField(db_index=True, default="pending", max_length=32),
),
("node_results", models.JSONField(blank=True, default=list)),
("latest_summary", models.JSONField(blank=True, default=dict)),
("created_at", models.DateTimeField(auto_now_add=True, db_index=True)),
("updated_at", models.DateTimeField(auto_now=True)),
("last_run_at", models.DateTimeField(blank=True, null=True)),
],
options={
"ordering": ["-updated_at", "-created_at"],
},
),
]

View File

46
apps/chat/models.py Normal file
View File

@@ -0,0 +1,46 @@
from django.db import models
class Conversation(models.Model):
"""
审核智能体会话主对象。
会话与资料包一一绑定,标题默认使用解析出的产品名称,
节点结果使用 JSON 挂载,便于页面按节点展示。
"""
STATUS_PENDING = "pending"
STATUS_PROCESSING = "processing"
STATUS_COMPLETED = "completed"
STATUS_REVIEW_REQUIRED = "review_required"
STATUS_BLOCKED = "blocked"
STATUS_FAILED = "failed"
conversation_id = models.CharField(max_length=64, unique=True, db_index=True)
title = models.CharField(max_length=255)
product_name = models.CharField(max_length=255, blank=True, db_index=True)
batch_id = models.CharField(max_length=64, blank=True, db_index=True)
task_status = models.CharField(max_length=32, default=STATUS_PENDING, db_index=True)
node_results = models.JSONField(default=list, blank=True)
latest_summary = models.JSONField(default=dict, blank=True)
created_at = models.DateTimeField(auto_now_add=True, db_index=True)
updated_at = models.DateTimeField(auto_now=True)
last_run_at = models.DateTimeField(null=True, blank=True)
class Meta:
ordering = ["-updated_at", "-created_at"]
def __str__(self) -> str:
return self.title
def get_task_status_display_text(self) -> str:
"""返回会话阶段的中文展示文案。"""
return {
self.STATUS_PENDING: "处理中",
self.STATUS_PROCESSING: "处理中",
self.STATUS_COMPLETED: "已完成",
self.STATUS_REVIEW_REQUIRED: "待复核",
self.STATUS_BLOCKED: "已阻断",
self.STATUS_FAILED: "失败",
"success": "已完成",
}.get(self.task_status, self.task_status)

205
apps/chat/services.py Normal file
View File

@@ -0,0 +1,205 @@
from collections.abc import Callable
from django.utils import timezone
from agent_core.orchestrator import run_agent
from agent_core.results import AgentResult
from apps.audit.services import create_audit_log, create_notification_record
from apps.scenarios.services import get_scenario
from .models import Conversation
def create_conversation_for_batch(batch_id: str, product_name: str) -> Conversation:
"""
为资料包创建主会话。
会话标题固定优先使用解析出的产品名称,
缺失时回退到批次号,确保前台始终有稳定标题。
"""
conversation = Conversation.objects.create(
conversation_id=_generate_conversation_id(),
title=product_name or f"未命名资料包-{batch_id}",
product_name=product_name,
batch_id=batch_id,
task_status=Conversation.STATUS_PENDING,
node_results=_build_initial_node_results(),
)
return conversation
def create_knowledge_conversation() -> Conversation:
"""
创建未绑定资料包的知识库问答会话。
该会话用于用户尚未上传资料时直接向 RAG 知识库提问,
因此 batch_id 与 product_name 保持为空Agent Core 通过空范围执行全局检索。
"""
return Conversation.objects.create(
conversation_id=_generate_conversation_id(),
title="知识库问答会话",
product_name="",
batch_id="",
task_status=Conversation.STATUS_PENDING,
node_results=_build_knowledge_node_results(),
)
def execute_conversation_agent(
*,
conversation: Conversation,
message: str,
document_ids: list[int],
detail_url_builder: Callable[[int], str] | None = None,
) -> tuple[AgentResult, object]:
"""
在服务层串起会话执行、审计留痕与通知落库。
View 只负责收集请求参数和渲染结果,不直接承载 Agent Core 编排。
"""
scenario = get_scenario("document_review")
try:
result = run_agent(
scenario,
message,
options={
"conversation_id": conversation.conversation_id,
"batch_id": conversation.batch_id,
"product_name": conversation.product_name,
"document_ids": document_ids,
},
)
except Exception as exc:
result = AgentResult(status="failed", error=str(exc), answer="")
audit_log = create_audit_log(
"document_review",
"注册审核智能体",
message,
result,
batch_id=conversation.batch_id,
conversation_id=conversation.conversation_id,
product_name=conversation.product_name,
)
_apply_agent_result_to_conversation(conversation, result)
detail_url = detail_url_builder(audit_log.id) if detail_url_builder else ""
_persist_notification_records(result, web_detail_url=detail_url)
return result, audit_log
def execute_conversation_export(*, batch, conversation: Conversation) -> dict:
"""
在服务层串起 Word 导出、会话摘要更新和审计留痕。
View 只负责提示成功/失败消息,不直接承载导出编排细节。
"""
from .export_service import (
generate_registration_export,
update_conversation_with_export_report,
)
upstream_summary = (
(conversation.latest_summary or {}).get("upstream_structured_output")
or (conversation.latest_summary or {}).get("structured_output")
or {}
)
export_report = generate_registration_export(
batch=batch,
conversation=conversation,
upstream_summary=upstream_summary,
)
update_conversation_with_export_report(conversation, export_report)
audit_log = create_audit_log(
"document_review",
"Word 回填导出",
"生成 Word 导出文件",
AgentResult(
answer=export_report.get("summary", ""),
structured_output=export_report,
status="success",
conversation_id=conversation.conversation_id,
batch_id=conversation.batch_id,
product_name=conversation.product_name,
node_results=conversation.node_results,
),
batch_id=conversation.batch_id,
conversation_id=conversation.conversation_id,
product_name=conversation.product_name,
)
return {
"export_report": export_report,
"audit_log": audit_log,
}
def _generate_conversation_id() -> str:
return f"conv-{Conversation.objects.count() + 1:03d}"
def _build_initial_node_results() -> list[dict]:
return [
{"code": "package_import", "label": "资料包导入", "status": "已完成"},
{"code": "overview", "label": "目录汇总", "status": "处理中"},
{"code": "completeness", "label": "法规完整性检查", "status": "待处理"},
{"code": "field_extraction", "label": "字段抽取", "status": "待处理"},
{"code": "consistency", "label": "一致性核查", "status": "待处理"},
{"code": "risk", "label": "风险预警", "status": "待处理"},
{"code": "word_export", "label": "Word 回填导出", "status": "待处理"},
{"code": "feishu_notify", "label": "飞书通知", "status": "待处理"},
]
def _build_knowledge_node_results() -> list[dict]:
return [
{"code": "knowledge_retrieval", "label": "知识库检索", "status": "待处理"},
{"code": "answer_generation", "label": "问答生成", "status": "待处理"},
{"code": "risk", "label": "风险预警", "status": "待处理"},
]
def _persist_notification_records(result: AgentResult, *, web_detail_url: str = "") -> None:
payload = result.notification_payload or {}
owners = payload.get("owners") or []
if not owners:
return
resolved_detail_url = payload.get("web_detail_url") or web_detail_url
resolved_message_status = payload.get("message_status") or (
"sent" if result.status == "success" else "failed"
)
resolved_receipt = payload.get("receipt") or {"status": result.status}
for owner in owners:
create_notification_record(
batch_id=payload.get("batch_id", ""),
conversation_id=payload.get("conversation_id", ""),
product_name=payload.get("product_name", ""),
trigger_source="agent_execution",
notify_reason=payload.get("notify_reason", "task_completed"),
owner_role=owner.get("owner_role", ""),
feishu_user_id=owner.get("feishu_user_id", ""),
message_status=resolved_message_status,
web_detail_url=resolved_detail_url,
receipt=resolved_receipt,
)
def _apply_agent_result_to_conversation(conversation: Conversation, result: AgentResult) -> None:
conversation.task_status = result.status
if result.node_results:
conversation.node_results = result.node_results
conversation.latest_summary = {
"answer": result.answer,
"status": result.status,
"error": result.error,
"structured_output": result.structured_output,
"notification_payload": result.notification_payload,
}
conversation.last_run_at = timezone.now()
conversation.save(
update_fields=[
"task_status",
"node_results",
"latest_summary",
"last_run_at",
"updated_at",
]
)

View File

@@ -5,7 +5,10 @@ from . import views
app_name = "chat" app_name = "chat"
# 当前 V1 仅保留一个场景对话入口,场景详情合并在对话页中展示 # 审核智能体前台以会话为中心
urlpatterns = [ urlpatterns = [
path("<str:scenario_id>/", views.index, name="index"), path("", views.index, name="index"),
path("<str:conversation_id>/", views.detail, name="detail"),
path("<str:conversation_id>/upload/", views.upload_documents, name="upload-documents"),
path("<str:conversation_id>/export-word/", views.export_word, name="export-word"),
] ]

View File

@@ -1,38 +1,112 @@
from django.shortcuts import render from django.contrib import messages
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.views.decorators.http import require_POST
from agent_core.orchestrator import run_agent
from agent_core.results import AgentResult from agent_core.results import AgentResult
from apps.audit.services import create_audit_log from apps.documents.models import SubmissionBatch, UploadedDocument
from apps.documents.models import UploadedDocument from apps.documents.services import append_documents_to_batch
from apps.scenarios.services import ScenarioNotFound, get_scenario
from .forms import ChatForm from .forms import ChatForm, ConversationUploadForm
from .models import Conversation
from .services import (
create_knowledge_conversation,
execute_conversation_agent,
execute_conversation_export,
)
RISK_LEVEL_DISPLAY = {
"high": "",
"medium": "",
"low": "",
}
PASS_STATUS_DISPLAY = {
"blocked": "已阻断",
"failed": "失败",
"review_required": "待复核",
"manual_review": "待复核",
"completed": "已完成",
"passed": "已完成",
}
EXPORT_STATUS_DISPLAY = {
"completed": "已完成",
"draft_only": "待复核",
"review_required": "待复核",
"manual_review": "待复核",
"blocked": "已阻断",
"failed": "失败",
"processing": "处理中",
"pending": "处理中",
}
NOTIFY_MESSAGE_STATUS_DISPLAY = {
"sent": "已发送",
"failed": "失败",
"pending": "处理中",
}
def index(request, scenario_id: str): def index(request):
# View 只负责请求编排、表单校验和模板渲染。 conversations = Conversation.objects.all()
# 具体 Agent 执行、审计写入和文档筛选规则分别交给独立模块处理。 if conversations.exists():
try: return redirect("chat:detail", conversation_id=conversations.first().conversation_id)
scenario = get_scenario(scenario_id) documents = UploadedDocument.objects.filter(batch__isnull=True)
except ScenarioNotFound: form = ChatForm(request.POST or None, documents=documents)
upload_form = ConversationUploadForm()
result = None
audit_log = None
conversation = None
if request.method == "POST" and form.is_valid():
conversation = create_knowledge_conversation()
result, audit_log = execute_conversation_agent(
conversation=conversation,
message=form.cleaned_data["message"],
document_ids=form.cleaned_data["document_ids"],
detail_url_builder=lambda log_id: reverse("audit:detail", args=[log_id]),
)
conversation.refresh_from_db()
documents = UploadedDocument.objects.filter(batch__isnull=True)
display_node_results = _normalize_node_results(conversation.node_results if conversation else [])
workspace_summary = _build_workspace_summary(conversation, None, display_node_results) if conversation else _build_empty_workspace_summary()
return render( return render(
request, request,
"chat/index.html", "chat/index.html",
{ {
"scenario": None, "conversation": conversation,
"form": ChatForm(), "conversations": [],
"error": "场景不存在,请返回首页检查配置。", "conversation_history": [],
"batch": None,
"form": form,
"documents": documents,
"document_count": documents.count(),
"result": result,
"audit_log": audit_log,
"node_results": display_node_results,
"active_node": None,
"workspace_summary": workspace_summary,
"conversation_context": _build_conversation_context(conversation, None, workspace_summary) if conversation else {},
"prompt_templates": _build_prompt_templates(),
"analysis_card": _build_analysis_card(result, conversation) if conversation else {},
"upload_form": upload_form,
"export_card": _build_export_card(result, conversation) if conversation else {},
"risk_card": _build_risk_card(result, conversation) if conversation else {},
"notify_card": _build_notify_card(result, conversation) if conversation else {},
}, },
status=404,
) )
def detail(request, conversation_id: str):
conversation = get_object_or_404(Conversation, conversation_id=conversation_id)
batch = SubmissionBatch.objects.filter(batch_id=conversation.batch_id).first()
documents = UploadedDocument.objects.filter(batch=batch)
form = ChatForm(request.POST or None, documents=documents)
upload_form = ConversationUploadForm()
result = None result = None
audit_log = None audit_log = None
documents = UploadedDocument.objects.filter( active_node = None
scenario_id=scenario["id"],
status=UploadedDocument.STATUS_INDEXED,
)
form = ChatForm(request.POST or None, documents=documents)
task_modes = [ task_modes = [
{"name": "目录汇总", "description": "汇总文件、页数、章节点和目录型文档。"}, {"name": "目录汇总", "description": "汇总文件、页数、章节点和目录型文档。"},
{"name": "完整性检查", "description": "对照法规模板检查齐套性、缺失项和错放项。"}, {"name": "完整性检查", "description": "对照法规模板检查齐套性、缺失项和错放项。"},
@@ -42,27 +116,335 @@ def index(request, scenario_id: str):
] ]
if request.method == "POST" and form.is_valid(): if request.method == "POST" and form.is_valid():
message = form.cleaned_data["message"] message = form.cleaned_data["message"]
try: result, audit_log = execute_conversation_agent(
# 只把必要的运行选项传给 Agent Core避免在 View 中散落模型细节。 conversation=conversation,
result = run_agent( message=message,
scenario, document_ids=form.cleaned_data["document_ids"],
message, detail_url_builder=lambda log_id: reverse("audit:detail", args=[log_id]),
options={"document_ids": form.cleaned_data["document_ids"]},
) )
except Exception as exc: active_node = "risk"
result = AgentResult(status="failed", error=str(exc), answer="") conversation.refresh_from_db()
audit_log = create_audit_log(scenario["id"], scenario["name"], message, result) display_node_results = _normalize_node_results(conversation.node_results)
workspace_summary = _build_workspace_summary(conversation, batch, display_node_results)
conversation_context = _build_conversation_context(conversation, batch, workspace_summary)
prompt_templates = _build_prompt_templates()
analysis_card = _build_analysis_card(result, conversation)
export_card = _build_export_card(result, conversation)
risk_card = _build_risk_card(result, conversation)
notify_card = _build_notify_card(result, conversation)
conversation_history = _build_conversation_history(Conversation.objects.all())
return render( return render(
request, request,
"chat/index.html", "chat/index.html",
{ {
"scenario": scenario, "conversation": conversation,
"conversations": Conversation.objects.all(),
"conversation_history": conversation_history,
"batch": batch,
"form": form, "form": form,
"documents": documents, "documents": documents,
"document_count": documents.count(), "document_count": documents.count(),
"result": result, "result": result,
"audit_log": audit_log, "audit_log": audit_log,
"task_modes": task_modes, "task_modes": task_modes,
"node_results": display_node_results,
"active_node": active_node,
"workspace_summary": workspace_summary,
"conversation_context": conversation_context,
"prompt_templates": prompt_templates,
"analysis_card": analysis_card,
"upload_form": upload_form,
"export_card": export_card,
"risk_card": risk_card,
"notify_card": notify_card,
}, },
) )
@require_POST
def upload_documents(request, conversation_id: str):
conversation = get_object_or_404(Conversation, conversation_id=conversation_id)
batch = get_object_or_404(SubmissionBatch, batch_id=conversation.batch_id)
upload_form = ConversationUploadForm(request.POST, request.FILES)
if upload_form.is_valid():
result = append_documents_to_batch(
"document_review",
batch,
upload_form.cleaned_data["uploaded_files"],
)
warning_count = len(result["registration_overview_report"]["warnings"])
message = "资料已补充到当前资料包。"
if warning_count:
message += f" 当前有 {warning_count} 条待复核提示。"
messages.success(request, message)
else:
messages.error(
request,
"补充资料失败:" + " ".join(upload_form.non_field_errors()) if upload_form.non_field_errors() else "补充资料失败。",
)
return redirect("chat:detail", conversation_id=conversation.conversation_id)
@require_POST
def export_word(request, conversation_id: str):
conversation = get_object_or_404(Conversation, conversation_id=conversation_id)
batch = get_object_or_404(SubmissionBatch, batch_id=conversation.batch_id)
try:
execute_conversation_export(
batch=batch,
conversation=conversation,
)
messages.success(request, "已生成新的 Word 导出文件。")
except Exception as exc:
messages.error(request, f"Word 导出失败:{exc}")
return redirect("chat:detail", conversation_id=conversation.conversation_id)
def _build_workspace_summary(
conversation: Conversation,
batch: SubmissionBatch | None,
display_node_results: list[dict] | None = None,
) -> dict:
normalized_nodes = display_node_results or _normalize_node_results(conversation.node_results)
node_status_map = {node.get("label"): node.get("status", "") for node in normalized_nodes}
risk_status = node_status_map.get("风险预警", "待处理")
notify_status = node_status_map.get("飞书通知", "待处理")
export_status = node_status_map.get("Word 回填导出", "待处理")
highest_risk_level = "" if risk_status in {"已阻断", "待复核"} else ""
latest_summary = conversation.latest_summary or {}
structured_output = latest_summary.get("structured_output") or {}
explicit_export_flag = structured_output.get("can_export_formally")
export_allowed = (
""
if explicit_export_flag is True
else ""
if explicit_export_flag is False
else ""
if risk_status in {"已阻断", "待复核"} or export_status in {"已阻断", "待复核", "失败"}
else ""
)
return {
"highest_risk_level": highest_risk_level,
"export_allowed": export_allowed,
"notify_status": notify_status,
"export_status": export_status,
"download_url": structured_output.get("download_url", ""),
"file_count": batch.file_count if batch else 0,
"page_count": batch.page_count if batch else 0,
}
def _build_empty_workspace_summary() -> dict:
return {
"highest_risk_level": "-",
"export_allowed": "",
"notify_status": "待处理",
"export_status": "待处理",
"download_url": "",
"file_count": 0,
"page_count": 0,
}
def _build_conversation_context(
conversation: Conversation,
batch: SubmissionBatch | None,
workspace_summary: dict,
) -> dict:
return {
"batch_id": conversation.batch_id,
"product_name": conversation.product_name,
"workflow_type": batch.workflow_type if batch else "registration",
"task_status": conversation.get_task_status_display_text(),
"highest_risk_level": workspace_summary.get("highest_risk_level", "-"),
"export_allowed": workspace_summary.get("export_allowed", "-"),
}
def _build_prompt_templates() -> list[str]:
return [
"请汇总当前资料包的章节点、页数和目录覆盖情况",
"请检查当前资料包缺失了哪些必交项和错放项",
"请抽取当前资料包的核心字段并标记低置信度项",
"请给出当前资料包的高风险项、责任人和整改建议",
]
def _build_conversation_history(conversations) -> list[dict]:
"""
组装左栏会话历史摘要。
左栏只展示稳定摘要字段,不在模板里拼风险判断逻辑。
"""
history = []
for item in conversations:
node_status_map = {node.get("label"): node.get("status", "") for node in item.node_results}
risk_status = node_status_map.get("风险预警", "待处理")
history.append(
{
"conversation_id": item.conversation_id,
"title": item.title,
"product_name": item.product_name,
"batch_id": item.batch_id,
"risk_level": "" if risk_status in {"已阻断", "待复核"} else "",
"updated_at": item.updated_at,
"batch_binding_label": "已绑定资料包" if item.batch_id else "未绑定资料包",
}
)
return history
def _build_analysis_card(result: AgentResult | None, conversation: Conversation) -> dict:
structured_output = {}
if result and result.structured_output:
structured_output = result.structured_output
else:
structured_output = (conversation.latest_summary or {}).get("structured_output") or {}
output_type = structured_output.get("output_type")
if output_type == "registration_overview_report":
return {
"kind": "overview",
"title": "目录汇总能力卡",
"summary": structured_output.get("product_name", ""),
"stats": [
{"label": "资料文件数", "value": structured_output.get("file_count", 0)},
{"label": "总页数", "value": structured_output.get("total_page_count", 0)},
],
"items": structured_output.get("chapter_summary") or [],
"warnings": structured_output.get("warnings") or [],
}
if output_type == "registration_completeness_report":
return {
"kind": "completeness",
"title": "完整性检查能力卡",
"summary": structured_output.get("summary", ""),
"stats": [{"label": "风险等级", "value": _get_risk_level_display_text(structured_output.get("risk_level", "-"))}],
"items": structured_output.get("missing_items") or [],
"warnings": structured_output.get("misplaced_items") or [],
}
if output_type == "registration_field_extraction_report":
return {
"kind": "field_extraction",
"title": "字段抽取能力卡",
"summary": structured_output.get("summary", ""),
"stats": [{"label": "字段数", "value": len(structured_output.get("field_items") or [])}],
"items": structured_output.get("field_items") or [],
"warnings": structured_output.get("low_confidence_items") or [],
}
if output_type == "registration_consistency_report":
return {
"kind": "consistency",
"title": "一致性核查能力卡",
"summary": structured_output.get("summary", ""),
"stats": [{"label": "风险等级", "value": _get_risk_level_display_text(structured_output.get("risk_level", "-"))}],
"items": structured_output.get("conflict_items") or [],
"warnings": structured_output.get("mixed_document_risks") or [],
}
return {}
def _build_export_card(result: AgentResult | None, conversation: Conversation) -> dict:
"""
统一组装 Word 导出能力卡上下文。
优先使用本次执行结果;若本次未执行,则回退到会话最新摘要。
"""
structured_output = {}
if result and result.structured_output:
structured_output = result.structured_output
else:
structured_output = (conversation.latest_summary or {}).get("structured_output") or {}
if structured_output.get("output_type") != "registration_word_export_report":
return {}
return {
"template_name": structured_output.get("template_name", ""),
"template_version": structured_output.get("template_version", ""),
"export_status": _get_export_status_display_text(structured_output.get("export_status", "")),
"filled_fields": structured_output.get("filled_fields") or [],
"blocked_fields": structured_output.get("blocked_fields") or [],
"download_url": structured_output.get("download_url", ""),
}
def _build_risk_card(result: AgentResult | None, conversation: Conversation) -> dict:
structured_output = {}
if result and result.structured_output:
structured_output = result.structured_output
else:
structured_output = (conversation.latest_summary or {}).get("structured_output") or {}
if structured_output.get("output_type") != "registration_risk_report":
return {}
return {
"summary": structured_output.get("summary", ""),
"highest_risk_level": _get_risk_level_display_text(
structured_output.get("highest_risk_level", "")
),
"pass_status": _get_pass_status_display_text(structured_output.get("pass_status", "")),
"manual_review_items": structured_output.get("manual_review_items") or [],
"risk_items": structured_output.get("risk_items") or [],
"owner_roles": structured_output.get("owner_roles") or [],
}
def _build_notify_card(result: AgentResult | None, conversation: Conversation) -> dict:
latest_summary = conversation.latest_summary or {}
structured_output = latest_summary.get("structured_output") or {}
notification_payload = latest_summary.get("notification_payload") or {}
if result and result.structured_output:
structured_output = result.structured_output
if result and result.notification_payload:
notification_payload = result.notification_payload
notify_reason = (
structured_output.get("notify_reason")
or notification_payload.get("notify_reason")
or ""
)
mentioned_users = structured_output.get("mentioned_users") or notification_payload.get("mentioned_users") or []
message_status = structured_output.get("message_status") or notification_payload.get("message_status") or ""
web_detail_url = structured_output.get("web_detail_url") or notification_payload.get("web_detail_url") or ""
owners = structured_output.get("owner_roles") or notification_payload.get("owners") or []
if not any([notify_reason, mentioned_users, message_status, web_detail_url, owners]):
return {}
return {
"notify_reason": notify_reason,
"mentioned_users": mentioned_users,
"message_status": _get_notify_message_status_display_text(message_status),
"web_detail_url": web_detail_url,
"owners": owners,
}
def _normalize_node_results(node_results: list[dict]) -> list[dict]:
normalized = []
for node in node_results or []:
item = dict(node)
label = item.get("label", "")
status = item.get("status", "")
if label == "飞书通知":
if status in {"已完成", "success", "sent"}:
item["status"] = "已发送"
elif status in {"failed", "error"}:
item["status"] = "失败"
elif status in {"pending", "processing"}:
item["status"] = "待处理"
normalized.append(item)
return normalized
def _get_risk_level_display_text(level: str) -> str:
return RISK_LEVEL_DISPLAY.get(level, level)
def _get_pass_status_display_text(status: str) -> str:
return PASS_STATUS_DISPLAY.get(status, status)
def _get_export_status_display_text(status: str) -> str:
return EXPORT_STATUS_DISPLAY.get(status, status)
def _get_notify_message_status_display_text(status: str) -> str:
return NOTIFY_MESSAGE_STATUS_DISPLAY.get(status, status)

View File

@@ -1,6 +1,6 @@
from django.contrib import admin from django.contrib import admin
from .models import UploadedDocument from .models import ExportedDocument, UploadedDocument
@admin.register(UploadedDocument) @admin.register(UploadedDocument)
@@ -9,3 +9,19 @@ class UploadedDocumentAdmin(admin.ModelAdmin):
list_display = ("id", "original_name", "scenario_id", "file_type", "status", "created_at") list_display = ("id", "original_name", "scenario_id", "file_type", "status", "created_at")
list_filter = ("status", "scenario_id", "file_type") list_filter = ("status", "scenario_id", "file_type")
search_fields = ("original_name", "scenario_id") search_fields = ("original_name", "scenario_id")
@admin.register(ExportedDocument)
class ExportedDocumentAdmin(admin.ModelAdmin):
"""管理导出记录,便于按批次、会话和产品回看导出产物。"""
list_display = (
"id",
"file_name",
"batch",
"conversation_id",
"product_name",
"export_mode",
"created_at",
)
list_filter = ("export_mode", "output_type", "template_name")
search_fields = ("file_name", "batch__batch_id", "conversation_id", "product_name")

View File

@@ -5,14 +5,31 @@ from django import forms
from apps.scenarios.services import ScenarioNotFound, get_scenario from apps.scenarios.services import ScenarioNotFound, get_scenario
from apps.scenarios.services import list_scenarios from apps.scenarios.services import list_scenarios
SUPPORTED_EXTENSIONS = {".txt", ".md", ".pdf", ".docx"} SUPPORTED_EXTENSIONS = {".txt", ".md", ".pdf", ".docx", ".zip", ".7z", ".rar"}
class MultipleFileInput(forms.ClearableFileInput):
allow_multiple_selected = True
class MultipleFileField(forms.FileField):
widget = MultipleFileInput
def clean(self, data, initial=None):
single_file_clean = super().clean
if not data:
return []
if isinstance(data, (list, tuple)):
return [single_file_clean(item, initial) for item in data]
return [single_file_clean(data, initial)]
class DocumentUploadForm(forms.Form): class DocumentUploadForm(forms.Form):
# 使用 ChoiceField 让表单自己维护场景选项, # 使用 ChoiceField 让表单自己维护场景选项,
# 这样模板、校验和后续扩展都能围绕一个入口完成。 # 这样模板、校验和后续扩展都能围绕一个入口完成。
scenario_id = forms.ChoiceField(label="场景", choices=()) scenario_id = forms.ChoiceField(label="场景", choices=())
file = forms.FileField(label="文件") files = MultipleFileField(label="文件或资料包", required=False)
file = forms.FileField(label="兼容单文件上传", required=False)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@@ -31,7 +48,28 @@ class DocumentUploadForm(forms.Form):
def clean_file(self): def clean_file(self):
uploaded_file = self.cleaned_data["file"] uploaded_file = self.cleaned_data["file"]
if not uploaded_file:
return uploaded_file
extension = Path(uploaded_file.name).suffix.lower() extension = Path(uploaded_file.name).suffix.lower()
if extension not in SUPPORTED_EXTENSIONS: if extension not in SUPPORTED_EXTENSIONS:
raise forms.ValidationError("仅支持 .txt、.md、.pdf.docx 文件") raise forms.ValidationError("仅支持 .txt、.md、.pdf.docx、.zip、.7z 和 .rar 文件")
return uploaded_file return uploaded_file
def clean_files(self):
uploaded_files = self.cleaned_data.get("files") or []
for uploaded_file in uploaded_files:
extension = Path(uploaded_file.name).suffix.lower()
if extension not in SUPPORTED_EXTENSIONS:
raise forms.ValidationError("仅支持 .txt、.md、.pdf、.docx、.zip、.7z 和 .rar 文件")
return uploaded_files
def clean(self):
cleaned_data = super().clean()
files = list(cleaned_data.get("files") or [])
file = cleaned_data.get("file")
if file:
files.append(file)
if not files:
raise forms.ValidationError("请至少上传一个文件或一个 zip 资料包。")
cleaned_data["uploaded_files"] = files
return cleaned_data

View File

@@ -0,0 +1,103 @@
# Generated by Django 5.2.14 on 2026-06-03 16:39
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("documents", "0001_initial"),
]
operations = [
migrations.CreateModel(
name="SubmissionBatch",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"batch_id",
models.CharField(db_index=True, max_length=64, unique=True),
),
(
"product_name",
models.CharField(blank=True, db_index=True, max_length=255),
),
(
"workflow_type",
models.CharField(default="registration", max_length=64),
),
(
"conversation_id",
models.CharField(blank=True, db_index=True, max_length=64),
),
("file_count", models.PositiveIntegerField(default=0)),
("page_count", models.PositiveIntegerField(default=0)),
("chapter_summary", models.JSONField(blank=True, default=list)),
(
"import_status",
models.CharField(db_index=True, default="pending", max_length=32),
),
("exception_count", models.PositiveIntegerField(default=0)),
("created_at", models.DateTimeField(auto_now_add=True, db_index=True)),
("updated_at", models.DateTimeField(auto_now=True)),
],
options={
"ordering": ["-created_at"],
},
),
migrations.AddField(
model_name="uploadeddocument",
name="chapter_code",
field=models.CharField(blank=True, max_length=32),
),
migrations.AddField(
model_name="uploadeddocument",
name="chapter_match_status",
field=models.CharField(blank=True, max_length=32),
),
migrations.AddField(
model_name="uploadeddocument",
name="document_role",
field=models.CharField(blank=True, max_length=64),
),
migrations.AddField(
model_name="uploadeddocument",
name="needs_manual_review",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="uploadeddocument",
name="page_count",
field=models.PositiveIntegerField(default=0),
),
migrations.AddField(
model_name="uploadeddocument",
name="page_count_confidence",
field=models.CharField(blank=True, max_length=32),
),
migrations.AddField(
model_name="uploadeddocument",
name="relative_path",
field=models.CharField(blank=True, max_length=500),
),
migrations.AddField(
model_name="uploadeddocument",
name="batch",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="documents",
to="documents.submissionbatch",
),
),
]

View File

@@ -0,0 +1,52 @@
# Generated by Django 5.2.14 on 2026-06-04 18:20
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("documents", "0002_submissionbatch_uploadeddocument_chapter_code_and_more"),
]
operations = [
migrations.CreateModel(
name="ExportedDocument",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("conversation_id", models.CharField(db_index=True, max_length=64)),
("product_name", models.CharField(blank=True, db_index=True, max_length=255)),
("template_name", models.CharField(blank=True, max_length=100)),
("template_version", models.CharField(blank=True, max_length=50)),
("export_mode", models.CharField(db_index=True, max_length=32)),
(
"output_type",
models.CharField(default="registration_word_export_report", max_length=100),
),
("file_name", models.CharField(max_length=255)),
("relative_path", models.CharField(max_length=500)),
("download_url", models.CharField(blank=True, max_length=500)),
("created_at", models.DateTimeField(auto_now_add=True, db_index=True)),
(
"batch",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="export_records",
to="documents.submissionbatch",
),
),
],
options={
"ordering": ["-created_at"],
},
),
]

View File

@@ -1,6 +1,48 @@
from django.db import models from django.db import models
class SubmissionBatch(models.Model):
"""
资料包主对象,承接导入、会话绑定和目录汇总结果。
Documents 模块负责维护资料包与文件的关系,
不在模型层耦合 Agent 执行细节。
"""
STATUS_PENDING = "pending"
STATUS_PROCESSING = "processing"
STATUS_COMPLETED = "completed"
STATUS_REVIEW_REQUIRED = "review_required"
STATUS_FAILED = "failed"
batch_id = models.CharField(max_length=64, unique=True, db_index=True)
product_name = models.CharField(max_length=255, blank=True, db_index=True)
workflow_type = models.CharField(max_length=64, default="registration")
conversation_id = models.CharField(max_length=64, blank=True, db_index=True)
file_count = models.PositiveIntegerField(default=0)
page_count = models.PositiveIntegerField(default=0)
chapter_summary = models.JSONField(default=list, blank=True)
import_status = models.CharField(max_length=32, default=STATUS_PENDING, db_index=True)
exception_count = models.PositiveIntegerField(default=0)
created_at = models.DateTimeField(auto_now_add=True, db_index=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ["-created_at"]
def __str__(self) -> str:
return self.product_name or self.batch_id
def get_import_status_display_text(self) -> str:
return {
self.STATUS_PENDING: "待导入",
self.STATUS_PROCESSING: "处理中",
self.STATUS_COMPLETED: "已完成",
self.STATUS_REVIEW_REQUIRED: "待复核",
self.STATUS_FAILED: "失败",
}.get(self.import_status, self.import_status)
class UploadedDocument(models.Model): class UploadedDocument(models.Model):
""" """
保存用户上传文档的元数据和入库状态。 保存用户上传文档的元数据和入库状态。
@@ -13,11 +55,25 @@ class UploadedDocument(models.Model):
STATUS_INDEXED = "indexed" STATUS_INDEXED = "indexed"
STATUS_FAILED = "failed" STATUS_FAILED = "failed"
batch = models.ForeignKey(
SubmissionBatch,
related_name="documents",
null=True,
blank=True,
on_delete=models.CASCADE,
)
scenario_id = models.CharField(max_length=100, db_index=True) scenario_id = models.CharField(max_length=100, db_index=True)
original_name = models.CharField(max_length=255) original_name = models.CharField(max_length=255)
file = models.FileField(upload_to="documents/%Y%m%d/") file = models.FileField(upload_to="documents/%Y%m%d/")
file_type = models.CharField(max_length=20) file_type = models.CharField(max_length=20)
size = models.PositiveIntegerField(default=0) size = models.PositiveIntegerField(default=0)
relative_path = models.CharField(max_length=500, blank=True)
chapter_code = models.CharField(max_length=32, blank=True)
document_role = models.CharField(max_length=64, blank=True)
page_count = models.PositiveIntegerField(default=0)
page_count_confidence = models.CharField(max_length=32, blank=True)
chapter_match_status = models.CharField(max_length=32, blank=True)
needs_manual_review = models.BooleanField(default=False)
status = models.CharField(max_length=20, default=STATUS_UPLOADED, db_index=True) status = models.CharField(max_length=20, default=STATUS_UPLOADED, db_index=True)
error_message = models.TextField(blank=True) error_message = models.TextField(blank=True)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
@@ -36,3 +92,36 @@ class UploadedDocument(models.Model):
self.STATUS_INDEXED: "已入库,可检索", self.STATUS_INDEXED: "已入库,可检索",
self.STATUS_FAILED: "入库失败", self.STATUS_FAILED: "入库失败",
}.get(self.status, self.status) }.get(self.status, self.status)
class ExportedDocument(models.Model):
"""
导出文件记录。
该对象属于资料包治理范围:
- Documents 维护导出产物与资料包关系
- Chat 只负责触发导出动作
- Audit 负责回看执行痕迹
"""
batch = models.ForeignKey(
SubmissionBatch,
related_name="export_records",
on_delete=models.CASCADE,
)
conversation_id = models.CharField(max_length=64, db_index=True)
product_name = models.CharField(max_length=255, blank=True, db_index=True)
template_name = models.CharField(max_length=100, blank=True)
template_version = models.CharField(max_length=50, blank=True)
export_mode = models.CharField(max_length=32, db_index=True)
output_type = models.CharField(max_length=100, default="registration_word_export_report")
file_name = models.CharField(max_length=255)
relative_path = models.CharField(max_length=500)
download_url = models.CharField(max_length=500, blank=True)
created_at = models.DateTimeField(auto_now_add=True, db_index=True)
class Meta:
ordering = ["-created_at"]
def __str__(self) -> str:
return self.file_name

View File

@@ -1,14 +1,24 @@
from pathlib import Path from pathlib import Path
from io import BytesIO
import re import re
import tempfile
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
from zipfile import BadZipFile, ZipFile from zipfile import BadZipFile, ZipFile
from agent_core.rag.ingest import ingest_document from agent_core.rag.ingest import ingest_document
from apps.chat.services import create_conversation_for_batch
from django.core.files.uploadedfile import SimpleUploadedFile
from .models import UploadedDocument from .models import ExportedDocument, SubmissionBatch, UploadedDocument
def create_uploaded_document(scenario_id: str, uploaded_file) -> UploadedDocument: def create_uploaded_document(
scenario_id: str,
uploaded_file,
batch: SubmissionBatch | None = None,
*,
relative_path: str | None = None,
) -> UploadedDocument:
""" """
保存上传文件的元数据记录。 保存上传文件的元数据记录。
@@ -17,15 +27,323 @@ def create_uploaded_document(scenario_id: str, uploaded_file) -> UploadedDocumen
""" """
extension = _detect_extension(uploaded_file.name) extension = _detect_extension(uploaded_file.name)
return UploadedDocument.objects.create( return UploadedDocument.objects.create(
batch=batch,
scenario_id=scenario_id, scenario_id=scenario_id,
original_name=uploaded_file.name, original_name=Path(relative_path or uploaded_file.name).name,
file=uploaded_file, file=uploaded_file,
file_type=extension, file_type=extension,
size=uploaded_file.size, size=uploaded_file.size,
relative_path=relative_path or uploaded_file.name,
status=UploadedDocument.STATUS_UPLOADED, status=UploadedDocument.STATUS_UPLOADED,
) )
def import_submission_batch(scenario_id: str, uploaded_files: list) -> dict:
"""
导入资料包并建立批次、文档、目录汇总和主会话。
当前实现保持离线稳定,重点保证:
- 资料包记录可落库
- 产品名称可解析
- 会话可自动绑定
- 可直接产出 overview report
"""
batch = SubmissionBatch.objects.create(
batch_id=_generate_batch_id(),
workflow_type="registration",
import_status=SubmissionBatch.STATUS_PROCESSING,
)
ingest_result = _ingest_files_into_batch(
batch=batch,
scenario_id=scenario_id,
uploaded_files=uploaded_files,
)
documents = ingest_result["documents"]
warnings = ingest_result["warnings"]
product_name = ingest_result["product_name"]
conversation = create_conversation_for_batch(batch.batch_id, product_name)
if not documents:
warnings.append("未发现可导入的支持文件,请检查资料包格式或补充 PDF/DOCX/MD/TXT 文件。")
batch.product_name = product_name
batch.conversation_id = conversation.conversation_id
batch.file_count = len(documents)
batch.page_count = ingest_result["page_count"]
batch.chapter_summary = ingest_result["chapter_summary"]
batch.exception_count = len(warnings)
if not documents:
batch.import_status = SubmissionBatch.STATUS_FAILED
elif warnings:
batch.import_status = SubmissionBatch.STATUS_REVIEW_REQUIRED
else:
batch.import_status = SubmissionBatch.STATUS_COMPLETED
batch.save(
update_fields=[
"product_name",
"conversation_id",
"file_count",
"page_count",
"chapter_summary",
"exception_count",
"import_status",
"updated_at",
]
)
return {
"batch_id": batch.batch_id,
"conversation_id": conversation.conversation_id,
"product_name": batch.product_name,
"registration_overview_report": {
"batch_id": batch.batch_id,
"product_name": batch.product_name,
"file_count": batch.file_count,
"total_page_count": batch.page_count,
"chapter_summary": batch.chapter_summary,
"documents": [
{
"document_id": document.id,
"original_name": document.original_name,
"chapter_code": document.chapter_code,
"page_count": document.page_count,
"document_role": document.document_role,
}
for document in documents
],
"warnings": warnings,
},
}
def append_documents_to_batch(
scenario_id: str,
batch: SubmissionBatch,
uploaded_files: list,
) -> dict:
"""
在既有资料包下继续补传文件,并保持会话绑定不变。
该服务只负责 Documents 侧的数据更新:
- 新文件继续归属原 batch
- conversation_id 不变
- 如原产品名为空,可用新增文件补齐
- 如新增文件产品名与原产品名冲突,则转为待复核
"""
ingest_result = _ingest_files_into_batch(
batch=batch,
scenario_id=scenario_id,
uploaded_files=uploaded_files,
keep_existing_product_name=True,
)
warnings = list(ingest_result["warnings"])
all_documents = list(batch.documents.order_by("id"))
if not all_documents:
warnings.append("未发现可导入的支持文件,请检查资料包格式或补充 PDF/DOCX/MD/TXT 文件。")
batch.import_status = SubmissionBatch.STATUS_FAILED
elif warnings:
batch.import_status = SubmissionBatch.STATUS_REVIEW_REQUIRED
else:
batch.import_status = SubmissionBatch.STATUS_COMPLETED
batch.product_name = ingest_result["product_name"]
batch.file_count = len(all_documents)
batch.page_count = ingest_result["page_count"]
batch.chapter_summary = ingest_result["chapter_summary"]
batch.exception_count = len(warnings)
batch.save(
update_fields=[
"product_name",
"file_count",
"page_count",
"chapter_summary",
"exception_count",
"import_status",
"updated_at",
]
)
if batch.conversation_id:
from apps.chat.models import Conversation
conversation = Conversation.objects.filter(conversation_id=batch.conversation_id).first()
if conversation:
conversation.product_name = batch.product_name
if batch.product_name:
conversation.title = batch.product_name
conversation.save(update_fields=["product_name", "title", "updated_at"])
return {
"batch_id": batch.batch_id,
"conversation_id": batch.conversation_id,
"product_name": batch.product_name,
"registration_overview_report": {
"batch_id": batch.batch_id,
"product_name": batch.product_name,
"file_count": batch.file_count,
"total_page_count": batch.page_count,
"chapter_summary": batch.chapter_summary,
"documents": [
{
"document_id": document.id,
"original_name": document.original_name,
"chapter_code": document.chapter_code,
"page_count": document.page_count,
"document_role": document.document_role,
}
for document in all_documents
],
"warnings": warnings,
},
}
def create_export_record(
*,
batch: SubmissionBatch,
conversation_id: str,
product_name: str,
template_name: str,
template_version: str,
export_mode: str,
output_type: str,
file_name: str,
relative_path: str,
download_url: str,
) -> ExportedDocument:
"""
保存导出文件记录,供资料包与处理历史统一回看。
"""
return ExportedDocument.objects.create(
batch=batch,
conversation_id=conversation_id,
product_name=product_name,
template_name=template_name,
template_version=template_version,
export_mode=export_mode,
output_type=output_type,
file_name=file_name,
relative_path=relative_path,
download_url=download_url,
)
def build_batch_rows(batches) -> list[dict]:
"""
为资料包列表补齐最近导出摘要。
"""
batch_ids = [batch.id for batch in batches]
latest_exports = {}
for record in ExportedDocument.objects.filter(batch_id__in=batch_ids).order_by("batch_id", "-created_at"):
latest_exports.setdefault(record.batch_id, record)
rows = []
for batch in batches:
rows.append(
{
"batch": batch,
"latest_export": latest_exports.get(batch.id),
}
)
return rows
def build_document_list_context(*, keyword: str = "") -> dict:
"""
组装资料包列表页所需的筛选结果与展示上下文。
View 只负责读取 query params批次搜索、统计和异常聚合统一放到服务层。
"""
batches = SubmissionBatch.objects.all()
if keyword:
batches = batches.filter(product_name__icontains=keyword) | batches.filter(
batch_id__icontains=keyword
)
batches = list(batches)
documents = list(UploadedDocument.objects.all())
status_counts = {
"pending": sum(
1 for batch in batches if batch.import_status == SubmissionBatch.STATUS_PENDING
),
"completed": sum(
1 for batch in batches if batch.import_status == SubmissionBatch.STATUS_COMPLETED
),
"review_required": sum(
1
for batch in batches
if batch.import_status == SubmissionBatch.STATUS_REVIEW_REQUIRED
),
"total": len(batches),
}
return {
"documents": documents,
"batches": batches,
"batch_rows": build_batch_rows(batches),
"keyword": keyword,
"status_counts": status_counts,
"processing_pipeline": [
{"title": "原始文件接收", "detail": "校验格式、大小和场景归属后保存原件。"},
{"title": "文本与表格抽取", "detail": "按 PDF / DOCX / MD / TXT 使用不同解析策略。"},
{"title": "页数统计与可信度评估", "detail": "对 Word 页数采用估算与可信度标记。"},
{"title": "章节点归类", "detail": "基于文件名、标题和正文线索识别 CH 节点。"},
{"title": "切片与索引入库", "detail": "生成知识切片,供 RAG、规则定位和审计引用使用。"},
],
"exception_items": build_exception_items(batches, documents),
}
def build_exception_items(batches, documents) -> list[dict]:
"""
聚合资料包页需要关注的异常提示。
只返回真实存在的异常来源,避免页面继续展示静态 demo 文案:
- 批次级待复核
- 文档级待人工复核
- 文档级处理失败
"""
items = []
for document in documents:
if document.status == UploadedDocument.STATUS_FAILED:
items.append(
{
"level": "失败",
"title": f"文档处理失败:{document.original_name}",
"detail": document.error_message or "文档处理异常,请重新上传或稍后重试。",
}
)
continue
if document.needs_manual_review:
review_reasons = []
if document.file_type.lower() == "docx" and document.page_count_confidence != "exact":
review_reasons.append("页数为估算值,建议人工确认")
if not document.chapter_code or document.chapter_match_status != "matched":
review_reasons.append("章节点未识别,建议人工确认归类")
items.append(
{
"level": "待确认",
"title": f"文档待人工复核:{document.original_name}",
"detail": "".join(review_reasons) or "资料存在待确认项,建议人工复核。",
}
)
for batch in batches:
if batch.import_status != SubmissionBatch.STATUS_REVIEW_REQUIRED:
continue
items.append(
{
"level": "待确认",
"title": f"资料包待复核:{batch.batch_id}",
"detail": (
f"{batch.product_name or '未识别产品名称'} 当前存在 "
f"{batch.exception_count} 项异常,请进入关联会话或处理历史继续复核。"
),
}
)
return items
def extract_text(document: UploadedDocument) -> str: def extract_text(document: UploadedDocument) -> str:
""" """
根据文档类型选择合适的文本抽取策略。 根据文档类型选择合适的文本抽取策略。
@@ -83,6 +401,332 @@ def _detect_extension(file_name: str) -> str:
return Path(file_name).suffix.lower().lstrip(".") return Path(file_name).suffix.lower().lstrip(".")
def _generate_batch_id() -> str:
return f"SUB-20260604-{SubmissionBatch.objects.count() + 1:03d}"
def _estimate_page_count(text: str) -> int:
stripped = text.strip()
if not stripped:
return 0
line_count = len([line for line in stripped.splitlines() if line.strip()])
return max(1, line_count)
def _resolve_page_count(document: UploadedDocument, text: str) -> tuple[int, str]:
"""
按文件类型返回页数与可信度。
- PDF优先统计真实页数
- DOCX优先读取 Word 页数元数据
- 其他类型:退回估算
"""
extension = document.file_type.lower()
if extension == "pdf":
page_count = _extract_pdf_page_count(Path(document.file.path))
if page_count > 0:
return page_count, "exact"
return _estimate_page_count(text), "estimated"
if extension == "docx":
page_count = _extract_docx_page_count(Path(document.file.path))
if page_count > 0:
return page_count, "exact"
return _estimate_page_count(text), "estimated"
return _estimate_page_count(text), "estimated"
def _expand_uploaded_files(uploaded_files: list) -> list[dict]:
expanded_files = []
warnings = []
for uploaded_file in uploaded_files:
extension = Path(uploaded_file.name).suffix.lower()
if extension == ".zip":
extraction = _extract_zip_entries(uploaded_file)
expanded_files.extend(extraction["files"])
warnings.extend(extraction["warnings"])
continue
if extension == ".7z":
extraction = _extract_7z_entries(uploaded_file)
expanded_files.extend(extraction["files"])
warnings.extend(extraction["warnings"])
continue
if extension == ".rar":
extraction = _extract_rar_entries(uploaded_file)
expanded_files.extend(extraction["files"])
warnings.extend(extraction["warnings"])
continue
expanded_files.append(
{
"relative_path": uploaded_file.name,
"uploaded_file": uploaded_file,
}
)
return {"files": expanded_files, "warnings": warnings}
def _ingest_files_into_batch(
*,
batch: SubmissionBatch,
scenario_id: str,
uploaded_files: list,
keep_existing_product_name: bool = False,
) -> dict:
expanded_result = _expand_uploaded_files(uploaded_files)
expanded_files = expanded_result["files"]
warnings = list(expanded_result["warnings"])
new_documents = []
new_candidates = []
for uploaded_item in expanded_files:
uploaded_file = uploaded_item["uploaded_file"]
relative_path = uploaded_item["relative_path"]
document = create_uploaded_document(
scenario_id,
uploaded_file,
batch=batch,
relative_path=relative_path,
)
text = extract_text(document)
page_count, page_count_confidence = _resolve_page_count(document, text)
document.page_count = page_count
document.page_count_confidence = page_count_confidence
document.document_role = _detect_document_role(document.relative_path)
document.chapter_code = _detect_chapter_code(document.relative_path, text)
document.chapter_match_status = "matched" if document.chapter_code else "unknown"
document.needs_manual_review = (
not bool(document.chapter_code)
or (document.file_type.lower() == "docx" and page_count_confidence != "exact")
)
if document.file_type.lower() == "docx" and page_count_confidence != "exact":
warnings.append(f"DOCX 页数无法精确统计:{document.relative_path}")
document.save(
update_fields=[
"page_count",
"page_count_confidence",
"document_role",
"chapter_code",
"chapter_match_status",
"needs_manual_review",
"updated_at",
]
)
new_documents.append(document)
new_candidates.extend(_extract_product_candidates(document.relative_path, text))
all_documents = list(batch.documents.order_by("id"))
chapter_summary = {}
total_pages = 0
for document in all_documents:
total_pages += document.page_count
chapter_key = document.chapter_code or "UNCLASSIFIED"
chapter_summary[chapter_key] = chapter_summary.get(chapter_key, 0) + 1
product_name = batch.product_name
if keep_existing_product_name and batch.product_name:
conflict_names = {
item["product_name"] for item in new_candidates if item["product_name"] != batch.product_name
}
if conflict_names:
warnings.append(
"新增文件与当前资料包产品名称不一致:"
+ " / ".join([batch.product_name, *sorted(conflict_names)])
)
else:
product_name, product_warnings = _select_product_name(new_candidates)
warnings.extend(product_warnings)
if keep_existing_product_name and not product_name:
product_name = batch.product_name
return {
"documents": all_documents if keep_existing_product_name else new_documents,
"new_documents": new_documents,
"warnings": warnings,
"product_name": product_name,
"page_count": total_pages if keep_existing_product_name else total_pages,
"chapter_summary": [
{"chapter_code": chapter_code, "document_count": count}
for chapter_code, count in sorted(chapter_summary.items())
],
}
def _extract_zip_entries(uploaded_file) -> dict:
archive_bytes = uploaded_file.read()
uploaded_file.seek(0)
entries = []
warnings = []
with ZipFile(BytesIO(archive_bytes)) as archive:
for info in archive.infolist():
if info.is_dir():
continue
relative_path = info.filename.replace("\\", "/")
extension = Path(relative_path).suffix.lower()
if extension not in {".txt", ".md", ".pdf", ".docx"}:
warnings.append(f"跳过不支持的文件:{relative_path}")
continue
file_data = archive.read(info.filename)
extracted_file = SimpleUploadedFile(
Path(relative_path).name,
file_data,
)
entries.append(
{
"relative_path": relative_path,
"uploaded_file": extracted_file,
}
)
return {"files": entries, "warnings": warnings}
def _extract_7z_entries(uploaded_file) -> dict:
try:
import py7zr
except ImportError as exc:
raise RuntimeError("处理 .7z 资料包需要安装 py7zr。") from exc
archive_bytes = uploaded_file.read()
uploaded_file.seek(0)
entries = []
warnings = []
with tempfile.TemporaryDirectory() as temp_dir:
with py7zr.SevenZipFile(BytesIO(archive_bytes), mode="r") as archive:
archive.extractall(path=temp_dir)
base_path = Path(temp_dir)
for file_path in sorted(base_path.rglob("*")):
if not file_path.is_file():
continue
relative_path = file_path.relative_to(base_path).as_posix()
extension = Path(relative_path).suffix.lower()
if extension not in {".txt", ".md", ".pdf", ".docx"}:
warnings.append(f"跳过不支持的文件:{relative_path}")
continue
extracted_file = SimpleUploadedFile(
file_path.name,
file_path.read_bytes(),
)
entries.append(
{
"relative_path": relative_path,
"uploaded_file": extracted_file,
}
)
return {"files": entries, "warnings": warnings}
def _extract_rar_entries(uploaded_file) -> dict:
try:
import rarfile
except ImportError as exc:
raise RuntimeError("处理 .rar 资料包需要安装 rarfile。") from exc
archive_bytes = uploaded_file.read()
uploaded_file.seek(0)
entries = []
warnings = []
with rarfile.RarFile(BytesIO(archive_bytes)) as archive:
for info in archive.infolist():
if info.is_dir():
continue
relative_path = info.filename.replace("\\", "/")
extension = Path(relative_path).suffix.lower()
if extension not in {".txt", ".md", ".pdf", ".docx"}:
warnings.append(f"跳过不支持的文件:{relative_path}")
continue
file_data = archive.read(info.filename)
extracted_file = SimpleUploadedFile(
Path(relative_path).name,
file_data,
)
entries.append(
{
"relative_path": relative_path,
"uploaded_file": extracted_file,
}
)
return {"files": entries, "warnings": warnings}
def _detect_document_role(file_name: str) -> str:
normalized = file_name.lower()
if "申请表" in file_name:
return "application_form"
if "说明书" in file_name:
return "product_manual"
if "产品列表" in file_name:
return "product_list"
if "声明" in file_name:
return "declaration"
if normalized.endswith(".pdf"):
return "pdf_document"
return "general_document"
def _detect_chapter_code(file_name: str, text: str) -> str:
for source in (file_name, text):
match = re.search(r"(CH\d+(?:\.\d+)*)", source, flags=re.IGNORECASE)
if match:
return match.group(1).upper()
if "监管" in file_name or "申请表" in file_name or "说明书" in file_name:
return "CH1"
return ""
def _extract_product_candidates(file_name: str, text: str) -> list[dict]:
source_type = _detect_candidate_source(file_name)
if not source_type:
return []
patterns = [
r"产品名称[:]\s*([^\n\r]+)",
r"名称[:]\s*([^\n\r]+检测试剂盒[^\n\r]*)",
]
for pattern in patterns:
match = re.search(pattern, text)
if match:
return [{"source_type": source_type, "product_name": match.group(1).strip()}]
cleaned = Path(file_name).stem.replace("目标产品", "").replace("说明书", "").strip("-_ ")
if cleaned and "申请表" not in cleaned and "产品列表" not in cleaned:
return [{"source_type": source_type, "product_name": cleaned}]
return []
def _detect_candidate_source(file_name: str) -> str:
if "申请表" in file_name:
return "application_form"
if "说明书" in file_name:
return "product_manual"
if "产品列表" in file_name:
return "product_list"
return ""
def _select_product_name(candidates: list[dict]) -> tuple[str, list[str]]:
if not candidates:
return "", ["未识别到产品名称,建议人工补录。"]
priority = {
"application_form": 1,
"product_manual": 2,
"product_list": 3,
}
sorted_candidates = sorted(
candidates,
key=lambda item: priority.get(item["source_type"], 99),
)
top_candidate = sorted_candidates[0]
warnings = []
conflict_names = {
item["product_name"]
for item in sorted_candidates
if item["product_name"] != top_candidate["product_name"]
}
if conflict_names:
warnings.append(
"产品名称来源冲突:"
+ " / ".join([top_candidate["product_name"], *sorted(conflict_names)])
)
return top_candidate["product_name"], warnings
def _read_text_file(path: Path) -> str: def _read_text_file(path: Path) -> str:
"""优先按 UTF-8 读取;失败时回退到系统默认编码。""" """优先按 UTF-8 读取;失败时回退到系统默认编码。"""
try: try:
@@ -102,6 +746,17 @@ def _extract_pdf_text(path: Path) -> str:
return _read_binary_text_fallback(path) return _read_binary_text_fallback(path)
def _extract_pdf_page_count(path: Path) -> int:
"""优先使用 pypdf 统计 PDF 真实页数。"""
try:
import pypdf
reader = pypdf.PdfReader(str(path))
return len(reader.pages)
except Exception:
return 0
def _extract_docx_text(path: Path) -> str: def _extract_docx_text(path: Path) -> str:
"""提取 Word XML 中的可见文字内容,不追求保留样式。""" """提取 Word XML 中的可见文字内容,不追求保留样式。"""
try: try:
@@ -115,6 +770,26 @@ def _extract_docx_text(path: Path) -> str:
return _read_binary_text_fallback(path) return _read_binary_text_fallback(path)
def _extract_docx_page_count(path: Path) -> int:
"""
从 Word 扩展属性中提取真实页数。
常见 docx 会在 `docProps/app.xml` 中写入 `<Pages>`。
若缺失该元数据,则由上层回退为估算并进入待复核。
"""
try:
with ZipFile(path) as archive:
app_xml = archive.read("docProps/app.xml")
root = ET.fromstring(app_xml)
namespace = {"ep": "http://schemas.openxmlformats.org/officeDocument/2006/extended-properties"}
pages_node = root.find(".//ep:Pages", namespace)
if pages_node is None or not (pages_node.text or "").strip():
return 0
return int((pages_node.text or "").strip())
except (BadZipFile, KeyError, ET.ParseError, ValueError):
return 0
def _read_binary_text_fallback(path: Path) -> str: def _read_binary_text_fallback(path: Path) -> str:
""" """
当结构化抽取失败时,退回到“尽可能保留纯文本”的保底方案。 当结构化抽取失败时,退回到“尽可能保留纯文本”的保底方案。

View File

@@ -6,49 +6,32 @@ from apps.scenarios.services import list_scenarios
from .forms import DocumentUploadForm from .forms import DocumentUploadForm
from .models import UploadedDocument from .models import UploadedDocument
from .services import create_uploaded_document, index_document from .services import (
build_document_list_context,
import_submission_batch,
def document_list(request): index_document,
# 列表页只负责展示文档元数据和可执行操作,不处理入库细节。
documents = UploadedDocument.objects.all()
status_counts = {
"uploaded": documents.filter(status=UploadedDocument.STATUS_UPLOADED).count(),
"indexed": documents.filter(status=UploadedDocument.STATUS_INDEXED).count(),
"failed": documents.filter(status=UploadedDocument.STATUS_FAILED).count(),
"total": documents.count(),
}
processing_pipeline = [
{"title": "原始文件接收", "detail": "校验格式、大小和场景归属后保存原件。"},
{"title": "文本与表格抽取", "detail": "按 PDF / DOCX / MD / TXT 使用不同解析策略。"},
{"title": "页数统计与可信度评估", "detail": "对 Word 页数采用估算与可信度标记。"},
{"title": "章节点归类", "detail": "基于文件名、标题和正文线索识别 CH 节点。"},
{"title": "切片与索引入库", "detail": "生成知识切片,供 RAG、规则定位和审计引用使用。"},
]
exception_items = [
{"level": "待确认", "title": "CH1.2 监管信息目录.docx", "detail": "目录页码与正文页数存在偏差,建议人工复核。"},
{"level": "低可信度", "title": "目标产品说明书.docx", "detail": "Word 页数为估算值,表格抽取质量良好。"},
{"level": "失败", "title": "沟通记录扫描件.pdf", "detail": "疑似扫描件,需补做 OCR 或重新上传清晰版。"},
]
return render(
request,
"documents/document_list.html",
{
"documents": documents,
"status_counts": status_counts,
"processing_pipeline": processing_pipeline,
"exception_items": exception_items,
},
) )
def document_list(request):
# 资料包页展示批次、会话绑定和关键异常,同时保留文档级明细便于演示。
context = build_document_list_context(keyword=(request.GET.get("keyword") or "").strip())
return render(request, "documents/document_list.html", context)
def upload(request): def upload(request):
# 上传成功后仅保存文件和元数据,是否入库由用户显式触发 # 上传成功后直接创建资料包并绑定主会话
if request.method == "POST": if request.method == "POST":
form = DocumentUploadForm(request.POST, request.FILES) form = DocumentUploadForm(request.POST, request.FILES)
if form.is_valid(): if form.is_valid():
create_uploaded_document(form.cleaned_data["scenario_id"], form.cleaned_data["file"]) result = import_submission_batch(
messages.success(request, "文件已上传,可继续执行入库。") form.cleaned_data["scenario_id"],
form.cleaned_data["uploaded_files"],
)
messages.success(
request,
f"资料包已导入,已绑定会话 {result['conversation_id']}",
)
return redirect("documents:list") return redirect("documents:list")
else: else:
form = DocumentUploadForm() form = DocumentUploadForm()
@@ -59,8 +42,9 @@ def upload(request):
"form": form, "form": form,
"scenarios": list_scenarios(), "scenarios": list_scenarios(),
"upload_checks": [ "upload_checks": [
"文件格式支持 PDF、DOCX、MD、TXT", "文件格式支持 PDF、DOCX、MD、TXT、ZIP、7Z 与 RAR 资料包",
"业务资料与法规依据资料需分开归属", "业务资料与法规依据资料需分开归属",
"支持一次上传多份文件并归并到同一个资料包",
"目录类文件会优先参与完整性校验", "目录类文件会优先参与完整性校验",
"上传完成后建议立即进入解析与入库流程", "上传完成后建议立即进入解析与入库流程",
], ],

40
apps/platform_ui/admin.py Normal file
View File

@@ -0,0 +1,40 @@
from django.contrib import admin
from .models import FeishuNotifyConfig, OwnerMapping, WordTemplateMapping
@admin.register(OwnerMapping)
class OwnerMappingAdmin(admin.ModelAdmin):
list_display = (
"owner_role",
"owner_name",
"department",
"chapter_scope",
"risk_scope",
"feishu_user_id",
"notify_enabled",
"is_active",
)
list_filter = ("notify_enabled", "is_active", "department")
search_fields = ("owner_role", "owner_name", "department", "chapter_scope", "risk_scope")
@admin.register(FeishuNotifyConfig)
class FeishuNotifyConfigAdmin(admin.ModelAdmin):
list_display = ("config_name", "notify_reason", "channel", "status", "is_active")
list_filter = ("notify_reason", "status", "is_active")
search_fields = ("config_name", "channel", "message_template")
@admin.register(WordTemplateMapping)
class WordTemplateMappingAdmin(admin.ModelAdmin):
list_display = (
"template_name",
"output_type",
"version",
"placeholder_count",
"status",
"is_active",
)
list_filter = ("output_type", "status", "is_active")
search_fields = ("template_name", "version", "field_mapping_summary")

View File

@@ -0,0 +1,108 @@
# Generated by Django 5.2.14 on 2026-06-03 18:11
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name="FeishuNotifyConfig",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("config_name", models.CharField(max_length=100)),
(
"notify_reason",
models.CharField(
choices=[
("task_completed", "task_completed"),
("task_failed", "task_failed"),
],
db_index=True,
max_length=32,
),
),
("channel", models.CharField(blank=True, max_length=100)),
("message_template", models.CharField(blank=True, max_length=255)),
("status", models.CharField(blank=True, max_length=32)),
("is_active", models.BooleanField(db_index=True, default=True)),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
],
options={
"ordering": ["notify_reason", "id"],
},
),
migrations.CreateModel(
name="OwnerMapping",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("owner_role", models.CharField(db_index=True, max_length=100)),
("owner_name", models.CharField(max_length=100)),
("department", models.CharField(blank=True, max_length=100)),
("chapter_scope", models.CharField(blank=True, max_length=100)),
("risk_scope", models.CharField(blank=True, max_length=255)),
("feishu_user_id", models.CharField(blank=True, max_length=100)),
("feishu_open_id", models.CharField(blank=True, max_length=100)),
("feishu_name", models.CharField(blank=True, max_length=100)),
("notify_enabled", models.BooleanField(default=True)),
("is_active", models.BooleanField(db_index=True, default=True)),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
],
options={
"ordering": ["owner_role", "id"],
},
),
migrations.CreateModel(
name="WordTemplateMapping",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("template_name", models.CharField(max_length=100)),
(
"output_type",
models.CharField(
default="registration_word_export_report", max_length=100
),
),
("version", models.CharField(blank=True, max_length=50)),
("placeholder_count", models.PositiveIntegerField(default=0)),
("status", models.CharField(blank=True, max_length=32)),
("field_mapping_summary", models.CharField(blank=True, max_length=255)),
("is_active", models.BooleanField(db_index=True, default=True)),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
],
options={
"ordering": ["template_name", "id"],
},
),
]

View File

View File

@@ -0,0 +1,78 @@
from django.db import models
class OwnerMapping(models.Model):
"""
责任人映射。
首版以 Django Admin 作为手工维护入口,字段口径与通知载荷保持一致。
"""
owner_role = models.CharField(max_length=100, db_index=True)
owner_name = models.CharField(max_length=100)
department = models.CharField(max_length=100, blank=True)
chapter_scope = models.CharField(max_length=100, blank=True)
risk_scope = models.CharField(max_length=255, blank=True)
feishu_user_id = models.CharField(max_length=100, blank=True)
feishu_open_id = models.CharField(max_length=100, blank=True)
feishu_name = models.CharField(max_length=100, blank=True)
notify_enabled = models.BooleanField(default=True)
is_active = models.BooleanField(default=True, db_index=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ["owner_role", "id"]
def __str__(self) -> str:
return f"{self.owner_role}-{self.owner_name}"
class FeishuNotifyConfig(models.Model):
"""
飞书通知配置。
"""
NOTIFY_REASON_COMPLETED = "task_completed"
NOTIFY_REASON_FAILED = "task_failed"
NOTIFY_REASON_CHOICES = [
(NOTIFY_REASON_COMPLETED, "task_completed"),
(NOTIFY_REASON_FAILED, "task_failed"),
]
config_name = models.CharField(max_length=100)
notify_reason = models.CharField(max_length=32, choices=NOTIFY_REASON_CHOICES, db_index=True)
channel = models.CharField(max_length=100, blank=True)
message_template = models.CharField(max_length=255, blank=True)
status = models.CharField(max_length=32, blank=True)
is_active = models.BooleanField(default=True, db_index=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ["notify_reason", "id"]
def __str__(self) -> str:
return self.config_name
class WordTemplateMapping(models.Model):
"""
Word 模板与字段映射摘要。
"""
template_name = models.CharField(max_length=100)
output_type = models.CharField(max_length=100, default="registration_word_export_report")
version = models.CharField(max_length=50, blank=True)
placeholder_count = models.PositiveIntegerField(default=0)
status = models.CharField(max_length=32, blank=True)
field_mapping_summary = models.CharField(max_length=255, blank=True)
is_active = models.BooleanField(default=True, db_index=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ["template_name", "id"]
def __str__(self) -> str:
return self.template_name

View File

@@ -1,3 +1,8 @@
from django.urls import reverse
from agent_core.governance import load_governance_config
def get_platform_demo_context(): def get_platform_demo_context():
batch = { batch = {
"name": "2026Q2-呼吸道多联检测试剂注册批次", "name": "2026Q2-呼吸道多联检测试剂注册批次",
@@ -28,8 +33,8 @@ def get_platform_demo_context():
quick_links = [ quick_links = [
{"title": "知识库配置", "url_name": "platform_ui:knowledge-base", "desc": "维护法规规则树与切片策略"}, {"title": "知识库配置", "url_name": "platform_ui:knowledge-base", "desc": "维护法规规则树与切片策略"},
{"title": "文件中心", "url_name": "documents:list", "desc": "查看上传、解析、切片与异常状态"}, {"title": "文件中心", "url_name": "documents:list", "desc": "查看上传、解析、切片与异常状态"},
{"title": "审核工作台", "url_name": "chat:index", "url_arg": "document_review", "desc": "发起审核、抽取与一致性核查演示"}, {"title": "审核智能体", "url_name": "chat:index", "desc": "发起审核、抽取与一致性核查演示"},
{"title": "工作台大屏", "url_name": "platform_ui:command-center", "desc": "面向演示的 Agent 流程解释大屏"}, {"title": "审核指挥台", "url_name": "platform_ui:command-center-v2", "desc": "面向演示的 Agent 流程解释大屏"},
] ]
knowledge_sources = [ knowledge_sources = [
{ {
@@ -109,6 +114,52 @@ def get_platform_demo_context():
{"label": "业务资料切片", "value": "342"}, {"label": "业务资料切片", "value": "342"},
{"label": "最近人工校订", "value": "2 次"}, {"label": "最近人工校订", "value": "2 次"},
] ]
governance_sections = [
{"title": "法规规则包", "desc": "维护章节规则、要求项与模板字段映射。"},
{"title": "RAG 文档源", "desc": "统一管理法规资料、业务资料和模板来源。"},
{"title": "RAG 切片", "desc": "查看切片摘要、召回状态和证据命中历史。"},
{"title": "字段 Schema", "desc": "维护强一致字段、回填字段和来源优先级。"},
{"title": "责任人映射", "desc": "按章节和风险类型维护飞书责任人实体。"},
{"title": "飞书通知配置", "desc": "固定支持 task_completed / task_failed 两类通知。"},
]
rag_chunks = [
{
"chunk_id": "chunk-001",
"document_name": "资料要求说明",
"chapter": "CH1",
"summary": "CH1.11.5 沟通记录需保留可追溯留痕。",
"status": "已启用",
},
{
"chunk_id": "chunk-002",
"document_name": "批准证明文件格式要求",
"chapter": "CH1",
"summary": "注册证输出模板需满足固定版式字段映射。",
"status": "待重建",
},
]
field_schemas = [
{
"field_code": "product_name",
"field_name": "产品名称",
"field_type": "string",
"fillable": "",
"strict_consistency": "",
"status": "启用",
},
{
"field_code": "storage_condition",
"field_name": "储存条件",
"field_type": "string",
"fillable": "",
"strict_consistency": "",
"status": "待校订",
},
]
governance_config = load_governance_config()
owner_mappings = governance_config["owner_mappings"]
feishu_configs = governance_config["feishu_configs"]
template_mappings = governance_config["template_mappings"]
mcp_connectors = [ mcp_connectors = [
{"name": "飞书任务通知", "kind": "协同办公", "auth": "App Token", "status": "已连接", "sync": "5 分钟前"}, {"name": "飞书任务通知", "kind": "协同办公", "auth": "App Token", "status": "已连接", "sync": "5 分钟前"},
{"name": "法规规则源导入", "kind": "法规服务", "auth": "文件轮询", "status": "待验证", "sync": "今天 08:50"}, {"name": "法规规则源导入", "kind": "法规服务", "auth": "文件轮询", "status": "待验证", "sync": "今天 08:50"},
@@ -128,6 +179,176 @@ def get_platform_demo_context():
{"time": "09:39", "title": "一致性检查", "detail": "检测到产品名称和样本类型存在跨文档冲突,升级为人工复核。"}, {"time": "09:39", "title": "一致性检查", "detail": "检测到产品名称和样本类型存在跨文档冲突,升级为人工复核。"},
{"time": "09:42", "title": "风险输出", "detail": "生成 3 条风险项、2 条补件建议与 1 条责任人通知任务。"}, {"time": "09:42", "title": "风险输出", "detail": "生成 3 条风险项、2 条补件建议与 1 条责任人通知任务。"},
] ]
command_batch = {
"id": "2025IVD-CL-0520-001",
"status": "进行中",
"workflow": "境内第三类",
"class": "III 类",
"created_at": "2025-05-20",
"applicant": "某某生物科技有限公司",
"reviewer": "张审评员",
"role": "审评专家",
"standard": "《体外诊断试剂注册与备案管理办法》及配套技术指导原则",
"version": "V2.12025-04-01",
}
command_metrics = [
{
"key": "completeness",
"label": "资料包完整性得分",
"value": "68",
"suffix": "/100",
"level": "较差",
"detail": "上次得分 612025-05-19",
},
{
"key": "health",
"label": "资料包健康度",
"value": "62",
"suffix": "项检查项",
"segments": [
{"label": "完整", "value": "28", "hint": "45%"},
{"label": "部分缺失", "value": "18", "hint": "29%"},
{"label": "缺失", "value": "16", "hint": "26%"},
],
},
{
"key": "risk",
"label": "风险分布",
"value": "22",
"suffix": "项风险",
"segments": [
{"label": "高风险", "value": "3", "tone": "danger"},
{"label": "中风险", "value": "7", "tone": "warning"},
{"label": "低风险", "value": "12", "tone": "success"},
],
},
{
"key": "progress",
"label": "审核进度",
"value": "46",
"suffix": "%",
"detail": "已完成 24 / 52 项任务",
},
]
command_flow = [
{"step": "1", "title": "资料准备", "date": "2025-05-20", "state": "done"},
{"step": "2", "title": "形式审查", "date": "2025-05-21", "state": "done"},
{"step": "3", "title": "技术审评", "date": "进行中", "state": "active"},
{"step": "4", "title": "核查检验", "date": "待开始", "state": "todo"},
{"step": "5", "title": "综合评审", "date": "待开始", "state": "todo"},
{"step": "6", "title": "行政审批", "date": "待开始", "state": "todo"},
{"step": "7", "title": "制证发证", "date": "待开始", "state": "todo"},
]
command_tabs = [
{"id": "completeness", "label": "注册完整性核查"},
{"id": "consistency", "label": "字段一致性"},
{"id": "risk", "label": "风险准入结论"},
{"id": "evidence", "label": "证据引用"},
{"id": "feishu", "label": "飞书通知状态"},
]
command_checks = [
{
"chapter": "1.产品基本信息",
"item": "产品名称",
"rule": "办法 第十条",
"status": "完整",
"risk": "低风险",
"problem": "-",
},
{
"chapter": "1.产品基本信息",
"item": "预期用途",
"rule": "办法 第十条",
"status": "完整",
"risk": "低风险",
"problem": "-",
},
{
"chapter": "2.综述资料",
"item": "产品描述",
"rule": "指导原则4.2.1",
"status": "完整",
"risk": "低风险",
"problem": "-",
},
{
"chapter": "2.综述资料",
"item": "作用原理",
"rule": "指导原则4.2.2",
"status": "部分缺失",
"risk": "中风险",
"problem": "缺少关键原理图及验证数据说明",
},
{
"chapter": "3.研究资料",
"item": "分析性能评估",
"rule": "指导原则5.3.1",
"status": "部分缺失",
"risk": "中风险",
"problem": "线性范围验证数据不完整",
},
{
"chapter": "3.研究资料",
"item": "阳性判断值",
"rule": "指导原则5.3.2",
"status": "缺失",
"risk": "高风险",
"problem": "未提供阳性判断值确定依据",
},
{
"chapter": "4.临床评价资料",
"item": "临床试验方案",
"rule": "指导原则6.2.1",
"status": "完整",
"risk": "低风险",
"problem": "-",
},
{
"chapter": "4.临床评价资料",
"item": "临床试验报告",
"rule": "指导原则6.2.2",
"status": "部分缺失",
"risk": "中风险",
"problem": "有效性结果分析不完整",
},
{
"chapter": "4.临床评价资料",
"item": "不良事件汇总分析",
"rule": "指导原则6.2.4",
"status": "缺失",
"risk": "高风险",
"problem": "未提供不良事件汇总分析报告",
},
{
"chapter": "5.生产资料",
"item": "生产工艺验证",
"rule": "指导原则7.2.3",
"status": "完整",
"risk": "低风险",
"problem": "-",
},
]
key_risks = [
{"title": "阳性判断值确定依据缺失", "level": "高风险"},
{"title": "不良事件汇总分析报告缺失", "level": "高风险"},
{"title": "临床试验方案偏离未充分说明", "level": "中风险"},
]
next_actions = [
{"title": "退回企业补充资料", "detail": "需企业补充 3 项高风险资料", "state": "待处理", "tone": "danger"},
{"title": "补充说明或澄清", "detail": "需企业说明 7 项中风险问题", "state": "待处理", "tone": "warning"},
{"title": "进入核查检验环节(可选)", "detail": "风险可控后进入下一环节", "state": "待处理", "tone": "success"},
]
owners = [
{"role": "审评专家", "name": "张审评员", "status": "当前处理人"},
{"role": "审核组长", "name": "李组长", "status": "待确认"},
{"role": "法规专员", "name": "王法规", "status": "协同处理"},
{"role": "临床专家", "name": "陈专家", "status": "协同处理"},
]
operation_logs = [
{"time": "2025-05-21 10:24", "actor": "张审评员", "action": "发起完整性核查"},
{"time": "2025-05-21 10:26", "actor": "Agent Core", "action": "命中 62 项检查规则"},
{"time": "2025-05-21 10:28", "actor": "Agent Core", "action": "生成风险准入结论"},
]
knowledge_filters = [ knowledge_filters = [
{"label": "全部", "active": True}, {"label": "全部", "active": True},
{"label": "法规依据", "active": False}, {"label": "法规依据", "active": False},
@@ -163,10 +384,146 @@ def get_platform_demo_context():
"knowledge_sources": knowledge_sources, "knowledge_sources": knowledge_sources,
"rule_tree": rule_tree, "rule_tree": rule_tree,
"knowledge_stats": knowledge_stats, "knowledge_stats": knowledge_stats,
"governance_sections": governance_sections,
"rag_chunks": rag_chunks,
"field_schemas": field_schemas,
"owner_mappings": owner_mappings,
"feishu_configs": feishu_configs,
"template_mappings": template_mappings,
"knowledge_filters": knowledge_filters, "knowledge_filters": knowledge_filters,
"knowledge_form": knowledge_form, "knowledge_form": knowledge_form,
"rule_form": rule_form, "rule_form": rule_form,
"mcp_connectors": mcp_connectors, "mcp_connectors": mcp_connectors,
"skills": skills, "skills": skills,
"workflow_steps": workflow_steps, "workflow_steps": workflow_steps,
"command_batch": command_batch,
"command_metrics": command_metrics,
"command_flow": command_flow,
"command_tabs": command_tabs,
"command_checks": command_checks,
"key_risks": key_risks,
"next_actions": next_actions,
"owners": owners,
"operation_logs": operation_logs,
}
def build_knowledge_base_context(selected_view: str) -> dict:
"""
组装知识库治理台上下文。
页面层只负责展示,治理对象导航、当前对象说明和 CRUD 入口统一由服务层提供。
"""
context = get_platform_demo_context()
governance_objects = _build_governance_objects()
active_object = next(
(item for item in governance_objects if item["key"] == selected_view),
governance_objects[0],
)
context.update(
{
"governance_objects": governance_objects,
"active_governance_object": active_object,
"governance_action_hub": _build_governance_action_hub(active_object, context),
}
)
return context
def _build_governance_objects() -> list[dict]:
return [
{
"key": "rule_packages",
"title": "法规规则包",
"summary": "按章-条-要求项-模板字段维护规则包版本和启停状态。",
"detail_title": "法规规则包详情",
"detail_copy": "支持新增、编辑、复制新版本、启停和查看章节要求详情。",
"actions": ["新增规则包", "编辑规则包", "复制新版本", "启用 / 停用", "查看章节要求详情"],
"admin_url": reverse("admin:index"),
},
{
"key": "knowledge_sources",
"title": "RAG 文档源",
"summary": "维护法规资料、模板资料和业务资料的入库版本。",
"detail_title": "RAG 文档源详情",
"detail_copy": "支持上传新文档源、替换版本、编辑元数据、停用和重新入库。",
"actions": ["上传新文档源", "替换版本", "编辑元数据", "停用文档源", "重新入库"],
"admin_url": reverse("admin:index"),
},
{
"key": "rag_chunks",
"title": "RAG 切片",
"summary": "查看切片摘要、章节、召回状态和证据命中历史。",
"detail_title": "RAG 切片详情",
"detail_copy": "支持手工切片、摘要编辑、合并拆分、删除和重建向量。",
"actions": ["新增手工切片", "编辑切片摘要", "合并切片", "拆分切片", "重建向量"],
"admin_url": reverse("admin:index"),
},
{
"key": "field_schemas",
"title": "字段 Schema",
"summary": "维护回填字段、强一致字段和来源优先级。",
"detail_title": "字段 Schema 详情",
"detail_copy": "支持新增字段、编辑字段、版本复制和启停管理。",
"actions": ["新增字段", "编辑字段", "启停字段", "复制 schema 版本", "查看来源优先级"],
"admin_url": reverse("admin:index"),
},
{
"key": "template_mappings",
"title": "Word 模板与字段映射",
"summary": "管理输出模板版本、占位符映射和阻断条件影响范围。",
"detail_title": "Word 模板与字段映射详情",
"detail_copy": "支持上传模板、编辑占位符映射、启停版本和模板预览。",
"actions": ["上传模板", "编辑模板元数据", "编辑占位符映射", "启用 / 停用版本", "预览模板"],
"admin_url": reverse("admin:platform_ui_wordtemplatemapping_changelist"),
},
{
"key": "owner_mappings",
"title": "责任人映射",
"summary": "按章节和风险类型维护责任角色、责任人和飞书标识。",
"detail_title": "责任人映射详情",
"detail_copy": "支持新增、编辑、启停、删除以及批量导入责任人映射。",
"actions": ["新增映射", "编辑映射", "启停映射", "删除映射", "批量导入映射"],
"admin_url": reverse("admin:platform_ui_ownermapping_changelist"),
},
{
"key": "feishu_configs",
"title": "飞书通知配置",
"summary": "固定支持 task_completed / task_failed 两类通知并维护消息模板。",
"detail_title": "飞书通知配置详情",
"detail_copy": "支持新增配置、编辑消息模板、启停配置和发送测试消息。",
"actions": ["新增配置", "编辑配置", "切换消息模板", "启用 / 停用", "发送测试消息"],
"admin_url": reverse("admin:platform_ui_feishunotifyconfig_changelist"),
},
]
def _build_governance_action_hub(active_object: dict, context: dict) -> dict:
"""
为治理台提供当前对象聚焦信息与跨入口动作。
目标是把知识库从“静态配置展示页”收口为“治理入口”:
- 明确当前治理对象
- 提供固定通知策略口径
- 提供跳转到审核智能体 / 资料包 / 处理历史的快速入口
"""
owner_count = len(context.get("owner_mappings") or [])
template_count = len(context.get("template_mappings") or [])
feishu_count = len(context.get("feishu_configs") or [])
return {
"title": "治理动作总览",
"current_object": active_object["title"],
"current_summary": active_object["summary"],
"status_items": [
{"label": "责任人映射", "value": f"{owner_count}"},
{"label": "Word 模板版本", "value": f"{template_count}"},
{"label": "通知配置", "value": f"{feishu_count}"},
],
"quick_actions": [
{"label": "进入审核智能体", "url": reverse("chat:index")},
{"label": "查看资料包", "url": reverse("documents:list")},
{"label": "查看处理历史", "url": reverse("audit:list")},
{"label": "进入后台维护", "url": active_object["admin_url"]},
],
"notify_reasons": ["task_completed", "task_failed"],
} }

View File

@@ -10,4 +10,5 @@ urlpatterns = [
path("mcp-center/", views.mcp_center, name="mcp-center"), path("mcp-center/", views.mcp_center, name="mcp-center"),
path("skills/", views.skill_studio, name="skills"), path("skills/", views.skill_studio, name="skills"),
path("command-center/", views.command_center, name="command-center"), path("command-center/", views.command_center, name="command-center"),
path("command-center-v2/", views.command_center_v2, name="command-center-v2"),
] ]

View File

@@ -1,10 +1,10 @@
from django.shortcuts import render from django.shortcuts import redirect, render
from .services import get_platform_demo_context from .services import build_knowledge_base_context, get_platform_demo_context
def knowledge_base(request): def knowledge_base(request):
context = get_platform_demo_context() context = build_knowledge_base_context(request.GET.get("view", ""))
return render(request, "platform_ui/knowledge_base.html", context) return render(request, "platform_ui/knowledge_base.html", context)
@@ -19,5 +19,9 @@ def skill_studio(request):
def command_center(request): def command_center(request):
return redirect("platform_ui:command-center-v2")
def command_center_v2(request):
context = get_platform_demo_context() context = get_platform_demo_context()
return render(request, "platform_ui/command_center.html", context) return render(request, "platform_ui/command_center_v2.html", context)

View File

@@ -108,6 +108,9 @@ MEDIA_ROOT = Path(os.environ.get("UPLOAD_ROOT", BASE_DIR / "data" / "uploads"))
# 配置目录和 Chroma 数据目录都允许外部覆盖,方便复试现场快速切换。 # 配置目录和 Chroma 数据目录都允许外部覆盖,方便复试现场快速切换。
SCENARIO_CONFIG_DIR = Path(os.environ.get("SCENARIO_CONFIG_DIR", BASE_DIR / "configs")) SCENARIO_CONFIG_DIR = Path(os.environ.get("SCENARIO_CONFIG_DIR", BASE_DIR / "configs"))
GOVERNANCE_CONFIG_PATH = Path(
os.environ.get("GOVERNANCE_CONFIG_PATH", BASE_DIR / "configs" / "governance.yaml")
)
CHROMA_PATH = Path(os.environ.get("CHROMA_PATH", BASE_DIR / "data" / "chroma")) CHROMA_PATH = Path(os.environ.get("CHROMA_PATH", BASE_DIR / "data" / "chroma"))
# LLM 与 Embedding 默认遵循“尽量少配置也能跑”的策略: # LLM 与 Embedding 默认遵循“尽量少配置也能跑”的策略:

View File

@@ -1,13 +1,16 @@
from django.conf import settings from django.conf import settings
from django.conf.urls.static import static from django.conf.urls.static import static
from django.contrib import admin from django.contrib import admin
from django.views.generic import RedirectView
from django.urls import include, path from django.urls import include, path
# 总路由只承担模块装配职责,不在这里写业务逻辑。 # 总路由只承担模块装配职责,不在这里写业务逻辑。
urlpatterns = [ urlpatterns = [
path("admin/", admin.site.urls), path("admin/", admin.site.urls),
path("", include("apps.scenarios.urls")), # 首页默认进入审核工作台,旧的平台总览改为非默认入口,便于演示聚焦主链路。
path("", RedirectView.as_view(pattern_name="chat:index", permanent=False)),
path("scenarios/", include("apps.scenarios.urls")),
path("chat/", include("apps.chat.urls")), path("chat/", include("apps.chat.urls")),
path("documents/", include("apps.documents.urls")), path("documents/", include("apps.documents.urls")),
path("audit/", include("apps.audit.urls")), path("audit/", include("apps.audit.urls")),

View File

@@ -1,22 +1,27 @@
id: document_review id: document_review
name: 文档审核助手 name: 注册资料审核助手
description: 检查合同、制度或 SOP 中的风险点和缺失项 description: 汇总体外诊断试剂注册申报资料目录与页数,并检查完整性、一致性和合规风险
applicable_questions: applicable_questions:
- 合同审核 - 资料目录与页数汇总
- 制度审核 - NMPA 注册申报资料完整性检查
- 产品关键信息抽取
- 跨文档一致性核查
- 合规风险预警
agent: agent:
role: 文档审核专家 role: 体外诊断试剂注册资料审核专家
goal: 根据审核规则和知识库内容输出结构化审核意见 goal: 根据 NMPA 注册申报资料要求、法规依据和上传资料输出结构化审核结论
instructions: instructions:
- 优先围绕资料目录、页数、章节点、完整性、字段一致性和风险建议回答
- 法规判断应优先依据本地规则和知识库证据,不得凭空补全缺失资料
- 不确定的问题必须标记为需人工复核 - 不确定的问题必须标记为需人工复核
- 输出必须包含风险等级和修改建议 - 输出必须包含风险等级、涉及文件、法规或规则依据和处理建议
rag: rag:
enabled: true enabled: true
collection: document_review collection: registration_documents
top_k: 5 top_k: 5
tools: tools:
- check_required_fields - check_required_fields
output: output:
type: document_review_report type: registration_risk_report
audit: audit:
enabled: true enabled: true

45
configs/governance.yaml Normal file
View File

@@ -0,0 +1,45 @@
owner_mappings:
- owner_role: 注册资料负责人
owner_name: 张三
department: 注册事务部
chapter_scope: CH1
risk_scope: 字段冲突 / 缺失项
feishu_user_id: ou_demo_1
feishu_open_id: on_demo_1
feishu_name: 张三
notify_enabled:
- owner_role: 注册申报负责人
owner_name: 李四
department: 临床注册组
chapter_scope: CH2-CH6
risk_scope: 完整性风险 / 导出阻断
feishu_user_id: ou_demo_2
feishu_open_id: on_demo_2
feishu_name: 李四
notify_enabled:
feishu_configs:
- config_name: 注册审核完成通知
notify_reason: task_completed
channel: 群机器人
message_template: 审核完成摘要 + @处理人
status: 启用
- config_name: 注册审核异常通知
notify_reason: task_failed
channel: 群机器人
message_template: 异常摘要 + @处理人
status: 启用
template_mappings:
- template_name: 注册证导出模板
output_type: registration_word_export_report
version: V1.0
placeholder_count: 18
status: 启用
field_mapping_summary: 产品名称 / 注册人 / 适用机型 / 储存条件
- template_name: 风险摘要导出模板
output_type: registration_word_export_report
version: V0.9
placeholder_count: 10
status: 待校验
field_mapping_summary: 风险等级 / 批次号 / 责任人 / 证据摘要

25
design-qa.md Normal file
View File

@@ -0,0 +1,25 @@
**Findings**
- No actionable P0/P1/P2 findings remain.
**Open Questions**
- The source visual is an Image Gen concept rather than a pixel-locked Figma file, so exact icon glyphs and logo geometry were implemented as code-native UI marks. The final prototype preserves the selected direction's hierarchy, density, risk states, and operational layout.
**Implementation Checklist**
- Source visual truth path: `C:\Users\bruce\.codex\generated_images\019e8bb8-b097-72b3-9c89-97cfca019c7c\ig_0fe578f839e33933016a1fae76771c81918c954bf5cbfe72d2.png`
- Implementation screenshot path: `D:\Code\DEMO-AGENT\output\playwright\command-center-v2-desktop.png`
- Mobile screenshot path: `D:\Code\DEMO-AGENT\output\playwright\command-center-v2-mobile.png`
- Full-view comparison evidence: `D:\Code\DEMO-AGENT\output\playwright\command-center-v2-comparison.png`
- Viewport: desktop `1440 x 1024`, mobile `390 x 844`
- State: default command-center workbench; desktop screenshot shows the selected direction's primary dashboard state.
- Focused region comparison evidence: not separately required; this is a dense dashboard concept and the full-view comparison makes the critical regions visible enough: sidebar, batch strip, metrics, workflow, audit table, and right risk rail.
- Fonts and typography: Segoe UI / PingFang SC / Microsoft YaHei fallback stack matches the professional enterprise SaaS feel; hierarchy and body sizes are readable, with no negative letter spacing.
- Spacing and layout rhythm: Desktop uses the same left rail, top context strip, four metric blocks, workflow strip, table region, and right-side risk rail as the source. Mobile switches to stacked sections to avoid cramped table-dashboard density.
- Colors and visual tokens: Deep navy sidebar, white work surface, cool gray borders, blue primary actions, red/orange/green semantic risk states match the selected visual direction.
- Image quality and asset fidelity: The source concept did not require product photos or decorative raster assets. The implementation uses lightweight UI marks and CSS primitives appropriate for a Django dashboard prototype.
- Copy and content: Chinese labels are aligned to the requirements: NMPA IVD review, registration batch, completeness check, risk gate, evidence, Feishu notification, responsible owners, and audit trail.
- Patches made since previous QA pass: added mobile wrapping for the header and batch strip, converted mobile actions to a single-column grid, adjusted the health legend, and suppressed horizontal overflow.
- final result: passed
**Follow-up Polish**
- P3: Replace letter-based placeholder nav icons with an icon font or library if the project later adds a frontend asset pipeline.
- P3: Add separate interactive tab bodies for consistency, evidence, and Feishu cards if the prototype needs a longer live demo script.

View File

@@ -1,376 +0,0 @@
# Registration Agent Prototype Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Build a new high-fidelity, demo-ready prototype UI for the registration review agent platform across homepage, knowledge base, document processing, agent workspace, MCP, Skills, and leadership dashboard screens.
**Architecture:** Keep the existing Django monolith and template routing boundaries, but replace the current visual system with a unified prototype shell and add a dedicated platform app for new governance pages. Use shared presentation data in service/view code so the pages tell one coherent business story without coupling design code to the existing agent execution internals.
**Tech Stack:** Django templates, Django views/URLs, Python service helpers, shared inline CSS in base template, existing forms/models where useful.
---
## File Structure
- Modify: `F:\PyCharm\DEMO-AGENT\templates\base.html`
- Modify: `F:\PyCharm\DEMO-AGENT\templates\scenarios\index.html`
- Modify: `F:\PyCharm\DEMO-AGENT\templates\documents\document_list.html`
- Modify: `F:\PyCharm\DEMO-AGENT\templates\documents\upload.html`
- Modify: `F:\PyCharm\DEMO-AGENT\templates\chat\index.html`
- Create: `F:\PyCharm\DEMO-AGENT\apps\platform_ui\__init__.py`
- Create: `F:\PyCharm\DEMO-AGENT\apps\platform_ui\apps.py`
- Create: `F:\PyCharm\DEMO-AGENT\apps\platform_ui\views.py`
- Create: `F:\PyCharm\DEMO-AGENT\apps\platform_ui\urls.py`
- Create: `F:\PyCharm\DEMO-AGENT\apps\platform_ui\services.py`
- Create: `F:\PyCharm\DEMO-AGENT\templates\platform_ui\knowledge_base.html`
- Create: `F:\PyCharm\DEMO-AGENT\templates\platform_ui\mcp_center.html`
- Create: `F:\PyCharm\DEMO-AGENT\templates\platform_ui\skill_studio.html`
- Create: `F:\PyCharm\DEMO-AGENT\templates\platform_ui\command_center.html`
- Modify: `F:\PyCharm\DEMO-AGENT\config\settings.py`
- Modify: `F:\PyCharm\DEMO-AGENT\config\urls.py`
- Test: `F:\PyCharm\DEMO-AGENT\tests\`
### Task 1: Register the new platform prototype app
**Files:**
- Create: `apps/platform_ui/__init__.py`
- Create: `apps/platform_ui/apps.py`
- Create: `apps/platform_ui/views.py`
- Create: `apps/platform_ui/urls.py`
- Modify: `config/settings.py`
- Modify: `config/urls.py`
- [ ] **Step 1: Add the Django app module skeleton**
```python
from django.apps import AppConfig
class PlatformUiConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "apps.platform_ui"
```
- [ ] **Step 2: Register the app in settings**
```python
INSTALLED_APPS = [
# ...
"apps.platform_ui",
]
```
- [ ] **Step 3: Add prototype page routes**
```python
urlpatterns = [
path("platform/", include("apps.platform_ui.urls")),
]
```
- [ ] **Step 4: Define view names for prototype pages**
```python
urlpatterns = [
path("knowledge-base/", views.knowledge_base, name="knowledge-base"),
path("mcp-center/", views.mcp_center, name="mcp-center"),
path("skills/", views.skill_studio, name="skills"),
path("command-center/", views.command_center, name="command-center"),
]
```
- [ ] **Step 5: Run framework validation**
Run: `python manage.py check`
Expected: PASS with no URL or app import errors
### Task 2: Create shared presentation data for the new prototype
**Files:**
- Create: `apps/platform_ui/services.py`
- Modify: `apps/platform_ui/views.py`
- [ ] **Step 1: Define one coherent demo dataset**
```python
def get_platform_demo_context():
return {
"batch": {...},
"knowledge_sources": [...],
"mcp_connectors": [...],
"skills": [...],
"workflow_steps": [...],
}
```
- [ ] **Step 2: Keep each page view thin**
```python
def knowledge_base(request):
context = get_platform_demo_context()
return render(request, "platform_ui/knowledge_base.html", context)
```
- [ ] **Step 3: Run framework validation**
Run: `python manage.py check`
Expected: PASS with importable views and service helpers
### Task 3: Replace the global visual system and navigation shell
**Files:**
- Modify: `templates/base.html`
- [ ] **Step 1: Rewrite the global shell**
```html
<body>
<div class="app-shell">
<aside class="sidebar">...</aside>
<main class="main-shell">
<header class="topbar">...</header>
{% block content %}{% endblock %}
</main>
</div>
</body>
```
- [ ] **Step 2: Define the new design tokens and shared components**
```css
:root {
--bg: #eef3f7;
--surface: #f8fbfd;
--panel: #ffffff;
--ink: #102033;
--accent: #1e5eff;
--signal: #d77a2b;
}
```
- [ ] **Step 3: Add global helpers for panels, metric cards, timelines, tables, pills, and section headers**
```css
.panel { ... }
.metric-card { ... }
.section-heading { ... }
.timeline-step { ... }
.data-table { ... }
```
- [ ] **Step 4: Render shared navigation links to all prototype surfaces**
```html
<a href="{% url 'scenarios:index' %}">任务总览</a>
<a href="{% url 'platform_ui:knowledge-base' %}">知识库配置</a>
<a href="{% url 'documents:list' %}">文件中心</a>
<a href="{% url 'platform_ui:command-center' %}">工作台大屏</a>
```
- [ ] **Step 5: Open a few pages manually**
Run: `python manage.py runserver`
Expected: the shell renders and every nav item resolves
### Task 4: Redesign the homepage as the business-closure entry point
**Files:**
- Modify: `templates/scenarios/index.html`
- Modify: `apps/scenarios/views.py` if extra presentation fields are needed
- [ ] **Step 1: Add a structured homepage context if needed**
```python
return render(
request,
"scenarios/index.html",
{
"scenarios": list_scenarios(),
"scenario_issues": list_scenario_issues(),
"hero_metrics": [...],
"workflow_overview": [...],
},
)
```
- [ ] **Step 2: Replace the page body with hero, metrics, workflow strip, risk board, and quick-entry modules**
```html
<section class="hero-band">...</section>
<section class="metrics-grid">...</section>
<section class="workflow-strip">...</section>
<section class="two-column-board">...</section>
```
- [ ] **Step 3: Verify the homepage**
Run: `python manage.py runserver`
Expected: `/` shows the new dashboard-like homepage
### Task 5: Redesign the document pages around parsing and slicing workflow
**Files:**
- Modify: `templates/documents/document_list.html`
- Modify: `templates/documents/upload.html`
- Optionally modify: `apps/documents/views.py`
- [ ] **Step 1: Add any lightweight display-only summary fields in the view if needed**
```python
return render(
request,
"documents/document_list.html",
{
"documents": documents,
"processing_summary": {...},
"exception_items": [...],
},
)
```
- [ ] **Step 2: Rebuild the list page into upload stats, pipeline board, anomaly box, and structured directory table**
```html
<section class="metrics-grid">...</section>
<section class="tri-column">...</section>
<section class="panel">
<table class="data-table">...</table>
</section>
```
- [ ] **Step 3: Rebuild the upload page into a guided import experience**
```html
<section class="dropzone-panel">...</section>
<section class="checklist-panel">...</section>
```
- [ ] **Step 4: Verify both pages**
Run: `python manage.py runserver`
Expected: `/documents/` and `/documents/upload/` match the new prototype style
### Task 6: Redesign the chat page as a controlled audit workspace
**Files:**
- Modify: `templates/chat/index.html`
- Optionally modify: `apps/chat/views.py`
- [ ] **Step 1: Add presentation-only helper blocks if the existing context is too sparse**
```python
return render(
request,
"chat/index.html",
{
...
"task_modes": [...],
"result_highlights": [...],
},
)
```
- [ ] **Step 2: Replace the template with a three-zone workspace**
```html
<section class="workspace-grid">
<div class="left-rail">...</div>
<div class="conversation-stage">...</div>
<div class="result-rail">...</div>
</section>
```
- [ ] **Step 3: Preserve the real form submission path while upgrading the visual output areas**
```html
<form method="post">...</form>
{% if result %} ... {% endif %}
```
- [ ] **Step 4: Verify one scenario page**
Run: `python manage.py runserver`
Expected: `/chat/<scenario_id>/` still submits and now renders as a workbench
### Task 7: Implement the governance and platform pages
**Files:**
- Create: `templates/platform_ui/knowledge_base.html`
- Create: `templates/platform_ui/mcp_center.html`
- Create: `templates/platform_ui/skill_studio.html`
- Create: `templates/platform_ui/command_center.html`
- Modify: `apps/platform_ui/views.py`
- Modify: `apps/platform_ui/services.py`
- [ ] **Step 1: Build the knowledge base page**
```html
<section class="three-column-board">...</section>
<section class="panel">...</section>
```
- [ ] **Step 2: Build the MCP center page**
```html
<section class="connector-grid">...</section>
<section class="import-flow">...</section>
```
- [ ] **Step 3: Build the Skill studio page**
```html
<section class="editor-layout">...</section>
<section class="version-board">...</section>
```
- [ ] **Step 4: Build the leadership command center page**
```html
<section class="command-stage">...</section>
<section class="risk-heatmap">...</section>
<section class="evidence-matrix">...</section>
```
- [ ] **Step 5: Verify page routing**
Run: `python manage.py check`
Expected: PASS and all `/platform/...` routes resolve
### Task 8: Regression, polish, and documentation sync
**Files:**
- Modify: `README.md` if navigation or page descriptions need updating
- Modify: `AGENTS.md` only if current implementation status needs sync
- [ ] **Step 1: Run core validation**
Run: `python manage.py check`
Expected: PASS
- [ ] **Step 2: Run targeted tests if template/view assumptions changed**
Run: `pytest`
Expected: PASS or clearly identified existing failures unrelated to the prototype
- [ ] **Step 3: Review the new screens for consistent terminology**
Check: page titles, batch name, product name, risk labels, and workflow terms all match the new design spec
- [ ] **Step 4: Update high-level documentation if needed**
```markdown
## 当前原型页面
- 任务总览
- 知识库配置
- 文件中心
- 审核工作台
- MCP 中心
- Skill Studio
- 工作台大屏
```
## Self-Review
- Spec coverage: the plan covers homepage, knowledge base, document center, chat workspace, MCP, Skill studio, and leadership dashboard.
- Placeholder scan: no TODO/TBD markers remain.
- Type consistency: route names, app names, and page concepts are consistent across tasks.

View File

@@ -1,185 +0,0 @@
# 注册审核智能体平台产品原型设计
## 目标
本次原型围绕“试剂盒临床注册文件准备与审核智能体平台”重新设计一套高保真、可点击、适合复试演示的 Web 产品界面。设计不沿用当前前端视觉而是以新版需求分析为准突出资料治理、法规知识底座、Agent 审核闭环、平台治理能力和可解释工作台。
## 设计范围
本次原型聚焦一条完整演示主线:
1. 首页进入批次级任务总览。
2. 查看法规知识库与结构化规则配置。
3. 上传并解析申报资料,查看切片、页数、章节点归类和异常状态。
4. 进入 AI Agent 审核工作台执行完整性检查、字段抽取和一致性核查。
5. 查看外部 MCP 能力接入情况。
6. 查看和编辑 Skill 编排能力。
7. 进入领导演示型工作台大屏查看 Agent 工作流程、解释依据和风险态势。
## 设计原则
### 1. 业务闭环优先
页面组织必须服务“资料准备到审核闭环”,而不是把系统包装成泛化 Agent 工具箱。
### 2. 高层能看懂,操作员也能讲清
首页与大屏承担讲故事职责知识库、文件中心、Agent 工作台承担业务可信度职责MCP 与 Skill 页面承担平台能力说明职责。
### 3. 可解释性强于炫技
任何页面都应尽量展示来源、阶段、规则命中、风险等级、人工复核状态,而不是堆叠模型术语。
### 4. 信息密度高但不压抑
整体风格走“专业监管科技工作台”,采用明亮底色、深色信息层、铜橙风险强调、清晰分栏和大面积结构化留白。
## 视觉方向
### 色彩
- 主背景采用浅米白与冷灰蓝混合渐变,强调专业与稳定。
- 主强调色采用深青蓝,承担导航、激活态、关键信息。
- 风险强调采用铜橙与深红,用于高风险、缺失、待处理。
- 成功状态采用低饱和绿色,用于已完成、已入库、规则已生效。
### 排版
- 页级标题使用强对比的中文大标题,突出“阶段感”。
- 模块级标题使用中等字号与清晰副标题。
- 指标数字使用紧凑等宽风格,强调可视化汇报感。
### 组件风格
- 顶部为平台导航与当前批次状态条。
- 页面内部以大面板、信息条、时间线、矩阵卡片、指标舱和结构树为主。
- 尽量避免传统后台里大量相同卡片堆叠,强化带状布局、流程图感和分析板感。
## 页面设计
### 1. 首页 / 任务总览
**目标:**
让评委在 10 秒内理解系统价值和当前批次进展。
**核心内容:**
- 批次概览 Hero当前申报批次、产品名、流程阶段、批次状态。
- 四个关键指标:资料齐套率、法规命中率、字段抽取完成度、高风险项数量。
- 演示主流程带:资料进入、规则配置、解析入库、审核执行、结果输出。
- 今日待办与高风险问题板。
- 快速入口:知识库、文件中心、审核工作台、大屏。
### 2. 知识库搭建与配置页
**目标:**
把“双层知识底座”讲透。
**核心内容:**
- 左侧法规规则树:按章、条、要求项、模板字段展示。
- 中部知识源列表:法规依据、业务资料、模板库、公告附件包。
- 右侧切片与生效配置:切片策略、召回阈值、适用流程、最近更新时间。
- 底部人工校订记录与知识更新入口。
### 3. 文件上传、切片与解析中心
**目标:**
展示 Documents 模块是“资料治理中心”。
**核心内容:**
- 顶部上传拖拽区与批次选择。
- 左侧批量导入队列:文件名、类型、页数、识别状态。
- 中部处理流水:保存原件、提取文本、识别表格、页数统计、章节点归类、切片入库。
- 右侧异常箱:疑似扫描件、归类待确认、页数低可信度、切片失败。
- 下方目录总览表:章节点、资料名称、状态、模板命中情况。
### 4. AI Agent 审核工作台
**目标:**
把自由问答升级为受控审核任务工作台。
**核心内容:**
- 顶部任务切换:目录汇总、完整性检查、字段抽取、一致性核查、综合风险报告。
- 左侧自然语言输入与资料范围选择。
- 中间 Agent 对话流与执行阶段条。
- 右侧结构化结果舱:结论摘要、字段表、缺失项、冲突项、风险建议。
- 下方证据区:法规条款引用、文档片段、来源页码、人工复核提示。
### 5. 外部 MCP 导入页
**目标:**
说明平台可扩展但不喧宾夺主。
**核心内容:**
- MCP 连接卡:法规源、飞书通知、模板服务、文档转换、企业数据源。
- 接入状态、鉴权方式、最近同步时间。
- 输入输出能力摘要。
- 一个简单的导入向导区块。
### 6. Skill 编辑与使用页
**目标:**
展示 Agent 可配置、可维护、可复用。
**核心内容:**
- Skill 列表完整性检查、字段抽取、一致性核查、Word 回填、飞书通知。
- 中部编辑区:角色说明、工具绑定、输入输出约束、启停配置。
- 右侧运行预览:最近测试结果、命中工具、失败原因。
- 底部版本历史和发布状态。
### 7. Agent 工作台大屏
**目标:**
作为演示高潮页,向领导/评委解释“这一轮审核发生了什么”。
**核心内容:**
- 顶部大标题与批次状态。
- 左侧流程总览:资料进入、规则装载、字段池建立、一致性比对、风险汇总。
- 中央主舞台Agent 工作流时间轴,展示每一步的输入、动作、输出和解释。
- 右侧风险热力与关键告警。
- 底部证据矩阵:法规依据、命中文档、责任人、待补动作。
## 跳转与演示动线
推荐演示顺序:
1. 首页说明平台定位。
2. 进入知识库页说明法规与规则如何维护。
3. 进入文件中心说明资料如何入库、切片、归类。
4. 进入审核工作台发起一次完整性或一致性检查。
5. 进入 Skill 页解释为何 Agent 行为可控。
6. 进入 MCP 页说明可接飞书与外部能力。
7. 最后切到大屏汇总整轮审核过程。
## 数据策略
本轮原型以高保真演示为优先,允许使用页面级演示数据,但必须满足:
- 术语与字段名称来自新版需求分析。
- 状态设计贴近真实业务流程。
- 页面间批次名称、产品名称、风险项和规则口径保持一致。
## 实现策略
基于当前 Django 模板工程直接重构前端:
1. 保留现有 Django 应用与路由边界。
2. 新增平台页应用承接知识库、MCP、Skill、大屏页面。
3. 重做 `base.html` 及所有主要模板的布局和样式。
4. 使用共享演示数据构造页面内容,保证视觉统一和讲解一致。
## 测试与验证
原型完成后至少验证:
1. Django 路由可访问。
2. 首页、知识库、文件中心、审核工作台、MCP、Skill、大屏页面均可打开。
3. 页面导航互通。
4. `python manage.py check` 通过。
5. 如无模板语法问题,关键页面可在本地服务下正常渲染。

View File

@@ -0,0 +1,151 @@
# 资料包导入页原型设计
## 1. 页面目标
把注册申报资料的导入、解包、扫描、目录汇总和章节点识别结果集中展示出来,让用户第一眼就明白本平台的输入对象是“资料包”,不是单篇文档。
本页还需要明确表达:资料包不是孤立文件集合,而是会与一个审核会话绑定,对话标题默认采用解析后的产品名称。
## 2. 适用角色
- 注册资料专员
- 文档整理人员
- 演示讲解人
## 3. 页面布局分区
页面建议采用“三段式”:
1. 顶部导入条
2. 中部处理看板
3. 底部目录汇总区
建议分区:
- 顶部:批次信息、上传入口、导入方式切换
- 左侧:文件 / 压缩包导入队列
- 中部:处理流水线
- 右侧:异常与待复核箱
- 底部:目录树与目录汇总表
## 4. 核心卡片 / 表格 / 状态区
### 4.1 导入入口卡
展示:
- 批量文件上传
- 文件夹导入
- 压缩包导入
- 支持格式标签:`pdf / docx / doc / zip / rar / 7z`
### 4.2 处理流水线
按步骤展示:
1. 创建批次
2. 文件校验
3. 解包
4. 文件树扫描
5. 页数统计
6. 章节点识别
7. 目录汇总
每一步显示:
- 当前状态
- 处理数量
- 成功 / 失败数
### 4.3 异常箱
展示以下异常类型:
- 页数待复核
- 扩展名与 MIME 不一致
- 扫描件待 OCR
- 章节点识别失败
- 解包失败
### 4.4 目录汇总表
表格列建议:
- 产品名称
- 关联会话
- 原始相对路径
- 文件名
- 文件类型
- 页数
- 页数可信度
- 章节点
- 资料名称
- 处理状态
- 是否命中法规目录
## 5. 关键字段定义
页面主要消费 `registration_overview_report`
关键字段:
- `batch_id`
- `product_name`
- `conversation_id`
- `file_count`
- `supported_file_count`
- `failed_file_count`
- `total_page_count`
- `page_count_status`
- `chapter_summary`
- `documents[]`
目录条目关键字段:
- `original_filename`
- `relative_path`
- `file_type`
- `chapter_code`
- `chapter_name`
- `page_count`
- `page_count_confidence`
- `processing_status`
- `needs_manual_review`
## 6. 关键交互
- 点击“上传压缩包”后,展示模拟导入进度。
- 点击目录树节点,可在右侧高亮对应文件。
- 点击异常项,可筛选下方表格。
- 点击单个文件行,可打开“文档详情抽屉”,展示页数统计方式和章节点识别结果。
- 点击资料包标题或“查看对话”,跳转到关联会话。
- 页面顶部搜索框支持按产品名称或批次号搜索资料包。
- 点击“在对话中查看完整性节点”,切回 `审核智能体` 中的对应节点结果。
## 7. 与上下游页面的数据关系
上游:无,属于演示主线起点。
下游:
- 法规完整性检查页直接消费目录汇总结果。
- 字段抽取页复用文档主数据、文本状态和章节点结果。
- 治理台中的 RAG 文档源管理可从该页二级入口进入。
## 8. 演示话术重点
- 强调本平台处理的是整套注册资料,不是单文档聊天。
- 强调资料包会与会话绑定,用户后续是围绕该产品资料持续对话和追溯。
- 强调压缩包、目录层级、页数和章节点是后续审核的事实基础。
- 强调异常箱的价值在于把“资料问题”前置,而不是等到审核后才发现。
## 9. 与知识库 / 治理台的关联入口
本页应提供以下治理入口:
- `查看 RAG 入库策略`
- `查看支持文件类型配置`
- `查看章节点识别规则`
- `重跑切片任务`
这些入口统一打开治理台抽屉,不在本页直接展开 CRUD。

View File

@@ -0,0 +1,162 @@
# 审核智能体页原型设计
## 1. 页面目标
作为整套原型的主入口,以 Agent 对话为核心承接资料上传、任务模板触发、节点式审核推进和结构化结果讲解,让用户在 10 秒内理解“这是一个审核智能体,而不是传统报表后台”。
## 2. 适用角色
- 注册申报负责人
- 项目经理
- 复试演示讲解人
## 3. 页面布局分区
页面建议采用“顶部对话上下文 + 三栏工作区”。
分区如下:
- 顶部:产品定位、提示词模板、当前会话摘要
- 左栏:对话历史
- 中栏:对话区 + 节点导航
- 右栏上部:上传栏
- 右栏下部:任务能力卡 / 结构化结果卡
## 4. 核心卡片 / 表格 / 状态区
### 4.1 顶部对话上下文区
展示:
- 批次编号
- 产品名称
- 当前流程类型
- 当前审核阶段
- 当前最高风险等级
- 是否允许正式导出
- 推荐提问模板
### 4.2 左栏对话历史
展示:
- 历史会话标题
- 会话对应产品名称
- 风险状态
- 最近更新时间
- 资料包绑定标记
交互:
- 点击某条会话,切换到对应产品资料的审核上下文。
- 会话标题默认采用解析后的 `product_name`
### 4.3 中栏对话区与节点导航
展示:
- 用户问题
- Agent 执行计划
- 节点结果消息
- 结构化摘要片段
- RAG 命中依据提示
节点条固定包含:
1. 资料包导入
2. 目录汇总
3. 法规完整性检查
4. 字段抽取
5. 一致性核查
6. 风险预警
7. Word 回填导出
8. 飞书通知
交互:
- 点击节点,定位到该阶段的消息和结构化结果。
- 当前执行节点状态实时变化为 `处理中 / 已完成 / 待复核 / 已阻断`
### 4.4 右栏上传区
展示:
- 当前资料包上传入口
- 最近上传文件
- 文件解析状态
- 支持格式说明
### 4.5 右栏任务能力卡
根据当前选中的节点或任务模板,展示不同信息:
- 目录汇总时展示文件数、页数、章节点覆盖
- 完整性检查时展示缺失项、错放项、法规依据摘要
- 字段抽取时展示字段数、低置信度、待复核状态
- 一致性核查时展示冲突字段、混档风险
- 风险预警时展示总风险等级、是否允许导出、整改建议
- Word 导出时展示回填字段、拦截项和导出状态
- 飞书通知时展示 `@` 处理人、通知原因和 Web 详情链接
## 5. 关键字段定义
建议聚合使用以下对象:
- `registration_overview_report`
- `registration_completeness_report`
- `registration_field_extraction_report`
- `registration_consistency_report`
- `registration_risk_report`
- `registration_word_export_report`
页面摘要字段:
- `conversation_id`
- `product_name`
- `batch_id`
- `task_name`
- `task_status`
- `last_run_at`
- `summary_label`
- `summary_value`
- `entry_target`
- `notify_reason`
## 6. 关键交互
- 点击历史会话切换产品审核上下文。
- 点击预设提问模板,自动填充对话输入框。
- 点击节点条跳转到对应执行阶段。
- 上传附件后,右栏任务卡片按 `文档解析中 -> 解析完成 -> 数据处理中 -> 处理完成` 实时更新。
- 点击能力卡查看来源证据、结构化结果或治理入口。
- 点击“打开治理台”查看规则版本、字段 Schema、模板版本和责任人映射。
## 7. 与上下游页面的数据关系
上游:
- 资料包页提供当前 `batch_id``product_name` 和会话绑定关系。
- 知识库页提供法规、模板、字段和飞书配置能力。
下游:
- 本页串联所有审核节点结果,是主执行页面。
- 处理历史页回看本页产生的节点消息、结构化结论和通知记录。
## 8. 演示话术重点
- 先讲“对话驱动的审核闭环”,再讲某个点上的智能能力。
- 突出这不是传统分页后台,而是带任务节点和结构化结果的审核智能体。
- 强调每个节点都有状态、有输入输出、有结构化结果,并且能追溯到知识库依据和资料包。
## 9. 与知识库 / 治理台的关联入口
本页右侧或右上角提供:
- `规则版本总览`
- `模板版本总览`
- `责任人映射总览`
- `飞书配置总览`
适合作为讲解治理能力的总入口。

View File

@@ -0,0 +1,125 @@
# 法规完整性检查页原型设计
## 1. 页面目标
对照 NMPA 法规要求,展示当前资料包的齐套性、错放情况、法规依据、风险等级和处理建议,让用户知道“缺了什么、为什么缺、依据是什么”。
在最新版原型中,本页更准确地说是 `审核智能体` 中“法规完整性检查”节点的展开视图,而不是独立执行入口。
## 2. 适用角色
- 注册申报负责人
- 法规专员
- 质控复核人员
## 3. 页面布局分区
推荐采用“三栏 + 底部证据表”布局:
- 左栏:法规目录树与章节范围
- 中栏:缺失项 / 错放项主列表
- 右栏:法规依据和风险摘要
- 底部:证据与建议表
## 4. 核心卡片 / 表格 / 状态区
### 4.1 法规目录树
展示:
- `CH1 ~ CH6`
- 已匹配数量
- 缺失数量
- 待复核数量
### 4.2 问题列表
分标签展示:
- 缺失项
- 错放项
- 疑似提供
- 待人工复核
每条记录展示:
- 章节
- 资料名称
- 当前状态
- 风险等级
- 命中文档或未命中说明
### 4.3 法规依据舱
展示:
- 规则包名称
- 规则版本
- 法规原文来源
- 证据片段
- 适用流程
### 4.4 处理建议表
列建议:
- 问题类型
- 问题说明
- 法规依据
- 风险等级
- 建议动作
- 责任角色
## 5. 关键字段定义
页面主要消费 `registration_completeness_report`
关键字段:
- `required_item_count`
- `provided_item_count`
- `missing_item_count`
- `suspected_item_count`
- `misplaced_item_count`
- `manual_review_item_count`
- `highest_risk_level`
- `pass_status`
- `missing_items[]`
- `misplaced_items[]`
- `manual_review_items[]`
- `evidence_refs[]`
- `suggestions[]`
## 6. 关键交互
- 点击目录树章节,筛选中部问题列表。
- 点击缺失项,右侧法规依据自动切换。
- 点击“查看证据片段”,展开 RAG 命中内容。
- 点击“在对话中查看字段抽取节点”,回到 `审核智能体` 继续主线。
## 7. 与上下游页面的数据关系
上游:消费 `registration_overview_report`
下游:
- 风险预警页复用完整性结论。
- 飞书通知页可引用完整性问题摘要。
- 治理台中的法规规则包和 RAG 文档源由本页进入最自然。
## 8. 演示话术重点
- 强调完整性判断由结构化规则决定,不由大模型自由发挥。
- 强调 RAG 的职责是给证据,不是当裁判。
- 强调风险等级和责任角色已经提前结构化,便于协同。
## 9. 与知识库 / 治理台的关联入口
本页应提供:
- `查看规则包`
- `查看法规原文切片`
- `新增法规来源`
- `编辑章节要求`
所有操作进入治理台对应 CRUD 视图。

View File

@@ -0,0 +1,126 @@
# 字段抽取与字段池页原型设计
## 1. 页面目标
展示从说明书、申请表、产品列表等资料中抽取出的结构化字段,并把来源、置信度、标准值、待复核状态和是否可回填统一展示出来。
在最新版原型中,本页对应 `审核智能体` 中“字段抽取”节点的细化视图。
## 2. 适用角色
- 注册资料专员
- 数据校对人员
- 模板回填使用人
## 3. 页面布局分区
建议采用“顶部摘要 + 中部字段池表格 + 右侧来源证据抽屉”。
分区如下:
- 顶部:字段抽取统计
- 中部:字段池主表
- 下方:待复核字段区
- 右侧:字段来源详情抽屉
## 4. 核心卡片 / 表格 / 状态区
### 4.1 抽取摘要卡
展示:
- 目标字段数量
- 已抽取数量
- 待复核字段数量
- 冲突候选数量
- 可回填字段数量
### 4.2 字段池主表
列建议:
- 字段编码
- 中文字段名
- 标准值
- 原文值
- 来源文档
- 来源位置
- 抽取方式
- 置信度
- 冲突状态
- 待复核状态
- 是否可回填
### 4.3 待复核区
重点突出:
- 低置信度字段
- 长文本归纳字段
- 来源不唯一字段
## 5. 关键字段定义
页面主要消费 `registration_field_extraction_report`
关键字段:
- `target_field_count`
- `extracted_field_count`
- `manual_review_field_count`
- `conflict_candidate_count`
- `field_pool_status`
- `field_pool_items[]`
- `manual_review_fields[]`
- `evidence_refs[]`
字段池条目关键字段:
- `field_key`
- `field_label`
- `standard_value`
- `raw_value`
- `source_document_name`
- `source_location`
- `extract_method`
- `confidence`
- `conflict_status`
- `manual_review_required`
- `fillable`
## 6. 关键交互
- 支持按“全部 / 可回填 / 待复核 / 高置信度”切换。
- 点击字段行打开来源详情。
- 点击“查看原文片段”展开证据区。
- 点击“标记推荐值”模拟人工确认。
- 点击“在对话中查看一致性核查节点”,回到 `审核智能体` 继续主线。
## 7. 与上下游页面的数据关系
上游:
- 来自资料导入页的文档主数据
- 来自完整性检查页的前置校验状态
下游:
- 一致性核查页直接使用字段池
- Word 回填导出页使用可回填字段集
## 8. 演示话术重点
- 强调统一字段池是后续一致性核查和回填导出的中间事实层。
- 强调固定字段优先规则抽取,长文本才交给 LLM 辅助归纳。
- 强调每个字段都有来源,不是“模型猜你要什么”。
## 9. 与知识库 / 治理台的关联入口
本页应提供:
- `维护字段 Schema`
- `维护字段来源优先级`
- `查看字段映射规则`
- `重跑抽取策略`
这些入口接入治理台的字段 Schema 和模板映射 CRUD。

View File

@@ -0,0 +1,115 @@
# 一致性核查页原型设计
## 1. 页面目标
基于统一字段池,对同一审核范围内不同文档的关键字段做完全一致比对,清楚展示冲突字段、来源对比和混档风险。
在最新版原型中,本页对应 `审核智能体` 中“一致性核查”节点的细化视图。
## 2. 适用角色
- 注册资料负责人
- 复核人员
- 项目经理
## 3. 页面布局分区
建议采用“范围确认条 + 冲突主表 + 对比视图 + 风险侧栏”。
分区如下:
- 顶部:审核范围确认条
- 中部:冲突字段表
- 下部:来源对比面板
- 右侧:混档风险与建议
## 4. 核心卡片 / 表格 / 状态区
### 4.1 范围确认条
展示:
- 当前批次
- 已纳入文档数量
- 排除法规资料说明
- 是否存在疑似跨产品文档
### 4.2 冲突字段主表
列建议:
- 字段名
- 比对结果
- 冲突值数量
- 来源文档数
- 最高风险等级
- 是否疑似混档
### 4.3 来源对比面板
按选中字段展示:
- 文档 A 值
- 文档 B 值
- 来源章节
- 来源页码
- 标准化结果
### 4.4 风险侧栏
展示:
- 冲突字段总数
- 混档告警数量
- 待复核字段数
- 建议处理动作
## 5. 关键字段定义
页面主要消费 `registration_consistency_report`
关键字段:
- `checked_field_count`
- `consistent_field_count`
- `conflict_field_count`
- `manual_review_field_count`
- `mixed_package_warning_count`
- `highest_risk_level`
- `pass_status`
- `conflict_fields[]`
- `manual_review_fields[]`
- `mixed_package_warnings[]`
- `suggestions[]`
## 6. 关键交互
- 点击字段行,在下方切换来源对比视图。
- 点击“仅看冲突字段”过滤表格。
- 点击混档告警可跳到相关文档范围说明。
- 点击“在对话中查看风险预警节点”,回到 `审核智能体` 继续主线。
## 7. 与上下游页面的数据关系
上游:消费字段池。
下游:
- 风险预警页复用冲突结论
- Word 回填导出页依据冲突状态做拦截
## 8. 演示话术重点
- 强调一致性核查不重新抽取字段,只对字段事实做比对。
- 强调系统默认严格一致规则,避免模糊通过。
- 强调混档风险是当前业务场景的关键价值点。
## 9. 与知识库 / 治理台的关联入口
本页应提供:
- `维护强一致字段规则`
- `查看审核范围配置`
- `查看混档识别规则`
这些入口进入治理台的规则包和字段规则维护视图。

View File

@@ -0,0 +1,112 @@
# 风险预警页原型设计
## 1. 页面目标
把完整性、字段抽取和一致性核查的结果统一归并成综合风险清单,给出总风险等级、是否通过、整改建议和责任角色。
在最新版原型中,本页对应 `审核智能体` 中“风险预警”节点的细化视图。
## 2. 适用角色
- 注册申报负责人
- 项目经理
- 业务主管
## 3. 页面布局分区
建议采用“总风险头图 + 风险矩阵 + 整改建议 + 责任分发区”。
分区如下:
- 顶部:总风险等级与通过结论
- 左侧:风险分布矩阵
- 中部:风险清单
- 右侧:责任角色与建议动作
## 4. 核心卡片 / 表格 / 状态区
### 4.1 总风险头图
展示:
- 当前批次
- 最高风险等级
- 是否通过
- 高 / 中 / 低风险数量
- 待人工复核数量
### 4.2 风险清单表
列建议:
- 风险类型
- 风险等级
- 问题描述
- 来源报告
- 关联文档
- 整改建议
- 责任角色
### 4.3 责任分发区
展示:
- 注册资料负责人
- 注册申报负责人
- 临床注册负责人
并显示每个角色对应待处理风险数。
## 5. 关键字段定义
页面主要消费 `registration_risk_report`
关键字段:
- `risk_item_count`
- `high_risk_count`
- `medium_risk_count`
- `low_risk_count`
- `manual_review_count`
- `highest_risk_level`
- `pass_status`
- `risk_items[]`
- `manual_review_items[]`
- `suggestions[]`
- `owner_notifications[]`
## 6. 关键交互
- 点击风险类型过滤清单。
- 点击责任角色查看该角色负责的风险。
- 点击“生成通知摘要”跳转飞书通知视图。
- 点击“查看导出影响”跳转 Word 回填导出页。
## 7. 与上下游页面的数据关系
上游:
- 完整性报告
- 字段抽取报告
- 一致性报告
下游:
- Word 回填导出页引用是否通过和阻断项
- 飞书通知视图引用责任人和风险摘要
## 8. 演示话术重点
- 强调整个系统不是只找问题,而是把问题归并成“可执行风险”。
- 强调是否通过和责任角色已经直接结构化,可以进入实际协同。
- 强调高风险会影响后续导出和通知动作。
## 9. 与知识库 / 治理台的关联入口
本页应提供:
- `维护风险规则`
- `维护责任角色映射`
- `查看准入判定规则`
这些入口进入治理台的风险规则和责任人映射 CRUD。

View File

@@ -0,0 +1,120 @@
# Word回填导出页原型设计
## 1. 页面目标
展示字段如何回填到注册申报表格或对照清单中,并说明哪些字段已回填、哪些字段被风险或冲突拦截、当前导出状态如何以及用户从哪里下载文件。
在最新版原型中,本页对应 `审核智能体` 中“Word 回填导出”节点的细化视图。
## 2. 适用角色
- 注册资料专员
- 表格整理人员
- 审核结果使用人
## 3. 页面布局分区
建议采用“模板与导出状态头部 + 回填字段表 + 拦截项区 + 下载区”。
分区如下:
- 顶部:模板选择与导出结论
- 中部:回填字段表
- 右侧:拦截项和版式校验
- 底部:导出记录与下载入口
## 4. 核心卡片 / 表格 / 状态区
### 4.1 导出头部
展示:
- 模板名称
- 模板版本
- 当前导出状态
- 是否允许正式版
- 是否允许草稿版
### 4.2 回填字段表
列建议:
- 占位符
- 字段名
- 字段值
- 来源
- 回填状态
- 是否必填
### 4.3 拦截项区
展示:
- 冲突字段拦截
- 高风险拦截
- 待复核字段拦截
- 缺失必填字段拦截
### 4.4 导出记录区
展示:
- 输出文件名
- 输出版本
- 导出时间
- 版式校验结果
- 下载入口
## 5. 关键字段定义
页面主要消费 `registration_word_export_report`
关键字段:
- `template_id`
- `export_status`
- `fillable_field_count`
- `filled_field_count`
- `blocked_field_count`
- `manual_review_field_count`
- `layout_check_status`
- `filled_fields[]`
- `blocked_fields[]`
- `output_file`
## 6. 关键交互
- 点击模板下拉模拟切换模板。
- 点击“生成草稿”切换导出状态。
- 点击“尝试生成正式版”时展示阻断提示。
- 点击某个拦截项,可查看其来源风险。
- 点击下载按钮,展示 mock 下载反馈。
## 7. 与上下游页面的数据关系
上游:
- 字段池
- 一致性核查报告
- 风险预警报告
下游:
- 飞书通知页可引用“已生成草稿”或“正式导出被阻断”的状态
## 8. 演示话术重点
- 强调平台不仅能抽取字段,还能进入真正的交付动作。
- 强调高风险不会被忽略,系统会阻止直接生成正式报送文件。
- 强调模板、字段映射、拦截条件都可治理,不写死在 Prompt 里。
## 9. 与知识库 / 治理台的关联入口
本页应提供:
- `维护 Word 模板`
- `维护字段映射`
- `查看导出版式校验规则`
- `查看导出记录`
这些入口进入治理台的模板与映射 CRUD。

View File

@@ -0,0 +1,109 @@
# 飞书通知视图原型设计
## 1. 页面目标
把风险预警和导出状态转成一张可发送的飞书消息卡片,展示责任人 `@`、消息摘要、Web 详情链接和发送回执,形成审核智能体之外的协同闭环。
当前 Demo 固定口径为:任务执行完成后直接通知,或在执行异常后直接通知,并 `@` 对应处理人。
## 2. 适用角色
- 注册申报负责人
- 协同群聊成员
- 演示讲解人
## 3. 页面布局分区
建议采用“双栏布局”:
- 左侧:飞书消息卡片预览
- 右侧:通知配置与发送回执
底部补一条 Web 详情页入口说明。
## 4. 核心卡片 / 表格 / 状态区
### 4.1 飞书消息卡片预览
展示:
- 批次编号
- 风险摘要
- 是否通过
- 责任人 `@`
- 关键风险条目
- Web 详情按钮
### 4.2 通知配置区
展示:
- 触发来源
- 通知原因
- 飞书群聊 ID
- 消息类型
- 是否包含责任人通知
- 是否附带 Web 链接
### 4.3 发送回执区
展示:
- 发送状态
- 消息 ID
- 发送时间
- 失败原因
## 5. 关键字段定义
页面主要消费 `feishu_notification_report`
关键字段:
- `send_status`
- `notify_reason`
- `message_type`
- `mentioned_users[]`
- `web_detail_url`
- `receipt.message_id`
- `receipt.sent_at`
同时复用风险页中的:
- `highest_risk_level`
- `pass_status`
- `owner_notifications[]`
## 6. 关键交互
- 点击“切换卡片样式”模拟不同消息模板。
- 点击“发送通知”切换为已发送状态。
- 点击责任人标签,查看角色映射详情。
- 点击 Web 详情按钮,跳转到 mock 审计详情链接。
- 点击“切换通知原因”,在 `task_completed / task_failed` 两类消息之间切换。
## 7. 与上下游页面的数据关系
上游:
- 风险预警页提供责任角色和风险摘要
- Word 回填导出页提供导出状态
下游:无,属于主线收尾页。
## 8. 演示话术重点
- 强调平台不止有 Web 界面,还有飞书协同入口。
- 强调系统可以在任务完成或异常后,直接把结果转换成责任人可执行的通知载荷。
- 强调通知里保留 Web 详情链接,便于追溯。
## 9. 与知识库 / 治理台的关联入口
本页应提供:
- `维护责任人映射`
- `维护飞书机器人配置`
- `维护消息模板`
- `查看发送日志`
这些入口进入治理台中的责任人映射和飞书通知配置 CRUD。

View File

@@ -0,0 +1,229 @@
# 知识库与治理台原型设计
## 1. 页面目标
作为整套原型的治理能力承载层统一展示法规知识、RAG 证据、字段标准、模板映射、责任人和飞书配置如何被维护,并明确支持完整 CRUD。
在最新版产品形态中,`知识库` 已升级为四个一级入口之一;同时仍允许从审核智能体、资料包和处理历史页通过治理入口快速打开对应对象详情。
## 2. 适用角色
- 规则维护人员
- 平台管理员
- 注册资料平台主管
- 演示讲解人
## 3. 治理台布局分区
建议采用“左侧治理对象导航 + 中部列表 + 右侧详情 / 编辑抽屉”的结构。
左侧对象导航固定包含:
1. 法规规则包
2. RAG 文档源
3. RAG 切片
4. 字段 Schema
5. Word 模板与字段映射
6. 责任人映射
7. 飞书通知配置
## 4. 法规规则包 CRUD
### 4.1 列表字段
- 规则包名称
- 适用流程
- 版本号
- 启用状态
- 最近更新时间
- 维护人
### 4.2 必备操作
- 新增规则包
- 编辑规则包
- 复制新版本
- 启用 / 停用
- 删除未生效草稿
- 查看章节要求详情
### 4.3 原型交互要求
- 点击规则包行,右侧打开章节树与要求项详情。
- 点击“新增规则包”,打开表单弹窗。
## 5. RAG 文档源 CRUD
### 5.1 列表字段
- 文档名称
- 文档类型
- 来源类别
- 版本号
- 入库状态
- 切片数量
- 最近同步时间
### 5.2 必备操作
- 上传新文档源
- 替换版本
- 编辑元数据
- 停用文档源
- 删除失效文档源
- 重新入库
### 5.3 原型交互要求
- 支持按“法规 / 模板 / 业务资料”过滤。
- 点击文档源查看切片预览和召回配置。
## 6. RAG 切片 CRUD
### 6.1 列表字段
- 切片 ID
- 所属文档
- 章节
- 片段摘要
- 切片长度
- 召回状态
- 最近更新时间
### 6.2 必备操作
- 新增手工切片
- 编辑切片摘要
- 合并切片
- 拆分切片
- 删除切片
- 重建向量
- 调整召回阈值
### 6.3 原型交互要求
- 点击切片行,右侧展示原文预览和命中场景。
- 支持“查看证据命中历史”的详情抽屉。
## 7. 字段 Schema CRUD
### 7.1 列表字段
- 字段编码
- 中文名
- 字段类型
- 是否可回填
- 是否强一致
- 启用状态
- 版本号
### 7.2 必备操作
- 新增字段
- 编辑字段
- 启停字段
- 删除草稿字段
- 复制 schema 版本
### 7.3 原型交互要求
- 点击字段查看来源优先级和适用页面。
- 支持切换“回填字段 / 强一致字段 / 全部字段”。
## 8. Word 模板与字段映射 CRUD
### 8.1 列表字段
- 模板名称
- 输出类型
- 版本号
- 占位符数量
- 启用状态
- 最近更新时间
### 8.2 必备操作
- 上传模板
- 编辑模板元数据
- 编辑占位符映射
- 启用 / 停用版本
- 删除未发布版本
- 预览模板
### 8.3 原型交互要求
- 点击模板打开占位符和字段映射详情。
- 支持查看“阻断条件影响哪些占位符”。
## 9. 责任人映射 CRUD
### 9.1 列表字段
- 映射类型
- 章节 / 风险类型
- 责任角色
- 责任人姓名
- 部门
- 飞书用户 ID
- 飞书 Open ID
- 飞书昵称
- 是否启用通知
- 启用状态
- 备注
### 9.2 必备操作
- 新增映射
- 编辑映射
- 启停映射
- 删除映射
- 批量导入映射
### 9.3 原型交互要求
- 点击某条映射可联动查看受影响风险项或飞书卡片预览。
## 10. 飞书通知配置 CRUD
### 10.1 列表字段
- 配置名称
- 群聊 / 机器人标识
- 消息模板
- 触发原因
- Web 链接模板
- 启用状态
- 最近测试状态
### 10.2 必备操作
- 新增配置
- 编辑配置
- 切换消息模板
- 启用 / 停用
- 删除草稿配置
- 发送测试消息
### 10.3 原型交互要求
- 支持查看 interactive card 模板预览。
- 支持查看最近一次回执记录。
- 固定展示两类触发原因:`task_completed``task_failed`
## 11. 与业务页面的入口关系
各主页面的治理入口分配如下:
- 资料包导入页RAG 文档源、章节点规则
- 法规完整性检查页法规规则包、RAG 切片
- 字段抽取页:字段 Schema、字段来源优先级
- 一致性核查页:强一致规则
- 风险预警页:风险规则、责任人映射
- Word 回填页:模板与字段映射
- 飞书通知视图:责任人映射、飞书通知配置
## 12. 演示话术重点
- 强调这套系统不是一次性写死,而是可以被维护和演进。
- 强调法规判断、RAG 证据、字段标准、模板映射和飞书配置都能被可视化治理。
- 强调 CRUD 的存在,说明平台具备从 Demo 走向实际业务平台的治理基础。

View File

@@ -0,0 +1,248 @@
# 注册审核平台整体原型设计
## 1. 产品定位与演示目标
本轮原型面向:
```text
试剂盒临床注册文件准备与审核智能体平台
```
原型不强调“通用 Agent 工具箱”,而是围绕 NMPA 体外诊断试剂注册申报资料场景,展示一条可讲解、可追溯、可协同的审核闭环。
本轮演示目标:
1. 让评委在 1 分钟内理解平台解决什么问题。
2. 让业务人员看到资料包导入、法规核查、字段抽取、一致性核查、风险预警、Word 回填、飞书协同的完整链路。
3. 让技术评委看到系统具备规则优先、RAG 证据解释、结构化输出和治理后台能力。
4. 为后续 Django 页面重构或前端实现提供可直接照做的页面规格。
## 2. 演示总动线
最新版原型采用“顶层 4 个 Tab + 对话内节点推进”的组织方式,推荐演示顺序如下:
1. 先进入 `审核智能体`,展示左侧会话历史、中间对话区、右侧上传区和任务卡片区。
2. 在对话页上传资料包,触发目录汇总、完整性检查、字段抽取、一致性核查和风险结论节点。
3. 点击对话节点,解释每一步的结构化结果如何在右侧卡片实时更新状态。
4. 切换到 `资料包`,展示资料包列表、按产品名称搜索、资料包与会话绑定关系,以及目录、页数、章节点结果。
5. 切换到 `知识库`说明法规资料、业务资料、RAG 切片、字段 Schema、模板映射、责任人映射和飞书配置如何治理。
6. 切换到 `处理历史`,说明如何按批次、产品和风险状态回看历史会话、资料规模和通知留痕。
7. 返回对话中的风险与导出节点,最后展示飞书完成通知或异常通知如何直接 `@` 处理人并携带 Web 详情链接。
法规依据不再以单独一级页面存在,而是作为知识库中的法规资料,由 RAG 在对话节点中返回依据说明。
## 3. 统一视觉风格
### 3.1 视觉关键词
- 监管科技
- 专业可信
- 高信息密度
- 可解释工作台
- 演示友好
### 3.2 色彩建议
- 主背景:以白色为主,辅以极浅灰蓝,保证整体简洁和专业。
- 主强调色:克制的深青蓝,用于导航、主按钮、激活态和重点数值。
- 风险色:铜橙、深红,用于缺失、冲突、高风险、阻断状态。
- 成功色:低饱和绿色,用于完成、可通过、已同步状态。
- 中性色:冷灰,用于说明文字、边框、标签和禁用态。
### 3.3 组件风格
- 使用“带状布局 + 信息舱 + 分析表格”的组合,不做普通后台卡片堆砌。
- 表格、目录树、证据侧栏、步骤时间线是核心表达方式。
- 高风险结论必须同时展示证据来源和责任角色,避免只有颜色没有解释。
## 4. 统一交互规范
### 4.1 全局导航
- 顶部固定展示:平台名称、顶层 Tab、右上角用户信息。
- 顶层 Tab 固定为:`审核智能体 / 资料包 / 知识库 / 处理历史`
- `审核智能体` 内部采用三栏布局,不再使用左侧 8 页面流程导航。
- 每个页面保留“打开治理台”或同等治理入口。
### 4.2 统一交互规则
- 对话是主执行入口,页面切换是不同信息视角,而不是不同执行系统。
- 所有结果区都支持“查看来源证据”。
- 所有关键对象都支持“打开详情抽屉”。
- 所有治理对象都遵循“列表 -> 详情 -> 新增 / 编辑 / 启停 / 删除”的统一 CRUD 结构。
- 风险阻断项必须在对话节点、Word 导出区和飞书通知视图里持续可见,保证前后呼应。
- 资料包必须可跳转到关联会话,会话标题默认采用解析后的 `product_name`
### 4.3 状态规范
统一状态口径:
- `待导入`
- `处理中`
- `已完成`
- `待复核`
- `已阻断`
- `已发送`
- `失败`
统一风险口径:
- `高`
- `中`
- `低`
- `待确认`
## 5. 统一 Mock 数据口径
本轮所有文档和 HTML 共用同一组演示数据,避免页面间口径冲突。
### 5.1 批次口径
- `batch_id`: `SUB-20260603-001`
- `workflow_type`: `registration`
- `product_name`: `新型冠状病毒 2019-nCoV 核酸检测试剂盒`
- `conversation_id`: `conv-001`
- `applicant_name`: `示例生物科技(上海)有限公司`
- `chapter_scope`: `CH1 ~ CH6`
### 5.2 主线风险口径
固定演示问题:
1. `CH1.11.4` 缺少一份必交声明类资料。
2. 一份沟通记录疑似错放到 `CH1.9` 目录。
3. 说明书与申请表中的产品名称存在文本不一致。
4. 储存条件字段存在待人工复核状态。
5. 风险等级为高,当前批次不允许正式导出,仅允许生成草稿。
6. 飞书通知在任务完成或出现异常后直接 `@注册资料负责人``@注册申报负责人`
### 5.3 共用对象定义
文档和 HTML 共用以下结构化对象名称:
- `registration_overview_report`
- `registration_completeness_report`
- `registration_field_extraction_report`
- `registration_consistency_report`
- `registration_risk_report`
- `registration_word_export_report`
- `feishu_notification_report`
治理台对象:
- `knowledge_rule_package`
- `rag_source_document`
- `rag_chunk_item`
- `field_schema_item`
- `template_mapping_item`
- `owner_mapping_item`
- `feishu_channel_config`
## 6. 页面跳转关系
主页面关系调整为:
```text
顶部导航
-> 审核智能体
-> 资料包
-> 知识库
-> 处理历史
审核智能体内部节点
-> 资料包导入
-> 法规完整性检查
-> 字段抽取与字段池
-> 一致性核查
-> 风险预警
-> Word 回填导出
-> 飞书通知
```
跨页关系约束:
- 资料包导入页产出 `registration_overview_report`
- 法规完整性检查页消费 `registration_overview_report`,产出 `registration_completeness_report`
- 字段抽取与字段池页消费导入结果和完整性结果,产出 `registration_field_extraction_report`
- 一致性核查页消费字段池,产出 `registration_consistency_report`
- 风险预警页消费前三类报告,产出 `registration_risk_report`
- Word 回填导出页消费字段池、一致性和风险报告,产出 `registration_word_export_report`
- 飞书通知视图消费风险报告和导出报告,产出 `feishu_notification_report`
## 7. HTML 演示站说明
### 7.1 交付方式
交付一个单文件 HTML
`docs/原型设计/registration-prototype-demo.html`
该文件仅展示 mock 内容,不接真实 Django 路由,不调用真实接口。
### 7.2 HTML 结构要求
- 一个全局 App Shell
- 四个顶层页面 section
- 一个以对话节点为核心的审核智能体 section
- 一个治理台抽屉层
- 一份统一 mock 数据对象
- 一组轻量 JavaScript 交互
### 7.3 必备交互
- 切换 `审核智能体 / 资料包 / 知识库 / 处理历史` 视图
- 切换会话历史
- 点击对话节点定位到对应执行阶段
- 展开 / 收起目录树
- 资料包按产品名称或批次号搜索
- 打开治理台抽屉
- 切换治理对象 CRUD 子视图
- 模拟 Word 导出状态切换
- 模拟飞书消息卡片预览
## 8. 文档拆分说明
本轮分页文档如下:
1. [1.1.资料包导入页原型设计](F:\PyCharm\DEMO-AGENT\docs\原型设计\1.1.资料包导入页原型设计.md)
2. [1.2.审核智能体页原型设计](F:\PyCharm\DEMO-AGENT\docs\原型设计\1.2.审核智能体页原型设计.md)
3. [1.3.法规完整性检查页原型设计](F:\PyCharm\DEMO-AGENT\docs\原型设计\1.3.法规完整性检查页原型设计.md)
4. [1.4.字段抽取与字段池页原型设计](F:\PyCharm\DEMO-AGENT\docs\原型设计\1.4.字段抽取与字段池页原型设计.md)
5. [1.5.一致性核查页原型设计](F:\PyCharm\DEMO-AGENT\docs\原型设计\1.5.一致性核查页原型设计.md)
6. [1.6.风险预警页原型设计](F:\PyCharm\DEMO-AGENT\docs\原型设计\1.6.风险预警页原型设计.md)
7. [1.7.Word回填导出页原型设计](F:\PyCharm\DEMO-AGENT\docs\原型设计\1.7.Word回填导出页原型设计.md)
8. [1.8.飞书通知视图原型设计](F:\PyCharm\DEMO-AGENT\docs\原型设计\1.8.飞书通知视图原型设计.md)
9. [1.9.知识库与治理台原型设计](F:\PyCharm\DEMO-AGENT\docs\原型设计\1.9.知识库与治理台原型设计.md)
## 9. 实现边界
本轮原型只解决:
1. 演示表达
2. 页面结构
3. 模块关系
4. 数据口径
5. 治理台 CRUD 展示
本轮原型不直接承诺:
1. 后端真实接口联调
2. Django 模板替换
3. 真实 RAG 召回
4. 真实 Word 文件生成
5. 真实飞书 OpenAPI 调用
## 10. 结论
这套原型的核心讲法应统一为:
```text
以对话为核心的审核智能体
-> 资料包解析与会话绑定
-> 节点式审核推进
-> 知识库 / RAG 解释
-> 风险与导出决策
-> 飞书协同闭环
```
治理台负责回答“规则和知识如何维护”,审核智能体负责回答“这一批资料现在审核到了哪里、为什么这样判断、下一步谁来处理”。

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,150 @@
# 1. 资料包导入与目录汇总详细设计
## 1. 设计目标
本步骤对应最新版原型中的 `资料包` 页面与 `审核智能体` 上传入口,目标是把用户导入的注册申报资料转为:
1. 可绑定会话的资料包对象
2. 可展示目录和页数的结构化结果
3. 可进入后续审核链路的文档底座
本步骤执行完成后,系统应至少产出:
1. `batch_id`
2. `product_name`
3. `conversation_id`
4. `registration_overview_report`
## 2. 页面与模块对应
### 2.1 `资料包` 页面
用于展示:
1. 资料包列表
2. 产品名称搜索框
3. 资料包与会话关联关系
4. 文件目录、页数、章节点和异常
### 2.2 `审核智能体` 页面
用于触发:
1. 上传资料包
2. 自动创建或绑定会话
3. 在对话中插入“目录汇总”节点结果
### 2.3 `apps.documents`
负责:
1. 资料包模型
2. 文档模型
3. 页数统计
4. 章节点识别
5. 目录汇总服务
## 3. 核心数据结构
### 3.1 SubmissionBatch
建议包含:
1. `batch_no`
2. `product_name`
3. `workflow_type`
4. `conversation_id`
5. `file_count`
6. `page_count`
7. `import_status`
8. `exception_count`
### 3.2 UploadedDocument
建议增加:
1. `batch_id`
2. `relative_path`
3. `chapter_code`
4. `document_role`
5. `page_count`
6. `page_count_confidence`
7. `chapter_match_status`
8. `needs_manual_review`
## 4. 主流程
```text
上传资料包
-> 创建批次
-> 保存原始文件
-> 解包 / 扫描目录
-> 统计页数
-> 识别产品名称
-> 识别章节点
-> 生成目录汇总
-> 创建或绑定会话
-> 返回资料包页与对话节点
```
## 5. 关键节点设计
### 5.1 产品名称解析
优先来源:
1. 申请表
2. 目标产品说明书
3. 产品列表
结果用途:
1. 作为资料包主标题
2. 作为会话标题
3. 作为资料包搜索主字段
### 5.2 资料包与会话绑定
规则固定为:
1. 新导入资料包默认生成一个主会话
2. 主会话标题使用解析后的 `product_name`
3. 资料包页“查看对话”跳转到 `conversation_id`
### 5.3 目录汇总输出
输出对象 `registration_overview_report` 至少包含:
1. `batch_id`
2. `product_name`
3. `file_count`
4. `total_page_count`
5. `chapter_summary`
6. `documents`
7. `warnings`
## 6. 异常策略
以下情况标记为待复核:
1. DOC 页数无法精确统计
2. 章节点无法确定
3. 产品名称来源冲突
4. 目录路径疑似错放
## 7. 与后续步骤的接口
本步骤向后续步骤提供:
1. `batch_id`
2. `conversation_id`
3. `product_name`
4. `document_scope`
5. `chapter_summary`
## 8. 验收标准
1. 资料包导入后形成批次记录。
2. 会话标题使用解析出的产品名称。
3. 资料包页支持按产品名称搜索。
4. 目录汇总结果可直接进入 Agent 节点展示。

View File

@@ -0,0 +1,66 @@
# 2. 法规完整性检查详细设计
## 1. 设计目标
本步骤对应 `审核智能体` 中的“完整性检查”节点,目标是基于结构化规则和法规 RAG 证据判断资料包是否齐套、是否错放、是否存在高风险缺失项。
## 2. 输入
1. `batch_id`
2. `conversation_id`
3. `product_name`
4. `chapter_summary`
5. `document_scope`
6. 规则包与法规知识索引
## 3. 规则与 RAG 分工
### 3.1 规则负责
1. 必交项判断
2. 章节点匹配
3. 缺失/错放分类
4. 风险等级映射
### 3.2 RAG 负责
1. 定位法规依据片段
2. 提供条款解释
3. 为对话输出引用依据
## 4. 主流程
```text
读取资料包目录
-> 加载完整性规则包
-> 比对章节点与资料要求
-> 输出缺失项 / 错放项 / 待复核项
-> 通过 RAG 补充法规依据
-> 生成完整性节点结果
```
## 5. 输出对象
`registration_completeness_report` 至少包含:
1. `missing_items`
2. `misplaced_items`
3. `manual_review_items`
4. `risk_level`
5. `rule_hits`
6. `rag_evidence`
## 6. 对话节点要求
完整性检查节点在会话中应展示:
1. 缺失项摘要
2. 错放项摘要
3. 风险等级
4. 法规依据来源
## 7. 验收标准
1. 能区分缺失、错放和待复核。
2. 规则结论与法规依据分层清晰。
3. 可直接用于后续风险预警和飞书通知。

View File

@@ -0,0 +1,58 @@
# 3. 字段抽取与统一字段池详细设计
## 1. 设计目标
本步骤负责从说明书、申请表、产品列表等资料中抽取关键字段,并写入统一字段池,供一致性核查、风险判断和 Word 回填复用。
## 2. 输入
1. `batch_id`
2. `conversation_id`
3. `product_name`
4. `document_scope`
5. `field_schema`
6. `source_priority`
## 3. 字段池模型
字段项至少包含:
1. `field_code`
2. `field_name`
3. `field_value`
4. `source_document_id`
5. `source_location`
6. `confidence`
7. `review_status`
8. `fillable`
## 4. 抽取策略
1. 规则抽取显式字段
2. 表格抽取规格与结构化字段
3. LLM 归纳长文本字段
4. 来源优先级合并同名字段
## 5. 输出对象
`registration_field_extraction_report` 至少包含:
1. `field_items`
2. `source_documents`
3. `low_confidence_items`
4. `fillable_items`
## 6. 对话节点要求
字段抽取节点应展示:
1. 已抽取字段数
2. 待复核字段数
3. 关键字段来源
4. 是否可回填
## 7. 验收标准
1. 统一字段池可支撑后续一致性核查和回填。
2. 低置信度字段有明确待复核标记。
3. 对话中可解释字段来源和采用逻辑。

View File

@@ -0,0 +1,56 @@
# 4. 一致性核查详细设计
## 1. 设计目标
本步骤负责基于统一字段池和资料包上下文,识别跨文档字段冲突、混档风险和待人工确认项。
## 2. 输入
1. `batch_id`
2. `conversation_id`
3. `product_name`
4. `field_pool`
5. `strong_consistency_rules`
## 3. 核查维度
1. 产品名称
2. 检测靶标
3. 适用范围
4. 储存条件
5. 规格/型号
## 4. 主流程
```text
加载字段池
-> 读取强一致规则
-> 对比多来源字段值
-> 识别冲突字段
-> 判断是否存在混档风险
-> 输出一致性节点结果
```
## 5. 输出对象
`registration_consistency_report` 至少包含:
1. `conflict_items`
2. `consistent_items`
3. `mixed_package_risk`
4. `recommended_value`
## 6. 对话节点要求
一致性节点应展示:
1. 冲突字段
2. 来源对比
3. 是否疑似混档
4. 建议采用值
## 7. 验收标准
1. 能对产品名称等强一致字段给出明确冲突结论。
2. 能说明冲突来自哪些文档。
3. 能把冲突结果直接传递给风险预警和飞书通知。

View File

@@ -0,0 +1,66 @@
# 5. 风险预警详细设计
## 1. 设计目标
本步骤汇总完整性检查、字段抽取和一致性核查结果,形成统一风险报告,并决定是否允许正式导出。
## 2. 输入
1. `registration_completeness_report`
2. `registration_field_extraction_report`
3. `registration_consistency_report`
4. `risk_rule_set`
## 3. 风险类型
1. 缺失必交资料
2. 资料错放
3. 字段低置信度
4. 字段冲突
5. 混档风险
6. 导出阻断风险
## 4. 输出对象
`registration_risk_report` 至少包含:
1. `risk_items`
2. `highest_risk_level`
3. `pass_status`
4. `manual_review_items`
5. `owner_roles`
6. `suggestions`
## 5. 责任角色输出
风险结果必须产出责任角色,为飞书协同做准备。责任角色实体至少包含:
1. `owner_role`
2. `owner_name`
3. `department`
4. `chapter_scope`
5. `risk_scope`
6. `feishu_user_id`
7. `feishu_open_id`
8. `feishu_name`
9. `notify_enabled`
风险结果还应直接给出通知触发语义,供飞书步骤复用:
1. 正常完成时使用 `task_completed`
2. 异常失败时使用 `task_failed`
## 6. 对话节点要求
风险节点应展示:
1. 总风险等级
2. 是否通过
3. 整改建议
4. 对应责任角色
## 7. 验收标准
1. 风险结论可直接决定是否允许正式导出。
2. 输出中包含责任角色和通知所需字段。
3. 能向飞书通知步骤直接提供摘要与责任人。

View File

@@ -0,0 +1,41 @@
# 6. Word 回填导出详细设计
## 1. 设计目标
本步骤负责把统一字段池中的可回填字段写入注册申报表格或对照清单,并在风险允许时生成导出文件。
## 2. 输入
1. `field_pool`
2. `template_mapping`
3. `registration_risk_report`
## 3. 导出策略
1. 始终允许生成草稿版
2. 命中高风险阻断正式版
3. 字段冲突或关键缺失时给出拦截原因
## 4. 输出对象
`registration_word_export_report` 至少包含:
1. `fillable_items`
2. `blocked_items`
3. `draft_export_status`
4. `formal_export_status`
5. `download_url`
## 5. 页面与会话要求
在对话或结果展示中应能看到:
1. 哪些字段已回填
2. 哪些字段被拦截
3. 为什么正式版不能导出
## 6. 验收标准
1. 草稿版和正式版策略区分明确。
2. 风险结果能阻断正式导出。
3. 导出结果可直接提供给飞书消息摘要使用。

View File

@@ -0,0 +1,131 @@
# 7. 飞书通知详细设计
## 1. 设计目标
本步骤负责在审核任务执行完成或执行异常时,通过飞书 `@` 对应处理人,并回传 Web 详情链接。
V1 当前 Demo 固定通知策略:
1. 执行完成后发送结果摘要并 `@` 处理人
2. 执行异常后发送异常摘要并 `@` 处理人
## 2. 角色信息模型
责任人信息不再只保留角色名,必须扩展为可通知实体,至少包含:
1. `owner_role`
2. `owner_name`
3. `department`
4. `chapter_scope`
5. `risk_scope`
6. `feishu_user_id`
7. `feishu_open_id`
8. `feishu_name`
9. `notify_enabled`
## 3. 输入
1. `batch_id`
2. `conversation_id`
3. `product_name`
4. `notify_reason`
5. `registration_risk_report`
6. `registration_word_export_report`
7. `owner_mapping`
其中 `notify_reason` 固定支持:
1. `task_completed`
2. `task_failed`
## 4. 输出对象
`feishu_notification_report` 至少包含:
1. `batch_id`
2. `conversation_id`
3. `notify_reason`
4. `mentioned_users`
5. `message_status`
6. `web_detail_url`
7. `receipt`
## 5. 主流程
```text
任务完成或异常
-> 读取责任角色与飞书账号
-> 构建飞书摘要
-> 构建 @ 处理人载荷
-> 发送飞书消息
-> 写回发送回执
-> 写入处理历史和审计
```
## 6. 通知内容要求
飞书消息至少应包含:
1. 任务名称
2. 产品名称
3. 批次号
4. 结果状态
5. 风险等级或异常摘要
6. `@` 处理人
7. Web 详情链接
## 7. 处理完成通知
触发条件:
1. 目录汇总完成
2. 风险报告完成
3. 导出状态已生成
输出重点:
1. 风险等级
2. 是否允许正式导出
3. 责任人
## 8. 执行异常通知
触发条件:
1. 资料解析失败
2. 规则执行失败
3. 回填导出失败
4. 外部依赖异常
输出重点:
1. 异常阶段
2. 异常摘要
3. 责任人
4. 是否建议人工介入
## 9. 与页面关系
### 9.1 审核智能体
可展示:
1. 本次是否已触发飞书通知
2. 飞书发送状态
3.`@` 的处理人
### 9.2 处理历史
可回看:
1. 通知原因
2. 接收人
3. 消息状态
4. Web 回链
## 10. 验收标准
1. 角色信息包含飞书账号相关字段。
2. 执行完成与执行异常两类通知链路完整。
3. 飞书消息支持直接 `@` 对应处理人。
4. 通知结果可在处理历史和审计中回溯。

View File

@@ -0,0 +1,90 @@
# Word回填导出编排Skill 设计
## 1. Skill 定位
`Word回填导出编排Skill` 是第六步工作流的总入口 Skill负责组织模板选择、字段映射加载、回填字段集构建、回填拦截检查、Word 渲染、版式校验和导出记录生成。
英文实现标识建议使用 `WordFillExportOrchestrateSkill`
## 2. 输入
```python
@dataclass
class WordFillExportOrchestrateInput:
batch_id: int
template_id: str
target_output_type: str
selected_field_keys: list[str] = field(default_factory=list)
allow_draft_when_blocked: bool = True
```
## 3. 输出
```python
@dataclass
class WordFillExportOrchestrateOutput:
report_type: str
batch_id: int
export_status: str
output_file: dict | None
filled_fields: list[dict]
blocked_fields: list[dict]
audit_id: int | None = None
```
## 4. 依赖 Skill
1. `模板选择Skill`
2. `模板字段映射加载Skill`
3. `回填字段集构建Skill`
4. `回填拦截检查Skill`
5. `Word模板渲染Skill`
6. `导出版式校验Skill`
7. `导出记录生成Skill`
## 5. 核心方法
### 5.1 `run(input) -> WordFillExportOrchestrateOutput`
主入口方法。
### 5.2 `load_export_context(input) -> WordExportContext`
加载字段池、风险报告和一致性报告。
### 5.3 `resolve_export_mode(blockers) -> str`
确认正式、草稿或拦截模式。
## 6. 技术实现
使用技术:
1. Tool Registry
2. Django ORM
3. Django Storage
4. dataclass/Pydantic
建议注册名:
```python
tool_registry.register(
name="word_fill_export_orchestrate",
handler=WordFillExportOrchestrateSkill().run,
)
```
## 7. 异常处理
1. 模板缺失:任务失败。
2. 字段池缺失:任务失败。
3. 正式导出被拦截:按配置生成草稿或直接返回拦截。
4. 渲染失败:写失败审计。
## 8. 测试要点
1. 能按顺序调用依赖 Skill。
2. 冲突字段导致正式导出拦截。
3. 草稿模式可生成文件。
4. 输出报告稳定。

View File

@@ -0,0 +1,67 @@
# Word模板渲染Skill 设计
## 1. Skill 定位
`Word模板渲染Skill` 负责将回填字段集写入 Word 模板,生成新的 `.docx` 文件。
英文实现标识建议使用 `WordTemplateRenderSkill`
## 2. 输入
```python
@dataclass
class WordTemplateRenderInput:
template_file_path: Path
fill_dataset: dict
export_mode: str
output_dir: Path
```
## 3. 输出
```python
@dataclass
class WordTemplateRenderOutput:
output_file_path: Path
rendered_placeholders: list[str]
render_warnings: list[dict]
```
## 4. 核心方法
### 4.1 `run(input) -> WordTemplateRenderOutput`
主入口方法。
### 4.2 `replace_paragraph_placeholders(document, values) -> None`
替换段落占位符。
### 4.3 `replace_table_placeholders(document, values) -> None`
替换表格占位符。
### 4.4 `save_document(document, output_path) -> Path`
保存文档。
## 5. 技术实现
使用技术:
1. `python-docx`
2. 可选 `docxtpl`
3. Django Storage
## 6. 异常处理
1. 模板打不开:任务失败。
2. 占位符替换失败:记录警告。
3. 保存失败:任务失败。
## 7. 测试要点
1. 段落占位符可替换。
2. 表格占位符可替换。
3. 输出文件存在。

View File

@@ -0,0 +1,88 @@
# 一致性报告生成Skill 设计
## 1. Skill 定位
`一致性报告生成Skill` 负责将字段比对结果和混档风险组装成稳定的 `registration_consistency_report`,并生成页面展示、审计和飞书摘要载荷。
英文实现标识建议使用 `ConsistencyReportBuildSkill`
## 2. 输入
```python
@dataclass
class ConsistencyReportBuildInput:
context: ConsistencyReviewContext
compare_results: list[FieldCompareResult]
mixed_package_warnings: list[dict]
```
## 3. 输出
```python
@dataclass
class ConsistencyReportBuildOutput:
report: dict
display_sections: list[dict]
audit_payload: dict
feishu_summary_payload: dict
```
## 4. 报告结构
报告必须包含:
1. `report_type`
2. `batch_id`
3. `field_rule_id`
4. `summary`
5. `consistent_fields`
6. `conflict_fields`
7. `manual_review_fields`
8. `mixed_package_warnings`
9. `suggestions`
## 5. 核心方法
### 5.1 `run(input) -> ConsistencyReportBuildOutput`
主入口方法。
### 5.2 `build_summary(compare_results, warnings) -> dict`
生成汇总。
### 5.3 `split_compare_results(compare_results) -> dict`
拆分一致、冲突、待复核字段。
### 5.4 `build_suggestions(conflicts, warnings) -> list[dict]`
生成处理建议。
### 5.5 `build_audit_payload(report, context) -> dict`
生成审计载荷。
## 6. 技术实现
使用技术:
1. dataclass/Pydantic
2. JSONField
3. Audit 服务
4. 页面展示 schema
## 7. 异常处理
1. 报告字段缺失:任务失败。
2. 没有可比对字段:输出空报告。
3. 飞书摘要构建失败:不影响 Web 报告。
4. 审计写入失败:记录系统警告。
## 8. 测试要点
1. 冲突字段进入 `conflict_fields`
2. 混档风险进入 `mixed_package_warnings`
3. 汇总数量正确。
4. 审计载荷包含审核范围。

View File

@@ -0,0 +1,103 @@
# 一致性核查编排Skill 设计
## 1. Skill 定位
`一致性核查编排Skill` 是第四步工作流的总入口 Skill负责组织审核范围确认、强一致规则加载、字段分组、完全一致比对、混档风险识别、字段池状态回写和报告生成。
英文实现标识建议使用 `ConsistencyReviewOrchestrateSkill`
## 2. 输入
```python
@dataclass
class ConsistencyReviewOrchestrateInput:
batch_id: int
scenario_id: str = "registration_consistency_review"
selected_document_ids: list[int] = field(default_factory=list)
field_rule_id: str = "ivd_strict_consistency_v1"
target_field_keys: list[str] = field(default_factory=list)
strict_mode: bool = True
```
## 3. 输出
```python
@dataclass
class ConsistencyReviewOrchestrateOutput:
report_type: str
batch_id: int
summary: dict
consistent_fields: list[dict]
conflict_fields: list[dict]
manual_review_fields: list[dict]
mixed_package_warnings: list[dict]
audit_id: int | None = None
```
## 4. 依赖 Skill
1. `审核范围确认Skill`
2. `强一致规则加载Skill`
3. `字段分组Skill`
4. `字段完全一致比对Skill`
5. `混档风险识别Skill`
6. `一致性报告生成Skill`
## 5. 核心方法
### 5.1 `run(input) -> ConsistencyReviewOrchestrateOutput`
主入口方法。
执行顺序:
1. 读取统一字段池。
2. 调用 `审核范围确认Skill`
3. 调用 `强一致规则加载Skill`
4. 调用 `字段分组Skill`
5. 调用 `字段完全一致比对Skill`
6. 调用 `混档风险识别Skill`
7. 回写字段池冲突状态。
8. 调用 `一致性报告生成Skill`
9. 写入审计。
### 5.2 `load_field_pool(batch_id) -> list[FieldPoolItem]`
读取字段池主表和候选值。
### 5.3 `update_field_pool_status(compare_results) -> FieldPoolUpdateResult`
回写一致、冲突、待复核状态。
## 6. 技术实现
使用技术:
1. Django ORM
2. Tool Registry
3. dataclass/Pydantic
4. Audit 服务
建议注册名:
```python
tool_registry.register(
name="consistency_review_orchestrate",
handler=ConsistencyReviewOrchestrateSkill().run,
)
```
## 7. 异常处理
1. 字段池不存在:任务失败并提示先执行字段抽取。
2. 审核范围为空:返回业务错误。
3. 规则缺失:任务失败并写审计。
4. 字段池回写失败:报告仍生成,但标记系统警告。
## 8. 测试要点
1. 能按顺序调用依赖 Skill。
2. 字段池缺失时返回清晰错误。
3. 冲突结果能回写字段池。
4. 输出报告 schema 稳定。

View File

@@ -0,0 +1,70 @@
# 准入判定Skill 设计
## 1. Skill 定位
`准入判定Skill` 负责根据风险项、人工复核项和准入规则计算最终是否通过。
英文实现标识建议使用 `AdmissionDecisionSkill`
## 2. 输入
```python
@dataclass
class AdmissionDecisionInput:
risk_items: list[RiskItem]
manual_review_items: list[dict]
admission_rules: dict
```
## 3. 输出
```python
@dataclass
class AdmissionDecisionOutput:
pass_status: str
highest_risk_level: str
decision_reason: str
score_detail: dict
```
## 4. 判定规则
1. 任一高风险:不通过。
2. 多个中风险:待整改后复核。
3. 只有低风险:条件通过。
4. 有人工复核项:待复核。
5. 无风险:通过。
## 5. 核心方法
### 5.1 `run(input) -> AdmissionDecisionOutput`
主入口方法。
### 5.2 `calculate_highest_level(risk_items) -> str`
计算最高风险等级。
### 5.3 `calculate_status(risk_items, manual_review_items, rules) -> str`
计算准入状态。
### 5.4 `build_decision_reason(status, risk_items) -> str`
生成判定理由。
## 6. 技术实现
使用技术:
1. 风险等级枚举
2. 准入规则 YAML
3. Python 规则判断
## 7. 测试要点
1. 高风险导致失败。
2. 中风险过多导致复核。
3. 低风险条件通过。
4. 无风险通过。

View File

@@ -0,0 +1,67 @@
# 前序报告汇总Skill 设计
## 1. Skill 定位
`前序报告汇总Skill` 负责读取当前批次已经生成的完整性、字段抽取和一致性核查报告,并统一转为风险评估上下文。
英文实现标识建议使用 `SourceReportCollectSkill`
## 2. 输入
```python
@dataclass
class SourceReportCollectInput:
batch_id: int
include_reports: list[str]
```
## 3. 输出
```python
@dataclass
class SourceReportCollectOutput:
reports: dict[str, dict]
missing_reports: list[str]
stale_reports: list[str]
validation_warnings: list[dict]
```
## 4. 核心方法
### 4.1 `run(input) -> SourceReportCollectOutput`
主入口方法。
### 4.2 `load_report(batch_id, report_type) -> dict | None`
读取报告快照。
### 4.3 `validate_report_freshness(report) -> bool`
校验报告是否过期。
### 4.4 `build_report_bundle(reports) -> SourceReportBundle`
构建报告集合。
## 5. 技术实现
使用技术:
1. Django ORM
2. JSONField
3. 报告版本号
## 6. 异常处理
1. 报告不存在:加入 `missing_reports`
2. 报告 schema 不完整:加入警告。
3. 报告过期:加入 `stale_reports`
## 7. 测试要点
1. 能读取多个报告。
2. 缺失报告能识别。
3. 过期报告能识别。
4. 输出 bundle 稳定。

View File

@@ -0,0 +1,137 @@
# 压缩包解包Skill 设计
## 1. Skill 定位
`压缩包解包Skill` 负责压缩包解包,是资料包导入链路中的文件展开能力。它需要支持 `zip``rar``7z`,并确保解包过程安全、可追踪、可复核。
本 Skill 的输出是“解包后的文件树”,不是文档审核结论。
英文实现标识建议使用 `ArchiveExtractionSkill`,用于 Python 类名和 Tool Registry 注册处理器。
## 2. 输入
```python
@dataclass
class ArchiveExtractionInput:
archive_path: Path
target_dir: Path
source_archive_name: str
max_file_count: int = 1000
max_total_size: int | None = None
```
## 3. 输出
```python
@dataclass
class ArchiveExtractionOutput:
status: str
archive_type: str
extracted_files: list[ExtractedFile]
skipped_items: list[SkippedArchiveItem]
warnings: list[dict]
```
`ExtractedFile` 字段:
1. `absolute_path`
2. `relative_path`
3. `file_size`
4. `source_archive_name`
5. `file_hash`
## 4. 支持格式与技术
| 格式 | 技术 |
|---|---|
| `zip` | Python 标准库 `zipfile` |
| `rar` | 纯 Python rar 解析依赖,优先选择不依赖系统命令的库 |
| `7z` | `py7zr` |
V1 可以先完成 `zip`,同时保留 `rar``7z` 的接口和失败提示。正式验收前需要补齐纯 Python 解包能力。
## 5. 核心方法
### 5.1 `run(input) -> ArchiveExtractionOutput`
主入口方法。
处理顺序:
1. 识别压缩包类型。
2. 打开压缩包。
3. 遍历成员文件。
4. 校验成员路径安全性。
5. 校验数量和大小限制。
6. 解包到批次隔离目录。
7. 生成解包结果。
### 5.2 `detect_archive_type(path) -> str`
根据扩展名和文件头识别压缩包类型。
返回:
1. `zip`
2. `rar`
3. `7z`
4. `unsupported`
### 5.3 `validate_member_path(member_name) -> str`
路径安全校验。
拒绝:
1. 绝对路径。
2. 包含 `..` 的路径。
3. Windows 盘符路径。
4. 空路径。
5. 控制字符。
### 5.4 `extract_zip(input) -> list[ExtractedFile]`
使用 `zipfile` 解包。
### 5.5 `extract_rar(input) -> list[ExtractedFile]`
使用纯 Python rar 依赖解包。
如果当前环境依赖未安装,返回 `unsupported_archive`,并提示需要安装对应依赖。
### 5.6 `extract_7z(input) -> list[ExtractedFile]`
使用 `py7zr` 解包。
### 5.7 `build_file_hash(path) -> str`
使用 `sha256` 生成文件指纹,用于重复文件识别。
## 6. 状态设计
| 状态 | 含义 |
|---|---|
| `extracted` | 解包成功 |
| `partial_extracted` | 部分成员解包失败 |
| `empty_archive` | 压缩包为空 |
| `encrypted_archive` | 加密压缩包 |
| `unsupported_archive` | 格式不支持 |
| `path_rejected` | 存在危险路径 |
| `extract_failed` | 解包失败 |
## 7. 异常处理
1. 加密压缩包:不尝试暴力解析,标记为待人工处理。
2. 路径穿越:拒绝危险成员并记录安全拦截。
3. 超大压缩包:超过配置限制时停止处理。
4. 成员数量过多:停止处理并提示资料包异常。
5. 文件名乱码:保留原始名称,展示层提示人工复核。
## 8. 测试要点
1. 普通 zip 解包成功。
2. 多层目录相对路径被保留。
3. `../evil.docx` 被拒绝。
4. 空压缩包返回 `empty_archive`
5. 不支持格式返回 `unsupported_archive`
6. 解包后文件哈希生成稳定。

View File

@@ -0,0 +1,62 @@
# 回填字段集构建Skill 设计
## 1. Skill 定位
`回填字段集构建Skill` 负责根据模板字段映射和统一字段池构建实际要写入 Word 模板的字段值集合。
英文实现标识建议使用 `FillDatasetBuildSkill`
## 2. 输入
```python
@dataclass
class FillDatasetBuildInput:
field_pool_items: list[FieldPoolItem]
template_mappings: list[dict]
selected_field_keys: list[str] = field(default_factory=list)
```
## 3. 输出
```python
@dataclass
class FillDatasetBuildOutput:
fill_dataset: dict
missing_required_fields: list[dict]
manual_review_fields: list[dict]
```
## 4. 核心方法
### 4.1 `run(input) -> FillDatasetBuildOutput`
主入口方法。
### 4.2 `resolve_field_value(mapping, field_pool) -> FillValue`
解析字段值。
### 4.3 `build_placeholder_values(mappings, field_pool) -> dict`
生成占位符和值。
## 5. 技术实现
使用技术:
1. 字段池数据
2. 模板映射
3. Python 字典构建
## 6. 异常处理
1. 必填字段缺失:进入缺失列表。
2. 字段待复核:进入待复核列表。
3. 字段不可回填:跳过。
## 7. 测试要点
1. 可回填字段进入 dataset。
2. 必填缺失可识别。
3. 待复核字段可识别。

View File

@@ -0,0 +1,70 @@
# 回填拦截检查Skill 设计
## 1. Skill 定位
`回填拦截检查Skill` 负责根据字段冲突、风险报告和必填字段缺失情况判断是否允许正式回填导出。
英文实现标识建议使用 `FillBlockerCheckSkill`
## 2. 输入
```python
@dataclass
class FillBlockerCheckInput:
fill_dataset: dict
risk_report: dict
consistency_report: dict
missing_required_fields: list[dict]
allow_draft_when_blocked: bool
```
## 3. 输出
```python
@dataclass
class FillBlockerCheckOutput:
export_mode: str
blocked_fields: list[dict]
blockers: list[dict]
```
## 4. 拦截规则
1. 高风险未处理:拦截正式版。
2. 字段冲突:拦截正式版。
3. 必填字段缺失:拦截正式版。
4. 待人工复核:允许草稿,不允许正式版。
## 5. 核心方法
### 5.1 `run(input) -> FillBlockerCheckOutput`
主入口方法。
### 5.2 `detect_high_risk_blocker(risk_report) -> list[dict]`
检测高风险。
### 5.3 `detect_conflict_blocker(consistency_report) -> list[dict]`
检测冲突字段。
### 5.4 `resolve_export_mode(blockers, allow_draft) -> str`
确定导出模式。
## 6. 技术实现
使用技术:
1. 风险报告
2. 一致性报告
3. 本地规则
## 7. 测试要点
1. 高风险拦截正式导出。
2. 冲突字段拦截正式导出。
3. 草稿模式可用。
4. 无拦截时正式导出。

View File

@@ -0,0 +1,75 @@
# 字段Schema加载Skill 设计
## 1. Skill 定位
`字段Schema加载Skill` 负责加载注册申报字段 schema提供字段定义、来源优先级、抽取方式、回填属性和一致性要求。
英文实现标识建议使用 `FieldSchemaLoadSkill`
## 2. 输入
```python
@dataclass
class FieldSchemaLoadInput:
field_schema_id: str
target_field_keys: list[str] = field(default_factory=list)
```
## 3. 输出
```python
@dataclass
class FieldSchemaLoadOutput:
field_schema_id: str
version: str
fields: list[FieldDefinition]
source_priority: dict
validation_warnings: list[dict]
```
## 4. 核心方法
### 4.1 `run(input) -> FieldSchemaLoadOutput`
主入口方法。
### 4.2 `load_schema_file(field_schema_id) -> dict`
从 YAML 读取字段 schema。
### 4.3 `validate_field_schema(raw_schema) -> FieldSchemaValidationResult`
校验字段定义。
### 4.4 `select_target_fields(schema, target_field_keys) -> list[FieldDefinition]`
筛选目标字段。
## 5. 技术实现
使用技术:
1. `PyYAML`
2. Pydantic
3. Django cache
建议路径:
```text
configs/registration/fields/ivd_registration_fields_v1.yaml
```
## 6. 异常处理
1. schema 文件不存在:任务失败。
2. 字段定义缺少 `field_key`:校验失败。
3. 目标字段不存在:返回业务错误。
4. 来源优先级缺失:允许执行,但记录警告。
## 7. 测试要点
1. schema 加载成功。
2. 目标字段筛选正确。
3. 缺少必填字段时报错。
4. 来源优先级输出正确。

View File

@@ -0,0 +1,66 @@
# 字段分组Skill 设计
## 1. Skill 定位
`字段分组Skill` 负责将统一字段池中的字段候选按字段编码和来源文档分组,形成可比对的数据单元。
英文实现标识建议使用 `FieldGroupSkill`
## 2. 输入
```python
@dataclass
class FieldGroupInput:
field_pool_items: list[FieldPoolItem]
field_candidates: list[FieldCandidateRecord]
scope_documents: list[DocumentFact]
```
## 3. 输出
```python
@dataclass
class FieldGroupOutput:
compare_units: list[FieldCompareUnit]
single_source_fields: list[dict]
excluded_candidates: list[dict]
```
## 4. 核心方法
### 4.1 `run(input) -> FieldGroupOutput`
主入口方法。
### 4.2 `group_by_field_key(candidates) -> dict`
按字段编码分组。
### 4.3 `filter_candidates_by_scope(candidates, scope_documents) -> list`
只保留审核范围内来源文档的候选。
### 4.4 `build_compare_unit(field_key, candidates) -> FieldCompareUnit`
构建字段比对单元。
## 5. 技术实现
使用技术:
1. Python 分组
2. 字段池候选记录
3. 文档范围过滤
## 6. 异常处理
1. 字段无候选:进入待复核。
2. 来源文档不在范围内:排除候选。
3. 单来源字段:标记 `single_source`
## 7. 测试要点
1. 候选按字段分组正确。
2. 范围外候选被排除。
3. 单来源字段识别正确。

View File

@@ -0,0 +1,77 @@
# 字段完全一致比对Skill 设计
## 1. Skill 定位
`字段完全一致比对Skill` 负责按强一致规则对同一字段的不同来源值执行完全一致比对。
英文实现标识建议使用 `ExactFieldCompareSkill`
本 Skill 不做语义相似判断。
## 2. 输入
```python
@dataclass
class ExactFieldCompareInput:
compare_units: list[FieldCompareUnit]
rules: list[ConsistencyFieldRule]
```
## 3. 输出
```python
@dataclass
class ExactFieldCompareOutput:
compare_results: list[FieldCompareResult]
conflict_fields: list[dict]
consistent_fields: list[dict]
manual_review_fields: list[dict]
```
## 4. 比对规则
1. 标准值完全相等才算一致。
2. 空值不算一致证据。
3. 待复核来源不算通过证据。
4. 原始值明显差异但标准值相同,进入待复核。
## 5. 核心方法
### 5.1 `run(input) -> ExactFieldCompareOutput`
主入口方法。
### 5.2 `compare_exact(unit, rule) -> FieldCompareResult`
完全一致比对。
### 5.3 `build_conflict_result(unit, values, rule) -> FieldCompareResult`
构建冲突结果。
### 5.4 `build_consistent_result(unit, rule) -> FieldCompareResult`
构建一致结果。
## 6. 技术实现
使用技术:
1. Python 字符串比较
2. 字段标准化值
3. 风险规则
## 7. 异常处理
1. 无候选值:待人工复核。
2. 单来源:不判冲突。
3. 字段无规则:不检查。
4. 候选来源待复核:结果待复核。
## 8. 测试要点
1. 相同标准值判一致。
2. 不同标准值判冲突。
3. 单来源不判冲突。
4. 待复核来源进入人工复核。

View File

@@ -0,0 +1,89 @@
# 字段抽取报告生成Skill 设计
## 1. Skill 定位
`字段抽取报告生成Skill` 负责将字段池写入结果组装成稳定的 `registration_field_extraction_report`,并生成页面展示、审计和飞书摘要所需的数据结构。
英文实现标识建议使用 `FieldExtractionReportBuildSkill`
## 2. 输入
```python
@dataclass
class FieldExtractionReportBuildInput:
context: FieldExtractionContext
field_pool_items: list[FieldPoolItem]
manual_review_fields: list[dict]
tool_calls: list[dict]
```
## 3. 输出
```python
@dataclass
class FieldExtractionReportBuildOutput:
report: dict
display_sections: list[dict]
audit_payload: dict
feishu_summary_payload: dict
```
## 4. 报告结构
报告必须包含:
1. `report_type`
2. `batch_id`
3. `field_schema_id`
4. `field_schema_version`
5. `summary`
6. `field_pool_items`
7. `manual_review_fields`
8. `evidence_refs`
9. `tool_calls`
## 5. 核心方法
### 5.1 `run(input) -> FieldExtractionReportBuildOutput`
主入口方法。
### 5.2 `build_summary(field_pool_items) -> dict`
汇总字段数量、已抽取数量、待复核数量和冲突候选数量。
### 5.3 `build_field_rows(field_pool_items) -> list[dict]`
生成字段池页面表格。
### 5.4 `build_audit_payload(report, context) -> dict`
生成审计载荷。
### 5.5 `build_feishu_summary_payload(report) -> dict`
生成飞书摘要载荷。
## 6. 技术实现
使用技术:
1. dataclass/Pydantic
2. JSONField
3. Audit 服务
4. 页面展示 schema
## 7. 异常处理
1. 字段池为空:输出空报告并提示无可用字段。
2. 报告字段缺失:任务失败。
3. 审计写入失败:报告仍返回,但记录系统警告。
4. 飞书摘要构建失败:不影响 Web 报告。
## 8. 测试要点
1. 输出 schema 稳定。
2. 字段池行展示完整。
3. 审计载荷包含字段 schema 版本。
4. 飞书摘要不包含敏感信息。

View File

@@ -0,0 +1,114 @@
# 字段抽取编排Skill 设计
## 1. Skill 定位
`字段抽取编排Skill` 是第三步工作流的总入口 Skill负责组织字段抽取范围确认、字段 schema 加载、规则抽取、表格抽取、长文本归纳、字段标准化、统一字段池写入和报告生成。
英文实现标识建议使用 `FieldExtractionOrchestrateSkill`
本 Skill 不直接完成每一种抽取细节,而是负责执行顺序和结果合并。
## 2. 输入
```python
@dataclass
class FieldExtractionOrchestrateInput:
batch_id: int
scenario_id: str = "registration_field_extraction"
field_schema_id: str = "ivd_registration_fields_v1"
selected_document_ids: list[int] = field(default_factory=list)
target_field_keys: list[str] = field(default_factory=list)
enable_llm_fallback: bool = True
enable_rag_context: bool = True
```
## 3. 输出
```python
@dataclass
class FieldExtractionOrchestrateOutput:
report_type: str
batch_id: int
field_schema_id: str
summary: dict
field_pool_items: list[dict]
manual_review_fields: list[dict]
evidence_refs: list[dict]
audit_id: int | None = None
```
## 4. 依赖 Skill
1. `字段抽取范围确认Skill`
2. `字段Schema加载Skill`
3. `规则字段抽取Skill`
4. `表格字段抽取Skill`
5. `长文本字段归纳Skill`
6. `字段标准化Skill`
7. `统一字段池写入Skill`
8. `字段抽取报告生成Skill`
## 5. 核心方法
### 5.1 `run(input) -> FieldExtractionOrchestrateOutput`
主入口方法。
执行顺序:
1. 加载执行上下文。
2. 调用 `字段抽取范围确认Skill`
3. 调用 `字段Schema加载Skill`
4. 调用 `规则字段抽取Skill`
5. 调用 `表格字段抽取Skill`
6. 按需调用 `长文本字段归纳Skill`
7. 调用 `字段标准化Skill`
8. 调用 `统一字段池写入Skill`
9. 调用 `字段抽取报告生成Skill`
10. 写入审计记录。
### 5.2 `load_execution_context(input) -> FieldExtractionContext`
加载批次、文档、完整性检查报告和已有字段池状态。
### 5.3 `merge_field_candidates(*candidate_groups) -> list[FieldCandidate]`
合并规则抽取、表格抽取和长文本归纳结果。
### 5.4 `filter_target_fields(schema, target_field_keys) -> list[FieldDefinition]`
筛选本次需要抽取的字段。
## 6. 技术实现
使用技术:
1. Python dataclass 或 Pydantic
2. Tool Registry
3. LLM Provider
4. Django 服务层
5. Audit 服务
建议注册名:
```python
tool_registry.register(
name="field_extraction_orchestrate",
handler=FieldExtractionOrchestrateSkill().run,
)
```
## 7. 异常处理
1. 无可抽取文档:返回业务提示。
2. 字段 schema 不存在:任务失败并写审计。
3. LLM 不可用:跳过 LLM保留规则和表格结果。
4. 所有抽取方式均失败:返回待人工复核报告。
## 8. 测试要点
1. 能按顺序调用依赖 Skill。
2. LLM 关闭时仍可执行规则抽取。
3. 无文档时返回清晰错误。
4. 输出报告结构稳定。

View File

@@ -0,0 +1,79 @@
# 字段抽取范围确认Skill 设计
## 1. Skill 定位
`字段抽取范围确认Skill` 负责确定本次字段抽取使用哪些文档,以及每个目标字段优先从哪些文档角色中抽取。
英文实现标识建议使用 `FieldExtractionScopeResolveSkill`
## 2. 输入
```python
@dataclass
class FieldExtractionScopeResolveInput:
documents: list[DocumentFact]
selected_document_ids: list[int]
target_field_keys: list[str]
field_source_priority: dict
```
## 3. 输出
```python
@dataclass
class FieldExtractionScopeResolveOutput:
extractable_documents: list[DocumentFact]
excluded_documents: list[dict]
field_document_plan: dict[str, list[DocumentFact]]
warnings: list[dict]
```
## 4. 文档筛选规则
参与抽取的文档必须满足:
1. `source_role = submission`
2. 文档处理状态可用。
3. 文档存在文本或表格结构。
4. 文档角色属于字段来源配置。
排除:
1. 法规依据资料。
2. 不支持文件。
3. 解析失败且无可用文本。
4. 用户未选择且不在默认来源范围内的文档。
## 5. 核心方法
### 5.1 `run(input) -> FieldExtractionScopeResolveOutput`
主入口方法。
### 5.2 `filter_extractable_documents(documents) -> list[DocumentFact]`
筛选可抽取文档。
### 5.3 `build_field_document_plan(fields, documents, priority) -> dict`
为每个字段构建候选文档顺序。
### 5.4 `collect_scope_warnings(documents) -> list[dict]`
收集待复核、解析失败、文本缺失等警告。
## 6. 技术实现
使用技术:
1. 文档角色枚举
2. YAML 来源优先级
3. Python 排序规则
## 7. 测试要点
1. 法规资料被排除。
2. 申请表、说明书、产品列表被纳入。
3. 用户选择文档时只使用选中范围。
4. 待复核文档会降低抽取可信度。

View File

@@ -0,0 +1,83 @@
# 字段标准化Skill 设计
## 1. Skill 定位
`字段标准化Skill` 负责对字段候选值进行清洗、标准化、置信度计算和冲突候选标记。
英文实现标识建议使用 `FieldNormalizeSkill`
## 2. 输入
```python
@dataclass
class FieldNormalizeInput:
candidates: list[FieldCandidate]
field_definitions: list[FieldDefinition]
source_priority: dict
```
## 3. 输出
```python
@dataclass
class FieldNormalizeOutput:
normalized_candidates: list[NormalizedFieldCandidate]
conflict_candidates: list[dict]
manual_review_candidates: list[dict]
```
## 4. 标准化规则
1. 去除首尾空白。
2. 合并连续空白。
3. 全角半角标准化。
4. 中文标点标准化。
5. 日期格式标准化。
6. 单位格式标准化。
7. 空值和异常长值标记待复核。
## 5. 核心方法
### 5.1 `run(input) -> FieldNormalizeOutput`
主入口方法。
### 5.2 `normalize_text_value(value) -> str`
文本清洗。
### 5.3 `normalize_date_value(value) -> str`
日期标准化。
### 5.4 `calculate_confidence(candidate, field_definition) -> str`
计算置信度。
### 5.5 `detect_conflict_candidates(candidates) -> list[dict]`
检测同字段多候选值差异。
## 6. 技术实现
使用技术:
1. Python 字符串处理
2. 正则表达式
3. 日期解析
4. 字段类型规则
## 7. 异常处理
1. 值为空:标记待复核。
2. 值过长:标记待复核。
3. 日期无法解析:保留原值并标记低可信。
4. 多候选不一致:标记 `conflict_candidate`
## 8. 测试要点
1. 空白和标点标准化正确。
2. 日期标准化正确。
3. 多候选冲突可识别。
4. 低可信候选进入待复核。

View File

@@ -0,0 +1,115 @@
# 完整性报告生成Skill 设计
## 1. Skill 定位
`完整性报告生成Skill` 负责把完整性检查链路中的规则判定结果、风险映射结果和法规证据组装成稳定的 `registration_completeness_report`
英文实现标识建议使用 `CompletenessReportBuildSkill`
本 Skill 不重新判定缺失,不重新检索证据,只负责报告结构、展示摘要和审计载荷生成。
## 2. 输入
```python
@dataclass
class CompletenessReportBuildInput:
execution_context: CompletenessExecutionContext
item_results: list[CompletenessItemResult]
evidence_refs: list[EvidenceRef]
pass_status: str
highest_risk_level: str
```
## 3. 输出
```python
@dataclass
class CompletenessReportBuildOutput:
report: dict
display_sections: list[dict]
audit_payload: dict
feishu_summary_payload: dict
```
## 4. 报告结构
报告必须包含:
1. `report_type`
2. `batch_id`
3. `workflow_type`
4. `rule_package_id`
5. `rule_version`
6. `chapter_scope`
7. `summary`
8. `matched_items`
9. `missing_items`
10. `misplaced_items`
11. `manual_review_items`
12. `evidence_refs`
13. `suggestions`
## 5. 核心方法
### 5.1 `run(input) -> CompletenessReportBuildOutput`
主入口方法。
### 5.2 `build_summary(item_results) -> dict`
汇总:
1. 要求项数量。
2. 已提供数量。
3. 缺失数量。
4. 疑似提供数量。
5. 错放数量。
6. 待复核数量。
7. 最高风险等级。
8. 是否通过。
### 5.3 `split_item_results(item_results) -> dict`
按状态拆分明细。
### 5.4 `attach_evidence(item_results, evidence_refs) -> list[dict]`
把法规证据挂到对应要求项。
### 5.5 `build_display_sections(report) -> list[dict]`
生成页面展示区块。
### 5.6 `build_audit_payload(report, context) -> dict`
生成审计载荷。
### 5.7 `build_feishu_summary_payload(report) -> dict`
生成飞书摘要载荷,供后续飞书通知步骤复用。
## 6. 技术实现
使用技术:
1. Pydantic/dataclass
2. JSONField
3. Django Audit 服务层
4. 结构化消息模板
## 7. 异常处理
1. 报告字段缺失:构建失败并写入失败审计。
2. 证据为空:正常输出,标记证据缺失。
3. 明细为空:输出空检查结果。
4. 风险等级缺失:按 `low` 处理,并记录规则警告。
## 8. 测试要点
1. 输出 schema 字段稳定。
2. 缺失项进入 `missing_items`
3. 错放项进入 `misplaced_items`
4. 待复核项进入 `manual_review_items`
5. 审计载荷包含规则版本和输入范围。
6. 飞书摘要载荷不包含敏感信息。

View File

@@ -0,0 +1,77 @@
# 审核范围确认Skill 设计
## 1. Skill 定位
`审核范围确认Skill` 负责确认本次一致性核查的文档范围,避免把不同项目、不同产品或法规依据资料混入同一轮比对。
英文实现标识建议使用 `ReviewScopeResolveSkill`
## 2. 输入
```python
@dataclass
class ReviewScopeResolveInput:
batch_id: int
documents: list[DocumentFact]
selected_document_ids: list[int] = field(default_factory=list)
```
## 3. 输出
```python
@dataclass
class ReviewScopeResolveOutput:
scope_documents: list[DocumentFact]
excluded_documents: list[dict]
scope_warnings: list[dict]
scope_status: str
```
## 4. 范围规则
1. 必须同一批次。
2. 必须是业务申报资料。
3. 法规资料排除。
4. 处理失败资料排除。
5. 待人工复核资料保留但标记警告。
6. 用户选中范围优先。
## 5. 核心方法
### 5.1 `run(input) -> ReviewScopeResolveOutput`
主入口方法。
### 5.2 `filter_by_selected_documents(documents, ids) -> list[DocumentFact]`
根据用户选择过滤文档。
### 5.3 `exclude_non_submission_documents(documents) -> list[dict]`
排除法规资料和非业务资料。
### 5.4 `build_scope_warnings(documents) -> list[dict]`
生成范围警告。
## 6. 技术实现
使用技术:
1. 文档主数据
2. Django ORM
3. 资料来源角色枚举
## 7. 异常处理
1. 范围为空:任务不可执行。
2. 只有单个文档:允许执行,但只能输出单来源结果。
3. 全部文档待复核:输出低可信警告。
## 8. 测试要点
1. 选中文档范围生效。
2. 法规资料被排除。
3. 待复核文档输出警告。
4. 空范围返回错误。

View File

@@ -0,0 +1,65 @@
# 导出版式校验Skill 设计
## 1. Skill 定位
`导出版式校验Skill` 负责检查回填后的 Word 文件是否仍有未替换占位符、必填字段是否落位、基础版式是否完整。
英文实现标识建议使用 `ExportLayoutCheckSkill`
## 2. 输入
```python
@dataclass
class ExportLayoutCheckInput:
output_file_path: Path
template_mappings: list[dict]
```
## 3. 输出
```python
@dataclass
class ExportLayoutCheckOutput:
layout_check_status: str
unfilled_placeholders: list[str]
layout_warnings: list[dict]
```
## 4. 核心方法
### 4.1 `run(input) -> ExportLayoutCheckOutput`
主入口方法。
### 4.2 `detect_unfilled_placeholders(file) -> list[str]`
检查残留占位符。
### 4.3 `validate_required_fields(file, mappings) -> list[dict]`
校验必填字段。
### 4.4 `validate_basic_layout(file) -> list[dict]`
校验基础版式元素。
## 5. 技术实现
使用技术:
1. `python-docx`
2. 可选 LibreOffice 转 PDF
3. 占位符扫描规则
## 6. 异常处理
1. 文件不存在:校验失败。
2. 残留占位符:标记待复核。
3. 版式元素缺失:标记警告。
## 7. 测试要点
1. 残留占位符可识别。
2. 必填字段缺失可识别。
3. 正常文档通过校验。

View File

@@ -0,0 +1,75 @@
# 导出记录生成Skill 设计
## 1. Skill 定位
`导出记录生成Skill` 负责保存导出文件元数据、生成下载入口、构建导出报告并写入审计。
英文实现标识建议使用 `ExportRecordBuildSkill`
## 2. 输入
```python
@dataclass
class ExportRecordBuildInput:
batch_id: int
output_file_path: Path | None
export_mode: str
layout_check_result: dict
filled_fields: list[dict]
blocked_fields: list[dict]
```
## 3. 输出
```python
@dataclass
class ExportRecordBuildOutput:
report: dict
output_file: dict | None
audit_payload: dict
```
## 4. 核心方法
### 4.1 `run(input) -> ExportRecordBuildOutput`
主入口方法。
### 4.2 `create_export_file_record(file_path) -> ExportedDocument`
创建导出文件记录。
### 4.3 `build_download_url(export_file) -> str`
生成下载 URL。
### 4.4 `build_report(input, output_file) -> dict`
生成导出报告。
### 4.5 `build_audit_payload(report) -> dict`
生成审计载荷。
## 5. 技术实现
使用技术:
1. Django ORM
2. Django Storage
3. JSONField
4. Audit 服务
## 6. 异常处理
1. 文件不存在:只生成拦截报告。
2. 下载链接生成失败:报告标记异常。
3. 审计失败:记录系统警告。
## 7. 测试要点
1. 文件记录创建成功。
2. 下载链接生成成功。
3. 拦截模式不创建文件。
4. 审计载荷完整。

View File

@@ -0,0 +1,74 @@
# 强一致规则加载Skill 设计
## 1. Skill 定位
`强一致规则加载Skill` 负责加载字段一致性核查规则,定义哪些字段必须完全一致、如何比对、冲突风险等级和是否参与回填拦截。
英文实现标识建议使用 `StrictConsistencyRuleLoadSkill`
## 2. 输入
```python
@dataclass
class StrictConsistencyRuleLoadInput:
field_rule_id: str
target_field_keys: list[str] = field(default_factory=list)
strict_mode: bool = True
```
## 3. 输出
```python
@dataclass
class StrictConsistencyRuleLoadOutput:
field_rule_id: str
version: str
rules: list[ConsistencyFieldRule]
validation_warnings: list[dict]
```
## 4. 核心方法
### 4.1 `run(input) -> StrictConsistencyRuleLoadOutput`
主入口方法。
### 4.2 `load_rule_file(field_rule_id) -> dict`
读取 YAML 规则。
### 4.3 `validate_rules(raw_rules) -> RuleValidationResult`
校验规则字段。
### 4.4 `select_target_rules(rules, target_field_keys) -> list[ConsistencyFieldRule]`
筛选本次目标字段规则。
## 5. 技术实现
使用技术:
1. YAML
2. Pydantic
3. Django cache
建议路径:
```text
configs/registration/consistency/ivd_strict_consistency_v1.yaml
```
## 6. 异常处理
1. 规则文件不存在:任务失败。
2. 目标字段无规则:标记不检查。
3. 风险等级缺失:默认中风险并记录警告。
## 7. 测试要点
1. 规则加载成功。
2. 目标字段筛选正确。
3. 缺少规则时报错。
4. `strict_mode` 生效。

View File

@@ -0,0 +1,71 @@
# 整改建议生成Skill 设计
## 1. Skill 定位
`整改建议生成Skill` 负责根据风险项生成处理建议、整改优先级和责任角色。
英文实现标识建议使用 `RectificationSuggestionBuildSkill`
## 2. 输入
```python
@dataclass
class RectificationSuggestionBuildInput:
risk_items: list[RiskItem]
suggestion_templates: dict
owner_role_mapping: dict
enable_llm_summary: bool = True
```
## 3. 输出
```python
@dataclass
class RectificationSuggestionBuildOutput:
suggestions: list[dict]
owner_notifications: list[dict]
summary_text: str
```
## 4. 核心方法
### 4.1 `run(input) -> RectificationSuggestionBuildOutput`
主入口方法。
### 4.2 `build_suggestion(risk_item) -> dict`
生成单项建议。
### 4.3 `resolve_owner_role(risk_item) -> str`
映射责任角色。
### 4.4 `sort_by_priority(suggestions) -> list[dict]`
按风险等级和业务优先级排序。
### 4.5 `build_summary_text(suggestions) -> str`
生成摘要,可选使用 LLM Provider。
## 5. 技术实现
使用技术:
1. 建议模板
2. 责任角色映射
3. 可选 LLM Provider
## 6. 异常处理
1. 模板缺失:使用默认建议。
2. 责任角色缺失:使用默认负责人。
3. LLM 不可用:使用本地摘要。
## 7. 测试要点
1. 高风险建议优先。
2. 责任角色映射正确。
3. LLM 不可用时本地摘要可用。

View File

@@ -0,0 +1,126 @@
# 文档页数统计Skill 设计
## 1. Skill 定位
`文档页数统计Skill` 负责为注册申报资料文件生成页数统计结果。页数是题面明确要求的关键指标,因此本 Skill 必须把页数、统计方法和可信度分开记录。
本 Skill 不负责文档正文抽取,不负责 OCR不负责合规判断。
英文实现标识建议使用 `DocumentPageCountSkill`,用于 Python 类名和 Tool Registry 注册处理器。
## 2. 输入
```python
@dataclass
class DocumentPageCountInput:
document_id: int
file_path: Path
file_type: str
options: dict = field(default_factory=dict)
```
## 3. 输出
```python
@dataclass
class PageCountResult:
document_id: int
page_count: int | None
method: str
confidence: str
status: str
message: str = ""
```
## 4. 页数统计策略
| 文件类型 | 策略 | 可信度 |
|---|---|---|
| `pdf` | 使用 `pypdf``PyMuPDF` 读取页数 | `exact` |
| `docx` | 优先读取 Word 统计信息,必要时转换 PDF 后统计 | `exact` |
| `doc` | 尝试转换后统计,失败则待复核 | `manual_review_required` |
| `txt` | 页数不适用 | `not_applicable` |
| `md` | 页数不适用 | `not_applicable` |
DOCX 是 V1 验收重点,不能用字数、段落数或估算分页代替精确页数。
## 5. 核心方法
### 5.1 `run(input) -> PageCountResult`
根据文件类型路由到具体统计方法。
### 5.2 `count_pdf_pages(path) -> PageCountResult`
推荐使用 `pypdf``PyMuPDF`
失败处理:
1. 文件损坏:`failed`
2. 加密 PDF`manual_review_required`
3. 无法读取:`failed`
### 5.3 `count_docx_pages(path) -> PageCountResult`
DOCX 精确页数建议采用两级策略:
1. 读取文档内部统计属性中的页数。
2. 若统计属性不可用,使用 LibreOffice headless 转 PDF再统计 PDF 页数。
如果两级策略均失败,输出 `manual_review_required`,并在页面突出显示。
### 5.4 `count_doc_pages(path) -> PageCountResult`
DOC 文件首版策略:
1. 尝试用兼容转换工具转 PDF。
2. 转换成功后统计 PDF 页数。
3. 转换失败则标记待人工复核。
### 5.5 `count_text_pages(path) -> PageCountResult`
TXT/MD 首版不做页数强制验收。
返回:
1. `page_count = None`
2. `method = "not_applicable"`
3. `confidence = "not_applicable"`
## 6. 技术实现
建议依赖:
1. `pypdf`
2. `PyMuPDF`
3. `python-docx`
4. LibreOffice headless可作为增强能力
如果 V1 Docker 环境暂不内置 LibreOffice应在配置中显式标注 `DOCX_PAGE_COUNT_STRATEGY`,并保证演示样本能通过已实现策略得到精确页数。
## 7. 状态设计
| 状态 | 含义 |
|---|---|
| `success` | 页数统计成功 |
| `not_applicable` | 文本类文件不适用 |
| `manual_review_required` | 需要人工复核 |
| `failed` | 统计失败 |
## 8. 落库字段
建议写入 `RegistrationDocument`
1. `page_count`
2. `page_count_method`
3. `page_count_confidence`
4. `page_count_status`
5. `processing_message`
## 9. 测试要点
1. PDF 返回精确页数。
2. DOCX 返回精确页数。
3. DOC 无法统计时标记待人工复核。
4. TXT/MD 返回不适用。
5. 损坏文件返回失败状态。

View File

@@ -0,0 +1,67 @@
# 模板字段映射加载Skill 设计
## 1. Skill 定位
`模板字段映射加载Skill` 负责加载 Word 模板占位符与统一字段池字段之间的映射关系。
英文实现标识建议使用 `TemplateFieldMappingLoadSkill`
## 2. 输入
```python
@dataclass
class TemplateFieldMappingLoadInput:
template_id: str
template_file_path: Path
```
## 3. 输出
```python
@dataclass
class TemplateFieldMappingLoadOutput:
template_id: str
mapping_version: str
mappings: list[dict]
missing_placeholders: list[str]
extra_placeholders: list[str]
```
## 4. 核心方法
### 4.1 `run(input) -> TemplateFieldMappingLoadOutput`
主入口方法。
### 4.2 `load_mapping(template_id) -> dict`
读取映射规则。
### 4.3 `scan_placeholders(template_file) -> list[str]`
扫描模板占位符。
### 4.4 `validate_mapping(mapping, placeholders) -> MappingValidationResult`
校验映射完整性。
## 5. 技术实现
使用技术:
1. YAML
2. `python-docx`
3. Pydantic
## 6. 异常处理
1. 映射不存在:任务失败。
2. 必填占位符缺失:任务失败。
3. 模板存在未映射占位符:标记警告。
## 7. 测试要点
1. 映射加载成功。
2. 模板占位符能扫描。
3. 缺失必填映射时报错。

View File

@@ -0,0 +1,65 @@
# 模板选择Skill 设计
## 1. Skill 定位
`模板选择Skill` 负责根据目标输出类型选择可用 Word 模板,并校验模板适用流程和版本状态。
英文实现标识建议使用 `WordTemplateSelectSkill`
## 2. 输入
```python
@dataclass
class WordTemplateSelectInput:
template_id: str
target_output_type: str
workflow_type: str = "registration"
```
## 3. 输出
```python
@dataclass
class WordTemplateSelectOutput:
template_id: str
template_version: str
template_file_path: Path
template_type: str
validation_warnings: list[dict]
```
## 4. 核心方法
### 4.1 `run(input) -> WordTemplateSelectOutput`
主入口方法。
### 4.2 `load_template(template_id) -> WordTemplate`
读取模板记录。
### 4.3 `validate_template(template, workflow_type) -> TemplateValidationResult`
校验模板适用性。
## 5. 技术实现
使用技术:
1. Django ORM
2. Django Storage
3. 模板元数据 YAML
## 6. 异常处理
1. 模板不存在:任务失败。
2. 模板文件丢失:任务失败。
3. 模板未启用:任务失败。
4. 流程不匹配:任务失败。
## 7. 测试要点
1. 能选择启用模板。
2. 禁用模板不可用。
3. 流程不匹配时报错。

View File

@@ -0,0 +1,136 @@
# 法规完整性检查Skill 设计
## 1. Skill 定位
`法规完整性检查Skill` 是第二步工作流的总编排 Skill负责根据资料包目录汇总结果、法规流程类型和本地规则包组织完整性检查链路并输出结构化完整性报告。
英文实现标识建议使用 `RegulationCompletenessCheckSkill`,用于 Python 类名和 Tool Registry 注册处理器。
本 Skill 不直接解析原始文件,不负责字段抽取,不负责综合风险报告。它消费第一步产生的文档事实。
## 2. 输入
```python
@dataclass
class RegulationCompletenessCheckInput:
batch_id: int
scenario_id: str = "registration_completeness_check"
workflow_type: str = "registration"
rule_package_id: str = "nmpa_ivd_registration_v1"
chapter_scope: list[str] = field(default_factory=list)
selected_document_ids: list[int] = field(default_factory=list)
enable_rag_evidence: bool = True
```
## 3. 输出
```python
@dataclass
class RegulationCompletenessCheckOutput:
report_type: str
batch_id: int
rule_package_id: str
rule_version: str
summary: dict
matched_items: list[dict]
missing_items: list[dict]
misplaced_items: list[dict]
manual_review_items: list[dict]
evidence_refs: list[dict]
audit_id: int | None = None
```
## 4. 依赖 Skill
1. `法规流程识别Skill`
2. `法规规则包加载Skill`
3. `资料要求匹配Skill`
4. `缺失错放判定Skill`
5. `法规证据检索Skill`
6. `完整性报告生成Skill`
## 5. 核心方法
### 5.1 `run(input) -> RegulationCompletenessCheckOutput`
主入口方法。
执行顺序:
1. 读取资料包目录汇总。
2. 调用 `法规流程识别Skill`
3. 调用 `法规规则包加载Skill`
4. 按章节范围展开法规要求项。
5. 调用 `资料要求匹配Skill`
6. 调用 `缺失错放判定Skill`
7. 按需调用 `法规证据检索Skill`
8. 调用 `完整性报告生成Skill`
9. 写入审计记录。
10. 返回完整性报告。
### 5.2 `load_execution_context(input) -> CompletenessExecutionContext`
加载执行上下文。
包含:
1. 批次信息。
2. 目录汇总报告。
3. 选中文档范围。
4. 场景配置。
5. 用户输入参数。
### 5.3 `select_document_facts(context) -> list[DocumentFact]`
从目录汇总中筛选参与完整性检查的文档。
如果 `selected_document_ids` 为空,则使用当前批次所有业务申报资料。
### 5.4 `build_requirement_scope(rule_package, chapter_scope) -> list[RequirementItem]`
根据章节点范围展开法规要求项。
V1 默认:
1.`chapter_scope` 时按范围执行。
2.`chapter_scope` 时优先执行 `CH1`
3. 后续支持全六章。
## 6. 技术实现
使用技术:
1. Python dataclass 或 Pydantic
2. Tool Registry
3. Django 服务层调用
4. JSONField 报告快照
5. Audit 服务
建议注册名:
```python
tool_registry.register(
name="regulation_completeness_check",
handler=RegulationCompletenessCheckSkill().run,
)
```
## 7. 异常处理
| 异常 | 处理 |
|---|---|
| 批次不存在 | 返回业务错误并写失败审计 |
| 目录汇总不存在 | 提示先执行资料包导入与目录汇总 |
| 规则包不存在 | 返回任务不可执行 |
| 流程不支持 | 返回流程配置错误 |
| RAG 不可用 | 保留规则判断,证据标记不可用 |
| 所选文档为空 | 返回空范围报告 |
## 8. 测试要点
1. 能基于目录汇总生成完整性报告。
2. 能调用依赖 Skill 并合并结果。
3. 规则包缺失时写入失败审计。
4. RAG 失败不阻断主链路。
5. 输出 schema 稳定。

View File

@@ -0,0 +1,90 @@
# 法规流程识别Skill 设计
## 1. Skill 定位
`法规流程识别Skill` 负责确认当前完整性检查适用哪一类法规流程,避免把注册申报、变更备案、变更注册和延续注册混用。
英文实现标识建议使用 `RegulationWorkflowResolveSkill`
V1 默认只执行 `registration`,但设计上保留扩展位。
## 2. 输入
```python
@dataclass
class RegulationWorkflowResolveInput:
batch_workflow_type: str | None
scenario_workflow_type: str | None
user_workflow_type: str | None
rule_package_supported_workflows: list[str]
```
## 3. 输出
```python
@dataclass
class RegulationWorkflowResolveOutput:
workflow_type: str
confidence: str
source: str
supported: bool
warnings: list[dict]
```
## 4. 识别优先级
1. 用户显式选择。
2. 场景配置。
3. 资料包批次字段。
4. 系统默认值 `registration`
如果多个来源冲突,标记为 `manual_review_required`,并优先使用用户显式选择。
## 5. 核心方法
### 5.1 `run(input) -> RegulationWorkflowResolveOutput`
主入口方法。
### 5.2 `resolve_workflow_type(input) -> str`
按优先级解析流程类型。
### 5.3 `detect_workflow_conflict(input) -> list[dict]`
检测用户选择、场景配置和批次字段是否冲突。
### 5.4 `validate_supported(workflow_type, supported_workflows) -> bool`
校验规则包是否支持当前流程。
## 6. 技术实现
使用技术:
1. 场景 YAML
2. 批次模型字段
3. 规则包元数据
4. Python 枚举
流程枚举:
1. `registration`
2. `change_record`
3. `change_registration`
4. `renewal`
## 7. 异常处理
1. 流程为空:使用 `registration`
2. 流程冲突:标记警告。
3. 流程不支持:阻断完整性检查。
4. 用户输入非法:返回业务化错误。
## 8. 测试要点
1. 默认返回 `registration`
2. 用户显式选择优先。
3. 场景和批次冲突时输出警告。
4. 不支持流程返回 `supported = false`

View File

@@ -0,0 +1,118 @@
# 法规规则包加载Skill 设计
## 1. Skill 定位
`法规规则包加载Skill` 负责加载、校验和展开本地结构化法规规则包,是完整性检查的规则来源入口。
英文实现标识建议使用 `RegulationRulePackageLoadSkill`
本 Skill 不读取用户资料,不做匹配判断,只处理法规规则本身。
## 2. 输入
```python
@dataclass
class RegulationRulePackageLoadInput:
rule_package_id: str
workflow_type: str
chapter_scope: list[str] = field(default_factory=list)
```
## 3. 输出
```python
@dataclass
class RegulationRulePackageLoadOutput:
rule_package_id: str
version: str
workflow_type: str
source_documents: list[dict]
requirements: list[dict]
risk_rules: dict
validation_warnings: list[dict]
```
## 4. 规则包来源
V1 默认来源:
```text
docs/原始材料/关于公布体外诊断试剂注册申报资料要求和批准证明文件格式的公告/
```
结构化规则建议维护在:
```text
configs/registration/rules/
nmpa_ivd_registration_v1.yaml
chapter_catalog.yaml
risk_mapping.yaml
```
## 5. 核心方法
### 5.1 `run(input) -> RegulationRulePackageLoadOutput`
主入口方法。
执行顺序:
1. 根据 `rule_package_id` 定位规则文件。
2. 读取 YAML。
3. 校验规则包元数据。
4. 校验规则包是否支持当前 `workflow_type`
5. 展开章节范围。
6. 返回要求项和风险规则。
### 5.2 `load_yaml_rule_file(path) -> dict`
读取 YAML 文件。
### 5.3 `validate_rule_package(raw_rules) -> RulePackageValidationResult`
校验:
1. 是否有规则包 ID。
2. 是否有版本。
3. 是否有适用流程。
4. 是否有章节。
5. 每个必交项是否有风险等级。
6. 每个要求项是否有稳定 `requirement_id`
### 5.4 `expand_requirements(rule_package, chapter_scope) -> list[RequirementItem]`
展开要求项。
如果 `chapter_scope = ["CH1"]`,只返回第 1 章要求项。
### 5.5 `load_risk_rules(rule_package) -> dict`
读取缺失、错放、待复核对应的风险映射规则。
## 6. 技术实现
使用技术:
1. `PyYAML`
2. Pydantic schema
3. Django cache
4. 文件修改时间检测
## 7. 异常处理
| 异常 | 处理 |
|---|---|
| 规则文件不存在 | 返回 `rule_package_not_found` |
| YAML 格式错误 | 返回 `rule_package_invalid` |
| 流程不匹配 | 返回 `workflow_not_supported` |
| 缺少风险配置 | 规则包校验失败 |
| 章节范围为空 | 使用默认范围 |
## 8. 测试要点
1. 成功加载规则包。
2. 规则包版本正确。
3. `CH1` 范围展开正确。
4. 不支持流程被拒绝。
5. 缺少必填字段时报校验错误。

View File

@@ -0,0 +1,100 @@
# 法规证据检索Skill 设计
## 1. Skill 定位
`法规证据检索Skill` 负责为完整性检查结果补充法规原文证据,用于解释、页面展示和审计留痕。
英文实现标识建议使用 `RegulationEvidenceRetrieveSkill`
本 Skill 不改变完整性判定结论。规则链路已经判定的缺失、错放和风险等级不能被 RAG 检索结果反向覆盖。
## 2. 输入
```python
@dataclass
class RegulationEvidenceRetrieveInput:
requirement_results: list[CompletenessItemResult]
rule_package_id: str
workflow_type: str
max_results_per_item: int = 3
```
## 3. 输出
```python
@dataclass
class RegulationEvidenceRetrieveOutput:
evidence_refs: list[EvidenceRef]
unavailable_items: list[dict]
warnings: list[dict]
```
`EvidenceRef` 字段:
1. `requirement_id`
2. `source_document`
3. `source_type`
4. `chapter_code`
5. `section_title`
6. `snippet`
7. `page_no`
8. `retrieval_score`
9. `metadata`
## 4. 检索范围
只检索法规资料,不检索业务申报资料。
metadata 过滤:
1. `source_role = regulation`
2. `workflow_type = registration`
3. `rule_package_id = nmpa_ivd_registration_v1`
4. `requirement_id``chapter_code`
## 5. 核心方法
### 5.1 `run(input) -> RegulationEvidenceRetrieveOutput`
主入口方法。
### 5.2 `build_evidence_query(item_result) -> str`
根据要求项名称、章节点和规则配置生成检索 query。
### 5.3 `retrieve_from_vector_store(query, metadata_filter) -> list[EvidenceRef]`
优先使用 Chroma。
### 5.4 `retrieve_from_fallback_index(query, metadata_filter) -> list[EvidenceRef]`
当 Chroma 不可用时,使用本地切片或关键词 fallback。
### 5.5 `normalize_evidence_results(results) -> list[EvidenceRef]`
统一证据格式。
## 6. 技术实现
使用技术:
1. Chroma
2. 本地 fallback 检索
3. 文本切片 metadata
4. Python 关键词匹配
## 7. 异常处理
1. 向量库不可用:降级 fallback。
2. fallback 也不可用:返回证据不可用警告。
3. 找不到证据:不阻断完整性报告。
4. 命中业务资料:过滤掉,避免把申报资料当法规依据。
## 8. 测试要点
1. 能根据要求项生成查询。
2. Chroma 可用时返回证据。
3. Chroma 不可用时 fallback 生效。
4. 证据检索失败不改变缺失结论。
5. 非法规资料被过滤。

View File

@@ -0,0 +1,73 @@
# 混档风险识别Skill 设计
## 1. Skill 定位
`混档风险识别Skill` 负责基于一致性核查结果识别疑似跨产品、跨批次或错误资料混入风险。
英文实现标识建议使用 `MixedPackageRiskDetectSkill`
## 2. 输入
```python
@dataclass
class MixedPackageRiskDetectInput:
compare_results: list[FieldCompareResult]
scope_documents: list[DocumentFact]
```
## 3. 输出
```python
@dataclass
class MixedPackageRiskDetectOutput:
mixed_package_warnings: list[dict]
highest_risk_level: str
```
## 4. 识别规则
1. 产品名称冲突:高风险。
2. 检测靶标冲突:高风险。
3. 产品名称和检测靶标指向不同产品:高风险。
4. 申请人名称冲突:高风险或待复核。
5. 相同文档角色出现多份不同产品文件:中风险。
## 5. 核心方法
### 5.1 `run(input) -> MixedPackageRiskDetectOutput`
主入口方法。
### 5.2 `detect_product_name_conflict(results) -> dict | None`
识别产品名称冲突。
### 5.3 `detect_target_conflict(results) -> dict | None`
识别检测靶标冲突。
### 5.4 `classify_warning_risk(warning) -> str`
映射风险等级。
## 6. 技术实现
使用技术:
1. 字段比对结果
2. 文档角色规则
3. 风险映射 YAML
## 7. 异常处理
1. 缺少产品名称字段:不输出混档结论,标记待复核。
2. 只有单来源:不输出混档结论。
3. 字段已冲突但来源不明:标记待人工确认。
## 8. 测试要点
1. 产品名称冲突输出高风险。
2. 检测靶标冲突输出高风险。
3. 单来源不输出混档风险。
4. 缺少核心字段时输出待复核。

View File

@@ -0,0 +1,185 @@
# 目录汇总Skill 设计
## 1. Skill 定位
`目录汇总Skill` 负责把一个资料包批次中的文档主数据聚合为目录汇总报告。它是第一步“资料包导入与目录汇总”的最终输出 Skill。
本 Skill 不重新扫描文件,不重新统计页数,只消费已经落库或已处理的文档事实。
英文实现标识建议使用 `DirectorySummarySkill`,用于 Python 类名和 Tool Registry 注册处理器。
## 2. 输入
```python
@dataclass
class DirectorySummaryInput:
batch_id: int
include_unsupported: bool = true
include_warnings: bool = true
```
## 3. 输出
```python
@dataclass
class RegistrationOverviewReport:
batch_id: int
batch_no: str
workflow_type: str
source_role: str
file_count: int
supported_file_count: int
unsupported_file_count: int
failed_file_count: int
manual_review_count: int
total_page_count: int
page_count_status: str
chapter_summary: list[dict]
documents: list[dict]
warnings: list[dict]
```
## 4. 核心方法
### 4.1 `run(input) -> RegistrationOverviewReport`
主入口方法。
执行顺序:
1. 读取批次信息。
2. 读取该批次所有文档记录。
3. 计算总文件数。
4. 计算支持文件、不支持文件、失败文件和待复核文件数量。
5. 汇总页数。
6. 按章节点聚合。
7. 生成文档明细。
8. 生成导入警告。
9. 返回结构化报告。
### 4.2 `summarize_counts(documents) -> dict`
计算:
1. `file_count`
2. `supported_file_count`
3. `unsupported_file_count`
4. `failed_file_count`
5. `manual_review_count`
### 4.3 `summarize_pages(documents) -> dict`
计算:
1. `total_page_count`
2. `exact_page_count`
3. `manual_review_page_count`
4. `page_count_status`
`page_count_status` 规则:
1. 全部精确:`exact`
2. 部分待复核:`partial_review_required`
3. 全部无法统计:`manual_review_required`
### 4.4 `summarize_chapters(documents) -> list[dict]`
`chapter_code` 聚合:
1. 文件数。
2. 页数。
3. 待复核数。
4. 主要资料角色。
无章节点的文件归入 `unclassified`
### 4.5 `build_document_rows(documents) -> list[dict]`
生成页面明细。
字段:
1. `document_id`
2. `relative_path`
3. `original_filename`
4. `file_type`
5. `page_count`
6. `page_count_confidence`
7. `chapter_code`
8. `chapter_name`
9. `document_role`
10. `processing_status`
11. `needs_manual_review`
### 4.6 `build_warnings(documents) -> list[dict]`
生成业务化提示。
提示类型:
1. `unsupported_file`
2. `page_count_review_required`
3. `chapter_unclassified`
4. `file_type_mismatch`
5. `duplicate_file`
6. `archive_extract_warning`
## 5. 技术实现
使用技术:
1. Django ORM 聚合查询
2. Python dataclass 或 Pydantic
3. JSONField 存储报告快照
建议注册名:
```python
tool_registry.register(
name="build_directory_summary",
handler=DirectorySummarySkill().run,
)
```
## 6. 报告存储
建议将目录汇总报告快照写入批次模型或独立模型:
```python
class SubmissionBatchSummary(models.Model):
batch = models.OneToOneField(SubmissionBatch, on_delete=models.CASCADE)
report_type = models.CharField(max_length=64, default="registration_overview_report")
report_json = models.JSONField(default=dict)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
```
## 7. 与页面接口
Documents 页面直接消费 `RegistrationOverviewReport`
1. 顶部显示统计卡片。
2. 中部显示按章节点聚合表。
3. 底部显示文件明细。
4. 侧边显示待人工复核提示。
## 8. 与后续 Agent Core 接口
法规完整性核查从本报告读取:
1. `batch_id`
2. `documents`
3. `chapter_summary`
4. `manual_review_count`
5. `warnings`
如果 `manual_review_count > 0`,后续完整性核查仍可执行,但最终报告应标记“存在资料处理不确定性”。
## 9. 测试要点
1. 空批次返回合理空报告。
2. 多章节点文件能正确聚合。
3. 页数合计只统计有明确页数的文档。
4. 待复核文件数量正确。
5. 不支持文件可选展示。
6. 输出 schema 字段稳定。

View File

@@ -0,0 +1,158 @@
# 章节点识别Skill 设计
## 1. Skill 定位
`章节点识别Skill` 负责对已登记文档进行章节点和资料角色的初步识别,为目录汇总和后续法规完整性核查提供结构化字段。
本 Skill 使用规则优先,不依赖 LLM。后续可以在人工复核或复杂标题解析中引入模型辅助但不作为 V1 必需能力。
英文实现标识建议使用 `ChapterClassificationSkill`,用于 Python 类名和 Tool Registry 注册处理器。
## 2. 输入
```python
@dataclass
class ChapterClassificationInput:
document_id: int
original_filename: str
relative_path: str
file_type: str
title_text: str | None = None
manual_hint: dict = field(default_factory=dict)
```
## 3. 输出
```python
@dataclass
class ChapterClassificationResult:
document_id: int
chapter_code: str | None
chapter_name: str | None
document_role: str | None
declared_document_name: str | None
confidence: str
status: str
evidence: list[dict]
```
## 4. 识别规则
### 4.1 章节点编码识别
从文件名和相对路径中识别:
1. `CH1.2`
2. `CH1.4`
3. `CH1.5`
4. `CH1.9`
5. `CH1.11.1`
6. `CH1.11.5`
7. `CH1.11.6`
正则示例:
```python
r"CH\s*(\d+(?:\.\d+)*)"
```
### 4.2 章节名称识别
从相对路径中识别:
1. `第1章 监管信息`
2. `第2章 综述资料`
3. `第3章 非临床资料`
4. `第4章 临床评价资料`
5. `第5章 产品说明书和标签样稿`
6. `第6章 质量管理体系文件`
### 4.3 文档角色识别
| 关键词 | document_role |
|---|---|
| `监管信息目录` | `regulatory_information_catalog` |
| `申请表` | `application_form` |
| `产品列表` | `product_list` |
| `符合标准的清单` | `standard_compliance_list` |
| `真实性声明` | `authenticity_statement` |
| `符合性声明` | `conformity_statement` |
| `沟通的说明` | `pre_submission_communication` |
| `说明书` | `product_instruction` |
## 5. 核心方法
### 5.1 `run(input) -> ChapterClassificationResult`
主入口方法。
执行顺序:
1. 从相对路径识别章名称。
2. 从文件名识别章节点编码。
3. 从文件名识别文档角色。
4. 如有标题文本,则用标题补充识别。
5. 计算置信度。
6. 返回识别结果。
### 5.2 `extract_chapter_code(text) -> str | None`
从路径或文件名提取 `CHx.x`
### 5.3 `extract_chapter_name(relative_path) -> str | None`
从目录层级识别章节名称。
### 5.4 `detect_document_role(text) -> str | None`
基于关键词和规则表识别文档角色。
### 5.5 `calculate_confidence(matches) -> str`
置信度规则:
1. 路径、文件名和标题一致:`high`
2. 文件名命中但路径缺失:`medium`
3. 只有关键词命中:`low`
4. 无法识别:`manual_review_required`
## 6. 技术实现
使用技术:
1. `re`
2. YAML 规则表
3. 可选 `python-docx` 首页标题抽取
4. Django 管理后台人工修正
建议规则文件:
```text
configs/registration/chapter_classification.yaml
```
## 7. 落库字段
建议写入 `RegistrationDocument`
1. `chapter_code`
2. `chapter_name`
3. `document_role`
4. `declared_document_name`
5. `classification_confidence`
6. `classification_status`
7. `needs_manual_review`
## 8. 异常处理
1. 文件名无章节点:尝试路径识别。
2. 路径与文件名冲突:标记待人工复核。
3. 识别为法规资料但批次为业务资料:标记潜在混入风险。
4. 同一文件命中多个角色:保留最高优先级角色,记录警告。
## 9. 测试要点
1. `CH1.4 申请表.docx` 识别为 `CH1.4``application_form`
2. `第1章 监管信息/CH1.2 监管信息目录.docx` 识别章节和目录角色。
3. 无章节点文件标记待人工复核。
4. 路径与文件名冲突时输出警告。

View File

@@ -0,0 +1,83 @@
# 统一字段池写入Skill 设计
## 1. Skill 定位
`统一字段池写入Skill` 负责将标准化后的字段候选写入统一字段池,并为每个字段选择推荐值、保留候选值和来源证据。
英文实现标识建议使用 `UnifiedFieldPoolWriteSkill`
## 2. 输入
```python
@dataclass
class UnifiedFieldPoolWriteInput:
batch_id: int
normalized_candidates: list[NormalizedFieldCandidate]
field_definitions: list[FieldDefinition]
source_priority: dict
```
## 3. 输出
```python
@dataclass
class UnifiedFieldPoolWriteOutput:
field_pool_items: list[FieldPoolItem]
candidate_records: list[dict]
manual_review_fields: list[dict]
write_status: str
```
## 4. 推荐值选择规则
1. 优先选择高置信候选。
2. 同置信度时按来源优先级选择。
3. 来源优先级一致时选择规则抽取结果。
4. 多候选值明显不同则标记 `conflict_candidate`
5. 待人工复核字段不作为无条件回填值。
## 5. 核心方法
### 5.1 `run(input) -> UnifiedFieldPoolWriteOutput`
主入口方法。
### 5.2 `group_candidates_by_field(candidates) -> dict`
`field_key` 分组。
### 5.3 `select_recommended_value(field_key, candidates, priority) -> FieldPoolItem`
选择推荐值。
### 5.4 `persist_field_pool_item(item) -> RegistrationFieldPoolItem`
写入字段池。
### 5.5 `persist_field_candidates(item, candidates) -> None`
保留所有候选值。
## 6. 技术实现
使用技术:
1. Django ORM
2. JSONField
3. 批量写入
4. 唯一约束:`batch + field_key`
## 7. 异常处理
1. 没有候选值:写入空字段并标记待复核。
2. 数据库写入失败:任务失败并写审计。
3. 字段重复写入:更新字段池版本或覆盖当前批次结果。
4. 候选冲突:保留候选并标记冲突候选。
## 8. 测试要点
1. 高置信候选被选为推荐值。
2. 来源优先级生效。
3. 冲突候选被保留。
4. 可回填字段标记正确。

View File

@@ -0,0 +1,118 @@
# 缺失错放判定Skill 设计
## 1. Skill 定位
`缺失错放判定Skill` 负责根据法规要求项和资料匹配结果,判定每个要求项的完整性状态,并映射完整性维度的风险等级和基础处理建议。
英文实现标识建议使用 `MissingMisplacementEvaluateSkill`
本 Skill 是完整性检查中真正产生“缺失、错放、待复核”结论的规则执行单元。
## 2. 输入
```python
@dataclass
class MissingMisplacementEvaluateInput:
requirements: list[RequirementItem]
matches: list[RequirementDocumentMatch]
document_uncertainties: list[DocumentUncertainty]
risk_rules: dict
```
## 3. 输出
```python
@dataclass
class MissingMisplacementEvaluateOutput:
item_results: list[CompletenessItemResult]
missing_items: list[dict]
misplaced_items: list[dict]
suspected_items: list[dict]
manual_review_items: list[dict]
pass_status: str
highest_risk_level: str
```
## 4. 判定规则
| 条件 | 状态 |
|---|---|
| 必交要求项有高置信匹配 | `provided` |
| 必交要求项没有匹配 | `missing` |
| 有低置信匹配 | `suspected` |
| 文档角色匹配但章节点不匹配 | `misplaced` |
| 命中文档待人工复核 | `manual_review_required` |
| 当前流程不适用 | `not_applicable` |
## 5. 风险映射
风险来源:
1. 规则项自身的 `risk_level_if_missing`
2. 错放风险映射。
3. 待复核不确定性。
4. 资料处理失败状态。
准入结论:
1. 存在高风险缺失:`failed`
2. 存在中风险缺失或错放:`review_required`
3. 只有低风险或待复核:`conditional_pass`
4. 全部已提供:`passed`
## 6. 核心方法
### 6.1 `run(input) -> MissingMisplacementEvaluateOutput`
主入口方法。
### 6.2 `evaluate_requirement_status(requirement, matches) -> CompletenessItemResult`
对单个要求项判定状态。
### 6.3 `detect_misplacement(requirement, matches) -> bool`
判断文档是否存在但归错章节点。
### 6.4 `detect_suspected_provided(requirement, matches) -> bool`
判断是否疑似提供。
### 6.5 `map_risk_level(requirement, status, risk_rules) -> str`
映射风险等级。
### 6.6 `build_suggestion(requirement, status, risk_level) -> str`
生成基础处理建议。
建议示例:
1. `补充 CH1.4 申请表,并由注册申报负责人复核。`
2. `核对该文件是否应归入 CH1.5 产品列表。`
3. `当前资料疑似提供但命名不规范,建议人工确认后修正章节点。`
## 7. 技术实现
使用技术:
1. Python 规则引擎
2. YAML 风险映射
3. Pydantic/dataclass
4. 本地建议模板
## 8. 异常处理
1. 要求项缺少风险配置:默认中风险,并记录规则警告。
2. 匹配结果为空:按必交状态判缺失或不适用。
3. 匹配冲突:标记待人工复核。
4. 文档处理失败:不作为已提供,进入待复核。
## 9. 测试要点
1. 必交项无匹配时判缺失。
2. 高风险缺失导致 `failed`
3. 低置信匹配进入 `suspected`
4. 章节点不一致进入 `misplaced`
5. 待复核文档不会直接判为通过。

View File

@@ -0,0 +1,81 @@
# 表格字段抽取Skill 设计
## 1. Skill 定位
`表格字段抽取Skill` 负责从申请表、产品列表、标准清单等表格结构中抽取字段候选值。
英文实现标识建议使用 `TableFieldExtractSkill`
## 2. 输入
```python
@dataclass
class TableFieldExtractInput:
documents: list[DocumentContent]
field_definitions: list[FieldDefinition]
```
## 3. 输出
```python
@dataclass
class TableFieldExtractOutput:
candidates: list[FieldCandidate]
failed_tables: list[dict]
tool_calls: list[dict]
```
## 4. 适用字段
1. 产品名称。
2. 包装规格。
3. 申请人名称。
4. 分类编码。
5. 生产地址。
6. 标准清单。
## 5. 核心方法
### 5.1 `run(input) -> TableFieldExtractOutput`
主入口方法。
### 5.2 `normalize_table(table) -> NormalizedTable`
标准化表头、空单元格和合并单元格。
### 5.3 `match_table_header(table, field_definition) -> TableMatch | None`
匹配表头。
### 5.4 `extract_cell_value(table, match) -> FieldCandidate`
抽取单元格值。
### 5.5 `build_table_source_location(table_index, row_index, col_index) -> SourceLocation`
记录表格来源位置。
## 6. 技术实现
使用技术:
1. `python-docx`
2. `pdfplumber`
3. 表头关键词映射
4. 合并单元格兼容处理
## 7. 异常处理
1. 无表格:跳过。
2. 表头无法识别:记录待复核。
3. 合并单元格解析失败:记录表格失败。
4. 多行多值:保留所有候选。
## 8. 测试要点
1. 能从申请表抽取产品名称。
2. 能从产品列表抽取包装规格。
3. 能记录表格坐标。
4. 表格解析失败不影响规则抽取。

View File

@@ -0,0 +1,78 @@
# 规则字段抽取Skill 设计
## 1. Skill 定位
`规则字段抽取Skill` 负责从标题、段落和固定标签中抽取字段候选值,适合处理格式稳定、标签明确的注册申报字段。
英文实现标识建议使用 `RuleFieldExtractSkill`
## 2. 输入
```python
@dataclass
class RuleFieldExtractInput:
documents: list[DocumentContent]
field_definitions: list[FieldDefinition]
```
## 3. 输出
```python
@dataclass
class RuleFieldExtractOutput:
candidates: list[FieldCandidate]
failed_fields: list[dict]
tool_calls: list[dict]
```
## 4. 抽取方式
1. 标题后取值。
2. 标签后取值。
3. 固定段落规则。
4. 正则匹配。
## 5. 核心方法
### 5.1 `run(input) -> RuleFieldExtractOutput`
主入口方法。
### 5.2 `extract_by_heading(document, field_definition) -> FieldCandidate | None`
从标题结构中抽取。
### 5.3 `extract_by_label(document, field_definition) -> FieldCandidate | None`
从标签字段中抽取。
### 5.4 `extract_by_regex(document, field_definition) -> FieldCandidate | None`
使用字段配置中的正则规则抽取。
### 5.5 `build_candidate(field, value, source) -> FieldCandidate`
构建字段候选。
## 6. 技术实现
使用技术:
1. `re`
2. 文本结构解析结果
3. 中文标点标准化
## 7. 异常处理
1. 文本为空:跳过该文档。
2. 多个候选:全部保留。
3. 正则异常:记录工具失败。
4. 候选值过长:标记待复核。
## 8. 测试要点
1. 能从标题抽取产品名称。
2. 能从标签抽取储存条件。
3. 多候选值全部保留。
4. 空文本不报错。

View File

@@ -0,0 +1,168 @@
# 资料包导入Skill 设计
## 1. Skill 定位
`资料包导入Skill` 是资料包导入工作流的总入口 Skill负责把用户上传的文件集合转化为一个可处理的资料包批次并协调解包、扫描、文档登记、页数统计、章节点识别和目录汇总。
它不直接处理法规完整性,不调用 LLM不执行 RAG 入库。
英文实现标识建议使用 `SubmissionPackageImportSkill`,用于 Python 类名和 Tool Registry 注册处理器。
## 2. 触发场景
1. 用户在 Web 工作台上传单文件。
2. 用户批量上传多个文件。
3. 用户上传压缩包。
4. 飞书入口后续触发资料包导入任务。
5. 管理员导入平台内置法规资料。
## 3. 输入
```python
@dataclass
class SubmissionPackageImportInput:
files: list[UploadedFileRef]
batch_name: str
workflow_type: str = "registration"
source_role: str = "submission"
created_by_id: int | None = None
import_options: dict = field(default_factory=dict)
```
字段说明:
| 字段 | 说明 |
|---|---|
| `files` | 用户上传文件引用 |
| `batch_name` | 资料包批次名称 |
| `workflow_type` | 注册流程类型V1 默认 `registration` |
| `source_role` | `submission``regulation` |
| `created_by_id` | 操作人 |
| `import_options` | 是否立即页数统计、是否跳过不支持文件等选项 |
## 4. 输出
```python
@dataclass
class SubmissionPackageImportOutput:
batch_id: int
batch_no: str
status: str
overview_report: dict
warnings: list[dict]
failed_items: list[dict]
```
## 5. 依赖 Skill
1. `压缩包解包Skill`
2. `资料包扫描Skill`
3. `文档页数统计Skill`
4. `章节点识别Skill`
5. `目录汇总Skill`
## 6. 核心方法
### 6.1 `run(input) -> SubmissionPackageImportOutput`
主入口方法。
执行顺序:
1. 创建 `SubmissionBatch`
2. 保存上传文件到批次隔离目录。
3. 对压缩包调用 `压缩包解包Skill`
4. 调用 `资料包扫描Skill` 扫描批次目录。
5. 创建 `RegistrationDocument` 记录。
6. 调用 `文档页数统计Skill`
7. 调用 `章节点识别Skill`
8. 调用 `目录汇总Skill`
9. 更新批次状态。
10. 返回目录汇总结果。
### 6.2 `create_batch(input) -> SubmissionBatch`
创建资料包批次。
状态:
1. 初始为 `created`
2. 文件保存开始后更新为 `importing`
3. 汇总阶段更新为 `summarizing`
4. 成功后更新为 `completed``partial_completed`
### 6.3 `save_uploaded_files(batch, files) -> list[StoredUpload]`
保存原始上传文件,并记录上传文件来源。
安全要求:
1. 过滤路径穿越。
2. 原始文件名只用于展示。
3. 真实存储路径使用批次目录和安全文件名。
### 6.4 `create_document_records(batch, scanned_files) -> list[RegistrationDocument]`
把扫描结果落库为文档主数据。
落库字段:
1. `original_filename`
2. `relative_path`
3. `file_type`
4. `file_size`
5. `file_hash`
6. `source_archive_name`
7. `source_role`
8. `workflow_type`
9. `processing_status`
### 6.5 `finalize_batch_status(batch, overview_report) -> str`
根据结果决定批次状态。
规则:
1. 全部成功:`completed`
2. 存在待人工复核:`partial_completed`
3. 存在不支持文件但有有效文件:`partial_completed`
4. 无有效文件:`failed`
## 7. 技术实现
使用技术:
1. Django ORM
2. Django Storage
3. `pathlib`
4. `dataclasses` 或 Pydantic
5. Tool Registry
建议注册名:
```python
tool_registry.register(
name="submission_package_import",
handler=SubmissionPackageImportSkill().run,
)
```
## 8. 异常处理
| 异常 | 处理 |
|---|---|
| 上传文件为空 | 拒绝该文件,记录失败项 |
| 批次目录创建失败 | 整体失败 |
| 解包失败 | 批次部分失败或整体失败 |
| 无支持文件 | 批次失败 |
| 页数统计部分失败 | 批次部分完成 |
| 章节点识别失败 | 文档标记待人工确认 |
## 9. 测试要点
1. 单文件导入成功。
2. 多文件导入共用同一批次。
3. 压缩包导入保留相对路径。
4. 不支持文件不会阻断有效文件。
5. 部分失败时批次为 `partial_completed`
6. 输出包含 `overview_report`

View File

@@ -0,0 +1,127 @@
# 资料包扫描Skill 设计
## 1. Skill 定位
`资料包扫描Skill` 负责扫描资料包根目录,生成后续文档登记、页数统计和章节点识别所需的文件清单。
它处理的是文件系统事实,不读取文档正文,不做页数统计。
英文实现标识建议使用 `PackageFileScanSkill`,用于 Python 类名和 Tool Registry 注册处理器。
## 2. 输入
```python
@dataclass
class PackageFileScanInput:
root_dir: Path
batch_id: int
source_role: str = "submission"
allowed_extensions: set[str] = field(default_factory=set)
```
## 3. 输出
```python
@dataclass
class PackageFileScanOutput:
scanned_files: list[ScannedFile]
unsupported_files: list[ScannedFile]
skipped_files: list[SkippedFile]
warnings: list[dict]
```
`ScannedFile` 字段:
1. `absolute_path`
2. `relative_path`
3. `original_filename`
4. `extension`
5. `file_size`
6. `file_hash`
7. `source_role`
## 4. 核心方法
### 4.1 `run(input) -> PackageFileScanOutput`
主入口方法。
执行顺序:
1. 遍历根目录。
2. 过滤目录和临时文件。
3. 识别文件类型。
4. 生成文件大小和哈希。
5. 区分支持文件、不支持文件和跳过文件。
6. 返回扫描结果。
### 4.2 `iter_files(root_dir) -> Iterator[Path]`
使用 `pathlib.Path.rglob("*")` 遍历文件。
### 4.3 `should_skip(path) -> bool`
跳过规则:
1. `.DS_Store`
2. `Thumbs.db`
3. `__MACOSX`
4. Office 临时文件 `~$*.docx`
5. 空文件
### 4.4 `is_supported_document(path) -> bool`
支持类型:
1. `pdf`
2. `docx`
3. `doc`
4. `txt`
5. `md`
压缩包在本 Skill 中不再进入支持文档列表,应由导入入口先交给 `压缩包解包Skill`
### 4.5 `build_relative_path(root_dir, path) -> str`
生成统一的相对路径,使用 `/` 作为内部存储分隔符。
### 4.6 `build_file_hash(path) -> str`
使用 `sha256`
## 5. 技术实现
使用技术:
1. `pathlib`
2. `hashlib`
3. `mimetypes`
4. 可选 `python-magic`
## 6. 输出用途
扫描结果用于:
1. 创建 `RegistrationDocument`
2. 保留资料包原始目录结构。
3. 识别重复文件。
4. 后续章节点识别。
5. 页面展示不支持文件提示。
## 7. 异常处理
| 异常 | 处理 |
|---|---|
| 根目录不存在 | 返回失败 |
| 文件读取失败 | 加入 `skipped_files` |
| 文件为空 | 加入 `skipped_files` |
| 类型不支持 | 加入 `unsupported_files` |
| 哈希计算失败 | 保留文件记录,标记警告 |
## 8. 测试要点
1. 多层目录扫描后相对路径正确。
2. Office 临时文件被跳过。
3. 不支持文件进入 `unsupported_files`
4. 支持文件进入 `scanned_files`
5. 哈希对同一文件稳定。

View File

@@ -0,0 +1,112 @@
# 资料要求匹配Skill 设计
## 1. Skill 定位
`资料要求匹配Skill` 负责把当前资料包中的文档事实与法规要求项进行匹配,生成每个要求项的候选命中文档和匹配证据。
英文实现标识建议使用 `RequirementDocumentMatchSkill`
本 Skill 只判断“是否可能命中”,不负责最终缺失或风险判定。
## 2. 输入
```python
@dataclass
class RequirementDocumentMatchInput:
documents: list[DocumentFact]
requirements: list[RequirementItem]
```
## 3. 输出
```python
@dataclass
class RequirementDocumentMatchOutput:
matches: list[RequirementDocumentMatch]
unmatched_documents: list[DocumentFact]
warnings: list[dict]
```
## 4. 匹配策略
### 4.1 章节点编码匹配
最高优先级。
示例:
1. 文档 `CH1.4 申请表.docx`
2. 要求项 `CH1.4 申请表`
3. 结果:高置信命中
### 4.2 文档角色匹配
使用第一步产生的 `document_role`
示例:
1. `application_form`
2. `product_list`
3. `regulatory_information_catalog`
### 4.3 关键词匹配
使用法规要求项中的关键词表匹配文件名和相对路径。
### 4.4 人工修正字段匹配
如果用户在后台修正了章节点或资料名称,应优先使用修正结果。
## 5. 核心方法
### 5.1 `run(input) -> RequirementDocumentMatchOutput`
主入口方法。
### 5.2 `match_by_chapter_code(document, requirement) -> MatchEvidence | None`
章节点编码完全相等时返回高置信证据。
### 5.3 `match_by_document_role(document, requirement) -> MatchEvidence | None`
文档角色命中期望角色时返回证据。
### 5.4 `match_by_keywords(document, requirement) -> MatchEvidence | None`
文件名、资料名称或相对路径命中关键词时返回证据。
### 5.5 `calculate_match_confidence(evidences) -> str`
置信度:
1. `high`
2. `medium`
3. `low`
4. `none`
## 6. 技术实现
使用技术:
1. Python 字符串规则
2. `re`
3. 中文空白和标点标准化
4. 可选 `rapidfuzz`
V1 中不建议过度依赖模糊匹配,避免把相似但不等价的资料误判为已提供。
## 7. 异常处理
1. 一个要求项命中多个文档:保留全部候选,交给判定 Skill 处理。
2. 一个文档命中多个要求项:标记潜在错放或重复。
3. 文档待人工复核:仍可参与匹配,但置信度不超过 `medium`
4. 章节点冲突:输出警告。
## 8. 测试要点
1. 章节点完全匹配返回高置信。
2. 文档角色匹配返回中高置信。
3. 只有关键词匹配返回低或中置信。
4. 待复核文档不会被高置信命中。
5. 多重命中输出警告。

View File

@@ -0,0 +1,83 @@
# 长文本字段归纳Skill 设计
## 1. Skill 定位
`长文本字段归纳Skill` 负责对规则和表格无法稳定抽取的长文本字段进行证据限定后的 LLM 归纳。
英文实现标识建议使用 `LongTextFieldSummarizeSkill`
本 Skill 必须通过 LLM Provider 调用模型,并支持 Mock Provider。
## 2. 输入
```python
@dataclass
class LongTextFieldSummarizeInput:
documents: list[DocumentContent]
field_definitions: list[FieldDefinition]
enable_rag_context: bool = True
```
## 3. 输出
```python
@dataclass
class LongTextFieldSummarizeOutput:
candidates: list[FieldCandidate]
evidence_refs: list[EvidenceRef]
tool_calls: list[dict]
failed_fields: list[dict]
```
## 4. 处理字段
1. 检测靶标。
2. 适用范围 / 预期用途。
3. 性能指标。
4. 临床评价路径。
## 5. 核心方法
### 5.1 `run(input) -> LongTextFieldSummarizeOutput`
主入口方法。
### 5.2 `locate_field_context(document, field_definition) -> list[EvidenceChunk]`
通过 RAG 或关键词定位候选片段。
### 5.3 `build_llm_prompt(field_definition, chunks) -> str`
构造限定上下文提示词。
### 5.4 `call_provider(prompt, output_schema) -> dict`
调用 LLM Provider。
### 5.5 `validate_output(output) -> FieldCandidate`
校验结构化输出。
## 6. 技术实现
使用技术:
1. RAG fallback / Chroma
2. LLM Provider
3. JSON schema
4. Mock Provider
## 7. 异常处理
1. 找不到候选片段:字段标记待人工复核。
2. Provider 不可用:跳过 LLM。
3. 输出 JSON 非法:丢弃结果。
4. 输出没有来源片段:标记低可信。
## 8. 测试要点
1. Mock Provider 可返回固定字段。
2. 找不到上下文时不会编造字段。
3. 非法 JSON 被拦截。
4. LLM 关闭时主流程仍可完成。

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