Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions crates/tw-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -415,10 +415,11 @@ pub struct RuleView {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RouteView {
pub name: String,
/// 对所有密钥生效
/// 没绑路由的密钥走的就是这条
pub default: bool,
/// 分到这条路由的密钥名。**默认路由这里是空的** —— 它不需要分配,
/// 而列出「所有密钥」会让人以为那是一次显式的分配。
/// **显式绑了这条路由的密钥。**默认路由这里通常是空的 —— 走它的人
/// 是「没绑」,不是「绑了它」,而把所有密钥列进来会让人以为那是
/// 一次次显式的选择。
pub clients: Vec<String>,
pub rules: Vec<RuleView>,
}
Expand Down
29 changes: 17 additions & 12 deletions crates/tw-config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@ pub struct Config {
/// 不写进文件(`generate_initial` 的六行里没有它)。
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub routes: Vec<tw_engine::RouteSet>,
/// 没绑路由的密钥走哪条。不写就是叫「默认」的那条。
///
/// **它是顶层的一个名字,不是某条路由身上的标志** —— 结构上就唯一,
/// 不需要一条「最多一条默认」的校验去维持。
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_route: Option<String>,
}

/// 便于构造,**不代表一份可用的配置** —— `providers` 和 `clients` 都是
Expand All @@ -99,6 +105,7 @@ impl Default for Config {
limits: Limits::default(),
groups: Vec::new(),
routes: Vec::new(),
default_route: None,
}
}
}
Expand All @@ -111,7 +118,7 @@ impl Default for Client {
key: String::new(),
max_concurrent: None,
allow: None,
routes: Vec::new(),
route: None,
}
}
}
Expand Down Expand Up @@ -141,13 +148,13 @@ impl Config {
self.providers.iter().map(|p| p.name.clone()).collect(),
self.groups.clone(),
self.routes.clone(),
// 密钥 → 它分到的路由。**引擎不认识密钥这个概念** ——
// 它只需要「这个名字要过哪几条路由」,所以映射在这里拍平,
self.default_route.clone(),
// 密钥 → 它绑的那条路由。**引擎不认识密钥这个概念** ——
// 它只需要「这个名字走哪条路由」,所以映射在这里拍平,
// 而不是把整个 `clients` 交进去。
self.clients
.iter()
.filter(|c| !c.routes.is_empty())
.map(|c| (c.name.clone(), c.routes.clone()))
.filter_map(|c| c.route.clone().map(|r| (c.name.clone(), r)))
.collect(),
)
}
Expand Down Expand Up @@ -323,14 +330,12 @@ pub struct Client {
/// - 写 `[]` → **一个都不给**。「临时禁用这个客户端」的正当用法
#[serde(default, skip_serializing_if = "Option::is_none")]
pub allow: Option<Vec<String>>,
/// 这把密钥额外走哪几条路由,按写的顺序求值。
/// 这把密钥走哪条路由。**一把密钥一条路由。**
///
/// **默认路由不用写在这里** —— 它对每一把密钥都生效。这里写的是
/// 「在默认之上,这把密钥还要过哪些规则」。
///
/// 一条路由可以分给多把密钥,不用复制。
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub routes: Vec<String>,
/// 不写就走默认路由(顶层的 `default_route`)。一条路由可以绑给
/// 多把密钥,规则只有一份。
#[serde(default, skip_serializing_if = "Option::is_none")]
pub route: Option<String>,
/// 网关密钥。`tw-` 前缀是刻意的:用户在客户端配置里看到它时,
/// 一眼就知道这不是某个上游的真 key。
pub key: String,
Expand Down
2 changes: 1 addition & 1 deletion crates/tw-config/src/reload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ mod tests {
fn a_route_pointing_at_a_deleted_group_is_caught_before_anything_swaps_in() {
// **语法正确但语义错误的配置最危险**:YAML 完全合法,运行时却会
// 把请求路由到空处。
let bad = "version: 1\nclients:\n - name: c\n key: tw-k\nproviders:\n - name: a\n base_url: https://x\n key: k\nroutes:\n - name: 默认\n default: true\n rules:\n - name: r\n to: 已经删掉的组\n";
let bad = "version: 1\nclients:\n - name: c\n key: tw-k\nproviders:\n - name: a\n base_url: https://x\n key: k\nroutes:\n - name: 默认\n rules:\n - name: r\n to: 已经删掉的组\n";
let r = try_parse(bad).unwrap_err();
assert_eq!(r.stage, Stage::Semantics);
assert!(r.message.contains("已经删掉的组"), "{r:?}");
Expand Down
16 changes: 6 additions & 10 deletions crates/tw-control/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -393,18 +393,14 @@ mod patch_seq_tests {
}

#[test]
fn a_new_key_can_carry_its_route_assignment_in_one_write() {
// 建密钥和分配路由是**一次写入**,不是两次 —— 中间那一刻
// 「有一把没分配路由的密钥」是个用户能看见的错误状态。
fn a_new_key_can_carry_its_route_binding_in_one_write() {
// 建密钥和绑路由是**一次写入**,不是两次 —— 中间那一刻
// 「有一把还没绑路由的密钥」是个用户能看见的中间状态。
let steps = resolve_path(CFG, "/clients").unwrap();
let out = tw_yaml::append(
CFG,
&steps,
"name: codex\nkey: tw-bbb\nroutes:\n - 长上下文",
)
.unwrap();
let out =
tw_yaml::append(CFG, &steps, "name: codex\nkey: tw-bbb\nroute: 长上下文").unwrap();
let cfg: tw_config::Config = serde_yaml_ng::from_str(&out).unwrap();
assert_eq!(cfg.clients[1].routes, vec!["长上下文".to_string()]);
assert_eq!(cfg.clients[1].route.as_deref(), Some("长上下文"));
}

#[test]
Expand Down
4 changes: 2 additions & 2 deletions crates/tw-control/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -252,11 +252,11 @@ async fn overview(State(s): State<ControlState>) -> Json<tw_api::Overview> {
.iter()
.map(|set| tw_api::RouteView {
name: set.name.clone(),
default: set.default,
default: set.name == engine.default_route(),
clients: cfg
.clients
.iter()
.filter(|c| c.routes.contains(&set.name))
.filter(|c| c.route.as_deref() == Some(set.name.as_str()))
.map(|c| c.name.clone())
.collect(),
rules: set
Expand Down
1 change: 0 additions & 1 deletion crates/tw-control/tests/dryrun.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ groups:
providers: [官方, 中转]
routes:
- name: 默认
default: true
rules:
- name: 带缓存的必须走官方
when: { cache: true }
Expand Down
Loading
Loading