# Research 会话拆分与记录化改造设计方案

> 文档性质：前后端设计与实施方案。
>
> 更新日期：2026-03-25。
>
> 对应需求文档：prd/research-session-split-prd.md。

---

## 1. 设计目标

本方案用于支撑以下能力落地：

1. Research 从单页工作台拆分为“列表 / 新建 / 详情”三页。
2. 引入服务端研究记录模型，替代仅依赖前端 localStorage 的状态管理。
3. 将研究执行升级为“Celery 后台任务 + Redis Stream + SSE 订阅”模型。
4. 扩展“汇报提纲”“讲话稿”两个输出模板。
5. 为后续澄清、研究后对话、重新生成保留数据模型与路由扩展位。

---

## 2. 当前实现基线

当前系统已具备的可复用能力：

### 2.1 前端

- 现有单页 Research 工作台：`frontend/src/views/ResearchView.vue`
- 本地 Research store：`frontend/src/stores/research.ts`
- Research API 封装：`frontend/src/api/research.ts`
- Research 类型定义：`frontend/src/types/research.ts`
- 报告导出工具：`frontend/src/utils/researchExport.ts`

### 2.2 后端

- Research API：`backend/app/api/v1/research.py`
- Research Schema：`backend/app/api/schemas/research.py`
- Research Engine：`backend/app/core/research_engine.py`
- Prompt 模板：`backend/app/prompts/research_prompts.py`
- 现有 session store：`backend/app/infrastructure/session_store.py`

### 2.3 基线判断

当前系统已经可以支撑“计划生成、研究执行、章节重跑、报告输出”。

真正缺失的是：

1. 后端研究记录实体与列表/详情查询 API。
2. 计划前澄清与研究后对话的独立协议。
3. 重新生成的版本关系。
4. 前端页面层级拆分后的新状态组织方式。

---

## 3. 总体架构设计

整体采用“保留现有引擎能力，在其外增加记录层与交互层”的策略。

### 3.1 分层

1. 记录层
   - 负责研究记录、报告子表、运行实例、状态与摘要字段。
2. 执行层
   - 复用现有 `build_plan`、`run_deep_research`、`rerun_section` 能力。
3. 任务层
   - 负责 Celery 投递、Worker 执行、Redis Stream 事件写入与 SSE 订阅。
4. 展示层
   - 前端三页结构与详情工作台展示。
5. 扩展层
   - 为后续澄清、研究后对话、重新生成预留接口与字段位置，但不纳入 Phase 1 实施范围。

### 3.2 核心设计原则

1. 不推翻现有 `ResearchEngine`，而是在其前后增加记录编排与后台任务层。
2. 研究记录、完整报告、运行实例分离建模，避免把页面临时状态直接当作持久化模型。
3. 执行生命周期独立于当前 SSE 连接，页面刷新不影响后端继续执行。
4. 重新生成、澄清、研究后对话保留扩展位，但不阻塞 Phase 1 主链路落地。

---

## 4. quick / deep 模式的技术定位

当前代码中 `ResearchTask.mode` 已暴露 `quick | deep`，但从执行实现看，现阶段主要仍复用同一套深度研究执行链路：

1. 后端核心执行入口仍是 `run_deep_research`
2. `task.mode` 当前主要作为任务语义输入参与计划与报告生成
3. 还没有完全独立的 quick 专属执行器、专属检索预算或专属输出协议

因此，新架构需要把 quick 的技术定位定义清楚。

### 4.1 quick 模式

quick 在技术上应被实现为“轻量化的 Research 策略”，而不是新建一套完全独立的系统。

建议 quick 复用同一套记录、计划、执行、详情页能力，但在以下维度做约束：

1. 澄清轮次更少
2. 检索文档预算更小
3. 图谱补证更克制
4. 计划结构更短
5. 章节数量更少
6. 结果摘要优先级更高

### 4.2 deep 模式

deep 继续作为完整研究策略：

1. 更完整的问题拆解
2. 更充分的材料覆盖
3. 更强的冲突识别和开放问题输出
4. 更适合复杂政策链条和执行研判

### 4.3 设计决策

本方案不建议为 quick 单独复制一套前后端页面和 API。

建议做法：

1. quick / deep 共用研究记录模型
2. quick / deep 共用列表页和详情页
3. 通过 mode 驱动 plan 策略、执行预算和输出结构差异

### 4.4 当前态与目标态

