# MiragenFlow 画布节点插件 本目录保留画布节点的源码级内部扩展能力。每个扩展是一个独立 TypeScript 目录,自带 `package.json` / `build.mjs` / `src/index.tsx` / `dist/`,可单独开发和构建。 内置节点包括文本、图片、音频、生成配置和组。Markdown、SVG、HTML、3D 全景和便利贴目录是 SDK 与内部集成示例。 作者只写节点 UI 与逻辑,类型、JSX、宿主 React 桥接和构建由 [`@miragenflow/plugin-sdk`](./sdk/README.md) 提供。当前产品不包含远程插件市场、注册表、自动发现或 URL 安装入口;需要上线的扩展应作为应用源码的一部分接入并随产品构建。 ## 目录约定 ``` plugins/canvas/ sdk/ # 插件 SDK(类型 + automatic JSX 运行时 + 构建助手) template/ # 起步模板:复制它开始写新插件 markdown/ # 每个插件一个独立目录 package.json build.mjs # 一行 buildPlugin,产物名取目录名 → dist/markdown.js tsconfig.json src/index.tsx # 插件源码(默认导出 definePlugin(...)) README.md svg/ html/ panorama/ sticky-note/ ... ``` ## 创建内部扩展 ```bash cp -r plugins/canvas/template plugins/canvas/my-plugin cd plugins/canvas/my-plugin # 改 package.json 的 name;改 src/index.tsx 里的 id / nodes[].type npm install npm run dev # watch 构建 dist/my-plugin.js npm run typecheck # tsc --noEmit,类型自检 ``` 产物名取**目录名**,复制后记得把目录改成插件名。 ## 构建 ```bash cd plugins/canvas/ npm install npm run build # → dist/.js npm run dev # watch,改动自动构建 ``` `buildPlugin` 只生成 `dist/` 产物,不会写入 `web/public/` 或生成远程清单。示例扩展默认不在产品界面中出现;商业构建需要在应用源码中显式注册准备启用的节点定义。 ## 用 SDK 写插件 默认导出 `definePlugin({...})`(对象形式,**无需再 `const { React } = runtime`**): ```tsx import { definePlugin, useState } from "@miragenflow/plugin-sdk"; import type { CanvasNodeContentProps } from "@miragenflow/plugin-sdk"; function Content({ ctx }: CanvasNodeContentProps) { const [n, setN] = useState(0); return ( ); } export default definePlugin({ id: "my-plugin", name: "我的插件", version: "1.0.0", css: "…", // 可选:插件样式,自动注入/清理 nodes: [ /* CanvasNodeDefinition[] */ ], setup(app) { return () => {}; }, // 可选,返回清理函数;app 含 injectCSS/emit/on }); ``` SDK 导出的 hooks(`useState/useEffect/useMemo/useRef/...`)运行时转发宿主 React,类型来自 `@types/react`。SDK 与依赖接入见 [`sdk/README.md`](./sdk/README.md)。 ### CanvasNodeDefinition ```ts { type: string; // 建议 ":",全局唯一 title: string; // 创建菜单/默认标题 icon: ReactNode; // emoji 字符串或任意 ReactNode description?: string; defaultSize: { width, height }; defaultMetadata?: object; // 新建节点初始 metadata(文本内容放 content) minimapColor?: string; showInCreateMenu?: boolean; // 默认 true hasSourceHandle?: boolean; // 右侧输出连接点,默认 true keepAspectRatio?: (node) => boolean; resource?: (node) => { kind: "text"|"image"|"audio", text?, url? } | null; // 作为上游输入被消费时输出什么 Content: ({ ctx }) => ReactNode; // 节点主体渲染 Panel?: ({ ctx, onClose }) => ReactNode; // 可选:节点下方面板 toolbar?: (ctx) => Array<{ id, title, label, icon, onClick, danger? }>; // 追加到 hover 工具栏 onDoubleClick?: (ctx) => boolean; // 返回 true 表示已处理双击 } ``` ### ctx:节点与画布交互接口 `Content` / `Panel` / `toolbar` 都会拿到 `ctx`(类型 `CanvasNodeContext`): | 能力 | 说明 | | --- | --- | | `ctx.node` | 当前节点数据(含 `metadata.content` 等) | | `ctx.theme` / `ctx.scale` | 当前画布主题 token 与缩放,用来让 UI 跟随主题 | | `ctx.updateMetadata(patch)` | 更新自身 metadata(如保存内容) | | `ctx.updateNode(patch)` | 更新自身 title/width/height | | `ctx.getNode(id)` / `ctx.getNodes()` / `ctx.getConnections()` | 读画布 | | `ctx.getUpstream()` / `ctx.getDownstream()` | 取上/下游相连节点 | | `ctx.applyOps(ops)` | 用画布指令集增删节点/连线、选择、触发生成(见下) | | `ctx.emit(event, payload)` / `ctx.on(event, handler)` | 节点/插件间事件通信 | | `ctx.storage` | 插件私有持久化(按插件 id 命名空间) | > `metadata` 的**内置字段**(content、status、model…)是强类型;插件写入的**自定义字段**读出为 `unknown`,按需 `as` 断言(参考 `sticky-note` 的 `pluginColor`)。 ### 画布指令集(ctx.applyOps) ```ts ctx.applyOps([ { type: "add_node", id?, nodeType, title?, x?, y?, width?, height?, metadata? }, { type: "update_node", id, patch?, metadata? }, { type: "delete_node", id? | ids? }, { type: "connect_nodes", fromNodeId, toNodeId }, { type: "delete_connections", id? | ids? | all? }, { type: "select_nodes", ids }, { type: "set_viewport", viewport }, { type: "run_generation", nodeId, mode?, prompt? }, ]); ``` ## 重依赖 / 资源 - **重依赖**(three.js、marked 等):不要打进 bundle,运行时 `await import("https://esm.sh/...")` 动态加载(esbuild 自动 external);在 `src/env.d.ts` 声明该模块以通过 tsc。参考 `panorama/`、`markdown/`。 - **CSS**:写独立 `.css`,`import css from "./styles.css"` 拿到字符串(esbuild `text` loader),放到 `css` 字段自动注入/清理;`src/env.d.ts` 声明 `*.css`。参考 `markdown/`。 - **HTML**:HTML 节点把 HTML 字符串塞进 sandbox iframe 的 `srcDoc`,自带 `