Skip to content

PTPatronus 插件系统设计 ​

本文档记录 PTPatronus 插件能力的目标模型和当前落地方案。整体原则是不照搬 MoviePilot 的 Python 动态导入,而是在 Go 后端中建立可控、可审计、可逐步扩展的插件系统。

目标 ​

  • 第一阶段:内置插件闭环。插件有清晰 manifest、启停状态、配置、事件订阅、动作执行、日志和前端管理页。
  • 第二阶段:外部进程插件。插件独立进程运行,通过 HTTP/gRPC/JSON-RPC 与主程序通信。
  • 第三阶段:插件市场。插件仓库提供索引、压缩包、哈希和签名,主程序负责安装、升级、回滚和安全校验。

当前实现 ​

后端新增 backend/internal/plugins:

  • Manifest:插件元数据,包括 ID、名称、版本、作者、权限、事件、动作和配置 schema。
  • Manager:负责插件注册、启停、配置保存、动作执行、事件总线订阅和日志记录。
  • Runtime:插件运行时能力,当前提供 Config、Log、Notice、Publish。
  • 内置插件:
    • event-audit:监听 plugin.test 和 webhook,写入审计日志。
    • notice-forwarder:将指定事件转成全局通知,并复用系统通知渠道。

数据库新增:

  • plugin_states:插件启停、安装状态、配置 JSON、最近事件时间和最近错误。
  • plugin_logs:插件动作、事件和运行日志。

前端新增:

  • /plugins 插件管理页。
  • 支持筛选、启停、配置、动作执行、测试事件、日志查看。

API ​

所有接口位于 /api/v1,需要登录和 VIP 准入;写操作需要管理员权限。

  • GET /plugins:插件列表。
  • GET /plugins/:id:插件详情。
  • PUT /plugins/:id/config:保存配置,body 为 { "config": {} }。
  • POST /plugins/:id/enable:启用插件。
  • POST /plugins/:id/disable:停用插件。
  • POST /plugins/:id/action:执行动作,body 为 { "action": "name", "input": {} }。
  • GET /plugins/logs?plugin_id=&limit=100:插件日志。
  • POST /plugins/events:发布测试或管理事件,body 为 { "type": "plugin.test", "data": {} }。
  • GET /plugins/market?source=:读取插件市场索引;source 留空时读取 data/plugin-market.json。
  • POST /plugins/market/install:从市场安装插件,body 为 { "source": "...", "id": "...", "accepted_permissions": [...], "accepted_permissions_by_plugin": [{ "id": "...", "accepted_permissions": [...] }] }。

Manifest 结构 ​

json
{
  "id": "notice-forwarder",
  "name": "事件通知桥",
  "version": "1.0.0",
  "author": "PTPatronus",
  "runtime": "builtin",
  "built_in": true,
  "permissions": ["event:read", "notice:write"],
  "events": [{ "type": "plugin.test", "description": "插件系统测试事件" }],
  "actions": [{ "name": "send_test_notice", "label": "发送测试通知" }],
  "config_schema": [
    { "key": "level", "label": "通知级别", "type": "select", "group": "通知" }
  ],
  "schedule": [
    { "id": "daily_update", "label": "每日拉取订阅", "action": "update_hosts", "cron": "0 6 * * *", "cron_config_key": "cron_update" }
  ]
}

config_schema[].group 可选,把字段归入配置表单的分区(订阅/网络/通知/定时 等),留空归入默认无标题组;仅影响 UI 组织。

插件级定时调度 ​

对标 MoviePilot 的 get_service(),让插件能"自调度"——到点自动调用某个 action,而不必手搓工作流 timer 节点。

声明:在 Manifest.Schedule 列出 ScheduleSpec:

  • id:稳定键,参与 cron job 命名 plugin:<pluginID>:<id>。
  • action:到点要调用的 action 名(必须在 Manifest.Actions 里)。
  • cron:默认 5 段 cron 表达式。
  • cron_config_key:可选。非空时从 config[此键] 读 cron 覆盖 cron;配置值为空串则该条调度停用——用户在前端清空 cron 字段即停,无需额外开关。
  • input:可选,传给 action 的静态入参。

