Files
sub2api-add/docs/BUSINESS_PLUGIN_FRAMEWORK_V1.md
T
Qiufeng 5feae3ad41
Business Plugins CI / check (plugin-admin) (push) Successful in 3m13s
Business Plugins CI / check (subscription-admin) (push) Successful in 1m41s
chore: initialize standalone business plugin repository
2026-08-27 23:36:08 +08:00

213 lines
11 KiB
Markdown
Raw 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 解耦的通用业务插件框架。业务插件是独立服务、独立端口、独立版本和独立 UI;它可以通过现有管理员自定义菜单嵌入 Core,也可以在新窗口运行。订阅管理只是一个可选业务插件,不能成为框架后台、Core 热路径或插件生命周期的固定组成部分。
## 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 的控制面只允许 Core `role=admin` 登录。插件不创建第二套 Core 用户表;插件会话只保存 `plugin_id`、`admin_user_id`、角色、会话版本和过期时间。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}
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
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 -> installed -> disabled -> starting -> healthy
│ │
│ └── error
└── incompatible
healthy -> draining -> disabled
healthy -> upgrading -> healthy
healthy -> rollback_pending -> healthy
```
- `discovered`:目录或清单被发现,尚未验签。
- `verified`:清单、签名、哈希和兼容性通过。
- `installed`:版本包已安全写入 staging 并原子切换。
- `disabled`:已安装但不接收业务请求,菜单默认隐藏。
- `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。包来源默认是管理员上传或受控本地目录,V1 不自动从互联网下载任意包。
## 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. 会话和嵌入
插件登录调用 Core 现有 `/auth/login`、按需 `/auth/login/2fa`,再调用 `/auth/me` 校验管理员角色。Core token 只存插件服务端会话,浏览器只持有 HttpOnly、Secure、SameSite Cookie 和插件 CSRF token。
Core 的自定义页面当前使用 sandbox iframe,且不会自动继承 Core `localStorage` 登录态。因此 V1 必须同时提供新窗口入口;iframe 首屏显示插件登录页是已知行为。真正无感 SSO 需要 V1.1 的一次性 code/state 或受控 `postMessage` 交接,不得把 JWT 放在 URL。
菜单注入使用 Core 现有 `custom_menu_items`:
```json
{
"id": "DOMAIN_PLUGIN_ID",
"label": "DOMAIN_PLUGIN_LABEL",
"url": "https://CORE_ORIGIN/extensions/DOMAIN_PLUGIN_ID/",
"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、日志或下载文件传递凭据。
- 不承诺独立进程是操作系统级沙箱。