189 lines
8.4 KiB
Markdown
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.
|