跳转到内容

插件解剖

一个插件的全部对外契约都写在 plugin.json(manifest)里。本篇讲清它的骨架和背后的运行时/安全模型;逐字段的完整定义见 plugin.json 参考。

{
"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"] }
}

四块内容:

  1. 身份 —— id(reverse-DNS 形态全局唯一,如 xinghan.pomodoro)、name、version、description。
  2. 运行时 —— runtime 二选一 + 对应入口字段(见下节)。
  3. 能力声明 —— capabilities 数组:后端进程可以调用哪些宿主能力。
  4. 贡献 —— contributes:插件向宿主 UI 与系统”长”出来的一切。
runtime: "static" → 必填 ui(静态目录)

宿主不为它起进程,直接把 ui/ 目录静态服务出去。适合纯展示/纯交互的小工具(番茄钟就是)。没有进程就没有 capability —— capabilities 通常为空;需要向其它插件投稿数据时用 manifest 的静态 contributions(加载时由宿主代为注入)。

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(...)

所有对宿主的访问都经它走 —— 这是唯一的门,也因此是唯一需要授权的面。

capabilities 数组是白名单,声明什么才能用什么:

声明开放的能力
dataEngine表格数据读写(查询/增改/字段/视图)
llm经宿主配置的模型发起对话(用户的 provider,插件不碰 key)
storage插件私有 KV 存储
events订阅宿主事件
secrets加密凭据存取
satellite:<kind>访问某类卫星项目(如 satellite:agent = Agent 会话库)

三条纪律:

  • 最小权限申报:这个数组是市场审核与用户安装时看到的授权面,如实只报真用到的。运行时强制目前覆盖卫星作用域(satellite:<kind> 未声明一律 403),其余命名空间的运行时拦截在路线上。
  • 写入走治理:dataEngine 的写方法最终走宿主的 changeset 管线 —— 用户可撤销、进历史、协同端同步。插件没有绕过治理的裸写通道。
  • 尺寸有护栏:字段/视图类贡献的单值写入默认 256KB 上限(防 WebSocket 帧超限与历史膨胀),可在贡献声明里收紧。

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 传引用)
贡献点投稿 data64KB
资产上传(ui 桥 uploadAsset)20MB

责任随自由:能调外网意味着密钥管理是你的责任 —— API key 走 settings 的 secret 类型或 cap.storage,永远别硬编码进包里。