Files
deerflow-code/frontend-web/docs/ai-writing-frontend-dev.md
T
2026-05-20 13:06:29 +08:00

26 KiB
Raw Blame History

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)

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

// 启动写作 → 返回 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 — 历史会话

// 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 里:

// 需要安装 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,主要类型:

// 业务实体
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 里注明改动原因。