# 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/ │ sandbox iframe 或新窗口 ▼ 反向代理 /extensions// ▼ 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/ ``` `` 只是文档占位符,实际清单必须列出具体路径、查询参数、分页上限和响应字段。浏览器永远不接触 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、日志或下载文件传递凭据。 - 不承诺独立进程是操作系统级沙箱。