Files
PlantAI-rag-new/docs/API.md

26 KiB
Raw Permalink Blame History

API.md —— BFF 接口契约

版本 1.0 | 作者 PM | 本文件由 PM 独占写入,所有开发/测试 agent 只读

BFF 是前端唯一的数据来源。前端禁止直连任何上游。 上游真实响应结构以 DISCOVERY.md 为准;本文件定义 BFF 暴露给前端的契约。 契约变更必须由 PM 在本文件修改并广播,任何人不得私自改接口形态。


1. 通用约定

  • Base:/api
  • 编码:UTF-8,JSON;时间一律 ISO 8601 UTC(2026-08-25T12:34:56Z)
  • ID:UUID4 字符串
  • 统一错误信封(所有非 2xx 与降级响应):
{ "ok": false, "error": { "code": "UPSTREAM_DOWN", "message": "知识问答服务当前不可用,请稍后重试", "degraded": true } }
code HTTP 含义 前端行为
UPSTREAM_DOWN 503 依赖的上游不可用 降级提示(禁用对应功能),不弹窗
UPSTREAM_ERROR 502 上游返回了错误 显示 message + 重试按钮
TIMEOUT 504 上游超时 显示「检索超时」+ 重试按钮
INVALID_CITATION 502 引用校验失败(已自动重试一次仍失败) 显示「引用校验失败」+ 重试按钮
NOT_FOUND 404 会话/消息不存在 回列表
VALIDATION 422 请求参数不合法 展示 message
INTERNAL 500 BFF 自身错误 显示 message

degraded 语义裁决(2026-08-25,PM):degraded:true 仅用于「上游服务整体不可用」(UPSTREAM_DOWN),对应前端的降级横幅/功能禁用;请求级失败(UPSTREAM_ERROR/TIMEOUT/INVALID_CITATION/NOT_FOUND/VALIDATION/INTERNAL)一律 degraded:false,对应前端的内联错误 + 重试。两者的 UI 处置不同,不得混用。

  • 成功响应不包 {ok:true} 信封,直接返回业务 JSON。
  • 健康接口(/api/health)例外:返回 {status, ...} 结构(见 §3)。
  • 请求参数校验错误(422)例外:FastAPI 原生 {"detail":[...]} 形态,不包统一信封(集成清单:M2 由全局异常处理器收敛为 VALIDATION 信封,前端 http.js 在收敛前需兼容两种形态)。
  • 已裁决的偏差(2026-08-25,A3):抽取上传用 SpooledTemporaryFile 中间落盘(≤5MB 驻内存),语义为「不整文件读进内存」;kg/extraction 信封用顶层 JSONResponse(与 mock 一致);/api/extraction/jobs/{id} 的 plan 拉取失败(含双上游不可达)置 plan:null 不报错(job 详情可用性优先)。

2. 块联合(消息内容单元)

助手消息的 blocks 数组元素为判别式联合:

{ "type": "text", "html": "<p>…</p>" }                       // 已消毒的 HTML
{ "type": "table", "headers": ["…"], "rows": [["…"]] }       // 二维数组
{ "type": "code", "text": "…" }
{ "type": "stream", "text": "…" }                            // 瞬态,绝不落库
{ "type": "pipeline", "status": "running|done|error",
  "steps": [ { "name": "规划", "state": "waiting|running|done", "desc": "…", "phase": "planning" } ] }
{ "type": "tool", "icon": "🔬", "name": "…", "status": "running|done|error",
  "dur": "42s", "open": true, "params": "…", "result": "…",
  "logs": [ { "text": "…", "cls": "log-ok|log-run" } ], "phase": "retrieval" }
{ "type": "sources", "items": [ <source> ] }                 // 见 §8
{ "type": "graph", "nodes": [ <node> ], "edges": [ <edge> ], "chains": [ <chain> ] }  // 见 §9

3. 健康与元信息

GET /api/health

