145 lines
12 KiB
Markdown
145 lines
12 KiB
Markdown
# 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
|