NX 扩展

编写 NX 扩展

扩展是您自己的程序,运行在 NX 面板旁边:有自己的服务或容器、自己的数据库,只通过网络、并且只以运营者批准的权限与面板通信。本页介绍扩展所需的一切,从第一次运行到在本目录上架。

什么是扩展

面板从不运行扩展的代码。运营者把扩展安装到一台主机上(面板所在的主机或其他主机),由该主机的 systemd 或 Docker 保持运行。然后面板对其进行注册:读取扩展的清单,向运营者展示它请求的内容,批准后为它颁发 API 令牌和签名的事件订阅。

  • 可以用任何语言编写:契约就是 HTTP 和 JSON。在 Go 中,addon-kit 已为您实现契约。
  • 把数据保存在您自己的数据库中。面板不为扩展保存任何数据,扩展自行备份。
  • 只请求完成工作所需的最少权限:运营者在批准前会逐条阅读每项权限及您给出的理由。

从模板开始

addon-template 是一个完整的最小扩展:可以注册、接收事件、响应健康检查并显示一个页面,附带安装脚本、compose 文件和发布工作流。复制并运行:

gh repo create my-addon --template nx-panel/addon-template --public --clone
cd my-addon
NX_OPT_PORT=8090 go run .
# not registered yet — claim code 3F9A1C0B7E21

面板只通过认领码注册由其信任的密钥签名的清单。要在使用 -tags addondev 构建的面板上试用,请用开发密钥签名,只有这样的面板才信任它:

go run github.com/nx-panel/addon-kit/cmd/nx-addon sign -dev nx-addon.json > signed.json
NX_MANIFEST_FILE=signed.json go run .

清单

nx-addon.json 位于两处:您公开仓库的根目录,本目录和面板的安装向导在运行任何东西之前从这里读取;以及运行中的扩展的 /.well-known/nx-addon.json,注册时从这里读取。两者必须是同一个文件。所有字段的参考是 SPEC.md;一个可用的示例:

{
  "api": 1,
  "slug": "hello",
  "name": "Hello",
  "version": "0.1.0",
  "publisher": "Your name",
  "license": "Apache-2.0",
  "description": { "en": "What it does, in a sentence or two." },
  "docs": "https://github.com/you/hello",
  "install": {
    "docker": { "image": "ghcr.io/you/hello", "compose": "deploy/compose.yml" },
    "binary": { "linux-amd64": "hello-linux-amd64.tar.gz", "linux-arm64": "hello-linux-arm64.tar.gz" },
    "options": [
      { "key": "port", "type": "port", "default": 8090, "label": { "en": "Port" } }
    ]
  },
  "scopes": [ { "scope": "users:read", "purpose": "to show how many accounts the panel has" } ],
  "events": ["user.created", "panel.addon_removed"],
  "webhook": "/nx/events",
  "setup": "/nx/setup",
  "health": "/health",
  "ui": "/"
}
  • slug 是扩展的身份:在目录、面板和更新检查中都是如此。永远不要更改;新的 slug 就是新的扩展。
  • version 是不带 v 的发布版本;面板据此显示更新标记。
  • scopes 是 API 令牌的权限,每项都附有在同意页面上显示的理由;events 会投递到 webhook。至少请求其中之一。
  • description 和每个 label 支持 en、fa、ru 和 zh;英文为必填,若提供了运营者的语言,面板会显示该语言。
  • 用面板的方式检查它:nx-addon check nx-addon.json。

安装选项

install.options 是面板安装向导在安装前提出的问题:端口、数据库、机器人令牌等。类型:string、secret、number、port、bool、choice、url、path(插件管理界面所在的基础路径:一个路径段,留空表示根路径;每个清单最多一个)。带有 when 的选项,只有在前面某个 choice 或 bool 取该值时才会询问。

{ "key": "database", "type": "choice", "choices": ["sqlite", "postgres"], "default": "sqlite",
  "label": { "en": "Database" } },
{ "key": "database_dsn", "type": "secret", "required": true, "when": { "database": "postgres" },
  "label": { "en": "PostgreSQL DSN" } }

# reaches the addon as
NX_OPT_DATABASE=postgres
NX_OPT_DATABASE_DSN=postgres://…
NX_PANEL_URL=https://panel.example
NX_CLAIM_CODE=K7QF-2M9D-XW4H-8RTA

每个回答都以环境变量 NX_OPT_<KEY> 传给扩展,同时还有面板地址和面板为本次安装签发的认领码。面板从不保存 secret:它只出现在仅显示一次的命令中,或通过 SSH 传递。

发布与 install.sh

两种安装方式——面板给出的命令,以及面板通过 SSH 自动安装——都会从您最新的 GitHub 发布中运行您自己的 install.sh。模板的脚本已经完成这一切;请保留它的参数:

