Skip to content

自定义插件开发

客户端「插件 → 开发文档」提供同样的接口说明、三个可安装示例与完整 JSON,可离线查阅。下文示例版本字段为占位符,实际可运行包从客户端获取。

插件中心、内置插件和客户端内开发文档支持客户端的全部 21 种界面语言,切换语言即可生效。用户插件的名称、简介和面板内容保持作者原文,客户端不自动翻译。

快速开始与示例

用一个本地插件包,将业务工具加入设备大屏。以下三个示例都可以下载、修改和实际安装,不需要编译或联网。

字段 / 能力类型 / 状态契约 / 限制
应用便笺app-notes设备名称 + 实时应用包名 + 输入便笺 + 复制;仅支持 Android。
应用工具app-tool仅在 Android 设置应用显示入口;离开设置时关闭面板,验证应用筛选和设备状态。
文字填入text-input先在 Android 上点选输入框,再点击面板按钮填入;不会发送或清空草稿。
  1. 下载示例包,解压后编辑 plugin.json。为自己的插件修改 id、name、author 和描述。

  2. 只压缩 plugin.json 本身,确保 ZIP 根目录没有其他文件或外层目录;后缀可用 .zip 或 .vmosplugin。

  3. 拖入插件中心,查看详情并确认权限后启用。打开设备独立大屏,点击右侧工具栏里的插件入口。

  4. 修改包后先卸载同 ID 旧包再安装。当前没有热重载或覆盖升级,保留原始包便于回退。

接入模型与能力边界

API 1 是声明式 Hook:插件声明需求,宿主订阅设备事件、插入按钮并渲染面板。它不是任意 JavaScript SDK,也不是采集/编码引擎接口。

字段 / 能力类型 / 状态契约 / 限制
app.package已实现 · Android宿主将当前前台包名绑定到文本,并根据 packages 控制入口。不是 OCR 读取应用名称。
toolbar已实现 · 独立大屏启用插件后动态加入一个工具按钮,无需重启;不支持任意位置、菜单或快捷键注入。
panel已实现 · 受限组件开发者声明文本、输入框和动作按钮;宿主负责主题、互斥切换、销毁。不是 HTML / Vue 页面加载器。
微信增强内置保留OCR、草稿替换、发送确认与输入法逻辑暂不抽离,不属于用户插件 API。

所有已开放接口只针对当前独立设备大屏;不提供所有设备枚举、群控、聚合大屏插件或后台执行。宿主内部的 IPC 和 window.vmosNative 不是插件可调用接口。

text
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_version1仅支持数字 1;这不是产品版本号。
idstring ≤ 100小写反向域名式标识,如 myteam.app-tool;必须包含点;vmos. 为保留前缀。正则见下方。
versionstring ≤ 60三段 1–4 位数字,可附 1–32 个字母、数字、点或连字符组成的后缀,如 MAJOR.MINOR.PATCH-beta。
name / description / authorstring非空,分别最多 60 / 500 / 80 字符。
permissionsstring[]0–4 项,不重复,必须来自权限表;不用权限也要传 []。
toolbar / panelobject各一个,分别参见工具栏和面板章节。

包最多 1 MiB;解压后 JSON 最多 128 KiB;只接受根目录 plugin.json。拒绝加密包、额外文件、路径穿越及损坏校验值。每客户端最多 50 个用户插件。

同 ID 安装被拒绝,不覆盖现有插件。API 不兼容或校验失败不写入新数据。当前不验证第三方作者身份,安装来源须自行信任。

text
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.namestring · device.info当前大屏设备标识和名称。不得当作用户账号或稳定的跨传输硬件 ID。
device.platform"android" · device.info当前设备平台。
device.readyboolean · device.info当前大屏是否已进入 streaming;显示 true / false,不表示手机已解锁或应用输入框已聚焦。断流会关闭面板。
device.can_controlboolean · device.infoready 且当前大屏允许控制;不是某个具体动作的成功保证。
app.packagestring | null · app.currentAndroid 前台应用完整包名,事件更新;未知或未提供时为空字符串。不缓存上一次包名给插件使用。
input.<id>string · none当前面板的输入值;无需设备权限,不递归解释值中的模板。

前台包名必须来自宿主当前状态,插件不能自己轮询手机。桌面/锁屏/权限遮挡下可能没有可用值;不要把空包名当作微信。应用变化只更新文本和入口,不自动触发复制或填入。

text
{
  "type": "text",
  "text": "{{device.name}}\n{{app.package}}\nControl: {{device.can_control}}"
}

工具栏扩展 Hook

每个启用的插件提供一个入口,加入大屏现有功能栏;不修改投屏画面或原有功能。

