Files
sub2api-add/docs/BUSINESS_PLUGIN_FRAMEWORK_V1.md
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

225 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Sub2API 独立业务插件框架 V1
状态:Accepted Contract / V1 参考实现已完成本地验收
本文定义与 Sub2API Core 解耦的通用业务插件框架。业务插件的后端服务、端口和版本独立;浏览器端由统一的 Plugin Admin 控制面承载,业务插件 UI 以模块方式挂载到同一个管理员 Shell 中。订阅管理只是一个可选业务模块,不能成为框架后台、Core 热路径或插件生命周期的固定组成部分。
> **前端架构修订(2026-08-30)**:此前“独立 UI/独立管理员后台”的表述仅指后端服务可以独立部署,不表示业务模块要再次登录。Plugin Admin 负责唯一的管理员登录、会话、导航和 CSRF;订阅模块安装并启用后才出现在控制面导航中,继承同一会话,不提供第二个登录页或第二套 Cookie。
## 1. 目标
V1 需要提供一个独立的插件控制面,负责:
- 展示已登记、已安装和可升级的业务插件;
- 校验包清单、发布者签名、文件哈希和 Core 兼容范围;
- 安装、启用、停用、健康检查、升级、回滚、卸载和配置插件;
- 保存插件版本、服务地址、运行状态、菜单声明和操作审计;
- 通过 Core 现有管理员鉴权复用操作者身份;
- 将已启用插件的管理员菜单注入 `custom_menu_items`;
- 让每个业务插件仅通过控制面或其受控模块 BFF 调用 Core 明确允许的 API。
V1 不改变 Core Go/Vue、数据库迁移、现有鉴权、前端路由或 `.s2plugin` transport ABI。控制面自己的注册表、安装目录、进程和配置存储属于独立服务;它不连接 Core PostgreSQL、Redis 或宿主业务表。
## 2. 与现有插件的关系
| 类型 | 现有 `.s2plugin` transport | Business Plugin V1 |
|---|---|---|
| 运行方式 | Core 子进程 + gRPC | 独立服务 + HTTP/BFF |
| 能力 | `openai.oauth.outbound_transport.v1` | 由清单声明的业务能力 |
| 生命周期 | Core `PluginManager` | 独立 Plugin Control Plane |
| UI | 配置 iframe + UI Bridge | 统一控制面中的业务模块页面 |
| 数据边界 | Core 负责账号转发与计费 | Core 负责权威业务数据,插件只读/投影 |
| 菜单 | Core 固定插件管理页 | `custom_menu_items` 管理员入口 |
现有 `.s2plugin` 的 `TransportPlugin`、清单 schema 和 capability 校验保持不变。业务插件不能仅通过声明一个新 capability 假装获得 HTTP 路由、数据库、支付或订阅权限。
## 3. V1 拓扑
```text
管理员浏览器
│ Core 登录后的管理员菜单
▼
Core /custom/<menu-id>
│ sandbox iframe 或新窗口
▼
反向代理 /extensions/<plugin-id>/
▼
Business Plugin Control Plane
├─ 插件目录、签名、版本和状态
├─ 独立进程 supervisor
├─ 管理员会话和审计
└─ 插件 BFF / Core API Adapter
│ 仅发送服务端 Bearer Core JWT + X-Request-ID
▼
Sub2API Core 现有鉴权、Admin API 和领域账本
```
控制面可以托管插件进程,也可以把进程交给 systemd、容器或 Kubernetes;无论采用哪种 supervisor,控制面都必须能读取健康状态并保留旧版本回滚点。停用时先撤销自有菜单,再将托管进程置于有界终止流程(SIGTERM,最多等待 10 秒后强制结束);反向代理负责停止新请求,外部服务只做健康探测并由其部署者负责停机。
## 4. 角色和权限
- `plugin_admin`:安装、启停、升级、回滚、卸载、配置和菜单注入。
- `plugin_operator`:查看状态、日志摘要和健康诊断,不改变包或凭据。
- `plugin_readonly`:只读查看已登记插件。
V1 的 Plugin Admin 控制面只允许 Core `role=admin` 登录。插件不创建第二套 Core 用户表;控制面会话保存 `admin_user_id`、角色、会话版本和过期时间,订阅等业务模块直接继承该会话。模块不得创建自己的登录页、Cookie 或独立权限入口。UI 隐藏按钮不等于授权,控制面、模块后端和 Core API 均须重新校验权限。
## 5. 管理面契约
控制面内部 API 的最小集合:
```text
GET /healthz
POST /login
POST /login/2fa
POST /logout
GET /api/me
GET /api/plugins
GET /api/plugins/{id}
GET /api/marketplace
POST /api/marketplace/install
POST /api/plugins/install
POST /api/plugins/{id}/install
POST /api/plugins/{id}/enable
POST /api/plugins/{id}/disable
POST /api/plugins/{id}/upgrade
POST /api/plugins/{id}/rollback
POST /api/plugins/{id}/uninstall
DELETE /api/plugins/{id}
GET /api/plugins/{id}/config
PUT /api/plugins/{id}/config
GET /api/audit
POST /api/menu-items/preview
POST /api/menu-items/apply
```
所有写请求要求 CSRF、操作者会话和幂等键。幂等指纹由服务端根据方法、路径、查询和请求体计算,失败操作也保留终态,重复请求不会重新执行。安装、启用、升级、回滚和卸载必须返回 operation ID,并可通过插件详情查询最终状态。控制面不把上述路径注册到 Core,也不声称 Core 已存在 `/api/v1/plugin-host/*`。
## 6. 插件生命周期
```text
discovered -> verified -> staged/disabled -> starting -> healthy
│ │
│ └── error
└── incompatible
healthy -> draining -> disabled
healthy -> upgrading -> healthy
healthy -> rollback_pending -> healthy
```
- `discovered`:目录或清单被发现,尚未验签。
- `verified`:清单、签名、哈希和兼容性通过。
- `staged/disabled`:版本包已安全写入 staging 并原子切换,但仍是“已入库、待启用”;不接收业务请求,菜单默认隐藏。
- `starting`:进程启动、端口和健康检查进行中。
- `healthy`:健康端点、就绪端点和(若响应提供)版本检查通过。
- `draining`:菜单已撤销,托管进程正在执行有界终止;在途请求由插件进程或反向代理按部署约定处理。
- `upgrading` / `rollback_pending`:新旧版本并存检查,只有健康版本成为 active。
- `error`:进程、健康、配置或 Core 合约失败,默认 fail-closed。
- `incompatible`:当前 Core baseline 或协议不满足清单要求。
已启用版本不能被原地覆盖。升级先安装新 revision、执行健康和契约检查,再原子更新 active revision;至少保留一个可回滚 revision。卸载只能作用于停用插件,不删除 Core 数据。控制面重启时重新校验 `healthy`/`enabled` 插件:托管 command 重新启动 active revision,外部服务重新探测 `service_url`;探测失败统一标记 `error` 并清空不可用端点。`backend.command` 与外部 `service_url` 是互斥运行模式,升级不会继承不属于新模式的旧端点。
## 7. 安装和包安全
安装包必须包含清单和 UI;若插件由控制面托管进程,再额外包含清单声明的服务文件:
```text
manifest.json
signature.json
ui/index.html
ui/assets/...
```
外部服务模式允许省略 `service/` 文件,但清单不得声明
`backend.command`,且启用前必须在控制面配置经过校验的 `service_url`。托管进程模式
必须声明 `backend.command`,并将该路径及二进制哈希放入包内。
控制面必须拒绝绝对路径、父目录跳转、重复条目、符号链接、未声明文件、超大文件和不匹配哈希。签名使用 Ed25519,签名覆盖 `manifest.json` 原始字节;清单中的 SHA-256 覆盖服务文件和 UI。发布者私钥不进入仓库、包、服务器或日志。
安装使用临时目录和原子 rename;失败不得破坏 active revision。包来源默认是管理员上传或受控本地目录。插件市场只允许服务端读取 schema v1 索引,并对 HTTPS 主机做精确 allowlist;浏览器只能提交索引中的 plugin ID/version,不能指定任意下载 URL。市场下载完成后仍只进入 `staged/disabled`,必须由管理员显式启用并通过健康检查。
## 8. Core API Adapter
每个插件清单声明精确的 `method + path` allowlist。控制面或插件 BFF 只能调用这些路径:
```http
POST /api/v1/auth/login
POST /api/v1/auth/login/2fa
POST /api/v1/auth/refresh
POST /api/v1/auth/logout
GET /api/v1/auth/me
GET /api/v1/settings/public
GET /api/v1/admin/<declared-read-endpoint>
```
`<declared-read-endpoint>` 只是文档占位符,实际清单必须列出具体路径、查询参数、分页上限和响应字段。浏览器永远不接触 Core JWT、refresh token 或 Admin Key;所有出站请求由服务端添加 `Authorization: Bearer ...` 和统一 `X-Request-ID`。
Core 返回 `401` 时,一次用户请求最多 refresh 一次;并发 refresh 必须按插件会话串行化。`403` 不重试,`429` 按 `Retry-After` 有上限退避,`5xx` 只重试明确幂等操作。响应按 DTO 或敏感键规则脱敏,不能把 token、密码、Cookie、secret 或完整凭据透传给 UI。
## 9. 会话和嵌入
Plugin Admin 登录调用 Core 现有 `/auth/login`、按需 `/auth/login/2fa`,再调用 `/auth/me` 校验管理员角色。Core token 只存控制面服务端会话,浏览器只持有控制面的 HttpOnly、Secure、SameSite Cookie 和 CSRF token;订阅模块请求沿用这套会话。
会话和进程是两个独立的生命周期:插件作为常驻服务运行,Core access token
到期时由后端按需 refresh,不要求重启插件。默认会话空闲 30 分钟、绝对上限
8 小时;refresh 失败或 Core 撤销管理员后清除会话并要求重新登录。V1 会话
默认只在插件进程内存中保存,所以控制面重启后需要重新登录一次,但已启用
插件会按 registry 恢复。跨实例或跨重启免登录必须接入插件自有加密共享会话
存储,不能把 Core token 放入浏览器或 Core 数据库。
Core 的自定义页面当前使用 sandbox iframe,且不会自动继承 Core `localStorage` 登录态。因此 iframe 首屏由 Plugin Admin 显示一次登录页;进入订阅模块时不再追加登录。真正无感 Core SSO 仍需要 V1.1 的一次性 code/state 或受控 `postMessage` 交接,不得把 JWT 放在 URL。
菜单注入使用 Core 现有 `custom_menu_items`:
```json
{
"id": "DOMAIN_PLUGIN_ID",
"label": "DOMAIN_PLUGIN_LABEL",
"url": "https://PLUGIN_PUBLIC_ORIGIN/extensions/qiu.plugin-admin/admin/#/modules/DOMAIN_MODULE/overview",
"visibility": "admin",
"sort_order": 200
}
```
控制面只能创建和更新自己声明的 ID,保留其他管理员菜单;应用前展示 diff 并记录审计。停用或卸载时先隐藏/移除自己拥有的菜单项,再停止进程。
## 10. 数据归属
Core 始终是用户身份、余额、订阅、订单、用量、权限、计费和审计的权威来源。控制面只保存插件包、revision、状态、服务配置、菜单声明、会话和操作索引。业务插件可以保存可删除、可重建的 projection,但 projection 不得作为 Core 网关放行、扣费或权限判断依据。
## 11. 安全和可运维性
- 监听地址默认 loopback,生产通过 HTTPS 反向代理暴露。
- 插件进程使用独立低权限账号/容器、最小文件权限和出站网络 allowlist。
- 配置 secret 使用服务端加密存储或 secret manager,UI 只显示 configured/rotatable,不回显原值。
- 日志只记录插件 ID、revision、操作者、operation ID、资源 ID、request ID、状态和耗时。
- 进程崩溃、健康失败、Core 不兼容或签名错误均 fail-closed,不静默切换到未验证版本。
- 停用、升级、回滚和卸载必须可重复执行;Core 数据不随插件卸载删除。
## 12. 分阶段实施
### Phase 0:契约冻结
冻结 manifest、签名、revision、控制面 API、状态机、反代路径、菜单字段、会话属性和 Core API allowlist。
### Phase 1:通用控制面
实现管理员登录、插件目录、包校验、安装/启停、健康检查、配置加密、审计、菜单 preview/apply、systemd/container 适配和回滚骨架。
### Phase 2:业务插件适配
提供 `DOMAIN_PLUGIN_ID` 级别的 SDK/模板和契约测试。订阅管理作为首个业务模块接入统一控制面,只实现自身领域页面和 Core 只读 API,不复制控制面的登录、导航和会话。
### Phase 3:生产增强
评审多实例共享状态、短时 Plugin Access Token、无感 SSO、Core Host Adapter、远程 registry、灰度升级和跨节点 drain;这些能力需要单独版本和安全评审。
## 13. 非目标
- 不把业务插件注册为现有 OpenAI OAuth transport。
- 不把任意业务路由、数据库、支付、余额扣款或每请求计费放进控制面。
- 不创建 Core 用户表、插件版账本或绕过 Core 鉴权。
- 不通过 URL、iframe、LocalStorage、HTML、日志或下载文件传递凭据。
- 不承诺独立进程是操作系统级沙箱。