当前态：

1. quick 只是字段级语义标记，尚未完整映射到执行预算
2. deep 是事实上的主执行路径

目标态：

1. quick 成为正式的轻量研究模式
2. deep 成为完整的复杂研究模式
3. 两者共享架构但策略不同

---

## 5. 后端设计

## 5.1 数据模型

建议新增 `research_records` 表，第一版采用“主表 + 大字段按需加载”的模式，避免把所有内容都堆到一张记录摘要表中。

建议字段：

- `id`：VARCHAR，主键
- `user_id`：VARCHAR，所属用户
- `session_id`：VARCHAR，关联现有运行时会话 ID
- `title`：VARCHAR，研究标题
- `mode`：VARCHAR，研究模式
- `status`：VARCHAR，状态
- `output_template`：VARCHAR，输出模板
- `summary`：TEXT，列表摘要
- `archived`：BOOLEAN，是否归档
- `task_json`：JSON，研究任务
- `plan_json`：JSON，研究计划
- `references_json`：JSON，研究引用摘要
- `imported_items_json`：JSON，显式导入资料
- `notes`：TEXT，备注
- `parent_record_id`：VARCHAR，父版本记录 ID
- `root_record_id`：VARCHAR，根记录 ID
- `version_no`：INT，版本号
- `last_error`：TEXT，最后错误信息
- `created_at`：DATETIME
- `updated_at`：DATETIME
- `completed_at`：DATETIME NULL

建议将完整报告拆到独立载体，例如：

1. `research_record_reports` 子表，按 `record_id` 存储 `report_json`
2. 或对象存储 / 文件存储，主表只保留摘要与索引字段

若采用子表方案，建议最小 Schema 为：

1. `record_id`：VARCHAR，主键或唯一键，关联 `research_records.id`
2. `report_json`：JSON，完整研究报告
3. `created_at`：DATETIME
4. `updated_at`：DATETIME

此外，Phase 1 需要新增 `research_record_runs` 表，最小字段建议为：

1. `run_id`：VARCHAR，主键
2. `record_id`：VARCHAR，关联 `research_records.id`
3. `user_id`：VARCHAR，所属用户，用于 run 层隔离
4. `run_type`：VARCHAR，`full` / `section_rerun`
5. `status`：VARCHAR，`pending` / `running` / `completed` / `failed`
6. `section_title`：VARCHAR，可空，仅章节重跑时有值
7. `started_at`：DATETIME NULL
8. `completed_at`：DATETIME NULL
9. `error`：TEXT NULL
10. `created_at`：DATETIME

这样可以避免：

1. 主表行过大
2. 列表查询误带大字段
3. 频繁更新报告时影响列表查询效率

### 5.1.1 状态枚举建议

- `draft`
- `planned`
- `running`
- `cancelled`
- `completed`
- `failed`

说明：

1. `cancelled` 在 Phase 1 仅保留枚举，不实现 API。
2. `clarifying` 保留为后续扩展状态，本期主流程不进入该状态。

### 5.1.2 为什么不直接复用 `research_sessions`

现有 `research_sessions` 仅保存历史问答轮次，接口语义是“追加对话历史”，不包含：

- 研究标题
- 输出模板
- 计划对象
- 报告对象
- 版本关系
- 列表摘要

因此不适合直接承载研究记录列表与详情。

### 5.1.3 `session_id` 关联建议

新的 `research_records` 不应替代现有 `research_sessions` 的运行期语义，而应与其关联。

建议：

1. `research_records.session_id` 保留为可空扩展位，本期不作为主执行依赖
2. Phase 1 的执行恢复以 `research_record_runs` + Redis Stream 为主
3. 记录层负责元数据与产物，run 层负责后台执行状态

这样可以把“记录态”和“执行态”衔接起来，而不是二选一。

### 5.1.4 `root_record_id` 建议

只依赖 `parent_record_id` 会导致版本链查询成本高。

因此建议增加：

1. `parent_record_id`：直接父版本
2. `root_record_id`：整条研究链的根 ID

这样可直接查询同一研究主题下的所有版本，无需递归。

### 5.1.5 mode 字段建议

记录层应显式保存 `mode`，因为它不仅是前端表单值，也会影响：

1. 澄清轮次策略
2. 计划生成策略
3. 执行预算
4. 默认导出风格