并发探测 7920、8000、7900 与 Neo4j(1s 超时,5s 缓存)。

{
  "status": "ok|degraded",
  "version": "0.1.0",
  "upstreams": {
    "qa":    { "ok": true,  "version": "1.1.73-assertion-anchored" },
    "rag":   { "ok": true },
    "ragAlt":{ "ok": true },
    "neo4j": { "ok": true, "active": "neo4j-plant-srt-pc2025-v3-20260817-normalized" }
  },
  "db": { "ok": true, "sessions": 12 }
}

GET /api/qa/info

透传上游 /info 并精简:

{
  "name": "QA V1.1.73 Assertion-Anchored Full Graph",
  "version": "1.1.73-assertion-anchored",
  "pipeline": [ "question", "provisional answer", "…共 12 段" ],
  "config": { "max_evidence": 40, "bge_rerank": true, "knowledge_chain_defaults": { "…": "…" } }
}

4. QA 问答

POST /api/qa/chat

请求(mode 由前端从 config/modes.js 拼出):

{
  "question": "水稻SPL基因家族的功能研究有哪些关键进展?",
  "use_llm": true,
  "debug": false,
  "chain_top_k": 3, "chain_hops": 2, "chain_max_edges": 12, "chain_max_nodes": 10,
  "native_chain_enabled": true, "native_chain_max_hops": 3, "native_chain_top_k": 5,
  "ablation": { "top_k_for_llm": 20, "graph_min_evidence": 10 },
  "evidence_first": true
}

字段说明(2026-08-26 方案 2/3 契约新增,BFF 透传不增删):

  • ablation:请求级证据量控制。上游的 QA_V1_MAX_EVIDENCE 环境变量是死代码(无消费者),真正决定 LLM 证据量的是此 dict(任意键透传,上游 pipeline 读取)。top_k_for_llm 默认 30,本项目设 20;graph_min_evidence 默认 15,与 top_k 配套下调至 10 防图证据挤压非图证据。仅 ask/plan 模式携带。
  • evidence_first:两级体验开关(见下节 event: evidence 帧)。BFF 在打开 chat 流时并发打上游 /search(无 LLM,约 92s),归一化后作为非终态帧先推给前端。仅 ask/plan 模式携带,search 模式天然无此需求。

响应:归一化 QA 响应(完整结构见 §7)。读超时 300s。invalid_llm_citation 自动重试一次。

POST /api/qa/chat/stream(SSE)

请求同 /chat。响应 Content-Type: text/event-stream,帧序列:

event: progress   data: {"stage":"entities_identified","phase":"planning","label":"已识别实体","details":{"entities":["OsSPL14",…]},"at":"2026-08-25T…Z"}
event: progress   data: {"stage":"evidence_review_completed","phase":"evidence","label":"证据审核完成","details":{"accepted_count":12,"publication_count":5},"at":"…"}
event: evidence   data: {<归一化 search 响应:sources/graph/chains 齐全,answer 为空串,mode:"search">}
event: complete   data: {<同 /chat 的归一化响应>}
event: error      data: {"code":"TIMEOUT","message":"检索超时","retryable":true}
event: timeout    data: {"message":"检索超时"}
event: raw        data: {<无法分类的上游原始 JSON>}
: ping            ← 15s 保活注释帧

event: evidence 帧规则(2026-08-26 方案 3 新增):

  • 非终态,0 或 1 次,仅在请求带 evidence_first:true 且并发 search 成功时出现,顺序在 progress 与 complete 之间(通常比 complete 早约 1-2 分钟到达)
  • payload 为归一化 search 响应(与 §7 同构,answer 恒为空串)
  • search 失败 → 静默降级为纯 chat 流(不发 evidence,不影响主链)
  • 前端把它装配为 sources/graph 块先展示;complete 到达后用 chat 的 sources 覆盖(保证引用 chip 编号与答案内引用一致)

