画布数据结构
画布本地存储、节点结构、媒体文件与清理机制
画布数据结构
本文档说明当前画布在前端本地保存的数据结构、图片文件的存储和清理方式,以及后续接入后端存储时建议保持的兼容边界。
当前存储位置
当前画布项目主要保存在浏览器本地:
- 画布项目 JSON:
localForage,数据库名infinite-canvas,storeNameapp_state,key 为infinite-canvas:canvas_store。 - 我的素材 JSON:
localForage,数据库名infinite-canvas,storeNameapp_state,key 为infinite-canvas:asset_store。 - 图片 Blob:单独存到
localForage实例,数据库名infinite-canvas,storeNameimage_files。 - 视频等媒体 Blob:单独存到
localForage实例,数据库名infinite-canvas,storeNamemedia_files。
画布 JSON 不直接长期保存大体积 base64 图片或视频。图片节点、视频节点、助手图片和素材媒体只保存展示 URL、storageKey 和元信息,真实 Blob 通过 storageKey 读取。
画布项目结构
每个画布项目是一个 CanvasProject:
type CanvasProject = {
id: string;
title: string;
createdAt: string;
updatedAt: string;
nodes: CanvasNodeData[];
connections: CanvasConnection[];
chatSessions: CanvasAssistantSession[];
activeChatId: string | null;
backgroundMode: "lines" | "dots" | "blank";
viewport: { x: number; y: number; k: number };
};字段说明:
id:画布项目 ID,当前前端生成。title:画布名称。createdAt/updatedAt:ISO 字符串。nodes:画布节点列表。connections:节点连线列表。chatSessions:右侧画布助手会话。activeChatId:当前选中的助手会话 ID。backgroundMode:画布背景模式。viewport:视口变换,x/y是屏幕平移,k是缩放比例。
节点结构
每个节点是一个 CanvasNodeData:
type CanvasNodeData = {
id: string;
type: "image" | "text" | "config" | "video";
title: string;
position: { x: number; y: number };
width: number;
height: number;
metadata?: CanvasNodeMetadata;
};通用字段:
id:节点 ID。type:节点类型,当前有图片、文本、生成配置、视频四类。title:节点标题。position:画布世界坐标,不是屏幕坐标。width/height:画布世界坐标下的节点尺寸。metadata:节点内容和业务状态。
metadata 当前常用字段:
type CanvasNodeMetadata = {
content?: string;
prompt?: string;
status?: "idle" | "success" | "loading" | "error";
errorDetails?: string;
fontSize?: number;
generationMode?: "text" | "image" | "video";
model?: string;
size?: string;
count?: number;
naturalWidth?: number;
naturalHeight?: number;
freeResize?: boolean;
isBatchRoot?: boolean;
batchRootId?: string;
batchChildIds?: string[];
primaryImageId?: string;
imageBatchExpanded?: boolean;
inputOrder?: string[];
storageKey?: string;
mimeType?: string;
bytes?: number;
};不同节点的使用方式:
- 图片节点:
content是当前可展示的图片 URL,通常是blob:URL;storageKey指向本地图片 Blob;naturalWidth/naturalHeight/bytes/mimeType保存原图信息。 - 视频节点:
content是当前可播放的视频 URL,通常是blob:URL;storageKey指向本地视频 Blob;bytes/mimeType保存文件信息。 - 文本节点:
content保存文本内容;fontSize保存字体大小;prompt/status/errorDetails保存生成状态。 - 生成配置节点:
generationMode/model/size/count/inputOrder保存生成配置;generationMode可选择文本、图片或视频;上游输入通过connections计算。 - 图片组节点:根节点用
isBatchRoot/batchChildIds/primaryImageId/imageBatchExpanded记录批量生成结果;子图节点用batchRootId指回根节点。
连线结构
每条连线是一个 CanvasConnection:
type CanvasConnection = {
id: string;
fromNodeId: string;
toNodeId: string;
};连线只保存节点 ID,不保存端口坐标。渲染时根据节点位置和尺寸计算路径。
删除节点时会同步删除以该节点为起点或终点的连线。删除图片组根节点时,会把对应子节点一起删除。
助手会话结构
助手会话保存在画布项目内:
type CanvasAssistantSession = {
id: string;
title: string;
messages: CanvasAssistantMessage[];
createdAt: string;
updatedAt: string;
};消息结构:
type CanvasAssistantMessage = {
id: string;
role: "user" | "assistant";
mode: "ask" | "image";
text: string;
isLoading?: boolean;
references?: CanvasAssistantReference[];
images?: CanvasAssistantImage[];
};图片引用和助手生成图片也遵循同一套图片存储规则:
dataUrl字段当前可能是blob:URL,也可能是旧数据中的data:image/...。storageKey存在时,以storageKey为准读取图片 Blob。- 发送到 AI 接口前,如果接口需要 base64,会通过
imageToDataUrl临时把 Blob URL 转成 data URL。
图片写入流程
所有新增图片应通过 uploadImage(input) 写入:
- 传入
Blob或 data URL。 - 内部转成
Blob。 - 生成
storageKey,格式为image:<id>。 - 把 Blob 写入
image_files。 - 创建
blob:URL,并缓存在内存objectUrls。 - 读取图片宽高,返回:
type UploadedImage = {
url: string;
storageKey: string;
width: number;
height: number;
bytes: number;
mimeType: string;
};图片节点会通过 imageMetadata(image) 写入:
{
content: image.url,
storageKey: image.storageKey,
status: "success",
naturalWidth: image.width,
naturalHeight: image.height,
bytes: image.bytes,
mimeType: image.mimeType
}因此,content 只适合当前浏览器会话展示,不能作为长期文件标识;长期标识是 storageKey。
图片读取和旧数据迁移
打开画布时会执行图片补水:
- 如果图片节点有
storageKey,通过resolveImageUrl(storageKey, fallback)读取 Blob 并生成新的blob:URL。 - 如果图片节点没有
storageKey,但content是旧的data:image/...,会调用uploadImage(content)迁移到image_files,并补上storageKey。 - 助手消息里的引用图和生成图也会执行同类逻辑。
我的素材读取时也会做迁移:
- 有
storageKey:恢复coverUrl和data.dataUrl的可展示 URL。 - 无
storageKey且保存了 base64:写入image_files,然后更新素材里的storageKey。
图片移除和清理
图片不是在删除节点时立即按节点逐张删除,而是做引用清理:
- 删除节点、清空画布、删除画布、删除素材、删除助手会话时,会触发
cleanupImages。 cleanupImages会收集当前仍被画布项目、素材和额外传入数据引用的所有storageKey。cleanupUnusedImages遍历image_files中的全部图片。- 不在引用集合里的图片会被删除。
- 删除时会同时
URL.revokeObjectURL,并从内存缓存objectUrls移除。
这套方式可以避免同一张图片被画布、素材或助手同时引用时误删。