插件开发指南(基于 ESBuild + TSX) 
本指南面向首次开发 Foxel 前端插件 App 的开发者,说明宿主提供的能力、插件对象规范、生命周期、清单上报机制,以及使用 TypeScript + React + esbuild 从 TSX 构建成自包含 JS 的方案。
快速检查清单 
- 已实现 
window.FoxelRegister(plugin),仅导出一个plugin对象 mount(container, ctx)内渲染,必要时实现unmount- 文件读取统一使用 
ctx.urls.downloadUrl - 仅操作 
container,样式使用唯一前缀/ID - 设置合适的 
supportedExts、name、icon、version - 产物为 IIFE 单文件:
dist/plugin.js 
工作原理与数据流 
- 加载方式:宿主以 
<script src="...">注入远程插件 JS。插件脚本执行后必须调用window.FoxelRegister(plugin)注册自身。 - 生命周期: 
mount(container, ctx):宿主传入一个空容器节点与上下文ctx;插件在该节点内渲染自己的 UI。unmount(container)?:可选,宿主在卸载时调用,用于清理副作用(移除事件、销毁 React root 等)。
 - 应用选择:宿主根据插件的 
supportedExts与当前文件扩展名匹配决定是否显示为可用应用。未上报或为空数组时默认允许(便于首次运行)。 - 清单上报:首次安装后,宿主会在成功加载插件并拿到 
plugin对象后,将其元数据上报至后端(非阻塞)。 
读取文件
插件读取文件内容统一使用 ctx.urls.downloadUrl,无需自行调用后端 API。
DOM 范围
仅操作传入的 container 节点。不要修改宿主页面其他 DOM 或全局样式;如需注入样式,请使用唯一前缀/ID 以避免污染。
宿主接口(Host API) 
- 全局注册函数:
window.FoxelRegister?: (plugin: RegisteredPlugin) => void - 插件对象 
RegisteredPlugin字段(均为可选,除mount外):mount(container: HTMLElement, ctx: PluginMountCtx): void | Promise<void>unmount?(container: HTMLElement): void | Promise<void>key?: string:唯一键,建议稳定可读(如org.demo.image-viewer)。name?: string:展示名称。version?: string:版本号(如1.0.0)。supportedExts?: string[]:支持的文件扩展名(小写、无点,如['jpg','png'])。defaultBounds?: { x?: number; y?: number; width?: number; height?: number }:默认窗口位置与尺寸。defaultMaximized?: boolean:是否默认最大化。icon?: string:图标 URL(建议绝对 URL 或 data URI)。description?/author?/website?/github?:元信息,用于“应用”页展示。
 mount上下文PluginMountCtx:filePath: string:当前打开的文件完整路径(虚拟文件系统路径)。entry: { name: string; is_dir: boolean; size: number; mtime: number; type?: string; is_image?: boolean }urls: { downloadUrl: string }:无需鉴权即可读取当前文件内容的临时 URL。host: { close(): void }:请求宿主关闭当前应用视图。
说明:
- 插件不需要自己调用后端 API。读取文件内容统一使用 
ctx.urls.downloadUrl。 - 插件只能操作传入的 
container节点,不要修改宿主页面其他 DOM 或全局样式(如需注入样式,请设置唯一 ID,避免污染)。 
清单上报与后端字段映射 
宿主会将插件对象字段映射到后端(POST /api/plugins/{id}/metadata):
- 命名映射兼容驼峰/下划线:
supportedExts -> supported_exts、defaultBounds -> default_bounds、defaultMaximized -> default_maximized。 - 存储模型见:
models/database.py:Plugin。 - 上报失败不影响插件运行(宿主忽略错误)。
 
开发与构建(TSX -> IIFE JS,自包含) 
- 目标:输出单文件 
dist/plugin.js,格式为 IIFE,可直接通过<script>加载执行。 - 依赖:
react、react-dom、typescript、esbuild(打包时将 React 一并内联,避免外部运行时依赖)。 - 构建命令(示例):
 
