Files
PlantAI-rag-new/docs/PRD.md
T
jerryandClaude Fable 5 1e94de7464 PlantAI-rag-new 首次提交:文献知识工作台
- BFF 薄适配层(FastAPI,8090):QA 代理/SSE 中继/Neo4j 直连/抽取代理/会话持久化
- Vue3 前端:五相位检索流水线、文献来源与证据链、知识图谱、提取任务四 tab
- 四档响应式(桌面/笔记本/平板/手机)、深浅主题
- 81 个 FR 全部实现,测试追溯矩阵 81/81,后端 121 测/前端 156 测
- 真实凭据已脱敏,运行时配置在 backend/.env(不入 git)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 20:47:52 +08:00

326 lines
29 KiB
Markdown
Raw 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.
# PRD.md —— PlantAI-rag-new 产品需求文档
> 版本 1.0 | 作者 PM | 本文件由 PM 独占写入,所有开发/测试 agent **只读**
>
> **本文件是全项目需求的唯一来源。** 开发 agent 按此实现,测试 agent 按此写用例。
> 每个功能点有唯一编号 `FR-<模块>-<序号>`,测试用例必须在追溯矩阵中与之一一对应。
> 需求有歧义时,向 PM 提出,**不要自行解释**。
---
## 0. 产品定位
面向植物生物信息学研究者的**文献知识问答工作台**。用户提出关于植物基因家族的科学问题,系统从已构建的文献知识库(约 100 篇论文、44,595 条关系断言)中检索证据,生成有引用锚定的答案,并展示完整的检索过程、证据来源与知识图谱。同时提供论文抽取任务的管理入口。
**使用场景**:桌面深度研究为主,**手机端随时查询为重要补充**(实验室、会议、田间)。
**不做什么**:不做多轮对话记忆(上游 `max_rounds:1`,每次提问独立检索);不做文献全文阅读器;不做基因家族分析流程执行(那是 PlantAI-dev 的职责)。
---
## 1. 术语表
| 术语 | 含义 |
|---|---|
| 会话 Session | 一组问答的容器,有标题、状态、时间。**注意:会话是组织单位,不是上下文单位**——同一会话内的多次提问互相独立 |
| 消息 Message | 会话内的一条记录,`role` 为 `user` 或 `assistant` |
| 块 Block | 助手消息的内容单元,判别式联合类型(text/table/code/stream/pipeline/tool/sources/graph) |
| 来源 Source | 一条支撑答案的文献证据,含标题、片段、分数、PMID/DOI 等 |
| 证据链 Chain | 知识图谱上的一条多跳推理路径,由若干 hop 组成 |
| 阶段 Stage | 上游 QA 服务推送的细粒度检索步骤(十余个) |
| 相位 Phase | 前端展示用的五阶段归并(规划/检索/证据/生成/完成) |
| 上游 Upstream | 现有的三个后端服务:QA(7920)、抽取(8000/7900)、Neo4j |
| BFF | 本项目的适配层后端,前端唯一直连对象 |
---
## 2. 全局约定
### 2.1 状态枚举
**会话状态** `idle | running | done | error`
- `idle` 排队中/新建未提问 —— 空心点,ink-subtle
- `running` 有进行中的问答 —— 青色 `#00D4FF` 脉冲点
- `done` 最近一次问答成功 —— success `#2ED3A0` 实心点
- `error` 最近一次问答失败或中断 —— error `#FF5C7A` 实心点
**消息状态** `streaming | done | error`
**相位状态** `waiting | running | done`(对应字形 ◌ / ● / ✓)
### 2.2 错误处理原则
- BFF 一切错误返回统一信封 `{"ok":false,"error":{"code","message","degraded"}}`,HTTP 状态码语义正确。
- 前端**不显示原始异常堆栈**,显示 `error.message` 的中文可读文案。
- `degraded:true` 表示「上游不可用」而非「请求有误」,UI 用降级提示(横幅/离线态)而非错误弹窗。
- **失败必须可见**:任何静默吞掉错误的实现视为缺陷。
### 2.3 文案约定
用户可见文案统一走 `src/i18n/labels.js`。例外:后端返回的 `error.message` 直接展示(后端已中文化)。
### 2.4 数据不伪造原则
UI 只展示后端真实返回的数据。**禁止**:占位假数据流入生产路径、把空结果渲染成看起来有内容的样子、用动画暗示实际不存在的流式过程(客户端揭示动画是明示的视觉设计,允许,但不得声称是 token 流)。
---
## 3. 模块与编号
| 模块 | 前缀 | 范围 |
|---|---|---|
| 应用外壳 | `FR-SHELL` | 三栏布局、顶栏、状态栏、折叠、背景 |
| 主题与无障碍 | `FR-THEME` | 深浅主题、动效降级、焦点、对比度 |
| 会话管理 | `FR-SESSION` | 列表、分组、增删改查、搜索 |
| 对话与问答 | `FR-CHAT` | 消息渲染、作曲器、流式、流水线、持久化 |
| 检索过程 | `FR-RETRIEVAL` | 工作台 Tab1 |
| 文献来源 | `FR-SOURCE` | 工作台 Tab2 |
| 提取任务 | `FR-EXTRACT` | 工作台 Tab3 |
| 知识图谱 | `FR-KG` | 工作台 Tab4 |
| 响应式 | `FR-RESP` | 四档断点与移动端专项 |
| 健康与降级 | `FR-HEALTH` | 上游探活与降级行为 |
---
## 4. FR-SHELL 应用外壳
| 编号 | 功能点 | 业务逻辑与边界 | 验收要点 |
|---|---|---|---|
| **FR-SHELL-001** | 桌面三栏网格 | `grid-template-columns: 260px minmax(0,1fr) 380px`;`grid-template-rows: 48px minmax(0,1fr) 28px`;区域 `"top top top" / "side chat bench" / "status status status"`。页面本身不滚动(`body{overflow:hidden}`),滚动发生在各栏内部 | 1440×900 下三栏宽度精确;缩放窗口不出现横向滚动条 |
| **FR-SHELL-002** | 顶栏 | 高 48px。左:9px 渐变发光圆点 + 「海古纪 PlantAI」渐变文字(`background-clip:text`)。中:当前会话标题(超长省略)+ 状态徽章。右:主题切换按钮。背景 `color-mix(in srgb, var(--pa-canvas) 88%, transparent)` + `backdrop-filter: blur(14px)`,底部 1px hairline | 无会话时中部显示「就绪」徽章;标题超长省略而非换行 |
| **FR-SHELL-003** | 底部状态栏 | 高 28px。左:4 个 7px agent 状态点(规划/检索/证据/生成),与流水线相位联动。右:`PlantAI RAG v0.1` + mono 的 QA 版本号 + 会话数 + 上游健康标记 | 问答进行中四点按相位依次点亮;QA 版本号来自 `/api/qa/info` |
| **FR-SHELL-004** | 左栏折叠 | 折叠后宽 64px 图标轨,显示 3 个 34×34 图标按钮(会话/图谱/设置),当前项左侧 2px 渐变竖条。折叠状态写入 localStorage `pa-side-collapsed` | 折叠/展开过渡 300ms `ease-out`;刷新后保持 |
| **FR-SHELL-005** | 右栏折叠 | 折叠后保留 28px 手柄条(**折叠 ≠ 隐藏**),点击手柄重新展开。状态写入 localStorage `pa-bench-collapsed` | 折叠后手柄可见可点;刷新后保持 |
| **FR-SHELL-006** | Aurora 背景 | 固定层,`z-index:0`,`pointer-events:none`。两个 560/620px 渐变光斑,`blur(120px)`,`aurora-drift` 16s 往复;叠加 48px 点阵,顶部渐隐遮罩。浅色主题下 `--pa-aurora-opacity:.08` | 不遮挡任何交互;浅色下明显衰减 |
| **FR-SHELL-007** | 首屏初始化 | 顺序:读 localStorage 主题并写 `documentElement.dataset.theme`(**必须在挂载前**,避免闪白)→ 挂载应用 → 并行请求会话列表与 `/api/health`、`/api/qa/info` → 无会话则显示欢迎空态 | 深色主题下刷新无白屏闪烁;接口失败不阻塞渲染 |
---
## 5. FR-THEME 主题与无障碍
| 编号 | 功能点 | 业务逻辑与边界 | 验收要点 |
|---|---|---|---|
| **FR-THEME-001** | 双主题等价 | 深色为默认。浅色**不是降级稿**,是同一套语义的另一组令牌值。所有组件必须在两套主题下都正确 | 逐组件走查两套主题无失色、无对比度不足 |
| **FR-THEME-002** | 切换与持久化 | 顶栏按钮切换,写 `localStorage['pa-theme']`,同步 `documentElement.dataset.theme`。图标日/月互换并 rotate 180°(400ms) | 切换即时生效;刷新保持 |
| **FR-THEME-003** | 浅色 glow 衰减 | 浅色下所有辉光乘 `--pa-glow-opacity:.35`——浅底上不衰减会糊 | 浅色下按钮悬停辉光明显弱于深色 |
| **FR-THEME-004** | 动效降级 | `@media (prefers-reduced-motion: reduce)` 下关闭全部 animation/transition;状态改由静态符号表达(实心/空心/✓/✕),不依赖运动传达信息 | 开启系统「减少动态效果」后无任何动画,且状态仍可辨认 |
| **FR-THEME-005** | 焦点与键盘 | `:focus-visible` 输出 2px `rgba(94,106,210,.5)` 焦点环,offset 1px。所有交互元素键盘可达,Tab 顺序符合视觉顺序 | 纯键盘可完成:新建会话→输入→发送→切换 tab |
| **FR-THEME-006** | 对比度 | 深色 ink/canvas ≈15:1,ink-muted/canvas ≥4.5:1;浅色同标准 | 抽检主要文本对比度达标 |
---
## 6. FR-SESSION 会话管理
| 编号 | 功能点 | 业务逻辑与边界 | 验收要点 |
|---|---|---|---|
| **FR-SESSION-001** | 列表与分组 | 按 `updated_at` 倒序,分「今天 / 昨天 / 更早」三组,组标签 11px/500/字距0.6px。空列表显示引导文案 | 跨零点后分组正确迁移 |
| **FR-SESSION-002** | 状态点四态 | 见 §2.1。`running` 脉冲动画,其余静态 | 四种状态视觉可区分,且不仅靠颜色(形状:空心/实心 + 动/静) |
| **FR-SESSION-003** | 新建会话 | 顶部渐变按钮(40px 高)。行为:调 `POST /api/sessions` 创建 → 切为活动会话 → 对话区显示欢迎空态。**不预先落一条空消息** | 连点两次产生两个独立会话,不重复 |
| **FR-SESSION-004** | 选择与加载 | 点击会话 → `GET /api/sessions/{id}` 拉取消息 → 渲染历史。加载中显示骨架屏。切换会话时若有进行中的流,**中断该流**并把原会话置 `error`(标注「已中断」) | 历史消息含表格/图谱块能正确重渲染 |
| **FR-SESSION-005** | 搜索过滤 | 搜索框对 `title` 做不区分大小写的子串匹配,实时过滤(前端过滤,不发请求)。无匹配显示「无匹配会话」 | 输入中文/英文均可过滤;清空恢复全量 |
| **FR-SESSION-006** | 重命名 | 双击标题或右键菜单进入编辑,回车提交 `PATCH /api/sessions/{id}`,Esc 取消。空标题不允许提交 | 重命名后列表与顶栏标题同步更新 |
| **FR-SESSION-007** | 删除 | 需二次确认。确认后 `DELETE /api/sessions/{id}`(级联删消息)。若删的是当前会话,切到列表中最新一个;列表空则显示欢迎空态 | 删除后刷新不再出现;消息一并清除 |
| **FR-SESSION-008** | 标题自动生成 | 新会话首次提问后,用问题前 30 字符(超出加省略号)作为标题,调 `PATCH` 落库。用户手动改过标题的会话(`meta.title_manual=true`)不再自动覆盖 | 首问后标题自动变化;手动改名后再提问不被覆盖 |
| **FR-SESSION-009** | 活动态高亮 | 左侧 2px 渐变竖条 + `color-mix(in srgb, var(--pa-g-to) 8%, transparent)` 底 + 22% 内描边 | 活动项一眼可辨,与悬停态区分 |
---
## 7. FR-CHAT 对话与问答(核心模块)
### 7.1 基础渲染
| 编号 | 功能点 | 业务逻辑与边界 | 验收要点 |
|---|---|---|---|
| **FR-CHAT-001** | 欢迎空态 | 消息为空时显示:46px 渐变菱形(✧)+ 渐变标题「海古纪 PlantAI 知识工作台」+ 副标题 + **3 张建议卡**(示例问题)。点击建议卡直接以该文本提问 | 卡片悬停上浮 2px + 青色微光;点击即发起真实提问 |
| **FR-CHAT-002** | 用户消息 | 右对齐,最大宽 70%,渐变底(深色 `135deg,#2A2A3E,#1C1C2A`;浅色 `#E8ECFF,#F4FBFF`)+ hairline-strong 边,r12,padding 10/15px | 长文本正确换行不溢出 |
| **FR-CHAT-003** | 助手消息 | **无底色全文**(Linear 式),左缘 2px 透明竖线悬停变渐变。头部:24px 渐变头像(✦)+ 「PlantAI 智能体」+ 时间。内容按块联合顺序渲染 | 多块混排顺序正确 |
| **FR-CHAT-004** | Markdown 渲染 | `markdown-it` 解析 → DOMPurify 消毒 → `v-html`。表格加 `md-table` 类:thead 用 2px 渐变下边框,数据格 mono(表头为「说明」的列除外,保持比例字体);代码块 code-bg + hairline + mono 12.5px | LLM 输出的表格/列表/加粗正确渲染;**注入脚本被消毒** |
| **FR-CHAT-014** | 自动滚动 | 新消息或流式更新时滚到底。**例外**:用户手动向上滚动后暂停自动滚动,并显示「回到最新」按钮;点击或用户滚回底部后恢复 | 阅读历史时不被强制拽到底 |
| **FR-CHAT-015** | 内联引用 | 答案文本中的引用标记渲染为可点 chip,点击滚动定位到工作台「文献来源」中对应条目并高亮 2s | 引用 chip 与来源列表编号一致 |
### 7.2 作曲器
| 编号 | 功能点 | 业务逻辑与边界 | 验收要点 |
|---|---|---|---|
| **FR-CHAT-005** | 输入框 | r16 surface-1 卡片,`:focus-within` 渐变描边 + 青色辉光。textarea 无边框透明,14px/1.6,min-height 46px,**max-height 160px 后内部滚动**。占位符提示示例问题 | 多行输入自动增高至上限后滚动 |
| **FR-CHAT-006** | 三种模式 | pill 切换,参数集定义在 `config/modes.js`:<br>**问答** `{use_llm:true}` → `/api/qa/chat/stream`<br>**方案** `{use_llm:true, native_chain_enabled:true, native_chain_max_hops:3, native_chain_top_k:5}` → 同上,深证据链<br>**检索** `{use_llm:false}` → `/api/qa/search`,只出来源不出答案<br>激活态渐变底 + 白字 + 青光 | 三种模式发出的请求体字段正确;检索模式不产生答案块 |
| **FR-CHAT-007** | 发送与快捷键 | `Enter` 换行,`⌘/Ctrl+Enter` 发送。发送按钮渐变 pill,空输入或忙碌时 `disabled`(opacity .45)。底部提示文案说明快捷键 | 快捷键在中英文输入法下均生效;空输入不可发送 |
| **FR-CHAT-011** | 停止/中断 | 流式进行中,发送按钮变为「停止」。点击 → `AbortController.abort()` → 关闭 SSE → 当前消息置 `error` 并标注「已停止」→ 已收到的块保留不丢 | 停止后 BFF 侧上游连接也关闭(不留悬挂请求) |
### 7.3 流式问答(最复杂的部分,详细说明)
**FR-CHAT-008 流式问答主流程**
业务逻辑,按时序:
1. 用户发送 → 前端立即在界面追加用户消息气泡 → `POST /api/sessions/{id}/messages` 落库(`role:user`)
2. 会话状态置 `running`,左栏状态点开始脉冲,底栏 agent 点复位
3. 前端追加一条助手消息骨架(`status:streaming`),内含一个 `pipeline` 块,五相位全为 `waiting`
4. `fetch` + `ReadableStream` 打开 `POST /api/qa/chat/stream`(**不能用 EventSource**——请求是带 JSON body 的 POST)
5. 逐帧消费:
- `event: progress` → 取 `phase` 字段(**BFF 已计算好,前端不解析上游 stage 名**)→ 更新 `pipeline` 块对应相位为 `running`,此前相位置 `done`;同时把该事件的 `details` 追加为工作台「检索过程」的一张阶段卡
- `event: complete` → payload 即完整归一化响应 → 用 `answer` 生成 `text` 块(走揭示动画)、`sources` 生成 `sources` 块、图谱数据生成 `graph` 块;`pipeline` 全部置 `done`
- `event: error` → 消息置 `error`,展示 `error.message`,保留已收到的块
- `event: timeout` → 同 error,文案为「检索超时」
- `event: raw` → 记入 `raw`,不渲染
- `:` 注释帧(keepalive)→ 忽略
6. 每 ~3s 调 `PATCH /api/sessions/{id}/messages/{mid}` 做检查点持久化
7. 终态:消息置 `done`/`error` 落库,会话状态同步,底栏 agent 点定格
边界与异常:
- **上游最长 240s**:期间必须持续有视觉反馈(相位卡 + shimmer + 已耗时计数),不得让用户面对静止界面
- **网络中断**(移动端切网常见):`fetch` reject → 置 `error`,文案「连接中断」,提供「重试」按钮。**不自动重连**——每次 LLM 调用昂贵,自动重试会放大成本
- **页面刷新中途**:重载后显示最后检查点内容 + 「已中断」标注,会话状态 `error`
- **同一会话并发提问**:忙碌时发送按钮 disabled,从交互上杜绝
**FR-CHAT-009 五阶段流水线卡**
| 相位 | 颜色令牌 | 上游 stage(由 BFF 归并) |
|---|---|---|
| 规划 | `--pa-t-planning` `#C0A8DD` | `understanding_started`、`entities_identified`、`related_search_started` |
| 检索 | `--pa-t-execution` `#9FBBE0` | `concepts_found`、`subquestion_retrieved`、`agent_review_started`、`subquestion_retry_started` |
| 证据 | `--pa-t-knowledge` `#9FC9A2` | `evidence_accepted`、`evidence_review_completed` |
| 生成 | `--pa-t-validation` `#DFA88F` | `answer_generation_started` |
| 完成 | `--pa-t-done` `#C08532` | `completed` |
渲染:纵向时间线,每相位 = 16px 状态点(✓ done / ● running / ◌ waiting)+ 2px 连接线 + 相位名 + 描述(取自最近一次该相位事件的摘要)+ 右侧状态文字;running 相位额外显示 11px 旋转 spinner。卡片头部「⚙ 检索流水线」+ 整体状态徽章(语义色)。
**关键约束**:相位色只回答「哪一步在做什么」,语义色只回答「整体成败」,两者不得混用。
**FR-CHAT-010 答案揭示动画**
上游**不做 token 级流式**,完整答案在 `complete` 一次到达。前端以 30ms/字符的速度揭示,尾部显示 8×15px 青色闪烁光标(`blink` 1s),下方显示 3px shimmer 进度线。揭示期间用户可点击「跳过」立即显示全文。揭示结束替换为最终 `text` 块。
边界:答案超过 2000 字符时,揭示速度自动加快(总时长上限 8s),避免长答案等待过久。
**FR-CHAT-012 错误与重试**
- 上游返回 HTTP≥400 且 `detail.error_code == "invalid_llm_citation"`:BFF 已自动重试一次;若仍失败,前端展示「引用校验失败,请重试」并提供重试按钮
- 其他错误:展示 `error.message`,提供重试按钮
- 重试 = 用相同问题与模式重新发起,**在同一条助手消息位置替换**,不新增用户消息
**FR-CHAT-013 消息持久化与恢复**
- 块联合完整落库(`stream` 块为瞬态,**不落库**,落库时已转为 `text`)
- `raw` 字段保存完整归一化响应,供排查与重渲染
- 刷新后从 `GET /api/sessions/{id}` 恢复全部块,包括表格、来源、图谱
---
## 8. FR-RETRIEVAL 检索过程(工作台 Tab1)
| 编号 | 功能点 | 业务逻辑与边界 | 验收要点 |
|---|---|---|---|
| **FR-RETRIEVAL-001** | 阶段卡列表 | 每个 SSE `progress` 事件生成一张卡,沿用原型「工具调用卡」视觉:折叠头(相位色图标 + 阶段中文名 + 状态点 + 耗时 mono)+ 展开体(`details` 以 mono 参数块呈现 + 摘要行) | 一次问答后卡片数量与收到的 progress 事件数一致 |
| **FR-RETRIEVAL-002** | 相位筛选 | chip:全部/规划/检索/证据/生成。激活态实心 `#5E6AD2` 底 + 白字 | 筛选后仅显示对应相位卡片 |
| **FR-RETRIEVAL-003** | 展开折叠 | 点击头部切换,chevron 旋转 90°,展开体 `fade-slide` 200ms。**运行中的卡默认展开**,完成后自动折叠 | 展开状态互相独立 |
| **FR-RETRIEVAL-004** | 运行中效果 | 运行中卡片头部 shimmer 扫光 1.8s 循环 + 状态点脉冲 | 完成后动效停止 |
| **FR-RETRIEVAL-005** | 检索计划 | 若响应含 `retrieval_plan`,在列表顶部以只读信息块展示;另展示 `/api/qa/info` 的 12 段 pipeline 描述作为静态说明 | 无 plan 时该区域不显示(不留空框) |
| **FR-RETRIEVAL-006** | 空态 | 未提问时显示「发起提问后,这里会实时展示检索过程」 | 空态不显示假卡片 |
---
## 9. FR-SOURCE 文献来源(工作台 Tab2)
| 编号 | 功能点 | 业务逻辑与边界 | 验收要点 |
|---|---|---|---|
| **FR-SOURCE-001** | 来源列表 | 沿用原型「产物文件」的 mono 列表视觉。每条:序号 + 标题(超长省略)+ mono 元信息行(`PMID:xxx` / `DOI:xxx` / 分数)+ 证据片段(默认 2 行截断,点击展开) | 条目数与 `source_count` 一致 |
| **FR-SOURCE-002** | 字段展示 | 展示归一化后的 `title / snippet / score / pmid / pmcid / doi / section / support_count`。缺失字段不显示占位符,**不显示 `undefined`/`null`** | 各字段缺失组合下渲染正常 |
| **FR-SOURCE-003** | 外链跳转 | 链接优先级:`url` > `doi`(拼 `https://doi.org/{doi}`)> `pmid`(拼 PubMed)> `pmcid`(拼 PMC)。均无则不显示链接图标。新标签页打开,带 `rel="noopener"` | 点击真实跳转到正确文献页 |
| **FR-SOURCE-004** | 证据链 | `chains` 按 `chain_id` 分组,每组可展开为逐跳列表:`源实体 —[关系]→ 目标实体`,每跳附 paper_id/PMID。跳数以徽章显示 | 多跳链条展开顺序正确 |
| **FR-SOURCE-005** | 会话级聚合 | 顶部切换「本次回答 / 本会话全部」。后者调 `GET /api/sessions/{id}/sources`,按 `paper_id` 去重并累计出现次数 | 去重正确,计数准确 |
| **FR-SOURCE-006** | 空态 | 无来源时显示「本次检索未命中文献」,**不显示空列表框** | 空结果不误导为加载中 |
---
## 10. FR-EXTRACT 提取任务(工作台 Tab3)
| 编号 | 功能点 | 业务逻辑与边界 | 验收要点 |
|---|---|---|---|
| **FR-EXTRACT-001** | 任务列表 | `GET /api/extraction/jobs`。每条沿用原型进度条视觉:任务名 + mono 百分比 + 6px 轨道 + 渐变填充 + sweep 扫光;100% 转为纯 success 色且停止扫光。**上游无 progress 数值**,百分比由 BFF 按 `stage` 派生(queued=5 / mineru=35 / llm=75 / completed=100,failed 显示失败态不显示进度条) | 列表与上游一致;派生数值正确;failed 任务呈现为错误态 |
| **FR-EXTRACT-002** | 任务详情 | 点击展开 `GET /api/extraction/jobs/{id}`,展示状态、message(中文进度描述)、统计字段(stage/species/figure_table 计数)、plan(仅 completed 有,未完成置 null 不显示) | 详情字段与上游一致;未完成任务不显示 plan 且不报错 |
| **FR-EXTRACT-003** | 运行日志 | `GET /api/extraction/run-log`,mono 等宽展示,最大高度内滚动,自动贴底 | 长日志不撑破布局 |
| **FR-EXTRACT-004** | 论文上传 | 作曲器区域附件按钮 → 选 PDF → `POST /api/extraction/upload`(multipart)→ 显示上传进度 → 成功返回 job_id 并刷新列表。**限制**:仅 PDF,单文件 ≤50MB。上游当前环境 MinerU 二进制路径缺失会导致新任务 failed(历史 27 个 failed 任务同因)——若发生,展示上游 message 的明确错误文案 | 非 PDF 被拒;超限被拒;错误文案可读 |
| **FR-EXTRACT-005** | 轮询刷新 | 存在 `running` 状态任务时每 5s 轮询列表;全部终态则停止轮询。切到其他 tab 时暂停轮询 | 无进行中任务时不空转 |
| **FR-EXTRACT-006** | 离线降级 | 抽取上游不可用时显示离线提示与重试按钮,不影响其他 tab | 8000/7900 全停时该 tab 优雅降级 |
---
## 11. FR-KG 知识图谱(工作台 Tab4)
**双数据源策略**(重要):
- **答案图谱**:来自当前消息的 `graph` 块(由 QA 响应的 `knowledge_graph`/`native_graph_chains` 归一化而来)。永远可用,Neo4j 挂了也不受影响。
- **自由浏览**:来自 `GET /api/kg/*`,BFF 用 Bolt 直连 Neo4j。上游 `/api/v1/kg/*` 已损坏(指向未运行的 7687),不可依赖。
| 编号 | 功能点 | 业务逻辑与边界 | 验收要点 |
|---|---|---|---|
| **FR-KG-001** | 答案图谱渲染 | 手写 SVG 力导向布局。节点圆点 + 标签,边为渐变线(`#5E6AD2→#00D4FF`)带关系名。节点数 >50 时自动降级为列表视图并提示 | 图谱与答案引用的实体一致 |
| **FR-KG-002** | 节点着色与图例 | 按实体 `type` 着色,用品牌色 + 辅助紫 + 五相位色构成调色板,底部图例。**同一 type 在会话内颜色稳定** | 图例与节点颜色对应 |
| **FR-KG-003** | 节点详情 | 点击节点 → 侧滑详情面板,展示实体属性;若 Neo4j 可用,额外调 `GET /api/kg/entities/{name}` 拉全量属性 | Neo4j 不可用时仍能显示消息内已有属性 |
| **FR-KG-004** | 实体搜索 | 搜索框 → `GET /api/kg/entities?q=` → 按 `name_norm` 精确/前缀匹配(走 name_norm 索引,0.1s 内返回)→ 选中渲染其邻域。**注意上游无全文索引**,搜索是精确/前缀匹配,非模糊全文检索 | 中英文实体名(精确或前缀)均可搜到;响应 <1s |
| **FR-KG-005** | 邻域展开 | `GET /api/kg/entities/{name}/neighborhood?depth=&limit=`。深度选择 1/2,默认 1(**两跳实测约 13s**,需提示较慢)。节点上限 200,每跳按 paper_count/support_count 降序截断防 hub 吃满预算 | 深度切换生效;超限有提示不卡死 |
| **FR-KG-006** | 缩放与平移 | 滚轮/双指缩放(0.3×–3×),拖拽平移,双击复位。**触摸端必须支持双指缩放与单指拖拽** | 手机上可流畅缩放平移 |
| **FR-KG-007** | 离线降级 | Neo4j 不可用时:搜索与邻域功能禁用并提示离线;**答案图谱照常渲染**(这是双数据源的意义) | 断开 Neo4j 后 tab 不白屏 |
| **FR-KG-008** | 空态 | 本次回答无图谱数据时提示「本次检索未产生图谱关系」,并引导使用实体搜索 | 空态不显示空画布 |
---
## 12. FR-RESP 响应式(手机为一等公民)
### 12.1 断点
| 编号 | 档位 | 宽度 | 布局规则 |
|---|---|---|---|
| **FR-RESP-001** | Desktop | ≥1280px | 完整三栏 260/1fr/380 |
| **FR-RESP-002** | Laptop | 1024–1279px | 三栏保留;左栏**自动**折叠为 64px 轨;右栏收窄至 340px |
| **FR-RESP-003** | Tablet | 768–1023px | 对话单栏铺满;左栏改**左抽屉**(scrim 遮罩 + 滑入),顶栏加汉堡按钮;右栏改**右抽屉**,顶栏加工作台按钮并显示来源数角标 |
| **FR-RESP-004** | Mobile | <768px | 单栏;顶栏 = 汉堡 + 标题 + 工作台按钮;对话铺满;作曲器吸底;工作台为**全屏覆盖层 + 顶部 tab 条**;底部状态栏隐藏,四个 agent 点合并为顶栏一枚聚合状态点 |
抽屉与覆盖层动效沿用 `ease-out` 300ms;打开时 scrim 淡入,点击 scrim 或按 Esc 关闭。
### 12.2 移动端专项
| 编号 | 功能点 | 业务逻辑与边界 | 验收要点 |
|---|---|---|---|
| **FR-RESP-005** | viewport 与安全区 | `<meta viewport ... viewport-fit=cover>`;作曲器 `padding-bottom: env(safe-area-inset-bottom)`;顶栏 `env(safe-area-inset-top)` | iPhone 刘海/Home 条不遮挡内容 |
| **FR-RESP-006** | iOS 输入放大规避 | <768px 时 textarea `font-size:16px`(低于 16px 必触发 iOS 自动缩放)。**禁止**用 `maximum-scale=1` 规避——伤害无障碍 | iPhone Safari 聚焦输入框页面不放大 |
| **FR-RESP-007** | 悬停态触摸等价 | 所有 hover 样式包在 `@media (hover: hover)` 内。触摸设备下:来源外链图标常驻可见、助手消息左缘线常驻、会话条目去掉悬停底色改用点击反馈 | 手机上无「需要悬停才出现」的功能 |
| **FR-RESP-008** | 触控目标 | <768px 下会话条目、bench tab、模式 pill、图标按钮 `min-height:44px`(iOS HIG 标准,高于 DESIGN.md 的 36px) | 手机上不误触 |
| **FR-RESP-009** | 视口高度 | 用 `100dvh`,保留 `100vh` 回退,规避移动浏览器地址栏伸缩导致的高度跳变 | 滚动时布局不跳动 |
| **FR-RESP-010** | 虚拟键盘 | 监听 `visualViewport` 的 resize:键盘弹出时压缩消息区高度并保持贴底,作曲器始终可见 | 键盘弹出不遮挡输入框 |
| **FR-RESP-011** | 表格横向滚动 | <768px 下答案内 `md-table` 包一层横向滚动容器,右侧加渐隐提示。**不压缩字号**(可读性优先) | 宽表格可横滑查看完整内容 |
| **FR-RESP-012** | 移动端性能降级 | <768px 下 aurora 模糊半径降低或 `--pa-aurora-opacity:0`;图谱节点数上限降至 30 | 中端手机滚动流畅无卡顿 |
| **FR-RESP-013** | 手势(可选) | 左缘右滑开会话抽屉,右滑关工作台覆盖层。**不作为验收门槛** | 实现则可用,不实现不算缺陷 |
---
## 13. FR-HEALTH 健康与降级
| 编号 | 功能点 | 业务逻辑与边界 | 验收要点 |
|---|---|---|---|
| **FR-HEALTH-001** | 健康轮询 | 挂载后请求 `GET /api/health`,此后每 30s 一次。结果写入 store,驱动状态栏标记与各降级态。轮询失败不弹窗,仅标记 | 上游恢复后 30s 内自动解禁 |
| **FR-HEALTH-002** | QA 不可用 | `/api/qa/*` 返 503 degraded:作曲器禁用并显示「知识问答服务当前不可用」,历史会话仍可浏览 | 停掉 7920 后 UI 明确提示而非静默失败 |
| **FR-HEALTH-003** | Neo4j 不可用 | 图谱 tab 搜索/邻域禁用并提示离线;答案图谱不受影响 | 见 FR-KG-007 |
| **FR-HEALTH-004** | 抽取不可用 | 见 FR-EXTRACT-006 | 仅该 tab 降级 |
| **FR-HEALTH-005** | 网络中断 | 流式中断时提示「连接中断」+ 重试按钮;**不自动重连**(避免重复触发昂贵 LLM 调用) | 手机切换网络后有明确提示 |
---
## 14. 非功能要求
| 项 | 要求 |
|---|---|
| 首屏可交互 | 桌面 <2s(本地网络),手机 <3s |
| 长任务反馈 | QA 最长 240s,期间必须持续有视觉反馈,无静止超过 5s 的状态 |
| 数据安全 | Neo4j 凭据只存在于 BFF 配置,**绝不下发到浏览器**;LLM 输出必须经 DOMPurify 消毒后才 `v-html` |
| 离线容忍 | 前端零外网请求(无 Google Fonts、无 CDN),符合 DESIGN.md 禁令 |
| 浏览器支持 | Safari 16+(iOS)、Chrome 110+、Firefox 110+ |
---
## 15. FR 索引(供追溯矩阵使用)
SHELL: 001–007(7)|THEME: 001–006(6)|SESSION: 001–009(9)|CHAT: 001–015(15)|RETRIEVAL: 001–006(6)|SOURCE: 001–006(6)|EXTRACT: 001–006(6)|KG: 001–008(8)|RESP: 001–013(13)|HEALTH: 001–005(5)
**合计 81 个功能点。** 测试用例追溯矩阵必须覆盖全部 81 项(FR-RESP-013 标注为可选,仍需有用例但允许标记为 N/A)。