你是否也曾有过这样的抓狂时刻: 在某个主流 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 实时读取画布状态、生成节点、编排工作流)        |  |
|  +-----------------------------------------------------------------------------+  |
+-----------------------------------------------------------------------------------+

这套架构由以下三个核心角色组成:

  1. 浏览器前端(Next.js App Router): 这是用户的主战场。它运行在浏览器中,负责高性能的画布渲染、丝滑的平移缩放、节点拖拽、连线高亮等交互。最酷的是,它是一个本地优先(Local-First)的应用,所有的画布数据、生成历史、甚至大体积的图片/视频 Blob,都通过 localforage 存储在用户本地的 IndexedDB 中。同时,它支持通过内置的 WebDAV 客户端,将数据加密同步到用户自己的私有云(如群晖 NAS、坚果云等)。

  2. 本地代理(Canvas Agent): 这是一个运行在用户本地电脑上的轻量级 Node.js 服务。为什么要有一个本地 Agent?因为浏览器是一个“沙盒”,它无法直接读取用户的本地文件系统,也无法直接启动本地的命令行工具。Canvas Agent 充当了“破局者”,它不仅管理着本地的开发工作区,还通过 SSE(Server-Sent Events) 与浏览器前端建立了一条双向、实时的通信管道。

  3. AI 终端(Codex / Claude Code / Cursor 等): 这是真正的“AI 程序员/创作助手”。它通过 Anthropic 推出的 MCP(Model Context Protocol,模型上下文协议) 与 Canvas Agent 进行标准化的 stdio 通信。AI 终端不需要知道网页端是如何实现的,它只需要调用 Canvas Agent 暴露的 canvas_get_statecanvas_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(清单文件)对比的增量同步算法

  1. 清单对比:每次同步时,首先下载远端的 manifest.json,对比本地与远端项目的 updatedAt 时间戳。

  2. 三路合并(Three-way Merge):对于冲突的项目,采用“最后写入者胜出(Last-Write-Wins)”的策略进行合并。

  3. 文件差异分析:对比本地和远端媒体文件的 storageKey 列表,找出“本地有、远端无”(需要上传)和“远端有、本地无”(需要下载)的文件差异集。

  4. 并发控制:由于浏览器并发请求过多会导致网络拥堵甚至请求掉线,同步模块引入了并发池(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 张图)  |
                       +-------------------+
  1. 输入源发散:你在画布中央创建一个文本节点,写下核心主体 Cyberpunk city。然后拉出三条连线,分别连接到三个子文本节点,分别写上不同的艺术风格(Anime styleRealistic 3DOil painting)。

  2. 生成配置节点(Config Composer):你创建一个“生成配置节点”,把这三个风格节点都连进去。这个配置节点就像一个聪明的“聚合器”,它会自动检测所有的上游输入,允许你预览和调整提示词的拼接顺序。

  3. 批量生成与叠卡预览:点击生成后,配置节点开始批量请求 AI 接口。生成的多张图片不会杂乱无章地铺满画布,而是会被打包成一个“图片组节点”。它支持像扑克牌一样的叠卡预览,你可以用鼠标滚轮或左右键快速切卡,展开查看全部结果,并一键将最满意的那张设为“主图”。

2. 接入火山方舟 Seedance 2.0:多模态视频生成的终极推演

生图的终点是生视频。现在的视频生成模型(如字节跳动的 Doubao-Seedance-2.0)支持极其复杂的输入:它不仅需要文本 Prompt,还可以接受最多 9 张参考图(控制角色、场景、道具)、3 个参考视频(控制运动轨迹)、甚至 3 个参考音频(控制背景声和节奏)。

在传统的网页表单里,要配置这么多输入,表单会臃肿得像一张报税单,用户根本无从下手。

但在无限画布上,这种多模态的复杂输入被简化成了一场视觉连线游戏:

