# DEV1 P1 自测报告(2026-09-03) ## 完成模块清单 ### A. vstore 独立包(仓库根 vstore/,端口 8094 独立服务) | 文件 | 职责 | |---|---| | pyproject.toml | 包定义(fastapi/uvicorn/httpx/cryptography/faiss-cpu/numpy/python-docx/pymupdf/PyYAML/python-multipart) | | settings.py | VstorePaths dataclass + 限额常量(MAX_ATTACH_COUNT=5、MAX_ATTACH_BYTES=20MB、MAX_ATTACH_CHARS=60000、MAX_SECTIONS=12、MAX_OUTLINE_CHUNKS=3);VSTORE_KEY 必填(缺失抛异常拒绝启动);VSTORE_ROOT/VAULT_PATH/INDEX_PATH(本地或 http(s)://)/EMBED_URL/EMBED_MODEL | | crypto.py | Fernet 封装 encrypt_bytes/decrypt_bytes;.enc 原子落盘;secure_delete 3 遍覆写 | | parsing.py | extract_docx→{outline,paragraphs,tables,style_hints}、extract_pdf(pymupdf ≤300 页)、extract_text_file(utf-8→gb18030)、outline_to_markdown、outline_to_embedding_chunks(>7500 字符按节切块 ≤3) | | embed.py | embed_batch:httpx 线程池并发 ≤8 调 ollama /api/embed(url/model 由 settings);单条失败降级零向量不整体失败 | | backend.py | parse_backend:http(s):// → RemoteBackend(HTTP 转译,接口语义与本地一致),其余(含 file://)→ LocalBackend;混合形态拒绝;启动握手(health+空检索)失败抛异常拒绝启动 | | vault.py | 单一主库 add/get_decrypted/delete/search/rebuild_index;明文纪律(原文不出函数末尾 del,磁盘只有 .enc);检索 meta 不含正文(keywords 只用用户 meta) | | retrieval.py | faiss IndexFlatIP + index.json 单一事实源(行号==json 下标);三档降级(向量→meta 关键词→最新 8 条);向量档分数阈值(零向量查询正确降级);增量写锁;rebuild 临时文件+os.replace 原子切换 | | api.py | FastAPI:/api/vstore/{ingest,search,documents/{id},documents/{id}/style,rebuild-index,health};错误信封 {ok,error:{code,message,degraded}} 与 BFF 一致 | ### B. sdk/app/services/docgen/(BFF 侧服务层)+ services/test_docgen_*.py | 文件 | 职责 | |---|---| | settings.py | DocgenPaths + 限额常量(与 vstore 对齐);DOCGEN_ROOT | | client.py | httpx.AsyncClient 客户端;重试(指数退避 0.5/1/2s);ingest/search/get_document/get_style/delete/update_metadata/rebuild/health;失败抛 DocgenUnavailable(router 兜底 UPSTREAM_DOWN)或返回错误结构,不抛未捕获异常 | | parsing.py | extract_docx_style(bytes)→style_hints(python-docx,导出对齐) | | llm.py | complete_text/complete_json(response_format json_object+一次 JSON 修复重试);3 次指数退避 1.5s/3s/6s(429/5xx/超时);httpx 裸调不用 openai SDK;env 键 DOCGEN_LLM_BASE_URL/API_KEY/MODEL/TIMEOUT=300 | | intent.py | route(text,has_attachments,has_active_artifact)→qa\|docgen\|edit\|export,词典与判定顺序严格按 cases_intent.md;漏判安全(不命中一律 qa);is_docgen_intent(async)供 dev2 护栏 | | md2docx.py | 行级状态机→Word 内建样式(Normal/Heading 1-3/List Bullet/List Number/Table Grid/Quote);中文正文宋体/标题黑体,font.name 与 w:eastAsia 同时设置;表头加粗灰底 | | md2pdf.py | markdown-it-py(commonmark) 出 HTML→PyMuPDF Story→PDF;字体用 /usr/share/fonts 真实文件(NotoSerifCJK 正文/NotoSansCJK 标题,禁用内置 CJK);Story 失败/空文本自动降级自写排版器(TextWriter 逐块排版) | | pipeline.py | generate_document(description,*,documents,template)→{markdown,md_path}:资料明文拉取(失败占位不阻断)→模版大纲(裁剪≤12 节)→无模版 complete_json(失败重试 1 次→六段式默认)→逐节生成(单节重试 2 次仍失败占位)→humanize 按行保护(标题/表格/列表行原样,只 humanize 正文段)→md 落盘 DOCGEN_ROOT/artifacts/ | | editor.py | apply_edit(md,instruction)→{markdown}(长文档分段改,段失败保留原段);smart_export(md,format,style)→Path | | converter.py(新增适配层) | docx_from_markdown(markdown,title)→bytes / pdf_from_markdown→bytes(async;dev2 桩实际 import 的签名;内调 md2docx/md2pdf 核心,bytes 不落中间文件) | ## 测试文件与用例数 | 测试文件 | 用例数 | 通过 | 说明 | |---|---|---|---| | vstore/tests/test_settings.py | 6 | 6 | 限额常量/路径/密钥必填 | | vstore/tests/test_crypto.py | 7 | 7 | 加解密/落盘原子/secure_delete | | vstore/tests/test_parsing.py | 12 | 12 | docx/pdf/文本/大纲派生 | | vstore/tests/test_embed.py | 5 | 5 | httpx.Client 注入,不碰真 ollama | | vstore/tests/test_backend.py | 9 | 9 | 三形态解析+远程握手失败拒绝 | | vstore/tests/test_retrieval.py | 12 | 12 | faiss 三档降级/原子 rebuild | | vstore/tests/test_vault.py | 13 | 13 | 加密落盘无明文断言/增删检索 | | vstore/tests/test_api.py | 8 | 8 | FastAPI TestClient | | sdk/app/services/test_docgen_intent.py | 30+3 | 33 | cases_intent.md 冻结 30 条全过 | | sdk/app/services/test_docgen_client.py | 12 | 12 | MockTransport,不连真 vstore | | sdk/app/services/test_docgen_llm.py | 8 | 8 | 假 httpx:重试/JSON 修复/认证头 | | sdk/app/services/test_docgen_md2docx.py | 5 | 5 | 打开验证段落/样式/中文字体 | | sdk/app/services/test_docgen_md2pdf.py | 3 | 3 | tmpdir 冒烟,验证中文可提取 | | sdk/app/services/test_docgen_pipeline.py | 6 | 6 | monkeypatch llm+client 全流程 | | sdk/app/services/test_docgen_editor.py | 6 | 6 | mock LLM | | sdk/app/services/test_docgen_parsing.py | 2 | 2 | 样式提取 | | sdk/app/services/test_docgen_converter.py | 2 | 2 | dev2 桩签名 bytes 产物 | | **合计** | **145** | **145** | 全部通过 | 自测命令:`cd /home/xzq/5090/teamWorkspace/zh/PlantAI-rag-new && /home/xzq/5090/teamWorkspace/zh/plantai-rag-new-venv/bin/python -m pytest vstore/tests/ sdk/app/services/test_docgen_*.py -q` → **145 passed** ## 与 dev2 桩签名对齐确认(TL 契约通知核对) 已读 sdk/app/routers/docgen.py 桩段并逐函数核对,验证 router 实际 import 的是本包真实模块(非桩): - client.py:ingest_bytes(filename, content, *, name, category, tags, description, as_template) / search_documents(query, *, as_template, top_k) / delete_document / update_metadata / rebuild_index + DocgenUnavailable —— **签名一致,均为 async** - pipeline.generate_document(description, *, documents, template) -> {markdown, md_path} —— 一致;**artifact 落库由 dev2 router 负责**(pipeline 拿不到 artifact_id,md 先写 DOCGEN_ROOT/artifacts/.md,router 再按 artifact_id 落一次) - editor.apply_edit(markdown, instruction) -> {markdown} —— 一致 - intent.is_docgen_intent(text) —— 已按 dev2 await 方式实现为 async 并返回 bool;route 四分类保留 - 转换器:dev2 实际 import 的是 `services/docgen.converter` 的 docx_from_markdown(markdown, title) -> bytes(async,两参),与 TL 通知的 `md2docx.py 导出 (markdown, out_path) -> Path` 不一致 —— **已双层满足**:md2docx/md2pdf 同步核心按 TL 签名提供,新增 converter.py 适配 dev2 桩 - ingest 返回含 document_id/name/category/tags/description/as_template/enc_path/embed_sha/embed_chunks —— 与 dev2 落库字段对齐 ## 已知限制(TL 已裁决,P1 正式验收通过) 以下为 TL 对报告 1-9 条的裁决结果(全部通过,P1 验收完成): 1. **converter.py 保留**(裁决通过):dev2 桩 import 的是 converter 的 bytes 签名,保留适配层正确,不改 dev2。「双层满足」方案即最终形态。 2. **update_metadata 维持现状**:BFF doc_library 落库为准;vstore PATCH 端点列入 V1.5 演进(本期不加)。 3. **wps-office 缺失**:以 Noto 为实际方案(NotoSerifCJK 正文 / NotoSansCJK 标题),记录为已知限制,不算缺陷。 4. **commonmark preset**:维持现状,V1.5 可考虑 linkify-it-py。 5. **远程后端联测**:R1 轮测 DC-CONF-03/04 会真实起受控假远端验证,若暴露问题由 dev1 修复。 6. **startup deprecation**:与 BFF 现有风格一致,不改。 7. **vstore library 列表端点**:BFF 走 doc_library 表,不需要。 8. **测试位置**:符合先例,通过。 9. **vstore_restart.sh**:TL 已创建(deploy/vstore_restart.sh),无需 dev1 处理。 状态:P1 已交付并验收通过,dev1 收工待命;R1 轮测若暴露 vstore/services 侧 bug,TL 指名修复。 ## R1 修复记录(2026-09-03) ### FAILOVER-02 节标题重复(已修复) - 问题:pipeline 逐节合并时无脑插入 `## 节标题`,LLM 输出首节自带同文本标题时出现重复标题。 - 修复:新增 `_merge_section(section_title, section_md)` 辅助函数——输出首行若是标题行(# 1-6 级)且文本与节标题相同(忽略前后空格与标题层级差异),保留 LLM 自带标题不重复插入;否则正常插入 `## 节标题`。H1 总标题仍只加一次(主流程不变)。 - 变更文件:`sdk/app/services/docgen/pipeline.py`(合并循环改走 `_merge_section`;`written` 拼接改用合并后的 block);`sdk/app/services/test_docgen_pipeline.py`(新增 3 用例)。 - 新增用例: 1. `test_generate_merge_no_duplicate_title_when_llm_output_has_heading`——mock 按节输出自带标题(## 与 ### 两个层级变体),断言每节标题行恰 1 条、节名全文恰 1 次; 2. `test_generate_merge_inserts_title_when_llm_output_has_no_heading`——mock 输出不带标题,断言正常插入且恰 1 次; 3. `test_merge_section_helper_rules`——`_merge_section` 单元规则(同文本去重/异文本插入/无标题插入/前后空格容忍)。 - 自测:`pytest sdk/app/services/test_docgen_pipeline.py -q` → 9 passed;全量 `pytest vstore/tests/ sdk/app/services/test_docgen_*.py -q` → 148 passed(无回归)。