建议在 `task_json` 中保留原值，同时在列表摘要层允许按需冗余一个 `mode` 字段，便于后续做筛选。

### 5.1.6 大字段加载策略

API 层必须避免列表接口返回大字段。

建议：

1. 列表接口不返回 `plan_json`、完整报告、完整消息内容
2. 详情接口分为“摘要详情”和“完整报告”两段加载
3. 报告区进入可视区域时再请求完整报告，或在详情页首次打开后异步加载

本方案明确采用方案 A：独立报告接口。

建议接口：

1. `GET /api/ai/v1/research/records/{record_id}`
   - 返回摘要详情，不包含完整 `report_json`
2. `GET /api/ai/v1/research/records/{record_id}/report`
   - 返回完整报告内容

这样可以避免 `include=report` 这类可选参数在前后端实现时继续引发歧义。

## 5.2 Repository / Store 设计

建议新增独立的记录仓储抽象，例如：

- `ResearchRecordStore`

建议能力：

1. `create_record`
2. `get_record`
3. `list_records`
4. `update_record`
5. `touch_record`
6. `archive_record`
7. `delete_record`
8. `get_report`
9. `save_report`

同时建议新增：

- `ResearchRecordRunStore`

能力：

1. `create_run`
2. `get_run(run_id, user_id)`
3. `get_latest_run(record_id, user_id)`
4. `update_run`

实现上可先提供：

- `MySQLResearchRecordStore`
- `MySQLResearchRecordRunStore`

后续如有缓存需求，再加 Redis 做只读缓存，不建议第一版把记录主存放到 Redis。

## 5.3 API 设计

建议新增 `research/records` 资源，不污染现有纯执行接口。

### 5.3.1 列表接口

`GET /api/ai/v1/research/records`

返回字段建议：

- `id`
- `title`
- `mode`
- `status`
- `output_template`
- `summary`
- `archived`
- `updated_at`
- `created_at`
- `version_no`

建议支持的查询参数：

- `q`：标题搜索
- `status`
- `mode`
- `output_template`
- `archived`

### 5.3.2 创建记录

`POST /api/ai/v1/research/records`

入参：

- 初始标题
- 初始 task draft
- 初始显式资料范围

返回：

- `record_id`

### 5.3.3 详情接口

`GET /api/ai/v1/research/records/{record_id}`

返回：

- 基本信息
- task
- plan
- references
- imported_items
- version 信息
- latest_run_id
- latest_run_status
- latest_run_type

说明：

1. 该接口默认不返回完整 `report_json`
2. 前端详情页打开后，再调用独立的 report 接口加载完整报告
3. 详情页刷新恢复依赖 latest run 元数据，而不是猜测旧 SSE 连接状态

### 5.3.4 完整报告接口

`GET /api/ai/v1/research/records/{record_id}/report`

返回：

- `record_id`
- `report`
- `updated_at`

### 5.3.5 计划前澄清

计划前澄清不纳入 Phase 1 实施范围。

本节保留为后续扩展位，当前设计要求仅为：

1. 不在 Phase 1 数据模型中落地澄清消息字段
2. 后续若扩展澄清接口，仍应挂在 `research/records/{record_id}` 资源下
3. 不影响本期“创建记录 -> 生成计划 -> 后台执行 -> 查看详情”的主链路

### 5.3.6 生成计划

`POST /api/ai/v1/research/records/{record_id}/plan`

入参：

- 空对象 `{}`

返回：

- 标准化后的 plan

与现有 `POST /research/plan` 的关系建议：

1. 保留旧接口，作为底层通用计划生成能力
2. 新的 records-plan 接口作为面向新架构的业务入口
3. records-plan 可以在服务层内部调用现有计划生成逻辑
4. 过渡期前端只应调用新入口，避免双入口长期扩散到页面层
5. 前端统一发送空对象 `{}`，由后端从 `record.task_json` 读取 task；若 task 缺失则返回 400

mode 策略建议：

1. `quick` 下生成更短的 objectives / sub_questions / section_outline
2. `deep` 下保留更完整的问题拆解

### 5.3.7 启动研究

`POST /api/ai/v1/research/records/{record_id}/run`

说明：

