mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 12:02:27 +08:00
feat(plugin): generic host services + read-only status bridge channel
Extend the local plugin system's host<->plugin contract with generic, capability-neutral facilities, so transport plugins can be built on top without embedding plugin-specific logic in the host. Host services (plugin dials a broker-provided HostService gRPC): - per-plugin namespaced KV store (key/value size and TTL bounded) - account directory: ListAccounts + ResolveOutboundIdentity, restricted to the accounts a plugin's declared capability already covers - InitHostServices lifecycle hook; plugins that don't implement it run unchanged and are simply offered no host services Read-only runtime status channel: - HealthResponse gains an opaque, plugin-defined status_json produced passively (no config apply, no upstream call, no side effects) - GET /admin/plugins/:id/status (ungated) and the plugin.status UI-bridge verb map to Health, so a status UI can poll live state without a step-up prompt or a "test" side effect - config.test no longer force-shows a host success toast; the plugin UI owns result presentation, so plugins that read status do not spam toasts Document the UI-bridge verb taxonomy (read vs mutate/test gating) and the status contract under pkg/pluginapi.
This commit is contained in:
@@ -30,6 +30,21 @@
|
||||
- Sub2API 继续负责响应状态处理、SSE 解析、错误映射、用量统计、计费和下游输出。
|
||||
- 灰度比例以账号 ID 稳定分桶,未命中的 OAuth 账号继续使用原有内置路径。
|
||||
|
||||
## 宿主服务(HostService)
|
||||
|
||||
`HostService` 是宿主经 go-plugin broker 反向暴露给插件的通用能力层,独立于具体插件类型,供有状态插件使用。它通过 `TransportPlugin.InitHostServices` 在运行时协商:宿主启动后把一个 broker 流 id 交给插件,插件用它拨号回宿主并获得 `HostServiceClient`。
|
||||
|
||||
- **可选且向后兼容**:未实现 `InitHostServices` 的旧插件返回 `Unimplemented`,宿主静默跳过,转发能力不受影响。宿主服务有独立的 `HostServiceAPIVersion`,新增能力不会改变传输契约版本,也不会使既有插件失效。
|
||||
- **随进程回收**:宿主服务实例与插件进程生命周期绑定,插件退出时 broker 关闭并自动 `GracefulStop`,无需插件手动清理。
|
||||
|
||||
当前提供的通用设施:
|
||||
|
||||
- **命名空间键值存储(KV)**:`KVGet` / `KVSet` / `KVDelete` / `KVList`,为插件持久化跨请求、跨副本、跨重启的状态。存储由 Redis 支撑,因此多实例部署天然共享同一份状态。
|
||||
- **命名空间隔离**:宿主根据服务该连接的运行时注入插件身份(`pluginKey`),插件无法伪造,也无法读写其它插件的命名空间。
|
||||
- **护栏**:`namespace` / `key` 仅允许 `[A-Za-z0-9._-]`;单值上限 256 KiB;`ttl_seconds` 为 0 表示不过期、正值有上限;`KVList` 返回条数有上限。
|
||||
|
||||
新增宿主设施时,在 `HostService` 上追加 RPC 即可,无需改动传输契约或清单格式。
|
||||
|
||||
## 包结构
|
||||
|
||||
```text
|
||||
@@ -61,15 +76,21 @@ ui/assets/...
|
||||
|
||||
插件 UI 由包内静态文件实现,宿主使用只有 `allow-scripts` 权限的 sandbox iframe 加载。iframe 没有管理员 Token,也不能直接访问管理 API。宿主为每次打开配置页生成短时资源 URL 和独立 Bridge Token,并且同时校验消息来源窗口与 Token。
|
||||
|
||||
UI 可以发送以下消息:
|
||||
UI 可以发送以下消息。消息按语义分层,鉴权与副作用一致对应(读操作免二次验证,写/主动测试需要二次验证),任何插件都可复用,不针对具体插件定制:
|
||||
|
||||
- `config.load`
|
||||
- `config.save`
|
||||
- `config.test`
|
||||
- `ui.resize`
|
||||
- `ui.notify`
|
||||
| 消息 | 语义 | 二次验证 | 映射的插件 RPC |
|
||||
| --- | --- | --- | --- |
|
||||
| `config.load` | 读取已保存配置 | 否 | 宿主数据库 |
|
||||
| `config.save` | 写入配置 | 是 | `ValidateConfig` + `ApplyConfig` |
|
||||
| `config.test` | 主动测试配置/连通性(可产生副作用) | 是 | `TestConfig` |
|
||||
| `plugin.status` | 读取运行时状态(无副作用) | 否 | `Health`(`status_json`) |
|
||||
| `ui.resize` / `ui.notify` | 仅 UI 交互 | — | — |
|
||||
|
||||
每个请求消息带 `request_id`,宿主以 `<type>.result` 返回结果。配置整体使用 Sub2API 的密钥加密后存入数据库;运行中插件会先验证并应用新配置,数据库写入失败时恢复旧配置。
|
||||
每个请求消息带 `request_id`,宿主以 `<type>.result` 返回结果。
|
||||
|
||||
- 配置整体使用 Sub2API 的密钥加密后存入数据库;运行中插件会先验证并应用新配置,数据库写入失败时恢复旧配置。
|
||||
- `config.test` 的结果由插件 UI 自行展示(内联或经 `ui.notify`),宿主不再对成功结果强制弹出提示,避免插件把它当作轻量状态轮询时刷屏。
|
||||
- `plugin.status` 是通用的**只读**状态通道:宿主经 `GET /admin/plugins/:id/status` 调用运行中插件的 `Health`,返回 `{healthy, message, status_json}`。`status_json` 是插件自定义的**不透明** JSON 快照(宿主不解析、不参与健康判定),插件必须以无副作用方式生成(不得应用配置、访问上游或触发探测),因此该端点只读、免二次验证。插件未运行时返回 `healthy=false` 且不含 `status_json`。这样带状态面板的插件无需滥用 `config.test` 即可展示实时状态。
|
||||
|
||||
## 协议源码
|
||||
|
||||
|
||||
@@ -44,10 +44,13 @@ UI 到宿主:
|
||||
| `config.load` | 无 | `config` |
|
||||
| `config.save` | `config` 对象 | 规范化后的 `config` |
|
||||
| `config.test` | 无 | `result` |
|
||||
| `plugin.status` | 无 | `result`(`Health`:`{healthy, message, status_json}`) |
|
||||
| `ui.resize` | `height` | 无响应 |
|
||||
| `ui.notify` | `level`、`message` | 无响应 |
|
||||
|
||||
`config.test` 在 v1 中测试已保存配置。UI 若要测试当前表单,应先调用 `config.save`。
|
||||
`config.test` 在 v1 中测试已保存配置(需二次验证,可产生副作用)。UI 若要测试当前表单,应先调用 `config.save`。
|
||||
|
||||
`plugin.status` 是只读运行时状态通道:无副作用、免二次验证、不弹宿主提示,供状态面板轮询。它映射到插件 `Health`,`result.status_json` 是插件自定义的不透明 JSON 快照。带状态展示的插件应使用它,而不是把 `config.test` 当作状态轮询。
|
||||
|
||||
## 必须执行的校验
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,28 @@ service TransportPlugin {
|
||||
rpc ApplyConfig(ApplyConfigRequest) returns (ApplyConfigResponse);
|
||||
rpc TestConfig(TestConfigRequest) returns (TestConfigResponse);
|
||||
rpc Forward(stream ForwardRequest) returns (stream ForwardResponse);
|
||||
// InitHostServices hands the plugin a go-plugin broker stream id it can dial
|
||||
// back to reach the host-provided HostService. It is an optional, generic
|
||||
// lifecycle hook: plugins built against an older contract leave it
|
||||
// Unimplemented and the host simply runs without offering host services.
|
||||
rpc InitHostServices(InitHostServicesRequest) returns (InitHostServicesResponse);
|
||||
}
|
||||
|
||||
// HostService is implemented by the host and dialed by the plugin over the
|
||||
// go-plugin broker id delivered via InitHostServices. It exposes generic,
|
||||
// capability-neutral facilities that any plugin may use; new facilities are
|
||||
// added here as new RPCs without changing the transport contract version.
|
||||
service HostService {
|
||||
rpc KVGet(KVGetRequest) returns (KVGetResponse);
|
||||
rpc KVSet(KVSetRequest) returns (KVSetResponse);
|
||||
rpc KVDelete(KVDeleteRequest) returns (KVDeleteResponse);
|
||||
rpc KVList(KVListRequest) returns (KVListResponse);
|
||||
// Account directory: enumerate the accounts the plugin is bound to act on and
|
||||
// resolve the outbound identity (credentials/headers/proxy) the host would
|
||||
// attach to a live request. The host restricts both calls to accounts the
|
||||
// requesting plugin's capability already covers.
|
||||
rpc ListAccounts(ListAccountsRequest) returns (ListAccountsResponse);
|
||||
rpc ResolveOutboundIdentity(ResolveOutboundIdentityRequest) returns (ResolveOutboundIdentityResponse);
|
||||
}
|
||||
|
||||
message GetInfoRequest {}
|
||||
@@ -28,6 +50,12 @@ message HealthRequest {}
|
||||
message HealthResponse {
|
||||
bool healthy = 1;
|
||||
string message = 2;
|
||||
// status_json is an optional plugin-defined JSON blob describing current runtime
|
||||
// state (e.g. a status summary for the config UI). It MUST be produced passively
|
||||
// — reading it must not apply config, reach upstream, or cause side effects — so
|
||||
// the host can expose it via a lightweight, ungated status endpoint. Ignored by
|
||||
// the host's health gating.
|
||||
string status_json = 3;
|
||||
}
|
||||
|
||||
message ValidateConfigRequest {
|
||||
@@ -57,6 +85,9 @@ message TestConfigResponse {
|
||||
bool success = 1;
|
||||
string message = 2;
|
||||
int64 latency_ms = 3;
|
||||
// status_json is an optional plugin-defined JSON blob surfaced to the config UI
|
||||
// alongside the test result (e.g. a runtime status summary). Ignored by the host.
|
||||
string status_json = 4;
|
||||
}
|
||||
|
||||
message HeaderValues {
|
||||
@@ -115,3 +146,79 @@ message ForwardResponse {
|
||||
ForwardResponseError error = 4;
|
||||
}
|
||||
}
|
||||
|
||||
message InitHostServicesRequest {
|
||||
// host_service_id is the broker stream id the plugin dials to reach HostService.
|
||||
uint32 host_service_id = 1;
|
||||
// host_service_api_version lets the plugin reject a host it cannot talk to.
|
||||
uint32 host_service_api_version = 2;
|
||||
}
|
||||
|
||||
message InitHostServicesResponse {
|
||||
// ready is true once the plugin has successfully dialed back the host service.
|
||||
bool ready = 1;
|
||||
string message = 2;
|
||||
}
|
||||
|
||||
// KV messages back a namespaced, per-plugin persistent key-value store. The
|
||||
// host derives the owning plugin from the runtime that serves the connection,
|
||||
// so a plugin can never read or write another plugin's namespace.
|
||||
message KVGetRequest {
|
||||
string namespace = 1;
|
||||
string key = 2;
|
||||
}
|
||||
|
||||
message KVGetResponse {
|
||||
bool found = 1;
|
||||
bytes value = 2;
|
||||
}
|
||||
|
||||
message KVSetRequest {
|
||||
string namespace = 1;
|
||||
string key = 2;
|
||||
bytes value = 3;
|
||||
// ttl_seconds > 0 sets an expiry; 0 stores the entry without expiry.
|
||||
int64 ttl_seconds = 4;
|
||||
}
|
||||
|
||||
message KVSetResponse {}
|
||||
|
||||
message KVDeleteRequest {
|
||||
string namespace = 1;
|
||||
string key = 2;
|
||||
}
|
||||
|
||||
message KVDeleteResponse {}
|
||||
|
||||
message KVListRequest {
|
||||
string namespace = 1;
|
||||
string key_prefix = 2;
|
||||
int32 limit = 3;
|
||||
}
|
||||
|
||||
message KVListResponse {
|
||||
repeated string keys = 1;
|
||||
}
|
||||
|
||||
message ListAccountsRequest {
|
||||
string platform = 1;
|
||||
string account_type = 2;
|
||||
}
|
||||
|
||||
message ListAccountsResponse {
|
||||
repeated int64 account_ids = 1;
|
||||
}
|
||||
|
||||
message ResolveOutboundIdentityRequest {
|
||||
int64 account_id = 1;
|
||||
}
|
||||
|
||||
message ResolveOutboundIdentityResponse {
|
||||
bool found = 1;
|
||||
int64 account_id = 2;
|
||||
string platform = 3;
|
||||
string account_type = 4;
|
||||
string proxy_url = 5;
|
||||
string token = 6;
|
||||
map<string, HeaderValues> headers = 7;
|
||||
}
|
||||
|
||||
@@ -19,12 +19,13 @@ import (
|
||||
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"
|
||||
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"
|
||||
TransportPlugin_InitHostServices_FullMethodName = "/sub2api.plugin.v1.TransportPlugin/InitHostServices"
|
||||
)
|
||||
|
||||
// TransportPluginClient is the client API for TransportPlugin service.
|
||||
@@ -37,6 +38,11 @@ type TransportPluginClient interface {
|
||||
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)
|
||||
// InitHostServices hands the plugin a go-plugin broker stream id it can dial
|
||||
// back to reach the host-provided HostService. It is an optional, generic
|
||||
// lifecycle hook: plugins built against an older contract leave it
|
||||
// Unimplemented and the host simply runs without offering host services.
|
||||
InitHostServices(ctx context.Context, in *InitHostServicesRequest, opts ...grpc.CallOption) (*InitHostServicesResponse, error)
|
||||
}
|
||||
|
||||
type transportPluginClient struct {
|
||||
@@ -110,6 +116,16 @@ func (c *transportPluginClient) Forward(ctx context.Context, opts ...grpc.CallOp
|
||||
// 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]
|
||||
|
||||
func (c *transportPluginClient) InitHostServices(ctx context.Context, in *InitHostServicesRequest, opts ...grpc.CallOption) (*InitHostServicesResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(InitHostServicesResponse)
|
||||
err := c.cc.Invoke(ctx, TransportPlugin_InitHostServices_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// TransportPluginServer is the server API for TransportPlugin service.
|
||||
// All implementations must embed UnimplementedTransportPluginServer
|
||||
// for forward compatibility.
|
||||
@@ -120,6 +136,11 @@ type TransportPluginServer interface {
|
||||
ApplyConfig(context.Context, *ApplyConfigRequest) (*ApplyConfigResponse, error)
|
||||
TestConfig(context.Context, *TestConfigRequest) (*TestConfigResponse, error)
|
||||
Forward(grpc.BidiStreamingServer[ForwardRequest, ForwardResponse]) error
|
||||
// InitHostServices hands the plugin a go-plugin broker stream id it can dial
|
||||
// back to reach the host-provided HostService. It is an optional, generic
|
||||
// lifecycle hook: plugins built against an older contract leave it
|
||||
// Unimplemented and the host simply runs without offering host services.
|
||||
InitHostServices(context.Context, *InitHostServicesRequest) (*InitHostServicesResponse, error)
|
||||
mustEmbedUnimplementedTransportPluginServer()
|
||||
}
|
||||
|
||||
@@ -148,6 +169,9 @@ func (UnimplementedTransportPluginServer) TestConfig(context.Context, *TestConfi
|
||||
func (UnimplementedTransportPluginServer) Forward(grpc.BidiStreamingServer[ForwardRequest, ForwardResponse]) error {
|
||||
return status.Error(codes.Unimplemented, "method Forward not implemented")
|
||||
}
|
||||
func (UnimplementedTransportPluginServer) InitHostServices(context.Context, *InitHostServicesRequest) (*InitHostServicesResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method InitHostServices not implemented")
|
||||
}
|
||||
func (UnimplementedTransportPluginServer) mustEmbedUnimplementedTransportPluginServer() {}
|
||||
func (UnimplementedTransportPluginServer) testEmbeddedByValue() {}
|
||||
|
||||
@@ -266,6 +290,24 @@ func _TransportPlugin_Forward_Handler(srv interface{}, stream grpc.ServerStream)
|
||||
// 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]
|
||||
|
||||
func _TransportPlugin_InitHostServices_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(InitHostServicesRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(TransportPluginServer).InitHostServices(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: TransportPlugin_InitHostServices_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(TransportPluginServer).InitHostServices(ctx, req.(*InitHostServicesRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// 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)
|
||||
@@ -293,6 +335,10 @@ var TransportPlugin_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "TestConfig",
|
||||
Handler: _TransportPlugin_TestConfig_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "InitHostServices",
|
||||
Handler: _TransportPlugin_InitHostServices_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{
|
||||
{
|
||||
@@ -304,3 +350,313 @@ var TransportPlugin_ServiceDesc = grpc.ServiceDesc{
|
||||
},
|
||||
Metadata: "plugin.proto",
|
||||
}
|
||||
|
||||
const (
|
||||
HostService_KVGet_FullMethodName = "/sub2api.plugin.v1.HostService/KVGet"
|
||||
HostService_KVSet_FullMethodName = "/sub2api.plugin.v1.HostService/KVSet"
|
||||
HostService_KVDelete_FullMethodName = "/sub2api.plugin.v1.HostService/KVDelete"
|
||||
HostService_KVList_FullMethodName = "/sub2api.plugin.v1.HostService/KVList"
|
||||
HostService_ListAccounts_FullMethodName = "/sub2api.plugin.v1.HostService/ListAccounts"
|
||||
HostService_ResolveOutboundIdentity_FullMethodName = "/sub2api.plugin.v1.HostService/ResolveOutboundIdentity"
|
||||
)
|
||||
|
||||
// HostServiceClient is the client API for HostService 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.
|
||||
//
|
||||
// HostService is implemented by the host and dialed by the plugin over the
|
||||
// go-plugin broker id delivered via InitHostServices. It exposes generic,
|
||||
// capability-neutral facilities that any plugin may use; new facilities are
|
||||
// added here as new RPCs without changing the transport contract version.
|
||||
type HostServiceClient interface {
|
||||
KVGet(ctx context.Context, in *KVGetRequest, opts ...grpc.CallOption) (*KVGetResponse, error)
|
||||
KVSet(ctx context.Context, in *KVSetRequest, opts ...grpc.CallOption) (*KVSetResponse, error)
|
||||
KVDelete(ctx context.Context, in *KVDeleteRequest, opts ...grpc.CallOption) (*KVDeleteResponse, error)
|
||||
KVList(ctx context.Context, in *KVListRequest, opts ...grpc.CallOption) (*KVListResponse, error)
|
||||
// Account directory: enumerate the accounts the plugin is bound to act on and
|
||||
// resolve the outbound identity (credentials/headers/proxy) the host would
|
||||
// attach to a live request. The host restricts both calls to accounts the
|
||||
// requesting plugin's capability already covers.
|
||||
ListAccounts(ctx context.Context, in *ListAccountsRequest, opts ...grpc.CallOption) (*ListAccountsResponse, error)
|
||||
ResolveOutboundIdentity(ctx context.Context, in *ResolveOutboundIdentityRequest, opts ...grpc.CallOption) (*ResolveOutboundIdentityResponse, error)
|
||||
}
|
||||
|
||||
type hostServiceClient struct {
|
||||
cc grpc.ClientConnInterface
|
||||
}
|
||||
|
||||
func NewHostServiceClient(cc grpc.ClientConnInterface) HostServiceClient {
|
||||
return &hostServiceClient{cc}
|
||||
}
|
||||
|
||||
func (c *hostServiceClient) KVGet(ctx context.Context, in *KVGetRequest, opts ...grpc.CallOption) (*KVGetResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(KVGetResponse)
|
||||
err := c.cc.Invoke(ctx, HostService_KVGet_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *hostServiceClient) KVSet(ctx context.Context, in *KVSetRequest, opts ...grpc.CallOption) (*KVSetResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(KVSetResponse)
|
||||
err := c.cc.Invoke(ctx, HostService_KVSet_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *hostServiceClient) KVDelete(ctx context.Context, in *KVDeleteRequest, opts ...grpc.CallOption) (*KVDeleteResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(KVDeleteResponse)
|
||||
err := c.cc.Invoke(ctx, HostService_KVDelete_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *hostServiceClient) KVList(ctx context.Context, in *KVListRequest, opts ...grpc.CallOption) (*KVListResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(KVListResponse)
|
||||
err := c.cc.Invoke(ctx, HostService_KVList_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *hostServiceClient) ListAccounts(ctx context.Context, in *ListAccountsRequest, opts ...grpc.CallOption) (*ListAccountsResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ListAccountsResponse)
|
||||
err := c.cc.Invoke(ctx, HostService_ListAccounts_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *hostServiceClient) ResolveOutboundIdentity(ctx context.Context, in *ResolveOutboundIdentityRequest, opts ...grpc.CallOption) (*ResolveOutboundIdentityResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ResolveOutboundIdentityResponse)
|
||||
err := c.cc.Invoke(ctx, HostService_ResolveOutboundIdentity_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// HostServiceServer is the server API for HostService service.
|
||||
// All implementations must embed UnimplementedHostServiceServer
|
||||
// for forward compatibility.
|
||||
//
|
||||
// HostService is implemented by the host and dialed by the plugin over the
|
||||
// go-plugin broker id delivered via InitHostServices. It exposes generic,
|
||||
// capability-neutral facilities that any plugin may use; new facilities are
|
||||
// added here as new RPCs without changing the transport contract version.
|
||||
type HostServiceServer interface {
|
||||
KVGet(context.Context, *KVGetRequest) (*KVGetResponse, error)
|
||||
KVSet(context.Context, *KVSetRequest) (*KVSetResponse, error)
|
||||
KVDelete(context.Context, *KVDeleteRequest) (*KVDeleteResponse, error)
|
||||
KVList(context.Context, *KVListRequest) (*KVListResponse, error)
|
||||
// Account directory: enumerate the accounts the plugin is bound to act on and
|
||||
// resolve the outbound identity (credentials/headers/proxy) the host would
|
||||
// attach to a live request. The host restricts both calls to accounts the
|
||||
// requesting plugin's capability already covers.
|
||||
ListAccounts(context.Context, *ListAccountsRequest) (*ListAccountsResponse, error)
|
||||
ResolveOutboundIdentity(context.Context, *ResolveOutboundIdentityRequest) (*ResolveOutboundIdentityResponse, error)
|
||||
mustEmbedUnimplementedHostServiceServer()
|
||||
}
|
||||
|
||||
// UnimplementedHostServiceServer 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 UnimplementedHostServiceServer struct{}
|
||||
|
||||
func (UnimplementedHostServiceServer) KVGet(context.Context, *KVGetRequest) (*KVGetResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method KVGet not implemented")
|
||||
}
|
||||
func (UnimplementedHostServiceServer) KVSet(context.Context, *KVSetRequest) (*KVSetResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method KVSet not implemented")
|
||||
}
|
||||
func (UnimplementedHostServiceServer) KVDelete(context.Context, *KVDeleteRequest) (*KVDeleteResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method KVDelete not implemented")
|
||||
}
|
||||
func (UnimplementedHostServiceServer) KVList(context.Context, *KVListRequest) (*KVListResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method KVList not implemented")
|
||||
}
|
||||
func (UnimplementedHostServiceServer) ListAccounts(context.Context, *ListAccountsRequest) (*ListAccountsResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ListAccounts not implemented")
|
||||
}
|
||||
func (UnimplementedHostServiceServer) ResolveOutboundIdentity(context.Context, *ResolveOutboundIdentityRequest) (*ResolveOutboundIdentityResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ResolveOutboundIdentity not implemented")
|
||||
}
|
||||
func (UnimplementedHostServiceServer) mustEmbedUnimplementedHostServiceServer() {}
|
||||
func (UnimplementedHostServiceServer) testEmbeddedByValue() {}
|
||||
|
||||
// UnsafeHostServiceServer may be embedded to opt out of forward compatibility for this service.
|
||||
// Use of this interface is not recommended, as added methods to HostServiceServer will
|
||||
// result in compilation errors.
|
||||
type UnsafeHostServiceServer interface {
|
||||
mustEmbedUnimplementedHostServiceServer()
|
||||
}
|
||||
|
||||
func RegisterHostServiceServer(s grpc.ServiceRegistrar, srv HostServiceServer) {
|
||||
// If the following call panics, it indicates UnimplementedHostServiceServer 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(&HostService_ServiceDesc, srv)
|
||||
}
|
||||
|
||||
func _HostService_KVGet_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(KVGetRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(HostServiceServer).KVGet(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: HostService_KVGet_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(HostServiceServer).KVGet(ctx, req.(*KVGetRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _HostService_KVSet_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(KVSetRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(HostServiceServer).KVSet(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: HostService_KVSet_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(HostServiceServer).KVSet(ctx, req.(*KVSetRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _HostService_KVDelete_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(KVDeleteRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(HostServiceServer).KVDelete(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: HostService_KVDelete_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(HostServiceServer).KVDelete(ctx, req.(*KVDeleteRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _HostService_KVList_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(KVListRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(HostServiceServer).KVList(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: HostService_KVList_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(HostServiceServer).KVList(ctx, req.(*KVListRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _HostService_ListAccounts_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ListAccountsRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(HostServiceServer).ListAccounts(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: HostService_ListAccounts_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(HostServiceServer).ListAccounts(ctx, req.(*ListAccountsRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _HostService_ResolveOutboundIdentity_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ResolveOutboundIdentityRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(HostServiceServer).ResolveOutboundIdentity(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: HostService_ResolveOutboundIdentity_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(HostServiceServer).ResolveOutboundIdentity(ctx, req.(*ResolveOutboundIdentityRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// HostService_ServiceDesc is the grpc.ServiceDesc for HostService service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
var HostService_ServiceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "sub2api.plugin.v1.HostService",
|
||||
HandlerType: (*HostServiceServer)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{
|
||||
MethodName: "KVGet",
|
||||
Handler: _HostService_KVGet_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "KVSet",
|
||||
Handler: _HostService_KVSet_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "KVDelete",
|
||||
Handler: _HostService_KVDelete_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "KVList",
|
||||
Handler: _HostService_KVList_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ListAccounts",
|
||||
Handler: _HostService_ListAccounts_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ResolveOutboundIdentity",
|
||||
Handler: _HostService_ResolveOutboundIdentity_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "plugin.proto",
|
||||
}
|
||||
|
||||
@@ -14,6 +14,10 @@ const (
|
||||
TransportAPIVersion = 1
|
||||
// UIBridgeVersion 是插件管理页与沙箱 UI 的消息协议版本。
|
||||
UIBridgeVersion = 1
|
||||
// HostServiceAPIVersion 是宿主反向服务(HostService)的契约版本。它独立于
|
||||
// TransportAPIVersion:宿主服务是叠加在传输契约之上的可选能力,通过
|
||||
// InitHostServices 在运行时协商,因此新增宿主能力不会使既有插件失效。
|
||||
HostServiceAPIVersion = 1
|
||||
// TransportPluginName 是 go-plugin 中注册的唯一能力名称。
|
||||
TransportPluginName = "oauth_transport"
|
||||
)
|
||||
@@ -25,19 +29,38 @@ var HandshakeConfig = hcplugin.HandshakeConfig{
|
||||
MagicCookieValue: "sub2api-plugin-v1",
|
||||
}
|
||||
|
||||
// HostBrokerReceiver 由需要反向调用宿主服务的插件传输实现选择性实现。
|
||||
// GRPCServer 在插件进程启动注册服务时把 go-plugin broker 交给它,插件随后可在
|
||||
// InitHostServices 中用宿主下发的 broker id 拨号回宿主。宿主侧不实现此接口。
|
||||
type HostBrokerReceiver interface {
|
||||
SetHostBroker(broker *hcplugin.GRPCBroker)
|
||||
}
|
||||
|
||||
// TransportClient 是宿主 Dispense 传输能力后拿到的句柄,除 gRPC 客户端外还捆绑了
|
||||
// 本连接的 go-plugin broker,宿主据此把 HostService 反向暴露给插件。
|
||||
type TransportClient struct {
|
||||
TransportPluginClient
|
||||
Broker *hcplugin.GRPCBroker
|
||||
}
|
||||
|
||||
// GRPCPlugin 把生成的 gRPC 服务注册到 go-plugin 子进程。
|
||||
type GRPCPlugin struct {
|
||||
hcplugin.NetRPCUnsupportedPlugin
|
||||
Impl TransportPluginServer
|
||||
}
|
||||
|
||||
func (p *GRPCPlugin) GRPCServer(_ *hcplugin.GRPCBroker, server *grpc.Server) error {
|
||||
func (p *GRPCPlugin) GRPCServer(broker *hcplugin.GRPCBroker, server *grpc.Server) error {
|
||||
RegisterTransportPluginServer(server, p.Impl)
|
||||
// 插件侧:把 broker 交给需要反向访问宿主服务的实现(可选能力)。
|
||||
if receiver, ok := p.Impl.(HostBrokerReceiver); ok {
|
||||
receiver.SetHostBroker(broker)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *GRPCPlugin) GRPCClient(_ context.Context, _ *hcplugin.GRPCBroker, conn *grpc.ClientConn) (any, error) {
|
||||
return NewTransportPluginClient(conn), nil
|
||||
func (p *GRPCPlugin) GRPCClient(_ context.Context, broker *hcplugin.GRPCBroker, conn *grpc.ClientConn) (any, error) {
|
||||
// 宿主侧:捆绑 broker,供 startPluginRuntime 反向暴露 HostService。
|
||||
return &TransportClient{TransportPluginClient: NewTransportPluginClient(conn), Broker: broker}, nil
|
||||
}
|
||||
|
||||
// ClientPluginMap 返回宿主侧使用的插件声明。
|
||||
|
||||
Reference in New Issue
Block a user