# 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 内容以 `` 标签嵌入人类消息末尾。 - 普通聊天:`sendMessage` 内部自动追加(仅当消息里还没有 `` 时) - 快捷动作:`buildQuickActionMessage(id, content)` 在构建时就包含内容 - 聊天面板显示时:`message-list-item.tsx` 调 `stripArtifactTag()` 剥掉标签,用户看不到 ```typescript // utils/quick-actions.ts export function buildQuickActionMessage(id, content) { const body = isCodeArtifact(content) ? content.code : content.fullMarkdown; return `${instruction}\n\n\n${body}\n`; } // core/messages/utils.ts export function stripArtifactTag(content: string): string { return content.replace(/]*>[\s\S]*?<\/artifact>/g, "").trim(); } ``` ### 3.3 ThinkTagParser(AI 改写思考过程解析) 后端支持两种思考格式: - Anthropic:独立 thinking block,以 `event: thinking` SSE 事件传输 - DeepSeek:把思考过程内联在文本里:`...content` 前端 `useWritingRewrite.ts` 的 `ThinkTagParser` 是流式状态机,处理跨 chunk 边界的 `` 标签。 ### 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 在文本流里混入 `` 标签 | `ThinkTagParser` 状态机,保留最后 6/7 字节防止跨 chunk 分割 | | loading→success 状态转换检测(stale closure) | 用 `prevStatusRef` + `useEffect` 检测 status 变化,取代直接读 state | | 快捷动作响应不出现在聊天面板 | `CanvasStreamContext` 统一 `sendMessage` 入口 | | LLM 看不到 artifact 内容(只有 state.messages) | 消息里直接嵌入 `` 标签,显示时剥离 | | `` 标签在聊天面板里显示 | `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 消耗较大。当前规模可接受 --- ## 六、快速开始下一次工作 ### 启动服务 ```bash # 后端(从 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` ### 类型检查 ```bash 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 内容注入(消息里嵌入 标签,显示时剥离) 请根据进度文档第七节"潜在开发方向"或我指定的任务继续开发。 ```