字段 / 能力类型 / 状态契约 / 限制
toolbar.labelstring · required非空,最多 12 字符;使用短名称避免挤占工具栏。
toolbar.iconenum · requiredpuzzle | message | text | info
toolbar.packagesstring[] · optional省略或 []:不按应用筛选。最多 20 个完整 Android 包名,每项最多 200 字符;精确匹配,无通配符。非空需要 app.current。
可见 / 可点击宿主行为启用且包名匹配时显示;未出帧时入口禁用。应用不匹配、停用、卸载时移除。
点击 / 再次点击宿主行为点击打开面板;再次点击同入口关闭。打开另一项功能时切换面板,不堆叠。

无需注册函数或手动 DOM 操作。没有自定义图片图标、工具栏排序、多按钮贡献和自定义快捷键接口。所有插件仅在 Android 设备大屏显示,iOS 不开放插件入口。

text
{
  "permissions": [
    "app.current"
  ],
  "toolbar": {
    "label": "App info",
    "icon": "info",
    "packages": [
      "com.android.settings"
    ]
  }
}

右侧面板开发

panel 是受限组件树,由宿主渲染和维护生命周期,自动适配深浅色。每个插件一个面板;当前固定宽度 360 px,可纵向滚动。

字段 / 能力类型 / 状态契约 / 限制
panel.title / panel.blocksstring / PluginBlock[]标题非空,最多 80 字符;blocks 1–30 项。
text: string ≤ 4000非空纯文本,支持变量和换行;不会执行 HTML。
id / label / default: stringid 小写字母开头,仅小写字母、数字、下划线,≤32 且唯一;label 非空≤80;default 可空≤4000。
label / action / text: stringlabel 非空≤80;text 非空≤4000,可用变量;action 仅接受动作表中的值。

输入默认值按原文显示,不做模板展开。输入值只保留在当前面板,关闭、切换设备、断流、停用、卸载均清空;没有持久化存储。变量展开后的动作文本最多 10000 字符,超出部分截断。

可用同面板的 input.id 组合工作流,但不能用脚本监听输入、自动发送或自定义网络请求。不要把内置微信的专用页面能力当作用户插件 API。

text
{
  "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.copybutton.action写入电脑剪贴板,不读取剪贴板;成功提示“已复制”。仅支持 Android。
device.input_text → device.input_textbutton.action仅当前可控 Android;请求填入焦点位置,不发送、不替换所有草稿。成功仅代表请求完成,不代表目标应用已接收。

解析后文本为空时不执行,也不显示成功。执行中同面板按钮禁用,失败不自动重试;宿主显示失败提示并记录动作类型,不记录正文。插件没有 Promise 返回值或成功/失败回调。

关闭面板或切换应用不能撤回已发出的输入请求。点击前应确认当前输入框,按钮名称应明确“填入,不发送”。微信增强不属于用户插件 API,继续保留内置专用逻辑。

生命周期与清理

下面是已实现的宿主事件语义,不是可调用的 onEnable/onAppChanged 函数。插件当前无需自行注册或注销监听器。

字段 / 能力类型 / 状态契约 / 限制
安装disabled校验并保存;不显示大屏入口,不执行动作。
启用enabled确认权限;同步现有大屏;按当前应用决定入口。
前台变化app.package更新绑定;不再匹配时关闭面板、清空输入、移除入口。仍匹配时保留当前输入。
打开 / 关闭panel打开时初始化默认值;关闭清空。切换到终端、画质等共用互斥面板。
控制关闭 / 断流device.can_control / ready控制关闭仅禁用输入动作;断流关闭面板,入口禁用。恢复不自动重开。
停用 / 卸载 / 换设备cleanup关闭面板、释放临时输入;不影响其他插件或微信话术库。重启保留安装与启用状态,不保留面板输入。

没有定时器或后台任务;设备/应用事件由现有宿主提供。未完成的用户动作不会因再次打开面板而重放。

错误与验收清单

安装错误目前在界面归并提示,下面的错误名用于对应校验逻辑和排查,不是插件可捕获的异常接口。

字段 / 能力类型 / 状态契约 / 限制
package_size / package_layout / package_crcZIP检查大小、根目录唯一 plugin.json、压缩方式和完整性。
unsupported_api / unsupported_fieldmanifestAPI 固定为 1;删除文档未定义字段。
invalid_id / invalid_manifest / invalid_blockmanifest检查字段类型、长度、版本、输入 ID 唯一性与组件结构。
invalid_permission / missing_permissionpermissions检查允许列表、重复项及动作/绑定所需权限。
invalid_binding / invalid_actionpanel变量必须存在;动作仅 copy 或 input_text;input 引用应对应已声明 ID。
already_installed / plugin_limitinstall同 ID 先卸载;用户插件上限 50。失败不覆盖旧插件。

验收顺序:安装默认停用 → 权限确认 → 启用 → 大屏入口 → 面板输入/复制 → 应用切换 → 控制关闭 → 断流 → 停用 → 重启 → 卸载。示例工具应在 Android 设置出现、离开后消失。

同时验证深浅色、中英文、窄窗口和两个大屏的状态隔离。文字填入请使用备忘录等无发送风险的输入框;不要把请求成功当成手机实际输入验收。

完整配置模板

app-notes

json
{
  "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

json
{
  "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

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