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
8 changes: 8 additions & 0 deletions crates/tw-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -680,6 +680,14 @@ pub enum PatchOp {
Remove {
path: String,
},
/// 把一个列表清成空的(`[]`)。
///
/// **和「把这个键删掉」不是一回事**,所以它不是 `Remove` 的循环:
/// `allow` 不写 = 跟客户端方言走,`allow: []` = 一个都不给。界面上
/// 那是两个不同的选项。
Clear {
path: String,
},
}

#[derive(Debug, Clone, Serialize, Deserialize)]
Expand Down
8 changes: 8 additions & 0 deletions crates/tw-control/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,14 @@ impl ConfigManager {
tw_yaml::remove(&text, parent, *i)
.map_err(|e| ApplyError::BadPath(format!("删 `{path}` 失败:{e}")))?
}
tw_api::PatchOp::Clear { path } => {
// 要清的列表可能根本还没写进文件 —— 「一个都不给」
// 正是用户第一次碰 `allow` 的那一下。`resolve_path`
// 对没写过的键会原样留成 Key,走得通。
let steps = resolve_path(&text, path).map_err(ApplyError::BadPath)?;
tw_yaml::clear_seq(&text, &steps)
.map_err(|e| ApplyError::BadPath(format!("清空 `{path}` 失败:{e}")))?
}
};
}
self.write(&text, Some(&cur.version()), origin).await
Expand Down
52 changes: 43 additions & 9 deletions crates/tw-engine/src/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,13 @@ pub struct Engine {
sets: Vec<RouteSet>,
/// 没绑定时走哪条。
default_route: String,
/// 这个名字是配置里写的,还是我们兜的。
///
/// **写错了要报错,没写不算错。**两者都表现为「找不到这条路由」,
/// 但一个是打字错误(必须拦),一个是这个字段本来就可以不写(必须
/// 能用)。不区分的话,二选一都会错:要么打错的名字被静默兜掉,
/// 要么没写这个字段的配置加载不了。
default_is_explicit: bool,
/// 密钥名 → 它绑的那条路由名。没有条目 = 走默认。
bound: std::collections::HashMap<String, String>,
groups: Vec<Group>,
Expand All @@ -461,8 +468,19 @@ impl Engine {
// 上游」都说不出口(tw-config::validate 里那段注释)。
//
// 这条今天已经立过一次,又被这里破坏了一次。测试抓住了。
let default_is_explicit = default_route.is_some();
let default_route = default_route.unwrap_or_else(|| DEFAULT_ROUTE.to_string());
if sets.iter().all(|s| s.rules.is_empty()) && !providers.is_empty() {
// **默认路由必须永远存在。**判据是「有没有叫这个名字的路由」,
// 不是「一条规则都没有」——
//
// 后者有个陷阱:配置里一添第一条自定义路由,隐式的那条默认就
// 不再合成,于是 `default_route` 指向一个不存在的名字、整份配置
// 失效。也就是说**「建第一条路由」这个动作本身会把配置写坏**,
// 而那正是界面上最常走的一步。端到端测出来的。
if !default_is_explicit
&& !sets.iter().any(|s| s.name == default_route)
&& !providers.is_empty()
{
const ALL: &str = "__all__";
groups.push(Group {
name: ALL.to_string(),
Expand All @@ -479,17 +497,15 @@ impl Engine {
deny: None,
guard: None,
};
match sets.iter_mut().find(|s| s.name == default_route) {
Some(d) => d.rules.push(fallback),
None => sets.push(RouteSet {
name: default_route.clone(),
rules: vec![fallback],
}),
}
sets.push(RouteSet {
name: default_route.clone(),
rules: vec![fallback],
});
}
Self {
sets,
default_route,
default_is_explicit,
bound,
groups,
providers,
Expand Down Expand Up @@ -560,7 +576,7 @@ impl Engine {
}
// 默认路由指了一个不存在的名字。**没绑路由的密钥会一条规则都
// 不过** —— 请求全部落到「没有任何规则命中」,而配置看起来完整。
if !self.sets.is_empty() && !self.sets.iter().any(|s| s.name == self.default_route) {
if self.default_is_explicit && !self.sets.iter().any(|s| s.name == self.default_route) {
return Err(RouteError::UnknownDefaultRoute(self.default_route.clone()));
}
// 绑了一个不存在的路由名 —— 那把密钥会静默地退回默认路由,
Expand Down Expand Up @@ -908,6 +924,24 @@ mod tests {
assert_eq!(e.validate(), Err(RouteError::DuplicateRoute("重名".into())));
}

#[test]
fn adding_the_first_custom_route_does_not_break_the_implicit_default() {
// **这个动作是界面上最常走的一步**,而它一度会把配置写坏:
// 合成条件原本是「一条规则都没有」,所以添了第一条自定义路由
// 之后隐式的默认就不再合成,`default_route` 指向一个不存在的
// 名字,整份配置失效。端到端测出来的。
let e = Engine::new(
vec!["a".into()],
vec![],
vec![set_of("长上下文", "a")],
None,
Default::default(),
);
assert!(e.validate().is_ok(), "{:?}", e.validate());
// 没绑路由的密钥仍然有规则可走
assert!(!e.rules_for_client("谁都行").is_empty());
}

#[test]
fn a_default_route_that_names_nothing_is_caught_at_load() {
// **没绑路由的密钥会一条规则都过不到** —— 请求全部落到
Expand Down
5 changes: 5 additions & 0 deletions crates/tw-types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,11 @@ pub fn substitute_template(
/// 并发上限。住在 tw-types 是因为配置和数据面都要认
/// 它,而它本身只是几个数字 —— 不该为此让 tw-config 依赖 tw-gateway。
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
// 配置里的每一个结构都拒未知字段,唯独这一个在 tw-types 里,漏了。
// 表现是 `max_body_bytes: 8388608` 写进去不报错、也不生效 —— 用户改
// 了个上限,界面说写成功了,什么都没发生。(这一层别的类型是聊天接口
// 的线上类型,上游随时会加字段,那些恰恰不能拒。)
#[serde(deny_unknown_fields)]
pub struct Limits {
/// 全局并发
#[serde(default = "d_max_concurrent")]
Expand Down
Loading
Loading