自定义插件开发
客户端「插件 → 开发文档」提供同样的接口说明、三个可安装示例与完整 JSON,可离线查阅。下文示例版本字段为占位符,实际可运行包从客户端获取。
插件中心、内置插件和客户端内开发文档支持客户端的全部 21 种界面语言,切换语言即可生效。用户插件的名称、简介和面板内容保持作者原文,客户端不自动翻译。
快速开始与示例
用一个本地插件包,将业务工具加入设备大屏。以下三个示例都可以下载、修改和实际安装,不需要编译或联网。
| 字段 / 能力 | 类型 / 状态 | 契约 / 限制 |
|---|---|---|
| 应用便笺 | app-notes | 设备名称 + 实时应用包名 + 输入便笺 + 复制;仅支持 Android。 |
| 应用工具 | app-tool | 仅在 Android 设置应用显示入口;离开设置时关闭面板,验证应用筛选和设备状态。 |
| 文字填入 | text-input | 先在 Android 上点选输入框,再点击面板按钮填入;不会发送或清空草稿。 |
下载示例包,解压后编辑 plugin.json。为自己的插件修改 id、name、author 和描述。
只压缩 plugin.json 本身,确保 ZIP 根目录没有其他文件或外层目录;后缀可用 .zip 或 .vmosplugin。
拖入插件中心,查看详情并确认权限后启用。打开设备独立大屏,点击右侧工具栏里的插件入口。
修改包后先卸载同 ID 旧包再安装。当前没有热重载或覆盖升级,保留原始包便于回退。
接入模型与能力边界
API 1 是声明式 Hook:插件声明需求,宿主订阅设备事件、插入按钮并渲染面板。它不是任意 JavaScript SDK,也不是采集/编码引擎接口。
| 字段 / 能力 | 类型 / 状态 | 契约 / 限制 |
|---|---|---|
| app.package | 已实现 · Android | 宿主将当前前台包名绑定到文本,并根据 packages 控制入口。不是 OCR 读取应用名称。 |
| toolbar | 已实现 · 独立大屏 | 启用插件后动态加入一个工具按钮,无需重启;不支持任意位置、菜单或快捷键注入。 |
| panel | 已实现 · 受限组件 | 开发者声明文本、输入框和动作按钮;宿主负责主题、互斥切换、销毁。不是 HTML / Vue 页面加载器。 |
| 微信增强 | 内置保留 | OCR、草稿替换、发送确认与输入法逻辑暂不抽离,不属于用户插件 API。 |
所有已开放接口只针对当前独立设备大屏;不提供所有设备枚举、群控、聚合大屏插件或后台执行。宿主内部的 IPC 和 window.vmosNative 不是插件可调用接口。
plugin.json → validate → install (disabled) → permission review → enable
→ device/app events → toolbar visibility → user click → exclusive panel
→ user action → permission + capability check → copy / fill清单与包格式
以下为 plugin.json 顶层字段。全部必填,未知字段直接拒绝;字符串长度按 JavaScript 字符串长度计。
| 字段 / 能力 | 类型 / 状态 | 契约 / 限制 |
|---|---|---|
| api_version | 1 | 仅支持数字 1;这不是产品版本号。 |
| id | string ≤ 100 | 小写反向域名式标识,如 myteam.app-tool;必须包含点;vmos. 为保留前缀。正则见下方。 |
| version | string ≤ 60 | 三段 1–4 位数字,可附 1–32 个字母、数字、点或连字符组成的后缀,如 MAJOR.MINOR.PATCH-beta。 |
| name / description / author | string | 非空,分别最多 60 / 500 / 80 字符。 |
| permissions | string[] | 0–4 项,不重复,必须来自权限表;不用权限也要传 []。 |
| toolbar / panel | object | 各一个,分别参见工具栏和面板章节。 |
包最多 1 MiB;解压后 JSON 最多 128 KiB;只接受根目录 plugin.json。拒绝加密包、额外文件、路径穿越及损坏校验值。每客户端最多 50 个用户插件。
同 ID 安装被拒绝,不覆盖现有插件。API 不兼容或校验失败不写入新数据。当前不验证第三方作者身份,安装来源须自行信任。
id: /^[a-z][a-z0-9-]*(?:\.[a-z0-9-]+)+$/
version: /^\d{1,4}\.\d{1,4}\.\d{1,4}(?:-[a-zA-Z0-9.-]{1,32})?$/设备与当前应用 Hook
在 text.text 和 button.text 中写 {{变量名}}。以下为宿主上下文类型,插值最终转换为字符串;没有订阅函数需要插件调用。
| 字段 / 能力 | 类型 / 状态 | 契约 / 限制 |
|---|---|---|
| device.id / device.name | string · device.info | 当前大屏设备标识和名称。不得当作用户账号或稳定的跨传输硬件 ID。 |
| device.platform | "android" · device.info | 当前设备平台。 |
| device.ready | boolean · device.info | 当前大屏是否已进入 streaming;显示 true / false,不表示手机已解锁或应用输入框已聚焦。断流会关闭面板。 |
| device.can_control | boolean · device.info | ready 且当前大屏允许控制;不是某个具体动作的成功保证。 |
| app.package | string | null · app.current | Android 前台应用完整包名,事件更新;未知或未提供时为空字符串。不缓存上一次包名给插件使用。 |
input.<id> | string · none | 当前面板的输入值;无需设备权限,不递归解释值中的模板。 |
前台包名必须来自宿主当前状态,插件不能自己轮询手机。桌面/锁屏/权限遮挡下可能没有可用值;不要把空包名当作微信。应用变化只更新文本和入口,不自动触发复制或填入。
{
"type": "text",
"text": "{{device.name}}\n{{app.package}}\nControl: {{device.can_control}}"
}工具栏扩展 Hook
每个启用的插件提供一个入口,加入大屏现有功能栏;不修改投屏画面或原有功能。
| 字段 / 能力 | 类型 / 状态 | 契约 / 限制 |
|---|---|---|
| toolbar.label | string · required | 非空,最多 12 字符;使用短名称避免挤占工具栏。 |
| toolbar.icon | enum · required | puzzle | message | text | info |
| toolbar.packages | string[] · optional | 省略或 []:不按应用筛选。最多 20 个完整 Android 包名,每项最多 200 字符;精确匹配,无通配符。非空需要 app.current。 |
| 可见 / 可点击 | 宿主行为 | 启用且包名匹配时显示;未出帧时入口禁用。应用不匹配、停用、卸载时移除。 |
| 点击 / 再次点击 | 宿主行为 | 点击打开面板;再次点击同入口关闭。打开另一项功能时切换面板,不堆叠。 |
无需注册函数或手动 DOM 操作。没有自定义图片图标、工具栏排序、多按钮贡献和自定义快捷键接口。所有插件仅在 Android 设备大屏显示,iOS 不开放插件入口。
{
"permissions": [
"app.current"
],
"toolbar": {
"label": "App info",
"icon": "info",
"packages": [
"com.android.settings"
]
}
}右侧面板开发
panel 是受限组件树,由宿主渲染和维护生命周期,自动适配深浅色。每个插件一个面板;当前固定宽度 360 px,可纵向滚动。
| 字段 / 能力 | 类型 / 状态 | 契约 / 限制 |
|---|---|---|
| panel.title / panel.blocks | string / PluginBlock[] | 标题非空,最多 80 字符;blocks 1–30 项。 |
| text: string ≤ 4000 | 非空纯文本,支持变量和换行;不会执行 HTML。 | |
| id / label / default: string | id 小写字母开头,仅小写字母、数字、下划线,≤32 且唯一;label 非空≤80;default 可空≤4000。 | |
| label / action / text: string | label 非空≤80;text 非空≤4000,可用变量;action 仅接受动作表中的值。 |
输入默认值按原文显示,不做模板展开。输入值只保留在当前面板,关闭、切换设备、断流、停用、卸载均清空;没有持久化存储。变量展开后的动作文本最多 10000 字符,超出部分截断。
可用同面板的 input.id 组合工作流,但不能用脚本监听输入、自动发送或自定义网络请求。不要把内置微信的专用页面能力当作用户插件 API。
{
"title": "Text tool",
"blocks": [
{
"type": "input",
"id": "message",
"label": "Message"
},
{
"type": "button",
"label": "Fill, no send",
"action": "device.input_text",
"text": "{{input.message}}"
}
]
}权限与动作
权限在安装时校验、启用时确认。动作必须来自当前面板的用户点击,宿主在执行前再次校验权限和设备能力。
| 字段 / 能力 | 类型 / 状态 | 契约 / 限制 |
|---|---|---|
| device.info | 读取上下文 | 允许 device.* 绑定;不允许访问其他设备。 |
| app.current | 读取应用 | 允许 app.package 和非空 toolbar.packages。 |
| clipboard.write → clipboard.copy | button.action | 写入电脑剪贴板,不读取剪贴板;成功提示“已复制”。仅支持 Android。 |
| device.input_text → device.input_text | button.action | 仅当前可控 Android;请求填入焦点位置,不发送、不替换所有草稿。成功仅代表请求完成,不代表目标应用已接收。 |
解析后文本为空时不执行,也不显示成功。执行中同面板按钮禁用,失败不自动重试;宿主显示失败提示并记录动作类型,不记录正文。插件没有 Promise 返回值或成功/失败回调。
关闭面板或切换应用不能撤回已发出的输入请求。点击前应确认当前输入框,按钮名称应明确“填入,不发送”。微信增强不属于用户插件 API,继续保留内置专用逻辑。
生命周期与清理
下面是已实现的宿主事件语义,不是可调用的 onEnable/onAppChanged 函数。插件当前无需自行注册或注销监听器。
| 字段 / 能力 | 类型 / 状态 | 契约 / 限制 |
|---|---|---|
| 安装 | disabled | 校验并保存;不显示大屏入口,不执行动作。 |
| 启用 | enabled | 确认权限;同步现有大屏;按当前应用决定入口。 |
| 前台变化 | app.package | 更新绑定;不再匹配时关闭面板、清空输入、移除入口。仍匹配时保留当前输入。 |
| 打开 / 关闭 | panel | 打开时初始化默认值;关闭清空。切换到终端、画质等共用互斥面板。 |
| 控制关闭 / 断流 | device.can_control / ready | 控制关闭仅禁用输入动作;断流关闭面板,入口禁用。恢复不自动重开。 |
| 停用 / 卸载 / 换设备 | cleanup | 关闭面板、释放临时输入;不影响其他插件或微信话术库。重启保留安装与启用状态,不保留面板输入。 |
没有定时器或后台任务;设备/应用事件由现有宿主提供。未完成的用户动作不会因再次打开面板而重放。
错误与验收清单
安装错误目前在界面归并提示,下面的错误名用于对应校验逻辑和排查,不是插件可捕获的异常接口。
| 字段 / 能力 | 类型 / 状态 | 契约 / 限制 |
|---|---|---|
| package_size / package_layout / package_crc | ZIP | 检查大小、根目录唯一 plugin.json、压缩方式和完整性。 |
| unsupported_api / unsupported_field | manifest | API 固定为 1;删除文档未定义字段。 |
| invalid_id / invalid_manifest / invalid_block | manifest | 检查字段类型、长度、版本、输入 ID 唯一性与组件结构。 |
| invalid_permission / missing_permission | permissions | 检查允许列表、重复项及动作/绑定所需权限。 |
| invalid_binding / invalid_action | panel | 变量必须存在;动作仅 copy 或 input_text;input 引用应对应已声明 ID。 |
| already_installed / plugin_limit | install | 同 ID 先卸载;用户插件上限 50。失败不覆盖旧插件。 |
验收顺序:安装默认停用 → 权限确认 → 启用 → 大屏入口 → 面板输入/复制 → 应用切换 → 控制关闭 → 断流 → 停用 → 重启 → 卸载。示例工具应在 Android 设置出现、离开后消失。
同时验证深浅色、中英文、窄窗口和两个大屏的状态隔离。文字填入请使用备忘录等无发送风险的输入框;不要把请求成功当成手机实际输入验收。
完整配置模板
app-notes
{
"api_version": 1,
"id": "example.app-notes",
"name": "App notes / 应用便笺",
"version": "PLUGIN_VERSION",
"author": "Example",
"description": "Live foreground app information and a local note. 实时前台应用信息与便笺。",
"permissions": [
"device.info",
"app.current",
"clipboard.write"
],
"toolbar": {
"label": "Notes / 便笺",
"icon": "info"
},
"panel": {
"title": "App notes / 应用便笺",
"blocks": [
{
"type": "text",
"text": "{{device.name}}\n{{app.package}}"
},
{
"type": "input",
"id": "note",
"label": "Note / 便笺"
},
{
"type": "button",
"label": "Copy / 复制",
"action": "clipboard.copy",
"text": "{{app.package}}\n{{input.note}}"
}
]
}
}app-tool
{
"api_version": 1,
"id": "example.app-tool",
"name": "App tool / 应用工具",
"version": "PLUGIN_VERSION",
"author": "VMOS Example",
"description": "Show a tool only in Android Settings. 仅在 Android 设置应用显示入口。",
"permissions": [
"device.info",
"app.current"
],
"toolbar": {
"label": "Info / 信息",
"icon": "info",
"packages": [
"com.android.settings"
]
},
"panel": {
"title": "App context / 应用上下文",
"blocks": [
{
"type": "text",
"text": "{{device.name}}\n{{app.package}}"
},
{
"type": "text",
"text": "Ready: {{device.ready}}\nControl: {{device.can_control}}"
}
]
}
}text-input
{
"api_version": 1,
"id": "example.text-input",
"name": "Text input / 文字填入",
"version": "PLUGIN_VERSION",
"author": "VMOS Example",
"description": "Fill text only on a user click, never send. 用户点击后填入,不发送。",
"permissions": [
"device.input_text"
],
"toolbar": {
"label": "Text / 文字",
"icon": "text"
},
"panel": {
"title": "Fill text / 填入文字",
"blocks": [
{
"type": "text",
"text": "Focus an Android input first. 请先在 Android 上选中输入框。"
},
{
"type": "input",
"id": "message",
"label": "Text / 内容",
"default": ""
},
{
"type": "button",
"label": "Fill, no send / 填入不发送",
"action": "device.input_text",
"text": "{{input.message}}"
}
]
}
}