mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-07 16:08:02 +08:00
feat: add OAuth outbound transport plugin system
This commit is contained in:
@@ -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` 只用于开发者自己构建的本地包。
|
||||
@@ -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。
|
||||
@@ -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 或浏览器存储中的宿主数据。
|
||||
Reference in New Issue
Block a user