1. 该接口只负责创建 `run` 记录并投递 Celery task，立即返回 `run_id`。
2. 真正执行在 Celery worker 中进行，不绑定当前 HTTP 请求生命周期。
3. Worker 逐条将 chunk 写入 Redis Stream，SSE 订阅端点只负责读取 Stream 并转发。
4. full run 完成后写回 `report_json`、`summary`、`references_json`、`completed_at`、`status=completed`。
5. 若执行失败，写回 `last_error` 与 `status=failed`。
6. `cancelled` 不在 Phase 1 实现。

mode 策略建议：

1. `quick`：减少检索预算、减少默认章节数、突出摘要和可直接汇报内容
2. `deep`：保持现有完整策略

### 5.3.8 研究后继续对话与重新生成

研究后继续对话、重新生成、版本链衍生操作不纳入 Phase 1 实施范围。

本期仅保留以下扩展要求：

1. `parent_record_id` / `root_record_id` / `version_no` 字段继续保留
2. 详情页头部允许展示版本信息占位，但不实现正式版本派生 API
3. 后续若增加 chat / regenerate 接口，仍应挂在 `research/records/{record_id}` 资源下

### 5.3.9 删除与归档

建议新增：

1. `POST /api/ai/v1/research/records/{record_id}/archive`
2. `DELETE /api/ai/v1/research/records/{record_id}`

Phase 1 约束：

1. 仅允许删除 `draft`、`planned`、`failed` 状态记录
2. `cancelled` 枚举保留，但不实现 cancel API

## 5.4 Service 与 Engine 改造

建议在现有 `ResearchEngine` 外增加一层编排服务，例如：

- `ResearchRecordService`

职责：

1. 维护研究记录生命周期。
2. 调用现有 `ResearchEngine.generate_plan` 能力。
3. 创建 run 记录并投递 Celery task。
4. 负责 Redis Stream 订阅与 SSE 转发。
5. 汇总摘要并回写列表字段。
6. 负责 records API 与旧 plan/run 能力之间的编排适配。

这样可以避免把“记录管理职责”直接塞进现有引擎。

### 5.4.1 quick 策略落点建议

不建议先写一个新的 `run_quick_research` 分支把逻辑整体复制一份。

更稳妥的做法是：

1. 在 `ResearchRecordService` 或执行编排层根据 `task.mode` 生成预算参数
2. 将预算参数传入现有执行流程
3. 在现有检索、计划、报告规范化阶段做分支处理

建议纳入差异化控制的参数：

1. `max_clarification_turns`
2. `max_context_docs`
3. `max_section_count`
4. `include_open_questions`
5. `include_conflicts`
6. `summary_priority`

### 5.4.2 service guide 结构化证据处理

现有 Research Engine 已对服务指南证据做特殊抽取和字段化补强。

新架构中该能力必须继续保留，并显式纳入以下路径：

1. 计划生成时的 retrieval focus 建议
2. 报告生成时的结构化证据上下文
3. “汇报提纲”“讲话稿”模板下的表达重组

需要补充评估的核心模块：

1. `research_engine.py`
2. `research_formatter.py`
3. `research_retriever.py`

## 5.5 Prompt 设计

计划前澄清与研究后继续对话 Prompt 不纳入 Phase 1 实施范围。

本节保留为后续扩展设计说明，建议新增两组 Prompt：

1. `RESEARCH_CLARIFY_SYSTEM`
2. `RESEARCH_POST_CHAT_SYSTEM`

### 5.5.1 澄清 Prompt 目标

让 LLM 做三件事：

1. 判断当前信息是否足够生成计划。
2. 若不足，提出最关键的 1 到 3 个澄清问题。
3. 若足够，输出归一化 task patch。

其中 quick / deep 的差异应写入 prompt：

1. quick 优先收敛到最少必要问题
2. deep 可以保留更完整的边界确认

### 5.5.2 研究后对话 Prompt 目标

让 LLM 明确区分两类输出：

1. 解释已有结果
2. 识别用户新增要求是否足以触发重新生成

### 5.5.3 模板与服务指南字段适配

新增模板不应只新增章节标题，还应明确如何消费 Guide 结构化字段：

1. `briefing_outline` 更适合组织为汇报项
2. `speech_draft` 更适合组织为发言口径

因此需要同步改造：

1. 默认章节
2. 默认交付物
3. 模板 prompt
4. Guide 字段到章节语义的映射

## 5.6 输出模板扩展

在后端以下位置同步扩展：

1. `backend/app/api/schemas/research.py`
   - 扩展 `output_template` 枚举
2. `backend/app/core/research_formatter.py`
   - 扩展默认章节与默认交付物
