313 lines
15 KiB
Markdown
313 lines
15 KiB
Markdown
# 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> 标签,显示时剥离)
|
||
|
||
请根据进度文档第七节"潜在开发方向"或我指定的任务继续开发。
|
||
```
|