Files
deerflow-code/frontend-web/docs/openCanvas-progress.md
T
2026-05-16 22:00:34 +08:00

15 KiB
Raw Blame History

Open Canvas 集成进度文档

最后更新:2026-05-16 当前分支:dhk-1


一、整体进度概览

Canvas 系统(双栏写作/编码工作区)

阶段 名称 状态
阶段一 前端路由与目录骨架 ✅ 已完成
阶段二 Canvas 双栏布局 ✅ 已完成
阶段三 前端 artifact 类型定义 ✅ 已完成
阶段四 后端 ThreadState 扩展 ✅ 已完成
阶段五 canvas_agent 注册 + 最小节点闭环 ✅ 已完成
阶段六 前后端流式同步 ✅ 已完成
阶段七 版本历史导航 ✅ 已完成
阶段八 局部选区改写(Writing Editor 级) ✅ 已完成
阶段九 快捷动作系统 ✅ 已完成
阶段十 本地模型选择支持 ✅ 已完成

AI Writing Editor(文本 artifact 编辑器内置 AI 改写)

步骤 名称 状态
Step 1 Tiptap 编辑器核心 ✅ 已完成
Step 2 写作工具栏 + 模型选择器 ✅ 已完成
Step 3a 气泡菜单(选区 AI 改写入口) ✅ 已完成
Step 3b 段落悬停按钮(整段 AI 改写入口) ✅ 已完成
Step 4 AI 改写菜单 + SSE 流式接口 ✅ 已完成
Step 5 候选结果预览 + 替换/插入 ✅ 已完成
Step 6 后端 POST /api/writing/rewrite(SSE,思考分离) ✅ 已完成
Step 7 视觉精修(字体、间距、排版) ✅ 已完成

当前状态:全部功能已完成 🎉


二、已完成工作详情

2.1 前端核心文件结构