bash
esbuild src/index.tsx \
  --bundle \
  --format=iife \
  --platform=browser \
  --target=es2019 \
  --jsx=automatic \
  --minify \
  --outfile=dist/plugin.js- 体积优化:
--define:process.env.NODE_ENV="production" 
文件结构建议:
package.json:含build指令。tsconfig.json:开启严格模式,jsx设为react-jsx。foxel.d.ts:声明宿主期望的类型,便于 TS 校验(只声明类型,不引入宿主代码)。src/App.tsx:React 组件。src/index.tsx:构造plugin对象并window.FoxelRegister(plugin)。
示例:文本查看器(TSX) 
位置:https://github.com/DrizzleTime/foxel-text-viewer
- 功能:加载 
downloadUrl文本;工具栏提供“换行/复制/关闭”。 - 支持扩展名:
['txt','md','log','json','yaml','yml','sh','py','js','ts','css','html'] - 关键点: 
- 在 
mount内创建 React root 渲染;在unmount中销毁 root。 - 仅操作 
container;样式以唯一 ID 注入。 
 - 在 
 
构建:npm i && npm run build(生成 dist/plugin.js)。
本地调试与安装 
- 后端与前端开发环境:
 
bash
# 后端
uv venv && source .venv/bin/activate && uv sync && \
uvicorn main:app --reload --host 0.0.0.0 --port 8000
# 前端(Vite 默认端口 5173)
cd web && bun install && bun run dev- 提供插件 JS 的可访问 URL:
 
- 方式 A:将示例构建产物复制到本仓库 
web/public/plugins/,例如web/public/plugins/my-text-viewer.js,则开发时 URL 为http://127.0.0.1:5173/plugins/my-text-viewer.js。 - 方式 B:使用任意静态服务器(如 nginx、Vercel、GitHub Pages)托管 
dist/plugin.js,使用其公网 URL。 
- 在“应用”页安装:
 
- 打开“应用”页 -> “安装应用” -> 粘贴插件 JS URL -> 安装。
 - 首次安装完成后宿主会更新清单信息;在文件管理页面选择对应文件即可找到插件应用。
 
最佳实践 
- 仅操作 
container;通过唯一 ID 注入样式,避免污染宿主全局。 icon使用绝对 URL 或 data URI;避免相对路径误指宿主站点。supportedExts使用小写、无点后缀;与宿主匹配逻辑一致。- 使用 
ctx.host.close()触发关闭;不要直接移除container。 - 如需并发请求,注意 
unmount时取消(AbortController)。 
清单上报
上报失败不会中断插件运行;宿主忽略该错误以保证体验。
常见问题 
- 未显示为可用应用:确认后缀匹配 
supportedExts;或先清空supportedExts以验证。 - 未上报清单:确认脚本已调用 
window.FoxelRegister(plugin)。 - 图标不显示:确保 URL 可访问或使用 data URI。
 - 脚本加载失败:检查 URL 是否可访问;
<script>不受 CORS 限制,但服务端可能有防盗链限制或 CSP 限制。 
附录:类型声明(摘自宿主实现) 
查看类型声明
ts
// foxel.d.ts 建议拷贝使用(简化版)
export interface RegisteredPlugin {
  mount: (container: HTMLElement, ctx: {
    filePath: string;
    entry: {
      name: string;
      is_dir: boolean;
      size: number;
      mtime: number;
      type?: string;
      is_image?: boolean;
    };
    urls: { downloadUrl: string };
    host: { close: () => void };
  }) => void | Promise<void>;
  unmount?: (container: HTMLElement) => void | Promise<void>;
  key?: string; name?: string; version?: string;
  supportedExts?: string[];
  defaultBounds?: { x?: number; y?: number; width?: number; height?: number };
  defaultMaximized?: boolean;
  icon?: string; description?: string; author?: string; website?: string; github?: string;
}
declare global { interface Window { FoxelRegister?: (plugin: RegisteredPlugin) => void } }按各示例 package.json 的 build 指令构建后,得到 dist/plugin.js,用于在“应用”页安装。