diff --git a/.github/workflows/publish-plugins.yml b/.github/workflows/publish-plugins.yml new file mode 100644 index 0000000..3779769 --- /dev/null +++ b/.github/workflows/publish-plugins.yml @@ -0,0 +1,47 @@ +name: Publish Official Plugins + +# 构建官方插件并把产物(bundle + 清单)强推到孤儿分支 plugins-dist。 +# 与 github-pages.yml 一致,在打版本 tag 时触发 —— 发版时 web 与官方插件一起发布。 +# 前端经 jsDelivr 从该分支远程拉取;构建产物不进 main、不进 git。 +on: + push: + tags: ["v*"] + workflow_dispatch: + +permissions: + contents: write + +concurrency: + group: publish-plugins + cancel-in-progress: false + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Install build dependencies + working-directory: plugins/canvas/registry + run: npm install --no-audit --no-fund + + - name: Build official plugins + working-directory: plugins/canvas/registry + run: npm run build + + - name: Publish to plugins-dist branch + working-directory: plugins/canvas/registry/dist + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + git init -q + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git checkout -q -b plugins-dist + git add -A + git commit -q -m "publish official plugins @ ${GITHUB_SHA::7}" + git push -f "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" plugins-dist diff --git a/.gitignore b/.gitignore index 2101f44..83a18b4 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,7 @@ data/ web/dist **/dist +web/public/plugins +plugins/canvas/*/dist +plugins/canvas/*/node_modules web/public/plugins \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index fa5e453..79d97e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ ## Unreleased ++ [新增] 画布节点插件系统:支持通过 URL 动态安装/启用/更新/卸载远程节点插件。 ++ [新增] 插件开发 SDK,可用 TypeScript 开发画布节点插件。 ++ [新增] 新增 Markdown、SVG、HTML、3D 全景、便利贴等示例插件。 ++ [新增] 官方插件注册表:节点插件面板可从项目仓库读取官方插件列表并一键安装。 ++ [新增] 在画布右上角工具栏新增「节点插件」入口。 ++ [新增] 支持自定义生图/视频接口调用方式以适配不同中转站。 + + ## v0.7.1 - 2026-07-15 + [修复] 修复通过 `crypto.randomUUID` 不可用导致页面白屏报错的问题,改用 nanoid 生成 id。 @@ -10,7 +18,7 @@ + [新增] Agent 对话消息改用 streamdown 流式渲染,提升Markdown 内容展示效果。 + [新增] Agent 新增画布、工作台、提示词库和素材等站点级工具。 -+ [新增] Agent 面板改为全站常驻右侧栏,开关时同步推动顶栏和页面内容,并保留独立入口按钮。 ++ [新增] Agent 面板改为全站常驻右侧栏,开关时同步推动顶栏和页面内容。 + [新增] Agent 新增 `site_navigate` 工具,支持页面跳转。 + [新增] Agent 对话运行中支持一键停止,中断当前 Codex turn。 + [新增] 画布节点支持统一维护名称字段,默认显示在节点上方,并可直接双击名称编辑。 diff --git a/SECURITY.md b/SECURITY.md index 3a12c7a..d65c860 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -31,6 +31,22 @@ Please include: ## Scope +### Canvas node plugins + +The canvas supports third-party node plugins loaded from a remote URL. By +design, an installed plugin's code runs directly inside the web app with full +access to the page, including locally stored data such as AI API keys. This is +an intentional trade-off for extensibility, and the installer shows a warning +before installing. Therefore: + +- Only install plugins from sources you trust. +- Reports that a *malicious plugin* can access page data or API keys are **out + of scope** — that is the documented behavior of the trust model. +- Reports **in scope** include: the app loading/executing plugin code without + the install confirmation, a plugin escaping its declared node type to break + core app integrity in ways not implied by "runs in the page", or the plugin + source cache being writable by an unrelated origin. + Examples of in-scope reports: - Cross-site scripting or token exfiltration in the web app. diff --git a/docs/content/docs/progress/pending-test.mdx b/docs/content/docs/progress/pending-test.mdx index edd676e..fb1b2dd 100644 --- a/docs/content/docs/progress/pending-test.mdx +++ b/docs/content/docs/progress/pending-test.mdx @@ -5,7 +5,13 @@ description: 当前版本已实现但仍需人工验证的变更项 # 待测试 -- 画布节点创建:鼠标左键双击画布空白区域会在点击位置打开节点选择菜单,可创建文本、图片、视频、音频、生成配置和组节点;双击已有节点或连线不应触发菜单。 +- 画布节点插件系统:内置节点(图片/文本/视频/音频/配置/组)改为统一注册表管理,创建菜单由注册表动态生成;需验证六种内置节点的创建、渲染、缩放、连线、小地图颜色、作为生成输入等行为与之前一致。内置节点仅这六种,其余均为插件。 +- 画布节点插件系统:左上菜单新增「节点插件」,可通过 URL 安装远程插件、启用/禁用、更新、卸载;安装时有安全警告说明插件代码在页面内执行、可访问本地数据。可用 `/plugins/sticky-note.js` 安装便利贴节点验证:换色、编辑、「衍生文本节点」(演示 applyOps 增节点+连线)。 +- 画布节点插件系统:示例插件 Markdown(编辑/渲染)、HTML(沙箱 iframe 渲染,支持 {{input}} 注入上游文本)、SVG(渲染/编辑,可取上游文本 SVG 源码)、3D 全景(three.js CDN 动态加载,可从上游图片节点取全景图并拖拽查看);均为远程插件,需分别安装 `/plugins/markdown.js`、`/plugins/html.js`、`/plugins/svg.js`、`/plugins/panorama.js` 后验证创建、渲染、编辑及与上游节点的交互。 +- 画布节点插件系统:卸载或禁用插件后,画布上遗留的该类型节点显示「缺少插件」占位且数据保留,重新安装/启用后恢复渲染;需验证刷新后已启用插件自动加载。 +- 画布节点插件系统:每个插件为 `plugins/canvas//` 独立目录(各自 package.json + esbuild + src + README,互不耦合),`npm run build` 产物落到 `dist/.js` 并同步到 `web/public/plugins/`;需验证各插件可独立构建与安装。 +- 画布节点插件本地开发:每个插件支持 `npm run dev`(watch 自动构建并同步到 `web/public/plugins/`),配合 `web/.env.local` 的 `VITE_DEV_PLUGINS`(逗号分隔 URL)每次刷新重新拉取、无需反复安装;需验证改源码刷新页面即生效。 +- 画布节点创建:鼠标左键双击画布空白区域会在点击位置打开节点选择菜单,可创建文本、图片、视频、音频、生成配置和组节点;双击已有节点或连线不应触发菜单。 - 本地 Agent 连接说明:新增提醒说明只有安装 Codex 插件或手动添加 MCP 后才会增加 Codex token 消耗,直接运行 `npx -y @basketikun/canvas-agent` 不会安装 MCP;需验证文案位置清晰。 - 本地 Agent 连接说明:画布面板和配置页 Codex Tab 改为区分“Codex 插件启动画布”和“直接运行 Agent 后网页连接”两种方式,需验证文案和布局清晰。 - 全站 Agent:Agent 面板从画布页抽离为全站常驻面板,挂在全局布局右侧,开/关只挤压左侧内容不遮挡;顶栏和画布工具条各有一个开关按钮,需验证在首页、生图、视频、素材、提示词库、画布等页面都能打开面板并保持 SSE 连接不断开。 diff --git a/plugins/canvas/README.md b/plugins/canvas/README.md new file mode 100644 index 0000000..d574e31 --- /dev/null +++ b/plugins/canvas/README.md @@ -0,0 +1,163 @@ +# Infinite Canvas 画布节点插件 + +给画布扩展自定义节点。每个插件是一个**独立目录**,用 **TypeScript** 编写,自带 `package.json` / `build.mjs` / `src/index.tsx` / `dist/`,互不耦合,可单独构建、发布、升级。 + +内置节点只有文本、图片、视频、音频、生成配置、组六种;其余节点(Markdown、SVG、HTML、3D 全景、便利贴……)都是插件。 + +作者只写节点 UI 与逻辑,**类型、JSX、宿主 React、构建全部由 [`@infinite-canvas/plugin-sdk`](./sdk/README.md) 提供**,写 TSX 全程有代码提示;产物仍是宿主加载器现有契约的 ESM(React external、宿主单例)。 + +## 目录约定 + +``` +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 构建,产物同步到 web/public/plugins/my-plugin.js +npm run typecheck # tsc --noEmit,类型自检 +``` + +产物名取**目录名**,复制后记得把目录改成插件名。 + +## 构建 / 发布 / 升级 + +```bash +cd plugins/canvas/ +npm install +npm run build # → dist/.js,并同步到 web/public/plugins/.js +npm run dev # watch,改动自动构建并同步 +``` + +把 `dist/.js` 托管到任意静态地址(CDN、GitHub Raw、对象存储),用户在画布「节点插件」管理器填该 URL 安装。升级时重新构建覆盖同一 URL,用户点「更新」即可。 + +## 官方插件注册表 + +本项目官方插件由 CI 集中构建后发布到孤儿分支 `plugins-dist`(**构建产物不进 git**),画布「节点插件」面板顶部的**官方插件**区经 jsDelivr 从该分支远程拉取并一键安装;第三方插件仍走下方「第三方插件」的 JS URL 安装。构建脚本与发布说明见 [`registry/`](./registry/README.md);清单地址可用 `VITE_PLUGIN_REGISTRY_URL` 覆盖成自建来源。 + +## 本地开发 + +`npm run dev` 起 watch,产物会同步到 `web/public/plugins/.js`。此后有两种方式在画布里用到它: + +**方式一(推荐):自动发现。** 画布启动时会扫描 `web/public/plugins/` 下的插件,自动加入「节点插件」管理器列表,**默认关闭**;打开开关即启用。无需手动填 URL,启用时会按文件重新拉取,配合 watch 改完刷新即最新。 + +**方式二:`VITE_DEV_PLUGINS`。** 在 `web/.env.local` 声明(逗号分隔多个),这些插件每次刷新页面都**重新拉取并直接激活**(不缓存、不落库、无开关): + +```env +VITE_DEV_PLUGINS=/plugins/markdown.js,/plugins/svg.js +``` + +再起画布 `web`(`npm run dev`)。流程即:改 `src/index.tsx` → watch 自动构建 → 刷新画布看到最新效果,无需反复安装。 + +## 用 SDK 写插件 + +默认导出 `definePlugin({...})`(对象形式,**无需再 `const { React } = runtime`**): + +```tsx +import { definePlugin, useState } from "@infinite-canvas/plugin-sdk"; +import type { CanvasNodeContentProps } from "@infinite-canvas/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"|"video"|"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`,自带 `