# Notebook 模块 PRD

## 1. 概述

Notebook 模块为用户提供一个以"来源"为中心的知识工作空间，参考 Google NotebookLM 设计理念。用户可以策展多个来源文档，在受限范围内与 AI 对话，并生成结构化输出文档。

### 核心价值
- **来源驱动**：对话仅基于用户选定的来源材料，回答可信、可溯源
- **知识隔离**：每个 Notebook 拥有独立的知识范围，互不干扰
- **结构化输出**：一键将零散知识整理为简报、方案、讲话稿等标准文档

---

## 2. 功能概览

### 2.1 三栏布局

| 区域 | 宽度 | 功能 |
|------|------|------|
| 左侧 - 来源管理 | 280px | 添加、查看、管理来源文件 |
| 中间 - 对话 | flex | AI 对话、引用展示、建议问题 |
| 右侧 - 输出文档 | 360px | 生成、管理、预览输出文档 |

### 2.2 Notebook 列表页
- 以卡片形式展示所有 Notebook
- 支持新建、搜索、归档、删除
- 显示来源数量、消息数、最近更新时间

---

## 3. 来源管理（左侧面板）

### 3.1 添加来源

三种方式：

**文档检索**
- 调用现有文档检索 API（`/api/v1/search`）
- 在弹窗中搜索、浏览结果
- 点击添加到 Notebook 来源
- 无需重新入库（直接引用已有 doc_id）
- 状态：直接标记为 `completed`

**上传文件**
- 支持格式：PDF、DOCX、DOC、XLSX、TXT、RTF、CSV
- 文件大小限制：50MB
- 上传后触发 Celery 异步入库任务
- 入库到 OpenSearch，ACL 标记为 `NB_{notebook_id}`
- 状态流转：`pending → processing → completed / failed`

**粘贴文字**
- 输入标题 + 文本内容
- 最大 200,000 字符
- 保存为临时文件后走入库流程
- 同样使用 `NB_{notebook_id}` ACL 标记

### 3.2 来源列表

- 显示所有已添加来源（最多 10 个）
- 每个来源显示：标题、来源类型图标、入库状态
- 入库状态：
  - `pending` - 等待处理（灰色）
  - `processing` - 处理中（蓝色旋转）
  - `completed` - 已完成（绿色）
  - `failed` - 失败（红色，可重试）
- 来源可以单独选中/取消选中（控制是否参与对话上下文）
- 删除来源（从列表移除 + 清理 ES 中的 NB_ 标记数据）
- 计数徽标：`3/10 来源`

### 3.3 来源入库自动摘要

入库完成后，自动为来源生成简短摘要（基于 LLM），存入 `notebook_sources.summary` 字段。用于：
- 来源列表中展示摘要
- 建议问题生成的上下文
- 输出文档生成的概述

### 3.4 约束

- 每个 Notebook 最多 10 个来源
- 同一文档不可重复添加（基于 doc_id 去重）
- 入库失败可重试（最多 2 次）

---

## 4. 对话功能（中间面板）

### 4.1 对话配置

用户可配置以下参数（存储在 notebook `config_json`）：

| 配置项 | 说明 | 选项 |
|--------|------|------|
| 对话目标 | 引导 AI 关注方向 | 自由文本输入 |
| 风格/角色 | AI 回答的语气和身份 | 自由文本，如"政策分析师"、"通俗易懂" |
| 回答长度 | 控制回复详细程度 | 简短 / 适中 / 详细 |

配置通过顶部工具栏的齿轮图标打开 Drawer 进行设置。

### 4.2 对话机制

**查询范围限制（核心，需求 4 严格定义）**
- 对话 **仅** 使用当前 Notebook 中已选中的来源作为参考
- **禁止** 查询全局知识库其他内容、禁止图谱扩展、禁止办事指南检索
- 每次提问必须在 **全部已选来源** 集合内做 **query-aware（基于问题的相关性）检索**
- 不得对来源数量做内部截断（已选来源最多 10 个，全部参与检索）
- 不得把整篇来源全文塞入上下文，必须按问题相关性选取最相关的片段

**实现方式**
- 所有来源的 doc_id 作为 `seed_doc_ids` 传入 `ResearchEngine.qa()`
- 上传/粘贴来源额外通过 `NB_{notebook_id}` ACL token 过滤
- `notebook_mode=True` 禁用全局 ES 搜索、图谱检索、办事指南检索
- `_fetch_graph_docs` 在 notebook 模式下传入 `question` 参数做 multi_match 相关性排序，而非 `None`
- notebook 模式下不截断 seed_doc_ids（移除 `[:8]` 限制）