frontend-web/src/open-canvas/
├── api/
│   ├── client.ts          # LangGraph SDK 客户端实例
│   ├── models.ts          # 模型列表 API
│   ├── rewrite.ts         # AI 改写 SSE 流(streamRewrite,parseSse)
│   ├── runs.ts            # streamCanvasRun(老的直接 API,已停用)
│   └── threads.ts         # createCanvasThread、getCanvasThread
├── components/
│   ├── artifact/
│   │   ├── CanvasArtifactHeader.tsx      # 右栏顶部:标题、语言、版本导航
│   │   ├── CanvasArtifactPanel.tsx       # 右栏:流式预览 / 编辑器 / 空状态
│   │   ├── CanvasArtifactVersionNav.tsx  # prev/next 版本切换
│   │   ├── CanvasEmptyState.tsx          # 5 种快速开始模板
│   │   ├── CanvasQuickActionBar.tsx      # 快捷动作按钮栏
│   │   ├── CodeArtifactRenderer.tsx      # CodeMirror 代码编辑器
│   │   ├── TextArtifactRenderer.tsx      # Tiptap 文本编辑器 + AI 改写流程
│   │   ├── WritingAiMenu.tsx             # AI 改写动作菜单(polish/expand/shorten/tone)
│   │   ├── WritingBubbleMenu.tsx         # 选区气泡菜单(portal 实现)
│   │   ├── WritingParagraphMenu.tsx      # 段落悬停 AI 改写按钮
│   │   ├── WritingRewritePreview.tsx     # 改写结果预览面板(含思考过程展开)
│   │   └── WritingToolbar.tsx            # 格式工具栏(含模型选择器)
│   ├── chat/
│   │   └── CanvasChatPanel.tsx           # 左栏:消息列表 + 输入框
│   ├── layout/
│   │   ├── CanvasHeader.tsx              # 顶部栏:返回、折叠、流式指示
│   │   ├── CanvasResizableLayout.tsx     # 左右可拖拽双栏(含 CanvasStreamProvider)
│   │   └── CanvasShell.tsx               # 顶层壳:加载已有线程状态
│   └── selectors/
│       └── CanvasModelSelector.tsx       # 模型下拉选择器
├── contexts/
│   ├── CanvasGraphContext.tsx   # artifact、streamingArtifact、isStreaming
│   ├── CanvasStreamContext.tsx  # 共享 useCanvasThreadStream 实例(sendMessage)
│   ├── CanvasThreadContext.tsx  # threadId、modelName
│   └── CanvasUIContext.tsx      # isChatCollapsed、writingSelection
├── editor/
│   ├── TiptapEditor.tsx         # EditorContent 包装器
│   └── TiptapExtensions.ts      # StarterKit + Underline + Placeholder + Markdown
├── hooks/
│   ├── useCanvasArtifact.ts     # artifact 初始化工具
│   ├── useCanvasQuickActions.ts # 快捷动作触发(走 CanvasStreamContext)
│   ├── useCanvasSelection.ts    # 文字/代码选区状态
│   ├── useCanvasStream.ts       # 旧直接 API 流(已停用,保留备用)
│   ├── useCanvasThread.ts       # 线程加载工具
│   ├── useCanvasThreadStream.ts # LangGraph SDK useStream hook(主流)
│   ├── useWritingEditor.ts      # Tiptap editor 实例管理
│   └── useWritingRewrite.ts     # AI 改写状态机(ThinkTagParser、AbortController)
├── pages/
│   ├── CanvasHomePage.tsx       # 欢迎页 + 快速开始
│   └── CanvasThreadPage.tsx     # Provider 装配层
├── routes/
│   └── OpenCanvasRoutes.tsx     # /page/canvas/* 子路由
├── types/
│   ├── artifact.ts              # CanvasArtifact、CanvasArtifactText、CanvasArtifactCode
│   ├── model.ts                 # CanvasModel
│   ├── quick-action.ts          # QuickAction、QuickActionId
│   ├── selection.ts             # CanvasTextSelection、CanvasCodeSelection
│   └── thread.ts                # CanvasMessage、CanvasThreadValues
└── utils/
    ├── apply-rewrite.ts         # applyReplace、applyInsert(Tiptap 事务)
    ├── artifact.ts              # getCurrentArtifactContent、setCurrentArtifactIndex 等
    ├── quick-actions.ts         # buildQuickActionMessage(含 artifact 内容注入)
    ├── selection.ts             # buildMessageWithSelection
    ├── stream.ts                # 流式工具
    └── thread.ts                # 线程工具

2.2 后端核心文件结构

offline-backend-20260512/backend/
├── packages/harness/deerflow/agents/
│   ├── canvas_agent/
│   │   ├── __init__.py
│   │   ├── agent.py      # make_canvas_agent():LangGraph graph 工厂
│   │   ├── prompt.py     # build_canvas_system_prompt():含 selection_rewrite + quick_actions
│   │   └── tools.py      # update_artifact_tool:创建/追加 artifact 版本
│   └── thread_state.py   # ThreadState(含 artifact 字段、CanvasArtifact TypedDict)
└── app/gateway/
    ├── app.py            # 注册 writing router
    └── routers/
        └── writing.py    # POST /api/writing/rewrite(SSE 流式)

langgraph.json 注册了两个 graph:

  • lead_agent → 主聊天 Agent
  • canvas_agent → Canvas 写作 Agent

2.3 关键 Context 层级

CanvasThreadPage
  ├── CanvasThreadProvider      # threadId、modelName
  │     ├── CanvasGraphProvider # artifact、streamingArtifact、isStreaming
  │     │     └── CanvasUIProvider  # isChatCollapsed、writingSelection
  │     │           └── CanvasShell
  │     │                 └── CanvasResizableLayout
  │     │                       └── CanvasStreamProvider  ← 持有 useCanvasThreadStream 单例
  │     │                             ├── CanvasChatPanel      # 读 useCanvasStreamContext()
  │     │                             └── CanvasArtifactPanel
  │     │                                   └── CanvasQuickActionBar  # 读 useCanvasStreamContext()

三、关键架构决策与注意事项

3.1 流式架构:单例流(重要)