3. `backend/app/prompts/research_prompts.py`
   - 扩展模板导向性约束

新增模板 code 建议：

- `briefing_outline`
- `speech_draft`

---

## 6. 前端设计

## 6.1 路由设计

将现有单个 `/research` 路由拆分为：

1. `/research`
2. `/research/new`
3. `/research/:id`

如后续需要显式版本对比，可扩展：

4. `/research/:id/versions/:versionId`

## 6.2 页面设计

### 6.2.1 列表页

建议新增页面：

- `frontend/src/views/ResearchListView.vue`

主要内容：

1. 顶部操作区
   - 新建研究
2. 列表区
   - 标题
   - 模式
   - 更新时间
   - 模板标签
   - 状态标签
   - 简要结论
3. 卡片操作
   - 查看详情
   - 归档
   - 删除

列表页应至少支持：

1. 标题搜索
2. 状态筛选
3. 归档视图切换

### 6.2.2 新建页

建议新增页面：

- `frontend/src/views/ResearchCreateView.vue`

主要内容：

1. 任务编辑器
2. 导入资料概览
3. 澄清消息区
4. 计划预览区
5. 启动研究按钮

推荐复用组件：

- `TaskEditor`
- `PlanViewer`
- `ChatMessage`

UI 建议：

1. quick 模式显示“快速研究”标签和更轻的说明文案
2. deep 模式显示“深度研究”标签和更完整的说明文案
3. 从导入链路进入时，优先落到 `/research/new`，并预填导入资料与初始标题

### 6.2.3 详情页

建议新增页面：

- `frontend/src/views/ResearchDetailView.vue`

主要内容：

1. 记录头部信息
2. 模式标签与版本信息
3. 计划区
4. 报告区
5. 时间线区
6. 证据区
7. 操作区（导出、归档、章节重跑）

推荐复用组件：

- `PlanViewer`
- `ReportViewer`
- `ResearchTimeline`
- `EvidencePanel`
- `ChatMessage`

### 6.2.4 现有组件迁移路径

建议迁移策略如下：

1. `frontend/src/views/research/TaskEditor.vue`
   - 提升为可供 `ResearchCreateView` 复用的独立业务组件
2. `PlanViewer.vue`、`ReportViewer.vue`、`ResearchTimeline.vue`、`EvidencePanel.vue`
   - 迁移到共享业务组件目录或保持原目录但从 `ResearchDetailView` 引用
3. `SessionSidebar.vue`
   - 不再作为主会话入口
   - 建议移除或改造成“版本列表 / 历史版本侧栏”

### 6.2.5 导入链路迁移

现有 `ImportToResearchDialog`、`useResearchImport`、`ResearchBasketDrawer` 需要适配新架构。

建议规则：

1. 从 Search / QA / 文档详情 / 事项详情导入 Research 时
   - 默认跳转到 `/research/new`
   - 预填导入资料、标题和初始 task 草稿
2. 若用户明确选择“导入已有研究”
   - 打开记录选择器，而不是旧的 SessionSidebar
3. `ResearchBasketDrawer` 中“前往 Research”
   - 从跳转 `/research` 改为优先跳转 `/research/new`

## 6.3 Store 设计

当前 `frontend/src/stores/research.ts` 更接近“单工作台运行态 store”。

建议拆分为两层：

1. `researchListStore`
   - 管理列表数据、当前过滤条件、列表刷新
2. `researchWorkbenchStore`
   - 管理单条记录详情、SSE 运行态、计划、报告与引用状态

命名建议统一为：

1. 后端：`ResearchRecordStore`
2. 前端：`useResearchRecordListStore`、`useResearchRecordWorkbenchStore`

避免前后端一个叫 record，一个叫 researchList 的语义漂移。

### 6.3.1 为什么拆 store

原因：

1. 列表状态和详情运行态职责不同。
2. 避免详情页的流式状态污染列表页缓存。
3. 更适配路由拆分后的数据获取模型。

建议在工作台 Store 中区分两类清理动作：

1. `reset()`
   - 全量清空 `recordId / record / plan / report / references / progress`
2. `resetStreamState(clearReport: boolean)`
   - 只清理流式回放产生的累积态
   - `clearReport = true` 时用于 full run 重订阅，清空 `report + progress + references`
   - `clearReport = false` 时用于 `section_rerun` 重订阅，仅清空 `progress + references`，保留已有完整报告

