Files
jerryandClaude Fable 5 8467dec3a8 fix(bff): 断流崩溃根治——SSE 取消链模块明文化 + 团队文档归档
客户端中途断开流时 Cython 协程 __Pyx_Coroutine_SendEx 断言 abort 整个 BFF。
证据链:3.2.8 崩 db.c / 3.3.0 仍崩(升级无效) / db 明文后迁移 sse_relay.c,
证明取消链上任意 Cython 协程均可触发。最终把 db.py/qa.py/sse_relay.py
加入编译白名单(明文),隔离实例断流探针 10/10 存活,生产同构建实测通过。
代价:3 模块失去编译硬化(无凭据内容),编译模块 30->27。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-03 21:13:56 +08:00

92 lines
9.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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/<uuid>.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(无回归)。