Files
sub2api-add/plugins/plugin-admin/README.md
T
Qiufeng ada4ab3c21
Business Plugins CI / check (plugin-admin) (push) Successful in 1m42s
Business Plugins CI / check (subscription-admin) (push) Successful in 1m30s
feat: complete unified plugin admin v1.1.0
2026-08-30 12:10:04 +08:00

189 lines
8.4 KiB
Markdown

# Sub2API Business Plugin Control Plane V1
This directory contains the independent administrator-only control plane for
Business Plugins. It is deliberately separate from Sub2API Core and from the
existing `.s2plugin` OpenAI OAuth transport runtime.
The control plane owns the installed-plugin registry, a constrained marketplace
catalog, signed package verification,
revision directories, lifecycle state, encrypted configuration metadata,
menu preview/apply, and its own audit log. Core remains authoritative for
users, administrator roles, balances, subscriptions, billing, and audit
records. The service never connects to Core PostgreSQL/Redis and never sends a
Core JWT or Admin Key to the browser.
## Run locally
```sh
CORE_BASE_URL=http://127.0.0.1:8080 \
PLUGIN_HOST=127.0.0.1 PLUGIN_PORT=8090 \
PLUGIN_REGISTRY_DIR=./data \
PLUGIN_ENV=development \
PLUGIN_ALLOW_UNSIGNED=true \
PLUGIN_CONFIG_KEY=local-development-secret-at-least-32-chars \
go run .
```
`PLUGIN_ALLOW_UNSIGNED=true` is a development-only switch. Production
packages must contain `signature.json`, use Ed25519, and match a trusted key
from `PLUGIN_TRUSTED_PUBLISHERS` (a JSON object of key ID to base64 public
key). `PLUGIN_CONFIG_KEY` is required in every environment; use a randomly
generated secret in production and keep it stable across restarts so encrypted
plugin configuration remains decryptable.
When the optional subscription module is enabled, set `PLUGIN_PUBLIC_URL` to
the externally reachable Plugin Admin base URL (without `/admin`), for example
`https://CORE_ORIGIN/extensions/qiu.plugin-admin`. Menu apply then always emits
`/admin/#/modules/subscription/overview`; a subscription service URL is never
used as a browser entrypoint.
Open `/admin/` directly or expose the service through the reverse proxy in
`deploy/`. The first login is the existing Core administrator login; no plugin
user table is created. The control plane stores only a short-lived server-side
session and encrypted plugin configuration.
The administrator UI is organized as separate hash-routed pages: overview,
installed plugins, marketplace, and operations. Each plugin has its own detail
route with secondary tabs for health, revisions, configuration, menu
integration, and history. See
[`../../docs/PLUGIN_ADMIN_UI_INFORMATION_ARCHITECTURE.md`](../../docs/PLUGIN_ADMIN_UI_INFORMATION_ARCHITECTURE.md)
for the page responsibilities and responsive layout contract.
### TDesign UI build
The browser shell is a standalone Vue 3 application based on the Tencent
TDesign Vue Next starter. It is built outside the Core repository and then
embedded by this Go service:
```sh
./build-ui.sh
```
The command runs `npm ci`, type-checks the application, bundles TDesign Icons
and ECharts locally (the service CSP does not allow a CDN), and copies the
three runtime files into `ui/`. The generated shell keeps the existing
`/login`, `/api/me`, CSRF and lifecycle contracts; no Core source or frontend
build configuration is involved.
### 会话与令牌生命周期
插件进程是常驻服务,不会因为一次登录、一次请求或一次令牌刷新而重启。
Core 的 access token 过期时,插件后端在当前请求链路中使用对应的 refresh
token 刷新一次,并继续完成请求;刷新后的 token 仍只保存在插件服务端。
浏览器始终只持有插件的 HttpOnly 会话 Cookie 和 CSRF token。
默认会话策略如下:
| 情况 | 行为 |
| --- | --- |
| 持续使用 | 每次请求滑动续期,通常无需重新登录 |
| 空闲超过 30 分钟 | 插件会话失效,下一次访问回到登录页 |
| 会话达到 8 小时 | 绝对过期,需要重新登录 |
| Core access token 过期 | 后端透明 refresh,不重启插件 |
| refresh token 被撤销/失效 | 清除插件会话并要求重新登录 |
| 插件进程重启 | V1 内存会话清空,需要重新登录一次;已登记的插件由控制面按 registry 恢复 |
因此日常使用不需要“用完就重启”。生产多实例若需要跨实例或跨重启保留
会话,应接入插件自己的加密 Redis/会话存储,并使用稳定的密钥;不要把
Core token 写入浏览器、Core 数据库或 URL。
## Control-plane endpoints
```text
GET /healthz
GET /readyz
POST /login POST /login/2fa POST /logout
GET /api/captcha-config
GET /api/marketplace POST /api/marketplace/install (JSON: plugin_id, version)
GET /api/me GET /api/plugins GET /api/plugins/{id}
POST /api/plugins/install (multipart field: package)
POST /api/plugins/{id}/install (multipart field: package)
POST /api/plugins/{id}/upgrade (multipart field: package)
POST /api/plugins/{id}/enable|disable|rollback|uninstall
DELETE /api/plugins/{id} (same delete operation as uninstall)
GET|PUT /api/plugins/{id}/config
POST /api/plugins/{id}/menu-preview|menu-apply
POST /api/menu-items/preview|apply (JSON: {"plugin_id":"..."})
GET /api/audit
GET /api/subscription/status
GET /api/subscription/audit
GET /api/subscription/plans
GET /api/subscription/subscriptions[/{id}]
GET /api/subscription/users/{id}[/subscriptions]
```
Every mutation requires the plugin CSRF token and an `Idempotency-Key`. A
mutation returns an operation ID even when it completes synchronously. A local
upload or marketplace download only verifies and stages the package in the
registry (`disabled` / “已入库,待启用”); it never starts a process. The
administrator must configure the service and click **enable**. Only a
successful health and readiness check changes the plugin to `healthy` and
installs its runtime/menu. Failed installation and upgrade never replace the
active revision. Delete/uninstall is allowed only after disable and removes
plugin files, not Core data.
The login page reads only public CAPTCHA fields from `/api/captcha-config`.
When Core enables Turnstile, Tencent Captcha, or Aliyun Captcha, the matching
challenge is rendered in this page and its one-time proof is forwarded
server-side to Core. Provider secrets are never returned. Keep the provider
origins in the example CSP when CAPTCHA is enabled; changing the Core setting
is picked up on the next login-page load and does not require a plugin restart.
## Marketplace catalog
The marketplace is server-side only. The browser receives metadata and sends a
plugin ID/version; it never receives an archive URL and cannot request an
arbitrary download. Set `PLUGIN_MARKETPLACE_INDEX` to a local JSON file (the
default) or an HTTPS index URL. Remote indexes and archives are restricted to
the exact hosts in `PLUGIN_MARKETPLACE_ALLOWED_HOSTS`; HTTP is accepted only
for loopback sources in `PLUGIN_ENV=development`. Redirects, credentials,
queries, fragments, oversized responses, path escapes, and hash mismatches are
rejected. The package must still pass the normal manifest signature, file hash,
and Core compatibility checks.
Catalog format (schema version 1):
```json
{
"schema_version": 1,
"source": "internal-release-catalog",
"entries": [
{
"plugin_id": "example.plugin",
"name": "Example Plugin",
"version": "1.0.0",
"description": "Administrator extension",
"archive_url": "example.plugin-1.0.0.s2plugin",
"archive_sha256": "SHA256_OF_ARCHIVE",
"archive_size": 12345,
"publisher_key_id": "publisher-key-id",
"core_api_baseline": "sub2api-0.1.183",
"tested_core_versions": ["0.1.183"],
"capabilities": ["example.v1"]
}
]
}
```
An entry is never an implicit upgrade. If the plugin ID is already registered,
use the existing upgrade flow, then enable it explicitly.
## Plugin package
Packages are ZIP files with `manifest.json`, optional detached
`signature.json`, and declared `ui/` files. A package may also include
`service/` files when the control plane owns the plugin process; external
service packages omit `backend.command` and require a configured loopback
`service_url` before enabling. SHA-256 hashes in the manifest cover every
declared file. Absolute paths, traversal, duplicate entries, symlinks,
undeclared hashes, oversized files, unknown manifest fields, and untrusted
publishers are rejected before staging. Installation uses a per-plugin
revision directory and an atomic registry JSON update.
## Deliberate V1 limits
The control plane does not register Core routes, change Core migrations, run
arbitrary proxy URLs, provide transparent iframe SSO, or move billing and
subscription hot-path logic out of Core. A subscription manager remains a
separate business plugin that consumes its own typed Core API adapter.