## 6.4 API 层设计

建议新增独立 API 文件：

- `frontend/src/api/researchRecords.ts`

主要职责：

1. 列表
2. 详情
3. 创建记录
4. 生成计划
5. 启动 full run / section rerun
6. 订阅运行事件

现有 `frontend/src/api/research.ts` 可继续保留，用于底层 plan/run/rerun body 组装与兼容逻辑。

## 6.5 类型设计

建议在 `frontend/src/types/research.ts` 中新增或拆出：

- `ResearchRecordSummary`
- `ResearchRecordDetail`
- `ResearchVersionInfo`
- `ResearchRecordRunSummary`

同时扩展：

- `ResearchOutputTemplate`

并保留：

- `ResearchMode = 'quick' | 'deep'`

同时前端状态建议与后端完全对齐：

- `draft`
- `clarifying`
- `planned`
- `running`
- `cancelled`
- `completed`
- `failed`

旧前端状态与新状态映射建议：

1. `idle -> draft`
2. `error -> failed`

新增：

- `briefing_outline`
- `speech_draft`

## 6.6 本地迁移与兼容

Phase 1 不做 localStorage 旧数据迁移。

兼容策略：

1. 新页面只读取服务端 records API。
2. 旧 `ResearchView.vue`、`SessionSidebar.vue`、`research.ts` 在所有调用点迁移完成后直接删除。
3. 不提供“导入旧本地研究”入口。

---

## 7. 交互流程设计

## 7.1 新建研究流程

1. 用户进入 `/research/new`
2. 填写初始任务
3. 创建记录，状态为 `draft`
4. 调用 records-plan 接口生成计划，状态为 `planned`
5. 用户点击“确认并开始研究”后跳转 `/research/:id?autoRun=true`
6. 详情页发起 run，返回 `run_id`
7. 详情页订阅 `runs/{run_id}/events`
8. Worker 完成后，记录进入 `completed` 或 `failed`

### 7.1.1 状态恢复与刷新策略

若用户在 `running` 阶段刷新页面：

1. 先拉取记录详情
2. 若状态仍为 `running` 且存在 `latest_run_id`，根据 `latest_run_type` 决定重放策略后重新订阅该 run 的事件流
3. 若 SSE 结束，必须先刷新 record，再按状态决定是否加载 report
4. 若后端已完成，则直接恢复为已完成详情页

重放策略要求：

1. `latest_run_type === 'full'` 时，重订阅前执行 `resetStreamState(true)`
2. `latest_run_type === 'section_rerun'` 时，重订阅前执行 `resetStreamState(false)`
3. `section_rerun` 流不会重放完整报告，因此重订阅时不得清空已有 report

### 7.2 SSE 断线重连与恢复

当前前端 `useSSE` 基于 `fetch + ReadableStream`，不具备浏览器原生 EventSource 的自动重连能力。

新架构建议：

1. 运行态写入服务端 record 状态、latest run 元数据与 Redis Stream 事件流
2. 用户刷新详情页时，先拉取 record 详情
3. 若状态仍为 `running`
   - 页面展示“研究仍在执行”
   - 支持重新订阅当前 run 的 SSE 事件流或轮询 record 状态
4. 若无法恢复原始流式 token 级输出，至少保证阶段进度和最终结果可恢复

第一版优先保证“刷新后能恢复状态和结果”，不强求原 token 流无缝续播。

### 7.5 权限控制

记录化 API 必须沿用现有用户隔离原则，但记录本身不属于文档 ACL 过滤范畴。

建议规则：
2. 新增 `research_record_reports`、`research_record_runs` 表与 store
3. 新增列表/详情/创建/报告/run/events API
4. 将执行升级为 Celery worker + Redis Stream + SSE 订阅
5. 扩展模板枚举
6. 把 `mode`、`archived`、`root_record_id`、`latest_run` 元数据纳入记录模型
7. 明确列表和详情不返回大字段 report
---

## 8. 实施计划

## 8.1 Phase 1

后端：
5. 详情页持有 useSSE 并订阅 run 事件
6. 接入标题搜索、删除/归档入口
7. 适配导入链路跳转 `/research/new`
8. 删除旧单页工作台与旧 store
3. 扩展模板枚举
4. 把 `mode` 纳入记录模型与列表返回
5. 增加 `session_id`、`root_record_id`、`archived`、`cancelled`
6. 明确列表不返回大字段

