261 lines
9.1 KiB
Markdown
261 lines
9.1 KiB
Markdown
# Open Canvas 集成进度文档
|
||
|
||
> 更新时间:2026-05-16
|
||
> 当前分支:dhk-1
|
||
|
||
---
|
||
|
||
## 一、整体进度概览
|
||
|
||
| 阶段 | 名称 | 状态 |
|
||
|------|------|------|
|
||
| 前端阶段一 | 路由与目录骨架 | ✅ 已完成 |
|
||
| 前端阶段二 | Canvas 双栏布局 | ✅ 已完成 |
|
||
| 前端阶段三 | 类型与状态协议 | ✅ 已完成(随阶段一一并完成) |
|
||
| 后端阶段一 | ThreadState 扩展 | ✅ 已完成 |
|
||
| 后端阶段二 | canvas_agent 注册 + 最小节点闭环 | ✅ 已完成 |
|
||
| 前端阶段四 | API 适配层 | ✅ 已完成 |
|
||
| 前端阶段五 | 线程与流式状态接入 | ✅ 已完成 |
|
||
| 前端阶段六 | Artifact 渲染器增强(CodeMirror / Markdown) | ✅ 已完成 |
|
||
| 前后端阶段七 | 局部选区改写 | ✅ 已完成 |
|
||
| 前后端阶段八 | 快捷动作系统 | ✅ 已完成 |
|
||
| 前后端阶段九 | 本地模型支持完善 | ✅ 已完成 |
|
||
|
||
**当前状态**:全部 9 个阶段已完成 🎉
|
||
|
||
---
|
||
|
||
## 二、已完成工作详情
|
||
|
||
### 2.1 前端路由接入
|
||
|
||
修改了 `frontend-web/src/pages/PageRoutes.tsx`:
|
||
|
||
```tsx
|
||
import OpenCanvasRoutes from "@/open-canvas/routes/OpenCanvasRoutes";
|
||
// 在 PageLayout 内部添加:
|
||
<Route path="canvas/*" element={<OpenCanvasRoutes />} />
|
||
```
|
||
|
||
子路由:
|
||
- `/page/canvas` → `CanvasHomePage`(欢迎页 + 快速开始)
|
||
- `/page/canvas/:thread_id` → `CanvasThreadPage`(主工作区)
|
||
|
||
### 2.2 前端目录结构(已建立,26 个文件)
|
||
|
||
```
|
||
frontend-web/src/open-canvas/
|
||
api/
|
||
client.ts # stub,Phase 5 实现
|
||
models.ts # stub,Phase 5 实现
|
||
threads.ts # stub,Phase 5 实现
|
||
runs.ts # stub,Phase 5 实现
|
||
components/
|
||
layout/
|
||
CanvasShell.tsx # 顶层布局壳,flex-col
|
||
CanvasHeader.tsx # 顶部栏:返回键、折叠按钮、标题、流式指示
|
||
CanvasResizableLayout.tsx # 左右可拖拽双栏(react-resizable-panels)
|
||
chat/
|
||
CanvasChatPanel.tsx # 左栏:欢迎状态 + 消息列表 + 禁用输入框
|
||
artifact/
|
||
CanvasArtifactPanel.tsx # 右栏:空状态或渲染器
|
||
CanvasArtifactHeader.tsx # 右栏顶部:图标、标题、语言 Badge、版本导航
|
||
CanvasArtifactVersionNav.tsx # 版本 prev/next 导航
|
||
CanvasEmptyState.tsx # 空状态:5 种快速开始模板
|
||
TextArtifactRenderer.tsx # 文本渲染(Phase 6 增强为 Markdown)
|
||
CodeArtifactRenderer.tsx # 代码渲染(Phase 6 增强为 CodeMirror)
|
||
contexts/
|
||
CanvasThreadContext.tsx # threadId、modelName
|
||
CanvasGraphContext.tsx # messages、artifact、isStreaming
|
||
CanvasUIContext.tsx # isChatCollapsed、isEditing、selection 等 UI 状态
|
||
hooks/
|
||
useCanvasStream.ts # stub,Phase 5 实现
|
||
useCanvasArtifact.ts # stub,Phase 6 实现
|
||
useCanvasSelection.ts # stub,Phase 7 实现
|
||
useCanvasQuickActions.ts # stub,Phase 8 实现
|
||
pages/
|
||
CanvasHomePage.tsx # 欢迎页 + 快速开始入口
|
||
CanvasThreadPage.tsx # 三层 Provider 装配 + CanvasShell
|
||
routes/
|
||
OpenCanvasRoutes.tsx # React Router 子路由
|
||
types/
|
||
artifact.ts # CanvasArtifact、CanvasArtifactText、CanvasArtifactCode
|
||
thread.ts # CanvasMessage、CanvasThreadValues、CanvasThreadMetadata
|
||
model.ts # CanvasModel(含 supports_canvas_* 能力字段)
|
||
selection.ts # CanvasTextSelection、CanvasCodeSelection
|
||
quick-action.ts # QuickAction、QuickActionId
|
||
utils/
|
||
artifact.ts # getCurrentArtifactContent、appendArtifactVersion 等工具
|
||
selection.ts # stub
|
||
thread.ts # stub
|
||
stream.ts # stub
|
||
quick-actions.ts # stub
|
||
constants/
|
||
index.ts # CANVAS_AGENT_ID、PROGRAMMING_LANGUAGES、QUICK_ACTIONS_*
|
||
```
|
||
|
||
### 2.3 核心类型定义
|
||
|
||
```ts
|
||
// types/artifact.ts
|
||
export type CanvasArtifact = {
|
||
currentIndex: number; // 从 1 开始
|
||
contents: CanvasArtifactContent[];
|
||
};
|
||
|
||
export type CanvasArtifactText = {
|
||
index: number; type: "text"; title: string; fullMarkdown: string;
|
||
};
|
||
|
||
export type CanvasArtifactCode = {
|
||
index: number; type: "code"; title: string;
|
||
language: ProgrammingLanguageOptions; code: string;
|
||
};
|
||
```
|
||
|
||
### 2.4 Context 结构
|
||
|
||
```tsx
|
||
// CanvasThreadPage.tsx 的装配顺序
|
||
<CanvasThreadProvider initialThreadId={thread_id}>
|
||
<CanvasGraphProvider>
|
||
<CanvasUIProvider>
|
||
<CanvasShell />
|
||
</CanvasUIProvider>
|
||
</CanvasGraphProvider>
|
||
</CanvasThreadProvider>
|
||
```
|
||
|
||
---
|
||
|
||
## 三、已解决的技术难题
|
||
|
||
### 3.1 react-resizable-panels v4.6.2 API 差异
|
||
|
||
此版本与旧版 API 不同,必须使用:
|
||
|
||
```tsx
|
||
// ✅ 正确写法
|
||
import type { GroupImperativeHandle } from "react-resizable-panels";
|
||
const layoutRef = useRef<GroupImperativeHandle>(null);
|
||
|
||
<ResizablePanelGroup
|
||
orientation="horizontal" // ← 不是 direction
|
||
groupRef={layoutRef} // ← 不是 ref
|
||
>
|
||
<ResizablePanel id="chat" ... /> // ← 必须有 id
|
||
<ResizablePanel id="artifact" ... /> // ← 必须有 id
|
||
</ResizablePanelGroup>
|
||
|
||
// 切换折叠
|
||
layoutRef.current?.setLayout({ chat: 0, artifact: 100 }); // ← 对象形式,非数组
|
||
```
|
||
|
||
### 3.2 Flex 高度链(防止底部内容被裁剪)
|
||
|
||
```
|
||
PageLayout(h-screen)
|
||
└── main(flex-1)
|
||
└── CanvasShell div(h-full flex flex-col)
|
||
└── CanvasResizableLayout wrapper(flex-1 min-h-0 overflow-hidden)← 关键
|
||
└── ResizablePanelGroup(h-full)
|
||
└── 面板内容(h-full)
|
||
```
|
||
|
||
**`min-h-0` 是必须的**,否则 flex 子项不会收缩,底部内容被推出屏幕外。
|
||
|
||
### 3.3 折叠/展开按钮
|
||
|
||
`CanvasHeader.tsx` 中通过 `useCanvasUIContext()` 控制 `isChatCollapsed`,
|
||
`CanvasResizableLayout.tsx` 中用 `useEffect` 监听并调用 `setLayout`:
|
||
|
||
```tsx
|
||
useEffect(() => {
|
||
layoutRef.current?.setLayout(
|
||
isChatCollapsed ? { chat: 0, artifact: 100 } : { chat: 35, artifact: 65 }
|
||
);
|
||
}, [isChatCollapsed]);
|
||
```
|
||
|
||
---
|
||
|
||
## 四、下一步:后端 ThreadState 扩展
|
||
|
||
**目标文件**:`offline-backend-20260512/backend/packages/harness/deerflow/agents/thread_state.py`
|
||
|
||
**任务**:在现有 `ThreadState` 中新增以下 Canvas 专用字段,不影响 `artifacts: list[str]`(文件产物):
|
||
|
||
```python
|
||
# 新增 TypedDict(建议单独定义)
|
||
class CanvasArtifactText(TypedDict):
|
||
index: int
|
||
type: Literal["text"]
|
||
title: str
|
||
fullMarkdown: str
|
||
|
||
class CanvasArtifactCode(TypedDict):
|
||
index: int
|
||
type: Literal["code"]
|
||
title: str
|
||
language: str
|
||
code: str
|
||
|
||
class CanvasArtifact(TypedDict):
|
||
currentIndex: int
|
||
contents: list[CanvasArtifactText | CanvasArtifactCode]
|
||
|
||
# 在 ThreadState 中新增字段
|
||
artifact: Optional[CanvasArtifact] # Canvas 主文档(区别于 artifacts 文件列表)
|
||
highlighted_text: Optional[str] # 用户选中的文本片段
|
||
highlighted_code: Optional[str] # 用户选中的代码片段
|
||
canvas_mode: Optional[Literal["text", "code"]]
|
||
quick_action_id: Optional[str] # 快捷动作 id
|
||
web_search_enabled: bool # 默认 False
|
||
web_search_results: Optional[list[dict]]
|
||
```
|
||
|
||
**验收标准**:
|
||
- 旧线程无 `artifact` 字段时不报错
|
||
- `pnpm typecheck` 无新增错误(前端字段已对齐)
|
||
- `make test` 全量回归通过
|
||
|
||
**之后紧接**:`canvas_agent` 注册 + `factory.py` 修改 + 最小节点闭环
|
||
|
||
---
|
||
|
||
## 五、关键文件位置
|
||
|
||
| 文件 | 用途 |
|
||
|------|------|
|
||
| `frontend-web/docs/openCanvas.md` | 主需求文档 |
|
||
| `frontend-web/docs/openCanvas-frontend-checklist.md` | 前端详细清单 |
|
||
| `frontend-web/docs/openCanvas-backend-checklist.md` | 后端详细清单 |
|
||
| `frontend-web/docs/openCanvas-progress.md` | 本进度文档 |
|
||
| `F:\react01\openCanvas` | open-canvas 源码参考(本地克隆) |
|
||
| `offline-backend-20260512/backend/packages/harness/deerflow/agents/thread_state.py` | 下一步主改文件 |
|
||
| `offline-backend-20260512/backend/packages/harness/deerflow/agents/factory.py` | canvas_agent 注册位置 |
|
||
|
||
---
|
||
|
||
## 六、架构决策记录
|
||
|
||
| 决策 | 内容 |
|
||
|------|------|
|
||
| canvas_agent 方案 | 新增独立 canvas_agent,不改 lead_agent(方案 A)|
|
||
| 路由形式 | `/page/canvas/*`,挂在 PageLayout 内 |
|
||
| artifact 字段共存 | `artifact`(Canvas 文档)与 `artifacts: list[str]`(文件列表)独立共存 |
|
||
| 模型配置真源 | 后端 config.yaml,前端动态加载 /api/models |
|
||
| 前端 artifact 索引 | 从 1 开始(与 open-canvas 对齐) |
|
||
| 流式首版策略 | 节点完成后输出完整 artifact 对象,不做 token 级局部流 |
|
||
|
||
---
|
||
|
||
## 七、开始新对话的推荐提示词
|
||
|
||
```
|
||
我们在集成 open-canvas 到 DeerFlow 项目。
|
||
参考进度文档:frontend-web/docs/openCanvas-progress.md
|
||
目前前端阶段一/二/三已完成,下一步是后端 ThreadState 扩展。
|
||
请从第四节"下一步:后端 ThreadState 扩展"开始继续。
|
||
```
|