生图还在“盲盒抽卡”?起底 BigBanana Canvas:如何用 Next.js + SSE + MCP 协议,让 AI 在无限画布上“指点江山”!
你是否也曾有过这样的抓狂时刻: 在某个主流 AI 聊天框里输入了一句精心雕琢的 Prompt,满怀期待地敲下回车,结果生成了一张“除了脸好看,背景、姿势、画风全错”的图片。你叹了口气,在对话框里追问:“把背景改成森林,姿势换成奔跑。” AI 爽快地答应了,然后……重新给你生成了一张脸完全不同、画风突变的新图。 你一拍桌子:“我要的是刚才那张图的脸,只是改背景!” AI 委屈巴巴:“对不起,我是个语言模型,每次生成都是随机的呢亲~”
这时候,你看着那条长长的、只能往上滚动的聊天记录,陷入了沉思:为什么在 2026 年,我们还在用 30 年前 BBS 论坛时代的“盖楼贴条”方式,来和代表人类科技巅峰的 AI 进行多模态创作?
这种线性的、单维度的、没有空间感的“聊天框”交互,简直就是 AI 创作的“紧箍咒”。生图变成了“盲盒抽卡”,每次敲回车都是一次命运的博弈。你无法直观地对比多张图,无法将 A 图的局部和 B 图的提示词拼接,更无法把创作过程像思维导图一样发散和沉淀。
难道就没有一种更爽、更直观、更符合人类直觉的创作方式吗?
答案是:无限画布(Infinite Canvas)。
今天,我们要扒开一款颠覆性生图工作台——BigBanana Canvas(体验地址:BigBanana Canvas - 无界画板生图创作工具)的底层代码,带大家看看如何利用 Next.js App Router + Zustand + localforage + SSE + MCP 协议,构建一个本地优先、去中心化同步、且能让终端 AI 代理(Agent)直接在画布上“指点江山”的未来级创作系统。
准备好可乐和爆米花,这是一篇长达万字的硬核技术干货,我们将从宏观架构、前端渲染、本地存储、跨设备同步、以及 AI 代理双向通信等多个维度,为你彻底拆解无限画布生图背后的核心技术奥秘!
一、 宏观架构:前端、本地 Agent 与 AI 终端的“三位一体”
在传统的 Web 应用中,架构通常是极简的“客户端-服务器-数据库”三层结构。但对于一个需要支持本地高隐私、大体积媒体文件、且能与本地 AI 终端(如 Codex、Claude Code)深度协同的生图画布来说,传统的架构根本无法支撑。
BigBanana Canvas 另辟蹊径,设计了一套精妙的“三位一体”宏观架构:
+-----------------------------------------------------------------------------------+
| 1. 浏览器前端 (Next.js) |
| +---------------------------+ +------------------------+ +------------------+ |
| | Zustand (状态管理) | | localforage (本地存储) | | WebDAV Sync 模块 | |
| +---------------------------+ +------------------------+ +------------------+ |
+------------------------------------------^---------------------------|------------+
| |
SSE (Server-Sent Events) HTTPS Proxy
| |
+------------------------------------------|---------------------------v------------+
| 2. 本地代理 (Canvas Agent) |
| +-----------------------------------------------------------------------------+ |
| | Express HTTP Server (管理本地工作区、线程、SSE 客户端、工具执行) | |
| +-----------------------------------------------------------------------------+ |
| | MCP Adapter (模型上下文协议适配器,将画布操作包装为标准 MCP Tools) | |
| +-----------------------------------------------------------------------------+ |
+------------------------------------------^----------------------------------------+
|
Stdio / MCP
|
+------------------------------------------v----------------------------------------+
| 3. AI 终端 (Codex / Claude Code) |
| +-----------------------------------------------------------------------------+ |
| | 大模型大脑 (通过调用 MCP Tools 实时读取画布状态、生成节点、编排工作流) | |
| +-----------------------------------------------------------------------------+ |
+-----------------------------------------------------------------------------------+
这套架构由以下三个核心角色组成:
-
浏览器前端(Next.js App Router): 这是用户的主战场。它运行在浏览器中,负责高性能的画布渲染、丝滑的平移缩放、节点拖拽、连线高亮等交互。最酷的是,它是一个本地优先(Local-First)的应用,所有的画布数据、生成历史、甚至大体积的图片/视频 Blob,都通过
localforage存储在用户本地的 IndexedDB 中。同时,它支持通过内置的 WebDAV 客户端,将数据加密同步到用户自己的私有云(如群晖 NAS、坚果云等)。 -
本地代理(Canvas Agent): 这是一个运行在用户本地电脑上的轻量级 Node.js 服务。为什么要有一个本地 Agent?因为浏览器是一个“沙盒”,它无法直接读取用户的本地文件系统,也无法直接启动本地的命令行工具。Canvas Agent 充当了“破局者”,它不仅管理着本地的开发工作区,还通过 SSE(Server-Sent Events) 与浏览器前端建立了一条双向、实时的通信管道。
-
AI 终端(Codex / Claude Code / Cursor 等): 这是真正的“AI 程序员/创作助手”。它通过 Anthropic 推出的 MCP(Model Context Protocol,模型上下文协议) 与 Canvas Agent 进行标准化的 stdio 通信。AI 终端不需要知道网页端是如何实现的,它只需要调用 Canvas Agent 暴露的
canvas_get_state、canvas_apply_ops等标准工具,就能像魔术师一样,直接在用户的浏览器画布上创建文本、生成图片、连接工作流。
这三者之间的数据流和控制流是如何闭环的?
当你在本地终端向 AI 助手下达指令:“在画布上帮我创建一个文生图工作流,提示词是‘一只戴着墨镜的酷猫’,并自动运行。”
-
第一步:AI 终端解析指令,发现需要操作画布,于是调用 MCP 工具
canvas_create_image_prompt_flow。 -
第二步:Canvas Agent 收到 MCP 请求,将其转化为具体的画布操作指令(Ops),并通过 SSE 实时推送给浏览器前端。
-
第三步:浏览器前端收到 SSE 指令,在画布上动态绘制出文本节点和生成配置节点,并自动建立连线,随后调用本地配置的 AI 接口开始生图。
-
第四步:前端执行完毕后,将结果(如节点 ID、图片状态)通过 POST 请求返回给 Canvas Agent 的
/canvas/result接口。 -
第五步:Canvas Agent 唤醒挂起的 Promise,将执行结果返回给 AI 终端。AI 终端在控制台优雅地回复:“主人,生图工作流已为您搭建并启动,请看浏览器!”
这种“空间协同”的体验,直接把 AI 交互从“纸上谈兵”提升到了“指点江山”的全新维度!
二、 前端核心:如何用 Next.js 打造一个丝滑的“无限画布”?
要让用户在画布上连续推演,画布的性能和交互体验必须是工业级的。如果拖动一下画布就掉帧,或者缩放时图片出现明显的锯齿和延迟,用户的创作灵感瞬间就会被浇灭。
1. 为什么不用 Canvas 绘图库,而是选择原生 DOM + CSS Transform?
在立项之初,很多团队会下意识地选择 PixiJS、Fabric.js 甚至 Three.js 这种基于 WebGL/Canvas 的重型绘图引擎。但深入思考后,你会发现这对于一个“AI 创作工作台”来说,反而是个深坑:
-
组件复用性极差:AI 生图需要大量的复杂 UI,比如富文本输入框、模型选择下拉菜单、带进度条的按钮、裁剪对话框等。如果用纯 Canvas 绘制这些组件,相当于要手写一套 UI 框架,工作量爆炸且体验极差。
-
DOM 的天然优势:如果使用原生 HTML/CSS,我们可以直接复用 React 生态和 Ant Design 的顶级组件。文本节点就是一个普通的
<textarea>,配置面板就是一个标准的 Ant Design<Form>。
因此,BigBanana Canvas 采用了 “DOM 承载内容,CSS Transform 承载视口变换” 的轻量级高性能方案。整个画布的缩放和平移,完全交给浏览器的 GPU 加速渲染。
2. 核心实现:PointerEvents 与 requestAnimationFrame 的完美探戈
在实现画布的平移(Pan)和缩放(Zoom)时,最忌讳的就是在 onPointerMove 事件里同步调用 React 的 setState 触发重新渲染。因为 Pointer 移动事件的触发频率极高(每秒上百次),如果每次都触发 React 的 Fiber 树调和,页面不卡死才怪。
BigBanana Canvas 在 infinite-canvas.tsx 中使用了一种非反应式(Non-reactive)的 Ref 缓存 + requestAnimationFrame 节流的经典优化模式:
// 核心视口变换状态,使用 Ref 存储,避免触发 React 重新渲染
const panState = useRef({
isPanning: false,
startX: 0,
startY: 0,
initialX: 0,
initialY: 0,
hasMoved: false,
});
const scaleRef = useRef(viewport.k);
const frameRef = useRef<number | null>(null);
const nextViewportRef = useRef<ViewportTransform | null>(null);
// 鼠标/指针移动时的处理函数
const handlePointerMove = (event: PointerEvent) => {
if (!panState.current.isPanning) return;
const dx = event.clientX - panState.current.startX;
const dy = event.clientY - panState.current.startY;
if (Math.abs(dx) > 3 || Math.abs(dy) > 3) {
panState.current.hasMoved = true;
}
// 将计算出的下一个视口状态暂存到 Ref 中
nextViewportRef.current = {
x: panState.current.initialX + dx,
y: panState.current.initialY + dy,
k: scaleRef.current,
};
// 如果当前帧已经有渲染任务在排队,直接返回,实现物理节流
if (frameRef.current) return;
// 申请在浏览器下一帧绘制时执行视口更新
frameRef.current = requestAnimationFrame(() => {
frameRef.current = null;
if (nextViewportRef.current) {
// 此时才真正触发状态更新,通知上层组件修改 CSS Transform
onViewportChange(nextViewportRef.current);
}
});
};
这种设计的精妙之处在于:不管鼠标移动得多快,视口状态的更新频率永远和屏幕刷新率(如 60Hz/120Hz)保持绝对同步。GPU 只需要在每一帧处理一次 transform: translate(x, y) scale(k) 的矩阵变换,消耗极低,拖拽体验如丝般顺滑。
3. 缩放中心计算:让画布永远围绕鼠标指针缩放
在无限画布中,缩放不能简单地以画布左上角 (0,0) 为原点,而必须以当前鼠标指针所在的屏幕坐标为中心进行缩放。否则,用户想看画布右下角的细节,一滚轮缩放,画面直接飞到了九霄云外。
这是一个经典的几何坐标转换问题。我们需要在缩放前后,保持鼠标指针下的“世界坐标”(World Coordinates)绝对不变。计算公式如下:
在代码中,这一数学逻辑被优雅地实现为:
const handleWheel = (event: React.WheelEvent<HTMLDivElement>) => {
// 排除不需要缩放的区域(如弹窗、下拉菜单)
const target = event.target instanceof Element ? event.target : null;
if (target?.closest("[data-canvas-no-zoom],.ant-modal,.ant-select-dropdown")) return;
const delta = -event.deltaY;
// 每次滚动缩放 1.1 倍或 1/1.1 倍
const factor = Math.pow(1.1, delta / 100);
const newScale = Math.min(Math.max(viewport.k * factor, 0.05), 5); // 限制缩放范围在 5% 到 500% 之间
const rect = containerRef.current?.getBoundingClientRect();
if (!rect) return;
// 1. 计算鼠标在屏幕视口中的相对位置
const mouseX = event.clientX - rect.left;
const mouseY = event.clientY - rect.top;
// 2. 逆向计算出鼠标当前指向的“画布世界坐标”
const worldX = (mouseX - viewport.x) / viewport.k;
const worldY = (mouseY - viewport.y) / viewport.k;
// 3. 根据公式计算出新的视口平移量 (x, y),确保缩放后该世界坐标依然对齐鼠标
onViewportChange({
x: mouseX - worldX * newScale,
y: mouseY - worldY * newScale,
k: newScale,
});
};
有了这个算法,用户就可以像操作 Google Maps 一样,指哪打哪,精准缩放画布的任何一个角落。
三、 存储与同步:本地优先(Local-First)与去中心化云同步的完美结合
很多生图工具为了省事,直接把用户的图片和配置全部上传到自己的服务器数据库。这不仅带来了高昂的带宽和存储成本,更让用户对自己的“创意资产隐私”产生了深深的担忧——谁也不想自己还没公开的创意草稿,在别人的服务器上裸奔。
BigBanana Canvas 彻底贯彻了本地优先(Local-First)的哲学:你的数据,完全属于你。
1. 存储痛点:如何避免大体积 Blob 撑爆 IndexedDB?
在无限画布中,用户会频繁上传参考图、生成高清大图、甚至导出几百兆的视频。如果我们把这些媒体文件直接转成 Base64 字符串,塞进画布的 JSON 树里,会发生什么?
-
内存暴涨:React 在进行状态比较和 Zustand 进行持久化时,需要频繁解析和序列化这个巨大的 JSON 字符串。浏览器会瞬间卡死。
-
存储超限:浏览器的 LocalStorage 只有 5MB 的硬性限制,根本存不下几张图。
为了解决这个问题,BigBanana Canvas 引入了“数据与媒体分离存储”的架构,并设计了精妙的“补水(Hydration)”与“垃圾回收(Garbage Collection)”机制。
A. 核心数据结构设计
画布的 JSON 结构(CanvasProject)非常轻量,它只保存结构化的配置和媒体文件的“指针”(storageKey),绝不保存真实的二进制数据:
type CanvasProject = {
id: string;
title: string;
nodes: CanvasNodeData[]; // 节点列表
connections: CanvasConnection[]; // 连线列表
viewport: ViewportTransform; // 视口状态
// ...
};
type CanvasNodeData = {
id: string;
type: "image" | "text" | "config" | "video";
position: { x: number; y: number };
width: number;
height: number;
metadata?: {
content?: string; // 运行时展示的临时 blob: URL
storageKey?: string; // 真实的本地存储唯一标识,如 "image:uuid"
naturalWidth?: number;
naturalHeight?: number;
bytes?: number;
mimeType?: string;
};
};
B. “补水”机制(Hydration)
当用户打开画布时,前端会经历一个“补水”的过程。它会遍历所有的节点,如果发现节点包含 storageKey,就会去专门存储二进制 Blob 的 image_files 数据库中读取真实的 Blob,并生成一个临时的、生命周期仅限于当前页面会话的 blob: URL,挂载到 content 字段上供 <img> 或 <video> 标签渲染:
// 伪代码:画布初始化时的“补水”流程
async function hydrateProject(project: CanvasProject): Promise<CanvasProject> {
const hydratedNodes = await Promise.all(project.nodes.map(async (node) => {
if (node.type === "image" && node.metadata?.storageKey) {
// 从独立的 localforage 实例中读取二进制 Blob
const blob = await imageFileStorage.getItem<Blob>(node.metadata.storageKey);
if (blob) {
// 生成临时的内存 URL
const blobUrl = URL.createObjectURL(blob);
return {
...node,
metadata: { ...node.metadata, content: blobUrl }
};
}
}
return node;
}));
return { ...project, nodes: hydratedNodes };
}
这样做的好处是:Zustand 持久化和状态比对时,处理的只是一个极其轻量的 JSON 树;而大体积的二进制数据则静静地躺在 IndexedDB 的物理存储中,只有在渲染时才会被临时加载到内存。
C. “垃圾回收”机制(Garbage Collection)
本地优先存储面临的另一个大问题是:空间碎片。 当用户在画布上删除了一个图片节点,或者清空了某个历史会话,如果直接把节点从 JSON 里删掉,IndexedDB 里存储的真实图片 Blob 就会变成无人认领的“幽灵文件”,日积月累,会彻底占满用户的硬盘。
为了解决这个问题,BigBanana Canvas 在用户进行删除操作时,会异步触发一条引用计数清理(Cleanup Unused Images)流水线:
export async function cleanupUnusedImages() {
// 1. 收集当前所有画布项目、所有素材、所有助手会话中正在被引用的 storageKey
const activeKeys = new Set<string>();
const projects = useCanvasStore.getState().projects;
projects.forEach(project => {
project.nodes.forEach(node => {
if (node.metadata?.storageKey) activeKeys.add(node.metadata.storageKey);
});
project.chatSessions.forEach(session => {
session.messages.forEach(msg => {
msg.images?.forEach(img => {
if (img.storageKey) activeKeys.add(img.storageKey);
});
});
});
});
const assets = useAssetStore.getState().assets;
assets.forEach(asset => {
if (asset.storageKey) activeKeys.add(asset.storageKey);
});
// 2. 遍历 image_files 数据库中的所有物理文件
const allStoredKeys = await imageFileStorage.keys();
for (const storedKey of allStoredKeys) {
// 3. 如果某个物理文件的 key 不在任何活跃引用集合中,果断将其斩杀!
if (!activeKeys.has(storedKey)) {
await imageFileStorage.removeItem(storedKey);
// 释放对应的内存 ObjectURL,防止内存泄漏
revokeObjectURLByKey(storedKey);
console.log(`[GC] 成功清理无引用媒体文件: ${storedKey}`);
}
}
}
这套完美的垃圾回收机制,确保了应用在本地运行几年,依然能保持身材苗条、运行如飞。
2. WebDAV 去中心化云同步:把数据主权彻底还给用户
本地优先虽然安全、快速,但有一个致命痛点:无法跨设备协同。你在公司电脑上画了一半的生图画布,回到家想用平板继续画,怎么办?
如果为此搭建一套集中的云存储服务,不仅违背了“本地优先、隐私至上”的初衷,还会让服务器成本飙升。
BigBanana Canvas 巧妙地选择了 WebDAV 协议。 WebDAV 是一个历史悠久、极其成熟的分布式文件写入协议。几乎所有的私有云盘(如群晖 NAS、威联通、坚果云、Nextcloud)都原生支持 WebDAV。通过支持 WebDAV,用户可以自己提供存储空间,应用只需要负责把本地的 IndexedDB 数据打包上传即可。
A. 突破浏览器沙盒:Next.js WebDAV CORS 代理
在浏览器中直接通过 JS 请求用户的 WebDAV 服务器,会遇到一个无法逾越的鸿沟:CORS(跨域资源共享)限制。绝大多数私有 NAS 或云盘的 WebDAV 服务,根本没有配置允许跨域的 Headers,浏览器会直接无情地拦截请求。
为了解决这个痛点,BigBanana Canvas 在 Next.js 服务端实现了一个极简、高效的流式转发代理(/webdav-proxy):
// web/src/app/webdav-proxy/route.ts
import { NextRequest } from "next/server";
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
export async function POST(request: NextRequest) {
// 从自定义请求头中提取真实的 WebDAV 目标地址和方法
const target = request.headers.get("x-webdav-target") || "";
const method = (request.headers.get("x-webdav-method") || "GET").toUpperCase();
if (!target) return new Response("Missing target", { status: 400 });
const url = new URL(target);
const headers = new Headers();
// 将前端传来的自定义代理头,还原为标准的 WebDAV 头
copyHeader(request, headers, "x-webdav-authorization", "Authorization");
copyHeader(request, headers, "x-webdav-depth", "Depth");
copyHeader(request, headers, "x-webdav-destination", "Destination");
copyHeader(request, headers, "x-webdav-overwrite", "Overwrite");
copyHeader(request, headers, "x-webdav-content-type", "Content-Type");
try {
// 读取请求体(如果是 PUT 上传文件,这里会拿到二进制 ArrayBuffer)
const body = method === "GET" || method === "HEAD" ? undefined : await request.arrayBuffer();
// 服务端发起真实的 WebDAV 请求,完美绕过浏览器的 CORS 限制
const response = await fetch(url, {
method,
headers,
body: body?.byteLength ? body : undefined,
});
// 将响应头和响应体原封不动地流式返回给浏览器前端
return new Response(method === "HEAD" ? null : response.body, {
status: response.status,
headers: responseHeaders(response.headers),
});
} catch (error) {
return new Response("Proxy Error", { status: 502 });
}
}
这个代理设计得极其克制:它不在服务端保存任何用户数据,不记录任何密码,纯粹扮演一个“交通警察”的角色,只做协议头的翻译和流量的转发。 既解决了 CORS 问题,又维护了用户的隐私安全。
B. 增量同步与并发控制算法
媒体文件(动辄几兆的高清图、几十兆的视频)如果每次同步都全量上传下载,网络带宽和用户耐心都会崩溃。
BigBanana Canvas 实现了一套基于 Manifest(清单文件)对比的增量同步算法:
-
清单对比:每次同步时,首先下载远端的
manifest.json,对比本地与远端项目的updatedAt时间戳。 -
三路合并(Three-way Merge):对于冲突的项目,采用“最后写入者胜出(Last-Write-Wins)”的策略进行合并。
-
文件差异分析:对比本地和远端媒体文件的
storageKey列表,找出“本地有、远端无”(需要上传)和“远端有、本地无”(需要下载)的文件差异集。 -
并发控制:由于浏览器并发请求过多会导致网络拥堵甚至请求掉线,同步模块引入了并发池(Concurrency Limit = 3)来控制上传和下载:
// 限制并发数为 3 的异步任务执行器
async function runWithConcurrency<T>(tasks: (() => Promise<T>)[], limit = 3) {
const results: T[] = [];
const executing = new Set<Promise<void>>();
for (const task of tasks) {
const p = Promise.resolve().then(() => task());
results.push(p as unknown as T);
if (limit <= tasks.length) {
const e: Promise<void> = p.then(() => { executing.delete(e); });
executing.add(e);
if (executing.size >= limit) {
await Promise.race(executing);
}
}
}
return Promise.all(results);
}
这套精密的同步算法,让大体积画布的跨设备同步变得无比轻快、稳定。
四、 跨界桥梁:Canvas Agent 与 MCP 协议的深度融合
如果说“无限画布”让人类的创作体验飞上天,那么 Canvas Agent + MCP 协议 的引入,则是给 AI 代理(Agent) 递上了一支画笔,让它能够直接在你的画布上作画。
1. 什么是 MCP(Model Context Protocol)?
在过去,AI 助手(如 Claude、ChatGPT)只能在它们自己的黑乎乎的终端或聊天框里生存。它们想了解你的项目状态,你得复制粘贴代码给它;它们想帮你改代码,只能吐出一段 Diff 让你手动应用。
Anthropic 推出的 MCP(模型上下文协议) 彻底打破了这堵墙。MCP 就像是 AI 时代的“USB 接口规范”。只要一个应用(比如我们的 Canvas Agent)实现了 MCP 协议,任何支持 MCP 的 AI 终端(如 Claude Code、Codex)就能通过这个标准接口,直接读取该应用的上下文,并调用该应用暴露的工具。
2. 双向通信的艺术:SSE(Server-Sent Events)与异步确认
Canvas Agent 作为一个本地 Node.js 服务,是如何与运行在浏览器里的 BigBanana Canvas 前端页面进行实时互动的?
很多开发者会第一反应选择 WebSocket。但 BigBanana Canvas 却选择了一条更轻量、更优雅的路:SSE(Server-Sent Events) + HTTP POST。
-
为什么选择 SSE? WebSocket 是全双工的,虽然强大,但协议握手复杂,且在某些严格的局域网防火墙下容易被拦截。而 SSE 是基于标准 HTTP 协议的单向推送技术,浏览器原生支持(
EventSource),具有天然的断线重连机制,非常适合“服务端向浏览器推送指令”的场景。 -
如何实现双向闭环? 当本地 Agent 想要让网页执行某个操作时,它通过 SSE 通道把指令推下去;网页执行完后,通过一个标准的 HTTP POST 请求把结果送上来。
让我们来看看这套双向通信在 canvas-session.ts 中的核心实现:
export class CanvasSession {
// 保存所有已连接的浏览器客户端响应对象 (SSE)
private clients = new Map<string, ServerResponse>();
// 保存正在等待网页返回结果的挂起请求
private pending = new Map<string, PendingRequest>();
private canvasState: CanvasSnapshot | null = null;
// 1. 浏览器前端调用此接口,建立 SSE 长连接
openEvents(url: URL, res: ServerResponse) {
const clientId = url.searchParams.get("clientId") || crypto.randomUUID();
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Connection": "keep-alive"
});
this.clients.set(clientId, res);
// 发送握手成功消息
sendEvent(res, "hello", { ok: true, clientId });
// 维持心跳,防止连接被网关断开
const timer = setInterval(() => sendEvent(res, "ping", { time: Date.now() }), 15000);
res.on("close", () => {
clearInterval(timer);
this.clients.delete(clientId);
});
}
// 2. 当本地 AI 终端调用 MCP 工具(例如创建节点)时,Agent 触发此函数
async callTool(name: string, input: Record<string, any>) {
const requestId = crypto.randomUUID();
// 构造推送到网页端的指令包
const opInstruction = {
requestId,
type: "apply_ops",
ops: [/* 具体的画布操作,如添加节点 */]
};
// 创建一个挂起的 Promise,等待网页端执行完后唤醒
const replyPromise = new Promise((resolve, reject) => {
this.pending.set(requestId, { resolve, reject });
});
// 通过 SSE 管道,向所有连接的浏览器页面广播这条指令
this.emitAll("instruction", opInstruction);
// 挂起当前线程,静静地等待浏览器端传来捷报
return replyPromise;
}
// 3. 浏览器端在画布上画完节点后,POST 请求此接口,送回执行结果
resolveResult(body: { requestId?: string; error?: string; result?: unknown }) {
if (!body.requestId) return;
const pendingTask = this.pending.get(body.requestId);
if (!pendingTask) return;
this.pending.delete(body.requestId);
if (body.error) {
pendingTask.reject(new Error(body.error));
} else {
// 唤醒挂起的 Promise,将结果返回给 AI 终端!
pendingTask.resolve(body.result);
}
}
}
这套设计简直是异步编程的艺术典范!它通过一个唯一的 requestId,将“本地命令行 -> 本地 Agent -> 浏览器 SSE -> 浏览器 DOM 绘制 -> 浏览器 POST -> 本地 Agent 唤醒 -> 命令行返回”这一长串跨越了物理进程、网络沙盒、甚至人类确认的复杂链路,完美地收拢在了一个优雅的 Promise 异步等待中。
更牛的是,为了防止 AI 代理在画布上“胡作非为”,网页端在收到 SSE 指令时,并不会默默执行,而是会在右侧的 Agent 运行日志中,弹出一个二次确认框。用户可以清晰地看到 AI 计划执行的每一个步骤(例如:“计划在坐标 (200, 100) 处新建一个文本节点”),点击“同意”后,网页才会真正调用 resolveResult。
这种“AI 提议,人类审批”的协同模式,既给予了 AI 极高的操作灵活性,又把终极控制权牢牢地握在人类手中。
五、 核心业务流:从“提示词发散”到“多参考视频生成”的连续推演
说了这么多硬核的底层架构,我们来看看在 BigBanana Canvas 中,这些技术是如何转化为令人惊叹的生图工作流体验的。
1. 经典的“连线发散”工作流
在传统的生图工具里,如果你想尝试 4 个不同的模型,或者 4 组不同的提示词,你得开 4 个网页,或者在同一个对话框里反复复制粘贴,记录极其混乱。
而在 BigBanana Canvas 的无限画布上,一切都变得无比直观:
+------------------+
| 1. 核心提示词节点 |
| "Cyberpunk city" |
+--------+---------+
|
+-----------------------+-----------------------+
| | |
+--------v---------+ +--------v---------+ +--------v---------+
| 2. 风格 A 文本节点| | 3. 风格 B 文本节点| | 4. 风格 C 文本节点|
| "Anime style" | | "Realistic 3D" | | "Oil painting" |
+--------+---------+ +--------+---------+ +--------+---------+
| | |
+-----------------------+-----------------------+
|
+---------v---------+
| 5. 生成配置节点 |
| (批量读取上游输入) |
+---------+---------+
|
+---------v---------+
| 6. 批量图片组节点 |
| (一键生成 6 张图) |
+-------------------+
-
输入源发散:你在画布中央创建一个文本节点,写下核心主体
Cyberpunk city。然后拉出三条连线,分别连接到三个子文本节点,分别写上不同的艺术风格(Anime style、Realistic 3D、Oil painting)。 -
生成配置节点(Config Composer):你创建一个“生成配置节点”,把这三个风格节点都连进去。这个配置节点就像一个聪明的“聚合器”,它会自动检测所有的上游输入,允许你预览和调整提示词的拼接顺序。
-
批量生成与叠卡预览:点击生成后,配置节点开始批量请求 AI 接口。生成的多张图片不会杂乱无章地铺满画布,而是会被打包成一个“图片组节点”。它支持像扑克牌一样的叠卡预览,你可以用鼠标滚轮或左右键快速切卡,展开查看全部结果,并一键将最满意的那张设为“主图”。
2. 接入火山方舟 Seedance 2.0:多模态视频生成的终极推演
生图的终点是生视频。现在的视频生成模型(如字节跳动的 Doubao-Seedance-2.0)支持极其复杂的输入:它不仅需要文本 Prompt,还可以接受最多 9 张参考图(控制角色、场景、道具)、3 个参考视频(控制运动轨迹)、甚至 3 个参考音频(控制背景声和节奏)。
在传统的网页表单里,要配置这么多输入,表单会臃肿得像一张报税单,用户根本无从下手。
但在无限画布上,这种多模态的复杂输入被简化成了一场视觉连线游戏:
+------------------+ +------------------+
| 角色参考图节点 | | 场景参考图节点 |
+--------+---------+ +--------+---------+
| |
+------------+------------+
|
+---------v---------+
| 生成配置节点 |<---+ [提示词文本节点]
| (Mode: 视频生成) |
+---------+---------+<---+ [动作参考视频节点]
|
+---------v---------+
| 视频播放器节点 |
| (原生预览播放) |
+-------------------+
-
直观的输入编排:你只需要把“角色参考图”节点、“场景参考图”节点、“动作参考视频”节点、以及“提示词”文本节点,统统连线指向“生成配置节点”。
-
智能类型识别:配置节点在发起请求前,会自动分析连线关系。它知道哪些是图片,哪些是视频,哪些是文本,并自动将它们归类为 Seedance 2.0 接口所需的
ref_images、ref_videos和prompt参数。 -
本地安全下载:视频生成成功后,前端不会直接引用脆弱的远程临时 URL(容易因过期或 CORS 无法播放),而是会尝试在后台自动将视频下载为本地的
Blob,并持久化到 IndexedDB 的media_files库中。无论何时打开画布,你的视频都能秒开播放。
这种将复杂的多模态参数配置转化为“画布连线”的交互设计,彻底降低了专业级 AI 创作的门槛。
六、 哲学思考:画布即 IDE,AI 协同的下一个纪元
当我们扒完 BigBanana Canvas 的底层实现,合上代码编辑器,我们不妨从更高的维度来思考:为什么“无限画布”会成为 AI 交互的终极形态?
传统的软件界面(如 Word、Excel、Photoshop、聊天框)都是“功能导向型”的。它们假设人类已经有了明确的步骤,界面只是提供工具。但在 AI 时代,创作和思考的过程是“非线性的、探索性的、人机共创的”。
无限画布本质上提供了一个“空间维度的记忆体(Spatial Memory)”:
-
打破时间线的限制:在聊天框里,过去的对话会被无情地推到上方,变成“历史”。而在画布上,所有的创意草稿、中间过程、最终结果,都以二维坐标的形式平铺在空间中。你的眼睛可以一瞥扫过十张图,大脑可以瞬间建立起它们之间的关联。
-
AI 代理的物理实体化:通过 MCP 协议,AI 不再是一个只能在后台打字的“幽灵”,而是成为了一个拥有实体操作能力的“虚拟助手”。它可以在你睡觉时,默默在画布的左侧帮你整理提示词,在右侧帮你批量跑图,并用连线把它的思考路径画出来。当你醒来,你面对的不是一堆冰冷的日志,而是一幅壮丽的“创意蓝图”。
未来,无限画布将不再仅仅用于生图。画布即 IDE,画布即操作系统。 你可以把一段代码连线到一个测试节点,再连线到一个部署节点;AI Agent 会在画布上自主地奔跑,帮你搬运数据、调用工具、解决问题。而你,只需要像一个将军一样,站在无限的沙盘前,指点江山。
七、 结语与福利
看到这里,你是不是已经被无限画布的底层技术和无穷魅力深深吸引,手痒难耐了?
别再忍受传统聊天框那无休止的“盲盒抽卡”和记忆丧失了。现在就去体验一下前沿技术带来的创作革命吧!
👉 立刻开启你的无界创作之旅:BigBanana Canvas 官方体验地址 (https://canvas.tree456.com/)
在 BigBanana Canvas 中,你可以:
-
体验丝滑无比的原生 GPU 加速无限画布。
-
零配置、零门槛直连你自己的 OpenAI 兼容接口,享受绝对的数据隐私。
-
体验将多模态输入简化为“连线游戏”的 Seedance 2.0 视频生成。
-
绑定你自己的 WebDAV,把创意的掌控权牢牢攥在自己手里。
技术改变创作,空间解放思维。 我们在无限画布的世界里,等你来画出属于你的第一笔!
更多推荐





所有评论(0)