| 字段 | 作用 | 修改规则 |
|---|---|---|
code |
插件的长期身份、存储命名空间和安装索引 | 发布后不可修改 |
version |
入口模块缓存指纹和商店发布版本 | 代码、清单或资源变更时递增 |
entry |
包内入口文件路径 | 变更后递增版本并确保文件存在 |
runtime |
插件运行时 | 当前固定为 vue |
| 场景 | 加载方式 | 修改后处理 |
|---|---|---|
| 本地调试 | 客户端托管 dist 入口,附带重载指纹 |
Vite 重建后点击“重新加载” |
| 已安装插件 | entryUrl?v=<version> |
必须递增 version 并重新安装/更新 |
| 独立窗口 | 复用同一插件上下文和版本 | 由宿主绑定入口和 pluginCode |
api && api.method 或可选链检测。enterAction.payload 可能是文本,也可能是图片 data URI;处理前先判断 type。api.onPluginEnter、api.onPluginOut、api.onThemeChange 等所有事件监听都返回清理函数;组件卸载时必须调用。这是理解本页其余内容的前提,值得单独说清楚。
你的插件代码是远程 ESM,改完发布即生效,用户下次打开就是新版。但插件能调用的宿主能力长在客户端里——宿主多会一样东西,就必须发一版客户端。而总有一部分用户停在旧版本没更新。
所以「我的插件在旧客户端上会怎样」不是意外情况,而是必然要面对的常态。下面两节给出两条应对路径,优先用第二条。
适合「这个能力缺了只是少个功能」的场景。
hasCapability 探测(推荐)宿主从 0.0.29 起提供能力探测接口:
| 接口 | 签名 | 说明 |
|---|---|---|
api.getApiLevel |
() => number |
宿主 API 级别。级别越高能力越多,只增不减 |
api.hasCapability |
(name) => boolean |
宿主是否具备某个能力(名字与 permissions 一致) |
api.getCapabilities |
() => string[] |
宿主全部能力名,按首次出现的级别排序 |
这三个接口在老客户端上根本不存在,直接调用会抛 TypeError——那正是我们要避免的事。所以必须用兜底写法:
typeof 探测(老客户端也能用)hasCapability 本身在老客户端上不存在,所以如果你的插件要兼容更早的版本,仍然需要 typeof 兜底:
| 能力 | 探测方式 | 老客户端上的表现 |
|---|---|---|
屏幕采集 api.desktopCapturer / api.capture / api.overlay |
typeof api?.desktopCapturer?.getSources === 'function' |
枚举回空数组;capture.getStream 抛错 |
api.file.write |
typeof api?.file?.write === 'function' |
退回「保存对话框 + 浏览器下载」,功能可用但拿不到落盘路径 |
api.holdSessionReset / api.hideMainWindowKeepAlive |
typeof api?.holdSessionReset === 'function' |
不存在;后台任务会在会话重置(默认 30 秒)后被卸载。老客户端上只能提示用户别收起窗口 |
api.systemPreferences.getMediaAccessStatus |
typeof api?.systemPreferences?.getMediaAccessStatus === 'function' |
不存在;被系统拒时只能给出笼统的错误提示 |
api.network.fetch |
typeof api?.network?.fetch === 'function' |
退回浏览器 fetch(受同源策略约束,跨域会失败) |
api.overlay.selectRegion() 返回的是 DIP 屏幕坐标矩形;把它交给 api.getDisplayMatching(rect) 可以换算到采集流内的位置。
比手写探测更省事,而且用户在你插件被打开之前就知道要升级。在 manifest.json 里声明:
| 字段 | 类型 | 说明 |
|---|---|---|
minApiLevel |
number |
宿主 API 级别下限。不声明 = 不挑宿主。可以不写,让宿主按 requires 推导;写了就必须 ≥ requires 隐含的级别,否则上传会被打回 |
requires |
string[] |
硬依赖的宿主能力,最多 12 项。缺任一项,宿主拒绝打开并显示升级提示页 |
optional |
string[] |
可选能力,最多 12 项。缺失时插件照常加载,宿主在界面上提示「有几项功能在当前客户端不可用」 |
requires 任一项 → 拒绝打开,提示页写明缺的是哪项能力,并给出「前往更新」入口;optional → 正常加载,宿主提示缺了几项;minApiLevel 高于本机级别 → 拒绝打开;requires 隐含的级别与 minApiLevel 谁高听谁的。| apiLevel | 能力 |
|---|---|
| 1 | clipboard:read、clipboard:write、network:fetch、file:dialog、file:read、file:write |
| 2 | screen:capture |
| 3 | file:read-content、lan:discover、qrcode:render |
| 4 | lan:scan |
| 5 | system:shell、clipboard:write-files |
| 6 | schedule:request |
| 7 | feature:dynamic、feature:mainpush、plugin:ready |
| 8 | ai:chat、ai:tools |
| 9 | ports:read、ports:kill |
能力名与 permissions 同名,但两者是两条独立的轴,别混:permissions 管「插件获准做什么」(服务端审核 + 运行期权限门),requires / optional 管「宿主会不会做」(客户端在加载前判定)。两个条件都满足才能真正调用。其中 qrcode:render、plugin:ready 属于「宿主得有、但不需要授权」的能力——它们只登记在能力清单里,不进权限白名单。详见权限与能力矩阵。
不要用
getAppVersion()判断能力。 版本号里既有修 bug 也有加能力,拿它比较迟早误判。判断能力请用apiLevel或hasCapability()。
optional,别写 requires。 requires 意味着「这个插件在旧客户端上完全不能用」,对用户是硬伤害;requires(例如纯录屏插件);permissions 管安全(我要读你的剪贴板,服务端审核、用户知情),requires / optional 管兼容(宿主得会屏幕采集)。两者都要满足才能真正调用。同一个名字会同时出现在两边,但回答的问题不同。声明之后你不必做任何商店侧的事——商店页会自动展示兼容状态:
minApiLevel / requires / optional 会在上传时校验,以下情况会被打回(HTTP 400):
requires 和 optional;minApiLevel 不是 ≥ 1 的整数,或超过上限;minApiLevel 低于 requires 隐含的级别(自相矛盾,客户端会按更严的那个执行)。manifest.code、manifest.entry 与包内文件一致。api 为空时仍不会白屏。manifest.permissions 里的每一项都真的用到了——多声明会让用户在安装时看到不必要的授权提示。requires / optional 里声明,且优先写成 optional(见上文「怎么写才对」)。requires 会把旧客户端用户整个挡在门外,别把用不到的能力写进去。