执行模型:

  • Manager 自持一个独立 *scheduler.Scheduler(复用 internal/scheduler 包装的 robfig/cron/v3,带 panic 恢复 + SkipIfStillRunning),在 Start 创建并启动,Stop 关停。
  • refreshSchedules(id) 在 Start/Register/SetEnabled/SaveConfig 时重建该插件的全部 job;Uninstall 清理。幂等。
  • 到点回调复用 Manager.ExecuteAction:自动做启用检查、构建 Runtime、30s 超时、日志/错误落库。
  • 仅当插件整体 enabled=true 时才登记 job;插件被禁用则全部摘除。

调度器与全局 site/workflow 调度器相互独立,App.Shutdown 会一并优雅停止。

外部 HTTP 插件 ​

第二阶段已支持 runtime:external-http。主程序会扫描 data/plugins/{plugin_id}/plugin.json,将外部插件注册到同一套插件管理页和 API。

插件包内包含 plugin.json:

json
{
  "id": "example.external",
  "name": "外部插件示例",
  "version": "1.0.0",
  "runtime": "external-http",
  "entry": {
    "command": "example-plugin",
    "args": ["--port", "${PORT}"],
    "health": "/health"
  },
  "permissions": ["event:read", "notice:write"]
}

如果插件服务由用户自行托管,也可以不声明 command,改用:

json
{
  "entry": {
    "base_url": "http://127.0.0.1:19090",
    "health": "/health"
  }
}

主程序负责:

  • 分配端口和临时 token。
  • 启动/停止外部进程。
  • 调用 /event、/action、/health。
  • 对每次调用设置超时和最大 body。
  • 将插件日志统一收敛到 plugin_logs。

外部插件 HTTP 约定:

  • POST /event
  • POST /action
  • GET /health

POST /event 请求:

json
{
  "type": "plugin.test",
  "data": { "message": "hello" },
  "config": {}
}

POST /action 请求:

json
{
  "action": "ping",
  "input": { "message": "hello" },
  "config": {}
}

POST /action 响应:

json
{
  "ok": true,
  "output": { "message": "pong" }
}

主程序启动外部进程时会注入环境变量:

  • PTP_PLUGIN_ID
  • PTP_PLUGIN_PORT
  • PTP_PLUGIN_TOKEN
  • PTP_PLUGIN_BASE_URL
  • PTP_PLUGIN_DIR

entry.command、entry.args、entry.env 支持占位符:

  • ${PORT}
  • ${TOKEN}
  • ${PLUGIN_DIR}
  • ${PLUGIN_ID}

示例插件见 examples/plugins/external-http-demo。复制到 data/plugins/external-http-demo 后,可在插件管理页执行“重载”加载;通过市场安装时会自动热加载。

插件市场 ​

第三阶段已支持静态市场索引和 zip 安装包。索引可以是 HTTP/HTTPS URL,也可以是本地文件。

默认官方仓库:全新安装首次启动时,后端自动播种官方插件市场仓库 gntv456/ptpatronus-plugin-market 为默认市场源(Manager.EnsureDefaultMarketSource,存储于 SystemConfig key plugin.market_sources)。它和第三方源一样是普通可编辑条目——前端「插件 → 插件市场 → 设置仓库」可删除(删除后不会自动恢复)、可追加任意第三方 GitHub 仓库或 plugin-market.json URL,也可用「恢复官方源」按钮重新加回。多源按列表顺序聚合,相同插件 ID 先列者优先。市场源管理接口:GET/PUT /plugins/market-sources(响应额外含 official 字段,即官方仓库地址)。

前端「插件 → 插件市场」中 source 留空时,后端按 plugin.market_sources 聚合;该列表为空时回退读取本地 data/plugin-market.json。

json
{
  "version": 1,
  "name": "PTPatronus 官方插件市场",
  "public_key": "base64-encoded-ed25519-public-key",
  "plugins": [
    {
      "id": "example.external",
      "name": "外部插件示例",
      "version": "1.0.0",
      "runtime": "external-http",
      "archive": "https://example.com/example.external-1.0.0.zip",
      "sha256": "...",
      "signature": "...",
      "category": "工具"
    }
  ]
}