+------------------+      +------------------+
|  角色参考图节点   |      |  场景参考图节点   |
+--------+---------+      +--------+---------+
         |                         |
         +------------+------------+
                      |
            +---------v---------+
            |  生成配置节点      |<---+ [提示词文本节点]
            |  (Mode: 视频生成)  |
            +---------+---------+<---+ [动作参考视频节点]
                      |
            +---------v---------+
            |   视频播放器节点   |
            |   (原生预览播放)   |
            +-------------------+
  • 直观的输入编排:你只需要把“角色参考图”节点、“场景参考图”节点、“动作参考视频”节点、以及“提示词”文本节点,统统连线指向“生成配置节点”。

  • 智能类型识别:配置节点在发起请求前,会自动分析连线关系。它知道哪些是图片,哪些是视频,哪些是文本,并自动将它们归类为 Seedance 2.0 接口所需的 ref_imagesref_videosprompt 参数。

  • 本地安全下载:视频生成成功后,前端不会直接引用脆弱的远程临时 URL(容易因过期或 CORS 无法播放),而是会尝试在后台自动将视频下载为本地的 Blob,并持久化到 IndexedDB 的 media_files 库中。无论何时打开画布,你的视频都能秒开播放。

这种将复杂的多模态参数配置转化为“画布连线”的交互设计,彻底降低了专业级 AI 创作的门槛。


六、 哲学思考:画布即 IDE,AI 协同的下一个纪元

当我们扒完 BigBanana Canvas 的底层实现,合上代码编辑器,我们不妨从更高的维度来思考:为什么“无限画布”会成为 AI 交互的终极形态?

传统的软件界面(如 Word、Excel、Photoshop、聊天框)都是“功能导向型”的。它们假设人类已经有了明确的步骤,界面只是提供工具。但在 AI 时代,创作和思考的过程是“非线性的、探索性的、人机共创的”

无限画布本质上提供了一个“空间维度的记忆体(Spatial Memory)”

  1. 打破时间线的限制:在聊天框里,过去的对话会被无情地推到上方,变成“历史”。而在画布上,所有的创意草稿、中间过程、最终结果,都以二维坐标的形式平铺在空间中。你的眼睛可以一瞥扫过十张图,大脑可以瞬间建立起它们之间的关联。

  2. AI 代理的物理实体化:通过 MCP 协议,AI 不再是一个只能在后台打字的“幽灵”,而是成为了一个拥有实体操作能力的“虚拟助手”。它可以在你睡觉时,默默在画布的左侧帮你整理提示词,在右侧帮你批量跑图,并用连线把它的思考路径画出来。当你醒来,你面对的不是一堆冰冷的日志,而是一幅壮丽的“创意蓝图”。

未来,无限画布将不再仅仅用于生图。画布即 IDE,画布即操作系统。 你可以把一段代码连线到一个测试节点,再连线到一个部署节点;AI Agent 会在画布上自主地奔跑,帮你搬运数据、调用工具、解决问题。而你,只需要像一个将军一样,站在无限的沙盘前,指点江山。


七、 结语与福利

看到这里,你是不是已经被无限画布的底层技术和无穷魅力深深吸引,手痒难耐了?

别再忍受传统聊天框那无休止的“盲盒抽卡”和记忆丧失了。现在就去体验一下前沿技术带来的创作革命吧!

👉 立刻开启你的无界创作之旅BigBanana Canvas 官方体验地址 (https://canvas.tree456.com/)

在 BigBanana Canvas 中,你可以:

  • 体验丝滑无比的原生 GPU 加速无限画布。

  • 零配置、零门槛直连你自己的 OpenAI 兼容接口,享受绝对的数据隐私。

  • 体验将多模态输入简化为“连线游戏”的 Seedance 2.0 视频生成。

  • 绑定你自己的 WebDAV,把创意的掌控权牢牢攥在自己手里。

技术改变创作,空间解放思维。 我们在无限画布的世界里,等你来画出属于你的第一笔!

更多AIGC文章

RAG技术全解:从原理到实战的简明指南

更多VibeCoding文章

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