**多轮对话**
- 支持同一 Notebook 内的连续对话
- 历史上下文通过 session_id 维护（Redis/MySQL 会话存储）
- 最多保留最近 4 轮对话作为 LLM 上下文

### 4.3 引用展示

复用现有 QA 模块的引用机制：
- 回答中使用 `[1]`、`[2]` 等标记引用来源
- 点击引用标记弹出 `DocumentQuickPreview`（已有组件）
- 回答下方显示 `CitationCard` 列表（已有组件）
- 引用仅来自当前 Notebook 来源

### 4.4 建议问题

- 对话区域下方显示建议问题（3-5 个）
- 初始建议：基于来源标题和摘要生成
- 每次回答后：根据对话上下文和来源内容生成新建议
- 点击建议问题自动填入输入框并发送

### 4.5 澄清机制（需求 6 严格定义）

**触发条件：**
- 已选来源为空时：前端直接提示"请先添加来源"，禁止发送
- 已选来源不足 2 个时：在输入框上方显示提示"来源较少，回答可能不够全面"
- LLM 认为问题不明确时：由 LLM 在回答中主动要求澄清（通过系统提示词约束）

**交互方式：**
- 澄清提示内嵌在 AI 回答中（如"您的问题可以从以下角度理解，请确认您想了解哪方面：…"）
- 不需要单独的澄清流程，用户直接在对话中补充说明即可
- 建议问题列表也可起到引导澄清的作用

**系统提示词约束：**
- 当 LLM 无法从来源中找到足够信息时，必须明确告知并建议用户换个角度提问或补充来源
- 禁止编造来源中不存在的内容

### 4.5 流式响应

- 使用 SSE（Server-Sent Events）流式推送
- 复用现有 `useSSE` composable
- 推送类型：`text`（正文）、`reference`（引用）、`done`（完成）、`error`（错误）
- 流式过程中在回答末尾追加建议问题

---

## 5. 输出文档（右侧面板）

### 5.1 支持的输出格式

**基础格式（原始需求明确的）：**

| 类型标识 | 名称 | 说明 |
|----------|------|------|
| `briefing` | 简报 | 摘要性简报，适合快速了解 |
| `study_guide` | 学习指南 | 章节式问答学习材料 |
| `blog_post` | 博文 | 博客风格文章 |
| `science_handbook` | 科普手册 | 技术参考文档 |
| `implementation_plan` | 实施方案 | 含阶段、步骤、时间线的行动计划 |
| `speech_draft` | 讲话稿 | 演讲要点和口语化内容 |

**扩展格式（代码已实现，可根据需要启用/禁用）：**

| 类型标识 | 名称 | 说明 |
|----------|------|------|
| `policy_brief` | 政策简报 | 政策分析与建议 |
| `summary_report` | 综合报告 | 全面分析报告 |
| `custom` | 自定义 | 用户描述输出要求 |

> 注：扩展格式超出原始需求范围，当前默认启用。如需收紧，可在前端 OutputFormatDialog 中移除对应选项。

### 5.2 生成流程

1. 用户点击"生成文档"，选择输出格式
2. 可选：输入自定义指令补充说明
3. 后端收集来源内容（从 ES 获取所有来源的 chunks）
4. 可选：纳入最近对话记录作为额外上下文
5. 构建格式专用 prompt → LLM 流式生成 → SSE 推送
6. 生成完成后保存到 `notebook_outputs` 表

### 5.3 输出列表

- 按时间倒序展示所有输出文档
- 每条记录显示：标题、类型图标、生成时间、状态
- 状态：`generating`（生成中）、`completed`（已完成）、`failed`（失败）
- 点击已完成的文档打开预览

### 5.4 输出预览

- 使用 Drawer 或模态框展示
- 通过 `MarkdownRenderer` 组件渲染 markdown 内容
- 支持复制全文、下载为 `.md` 文件
- 支持删除输出

---

## 6. 数据架构

### 6.1 MySQL 表结构

