mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-07 14:58:23 +08:00
feat: add OAuth outbound transport plugin system
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
# Sub2API 本地插件协议
|
||||
|
||||
本目录是插件开发者可以依赖的公开契约。`v1/plugin.proto` 和 `v1/runtime.go` 定义进程协议,`v1/manifest.schema.json` 定义包清单,`docs/` 记录开发和发布规范。Provider 私有实现不应放入本目录。
|
||||
|
||||
## 开发文档
|
||||
|
||||
- [开发指南](docs/development.md):从运行时、配置到集成测试的完整流程。
|
||||
- [UI Bridge](docs/ui-bridge.md):沙箱配置 UI 的消息结构和安全要求。
|
||||
- [包格式](docs/package-format.md):清单、文件哈希、签名和版本规则。
|
||||
- [安全边界](docs/security.md):进程权限、敏感数据和故障策略。
|
||||
|
||||
## 实体与运行方式
|
||||
|
||||
插件的交付实体是一个 `.s2plugin` 文件,本质上是带清单、签名、独立可执行文件和静态 UI 的 ZIP 包。管理员在独立的插件管理页手动上传,Sub2API 不从网络自动下载插件,也不要求 Docker。
|
||||
|
||||
启用后,Sub2API 以子进程方式拉起当前操作系统和 CPU 架构对应的二进制,通过本机 gRPC 流传递请求与响应。插件进程退出时会随 Sub2API 清理;停用时先停止接收新请求,再等待正在处理的请求结束。
|
||||
|
||||
多实例部署不要求共享插件目录。宿主会在数据库保存已验签的原始插件包,各实例缺少本地文件时会重新验签和解包,并周期性对齐启用状态、灰度比例和加密配置。所有实例必须连接同一数据库并使用相同的加密密钥。
|
||||
|
||||
独立进程是代码和发布边界,不是操作系统安全沙箱。插件拥有 Sub2API 服务用户所拥有的文件和网络权限,因此只应安装可信发布者的签名包。闭源二进制可提高源码分发门槛,但不能承诺无法反编译。
|
||||
|
||||
## 初期能力边界
|
||||
|
||||
当前只接受 `openai.oauth.outbound_transport.v1`:
|
||||
|
||||
- 仅匹配 `platform=openai` 且 `account_type=oauth` 的上游 HTTP 请求。
|
||||
- API Key 账号、其他 provider、OAuth 登录与 Token 刷新流程不进入插件。
|
||||
- 插件建立真实的上游 HTTP/TLS 连接并返回原始 HTTP 响应。
|
||||
- 命中插件的 OAuth WebSocket 账号会使用 Sub2API 现有 HTTP Bridge,不直接建立上游 WebSocket,避免绕过 v1 HTTP 插件协议。
|
||||
- Sub2API 继续负责响应状态处理、SSE 解析、错误映射、用量统计、计费和下游输出。
|
||||
- 灰度比例以账号 ID 稳定分桶,未命中的 OAuth 账号继续使用原有内置路径。
|
||||
|
||||
## 包结构
|
||||
|
||||
```text
|
||||
manifest.json
|
||||
signature.json # 生产包必需
|
||||
runtimes/linux-amd64/plugin
|
||||
runtimes/linux-arm64/plugin
|
||||
runtimes/windows-amd64/plugin.exe
|
||||
ui/index.html
|
||||
ui/assets/...
|
||||
```
|
||||
|
||||
`manifest.json` 必须声明所有运行时和 UI 文件的 SHA-256。`signature.json` 使用受信任发布者的 Ed25519 私钥对 `manifest.json` 原始字节签名。官方 OpenAI Transport 公钥由宿主内置,第三方发布者公钥由部署者追加到 `plugins.trusted_publishers`。文件哈希由已签名清单保护。
|
||||
|
||||
插件默认保持停用。未签名包默认拒绝安装;`plugins.allow_unsigned` 只应用于开发者自己构建的本地调试包。
|
||||
|
||||
## 兼容性
|
||||
|
||||
清单必须同时声明:
|
||||
|
||||
- `requires.sub2api`:允许的 Sub2API 语义化版本范围。
|
||||
- `requires.recommended_sub2api_version`:建议使用的宿主版本。
|
||||
- `requires.tested_sub2api_versions`:发布者实际验证过的宿主版本。
|
||||
- `plugin_protocol`、`transport_api`、`ui_bridge`:三个独立协议版本。
|
||||
|
||||
宿主版本超出范围时,插件可以安装并查看,但保持“不兼容”状态且不能启用。版本在范围内但未列入已测试版本时,管理员必须再次确认才能启用。
|
||||
|
||||
## UI 隔离与 Bridge
|
||||
|
||||
插件 UI 由包内静态文件实现,宿主使用只有 `allow-scripts` 权限的 sandbox iframe 加载。iframe 没有管理员 Token,也不能直接访问管理 API。宿主为每次打开配置页生成短时资源 URL 和独立 Bridge Token,并且同时校验消息来源窗口与 Token。
|
||||
|
||||
UI 可以发送以下消息:
|
||||
|
||||
- `config.load`
|
||||
- `config.save`
|
||||
- `config.test`
|
||||
- `ui.resize`
|
||||
- `ui.notify`
|
||||
|
||||
每个请求消息带 `request_id`,宿主以 `<type>.result` 返回结果。配置整体使用 Sub2API 的密钥加密后存入数据库;运行中插件会先验证并应用新配置,数据库写入失败时恢复旧配置。
|
||||
|
||||
## 协议源码
|
||||
|
||||
- `v1/plugin.proto`:稳定的进程间消息定义。
|
||||
- `v1/runtime.go`:Go 插件进程启动入口和宿主客户端声明。
|
||||
- `v1/manifest.schema.json`:`manifest.json` 的 JSON Schema。
|
||||
|
||||
插件通过进程协议协作,不使用 Go 动态链接,也不要求插件与 Sub2API 使用相同编译器或共享内存 ABI。
|
||||
@@ -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 或浏览器存储中的宿主数据。
|
||||
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://sub2api.local/schemas/plugin-manifest-v1.json",
|
||||
"title": "Sub2API Plugin Manifest v1",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["schema_version", "id", "name", "version", "requires", "capabilities", "runtimes", "ui", "files"],
|
||||
"properties": {
|
||||
"schema_version": { "const": 1 },
|
||||
"id": { "type": "string", "maxLength": 160, "pattern": "^[a-z0-9]+([._-][a-z0-9]+)+$" },
|
||||
"name": { "type": "string", "minLength": 1, "maxLength": 160 },
|
||||
"version": { "type": "string", "pattern": "^v?(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?(\\+[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?$" },
|
||||
"description": { "type": "string" },
|
||||
"author": { "type": "string", "maxLength": 160 },
|
||||
"requires": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["sub2api", "plugin_protocol", "transport_api", "ui_bridge"],
|
||||
"properties": {
|
||||
"sub2api": { "type": "string", "minLength": 1 },
|
||||
"recommended_sub2api_version": { "type": "string" },
|
||||
"tested_sub2api_versions": { "type": "array", "items": { "type": "string" }, "uniqueItems": true },
|
||||
"plugin_protocol": { "const": 1 },
|
||||
"transport_api": { "const": 1 },
|
||||
"ui_bridge": { "const": 1 }
|
||||
}
|
||||
},
|
||||
"capabilities": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["id", "platform", "account_type"],
|
||||
"properties": {
|
||||
"id": { "const": "openai.oauth.outbound_transport.v1" },
|
||||
"platform": { "const": "openai" },
|
||||
"account_type": { "const": "oauth" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"runtimes": {
|
||||
"type": "object",
|
||||
"minProperties": 1,
|
||||
"patternProperties": {
|
||||
"^[a-z0-9]+-[a-z0-9]+$": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["path"],
|
||||
"properties": { "path": { "$ref": "#/$defs/safePath" } }
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"ui": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["entrypoint"],
|
||||
"properties": { "entrypoint": { "type": "string", "pattern": "^ui/.+" } }
|
||||
},
|
||||
"files": {
|
||||
"type": "object",
|
||||
"minProperties": 2,
|
||||
"propertyNames": { "$ref": "#/$defs/safePath" },
|
||||
"additionalProperties": { "type": "string", "pattern": "^[a-f0-9]{64}$" }
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"safePath": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "^(?!/)(?!.*(?:^|/)\\.\\.(?:/|$))(?!.*\\\\).+$"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package pluginv1
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestManifestSchemaIsValidJSON(t *testing.T) {
|
||||
raw, err := os.ReadFile("manifest.schema.json")
|
||||
if err != nil {
|
||||
t.Fatalf("读取插件清单 Schema 失败: %v", err)
|
||||
}
|
||||
var schema map[string]any
|
||||
if err := json.Unmarshal(raw, &schema); err != nil {
|
||||
t.Fatalf("插件清单 Schema 不是有效 JSON: %v", err)
|
||||
}
|
||||
if schema["$schema"] != "https://json-schema.org/draft/2020-12/schema" {
|
||||
t.Fatalf("插件清单 Schema 版本不符合预期: %v", schema["$schema"])
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,117 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package sub2api.plugin.v1;
|
||||
|
||||
option go_package = "github.com/Wei-Shaw/sub2api/pkg/pluginapi/v1;pluginv1";
|
||||
|
||||
service TransportPlugin {
|
||||
rpc GetInfo(GetInfoRequest) returns (GetInfoResponse);
|
||||
rpc Health(HealthRequest) returns (HealthResponse);
|
||||
rpc ValidateConfig(ValidateConfigRequest) returns (ValidateConfigResponse);
|
||||
rpc ApplyConfig(ApplyConfigRequest) returns (ApplyConfigResponse);
|
||||
rpc TestConfig(TestConfigRequest) returns (TestConfigResponse);
|
||||
rpc Forward(stream ForwardRequest) returns (stream ForwardResponse);
|
||||
}
|
||||
|
||||
message GetInfoRequest {}
|
||||
|
||||
message GetInfoResponse {
|
||||
string plugin_id = 1;
|
||||
string plugin_version = 2;
|
||||
uint32 protocol_version = 3;
|
||||
uint32 transport_api_version = 4;
|
||||
repeated string capabilities = 5;
|
||||
}
|
||||
|
||||
message HealthRequest {}
|
||||
|
||||
message HealthResponse {
|
||||
bool healthy = 1;
|
||||
string message = 2;
|
||||
}
|
||||
|
||||
message ValidateConfigRequest {
|
||||
bytes config_json = 1;
|
||||
}
|
||||
|
||||
message ValidateConfigResponse {
|
||||
bool valid = 1;
|
||||
string message = 2;
|
||||
bytes normalized_config_json = 3;
|
||||
}
|
||||
|
||||
message ApplyConfigRequest {
|
||||
bytes config_json = 1;
|
||||
}
|
||||
|
||||
message ApplyConfigResponse {
|
||||
bool applied = 1;
|
||||
string message = 2;
|
||||
}
|
||||
|
||||
message TestConfigRequest {
|
||||
bytes config_json = 1;
|
||||
}
|
||||
|
||||
message TestConfigResponse {
|
||||
bool success = 1;
|
||||
string message = 2;
|
||||
int64 latency_ms = 3;
|
||||
}
|
||||
|
||||
message HeaderValues {
|
||||
repeated string values = 1;
|
||||
}
|
||||
|
||||
message ForwardRequestStart {
|
||||
string request_id = 1;
|
||||
string method = 2;
|
||||
string url = 3;
|
||||
string host = 4;
|
||||
map<string, HeaderValues> headers = 5;
|
||||
string proxy_url = 6;
|
||||
int64 account_id = 7;
|
||||
int32 account_concurrency = 8;
|
||||
string platform = 9;
|
||||
string account_type = 10;
|
||||
int64 content_length = 11;
|
||||
bool has_body = 12;
|
||||
}
|
||||
|
||||
message ForwardRequest {
|
||||
oneof frame {
|
||||
ForwardRequestStart start = 1;
|
||||
bytes body_chunk = 2;
|
||||
bool body_end = 3;
|
||||
}
|
||||
}
|
||||
|
||||
message ForwardResponseStart {
|
||||
int32 status_code = 1;
|
||||
string status = 2;
|
||||
string protocol = 3;
|
||||
int32 protocol_major = 4;
|
||||
int32 protocol_minor = 5;
|
||||
map<string, HeaderValues> headers = 6;
|
||||
int64 content_length = 7;
|
||||
}
|
||||
|
||||
message ForwardResponseEnd {
|
||||
int64 bytes_received = 1;
|
||||
int64 duration_ms = 2;
|
||||
}
|
||||
|
||||
message ForwardResponseError {
|
||||
string code = 1;
|
||||
string message = 2;
|
||||
bool request_sent = 3;
|
||||
}
|
||||
|
||||
message ForwardResponse {
|
||||
oneof frame {
|
||||
ForwardResponseStart start = 1;
|
||||
bytes body_chunk = 2;
|
||||
ForwardResponseEnd end = 3;
|
||||
ForwardResponseError error = 4;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.6.2
|
||||
// - protoc v3.12.4
|
||||
// source: plugin.proto
|
||||
|
||||
package pluginv1
|
||||
|
||||
import (
|
||||
context "context"
|
||||
grpc "google.golang.org/grpc"
|
||||
codes "google.golang.org/grpc/codes"
|
||||
status "google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the grpc package it is being compiled against.
|
||||
// Requires gRPC-Go v1.64.0 or later.
|
||||
const _ = grpc.SupportPackageIsVersion9
|
||||
|
||||
const (
|
||||
TransportPlugin_GetInfo_FullMethodName = "/sub2api.plugin.v1.TransportPlugin/GetInfo"
|
||||
TransportPlugin_Health_FullMethodName = "/sub2api.plugin.v1.TransportPlugin/Health"
|
||||
TransportPlugin_ValidateConfig_FullMethodName = "/sub2api.plugin.v1.TransportPlugin/ValidateConfig"
|
||||
TransportPlugin_ApplyConfig_FullMethodName = "/sub2api.plugin.v1.TransportPlugin/ApplyConfig"
|
||||
TransportPlugin_TestConfig_FullMethodName = "/sub2api.plugin.v1.TransportPlugin/TestConfig"
|
||||
TransportPlugin_Forward_FullMethodName = "/sub2api.plugin.v1.TransportPlugin/Forward"
|
||||
)
|
||||
|
||||
// TransportPluginClient is the client API for TransportPlugin service.
|
||||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||
type TransportPluginClient interface {
|
||||
GetInfo(ctx context.Context, in *GetInfoRequest, opts ...grpc.CallOption) (*GetInfoResponse, error)
|
||||
Health(ctx context.Context, in *HealthRequest, opts ...grpc.CallOption) (*HealthResponse, error)
|
||||
ValidateConfig(ctx context.Context, in *ValidateConfigRequest, opts ...grpc.CallOption) (*ValidateConfigResponse, error)
|
||||
ApplyConfig(ctx context.Context, in *ApplyConfigRequest, opts ...grpc.CallOption) (*ApplyConfigResponse, error)
|
||||
TestConfig(ctx context.Context, in *TestConfigRequest, opts ...grpc.CallOption) (*TestConfigResponse, error)
|
||||
Forward(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[ForwardRequest, ForwardResponse], error)
|
||||
}
|
||||
|
||||
type transportPluginClient struct {
|
||||
cc grpc.ClientConnInterface
|
||||
}
|
||||
|
||||
func NewTransportPluginClient(cc grpc.ClientConnInterface) TransportPluginClient {
|
||||
return &transportPluginClient{cc}
|
||||
}
|
||||
|
||||
func (c *transportPluginClient) GetInfo(ctx context.Context, in *GetInfoRequest, opts ...grpc.CallOption) (*GetInfoResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(GetInfoResponse)
|
||||
err := c.cc.Invoke(ctx, TransportPlugin_GetInfo_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *transportPluginClient) Health(ctx context.Context, in *HealthRequest, opts ...grpc.CallOption) (*HealthResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(HealthResponse)
|
||||
err := c.cc.Invoke(ctx, TransportPlugin_Health_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *transportPluginClient) ValidateConfig(ctx context.Context, in *ValidateConfigRequest, opts ...grpc.CallOption) (*ValidateConfigResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ValidateConfigResponse)
|
||||
err := c.cc.Invoke(ctx, TransportPlugin_ValidateConfig_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *transportPluginClient) ApplyConfig(ctx context.Context, in *ApplyConfigRequest, opts ...grpc.CallOption) (*ApplyConfigResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ApplyConfigResponse)
|
||||
err := c.cc.Invoke(ctx, TransportPlugin_ApplyConfig_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *transportPluginClient) TestConfig(ctx context.Context, in *TestConfigRequest, opts ...grpc.CallOption) (*TestConfigResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(TestConfigResponse)
|
||||
err := c.cc.Invoke(ctx, TransportPlugin_TestConfig_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *transportPluginClient) Forward(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[ForwardRequest, ForwardResponse], error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
stream, err := c.cc.NewStream(ctx, &TransportPlugin_ServiceDesc.Streams[0], TransportPlugin_Forward_FullMethodName, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
x := &grpc.GenericClientStream[ForwardRequest, ForwardResponse]{ClientStream: stream}
|
||||
return x, nil
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type TransportPlugin_ForwardClient = grpc.BidiStreamingClient[ForwardRequest, ForwardResponse]
|
||||
|
||||
// TransportPluginServer is the server API for TransportPlugin service.
|
||||
// All implementations must embed UnimplementedTransportPluginServer
|
||||
// for forward compatibility.
|
||||
type TransportPluginServer interface {
|
||||
GetInfo(context.Context, *GetInfoRequest) (*GetInfoResponse, error)
|
||||
Health(context.Context, *HealthRequest) (*HealthResponse, error)
|
||||
ValidateConfig(context.Context, *ValidateConfigRequest) (*ValidateConfigResponse, error)
|
||||
ApplyConfig(context.Context, *ApplyConfigRequest) (*ApplyConfigResponse, error)
|
||||
TestConfig(context.Context, *TestConfigRequest) (*TestConfigResponse, error)
|
||||
Forward(grpc.BidiStreamingServer[ForwardRequest, ForwardResponse]) error
|
||||
mustEmbedUnimplementedTransportPluginServer()
|
||||
}
|
||||
|
||||
// UnimplementedTransportPluginServer must be embedded to have
|
||||
// forward compatible implementations.
|
||||
//
|
||||
// NOTE: this should be embedded by value instead of pointer to avoid a nil
|
||||
// pointer dereference when methods are called.
|
||||
type UnimplementedTransportPluginServer struct{}
|
||||
|
||||
func (UnimplementedTransportPluginServer) GetInfo(context.Context, *GetInfoRequest) (*GetInfoResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method GetInfo not implemented")
|
||||
}
|
||||
func (UnimplementedTransportPluginServer) Health(context.Context, *HealthRequest) (*HealthResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method Health not implemented")
|
||||
}
|
||||
func (UnimplementedTransportPluginServer) ValidateConfig(context.Context, *ValidateConfigRequest) (*ValidateConfigResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ValidateConfig not implemented")
|
||||
}
|
||||
func (UnimplementedTransportPluginServer) ApplyConfig(context.Context, *ApplyConfigRequest) (*ApplyConfigResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ApplyConfig not implemented")
|
||||
}
|
||||
func (UnimplementedTransportPluginServer) TestConfig(context.Context, *TestConfigRequest) (*TestConfigResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method TestConfig not implemented")
|
||||
}
|
||||
func (UnimplementedTransportPluginServer) Forward(grpc.BidiStreamingServer[ForwardRequest, ForwardResponse]) error {
|
||||
return status.Error(codes.Unimplemented, "method Forward not implemented")
|
||||
}
|
||||
func (UnimplementedTransportPluginServer) mustEmbedUnimplementedTransportPluginServer() {}
|
||||
func (UnimplementedTransportPluginServer) testEmbeddedByValue() {}
|
||||
|
||||
// UnsafeTransportPluginServer may be embedded to opt out of forward compatibility for this service.
|
||||
// Use of this interface is not recommended, as added methods to TransportPluginServer will
|
||||
// result in compilation errors.
|
||||
type UnsafeTransportPluginServer interface {
|
||||
mustEmbedUnimplementedTransportPluginServer()
|
||||
}
|
||||
|
||||
func RegisterTransportPluginServer(s grpc.ServiceRegistrar, srv TransportPluginServer) {
|
||||
// If the following call panics, it indicates UnimplementedTransportPluginServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
|
||||
t.testEmbeddedByValue()
|
||||
}
|
||||
s.RegisterService(&TransportPlugin_ServiceDesc, srv)
|
||||
}
|
||||
|
||||
func _TransportPlugin_GetInfo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(GetInfoRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(TransportPluginServer).GetInfo(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: TransportPlugin_GetInfo_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(TransportPluginServer).GetInfo(ctx, req.(*GetInfoRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _TransportPlugin_Health_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(HealthRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(TransportPluginServer).Health(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: TransportPlugin_Health_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(TransportPluginServer).Health(ctx, req.(*HealthRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _TransportPlugin_ValidateConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ValidateConfigRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(TransportPluginServer).ValidateConfig(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: TransportPlugin_ValidateConfig_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(TransportPluginServer).ValidateConfig(ctx, req.(*ValidateConfigRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _TransportPlugin_ApplyConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ApplyConfigRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(TransportPluginServer).ApplyConfig(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: TransportPlugin_ApplyConfig_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(TransportPluginServer).ApplyConfig(ctx, req.(*ApplyConfigRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _TransportPlugin_TestConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(TestConfigRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(TransportPluginServer).TestConfig(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: TransportPlugin_TestConfig_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(TransportPluginServer).TestConfig(ctx, req.(*TestConfigRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _TransportPlugin_Forward_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||
return srv.(TransportPluginServer).Forward(&grpc.GenericServerStream[ForwardRequest, ForwardResponse]{ServerStream: stream})
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type TransportPlugin_ForwardServer = grpc.BidiStreamingServer[ForwardRequest, ForwardResponse]
|
||||
|
||||
// TransportPlugin_ServiceDesc is the grpc.ServiceDesc for TransportPlugin service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
var TransportPlugin_ServiceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "sub2api.plugin.v1.TransportPlugin",
|
||||
HandlerType: (*TransportPluginServer)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{
|
||||
MethodName: "GetInfo",
|
||||
Handler: _TransportPlugin_GetInfo_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "Health",
|
||||
Handler: _TransportPlugin_Health_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ValidateConfig",
|
||||
Handler: _TransportPlugin_ValidateConfig_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ApplyConfig",
|
||||
Handler: _TransportPlugin_ApplyConfig_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "TestConfig",
|
||||
Handler: _TransportPlugin_TestConfig_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{
|
||||
{
|
||||
StreamName: "Forward",
|
||||
Handler: _TransportPlugin_Forward_Handler,
|
||||
ServerStreams: true,
|
||||
ClientStreams: true,
|
||||
},
|
||||
},
|
||||
Metadata: "plugin.proto",
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package pluginv1
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
hcplugin "github.com/hashicorp/go-plugin"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
const (
|
||||
// ProtocolVersion 是宿主与插件进程握手协议版本。
|
||||
ProtocolVersion = 1
|
||||
// TransportAPIVersion 是 OpenAI OAuth 出站传输契约版本。
|
||||
TransportAPIVersion = 1
|
||||
// UIBridgeVersion 是插件管理页与沙箱 UI 的消息协议版本。
|
||||
UIBridgeVersion = 1
|
||||
// TransportPluginName 是 go-plugin 中注册的唯一能力名称。
|
||||
TransportPluginName = "oauth_transport"
|
||||
)
|
||||
|
||||
// HandshakeConfig 防止普通可执行文件被误当成 Sub2API 插件启动。
|
||||
var HandshakeConfig = hcplugin.HandshakeConfig{
|
||||
ProtocolVersion: ProtocolVersion,
|
||||
MagicCookieKey: "SUB2API_PLUGIN_MAGIC_COOKIE",
|
||||
MagicCookieValue: "sub2api-plugin-v1",
|
||||
}
|
||||
|
||||
// GRPCPlugin 把生成的 gRPC 服务注册到 go-plugin 子进程。
|
||||
type GRPCPlugin struct {
|
||||
hcplugin.NetRPCUnsupportedPlugin
|
||||
Impl TransportPluginServer
|
||||
}
|
||||
|
||||
func (p *GRPCPlugin) GRPCServer(_ *hcplugin.GRPCBroker, server *grpc.Server) error {
|
||||
RegisterTransportPluginServer(server, p.Impl)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *GRPCPlugin) GRPCClient(_ context.Context, _ *hcplugin.GRPCBroker, conn *grpc.ClientConn) (any, error) {
|
||||
return NewTransportPluginClient(conn), nil
|
||||
}
|
||||
|
||||
// ClientPluginMap 返回宿主侧使用的插件声明。
|
||||
func ClientPluginMap() map[string]hcplugin.Plugin {
|
||||
return map[string]hcplugin.Plugin{
|
||||
TransportPluginName: &GRPCPlugin{},
|
||||
}
|
||||
}
|
||||
|
||||
// Serve 启动一个实现了传输协议的插件进程。
|
||||
func Serve(impl TransportPluginServer) {
|
||||
hcplugin.Serve(&hcplugin.ServeConfig{
|
||||
HandshakeConfig: HandshakeConfig,
|
||||
Plugins: map[string]hcplugin.Plugin{
|
||||
TransportPluginName: &GRPCPlugin{Impl: impl},
|
||||
},
|
||||
GRPCServer: hcplugin.DefaultGRPCServer,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user