539 lines
26 KiB
Markdown
539 lines
26 KiB
Markdown
# AI 写作 — 前端开发文档
|
||
|
||
> **本文档目的**:帮助前端开发者快速理解 AI 写作子系统的代码结构、数据流、状态机和扩展点。
|
||
>
|
||
> **配套文档**:
|
||
> - `ai-writing-说明.md` — 故障排查与运维向解决方案
|
||
> - `ai-writing-assistant-backend-api.md` — 后端 API 详细字段定义
|
||
> - `ai-writing-assistant-source-map.md` — 旧版前端结构索引(已合并到本文档)
|
||
|
||
---
|
||
|
||
## 一、产品定位
|
||
|
||
AI 写作是 DeerFlow 的「**杂志社协作写作**」子产品,由 LangGraph 后端驱动 4 个 AI Agent(**素材收集专家 / 作家 / 编辑 / 系统**)协作完成一篇文章。用户通过 **4 个干预点**(素材确认、大纲确认、草稿确认、审核确认)参与决策,UI 实时呈现进度。
|
||
|
||
### UI 入口
|
||
- 路由:`/canvas/ai-writing`(在 `WorkspaceRoutes.tsx` 注册)
|
||
- 入口页:`src/open-canvas/pages/AIWritingPage.tsx`
|
||
- 双栏布局:左 AI 写作面板(聊天 + 干预),右草稿预览
|
||
|
||
---
|
||
|
||
## 二、目录结构
|
||
|
||
```
|
||
frontend-web/src/open-canvas/
|
||
├── pages/
|
||
│ └── AIWritingPage.tsx # 入口页(双栏 Resizable 布局)
|
||
│
|
||
├── api/ # ── 后端 API 适配层 ──
|
||
│ ├── ai-writing.ts # /start /resume + SSE 订阅(核心)
|
||
│ ├── ai-writing-sessions.ts # 历史会话 CRUD + transcript 保存
|
||
│ └── ai-writing-article-types.ts # 文章类型 CRUD
|
||
│
|
||
├── hooks/ # ── 状态管理 ──
|
||
│ ├── useAIWriting.ts # 写作主状态机(最重要)
|
||
│ ├── useAIWritingSessions.ts # 历史会话列表 hook
|
||
│ └── useArticleTypes.ts # 文章类型选项 hook
|
||
│
|
||
├── components/ai-writing/ # ── UI 组件 ──
|
||
│ ├── AIWritingPanel.tsx # 左侧主面板(聊天 + 干预编排)
|
||
│ ├── AIWritingDraftPanel.tsx # 右侧草稿预览面板
|
||
│ ├── AIWritingPageHeader.tsx # 顶部标题/历史下拉
|
||
│ ├── AIWritingForm.tsx # 写作请求表单(首次启动用)
|
||
│ │
|
||
│ ├── AIWritingTimeline.tsx # 时间线主容器
|
||
│ ├── TimelineStep.tsx # 单条进度事件
|
||
│ ├── CompletedInterventionCard.tsx # 已完成的干预记录
|
||
│ │
|
||
│ ├── InterventionCard.tsx # 4 个干预点 + 求助点的卡片(核心)
|
||
│ ├── MaterialsCard.tsx # 素材确认
|
||
│ ├── MaterialItem.tsx # 单个素材项
|
||
│ ├── MaterialsToolbar.tsx # 素材筛选/搜索工具栏
|
||
│ ├── OutlineCard.tsx # 大纲确认(含编辑)
|
||
│ ├── OutlineSection.tsx # 大纲单章节
|
||
│ ├── ReviewCard.tsx # 审核确认
|
||
│ ├── IssueList.tsx # 审核意见列表
|
||
│ ├── ScoreBar.tsx # 评分进度条
|
||
│ │
|
||
│ ├── OutlineStreamingView.tsx # 大纲流式打字机
|
||
│ ├── DraftStreamingView.tsx # 草稿流式打字机
|
||
│ ├── ReviewStreamingView.tsx # 审核流式打字机
|
||
│ ├── WritingSkeletonCard.tsx # 加载骨架屏
|
||
│ ├── RevisionSummaryCard.tsx # 修改前后对比摘要
|
||
│ │
|
||
│ ├── AIWritingHistoryDropdown.tsx # 历史会话下拉
|
||
│ └── AIWritingHistoryView.tsx # 历史会话只读回显
|
||
│
|
||
└── types/
|
||
└── ai-writing.ts # 全部类型定义(重要参考)
|
||
```
|
||
|
||
---
|
||
|
||
## 三、数据流总览
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ AIWritingPanel │
|
||
│ ┌──────────────────────────────────────────────────────────┐ │
|
||
│ │ useAIWriting() 主状态 hook │ │
|
||
│ │ ┌────────────────────────────────────────────────────┐ │ │
|
||
│ │ │ startWriting() ─── POST /api/ai-writing/start │ │ │
|
||
│ │ │ ↓ 返回 session_id │ │ │
|
||
│ │ │ │ │ │
|
||
│ │ │ subscribeToProgress(sessionId) │ │ │
|
||
│ │ │ ─── GET /api/ai-writing/{id}/stream (SSE) │ │ │
|
||
│ │ │ ─── 持续推 ProgressEvent 给 handleEvent │ │ │
|
||
│ │ │ ─── handleEvent reducer → setState │ │ │
|
||
│ │ │ │ │ │
|
||
│ │ │ submitIntervention() │ │ │
|
||
│ │ │ ─── POST /api/ai-writing/{id}/resume │ │ │
|
||
│ │ │ ─── graph 续推 → 新事件继续走 SSE │ │ │
|
||
│ │ │ │ │ │
|
||
│ │ │ saveAIWritingTranscript()(fire-and-forget) │ │ │
|
||
│ │ │ ─── PUT /api/ai-writing/sessions/{id}/transcript │ │
|
||
│ │ └────────────────────────────────────────────────────┘ │ │
|
||
│ └──────────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ 渲染: │
|
||
│ ├ AIWritingForm(idle 状态) │
|
||
│ ├ AIWritingTimeline(运行中) │
|
||
│ │ ├ TimelineStep × N(progressEvents) │
|
||
│ │ └ CompletedInterventionCard × N(已完成的干预) │
|
||
│ ├ {Outline,Draft,Review}StreamingView(流式打字机) │
|
||
│ ├ InterventionCard(当前 currentPause 存在时弹出) │
|
||
│ └ WritingSkeletonCard(运行中无 pause 时的骨架屏) │
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## 四、状态机(useAIWriting.ts)
|
||
|
||
### 状态字段(完整定义见 `types/ai-writing.ts`)
|
||
|
||
```typescript
|
||
interface AIWritingState {
|
||
// —— 核心标识 ——
|
||
sessionId: string | null; // 后端返回的会话 id
|
||
status: WritingStatus; // 当前阶段(13 种状态之一)
|
||
isPaused: boolean; // 是否在干预点等待用户
|
||
error: string | null;
|
||
|
||
// —— 配置 ——
|
||
request: AIWritingRequest | null; // 用户提交的写作请求
|
||
|
||
// —— 累计数据 ——
|
||
progressEvents: ProgressEvent[]; // SSE 事件全量历史(供时间线渲染)
|
||
materials: AIMaterial[]; // 素材列表(素材就绪后填充)
|
||
outline: AIOutline | null; // 大纲(大纲就绪后填充)
|
||
currentDraftMarkdown: string; // 草稿正文(草稿就绪后填充)
|
||
currentDraftTitle: string;
|
||
reviewResult: ReviewResult | null;
|
||
revisionCount: number;
|
||
completedInterventions: CompletedIntervention[]; // 已完成的干预历史
|
||
|
||
// —— 干预 ——
|
||
currentPause: ProgressEvent | null; // 当前挂起的 await_user 事件
|
||
|
||
// —— 流式视图状态 ——
|
||
streamingDraftTitle: string;
|
||
streamingDraftSections: StreamingDraftSection[];
|
||
outlineStreamingText: string;
|
||
outlineThinkingText: string;
|
||
researchProgress: string;
|
||
streamingReviewItems: StreamingReviewItem[];
|
||
revisionSummary: RevisionSummary | null;
|
||
pendingRevisionContext: { /* ... */ } | null;
|
||
}
|
||
```
|
||
|
||
### 状态转移图(写作状态机)
|
||
|
||
```
|
||
┌──────┐
|
||
│ idle │ ←─────────────────────────────────────┐
|
||
└──┬───┘ │
|
||
│ startWriting() │
|
||
↓ │
|
||
┌─────────────────┐ │
|
||
│ researching │ │
|
||
└──┬──────────────┘ │
|
||
↓ materials_ready │
|
||
┌─────────────────────────────┐ │
|
||
│ awaiting_material_confirm │ ← await_user │
|
||
└──┬──────────────────────────┘ │
|
||
↓ submit(confirm) │
|
||
┌─────────────────┐ │
|
||
│ writing_outline │ ← (re_search 路径) │
|
||
└──┬──────────────┘ │
|
||
↓ outline_ready │
|
||
┌─────────────────────────────┐ │
|
||
│ awaiting_outline_confirm │ │
|
||
└──┬──────────────────────────┘ │
|
||
↓ submit(confirm) │
|
||
┌─────────────────┐ │
|
||
│ writing_draft │ ← (re_outline 路径) │
|
||
└──┬──────────────┘ │
|
||
│ │
|
||
┌───────┴────────┐ │
|
||
↓ ↓ │
|
||
awaiting_section_ awaiting_draft_confirm │
|
||
help (素材不足) └─┬───┬───┬──────────────┐ │
|
||
│ (supplement) │ │ │ to_editor │ │
|
||
│ ↑回到 research │ │ ↓ │ │
|
||
│ │ │ reviewing │ │
|
||
│ │ │ └─┬──────┐ │ │
|
||
│ │ │ ↓ ↓ │ │
|
||
│ │ │ awaiting_review_confirm │
|
||
│ │ │ └─┬────────┐ │
|
||
│ │ │ ↓ submit ↓ │
|
||
│ │ │ revising done ───────────────┘
|
||
│ │ │ │
|
||
│ │ │ └─→ writing_draft (循环修订)
|
||
│ │ │
|
||
│ │ │ user_revise
|
||
│ │ └─→ revising
|
||
│ │
|
||
│ │ finalize
|
||
│ └─→ done
|
||
│
|
||
└──→ (delete/loose) writing_draft 继续
|
||
```
|
||
|
||
### handleEvent reducer(核心)
|
||
|
||
`useAIWriting.ts` 里的 `handleEvent` 是个**纯 reducer**,根据 SSE 事件类型更新状态:
|
||
|
||
| 事件类型 | 主要影响的状态字段 |
|
||
|---|---|
|
||
| `connected` | 仅记录到 progressEvents |
|
||
| `step_start` | 切到对应 `status`(如「编辑」→ reviewing) |
|
||
| `step_done` | 仅记录 |
|
||
| `research_progress` | `researchProgress`(实时检索进度文案) |
|
||
| `materials_ready` | `materials`、清空 researchProgress |
|
||
| `outline_chunk` | `outlineStreamingText` / `outlineThinkingText`(打字机) |
|
||
| `outline_ready` | `outline`、清空流式文本 |
|
||
| `draft_chunk` | `streamingDraftSections`(按章节拼接打字机) |
|
||
| `draft_ready` | `currentDraftMarkdown` / `currentDraftTitle` |
|
||
| `review_chunk` | `streamingReviewItems[i].streamingText`(逐条打字机) |
|
||
| `review_item_done` | `streamingReviewItems[i]` 标记 isDone + 结构化结果 |
|
||
| `review_ready` | `reviewResult` |
|
||
| `await_user` | `currentPause = event`,`isPaused = true`,**弹出 InterventionCard** |
|
||
| `resumed` | `currentPause = null` |
|
||
| `notice` | 仅记录(非致命告警) |
|
||
| `done` | `status = 'done'` |
|
||
| `error` | `status = 'error'`,`error = event.message` |
|
||
|
||
---
|
||
|
||
## 五、API 层(api/)
|
||
|
||
### 1. `api/ai-writing.ts` — 核心写作 API
|
||
|
||
```typescript
|
||
// 启动写作 → 返回 session_id
|
||
export async function startWritingSession(req: AIWritingRequest): Promise<string>
|
||
|
||
// 订阅 SSE 进度流 → 返回取消订阅函数
|
||
export function subscribeToProgress(
|
||
sessionId: string,
|
||
onEvent: (event: ProgressEvent) => void,
|
||
): () => void
|
||
|
||
// 提交用户干预 → 让 graph 续推
|
||
export async function resumeWriting(
|
||
sessionId: string,
|
||
payload: UserInterventionPayload,
|
||
): Promise<void>
|
||
```
|
||
|
||
**关键实现细节**:
|
||
- **snake_case ↔ camelCase 转换**:后端返回 snake_case,前端类型是 camelCase。所有 normalize 函数(`normalizeMaterial`、`normalizeOutlineSection`...)集中在这一层做转换
|
||
- **SSE 事件白名单**:`SSE_EVENT_TYPES` 数组列出所有合法事件名,未知事件会被静默忽略
|
||
- **取消订阅**:`subscribeToProgress` 返回的函数会调用 `AbortController.abort()` 关闭 fetch stream
|
||
- **重连机制**:当前**没有内置重连** — 如果 SSE 断开(网络抖动、反代超时),需要用户主动重启写作或刷新页面
|
||
- 后端 `_recover_session` 能让 SSE 重连时从 checkpointer 恢复事件,但前端没主动重连逻辑
|
||
|
||
### 2. `api/ai-writing-sessions.ts` — 历史会话
|
||
|
||
```typescript
|
||
// CRUD
|
||
listAIWritingSessions() : Promise<AIWritingSessionSummary[]>
|
||
getAIWritingSession(id) : Promise<AIWritingSessionSummary>
|
||
renameAIWritingSession(id, title) : Promise<AIWritingSessionSummary>
|
||
deleteAIWritingSession(id) : Promise<void>
|
||
|
||
// 时间线持久化(fire-and-forget,永不抛错)
|
||
saveAIWritingTranscript(id, transcript) : Promise<boolean>
|
||
```
|
||
|
||
**关键设计**:`saveAIWritingTranscript` 内部用 try/catch 吞掉所有异常(网络错误、HTTP 500 等),只 `console.warn` 不影响主流程。这是**「锦上添花」契约**的前端侧。
|
||
|
||
### 3. `api/ai-writing-article-types.ts` — 文章类型管理
|
||
|
||
| 函数 | 用途 |
|
||
|---|---|
|
||
| `listArticleTypes()` | 表单下拉框选项 |
|
||
| `createArticleType()` | 管理员新增 |
|
||
| `updateArticleType()` | 管理员编辑 |
|
||
| `deleteArticleType()` | 管理员删除 |
|
||
|
||
---
|
||
|
||
## 六、组件树详解
|
||
|
||
### 顶层:`AIWritingPanel`(457 行,最大)
|
||
|
||
负责:
|
||
1. 调用 `useAIWriting()` 拿主状态
|
||
2. 调用 `useAIWritingSessions()` 拿历史列表
|
||
3. 协调表单 / 时间线 / 干预卡 / 流式视图 / 草稿同步
|
||
4. **自动滚底逻辑**:检测用户是否手动上划,决定是否跟随最新内容
|
||
5. **草稿同步到右侧画布**:监听 `currentDraftMarkdown` 变化,调 `setArtifact` 更新右侧
|
||
6. **历史会话回显**:当 `historySession` 非空时,渲染 `AIWritingHistoryView` 而不是实时面板
|
||
|
||
### 干预卡:`InterventionCard`(663 行,第二大)
|
||
|
||
按 `pausePoint` 渲染**不同的卡片**:
|
||
|
||
| pausePoint | 卡片 | 用户能做的 |
|
||
|---|---|---|
|
||
| `material_confirm` | `MaterialsCard` | 勾选保留素材 / 加关键词重新搜 |
|
||
| `outline_confirm` | `OutlineCard` | 直接编辑大纲 / 给反馈让重写 |
|
||
| `draft_confirm` | 草稿确认卡 | 定稿 / 给修订意见 / 提交编辑审核 |
|
||
| `review_confirm` | `ReviewCard` | 勾选采纳的意见 / 强制通过 |
|
||
| `section_help` | 求助卡 | 逐章选择:补充检索 / 删除 / 按通用知识续写 |
|
||
|
||
### 时间线:`AIWritingTimeline`
|
||
|
||
迭代 `progressEvents`,每个 `step_start` / `step_done` / `notice` 渲染成一条 `TimelineStep`。`CompletedInterventionCard` 穿插显示已完成的干预记录。
|
||
|
||
### 流式视图:`{Outline,Draft,Review}StreamingView`
|
||
|
||
- **OutlineStreamingView**:打字机渲染 `outlineStreamingText`,支持 `<think>...</think>` 折叠显示
|
||
- **DraftStreamingView**:按章节流式拼接 `streamingDraftSections`,每章节独立打字机
|
||
- **ReviewStreamingView**:3 条审核(事实 / 逻辑 / 语言)并行打字机,完成后切到结构化评分
|
||
|
||
### 历史回显:`AIWritingHistoryView`
|
||
|
||
只读模式,从 `AIWritingSessionSummary` 重建 UI。**当 `transcript` 字段有值时**,从里面拿到完整 `progressEvents`,UI 表现跟实时一模一样。`transcript` 没有时退回到只显示最终草稿。
|
||
|
||
---
|
||
|
||
## 七、SSE 事件协议(前后端契约)
|
||
|
||
后端通过 SSE 推 `event: <type>\ndata: <json>\n\n` 格式,前端用浏览器原生 `fetch + ReadableStream` 解析(不用 `EventSource`,因为 EventSource 不能自定义 header)。
|
||
|
||
### 解析流程
|
||
|
||
```
|
||
ReadableStream → TextDecoder
|
||
→ 按 "\n\n" 分帧
|
||
→ 每帧匹配 /^event: (.+)\ndata: (.+)$/
|
||
→ JSON.parse(data)
|
||
→ normalize(parsed)(snake → camel)
|
||
→ onEvent(normalized)
|
||
```
|
||
|
||
### 事件载荷字段(不完全列表)
|
||
|
||
| 字段 | 出现在哪些事件 | 用途 |
|
||
|---|---|---|
|
||
| `type` | 全部 | 事件类别 |
|
||
| `sessionId` | 全部 | 标识 |
|
||
| `message` | 大多数 | 显示给用户的文案 |
|
||
| `agentName` | step_* | 「素材收集专家」/「作家」/「编辑」/「系统」 |
|
||
| `materials` | materials_ready | 素材数组 |
|
||
| `outline` | outline_ready | 大纲对象 |
|
||
| `draftMarkdown` / `draftTitle` | draft_ready | 草稿内容 |
|
||
| `reviewResult` | review_ready | 完整审核结果 |
|
||
| `pausePoint` | await_user | 哪个干预点 |
|
||
| `pauseContext` | await_user | 干预所需的上下文(如待确认的素材列表) |
|
||
| `blockedSections` | await_user (section_help) | 素材不足的章节列表 |
|
||
| `title` / `detail` / `raw` | notice / error | 非致命告警的标题、详情、原始错误 |
|
||
|
||
---
|
||
|
||
## 八、扩展指南:常见开发任务
|
||
|
||
### 新增一个干预点
|
||
|
||
1. **前端类型扩展**:
|
||
- 在 `types/ai-writing.ts` 的 `ProgressEvent.pausePoint` 联合类型里加新 case
|
||
- 在 `WritingStatus` 加对应的 `awaiting_xxx`
|
||
- 在 `CompletedIntervention.pausePoint` 加 case
|
||
|
||
2. **写 InterventionCard 分支**:在 `InterventionCard.tsx` 里加新 `pausePoint` 分支,做 UI + 提交逻辑
|
||
|
||
3. **后端配合**(详见后端开发文档):
|
||
- `graph.py` 加 `pause_xxx_confirm` 节点
|
||
- graph 拓扑加这个节点的边
|
||
|
||
4. **完成干预后的清理**:在 `useAIWriting.ts` 的 `submitIntervention` 函数里,**确保把这次干预 push 到 `completedInterventions`**,让历史能看到
|
||
|
||
### 新增一个 SSE 事件类型
|
||
|
||
1. `types/ai-writing.ts`:
|
||
- `ProgressEventType` 联合类型加新值
|
||
- `ProgressEvent` 接口加新字段(如果有载荷)
|
||
|
||
2. `api/ai-writing.ts`:
|
||
- `SSE_EVENT_TYPES` 数组加新值(不加会被忽略)
|
||
- `normalizeEvent` 函数里加字段转换(如果是 snake_case 需要 normalize)
|
||
|
||
3. `useAIWriting.ts` 的 `handleEvent`:加新 case 处理状态更新
|
||
|
||
4. 组件层:根据需要在时间线 / 干预卡 / 流式视图里渲染
|
||
|
||
### 修改一个流式打字机的行为
|
||
|
||
每个 streaming view 内部都是独立的:
|
||
- 状态在 `useAIWriting.ts` 的 `streaming*` 系列字段
|
||
- 渲染在 `*StreamingView.tsx`
|
||
- 流式 chunk 处理在 `handleEvent` 的 `outline_chunk` / `draft_chunk` / `review_chunk` 分支
|
||
|
||
### 新增草稿到右侧画布的逻辑
|
||
|
||
`AIWritingPanel.tsx` 里 `useEffect` 监听 `state.currentDraftMarkdown` → 调 `setArtifact` 更新右侧 `CanvasGraphContext`。如果需要新的 artifact 类型,看 `contexts/CanvasGraphContext.tsx` 的 artifact 接口。
|
||
|
||
---
|
||
|
||
## 九、容错与边界处理
|
||
|
||
### 已落实的容错(不会阻断写作)
|
||
- ✅ `saveAIWritingTranscript` 失败仅 `console.warn`
|
||
- ✅ 历史会话列表加载失败仅 `console.warn`
|
||
- ✅ 重命名/删除会话失败静默吞掉
|
||
- ✅ 后端 SSE 推 `notice` / `error` 事件时,前端只渲染不中断
|
||
- ✅ JSON parse 失败时 SSE 帧被静默丢弃(不影响后续帧)
|
||
|
||
### 已知边界(可能影响 UI 体验)
|
||
- ⚠️ **SSE 断线无自动重连** — 网络抖动 / 反代超时后用户需要手动刷新
|
||
- ⚠️ **多 worker 部署下 SSE 事件丢失**(详见 `ai-writing-说明.md`)—— 前端只能感知到「卡住」,没有重连恢复机制
|
||
- ⚠️ `outline_chunk` / `draft_chunk` / `review_chunk` 的 `text` / `is_thinking` 字段是 snake_case,前端类型是 `text` / `isThinking` 但 normalize 时是**保留 snake_case 直接读**(参考 `useAIWriting.ts` 的 `(event as unknown as { text: string }).text`),这是历史包袱,未来 normalize 函数应统一处理
|
||
|
||
---
|
||
|
||
## 十、调试技巧
|
||
|
||
### 看 SSE 原始事件
|
||
浏览器 DevTools → Network → 找 `/api/ai-writing/<id>/stream` 请求 → Response(注意是 EventStream 格式,不能直接展开 JSON)。
|
||
|
||
### 查当前 hook 状态
|
||
在浏览器 console 里:
|
||
```js
|
||
// 需要安装 React DevTools,在 Components tab 找到 AIWritingPanel
|
||
// 或在 useAIWriting 里临时加:
|
||
useEffect(() => { (window as any).__aiwriting = state; }, [state]);
|
||
// 然后在 console 里 __aiwriting 看
|
||
```
|
||
|
||
### 模拟事件
|
||
开发期可以注释掉 `subscribeToProgress`,手动调 `handleEvent({...})` 测试 reducer 行为。
|
||
|
||
### 看后端 transcript
|
||
```
|
||
GET /api/ai-writing/sessions/<id>
|
||
```
|
||
返回里 `transcript` 字段就是前端组装过的完整时间线 JSON。
|
||
|
||
---
|
||
|
||
## 十一、性能注意点
|
||
|
||
### 大数组渲染
|
||
- `progressEvents` 在长写作里可能上千条 → `AIWritingTimeline` 应该考虑虚拟滚动(当前未实现,会议室会议讨论过)
|
||
- `streamingDraftSections` 章节多时也是潜在瓶颈
|
||
|
||
### transcript PUT 频率
|
||
- 当前每次 `progressEvents` 变化都会触发保存(实际上是 useEffect 监听)
|
||
- 大量 chunk 事件会导致**每秒 N 次 PUT**,后端 MySQL 写压力大
|
||
- **优化建议**:在 `useAIWriting.ts` 给 saveAIWritingTranscript 加 debounce(1-2 秒),或者只在 `await_user` / `done` / `error` 关键节点保存
|
||
|
||
### 浏览器内存
|
||
- progressEvents 不主动清理 → 长会话可能占用几十 MB
|
||
- 完成一篇文章后建议 `reset()` 清空状态
|
||
|
||
---
|
||
|
||
## 十二、相关后端接口(速查)
|
||
|
||
| 接口 | 用途 | 前端调用方 |
|
||
|---|---|---|
|
||
| `POST /api/ai-writing/start` | 启动写作 | `startWritingSession` |
|
||
| `GET /api/ai-writing/{id}/stream` | SSE 进度流 | `subscribeToProgress` |
|
||
| `POST /api/ai-writing/{id}/resume` | 提交干预 | `resumeWriting` |
|
||
| `GET /api/ai-writing/{id}/state` | 查 graph 状态快照(调试用) | 暂未调用 |
|
||
| `GET /api/ai-writing/sessions` | 历史列表 | `listAIWritingSessions` |
|
||
| `GET /api/ai-writing/sessions/{id}` | 历史详情 | `getAIWritingSession` |
|
||
| `PATCH /api/ai-writing/sessions/{id}` | 重命名 | `renameAIWritingSession` |
|
||
| `DELETE /api/ai-writing/sessions/{id}` | 删除会话 | `deleteAIWritingSession` |
|
||
| `PUT /api/ai-writing/sessions/{id}/transcript` | 保存时间线(fire-and-forget) | `saveAIWritingTranscript` |
|
||
| `GET /api/ai-writing/article-types` | 文章类型下拉 | `listArticleTypes` |
|
||
| `POST /api/ai-writing/admin/cleanup` | admin 手动清理 | 暂未在 UI 暴露 |
|
||
|
||
---
|
||
|
||
## 十三、未来优化方向
|
||
|
||
| 项目 | 说明 | 工作量 |
|
||
|---|---|---|
|
||
| transcript PUT 防抖 | 减少 MySQL 写压力 | 1 小时 |
|
||
| SSE 自动重连 | 网络抖动 / 反代超时后能恢复 | 0.5 天 |
|
||
| 时间线虚拟滚动 | 处理超长 progressEvents | 0.5 天 |
|
||
| 干预卡组件解耦 | 663 行的 InterventionCard 拆成 5 个独立卡片 | 1 天 |
|
||
| AbortController 串行化 | 避免重启写作时旧 SSE 还没断 | 0.5 天 |
|
||
| 后端字段统一 camelCase | 消除 snake_case ↔ camelCase 转换层 | 需后端配合 |
|
||
|
||
---
|
||
|
||
## 附录 A:所有类型一览
|
||
|
||
完整定义在 `types/ai-writing.ts`,主要类型:
|
||
|
||
```typescript
|
||
// 业务实体
|
||
AIMaterial // 单个素材
|
||
AIOutline // 大纲
|
||
OutlineSection // 大纲单章节
|
||
ReviewIssue // 审核问题
|
||
ReviewResult // 完整审核结果
|
||
BlockedSection // 素材不足的章节
|
||
SectionDecision // 章节求助决策
|
||
|
||
// 流程
|
||
WritingStatus // 13 种状态
|
||
ProgressEventType // 17 种事件类型
|
||
ProgressEvent // SSE 事件载荷
|
||
UserInterventionPayload // 干预提交体
|
||
AIWritingRequest // 启动写作的请求
|
||
CompletedIntervention // 已完成的干预记录
|
||
|
||
// 流式视图
|
||
StreamingDraftSection
|
||
StreamingReviewItem
|
||
RevisionSummary
|
||
|
||
// Hook 状态
|
||
AIWritingState // useAIWriting 的完整状态
|
||
```
|
||
|
||
---
|
||
|
||
## 附录 B:常见踩坑
|
||
|
||
| 现象 | 原因 | 解决 |
|
||
|---|---|---|
|
||
| 前端类型 camelCase 但拿到 undefined | 后端 snake_case 没 normalize | 在 `api/ai-writing.ts` 的 normalize 函数里加字段 |
|
||
| 进度卡住不动 | SSE 断了或落到错的 worker | 刷新页面,看 `ai-writing-说明.md` 多 worker 部分 |
|
||
| 历史会话回显时间线缺失 | transcript 保存失败 | DB 列容量 / 后端日志查 `save_transcript` |
|
||
| 修改大纲后没生效 | 用户 PATCH editedOutline 但没勾选 confirm | InterventionCard 大纲分支检查 action 字段 |
|
||
| 草稿渲染换行错乱 | LLM 输出包含奇怪 markdown | `DraftStreamingView` 里 markdown parser 设置 |
|
||
|
||
---
|
||
|
||
如需补充内容,请直接编辑本文件,并在 git commit message 里注明改动原因。
|