15 KiB
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→ 主聊天 Agentcanvas_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: thinkingSSE 事件传输 - 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,设计取舍)
- 版本切换不持久化:切换 artifact 版本后刷新页面会回到最新版本
- useCanvasStream 孤立:旧的直接 API 流 hook 保留但未被调用,可后续清理
- CanvasShell 的 setMessages 冗余:加载旧线程时
setMessages写入 CanvasGraphContext,但CanvasChatPanel读的是thread.messages(来自 useStream + fetchStateHistory)。setArtifact 仍有效 - 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> 标签,显示时剥离)
请根据进度文档第七节"潜在开发方向"或我指定的任务继续开发。