1. 增加计划前澄清模式。
2. 增加研究后继续对话。
3. 增加基于反馈重新生成。
4. 落实版本链派生操作。
5. 补充 cancel 能力。
6. 继续细化 quick 的轻量化执行策略。
5. 在页面上明确 quick / deep 的定位说明
6. 接入标题搜索、删除/归档入口
7. 适配导入链路跳转 `/research/new`


## 8.2 Phase 2

后端：

1. 新增澄清 API
2. 新增研究后对话 API
3. 新增重新生成 API
4. 接入 quick 轻量化预算策略
5. 增加取消与刷新恢复相关接口或进度快照能力

前端：

1. 新建页增加澄清 UI
2. 详情页增加研究后对话区
3. 详情页增加重新生成入口
4. 详情页增加 running 恢复提示与重连能力

## 8.3 Phase 3

1. 加列表筛选和排序
2. 加版本历史与版本比较
3. 优化导出样式

---

## 9. 测试建议

## 9.1 后端测试

新增覆盖：

1. 研究记录创建、列表、详情
2. 状态流转
3. 澄清接口
4. 重新生成创建新版本而不覆盖旧记录
5. 新增模板的 plan / run 输出合法性
6. quick 与 deep 在计划结构和执行预算上的差异行为
7. session_id 关联与刷新恢复
8. 删除、归档、取消
9. user_id 数据隔离

## 9.2 前端测试

建议至少完成：

1. 列表页渲染与跳转
2. 新建页澄清流程
3. 详情页数据回显
4. 重新生成交互
5. 两个新模板的 UI 回显与导出入口
6. quick 升级为 deep 的交互验证
7. 导入链路跳转到 `/research/new` 的验证
8. running 页面刷新后的状态恢复验证

## 9.3 回归验证

重点回归：

1. 原有 plan / run / rerun 不退化
2. 证据工作区展示不退化
3. 导出能力不退化
4. 从 Search / QA / 文档详情导入到 Research 的链路不退化
5. Guide 结构化证据在新模板下不退化

---

## 10. 主要风险与规避

### 10.1 记录模型与旧会话模型并存风险

规避方式：

1. 新功能走新 record API。
2. 旧 workbench store 保留一段时间，仅用于兼容。

### 10.2 状态同步风险

列表页与详情页会同时依赖同一条记录状态。

规避方式：

1. 详情页 SSE 结束后主动刷新记录详情与列表摘要。
2. 列表页进入详情前尽量使用服务端状态，不依赖本地推测。

### 10.3 澄清流程过长风险

规避方式：

1. 限制轮次。
2. Prompt 明确要求优先提最关键问题。
3. 超过轮次仍信息不足时，允许用户强制生成计划。

### 10.4 重新生成语义不清风险

规避方式：

1. UI 明确标注“将创建新版本”。
2. 在新版本头部展示来源版本。

### 10.5 quick 定位含混风险

如果文档和 UI 不明确，用户会把 quick 误解为 QA，或者误以为 quick 与 deep 只是名称不同。

规避方式：

1. UI 显式说明 quick 是轻量研究，不是普通问答
2. 列表和详情页展示 mode 标签
3. 在策略实现上逐步补齐 quick 的差异化预算

### 10.6 大字段与查询性能风险

如果主表直接承载完整报告，列表和详情接口都可能被大 JSON 拖慢。

规避方式：

1. 大字段拆表或外置存储
2. 列表接口严格只返回摘要字段
3. 详情接口按需加载完整报告

### 10.7 新旧入口并存风险

若页面层同时直接使用旧 `/research/plan` 和新 `/research/records/{id}/plan`，会造成调用路径混乱。

规避方式：

1. 页面层统一调用 records 入口
2. 旧 plan/run 接口只保留在服务编排层内部复用

---

## 11. Phase 1 实施约束

以下约束在实施计划评审中确定，实施时必须遵守：

### 11.1 执行架构约束

1. 研究执行采用"**Celery 后台任务 + Redis Stream + SSE 订阅**"模型：
   - `POST /records/{id}/run` 创建 run 记录 + 投递 Celery task，立即返回 run_id
   - Worker 独立执行 engine，逐条 chunk 写入 Redis Stream
   - `POST /records/{id}/runs/{run_id}/events` SSE 订阅 Redis Stream
   - SSE 断开不影响后台执行
