插件解剖
一个插件的全部对外契约都写在 plugin.json(manifest)里。本篇讲清它的骨架和背后的运行时/安全模型;逐字段的完整定义见 plugin.json 参考。
manifest 骨架
Section titled “manifest 骨架”{ "id": "xinghan.example", "name": "示例插件", "version": "1.0.0", "description": "一句话说明(市场卡片用)", "runtime": "node", "entry": "dist/server.js", "devEntry": "src/server.ts", "ui": "ui", "capabilities": ["dataEngine", "storage"], "contributes": { "views": [], "contextMenus": [] }, "market": { "category": "效率", "tags": ["demo"] }}四块内容:
- 身份 ——
id(reverse-DNS 形态全局唯一,如xinghan.pomodoro)、name、version、description。 - 运行时 ——
runtime二选一 + 对应入口字段(见下节)。 - 能力声明 ——
capabilities数组:后端进程可以调用哪些宿主能力。 - 贡献 ——
contributes:插件向宿主 UI 与系统”长”出来的一切。
static —— 纯前端
Section titled “static —— 纯前端”runtime: "static" → 必填 ui(静态目录)宿主不为它起进程,直接把 ui/ 目录静态服务出去。适合纯展示/纯交互的小工具(番茄钟就是)。没有进程就没有 capability —— capabilities 通常为空;需要向其它插件投稿数据时用 manifest 的静态 contributions(加载时由宿主代为注入)。
node —— 带后端进程
Section titled “node —— 带后端进程”runtime: "node" → 必填 entry(启动入口)宿主为插件 fork 一个 node 子进程,把 /plugins/<id>/* 的 HTTP 请求反代给它 —— 你的 UI 与后端之间就是普通的 HTTP。进程启动时从 SDK 拿到一个 CapabilityClient:
import { CapabilityClient } from '@xinghan/plugin-sdk/server'
const cap = new CapabilityClient({ baseUrl: process.env.CAPABILITY_BASE_URL, // 宿主注入 token: process.env.CAPABILITY_TOKEN,})// cap.dataEngine.queryRecords(...) / cap.storage.get(...) / cap.fileTree.list(...)所有对宿主的访问都经它走 —— 这是唯一的门,也因此是唯一需要授权的面。
能力授权模型
Section titled “能力授权模型”capabilities 数组是白名单,声明什么才能用什么:
| 声明 | 开放的能力 |
|---|---|
dataEngine | 表格数据读写(查询/增改/字段/视图) |
llm | 经宿主配置的模型发起对话(用户的 provider,插件不碰 key) |
storage | 插件私有 KV 存储 |
events | 订阅宿主事件 |
secrets | 加密凭据存取 |
satellite:<kind> | 访问某类卫星项目(如 satellite:agent = Agent 会话库) |
三条纪律:
- 最小权限申报:这个数组是市场审核与用户安装时看到的授权面,如实只报真用到的。运行时强制目前覆盖卫星作用域(
satellite:<kind>未声明一律 403),其余命名空间的运行时拦截在路线上。 - 写入走治理:
dataEngine的写方法最终走宿主的 changeset 管线 —— 用户可撤销、进历史、协同端同步。插件没有绕过治理的裸写通道。 - 尺寸有护栏:字段/视图类贡献的单值写入默认 256KB 上限(防 WebSocket 帧超限与历史膨胀),可在贡献声明里收紧。
contributes:十类贡献点
Section titled “contributes:十类贡献点”contributes 是插件”长”进宿主的全部方式,十类各管一件事:
| 键 | 一句话 | 详细 |
|---|---|---|
views | 一块 iframe UI,挂在抽屉/活动栏/设置页等位置 | 面板视图 |
commands | 命令与快捷键 | 右键菜单与命令 |
contextMenus | 表格/文件树右键菜单项,点击带选区上下文路由回插件 | 同上 |
settings | 简单 key/value 配置项,宿主偏好页自动渲染表单 | manifest 参考 |
fieldTypes | 自定义字段类型:骑在基础类型上存储,渲染/编辑归插件 | 同上 |
fileTypes | 自定义文件类型:新建入口 + 编辑区渲染;与内建同名 = 认领接管 | 同上 |
fileRenderers | 只接管某 fileType 的渲染(不进新建菜单) | 同上 |
tableViews | 同一张表的另一种投影视图(日历/画廊/…) | 同上 |
automationNodes | 自定义自动化节点:声明式配置面板 + 后端执行 | 同上 |
contributionPoints / contributions | 插件间扩展面:开放一个点 / 向某个点投稿纯数据 | 同上 |
一个贯穿的设计哲学值得记住:存储形态不变,体验归插件。字段插件骑在基础类型上(未装插件的协同端仍能显示摘要)、文件插件认领内建类型(卸载后节点零迁移回归内建编辑器)、表视图未装时显示占位但数据不丢 —— 你的插件消失时,用户的数据永远完好。
开发前最常问的”我能不能……”,一次说清:
能自由做的:
- 调外网——iframe 没有外联 CSP 白名单,
fetch任意 HTTPS 服务、加载 CDN 资源都可以;node 后端就是普通 node 进程,网络同样不设限。翻译、AI、地图这类要打第三方 API 的插件不需要任何申请; - iframe sandbox 近乎全放行(脚本/表单/弹窗/模态/下载/剪贴板等都开着),唯一收窄:顶层导航需要用户手势(脚本不能静默把整个应用跳走)。
真正的边界只有一条:宿主的数据与能力只能经声明过的 capability / ui 桥进出。你拿不到宿主的 DOM、别的插件的进程状态、未声明的能力面 —— 隔离靠 iframe + 独立进程 + 能力白名单三层,而不是靠审查你的代码。
尺寸护栏(防历史膨胀与协同帧超限,都可在贡献声明里收紧):
| 面 | 默认上限 |
|---|---|
| 字段 cell 值 / 表视图单值写入 | 256KB(validation.maxBytes / writes.maxBytes) |
| 自动化节点返回值 | 256KB(大产物走 uploadResultAsset 传引用) |
| 贡献点投稿 data | 64KB |
资产上传(ui 桥 uploadAsset) | 20MB |
责任随自由:能调外网意味着密钥管理是你的责任 —— API key 走 settings 的 secret 类型或 cap.storage,永远别硬编码进包里。