```sql
-- 主表
CREATE TABLE IF NOT EXISTS notebooks (
    id          VARCHAR(128) NOT NULL PRIMARY KEY,
    user_id     VARCHAR(128) NOT NULL,
    title       VARCHAR(500) NOT NULL DEFAULT '未命名 Notebook',
    description TEXT,
    status      VARCHAR(32)  NOT NULL DEFAULT 'active',
    config_json JSON,
    created_at  DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at  DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX idx_nb_user (user_id),
    INDEX idx_nb_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 来源表
CREATE TABLE IF NOT EXISTS notebook_sources (
    id             VARCHAR(128) NOT NULL PRIMARY KEY,
    notebook_id    VARCHAR(128) NOT NULL,
    user_id        VARCHAR(128) NOT NULL,
    source_type    VARCHAR(32)  NOT NULL,
    title          VARCHAR(500) NOT NULL DEFAULT '',
    doc_id         VARCHAR(256),
    content_hash   VARCHAR(128),
    file_path      VARCHAR(1024),
    paste_text     LONGTEXT,
    ingest_status  VARCHAR(32)  NOT NULL DEFAULT 'pending',
    ingest_task_id VARCHAR(128),
    ingest_error   TEXT,
    metadata_json  JSON,
    summary        TEXT,
    selected       TINYINT(1)   NOT NULL DEFAULT 1,
    sort_order     INT          NOT NULL DEFAULT 0,
    created_at     DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at     DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX idx_ns_notebook (notebook_id),
    UNIQUE KEY uk_ns_nb_doc (notebook_id, doc_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 消息表
CREATE TABLE IF NOT EXISTS notebook_messages (
    id               VARCHAR(128) NOT NULL PRIMARY KEY,
    notebook_id      VARCHAR(128) NOT NULL,
    user_id          VARCHAR(128) NOT NULL,
    session_id       VARCHAR(128) NOT NULL DEFAULT 'default',
    role             VARCHAR(16)  NOT NULL,
    content          LONGTEXT,
    references_json  JSON,
    graph_data_json  JSON,
    suggestions_json JSON,
    created_at       DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_nm_nb_sess (notebook_id, session_id),
    INDEX idx_nm_created (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 输出文档表
CREATE TABLE IF NOT EXISTS notebook_outputs (
    id           VARCHAR(128) NOT NULL PRIMARY KEY,
    notebook_id  VARCHAR(128) NOT NULL,
    user_id      VARCHAR(128) NOT NULL,
    output_type  VARCHAR(64)  NOT NULL,
    title        VARCHAR(500) NOT NULL DEFAULT '',
    content_md   LONGTEXT,
    status       VARCHAR(32)  NOT NULL DEFAULT 'generating',
    error        TEXT,
    task_id      VARCHAR(128),
    context_json JSON,
    created_at   DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at   DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX idx_no_notebook (notebook_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```

### 6.2 来源隔离策略

- **检索导入**：直接引用全局 ES 索引中的 doc_id，通过 `seed_doc_ids` 加载
- **上传/粘贴**：入库时设置 `acl_ids: ["NB_{notebook_id}"]`，查询时构建包含该 token 的 PermissionContext
- **图谱隔离**：`notebook_mode=True` 时跳过 Neo4j 图谱扩展
- **权限模型**：`NB_{notebook_id}` 是 notebook 专属 ACL token，仅限该 notebook 的 owner 持有；全局用户 token（U_/O_/D_ 等）用于读取检索导入的来源

### 6.3 来源生命周期规则

| 事件 | 行为 |
|------|------|
| 添加检索来源 | 仅记录 doc_id 引用，不复制数据，不产生本地文件 |
| 添加上传来源 | 文件保存到 `{file_storage_path}/notebook_uploads/`，入库到 ES（NB_ ACL） |
| 添加粘贴来源 | 文本保存到 `{file_storage_path}/notebook_paste/{source_id}.txt`，入库到 ES |
| 删除来源 | 从 notebook_sources 表移除；上传/粘贴来源同步删除 ES 中 doc_id 对应的 meta 和 chunks |
| 删除 Notebook | 级联删除所有 sources、messages、outputs 表记录；上传/粘贴来源的 ES 数据一并清理 |
| 重试入库 | 幂等操作：重置 ingest_status=pending → 重新派发 Celery 任务；如 ES 中已有数据则先清理再重新入库 |
| 本地文件清理 | 当前版本不主动删除磁盘文件（保留以便重试）；后续可增加定期清理 |
| 跨 Notebook 复用 | 同一文档可被多个 Notebook 引用（检索来源直接引用同一 doc_id）；上传/粘贴来源不跨 Notebook 共享 |

---

## 7. API 设计

### 7.1 Notebook CRUD

| 方法 | 路径 | 说明 |
|------|------|------|
| POST | `/api/v1/notebook` | 新建 |
| GET | `/api/v1/notebook` | 列表 |
| GET | `/api/v1/notebook/{id}` | 详情（含来源） |
| PUT | `/api/v1/notebook/{id}` | 更新 |
| DELETE | `/api/v1/notebook/{id}` | 删除（级联） |
| POST | `/api/v1/notebook/{id}/archive` | 归档 |

### 7.2 来源管理

