diff --git a/docs/详细设计/1.资料包导入与目录汇总.md b/docs/详细设计/1.资料包导入与目录汇总.md new file mode 100644 index 0000000..3220f26 --- /dev/null +++ b/docs/详细设计/1.资料包导入与目录汇总.md @@ -0,0 +1,600 @@ +# 1. 资料包导入与目录汇总详细设计 + +## 1. 设计目标 + +本步骤是注册申报资料审核工作流的入口,目标是把用户上传的注册申报资料包转化为可供后续法规完整性核查、字段抽取、一致性检查和风险预警使用的结构化文档底座。 + +本步骤需要完成以下业务结果: + +1. 支持单文件、多文件、文件夹和压缩包导入。 +2. 支持 `zip`、`rar`、`7z` 压缩包解包,并保留压缩包内原始相对路径。 +3. 为每个文件建立注册资料记录,包含文件名、类型、大小、原始路径、所属批次和处理状态。 +4. 统计文件页数,`PDF` 和 `DOCX` 必须精确统计,`DOC` 无法精确统计时标记为待人工复核。 +5. 初步识别章节点、资料名称、资料类别和目录类文档。 +6. 生成资料目录汇总结果,供页面展示和 Agent Core 后续任务使用。 + +本步骤不负责最终法规完整性判定,不负责字段抽取,不负责 RAG 入库。但它需要产出足够稳定的文档主数据,作为后续所有审核任务的事实输入。 + +## 2. 所属模块与边界 + +### 2.1 Django Documents + +`apps.documents` 负责接收上传请求、保存原始文件、创建资料包批次、维护文档记录和展示目录汇总。 + +本步骤中 Django 侧建议提供: + +1. 上传页和资料包导入入口。 +2. 资料包批次模型。 +3. 文档主数据模型。 +4. 文档处理状态字段。 +5. 目录汇总服务。 +6. 目录汇总页面。 + +### 2.2 Agent Core + +`agent_core` 负责沉淀可复用的资料包处理 Skill 和 Tool Registry 注册项。 + +本步骤中 Agent Core 建议提供: + +1. `资料包导入Skill` +2. `压缩包解包Skill` +3. `资料包扫描Skill` +4. `文档页数统计Skill` +5. `章节点识别Skill` +6. `目录汇总Skill` + +这些 Skill 不直接依赖 Django View。Django View 只负责收集参数并调用服务层,服务层再调用 Agent Core 的 Skill 或 Tool。 + +### 2.3 Audit + +`apps.audit` 在本步骤中只记录资料包导入过程,不做审核结论留痕。 + +建议记录: + +1. 导入入口。 +2. 导入文件数量。 +3. 解包结果。 +4. 页数统计结果。 +5. 失败文件数量。 +6. 待人工复核文件数量。 + +## 3. 输入输出 + +### 3.1 输入 + +用户可以通过以下方式导入资料: + +1. 单文件上传。 +2. 多文件批量上传。 +3. 文件夹上传,前端传递每个文件的相对路径。 +4. 压缩包上传,后端解压后还原相对路径。 + +支持文件类型: + +1. `pdf` +2. `docx` +3. `doc` +4. `txt` +5. `md` +6. `zip` +7. `rar` +8. `7z` + +### 3.2 输出 + +本步骤输出 `registration_overview_report` 的第一版结构,核心字段如下: + +```json +{ + "batch_id": "SUB-20260603-001", + "workflow_type": "registration", + "source_role": "submission", + "file_count": 8, + "supported_file_count": 7, + "failed_file_count": 1, + "total_page_count": 36, + "page_count_status": "partial_review_required", + "chapter_summary": [ + { + "chapter_code": "CH1.2", + "chapter_name": "监管信息目录", + "file_count": 1, + "page_count": 3 + } + ], + "documents": [], + "warnings": [] +} +``` + +每个 `documents` 元素至少包含: + +```json +{ + "document_id": 1001, + "original_filename": "CH1.4 申请表.docx", + "relative_path": "第1章 监管信息/CH1.4 申请表.docx", + "file_type": "docx", + "file_size": 1048576, + "chapter_code": "CH1.4", + "chapter_name": "申请表", + "document_role": "application_form", + "page_count": 5, + "page_count_method": "docx_exact", + "page_count_confidence": "exact", + "processing_status": "summarized", + "review_status": "normal", + "needs_manual_review": false +} +``` + +## 4. 主工作流 + +```text +用户选择导入资料 +-> 创建资料包批次 +-> 校验上传文件 +-> 判断是否压缩包 +-> 解包或登记原始文件 +-> 扫描文件树 +-> 过滤不支持文件 +-> 创建文档主数据 +-> 统计页数 +-> 初步识别章节点 +-> 识别目录类/声明类/沟通类文件 +-> 汇总目录与页数 +-> 写入导入审计 +-> 返回目录汇总页面 +``` + +## 5. 节点详细设计 + +### 5.1 节点一:创建资料包批次 + +业务功能: + +1. 为一次导入创建独立批次。 +2. 记录导入来源、导入人、任务类型和资料来源角色。 +3. 建立隔离存储目录,避免不同项目或不同批次文件混在一起。 + +建议模型: + +```python +class SubmissionBatch(models.Model): + batch_no = models.CharField(max_length=64, unique=True) + name = models.CharField(max_length=255) + workflow_type = models.CharField(max_length=32, default="registration") + source_role = models.CharField(max_length=32, default="submission") + import_status = models.CharField(max_length=32, default="created") + created_by = models.ForeignKey(User, null=True, on_delete=models.SET_NULL) + created_at = models.DateTimeField(auto_now_add=True) +``` + +使用技术: + +1. Django ORM +2. Django Storage +3. `uuid` 或业务编号生成器 + +产生方法: + +1. `create_submission_batch(params) -> SubmissionBatch` +2. `build_batch_storage_path(batch) -> Path` + +对应 Skill: + +1. `资料包导入Skill` + +### 5.2 节点二:上传文件校验 + +业务功能: + +1. 校验文件扩展名是否支持。 +2. 校验文件大小是否超过配置限制。 +3. 校验文件名是否包含危险路径。 +4. 识别普通文件和压缩包。 + +使用技术: + +1. Django uploaded file API +2. `pathlib.PurePath` +3. `python-magic` 或 `mimetypes` + +校验规则: + +1. 禁止绝对路径。 +2. 禁止 `..` 路径穿越。 +3. 禁止空文件。 +4. 允许中文文件名。 +5. 对扩展名和 MIME 不一致的文件标记为待复核。 + +产生方法: + +1. `validate_uploaded_file(uploaded_file) -> FileValidationResult` +2. `detect_upload_kind(uploaded_file) -> Literal["document", "archive"]` + +对应 Skill: + +1. `资料包导入Skill` + +### 5.3 节点三:压缩包解包 + +业务功能: + +1. 支持 `zip`、`rar`、`7z`。 +2. 解包到批次隔离目录。 +3. 保留压缩包内多层相对路径。 +4. 记录每个解包文件来自哪个压缩包。 +5. 对空包、损坏包、加密包、嵌套异常给出业务状态。 + +使用技术: + +1. `zipfile` +2. `rarfile` 或纯 Python rar 解析依赖 +3. `py7zr` +4. `pathlib` +5. 文件路径安全检查函数 + +关键状态: + +1. `extracted` +2. `empty_archive` +3. `encrypted_archive` +4. `unsupported_archive` +5. `extract_failed` +6. `path_rejected` + +产生方法: + +1. `extract_archive(archive_path, target_dir) -> ArchiveExtractionResult` +2. `safe_extract_member(member_name, target_dir) -> Path` +3. `normalize_relative_path(raw_path) -> str` + +对应 Skill: + +1. `压缩包解包Skill` + +### 5.4 节点四:扫描文件树 + +业务功能: + +1. 遍历本次批次目录。 +2. 过滤临时文件、系统隐藏文件和不支持文件。 +3. 生成待处理文件清单。 +4. 保留相对于资料包根目录的路径。 + +使用技术: + +1. `pathlib.Path.rglob` +2. 文件哈希 `hashlib.sha256` +3. 后缀白名单 + +过滤规则: + +1. 跳过 `__MACOSX`。 +2. 跳过 `.DS_Store`。 +3. 跳过 Office 临时文件,如 `~$申请表.docx`。 +4. 对 `.exe` 等不支持文件记录为 `unsupported`,不进入后续页数统计。 + +产生方法: + +1. `scan_submission_package(root_dir) -> PackageScanResult` +2. `build_file_fingerprint(file_path) -> str` +3. `is_supported_document(file_path) -> bool` + +对应 Skill: + +1. `资料包扫描Skill` + +### 5.5 节点五:创建文档主数据 + +业务功能: + +1. 为每个文件创建文档记录。 +2. 写入文件名、相对路径、文件类型、大小、哈希、来源压缩包。 +3. 标记初始处理状态。 +4. 区分业务申报资料和法规依据资料。 + +建议模型: + +```python +class RegistrationDocument(models.Model): + batch = models.ForeignKey(SubmissionBatch, on_delete=models.CASCADE) + original_filename = models.CharField(max_length=255) + relative_path = models.CharField(max_length=1024) + file_type = models.CharField(max_length=32) + file_size = models.PositiveBigIntegerField(default=0) + file_hash = models.CharField(max_length=128, blank=True) + source_archive_name = models.CharField(max_length=255, blank=True) + source_role = models.CharField(max_length=32, default="submission") + workflow_type = models.CharField(max_length=32, default="registration") + processing_status = models.CharField(max_length=32, default="created") +``` + +使用技术: + +1. Django ORM +2. 批量创建 `bulk_create` +3. 唯一性约束:`batch + relative_path` + +产生方法: + +1. `create_document_records(batch, scanned_files) -> list[RegistrationDocument]` +2. `mark_unsupported_file(batch, scanned_file, reason) -> RegistrationDocument` + +对应 Skill: + +1. `资料包导入Skill` +2. `资料包扫描Skill` + +### 5.6 节点六:页数统计 + +业务功能: + +1. 为每份文档统计页数。 +2. 写入页数统计方法和可信度。 +3. 对无法精确统计的文档标记待人工复核。 + +使用技术: + +1. PDF:`pypdf` 或 `PyMuPDF` +2. DOCX:优先读取 Word 兼容统计结果,必要时通过 LibreOffice headless 转 PDF 后精确统计 +3. DOC:尝试转换或解析,失败时标记 `manual_review_required` +4. TXT/MD:页数不作为强制指标,可按 `not_applicable` 或导出预览后统计 + +页数状态: + +1. `exact` +2. `not_applicable` +3. `manual_review_required` +4. `failed` + +产生方法: + +1. `count_document_pages(document) -> PageCountResult` +2. `count_pdf_pages(path) -> PageCountResult` +3. `count_docx_pages(path) -> PageCountResult` +4. `count_doc_pages(path) -> PageCountResult` + +对应 Skill: + +1. `文档页数统计Skill` + +### 5.7 节点七:章节点与资料名称识别 + +业务功能: + +1. 从文件名、相对路径和标题文本中识别章节点。 +2. 识别资料名称。 +3. 标记目录类、声明类、申请表、产品列表、历史沟通说明等文档角色。 +4. 对无法确认的文件标记待人工确认。 + +使用技术: + +1. 正则表达式 +2. 本地章节点规则 YAML +3. `python-docx` 抽取首页标题 +4. 简单关键词规则 + +识别优先级: + +1. 相对路径中的章名,如 `第1章 监管信息` +2. 文件名中的章节点编码,如 `CH1.4` +3. 文件名中的标准资料名称,如 `申请表` +4. 文档首页标题 +5. 用户手动选择的章节点 + +产生方法: + +1. `classify_chapter_node(document) -> ChapterClassificationResult` +2. `extract_chapter_code_from_path(relative_path) -> str | None` +3. `detect_document_role(filename, title_text) -> str` + +对应 Skill: + +1. `章节点识别Skill` + +### 5.8 节点八:目录汇总生成 + +业务功能: + +1. 汇总当前批次所有文档。 +2. 计算文件数量、支持文件数量、失败数量、总页数。 +3. 按章节点聚合文件和页数。 +4. 输出待人工复核清单。 +5. 生成页面展示和 Agent Core 输入使用的统一结构。 + +使用技术: + +1. Django ORM 聚合查询 +2. Python dataclass 或 Pydantic schema +3. JSONField 存储结构化结果 + +产生方法: + +1. `build_directory_summary(batch_id) -> RegistrationOverviewReport` +2. `summarize_chapters(documents) -> list[ChapterSummary]` +3. `collect_import_warnings(documents) -> list[ImportWarning]` + +对应 Skill: + +1. `目录汇总Skill` + +### 5.9 节点九:审计留痕 + +业务功能: + +1. 记录资料包导入执行过程。 +2. 记录失败文件和待人工复核文件。 +3. 为后续完整性核查提供可追溯基础。 + +使用技术: + +1. `apps.audit` 服务层 +2. JSONField +3. 敏感信息脱敏函数 + +产生方法: + +1. `record_import_audit(batch, overview_report) -> AuditLog` +2. `sanitize_import_context(context) -> dict` + +对应 Skill: + +1. 本步骤不单独产生 Audit Skill,由 Django 服务层调用 Audit 服务完成。 + +## 6. Skill 清单 + +本步骤产生以下 Skill 设计文档: + +1. [资料包导入Skill](skill/资料包导入Skill.md) +2. [压缩包解包Skill](skill/压缩包解包Skill.md) +3. [资料包扫描Skill](skill/资料包扫描Skill.md) +4. [文档页数统计Skill](skill/文档页数统计Skill.md) +5. [章节点识别Skill](skill/章节点识别Skill.md) +6. [目录汇总Skill](skill/目录汇总Skill.md) + +## 7. 数据状态设计 + +### 7.1 批次状态 + +| 状态 | 含义 | +|---|---| +| `created` | 已创建批次,尚未完成文件登记 | +| `importing` | 正在导入或解包 | +| `summarizing` | 正在统计页数和目录 | +| `completed` | 导入与目录汇总完成 | +| `partial_completed` | 部分文件失败或需复核 | +| `failed` | 整体导入失败 | + +### 7.2 文档状态 + +| 状态 | 含义 | +|---|---| +| `created` | 已创建记录 | +| `unsupported` | 文件类型不支持 | +| `page_counted` | 已完成页数统计 | +| `classified` | 已完成章节点识别 | +| `summarized` | 已进入目录汇总 | +| `manual_review_required` | 需要人工复核 | +| `failed` | 处理失败 | + +### 7.3 页数可信度 + +| 状态 | 含义 | +|---|---| +| `exact` | 精确页数 | +| `not_applicable` | 不适用 | +| `manual_review_required` | 无法精确统计,需人工复核 | +| `failed` | 统计失败 | + +## 8. 异常处理 + +本步骤需要业务化处理以下异常: + +1. 上传空文件:拒绝导入,提示文件为空。 +2. 文件名路径穿越:拒绝该文件,记录安全拦截。 +3. 压缩包损坏:批次标记为 `failed` 或 `partial_completed`。 +4. 压缩包加密:标记为 `manual_review_required`。 +5. 解包后无支持文件:批次标记为 `partial_completed`,页面提示未发现可处理资料。 +6. DOC 页数无法精确统计:文档标记待人工复核,不阻断目录汇总。 +7. DOCX 页数无法精确统计:文档标记失败或待复核,并在汇总结果中突出提示。 +8. 文件内容与扩展名不一致:记录风险提示,仍可尝试解析。 +9. 同名相对路径重复:保留第一份,后续文件加版本后缀或标记重复。 +10. 混入不支持文件:记录为 `unsupported`,不进入页数统计。 + +## 9. 页面展示 + +资料包导入完成后,Documents 页面建议展示: + +1. 批次名称和导入时间。 +2. 导入文件总数。 +3. 支持文件数量。 +4. 页数合计。 +5. 待人工复核数量。 +6. 解包失败或不支持文件数量。 +7. 按章节点聚合的目录表。 +8. 文件明细表。 + +文件明细表字段: + +1. 原始相对路径。 +2. 文件名。 +3. 文件类型。 +4. 页数。 +5. 页数可信度。 +6. 章节点。 +7. 资料名称。 +8. 处理状态。 +9. 是否需人工复核。 + +## 10. 与后续步骤的接口 + +后续法规完整性核查需要读取: + +1. `batch_id` +2. `workflow_type` +3. `source_role` +4. `documents[].document_id` +5. `documents[].relative_path` +6. `documents[].chapter_code` +7. `documents[].document_role` +8. `documents[].page_count` +9. `documents[].processing_status` +10. `documents[].needs_manual_review` + +后续 RAG 入库需要读取: + +1. 文件路径。 +2. 文件类型。 +3. 章节点。 +4. 资料名称。 +5. 业务资料或法规资料角色。 +6. 文档处理状态。 + +## 11. 测试设计 + +### 11.1 单元测试 + +1. 上传文件校验。 +2. 路径安全校验。 +3. 压缩包解包。 +4. 文件扫描过滤。 +5. 页数统计。 +6. 章节点识别。 +7. 目录汇总聚合。 + +### 11.2 服务层测试 + +1. 单文件导入生成文档记录。 +2. 多文件导入生成同一批次。 +3. 压缩包导入保留相对路径。 +4. 解包失败时批次状态正确。 +5. DOC 无法统计页数时标记待复核。 +6. 目录汇总结果包含文件数量、页数、章节点和警告。 + +### 11.3 页面测试 + +1. 上传成功后跳转到目录汇总页。 +2. 页面展示页数可信度。 +3. 页面展示待人工复核提示。 +4. 不支持文件有清晰提示。 + +## 12. V1 实现建议 + +V1 建议先完成以下最小闭环: + +1. 支持单文件和多文件上传。 +2. 支持 `zip` 解包,并预留 `rar`、`7z` 依赖接入点。 +3. 支持 `PDF`、`DOCX` 页数统计。 +4. `DOC` 文件先标记待人工复核。 +5. 基于文件名和路径识别 `CH1.*` 章节点。 +6. 生成目录汇总页面和结构化 `registration_overview_report`。 + +在 V1 后续增强中补齐: + +1. `rar`、`7z` 纯 Python 解包。 +2. DOC 转换后精确统计。 +3. 目录类文档内容解析。 +4. 文件夹上传前端体验。 +5. 后台人工修正章节点和资料名称。 diff --git a/docs/详细设计/skill/压缩包解包Skill.md b/docs/详细设计/skill/压缩包解包Skill.md new file mode 100644 index 0000000..c9f4f1b --- /dev/null +++ b/docs/详细设计/skill/压缩包解包Skill.md @@ -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. 解包后文件哈希生成稳定。 diff --git a/docs/详细设计/skill/文档页数统计Skill.md b/docs/详细设计/skill/文档页数统计Skill.md new file mode 100644 index 0000000..f059a22 --- /dev/null +++ b/docs/详细设计/skill/文档页数统计Skill.md @@ -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. 损坏文件返回失败状态。 diff --git a/docs/详细设计/skill/目录汇总Skill.md b/docs/详细设计/skill/目录汇总Skill.md new file mode 100644 index 0000000..daf5515 --- /dev/null +++ b/docs/详细设计/skill/目录汇总Skill.md @@ -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 字段稳定。 diff --git a/docs/详细设计/skill/章节点识别Skill.md b/docs/详细设计/skill/章节点识别Skill.md new file mode 100644 index 0000000..378454a --- /dev/null +++ b/docs/详细设计/skill/章节点识别Skill.md @@ -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. 路径与文件名冲突时输出警告。 diff --git a/docs/详细设计/skill/资料包导入Skill.md b/docs/详细设计/skill/资料包导入Skill.md new file mode 100644 index 0000000..1df3d82 --- /dev/null +++ b/docs/详细设计/skill/资料包导入Skill.md @@ -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`。 diff --git a/docs/详细设计/skill/资料包扫描Skill.md b/docs/详细设计/skill/资料包扫描Skill.md new file mode 100644 index 0000000..1511107 --- /dev/null +++ b/docs/详细设计/skill/资料包扫描Skill.md @@ -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. 哈希对同一文件稳定。