234 lines
12 KiB
Markdown
234 lines
12 KiB
Markdown
# Sub2API 插件开发教程
|
||
|
||
本文面向希望为 Sub2API 开发、打包和发布插件的团队。插件是独立进程和静态 UI 组成的 `.s2plugin` 包,宿主通过稳定的 gRPC 协议调用它。本文以当前宿主已经定义的 `openai.oauth.outbound_transport.v1` 能力作为协议示例,说明开发者需要准备什么、哪些职责属于插件、哪些职责仍由 Sub2API 负责。
|
||
|
||
本文不是一个可直接安装的完整插件,也不代表 Sub2API 已经发布对应的官方插件包。当前文档主要描述公开协议、宿主边界和开发流程。后续是否发布可安装包、支持哪些 Provider,以及如何提供示例仓库,都需要另行公告。
|
||
|
||
## 1. 准备开发环境
|
||
|
||
建议使用以下环境:
|
||
|
||
- Go 1.21 或更高版本;
|
||
- Node.js(仅在插件 UI 使用 JavaScript 时需要);
|
||
- Git;
|
||
- 与目标部署环境一致的构建工具链。
|
||
|
||
协议定义和通用说明位于:
|
||
|
||
- `backend/pkg/pluginapi/v1/plugin.proto`:进程间消息和流式请求定义;
|
||
- `backend/pkg/pluginapi/v1/runtime.go`:插件进程启动入口;
|
||
- `backend/pkg/pluginapi/v1/manifest.schema.json`:包清单 JSON Schema;
|
||
- `backend/pkg/pluginapi/docs/`:开发、UI Bridge、包格式和安全边界说明。
|
||
|
||
目前暂未提供可直接复制的官方示例源码。开发者可以按照本文的目录和协议说明创建自己的插件工程;示例仓库发布后,会在本文补充正式的获取地址、目录说明和版本要求。公开协议始终以 `backend/pkg/pluginapi/` 为准。
|
||
|
||
## 2. 创建插件工程
|
||
|
||
在示例仓库发布前,可以先创建一个独立的 Go 工程,目录建议如下:
|
||
|
||
```text
|
||
my-plugin/
|
||
├── cmd/<plugin>/main.go
|
||
├── internal/pluginconfig/
|
||
├── internal/transport/
|
||
├── ui/index.html
|
||
├── ui/assets/
|
||
├── tools/
|
||
├── manifest.source.json
|
||
└── build.sh
|
||
```
|
||
|
||
开发时至少准备以下部分:
|
||
|
||
1. `manifest.source.json`:插件 ID、名称、版本、作者、能力和兼容的 Sub2API 版本;
|
||
2. `cmd/<plugin>/main.go`:启动入口和运行时版本注入,并同步打包器中的构建目标和二进制名称;
|
||
3. `internal/pluginconfig/`:配置结构、默认值、严格校验和规范化;
|
||
4. `internal/transport/`:HTTP 客户端、代理、请求头、请求体、网络连接参数、响应流和资源回收;
|
||
5. `ui/index.html` 与 `ui/assets/`:插件自己的配置界面;
|
||
6. 单元测试、进程集成测试和目标平台构建配置。
|
||
|
||
入口文件应保持很小,只负责调用 `pluginv1.Serve`。实际逻辑放在可独立测试的包中,避免把配置解析、网络请求和协议组装全部写在 `main.go`。
|
||
|
||
## 3. 编写运行时
|
||
|
||
运行时实现 `TransportPlugin` 服务,必须满足以下约定:
|
||
|
||
| 方法 | 要求 |
|
||
| --- | --- |
|
||
| `GetInfo` | 返回的插件 ID、版本、协议版本、传输 API 版本和能力必须与清单一致。 |
|
||
| `Health` | 快速返回进程是否可以接收新请求,不执行长时间网络探测。 |
|
||
| `ValidateConfig` | 严格解析 JSON,拒绝未知字段和非法范围,并返回完整的规范化配置。 |
|
||
| `ApplyConfig` | 成功后原子切换配置;失败时保留旧配置和旧连接。 |
|
||
| `TestConfig` | 针对已保存配置进行快速诊断,返回简短、可展示的结果。 |
|
||
| `Forward` | 按协议接收请求流,发出上游请求,再按顺序返回响应流。 |
|
||
|
||
请求帧顺序为 `start`、零到多个 `body_chunk`、`body_end`;响应帧顺序为 `start`、零到多个 `body_chunk`、`end`。不能继续处理时发送 `error` 帧。
|
||
|
||
`ForwardResponseError.request_sent` 必须准确:只有在能够确认尚未调用上游 HTTP Transport 时才返回 `false`;一旦已经调用,或无法确认上游是否收到请求,就返回 `true`。宿主会据此决定是否允许切换账号重试,避免重复执行同一个请求。
|
||
|
||
资源管理也属于运行时契约:复用 HTTP Transport 和连接池,配置切换时关闭旧空闲连接,沿用 gRPC stream 的 context 取消 DNS、连接、上传和响应读取,并始终关闭上游响应体。日志和错误消息不能包含 Token、代理凭据、完整请求体或敏感响应头。
|
||
|
||
## 4. 设计插件配置
|
||
|
||
插件配置由插件定义,由 Sub2API 加密保存。推荐流程是:
|
||
|
||
1. 在 `internal/pluginconfig.Config` 中定义字段和默认值;
|
||
2. 使用 `json.Decoder.DisallowUnknownFields` 等严格方式解析;
|
||
3. 将空对象规范化为完整默认配置;
|
||
4. 在 `ValidateConfig` 和 `ApplyConfig` 中复用同一套校验;
|
||
5. 配置应用成功后再让宿主保存,保存失败时允许恢复旧配置。
|
||
|
||
JSON 字段统一使用 `snake_case`。敏感配置不要放入 URL、UI 通知、诊断结果或日志。插件不应从 UI 读取、刷新或持久化 OAuth Token;宿主只在运行时调用需要的网络转发接口。
|
||
|
||
## 5. 实现插件自己的配置 UI
|
||
|
||
UI 是插件包内的静态页面,不需要修改 Sub2API 前端源码。宿主会在受限 iframe 中加载 `ui/index.html`,并通过 UI Bridge 提供配置读写和测试能力。
|
||
|
||
页面初始化流程:
|
||
|
||
1. 加载包内 HTML、CSS 和 JavaScript;
|
||
2. 创建 Bridge 并注册 `message` 监听;
|
||
3. 发送 `sub2api.plugin.ready`;
|
||
4. 调用 `config.load` 渲染表单;
|
||
5. 编辑后调用 `config.save`;
|
||
6. 测试前先保存,再调用 `config.test`;
|
||
7. 页面卸载时调用 `dispose()`。
|
||
|
||
当前 Bridge 支持:
|
||
|
||
| 消息 | 用途 |
|
||
| --- | --- |
|
||
| `config.load` | 读取当前配置。 |
|
||
| `config.save` | 提交配置,由运行时校验、应用并加密保存。 |
|
||
| `config.test` | 运行已保存配置的诊断。 |
|
||
| `ui.resize` | 调整配置 iframe 高度。 |
|
||
| `ui.notify` | 显示成功、错误或提示消息。 |
|
||
|
||
每条消息都必须带 `request_id`,并校验 `event.source`、消息来源标识和 Bridge Token。不要依赖 CDN、远程脚本、Cookie 或本地存储。页面需要兼容窄屏和明暗主题,并正确处理加载、保存、测试、超时和未保存状态。
|
||
|
||
详细信封格式见 `backend/pkg/pluginapi/docs/ui-bridge.md`。如果后续示例仓库提供可复用的 Bridge SDK,本文会在示例仓库章节补充对应路径和使用方式。
|
||
|
||
## 6. 编写包清单
|
||
|
||
只维护 `manifest.source.json`,不要手工编辑构建目录中的 `manifest.json`。至少需要填写:
|
||
|
||
```json
|
||
{
|
||
"schema_version": 1,
|
||
"id": "example.openai.transport",
|
||
"name": "Example OpenAI Transport",
|
||
"version": "0.1.0",
|
||
"requires": {
|
||
"sub2api": ">=0.1.179 <0.2.0",
|
||
"recommended_sub2api_version": "0.1.179",
|
||
"tested_sub2api_versions": ["0.1.179"],
|
||
"plugin_protocol": 1,
|
||
"transport_api": 1,
|
||
"ui_bridge": 1
|
||
},
|
||
"capabilities": [
|
||
{
|
||
"id": "openai.oauth.outbound_transport.v1",
|
||
"platform": "openai",
|
||
"account_type": "oauth"
|
||
}
|
||
],
|
||
"runtimes": {},
|
||
"ui": { "entrypoint": "ui/index.html" },
|
||
"files": {}
|
||
}
|
||
```
|
||
|
||
打包器会自动填充目标平台运行时、UI 和运行时文件的 SHA-256。清单中的 `requires.sub2api` 是硬兼容范围;`tested_sub2api_versions` 应只填写真实验证过的版本;`recommended_sub2api_version` 用于管理页面展示。当前宿主仅处理 `openai.oauth.outbound_transport.v1`,声明其他能力不会自动产生新路由。后续增加 Provider 支持时,会在协议、能力清单和宿主路由完成适配后,再补充对应的清单示例。
|
||
|
||
## 7. 生成密钥并签名
|
||
|
||
生产包应始终签名,宿主默认拒绝未签名包。可以使用插件工程中的密钥生成工具生成一对 Ed25519 密钥;示例仓库发布后会提供标准工具和完整命令:
|
||
|
||
```bash
|
||
go run ./tools/keygen -out build/keys/my-publisher
|
||
```
|
||
|
||
生成的 `my-publisher.private` 只保存在受控的开发机或 CI Secret 中,不能提交到源码仓库、插件包或部署服务器。公钥是 Base64 文本,可以提供给部署者。
|
||
|
||
插件工程的 `build.sh` 应调用标准打包器。自定义发布者密钥时必须同时提供 `-signing-key` 和 `-key-id`:
|
||
|
||
```bash
|
||
./build.sh \
|
||
-signing-key /安全目录/my-publisher.private \
|
||
-key-id my-publisher-v1 \
|
||
-output dist/my-openai-plugin.s2plugin
|
||
```
|
||
|
||
签名覆盖最终 `manifest.json` 的精确字节;清单中的文件哈希再覆盖运行时和 UI 文件。签名完成后不要重新格式化 `manifest.json`。
|
||
|
||
部署者在 Sub2API 配置文件中追加公钥:
|
||
|
||
```yaml
|
||
plugins:
|
||
allow_unsigned: false
|
||
trusted_publishers:
|
||
my-publisher-v1: "BASE64_ED25519_PUBLIC_KEY"
|
||
```
|
||
|
||
`trusted_publishers` 是在宿主内置官方公钥之外追加的信任来源,不能覆盖内置公钥。`signature.json` 中的 `key_id` 必须与配置键完全一致。密钥轮换时先发布包含新公钥的宿主配置或版本,再发布新签名包,最后再停用旧密钥。
|
||
|
||
开发阶段如需使用未签名包,只应在隔离的本地环境临时设置 `plugins.allow_unsigned: true`,测试完成后立即恢复为 `false`。
|
||
|
||
## 8. 构建、测试和安装
|
||
|
||
在插件目录执行:
|
||
|
||
```bash
|
||
go test ./... -count=1
|
||
node --check ui/assets/bridge-v1.js
|
||
node --check ui/assets/app.js
|
||
./build.sh
|
||
unzip -t dist/*.s2plugin
|
||
```
|
||
|
||
回到 Sub2API 仓库根目录后,再使用真实构建包运行宿主集成测试:
|
||
|
||
```bash
|
||
cd ../..
|
||
SUB2API_TEST_PLUGIN_PACKAGE=plugins/my-openai-plugin/dist/my-openai-plugin.s2plugin \
|
||
go test ./backend/internal/service -run '^TestPluginRuntimeIntegration$' -count=1
|
||
```
|
||
|
||
最低测试集应覆盖配置默认值和边界值、插件身份、请求和响应分块、流式响应、上下文取消、插件退出、代理开关、包哈希、签名、路径安全、目标平台运行时以及 UI Bridge 的加载、保存、测试、错误和超时。
|
||
|
||
安装后先保持停用,确认清单兼容性、签名和诊断结果,再按账号灰度启用。API Key 账号和未命中灰度的 OAuth 账号继续走 Sub2API 原有路径。
|
||
|
||
## 9. 发布前检查清单
|
||
|
||
- 插件版本与 `GetInfo` 返回值一致;
|
||
- `requires.sub2api` 覆盖范围经过验证,没有未经测试的破坏性版本;
|
||
- `tested_sub2api_versions` 与实际测试记录一致;
|
||
- 每个支持的平台和架构都有运行时文件;
|
||
- 生产包存在有效 `signature.json`,公钥已交付部署者;
|
||
- 包中没有私钥、源映射、测试数据、日志和临时文件;
|
||
- UI 不依赖外部资源,也不保存宿主会话信息;
|
||
- 配置切换、请求取消、响应关闭和错误重试语义经过测试;
|
||
- 发布说明包含升级、停用、回滚和兼容版本信息。
|
||
|
||
## 10. 常见问题
|
||
|
||
| 现象 | 排查方向 |
|
||
| --- | --- |
|
||
| 安装提示签名不受信任 | 检查 `signature.json.key_id`、Base64 公钥和配置键是否完全一致。 |
|
||
| 插件显示不兼容 | 检查 `requires.sub2api`、`plugin_protocol`、`transport_api` 和 `ui_bridge`。 |
|
||
| 插件进程无法启动 | 检查目标系统和架构对应的运行时路径、可执行权限和运行用户权限。 |
|
||
| 配置页无法加载 | 检查 `ui.entrypoint`、UI 文件哈希、Bridge Token 校验和 iframe 消息来源。 |
|
||
| 保存后配置未生效 | 查看 `ValidateConfig`、`ApplyConfig` 返回的规范化配置和诊断信息。 |
|
||
| 请求失败后重复执行 | 检查 `ForwardResponseError.request_sent` 是否准确反映请求是否可能已发出。 |
|
||
|
||
## 11. 需要扩展能力时
|
||
|
||
如果新插件需要支持其他 Provider、其他账号类型或新的消息字段,应先扩展并版本化公开协议,再由宿主增加能力匹配和生命周期处理。不要仅通过清单声明一个宿主尚未实现的能力。这样可以让旧插件继续运行,也能让新宿主明确拒绝不兼容的插件。
|
||
|
||
Sub2API 后续会持续补充更多 Provider 的插件适配说明,包括能力标识、请求和响应契约、配置字段、UI Bridge 使用方式、版本兼容要求以及测试清单。本文会随着这些能力的落地继续更新,Provider 专属章节会放在本节之后。
|
||
|
||
## 12. 示例仓库预留
|
||
|
||
后续计划提供独立的插件示例仓库,用于存放可复用的运行时骨架、UI 组件、打包工具和各 Provider 的最小实现。目前示例仓库尚未准备完成,因此暂不提供地址;正式发布后会在这里补充仓库地址、适用的 Sub2API 版本、示例插件版本和构建说明。
|