问题:原先 CanvasChatPanel 调 useCanvasThreadStream(),CanvasQuickActionBar 调 useCanvasStream()(旧的直接 API),两套流互不相通,快捷动作响应不出现在聊天面板。

解决方案:新增 CanvasStreamContext,在 CanvasResizableLayout 中调用 useCanvasThreadStream() 一次,通过 Context 共享给所有子组件。CanvasChatPanel 和 useCanvasQuickActions 都从 useCanvasStreamContext() 取 sendMessage。

useCanvasStream(旧,已停用) ← 不再被调用,保留备用
useCanvasThreadStream(新,LangGraph SDK) ← 通过 CanvasStreamContext 统一共享

3.2 artifact 内容注入机制(重要)

问题:LangGraph create_agent 只把 state.messages 传给 LLM,state.artifact 字段不会自动可见。Agent 做 expand/shorten 等操作时看不到要改写的内容。

解决方案:在每次 sendMessage 时,把当前 artifact 内容以 <artifact> 标签嵌入人类消息末尾。

  • 普通聊天:sendMessage 内部自动追加(仅当消息里还没有 <artifact> 时)
  • 快捷动作:buildQuickActionMessage(id, content) 在构建时就包含内容
  • 聊天面板显示时:message-list-item.tsx 调 stripArtifactTag() 剥掉标签,用户看不到
// utils/quick-actions.ts
export function buildQuickActionMessage(id, content) {
  const body = isCodeArtifact(content) ? content.code : content.fullMarkdown;
  return `${instruction}\n\n<artifact ${tag}>\n${body}\n</artifact>`;
}

// core/messages/utils.ts
export function stripArtifactTag(content: string): string {
  return content.replace(/<artifact[^>]*>[\s\S]*?<\/artifact>/g, "").trim();
}

3.3 ThinkTagParser(AI 改写思考过程解析)

后端支持两种思考格式:

  • Anthropic:独立 thinking block,以 event: thinking SSE 事件传输
  • DeepSeek:把思考过程内联在文本里:<think>...</think>content

前端 useWritingRewrite.ts 的 ThinkTagParser 是流式状态机,处理跨 chunk 边界的 <think> 标签。

3.4 版本历史:本地切换

CanvasArtifactVersionNav 的 prev/next 只修改本地 CanvasGraphContext.artifact.currentIndex,不写回后端。刷新后 currentIndex 重置为后端存储的最新值(这是有意的简化设计)。

3.5 Writing Editor 流程(三阶段状态机)

idle → menu → preview → idle
         ↑              ↓
     openAiMenu      closeAll
  • idle:正常编辑
  • menu:显示 WritingAiMenu(选择 polish/expand/shorten/tone)
  • preview:显示 WritingRewritePreview(SSE 流式结果 + 思考过程 + 替换/插入按钮)

四、已解决的技术难题

问题 解法
react-resizable-panels v4 API 变更(orientation 不是 direction,groupRef 不是 ref,setLayout 参数为 {id: size} 对象) 参考 v4 文档,逐一修正
Flex 高度链底部裁剪 在 wrapper div 加 min-h-0,配合 flex-1 overflow-hidden
Tiptap v3 BubbleMenu 已从 React 组件改为纯 Extension 自定义 portal 实现:监听 selectionUpdate,手动计算位置
段落悬停按钮鼠标桥接:鼠标经过 6px 间隙时按钮消失 文档级 mousemove 监听 + 380ms 延迟隐藏,判断鼠标是否在扩展热区内
弹窗超出屏幕底部 检测 spaceBelow vs spaceAbove,空间不足时翻转到光标上方
DeepSeek 在文本流里混入 <think> 标签 ThinkTagParser 状态机,保留最后 6/7 字节防止跨 chunk 分割
loading→success 状态转换检测(stale closure) 用 prevStatusRef + useEffect 检测 status 变化,取代直接读 state
快捷动作响应不出现在聊天面板 CanvasStreamContext 统一 sendMessage 入口
LLM 看不到 artifact 内容(只有 state.messages) 消息里直接嵌入 <artifact> 标签,显示时剥离
<artifact> 标签在聊天面板里显示 message-list-item.tsx 调 stripArtifactTag()

