> ## Documentation Index
> Fetch the complete documentation index at: https://gproxy.leenhawk.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 新增渠道

> v4 渠道的结构：BaseChannel 契约、可选能力 trait、从 Cargo feature 到完成注册的步骤，以及如何在本仓库之外发布渠道。

本文面向需要让 GPROXY 接入一个尚未支持的上游的贡献者，无论渠道放在本仓库里，还是放在自己的 crate 里。

一个渠道对应一族上游，负责这些事：上游的 URL、凭证的注入方式、支持哪些线协议方言、怎样把响应整形成各操作的标准响应（宿主从这里读取用量），以及账号型上游的登录、刷新和配额查询。路由、凭证选择、失败转移、协议转换、结算和 capture 由引擎负责。渠道不读数据库，也不选择传输后端。

渠道在编译时链接，不在运行时加载。渠道可以放在两个位置：

* 本仓库：作为 `gproxy-channel` 的一个模块，挂在独立的 Cargo feature 后面，通过 PR 合入。合入后所有宿主都能使用，包括 Workers 宿主。见[向本仓库添加渠道](#向本仓库添加渠道)。
* 自己的 crate：一个按公开契约实现 `BaseChannel` 的类型，链接进一个除此之外与原版相同的原生二进制或 Worker。本仓库无需任何改动。见[在本仓库之外](#在本仓库之外)。

两种方式实现的是同一个 trait，写法相同，区别只在注册方式。默认不编译任何具体渠道。

## 什么时候需要渠道

渠道要跟着别人的线协议持续维护，所以只有在一行供应商配置表达不了上游的需求时才值得新增：路径随操作变化、请求体需要改写、有账号接口要读取，或者要模拟某个客户端身份。

如果差别只是源地址和一个请求头，用 `custom` 供应商即可，见[不需要渠道的厂商](https://gproxy.leenhawk.com/zh-cn/guides/providers.md#接入兼容服务)。

## BaseChannel 契约

`BaseChannel` 是唯一必须实现的 trait，`id` 是其中唯一必须实现的方法。每个协议操作都有独立的异步方法和默认实现：用 `prepare`（HTTP）或 `prepare_connect`（WebSocket）构造请求，再交给宿主分配的 client 发送。

两个准备钩子默认都返回“不支持”。因此渠道支持的操作就是它准备或覆写的那些，没有别的。

```text theme={null}
宿主选定供应商、凭证、client
  → ChannelBinding::new(&channel, provider, credential, client)
  → binding.send(OperationKey, WireRequest<HttpBody>)
    binding.connect(OperationKey, WireRequest<()>)
  → BaseChannel 上那个具名操作方法
  → 默认：prepare + client.send  /  prepare_connect + client.connect
    或渠道自己经同一个 client 的多次调用流程
```

宿主传入的是借用的视图，能公开的字段都公开：

* `ProviderView`：`id`、`channel`、可选的 `base_url`，以及由渠道自行解码为类型化设置的 `config` JSON。
* `CredentialView`：`id`、`provider_id`、`auth_kind`、`secret` JSON（不实现 `Debug` 和 `Serialize`）、宿主记录的公开 `metadata`、`version`、`expires_at_ms`。
* `PrepareContext`：`prepare` 和 `prepare_connect` 的参数，包含两个视图、原生 `OperationKey`、`WireRequest` 和可选的端点覆盖。
* `OperationContext`：操作方法的参数，包含两个视图、操作的方言、`WireRequest`、一个归调用方持有的 client、跨请求状态、宿主实例 id 和可选的端点覆盖。
* `CredentialContext`：供应商、凭证和 client，用于刷新、配额和服务。

除 `prepare` 外，渠道通常还要覆写 `native_dialects`（上游对某个操作原生接受的方言；返回空表示交给配置决定）。如果渠道也接受其他方言的请求，再覆写 `default_conversion_target`，指定宿主在调用 `prepare` 之前把请求转换成哪种方言。

### 准备与错误的规则

* 准备是同步的纯函数：不做 I/O，不保留隐藏状态，不构造 client。操作覆写可以发起多次交换，但只能通过 `context.client`；响应流结束后也可以继续持有 client 完成收尾，例如释放厂商侧的会话。
* 来源鉴权不会到达上游。转发助手会丢弃逐跳请求头，以及 `host`、`content-length`、`authorization`、`x-api-key`、`x-goog-api-key` 和 `api-key`；`strip_query_auth` 会从查询串中删除 `key`、`access_token` 和 `api_key`。之后由渠道根据凭证加上自己的鉴权。
* 转发的客户端请求头取全局白名单、供应商 `allowed_headers` 与渠道声明头的并集，`content-type` 始终保留。列表缺失或为空时不增加任何允许项。
* 端点覆盖是完整的方法 URL，同时替换 base URL 和渠道的默认路径，不会再往后拼接。
* 非 2xx 应答作为响应返回，状态码、请求头和惰性读取的 body 保持原样。错误变体只用于能力调用（登录、刷新、配额），以及多次调用覆写中的中间交换，因为那里失败的应答不是最终返回的响应。
* `RefreshRejected` 表示上游明确拒绝了这把凭证（`invalid_grant`、已撤销）。宿主默认把凭证标记为失效。渠道可以覆写 `persist_rejection`，只针对当前内存中的凭证版本记住某个拒绝原因，下次尝试前重新加载持久化的材料。短暂的传输失败或 5xx 必须作为传输错误返回，让宿主稍后重试；误用 `RefreshRejected` 会让一个正常的账号失效。
* `ContinuationElsewhere` 表示一条活动的上游连接由另一个宿主进程持有。宿主会把请求转到那个实例，凭证本身没有问题。

## 可选能力

每项能力都是独立的 trait，通过默认返回 `None` 的访问器提供。渠道按上游实际提供的功能实现，各项之间互不依赖，例如可以报告配额窗口而不提供重置。

| 访问器 | 用途 |
| - | - |
| `credential_refresh` | 产出完整替换的密钥、过期时间和可选的公开 metadata；宿主以 `version` 上的 CAS 写回 |
| `oauth_authorization_code` | 用宿主提供的 PKCE challenge 和 state 构造授权 URL，并用 code 换取凭证。上游客户端生成的 verifier 不是 32 字节时，覆写 `pkce_verifier_bytes` |
| `oauth_device_code` | `start` 和单步 `poll`，轮询节奏由宿主安排 |
| `quota_model` | 同步纯函数：根据凭证的 `auth_kind` 和 metadata 给出它有哪些配额维度 |
| `quota_query` | 从上游的用量接口读取配额快照 |
| `quota_headers` | 把响应头转换为配额条目；为空表示上游未报告 |
| `quota_reset` | 在出售重置额度的上游上查询额度并手动重置 |
| `usage_extras` | 厂商自有的用量字段，作为宿主从整形响应中读取的标准用量的补充 |
| `response_reason_observer` | 对原始上游响应分类（限流、过载、拒绝等）。默认实现解析标准 JSON 和 SSE，只有私有线格式才需要覆写 |
| `websocket_response_reason` | 同上，针对一条完整的入站 WebSocket 消息 |
| `services` | 厂商控制面路由，见 [CLI 客户端](https://gproxy.leenhawk.com/zh-cn/guides/cli-clients.md) |

`classify_rejection` 是 `BaseChannel` 上的普通方法，不是访问器。宿主对每个非 2xx 应答调用它，并传入 body 的有限前缀；2xx 流以错误帧开头时也会调用。它返回这次拒绝对凭证意味着什么（限流、配额耗尽、余额不足、未认证、不提供该模型，或请求本身有误），上游给出等待时间时一并返回。默认实现只读取结构化的错误字段，需要识别厂商自己的措辞时再覆写。

### 凭证与刷新

运维已有的密钥走普通的凭证新增和更新接口，用 `validate_credential_secret` 校验格式。如果材料在首次使用前需要联网换取（例如尚未换取令牌的服务账号密钥），由 `initial_credential_expiry` 返回 `Some(0)`，并通过 `CredentialRefresh` 实现换取过程。

刷新返回完整替换的密钥和过期时间，以及可选的公开 metadata（`None` 表示保留原值）。宿主用同一个版本 CAS 一起保存这三项。密钥不得放进 metadata。

`CredentialRefresh::proactive_refresh_reason` 决定请求时是否续期。默认在访问凭据五分钟内过期，或刷新凭据（密钥中的 `refresh_expires_at_ms`）一天内过期时续期。渠道可以覆写它以匹配上游的节奏；令牌接口接受续期原因时，再实现 `refresh_with_reason`。`response_update` 用于从上游响应头中获取轮换后的凭证；即使不接受任何更新，也必须从响应中删除含密钥的响应头。

用量不需要任何能力：宿主从渠道整形后的标准响应中读取。不报告用量的渠道记为“未报告”，而不是 0。

### descriptor 能力与状态

有两项 descriptor 能力无法从访问器推断，具备的渠道需要在自己的 `descriptor` 中声明：`websocket`（覆写了某个 socket 操作）和 `paid_usage`（其 `QuotaModel::allows_paid_usage` 读取凭证 metadata 中的 `allow_paid_usage` 标志，控制台据此显示对应开关）。

跨请求状态由宿主限定在一个供应商加一把凭证的范围内，并以 CAS 写入。binding 的默认状态拒绝一切写入，所以渠道只有在宿主提供状态时才能使用。

### 登录状态（`provider_state`）

两种 OAuth 流程都带有一个 `provider_state` 映射，存放渠道在用户完成授权之后还需要的信息：`start`（或 `authorize`）放进去的内容会原样交回给 `poll`（或 `exchange`）。内容由渠道自行决定，例如非标准的设备句柄，或登录过程为自己注册的 client。

与宿主作为凭证 metadata 公开的 `provider_fields` 不同，这个映射可以存放密钥。它只保存在宿主的登录会话中，会话生命周期短、存于缓存、从不渲染，登录结束即丢弃。需要在登录之后继续保留的信息必须由 exchange 返回：公开信息放进 `provider_fields`（会成为 metadata），机密信息放进 `provider_secrets`，后者与令牌一起加密封存，不会成为 metadata。

## 向本仓库添加渠道

以 `custom`（API key）和 `codex`（OAuth 账号）为参考。最近新增的 `opendesign` 是一个小型 API key 渠道，可以从它的提交看到新渠道涉及的全部文件。

1. 在 `crates/gproxy-channel/Cargo.toml` 中声明 feature，只列出该渠道需要的可选依赖。仅 wasm 需要的依赖放在 `cfg(target_arch = "wasm32")` 的 target 表下。

   ```toml theme={null}
   [features]
   # 一行说明上游是什么、这个渠道覆盖到哪。
   acme = ["dep:base64", "dep:web-time"]
   ```

2. 在 `src/channels/mod.rs` 的 `channels!` 列表中加一行：feature、模块，以及宿主要注册的值。这一行同时声明模块并把渠道加入 `compiled_in()`，所有宿主都通过它找到渠道。

   ```rust theme={null}
   "acme" => acme, acme::Acme;
   ```

   注册值是一个表达式，需要构造的渠道写成 `claudeweb::ClaudeWeb::new()`；一个模块在同一套线协议上服务两个产品时，两个值都列出（`opencode::OpenCode::ZEN, opencode::OpenCode::GO`）。如果渠道用到 `shared`，还要把它的 feature 加进该模块的 `cfg(any(...))`。

3. 按“一个文件一件事”组织模块。小渠道只有一个 `acme.rs`，大渠道用目录：

   ```text theme={null}
   src/channels/acme/
     mod.rs        id、单元结构体、re-export、模块文档
     config.rs     供应商的 `config` JSON，serde(default)，忽略未知键
     request.rs    prepare / prepare_connect / 被覆写的操作
     oauth.rs      登录与刷新能力
     quota.rs      配额能力
     usage.rs      用量能力
     services.rs   厂商控制面路由
   ```

   模块文档要写明每一条线协议事实的来源：厂商的 API 参考，或 `samples/` 下的 CLI 源码。其他渠道的代码不能作为来源。

4. 实现 `BaseChannel`。最少需要 `id`、`native_dialects` 和 `prepare`。结构体是无状态的单元结构体，一个实例服务该渠道下的所有供应商。

   同时覆写 `descriptor`。默认实现会根据访问器填好能力，但显示名称以及从供应商 `config` JSON 中解码哪些键，只有渠道自己知道。管理界面只依据 descriptor 渲染供应商表单（启用 `ts` feature 时它也是一个 TypeScript 类型），descriptor 中没有的键就无法在界面上设置。追加 `HOST_CONFIG_KEYS` 来声明宿主自己的键。

   当上游需要多次交换、本地合成的应答、另一种传输方式，或需要把响应改写为声明的原生方言时，覆写具体的操作方法，而不是 `prepare`。

5. 声明厂商客户端发送的内容。如果渠道模拟某个 CLI，导出它的请求头常量并据此构造白名单，这样供应商的 `allowed_headers` 去不掉 CLI 自己的请求头。同时把这些身份请求头传给转发助手的丢弃列表，防止客户端伪造。上游会对客户端做指纹识别时，由 `default_connection` 返回默认连接配置；凭证或供应商上显式指定的配置仍然优先。

6. 把能力实现为独立的类型，由访问器返回。每个 trait 都有自己的规则：刷新返回完整替换，不做合并；配额维度来自凭证 metadata 中的套餐信息，不访问网络；用量观察者做累积快照，它的 `finish` 由宿主告知流是完整结束还是被中断，因为读到 EOF 并不代表用量完整。

7. 可复用的线协议机制放在 `src/channels/shared/`，按使用它们的 feature 条件编译。策略留在渠道中，shared 模块只执行渠道要求的操作。

8. 测试放在代码旁边的 `tests/<id>.rs`，用 `#![cfg(feature = "…")]` 控制。手工构造供应商和凭证视图，用测试密钥调用 `prepare`，断言 URL、注入的鉴权和被丢弃的请求头。把抓取的帧交给用量观察者，解析录制的配额请求头和用量 body。多次调用覆写用脚本化的出站 client 测试。这里不测试引擎。

9. 把 feature 转发到每个宿主。引擎本身不列举渠道：`gproxy-sdk` 注册 `compiled_in()` 返回的全部渠道，所以只有当二进制的 feature 集合启用了渠道的 feature 时，二进制才能使用它。用同一个名字把 feature 加到：

   | 文件 | 改动 |
   | - | - |
   | `crates/gproxy-sdk/Cargo.toml` | `acme = ["gproxy-channel/acme"]`，并把 `"acme"` 加进 `channels` 列表 |
   | `crates/gproxy/Cargo.toml` | `acme = ["gproxy-sdk/acme"]` |
   | `crates/gproxy-host-edge/Cargo.toml` | `acme = ["gproxy-sdk/acme"]` |
   | `crates/gproxy-app/src/v3/channels.rs` | 把 id 加进 `V4_CHANNELS`（并修改数组长度），使 v3 导入接受它 |

   注册时重复的 id 会被拒绝。供应商引用未注册的渠道属于配置错误，不会回退到其他渠道。

10. 补充文档：在 `crates/gproxy-channel/README.md` 的 feature 表中加一行，并在文档子模块 `guides/providers.mdx` 的“支持的渠道”表中加一行，四种语言都要改。

### 检查构建

```sh theme={null}
cargo fmt --all
cargo clippy -p gproxy-channel --all-features --all-targets -- -D warnings
cargo clippy -p gproxy-channel --all-features --target wasm32-unknown-unknown --lib -- -D warnings
cargo test   -p gproxy-channel --all-features
```

`gproxy-channel` 默认不启用任何渠道，所以这些命令必须带 `--all-features`，否则一个渠道都不会编译。每个渠道都必须能为原生目标和 `wasm32-unknown-unknown` 构建，Workers 宿主因此可以带上完整的渠道集合。lint 报告的问题要通过修改代码解决，不要加 `#[allow]`。

合入后不需要其他步骤：新渠道上的供应商会连同渠道的 descriptor 出现在 `GET /admin/api/channels` 中，控制台据此渲染表单。文档中的渠道参考由 CI 根据已注册的渠道重新生成。

## 在本仓库之外

渠道可以放在独立的 crate 中，不必 fork 就能发布，两种宿主都支持。原生二进制仍是原版 `gproxy`（命令、参数、控制台和数据库都相同），Worker 仍是原版 GPROXY Worker，只是在内置渠道之外多注册了你的渠道。`examples/channel-plugin` 是一套完整可运行的三个 crate：一个文件实现的 OpenAI 兼容 API key 渠道，以及每个宿主几行代码的入口。CI 每次改动都会构建这个示例，所以它与当前契约保持一致。

### 目录结构

```text theme={null}
my-gproxy/
  channel/    渠道：一个实现 BaseChannel 的单元结构体，不依赖任何宿主
  native/     gproxy 二进制，带上它
  edge/       Cloudflare Worker，带上它（只在部署到那里时需要）
```

渠道单独成为一个 crate，两个宿主才能都依赖它。渠道不能依赖原生宿主：`gproxy` 无法编译到 wasm，Worker 也会因此无法编译。

```rust theme={null}
// native/src/main.rs
use std::{process::ExitCode, sync::Arc};

fn main() -> ExitCode {
    gproxy::main_with(vec![Arc::new(my_channel::Acme)])
}
```

`main_with` 就是完整的原版 `main`：加载 `.env`，解析相同的参数，建立运行时并启动。自带运行时的程序改为调用 `gproxy::run_with(cli, channels)`。

```rust theme={null}
// edge/src/lib.rs
#[cfg(target_arch = "wasm32")]
mod entry {
    use std::sync::Arc;

    use worker::{Context, Env, HttpRequest, event};

    #[event(fetch)]
    async fn fetch(
        request: HttpRequest,
        env: Env,
        ctx: Context,
    ) -> worker::Result<gproxy_host_edge::Response> {
        gproxy_host_edge::fetch_with(request, env, ctx, || vec![Arc::new(my_channel::Acme)])
            .await
    }
}
```

`fetch_with` 就是完整的原版 fetch handler。一个 Worker 模块只能有一个 `#[event(fetch)]`，所以 edge crate 依赖 `gproxy-host-edge` 时要关闭它的 `entry` feature，原版 handler 正是由这个 feature 声明的。闭包在每个 isolate 中只运行一次，即第一个请求组装实例时。

在两种宿主中，`id` 与内置渠道相同的渠道都会替换该内置渠道。要发布修改过的现有渠道，就用这种方式。

### 依赖

按路径依赖一份 gproxy checkout，版本与你要构建的目标一致：

```toml theme={null}
# channel/Cargo.toml
[dependencies]
gproxy-channel = { path = "../../gproxy/crates/gproxy-channel" }
gproxy-protocol = { path = "../../gproxy/crates/gproxy-protocol", features = ["exhaustive"] }
http = "1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
```

```toml theme={null}
# native/Cargo.toml
[dependencies]
my-channel = { path = "../channel" }
gproxy = { path = "../../gproxy/crates/gproxy", features = ["channels", "memory", "fs", "embedded-console"] }
```

```toml theme={null}
# edge/Cargo.toml
[lib]
crate-type = ["cdylib", "rlib"]

[target.'cfg(target_arch = "wasm32")'.dependencies]
my-channel = { path = "../channel" }
gproxy-host-edge = { path = "../../gproxy/crates/gproxy-host-edge", default-features = false, features = ["channels", "d1", "libsql"] }
worker = { version = "0.8", default-features = false, features = ["http"] }
```

```toml theme={null}
# Cargo.toml，workspace 根
[workspace]
resolver = "3"
members = ["channel", "native", "edge"]

# 从 checkout 根目录的 Cargo.toml 复制：patch 只在声明它的 workspace 里生效，
# 而构建依赖这些。
[patch.crates-io]
sqlx-sqlite = { path = "../gproxy/vendor/sqlx-sqlite" }
btls-sys = { git = "https://github.com/0x676e67/btls", rev = "609f955e6de4df4a215ee9b273d327af1c0771db" }
```

checkout 自己的 patch 表里还有 `glib`，只有桌面宿主会用到，仓库外的 workspace 不需要这一项。第一次构建前，把 checkout 的 `Cargo.lock` 复制到根 `Cargo.toml` 旁边，使依赖解析到 gproxy 测试过的版本。

之所以使用 checkout 而不是 git 依赖，是因为原生控制台从 `crates/gproxy-host-axum/assets/web` 编译进二进制，而这个目录没有提交到仓库。先在 checkout 的 `console/` 中执行 `pnpm install && pnpm build`；或者从 features 中去掉 `embedded-console`，构建不带控制台的二进制。下面的 Worker 构建同样从 checkout 获取控制台。

`gproxy` 和 `gproxy-host-edge` 的 feature 列表用法与原版构建相同：`channels` 启用全部内置渠道，也可以只列出部署要用的渠道。

### 构建 Worker

checkout 中的 `deploy/cloudflare` 可以构建任意 Worker crate，不限于原版。用绝对路径指向你的 crate，然后照常部署：

```sh theme={null}
cd ../gproxy/deploy/cloudflare
pnpm install
GPROXY_EDGE_CRATE=/absolute/path/to/my-gproxy/edge pnpm run build
pnpm run deploy
```

这一步会在你的 crate 中运行 `worker-build`，去掉调试段，再把 Worker 复制到 checkout 的 `wrangler.toml`、控制台资源及其 headers 文件旁边。[边缘部署](https://gproxy.leenhawk.com/zh-cn/deployment/edge.md)中的内容（D1 绑定、密钥、`GPROXY_CONFIG`）全部照常适用。

用于 Worker 的渠道必须能编译到 `wasm32-unknown-unknown`，所有内置渠道都满足这一点。具体来说：不使用 `tokio` 网络，不访问文件系统，用 `web-time` 代替 `std::time::Instant`，所有请求都通过宿主提供的 client 发送。用 `cargo check --target wasm32-unknown-unknown -p my-channel` 检查。

### 公开契约

渠道实现或接收的一切都在 `gproxy_channel::channel` 中公开：`BaseChannel`、上下文与视图类型、所有能力 trait、descriptor 类型（包括 `HOST_CONFIG_KEYS`），以及构造请求所需的助手：处理客户端请求头的 `forwardable` 和 `HeaderAllowlist`、写入运维配置的静态请求头的 `insert_configured`、清理查询串的 `strip_query_auth`，以及 `standard_reason_observer`。协议词汇（`Dialect`、`Operation`、`OperationKey`、`HttpBody`、`WireRequest`）来自 `gproxy_protocol`。

`gproxy_channel::channels::shared` 不公开。它针对本仓库的渠道编写，会随这些渠道一起变化，需要时复制过去。

[契约中的规则](#准备与错误的规则)同样适用，尤其是准备必须是纯函数、来源鉴权不会到达上游。

### 与内置渠道的区别

* 控制台与对待其他渠道一样，根据 descriptor 渲染你的渠道：供应商表单来自 `config_keys`，凭证表单来自 `credential_kinds`，登录按钮来自 `login_modes`，配额、重置、刷新和付费用量控件来自 `capabilities`。缺少的是为内置 id 单独编写的内容，例如定制的提示文字或模型预设组。
* 原生二进制的自更新会拒绝安装。发布的版本是本仓库的构建，装上之后会丢掉你的渠道，下次重启后其上的所有供应商都会失效。请基于新版本重新构建。检查更新功能照常可用。
* 契约随 gproxy 的版本变化。实际变更都是增量的（多数协议类型带 `#[non_exhaustive]`，`BaseChannel` 新增的方法都有默认实现），但仍应基于你实际运行的版本构建。写 `ChannelCapabilities { quota_query: true, ..Default::default() }`，不要逐个列出字段，这样新增能力不会导致构建失败。

## 给编码 Agent 的清单

与上面相同的步骤，按 agent 执行的顺序列出。英文版本节标题为 “Checklist for Coding Agents”，仓库的 `AGENTS.md` 指向它。

内置渠道（向本仓库提 PR）：

1. 以 `crates/gproxy-channel/src/channels/nvidia.rs`（API key）或 `codex/`（OAuth）为模板，并阅读 `samples/` 下厂商的线协议源码或其 API 参考。
2. 在 `crates/gproxy-channel/Cargo.toml` 中加 feature，在 `src/channels/mod.rs` 的 `channels!` 中加一行，并把 feature 加进渠道用到的 `src/channels/shared/` 中的每个 `cfg(any(...))`。
3. 编写 `src/channels/<id>.rs` 或 `src/channels/<id>/`，覆写 `descriptor`。
4. 在第 9 步列出的四处转发 feature：`crates/gproxy-sdk/Cargo.toml`（feature 和 `channels` 列表）、`crates/gproxy/Cargo.toml`、`crates/gproxy-host-edge/Cargo.toml`，以及 `crates/gproxy-app/src/v3/channels.rs` 中的 `V4_CHANNELS`（修改数组长度）。
5. 如果渠道支持 `allow_paid_usage` 或提供 WebSocket，在 descriptor 的 capabilities 中设置 `paid_usage` / `websocket`。
6. 在 `crates/gproxy-channel/README.md` 中加一行，并在 `docs/guides/providers.mdx`、`docs/zh-cn/guides/providers.mdx`、`docs/zh-tw/guides/providers.mdx`、`docs/ru/guides/providers.mdx` 的“支持的渠道”表中加一行（文档改动在子模块中提交）。
7. 运行 `pnpm --dir docs generate:reference`，把已注册的渠道导出到参考文档。不要提交生成的 JSON，CI 会导出并提交。
8. 运行[检查构建](#检查构建)中的命令。

仓库外的渠道：

1. 把 `examples/channel-plugin` 复制到 checkout 之外：`channel/` 必须复制，`native/` 和 `edge/` 按要部署的宿主选择。加上前文带 `[patch.crates-io]` 表的 workspace 根 `Cargo.toml`，并复制 `Cargo.lock`。
2. 在每个 crate 的 `Cargo.toml` 中，用包自己的 `version` 和 `edition = "2024"` 替换 `*.workspace = true` 这些键，并把每个 `workspace = true` 依赖改为前文所示的路径依赖。
3. 重命名包、二进制、结构体和 `ID`。除非就是要替换某个内置渠道，否则选一个内置渠道没有用过的 id。
4. 只基于 `gproxy_channel::channel` 实现，不要使用 `channels::shared`。
5. 运行 `cargo clippy --all-targets -- -D warnings`；部署 Worker 时再运行 `cargo check --target wasm32-unknown-unknown -p <channel> -p <edge>`。
6. 在全新的 `GPROXY_DATA_DIR` 上运行原生二进制，或按前文构建后在 `deploy/cloudflare` 中用 `wrangler dev --local` 运行 Worker，确认 `GET /admin/api/channels` 列出了这个 id。


## Related topics

- [从源码构建](https://gproxy.leenhawk.com/zh-cn/development/building.md)
- [嵌入 SDK](https://gproxy.leenhawk.com/zh-cn/development/embedding.md)
- [渠道能力目录](https://gproxy.leenhawk.com/zh-cn/reference/channel-catalog.md)
- [供应商与凭证](https://gproxy.leenhawk.com/zh-cn/guides/providers.md)
- [基本概念](https://gproxy.leenhawk.com/zh-cn/introduction/concepts.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.