2. 新增 `research_record_runs` 表追踪执行实例（含 user_id 字段做隔离）
3. 新建页只负责创建记录 + 生成计划，不发起执行
4. 前端 `useSSE()` composable 由详情页组件持有，Store 不持有 SSE 连接，只暴露 `handleSSEEvent()`
5. 新建页点击"确认并开始研究"后跳转 `/research/:id?autoRun=true`，详情页自动启动并立即清除 autoRun 参数
6. 详情页恢复依赖 `ResearchRecordDetail.latest_run_id/latest_run_status` 元数据，不依赖猜测
7. `subscribe_run()` 签名必须包含 `record_id + run_id + user_id`，落实 run_id → record_id → user_id 三级归属校验
8. Celery task 不传 `PermissionContext` 对象，只传 `user_id + acl_tokens`，Worker 内重建权限上下文
9. `celery_app.py` 必须注册 `app.tasks.research_task`
10. `research_task.py` 必须采用与 `ingest_task.py` 相同的 `_run_async + Worker 自建客户端` 模式，不依赖 `app.state`、`Depends` 或 API helper

### 11.2 服务层持久化约束

1. Celery worker 在执行 engine iterator 时同步聚合 chunk，组装完整 report 并落库。
2. engine 异常时先 yield `error` 再 yield `done`（见 research_engine.py:998-1002），必须用 `had_error` 标记防止 `done` 覆盖 `failed` 状态。
3. full run 成功必须同时写 `records.completed_at`；section rerun 不改 `completed_at`，只用 `touch_record` 更新 `updated_at`。
4. `rerun_section()` 完成后必须读取现有 report，用新章节替换同标题章节（含 title/summary/content/source_doc_ids 完整字段），回写子表。
5. references 聚合时按 key 去重：guide 类型用 `guide:{profile_id or doc_id}`，其他用 `doc:{doc_id}`，与前端 `getReferenceKey`（stores/research.ts:71）一致。
6. Redis Stream 字段用字符串 key（当前 `decode_responses=True`）。
7. full run / section rerun 更新 run 状态时，必须同步维护 `started_at` / `completed_at`。
8. `section_rerun` 开始时可将 `record.status` 置为 `running`；成功时必须恢复为 `completed`；失败时也必须恢复为 `completed`，不能卡在 `running`。
9. `section_rerun` 成功后必须同步更新 `record.references_json`：收集本次 rerun 产生的 `reference` chunk，与既有 `references_json` 按同一去重 key 合并后回写主表。
10. Celery 入队失败补偿必须区分 full run 与 `section_rerun`：
   - full run 入队失败：`run=failed`，`record.status=failed`，写 `record.last_error`，API 返回 503
   - `section_rerun` 入队失败：`run=failed`，写 `run.error` 与 `record.last_error`，但不降低 `record.status`
   - 两种场景都必须写 `run.completed_at`

### 11.3 导入资料透传约束

`_extract_seed_doc_ids(record)` 方法统一从 `imported_items_json` 提取 doc_id，三条链路（plan/run/rerun）共用：
- `item_type === 'document'` 或 `'snippet'` → 取 `doc_id`
- `item_type === 'matter'` → 取 `governing_doc_ids`
- `item_type === 'answer'` → 取 `references[].doc_id`
- 与 `task_json.required_doc_ids` 合并去重

### 11.4 其他约束

1. Phase 1 不含 cancel API，`cancelled` 状态保留枚举但不实现。
2. 删除规则：仅允许 `draft`、`planned`、`failed`。
3. 创建记录时 `task` 为必填。
4. 不做 localStorage 旧数据迁移。
5. 直接替换旧页面，不保留回退路径。
6. 研究报告通过独立 API `GET /records/{id}/report` 加载，详情和列表接口不返回 report。
7. 列表接口固定返回 `{ records, total }`，后端执行 COUNT + 分页 SELECT 两条查询。
8. records 体系使用独立请求 Schema（`ResearchRecordPlanRequest`、`ResearchRecordSectionRerunRequest`），禁止复用旧 schema。
9. `generateRecordPlan()` 前端统一发送空对象 `{}`，由后端从 `record.task_json` 读取任务。
10. `useSSE()` 在 `AbortError` / 组件卸载场景不得触发业务完成态 `onDone`；`onDone` 仅表示服务端流自然结束。这是 Phase 1 的唯一实现口径。
