diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ad7e04f..8c49748 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: CI +name: 持续集成 on: push: @@ -8,7 +8,7 @@ on: jobs: ci: - name: Lint & Test & Build + name: 检查、测试和构建 runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 @@ -17,8 +17,18 @@ jobs: with: go-version-file: go.mod - - name: Install golangci-lint + - uses: actions/setup-node@v6 + with: + node-version: 22 + cache: npm + cache-dependency-path: theme/package-lock.json + + - name: 安装主题包依赖 + working-directory: theme + run: npm ci + + - name: 安装 golangci-lint run: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest - - name: Run CI checks + - name: 运行 CI 检查 run: make ci diff --git a/.github/workflows/publish-theme.yml b/.github/workflows/publish-theme.yml new file mode 100644 index 0000000..cb43644 --- /dev/null +++ b/.github/workflows/publish-theme.yml @@ -0,0 +1,41 @@ +name: 发布 npm 公共包 + +on: + release: + types: [published] + workflow_dispatch: + +permissions: + contents: read + +jobs: + publish: + name: 发布 @doudou-start/airgate-theme + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - uses: actions/setup-node@v6 + with: + node-version: 22 + registry-url: https://registry.npmjs.org + cache: npm + cache-dependency-path: theme/package-lock.json + + - name: 安装依赖 + working-directory: theme + run: npm ci + + - name: 构建包 + working-directory: theme + run: npm run build + + - name: 检查包内容 + working-directory: theme + run: npm pack --dry-run + + - name: 发布包 + working-directory: theme + run: npm publish --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore index da07a7d..310ada1 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,5 @@ coverage.out # 本地工具链 .tools/ -# 前端依赖和构建产物 -frontend/node_modules/ -frontend/dist/ +# 主题包依赖 +theme/node_modules/ diff --git a/Makefile b/Makefile index d4e1455..2c902f0 100644 --- a/Makefile +++ b/Makefile @@ -11,14 +11,14 @@ PROTOC_GEN_GRPC_VER := v1.6.0 TOOLS_DIR := $(CURDIR)/.tools PROTOC_BIN := $(TOOLS_DIR)/bin/protoc -.PHONY: help ci pre-commit lint fmt test vet build proto proto-tools clean setup-hooks +.PHONY: help ci pre-commit lint fmt test vet build theme-package-build theme-package-check theme theme-check proto proto-check proto-tools clean setup-hooks help: ## 显示帮助信息 @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-18s\033[0m %s\n", $$1, $$2}' # ===================== 质量检查 ===================== -ci: lint test vet build ## 本地运行与 CI 完全一致的检查 +ci: lint test vet build proto-check theme-package-check theme-check ## 本地运行与 CI 完全一致的检查 pre-commit: lint vet build ## pre-commit hook 调用(跳过耗时的 race 测试) @@ -49,11 +49,17 @@ vet: ## 静态分析 build: ## 编译检查 $(GO) build ./... -# ===================== 前端主题 ===================== +# ===================== 主题包 ===================== -theme: ## 构建前端主题包并生成 DevServer 用 theme.css - cd frontend && npm run build - node --input-type=module -e "import{generateThemeCSS}from'./frontend/dist/css.js';process.stdout.write(generateThemeCSS())" > devserver/static/theme.css +theme-package-build: ## 构建 AirGate 主题包 + cd theme && npm run build + +theme-package-check: theme-package-build ## 校验 AirGate 主题包构建产物无漂移 + @git diff --exit-code -- theme/dist + @echo "AirGate 主题包构建产物无漂移" + +theme: theme-package-build ## 构建 AirGate 主题包并生成 DevServer 用 theme.css + node --input-type=module -e "import{generateThemeCSS}from'./theme/dist/css.js';process.stdout.write(generateThemeCSS() + '\n')" > devkit/devserver/static/theme.css @echo "theme.css 已生成" # ===================== 代码生成 ===================== @@ -79,12 +85,20 @@ proto-tools: ## 安装指定版本的 protoc 和 Go 插件 @echo "protoc-gen-go / protoc-gen-go-grpc 已就绪" proto: proto-tools ## 重新生成 protobuf 代码 - @cd proto && PATH=$(TOOLS_DIR)/bin:$$PATH $(PROTOC_BIN) \ + @cd protocol/proto && PATH=$(TOOLS_DIR)/bin:$$PATH $(PROTOC_BIN) \ --go_out=. --go_opt=paths=source_relative \ --go-grpc_out=. --go-grpc_opt=paths=source_relative \ plugin.proto @echo "Proto 代码生成完成" +proto-check: proto ## 校验 protobuf 生成代码无漂移 + @git diff --exit-code -- protocol/proto/plugin.pb.go protocol/proto/plugin_grpc.pb.go protocol/proto/plugin.proto + @echo "Proto 代码无漂移" + +theme-check: theme ## 校验 DevServer 主题 CSS 无漂移 + @git diff --exit-code -- devkit/devserver/static/theme.css + @echo "theme.css 无漂移" + # ===================== Git Hooks ===================== setup-hooks: ## 安装 Git pre-commit hook diff --git a/README.md b/README.md index d6d9c2e..745ab82 100644 --- a/README.md +++ b/README.md @@ -1,208 +1,122 @@

AirGate SDK

-

AirGate 插件生态的接口契约与开发套件

+

AirGate 插件生态的公共契约与开发工具包

- release - godoc - license - go - grpc + 发布版本 + Go 文档 + 许可证 + Go 版本 + gRPC 插件协议

--- -AirGate SDK 是 [airgate-core](https://github.com/DouDOU-start/airgate-core) 插件生态的**协议层**,定义了插件与 core 之间的全部边界:接口契约、共享类型、gRPC 桥接、本地开发服务器和统一前端主题。 +AirGate SDK 是 [AirGate Core](https://github.com/DouDOU-start/airgate-core) 和插件之间的公共契约。它定义插件要实现什么接口、Core 如何启动插件进程、双方如何通过 gRPC 通信,以及插件前端如何复用统一主题和公共组件。 -- **Core** = 用户、账号、调度、计费、限流、订阅、管理后台 —— 平台无关的通用能力 -- **SDK**(本仓库)= 插件如何被装载、被调度、被回调的全部规则 -- **Plugin** = 依赖 SDK 实现接口的独立 Go 进程,提供具体平台的能力 +简单理解: -同一份契约在 core 和插件两端使用,保证升级不会偏离。底层走 [hashicorp/go-plugin](https://github.com/hashicorp/go-plugin) 的 gRPC 模式,每个插件运行在自己的子进程里,崩溃不影响 core 与其他插件。 +- **Core** 负责用户、账号、调度、限流、插件管理、记录存储和管理后台。 +- **SDK** 定义接口、协议、运行时适配、本地开发工具和前端基础包。 +- **Plugin** 是独立 Go 进程,依赖 SDK 实现具体平台或扩展能力。 + +## 安装 ```bash go get github.com/DouDOU-start/airgate-sdk@latest ``` -## ✨ 核心特性 - -- **🔌 三类插件模型** — `GatewayPlugin`(upstream 适配)/ `ExtensionPlugin`(路由 + 后台任务)/ `MiddlewarePlugin`(forward 路径拦截层,旁路观察 / 审计 / 脱敏),详见 [ADR-0001](docs/adr/0001-plugin-capability-and-isolation-model.md) -- **🛂 能力模型** — 插件显式声明所需的 HostService / Middleware capability,Core 侧 gRPC interceptor 按"插件类型 → 允许集合"做最小权限校验(SDK `0.3.0` 起强制) -- **🔁 反向通道 HostService** — 插件通过 `HostAware` 可选接口拿到 `Host`,直接回调 core 能力(选号 / 探测 / 列分组),走 hashicorp/go-plugin GRPCBroker 子进程隧道,无需 admin HTTP + API key -- **🧩 最小契约** — 插件只需声明账号格式 / 模型 / 路由,并实现 `Forward`,core 自动接管账号管理、调度、计费、限流 -- **🎨 前端集成** — 独立页面 (`FrontendPages`) + 组件嵌入 (`FrontendWidgets`),通过 `WebAssetsProvider` 统一打包到二进制 -- **🎭 统一主题** — 内置 `@airgate/theme` 包提供共享 token、亮暗切换、Tailwind 桥接和插件作用域隔离 -- **🛠 本地开发服务器** — `devserver` 包模拟 core 行为,**插件无需部署 core 即可端到端测试**账号、HTTP/SSE 转发、WebSocket -- **📦 进程隔离** — 基于 hashicorp/go-plugin gRPC 模式,崩溃隔离、独立热更、独立发版 - -## 🧩 三类插件 - -| 类型 | 接口 | 定位 | 参考实现 | -|---|---|---|---| -| **网关插件** | `GatewayPlugin` | AI API 代理。声明模型/路由/账号格式 + 实现 `Forward`,core 自动调度 + 计费 + 限流 | [airgate-openai](https://github.com/DouDOU-start/airgate-openai) | -| **扩展插件** | `ExtensionPlugin` | 一切非网关场景。提供路由注册、数据库迁移、后台任务三大基础能力 | [airgate-epay](https://github.com/DouDOU-start/airgate-epay) · [airgate-health](https://github.com/DouDOU-start/airgate-health) | -| **中间件插件** | `MiddlewarePlugin` | Forward 路径的旁路拦截层:请求/响应记录、审计、脱敏、流量采样、合规标签注入 | (示例计划:`airgate-audit`) | - -三种角色的边界是**互斥**的:gateway **替代** upstream;extension **并行** 扩展(独立路由表 / 定时任务);middleware **拦截** 每次 forward 的前后事件,**永远不能 block 生产流量**(详见 Decision 2 的失败语义)。 - -### 网关插件 `GatewayPlugin` - -| 方法 | 职责 | -|---|---| -| `Platform()` | 返回业务平台键(如 `"openai"`) | -| `Models()` | 声明支持的模型 + 单价(core 用于计费) | -| `Routes()` | 声明 API 端点(如 `POST /v1/chat/completions`),core 自动注册 | -| `Forward(ctx, req)` | 拿到 core 调度好的账号,转发请求并返回 token 用量 + 账号状态反馈 | -| `ValidateAccount(ctx, cred)` | 添加/导入账号时由 core 调用验证凭证 | -| `QueryQuota(ctx, cred)` | core 定时巡检账号额度 | -| `HandleWebSocket(ctx, conn)` | 处理 WebSocket 双向通信(如 Realtime API) | - -### 扩展插件 `ExtensionPlugin` - -| 能力 | 方法 | 说明 | -|---|---|---| -| 自定义路由 | `RegisterRoutes(r)` | 注册任意 HTTP API | -| 数据库迁移 | `Migrate()` | 创建插件专属表(通过 Config 获取 DSN 自行建连) | -| 后台任务 | `BackgroundTasks()` | 声明定时任务,core 负责调度 | - -### 中间件插件 `MiddlewarePlugin` - -| 方法 | 职责 | -|---|---| -| `OnForwardBegin(ctx, req)` | 选完账号 / 还没调 upstream 之前触发。返回 `Decision` 可放行 / 拒绝 / 追加 header | -| `OnForwardEnd(ctx, evt)` | upstream 返回之后 / 写 usage_log 之前触发。拿到完整的请求 + 响应元数据 | - -**关键设计约定**(详见 [ADR-0001 Decision 2/3](docs/adr/0001-plugin-capability-and-isolation-model.md)): - -- **失败即跳过**:`OnForwardBegin` / `OnForwardEnd` 返回 `error` 只 log warn,不阻塞主流程。唯一例外是 `OnForwardBegin` 显式返回 `DecisionDeny` -- **LIFO 链顺序**:多个 middleware 按 `PluginInfo.Priority` 升序调 Begin、**降序**调 End(像 middleware stack 展开) -- **Payload 两段式**:默认只传元数据(`request_id` / `user_id` / `group_id` / `account_id` / `platform` / `model` / 用量);声明 `CapabilityMiddlewareReadBody` 的插件额外收到 `request_body` / `response_body` + headers -- **流式响应的 body 摘要**:End 阶段流式响应的 `ResponseBody` 只给首次非空 chunk 拼装的摘要,完整流式内容留给未来的 `OnStreamChunk`(ADR-0002) -- **跨 hook 上下文**:`Metadata` 字段是所有 middleware 共享的 KV bag,从 Begin 贯穿到 End - -### 可选能力 - -所有插件类型都可额外实现以下接口,core 通过类型断言自动检测: - -| 接口 | 用途 | -|---|---| -| `WebAssetsProvider` | 提供前端静态资源(独立页面 / 嵌入组件) | -| `ConfigWatcher` | 配置热更新 | -| `HealthChecker` | 自定义健康检查逻辑 | -| `RequestHandler` | 处理 `/api/v1/admin/plugins/:name/rpc/*` 透传请求 | -| `HostAware` | 通过 `ctx.(sdk.HostAware).Host()` 拿到反向调用 core 的 `Host` 客户端 | - -## 🛂 能力模型(Capability) - -`SDKVersion = "0.3.0"` 起,插件调用 `HostService` 或使用 middleware 特殊 payload 必须**显式声明** capability,否则 Core 的 gRPC interceptor 会返回 `PermissionDenied`。 +Go 插件通常只需要两个包: ```go -func (p *MyExtension) Info() sdk.PluginInfo { - return sdk.PluginInfo{ - ID: "ext-monitor", - Type: sdk.PluginTypeExtension, - Capabilities: []string{ - sdk.CapabilityHostListGroups, - sdk.CapabilityHostProbeForward, - sdk.CapabilityHostReportAccountResult, - }, - // ... - } -} +import ( + sdk "github.com/DouDOU-start/airgate-sdk/sdkgo" + runtime "github.com/DouDOU-start/airgate-sdk/runtimego/grpc" +) ``` -当前 capability 清单(Core 按"插件类型 → 允许集合"做交集后得到有效权限): - -| Capability | 用途 | 允许的插件类型 | -|---|---|---| -| `host.list_groups` | `Host.ListGroups()` 列出分组 | `extension`, `middleware` | -| `host.select_account` | `Host.SelectAccount()` 走真实调度选号 | `extension` | -| `host.probe_forward` | `Host.ProbeForward()` 黑盒探测 | `extension`(probe 子类) | -| `host.report_account_result` | `Host.ReportAccountResult()` 反馈状态机 | `extension`(probe 子类) | -| `middleware.read_body` | middleware 接收 `request_body` / `response_body` | `middleware` | - -**向后兼容**:SDK `<= 0.2.x` 的旧插件不声明 `Capabilities` 仍然可以跑(通过 `sdk_version` 字段豁免),但管理后台会显示"兼容模式"警告。`>= 0.3.x` 起强制校验。 +前端插件正式使用 npm 公共包: -**命名规范**:`.`。新增 capability 必须在 ADR 里说明语义 / owner / 允许的插件类型。 - -## 🔁 反向通道 HostService - -过去插件要回调 core(列分组、选号、探测)只能走 admin HTTP API + admin key —— 管理员要手工生成 key、插件要拼 URL 签 Bearer、同机两个进程也被迫走完整 HTTP+JSON 栈。`HostService` 通过 hashicorp/go-plugin 的 `GRPCBroker` 为每个插件子进程架起一条**反向 gRPC stream**,子进程隧道天然互信。 - -```go -type MyExtension struct { - host sdk.Host +```json +{ + "dependencies": { + "@doudou-start/airgate-theme": "^1.0.0" + } } +``` -func (p *MyExtension) Init(ctx sdk.PluginContext) error { - // HostAware 是可选接口:旧版 Core / devserver / 测试 mock 都可以不实现 - if h, ok := ctx.(sdk.HostAware); ok { - p.host = h.Host() // 仍可能为 nil,调用方需 nil-check - } - return nil +本地联调 SDK 源码时可以临时改为: + +```json +{ + "dependencies": { + "@doudou-start/airgate-theme": "file:../../airgate-sdk/theme" + } } +``` -func (p *MyExtension) probe(ctx context.Context) { - if p.host == nil { return } +## 仓库结构 - groups, err := p.host.ListGroups(ctx) - if err != nil { /* ... */ } +| 目录 | 用途 | 谁会用 | +|---|---|---| +| `sdkgo/` | Go 插件接口、共享类型、Capability、Host 调用类型 | 插件作者 | +| `protocol/proto/` | `airgate.plugin.v1` protobuf 协议和生成代码 | Core / runtime | +| `runtimego/grpc/` | hashicorp/go-plugin、gRPC bridge、proto 转换、Core 反向调用通道 | 插件入口 / Core 加载器 | +| `devkit/devserver/` | 本地开发服务器,无需启动完整 Core 即可调试插件 | 插件作者 | +| `theme/` | `@doudou-start/airgate-theme`:主题 token、样式隔离、Tailwind helper、公共组件 | 插件前端 | +| `docs/` | 设计边界和前端样式规范 | 维护者 | - for _, g := range groups { - result, _ := p.host.ProbeForward(ctx, sdk.HostProbeForwardRequest{GroupID: g.ID}) - p.host.ReportAccountResult(ctx, result.AccountID, result.Success, result.ErrorMsg) - } -} -``` +普通插件业务代码不直接依赖 `protocol/proto`。 -当前 v1 暴露的 4 个 RPC(克制暴露面,等真实需求再加): +## 插件类型 -| RPC | 语义 | -|---|---| -| `SelectAccount` | 走和真实用户请求完全相同的调度路径选号 | -| `ProbeForward` | 黑盒探测 chat completion:跳过 `usage_log` / 余额扣款,但仍触发 `ReportResult` | -| `ListGroups` | 列出所有分组(id / name / platform / 是否独占 / 倍率) | -| `ReportAccountResult` | 把账号调用结果反馈给 scheduler 的失败计数器 / 状态机 | +| 类型 | 接口 | 用途 | +|---|---|---| +| `gateway` | `sdk.GatewayPlugin` | 接入 AI 平台,声明模型、路由、账号字段,并转发请求 | +| `extension` | `sdk.ExtensionPlugin` | 后台任务、自定义 API、支付、健康监控等非网关能力 | +| `middleware` | `sdk.MiddlewarePlugin` | forward 前后的旁路拦截,例如审计、脱敏、采样、合规标签 | -**设计原则**(详见 [ADR-0001 §2](docs/adr/0001-plugin-capability-and-isolation-model.md)): -- **只加字段不删字段**(protobuf 天然向前兼容) -- **加新 RPC 用新 rpc name**,不 hijack 旧的 -- **新能力必须伴随新 capability flag**,旧插件不声明就不启用 -- **Core 是 trust root**:HostService 所有输入做参数校验,credentials / password_hash / admin key 等敏感字段永远不通过 RPC 流向插件 +## 如何写一个 Gateway 插件 -## 🛠 技术栈 +Gateway 插件的核心工作只有三件事: -| 层 | 技术 | -|---|---| -| 语言 | Go 1.25 | -| 插件协议 | hashicorp/go-plugin (gRPC + protobuf) | -| 序列化 | protobuf v3 | -| 前端主题 | TypeScript · CSS Variables · Tailwind 桥接 | -| 开发服务器 | net/http + 内嵌 HTML 管理 UI | +1. 在 `Info` 中声明插件信息和账号字段。 +2. 在 `Models` / `Routes` 中声明模型和 API 路由。 +3. 在 `Forward` 中把请求转发到真实上游,并返回 `ForwardOutcome`。 -## 🚀 快速开始 +账号管理、添加账号、编辑账号和使用记录页面由插件前端承接。插件通过 `FrontendPages` +/ `FrontendWidgets` 声明入口,通过 `WebAssetsProvider` 提供静态资源;页面需要的数据走 +插件自己的 API,不进入 `GatewayService`。 -### 1. 编写一个最小网关插件 +入口代码: ```go package main import ( - "context" - sdk "github.com/DouDOU-start/airgate-sdk" - "github.com/DouDOU-start/airgate-sdk/grpc" + sdk "github.com/DouDOU-start/airgate-sdk/sdkgo" + runtime "github.com/DouDOU-start/airgate-sdk/runtimego/grpc" ) -type MyGateway struct{} +type Gateway struct{} + +func main() { + runtime.Serve(&Gateway{}) +} +``` -func (g *MyGateway) Info() sdk.PluginInfo { +关键方法。下面只展示接口形状,不是完整可编译文件: + +```go +func (g *Gateway) Info() sdk.PluginInfo { return sdk.PluginInfo{ - ID: "gateway-myplatform", - Name: "My Platform 网关", - Version: "1.0.0", - Type: sdk.PluginTypeGateway, + ID: "gateway-demo", + Name: "Demo Gateway", + Version: "0.1.0", + SDKVersion: sdk.SDKVersion, + Type: sdk.PluginTypeGateway, AccountTypes: []sdk.AccountType{{ Key: "apikey", Label: "API Key", @@ -213,336 +127,256 @@ func (g *MyGateway) Info() sdk.PluginInfo { } } -func (g *MyGateway) Init(ctx sdk.PluginContext) error { return nil } -func (g *MyGateway) Start(_ context.Context) error { return nil } -func (g *MyGateway) Stop(_ context.Context) error { return nil } +func (g *Gateway) Platform() string { return "demo" } -func (g *MyGateway) Platform() string { return "myplatform" } - -func (g *MyGateway) Models() []sdk.ModelInfo { +func (g *Gateway) Models() []sdk.ModelInfo { return []sdk.ModelInfo{{ - ID: "my-model-v1", Name: "My Model V1", - ContextWindow: 128000, MaxOutputTokens: 16384, - InputPrice: 1.0, OutputPrice: 3.0, + ID: "demo-model", + Name: "Demo Model", + ContextWindow: 128000, + MaxOutputTokens: 8192, + Capabilities: []string{sdk.ModelCapChat}, }} } -func (g *MyGateway) Routes() []sdk.RouteDefinition { - return []sdk.RouteDefinition{ - {Method: "POST", Path: "/v1/chat/completions"}, - } +func (g *Gateway) Routes() []sdk.RouteDefinition { + return []sdk.RouteDefinition{{Method: "POST", Path: "/v1/chat/completions"}} } -func (g *MyGateway) Forward(ctx context.Context, req *sdk.ForwardRequest) (*sdk.ForwardResult, error) { - // req.Account — Core 已调度好的账号 - // req.Body / req.Headers — 原始请求 - // req.Writer — 流式写入 SSE - return &sdk.ForwardResult{ - StatusCode: 200, - InputTokens: 100, OutputTokens: 50, - InputCost: 0.0001, OutputCost: 0.00015, - Model: "my-model-v1", +func (g *Gateway) Forward(ctx context.Context, req *sdk.ForwardRequest) (sdk.ForwardOutcome, error) { + // 这里请求真实上游;示例用固定响应代替。 + body := []byte(`{"id":"demo","choices":[]}`) + headers := http.Header{"Content-Type": []string{"application/json"}} + if req.Stream && req.Writer != nil { + for key, values := range headers { + for _, value := range values { + req.Writer.Header().Add(key, value) + } + } + req.Writer.WriteHeader(http.StatusOK) + _, _ = req.Writer.Write(body) + } + + return sdk.ForwardOutcome{ + Kind: sdk.OutcomeSuccess, + Upstream: sdk.UpstreamResponse{ + StatusCode: http.StatusOK, + Headers: headers, + Body: body, + }, + Usage: &sdk.Usage{ + Model: "demo-model", + AccountCost: 0.000035, + Currency: "USD", + Summary: "输入 10 token,输出 5 token", + Attributes: []sdk.UsageAttribute{ + {Key: "reasoning_effort", Label: "思考层级", Kind: "reasoning", Value: "high"}, + {Key: "resolution", Label: "分辨率", Kind: "resolution", Value: "1024x1024"}, + }, + Metrics: []sdk.UsageMetric{ + {Key: "input_tokens", Label: "输入 token", Kind: "token", Unit: "token", Value: 10}, + {Key: "output_tokens", Label: "输出 token", Kind: "token", Unit: "token", Value: 5}, + }, + CostDetails: []sdk.UsageCostDetail{ + {Key: "input", Label: "输入费用", AccountCost: 0.00001, Currency: "USD"}, + {Key: "output", Label: "输出费用", AccountCost: 0.000025, Currency: "USD"}, + }, + }, }, nil } +``` -func (g *MyGateway) ValidateAccount(ctx context.Context, cred map[string]string) error { return nil } -func (g *MyGateway) QueryQuota(ctx context.Context, cred map[string]string) (*sdk.QuotaInfo, error) { - return nil, sdk.ErrNotSupported -} -func (g *MyGateway) HandleWebSocket(ctx context.Context, conn sdk.WebSocketConn) (*sdk.ForwardResult, error) { - return nil, sdk.ErrNotSupported -} +完整 `GatewayPlugin` 还需要实现: -func main() { grpc.Serve(&MyGateway{}) } -``` +| 方法 | 用途 | +|---|---| +| `Init` / `Start` / `Stop` | 插件生命周期 | +| `ValidateAccount` | 添加账号时验证凭证 | +| `HandleWebSocket` | 处理 WebSocket;不支持时返回 `sdk.ErrNotSupported` | -### 2. 本地开发验证(无需部署 core) +本地调试使用 devserver: ```go package main -import ( - "log" - "github.com/DouDOU-start/airgate-sdk/devserver" -) +import "github.com/DouDOU-start/airgate-sdk/devkit/devserver" func main() { - if err := devserver.Run(devserver.Config{Plugin: &MyGateway{}}); err != nil { - log.Fatal(err) + if err := devserver.Run(devserver.Config{Plugin: &Gateway{}}); err != nil { + panic(err) } } ``` -启动后访问 `http://localhost:18080`,即可看到管理 UI,支持账号 CRUD、HTTP/SSE 代理转发、WebSocket 升级、插件前端资源服务。命令行参数 `-addr` / `-data` / `-log` 可覆盖默认配置。 - -### 3. 构建与发布 - -```bash -go build -o my-plugin . -# 打包:my-plugin.tar.gz 包含二进制 + plugin.yaml -``` - -完整范例(含 Makefile / release workflow / 前端嵌入)见 [airgate-openai](https://github.com/DouDOU-start/airgate-openai)。 - -## 🏗 架构 - -```text -┌─────────────────────── Core ────────────────────────┐ -│ 账号管理 / 调度 / 计费 / 限流 / 订阅 / 管理后台 │ -│ │ -│ ┌──────────────┐ ┌──────────────┐ ┌────────────┐ │ -│ │ PluginService│ │GatewayService│ │ ExtService │ │ Core → Plugin -│ │ Middleware- │ │ │ │ │ │ -│ │ Service │ │ │ │ │ │ -│ └──────────────┘ └──────────────┘ └────────────┘ │ -│ ▲ │ -│ │ HostService(反向 stream,经 GRPCBroker) │ Plugin → Core -└─────────┼────────────────────────────────────────────┘ - │ - ┌───────┴────────────────────────────────────────┐ - │ Plugin subprocess (hashicorp/go-plugin) │ - │ │ - │ GatewayPlugin / ExtensionPlugin / MiddlewarePl │ - │ Capabilities: [host.list_groups, ...] │ - └─────────────────────────────────────────────────┘ -``` +## 运行原理 -**请求生命周期(含 middleware chain)**: +AirGate 的插件运行链路如下: ```text -用户请求 - │ - ▼ -Core 鉴权 + 限流 + 选账号 - │ - ▼ -Middleware.OnForwardBegin (按 Priority 升序依次调) - │ ├─ Decision=Allow → 继续 - │ ├─ Decision=Mutate → 追加 header 继续 - │ └─ Decision=Deny → 直接返回给用户 - ▼ -Gateway.Forward() ──► 上游 AI API - │ - ▼ -Middleware.OnForwardEnd (按 Priority 降序依次调,LIFO) - │ 拿到完整 metadata + 按需 body - ▼ -Core 写 usage_log / 计费 / 账号状态处置 +Core 启动插件子进程 + -> runtimego/grpc 完成 go-plugin handshake + -> Core 调用 Info / Init / Start 获取插件信息 + -> Core 挂载插件页面、静态资源、schema、健康检查和 API 代理 + -> Core 按插件类型注册网关、middleware、扩展路由、迁移、后台任务或事件订阅 + -> 用户请求进入 Core + -> Core 完成鉴权、限流和账号调度 + -> Core 调用 Gateway.Forward + -> 插件请求上游并返回 ForwardOutcome + -> Core 存储插件返回的 usage/account_cost,按倍率计算用户扣费,更新账号状态,返回用户响应 ``` -**反向调用(插件 → Core)**: +关键边界: -```text -Plugin.probe() - └─ ctx.(HostAware).Host().ListGroups(ctx) - │ - │ gRPC stream (GRPCBroker 子进程隧道,无需 admin key) - ▼ - Core: HostService interceptor - │ - │ 检查 plugin capability set - │ 未声明 → PermissionDenied - ▼ - Core: groupRepo.List() -``` +- Core 默认只管理插件生命周期、页面入口、静态资源、schema、健康检查和 API 代理。 +- Gateway 插件是主请求链路,Core 会主动调用 `Forward`、账号验证和 WebSocket 处理。 +- SDK 不内置平台计费规则;网关插件计算标准账号成本并填入 `Usage.AccountCost`、`Usage.Attributes`、`Usage.Metrics`、`Usage.CostDetails`。 +- Core 统一入库后,根据用户、分组、模型等倍率写入 `UserCost` / `BillingMultiplier`;倍率规则不进入 SDK。 +- 账号管理和使用记录 UI 由插件提供静态资源,Core 只加载页面、插槽和插件 API 代理,不解释平台字段。 +- Middleware、扩展路由、后台任务、事件订阅、异步任务处理都属于插件显式暴露的能力;没有暴露就不会被 Core 调度。 +- Extension 插件做独立业务扩展,业务入口来自页面、插件 API、后台任务或事件,不应绕过 Core 直接访问核心业务库;需要 Core 能力时通过 `Host.Invoke` 或 `Host.InvokeStream` 回调。 -**账号模型**:Core 用一张 `accounts` 表存所有平台账号,靠 `platform` + `type` 区分。SDK `Account` 是 core 传给插件的**最小视图**,只包含 `ID / Name / Platform / Type / Credentials / ProxyURL` —— 调度和计费参数全部留在 core。 +## 插件回调 Core -## 🎨 前端集成 +插件要回调 Core 能力时,通过 `Host.Invoke` 或 `Host.InvokeStream` 调用。SDK 不为 Core 方法定义强类型接口;Core 自己维护方法注册表,并在加载和调用时校验插件权限、插件类型、请求 schema、是否允许流式和幂等规则。 -插件的前端能力分两种,通过同一套 `WebAssetsProvider` 资源机制提供: +这意味着后续扩展 Core 能力通常不需要改 SDK: -| 模式 | 说明 | 谁控制布局 | -|---|---|---| -| **独立页面** `FrontendPages` | 插件拥有完整页面,core 分配路由和导航入口 | 插件 | -| **组件嵌入** `FrontendWidgets` | 插件提供组件片段,嵌入 core 已有页面的指定 Slot | Core | +- Core 新增 method,例如 `scheduler.select_account`、`tasks.update`、`notifications.publish`。 +- 插件声明 `host.invoke`,必要时再声明 `host.invoke.` 做细粒度授权。 +- 普通调用使用 `Invoke`,通过 `Payload` 传 JSON 对象语义的参数,通过 `Response.Payload` 读取结果。 +- 流式调用使用 `InvokeStream`,通过 `HostStreamFrame` 双向传递 chunk、progress、result 等帧。 ```go -// 独立页面 -FrontendPages: []sdk.FrontendPage{ - {Path: "/dashboard", Title: "仪表盘", Icon: "chart"}, -}, - -// 嵌入到 core 账号管理页的指定插槽 -FrontendWidgets: []sdk.FrontendWidget{ - {Slot: sdk.SlotAccountForm, EntryFile: "widgets/account-form.js"}, - {Slot: sdk.SlotAccountDetail, EntryFile: "widgets/account-detail.js"}, -}, +Capabilities: []sdk.Capability{ + sdk.CapabilityHostInvoke, + sdk.CapabilityForHostMethod("tasks.update"), +} ``` -**宿主边界**:Core 拥有路由、导航、弹窗骨架、Slot 位置和生命周期;Widget 只渲染 slot 内部内容,不假设控制整个页面。详见 [docs/plugin-style-guide.md](docs/plugin-style-guide.md)。 +插件在 `Init` 中获取 Host: -## 🎭 主题系统 `@airgate/theme` +```go +func (p *Plugin) Init(ctx sdk.PluginContext) error { + if h, ok := ctx.(sdk.HostAware); ok { + p.host = h.Host() + } + return nil +} +``` -SDK 在 `frontend/` 目录提供统一的前端主题包,作为 core 和所有插件的颜色/样式**唯一来源**,支持亮暗切换。 +调用 Core 方法: -```json -// 插件 package.json -{ "dependencies": { "@airgate/theme": "file:../../airgate-sdk/frontend" } } +```go +resp, err := p.host.Invoke(ctx, sdk.HostInvokeRequest{ + Method: "tasks.update", + Payload: map[string]interface{}{ + "task_id": taskID, + "status": sdk.TaskStatusCompleted.String(), + "progress": 100, + }, +}) ``` -```typescript -import { cssVar, themeStyle } from '@airgate/theme'; +调用 Core 流式方法: + +```go +stream, err := p.host.InvokeStream(ctx, sdk.HostStreamRequest{ + Method: "chat.stream", + Payload: map[string]interface{}{"prompt": "hello"}, +}) +if err != nil { + return err +} +defer stream.CloseSend() -color: cssVar('text') // → 'var(--ag-text, #e8ecf4)' -backgroundColor: cssVar('bgSurface') // → 'var(--ag-bg-surface, #1c2237)' +for { + frame, err := stream.Recv() + if err == io.EOF { + break + } + if err != nil { + return err + } + if frame.Done { + break + } + // 使用 frame.Event / frame.Payload 处理流式数据。 +} ``` -`@airgate/theme/plugin` 子包额外提供:`ensurePluginStyleFoundation()` 主题注入、`useScopedPluginTheme()` 亮暗跟随、`createPluginTailwindConfig()` Tailwind 桥接,以及 `Field` / `TextInput` / `Button` 等基础 primitives。 +当前内置 capability: -| 规范 | 说明 | +| Capability | 用途 | |---|---| -| 唯一 token 源 | 颜色/阴影/圆角/字体统一来自 `@airgate/theme` | -| 作用域隔离 | 插件根节点必须用自己的 scope selector,Tailwind 配 `important` | -| 不覆盖宿主骨架 | 插件不得重写 core Modal / Page / Sidebar 全局样式 | -| 亮暗天然可用 | 不写死颜色,所有前景/背景/边框走 token | +| `host.invoke` | 允许插件调用 Core 开放的方法 | +| `host.invoke.` | Core method 级细粒度授权,例如 `host.invoke.tasks.update` | +| `middleware.read_body` | middleware 接收请求/响应 body | -## 📁 项目结构 +完整规则见 [SDK 包边界](docs/sdk-package-boundaries.md)。 -```text -airgate-sdk/ -├── plugin.go # Plugin 基础接口 + PluginInfo + Capability 常量 + 可选接口 -├── gateway.go # GatewayPlugin 接口 -├── extension.go # ExtensionPlugin 接口 -├── middleware.go # MiddlewarePlugin 接口 + MiddlewareRequest/Event/Decision -├── host.go # HostService 客户端接口(反向通道)+ HostAware 可选接口 -├── models.go # 共享类型:Account / ForwardRequest / ForwardResult -├── billing.go # 计费相关类型 + 账号用量视图 -├── errors.go # 标准错误(ErrNotSupported 等) -├── log.go # 日志桥接 -├── grpc/ # gRPC 桥接层(hashicorp/go-plugin 适配) -│ ├── go_plugin.go # Serve() 入口 + GRPCBroker 反向 stream -│ ├── host_client.go # 插件侧的 HostService 客户端封装 -│ ├── middleware_*.go # MiddlewareService client / server -│ └── *_client.go # 各插件类型的 client / server -├── devserver/ # 本地开发服务器 -│ ├── server.go # Config + Run() 入口 -│ ├── accounts.go # 账号 CRUD(JSON 文件持久化) -│ ├── proxy.go # HTTP / SSE / WebSocket 代理 -│ └── static/ # 内嵌管理 UI -├── frontend/ # @airgate/theme + @airgate/theme/plugin -├── proto/ # protobuf 定义(5 个 service: Plugin/Gateway/Extension/Middleware/Host) -└── docs/ - ├── adr/ # 架构决策记录(ADR-0001 起) - └── plugin-style-guide.md -``` +## 插件私有数据 -**推荐的插件项目结构**: +插件私有数据库使用 `plugin_dsn`。Core 注入的 DSN 指向插件独立 schema,插件不得读取 Core 业务库 DSN。 -```text -my-plugin/ -├── backend/ -│ ├── main.go # gRPC 入口(grpc.Serve(...)) -│ ├── cmd/devserver/main.go # 开发入口(约 20 行) -│ └── internal/gateway/ # 接口实现 -├── web/ # 前端源码(可选) -│ ├── src/{pages,widgets}/ -│ └── dist/ # 构建产物(go:embed 打入二进制) -├── .github/workflows/ # ci.yml + release.yml -├── Makefile -└── plugin.yaml # 由代码生成的分发文件 +```go +func (p *Plugin) Init(ctx sdk.PluginContext) error { + dsn := sdk.GetPluginDSN(ctx) + if dsn == "" { + return nil + } + // 使用插件私有 schema 建表和读写数据。 + return nil +} ``` -## 📦 打包与发布 - -`plugin.yaml` 是由插件代码生成的**分发文件**,仅用于安装和市场展示。**运行时真相始终在插件代码里**,core 不依赖 `plugin.yaml` 做运行时决策。 - -```yaml -id: gateway-myplatform -name: My Platform 网关 -version: 1.0.0 -type: gateway -min_core_version: "1.0.0" -platform: myplatform -routes: - - { method: POST, path: /v1/chat/completions } -models: - - { id: my-model-v1, name: My Model V1, input_price: 1.0, output_price: 3.0 } -account_types: - - key: apikey - fields: - - { key: api_key, label: API Key, type: password, required: true } -``` +## 前端插件 SDK -**打包格式**: +`theme/` 发布为 npm 公共包 `@doudou-start/airgate-theme`,用于插件前端复用 AirGate 的主题和公共组件。 -```text -my-plugin.tar.gz -├── my-plugin # 插件二进制(前端资源已 go:embed 打入) -└── plugin.yaml # 分发元信息 +| 入口 | 用途 | +|---|---| +| `@doudou-start/airgate-theme` | token、CSS 工具、Tailwind bridge、插件前端类型和公共组件统一出口 | +| `@doudou-start/airgate-theme/plugin` | 插件样式隔离、主题同步、Tailwind helper、公共 UI 组件 | +| `@doudou-start/airgate-theme/css` | CSS 变量生成和运行时主题注入 | +| `@doudou-start/airgate-theme/tailwind` | Tailwind 主题桥接 | + +推荐插件前端使用: + +```tsx +import { + Button, + Field, + SecretInput, + createPluginTailwindConfig, + ensurePluginStyleFoundation, + useScopedPluginTheme, +} from "@doudou-start/airgate-theme/plugin"; ``` -**发布检查清单**: +完整样式规则见 [插件前端样式规范](docs/plugin-style-guide.md)。 -- [ ] `go test ./...` / `go vet ./...` 通过 -- [ ] 重新生成最新 `plugin.yaml` -- [ ] 构建多架构二进制(amd64 / arm64) -- [ ] 如有前端,构建并嵌入 `dist/` -- [ ] 打包并验证完整性 - -## 🔧 SDK 开发工具 - -```bash -make lint # 代码检查 -make fmt # 代码格式化 -make test # 运行测试 -make proto # 重新生成 protobuf 代码 -``` +网关插件账号相关 UI 建议使用这些稳定插槽。Core 仍提供通用账号列表和详情框架, +插件只补平台差异片段;需要完整独立页面时使用 `FrontendPages`。devserver +可直接预览 `account-create` / `account-edit`,其他插槽由 Core 对应页面加载。 -## 👀 给 Core 开发者 +| Slot | 用途 | +|---|---| +| `account-identity` | 账号标识、套餐、状态等平台差异信息 | +| `account-create` | 添加账号 | +| `account-edit` | 编辑账号 | +| `account-usage-window` | 账号用量窗口、额度、重置时间等平台差异信息 | +| `usage-metric-detail` | 使用记录里的计量明细,例如 token、模型、思考层级、分辨率、图片张数 | +| `usage-cost-detail` | 使用记录里的费用明细,例如单价、账号成本、Core 倍率、用户扣费 | -Core 启动插件后的消费流程: +## 开发命令 -```text -启动插件进程(go-plugin) - → 通过 GRPCBroker 注册 HostService 反向 stream(若启用) - → Info() 获取元信息(ID、类型、Capabilities、账号格式、前端声明) - → Capability 校验: - 有效集 = Info.Capabilities ∩ 插件类型允许集合 - 注册到 HostService interceptor 的 per-plugin context - → Init(ctx) 注入 config + log_level + host_broker_id - → Start(ctx) - -Gateway 插件专属: - → Platform() / Models() / Routes() / GetWebAssets() - → ValidateAccount(ctx, cred) 添加账号时 - → QueryQuota(ctx, cred) 定时巡检 - -Extension 插件专属: - → Migrate() - → GetBackgroundTasks() + 调度器按 Interval 触发 RunBackgroundTask(name) - → HandleRequest / HandleStreamRequest(/api/v1/admin/plugins/:name/rpc/* 透传) - -HTTP 请求到达时(forward 路径): - → Core 鉴权 + 限流 + 调度账号 - → Middleware.OnForwardBegin(按 Priority 升序;Deny → 直接拒绝) - → Gateway.Forward(ctx, req) - → Middleware.OnForwardEnd(按 Priority 降序,LIFO) - → Core 写 usage_log + 处置账号状态(rate_limited / disabled / expired) - -插件发起反向调用时: - → HostService gRPC interceptor 从 context 取出该插件的 capability set - → 未声明 → status.PermissionDenied - → 放行 → core 业务层处理 +```bash +make ci # 运行 Go、proto、前端和主题漂移检查 +make proto # 重新生成 protocol/proto +make theme # 重新生成 DevServer 主题 CSS +cd theme && npm run build # 构建 @doudou-start/airgate-theme ``` -Core 必须遵守的约定: - -- 以 `PluginInfo.ID` 作为运行时键(API 路径、资源挂载、缓存) -- 以 `Platform()` 作为业务键(账号关联、调度、计费) -- 以插件运行时返回的元信息为准,**不依赖 `plugin.yaml` 做运行时决策** -- 添加账号时必须调用 `ValidateAccount`,验证失败拒绝保存 -- 账号管理 UI 统一由插件 `FrontendWidgets` 渲染,core 不做默认表单生成 -- **middleware 的失败永远不能 block 生产流量**:`OnForwardBegin/End` 返回 error 只 log warn;唯一阻断途径是 `OnForwardBegin` 返回 `Decision{Action: Deny}` -- **capability 校验在 interceptor 层强制**:core 业务代码不应再做 capability 判断(单一真相源) -- 插件**不得**拿到 core 业务数据库的 DSN;core 业务数据一律通过 `HostService` RPC 暴露(详见 [ADR-0001 Decision 1/5](docs/adr/0001-plugin-capability-and-isolation-model.md)) - -## 🤝 贡献 / 反馈 - -- Bug / Feature: [Issues](https://github.com/DouDOU-start/airgate-sdk/issues) -- 主仓库: [airgate-core](https://github.com/DouDOU-start/airgate-core) -- 插件参考实现: [airgate-openai](https://github.com/DouDOU-start/airgate-openai) · [airgate-epay](https://github.com/DouDOU-start/airgate-epay) · [airgate-health](https://github.com/DouDOU-start/airgate-health) - -## 📜 License +## License MIT diff --git a/billing.go b/billing.go deleted file mode 100644 index fca41f8..0000000 --- a/billing.go +++ /dev/null @@ -1,263 +0,0 @@ -package sdk - -import ( - "strings" - "time" -) - -// CostInput 费用计算输入。 -type CostInput struct { - InputTokens int - OutputTokens int - CachedInputTokens int // cache read tokens - CacheCreationTokens int // cache write 总数(= 5m + 1h;breakdown 缺失时作为 5m 计价的兜底) - CacheCreation5mTokens int // cache write(5 分钟 TTL,1.25x input) - CacheCreation1hTokens int // cache write(1 小时 TTL,2.00x input) - ServiceTier string // "priority" 使用优先级价格 -} - -// CostResult 费用计算结果(美元) -type CostResult struct { - InputCost float64 - OutputCost float64 - CachedInputCost float64 // cache read 费用 - CacheCreationCost float64 // cache write 费用 -} - -// TotalCost 返回费用总和 -func (r CostResult) TotalCost() float64 { - return r.InputCost + r.OutputCost + r.CachedInputCost + r.CacheCreationCost -} - -// CalculateCost 根据 token 数和模型价格计算费用 -// ModelInfo 中的价格单位为"每百万 token",此函数自动转换为每 token -// -// 输入约定: -// - input.InputTokens 已经是 **扣除 cached 后** 的非缓存输入(插件负责扣除) -// - input.CachedInputTokens 是命中缓存的 token 数 -// - 完整 input_tokens = InputTokens + CachedInputTokens,长上下文阈值基于此 -// -// 计费顺序(对齐 OpenAI 官方): -// 1. 按 service_tier 选单价:standard / priority(配置价,缺省 ×2) / fast(×2.5) / flex|batch(×0.5) -// 2. 命中长上下文阈值 → 再乘长上下文倍率(input/cached/output 独立系数) -// 3. 三项独立计价后相加,cached 不重复计入 input -func CalculateCost(input CostInput, model ModelInfo) CostResult { - inputPrice := model.InputPrice / 1_000_000 - outputPrice := model.OutputPrice / 1_000_000 - cachedInputPrice := model.CachedInputPrice / 1_000_000 - - // 缓存写入价格(Anthropic 两档 TTL,均基于 input 价格): - // - 5 分钟 TTL:input × 1.25(默认档) - // - 1 小时 TTL:input × 2.00(长效档) - // 未显式配置时按官方倍率兜底。 - cacheCreation5mPrice := model.CacheCreationPrice / 1_000_000 - if model.CacheCreationPrice == 0 && model.InputPrice > 0 { - cacheCreation5mPrice = model.InputPrice * 1.25 / 1_000_000 - } - cacheCreation1hPrice := model.CacheCreation1hPrice / 1_000_000 - if model.CacheCreation1hPrice == 0 && model.InputPrice > 0 { - cacheCreation1hPrice = model.InputPrice * 2.0 / 1_000_000 - } - - tier := strings.ToLower(strings.TrimSpace(input.ServiceTier)) - - switch tier { - case "priority": - // Priority 档:价格约为标准 × 2。优先使用配置单价,未配置兜底 × 2。 - if model.InputPricePriority > 0 { - inputPrice = model.InputPricePriority / 1_000_000 - } else { - inputPrice *= 2.0 - } - if model.OutputPricePriority > 0 { - outputPrice = model.OutputPricePriority / 1_000_000 - } else { - outputPrice *= 2.0 - } - if model.CachedInputPricePriority > 0 { - cachedInputPrice = model.CachedInputPricePriority / 1_000_000 - } else { - cachedInputPrice *= 2.0 - } - cacheCreationMultiplier := 2.0 - if model.InputPrice > 0 && model.InputPricePriority > 0 { - cacheCreationMultiplier = model.InputPricePriority / model.InputPrice - } - cacheCreation5mPrice *= cacheCreationMultiplier - cacheCreation1hPrice *= cacheCreationMultiplier - - case "fast": - // Fast 档:OpenAI Codex 官方独立 tier,价格约为标准 × 2.5。 - if model.InputPriceFast > 0 { - inputPrice = model.InputPriceFast / 1_000_000 - } else { - inputPrice *= 2.5 - } - if model.OutputPriceFast > 0 { - outputPrice = model.OutputPriceFast / 1_000_000 - } else { - outputPrice *= 2.5 - } - if model.CachedInputPriceFast > 0 { - cachedInputPrice = model.CachedInputPriceFast / 1_000_000 - } else { - cachedInputPrice *= 2.5 - } - cacheCreation5mPrice *= 2.5 - cacheCreation1hPrice *= 2.5 - - case "flex", "batch": - // Flex / Batch 档:价格约为标准 × 0.5。优先使用配置单价,未配置兜底 × 0.5。 - if model.InputPriceFlex > 0 { - inputPrice = model.InputPriceFlex / 1_000_000 - } else { - inputPrice *= 0.5 - } - if model.OutputPriceFlex > 0 { - outputPrice = model.OutputPriceFlex / 1_000_000 - } else { - outputPrice *= 0.5 - } - if model.CachedInputPriceFlex > 0 { - cachedInputPrice = model.CachedInputPriceFlex / 1_000_000 - } else { - cachedInputPrice *= 0.5 - } - cacheCreation5mPrice *= 0.5 - cacheCreation1hPrice *= 0.5 - } - - // 长上下文阶梯(仅配置了 LongContextThreshold 的模型启用) - if model.LongContextThreshold > 0 { - fullInput := input.InputTokens + input.CachedInputTokens + input.CacheCreationTokens - if fullInput > model.LongContextThreshold { - if model.LongContextInputMultiplier > 1 { - inputPrice *= model.LongContextInputMultiplier - cacheCreation5mPrice *= model.LongContextInputMultiplier - cacheCreation1hPrice *= model.LongContextInputMultiplier - } - if model.LongContextOutputMultiplier > 1 { - outputPrice *= model.LongContextOutputMultiplier - } - if model.LongContextCachedMultiplier > 1 { - cachedInputPrice *= model.LongContextCachedMultiplier - } - } - } - - // Cache creation 分档计费: - // - 插件透传了 5m/1h 明细 → 按各自单价分别计算 - // - 插件只透传了总数(breakdown 缺失)→ 全部按 5m 默认档计价(向后兼容) - var cacheCreationCost float64 - if input.CacheCreation5mTokens > 0 || input.CacheCreation1hTokens > 0 { - cacheCreationCost = float64(input.CacheCreation5mTokens)*cacheCreation5mPrice + - float64(input.CacheCreation1hTokens)*cacheCreation1hPrice - } else { - cacheCreationCost = float64(input.CacheCreationTokens) * cacheCreation5mPrice - } - - return CostResult{ - InputCost: float64(input.InputTokens) * inputPrice, - OutputCost: float64(input.OutputTokens) * outputPrice, - CachedInputCost: float64(input.CachedInputTokens) * cachedInputPrice, - CacheCreationCost: cacheCreationCost, - } -} - -// AccountUsageWindow 描述账号的单个用量窗口。 -// 插件负责把平台专属窗口语义归一化到这个结构,Core 只做通用展示。 -type AccountUsageWindow struct { - Key string `json:"key,omitempty"` - Label string `json:"label"` - UsedPercent float64 `json:"used_percent"` - ResetAt string `json:"reset_at,omitempty"` - ResetSeconds int `json:"reset_seconds,omitempty"` -} - -// AccountTodayStats 账号当天(本地时区自然日)在 usage_logs 中的聚合统计。 -// 由 Core 基于 usage_logs 计算,插件不需要生成。 -// -// - Requests 请求总数(count) -// - Tokens token 总数(input + output + cache_read + cache_creation) -// - AccountCost 账号成本 = SUM(account_cost)(上游账号的真实消耗,不受用户侧倍率影响) -// - UserCost 用户消耗 = SUM(actual_cost)(扣 User.balance 的金额) -type AccountTodayStats struct { - Requests int64 `json:"requests"` - Tokens int64 `json:"tokens"` - AccountCost float64 `json:"account_cost"` - UserCost float64 `json:"user_cost"` -} - -// AccountUsageCredits 描述账号的额度信息。 -type AccountUsageCredits struct { - Balance float64 `json:"balance"` - Unlimited bool `json:"unlimited"` -} - -// AccountUsageInfo 描述单个账号的通用用量视图。 -// -// TodayStats 是 Core 本地聚合的当天统计(从 usage_logs 按自然日计算), -// 和 Windows 是两码事:Windows 反映上游 quota 百分比,TodayStats 反映 -// 本地 gateway 视角的账号当天真实消耗。 -type AccountUsageInfo struct { - UpdatedAt string `json:"updated_at,omitempty"` - Windows []AccountUsageWindow `json:"windows,omitempty"` - Credits *AccountUsageCredits `json:"credits,omitempty"` - TodayStats *AccountTodayStats `json:"today_stats,omitempty"` -} - -// AccountUsageError 描述插件在探测账号用量时发现的单账号错误。 -type AccountUsageError struct { - ID int64 `json:"id"` - Message string `json:"message"` -} - -// AccountUsageAccountsResponse 是 usage/accounts 之类账号批量用量接口的通用响应。 -type AccountUsageAccountsResponse struct { - Accounts map[string]AccountUsageInfo `json:"accounts"` - Errors []AccountUsageError `json:"errors,omitempty"` -} - -// ResetAtFromBase 根据基准时间和 reset_after_seconds 计算绝对重置时间。 -// 负数会被钳制为 0。 -func ResetAtFromBase(base time.Time, resetAfterSeconds int) *time.Time { - if base.IsZero() { - base = time.Now() - } - sec := resetAfterSeconds - if sec < 0 { - sec = 0 - } - resetAt := base.UTC().Add(time.Duration(sec) * time.Second) - return &resetAt -} - -// RemainingSecondsUntil 返回从 now 到 resetAt 的剩余秒数。 -// 过期或 nil 一律返回 0。 -func RemainingSecondsUntil(resetAt *time.Time, now time.Time) int { - if resetAt == nil { - return 0 - } - if now.IsZero() { - now = time.Now() - } - diff := int(resetAt.UTC().Sub(now.UTC()).Seconds()) - if diff < 0 { - return 0 - } - return diff -} - -// NewAccountUsageWindow 构建通用用量窗口,并同时填充 reset_at / reset_seconds。 -func NewAccountUsageWindow(key, label string, usedPercent float64, resetAt *time.Time, now time.Time) AccountUsageWindow { - window := AccountUsageWindow{ - Key: key, - Label: label, - UsedPercent: usedPercent, - } - if resetAt != nil { - window.ResetAt = resetAt.UTC().Format(time.RFC3339) - window.ResetSeconds = RemainingSecondsUntil(resetAt, now) - } - return window -} diff --git a/billing_test.go b/billing_test.go deleted file mode 100644 index 19d260b..0000000 --- a/billing_test.go +++ /dev/null @@ -1,453 +0,0 @@ -package sdk - -import ( - "math" - "testing" - "time" -) - -// ==================== 费用计算测试 ==================== - -func almostEqual(a, b float64) bool { - return math.Abs(a-b) < 1e-12 -} - -func TestCalculateCost_Standard(t *testing.T) { - model := ModelInfo{ - InputPrice: 3.0, // $3/1M tokens - OutputPrice: 15.0, // $15/1M tokens - CachedInputPrice: 0.3, // $0.3/1M tokens - CacheCreationPrice: 3.75, // $3.75/1M tokens (1.25x input) - } - result := CalculateCost(CostInput{ - InputTokens: 1000, - OutputTokens: 500, - CachedInputTokens: 2000, - CacheCreationTokens: 1000, - }, model) - - if !almostEqual(result.InputCost, 0.003) { - t.Errorf("InputCost = %v, want 0.003", result.InputCost) - } - if !almostEqual(result.OutputCost, 0.0075) { - t.Errorf("OutputCost = %v, want 0.0075", result.OutputCost) - } - if !almostEqual(result.CachedInputCost, 0.0006) { - t.Errorf("CachedInputCost = %v, want 0.0006", result.CachedInputCost) - } - // cache creation: 1000 × 3.75/1e6 = 0.00375 - if !almostEqual(result.CacheCreationCost, 0.00375) { - t.Errorf("CacheCreationCost = %v, want 0.00375", result.CacheCreationCost) - } - // total: 0.003 + 0.0075 + 0.0006 + 0.00375 = 0.01485 - if !almostEqual(result.TotalCost(), 0.01485) { - t.Errorf("TotalCost = %v, want 0.01485", result.TotalCost()) - } -} - -func TestCalculateCost_CacheCreationFallback(t *testing.T) { - // CacheCreationPrice 未配置时,按 input × 1.25 兜底 - model := ModelInfo{ - InputPrice: 3.0, - OutputPrice: 15.0, - CachedInputPrice: 0.3, - // CacheCreationPrice 未设置 - } - result := CalculateCost(CostInput{ - CacheCreationTokens: 1000, - }, model) - - // fallback: 1000 × (3.0 × 1.25 / 1e6) = 1000 × 3.75e-6 = 0.00375 - if !almostEqual(result.CacheCreationCost, 0.00375) { - t.Errorf("CacheCreationCost fallback = %v, want 0.00375", result.CacheCreationCost) - } -} - -func TestCalculateCost_CacheCreation5m1hBreakdown(t *testing.T) { - // 同时有 5m 和 1h breakdown 时分别计价 - // Claude Sonnet: input $3, cache_5m $3.75, cache_1h $6 - model := ModelInfo{ - InputPrice: 3.0, - OutputPrice: 15.0, - CachedInputPrice: 0.3, - CacheCreationPrice: 3.75, // 5m - CacheCreation1hPrice: 6.0, // 1h - } - result := CalculateCost(CostInput{ - CacheCreationTokens: 3000, // 总数 - CacheCreation5mTokens: 2000, - CacheCreation1hTokens: 1000, - }, model) - - // 5m: 2000 × 3.75/1e6 = 0.0075 - // 1h: 1000 × 6.0/1e6 = 0.006 - // 合计: 0.0135 - if !almostEqual(result.CacheCreationCost, 0.0135) { - t.Errorf("CacheCreationCost 5m/1h breakdown = %v, want 0.0135", result.CacheCreationCost) - } -} - -func TestCalculateCost_CacheCreation1hFallback(t *testing.T) { - // 只配了 InputPrice,1h 价格按 input × 2 兜底 - model := ModelInfo{ - InputPrice: 3.0, - } - result := CalculateCost(CostInput{ - CacheCreation1hTokens: 1000, - }, model) - - // fallback: 1000 × (3.0 × 2.0 / 1e6) = 0.006 - if !almostEqual(result.CacheCreationCost, 0.006) { - t.Errorf("CacheCreationCost 1h fallback = %v, want 0.006", result.CacheCreationCost) - } -} - -func TestCalculateCost_CacheCreationBreakdownMissingFallsBackTo5m(t *testing.T) { - // 没有 5m/1h breakdown 时,所有 CacheCreationTokens 按 5m 价格计(向后兼容) - model := ModelInfo{ - InputPrice: 3.0, - CacheCreationPrice: 3.75, - CacheCreation1hPrice: 6.0, - } - result := CalculateCost(CostInput{ - CacheCreationTokens: 1000, - // 5m/1h 未设置 - }, model) - - // 全部按 5m: 1000 × 3.75/1e6 = 0.00375 - if !almostEqual(result.CacheCreationCost, 0.00375) { - t.Errorf("CacheCreationCost aggregate fallback = %v, want 0.00375", result.CacheCreationCost) - } -} - -func TestCalculateCost_Priority(t *testing.T) { - model := ModelInfo{ - InputPrice: 3.0, - OutputPrice: 15.0, - CachedInputPrice: 0.3, - InputPricePriority: 6.0, // 2x - OutputPricePriority: 30.0, - CachedInputPricePriority: 0.6, - } - result := CalculateCost(CostInput{ - InputTokens: 1000, - OutputTokens: 500, - ServiceTier: "priority", - }, model) - - if !almostEqual(result.InputCost, 0.006) { - t.Errorf("InputCost = %v, want 0.006", result.InputCost) - } - if !almostEqual(result.OutputCost, 0.015) { - t.Errorf("OutputCost = %v, want 0.015", result.OutputCost) - } -} - -func TestCalculateCost_PriorityFallbackDoublesStandard(t *testing.T) { - // priority 价格未配置时按标准 × 2 兜底(对齐 OpenAI 官方 priority 档规则) - model := ModelInfo{ - InputPrice: 3.0, - OutputPrice: 15.0, - CachedInputPrice: 0.3, - } - result := CalculateCost(CostInput{ - InputTokens: 1000, - OutputTokens: 500, - CachedInputTokens: 2000, - ServiceTier: "priority", - }, model) - - if !almostEqual(result.InputCost, 0.006) { - t.Errorf("InputCost = %v, want 0.006 (2× fallback)", result.InputCost) - } - if !almostEqual(result.OutputCost, 0.015) { - t.Errorf("OutputCost = %v, want 0.015 (2× fallback)", result.OutputCost) - } - if !almostEqual(result.CachedInputCost, 0.0012) { - t.Errorf("CachedInputCost = %v, want 0.0012 (2× fallback)", result.CachedInputCost) - } -} - -func TestCalculateCost_Fast(t *testing.T) { - model := ModelInfo{ - InputPrice: 3.0, - OutputPrice: 15.0, - CachedInputPrice: 0.3, - InputPriceFast: 7.5, - OutputPriceFast: 37.5, - CachedInputPriceFast: 0.75, - } - result := CalculateCost(CostInput{ - InputTokens: 1000, - OutputTokens: 500, - CachedInputTokens: 2000, - ServiceTier: "fast", - }, model) - - if !almostEqual(result.InputCost, 0.0075) { - t.Errorf("InputCost = %v, want 0.0075", result.InputCost) - } - if !almostEqual(result.OutputCost, 0.01875) { - t.Errorf("OutputCost = %v, want 0.01875", result.OutputCost) - } - if !almostEqual(result.CachedInputCost, 0.0015) { - t.Errorf("CachedInputCost = %v, want 0.0015", result.CachedInputCost) - } -} - -func TestCalculateCost_FastFallbackUsesTwoPointFiveX(t *testing.T) { - model := ModelInfo{ - InputPrice: 3.0, - OutputPrice: 15.0, - CachedInputPrice: 0.3, - } - result := CalculateCost(CostInput{ - InputTokens: 1000, - OutputTokens: 500, - CachedInputTokens: 2000, - ServiceTier: "fast", - }, model) - - if !almostEqual(result.InputCost, 0.0075) { - t.Errorf("InputCost = %v, want 0.0075 (2.5× fallback)", result.InputCost) - } - if !almostEqual(result.OutputCost, 0.01875) { - t.Errorf("OutputCost = %v, want 0.01875 (2.5× fallback)", result.OutputCost) - } - if !almostEqual(result.CachedInputCost, 0.0015) { - t.Errorf("CachedInputCost = %v, want 0.0015 (2.5× fallback)", result.CachedInputCost) - } -} - -func TestCalculateCost_Flex(t *testing.T) { - // 显式配置的 flex 单价优先 - model := ModelInfo{ - InputPrice: 2.5, - OutputPrice: 15.0, - CachedInputPrice: 0.25, - InputPriceFlex: 1.25, - OutputPriceFlex: 7.5, - CachedInputPriceFlex: 0.125, - } - result := CalculateCost(CostInput{ - InputTokens: 1000, - OutputTokens: 500, - CachedInputTokens: 2000, - ServiceTier: "flex", - }, model) - - if !almostEqual(result.InputCost, 0.00125) { - t.Errorf("InputCost = %v, want 0.00125", result.InputCost) - } - if !almostEqual(result.OutputCost, 0.00375) { - t.Errorf("OutputCost = %v, want 0.00375", result.OutputCost) - } - if !almostEqual(result.CachedInputCost, 0.00025) { - t.Errorf("CachedInputCost = %v, want 0.00025", result.CachedInputCost) - } -} - -func TestCalculateCost_FlexFallbackHalvesStandard(t *testing.T) { - // flex 价格未配置时按标准 × 0.5 兜底 - model := ModelInfo{ - InputPrice: 2.5, - OutputPrice: 15.0, - CachedInputPrice: 0.25, - } - result := CalculateCost(CostInput{ - InputTokens: 1000, - OutputTokens: 500, - CachedInputTokens: 2000, - ServiceTier: "batch", // batch 与 flex 同价 - }, model) - - if !almostEqual(result.InputCost, 0.00125) { - t.Errorf("InputCost = %v, want 0.00125 (0.5× fallback)", result.InputCost) - } - if !almostEqual(result.OutputCost, 0.00375) { - t.Errorf("OutputCost = %v, want 0.00375 (0.5× fallback)", result.OutputCost) - } - if !almostEqual(result.CachedInputCost, 0.00025) { - t.Errorf("CachedInputCost = %v, want 0.00025 (0.5× fallback)", result.CachedInputCost) - } -} - -func TestCalculateCost_LongContext_Standard(t *testing.T) { - // gpt-5.4 完整 input_tokens = 200k + 80k = 280k > 272k → 长上下文档 - // 标准档 input ×2 / cached ×2 / output ×1.5 - model := ModelInfo{ - InputPrice: 2.5, - OutputPrice: 15.0, - CachedInputPrice: 0.25, - LongContextThreshold: 272_000, - LongContextInputMultiplier: 2.0, - LongContextOutputMultiplier: 1.5, - LongContextCachedMultiplier: 2.0, - } - result := CalculateCost(CostInput{ - InputTokens: 200_000, - CachedInputTokens: 80_000, - OutputTokens: 10_000, - }, model) - - // input: 200000 × (2.5 × 2 / 1e6) = 1.0 - if !almostEqual(result.InputCost, 1.0) { - t.Errorf("InputCost = %v, want 1.0", result.InputCost) - } - // output: 10000 × (15 × 1.5 / 1e6) = 0.225 - if !almostEqual(result.OutputCost, 0.225) { - t.Errorf("OutputCost = %v, want 0.225", result.OutputCost) - } - // cached: 80000 × (0.25 × 2 / 1e6) = 0.04 - if !almostEqual(result.CachedInputCost, 0.04) { - t.Errorf("CachedInputCost = %v, want 0.04", result.CachedInputCost) - } -} - -func TestCalculateCost_LongContext_BelowThreshold(t *testing.T) { - // 完整 input_tokens = 100k + 80k = 180k < 272k → 不触发长上下文 - model := ModelInfo{ - InputPrice: 2.5, - OutputPrice: 15.0, - CachedInputPrice: 0.25, - LongContextThreshold: 272_000, - LongContextInputMultiplier: 2.0, - LongContextOutputMultiplier: 1.5, - LongContextCachedMultiplier: 2.0, - } - result := CalculateCost(CostInput{ - InputTokens: 100_000, - CachedInputTokens: 80_000, - OutputTokens: 10_000, - }, model) - - // 按标准价:100000 × 2.5/1e6 = 0.25 - if !almostEqual(result.InputCost, 0.25) { - t.Errorf("InputCost = %v, want 0.25 (standard)", result.InputCost) - } - // 10000 × 15/1e6 = 0.15 - if !almostEqual(result.OutputCost, 0.15) { - t.Errorf("OutputCost = %v, want 0.15 (standard)", result.OutputCost) - } -} - -func TestCalculateCost_LongContext_PriorityStacks(t *testing.T) { - // priority 档先选 priority 单价,再叠加长上下文倍率 - model := ModelInfo{ - InputPrice: 2.5, - OutputPrice: 15.0, - CachedInputPrice: 0.25, - InputPricePriority: 5.0, - OutputPricePriority: 30.0, - CachedInputPricePriority: 0.5, - LongContextThreshold: 272_000, - LongContextInputMultiplier: 2.0, - LongContextOutputMultiplier: 1.5, - LongContextCachedMultiplier: 2.0, - } - result := CalculateCost(CostInput{ - InputTokens: 200_000, - CachedInputTokens: 80_000, // full = 280k > 272k - OutputTokens: 10_000, - ServiceTier: "priority", - }, model) - - // input: 200000 × (5 × 2 / 1e6) = 2.0 - if !almostEqual(result.InputCost, 2.0) { - t.Errorf("InputCost = %v, want 2.0 (priority + longctx)", result.InputCost) - } - // output: 10000 × (30 × 1.5 / 1e6) = 0.45 - if !almostEqual(result.OutputCost, 0.45) { - t.Errorf("OutputCost = %v, want 0.45 (priority + longctx)", result.OutputCost) - } - // cached: 80000 × (0.5 × 2 / 1e6) = 0.08 - if !almostEqual(result.CachedInputCost, 0.08) { - t.Errorf("CachedInputCost = %v, want 0.08 (priority + longctx)", result.CachedInputCost) - } -} - -func TestCalculateCost_LongContext_FlexStacks(t *testing.T) { - // flex 档:先半价,再叠加长上下文倍率 - model := ModelInfo{ - InputPrice: 2.5, - OutputPrice: 15.0, - CachedInputPrice: 0.25, - LongContextThreshold: 272_000, - LongContextInputMultiplier: 2.0, - LongContextOutputMultiplier: 1.5, - LongContextCachedMultiplier: 2.0, - } - result := CalculateCost(CostInput{ - InputTokens: 200_000, - CachedInputTokens: 80_000, - OutputTokens: 10_000, - ServiceTier: "flex", - }, model) - - // input: 200000 × (2.5 × 0.5 × 2 / 1e6) = 200000 × 2.5e-6 = 0.5 - if !almostEqual(result.InputCost, 0.5) { - t.Errorf("InputCost = %v, want 0.5 (flex + longctx)", result.InputCost) - } - // output: 10000 × (15 × 0.5 × 1.5 / 1e6) = 10000 × 11.25e-6 = 0.1125 - if !almostEqual(result.OutputCost, 0.1125) { - t.Errorf("OutputCost = %v, want 0.1125 (flex + longctx)", result.OutputCost) - } - // cached: 80000 × (0.25 × 0.5 × 2 / 1e6) = 80000 × 0.25e-6 = 0.02 - if !almostEqual(result.CachedInputCost, 0.02) { - t.Errorf("CachedInputCost = %v, want 0.02 (flex + longctx)", result.CachedInputCost) - } -} - -func TestCalculateCost_ZeroTokens(t *testing.T) { - model := ModelInfo{InputPrice: 3.0, OutputPrice: 15.0} - result := CalculateCost(CostInput{}, model) - - if result.TotalCost() != 0 { - t.Errorf("TotalCost = %v, want 0", result.TotalCost()) - } -} - -// ==================== 账号用量测试 ==================== - -func TestResetAtFromBaseClampsNegativeSeconds(t *testing.T) { - base := time.Date(2026, 3, 27, 10, 0, 0, 0, time.UTC) - resetAt := ResetAtFromBase(base, -30) - if resetAt == nil { - t.Fatal("expected resetAt") - return - } - if !resetAt.Equal(base) { - t.Fatalf("expected resetAt=%s, got %s", base, *resetAt) - } -} - -func TestRemainingSecondsUntil(t *testing.T) { - now := time.Date(2026, 3, 27, 10, 0, 0, 0, time.UTC) - resetAt := time.Date(2026, 3, 27, 10, 5, 0, 0, time.UTC) - if got := RemainingSecondsUntil(&resetAt, now); got != 300 { - t.Fatalf("expected 300, got %d", got) - } -} - -func TestRemainingSecondsUntilExpired(t *testing.T) { - now := time.Date(2026, 3, 27, 10, 5, 0, 0, time.UTC) - resetAt := time.Date(2026, 3, 27, 10, 0, 0, 0, time.UTC) - if got := RemainingSecondsUntil(&resetAt, now); got != 0 { - t.Fatalf("expected 0, got %d", got) - } -} - -func TestNewAccountUsageWindow(t *testing.T) { - now := time.Date(2026, 3, 27, 10, 0, 0, 0, time.UTC) - resetAt := time.Date(2026, 3, 27, 10, 5, 0, 0, time.UTC) - window := NewAccountUsageWindow("5h", "5h", 25, &resetAt, now) - if window.Key != "5h" { - t.Fatalf("expected key=5h, got %q", window.Key) - } - if window.ResetAt != "2026-03-27T10:05:00Z" { - t.Fatalf("expected reset_at to be serialized, got %q", window.ResetAt) - } - if window.ResetSeconds != 300 { - t.Fatalf("expected reset_seconds=300, got %d", window.ResetSeconds) - } -} diff --git a/capability_test.go b/capability_test.go deleted file mode 100644 index 9344007..0000000 --- a/capability_test.go +++ /dev/null @@ -1,114 +0,0 @@ -package sdk_test - -import ( - "reflect" - "testing" - - sdk "github.com/DouDOU-start/airgate-sdk" -) - -func TestIsKnownCapability(t *testing.T) { - known := []sdk.Capability{ - sdk.CapabilityHostListGroups, - sdk.CapabilityHostSelectAccount, - sdk.CapabilityHostProbeForward, - sdk.CapabilityHostReportAccountResult, - sdk.CapabilityMiddlewareReadBody, - } - for _, c := range known { - if !sdk.IsKnownCapability(c) { - t.Errorf("IsKnownCapability(%q) = false, want true", c) - } - } - - if sdk.IsKnownCapability("host.totally_made_up") { - t.Error("IsKnownCapability returned true for an unknown capability") - } -} - -func TestKnownCapabilitiesSortedAndComplete(t *testing.T) { - caps := sdk.KnownCapabilities() - if len(caps) < 5 { - t.Fatalf("KnownCapabilities returned %d entries, expected at least 5", len(caps)) - } - for i := 1; i < len(caps); i++ { - if caps[i-1] >= caps[i] { - t.Errorf("KnownCapabilities not sorted: %q >= %q at index %d", caps[i-1], caps[i], i) - } - } -} - -func TestValidateCapabilities_HappyPath(t *testing.T) { - report := sdk.ValidateCapabilities(sdk.PluginTypeExtension, []sdk.Capability{ - sdk.CapabilityHostListGroups, - sdk.CapabilityHostProbeForward, - sdk.CapabilityHostReportAccountResult, - }) - if report.HasIssues() { - t.Errorf("HasIssues() = true, want false; report=%+v", report) - } - want := []sdk.Capability{ - sdk.CapabilityHostListGroups, - sdk.CapabilityHostProbeForward, - sdk.CapabilityHostReportAccountResult, - } - // Effective is sorted, so compare against sorted want - wantSorted := []sdk.Capability{ - sdk.CapabilityHostListGroups, // host.list_groups - sdk.CapabilityHostProbeForward, // host.probe_forward - sdk.CapabilityHostReportAccountResult, // host.report_account_result - } - _ = want - if !reflect.DeepEqual(report.Effective, wantSorted) { - t.Errorf("Effective = %v, want %v", report.Effective, wantSorted) - } -} - -func TestValidateCapabilities_Unknown(t *testing.T) { - report := sdk.ValidateCapabilities(sdk.PluginTypeExtension, []sdk.Capability{ - sdk.CapabilityHostListGroups, - "host.probe_forawrd", // typo of probe_forward - }) - if !report.HasIssues() { - t.Fatal("HasIssues() = false, expected true for unknown capability") - } - if len(report.Unknown) != 1 || report.Unknown[0] != "host.probe_forawrd" { - t.Errorf("Unknown = %v, want [host.probe_forawrd]", report.Unknown) - } - if len(report.Effective) != 1 || report.Effective[0] != sdk.CapabilityHostListGroups { - t.Errorf("Effective = %v, want [%v]", report.Effective, sdk.CapabilityHostListGroups) - } -} - -func TestValidateCapabilities_Denied(t *testing.T) { - // gateway 插件声明了 extension 专属的 capability - report := sdk.ValidateCapabilities(sdk.PluginTypeGateway, []sdk.Capability{ - sdk.CapabilityHostProbeForward, - }) - if !report.HasIssues() { - t.Fatal("HasIssues() = false, expected true for type-denied capability") - } - if len(report.Denied) != 1 || report.Denied[0] != sdk.CapabilityHostProbeForward { - t.Errorf("Denied = %v, want [%v]", report.Denied, sdk.CapabilityHostProbeForward) - } - if len(report.Effective) != 0 { - t.Errorf("Effective = %v, want empty", report.Effective) - } -} - -func TestValidateCapabilities_Dedup(t *testing.T) { - report := sdk.ValidateCapabilities(sdk.PluginTypeExtension, []sdk.Capability{ - sdk.CapabilityHostListGroups, - sdk.CapabilityHostListGroups, // duplicate - sdk.CapabilityHostListGroups, - }) - if len(report.Effective) != 1 { - t.Errorf("Effective = %v, want exactly 1 entry after dedup", report.Effective) - } -} - -func TestGetPluginDSN_NilCtx(t *testing.T) { - if got := sdk.GetPluginDSN(nil); got != "" { - t.Errorf("GetPluginDSN(nil) = %q, want empty", got) - } -} diff --git a/devserver/accounts.go b/devkit/devserver/accounts.go similarity index 100% rename from devserver/accounts.go rename to devkit/devserver/accounts.go diff --git a/devserver/context.go b/devkit/devserver/context.go similarity index 96% rename from devserver/context.go rename to devkit/devserver/context.go index 7f022c8..1f7f016 100644 --- a/devserver/context.go +++ b/devkit/devserver/context.go @@ -5,7 +5,7 @@ import ( "os" "time" - sdk "github.com/DouDOU-start/airgate-sdk" + sdk "github.com/DouDOU-start/airgate-sdk/sdkgo" ) // devPluginContext 开发模式的 PluginContext 实现 diff --git a/devkit/devserver/doc.go b/devkit/devserver/doc.go new file mode 100644 index 0000000..d99fd21 --- /dev/null +++ b/devkit/devserver/doc.go @@ -0,0 +1,5 @@ +// Package devserver 提供本地插件开发服务器。 +// +// 本包用于在不启动完整 airgate-core 的情况下调试网关插件。 +// 生产插件二进制不应依赖 devserver。 +package devserver diff --git a/devserver/logger.go b/devkit/devserver/logger.go similarity index 100% rename from devserver/logger.go rename to devkit/devserver/logger.go diff --git a/devserver/proxy.go b/devkit/devserver/proxy.go similarity index 71% rename from devserver/proxy.go rename to devkit/devserver/proxy.go index 16fa7d4..7bbf369 100644 --- a/devserver/proxy.go +++ b/devkit/devserver/proxy.go @@ -10,7 +10,7 @@ import ( "github.com/gorilla/websocket" - sdk "github.com/DouDOU-start/airgate-sdk" + sdk "github.com/DouDOU-start/airgate-sdk/sdkgo" ) // ProxyHandler 将请求代理给插件 @@ -74,6 +74,13 @@ func (p *ProxyHandler) handleHTTP(w http.ResponseWriter, r *http.Request) { } tried[account.ID] = true + var bufferedWriter *devBufferWriter + responseWriter := w + if !stream { + bufferedWriter = &devBufferWriter{} + responseWriter = bufferedWriter + } + fwdReq := &sdk.ForwardRequest{ Account: &sdk.Account{ ID: account.ID, @@ -83,7 +90,7 @@ func (p *ProxyHandler) handleHTTP(w http.ResponseWriter, r *http.Request) { Body: body, Headers: headers.Clone(), Stream: stream, - Writer: w, + Writer: responseWriter, } outcome, fwdErr := p.plugin.Forward(r.Context(), fwdReq) @@ -102,20 +109,28 @@ func (p *ProxyHandler) handleHTTP(w http.ResponseWriter, r *http.Request) { log.Printf("Forward 失败: %v", fwdErr) if outcome.Upstream.StatusCode == 0 { http.Error(w, `{"error":"`+fwdErr.Error()+`"}`, http.StatusBadGateway) + return + } + if !stream { + writeForwardOutcome(w, outcome, bufferedWriter) } return } - var tokensIn, tokensOut int - var model string + var model, usageSummary, currency string + var accountCost float64 if outcome.Usage != nil { - tokensIn = outcome.Usage.InputTokens - tokensOut = outcome.Usage.OutputTokens model = outcome.Usage.Model + usageSummary = outcome.Usage.Summary + accountCost = outcome.Usage.AccountCost + currency = outcome.Usage.Currency } - log.Printf("Forward 完成: kind=%s status=%d model=%s input=%d output=%d duration=%s account=%d(%s)", - outcome.Kind, outcome.Upstream.StatusCode, model, tokensIn, tokensOut, outcome.Duration, + log.Printf("Forward 完成: kind=%s status=%d model=%s account_cost=%.6f%s usage=%s duration=%s account=%d(%s)", + outcome.Kind, outcome.Upstream.StatusCode, model, accountCost, currency, usageSummary, outcome.Duration, account.ID, account.Name) + if !stream { + writeForwardOutcome(w, outcome, bufferedWriter) + } return } @@ -173,6 +188,61 @@ func (p *ProxyHandler) selectAccount() *DevAccount { return p.store.First() } +type devBufferWriter struct { + headers http.Header + code int + body []byte +} + +func (w *devBufferWriter) Header() http.Header { + if w.headers == nil { + w.headers = make(http.Header) + } + return w.headers +} + +func (w *devBufferWriter) Write(data []byte) (int, error) { + w.body = append(w.body, data...) + return len(data), nil +} + +func (w *devBufferWriter) WriteHeader(statusCode int) { + w.code = statusCode +} + +func writeForwardOutcome(w http.ResponseWriter, outcome sdk.ForwardOutcome, buffered *devBufferWriter) { + statusCode := outcome.Upstream.StatusCode + headers := outcome.Upstream.Headers + body := outcome.Upstream.Body + + if buffered != nil { + if statusCode == 0 && buffered.code > 0 { + statusCode = buffered.code + } + if len(headers) == 0 { + headers = buffered.Header() + } + if len(body) == 0 && len(buffered.body) > 0 { + body = buffered.body + } + } + if statusCode == 0 { + statusCode = http.StatusOK + } + for key, values := range headers { + for _, value := range values { + w.Header().Add(key, value) + } + } + w.WriteHeader(statusCode) + if len(body) == 0 { + return + } + if _, err := w.Write(body); err != nil { + log.Printf("写入 Forward 响应失败: %v", err) + } +} + // devWebSocketConn 包装 gorilla/websocket.Conn 为 sdk.WebSocketConn type devWebSocketConn struct { conn *websocket.Conn diff --git a/devkit/devserver/proxy_test.go b/devkit/devserver/proxy_test.go new file mode 100644 index 0000000..11f9e96 --- /dev/null +++ b/devkit/devserver/proxy_test.go @@ -0,0 +1,115 @@ +package devserver + +import ( + "context" + "net/http" + "net/http/httptest" + "path/filepath" + "testing" + + sdk "github.com/DouDOU-start/airgate-sdk/sdkgo" +) + +type proxyTestGateway struct { + outcome sdk.ForwardOutcome + write bool +} + +func (g *proxyTestGateway) Info() sdk.PluginInfo { + return sdk.PluginInfo{ + ID: "proxy-test", + Name: "Proxy Test", + Version: "0.1.0", + SDKVersion: sdk.SDKVersion, + Type: sdk.PluginTypeGateway, + } +} + +func (g *proxyTestGateway) Init(sdk.PluginContext) error { return nil } +func (g *proxyTestGateway) Start(context.Context) error { return nil } +func (g *proxyTestGateway) Stop(context.Context) error { return nil } +func (g *proxyTestGateway) Platform() string { return "test" } +func (g *proxyTestGateway) Models() []sdk.ModelInfo { return nil } +func (g *proxyTestGateway) Routes() []sdk.RouteDefinition { return nil } +func (g *proxyTestGateway) ValidateAccount(context.Context, map[string]string) error { + return nil +} +func (g *proxyTestGateway) HandleWebSocket(context.Context, sdk.WebSocketConn) (sdk.ForwardOutcome, error) { + return sdk.ForwardOutcome{}, sdk.ErrNotSupported +} + +func (g *proxyTestGateway) Forward(_ context.Context, req *sdk.ForwardRequest) (sdk.ForwardOutcome, error) { + if g.write && req.Writer != nil { + req.Writer.Header().Set("X-Buffered", "yes") + req.Writer.WriteHeader(http.StatusCreated) + _, _ = req.Writer.Write([]byte("buffered")) + } + return g.outcome, nil +} + +func newProxyTestStore(t *testing.T) *AccountStore { + t.Helper() + store := NewAccountStore(filepath.Join(t.TempDir(), "accounts.json")) + store.Create(DevAccount{ + Name: "test", + AccountType: "apikey", + Credentials: map[string]string{"api_key": "sk-test"}, + }) + return store +} + +func TestProxyWritesForwardOutcomeForNonStream(t *testing.T) { + t.Parallel() + + handler := &ProxyHandler{ + plugin: &proxyTestGateway{outcome: sdk.ForwardOutcome{ + Kind: sdk.OutcomeSuccess, + Upstream: sdk.UpstreamResponse{ + StatusCode: http.StatusAccepted, + Headers: http.Header{"X-Outcome": []string{"yes"}}, + Body: []byte("from outcome"), + }, + }}, + store: newProxyTestStore(t), + } + + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", nil) + handler.ServeHTTP(rec, req) + + if rec.Code != http.StatusAccepted { + t.Fatalf("status = %d,期望 %d", rec.Code, http.StatusAccepted) + } + if rec.Header().Get("X-Outcome") != "yes" { + t.Fatalf("X-Outcome = %q,期望 yes", rec.Header().Get("X-Outcome")) + } + if rec.Body.String() != "from outcome" { + t.Fatalf("body = %q,期望 from outcome", rec.Body.String()) + } +} + +func TestProxyCapturesBufferedWriterForNonStream(t *testing.T) { + t.Parallel() + + handler := &ProxyHandler{ + plugin: &proxyTestGateway{ + write: true, + outcome: sdk.ForwardOutcome{Kind: sdk.OutcomeSuccess}, + }, + store: newProxyTestStore(t), + } + + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", nil) + handler.ServeHTTP(rec, req) + + if rec.Code != http.StatusCreated { + t.Fatalf("status = %d,期望 %d", rec.Code, http.StatusCreated) + } + if rec.Header().Get("X-Buffered") != "yes" { + t.Fatalf("X-Buffered = %q,期望 yes", rec.Header().Get("X-Buffered")) + } + if rec.Body.String() != "buffered" { + t.Fatalf("body = %q,期望 buffered", rec.Body.String()) + } +} diff --git a/devserver/scheduler.go b/devkit/devserver/scheduler.go similarity index 99% rename from devserver/scheduler.go rename to devkit/devserver/scheduler.go index a3d1ddd..916df7a 100644 --- a/devserver/scheduler.go +++ b/devkit/devserver/scheduler.go @@ -9,7 +9,7 @@ import ( "sync" "time" - sdk "github.com/DouDOU-start/airgate-sdk" + sdk "github.com/DouDOU-start/airgate-sdk/sdkgo" ) // SchedulePolicy 调度策略 diff --git a/devserver/server.go b/devkit/devserver/server.go similarity index 79% rename from devserver/server.go rename to devkit/devserver/server.go index 705cbce..f41ea30 100644 --- a/devserver/server.go +++ b/devkit/devserver/server.go @@ -10,12 +10,13 @@ import ( "log/slog" "net/http" "os" + "path" "path/filepath" "strconv" "strings" "time" - sdk "github.com/DouDOU-start/airgate-sdk" + sdk "github.com/DouDOU-start/airgate-sdk/sdkgo" ) //go:embed static @@ -143,7 +144,12 @@ func Run(cfg Config) error { // 从 plugin.Routes() 提取路径前缀注册代理 proxy := &ProxyHandler{plugin: cfg.Plugin, store: store, scheduler: scheduler} prefixes := routePrefixes(cfg.Plugin.Routes()) + hasRootProxy := false for _, prefix := range prefixes { + if prefix == "/" { + hasRootProxy = true + continue + } mux.Handle(prefix, proxy) } @@ -175,7 +181,16 @@ func Run(cfg Config) error { if err != nil { return err } - mux.Handle("/", http.FileServer(http.FS(staticFS))) + staticHandler := http.FileServer(http.FS(staticFS)) + if hasRootProxy { + mux.Handle("/", &rootRouteHandler{ + proxy: proxy, + static: staticHandler, + routes: cfg.Plugin.Routes(), + }) + } else { + mux.Handle("/", staticHandler) + } info := cfg.Plugin.Info() log.Printf("devserver 启动: http://localhost%s", *addr) @@ -185,14 +200,15 @@ func Run(cfg Config) error { return http.ListenAndServe(*addr, mux) } -// routePrefixes 从路由声明中提取不重复的路径前缀(如 /v1/) +// routePrefixes 从路由声明中提取不重复的路径前缀(如 /v1/)。 func routePrefixes(routes []sdk.RouteDefinition) []string { seen := make(map[string]bool) prefixes := make([]string, 0, len(routes)) for _, r := range routes { - // 取第二个 / 之前的部分作为前缀 - parts := strings.SplitN(strings.TrimPrefix(r.Path, "/"), "/", 2) - prefix := "/" + parts[0] + "/" + prefix, ok := routePrefix(r.Path) + if !ok { + continue + } if !seen[prefix] { seen[prefix] = true prefixes = append(prefixes, prefix) @@ -200,3 +216,51 @@ func routePrefixes(routes []sdk.RouteDefinition) []string { } return prefixes } + +func routePrefix(routePath string) (string, bool) { + routePath = strings.TrimSpace(routePath) + if routePath == "" { + return "", false + } + if !strings.HasPrefix(routePath, "/") { + routePath = "/" + routePath + } + cleaned := path.Clean(routePath) + if cleaned == "/" { + return "/", true + } + parts := strings.SplitN(strings.TrimPrefix(cleaned, "/"), "/", 2) + return "/" + parts[0] + "/", true +} + +type rootRouteHandler struct { + proxy http.Handler + static http.Handler + routes []sdk.RouteDefinition +} + +func (h *rootRouteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + for _, route := range h.routes { + prefix, ok := routePrefix(route.Path) + if !ok || prefix != "/" { + continue + } + if path.Clean(r.URL.Path) != "/" { + continue + } + if !routeMethodMatches(route.Method, r.Method) { + continue + } + h.proxy.ServeHTTP(w, r) + return + } + h.static.ServeHTTP(w, r) +} + +func routeMethodMatches(routeMethod, requestMethod string) bool { + routeMethod = strings.TrimSpace(routeMethod) + if routeMethod == "" { + return true + } + return strings.EqualFold(routeMethod, requestMethod) +} diff --git a/devkit/devserver/server_test.go b/devkit/devserver/server_test.go new file mode 100644 index 0000000..0ad6019 --- /dev/null +++ b/devkit/devserver/server_test.go @@ -0,0 +1,69 @@ +package devserver + +import ( + "net/http" + "net/http/httptest" + "testing" + + sdk "github.com/DouDOU-start/airgate-sdk/sdkgo" +) + +func TestRoutePrefixesDeduplicates(t *testing.T) { + t.Parallel() + + got := routePrefixes([]sdk.RouteDefinition{ + {Path: "/v1/chat/completions"}, + {Path: "/v1/models"}, + {Path: "/oauth/callback"}, + {Path: "/"}, + {Path: "v2/messages"}, + {Path: ""}, + }) + want := []string{"/v1/", "/oauth/", "/", "/v2/"} + if len(got) != len(want) { + t.Fatalf("routePrefixes length = %d, want %d (%v)", len(got), len(want), got) + } + for i := range want { + if got[i] != want[i] { + t.Fatalf("routePrefixes[%d] = %q, want %q", i, got[i], want[i]) + } + } +} + +func TestRootRouteHandlerOnlyProxiesRootMatch(t *testing.T) { + t.Parallel() + + var proxied, servedStatic int + h := &rootRouteHandler{ + routes: []sdk.RouteDefinition{{Method: http.MethodPost, Path: "/"}}, + proxy: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + proxied++ + w.WriteHeader(http.StatusAccepted) + }), + static: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + servedStatic++ + w.WriteHeader(http.StatusOK) + }), + } + + req := httptest.NewRequest(http.MethodGet, "/", nil) + rec := httptest.NewRecorder() + h.ServeHTTP(rec, req) + if rec.Code != http.StatusOK || servedStatic != 1 || proxied != 0 { + t.Fatalf("GET / 应走静态页面,code=%d static=%d proxy=%d", rec.Code, servedStatic, proxied) + } + + req = httptest.NewRequest(http.MethodPost, "/", nil) + rec = httptest.NewRecorder() + h.ServeHTTP(rec, req) + if rec.Code != http.StatusAccepted || proxied != 1 { + t.Fatalf("POST / 应走插件代理,code=%d proxy=%d", rec.Code, proxied) + } + + req = httptest.NewRequest(http.MethodPost, "/theme.css", nil) + rec = httptest.NewRecorder() + h.ServeHTTP(rec, req) + if rec.Code != http.StatusOK || servedStatic != 2 || proxied != 1 { + t.Fatalf("非根路径应走静态页面,code=%d static=%d proxy=%d", rec.Code, servedStatic, proxied) + } +} diff --git a/devserver/static/index.html b/devkit/devserver/static/index.html similarity index 94% rename from devserver/static/index.html rename to devkit/devserver/static/index.html index c4caf2d..74526f4 100644 --- a/devserver/static/index.html +++ b/devkit/devserver/static/index.html @@ -372,53 +372,6 @@ text-transform: uppercase; letter-spacing: 0.04em; } - .acct-plan-badge { - font-size: 0.55rem; - font-weight: 600; - padding: 0.08rem 0.35rem; - border-radius: 9999px; - text-transform: capitalize; - margin-left: 0.35rem; - vertical-align: middle; - } - .acct-plan-free { background: #f3f4f6; color: #6b7280; } - .acct-plan-plus { background: #d1fae5; color: #059669; } - .acct-plan-pro { background: #ede9fe; color: #7c3aed; } - .acct-plan-team { background: #dbeafe; color: #2563eb; } - /* 用量条 */ - .acct-usage { - display: flex; align-items: center; gap: 0.35rem; - margin-top: 0.25rem; flex-wrap: wrap; - } - .usage-label { - font-size: 0.6rem; font-weight: 600; - color: var(--ag-text-secondary); - min-width: 1.2rem; - } - .usage-bar { - width: 60px; height: 5px; - background: var(--ag-border); - border-radius: 3px; - overflow: hidden; - } - .usage-fill { - display: block; height: 100%; - border-radius: 3px; - transition: width 0.3s; - } - .usage-ok { background: #22c55e; } - .usage-warn { background: #f59e0b; } - .usage-danger { background: #ef4444; } - .usage-pct { - font-size: 0.6rem; font-family: var(--ag-font-mono); - color: var(--ag-text-secondary); - min-width: 2.5rem; - } - .usage-meta { - font-size: 0.55rem; color: var(--ag-text-tertiary); - background: var(--ag-bg-surface); padding: 0.1rem 0.35rem; - border-radius: 3px; margin-right: 4px; white-space: nowrap; - } .acct-weight-tag { font-family: var(--ag-font-mono); font-size: 0.65rem; diff --git a/devserver/static/js/accounts.js b/devkit/devserver/static/js/accounts.js similarity index 54% rename from devserver/static/js/accounts.js rename to devkit/devserver/static/js/accounts.js index 41c7bdb..5a5d21f 100644 --- a/devserver/static/js/accounts.js +++ b/devkit/devserver/static/js/accounts.js @@ -1,18 +1,8 @@ // 账号列表、CRUD、连通测试 -import { API, getBadgeStyle, getTypeLabel, maskKey, iconLetter } from './utils.js'; +import { API, getBadgeStyle, getTypeLabel, maskKey, iconLetter, escapeHtml } from './utils.js'; import { loadScheduler } from './scheduler.js'; -/** 从 JWT 中解析 plan_type(不验签) */ -function parsePlanFromJWT(token) { - try { - const parts = token.split('.'); - if (parts.length !== 3) return ''; - const payload = JSON.parse(atob(parts[1].replace(/-/g, '+').replace(/_/g, '/'))); - return payload?.['https://api.openai.com/auth']?.chatgpt_plan_type || ''; - } catch { return ''; } -} - export async function loadAccounts() { const res = await fetch(API + '/api/accounts'); const accounts = await res.json(); @@ -27,17 +17,14 @@ export async function loadAccounts() { const credKeys = Object.keys(a.credentials || {}); const credHint = credKeys.length > 0 ? maskKey(a.credentials[credKeys[0]]) : ''; const weight = a.weight || 1; - const planType = a.credentials?.plan_type || parsePlanFromJWT(a.credentials?.access_token || ''); - const planBadge = planType ? `${planType}` : ''; return `
${iconLetter(a.name, a.account_type)}
-
${a.name || '未命名'} ${planBadge}
+
${escapeHtml(a.name || '未命名')}
- ${typeLabel} - ${credHint ? `${credHint}` : ''} + ${escapeHtml(typeLabel)} + ${credHint ? `${escapeHtml(credHint)}` : ''}
-
W:${weight} @@ -48,8 +35,6 @@ export async function loadAccounts() {
`; }).join(''); - // 异步加载每个账号的用量 - accounts.forEach(a => loadAccountUsage(a.id)); } export function editWeight(event, id, current) { @@ -109,47 +94,3 @@ export async function deleteAccount(id) { loadAccounts(); loadScheduler(); } - -/** 查询并展示账号用量(Codex 速率限制) */ -export async function loadAccountUsage(id) { - const el = document.getElementById('usage-' + id); - if (!el) return; - try { - const res = await fetch(API + '/api/accounts/usage/' + id); - const data = await res.json(); - if (!data.available || !data.usage) { - el.style.display = 'none'; - return; - } - const u = data.usage; - const parts = []; - const wl = (min) => min >= 1440 ? Math.round(min / 1440) + 'd' : min >= 60 ? Math.round(min / 60) + 'h' : min + 'm'; - const bar = (label, pct) => `${label}${pct.toFixed(1)}%`; - // 总限制(primary_window_minutes > 0 表示头存在) - if (u.primary_window_minutes > 0) parts.push(bar(wl(u.primary_window_minutes), u.primary_used_percent)); - if (u.secondary_window_minutes > 0) parts.push(bar(wl(u.secondary_window_minutes), u.secondary_used_percent)); - // 模型子限制(bengalfox) - // 从 limit_name 提取短名,如 "GPT-5.3-Codex-Spark" → "Spark" - const shortLimit = (u.limit_name || '').split('-').pop() || 'model'; - if (u.bengalfox_primary_window_minutes > 0) { - parts.push(bar(shortLimit + ' ' + wl(u.bengalfox_primary_window_minutes), u.bengalfox_primary_used_percent)); - } - if (u.bengalfox_secondary_window_minutes > 0) { - parts.push(bar(shortLimit + ' ' + wl(u.bengalfox_secondary_window_minutes), u.bengalfox_secondary_used_percent)); - } - if (parts.length === 0) { - el.style.display = 'none'; - return; - } - el.innerHTML = parts.join(''); - el.style.display = 'flex'; - } catch { - el.style.display = 'none'; - } -} - -function usageColor(pct) { - if (pct >= 90) return 'usage-danger'; - if (pct >= 70) return 'usage-warn'; - return 'usage-ok'; -} diff --git a/devserver/static/js/app.js b/devkit/devserver/static/js/app.js similarity index 100% rename from devserver/static/js/app.js rename to devkit/devserver/static/js/app.js diff --git a/devserver/static/js/form.js b/devkit/devserver/static/js/form.js similarity index 100% rename from devserver/static/js/form.js rename to devkit/devserver/static/js/form.js diff --git a/devserver/static/js/scheduler.js b/devkit/devserver/static/js/scheduler.js similarity index 100% rename from devserver/static/js/scheduler.js rename to devkit/devserver/static/js/scheduler.js diff --git a/devserver/static/js/utils.js b/devkit/devserver/static/js/utils.js similarity index 90% rename from devserver/static/js/utils.js rename to devkit/devserver/static/js/utils.js index 49c4bd9..42d2699 100644 --- a/devserver/static/js/utils.js +++ b/devkit/devserver/static/js/utils.js @@ -39,6 +39,15 @@ export function iconLetter(name, typeKey) { return (typeKey || '?')[0].toUpperCase(); } +export function escapeHtml(value) { + return String(value ?? '') + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + export function applyTheme(theme) { const nextTheme = theme === 'light' ? 'light' : 'dark'; document.documentElement.setAttribute('data-theme', nextTheme); diff --git a/devserver/static/js/widget.js b/devkit/devserver/static/js/widget.js similarity index 84% rename from devserver/static/js/widget.js rename to devkit/devserver/static/js/widget.js index a13cc94..d3a8b19 100644 --- a/devserver/static/js/widget.js +++ b/devkit/devserver/static/js/widget.js @@ -3,24 +3,26 @@ import { pluginInfo, API } from './utils.js'; -let pluginModule = null; +const pluginModules = new Map(); let widgetRoot = null; /** 加载插件前端模块(仅加载一次) */ -async function loadPluginModule() { - if (pluginModule !== null) return pluginModule; - const widget = pluginInfo?.frontend_widgets?.find(w => w.slot === 'account-form'); +async function loadPluginModule(slotName) { + if (pluginModules.has(slotName)) return pluginModules.get(slotName); + const widget = pluginInfo?.frontend_widgets?.find(w => w.slot === slotName); if (!widget) { - pluginModule = false; + pluginModules.set(slotName, false); return false; } try { - pluginModule = await import('/plugin-assets/' + widget.entry_file); + const mod = await import('/plugin-assets/' + widget.entry_file); + pluginModules.set(slotName, mod); + return mod; } catch (e) { console.warn('加载插件前端模块失败:', e); - pluginModule = false; + pluginModules.set(slotName, false); + return false; } - return pluginModule; } /** @@ -35,8 +37,10 @@ export async function tryLoadPluginWidget(typeKey, formState) { const typeCards = document.getElementById('type-cards'); const dialog = document.getElementById('form-dialog'); - const mod = await loadPluginModule(); - if (!mod || !mod.default?.accountForm) { + const slotName = formState.mode === 'edit' ? 'account-edit' : 'account-create'; + const componentName = formState.mode === 'edit' ? 'accountEdit' : 'accountCreate'; + const mod = await loadPluginModule(slotName); + if (!mod || !mod.default?.[componentName]) { slot.style.display = 'none'; slot.innerHTML = ''; credFields.style.display = ''; @@ -50,7 +54,7 @@ export async function tryLoadPluginWidget(typeKey, formState) { typeCards.style.display = 'none'; dialog?.setAttribute('data-form-mode', 'plugin'); - const FormComponent = mod.default.accountForm; + const FormComponent = mod.default[componentName]; // 初始化 React root(仅一次) if (!widgetRoot) { diff --git a/devkit/devserver/static/theme.css b/devkit/devserver/static/theme.css new file mode 100644 index 0000000..2e67685 --- /dev/null +++ b/devkit/devserver/static/theme.css @@ -0,0 +1,124 @@ +:root { + --ag-radius-sm: 0.25rem; + --ag-radius-md: 0.25rem; + --ag-radius-lg: 0.25rem; + --ag-radius-xl: 0.25rem; + --ag-field-radius: 0.5rem; + --ag-font-sans: 'Geist Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + --ag-font-mono: 'Geist Mono', 'SF Mono', 'Cascadia Code', monospace; + --ag-transition: 200ms cubic-bezier(0.4, 0, 0.2, 1); + --ag-transition-slow: 400ms cubic-bezier(0.4, 0, 0.2, 1); + --ag-sidebar-width: 260px; + --ag-sidebar-collapsed: 72px; + --ag-topbar-height: 64px; +} + +:root[data-theme="dark"] { + --ag-primary: oklch(0.9848 0 0); + --ag-primary-foreground: oklch(15% 0.0000 0.00); + --ag-primary-hover: color-mix(in oklab, oklch(0.9848 0 0) 88%, oklch(15% 0.0000 0.00) 12%); + --ag-primary-subtle: color-mix(in oklab, oklch(0.9848 0 0) 14%, transparent); + --ag-primary-glow: color-mix(in oklab, oklch(0.9848 0 0) 22%, transparent); + --ag-success: oklch(73.29% 0.1935 120.35); + --ag-success-foreground: oklch(21.03% 0.0059 120.35); + --ag-success-subtle: color-mix(in oklab, oklch(73.29% 0.1935 120.35) 15%, transparent); + --ag-warning: oklch(0.8803 0.1348 86.06); + --ag-warning-foreground: oklch(15% 0.0404 86.06); + --ag-warning-subtle: color-mix(in oklab, oklch(0.8803 0.1348 86.06) 15%, transparent); + --ag-danger: oklch(0.7044 0.1872 23.19); + --ag-danger-foreground: oklch(15% 0.0500 23.19); + --ag-danger-subtle: color-mix(in oklab, oklch(0.7044 0.1872 23.19) 15%, transparent); + --ag-info: oklch(0.9848 0 0); + --ag-info-subtle: color-mix(in oklab, oklch(0.9848 0 0) 14%, transparent); + --ag-default-bg: oklch(27.40% 0.0000 0.00); + --ag-default-foreground: oklch(99.11% 0 0); + --ag-field-background: oklch(21.03% 0.0000 0.00); + --ag-field-foreground: oklch(99.11% 0.0000 0.00); + --ag-field-placeholder: oklch(70.50% 0.0000 0.00); + --ag-muted: oklch(70.50% 0.0000 0.00); + --ag-overlay: oklch(21.03% 0.0000 0.00); + --ag-overlay-foreground: oklch(99.11% 0.0000 0.00); + --ag-scrollbar: oklch(70.50% 0.0000 0.00); + --ag-segment: oklch(39.64% 0.0000 0.00); + --ag-segment-foreground: oklch(99.11% 0.0000 0.00); + --ag-surface: oklch(21.03% 0.0000 0.00); + --ag-surface-foreground: oklch(99.11% 0.0000 0.00); + --ag-surface-secondary: oklch(25.70% 0.0000 0.00); + --ag-surface-secondary-foreground: oklch(99.11% 0.0000 0.00); + --ag-surface-tertiary: oklch(27.21% 0.0000 0.00); + --ag-surface-tertiary-foreground: oklch(99.11% 0.0000 0.00); + --ag-bg-deep: oklch(12.00% 0.0000 0.00); + --ag-bg: oklch(12.00% 0.0000 0.00); + --ag-bg-elevated: oklch(21.03% 0.0000 0.00); + --ag-bg-surface: oklch(21.03% 0.0000 0.00); + --ag-bg-hover: oklch(25.70% 0.0000 0.00); + --ag-bg-active: oklch(27.21% 0.0000 0.00); + --ag-border: oklch(28.00% 0.0000 0.00); + --ag-border-subtle: oklch(25.00% 0.0000 0.00); + --ag-border-focus: oklch(0.9848 0 0); + --ag-text: oklch(99.11% 0.0000 0.00); + --ag-text-secondary: oklch(70.50% 0.0000 0.00); + --ag-text-tertiary: oklch(70.50% 0.0000 0.00); + --ag-text-inverse: oklch(15% 0.0000 0.00); + --ag-glass: color-mix(in oklab, oklch(21.03% 0.0000 0.00) 92%, transparent); + --ag-glass-border: oklch(28.00% 0.0000 0.00); + --ag-shadow-sm: 0 0 0 0 transparent inset; + --ag-shadow-md: 0 0 0 0 transparent inset; + --ag-shadow-lg: 0 0 1px 0 #ffffff4d inset; + --ag-shadow-glow: 0 0 0 1px color-mix(in oklab, oklch(0.9848 0 0) 18%, transparent); +} + +:root[data-theme="light"] { + --ag-primary: oklch(0 0 0); + --ag-primary-foreground: oklch(99.11% 0 0); + --ag-primary-hover: color-mix(in oklab, oklch(0 0 0) 88%, oklch(99.11% 0 0) 12%); + --ag-primary-subtle: color-mix(in oklab, oklch(0 0 0) 10%, transparent); + --ag-primary-glow: color-mix(in oklab, oklch(0 0 0) 16%, transparent); + --ag-success: oklch(73.29% 0.1935 120.35); + --ag-success-foreground: oklch(21.03% 0.0059 120.35); + --ag-success-subtle: color-mix(in oklab, oklch(73.29% 0.1935 120.35) 15%, transparent); + --ag-warning: oklch(0.8446 0.1525 80.6); + --ag-warning-foreground: oklch(15% 0.0457 80.60); + --ag-warning-subtle: color-mix(in oklab, oklch(0.8446 0.1525 80.6) 15%, transparent); + --ag-danger: oklch(0.573 0.2249 21.97); + --ag-danger-foreground: oklch(98% 0.0200 21.97); + --ag-danger-subtle: color-mix(in oklab, oklch(0.573 0.2249 21.97) 15%, transparent); + --ag-info: oklch(0 0 0); + --ag-info-subtle: color-mix(in oklab, oklch(0 0 0) 10%, transparent); + --ag-default-bg: oklch(94.00% 0.0000 0.00); + --ag-default-foreground: oklch(21.03% 0.0059 0.00); + --ag-field-background: oklch(100.00% 0.0000 0.00); + --ag-field-foreground: oklch(21.03% 0.0000 0.00); + --ag-field-placeholder: oklch(55.17% 0.0000 0.00); + --ag-muted: oklch(55.17% 0.0000 0.00); + --ag-overlay: oklch(100.00% 0.0000 0.00); + --ag-overlay-foreground: oklch(21.03% 0.0000 0.00); + --ag-scrollbar: oklch(87.10% 0.0000 0.00); + --ag-segment: oklch(100.00% 0.0000 0.00); + --ag-segment-foreground: oklch(21.03% 0.0000 0.00); + --ag-surface: oklch(100.00% 0.0000 0.00); + --ag-surface-foreground: oklch(21.03% 0.0000 0.00); + --ag-surface-secondary: oklch(95.24% 0.0000 0.00); + --ag-surface-secondary-foreground: oklch(21.03% 0.0000 0.00); + --ag-surface-tertiary: oklch(93.73% 0.0000 0.00); + --ag-surface-tertiary-foreground: oklch(21.03% 0.0000 0.00); + --ag-bg-deep: oklch(97.02% 0.0000 0.00); + --ag-bg: oklch(97.02% 0.0000 0.00); + --ag-bg-elevated: oklch(100.00% 0.0000 0.00); + --ag-bg-surface: oklch(100.00% 0.0000 0.00); + --ag-bg-hover: oklch(95.24% 0.0000 0.00); + --ag-bg-active: oklch(93.73% 0.0000 0.00); + --ag-border: oklch(90.00% 0.0000 0.00); + --ag-border-subtle: oklch(92.00% 0.0000 0.00); + --ag-border-focus: oklch(0 0 0); + --ag-text: oklch(21.03% 0.0000 0.00); + --ag-text-secondary: oklch(55.17% 0.0000 0.00); + --ag-text-tertiary: oklch(55.17% 0.0000 0.00); + --ag-text-inverse: oklch(99.11% 0 0); + --ag-glass: color-mix(in oklab, oklch(100.00% 0.0000 0.00) 92%, transparent); + --ag-glass-border: oklch(90.00% 0.0000 0.00); + --ag-shadow-sm: 0 2px 4px 0 #0000000a, 0 1px 2px 0 #0000000f, 0 0 1px 0 #0000000f; + --ag-shadow-md: 0 2px 4px 0 #0000000a, 0 1px 2px 0 #0000000f, 0 0 1px 0 #0000000f; + --ag-shadow-lg: 0 2px 8px 0 #0000000f, 0 -6px 12px 0 #00000008, 0 14px 28px 0 #00000014; + --ag-shadow-glow: 0 0 0 1px color-mix(in oklab, oklch(0 0 0) 12%, transparent); +} diff --git a/devserver/server_test.go b/devserver/server_test.go deleted file mode 100644 index aea71c4..0000000 --- a/devserver/server_test.go +++ /dev/null @@ -1,27 +0,0 @@ -package devserver - -import ( - "testing" - - sdk "github.com/DouDOU-start/airgate-sdk" -) - -func TestRoutePrefixesDeduplicates(t *testing.T) { - t.Parallel() - - got := routePrefixes([]sdk.RouteDefinition{ - {Path: "/v1/chat/completions"}, - {Path: "/v1/models"}, - {Path: "/oauth/callback"}, - {Path: "/"}, - }) - want := []string{"/v1/", "/oauth/", "//"} - if len(got) != len(want) { - t.Fatalf("routePrefixes length = %d, want %d (%v)", len(got), len(want), got) - } - for i := range want { - if got[i] != want[i] { - t.Fatalf("routePrefixes[%d] = %q, want %q", i, got[i], want[i]) - } - } -} diff --git a/devserver/static/theme.css b/devserver/static/theme.css deleted file mode 100644 index d66ac5d..0000000 --- a/devserver/static/theme.css +++ /dev/null @@ -1,91 +0,0 @@ -:root { - --ag-radius-sm: 12px; - --ag-radius-md: 18px; - --ag-radius-lg: 22px; - --ag-radius-xl: 28px; - --ag-font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; - --ag-font-mono: 'JetBrains Mono', 'SF Mono', 'Cascadia Code', monospace; - --ag-transition: 200ms cubic-bezier(0.4, 0, 0.2, 1); - --ag-transition-slow: 400ms cubic-bezier(0.4, 0, 0.2, 1); - --ag-sidebar-width: 260px; - --ag-sidebar-collapsed: 72px; - --ag-topbar-height: 64px; -} - -:root[data-theme="dark"] { - --ag-primary: #3ecfb4; - --ag-primary-hover: #62dcc4; - --ag-primary-subtle: rgba(62, 207, 180, 0.08); - --ag-primary-glow: rgba(62, 207, 180, 0.14); - --ag-success: #34d399; - --ag-success-subtle: rgba(52, 211, 153, 0.12); - --ag-warning: #fbbf24; - --ag-warning-subtle: rgba(251, 191, 36, 0.12); - --ag-danger: #fb7185; - --ag-danger-subtle: rgba(251, 113, 133, 0.12); - --ag-info: #7dd3fc; - --ag-info-subtle: rgba(125, 211, 252, 0.12); - --ag-bg-deep: #06080e; - --ag-bg: #0c0f17; - --ag-bg-elevated: #131722; - --ag-bg-surface: #1a1e2a; - --ag-bg-hover: #232836; - --ag-bg-active: #2c3240; - --ag-border: rgba(148, 175, 225, 0.08); - --ag-border-subtle: rgba(148, 175, 225, 0.05); - --ag-border-focus: rgba(62, 207, 180, 0.40); - --ag-text: #e2e6f0; - --ag-text-secondary: #8d93a8; - --ag-text-tertiary: #565d73; - --ag-text-inverse: #06080e; - --ag-glass: rgba(148, 175, 225, 0.03); - --ag-glass-border: rgba(148, 175, 225, 0.06); - --ag-shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.36); - --ag-shadow-md: 0 8px 24px rgba(0, 0, 0, 0.48); - --ag-shadow-lg: 0 20px 48px rgba(0, 0, 0, 0.60); - --ag-shadow-glow: 0 0 0 1px rgba(62, 207, 180, 0.08), 0 8px 32px rgba(62, 207, 180, 0.10); -} - -:root[data-theme="light"] { - --ag-primary: #0d9488; - --ag-primary-hover: #0b7e74; - --ag-primary-subtle: rgba(13, 148, 136, 0.05); - --ag-primary-glow: rgba(13, 148, 136, 0.10); - --ag-success: #16a34a; - --ag-success-subtle: rgba(22, 163, 74, 0.06); - --ag-warning: #d97706; - --ag-warning-subtle: rgba(217, 119, 6, 0.06); - --ag-danger: #e11d48; - --ag-danger-subtle: rgba(225, 29, 72, 0.06); - --ag-info: #2563eb; - --ag-info-subtle: rgba(37, 99, 235, 0.06); - --ag-bg-deep: #f1f3f8; - --ag-bg: #f6f7fb; - --ag-bg-elevated: #ffffff; - --ag-bg-surface: #ffffff; - --ag-bg-hover: #eaedf5; - --ag-bg-active: #e0e4ee; - --ag-border: #d6dae6; - --ag-border-subtle: #e6e9f2; - --ag-border-focus: rgba(13, 148, 136, 0.45); - --ag-text: #131830; - --ag-text-secondary: #424866; - --ag-text-tertiary: #6e7490; - --ag-text-inverse: #ffffff; - --ag-glass: rgba(255, 255, 255, 0.92); - --ag-glass-border: rgba(10, 20, 60, 0.06); - --ag-shadow-sm: 0 1px 3px rgba(10, 20, 60, 0.06), 0 1px 2px rgba(10, 20, 60, 0.04); - --ag-shadow-md: 0 4px 12px rgba(10, 20, 60, 0.07), 0 2px 4px rgba(10, 20, 60, 0.04); - --ag-shadow-lg: 0 16px 40px rgba(10, 20, 60, 0.09), 0 4px 8px rgba(10, 20, 60, 0.04); - --ag-shadow-glow: 0 0 0 1px rgba(13, 148, 136, 0.10), 0 8px 24px rgba(13, 148, 136, 0.07); -} - -:root[data-theme="light"] .ag-elevation-modal { - --ag-bg-elevated: #eef0f7; - --ag-bg-surface: #e6e9f2; - --ag-bg-hover: #e0e4ee; - --ag-glass-border: #d2d6e3; - --ag-border: #c8cdd9; - --ag-shadow-sm: none; - --ag-shadow-md: none; -} diff --git a/docs/adr/0001-plugin-capability-and-isolation-model.md b/docs/adr/0001-plugin-capability-and-isolation-model.md deleted file mode 100644 index b172938..0000000 --- a/docs/adr/0001-plugin-capability-and-isolation-model.md +++ /dev/null @@ -1,423 +0,0 @@ -# ADR-0001:插件能力模型与隔离边界 - -- **状态**:Accepted -- **日期**:2026-04-10 -- **作者**:AirGate 核心团队 -- **影响范围**:airgate-sdk(proto / pluginsdk / grpc bridge)、airgate-core(plugin manager / host service / forwarder)、所有现有插件(airgate-openai、airgate-health、airgate-epay) - ---- - -## 1. Context(为什么要写这份 ADR) - -### 1.1 插件系统的演进历史 - -AirGate 从一开始就把"可插拔"作为架构目标。插件以 hashicorp/go-plugin 子进程形式运行,通过 gRPC 与 core 通信。已经上线的插件有: - -- **airgate-openai**(gateway 类型):OpenAI/Anthropic upstream adapter -- **airgate-epay**(extension 类型):第三方支付接入 + 订单表 -- **airgate-health**(extension 类型):分组级健康监控探测 + 时序表 - -最初的协议(`plugin.proto` v1)只定义了 **Core → Plugin** 方向的三个服务:`PluginService`(生命周期)、`GatewayService`(upstream 转发)、`ExtensionService`(后台任务 + HTTP 代理)。没有 **Plugin → Core** 的反向通道。 - -### 1.2 Step 1 补上了反向通道,但留下了四个结构性问题 - -2026-04-09 的 Step 1 重构里引入了 `HostService`:Core 通过 hashicorp/go-plugin 的 `GRPCBroker` 向每个插件子进程暴露一条反向 gRPC stream,让插件可以回调 core 能力(`SelectAccount` / `ProbeForward` / `ListGroups` / `ReportAccountResult`)。Step 2 用这套机制把 airgate-health 从账号级探测重写为分组级黑盒探测。 - -Step 2 落地后,我们识别出 4 个结构性问题需要在 Step 3 之前解决: - -**问题 1:插件直连 core 业务数据库** - -现状:`airgate-health` 和 `airgate-epay` 都通过 `db_dsn` 注入直接拿 core 的 PostgreSQL 连接字符串,然后用 `database/sql` 自己跑 SQL。后果: - -- 插件能读写 core 任何表(`accounts` / `users` / `api_keys` / `usage_logs`),没有权限边界 -- 插件与 core schema 强耦合。core 重命名字段或改表结构,插件就挂。`airgate-health/aggregator.go` 里 `SELECT ... FROM groups g JOIN account_groups ag ON ...` 就是这类反模式的典型 -- 没有审计、没有事务边界、没有最小权限原则 - -**问题 2:HostService 权限完全无差别** - -HostService 的 5 个 RPC 对任何插件一视同仁。这导致: - -- 一个监控用途的扩展插件可以调 `ReportAccountResult` 污染账号状态机 -- 一个 upstream gateway 插件可以调 `ListGroups` 读到所有分组元信息 -- 无法在审计时回答"这个插件到底用了哪些 core 能力" - -**问题 3:Forward 路径没有插件 hook 点** - -我们想给 AirGate 加"请求/响应中间层"——例如: - -- 内容审计(记录完整 request/response 到独立日志库) -- 敏感数据脱敏 -- 跨 request 合规标签注入 -- 旁路回放 / 流量镜像 / 异常样本采样 - -这些需求天然就是 middleware 模式。但当前 `forwarder.go:143 Forward(c *gin.Context)` 是一个闭合流程,内部直接 `buildForwardState → ensureAllowed → selectAccount → prepare → execute → finish`,没有任何第三方插件插桩的地方。 - -此外,当前的 `GatewayService.Forward` 是为"插件作为 upstream adapter"设计的——一个 gateway 插件**替代**了 upstream。我们想要的 middleware 插件是另一种角色:**旁路观察 + 允许修改 request/response**,不替代 upstream。SDK 里现在根本没有这种角色。 - -**问题 4:插件类型只有两种** - -`sdk.PluginType` 当前只有 `gateway` 和 `extension`。问题 3 里的"请求中间层"既不是 gateway 也不是 extension。强行塞进 extension 会让 extension 的语义越来越模糊。 - -### 1.3 现在做还是以后做 - -这些问题不是 Step 2 引入的,是 Step 1 为了"最快打通反向通道"一并带过的历史债。**现在做最便宜**,理由: - -- 插件生态还小(3 个官方插件),改动面可控 -- 还没有第三方插件作者,没有向前兼容压力 -- 正准备写"请求中间层"这种新类型的插件,这是最后的窗口 - -一旦第三方作者开始写插件、或者中间层插件写完再回头改,代价会翻倍。 - ---- - -## 2. Design Principles(未来所有 SDK 演进的宪法) - -### 原则 1:插件永远不直连 core 业务表 - -core 的业务数据(`accounts` / `groups` / `users` / `usage_logs` / `api_keys` 等)一律通过 `HostService` RPC 访问。**插件不拿 core schema 的 DSN**。 - -例外:插件仍然可以把**自己的数据**存在 PostgreSQL 里,但必须在独立 schema 下,通过受限 postgres 角色访问(参见原则 5)。 - -### 原则 2:能力按插件类型分级授权(Capability-based) - -不同的 PluginType 拿到不同的 HostService 接口。具体机制: - -1. 插件在 `PluginInfo.Capabilities` 里声明它**将要使用**的所有 HostService 能力 -2. Core 启动插件时校验: - - SDK 版本是否支持这些 capability - - 插件类型是否有权使用这些 capability(用"插件类型 → 允许的 capability 集合"映射表) -3. Core 用一个 gRPC interceptor,在每次 HostService RPC 调用时检查"这个插件声明了这个 capability 吗?",未声明直接返回 `PermissionDenied` - -这是**最小权限原则**的落地:一个插件拿到的能力是它"明确索取的 ∩ 插件类型允许的"。 - -### 原则 3:引入 middleware 插件类型 - -为"请求/响应记录 / 审计 / 脱敏 / 流量采样"这类场景新建一种插件角色: - -- 不替代 upstream(那是 gateway 的工作) -- 不跑后台任务(那是 extension 的工作) -- **核心职责**:在每次 forward 的前后被 core 回调,可观察、可修改请求/响应 - -这把"中间件"从"代码里的 interface"抬到了**协议层的 RPC**。任何语言写的插件都能做 middleware,不局限于 Go。 - -### 原则 4:SDK 协议向前兼容 - -HostService / MiddlewareService 一旦暴露就按"契约"对待: - -- **只加字段不删字段**(protobuf 天然支持) -- **加新 RPC 用新 rpc name**,不 hijack 旧的 -- **新能力必须伴随新 capability flag**,旧插件不声明就不启用 -- deprecated 字段保留至少 2 个大版本 -- SDK 版本号(`sdk.SDKVersion`)与 proto 的向后兼容性同步 - -### 原则 5:Core 是 trust root,插件是 untrusted - -即便是自己写的插件,也按"不可信"对待: - -- 所有 HostService 输入做参数校验(边界 / 类型 / 长度) -- 插件声明的 capability 不能超过 SDK 版本允许的集合 -- credentials / password_hash / admin_api_key 等敏感字段**永远不通过 RPC 流向插件**(即使 `HostService.GetAccount` 也会脱敏) -- 插件的 frontend 资源在独立 iframe 或加 sandbox 属性 -- 插件写自己的表用独立 postgres 角色,只授 `USAGE + ALL` 在自己的 schema 上,`public` schema **全部 REVOKE** - -### 原则 6:所有决策记录在 ADR 里 - -未来的 SDK / plugin 架构改动都必须有一份对应的 ADR,遵循本文档的结构(Context → Design → Decision → Migration → Risks)。ADR 一旦 accepted 就不改历史,通过新 ADR `supersedes` 旧 ADR。 - ---- - -## 3. Decisions(5 个拍板的决策) - -### Decision 1(Q1):插件禁止直连 core 业务数据库 - -**选择**:插件不能读写 `public` schema 下的 core 表。core 业务数据一律通过 `HostService` RPC 访问。插件自己的数据存在独立 schema(见 Decision 5)。 - -**rejected 方案**: -- "允许只读" → 仍然有 schema 耦合问题(core 改表名插件就挂) -- "文档约定" → 靠自觉永远不可靠 - -**影响**: -- `airgate-health` 的 `aggregator.go` 中所有 `SELECT ... FROM groups` / `SELECT ... FROM accounts` 要迁移到 HostService RPC。预计改 ~10 个 SQL 语句 -- `airgate-epay` 继续用独立 schema 存 `payment_orders`,不受影响 -- core 不再向插件注入 `db_dsn` 字段。改为注入 `plugin_dsn`(见 Decision 5) - -### Decision 2(Q2):middleware 插件起步只做 2 个 hook 点 - -**选择**:`OnForwardBegin` + `OnForwardEnd` - -**签名**: - -```go -type MiddlewarePlugin interface { - Plugin - - // OnForwardBegin 在 core 选完账号、但还没调 upstream 插件之前触发。 - // 允许返回 decision 修改请求(加 header、改 body),或拒绝放行(返回 Deny)。 - OnForwardBegin(ctx context.Context, req *MiddlewareRequest) (*MiddlewareDecision, error) - - // OnForwardEnd 在 upstream 插件返回之后、core 写 usage_log 之前触发。 - // 拿到完整的请求 + 响应元数据(但不一定包含 body,见 Decision 3)。 - // 返回 error 不影响主流程(只 log warn),保证 middleware 永远不 block 生产流量。 - OnForwardEnd(ctx context.Context, evt *MiddlewareEvent) error -} -``` - -**不做**(留给未来 ADR):`OnSelectAccount` / `OnFailover` / `OnStreamChunk` - -**理由**: -- Begin + End 覆盖 90% 的用例(日志 / 审计 / 合规标签 / 脱敏) -- 流式 chunk 级 hook 性能代价大,只在少数场景必要,延后引入 -- 每加一个 hook 都是永久协议承诺,宁缺毋滥 - -**顺序**: -- 多个 middleware 按 `Priority`(PluginInfo 新字段)从小到大排序依次调用 -- `OnForwardBegin` 按 priority 升序 -- `OnForwardEnd` 按 priority **降序**(LIFO,像 middleware stack 展开) - -**失败语义**: -- `OnForwardBegin` 返回 error → 该 middleware 被跳过,流程继续(不 block 生产) -- `OnForwardBegin` 返回 `MiddlewareDecision{Action: Deny}` → 整个请求被拒绝,返回给用户的错误信息来自 decision -- `OnForwardEnd` 返回 error → log warn,其余 middleware 仍然照常跑 - -### Decision 3(Q3):middleware payload 采用两段式 - -**选择**:默认只传元数据;需要 body 的 middleware 显式声明 capability `middleware.read_body` - -**MiddlewareRequest / MiddlewareEvent 字段设计**: - -```proto -message MiddlewareRequest { - // 元数据(默认传) - string request_id = 1; - int64 user_id = 2; - int64 group_id = 3; - int64 account_id = 4; - string platform = 5; - string model = 6; - bool stream = 7; - int64 input_tokens_est = 8; // core 侧的粗略估算 - - // 按需传(声明了 middleware.read_body 的插件才会收到) - bytes request_body = 100; - map request_headers = 101; -} - -message MiddlewareEvent { - string request_id = 1; - // ... 同上的元数据 - - int64 status_code = 20; - int64 duration_ms = 21; - int64 input_tokens = 22; // 实际值(插件计算后) - int64 output_tokens = 23; - int64 first_token_ms = 24; - string error_kind = 25; // 失败时填 - string error_msg = 26; - - // 按需传 - bytes response_body = 100; - map response_headers = 101; -} -``` - -**理由**: -- 大部分 middleware(日志 / 计费 / 审计标签)只需要元数据。0 额外开销 -- 内容审计 / 脱敏 / replay 这类真的需要 body 的场景,显式 opt-in -- 管理员在"插件管理"页能清楚看到"这个 middleware 插件声明了读取 body 的权限",有知情同意 - -**取舍**: -- 声明了 `middleware.read_body` 的插件会让该次请求在 core 侧多一次 body 序列化 + gRPC 传输。对开发者透明但成本真实 -- 流式 body 的处理:Begin 阶段能拿到完整 request body;End 阶段流式响应的 response_body 只传**聚合后的首次非空 chunk 拼装**的文本摘要(完整流式内容 hook 留给未来的 `OnStreamChunk`) - -### Decision 4(Q4):capability 系统现在就做 - -**选择**:Step 3 一并落地 capability 模型 - -**proto 改动**: - -```proto -message PluginInfoResponse { - // ... 现有字段 - repeated string capabilities = 14; // 新增:插件声明的能力列表 -} -``` - -**capability 命名规范**:`.` - -当前(Step 1 + Step 2)已经隐式使用的能力,现在要求 **显式声明**: - -| Capability | Owner RPC | 允许的插件类型 | -|---|---|---| -| `host.list_groups` | `HostService.ListGroups` | extension, middleware | -| `host.probe_forward` | `HostService.ProbeForward` | extension(只给 probe 子类,详见未来的 PluginSubtype) | -| `host.select_account` | `HostService.SelectAccount` | extension | -| `host.report_account_result` | `HostService.ReportAccountResult` | extension(只给 probe 子类) | -| `middleware.read_body` | 改变 MiddlewareRequest/Event 的 body 字段填充 | middleware | - -**未来可能新增**(不在 Step 3 范围内,仅作预留): -- `host.list_accounts` / `host.get_account` / `host.list_users`(只读业务数据) -- `host.write_usage_log`(某些计费中间件) -- `middleware.rewrite_request` / `middleware.rewrite_response`(有副作用的 middleware) - -**core 侧实现**: -1. 插件启动时 `PluginInfo.Capabilities` 和"插件类型 → 允许集合"做交集,产出**有效 capability set** -2. `HostService` 注册一个 gRPC unary interceptor,每次 RPC 调用时从 `context` 里取出本插件的 capability set,检查当前 method 是否允许 -3. 未允许 → 返回 `status.Errorf(codes.PermissionDenied, "plugin %s lacks capability %s", pluginID, cap)` -4. 管理员页能看到每个插件的 capability 列表 + 一个"capability 校验失败次数"计数 - -**迁移**: -- `airgate-health` 的 `metadata.go` 加 `Capabilities: []string{"host.list_groups", "host.probe_forward", "host.report_account_result"}` -- `airgate-openai` / `airgate-epay` 当前没用 HostService,`Capabilities` 留空即可 -- 旧版本插件(未声明 Capabilities):**SDK 版本 <= 0.2.x 的插件豁免**(向后兼容),SDK 版本 >= 0.3.x 的插件必须声明。Core 侧按 sdk_version 字段区分 - -### Decision 5(Q5):插件数据库使用独立 schema - -**选择**:同一个 PostgreSQL 实例下为每个有 DB 需求的插件创建独立 schema + 独立受限角色 - -**机制**: - -1. Core 启动时,对每个已加载的插件: - - 检查 DB 里是否存在 `plugin_` schema,不存在则创建(`CREATE SCHEMA IF NOT EXISTS`) - - 检查是否存在 `plugin__role` 角色,不存在则创建,密码随机生成存 settings 表 - - `GRANT USAGE, CREATE ON SCHEMA plugin_ TO plugin__role` - - `REVOKE ALL ON SCHEMA public FROM plugin__role` -2. Core 向插件注入 `plugin_dsn`(不再叫 `db_dsn`),DSN 里: - - 用户名 = `plugin__role` - - 密码 = 上面生成的随机值 - - `search_path=plugin_`(所有 SQL 默认查这个 schema) -3. 插件 Init 时拿到 `plugin_dsn`,用 `database/sql` 正常连接。插件代码里写 `CREATE TABLE group_health_probes (...)` 会被自动建在 `plugin_airgate_health.group_health_probes`,而 `SELECT * FROM groups` 会因为 `public` schema 的 REVOKE 而直接被 PostgreSQL 拒绝 - -**rejected 方案**: -- **独立 DB 实例**:需要额外运维成本(备份 / 连接池 / 监控),对单机部署不友好 -- **KV-only RPC**:airgate-health 的日桶聚合 SQL 没法做 - -**好处**: -- 权限在 PostgreSQL 层面强制执行,core 代码层不需要任何审查 -- 插件开发体验几乎不变:原来 `CREATE TABLE health_probes` 现在建在 `plugin_airgate_health.health_probes`,对 SQL 透明 -- 未来想迁移到独立 DB 实例也容易:只要改 DSN,插件代码不变 - -**迁移**: -- **airgate-health**:旧表 `public.health_probes` 已在 Step 2 被 DROP;新表 `public.group_health_probes` 在 Step 2 建在了 `public` schema。Step 3 需要把它迁到 `plugin_airgate_health.group_health_probes`。迁移 SQL:`ALTER TABLE public.group_health_probes SET SCHEMA plugin_airgate_health;` -- **airgate-epay**:类似,`payment_*` 表 SET SCHEMA 到 `plugin_airgate_epay` -- **airgate-openai**:有 `plugin_openai_session_states` / `plugin_anthropic_digest_sessions`,同样迁移 -- **core 自己的 `plugins` / `plugin_sources` / `plugin_account_usage_snapshots` 表保留在 `public`**(这是 core 自己的表,不是插件的表) - ---- - -## 4. Implementation Plan(分步落地) - -### Step 3:Capability + Middleware + DB 隔离(这份 ADR 的实现) - -**核心产物**: -- proto 加 `capabilities` 字段、`MiddlewareService` 服务、Middleware 相关 messages -- sdk/pluginsdk 加 `MiddlewarePlugin` interface、`PluginTypeMiddleware` 常量、capability 常量表 -- airgate-core 加 HostService interceptor + middleware chain + DB isolation 逻辑 -- 文档和样例 - -**范围边界**: -- **做**:capability 模型、middleware 接口、DB schema 隔离、已有 3 个插件的迁移 -- **不做**:`OnSelectAccount` / `OnFailover` / `OnStreamChunk`(留给未来 ADR) -- **不做**:`host.list_accounts` / `host.get_account` 等新业务查询 RPC(等真的有插件要用再加) - -### Step 4+:未来 ADR 的候选议题 - -- ADR-0002:Middleware 流式 hook 点(`OnStreamChunk`) -- ADR-0003:插件级资源配额与故障隔离(一个失控的 middleware 不能拖垮 core) -- ADR-0004:插件热更新与 capability 变更(管理员开关某个 capability 后的生效路径) -- ADR-0005:跨插件事件总线(如果出现 plugin A 需要订阅 plugin B 产生的事件) - ---- - -## 5. Migration Plan(三个现有插件怎么升级) - -### 5.1 airgate-openai(gateway 插件) - -- 不使用 HostService,不使用插件 DB -- **唯一改动**:`PluginInfo.Capabilities = []string{}`(可空) -- 风险:低 - -### 5.2 airgate-epay(extension 插件,payment) - -- 不使用 HostService,但使用 `db_dsn` -- **改动**: - 1. 配置键从 `db_dsn` 改为 `plugin_dsn`(或者 SDK 新增 helper `ctx.PluginDB()` 返回已配好 search_path 的 `*sql.DB`) - 2. `PluginInfo.Capabilities = []string{}` - 3. 启动时 core 自动迁移 `payment_*` 表到 `plugin_airgate_epay` schema -- 风险:中(DB 迁移需要停机或小心做) - -### 5.3 airgate-health(extension 插件,monitoring) - -- 使用 HostService + 插件 DB -- **改动**: - 1. `PluginInfo.Capabilities = []string{"host.list_groups", "host.probe_forward", "host.report_account_result"}` - 2. 配置键从 `db_dsn` 改为 `plugin_dsn` - 3. `aggregator.go` 里 `SELECT ... FROM groups` 的 SQL 迁移到 `host.ListGroups()` RPC 调用 - 4. `group_health_probes` 表 `SET SCHEMA plugin_airgate_health` - 5. `fillMissingPlatforms` 里 `SELECT platform, COUNT(*) FROM groups GROUP BY platform` 迁到 HostService 新 RPC `ListPlatforms()` —— 或者干脆从 `ListGroups()` 的结果里在 Go 侧聚合 -- 风险:中高(SQL 改动面广) - -### 5.4 迁移顺序 - -1. **Step 3a**:先做 proto + sdk + core 的基础设施改动(capability、middleware 接口、DB 隔离的 core 侧)。向后兼容:旧插件不声明 capability 仍然跑(sdk_version 豁免) -2. **Step 3b**:迁移 airgate-openai(最简单,验证 capability 声明路径) -3. **Step 3c**:迁移 airgate-epay(验证 DB schema 隔离的自动迁移路径) -4. **Step 3d**:迁移 airgate-health(验证"SQL 迁到 RPC"路径,这是最复杂的) -5. **Step 3e**:写第一个 middleware 插件(建议写 `airgate-audit`,最小 MVP 就是 `OnForwardEnd` 写一行到独立 schema 的 `audit_events` 表)来端到端验证 middleware 接口 - ---- - -## 6. Risks and Open Questions - -### 6.1 风险 - -**R1:DB 迁移失败导致插件起不来** -- 缓解:迁移 SQL 写成幂等(`SET SCHEMA` 本身幂等)、遇到已存在的目标表报错时 log + skip -- 兜底:保留一个"恢复模式"——管理员可以强制插件以旧 DSN 启动一次,手动干预数据 - -**R2:middleware 插件拖慢关键路径** -- 缓解:每次 OnForwardBegin/End 调用设 deadline(默认 200ms),超时即跳过该 middleware(log warn) -- 缓解:middleware chain 总超时预算(默认 500ms),超预算剩下的 middleware 全部跳过 -- 监控:core 暴露 `middleware_latency_ms` / `middleware_timeout_total` 指标 - -**R3:capability 声明漂移** -- 问题:插件代码里调 `host.list_groups`,但忘了声明 capability → 每次调用都被 interceptor 拒绝 -- 缓解:插件首次 Dispense 后做一次 self-check——把声明的 capabilities 发给 core,core 返回"你声明的里有 X 个是我不认识的" + "你没声明但未来可能用到的 Y 个是这些"(只 hint) - -**R4:向后兼容打破存量插件** -- 缓解:用 `sdk_version` 字段区分新旧行为。SDK 0.2.x 的插件豁免 capability 校验,SDK 0.3.x 起强制 -- 存量插件给一个 grace period(一个大版本),期间只 log warn 不 block - -### 6.2 Open Questions - -**Q-open-1:middleware 的链式修改如何 merge?** - -如果 middleware A 在 Begin 阶段改了 request header,middleware B 也改了同一个 header,谁赢? -- **倾向**:按 priority 顺序后来者覆盖,不做自动 merge。但这需要在 `MiddlewareDecision` 里明确语义 -- **延后**:Step 3 MVP 只支持 header 的"追加不覆盖",修改 body 留到有真实需求时再设计 - -**Q-open-2:capability 需要 versioning 吗?** - -如果未来 `host.list_groups` 的返回结构加字段,算不算 breaking? -- **倾向**:proto 本身的字段增加是向前兼容的,不需要新 capability。但如果语义变化(比如"以后 ListGroups 默认只返回启用的分组"),就必须引入 `host.list_groups.v2` -- **当前不解决**:等真的遇到了再说 - -**Q-open-3:middleware 之间能互相通信吗?** - -比如 A 在 Begin 里给 request 打了个 tag,B 在 End 里要读这个 tag。 -- **倾向**:在 MiddlewareRequest/Event 里加一个 `map metadata` 字段,所有 middleware 共享一个 KV bag -- **Step 3**:加这个字段,但不规定命名空间规则。让它自由生长,三个月后复盘是否需要规则 - ---- - -## 7. References - -- Step 1(HostService 引入):提交 `TBD`(本 ADR approve 后在这里回填) -- Step 2(airgate-health 分组级重写):提交 `TBD` -- hashicorp/go-plugin GRPCBroker 文档:https://github.com/hashicorp/go-plugin/blob/master/docs/grpc-broker.md -- Capability-based security(Wikipedia):https://en.wikipedia.org/wiki/Capability-based_security - ---- - -## 8. Changelog - -- **2026-04-10**:v1 draft,AirGate 核心团队 diff --git a/docs/async-task-state-machine.md b/docs/async-task-state-machine.md new file mode 100644 index 0000000..f84c934 --- /dev/null +++ b/docs/async-task-state-machine.md @@ -0,0 +1,1384 @@ +# 异步任务状态机设计 + +本文定义 AirGate 在 Core、SDK、插件之间处理长耗时生成任务的统一设计。目标是让对外业务 API 继续保持原有协议形态,只在响应中追加 AirGate `task_id` 作为追踪字段;Core 内部再把同步上游、异步上游和流式上游统一转换成任务状态机,插件只负责任务类型的业务编解码。 + +## 背景 + +当前 `airgate-openai` 的生图任务已经暴露出几个结构性问题: + +- 状态查询路径写死在图片语义里,例如 `/v1/images/tasks`、`/v1/images/tasks/list`。 +- 插件里的任务创建、执行、查询、列表、响应格式都绑定 `image_generation`。 +- 有些上游同步返回图片,有些上游先返回自己的 `task_id` 再轮询,有些上游通过 SSE / WebSocket 流式返回,当前代码需要在具体图片逻辑里混合处理这些差异。 +- 后续视频生成、音乐生成、语音合成、文件处理等都可能需要“先返回任务 ID,后查状态”的交互,如果继续按资源类型复制一套任务代码,会很快变成多套状态机。 +- 任务能力不是 OpenAI 独有,其他平台插件也可能需要同样的异步执行模型。 + +因此需要先抽象“任务生命周期”和“上游执行策略”,再把图片任务迁进去。 + +## 设计目标 + +1. Core 对插件和协议适配层提供统一任务能力;对业务客户端保持原 API 标准,不要求客户端知道上游是同步、异步还是流式。 +2. Core 内部维护唯一的任务状态机,负责创建、排队、执行、重试、取消、查询、列表和权限校验。 +3. SDK 提供稳定任务契约,让插件声明任务类型、输入输出 schema、执行能力和查询展示语义。 +4. 插件只实现任务业务逻辑:如何识别请求、如何构造任务 input、如何调用上游、如何标准化 output。 +5. 图片、视频、音乐、语音、文件处理等长耗时任务都能复用同一套 Core 机制。 +6. 对外兼容响应中追加的 `task_id` 必须是 AirGate 自己的任务 ID;上游自己的任务 ID 只能作为内部执行细节存储。 +7. 支持同步等待模式和后台任务模式,并允许 Core 在等待超时后自动转为后台任务。 +8. 保持现有 OpenAI 兼容 API 可迁移,不要求一次性废弃 `/v1/images/generations` 等现有入口。 + +## 非目标 + +- 不在 SDK 中编码 OpenAI、Anthropic 或任意平台的具体参数。 +- 不让 Core 理解图片、视频、音乐的业务字段含义。 +- 不把某个平台的定价、模型规则、轮询接口写进 Core。 +- 不要求所有任务都必须异步执行。短任务仍然可以同步返回。 +- 不要求所有插件都立即实现新任务协议。应支持分阶段迁移。 +- 不把通用 `/v1/tasks` 作为业务客户端的默认新标准。原来是 OpenAI Images、某个平台视频 API 或现有 AirGate 图片查询 API,对外就继续按原协议返回。 + +## 核心概念 + +### AirGate Task + +AirGate Task 是 Core 管理的统一任务实体。它是 Core、SDK、插件、管理后台之间的内部稳定对象,不是所有业务客户端都必须直接消费的外部响应格式。 + +任务对象建议如下: + +```json +{ + "id": "ag_task_123", + "plugin_id": "gateway-openai", + "type": "image.generate", + "status": "processing", + "progress": 35, + "stage": "polling_upstream", + "attributes": { + "platform": "openai", + "model": "gpt-image-2", + "size": "1024x1024" + }, + "input": { + "model": "gpt-image-2", + "prompt": "一只柴犬坐在樱花树下", + "size": "1024x1024" + }, + "output": null, + "error": null, + "created_at": "2026-05-12T10:00:00Z", + "updated_at": "2026-05-12T10:00:10Z", + "started_at": "2026-05-12T10:00:01Z", + "completed_at": null +} +``` + +`id` 是 AirGate 任务 ID,不等于上游任务 ID。协议适配层对外返回时通常命名为 `task_id`,并追加到原协议响应中。 + +Core 固定字段必须保持最小化,只包含任务生命周期、归属和安全边界。模型、平台、分辨率、时长、质量、音色等都不应成为 Core 任务表的硬编码列;它们属于插件声明的 `input`、`attributes`、`execution` 或最终 `Usage`。Core 可以保存、返回、按声明做弱索引,但不理解这些字段的业务含义。 + +模型很重要,但它是“使用记录维度”,不是“任务生命周期字段”。任务和使用记录统一只记录一个主模型字段:`model`。这个字段始终表示实际执行和计费的模型。模型映射、降级和工具链细节对用户透明,不进入通用任务或使用记录字段。 + +建议模型按以下位置记录: + +| 位置 | 用途 | 示例 | +| --- | --- | --- | +| `input.model` | 任务请求里的模型,用于任务复现 | `gpt-image-2` | +| `attributes.model` | 未完成任务列表里的展示/粗筛选 | `gpt-image-2` | +| `execution.*` | 插件内部工具链细节,不作为用户侧模型维度 | `tool_model=gpt-5.4` | +| `usage.Model` / `usage.Attributes.model` | 完成后的审计、计费、统计事实 | `gpt-5.4` | + +`Usage` 是最终事实来源。任务完成前可以用 `attributes` 暂存展示值;任务完成后应以关联的使用记录为准。 + +示例: + +- 用户请求 `gpt-image-2`,上游也按 `gpt-image-2` 执行:`model=gpt-image-2`。 +- 视频或音乐任务如存在模型字段,同样只记录实际执行和计费模型。 + +生图的分辨率、视频的时长和帧率、音乐的时长和质量、语音的音色和格式也不能进入 Core 固定字段。它们都属于任务类型自己的 input schema。Core 只保存和透传这些结构化 JSON,不理解字段语义。 + +通用字段与类型参数的边界: + +| 层级 | 字段示例 | Core 是否理解 | 用途 | +| --- | --- | --- | --- | +| 固定生命周期字段 | `id`、`plugin_id`、`type`、`status`、`progress`、`user_id` | 是 | 权限、状态机、查询、列表 | +| 类型化 input | `size`、`duration_seconds`、`quality`、`aspect_ratio`、`voice` | 否 | 插件校验、上游请求构造、任务复现 | +| 展示/弱索引字段 | `attributes.model`、`attributes.duration_seconds`、`attributes.resolution` | 只理解字符串键值 | 任务未完成时的列表页、管理后台、粗筛选 | +| 执行细节 | `execution` | 否 | 插件内部轮询、上游 task id、阶段信息 | + +例如图片任务 input: + +```json +{ + "prompt": "一只柴犬坐在樱花树下", + "size": "1024x1024", + "quality": "high", + "background": "transparent", + "output_format": "png" +} +``` + +视频任务 input: + +```json +{ + "prompt": "城市夜景航拍", + "duration_seconds": 8, + "aspect_ratio": "16:9", + "resolution": "1080p", + "fps": 24 +} +``` + +音乐任务 input: + +```json +{ + "prompt": "轻快的电子流行音乐", + "duration_seconds": 30, + "quality": "high", + "format": "mp3", + "loopable": false +} +``` + +Core 不为这些字段建固定列。需要列表展示时,插件可以在创建任务时写入少量字符串化的 `attributes`: + +```json +{ + "attributes": { + "duration_seconds": "8", + "resolution": "1080p", + "aspect_ratio": "16:9" + } +} +``` + +`attributes` 只放少量字符串化、可展示、可粗筛选的维度;完整参数仍以 `input` 为准。 + +### Upstream Task + +Upstream Task 是某些上游平台返回的任务对象,例如: + +```json +{ + "task_id": "img_abc", + "status_url": "https://provider.example/tasks/img_abc" +} +``` + +它只属于插件执行细节,必须持久化到任务的 `execution` 字段,不作为客户端主 ID。 + +持久化 `execution` 的原因: + +- Core worker 或插件进程重启后,需要继续按上游任务 ID 轮询。 +- 等待模式超时转后台后,需要恢复上游任务状态。 +- 上游短暂失败后重试时,需要判断是继续轮询已有上游任务,还是重新创建。 +- 取消任务时,如果上游支持取消,需要拿上游任务 ID 调用取消接口。 + +客户端查询时也不应直接使用上游任务 ID。查询入口由协议适配层决定: + +```text +GET /v1/images/tasks/ag_task_123 +``` + +或在未来的 Core 管理 API 中查询: + +```text +GET /v1/tasks/ag_task_123 +``` + +但无论入口路径如何,最终查询的都是 AirGate Task,而不是直接查询上游: + +```text +GET /provider/tasks/img_abc +``` + +### Task Type + +任务类型使用领域动作命名,而不是平台命名: + +```text +image.generate +image.edit +video.generate +music.generate +audio.speech +file.process +``` + +平台差异由 `plugin_id`、插件 schema、`input` 和 `attributes` 表达,不应把平台或模型塞进类型名里。 + +不推荐: + +```text +openai_image_generation +gpt_image_task +``` + +推荐: + +```text +image.generate +``` + +### Execution Mode + +Execution Mode 表示上游实际如何产出结果。 + +```go +type TaskExecutionMode string + +const ( + TaskExecutionSyncResult TaskExecutionMode = "sync_result" + TaskExecutionUpstreamTask TaskExecutionMode = "upstream_task" + TaskExecutionStreamResult TaskExecutionMode = "stream_result" +) +``` + +含义: + +| 模式 | 上游行为 | 插件行为 | +| --- | --- | --- | +| `sync_result` | 单次 HTTP 调用直接返回最终结果 | 解析响应并完成 AirGate Task | +| `upstream_task` | 上游返回自己的 `task_id` | 保存上游任务信息,轮询直到完成 | +| `stream_result` | 上游通过 SSE / WebSocket 持续返回 | 消费流并聚合最终结果 | + +Execution Mode 是插件内部执行策略,不直接决定客户端使用同步还是异步接口。 + +### Response Mode + +Response Mode 表示协议适配层希望 Core 如何处理本次请求的等待策略。它可以来自网关配置、插件默认值、Header 或显式 AirGate 扩展参数,但不要求所有业务客户端都感知这个概念。 + +```go +type TaskResponseMode string + +const ( + TaskResponseModeWait TaskResponseMode = "wait" + TaskResponseModeBackground TaskResponseMode = "background" + TaskResponseModeAuto TaskResponseMode = "auto" +) +``` + +| 模式 | 协议适配层语义 | +| --- | --- | +| `wait` | Core 尽量等待任务完成,适配层返回原协议最终结果并追加 `task_id` | +| `background` | Core 创建任务后立即返回,适配层返回该协议自己的异步响应并带上 `task_id` | +| `auto` | Core 等待一小段时间;超时未完成则由适配层返回该协议自己的异步响应并带上 `task_id` | + +Response Mode 与 Execution Mode 独立。 + +例如: + +- `wait` + 上游 `sync_result`:通常直接返回原协议最终结果,并追加 `task_id`。 +- `wait` + 上游 `upstream_task`:Core 可以轮询一段时间,完成则返回原协议最终结果;超时则由适配层返回原协议的异步响应。 +- `background` + 上游 `sync_result`:Core 仍然立即创建后台任务,适配层返回异步响应,后台 worker 同步调上游并完成任务。 +- `auto` + 上游 `stream_result`:Core 聚合流;超过等待窗口就转后台,适配层返回异步响应。 + +## 状态机 + +Core 维护统一状态机: + +```text +pending + -> processing + -> completed + -> failed + -> cancelling -> cancelled + -> retrying -> pending +``` + +建议状态定义: + +| 状态 | 含义 | 可被查询 | +| --- | --- | --- | +| `pending` | 已创建,等待调度 | 是 | +| `processing` | 插件正在执行 | 是 | +| `retrying` | 本次执行失败,等待下一次重试 | 是 | +| `completed` | 最终结果已写入 output | 是 | +| `failed` | 最终失败,已无重试 | 是 | +| `cancelling` | 正在请求取消 | 是 | +| `cancelled` | 已取消 | 是 | + +状态迁移规则: + +```text +pending -> processing +processing -> completed +processing -> failed +processing -> retrying +retrying -> pending +processing -> cancelling +cancelling -> cancelled +cancelling -> failed +``` + +禁止迁移: + +```text +completed -> processing +failed -> processing +cancelled -> processing +``` + +终态: + +```text +completed +failed +cancelled +``` + +Core 应校验状态迁移,插件不能随意把任意状态写入任务。 + +## 对外 API 兼容原则 + +业务客户端看到的 API 应继续保持原平台或当前项目已经定义的标准。Core 内部可以统一成 AirGate Task,但协议适配层对外返回时必须投影回原协议响应,只额外追加 `task_id` 作为 AirGate 追踪 ID。 + +核心原则: + +1. 创建入口不改。原来调用 `POST /v1/images/generations`,仍然调用这个入口;未来视频、音乐也按各自平台或插件已经定义的入口暴露。 +2. 请求 body 不强制新增通用任务字段。模型、分辨率、时长、质量等仍按原协议传递。 +3. 响应 schema 不替换成通用 Task schema。完成、处理中、失败、查询、列表都由对应协议适配层决定响应结构。 +4. 响应中允许追加 `task_id`。这个字段始终是 AirGate Task ID,不是上游任务 ID。 +5. 上游同步、上游异步、上游流式只影响 Core 内部执行模式,不影响外部 API 标准。 +6. 通用 `/v1/tasks` 可以作为 Core 管理 API、调试 API 或后台管理 API,但不作为业务客户端迁移目标。 + +### 创建入口映射 + +现有 OpenAI 风格入口继续保留: + +```text +POST /v1/images/generations +POST /v1/images/edits +``` + +协议适配层在内部映射到任务类型: + +```text +/v1/images/generations -> type=image.generate +/v1/images/edits -> type=image.edit +``` + +流程: + +```text +客户端调用原入口 +协议适配层解析原协议请求 +协议适配层构造 Task input +Core 创建 AirGate Task +Core 按状态机调度执行 +插件调用上游并写入 output / error / usage +协议适配层把 Task 结果投影回原协议响应 +响应中追加 task_id +``` + +### 完成响应 + +如果 OpenAI Images 兼容接口在等待窗口内完成,响应仍然是 Images API schema,只追加 `task_id`: + +```json +{ + "created": 1713833628, + "data": [ + { + "b64_json": "..." + } + ], + "usage": {}, + "task_id": "ag_task_123" +} +``` + +严格兼容模式下,如果某些官方 SDK 对未知字段非常敏感,可以通过网关配置决定是否追加 `task_id`;但 AirGate 自己的默认响应建议追加,便于记录、排查和后续查询。 + +### 未完成响应 + +如果调用选择后台执行,或 `wait` / `auto` 等待超时,响应也不应强制改成通用 Task schema,而应使用当前入口已有的异步响应形态。 + +例如当前项目若已有图片任务响应: + +```json +{ + "task_id": "ag_task_123", + "status": "processing" +} +``` + +就继续保持这个响应。未来视频、音乐如果某个平台标准本身有 job / task / generation 对象,也映射成对应平台对象,只把其中的 ID 换成 AirGate `task_id` 或额外追加 AirGate `task_id`。 + +如果某个同步协议本身没有异步响应标准,默认不应突然返回 AirGate Task 对象。只有在客户端显式选择异步模式,或该插件文档明确声明 AirGate 扩展响应时,才返回该协议适配层定义的异步响应。 + +### 查询和列表 + +查询入口也保持原有协议或当前项目已有路径。例如当前图片任务可以继续: + +```text +GET /v1/images/tasks/{task_id} +GET /v1/images/tasks/list +``` + +这些入口内部查询 Core Task,但响应仍由图片协议适配层生成。后续视频、音乐如果需要查询,也按各自协议风格暴露,例如: + +```text +GET /v1/videos/tasks/{task_id} +GET /v1/music/tasks/{task_id} +``` + +是否真的使用这些路径由对应插件协议决定,Core 不要求所有媒体类型共享同一条外部查询路径。 + +### 取消任务 + +取消同样由协议适配层决定是否暴露以及如何命名。Core 只提供内部取消能力: + +```text +tasks.cancel +``` + +取消能力取决于插件声明: + +- 如果上游支持取消,插件调用上游取消接口。 +- 如果上游不支持取消,Core 标记 `cancelling` 后尽力中止本地轮询或流消费。 +- 如果任务已完成,取消返回当前终态,不应破坏结果。 + +### Core 管理 API + +通用任务 API 可以存在,但它的定位是内部管理、后台页面、调试、测试或运维,不是业务客户端的标准协议: + +```text +GET /v1/tasks/{task_id} +GET /v1/tasks?type=image.generate&status=completed&limit=20&offset=0 +POST /v1/tasks/{task_id}/cancel +``` + +这类 API 可以返回 AirGate Task schema,因为调用方明确知道自己在访问 AirGate 管理能力。普通业务 API 不应直接返回这个 schema。 + +## Core 职责 + +Core 负责以下能力: + +1. 任务表结构。 +2. 状态机校验。 +3. 用户和分组权限校验。 +4. 账号调度和 failover。 +5. 后台 worker 分发任务给插件。 +6. 根据任务类型找到插件和 handler。 +7. 记录任务 attempts、max_attempts、priority。 +8. 写入 output、error、progress。 +9. 查询和列表接口。 +10. 取消任务接口。 +11. 任务结果与使用记录关联。 +12. 任务过期清理策略。 +13. 对同步等待模式实现 wait / timeout / fallback to task。 + +Core 不应理解: + +- 图片 prompt 字段怎么解析。 +- 视频时长如何映射到上游参数。 +- 音乐质量、音色、格式、循环等字段是否合法。 +- 某个模型支持哪些扩展参数。 +- 上游任务状态字段叫什么。 +- 某个平台的失败码是否可重试。 +- 某个平台的具体计费公式。 + +这些都属于插件。 + +## SDK 职责 + +SDK 需要表达三类契约。 + +### 任务 schema 声明 + +任务 schema 在新协议中的最小形态: + +```go +type TaskSchema struct { + Type string + Input PayloadSchema + Output PayloadSchema + Metadata map[string]string +} +``` + +任务类型展示名称和说明不是状态机必要字段,可以由管理后台根据 `type`、`Metadata` 或插件前端 schema 生成,不进入 Core 任务协议。 + +建议扩展或通过 `Metadata` 先承载执行策略: + +```go +type TaskSchema struct { + Type string + Input PayloadSchema + Output PayloadSchema + DefaultMode string + MaxAttempts int + Cancellable bool + ProgressMode string + ResultProtocol string + Metadata map[string]string +} +``` + +字段含义: + +| 字段 | 含义 | +| --- | --- | +| `DefaultMode` | 默认响应模式,通常是 `auto` 或 `wait` | +| `MaxAttempts` | 默认最大重试次数 | +| `Cancellable` | 是否支持取消 | +| `ProgressMode` | `none`、`percent`、`stage` | +| `ResultProtocol` | `task`、`openai.images`、`openai.responses` 等展示/兼容提示 | + +不同任务类型和模型的扩展参数通过 `Input` 的 JSON Schema 表达。Core 不为这些参数新增 Go 字段或数据库列。 + +图片任务 schema 示例: + +```json +{ + "type": "object", + "required": ["prompt"], + "properties": { + "prompt": { "type": "string" }, + "size": { "type": "string", "examples": ["1024x1024", "1536x1024"] }, + "quality": { "type": "string", "enum": ["low", "medium", "high", "auto"] }, + "background": { "type": "string", "enum": ["opaque", "transparent"] }, + "output_format": { "type": "string", "enum": ["png", "jpeg", "webp"] } + }, + "additionalProperties": true +} +``` + +视频任务 schema 示例: + +```json +{ + "type": "object", + "required": ["prompt"], + "properties": { + "prompt": { "type": "string" }, + "duration_seconds": { "type": "integer", "minimum": 1, "maximum": 60 }, + "resolution": { "type": "string" }, + "aspect_ratio": { "type": "string" }, + "fps": { "type": "integer" } + }, + "additionalProperties": true +} +``` + +音乐任务 schema 示例: + +```json +{ + "type": "object", + "required": ["prompt"], + "properties": { + "prompt": { "type": "string" }, + "duration_seconds": { "type": "integer", "minimum": 1 }, + "quality": { "type": "string" }, + "format": { "type": "string" }, + "loopable": { "type": "boolean" } + }, + "additionalProperties": true +} +``` + +这里建议 `additionalProperties: true`,原因是上游能力变化很快,插件需要能先透传新参数;插件可以按模型能力做更严格的运行时校验。管理后台和开发工具使用 schema 渲染表单和提示,Core 后端只保存 JSON。 + +如果短期不想改 SDK 强类型,可以先放入 `Metadata`: + +```go +Metadata: map[string]string{ + "default_mode": "auto", + "max_attempts": "3", + "cancellable": "false", + "progress_mode": "percent", + "result_protocol": "openai.images", +} +``` + +### 任务执行接口 + +当前 SDK 只有: + +```go +type TaskProcessor interface { + ProcessTask(ctx context.Context, task HostTask) error + TaskTypes() []string +} +``` + +中期建议演进为: + +```go +type TaskDefinitionProvider interface { + TaskDefinitions() []TaskDefinition +} + +type TaskDefinition struct { + Schema TaskSchema + Handler TaskHandler +} + +type TaskHandler interface { + Execute(ctx context.Context, task HostTask, runtime TaskRuntime) (*TaskResult, error) +} +``` + +其中 `TaskRuntime` 由 SDK 或插件本地封装,负责安全地更新状态: + +```go +type TaskRuntime interface { + SetProcessing(ctx context.Context, progress int, stage string) error + SetProgress(ctx context.Context, progress int, stage string) error + Complete(ctx context.Context, output map[string]any, usage *Usage) error + Fail(ctx context.Context, err TaskError) error + IsCancellationRequested(ctx context.Context) bool +} +``` + +短期为了减少 SDK 破坏,可以先在插件内部实现这个 runtime,Core 仍然通过 `tasks.update` 接收状态更新。 + +### Host method + +现有 Host method: + +```text +tasks.create +tasks.update +tasks.get +tasks.list +gateway.forward +``` + +建议补齐或标准化: + +```text +tasks.cancel +tasks.append_event +tasks.get_cancellation +``` + +`tasks.append_event` 用于记录阶段事件,例如: + +```json +{ + "task_id": "ag_task_123", + "event": "upstream_task_created", + "payload": { + "mode": "upstream_task" + } +} +``` + +如果暂时不做 event 表,也可以把 execution 信息存进任务 output 的内部字段,但不建议长期这么做。 + +### 资产存储 + +Core 提供统一的资产存储能力,插件不应自行实现文件下载和持久化。 + +已有 Host method: + +```text +assets.store 存储原始字节(插件已有数据在内存中) +assets.store_url 从外部 URL 下载并存储(Core 负责 HTTP 下载、大小限制、Content-Type 检测) +assets.get_url 获取已存储资产的可访问 URL +assets.get_bytes 获取已存储资产的原始字节 +``` + +职责边界: + +| 层级 | 职责 | 示例 | +| --- | --- | --- | +| Core 资产存储 | HTTP 下载、大小限制、Content-Type 检测、持久化(本地 / S3)、URL 签发 | `assets.store_url` 下载 50MB 以内的外部图片 | +| 插件 | 识别 output 中哪些字段含可下载媒体、调用对应 Host method、注册到自己的资产跟踪表 | 遍历 Images API 响应的 `data[]`,对 `b64_json` 调 `assets.store`,对外部 `url` 调 `assets.store_url` | +| Core 任务系统 | 持久化 output JSON、状态机、权限 | 不解析 output 中的 URL 或 base64 字段 | + +设计原则: + +- Core 不理解任务 output 的结构,不自动扫描或处理 output 中的媒体字段。 +- 插件在执行任务时主动调用 `assets.store` 或 `assets.store_url`,把外部 URL 或 base64 转为 Core 管理的本地 URL,再写入 output。 +- 任务完成后 output 中应只包含稳定的本地 URL,不应包含可能过期的外部签名 URL。 +- 后续新增视频、音乐等媒体类型时,插件只需调用同一组 Host method,不需要各自实现下载逻辑。 + +## 插件职责 + +插件实现任务定义,不实现通用状态机。 + +建议插件内部结构: + +```go +type TaskHandler interface { + Type() string + BuildInput(ctx context.Context, req *sdk.ForwardRequest) (map[string]any, error) + Execute(ctx context.Context, task sdk.HostTask, runtime TaskRuntime) error + BuildResponse(task *sdk.HostTask) map[string]any +} +``` + +如果同一插件有多个任务: + +```go +type TaskRegistry struct { + handlers map[string]TaskHandler +} +``` + +OpenAI 插件注册: + +```go +registry.Register(OpenAIImageGenerateTask{}) +registry.Register(OpenAIImageEditTask{}) +registry.Register(OpenAIVideoGenerateTask{}) +registry.Register(OpenAIMusicGenerateTask{}) +``` + +`ProcessTask` 只做分发: + +```go +func (g *OpenAIGateway) ProcessTask(ctx context.Context, task sdk.HostTask) error { + handler := g.tasks.Get(task.TaskType) + if handler == nil { + return fmt.Errorf("不支持的任务类型: %s", task.TaskType) + } + return g.runner.Run(ctx, task, handler) +} +``` + +## 上游执行策略 + +### 同步上游 + +流程: + +```text +Core 创建 AirGate Task +Core worker 分发给插件 +插件调用上游 +上游直接返回最终结果 +插件标准化 output +Core 标记 completed +``` + +适合: + +- 当前很多 Images API 直连上游。 +- 小文件处理。 +- 短音频生成。 + +### 异步上游 + +流程: + +```text +Core 创建 AirGate Task +插件调用上游创建任务 +上游返回 upstream_task_id +插件保存 execution 信息 +插件轮询上游状态 +上游完成 +插件获取最终结果 +Core 标记 completed +``` + +插件内部 execution 示例: + +```json +{ + "mode": "upstream_task", + "provider": "apimart", + "upstream_task_id": "img_abc", + "status_url": "https://provider.example/tasks/img_abc", + "poll_interval_ms": 3000, + "last_status": "running", + "next_poll_at": "2026-05-12T10:00:20Z", + "created_at": "2026-05-12T10:00:01Z", + "updated_at": "2026-05-12T10:00:10Z" +} +``` + +这类字段是任务恢复所必需的持久化状态,不应直接作为公共 output 暴露给普通用户;管理后台可以按权限查看脱敏信息。 + +`execution` 更新策略: + +- 插件拿到上游任务 ID 后应立即写入 `execution`,再进入轮询。 +- 每次轮询后更新 `last_status`、`updated_at`、`next_poll_at` 等恢复所需字段。 +- 如果上游返回结果 URL、文件 ID 或下载 token,优先保存可恢复的引用,不要只保存在内存里。 +- 如果字段包含签名 URL、token 或账号相关敏感信息,应加密、脱敏或只保存可重新获取结果的非敏感 ID。 +- Core 不解析 `execution` 业务字段,只负责持久化、权限隔离和传回同一插件继续执行。 + +### 流式上游 + +流程: + +```text +Core 创建 AirGate Task +插件打开 SSE / WebSocket +插件消费事件 +插件更新 progress / stage +插件聚合最终结果 +Core 标记 completed +``` + +适合: + +- ChatGPT OAuth WebSocket 图片工具。 +- 后续可能的流式视频生成进度。 + +## 任务与使用记录 + +任务表不是审计和计费事实表。AirGate 里所有可计费或需要审计的调用都应该落到统一使用记录里,包括: + +- 图片生成和图片编辑。 +- 视频生成。 +- 音乐生成。 +- 语音合成。 +- 未来其它工具型、媒体型或文件型任务。 + +普通对话模型调用、Responses / Chat Completions 调用、Anthropic Messages 协议翻译等不需要任务状态机。它们仍然通过现有同步或流式 Forward 路径直接写 Usage。任务状态机只处理长耗时、可后台化、需要查询状态的生成或处理类任务。 + +任务和使用记录的关系: + +```text +同步普通调用 + ForwardOutcome.Usage -> Core 写使用记录 + +异步任务调用 + Core 创建 Task + 插件执行 Task + 插件返回 Usage + Core 写使用记录 + Task.usage_id -> Usage.id +``` + +也就是说,任务只回答“这件事做到哪一步了”,使用记录回答“这次调用实际用了什么、产出了什么计量、花了多少钱”。 + +### Usage 统一维度 + +SDK 已经提供通用用量结构: + +```go +type Usage struct { + Model string + Summary string + Attributes []UsageAttribute + Metrics []UsageMetric + CostDetails []UsageCostDetail + Metadata map[string]string +} +``` + +模型和扩展参数应进入 Usage,而不是 Core 任务固定列。对话类调用也使用同一个 Usage 结构,但不经过 Task。 + +图片生成示例: + +```json +{ + "model": "gpt-image-2", + "summary": "图片生成 · gpt-image-2 · 1024x1024", + "attributes": [ + { "key": "modality", "kind": "custom", "label": "类型", "value": "image" }, + { "key": "model", "kind": "model", "label": "模型", "value": "gpt-image-2" }, + { "key": "resolution", "kind": "resolution", "label": "分辨率", "value": "1024x1024" }, + { "key": "quality", "kind": "quality", "label": "质量", "value": "high" } + ], + "metrics": [ + { "key": "image_count", "kind": "image", "label": "图片张数", "unit": "image", "value": 1 }, + { "key": "output_tokens", "kind": "token", "label": "图像输出 Token", "unit": "token", "value": 4160 } + ] +} +``` + +视频生成示例: + +```json +{ + "model": "video-model", + "summary": "视频生成 · 8s · 1080p", + "attributes": [ + { "key": "modality", "kind": "custom", "label": "类型", "value": "video" }, + { "key": "model", "kind": "model", "label": "模型", "value": "video-model" }, + { "key": "resolution", "kind": "resolution", "label": "分辨率", "value": "1080p" }, + { "key": "quality", "kind": "quality", "label": "质量", "value": "standard" } + ], + "metrics": [ + { "key": "video_seconds", "kind": "video", "label": "视频时长", "unit": "second", "value": 8 } + ] +} +``` + +音乐生成示例: + +```json +{ + "model": "music-model", + "summary": "音乐生成 · 30s · high", + "attributes": [ + { "key": "modality", "kind": "custom", "label": "类型", "value": "music" }, + { "key": "model", "kind": "model", "label": "模型", "value": "music-model" }, + { "key": "quality", "kind": "quality", "label": "质量", "value": "high" }, + { "key": "format", "kind": "custom", "label": "格式", "value": "mp3" } + ], + "metrics": [ + { "key": "audio_seconds", "kind": "audio", "label": "音频时长", "unit": "second", "value": 30 } + ] +} +``` + +### Task 与 Usage 的字段边界 + +| 信息 | Task 中的位置 | Usage 中的位置 | 说明 | +| --- | --- | --- | --- | +| 生命周期状态 | `status`、`progress`、`stage` | 不存 | Task 独有 | +| 客户端请求参数 | `input` | 可按需复制到 `Attributes` | 完整参数以 Task input 为准 | +| 上游执行细节 | `execution` | 可脱敏后进入 `Metadata` | 普通用户默认不看 execution | +| 模型 | `input.model` / `attributes.model` 临时展示 | `Model` / `Attributes` | 计费与审计以 Usage 为准 | +| 分辨率、时长、质量 | `input` / `attributes` 临时展示 | `Attributes` / `Metrics` | 统计以 Usage 为准 | +| token、图片张数、视频秒数 | 不建议存 Task 固定列 | `Metrics` | 统一统计入口 | +| 成本 | 不建议存 Task 固定列 | `AccountCost` / `CostDetails` | 统一扣费入口 | +| 使用记录关联 | `usage_id` | `id` | Task 完成后关联 | + +列表页如果要展示未完成任务,可以读取 Task 的 `attributes`,或按插件声明的 schema 组合展示文案。完成后的历史账单、统计图、成本明细必须读取 Usage。 + +## OpenAI 图片迁移映射 + +当前行为: + +```text +/v1/images/generations +/v1/images/edits +/v1/images/tasks +/v1/images/tasks/list +``` + +目标映射: + +| 当前入口 | 目标任务类型 | +| --- | --- | +| `/v1/images/generations` | `image.generate` | +| `/v1/images/edits` | `image.edit` | + +当前上游路径: + +| 上游情况 | Execution Mode | +| --- | --- | +| API Key 上游直接返回 Images JSON | `sync_result` | +| API Key 中转返回上游 `task_id` | `upstream_task` | +| OAuth Responses tool / WebSocket 聚合图片 | `stream_result` | +| Web Reverse 生成后返回图片 | `sync_result` 或 `stream_result`,取决于实现细节 | + +迁移后的插件内部结构: + +```text +task_registry.go +task_runner.go +task_http.go +task_image_generate.go +task_image_edit.go +``` + +`task_images.go` 不再同时承担所有职责。 + +## 数据模型建议 + +Core 任务表建议字段: + +```text +id +plugin_id +task_type +status +progress +stage +user_id +input +output +attributes +execution +error_type +error_code +error_message +usage_id +attempts +max_attempts +priority +idempotency_key +created_at +updated_at +started_at +completed_at +cancel_requested_at +expires_at +``` + +字段说明: + +| 字段 | 说明 | +| --- | --- | +| `input` | 插件标准化后的任务输入 | +| `output` | 插件标准化后的任务输出 | +| `attributes` | 插件提供的少量展示/筛选维度,值建议统一转字符串 | +| `execution` | 插件内部执行状态,例如 upstream task id、轮询状态;必须持久化以支持重启恢复 | +| `usage_id` | 关联使用记录;完成后的模型、计量和费用事实以 usage 为准 | +| `idempotency_key` | 防止客户端重复创建相同任务 | +| `expires_at` | 任务结果保留时间 | + +`execution` 应有权限边界。普通用户查询任务时不返回或只返回脱敏摘要。 + +## 错误模型 + +任务错误建议统一为: + +```json +{ + "type": "upstream_error", + "code": "rate_limited", + "message": "请求暂时无法完成,请稍后重试", + "retryable": true +} +``` + +错误类型建议: + +| type | 含义 | +| --- | --- | +| `invalid_request` | 客户端参数错误 | +| `auth_error` | 上游认证失败 | +| `rate_limited` | 上游限流 | +| `quota_exceeded` | 额度不足 | +| `upstream_error` | 上游服务错误 | +| `timeout` | 执行超时 | +| `cancelled` | 用户取消 | +| `internal_error` | Core 或插件内部错误 | + +插件负责把上游错误映射到标准错误。Core 负责按错误类型决定是否重试和如何展示。 + +## 重试策略 + +Core 应控制重试次数,插件提供错误是否可重试的建议。 + +建议: + +- `invalid_request` 不重试。 +- `auth_error` 不重试,并可能标记账号不可用。 +- `rate_limited` 可以按 `retry_after` 重试。 +- `upstream_error` 可以重试。 +- `timeout` 可以重试,但要限制总耗时。 +- 用户取消不重试。 + +任务 attempt 不应重复扣费。只有上游成功返回可计费用量时才写使用记录;失败费用是否记录由插件按平台规则决定。 + +## 同步等待策略 + +Core 对 `wait` / `auto` 模式应有统一等待窗口。 + +建议配置: + +```text +default_wait_timeout = 120s +max_wait_timeout = 300s +background_poll_interval = 1s +``` + +等待模式流程: + +```text +创建任务 +立即调度 +等待 completed / failed / cancelled +如果超时: + 协议适配层返回原协议的异步响应 + task_id +如果完成: + 协议适配层返回原协议最终响应 + task_id +``` + +这使同步上游和异步上游都可以对外表现为原同步接口;只有等待超时或显式后台模式时,才返回该协议自己的异步响应。 + +## 结果协议 + +任务 output 应在 Core 内部统一,但对外必须支持原协议返回。业务客户端不直接消费 `task_output`,而是消费协议适配层生成的响应。 + +统一任务 output: + +```json +{ + "kind": "image", + "items": [ + { + "mime_type": "image/png", + "b64_json": "..." + } + ], + "usage": {} +} +``` + +OpenAI Images 兼容响应: + +```json +{ + "created": 1713833628, + "data": [ + { + "b64_json": "..." + } + ], + "usage": {} +} +``` + +插件应提供 output 到协议响应的转换器。Core 不理解图片字段,只调用插件声明的转换能力,或让插件在任务完成时同时写入: + +```json +{ + "task_output": {}, + "protocol_outputs": { + "openai.images": {} + } +} +``` + +短期可以先让插件的旧入口查询任务后自行转换;长期再把协议转换纳入 SDK 能力。 + +## 权限与安全 + +Core 查询任务时必须校验: + +- 当前用户只能查询自己的任务。 +- 管理员可以查询所有任务。 +- API Key 只能查询该 Key 所属用户/分组创建的任务。 +- 插件只能更新自己创建的任务。 +- 插件不能越权读取其他插件任务。 + +敏感字段: + +- 上游 access token 不得进入 task input / output。 +- 上游 `upstream_task_id` 默认不返回普通用户。 +- 上游原始错误需要脱敏后再写入 `error.message`。 +- 原始响应如包含签名 URL,需要设置过期和权限边界。 + +## 幂等性 + +创建任务应支持幂等键: + +```text +Idempotency-Key: xxx +``` + +或: + +```json +{ + "idempotency_key": "xxx" +} +``` + +同一用户、同一插件、同一任务类型、同一幂等键,在任务未过期前应返回同一个 AirGate Task。 + +这能避免客户端超时重试时重复生成图片/视频并重复扣费。 + +## 进度语义 + +进度应是 Core 通用字段,但含义由插件声明。 + +建议: + +| progress | 通用含义 | +| --- | --- | +| 0 | 已创建 | +| 10 | 开始处理 | +| 30 | 已发送到上游 | +| 50 | 上游处理中 | +| 80 | 正在下载或整理结果 | +| 100 | 完成 | + +插件可以附加 `stage`: + +```json +{ + "progress": 50, + "stage": "polling_upstream" +} +``` + +普通客户端可以只看百分比,高级 UI 可以展示 stage。 + +## 迁移计划 + +### 阶段一:文档与内部抽象 + +目标:不改外部行为,先让 `airgate-openai` 内部不再写死图片状态机。 + +改动: + +- 新增插件内部 `TaskRegistry`。 +- 新增插件内部 `TaskRunner`。 +- 把当前 `image_generation` 迁为 `image.generate` / `image.edit` 的 handler。 +- `ProcessTask` 改为按 `task.TaskType` 分发。 +- 查询响应改为通用 `buildTaskResponse`,图片字段由 handler 注入。 +- 保留 `/v1/images/tasks` 和 `/v1/images/tasks/list`,但内部调用通用查询函数。 + +验收: + +- 现有图片任务行为不变。 +- 新增任务类型不需要改 `ProcessTask` 主流程。 +- 后端测试通过。 + +### 阶段二:SDK schema 补齐 + +目标:插件能声明结构化任务能力。 + +改动: + +- 扩展 `TaskSchema` 或先用 `Metadata` 标准化字段。 +- `SchemaProvider` 返回 task schema。 +- devserver 展示任务 schema。 +- Core 能读取插件 task schema。 + +验收: + +- `airgate-openai` 能声明 `image.generate` / `image.edit`。 +- Core 管理后台能看到插件支持的任务类型。 + +### 阶段三:Core 通用任务服务 + +目标:Core 提供内部统一任务服务,协议适配层通过它创建、查询、取消和更新任务;业务客户端仍走原 API。 + +改动: + +- Core 新增通用任务创建、查询、列表、取消 service。 +- Core 实现 wait/background/auto。 +- Core 实现幂等键。 +- Core worker 支持按 task type 分发给插件。 +- Core 对 task output 做权限过滤。 +- 可选提供受权限保护的 `/v1/tasks` 管理 API,用于后台、调试和运维。 + +验收: + +- 图片协议适配层可以通过 Core service 查询 `task_id` 对应的内部任务。 +- 同步上游和异步上游在原图片 API 中表现一致。 +- 普通业务客户端不需要迁移到 `/v1/tasks`。 + +### 阶段四:协议入口接入 Core 任务服务 + +目标:OpenAI Images 等原有入口复用 Core 任务服务,但对外响应保持原协议。 + +改动: + +- `/v1/images/generations` 内部创建 `image.generate` 任务,完成时返回 Images schema 并追加 `task_id`。 +- `/v1/images/edits` 内部创建 `image.edit` 任务,完成时返回 Images schema 并追加 `task_id`。 +- `/v1/images/tasks` 和 `/v1/images/tasks/list` 继续保持当前响应结构,内部改为查询 Core Task。 +- 新增视频/音乐任务时只加 handler 和 schema。 + +验收: + +- OpenAI SDK 默认路径仍可同步拿结果。 +- 返回中能看到 AirGate `task_id`。 +- 显式异步模式返回当前协议定义的异步响应,而不是通用 AirGate Task schema。 +- 视频/音乐不新增专属状态机。 + +### 阶段五:清理内部重复状态机 + +目标:清理插件内部按图片写死的状态机和轮询代码,保留对外兼容路由。 + +前提: + +- Core 通用任务服务已稳定。 +- 图片入口已通过协议适配层读写 Core Task。 +- 视频、音乐或其他任务类型能复用同一套 runner。 + +改动: + +- 删除插件里的图片专用状态迁移、轮询和重试分支。 +- 保留 `/v1/images/tasks` 和 `/v1/images/tasks/list` 的对外兼容壳。 +- 查询函数只负责协议响应投影,不再自己管理状态机。 + +## 当前 `airgate-openai` 应先怎么改 + +建议先做插件内部重构,不等待 Core 完整改造。 + +第一步文件结构: + +```text +backend/internal/gateway/task_registry.go +backend/internal/gateway/task_runner.go +backend/internal/gateway/task_http.go +backend/internal/gateway/task_image.go +``` + +职责: + +| 文件 | 职责 | +| --- | --- | +| `task_registry.go` | 注册和查找 task handler | +| `task_runner.go` | 通用状态迁移、调用 `forwardViaHost`、错误处理 | +| `task_http.go` | 从 HTTP 请求解析 task_id、列表参数、写当前协议的任务查询响应 | +| `task_image.go` | OpenAI 图片 input/output 编解码 | + +第二步替换点: + +- `TaskTypes()` 从 registry 生成。 +- `ProcessTask()` 从 registry 查 handler。 +- `forwardImagesViaTask()` 改成 `forwardTask()`,图片 handler 负责 `BuildInput`。 +- `buildTaskRequestBody()` 移入图片 handler。 +- `buildTaskResponse()` 支持 handler 自定义 output projection。 + +第三步保留行为: + +- 仍然支持 `/v1/images/tasks`。 +- 仍然返回当前图片任务响应字段。 +- 仍然支持当前 API Key 同步上游和上游异步 `task_id` 轮询。 + +这样后续加视频时只做: + +```go +registry.Register(OpenAIVideoGenerateTask{}) +``` + +不改状态机。 + +## 测试计划 + +Core 测试: + +- 创建内部任务后能返回 AirGate `task_id` 给协议适配层。 +- wait 模式任务完成时返回最终结果。 +- wait 超时返回待处理状态和 `task_id`,由协议适配层投影成原协议异步响应。 +- background 模式立即返回 `task_id` 给协议适配层。 +- 查询任务权限校验。 +- 状态机非法迁移被拒绝。 +- 幂等键重复请求返回同一任务。 +- 取消 pending / processing / completed 任务。 + +SDK 测试: + +- `TaskSchema` protobuf 往返。 +- `TaskTypes` / `TaskDefinitions` 能被 gRPC runtime 正确暴露。 +- Host method capability 校验。 + +插件测试: + +- 图片文生图请求能构造 `image.generate` input。 +- 图片编辑请求能构造 `image.edit` input。 +- 同步上游响应能完成任务。 +- 上游 `task_id` 响应能进入轮询并完成任务。 +- 完成响应保持 OpenAI Images schema,并额外包含 AirGate `task_id`。 +- 未完成响应保持当前图片任务响应结构,不返回通用 AirGate Task schema。 +- 上游错误能写入标准 error。 +- `ProcessTask` 遇到未知 task type 返回明确错误。 +- 旧 `/v1/images/tasks` 查询仍然兼容。 + +## 开放问题 + +1. 通用 `/v1/tasks` 是否需要对外暴露为管理 API;如果暴露,权限边界和返回字段需要单独设计。 +2. `task_id` 使用数字 ID 还是字符串 ID?建议对外使用字符串,例如 `ag_task_123`,内部可继续用 bigint。 +3. OpenAI SDK 兼容路径默认应该是 `wait` 还是 `auto`?建议默认 `wait`,显式请求才异步。 +4. task output 是否长期保存完整 base64?图片/视频结果可能很大,后续需要对象存储或结果过期策略。 +5. 插件是否需要实现协议响应转换接口,还是由插件在 output 中同时写入兼容响应? +6. 取消任务是否必须进入 SDK 强类型接口,还是先通过 Host method 实现? + +## 结论 + +异步任务不应该作为图片功能的附属能力存在。它应该是 AirGate Core 的通用执行模型: + +- Core 管任务生命周期。 +- SDK 管任务契约声明。 +- 插件管任务业务执行。 +- 协议适配层把原 API 请求转成内部 Task,再把 Task 结果投影回原 API 响应。 +- 业务客户端继续看原协议响应,只额外拿到 AirGate `task_id`,不看上游同步/异步差异。 + +当前最稳妥的落地路径是先在 `airgate-openai` 内部抽出任务注册表和 runner,保持现有接口不变并追加 `task_id`;然后补 SDK schema;最后让 Core 接管统一任务服务。通用 `/v1/tasks` 只作为管理和调试能力考虑,不作为业务客户端的新标准。 diff --git a/docs/plugin-style-guide.md b/docs/plugin-style-guide.md index bd5245e..578a8d4 100644 --- a/docs/plugin-style-guide.md +++ b/docs/plugin-style-guide.md @@ -27,6 +27,8 @@ your-plugin/ ## 2. 依赖配置 +插件前端 SDK 的正式包名是 `@doudou-start/airgate-theme`。插件业务代码优先使用 `@doudou-start/airgate-theme/plugin`,该入口提供主题初始化、样式作用域、Tailwind bridge、插件前端类型和公共 UI 组件。 + ### package.json ```json @@ -37,7 +39,7 @@ your-plugin/ "dev": "vite build --watch" }, "dependencies": { - "@airgate/theme": "file:../../airgate-sdk/frontend", + "@doudou-start/airgate-theme": "^1.0.0", "react": "^19.0.0", "react-dom": "^19.0.0" }, @@ -53,7 +55,7 @@ your-plugin/ } ``` -> **注意**:`react` 和 `react-dom` 仅用于类型,运行时由 Core 通过 `window.__airgate_shared` 提供。 +> **注意**:`react` 和 `react-dom` 不打包进插件产物;运行时由 Core 或 devserver 通过 import map 或等价模块别名提供。 ### vite.config.ts @@ -72,7 +74,7 @@ export default defineConfig({ outDir: 'dist', rollupOptions: { // React 由 Core 提供,不要打包 - external: ['react', 'react-dom', 'react/jsx-runtime'], + external: ['react', 'react-dom', 'react-dom/client', 'react/jsx-runtime'], }, }, }); @@ -82,7 +84,7 @@ export default defineConfig({ ```ts import type { Config } from 'tailwindcss'; -import { createPluginTailwindConfig } from '@airgate/theme/plugin'; +import { createPluginTailwindConfig } from '@doudou-start/airgate-theme/plugin'; const config: Config = { content: ['./src/**/*.{ts,tsx}'], @@ -116,7 +118,7 @@ module.exports = { ### theme/runtime.ts ```ts -import { ensurePluginStyleFoundation } from '@airgate/theme/plugin'; +import { ensurePluginStyleFoundation } from '@doudou-start/airgate-theme/plugin'; import tailwindCssText from '../styles/tailwind.css?inline'; export const THEME_SCOPE_SELECTOR = '[data-ag-YOUR_PLUGIN-root]'; @@ -124,13 +126,11 @@ export const THEME_ATTRIBUTE = 'data-theme'; export const STYLE_ID = 'ag-YOUR_PLUGIN-theme-vars'; export const FOUNDATION_STYLE_ID = 'ag-YOUR_PLUGIN-plugin-foundation'; export const TAILWIND_STYLE_ID = 'ag-YOUR_PLUGIN-tailwind'; -export const STORAGE_KEY = 'ag-YOUR_PLUGIN-theme'; export function ensurePluginStyles(): void { ensurePluginStyleFoundation({ scopeSelector: THEME_SCOPE_SELECTOR, themeAttribute: THEME_ATTRIBUTE, - storageKey: STORAGE_KEY, themeStyleId: STYLE_ID, foundationStyleId: FOUNDATION_STYLE_ID, extraCssText: tailwindCssText, // 注入编译好的 Tailwind CSS @@ -151,8 +151,11 @@ import { ensurePluginStyles } from './theme/runtime'; ensurePluginStyles(); export default { - accountForm: YourComponent, // 或 routes / menuItems + accountCreate: YourComponent, // 或 accountEdit / accountIdentity / accountUsageWindow / usageMetricDetail / usageCostDetail / routes / menuItems }; + +// 使用记录“模型”列行级别扩展展示(可选) +// usageModelMeta: (props: UsageRecordSurfaceProps) => ReactNode ``` ## 4. 组件根节点 — 作用域绑定 @@ -162,7 +165,7 @@ export default { 2. 使用 `useScopedPluginTheme` 跟随 Core 的明暗切换 ```tsx -import { useScopedPluginTheme } from '@airgate/theme/plugin'; +import { useScopedPluginTheme } from '@doudou-start/airgate-theme/plugin'; const THEME_ATTRIBUTE = 'data-theme'; const STORAGE_KEY = 'ag-YOUR_PLUGIN-theme'; @@ -187,42 +190,42 @@ export function YourComponent(props) { ## 5. 设计 Token 参考 -所有插件样式通过 CSS 变量 `--ag-*` 引用,Tailwind 工具类已映射好(带 `agw-` 前缀)。 +所有插件样式通过 CSS 变量 `--ag-*` 引用,Tailwind 工具类已映射好(带 `agw-` 前缀)。具体 token 值由 `theme/src/tokens.ts` 生成,文档只说明语义,避免样式值漂移。 ### 颜色 -| Token | Tailwind 类 | 暗色值 | 用途 | -|---|---|---|---| -| `--ag-primary` | `agw-text-primary` / `agw-bg-primary` | `#2dd4a8` | 主操作、链接、选中态 | -| `--ag-primary-hover` | `agw-bg-primary-hover` | `#5de8c2` | 主色悬停 | -| `--ag-primary-subtle` | `agw-bg-primary-subtle` | `rgba(45,212,168,0.10)` | 主色背景/高亮 | -| `--ag-primary-glow` | — | `rgba(45,212,168,0.18)` | 发光阴影 | -| `--ag-success` | `agw-text-success` | `#22c55e` | 成功状态 | -| `--ag-warning` | `agw-text-warning` | `#f59e0b` | 警告状态 | -| `--ag-danger` | `agw-text-danger` | `#ef4444` | 错误/删除 | -| `--ag-info` | `agw-text-info` | `#60a5fa` | 信息/辅助色 | +| Token | Tailwind 类 | 用途 | +|---|---|---| +| `--ag-primary` | `agw-text-primary` / `agw-bg-primary` | 主操作、链接、选中态 | +| `--ag-primary-hover` | `agw-bg-primary-hover` | 主色悬停 | +| `--ag-primary-subtle` | `agw-bg-primary-subtle` | 主色背景/高亮 | +| `--ag-primary-glow` | — | 发光阴影 | +| `--ag-success` | `agw-text-success` | 成功状态 | +| `--ag-warning` | `agw-text-warning` | 警告状态 | +| `--ag-danger` | `agw-text-danger` | 错误/删除 | +| `--ag-info` | `agw-text-info` | 信息/辅助色 | ### 背景层级 从深到浅,形成空间层次: -| Token | Tailwind 类 | 暗色值 | 用途 | -|---|---|---|---| -| `--ag-bg-deep` | `agw-bg-bg-deep` | `#0a0a0c` | 页面最底层 | -| `--ag-bg` | `agw-bg-bg` | `#111113` | 侧边栏/主面板 | -| `--ag-bg-elevated` | `agw-bg-bg-elevated` | `#18181b` | 卡片/弹窗/下拉 | -| `--ag-bg-surface` | `agw-bg-surface` | `#1e1e21` | 输入框/表单区域 | -| `--ag-bg-hover` | `agw-bg-bg-hover` | `#27272a` | 悬停态背景 | -| `--ag-bg-active` | `agw-bg-bg-active` | `#303033` | 按下/激活态 | +| Token | Tailwind 类 | 用途 | +|---|---|---| +| `--ag-bg-deep` | `agw-bg-bg-deep` | 页面最底层 | +| `--ag-bg` | `agw-bg-bg` | 侧边栏/主面板 | +| `--ag-bg-elevated` | `agw-bg-bg-elevated` | 卡片/弹窗/下拉 | +| `--ag-bg-surface` | `agw-bg-surface` | 输入框/表单区域 | +| `--ag-bg-hover` | `agw-bg-bg-hover` | 悬停态背景 | +| `--ag-bg-active` | `agw-bg-bg-active` | 按下/激活态 | ### 文字 -| Token | Tailwind 类 | 暗色值 | 用途 | -|---|---|---|---| -| `--ag-text` | `agw-text-text` | `#ececf0` | 主文字 | -| `--ag-text-secondary` | `agw-text-text-secondary` | `#a1a1aa` | 次要文字/标签 | -| `--ag-text-tertiary` | `agw-text-text-tertiary` | `#63636e` | 提示文字/占位符 | -| `--ag-text-inverse` | `agw-text-text-inverse` | `#0a0a0c` | 反色(主色按钮文字) | +| Token | Tailwind 类 | 用途 | +|---|---|---| +| `--ag-text` | `agw-text-text` | 主文字 | +| `--ag-text-secondary` | `agw-text-text-secondary` | 次要文字/标签 | +| `--ag-text-tertiary` | `agw-text-text-tertiary` | 提示文字/占位符 | +| `--ag-text-inverse` | `agw-text-text-inverse` | 反色(主色按钮文字) | ### 边框 @@ -235,17 +238,18 @@ export function YourComponent(props) { ### 其他 -| Token | Tailwind 类 | 值 | +| Token | Tailwind 类 | 用途 | |---|---|---| -| `--ag-radius-sm` | `agw-rounded-sm` | `6px` | -| `--ag-radius-md` | `agw-rounded-md` | `10px` | -| `--ag-radius-lg` | `agw-rounded-lg` | `14px` | -| `--ag-font-sans` | `agw-font-sans` | Inter | -| `--ag-font-mono` | `agw-font-mono` | JetBrains Mono | +| `--ag-radius-sm` | `agw-rounded-sm` | 小圆角 | +| `--ag-radius-md` | `agw-rounded-md` | 中圆角 | +| `--ag-radius-lg` | `agw-rounded-lg` | 大圆角 | +| `--ag-field-radius` | `agw-rounded-field` | 表单控件圆角 | +| `--ag-font-sans` | `agw-font-sans` | 正文字体 | +| `--ag-font-mono` | `agw-font-mono` | 等宽字体 | ## 6. SDK 提供的 UI 组件 -SDK 提供了一套预制组件(`@airgate/theme/plugin`),样式与 Core 保持一致,**优先使用这些组件**: +SDK 提供了一套预制组件(`@doudou-start/airgate-theme/plugin`),样式与 Core 保持一致,属于插件前端稳定公共契约。插件业务 UI **优先使用这些组件**: ```tsx import { @@ -261,7 +265,7 @@ import { FormActions, // 表单操作区(flex wrap) Badge, // 标签徽章(neutral / success / violet / info) StatusText, // 内联状态文字(info / success / error) -} from '@airgate/theme/plugin'; +} from '@doudou-start/airgate-theme/plugin'; ``` ### 使用示例 @@ -376,7 +380,7 @@ npm run build 修改 SDK token 后的刷新流程: ```bash -cd airgate-sdk/frontend && npm run build # 1. 编译 SDK +cd airgate-sdk/theme && npm run build # 1. 编译 SDK cd your-plugin/web && npm run build # 2. 重建插件(打包新 token) # 3. 刷新浏览器 ``` diff --git a/docs/sdk-package-boundaries.md b/docs/sdk-package-boundaries.md new file mode 100644 index 0000000..f5c02fb --- /dev/null +++ b/docs/sdk-package-boundaries.md @@ -0,0 +1,111 @@ +# SDK 包边界 + +本文定义 `airgate-sdk` 单仓库内的包职责边界。新增能力必须先判断归属,不能默认修改 `sdkgo` 根接口。 + +## 分层 + +| 包 | 职责 | 可依赖 | 不应包含 | +| --- | --- | --- | --- | +| `sdkgo` | 插件作者 API、共享类型、capability helper、日志 helper | Go 标准库、少量稳定依赖 | protobuf、gRPC、go-plugin、devserver、Core 产品逻辑 | +| `protocol/proto` | protobuf schema 与生成代码 | protobuf runtime | 插件业务 helper、Core 实现细节 | +| `runtimego/grpc` | go-plugin/gRPC 适配、stream bridge、proto 转换、Core 反向调用 broker | `sdkgo`、`protocol/proto` | 插件业务逻辑、devserver UI | +| `devkit/devserver` | 本地开发服务器和 fake core 能力 | `sdkgo` | 生产运行时依赖、Core 数据库访问 | +| `theme` | 前端插件 API、主题 token、样式注入、Tailwind bridge、公共 UI 组件 | TypeScript 生态、React peer dependency | Go runtime、Core 后端逻辑、具体插件业务页面 | + +## 新需求判断 + +只有以下变化可以修改 `sdkgo` 稳定接口: + +- 新增稳定插件角色或生命周期钩子。 +- 新增跨插件通用领域原语。 +- 新增需要 SDK 表达的跨插件通用 capability。 +- 修正已有公开契约的错误或缺口。 + +只有以下变化可以修改 `theme` 稳定接口: + +- 新增跨插件通用的主题 token、CSS 变量或 Tailwind bridge 能力。 +- 新增多个插件都会复用的基础 UI 组件。 +- 新增插件前端模块加载、账号表单、路由、菜单、OAuth 桥接等公共类型。 +- 修正已有公共组件、样式 helper 或主题契约的错误。 + +以下变化不应直接修改 `sdkgo` 根接口: + +- 某个 provider 新增参数、route、错误格式或模型字段。 +- 某个 UI 页面需要额外产品数据。 +- 某个插件需要私有后台任务状态。 +- 某个平台新增计费规则、套餐、价格档位、重置窗口或用量展示字段。 +- 某个实验性功能只服务单个官方插件。 +- Core 内部实现为了方便调用而需要的 helper。 + +这些变化应优先放到 manifest、插件私有 metadata、Core 方法注册表、插件私有数据库或明确 schema 的插件 API 中。 + +前端页面需求应优先进入具体插件前端代码。只有当样式、组件或类型能被多个插件稳定复用时,才进入 `theme`。 + +## 弱契约扩展点 + +SDK 提供少量弱契约扩展点,用来承接展示、分类和通用计量等变化,避免为每个插件需求新增强类型字段: + +- `PluginInfo.Metadata`:插件市场分类、标签、展示提示等。 +- `ModelInfo.Metadata`:模型家族、展示分组、供应商标签等。 +- `RouteDefinition.Metadata`:路由文档链接、展示分组、调试提示等。 +- `Usage.Attributes`:模型、思考层级、分辨率、质量档、服务档位等非数值审计维度。 +- `Usage.Metrics`:图片张数、视频秒数、音频分钟数、工具调用次数、token 等插件计算后的通用计量结果。 +- `Usage.CostDetails`:费用明细;插件填账号成本,Core 填用户扣费和倍率。 +- `Usage.Metadata`:单次调用的展示或审计辅助信息。 +- `EventHandler`:Core 向插件推送标准事件。 +- `Host.Invoke` / `Host.InvokeStream`:插件用 `method + payload` 调用 Core 开放的方法,必须由 `host.invoke` 或 `host.invoke.` capability 门控。 +- `SchemaProvider`:插件声明 routes、tasks、events、invokes 的 payload schema;流式 Host method 用 `InvokeSchema.Transport`、`ClientFrame`、`ServerFrame` 描述传输模式和帧结构。 + +这些字段不能用于权限、调度、账号状态机或敏感数据传递。平台计费规则不得进入 SDK;网关插件负责计算标准账号成本 `Usage.AccountCost` / `Currency` 和审计明细。Core 统一入库、索引、汇总,并写入 `UserCost` / `BillingMultiplier`。 + +## 用量与计费边界 + +SDK 不提供 `CalculateCost`、价格档位、token 拆分公式或平台套餐模型。 + +- 模型声明只包含 `ID`、`Name`、上下文窗口、最大输出和能力标签。 +- 单次调用的标准账号成本由网关插件写入 `Usage.AccountCost` / `Usage.Currency`。 +- Core 根据用户、分组、模型等倍率计算用户侧扣费,写入 `Usage.UserCost` / `Usage.BillingMultiplier`。 +- 模型、思考层级、分辨率、质量档等非数值维度统一放入 `Usage.Attributes`。 +- token、图片、音频、视频、请求数等数值明细统一放入 `Usage.Metrics`。 +- 标准账号成本和用户扣费拆分统一放入 `Usage.CostDetails`:插件填 `AccountCost`,Core 填 `UserCost` / `BillingMultiplier`。 +- 使用记录和账号管理页面由插件前端与插件私有 API 实现,SDK 不定义账号用量查询 RPC。 +- Core 不应把平台规则写入 SDK 或 Core 公共逻辑;Core 统一入库 `Usage`,需要页面时加载插件的静态资源和 API 代理。 + +## 网关前端边界 + +网关插件可以通过 `FrontendPages` / `FrontendWidgets` 声明账号相关入口,通过 `WebAssetsProvider` 提供静态资源。Core 负责加载资源、传递登录上下文和代理插件 API,不理解页面内部数据结构。 + +账号管理不作为整页 slot。Core 保留通用账号列表和详情框架,插件只补平台差异片段: + +- `account-identity`:账号标识、套餐、状态等平台差异信息。 +- `account-create`:添加账号。 +- `account-edit`:编辑账号。 +- `account-usage-window`:账号用量窗口、额度、重置时间等平台差异信息。 +- `usage-metric-detail`:使用记录里的计量明细,例如 token、模型、思考层级、分辨率、图片张数。 +- `usage-cost-detail`:使用记录里的费用明细,例如单价、账号成本、Core 倍率、用户扣费。 + +新增平台页面优先放在插件自己的 `FrontendPages`;只有多个插件都需要同一宿主位置时,才新增通用 slot。 + +## Host 调用约束 + +SDK 的 `Host` 接口只保留通用调用通道:`Invoke(ctx, req)` 和 `InvokeStream(ctx, req)`。新增 Core 宿主能力不得向 `Host` 追加业务方法;应在 Core 注册一个 method,并声明 method 的权限、schema、传输模式和实现。 + +Core method 必须至少定义: + +- method 名称,例如 `scheduler.select_account`、`tasks.update`。 +- 允许的插件类型和可调用插件范围。 +- 所需 capability,至少 `host.invoke`,敏感方法应使用 `host.invoke.`。 +- 请求、响应和流式 frame payload schema。 +- 传输模式:普通 request/response、server stream、client stream 或 bidirectional stream。 +- 幂等策略、敏感字段暴露规则和审计日志。 + +SDK 只提供传输契约和自检 helper,不承载 Core 方法枚举。这样 Core 后续扩展 method 时,不需要为每个能力修改 SDK。 + +## 当前导入路径 + +- 插件业务代码使用 `github.com/DouDOU-start/airgate-sdk/sdkgo`。 +- 插件运行入口使用 `github.com/DouDOU-start/airgate-sdk/runtimego/grpc`。 +- 本地开发工具使用 `github.com/DouDOU-start/airgate-sdk/devkit/devserver`。 +- 普通插件业务代码不直接导入 `protocol/proto`。 +- 插件前端使用 `@doudou-start/airgate-theme/plugin` 引用样式隔离、主题同步、Tailwind helper 和公共 UI 组件。 +- 宿主前端或工具代码可使用 `@doudou-start/airgate-theme`、`@doudou-start/airgate-theme/css`、`@doudou-start/airgate-theme/tailwind` 引用 token 与主题桥接能力。 diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml deleted file mode 100644 index b5ba770..0000000 --- a/frontend/pnpm-lock.yaml +++ /dev/null @@ -1,48 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - devDependencies: - '@types/react': - specifier: ^19.0.0 - version: 19.2.14 - react: - specifier: ^19.0.0 - version: 19.2.5 - typescript: - specifier: ^5.7.0 - version: 5.9.3 - -packages: - - '@types/react@19.2.14': - resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} - - csstype@3.2.3: - resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} - - react@19.2.5: - resolution: {integrity: sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==} - engines: {node: '>=0.10.0'} - - typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} - engines: {node: '>=14.17'} - hasBin: true - -snapshots: - - '@types/react@19.2.14': - dependencies: - csstype: 3.2.3 - - csstype@3.2.3: {} - - react@19.2.5: {} - - typescript@5.9.3: {} diff --git a/grpc/host_client.go b/grpc/host_client.go deleted file mode 100644 index 7e17dc0..0000000 --- a/grpc/host_client.go +++ /dev/null @@ -1,361 +0,0 @@ -package grpc - -import ( - "context" - "io" - "log/slog" - "time" - - sdk "github.com/DouDOU-start/airgate-sdk" - pb "github.com/DouDOU-start/airgate-sdk/proto" -) - -// hostClient 把 pb.HostServiceClient 包装成 sdk.Host 接口。 -// 插件代码看到的是 sdk.Host(plain Go),不直接接触 protobuf 类型, -// 这样未来 proto 演进时插件源码不需要跟着改。 -// -// 日志策略:core→plugin 的 conn 由 hashicorp/go-plugin 内部建立,我们没法装拦截器, -// 所以这里手写 RPC 进入 / 失败 / 完成日志。Error 强制打点;正常路径只 Debug。 -// 跨进程链路靠 LoggingUnaryClientInterceptor 写入 outgoing metadata 的 request_id 串起来。 -type hostClient struct { - c pb.HostServiceClient -} - -// NewHostClient 用一个 grpc client 构造 sdk.Host。 -// 一般由 grpcPluginContext.Host() lazy 调用,不建议插件直接构造。 -func NewHostClient(c pb.HostServiceClient) sdk.Host { - return &hostClient{c: c} -} - -// hostRPCLogger 派生 host 调用专用 logger,并返回起始时间。 -func hostRPCLogger(ctx context.Context, method string) (*slog.Logger, time.Time) { - return sdk.LoggerFromContext(ctx).With("host_rpc", method), time.Now() -} - -// ── 调度 ── - -func (h *hostClient) SelectAccount(ctx context.Context, req sdk.HostSelectAccountRequest) (*sdk.HostSelectAccountResult, error) { - logger, start := hostRPCLogger(ctx, "SelectAccount") - resp, err := h.c.SelectAccount(ctx, &pb.HostSelectAccountRequest{ - GroupId: req.GroupID, - Model: req.Model, - SessionId: req.SessionID, - ExcludeAccountIds: req.ExcludeAccountIDs, - }) - if err != nil { - logger.Error("host_call_select_account_failed", - sdk.LogFieldGroupID, req.GroupID, - sdk.LogFieldModel, req.Model, - sdk.LogFieldDurationMs, time.Since(start).Milliseconds(), - sdk.LogFieldError, err, - ) - return nil, err - } - logger.Debug("host_call_select_account_completed", - sdk.LogFieldDurationMs, time.Since(start).Milliseconds(), - ) - return &sdk.HostSelectAccountResult{ - AccountID: resp.AccountId, - AccountName: resp.AccountName, - Platform: resp.Platform, - }, nil -} - -func (h *hostClient) ReportAccountResult(ctx context.Context, accountID int64, success bool, errMsg string) error { - logger, start := hostRPCLogger(ctx, "ReportAccountResult") - _, err := h.c.ReportAccountResult(ctx, &pb.HostReportAccountResultRequest{ - AccountId: accountID, - Success: success, - ErrorMsg: errMsg, - }) - if err != nil { - logger.Error("host_call_report_account_result_failed", - sdk.LogFieldAccountID, accountID, - sdk.LogFieldDurationMs, time.Since(start).Milliseconds(), - sdk.LogFieldError, err, - ) - return err - } - return nil -} - -// ── 探测 ── - -func (h *hostClient) ProbeForward(ctx context.Context, req sdk.HostProbeForwardRequest) (*sdk.HostProbeForwardResult, error) { - logger, start := hostRPCLogger(ctx, "ProbeForward") - resp, err := h.c.ProbeForward(ctx, &pb.HostProbeForwardRequest{ - GroupId: req.GroupID, - Model: req.Model, - }) - if err != nil { - logger.Error("host_call_probe_forward_failed", - sdk.LogFieldGroupID, req.GroupID, - sdk.LogFieldModel, req.Model, - sdk.LogFieldDurationMs, time.Since(start).Milliseconds(), - sdk.LogFieldError, err, - ) - return nil, err - } - return &sdk.HostProbeForwardResult{ - Success: resp.Success, - AccountID: resp.AccountId, - Platform: resp.Platform, - Model: resp.Model, - StatusCode: resp.StatusCode, - LatencyMs: resp.LatencyMs, - ErrorKind: resp.ErrorKind, - ErrorMsg: resp.ErrorMsg, - }, nil -} - -// ── Forward 管线 ── - -func (h *hostClient) Forward(ctx context.Context, req sdk.HostForwardRequest) (*sdk.HostForwardResponse, error) { - logger, start := hostRPCLogger(ctx, "Forward") - resp, err := h.c.Forward(ctx, &pb.HostForwardRequest{ - UserId: req.UserID, - GroupId: req.GroupID, - Model: req.Model, - Method: req.Method, - Path: req.Path, - Headers: httpHeadersToProto(req.Headers), - Body: req.Body, - Stream: req.Stream, - }) - if err != nil { - logger.Error("host_call_forward_failed", - sdk.LogFieldUserID, req.UserID, - sdk.LogFieldGroupID, req.GroupID, - sdk.LogFieldModel, req.Model, - sdk.LogFieldDurationMs, time.Since(start).Milliseconds(), - sdk.LogFieldError, err, - ) - return nil, err - } - result := &sdk.HostForwardResponse{ - StatusCode: int(resp.StatusCode), - Headers: protoHeadersToHTTP(resp.Headers), - Body: resp.Body, - } - if resp.Usage != nil { - result.Usage = sdk.HostForwardUsage{ - InputTokens: resp.Usage.InputTokens, - OutputTokens: resp.Usage.OutputTokens, - Cost: resp.Usage.Cost, - Model: resp.Usage.Model, - } - } - return result, nil -} - -func (h *hostClient) ForwardStream(ctx context.Context, req sdk.HostForwardRequest, callback func(chunk sdk.HostForwardChunk) error) error { - logger, start := hostRPCLogger(ctx, "ForwardStream") - stream, err := h.c.ForwardStream(ctx, &pb.HostForwardRequest{ - UserId: req.UserID, - GroupId: req.GroupID, - Model: req.Model, - Method: req.Method, - Path: req.Path, - Headers: httpHeadersToProto(req.Headers), - Body: req.Body, - Stream: req.Stream, - }) - if err != nil { - logger.Error("host_call_forward_stream_open_failed", - sdk.LogFieldUserID, req.UserID, - sdk.LogFieldGroupID, req.GroupID, - sdk.LogFieldModel, req.Model, - sdk.LogFieldDurationMs, time.Since(start).Milliseconds(), - sdk.LogFieldError, err, - ) - return err - } - for { - chunk, err := stream.Recv() - if err == io.EOF { - return nil - } - if err != nil { - logger.Error("host_call_forward_stream_recv_failed", - sdk.LogFieldUserID, req.UserID, - sdk.LogFieldGroupID, req.GroupID, - sdk.LogFieldModel, req.Model, - sdk.LogFieldDurationMs, time.Since(start).Milliseconds(), - sdk.LogFieldError, err, - ) - return err - } - c := sdk.HostForwardChunk{ - Data: chunk.Data, - Done: chunk.Done, - StatusCode: int(chunk.StatusCode), - Headers: protoHeadersToHTTP(chunk.Headers), - } - if chunk.Usage != nil { - c.Usage = sdk.HostForwardUsage{ - InputTokens: chunk.Usage.InputTokens, - OutputTokens: chunk.Usage.OutputTokens, - Cost: chunk.Usage.Cost, - Model: chunk.Usage.Model, - } - } - if err := callback(c); err != nil { - logger.Error("host_call_forward_stream_callback_failed", - sdk.LogFieldUserID, req.UserID, - sdk.LogFieldGroupID, req.GroupID, - sdk.LogFieldModel, req.Model, - sdk.LogFieldDurationMs, time.Since(start).Milliseconds(), - sdk.LogFieldError, err, - ) - return err - } - } -} - -// ── 数据查询 ── - -func (h *hostClient) ListGroups(ctx context.Context) ([]sdk.HostGroup, error) { - logger, start := hostRPCLogger(ctx, "ListGroups") - resp, err := h.c.ListGroups(ctx, &pb.HostListGroupsRequest{}) - if err != nil { - logger.Error("host_call_list_groups_failed", - sdk.LogFieldDurationMs, time.Since(start).Milliseconds(), - sdk.LogFieldError, err, - ) - return nil, err - } - groups := make([]sdk.HostGroup, 0, len(resp.Groups)) - for _, g := range resp.Groups { - groups = append(groups, sdk.HostGroup{ - ID: g.Id, - Name: g.Name, - Platform: g.Platform, - IsExclusive: g.IsExclusive, - RateMultiplier: g.RateMultiplier, - }) - } - return groups, nil -} - -func (h *hostClient) ListPlatforms(ctx context.Context) ([]sdk.HostPlatform, error) { - logger, start := hostRPCLogger(ctx, "ListPlatforms") - resp, err := h.c.ListPlatforms(ctx, &pb.HostListPlatformsRequest{}) - if err != nil { - logger.Error("host_call_list_platforms_failed", - sdk.LogFieldDurationMs, time.Since(start).Milliseconds(), - sdk.LogFieldError, err, - ) - return nil, err - } - platforms := make([]sdk.HostPlatform, 0, len(resp.Platforms)) - for _, p := range resp.Platforms { - platforms = append(platforms, sdk.HostPlatform{ - Name: p.Name, - DisplayName: p.DisplayName, - }) - } - return platforms, nil -} - -func (h *hostClient) ListModels(ctx context.Context, platform string) ([]sdk.ModelInfo, error) { - logger, start := hostRPCLogger(ctx, "ListModels") - resp, err := h.c.ListModels(ctx, &pb.HostListModelsRequest{Platform: platform}) - if err != nil { - logger.Error("host_call_list_models_failed", - sdk.LogFieldPlatform, platform, - sdk.LogFieldDurationMs, time.Since(start).Milliseconds(), - sdk.LogFieldError, err, - ) - return nil, err - } - models := make([]sdk.ModelInfo, 0, len(resp.Models)) - for _, m := range resp.Models { - models = append(models, sdk.ModelInfo{ - ID: m.Id, - Name: m.Name, - InputPrice: m.InputPrice, - OutputPrice: m.OutputPrice, - CachedInputPrice: m.CachedInputPrice, - CacheCreationPrice: m.CacheCreationPrice, - CacheCreation1hPrice: m.CacheCreation_1HPrice, - ContextWindow: int(m.ContextWindow), - MaxOutputTokens: int(m.MaxOutputTokens), - InputPricePriority: m.InputPricePriority, - OutputPricePriority: m.OutputPricePriority, - }) - } - return models, nil -} - -func (h *hostClient) GetUserInfo(ctx context.Context, userID int64) (*sdk.HostUserInfo, error) { - logger, start := hostRPCLogger(ctx, "GetUserInfo") - resp, err := h.c.GetUserInfo(ctx, &pb.HostGetUserInfoRequest{UserId: userID}) - if err != nil { - logger.Error("host_call_get_user_info_failed", - sdk.LogFieldUserID, userID, - sdk.LogFieldDurationMs, time.Since(start).Milliseconds(), - sdk.LogFieldError, err, - ) - return nil, err - } - return &sdk.HostUserInfo{ - UserID: resp.UserId, - Username: resp.Username, - Email: resp.Email, - Role: resp.Role, - Balance: resp.Balance, - Status: resp.Status, - }, nil -} - -func (h *hostClient) StoreAsset(ctx context.Context, req sdk.HostStoreAssetRequest) (*sdk.HostStoredAsset, error) { - logger, start := hostRPCLogger(ctx, "StoreAsset") - resp, err := h.c.StoreAsset(ctx, &pb.HostStoreAssetRequest{ - UserId: req.UserID, - Scope: req.Scope, - ContentType: req.ContentType, - Data: req.Data, - FileExtension: req.FileExtension, - }) - if err != nil { - logger.Error("host_call_store_asset_failed", - sdk.LogFieldUserID, req.UserID, - sdk.LogFieldDurationMs, time.Since(start).Milliseconds(), - sdk.LogFieldError, err, - ) - return nil, err - } - return &sdk.HostStoredAsset{ - AssetID: resp.AssetId, - ObjectKey: resp.ObjectKey, - PublicURL: resp.PublicUrl, - SizeBytes: resp.SizeBytes, - ContentType: resp.ContentType, - }, nil -} - -func (h *hostClient) GetAssetURL(ctx context.Context, objectKey string) (string, error) { - logger, start := hostRPCLogger(ctx, "GetAssetURL") - resp, err := h.c.GetAssetURL(ctx, &pb.HostGetAssetURLRequest{ObjectKey: objectKey}) - if err != nil { - logger.Error("host_call_get_asset_url_failed", - sdk.LogFieldDurationMs, time.Since(start).Milliseconds(), - sdk.LogFieldError, err, - ) - return "", err - } - return resp.PublicUrl, nil -} - -func (h *hostClient) GetAssetBytes(ctx context.Context, objectKey string) (*sdk.HostAssetBytes, error) { - logger, start := hostRPCLogger(ctx, "GetAssetBytes") - resp, err := h.c.GetAssetBytes(ctx, &pb.HostGetAssetBytesRequest{ObjectKey: objectKey}) - if err != nil { - logger.Error("host_call_get_asset_bytes_failed", - sdk.LogFieldDurationMs, time.Since(start).Milliseconds(), - sdk.LogFieldError, err, - ) - return nil, err - } - return &sdk.HostAssetBytes{Data: resp.Data, ContentType: resp.ContentType}, nil -} diff --git a/host.go b/host.go deleted file mode 100644 index e8348ee..0000000 --- a/host.go +++ /dev/null @@ -1,199 +0,0 @@ -package sdk - -import ( - "context" - "net/http" -) - -// Host Core 暴露给插件的反向调用接口(plugin → core)。 -// -// 通过 hashicorp/go-plugin 的 GRPCBroker 架起子进程隧道,插件无需 admin HTTP / Bearer 鉴权。 -// -// 在插件 Init 里通过 HostAware 拿到: -// -// func (p *MyPlugin) Init(ctx sdk.PluginContext) error { -// if h, ok := ctx.(sdk.HostAware); ok { -// p.host = h.Host() // 可能为 nil -// } -// return nil -// } -type Host interface { - // ── 调度 ── - - // SelectAccount 走和真实用户请求完全相同的调度路径选出一个账号。 - SelectAccount(ctx context.Context, req HostSelectAccountRequest) (*HostSelectAccountResult, error) - - // ReportAccountResult 把账号调用结果反馈给 scheduler 状态机。 - ReportAccountResult(ctx context.Context, accountID int64, success bool, errMsg string) error - - // ── 探测 ── - - // ProbeForward 内部组装一次最小 chat completion 请求执行黑盒探测: - // 跳过 usage_log 与余额扣款,但仍 ReportResult 反哺账号状态机。 - ProbeForward(ctx context.Context, req HostProbeForwardRequest) (*HostProbeForwardResult, error) - - // ── Forward 管线(完整计费) ── - - // Forward 非流式业务转发:走完整管线(调度 → 网关 → 计费 → usage_log)。 - Forward(ctx context.Context, req HostForwardRequest) (*HostForwardResponse, error) - - // ForwardStream 流式业务转发:结果通过 callback 逐块回调。 - // callback 返回 error 时 Core 侧中断流。最后一块 Done=true 携带 Usage。 - ForwardStream(ctx context.Context, req HostForwardRequest, callback func(chunk HostForwardChunk) error) error - - // ── 数据查询 ── - - // ListGroups 列出 Core 当前所有分组。 - ListGroups(ctx context.Context) ([]HostGroup, error) - - // ListPlatforms 列出已加载的网关平台。 - ListPlatforms(ctx context.Context) ([]HostPlatform, error) - - // ListModels 列出指定平台的模型列表。 - ListModels(ctx context.Context, platform string) ([]ModelInfo, error) - - // GetUserInfo 获取用户基本信息。 - GetUserInfo(ctx context.Context, userID int64) (*HostUserInfo, error) - - // StoreAsset 由 Core 根据全局 storage 设置保存资产并返回可访问 URL。 - StoreAsset(ctx context.Context, req HostStoreAssetRequest) (*HostStoredAsset, error) - - // GetAssetURL 根据 object key 返回当前配置下的可访问 URL。 - GetAssetURL(ctx context.Context, objectKey string) (string, error) - - // GetAssetBytes 根据 object key 返回资产原始内容。 - GetAssetBytes(ctx context.Context, objectKey string) (*HostAssetBytes, error) -} - -// HostAware 可选接口:PluginContext 实现它就能暴露 Host。老 dev server / 测试 mock 可忽略。 -type HostAware interface { - // Host 返回 Host 客户端;可能为 nil(Core 版本不支持 / 未启用)。 - Host() Host -} - -// ── 调度 ── - -// HostSelectAccountRequest 调度选号入参。 -type HostSelectAccountRequest struct { - GroupID int64 - Model string - SessionID string - ExcludeAccountIDs []int64 -} - -// HostSelectAccountResult 调度选号结果。 -type HostSelectAccountResult struct { - AccountID int64 - AccountName string - Platform string -} - -// ── 探测 ── - -// HostProbeForwardRequest 黑盒探测入参。 -type HostProbeForwardRequest struct { - GroupID int64 - Model string -} - -// HostProbeForwardResult 黑盒探测结果。 -type HostProbeForwardResult struct { - Success bool - AccountID int64 - Platform string - Model string - StatusCode int64 - LatencyMs int64 - ErrorKind string - ErrorMsg string -} - -// ── Forward 管线 ── - -// HostForwardRequest 业务转发入参。 -type HostForwardRequest struct { - UserID int64 - GroupID int64 - Model string - Method string - Path string - Headers http.Header - Body []byte - Stream bool -} - -// HostForwardResponse 非流式转发结果。 -type HostForwardResponse struct { - StatusCode int - Headers http.Header - Body []byte - Usage HostForwardUsage -} - -// HostForwardChunk 流式转发的单块数据。 -type HostForwardChunk struct { - Data []byte - Done bool - StatusCode int - Headers http.Header - Usage HostForwardUsage -} - -// HostForwardUsage 转发的 token / 费用摘要。 -type HostForwardUsage struct { - InputTokens int64 - OutputTokens int64 - Cost float64 - Model string -} - -// ── 数据查询 ── - -// HostGroup Host.ListGroups 返回的分组摘要。 -type HostGroup struct { - ID int64 - Name string - Platform string - IsExclusive bool - RateMultiplier float64 -} - -// HostPlatform 已加载的网关平台。 -type HostPlatform struct { - Name string - DisplayName string -} - -// HostUserInfo 用户基本信息。 -type HostUserInfo struct { - UserID int64 - Username string - Email string - Role string - Balance float64 - Status string -} - -// HostStoreAssetRequest 资产存储入参。 -type HostStoreAssetRequest struct { - UserID int64 - Scope string - ContentType string - Data []byte - FileExtension string -} - -// HostStoredAsset 资产存储结果。 -type HostStoredAsset struct { - AssetID string - ObjectKey string - PublicURL string - SizeBytes int64 - ContentType string -} - -// HostAssetBytes 资产读取结果。 -type HostAssetBytes struct { - Data []byte - ContentType string -} diff --git a/models.go b/models.go deleted file mode 100644 index ad6155c..0000000 --- a/models.go +++ /dev/null @@ -1,119 +0,0 @@ -package sdk - -import "net/http" - -// Account 上游账户(Core 调度后传给插件的最小视图)。 -type Account struct { - ID int64 `json:"id"` - Name string `json:"name"` - Platform string `json:"platform"` - Type string `json:"type"` // 对应 AccountType.Key(apikey / oauth / ...) - Credentials map[string]string `json:"credentials"` // JSONB 透传,结构由 Type 决定 - ProxyURL string `json:"proxy_url"` -} - -// ModelInfo 插件声明的模型信息(Core 缓存用于调度/展示,不再做计费)。 -// -// 定价档位(对齐 OpenAI 官方): -// - 标准档:InputPrice / OutputPrice / CachedInputPrice -// - Priority 档:*Priority 字段;未配置时 CalculateCost 以标准价 × 2 兜底 -// - Fast 档:*Fast 字段;未配置时 CalculateCost 以标准价 × 2.5 兜底 -// - Flex / Batch 档:*Flex 字段;未配置时以标准价 × 0.5 兜底 -// - 长上下文档(仅 gpt-5.4 家族):完整 input_tokens 超过 LongContextThreshold -// 且非 priority 时整次请求按倍率计费 -type ModelInfo struct { - ID string `json:"id"` - Name string `json:"name"` - ContextWindow int `json:"context_window"` - MaxOutputTokens int `json:"max_output_tokens"` - InputPrice float64 `json:"input_price"` // $/1M token - OutputPrice float64 `json:"output_price"` // $/1M token - CachedInputPrice float64 `json:"cached_input_price"` // cache read,$/1M token - CacheCreationPrice float64 `json:"cache_creation_price"` // cache write 5m TTL(1.25x input) - CacheCreation1hPrice float64 `json:"cache_creation_1h_price"` // cache write 1h TTL(2.00x input) - - InputPricePriority float64 `json:"input_price_priority,omitempty"` - OutputPricePriority float64 `json:"output_price_priority,omitempty"` - CachedInputPricePriority float64 `json:"cached_input_price_priority,omitempty"` - - InputPriceFast float64 `json:"input_price_fast,omitempty"` - OutputPriceFast float64 `json:"output_price_fast,omitempty"` - CachedInputPriceFast float64 `json:"cached_input_price_fast,omitempty"` - - InputPriceFlex float64 `json:"input_price_flex,omitempty"` - OutputPriceFlex float64 `json:"output_price_flex,omitempty"` - CachedInputPriceFlex float64 `json:"cached_input_price_flex,omitempty"` - - // 长上下文阶梯(仅 gpt-5.4 家族启用;判定基于完整 input_tokens = 非缓存+缓存命中) - LongContextThreshold int `json:"long_context_threshold,omitempty"` - LongContextInputMultiplier float64 `json:"long_context_input_multiplier,omitempty"` - LongContextOutputMultiplier float64 `json:"long_context_output_multiplier,omitempty"` - LongContextCachedMultiplier float64 `json:"long_context_cached_multiplier,omitempty"` -} - -// RouteDefinition 网关插件声明的 API 端点。 -type RouteDefinition struct { - Method string `json:"method"` - Path string `json:"path"` - Description string `json:"description"` -} - -// RouteRegistrar 扩展插件使用的路由注册器。 -type RouteRegistrar interface { - Handle(method, path string, handler http.HandlerFunc) - Group(prefix string) RouteRegistrar -} - -// CredentialField 凭证字段声明。 -type CredentialField struct { - Key string `json:"key"` - Label string `json:"label"` - Type string `json:"type"` // text / password / textarea / select - Required bool `json:"required"` - Placeholder string `json:"placeholder"` - EditDisabled bool `json:"edit_disabled,omitempty"` -} - -// AccountType 账号类型声明。 -type AccountType struct { - Key string `json:"key"` - Label string `json:"label"` - Description string `json:"description"` - Fields []CredentialField `json:"fields"` -} - -// FrontendPage 前端独立页面声明。 -type FrontendPage struct { - Path string `json:"path"` - Title string `json:"title"` - Icon string `json:"icon"` - Description string `json:"description"` - // Audience 决定页面可见范围: - // "admin" / "" 仅管理员(默认) - // "user" 仅普通登录用户 - // "all" 所有登录用户 - Audience string `json:"audience,omitempty"` -} - -// 前端组件插槽。 -const ( - SlotAccountForm = "account-form" - SlotAccountDetail = "account-detail" -) - -// FrontendWidget 前端组件嵌入声明。 -type FrontendWidget struct { - Slot string `json:"slot"` - EntryFile string `json:"entry_file"` - Title string `json:"title"` -} - -// QuotaInfo 账号额度信息。 -type QuotaInfo struct { - Total float64 `json:"total"` - Used float64 `json:"used"` - Remaining float64 `json:"remaining"` - Currency string `json:"currency"` // 如 "USD" - ExpiresAt string `json:"expires_at"` // ISO 8601 - Extra map[string]string `json:"extra"` -} diff --git a/proto/plugin.proto b/proto/plugin.proto deleted file mode 100644 index 2bbe4f7..0000000 --- a/proto/plugin.proto +++ /dev/null @@ -1,688 +0,0 @@ -syntax = "proto3"; - -package airgate.plugin.v1; - -option go_package = "github.com/DouDOU-start/airgate-sdk/proto"; - -// ==================== 插件基础服务 ==================== - -service PluginService { - rpc GetInfo(Empty) returns (PluginInfoResponse); - rpc Init(InitRequest) returns (Empty); - rpc Start(Empty) returns (Empty); - rpc Stop(Empty) returns (Empty); - // 获取插件的前端静态资源 - rpc GetWebAssets(Empty) returns (WebAssetsResponse); - // 健康检查(可选,插件未实现时返回成功) - rpc HealthCheck(Empty) returns (Empty); - // 通用请求代理:Core 透传,插件自行路由(可选,插件未实现时返回 501) - rpc HandleRequest(HttpRequest) returns (HttpResponse); -} - -// ==================== 网关服务 ==================== - -service GatewayService { - rpc GetPlatform(Empty) returns (StringResponse); - rpc GetModels(Empty) returns (ModelsResponse); - rpc GetRoutes(Empty) returns (RoutesResponse); - rpc Forward(ForwardRequest) returns (ForwardOutcome); - rpc ForwardStream(ForwardRequest) returns (stream ForwardChunk); - rpc ValidateAccount(CredentialsRequest) returns (Empty); - rpc QueryQuota(CredentialsRequest) returns (QuotaInfoResponse); - // WebSocket 双向流 - rpc HandleWebSocket(stream WebSocketFrame) returns (stream WebSocketFrame); -} - -// ==================== 扩展服务 ==================== - -service ExtensionService { - rpc Migrate(Empty) returns (Empty); - rpc GetBackgroundTasks(Empty) returns (BackgroundTasksResponse); - // 由 Core 调度器按 Interval 周期触发;插件进程内查表执行 Handler - rpc RunBackgroundTask(RunBackgroundTaskRequest) returns (Empty); - // HTTP 请求由核心代理到插件 - rpc HandleRequest(HttpRequest) returns (HttpResponse); - rpc HandleStreamRequest(HttpRequest) returns (stream HttpResponseChunk); -} - -// ==================== 中间件服务(Core → Plugin,forward 路径拦截) ==================== -// -// MiddlewareService 让"请求中间层"这种旁路插件能拿到每次 forward 的前后事件: -// - OnForwardBegin — 选完账号 / 还没调 upstream 之前,能改 headers / 拒绝请求 -// - OnForwardEnd — upstream 返回之后 / 写 usage_log 之前,拿到完整 metadata -// -// 设计原则(详见 ADR-0001 Decision 2/3): -// 1. middleware 挂了不能 block 生产:返回 error 只 log warn,流程继续; -// 只有 OnForwardBegin 明确返回 Action=DENY 才会拒绝请求 -// 2. 多个 middleware 按 priority 排序。Begin 升序、End 降序(LIFO) -// 3. payload 两段式:默认只传元数据,声明 middleware.read_body capability 的插件 -// 才会收到 request_body / response_body -// 4. 流式响应的 response_body 只给摘要(首次非空 chunk 拼装),完整流式内容留给 -// 未来的 OnStreamChunk(ADR-0002) -// -// 新角色:middleware 插件不是 gateway(不替代 upstream),也不是 extension -//(不跑后台任务 + 自定义 HTTP)。它在 PluginInfo.type = "middleware" 中声明。 -service MiddlewareService { - rpc OnForwardBegin(MiddlewareRequest) returns (MiddlewareDecision); - rpc OnForwardEnd(MiddlewareEvent) returns (Empty); -} - -// ==================== Host 服务(反向调用:插件 → Core) ==================== -// -// 由 Core 实现,通过 hashicorp/go-plugin 的 GRPCBroker 暴露给插件子进程。 -// 替代旧的 admin HTTP API + admin_api_key 模式:插件不再需要 HTTP client、 -// 不再需要 Bearer 鉴权——broker 的子进程隧道天然互信。 -// -// 设计原则: -// 1. 覆盖插件常见需求的通用原语层——新增插件应只组合已有 RPC,无需扩 proto; -// 2. 副作用类 RPC(Forward / ProbeForward / ReportAccountResult)明确语义边界; -// 3. ProbeForward 与 Forward 分开,便于权限控制 + 计费跳过 + 日志区分; -// 4. 每个 RPC 由 Capability 门控,Core interceptor 做准入校验。 -// -// 能力分类: -// ── 调度 ── -// - SelectAccount — 调度选号(同用户路径) -// - ReportAccountResult — 反馈账号调用结果到状态机 -// ── 探测 ── -// - ProbeForward — 黑盒探测请求:走完整调度但跳过 usage_log / 余额扣款 -// ── Forward 管线(完整计费) ── -// - Forward — 非流式业务转发(调度 → 网关 → 计费 → 记录) -// - ForwardStream — 流式业务转发(SSE / chunked) -// ── 数据查询 ── -// - ListGroups — 所有分组 -// - ListPlatforms — 已加载的网关平台 -// - ListModels — 指定平台的模型列表 -// - GetUserInfo — 用户基本信息 + 余额 - -service HostService { - // ── 调度 ── - - // 选号:根据 (group_id, model) 走和真实用户请求完全相同的调度路径。 - rpc SelectAccount(HostSelectAccountRequest) returns (HostSelectAccountResponse); - - // 把账号调用结果反馈给 scheduler 的失败计数器/状态机。 - rpc ReportAccountResult(HostReportAccountResultRequest) returns (Empty); - - // ── 探测 ── - - // 黑盒探测:内部组装一次最小的 chat completion 请求并直接执行。 - // 跳过 usage_log 写入、跳过用户余额扣款,仍然 ReportResult 反哺账号状态机。 - rpc ProbeForward(HostProbeForwardRequest) returns (HostProbeForwardResponse); - - // ── Forward 管线(完整计费) ── - - // 非流式业务转发:走完整管线(调度 → 网关插件 → 计费 → usage_log)。 - // 调用方需提供 user_id + group_id 以确定计费主体和调度路径。 - rpc Forward(HostForwardRequest) returns (HostForwardResponse); - - // 流式业务转发:与 Forward 相同管线,结果通过 server stream 逐块返回。 - // 最后一块 done=true 携带 usage 信息(计费已在 Core 侧完成)。 - rpc ForwardStream(HostForwardRequest) returns (stream HostForwardChunk); - - // ── 数据查询 ── - - // 列出所有分组。 - rpc ListGroups(HostListGroupsRequest) returns (HostListGroupsResponse); - - // 列出已加载的网关平台(每个 gateway 插件对应一个 platform)。 - rpc ListPlatforms(HostListPlatformsRequest) returns (HostListPlatformsResponse); - - // 列出指定平台的模型列表。 - rpc ListModels(HostListModelsRequest) returns (HostListModelsResponse); - - // 获取用户基本信息(余额、角色、状态)。 - rpc GetUserInfo(HostGetUserInfoRequest) returns (HostGetUserInfoResponse); - - // 资产存储:由 Core 根据全局 storage 设置选择 MinIO/S3 或本地磁盘。 - rpc StoreAsset(HostStoreAssetRequest) returns (HostStoreAssetResponse); - rpc GetAssetURL(HostGetAssetURLRequest) returns (HostGetAssetURLResponse); - rpc GetAssetBytes(HostGetAssetBytesRequest) returns (HostGetAssetBytesResponse); -} - -message HostSelectAccountRequest { - int64 group_id = 1; - string model = 2; // 可空:空时 Core 用 platform 的第一个 model - string session_id = 3; // 可空 - repeated int64 exclude_account_ids = 4; -} - -message HostSelectAccountResponse { - int64 account_id = 1; - string account_name = 2; - string platform = 3; -} - -message HostProbeForwardRequest { - int64 group_id = 1; - string model = 2; // 可空:自动取 platform 第一个 model -} - -message HostProbeForwardResponse { - bool success = 1; - int64 account_id = 2; // 本次实际命中的账号 ID(用于运维诊断) - string platform = 3; - string model = 4; // 实际探测用的 model - int64 status_code = 5; - int64 latency_ms = 6; - string error_kind = 7; // "" / "no_account" / "scheduler" / "upstream_5xx" / "timeout" / ... - string error_msg = 8; -} - -message HostListGroupsRequest {} - -message HostGroup { - int64 id = 1; - string name = 2; - string platform = 3; - bool is_exclusive = 4; - double rate_multiplier = 5; -} - -message HostListGroupsResponse { - repeated HostGroup groups = 1; -} - -message HostReportAccountResultRequest { - int64 account_id = 1; - bool success = 2; - string error_msg = 3; // 失败时上报,便于调试 -} - -// ── Host Forward 消息 ── - -// HostForwardRequest 业务转发入参。 -// user_id + group_id 共同确定计费主体和调度路径。 -message HostForwardRequest { - int64 user_id = 1; // 计费主体(扣余额的用户) - int64 group_id = 2; // 调度分组 - string model = 3; // 可空:空时 Core 取该 platform 的第一个 model - string method = 4; // HTTP method(POST / GET / ...) - string path = 5; // 请求路径(如 /v1/chat/completions) - map headers = 6; - bytes body = 7; - bool stream = 8; // 是否流式 -} - -// HostForwardResponse 非流式转发结果。 -message HostForwardResponse { - int32 status_code = 1; - map headers = 2; - bytes body = 3; - HostForwardUsage usage = 4; -} - -// HostForwardChunk 流式转发的单块数据。 -// 第一块携带 status_code + headers;最后一块 done=true 携带 usage。 -message HostForwardChunk { - bytes data = 1; - bool done = 2; - int32 status_code = 3; // 仅首块 - map headers = 4; // 仅首块 - HostForwardUsage usage = 5; // 仅末块 -} - -// HostForwardUsage 转发的 token / 费用摘要(Core 侧计算后回传给调用方插件)。 -message HostForwardUsage { - int64 input_tokens = 1; - int64 output_tokens = 2; - double cost = 3; // 总费用(已计入倍率) - string model = 4; // 实际使用的 model(可能被 Core 改写) -} - -// ── Host 数据查询消息 ── - -message HostListPlatformsRequest {} - -message HostPlatform { - string name = 1; // 平台标识(如 "openai") - string display_name = 2; // 展示名称(如 "OpenAI 网关") -} - -message HostListPlatformsResponse { - repeated HostPlatform platforms = 1; -} - -message HostListModelsRequest { - string platform = 1; // 必填:平台标识 -} - -message HostListModelsResponse { - repeated ModelInfoProto models = 1; // 复用已有 ModelInfoProto,避免平行数据结构漂移 -} - -message HostGetUserInfoRequest { - int64 user_id = 1; -} - -message HostGetUserInfoResponse { - int64 user_id = 1; - string username = 2; - string email = 3; - string role = 4; // "admin" / "user" - double balance = 5; - string status = 6; // "active" / "disabled" -} - -message HostStoreAssetRequest { - int64 user_id = 1; - string scope = 2; - string content_type = 3; - bytes data = 4; - string file_extension = 5; -} - -message HostStoreAssetResponse { - string asset_id = 1; - string object_key = 2; - string public_url = 3; - int64 size_bytes = 4; - string content_type = 5; -} - -message HostGetAssetURLRequest { - string object_key = 1; -} - -message HostGetAssetURLResponse { - string public_url = 1; -} - -message HostGetAssetBytesRequest { - string object_key = 1; -} - -message HostGetAssetBytesResponse { - bytes data = 1; - string content_type = 2; -} - -// ==================== 通用消息 ==================== - -message Empty {} - -message StringResponse { - string value = 1; -} - -// HeaderValues 支持同一 Header 有多个值(如 Set-Cookie) -message HeaderValues { - repeated string values = 1; -} - -// ==================== 插件信息 ==================== - -message PluginInfoResponse { - string id = 1; - string name = 2; - string version = 3; - string description = 4; - string author = 5; - string type = 6; - repeated AccountTypeProto account_types = 7; - repeated FrontendPageProto frontend_pages = 8; - repeated FrontendWidgetProto frontend_widgets = 9; - string sdk_version = 10; // 插件编译时的 SDK 版本 - repeated string dependencies = 11; // 依赖的其他插件 ID - repeated ConfigFieldProto config_schema = 12; // 配置项声明 - repeated string instruction_presets = 13; // 可用的 instructions 预设名称列表 - // capabilities 声明的 HostService / Middleware 能力。Core 启动时按 - // "插件类型 → 允许集合" 做交集,gRPC interceptor 按此 set 做准入校验。 - // SDK 版本 <= 0.2.x 的插件豁免(兼容存量),0.3.x 起强制。详见 ADR-0001 Decision 4。 - repeated string capabilities = 14; - // priority 中间件链中的排序权重(数值越小越早进 Begin、越晚出 End)。 - // 仅 type="middleware" 插件使用;其他类型忽略。默认 100。 - int32 priority = 15; -} - -message ConfigFieldProto { - string key = 1; - string label = 2; - string type = 3; - bool required = 4; - string default_value = 5; - string description = 6; - string placeholder = 7; -} - -message AccountTypeProto { - string key = 1; - string label = 2; - string description = 3; - repeated CredentialFieldProto fields = 4; -} - -message CredentialFieldProto { - string key = 1; - string label = 2; - string type = 3; - bool required = 4; - string placeholder = 5; - bool edit_disabled = 6; // 编辑模式下隐藏该字段 -} - -message FrontendPageProto { - string path = 1; - string title = 2; - string icon = 3; - string description = 4; - string audience = 5; // "admin" | "user" | "all",空 = "admin" -} - -message FrontendWidgetProto { - string slot = 1; - string entry_file = 2; - string title = 3; -} - -message InitRequest { - map config = 1; - string log_level = 2; - // host_broker_id 是 Core 通过 hashicorp/go-plugin GRPCBroker 启动的 - // HostService stream 的 ID。插件 Init 时拿到 ID 后,可以通过 broker.Dial(id) - // 拿到 HostService 的 grpc client,回调 Core 提供的能力(SelectAccount / - // ProbeForward / ListGroups 等)。0 表示 Core 没启用 HostService。 - uint32 host_broker_id = 3; -} - -// ==================== 网关消息 ==================== - -message ModelInfoProto { - string id = 1; - string name = 2; - reserved 3; // 原 max_tokens,拆分为 context_window + max_output_tokens - double input_price = 4; - double output_price = 5; - double cached_input_price = 6; - double input_price_priority = 7; - double output_price_priority = 8; - double cached_input_price_priority = 9; - int64 context_window = 10; - int64 max_output_tokens = 11; - double cache_creation_price = 12; // 缓存写入 5m TTL 单价($/1M token,1.25x input) - double cache_creation_1h_price = 13; // 缓存写入 1h TTL 单价($/1M token,2.00x input) -} - -message ModelsResponse { - repeated ModelInfoProto models = 1; -} - -message RouteDefinitionProto { - string method = 1; - string path = 2; - string description = 3; -} - -message RoutesResponse { - repeated RouteDefinitionProto routes = 1; -} - -message AccountProto { - int64 id = 1; - string name = 2; - string platform = 3; - string type = 4; - bytes credentials_json = 5; - string proxy_url = 6; -} - -message ForwardRequest { - reserved 1, 2, 3, 4, 5, 6; // 原散落的 account 字段,已合并为 AccountProto - bytes body = 7; - map headers = 8; - string model = 9; - bool stream = 10; - AccountProto account = 11; -} - -// OutcomeKind 插件对一次 Forward 的判决。 -// 字段值与 sdk.OutcomeKind 常量一一对应;0 = UNKNOWN(插件未声明)。 -enum OutcomeKind { - OUTCOME_UNKNOWN = 0; - OUTCOME_SUCCESS = 1; - OUTCOME_CLIENT_ERROR = 2; - OUTCOME_ACCOUNT_RATE_LIMITED = 3; - OUTCOME_ACCOUNT_DEAD = 4; - OUTCOME_UPSTREAM_TRANSIENT = 5; - OUTCOME_STREAM_ABORTED = 6; -} - -// UpstreamResponse 上游返回的原始 HTTP 快照。 -message UpstreamResponse { - int32 status_code = 1; - map headers = 2; - bytes body = 3; -} - -// Usage 单次调用的 token / 费用统计。非 Success 判决下应为空。 -message Usage { - int64 input_tokens = 1; - int64 output_tokens = 2; - int64 cached_input_tokens = 3; - int64 cache_creation_tokens = 4; - int64 cache_creation_5m_tokens = 5; - int64 cache_creation_1h_tokens = 6; - int64 reasoning_output_tokens = 7; - - double input_cost = 10; - double output_cost = 11; - double cached_input_cost = 12; - double cache_creation_cost = 13; - - double input_price = 20; - double output_price = 21; - double cached_input_price = 22; - double cache_creation_price = 23; - double cache_creation_1h_price = 24; - - string model = 30; - string service_tier = 31; - int64 first_token_ms = 32; - - // image_size 是图像生成请求实际出图的尺寸("WxH",例如 "1024x1024"、"3840x2160")。 - // 网关侧按 1K/2K/4K 三档计费,把分档来源(实际尺寸)记下来,admin 后台 usage_log - // 显示费用时旁边带上 size,用户能直观看出"为什么这次扣了 0.40"。 - // 非图像请求留空。 - string image_size = 33; -} - -// ForwardOutcome 插件对一次 Forward 的完整判决结果。 -message ForwardOutcome { - OutcomeKind kind = 1; - UpstreamResponse upstream = 2; - Usage usage = 3; - int64 duration_ms = 4; - int64 retry_after_ms = 5; - string reason = 6; - map updated_credentials = 7; -} - -message ForwardChunk { - bytes data = 1; - bool done = 2; - ForwardOutcome final_outcome = 3; - int32 status_code = 4; - map headers = 5; -} - -message CredentialsRequest { - map credentials = 1; -} - -message QuotaInfoResponse { - double total = 1; - double used = 2; - double remaining = 3; - string currency = 4; - string expires_at = 5; - map extra = 6; -} - -// ==================== HTTP 代理消息 ==================== - -message HttpRequest { - string method = 1; - string path = 2; - string query = 3; - map headers = 4; - bytes body = 5; - string remote_addr = 6; -} - -message HttpResponse { - int32 status_code = 1; - map headers = 2; - bytes body = 3; -} - -message HttpResponseChunk { - bytes data = 1; - bool done = 2; - int32 status_code = 3; - map headers = 4; -} - -// ==================== 扩展消息 ==================== - -message BackgroundTaskProto { - string name = 1; - int64 interval_ms = 2; -} - -message BackgroundTasksResponse { - repeated BackgroundTaskProto tasks = 1; -} - -message RunBackgroundTaskRequest { - string name = 1; -} - -// ==================== WebSocket 消息 ==================== - -message WebSocketFrame { - enum FrameType { - CONNECT = 0; // 连接建立,携带元信息(仅第一帧) - TEXT = 1; // 文本消息 - BINARY = 2; // 二进制消息 - CLOSE = 3; // 关闭连接 - RESULT = 4; // 连接结束,携带 ForwardResult - } - - FrameType type = 1; - bytes data = 2; - - // 仅 CONNECT 帧使用 - WebSocketConnectInfo connect_info = 3; - - // 仅 CLOSE 帧使用 - int32 close_code = 4; - string close_reason = 5; - - // 仅 RESULT 帧使用 - ForwardOutcome outcome = 6; -} - -message WebSocketConnectInfo { - string path = 1; - string query = 2; - map headers = 3; - string remote_addr = 4; - string connection_id = 5; - reserved 6, 7, 8, 9, 10, 11; // 原散落的 account 字段,已合并为 AccountProto - AccountProto account = 12; -} - -// ==================== 前端资源 ==================== - -message WebAssetFile { - string path = 1; - bytes content = 2; -} - -message WebAssetsResponse { - repeated WebAssetFile files = 1; - bool has_assets = 2; -} - -// ==================== 中间件消息 ==================== -// -// MiddlewareRequest / MiddlewareEvent / MiddlewareDecision 用于 MiddlewareService。 -// 详细字段语义见 ADR-0001 Decision 3。 - -// MiddlewareRequest OnForwardBegin 的输入:请求元数据 + 可选的 request body。 -message MiddlewareRequest { - // === 核心元数据(默认总是填充)=== - string request_id = 1; // core 为本次请求分配的唯一 ID(便于跨 Begin/End 关联) - int64 user_id = 2; - int64 group_id = 3; - int64 account_id = 4; // 已由 scheduler 选出 - string platform = 5; - string model = 6; - bool stream = 7; - int64 input_tokens_est = 8; // core 侧粗略估算,仅用于早期决策 - - // metadata KV bag:供多个 middleware 之间传递上下文(Open Question Q-open-3)。 - // 命名空间规则暂不强制,未来可能收紧。 - map metadata = 9; - - // === 按需字段(声明了 middleware.read_body capability 的插件才会收到)=== - bytes request_body = 100; - map request_headers = 101; -} - -// MiddlewareEvent OnForwardEnd 的输入:完整的请求 + 响应元数据。 -message MiddlewareEvent { - // === 核心元数据(与 MiddlewareRequest 对齐)=== - string request_id = 1; - int64 user_id = 2; - int64 group_id = 3; - int64 account_id = 4; - string platform = 5; - string model = 6; - bool stream = 7; - int64 input_tokens_est = 8; // 与 MiddlewareRequest 字段对齐:core 侧粗略估算, - // 便于 middleware 做 estimate vs actual 比对。 - - // === 响应结果 === - int64 status_code = 20; - int64 duration_ms = 21; - int64 input_tokens = 22; - int64 output_tokens = 23; - int64 cached_input_tokens = 24; - int64 first_token_ms = 25; - string error_kind = 26; // "" / "upstream_5xx" / "timeout" / "no_account" / ... - string error_msg = 27; // 限长 512,见 core 实现 - - // 费用快照(core 已计算好) - double input_cost = 30; - double output_cost = 31; - double cached_input_cost = 32; - - // metadata 延续自 OnForwardBegin 的 bag - map metadata = 40; - - // === 按需字段(声明了 middleware.read_body capability 的插件才会收到)=== - // 流式响应时 response_body 只给摘要(首次非空 chunk 拼装),完整流式内容 - // 留给未来的 OnStreamChunk(ADR-0002)。 - bytes response_body = 100; - map response_headers = 101; -} - -// MiddlewareDecision OnForwardBegin 的输出:放行 / 拒绝 / 改请求。 -message MiddlewareDecision { - enum Action { - ALLOW = 0; // 默认放行 - DENY = 1; // 拒绝请求,status_code + error_message 会直接返回给用户 - MUTATE = 2; // 放行但修改请求(当前只支持 set_headers) - } - Action action = 1; - - // action=DENY 时的错误码 / 文案(对用户可见) - int32 deny_status_code = 10; // 默认 403 if Action=DENY and 未指定 - string deny_message = 11; - - // action=MUTATE 时要追加/覆盖的请求头 - map set_headers = 20; - - // 贯穿式 metadata:无论 allow/deny/mutate,都能往 bag 里写东西供后续 middleware / End 使用 - map metadata = 30; -} diff --git a/protocol/proto/doc.go b/protocol/proto/doc.go new file mode 100644 index 0000000..ed21cdd --- /dev/null +++ b/protocol/proto/doc.go @@ -0,0 +1,4 @@ +// Package proto 是 AirGate 插件协议的 protobuf 生成代码。 +// +// 普通插件业务代码应优先使用 sdkgo 包,不应直接依赖本包。 +package proto diff --git a/proto/plugin.pb.go b/protocol/proto/plugin.pb.go similarity index 56% rename from proto/plugin.pb.go rename to protocol/proto/plugin.pb.go index ee56c6b..52c4782 100644 --- a/proto/plugin.pb.go +++ b/protocol/proto/plugin.pb.go @@ -26,13 +26,14 @@ const ( type OutcomeKind int32 const ( - OutcomeKind_OUTCOME_UNKNOWN OutcomeKind = 0 - OutcomeKind_OUTCOME_SUCCESS OutcomeKind = 1 - OutcomeKind_OUTCOME_CLIENT_ERROR OutcomeKind = 2 - OutcomeKind_OUTCOME_ACCOUNT_RATE_LIMITED OutcomeKind = 3 - OutcomeKind_OUTCOME_ACCOUNT_DEAD OutcomeKind = 4 - OutcomeKind_OUTCOME_UPSTREAM_TRANSIENT OutcomeKind = 5 - OutcomeKind_OUTCOME_STREAM_ABORTED OutcomeKind = 6 + OutcomeKind_OUTCOME_UNKNOWN OutcomeKind = 0 + OutcomeKind_OUTCOME_SUCCESS OutcomeKind = 1 + OutcomeKind_OUTCOME_CLIENT_ERROR OutcomeKind = 2 + OutcomeKind_OUTCOME_ACCOUNT_RATE_LIMITED OutcomeKind = 3 + OutcomeKind_OUTCOME_ACCOUNT_DEAD OutcomeKind = 4 + OutcomeKind_OUTCOME_UPSTREAM_TRANSIENT OutcomeKind = 5 + OutcomeKind_OUTCOME_STREAM_ABORTED OutcomeKind = 6 + OutcomeKind_OUTCOME_ACCOUNT_MODEL_UNSUPPORTED OutcomeKind = 7 ) // Enum value maps for OutcomeKind. @@ -45,15 +46,17 @@ var ( 4: "OUTCOME_ACCOUNT_DEAD", 5: "OUTCOME_UPSTREAM_TRANSIENT", 6: "OUTCOME_STREAM_ABORTED", + 7: "OUTCOME_ACCOUNT_MODEL_UNSUPPORTED", } OutcomeKind_value = map[string]int32{ - "OUTCOME_UNKNOWN": 0, - "OUTCOME_SUCCESS": 1, - "OUTCOME_CLIENT_ERROR": 2, - "OUTCOME_ACCOUNT_RATE_LIMITED": 3, - "OUTCOME_ACCOUNT_DEAD": 4, - "OUTCOME_UPSTREAM_TRANSIENT": 5, - "OUTCOME_STREAM_ABORTED": 6, + "OUTCOME_UNKNOWN": 0, + "OUTCOME_SUCCESS": 1, + "OUTCOME_CLIENT_ERROR": 2, + "OUTCOME_ACCOUNT_RATE_LIMITED": 3, + "OUTCOME_ACCOUNT_DEAD": 4, + "OUTCOME_UPSTREAM_TRANSIENT": 5, + "OUTCOME_STREAM_ABORTED": 6, + "OUTCOME_ACCOUNT_MODEL_UNSUPPORTED": 7, } ) @@ -91,7 +94,7 @@ const ( WebSocketFrame_TEXT WebSocketFrame_FrameType = 1 // 文本消息 WebSocketFrame_BINARY WebSocketFrame_FrameType = 2 // 二进制消息 WebSocketFrame_CLOSE WebSocketFrame_FrameType = 3 // 关闭连接 - WebSocketFrame_RESULT WebSocketFrame_FrameType = 4 // 连接结束,携带 ForwardResult + WebSocketFrame_RESULT WebSocketFrame_FrameType = 4 // 连接结束,携带 ForwardOutcome ) // Enum value maps for WebSocketFrame_FrameType. @@ -136,7 +139,7 @@ func (x WebSocketFrame_FrameType) Number() protoreflect.EnumNumber { // Deprecated: Use WebSocketFrame_FrameType.Descriptor instead. func (WebSocketFrame_FrameType) EnumDescriptor() ([]byte, []int) { - return file_plugin_proto_rawDescGZIP(), []int{53, 0} + return file_plugin_proto_rawDescGZIP(), []int{30, 0} } type MiddlewareDecision_Action int32 @@ -185,33 +188,29 @@ func (x MiddlewareDecision_Action) Number() protoreflect.EnumNumber { // Deprecated: Use MiddlewareDecision_Action.Descriptor instead. func (MiddlewareDecision_Action) EnumDescriptor() ([]byte, []int) { - return file_plugin_proto_rawDescGZIP(), []int{59, 0} + return file_plugin_proto_rawDescGZIP(), []int{42, 0} } -type HostSelectAccountRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - GroupId int64 `protobuf:"varint,1,opt,name=group_id,json=groupId,proto3" json:"group_id,omitempty"` - Model string `protobuf:"bytes,2,opt,name=model,proto3" json:"model,omitempty"` // 可空:空时 Core 用 platform 的第一个 model - SessionId string `protobuf:"bytes,3,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` // 可空 - ExcludeAccountIds []int64 `protobuf:"varint,4,rep,packed,name=exclude_account_ids,json=excludeAccountIds,proto3" json:"exclude_account_ids,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache +type Empty struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *HostSelectAccountRequest) Reset() { - *x = HostSelectAccountRequest{} +func (x *Empty) Reset() { + *x = Empty{} mi := &file_plugin_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *HostSelectAccountRequest) String() string { +func (x *Empty) String() string { return protoimpl.X.MessageStringOf(x) } -func (*HostSelectAccountRequest) ProtoMessage() {} +func (*Empty) ProtoMessage() {} -func (x *HostSelectAccountRequest) ProtoReflect() protoreflect.Message { +func (x *Empty) ProtoReflect() protoreflect.Message { mi := &file_plugin_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -223,62 +222,32 @@ func (x *HostSelectAccountRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use HostSelectAccountRequest.ProtoReflect.Descriptor instead. -func (*HostSelectAccountRequest) Descriptor() ([]byte, []int) { +// Deprecated: Use Empty.ProtoReflect.Descriptor instead. +func (*Empty) Descriptor() ([]byte, []int) { return file_plugin_proto_rawDescGZIP(), []int{0} } -func (x *HostSelectAccountRequest) GetGroupId() int64 { - if x != nil { - return x.GroupId - } - return 0 -} - -func (x *HostSelectAccountRequest) GetModel() string { - if x != nil { - return x.Model - } - return "" -} - -func (x *HostSelectAccountRequest) GetSessionId() string { - if x != nil { - return x.SessionId - } - return "" -} - -func (x *HostSelectAccountRequest) GetExcludeAccountIds() []int64 { - if x != nil { - return x.ExcludeAccountIds - } - return nil -} - -type HostSelectAccountResponse struct { +type StringResponse struct { state protoimpl.MessageState `protogen:"open.v1"` - AccountId int64 `protobuf:"varint,1,opt,name=account_id,json=accountId,proto3" json:"account_id,omitempty"` - AccountName string `protobuf:"bytes,2,opt,name=account_name,json=accountName,proto3" json:"account_name,omitempty"` - Platform string `protobuf:"bytes,3,opt,name=platform,proto3" json:"platform,omitempty"` + Value string `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *HostSelectAccountResponse) Reset() { - *x = HostSelectAccountResponse{} +func (x *StringResponse) Reset() { + *x = StringResponse{} mi := &file_plugin_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *HostSelectAccountResponse) String() string { +func (x *StringResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*HostSelectAccountResponse) ProtoMessage() {} +func (*StringResponse) ProtoMessage() {} -func (x *HostSelectAccountResponse) ProtoReflect() protoreflect.Message { +func (x *StringResponse) ProtoReflect() protoreflect.Message { mi := &file_plugin_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -290,54 +259,40 @@ func (x *HostSelectAccountResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use HostSelectAccountResponse.ProtoReflect.Descriptor instead. -func (*HostSelectAccountResponse) Descriptor() ([]byte, []int) { +// Deprecated: Use StringResponse.ProtoReflect.Descriptor instead. +func (*StringResponse) Descriptor() ([]byte, []int) { return file_plugin_proto_rawDescGZIP(), []int{1} } -func (x *HostSelectAccountResponse) GetAccountId() int64 { - if x != nil { - return x.AccountId - } - return 0 -} - -func (x *HostSelectAccountResponse) GetAccountName() string { - if x != nil { - return x.AccountName - } - return "" -} - -func (x *HostSelectAccountResponse) GetPlatform() string { +func (x *StringResponse) GetValue() string { if x != nil { - return x.Platform + return x.Value } return "" } -type HostProbeForwardRequest struct { +// HeaderValues 支持同一 Header 有多个值(如 Set-Cookie) +type HeaderValues struct { state protoimpl.MessageState `protogen:"open.v1"` - GroupId int64 `protobuf:"varint,1,opt,name=group_id,json=groupId,proto3" json:"group_id,omitempty"` - Model string `protobuf:"bytes,2,opt,name=model,proto3" json:"model,omitempty"` // 可空:自动取 platform 第一个 model + Values []string `protobuf:"bytes,1,rep,name=values,proto3" json:"values,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *HostProbeForwardRequest) Reset() { - *x = HostProbeForwardRequest{} +func (x *HeaderValues) Reset() { + *x = HeaderValues{} mi := &file_plugin_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *HostProbeForwardRequest) String() string { +func (x *HeaderValues) String() string { return protoimpl.X.MessageStringOf(x) } -func (*HostProbeForwardRequest) ProtoMessage() {} +func (*HeaderValues) ProtoMessage() {} -func (x *HostProbeForwardRequest) ProtoReflect() protoreflect.Message { +func (x *HeaderValues) ProtoReflect() protoreflect.Message { mi := &file_plugin_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -349,53 +304,60 @@ func (x *HostProbeForwardRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use HostProbeForwardRequest.ProtoReflect.Descriptor instead. -func (*HostProbeForwardRequest) Descriptor() ([]byte, []int) { +// Deprecated: Use HeaderValues.ProtoReflect.Descriptor instead. +func (*HeaderValues) Descriptor() ([]byte, []int) { return file_plugin_proto_rawDescGZIP(), []int{2} } -func (x *HostProbeForwardRequest) GetGroupId() int64 { - if x != nil { - return x.GroupId - } - return 0 -} - -func (x *HostProbeForwardRequest) GetModel() string { +func (x *HeaderValues) GetValues() []string { if x != nil { - return x.Model + return x.Values } - return "" + return nil } -type HostProbeForwardResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` - AccountId int64 `protobuf:"varint,2,opt,name=account_id,json=accountId,proto3" json:"account_id,omitempty"` // 本次实际命中的账号 ID(用于运维诊断) - Platform string `protobuf:"bytes,3,opt,name=platform,proto3" json:"platform,omitempty"` - Model string `protobuf:"bytes,4,opt,name=model,proto3" json:"model,omitempty"` // 实际探测用的 model - StatusCode int64 `protobuf:"varint,5,opt,name=status_code,json=statusCode,proto3" json:"status_code,omitempty"` - LatencyMs int64 `protobuf:"varint,6,opt,name=latency_ms,json=latencyMs,proto3" json:"latency_ms,omitempty"` - ErrorKind string `protobuf:"bytes,7,opt,name=error_kind,json=errorKind,proto3" json:"error_kind,omitempty"` // "" / "no_account" / "scheduler" / "upstream_5xx" / "timeout" / ... - ErrorMsg string `protobuf:"bytes,8,opt,name=error_msg,json=errorMsg,proto3" json:"error_msg,omitempty"` +type PluginInfoResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Version string `protobuf:"bytes,3,opt,name=version,proto3" json:"version,omitempty"` + Description string `protobuf:"bytes,4,opt,name=description,proto3" json:"description,omitempty"` + Author string `protobuf:"bytes,5,opt,name=author,proto3" json:"author,omitempty"` + Type string `protobuf:"bytes,6,opt,name=type,proto3" json:"type,omitempty"` + AccountTypes []*AccountTypeProto `protobuf:"bytes,7,rep,name=account_types,json=accountTypes,proto3" json:"account_types,omitempty"` + FrontendPages []*FrontendPageProto `protobuf:"bytes,8,rep,name=frontend_pages,json=frontendPages,proto3" json:"frontend_pages,omitempty"` + FrontendWidgets []*FrontendWidgetProto `protobuf:"bytes,9,rep,name=frontend_widgets,json=frontendWidgets,proto3" json:"frontend_widgets,omitempty"` + SdkVersion string `protobuf:"bytes,10,opt,name=sdk_version,json=sdkVersion,proto3" json:"sdk_version,omitempty"` // 插件编译时的 SDK 版本 + Dependencies []string `protobuf:"bytes,11,rep,name=dependencies,proto3" json:"dependencies,omitempty"` // 依赖的其他插件 ID + ConfigSchema []*ConfigFieldProto `protobuf:"bytes,12,rep,name=config_schema,json=configSchema,proto3" json:"config_schema,omitempty"` // 配置项声明 + InstructionPresets []string `protobuf:"bytes,13,rep,name=instruction_presets,json=instructionPresets,proto3" json:"instruction_presets,omitempty"` // 可用的 instructions 预设名称列表 + // capabilities 声明 Host.Invoke / Host.InvokeStream、method 级授权和 Middleware 能力。 + // Core 启动时按插件类型、方法注册表和 RPC 调用做准入校验。 + Capabilities []string `protobuf:"bytes,14,rep,name=capabilities,proto3" json:"capabilities,omitempty"` + // priority 中间件链中的排序权重(数值越小越早进 Begin、越晚出 End)。 + // 仅 type="middleware" 插件使用;其他类型忽略。默认 100。 + Priority int32 `protobuf:"varint,15,opt,name=priority,proto3" json:"priority,omitempty"` + // metadata 保存插件声明层面的弱契约扩展信息,例如分类、市场标签、展示提示。 + // 需要 Core 授权或参与调度的数据必须进入显式字段或 capability。 + Metadata map[string]string `protobuf:"bytes,16,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *HostProbeForwardResponse) Reset() { - *x = HostProbeForwardResponse{} +func (x *PluginInfoResponse) Reset() { + *x = PluginInfoResponse{} mi := &file_plugin_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *HostProbeForwardResponse) String() string { +func (x *PluginInfoResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*HostProbeForwardResponse) ProtoMessage() {} +func (*PluginInfoResponse) ProtoMessage() {} -func (x *HostProbeForwardResponse) ProtoReflect() protoreflect.Message { +func (x *PluginInfoResponse) ProtoReflect() protoreflect.Message { mi := &file_plugin_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -407,201 +369,151 @@ func (x *HostProbeForwardResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use HostProbeForwardResponse.ProtoReflect.Descriptor instead. -func (*HostProbeForwardResponse) Descriptor() ([]byte, []int) { +// Deprecated: Use PluginInfoResponse.ProtoReflect.Descriptor instead. +func (*PluginInfoResponse) Descriptor() ([]byte, []int) { return file_plugin_proto_rawDescGZIP(), []int{3} } -func (x *HostProbeForwardResponse) GetSuccess() bool { +func (x *PluginInfoResponse) GetId() string { if x != nil { - return x.Success + return x.Id } - return false + return "" } -func (x *HostProbeForwardResponse) GetAccountId() int64 { +func (x *PluginInfoResponse) GetName() string { if x != nil { - return x.AccountId + return x.Name } - return 0 + return "" } -func (x *HostProbeForwardResponse) GetPlatform() string { +func (x *PluginInfoResponse) GetVersion() string { if x != nil { - return x.Platform + return x.Version } return "" } -func (x *HostProbeForwardResponse) GetModel() string { +func (x *PluginInfoResponse) GetDescription() string { if x != nil { - return x.Model + return x.Description } return "" } -func (x *HostProbeForwardResponse) GetStatusCode() int64 { +func (x *PluginInfoResponse) GetAuthor() string { if x != nil { - return x.StatusCode + return x.Author } - return 0 + return "" } -func (x *HostProbeForwardResponse) GetLatencyMs() int64 { +func (x *PluginInfoResponse) GetType() string { if x != nil { - return x.LatencyMs + return x.Type } - return 0 + return "" } -func (x *HostProbeForwardResponse) GetErrorKind() string { +func (x *PluginInfoResponse) GetAccountTypes() []*AccountTypeProto { if x != nil { - return x.ErrorKind + return x.AccountTypes } - return "" + return nil } -func (x *HostProbeForwardResponse) GetErrorMsg() string { +func (x *PluginInfoResponse) GetFrontendPages() []*FrontendPageProto { if x != nil { - return x.ErrorMsg + return x.FrontendPages } - return "" -} - -type HostListGroupsRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *HostListGroupsRequest) Reset() { - *x = HostListGroupsRequest{} - mi := &file_plugin_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *HostListGroupsRequest) String() string { - return protoimpl.X.MessageStringOf(x) + return nil } -func (*HostListGroupsRequest) ProtoMessage() {} - -func (x *HostListGroupsRequest) ProtoReflect() protoreflect.Message { - mi := &file_plugin_proto_msgTypes[4] +func (x *PluginInfoResponse) GetFrontendWidgets() []*FrontendWidgetProto { if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms + return x.FrontendWidgets } - return mi.MessageOf(x) -} - -// Deprecated: Use HostListGroupsRequest.ProtoReflect.Descriptor instead. -func (*HostListGroupsRequest) Descriptor() ([]byte, []int) { - return file_plugin_proto_rawDescGZIP(), []int{4} -} - -type HostGroup struct { - state protoimpl.MessageState `protogen:"open.v1"` - Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` - Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` - Platform string `protobuf:"bytes,3,opt,name=platform,proto3" json:"platform,omitempty"` - IsExclusive bool `protobuf:"varint,4,opt,name=is_exclusive,json=isExclusive,proto3" json:"is_exclusive,omitempty"` - RateMultiplier float64 `protobuf:"fixed64,5,opt,name=rate_multiplier,json=rateMultiplier,proto3" json:"rate_multiplier,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *HostGroup) Reset() { - *x = HostGroup{} - mi := &file_plugin_proto_msgTypes[5] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *HostGroup) String() string { - return protoimpl.X.MessageStringOf(x) + return nil } -func (*HostGroup) ProtoMessage() {} - -func (x *HostGroup) ProtoReflect() protoreflect.Message { - mi := &file_plugin_proto_msgTypes[5] +func (x *PluginInfoResponse) GetSdkVersion() string { if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms + return x.SdkVersion } - return mi.MessageOf(x) + return "" } -// Deprecated: Use HostGroup.ProtoReflect.Descriptor instead. -func (*HostGroup) Descriptor() ([]byte, []int) { - return file_plugin_proto_rawDescGZIP(), []int{5} +func (x *PluginInfoResponse) GetDependencies() []string { + if x != nil { + return x.Dependencies + } + return nil } -func (x *HostGroup) GetId() int64 { +func (x *PluginInfoResponse) GetConfigSchema() []*ConfigFieldProto { if x != nil { - return x.Id + return x.ConfigSchema } - return 0 + return nil } -func (x *HostGroup) GetName() string { +func (x *PluginInfoResponse) GetInstructionPresets() []string { if x != nil { - return x.Name + return x.InstructionPresets } - return "" + return nil } -func (x *HostGroup) GetPlatform() string { +func (x *PluginInfoResponse) GetCapabilities() []string { if x != nil { - return x.Platform + return x.Capabilities } - return "" + return nil } -func (x *HostGroup) GetIsExclusive() bool { +func (x *PluginInfoResponse) GetPriority() int32 { if x != nil { - return x.IsExclusive + return x.Priority } - return false + return 0 } -func (x *HostGroup) GetRateMultiplier() float64 { +func (x *PluginInfoResponse) GetMetadata() map[string]string { if x != nil { - return x.RateMultiplier + return x.Metadata } - return 0 + return nil } -type HostListGroupsResponse struct { +type ConfigFieldProto struct { state protoimpl.MessageState `protogen:"open.v1"` - Groups []*HostGroup `protobuf:"bytes,1,rep,name=groups,proto3" json:"groups,omitempty"` + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + Label string `protobuf:"bytes,2,opt,name=label,proto3" json:"label,omitempty"` + Type string `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"` + Required bool `protobuf:"varint,4,opt,name=required,proto3" json:"required,omitempty"` + DefaultValue string `protobuf:"bytes,5,opt,name=default_value,json=defaultValue,proto3" json:"default_value,omitempty"` + Description string `protobuf:"bytes,6,opt,name=description,proto3" json:"description,omitempty"` + Placeholder string `protobuf:"bytes,7,opt,name=placeholder,proto3" json:"placeholder,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *HostListGroupsResponse) Reset() { - *x = HostListGroupsResponse{} - mi := &file_plugin_proto_msgTypes[6] +func (x *ConfigFieldProto) Reset() { + *x = ConfigFieldProto{} + mi := &file_plugin_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *HostListGroupsResponse) String() string { +func (x *ConfigFieldProto) String() string { return protoimpl.X.MessageStringOf(x) } -func (*HostListGroupsResponse) ProtoMessage() {} +func (*ConfigFieldProto) ProtoMessage() {} -func (x *HostListGroupsResponse) ProtoReflect() protoreflect.Message { - mi := &file_plugin_proto_msgTypes[6] +func (x *ConfigFieldProto) ProtoReflect() protoreflect.Message { + mi := &file_plugin_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -612,109 +524,85 @@ func (x *HostListGroupsResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use HostListGroupsResponse.ProtoReflect.Descriptor instead. -func (*HostListGroupsResponse) Descriptor() ([]byte, []int) { - return file_plugin_proto_rawDescGZIP(), []int{6} +// Deprecated: Use ConfigFieldProto.ProtoReflect.Descriptor instead. +func (*ConfigFieldProto) Descriptor() ([]byte, []int) { + return file_plugin_proto_rawDescGZIP(), []int{4} } -func (x *HostListGroupsResponse) GetGroups() []*HostGroup { +func (x *ConfigFieldProto) GetKey() string { if x != nil { - return x.Groups + return x.Key } - return nil + return "" } -type HostReportAccountResultRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - AccountId int64 `protobuf:"varint,1,opt,name=account_id,json=accountId,proto3" json:"account_id,omitempty"` - Success bool `protobuf:"varint,2,opt,name=success,proto3" json:"success,omitempty"` - ErrorMsg string `protobuf:"bytes,3,opt,name=error_msg,json=errorMsg,proto3" json:"error_msg,omitempty"` // 失败时上报,便于调试 - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache +func (x *ConfigFieldProto) GetLabel() string { + if x != nil { + return x.Label + } + return "" } -func (x *HostReportAccountResultRequest) Reset() { - *x = HostReportAccountResultRequest{} - mi := &file_plugin_proto_msgTypes[7] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *HostReportAccountResultRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*HostReportAccountResultRequest) ProtoMessage() {} - -func (x *HostReportAccountResultRequest) ProtoReflect() protoreflect.Message { - mi := &file_plugin_proto_msgTypes[7] +func (x *ConfigFieldProto) GetType() string { if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms + return x.Type } - return mi.MessageOf(x) + return "" } -// Deprecated: Use HostReportAccountResultRequest.ProtoReflect.Descriptor instead. -func (*HostReportAccountResultRequest) Descriptor() ([]byte, []int) { - return file_plugin_proto_rawDescGZIP(), []int{7} +func (x *ConfigFieldProto) GetRequired() bool { + if x != nil { + return x.Required + } + return false } -func (x *HostReportAccountResultRequest) GetAccountId() int64 { +func (x *ConfigFieldProto) GetDefaultValue() string { if x != nil { - return x.AccountId + return x.DefaultValue } - return 0 + return "" } -func (x *HostReportAccountResultRequest) GetSuccess() bool { +func (x *ConfigFieldProto) GetDescription() string { if x != nil { - return x.Success + return x.Description } - return false + return "" } -func (x *HostReportAccountResultRequest) GetErrorMsg() string { +func (x *ConfigFieldProto) GetPlaceholder() string { if x != nil { - return x.ErrorMsg + return x.Placeholder } return "" } -// HostForwardRequest 业务转发入参。 -// user_id + group_id 共同确定计费主体和调度路径。 -type HostForwardRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - UserId int64 `protobuf:"varint,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` // 计费主体(扣余额的用户) - GroupId int64 `protobuf:"varint,2,opt,name=group_id,json=groupId,proto3" json:"group_id,omitempty"` // 调度分组 - Model string `protobuf:"bytes,3,opt,name=model,proto3" json:"model,omitempty"` // 可空:空时 Core 取该 platform 的第一个 model - Method string `protobuf:"bytes,4,opt,name=method,proto3" json:"method,omitempty"` // HTTP method(POST / GET / ...) - Path string `protobuf:"bytes,5,opt,name=path,proto3" json:"path,omitempty"` // 请求路径(如 /v1/chat/completions) - Headers map[string]*HeaderValues `protobuf:"bytes,6,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - Body []byte `protobuf:"bytes,7,opt,name=body,proto3" json:"body,omitempty"` - Stream bool `protobuf:"varint,8,opt,name=stream,proto3" json:"stream,omitempty"` // 是否流式 +type AccountTypeProto struct { + state protoimpl.MessageState `protogen:"open.v1"` + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + Label string `protobuf:"bytes,2,opt,name=label,proto3" json:"label,omitempty"` + Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` + Fields []*CredentialFieldProto `protobuf:"bytes,4,rep,name=fields,proto3" json:"fields,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *HostForwardRequest) Reset() { - *x = HostForwardRequest{} - mi := &file_plugin_proto_msgTypes[8] +func (x *AccountTypeProto) Reset() { + *x = AccountTypeProto{} + mi := &file_plugin_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *HostForwardRequest) String() string { +func (x *AccountTypeProto) String() string { return protoimpl.X.MessageStringOf(x) } -func (*HostForwardRequest) ProtoMessage() {} +func (*AccountTypeProto) ProtoMessage() {} -func (x *HostForwardRequest) ProtoReflect() protoreflect.Message { - mi := &file_plugin_proto_msgTypes[8] +func (x *AccountTypeProto) ProtoReflect() protoreflect.Message { + mi := &file_plugin_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -725,93 +613,66 @@ func (x *HostForwardRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use HostForwardRequest.ProtoReflect.Descriptor instead. -func (*HostForwardRequest) Descriptor() ([]byte, []int) { - return file_plugin_proto_rawDescGZIP(), []int{8} -} - -func (x *HostForwardRequest) GetUserId() int64 { - if x != nil { - return x.UserId - } - return 0 -} - -func (x *HostForwardRequest) GetGroupId() int64 { - if x != nil { - return x.GroupId - } - return 0 +// Deprecated: Use AccountTypeProto.ProtoReflect.Descriptor instead. +func (*AccountTypeProto) Descriptor() ([]byte, []int) { + return file_plugin_proto_rawDescGZIP(), []int{5} } -func (x *HostForwardRequest) GetModel() string { +func (x *AccountTypeProto) GetKey() string { if x != nil { - return x.Model + return x.Key } return "" } -func (x *HostForwardRequest) GetMethod() string { +func (x *AccountTypeProto) GetLabel() string { if x != nil { - return x.Method + return x.Label } return "" } -func (x *HostForwardRequest) GetPath() string { +func (x *AccountTypeProto) GetDescription() string { if x != nil { - return x.Path + return x.Description } return "" } -func (x *HostForwardRequest) GetHeaders() map[string]*HeaderValues { - if x != nil { - return x.Headers - } - return nil -} - -func (x *HostForwardRequest) GetBody() []byte { +func (x *AccountTypeProto) GetFields() []*CredentialFieldProto { if x != nil { - return x.Body + return x.Fields } return nil } -func (x *HostForwardRequest) GetStream() bool { - if x != nil { - return x.Stream - } - return false -} - -// HostForwardResponse 非流式转发结果。 -type HostForwardResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - StatusCode int32 `protobuf:"varint,1,opt,name=status_code,json=statusCode,proto3" json:"status_code,omitempty"` - Headers map[string]*HeaderValues `protobuf:"bytes,2,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - Body []byte `protobuf:"bytes,3,opt,name=body,proto3" json:"body,omitempty"` - Usage *HostForwardUsage `protobuf:"bytes,4,opt,name=usage,proto3" json:"usage,omitempty"` +type CredentialFieldProto struct { + state protoimpl.MessageState `protogen:"open.v1"` + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + Label string `protobuf:"bytes,2,opt,name=label,proto3" json:"label,omitempty"` + Type string `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"` + Required bool `protobuf:"varint,4,opt,name=required,proto3" json:"required,omitempty"` + Placeholder string `protobuf:"bytes,5,opt,name=placeholder,proto3" json:"placeholder,omitempty"` + EditDisabled bool `protobuf:"varint,6,opt,name=edit_disabled,json=editDisabled,proto3" json:"edit_disabled,omitempty"` // 编辑模式下隐藏该字段 unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *HostForwardResponse) Reset() { - *x = HostForwardResponse{} - mi := &file_plugin_proto_msgTypes[9] +func (x *CredentialFieldProto) Reset() { + *x = CredentialFieldProto{} + mi := &file_plugin_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *HostForwardResponse) String() string { +func (x *CredentialFieldProto) String() string { return protoimpl.X.MessageStringOf(x) } -func (*HostForwardResponse) ProtoMessage() {} +func (*CredentialFieldProto) ProtoMessage() {} -func (x *HostForwardResponse) ProtoReflect() protoreflect.Message { - mi := &file_plugin_proto_msgTypes[9] +func (x *CredentialFieldProto) ProtoReflect() protoreflect.Message { + mi := &file_plugin_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -822,67 +683,79 @@ func (x *HostForwardResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use HostForwardResponse.ProtoReflect.Descriptor instead. -func (*HostForwardResponse) Descriptor() ([]byte, []int) { - return file_plugin_proto_rawDescGZIP(), []int{9} +// Deprecated: Use CredentialFieldProto.ProtoReflect.Descriptor instead. +func (*CredentialFieldProto) Descriptor() ([]byte, []int) { + return file_plugin_proto_rawDescGZIP(), []int{6} } -func (x *HostForwardResponse) GetStatusCode() int32 { +func (x *CredentialFieldProto) GetKey() string { if x != nil { - return x.StatusCode + return x.Key } - return 0 + return "" } -func (x *HostForwardResponse) GetHeaders() map[string]*HeaderValues { +func (x *CredentialFieldProto) GetLabel() string { if x != nil { - return x.Headers + return x.Label } - return nil + return "" } -func (x *HostForwardResponse) GetBody() []byte { +func (x *CredentialFieldProto) GetType() string { if x != nil { - return x.Body + return x.Type } - return nil + return "" } -func (x *HostForwardResponse) GetUsage() *HostForwardUsage { +func (x *CredentialFieldProto) GetRequired() bool { if x != nil { - return x.Usage + return x.Required } - return nil + return false } -// HostForwardChunk 流式转发的单块数据。 -// 第一块携带 status_code + headers;最后一块 done=true 携带 usage。 -type HostForwardChunk struct { - state protoimpl.MessageState `protogen:"open.v1"` - Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` - Done bool `protobuf:"varint,2,opt,name=done,proto3" json:"done,omitempty"` - StatusCode int32 `protobuf:"varint,3,opt,name=status_code,json=statusCode,proto3" json:"status_code,omitempty"` // 仅首块 - Headers map[string]*HeaderValues `protobuf:"bytes,4,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` // 仅首块 - Usage *HostForwardUsage `protobuf:"bytes,5,opt,name=usage,proto3" json:"usage,omitempty"` // 仅末块 +func (x *CredentialFieldProto) GetPlaceholder() string { + if x != nil { + return x.Placeholder + } + return "" +} + +func (x *CredentialFieldProto) GetEditDisabled() bool { + if x != nil { + return x.EditDisabled + } + return false +} + +type FrontendPageProto struct { + state protoimpl.MessageState `protogen:"open.v1"` + Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` + Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"` + Icon string `protobuf:"bytes,3,opt,name=icon,proto3" json:"icon,omitempty"` + Description string `protobuf:"bytes,4,opt,name=description,proto3" json:"description,omitempty"` + Audience string `protobuf:"bytes,5,opt,name=audience,proto3" json:"audience,omitempty"` // "admin" | "user" | "all",空 = "admin" unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *HostForwardChunk) Reset() { - *x = HostForwardChunk{} - mi := &file_plugin_proto_msgTypes[10] +func (x *FrontendPageProto) Reset() { + *x = FrontendPageProto{} + mi := &file_plugin_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *HostForwardChunk) String() string { +func (x *FrontendPageProto) String() string { return protoimpl.X.MessageStringOf(x) } -func (*HostForwardChunk) ProtoMessage() {} +func (*FrontendPageProto) ProtoMessage() {} -func (x *HostForwardChunk) ProtoReflect() protoreflect.Message { - mi := &file_plugin_proto_msgTypes[10] +func (x *FrontendPageProto) ProtoReflect() protoreflect.Message { + mi := &file_plugin_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -893,72 +766,70 @@ func (x *HostForwardChunk) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use HostForwardChunk.ProtoReflect.Descriptor instead. -func (*HostForwardChunk) Descriptor() ([]byte, []int) { - return file_plugin_proto_rawDescGZIP(), []int{10} +// Deprecated: Use FrontendPageProto.ProtoReflect.Descriptor instead. +func (*FrontendPageProto) Descriptor() ([]byte, []int) { + return file_plugin_proto_rawDescGZIP(), []int{7} } -func (x *HostForwardChunk) GetData() []byte { +func (x *FrontendPageProto) GetPath() string { if x != nil { - return x.Data + return x.Path } - return nil + return "" } -func (x *HostForwardChunk) GetDone() bool { +func (x *FrontendPageProto) GetTitle() string { if x != nil { - return x.Done + return x.Title } - return false + return "" } -func (x *HostForwardChunk) GetStatusCode() int32 { +func (x *FrontendPageProto) GetIcon() string { if x != nil { - return x.StatusCode + return x.Icon } - return 0 + return "" } -func (x *HostForwardChunk) GetHeaders() map[string]*HeaderValues { +func (x *FrontendPageProto) GetDescription() string { if x != nil { - return x.Headers + return x.Description } - return nil + return "" } -func (x *HostForwardChunk) GetUsage() *HostForwardUsage { +func (x *FrontendPageProto) GetAudience() string { if x != nil { - return x.Usage + return x.Audience } - return nil + return "" } -// HostForwardUsage 转发的 token / 费用摘要(Core 侧计算后回传给调用方插件)。 -type HostForwardUsage struct { +type FrontendWidgetProto struct { state protoimpl.MessageState `protogen:"open.v1"` - InputTokens int64 `protobuf:"varint,1,opt,name=input_tokens,json=inputTokens,proto3" json:"input_tokens,omitempty"` - OutputTokens int64 `protobuf:"varint,2,opt,name=output_tokens,json=outputTokens,proto3" json:"output_tokens,omitempty"` - Cost float64 `protobuf:"fixed64,3,opt,name=cost,proto3" json:"cost,omitempty"` // 总费用(已计入倍率) - Model string `protobuf:"bytes,4,opt,name=model,proto3" json:"model,omitempty"` // 实际使用的 model(可能被 Core 改写) + Slot string `protobuf:"bytes,1,opt,name=slot,proto3" json:"slot,omitempty"` + EntryFile string `protobuf:"bytes,2,opt,name=entry_file,json=entryFile,proto3" json:"entry_file,omitempty"` + Title string `protobuf:"bytes,3,opt,name=title,proto3" json:"title,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *HostForwardUsage) Reset() { - *x = HostForwardUsage{} - mi := &file_plugin_proto_msgTypes[11] +func (x *FrontendWidgetProto) Reset() { + *x = FrontendWidgetProto{} + mi := &file_plugin_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *HostForwardUsage) String() string { +func (x *FrontendWidgetProto) String() string { return protoimpl.X.MessageStringOf(x) } -func (*HostForwardUsage) ProtoMessage() {} +func (*FrontendWidgetProto) ProtoMessage() {} -func (x *HostForwardUsage) ProtoReflect() protoreflect.Message { - mi := &file_plugin_proto_msgTypes[11] +func (x *FrontendWidgetProto) ProtoReflect() protoreflect.Message { + mi := &file_plugin_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -969,60 +840,60 @@ func (x *HostForwardUsage) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use HostForwardUsage.ProtoReflect.Descriptor instead. -func (*HostForwardUsage) Descriptor() ([]byte, []int) { - return file_plugin_proto_rawDescGZIP(), []int{11} -} - -func (x *HostForwardUsage) GetInputTokens() int64 { - if x != nil { - return x.InputTokens - } - return 0 +// Deprecated: Use FrontendWidgetProto.ProtoReflect.Descriptor instead. +func (*FrontendWidgetProto) Descriptor() ([]byte, []int) { + return file_plugin_proto_rawDescGZIP(), []int{8} } -func (x *HostForwardUsage) GetOutputTokens() int64 { +func (x *FrontendWidgetProto) GetSlot() string { if x != nil { - return x.OutputTokens + return x.Slot } - return 0 + return "" } -func (x *HostForwardUsage) GetCost() float64 { +func (x *FrontendWidgetProto) GetEntryFile() string { if x != nil { - return x.Cost + return x.EntryFile } - return 0 + return "" } -func (x *HostForwardUsage) GetModel() string { +func (x *FrontendWidgetProto) GetTitle() string { if x != nil { - return x.Model + return x.Title } return "" } -type HostListPlatformsRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache +type InitRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Config map[string]string `protobuf:"bytes,1,rep,name=config,proto3" json:"config,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + LogLevel string `protobuf:"bytes,2,opt,name=log_level,json=logLevel,proto3" json:"log_level,omitempty"` + // core_invoke_broker_id 是 Core 通过 hashicorp/go-plugin GRPCBroker 启动的 + // 反向调用 stream ID。插件 Init 时拿到 ID 后,可以通过 broker.Dial(id) + // 拿到 CoreInvokeService grpc client,通过 Invoke / InvokeStream 回调 Core 开放的方法。 + // 0 表示 Core 没启用反向调用。 + CoreInvokeBrokerId uint32 `protobuf:"varint,3,opt,name=core_invoke_broker_id,json=coreInvokeBrokerId,proto3" json:"core_invoke_broker_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *HostListPlatformsRequest) Reset() { - *x = HostListPlatformsRequest{} - mi := &file_plugin_proto_msgTypes[12] +func (x *InitRequest) Reset() { + *x = InitRequest{} + mi := &file_plugin_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *HostListPlatformsRequest) String() string { +func (x *InitRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*HostListPlatformsRequest) ProtoMessage() {} +func (*InitRequest) ProtoMessage() {} -func (x *HostListPlatformsRequest) ProtoReflect() protoreflect.Message { - mi := &file_plugin_proto_msgTypes[12] +func (x *InitRequest) ProtoReflect() protoreflect.Message { + mi := &file_plugin_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1033,34 +904,59 @@ func (x *HostListPlatformsRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use HostListPlatformsRequest.ProtoReflect.Descriptor instead. -func (*HostListPlatformsRequest) Descriptor() ([]byte, []int) { - return file_plugin_proto_rawDescGZIP(), []int{12} +// Deprecated: Use InitRequest.ProtoReflect.Descriptor instead. +func (*InitRequest) Descriptor() ([]byte, []int) { + return file_plugin_proto_rawDescGZIP(), []int{9} } -type HostPlatform struct { - state protoimpl.MessageState `protogen:"open.v1"` - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` // 平台标识(如 "openai") - DisplayName string `protobuf:"bytes,2,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"` // 展示名称(如 "OpenAI 网关") - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache +func (x *InitRequest) GetConfig() map[string]string { + if x != nil { + return x.Config + } + return nil } -func (x *HostPlatform) Reset() { - *x = HostPlatform{} - mi := &file_plugin_proto_msgTypes[13] +func (x *InitRequest) GetLogLevel() string { + if x != nil { + return x.LogLevel + } + return "" +} + +func (x *InitRequest) GetCoreInvokeBrokerId() uint32 { + if x != nil { + return x.CoreInvokeBrokerId + } + return 0 +} + +type ModelInfoProto struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + ContextWindow int64 `protobuf:"varint,3,opt,name=context_window,json=contextWindow,proto3" json:"context_window,omitempty"` + MaxOutputTokens int64 `protobuf:"varint,4,opt,name=max_output_tokens,json=maxOutputTokens,proto3" json:"max_output_tokens,omitempty"` + Capabilities []string `protobuf:"bytes,5,rep,name=capabilities,proto3" json:"capabilities,omitempty"` + Metadata map[string]string `protobuf:"bytes,6,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ModelInfoProto) Reset() { + *x = ModelInfoProto{} + mi := &file_plugin_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *HostPlatform) String() string { +func (x *ModelInfoProto) String() string { return protoimpl.X.MessageStringOf(x) } -func (*HostPlatform) ProtoMessage() {} +func (*ModelInfoProto) ProtoMessage() {} -func (x *HostPlatform) ProtoReflect() protoreflect.Message { - mi := &file_plugin_proto_msgTypes[13] +func (x *ModelInfoProto) ProtoReflect() protoreflect.Message { + mi := &file_plugin_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1071,91 +967,75 @@ func (x *HostPlatform) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use HostPlatform.ProtoReflect.Descriptor instead. -func (*HostPlatform) Descriptor() ([]byte, []int) { - return file_plugin_proto_rawDescGZIP(), []int{13} +// Deprecated: Use ModelInfoProto.ProtoReflect.Descriptor instead. +func (*ModelInfoProto) Descriptor() ([]byte, []int) { + return file_plugin_proto_rawDescGZIP(), []int{10} } -func (x *HostPlatform) GetName() string { +func (x *ModelInfoProto) GetId() string { if x != nil { - return x.Name + return x.Id } return "" } -func (x *HostPlatform) GetDisplayName() string { +func (x *ModelInfoProto) GetName() string { if x != nil { - return x.DisplayName + return x.Name } return "" } -type HostListPlatformsResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Platforms []*HostPlatform `protobuf:"bytes,1,rep,name=platforms,proto3" json:"platforms,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *HostListPlatformsResponse) Reset() { - *x = HostListPlatformsResponse{} - mi := &file_plugin_proto_msgTypes[14] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *HostListPlatformsResponse) String() string { - return protoimpl.X.MessageStringOf(x) +func (x *ModelInfoProto) GetContextWindow() int64 { + if x != nil { + return x.ContextWindow + } + return 0 } -func (*HostListPlatformsResponse) ProtoMessage() {} - -func (x *HostListPlatformsResponse) ProtoReflect() protoreflect.Message { - mi := &file_plugin_proto_msgTypes[14] +func (x *ModelInfoProto) GetMaxOutputTokens() int64 { if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms + return x.MaxOutputTokens } - return mi.MessageOf(x) + return 0 } -// Deprecated: Use HostListPlatformsResponse.ProtoReflect.Descriptor instead. -func (*HostListPlatformsResponse) Descriptor() ([]byte, []int) { - return file_plugin_proto_rawDescGZIP(), []int{14} +func (x *ModelInfoProto) GetCapabilities() []string { + if x != nil { + return x.Capabilities + } + return nil } -func (x *HostListPlatformsResponse) GetPlatforms() []*HostPlatform { +func (x *ModelInfoProto) GetMetadata() map[string]string { if x != nil { - return x.Platforms + return x.Metadata } return nil } -type HostListModelsRequest struct { +type ModelsResponse struct { state protoimpl.MessageState `protogen:"open.v1"` - Platform string `protobuf:"bytes,1,opt,name=platform,proto3" json:"platform,omitempty"` // 必填:平台标识 + Models []*ModelInfoProto `protobuf:"bytes,1,rep,name=models,proto3" json:"models,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *HostListModelsRequest) Reset() { - *x = HostListModelsRequest{} - mi := &file_plugin_proto_msgTypes[15] +func (x *ModelsResponse) Reset() { + *x = ModelsResponse{} + mi := &file_plugin_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *HostListModelsRequest) String() string { +func (x *ModelsResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*HostListModelsRequest) ProtoMessage() {} +func (*ModelsResponse) ProtoMessage() {} -func (x *HostListModelsRequest) ProtoReflect() protoreflect.Message { - mi := &file_plugin_proto_msgTypes[15] +func (x *ModelsResponse) ProtoReflect() protoreflect.Message { + mi := &file_plugin_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1166,40 +1046,43 @@ func (x *HostListModelsRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use HostListModelsRequest.ProtoReflect.Descriptor instead. -func (*HostListModelsRequest) Descriptor() ([]byte, []int) { - return file_plugin_proto_rawDescGZIP(), []int{15} +// Deprecated: Use ModelsResponse.ProtoReflect.Descriptor instead. +func (*ModelsResponse) Descriptor() ([]byte, []int) { + return file_plugin_proto_rawDescGZIP(), []int{11} } -func (x *HostListModelsRequest) GetPlatform() string { +func (x *ModelsResponse) GetModels() []*ModelInfoProto { if x != nil { - return x.Platform + return x.Models } - return "" + return nil } -type HostListModelsResponse struct { +type RouteDefinitionProto struct { state protoimpl.MessageState `protogen:"open.v1"` - Models []*ModelInfoProto `protobuf:"bytes,1,rep,name=models,proto3" json:"models,omitempty"` // 复用已有 ModelInfoProto,避免平行数据结构漂移 + Method string `protobuf:"bytes,1,opt,name=method,proto3" json:"method,omitempty"` + Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` + Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` + Metadata map[string]string `protobuf:"bytes,4,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *HostListModelsResponse) Reset() { - *x = HostListModelsResponse{} - mi := &file_plugin_proto_msgTypes[16] +func (x *RouteDefinitionProto) Reset() { + *x = RouteDefinitionProto{} + mi := &file_plugin_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *HostListModelsResponse) String() string { +func (x *RouteDefinitionProto) String() string { return protoimpl.X.MessageStringOf(x) } -func (*HostListModelsResponse) ProtoMessage() {} +func (*RouteDefinitionProto) ProtoMessage() {} -func (x *HostListModelsResponse) ProtoReflect() protoreflect.Message { - mi := &file_plugin_proto_msgTypes[16] +func (x *RouteDefinitionProto) ProtoReflect() protoreflect.Message { + mi := &file_plugin_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1210,40 +1093,61 @@ func (x *HostListModelsResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use HostListModelsResponse.ProtoReflect.Descriptor instead. -func (*HostListModelsResponse) Descriptor() ([]byte, []int) { - return file_plugin_proto_rawDescGZIP(), []int{16} +// Deprecated: Use RouteDefinitionProto.ProtoReflect.Descriptor instead. +func (*RouteDefinitionProto) Descriptor() ([]byte, []int) { + return file_plugin_proto_rawDescGZIP(), []int{12} } -func (x *HostListModelsResponse) GetModels() []*ModelInfoProto { +func (x *RouteDefinitionProto) GetMethod() string { if x != nil { - return x.Models + return x.Method + } + return "" +} + +func (x *RouteDefinitionProto) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *RouteDefinitionProto) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *RouteDefinitionProto) GetMetadata() map[string]string { + if x != nil { + return x.Metadata } return nil } -type HostGetUserInfoRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - UserId int64 `protobuf:"varint,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` +type RoutesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Routes []*RouteDefinitionProto `protobuf:"bytes,1,rep,name=routes,proto3" json:"routes,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *HostGetUserInfoRequest) Reset() { - *x = HostGetUserInfoRequest{} - mi := &file_plugin_proto_msgTypes[17] +func (x *RoutesResponse) Reset() { + *x = RoutesResponse{} + mi := &file_plugin_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *HostGetUserInfoRequest) String() string { +func (x *RoutesResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*HostGetUserInfoRequest) ProtoMessage() {} +func (*RoutesResponse) ProtoMessage() {} -func (x *HostGetUserInfoRequest) ProtoReflect() protoreflect.Message { - mi := &file_plugin_proto_msgTypes[17] +func (x *RoutesResponse) ProtoReflect() protoreflect.Message { + mi := &file_plugin_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1254,45 +1158,45 @@ func (x *HostGetUserInfoRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use HostGetUserInfoRequest.ProtoReflect.Descriptor instead. -func (*HostGetUserInfoRequest) Descriptor() ([]byte, []int) { - return file_plugin_proto_rawDescGZIP(), []int{17} +// Deprecated: Use RoutesResponse.ProtoReflect.Descriptor instead. +func (*RoutesResponse) Descriptor() ([]byte, []int) { + return file_plugin_proto_rawDescGZIP(), []int{13} } -func (x *HostGetUserInfoRequest) GetUserId() int64 { +func (x *RoutesResponse) GetRoutes() []*RouteDefinitionProto { if x != nil { - return x.UserId + return x.Routes } - return 0 + return nil } -type HostGetUserInfoResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - UserId int64 `protobuf:"varint,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` - Username string `protobuf:"bytes,2,opt,name=username,proto3" json:"username,omitempty"` - Email string `protobuf:"bytes,3,opt,name=email,proto3" json:"email,omitempty"` - Role string `protobuf:"bytes,4,opt,name=role,proto3" json:"role,omitempty"` // "admin" / "user" - Balance float64 `protobuf:"fixed64,5,opt,name=balance,proto3" json:"balance,omitempty"` - Status string `protobuf:"bytes,6,opt,name=status,proto3" json:"status,omitempty"` // "active" / "disabled" - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache +type AccountProto struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Platform string `protobuf:"bytes,3,opt,name=platform,proto3" json:"platform,omitempty"` + Type string `protobuf:"bytes,4,opt,name=type,proto3" json:"type,omitempty"` + CredentialsJson []byte `protobuf:"bytes,5,opt,name=credentials_json,json=credentialsJson,proto3" json:"credentials_json,omitempty"` + ProxyUrl string `protobuf:"bytes,6,opt,name=proxy_url,json=proxyUrl,proto3" json:"proxy_url,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *HostGetUserInfoResponse) Reset() { - *x = HostGetUserInfoResponse{} - mi := &file_plugin_proto_msgTypes[18] +func (x *AccountProto) Reset() { + *x = AccountProto{} + mi := &file_plugin_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *HostGetUserInfoResponse) String() string { +func (x *AccountProto) String() string { return protoimpl.X.MessageStringOf(x) } -func (*HostGetUserInfoResponse) ProtoMessage() {} +func (*AccountProto) ProtoMessage() {} -func (x *HostGetUserInfoResponse) ProtoReflect() protoreflect.Message { - mi := &file_plugin_proto_msgTypes[18] +func (x *AccountProto) ProtoReflect() protoreflect.Message { + mi := &file_plugin_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1303,79 +1207,79 @@ func (x *HostGetUserInfoResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use HostGetUserInfoResponse.ProtoReflect.Descriptor instead. -func (*HostGetUserInfoResponse) Descriptor() ([]byte, []int) { - return file_plugin_proto_rawDescGZIP(), []int{18} +// Deprecated: Use AccountProto.ProtoReflect.Descriptor instead. +func (*AccountProto) Descriptor() ([]byte, []int) { + return file_plugin_proto_rawDescGZIP(), []int{14} } -func (x *HostGetUserInfoResponse) GetUserId() int64 { +func (x *AccountProto) GetId() int64 { if x != nil { - return x.UserId + return x.Id } return 0 } -func (x *HostGetUserInfoResponse) GetUsername() string { +func (x *AccountProto) GetName() string { if x != nil { - return x.Username + return x.Name } return "" } -func (x *HostGetUserInfoResponse) GetEmail() string { +func (x *AccountProto) GetPlatform() string { if x != nil { - return x.Email + return x.Platform } return "" } -func (x *HostGetUserInfoResponse) GetRole() string { +func (x *AccountProto) GetType() string { if x != nil { - return x.Role + return x.Type } return "" } -func (x *HostGetUserInfoResponse) GetBalance() float64 { +func (x *AccountProto) GetCredentialsJson() []byte { if x != nil { - return x.Balance + return x.CredentialsJson } - return 0 + return nil } -func (x *HostGetUserInfoResponse) GetStatus() string { +func (x *AccountProto) GetProxyUrl() string { if x != nil { - return x.Status + return x.ProxyUrl } return "" } -type HostStoreAssetRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - UserId int64 `protobuf:"varint,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` - Scope string `protobuf:"bytes,2,opt,name=scope,proto3" json:"scope,omitempty"` - ContentType string `protobuf:"bytes,3,opt,name=content_type,json=contentType,proto3" json:"content_type,omitempty"` - Data []byte `protobuf:"bytes,4,opt,name=data,proto3" json:"data,omitempty"` - FileExtension string `protobuf:"bytes,5,opt,name=file_extension,json=fileExtension,proto3" json:"file_extension,omitempty"` +type ForwardRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Body []byte `protobuf:"bytes,7,opt,name=body,proto3" json:"body,omitempty"` + Headers map[string]*HeaderValues `protobuf:"bytes,8,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + Model string `protobuf:"bytes,9,opt,name=model,proto3" json:"model,omitempty"` + Stream bool `protobuf:"varint,10,opt,name=stream,proto3" json:"stream,omitempty"` + Account *AccountProto `protobuf:"bytes,11,opt,name=account,proto3" json:"account,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *HostStoreAssetRequest) Reset() { - *x = HostStoreAssetRequest{} - mi := &file_plugin_proto_msgTypes[19] +func (x *ForwardRequest) Reset() { + *x = ForwardRequest{} + mi := &file_plugin_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *HostStoreAssetRequest) String() string { +func (x *ForwardRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*HostStoreAssetRequest) ProtoMessage() {} +func (*ForwardRequest) ProtoMessage() {} -func (x *HostStoreAssetRequest) ProtoReflect() protoreflect.Message { - mi := &file_plugin_proto_msgTypes[19] +func (x *ForwardRequest) ProtoReflect() protoreflect.Message { + mi := &file_plugin_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1386,72 +1290,71 @@ func (x *HostStoreAssetRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use HostStoreAssetRequest.ProtoReflect.Descriptor instead. -func (*HostStoreAssetRequest) Descriptor() ([]byte, []int) { - return file_plugin_proto_rawDescGZIP(), []int{19} +// Deprecated: Use ForwardRequest.ProtoReflect.Descriptor instead. +func (*ForwardRequest) Descriptor() ([]byte, []int) { + return file_plugin_proto_rawDescGZIP(), []int{15} } -func (x *HostStoreAssetRequest) GetUserId() int64 { +func (x *ForwardRequest) GetBody() []byte { if x != nil { - return x.UserId + return x.Body } - return 0 + return nil } -func (x *HostStoreAssetRequest) GetScope() string { +func (x *ForwardRequest) GetHeaders() map[string]*HeaderValues { if x != nil { - return x.Scope + return x.Headers } - return "" + return nil } -func (x *HostStoreAssetRequest) GetContentType() string { +func (x *ForwardRequest) GetModel() string { if x != nil { - return x.ContentType + return x.Model } return "" } -func (x *HostStoreAssetRequest) GetData() []byte { +func (x *ForwardRequest) GetStream() bool { if x != nil { - return x.Data + return x.Stream } - return nil + return false } -func (x *HostStoreAssetRequest) GetFileExtension() string { +func (x *ForwardRequest) GetAccount() *AccountProto { if x != nil { - return x.FileExtension + return x.Account } - return "" + return nil } -type HostStoreAssetResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - AssetId string `protobuf:"bytes,1,opt,name=asset_id,json=assetId,proto3" json:"asset_id,omitempty"` - ObjectKey string `protobuf:"bytes,2,opt,name=object_key,json=objectKey,proto3" json:"object_key,omitempty"` - PublicUrl string `protobuf:"bytes,3,opt,name=public_url,json=publicUrl,proto3" json:"public_url,omitempty"` - SizeBytes int64 `protobuf:"varint,4,opt,name=size_bytes,json=sizeBytes,proto3" json:"size_bytes,omitempty"` - ContentType string `protobuf:"bytes,5,opt,name=content_type,json=contentType,proto3" json:"content_type,omitempty"` +// UpstreamResponse 上游返回的原始 HTTP 快照。 +type UpstreamResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + StatusCode int32 `protobuf:"varint,1,opt,name=status_code,json=statusCode,proto3" json:"status_code,omitempty"` + Headers map[string]*HeaderValues `protobuf:"bytes,2,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + Body []byte `protobuf:"bytes,3,opt,name=body,proto3" json:"body,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *HostStoreAssetResponse) Reset() { - *x = HostStoreAssetResponse{} - mi := &file_plugin_proto_msgTypes[20] +func (x *UpstreamResponse) Reset() { + *x = UpstreamResponse{} + mi := &file_plugin_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *HostStoreAssetResponse) String() string { +func (x *UpstreamResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*HostStoreAssetResponse) ProtoMessage() {} +func (*UpstreamResponse) ProtoMessage() {} -func (x *HostStoreAssetResponse) ProtoReflect() protoreflect.Message { - mi := &file_plugin_proto_msgTypes[20] +func (x *UpstreamResponse) ProtoReflect() protoreflect.Message { + mi := &file_plugin_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1462,68 +1365,59 @@ func (x *HostStoreAssetResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use HostStoreAssetResponse.ProtoReflect.Descriptor instead. -func (*HostStoreAssetResponse) Descriptor() ([]byte, []int) { - return file_plugin_proto_rawDescGZIP(), []int{20} -} - -func (x *HostStoreAssetResponse) GetAssetId() string { - if x != nil { - return x.AssetId - } - return "" -} - -func (x *HostStoreAssetResponse) GetObjectKey() string { - if x != nil { - return x.ObjectKey - } - return "" +// Deprecated: Use UpstreamResponse.ProtoReflect.Descriptor instead. +func (*UpstreamResponse) Descriptor() ([]byte, []int) { + return file_plugin_proto_rawDescGZIP(), []int{16} } -func (x *HostStoreAssetResponse) GetPublicUrl() string { +func (x *UpstreamResponse) GetStatusCode() int32 { if x != nil { - return x.PublicUrl + return x.StatusCode } - return "" + return 0 } -func (x *HostStoreAssetResponse) GetSizeBytes() int64 { +func (x *UpstreamResponse) GetHeaders() map[string]*HeaderValues { if x != nil { - return x.SizeBytes + return x.Headers } - return 0 + return nil } -func (x *HostStoreAssetResponse) GetContentType() string { +func (x *UpstreamResponse) GetBody() []byte { if x != nil { - return x.ContentType + return x.Body } - return "" + return nil } -type HostGetAssetURLRequest struct { +// UsageAttribute 是插件计算后的通用审计维度。 +type UsageAttribute struct { state protoimpl.MessageState `protogen:"open.v1"` - ObjectKey string `protobuf:"bytes,1,opt,name=object_key,json=objectKey,proto3" json:"object_key,omitempty"` + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + Label string `protobuf:"bytes,2,opt,name=label,proto3" json:"label,omitempty"` + Kind string `protobuf:"bytes,3,opt,name=kind,proto3" json:"kind,omitempty"` + Value string `protobuf:"bytes,4,opt,name=value,proto3" json:"value,omitempty"` + Metadata map[string]string `protobuf:"bytes,5,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *HostGetAssetURLRequest) Reset() { - *x = HostGetAssetURLRequest{} - mi := &file_plugin_proto_msgTypes[21] +func (x *UsageAttribute) Reset() { + *x = UsageAttribute{} + mi := &file_plugin_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *HostGetAssetURLRequest) String() string { +func (x *UsageAttribute) String() string { return protoimpl.X.MessageStringOf(x) } -func (*HostGetAssetURLRequest) ProtoMessage() {} +func (*UsageAttribute) ProtoMessage() {} -func (x *HostGetAssetURLRequest) ProtoReflect() protoreflect.Message { - mi := &file_plugin_proto_msgTypes[21] +func (x *UsageAttribute) ProtoReflect() protoreflect.Message { + mi := &file_plugin_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1534,40 +1428,76 @@ func (x *HostGetAssetURLRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use HostGetAssetURLRequest.ProtoReflect.Descriptor instead. -func (*HostGetAssetURLRequest) Descriptor() ([]byte, []int) { - return file_plugin_proto_rawDescGZIP(), []int{21} +// Deprecated: Use UsageAttribute.ProtoReflect.Descriptor instead. +func (*UsageAttribute) Descriptor() ([]byte, []int) { + return file_plugin_proto_rawDescGZIP(), []int{17} +} + +func (x *UsageAttribute) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *UsageAttribute) GetLabel() string { + if x != nil { + return x.Label + } + return "" +} + +func (x *UsageAttribute) GetKind() string { + if x != nil { + return x.Kind + } + return "" } -func (x *HostGetAssetURLRequest) GetObjectKey() string { +func (x *UsageAttribute) GetValue() string { if x != nil { - return x.ObjectKey + return x.Value } return "" } -type HostGetAssetURLResponse struct { +func (x *UsageAttribute) GetMetadata() map[string]string { + if x != nil { + return x.Metadata + } + return nil +} + +// UsageMetric 是插件计算后的通用计量结果。 +type UsageMetric struct { state protoimpl.MessageState `protogen:"open.v1"` - PublicUrl string `protobuf:"bytes,1,opt,name=public_url,json=publicUrl,proto3" json:"public_url,omitempty"` + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + Label string `protobuf:"bytes,2,opt,name=label,proto3" json:"label,omitempty"` + Kind string `protobuf:"bytes,3,opt,name=kind,proto3" json:"kind,omitempty"` + Unit string `protobuf:"bytes,4,opt,name=unit,proto3" json:"unit,omitempty"` + Value float64 `protobuf:"fixed64,5,opt,name=value,proto3" json:"value,omitempty"` + AccountCost float64 `protobuf:"fixed64,6,opt,name=account_cost,json=accountCost,proto3" json:"account_cost,omitempty"` + Currency string `protobuf:"bytes,7,opt,name=currency,proto3" json:"currency,omitempty"` + Metadata map[string]string `protobuf:"bytes,8,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *HostGetAssetURLResponse) Reset() { - *x = HostGetAssetURLResponse{} - mi := &file_plugin_proto_msgTypes[22] +func (x *UsageMetric) Reset() { + *x = UsageMetric{} + mi := &file_plugin_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *HostGetAssetURLResponse) String() string { +func (x *UsageMetric) String() string { return protoimpl.X.MessageStringOf(x) } -func (*HostGetAssetURLResponse) ProtoMessage() {} +func (*UsageMetric) ProtoMessage() {} -func (x *HostGetAssetURLResponse) ProtoReflect() protoreflect.Message { - mi := &file_plugin_proto_msgTypes[22] +func (x *UsageMetric) ProtoReflect() protoreflect.Message { + mi := &file_plugin_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1578,85 +1508,96 @@ func (x *HostGetAssetURLResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use HostGetAssetURLResponse.ProtoReflect.Descriptor instead. -func (*HostGetAssetURLResponse) Descriptor() ([]byte, []int) { - return file_plugin_proto_rawDescGZIP(), []int{22} +// Deprecated: Use UsageMetric.ProtoReflect.Descriptor instead. +func (*UsageMetric) Descriptor() ([]byte, []int) { + return file_plugin_proto_rawDescGZIP(), []int{18} } -func (x *HostGetAssetURLResponse) GetPublicUrl() string { +func (x *UsageMetric) GetKey() string { if x != nil { - return x.PublicUrl + return x.Key } return "" } -type HostGetAssetBytesRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - ObjectKey string `protobuf:"bytes,1,opt,name=object_key,json=objectKey,proto3" json:"object_key,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache +func (x *UsageMetric) GetLabel() string { + if x != nil { + return x.Label + } + return "" } -func (x *HostGetAssetBytesRequest) Reset() { - *x = HostGetAssetBytesRequest{} - mi := &file_plugin_proto_msgTypes[23] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) +func (x *UsageMetric) GetKind() string { + if x != nil { + return x.Kind + } + return "" } -func (x *HostGetAssetBytesRequest) String() string { - return protoimpl.X.MessageStringOf(x) +func (x *UsageMetric) GetUnit() string { + if x != nil { + return x.Unit + } + return "" } -func (*HostGetAssetBytesRequest) ProtoMessage() {} - -func (x *HostGetAssetBytesRequest) ProtoReflect() protoreflect.Message { - mi := &file_plugin_proto_msgTypes[23] +func (x *UsageMetric) GetValue() float64 { if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms + return x.Value } - return mi.MessageOf(x) + return 0 } -// Deprecated: Use HostGetAssetBytesRequest.ProtoReflect.Descriptor instead. -func (*HostGetAssetBytesRequest) Descriptor() ([]byte, []int) { - return file_plugin_proto_rawDescGZIP(), []int{23} +func (x *UsageMetric) GetAccountCost() float64 { + if x != nil { + return x.AccountCost + } + return 0 } -func (x *HostGetAssetBytesRequest) GetObjectKey() string { +func (x *UsageMetric) GetCurrency() string { if x != nil { - return x.ObjectKey + return x.Currency } return "" } -type HostGetAssetBytesResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` - ContentType string `protobuf:"bytes,2,opt,name=content_type,json=contentType,proto3" json:"content_type,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache +func (x *UsageMetric) GetMetadata() map[string]string { + if x != nil { + return x.Metadata + } + return nil } -func (x *HostGetAssetBytesResponse) Reset() { - *x = HostGetAssetBytesResponse{} - mi := &file_plugin_proto_msgTypes[24] +// UsageCostDetail 是通用费用明细。 +type UsageCostDetail struct { + state protoimpl.MessageState `protogen:"open.v1"` + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + Label string `protobuf:"bytes,2,opt,name=label,proto3" json:"label,omitempty"` + AccountCost float64 `protobuf:"fixed64,3,opt,name=account_cost,json=accountCost,proto3" json:"account_cost,omitempty"` + UserCost float64 `protobuf:"fixed64,4,opt,name=user_cost,json=userCost,proto3" json:"user_cost,omitempty"` + BillingMultiplier float64 `protobuf:"fixed64,5,opt,name=billing_multiplier,json=billingMultiplier,proto3" json:"billing_multiplier,omitempty"` + Currency string `protobuf:"bytes,6,opt,name=currency,proto3" json:"currency,omitempty"` + Metadata map[string]string `protobuf:"bytes,7,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UsageCostDetail) Reset() { + *x = UsageCostDetail{} + mi := &file_plugin_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *HostGetAssetBytesResponse) String() string { +func (x *UsageCostDetail) String() string { return protoimpl.X.MessageStringOf(x) } -func (*HostGetAssetBytesResponse) ProtoMessage() {} +func (*UsageCostDetail) ProtoMessage() {} -func (x *HostGetAssetBytesResponse) ProtoReflect() protoreflect.Message { - mi := &file_plugin_proto_msgTypes[24] +func (x *UsageCostDetail) ProtoReflect() protoreflect.Message { + mi := &file_plugin_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1667,83 +1608,93 @@ func (x *HostGetAssetBytesResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use HostGetAssetBytesResponse.ProtoReflect.Descriptor instead. -func (*HostGetAssetBytesResponse) Descriptor() ([]byte, []int) { - return file_plugin_proto_rawDescGZIP(), []int{24} +// Deprecated: Use UsageCostDetail.ProtoReflect.Descriptor instead. +func (*UsageCostDetail) Descriptor() ([]byte, []int) { + return file_plugin_proto_rawDescGZIP(), []int{19} } -func (x *HostGetAssetBytesResponse) GetData() []byte { +func (x *UsageCostDetail) GetKey() string { if x != nil { - return x.Data + return x.Key } - return nil + return "" } -func (x *HostGetAssetBytesResponse) GetContentType() string { +func (x *UsageCostDetail) GetLabel() string { if x != nil { - return x.ContentType + return x.Label } return "" } -type Empty struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache +func (x *UsageCostDetail) GetAccountCost() float64 { + if x != nil { + return x.AccountCost + } + return 0 } -func (x *Empty) Reset() { - *x = Empty{} - mi := &file_plugin_proto_msgTypes[25] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) +func (x *UsageCostDetail) GetUserCost() float64 { + if x != nil { + return x.UserCost + } + return 0 } -func (x *Empty) String() string { - return protoimpl.X.MessageStringOf(x) +func (x *UsageCostDetail) GetBillingMultiplier() float64 { + if x != nil { + return x.BillingMultiplier + } + return 0 } -func (*Empty) ProtoMessage() {} - -func (x *Empty) ProtoReflect() protoreflect.Message { - mi := &file_plugin_proto_msgTypes[25] +func (x *UsageCostDetail) GetCurrency() string { if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms + return x.Currency } - return mi.MessageOf(x) + return "" } -// Deprecated: Use Empty.ProtoReflect.Descriptor instead. -func (*Empty) Descriptor() ([]byte, []int) { - return file_plugin_proto_rawDescGZIP(), []int{25} +func (x *UsageCostDetail) GetMetadata() map[string]string { + if x != nil { + return x.Metadata + } + return nil } -type StringResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Value string `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache +// Usage 单次调用的用量与费用结果。非 Success 判决下应为空。 +type Usage struct { + state protoimpl.MessageState `protogen:"open.v1"` + Model string `protobuf:"bytes,1,opt,name=model,proto3" json:"model,omitempty"` + AccountCost float64 `protobuf:"fixed64,2,opt,name=account_cost,json=accountCost,proto3" json:"account_cost,omitempty"` + UserCost float64 `protobuf:"fixed64,3,opt,name=user_cost,json=userCost,proto3" json:"user_cost,omitempty"` + BillingMultiplier float64 `protobuf:"fixed64,4,opt,name=billing_multiplier,json=billingMultiplier,proto3" json:"billing_multiplier,omitempty"` + Currency string `protobuf:"bytes,5,opt,name=currency,proto3" json:"currency,omitempty"` + Summary string `protobuf:"bytes,6,opt,name=summary,proto3" json:"summary,omitempty"` + FirstTokenMs int64 `protobuf:"varint,7,opt,name=first_token_ms,json=firstTokenMs,proto3" json:"first_token_ms,omitempty"` + Metrics []*UsageMetric `protobuf:"bytes,8,rep,name=metrics,proto3" json:"metrics,omitempty"` + Attributes []*UsageAttribute `protobuf:"bytes,9,rep,name=attributes,proto3" json:"attributes,omitempty"` + CostDetails []*UsageCostDetail `protobuf:"bytes,10,rep,name=cost_details,json=costDetails,proto3" json:"cost_details,omitempty"` + Metadata map[string]string `protobuf:"bytes,11,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *StringResponse) Reset() { - *x = StringResponse{} - mi := &file_plugin_proto_msgTypes[26] +func (x *Usage) Reset() { + *x = Usage{} + mi := &file_plugin_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *StringResponse) String() string { +func (x *Usage) String() string { return protoimpl.X.MessageStringOf(x) } -func (*StringResponse) ProtoMessage() {} +func (*Usage) ProtoMessage() {} -func (x *StringResponse) ProtoReflect() protoreflect.Message { - mi := &file_plugin_proto_msgTypes[26] +func (x *Usage) ProtoReflect() protoreflect.Message { + mi := &file_plugin_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1754,104 +1705,117 @@ func (x *StringResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use StringResponse.ProtoReflect.Descriptor instead. -func (*StringResponse) Descriptor() ([]byte, []int) { - return file_plugin_proto_rawDescGZIP(), []int{26} +// Deprecated: Use Usage.ProtoReflect.Descriptor instead. +func (*Usage) Descriptor() ([]byte, []int) { + return file_plugin_proto_rawDescGZIP(), []int{20} } -func (x *StringResponse) GetValue() string { +func (x *Usage) GetModel() string { if x != nil { - return x.Value + return x.Model } return "" } -// HeaderValues 支持同一 Header 有多个值(如 Set-Cookie) -type HeaderValues struct { - state protoimpl.MessageState `protogen:"open.v1"` - Values []string `protobuf:"bytes,1,rep,name=values,proto3" json:"values,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache +func (x *Usage) GetAccountCost() float64 { + if x != nil { + return x.AccountCost + } + return 0 } -func (x *HeaderValues) Reset() { - *x = HeaderValues{} - mi := &file_plugin_proto_msgTypes[27] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) +func (x *Usage) GetUserCost() float64 { + if x != nil { + return x.UserCost + } + return 0 } -func (x *HeaderValues) String() string { - return protoimpl.X.MessageStringOf(x) +func (x *Usage) GetBillingMultiplier() float64 { + if x != nil { + return x.BillingMultiplier + } + return 0 } -func (*HeaderValues) ProtoMessage() {} +func (x *Usage) GetCurrency() string { + if x != nil { + return x.Currency + } + return "" +} -func (x *HeaderValues) ProtoReflect() protoreflect.Message { - mi := &file_plugin_proto_msgTypes[27] +func (x *Usage) GetSummary() string { if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms + return x.Summary } - return mi.MessageOf(x) + return "" } -// Deprecated: Use HeaderValues.ProtoReflect.Descriptor instead. -func (*HeaderValues) Descriptor() ([]byte, []int) { - return file_plugin_proto_rawDescGZIP(), []int{27} +func (x *Usage) GetFirstTokenMs() int64 { + if x != nil { + return x.FirstTokenMs + } + return 0 } -func (x *HeaderValues) GetValues() []string { +func (x *Usage) GetMetrics() []*UsageMetric { if x != nil { - return x.Values + return x.Metrics } return nil } -type PluginInfoResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` - Version string `protobuf:"bytes,3,opt,name=version,proto3" json:"version,omitempty"` - Description string `protobuf:"bytes,4,opt,name=description,proto3" json:"description,omitempty"` - Author string `protobuf:"bytes,5,opt,name=author,proto3" json:"author,omitempty"` - Type string `protobuf:"bytes,6,opt,name=type,proto3" json:"type,omitempty"` - AccountTypes []*AccountTypeProto `protobuf:"bytes,7,rep,name=account_types,json=accountTypes,proto3" json:"account_types,omitempty"` - FrontendPages []*FrontendPageProto `protobuf:"bytes,8,rep,name=frontend_pages,json=frontendPages,proto3" json:"frontend_pages,omitempty"` - FrontendWidgets []*FrontendWidgetProto `protobuf:"bytes,9,rep,name=frontend_widgets,json=frontendWidgets,proto3" json:"frontend_widgets,omitempty"` - SdkVersion string `protobuf:"bytes,10,opt,name=sdk_version,json=sdkVersion,proto3" json:"sdk_version,omitempty"` // 插件编译时的 SDK 版本 - Dependencies []string `protobuf:"bytes,11,rep,name=dependencies,proto3" json:"dependencies,omitempty"` // 依赖的其他插件 ID - ConfigSchema []*ConfigFieldProto `protobuf:"bytes,12,rep,name=config_schema,json=configSchema,proto3" json:"config_schema,omitempty"` // 配置项声明 - InstructionPresets []string `protobuf:"bytes,13,rep,name=instruction_presets,json=instructionPresets,proto3" json:"instruction_presets,omitempty"` // 可用的 instructions 预设名称列表 - // capabilities 声明的 HostService / Middleware 能力。Core 启动时按 - // "插件类型 → 允许集合" 做交集,gRPC interceptor 按此 set 做准入校验。 - // SDK 版本 <= 0.2.x 的插件豁免(兼容存量),0.3.x 起强制。详见 ADR-0001 Decision 4。 - Capabilities []string `protobuf:"bytes,14,rep,name=capabilities,proto3" json:"capabilities,omitempty"` - // priority 中间件链中的排序权重(数值越小越早进 Begin、越晚出 End)。 - // 仅 type="middleware" 插件使用;其他类型忽略。默认 100。 - Priority int32 `protobuf:"varint,15,opt,name=priority,proto3" json:"priority,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache +func (x *Usage) GetAttributes() []*UsageAttribute { + if x != nil { + return x.Attributes + } + return nil } -func (x *PluginInfoResponse) Reset() { - *x = PluginInfoResponse{} - mi := &file_plugin_proto_msgTypes[28] +func (x *Usage) GetCostDetails() []*UsageCostDetail { + if x != nil { + return x.CostDetails + } + return nil +} + +func (x *Usage) GetMetadata() map[string]string { + if x != nil { + return x.Metadata + } + return nil +} + +// ForwardOutcome 插件对一次 Forward 的完整判决结果。 +type ForwardOutcome struct { + state protoimpl.MessageState `protogen:"open.v1"` + Kind OutcomeKind `protobuf:"varint,1,opt,name=kind,proto3,enum=airgate.plugin.v1.OutcomeKind" json:"kind,omitempty"` + Upstream *UpstreamResponse `protobuf:"bytes,2,opt,name=upstream,proto3" json:"upstream,omitempty"` + Usage *Usage `protobuf:"bytes,3,opt,name=usage,proto3" json:"usage,omitempty"` + DurationMs int64 `protobuf:"varint,4,opt,name=duration_ms,json=durationMs,proto3" json:"duration_ms,omitempty"` + RetryAfterMs int64 `protobuf:"varint,5,opt,name=retry_after_ms,json=retryAfterMs,proto3" json:"retry_after_ms,omitempty"` + Reason string `protobuf:"bytes,6,opt,name=reason,proto3" json:"reason,omitempty"` + UpdatedCredentials map[string]string `protobuf:"bytes,7,rep,name=updated_credentials,json=updatedCredentials,proto3" json:"updated_credentials,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ForwardOutcome) Reset() { + *x = ForwardOutcome{} + mi := &file_plugin_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *PluginInfoResponse) String() string { +func (x *ForwardOutcome) String() string { return protoimpl.X.MessageStringOf(x) } -func (*PluginInfoResponse) ProtoMessage() {} +func (*ForwardOutcome) ProtoMessage() {} -func (x *PluginInfoResponse) ProtoReflect() protoreflect.Message { - mi := &file_plugin_proto_msgTypes[28] +func (x *ForwardOutcome) ProtoReflect() protoreflect.Message { + mi := &file_plugin_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1862,144 +1826,207 @@ func (x *PluginInfoResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use PluginInfoResponse.ProtoReflect.Descriptor instead. -func (*PluginInfoResponse) Descriptor() ([]byte, []int) { - return file_plugin_proto_rawDescGZIP(), []int{28} +// Deprecated: Use ForwardOutcome.ProtoReflect.Descriptor instead. +func (*ForwardOutcome) Descriptor() ([]byte, []int) { + return file_plugin_proto_rawDescGZIP(), []int{21} } -func (x *PluginInfoResponse) GetId() string { +func (x *ForwardOutcome) GetKind() OutcomeKind { if x != nil { - return x.Id + return x.Kind } - return "" + return OutcomeKind_OUTCOME_UNKNOWN } -func (x *PluginInfoResponse) GetName() string { +func (x *ForwardOutcome) GetUpstream() *UpstreamResponse { if x != nil { - return x.Name + return x.Upstream } - return "" + return nil } -func (x *PluginInfoResponse) GetVersion() string { +func (x *ForwardOutcome) GetUsage() *Usage { if x != nil { - return x.Version + return x.Usage } - return "" + return nil } -func (x *PluginInfoResponse) GetDescription() string { +func (x *ForwardOutcome) GetDurationMs() int64 { if x != nil { - return x.Description + return x.DurationMs } - return "" + return 0 } -func (x *PluginInfoResponse) GetAuthor() string { +func (x *ForwardOutcome) GetRetryAfterMs() int64 { if x != nil { - return x.Author + return x.RetryAfterMs } - return "" + return 0 } -func (x *PluginInfoResponse) GetType() string { +func (x *ForwardOutcome) GetReason() string { if x != nil { - return x.Type + return x.Reason } return "" } -func (x *PluginInfoResponse) GetAccountTypes() []*AccountTypeProto { +func (x *ForwardOutcome) GetUpdatedCredentials() map[string]string { if x != nil { - return x.AccountTypes + return x.UpdatedCredentials } return nil } -func (x *PluginInfoResponse) GetFrontendPages() []*FrontendPageProto { +type ForwardChunk struct { + state protoimpl.MessageState `protogen:"open.v1"` + Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` + Done bool `protobuf:"varint,2,opt,name=done,proto3" json:"done,omitempty"` + FinalOutcome *ForwardOutcome `protobuf:"bytes,3,opt,name=final_outcome,json=finalOutcome,proto3" json:"final_outcome,omitempty"` + StatusCode int32 `protobuf:"varint,4,opt,name=status_code,json=statusCode,proto3" json:"status_code,omitempty"` + Headers map[string]*HeaderValues `protobuf:"bytes,5,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ForwardChunk) Reset() { + *x = ForwardChunk{} + mi := &file_plugin_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ForwardChunk) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ForwardChunk) ProtoMessage() {} + +func (x *ForwardChunk) ProtoReflect() protoreflect.Message { + mi := &file_plugin_proto_msgTypes[22] if x != nil { - return x.FrontendPages + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms } - return nil + return mi.MessageOf(x) } -func (x *PluginInfoResponse) GetFrontendWidgets() []*FrontendWidgetProto { +// Deprecated: Use ForwardChunk.ProtoReflect.Descriptor instead. +func (*ForwardChunk) Descriptor() ([]byte, []int) { + return file_plugin_proto_rawDescGZIP(), []int{22} +} + +func (x *ForwardChunk) GetData() []byte { if x != nil { - return x.FrontendWidgets + return x.Data } return nil } -func (x *PluginInfoResponse) GetSdkVersion() string { +func (x *ForwardChunk) GetDone() bool { if x != nil { - return x.SdkVersion + return x.Done } - return "" + return false } -func (x *PluginInfoResponse) GetDependencies() []string { +func (x *ForwardChunk) GetFinalOutcome() *ForwardOutcome { if x != nil { - return x.Dependencies + return x.FinalOutcome } return nil } -func (x *PluginInfoResponse) GetConfigSchema() []*ConfigFieldProto { +func (x *ForwardChunk) GetStatusCode() int32 { if x != nil { - return x.ConfigSchema + return x.StatusCode } - return nil + return 0 } -func (x *PluginInfoResponse) GetInstructionPresets() []string { +func (x *ForwardChunk) GetHeaders() map[string]*HeaderValues { if x != nil { - return x.InstructionPresets + return x.Headers } return nil } -func (x *PluginInfoResponse) GetCapabilities() []string { +type CredentialsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Credentials map[string]string `protobuf:"bytes,1,rep,name=credentials,proto3" json:"credentials,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CredentialsRequest) Reset() { + *x = CredentialsRequest{} + mi := &file_plugin_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CredentialsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CredentialsRequest) ProtoMessage() {} + +func (x *CredentialsRequest) ProtoReflect() protoreflect.Message { + mi := &file_plugin_proto_msgTypes[23] if x != nil { - return x.Capabilities + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms } - return nil + return mi.MessageOf(x) } -func (x *PluginInfoResponse) GetPriority() int32 { +// Deprecated: Use CredentialsRequest.ProtoReflect.Descriptor instead. +func (*CredentialsRequest) Descriptor() ([]byte, []int) { + return file_plugin_proto_rawDescGZIP(), []int{23} +} + +func (x *CredentialsRequest) GetCredentials() map[string]string { if x != nil { - return x.Priority + return x.Credentials } - return 0 + return nil } -type ConfigFieldProto struct { - state protoimpl.MessageState `protogen:"open.v1"` - Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` - Label string `protobuf:"bytes,2,opt,name=label,proto3" json:"label,omitempty"` - Type string `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"` - Required bool `protobuf:"varint,4,opt,name=required,proto3" json:"required,omitempty"` - DefaultValue string `protobuf:"bytes,5,opt,name=default_value,json=defaultValue,proto3" json:"default_value,omitempty"` - Description string `protobuf:"bytes,6,opt,name=description,proto3" json:"description,omitempty"` - Placeholder string `protobuf:"bytes,7,opt,name=placeholder,proto3" json:"placeholder,omitempty"` +type HttpRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Method string `protobuf:"bytes,1,opt,name=method,proto3" json:"method,omitempty"` + Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` + Query string `protobuf:"bytes,3,opt,name=query,proto3" json:"query,omitempty"` + Headers map[string]*HeaderValues `protobuf:"bytes,4,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + Body []byte `protobuf:"bytes,5,opt,name=body,proto3" json:"body,omitempty"` + RemoteAddr string `protobuf:"bytes,6,opt,name=remote_addr,json=remoteAddr,proto3" json:"remote_addr,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *ConfigFieldProto) Reset() { - *x = ConfigFieldProto{} - mi := &file_plugin_proto_msgTypes[29] +func (x *HttpRequest) Reset() { + *x = HttpRequest{} + mi := &file_plugin_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ConfigFieldProto) String() string { +func (x *HttpRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ConfigFieldProto) ProtoMessage() {} +func (*HttpRequest) ProtoMessage() {} -func (x *ConfigFieldProto) ProtoReflect() protoreflect.Message { - mi := &file_plugin_proto_msgTypes[29] +func (x *HttpRequest) ProtoReflect() protoreflect.Message { + mi := &file_plugin_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2010,85 +2037,77 @@ func (x *ConfigFieldProto) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ConfigFieldProto.ProtoReflect.Descriptor instead. -func (*ConfigFieldProto) Descriptor() ([]byte, []int) { - return file_plugin_proto_rawDescGZIP(), []int{29} +// Deprecated: Use HttpRequest.ProtoReflect.Descriptor instead. +func (*HttpRequest) Descriptor() ([]byte, []int) { + return file_plugin_proto_rawDescGZIP(), []int{24} } -func (x *ConfigFieldProto) GetKey() string { +func (x *HttpRequest) GetMethod() string { if x != nil { - return x.Key + return x.Method } return "" } -func (x *ConfigFieldProto) GetLabel() string { +func (x *HttpRequest) GetPath() string { if x != nil { - return x.Label + return x.Path } return "" } -func (x *ConfigFieldProto) GetType() string { +func (x *HttpRequest) GetQuery() string { if x != nil { - return x.Type + return x.Query } return "" } -func (x *ConfigFieldProto) GetRequired() bool { +func (x *HttpRequest) GetHeaders() map[string]*HeaderValues { if x != nil { - return x.Required - } - return false -} - -func (x *ConfigFieldProto) GetDefaultValue() string { - if x != nil { - return x.DefaultValue + return x.Headers } - return "" + return nil } -func (x *ConfigFieldProto) GetDescription() string { +func (x *HttpRequest) GetBody() []byte { if x != nil { - return x.Description + return x.Body } - return "" + return nil } -func (x *ConfigFieldProto) GetPlaceholder() string { +func (x *HttpRequest) GetRemoteAddr() string { if x != nil { - return x.Placeholder + return x.RemoteAddr } return "" } -type AccountTypeProto struct { - state protoimpl.MessageState `protogen:"open.v1"` - Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` - Label string `protobuf:"bytes,2,opt,name=label,proto3" json:"label,omitempty"` - Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` - Fields []*CredentialFieldProto `protobuf:"bytes,4,rep,name=fields,proto3" json:"fields,omitempty"` +type HttpResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + StatusCode int32 `protobuf:"varint,1,opt,name=status_code,json=statusCode,proto3" json:"status_code,omitempty"` + Headers map[string]*HeaderValues `protobuf:"bytes,2,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + Body []byte `protobuf:"bytes,3,opt,name=body,proto3" json:"body,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *AccountTypeProto) Reset() { - *x = AccountTypeProto{} - mi := &file_plugin_proto_msgTypes[30] +func (x *HttpResponse) Reset() { + *x = HttpResponse{} + mi := &file_plugin_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *AccountTypeProto) String() string { +func (x *HttpResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*AccountTypeProto) ProtoMessage() {} +func (*HttpResponse) ProtoMessage() {} -func (x *AccountTypeProto) ProtoReflect() protoreflect.Message { - mi := &file_plugin_proto_msgTypes[30] +func (x *HttpResponse) ProtoReflect() protoreflect.Message { + mi := &file_plugin_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2099,66 +2118,57 @@ func (x *AccountTypeProto) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use AccountTypeProto.ProtoReflect.Descriptor instead. -func (*AccountTypeProto) Descriptor() ([]byte, []int) { - return file_plugin_proto_rawDescGZIP(), []int{30} -} - -func (x *AccountTypeProto) GetKey() string { - if x != nil { - return x.Key - } - return "" +// Deprecated: Use HttpResponse.ProtoReflect.Descriptor instead. +func (*HttpResponse) Descriptor() ([]byte, []int) { + return file_plugin_proto_rawDescGZIP(), []int{25} } -func (x *AccountTypeProto) GetLabel() string { +func (x *HttpResponse) GetStatusCode() int32 { if x != nil { - return x.Label + return x.StatusCode } - return "" + return 0 } -func (x *AccountTypeProto) GetDescription() string { +func (x *HttpResponse) GetHeaders() map[string]*HeaderValues { if x != nil { - return x.Description + return x.Headers } - return "" + return nil } -func (x *AccountTypeProto) GetFields() []*CredentialFieldProto { +func (x *HttpResponse) GetBody() []byte { if x != nil { - return x.Fields + return x.Body } return nil } -type CredentialFieldProto struct { - state protoimpl.MessageState `protogen:"open.v1"` - Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` - Label string `protobuf:"bytes,2,opt,name=label,proto3" json:"label,omitempty"` - Type string `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"` - Required bool `protobuf:"varint,4,opt,name=required,proto3" json:"required,omitempty"` - Placeholder string `protobuf:"bytes,5,opt,name=placeholder,proto3" json:"placeholder,omitempty"` - EditDisabled bool `protobuf:"varint,6,opt,name=edit_disabled,json=editDisabled,proto3" json:"edit_disabled,omitempty"` // 编辑模式下隐藏该字段 +type HttpResponseChunk struct { + state protoimpl.MessageState `protogen:"open.v1"` + Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` + Done bool `protobuf:"varint,2,opt,name=done,proto3" json:"done,omitempty"` + StatusCode int32 `protobuf:"varint,3,opt,name=status_code,json=statusCode,proto3" json:"status_code,omitempty"` + Headers map[string]*HeaderValues `protobuf:"bytes,4,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *CredentialFieldProto) Reset() { - *x = CredentialFieldProto{} - mi := &file_plugin_proto_msgTypes[31] +func (x *HttpResponseChunk) Reset() { + *x = HttpResponseChunk{} + mi := &file_plugin_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *CredentialFieldProto) String() string { +func (x *HttpResponseChunk) String() string { return protoimpl.X.MessageStringOf(x) } -func (*CredentialFieldProto) ProtoMessage() {} +func (*HttpResponseChunk) ProtoMessage() {} -func (x *CredentialFieldProto) ProtoReflect() protoreflect.Message { - mi := &file_plugin_proto_msgTypes[31] +func (x *HttpResponseChunk) ProtoReflect() protoreflect.Message { + mi := &file_plugin_proto_msgTypes[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2169,79 +2179,62 @@ func (x *CredentialFieldProto) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use CredentialFieldProto.ProtoReflect.Descriptor instead. -func (*CredentialFieldProto) Descriptor() ([]byte, []int) { - return file_plugin_proto_rawDescGZIP(), []int{31} -} - -func (x *CredentialFieldProto) GetKey() string { - if x != nil { - return x.Key - } - return "" -} - -func (x *CredentialFieldProto) GetLabel() string { - if x != nil { - return x.Label - } - return "" +// Deprecated: Use HttpResponseChunk.ProtoReflect.Descriptor instead. +func (*HttpResponseChunk) Descriptor() ([]byte, []int) { + return file_plugin_proto_rawDescGZIP(), []int{26} } -func (x *CredentialFieldProto) GetType() string { +func (x *HttpResponseChunk) GetData() []byte { if x != nil { - return x.Type + return x.Data } - return "" + return nil } -func (x *CredentialFieldProto) GetRequired() bool { +func (x *HttpResponseChunk) GetDone() bool { if x != nil { - return x.Required + return x.Done } return false } -func (x *CredentialFieldProto) GetPlaceholder() string { +func (x *HttpResponseChunk) GetStatusCode() int32 { if x != nil { - return x.Placeholder + return x.StatusCode } - return "" + return 0 } -func (x *CredentialFieldProto) GetEditDisabled() bool { +func (x *HttpResponseChunk) GetHeaders() map[string]*HeaderValues { if x != nil { - return x.EditDisabled + return x.Headers } - return false + return nil } -type FrontendPageProto struct { +type BackgroundTaskProto struct { state protoimpl.MessageState `protogen:"open.v1"` - Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` - Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"` - Icon string `protobuf:"bytes,3,opt,name=icon,proto3" json:"icon,omitempty"` - Description string `protobuf:"bytes,4,opt,name=description,proto3" json:"description,omitempty"` - Audience string `protobuf:"bytes,5,opt,name=audience,proto3" json:"audience,omitempty"` // "admin" | "user" | "all",空 = "admin" + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + IntervalMs int64 `protobuf:"varint,2,opt,name=interval_ms,json=intervalMs,proto3" json:"interval_ms,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *FrontendPageProto) Reset() { - *x = FrontendPageProto{} - mi := &file_plugin_proto_msgTypes[32] +func (x *BackgroundTaskProto) Reset() { + *x = BackgroundTaskProto{} + mi := &file_plugin_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *FrontendPageProto) String() string { +func (x *BackgroundTaskProto) String() string { return protoimpl.X.MessageStringOf(x) } -func (*FrontendPageProto) ProtoMessage() {} +func (*BackgroundTaskProto) ProtoMessage() {} -func (x *FrontendPageProto) ProtoReflect() protoreflect.Message { - mi := &file_plugin_proto_msgTypes[32] +func (x *BackgroundTaskProto) ProtoReflect() protoreflect.Message { + mi := &file_plugin_proto_msgTypes[27] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2252,70 +2245,91 @@ func (x *FrontendPageProto) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use FrontendPageProto.ProtoReflect.Descriptor instead. -func (*FrontendPageProto) Descriptor() ([]byte, []int) { - return file_plugin_proto_rawDescGZIP(), []int{32} +// Deprecated: Use BackgroundTaskProto.ProtoReflect.Descriptor instead. +func (*BackgroundTaskProto) Descriptor() ([]byte, []int) { + return file_plugin_proto_rawDescGZIP(), []int{27} } -func (x *FrontendPageProto) GetPath() string { +func (x *BackgroundTaskProto) GetName() string { if x != nil { - return x.Path + return x.Name } return "" } -func (x *FrontendPageProto) GetTitle() string { +func (x *BackgroundTaskProto) GetIntervalMs() int64 { if x != nil { - return x.Title + return x.IntervalMs } - return "" + return 0 } -func (x *FrontendPageProto) GetIcon() string { - if x != nil { - return x.Icon - } - return "" +type BackgroundTasksResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Tasks []*BackgroundTaskProto `protobuf:"bytes,1,rep,name=tasks,proto3" json:"tasks,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *FrontendPageProto) GetDescription() string { +func (x *BackgroundTasksResponse) Reset() { + *x = BackgroundTasksResponse{} + mi := &file_plugin_proto_msgTypes[28] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BackgroundTasksResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BackgroundTasksResponse) ProtoMessage() {} + +func (x *BackgroundTasksResponse) ProtoReflect() protoreflect.Message { + mi := &file_plugin_proto_msgTypes[28] if x != nil { - return x.Description + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms } - return "" + return mi.MessageOf(x) } -func (x *FrontendPageProto) GetAudience() string { +// Deprecated: Use BackgroundTasksResponse.ProtoReflect.Descriptor instead. +func (*BackgroundTasksResponse) Descriptor() ([]byte, []int) { + return file_plugin_proto_rawDescGZIP(), []int{28} +} + +func (x *BackgroundTasksResponse) GetTasks() []*BackgroundTaskProto { if x != nil { - return x.Audience + return x.Tasks } - return "" + return nil } -type FrontendWidgetProto struct { +type RunBackgroundTaskRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - Slot string `protobuf:"bytes,1,opt,name=slot,proto3" json:"slot,omitempty"` - EntryFile string `protobuf:"bytes,2,opt,name=entry_file,json=entryFile,proto3" json:"entry_file,omitempty"` - Title string `protobuf:"bytes,3,opt,name=title,proto3" json:"title,omitempty"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *FrontendWidgetProto) Reset() { - *x = FrontendWidgetProto{} - mi := &file_plugin_proto_msgTypes[33] +func (x *RunBackgroundTaskRequest) Reset() { + *x = RunBackgroundTaskRequest{} + mi := &file_plugin_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *FrontendWidgetProto) String() string { +func (x *RunBackgroundTaskRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*FrontendWidgetProto) ProtoMessage() {} +func (*RunBackgroundTaskRequest) ProtoMessage() {} -func (x *FrontendWidgetProto) ProtoReflect() protoreflect.Message { - mi := &file_plugin_proto_msgTypes[33] +func (x *RunBackgroundTaskRequest) ProtoReflect() protoreflect.Message { + mi := &file_plugin_proto_msgTypes[29] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2326,60 +2340,48 @@ func (x *FrontendWidgetProto) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use FrontendWidgetProto.ProtoReflect.Descriptor instead. -func (*FrontendWidgetProto) Descriptor() ([]byte, []int) { - return file_plugin_proto_rawDescGZIP(), []int{33} +// Deprecated: Use RunBackgroundTaskRequest.ProtoReflect.Descriptor instead. +func (*RunBackgroundTaskRequest) Descriptor() ([]byte, []int) { + return file_plugin_proto_rawDescGZIP(), []int{29} } -func (x *FrontendWidgetProto) GetSlot() string { +func (x *RunBackgroundTaskRequest) GetName() string { if x != nil { - return x.Slot - } - return "" -} - -func (x *FrontendWidgetProto) GetEntryFile() string { - if x != nil { - return x.EntryFile - } - return "" -} - -func (x *FrontendWidgetProto) GetTitle() string { - if x != nil { - return x.Title + return x.Name } return "" } -type InitRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Config map[string]string `protobuf:"bytes,1,rep,name=config,proto3" json:"config,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - LogLevel string `protobuf:"bytes,2,opt,name=log_level,json=logLevel,proto3" json:"log_level,omitempty"` - // host_broker_id 是 Core 通过 hashicorp/go-plugin GRPCBroker 启动的 - // HostService stream 的 ID。插件 Init 时拿到 ID 后,可以通过 broker.Dial(id) - // 拿到 HostService 的 grpc client,回调 Core 提供的能力(SelectAccount / - // ProbeForward / ListGroups 等)。0 表示 Core 没启用 HostService。 - HostBrokerId uint32 `protobuf:"varint,3,opt,name=host_broker_id,json=hostBrokerId,proto3" json:"host_broker_id,omitempty"` +type WebSocketFrame struct { + state protoimpl.MessageState `protogen:"open.v1"` + Type WebSocketFrame_FrameType `protobuf:"varint,1,opt,name=type,proto3,enum=airgate.plugin.v1.WebSocketFrame_FrameType" json:"type,omitempty"` + Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` + // 仅 CONNECT 帧使用 + ConnectInfo *WebSocketConnectInfo `protobuf:"bytes,3,opt,name=connect_info,json=connectInfo,proto3" json:"connect_info,omitempty"` + // 仅 CLOSE 帧使用 + CloseCode int32 `protobuf:"varint,4,opt,name=close_code,json=closeCode,proto3" json:"close_code,omitempty"` + CloseReason string `protobuf:"bytes,5,opt,name=close_reason,json=closeReason,proto3" json:"close_reason,omitempty"` + // 仅 RESULT 帧使用 + Outcome *ForwardOutcome `protobuf:"bytes,6,opt,name=outcome,proto3" json:"outcome,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *InitRequest) Reset() { - *x = InitRequest{} - mi := &file_plugin_proto_msgTypes[34] +func (x *WebSocketFrame) Reset() { + *x = WebSocketFrame{} + mi := &file_plugin_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *InitRequest) String() string { +func (x *WebSocketFrame) String() string { return protoimpl.X.MessageStringOf(x) } -func (*InitRequest) ProtoMessage() {} +func (*WebSocketFrame) ProtoMessage() {} -func (x *InitRequest) ProtoReflect() protoreflect.Message { - mi := &file_plugin_proto_msgTypes[34] +func (x *WebSocketFrame) ProtoReflect() protoreflect.Message { + mi := &file_plugin_proto_msgTypes[30] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2390,65 +2392,80 @@ func (x *InitRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use InitRequest.ProtoReflect.Descriptor instead. -func (*InitRequest) Descriptor() ([]byte, []int) { - return file_plugin_proto_rawDescGZIP(), []int{34} +// Deprecated: Use WebSocketFrame.ProtoReflect.Descriptor instead. +func (*WebSocketFrame) Descriptor() ([]byte, []int) { + return file_plugin_proto_rawDescGZIP(), []int{30} } -func (x *InitRequest) GetConfig() map[string]string { +func (x *WebSocketFrame) GetType() WebSocketFrame_FrameType { if x != nil { - return x.Config + return x.Type + } + return WebSocketFrame_CONNECT +} + +func (x *WebSocketFrame) GetData() []byte { + if x != nil { + return x.Data } return nil } -func (x *InitRequest) GetLogLevel() string { +func (x *WebSocketFrame) GetConnectInfo() *WebSocketConnectInfo { if x != nil { - return x.LogLevel + return x.ConnectInfo } - return "" + return nil } -func (x *InitRequest) GetHostBrokerId() uint32 { +func (x *WebSocketFrame) GetCloseCode() int32 { if x != nil { - return x.HostBrokerId + return x.CloseCode } return 0 } -type ModelInfoProto struct { - state protoimpl.MessageState `protogen:"open.v1"` - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` - InputPrice float64 `protobuf:"fixed64,4,opt,name=input_price,json=inputPrice,proto3" json:"input_price,omitempty"` - OutputPrice float64 `protobuf:"fixed64,5,opt,name=output_price,json=outputPrice,proto3" json:"output_price,omitempty"` - CachedInputPrice float64 `protobuf:"fixed64,6,opt,name=cached_input_price,json=cachedInputPrice,proto3" json:"cached_input_price,omitempty"` - InputPricePriority float64 `protobuf:"fixed64,7,opt,name=input_price_priority,json=inputPricePriority,proto3" json:"input_price_priority,omitempty"` - OutputPricePriority float64 `protobuf:"fixed64,8,opt,name=output_price_priority,json=outputPricePriority,proto3" json:"output_price_priority,omitempty"` - CachedInputPricePriority float64 `protobuf:"fixed64,9,opt,name=cached_input_price_priority,json=cachedInputPricePriority,proto3" json:"cached_input_price_priority,omitempty"` - ContextWindow int64 `protobuf:"varint,10,opt,name=context_window,json=contextWindow,proto3" json:"context_window,omitempty"` - MaxOutputTokens int64 `protobuf:"varint,11,opt,name=max_output_tokens,json=maxOutputTokens,proto3" json:"max_output_tokens,omitempty"` - CacheCreationPrice float64 `protobuf:"fixed64,12,opt,name=cache_creation_price,json=cacheCreationPrice,proto3" json:"cache_creation_price,omitempty"` // 缓存写入 5m TTL 单价($/1M token,1.25x input) - CacheCreation_1HPrice float64 `protobuf:"fixed64,13,opt,name=cache_creation_1h_price,json=cacheCreation1hPrice,proto3" json:"cache_creation_1h_price,omitempty"` // 缓存写入 1h TTL 单价($/1M token,2.00x input) - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache +func (x *WebSocketFrame) GetCloseReason() string { + if x != nil { + return x.CloseReason + } + return "" } -func (x *ModelInfoProto) Reset() { - *x = ModelInfoProto{} - mi := &file_plugin_proto_msgTypes[35] +func (x *WebSocketFrame) GetOutcome() *ForwardOutcome { + if x != nil { + return x.Outcome + } + return nil +} + +type WebSocketConnectInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` + Query string `protobuf:"bytes,2,opt,name=query,proto3" json:"query,omitempty"` + Headers map[string]*HeaderValues `protobuf:"bytes,3,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + RemoteAddr string `protobuf:"bytes,4,opt,name=remote_addr,json=remoteAddr,proto3" json:"remote_addr,omitempty"` + ConnectionId string `protobuf:"bytes,5,opt,name=connection_id,json=connectionId,proto3" json:"connection_id,omitempty"` + Account *AccountProto `protobuf:"bytes,12,opt,name=account,proto3" json:"account,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WebSocketConnectInfo) Reset() { + *x = WebSocketConnectInfo{} + mi := &file_plugin_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ModelInfoProto) String() string { +func (x *WebSocketConnectInfo) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ModelInfoProto) ProtoMessage() {} +func (*WebSocketConnectInfo) ProtoMessage() {} -func (x *ModelInfoProto) ProtoReflect() protoreflect.Message { - mi := &file_plugin_proto_msgTypes[35] +func (x *WebSocketConnectInfo) ProtoReflect() protoreflect.Message { + mi := &file_plugin_proto_msgTypes[31] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2459,117 +2476,128 @@ func (x *ModelInfoProto) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ModelInfoProto.ProtoReflect.Descriptor instead. -func (*ModelInfoProto) Descriptor() ([]byte, []int) { - return file_plugin_proto_rawDescGZIP(), []int{35} +// Deprecated: Use WebSocketConnectInfo.ProtoReflect.Descriptor instead. +func (*WebSocketConnectInfo) Descriptor() ([]byte, []int) { + return file_plugin_proto_rawDescGZIP(), []int{31} } -func (x *ModelInfoProto) GetId() string { +func (x *WebSocketConnectInfo) GetPath() string { if x != nil { - return x.Id + return x.Path } return "" } -func (x *ModelInfoProto) GetName() string { +func (x *WebSocketConnectInfo) GetQuery() string { if x != nil { - return x.Name + return x.Query } return "" } -func (x *ModelInfoProto) GetInputPrice() float64 { +func (x *WebSocketConnectInfo) GetHeaders() map[string]*HeaderValues { if x != nil { - return x.InputPrice + return x.Headers } - return 0 + return nil } -func (x *ModelInfoProto) GetOutputPrice() float64 { +func (x *WebSocketConnectInfo) GetRemoteAddr() string { if x != nil { - return x.OutputPrice + return x.RemoteAddr } - return 0 + return "" } -func (x *ModelInfoProto) GetCachedInputPrice() float64 { +func (x *WebSocketConnectInfo) GetConnectionId() string { if x != nil { - return x.CachedInputPrice + return x.ConnectionId } - return 0 + return "" } -func (x *ModelInfoProto) GetInputPricePriority() float64 { +func (x *WebSocketConnectInfo) GetAccount() *AccountProto { if x != nil { - return x.InputPricePriority + return x.Account } - return 0 + return nil } -func (x *ModelInfoProto) GetOutputPricePriority() float64 { - if x != nil { - return x.OutputPricePriority - } - return 0 +type WebAssetFile struct { + state protoimpl.MessageState `protogen:"open.v1"` + Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` + Content []byte `protobuf:"bytes,2,opt,name=content,proto3" json:"content,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *ModelInfoProto) GetCachedInputPricePriority() float64 { - if x != nil { - return x.CachedInputPricePriority - } - return 0 +func (x *WebAssetFile) Reset() { + *x = WebAssetFile{} + mi := &file_plugin_proto_msgTypes[32] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } -func (x *ModelInfoProto) GetContextWindow() int64 { - if x != nil { - return x.ContextWindow - } - return 0 +func (x *WebAssetFile) String() string { + return protoimpl.X.MessageStringOf(x) } -func (x *ModelInfoProto) GetMaxOutputTokens() int64 { +func (*WebAssetFile) ProtoMessage() {} + +func (x *WebAssetFile) ProtoReflect() protoreflect.Message { + mi := &file_plugin_proto_msgTypes[32] if x != nil { - return x.MaxOutputTokens + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms } - return 0 + return mi.MessageOf(x) } -func (x *ModelInfoProto) GetCacheCreationPrice() float64 { +// Deprecated: Use WebAssetFile.ProtoReflect.Descriptor instead. +func (*WebAssetFile) Descriptor() ([]byte, []int) { + return file_plugin_proto_rawDescGZIP(), []int{32} +} + +func (x *WebAssetFile) GetPath() string { if x != nil { - return x.CacheCreationPrice + return x.Path } - return 0 + return "" } -func (x *ModelInfoProto) GetCacheCreation_1HPrice() float64 { +func (x *WebAssetFile) GetContent() []byte { if x != nil { - return x.CacheCreation_1HPrice + return x.Content } - return 0 + return nil } -type ModelsResponse struct { +type WebAssetsResponse struct { state protoimpl.MessageState `protogen:"open.v1"` - Models []*ModelInfoProto `protobuf:"bytes,1,rep,name=models,proto3" json:"models,omitempty"` + Files []*WebAssetFile `protobuf:"bytes,1,rep,name=files,proto3" json:"files,omitempty"` + HasAssets bool `protobuf:"varint,2,opt,name=has_assets,json=hasAssets,proto3" json:"has_assets,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *ModelsResponse) Reset() { - *x = ModelsResponse{} - mi := &file_plugin_proto_msgTypes[36] +func (x *WebAssetsResponse) Reset() { + *x = WebAssetsResponse{} + mi := &file_plugin_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ModelsResponse) String() string { +func (x *WebAssetsResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ModelsResponse) ProtoMessage() {} +func (*WebAssetsResponse) ProtoMessage() {} -func (x *ModelsResponse) ProtoReflect() protoreflect.Message { - mi := &file_plugin_proto_msgTypes[36] +func (x *WebAssetsResponse) ProtoReflect() protoreflect.Message { + mi := &file_plugin_proto_msgTypes[33] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2580,42 +2608,50 @@ func (x *ModelsResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ModelsResponse.ProtoReflect.Descriptor instead. -func (*ModelsResponse) Descriptor() ([]byte, []int) { - return file_plugin_proto_rawDescGZIP(), []int{36} +// Deprecated: Use WebAssetsResponse.ProtoReflect.Descriptor instead. +func (*WebAssetsResponse) Descriptor() ([]byte, []int) { + return file_plugin_proto_rawDescGZIP(), []int{33} } -func (x *ModelsResponse) GetModels() []*ModelInfoProto { +func (x *WebAssetsResponse) GetFiles() []*WebAssetFile { if x != nil { - return x.Models + return x.Files } return nil } -type RouteDefinitionProto struct { +func (x *WebAssetsResponse) GetHasAssets() bool { + if x != nil { + return x.HasAssets + } + return false +} + +type PayloadSchemaProto struct { state protoimpl.MessageState `protogen:"open.v1"` - Method string `protobuf:"bytes,1,opt,name=method,proto3" json:"method,omitempty"` - Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` - Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` + ContentType string `protobuf:"bytes,1,opt,name=content_type,json=contentType,proto3" json:"content_type,omitempty"` + Schema string `protobuf:"bytes,2,opt,name=schema,proto3" json:"schema,omitempty"` // JSON Schema 字符串 + Example string `protobuf:"bytes,3,opt,name=example,proto3" json:"example,omitempty"` // JSON 示例字符串 + Metadata map[string]string `protobuf:"bytes,4,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *RouteDefinitionProto) Reset() { - *x = RouteDefinitionProto{} - mi := &file_plugin_proto_msgTypes[37] +func (x *PayloadSchemaProto) Reset() { + *x = PayloadSchemaProto{} + mi := &file_plugin_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *RouteDefinitionProto) String() string { +func (x *PayloadSchemaProto) String() string { return protoimpl.X.MessageStringOf(x) } -func (*RouteDefinitionProto) ProtoMessage() {} +func (*PayloadSchemaProto) ProtoMessage() {} -func (x *RouteDefinitionProto) ProtoReflect() protoreflect.Message { - mi := &file_plugin_proto_msgTypes[37] +func (x *PayloadSchemaProto) ProtoReflect() protoreflect.Message { + mi := &file_plugin_proto_msgTypes[34] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2626,103 +2662,66 @@ func (x *RouteDefinitionProto) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use RouteDefinitionProto.ProtoReflect.Descriptor instead. -func (*RouteDefinitionProto) Descriptor() ([]byte, []int) { - return file_plugin_proto_rawDescGZIP(), []int{37} +// Deprecated: Use PayloadSchemaProto.ProtoReflect.Descriptor instead. +func (*PayloadSchemaProto) Descriptor() ([]byte, []int) { + return file_plugin_proto_rawDescGZIP(), []int{34} } -func (x *RouteDefinitionProto) GetMethod() string { +func (x *PayloadSchemaProto) GetContentType() string { if x != nil { - return x.Method + return x.ContentType } return "" } -func (x *RouteDefinitionProto) GetPath() string { +func (x *PayloadSchemaProto) GetSchema() string { if x != nil { - return x.Path + return x.Schema } return "" } -func (x *RouteDefinitionProto) GetDescription() string { +func (x *PayloadSchemaProto) GetExample() string { if x != nil { - return x.Description + return x.Example } return "" } -type RoutesResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Routes []*RouteDefinitionProto `protobuf:"bytes,1,rep,name=routes,proto3" json:"routes,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *RoutesResponse) Reset() { - *x = RoutesResponse{} - mi := &file_plugin_proto_msgTypes[38] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *RoutesResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*RoutesResponse) ProtoMessage() {} - -func (x *RoutesResponse) ProtoReflect() protoreflect.Message { - mi := &file_plugin_proto_msgTypes[38] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use RoutesResponse.ProtoReflect.Descriptor instead. -func (*RoutesResponse) Descriptor() ([]byte, []int) { - return file_plugin_proto_rawDescGZIP(), []int{38} -} - -func (x *RoutesResponse) GetRoutes() []*RouteDefinitionProto { +func (x *PayloadSchemaProto) GetMetadata() map[string]string { if x != nil { - return x.Routes + return x.Metadata } return nil } -type AccountProto struct { - state protoimpl.MessageState `protogen:"open.v1"` - Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` - Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` - Platform string `protobuf:"bytes,3,opt,name=platform,proto3" json:"platform,omitempty"` - Type string `protobuf:"bytes,4,opt,name=type,proto3" json:"type,omitempty"` - CredentialsJson []byte `protobuf:"bytes,5,opt,name=credentials_json,json=credentialsJson,proto3" json:"credentials_json,omitempty"` - ProxyUrl string `protobuf:"bytes,6,opt,name=proxy_url,json=proxyUrl,proto3" json:"proxy_url,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache +type RouteSchemaProto struct { + state protoimpl.MessageState `protogen:"open.v1"` + Method string `protobuf:"bytes,1,opt,name=method,proto3" json:"method,omitempty"` + Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` + Summary string `protobuf:"bytes,3,opt,name=summary,proto3" json:"summary,omitempty"` + Request *PayloadSchemaProto `protobuf:"bytes,4,opt,name=request,proto3" json:"request,omitempty"` + Response *PayloadSchemaProto `protobuf:"bytes,5,opt,name=response,proto3" json:"response,omitempty"` + Metadata map[string]string `protobuf:"bytes,6,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *AccountProto) Reset() { - *x = AccountProto{} - mi := &file_plugin_proto_msgTypes[39] +func (x *RouteSchemaProto) Reset() { + *x = RouteSchemaProto{} + mi := &file_plugin_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *AccountProto) String() string { +func (x *RouteSchemaProto) String() string { return protoimpl.X.MessageStringOf(x) } -func (*AccountProto) ProtoMessage() {} +func (*RouteSchemaProto) ProtoMessage() {} -func (x *AccountProto) ProtoReflect() protoreflect.Message { - mi := &file_plugin_proto_msgTypes[39] +func (x *RouteSchemaProto) ProtoReflect() protoreflect.Message { + mi := &file_plugin_proto_msgTypes[35] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2733,79 +2732,79 @@ func (x *AccountProto) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use AccountProto.ProtoReflect.Descriptor instead. -func (*AccountProto) Descriptor() ([]byte, []int) { - return file_plugin_proto_rawDescGZIP(), []int{39} +// Deprecated: Use RouteSchemaProto.ProtoReflect.Descriptor instead. +func (*RouteSchemaProto) Descriptor() ([]byte, []int) { + return file_plugin_proto_rawDescGZIP(), []int{35} } -func (x *AccountProto) GetId() int64 { +func (x *RouteSchemaProto) GetMethod() string { if x != nil { - return x.Id + return x.Method } - return 0 + return "" } -func (x *AccountProto) GetName() string { +func (x *RouteSchemaProto) GetPath() string { if x != nil { - return x.Name + return x.Path } return "" } -func (x *AccountProto) GetPlatform() string { +func (x *RouteSchemaProto) GetSummary() string { if x != nil { - return x.Platform + return x.Summary } return "" } -func (x *AccountProto) GetType() string { +func (x *RouteSchemaProto) GetRequest() *PayloadSchemaProto { if x != nil { - return x.Type + return x.Request } - return "" + return nil } -func (x *AccountProto) GetCredentialsJson() []byte { +func (x *RouteSchemaProto) GetResponse() *PayloadSchemaProto { if x != nil { - return x.CredentialsJson + return x.Response } return nil } -func (x *AccountProto) GetProxyUrl() string { +func (x *RouteSchemaProto) GetMetadata() map[string]string { if x != nil { - return x.ProxyUrl + return x.Metadata } - return "" + return nil } -type ForwardRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Body []byte `protobuf:"bytes,7,opt,name=body,proto3" json:"body,omitempty"` - Headers map[string]*HeaderValues `protobuf:"bytes,8,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - Model string `protobuf:"bytes,9,opt,name=model,proto3" json:"model,omitempty"` - Stream bool `protobuf:"varint,10,opt,name=stream,proto3" json:"stream,omitempty"` - Account *AccountProto `protobuf:"bytes,11,opt,name=account,proto3" json:"account,omitempty"` +type TaskSchemaProto struct { + state protoimpl.MessageState `protogen:"open.v1"` + Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` + Summary string `protobuf:"bytes,2,opt,name=summary,proto3" json:"summary,omitempty"` + Input *PayloadSchemaProto `protobuf:"bytes,3,opt,name=input,proto3" json:"input,omitempty"` + Output *PayloadSchemaProto `protobuf:"bytes,4,opt,name=output,proto3" json:"output,omitempty"` + Metadata map[string]string `protobuf:"bytes,5,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *ForwardRequest) Reset() { - *x = ForwardRequest{} - mi := &file_plugin_proto_msgTypes[40] +func (x *TaskSchemaProto) Reset() { + *x = TaskSchemaProto{} + mi := &file_plugin_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ForwardRequest) String() string { +func (x *TaskSchemaProto) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ForwardRequest) ProtoMessage() {} +func (*TaskSchemaProto) ProtoMessage() {} -func (x *ForwardRequest) ProtoReflect() protoreflect.Message { - mi := &file_plugin_proto_msgTypes[40] +func (x *TaskSchemaProto) ProtoReflect() protoreflect.Message { + mi := &file_plugin_proto_msgTypes[36] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2816,71 +2815,72 @@ func (x *ForwardRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ForwardRequest.ProtoReflect.Descriptor instead. -func (*ForwardRequest) Descriptor() ([]byte, []int) { - return file_plugin_proto_rawDescGZIP(), []int{40} +// Deprecated: Use TaskSchemaProto.ProtoReflect.Descriptor instead. +func (*TaskSchemaProto) Descriptor() ([]byte, []int) { + return file_plugin_proto_rawDescGZIP(), []int{36} } -func (x *ForwardRequest) GetBody() []byte { +func (x *TaskSchemaProto) GetType() string { if x != nil { - return x.Body + return x.Type } - return nil + return "" } -func (x *ForwardRequest) GetHeaders() map[string]*HeaderValues { +func (x *TaskSchemaProto) GetSummary() string { if x != nil { - return x.Headers + return x.Summary } - return nil + return "" } -func (x *ForwardRequest) GetModel() string { +func (x *TaskSchemaProto) GetInput() *PayloadSchemaProto { if x != nil { - return x.Model + return x.Input } - return "" + return nil } -func (x *ForwardRequest) GetStream() bool { +func (x *TaskSchemaProto) GetOutput() *PayloadSchemaProto { if x != nil { - return x.Stream + return x.Output } - return false + return nil } -func (x *ForwardRequest) GetAccount() *AccountProto { +func (x *TaskSchemaProto) GetMetadata() map[string]string { if x != nil { - return x.Account + return x.Metadata } return nil } -// UpstreamResponse 上游返回的原始 HTTP 快照。 -type UpstreamResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - StatusCode int32 `protobuf:"varint,1,opt,name=status_code,json=statusCode,proto3" json:"status_code,omitempty"` - Headers map[string]*HeaderValues `protobuf:"bytes,2,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - Body []byte `protobuf:"bytes,3,opt,name=body,proto3" json:"body,omitempty"` +type EventSchemaProto struct { + state protoimpl.MessageState `protogen:"open.v1"` + Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` + Source string `protobuf:"bytes,2,opt,name=source,proto3" json:"source,omitempty"` + Summary string `protobuf:"bytes,3,opt,name=summary,proto3" json:"summary,omitempty"` + Payload *PayloadSchemaProto `protobuf:"bytes,4,opt,name=payload,proto3" json:"payload,omitempty"` + Metadata map[string]string `protobuf:"bytes,5,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *UpstreamResponse) Reset() { - *x = UpstreamResponse{} - mi := &file_plugin_proto_msgTypes[41] +func (x *EventSchemaProto) Reset() { + *x = EventSchemaProto{} + mi := &file_plugin_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *UpstreamResponse) String() string { +func (x *EventSchemaProto) String() string { return protoimpl.X.MessageStringOf(x) } -func (*UpstreamResponse) ProtoMessage() {} +func (*EventSchemaProto) ProtoMessage() {} -func (x *UpstreamResponse) ProtoReflect() protoreflect.Message { - mi := &file_plugin_proto_msgTypes[41] +func (x *EventSchemaProto) ProtoReflect() protoreflect.Message { + mi := &file_plugin_proto_msgTypes[37] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2891,78 +2891,75 @@ func (x *UpstreamResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use UpstreamResponse.ProtoReflect.Descriptor instead. -func (*UpstreamResponse) Descriptor() ([]byte, []int) { - return file_plugin_proto_rawDescGZIP(), []int{41} +// Deprecated: Use EventSchemaProto.ProtoReflect.Descriptor instead. +func (*EventSchemaProto) Descriptor() ([]byte, []int) { + return file_plugin_proto_rawDescGZIP(), []int{37} } -func (x *UpstreamResponse) GetStatusCode() int32 { +func (x *EventSchemaProto) GetType() string { if x != nil { - return x.StatusCode + return x.Type } - return 0 + return "" } -func (x *UpstreamResponse) GetHeaders() map[string]*HeaderValues { +func (x *EventSchemaProto) GetSource() string { if x != nil { - return x.Headers + return x.Source + } + return "" +} + +func (x *EventSchemaProto) GetSummary() string { + if x != nil { + return x.Summary + } + return "" +} + +func (x *EventSchemaProto) GetPayload() *PayloadSchemaProto { + if x != nil { + return x.Payload } return nil } -func (x *UpstreamResponse) GetBody() []byte { +func (x *EventSchemaProto) GetMetadata() map[string]string { if x != nil { - return x.Body + return x.Metadata } return nil } -// Usage 单次调用的 token / 费用统计。非 Success 判决下应为空。 -type Usage struct { - state protoimpl.MessageState `protogen:"open.v1"` - InputTokens int64 `protobuf:"varint,1,opt,name=input_tokens,json=inputTokens,proto3" json:"input_tokens,omitempty"` - OutputTokens int64 `protobuf:"varint,2,opt,name=output_tokens,json=outputTokens,proto3" json:"output_tokens,omitempty"` - CachedInputTokens int64 `protobuf:"varint,3,opt,name=cached_input_tokens,json=cachedInputTokens,proto3" json:"cached_input_tokens,omitempty"` - CacheCreationTokens int64 `protobuf:"varint,4,opt,name=cache_creation_tokens,json=cacheCreationTokens,proto3" json:"cache_creation_tokens,omitempty"` - CacheCreation_5MTokens int64 `protobuf:"varint,5,opt,name=cache_creation_5m_tokens,json=cacheCreation5mTokens,proto3" json:"cache_creation_5m_tokens,omitempty"` - CacheCreation_1HTokens int64 `protobuf:"varint,6,opt,name=cache_creation_1h_tokens,json=cacheCreation1hTokens,proto3" json:"cache_creation_1h_tokens,omitempty"` - ReasoningOutputTokens int64 `protobuf:"varint,7,opt,name=reasoning_output_tokens,json=reasoningOutputTokens,proto3" json:"reasoning_output_tokens,omitempty"` - InputCost float64 `protobuf:"fixed64,10,opt,name=input_cost,json=inputCost,proto3" json:"input_cost,omitempty"` - OutputCost float64 `protobuf:"fixed64,11,opt,name=output_cost,json=outputCost,proto3" json:"output_cost,omitempty"` - CachedInputCost float64 `protobuf:"fixed64,12,opt,name=cached_input_cost,json=cachedInputCost,proto3" json:"cached_input_cost,omitempty"` - CacheCreationCost float64 `protobuf:"fixed64,13,opt,name=cache_creation_cost,json=cacheCreationCost,proto3" json:"cache_creation_cost,omitempty"` - InputPrice float64 `protobuf:"fixed64,20,opt,name=input_price,json=inputPrice,proto3" json:"input_price,omitempty"` - OutputPrice float64 `protobuf:"fixed64,21,opt,name=output_price,json=outputPrice,proto3" json:"output_price,omitempty"` - CachedInputPrice float64 `protobuf:"fixed64,22,opt,name=cached_input_price,json=cachedInputPrice,proto3" json:"cached_input_price,omitempty"` - CacheCreationPrice float64 `protobuf:"fixed64,23,opt,name=cache_creation_price,json=cacheCreationPrice,proto3" json:"cache_creation_price,omitempty"` - CacheCreation_1HPrice float64 `protobuf:"fixed64,24,opt,name=cache_creation_1h_price,json=cacheCreation1hPrice,proto3" json:"cache_creation_1h_price,omitempty"` - Model string `protobuf:"bytes,30,opt,name=model,proto3" json:"model,omitempty"` - ServiceTier string `protobuf:"bytes,31,opt,name=service_tier,json=serviceTier,proto3" json:"service_tier,omitempty"` - FirstTokenMs int64 `protobuf:"varint,32,opt,name=first_token_ms,json=firstTokenMs,proto3" json:"first_token_ms,omitempty"` - // image_size 是图像生成请求实际出图的尺寸("WxH",例如 "1024x1024"、"3840x2160")。 - // 网关侧按 1K/2K/4K 三档计费,把分档来源(实际尺寸)记下来,admin 后台 usage_log - // 显示费用时旁边带上 size,用户能直观看出"为什么这次扣了 0.40"。 - // 非图像请求留空。 - ImageSize string `protobuf:"bytes,33,opt,name=image_size,json=imageSize,proto3" json:"image_size,omitempty"` +type InvokeSchemaProto struct { + state protoimpl.MessageState `protogen:"open.v1"` + Method string `protobuf:"bytes,1,opt,name=method,proto3" json:"method,omitempty"` + Summary string `protobuf:"bytes,2,opt,name=summary,proto3" json:"summary,omitempty"` + Request *PayloadSchemaProto `protobuf:"bytes,3,opt,name=request,proto3" json:"request,omitempty"` + Response *PayloadSchemaProto `protobuf:"bytes,4,opt,name=response,proto3" json:"response,omitempty"` + Metadata map[string]string `protobuf:"bytes,5,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + Transport string `protobuf:"bytes,6,opt,name=transport,proto3" json:"transport,omitempty"` // unary / server_stream / client_stream / bidirectional_stream + ClientFrame *PayloadSchemaProto `protobuf:"bytes,7,opt,name=client_frame,json=clientFrame,proto3" json:"client_frame,omitempty"` // InvokeStream client frame payload schema + ServerFrame *PayloadSchemaProto `protobuf:"bytes,8,opt,name=server_frame,json=serverFrame,proto3" json:"server_frame,omitempty"` // InvokeStream server frame payload schema unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *Usage) Reset() { - *x = Usage{} - mi := &file_plugin_proto_msgTypes[42] +func (x *InvokeSchemaProto) Reset() { + *x = InvokeSchemaProto{} + mi := &file_plugin_proto_msgTypes[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *Usage) String() string { +func (x *InvokeSchemaProto) String() string { return protoimpl.X.MessageStringOf(x) } -func (*Usage) ProtoMessage() {} +func (*InvokeSchemaProto) ProtoMessage() {} -func (x *Usage) ProtoReflect() protoreflect.Message { - mi := &file_plugin_proto_msgTypes[42] +func (x *InvokeSchemaProto) ProtoReflect() protoreflect.Message { + mi := &file_plugin_proto_msgTypes[38] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2973,180 +2970,180 @@ func (x *Usage) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use Usage.ProtoReflect.Descriptor instead. -func (*Usage) Descriptor() ([]byte, []int) { - return file_plugin_proto_rawDescGZIP(), []int{42} +// Deprecated: Use InvokeSchemaProto.ProtoReflect.Descriptor instead. +func (*InvokeSchemaProto) Descriptor() ([]byte, []int) { + return file_plugin_proto_rawDescGZIP(), []int{38} } -func (x *Usage) GetInputTokens() int64 { +func (x *InvokeSchemaProto) GetMethod() string { if x != nil { - return x.InputTokens + return x.Method } - return 0 + return "" } -func (x *Usage) GetOutputTokens() int64 { +func (x *InvokeSchemaProto) GetSummary() string { if x != nil { - return x.OutputTokens + return x.Summary } - return 0 + return "" } -func (x *Usage) GetCachedInputTokens() int64 { +func (x *InvokeSchemaProto) GetRequest() *PayloadSchemaProto { if x != nil { - return x.CachedInputTokens + return x.Request } - return 0 + return nil } -func (x *Usage) GetCacheCreationTokens() int64 { +func (x *InvokeSchemaProto) GetResponse() *PayloadSchemaProto { if x != nil { - return x.CacheCreationTokens + return x.Response } - return 0 + return nil } -func (x *Usage) GetCacheCreation_5MTokens() int64 { +func (x *InvokeSchemaProto) GetMetadata() map[string]string { if x != nil { - return x.CacheCreation_5MTokens + return x.Metadata } - return 0 + return nil } -func (x *Usage) GetCacheCreation_1HTokens() int64 { +func (x *InvokeSchemaProto) GetTransport() string { if x != nil { - return x.CacheCreation_1HTokens + return x.Transport } - return 0 + return "" } -func (x *Usage) GetReasoningOutputTokens() int64 { +func (x *InvokeSchemaProto) GetClientFrame() *PayloadSchemaProto { if x != nil { - return x.ReasoningOutputTokens + return x.ClientFrame } - return 0 + return nil } -func (x *Usage) GetInputCost() float64 { +func (x *InvokeSchemaProto) GetServerFrame() *PayloadSchemaProto { if x != nil { - return x.InputCost + return x.ServerFrame } - return 0 + return nil } -func (x *Usage) GetOutputCost() float64 { - if x != nil { - return x.OutputCost - } - return 0 +type PluginSchemaResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Routes []*RouteSchemaProto `protobuf:"bytes,1,rep,name=routes,proto3" json:"routes,omitempty"` + Tasks []*TaskSchemaProto `protobuf:"bytes,2,rep,name=tasks,proto3" json:"tasks,omitempty"` + Events []*EventSchemaProto `protobuf:"bytes,3,rep,name=events,proto3" json:"events,omitempty"` + Invokes []*InvokeSchemaProto `protobuf:"bytes,4,rep,name=invokes,proto3" json:"invokes,omitempty"` + Metadata map[string]string `protobuf:"bytes,5,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *Usage) GetCachedInputCost() float64 { - if x != nil { - return x.CachedInputCost - } - return 0 +func (x *PluginSchemaResponse) Reset() { + *x = PluginSchemaResponse{} + mi := &file_plugin_proto_msgTypes[39] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } -func (x *Usage) GetCacheCreationCost() float64 { - if x != nil { - return x.CacheCreationCost - } - return 0 +func (x *PluginSchemaResponse) String() string { + return protoimpl.X.MessageStringOf(x) } -func (x *Usage) GetInputPrice() float64 { +func (*PluginSchemaResponse) ProtoMessage() {} + +func (x *PluginSchemaResponse) ProtoReflect() protoreflect.Message { + mi := &file_plugin_proto_msgTypes[39] if x != nil { - return x.InputPrice + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms } - return 0 + return mi.MessageOf(x) } -func (x *Usage) GetOutputPrice() float64 { - if x != nil { - return x.OutputPrice - } - return 0 +// Deprecated: Use PluginSchemaResponse.ProtoReflect.Descriptor instead. +func (*PluginSchemaResponse) Descriptor() ([]byte, []int) { + return file_plugin_proto_rawDescGZIP(), []int{39} } -func (x *Usage) GetCachedInputPrice() float64 { +func (x *PluginSchemaResponse) GetRoutes() []*RouteSchemaProto { if x != nil { - return x.CachedInputPrice + return x.Routes } - return 0 + return nil } -func (x *Usage) GetCacheCreationPrice() float64 { +func (x *PluginSchemaResponse) GetTasks() []*TaskSchemaProto { if x != nil { - return x.CacheCreationPrice + return x.Tasks } - return 0 + return nil } -func (x *Usage) GetCacheCreation_1HPrice() float64 { +func (x *PluginSchemaResponse) GetEvents() []*EventSchemaProto { if x != nil { - return x.CacheCreation_1HPrice + return x.Events } - return 0 + return nil } -func (x *Usage) GetModel() string { +func (x *PluginSchemaResponse) GetInvokes() []*InvokeSchemaProto { if x != nil { - return x.Model + return x.Invokes } - return "" + return nil } -func (x *Usage) GetServiceTier() string { +func (x *PluginSchemaResponse) GetMetadata() map[string]string { if x != nil { - return x.ServiceTier + return x.Metadata } - return "" + return nil } -func (x *Usage) GetFirstTokenMs() int64 { - if x != nil { - return x.FirstTokenMs - } - return 0 +// MiddlewareRequest OnForwardBegin 的输入:请求元数据 + 可选的 request body。 +type MiddlewareRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // === 核心元数据(默认总是填充)=== + RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` // core 为本次请求分配的唯一 ID(便于跨 Begin/End 关联) + UserId int64 `protobuf:"varint,2,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + GroupId int64 `protobuf:"varint,3,opt,name=group_id,json=groupId,proto3" json:"group_id,omitempty"` + AccountId int64 `protobuf:"varint,4,opt,name=account_id,json=accountId,proto3" json:"account_id,omitempty"` // 已由 scheduler 选出 + Platform string `protobuf:"bytes,5,opt,name=platform,proto3" json:"platform,omitempty"` + Model string `protobuf:"bytes,6,opt,name=model,proto3" json:"model,omitempty"` + Stream bool `protobuf:"varint,7,opt,name=stream,proto3" json:"stream,omitempty"` + Estimates []*UsageMetric `protobuf:"bytes,8,rep,name=estimates,proto3" json:"estimates,omitempty"` // Core 或插件提供的通用预估值,仅用于早期决策 + // metadata KV bag:供多个 middleware 之间传递上下文。 + // 命名空间规则由 Core 管理,插件不应依赖未声明的敏感字段。 + Metadata map[string]string `protobuf:"bytes,9,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // === 按需字段(声明了 middleware.read_body capability 的插件才会收到)=== + RequestBody []byte `protobuf:"bytes,100,opt,name=request_body,json=requestBody,proto3" json:"request_body,omitempty"` + RequestHeaders map[string]*HeaderValues `protobuf:"bytes,101,rep,name=request_headers,json=requestHeaders,proto3" json:"request_headers,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *Usage) GetImageSize() string { - if x != nil { - return x.ImageSize - } - return "" -} - -// ForwardOutcome 插件对一次 Forward 的完整判决结果。 -type ForwardOutcome struct { - state protoimpl.MessageState `protogen:"open.v1"` - Kind OutcomeKind `protobuf:"varint,1,opt,name=kind,proto3,enum=airgate.plugin.v1.OutcomeKind" json:"kind,omitempty"` - Upstream *UpstreamResponse `protobuf:"bytes,2,opt,name=upstream,proto3" json:"upstream,omitempty"` - Usage *Usage `protobuf:"bytes,3,opt,name=usage,proto3" json:"usage,omitempty"` - DurationMs int64 `protobuf:"varint,4,opt,name=duration_ms,json=durationMs,proto3" json:"duration_ms,omitempty"` - RetryAfterMs int64 `protobuf:"varint,5,opt,name=retry_after_ms,json=retryAfterMs,proto3" json:"retry_after_ms,omitempty"` - Reason string `protobuf:"bytes,6,opt,name=reason,proto3" json:"reason,omitempty"` - UpdatedCredentials map[string]string `protobuf:"bytes,7,rep,name=updated_credentials,json=updatedCredentials,proto3" json:"updated_credentials,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ForwardOutcome) Reset() { - *x = ForwardOutcome{} - mi := &file_plugin_proto_msgTypes[43] +func (x *MiddlewareRequest) Reset() { + *x = MiddlewareRequest{} + mi := &file_plugin_proto_msgTypes[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ForwardOutcome) String() string { +func (x *MiddlewareRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ForwardOutcome) ProtoMessage() {} +func (*MiddlewareRequest) ProtoMessage() {} -func (x *ForwardOutcome) ProtoReflect() protoreflect.Message { - mi := &file_plugin_proto_msgTypes[43] +func (x *MiddlewareRequest) ProtoReflect() protoreflect.Message { + mi := &file_plugin_proto_msgTypes[40] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3157,158 +3154,131 @@ func (x *ForwardOutcome) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ForwardOutcome.ProtoReflect.Descriptor instead. -func (*ForwardOutcome) Descriptor() ([]byte, []int) { - return file_plugin_proto_rawDescGZIP(), []int{43} -} - -func (x *ForwardOutcome) GetKind() OutcomeKind { - if x != nil { - return x.Kind - } - return OutcomeKind_OUTCOME_UNKNOWN +// Deprecated: Use MiddlewareRequest.ProtoReflect.Descriptor instead. +func (*MiddlewareRequest) Descriptor() ([]byte, []int) { + return file_plugin_proto_rawDescGZIP(), []int{40} } -func (x *ForwardOutcome) GetUpstream() *UpstreamResponse { +func (x *MiddlewareRequest) GetRequestId() string { if x != nil { - return x.Upstream + return x.RequestId } - return nil + return "" } -func (x *ForwardOutcome) GetUsage() *Usage { +func (x *MiddlewareRequest) GetUserId() int64 { if x != nil { - return x.Usage + return x.UserId } - return nil + return 0 } -func (x *ForwardOutcome) GetDurationMs() int64 { +func (x *MiddlewareRequest) GetGroupId() int64 { if x != nil { - return x.DurationMs + return x.GroupId } return 0 } -func (x *ForwardOutcome) GetRetryAfterMs() int64 { +func (x *MiddlewareRequest) GetAccountId() int64 { if x != nil { - return x.RetryAfterMs + return x.AccountId } return 0 } -func (x *ForwardOutcome) GetReason() string { +func (x *MiddlewareRequest) GetPlatform() string { if x != nil { - return x.Reason + return x.Platform } return "" } -func (x *ForwardOutcome) GetUpdatedCredentials() map[string]string { +func (x *MiddlewareRequest) GetModel() string { if x != nil { - return x.UpdatedCredentials + return x.Model } - return nil -} - -type ForwardChunk struct { - state protoimpl.MessageState `protogen:"open.v1"` - Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` - Done bool `protobuf:"varint,2,opt,name=done,proto3" json:"done,omitempty"` - FinalOutcome *ForwardOutcome `protobuf:"bytes,3,opt,name=final_outcome,json=finalOutcome,proto3" json:"final_outcome,omitempty"` - StatusCode int32 `protobuf:"varint,4,opt,name=status_code,json=statusCode,proto3" json:"status_code,omitempty"` - Headers map[string]*HeaderValues `protobuf:"bytes,5,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ForwardChunk) Reset() { - *x = ForwardChunk{} - mi := &file_plugin_proto_msgTypes[44] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ForwardChunk) String() string { - return protoimpl.X.MessageStringOf(x) + return "" } -func (*ForwardChunk) ProtoMessage() {} - -func (x *ForwardChunk) ProtoReflect() protoreflect.Message { - mi := &file_plugin_proto_msgTypes[44] +func (x *MiddlewareRequest) GetStream() bool { if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms + return x.Stream } - return mi.MessageOf(x) -} - -// Deprecated: Use ForwardChunk.ProtoReflect.Descriptor instead. -func (*ForwardChunk) Descriptor() ([]byte, []int) { - return file_plugin_proto_rawDescGZIP(), []int{44} + return false } -func (x *ForwardChunk) GetData() []byte { +func (x *MiddlewareRequest) GetEstimates() []*UsageMetric { if x != nil { - return x.Data + return x.Estimates } return nil } -func (x *ForwardChunk) GetDone() bool { - if x != nil { - return x.Done - } - return false -} - -func (x *ForwardChunk) GetFinalOutcome() *ForwardOutcome { +func (x *MiddlewareRequest) GetMetadata() map[string]string { if x != nil { - return x.FinalOutcome + return x.Metadata } return nil } -func (x *ForwardChunk) GetStatusCode() int32 { +func (x *MiddlewareRequest) GetRequestBody() []byte { if x != nil { - return x.StatusCode + return x.RequestBody } - return 0 + return nil } -func (x *ForwardChunk) GetHeaders() map[string]*HeaderValues { +func (x *MiddlewareRequest) GetRequestHeaders() map[string]*HeaderValues { if x != nil { - return x.Headers + return x.RequestHeaders } return nil } -type CredentialsRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Credentials map[string]string `protobuf:"bytes,1,rep,name=credentials,proto3" json:"credentials,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache +// MiddlewareEvent OnForwardEnd 的输入:完整的请求 + 响应元数据。 +type MiddlewareEvent struct { + state protoimpl.MessageState `protogen:"open.v1"` + // === 核心元数据(与 MiddlewareRequest 对齐)=== + RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + UserId int64 `protobuf:"varint,2,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + GroupId int64 `protobuf:"varint,3,opt,name=group_id,json=groupId,proto3" json:"group_id,omitempty"` + AccountId int64 `protobuf:"varint,4,opt,name=account_id,json=accountId,proto3" json:"account_id,omitempty"` + Platform string `protobuf:"bytes,5,opt,name=platform,proto3" json:"platform,omitempty"` + Model string `protobuf:"bytes,6,opt,name=model,proto3" json:"model,omitempty"` + Stream bool `protobuf:"varint,7,opt,name=stream,proto3" json:"stream,omitempty"` + Estimates []*UsageMetric `protobuf:"bytes,8,rep,name=estimates,proto3" json:"estimates,omitempty"` // 与 MiddlewareRequest 字段对齐,便于 estimate vs actual 比对。 + // === 响应结果 === + StatusCode int64 `protobuf:"varint,20,opt,name=status_code,json=statusCode,proto3" json:"status_code,omitempty"` + DurationMs int64 `protobuf:"varint,21,opt,name=duration_ms,json=durationMs,proto3" json:"duration_ms,omitempty"` + Usage *Usage `protobuf:"bytes,22,opt,name=usage,proto3" json:"usage,omitempty"` + ErrorKind string `protobuf:"bytes,23,opt,name=error_kind,json=errorKind,proto3" json:"error_kind,omitempty"` // "" / "upstream_5xx" / "timeout" / "no_account" / ... + ErrorMsg string `protobuf:"bytes,24,opt,name=error_msg,json=errorMsg,proto3" json:"error_msg,omitempty"` // 限长 512,见 core 实现 + // metadata 延续自 OnForwardBegin 的 bag + Metadata map[string]string `protobuf:"bytes,40,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // === 按需字段(声明了 middleware.read_body capability 的插件才会收到)=== + // 流式响应时 response_body 只给摘要(首次非空 chunk 拼装)。 + ResponseBody []byte `protobuf:"bytes,100,opt,name=response_body,json=responseBody,proto3" json:"response_body,omitempty"` + ResponseHeaders map[string]*HeaderValues `protobuf:"bytes,101,rep,name=response_headers,json=responseHeaders,proto3" json:"response_headers,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *CredentialsRequest) Reset() { - *x = CredentialsRequest{} - mi := &file_plugin_proto_msgTypes[45] +func (x *MiddlewareEvent) Reset() { + *x = MiddlewareEvent{} + mi := &file_plugin_proto_msgTypes[41] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *CredentialsRequest) String() string { +func (x *MiddlewareEvent) String() string { return protoimpl.X.MessageStringOf(x) } -func (*CredentialsRequest) ProtoMessage() {} +func (*MiddlewareEvent) ProtoMessage() {} -func (x *CredentialsRequest) ProtoReflect() protoreflect.Message { - mi := &file_plugin_proto_msgTypes[45] +func (x *MiddlewareEvent) ProtoReflect() protoreflect.Message { + mi := &file_plugin_proto_msgTypes[41] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3319,271 +3289,153 @@ func (x *CredentialsRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use CredentialsRequest.ProtoReflect.Descriptor instead. -func (*CredentialsRequest) Descriptor() ([]byte, []int) { - return file_plugin_proto_rawDescGZIP(), []int{45} +// Deprecated: Use MiddlewareEvent.ProtoReflect.Descriptor instead. +func (*MiddlewareEvent) Descriptor() ([]byte, []int) { + return file_plugin_proto_rawDescGZIP(), []int{41} } -func (x *CredentialsRequest) GetCredentials() map[string]string { +func (x *MiddlewareEvent) GetRequestId() string { if x != nil { - return x.Credentials + return x.RequestId } - return nil + return "" } -type QuotaInfoResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Total float64 `protobuf:"fixed64,1,opt,name=total,proto3" json:"total,omitempty"` - Used float64 `protobuf:"fixed64,2,opt,name=used,proto3" json:"used,omitempty"` - Remaining float64 `protobuf:"fixed64,3,opt,name=remaining,proto3" json:"remaining,omitempty"` - Currency string `protobuf:"bytes,4,opt,name=currency,proto3" json:"currency,omitempty"` - ExpiresAt string `protobuf:"bytes,5,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"` - Extra map[string]string `protobuf:"bytes,6,rep,name=extra,proto3" json:"extra,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache +func (x *MiddlewareEvent) GetUserId() int64 { + if x != nil { + return x.UserId + } + return 0 } -func (x *QuotaInfoResponse) Reset() { - *x = QuotaInfoResponse{} - mi := &file_plugin_proto_msgTypes[46] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) +func (x *MiddlewareEvent) GetGroupId() int64 { + if x != nil { + return x.GroupId + } + return 0 } -func (x *QuotaInfoResponse) String() string { - return protoimpl.X.MessageStringOf(x) +func (x *MiddlewareEvent) GetAccountId() int64 { + if x != nil { + return x.AccountId + } + return 0 } -func (*QuotaInfoResponse) ProtoMessage() {} +func (x *MiddlewareEvent) GetPlatform() string { + if x != nil { + return x.Platform + } + return "" +} -func (x *QuotaInfoResponse) ProtoReflect() protoreflect.Message { - mi := &file_plugin_proto_msgTypes[46] +func (x *MiddlewareEvent) GetModel() string { if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms + return x.Model } - return mi.MessageOf(x) + return "" } -// Deprecated: Use QuotaInfoResponse.ProtoReflect.Descriptor instead. -func (*QuotaInfoResponse) Descriptor() ([]byte, []int) { - return file_plugin_proto_rawDescGZIP(), []int{46} +func (x *MiddlewareEvent) GetStream() bool { + if x != nil { + return x.Stream + } + return false } -func (x *QuotaInfoResponse) GetTotal() float64 { +func (x *MiddlewareEvent) GetEstimates() []*UsageMetric { if x != nil { - return x.Total + return x.Estimates } - return 0 + return nil } -func (x *QuotaInfoResponse) GetUsed() float64 { +func (x *MiddlewareEvent) GetStatusCode() int64 { if x != nil { - return x.Used + return x.StatusCode } return 0 } -func (x *QuotaInfoResponse) GetRemaining() float64 { +func (x *MiddlewareEvent) GetDurationMs() int64 { if x != nil { - return x.Remaining + return x.DurationMs } return 0 } -func (x *QuotaInfoResponse) GetCurrency() string { +func (x *MiddlewareEvent) GetUsage() *Usage { if x != nil { - return x.Currency + return x.Usage + } + return nil +} + +func (x *MiddlewareEvent) GetErrorKind() string { + if x != nil { + return x.ErrorKind } return "" } -func (x *QuotaInfoResponse) GetExpiresAt() string { +func (x *MiddlewareEvent) GetErrorMsg() string { if x != nil { - return x.ExpiresAt + return x.ErrorMsg } return "" } -func (x *QuotaInfoResponse) GetExtra() map[string]string { +func (x *MiddlewareEvent) GetMetadata() map[string]string { if x != nil { - return x.Extra + return x.Metadata } return nil } -type HttpRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Method string `protobuf:"bytes,1,opt,name=method,proto3" json:"method,omitempty"` - Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` - Query string `protobuf:"bytes,3,opt,name=query,proto3" json:"query,omitempty"` - Headers map[string]*HeaderValues `protobuf:"bytes,4,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - Body []byte `protobuf:"bytes,5,opt,name=body,proto3" json:"body,omitempty"` - RemoteAddr string `protobuf:"bytes,6,opt,name=remote_addr,json=remoteAddr,proto3" json:"remote_addr,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *HttpRequest) Reset() { - *x = HttpRequest{} - mi := &file_plugin_proto_msgTypes[47] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *HttpRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*HttpRequest) ProtoMessage() {} - -func (x *HttpRequest) ProtoReflect() protoreflect.Message { - mi := &file_plugin_proto_msgTypes[47] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use HttpRequest.ProtoReflect.Descriptor instead. -func (*HttpRequest) Descriptor() ([]byte, []int) { - return file_plugin_proto_rawDescGZIP(), []int{47} -} - -func (x *HttpRequest) GetMethod() string { - if x != nil { - return x.Method - } - return "" -} - -func (x *HttpRequest) GetPath() string { - if x != nil { - return x.Path - } - return "" -} - -func (x *HttpRequest) GetQuery() string { - if x != nil { - return x.Query - } - return "" -} - -func (x *HttpRequest) GetHeaders() map[string]*HeaderValues { - if x != nil { - return x.Headers - } - return nil -} - -func (x *HttpRequest) GetBody() []byte { - if x != nil { - return x.Body - } - return nil -} - -func (x *HttpRequest) GetRemoteAddr() string { - if x != nil { - return x.RemoteAddr - } - return "" -} - -type HttpResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - StatusCode int32 `protobuf:"varint,1,opt,name=status_code,json=statusCode,proto3" json:"status_code,omitempty"` - Headers map[string]*HeaderValues `protobuf:"bytes,2,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - Body []byte `protobuf:"bytes,3,opt,name=body,proto3" json:"body,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *HttpResponse) Reset() { - *x = HttpResponse{} - mi := &file_plugin_proto_msgTypes[48] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *HttpResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*HttpResponse) ProtoMessage() {} - -func (x *HttpResponse) ProtoReflect() protoreflect.Message { - mi := &file_plugin_proto_msgTypes[48] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use HttpResponse.ProtoReflect.Descriptor instead. -func (*HttpResponse) Descriptor() ([]byte, []int) { - return file_plugin_proto_rawDescGZIP(), []int{48} -} - -func (x *HttpResponse) GetStatusCode() int32 { - if x != nil { - return x.StatusCode - } - return 0 -} - -func (x *HttpResponse) GetHeaders() map[string]*HeaderValues { +func (x *MiddlewareEvent) GetResponseBody() []byte { if x != nil { - return x.Headers + return x.ResponseBody } return nil } -func (x *HttpResponse) GetBody() []byte { +func (x *MiddlewareEvent) GetResponseHeaders() map[string]*HeaderValues { if x != nil { - return x.Body + return x.ResponseHeaders } return nil } -type HttpResponseChunk struct { - state protoimpl.MessageState `protogen:"open.v1"` - Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` - Done bool `protobuf:"varint,2,opt,name=done,proto3" json:"done,omitempty"` - StatusCode int32 `protobuf:"varint,3,opt,name=status_code,json=statusCode,proto3" json:"status_code,omitempty"` - Headers map[string]*HeaderValues `protobuf:"bytes,4,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` +// MiddlewareDecision OnForwardBegin 的输出:放行 / 拒绝 / 改请求。 +type MiddlewareDecision struct { + state protoimpl.MessageState `protogen:"open.v1"` + Action MiddlewareDecision_Action `protobuf:"varint,1,opt,name=action,proto3,enum=airgate.plugin.v1.MiddlewareDecision_Action" json:"action,omitempty"` + // action=DENY 时的错误码 / 文案(对用户可见) + DenyStatusCode int32 `protobuf:"varint,10,opt,name=deny_status_code,json=denyStatusCode,proto3" json:"deny_status_code,omitempty"` // 默认 403 if Action=DENY and 未指定 + DenyMessage string `protobuf:"bytes,11,opt,name=deny_message,json=denyMessage,proto3" json:"deny_message,omitempty"` + // action=MUTATE 时要追加/覆盖的请求头 + SetHeaders map[string]*HeaderValues `protobuf:"bytes,20,rep,name=set_headers,json=setHeaders,proto3" json:"set_headers,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // 贯穿式 metadata:无论 allow/deny/mutate,都能往 bag 里写东西供后续 middleware / End 使用 + Metadata map[string]string `protobuf:"bytes,30,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *HttpResponseChunk) Reset() { - *x = HttpResponseChunk{} - mi := &file_plugin_proto_msgTypes[49] +func (x *MiddlewareDecision) Reset() { + *x = MiddlewareDecision{} + mi := &file_plugin_proto_msgTypes[42] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *HttpResponseChunk) String() string { +func (x *MiddlewareDecision) String() string { return protoimpl.X.MessageStringOf(x) } -func (*HttpResponseChunk) ProtoMessage() {} +func (*MiddlewareDecision) ProtoMessage() {} -func (x *HttpResponseChunk) ProtoReflect() protoreflect.Message { - mi := &file_plugin_proto_msgTypes[49] +func (x *MiddlewareDecision) ProtoReflect() protoreflect.Message { + mi := &file_plugin_proto_msgTypes[42] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3594,209 +3446,71 @@ func (x *HttpResponseChunk) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use HttpResponseChunk.ProtoReflect.Descriptor instead. -func (*HttpResponseChunk) Descriptor() ([]byte, []int) { - return file_plugin_proto_rawDescGZIP(), []int{49} -} - -func (x *HttpResponseChunk) GetData() []byte { - if x != nil { - return x.Data - } - return nil +// Deprecated: Use MiddlewareDecision.ProtoReflect.Descriptor instead. +func (*MiddlewareDecision) Descriptor() ([]byte, []int) { + return file_plugin_proto_rawDescGZIP(), []int{42} } -func (x *HttpResponseChunk) GetDone() bool { +func (x *MiddlewareDecision) GetAction() MiddlewareDecision_Action { if x != nil { - return x.Done + return x.Action } - return false + return MiddlewareDecision_ALLOW } -func (x *HttpResponseChunk) GetStatusCode() int32 { +func (x *MiddlewareDecision) GetDenyStatusCode() int32 { if x != nil { - return x.StatusCode + return x.DenyStatusCode } return 0 } -func (x *HttpResponseChunk) GetHeaders() map[string]*HeaderValues { - if x != nil { - return x.Headers - } - return nil -} - -type BackgroundTaskProto struct { - state protoimpl.MessageState `protogen:"open.v1"` - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - IntervalMs int64 `protobuf:"varint,2,opt,name=interval_ms,json=intervalMs,proto3" json:"interval_ms,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *BackgroundTaskProto) Reset() { - *x = BackgroundTaskProto{} - mi := &file_plugin_proto_msgTypes[50] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *BackgroundTaskProto) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*BackgroundTaskProto) ProtoMessage() {} - -func (x *BackgroundTaskProto) ProtoReflect() protoreflect.Message { - mi := &file_plugin_proto_msgTypes[50] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use BackgroundTaskProto.ProtoReflect.Descriptor instead. -func (*BackgroundTaskProto) Descriptor() ([]byte, []int) { - return file_plugin_proto_rawDescGZIP(), []int{50} -} - -func (x *BackgroundTaskProto) GetName() string { +func (x *MiddlewareDecision) GetDenyMessage() string { if x != nil { - return x.Name + return x.DenyMessage } return "" } -func (x *BackgroundTaskProto) GetIntervalMs() int64 { - if x != nil { - return x.IntervalMs - } - return 0 -} - -type BackgroundTasksResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Tasks []*BackgroundTaskProto `protobuf:"bytes,1,rep,name=tasks,proto3" json:"tasks,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *BackgroundTasksResponse) Reset() { - *x = BackgroundTasksResponse{} - mi := &file_plugin_proto_msgTypes[51] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *BackgroundTasksResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*BackgroundTasksResponse) ProtoMessage() {} - -func (x *BackgroundTasksResponse) ProtoReflect() protoreflect.Message { - mi := &file_plugin_proto_msgTypes[51] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use BackgroundTasksResponse.ProtoReflect.Descriptor instead. -func (*BackgroundTasksResponse) Descriptor() ([]byte, []int) { - return file_plugin_proto_rawDescGZIP(), []int{51} -} - -func (x *BackgroundTasksResponse) GetTasks() []*BackgroundTaskProto { +func (x *MiddlewareDecision) GetSetHeaders() map[string]*HeaderValues { if x != nil { - return x.Tasks + return x.SetHeaders } return nil } -type RunBackgroundTaskRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *RunBackgroundTaskRequest) Reset() { - *x = RunBackgroundTaskRequest{} - mi := &file_plugin_proto_msgTypes[52] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *RunBackgroundTaskRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*RunBackgroundTaskRequest) ProtoMessage() {} - -func (x *RunBackgroundTaskRequest) ProtoReflect() protoreflect.Message { - mi := &file_plugin_proto_msgTypes[52] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use RunBackgroundTaskRequest.ProtoReflect.Descriptor instead. -func (*RunBackgroundTaskRequest) Descriptor() ([]byte, []int) { - return file_plugin_proto_rawDescGZIP(), []int{52} -} - -func (x *RunBackgroundTaskRequest) GetName() string { +func (x *MiddlewareDecision) GetMetadata() map[string]string { if x != nil { - return x.Name - } - return "" -} - -type WebSocketFrame struct { - state protoimpl.MessageState `protogen:"open.v1"` - Type WebSocketFrame_FrameType `protobuf:"varint,1,opt,name=type,proto3,enum=airgate.plugin.v1.WebSocketFrame_FrameType" json:"type,omitempty"` - Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` - // 仅 CONNECT 帧使用 - ConnectInfo *WebSocketConnectInfo `protobuf:"bytes,3,opt,name=connect_info,json=connectInfo,proto3" json:"connect_info,omitempty"` - // 仅 CLOSE 帧使用 - CloseCode int32 `protobuf:"varint,4,opt,name=close_code,json=closeCode,proto3" json:"close_code,omitempty"` - CloseReason string `protobuf:"bytes,5,opt,name=close_reason,json=closeReason,proto3" json:"close_reason,omitempty"` - // 仅 RESULT 帧使用 - Outcome *ForwardOutcome `protobuf:"bytes,6,opt,name=outcome,proto3" json:"outcome,omitempty"` + return x.Metadata + } + return nil +} + +type EventSubscriptionProto struct { + state protoimpl.MessageState `protogen:"open.v1"` + Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` + Source string `protobuf:"bytes,2,opt,name=source,proto3" json:"source,omitempty"` + Filter map[string]string `protobuf:"bytes,3,rep,name=filter,proto3" json:"filter,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + Metadata map[string]string `protobuf:"bytes,4,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *WebSocketFrame) Reset() { - *x = WebSocketFrame{} - mi := &file_plugin_proto_msgTypes[53] +func (x *EventSubscriptionProto) Reset() { + *x = EventSubscriptionProto{} + mi := &file_plugin_proto_msgTypes[43] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *WebSocketFrame) String() string { +func (x *EventSubscriptionProto) String() string { return protoimpl.X.MessageStringOf(x) } -func (*WebSocketFrame) ProtoMessage() {} +func (*EventSubscriptionProto) ProtoMessage() {} -func (x *WebSocketFrame) ProtoReflect() protoreflect.Message { - mi := &file_plugin_proto_msgTypes[53] +func (x *EventSubscriptionProto) ProtoReflect() protoreflect.Message { + mi := &file_plugin_proto_msgTypes[43] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3807,80 +3521,113 @@ func (x *WebSocketFrame) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use WebSocketFrame.ProtoReflect.Descriptor instead. -func (*WebSocketFrame) Descriptor() ([]byte, []int) { - return file_plugin_proto_rawDescGZIP(), []int{53} +// Deprecated: Use EventSubscriptionProto.ProtoReflect.Descriptor instead. +func (*EventSubscriptionProto) Descriptor() ([]byte, []int) { + return file_plugin_proto_rawDescGZIP(), []int{43} } -func (x *WebSocketFrame) GetType() WebSocketFrame_FrameType { +func (x *EventSubscriptionProto) GetType() string { if x != nil { return x.Type } - return WebSocketFrame_CONNECT + return "" } -func (x *WebSocketFrame) GetData() []byte { +func (x *EventSubscriptionProto) GetSource() string { if x != nil { - return x.Data + return x.Source } - return nil + return "" } -func (x *WebSocketFrame) GetConnectInfo() *WebSocketConnectInfo { +func (x *EventSubscriptionProto) GetFilter() map[string]string { if x != nil { - return x.ConnectInfo + return x.Filter } return nil } -func (x *WebSocketFrame) GetCloseCode() int32 { +func (x *EventSubscriptionProto) GetMetadata() map[string]string { if x != nil { - return x.CloseCode + return x.Metadata } - return 0 + return nil } -func (x *WebSocketFrame) GetCloseReason() string { +type EventSubscriptionsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Subscriptions []*EventSubscriptionProto `protobuf:"bytes,1,rep,name=subscriptions,proto3" json:"subscriptions,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EventSubscriptionsResponse) Reset() { + *x = EventSubscriptionsResponse{} + mi := &file_plugin_proto_msgTypes[44] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EventSubscriptionsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EventSubscriptionsResponse) ProtoMessage() {} + +func (x *EventSubscriptionsResponse) ProtoReflect() protoreflect.Message { + mi := &file_plugin_proto_msgTypes[44] if x != nil { - return x.CloseReason + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms } - return "" + return mi.MessageOf(x) } -func (x *WebSocketFrame) GetOutcome() *ForwardOutcome { +// Deprecated: Use EventSubscriptionsResponse.ProtoReflect.Descriptor instead. +func (*EventSubscriptionsResponse) Descriptor() ([]byte, []int) { + return file_plugin_proto_rawDescGZIP(), []int{44} +} + +func (x *EventSubscriptionsResponse) GetSubscriptions() []*EventSubscriptionProto { if x != nil { - return x.Outcome + return x.Subscriptions } return nil } -type WebSocketConnectInfo struct { - state protoimpl.MessageState `protogen:"open.v1"` - Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` - Query string `protobuf:"bytes,2,opt,name=query,proto3" json:"query,omitempty"` - Headers map[string]*HeaderValues `protobuf:"bytes,3,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - RemoteAddr string `protobuf:"bytes,4,opt,name=remote_addr,json=remoteAddr,proto3" json:"remote_addr,omitempty"` - ConnectionId string `protobuf:"bytes,5,opt,name=connection_id,json=connectionId,proto3" json:"connection_id,omitempty"` - Account *AccountProto `protobuf:"bytes,12,opt,name=account,proto3" json:"account,omitempty"` +type PluginEvent struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Type string `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"` + Source string `protobuf:"bytes,3,opt,name=source,proto3" json:"source,omitempty"` + Subject string `protobuf:"bytes,4,opt,name=subject,proto3" json:"subject,omitempty"` + UserId int64 `protobuf:"varint,5,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + GroupId int64 `protobuf:"varint,6,opt,name=group_id,json=groupId,proto3" json:"group_id,omitempty"` + Payload []byte `protobuf:"bytes,7,opt,name=payload,proto3" json:"payload,omitempty"` // JSON 编码的对象 + Metadata map[string]string `protobuf:"bytes,8,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + OccurredAt int64 `protobuf:"varint,9,opt,name=occurred_at,json=occurredAt,proto3" json:"occurred_at,omitempty"` // unix millis, 0 = unset unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *WebSocketConnectInfo) Reset() { - *x = WebSocketConnectInfo{} - mi := &file_plugin_proto_msgTypes[54] +func (x *PluginEvent) Reset() { + *x = PluginEvent{} + mi := &file_plugin_proto_msgTypes[45] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *WebSocketConnectInfo) String() string { +func (x *PluginEvent) String() string { return protoimpl.X.MessageStringOf(x) } -func (*WebSocketConnectInfo) ProtoMessage() {} +func (*PluginEvent) ProtoMessage() {} -func (x *WebSocketConnectInfo) ProtoReflect() protoreflect.Message { - mi := &file_plugin_proto_msgTypes[54] +func (x *PluginEvent) ProtoReflect() protoreflect.Message { + mi := &file_plugin_proto_msgTypes[45] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3891,128 +3638,97 @@ func (x *WebSocketConnectInfo) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use WebSocketConnectInfo.ProtoReflect.Descriptor instead. -func (*WebSocketConnectInfo) Descriptor() ([]byte, []int) { - return file_plugin_proto_rawDescGZIP(), []int{54} +// Deprecated: Use PluginEvent.ProtoReflect.Descriptor instead. +func (*PluginEvent) Descriptor() ([]byte, []int) { + return file_plugin_proto_rawDescGZIP(), []int{45} } -func (x *WebSocketConnectInfo) GetPath() string { +func (x *PluginEvent) GetId() string { if x != nil { - return x.Path + return x.Id } return "" } -func (x *WebSocketConnectInfo) GetQuery() string { +func (x *PluginEvent) GetType() string { if x != nil { - return x.Query + return x.Type } return "" } -func (x *WebSocketConnectInfo) GetHeaders() map[string]*HeaderValues { +func (x *PluginEvent) GetSource() string { if x != nil { - return x.Headers + return x.Source } - return nil + return "" } -func (x *WebSocketConnectInfo) GetRemoteAddr() string { +func (x *PluginEvent) GetSubject() string { if x != nil { - return x.RemoteAddr + return x.Subject } return "" } -func (x *WebSocketConnectInfo) GetConnectionId() string { +func (x *PluginEvent) GetUserId() int64 { if x != nil { - return x.ConnectionId + return x.UserId } - return "" + return 0 } -func (x *WebSocketConnectInfo) GetAccount() *AccountProto { +func (x *PluginEvent) GetGroupId() int64 { if x != nil { - return x.Account + return x.GroupId } - return nil -} - -type WebAssetFile struct { - state protoimpl.MessageState `protogen:"open.v1"` - Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` - Content []byte `protobuf:"bytes,2,opt,name=content,proto3" json:"content,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *WebAssetFile) Reset() { - *x = WebAssetFile{} - mi := &file_plugin_proto_msgTypes[55] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *WebAssetFile) String() string { - return protoimpl.X.MessageStringOf(x) + return 0 } -func (*WebAssetFile) ProtoMessage() {} - -func (x *WebAssetFile) ProtoReflect() protoreflect.Message { - mi := &file_plugin_proto_msgTypes[55] +func (x *PluginEvent) GetPayload() []byte { if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms + return x.Payload } - return mi.MessageOf(x) -} - -// Deprecated: Use WebAssetFile.ProtoReflect.Descriptor instead. -func (*WebAssetFile) Descriptor() ([]byte, []int) { - return file_plugin_proto_rawDescGZIP(), []int{55} + return nil } -func (x *WebAssetFile) GetPath() string { +func (x *PluginEvent) GetMetadata() map[string]string { if x != nil { - return x.Path + return x.Metadata } - return "" + return nil } -func (x *WebAssetFile) GetContent() []byte { +func (x *PluginEvent) GetOccurredAt() int64 { if x != nil { - return x.Content + return x.OccurredAt } - return nil + return 0 } -type WebAssetsResponse struct { +type EventHandleResponse struct { state protoimpl.MessageState `protogen:"open.v1"` - Files []*WebAssetFile `protobuf:"bytes,1,rep,name=files,proto3" json:"files,omitempty"` - HasAssets bool `protobuf:"varint,2,opt,name=has_assets,json=hasAssets,proto3" json:"has_assets,omitempty"` + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` + ErrorMessage string `protobuf:"bytes,2,opt,name=error_message,json=errorMessage,proto3" json:"error_message,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *WebAssetsResponse) Reset() { - *x = WebAssetsResponse{} - mi := &file_plugin_proto_msgTypes[56] +func (x *EventHandleResponse) Reset() { + *x = EventHandleResponse{} + mi := &file_plugin_proto_msgTypes[46] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *WebAssetsResponse) String() string { +func (x *EventHandleResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*WebAssetsResponse) ProtoMessage() {} +func (*EventHandleResponse) ProtoMessage() {} -func (x *WebAssetsResponse) ProtoReflect() protoreflect.Message { - mi := &file_plugin_proto_msgTypes[56] +func (x *EventHandleResponse) ProtoReflect() protoreflect.Message { + mi := &file_plugin_proto_msgTypes[46] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4023,62 +3739,50 @@ func (x *WebAssetsResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use WebAssetsResponse.ProtoReflect.Descriptor instead. -func (*WebAssetsResponse) Descriptor() ([]byte, []int) { - return file_plugin_proto_rawDescGZIP(), []int{56} +// Deprecated: Use EventHandleResponse.ProtoReflect.Descriptor instead. +func (*EventHandleResponse) Descriptor() ([]byte, []int) { + return file_plugin_proto_rawDescGZIP(), []int{46} } -func (x *WebAssetsResponse) GetFiles() []*WebAssetFile { +func (x *EventHandleResponse) GetSuccess() bool { if x != nil { - return x.Files + return x.Success } - return nil + return false } -func (x *WebAssetsResponse) GetHasAssets() bool { +func (x *EventHandleResponse) GetErrorMessage() string { if x != nil { - return x.HasAssets + return x.ErrorMessage } - return false + return "" } -// MiddlewareRequest OnForwardBegin 的输入:请求元数据 + 可选的 request body。 -type MiddlewareRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // === 核心元数据(默认总是填充)=== - RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` // core 为本次请求分配的唯一 ID(便于跨 Begin/End 关联) - UserId int64 `protobuf:"varint,2,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` - GroupId int64 `protobuf:"varint,3,opt,name=group_id,json=groupId,proto3" json:"group_id,omitempty"` - AccountId int64 `protobuf:"varint,4,opt,name=account_id,json=accountId,proto3" json:"account_id,omitempty"` // 已由 scheduler 选出 - Platform string `protobuf:"bytes,5,opt,name=platform,proto3" json:"platform,omitempty"` - Model string `protobuf:"bytes,6,opt,name=model,proto3" json:"model,omitempty"` - Stream bool `protobuf:"varint,7,opt,name=stream,proto3" json:"stream,omitempty"` - InputTokensEst int64 `protobuf:"varint,8,opt,name=input_tokens_est,json=inputTokensEst,proto3" json:"input_tokens_est,omitempty"` // core 侧粗略估算,仅用于早期决策 - // metadata KV bag:供多个 middleware 之间传递上下文(Open Question Q-open-3)。 - // 命名空间规则暂不强制,未来可能收紧。 - Metadata map[string]string `protobuf:"bytes,9,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - // === 按需字段(声明了 middleware.read_body capability 的插件才会收到)=== - RequestBody []byte `protobuf:"bytes,100,opt,name=request_body,json=requestBody,proto3" json:"request_body,omitempty"` - RequestHeaders map[string]*HeaderValues `protobuf:"bytes,101,rep,name=request_headers,json=requestHeaders,proto3" json:"request_headers,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` +type HostInvokeRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Method string `protobuf:"bytes,1,opt,name=method,proto3" json:"method,omitempty"` + Payload []byte `protobuf:"bytes,2,opt,name=payload,proto3" json:"payload,omitempty"` // JSON 编码的对象,由 Core method 自己校验 schema + IdempotencyKey string `protobuf:"bytes,3,opt,name=idempotency_key,json=idempotencyKey,proto3" json:"idempotency_key,omitempty"` // 副作用方法的幂等键;只读方法可留空 + Metadata map[string]string `protobuf:"bytes,4,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` // 调用级辅助信息,不用于替代权限、调度或核心业务字段 unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *MiddlewareRequest) Reset() { - *x = MiddlewareRequest{} - mi := &file_plugin_proto_msgTypes[57] +func (x *HostInvokeRequest) Reset() { + *x = HostInvokeRequest{} + mi := &file_plugin_proto_msgTypes[47] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *MiddlewareRequest) String() string { +func (x *HostInvokeRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*MiddlewareRequest) ProtoMessage() {} +func (*HostInvokeRequest) ProtoMessage() {} -func (x *MiddlewareRequest) ProtoReflect() protoreflect.Message { - mi := &file_plugin_proto_msgTypes[57] +func (x *HostInvokeRequest) ProtoReflect() protoreflect.Message { + mi := &file_plugin_proto_msgTypes[47] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4089,139 +3793,127 @@ func (x *MiddlewareRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use MiddlewareRequest.ProtoReflect.Descriptor instead. -func (*MiddlewareRequest) Descriptor() ([]byte, []int) { - return file_plugin_proto_rawDescGZIP(), []int{57} +// Deprecated: Use HostInvokeRequest.ProtoReflect.Descriptor instead. +func (*HostInvokeRequest) Descriptor() ([]byte, []int) { + return file_plugin_proto_rawDescGZIP(), []int{47} } -func (x *MiddlewareRequest) GetRequestId() string { +func (x *HostInvokeRequest) GetMethod() string { if x != nil { - return x.RequestId + return x.Method } return "" } -func (x *MiddlewareRequest) GetUserId() int64 { +func (x *HostInvokeRequest) GetPayload() []byte { if x != nil { - return x.UserId + return x.Payload } - return 0 + return nil } -func (x *MiddlewareRequest) GetGroupId() int64 { +func (x *HostInvokeRequest) GetIdempotencyKey() string { if x != nil { - return x.GroupId + return x.IdempotencyKey } - return 0 + return "" } -func (x *MiddlewareRequest) GetAccountId() int64 { +func (x *HostInvokeRequest) GetMetadata() map[string]string { if x != nil { - return x.AccountId + return x.Metadata } - return 0 + return nil } -func (x *MiddlewareRequest) GetPlatform() string { - if x != nil { - return x.Platform - } - return "" +type HostInvokeResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Status string `protobuf:"bytes,1,opt,name=status,proto3" json:"status,omitempty"` // method 业务状态;传输/鉴权/schema 错误走 gRPC error + Payload []byte `protobuf:"bytes,2,opt,name=payload,proto3" json:"payload,omitempty"` // JSON 编码的对象 + Metadata map[string]string `protobuf:"bytes,3,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` // 调用级辅助信息 + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *MiddlewareRequest) GetModel() string { - if x != nil { - return x.Model - } - return "" +func (x *HostInvokeResponse) Reset() { + *x = HostInvokeResponse{} + mi := &file_plugin_proto_msgTypes[48] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } -func (x *MiddlewareRequest) GetStream() bool { - if x != nil { - return x.Stream - } - return false +func (x *HostInvokeResponse) String() string { + return protoimpl.X.MessageStringOf(x) } -func (x *MiddlewareRequest) GetInputTokensEst() int64 { +func (*HostInvokeResponse) ProtoMessage() {} + +func (x *HostInvokeResponse) ProtoReflect() protoreflect.Message { + mi := &file_plugin_proto_msgTypes[48] if x != nil { - return x.InputTokensEst + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms } - return 0 + return mi.MessageOf(x) } -func (x *MiddlewareRequest) GetMetadata() map[string]string { +// Deprecated: Use HostInvokeResponse.ProtoReflect.Descriptor instead. +func (*HostInvokeResponse) Descriptor() ([]byte, []int) { + return file_plugin_proto_rawDescGZIP(), []int{48} +} + +func (x *HostInvokeResponse) GetStatus() string { if x != nil { - return x.Metadata + return x.Status } - return nil + return "" } -func (x *MiddlewareRequest) GetRequestBody() []byte { +func (x *HostInvokeResponse) GetPayload() []byte { if x != nil { - return x.RequestBody + return x.Payload } return nil } -func (x *MiddlewareRequest) GetRequestHeaders() map[string]*HeaderValues { +func (x *HostInvokeResponse) GetMetadata() map[string]string { if x != nil { - return x.RequestHeaders + return x.Metadata } return nil } -// MiddlewareEvent OnForwardEnd 的输入:完整的请求 + 响应元数据。 -type MiddlewareEvent struct { - state protoimpl.MessageState `protogen:"open.v1"` - // === 核心元数据(与 MiddlewareRequest 对齐)=== - RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` - UserId int64 `protobuf:"varint,2,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` - GroupId int64 `protobuf:"varint,3,opt,name=group_id,json=groupId,proto3" json:"group_id,omitempty"` - AccountId int64 `protobuf:"varint,4,opt,name=account_id,json=accountId,proto3" json:"account_id,omitempty"` - Platform string `protobuf:"bytes,5,opt,name=platform,proto3" json:"platform,omitempty"` - Model string `protobuf:"bytes,6,opt,name=model,proto3" json:"model,omitempty"` - Stream bool `protobuf:"varint,7,opt,name=stream,proto3" json:"stream,omitempty"` - InputTokensEst int64 `protobuf:"varint,8,opt,name=input_tokens_est,json=inputTokensEst,proto3" json:"input_tokens_est,omitempty"` // 与 MiddlewareRequest 字段对齐:core 侧粗略估算, - // === 响应结果 === - StatusCode int64 `protobuf:"varint,20,opt,name=status_code,json=statusCode,proto3" json:"status_code,omitempty"` - DurationMs int64 `protobuf:"varint,21,opt,name=duration_ms,json=durationMs,proto3" json:"duration_ms,omitempty"` - InputTokens int64 `protobuf:"varint,22,opt,name=input_tokens,json=inputTokens,proto3" json:"input_tokens,omitempty"` - OutputTokens int64 `protobuf:"varint,23,opt,name=output_tokens,json=outputTokens,proto3" json:"output_tokens,omitempty"` - CachedInputTokens int64 `protobuf:"varint,24,opt,name=cached_input_tokens,json=cachedInputTokens,proto3" json:"cached_input_tokens,omitempty"` - FirstTokenMs int64 `protobuf:"varint,25,opt,name=first_token_ms,json=firstTokenMs,proto3" json:"first_token_ms,omitempty"` - ErrorKind string `protobuf:"bytes,26,opt,name=error_kind,json=errorKind,proto3" json:"error_kind,omitempty"` // "" / "upstream_5xx" / "timeout" / "no_account" / ... - ErrorMsg string `protobuf:"bytes,27,opt,name=error_msg,json=errorMsg,proto3" json:"error_msg,omitempty"` // 限长 512,见 core 实现 - // 费用快照(core 已计算好) - InputCost float64 `protobuf:"fixed64,30,opt,name=input_cost,json=inputCost,proto3" json:"input_cost,omitempty"` - OutputCost float64 `protobuf:"fixed64,31,opt,name=output_cost,json=outputCost,proto3" json:"output_cost,omitempty"` - CachedInputCost float64 `protobuf:"fixed64,32,opt,name=cached_input_cost,json=cachedInputCost,proto3" json:"cached_input_cost,omitempty"` - // metadata 延续自 OnForwardBegin 的 bag - Metadata map[string]string `protobuf:"bytes,40,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - // === 按需字段(声明了 middleware.read_body capability 的插件才会收到)=== - // 流式响应时 response_body 只给摘要(首次非空 chunk 拼装),完整流式内容 - // 留给未来的 OnStreamChunk(ADR-0002)。 - ResponseBody []byte `protobuf:"bytes,100,opt,name=response_body,json=responseBody,proto3" json:"response_body,omitempty"` - ResponseHeaders map[string]*HeaderValues `protobuf:"bytes,101,rep,name=response_headers,json=responseHeaders,proto3" json:"response_headers,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *MiddlewareEvent) Reset() { - *x = MiddlewareEvent{} - mi := &file_plugin_proto_msgTypes[58] +type HostStreamFrame struct { + state protoimpl.MessageState `protogen:"open.v1"` + Method string `protobuf:"bytes,1,opt,name=method,proto3" json:"method,omitempty"` // 首个 client frame 必填;后续 frame 可留空 + Event string `protobuf:"bytes,2,opt,name=event,proto3" json:"event,omitempty"` // method 内部约定的帧类型 + Payload []byte `protobuf:"bytes,3,opt,name=payload,proto3" json:"payload,omitempty"` // JSON 编码的对象 + IdempotencyKey string `protobuf:"bytes,4,opt,name=idempotency_key,json=idempotencyKey,proto3" json:"idempotency_key,omitempty"` // 首个 client frame 使用 + Metadata map[string]string `protobuf:"bytes,5,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` // 调用级或帧级辅助信息 + Done bool `protobuf:"varint,6,opt,name=done,proto3" json:"done,omitempty"` // method 业务最终帧;传输结束仍以 stream EOF 为准 + Status string `protobuf:"bytes,7,opt,name=status,proto3" json:"status,omitempty"` // method 业务状态,通常只在最终帧使用 + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HostStreamFrame) Reset() { + *x = HostStreamFrame{} + mi := &file_plugin_proto_msgTypes[49] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *MiddlewareEvent) String() string { +func (x *HostStreamFrame) String() string { return protoimpl.X.MessageStringOf(x) } -func (*MiddlewareEvent) ProtoMessage() {} +func (*HostStreamFrame) ProtoMessage() {} -func (x *MiddlewareEvent) ProtoReflect() protoreflect.Message { - mi := &file_plugin_proto_msgTypes[58] +func (x *HostStreamFrame) ProtoReflect() protoreflect.Message { + mi := &file_plugin_proto_msgTypes[49] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4232,195 +3924,202 @@ func (x *MiddlewareEvent) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use MiddlewareEvent.ProtoReflect.Descriptor instead. -func (*MiddlewareEvent) Descriptor() ([]byte, []int) { - return file_plugin_proto_rawDescGZIP(), []int{58} +// Deprecated: Use HostStreamFrame.ProtoReflect.Descriptor instead. +func (*HostStreamFrame) Descriptor() ([]byte, []int) { + return file_plugin_proto_rawDescGZIP(), []int{49} } -func (x *MiddlewareEvent) GetRequestId() string { +func (x *HostStreamFrame) GetMethod() string { if x != nil { - return x.RequestId + return x.Method } return "" } -func (x *MiddlewareEvent) GetUserId() int64 { - if x != nil { - return x.UserId - } - return 0 -} - -func (x *MiddlewareEvent) GetGroupId() int64 { +func (x *HostStreamFrame) GetEvent() string { if x != nil { - return x.GroupId + return x.Event } - return 0 + return "" } -func (x *MiddlewareEvent) GetAccountId() int64 { +func (x *HostStreamFrame) GetPayload() []byte { if x != nil { - return x.AccountId + return x.Payload } - return 0 + return nil } -func (x *MiddlewareEvent) GetPlatform() string { +func (x *HostStreamFrame) GetIdempotencyKey() string { if x != nil { - return x.Platform + return x.IdempotencyKey } return "" } -func (x *MiddlewareEvent) GetModel() string { +func (x *HostStreamFrame) GetMetadata() map[string]string { if x != nil { - return x.Model + return x.Metadata } - return "" + return nil } -func (x *MiddlewareEvent) GetStream() bool { +func (x *HostStreamFrame) GetDone() bool { if x != nil { - return x.Stream + return x.Done } return false } -func (x *MiddlewareEvent) GetInputTokensEst() int64 { +func (x *HostStreamFrame) GetStatus() string { if x != nil { - return x.InputTokensEst + return x.Status } - return 0 + return "" } -func (x *MiddlewareEvent) GetStatusCode() int64 { - if x != nil { - return x.StatusCode - } - return 0 +type ProcessTaskRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + TaskId int64 `protobuf:"varint,1,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` + TaskType string `protobuf:"bytes,2,opt,name=task_type,json=taskType,proto3" json:"task_type,omitempty"` + Input []byte `protobuf:"bytes,3,opt,name=input,proto3" json:"input,omitempty"` // JSON + UserId int64 `protobuf:"varint,4,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *MiddlewareEvent) GetDurationMs() int64 { - if x != nil { - return x.DurationMs - } - return 0 +func (x *ProcessTaskRequest) Reset() { + *x = ProcessTaskRequest{} + mi := &file_plugin_proto_msgTypes[50] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } -func (x *MiddlewareEvent) GetInputTokens() int64 { - if x != nil { - return x.InputTokens - } - return 0 +func (x *ProcessTaskRequest) String() string { + return protoimpl.X.MessageStringOf(x) } -func (x *MiddlewareEvent) GetOutputTokens() int64 { +func (*ProcessTaskRequest) ProtoMessage() {} + +func (x *ProcessTaskRequest) ProtoReflect() protoreflect.Message { + mi := &file_plugin_proto_msgTypes[50] if x != nil { - return x.OutputTokens + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms } - return 0 + return mi.MessageOf(x) } -func (x *MiddlewareEvent) GetCachedInputTokens() int64 { - if x != nil { - return x.CachedInputTokens - } - return 0 +// Deprecated: Use ProcessTaskRequest.ProtoReflect.Descriptor instead. +func (*ProcessTaskRequest) Descriptor() ([]byte, []int) { + return file_plugin_proto_rawDescGZIP(), []int{50} } -func (x *MiddlewareEvent) GetFirstTokenMs() int64 { +func (x *ProcessTaskRequest) GetTaskId() int64 { if x != nil { - return x.FirstTokenMs + return x.TaskId } return 0 } -func (x *MiddlewareEvent) GetErrorKind() string { +func (x *ProcessTaskRequest) GetTaskType() string { if x != nil { - return x.ErrorKind + return x.TaskType } return "" } -func (x *MiddlewareEvent) GetErrorMsg() string { +func (x *ProcessTaskRequest) GetInput() []byte { if x != nil { - return x.ErrorMsg + return x.Input } - return "" + return nil } -func (x *MiddlewareEvent) GetInputCost() float64 { +func (x *ProcessTaskRequest) GetUserId() int64 { if x != nil { - return x.InputCost + return x.UserId } return 0 } -func (x *MiddlewareEvent) GetOutputCost() float64 { - if x != nil { - return x.OutputCost - } - return 0 +type ProcessTaskResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` + ErrorMessage string `protobuf:"bytes,2,opt,name=error_message,json=errorMessage,proto3" json:"error_message,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *MiddlewareEvent) GetCachedInputCost() float64 { - if x != nil { - return x.CachedInputCost - } - return 0 +func (x *ProcessTaskResponse) Reset() { + *x = ProcessTaskResponse{} + mi := &file_plugin_proto_msgTypes[51] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } -func (x *MiddlewareEvent) GetMetadata() map[string]string { +func (x *ProcessTaskResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProcessTaskResponse) ProtoMessage() {} + +func (x *ProcessTaskResponse) ProtoReflect() protoreflect.Message { + mi := &file_plugin_proto_msgTypes[51] if x != nil { - return x.Metadata + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms } - return nil + return mi.MessageOf(x) } -func (x *MiddlewareEvent) GetResponseBody() []byte { +// Deprecated: Use ProcessTaskResponse.ProtoReflect.Descriptor instead. +func (*ProcessTaskResponse) Descriptor() ([]byte, []int) { + return file_plugin_proto_rawDescGZIP(), []int{51} +} + +func (x *ProcessTaskResponse) GetSuccess() bool { if x != nil { - return x.ResponseBody + return x.Success } - return nil + return false } -func (x *MiddlewareEvent) GetResponseHeaders() map[string]*HeaderValues { +func (x *ProcessTaskResponse) GetErrorMessage() string { if x != nil { - return x.ResponseHeaders + return x.ErrorMessage } - return nil + return "" } -// MiddlewareDecision OnForwardBegin 的输出:放行 / 拒绝 / 改请求。 -type MiddlewareDecision struct { - state protoimpl.MessageState `protogen:"open.v1"` - Action MiddlewareDecision_Action `protobuf:"varint,1,opt,name=action,proto3,enum=airgate.plugin.v1.MiddlewareDecision_Action" json:"action,omitempty"` - // action=DENY 时的错误码 / 文案(对用户可见) - DenyStatusCode int32 `protobuf:"varint,10,opt,name=deny_status_code,json=denyStatusCode,proto3" json:"deny_status_code,omitempty"` // 默认 403 if Action=DENY and 未指定 - DenyMessage string `protobuf:"bytes,11,opt,name=deny_message,json=denyMessage,proto3" json:"deny_message,omitempty"` - // action=MUTATE 时要追加/覆盖的请求头 - SetHeaders map[string]*HeaderValues `protobuf:"bytes,20,rep,name=set_headers,json=setHeaders,proto3" json:"set_headers,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - // 贯穿式 metadata:无论 allow/deny/mutate,都能往 bag 里写东西供后续 middleware / End 使用 - Metadata map[string]string `protobuf:"bytes,30,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` +type TaskTypesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Types []string `protobuf:"bytes,1,rep,name=types,proto3" json:"types,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *MiddlewareDecision) Reset() { - *x = MiddlewareDecision{} - mi := &file_plugin_proto_msgTypes[59] +func (x *TaskTypesResponse) Reset() { + *x = TaskTypesResponse{} + mi := &file_plugin_proto_msgTypes[52] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *MiddlewareDecision) String() string { +func (x *TaskTypesResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*MiddlewareDecision) ProtoMessage() {} +func (*TaskTypesResponse) ProtoMessage() {} -func (x *MiddlewareDecision) ProtoReflect() protoreflect.Message { - mi := &file_plugin_proto_msgTypes[59] +func (x *TaskTypesResponse) ProtoReflect() protoreflect.Message { + mi := &file_plugin_proto_msgTypes[52] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4431,42 +4130,14 @@ func (x *MiddlewareDecision) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use MiddlewareDecision.ProtoReflect.Descriptor instead. -func (*MiddlewareDecision) Descriptor() ([]byte, []int) { - return file_plugin_proto_rawDescGZIP(), []int{59} -} - -func (x *MiddlewareDecision) GetAction() MiddlewareDecision_Action { - if x != nil { - return x.Action - } - return MiddlewareDecision_ALLOW -} - -func (x *MiddlewareDecision) GetDenyStatusCode() int32 { - if x != nil { - return x.DenyStatusCode - } - return 0 -} - -func (x *MiddlewareDecision) GetDenyMessage() string { - if x != nil { - return x.DenyMessage - } - return "" -} - -func (x *MiddlewareDecision) GetSetHeaders() map[string]*HeaderValues { - if x != nil { - return x.SetHeaders - } - return nil +// Deprecated: Use TaskTypesResponse.ProtoReflect.Descriptor instead. +func (*TaskTypesResponse) Descriptor() ([]byte, []int) { + return file_plugin_proto_rawDescGZIP(), []int{52} } -func (x *MiddlewareDecision) GetMetadata() map[string]string { +func (x *TaskTypesResponse) GetTypes() []string { if x != nil { - return x.Metadata + return x.Types } return nil } @@ -4475,135 +4146,12 @@ var File_plugin_proto protoreflect.FileDescriptor const file_plugin_proto_rawDesc = "" + "\n" + - "\fplugin.proto\x12\x11airgate.plugin.v1\"\x9a\x01\n" + - "\x18HostSelectAccountRequest\x12\x19\n" + - "\bgroup_id\x18\x01 \x01(\x03R\agroupId\x12\x14\n" + - "\x05model\x18\x02 \x01(\tR\x05model\x12\x1d\n" + - "\n" + - "session_id\x18\x03 \x01(\tR\tsessionId\x12.\n" + - "\x13exclude_account_ids\x18\x04 \x03(\x03R\x11excludeAccountIds\"y\n" + - "\x19HostSelectAccountResponse\x12\x1d\n" + - "\n" + - "account_id\x18\x01 \x01(\x03R\taccountId\x12!\n" + - "\faccount_name\x18\x02 \x01(\tR\vaccountName\x12\x1a\n" + - "\bplatform\x18\x03 \x01(\tR\bplatform\"J\n" + - "\x17HostProbeForwardRequest\x12\x19\n" + - "\bgroup_id\x18\x01 \x01(\x03R\agroupId\x12\x14\n" + - "\x05model\x18\x02 \x01(\tR\x05model\"\x81\x02\n" + - "\x18HostProbeForwardResponse\x12\x18\n" + - "\asuccess\x18\x01 \x01(\bR\asuccess\x12\x1d\n" + - "\n" + - "account_id\x18\x02 \x01(\x03R\taccountId\x12\x1a\n" + - "\bplatform\x18\x03 \x01(\tR\bplatform\x12\x14\n" + - "\x05model\x18\x04 \x01(\tR\x05model\x12\x1f\n" + - "\vstatus_code\x18\x05 \x01(\x03R\n" + - "statusCode\x12\x1d\n" + - "\n" + - "latency_ms\x18\x06 \x01(\x03R\tlatencyMs\x12\x1d\n" + - "\n" + - "error_kind\x18\a \x01(\tR\terrorKind\x12\x1b\n" + - "\terror_msg\x18\b \x01(\tR\berrorMsg\"\x17\n" + - "\x15HostListGroupsRequest\"\x97\x01\n" + - "\tHostGroup\x12\x0e\n" + - "\x02id\x18\x01 \x01(\x03R\x02id\x12\x12\n" + - "\x04name\x18\x02 \x01(\tR\x04name\x12\x1a\n" + - "\bplatform\x18\x03 \x01(\tR\bplatform\x12!\n" + - "\fis_exclusive\x18\x04 \x01(\bR\visExclusive\x12'\n" + - "\x0frate_multiplier\x18\x05 \x01(\x01R\x0erateMultiplier\"N\n" + - "\x16HostListGroupsResponse\x124\n" + - "\x06groups\x18\x01 \x03(\v2\x1c.airgate.plugin.v1.HostGroupR\x06groups\"v\n" + - "\x1eHostReportAccountResultRequest\x12\x1d\n" + - "\n" + - "account_id\x18\x01 \x01(\x03R\taccountId\x12\x18\n" + - "\asuccess\x18\x02 \x01(\bR\asuccess\x12\x1b\n" + - "\terror_msg\x18\x03 \x01(\tR\berrorMsg\"\xe1\x02\n" + - "\x12HostForwardRequest\x12\x17\n" + - "\auser_id\x18\x01 \x01(\x03R\x06userId\x12\x19\n" + - "\bgroup_id\x18\x02 \x01(\x03R\agroupId\x12\x14\n" + - "\x05model\x18\x03 \x01(\tR\x05model\x12\x16\n" + - "\x06method\x18\x04 \x01(\tR\x06method\x12\x12\n" + - "\x04path\x18\x05 \x01(\tR\x04path\x12L\n" + - "\aheaders\x18\x06 \x03(\v22.airgate.plugin.v1.HostForwardRequest.HeadersEntryR\aheaders\x12\x12\n" + - "\x04body\x18\a \x01(\fR\x04body\x12\x16\n" + - "\x06stream\x18\b \x01(\bR\x06stream\x1a[\n" + - "\fHeadersEntry\x12\x10\n" + - "\x03key\x18\x01 \x01(\tR\x03key\x125\n" + - "\x05value\x18\x02 \x01(\v2\x1f.airgate.plugin.v1.HeaderValuesR\x05value:\x028\x01\"\xb1\x02\n" + - "\x13HostForwardResponse\x12\x1f\n" + - "\vstatus_code\x18\x01 \x01(\x05R\n" + - "statusCode\x12M\n" + - "\aheaders\x18\x02 \x03(\v23.airgate.plugin.v1.HostForwardResponse.HeadersEntryR\aheaders\x12\x12\n" + - "\x04body\x18\x03 \x01(\fR\x04body\x129\n" + - "\x05usage\x18\x04 \x01(\v2#.airgate.plugin.v1.HostForwardUsageR\x05usage\x1a[\n" + - "\fHeadersEntry\x12\x10\n" + - "\x03key\x18\x01 \x01(\tR\x03key\x125\n" + - "\x05value\x18\x02 \x01(\v2\x1f.airgate.plugin.v1.HeaderValuesR\x05value:\x028\x01\"\xbf\x02\n" + - "\x10HostForwardChunk\x12\x12\n" + - "\x04data\x18\x01 \x01(\fR\x04data\x12\x12\n" + - "\x04done\x18\x02 \x01(\bR\x04done\x12\x1f\n" + - "\vstatus_code\x18\x03 \x01(\x05R\n" + - "statusCode\x12J\n" + - "\aheaders\x18\x04 \x03(\v20.airgate.plugin.v1.HostForwardChunk.HeadersEntryR\aheaders\x129\n" + - "\x05usage\x18\x05 \x01(\v2#.airgate.plugin.v1.HostForwardUsageR\x05usage\x1a[\n" + - "\fHeadersEntry\x12\x10\n" + - "\x03key\x18\x01 \x01(\tR\x03key\x125\n" + - "\x05value\x18\x02 \x01(\v2\x1f.airgate.plugin.v1.HeaderValuesR\x05value:\x028\x01\"\x84\x01\n" + - "\x10HostForwardUsage\x12!\n" + - "\finput_tokens\x18\x01 \x01(\x03R\vinputTokens\x12#\n" + - "\routput_tokens\x18\x02 \x01(\x03R\foutputTokens\x12\x12\n" + - "\x04cost\x18\x03 \x01(\x01R\x04cost\x12\x14\n" + - "\x05model\x18\x04 \x01(\tR\x05model\"\x1a\n" + - "\x18HostListPlatformsRequest\"E\n" + - "\fHostPlatform\x12\x12\n" + - "\x04name\x18\x01 \x01(\tR\x04name\x12!\n" + - "\fdisplay_name\x18\x02 \x01(\tR\vdisplayName\"Z\n" + - "\x19HostListPlatformsResponse\x12=\n" + - "\tplatforms\x18\x01 \x03(\v2\x1f.airgate.plugin.v1.HostPlatformR\tplatforms\"3\n" + - "\x15HostListModelsRequest\x12\x1a\n" + - "\bplatform\x18\x01 \x01(\tR\bplatform\"S\n" + - "\x16HostListModelsResponse\x129\n" + - "\x06models\x18\x01 \x03(\v2!.airgate.plugin.v1.ModelInfoProtoR\x06models\"1\n" + - "\x16HostGetUserInfoRequest\x12\x17\n" + - "\auser_id\x18\x01 \x01(\x03R\x06userId\"\xaa\x01\n" + - "\x17HostGetUserInfoResponse\x12\x17\n" + - "\auser_id\x18\x01 \x01(\x03R\x06userId\x12\x1a\n" + - "\busername\x18\x02 \x01(\tR\busername\x12\x14\n" + - "\x05email\x18\x03 \x01(\tR\x05email\x12\x12\n" + - "\x04role\x18\x04 \x01(\tR\x04role\x12\x18\n" + - "\abalance\x18\x05 \x01(\x01R\abalance\x12\x16\n" + - "\x06status\x18\x06 \x01(\tR\x06status\"\xa4\x01\n" + - "\x15HostStoreAssetRequest\x12\x17\n" + - "\auser_id\x18\x01 \x01(\x03R\x06userId\x12\x14\n" + - "\x05scope\x18\x02 \x01(\tR\x05scope\x12!\n" + - "\fcontent_type\x18\x03 \x01(\tR\vcontentType\x12\x12\n" + - "\x04data\x18\x04 \x01(\fR\x04data\x12%\n" + - "\x0efile_extension\x18\x05 \x01(\tR\rfileExtension\"\xb3\x01\n" + - "\x16HostStoreAssetResponse\x12\x19\n" + - "\basset_id\x18\x01 \x01(\tR\aassetId\x12\x1d\n" + - "\n" + - "object_key\x18\x02 \x01(\tR\tobjectKey\x12\x1d\n" + - "\n" + - "public_url\x18\x03 \x01(\tR\tpublicUrl\x12\x1d\n" + - "\n" + - "size_bytes\x18\x04 \x01(\x03R\tsizeBytes\x12!\n" + - "\fcontent_type\x18\x05 \x01(\tR\vcontentType\"7\n" + - "\x16HostGetAssetURLRequest\x12\x1d\n" + - "\n" + - "object_key\x18\x01 \x01(\tR\tobjectKey\"8\n" + - "\x17HostGetAssetURLResponse\x12\x1d\n" + - "\n" + - "public_url\x18\x01 \x01(\tR\tpublicUrl\"9\n" + - "\x18HostGetAssetBytesRequest\x12\x1d\n" + - "\n" + - "object_key\x18\x01 \x01(\tR\tobjectKey\"R\n" + - "\x19HostGetAssetBytesResponse\x12\x12\n" + - "\x04data\x18\x01 \x01(\fR\x04data\x12!\n" + - "\fcontent_type\x18\x02 \x01(\tR\vcontentType\"\a\n" + + "\fplugin.proto\x12\x11airgate.plugin.v1\"\a\n" + "\x05Empty\"&\n" + "\x0eStringResponse\x12\x14\n" + "\x05value\x18\x01 \x01(\tR\x05value\"&\n" + "\fHeaderValues\x12\x16\n" + - "\x06values\x18\x01 \x03(\tR\x06values\"\x8a\x05\n" + + "\x06values\x18\x01 \x03(\tR\x06values\"\x98\x06\n" + "\x12PluginInfoResponse\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" + "\x04name\x18\x02 \x01(\tR\x04name\x12\x18\n" + @@ -4621,7 +4169,11 @@ const file_plugin_proto_rawDesc = "" + "\rconfig_schema\x18\f \x03(\v2#.airgate.plugin.v1.ConfigFieldProtoR\fconfigSchema\x12/\n" + "\x13instruction_presets\x18\r \x03(\tR\x12instructionPresets\x12\"\n" + "\fcapabilities\x18\x0e \x03(\tR\fcapabilities\x12\x1a\n" + - "\bpriority\x18\x0f \x01(\x05R\bpriority\"\xd3\x01\n" + + "\bpriority\x18\x0f \x01(\x05R\bpriority\x12O\n" + + "\bmetadata\x18\x10 \x03(\v23.airgate.plugin.v1.PluginInfoResponse.MetadataEntryR\bmetadata\x1a;\n" + + "\rMetadataEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xd3\x01\n" + "\x10ConfigFieldProto\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05label\x18\x02 \x01(\tR\x05label\x12\x12\n" + @@ -4652,35 +4204,34 @@ const file_plugin_proto_rawDesc = "" + "\x04slot\x18\x01 \x01(\tR\x04slot\x12\x1d\n" + "\n" + "entry_file\x18\x02 \x01(\tR\tentryFile\x12\x14\n" + - "\x05title\x18\x03 \x01(\tR\x05title\"\xcf\x01\n" + + "\x05title\x18\x03 \x01(\tR\x05title\"\xdc\x01\n" + "\vInitRequest\x12B\n" + "\x06config\x18\x01 \x03(\v2*.airgate.plugin.v1.InitRequest.ConfigEntryR\x06config\x12\x1b\n" + - "\tlog_level\x18\x02 \x01(\tR\blogLevel\x12$\n" + - "\x0ehost_broker_id\x18\x03 \x01(\rR\fhostBrokerId\x1a9\n" + + "\tlog_level\x18\x02 \x01(\tR\blogLevel\x121\n" + + "\x15core_invoke_broker_id\x18\x03 \x01(\rR\x12coreInvokeBrokerId\x1a9\n" + "\vConfigEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\x8d\x04\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xb5\x02\n" + "\x0eModelInfoProto\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" + - "\x04name\x18\x02 \x01(\tR\x04name\x12\x1f\n" + - "\vinput_price\x18\x04 \x01(\x01R\n" + - "inputPrice\x12!\n" + - "\foutput_price\x18\x05 \x01(\x01R\voutputPrice\x12,\n" + - "\x12cached_input_price\x18\x06 \x01(\x01R\x10cachedInputPrice\x120\n" + - "\x14input_price_priority\x18\a \x01(\x01R\x12inputPricePriority\x122\n" + - "\x15output_price_priority\x18\b \x01(\x01R\x13outputPricePriority\x12=\n" + - "\x1bcached_input_price_priority\x18\t \x01(\x01R\x18cachedInputPricePriority\x12%\n" + - "\x0econtext_window\x18\n" + - " \x01(\x03R\rcontextWindow\x12*\n" + - "\x11max_output_tokens\x18\v \x01(\x03R\x0fmaxOutputTokens\x120\n" + - "\x14cache_creation_price\x18\f \x01(\x01R\x12cacheCreationPrice\x125\n" + - "\x17cache_creation_1h_price\x18\r \x01(\x01R\x14cacheCreation1hPriceJ\x04\b\x03\x10\x04\"K\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x12%\n" + + "\x0econtext_window\x18\x03 \x01(\x03R\rcontextWindow\x12*\n" + + "\x11max_output_tokens\x18\x04 \x01(\x03R\x0fmaxOutputTokens\x12\"\n" + + "\fcapabilities\x18\x05 \x03(\tR\fcapabilities\x12K\n" + + "\bmetadata\x18\x06 \x03(\v2/.airgate.plugin.v1.ModelInfoProto.MetadataEntryR\bmetadata\x1a;\n" + + "\rMetadataEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"K\n" + "\x0eModelsResponse\x129\n" + - "\x06models\x18\x01 \x03(\v2!.airgate.plugin.v1.ModelInfoProtoR\x06models\"d\n" + + "\x06models\x18\x01 \x03(\v2!.airgate.plugin.v1.ModelInfoProtoR\x06models\"\xf4\x01\n" + "\x14RouteDefinitionProto\x12\x16\n" + "\x06method\x18\x01 \x01(\tR\x06method\x12\x12\n" + "\x04path\x18\x02 \x01(\tR\x04path\x12 \n" + - "\vdescription\x18\x03 \x01(\tR\vdescription\"Q\n" + + "\vdescription\x18\x03 \x01(\tR\vdescription\x12Q\n" + + "\bmetadata\x18\x04 \x03(\v25.airgate.plugin.v1.RouteDefinitionProto.MetadataEntryR\bmetadata\x1a;\n" + + "\rMetadataEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"Q\n" + "\x0eRoutesResponse\x12?\n" + "\x06routes\x18\x01 \x03(\v2'.airgate.plugin.v1.RouteDefinitionProtoR\x06routes\"\xaa\x01\n" + "\fAccountProto\x12\x0e\n" + @@ -4707,33 +4258,57 @@ const file_plugin_proto_rawDesc = "" + "\x04body\x18\x03 \x01(\fR\x04body\x1a[\n" + "\fHeadersEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x125\n" + - "\x05value\x18\x02 \x01(\v2\x1f.airgate.plugin.v1.HeaderValuesR\x05value:\x028\x01\"\xd2\x06\n" + - "\x05Usage\x12!\n" + - "\finput_tokens\x18\x01 \x01(\x03R\vinputTokens\x12#\n" + - "\routput_tokens\x18\x02 \x01(\x03R\foutputTokens\x12.\n" + - "\x13cached_input_tokens\x18\x03 \x01(\x03R\x11cachedInputTokens\x122\n" + - "\x15cache_creation_tokens\x18\x04 \x01(\x03R\x13cacheCreationTokens\x127\n" + - "\x18cache_creation_5m_tokens\x18\x05 \x01(\x03R\x15cacheCreation5mTokens\x127\n" + - "\x18cache_creation_1h_tokens\x18\x06 \x01(\x03R\x15cacheCreation1hTokens\x126\n" + - "\x17reasoning_output_tokens\x18\a \x01(\x03R\x15reasoningOutputTokens\x12\x1d\n" + - "\n" + - "input_cost\x18\n" + - " \x01(\x01R\tinputCost\x12\x1f\n" + - "\voutput_cost\x18\v \x01(\x01R\n" + - "outputCost\x12*\n" + - "\x11cached_input_cost\x18\f \x01(\x01R\x0fcachedInputCost\x12.\n" + - "\x13cache_creation_cost\x18\r \x01(\x01R\x11cacheCreationCost\x12\x1f\n" + - "\vinput_price\x18\x14 \x01(\x01R\n" + - "inputPrice\x12!\n" + - "\foutput_price\x18\x15 \x01(\x01R\voutputPrice\x12,\n" + - "\x12cached_input_price\x18\x16 \x01(\x01R\x10cachedInputPrice\x120\n" + - "\x14cache_creation_price\x18\x17 \x01(\x01R\x12cacheCreationPrice\x125\n" + - "\x17cache_creation_1h_price\x18\x18 \x01(\x01R\x14cacheCreation1hPrice\x12\x14\n" + - "\x05model\x18\x1e \x01(\tR\x05model\x12!\n" + - "\fservice_tier\x18\x1f \x01(\tR\vserviceTier\x12$\n" + - "\x0efirst_token_ms\x18 \x01(\x03R\ffirstTokenMs\x12\x1d\n" + + "\x05value\x18\x02 \x01(\v2\x1f.airgate.plugin.v1.HeaderValuesR\x05value:\x028\x01\"\xec\x01\n" + + "\x0eUsageAttribute\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05label\x18\x02 \x01(\tR\x05label\x12\x12\n" + + "\x04kind\x18\x03 \x01(\tR\x04kind\x12\x14\n" + + "\x05value\x18\x04 \x01(\tR\x05value\x12K\n" + + "\bmetadata\x18\x05 \x03(\v2/.airgate.plugin.v1.UsageAttribute.MetadataEntryR\bmetadata\x1a;\n" + + "\rMetadataEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xb9\x02\n" + + "\vUsageMetric\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05label\x18\x02 \x01(\tR\x05label\x12\x12\n" + + "\x04kind\x18\x03 \x01(\tR\x04kind\x12\x12\n" + + "\x04unit\x18\x04 \x01(\tR\x04unit\x12\x14\n" + + "\x05value\x18\x05 \x01(\x01R\x05value\x12!\n" + + "\faccount_cost\x18\x06 \x01(\x01R\vaccountCost\x12\x1a\n" + + "\bcurrency\x18\a \x01(\tR\bcurrency\x12H\n" + + "\bmetadata\x18\b \x03(\v2,.airgate.plugin.v1.UsageMetric.MetadataEntryR\bmetadata\x1a;\n" + + "\rMetadataEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xcf\x02\n" + + "\x0fUsageCostDetail\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05label\x18\x02 \x01(\tR\x05label\x12!\n" + + "\faccount_cost\x18\x03 \x01(\x01R\vaccountCost\x12\x1b\n" + + "\tuser_cost\x18\x04 \x01(\x01R\buserCost\x12-\n" + + "\x12billing_multiplier\x18\x05 \x01(\x01R\x11billingMultiplier\x12\x1a\n" + + "\bcurrency\x18\x06 \x01(\tR\bcurrency\x12L\n" + + "\bmetadata\x18\a \x03(\v20.airgate.plugin.v1.UsageCostDetail.MetadataEntryR\bmetadata\x1a;\n" + + "\rMetadataEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xad\x04\n" + + "\x05Usage\x12\x14\n" + + "\x05model\x18\x01 \x01(\tR\x05model\x12!\n" + + "\faccount_cost\x18\x02 \x01(\x01R\vaccountCost\x12\x1b\n" + + "\tuser_cost\x18\x03 \x01(\x01R\buserCost\x12-\n" + + "\x12billing_multiplier\x18\x04 \x01(\x01R\x11billingMultiplier\x12\x1a\n" + + "\bcurrency\x18\x05 \x01(\tR\bcurrency\x12\x18\n" + + "\asummary\x18\x06 \x01(\tR\asummary\x12$\n" + + "\x0efirst_token_ms\x18\a \x01(\x03R\ffirstTokenMs\x128\n" + + "\ametrics\x18\b \x03(\v2\x1e.airgate.plugin.v1.UsageMetricR\ametrics\x12A\n" + "\n" + - "image_size\x18! \x01(\tR\timageSize\"\xc7\x03\n" + + "attributes\x18\t \x03(\v2!.airgate.plugin.v1.UsageAttributeR\n" + + "attributes\x12E\n" + + "\fcost_details\x18\n" + + " \x03(\v2\".airgate.plugin.v1.UsageCostDetailR\vcostDetails\x12B\n" + + "\bmetadata\x18\v \x03(\v2&.airgate.plugin.v1.Usage.MetadataEntryR\bmetadata\x1a;\n" + + "\rMetadataEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xc7\x03\n" + "\x0eForwardOutcome\x122\n" + "\x04kind\x18\x01 \x01(\x0e2\x1e.airgate.plugin.v1.OutcomeKindR\x04kind\x12?\n" + "\bupstream\x18\x02 \x01(\v2#.airgate.plugin.v1.UpstreamResponseR\bupstream\x12.\n" + @@ -4760,18 +4335,6 @@ const file_plugin_proto_rawDesc = "" + "\vcredentials\x18\x01 \x03(\v26.airgate.plugin.v1.CredentialsRequest.CredentialsEntryR\vcredentials\x1a>\n" + "\x10CredentialsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\x97\x02\n" + - "\x11QuotaInfoResponse\x12\x14\n" + - "\x05total\x18\x01 \x01(\x01R\x05total\x12\x12\n" + - "\x04used\x18\x02 \x01(\x01R\x04used\x12\x1c\n" + - "\tremaining\x18\x03 \x01(\x01R\tremaining\x12\x1a\n" + - "\bcurrency\x18\x04 \x01(\tR\bcurrency\x12\x1d\n" + - "\n" + - "expires_at\x18\x05 \x01(\tR\texpiresAt\x12E\n" + - "\x05extra\x18\x06 \x03(\v2/.airgate.plugin.v1.QuotaInfoResponse.ExtraEntryR\x05extra\x1a8\n" + - "\n" + - "ExtraEntry\x12\x10\n" + - "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xa8\x02\n" + "\vHttpRequest\x12\x16\n" + "\x06method\x18\x01 \x01(\tR\x06method\x12\x12\n" + @@ -4844,7 +4407,64 @@ const file_plugin_proto_rawDesc = "" + "\x11WebAssetsResponse\x125\n" + "\x05files\x18\x01 \x03(\v2\x1f.airgate.plugin.v1.WebAssetFileR\x05files\x12\x1d\n" + "\n" + - "has_assets\x18\x02 \x01(\bR\thasAssets\"\xf0\x04\n" + + "has_assets\x18\x02 \x01(\bR\thasAssets\"\xf7\x01\n" + + "\x12PayloadSchemaProto\x12!\n" + + "\fcontent_type\x18\x01 \x01(\tR\vcontentType\x12\x16\n" + + "\x06schema\x18\x02 \x01(\tR\x06schema\x12\x18\n" + + "\aexample\x18\x03 \x01(\tR\aexample\x12O\n" + + "\bmetadata\x18\x04 \x03(\v23.airgate.plugin.v1.PayloadSchemaProto.MetadataEntryR\bmetadata\x1a;\n" + + "\rMetadataEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xe8\x02\n" + + "\x10RouteSchemaProto\x12\x16\n" + + "\x06method\x18\x01 \x01(\tR\x06method\x12\x12\n" + + "\x04path\x18\x02 \x01(\tR\x04path\x12\x18\n" + + "\asummary\x18\x03 \x01(\tR\asummary\x12?\n" + + "\arequest\x18\x04 \x01(\v2%.airgate.plugin.v1.PayloadSchemaProtoR\arequest\x12A\n" + + "\bresponse\x18\x05 \x01(\v2%.airgate.plugin.v1.PayloadSchemaProtoR\bresponse\x12M\n" + + "\bmetadata\x18\x06 \x03(\v21.airgate.plugin.v1.RouteSchemaProto.MetadataEntryR\bmetadata\x1a;\n" + + "\rMetadataEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xc6\x02\n" + + "\x0fTaskSchemaProto\x12\x12\n" + + "\x04type\x18\x01 \x01(\tR\x04type\x12\x18\n" + + "\asummary\x18\x02 \x01(\tR\asummary\x12;\n" + + "\x05input\x18\x03 \x01(\v2%.airgate.plugin.v1.PayloadSchemaProtoR\x05input\x12=\n" + + "\x06output\x18\x04 \x01(\v2%.airgate.plugin.v1.PayloadSchemaProtoR\x06output\x12L\n" + + "\bmetadata\x18\x05 \x03(\v20.airgate.plugin.v1.TaskSchemaProto.MetadataEntryR\bmetadata\x1a;\n" + + "\rMetadataEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xa5\x02\n" + + "\x10EventSchemaProto\x12\x12\n" + + "\x04type\x18\x01 \x01(\tR\x04type\x12\x16\n" + + "\x06source\x18\x02 \x01(\tR\x06source\x12\x18\n" + + "\asummary\x18\x03 \x01(\tR\asummary\x12?\n" + + "\apayload\x18\x04 \x01(\v2%.airgate.plugin.v1.PayloadSchemaProtoR\apayload\x12M\n" + + "\bmetadata\x18\x05 \x03(\v21.airgate.plugin.v1.EventSchemaProto.MetadataEntryR\bmetadata\x1a;\n" + + "\rMetadataEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\x88\x04\n" + + "\x11InvokeSchemaProto\x12\x16\n" + + "\x06method\x18\x01 \x01(\tR\x06method\x12\x18\n" + + "\asummary\x18\x02 \x01(\tR\asummary\x12?\n" + + "\arequest\x18\x03 \x01(\v2%.airgate.plugin.v1.PayloadSchemaProtoR\arequest\x12A\n" + + "\bresponse\x18\x04 \x01(\v2%.airgate.plugin.v1.PayloadSchemaProtoR\bresponse\x12N\n" + + "\bmetadata\x18\x05 \x03(\v22.airgate.plugin.v1.InvokeSchemaProto.MetadataEntryR\bmetadata\x12\x1c\n" + + "\ttransport\x18\x06 \x01(\tR\ttransport\x12H\n" + + "\fclient_frame\x18\a \x01(\v2%.airgate.plugin.v1.PayloadSchemaProtoR\vclientFrame\x12H\n" + + "\fserver_frame\x18\b \x01(\v2%.airgate.plugin.v1.PayloadSchemaProtoR\vserverFrame\x1a;\n" + + "\rMetadataEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\x9a\x03\n" + + "\x14PluginSchemaResponse\x12;\n" + + "\x06routes\x18\x01 \x03(\v2#.airgate.plugin.v1.RouteSchemaProtoR\x06routes\x128\n" + + "\x05tasks\x18\x02 \x03(\v2\".airgate.plugin.v1.TaskSchemaProtoR\x05tasks\x12;\n" + + "\x06events\x18\x03 \x03(\v2#.airgate.plugin.v1.EventSchemaProtoR\x06events\x12>\n" + + "\ainvokes\x18\x04 \x03(\v2$.airgate.plugin.v1.InvokeSchemaProtoR\ainvokes\x12Q\n" + + "\bmetadata\x18\x05 \x03(\v25.airgate.plugin.v1.PluginSchemaResponse.MetadataEntryR\bmetadata\x1a;\n" + + "\rMetadataEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\x84\x05\n" + "\x11MiddlewareRequest\x12\x1d\n" + "\n" + "request_id\x18\x01 \x01(\tR\trequestId\x12\x17\n" + @@ -4854,8 +4474,8 @@ const file_plugin_proto_rawDesc = "" + "account_id\x18\x04 \x01(\x03R\taccountId\x12\x1a\n" + "\bplatform\x18\x05 \x01(\tR\bplatform\x12\x14\n" + "\x05model\x18\x06 \x01(\tR\x05model\x12\x16\n" + - "\x06stream\x18\a \x01(\bR\x06stream\x12(\n" + - "\x10input_tokens_est\x18\b \x01(\x03R\x0einputTokensEst\x12N\n" + + "\x06stream\x18\a \x01(\bR\x06stream\x12<\n" + + "\testimates\x18\b \x03(\v2\x1e.airgate.plugin.v1.UsageMetricR\testimates\x12N\n" + "\bmetadata\x18\t \x03(\v22.airgate.plugin.v1.MiddlewareRequest.MetadataEntryR\bmetadata\x12!\n" + "\frequest_body\x18d \x01(\fR\vrequestBody\x12a\n" + "\x0frequest_headers\x18e \x03(\v28.airgate.plugin.v1.MiddlewareRequest.RequestHeadersEntryR\x0erequestHeaders\x1a;\n" + @@ -4864,7 +4484,7 @@ const file_plugin_proto_rawDesc = "" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1ab\n" + "\x13RequestHeadersEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x125\n" + - "\x05value\x18\x02 \x01(\v2\x1f.airgate.plugin.v1.HeaderValuesR\x05value:\x028\x01\"\xf8\a\n" + + "\x05value\x18\x02 \x01(\v2\x1f.airgate.plugin.v1.HeaderValuesR\x05value:\x028\x01\"\xb2\x06\n" + "\x0fMiddlewareEvent\x12\x1d\n" + "\n" + "request_id\x18\x01 \x01(\tR\trequestId\x12\x17\n" + @@ -4874,24 +4494,16 @@ const file_plugin_proto_rawDesc = "" + "account_id\x18\x04 \x01(\x03R\taccountId\x12\x1a\n" + "\bplatform\x18\x05 \x01(\tR\bplatform\x12\x14\n" + "\x05model\x18\x06 \x01(\tR\x05model\x12\x16\n" + - "\x06stream\x18\a \x01(\bR\x06stream\x12(\n" + - "\x10input_tokens_est\x18\b \x01(\x03R\x0einputTokensEst\x12\x1f\n" + + "\x06stream\x18\a \x01(\bR\x06stream\x12<\n" + + "\testimates\x18\b \x03(\v2\x1e.airgate.plugin.v1.UsageMetricR\testimates\x12\x1f\n" + "\vstatus_code\x18\x14 \x01(\x03R\n" + "statusCode\x12\x1f\n" + "\vduration_ms\x18\x15 \x01(\x03R\n" + - "durationMs\x12!\n" + - "\finput_tokens\x18\x16 \x01(\x03R\vinputTokens\x12#\n" + - "\routput_tokens\x18\x17 \x01(\x03R\foutputTokens\x12.\n" + - "\x13cached_input_tokens\x18\x18 \x01(\x03R\x11cachedInputTokens\x12$\n" + - "\x0efirst_token_ms\x18\x19 \x01(\x03R\ffirstTokenMs\x12\x1d\n" + - "\n" + - "error_kind\x18\x1a \x01(\tR\terrorKind\x12\x1b\n" + - "\terror_msg\x18\x1b \x01(\tR\berrorMsg\x12\x1d\n" + + "durationMs\x12.\n" + + "\x05usage\x18\x16 \x01(\v2\x18.airgate.plugin.v1.UsageR\x05usage\x12\x1d\n" + "\n" + - "input_cost\x18\x1e \x01(\x01R\tinputCost\x12\x1f\n" + - "\voutput_cost\x18\x1f \x01(\x01R\n" + - "outputCost\x12*\n" + - "\x11cached_input_cost\x18 \x01(\x01R\x0fcachedInputCost\x12L\n" + + "error_kind\x18\x17 \x01(\tR\terrorKind\x12\x1b\n" + + "\terror_msg\x18\x18 \x01(\tR\berrorMsg\x12L\n" + "\bmetadata\x18( \x03(\v20.airgate.plugin.v1.MiddlewareEvent.MetadataEntryR\bmetadata\x12#\n" + "\rresponse_body\x18d \x01(\fR\fresponseBody\x12b\n" + "\x10response_headers\x18e \x03(\v27.airgate.plugin.v1.MiddlewareEvent.ResponseHeadersEntryR\x0fresponseHeaders\x1a;\n" + @@ -4919,7 +4531,73 @@ const file_plugin_proto_rawDesc = "" + "\x05ALLOW\x10\x00\x12\b\n" + "\x04DENY\x10\x01\x12\n" + "\n" + - "\x06MUTATE\x10\x02*\xc9\x01\n" + + "\x06MUTATE\x10\x02\"\xe0\x02\n" + + "\x16EventSubscriptionProto\x12\x12\n" + + "\x04type\x18\x01 \x01(\tR\x04type\x12\x16\n" + + "\x06source\x18\x02 \x01(\tR\x06source\x12M\n" + + "\x06filter\x18\x03 \x03(\v25.airgate.plugin.v1.EventSubscriptionProto.FilterEntryR\x06filter\x12S\n" + + "\bmetadata\x18\x04 \x03(\v27.airgate.plugin.v1.EventSubscriptionProto.MetadataEntryR\bmetadata\x1a9\n" + + "\vFilterEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1a;\n" + + "\rMetadataEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"m\n" + + "\x1aEventSubscriptionsResponse\x12O\n" + + "\rsubscriptions\x18\x01 \x03(\v2).airgate.plugin.v1.EventSubscriptionProtoR\rsubscriptions\"\xd9\x02\n" + + "\vPluginEvent\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" + + "\x04type\x18\x02 \x01(\tR\x04type\x12\x16\n" + + "\x06source\x18\x03 \x01(\tR\x06source\x12\x18\n" + + "\asubject\x18\x04 \x01(\tR\asubject\x12\x17\n" + + "\auser_id\x18\x05 \x01(\x03R\x06userId\x12\x19\n" + + "\bgroup_id\x18\x06 \x01(\x03R\agroupId\x12\x18\n" + + "\apayload\x18\a \x01(\fR\apayload\x12H\n" + + "\bmetadata\x18\b \x03(\v2,.airgate.plugin.v1.PluginEvent.MetadataEntryR\bmetadata\x12\x1f\n" + + "\voccurred_at\x18\t \x01(\x03R\n" + + "occurredAt\x1a;\n" + + "\rMetadataEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"T\n" + + "\x13EventHandleResponse\x12\x18\n" + + "\asuccess\x18\x01 \x01(\bR\asuccess\x12#\n" + + "\rerror_message\x18\x02 \x01(\tR\ferrorMessage\"\xfb\x01\n" + + "\x11HostInvokeRequest\x12\x16\n" + + "\x06method\x18\x01 \x01(\tR\x06method\x12\x18\n" + + "\apayload\x18\x02 \x01(\fR\apayload\x12'\n" + + "\x0fidempotency_key\x18\x03 \x01(\tR\x0eidempotencyKey\x12N\n" + + "\bmetadata\x18\x04 \x03(\v22.airgate.plugin.v1.HostInvokeRequest.MetadataEntryR\bmetadata\x1a;\n" + + "\rMetadataEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xd4\x01\n" + + "\x12HostInvokeResponse\x12\x16\n" + + "\x06status\x18\x01 \x01(\tR\x06status\x12\x18\n" + + "\apayload\x18\x02 \x01(\fR\apayload\x12O\n" + + "\bmetadata\x18\x03 \x03(\v23.airgate.plugin.v1.HostInvokeResponse.MetadataEntryR\bmetadata\x1a;\n" + + "\rMetadataEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xb9\x02\n" + + "\x0fHostStreamFrame\x12\x16\n" + + "\x06method\x18\x01 \x01(\tR\x06method\x12\x14\n" + + "\x05event\x18\x02 \x01(\tR\x05event\x12\x18\n" + + "\apayload\x18\x03 \x01(\fR\apayload\x12'\n" + + "\x0fidempotency_key\x18\x04 \x01(\tR\x0eidempotencyKey\x12L\n" + + "\bmetadata\x18\x05 \x03(\v20.airgate.plugin.v1.HostStreamFrame.MetadataEntryR\bmetadata\x12\x12\n" + + "\x04done\x18\x06 \x01(\bR\x04done\x12\x16\n" + + "\x06status\x18\a \x01(\tR\x06status\x1a;\n" + + "\rMetadataEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"y\n" + + "\x12ProcessTaskRequest\x12\x17\n" + + "\atask_id\x18\x01 \x01(\x03R\x06taskId\x12\x1b\n" + + "\ttask_type\x18\x02 \x01(\tR\btaskType\x12\x14\n" + + "\x05input\x18\x03 \x01(\fR\x05input\x12\x17\n" + + "\auser_id\x18\x04 \x01(\x03R\x06userId\"T\n" + + "\x13ProcessTaskResponse\x12\x18\n" + + "\asuccess\x18\x01 \x01(\bR\asuccess\x12#\n" + + "\rerror_message\x18\x02 \x01(\tR\ferrorMessage\")\n" + + "\x11TaskTypesResponse\x12\x14\n" + + "\x05types\x18\x01 \x03(\tR\x05types*\xf0\x01\n" + "\vOutcomeKind\x12\x13\n" + "\x0fOUTCOME_UNKNOWN\x10\x00\x12\x13\n" + "\x0fOUTCOME_SUCCESS\x10\x01\x12\x18\n" + @@ -4927,50 +4605,42 @@ const file_plugin_proto_rawDesc = "" + "\x1cOUTCOME_ACCOUNT_RATE_LIMITED\x10\x03\x12\x18\n" + "\x14OUTCOME_ACCOUNT_DEAD\x10\x04\x12\x1e\n" + "\x1aOUTCOME_UPSTREAM_TRANSIENT\x10\x05\x12\x1a\n" + - "\x16OUTCOME_STREAM_ABORTED\x10\x062\xfb\x03\n" + + "\x16OUTCOME_STREAM_ABORTED\x10\x06\x12%\n" + + "!OUTCOME_ACCOUNT_MODEL_UNSUPPORTED\x10\a2\xcb\x04\n" + "\rPluginService\x12J\n" + "\aGetInfo\x12\x18.airgate.plugin.v1.Empty\x1a%.airgate.plugin.v1.PluginInfoResponse\x12@\n" + "\x04Init\x12\x1e.airgate.plugin.v1.InitRequest\x1a\x18.airgate.plugin.v1.Empty\x12;\n" + "\x05Start\x12\x18.airgate.plugin.v1.Empty\x1a\x18.airgate.plugin.v1.Empty\x12:\n" + "\x04Stop\x12\x18.airgate.plugin.v1.Empty\x1a\x18.airgate.plugin.v1.Empty\x12N\n" + - "\fGetWebAssets\x12\x18.airgate.plugin.v1.Empty\x1a$.airgate.plugin.v1.WebAssetsResponse\x12A\n" + + "\fGetWebAssets\x12\x18.airgate.plugin.v1.Empty\x1a$.airgate.plugin.v1.WebAssetsResponse\x12N\n" + + "\tGetSchema\x12\x18.airgate.plugin.v1.Empty\x1a'.airgate.plugin.v1.PluginSchemaResponse\x12A\n" + "\vHealthCheck\x12\x18.airgate.plugin.v1.Empty\x1a\x18.airgate.plugin.v1.Empty\x12P\n" + - "\rHandleRequest\x12\x1e.airgate.plugin.v1.HttpRequest\x1a\x1f.airgate.plugin.v1.HttpResponse2\xa4\x05\n" + + "\rHandleRequest\x12\x1e.airgate.plugin.v1.HttpRequest\x1a\x1f.airgate.plugin.v1.HttpResponse2\xc9\x04\n" + "\x0eGatewayService\x12J\n" + "\vGetPlatform\x12\x18.airgate.plugin.v1.Empty\x1a!.airgate.plugin.v1.StringResponse\x12H\n" + "\tGetModels\x12\x18.airgate.plugin.v1.Empty\x1a!.airgate.plugin.v1.ModelsResponse\x12H\n" + "\tGetRoutes\x12\x18.airgate.plugin.v1.Empty\x1a!.airgate.plugin.v1.RoutesResponse\x12O\n" + "\aForward\x12!.airgate.plugin.v1.ForwardRequest\x1a!.airgate.plugin.v1.ForwardOutcome\x12U\n" + "\rForwardStream\x12!.airgate.plugin.v1.ForwardRequest\x1a\x1f.airgate.plugin.v1.ForwardChunk0\x01\x12R\n" + - "\x0fValidateAccount\x12%.airgate.plugin.v1.CredentialsRequest\x1a\x18.airgate.plugin.v1.Empty\x12Y\n" + - "\n" + - "QueryQuota\x12%.airgate.plugin.v1.CredentialsRequest\x1a$.airgate.plugin.v1.QuotaInfoResponse\x12[\n" + - "\x0fHandleWebSocket\x12!.airgate.plugin.v1.WebSocketFrame\x1a!.airgate.plugin.v1.WebSocketFrame(\x010\x012\xba\x03\n" + + "\x0fValidateAccount\x12%.airgate.plugin.v1.CredentialsRequest\x1a\x18.airgate.plugin.v1.Empty\x12[\n" + + "\x0fHandleWebSocket\x12!.airgate.plugin.v1.WebSocketFrame\x1a!.airgate.plugin.v1.WebSocketFrame(\x010\x012\xe8\x04\n" + "\x10ExtensionService\x12=\n" + "\aMigrate\x12\x18.airgate.plugin.v1.Empty\x1a\x18.airgate.plugin.v1.Empty\x12Z\n" + "\x12GetBackgroundTasks\x12\x18.airgate.plugin.v1.Empty\x1a*.airgate.plugin.v1.BackgroundTasksResponse\x12Z\n" + "\x11RunBackgroundTask\x12+.airgate.plugin.v1.RunBackgroundTaskRequest\x1a\x18.airgate.plugin.v1.Empty\x12P\n" + "\rHandleRequest\x12\x1e.airgate.plugin.v1.HttpRequest\x1a\x1f.airgate.plugin.v1.HttpResponse\x12]\n" + - "\x13HandleStreamRequest\x12\x1e.airgate.plugin.v1.HttpRequest\x1a$.airgate.plugin.v1.HttpResponseChunk0\x012\xc0\x01\n" + + "\x13HandleStreamRequest\x12\x1e.airgate.plugin.v1.HttpRequest\x1a$.airgate.plugin.v1.HttpResponseChunk0\x01\x12\\\n" + + "\vProcessTask\x12%.airgate.plugin.v1.ProcessTaskRequest\x1a&.airgate.plugin.v1.ProcessTaskResponse\x12N\n" + + "\fGetTaskTypes\x12\x18.airgate.plugin.v1.Empty\x1a$.airgate.plugin.v1.TaskTypesResponse2\xc0\x01\n" + "\x11MiddlewareService\x12]\n" + "\x0eOnForwardBegin\x12$.airgate.plugin.v1.MiddlewareRequest\x1a%.airgate.plugin.v1.MiddlewareDecision\x12L\n" + - "\fOnForwardEnd\x12\".airgate.plugin.v1.MiddlewareEvent\x1a\x18.airgate.plugin.v1.Empty2\xcc\t\n" + - "\vHostService\x12j\n" + - "\rSelectAccount\x12+.airgate.plugin.v1.HostSelectAccountRequest\x1a,.airgate.plugin.v1.HostSelectAccountResponse\x12b\n" + - "\x13ReportAccountResult\x121.airgate.plugin.v1.HostReportAccountResultRequest\x1a\x18.airgate.plugin.v1.Empty\x12g\n" + - "\fProbeForward\x12*.airgate.plugin.v1.HostProbeForwardRequest\x1a+.airgate.plugin.v1.HostProbeForwardResponse\x12X\n" + - "\aForward\x12%.airgate.plugin.v1.HostForwardRequest\x1a&.airgate.plugin.v1.HostForwardResponse\x12]\n" + - "\rForwardStream\x12%.airgate.plugin.v1.HostForwardRequest\x1a#.airgate.plugin.v1.HostForwardChunk0\x01\x12a\n" + - "\n" + - "ListGroups\x12(.airgate.plugin.v1.HostListGroupsRequest\x1a).airgate.plugin.v1.HostListGroupsResponse\x12j\n" + - "\rListPlatforms\x12+.airgate.plugin.v1.HostListPlatformsRequest\x1a,.airgate.plugin.v1.HostListPlatformsResponse\x12a\n" + - "\n" + - "ListModels\x12(.airgate.plugin.v1.HostListModelsRequest\x1a).airgate.plugin.v1.HostListModelsResponse\x12d\n" + - "\vGetUserInfo\x12).airgate.plugin.v1.HostGetUserInfoRequest\x1a*.airgate.plugin.v1.HostGetUserInfoResponse\x12a\n" + - "\n" + - "StoreAsset\x12(.airgate.plugin.v1.HostStoreAssetRequest\x1a).airgate.plugin.v1.HostStoreAssetResponse\x12d\n" + - "\vGetAssetURL\x12).airgate.plugin.v1.HostGetAssetURLRequest\x1a*.airgate.plugin.v1.HostGetAssetURLResponse\x12j\n" + - "\rGetAssetBytes\x12+.airgate.plugin.v1.HostGetAssetBytesRequest\x1a,.airgate.plugin.v1.HostGetAssetBytesResponseB+Z)github.com/DouDOU-start/airgate-sdk/protob\x06proto3" + "\fOnForwardEnd\x12\".airgate.plugin.v1.MiddlewareEvent\x1a\x18.airgate.plugin.v1.Empty2\xc7\x01\n" + + "\fEventService\x12`\n" + + "\x15GetEventSubscriptions\x12\x18.airgate.plugin.v1.Empty\x1a-.airgate.plugin.v1.EventSubscriptionsResponse\x12U\n" + + "\vHandleEvent\x12\x1e.airgate.plugin.v1.PluginEvent\x1a&.airgate.plugin.v1.EventHandleResponse2\xc6\x01\n" + + "\x11CoreInvokeService\x12U\n" + + "\x06Invoke\x12$.airgate.plugin.v1.HostInvokeRequest\x1a%.airgate.plugin.v1.HostInvokeResponse\x12Z\n" + + "\fInvokeStream\x12\".airgate.plugin.v1.HostStreamFrame\x1a\".airgate.plugin.v1.HostStreamFrame(\x010\x01B4Z2github.com/DouDOU-start/airgate-sdk/protocol/protob\x06proto3" var ( file_plugin_proto_rawDescOnce sync.Once @@ -4985,223 +4655,246 @@ func file_plugin_proto_rawDescGZIP() []byte { } var file_plugin_proto_enumTypes = make([]protoimpl.EnumInfo, 3) -var file_plugin_proto_msgTypes = make([]protoimpl.MessageInfo, 80) +var file_plugin_proto_msgTypes = make([]protoimpl.MessageInfo, 88) var file_plugin_proto_goTypes = []any{ - (OutcomeKind)(0), // 0: airgate.plugin.v1.OutcomeKind - (WebSocketFrame_FrameType)(0), // 1: airgate.plugin.v1.WebSocketFrame.FrameType - (MiddlewareDecision_Action)(0), // 2: airgate.plugin.v1.MiddlewareDecision.Action - (*HostSelectAccountRequest)(nil), // 3: airgate.plugin.v1.HostSelectAccountRequest - (*HostSelectAccountResponse)(nil), // 4: airgate.plugin.v1.HostSelectAccountResponse - (*HostProbeForwardRequest)(nil), // 5: airgate.plugin.v1.HostProbeForwardRequest - (*HostProbeForwardResponse)(nil), // 6: airgate.plugin.v1.HostProbeForwardResponse - (*HostListGroupsRequest)(nil), // 7: airgate.plugin.v1.HostListGroupsRequest - (*HostGroup)(nil), // 8: airgate.plugin.v1.HostGroup - (*HostListGroupsResponse)(nil), // 9: airgate.plugin.v1.HostListGroupsResponse - (*HostReportAccountResultRequest)(nil), // 10: airgate.plugin.v1.HostReportAccountResultRequest - (*HostForwardRequest)(nil), // 11: airgate.plugin.v1.HostForwardRequest - (*HostForwardResponse)(nil), // 12: airgate.plugin.v1.HostForwardResponse - (*HostForwardChunk)(nil), // 13: airgate.plugin.v1.HostForwardChunk - (*HostForwardUsage)(nil), // 14: airgate.plugin.v1.HostForwardUsage - (*HostListPlatformsRequest)(nil), // 15: airgate.plugin.v1.HostListPlatformsRequest - (*HostPlatform)(nil), // 16: airgate.plugin.v1.HostPlatform - (*HostListPlatformsResponse)(nil), // 17: airgate.plugin.v1.HostListPlatformsResponse - (*HostListModelsRequest)(nil), // 18: airgate.plugin.v1.HostListModelsRequest - (*HostListModelsResponse)(nil), // 19: airgate.plugin.v1.HostListModelsResponse - (*HostGetUserInfoRequest)(nil), // 20: airgate.plugin.v1.HostGetUserInfoRequest - (*HostGetUserInfoResponse)(nil), // 21: airgate.plugin.v1.HostGetUserInfoResponse - (*HostStoreAssetRequest)(nil), // 22: airgate.plugin.v1.HostStoreAssetRequest - (*HostStoreAssetResponse)(nil), // 23: airgate.plugin.v1.HostStoreAssetResponse - (*HostGetAssetURLRequest)(nil), // 24: airgate.plugin.v1.HostGetAssetURLRequest - (*HostGetAssetURLResponse)(nil), // 25: airgate.plugin.v1.HostGetAssetURLResponse - (*HostGetAssetBytesRequest)(nil), // 26: airgate.plugin.v1.HostGetAssetBytesRequest - (*HostGetAssetBytesResponse)(nil), // 27: airgate.plugin.v1.HostGetAssetBytesResponse - (*Empty)(nil), // 28: airgate.plugin.v1.Empty - (*StringResponse)(nil), // 29: airgate.plugin.v1.StringResponse - (*HeaderValues)(nil), // 30: airgate.plugin.v1.HeaderValues - (*PluginInfoResponse)(nil), // 31: airgate.plugin.v1.PluginInfoResponse - (*ConfigFieldProto)(nil), // 32: airgate.plugin.v1.ConfigFieldProto - (*AccountTypeProto)(nil), // 33: airgate.plugin.v1.AccountTypeProto - (*CredentialFieldProto)(nil), // 34: airgate.plugin.v1.CredentialFieldProto - (*FrontendPageProto)(nil), // 35: airgate.plugin.v1.FrontendPageProto - (*FrontendWidgetProto)(nil), // 36: airgate.plugin.v1.FrontendWidgetProto - (*InitRequest)(nil), // 37: airgate.plugin.v1.InitRequest - (*ModelInfoProto)(nil), // 38: airgate.plugin.v1.ModelInfoProto - (*ModelsResponse)(nil), // 39: airgate.plugin.v1.ModelsResponse - (*RouteDefinitionProto)(nil), // 40: airgate.plugin.v1.RouteDefinitionProto - (*RoutesResponse)(nil), // 41: airgate.plugin.v1.RoutesResponse - (*AccountProto)(nil), // 42: airgate.plugin.v1.AccountProto - (*ForwardRequest)(nil), // 43: airgate.plugin.v1.ForwardRequest - (*UpstreamResponse)(nil), // 44: airgate.plugin.v1.UpstreamResponse - (*Usage)(nil), // 45: airgate.plugin.v1.Usage - (*ForwardOutcome)(nil), // 46: airgate.plugin.v1.ForwardOutcome - (*ForwardChunk)(nil), // 47: airgate.plugin.v1.ForwardChunk - (*CredentialsRequest)(nil), // 48: airgate.plugin.v1.CredentialsRequest - (*QuotaInfoResponse)(nil), // 49: airgate.plugin.v1.QuotaInfoResponse - (*HttpRequest)(nil), // 50: airgate.plugin.v1.HttpRequest - (*HttpResponse)(nil), // 51: airgate.plugin.v1.HttpResponse - (*HttpResponseChunk)(nil), // 52: airgate.plugin.v1.HttpResponseChunk - (*BackgroundTaskProto)(nil), // 53: airgate.plugin.v1.BackgroundTaskProto - (*BackgroundTasksResponse)(nil), // 54: airgate.plugin.v1.BackgroundTasksResponse - (*RunBackgroundTaskRequest)(nil), // 55: airgate.plugin.v1.RunBackgroundTaskRequest - (*WebSocketFrame)(nil), // 56: airgate.plugin.v1.WebSocketFrame - (*WebSocketConnectInfo)(nil), // 57: airgate.plugin.v1.WebSocketConnectInfo - (*WebAssetFile)(nil), // 58: airgate.plugin.v1.WebAssetFile - (*WebAssetsResponse)(nil), // 59: airgate.plugin.v1.WebAssetsResponse - (*MiddlewareRequest)(nil), // 60: airgate.plugin.v1.MiddlewareRequest - (*MiddlewareEvent)(nil), // 61: airgate.plugin.v1.MiddlewareEvent - (*MiddlewareDecision)(nil), // 62: airgate.plugin.v1.MiddlewareDecision - nil, // 63: airgate.plugin.v1.HostForwardRequest.HeadersEntry - nil, // 64: airgate.plugin.v1.HostForwardResponse.HeadersEntry - nil, // 65: airgate.plugin.v1.HostForwardChunk.HeadersEntry - nil, // 66: airgate.plugin.v1.InitRequest.ConfigEntry - nil, // 67: airgate.plugin.v1.ForwardRequest.HeadersEntry - nil, // 68: airgate.plugin.v1.UpstreamResponse.HeadersEntry - nil, // 69: airgate.plugin.v1.ForwardOutcome.UpdatedCredentialsEntry - nil, // 70: airgate.plugin.v1.ForwardChunk.HeadersEntry - nil, // 71: airgate.plugin.v1.CredentialsRequest.CredentialsEntry - nil, // 72: airgate.plugin.v1.QuotaInfoResponse.ExtraEntry - nil, // 73: airgate.plugin.v1.HttpRequest.HeadersEntry - nil, // 74: airgate.plugin.v1.HttpResponse.HeadersEntry - nil, // 75: airgate.plugin.v1.HttpResponseChunk.HeadersEntry - nil, // 76: airgate.plugin.v1.WebSocketConnectInfo.HeadersEntry - nil, // 77: airgate.plugin.v1.MiddlewareRequest.MetadataEntry - nil, // 78: airgate.plugin.v1.MiddlewareRequest.RequestHeadersEntry - nil, // 79: airgate.plugin.v1.MiddlewareEvent.MetadataEntry - nil, // 80: airgate.plugin.v1.MiddlewareEvent.ResponseHeadersEntry - nil, // 81: airgate.plugin.v1.MiddlewareDecision.SetHeadersEntry - nil, // 82: airgate.plugin.v1.MiddlewareDecision.MetadataEntry + (OutcomeKind)(0), // 0: airgate.plugin.v1.OutcomeKind + (WebSocketFrame_FrameType)(0), // 1: airgate.plugin.v1.WebSocketFrame.FrameType + (MiddlewareDecision_Action)(0), // 2: airgate.plugin.v1.MiddlewareDecision.Action + (*Empty)(nil), // 3: airgate.plugin.v1.Empty + (*StringResponse)(nil), // 4: airgate.plugin.v1.StringResponse + (*HeaderValues)(nil), // 5: airgate.plugin.v1.HeaderValues + (*PluginInfoResponse)(nil), // 6: airgate.plugin.v1.PluginInfoResponse + (*ConfigFieldProto)(nil), // 7: airgate.plugin.v1.ConfigFieldProto + (*AccountTypeProto)(nil), // 8: airgate.plugin.v1.AccountTypeProto + (*CredentialFieldProto)(nil), // 9: airgate.plugin.v1.CredentialFieldProto + (*FrontendPageProto)(nil), // 10: airgate.plugin.v1.FrontendPageProto + (*FrontendWidgetProto)(nil), // 11: airgate.plugin.v1.FrontendWidgetProto + (*InitRequest)(nil), // 12: airgate.plugin.v1.InitRequest + (*ModelInfoProto)(nil), // 13: airgate.plugin.v1.ModelInfoProto + (*ModelsResponse)(nil), // 14: airgate.plugin.v1.ModelsResponse + (*RouteDefinitionProto)(nil), // 15: airgate.plugin.v1.RouteDefinitionProto + (*RoutesResponse)(nil), // 16: airgate.plugin.v1.RoutesResponse + (*AccountProto)(nil), // 17: airgate.plugin.v1.AccountProto + (*ForwardRequest)(nil), // 18: airgate.plugin.v1.ForwardRequest + (*UpstreamResponse)(nil), // 19: airgate.plugin.v1.UpstreamResponse + (*UsageAttribute)(nil), // 20: airgate.plugin.v1.UsageAttribute + (*UsageMetric)(nil), // 21: airgate.plugin.v1.UsageMetric + (*UsageCostDetail)(nil), // 22: airgate.plugin.v1.UsageCostDetail + (*Usage)(nil), // 23: airgate.plugin.v1.Usage + (*ForwardOutcome)(nil), // 24: airgate.plugin.v1.ForwardOutcome + (*ForwardChunk)(nil), // 25: airgate.plugin.v1.ForwardChunk + (*CredentialsRequest)(nil), // 26: airgate.plugin.v1.CredentialsRequest + (*HttpRequest)(nil), // 27: airgate.plugin.v1.HttpRequest + (*HttpResponse)(nil), // 28: airgate.plugin.v1.HttpResponse + (*HttpResponseChunk)(nil), // 29: airgate.plugin.v1.HttpResponseChunk + (*BackgroundTaskProto)(nil), // 30: airgate.plugin.v1.BackgroundTaskProto + (*BackgroundTasksResponse)(nil), // 31: airgate.plugin.v1.BackgroundTasksResponse + (*RunBackgroundTaskRequest)(nil), // 32: airgate.plugin.v1.RunBackgroundTaskRequest + (*WebSocketFrame)(nil), // 33: airgate.plugin.v1.WebSocketFrame + (*WebSocketConnectInfo)(nil), // 34: airgate.plugin.v1.WebSocketConnectInfo + (*WebAssetFile)(nil), // 35: airgate.plugin.v1.WebAssetFile + (*WebAssetsResponse)(nil), // 36: airgate.plugin.v1.WebAssetsResponse + (*PayloadSchemaProto)(nil), // 37: airgate.plugin.v1.PayloadSchemaProto + (*RouteSchemaProto)(nil), // 38: airgate.plugin.v1.RouteSchemaProto + (*TaskSchemaProto)(nil), // 39: airgate.plugin.v1.TaskSchemaProto + (*EventSchemaProto)(nil), // 40: airgate.plugin.v1.EventSchemaProto + (*InvokeSchemaProto)(nil), // 41: airgate.plugin.v1.InvokeSchemaProto + (*PluginSchemaResponse)(nil), // 42: airgate.plugin.v1.PluginSchemaResponse + (*MiddlewareRequest)(nil), // 43: airgate.plugin.v1.MiddlewareRequest + (*MiddlewareEvent)(nil), // 44: airgate.plugin.v1.MiddlewareEvent + (*MiddlewareDecision)(nil), // 45: airgate.plugin.v1.MiddlewareDecision + (*EventSubscriptionProto)(nil), // 46: airgate.plugin.v1.EventSubscriptionProto + (*EventSubscriptionsResponse)(nil), // 47: airgate.plugin.v1.EventSubscriptionsResponse + (*PluginEvent)(nil), // 48: airgate.plugin.v1.PluginEvent + (*EventHandleResponse)(nil), // 49: airgate.plugin.v1.EventHandleResponse + (*HostInvokeRequest)(nil), // 50: airgate.plugin.v1.HostInvokeRequest + (*HostInvokeResponse)(nil), // 51: airgate.plugin.v1.HostInvokeResponse + (*HostStreamFrame)(nil), // 52: airgate.plugin.v1.HostStreamFrame + (*ProcessTaskRequest)(nil), // 53: airgate.plugin.v1.ProcessTaskRequest + (*ProcessTaskResponse)(nil), // 54: airgate.plugin.v1.ProcessTaskResponse + (*TaskTypesResponse)(nil), // 55: airgate.plugin.v1.TaskTypesResponse + nil, // 56: airgate.plugin.v1.PluginInfoResponse.MetadataEntry + nil, // 57: airgate.plugin.v1.InitRequest.ConfigEntry + nil, // 58: airgate.plugin.v1.ModelInfoProto.MetadataEntry + nil, // 59: airgate.plugin.v1.RouteDefinitionProto.MetadataEntry + nil, // 60: airgate.plugin.v1.ForwardRequest.HeadersEntry + nil, // 61: airgate.plugin.v1.UpstreamResponse.HeadersEntry + nil, // 62: airgate.plugin.v1.UsageAttribute.MetadataEntry + nil, // 63: airgate.plugin.v1.UsageMetric.MetadataEntry + nil, // 64: airgate.plugin.v1.UsageCostDetail.MetadataEntry + nil, // 65: airgate.plugin.v1.Usage.MetadataEntry + nil, // 66: airgate.plugin.v1.ForwardOutcome.UpdatedCredentialsEntry + nil, // 67: airgate.plugin.v1.ForwardChunk.HeadersEntry + nil, // 68: airgate.plugin.v1.CredentialsRequest.CredentialsEntry + nil, // 69: airgate.plugin.v1.HttpRequest.HeadersEntry + nil, // 70: airgate.plugin.v1.HttpResponse.HeadersEntry + nil, // 71: airgate.plugin.v1.HttpResponseChunk.HeadersEntry + nil, // 72: airgate.plugin.v1.WebSocketConnectInfo.HeadersEntry + nil, // 73: airgate.plugin.v1.PayloadSchemaProto.MetadataEntry + nil, // 74: airgate.plugin.v1.RouteSchemaProto.MetadataEntry + nil, // 75: airgate.plugin.v1.TaskSchemaProto.MetadataEntry + nil, // 76: airgate.plugin.v1.EventSchemaProto.MetadataEntry + nil, // 77: airgate.plugin.v1.InvokeSchemaProto.MetadataEntry + nil, // 78: airgate.plugin.v1.PluginSchemaResponse.MetadataEntry + nil, // 79: airgate.plugin.v1.MiddlewareRequest.MetadataEntry + nil, // 80: airgate.plugin.v1.MiddlewareRequest.RequestHeadersEntry + nil, // 81: airgate.plugin.v1.MiddlewareEvent.MetadataEntry + nil, // 82: airgate.plugin.v1.MiddlewareEvent.ResponseHeadersEntry + nil, // 83: airgate.plugin.v1.MiddlewareDecision.SetHeadersEntry + nil, // 84: airgate.plugin.v1.MiddlewareDecision.MetadataEntry + nil, // 85: airgate.plugin.v1.EventSubscriptionProto.FilterEntry + nil, // 86: airgate.plugin.v1.EventSubscriptionProto.MetadataEntry + nil, // 87: airgate.plugin.v1.PluginEvent.MetadataEntry + nil, // 88: airgate.plugin.v1.HostInvokeRequest.MetadataEntry + nil, // 89: airgate.plugin.v1.HostInvokeResponse.MetadataEntry + nil, // 90: airgate.plugin.v1.HostStreamFrame.MetadataEntry } var file_plugin_proto_depIdxs = []int32{ - 8, // 0: airgate.plugin.v1.HostListGroupsResponse.groups:type_name -> airgate.plugin.v1.HostGroup - 63, // 1: airgate.plugin.v1.HostForwardRequest.headers:type_name -> airgate.plugin.v1.HostForwardRequest.HeadersEntry - 64, // 2: airgate.plugin.v1.HostForwardResponse.headers:type_name -> airgate.plugin.v1.HostForwardResponse.HeadersEntry - 14, // 3: airgate.plugin.v1.HostForwardResponse.usage:type_name -> airgate.plugin.v1.HostForwardUsage - 65, // 4: airgate.plugin.v1.HostForwardChunk.headers:type_name -> airgate.plugin.v1.HostForwardChunk.HeadersEntry - 14, // 5: airgate.plugin.v1.HostForwardChunk.usage:type_name -> airgate.plugin.v1.HostForwardUsage - 16, // 6: airgate.plugin.v1.HostListPlatformsResponse.platforms:type_name -> airgate.plugin.v1.HostPlatform - 38, // 7: airgate.plugin.v1.HostListModelsResponse.models:type_name -> airgate.plugin.v1.ModelInfoProto - 33, // 8: airgate.plugin.v1.PluginInfoResponse.account_types:type_name -> airgate.plugin.v1.AccountTypeProto - 35, // 9: airgate.plugin.v1.PluginInfoResponse.frontend_pages:type_name -> airgate.plugin.v1.FrontendPageProto - 36, // 10: airgate.plugin.v1.PluginInfoResponse.frontend_widgets:type_name -> airgate.plugin.v1.FrontendWidgetProto - 32, // 11: airgate.plugin.v1.PluginInfoResponse.config_schema:type_name -> airgate.plugin.v1.ConfigFieldProto - 34, // 12: airgate.plugin.v1.AccountTypeProto.fields:type_name -> airgate.plugin.v1.CredentialFieldProto - 66, // 13: airgate.plugin.v1.InitRequest.config:type_name -> airgate.plugin.v1.InitRequest.ConfigEntry - 38, // 14: airgate.plugin.v1.ModelsResponse.models:type_name -> airgate.plugin.v1.ModelInfoProto - 40, // 15: airgate.plugin.v1.RoutesResponse.routes:type_name -> airgate.plugin.v1.RouteDefinitionProto - 67, // 16: airgate.plugin.v1.ForwardRequest.headers:type_name -> airgate.plugin.v1.ForwardRequest.HeadersEntry - 42, // 17: airgate.plugin.v1.ForwardRequest.account:type_name -> airgate.plugin.v1.AccountProto - 68, // 18: airgate.plugin.v1.UpstreamResponse.headers:type_name -> airgate.plugin.v1.UpstreamResponse.HeadersEntry - 0, // 19: airgate.plugin.v1.ForwardOutcome.kind:type_name -> airgate.plugin.v1.OutcomeKind - 44, // 20: airgate.plugin.v1.ForwardOutcome.upstream:type_name -> airgate.plugin.v1.UpstreamResponse - 45, // 21: airgate.plugin.v1.ForwardOutcome.usage:type_name -> airgate.plugin.v1.Usage - 69, // 22: airgate.plugin.v1.ForwardOutcome.updated_credentials:type_name -> airgate.plugin.v1.ForwardOutcome.UpdatedCredentialsEntry - 46, // 23: airgate.plugin.v1.ForwardChunk.final_outcome:type_name -> airgate.plugin.v1.ForwardOutcome - 70, // 24: airgate.plugin.v1.ForwardChunk.headers:type_name -> airgate.plugin.v1.ForwardChunk.HeadersEntry - 71, // 25: airgate.plugin.v1.CredentialsRequest.credentials:type_name -> airgate.plugin.v1.CredentialsRequest.CredentialsEntry - 72, // 26: airgate.plugin.v1.QuotaInfoResponse.extra:type_name -> airgate.plugin.v1.QuotaInfoResponse.ExtraEntry - 73, // 27: airgate.plugin.v1.HttpRequest.headers:type_name -> airgate.plugin.v1.HttpRequest.HeadersEntry - 74, // 28: airgate.plugin.v1.HttpResponse.headers:type_name -> airgate.plugin.v1.HttpResponse.HeadersEntry - 75, // 29: airgate.plugin.v1.HttpResponseChunk.headers:type_name -> airgate.plugin.v1.HttpResponseChunk.HeadersEntry - 53, // 30: airgate.plugin.v1.BackgroundTasksResponse.tasks:type_name -> airgate.plugin.v1.BackgroundTaskProto - 1, // 31: airgate.plugin.v1.WebSocketFrame.type:type_name -> airgate.plugin.v1.WebSocketFrame.FrameType - 57, // 32: airgate.plugin.v1.WebSocketFrame.connect_info:type_name -> airgate.plugin.v1.WebSocketConnectInfo - 46, // 33: airgate.plugin.v1.WebSocketFrame.outcome:type_name -> airgate.plugin.v1.ForwardOutcome - 76, // 34: airgate.plugin.v1.WebSocketConnectInfo.headers:type_name -> airgate.plugin.v1.WebSocketConnectInfo.HeadersEntry - 42, // 35: airgate.plugin.v1.WebSocketConnectInfo.account:type_name -> airgate.plugin.v1.AccountProto - 58, // 36: airgate.plugin.v1.WebAssetsResponse.files:type_name -> airgate.plugin.v1.WebAssetFile - 77, // 37: airgate.plugin.v1.MiddlewareRequest.metadata:type_name -> airgate.plugin.v1.MiddlewareRequest.MetadataEntry - 78, // 38: airgate.plugin.v1.MiddlewareRequest.request_headers:type_name -> airgate.plugin.v1.MiddlewareRequest.RequestHeadersEntry - 79, // 39: airgate.plugin.v1.MiddlewareEvent.metadata:type_name -> airgate.plugin.v1.MiddlewareEvent.MetadataEntry - 80, // 40: airgate.plugin.v1.MiddlewareEvent.response_headers:type_name -> airgate.plugin.v1.MiddlewareEvent.ResponseHeadersEntry - 2, // 41: airgate.plugin.v1.MiddlewareDecision.action:type_name -> airgate.plugin.v1.MiddlewareDecision.Action - 81, // 42: airgate.plugin.v1.MiddlewareDecision.set_headers:type_name -> airgate.plugin.v1.MiddlewareDecision.SetHeadersEntry - 82, // 43: airgate.plugin.v1.MiddlewareDecision.metadata:type_name -> airgate.plugin.v1.MiddlewareDecision.MetadataEntry - 30, // 44: airgate.plugin.v1.HostForwardRequest.HeadersEntry.value:type_name -> airgate.plugin.v1.HeaderValues - 30, // 45: airgate.plugin.v1.HostForwardResponse.HeadersEntry.value:type_name -> airgate.plugin.v1.HeaderValues - 30, // 46: airgate.plugin.v1.HostForwardChunk.HeadersEntry.value:type_name -> airgate.plugin.v1.HeaderValues - 30, // 47: airgate.plugin.v1.ForwardRequest.HeadersEntry.value:type_name -> airgate.plugin.v1.HeaderValues - 30, // 48: airgate.plugin.v1.UpstreamResponse.HeadersEntry.value:type_name -> airgate.plugin.v1.HeaderValues - 30, // 49: airgate.plugin.v1.ForwardChunk.HeadersEntry.value:type_name -> airgate.plugin.v1.HeaderValues - 30, // 50: airgate.plugin.v1.HttpRequest.HeadersEntry.value:type_name -> airgate.plugin.v1.HeaderValues - 30, // 51: airgate.plugin.v1.HttpResponse.HeadersEntry.value:type_name -> airgate.plugin.v1.HeaderValues - 30, // 52: airgate.plugin.v1.HttpResponseChunk.HeadersEntry.value:type_name -> airgate.plugin.v1.HeaderValues - 30, // 53: airgate.plugin.v1.WebSocketConnectInfo.HeadersEntry.value:type_name -> airgate.plugin.v1.HeaderValues - 30, // 54: airgate.plugin.v1.MiddlewareRequest.RequestHeadersEntry.value:type_name -> airgate.plugin.v1.HeaderValues - 30, // 55: airgate.plugin.v1.MiddlewareEvent.ResponseHeadersEntry.value:type_name -> airgate.plugin.v1.HeaderValues - 30, // 56: airgate.plugin.v1.MiddlewareDecision.SetHeadersEntry.value:type_name -> airgate.plugin.v1.HeaderValues - 28, // 57: airgate.plugin.v1.PluginService.GetInfo:input_type -> airgate.plugin.v1.Empty - 37, // 58: airgate.plugin.v1.PluginService.Init:input_type -> airgate.plugin.v1.InitRequest - 28, // 59: airgate.plugin.v1.PluginService.Start:input_type -> airgate.plugin.v1.Empty - 28, // 60: airgate.plugin.v1.PluginService.Stop:input_type -> airgate.plugin.v1.Empty - 28, // 61: airgate.plugin.v1.PluginService.GetWebAssets:input_type -> airgate.plugin.v1.Empty - 28, // 62: airgate.plugin.v1.PluginService.HealthCheck:input_type -> airgate.plugin.v1.Empty - 50, // 63: airgate.plugin.v1.PluginService.HandleRequest:input_type -> airgate.plugin.v1.HttpRequest - 28, // 64: airgate.plugin.v1.GatewayService.GetPlatform:input_type -> airgate.plugin.v1.Empty - 28, // 65: airgate.plugin.v1.GatewayService.GetModels:input_type -> airgate.plugin.v1.Empty - 28, // 66: airgate.plugin.v1.GatewayService.GetRoutes:input_type -> airgate.plugin.v1.Empty - 43, // 67: airgate.plugin.v1.GatewayService.Forward:input_type -> airgate.plugin.v1.ForwardRequest - 43, // 68: airgate.plugin.v1.GatewayService.ForwardStream:input_type -> airgate.plugin.v1.ForwardRequest - 48, // 69: airgate.plugin.v1.GatewayService.ValidateAccount:input_type -> airgate.plugin.v1.CredentialsRequest - 48, // 70: airgate.plugin.v1.GatewayService.QueryQuota:input_type -> airgate.plugin.v1.CredentialsRequest - 56, // 71: airgate.plugin.v1.GatewayService.HandleWebSocket:input_type -> airgate.plugin.v1.WebSocketFrame - 28, // 72: airgate.plugin.v1.ExtensionService.Migrate:input_type -> airgate.plugin.v1.Empty - 28, // 73: airgate.plugin.v1.ExtensionService.GetBackgroundTasks:input_type -> airgate.plugin.v1.Empty - 55, // 74: airgate.plugin.v1.ExtensionService.RunBackgroundTask:input_type -> airgate.plugin.v1.RunBackgroundTaskRequest - 50, // 75: airgate.plugin.v1.ExtensionService.HandleRequest:input_type -> airgate.plugin.v1.HttpRequest - 50, // 76: airgate.plugin.v1.ExtensionService.HandleStreamRequest:input_type -> airgate.plugin.v1.HttpRequest - 60, // 77: airgate.plugin.v1.MiddlewareService.OnForwardBegin:input_type -> airgate.plugin.v1.MiddlewareRequest - 61, // 78: airgate.plugin.v1.MiddlewareService.OnForwardEnd:input_type -> airgate.plugin.v1.MiddlewareEvent - 3, // 79: airgate.plugin.v1.HostService.SelectAccount:input_type -> airgate.plugin.v1.HostSelectAccountRequest - 10, // 80: airgate.plugin.v1.HostService.ReportAccountResult:input_type -> airgate.plugin.v1.HostReportAccountResultRequest - 5, // 81: airgate.plugin.v1.HostService.ProbeForward:input_type -> airgate.plugin.v1.HostProbeForwardRequest - 11, // 82: airgate.plugin.v1.HostService.Forward:input_type -> airgate.plugin.v1.HostForwardRequest - 11, // 83: airgate.plugin.v1.HostService.ForwardStream:input_type -> airgate.plugin.v1.HostForwardRequest - 7, // 84: airgate.plugin.v1.HostService.ListGroups:input_type -> airgate.plugin.v1.HostListGroupsRequest - 15, // 85: airgate.plugin.v1.HostService.ListPlatforms:input_type -> airgate.plugin.v1.HostListPlatformsRequest - 18, // 86: airgate.plugin.v1.HostService.ListModels:input_type -> airgate.plugin.v1.HostListModelsRequest - 20, // 87: airgate.plugin.v1.HostService.GetUserInfo:input_type -> airgate.plugin.v1.HostGetUserInfoRequest - 22, // 88: airgate.plugin.v1.HostService.StoreAsset:input_type -> airgate.plugin.v1.HostStoreAssetRequest - 24, // 89: airgate.plugin.v1.HostService.GetAssetURL:input_type -> airgate.plugin.v1.HostGetAssetURLRequest - 26, // 90: airgate.plugin.v1.HostService.GetAssetBytes:input_type -> airgate.plugin.v1.HostGetAssetBytesRequest - 31, // 91: airgate.plugin.v1.PluginService.GetInfo:output_type -> airgate.plugin.v1.PluginInfoResponse - 28, // 92: airgate.plugin.v1.PluginService.Init:output_type -> airgate.plugin.v1.Empty - 28, // 93: airgate.plugin.v1.PluginService.Start:output_type -> airgate.plugin.v1.Empty - 28, // 94: airgate.plugin.v1.PluginService.Stop:output_type -> airgate.plugin.v1.Empty - 59, // 95: airgate.plugin.v1.PluginService.GetWebAssets:output_type -> airgate.plugin.v1.WebAssetsResponse - 28, // 96: airgate.plugin.v1.PluginService.HealthCheck:output_type -> airgate.plugin.v1.Empty - 51, // 97: airgate.plugin.v1.PluginService.HandleRequest:output_type -> airgate.plugin.v1.HttpResponse - 29, // 98: airgate.plugin.v1.GatewayService.GetPlatform:output_type -> airgate.plugin.v1.StringResponse - 39, // 99: airgate.plugin.v1.GatewayService.GetModels:output_type -> airgate.plugin.v1.ModelsResponse - 41, // 100: airgate.plugin.v1.GatewayService.GetRoutes:output_type -> airgate.plugin.v1.RoutesResponse - 46, // 101: airgate.plugin.v1.GatewayService.Forward:output_type -> airgate.plugin.v1.ForwardOutcome - 47, // 102: airgate.plugin.v1.GatewayService.ForwardStream:output_type -> airgate.plugin.v1.ForwardChunk - 28, // 103: airgate.plugin.v1.GatewayService.ValidateAccount:output_type -> airgate.plugin.v1.Empty - 49, // 104: airgate.plugin.v1.GatewayService.QueryQuota:output_type -> airgate.plugin.v1.QuotaInfoResponse - 56, // 105: airgate.plugin.v1.GatewayService.HandleWebSocket:output_type -> airgate.plugin.v1.WebSocketFrame - 28, // 106: airgate.plugin.v1.ExtensionService.Migrate:output_type -> airgate.plugin.v1.Empty - 54, // 107: airgate.plugin.v1.ExtensionService.GetBackgroundTasks:output_type -> airgate.plugin.v1.BackgroundTasksResponse - 28, // 108: airgate.plugin.v1.ExtensionService.RunBackgroundTask:output_type -> airgate.plugin.v1.Empty - 51, // 109: airgate.plugin.v1.ExtensionService.HandleRequest:output_type -> airgate.plugin.v1.HttpResponse - 52, // 110: airgate.plugin.v1.ExtensionService.HandleStreamRequest:output_type -> airgate.plugin.v1.HttpResponseChunk - 62, // 111: airgate.plugin.v1.MiddlewareService.OnForwardBegin:output_type -> airgate.plugin.v1.MiddlewareDecision - 28, // 112: airgate.plugin.v1.MiddlewareService.OnForwardEnd:output_type -> airgate.plugin.v1.Empty - 4, // 113: airgate.plugin.v1.HostService.SelectAccount:output_type -> airgate.plugin.v1.HostSelectAccountResponse - 28, // 114: airgate.plugin.v1.HostService.ReportAccountResult:output_type -> airgate.plugin.v1.Empty - 6, // 115: airgate.plugin.v1.HostService.ProbeForward:output_type -> airgate.plugin.v1.HostProbeForwardResponse - 12, // 116: airgate.plugin.v1.HostService.Forward:output_type -> airgate.plugin.v1.HostForwardResponse - 13, // 117: airgate.plugin.v1.HostService.ForwardStream:output_type -> airgate.plugin.v1.HostForwardChunk - 9, // 118: airgate.plugin.v1.HostService.ListGroups:output_type -> airgate.plugin.v1.HostListGroupsResponse - 17, // 119: airgate.plugin.v1.HostService.ListPlatforms:output_type -> airgate.plugin.v1.HostListPlatformsResponse - 19, // 120: airgate.plugin.v1.HostService.ListModels:output_type -> airgate.plugin.v1.HostListModelsResponse - 21, // 121: airgate.plugin.v1.HostService.GetUserInfo:output_type -> airgate.plugin.v1.HostGetUserInfoResponse - 23, // 122: airgate.plugin.v1.HostService.StoreAsset:output_type -> airgate.plugin.v1.HostStoreAssetResponse - 25, // 123: airgate.plugin.v1.HostService.GetAssetURL:output_type -> airgate.plugin.v1.HostGetAssetURLResponse - 27, // 124: airgate.plugin.v1.HostService.GetAssetBytes:output_type -> airgate.plugin.v1.HostGetAssetBytesResponse - 91, // [91:125] is the sub-list for method output_type - 57, // [57:91] is the sub-list for method input_type - 57, // [57:57] is the sub-list for extension type_name - 57, // [57:57] is the sub-list for extension extendee - 0, // [0:57] is the sub-list for field type_name + 8, // 0: airgate.plugin.v1.PluginInfoResponse.account_types:type_name -> airgate.plugin.v1.AccountTypeProto + 10, // 1: airgate.plugin.v1.PluginInfoResponse.frontend_pages:type_name -> airgate.plugin.v1.FrontendPageProto + 11, // 2: airgate.plugin.v1.PluginInfoResponse.frontend_widgets:type_name -> airgate.plugin.v1.FrontendWidgetProto + 7, // 3: airgate.plugin.v1.PluginInfoResponse.config_schema:type_name -> airgate.plugin.v1.ConfigFieldProto + 56, // 4: airgate.plugin.v1.PluginInfoResponse.metadata:type_name -> airgate.plugin.v1.PluginInfoResponse.MetadataEntry + 9, // 5: airgate.plugin.v1.AccountTypeProto.fields:type_name -> airgate.plugin.v1.CredentialFieldProto + 57, // 6: airgate.plugin.v1.InitRequest.config:type_name -> airgate.plugin.v1.InitRequest.ConfigEntry + 58, // 7: airgate.plugin.v1.ModelInfoProto.metadata:type_name -> airgate.plugin.v1.ModelInfoProto.MetadataEntry + 13, // 8: airgate.plugin.v1.ModelsResponse.models:type_name -> airgate.plugin.v1.ModelInfoProto + 59, // 9: airgate.plugin.v1.RouteDefinitionProto.metadata:type_name -> airgate.plugin.v1.RouteDefinitionProto.MetadataEntry + 15, // 10: airgate.plugin.v1.RoutesResponse.routes:type_name -> airgate.plugin.v1.RouteDefinitionProto + 60, // 11: airgate.plugin.v1.ForwardRequest.headers:type_name -> airgate.plugin.v1.ForwardRequest.HeadersEntry + 17, // 12: airgate.plugin.v1.ForwardRequest.account:type_name -> airgate.plugin.v1.AccountProto + 61, // 13: airgate.plugin.v1.UpstreamResponse.headers:type_name -> airgate.plugin.v1.UpstreamResponse.HeadersEntry + 62, // 14: airgate.plugin.v1.UsageAttribute.metadata:type_name -> airgate.plugin.v1.UsageAttribute.MetadataEntry + 63, // 15: airgate.plugin.v1.UsageMetric.metadata:type_name -> airgate.plugin.v1.UsageMetric.MetadataEntry + 64, // 16: airgate.plugin.v1.UsageCostDetail.metadata:type_name -> airgate.plugin.v1.UsageCostDetail.MetadataEntry + 21, // 17: airgate.plugin.v1.Usage.metrics:type_name -> airgate.plugin.v1.UsageMetric + 20, // 18: airgate.plugin.v1.Usage.attributes:type_name -> airgate.plugin.v1.UsageAttribute + 22, // 19: airgate.plugin.v1.Usage.cost_details:type_name -> airgate.plugin.v1.UsageCostDetail + 65, // 20: airgate.plugin.v1.Usage.metadata:type_name -> airgate.plugin.v1.Usage.MetadataEntry + 0, // 21: airgate.plugin.v1.ForwardOutcome.kind:type_name -> airgate.plugin.v1.OutcomeKind + 19, // 22: airgate.plugin.v1.ForwardOutcome.upstream:type_name -> airgate.plugin.v1.UpstreamResponse + 23, // 23: airgate.plugin.v1.ForwardOutcome.usage:type_name -> airgate.plugin.v1.Usage + 66, // 24: airgate.plugin.v1.ForwardOutcome.updated_credentials:type_name -> airgate.plugin.v1.ForwardOutcome.UpdatedCredentialsEntry + 24, // 25: airgate.plugin.v1.ForwardChunk.final_outcome:type_name -> airgate.plugin.v1.ForwardOutcome + 67, // 26: airgate.plugin.v1.ForwardChunk.headers:type_name -> airgate.plugin.v1.ForwardChunk.HeadersEntry + 68, // 27: airgate.plugin.v1.CredentialsRequest.credentials:type_name -> airgate.plugin.v1.CredentialsRequest.CredentialsEntry + 69, // 28: airgate.plugin.v1.HttpRequest.headers:type_name -> airgate.plugin.v1.HttpRequest.HeadersEntry + 70, // 29: airgate.plugin.v1.HttpResponse.headers:type_name -> airgate.plugin.v1.HttpResponse.HeadersEntry + 71, // 30: airgate.plugin.v1.HttpResponseChunk.headers:type_name -> airgate.plugin.v1.HttpResponseChunk.HeadersEntry + 30, // 31: airgate.plugin.v1.BackgroundTasksResponse.tasks:type_name -> airgate.plugin.v1.BackgroundTaskProto + 1, // 32: airgate.plugin.v1.WebSocketFrame.type:type_name -> airgate.plugin.v1.WebSocketFrame.FrameType + 34, // 33: airgate.plugin.v1.WebSocketFrame.connect_info:type_name -> airgate.plugin.v1.WebSocketConnectInfo + 24, // 34: airgate.plugin.v1.WebSocketFrame.outcome:type_name -> airgate.plugin.v1.ForwardOutcome + 72, // 35: airgate.plugin.v1.WebSocketConnectInfo.headers:type_name -> airgate.plugin.v1.WebSocketConnectInfo.HeadersEntry + 17, // 36: airgate.plugin.v1.WebSocketConnectInfo.account:type_name -> airgate.plugin.v1.AccountProto + 35, // 37: airgate.plugin.v1.WebAssetsResponse.files:type_name -> airgate.plugin.v1.WebAssetFile + 73, // 38: airgate.plugin.v1.PayloadSchemaProto.metadata:type_name -> airgate.plugin.v1.PayloadSchemaProto.MetadataEntry + 37, // 39: airgate.plugin.v1.RouteSchemaProto.request:type_name -> airgate.plugin.v1.PayloadSchemaProto + 37, // 40: airgate.plugin.v1.RouteSchemaProto.response:type_name -> airgate.plugin.v1.PayloadSchemaProto + 74, // 41: airgate.plugin.v1.RouteSchemaProto.metadata:type_name -> airgate.plugin.v1.RouteSchemaProto.MetadataEntry + 37, // 42: airgate.plugin.v1.TaskSchemaProto.input:type_name -> airgate.plugin.v1.PayloadSchemaProto + 37, // 43: airgate.plugin.v1.TaskSchemaProto.output:type_name -> airgate.plugin.v1.PayloadSchemaProto + 75, // 44: airgate.plugin.v1.TaskSchemaProto.metadata:type_name -> airgate.plugin.v1.TaskSchemaProto.MetadataEntry + 37, // 45: airgate.plugin.v1.EventSchemaProto.payload:type_name -> airgate.plugin.v1.PayloadSchemaProto + 76, // 46: airgate.plugin.v1.EventSchemaProto.metadata:type_name -> airgate.plugin.v1.EventSchemaProto.MetadataEntry + 37, // 47: airgate.plugin.v1.InvokeSchemaProto.request:type_name -> airgate.plugin.v1.PayloadSchemaProto + 37, // 48: airgate.plugin.v1.InvokeSchemaProto.response:type_name -> airgate.plugin.v1.PayloadSchemaProto + 77, // 49: airgate.plugin.v1.InvokeSchemaProto.metadata:type_name -> airgate.plugin.v1.InvokeSchemaProto.MetadataEntry + 37, // 50: airgate.plugin.v1.InvokeSchemaProto.client_frame:type_name -> airgate.plugin.v1.PayloadSchemaProto + 37, // 51: airgate.plugin.v1.InvokeSchemaProto.server_frame:type_name -> airgate.plugin.v1.PayloadSchemaProto + 38, // 52: airgate.plugin.v1.PluginSchemaResponse.routes:type_name -> airgate.plugin.v1.RouteSchemaProto + 39, // 53: airgate.plugin.v1.PluginSchemaResponse.tasks:type_name -> airgate.plugin.v1.TaskSchemaProto + 40, // 54: airgate.plugin.v1.PluginSchemaResponse.events:type_name -> airgate.plugin.v1.EventSchemaProto + 41, // 55: airgate.plugin.v1.PluginSchemaResponse.invokes:type_name -> airgate.plugin.v1.InvokeSchemaProto + 78, // 56: airgate.plugin.v1.PluginSchemaResponse.metadata:type_name -> airgate.plugin.v1.PluginSchemaResponse.MetadataEntry + 21, // 57: airgate.plugin.v1.MiddlewareRequest.estimates:type_name -> airgate.plugin.v1.UsageMetric + 79, // 58: airgate.plugin.v1.MiddlewareRequest.metadata:type_name -> airgate.plugin.v1.MiddlewareRequest.MetadataEntry + 80, // 59: airgate.plugin.v1.MiddlewareRequest.request_headers:type_name -> airgate.plugin.v1.MiddlewareRequest.RequestHeadersEntry + 21, // 60: airgate.plugin.v1.MiddlewareEvent.estimates:type_name -> airgate.plugin.v1.UsageMetric + 23, // 61: airgate.plugin.v1.MiddlewareEvent.usage:type_name -> airgate.plugin.v1.Usage + 81, // 62: airgate.plugin.v1.MiddlewareEvent.metadata:type_name -> airgate.plugin.v1.MiddlewareEvent.MetadataEntry + 82, // 63: airgate.plugin.v1.MiddlewareEvent.response_headers:type_name -> airgate.plugin.v1.MiddlewareEvent.ResponseHeadersEntry + 2, // 64: airgate.plugin.v1.MiddlewareDecision.action:type_name -> airgate.plugin.v1.MiddlewareDecision.Action + 83, // 65: airgate.plugin.v1.MiddlewareDecision.set_headers:type_name -> airgate.plugin.v1.MiddlewareDecision.SetHeadersEntry + 84, // 66: airgate.plugin.v1.MiddlewareDecision.metadata:type_name -> airgate.plugin.v1.MiddlewareDecision.MetadataEntry + 85, // 67: airgate.plugin.v1.EventSubscriptionProto.filter:type_name -> airgate.plugin.v1.EventSubscriptionProto.FilterEntry + 86, // 68: airgate.plugin.v1.EventSubscriptionProto.metadata:type_name -> airgate.plugin.v1.EventSubscriptionProto.MetadataEntry + 46, // 69: airgate.plugin.v1.EventSubscriptionsResponse.subscriptions:type_name -> airgate.plugin.v1.EventSubscriptionProto + 87, // 70: airgate.plugin.v1.PluginEvent.metadata:type_name -> airgate.plugin.v1.PluginEvent.MetadataEntry + 88, // 71: airgate.plugin.v1.HostInvokeRequest.metadata:type_name -> airgate.plugin.v1.HostInvokeRequest.MetadataEntry + 89, // 72: airgate.plugin.v1.HostInvokeResponse.metadata:type_name -> airgate.plugin.v1.HostInvokeResponse.MetadataEntry + 90, // 73: airgate.plugin.v1.HostStreamFrame.metadata:type_name -> airgate.plugin.v1.HostStreamFrame.MetadataEntry + 5, // 74: airgate.plugin.v1.ForwardRequest.HeadersEntry.value:type_name -> airgate.plugin.v1.HeaderValues + 5, // 75: airgate.plugin.v1.UpstreamResponse.HeadersEntry.value:type_name -> airgate.plugin.v1.HeaderValues + 5, // 76: airgate.plugin.v1.ForwardChunk.HeadersEntry.value:type_name -> airgate.plugin.v1.HeaderValues + 5, // 77: airgate.plugin.v1.HttpRequest.HeadersEntry.value:type_name -> airgate.plugin.v1.HeaderValues + 5, // 78: airgate.plugin.v1.HttpResponse.HeadersEntry.value:type_name -> airgate.plugin.v1.HeaderValues + 5, // 79: airgate.plugin.v1.HttpResponseChunk.HeadersEntry.value:type_name -> airgate.plugin.v1.HeaderValues + 5, // 80: airgate.plugin.v1.WebSocketConnectInfo.HeadersEntry.value:type_name -> airgate.plugin.v1.HeaderValues + 5, // 81: airgate.plugin.v1.MiddlewareRequest.RequestHeadersEntry.value:type_name -> airgate.plugin.v1.HeaderValues + 5, // 82: airgate.plugin.v1.MiddlewareEvent.ResponseHeadersEntry.value:type_name -> airgate.plugin.v1.HeaderValues + 5, // 83: airgate.plugin.v1.MiddlewareDecision.SetHeadersEntry.value:type_name -> airgate.plugin.v1.HeaderValues + 3, // 84: airgate.plugin.v1.PluginService.GetInfo:input_type -> airgate.plugin.v1.Empty + 12, // 85: airgate.plugin.v1.PluginService.Init:input_type -> airgate.plugin.v1.InitRequest + 3, // 86: airgate.plugin.v1.PluginService.Start:input_type -> airgate.plugin.v1.Empty + 3, // 87: airgate.plugin.v1.PluginService.Stop:input_type -> airgate.plugin.v1.Empty + 3, // 88: airgate.plugin.v1.PluginService.GetWebAssets:input_type -> airgate.plugin.v1.Empty + 3, // 89: airgate.plugin.v1.PluginService.GetSchema:input_type -> airgate.plugin.v1.Empty + 3, // 90: airgate.plugin.v1.PluginService.HealthCheck:input_type -> airgate.plugin.v1.Empty + 27, // 91: airgate.plugin.v1.PluginService.HandleRequest:input_type -> airgate.plugin.v1.HttpRequest + 3, // 92: airgate.plugin.v1.GatewayService.GetPlatform:input_type -> airgate.plugin.v1.Empty + 3, // 93: airgate.plugin.v1.GatewayService.GetModels:input_type -> airgate.plugin.v1.Empty + 3, // 94: airgate.plugin.v1.GatewayService.GetRoutes:input_type -> airgate.plugin.v1.Empty + 18, // 95: airgate.plugin.v1.GatewayService.Forward:input_type -> airgate.plugin.v1.ForwardRequest + 18, // 96: airgate.plugin.v1.GatewayService.ForwardStream:input_type -> airgate.plugin.v1.ForwardRequest + 26, // 97: airgate.plugin.v1.GatewayService.ValidateAccount:input_type -> airgate.plugin.v1.CredentialsRequest + 33, // 98: airgate.plugin.v1.GatewayService.HandleWebSocket:input_type -> airgate.plugin.v1.WebSocketFrame + 3, // 99: airgate.plugin.v1.ExtensionService.Migrate:input_type -> airgate.plugin.v1.Empty + 3, // 100: airgate.plugin.v1.ExtensionService.GetBackgroundTasks:input_type -> airgate.plugin.v1.Empty + 32, // 101: airgate.plugin.v1.ExtensionService.RunBackgroundTask:input_type -> airgate.plugin.v1.RunBackgroundTaskRequest + 27, // 102: airgate.plugin.v1.ExtensionService.HandleRequest:input_type -> airgate.plugin.v1.HttpRequest + 27, // 103: airgate.plugin.v1.ExtensionService.HandleStreamRequest:input_type -> airgate.plugin.v1.HttpRequest + 53, // 104: airgate.plugin.v1.ExtensionService.ProcessTask:input_type -> airgate.plugin.v1.ProcessTaskRequest + 3, // 105: airgate.plugin.v1.ExtensionService.GetTaskTypes:input_type -> airgate.plugin.v1.Empty + 43, // 106: airgate.plugin.v1.MiddlewareService.OnForwardBegin:input_type -> airgate.plugin.v1.MiddlewareRequest + 44, // 107: airgate.plugin.v1.MiddlewareService.OnForwardEnd:input_type -> airgate.plugin.v1.MiddlewareEvent + 3, // 108: airgate.plugin.v1.EventService.GetEventSubscriptions:input_type -> airgate.plugin.v1.Empty + 48, // 109: airgate.plugin.v1.EventService.HandleEvent:input_type -> airgate.plugin.v1.PluginEvent + 50, // 110: airgate.plugin.v1.CoreInvokeService.Invoke:input_type -> airgate.plugin.v1.HostInvokeRequest + 52, // 111: airgate.plugin.v1.CoreInvokeService.InvokeStream:input_type -> airgate.plugin.v1.HostStreamFrame + 6, // 112: airgate.plugin.v1.PluginService.GetInfo:output_type -> airgate.plugin.v1.PluginInfoResponse + 3, // 113: airgate.plugin.v1.PluginService.Init:output_type -> airgate.plugin.v1.Empty + 3, // 114: airgate.plugin.v1.PluginService.Start:output_type -> airgate.plugin.v1.Empty + 3, // 115: airgate.plugin.v1.PluginService.Stop:output_type -> airgate.plugin.v1.Empty + 36, // 116: airgate.plugin.v1.PluginService.GetWebAssets:output_type -> airgate.plugin.v1.WebAssetsResponse + 42, // 117: airgate.plugin.v1.PluginService.GetSchema:output_type -> airgate.plugin.v1.PluginSchemaResponse + 3, // 118: airgate.plugin.v1.PluginService.HealthCheck:output_type -> airgate.plugin.v1.Empty + 28, // 119: airgate.plugin.v1.PluginService.HandleRequest:output_type -> airgate.plugin.v1.HttpResponse + 4, // 120: airgate.plugin.v1.GatewayService.GetPlatform:output_type -> airgate.plugin.v1.StringResponse + 14, // 121: airgate.plugin.v1.GatewayService.GetModels:output_type -> airgate.plugin.v1.ModelsResponse + 16, // 122: airgate.plugin.v1.GatewayService.GetRoutes:output_type -> airgate.plugin.v1.RoutesResponse + 24, // 123: airgate.plugin.v1.GatewayService.Forward:output_type -> airgate.plugin.v1.ForwardOutcome + 25, // 124: airgate.plugin.v1.GatewayService.ForwardStream:output_type -> airgate.plugin.v1.ForwardChunk + 3, // 125: airgate.plugin.v1.GatewayService.ValidateAccount:output_type -> airgate.plugin.v1.Empty + 33, // 126: airgate.plugin.v1.GatewayService.HandleWebSocket:output_type -> airgate.plugin.v1.WebSocketFrame + 3, // 127: airgate.plugin.v1.ExtensionService.Migrate:output_type -> airgate.plugin.v1.Empty + 31, // 128: airgate.plugin.v1.ExtensionService.GetBackgroundTasks:output_type -> airgate.plugin.v1.BackgroundTasksResponse + 3, // 129: airgate.plugin.v1.ExtensionService.RunBackgroundTask:output_type -> airgate.plugin.v1.Empty + 28, // 130: airgate.plugin.v1.ExtensionService.HandleRequest:output_type -> airgate.plugin.v1.HttpResponse + 29, // 131: airgate.plugin.v1.ExtensionService.HandleStreamRequest:output_type -> airgate.plugin.v1.HttpResponseChunk + 54, // 132: airgate.plugin.v1.ExtensionService.ProcessTask:output_type -> airgate.plugin.v1.ProcessTaskResponse + 55, // 133: airgate.plugin.v1.ExtensionService.GetTaskTypes:output_type -> airgate.plugin.v1.TaskTypesResponse + 45, // 134: airgate.plugin.v1.MiddlewareService.OnForwardBegin:output_type -> airgate.plugin.v1.MiddlewareDecision + 3, // 135: airgate.plugin.v1.MiddlewareService.OnForwardEnd:output_type -> airgate.plugin.v1.Empty + 47, // 136: airgate.plugin.v1.EventService.GetEventSubscriptions:output_type -> airgate.plugin.v1.EventSubscriptionsResponse + 49, // 137: airgate.plugin.v1.EventService.HandleEvent:output_type -> airgate.plugin.v1.EventHandleResponse + 51, // 138: airgate.plugin.v1.CoreInvokeService.Invoke:output_type -> airgate.plugin.v1.HostInvokeResponse + 52, // 139: airgate.plugin.v1.CoreInvokeService.InvokeStream:output_type -> airgate.plugin.v1.HostStreamFrame + 112, // [112:140] is the sub-list for method output_type + 84, // [84:112] is the sub-list for method input_type + 84, // [84:84] is the sub-list for extension type_name + 84, // [84:84] is the sub-list for extension extendee + 0, // [0:84] is the sub-list for field type_name } func init() { file_plugin_proto_init() } @@ -5215,9 +4908,9 @@ func file_plugin_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_plugin_proto_rawDesc), len(file_plugin_proto_rawDesc)), NumEnums: 3, - NumMessages: 80, + NumMessages: 88, NumExtensions: 0, - NumServices: 5, + NumServices: 6, }, GoTypes: file_plugin_proto_goTypes, DependencyIndexes: file_plugin_proto_depIdxs, diff --git a/protocol/proto/plugin.proto b/protocol/proto/plugin.proto new file mode 100644 index 0000000..ce2dda3 --- /dev/null +++ b/protocol/proto/plugin.proto @@ -0,0 +1,601 @@ +syntax = "proto3"; + +package airgate.plugin.v1; + +option go_package = "github.com/DouDOU-start/airgate-sdk/protocol/proto"; + +// ==================== 插件基础服务 ==================== + +service PluginService { + rpc GetInfo(Empty) returns (PluginInfoResponse); + rpc Init(InitRequest) returns (Empty); + rpc Start(Empty) returns (Empty); + rpc Stop(Empty) returns (Empty); + // 获取插件的前端静态资源 + rpc GetWebAssets(Empty) returns (WebAssetsResponse); + // 获取插件结构化能力清单(可选,未实现时返回空) + rpc GetSchema(Empty) returns (PluginSchemaResponse); + // 健康检查(可选,插件未实现时返回成功) + rpc HealthCheck(Empty) returns (Empty); + // 通用请求代理:Core 透传,插件自行路由(可选,插件未实现时返回 501) + rpc HandleRequest(HttpRequest) returns (HttpResponse); +} + +// ==================== 网关服务 ==================== + +service GatewayService { + rpc GetPlatform(Empty) returns (StringResponse); + rpc GetModels(Empty) returns (ModelsResponse); + rpc GetRoutes(Empty) returns (RoutesResponse); + rpc Forward(ForwardRequest) returns (ForwardOutcome); + rpc ForwardStream(ForwardRequest) returns (stream ForwardChunk); + rpc ValidateAccount(CredentialsRequest) returns (Empty); + // WebSocket 双向流 + rpc HandleWebSocket(stream WebSocketFrame) returns (stream WebSocketFrame); +} + +// ==================== 扩展服务 ==================== + +service ExtensionService { + rpc Migrate(Empty) returns (Empty); + rpc GetBackgroundTasks(Empty) returns (BackgroundTasksResponse); + // 由 Core 调度器按 Interval 周期触发;插件进程内查表执行 Handler + rpc RunBackgroundTask(RunBackgroundTaskRequest) returns (Empty); + // HTTP 请求由核心代理到插件 + rpc HandleRequest(HttpRequest) returns (HttpResponse); + rpc HandleStreamRequest(HttpRequest) returns (stream HttpResponseChunk); + // 异步任务:Core 分发 pending 任务给插件处理 + rpc ProcessTask(ProcessTaskRequest) returns (ProcessTaskResponse); + // 返回此插件支持的任务类型列表 + rpc GetTaskTypes(Empty) returns (TaskTypesResponse); +} + +// ==================== 中间件服务(Core → Plugin,forward 路径拦截) ==================== +// +// MiddlewareService 让"请求中间层"这种旁路插件能拿到每次 forward 的前后事件: +// - OnForwardBegin — 选完账号 / 还没调 upstream 之前,能改 headers / 拒绝请求 +// - OnForwardEnd — upstream 返回之后 / 写 usage_log 之前,拿到完整 metadata +// +// 设计原则: +// 1. middleware 挂了不能 block 生产:返回 error 只 log warn,流程继续; +// 只有 OnForwardBegin 明确返回 Action=DENY 才会拒绝请求 +// 2. 多个 middleware 按 priority 排序。Begin 升序、End 降序(LIFO) +// 3. payload 两段式:默认只传元数据,声明 middleware.read_body capability 的插件 +// 才会收到 request_body / response_body +// 4. 流式响应的 response_body 只给摘要(首次非空 chunk 拼装),完整流式内容应由 +// 独立的流式事件能力承载 +// +// 新角色:middleware 插件不是 gateway(不替代 upstream),也不是 extension +//(不跑后台任务 + 自定义 HTTP)。它在 PluginInfo.type = "middleware" 中声明。 +service MiddlewareService { + rpc OnForwardBegin(MiddlewareRequest) returns (MiddlewareDecision); + rpc OnForwardEnd(MiddlewareEvent) returns (Empty); +} + +// ==================== 事件服务(Core → Plugin) ==================== + +service EventService { + rpc GetEventSubscriptions(Empty) returns (EventSubscriptionsResponse); + rpc HandleEvent(PluginEvent) returns (EventHandleResponse); +} + +// ==================== Host 服务(反向调用:插件 → Core) ==================== +// +// 由 Core 实现,通过 hashicorp/go-plugin 的 GRPCBroker 暴露给插件子进程。 +// SDK 只定义通用 Invoke / InvokeStream 通道;Core 通过方法注册表决定开放哪些方法、 +// 允许哪些插件调用、请求/响应 schema、是否支持流式以及幂等策略。 +service CoreInvokeService { + rpc Invoke(HostInvokeRequest) returns (HostInvokeResponse); + rpc InvokeStream(stream HostStreamFrame) returns (stream HostStreamFrame); +} + +// ==================== 通用消息 ==================== + +message Empty {} + +message StringResponse { + string value = 1; +} + +// HeaderValues 支持同一 Header 有多个值(如 Set-Cookie) +message HeaderValues { + repeated string values = 1; +} + +// ==================== 插件信息 ==================== + +message PluginInfoResponse { + string id = 1; + string name = 2; + string version = 3; + string description = 4; + string author = 5; + string type = 6; + repeated AccountTypeProto account_types = 7; + repeated FrontendPageProto frontend_pages = 8; + repeated FrontendWidgetProto frontend_widgets = 9; + string sdk_version = 10; // 插件编译时的 SDK 版本 + repeated string dependencies = 11; // 依赖的其他插件 ID + repeated ConfigFieldProto config_schema = 12; // 配置项声明 + repeated string instruction_presets = 13; // 可用的 instructions 预设名称列表 + // capabilities 声明 Host.Invoke / Host.InvokeStream、method 级授权和 Middleware 能力。 + // Core 启动时按插件类型、方法注册表和 RPC 调用做准入校验。 + repeated string capabilities = 14; + // priority 中间件链中的排序权重(数值越小越早进 Begin、越晚出 End)。 + // 仅 type="middleware" 插件使用;其他类型忽略。默认 100。 + int32 priority = 15; + // metadata 保存插件声明层面的弱契约扩展信息,例如分类、市场标签、展示提示。 + // 需要 Core 授权或参与调度的数据必须进入显式字段或 capability。 + map metadata = 16; +} + +message ConfigFieldProto { + string key = 1; + string label = 2; + string type = 3; + bool required = 4; + string default_value = 5; + string description = 6; + string placeholder = 7; +} + +message AccountTypeProto { + string key = 1; + string label = 2; + string description = 3; + repeated CredentialFieldProto fields = 4; +} + +message CredentialFieldProto { + string key = 1; + string label = 2; + string type = 3; + bool required = 4; + string placeholder = 5; + bool edit_disabled = 6; // 编辑模式下隐藏该字段 +} + +message FrontendPageProto { + string path = 1; + string title = 2; + string icon = 3; + string description = 4; + string audience = 5; // "admin" | "user" | "all",空 = "admin" +} + +message FrontendWidgetProto { + string slot = 1; + string entry_file = 2; + string title = 3; +} + +message InitRequest { + map config = 1; + string log_level = 2; + // core_invoke_broker_id 是 Core 通过 hashicorp/go-plugin GRPCBroker 启动的 + // 反向调用 stream ID。插件 Init 时拿到 ID 后,可以通过 broker.Dial(id) + // 拿到 CoreInvokeService grpc client,通过 Invoke / InvokeStream 回调 Core 开放的方法。 + // 0 表示 Core 没启用反向调用。 + uint32 core_invoke_broker_id = 3; +} + +// ==================== 网关消息 ==================== + +message ModelInfoProto { + string id = 1; + string name = 2; + int64 context_window = 3; + int64 max_output_tokens = 4; + repeated string capabilities = 5; + map metadata = 6; +} + +message ModelsResponse { + repeated ModelInfoProto models = 1; +} + +message RouteDefinitionProto { + string method = 1; + string path = 2; + string description = 3; + map metadata = 4; +} + +message RoutesResponse { + repeated RouteDefinitionProto routes = 1; +} + +message AccountProto { + int64 id = 1; + string name = 2; + string platform = 3; + string type = 4; + bytes credentials_json = 5; + string proxy_url = 6; +} + +message ForwardRequest { + reserved 1, 2, 3, 4, 5, 6; // 保留历史字段号,避免 wire 级复用 + bytes body = 7; + map headers = 8; + string model = 9; + bool stream = 10; + AccountProto account = 11; +} + +// OutcomeKind 插件对一次 Forward 的判决。 +// 字段值与 sdk.OutcomeKind 常量一一对应;0 = UNKNOWN(插件未声明)。 +enum OutcomeKind { + OUTCOME_UNKNOWN = 0; + OUTCOME_SUCCESS = 1; + OUTCOME_CLIENT_ERROR = 2; + OUTCOME_ACCOUNT_RATE_LIMITED = 3; + OUTCOME_ACCOUNT_DEAD = 4; + OUTCOME_UPSTREAM_TRANSIENT = 5; + OUTCOME_STREAM_ABORTED = 6; + OUTCOME_ACCOUNT_MODEL_UNSUPPORTED = 7; +} + +// UpstreamResponse 上游返回的原始 HTTP 快照。 +message UpstreamResponse { + int32 status_code = 1; + map headers = 2; + bytes body = 3; +} + +// UsageAttribute 是插件计算后的通用审计维度。 +message UsageAttribute { + string key = 1; + string label = 2; + string kind = 3; + string value = 4; + map metadata = 5; +} + +// UsageMetric 是插件计算后的通用计量结果。 +message UsageMetric { + string key = 1; + string label = 2; + string kind = 3; + string unit = 4; + double value = 5; + double account_cost = 6; + string currency = 7; + map metadata = 8; +} + +// UsageCostDetail 是通用费用明细。 +message UsageCostDetail { + string key = 1; + string label = 2; + double account_cost = 3; + double user_cost = 4; + double billing_multiplier = 5; + string currency = 6; + map metadata = 7; +} + +// Usage 单次调用的用量与费用结果。非 Success 判决下应为空。 +message Usage { + string model = 1; + double account_cost = 2; + double user_cost = 3; + double billing_multiplier = 4; + string currency = 5; + string summary = 6; + int64 first_token_ms = 7; + repeated UsageMetric metrics = 8; + repeated UsageAttribute attributes = 9; + repeated UsageCostDetail cost_details = 10; + map metadata = 11; +} + +// ForwardOutcome 插件对一次 Forward 的完整判决结果。 +message ForwardOutcome { + OutcomeKind kind = 1; + UpstreamResponse upstream = 2; + Usage usage = 3; + int64 duration_ms = 4; + int64 retry_after_ms = 5; + string reason = 6; + map updated_credentials = 7; +} + +message ForwardChunk { + bytes data = 1; + bool done = 2; + ForwardOutcome final_outcome = 3; + int32 status_code = 4; + map headers = 5; +} + +message CredentialsRequest { + map credentials = 1; +} + +// ==================== HTTP 代理消息 ==================== + +message HttpRequest { + string method = 1; + string path = 2; + string query = 3; + map headers = 4; + bytes body = 5; + string remote_addr = 6; +} + +message HttpResponse { + int32 status_code = 1; + map headers = 2; + bytes body = 3; +} + +message HttpResponseChunk { + bytes data = 1; + bool done = 2; + int32 status_code = 3; + map headers = 4; +} + +// ==================== 扩展消息 ==================== + +message BackgroundTaskProto { + string name = 1; + int64 interval_ms = 2; +} + +message BackgroundTasksResponse { + repeated BackgroundTaskProto tasks = 1; +} + +message RunBackgroundTaskRequest { + string name = 1; +} + +// ==================== WebSocket 消息 ==================== + +message WebSocketFrame { + enum FrameType { + CONNECT = 0; // 连接建立,携带元信息(仅第一帧) + TEXT = 1; // 文本消息 + BINARY = 2; // 二进制消息 + CLOSE = 3; // 关闭连接 + RESULT = 4; // 连接结束,携带 ForwardOutcome + } + + FrameType type = 1; + bytes data = 2; + + // 仅 CONNECT 帧使用 + WebSocketConnectInfo connect_info = 3; + + // 仅 CLOSE 帧使用 + int32 close_code = 4; + string close_reason = 5; + + // 仅 RESULT 帧使用 + ForwardOutcome outcome = 6; +} + +message WebSocketConnectInfo { + string path = 1; + string query = 2; + map headers = 3; + string remote_addr = 4; + string connection_id = 5; + reserved 6, 7, 8, 9, 10, 11; // 保留历史字段号,避免 wire 级复用 + AccountProto account = 12; +} + +// ==================== 前端资源 ==================== + +message WebAssetFile { + string path = 1; + bytes content = 2; +} + +message WebAssetsResponse { + repeated WebAssetFile files = 1; + bool has_assets = 2; +} + +// ==================== 插件能力清单 ==================== + +message PayloadSchemaProto { + string content_type = 1; + string schema = 2; // JSON Schema 字符串 + string example = 3; // JSON 示例字符串 + map metadata = 4; +} + +message RouteSchemaProto { + string method = 1; + string path = 2; + string summary = 3; + PayloadSchemaProto request = 4; + PayloadSchemaProto response = 5; + map metadata = 6; +} + +message TaskSchemaProto { + string type = 1; + string summary = 2; + PayloadSchemaProto input = 3; + PayloadSchemaProto output = 4; + map metadata = 5; +} + +message EventSchemaProto { + string type = 1; + string source = 2; + string summary = 3; + PayloadSchemaProto payload = 4; + map metadata = 5; +} + +message InvokeSchemaProto { + string method = 1; + string summary = 2; + PayloadSchemaProto request = 3; + PayloadSchemaProto response = 4; + map metadata = 5; + string transport = 6; // unary / server_stream / client_stream / bidirectional_stream + PayloadSchemaProto client_frame = 7; // InvokeStream client frame payload schema + PayloadSchemaProto server_frame = 8; // InvokeStream server frame payload schema +} + +message PluginSchemaResponse { + repeated RouteSchemaProto routes = 1; + repeated TaskSchemaProto tasks = 2; + repeated EventSchemaProto events = 3; + repeated InvokeSchemaProto invokes = 4; + map metadata = 5; +} + +// ==================== 中间件消息 ==================== +// +// MiddlewareRequest / MiddlewareEvent / MiddlewareDecision 用于 MiddlewareService。 + +// MiddlewareRequest OnForwardBegin 的输入:请求元数据 + 可选的 request body。 +message MiddlewareRequest { + // === 核心元数据(默认总是填充)=== + string request_id = 1; // core 为本次请求分配的唯一 ID(便于跨 Begin/End 关联) + int64 user_id = 2; + int64 group_id = 3; + int64 account_id = 4; // 已由 scheduler 选出 + string platform = 5; + string model = 6; + bool stream = 7; + repeated UsageMetric estimates = 8; // Core 或插件提供的通用预估值,仅用于早期决策 + + // metadata KV bag:供多个 middleware 之间传递上下文。 + // 命名空间规则由 Core 管理,插件不应依赖未声明的敏感字段。 + map metadata = 9; + + // === 按需字段(声明了 middleware.read_body capability 的插件才会收到)=== + bytes request_body = 100; + map request_headers = 101; +} + +// MiddlewareEvent OnForwardEnd 的输入:完整的请求 + 响应元数据。 +message MiddlewareEvent { + // === 核心元数据(与 MiddlewareRequest 对齐)=== + string request_id = 1; + int64 user_id = 2; + int64 group_id = 3; + int64 account_id = 4; + string platform = 5; + string model = 6; + bool stream = 7; + repeated UsageMetric estimates = 8; // 与 MiddlewareRequest 字段对齐,便于 estimate vs actual 比对。 + + // === 响应结果 === + int64 status_code = 20; + int64 duration_ms = 21; + Usage usage = 22; + string error_kind = 23; // "" / "upstream_5xx" / "timeout" / "no_account" / ... + string error_msg = 24; // 限长 512,见 core 实现 + + // metadata 延续自 OnForwardBegin 的 bag + map metadata = 40; + + // === 按需字段(声明了 middleware.read_body capability 的插件才会收到)=== + // 流式响应时 response_body 只给摘要(首次非空 chunk 拼装)。 + bytes response_body = 100; + map response_headers = 101; +} + +// MiddlewareDecision OnForwardBegin 的输出:放行 / 拒绝 / 改请求。 +message MiddlewareDecision { + enum Action { + ALLOW = 0; // 默认放行 + DENY = 1; // 拒绝请求,status_code + error_message 会直接返回给用户 + MUTATE = 2; // 放行但修改请求(当前只支持 set_headers) + } + Action action = 1; + + // action=DENY 时的错误码 / 文案(对用户可见) + int32 deny_status_code = 10; // 默认 403 if Action=DENY and 未指定 + string deny_message = 11; + + // action=MUTATE 时要追加/覆盖的请求头 + map set_headers = 20; + + // 贯穿式 metadata:无论 allow/deny/mutate,都能往 bag 里写东西供后续 middleware / End 使用 + map metadata = 30; +} + +// ==================== 事件消息 ==================== + +message EventSubscriptionProto { + string type = 1; + string source = 2; + map filter = 3; + map metadata = 4; +} + +message EventSubscriptionsResponse { + repeated EventSubscriptionProto subscriptions = 1; +} + +message PluginEvent { + string id = 1; + string type = 2; + string source = 3; + string subject = 4; + int64 user_id = 5; + int64 group_id = 6; + bytes payload = 7; // JSON 编码的对象 + map metadata = 8; + int64 occurred_at = 9; // unix millis, 0 = unset +} + +message EventHandleResponse { + bool success = 1; + string error_message = 2; +} + +// ==================== Host 调用消息 ==================== + +message HostInvokeRequest { + string method = 1; + bytes payload = 2; // JSON 编码的对象,由 Core method 自己校验 schema + string idempotency_key = 3; // 副作用方法的幂等键;只读方法可留空 + map metadata = 4; // 调用级辅助信息,不用于替代权限、调度或核心业务字段 +} + +message HostInvokeResponse { + string status = 1; // method 业务状态;传输/鉴权/schema 错误走 gRPC error + bytes payload = 2; // JSON 编码的对象 + map metadata = 3; // 调用级辅助信息 +} + +message HostStreamFrame { + string method = 1; // 首个 client frame 必填;后续 frame 可留空 + string event = 2; // method 内部约定的帧类型 + bytes payload = 3; // JSON 编码的对象 + string idempotency_key = 4; // 首个 client frame 使用 + map metadata = 5; // 调用级或帧级辅助信息 + bool done = 6; // method 业务最终帧;传输结束仍以 stream EOF 为准 + string status = 7; // method 业务状态,通常只在最终帧使用 +} + +// ==================== 异步任务 ==================== + +// Core → Extension (task dispatch) + +message ProcessTaskRequest { + int64 task_id = 1; + string task_type = 2; + bytes input = 3; // JSON + int64 user_id = 4; +} + +message ProcessTaskResponse { + bool success = 1; + string error_message = 2; +} + +message TaskTypesResponse { + repeated string types = 1; +} diff --git a/proto/plugin_grpc.pb.go b/protocol/proto/plugin_grpc.pb.go similarity index 69% rename from proto/plugin_grpc.pb.go rename to protocol/proto/plugin_grpc.pb.go index c1e1487..db9b4ce 100644 --- a/proto/plugin_grpc.pb.go +++ b/protocol/proto/plugin_grpc.pb.go @@ -24,6 +24,7 @@ const ( PluginService_Start_FullMethodName = "/airgate.plugin.v1.PluginService/Start" PluginService_Stop_FullMethodName = "/airgate.plugin.v1.PluginService/Stop" PluginService_GetWebAssets_FullMethodName = "/airgate.plugin.v1.PluginService/GetWebAssets" + PluginService_GetSchema_FullMethodName = "/airgate.plugin.v1.PluginService/GetSchema" PluginService_HealthCheck_FullMethodName = "/airgate.plugin.v1.PluginService/HealthCheck" PluginService_HandleRequest_FullMethodName = "/airgate.plugin.v1.PluginService/HandleRequest" ) @@ -38,6 +39,8 @@ type PluginServiceClient interface { Stop(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*Empty, error) // 获取插件的前端静态资源 GetWebAssets(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*WebAssetsResponse, error) + // 获取插件结构化能力清单(可选,未实现时返回空) + GetSchema(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*PluginSchemaResponse, error) // 健康检查(可选,插件未实现时返回成功) HealthCheck(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*Empty, error) // 通用请求代理:Core 透传,插件自行路由(可选,插件未实现时返回 501) @@ -102,6 +105,16 @@ func (c *pluginServiceClient) GetWebAssets(ctx context.Context, in *Empty, opts return out, nil } +func (c *pluginServiceClient) GetSchema(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*PluginSchemaResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(PluginSchemaResponse) + err := c.cc.Invoke(ctx, PluginService_GetSchema_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *pluginServiceClient) HealthCheck(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*Empty, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(Empty) @@ -132,6 +145,8 @@ type PluginServiceServer interface { Stop(context.Context, *Empty) (*Empty, error) // 获取插件的前端静态资源 GetWebAssets(context.Context, *Empty) (*WebAssetsResponse, error) + // 获取插件结构化能力清单(可选,未实现时返回空) + GetSchema(context.Context, *Empty) (*PluginSchemaResponse, error) // 健康检查(可选,插件未实现时返回成功) HealthCheck(context.Context, *Empty) (*Empty, error) // 通用请求代理:Core 透传,插件自行路由(可选,插件未实现时返回 501) @@ -161,6 +176,9 @@ func (UnimplementedPluginServiceServer) Stop(context.Context, *Empty) (*Empty, e func (UnimplementedPluginServiceServer) GetWebAssets(context.Context, *Empty) (*WebAssetsResponse, error) { return nil, status.Error(codes.Unimplemented, "method GetWebAssets not implemented") } +func (UnimplementedPluginServiceServer) GetSchema(context.Context, *Empty) (*PluginSchemaResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetSchema not implemented") +} func (UnimplementedPluginServiceServer) HealthCheck(context.Context, *Empty) (*Empty, error) { return nil, status.Error(codes.Unimplemented, "method HealthCheck not implemented") } @@ -278,6 +296,24 @@ func _PluginService_GetWebAssets_Handler(srv interface{}, ctx context.Context, d return interceptor(ctx, in, info, handler) } +func _PluginService_GetSchema_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PluginServiceServer).GetSchema(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: PluginService_GetSchema_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PluginServiceServer).GetSchema(ctx, req.(*Empty)) + } + return interceptor(ctx, in, info, handler) +} + func _PluginService_HealthCheck_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(Empty) if err := dec(in); err != nil { @@ -341,6 +377,10 @@ var PluginService_ServiceDesc = grpc.ServiceDesc{ MethodName: "GetWebAssets", Handler: _PluginService_GetWebAssets_Handler, }, + { + MethodName: "GetSchema", + Handler: _PluginService_GetSchema_Handler, + }, { MethodName: "HealthCheck", Handler: _PluginService_HealthCheck_Handler, @@ -361,7 +401,6 @@ const ( GatewayService_Forward_FullMethodName = "/airgate.plugin.v1.GatewayService/Forward" GatewayService_ForwardStream_FullMethodName = "/airgate.plugin.v1.GatewayService/ForwardStream" GatewayService_ValidateAccount_FullMethodName = "/airgate.plugin.v1.GatewayService/ValidateAccount" - GatewayService_QueryQuota_FullMethodName = "/airgate.plugin.v1.GatewayService/QueryQuota" GatewayService_HandleWebSocket_FullMethodName = "/airgate.plugin.v1.GatewayService/HandleWebSocket" ) @@ -375,7 +414,6 @@ type GatewayServiceClient interface { Forward(ctx context.Context, in *ForwardRequest, opts ...grpc.CallOption) (*ForwardOutcome, error) ForwardStream(ctx context.Context, in *ForwardRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ForwardChunk], error) ValidateAccount(ctx context.Context, in *CredentialsRequest, opts ...grpc.CallOption) (*Empty, error) - QueryQuota(ctx context.Context, in *CredentialsRequest, opts ...grpc.CallOption) (*QuotaInfoResponse, error) // WebSocket 双向流 HandleWebSocket(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[WebSocketFrame, WebSocketFrame], error) } @@ -457,16 +495,6 @@ func (c *gatewayServiceClient) ValidateAccount(ctx context.Context, in *Credenti return out, nil } -func (c *gatewayServiceClient) QueryQuota(ctx context.Context, in *CredentialsRequest, opts ...grpc.CallOption) (*QuotaInfoResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(QuotaInfoResponse) - err := c.cc.Invoke(ctx, GatewayService_QueryQuota_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - func (c *gatewayServiceClient) HandleWebSocket(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[WebSocketFrame, WebSocketFrame], error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) stream, err := c.cc.NewStream(ctx, &GatewayService_ServiceDesc.Streams[1], GatewayService_HandleWebSocket_FullMethodName, cOpts...) @@ -490,7 +518,6 @@ type GatewayServiceServer interface { Forward(context.Context, *ForwardRequest) (*ForwardOutcome, error) ForwardStream(*ForwardRequest, grpc.ServerStreamingServer[ForwardChunk]) error ValidateAccount(context.Context, *CredentialsRequest) (*Empty, error) - QueryQuota(context.Context, *CredentialsRequest) (*QuotaInfoResponse, error) // WebSocket 双向流 HandleWebSocket(grpc.BidiStreamingServer[WebSocketFrame, WebSocketFrame]) error mustEmbedUnimplementedGatewayServiceServer() @@ -521,9 +548,6 @@ func (UnimplementedGatewayServiceServer) ForwardStream(*ForwardRequest, grpc.Ser func (UnimplementedGatewayServiceServer) ValidateAccount(context.Context, *CredentialsRequest) (*Empty, error) { return nil, status.Error(codes.Unimplemented, "method ValidateAccount not implemented") } -func (UnimplementedGatewayServiceServer) QueryQuota(context.Context, *CredentialsRequest) (*QuotaInfoResponse, error) { - return nil, status.Error(codes.Unimplemented, "method QueryQuota not implemented") -} func (UnimplementedGatewayServiceServer) HandleWebSocket(grpc.BidiStreamingServer[WebSocketFrame, WebSocketFrame]) error { return status.Error(codes.Unimplemented, "method HandleWebSocket not implemented") } @@ -649,24 +673,6 @@ func _GatewayService_ValidateAccount_Handler(srv interface{}, ctx context.Contex return interceptor(ctx, in, info, handler) } -func _GatewayService_QueryQuota_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(CredentialsRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(GatewayServiceServer).QueryQuota(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: GatewayService_QueryQuota_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(GatewayServiceServer).QueryQuota(ctx, req.(*CredentialsRequest)) - } - return interceptor(ctx, in, info, handler) -} - func _GatewayService_HandleWebSocket_Handler(srv interface{}, stream grpc.ServerStream) error { return srv.(GatewayServiceServer).HandleWebSocket(&grpc.GenericServerStream[WebSocketFrame, WebSocketFrame]{ServerStream: stream}) } @@ -701,10 +707,6 @@ var GatewayService_ServiceDesc = grpc.ServiceDesc{ MethodName: "ValidateAccount", Handler: _GatewayService_ValidateAccount_Handler, }, - { - MethodName: "QueryQuota", - Handler: _GatewayService_QueryQuota_Handler, - }, }, Streams: []grpc.StreamDesc{ { @@ -728,6 +730,8 @@ const ( ExtensionService_RunBackgroundTask_FullMethodName = "/airgate.plugin.v1.ExtensionService/RunBackgroundTask" ExtensionService_HandleRequest_FullMethodName = "/airgate.plugin.v1.ExtensionService/HandleRequest" ExtensionService_HandleStreamRequest_FullMethodName = "/airgate.plugin.v1.ExtensionService/HandleStreamRequest" + ExtensionService_ProcessTask_FullMethodName = "/airgate.plugin.v1.ExtensionService/ProcessTask" + ExtensionService_GetTaskTypes_FullMethodName = "/airgate.plugin.v1.ExtensionService/GetTaskTypes" ) // ExtensionServiceClient is the client API for ExtensionService service. @@ -741,6 +745,10 @@ type ExtensionServiceClient interface { // HTTP 请求由核心代理到插件 HandleRequest(ctx context.Context, in *HttpRequest, opts ...grpc.CallOption) (*HttpResponse, error) HandleStreamRequest(ctx context.Context, in *HttpRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[HttpResponseChunk], error) + // 异步任务:Core 分发 pending 任务给插件处理 + ProcessTask(ctx context.Context, in *ProcessTaskRequest, opts ...grpc.CallOption) (*ProcessTaskResponse, error) + // 返回此插件支持的任务类型列表 + GetTaskTypes(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*TaskTypesResponse, error) } type extensionServiceClient struct { @@ -810,6 +818,26 @@ func (c *extensionServiceClient) HandleStreamRequest(ctx context.Context, in *Ht // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. type ExtensionService_HandleStreamRequestClient = grpc.ServerStreamingClient[HttpResponseChunk] +func (c *extensionServiceClient) ProcessTask(ctx context.Context, in *ProcessTaskRequest, opts ...grpc.CallOption) (*ProcessTaskResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ProcessTaskResponse) + err := c.cc.Invoke(ctx, ExtensionService_ProcessTask_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *extensionServiceClient) GetTaskTypes(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*TaskTypesResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(TaskTypesResponse) + err := c.cc.Invoke(ctx, ExtensionService_GetTaskTypes_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // ExtensionServiceServer is the server API for ExtensionService service. // All implementations must embed UnimplementedExtensionServiceServer // for forward compatibility. @@ -821,6 +849,10 @@ type ExtensionServiceServer interface { // HTTP 请求由核心代理到插件 HandleRequest(context.Context, *HttpRequest) (*HttpResponse, error) HandleStreamRequest(*HttpRequest, grpc.ServerStreamingServer[HttpResponseChunk]) error + // 异步任务:Core 分发 pending 任务给插件处理 + ProcessTask(context.Context, *ProcessTaskRequest) (*ProcessTaskResponse, error) + // 返回此插件支持的任务类型列表 + GetTaskTypes(context.Context, *Empty) (*TaskTypesResponse, error) mustEmbedUnimplementedExtensionServiceServer() } @@ -846,6 +878,12 @@ func (UnimplementedExtensionServiceServer) HandleRequest(context.Context, *HttpR func (UnimplementedExtensionServiceServer) HandleStreamRequest(*HttpRequest, grpc.ServerStreamingServer[HttpResponseChunk]) error { return status.Error(codes.Unimplemented, "method HandleStreamRequest not implemented") } +func (UnimplementedExtensionServiceServer) ProcessTask(context.Context, *ProcessTaskRequest) (*ProcessTaskResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ProcessTask not implemented") +} +func (UnimplementedExtensionServiceServer) GetTaskTypes(context.Context, *Empty) (*TaskTypesResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetTaskTypes not implemented") +} func (UnimplementedExtensionServiceServer) mustEmbedUnimplementedExtensionServiceServer() {} func (UnimplementedExtensionServiceServer) testEmbeddedByValue() {} @@ -950,6 +988,42 @@ func _ExtensionService_HandleStreamRequest_Handler(srv interface{}, stream grpc. // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. type ExtensionService_HandleStreamRequestServer = grpc.ServerStreamingServer[HttpResponseChunk] +func _ExtensionService_ProcessTask_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ProcessTaskRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ExtensionServiceServer).ProcessTask(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ExtensionService_ProcessTask_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ExtensionServiceServer).ProcessTask(ctx, req.(*ProcessTaskRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ExtensionService_GetTaskTypes_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ExtensionServiceServer).GetTaskTypes(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ExtensionService_GetTaskTypes_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ExtensionServiceServer).GetTaskTypes(ctx, req.(*Empty)) + } + return interceptor(ctx, in, info, handler) +} + // ExtensionService_ServiceDesc is the grpc.ServiceDesc for ExtensionService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -973,6 +1047,14 @@ var ExtensionService_ServiceDesc = grpc.ServiceDesc{ MethodName: "HandleRequest", Handler: _ExtensionService_HandleRequest_Handler, }, + { + MethodName: "ProcessTask", + Handler: _ExtensionService_ProcessTask_Handler, + }, + { + MethodName: "GetTaskTypes", + Handler: _ExtensionService_GetTaskTypes_Handler, + }, }, Streams: []grpc.StreamDesc{ { @@ -999,14 +1081,14 @@ const ( // - OnForwardBegin — 选完账号 / 还没调 upstream 之前,能改 headers / 拒绝请求 // - OnForwardEnd — upstream 返回之后 / 写 usage_log 之前,拿到完整 metadata // -// 设计原则(详见 ADR-0001 Decision 2/3): +// 设计原则: // 1. middleware 挂了不能 block 生产:返回 error 只 log warn,流程继续; // 只有 OnForwardBegin 明确返回 Action=DENY 才会拒绝请求 // 2. 多个 middleware 按 priority 排序。Begin 升序、End 降序(LIFO) // 3. payload 两段式:默认只传元数据,声明 middleware.read_body capability 的插件 // 才会收到 request_body / response_body -// 4. 流式响应的 response_body 只给摘要(首次非空 chunk 拼装),完整流式内容留给 -// 未来的 OnStreamChunk(ADR-0002) +// 4. 流式响应的 response_body 只给摘要(首次非空 chunk 拼装),完整流式内容应由 +// 独立的流式事件能力承载 // // 新角色:middleware 插件不是 gateway(不替代 upstream),也不是 extension // (不跑后台任务 + 自定义 HTTP)。它在 PluginInfo.type = "middleware" 中声明。 @@ -1053,14 +1135,14 @@ func (c *middlewareServiceClient) OnForwardEnd(ctx context.Context, in *Middlewa // - OnForwardBegin — 选完账号 / 还没调 upstream 之前,能改 headers / 拒绝请求 // - OnForwardEnd — upstream 返回之后 / 写 usage_log 之前,拿到完整 metadata // -// 设计原则(详见 ADR-0001 Decision 2/3): +// 设计原则: // 1. middleware 挂了不能 block 生产:返回 error 只 log warn,流程继续; // 只有 OnForwardBegin 明确返回 Action=DENY 才会拒绝请求 // 2. 多个 middleware 按 priority 排序。Begin 升序、End 降序(LIFO) // 3. payload 两段式:默认只传元数据,声明 middleware.read_body capability 的插件 // 才会收到 request_body / response_body -// 4. 流式响应的 response_body 只给摘要(首次非空 chunk 拼装),完整流式内容留给 -// 未来的 OnStreamChunk(ADR-0002) +// 4. 流式响应的 response_body 只给摘要(首次非空 chunk 拼装),完整流式内容应由 +// 独立的流式事件能力承载 // // 新角色:middleware 插件不是 gateway(不替代 upstream),也不是 extension // (不跑后台任务 + 自定义 HTTP)。它在 PluginInfo.type = "middleware" 中声明。 @@ -1161,550 +1243,287 @@ var MiddlewareService_ServiceDesc = grpc.ServiceDesc{ } const ( - HostService_SelectAccount_FullMethodName = "/airgate.plugin.v1.HostService/SelectAccount" - HostService_ReportAccountResult_FullMethodName = "/airgate.plugin.v1.HostService/ReportAccountResult" - HostService_ProbeForward_FullMethodName = "/airgate.plugin.v1.HostService/ProbeForward" - HostService_Forward_FullMethodName = "/airgate.plugin.v1.HostService/Forward" - HostService_ForwardStream_FullMethodName = "/airgate.plugin.v1.HostService/ForwardStream" - HostService_ListGroups_FullMethodName = "/airgate.plugin.v1.HostService/ListGroups" - HostService_ListPlatforms_FullMethodName = "/airgate.plugin.v1.HostService/ListPlatforms" - HostService_ListModels_FullMethodName = "/airgate.plugin.v1.HostService/ListModels" - HostService_GetUserInfo_FullMethodName = "/airgate.plugin.v1.HostService/GetUserInfo" - HostService_StoreAsset_FullMethodName = "/airgate.plugin.v1.HostService/StoreAsset" - HostService_GetAssetURL_FullMethodName = "/airgate.plugin.v1.HostService/GetAssetURL" - HostService_GetAssetBytes_FullMethodName = "/airgate.plugin.v1.HostService/GetAssetBytes" + EventService_GetEventSubscriptions_FullMethodName = "/airgate.plugin.v1.EventService/GetEventSubscriptions" + EventService_HandleEvent_FullMethodName = "/airgate.plugin.v1.EventService/HandleEvent" ) -// HostServiceClient is the client API for HostService service. +// EventServiceClient is the client API for EventService 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 HostServiceClient interface { - // 选号:根据 (group_id, model) 走和真实用户请求完全相同的调度路径。 - SelectAccount(ctx context.Context, in *HostSelectAccountRequest, opts ...grpc.CallOption) (*HostSelectAccountResponse, error) - // 把账号调用结果反馈给 scheduler 的失败计数器/状态机。 - ReportAccountResult(ctx context.Context, in *HostReportAccountResultRequest, opts ...grpc.CallOption) (*Empty, error) - // 黑盒探测:内部组装一次最小的 chat completion 请求并直接执行。 - // 跳过 usage_log 写入、跳过用户余额扣款,仍然 ReportResult 反哺账号状态机。 - ProbeForward(ctx context.Context, in *HostProbeForwardRequest, opts ...grpc.CallOption) (*HostProbeForwardResponse, error) - // 非流式业务转发:走完整管线(调度 → 网关插件 → 计费 → usage_log)。 - // 调用方需提供 user_id + group_id 以确定计费主体和调度路径。 - Forward(ctx context.Context, in *HostForwardRequest, opts ...grpc.CallOption) (*HostForwardResponse, error) - // 流式业务转发:与 Forward 相同管线,结果通过 server stream 逐块返回。 - // 最后一块 done=true 携带 usage 信息(计费已在 Core 侧完成)。 - ForwardStream(ctx context.Context, in *HostForwardRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[HostForwardChunk], error) - // 列出所有分组。 - ListGroups(ctx context.Context, in *HostListGroupsRequest, opts ...grpc.CallOption) (*HostListGroupsResponse, error) - // 列出已加载的网关平台(每个 gateway 插件对应一个 platform)。 - ListPlatforms(ctx context.Context, in *HostListPlatformsRequest, opts ...grpc.CallOption) (*HostListPlatformsResponse, error) - // 列出指定平台的模型列表。 - ListModels(ctx context.Context, in *HostListModelsRequest, opts ...grpc.CallOption) (*HostListModelsResponse, error) - // 获取用户基本信息(余额、角色、状态)。 - GetUserInfo(ctx context.Context, in *HostGetUserInfoRequest, opts ...grpc.CallOption) (*HostGetUserInfoResponse, error) - // 资产存储:由 Core 根据全局 storage 设置选择 MinIO/S3 或本地磁盘。 - StoreAsset(ctx context.Context, in *HostStoreAssetRequest, opts ...grpc.CallOption) (*HostStoreAssetResponse, error) - GetAssetURL(ctx context.Context, in *HostGetAssetURLRequest, opts ...grpc.CallOption) (*HostGetAssetURLResponse, error) - GetAssetBytes(ctx context.Context, in *HostGetAssetBytesRequest, opts ...grpc.CallOption) (*HostGetAssetBytesResponse, error) -} - -type hostServiceClient struct { - cc grpc.ClientConnInterface -} - -func NewHostServiceClient(cc grpc.ClientConnInterface) HostServiceClient { - return &hostServiceClient{cc} -} - -func (c *hostServiceClient) SelectAccount(ctx context.Context, in *HostSelectAccountRequest, opts ...grpc.CallOption) (*HostSelectAccountResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(HostSelectAccountResponse) - err := c.cc.Invoke(ctx, HostService_SelectAccount_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *hostServiceClient) ReportAccountResult(ctx context.Context, in *HostReportAccountResultRequest, opts ...grpc.CallOption) (*Empty, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(Empty) - err := c.cc.Invoke(ctx, HostService_ReportAccountResult_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *hostServiceClient) ProbeForward(ctx context.Context, in *HostProbeForwardRequest, opts ...grpc.CallOption) (*HostProbeForwardResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(HostProbeForwardResponse) - err := c.cc.Invoke(ctx, HostService_ProbeForward_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *hostServiceClient) Forward(ctx context.Context, in *HostForwardRequest, opts ...grpc.CallOption) (*HostForwardResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(HostForwardResponse) - err := c.cc.Invoke(ctx, HostService_Forward_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *hostServiceClient) ForwardStream(ctx context.Context, in *HostForwardRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[HostForwardChunk], error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - stream, err := c.cc.NewStream(ctx, &HostService_ServiceDesc.Streams[0], HostService_ForwardStream_FullMethodName, cOpts...) - if err != nil { - return nil, err - } - x := &grpc.GenericClientStream[HostForwardRequest, HostForwardChunk]{ClientStream: stream} - if err := x.ClientStream.SendMsg(in); err != nil { - return nil, err - } - if err := x.ClientStream.CloseSend(); err != nil { - return nil, err - } - return x, nil +type EventServiceClient interface { + GetEventSubscriptions(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*EventSubscriptionsResponse, error) + HandleEvent(ctx context.Context, in *PluginEvent, opts ...grpc.CallOption) (*EventHandleResponse, error) } -// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. -type HostService_ForwardStreamClient = grpc.ServerStreamingClient[HostForwardChunk] - -func (c *hostServiceClient) ListGroups(ctx context.Context, in *HostListGroupsRequest, opts ...grpc.CallOption) (*HostListGroupsResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(HostListGroupsResponse) - err := c.cc.Invoke(ctx, HostService_ListGroups_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *hostServiceClient) ListPlatforms(ctx context.Context, in *HostListPlatformsRequest, opts ...grpc.CallOption) (*HostListPlatformsResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(HostListPlatformsResponse) - err := c.cc.Invoke(ctx, HostService_ListPlatforms_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil +type eventServiceClient struct { + cc grpc.ClientConnInterface } -func (c *hostServiceClient) ListModels(ctx context.Context, in *HostListModelsRequest, opts ...grpc.CallOption) (*HostListModelsResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(HostListModelsResponse) - err := c.cc.Invoke(ctx, HostService_ListModels_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil +func NewEventServiceClient(cc grpc.ClientConnInterface) EventServiceClient { + return &eventServiceClient{cc} } -func (c *hostServiceClient) GetUserInfo(ctx context.Context, in *HostGetUserInfoRequest, opts ...grpc.CallOption) (*HostGetUserInfoResponse, error) { +func (c *eventServiceClient) GetEventSubscriptions(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*EventSubscriptionsResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(HostGetUserInfoResponse) - err := c.cc.Invoke(ctx, HostService_GetUserInfo_FullMethodName, in, out, cOpts...) + out := new(EventSubscriptionsResponse) + err := c.cc.Invoke(ctx, EventService_GetEventSubscriptions_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } -func (c *hostServiceClient) StoreAsset(ctx context.Context, in *HostStoreAssetRequest, opts ...grpc.CallOption) (*HostStoreAssetResponse, error) { +func (c *eventServiceClient) HandleEvent(ctx context.Context, in *PluginEvent, opts ...grpc.CallOption) (*EventHandleResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(HostStoreAssetResponse) - err := c.cc.Invoke(ctx, HostService_StoreAsset_FullMethodName, in, out, cOpts...) + out := new(EventHandleResponse) + err := c.cc.Invoke(ctx, EventService_HandleEvent_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } -func (c *hostServiceClient) GetAssetURL(ctx context.Context, in *HostGetAssetURLRequest, opts ...grpc.CallOption) (*HostGetAssetURLResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(HostGetAssetURLResponse) - err := c.cc.Invoke(ctx, HostService_GetAssetURL_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *hostServiceClient) GetAssetBytes(ctx context.Context, in *HostGetAssetBytesRequest, opts ...grpc.CallOption) (*HostGetAssetBytesResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(HostGetAssetBytesResponse) - err := c.cc.Invoke(ctx, HostService_GetAssetBytes_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil +// EventServiceServer is the server API for EventService service. +// All implementations must embed UnimplementedEventServiceServer +// for forward compatibility. +type EventServiceServer interface { + GetEventSubscriptions(context.Context, *Empty) (*EventSubscriptionsResponse, error) + HandleEvent(context.Context, *PluginEvent) (*EventHandleResponse, error) + mustEmbedUnimplementedEventServiceServer() } -// HostServiceServer is the server API for HostService service. -// All implementations must embed UnimplementedHostServiceServer -// for forward compatibility. -type HostServiceServer interface { - // 选号:根据 (group_id, model) 走和真实用户请求完全相同的调度路径。 - SelectAccount(context.Context, *HostSelectAccountRequest) (*HostSelectAccountResponse, error) - // 把账号调用结果反馈给 scheduler 的失败计数器/状态机。 - ReportAccountResult(context.Context, *HostReportAccountResultRequest) (*Empty, error) - // 黑盒探测:内部组装一次最小的 chat completion 请求并直接执行。 - // 跳过 usage_log 写入、跳过用户余额扣款,仍然 ReportResult 反哺账号状态机。 - ProbeForward(context.Context, *HostProbeForwardRequest) (*HostProbeForwardResponse, error) - // 非流式业务转发:走完整管线(调度 → 网关插件 → 计费 → usage_log)。 - // 调用方需提供 user_id + group_id 以确定计费主体和调度路径。 - Forward(context.Context, *HostForwardRequest) (*HostForwardResponse, error) - // 流式业务转发:与 Forward 相同管线,结果通过 server stream 逐块返回。 - // 最后一块 done=true 携带 usage 信息(计费已在 Core 侧完成)。 - ForwardStream(*HostForwardRequest, grpc.ServerStreamingServer[HostForwardChunk]) error - // 列出所有分组。 - ListGroups(context.Context, *HostListGroupsRequest) (*HostListGroupsResponse, error) - // 列出已加载的网关平台(每个 gateway 插件对应一个 platform)。 - ListPlatforms(context.Context, *HostListPlatformsRequest) (*HostListPlatformsResponse, error) - // 列出指定平台的模型列表。 - ListModels(context.Context, *HostListModelsRequest) (*HostListModelsResponse, error) - // 获取用户基本信息(余额、角色、状态)。 - GetUserInfo(context.Context, *HostGetUserInfoRequest) (*HostGetUserInfoResponse, error) - // 资产存储:由 Core 根据全局 storage 设置选择 MinIO/S3 或本地磁盘。 - StoreAsset(context.Context, *HostStoreAssetRequest) (*HostStoreAssetResponse, error) - GetAssetURL(context.Context, *HostGetAssetURLRequest) (*HostGetAssetURLResponse, error) - GetAssetBytes(context.Context, *HostGetAssetBytesRequest) (*HostGetAssetBytesResponse, error) - mustEmbedUnimplementedHostServiceServer() -} - -// UnimplementedHostServiceServer must be embedded to have +// UnimplementedEventServiceServer 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{} +type UnimplementedEventServiceServer struct{} -func (UnimplementedHostServiceServer) SelectAccount(context.Context, *HostSelectAccountRequest) (*HostSelectAccountResponse, error) { - return nil, status.Error(codes.Unimplemented, "method SelectAccount not implemented") -} -func (UnimplementedHostServiceServer) ReportAccountResult(context.Context, *HostReportAccountResultRequest) (*Empty, error) { - return nil, status.Error(codes.Unimplemented, "method ReportAccountResult not implemented") -} -func (UnimplementedHostServiceServer) ProbeForward(context.Context, *HostProbeForwardRequest) (*HostProbeForwardResponse, error) { - return nil, status.Error(codes.Unimplemented, "method ProbeForward not implemented") -} -func (UnimplementedHostServiceServer) Forward(context.Context, *HostForwardRequest) (*HostForwardResponse, error) { - return nil, status.Error(codes.Unimplemented, "method Forward not implemented") -} -func (UnimplementedHostServiceServer) ForwardStream(*HostForwardRequest, grpc.ServerStreamingServer[HostForwardChunk]) error { - return status.Error(codes.Unimplemented, "method ForwardStream not implemented") -} -func (UnimplementedHostServiceServer) ListGroups(context.Context, *HostListGroupsRequest) (*HostListGroupsResponse, error) { - return nil, status.Error(codes.Unimplemented, "method ListGroups not implemented") -} -func (UnimplementedHostServiceServer) ListPlatforms(context.Context, *HostListPlatformsRequest) (*HostListPlatformsResponse, error) { - return nil, status.Error(codes.Unimplemented, "method ListPlatforms not implemented") -} -func (UnimplementedHostServiceServer) ListModels(context.Context, *HostListModelsRequest) (*HostListModelsResponse, error) { - return nil, status.Error(codes.Unimplemented, "method ListModels not implemented") +func (UnimplementedEventServiceServer) GetEventSubscriptions(context.Context, *Empty) (*EventSubscriptionsResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetEventSubscriptions not implemented") } -func (UnimplementedHostServiceServer) GetUserInfo(context.Context, *HostGetUserInfoRequest) (*HostGetUserInfoResponse, error) { - return nil, status.Error(codes.Unimplemented, "method GetUserInfo not implemented") +func (UnimplementedEventServiceServer) HandleEvent(context.Context, *PluginEvent) (*EventHandleResponse, error) { + return nil, status.Error(codes.Unimplemented, "method HandleEvent not implemented") } -func (UnimplementedHostServiceServer) StoreAsset(context.Context, *HostStoreAssetRequest) (*HostStoreAssetResponse, error) { - return nil, status.Error(codes.Unimplemented, "method StoreAsset not implemented") -} -func (UnimplementedHostServiceServer) GetAssetURL(context.Context, *HostGetAssetURLRequest) (*HostGetAssetURLResponse, error) { - return nil, status.Error(codes.Unimplemented, "method GetAssetURL not implemented") -} -func (UnimplementedHostServiceServer) GetAssetBytes(context.Context, *HostGetAssetBytesRequest) (*HostGetAssetBytesResponse, error) { - return nil, status.Error(codes.Unimplemented, "method GetAssetBytes not implemented") -} -func (UnimplementedHostServiceServer) mustEmbedUnimplementedHostServiceServer() {} -func (UnimplementedHostServiceServer) testEmbeddedByValue() {} +func (UnimplementedEventServiceServer) mustEmbedUnimplementedEventServiceServer() {} +func (UnimplementedEventServiceServer) 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 +// UnsafeEventServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to EventServiceServer will // result in compilation errors. -type UnsafeHostServiceServer interface { - mustEmbedUnimplementedHostServiceServer() +type UnsafeEventServiceServer interface { + mustEmbedUnimplementedEventServiceServer() } -func RegisterHostServiceServer(s grpc.ServiceRegistrar, srv HostServiceServer) { - // If the following call panics, it indicates UnimplementedHostServiceServer was +func RegisterEventServiceServer(s grpc.ServiceRegistrar, srv EventServiceServer) { + // If the following call panics, it indicates UnimplementedEventServiceServer 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) + s.RegisterService(&EventService_ServiceDesc, srv) } -func _HostService_SelectAccount_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(HostSelectAccountRequest) +func _EventService_GetEventSubscriptions_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Empty) if err := dec(in); err != nil { return nil, err } if interceptor == nil { - return srv.(HostServiceServer).SelectAccount(ctx, in) + return srv.(EventServiceServer).GetEventSubscriptions(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: HostService_SelectAccount_FullMethodName, + FullMethod: EventService_GetEventSubscriptions_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(HostServiceServer).SelectAccount(ctx, req.(*HostSelectAccountRequest)) + return srv.(EventServiceServer).GetEventSubscriptions(ctx, req.(*Empty)) } return interceptor(ctx, in, info, handler) } -func _HostService_ReportAccountResult_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(HostReportAccountResultRequest) +func _EventService_HandleEvent_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(PluginEvent) if err := dec(in); err != nil { return nil, err } if interceptor == nil { - return srv.(HostServiceServer).ReportAccountResult(ctx, in) + return srv.(EventServiceServer).HandleEvent(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: HostService_ReportAccountResult_FullMethodName, + FullMethod: EventService_HandleEvent_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(HostServiceServer).ReportAccountResult(ctx, req.(*HostReportAccountResultRequest)) + return srv.(EventServiceServer).HandleEvent(ctx, req.(*PluginEvent)) } return interceptor(ctx, in, info, handler) } -func _HostService_ProbeForward_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(HostProbeForwardRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(HostServiceServer).ProbeForward(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: HostService_ProbeForward_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(HostServiceServer).ProbeForward(ctx, req.(*HostProbeForwardRequest)) - } - return interceptor(ctx, in, info, handler) +// EventService_ServiceDesc is the grpc.ServiceDesc for EventService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var EventService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "airgate.plugin.v1.EventService", + HandlerType: (*EventServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "GetEventSubscriptions", + Handler: _EventService_GetEventSubscriptions_Handler, + }, + { + MethodName: "HandleEvent", + Handler: _EventService_HandleEvent_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "plugin.proto", } -func _HostService_Forward_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(HostForwardRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(HostServiceServer).Forward(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: HostService_Forward_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(HostServiceServer).Forward(ctx, req.(*HostForwardRequest)) - } - return interceptor(ctx, in, info, handler) +const ( + CoreInvokeService_Invoke_FullMethodName = "/airgate.plugin.v1.CoreInvokeService/Invoke" + CoreInvokeService_InvokeStream_FullMethodName = "/airgate.plugin.v1.CoreInvokeService/InvokeStream" +) + +// CoreInvokeServiceClient is the client API for CoreInvokeService 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. +// +// ==================== Host 服务(反向调用:插件 → Core) ==================== +// +// 由 Core 实现,通过 hashicorp/go-plugin 的 GRPCBroker 暴露给插件子进程。 +// SDK 只定义通用 Invoke / InvokeStream 通道;Core 通过方法注册表决定开放哪些方法、 +// 允许哪些插件调用、请求/响应 schema、是否支持流式以及幂等策略。 +type CoreInvokeServiceClient interface { + Invoke(ctx context.Context, in *HostInvokeRequest, opts ...grpc.CallOption) (*HostInvokeResponse, error) + InvokeStream(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[HostStreamFrame, HostStreamFrame], error) } -func _HostService_ForwardStream_Handler(srv interface{}, stream grpc.ServerStream) error { - m := new(HostForwardRequest) - if err := stream.RecvMsg(m); err != nil { - return err - } - return srv.(HostServiceServer).ForwardStream(m, &grpc.GenericServerStream[HostForwardRequest, HostForwardChunk]{ServerStream: stream}) +type coreInvokeServiceClient struct { + cc grpc.ClientConnInterface } -// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. -type HostService_ForwardStreamServer = grpc.ServerStreamingServer[HostForwardChunk] +func NewCoreInvokeServiceClient(cc grpc.ClientConnInterface) CoreInvokeServiceClient { + return &coreInvokeServiceClient{cc} +} -func _HostService_ListGroups_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(HostListGroupsRequest) - if err := dec(in); err != nil { +func (c *coreInvokeServiceClient) Invoke(ctx context.Context, in *HostInvokeRequest, opts ...grpc.CallOption) (*HostInvokeResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(HostInvokeResponse) + err := c.cc.Invoke(ctx, CoreInvokeService_Invoke_FullMethodName, in, out, cOpts...) + if err != nil { return nil, err } - if interceptor == nil { - return srv.(HostServiceServer).ListGroups(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: HostService_ListGroups_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(HostServiceServer).ListGroups(ctx, req.(*HostListGroupsRequest)) - } - return interceptor(ctx, in, info, handler) + return out, nil } -func _HostService_ListPlatforms_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(HostListPlatformsRequest) - if err := dec(in); err != nil { +func (c *coreInvokeServiceClient) InvokeStream(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[HostStreamFrame, HostStreamFrame], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &CoreInvokeService_ServiceDesc.Streams[0], CoreInvokeService_InvokeStream_FullMethodName, cOpts...) + if err != nil { return nil, err } - if interceptor == nil { - return srv.(HostServiceServer).ListPlatforms(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: HostService_ListPlatforms_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(HostServiceServer).ListPlatforms(ctx, req.(*HostListPlatformsRequest)) - } - return interceptor(ctx, in, info, handler) + x := &grpc.GenericClientStream[HostStreamFrame, HostStreamFrame]{ClientStream: stream} + return x, nil } -func _HostService_ListModels_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(HostListModelsRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(HostServiceServer).ListModels(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: HostService_ListModels_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(HostServiceServer).ListModels(ctx, req.(*HostListModelsRequest)) - } - return interceptor(ctx, in, info, handler) +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type CoreInvokeService_InvokeStreamClient = grpc.BidiStreamingClient[HostStreamFrame, HostStreamFrame] + +// CoreInvokeServiceServer is the server API for CoreInvokeService service. +// All implementations must embed UnimplementedCoreInvokeServiceServer +// for forward compatibility. +// +// ==================== Host 服务(反向调用:插件 → Core) ==================== +// +// 由 Core 实现,通过 hashicorp/go-plugin 的 GRPCBroker 暴露给插件子进程。 +// SDK 只定义通用 Invoke / InvokeStream 通道;Core 通过方法注册表决定开放哪些方法、 +// 允许哪些插件调用、请求/响应 schema、是否支持流式以及幂等策略。 +type CoreInvokeServiceServer interface { + Invoke(context.Context, *HostInvokeRequest) (*HostInvokeResponse, error) + InvokeStream(grpc.BidiStreamingServer[HostStreamFrame, HostStreamFrame]) error + mustEmbedUnimplementedCoreInvokeServiceServer() } -func _HostService_GetUserInfo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(HostGetUserInfoRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(HostServiceServer).GetUserInfo(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: HostService_GetUserInfo_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(HostServiceServer).GetUserInfo(ctx, req.(*HostGetUserInfoRequest)) - } - return interceptor(ctx, in, info, handler) +// UnimplementedCoreInvokeServiceServer 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 UnimplementedCoreInvokeServiceServer struct{} + +func (UnimplementedCoreInvokeServiceServer) Invoke(context.Context, *HostInvokeRequest) (*HostInvokeResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Invoke not implemented") +} +func (UnimplementedCoreInvokeServiceServer) InvokeStream(grpc.BidiStreamingServer[HostStreamFrame, HostStreamFrame]) error { + return status.Error(codes.Unimplemented, "method InvokeStream not implemented") } +func (UnimplementedCoreInvokeServiceServer) mustEmbedUnimplementedCoreInvokeServiceServer() {} +func (UnimplementedCoreInvokeServiceServer) testEmbeddedByValue() {} -func _HostService_StoreAsset_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(HostStoreAssetRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(HostServiceServer).StoreAsset(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: HostService_StoreAsset_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(HostServiceServer).StoreAsset(ctx, req.(*HostStoreAssetRequest)) - } - return interceptor(ctx, in, info, handler) +// UnsafeCoreInvokeServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to CoreInvokeServiceServer will +// result in compilation errors. +type UnsafeCoreInvokeServiceServer interface { + mustEmbedUnimplementedCoreInvokeServiceServer() } -func _HostService_GetAssetURL_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(HostGetAssetURLRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(HostServiceServer).GetAssetURL(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: HostService_GetAssetURL_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(HostServiceServer).GetAssetURL(ctx, req.(*HostGetAssetURLRequest)) +func RegisterCoreInvokeServiceServer(s grpc.ServiceRegistrar, srv CoreInvokeServiceServer) { + // If the following call panics, it indicates UnimplementedCoreInvokeServiceServer 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() } - return interceptor(ctx, in, info, handler) + s.RegisterService(&CoreInvokeService_ServiceDesc, srv) } -func _HostService_GetAssetBytes_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(HostGetAssetBytesRequest) +func _CoreInvokeService_Invoke_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(HostInvokeRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { - return srv.(HostServiceServer).GetAssetBytes(ctx, in) + return srv.(CoreInvokeServiceServer).Invoke(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: HostService_GetAssetBytes_FullMethodName, + FullMethod: CoreInvokeService_Invoke_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(HostServiceServer).GetAssetBytes(ctx, req.(*HostGetAssetBytesRequest)) + return srv.(CoreInvokeServiceServer).Invoke(ctx, req.(*HostInvokeRequest)) } return interceptor(ctx, in, info, handler) } -// HostService_ServiceDesc is the grpc.ServiceDesc for HostService service. +func _CoreInvokeService_InvokeStream_Handler(srv interface{}, stream grpc.ServerStream) error { + return srv.(CoreInvokeServiceServer).InvokeStream(&grpc.GenericServerStream[HostStreamFrame, HostStreamFrame]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type CoreInvokeService_InvokeStreamServer = grpc.BidiStreamingServer[HostStreamFrame, HostStreamFrame] + +// CoreInvokeService_ServiceDesc is the grpc.ServiceDesc for CoreInvokeService 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: "airgate.plugin.v1.HostService", - HandlerType: (*HostServiceServer)(nil), +var CoreInvokeService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "airgate.plugin.v1.CoreInvokeService", + HandlerType: (*CoreInvokeServiceServer)(nil), Methods: []grpc.MethodDesc{ { - MethodName: "SelectAccount", - Handler: _HostService_SelectAccount_Handler, - }, - { - MethodName: "ReportAccountResult", - Handler: _HostService_ReportAccountResult_Handler, - }, - { - MethodName: "ProbeForward", - Handler: _HostService_ProbeForward_Handler, - }, - { - MethodName: "Forward", - Handler: _HostService_Forward_Handler, - }, - { - MethodName: "ListGroups", - Handler: _HostService_ListGroups_Handler, - }, - { - MethodName: "ListPlatforms", - Handler: _HostService_ListPlatforms_Handler, - }, - { - MethodName: "ListModels", - Handler: _HostService_ListModels_Handler, - }, - { - MethodName: "GetUserInfo", - Handler: _HostService_GetUserInfo_Handler, - }, - { - MethodName: "StoreAsset", - Handler: _HostService_StoreAsset_Handler, - }, - { - MethodName: "GetAssetURL", - Handler: _HostService_GetAssetURL_Handler, - }, - { - MethodName: "GetAssetBytes", - Handler: _HostService_GetAssetBytes_Handler, + MethodName: "Invoke", + Handler: _CoreInvokeService_Invoke_Handler, }, }, Streams: []grpc.StreamDesc{ { - StreamName: "ForwardStream", - Handler: _HostService_ForwardStream_Handler, + StreamName: "InvokeStream", + Handler: _CoreInvokeService_InvokeStream_Handler, ServerStreams: true, + ClientStreams: true, }, }, Metadata: "plugin.proto", diff --git a/grpc/common.go b/runtimego/grpc/common.go similarity index 88% rename from grpc/common.go rename to runtimego/grpc/common.go index c800740..d071f1b 100644 --- a/grpc/common.go +++ b/runtimego/grpc/common.go @@ -6,8 +6,8 @@ import ( "net/http" "time" - sdk "github.com/DouDOU-start/airgate-sdk" - pb "github.com/DouDOU-start/airgate-sdk/proto" + pb "github.com/DouDOU-start/airgate-sdk/protocol/proto" + sdk "github.com/DouDOU-start/airgate-sdk/sdkgo" ) // defaultGRPCTimeout gRPC 内部调用的默认超时时间 @@ -21,17 +21,18 @@ func withTimeout() (context.Context, context.CancelFunc) { // pluginBase 封装所有 gRPC Client 共有的 Plugin 接口方法, // 通过嵌入到各具体 Client 中消除重复代码 // -// hostBrokerID 由 *GRPCPlugin.GRPCClient 钩子在启动 HostService stream 后填入, -// 在 Init() 时透传给插件进程的 InitRequest。0 表示 host 不可用。 +// coreInvokeBrokerID 由 *GRPCPlugin.GRPCClient 钩子在启动反向调用 stream 后填入, +// 在 Init() 时透传给插件进程的 InitRequest。0 表示反向调用不可用。 // // 日志策略:core 通过 hashicorp/go-plugin 内部建的 grpc.ClientConn 拿到 RPC 句柄, // 我们没法直接装 client 端拦截器(go-plugin 不暴露注入点)。所以在每个方法里手写 // 进入 / 失败 / 完成日志,配合 server 端拦截器一起把调用链覆盖到位。失败一定打 Error, // 成功路径只打 Debug 防止污染 info 流。 type pluginBase struct { - plugin pb.PluginServiceClient - cachedInfo *sdk.PluginInfo - hostBrokerID uint32 + plugin pb.PluginServiceClient + event pb.EventServiceClient + cachedInfo *sdk.PluginInfo + coreInvokeBrokerID uint32 } // pluginIDForLog 取 cached 的插件 ID 给日志用;若还没缓存则返回空串。 @@ -82,6 +83,7 @@ func (b *pluginBase) Info() sdk.PluginInfo { Author: resp.Author, Type: sdk.PluginType(resp.Type), Dependencies: resp.Dependencies, + Metadata: resp.Metadata, } if len(resp.ConfigSchema) > 0 { @@ -179,9 +181,9 @@ func (b *pluginBase) Init(ctx sdk.PluginContext) error { logger, start := b.rpcLogger(grpcCtx, "Init") logger.Debug("plugin_call_init_start") _, err := b.plugin.Init(grpcCtx, &pb.InitRequest{ - Config: config, - LogLevel: logLevel, - HostBrokerId: b.hostBrokerID, + Config: config, + LogLevel: logLevel, + CoreInvokeBrokerId: b.coreInvokeBrokerID, }) if err != nil { logger.Error("plugin_call_init_failed", @@ -256,6 +258,23 @@ func (b *pluginBase) GetWebAssets() (map[string][]byte, error) { return assets, nil } +// Schema 获取插件结构化能力清单。 +func (b *pluginBase) Schema() sdk.PluginSchema { + ctx, cancel := withTimeout() + defer cancel() + + logger, start := b.rpcLogger(ctx, "GetSchema") + resp, err := b.plugin.GetSchema(ctx, &pb.Empty{}) + if err != nil { + logger.Error("plugin_call_get_schema_failed", + sdk.LogFieldDurationMs, time.Since(start).Milliseconds(), + sdk.LogFieldError, err, + ) + return sdk.PluginSchema{} + } + return schemaFromProto(resp) +} + // HealthCheck 健康检查(客户端侧调用) func (b *pluginBase) HealthCheck(ctx context.Context) error { logger, start := b.rpcLogger(ctx, "HealthCheck") @@ -306,18 +325,12 @@ func convertModels(pbModels []*pb.ModelInfoProto) []sdk.ModelInfo { models := make([]sdk.ModelInfo, len(pbModels)) for i, m := range pbModels { models[i] = sdk.ModelInfo{ - ID: m.Id, - Name: m.Name, - ContextWindow: int(m.ContextWindow), - MaxOutputTokens: int(m.MaxOutputTokens), - InputPrice: m.InputPrice, - OutputPrice: m.OutputPrice, - CachedInputPrice: m.CachedInputPrice, - CacheCreationPrice: m.CacheCreationPrice, - CacheCreation1hPrice: m.GetCacheCreation_1HPrice(), - InputPricePriority: m.InputPricePriority, - OutputPricePriority: m.OutputPricePriority, - CachedInputPricePriority: m.CachedInputPricePriority, + ID: m.Id, + Name: m.Name, + ContextWindow: int(m.ContextWindow), + MaxOutputTokens: int(m.MaxOutputTokens), + Capabilities: m.Capabilities, + Metadata: m.Metadata, } } return models diff --git a/grpc/context.go b/runtimego/grpc/context.go similarity index 72% rename from grpc/context.go rename to runtimego/grpc/context.go index cfa7425..cadbd09 100644 --- a/grpc/context.go +++ b/runtimego/grpc/context.go @@ -9,32 +9,49 @@ import ( goplugin "github.com/hashicorp/go-plugin" - sdk "github.com/DouDOU-start/airgate-sdk" - pb "github.com/DouDOU-start/airgate-sdk/proto" + pb "github.com/DouDOU-start/airgate-sdk/protocol/proto" + sdk "github.com/DouDOU-start/airgate-sdk/sdkgo" ) // grpcPluginContext 通过 gRPC 传入的插件上下文(插件进程侧)。 // // 实现了 sdk.PluginContext,并可选实现 sdk.HostAware:当 Core 通过 InitRequest -// 注入了 host_broker_id 时,Host() 会通过 broker.Dial(id) 拿到一个反向 grpc -// 连接,把 HostService client 包装成 sdk.Host 返回给插件。 +// 注入了 core_invoke_broker_id 时,Host() 会通过 broker.Dial(id) 拿到一个反向 grpc +// 连接,把 CoreInvokeService client 包装成 sdk.Host 返回给插件。 // // Host() 是 lazy 的:第一次调用时才 dial。理由: // - 大多数插件根本不需要 host,避免无谓建立 stream // - 即便需要,也常常是在 Start() 而非 Init() 阶段才用到 -// - dial 失败可以回退到 nil(与"Core 不支持 host"语义一致),不阻塞 Init +// - dial 失败会记录错误并返回 nil,不阻塞 Init type grpcPluginContext struct { config sdk.PluginConfig logger *slog.Logger - broker *goplugin.GRPCBroker // 由 PluginGRPCServer 注入;plugin 进程侧才有 - hostBrokerID uint32 // 0 = host 不可用 + broker *goplugin.GRPCBroker // 由 PluginGRPCServer 注入;plugin 进程侧才有 + coreInvokeBrokerID uint32 // 0 = 反向调用不可用 hostOnce sync.Once host sdk.Host hostErr error } +func (c *grpcPluginContext) initHost() { + if c.broker == nil { + c.hostErr = errors.New("host broker not available") + return + } + if c.coreInvokeBrokerID == 0 { + c.hostErr = errors.New("core invoke not enabled") + return + } + conn, err := c.broker.Dial(c.coreInvokeBrokerID) + if err != nil { + c.hostErr = err + return + } + c.host = NewHostClient(pb.NewCoreInvokeServiceClient(conn)) +} + func (c *grpcPluginContext) Logger() *slog.Logger { if c.logger == nil { return slog.Default() @@ -61,26 +78,11 @@ func (c *grpcPluginContext) PluginDSN() string { // Host 实现 sdk.HostAware。返回 nil 表示不可用(非错误,调用方做 nil-check)。 // // 失败情形(都返回 nil + 内部记录 err): -// - host_broker_id == 0(Core 没启用 HostService) +// - core_invoke_broker_id == 0(Core 没启用反向调用) // - broker == nil(不在 plugin 进程内 / 测试 mock) // - broker.Dial 失败(超时 / Core 进程退出) func (c *grpcPluginContext) Host() sdk.Host { - c.hostOnce.Do(func() { - if c.broker == nil { - c.hostErr = errors.New("host broker not available") - return - } - if c.hostBrokerID == 0 { - c.hostErr = errors.New("host service not enabled by core") - return - } - conn, err := c.broker.Dial(c.hostBrokerID) - if err != nil { - c.hostErr = err - return - } - c.host = NewHostClient(pb.NewHostServiceClient(conn)) - }) + c.hostOnce.Do(c.initHost) return c.host } @@ -89,7 +91,7 @@ func (c *grpcPluginContext) Host() sdk.Host { // // if hc, ok := ctx.(interface{ HostError() error }); ok { ... } func (c *grpcPluginContext) HostError() error { - c.hostOnce.Do(func() {}) // 确保 Once 已经 fire 过 + c.hostOnce.Do(c.initHost) return c.hostErr } diff --git a/grpc/context_test.go b/runtimego/grpc/context_test.go similarity index 91% rename from grpc/context_test.go rename to runtimego/grpc/context_test.go index b347eda..731200d 100644 --- a/grpc/context_test.go +++ b/runtimego/grpc/context_test.go @@ -167,3 +167,14 @@ func TestGetAll(t *testing.T) { } } } + +func TestPluginContextHostErrorInitializesHostState(t *testing.T) { + ctx := &grpcPluginContext{coreInvokeBrokerID: 1} + + if err := ctx.HostError(); err == nil { + t.Fatal("HostError() 应返回 Host 初始化失败原因") + } + if host := ctx.Host(); host != nil { + t.Fatalf("Host() = %T,期望 nil", host) + } +} diff --git a/grpc/convert_test.go b/runtimego/grpc/convert_test.go similarity index 81% rename from grpc/convert_test.go rename to runtimego/grpc/convert_test.go index 79ce88f..65fdd04 100644 --- a/grpc/convert_test.go +++ b/runtimego/grpc/convert_test.go @@ -7,8 +7,8 @@ import ( "testing" "time" - sdk "github.com/DouDOU-start/airgate-sdk" - pb "github.com/DouDOU-start/airgate-sdk/proto" + pb "github.com/DouDOU-start/airgate-sdk/protocol/proto" + sdk "github.com/DouDOU-start/airgate-sdk/sdkgo" ) // --------------------------------------------------------------------------- @@ -91,19 +91,47 @@ func TestForwardOutcome_RoundTrip(t *testing.T) { Body: []byte(`{"ok":true}`), }, Usage: &sdk.Usage{ - InputTokens: 150, - OutputTokens: 300, - CachedInputTokens: 50, - CacheCreationTokens: 80, - ReasoningOutputTokens: 25, - Model: "claude-opus-4-20250514", - FirstTokenMs: 120, - ServiceTier: "priority", - InputCost: 0.00375, - OutputCost: 0.0225, - CachedInputCost: 0.000125, - CacheCreationCost: 0.0005, - CacheCreationPrice: 6.25, + Model: "claude-opus-4-20250514", + AccountCost: 0.026875, + UserCost: 0.05375, + BillingMultiplier: 2, + Currency: "USD", + Summary: "输入 150 token,输出 300 token", + FirstTokenMs: 120, + Attributes: []sdk.UsageAttribute{ + {Key: "reasoning_effort", Label: "思考层级", Kind: "reasoning", Value: "high"}, + {Key: "resolution", Label: "分辨率", Kind: "resolution", Value: "1024x1024"}, + }, + Metrics: []sdk.UsageMetric{ + { + Key: "image_generation", + Label: "图片生成", + Kind: "image", + Unit: "image", + Value: 2, + AccountCost: 0.08, + Currency: "USD", + Metadata: map[string]string{ + "size": "1024x1024", + }, + }, + }, + CostDetails: []sdk.UsageCostDetail{ + { + Key: "image_generation", + Label: "图片生成费用", + AccountCost: 0.08, + UserCost: 0.16, + BillingMultiplier: 2, + Currency: "USD", + Metadata: map[string]string{ + "tier": "standard", + }, + }, + }, + Metadata: map[string]string{ + "billing_mode": "mixed", + }, }, Duration: 2500 * time.Millisecond, RetryAfter: 30000 * time.Millisecond, @@ -164,6 +192,15 @@ func TestForwardOutcome_KindPreserved(t *testing.T) { t.Errorf("Kind %q not preserved: got %q", k, restored.Kind) } } + // AccountModelUnsupported 已归入 ClientError,round-trip 后变为 ClientError + { + original := sdk.ForwardOutcome{Kind: sdk.OutcomeAccountModelUnsupported} //nolint:staticcheck // 测试兼容映射 + proto := outcomeToProto(original) + restored := outcomeFromProto(proto) + if restored.Kind != sdk.OutcomeClientError { + t.Errorf("AccountModelUnsupported should map to ClientError, got %q", restored.Kind) + } + } } func TestForwardOutcome_ZeroValues(t *testing.T) { @@ -269,16 +306,14 @@ func TestBuildAccount_EmptyCredentials(t *testing.T) { func TestConvertModels(t *testing.T) { pbModels := []*pb.ModelInfoProto{ { - Id: "gpt-4", - Name: "GPT-4", - ContextWindow: 8192, - MaxOutputTokens: 4096, - InputPrice: 30.0, - OutputPrice: 60.0, - CachedInputPrice: 15.0, - InputPricePriority: 45.0, - OutputPricePriority: 90.0, - CachedInputPricePriority: 22.5, + Id: "gpt-4", + Name: "GPT-4", + ContextWindow: 8192, + MaxOutputTokens: 4096, + Capabilities: []string{sdk.ModelCapChat}, + Metadata: map[string]string{ + "family": "gpt", + }, }, } @@ -288,16 +323,14 @@ func TestConvertModels(t *testing.T) { t.Fatalf("expected 1 model, got %d", len(models)) } expected := sdk.ModelInfo{ - ID: "gpt-4", - Name: "GPT-4", - ContextWindow: 8192, - MaxOutputTokens: 4096, - InputPrice: 30.0, - OutputPrice: 60.0, - CachedInputPrice: 15.0, - InputPricePriority: 45.0, - OutputPricePriority: 90.0, - CachedInputPricePriority: 22.5, + ID: "gpt-4", + Name: "GPT-4", + ContextWindow: 8192, + MaxOutputTokens: 4096, + Capabilities: []string{sdk.ModelCapChat}, + Metadata: map[string]string{ + "family": "gpt", + }, } if !reflect.DeepEqual(models[0], expected) { t.Fatalf("convertModels mismatch:\n got: %+v\n want: %+v", models[0], expected) diff --git a/runtimego/grpc/doc.go b/runtimego/grpc/doc.go new file mode 100644 index 0000000..aaf80bd --- /dev/null +++ b/runtimego/grpc/doc.go @@ -0,0 +1,5 @@ +// Package grpc 提供 AirGate Go 插件运行时的 gRPC 适配层。 +// +// 本包负责 hashicorp/go-plugin 集成、protobuf 与 sdkgo 类型转换、 +// Core 反向调用通道、流式响应桥接和插件进程启动。 +package grpc diff --git a/runtimego/grpc/event_schema_test.go b/runtimego/grpc/event_schema_test.go new file mode 100644 index 0000000..ef55a64 --- /dev/null +++ b/runtimego/grpc/event_schema_test.go @@ -0,0 +1,97 @@ +package grpc + +import ( + "reflect" + "testing" + "time" + + sdk "github.com/DouDOU-start/airgate-sdk/sdkgo" +) + +func TestPluginEventRoundTrip(t *testing.T) { + original := sdk.PluginEvent{ + ID: "evt_1", + Type: "account.updated", + Source: "core", + Subject: "account:42", + UserID: 7, + GroupID: 9, + Payload: map[string]interface{}{ + "account_id": float64(42), + "status": "active", + }, + Metadata: map[string]string{"trace": "abc"}, + OccurredAt: time.UnixMilli(1700000000123), + } + + protoEvent, err := eventToProto(original) + if err != nil { + t.Fatalf("eventToProto() error = %v", err) + } + restored, err := eventFromProto(protoEvent) + if err != nil { + t.Fatalf("eventFromProto() error = %v", err) + } + if !reflect.DeepEqual(original, restored) { + t.Fatalf("PluginEvent round-trip mismatch:\n original: %+v\n restored: %+v", original, restored) + } +} + +func TestEventSubscriptionRoundTrip(t *testing.T) { + original := sdk.EventSubscription{ + Type: "task.*", + Source: "core", + Filter: map[string]string{ + "task_type": "image_generation", + }, + Metadata: map[string]string{"note": "demo"}, + } + + restored := subscriptionFromProto(subscriptionToProto(original)) + if !reflect.DeepEqual(original, restored) { + t.Fatalf("EventSubscription round-trip mismatch:\n original: %+v\n restored: %+v", original, restored) + } +} + +func TestPluginSchemaRoundTrip(t *testing.T) { + original := sdk.PluginSchema{ + Routes: []sdk.RouteSchema{{ + Method: "POST", + Path: "/api/demo", + Summary: "演示接口", + Request: sdk.PayloadSchema{ + ContentType: "application/json", + Schema: `{"type":"object"}`, + }, + Response: sdk.PayloadSchema{Example: `{"ok":true}`}, + Metadata: map[string]string{"group": "demo"}, + }}, + Tasks: []sdk.TaskSchema{{ + Type: "image_generation", + Summary: "生成图片", + Input: sdk.PayloadSchema{Schema: `{"type":"object"}`}, + Output: sdk.PayloadSchema{Schema: `{"type":"object"}`}, + }}, + Events: []sdk.EventSchema{{ + Type: "account.updated", + Source: "core", + Summary: "账号更新", + Payload: sdk.PayloadSchema{Schema: `{"type":"object"}`}, + }}, + Invokes: []sdk.InvokeSchema{{ + Method: "chat.stream", + Summary: "流式对话", + Transport: sdk.InvokeTransportBidirectionalStream, + Request: sdk.PayloadSchema{Schema: `{"type":"object"}`}, + Response: sdk.PayloadSchema{Schema: `{"type":"object"}`}, + ClientFrame: sdk.PayloadSchema{Schema: `{"type":"object","properties":{"delta":{"type":"string"}}}`}, + ServerFrame: sdk.PayloadSchema{Schema: `{"type":"object","properties":{"text":{"type":"string"}}}`}, + }}, + Metadata: map[string]string{"version": "1"}, + } + + restored := schemaFromProto(schemaToProto(original)) + if !reflect.DeepEqual(original, restored) { + t.Fatalf("PluginSchema round-trip mismatch:\n original: %+v\n restored: %+v", original, restored) + } +} diff --git a/runtimego/grpc/event_server.go b/runtimego/grpc/event_server.go new file mode 100644 index 0000000..45919b6 --- /dev/null +++ b/runtimego/grpc/event_server.go @@ -0,0 +1,152 @@ +package grpc + +import ( + "context" + "errors" + "time" + + pb "github.com/DouDOU-start/airgate-sdk/protocol/proto" + sdk "github.com/DouDOU-start/airgate-sdk/sdkgo" +) + +// EventGRPCServer 将可选 EventHandler 包装为 gRPC 服务。 +type EventGRPCServer struct { + pb.UnimplementedEventServiceServer + Impl sdk.Plugin +} + +func eventToProto(e sdk.PluginEvent) (*pb.PluginEvent, error) { + occurredAt := int64(0) + if !e.OccurredAt.IsZero() { + occurredAt = e.OccurredAt.UnixMilli() + } + payload, err := mapToJSONPayload(e.Payload) + if err != nil { + return nil, err + } + return &pb.PluginEvent{ + Id: e.ID, + Type: e.Type, + Source: e.Source, + Subject: e.Subject, + UserId: e.UserID, + GroupId: e.GroupID, + Payload: payload, + Metadata: e.Metadata, + OccurredAt: occurredAt, + }, nil +} + +func eventFromProto(p *pb.PluginEvent) (sdk.PluginEvent, error) { + if p == nil { + return sdk.PluginEvent{}, nil + } + payload, err := jsonPayloadToMap(p.Payload) + if err != nil { + return sdk.PluginEvent{}, err + } + event := sdk.PluginEvent{ + ID: p.Id, + Type: p.Type, + Source: p.Source, + Subject: p.Subject, + UserID: p.UserId, + GroupID: p.GroupId, + Payload: payload, + Metadata: p.Metadata, + } + if p.OccurredAt > 0 { + event.OccurredAt = time.UnixMilli(p.OccurredAt) + } + return event, nil +} + +func subscriptionToProto(s sdk.EventSubscription) *pb.EventSubscriptionProto { + return &pb.EventSubscriptionProto{ + Type: s.Type, + Source: s.Source, + Filter: s.Filter, + Metadata: s.Metadata, + } +} + +func subscriptionFromProto(p *pb.EventSubscriptionProto) sdk.EventSubscription { + if p == nil { + return sdk.EventSubscription{} + } + return sdk.EventSubscription{ + Type: p.Type, + Source: p.Source, + Filter: p.Filter, + Metadata: p.Metadata, + } +} + +func (s *EventGRPCServer) GetEventSubscriptions(_ context.Context, _ *pb.Empty) (*pb.EventSubscriptionsResponse, error) { + handler, ok := s.Impl.(sdk.EventHandler) + if !ok { + return &pb.EventSubscriptionsResponse{}, nil + } + subscriptions := handler.EventSubscriptions() + resp := &pb.EventSubscriptionsResponse{} + if len(subscriptions) > 0 { + resp.Subscriptions = make([]*pb.EventSubscriptionProto, 0, len(subscriptions)) + } + for _, sub := range subscriptions { + resp.Subscriptions = append(resp.Subscriptions, subscriptionToProto(sub)) + } + return resp, nil +} + +func (s *EventGRPCServer) HandleEvent(ctx context.Context, req *pb.PluginEvent) (*pb.EventHandleResponse, error) { + handler, ok := s.Impl.(sdk.EventHandler) + if !ok { + return &pb.EventHandleResponse{Success: false, ErrorMessage: "plugin does not implement EventHandler"}, nil + } + event, err := eventFromProto(req) + if err != nil { + return &pb.EventHandleResponse{Success: false, ErrorMessage: err.Error()}, nil + } + if err := handler.HandleEvent(ctx, event); err != nil { + return &pb.EventHandleResponse{Success: false, ErrorMessage: err.Error()}, nil + } + return &pb.EventHandleResponse{Success: true}, nil +} + +// EventSubscriptions 获取插件订阅的事件列表。 +func (b *pluginBase) EventSubscriptions() []sdk.EventSubscription { + if b.event == nil { + return nil + } + ctx, cancel := withTimeout() + defer cancel() + + resp, err := b.event.GetEventSubscriptions(ctx, &pb.Empty{}) + if err != nil { + return nil + } + out := make([]sdk.EventSubscription, 0, len(resp.Subscriptions)) + for _, sub := range resp.Subscriptions { + out = append(out, subscriptionFromProto(sub)) + } + return out +} + +// HandleEvent 将 Core 事件推送给插件。 +func (b *pluginBase) HandleEvent(ctx context.Context, event sdk.PluginEvent) error { + if b.event == nil { + return errors.New("event service is not initialized") + } + protoEvent, err := eventToProto(event) + if err != nil { + return err + } + resp, err := b.event.HandleEvent(ctx, protoEvent) + if err != nil { + return err + } + if resp != nil && !resp.Success { + return errors.New(resp.ErrorMessage) + } + return nil +} diff --git a/grpc/extension_client.go b/runtimego/grpc/extension_client.go similarity index 78% rename from grpc/extension_client.go rename to runtimego/grpc/extension_client.go index 27f0b25..2148d7b 100644 --- a/grpc/extension_client.go +++ b/runtimego/grpc/extension_client.go @@ -4,8 +4,8 @@ import ( "context" "time" - sdk "github.com/DouDOU-start/airgate-sdk" - pb "github.com/DouDOU-start/airgate-sdk/proto" + pb "github.com/DouDOU-start/airgate-sdk/protocol/proto" + sdk "github.com/DouDOU-start/airgate-sdk/sdkgo" ) // migrateTimeout 数据库迁移的超时时间(迁移可能涉及大量数据,需要较长时间) @@ -70,3 +70,19 @@ func (c *ExtensionGRPCClient) HandleHTTPRequest(ctx context.Context, req *pb.Htt func (c *ExtensionGRPCClient) HandleHTTPStreamRequest(ctx context.Context, req *pb.HttpRequest) (pb.ExtensionService_HandleStreamRequestClient, error) { return c.extension.HandleStreamRequest(ctx, req) } + +// ── 异步任务 ── + +// ProcessTask 由 Core 任务分发器调用,将 pending 任务分发给插件处理。 +func (c *ExtensionGRPCClient) ProcessTask(ctx context.Context, req *pb.ProcessTaskRequest) (*pb.ProcessTaskResponse, error) { + return c.extension.ProcessTask(ctx, req) +} + +// GetTaskTypes 返回此插件支持的任务类型列表。 +func (c *ExtensionGRPCClient) GetTaskTypes(ctx context.Context) ([]string, error) { + resp, err := c.extension.GetTaskTypes(ctx, &pb.Empty{}) + if err != nil { + return nil, err + } + return resp.Types, nil +} diff --git a/grpc/extension_router.go b/runtimego/grpc/extension_router.go similarity index 97% rename from grpc/extension_router.go rename to runtimego/grpc/extension_router.go index 135ea03..db80eb0 100644 --- a/grpc/extension_router.go +++ b/runtimego/grpc/extension_router.go @@ -5,7 +5,7 @@ import ( "strings" "sync" - sdk "github.com/DouDOU-start/airgate-sdk" + sdk "github.com/DouDOU-start/airgate-sdk/sdkgo" ) // extensionRouter 实现 sdk.RouteRegistrar,用于在 gRPC 模式下将 HTTP 请求分发到注册的处理函数 diff --git a/grpc/extension_server.go b/runtimego/grpc/extension_server.go similarity index 86% rename from grpc/extension_server.go rename to runtimego/grpc/extension_server.go index 20be0b2..efd2766 100644 --- a/grpc/extension_server.go +++ b/runtimego/grpc/extension_server.go @@ -3,14 +3,15 @@ package grpc import ( "bytes" "context" + "encoding/json" "fmt" "net/http" "net/http/httptest" "strings" "sync" - sdk "github.com/DouDOU-start/airgate-sdk" - pb "github.com/DouDOU-start/airgate-sdk/proto" + pb "github.com/DouDOU-start/airgate-sdk/protocol/proto" + sdk "github.com/DouDOU-start/airgate-sdk/sdkgo" ) // streamResponseWriter 把每次 Write 调用转为 gRPC HttpResponseChunk 发送。 @@ -257,3 +258,37 @@ func httpResponseToPB(rec *httptest.ResponseRecorder) *pb.HttpResponse { Body: rec.Body.Bytes(), } } + +// ── 异步任务 ── + +func (s *ExtensionGRPCServer) ProcessTask(ctx context.Context, req *pb.ProcessTaskRequest) (*pb.ProcessTaskResponse, error) { + tp, ok := s.Impl.(sdk.TaskProcessor) + if !ok { + return &pb.ProcessTaskResponse{Success: false, ErrorMessage: "plugin does not implement TaskProcessor"}, nil + } + + var input map[string]interface{} + if len(req.Input) > 0 { + _ = json.Unmarshal(req.Input, &input) + } + + task := sdk.HostTask{ + ID: req.TaskId, + TaskType: req.TaskType, + UserID: req.UserId, + Input: input, + } + + if err := tp.ProcessTask(ctx, task); err != nil { + return &pb.ProcessTaskResponse{Success: false, ErrorMessage: err.Error()}, nil + } + return &pb.ProcessTaskResponse{Success: true}, nil +} + +func (s *ExtensionGRPCServer) GetTaskTypes(ctx context.Context, _ *pb.Empty) (*pb.TaskTypesResponse, error) { + tp, ok := s.Impl.(sdk.TaskProcessor) + if !ok { + return &pb.TaskTypesResponse{}, nil + } + return &pb.TaskTypesResponse{Types: tp.TaskTypes()}, nil +} diff --git a/grpc/gateway_client.go b/runtimego/grpc/gateway_client.go similarity index 93% rename from grpc/gateway_client.go rename to runtimego/grpc/gateway_client.go index e112ce4..133275c 100644 --- a/grpc/gateway_client.go +++ b/runtimego/grpc/gateway_client.go @@ -8,8 +8,8 @@ import ( "net/http" "sync" - sdk "github.com/DouDOU-start/airgate-sdk" - pb "github.com/DouDOU-start/airgate-sdk/proto" + pb "github.com/DouDOU-start/airgate-sdk/protocol/proto" + sdk "github.com/DouDOU-start/airgate-sdk/sdkgo" ) // GatewayGRPCClient 把 gRPC 客户端包装成 GatewayPlugin 接口,供 Core 消费。 @@ -95,6 +95,7 @@ func (c *GatewayGRPCClient) Routes() []sdk.RouteDefinition { Method: r.Method, Path: r.Path, Description: r.Description, + Metadata: r.Metadata, } } c.mu.Lock() @@ -193,21 +194,6 @@ func (c *GatewayGRPCClient) ValidateAccount(ctx context.Context, credentials map return err } -func (c *GatewayGRPCClient) QueryQuota(ctx context.Context, credentials map[string]string) (*sdk.QuotaInfo, error) { - resp, err := c.gateway.QueryQuota(ctx, &pb.CredentialsRequest{Credentials: credentials}) - if err != nil { - return nil, err - } - return &sdk.QuotaInfo{ - Total: resp.Total, - Used: resp.Used, - Remaining: resp.Remaining, - Currency: resp.Currency, - ExpiresAt: resp.ExpiresAt, - Extra: resp.Extra, - }, nil -} - // HandleWebSocket 通过 gRPC 双向流处理 WebSocket(Core 侧调用)。 func (c *GatewayGRPCClient) HandleWebSocket(ctx context.Context, conn sdk.WebSocketConn) (sdk.ForwardOutcome, error) { stream, err := c.gateway.HandleWebSocket(ctx) diff --git a/grpc/gateway_server.go b/runtimego/grpc/gateway_server.go similarity index 64% rename from grpc/gateway_server.go rename to runtimego/grpc/gateway_server.go index 7b1ba4a..d702ca6 100644 --- a/grpc/gateway_server.go +++ b/runtimego/grpc/gateway_server.go @@ -6,8 +6,8 @@ import ( "net/http" "time" - sdk "github.com/DouDOU-start/airgate-sdk" - pb "github.com/DouDOU-start/airgate-sdk/proto" + pb "github.com/DouDOU-start/airgate-sdk/protocol/proto" + sdk "github.com/DouDOU-start/airgate-sdk/sdkgo" ) // GatewayGRPCServer 将 GatewayPlugin 包装为 gRPC 服务端。 @@ -28,18 +28,12 @@ func (s *GatewayGRPCServer) GetModels(_ context.Context, _ *pb.Empty) (*pb.Model } for _, m := range models { resp.Models = append(resp.Models, &pb.ModelInfoProto{ - Id: m.ID, - Name: m.Name, - ContextWindow: int64(m.ContextWindow), - MaxOutputTokens: int64(m.MaxOutputTokens), - InputPrice: m.InputPrice, - OutputPrice: m.OutputPrice, - CachedInputPrice: m.CachedInputPrice, - CacheCreationPrice: m.CacheCreationPrice, - CacheCreation_1HPrice: m.CacheCreation1hPrice, - InputPricePriority: m.InputPricePriority, - OutputPricePriority: m.OutputPricePriority, - CachedInputPricePriority: m.CachedInputPricePriority, + Id: m.ID, + Name: m.Name, + ContextWindow: int64(m.ContextWindow), + MaxOutputTokens: int64(m.MaxOutputTokens), + Capabilities: m.Capabilities, + Metadata: m.Metadata, }) } return resp, nil @@ -56,6 +50,7 @@ func (s *GatewayGRPCServer) GetRoutes(_ context.Context, _ *pb.Empty) (*pb.Route Method: r.Method, Path: r.Path, Description: r.Description, + Metadata: r.Metadata, }) } return resp, nil @@ -132,6 +127,8 @@ func outcomeKindToProto(k sdk.OutcomeKind) pb.OutcomeKind { return pb.OutcomeKind_OUTCOME_UPSTREAM_TRANSIENT case sdk.OutcomeStreamAborted: return pb.OutcomeKind_OUTCOME_STREAM_ABORTED + case sdk.OutcomeAccountModelUnsupported: //nolint:staticcheck // 兼容旧插件 + return pb.OutcomeKind_OUTCOME_CLIENT_ERROR default: return pb.OutcomeKind_OUTCOME_UNKNOWN } @@ -151,6 +148,8 @@ func outcomeKindFromProto(k pb.OutcomeKind) sdk.OutcomeKind { return sdk.OutcomeUpstreamTransient case pb.OutcomeKind_OUTCOME_STREAM_ABORTED: return sdk.OutcomeStreamAborted + case pb.OutcomeKind_OUTCOME_ACCOUNT_MODEL_UNSUPPORTED: + return sdk.OutcomeClientError default: return sdk.OutcomeUnknown } @@ -176,52 +175,181 @@ func upstreamFromProto(p *pb.UpstreamResponse) sdk.UpstreamResponse { } func usageToProto(u sdk.Usage) *pb.Usage { - return &pb.Usage{ - InputTokens: int64(u.InputTokens), - OutputTokens: int64(u.OutputTokens), - CachedInputTokens: int64(u.CachedInputTokens), - CacheCreationTokens: int64(u.CacheCreationTokens), - CacheCreation_5MTokens: int64(u.CacheCreation5mTokens), - CacheCreation_1HTokens: int64(u.CacheCreation1hTokens), - ReasoningOutputTokens: int64(u.ReasoningOutputTokens), - InputCost: u.InputCost, - OutputCost: u.OutputCost, - CachedInputCost: u.CachedInputCost, - CacheCreationCost: u.CacheCreationCost, - InputPrice: u.InputPrice, - OutputPrice: u.OutputPrice, - CachedInputPrice: u.CachedInputPrice, - CacheCreationPrice: u.CacheCreationPrice, - CacheCreation_1HPrice: u.CacheCreation1hPrice, - Model: u.Model, - ServiceTier: u.ServiceTier, - FirstTokenMs: u.FirstTokenMs, - ImageSize: u.ImageSize, - } + out := &pb.Usage{ + Model: u.Model, + AccountCost: u.AccountCost, + UserCost: u.UserCost, + BillingMultiplier: u.BillingMultiplier, + Currency: u.Currency, + Summary: u.Summary, + FirstTokenMs: u.FirstTokenMs, + Metadata: u.Metadata, + } + out.Attributes = usageAttributesToProto(u.Attributes) + out.Metrics = usageMetricsToProto(u.Metrics) + out.CostDetails = usageCostDetailsToProto(u.CostDetails) + return out } func usageFromProto(p *pb.Usage) sdk.Usage { - return sdk.Usage{ - InputTokens: int(p.InputTokens), - OutputTokens: int(p.OutputTokens), - CachedInputTokens: int(p.CachedInputTokens), - CacheCreationTokens: int(p.CacheCreationTokens), - CacheCreation5mTokens: int(p.CacheCreation_5MTokens), - CacheCreation1hTokens: int(p.CacheCreation_1HTokens), - ReasoningOutputTokens: int(p.ReasoningOutputTokens), - InputCost: p.InputCost, - OutputCost: p.OutputCost, - CachedInputCost: p.CachedInputCost, - CacheCreationCost: p.CacheCreationCost, - InputPrice: p.InputPrice, - OutputPrice: p.OutputPrice, - CachedInputPrice: p.CachedInputPrice, - CacheCreationPrice: p.CacheCreationPrice, - CacheCreation1hPrice: p.CacheCreation_1HPrice, - Model: p.Model, - ServiceTier: p.ServiceTier, - FirstTokenMs: p.FirstTokenMs, - ImageSize: p.ImageSize, + out := sdk.Usage{ + Model: p.Model, + AccountCost: p.AccountCost, + UserCost: p.UserCost, + BillingMultiplier: p.BillingMultiplier, + Currency: p.Currency, + Summary: p.Summary, + FirstTokenMs: p.FirstTokenMs, + Metadata: p.Metadata, + } + out.Attributes = usageAttributesFromProto(p.Attributes) + out.Metrics = usageMetricsFromProto(p.Metrics) + out.CostDetails = usageCostDetailsFromProto(p.CostDetails) + return out +} + +func usageAttributesToProto(attrs []sdk.UsageAttribute) []*pb.UsageAttribute { + if len(attrs) == 0 { + return nil + } + out := make([]*pb.UsageAttribute, 0, len(attrs)) + for _, a := range attrs { + out = append(out, usageAttributeToProto(a)) + } + return out +} + +func usageAttributesFromProto(attrs []*pb.UsageAttribute) []sdk.UsageAttribute { + if len(attrs) == 0 { + return nil + } + out := make([]sdk.UsageAttribute, 0, len(attrs)) + for _, a := range attrs { + out = append(out, usageAttributeFromProto(a)) + } + return out +} + +func usageAttributeToProto(a sdk.UsageAttribute) *pb.UsageAttribute { + return &pb.UsageAttribute{ + Key: a.Key, + Label: a.Label, + Kind: a.Kind, + Value: a.Value, + Metadata: a.Metadata, + } +} + +func usageAttributeFromProto(p *pb.UsageAttribute) sdk.UsageAttribute { + if p == nil { + return sdk.UsageAttribute{} + } + return sdk.UsageAttribute{ + Key: p.Key, + Label: p.Label, + Kind: p.Kind, + Value: p.Value, + Metadata: p.Metadata, + } +} + +func usageMetricsToProto(metrics []sdk.UsageMetric) []*pb.UsageMetric { + if len(metrics) == 0 { + return nil + } + out := make([]*pb.UsageMetric, 0, len(metrics)) + for _, m := range metrics { + out = append(out, usageMetricToProto(m)) + } + return out +} + +func usageMetricsFromProto(metrics []*pb.UsageMetric) []sdk.UsageMetric { + if len(metrics) == 0 { + return nil + } + out := make([]sdk.UsageMetric, 0, len(metrics)) + for _, m := range metrics { + out = append(out, usageMetricFromProto(m)) + } + return out +} + +func usageMetricToProto(m sdk.UsageMetric) *pb.UsageMetric { + return &pb.UsageMetric{ + Key: m.Key, + Label: m.Label, + Kind: m.Kind, + Unit: m.Unit, + Value: m.Value, + AccountCost: m.AccountCost, + Currency: m.Currency, + Metadata: m.Metadata, + } +} + +func usageMetricFromProto(p *pb.UsageMetric) sdk.UsageMetric { + if p == nil { + return sdk.UsageMetric{} + } + return sdk.UsageMetric{ + Key: p.Key, + Label: p.Label, + Kind: p.Kind, + Unit: p.Unit, + Value: p.Value, + AccountCost: p.AccountCost, + Currency: p.Currency, + Metadata: p.Metadata, + } +} + +func usageCostDetailsToProto(details []sdk.UsageCostDetail) []*pb.UsageCostDetail { + if len(details) == 0 { + return nil + } + out := make([]*pb.UsageCostDetail, 0, len(details)) + for _, c := range details { + out = append(out, usageCostDetailToProto(c)) + } + return out +} + +func usageCostDetailsFromProto(details []*pb.UsageCostDetail) []sdk.UsageCostDetail { + if len(details) == 0 { + return nil + } + out := make([]sdk.UsageCostDetail, 0, len(details)) + for _, c := range details { + out = append(out, usageCostDetailFromProto(c)) + } + return out +} + +func usageCostDetailToProto(c sdk.UsageCostDetail) *pb.UsageCostDetail { + return &pb.UsageCostDetail{ + Key: c.Key, + Label: c.Label, + AccountCost: c.AccountCost, + UserCost: c.UserCost, + BillingMultiplier: c.BillingMultiplier, + Currency: c.Currency, + Metadata: c.Metadata, + } +} + +func usageCostDetailFromProto(p *pb.UsageCostDetail) sdk.UsageCostDetail { + if p == nil { + return sdk.UsageCostDetail{} + } + return sdk.UsageCostDetail{ + Key: p.Key, + Label: p.Label, + AccountCost: p.AccountCost, + UserCost: p.UserCost, + BillingMultiplier: p.BillingMultiplier, + Currency: p.Currency, + Metadata: p.Metadata, } } @@ -317,12 +445,14 @@ func (s *GatewayGRPCServer) ForwardStream(req *pb.ForwardRequest, stream pb.Gate outcome.Duration = time.Since(startTime) } - if err := sw.flushMeta(); err != nil { - sdk.LoggerFromContext(ctx).Error("gateway_forward_stream_meta_flush_failed", - sdk.LogFieldModel, req.Model, - sdk.LogFieldError, err, - ) - return err + if sw.sent || sw.wroteHeader { + if err := sw.flushMeta(); err != nil { + sdk.LoggerFromContext(ctx).Error("gateway_forward_stream_meta_flush_failed", + sdk.LogFieldModel, req.Model, + sdk.LogFieldError, err, + ) + return err + } } if err := stream.Send(&pb.ForwardChunk{ Done: true, @@ -344,27 +474,13 @@ func (s *GatewayGRPCServer) ValidateAccount(ctx context.Context, req *pb.Credent return &pb.Empty{}, nil } -func (s *GatewayGRPCServer) QueryQuota(ctx context.Context, req *pb.CredentialsRequest) (*pb.QuotaInfoResponse, error) { - info, err := s.Impl.QueryQuota(ctx, req.Credentials) - if err != nil { - return nil, err - } - return &pb.QuotaInfoResponse{ - Total: info.Total, - Used: info.Used, - Remaining: info.Remaining, - Currency: info.Currency, - ExpiresAt: info.ExpiresAt, - Extra: info.Extra, - }, nil -} - // streamWriter 把 gRPC 流包装成 http.ResponseWriter。 type streamWriter struct { - stream pb.GatewayService_ForwardStreamServer - headers http.Header - code int - sent bool + stream pb.GatewayService_ForwardStreamServer + headers http.Header + code int + wroteHeader bool + sent bool } func (w *streamWriter) Header() http.Header { @@ -401,7 +517,13 @@ func (w *streamWriter) Write(data []byte) (int, error) { return total, nil } -func (w *streamWriter) WriteHeader(statusCode int) { w.code = statusCode } +func (w *streamWriter) WriteHeader(statusCode int) { + if w.sent || w.wroteHeader { + return + } + w.code = statusCode + w.wroteHeader = true +} func (w *streamWriter) flushMeta() error { if w.sent { diff --git a/grpc/gateway_stream_test.go b/runtimego/grpc/gateway_stream_test.go similarity index 76% rename from grpc/gateway_stream_test.go rename to runtimego/grpc/gateway_stream_test.go index 6fc1d9e..e3b3482 100644 --- a/grpc/gateway_stream_test.go +++ b/runtimego/grpc/gateway_stream_test.go @@ -10,8 +10,8 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/metadata" - sdk "github.com/DouDOU-start/airgate-sdk" - pb "github.com/DouDOU-start/airgate-sdk/proto" + pb "github.com/DouDOU-start/airgate-sdk/protocol/proto" + sdk "github.com/DouDOU-start/airgate-sdk/sdkgo" ) type stubForwardStreamServer struct { @@ -89,13 +89,29 @@ func (c *stubGatewayServiceClient) ForwardStream(context.Context, *pb.ForwardReq func (c *stubGatewayServiceClient) ValidateAccount(context.Context, *pb.CredentialsRequest, ...grpc.CallOption) (*pb.Empty, error) { return nil, nil } -func (c *stubGatewayServiceClient) QueryQuota(context.Context, *pb.CredentialsRequest, ...grpc.CallOption) (*pb.QuotaInfoResponse, error) { - return nil, nil -} func (c *stubGatewayServiceClient) HandleWebSocket(context.Context, ...grpc.CallOption) (grpc.BidiStreamingClient[pb.WebSocketFrame, pb.WebSocketFrame], error) { return nil, nil } +type stubGatewayPlugin struct { + forward func(context.Context, *sdk.ForwardRequest) (sdk.ForwardOutcome, error) +} + +func (p stubGatewayPlugin) Info() sdk.PluginInfo { return sdk.PluginInfo{} } +func (p stubGatewayPlugin) Init(sdk.PluginContext) error { return nil } +func (p stubGatewayPlugin) Start(context.Context) error { return nil } +func (p stubGatewayPlugin) Stop(context.Context) error { return nil } +func (p stubGatewayPlugin) Platform() string { return "test" } +func (p stubGatewayPlugin) Models() []sdk.ModelInfo { return nil } +func (p stubGatewayPlugin) Routes() []sdk.RouteDefinition { return nil } +func (p stubGatewayPlugin) ValidateAccount(context.Context, map[string]string) error { return nil } +func (p stubGatewayPlugin) HandleWebSocket(context.Context, sdk.WebSocketConn) (sdk.ForwardOutcome, error) { + return sdk.ForwardOutcome{}, sdk.ErrNotSupported +} +func (p stubGatewayPlugin) Forward(ctx context.Context, req *sdk.ForwardRequest) (sdk.ForwardOutcome, error) { + return p.forward(ctx, req) +} + type captureWriter struct { header http.Header status int @@ -151,6 +167,36 @@ func TestStreamWriterFlushMetaBeforeBody(t *testing.T) { } } +func TestGatewayGRPCServerForwardStreamDoesNotFlushMetaWithoutCommittedResponse(t *testing.T) { + server := &GatewayGRPCServer{ + Impl: stubGatewayPlugin{ + forward: func(_ context.Context, req *sdk.ForwardRequest) (sdk.ForwardOutcome, error) { + req.Writer.Header().Set("Content-Type", "text/event-stream") + return sdk.ForwardOutcome{ + Kind: sdk.OutcomeUpstreamTransient, + Upstream: sdk.UpstreamResponse{StatusCode: http.StatusBadGateway}, + Reason: "空流", + }, nil + }, + }, + } + stream := &stubForwardStreamServer{} + + if err := server.ForwardStream(&pb.ForwardRequest{}, stream); err != nil { + t.Fatalf("ForwardStream() error = %v", err) + } + if len(stream.chunks) != 1 { + t.Fatalf("expected only final outcome chunk, got %d chunks: %+v", len(stream.chunks), stream.chunks) + } + final := stream.chunks[0] + if !final.Done || final.FinalOutcome == nil { + t.Fatalf("expected final outcome chunk, got %+v", final) + } + if final.StatusCode != 0 || len(final.Headers) != 0 || len(final.Data) != 0 { + t.Fatalf("final chunk should not commit HTTP response, got %+v", final) + } +} + func TestGatewayGRPCClientForwardStreamAppliesStatusAndHeaders(t *testing.T) { client := &GatewayGRPCClient{ gateway: &stubGatewayServiceClient{ diff --git a/grpc/go_plugin.go b/runtimego/grpc/go_plugin.go similarity index 59% rename from grpc/go_plugin.go rename to runtimego/grpc/go_plugin.go index 8aca307..f386c60 100644 --- a/grpc/go_plugin.go +++ b/runtimego/grpc/go_plugin.go @@ -8,8 +8,8 @@ import ( goplugin "github.com/hashicorp/go-plugin" "google.golang.org/grpc" - sdk "github.com/DouDOU-start/airgate-sdk" - pb "github.com/DouDOU-start/airgate-sdk/proto" + pb "github.com/DouDOU-start/airgate-sdk/protocol/proto" + sdk "github.com/DouDOU-start/airgate-sdk/sdkgo" ) // 确保所有 Plugin 类型都实现了 goplugin.GRPCPlugin 接口 @@ -21,29 +21,35 @@ var ( // GatewayGRPCPlugin 实现 hashicorp/go-plugin.GRPCPlugin 接口 // -// HostImpl 字段由 Core 在构造 ClientConfig 时注入。当 HostImpl 非 nil 时, -// GRPCClient 钩子会通过 GRPCBroker 启一条新的 stream,注册 HostService server, -// 把 stream id 通过 pluginBase.hostBrokerID 透传给后续 Init 调用。 +// CoreInvokeImpl 字段由 Core 在构造 ClientConfig 时注入。当 CoreInvokeImpl 非 nil 时, +// GRPCClient 钩子会通过 GRPCBroker 启一条新的 stream,注册 CoreInvokeService server, +// 把 stream id 通过 pluginBase.coreInvokeBrokerID 透传给后续 Init 调用。 // -// 插件进程构造 GRPCServer 时不会用到 HostImpl(HostImpl 只在 host 侧有值), -// 所以插件二进制 main.go 里 Serve(impl) 时不需要也不能填 HostImpl。 +// 插件进程构造 GRPCServer 时不会用到 CoreInvokeImpl(CoreInvokeImpl 只在 Core 侧有值), +// 所以插件二进制 main.go 里 Serve(impl) 时不需要也不能填 CoreInvokeImpl。 type GatewayGRPCPlugin struct { goplugin.Plugin - Impl sdk.GatewayPlugin - HostImpl pb.HostServiceServer // host 侧注入;plugin 侧为 nil + Impl sdk.GatewayPlugin + CoreInvokeImpl pb.CoreInvokeServiceServer // Core 侧注入;plugin 侧为 nil } func (p *GatewayGRPCPlugin) GRPCServer(broker *goplugin.GRPCBroker, s *grpc.Server) error { pb.RegisterPluginServiceServer(s, &PluginGRPCServer{Impl: p.Impl, Broker: broker}) pb.RegisterGatewayServiceServer(s, &GatewayGRPCServer{Impl: p.Impl}) + pb.RegisterEventServiceServer(s, &EventGRPCServer{Impl: p.Impl}) + if tp, ok := p.Impl.(sdk.TaskProcessor); ok { + extServer := &ExtensionGRPCServer{Impl: &gatewayTaskAdapter{GatewayPlugin: p.Impl, tp: tp}} + extServer.initRouter() + pb.RegisterExtensionServiceServer(s, extServer) + } return nil } func (p *GatewayGRPCPlugin) GRPCClient(_ context.Context, broker *goplugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) { - hostBrokerID := startHostStream(broker, p.HostImpl) + coreInvokeBrokerID := startCoreInvokeStream(broker, p.CoreInvokeImpl) pluginClient := pb.NewPluginServiceClient(c) return &GatewayGRPCClient{ - pluginBase: pluginBase{plugin: pluginClient, hostBrokerID: hostBrokerID}, + pluginBase: pluginBase{plugin: pluginClient, event: pb.NewEventServiceClient(c), coreInvokeBrokerID: coreInvokeBrokerID}, gateway: pb.NewGatewayServiceClient(c), }, nil } @@ -51,8 +57,8 @@ func (p *GatewayGRPCPlugin) GRPCClient(_ context.Context, broker *goplugin.GRPCB // ExtensionGRPCPlugin 实现扩展插件的 go-plugin 接口 type ExtensionGRPCPlugin struct { goplugin.Plugin - Impl sdk.ExtensionPlugin - HostImpl pb.HostServiceServer + Impl sdk.ExtensionPlugin + CoreInvokeImpl pb.CoreInvokeServiceServer } func (p *ExtensionGRPCPlugin) GRPCServer(broker *goplugin.GRPCBroker, s *grpc.Server) error { @@ -60,54 +66,55 @@ func (p *ExtensionGRPCPlugin) GRPCServer(broker *goplugin.GRPCBroker, s *grpc.Se extServer := &ExtensionGRPCServer{Impl: p.Impl} extServer.initRouter() pb.RegisterExtensionServiceServer(s, extServer) + pb.RegisterEventServiceServer(s, &EventGRPCServer{Impl: p.Impl}) return nil } func (p *ExtensionGRPCPlugin) GRPCClient(_ context.Context, broker *goplugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) { - hostBrokerID := startHostStream(broker, p.HostImpl) + coreInvokeBrokerID := startCoreInvokeStream(broker, p.CoreInvokeImpl) pluginClient := pb.NewPluginServiceClient(c) return &ExtensionGRPCClient{ - pluginBase: pluginBase{plugin: pluginClient, hostBrokerID: hostBrokerID}, + pluginBase: pluginBase{plugin: pluginClient, event: pb.NewEventServiceClient(c), coreInvokeBrokerID: coreInvokeBrokerID}, extension: pb.NewExtensionServiceClient(c), }, nil } -// MiddlewareGRPCPlugin 实现中间件插件的 go-plugin 接口(ADR-0001 Decision 2)。 +// MiddlewareGRPCPlugin 实现中间件插件的 go-plugin 接口。 // -// HostImpl 用法与 GatewayGRPCPlugin 相同:core 侧注入 HostService 实现, +// CoreInvokeImpl 用法与 GatewayGRPCPlugin 相同:Core 侧注入反向调用实现, // 在 GRPCClient 钩子里通过 GRPCBroker 启反向 stream。 type MiddlewareGRPCPlugin struct { goplugin.Plugin - Impl sdk.MiddlewarePlugin - HostImpl pb.HostServiceServer + Impl sdk.MiddlewarePlugin + CoreInvokeImpl pb.CoreInvokeServiceServer } func (p *MiddlewareGRPCPlugin) GRPCServer(broker *goplugin.GRPCBroker, s *grpc.Server) error { pb.RegisterPluginServiceServer(s, &PluginGRPCServer{Impl: p.Impl, Broker: broker}) pb.RegisterMiddlewareServiceServer(s, &MiddlewareGRPCServer{Impl: p.Impl}) + pb.RegisterEventServiceServer(s, &EventGRPCServer{Impl: p.Impl}) return nil } func (p *MiddlewareGRPCPlugin) GRPCClient(_ context.Context, broker *goplugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) { - hostBrokerID := startHostStream(broker, p.HostImpl) + coreInvokeBrokerID := startCoreInvokeStream(broker, p.CoreInvokeImpl) pluginClient := pb.NewPluginServiceClient(c) return &MiddlewareGRPCClient{ - pluginBase: pluginBase{plugin: pluginClient, hostBrokerID: hostBrokerID}, + pluginBase: pluginBase{plugin: pluginClient, event: pb.NewEventServiceClient(c), coreInvokeBrokerID: coreInvokeBrokerID}, mw: pb.NewMiddlewareServiceClient(c), }, nil } -// startHostStream 在 host 进程侧通过 GRPCBroker 启动一条新的 stream, -// 注册 HostService server,返回 stream id(作为 host_broker_id 透传给插件)。 +// startCoreInvokeStream 在 Core 进程侧通过 GRPCBroker 启动一条新的 stream, +// 注册 CoreInvokeService server,返回 stream id(作为 core_invoke_broker_id 透传给插件)。 // -// hostImpl 为 nil 时表示 Core 没启用 HostService,返回 0;插件 Init 收到 0 -// 后会在 ctx.Host() 时返回 nil。这是软失败:旧版 Core / 不需要 host 的部署 -// 都正常工作。 +// coreInvokeImpl 为 nil 时表示 Core 没启用反向调用,返回 0;插件 Init 收到 0 +// 后会在 ctx.Host() 时返回 nil。 // // 这里的 grpc.NewServer 会装上 LoggingUnaryServerInterceptor / LoggingStreamServerInterceptor, // 插件→core 的 host 调用因此可观测。 -func startHostStream(broker *goplugin.GRPCBroker, hostImpl pb.HostServiceServer) uint32 { - if hostImpl == nil || broker == nil { +func startCoreInvokeStream(broker *goplugin.GRPCBroker, coreInvokeImpl pb.CoreInvokeServiceServer) uint32 { + if coreInvokeImpl == nil || broker == nil { return 0 } id := broker.NextId() @@ -119,10 +126,10 @@ func startHostStream(broker *goplugin.GRPCBroker, hostImpl pb.HostServiceServer) grpc.ChainStreamInterceptor(LoggingStreamServerInterceptor()), ) s := grpc.NewServer(opts...) - pb.RegisterHostServiceServer(s, hostImpl) + pb.RegisterCoreInvokeServiceServer(s, coreInvokeImpl) return s }) - slog.Debug("HostService stream 已就绪", "broker_id", id) + slog.Debug("CoreInvoke stream 已就绪", "broker_id", id) return id } @@ -164,6 +171,26 @@ func Serve(impl interface{}) { }) } +// gatewayTaskAdapter 把同时实现了 TaskProcessor 的 GatewayPlugin 包装为 +// ExtensionPlugin,让 ExtensionGRPCServer 能通过类型断言调用 ProcessTask / GetTaskTypes。 +// RegisterRoutes / Migrate / BackgroundTasks 保持空操作——网关插件不需要扩展插件的这些能力。 +type gatewayTaskAdapter struct { + sdk.GatewayPlugin + tp sdk.TaskProcessor +} + +func (a *gatewayTaskAdapter) RegisterRoutes(_ sdk.RouteRegistrar) {} +func (a *gatewayTaskAdapter) Migrate() error { return nil } +func (a *gatewayTaskAdapter) BackgroundTasks() []sdk.BackgroundTask { + return nil +} +func (a *gatewayTaskAdapter) ProcessTask(ctx context.Context, task sdk.HostTask) error { + return a.tp.ProcessTask(ctx, task) +} +func (a *gatewayTaskAdapter) TaskTypes() []string { + return a.tp.TaskTypes() +} + // PluginGRPCMaxMessageBytes 是插件 gRPC 服务端单条消息最大字节数(收/发同值)。 // 默认 4 MB 经常被大段 LLM 响应或翻译后的 SSE 事件击穿,统一抬到 64 MB; // 必须与 core 侧 ClientConfig.GRPCDialOptions 中的上限保持一致。 diff --git a/runtimego/grpc/go_plugin_test.go b/runtimego/grpc/go_plugin_test.go new file mode 100644 index 0000000..f97b849 --- /dev/null +++ b/runtimego/grpc/go_plugin_test.go @@ -0,0 +1,97 @@ +package grpc + +import ( + "context" + "testing" + + gogrpc "google.golang.org/grpc" + + pb "github.com/DouDOU-start/airgate-sdk/protocol/proto" + sdk "github.com/DouDOU-start/airgate-sdk/sdkgo" +) + +type taskGatewayPlugin struct{} + +func (taskGatewayPlugin) Info() sdk.PluginInfo { + return sdk.PluginInfo{ID: "task-gateway", Type: sdk.PluginTypeGateway} +} + +func (taskGatewayPlugin) Init(sdk.PluginContext) error { return nil } +func (taskGatewayPlugin) Start(context.Context) error { return nil } +func (taskGatewayPlugin) Stop(context.Context) error { return nil } + +func (taskGatewayPlugin) Platform() string { return "demo" } +func (taskGatewayPlugin) Models() []sdk.ModelInfo { return nil } +func (taskGatewayPlugin) Routes() []sdk.RouteDefinition { return nil } + +func (taskGatewayPlugin) Forward(context.Context, *sdk.ForwardRequest) (sdk.ForwardOutcome, error) { + return sdk.ForwardOutcome{}, nil +} + +func (taskGatewayPlugin) ValidateAccount(context.Context, map[string]string) error { return nil } + +func (taskGatewayPlugin) HandleWebSocket(context.Context, sdk.WebSocketConn) (sdk.ForwardOutcome, error) { + return sdk.ForwardOutcome{}, sdk.ErrNotSupported +} + +func (taskGatewayPlugin) ProcessTask(context.Context, sdk.HostTask) error { return nil } +func (taskGatewayPlugin) TaskTypes() []string { return []string{"image_generation"} } + +func TestGatewayGRPCPlugin_RegistersTaskExtensionService(t *testing.T) { + server := gogrpc.NewServer() + t.Cleanup(server.Stop) + + if err := (&GatewayGRPCPlugin{Impl: taskGatewayPlugin{}}).GRPCServer(nil, server); err != nil { + t.Fatalf("GRPCServer() error = %v", err) + } + + services := server.GetServiceInfo() + for _, name := range []string{ + pb.PluginService_ServiceDesc.ServiceName, + pb.GatewayService_ServiceDesc.ServiceName, + pb.EventService_ServiceDesc.ServiceName, + pb.ExtensionService_ServiceDesc.ServiceName, + } { + if _, ok := services[name]; !ok { + t.Fatalf("未注册服务 %s,已注册: %#v", name, services) + } + } +} + +func TestGatewayGRPCPlugin_DoesNotRegisterTaskExtensionForPlainGateway(t *testing.T) { + server := gogrpc.NewServer() + t.Cleanup(server.Stop) + + if err := (&GatewayGRPCPlugin{Impl: plainGatewayPlugin{}}).GRPCServer(nil, server); err != nil { + t.Fatalf("GRPCServer() error = %v", err) + } + + services := server.GetServiceInfo() + if _, ok := services[pb.ExtensionService_ServiceDesc.ServiceName]; ok { + t.Fatalf("普通网关不应注册 ExtensionService") + } +} + +type plainGatewayPlugin struct{} + +func (plainGatewayPlugin) Info() sdk.PluginInfo { + return sdk.PluginInfo{ID: "plain-gateway", Type: sdk.PluginTypeGateway} +} + +func (plainGatewayPlugin) Init(sdk.PluginContext) error { return nil } +func (plainGatewayPlugin) Start(context.Context) error { return nil } +func (plainGatewayPlugin) Stop(context.Context) error { return nil } + +func (plainGatewayPlugin) Platform() string { return "demo" } +func (plainGatewayPlugin) Models() []sdk.ModelInfo { return nil } +func (plainGatewayPlugin) Routes() []sdk.RouteDefinition { return nil } + +func (plainGatewayPlugin) Forward(context.Context, *sdk.ForwardRequest) (sdk.ForwardOutcome, error) { + return sdk.ForwardOutcome{}, nil +} + +func (plainGatewayPlugin) ValidateAccount(context.Context, map[string]string) error { return nil } + +func (plainGatewayPlugin) HandleWebSocket(context.Context, sdk.WebSocketConn) (sdk.ForwardOutcome, error) { + return sdk.ForwardOutcome{}, sdk.ErrNotSupported +} diff --git a/grpc/handshake.go b/runtimego/grpc/handshake.go similarity index 100% rename from grpc/handshake.go rename to runtimego/grpc/handshake.go diff --git a/runtimego/grpc/host_client.go b/runtimego/grpc/host_client.go new file mode 100644 index 0000000..e85a7e5 --- /dev/null +++ b/runtimego/grpc/host_client.go @@ -0,0 +1,157 @@ +package grpc + +import ( + "context" + "log/slog" + "time" + + "google.golang.org/grpc" + + pb "github.com/DouDOU-start/airgate-sdk/protocol/proto" + sdk "github.com/DouDOU-start/airgate-sdk/sdkgo" +) + +// hostClient 把 pb.CoreInvokeServiceClient 包装成 sdk.Host 接口。 +// 插件代码只看到 sdk.Host,不直接接触 protobuf 类型。 +type hostClient struct { + c pb.CoreInvokeServiceClient +} + +// NewHostClient 用一个 grpc client 构造 sdk.Host。 +// 一般由 grpcPluginContext.Host() lazy 调用,不建议插件直接构造。 +func NewHostClient(c pb.CoreInvokeServiceClient) sdk.Host { + return &hostClient{c: c} +} + +// hostRPCLogger 派生 host 调用专用 logger,并返回起始时间。 +func hostRPCLogger(ctx context.Context, method string) (*slog.Logger, time.Time) { + return sdk.LoggerFromContext(ctx).With("host_rpc", method), time.Now() +} + +// Invoke 调用 Core 方法注册表中开放的方法。 +func (h *hostClient) Invoke(ctx context.Context, req sdk.HostInvokeRequest) (*sdk.HostInvokeResponse, error) { + logger, start := hostRPCLogger(ctx, "Invoke") + payload, err := mapToJSONPayload(req.Payload) + if err != nil { + logger.Error("host_call_invoke_payload_encode_failed", + "method", req.Method, + sdk.LogFieldDurationMs, time.Since(start).Milliseconds(), + sdk.LogFieldError, err, + ) + return nil, err + } + resp, err := h.c.Invoke(ctx, &pb.HostInvokeRequest{ + Method: req.Method, + Payload: payload, + IdempotencyKey: req.IdempotencyKey, + Metadata: req.Metadata, + }) + if err != nil { + logger.Error("host_call_invoke_failed", + "method", req.Method, + sdk.LogFieldDurationMs, time.Since(start).Milliseconds(), + sdk.LogFieldError, err, + ) + return nil, err + } + responsePayload, err := jsonPayloadToMap(resp.Payload) + if err != nil { + logger.Error("host_call_invoke_payload_decode_failed", + "method", req.Method, + sdk.LogFieldDurationMs, time.Since(start).Milliseconds(), + sdk.LogFieldError, err, + ) + return nil, err + } + logger.Debug("host_call_invoke_completed", + "method", req.Method, + sdk.LogFieldDurationMs, time.Since(start).Milliseconds(), + ) + return &sdk.HostInvokeResponse{ + Status: resp.Status, + Payload: responsePayload, + Metadata: resp.Metadata, + }, nil +} + +// InvokeStream 调用 Core 方法注册表中开放的流式方法。 +func (h *hostClient) InvokeStream(ctx context.Context, req sdk.HostStreamRequest) (sdk.HostStream, error) { + logger, start := hostRPCLogger(ctx, "InvokeStream") + payload, err := mapToJSONPayload(req.Payload) + if err != nil { + logger.Error("host_call_invoke_stream_payload_encode_failed", + "method", req.Method, + sdk.LogFieldDurationMs, time.Since(start).Milliseconds(), + sdk.LogFieldError, err, + ) + return nil, err + } + stream, err := h.c.InvokeStream(ctx) + if err != nil { + logger.Error("host_call_invoke_stream_open_failed", + "method", req.Method, + sdk.LogFieldDurationMs, time.Since(start).Milliseconds(), + sdk.LogFieldError, err, + ) + return nil, err + } + if err := stream.Send(&pb.HostStreamFrame{ + Method: req.Method, + Payload: payload, + IdempotencyKey: req.IdempotencyKey, + Metadata: req.Metadata, + }); err != nil { + _ = stream.CloseSend() + logger.Error("host_call_invoke_stream_start_failed", + "method", req.Method, + sdk.LogFieldDurationMs, time.Since(start).Milliseconds(), + sdk.LogFieldError, err, + ) + return nil, err + } + logger.Debug("host_call_invoke_stream_opened", + "method", req.Method, + sdk.LogFieldDurationMs, time.Since(start).Milliseconds(), + ) + return &hostStream{stream: stream}, nil +} + +type hostStream struct { + stream grpc.BidiStreamingClient[pb.HostStreamFrame, pb.HostStreamFrame] +} + +func (s *hostStream) Send(frame sdk.HostStreamFrame) error { + payload, err := mapToJSONPayload(frame.Payload) + if err != nil { + return err + } + return s.stream.Send(&pb.HostStreamFrame{ + Event: frame.Event, + Status: frame.Status, + Payload: payload, + Metadata: frame.Metadata, + Done: frame.Done, + }) +} + +func (s *hostStream) Recv() (*sdk.HostStreamFrame, error) { + frame, err := s.stream.Recv() + if err != nil { + return nil, err + } + payload, err := jsonPayloadToMap(frame.Payload) + if err != nil { + return nil, err + } + return &sdk.HostStreamFrame{ + Event: frame.Event, + Status: frame.Status, + Payload: payload, + Metadata: frame.Metadata, + Done: frame.Done, + }, nil +} + +func (s *hostStream) CloseSend() error { + return s.stream.CloseSend() +} diff --git a/runtimego/grpc/host_client_test.go b/runtimego/grpc/host_client_test.go new file mode 100644 index 0000000..85303f6 --- /dev/null +++ b/runtimego/grpc/host_client_test.go @@ -0,0 +1,243 @@ +package grpc + +import ( + "context" + "io" + "reflect" + "testing" + + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" + + pb "github.com/DouDOU-start/airgate-sdk/protocol/proto" + sdk "github.com/DouDOU-start/airgate-sdk/sdkgo" +) + +type stubCoreInvokeClient struct { + stream *stubHostStreamClient + invokeReq *pb.HostInvokeRequest + invokeResp *pb.HostInvokeResponse +} + +func (c *stubCoreInvokeClient) Invoke(_ context.Context, req *pb.HostInvokeRequest, _ ...grpc.CallOption) (*pb.HostInvokeResponse, error) { + c.invokeReq = req + if c.invokeResp != nil { + return c.invokeResp, nil + } + return &pb.HostInvokeResponse{Status: "ok"}, nil +} + +func (c *stubCoreInvokeClient) InvokeStream(context.Context, ...grpc.CallOption) (grpc.BidiStreamingClient[pb.HostStreamFrame, pb.HostStreamFrame], error) { + return c.stream, nil +} + +type stubHostStreamClient struct { + ctx context.Context + sentFrames []*pb.HostStreamFrame + recvFrames []*pb.HostStreamFrame + recvIndex int + closed bool +} + +func (s *stubHostStreamClient) Send(frame *pb.HostStreamFrame) error { + s.sentFrames = append(s.sentFrames, frame) + return nil +} + +func (s *stubHostStreamClient) Recv() (*pb.HostStreamFrame, error) { + if s.recvIndex >= len(s.recvFrames) { + return nil, io.EOF + } + frame := s.recvFrames[s.recvIndex] + s.recvIndex++ + return frame, nil +} + +func (s *stubHostStreamClient) Header() (metadata.MD, error) { return metadata.MD{}, nil } +func (s *stubHostStreamClient) Trailer() metadata.MD { return metadata.MD{} } +func (s *stubHostStreamClient) CloseSend() error { + s.closed = true + return nil +} +func (s *stubHostStreamClient) Context() context.Context { + if s.ctx != nil { + return s.ctx + } + return context.Background() +} +func (s *stubHostStreamClient) SendMsg(any) error { return nil } +func (s *stubHostStreamClient) RecvMsg(any) error { return nil } + +func mustJSONPayload(t *testing.T, payload map[string]interface{}) []byte { + t.Helper() + data, err := mapToJSONPayload(payload) + if err != nil { + t.Fatalf("mapToJSONPayload() error = %v", err) + } + return data +} + +func mustJSONPayloadMap(t *testing.T, data []byte) map[string]interface{} { + t.Helper() + payload, err := jsonPayloadToMap(data) + if err != nil { + t.Fatalf("jsonPayloadToMap() error = %v", err) + } + return payload +} + +func TestHostClientInvokeStreamRoundTrip(t *testing.T) { + grpcStream := &stubHostStreamClient{ + recvFrames: []*pb.HostStreamFrame{ + { + Event: "chunk", + Payload: mustJSONPayload(t, map[string]interface{}{"delta": "hello"}), + Metadata: map[string]string{"seq": "1"}, + }, + { + Event: "result", + Status: "ok", + Payload: mustJSONPayload(t, map[string]interface{}{"done": true}), + Done: true, + }, + }, + } + host := NewHostClient(&stubCoreInvokeClient{stream: grpcStream}) + + stream, err := host.InvokeStream(context.Background(), sdk.HostStreamRequest{ + Method: "chat.stream", + Payload: map[string]interface{}{"prompt": "hi"}, + IdempotencyKey: "idem_1", + Metadata: map[string]string{"trace": "abc"}, + }) + if err != nil { + t.Fatalf("InvokeStream() error = %v", err) + } + if len(grpcStream.sentFrames) != 1 { + t.Fatalf("首帧数量 = %d,期望 1", len(grpcStream.sentFrames)) + } + start := grpcStream.sentFrames[0] + if start.Method != "chat.stream" || start.IdempotencyKey != "idem_1" { + t.Fatalf("首帧 method/idempotency = %q/%q", start.Method, start.IdempotencyKey) + } + if got := mustJSONPayloadMap(t, start.Payload); !reflect.DeepEqual(got, map[string]interface{}{"prompt": "hi"}) { + t.Fatalf("首帧 payload = %v", got) + } + if !reflect.DeepEqual(start.Metadata, map[string]string{"trace": "abc"}) { + t.Fatalf("首帧 metadata = %v", start.Metadata) + } + + if err := stream.Send(sdk.HostStreamFrame{ + Event: "client_ack", + Payload: map[string]interface{}{"received": float64(1)}, + Metadata: map[string]string{"side": "plugin"}, + }); err != nil { + t.Fatalf("Send() error = %v", err) + } + if len(grpcStream.sentFrames) != 2 { + t.Fatalf("发送帧数量 = %d,期望 2", len(grpcStream.sentFrames)) + } + ack := grpcStream.sentFrames[1] + if ack.Method != "" || ack.Event != "client_ack" { + t.Fatalf("后续帧 method/event = %q/%q", ack.Method, ack.Event) + } + if got := mustJSONPayloadMap(t, ack.Payload); !reflect.DeepEqual(got, map[string]interface{}{"received": float64(1)}) { + t.Fatalf("后续帧 payload = %v", got) + } + + chunk, err := stream.Recv() + if err != nil { + t.Fatalf("Recv chunk error = %v", err) + } + if chunk.Event != "chunk" || !reflect.DeepEqual(chunk.Payload, map[string]interface{}{"delta": "hello"}) { + t.Fatalf("chunk = %+v", chunk) + } + + final, err := stream.Recv() + if err != nil { + t.Fatalf("Recv final error = %v", err) + } + if !final.Done || final.Status != "ok" || !reflect.DeepEqual(final.Payload, map[string]interface{}{"done": true}) { + t.Fatalf("final = %+v", final) + } + + if err := stream.CloseSend(); err != nil { + t.Fatalf("CloseSend() error = %v", err) + } + if !grpcStream.closed { + t.Fatal("底层 stream 未关闭发送方向") + } +} + +func TestHostClientInvokeRejectsInvalidPayload(t *testing.T) { + client := &stubCoreInvokeClient{} + host := NewHostClient(client) + + _, err := host.Invoke(context.Background(), sdk.HostInvokeRequest{ + Method: "tasks.update", + Payload: map[string]interface{}{"bad": func() {}}, + }) + if err == nil { + t.Fatal("Invoke() 应拒绝不可 JSON 编码的 payload") + } + if client.invokeReq != nil { + t.Fatal("payload 编码失败后不应发起 gRPC 调用") + } +} + +func TestHostClientInvokeRejectsMalformedResponsePayload(t *testing.T) { + host := NewHostClient(&stubCoreInvokeClient{ + invokeResp: &pb.HostInvokeResponse{ + Status: "ok", + Payload: []byte("{bad"), + }, + }) + + _, err := host.Invoke(context.Background(), sdk.HostInvokeRequest{Method: "tasks.get"}) + if err == nil { + t.Fatal("Invoke() 应拒绝 Core 返回的非法 JSON payload") + } +} + +func TestHostClientInvokeStreamRejectsInvalidInitialPayload(t *testing.T) { + grpcStream := &stubHostStreamClient{} + host := NewHostClient(&stubCoreInvokeClient{stream: grpcStream}) + + _, err := host.InvokeStream(context.Background(), sdk.HostStreamRequest{ + Method: "chat.stream", + Payload: map[string]interface{}{"bad": func() {}}, + }) + if err == nil { + t.Fatal("InvokeStream() 应拒绝不可 JSON 编码的首帧 payload") + } + if len(grpcStream.sentFrames) != 0 { + t.Fatalf("payload 编码失败后发送帧数量 = %d,期望 0", len(grpcStream.sentFrames)) + } +} + +func TestHostStreamSendRejectsInvalidPayload(t *testing.T) { + grpcStream := &stubHostStreamClient{} + stream := &hostStream{stream: grpcStream} + + err := stream.Send(sdk.HostStreamFrame{ + Event: "client_chunk", + Payload: map[string]interface{}{"bad": func() {}}, + }) + if err == nil { + t.Fatal("Send() 应拒绝不可 JSON 编码的 payload") + } + if len(grpcStream.sentFrames) != 0 { + t.Fatalf("payload 编码失败后发送帧数量 = %d,期望 0", len(grpcStream.sentFrames)) + } +} + +func TestHostStreamRecvRejectsMalformedPayload(t *testing.T) { + stream := &hostStream{stream: &stubHostStreamClient{ + recvFrames: []*pb.HostStreamFrame{{Event: "chunk", Payload: []byte("{bad")}}, + }} + + _, err := stream.Recv() + if err == nil { + t.Fatal("Recv() 应拒绝 Core 返回的非法 JSON payload") + } +} diff --git a/runtimego/grpc/json_payload.go b/runtimego/grpc/json_payload.go new file mode 100644 index 0000000..c3ad6a0 --- /dev/null +++ b/runtimego/grpc/json_payload.go @@ -0,0 +1,28 @@ +package grpc + +import ( + "encoding/json" + "fmt" +) + +func mapToJSONPayload(m map[string]interface{}) ([]byte, error) { + if len(m) == 0 { + return nil, nil + } + data, err := json.Marshal(m) + if err != nil { + return nil, fmt.Errorf("JSON payload 编码失败: %w", err) + } + return data, nil +} + +func jsonPayloadToMap(data []byte) (map[string]interface{}, error) { + if len(data) == 0 { + return nil, nil + } + var out map[string]interface{} + if err := json.Unmarshal(data, &out); err != nil { + return nil, fmt.Errorf("JSON payload 解码失败: %w", err) + } + return out, nil +} diff --git a/grpc/logging_interceptor.go b/runtimego/grpc/logging_interceptor.go similarity index 99% rename from grpc/logging_interceptor.go rename to runtimego/grpc/logging_interceptor.go index e23efe3..5c371cc 100644 --- a/grpc/logging_interceptor.go +++ b/runtimego/grpc/logging_interceptor.go @@ -11,7 +11,7 @@ import ( "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" - sdk "github.com/DouDOU-start/airgate-sdk" + sdk "github.com/DouDOU-start/airgate-sdk/sdkgo" ) // MetadataRequestIDKey 是 gRPC metadata 中 request_id 的键。 diff --git a/grpc/middleware_client.go b/runtimego/grpc/middleware_client.go similarity index 66% rename from grpc/middleware_client.go rename to runtimego/grpc/middleware_client.go index 0521944..b5e27b0 100644 --- a/grpc/middleware_client.go +++ b/runtimego/grpc/middleware_client.go @@ -4,8 +4,8 @@ import ( "context" "time" - sdk "github.com/DouDOU-start/airgate-sdk" - pb "github.com/DouDOU-start/airgate-sdk/proto" + pb "github.com/DouDOU-start/airgate-sdk/protocol/proto" + sdk "github.com/DouDOU-start/airgate-sdk/sdkgo" ) // MiddlewareGRPCClient 把 pb.MiddlewareServiceClient 包装成给 core 用的 plain Go API。 @@ -13,7 +13,7 @@ import ( // 嵌入 pluginBase 自动获得 Info / Init / Start / Stop / GetWebAssets / HealthCheck // 等基础能力,与 GatewayGRPCClient / ExtensionGRPCClient 平行。 // -// 失败语义(ADR-0001 Decision 2):transport 层 error 在 core 侧由 manager 转化为 +// 失败语义:transport 层 error 在 core 侧由 manager 转化为 // log warn + 跳过本次调用。本 client 只做 protobuf 序列化,不吞 error。 type MiddlewareGRPCClient struct { pluginBase @@ -49,16 +49,16 @@ func middlewareRequestToProto(req *sdk.MiddlewareRequest) *pb.MiddlewareRequest return &pb.MiddlewareRequest{} } out := &pb.MiddlewareRequest{ - RequestId: req.RequestID, - UserId: req.UserID, - GroupId: req.GroupID, - AccountId: req.AccountID, - Platform: req.Platform, - Model: req.Model, - Stream: req.Stream, - InputTokensEst: req.InputTokensEst, - Metadata: cloneStringMapMW(req.Metadata), - RequestBody: req.RequestBody, + RequestId: req.RequestID, + UserId: req.UserID, + GroupId: req.GroupID, + AccountId: req.AccountID, + Platform: req.Platform, + Model: req.Model, + Stream: req.Stream, + Estimates: usageMetricsToProto(req.Estimates), + Metadata: cloneStringMapMW(req.Metadata), + RequestBody: req.RequestBody, } if len(req.RequestHeaders) > 0 { out.RequestHeaders = httpHeadersToProto(req.RequestHeaders) @@ -71,27 +71,23 @@ func middlewareEventToProto(evt *sdk.MiddlewareEvent) *pb.MiddlewareEvent { return &pb.MiddlewareEvent{} } out := &pb.MiddlewareEvent{ - RequestId: evt.RequestID, - UserId: evt.UserID, - GroupId: evt.GroupID, - AccountId: evt.AccountID, - Platform: evt.Platform, - Model: evt.Model, - Stream: evt.Stream, - InputTokensEst: evt.InputTokensEst, - StatusCode: int64(evt.StatusCode), - DurationMs: int64(evt.Duration / time.Millisecond), - InputTokens: evt.InputTokens, - OutputTokens: evt.OutputTokens, - CachedInputTokens: evt.CachedInputTokens, - FirstTokenMs: evt.FirstTokenMs, - ErrorKind: evt.ErrorKind, - ErrorMsg: evt.ErrorMsg, - InputCost: evt.InputCost, - OutputCost: evt.OutputCost, - CachedInputCost: evt.CachedInputCost, - Metadata: cloneStringMapMW(evt.Metadata), - ResponseBody: evt.ResponseBody, + RequestId: evt.RequestID, + UserId: evt.UserID, + GroupId: evt.GroupID, + AccountId: evt.AccountID, + Platform: evt.Platform, + Model: evt.Model, + Stream: evt.Stream, + Estimates: usageMetricsToProto(evt.Estimates), + StatusCode: int64(evt.StatusCode), + DurationMs: int64(evt.Duration / time.Millisecond), + ErrorKind: evt.ErrorKind, + ErrorMsg: evt.ErrorMsg, + Metadata: cloneStringMapMW(evt.Metadata), + ResponseBody: evt.ResponseBody, + } + if evt.Usage != nil { + out.Usage = usageToProto(*evt.Usage) } if len(evt.ResponseHeaders) > 0 { out.ResponseHeaders = httpHeadersToProto(evt.ResponseHeaders) diff --git a/grpc/middleware_server.go b/runtimego/grpc/middleware_server.go similarity index 70% rename from grpc/middleware_server.go rename to runtimego/grpc/middleware_server.go index 390ed86..23c4b74 100644 --- a/grpc/middleware_server.go +++ b/runtimego/grpc/middleware_server.go @@ -4,16 +4,15 @@ import ( "context" "time" - sdk "github.com/DouDOU-start/airgate-sdk" - pb "github.com/DouDOU-start/airgate-sdk/proto" + pb "github.com/DouDOU-start/airgate-sdk/protocol/proto" + sdk "github.com/DouDOU-start/airgate-sdk/sdkgo" ) // MiddlewareGRPCServer 把 sdk.MiddlewarePlugin 实现包成 gRPC server。 // // 失败语义:插件代码返回 error 时,server 仍然返回 (response, nil) 给 core。 // 这样 core 在 transport 层不会看到 error,由 core 自己根据 response 内容判断 -// (或在 core 侧做 deadline 超时控制)。这是 ADR-0001 Decision 2 "middleware 永远 -// 不能 block 生产" 的落地点。 +// (或在 core 侧做 deadline 超时控制)。middleware 调用不得阻塞生产流量。 // // 目前 server 端不主动吞 error;client 端会把 transport error 转化为 log warn // 然后让 core 跳过这个 middleware。两层都有保护。 @@ -63,16 +62,16 @@ func middlewareRequestFromProto(req *pb.MiddlewareRequest) *sdk.MiddlewareReques return nil } out := &sdk.MiddlewareRequest{ - RequestID: req.RequestId, - UserID: req.UserId, - GroupID: req.GroupId, - AccountID: req.AccountId, - Platform: req.Platform, - Model: req.Model, - Stream: req.Stream, - InputTokensEst: req.InputTokensEst, - Metadata: cloneStringMapMW(req.Metadata), - RequestBody: req.RequestBody, + RequestID: req.RequestId, + UserID: req.UserId, + GroupID: req.GroupId, + AccountID: req.AccountId, + Platform: req.Platform, + Model: req.Model, + Stream: req.Stream, + Estimates: usageMetricsFromProto(req.Estimates), + Metadata: cloneStringMapMW(req.Metadata), + RequestBody: req.RequestBody, } if len(req.RequestHeaders) > 0 { out.RequestHeaders = protoHeadersToHTTP(req.RequestHeaders) @@ -85,27 +84,24 @@ func middlewareEventFromProto(evt *pb.MiddlewareEvent) *sdk.MiddlewareEvent { return nil } out := &sdk.MiddlewareEvent{ - RequestID: evt.RequestId, - UserID: evt.UserId, - GroupID: evt.GroupId, - AccountID: evt.AccountId, - Platform: evt.Platform, - Model: evt.Model, - Stream: evt.Stream, - InputTokensEst: evt.InputTokensEst, - StatusCode: int32(evt.StatusCode), - Duration: time.Duration(evt.DurationMs) * time.Millisecond, - InputTokens: evt.InputTokens, - OutputTokens: evt.OutputTokens, - CachedInputTokens: evt.CachedInputTokens, - FirstTokenMs: evt.FirstTokenMs, - ErrorKind: evt.ErrorKind, - ErrorMsg: evt.ErrorMsg, - InputCost: evt.InputCost, - OutputCost: evt.OutputCost, - CachedInputCost: evt.CachedInputCost, - Metadata: cloneStringMapMW(evt.Metadata), - ResponseBody: evt.ResponseBody, + RequestID: evt.RequestId, + UserID: evt.UserId, + GroupID: evt.GroupId, + AccountID: evt.AccountId, + Platform: evt.Platform, + Model: evt.Model, + Stream: evt.Stream, + Estimates: usageMetricsFromProto(evt.Estimates), + StatusCode: int32(evt.StatusCode), + Duration: time.Duration(evt.DurationMs) * time.Millisecond, + ErrorKind: evt.ErrorKind, + ErrorMsg: evt.ErrorMsg, + Metadata: cloneStringMapMW(evt.Metadata), + ResponseBody: evt.ResponseBody, + } + if evt.Usage != nil { + u := usageFromProto(evt.Usage) + out.Usage = &u } if len(evt.ResponseHeaders) > 0 { out.ResponseHeaders = protoHeadersToHTTP(evt.ResponseHeaders) diff --git a/grpc/plugin_server.go b/runtimego/grpc/plugin_server.go similarity index 91% rename from grpc/plugin_server.go rename to runtimego/grpc/plugin_server.go index 66b8dbb..2659fd9 100644 --- a/grpc/plugin_server.go +++ b/runtimego/grpc/plugin_server.go @@ -7,15 +7,15 @@ import ( goplugin "github.com/hashicorp/go-plugin" - sdk "github.com/DouDOU-start/airgate-sdk" - pb "github.com/DouDOU-start/airgate-sdk/proto" + pb "github.com/DouDOU-start/airgate-sdk/protocol/proto" + sdk "github.com/DouDOU-start/airgate-sdk/sdkgo" ) // PluginGRPCServer 将 sdk.Plugin 实现包装为 gRPC 服务端 // // Broker 字段由 GatewayGRPCPlugin / ExtensionGRPCPlugin 在 GRPCServer 钩子里注入。 // 它代表插件进程侧的 hashicorp/go-plugin GRPCBroker,用于通过 broker.Dial() 拿到 -// Core 暴露的 HostService 反向连接。 +// Core 暴露的反向调用连接。 type PluginGRPCServer struct { pb.UnimplementedPluginServiceServer Impl sdk.Plugin @@ -33,6 +33,7 @@ func (s *PluginGRPCServer) GetInfo(_ context.Context, _ *pb.Empty) (*pb.PluginIn Type: string(info.Type), SdkVersion: info.SDKVersion, Dependencies: info.Dependencies, + Metadata: info.Metadata, } if len(info.ConfigSchema) > 0 { @@ -120,9 +121,9 @@ func (s *PluginGRPCServer) Init(_ context.Context, req *pb.InitRequest) (*pb.Emp slog.Info("plugin_init_start", sdk.LogFieldPluginID, info.ID) pctx := &grpcPluginContext{ - config: &mapConfig{data: req.Config}, - broker: s.Broker, - hostBrokerID: req.HostBrokerId, + config: &mapConfig{data: req.Config}, + broker: s.Broker, + coreInvokeBrokerID: req.CoreInvokeBrokerId, } if err := s.Impl.Init(pctx); err != nil { slog.Error("plugin_init_failed", @@ -191,6 +192,15 @@ func (s *PluginGRPCServer) GetWebAssets(_ context.Context, _ *pb.Empty) (*pb.Web return resp, nil } +// GetSchema 获取插件结构化能力清单。 +func (s *PluginGRPCServer) GetSchema(_ context.Context, _ *pb.Empty) (*pb.PluginSchemaResponse, error) { + provider, ok := s.Impl.(sdk.SchemaProvider) + if !ok { + return &pb.PluginSchemaResponse{}, nil + } + return schemaToProto(provider.Schema()), nil +} + // HandleRequest 通用请求代理,插件实现 RequestHandler 接口即可处理自定义请求 func (s *PluginGRPCServer) HandleRequest(ctx context.Context, req *pb.HttpRequest) (*pb.HttpResponse, error) { handler, ok := s.Impl.(sdk.RequestHandler) diff --git a/runtimego/grpc/schema.go b/runtimego/grpc/schema.go new file mode 100644 index 0000000..68a0a5a --- /dev/null +++ b/runtimego/grpc/schema.go @@ -0,0 +1,147 @@ +package grpc + +import ( + pb "github.com/DouDOU-start/airgate-sdk/protocol/proto" + sdk "github.com/DouDOU-start/airgate-sdk/sdkgo" +) + +func payloadSchemaToProto(s sdk.PayloadSchema) *pb.PayloadSchemaProto { + if s.ContentType == "" && s.Schema == "" && s.Example == "" && len(s.Metadata) == 0 { + return nil + } + return &pb.PayloadSchemaProto{ + ContentType: s.ContentType, + Schema: s.Schema, + Example: s.Example, + Metadata: s.Metadata, + } +} + +func payloadSchemaFromProto(p *pb.PayloadSchemaProto) sdk.PayloadSchema { + if p == nil { + return sdk.PayloadSchema{} + } + return sdk.PayloadSchema{ + ContentType: p.ContentType, + Schema: p.Schema, + Example: p.Example, + Metadata: p.Metadata, + } +} + +func schemaToProto(s sdk.PluginSchema) *pb.PluginSchemaResponse { + out := &pb.PluginSchemaResponse{Metadata: s.Metadata} + if len(s.Routes) > 0 { + out.Routes = make([]*pb.RouteSchemaProto, 0, len(s.Routes)) + for _, r := range s.Routes { + out.Routes = append(out.Routes, &pb.RouteSchemaProto{ + Method: r.Method, + Path: r.Path, + Summary: r.Summary, + Request: payloadSchemaToProto(r.Request), + Response: payloadSchemaToProto(r.Response), + Metadata: r.Metadata, + }) + } + } + if len(s.Tasks) > 0 { + out.Tasks = make([]*pb.TaskSchemaProto, 0, len(s.Tasks)) + for _, t := range s.Tasks { + out.Tasks = append(out.Tasks, &pb.TaskSchemaProto{ + Type: t.Type, + Summary: t.Summary, + Input: payloadSchemaToProto(t.Input), + Output: payloadSchemaToProto(t.Output), + Metadata: t.Metadata, + }) + } + } + if len(s.Events) > 0 { + out.Events = make([]*pb.EventSchemaProto, 0, len(s.Events)) + for _, e := range s.Events { + out.Events = append(out.Events, &pb.EventSchemaProto{ + Type: e.Type, + Source: e.Source, + Summary: e.Summary, + Payload: payloadSchemaToProto(e.Payload), + Metadata: e.Metadata, + }) + } + } + if len(s.Invokes) > 0 { + out.Invokes = make([]*pb.InvokeSchemaProto, 0, len(s.Invokes)) + for _, i := range s.Invokes { + out.Invokes = append(out.Invokes, &pb.InvokeSchemaProto{ + Method: i.Method, + Summary: i.Summary, + Request: payloadSchemaToProto(i.Request), + Response: payloadSchemaToProto(i.Response), + Transport: string(i.Transport), + ClientFrame: payloadSchemaToProto(i.ClientFrame), + ServerFrame: payloadSchemaToProto(i.ServerFrame), + Metadata: i.Metadata, + }) + } + } + return out +} + +func schemaFromProto(p *pb.PluginSchemaResponse) sdk.PluginSchema { + if p == nil { + return sdk.PluginSchema{} + } + out := sdk.PluginSchema{Metadata: p.Metadata} + if len(p.Routes) > 0 { + out.Routes = make([]sdk.RouteSchema, 0, len(p.Routes)) + for _, r := range p.Routes { + out.Routes = append(out.Routes, sdk.RouteSchema{ + Method: r.Method, + Path: r.Path, + Summary: r.Summary, + Request: payloadSchemaFromProto(r.Request), + Response: payloadSchemaFromProto(r.Response), + Metadata: r.Metadata, + }) + } + } + if len(p.Tasks) > 0 { + out.Tasks = make([]sdk.TaskSchema, 0, len(p.Tasks)) + for _, t := range p.Tasks { + out.Tasks = append(out.Tasks, sdk.TaskSchema{ + Type: t.Type, + Summary: t.Summary, + Input: payloadSchemaFromProto(t.Input), + Output: payloadSchemaFromProto(t.Output), + Metadata: t.Metadata, + }) + } + } + if len(p.Events) > 0 { + out.Events = make([]sdk.EventSchema, 0, len(p.Events)) + for _, e := range p.Events { + out.Events = append(out.Events, sdk.EventSchema{ + Type: e.Type, + Source: e.Source, + Summary: e.Summary, + Payload: payloadSchemaFromProto(e.Payload), + Metadata: e.Metadata, + }) + } + } + if len(p.Invokes) > 0 { + out.Invokes = make([]sdk.InvokeSchema, 0, len(p.Invokes)) + for _, i := range p.Invokes { + out.Invokes = append(out.Invokes, sdk.InvokeSchema{ + Method: i.Method, + Summary: i.Summary, + Request: payloadSchemaFromProto(i.Request), + Response: payloadSchemaFromProto(i.Response), + Transport: sdk.InvokeTransport(i.Transport), + ClientFrame: payloadSchemaFromProto(i.ClientFrame), + ServerFrame: payloadSchemaFromProto(i.ServerFrame), + Metadata: i.Metadata, + }) + } + } + return out +} diff --git a/grpc/ws_server.go b/runtimego/grpc/ws_server.go similarity index 96% rename from grpc/ws_server.go rename to runtimego/grpc/ws_server.go index 75917dd..c7d9670 100644 --- a/grpc/ws_server.go +++ b/runtimego/grpc/ws_server.go @@ -5,8 +5,8 @@ import ( "fmt" "io" - sdk "github.com/DouDOU-start/airgate-sdk" - pb "github.com/DouDOU-start/airgate-sdk/proto" + pb "github.com/DouDOU-start/airgate-sdk/protocol/proto" + sdk "github.com/DouDOU-start/airgate-sdk/sdkgo" ) // HandleWebSocket 处理核心发来的 WebSocket 双向流 diff --git a/capability.go b/sdkgo/capability.go similarity index 57% rename from capability.go rename to sdkgo/capability.go index e63c3f7..5452a12 100644 --- a/capability.go +++ b/sdkgo/capability.go @@ -1,61 +1,68 @@ package sdk -import "sort" +import ( + "sort" + "strings" +) // Capability 类型化的能力标识符(命名规范:.)。 // 所有 capability 常量必须用此类型以便编译期捕获拼写错误。 // -// 运行时授权由 Core 的 gRPC interceptor 强制执行;本类型是 SDK 侧的强类型入口, -// 并不绕过 Core 的准入校验。 +// 运行时授权由 Core 强制执行;SDK 只负责声明格式和基础类型自检。 type Capability string func (c Capability) String() string { return string(c) } const ( - CapabilityHostListGroups Capability = "host.list_groups" - CapabilityHostSelectAccount Capability = "host.select_account" - CapabilityHostProbeForward Capability = "host.probe_forward" - CapabilityHostReportAccountResult Capability = "host.report_account_result" - CapabilityHostForward Capability = "host.forward" - CapabilityHostListPlatforms Capability = "host.list_platforms" - CapabilityHostListModels Capability = "host.list_models" - CapabilityHostGetUserInfo Capability = "host.get_user_info" - CapabilityHostAssetStorage Capability = "host.asset_storage" + // CapabilityHostInvoke 允许插件通过 Host.Invoke / Host.InvokeStream 调用 Core 开放的方法。 + // + // Core 可以进一步要求插件声明 method 级 capability: + // + // host.invoke. + // + // 例如: + // + // host.invoke.scheduler.select_account + // host.invoke.tasks.update + CapabilityHostInvoke Capability = "host.invoke" CapabilityMiddlewareReadBody Capability = "middleware.read_body" ) -// capabilityAllowedTypes "插件类型 → 允许声明的 capability" 权威表。 -// Core 侧的 interceptor 也应读这里,避免双份维护。新增 capability 同步更新。 +const hostInvokeMethodCapabilityPrefix = "host.invoke." + +// CapabilityForHostMethod 返回某个 Core method 对应的细粒度 capability。 +func CapabilityForHostMethod(method string) Capability { + if method == "" { + return CapabilityHostInvoke + } + return Capability(hostInvokeMethodCapabilityPrefix + method) +} + +// capabilityAllowedTypes 是 SDK 侧的基础类型白名单。 +// +// Core 仍必须按方法注册表做最终授权,例如校验插件 ID、插件类型、method +// 是否开放、请求 schema、敏感字段和幂等策略。 var capabilityAllowedTypes = map[Capability]map[PluginType]bool{ - CapabilityHostListGroups: { - PluginTypeExtension: true, - PluginTypeMiddleware: true, - }, - CapabilityHostSelectAccount: {PluginTypeExtension: true}, - CapabilityHostProbeForward: {PluginTypeExtension: true}, - CapabilityHostReportAccountResult: {PluginTypeExtension: true}, - CapabilityHostForward: {PluginTypeExtension: true}, - CapabilityHostListPlatforms: { + CapabilityHostInvoke: { + PluginTypeGateway: true, PluginTypeExtension: true, PluginTypeMiddleware: true, }, - CapabilityHostListModels: { - PluginTypeExtension: true, - PluginTypeMiddleware: true, - }, - CapabilityHostGetUserInfo: {PluginTypeExtension: true}, - CapabilityHostAssetStorage: {PluginTypeExtension: true}, CapabilityMiddlewareReadBody: {PluginTypeMiddleware: true}, } // IsKnownCapability 判断 capability 是否在当前 SDK 版本的已知集合内。 func IsKnownCapability(c Capability) bool { + if isHostInvokeMethodCapability(c) { + return true + } _, ok := capabilityAllowedTypes[c] return ok } -// KnownCapabilities 返回所有已知 capability,按字典序排序。 +// KnownCapabilities 返回 SDK 内置 capability,按字典序排序。 +// host.invoke. 属于动态 capability,不会出现在此列表中。 func KnownCapabilities() []Capability { out := make([]Capability, 0, len(capabilityAllowedTypes)) for c := range capabilityAllowedTypes { @@ -65,6 +72,19 @@ func KnownCapabilities() []Capability { return out } +func isHostInvokeMethodCapability(c Capability) bool { + v := string(c) + return strings.HasPrefix(v, hostInvokeMethodCapabilityPrefix) && len(v) > len(hostInvokeMethodCapabilityPrefix) +} + +func allowedPluginTypesForCapability(c Capability) (map[PluginType]bool, bool) { + if isHostInvokeMethodCapability(c) { + return capabilityAllowedTypes[CapabilityHostInvoke], true + } + allowedTypes, known := capabilityAllowedTypes[c] + return allowedTypes, known +} + // CapabilityValidationReport ValidateCapabilities 的输出。 type CapabilityValidationReport struct { // Effective 当前 plugin type 下实际生效的 capability = 声明 ∩ 类型允许,去重+排序。 @@ -80,7 +100,7 @@ func (r CapabilityValidationReport) HasIssues() bool { return len(r.Unknown) > 0 || len(r.Denied) > 0 } -// ValidateCapabilities 对一组声明做 self-check。授权决策仍由 Core 的 interceptor 负责, +// ValidateCapabilities 对一组声明做 self-check。授权决策仍由 Core 的方法注册表负责, // 这里只做"声明 vs 已知 vs 类型允许"的纸面检查。 func ValidateCapabilities(typ PluginType, declared []Capability) CapabilityValidationReport { seen := make(map[Capability]bool, len(declared)) @@ -95,7 +115,7 @@ func ValidateCapabilities(typ PluginType, declared []Capability) CapabilityValid } seen[c] = true - allowedTypes, known := capabilityAllowedTypes[c] + allowedTypes, known := allowedPluginTypesForCapability(c) if !known { unknown = append(unknown, c) continue diff --git a/sdkgo/capability_test.go b/sdkgo/capability_test.go new file mode 100644 index 0000000..a35744c --- /dev/null +++ b/sdkgo/capability_test.go @@ -0,0 +1,110 @@ +package sdk_test + +import ( + "reflect" + "testing" + + sdk "github.com/DouDOU-start/airgate-sdk/sdkgo" +) + +func TestIsKnownCapability(t *testing.T) { + known := []sdk.Capability{ + sdk.CapabilityHostInvoke, + sdk.CapabilityForHostMethod("scheduler.select_account"), + sdk.CapabilityMiddlewareReadBody, + } + for _, c := range known { + if !sdk.IsKnownCapability(c) { + t.Errorf("IsKnownCapability(%q) = false,期望 true", c) + } + } + + unknown := []sdk.Capability{ + "host.invoke.", + "host.invokee.tasks.update", + } + for _, c := range unknown { + if sdk.IsKnownCapability(c) { + t.Errorf("IsKnownCapability(%q) = true,期望 false", c) + } + } +} + +func TestKnownCapabilitiesSortedAndComplete(t *testing.T) { + caps := sdk.KnownCapabilities() + want := []sdk.Capability{ + sdk.CapabilityHostInvoke, + sdk.CapabilityMiddlewareReadBody, + } + if !reflect.DeepEqual(caps, want) { + t.Fatalf("KnownCapabilities() = %v,期望 %v", caps, want) + } +} + +func TestValidateCapabilities_HostInvoke(t *testing.T) { + report := sdk.ValidateCapabilities(sdk.PluginTypeExtension, []sdk.Capability{ + sdk.CapabilityHostInvoke, + sdk.CapabilityForHostMethod("scheduler.select_account"), + }) + if report.HasIssues() { + t.Fatalf("HasIssues() = true,期望 false;report=%+v", report) + } + want := []sdk.Capability{ + sdk.CapabilityHostInvoke, + sdk.CapabilityForHostMethod("scheduler.select_account"), + } + if !reflect.DeepEqual(report.Effective, want) { + t.Errorf("Effective = %v,期望 %v", report.Effective, want) + } +} + +func TestValidateCapabilities_Unknown(t *testing.T) { + report := sdk.ValidateCapabilities(sdk.PluginTypeExtension, []sdk.Capability{ + sdk.CapabilityHostInvoke, + "host.invoke.", + "host.invokee.tasks.update", + }) + if !report.HasIssues() { + t.Fatal("HasIssues() = false,期望检测到未知 capability") + } + wantUnknown := []sdk.Capability{"host.invoke.", "host.invokee.tasks.update"} + if !reflect.DeepEqual(report.Unknown, wantUnknown) { + t.Errorf("Unknown = %v,期望 %v", report.Unknown, wantUnknown) + } + if len(report.Effective) != 1 || report.Effective[0] != sdk.CapabilityHostInvoke { + t.Errorf("Effective = %v,期望 [%v]", report.Effective, sdk.CapabilityHostInvoke) + } +} + +func TestValidateCapabilities_Denied(t *testing.T) { + report := sdk.ValidateCapabilities(sdk.PluginTypeGateway, []sdk.Capability{ + sdk.CapabilityMiddlewareReadBody, + }) + if !report.HasIssues() { + t.Fatal("HasIssues() = false,期望检测到插件类型不允许的 capability") + } + if len(report.Denied) != 1 || report.Denied[0] != sdk.CapabilityMiddlewareReadBody { + t.Errorf("Denied = %v,期望 [%v]", report.Denied, sdk.CapabilityMiddlewareReadBody) + } + if len(report.Effective) != 0 { + t.Errorf("Effective = %v,期望为空", report.Effective) + } +} + +func TestValidateCapabilities_Dedup(t *testing.T) { + capability := sdk.CapabilityForHostMethod("tasks.update") + report := sdk.ValidateCapabilities(sdk.PluginTypeExtension, []sdk.Capability{ + capability, + capability, + capability, + }) + if len(report.Effective) != 1 { + t.Errorf("Effective = %v,期望去重后只有 1 个", report.Effective) + } +} + +func TestGetPluginDSN_NilCtx(t *testing.T) { + if got := sdk.GetPluginDSN(nil); got != "" { + t.Errorf("GetPluginDSN(nil) = %q,期望空字符串", got) + } +} diff --git a/sdkgo/doc.go b/sdkgo/doc.go new file mode 100644 index 0000000..c241ddf --- /dev/null +++ b/sdkgo/doc.go @@ -0,0 +1,5 @@ +// Package sdk 定义 AirGate 插件作者直接使用的 Go SDK。 +// +// 本包只放稳定插件契约、共享类型、capability 辅助和日志辅助。 +// gRPC、go-plugin、broker、protobuf 转换等运行时细节属于 runtimego/grpc。 +package sdk diff --git a/errors.go b/sdkgo/errors.go similarity index 88% rename from errors.go rename to sdkgo/errors.go index 20a4a3c..dece12b 100644 --- a/errors.go +++ b/sdkgo/errors.go @@ -2,7 +2,7 @@ package sdk import "errors" -// ErrNotSupported 插件不支持某项可选能力时返回(如 QueryQuota / HandleWebSocket)。 +// ErrNotSupported 插件不支持某项可选能力时返回(如 HandleWebSocket)。 var ErrNotSupported = errors.New("not supported") // ErrInvalidCredentials ValidateAccount 判定凭证格式/语义不合法时返回。 diff --git a/errors_test.go b/sdkgo/errors_test.go similarity index 97% rename from errors_test.go rename to sdkgo/errors_test.go index 7cdc0bb..68d05ac 100644 --- a/errors_test.go +++ b/sdkgo/errors_test.go @@ -5,7 +5,7 @@ import ( "fmt" "testing" - sdk "github.com/DouDOU-start/airgate-sdk" + sdk "github.com/DouDOU-start/airgate-sdk/sdkgo" ) func TestSentinelErrorsAreDistinct(t *testing.T) { diff --git a/sdkgo/event.go b/sdkgo/event.go new file mode 100644 index 0000000..d50028f --- /dev/null +++ b/sdkgo/event.go @@ -0,0 +1,38 @@ +package sdk + +import ( + "context" + "time" +) + +// EventSubscription 声明插件希望接收的 Core 事件。 +// +// Type 支持精确事件名,也可由 Core 约定支持通配符,例如 "account.*"。 +// Filter 是弱契约过滤条件,只用于事件分发提示;安全过滤仍由 Core 负责。 +type EventSubscription struct { + Type string `json:"type"` + Source string `json:"source,omitempty"` + Filter map[string]string `json:"filter,omitempty"` + Metadata map[string]string `json:"metadata,omitempty"` +} + +// PluginEvent 是 Core 推送给插件的标准事件结构。 +// +// Payload 是事件类型相关的 JSON 对象;稳定事件应在插件 schema 中声明 payload schema。 +type PluginEvent struct { + ID string `json:"id,omitempty"` + Type string `json:"type"` + Source string `json:"source,omitempty"` + Subject string `json:"subject,omitempty"` + UserID int64 `json:"user_id,omitempty"` + GroupID int64 `json:"group_id,omitempty"` + Payload map[string]interface{} `json:"payload,omitempty"` + Metadata map[string]string `json:"metadata,omitempty"` + OccurredAt time.Time `json:"occurred_at,omitempty"` +} + +// EventHandler 可选接口:插件实现后即可订阅并处理 Core 推送的事件。 +type EventHandler interface { + EventSubscriptions() []EventSubscription + HandleEvent(ctx context.Context, event PluginEvent) error +} diff --git a/extension.go b/sdkgo/extension.go similarity index 100% rename from extension.go rename to sdkgo/extension.go diff --git a/gateway.go b/sdkgo/gateway.go similarity index 86% rename from gateway.go rename to sdkgo/gateway.go index 0acde94..3f08f19 100644 --- a/gateway.go +++ b/sdkgo/gateway.go @@ -31,8 +31,9 @@ type WebSocketConnectInfo struct { // GatewayPlugin 网关插件接口。 // -// Core 负责:账号调度、并发/RPM 限流、计费、failover。 -// 插件负责:声明模型/路由、转发请求、验证凭证、查询额度。 +// Core 负责:账号调度、并发/RPM 限流、结算记录、failover。 +// 插件负责:声明模型/路由、转发请求、验证凭证;账号管理和使用记录页面通过 +// 插件前端静态资源与插件私有 API 承接。 // // Forward 的返回契约: // - ForwardOutcome 表达业务判决(成功 / 客户端错 / 账号限流 / 账号死 / 上游抖动 / 流中断) @@ -49,7 +50,6 @@ type GatewayPlugin interface { Forward(ctx context.Context, req *ForwardRequest) (ForwardOutcome, error) ValidateAccount(ctx context.Context, credentials map[string]string) error - QueryQuota(ctx context.Context, credentials map[string]string) (*QuotaInfo, error) // HandleWebSocket 处理 WebSocket 双向通信。连接结束后返回 ForwardOutcome。 // 不支持时返回 ErrNotSupported。 @@ -65,6 +65,6 @@ type ForwardRequest struct { Stream bool // Writer 流式响应写入目标。 - // TODO(PR3): 替换为 StreamSink 抽象,让 Core 能在首字节落地前决定是否 failover。 + // Core 负责把写入内容转发给用户,并在调用结束后根据 ForwardOutcome 写记录和更新账号状态。 Writer http.ResponseWriter } diff --git a/sdkgo/host.go b/sdkgo/host.go new file mode 100644 index 0000000..f17180e --- /dev/null +++ b/sdkgo/host.go @@ -0,0 +1,91 @@ +package sdk + +import ( + "context" +) + +// Host Core 暴露给插件的反向调用接口(plugin → core)。 +// +// 通过 hashicorp/go-plugin 的 GRPCBroker 架起子进程隧道,插件无需 admin HTTP / Bearer 鉴权。 +// SDK 只定义通用调用通道,不定义 Core 方法清单;具体 method、入参和出参由 Core 的 +// 方法注册表和插件 schema 共同约定,并由 Core 按 capability 强制授权。 +// +// 在插件 Init 里通过 HostAware 拿到: +// +// func (p *MyPlugin) Init(ctx sdk.PluginContext) error { +// if h, ok := ctx.(sdk.HostAware); ok { +// p.host = h.Host() // 可能为 nil +// } +// return nil +// } +type Host interface { + // Invoke 调用 Core 开放的方法。 + // Method 使用点分命名,例如 "scheduler.select_account"、"tasks.update"。 + // Payload 是 JSON 对象语义,运行时会通过 protobuf bytes 传输。 + Invoke(ctx context.Context, req HostInvokeRequest) (*HostInvokeResponse, error) + + // InvokeStream 调用 Core 开放的流式方法。 + // 首帧由 SDK 根据 req 自动发送;后续由返回的 HostStream 发送和接收。 + InvokeStream(ctx context.Context, req HostStreamRequest) (HostStream, error) +} + +// HostAware 可选接口:PluginContext 实现它就能暴露 Host。 +type HostAware interface { + // Host 返回 Host 客户端;可能为 nil(Core 版本不支持 / 未启用)。 + Host() Host +} + +// HostInvokeRequest 是插件调用 Core 方法的通用请求。 +type HostInvokeRequest struct { + Method string + // Payload 是方法入参。SDK 不解释字段含义,Core method 自己校验 schema。 + Payload map[string]interface{} + // IdempotencyKey 用于创建任务、下单等副作用方法的幂等控制;只读方法可留空。 + IdempotencyKey string + // Metadata 是调用级辅助信息,不应用于替代权限、调度或核心业务字段。 + Metadata map[string]string +} + +// HostInvokeResponse 是 Core 方法调用的通用响应。 +type HostInvokeResponse struct { + // Status 是方法自己的业务状态;传输错误、鉴权错误和 schema 错误应通过 error 返回。 + Status string + // Payload 是方法出参。SDK 不解释字段含义。 + Payload map[string]interface{} + // Metadata 是调用级辅助信息。 + Metadata map[string]string +} + +// HostStreamRequest 是插件调用 Core 流式方法的起始请求。 +type HostStreamRequest struct { + Method string + // Payload 是首帧入参。SDK 不解释字段含义,Core method 自己校验 schema。 + Payload map[string]interface{} + // IdempotencyKey 用于副作用类流式方法的幂等控制;只读方法可留空。 + IdempotencyKey string + // Metadata 是调用级辅助信息,不应用于替代权限、调度或核心业务字段。 + Metadata map[string]string +} + +// HostStream 是插件与 Core 之间的双向流。 +// +// Recv 在流结束时返回 io.EOF。调用方不再发送数据时应调用 CloseSend。 +type HostStream interface { + Send(frame HostStreamFrame) error + Recv() (*HostStreamFrame, error) + CloseSend() error +} + +// HostStreamFrame 是 InvokeStream 的单帧数据。 +type HostStreamFrame struct { + // Event 是 method 内部约定的帧类型,例如 "chunk"、"progress"、"error"、"result"。 + Event string + // Status 是 method 自己的业务状态,通常只在最终帧使用。 + Status string + // Payload 是当前帧的 JSON 对象语义数据。 + Payload map[string]interface{} + // Metadata 是帧级辅助信息。 + Metadata map[string]string + // Done 表示这是当前流的最终业务帧;传输层结束仍以 io.EOF 为准。 + Done bool +} diff --git a/log.go b/sdkgo/log.go similarity index 100% rename from log.go rename to sdkgo/log.go diff --git a/log_pretty.go b/sdkgo/log_pretty.go similarity index 100% rename from log_pretty.go rename to sdkgo/log_pretty.go diff --git a/middleware.go b/sdkgo/middleware.go similarity index 75% rename from middleware.go rename to sdkgo/middleware.go index 909abca..370de54 100644 --- a/middleware.go +++ b/sdkgo/middleware.go @@ -7,7 +7,7 @@ import ( ) // DefaultMiddlewareDeadline / DefaultMiddlewareChainBudget 单 hook / 整条链的默认超时预算。 -// Core 侧按此兜底,middleware 超时不得 block 主流程,只会被跳过并 log warn。 +// Core 侧按此控制超时,middleware 超时不得 block 主流程,只会被跳过并 log warn。 const ( DefaultMiddlewareDeadline = 200 * time.Millisecond DefaultMiddlewareChainBudget = 500 * time.Millisecond @@ -41,14 +41,14 @@ const ( // MiddlewareRequest OnForwardBegin 的入参。 type MiddlewareRequest struct { - RequestID string - UserID int64 - GroupID int64 - AccountID int64 - Platform string - Model string - Stream bool - InputTokensEst int64 + RequestID string + UserID int64 + GroupID int64 + AccountID int64 + Platform string + Model string + Stream bool + Estimates []UsageMetric // Metadata 贯穿 Begin/End 的 KV bag,多个 middleware 之间共享。 Metadata map[string]string @@ -60,27 +60,20 @@ type MiddlewareRequest struct { // MiddlewareEvent OnForwardEnd 的入参。 type MiddlewareEvent struct { - RequestID string - UserID int64 - GroupID int64 - AccountID int64 - Platform string - Model string - Stream bool - InputTokensEst int64 - - StatusCode int32 - Duration time.Duration - InputTokens int64 - OutputTokens int64 - CachedInputTokens int64 - FirstTokenMs int64 - ErrorKind string // "" / "upstream_5xx" / "timeout" / "no_account" / ... - ErrorMsg string - - InputCost float64 - OutputCost float64 - CachedInputCost float64 + RequestID string + UserID int64 + GroupID int64 + AccountID int64 + Platform string + Model string + Stream bool + Estimates []UsageMetric + + StatusCode int32 + Duration time.Duration + Usage *Usage + ErrorKind string // "" / "upstream_5xx" / "timeout" / "no_account" / ... + ErrorMsg string Metadata map[string]string diff --git a/sdkgo/models.go b/sdkgo/models.go new file mode 100644 index 0000000..f779581 --- /dev/null +++ b/sdkgo/models.go @@ -0,0 +1,117 @@ +package sdk + +import "net/http" + +// 标准模型能力常量。网关插件在声明 Models() 时应为每个模型填充 Capabilities 字段。 +// Playground / Core 按此分类展示和过滤。新增能力类型在此追加即可,无需改 proto。 +const ( + ModelCapChat = "chat" // 文本对话 + ModelCapReasoning = "reasoning" // 推理 / 思维链 + ModelCapImageGeneration = "image_generation" // 图像生成 + ModelCapImageEdit = "image_edit" // 图像编辑 + ModelCapVideoGeneration = "video_generation" // 视频生成 + ModelCapAudioGeneration = "audio_generation" // 音频/音乐生成 + ModelCapTTS = "tts" // 文本转语音 + ModelCapSTT = "stt" // 语音转文字 + ModelCapCodeExecution = "code_execution" // 代码执行 + ModelCapEmbedding = "embedding" // 向量嵌入 +) + +// Account 上游账户(Core 调度后传给插件的最小视图)。 +type Account struct { + ID int64 `json:"id"` + Name string `json:"name"` + Platform string `json:"platform"` + Type string `json:"type"` // 对应 AccountType.Key(apikey / oauth / ...) + Credentials map[string]string `json:"credentials"` // JSONB 透传,结构由 Type 决定 + ProxyURL string `json:"proxy_url"` +} + +// ModelInfo 插件声明的模型信息。 +// +// Core 可用这些字段做展示、基础选择和能力过滤;具体价格、倍率、套餐和用量算法 +// 由网关插件在 Forward 中计算后写入 Usage。 +type ModelInfo struct { + ID string `json:"id"` + Name string `json:"name"` + ContextWindow int `json:"context_window"` + MaxOutputTokens int `json:"max_output_tokens"` + Capabilities []string `json:"capabilities,omitempty"` + + // Metadata 保存展示、分类、供应商标签等非核心扩展信息。 + // Core 不应依赖这里做调度、计费或权限判断。 + Metadata map[string]string `json:"metadata,omitempty"` +} + +// HasCapability 检查模型是否具备指定能力。 +func (m *ModelInfo) HasCapability(cap string) bool { + for _, c := range m.Capabilities { + if c == cap { + return true + } + } + return false +} + +// RouteDefinition 网关插件声明的 API 端点。 +type RouteDefinition struct { + Method string `json:"method"` + Path string `json:"path"` + Description string `json:"description"` + // Metadata 保存路由展示、分类、文档链接等非核心扩展信息。 + Metadata map[string]string `json:"metadata,omitempty"` +} + +// RouteRegistrar 扩展插件使用的路由注册器。 +type RouteRegistrar interface { + Handle(method, path string, handler http.HandlerFunc) + Group(prefix string) RouteRegistrar +} + +// CredentialField 凭证字段声明。 +type CredentialField struct { + Key string `json:"key"` + Label string `json:"label"` + Type string `json:"type"` // text / password / textarea / select + Required bool `json:"required"` + Placeholder string `json:"placeholder"` + EditDisabled bool `json:"edit_disabled,omitempty"` +} + +// AccountType 账号类型声明。 +type AccountType struct { + Key string `json:"key"` + Label string `json:"label"` + Description string `json:"description"` + Fields []CredentialField `json:"fields"` +} + +// FrontendPage 前端独立页面声明。 +type FrontendPage struct { + Path string `json:"path"` + Title string `json:"title"` + Icon string `json:"icon"` + Description string `json:"description"` + // Audience 决定页面可见范围: + // "admin" / "" 仅管理员(默认) + // "user" 仅普通登录用户 + // "all" 所有登录用户 + Audience string `json:"audience,omitempty"` +} + +// 前端组件插槽。 +const ( + SlotAccountIdentity = "account-identity" + SlotAccountCreate = "account-create" + SlotAccountEdit = "account-edit" + SlotAccountUsageWindow = "account-usage-window" + SlotUsageMetricDetail = "usage-metric-detail" + SlotUsageCostDetail = "usage-cost-detail" +) + +// FrontendWidget 前端组件嵌入声明。 +type FrontendWidget struct { + Slot string `json:"slot"` + EntryFile string `json:"entry_file"` + Title string `json:"title"` +} diff --git a/models_test.go b/sdkgo/models_test.go similarity index 58% rename from models_test.go rename to sdkgo/models_test.go index 6615e02..c9f79c1 100644 --- a/models_test.go +++ b/sdkgo/models_test.go @@ -4,7 +4,7 @@ import ( "encoding/json" "testing" - sdk "github.com/DouDOU-start/airgate-sdk" + sdk "github.com/DouDOU-start/airgate-sdk/sdkgo" ) func TestConfigFieldJSONRoundTrip(t *testing.T) { @@ -35,8 +35,8 @@ func TestConfigFieldJSONRoundTrip(t *testing.T) { func TestConfigFieldJSONTags(t *testing.T) { cf := sdk.ConfigField{ - Key: "db_dsn", - Label: "Database DSN", + Key: "plugin_dsn", + Label: "插件数据库 DSN", Type: "password", Required: true, // Default, Description, Placeholder left empty (omitempty) @@ -164,113 +164,3 @@ func TestCredentialFieldJSONKeys(t *testing.T) { } } } - -func TestQuotaInfoExtraMapNil(t *testing.T) { - qi := sdk.QuotaInfo{ - Total: 100.0, - Used: 25.5, - Remaining: 74.5, - Currency: "USD", - } - - if qi.Extra != nil { - t.Errorf("Extra should be nil when not initialized, got %v", qi.Extra) - } - - // JSON round-trip with nil Extra - data, err := json.Marshal(qi) - if err != nil { - t.Fatalf("Marshal: %v", err) - } - - var got sdk.QuotaInfo - if err := json.Unmarshal(data, &got); err != nil { - t.Fatalf("Unmarshal: %v", err) - } - - if got.Total != qi.Total { - t.Errorf("Total = %v, want %v", got.Total, qi.Total) - } - if got.Currency != qi.Currency { - t.Errorf("Currency = %q, want %q", got.Currency, qi.Currency) - } -} - -func TestQuotaInfoExtraMapPopulated(t *testing.T) { - qi := sdk.QuotaInfo{ - Total: 1000.0, - Used: 200.0, - Remaining: 800.0, - Currency: "CNY", - ExpiresAt: "2026-12-31T23:59:59Z", - Extra: map[string]string{ - "plan": "enterprise", - "region": "us-east-1", - "tier": "premium", - }, - } - - data, err := json.Marshal(qi) - if err != nil { - t.Fatalf("Marshal: %v", err) - } - - var got sdk.QuotaInfo - if err := json.Unmarshal(data, &got); err != nil { - t.Fatalf("Unmarshal: %v", err) - } - - if len(got.Extra) != 3 { - t.Fatalf("Extra length = %d, want 3", len(got.Extra)) - } - - for k, want := range qi.Extra { - if gotVal, ok := got.Extra[k]; !ok { - t.Errorf("Extra missing key %q", k) - } else if gotVal != want { - t.Errorf("Extra[%q] = %q, want %q", k, gotVal, want) - } - } -} - -func TestQuotaInfoExtraMapMutation(t *testing.T) { - qi := sdk.QuotaInfo{ - Extra: make(map[string]string), - } - - qi.Extra["key1"] = "val1" - qi.Extra["key2"] = "val2" - - if len(qi.Extra) != 2 { - t.Fatalf("Extra length = %d, want 2", len(qi.Extra)) - } - - delete(qi.Extra, "key1") - if len(qi.Extra) != 1 { - t.Fatalf("Extra length after delete = %d, want 1", len(qi.Extra)) - } - - if _, ok := qi.Extra["key1"]; ok { - t.Error("key1 should have been deleted") - } - if v := qi.Extra["key2"]; v != "val2" { - t.Errorf("Extra[key2] = %q, want %q", v, "val2") - } -} - -func TestQuotaInfoJSONWithNullExtra(t *testing.T) { - // Simulate JSON with explicit null for extra - raw := `{"total":50,"used":10,"remaining":40,"currency":"EUR","expires_at":"","extra":null}` - - var qi sdk.QuotaInfo - if err := json.Unmarshal([]byte(raw), &qi); err != nil { - t.Fatalf("Unmarshal: %v", err) - } - - if qi.Total != 50 { - t.Errorf("Total = %v, want 50", qi.Total) - } - if qi.Extra != nil { - t.Errorf("Extra should be nil for JSON null, got %v", qi.Extra) - } -} diff --git a/outcome.go b/sdkgo/outcome.go similarity index 71% rename from outcome.go rename to sdkgo/outcome.go index 8c83034..f86e619 100644 --- a/outcome.go +++ b/sdkgo/outcome.go @@ -19,7 +19,7 @@ const ( // OutcomeSuccess 上游返回 2xx,Usage 必填。 OutcomeSuccess - // OutcomeClientError 4xx,错在客户端请求本身(model 不存在、context 过长、参数非法)。 + // OutcomeClientError 4xx,错在客户端请求本身(context 过长、参数非法等)。 // 换账号救不回来,Core 会把 Upstream 原样透传给客户端,不罚账号。 OutcomeClientError @@ -38,6 +38,10 @@ const ( // OutcomeStreamAborted 流式响应已经开始写入客户端,中途断开。 // 不能 failover(字节已经发出去了),也不能把账号直接标死。 OutcomeStreamAborted + + // Deprecated: OutcomeAccountModelUnsupported 已归入 OutcomeClientError。 + // 保留常量避免编译失败,运行时等同于 ClientError。 + OutcomeAccountModelUnsupported ) // String 返回人类可读名称,用于日志。 @@ -55,6 +59,8 @@ func (k OutcomeKind) String() string { return "upstream_transient" case OutcomeStreamAborted: return "stream_aborted" + case OutcomeAccountModelUnsupported: + return "client_error" default: return "unknown" } @@ -82,8 +88,8 @@ func (k OutcomeKind) ShouldFailover() bool { // UpstreamResponse 上游返回的原始 HTTP 快照。 // -// 语义:Success / ClientError 时 Core 会把 Body + Headers 原样透传给客户端。 -// 其他 Kind 下 Upstream 仅作为诊断信息保留,不透传。 +// 语义:插件应尽量保存上游实际响应。Core 会先基于 Kind 组织调度 / failover; +// 最终不再重试时,若 Upstream 有可返回响应,则优先原样返回给客户端。 // StreamAborted 场景 Body 通常为空(字节已经流给客户端)。 type UpstreamResponse struct { StatusCode int @@ -91,41 +97,26 @@ type UpstreamResponse struct { Body []byte } -// Usage 单次调用的 token / 费用统计。 +// Usage 是插件计算后的单次调用用量与费用结果。 // // 只有 OutcomeSuccess 下 Usage 必填;OutcomeClientError 如果上游也计费(如部分重置 context // 后仍计 token)可填;其他 Kind 下应为 nil。 // -// 费用字段(*Cost)由插件根据单价 × token 计算后传回,Core 不再关心模型定价。 -// 单价字段(*Price)纯粹透传存储,便于 usage_log 审计。 +// 平台价格、token 拆分、图片分档等标准计费规则全部由网关插件自己实现。 +// 插件填 AccountCost / Currency;Core 统一入库后按用户、分组、模型等倍率 +// 写入 UserCost / BillingMultiplier。 type Usage struct { - InputTokens int - OutputTokens int - CachedInputTokens int - CacheCreationTokens int - CacheCreation5mTokens int - CacheCreation1hTokens int - ReasoningOutputTokens int - - InputCost float64 - OutputCost float64 - CachedInputCost float64 - CacheCreationCost float64 - - InputPrice float64 - OutputPrice float64 - CachedInputPrice float64 - CacheCreationPrice float64 - CacheCreation1hPrice float64 - - Model string - ServiceTier string - FirstTokenMs int64 - - // ImageSize 图像生成请求的实际出图尺寸("WxH",例如 "1024x1024"、"3840x2160")。 - // 网关侧按 1K/2K/4K 三档计费,把分档来源(实际尺寸)记下来,admin 后台 usage_log - // 显示费用时旁边带上 size,用户能直观看出"为什么这次扣了 0.40"。非图像请求留空。 - ImageSize string + Model string `json:"model,omitempty"` + AccountCost float64 `json:"account_cost,omitempty"` + UserCost float64 `json:"user_cost,omitempty"` + BillingMultiplier float64 `json:"billing_multiplier,omitempty"` + Currency string `json:"currency,omitempty"` + Summary string `json:"summary,omitempty"` + FirstTokenMs int64 `json:"first_token_ms,omitempty"` + Attributes []UsageAttribute `json:"attributes,omitempty"` + Metrics []UsageMetric `json:"metrics,omitempty"` + CostDetails []UsageCostDetail `json:"cost_details,omitempty"` + Metadata map[string]string `json:"metadata,omitempty"` } // ForwardOutcome 是插件对一次 Forward 的完整判决结果。 diff --git a/outcome_test.go b/sdkgo/outcome_test.go similarity index 84% rename from outcome_test.go rename to sdkgo/outcome_test.go index 37abb44..e0317a2 100644 --- a/outcome_test.go +++ b/sdkgo/outcome_test.go @@ -3,7 +3,7 @@ package sdk_test import ( "testing" - sdk "github.com/DouDOU-start/airgate-sdk" + sdk "github.com/DouDOU-start/airgate-sdk/sdkgo" ) func TestOutcomeKind_String(t *testing.T) { @@ -18,6 +18,7 @@ func TestOutcomeKind_String(t *testing.T) { {sdk.OutcomeAccountDead, "account_dead"}, {sdk.OutcomeUpstreamTransient, "upstream_transient"}, {sdk.OutcomeStreamAborted, "stream_aborted"}, + {sdk.OutcomeAccountModelUnsupported, "client_error"}, {sdk.OutcomeKind(99), "unknown"}, // 非枚举值回退到 unknown } for _, tc := range cases { @@ -38,6 +39,7 @@ func TestOutcomeKind_IsSuccess(t *testing.T) { sdk.OutcomeAccountDead, sdk.OutcomeUpstreamTransient, sdk.OutcomeStreamAborted, + sdk.OutcomeAccountModelUnsupported, } for _, k := range nonSuccess { if k.IsSuccess() { @@ -62,6 +64,7 @@ func TestOutcomeKind_IsAccountFault(t *testing.T) { sdk.OutcomeClientError, sdk.OutcomeUpstreamTransient, sdk.OutcomeStreamAborted, + sdk.OutcomeAccountModelUnsupported, } for _, k := range notAccountFaults { if k.IsAccountFault() { @@ -82,10 +85,11 @@ func TestOutcomeKind_ShouldFailover(t *testing.T) { } } noFailover := []sdk.OutcomeKind{ - sdk.OutcomeUnknown, // 插件没说的情况下不盲目重试 - sdk.OutcomeSuccess, // 成功无需 failover - sdk.OutcomeClientError, // 换号也救不回来 - sdk.OutcomeStreamAborted, // 字节已发给客户端 + sdk.OutcomeUnknown, // 插件没说的情况下不盲目重试 + sdk.OutcomeSuccess, // 成功无需 failover + sdk.OutcomeClientError, // 换号也救不回来 + sdk.OutcomeStreamAborted, // 字节已发给客户端 + sdk.OutcomeAccountModelUnsupported, // 已归入 ClientError } for _, k := range noFailover { if k.ShouldFailover() { diff --git a/plugin.go b/sdkgo/plugin.go similarity index 88% rename from plugin.go rename to sdkgo/plugin.go index cd00452..838c617 100644 --- a/plugin.go +++ b/sdkgo/plugin.go @@ -29,8 +29,7 @@ const ( ) // SDKVersion 当前 SDK 版本,插件编译时嵌入到 PluginInfo。 -// 0.3.0 起强制 Capability 声明:未声明 capability 的插件调用 HostService 会被拒绝。 -const SDKVersion = "0.3.0" +const SDKVersion = "1.0.0" // PluginInfo 插件元信息。 type PluginInfo struct { @@ -48,6 +47,9 @@ type PluginInfo struct { FrontendWidgets []FrontendWidget `json:"frontend_widgets"` InstructionPresets []string `json:"instruction_presets"` Capabilities []Capability `json:"capabilities"` + // Metadata 保存插件声明层面的非核心扩展信息。 + // 只放展示、分类、市场索引等弱契约字段;需要 Core 授权或参与调度的字段必须进入显式 SDK 契约。 + Metadata map[string]string `json:"metadata,omitempty"` // Priority 仅对 type=middleware 生效:Begin 升序、End 降序(LIFO)。默认 100。 Priority int32 `json:"priority"` } @@ -64,8 +66,7 @@ type ConfigField struct { } // PluginContext Core 注入给插件的最小上下文:Logger + Config。 -// 其它能力(Host 反向调用、插件专属 DB DSN 等)通过可选接口(HostAware / PluginDSNAware)暴露, -// 避免给 PluginContext 加方法造成 breaking change。 +// 其它能力(Host 反向调用、插件专属 DB DSN 等)通过可选接口暴露。 type PluginContext interface { Logger() *slog.Logger Config() PluginConfig @@ -82,7 +83,7 @@ type PluginDSNAware interface { PluginDSN() string } -// GetPluginDSN PluginDSNAware 的便利访问器:ctx 实现了接口就返回 DSN,否则回退读 Config。 +// GetPluginDSN PluginDSNAware 的便利访问器。 func GetPluginDSN(ctx PluginContext) string { if ctx == nil { return "" @@ -90,9 +91,6 @@ func GetPluginDSN(ctx PluginContext) string { if d, ok := ctx.(PluginDSNAware); ok { return d.PluginDSN() } - if cfg := ctx.Config(); cfg != nil { - return cfg.GetString(PluginDSNConfigKey) - } return "" } @@ -111,7 +109,7 @@ type WebAssetsProvider interface { GetWebAssets() map[string][]byte } -// RequestHandler 可选:Core 将 /api/v1/admin/plugins/:name/rpc/* 透传给插件自行路由。 +// RequestHandler 可选:Core 将插件私有 API 请求透传给插件自行路由。 type RequestHandler interface { HandleRequest(ctx context.Context, method, path, query string, headers http.Header, body []byte) (statusCode int, respHeaders http.Header, respBody []byte, err error) } diff --git a/plugin_test.go b/sdkgo/plugin_test.go similarity index 90% rename from plugin_test.go rename to sdkgo/plugin_test.go index 91a48e5..4481cd7 100644 --- a/plugin_test.go +++ b/sdkgo/plugin_test.go @@ -5,7 +5,7 @@ import ( "strings" "testing" - sdk "github.com/DouDOU-start/airgate-sdk" + sdk "github.com/DouDOU-start/airgate-sdk/sdkgo" ) func TestSDKVersion(t *testing.T) { @@ -59,11 +59,14 @@ func TestPluginInfoJSON(t *testing.T) { }, FrontendWidgets: []sdk.FrontendWidget{ { - Slot: "account-form", + Slot: sdk.SlotAccountCreate, EntryFile: "form.js", Title: "Account Form", }, }, + Metadata: map[string]string{ + "category": "gateway", + }, } data, err := json.Marshal(info) @@ -92,6 +95,9 @@ func TestPluginInfoJSON(t *testing.T) { if decoded.Type != info.Type { t.Errorf("Type = %q, want %q", decoded.Type, info.Type) } + if decoded.Metadata["category"] != "gateway" { + t.Errorf("Metadata[category] = %q, want gateway", decoded.Metadata["category"]) + } // Verify Dependencies if len(decoded.Dependencies) != 2 { @@ -118,7 +124,7 @@ func TestPluginInfoJSON(t *testing.T) { // Verify JSON keys exist in raw output raw := string(data) - for _, key := range []string{`"sdk_version"`, `"dependencies"`, `"config_schema"`} { + for _, key := range []string{`"sdk_version"`, `"dependencies"`, `"config_schema"`, `"metadata"`} { if !strings.Contains(raw, key) { t.Errorf("JSON output missing key %s", key) } diff --git a/sdkgo/schema.go b/sdkgo/schema.go new file mode 100644 index 0000000..da575d5 --- /dev/null +++ b/sdkgo/schema.go @@ -0,0 +1,77 @@ +package sdk + +// PayloadSchema 描述一个 JSON payload 的结构。 +// +// Schema 通常是 JSON Schema 字符串;Example 是 JSON 示例字符串。 +type PayloadSchema struct { + ContentType string `json:"content_type,omitempty"` + Schema string `json:"schema,omitempty"` + Example string `json:"example,omitempty"` + Metadata map[string]string `json:"metadata,omitempty"` +} + +// RouteSchema 描述插件公开的自定义 HTTP API。 +type RouteSchema struct { + Method string `json:"method"` + Path string `json:"path"` + Summary string `json:"summary,omitempty"` + Request PayloadSchema `json:"request,omitempty"` + Response PayloadSchema `json:"response,omitempty"` + Metadata map[string]string `json:"metadata,omitempty"` +} + +// TaskSchema 描述插件支持的异步任务类型。 +type TaskSchema struct { + Type string `json:"type"` + Summary string `json:"summary,omitempty"` + Input PayloadSchema `json:"input,omitempty"` + Output PayloadSchema `json:"output,omitempty"` + Metadata map[string]string `json:"metadata,omitempty"` +} + +// EventSchema 描述插件订阅或发布的事件类型。 +type EventSchema struct { + Type string `json:"type"` + Source string `json:"source,omitempty"` + Summary string `json:"summary,omitempty"` + Payload PayloadSchema `json:"payload,omitempty"` + Metadata map[string]string `json:"metadata,omitempty"` +} + +// InvokeTransport 描述 Host.Invoke method 的传输模式。 +type InvokeTransport string + +const ( + InvokeTransportUnary InvokeTransport = "unary" + InvokeTransportServerStream InvokeTransport = "server_stream" + InvokeTransportClientStream InvokeTransport = "client_stream" + InvokeTransportBidirectionalStream InvokeTransport = "bidirectional_stream" +) + +// InvokeSchema 描述通过 Host.Invoke 或 Host.InvokeStream 调用的扩展动作。 +type InvokeSchema struct { + Method string `json:"method"` + Summary string `json:"summary,omitempty"` + // Transport 为空时等价于 unary。 + Transport InvokeTransport `json:"transport,omitempty"` + Request PayloadSchema `json:"request,omitempty"` + Response PayloadSchema `json:"response,omitempty"` + // ClientFrame / ServerFrame 只用于 InvokeStream,描述双方 frame payload。 + ClientFrame PayloadSchema `json:"client_frame,omitempty"` + ServerFrame PayloadSchema `json:"server_frame,omitempty"` + Metadata map[string]string `json:"metadata,omitempty"` +} + +// PluginSchema 是插件的能力发现清单,用于 Core、管理后台和开发工具了解插件可用能力。 +type PluginSchema struct { + Routes []RouteSchema `json:"routes,omitempty"` + Tasks []TaskSchema `json:"tasks,omitempty"` + Events []EventSchema `json:"events,omitempty"` + Invokes []InvokeSchema `json:"invokes,omitempty"` + Metadata map[string]string `json:"metadata,omitempty"` +} + +// SchemaProvider 可选接口:插件实现后可向 Core 暴露结构化能力清单。 +type SchemaProvider interface { + Schema() PluginSchema +} diff --git a/sdkgo/task.go b/sdkgo/task.go new file mode 100644 index 0000000..d44d5db --- /dev/null +++ b/sdkgo/task.go @@ -0,0 +1,74 @@ +package sdk + +import ( + "context" + "time" +) + +type TaskStatus string + +const ( + TaskStatusPending TaskStatus = "pending" + TaskStatusProcessing TaskStatus = "processing" + TaskStatusCompleted TaskStatus = "completed" + TaskStatusFailed TaskStatus = "failed" + TaskStatusCancelled TaskStatus = "cancelled" +) + +func (s TaskStatus) String() string { return string(s) } + +// TaskProcessor 可选接口:扩展插件实现它来处理 Core 分发的异步任务。 +// +// Core 在后台轮询 pending 任务,按 plugin_id 找到对应插件调用 ProcessTask。 +// 插件内部如需回写进度,应通过 Host.Invoke 调用 Core 开放的任务方法。 +// +// 使用方式: +// +// func (p *MyPlugin) ProcessTask(ctx context.Context, task sdk.HostTask) error { +// _, _ = p.host.Invoke(ctx, sdk.HostInvokeRequest{ +// Method: "tasks.update", +// Payload: map[string]interface{}{ +// "task_id": task.ID, +// "status": sdk.TaskStatusProcessing.String(), +// "progress": 10, +// }, +// }) +// // ... 执行任务逻辑 ... +// _, _ = p.host.Invoke(ctx, sdk.HostInvokeRequest{ +// Method: "tasks.update", +// Payload: map[string]interface{}{ +// "task_id": task.ID, +// "status": sdk.TaskStatusCompleted.String(), +// "output": result, +// }, +// }) +// return nil +// } +// +// func (p *MyPlugin) TaskTypes() []string { return []string{"image_generation"} } +type TaskProcessor interface { + // ProcessTask 处理一个异步任务。Context 带有超时。 + ProcessTask(ctx context.Context, task HostTask) error + + // TaskTypes 返回此插件能处理的任务类型列表。 + TaskTypes() []string +} + +// HostTask 任务完整信息。 +type HostTask struct { + ID int64 + PluginID string + TaskType string + Status TaskStatus // pending, processing, completed, failed, cancelled + UserID int64 + Input map[string]interface{} + Output map[string]interface{} + ErrorMessage string + Progress int // 0-100 + Attempts int + MaxAttempts int + CreatedAt time.Time + UpdatedAt time.Time + StartedAt *time.Time + CompletedAt *time.Time +} diff --git a/sdkgo/usage.go b/sdkgo/usage.go new file mode 100644 index 0000000..5c176c3 --- /dev/null +++ b/sdkgo/usage.go @@ -0,0 +1,42 @@ +package sdk + +// UsageAttribute 是一次调用的通用审计维度。 +// +// 适合存储模型、思考层级、分辨率、质量档、服务档位等非数值或枚举型信息。 +// Core 可统一入库和检索,但不根据这些字段推导平台计费规则。 +type UsageAttribute struct { + Key string `json:"key,omitempty"` + Label string `json:"label"` + Kind string `json:"kind,omitempty"` // model / reasoning / resolution / tier / quality / custom + Value string `json:"value"` + Metadata map[string]string `json:"metadata,omitempty"` +} + +// UsageMetric 是一次调用的通用计量结果。 +// +// 插件负责根据平台标准计费规则计算 Value 和 AccountCost。SDK 不提供价格字段、倍率规则或 token +// 公式,Core 不应基于 Key 推导平台计费语义。 +type UsageMetric struct { + Key string `json:"key,omitempty"` + Label string `json:"label"` + Kind string `json:"kind,omitempty"` // token / request / image / audio / video / custom + Unit string `json:"unit,omitempty"` + Value float64 `json:"value"` + AccountCost float64 `json:"account_cost,omitempty"` + Currency string `json:"currency,omitempty"` + Metadata map[string]string `json:"metadata,omitempty"` +} + +// UsageCostDetail 是一次调用的通用费用明细。 +// +// 插件负责把平台价格、套餐规则和折扣计算成 AccountCost。Core 可统一入库, +// 再按用户、分组、模型等倍率写入 UserCost / BillingMultiplier。 +type UsageCostDetail struct { + Key string `json:"key,omitempty"` + Label string `json:"label"` + AccountCost float64 `json:"account_cost"` + UserCost float64 `json:"user_cost,omitempty"` + BillingMultiplier float64 `json:"billing_multiplier,omitempty"` + Currency string `json:"currency,omitempty"` + Metadata map[string]string `json:"metadata,omitempty"` +} diff --git a/sdkgo/usage_test.go b/sdkgo/usage_test.go new file mode 100644 index 0000000..572800c --- /dev/null +++ b/sdkgo/usage_test.go @@ -0,0 +1,73 @@ +package sdk_test + +import ( + "encoding/json" + "strings" + "testing" + + sdk "github.com/DouDOU-start/airgate-sdk/sdkgo" +) + +func TestUsageJSONRoundTrip(t *testing.T) { + usage := sdk.Usage{ + Model: "demo-model", + AccountCost: 0.042, + UserCost: 0.084, + BillingMultiplier: 2, + Currency: "USD", + Summary: "输入 10 token,输出 5 token", + FirstTokenMs: 120, + Attributes: []sdk.UsageAttribute{ + {Key: "reasoning_effort", Label: "思考层级", Kind: "reasoning", Value: "high"}, + {Key: "resolution", Label: "分辨率", Kind: "resolution", Value: "1024x1024"}, + }, + Metrics: []sdk.UsageMetric{ + { + Key: "input_tokens", + Label: "输入 token", + Kind: "token", + Unit: "token", + Value: 10, + AccountCost: 0.01, + Currency: "USD", + }, + }, + CostDetails: []sdk.UsageCostDetail{ + {Key: "input", Label: "输入费用", AccountCost: 0.01, UserCost: 0.02, BillingMultiplier: 2, Currency: "USD"}, + }, + Metadata: map[string]string{"provider": "demo"}, + } + + data, err := json.Marshal(usage) + if err != nil { + t.Fatalf("Marshal: %v", err) + } + + var got sdk.Usage + if err := json.Unmarshal(data, &got); err != nil { + t.Fatalf("Unmarshal: %v", err) + } + if got.Model != usage.Model || got.AccountCost != usage.AccountCost || got.UserCost != usage.UserCost || got.BillingMultiplier != usage.BillingMultiplier || got.Currency != usage.Currency { + t.Fatalf("Usage round-trip mismatch: got %+v, want %+v", got, usage) + } + raw := string(data) + for _, key := range []string{"account_cost", "user_cost", "billing_multiplier", "first_token_ms", "cost_details"} { + if !strings.Contains(raw, `"`+key+`"`) { + t.Fatalf("Usage JSON 缺少 snake_case key %q: %s", key, raw) + } + } + for _, key := range []string{"AccountCost", "UserCost", "BillingMultiplier", "FirstTokenMs", "CostDetails"} { + if strings.Contains(raw, `"`+key+`"`) { + t.Fatalf("Usage JSON 不应包含 PascalCase key %q: %s", key, raw) + } + } + if len(got.Metrics) != 1 || got.Metrics[0].Key != "input_tokens" { + t.Fatalf("Metrics round-trip mismatch: %+v", got.Metrics) + } + if len(got.Attributes) != 2 || got.Attributes[0].Key != "reasoning_effort" { + t.Fatalf("Attributes round-trip mismatch: %+v", got.Attributes) + } + if len(got.CostDetails) != 1 || got.CostDetails[0].Key != "input" { + t.Fatalf("CostDetails round-trip mismatch: %+v", got.CostDetails) + } +} diff --git a/theme/dist/css.d.ts b/theme/dist/css.d.ts new file mode 100644 index 0000000..b9559ee --- /dev/null +++ b/theme/dist/css.d.ts @@ -0,0 +1,77 @@ +import type { AppShellTokens, CssVarOptions, FoundationTokens, StaticTokens, TailwindBridgeOptions, ThemeCSSOptions, ThemeInjectionOptions, ThemeName, ThemeSetOptions, ThemeStorageOptions, ThemeTokens } from './types.js'; +/** 主题 token → CSS 变量名映射 */ +export declare const tokenToCssVar: Record; +/** 静态 token → CSS 变量名映射 */ +export declare const staticToCssVar: Record; +/** 生成基础 token 的 CSS 变量名映射 */ +export declare function createFoundationCssVarMap(options?: CssVarOptions): Record; +/** 生成应用壳层 token 的 CSS 变量名映射 */ +export declare function createAppShellCssVarMap(options?: CssVarOptions): Record; +/** 生成主题 token 的 CSS 变量名映射 */ +export declare function createThemeCssVarMap(options?: CssVarOptions): Record; +/** 生成静态 token 的 CSS 变量名映射 */ +export declare function createStaticCssVarMap(options?: CssVarOptions): Record; +/** + * 生成完整的 CSS 变量定义字符串。 + * 默认输出::root(静态)+ :root[data-theme="dark"] + :root[data-theme="light"] + * 也支持在局部容器下生成作用域主题。 + */ +export declare function generateThemeCSS(options?: ThemeCSSOptions): string; +/** 运行时注入主题 CSS 到 */ +export declare function injectThemeStyle(options?: ThemeInjectionOptions | string): void; +/** 设置当前主题(data-theme 属性 + localStorage) */ +export declare function setTheme(theme: ThemeName, options?: ThemeSetOptions): void; +/** 读取已保存的主题偏好,默认 dark */ +export declare function getStoredTheme(options?: ThemeStorageOptions): ThemeName; +/** 生成 Tailwind 可消费的 theme bridge */ +export declare function createTailwindThemeBridge(options?: TailwindBridgeOptions): { + readonly colors: { + readonly primary: `var(${string})`; + readonly 'primary-hover': `var(${string})`; + readonly 'primary-subtle': `var(${string})`; + readonly success: `var(${string})`; + readonly 'success-subtle': `var(${string})`; + readonly warning: `var(${string})`; + readonly 'warning-subtle': `var(${string})`; + readonly danger: `var(${string})`; + readonly 'danger-subtle': `var(${string})`; + readonly info: `var(${string})`; + readonly 'info-subtle': `var(${string})`; + readonly bg: `var(${string})`; + readonly 'bg-deep': `var(${string})`; + readonly 'bg-elevated': `var(${string})`; + readonly surface: `var(${string})`; + readonly 'bg-hover': `var(${string})`; + readonly 'bg-active': `var(${string})`; + readonly border: `var(${string})`; + readonly 'border-subtle': `var(${string})`; + readonly 'border-focus': `var(${string})`; + readonly text: `var(${string})`; + readonly 'text-secondary': `var(${string})`; + readonly 'text-tertiary': `var(${string})`; + readonly 'text-inverse': `var(${string})`; + readonly glass: `var(${string})`; + readonly 'glass-border': `var(${string})`; + }; + readonly borderRadius: { + readonly sm: `var(${string})`; + readonly md: `var(${string})`; + readonly lg: `var(${string})`; + readonly xl: `var(${string})`; + readonly field: `var(${string})`; + }; + readonly fontFamily: { + readonly sans: `var(${string})`; + readonly mono: `var(${string})`; + }; + readonly boxShadow: { + readonly sm: `var(${string})`; + readonly md: `var(${string})`; + readonly lg: `var(${string})`; + readonly glow: `var(${string})`; + }; + readonly transitionDuration: { + readonly DEFAULT: `var(${string})`; + readonly slow: `var(${string})`; + }; +}; diff --git a/theme/dist/css.js b/theme/dist/css.js new file mode 100644 index 0000000..00d0bba --- /dev/null +++ b/theme/dist/css.js @@ -0,0 +1,191 @@ +import { appShellTokens, foundationTokens, themes, staticTokens, lightElevationContexts } from './tokens.js'; +/** camelCase → kebab-case */ +function toKebab(key) { + return key.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase()); +} +function resolvePrefix(prefix = 'ag') { + return prefix.trim() || 'ag'; +} +function variableName(prefix, key) { + return `--${prefix}-${toKebab(key)}`; +} +function selectorForScope(scopeSelector = ':root', themeAttribute = 'data-theme', theme) { + if (!theme) + return scopeSelector; + if (scopeSelector === ':root') { + return `:root[${themeAttribute}="${theme}"]`; + } + return `${scopeSelector}[${themeAttribute}="${theme}"]`; +} +function varsBlock(values, prefix) { + return Object.entries(values) + .map(([key, value]) => ` ${variableName(prefix, key)}: ${value};`) + .join('\n'); +} +/** 主题 token → CSS 变量名映射 */ +export const tokenToCssVar = Object.keys(themes.dark).reduce((acc, key) => { + acc[key] = variableName('ag', key); + return acc; +}, {}); +/** 静态 token → CSS 变量名映射 */ +export const staticToCssVar = Object.keys(staticTokens).reduce((acc, key) => { + acc[key] = variableName('ag', key); + return acc; +}, {}); +/** 生成基础 token 的 CSS 变量名映射 */ +export function createFoundationCssVarMap(options = {}) { + const prefix = resolvePrefix(options.prefix); + return Object.keys(foundationTokens).reduce((acc, key) => { + acc[key] = variableName(prefix, key); + return acc; + }, {}); +} +/** 生成应用壳层 token 的 CSS 变量名映射 */ +export function createAppShellCssVarMap(options = {}) { + const prefix = resolvePrefix(options.prefix); + return Object.keys(appShellTokens).reduce((acc, key) => { + acc[key] = variableName(prefix, key); + return acc; + }, {}); +} +/** 生成主题 token 的 CSS 变量名映射 */ +export function createThemeCssVarMap(options = {}) { + const prefix = resolvePrefix(options.prefix); + return Object.keys(themes.dark).reduce((acc, key) => { + acc[key] = variableName(prefix, key); + return acc; + }, {}); +} +/** 生成静态 token 的 CSS 变量名映射 */ +export function createStaticCssVarMap(options = {}) { + const prefix = resolvePrefix(options.prefix); + return Object.keys(staticTokens).reduce((acc, key) => { + acc[key] = variableName(prefix, key); + return acc; + }, {}); +} +/** + * 生成完整的 CSS 变量定义字符串。 + * 默认输出::root(静态)+ :root[data-theme="dark"] + :root[data-theme="light"] + * 也支持在局部容器下生成作用域主题。 + */ +export function generateThemeCSS(options = {}) { + const prefix = resolvePrefix(options.prefix); + const scopeSelector = options.scopeSelector || ':root'; + const themeAttribute = options.themeAttribute || 'data-theme'; + const blocks = [ + `${selectorForScope(scopeSelector)} {\n${varsBlock(staticTokens, prefix)}\n}`, + `${selectorForScope(scopeSelector, themeAttribute, 'dark')} {\n${varsBlock(themes.dark, prefix)}\n}`, + `${selectorForScope(scopeSelector, themeAttribute, 'light')} {\n${varsBlock(themes.light, prefix)}\n}`, + ]; + // Elevation context blocks (light theme only) + const lightSelector = selectorForScope(scopeSelector, themeAttribute, 'light'); + for (const [ctx, overrides] of Object.entries(lightElevationContexts)) { + if (Object.keys(overrides).length === 0) + continue; + blocks.push(`${lightSelector} .ag-elevation-${ctx} {\n${varsBlock(overrides, prefix)}\n}`); + } + return blocks.join('\n\n'); +} +/** 运行时注入主题 CSS 到 */ +export function injectThemeStyle(options = 'ag-theme-vars') { + if (typeof document === 'undefined') + return; + const resolvedOptions = typeof options === 'string' + ? { styleId: options } + : options; + const targetDocument = resolvedOptions.targetDocument || document; + const styleId = resolvedOptions.styleId || 'ag-theme-vars'; + let el = targetDocument.getElementById(styleId); + if (!el) { + el = targetDocument.createElement('style'); + el.id = styleId; + targetDocument.head.appendChild(el); + } + el.textContent = generateThemeCSS(resolvedOptions); +} +/** 设置当前主题(data-theme 属性 + localStorage) */ +export function setTheme(theme, options = {}) { + if (typeof document === 'undefined') + return; + const themeAttribute = options.themeAttribute || 'data-theme'; + const target = options.target || document.documentElement; + target.setAttribute(themeAttribute, theme); + try { + if (typeof localStorage !== 'undefined') { + localStorage.setItem(options.storageKey || 'ag-theme', theme); + } + } + catch { + // Theme switching should keep working when storage is unavailable. + } +} +/** 读取已保存的主题偏好,默认 dark */ +export function getStoredTheme(options = {}) { + if (typeof localStorage === 'undefined') + return 'dark'; + try { + return localStorage.getItem(options.storageKey || 'ag-theme') || 'dark'; + } + catch { + return 'dark'; + } +} +/** 生成 Tailwind 可消费的 theme bridge */ +export function createTailwindThemeBridge(options = {}) { + const prefix = resolvePrefix(options.prefix); + const themeVars = createThemeCssVarMap({ prefix }); + const staticVars = createStaticCssVarMap({ prefix }); + const foundationVars = createFoundationCssVarMap({ prefix }); + return { + colors: { + primary: `var(${themeVars.primary})`, + 'primary-hover': `var(${themeVars.primaryHover})`, + 'primary-subtle': `var(${themeVars.primarySubtle})`, + success: `var(${themeVars.success})`, + 'success-subtle': `var(${themeVars.successSubtle})`, + warning: `var(${themeVars.warning})`, + 'warning-subtle': `var(${themeVars.warningSubtle})`, + danger: `var(${themeVars.danger})`, + 'danger-subtle': `var(${themeVars.dangerSubtle})`, + info: `var(${themeVars.info})`, + 'info-subtle': `var(${themeVars.infoSubtle})`, + bg: `var(${themeVars.bg})`, + 'bg-deep': `var(${themeVars.bgDeep})`, + 'bg-elevated': `var(${themeVars.bgElevated})`, + surface: `var(${themeVars.bgSurface})`, + 'bg-hover': `var(${themeVars.bgHover})`, + 'bg-active': `var(${themeVars.bgActive})`, + border: `var(${themeVars.border})`, + 'border-subtle': `var(${themeVars.borderSubtle})`, + 'border-focus': `var(${themeVars.borderFocus})`, + text: `var(${themeVars.text})`, + 'text-secondary': `var(${themeVars.textSecondary})`, + 'text-tertiary': `var(${themeVars.textTertiary})`, + 'text-inverse': `var(${themeVars.textInverse})`, + glass: `var(${themeVars.glass})`, + 'glass-border': `var(${themeVars.glassBorder})`, + }, + borderRadius: { + sm: `var(${foundationVars.radiusSm})`, + md: `var(${foundationVars.radiusMd})`, + lg: `var(${foundationVars.radiusLg})`, + xl: `var(${foundationVars.radiusXl})`, + field: `var(${foundationVars.fieldRadius})`, + }, + fontFamily: { + sans: `var(${staticVars.fontSans})`, + mono: `var(${staticVars.fontMono})`, + }, + boxShadow: { + sm: `var(${themeVars.shadowSm})`, + md: `var(${themeVars.shadowMd})`, + lg: `var(${themeVars.shadowLg})`, + glow: `var(${themeVars.shadowGlow})`, + }, + transitionDuration: { + DEFAULT: `var(${staticVars.transition})`, + slow: `var(${staticVars.transitionSlow})`, + }, + }; +} diff --git a/theme/dist/helpers.d.ts b/theme/dist/helpers.d.ts new file mode 100644 index 0000000..18984e6 --- /dev/null +++ b/theme/dist/helpers.d.ts @@ -0,0 +1,20 @@ +import type { CssVarOptions, StaticTokens, ThemeTokens } from './types.js'; +/** 所有可用 token 名称 */ +export type TokenName = keyof ThemeTokens | keyof StaticTokens; +/** + * 获取带默认值的 CSS var() 引用。 + * 同时支持主题 token 和静态 token。 + * + * @example + * cssVar('primary') // → 'var(--ag-primary, #3b82f6)' + * cssVar('bgSurface') // → 'var(--ag-bg-surface, #1c2237)' + * cssVar('fieldRadius') // → 'var(--ag-field-radius, 0.5rem)' + */ +export declare function cssVar(token: TokenName, options?: CssVarOptions): string; +/** + * 批量生成 CSSProperties 对象。 + * + * @example + * themeStyle({ color: 'text', backgroundColor: 'bgSurface', borderRadius: 'radiusMd' }) + */ +export declare function themeStyle(mapping: Partial>, options?: CssVarOptions): Record; diff --git a/theme/dist/helpers.js b/theme/dist/helpers.js new file mode 100644 index 0000000..1652351 --- /dev/null +++ b/theme/dist/helpers.js @@ -0,0 +1,37 @@ +import { darkTheme, staticTokens } from './tokens.js'; +import { createStaticCssVarMap, createThemeCssVarMap } from './css.js'; +const defaultThemeCssVarMap = createThemeCssVarMap(); +const defaultStaticCssVarMap = createStaticCssVarMap(); +/** + * 获取带默认值的 CSS var() 引用。 + * 同时支持主题 token 和静态 token。 + * + * @example + * cssVar('primary') // → 'var(--ag-primary, #3b82f6)' + * cssVar('bgSurface') // → 'var(--ag-bg-surface, #1c2237)' + * cssVar('fieldRadius') // → 'var(--ag-field-radius, 0.5rem)' + */ +export function cssVar(token, options = {}) { + const themeCssVarMap = options.prefix ? createThemeCssVarMap(options) : defaultThemeCssVarMap; + const staticCssVarMap = options.prefix ? createStaticCssVarMap(options) : defaultStaticCssVarMap; + if (token in themeCssVarMap) { + const t = token; + return `var(${themeCssVarMap[t]}, ${darkTheme[t]})`; + } + const s = token; + return `var(${staticCssVarMap[s]}, ${staticTokens[s]})`; +} +/** + * 批量生成 CSSProperties 对象。 + * + * @example + * themeStyle({ color: 'text', backgroundColor: 'bgSurface', borderRadius: 'radiusMd' }) + */ +export function themeStyle(mapping, options = {}) { + const result = {}; + for (const [cssProp, token] of Object.entries(mapping)) { + if (token) + result[cssProp] = cssVar(token, options); + } + return result; +} diff --git a/theme/dist/index.d.ts b/theme/dist/index.d.ts new file mode 100644 index 0000000..ab4d1d6 --- /dev/null +++ b/theme/dist/index.d.ts @@ -0,0 +1,7 @@ +export type { AppShellTokens, CssVarOptions, ElevationContext, FoundationTokens, StaticTokenGroups, StaticTokens, TailwindBridgeOptions, ThemeCSSOptions, ThemeInjectionOptions, ThemeName, ThemeSetOptions, ThemeStorageOptions, ThemeTokens, } from './types.js'; +export { appShellTokens, darkTheme, foundationTokens, lightElevationContexts, lightTheme, staticTokenGroups, staticTokens, decorativePalette, themes, } from './tokens.js'; +export { createAppShellCssVarMap, createFoundationCssVarMap, createStaticCssVarMap, createTailwindThemeBridge, createThemeCssVarMap, generateThemeCSS, getStoredTheme, injectThemeStyle, setTheme, staticToCssVar, tokenToCssVar, } from './css.js'; +export type { TokenName } from './helpers.js'; +export { cssVar, themeStyle } from './helpers.js'; +export type { AccountSurfaceProps, AccountFormProps, BadgeProps, ButtonProps, ButtonVariant, CardProps, FieldProps, FormActionsProps, PluginBatchAccountInput, PluginBatchImportResult, PluginFrontendModule, PluginMenuItemDefinition, PluginOAuthBatchExchangeResult, PluginOAuthBridge, PluginOAuthExchangeResult, PluginOAuthStartResult, PanelHeaderProps, PluginPlatformIconProps, PluginStatusKind, PluginStyleFoundationOptions, PluginTailwindConfigOptions, PluginRouteDefinition, ResolvePluginThemeOptions, ScopedPluginThemeOptions, SectionProps, SelectableCardProps, StatusTextProps, UsageRecordSurfaceProps, } from './plugin.js'; +export { DEFAULT_PLUGIN_CLASS_PREFIX, DEFAULT_PLUGIN_FOUNDATION_STYLE_ID, DEFAULT_PLUGIN_THEME_ATTRIBUTE, DEFAULT_PLUGIN_THEME_STYLE_ID, Badge, Button, Card, Field, FormActions, PanelHeader, Section, SecretInput, SelectableCard, StatusText, TextInput, TextArea, cn, createPluginTailwindConfig, ensurePluginStyleFoundation, injectStyle, pluginFoundationCssText, resolvePluginTheme, useScopedPluginTheme, } from './plugin.js'; diff --git a/theme/dist/index.js b/theme/dist/index.js new file mode 100644 index 0000000..b497dd1 --- /dev/null +++ b/theme/dist/index.js @@ -0,0 +1,7 @@ +// Token 常量 +export { appShellTokens, darkTheme, foundationTokens, lightElevationContexts, lightTheme, staticTokenGroups, staticTokens, decorativePalette, themes, } from './tokens.js'; +// CSS 生成与运行时 +export { createAppShellCssVarMap, createFoundationCssVarMap, createStaticCssVarMap, createTailwindThemeBridge, createThemeCssVarMap, generateThemeCSS, getStoredTheme, injectThemeStyle, setTheme, staticToCssVar, tokenToCssVar, } from './css.js'; +export { cssVar, themeStyle } from './helpers.js'; +// 插件前端 SDK:样式注入、主题同步、Tailwind bridge 和公共组件 +export { DEFAULT_PLUGIN_CLASS_PREFIX, DEFAULT_PLUGIN_FOUNDATION_STYLE_ID, DEFAULT_PLUGIN_THEME_ATTRIBUTE, DEFAULT_PLUGIN_THEME_STYLE_ID, Badge, Button, Card, Field, FormActions, PanelHeader, Section, SecretInput, SelectableCard, StatusText, TextInput, TextArea, cn, createPluginTailwindConfig, ensurePluginStyleFoundation, injectStyle, pluginFoundationCssText, resolvePluginTheme, useScopedPluginTheme, } from './plugin.js'; diff --git a/theme/dist/plugin.d.ts b/theme/dist/plugin.d.ts new file mode 100644 index 0000000..07eb669 --- /dev/null +++ b/theme/dist/plugin.d.ts @@ -0,0 +1,231 @@ +import { type ButtonHTMLAttributes, type ComponentType, type CSSProperties, type InputHTMLAttributes, type ReactNode, type TextareaHTMLAttributes } from 'react'; +import type { ThemeName } from './types.js'; +export declare const DEFAULT_PLUGIN_THEME_ATTRIBUTE = "data-theme"; +export declare const DEFAULT_PLUGIN_CLASS_PREFIX = "agw-"; +export declare const DEFAULT_PLUGIN_THEME_STYLE_ID = "ag-plugin-theme-vars"; +export declare const DEFAULT_PLUGIN_FOUNDATION_STYLE_ID = "ag-plugin-foundation"; +export interface PluginStyleFoundationOptions { + scopeSelector: string; + themeAttribute?: string; + themeStyleId?: string; + foundationStyleId?: string; + extraCssText?: string; + extraStyleId?: string; + targetDocument?: Document; +} +export interface ResolvePluginThemeOptions { + storageKey?: string; +} +export interface ScopedPluginThemeOptions { + themeAttribute?: string; + storageKey?: string; +} +export interface PluginTailwindConfigOptions { + scopeSelector: string; + classPrefix?: string; + tokenPrefix?: string; +} +export type PluginStatusKind = 'info' | 'success' | 'error'; +export interface PluginOAuthStartResult { + authorizeURL: string; + state: string; +} +export interface PluginOAuthExchangeResult { + accountType: string; + accountName: string; + credentials: Record; +} +export interface PluginOAuthBatchExchangeResult { + accountType: string; + accountName: string; + credentials: Record; + status: 'ok' | 'failed'; + error?: string; +} +export interface PluginOAuthBridge { + start: () => Promise; + exchange: (callbackURL: string) => Promise; + batchExchange?: (sessionKeys: string[]) => Promise; + importRefresh?: (refreshToken: string, clientId?: string) => Promise; + batchImportRefresh?: (refreshTokens: string[], clientId?: string) => Promise; +} +export interface PluginBatchAccountInput { + name: string; + type: string; + credentials: Record; +} +export interface PluginBatchImportResult { + imported: number; + failed: number; +} +export interface AccountFormProps { + credentials: Record; + onChange: (credentials: Record) => void; + mode: 'create' | 'edit'; + accountType?: string; + onAccountTypeChange?: (type: string) => void; + onSuggestedName?: (name: string) => void; + onBatchModeChange?: (isBatch: boolean) => void; + onBatchImport?: (accounts: PluginBatchAccountInput[]) => Promise; + oauth?: PluginOAuthBridge; +} +export interface PluginRouteDefinition { + path: string; + component: ComponentType; +} +export interface PluginMenuItemDefinition { + path: string; + title: string; + icon: string; +} +export interface PluginPlatformIconProps { + className?: string; + style?: CSSProperties; +} +export interface AccountSurfaceProps { + accountId?: string | number; + accountType?: string; + context?: Record; +} +export interface UsageRecordSurfaceProps { + recordId?: string | number; + context?: Record; +} +export interface PluginFrontendModule { + routes?: PluginRouteDefinition[]; + menuItems?: PluginMenuItemDefinition[]; + accountIdentity?: ComponentType; + accountCreate?: ComponentType; + accountEdit?: ComponentType; + accountUsageWindow?: ComponentType; + /** + * 使用记录列表中“模型”列的行级别扩展渲染器。 + * + * 用途:为平台补充展示模型分档信息(例如图像分辨率、推理强度、服务层级等), + * Core 只提供表格骨架与回退渲染,不内置平台语义。 + */ + usageModelMeta?: ComponentType; + usageMetricDetail?: ComponentType; + usageCostDetail?: ComponentType; + platformIcon?: ComponentType; +} +export declare const pluginFoundationCssText = "\n/* \u2500\u2500 AirGate \u2014 Plugin Foundation \u2500\u2500 */\n\n.agw-form-shell {\n display: flex;\n flex-direction: column;\n gap: 1rem;\n min-width: 0;\n font-family: var(--ag-font-sans);\n font-size: 0.875rem;\n color: var(--ag-text);\n letter-spacing: 0;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n\n.agw-form-shell > * {\n min-width: 0;\n}\n\n.agw-field {\n display: flex;\n flex-direction: column;\n gap: 0.375rem;\n}\n\n.agw-section {\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n}\n\n.agw-section-content {\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n}\n\n.agw-panel-header {\n display: flex;\n flex-direction: column;\n gap: 0.25rem;\n}\n\n.agw-panel-title {\n font-size: 0.875rem;\n font-weight: 600;\n letter-spacing: 0;\n color: var(--ag-text);\n}\n\n.agw-panel-eyebrow {\n font-size: 0.625rem;\n font-weight: 500;\n text-transform: uppercase;\n letter-spacing: 0;\n color: var(--ag-text-tertiary);\n font-family: var(--ag-font-mono);\n}\n\n.agw-panel-description {\n font-size: 0.75rem;\n line-height: 1.65;\n color: var(--ag-text-secondary);\n}\n\n.agw-label {\n font-size: 0.6875rem;\n font-weight: 500;\n text-transform: uppercase;\n letter-spacing: 0;\n color: var(--ag-text-secondary);\n}\n\n.agw-label-required {\n margin-left: 0.25rem;\n color: var(--ag-danger);\n}\n\n.agw-hint {\n font-size: 0.75rem;\n line-height: 1.65;\n color: var(--ag-text-tertiary);\n}\n\n.agw-input {\n display: block;\n width: 100%;\n border: 1px solid color-mix(in oklab, var(--ag-border) 88%, transparent);\n border-radius: var(--ag-field-radius, 0.5rem);\n background: var(--ag-field-background);\n padding: 0.5rem 0.75rem;\n color: var(--ag-field-foreground);\n font-size: 0.875rem;\n outline: none;\n box-shadow: var(--ag-shadow-sm);\n transition: border-color var(--ag-transition), box-shadow var(--ag-transition), background-color var(--ag-transition);\n}\n\n.agw-input::placeholder {\n color: var(--ag-field-placeholder);\n}\n\n.agw-input:hover {\n background: color-mix(in oklab, var(--ag-field-background) 86%, var(--ag-surface) 14%);\n border-color: color-mix(in oklab, var(--ag-border) 92%, var(--ag-text) 8%);\n}\n\n.agw-input:focus,\n.agw-input:focus-visible {\n border-color: var(--ag-border-focus);\n box-shadow: 0 0 0 2px color-mix(in oklab, var(--ag-primary) 22%, transparent);\n}\n\n.agw-input-mono {\n font-family: var(--ag-font-mono);\n}\n\n.agw-textarea {\n min-height: 76px;\n resize: vertical;\n}\n\n.agw-card {\n border: 1px solid var(--ag-border);\n border-radius: var(--ag-radius-sm);\n background: var(--ag-surface);\n padding: 1rem;\n transition: border-color var(--ag-transition), background-color var(--ag-transition), box-shadow var(--ag-transition);\n}\n\n.agw-status-inline {\n display: inline-flex;\n align-items: center;\n padding: 0.25rem 0.75rem;\n border: 1px solid var(--ag-border);\n border-radius: var(--ag-radius-sm);\n background: var(--ag-surface-secondary);\n font-size: 0.75rem;\n font-weight: 500;\n}\n\n.agw-status-inline-info {\n color: var(--ag-text-secondary);\n}\n\n.agw-status-inline-success {\n color: var(--ag-success);\n}\n\n.agw-status-inline-error {\n color: var(--ag-danger);\n}\n\n.agw-panel {\n gap: 0;\n padding: 1.25rem;\n background: var(--ag-surface);\n border: 1px solid var(--ag-border);\n border-radius: var(--ag-radius-sm);\n}\n\n.agw-card-active {\n border-color: var(--ag-border-focus);\n background: var(--ag-primary-subtle);\n box-shadow: 0 0 0 1px color-mix(in oklab, var(--ag-primary) 22%, transparent);\n}\n\n.agw-selectable-card {\n position: relative;\n width: 100%;\n overflow: hidden;\n text-align: left;\n cursor: pointer;\n}\n\n.agw-selectable-card:hover {\n border-color: var(--ag-border);\n background: var(--ag-bg-surface);\n}\n\n.agw-focus-ring:focus-visible {\n outline: 1.5px solid var(--ag-primary);\n outline-offset: 2px;\n box-shadow: 0 0 0 2px color-mix(in oklab, var(--ag-primary) 18%, transparent);\n}\n\n.agw-button-primary,\n.agw-button-secondary,\n.agw-button-outline {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 0.5rem;\n border-radius: var(--ag-radius-sm);\n padding: 0.5rem 1rem;\n font-size: 0.875rem;\n font-weight: 500;\n cursor: pointer;\n transition: border-color 200ms, color 200ms, background-color 200ms, opacity 200ms, box-shadow 200ms;\n}\n\n.agw-button-primary {\n border: 1px solid transparent;\n background: var(--ag-primary);\n color: var(--ag-primary-foreground);\n box-shadow: none;\n}\n\n.agw-button-primary:hover {\n background: var(--ag-primary-hover);\n}\n\n.agw-button-secondary {\n border: 1px solid var(--ag-border);\n background: var(--ag-default-bg);\n color: var(--ag-default-foreground);\n}\n\n.agw-button-secondary:hover {\n border-color: var(--ag-border);\n background: var(--ag-bg-hover);\n}\n\n.agw-button-outline {\n border: 1px solid var(--ag-border);\n background: transparent;\n color: var(--ag-text);\n}\n\n.agw-button-outline:hover {\n background: var(--ag-primary-subtle);\n}\n\n.agw-form-actions {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 0.625rem;\n}\n\n.agw-badge {\n display: inline-flex;\n align-items: center;\n border-radius: var(--ag-radius-sm);\n padding: 0.25rem 0.625rem;\n font-size: 0.6875rem;\n font-weight: 500;\n letter-spacing: 0;\n}\n\n.agw-badge-neutral {\n background: var(--ag-default-bg);\n color: var(--ag-default-foreground);\n}\n\n.agw-badge-success {\n background: var(--ag-success-subtle);\n color: var(--ag-success);\n}\n\n.agw-badge-violet {\n background: var(--ag-info-subtle);\n color: var(--ag-info);\n}\n\n.agw-badge-info {\n background: var(--ag-primary-subtle);\n color: var(--ag-primary);\n}\n\n.agw-button-primary:disabled,\n.agw-button-secondary:disabled,\n.agw-button-outline:disabled,\n.agw-input:disabled,\n.agw-selectable-card:disabled {\n cursor: not-allowed;\n opacity: 0.45;\n}\n\n/* \u2500\u2500 Light theme and modal elevation follow HeroUI bridge tokens. \u2500\u2500 */\n\n[data-theme=\"light\"] .agw-card,\n[data-theme=\"light\"] .agw-panel {\n box-shadow: var(--ag-shadow-sm);\n}\n\n.ag-elevation-modal .agw-input {\n background: var(--ag-field-background);\n border-color: color-mix(in oklab, var(--ag-border) 88%, transparent);\n box-shadow: var(--ag-shadow-sm);\n}\n\n.ag-elevation-modal .agw-card,\n.ag-elevation-modal .agw-panel {\n background: var(--ag-surface);\n border-color: var(--ag-border);\n box-shadow: none;\n}\n\n.ag-elevation-modal .agw-card:hover,\n.ag-elevation-modal .agw-selectable-card:hover {\n background: var(--ag-surface-secondary);\n border-color: var(--ag-border);\n}\n\n.ag-elevation-modal .agw-card-active {\n background: var(--ag-primary-subtle);\n border-color: var(--ag-border-focus);\n}\n\n.ag-elevation-modal .agw-button-secondary {\n background: var(--ag-default-bg);\n border-color: var(--ag-border);\n}\n\n.ag-elevation-modal .agw-button-secondary:hover {\n background: var(--ag-bg-hover);\n border-color: var(--ag-border);\n}\n"; +export declare function injectStyle(id: string, cssText: string, targetDocument?: Document): void; +export declare function ensurePluginStyleFoundation({ scopeSelector, themeAttribute, themeStyleId, foundationStyleId, extraCssText, extraStyleId, targetDocument, }: PluginStyleFoundationOptions): void; +export declare function resolvePluginTheme({ storageKey }?: ResolvePluginThemeOptions): ThemeName; +export declare function useScopedPluginTheme(options?: ScopedPluginThemeOptions): import("react").RefObject; +export declare function createPluginTailwindConfig({ scopeSelector, classPrefix, tokenPrefix, }: PluginTailwindConfigOptions): { + readonly prefix: string; + readonly important: string; + readonly theme: { + readonly extend: { + readonly colors: { + readonly primary: `var(${string})`; + readonly 'primary-hover': `var(${string})`; + readonly 'primary-subtle': `var(${string})`; + readonly success: `var(${string})`; + readonly 'success-subtle': `var(${string})`; + readonly warning: `var(${string})`; + readonly 'warning-subtle': `var(${string})`; + readonly danger: `var(${string})`; + readonly 'danger-subtle': `var(${string})`; + readonly info: `var(${string})`; + readonly 'info-subtle': `var(${string})`; + readonly bg: `var(${string})`; + readonly 'bg-deep': `var(${string})`; + readonly 'bg-elevated': `var(${string})`; + readonly surface: `var(${string})`; + readonly 'bg-hover': `var(${string})`; + readonly 'bg-active': `var(${string})`; + readonly border: `var(${string})`; + readonly 'border-subtle': `var(${string})`; + readonly 'border-focus': `var(${string})`; + readonly text: `var(${string})`; + readonly 'text-secondary': `var(${string})`; + readonly 'text-tertiary': `var(${string})`; + readonly 'text-inverse': `var(${string})`; + readonly glass: `var(${string})`; + readonly 'glass-border': `var(${string})`; + }; + readonly borderRadius: { + readonly sm: `var(${string})`; + readonly md: `var(${string})`; + readonly lg: `var(${string})`; + readonly xl: `var(${string})`; + readonly field: `var(${string})`; + }; + readonly fontFamily: { + readonly sans: `var(${string})`; + readonly mono: `var(${string})`; + }; + readonly boxShadow: { + readonly sm: `var(${string})`; + readonly md: `var(${string})`; + readonly lg: `var(${string})`; + readonly glow: `var(${string})`; + }; + readonly transitionDuration: { + readonly DEFAULT: `var(${string})`; + readonly slow: `var(${string})`; + }; + }; + }; + readonly corePlugins: { + readonly preflight: false; + }; +}; +export declare function cn(...values: Array): string; +export interface FieldProps { + label: ReactNode; + required?: boolean; + hint?: ReactNode; + children: ReactNode; + className?: string; +} +export declare function Field({ label, required, hint, children, className }: FieldProps): import("react/jsx-runtime").JSX.Element; +export declare function TextInput({ className, ...props }: InputHTMLAttributes): import("react/jsx-runtime").JSX.Element; +export declare function SecretInput({ className, ...props }: InputHTMLAttributes): import("react/jsx-runtime").JSX.Element; +export declare function TextArea({ className, ...props }: TextareaHTMLAttributes): import("react/jsx-runtime").JSX.Element; +export interface PanelHeaderProps { + title: ReactNode; + description?: ReactNode; + eyebrow?: ReactNode; + className?: string; +} +export declare function PanelHeader({ title, description, eyebrow, className }: PanelHeaderProps): import("react/jsx-runtime").JSX.Element; +export interface SectionProps extends PanelHeaderProps { + children: ReactNode; + panel?: boolean; + contentClassName?: string; +} +export declare function Section({ title, description, eyebrow, children, panel, className, contentClassName, }: SectionProps): import("react/jsx-runtime").JSX.Element; +export interface CardProps { + children: ReactNode; + className?: string; +} +export declare function Card({ children, className }: CardProps): import("react/jsx-runtime").JSX.Element; +export interface SelectableCardProps extends ButtonHTMLAttributes { + active?: boolean; +} +export declare function SelectableCard({ active, className, children, ...props }: SelectableCardProps): import("react/jsx-runtime").JSX.Element; +export type ButtonVariant = 'primary' | 'secondary' | 'outline'; +export interface ButtonProps extends ButtonHTMLAttributes { + variant?: ButtonVariant; +} +export declare function Button({ variant, className, children, ...props }: ButtonProps): import("react/jsx-runtime").JSX.Element; +export interface FormActionsProps { + children: ReactNode; + className?: string; +} +export declare function FormActions({ children, className }: FormActionsProps): import("react/jsx-runtime").JSX.Element; +export interface BadgeProps { + children: ReactNode; + tone?: 'neutral' | 'success' | 'violet' | 'info'; + className?: string; +} +export declare function Badge({ children, tone, className }: BadgeProps): import("react/jsx-runtime").JSX.Element; +export interface StatusTextProps { + type: PluginStatusKind; + text: string; +} +export declare function StatusText({ type, text }: StatusTextProps): import("react/jsx-runtime").JSX.Element; diff --git a/theme/dist/plugin.js b/theme/dist/plugin.js new file mode 100644 index 0000000..64c6681 --- /dev/null +++ b/theme/dist/plugin.js @@ -0,0 +1,467 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useLayoutEffect, useRef, } from 'react'; +import { createTailwindThemeBridge, getStoredTheme, injectThemeStyle, setTheme } from './css.js'; +export const DEFAULT_PLUGIN_THEME_ATTRIBUTE = 'data-theme'; +export const DEFAULT_PLUGIN_CLASS_PREFIX = 'agw-'; +export const DEFAULT_PLUGIN_THEME_STYLE_ID = 'ag-plugin-theme-vars'; +export const DEFAULT_PLUGIN_FOUNDATION_STYLE_ID = 'ag-plugin-foundation'; +export const pluginFoundationCssText = ` +/* ── AirGate — Plugin Foundation ── */ + +.agw-form-shell { + display: flex; + flex-direction: column; + gap: 1rem; + min-width: 0; + font-family: var(--ag-font-sans); + font-size: 0.875rem; + color: var(--ag-text); + letter-spacing: 0; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.agw-form-shell > * { + min-width: 0; +} + +.agw-field { + display: flex; + flex-direction: column; + gap: 0.375rem; +} + +.agw-section { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.agw-section-content { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.agw-panel-header { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.agw-panel-title { + font-size: 0.875rem; + font-weight: 600; + letter-spacing: 0; + color: var(--ag-text); +} + +.agw-panel-eyebrow { + font-size: 0.625rem; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0; + color: var(--ag-text-tertiary); + font-family: var(--ag-font-mono); +} + +.agw-panel-description { + font-size: 0.75rem; + line-height: 1.65; + color: var(--ag-text-secondary); +} + +.agw-label { + font-size: 0.6875rem; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0; + color: var(--ag-text-secondary); +} + +.agw-label-required { + margin-left: 0.25rem; + color: var(--ag-danger); +} + +.agw-hint { + font-size: 0.75rem; + line-height: 1.65; + color: var(--ag-text-tertiary); +} + +.agw-input { + display: block; + width: 100%; + border: 1px solid color-mix(in oklab, var(--ag-border) 88%, transparent); + border-radius: var(--ag-field-radius, 0.5rem); + background: var(--ag-field-background); + padding: 0.5rem 0.75rem; + color: var(--ag-field-foreground); + font-size: 0.875rem; + outline: none; + box-shadow: var(--ag-shadow-sm); + transition: border-color var(--ag-transition), box-shadow var(--ag-transition), background-color var(--ag-transition); +} + +.agw-input::placeholder { + color: var(--ag-field-placeholder); +} + +.agw-input:hover { + background: color-mix(in oklab, var(--ag-field-background) 86%, var(--ag-surface) 14%); + border-color: color-mix(in oklab, var(--ag-border) 92%, var(--ag-text) 8%); +} + +.agw-input:focus, +.agw-input:focus-visible { + border-color: var(--ag-border-focus); + box-shadow: 0 0 0 2px color-mix(in oklab, var(--ag-primary) 22%, transparent); +} + +.agw-input-mono { + font-family: var(--ag-font-mono); +} + +.agw-textarea { + min-height: 76px; + resize: vertical; +} + +.agw-card { + border: 1px solid var(--ag-border); + border-radius: var(--ag-radius-sm); + background: var(--ag-surface); + padding: 1rem; + transition: border-color var(--ag-transition), background-color var(--ag-transition), box-shadow var(--ag-transition); +} + +.agw-status-inline { + display: inline-flex; + align-items: center; + padding: 0.25rem 0.75rem; + border: 1px solid var(--ag-border); + border-radius: var(--ag-radius-sm); + background: var(--ag-surface-secondary); + font-size: 0.75rem; + font-weight: 500; +} + +.agw-status-inline-info { + color: var(--ag-text-secondary); +} + +.agw-status-inline-success { + color: var(--ag-success); +} + +.agw-status-inline-error { + color: var(--ag-danger); +} + +.agw-panel { + gap: 0; + padding: 1.25rem; + background: var(--ag-surface); + border: 1px solid var(--ag-border); + border-radius: var(--ag-radius-sm); +} + +.agw-card-active { + border-color: var(--ag-border-focus); + background: var(--ag-primary-subtle); + box-shadow: 0 0 0 1px color-mix(in oklab, var(--ag-primary) 22%, transparent); +} + +.agw-selectable-card { + position: relative; + width: 100%; + overflow: hidden; + text-align: left; + cursor: pointer; +} + +.agw-selectable-card:hover { + border-color: var(--ag-border); + background: var(--ag-bg-surface); +} + +.agw-focus-ring:focus-visible { + outline: 1.5px solid var(--ag-primary); + outline-offset: 2px; + box-shadow: 0 0 0 2px color-mix(in oklab, var(--ag-primary) 18%, transparent); +} + +.agw-button-primary, +.agw-button-secondary, +.agw-button-outline { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + border-radius: var(--ag-radius-sm); + padding: 0.5rem 1rem; + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + transition: border-color 200ms, color 200ms, background-color 200ms, opacity 200ms, box-shadow 200ms; +} + +.agw-button-primary { + border: 1px solid transparent; + background: var(--ag-primary); + color: var(--ag-primary-foreground); + box-shadow: none; +} + +.agw-button-primary:hover { + background: var(--ag-primary-hover); +} + +.agw-button-secondary { + border: 1px solid var(--ag-border); + background: var(--ag-default-bg); + color: var(--ag-default-foreground); +} + +.agw-button-secondary:hover { + border-color: var(--ag-border); + background: var(--ag-bg-hover); +} + +.agw-button-outline { + border: 1px solid var(--ag-border); + background: transparent; + color: var(--ag-text); +} + +.agw-button-outline:hover { + background: var(--ag-primary-subtle); +} + +.agw-form-actions { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.625rem; +} + +.agw-badge { + display: inline-flex; + align-items: center; + border-radius: var(--ag-radius-sm); + padding: 0.25rem 0.625rem; + font-size: 0.6875rem; + font-weight: 500; + letter-spacing: 0; +} + +.agw-badge-neutral { + background: var(--ag-default-bg); + color: var(--ag-default-foreground); +} + +.agw-badge-success { + background: var(--ag-success-subtle); + color: var(--ag-success); +} + +.agw-badge-violet { + background: var(--ag-info-subtle); + color: var(--ag-info); +} + +.agw-badge-info { + background: var(--ag-primary-subtle); + color: var(--ag-primary); +} + +.agw-button-primary:disabled, +.agw-button-secondary:disabled, +.agw-button-outline:disabled, +.agw-input:disabled, +.agw-selectable-card:disabled { + cursor: not-allowed; + opacity: 0.45; +} + +/* ── Light theme and modal elevation follow HeroUI bridge tokens. ── */ + +[data-theme="light"] .agw-card, +[data-theme="light"] .agw-panel { + box-shadow: var(--ag-shadow-sm); +} + +.ag-elevation-modal .agw-input { + background: var(--ag-field-background); + border-color: color-mix(in oklab, var(--ag-border) 88%, transparent); + box-shadow: var(--ag-shadow-sm); +} + +.ag-elevation-modal .agw-card, +.ag-elevation-modal .agw-panel { + background: var(--ag-surface); + border-color: var(--ag-border); + box-shadow: none; +} + +.ag-elevation-modal .agw-card:hover, +.ag-elevation-modal .agw-selectable-card:hover { + background: var(--ag-surface-secondary); + border-color: var(--ag-border); +} + +.ag-elevation-modal .agw-card-active { + background: var(--ag-primary-subtle); + border-color: var(--ag-border-focus); +} + +.ag-elevation-modal .agw-button-secondary { + background: var(--ag-default-bg); + border-color: var(--ag-border); +} + +.ag-elevation-modal .agw-button-secondary:hover { + background: var(--ag-bg-hover); + border-color: var(--ag-border); +} +`; +export function injectStyle(id, cssText, targetDocument) { + const doc = targetDocument || (typeof document !== 'undefined' ? document : undefined); + if (!doc) + return; + let element = doc.getElementById(id); + if (!element) { + element = doc.createElement('style'); + element.id = id; + doc.head.appendChild(element); + } + if (element.textContent !== cssText) { + element.textContent = cssText; + } +} +export function ensurePluginStyleFoundation({ scopeSelector, themeAttribute = DEFAULT_PLUGIN_THEME_ATTRIBUTE, themeStyleId = DEFAULT_PLUGIN_THEME_STYLE_ID, foundationStyleId = DEFAULT_PLUGIN_FOUNDATION_STYLE_ID, extraCssText, extraStyleId, targetDocument, }) { + injectThemeStyle({ + styleId: themeStyleId, + scopeSelector, + themeAttribute, + targetDocument, + }); + injectStyle(foundationStyleId, pluginFoundationCssText, targetDocument); + if (extraCssText && extraStyleId) { + injectStyle(extraStyleId, extraCssText, targetDocument); + } +} +export function resolvePluginTheme({ storageKey } = {}) { + const theme = getStoredTheme({ storageKey }); + return theme === 'light' ? 'light' : 'dark'; +} +function resolveInheritedTheme(element, themeAttribute, storageKey) { + const scopedAncestor = element.parentElement?.closest(`[${themeAttribute}]`); + const hostTheme = scopedAncestor?.getAttribute(themeAttribute) + || document.documentElement.getAttribute(themeAttribute); + return hostTheme === 'light' + ? 'light' + : hostTheme === 'dark' + ? 'dark' + : resolvePluginTheme({ storageKey }); +} +export function useScopedPluginTheme(options = {}) { + const { themeAttribute = DEFAULT_PLUGIN_THEME_ATTRIBUTE, storageKey } = options; + const ref = useRef(null); + useLayoutEffect(() => { + const element = ref.current; + if (!element) + return; + const applyTheme = () => { + setTheme(resolveInheritedTheme(element, themeAttribute, storageKey), { + target: element, + themeAttribute, + storageKey, + }); + }; + applyTheme(); + const hostElement = element.parentElement?.closest(`[${themeAttribute}]`) + || document.documentElement; + const observer = new MutationObserver((mutations) => { + for (const mutation of mutations) { + if (mutation.type === 'attributes' && mutation.attributeName === themeAttribute) { + applyTheme(); + break; + } + } + }); + observer.observe(hostElement, { attributes: true, attributeFilter: [themeAttribute] }); + return () => observer.disconnect(); + }, [themeAttribute, storageKey]); + return ref; +} +export function createPluginTailwindConfig({ scopeSelector, classPrefix = DEFAULT_PLUGIN_CLASS_PREFIX, tokenPrefix, }) { + return { + prefix: classPrefix, + important: scopeSelector, + theme: { + extend: { + ...createTailwindThemeBridge(tokenPrefix ? { prefix: tokenPrefix } : {}), + }, + }, + corePlugins: { + preflight: false, + }, + }; +} +export function cn(...values) { + return values.filter(Boolean).join(' '); +} +export function Field({ label, required = false, hint, children, className }) { + return (_jsxs("div", { className: cn('agw-field', className), children: [_jsxs("label", { className: "agw-label", children: [label, required && _jsx("span", { className: "agw-label-required", children: "*" })] }), children, hint ? _jsx("div", { className: "agw-hint", children: hint }) : null] })); +} +export function TextInput({ className, ...props }) { + return _jsx("input", { ...props, className: cn('agw-input', className) }); +} +export function SecretInput({ className, ...props }) { + return _jsx("input", { ...props, type: "password", className: cn('agw-input agw-input-mono', className) }); +} +export function TextArea({ className, ...props }) { + return _jsx("textarea", { ...props, className: cn('agw-input agw-input-mono agw-textarea', className) }); +} +export function PanelHeader({ title, description, eyebrow, className }) { + return (_jsxs("div", { className: cn('agw-panel-header', className), children: [eyebrow ? _jsx("div", { className: "agw-panel-eyebrow", children: eyebrow }) : null, _jsx("div", { className: "agw-panel-title", children: title }), description ? _jsx("div", { className: "agw-panel-description", children: description }) : null] })); +} +export function Section({ title, description, eyebrow, children, panel = false, className, contentClassName, }) { + return (_jsxs("div", { className: cn(panel ? 'agw-panel agw-section' : 'agw-section', className), children: [_jsx(PanelHeader, { title: title, description: description, eyebrow: eyebrow }), _jsx("div", { className: cn('agw-section-content', contentClassName), children: children })] })); +} +export function Card({ children, className }) { + return _jsx("div", { className: cn('agw-card', className), children: children }); +} +export function SelectableCard({ active = false, className, children, ...props }) { + return (_jsx("button", { ...props, type: props.type || 'button', className: cn('agw-card agw-selectable-card agw-focus-ring', active && 'agw-card-active', className), children: children })); +} +const buttonClassMap = { + primary: 'agw-button-primary', + secondary: 'agw-button-secondary', + outline: 'agw-button-outline', +}; +export function Button({ variant = 'secondary', className, children, ...props }) { + return (_jsx("button", { ...props, type: props.type || 'button', className: cn('agw-focus-ring', buttonClassMap[variant], className), children: children })); +} +export function FormActions({ children, className }) { + return _jsx("div", { className: cn('agw-form-actions', className), children: children }); +} +const badgeToneClassMap = { + neutral: 'agw-badge-neutral', + success: 'agw-badge-success', + violet: 'agw-badge-violet', + info: 'agw-badge-info', +}; +export function Badge({ children, tone = 'neutral', className }) { + return _jsx("span", { className: cn('agw-badge', badgeToneClassMap[tone], className), children: children }); +} +const statusClassMap = { + info: 'agw-status-inline-info', + success: 'agw-status-inline-success', + error: 'agw-status-inline-error', +}; +export function StatusText({ type, text }) { + return _jsx("div", { className: cn('agw-status-inline', statusClassMap[type]), children: text }); +} diff --git a/theme/dist/tailwind.d.ts b/theme/dist/tailwind.d.ts new file mode 100644 index 0000000..0237631 --- /dev/null +++ b/theme/dist/tailwind.d.ts @@ -0,0 +1,54 @@ +import { createTailwindThemeBridge } from './css.js'; +declare const tailwindThemeBridge: { + readonly colors: { + readonly primary: `var(${string})`; + readonly 'primary-hover': `var(${string})`; + readonly 'primary-subtle': `var(${string})`; + readonly success: `var(${string})`; + readonly 'success-subtle': `var(${string})`; + readonly warning: `var(${string})`; + readonly 'warning-subtle': `var(${string})`; + readonly danger: `var(${string})`; + readonly 'danger-subtle': `var(${string})`; + readonly info: `var(${string})`; + readonly 'info-subtle': `var(${string})`; + readonly bg: `var(${string})`; + readonly 'bg-deep': `var(${string})`; + readonly 'bg-elevated': `var(${string})`; + readonly surface: `var(${string})`; + readonly 'bg-hover': `var(${string})`; + readonly 'bg-active': `var(${string})`; + readonly border: `var(${string})`; + readonly 'border-subtle': `var(${string})`; + readonly 'border-focus': `var(${string})`; + readonly text: `var(${string})`; + readonly 'text-secondary': `var(${string})`; + readonly 'text-tertiary': `var(${string})`; + readonly 'text-inverse': `var(${string})`; + readonly glass: `var(${string})`; + readonly 'glass-border': `var(${string})`; + }; + readonly borderRadius: { + readonly sm: `var(${string})`; + readonly md: `var(${string})`; + readonly lg: `var(${string})`; + readonly xl: `var(${string})`; + readonly field: `var(${string})`; + }; + readonly fontFamily: { + readonly sans: `var(${string})`; + readonly mono: `var(${string})`; + }; + readonly boxShadow: { + readonly sm: `var(${string})`; + readonly md: `var(${string})`; + readonly lg: `var(${string})`; + readonly glow: `var(${string})`; + }; + readonly transitionDuration: { + readonly DEFAULT: `var(${string})`; + readonly slow: `var(${string})`; + }; +}; +export default tailwindThemeBridge; +export { createTailwindThemeBridge }; diff --git a/theme/dist/tailwind.js b/theme/dist/tailwind.js new file mode 100644 index 0000000..953a43f --- /dev/null +++ b/theme/dist/tailwind.js @@ -0,0 +1,4 @@ +import { createTailwindThemeBridge } from './css.js'; +const tailwindThemeBridge = createTailwindThemeBridge(); +export default tailwindThemeBridge; +export { createTailwindThemeBridge }; diff --git a/theme/dist/tokens.d.ts b/theme/dist/tokens.d.ts new file mode 100644 index 0000000..1e24e4a --- /dev/null +++ b/theme/dist/tokens.d.ts @@ -0,0 +1,23 @@ +import type { AppShellTokens, ElevationContext, FoundationTokens, StaticTokenGroups, StaticTokens, ThemeName, ThemeTokens } from './types.js'; +/** 暗色主题 — HeroUI Theme Builder preset */ +export declare const darkTheme: ThemeTokens; +/** 亮色主题 — HeroUI Theme Builder preset */ +export declare const lightTheme: ThemeTokens; +/** 通用基础 token:HeroUI Radius 为 Small,Radius Form 为 Medium。 */ +export declare const foundationTokens: FoundationTokens; +/** 应用壳层 token */ +export declare const appShellTokens: AppShellTokens; +/** 分组后的静态 token */ +export declare const staticTokenGroups: StaticTokenGroups; +/** 不随主题变化的静态 token */ +export declare const staticTokens: StaticTokens; +/** 图表/头像装饰色(与主题无关的固定调色板) */ +export declare const decorativePalette: readonly ["#3b82f6", "#10b981", "#f59e0b", "#ef4444", "#8b5cf6", "#06b6d4", "#ec4899", "#84cc16", "#f97316", "#6366f1", "#0d9488", "#a855f7"]; +/** 主题集合 */ +export declare const themes: Record; +/** + * 亮色主题 elevation 上下文覆盖 + * 不同 UI 层级(页面 → 弹窗 → 下拉)需要不同的背景/边框/阴影值。 + * 宿主在容器上设置 .ag-elevation-{context} class,子组件自动继承正确的 token 值。 + */ +export declare const lightElevationContexts: Record>; diff --git a/theme/dist/tokens.js b/theme/dist/tokens.js new file mode 100644 index 0000000..3daf5d7 --- /dev/null +++ b/theme/dist/tokens.js @@ -0,0 +1,172 @@ +/** 暗色主题 — HeroUI Theme Builder preset */ +export const darkTheme = { + primary: 'oklch(0.9848 0 0)', + primaryForeground: 'oklch(15% 0.0000 0.00)', + primaryHover: 'color-mix(in oklab, oklch(0.9848 0 0) 88%, oklch(15% 0.0000 0.00) 12%)', + primarySubtle: 'color-mix(in oklab, oklch(0.9848 0 0) 14%, transparent)', + primaryGlow: 'color-mix(in oklab, oklch(0.9848 0 0) 22%, transparent)', + success: 'oklch(73.29% 0.1935 120.35)', + successForeground: 'oklch(21.03% 0.0059 120.35)', + successSubtle: 'color-mix(in oklab, oklch(73.29% 0.1935 120.35) 15%, transparent)', + warning: 'oklch(0.8803 0.1348 86.06)', + warningForeground: 'oklch(15% 0.0404 86.06)', + warningSubtle: 'color-mix(in oklab, oklch(0.8803 0.1348 86.06) 15%, transparent)', + danger: 'oklch(0.7044 0.1872 23.19)', + dangerForeground: 'oklch(15% 0.0500 23.19)', + dangerSubtle: 'color-mix(in oklab, oklch(0.7044 0.1872 23.19) 15%, transparent)', + info: 'oklch(0.9848 0 0)', + infoSubtle: 'color-mix(in oklab, oklch(0.9848 0 0) 14%, transparent)', + defaultBg: 'oklch(27.40% 0.0000 0.00)', + defaultForeground: 'oklch(99.11% 0 0)', + fieldBackground: 'oklch(21.03% 0.0000 0.00)', + fieldForeground: 'oklch(99.11% 0.0000 0.00)', + fieldPlaceholder: 'oklch(70.50% 0.0000 0.00)', + muted: 'oklch(70.50% 0.0000 0.00)', + overlay: 'oklch(21.03% 0.0000 0.00)', + overlayForeground: 'oklch(99.11% 0.0000 0.00)', + scrollbar: 'oklch(70.50% 0.0000 0.00)', + segment: 'oklch(39.64% 0.0000 0.00)', + segmentForeground: 'oklch(99.11% 0.0000 0.00)', + surface: 'oklch(21.03% 0.0000 0.00)', + surfaceForeground: 'oklch(99.11% 0.0000 0.00)', + surfaceSecondary: 'oklch(25.70% 0.0000 0.00)', + surfaceSecondaryForeground: 'oklch(99.11% 0.0000 0.00)', + surfaceTertiary: 'oklch(27.21% 0.0000 0.00)', + surfaceTertiaryForeground: 'oklch(99.11% 0.0000 0.00)', + bgDeep: 'oklch(12.00% 0.0000 0.00)', + bg: 'oklch(12.00% 0.0000 0.00)', + bgElevated: 'oklch(21.03% 0.0000 0.00)', + bgSurface: 'oklch(21.03% 0.0000 0.00)', + bgHover: 'oklch(25.70% 0.0000 0.00)', + bgActive: 'oklch(27.21% 0.0000 0.00)', + border: 'oklch(28.00% 0.0000 0.00)', + borderSubtle: 'oklch(25.00% 0.0000 0.00)', + borderFocus: 'oklch(0.9848 0 0)', + text: 'oklch(99.11% 0.0000 0.00)', + textSecondary: 'oklch(70.50% 0.0000 0.00)', + textTertiary: 'oklch(70.50% 0.0000 0.00)', + textInverse: 'oklch(15% 0.0000 0.00)', + glass: 'color-mix(in oklab, oklch(21.03% 0.0000 0.00) 92%, transparent)', + glassBorder: 'oklch(28.00% 0.0000 0.00)', + shadowSm: '0 0 0 0 transparent inset', + shadowMd: '0 0 0 0 transparent inset', + shadowLg: '0 0 1px 0 #ffffff4d inset', + shadowGlow: '0 0 0 1px color-mix(in oklab, oklch(0.9848 0 0) 18%, transparent)', +}; +/** 亮色主题 — HeroUI Theme Builder preset */ +export const lightTheme = { + primary: 'oklch(0 0 0)', + primaryForeground: 'oklch(99.11% 0 0)', + primaryHover: 'color-mix(in oklab, oklch(0 0 0) 88%, oklch(99.11% 0 0) 12%)', + primarySubtle: 'color-mix(in oklab, oklch(0 0 0) 10%, transparent)', + primaryGlow: 'color-mix(in oklab, oklch(0 0 0) 16%, transparent)', + success: 'oklch(73.29% 0.1935 120.35)', + successForeground: 'oklch(21.03% 0.0059 120.35)', + successSubtle: 'color-mix(in oklab, oklch(73.29% 0.1935 120.35) 15%, transparent)', + warning: 'oklch(0.8446 0.1525 80.6)', + warningForeground: 'oklch(15% 0.0457 80.60)', + warningSubtle: 'color-mix(in oklab, oklch(0.8446 0.1525 80.6) 15%, transparent)', + danger: 'oklch(0.573 0.2249 21.97)', + dangerForeground: 'oklch(98% 0.0200 21.97)', + dangerSubtle: 'color-mix(in oklab, oklch(0.573 0.2249 21.97) 15%, transparent)', + info: 'oklch(0 0 0)', + infoSubtle: 'color-mix(in oklab, oklch(0 0 0) 10%, transparent)', + defaultBg: 'oklch(94.00% 0.0000 0.00)', + defaultForeground: 'oklch(21.03% 0.0059 0.00)', + fieldBackground: 'oklch(100.00% 0.0000 0.00)', + fieldForeground: 'oklch(21.03% 0.0000 0.00)', + fieldPlaceholder: 'oklch(55.17% 0.0000 0.00)', + muted: 'oklch(55.17% 0.0000 0.00)', + overlay: 'oklch(100.00% 0.0000 0.00)', + overlayForeground: 'oklch(21.03% 0.0000 0.00)', + scrollbar: 'oklch(87.10% 0.0000 0.00)', + segment: 'oklch(100.00% 0.0000 0.00)', + segmentForeground: 'oklch(21.03% 0.0000 0.00)', + surface: 'oklch(100.00% 0.0000 0.00)', + surfaceForeground: 'oklch(21.03% 0.0000 0.00)', + surfaceSecondary: 'oklch(95.24% 0.0000 0.00)', + surfaceSecondaryForeground: 'oklch(21.03% 0.0000 0.00)', + surfaceTertiary: 'oklch(93.73% 0.0000 0.00)', + surfaceTertiaryForeground: 'oklch(21.03% 0.0000 0.00)', + bgDeep: 'oklch(97.02% 0.0000 0.00)', + bg: 'oklch(97.02% 0.0000 0.00)', + bgElevated: 'oklch(100.00% 0.0000 0.00)', + bgSurface: 'oklch(100.00% 0.0000 0.00)', + bgHover: 'oklch(95.24% 0.0000 0.00)', + bgActive: 'oklch(93.73% 0.0000 0.00)', + border: 'oklch(90.00% 0.0000 0.00)', + borderSubtle: 'oklch(92.00% 0.0000 0.00)', + borderFocus: 'oklch(0 0 0)', + text: 'oklch(21.03% 0.0000 0.00)', + textSecondary: 'oklch(55.17% 0.0000 0.00)', + textTertiary: 'oklch(55.17% 0.0000 0.00)', + textInverse: 'oklch(99.11% 0 0)', + glass: 'color-mix(in oklab, oklch(100.00% 0.0000 0.00) 92%, transparent)', + glassBorder: 'oklch(90.00% 0.0000 0.00)', + shadowSm: '0 2px 4px 0 #0000000a, 0 1px 2px 0 #0000000f, 0 0 1px 0 #0000000f', + shadowMd: '0 2px 4px 0 #0000000a, 0 1px 2px 0 #0000000f, 0 0 1px 0 #0000000f', + shadowLg: '0 2px 8px 0 #0000000f, 0 -6px 12px 0 #00000008, 0 14px 28px 0 #00000014', + shadowGlow: '0 0 0 1px color-mix(in oklab, oklch(0 0 0) 12%, transparent)', +}; +/** 通用基础 token:HeroUI Radius 为 Small,Radius Form 为 Medium。 */ +export const foundationTokens = { + radiusSm: '0.25rem', + radiusMd: '0.25rem', + radiusLg: '0.25rem', + radiusXl: '0.25rem', + fieldRadius: '0.5rem', + fontSans: "'Geist Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif", + fontMono: "'Geist Mono', 'SF Mono', 'Cascadia Code', monospace", + transition: '200ms cubic-bezier(0.4, 0, 0.2, 1)', + transitionSlow: '400ms cubic-bezier(0.4, 0, 0.2, 1)', +}; +/** 应用壳层 token */ +export const appShellTokens = { + sidebarWidth: '260px', + sidebarCollapsed: '72px', + topbarHeight: '64px', +}; +/** 分组后的静态 token */ +export const staticTokenGroups = { + foundation: foundationTokens, + appShell: appShellTokens, +}; +/** 不随主题变化的静态 token */ +export const staticTokens = { + ...foundationTokens, + ...appShellTokens, +}; +/** 图表/头像装饰色(与主题无关的固定调色板) */ +export const decorativePalette = [ + '#3b82f6', // blue + '#10b981', // emerald + '#f59e0b', // amber + '#ef4444', // red + '#8b5cf6', // violet + '#06b6d4', // cyan + '#ec4899', // pink + '#84cc16', // lime + '#f97316', // orange + '#6366f1', // indigo + '#0d9488', // teal + '#a855f7', // purple +]; +/** 主题集合 */ +export const themes = { + dark: darkTheme, + light: lightTheme, +}; +/** + * 亮色主题 elevation 上下文覆盖 + * 不同 UI 层级(页面 → 弹窗 → 下拉)需要不同的背景/边框/阴影值。 + * 宿主在容器上设置 .ag-elevation-{context} class,子组件自动继承正确的 token 值。 + */ +export const lightElevationContexts = { + modal: { + // HeroUI preset already provides overlay/surface tokens for modal elevation. + // Keep this empty unless a plugin foundation rule needs a scoped correction. + }, + dropdown: { + // HeroUI dropdown/tooltip surfaces are now handled by the host bridge. + }, +}; diff --git a/theme/dist/types.d.ts b/theme/dist/types.d.ts new file mode 100644 index 0000000..b5c544a --- /dev/null +++ b/theme/dist/types.d.ts @@ -0,0 +1,108 @@ +/** 随主题变化的语义 token(颜色、阴影等) */ +export interface ThemeTokens { + primary: string; + primaryForeground: string; + primaryHover: string; + primarySubtle: string; + primaryGlow: string; + success: string; + successForeground: string; + successSubtle: string; + warning: string; + warningForeground: string; + warningSubtle: string; + danger: string; + dangerForeground: string; + dangerSubtle: string; + info: string; + infoSubtle: string; + defaultBg: string; + defaultForeground: string; + fieldBackground: string; + fieldForeground: string; + fieldPlaceholder: string; + muted: string; + overlay: string; + overlayForeground: string; + scrollbar: string; + segment: string; + segmentForeground: string; + surface: string; + surfaceForeground: string; + surfaceSecondary: string; + surfaceSecondaryForeground: string; + surfaceTertiary: string; + surfaceTertiaryForeground: string; + bgDeep: string; + bg: string; + bgElevated: string; + bgSurface: string; + bgHover: string; + bgActive: string; + border: string; + borderSubtle: string; + borderFocus: string; + text: string; + textSecondary: string; + textTertiary: string; + textInverse: string; + glass: string; + glassBorder: string; + shadowSm: string; + shadowMd: string; + shadowLg: string; + shadowGlow: string; +} +/** 通用基础 token:组件和布局都可复用 */ +export interface FoundationTokens { + radiusSm: string; + radiusMd: string; + radiusLg: string; + radiusXl: string; + fieldRadius: string; + fontSans: string; + fontMono: string; + transition: string; + transitionSlow: string; +} +/** 应用壳层 token:不建议在通用组件中直接依赖 */ +export interface AppShellTokens { + sidebarWidth: string; + sidebarCollapsed: string; + topbarHeight: string; +} +/** 不随主题变化的 token */ +export interface StaticTokens extends FoundationTokens, AppShellTokens { +} +export interface StaticTokenGroups { + foundation: FoundationTokens; + appShell: AppShellTokens; +} +export type ThemeName = 'dark' | 'light'; +export interface ThemeScopeOptions { + scopeSelector?: string; + themeAttribute?: string; + prefix?: string; +} +export interface ThemeCSSOptions extends ThemeScopeOptions { +} +export interface ThemeInjectionOptions extends ThemeScopeOptions { + styleId?: string; + targetDocument?: Document; +} +export interface ThemeSetOptions { + target?: HTMLElement; + themeAttribute?: string; + storageKey?: string; +} +export interface ThemeStorageOptions { + storageKey?: string; +} +export interface CssVarOptions { + prefix?: string; +} +export interface TailwindBridgeOptions { + prefix?: string; +} +/** Elevation context — UI 层级上下文,用于自动调整子组件的 token 值 */ +export type ElevationContext = 'modal' | 'dropdown'; diff --git a/theme/dist/types.js b/theme/dist/types.js new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/theme/dist/types.js @@ -0,0 +1 @@ +export {}; diff --git a/frontend/package-lock.json b/theme/package-lock.json similarity index 91% rename from frontend/package-lock.json rename to theme/package-lock.json index 73475b6..0107030 100644 --- a/frontend/package-lock.json +++ b/theme/package-lock.json @@ -1,12 +1,13 @@ { - "name": "@airgate/theme", - "version": "0.1.0", + "name": "@doudou-start/airgate-theme", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@airgate/theme", - "version": "0.1.0", + "name": "@doudou-start/airgate-theme", + "version": "1.0.0", + "license": "MIT", "devDependencies": { "@types/react": "^19.0.0", "react": "^19.0.0", diff --git a/frontend/package.json b/theme/package.json similarity index 65% rename from frontend/package.json rename to theme/package.json index fcb3ae1..c69c029 100644 --- a/frontend/package.json +++ b/theme/package.json @@ -1,7 +1,19 @@ { - "name": "@airgate/theme", - "version": "0.1.0", + "name": "@doudou-start/airgate-theme", + "version": "1.0.0", + "description": "AirGate 插件前端主题、样式隔离和公共组件包", "type": "module", + "packageManager": "npm@11.6.2", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/DouDOU-start/airgate-sdk.git", + "directory": "theme" + }, + "publishConfig": { + "registry": "https://registry.npmjs.org", + "access": "public" + }, "main": "dist/index.js", "types": "dist/index.d.ts", "exports": { @@ -31,6 +43,7 @@ ], "scripts": { "build": "tsc", + "prepack": "npm run build", "dev": "tsc --watch" }, "peerDependencies": { diff --git a/frontend/src/css.ts b/theme/src/css.ts similarity index 100% rename from frontend/src/css.ts rename to theme/src/css.ts diff --git a/frontend/src/helpers.ts b/theme/src/helpers.ts similarity index 97% rename from frontend/src/helpers.ts rename to theme/src/helpers.ts index 29f44fa..667773e 100644 --- a/frontend/src/helpers.ts +++ b/theme/src/helpers.ts @@ -9,7 +9,7 @@ const defaultThemeCssVarMap = createThemeCssVarMap(); const defaultStaticCssVarMap = createStaticCssVarMap(); /** - * 获取带 fallback 的 CSS var() 引用。 + * 获取带默认值的 CSS var() 引用。 * 同时支持主题 token 和静态 token。 * * @example diff --git a/frontend/src/index.ts b/theme/src/index.ts similarity index 57% rename from frontend/src/index.ts rename to theme/src/index.ts index ab17108..79a5f71 100644 --- a/frontend/src/index.ts +++ b/theme/src/index.ts @@ -48,7 +48,14 @@ export type { TokenName } from './helpers.js'; export { cssVar, themeStyle } from './helpers.js'; export type { + AccountSurfaceProps, AccountFormProps, + BadgeProps, + ButtonProps, + ButtonVariant, + CardProps, + FieldProps, + FormActionsProps, PluginBatchAccountInput, PluginBatchImportResult, PluginFrontendModule, @@ -57,6 +64,43 @@ export type { PluginOAuthBridge, PluginOAuthExchangeResult, PluginOAuthStartResult, + PanelHeaderProps, PluginPlatformIconProps, + PluginStatusKind, + PluginStyleFoundationOptions, + PluginTailwindConfigOptions, PluginRouteDefinition, + ResolvePluginThemeOptions, + ScopedPluginThemeOptions, + SectionProps, + SelectableCardProps, + StatusTextProps, + UsageRecordSurfaceProps, +} from './plugin.js'; + +// 插件前端 SDK:样式注入、主题同步、Tailwind bridge 和公共组件 +export { + DEFAULT_PLUGIN_CLASS_PREFIX, + DEFAULT_PLUGIN_FOUNDATION_STYLE_ID, + DEFAULT_PLUGIN_THEME_ATTRIBUTE, + DEFAULT_PLUGIN_THEME_STYLE_ID, + Badge, + Button, + Card, + Field, + FormActions, + PanelHeader, + Section, + SecretInput, + SelectableCard, + StatusText, + TextInput, + TextArea, + cn, + createPluginTailwindConfig, + ensurePluginStyleFoundation, + injectStyle, + pluginFoundationCssText, + resolvePluginTheme, + useScopedPluginTheme, } from './plugin.js'; diff --git a/frontend/src/plugin.tsx b/theme/src/plugin.tsx similarity index 91% rename from frontend/src/plugin.tsx rename to theme/src/plugin.tsx index 57812a6..1e5e250 100644 --- a/frontend/src/plugin.tsx +++ b/theme/src/plugin.tsx @@ -19,7 +19,6 @@ export const DEFAULT_PLUGIN_FOUNDATION_STYLE_ID = 'ag-plugin-foundation'; export interface PluginStyleFoundationOptions { scopeSelector: string; themeAttribute?: string; - storageKey?: string; themeStyleId?: string; foundationStyleId?: string; extraCssText?: string; @@ -113,10 +112,33 @@ export interface PluginPlatformIconProps { style?: CSSProperties; } +export interface AccountSurfaceProps { + accountId?: string | number; + accountType?: string; + context?: Record; +} + +export interface UsageRecordSurfaceProps { + recordId?: string | number; + context?: Record; +} + export interface PluginFrontendModule { routes?: PluginRouteDefinition[]; menuItems?: PluginMenuItemDefinition[]; - accountForm?: ComponentType; + accountIdentity?: ComponentType; + accountCreate?: ComponentType; + accountEdit?: ComponentType; + accountUsageWindow?: ComponentType; + /** + * 使用记录列表中“模型”列的行级别扩展渲染器。 + * + * 用途:为平台补充展示模型分档信息(例如图像分辨率、推理强度、服务层级等), + * Core 只提供表格骨架与回退渲染,不内置平台语义。 + */ + usageModelMeta?: ComponentType; + usageMetricDetail?: ComponentType; + usageCostDetail?: ComponentType; platformIcon?: ComponentType; } @@ -442,14 +464,15 @@ export const pluginFoundationCssText = ` } `; -export function injectStyle(id: string, cssText: string, targetDocument: Document = document): void { - if (typeof document === 'undefined') return; +export function injectStyle(id: string, cssText: string, targetDocument?: Document): void { + const doc = targetDocument || (typeof document !== 'undefined' ? document : undefined); + if (!doc) return; - let element = targetDocument.getElementById(id) as HTMLStyleElement | null; + let element = doc.getElementById(id) as HTMLStyleElement | null; if (!element) { - element = targetDocument.createElement('style'); + element = doc.createElement('style'); element.id = id; - targetDocument.head.appendChild(element); + doc.head.appendChild(element); } if (element.textContent !== cssText) { @@ -562,7 +585,7 @@ export function cn(...values: Array): string return values.filter(Boolean).join(' '); } -interface FieldProps { +export interface FieldProps { label: ReactNode; required?: boolean; hint?: ReactNode; @@ -595,7 +618,7 @@ export function TextArea({ className, ...props }: TextareaHTMLAttributes; } -interface PanelHeaderProps { +export interface PanelHeaderProps { title: ReactNode; description?: ReactNode; eyebrow?: ReactNode; @@ -612,7 +635,7 @@ export function PanelHeader({ title, description, eyebrow, className }: PanelHea ); } -interface SectionProps extends PanelHeaderProps { +export interface SectionProps extends PanelHeaderProps { children: ReactNode; panel?: boolean; contentClassName?: string; @@ -635,7 +658,7 @@ export function Section({ ); } -interface CardProps { +export interface CardProps { children: ReactNode; className?: string; } @@ -644,7 +667,7 @@ export function Card({ children, className }: CardProps) { return
{children}
; } -interface SelectableCardProps extends ButtonHTMLAttributes { +export interface SelectableCardProps extends ButtonHTMLAttributes { active?: boolean; } @@ -660,7 +683,7 @@ export function SelectableCard({ active = false, className, children, ...props } ); } -type ButtonVariant = 'primary' | 'secondary' | 'outline'; +export type ButtonVariant = 'primary' | 'secondary' | 'outline'; const buttonClassMap: Record = { primary: 'agw-button-primary', @@ -668,7 +691,7 @@ const buttonClassMap: Record = { outline: 'agw-button-outline', }; -interface ButtonProps extends ButtonHTMLAttributes { +export interface ButtonProps extends ButtonHTMLAttributes { variant?: ButtonVariant; } @@ -684,7 +707,7 @@ export function Button({ variant = 'secondary', className, children, ...props }: ); } -interface FormActionsProps { +export interface FormActionsProps { children: ReactNode; className?: string; } @@ -693,7 +716,7 @@ export function FormActions({ children, className }: FormActionsProps) { return
{children}
; } -interface BadgeProps { +export interface BadgeProps { children: ReactNode; tone?: 'neutral' | 'success' | 'violet' | 'info'; className?: string; @@ -710,7 +733,7 @@ export function Badge({ children, tone = 'neutral', className }: BadgeProps) { return {children}; } -interface StatusTextProps { +export interface StatusTextProps { type: PluginStatusKind; text: string; } diff --git a/frontend/src/tailwind.ts b/theme/src/tailwind.ts similarity index 100% rename from frontend/src/tailwind.ts rename to theme/src/tailwind.ts diff --git a/frontend/src/tokens.ts b/theme/src/tokens.ts similarity index 99% rename from frontend/src/tokens.ts rename to theme/src/tokens.ts index 240d998..5fcba6e 100644 --- a/frontend/src/tokens.ts +++ b/theme/src/tokens.ts @@ -160,7 +160,7 @@ export const staticTokenGroups: StaticTokenGroups = { appShell: appShellTokens, }; -/** 不随主题变化的静态 token(向后兼容的扁平导出) */ +/** 不随主题变化的静态 token */ export const staticTokens: StaticTokens = { ...foundationTokens, ...appShellTokens, diff --git a/frontend/src/types.ts b/theme/src/types.ts similarity index 97% rename from frontend/src/types.ts rename to theme/src/types.ts index 47e8c1c..41a9ffe 100644 --- a/frontend/src/types.ts +++ b/theme/src/types.ts @@ -89,7 +89,7 @@ export interface AppShellTokens { topbarHeight: string; } -/** 不随主题变化的 token(保持向后兼容的扁平结构) */ +/** 不随主题变化的 token */ export interface StaticTokens extends FoundationTokens, AppShellTokens {} export interface StaticTokenGroups { diff --git a/frontend/tsconfig.json b/theme/tsconfig.json similarity index 100% rename from frontend/tsconfig.json rename to theme/tsconfig.json