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

313 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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()` 剥掉标签,用户看不到
```typescript
// 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 消耗较大。当前规模可接受
---
## 六、快速开始下一次工作
### 启动服务
```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 内容注入(消息里嵌入 <artifact> 标签,显示时剥离)
请根据进度文档第七节"潜在开发方向"或我指定的任务继续开发。
```