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

12 KiB
Raw Permalink Blame History

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