Files

540 lines
22 KiB
Markdown

---
name: create-moviepilot-plugin
version: 3
description: >-
Use this skill when the user asks to create, modify, debug, validate, or
scaffold a MoviePilot local plugin. Covers MoviePilot V2 plugin development,
_PluginBase implementations, package.v2.json/package.json market metadata,
plugins.v2/plugins source layout, PLUGIN_LOCAL_REPO_PATHS local plugin
sources, plugin APIs, Vuetify JSON forms/pages/dashboards, Vue module
federation remote components, get_render_mode, get_sidebar_nav, plugin
sidebar pages, commands, services, workflow actions, agent tools, and local
install/reload flows. Also use for Chinese requests mentioning 编写插件、本地插件源,
插件开发, V2插件, 插件市场, 本地安装插件, 插件热加载, 前端联邦, 侧栏入口, Vue插件页面.
allowed-tools: list_directory read_file write_file edit_file execute_command search_web browse_webpage query_system_settings update_system_settings query_market_plugins install_plugin reload_plugin query_installed_plugins
---
# Create MoviePilot Plugin
Use this skill to build or revise MoviePilot plugins that can be developed from
a local plugin source and installed into the running MoviePilot instance.
## Ground Truth
- Host plugin contract: `app/plugins/__init__.py`, especially `_PluginBase`.
- Host plugin discovery, local source sync, install, reload: `app/core/plugin.py`
and `app/helper/plugin.py`.
- Host plugin endpoints, API auth, static files, remotes, and sidebar nav:
`app/api/endpoints/plugin.py`.
- Local development note: `docs/development-setup.md`.
- Plugin repository conventions: `MoviePilot-Plugins` uses `plugins.v2/` with
`package.v2.json` for V2 plugins; legacy or cross-generation entries may use
`plugins/` with `package.json`.
- When working in or from `MoviePilot-Plugins`, read its `README.md`,
`docs/Repository_Guide.md`, and `docs/V2_Plugin_Development.md`. For
scenario-specific extensions, read the matching `docs/faq/*.md`.
## Code Tool Workflow
- Use `execute_command(action="run")` with `rg` and narrow globs or paths to
locate plugin classes, extension points, tests, and package entries. Use
`list_directory` only when inspecting one known folder or a configured remote
storage backend.
- Read the relevant implementation and adjacent example before editing.
- If `read_file` reports truncation, continue with smaller `start_line` and
`end_line` ranges until all relevant sections have been inspected.
- Before using a Python or Node.js dependency API, determine the exact installed
or locked version from requirements, package manifests, lockfiles, local
package source, and `.pyi`/`.d.ts` declarations. If those are insufficient,
use `search_web` with the official documentation domain and `browse_webpage`
to read the matching version. Do not guess API signatures from memory or mix
examples from different major versions. Search the relevant package directory,
`.venv`, or `node_modules` directly with `rg` instead of scanning the entire
project without bounds.
- Use `edit_file` for localized changes. Its `old_text` must identify one exact
location by default; add surrounding context instead of enabling
`replace_all` unless every match intentionally changes.
- Use `write_file` for new files. Existing files require `overwrite=true` for a
full rewrite; first call `read_file(include_metadata=true)` and pass its
`sha256` as `expected_sha256` when replacing previously read content.
- Use `execute_command(action="run")` for short validation, Git, and diagnostic
commands. Use `action="start"` only for interactive or long-running commands,
then continue through the returned session ID.
- Do not use shell redirection or inline scripts to perform source edits or to
bypass a file-tool permission error.
- When the plugin uses Vue federation, also read
`MoviePilot-Frontend/docs/module-federation-guide.md`,
`MoviePilot-Frontend/docs/federation-troubleshooting.md`,
`MoviePilot-Frontend/src/utils/federationLoader.ts`, and
`MoviePilot-Frontend/src/pages/plugin-app.vue`.
- Repository boundaries: `MoviePilot` owns runtime loading, API registration,
events, services, data, and permissions; `MoviePilot-Frontend` owns plugin UI
rendering, federation loading, and sidebar pages; `MoviePilot-Plugins` owns
plugin source, icons, package indexes, and release metadata.
## Pre-Flight
1. Understand the user request: plugin purpose, trigger mode, configuration,
output UI, whether it needs a scheduler, API, command, workflow action, or
agent tool.
2. Run the UI Mode Selection Gate before writing any UI code.
- If the user already explicitly chose JSON config/Vuetify JSON or Vue
federation, follow that choice.
- If the plugin has any UI surface and the user has not chosen a mode, ask
them to choose between the two modes below and wait for the answer before
implementing UI files or schemas.
- Do not silently default to either mode just because one seems easier.
3. Inspect existing plugins before creating a new one:
- Local runtime examples: `app/plugins/<plugin>/__init__.py`
- Market/local source candidates: use `query_market_plugins` when the
running instance is available.
- Installed plugin candidates: use `query_installed_plugins`; its summaries
include `repo_url` when the source can be matched from a local plugin
repository or plugin market metadata.
- For Vue federation examples, prefer current compliant plugins such as
`MoviePilot-Plugins/plugins.v2/agenttokens/` and the frontend example
`MoviePilot-Frontend/examples/plugin-component/`.
4. Determine the target source path:
- Query `PLUGIN_LOCAL_REPO_PATHS` with `query_system_settings` when possible.
- If exactly one local plugin repository is configured, prefer that path.
- If several are configured, choose the one the user named; otherwise ask
which repository to use.
- If none is configured, set it before writing plugin code:
`update_system_settings(setting_key="PLUGIN_LOCAL_REPO_PATHS", value="local-plugins", operation="replace")`.
`local-plugins` is resolved relative to the MoviePilot root by the local
plugin source loader. Create that source directory and write the plugin
under it; do not write new plugin source directly into `app/plugins/`
unless the user explicitly asks for a runtime-only experiment.
5. Choose the plugin ID:
- Class name is the plugin ID, for example `MyNotifier`.
- Directory name is the class name lowercased, for example `mynotifier`.
- Avoid collisions with installed or market plugins unless the user is
explicitly modifying that plugin.
- Do not hardcode the original plugin ID for data/config namespaces when the
plugin may support clones; use `self.__class__.__name__`.
## UI Mode Selection Gate
MoviePilot plugin UI has exactly two implementation modes. Make the user choose
one whenever the request includes configuration, detail pages, dashboards,
sidebar pages, or any other plugin UI and the mode is not already explicit.
Ask a concise question like:
```text
这个插件 UI 用哪种方式实现?
1. JSON 配置:后端返回 Vuetify JSON,适合普通配置表单、简单详情页和轻量仪表板。
2. 联邦 UI:独立 Vue 远程组件,适合复杂交互、自定义布局、侧栏全页或多页面。
```
Selection rules:
- **JSON config / Vuetify JSON**: implement `get_form()`, `get_page()`, and
`get_dashboard()` with JSON component schemas. No frontend build or
`dist/assets/remoteEntry.js` is needed.
- **Federation UI / Vue remote component**: implement `get_render_mode()`,
expose Vue components through Vite federation, build frontend assets into the
plugin directory, and use `get_sidebar_nav()` only when a sidebar page is
requested.
- If the plugin truly has no user-facing UI, state that no UI mode is needed
and implement only the backend extension points the request requires.
- Backend-only work may proceed while waiting only if it cannot constrain or
preclude either UI mode.
## Local Source Layout
Default to V2 layout for new local plugins:
```text
<local-plugin-repo>/
├── package.v2.json
└── plugins.v2/
└── <plugin_id_lower>/
├── __init__.py
├── requirements.txt # only when extra runtime dependencies are necessary
└── ... # helper modules, schemas, static assets
```
For a Vue federation plugin, the runtime requirement is the built remote assets
under the plugin directory:
```text
plugins.v2/<plugin_id_lower>/
├── __init__.py
├── dist/
│ └── assets/
│ ├── remoteEntry.js
│ └── ... # JS/CSS/assets referenced by remoteEntry
├── package.json # optional frontend build project metadata
├── vite.config.js # optional frontend build config
└── src/ # optional source, not required at runtime
```
Do not rely on frontend source files at runtime. If the source is kept in the
plugin repository for maintainability, still build and ship the `dist/assets`
files required by `remoteEntry.js`.
Only use the legacy layout when the user explicitly needs it:
```text
<local-plugin-repo>/
├── package.json
└── plugins/
└── <plugin_id_lower>/
└── __init__.py
```
For legacy `package.json` entries that should work on V2, include `"v2": true`.
For V2-first work, prefer `package.v2.json` and `plugins.v2/`.
## Package Metadata
Add or update the package entry for the plugin ID. Keep the package version and
the class `plugin_version` synchronized.
```json
{
"MyNotifier": {
"name": "通知示例",
"description": "根据用户配置发送示例通知。",
"labels": "消息通知",
"version": "1.0.0",
"icon": "mynotifier.png",
"author": "local",
"level": 1,
"system_version": ">=2.12.0",
"history": {
"v1.0.0": "初始版本"
}
}
}
```
Rules:
- The package object key must match the plugin class name.
- `version` must match `plugin_version`.
- `name`, `description`, `icon`, `author`, `labels`, and `level` should match
the plugin class attributes when those attributes exist (`plugin_name`,
`plugin_desc`, `plugin_icon`, `plugin_author`, `plugin_label`, `auth_level`).
- `history` should record user-readable changes for each published version.
- Use `system_version` when the plugin depends on a host capability introduced
in a specific MoviePilot version, including new backend APIs, helpers, events,
Vue federation behavior, sidebar nav, dashboard behavior, or agent tools.
- Use `"release": true` only when the plugin is intentionally distributed by a
GitHub Release archive.
- New plugin entries should usually be appended to the package index so they
appear as newer marketplace items.
- Do not add dependencies unless they are actually required. If
`requirements.txt` changes, the user must reinstall the plugin; hot reload is
not enough to install dependencies.
- Plugin dependencies are installed into the shared MoviePilot Python
environment. Do not pin or downgrade packages already provided by MoviePilot
unless the user has explicitly accepted the compatibility risk.
## Implementation Skeleton
Implement all abstract methods from `_PluginBase`. All new functions and
methods need Chinese docstrings; public classes, public methods, and public
functions are a hard review gate.
```python
from typing import Any, Dict, List, Optional, Tuple
from app.plugins import _PluginBase
class MyNotifier(_PluginBase):
"""通知示例插件。"""
plugin_name = "通知示例"
plugin_desc = "根据用户配置发送示例通知。"
plugin_icon = "mynotifier.png"
plugin_version = "1.0.0"
plugin_label = "消息通知"
plugin_author = "local"
plugin_config_prefix = "mynotifier_"
plugin_order = 100
auth_level = 1
_enabled = False
_message = ""
def init_plugin(self, config: dict = None) -> None:
"""根据插件配置初始化运行状态。"""
self.stop_service()
self._enabled = False
self._message = ""
if not config:
return
self._enabled = bool(config.get("enabled"))
self._message = str(config.get("message") or "")
def get_state(self) -> bool:
"""获取插件启用状态。"""
return self._enabled
@staticmethod
def get_command() -> List[Dict[str, Any]]:
"""返回插件远程命令列表。"""
return []
def get_api(self) -> List[Dict[str, Any]]:
"""返回插件 API 列表。"""
return []
def get_form(self) -> Tuple[Optional[List[dict]], Dict[str, Any]]:
"""返回插件配置表单与默认配置。"""
return [
{
"component": "VForm",
"content": [
{
"component": "VSwitch",
"props": {
"model": "enabled",
"label": "启用插件"
}
},
{
"component": "VTextField",
"props": {
"model": "message",
"label": "通知内容"
}
}
]
}
], {
"enabled": False,
"message": ""
}
def get_page(self) -> Optional[List[dict]]:
"""返回插件详情页面。"""
if not self._enabled:
return None
return [
{
"component": "VAlert",
"props": {
"type": "info",
"text": self._message or "插件已启用"
}
}
]
def stop_service(self) -> None:
"""停止插件后台服务并释放资源。"""
return None
```
## Extension Points
Use only the extension points the requested plugin actually needs:
- Configuration: `get_form()` returns Vuetify form schema and default data;
`init_plugin()` reads config; `update_config()` persists internal changes.
- Data: use `save_data()`, `get_data()`, `del_data()`, and `get_data_path()`.
- Notification: use `post_message()` instead of directly calling message
modules.
- APIs: return route definitions from `get_api()`; default auth is `apikey`
when `auth` is omitted. Vue component APIs should normally use
`auth: "bear"` and be called through the `api` prop passed by the frontend.
- Commands: return slash-command definitions from `get_command()` and dispatch
through MoviePilot events.
- Services: return scheduler services from `get_service()` and always clean
them up in `stop_service()`.
- Dashboards: use `get_dashboard_meta()` and `get_dashboard()` for homepage
widgets.
- Workflow actions: use `get_actions()`; action functions receive
`ActionContent` first and return `(success, action_content)`.
- Agent tools: use `get_agent_tools()`; each tool class must inherit
`app.agent.tools.base.MoviePilotTool`.
- Custom Vue UI: implement `get_render_mode()` only when Vuetify schema cannot
satisfy the request. Return `("vue", "<compiled-assets-path>")` and include
built frontend assets in the plugin directory.
## Vue Federation UI
Use Vue federation only after the Pre-Flight UI decision says JSON schema is not
enough. A Vue plugin must align backend methods, built files, and federation
exposes.
Backend requirements:
```python
from typing import Any, Dict, List, Tuple
@staticmethod
def get_render_mode() -> Tuple[str, str]:
"""声明插件使用 Vue 联邦组件渲染。"""
return "vue", "dist/assets"
def get_form(self) -> Tuple[List[dict], Dict[str, Any]]:
"""Vue 模式下返回默认配置模型。"""
return [], self._current_config()
def get_page(self) -> List[dict]:
"""Vue 模式下详情页由远程 Page 组件渲染。"""
return []
```
When the plugin needs a main-layout sidebar page, also implement:
```python
def get_sidebar_nav(self) -> List[Dict[str, Any]]:
"""声明插件在主界面左侧导航栏中的全页入口。"""
if not self.get_state():
return []
return [
{
"nav_key": "main",
"title": "我的插件",
"icon": "mdi-puzzle",
"section": "system",
"permission": "manage",
"order": 10,
}
]
```
Sidebar rules:
- Sidebar entries are only aggregated for enabled plugins whose
`get_render_mode()` returns `"vue"`.
- `section` must be one of `start`, `discovery`, `subscribe`, `organize`,
`system`; invalid values fall back to `system`.
- `permission` may be `subscribe`, `discovery`, `search`, `manage`, or `admin`;
invalid values are ignored.
- `nav_key` defaults to `main` and must not contain `/`, `?`, `#`, or spaces.
- Multiple sidebar entries are allowed; each entry needs a stable `nav_key`.
Frontend federation requirements:
```js
federation({
name: 'MyPlugin',
filename: 'remoteEntry.js',
exposes: {
'./Page': './src/components/Page.vue',
'./Config': './src/components/Config.vue',
'./Dashboard': './src/components/Dashboard.vue',
'./AppPage': './src/components/AppPage.vue',
'./AppPageSettings': './src/components/AppPageSettings.vue',
},
shared: {
vue: { requiredVersion: false, generate: false },
vuetify: { requiredVersion: false, generate: false, singleton: true },
'vuetify/styles': { requiredVersion: false, generate: false, singleton: true },
},
format: 'esm',
})
```
Build requirements:
- Set Vite `build.target` to `esnext` because federation uses top-level await.
- Use `cssCodeSplit: true` and scoped/component-local styles where possible.
- Build with the frontend project's documented command, then keep `remoteEntry.js`
and every JS/CSS/asset file it references under `dist/assets`.
- Do not add frontend runtime dependencies to the plugin Python
`requirements.txt`; keep frontend dependencies in the frontend build project.
Component contracts:
- `Page` renders the plugin detail dialog and may emit `action`, `switch`, and
`close`.
- `Config` renders plugin settings, receives `initialConfig` and `api`, and
emits `save`, `close`, and `switch`.
- `Dashboard` receives `config` and `allowRefresh`.
- `AppPage` renders the main-layout sidebar page and receives `api`, `pluginId`,
and `navKey`.
- For sidebar `nav_key=main`, the frontend loads `./AppPage` then `./Page`.
- For any other `nav_key`, the frontend loads `./AppPage{PascalCase(nav_key)}`,
then `./AppPage`, then `./Page`. Examples: `settings -> AppPageSettings`,
`my_tool -> AppPageMyTool`.
- A single `AppPage` may branch on `navKey`, or separate
`AppPage{PascalCase}` files may be exposed for specific entries.
Vue API calls:
- Define frontend-facing plugin APIs with `auth: "bear"`.
- Call them with the injected API object, for example
`props.api.get(\`plugin/${props.pluginId}/history\`)`.
- Do not pass `settings.API_TOKEN` into Vue components for browser-side calls.
## Local Install And Reload
1. After writing files in a configured local plugin repository, call
`query_market_plugins(query="<PluginID>", force_refresh=True)` to confirm the
local source is visible.
2. Install or reinstall with `install_plugin(plugin_id="<PluginID>", force=True)`.
The install flow copies the source into `app/plugins/<plugin_id_lower>/`.
3. If `PLUGIN_AUTO_RELOAD` or development mode is enabled, Python source changes
in an installed local plugin can auto-sync and reload. If it is not enabled,
call `reload_plugin(plugin_id="<PluginID>")` after editing runtime files.
4. When `requirements.txt` changes, reinstall with `force=True`; reloading alone
does not install new dependencies.
## Validation
- Re-read the changed files and confirm class name, directory name, package ID,
and package version are consistent.
- Confirm every public class, public method, and public function has a Chinese
docstring.
- Confirm every newly written function or method has a Chinese docstring, even
when it is private helper code.
- For Vue federation plugins, confirm `get_render_mode()` returns
`("vue", "dist/assets")` or the actual built asset path, and that
`dist/assets/remoteEntry.js` exists.
- For sidebar plugins, confirm the plugin is enabled, `get_state()` returns
`True`, `get_sidebar_nav()` returns valid items, and matching `AppPage`
exposes exist for all non-main `nav_key` values or a generic `AppPage` handles
them.
- Confirm frontend-facing API routes use `auth: "bear"` and browser code calls
them through the provided `api` prop.
- Keep external HTTP calls behind MoviePilot utilities and avoid real network
calls in tests.
- If the plugin has non-trivial logic, add or update pytest-native tests. Plugin
repositories can use `app.testing.bootstrap.prepare_v2_backend()` to prepare a
temporary MoviePilot backend and inject `<repo>/plugins.v2` into `sys.path`.
- Run the narrowest allowed validation for the touched area. In this repository,
follow `docs/rules/03-commands.md`; for plugin-only repositories, follow their
own documented validation commands.
- For plugin repository Python changes, use the host Python environment when
possible and run at least syntax compilation for touched plugin files.
- For Vue federation changes, run the frontend project's documented typecheck
and build commands when available, then verify the built assets were copied to
the plugin directory.
## Vue Federation Troubleshooting
- `GET /api/v1/plugin/remotes?token=moviepilot` should include the plugin with a
URL ending in `/plugin/file/<plugin_id_lower>/<dist_path>/remoteEntry.js`.
- `GET /api/v1/plugin/sidebar_nav` should include sidebar entries for enabled
Vue plugins with valid `nav_key`, `section`, and `permission`.
- If the console says `Module name 'vue' does not resolve to a valid URL`, check
the federation `shared` config and use `requiredVersion: false`.
- If the console says top-level await is unavailable, set `build.target` to
`esnext`.
- If dynamic import fails, check the remote file request status, the computed
`remoteEntry.js` path, and whether the installed runtime plugin directory
actually contains the built assets.
- If a sidebar page is blank, check the expose name resolution for the current
`nav_key` and fallbacks (`AppPage{PascalCase}` -> `AppPage` -> `Page`).
## Final Report
Report:
- Plugin ID, source path, and runtime path if installed.
- Package file changed (`package.v2.json` or `package.json`).
- UI mode used (`vuetify` JSON or `vue` federation), and for Vue plugins the
exposed components and built asset path.
- Whether the plugin was installed or reloaded.
- Validation commands run, or why validation was not run.