安装流程:

  1. 拉取市场索引。
  2. 校验插件 ID、版本、runtime 和权限声明。
  3. 下载 zip 到临时目录。
  4. 校验 SHA-256。
  5. 如果索引提供 public_key 且条目提供 signature,使用 Ed25519 校验 zip 原始字节签名。
  6. 安全解压,阻止 zip 路径穿越。
  7. 校验 zip 内 plugin.json 的 id 和 runtime。
  8. 解压到 data/plugins/{plugin_id}。
  9. 覆盖安装前备份旧目录,失败自动回滚。
  10. 立即加载外部插件,返回 restart_needed=false。

当前支持:

  • runtime=external-http
  • zip 内必须包含且只能包含一个 plugin.json
  • sha256 为空时跳过哈希校验,生产市场必须填写
  • Ed25519 签名校验:public_key 和 signature 必须成对提供,均为标准 Base64
  • category(可选):市场展示用分类标签(如「媒体库」「短剧」「网盘」「工具」),供前端「插件市场」表格分组/筛选。只存在于市场元数据,不参与签名校验(签名仅盖 archive zip 字节),故调整分类/描述/关键词无需重新打包或重签
  • 外部插件热加载、重载、卸载

暂未启用:

  • 签名信任链/多公钥轮换:当前是单市场索引公钥模型。

安全约束 ​

  • 插件必须声明权限,UI 明示权限。
  • 配置、数据和日志按插件 ID 隔离。
  • 外部插件默认不能直接访问主数据库。
  • 事件和动作调用必须有超时。
  • 插件安装包必须校验哈希;可信市场应同时启用 Ed25519 签名。
  • 插件市场安装前必须展示来源、版本、权限和变更。
  • 插件报错只记录最近错误和日志,不影响主业务链路继续运行。

插件持久化与站点访问(Runtime 扩展) ​

为支持复杂插件(失败队列、调度锚点、多站交互),Runtime 在 Config/Log/Notice/Publish 之外扩展:

  • KVGet/KVSet/KVDelete(key):per-plugin key-value 持久化(plugin_kv 表,唯一索引 plugin_id+key)。失败队列、织梦邮件时间等运行时状态存这里,不污染用户可见的 ConfigJSON。
  • SiteList() []SiteRef / SiteCookie(id):枚举已配置站点 + 解密 cookie(复用 SiteOper 的 AES-GCM 解密管线)。喊话/采集类插件据此遍历站点。
  • SetRuntimeConfig(key, val):合并写回 ConfigJSON 单键 + 触发 refreshSchedules(动态 cron 用,如织梦 zm_cron)。

groupchat 喊话子包 ​

internal/plugins/groupchat/ 是 14 站 PT 喊话自动化引擎(对标 MP groupchatzone 完整移植)。自包含、不 import plugins 包(避免循环;plugins 层用 gcRTAdapter 把 plugins.Runtime 适配成 groupchat.Runtime)。

  • 14 站 handler 注册表(Dispatch 按站名子串匹配,NexusPHP 兜底):好学 / 织梦 / LongPT / 青蛙 / 象站 / 幸运 / 藏宝阁 / 天枢 / Moment / PTLGS / PTS / 13City + NexusPHP 兜底。14/14 纯 HTTP(原版 Playwright 路径是死代码)。
  • 共享 NexusPHP 基类(Send = GET /shoutbox.php?shbox_text=&shout=我喊&sent=yes&type=shoutbox;PollFeedback 重拉解析 @user 行 + 关键词奖励分类),各特化站覆写 Send/PollFeedback。
  • 织梦独立 Schedule:cron 由邮件时间+24h 动态算出,SetRuntimeConfig("zm_cron", cron) 触发重排;主任务首次带上织梦引导初始化。
  • 失败重试队列(KV 持久化 + time.AfterFunc 递归,至上限)。
  • 奖励副作用:青蛙每日福利(/api/bonus-shop/exchange)、LongPT 抽奖、13City 诸神赐福勋章(/ajax.php buyMedal)。

注:无真实样本站点(hxpt/13city/cangbao/ptlgs/dubhe/moment/ptskit/luckpt/vicomo)的 feedback XPath/关键词移植自原版 Python,待真机校准;织梦/LongPT/青蛙/NexusPHP 这 4+1 代理读到了精确端点/字段,可信度高。