五、当前已知限制(非 bug,设计取舍)

  1. 版本切换不持久化:切换 artifact 版本后刷新页面会回到最新版本
  2. useCanvasStream 孤立:旧的直接 API 流 hook 保留但未被调用,可后续清理
  3. CanvasShell 的 setMessages 冗余:加载旧线程时 setMessages 写入 CanvasGraphContext,但 CanvasChatPanel 读的是 thread.messages(来自 useStream + fetchStateHistory)。setArtifact 仍有效
  4. artifact 内容随消息发送:大文档每条消息都附带全文,token 消耗较大。当前规模可接受

六、快速开始下一次工作

启动服务

# 后端(从 offline-backend-20260512/backend/)
make dev       # 启动 Gateway API on port 8001

# 前端(从 frontend-web/)
pnpm dev       # 启动 Vite on port 5174

访问路径

  • 主页:http://localhost:5174
  • Canvas 入口:点击左侧导航 → Canvas,或直接访问 http://localhost:5174/page/canvas

类型检查

cd frontend-web && pnpm typecheck
# 预期:只有 CodeArtifactRenderer.tsx 的 CodeMirror 版本冲突错误(预存 bug,不影响运行)

七、如需继续开发的潜在方向

方向 描述 优先级
版本切换持久化 将 currentIndex 变更写回后端 ThreadState 低
artifact 内容 token 优化 仅发送 diff 或用 summary 代替全文 低
清理 useCanvasStream 删除未被调用的旧 hook 低
代码 artifact 的 AI 改写 CodeArtifactRenderer 也接入 WritingAiMenu 流程 中
Canvas 选区→聊天(高级) 用户在右栏选中文字后在左栏输入指令,自动带选区上下文 中
Tiptap 扩展(表格、任务列表) 增加 Table、TaskList extension 低

八、关键文件速查

文件 说明
frontend-web/docs/openCanvas-progress.md 本文档
frontend-web/src/open-canvas/contexts/CanvasStreamContext.tsx 流单例 Context
frontend-web/src/open-canvas/hooks/useCanvasThreadStream.ts 主流 hook(LangGraph SDK)
frontend-web/src/open-canvas/hooks/useWritingRewrite.ts AI 改写状态机
frontend-web/src/open-canvas/api/rewrite.ts 改写 SSE 客户端
frontend-web/src/open-canvas/utils/quick-actions.ts 快捷动作消息构建
frontend-web/src/core/messages/utils.ts stripArtifactTag 在此
frontend-web/src/components/workspace/messages/message-list-item.tsx 调用 stripArtifactTag 的位置
frontend-web/src/styles/globals.css Tiptap 写作编辑器样式(.tiptap-writing-editor)
offline-backend-20260512/backend/packages/harness/deerflow/agents/canvas_agent/agent.py canvas_agent 工厂
offline-backend-20260512/backend/packages/harness/deerflow/agents/canvas_agent/tools.py update_artifact_tool
offline-backend-20260512/backend/packages/harness/deerflow/agents/canvas_agent/prompt.py 系统提示词
offline-backend-20260512/backend/app/gateway/routers/writing.py AI 改写后端接口
offline-backend-20260512/backend/langgraph.json canvas_agent 注册

九、开始新对话的推荐提示词

我们在开发 DeerFlow 的 Canvas 写作模式(open-canvas 集成)。
详细进度见:frontend-web/docs/openCanvas-progress.md

当前全部功能已完成,包括:
- Canvas 双栏布局、canvas_agent、版本历史
- AI Writing Editor(Tiptap + SSE 改写 + 思考过程展开)
- 快捷动作(通过 CanvasStreamContext 统一流)
- artifact 内容注入(消息里嵌入 <artifact> 标签,显示时剥离)

请根据进度文档第七节"潜在开发方向"或我指定的任务继续开发。