11 KiB
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 拓扑
管理员浏览器
│ 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 的最小集合:
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. 插件生命周期
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;若插件由控制面托管进程,再额外包含清单声明的服务文件:
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 只能调用这些路径:
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:
{
"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、日志或下载文件传递凭据。
- 不承诺独立进程是操作系统级沙箱。