Files

533 lines
26 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.
# 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 与降级响应):
```json
{ "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` 数组元素为判别式联合:
```jsonc
{ "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 缓存)。
```json
{
"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` 并精简:
```json
{
"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` 拼出):
```json
{
"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 降级体**(不返回错误信封):
```json
{ "mode": "search", "answer": "", "sources": [], "source_count": 0,
"degraded": true, "reason": "TIMEOUT" }
```
正常空结果(未超时但无命中)`degraded:false`——前端据此区分「检索超时」与「没有检索到有效回答」。
---
## 5. 会话与消息
### GET /api/sessions
```json
[ { "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`:
```json
{ "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`(出现次数):
```json
{ "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
```json
{ "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` 统计:
```json
{ "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 索引:
```json
{ "query": "OsSPL14", "results": [ { "id": "cent_xxx", "name": "OsSPL14", "type": "gene", "support_count": 12 } ] }
```
### GET /api/kg/entities/{id}
`id` 为 `canonical_id`(`cent_*`)或名字(先按 `name_norm` 定位):
```json
{ "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 吃满预算):
```json
{ "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 共用)
```json
{
"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(来源)结构
```json
{
"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)
```json
{ "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 数组的长度**,与上游「截断后条数」语义一致):
```json
{ "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 归一化为:
```json
{ "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`(可选;缺省返回默认开关)
- 响应:
```json
{
"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/意图护栏/配额/模板检索/资料快照)不变,通过后立即返回:
```json
{ "ok": true, "task_id": "<uuid4>", "status": "queued" }
```
生成主体在后台任务执行(并发上限 `DOCGEN_MAX_CONCURRENT_TASKS` 默认 2;
满并发 → 429 `BUSY` 信封)。
### GET /api/docgen/tasks/{task_id} → 200
```json
{ "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 日志)。