Custom plugin development
Plugins → Developer docs provides this reference, three installable examples and complete JSON offline. Version fields below are placeholders; download runnable packages from the client.
The plugin center, built-in plugins and in-app developer documentation follow all 21 client interface languages. User-plugin names, descriptions and panel content remain as supplied by the author; the client does not translate them automatically.
Quick start & examples
Add workflow tools to a device window with a local package. All three examples below can be downloaded, edited and installed without compilation or a network connection.
| Field / capability | Type / status | Contract / limits |
|---|---|---|
| App notes | app-notes | Device name, live package, local note and copy. Android only. |
| App tool | app-tool | Appears only in Android Settings. Leaving Settings closes its panel; demonstrates app filtering and device state. |
| Text input | text-input | Focus an Android input, then click the panel button to fill. Does not send or clear drafts. |
Download and extract an example. Edit plugin.json with your own id, name, author and description.
ZIP only plugin.json at the archive root, without other files or a containing folder. Use .zip or .vmosplugin.
Drop it into Plugin center, review details and enable after checking permissions. Open an independent device window and use the plugin tool.
Uninstall the old ID before installing an edited package. Hot reload and in-place updates are unavailable; keep the original package for rollback.
Integration model & scope
API 1 provides declarative hooks: the host subscribes to device events, contributes buttons and renders panels from a manifest. It is not an arbitrary JavaScript SDK or capture/codec API.
| Field / capability | Type / status | Contract / limits |
|---|---|---|
| app.package | Available · Android | The host binds the foreground package to text and filters tools via packages. This does not OCR the app name. |
| toolbar | Available · independent window | Adds one tool dynamically when enabled, without restarting. No arbitrary placement, menu or shortcut injection. |
| panel | Available · bounded components | Declare text, inputs and action buttons. The host owns theme, exclusive switching and cleanup. Not an HTML / Vue page loader. |
| WeChat enhancement | Remains built-in | OCR, draft replacement, send confirmation and IME handling stay internal, outside the user-plugin API. |
Exposed capabilities target only the current independent device window. No all-device enumeration, group control, aggregate-window plugins or background execution. Internal IPC and window.vmosNative are not public plugin APIs.
plugin.json → validate → install (disabled) → permission review → enable
→ device/app events → toolbar visibility → user click → exclusive panel
→ user action → permission + capability check → copy / fillManifest & package
All top-level fields below are required. Unknown fields are rejected. String lengths use JavaScript string length.
| Field / capability | Type / status | Contract / limits |
|---|---|---|
| api_version | 1 | Only numeric 1; not the product version. |
| id | string ≤ 100 | Lowercase reverse-domain-style ID, e.g. myteam.app-tool; a dot is required, vmos. is reserved. Regex below. |
| version | string ≤ 60 | Three 1–4 digit parts, optionally a 1–32 character suffix of letters, digits, dots or hyphens; e.g. MAJOR.MINOR.PATCH-beta. |
| name / description / author | string | Non-empty; at most 60 / 500 / 80 characters respectively. |
| permissions | string[] | 0–4 unique entries from the permissions table; use [] when none are needed. |
| toolbar / panel | object | One each; see Toolbar and Panel sections. |
At most 1 MiB compressed and 128 KiB extracted JSON, root plugin.json only. Encrypted archives, extra files, path traversal and corrupt checksums are rejected. At most 50 user plugins per client.
Duplicate IDs are rejected without overwriting existing plugins. Invalid or incompatible packages are not saved. Third-party authors are not identity-verified; trust the package source yourself.
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})?$/Device & current-app hooks
Use {{binding}} in text.text and button.text. The host context types below render as strings; plugins do not call a subscription function.
| Field / capability | Type / status | Contract / limits |
|---|---|---|
| device.id / device.name | string · device.info | Current window device ID and name; not a user account or stable cross-transport hardware ID. |
| device.platform | "android" · device.info | Current device platform. |
| device.ready | boolean · device.info | Whether the window is streaming; renders true / false. Does not mean unlocked or input-focused. Losing readiness closes the panel. |
| device.can_control | boolean · device.info | ready and window control enabled; not a guarantee that any specific action works. |
| app.package | string | null · app.current | Full Android foreground package, updated by events. Unknown or unavailable renders empty. Plugins are not supplied a cached previous package. |
input.<id> | string · none | Current panel input value; no device permission, no recursive template expansion. |
Package context comes from host state, not plugin polling. Home, lock or protected screens may provide no usable value; empty does not mean WeChat. Changes update text and visibility only, never copy or fill automatically.
{
"type": "text",
"text": "{{device.name}}\n{{app.package}}\nControl: {{device.can_control}}"
}Toolbar contribution hook
Each enabled plugin contributes one tool to the existing device toolbar without modifying the video or built-in tools.
| Field / capability | Type / status | Contract / limits |
|---|---|---|
| toolbar.label | string · required | Non-empty, up to 12 characters. Keep it short. |
| toolbar.icon | enum · required | puzzle | message | text | info |
| toolbar.packages | string[] · optional | Omitted or []: no app filter. Up to 20 exact Android packages, 200 characters each; no wildcards. Non-empty requires app.current. |
| Visible / clickable | Host behavior | Visible while enabled and matching. Disabled until streaming; removed on mismatch, disable or uninstall. |
| Click / click again | Host behavior | Click opens; clicking the active tool closes. Opening another tool switches the dock instead of stacking panels. |
No registration function or manual DOM manipulation. Custom image icons, ordering, multiple contributed buttons and custom shortcuts are not exposed. All plugins are available only in Android device windows; iOS has no plugin entry.
{
"permissions": [
"app.current"
],
"toolbar": {
"label": "App info",
"icon": "info",
"packages": [
"com.android.settings"
]
}
}Right-side panel development
panel is a bounded component tree rendered and managed by the host, with theme support. One panel per plugin, currently 360 px wide with vertical scrolling.
| Field / capability | Type / status | Contract / limits |
|---|---|---|
| panel.title / panel.blocks | string / PluginBlock[] | Non-empty title, up to 80 characters; 1–30 blocks. |
| text: string ≤ 4000 | Non-empty plain text, with bindings and newlines; no HTML execution. | |
| id / label / default: string | id starts lowercase, uses lowercase letters/digits/underscores, ≤32 and unique; label non-empty ≤80; optional default may be empty, ≤4000. | |
| label / action / text: string | Non-empty label ≤80; non-empty text ≤4000 with optional bindings; action must be in the action table. |
Input defaults are literal. Values stay only in the current panel and clear on close, device switch, lost readiness, disable or uninstall. No persistent storage. Expanded action text is truncated to 10000 characters.
Compose workflows from panel inputs, but no scripts listening to input, automatic sends or custom network requests. Built-in WeChat UI capabilities are not user-plugin APIs.
{
"title": "Text tool",
"blocks": [
{
"type": "input",
"id": "message",
"label": "Message"
},
{
"type": "button",
"label": "Fill, no send",
"action": "device.input_text",
"text": "{{input.message}}"
}
]
}Permissions & actions
Permissions are validated at install and confirmed on enable. Actions require a user click in the active panel; the host checks permissions and device capability again before executing.
| Field / capability | Type / status | Contract / limits |
|---|---|---|
| device.info | Read context | Allows device.* bindings, not other devices. |
| app.current | Read app | Allows app.package and non-empty toolbar.packages. |
| clipboard.write → clipboard.copy | button.action | Writes, never reads the computer clipboard. Success shows Copied. Android only. |
| device.input_text → device.input_text | button.action | Current controllable Android only. Requests insertion at focus, never sends or replaces the entire draft. Completion acknowledges the request, not target-app receipt. |
Empty resolved text does nothing and shows no success. Buttons are busy during execution; failure is not retried. The host reports failures and logs action type, not body text. Plugins receive no Promise or completion callback.
Closing a panel or switching apps cannot undo an input request already dispatched. Confirm input focus first and label the action Fill, no send. Built-in WeChat remains outside the user-plugin API.
Lifecycle & cleanup
These are implemented host event semantics, not callable onEnable/onAppChanged functions. Plugins do not register or remove listeners.
| Field / capability | Type / status | Contract / limits |
|---|---|---|
| Install | disabled | Validate and save; no device tool or action yet. |
| Enable | enabled | Confirm permissions, synchronize open windows and evaluate current app visibility. |
| App changed | app.package | Update bindings; mismatch closes and clears the panel, removing the tool. Matching changes preserve inputs. |
| Open / close | panel | Opening initializes defaults; closing clears. Shares the exclusive dock with Terminal, Quality and other tools. |
| Control off / not ready | device.can_control / ready | Control off disables input actions; lost readiness closes the panel and disables the tool. Recovery does not reopen it. |
| Disable / uninstall / device change | cleanup | Close and clear transient input without affecting other plugins or WeChat phrases. Restart keeps install/enable state, not panel inputs. |
No timers or background tasks. Existing host events supply device/app state. Reopening does not replay an earlier action.
Errors & acceptance checklist
The UI currently groups installation errors. Names below identify validation failures for troubleshooting, not exceptions catchable by plugin code.
| Field / capability | Type / status | Contract / limits |
|---|---|---|
| package_size / package_layout / package_crc | ZIP | Check size, a sole root plugin.json, compression and integrity. |
| unsupported_api / unsupported_field | manifest | Use API 1 and remove undocumented fields. |
| invalid_id / invalid_manifest / invalid_block | manifest | Check types, lengths, version, unique input IDs and block shape. |
| invalid_permission / missing_permission | permissions | Check the allowed list, duplicates and action/binding permissions. |
| invalid_binding / invalid_action | panel | Bindings must exist; only copy or input_text actions; input references must match a declared ID. |
| already_installed / plugin_limit | install | Uninstall the duplicate ID first; 50-plugin limit. Failures do not overwrite existing plugins. |
Acceptance: install disabled → permission review → enable → tool → panel input/copy → app change → control off → disconnect → disable → restart → uninstall. The app-tool example appears in Android Settings and disappears elsewhere.
Also check themes, languages, narrow windows and isolation between two device windows. Test filling in a notes field without message sending; request completion is not hardware input acceptance.
Complete manifest templates
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}}"
}
]
}
}