sh install.sh --method script|docker   # binary + systemd, or the compose stack
           --version v0.1.0               # a release tag (default: the latest)
           --binary-file FILE             # use this archive instead of downloading it
           --opt KEY=VALUE                # an answer to an install option; repeat
           --panel-url URL                # → NX_PANEL_URL
           --claim-code CODE              # → NX_CLAIM_CODE
sh install.sh                             # again: update in place, answers and data kept
sh install.sh --uninstall [--purge]       # remove; --purge deletes the data too

每个发布必须包含的内容,全部位于同一标签:

nx-addon.json             the manifest, its version = the tag
install.sh                    at the root of the repository, at the tag
deploy/compose.yml            what "install.docker.compose" names
hello-linux-amd64.tar.gz      every asset "install.binary" names (release assets)
hello-linux-arm64.tar.gz
SHA256SUMS                    sha256sum output for the assets — the panel checks it
ghcr.io/you/hello:0.1.0       the image, tagged with the version

通过 SSH 以服务方式安装时,面板会自己下载对应主机架构的二进制文件,用 SHA256SUMS 校验,再通过 --binary-file 上传,因此主机无需访问 GitHub。脚本以 root 运行;所有内容都放在 /opt/nx-addons/<slug> 下,回答在 .env,数据在 data/。再次运行脚本必须原地更新。

注册、事件与健康检查

运营者批准后,面板会把凭据连同认领码 POST 到您的 setup 路径。只接受与您启动时所用(或您自行生成并写入日志)的认领码一致的请求,然后丢弃该码。把令牌和 webhook 密钥保存在只有您能读取的文件中。

POST /nx/setup
{
  "api": 1,
  "claimCode": "K7QF-2M9D-XW4H-8RTA",
  "panel": { "url": "https://panel.example", "version": "1.8.0", "id": "…" },
  "token": "nx_…",
  "scopes": ["users:read"],
  "webhook": { "id": 7, "url": "…/nx/events", "events": ["user.created"], "secret": "…" }
}

事件以签名形式到达您的 webhook 路径:

X-Nx-Delivery: 9b1f…            # the same on every retry: handle it once
X-Nx-Signature: t=1791000000,v1=<hex>
# v1 = HMAC-SHA256(secret, "<t>.<body>"); refuse a t more than 5 minutes off

health 路径每分钟被请求一次,正常时必须返回 200;失败会显示在运营者的扩展页面上。用您的令牌调用面板的 /api/v1,并在每次写操作中发送 Idempotency-Key,确保重试绝不会执行两次:

GET https://panel.example/api/v1/users?limit=1
Authorization: Bearer nx_…
Idempotency-Key: <one per logical write>   # on POST / PUT / PATCH / DELETE

运营者移除扩展时,扩展会在令牌被删除前收到 panel.addon_removed:请丢弃凭据,以便之后可以重新注册。在 Go 中,这一切就是 addon.New、Mount、OnEvent 和 Forget。

官方、已验证、非官方

级别由谁上架清单签名在面板中
官方NXNX 的密钥可通过命令或经 SSH 自动安装;同意页面显示 NX
已验证您,经 NX 审核您自己的密钥,由签名索引担保与官方相同,并标注您的名字
非官方您,经过简单检查您的密钥,或不签名卡片和同意页面上有警告;只能通过命令安装

用目录不担保的密钥签名的清单视为未签名;未签名的扩展只能手动注册,由运营者为其创建令牌。

如何上架

  1. 将仓库设为公开,根目录有清单,并至少有一个发布。
  2. 如需已验证上架,请创建自己的签名密钥并为清单签名。签名覆盖包括版本在内的所有字段,因此每次发布都要重新签名:
go run github.com/nx-panel/addon-kit/cmd/nx-addon keygen -out addon.key          # keep addon.key out of the repository
go run github.com/nx-panel/addon-kit/cmd/nx-addon sign -key addon.key nx-addon.json > signed.json && mv signed.json nx-addon.json
go run github.com/nx-panel/addon-kit/cmd/nx-addon check nx-addon.json
go run github.com/nx-panel/addon-kit/cmd/nx-addon verify -pub <your public key> nx-addon.json
  1. 通过提交表单提交仓库;申请已验证审核时请附上您的公钥。表单会立即读取仓库,若有问题会直接指出。
  2. NX 进行审核。上架后,本目录每十分钟重新读取一次最新发布;面板每天读取一次签名索引,或在运营者要求时读取。

如果某个扩展的最新发布不再通过检查,目录会继续显示其最后一个正常版本并标记为过时,直到新的发布再次通过。

更新

发布一个 version 更高的版本:本目录会在十分钟内发现它,各个面板在下一次读取时会在您的扩展上显示更新标记。运营者通过命令更新,或者对于官方和已验证的扩展,直接在面板中更新;安装脚本会再次原地运行。

请求新权限或新事件的更新会被安装,但面板会暂缓新的权限,直到运营者批准;在此期间扩展继续以原有权限工作。请在真正需要某项权限的那个发布中再请求它。