| 方法 | 路径 | 说明 |
|------|------|------|
| POST | `/api/v1/notebook/{id}/sources/search` | 从检索添加 |
| POST | `/api/v1/notebook/{id}/sources/upload` | 上传文件 |
| POST | `/api/v1/notebook/{id}/sources/paste` | 粘贴文字 |
| GET | `/api/v1/notebook/{id}/sources` | 来源列表 |
| PUT | `/api/v1/notebook/{id}/sources/{sid}` | 更新（选中/重命名） |
| DELETE | `/api/v1/notebook/{id}/sources/{sid}` | 删除来源 |
| POST | `/api/v1/notebook/{id}/sources/{sid}/retry` | 重试入库 |

### 7.3 对话

| 方法 | 路径 | 说明 |
|------|------|------|
| POST | `/api/v1/notebook/{id}/chat` | SSE 流式对话 |
| GET | `/api/v1/notebook/{id}/chat/history` | 历史消息 |
| POST | `/api/v1/notebook/{id}/chat/suggest` | 生成建议问题 |
| PUT | `/api/v1/notebook/{id}/chat/config` | 更新对话配置 |

### 7.4 输出文档

| 方法 | 路径 | 说明 |
|------|------|------|
| POST | `/api/v1/notebook/{id}/outputs` | 生成（SSE） |
| GET | `/api/v1/notebook/{id}/outputs` | 列表 |
| GET | `/api/v1/notebook/{id}/outputs/{oid}` | 详情 |
| DELETE | `/api/v1/notebook/{id}/outputs/{oid}` | 删除 |

---

## 8. 已实现功能清单

### 8.1 后端
- Notebook CRUD（新建/列表/详情/更新/删除/归档）
- 来源管理（文档检索添加/文件上传/粘贴文字/删除/重试/选中切换）
- 来源入库（Celery 异步任务，NB_ ACL 隔离，去重兼容，自动摘要生成）
- 来源内容获取（`GET /sources/{sid}/chunks`，用于预览原文）
- 范围隔离对话（notebook_mode 跳过全局搜索/图谱/指南，query-aware 检索全部来源）
- 对话配置（目标/风格角色/回答长度，含预设模板）
- 对话历史持久化（MySQL notebook_messages 表）
- 建议问题生成（基于来源摘要 + 对话上下文）
- 输出文档生成（9 种格式模板，SSE 流式，记录来源数量）
- 输出文档 CRUD
- 片段级引用（citation_map，每个 passage 独立编号，前端按文档分组显示）
- 无来源时引导提示（不阻断对话，返回添加来源建议）
- 检索结果 score 阈值过滤（配置项 `search_score_threshold`，默认 0.5）

### 8.2 前端
- Notebook 列表页（卡片布局，搜索/归档/删除，显示来源数/消息数/日期）
- Notebook 三栏详情页（来源 | 对话 | 输出文档）
- 标题栏可编辑（点击标题进入编辑，回车保存）
- 来源面板（添加来源弹窗含文档检索多选/上传/粘贴三个 Tab，入库状态轮询，摘要 tooltip，预览原文按钮）
- 对话面板（用户消息右侧/系统消息左侧气泡，无头像名称，智能时间显示）
- 对话配置抽屉（6 个预设风格标签 + 自定义输入 + 回答长度选择）
- 建议问题（对话后动态生成，点击直接发送）
- 引用来源按文档分组（只显示正文中实际出现的编号）
- 输出文档面板（格式选择弹窗 9 种模板，列表显示类型/来源数/时间）
- 输出预览（复用 ReportDocumentReader 全屏阅读器，含目录/进度条/复制/下载 MD/导出 Word）
- 输入框优化（textarea 可换行，Enter 发送 / Shift+Enter 换行，卡片式样式）

### 8.3 全局优化
- ChatMessage 组件重构（用户右侧/系统左侧，去掉头像名称，优化时间显示）
- "加入 Research" 全部改为 "加入资料篮"，去掉弹窗直接加入
- "Research 参考资料篮" → "参考资料篮"
- 搜索页去掉批量选择 checkbox，已在资料篮的按钮 disabled
- ReportDocumentReader 新增导出 Word / 复制内容 / 下载 Markdown 浮动按钮
- 搜索分页 hybrid 模式修复（fetch_size 动态调整 + 后处理分页切片）
- 检索结果 score 低于阈值自动过滤
- `research_retriever.py` 修复 `doc_id` → `doc_ids` 字段名

---

## 9. 非功能需求

- **性能**：对话首字响应 < 2s，来源入库 < 60s（普通文档）
- **并发**：支持同一用户同时打开多个 Notebook
- **数据隔离**：用户只能访问自己的 Notebook，所有查询带 user_id 过滤
- **错误恢复**：入库失败可重试，对话错误有明确提示
- **兼容性**：复用现有 QA/Research 引擎，零破坏性修改