规则:

  • phase 由 BFF 计算,值域 planning|retrieval|evidence|generation|done。前端绝不解析上游 stage 名。
  • label 为该事件的中文名(BFF 内置映射,与 i18n/labels.js 一致)。
  • complete 之后流关闭。上游 240s 无事件 → event: timeout 并关闭。
  • 前端 AbortController 断开 → BFF 取消上游请求。

阶段→相位映射(BFF 与前端共用,见 i18n/labels.js):

phase stage label
planning understanding_started 开始理解问题
planning entities_identified 已识别实体
planning related_search_started 开始关联检索
retrieval concepts_found 发现相关概念
retrieval subquestion_retrieved 子问题召回完成
retrieval agent_review_started 开始智能体审核
retrieval subquestion_retry_started 子问题重试中
evidence evidence_accepted 证据已采纳
evidence evidence_review_completed 证据审核完成
generation answer_generation_started 开始生成答案
done completed 检索完成

未知 stage → phase:"retrieval",label = stage 原文。

POST /api/qa/search

请求同 /chat 但 BFF 强制 use_llm:false + native_chain_enabled:false(缺陷 #28,2026-09-10:检索模式关闭图链,只走 Qdrant 向量检索——与 sse_relay 的 evidence_first 先例同法)。响应:归一化响应但 answer 为空、mode:"search"。

超时降级(缺陷 #28):上游读超时(180s 上限)时返回 200 降级体(不返回错误信封):

{ "mode": "search", "answer": "", "sources": [], "source_count": 0,
  "degraded": true, "reason": "TIMEOUT" }

正常空结果(未超时但无命中)degraded:false——前端据此区分「检索超时」与「没有检索到有效回答」。


5. 会话与消息

GET /api/sessions

[ { "id": "uuid", "title": "水稻SPL…", "status": "done",
    "created_at": "…", "updated_at": "…", "message_count": 2,
    "meta": {"title_manual": false} } ]

按 updated_at 倒序。

POST /api/sessions

请求 { "title": "新会话" }(可空,默认「新会话」)。返回会话对象。

GET /api/sessions/{id}

返回会话对象 + messages:

{ "id": "…", "title": "…", "status": "done", "created_at": "…", "updated_at": "…",
  "meta": {"title_manual": false},
  "messages": [
    { "id": "…", "role": "user", "kind": "qa", "status": "done",
      "blocks": [], "raw": null,
      "text": "问题原文",                    ← 仅 role=user 有
      "created_at": "…", "updated_at": "…" },
    { "id": "…", "role": "assistant", "kind": "qa", "status": "done",
      "blocks": [ …§2 块联合… ], "raw": {…},
      "created_at": "…", "updated_at": "…" }
  ] }

PATCH /api/sessions/{id}

{ "title": "…" } 或 { "status": "running|done|error" }。

DELETE /api/sessions/{id}

204。级联删除消息。

POST /api/sessions/{id}/messages

{ "role": "user", "text": "…" } → 返回消息对象。 { "role": "assistant", "kind": "qa", "status": "streaming", "blocks": [<初始 blocks>] } → 返回消息对象(流式骨架)。

PATCH /api/sessions/{id}/messages/{mid}

{ "status": "…", "blocks": [<检查点 blocks>], "raw": {…} } → 返回更新后的消息。流式检查点每 ~3s 调用一次。

GET /api/sessions/{id}/sources

全会话来源聚合:按 paper_id 去重(无 paper_id 时按 id),带 count(出现次数):

{ "session_id": "…", "total": 12, "sources": [ { <source 字段见 §8>, "count": 3 } ] }

6. 知识图谱浏览(BFF 直连 Neo4j)

真实 schema 见 DISCOVERY.md §2。当选实例 neo4j-plant-srt-direct-v1-full(bolt 18788,db neo4j)。 schema 为 canonical:节点 SRT2CanonicalEntity(entity_type/name/name_norm/canonical_id/support_count)、SRT2CanonicalAssertion(证据断言,含 source/relation_norm/target/best_evidence/title/doi/pmid/publication_year 等)、SRT2Paper;关系 SRT2_CANONICAL_ASSERTS / SRT2_SUPPORTED_BY。 没有全文索引;名字检索走 name_norm RANGE 索引(= 或 STARTS WITH,0.09–0.10s)。不得创建索引修改共享实例(团队决策前)。 回退顺序:18788 → 19788(normalized 实例,schema 不同需单独 Cypher 适配)→ 17687(空库)。

GET /api/kg/status

{ "ok": true, "active": "neo4j-plant-srt-direct-v1-full",
  "schema": "canonical",
  "labels": ["SRT2CanonicalEntity","SRT2CanonicalAssertion","SRT2Paper"],
  "relationship_types": ["SRT2_CANONICAL_ASSERTS","SRT2_SUPPORTED_BY"],
  "entity_count": 3820996, "assertion_count": 8246875,
  "name_index": "srt2_canonical_entity_name_norm", "fulltext_index": null }

Neo4j 不可用 → 503 降级信封(UPSTREAM_DOWN)。

GET /api/kg/overview

按 entity_type 与 relation_category 统计:

{ "entities_by_type": [ {"type": "gene", "count": 43210}, … ],
  "relations_by_category": [ {"type": "characterized_in", "count": 12033}, … ] }

GET /api/kg/entities?q=OsSPL14&limit=20

name_norm = toLower(q) 或 STARTS WITH toLower(q),走 name_norm 索引:

{ "query": "OsSPL14", "results": [ { "id": "cent_xxx", "name": "OsSPL14", "type": "gene", "support_count": 12 } ] }

GET /api/kg/entities/{id}

id 为 canonical_id(cent_*)或名字(先按 name_norm 定位):

{ "id": "cent_xxx", "name": "OsSPL14", "type": "gene", "properties": { …节点全量属性… } }

未命中 → 404 信封(NOT_FOUND)。

GET /api/kg/entities/{id}/neighborhood?depth=1&limit=50

depth 1–2(默认 1,两跳约 13s 不默认开启),limit ≤200。每跳按 paper_count/support_count 降序截断(防 hub 吃满预算):

{ "center": "cent_xxx", "center_name": "OsSPL14", "depth": 1,
  "nodes": [ {"id":"cent_xxx","label":"OsSPL14","type":"gene"}, … ],
  "edges": [ {"source":"cent_xxx","target":"cent_yyy","relation":"characterized_in","label":"characterized_in",
              "assertion_id":"cass_…","paper_count":5,"support_count":9,"title":"…","doi":"…","pmid":"…"} ] }

/api/kg/* 与消息内 graph 块使用同一 {nodes,edges} 形态——前端只有一个图渲染器。字段差异由 BFF 在 kg 路由处补齐(relation/label 同值,chain_id/hop 为 null 的边照常渲染)。


7. 归一化 QA 响应(/chat、/chat/stream 的 complete、/search 共用)

{
  "query_id": "…", "version": "1.1.73-assertion-anchored",
  "mode": "chat|search",
  "answer": "…markdown 文本…",           ← search 模式为 ""
  "source_count": 12,
  "sources": [ <source,见 §8> ],
  "used_sources": [ <source> ],
  "chains": [ <chain,见 §9> ],
  "graph": { "nodes": [<node>], "edges": [<edge>], "chains": [<chain>] },
  "retrieval_plan": {…} , "retrieval_notes": "…",
  "timing": { "total_s": 41.2, "…": "…" },
  "citation_map": {…},
  "raw": { …上游原始完整响应… }
}

归一化别名规则(pick(obj, *names) 取第一个存在的键;真实字段名以 DISCOVERY.md §4.1 为准):

  • answer | response | message → answer
  • sources | citations → sources;used_sources | references → used_sources
  • retrieval_plan | agentic_plan | query_plan → retrieval_plan(真实上游的键是 query_plan,其内嵌套 agentic_plan);retrieval_notes | notes → retrieval_notes
  • timing | timings → timing(真实键是 timings)
  • citation_map | citation_validation → citation_map(真实键是 citation_validation)
  • knowledge_graph / graph_edges / native_graph_chains / graph_knowledge_chains → graph(链拆分进 chains)

链条容错:native_graph_chains 可能是列表(真实形态,305 条),graph_knowledge_chains 可能是 dict({mode, chains[], chain_count, audit, summary})——两种形态都要能提取出 hop 列表。真实 chain item:{chain_id, chain_source, hop_count, path_score, start, end, bridge_entities, hops[], score_breakdown};真实 hop:{hop_index, from_id, from_name, to_id, to_name, source, relation, target, source_canonical_id, target_canonical_id, source_type, target_type, relation_category, assertion_id, paper_id, title, pmid, pmcid, doi, species, evidence, support_count, paper_count, best_score, edge_score, …}。归一化时:from_name|source → source、to_name|target → target、from_id|source_canonical_id → source 节点 id、to_id|target_canonical_id → target 节点 id、from_name/to_name → 节点 label、source_type/target_type → 节点 type、paper_id→paper_id、assertion_id → assertion_id。

已裁决的补充(2026-08-25,A1 偏差裁决通过):

  • hop 归一化中间产物额外携带 source_id/target_id(内部派生用,最终 graph/chains 输出仍与 §9 一致)。
  • 来源聚合(GET /sessions/{id}/sources)去重键为 paper_id(无则 id),聚合值取首次出现的完整 source item,count 为出现次数。
  • search 模式 answer 恒为空串(而非缺键)。
  • PATCH 消息/会话空 body(三个字段全缺)按幂等成功返回当前状态。
  • PATCH 会话 title 时置 meta.title_manual=true(对应 PRD FR-SESSION-008 的手动改名保护)。

8. Source(来源)结构

{
  "id": "…", "title": "Tartary buckwheat SPL…",
  "paper_id": "PMID12345|paper_xxx", "chunk_id": "…",
  "pmid": "31286919", "pmcid": "PMC…", "doi": "10.xxx/…", "url": "https://doi.org/…",
  "snippet": "…证据片段…", "score": 0.87,
  "locator": "…", "section": "Results", "support_count": 2,
  "type": "…", "metadata": {…}
}

别名映射(真实 source item 为 26 键,见 DISCOVERY.md §4.1): content|evidence|text|snippet → snippet; doc_id|paper_id|pubmed_id|id|chunk_id → paper_id(chunk_id 单独保留;真实键是 paper_id); final_score|score|confidence|rerank_score → score(真实键是 final_score,另有 vector_score/rrf_score/rerank_score 保留进 metadata); title|paper_title|source → title(注意:真实 item 的 source 是断言主体实体名而非论文标题;title 存在时优先取 title,两者并存时 source 落入 metadata.source_entity); url|doi_url → url;只有 doi 时派生 url = https://doi.org/{doi}; evidence_id|id → id;relation_id|assertion_id → metadata.assertion_id; output_layer、claim_scope、cluster_id、support_count、section、species 原样透传。 缺失字段键必须存在(值为 null),前端不得渲染 undefined。


9. 图谱结构(Node / Edge / Chain)

{ "node": { "id": "OsSPL14", "label": "OsSPL14", "type": "Gene" } }
{ "edge": { "source": "OsSPL14", "target": "panicle", "relation": "expressed_in",
            "label": "expressed_in", "chain_id": "c1", "hop": 1,
            "paper_id": "…", "pmid": "…", "pmcid": "…", "doi": "…" } }
{ "chain": { "chain_id": "c1",
             "hops": [ { "source": "OsSPL14", "target": "panicle", "relation": "expressed_in",
                         "source_type": "Gene", "target_type": "Tissue",
                         "paper_id": "…", "pmid": "…", "pmcid": "…", "doi": "…" } ] } }

10. 抽取任务

上游真实结构见 DISCOVERY.md §3。上游没有 progress 数值字段,BFF 必须按 stage 派生 progress_pct。

GET /api/extraction/jobs?limit=20

上游响应 {"jobs":[...], "count":N},BFF 归一化为(count 为归一化后 jobs 数组的长度,与上游「截断后条数」语义一致):

{ "jobs": [
  { "job_id": "2943168e9c48", "title": "wang2019.pdf",
    "status": "queued|running|completed|failed",
    "stage": "queued|mineru|llm|completed|failed",
    "substage": "done|null",
    "progress_pct": 100,                     ← BFF 派生:queued=5, mineru=35, llm=75, completed=100, failed=null
    "message": "MinerU+LLM 方法学提取完成",  ← 上游中文进度描述
    "paper_id": "demo_paper", "family": "",
    "source_filename": "wang2019.pdf",
    "stage_count": 6, "species_count": 1,
    "figure_table_caption_count": 9, "figure_table_stage_count": 12,
    "updated_at": "2026-08-07T09:42:17",
    "config_url": "/api/v1/methodology/extract-jobs/{id}/bbav5-config",
    "methodology_plan_url": "/api/v1/methodology/extract-jobs/{id}/plan"
  } ],
  "count": 49 }

title 派生规则:source_filename 优先,缺失时用 paper_id,再缺用 job_id。

GET /api/extraction/jobs/{job_id}

上游返回扁平 JSON(与列表 item 同字段 + content_list、figure_table_files{...}、figure_table_warning、figure_table_inventory_url、reproduction_plan_preview_url、log_path)。BFF 归一化为:

{ "job_id": "…", "title": "…", "status": "…", "stage": "…", "progress_pct": 100,
  "message": "…", "paper_id": "…", "family": "…",
  "stats": { "stage_count": 6, "species_count": 1, "figure_table_caption_count": 9, "figure_table_stage_count": 12 },
  "detail": { …上游原始扁平 JSON 全量… },
  "plan": {…} | null }

未完成时 plan 为 null(上游返回 409,BFF 吞掉并置 null,不报错)。

GET /api/extraction/processing-status · /progress · /summary · /run-log

透传上游(全部是 JSON:前三者 {"rows":[...]},run-log 为 {"text":"..."},不是纯文本)。rows 为空属正常现象(上游批次目录未配置),前端显示空态而非错误。

POST /api/extraction/upload

multipart:file(必填,PDF 或 *_content_list.json)+ 可选 paper_id(默认 uploaded_paper)、family、output_name、llm_model、sandbox。BFF 仅限 PDF(≤50MB)。 成功(上游形态):{ "success": true, "job_id": "…", "status_url": "…", "message": "已提交抽取任务" }。 MinerU 不可用:502 降级信封,message 说明原因。读超时 600s。

上游选择:默认 8000;8000 不可达时自动回落 7900(每次请求探测,不缓存失败状态超过 30s)。注意 7900 的数据目录独立(任务列表为空属正常)。


11. 前端消费要点(给 B2/B3)

  • /api/qa/* 与 /api/qa/search 的请求 body 由 config/modes.js 拼装,question 之外字段前端不可增删。
  • SSE 消费用 fetch + ReadableStream(禁止 EventSource),解析器处理 event:/data:/: 三种行。
  • 流式持久化节奏:骨架 POST /messages → 每 3s PATCH 检查点 → 终态 PATCH。刷新中断恢复依赖此节奏。
  • 所有列表请求失败都渲染空态/离线态,禁止无限 loading 转圈。

§6 能力端点(Capability Gateway,2026-09-02 起)

GET /api/capabilities

能力清单 + 默认值 + 当前会话开关 + QA 注册表合并。

  • Query:session_id(可选;缺省返回默认开关)
  • 响应:
{
  "capabilities": [
    {"name": "memory", "label": "多轮记忆", "description": "...",
     "provider": "sdk", "session_default": true, "enabled": true},
    {"name": "web_search", "label": "联网搜索", "...": "...",
     "provider": "qa_gateway", "session_default": false,
     "gated_by": "QA_V1_ENABLE_WEB_SEARCH", "enabled": false}
  ],
  "defaults": {"memory": true, "web_search": false, "humanize": false},
  "installed": [{"name": "web_search", "description": "..."}],
  "load_warnings": []
}

POST /api/capabilities/install

聊天内自助安装(BFF 透传 QA loopback)。热加载生效,不重启。

  • 请求:{"action": "register", "manifest": "<一段 YAML>", "apply": true} 或 {"action": "unregister", "name": "<能力名>"}
  • 响应(register):{"ok": true, "installed": ["<name>"], "warnings": [], "diff": {"added": [...], "removed": [], "changed": []}, "hot_reload": true}
  • manifest 白名单:provider ∈ {http, mcp, python, prompt_pack}、 permission=read_only、单条目 ≤8KB、yaml 总大小 ≤1MB;坏条目跳过
  • 错误:400 VALIDATION(manifest 校验失败)/ 503 UPSTREAM_DOWN / 502 UPSTREAM_ERROR

PATCH /api/sessions/{id}(§5 扩展)

meta 字段新增(2026-09-02):{"meta": {"capabilities": {"web_search": true}}}。 白名单键:memory / web_search / humanize;非白名单键丢弃、值强转 bool; 与既有 meta 深度合并(title_manual 等键不受影响)。

POST /api/qa/chat 与 /api/qa/chat/stream(§4 扩展)

请求体新增 BFF 契约字段 session_id(BFF 剥离后注入上游 history/capabilities, 不达 QA)。不传 session_id 时行为与既往一致。

12. 文档生成 docgen(缺陷 #52,2026-09-10 起异步任务化)

POST /api/docgen/generate → 202

请求体同现状(description / session_id? / library_ids[]? / template_id?)。 快路径校验(description/意图护栏/配额/模板检索/资料快照)不变,通过后立即返回:

{ "ok": true, "task_id": "<uuid4>", "status": "queued" }

生成主体在后台任务执行(并发上限 DOCGEN_MAX_CONCURRENT_TASKS 默认 2; 满并发 → 429 BUSY 信封)。

GET /api/docgen/tasks/{task_id} → 200

{ "ok": true, "task": {
    "id": "<uuid4>",
    "session_id": "s1",
    "status": "queued|running|done|error",
    "progress": [ { "section": 1, "title": "…", "state": "done|running|pending" } ],
    "artifact": { "id": "…", "title": "…", "template_name": "…", "template_key": "…",
                  "has_html": true, "letterhead": "…", "file_name": "…" },
    "error": null } }
  • artifact:done 时回填制品详情;其余状态为 null。
  • error:失败时中文可读文案(DOCGEN_LLM_* 不齐时写明缺哪个键); 服务重启中断的 running/queued 任务启动时置 error「服务重启,任务中断,请重试」。
  • 任务不存在 → 404 NOT_FOUND。

GET /api/docgen/tasks?session_id={sid} → 200

{ "ok": true, "tasks": [ <task 对象列表,created_at 倒序,上限 100> ] }。

POST /api/docgen/artifacts/{id}/export 的 docx 分支(缺陷 #47a)

format:"docx" 与 PDF 同构:取 template_key/frozen_css/letterhead/template_name, bioflow 走 render_bioflow_report、其余 render_html 产 HTML → docx_from_html_document(红头红字居中 22pt 黑体、h1-h3 黑体分级、表格 Table Grid+表头灰底、图片内嵌失败落 alt)。转换异常回退 md2docx 旧路径 (env DOCGEN_DOCX_VIA_HTML=1 默认开,置 0 直接走旧路径,记 warning 日志)。