Skip to content

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 / capabilityType / statusContract / limits
App notesapp-notesDevice name, live package, local note and copy. Android only.
App toolapp-toolAppears only in Android Settings. Leaving Settings closes its panel; demonstrates app filtering and device state.
Text inputtext-inputFocus an Android input, then click the panel button to fill. Does not send or clear drafts.
  1. Download and extract an example. Edit plugin.json with your own id, name, author and description.

  2. ZIP only plugin.json at the archive root, without other files or a containing folder. Use .zip or .vmosplugin.

  3. Drop it into Plugin center, review details and enable after checking permissions. Open an independent device window and use the plugin tool.

  4. 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 / capabilityType / statusContract / limits
app.packageAvailable · AndroidThe host binds the foreground package to text and filters tools via packages. This does not OCR the app name.
toolbarAvailable · independent windowAdds one tool dynamically when enabled, without restarting. No arbitrary placement, menu or shortcut injection.
panelAvailable · bounded componentsDeclare text, inputs and action buttons. The host owns theme, exclusive switching and cleanup. Not an HTML / Vue page loader.
WeChat enhancementRemains built-inOCR, 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.

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

Manifest & package

All top-level fields below are required. Unknown fields are rejected. String lengths use JavaScript string length.

Field / capabilityType / statusContract / limits
api_version1Only numeric 1; not the product version.
idstring ≤ 100Lowercase reverse-domain-style ID, e.g. myteam.app-tool; a dot is required, vmos. is reserved. Regex below.
versionstring ≤ 60Three 1–4 digit parts, optionally a 1–32 character suffix of letters, digits, dots or hyphens; e.g. MAJOR.MINOR.PATCH-beta.
name / description / authorstringNon-empty; at most 60 / 500 / 80 characters respectively.
permissionsstring[]0–4 unique entries from the permissions table; use [] when none are needed.
toolbar / panelobjectOne 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.

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})?$/

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 / capabilityType / statusContract / limits
device.id / device.namestring · device.infoCurrent window device ID and name; not a user account or stable cross-transport hardware ID.
device.platform"android" · device.infoCurrent device platform.
device.readyboolean · device.infoWhether the window is streaming; renders true / false. Does not mean unlocked or input-focused. Losing readiness closes the panel.
device.can_controlboolean · device.infoready and window control enabled; not a guarantee that any specific action works.
app.packagestring | null · app.currentFull Android foreground package, updated by events. Unknown or unavailable renders empty. Plugins are not supplied a cached previous package.
input.<id>string · noneCurrent 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.

text
{
  "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 / capabilityType / statusContract / limits
toolbar.labelstring · requiredNon-empty, up to 12 characters. Keep it short.
toolbar.iconenum · requiredpuzzle | message | text | info
toolbar.packagesstring[] · optionalOmitted or []: no app filter. Up to 20 exact Android packages, 200 characters each; no wildcards. Non-empty requires app.current.
Visible / clickableHost behaviorVisible while enabled and matching. Disabled until streaming; removed on mismatch, disable or uninstall.
Click / click againHost behaviorClick 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.

text
{
  "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 / capabilityType / statusContract / limits
panel.title / panel.blocksstring / PluginBlock[]Non-empty title, up to 80 characters; 1–30 blocks.
text: string ≤ 4000Non-empty plain text, with bindings and newlines; no HTML execution.
id / label / default: stringid starts lowercase, uses lowercase letters/digits/underscores, ≤32 and unique; label non-empty ≤80; optional default may be empty, ≤4000.
label / action / text: stringNon-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.

text
{
  "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 / capabilityType / statusContract / limits
device.infoRead contextAllows device.* bindings, not other devices.
app.currentRead appAllows app.package and non-empty toolbar.packages.
clipboard.write → clipboard.copybutton.actionWrites, never reads the computer clipboard. Success shows Copied. Android only.
device.input_text → device.input_textbutton.actionCurrent 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 / capabilityType / statusContract / limits
InstalldisabledValidate and save; no device tool or action yet.
EnableenabledConfirm permissions, synchronize open windows and evaluate current app visibility.
App changedapp.packageUpdate bindings; mismatch closes and clears the panel, removing the tool. Matching changes preserve inputs.
Open / closepanelOpening initializes defaults; closing clears. Shares the exclusive dock with Terminal, Quality and other tools.
Control off / not readydevice.can_control / readyControl off disables input actions; lost readiness closes the panel and disables the tool. Recovery does not reopen it.
Disable / uninstall / device changecleanupClose 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 / capabilityType / statusContract / limits
package_size / package_layout / package_crcZIPCheck size, a sole root plugin.json, compression and integrity.
unsupported_api / unsupported_fieldmanifestUse API 1 and remove undocumented fields.
invalid_id / invalid_manifest / invalid_blockmanifestCheck types, lengths, version, unique input IDs and block shape.
invalid_permission / missing_permissionpermissionsCheck the allowed list, duplicates and action/binding permissions.
invalid_binding / invalid_actionpanelBindings must exist; only copy or input_text actions; input references must match a declared ID.
already_installed / plugin_limitinstallUninstall 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

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