Files
PlantAI-rag-new/docs/team/report_dev2.md
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

145 lines
12 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.
# P2 开发报告 —— 开发 agent-2(BFF 集成/API + 批量导入工具)
日期:2026-09-03
## 一、改动文件清单
### 修改(4 个)
| 文件 | 改动 |
|---|---|
| sdk/app/config.py | Settings 追加 docgen 键:docgen_root(默认 data/docgen)、docgen_artifact_ttl_days=30、docgen_quota_gb=50、docgen_llm_base_url(默认 https://api.deepseek.com)、docgen_llm_api_key(默认空)、docgen_llm_model=deepseek-chat、docgen_llm_timeout=300、vstore_base_url(默认 http://127.0.0.1:8094)。全部可被环境变量覆盖 |
| sdk/app/schemas.py | Block 联合追加 ArtifactBlock(type:"artifact"、artifact_id/title/template_name/status);新增 §12 模型:ArtifactOut、LibraryItemOut、LibraryPatch、DocgenGenerateRequest、DocgenEditRequest、ArtifactContentPatch、DocgenExportRequest(format Literal["pdf","docx"])、DocgenImportLine(导入工具 CSV 契约) |
| sdk/app/db.py | _SCHEMA 不动;新增 _DOCGEN_SCHEMA 两表(doc_library:id/name/category/tags/description/as_template/enc_path/embed_sha/active/created_at;doc_artifacts:id/session_id/status/title/template_name/md_path/docx_path/pdf_path/error/created_at)+ 4 个索引。connect() 内单独 executescript,try/except 失败只记日志不阻断启动;CREATE IF NOT EXISTS 幂等 |
| sdk/app/main.py | 挂载 docgen.router(import 加入 routers 包导入行 + include_router) |
### 新增(4 个)
| 文件 | 内容 |
|---|---|
| sdk/app/routers/docgen.py | BFF 全部 docgen 端点(见 §二);错误信封沿用 app.upstream.envelope |
| sdk/app/routers/test_docgen_api.py | 路由层测试:TestClient + monkeypatch mock 服务层 + 临时 SQLite,不连真实 vstore/LLM |
| tooling/scripts/docgen_import.py | 批量导入工具:并发 8、--dry-run、--keep-source、metadata.csv 映射、成功才删源明文目录、失败不删不重建、rebuild-index |
| tooling/scripts/test_docgen_import.py | 导入工具单测:httpx.MockTransport 假 vstore |
## 二、端点与行为(routers/docgen.py)
- POST /api/docgen/library(multipart,100MB 护栏)→ client.ingest_bytes → {ok, items:[{library_id,name,size,as_template}]};ingest 失败不落库,VALIDATION 信封(chips 标红可重试);vstore 不可达 → 503 UPSTREAM_DOWN
- POST /api/docgen/generate:description 必填 ≤2000 字;意图护栏 → 模版检索(失败→空模版集不阻断)→ 资料落库行(library_ids 空=全部 active)→ pipeline.generate_document → artifact 落库 {ok, artifact};服务未就绪 → 503 + 落 error 行(前端可查状态)
- GET/PATCH /artifacts/{id}/content(md 原文/保存编辑)
- POST /artifacts/{id}/edit:{instruction} → {ok, markdown},编辑结果同步落盘
- POST /artifacts/{id}/export:{format:"pdf"|"docx"} → {ok, download_url},同步转换(内容取最新编辑版)
- GET /artifacts/{id}/download?format=:FileResponse attachment(RFC 5987 文件名)
- 主库:GET /library(q/category/as_template 落库过滤)、GET/PATCH/DELETE /library/{id}(PATCH/DELETE 的 vstore 透传失败容错)、POST /library/rebuild-index(透传,不可达 → 503)
- 意图护栏:generate/edit/export 先跑 intent.is_docgen_intent,非 docgen 意图 → 422 INTENT_REJECTED(提示走 QA)
- 路径穿越防护:_safe_rel 拒绝绝对路径与 "..";制品落盘在 docgen_root 下
## 三、dev1 依赖与接口桩(重要)
services/docgen/ 当时尚未交付,docgen.py 按任务书契约做了最小接口桩(try/except ImportError 回落,签名与契约一致):
- client.py:DocgenUnavailable、ingest_bytes、search_documents、delete_document、update_metadata、rebuild_index
- pipeline.py:generate_document(description, *, documents, template)
- editor.py:apply_edit(markdown, instruction)
- intent.py:is_docgen_intent(text)
- converter.py:docx_from_markdown / pdf_from_markdown(任务书未列但 export 必需,桩同签)
**待 TL 联动联测项:**
1. dev1 交付后删掉 docgen.py 顶部桩段即可(router 主体零改动),但需对齐桩签名与 dev1 真实签名(尤其 client 函数名/参数、generate_document 返回结构 {markdown, md_path}、edit 返回 {markdown, md_path?})
2. 任务书兜底要求「document 拉取失败 → 缺失占位提示(不阻断)」:资料全文拉取属服务层职责(router 只传 doc_library 落库行 dict 给 pipeline),具体占位形态待 dev1 服务层实现后联测
3. export 的转换实现(docx/pdf 生成)由 dev1 converter.py 负责;契约未在任务书列明,本报告提请 TL 确认归属
## 四、自测结果
- 路由层:pytest sdk/app/routers/test_docgen_api.py -q → **29 passed**(上传 3、生成 4、content/edit/export/download 12、主库 8、块联合 2;含 422/404/503/204 与 vstore 容错路径)
- 导入工具:pytest tooling/scripts/test_docgen_import.py -q → **7 passed**(dry-run、成功删源、keep-source、部分失败不删不重建、并发 ingest)
- 回归抽查(只跑目标相关,未跑全量):sdk/app/test_db.py 5 passed;sdk/app/test_sessions_api.py 13 passed(Block 联合扩展无回归)
- config/schemas:import + 默认值断言通过(含 DocgenExportRequest 非法格式 422 校验)
- db:tmp sqlite 验证两表字段齐全 + 二次 connect 幂等通过
- main.py:openapi 确认 8 个 docgen 路径挂载
- 合计:**54 passed,0 failed**(29+7+5+13)
## 五、已知限制 / 需 TL 裁决
1. **QUOTA_EXCEEDED 信封未实现**:docgen_quota_gb 配置键已就位,但 quota 校验逻辑(制品目录体积扫描)属服务层/运维职责,任务书端点清单未包含对应入口——是否需要 BFF 侧实现,请 TL 裁决
2. **docgen_artifact_ttl_days 暂无消费者**:配置键已就位,TTL 清理循环建议放在 vstore 或运维侧,任务书未列对应端点
3. **INTENT_REJECTED / QUOTA_EXCEEDED 是 envelope 新 code**:upstream.py 的 _ERROR_MESSAGES/status 映射表未含这两个 code,docgen.py 调用时显式传 status=422/507 与 message,未改 upstream.py(不在任务书文件清单)
4. **legacy.py 既有告警**:openapi 生成时 Duplicate Operation ID 告警(legacy 路由,非本次改动引入)
5. **编译链**:docgen.py / test_docgen_api.py 属新增文件,compile_sdk.sh 白名单机制(main/config/schemas 明文)无需改动;docgen_import.py 在 tooling/ 不进编译链——无需 TL 确认
6. 服务层以 dict 读写落库(不引 pydantic),与契约一致
## 六、纪律自检
- 未碰 capabilities.py/qa.py 等非清单文件;未连生产端口(8090/8091 等);未 commit/push;未重启任何服务
- tests/docgen/、deploy/vstore_restart.sh、docs/ 下其他文件为他家产物,未动
## 七、补丁记录(2026-09-03,TL 裁决后追加)
TL 对报告 §五 3 个问题的裁决:1 QUOTA_EXCEEDED 由 BFF 实现;2 TTL 由 BFF 惰性清理;3 新错误码维持现状(不改 upstream.py);4 converter 归属 dev1(交付后 TL 统一删桩,本 agent 不处理)。
### 补丁内容(只改 sdk/app/routers/docgen.py 与 test_docgen_api.py)
1. **配额 _check_quota()**(routers/docgen.py):
- artifacts/ 目录体积扫描(rglob + st_size,跳过符号链接,不抛异常)
- 超 docgen_quota_gb → 507 QUOTA_EXCEEDED 信封(degraded=true,含已用/配额信息)
- 拒绝点:generate(意图护栏之后、模版检索之前)、export(护栏之后)
- generate 成功后顺带复查一次(写盘后体积更准;超限仅告警日志,artifact 已存在仍返回成功)
2. **TTL 惰性清理 _prune_expired_artifacts()**(routers/docgen.py):
- created_at 超 docgen_artifact_ttl_days 的 artifact:删除 md/docx/pdf 文件(unlink missing_ok,路径穿越防护 _safe_rel)+ DB 行 status 置 expired、路径列清空
- 在 generate 开始前调用;清理异常只记日志不阻断生成
3. 模块 docstring 更新(配额/TTL 语义说明)
### 新增测试用例(test_docgen_api.py,+5)
- test_generate_quota_exceeded_507:quota=0 设置下 generate 返回 507 QUOTA_EXCEEDED(degraded=true)
- test_export_quota_exceeded_507:同设置下 export 返回 507
- test_check_quota_passes_when_under_limit:默认设置下 generate/export 不被误拦
- test_generate_prunes_expired_artifacts:过期行(31 天前)文件被删 + status 置 expired;新鲜行(1 天前)不受影响
- test_prune_expired_missing_files_tolerated:过期行文件缺失时清理容错(不抛异常,行仍置 expired)
### 补丁后自测
- 路由层:`pytest sdk/app/routers/test_docgen_api.py -q` → **34 passed**(原 29 + 新 5)
- 导入工具:`pytest tooling/scripts/test_docgen_import.py -q` → 7 passed(无回归)
- 合计:**41 passed,0 failed**
## 八、测试污染修复记录(2026-09-03,TL 联测诊断后)
### 现象
单独跑 test_docgen_api.py 34 全过;与任何其他测试文件合跑 → 32 ERROR。
错误链:`'test_library_upload_ok' requested an async fixture 'db', with no plugin or hook that handled it` → fixture 链 `assert not self._finalizers` 崩溃。
### 根因(TL 诊断,已采纳)
pytest-asyncio 1.4.0 的 event_loop fixture 跨文件复用 bug:async fixture(db)与 sync 测试(client/测试函数)混用,多文件合跑时 loop finalizers 跨文件残留,async fixture 链崩溃。本仓库可靠模式是 QA 验收脚本 tests/docgen/test_docgen_api_acceptance.py 的纯 sync fixture + 手动事件循环。
### 修复(方案 A,只改 sdk/app/routers/test_docgen_api.py)
db fixture 由 async 改为纯 sync:connect/close 分别经 `_run()`(asyncio.new_event_loop().run_until_complete)执行;fixture 链(db/client/_reset_mocks)全 sync;测试函数内 db 访问维持 `_run(async def inner(): ...)` 闭包模式(QA 验收脚本同款)。文件头部注释记录诊断原因,防回归。
### 三组合跑结果(验收标准全过)
1. `pytest sdk/app/routers/test_docgen_api.py tooling/scripts/test_docgen_import.py -q` → **41 passed**(无 ERROR)
2. `pytest sdk/app/routers/test_docgen_api.py -q` → **34 passed**
3. `pytest sdk/app/routers/test_docgen_api.py tooling/scripts/test_docgen_import.py sdk/app/services/test_docgen_*.py vstore/tests/ -q` → **186 passed**(与 TL 预计一致)
未动被测代码 docgen.py(dev1 删桩替换后由 TL 管理)。
## 九、R1 修复记录(2026-09-03,TL 审阅 FAIL 1:护栏结构性矛盾)
### 根因(TL 定性)
_intent_guard 对所有端点统一调 is_docgen_intent(text)(= route(text, False, False) == "docgen"):
- generate 传 description → 正确
- edit 传 instruction(如「润色一下第一段」)→ route 判 edit(has_active_artifact 硬编码 False)→ 恒 422
- export 传合成文本 → 恒 422
### 修复(按 TL 要求,只改 sdk/app/routers/docgen.py 护栏相关行 + test_docgen_api.py)
1. import 增加 route:`from ..services.docgen.intent import is_docgen_intent, route`(is_docgen_intent 布尔接口未动)
2. _intent_guard 改为 `_intent_guard(text, *, has_active_artifact: bool)`,内部直接 route 三参判定:
- generate:has_active_artifact=False → route(...) == "docgen" 放行(维持原语义)
- edit:has_active_artifact=True → route(...) in ("docgen", "edit") 放行(编辑指令判 edit)
- export:取消护栏(format 已有 DocgenExportRequest Literal 白名单;DC-API-09 format 校验继续生效)
- 路由异常 → 放行(漏判安全,护栏不阻断)
3. 测试适配真实 route:generate 各用例 description 改为含写动词+文档类型(「写一份…」);edit 拒绝用例 monkeypatch 目标改为 route(机制验证);新增 4 个验收用例:
- edit「润色一下第一段」→ 200(route 判 edit 放行)
- export 合法 format → 200(无护栏)
- export 非法 format → 422 VALIDATION(白名单仍生效)
- edit 问答类指令「帮我查个文献」→ 422 INTENT_REJECTED(护栏仍生效)
- generate「今天天气怎么样」→ 422 INTENT_REJECTED(真实 route 判定)
### 自测结果
- `pytest sdk/app/routers/test_docgen_api.py sdk/app/services/test_docgen_*.py sdk/app/test_db.py sdk/app/test_sessions_api.py -q` → **135 passed**
- 污染回归组合:`test_docgen_api.py + tooling/scripts/test_docgen_import.py` → 45 passed;`+ sdk/app/services/test_docgen_*.py + vstore/tests/` → 193 passed
- 均无 ERROR、无 FAILED