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:
shaw
2026-09-19 11:25:56 +08:00
parent efe9aab1e4
commit c63bd14a02
22 changed files with 2703 additions and 86 deletions
+28 -7
View File
@@ -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` 即可展示实时状态。
## 协议源码
+4 -1
View File
@@ -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
+107
View File
@@ -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;
}
+362 -6
View File
@@ -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",
}
+26 -3
View File
@@ -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 返回宿主侧使用的插件声明。