我把 AI 画布项目“拆到螺丝级”:Infinite Canvas 如何把 Next.js、localForage、多模型生成与本地 Agent 组装成一条可用生产线
先抛个问题你现在做一张图通常要在多少个窗口来回横跳提示词在记事本、参考图在网盘、生成历史在聊天记录、素材在某个“临时文件夹永远找不到”最后把结果拖进 PPT 再手动对齐。这套流程最大的问题不是“慢”而是上下文断裂你每次创作都像失忆重启。这篇文章聊的项目是infinite-canvas。它不只是一个“能生图的页面”而是把创作过程拆成可编排节点再把 AI 生成、素材沉淀、会话上下文、跨设备同步、Agent 自动化全部塞进同一张无限画布里。说人话它试图把“灵感流”变成“生产流”。我会基于项目真实代码web/canvas-agent/做一次完整拆解重点讲清楚三件事它的技术架构到底怎么分层为什么这么分核心实现里有哪些值得借鉴的工程决策这套设计在实际业务里能落到哪些场景以及下一步怎么演进。全文较长建议先收藏再看。看完你会发现“无限画布”真正难的不是画布而是状态与上下文治理。一、项目背景为什么不是“再做一个生图页面”过去两年AI 创作工具多到像手机里的拍照 App一个负责文生图一个负责图生图一个负责视频一个负责 prompt 优化再加一个素材站、一个剪贴板管理器创作者在工具之间反复搬运数据。infinite-canvas的核心判断很直接创作是链路不是单点请求一次输出通常依赖前序文本、参考图、参数试验和多轮迭代。上下文比模型更重要模型会升级但“这个结果从哪来、为何变成这样”必须可追溯。本地优先仍然有价值对个人创作者来说先把体验跑通未必要先上后端大系统。所以它把“节点 连线”当成创作语言文本节点写意图图片/视频/音频节点放素材配置节点定义生成参数连线表达依赖关系。你不是“点按钮生成”而是在“搭工作流”。二、整体架构这不是一个页面而是五层协作先给一张文字版架构图帮助你快速定位模块┌────────────────────────────────────────────────────────────┐ │ 浏览器前端 (Next.js) │ │ 画布交互层: 节点/连线/缩放/历史/快捷键 │ │ 生成编排层: 文本/图像/视频/音频统一调度 │ │ 助手层: 在线 Agent 本地 Agent 面板 │ ├────────────────────────────────────────────────────────────┤ │ 本地数据层 (Zustand localForage) │ │ 项目JSON、素材JSON、图片Blob、媒体Blob、生成日志 │ ├────────────────────────────────────────────────────────────┤ │ AI 调用层 (OpenAI/Gemini/Seedance) │ │ 多渠道配置、模型能力分组、接口格式适配、流式响应处理 │ ├────────────────────────────────────────────────────────────┤ │ 可选同步层 (WebDAV Next.js Proxy) │ │ 清单合并、增量文件上传、缺失媒体补齐、跨设备迁移 │ ├────────────────────────────────────────────────────────────┤ │ 本机 Agent 层 (canvas-agent MCP Codex) │ │ SSE事件、工具调用确认、线程管理、工作区隔离 │ └────────────────────────────────────────────────────────────┘这套架构最妙的地方是在线能力和本地能力并行存在互不强绑。只想“开箱即用”直接用网页在线 Agentcanvas-assistant-panel.tsx想让本机 Codex/Claude 深度参与启本地canvas-agent走 MCP 工具链路这比“全在线”或“全本地”的单一路径更贴近真实用户分层。三、数据模型设计把“创作过程”变成可序列化对象1项目模型CanvasProject项目状态由useCanvasStore管理核心结构包括nodes节点数组connections连线数组chatSessions/activeChatId助手对话上下文backgroundMode/showImageInfo/viewport界面和视图状态也就是说项目不是只存“画面”而是把“聊天过程”和“工作区观感”都保存下来回到项目时能接续上下文。2节点模型CanvasNodeData项目支持五类节点代码真实定义textimageconfigvideoaudiometadata承担业务语义例如生成状态idle/loading/success/error生成参数model/size/quality/count/...媒体元信息storageKey/mimeType/bytes/naturalWidth/...批量关系isBatchRoot/batchRootId/batchChildIds/primaryImageId这套模型的意义在于节点既是 UI 元素也是业务单元。你可以把它当“可视化工作流 DSL”。四、交互内核无限画布为什么“丝滑”不少画布类应用失败不是因为功能少而是拖起来卡、撤销不准、误操作多。这个项目在交互层做了几件很实在的事。1视口变换统一处理InfiniteCanvas组件维护viewport { x, y, k }所有坐标换算都通过screenToCanvas统一转换。滚轮缩放采用“鼠标锚点缩放”而不是固定中心缩放用户体感会自然很多。2拖拽期间暂停历史提交节点拖动时代码将historyPausedRef.current true结束拖拽再恢复。这样不会在拖动过程写进几十条“半成品快照”。3历史系统做了“延迟合并提交”历史不是每次setState都立刻压栈而是 180ms 合并提交最多保留 50 步slice(-49) 当前。// 精简自 canvas-client-page.tsx if (historyCommitTimerRef.current) clearTimeout(historyCommitTimerRef.current); historyCommitTimerRef.current setTimeout(() { historyRef.current.past [...historyRef.current.past.slice(-49), last]; historyRef.current.future []; lastHistoryRef.current current; }, 180);这种“短窗口聚合”对交互系统很关键既保留撤销可用性又避免历史膨胀。4复制粘贴不仅复制节点还复制内部连线copySelectedNodes会把“选中集合内部的连接关系”一并写入剪贴板pasteCopiedNodes再通过idMap重建连线。你复制的是“局部子图”不是“孤立节点”。5连接拖到空白区弹出“创建并连接”菜单这是一个很容易被忽略但体验很强的设计。当用户从节点拖一条连接到空白处不是报错而是弹菜单让你“新建文本/图片/视频/配置节点并自动连线”。工作流搭建速度会明显提升。五、生成引擎统一入口分模式执行核心生成入口在canvas-client-page.tsx的handleGenerateNode。它不是“一个按钮调一个 API”而是一个完整状态机读取来源节点与配置构造上下文文本、参考图、参考视频、参考音频根据模式分支image/video/audio/text按节点更新状态loading/success/error生成成功后落地为新节点并建立连线记录并管理AbortController支持中断1上下文构造从“连接图”里抽资源buildNodeGenerationContextbuildNodeGenerationInputs会沿连线找上游资源。如果是配置节点并启用了 composer还会解析[node:xxx]引用语法把节点内容替换为标签文本并注入对应媒体引用。// 精简自 canvas-node-generation.ts for (const match of prompt.matchAll(/\[node:([^\]])\]/g)) { const input inputByNodeId.get(match[1]); // 文本引用替换为“【文本1】”图片/视频/音频替换为标签并进入 references }这一点非常像“可视化 prompt 模板引擎”而且是和画布图结构绑定的。2图片批量生成根节点 子节点组当count 1时不是简单塞一个数组而是创建一个根节点isBatchRoot多个子节点batchRootId根子连线并支持折叠/展开批量结果切换主图primaryImageId失败重试保留上下文这就是“把 AI 结果结构化”而不是“吐一堆图”。3文本生成也走同一逻辑文本模式同样可以从配置节点批量生成多个文本子节点。并且支持流式回写requestImageQuestion回调里持续更新节点内容用户能看到“边生成边长字”。4中断机制项目用generationRequestsRef: MaptargetNodeId, request跟踪请求stopGenerationByRunningId会批量abort()同一批运行。这比“全局一个 loading 标志位”靠谱得多适合并发节点生成场景。六、媒体存储策略JSON 轻量Blob 独立很多类似项目把 base64 直接塞进项目 JSON前期方便后期灾难。这个项目走了更稳妥的路结构化状态项目、素材走localForage app_state图片 Blob 单独进image_files视频/音频 Blob 单独进media_files业务里通过storageKey关联uploadImage的处理非常典型const storageKey image:${nanoid()}; await store.setItem(storageKey, blob); const url URL.createObjectURL(blob); objectUrls.set(storageKey, url);这个设计的好处项目 JSON 更小、更容易序列化导入导出大文件不反复 JSON 编码解码支持按引用清理避免误删共享媒体旧数据可迁移发现data:image/...时自动转存 Blob 并补storageKey清理策略也很工程化cleanupUnusedImages/cleanupUnusedMedia会递归扫描“项目 素材 额外上下文”里所有storageKey再删除未被引用的 Blob。这是一种“引用可达性清理”而不是“谁删节点谁删文件”的脆弱策略。七、多模型接入不是多写几个下拉框那么简单配置中心use-config-store.ts做了一套我很认可的建模方式模型值编码为channelId::modelName统一函数resolveModelRequestConfig在请求前解析为真实baseUrl/apiKey/apiFormat/model能力维度分组imageModels/videoModels/textModels/audioModels// 精简自 use-config-store.ts export function resolveModelRequestConfig(config, value) { const channel resolveModelChannel(config, value); return { ...config, model: modelOptionName(value || config.model), baseUrl: channel.baseUrl, apiKey: channel.apiKey, apiFormat: channel.apiFormat, }; }这让“同一个页面调用不同供应商”变得很顺图片OpenAIimages/generations GeminigenerateContent文本/工具OpenAIresponsesSSE GeministreamGenerateContent视频OpenAI 风格videos或 Seedance 任务接口音频OpenAIaudio/speechGemini 通道会明确提示不支持另外项目还处理了不少边角火山 Agent Plan Base URL 自动规范化避免重复拼/v1图像尺寸支持比例转实际像素并做边界校验长边、像素总量、16 倍数错误提示按状态码细分401/403、429 等这类“脏活”才是多模型接入真正的工程成本。八、提示词库不是静态文档而是可检索内容池/api/promptsroute 做了三件事抓取多个 GitHub 仓库原始内容README、JSON、案例文件解析为统一 Prompt 结构标题、标签、预览、来源用内存缓存 TTL1 小时减少重复拉取它还用了loadingPromptsPromise 做并发去重同一时刻多个请求进来只触发一次真实拉取。这让“提示词库”从静态页面变成了可维护的外部知识源。九、WebDAV 同步可选云不强依赖后端本项目强调本地优先但提供了跨设备同步能力。关键思路是“按领域分清单”而不是搞一锅大 JSON。同步域分成四块canvasassetsimage-workbenchvideo-workbench每个域各有一个manifest.json包含data结构化业务数据filesBlob 索引storageKey/path/mimeType/bytes同步流程大致是拉远端清单合并本地与远端按id 时间戳下载本地缺失媒体上传本地新增或变更媒体回写新清单媒体传输用并发池默认 3控制吞吐// 精简自 app-sync.ts await runWithConcurrency(tasks, 3, async ({ item, blob }) { await uploadWebdavFile(config, item.path, blob, item.mimeType); });如果 WebDAV 服务 CORS 不友好还能切nextjs代理模式走webdav-proxyroute 转发。这个设计对 NAS 用户和自建 WebDAV 用户非常实用。十、最有辨识度的链路在线 Agent 本地 Agent 双系统这是我认为这个项目最“有产品想法”的地方。它没有把 Agent 强行做成单一路径而是拆成两个互补模式。10.1 在线 Agent网页内置核心在canvas-assistant-panel.tsx预定义大量工具 schemacanvas_create_node、canvas_generate_image、canvas_apply_ops等每轮请求都把当前画布快照精简版和选中引用发给模型工具循环分两阶段第一步tool_choice: required强制模型先用工具后续tool_choice: auto自动续跑最多 4 步ONLINE_AGENT_MAX_STEPS如果开启“工具确认”写操作会先进入待审批消息再由用户批准执行。这让在线 Agent 既有自动化能力又能控制风险。10.2 本地 Agentcanvas-agent前端面板canvas-local-agent-panel.tsx通过EventSource连接本机服务hello连接建立tool_call请求网页执行画布工具agent_eventCodex 结构化事件流agent_done一轮完成本地模式的关键特性上传图片可作为附件传给本地 Codex限制 6 张约 28MBcanvas_apply_ops默认二次确认防止误改可查看本地线程历史、恢复会话、删除会话从体验上看它把“浏览器画布”和“本机编码 Agent”连成了一个工作台。10.3 canvas-agent 服务端SSE MCP 线程工作区canvas-agent本身是一个 Node 服务入口在http-server.ts/agents.ts。几个关键点非常值得看仅监听 127.0.0.1默认本机可见不做公网暴露。token 鉴权 origin 记忆首次带 token 的 origin 会被记录后续只允许已登记来源访问。画布工具请求-回执闭环服务端发tool_call网页执行后POST /canvas/result回传服务端 resolve pending promise。每个画布独立 Codex 工作区目录在~/.infinite-canvas/codex-workspaces/canvasId减少线程串场。Codex app-server 直连通过openai/codex的app-server --stdio走结构化事件流。路由骨架如下精简app.get(/events, ...); // SSE 事件通道 app.post(/canvas/state, ...); // 前端上传当前快照 app.post(/canvas/result, ...); // 前端回传工具执行结果 app.post(/api/tools, ...); // MCP/HTTP 工具调用入口 app.post(/agent/codex/turn, ...);// 发起本地 Codex 一轮它还提供mcp模式canvas-agent mcp启动后把同一套工具注册到 MCP命令行 Codex/Claude 可以直接调画布工具。这就形成了“网页助手”和“终端助手”共享工具协议的统一面。十一、从 0 到 1 怎么用一条最顺手的实践路径下面这套流程基本是我自己验证后认为最顺手的步骤 1先把画布跑起来cd web bun install bun run dev打开http://localhost:3000右上角配置里填渠道 Base URLAPI Key文本/图片/视频/音频模型步骤 2搭一个“可复用生成流”新建文本节点写核心需求新建配置节点选择generationMode连线文本 - 配置补充参考图节点再连到配置节点触发生成并保留结果节点与连线这样下次改文案时不需要重造结构只改局部节点即可。步骤 3把结果沉淀进素材把图片/视频/文本节点直接“加入我的素材”后续在画布、工作台、提示词库联动时复用。这个动作看似小长期会变成你的私有内容资产库。步骤 4可选接本地 Agent 提升生产效率npx -y basketikun/canvas-agent复制终端输出的 Local URL 和 token填到 Agent 面板连接。之后你可以直接说“把左侧三张图做成 9:16 视频流并给我三版文案”。十二、真实应用场景它不是只能“玩图”场景 1品牌海报快速迭代文本节点放品牌调性和卖点图片节点放产品图和风格参考配置节点控制模型和比例批量生成后根节点收敛主图优点版本关系清楚甲方让“改回第三版字体风格”时不崩溃。场景 2短视频分镜草案文本节点写分镜描述图片节点做视觉参考视频节点串联结果音频节点尝试口播或背景音优点同一画布里把文案、画面、声音关联起来不再跨工具拼接。场景 3教育内容工业化一套课程模板画布复用多次每个知识点替换文本节点自动生成图文素材结果批量沉淀素材库供下一轮课程复用优点降低制作波动团队协作更可复制。场景 4电商上新素材流水线商品图做参考节点文案节点输出卖点变化批量生图得到主图、详情图、活动图通过 WebDAV 同步给多设备或多成员查看优点效率提升来自流程稳定而不仅仅是模型速度。场景 5一人 AI 工作室如果你是“写文案 做图 剪视频 发内容”全包选手这类项目最大的价值是把脑内切换成本降下来。你终于可以把注意力放在“审美与表达”不是“文件去哪了”。十三、工程层面的几个高价值细节这里总结几个我认为非常“可抄作业”的点状态与二进制分层存储JSON 和 Blob 分仓避免状态肥大。历史提交合并窗口拖拽和频繁编辑时不污染撤销栈。图结构驱动上下文生成输入来自连线关系不靠临时参数。统一工具协议在线 Agent、本地 Agent、MCP 共享同一语义。可控自动化写操作可二次确认防止 Agent 误伤。多模型适配前置请求前解析渠道、格式、模型能力避免页面散落判断。同步域拆分WebDAV 清单按业务域分治迁移和排障都轻松。工作区隔离本地 Agent 线程按画布隔离 cwd降低跨项目污染。事件流透明化日志和诊断面板让“Agent 到底干了啥”可见可追。失败可恢复重试、取消、错误细分信息贯穿生成链路。一句话总结它不是“功能堆叠”而是“边界清晰的系统拼装”。十四、当前限制与务实建议先说限制这很重要项目和素材默认保存在浏览器本地不是天然云端协作产品API Key 在浏览器本地保存适合个人可信环境部分上游接口对本地参考媒体支持程度不一移动端触控体验还不是这个项目当前主战场。如果你打算用于团队生产建议优先做三件事固化“模型渠道规范”和默认参数模板打通 WebDAV 或对象存储至少先做可追溯备份对关键画布建立命名规范节点命名、版本命名、导出命名。别小看这三件基础治理能省下大量返工。十五、未来趋势下一代创作工作台会长什么样结合这个项目的现状我认为它最值得继续演进的方向有六个1多用户实时协作CRDT / OT从“个人画布”进化到“多人同步编辑”需要真正的冲突解决与权限模型。这一步难但一旦做成产品形态会从工具跃迁成平台。2远端媒体分层存储本地优先可以保留但应提供“冷热分层”热数据本地、冷数据对象存储避免长期本地空间膨胀。3工作流模板化把“高复用节点子图”沉淀为模板例如广告图模板、分镜模板让新任务一键套用不必从空白画布开始。4语义检索素材库现在素材检索主要是关键词后续可加入向量检索支持“找一张类似这个情绪但更明亮的图”。5模型能力自动协商根据任务类型、预算、时延目标自动选择渠道与模型不再靠人工切换。6Agent 编排与审计把 Agent 工具调用转成“可回放任务清单”并支持策略审计哪些操作必须人工确认哪些可自动执行。十六、源码级链路复盘一次“从需求到结果”到底经历了什么前面讲了架构现在我们按真实调用顺序复盘一次常见链路。假设用户在画布里做了这件事“我给你一段产品文案 两张参考图帮我出 4 张风格统一的海报初稿。”第 0 步用户动作进入画布状态用户把文案贴到文本节点把两张图拖入画布变成图片节点再新建一个配置节点并连接。这一刻useCanvasStore和画布页面内部状态里已经形成了“可计算图结构”。这很关键如果你只在按钮点击时临时拼参数就无法稳定复用。而图结构一旦形成每次生成都是“同一流程不同输入”。第 1 步生成入口拿到稳定快照handleGenerateNode被触发时会先基于当前节点集合和连线集合构建上下文。这一步不是直接调接口而是先判断目标节点是什么类型上游是否存在可用文本输入是否有图片/视频/音频参考当前模型配置是否完整是否存在进行中的同批次请求。如果配置不齐流程会在这里短路并写入可读错误信息。这比“后端 400 再兜底弹框”用户体验要好太多。第 2 步上下文提取与 prompt 组装buildNodeGenerationContext会把连线图中的输入抽成可消费结构。如果你在配置节点里使用[node:xxx]则hydrateNodeGenerationContext会做两件事把引用替换成可读标签方便模型理解角色把媒体引用转为实际可上传资源列表。你可以把这层理解成“面向模型的中间表示IR”。第 3 步模式分发与供应商适配到了 API 层代码会根据generationMode进入不同 service图片走image.ts视频走video.ts音频走audio.ts文本走问答/工具调用逻辑同一模式内部还要根据渠道格式分支OpenAI 兼容风格Gemini 风格Seedance 任务式风格视频。这里最容易写崩的是“参数矩阵爆炸”。项目的做法是尽量把“通用参数”前置归一化再在最后一跳做供应商映射降低分支复杂度。第 4 步流式过程可视化无论是文本流、工具流还是任务轮询UI 都在节点上显示过程状态。用户看到的是“节点正在生长”不是“按钮转圈等奇迹”。以文本为例流式响应会持续回调更新节点内容以视频为例任务创建后轮询状态成功后再下载并存储本地 Blob。第 5 步结果落地与关系维护生成成功并不是结束。项目会继续完成以下动作创建新节点或更新目标节点建立来源与结果的连线关系写入媒体storageKey更新批量根节点元数据主图、子图列表清理进行中请求引用。这一步确保“结果不是孤儿”而是回到可追踪图结构里。第 6 步历史与持久化收敛节点变更最终进入历史系统并延迟提交随后触发 store 持久化。持久化采用zustand persist localForage项目结构和媒体索引写入状态仓媒体实体写入 Blob 仓。用户刷新页面几乎总能回到之前那一刻。十七、落地时最容易踩的 10 个坑附规避建议如果你也在做类似系统下面这些坑大概率会遇到。我把“症状 - 原因 - 建议”写在一起方便直接对照。1症状撤销一团乱麻原因把拖拽过程中每一帧都入历史。建议拖拽期间暂停 history结束后一次性提交并加时间窗口合并。2症状项目文件越来越大导出越来越慢原因把 base64 媒体塞进 JSON。建议结构化状态与媒体实体分离存储状态只保存storageKey引用。3症状用户说“我改了参数但生成结果像没变”原因上下文来源混乱输入不是从图结构统一提取。建议统一从连线图推导输入不允许页面局部拼参数覆盖图语义。4症状支持第三家模型后代码分支爆炸原因页面层直接写供应商判断。建议建立配置归一化层把供应商差异压到 service 末端映射。5症状Agent 偶尔“误删半个画布”原因写工具无确认机制且缺乏可回滚快照。建议写操作默认确认批量操作前保存agentUndoSnapshot可一键回退。6症状本地 Agent 偶尔请求失败浏览器报跨域原因本机服务没有正确处理 origin 或私网访问头。建议本地服务固定127.0.0.1同时维护 token 允许来源白名单。7症状跨设备同步后媒体“节点在图不在”原因只同步了 JSON没同步 Blob 文件。建议清单里必须包含文件索引按索引补齐缺失媒体后再渲染。8症状并发生成时偶发状态串台原因全局单 loading 状态、无请求实例隔离。建议按runningId或targetNodeId建立请求 Map取消时精准中断。9症状Prompt 系统卡顿打开页面就慢原因每次都实时抓取第三方仓库。建议加 TTL 缓存 Promise 去重首个请求加载后复用结果。10症状用户不信任 Agent 自动化原因过程不可见只给最终结果。建议提供事件流日志、工具调用摘要、逐步确认和失败可诊断信息。结语真正的门槛不是模型而是“可持续创作系统”infinite-canvas给我的最大启发是当大家都在卷“再快一点出图”时它在卷另一件更难但更值钱的事——如何把创作过程沉淀成可复用、可追踪、可协作的系统。如果你只把它当“生图工具”会觉得它功能挺多。如果你把它当“创作操作系统”的雏形你会发现它很多看似琐碎的设计节点 metadata、历史快照、工具协议、同步清单、线程工作区其实都在为同一件事服务让创作从“灵光一闪”变成“稳定交付”。说得直白一点今天你能做出一张好图很厉害明天你还能稳定做出第 100 张而且知道每一张怎么来的这才是工程价值。附关键代码入口索引建议二刷时对照画布主逻辑web/src/app/(user)/canvas/[id]/canvas-client-page.tsx生成上下文web/src/app/(user)/canvas/components/canvas-node-generation.ts资源引用推导web/src/app/(user)/canvas/utils/canvas-resource-references.ts在线 Agentweb/src/app/(user)/canvas/components/canvas-assistant-panel.tsx本地 Agent 面板web/src/app/(user)/canvas/components/canvas-local-agent-panel.tsx画布状态存储web/src/app/(user)/canvas/stores/use-canvas-store.ts素材状态存储web/src/stores/use-asset-store.ts配置中心web/src/stores/use-config-store.ts图片 API 适配web/src/services/api/image.ts视频 API 适配web/src/services/api/video.ts音频 API 适配web/src/services/api/audio.tsWebDAV 同步web/src/services/app-sync.ts、web/src/services/webdav-sync.ts提示词聚合 routeweb/src/app/api/prompts/route.ts本地 Agent 服务canvas-agent/src/http-server.ts本地 Agent 会话桥canvas-agent/src/canvas-session.tsCodex 集成canvas-agent/src/agents.tsMCP 服务canvas-agent/src/mcp-server.ts如果你准备把这套思路用在自己的项目里建议先从两件事开始把“生成请求”升级成“节点化工作流”把“结果落地”升级成“可追溯状态 独立媒体存储”。先把这两步走稳你的 AI 创作系统就已经超过大多数“只会调接口”的项目了。更多AIGC文章RAG技术全解从原理到实战的简明指南更多VibeCoding文章