forumsignin 论坛签到子包 ​

internal/plugins/forumsignin/ 是论坛/PT 站论坛区每日签到引擎(对标 MP madrays/imaliang 论坛签到插件)。与 groupchat 同构(自包含、不 import plugins 包,fsRTAdapter 适配 Runtime),但凭据由插件配置承载而非 PT 站点列表(NodeSeek/SSDForum 等非 PT 论坛不在站点系统内)。

  • 6 站 handler 注册表(Lookup(key) 精确匹配):NodeSeek(POST /api/attendance/checkIn JSON {success,message})+ 5 个 NexusPHP 系(蜂巢/飞牛/柠檬/国语视界/SSDForum,共享 nexusSignin 基类:GET {base}/{path} 默认 signin.php,HTML 关键词分类 签到成功/今日已签到/cookie 失效)。Supported() 按注册顺序返回 key+label。
  • 配置文本 forums:每行 key|base|cookie[|path](SplitN("|",4),cookie 第 3 列可含除换行外任意字符;path 第 4 列可覆盖默认签到路径,便于逐站校准)。# 开头/空行忽略。
  • Engine:解析凭据 → 逐站 Signin → 收集 SiteResult → 通知(成功/失败计数)→ 失败重试(KV 持久化 + time.AfterFunc 递归,至上限)。
  • Schedule:每日论坛签到(默认 0 8 * * *,cron 可改/留空停用);动作 run(立即签到,返回每站结果)/ labels(支持站点列表)。

注:签到端点/字段名基于社区逆向契约,httptest mock 覆盖,待真机联调(每站 path 可在配置行第 4 列覆盖以适配差异)。

开发新内置插件 ​

  1. 在 backend/internal/plugins 中实现 Plugin 接口。
  2. 提供 Manifest()、DefaultConfig()、HandleEvent()、ExecuteAction()。
  3. 在 NewManager 中注册插件。
  4. 若需要新的系统能力,优先通过 Runtime 增加受控方法,不直接把数据库或完整服务暴露给插件。
  5. 前端无需为每个插件写专属页面;配置和动作由 schema 自动渲染。
  6. 需要定时自治(如每日签到、定时拉取)时,在 Manifest.Schedule 声明调度;若要让用户改 cron,配一个 type:"text" 的 config 字段并在 cron_config_key 引用其 key,留空即停。

迁移自 MoviePilot 的内置插件 ​

位于 backend/internal/plugins/migrated_moviepilot.go,用 Go 重写为"被动 action"模型(自带 SSRF/内网守卫),并按上述机制接上分组配置与定时调度:

  • cloudflare-subscribe:拉取 Cloudflare hosts 订阅。分组 订阅/网络/通知/定时;每日拉取订阅 调度(默认 0 6 * * *,cron_update 可改)。
  • auto-speed:轻量网络测速。分组 测速/网络/通知/定时;定时测速 调度(默认每 6 小时,cron_speedtest 可改)。
  • xiaomi-router:小米路由器状态/端口映射。分组 连接/通知/快捷预设/定时;定时刷新状态 调度(默认每 6 小时,cron_status 可改)。
  • group-chat-zone:14 站 PT 喊话自动化平台(对标 MP groupchatzone 完整移植)。分组 基础/执行/站点/网络;两条 Schedule(主任务 + 织梦邮件时间+24h 独立);失败重试队列(KV 持久化);青蛙每日福利/LongPT抽奖/13City诸神赐福勋章。详见下文「groupchat 子包」。
  • twofa-helper:TOTP 验证码生成。分组 账号/参数(按需生成,无定时)。
  • ptp-forum-signin:论坛每日签到(NodeSeek/蜂巢/飞牛/柠檬/国语视界/SSDForum)。凭据由插件配置承载(forums 文本框每行 key|base|cookie[|path]),不走 PT 站点列表(NodeSeek/SSDForum 等非 PT 论坛)。分组 站点/通用/调度;每日论坛签到 调度(默认 0 8 * * *,cron 可改);失败重试队列(KV 持久化)。详见下文「forumsignin 子包」。

其余 MoviePilot 重型签到插件(勋章墙/朱雀助手等)尚未迁移,属后续独立工作。