feat: add OAuth outbound transport plugin system

This commit is contained in:
shaw
2026-08-24 09:03:37 +08:00
parent d45135d87d
commit 40ea3aebad
98 changed files with 7681 additions and 141 deletions
+67
View File
@@ -0,0 +1,67 @@
# 插件开发指南
## 稳定边界
当前宿主只支持 `openai.oauth.outbound_transport.v1`。插件负责建立实际上游 HTTP/TLS 连接,Sub2API 负责账号选择、OAuth Token 生命周期、下游协议、响应解析、SSE、错误映射、用量统计和计费。
插件不应修改 API Key 路径,也不应自行刷新或持久化 OAuth Token。
## 推荐结构
```text
plugin/
├── cmd/<plugin>/main.go
├── internal/config/
├── internal/transport/
├── ui/index.html
├── ui/assets/
├── tools/packager/
├── manifest.source.json
└── README.md
```
入口只调用 `pluginv1.Serve`。配置解析和传输实现放入独立包,以便不启动子进程就能单元测试。
## 运行时方法
| 方法 | 要求 |
|---|---|
| `GetInfo` | ID、版本、协议和能力必须与清单一致 |
| `Health` | 返回进程是否可以接受新请求,不执行昂贵探测 |
| `ValidateConfig` | 严格解析并返回完整规范化 JSON |
| `ApplyConfig` | 原子应用配置;失败时保留旧配置 |
| `TestConfig` | 验证当前环境和已保存配置,返回简短诊断 |
| `Forward` | 双向流式传输请求与原始 HTTP 响应 |
请求帧顺序:`start`、零到多个 `body_chunk`、`body_end`。响应帧顺序:`start`、零到多个 `body_chunk`、`end`。不能继续处理的错误使用 `error` 帧。
`request_sent` 必须如实表示请求是否可能已经到达上游。值为 `true` 时宿主禁止自动切换账号重放;只有能确认尚未调用上游 Transport 时才能返回 `false`。
## 配置
- JSON 字段统一使用 `snake_case`。
- 拒绝未知字段、非法范围和受保护请求头。
- 默认配置必须完整,空对象应规范化为所有默认字段。
- 保存时由插件先验证和应用,再由宿主加密写入数据库。
- 数据库写入失败时宿主会尝试恢复旧配置,插件必须允许重复应用。
## 资源管理
- 复用 HTTP Transport 和连接池,不要为每个请求创建新连接池。
- 配置切换后关闭旧空闲连接。
- 使用 stream context 取消 DNS、连接、上传和响应读取。
- 始终关闭上游响应体。
- 不在插件内无限缓存按账号区分的客户端。
## 最低测试集
- 配置默认值、未知字段、边界值和深复制。
- 插件身份及协议版本。
- 请求体分块、无请求体、固定 Content-Length。
- 响应状态、重复请求头、流式响应和响应读取错误。
- 上下文取消、插件退出和超时。
- 代理开启与禁用。
- 包哈希、签名、路径穿越和目标平台运行时。
- UI Bridge 加载、保存、测试、错误和超时。
发布前还应使用真实构建包运行宿主的插件进程集成测试。
@@ -0,0 +1,45 @@
# `.s2plugin` 包格式
`.s2plugin` 是 ZIP 文件,根目录必须包含 `manifest.json`,生产包还必须包含 `signature.json`。
## 标准布局
```text
manifest.json
signature.json
runtimes/<goos>-<goarch>/<binary>
ui/index.html
ui/assets/...
```
所有运行时和 UI 文件必须出现在 `manifest.files`,值为小写十六进制 SHA-256。清单和签名文件自身不写入 `files`。
包不允许绝对路径、父目录跳转、重复路径、符号链接、未声明文件或缺失文件。宿主还限制上传大小、解压后大小和文件数量。
## 清单
字段规范见 [`v1/manifest.schema.json`](../v1/manifest.schema.json)。版本字段含义:
- `version`:插件自身语义化版本。
- `requires.sub2api`:宿主硬兼容范围。
- `recommended_sub2api_version`:建议宿主版本。
- `tested_sub2api_versions`:发布者真实验证过的版本。
- `plugin_protocol`:进程握手协议。
- `transport_api`:请求和响应帧协议。
- `ui_bridge`:配置 UI 消息协议。
## 签名
`signature.json`:
```json
{
"algorithm": "ed25519",
"key_id": "publisher-key-id",
"signature": "BASE64_SIGNATURE"
}
```
签名对象是 `manifest.json` 的精确原始字节。发布者私钥不得进入插件包、源码仓库或 Sub2API 运行环境。部署者只配置 Base64 Ed25519 公钥。
默认生产配置拒绝未签名包。官方 OpenAI Transport 使用宿主内置公钥验签,不需要配置;其他发布者仍需配置 `trusted_publishers`。`allow_unsigned` 只用于开发者自己构建的本地包。
+29
View File
@@ -0,0 +1,29 @@
# 插件安全边界
## 能提供的隔离
- 私有实现以独立二进制交付,宿主公开源码不包含其业务逻辑。
- 进程协议避免 Go 动态链接和共享内存 ABI。
- 包签名和文件哈希防止未授权替换。
- UI 使用短时 URL、独立 Bridge Token 和 sandbox iframe。
- 插件故障时 OAuth 插件路径失败关闭,不静默切回另一种网络行为。
## 不能提供的保证
- 闭源二进制仍可能被逆向分析。
- 子进程不是操作系统沙箱。
- 插件拥有 Sub2API 服务用户可访问的文件、环境变量和网络权限。
- 包签名证明发布者身份,不证明实现无漏洞或符合 Provider 条款。
## 部署要求
- 官方 OpenAI Transport 使用宿主内置公钥;只向 `plugins.trusted_publishers` 添加经过审核的第三方公钥。
- 使用专用低权限系统用户运行 Sub2API。
- 限制该用户的文件权限、出站网络和环境变量。
- 不向插件环境注入无关密钥。
- 对插件升级保留旧包和回滚流程。
- 记录安装、启用、停用、配置和删除操作,但不记录配置明文。
## 敏感数据
插件处理真实 OAuth Authorization 请求头,必须避免将请求头、请求体、代理凭据和上游敏感响应写入日志或诊断消息。UI 配置中不应出现 OAuth Token。
+56
View File
@@ -0,0 +1,56 @@
# UI Bridge v1
## 加载方式
宿主为每次打开配置页创建短时 UI 会话:
```text
/api/v1/plugin-ui/<asset-token>/index.html#bridge_token=<bridge-token>
```
资源 Token 用于读取包内 `ui/` 文件,Bridge Token 只存在于 URL fragment,不会发送到服务器。iframe 使用 `sandbox="allow-scripts"`,不授予 `allow-same-origin`。
UI 只能加载包内、已在清单声明的资源。CSP 禁止外部网络连接、表单提交和外部 frame。
## 消息信封
UI 到宿主:
```json
{
"source": "sub2api-plugin-ui",
"bridge_token": "TOKEN",
"type": "config.load",
"request_id": "UNIQUE_ID"
}
```
宿主到 UI:
```json
{
"source": "sub2api-plugin-host",
"bridge_token": "TOKEN",
"request_id": "UNIQUE_ID",
"ok": true
}
```
## 方法
| `type` | UI 参数 | 成功响应 |
|---|---|---|
| `sub2api.plugin.ready` | 无 | 无响应 |
| `config.load` | 无 | `config` |
| `config.save` | `config` 对象 | 规范化后的 `config` |
| `config.test` | 无 | `result` |
| `ui.resize` | `height` | 无响应 |
| `ui.notify` | `level`、`message` | 无响应 |
`config.test` 在 v1 中测试已保存配置。UI 若要测试当前表单,应先调用 `config.save`。
## 必须执行的校验
UI 接收消息时必须验证 `event.source === parent`、消息来源标识、Bridge Token 和等待中的 `request_id`。每个请求必须有超时和卸载清理。
宿主不会向 iframe 提供管理员 Token。插件 UI 不得尝试访问管理 API、Cookie、父页面 DOM 或浏览器存储中的宿主数据。