feat(plugin): implement canvas node plugin system with dynamic registration and remote installation

This commit is contained in:
HouYunFei
2026-07-15 11:47:25 +08:00
parent f0db29d8e3
commit eef4d96787
51 changed files with 4933 additions and 72 deletions
+102
View File
@@ -0,0 +1,102 @@
import type { ComponentType, ReactNode } from "react";
import type { CanvasAgentOp } from "@/lib/canvas/canvas-agent-ops";
import type { CanvasTheme } from "@/lib/canvas-theme";
import type { CanvasConnection, CanvasNodeData, CanvasNodeMetadata } from "@/types/canvas";
import type { CanvasResourceKind } from "@/lib/canvas/canvas-resource-references";
// 插件节点作为上游输入被消费时输出的资源
export type CanvasNodeResource = { kind: CanvasResourceKind; text?: string; url?: string };
// 节点自带的工具栏按钮(追加到 hover 工具栏尾部)
export type CanvasNodeToolbarItem = {
id: string;
title: string;
label: string;
icon: ReactNode;
onClick: () => void;
active?: boolean;
danger?: boolean;
};
// 每个节点渲染时注入的上下文,是插件与画布交互的核心接口
export type CanvasNodeContext = {
node: CanvasNodeData;
theme: CanvasTheme;
scale: number;
// 自身数据
updateMetadata: (patch: CanvasNodeMetadata) => void;
updateNode: (patch: Partial<Pick<CanvasNodeData, "title" | "width" | "height">>) => void;
// 图访问
getNode: (id: string) => CanvasNodeData | null;
getNodes: () => CanvasNodeData[];
getConnections: () => CanvasConnection[];
getUpstream: () => CanvasNodeData[];
getDownstream: () => CanvasNodeData[];
// 画布操作,复用 Agent 指令集(增删节点/连线/选择/视口/触发生成)
applyOps: (ops: CanvasAgentOp[]) => void;
// 节点间/插件间通信
emit: (event: string, payload?: unknown) => void;
on: (event: string, handler: (payload: unknown) => void) => () => void;
// 插件私有持久化,命名空间隔离
storage: PluginStorage;
};
export type PluginStorage = {
get: <T = unknown>(key: string) => Promise<T | null>;
set: (key: string, value: unknown) => Promise<void>;
remove: (key: string) => Promise<void>;
};
// 画布宿主提供的、与具体节点无关的能力集合(由画布页面构建、注入渲染链路)
export type CanvasPluginHost = {
getNode: (id: string) => CanvasNodeData | null;
getNodes: () => CanvasNodeData[];
getConnections: () => CanvasConnection[];
getUpstream: (nodeId: string) => CanvasNodeData[];
getDownstream: (nodeId: string) => CanvasNodeData[];
updateNode: (nodeId: string, patch: Partial<Pick<CanvasNodeData, "title" | "width" | "height">>) => void;
updateMetadata: (nodeId: string, patch: CanvasNodeMetadata) => void;
applyOps: (ops: CanvasAgentOp[]) => void;
};
// 节点类型定义:内置节点与插件节点统一走这套结构
export type CanvasNodeDefinition = {
type: string; // 内置如 "image";插件建议 "<pluginId>:<name>"
title: string;
icon: ReactNode;
description?: string;
defaultSize: { width: number; height: number };
defaultMetadata?: CanvasNodeMetadata;
minimapColor?: string;
showInCreateMenu?: boolean; // 默认 true
hasSourceHandle?: boolean; // 右侧输出连接点,默认 true
keepAspectRatio?: (node: CanvasNodeData) => boolean;
resource?: (node: CanvasNodeData) => CanvasNodeResource | null;
// 渲染:内置节点由 canvas-node 内部渲染器负责,可不提供 Content
Content?: ComponentType<{ ctx: CanvasNodeContext }>;
Panel?: ComponentType<{ ctx: CanvasNodeContext; onClose: () => void }>;
toolbar?: (ctx: CanvasNodeContext) => CanvasNodeToolbarItem[];
onDoubleClick?: (ctx: CanvasNodeContext) => boolean; // 返回 true 表示已处理
};
// 插件启动时可访问的应用能力
export type CanvasPluginApp = {
version: string;
emit: (event: string, payload?: unknown) => void;
on: (event: string, handler: (payload: unknown) => void) => () => void;
// 注入插件样式,返回移除函数;传 key 时同 key 覆盖旧样式
injectCSS: (css: string, key?: string) => () => void;
};
// 插件包默认导出
export type CanvasPlugin = {
id: string;
name: string;
version: string;
description?: string;
minAppVersion?: string;
css?: string; // 插件样式,启用时自动注入、卸载/禁用时自动清理
nodes: CanvasNodeDefinition[];
setup?: (app: CanvasPluginApp) => void | (() => void);
};
+5 -2
View File
@@ -18,6 +18,9 @@ export enum CanvasNodeType {
Group = "group",
}
// 节点类型放开为字符串,内置类型用 CanvasNodeType,插件类型为 "<pluginId>:<name>"
export type CanvasNodeTypeId = CanvasNodeType | (string & {});
export type CanvasNodeStatus = "idle" | "success" | "loading" | "error";
export type CanvasGenerationMode = "text" | "image" | "video" | "audio";
export type CanvasImageGenerationType = "generation" | "edit";
@@ -62,7 +65,7 @@ export type CanvasNodeMetadata = {
export type CanvasNodeData = {
id: string;
type: CanvasNodeType;
type: CanvasNodeTypeId;
title: string;
position: Position;
width: number;
@@ -78,7 +81,7 @@ export type CanvasConnection = {
export type CanvasAssistantReference = {
id: string;
type: CanvasNodeType;
type: CanvasNodeTypeId;
title: string;
dataUrl?: string;
storageKey?: string;