fix(codex): treat model_catalog_json as a plain key field

A model_catalog_json in config.toml that no provider owned was never
touched, and CC Switch skipped writing its own pointer while it was
there. A pointer left over from an older version could keep the
generated catalog (model mapping or aggregated models) from taking
effect, with no warning at all in direct mode. Users then tried to write
the pointer by hand, which every later write removed again.

- The pointer now follows the target provider like model does: the
  provider's own pointer if its config has one, otherwise CC Switch's
  catalog when one is generated, otherwise none. Whatever config.toml
  held before is replaced.
- Drop the configOwnsCatalog warning and the live-pointer handling behind
  "Use CC Switch's model catalog". The button stays for a routed provider
  whose own config points at another catalog.
- The provider editor no longer shows a pointer it would only carry over
  from config.toml, so the special case that kept it out of the card is
  gone too.
This commit is contained in:
Jason committed 2026-10-05 20:30:56 +08:00
1 parent 2fdcebf16b
commit cad592e04f
14 files changed
+116 -176

No files matched your search

+13 -37
View File
@@ -520,10 +520,10 @@ pub struct CodexConfigPatch {
pub nested: Vec<(Vec<String>, TomlValue)>,
/// 独有字段的目标值(含 `web_search`)。
pub exclusive: Vec<(String, TomlValue)>,
/// 上一家带进来的独有字段和它行里指定的模型目录指针:live 里的值还相同才删。
/// 上一家带进来的独有字段:live 里的值还相同才删。
pub outgoing: Vec<(String, TomlValue)>,
pub route: RouteWrite,
/// 指向 CC Switch 生成的模型目录(用户自己的指针不认领、不删除)。
/// 生成了模型目录:行里没有自己的指针时,`model_catalog_json` 写成 CC Switch 的目录。
pub catalog: bool,
/// 旧版按别的 id 写进去的表,能证明是 CC Switch 写的就删掉(里面可能有真实 Key)。
pub retired: Vec<KnownTable>,
@@ -554,19 +554,6 @@ fn is_cc_switch_catalog(value: &str) -> bool {
Path::new(value).file_name().and_then(|name| name.to_str()) == Some(CATALOG_FILENAME)
}
/// live 的 `model_catalog_json` 指向别的目录(路由那家的行指定的,或用户自己写的):
/// 写入时照留(见 [`CodexConfigPatch::apply_to`] 第 5 步),Codex 只读那个文件,
/// CC Switch 生成的目录不生效。
pub fn live_catalog_is_foreign(config_text: &str) -> bool {
live_foreign_catalog(config_text).is_some()
}
/// live 里指向别的目录的 `model_catalog_json` 的值(见 [`live_catalog_is_foreign`])。
pub fn live_foreign_catalog(config_text: &str) -> Option<TomlValue> {
let doc = config_text.parse::<DocumentMut>().ok()?;
foreign_catalog(&doc).cloned().map(undecorated)
}
/// 去掉行里自己指定的模型目录指针([`row_catalog_pointer`]),其余内容原样;行里没有时为
/// `None`。
pub fn without_row_catalog(config_text: &str) -> Option<String> {
@@ -596,9 +583,8 @@ pub fn live_catalog_is_ours(config_text: &str) -> bool {
})
}
/// 行里自己指定的模型目录指针(投影的 `top` 只收不是 CC Switch 的指针)。它和独有字段
/// 一样跟着这一家走:切走时 live 里的值还相同就删(见 [`CodexConfigPatch::outgoing`]),
/// 否则第 1 步会把它当成用户的指针留下,之后每一家都用它的模型目录。
/// 行里自己指定的模型目录指针(投影的 `top` 只收不是 CC Switch 的指针)。它和别的关键
/// 字段一样只属于这一家:切到别家时第 1 步清掉。
pub fn row_catalog_pointer(top: &[(String, TomlValue)]) -> Option<&(String, TomlValue)> {
top.iter().find(|(key, _)| key == MODEL_CATALOG_JSON)
}
@@ -620,8 +606,8 @@ impl CodexConfigPatch {
.collect();
let root = doc.as_table_mut();
// 1. 清空顶层关键字段。目标里也有的(含选路要写的)留给后面原位改值;模型目录
// 指针只认自己的,上一家行里指定的指针在第 3 步按值删。
// 1. 清空顶层关键字段。目标里也有的(含选路要写的)留给后面原位改值。模型目录指针
// 同样不论原来指向哪里:要写 CC Switch 的目录时留给第 5 步原位改值,否则删掉。
let doomed: Vec<String> = root
.iter()
.map(|(key, _)| key.to_string())
@@ -632,14 +618,8 @@ impl CodexConfigPatch {
})
.collect();
for key in doomed {
if key == MODEL_CATALOG_JSON {
let ours = root
.get(MODEL_CATALOG_JSON)
.and_then(Item::as_str)
.is_some_and(is_cc_switch_catalog);
if !ours || self.catalog {
continue;
}
if key == MODEL_CATALOG_JSON && self.catalog {
continue;
}
root.remove(&key);
}
@@ -665,9 +645,11 @@ impl CodexConfigPatch {
}
}
// 3. 上一家带进来的独有字段和模型目录指针:值还相同才删。
// 3. 上一家带进来的独有字段:值还相同才删。关键字段第 1 步已经清过(旧版契约里记着的
// 模型目录指针也在这里跳过)。
for (key, value) in &self.outgoing {
if target_top.contains(&key.as_str()) {
if target_top.contains(&key.as_str()) || floor::CODEX_FLOOR_TOP.contains(&key.as_str())
{
continue;
}
let matches = root
@@ -703,13 +685,7 @@ impl CodexConfigPatch {
}
let row_pointer = self.top.iter().any(|(key, _)| key == MODEL_CATALOG_JSON);
if self.catalog && !row_pointer {
let user_pointer = root
.get(MODEL_CATALOG_JSON)
.and_then(Item::as_str)
.is_some_and(|value| !is_cc_switch_catalog(value));
if !user_pointer {
put_value(root, MODEL_CATALOG_JSON, &TomlValue::from(CATALOG_FILENAME));
}
put_value(root, MODEL_CATALOG_JSON, &TomlValue::from(CATALOG_FILENAME));
}
check_effective_route(doc, selector)
+65 -79
View File
@@ -328,11 +328,7 @@ async fn write_proxy(
codex_direct::prepare_official_rows(&state.db, &owner, &spec, &mut prepared)
.await
.map_err(err)?;
let mut planned =
codex_direct::plan(&state.db, &owner, &spec, &prepared).map_err(err)?;
if op_name == op::CATALOG {
planned.release_live_catalog(&codex_direct::read_config_text());
}
let planned = codex_direct::plan(&state.db, &owner, &spec, &prepared).map_err(err)?;
let unchanged = !force && live_now.has_contract(&planned.contract.key);
target.contract = Some(planned.contract.clone());
let pending = pending(target);
@@ -1097,13 +1093,13 @@ pub fn stack_views(state: &AppState, app: &AppType) -> Result<StackView, String>
}
/// [`stack_views`] 再加上 Codex 客户端是不是还在用旧的模型列表(要读进程表,放到阻塞线程池
/// 里)。路由那家自己管理目录、或者用户自己指定了目录时,Stack 模型本来就不发布或不生效,重启
/// 也看不到,已经有 `notice` 说明,不再查。
/// 里)。路由那家自己管理目录时,Stack 模型本来就不发布,重启也看不到,已经有 `notice` 说明,
/// 不再查。
pub async fn stack_view_with_clients(state: &AppState, app: &AppType) -> Result<StackView, String> {
let mut view = stack_views(state, app)?;
if matches!(app, AppType::Codex)
&& view.active
&& !matches!(view.notice, Some("routeOwnsCatalog" | "configOwnsCatalog"))
&& view.notice != Some("routeOwnsCatalog")
&& codex_publishes_stack_models(state, &settled_stack(app)?)
{
view.stale_clients = codex_direct::off_runtime(|| {
@@ -1127,8 +1123,7 @@ fn codex_publishes_stack_models(state: &AppState, stack: &StackState) -> bool {
}
/// Codex 在 Stack 模式下有要发布的 Stack 模型,客户端却看不到或看不全:路由那家自己管理模型
/// 目录文件(Stack 模型不发布);`config.toml` 里用户自己指定了模型目录(生成的目录不生效);
/// 或者官方做默认、最近一次写目录时没拿到官方列表。
/// 目录文件(Stack 模型不发布);或者官方做默认、最近一次写目录时没拿到官方列表。
fn codex_stack_notice(state: &AppState, stack: &StackState) -> Option<&'static str> {
let (_, route) = attached_route(state, &AppType::Codex).ok()??;
let published =
@@ -1139,10 +1134,6 @@ fn codex_stack_notice(state: &AppState, stack: &StackState) -> Option<&'static s
if codex_direct::route_owns_catalog(&route) {
return Some("routeOwnsCatalog");
}
// 按客户端里实际生效的指针看:用户直接写进 config.toml 的指针写入时照留。
if crate::live::project::codex::live_catalog_is_foreign(&codex_direct::read_config_text()) {
return Some("configOwnsCatalog");
}
if !codex_direct::is_official(&route) {
return None;
}
@@ -1153,11 +1144,11 @@ fn codex_stack_notice(state: &AppState, stack: &StackState) -> Option<&'static s
}
}
/// Codex Stack 模式下聚合的模型被别的模型目录挡住(`routeOwnsCatalog` / `configOwnsCatalog`,
/// 用户在提示上点了才调):去掉路由那家行里和 `config.toml` 里指向别的文件的
/// `model_catalog_json`,按当前路由重写,改用 CC Switch 生成的目录。返回之后还剩的提示。
/// Codex Stack 模式下聚合的模型被路由那家自己的模型目录挡住(`routeOwnsCatalog`,用户在提示
/// 上点了才调):去掉它行里指向别的文件的 `model_catalog_json`,按当前路由重写,改用 CC Switch
/// 生成的目录。返回之后还剩的提示。
///
/// 先改行再写客户端:写失败时契约里还记着旧指针,下一次重写按值删掉,不会留下。
/// 先改行再写客户端:写失败时下一次重写按新的行投影,指针照样清掉。
pub async fn adopt_codex_stack_catalog(state: &AppState) -> Result<Option<&'static str>, String> {
let app = AppType::Codex;
let _guard = lock_settled(state, &app).await.map_err(err)?;
@@ -3132,13 +3123,18 @@ command = "fs-server"
exit(&state, &AppType::Codex).await.expect("exit");
assert_eq!(pointer(), None, "{}", codex_text());
// 用户自己写进 live 的指针不认领、不删,也不被 CC Switch 的指针替换。
// 指针是关键字段:手写进 live 的值不保留,有目录时换成 CC Switch 的,没有就删。
let with_user = format!("model_catalog_json = \"/work/mine.json\"\n{}", codex_text());
fs::write(codex_config_path(), with_user).unwrap();
ProviderService::switch(&state, AppType::Codex, "b").expect("to b");
assert_eq!(pointer().as_deref(), Some("/work/mine.json"));
assert_eq!(pointer().as_deref(), Some(ours), "{}", codex_text());
let with_user = codex_text().replace(
&format!("model_catalog_json = \"{ours}\""),
"model_catalog_json = \"/work/mine.json\"",
);
fs::write(codex_config_path(), with_user).unwrap();
ProviderService::switch(&state, AppType::Codex, "c").expect("to c");
assert_eq!(pointer().as_deref(), Some("/work/mine.json"));
assert_eq!(pointer(), None, "{}", codex_text());
}
#[tokio::test]
@@ -5921,30 +5917,23 @@ model_provider = "c"
}
/// 用户直接在 config.toml 里指定的模型目录:写入时照留,生成的目录不生效,同样要提示。
/// 手写进 config.toml 的指针挡不住聚合:进代理时按关键字段清掉,加进 Stack 后换上合并目录。
#[tokio::test]
#[serial]
async fn codex_a_users_own_catalog_pointer_reports_stack_models_unseen() {
async fn codex_a_hand_written_catalog_pointer_gives_way_to_the_stack_catalog() {
let _home = Home::new();
seed_codex("model_catalog_json = \"/work/global-models.json\"\n", None);
let state = state_with(AppType::Codex, &codex_stack_rows(), "a").await;
enter(&state, &AppType::Codex, true).await.expect("enter");
assert!(
codex_doc().get("model_catalog_json").is_none(),
"{}",
codex_text()
);
let notice = set_stack_member(&state, &AppType::Codex, "deepseek", true)
.await
.expect("stack");
assert_eq!(notice, Some("configOwnsCatalog"));
assert_eq!(
codex_doc()["model_catalog_json"].as_str(),
Some("/work/global-models.json")
);
// 用户删掉自己的指针,下一次写入换上 CC Switch 的目录,不再提示。
let without =
codex_text().replace("model_catalog_json = \"/work/global-models.json\"\n", "");
fs::write(codex_config_path(), without).unwrap();
let notice = set_stack_member(&state, &AppType::Codex, "zhipu", true)
.await
.expect("stack zhipu");
assert_eq!(notice, None);
assert_eq!(
codex_doc()["model_catalog_json"].as_str(),
@@ -5999,32 +5988,11 @@ model_provider = "c"
assert_eq!(stack_views(&state, &AppType::Codex).unwrap().notice, None);
}
/// 指针是用户直接写在 config.toml 里的:契约没变也要重写,去掉它、换上合并目录。
/// 编辑器里的模型目录指针归这张卡:live 里手写的不显示、不收进行,保存当前卡时清掉;用户在
/// 编辑器里写的外来指针存进行、随卡写入;指向 CC Switch 目录的指针不存(由写入方决定)。
#[tokio::test]
#[serial]
async fn codex_adopting_the_catalog_drops_a_users_own_pointer() {
let _home = Home::new();
seed_codex("model_catalog_json = \"/work/global-models.json\"\n", None);
let state = state_with(AppType::Codex, &codex_stack_rows(), "a").await;
enter(&state, &AppType::Codex, true).await.expect("enter");
set_codex_member(&state, "deepseek", true).await;
let contract = mode(&AppType::Codex).contract.unwrap();
let notice = adopt_codex_stack_catalog(&state).await.expect("adopt");
assert_eq!(notice, None);
assert_eq!(
codex_doc()["model_catalog_json"].as_str(),
Some(crate::codex_config::CC_SWITCH_CODEX_MODEL_CATALOG_FILENAME)
);
assert_eq!(mode(&AppType::Codex).contract.unwrap(), contract);
assert_eq!(stack_views(&state, &AppType::Codex).unwrap().notice, None);
}
/// 用户写在 config.toml 里的模型目录指针:编辑器里原样保存不收进行(否则从此成了这一家
/// 自己的指针);用户在编辑器里改了它的值才算这一家的。
#[tokio::test]
#[serial]
async fn codex_editor_leaves_a_users_catalog_pointer_in_live() {
async fn codex_editor_catalog_pointer_belongs_to_the_card() {
let _home = Home::new();
set_preservation(true);
seed_codex(
@@ -6064,34 +6032,52 @@ model_provider = "c"
.unwrap()
.to_string()
};
let with_pointer = |base: &Value, pointer: &str| {
let mut edited = base.clone();
edited["config"] = json!(format!(
"model_catalog_json = {pointer:?}\n{}",
base["config"].as_str().unwrap()
));
edited
};
let pointer = || {
codex_doc()
.get("model_catalog_json")
.and_then(|value| value.as_str().map(str::to_string))
};
for id in ["a", "b"] {
let (row, base) = open(id);
assert!(
base["config"].as_str().unwrap().contains("/work/mine.json"),
"{base}"
);
save(row, base.clone(), base).expect("save as is");
let config = row_config(id);
assert!(!config.contains("model_catalog_json"), "{id}: {config}");
assert_eq!(
codex_doc()["model_catalog_json"].as_str(),
Some("/work/mine.json")
);
}
let (row, base) = open("a");
assert!(
!base["config"]
.as_str()
.unwrap()
.contains("model_catalog_json"),
"{base}"
);
save(row, base.clone(), base).expect("save as is");
assert!(!row_config("a").contains("model_catalog_json"));
assert_eq!(pointer(), None, "{}", codex_text());
// 手写 CC Switch 自己的目录:a 没有模型映射,不生成目录,行里不存、live 里也不写。
let ours = crate::codex_config::get_codex_model_catalog_path()
.display()
.to_string();
let (row, base) = open("a");
save(row, with_pointer(&base, &ours), base).expect("save ours");
assert!(!row_config("a").contains("model_catalog_json"));
assert_eq!(pointer(), None, "{}", codex_text());
let (row, base) = open("b");
let mut edited = base.clone();
edited["config"] = json!(base["config"]
.as_str()
.unwrap()
.replace("/work/mine.json", "/work/b.json"));
save(row, edited, base).expect("save changed pointer");
save(row, with_pointer(&base, "/work/b.json"), base).expect("save b pointer");
assert!(
row_config("b").contains("model_catalog_json = \"/work/b.json\""),
"{}",
row_config("b")
);
ProviderService::switch(&state, AppType::Codex, "b").expect("to b");
assert_eq!(pointer().as_deref(), Some("/work/b.json"));
ProviderService::switch(&state, AppType::Codex, "a").expect("to a");
assert_eq!(pointer(), None, "{}", codex_text());
}
/// 普通保存入口(不带编辑器底)改了 Stack 里那家的模型目录:合并目录跟着重算。
+1 -2
View File
@@ -616,8 +616,7 @@ pub struct StackView {
pub active: bool,
pub members: Vec<StackMemberView>,
/// Codex Stack 模型客户端看不到或看不全:`routeOwnsCatalog` 路由那家自己管理模型目录
/// 文件,Stack 模型不发布;`configOwnsCatalog` 用户在 `config.toml` 里指定了自己的模型
/// 目录,生成的目录不生效;官方做路由时官方模型列表暂未取到:`officialModelsBundled`
/// 文件,Stack 模型不发布;官方做路由时官方模型列表暂未取到:`officialModelsBundled`
/// 暂用 Codex 自带的列表(可能缺账号专属的模型),`officialModelsUnavailable` Stack 模型
/// 暂不可用。
#[serde(skip_serializing_if = "Option::is_none")]
+2 -2
View File
@@ -62,8 +62,8 @@ pub struct Contract {
pub version: u32,
/// 契约内容的摘要:切换路由时摘要相同,客户端文件就不用动。
pub key: String,
/// 契约写进客户端的独有字段(Codex 还有路由行里指定的模型目录指针)。退出代理时按它
/// 删除(值相同才删):路由供应商的行之后可能被编辑过,不能到时再按行重新计算。
/// 契约写进客户端的独有字段。退出代理时按它删除(值相同才删):路由供应商的行之后可能
/// 被编辑过,不能到时再按行重新计算。
#[serde(default, skip_serializing_if = "Map::is_empty")]
pub exclusive: Map<String, Value>,
}
@@ -35,9 +35,9 @@ use crate::live::engine::{digest, read_current, DeviceStore, LiveFile};
use crate::live::patch::toml::{value_text, TomlDocPatch, TomlSteps};
use crate::live::patch::{Guarded, LivePatch, WholeFile};
use crate::live::project::codex::{
live_foreign_catalog, official_mirror_table, proxy_route_table, requires_openai_auth,
row_catalog_pointer, without_row_catalog, CodexConfigPatch, CodexProjection, KnownTable, Route,
RouteAuth, RouteWrite, RowInput, MODEL_CATALOG_JSON, ROUTE_ID, WEB_SEARCH_DISABLED,
official_mirror_table, proxy_route_table, requires_openai_auth, row_catalog_pointer,
without_row_catalog, CodexConfigPatch, CodexProjection, KnownTable, Route, RouteAuth,
RouteWrite, RowInput, ROUTE_ID, WEB_SEARCH_DISABLED,
};
use crate::mode::contract::CONTRACT_VERSION;
use crate::mode::operation::{AppWrite, FileChange, OperationReport};
@@ -387,15 +387,11 @@ fn exclusive_of(provider: &Provider, projection: &CodexProjection) -> Vec<(Strin
exclusive
}
/// live 现在对应的那一家带进来的独有字段和行里指定的模型目录指针:切走时值还相同就删。
/// live 现在对应的那一家带进来的独有字段:切走时值还相同就删。
pub(crate) fn outgoing_exclusive(owner: &Owner<'_>) -> Vec<(String, TomlValue)> {
match owner {
Owner::Provider(provider) => match project(provider) {
Ok(projection) => {
let mut fields = exclusive_of(provider, &projection);
fields.extend(row_catalog_pointer(&projection.top).cloned());
fields
}
Ok(projection) => exclusive_of(provider, &projection),
Err(err) => {
log::warn!(
"无法投影 Codex 供应商 {} 的独有字段,切走时不清理它们: {err}",
@@ -530,16 +526,6 @@ impl Planned {
pub(crate) fn config(&self) -> &CodexConfigPatch {
&self.config
}
/// 改用 CC Switch 生成的目录:live 里用户自己写的、指向别的文件的指针这次也去掉(写入时
/// 值还相同才删)。行里自己指定的指针不在这里,由调用方先从行里去掉。
pub(crate) fn release_live_catalog(&mut self, config_text: &str) {
if let Some(pointer) = live_foreign_catalog(config_text) {
self.config
.outgoing
.push((MODEL_CATALOG_JSON.to_string(), pointer));
}
}
}
/// 算出写入内容;行有问题(比如会把官方登录发给第三方)就在这里报错,什么都不写。
@@ -873,7 +859,6 @@ fn contract_of(
exclusive: config
.exclusive
.iter()
.chain(row_catalog_pointer(&config.top))
.map(|(key, value)| (key.clone(), Value::String(value_text(value))))
.collect(),
}
@@ -26,8 +26,7 @@ use crate::live::engine::{read_current, LiveFile};
use crate::live::floor;
use crate::live::patch::toml::parse;
use crate::live::project::codex::{
foreign_catalog, is_keyless_fallback, CodexProjection, Route, RowInput, MODEL_CATALOG_JSON,
OFFICIAL_PROXY_ROUTE_ID, ROUTE_ID,
is_keyless_fallback, CodexProjection, Route, RowInput, OFFICIAL_PROXY_ROUTE_ID, ROUTE_ID,
};
use crate::mode::operation::{AppWrite, FileChange};
use crate::mode::state::{op, PendingTarget};
@@ -269,8 +268,8 @@ impl LiveOwner {
}
}
/// live 里用户自己的独有字段和指向别的文件的模型目录指针:live 现在对应的那家带进来的
/// (值还相同的)不算。只给不知道草稿的新增用(见 [`Origin::Live`])。
/// live 里用户自己的独有字段:live 现在对应的那家带进来的(值还相同的)不算。只给不知道
/// 草稿的新增用(见 [`Origin::Live`])。
///
/// 只读 live、按值去掉那一家的,不走投影:投影会校验生效的 profile,而空行不写
/// `model_provider`,profile 选了路由表就会被当成覆盖路由拒绝。
@@ -281,10 +280,6 @@ pub(crate) fn live_exclusive(state: &AppState) -> Result<Vec<Entry>, AppError> {
let owned = codex_direct::outgoing_exclusive(&LiveOwner::read(state)?.owner());
Ok(exclusive_entries(&doc)
.into_iter()
.chain(foreign_catalog(&doc).map(|pointer| Entry {
path: vec![MODEL_CATALOG_JSON.to_string()],
item: Item::Value(pointer.clone()),
}))
.filter(|entry| {
!owned.iter().any(|(key, value)| {
entry.path[0] == *key && render(&entry.item) == render(&Item::Value(value.clone()))
@@ -364,22 +359,6 @@ pub(crate) fn plan_save(
.into_iter()
.filter(|entry| edited_doc.get(&entry.path[0]).is_none());
// 用户写在 config.toml 里、指向别的文件的模型目录指针同理:用户没动就不收进行,否则保存
// 一次就成了这一家自己的指针,每次切到它都改用那个目录(聚合的模型也发布不了)。它是
// 关键字段,不进全局改动,删掉它走 Stack 提示上的「改用 CC Switch 的模型目录」。
let pointer_from_live = foreign_catalog(&base_doc).is_some_and(|pointer| match origin {
Origin::Row(row) => foreign_catalog(row).is_none(),
Origin::Live(live) => live.iter().any(|entry| {
entry.path == [MODEL_CATALOG_JSON]
&& render(&entry.item) == render(&Item::Value(pointer.clone()))
}),
});
if pointer_from_live
&& rendered(&edited_doc, MODEL_CATALOG_JSON) == rendered(&base_doc, MODEL_CATALOG_JSON)
{
projection.top.retain(|(key, _)| key != MODEL_CATALOG_JSON);
}
// 打开时和保存时选中的路由表都归供应商:用户在编辑器里把 custom 改名成别的表,那张表
// 连同里面的 Key 不能当成全局设置留在 live 里。
let routes: Vec<&str> = [selected_route(&base_doc), selected_route(&edited_doc)]
+24 -2
View File
@@ -31,7 +31,7 @@
//! | 20 | 进入代理不写 auth.json;第三方路由契约用字面值 `PROXY_MANAGED` | 已有 `codex_official_to_deepseek_then_takeover_...` |
//! | 21 | 代理下官方路由不写占位凭据,客户端带自己的真实登录 | crate 内:`mode::controller` 的 `codex_routes_between_official_and_third_party_contracts` |
//! | 22 | 退出代理不覆盖用户此刻的登录状态(期间登出就保持登出,重新登录就保留新登录) | 本文件 |
//! | 23 | `model_catalog_json` 只认领 `cc-switch-model-catalog.json`;用户自己的指针不认领、不删除 | 本文件 |
//! | 23 | `model_catalog_json` 是关键字段:卡里自带的指针随卡写入;否则有生成的目录写 `cc-switch-model-catalog.json`,没有就删;live 里手写的值不保留 | 本文件 |
//! | 24 | `web_search = "disabled"` 只删 CC Switch 写的哨兵值,用户的其他值保留 | 本文件 |
//! | 25 | auth.json 删不掉时切换照常成功,返回 `codex_auth_cleanup_failed` 警告 | 本文件 |
//!
@@ -683,7 +683,29 @@ fn model_catalog_pointer_ownership() {
assert_eq!(
live_config()["model_catalog_json"].as_str(),
Some("/Users/me/my-catalog.json"),
"a user-managed catalog pointer is never claimed or removed"
"a card's own catalog pointer wins over the generated catalog"
);
let hand_written = live_config_text().replace(
"model_catalog_json = \"/Users/me/my-catalog.json\"",
"model_catalog_json = \"/Users/me/hand-written.json\"",
);
std::fs::write(get_codex_config_path(), hand_written).expect("hand-write a pointer");
switch(&state, "owned").expect("switch back to owned");
assert_eq!(
live_config()["model_catalog_json"].as_str(),
Some("cc-switch-model-catalog.json"),
"a pointer written into config.toml by hand is replaced like any key field"
);
let hand_written = live_config_text().replace(
"model_catalog_json = \"cc-switch-model-catalog.json\"",
"model_catalog_json = \"/Users/me/hand-written.json\"",
);
std::fs::write(get_codex_config_path(), hand_written).expect("hand-write a pointer");
switch(&state, "plain").expect("switch to plain again");
assert!(
live_config().get("model_catalog_json").is_none(),
"a hand-written pointer is removed when the target has no catalog"
);
}
@@ -437,8 +437,7 @@ export function SwitchModePanel({
tone="warning"
title={t(`provider.${stack.notice}`)}
actions={
stack.notice === "routeOwnsCatalog" ||
stack.notice === "configOwnsCatalog" ? (
stack.notice === "routeOwnsCatalog" ? (
<Button
variant="neutral"
size="compact"
-1
View File
@@ -119,7 +119,6 @@
"officialModelsBundled": "The official model list isn't available yet, so Codex's built-in list is used for now and account-specific models may be missing",
"officialModelsUnavailable": "Aggregated models are unavailable for now: the Codex model list can't be read",
"routeOwnsCatalog": "Aggregated models won't show up in Codex: the routed provider uses its own model catalog file (model_catalog_json)",
"configOwnsCatalog": "Aggregated models won't show up in Codex: model_catalog_json in config.toml points to a different model catalog file",
"adoptCatalog": "Use CC Switch's model catalog",
"adoptCatalogDone": "Now using CC Switch's model catalog. Restart Codex to apply",
"adoptCatalogFailed": "Failed to switch to CC Switch's model catalog: {{error}}",
-1
View File
@@ -119,7 +119,6 @@
"officialModelsBundled": "公式モデル一覧をまだ取得できていないため、Codex 内蔵の一覧を使っています。アカウント専用のモデルが表示されない場合があります",
"officialModelsUnavailable": "Codex のモデル一覧を読み込めないため、集約したモデルは現在使用できません",
"routeOwnsCatalog": "ルーティング先のプロバイダーが独自のモデルカタログファイル(model_catalog_json)を使っているため、集約したモデルは Codex に表示されません",
"configOwnsCatalog": "config.toml の model_catalog_json が別のモデルカタログファイルを指しているため、集約したモデルは Codex に表示されません",
"adoptCatalog": "CC Switch のモデルカタログを使う",
"adoptCatalogDone": "CC Switch のモデルカタログに切り替えました。Codex を再起動すると反映されます",
"adoptCatalogFailed": "CC Switch のモデルカタログへの切り替えに失敗しました:{{error}}",
-1
View File
@@ -119,7 +119,6 @@
"officialModelsBundled": "官方模型列表暫未取得,暫用 Codex 內建列表,可能缺少帳號專屬模型",
"officialModelsUnavailable": "聚合的模型暫不可用:讀取不到 Codex 模型列表",
"routeOwnsCatalog": "聚合的模型不會出現在 Codex 中:路由供應商使用自己的模型目錄檔案(model_catalog_json)",
"configOwnsCatalog": "聚合的模型不會出現在 Codex 中:config.toml 的 model_catalog_json 指向了別的模型目錄檔案",
"adoptCatalog": "改用 CC Switch 的模型目錄",
"adoptCatalogDone": "已改用 CC Switch 的模型目錄,重新啟動 Codex 後生效",
"adoptCatalogFailed": "改用 CC Switch 的模型目錄失敗:{{error}}",
-1
View File
@@ -119,7 +119,6 @@
"officialModelsBundled": "官方模型列表暂未取到,暂用 Codex 内置列表,可能缺少账号专属模型",
"officialModelsUnavailable": "聚合的模型暂不可用:读取不到 Codex 模型列表",
"routeOwnsCatalog": "聚合的模型不会出现在 Codex 里:路由供应商使用自己的模型目录文件(model_catalog_json)",
"configOwnsCatalog": "聚合的模型不会出现在 Codex 里:config.toml 的 model_catalog_json 指向了别的模型目录文件",
"adoptCatalog": "改用 CC Switch 的模型目录",
"adoptCatalogDone": "已改用 CC Switch 的模型目录,重启 Codex 后生效",
"adoptCatalogFailed": "改用 CC Switch 的模型目录失败:{{error}}",
+1 -1
View File
@@ -152,7 +152,7 @@ export function useSetProxyStackMember() {
}
/**
* Codex 聚合的模型被别的模型目录挡住(routeOwnsCatalog / configOwnsCatalog)时,改用 CC Switch
* Codex 聚合的模型被路由供应商自己的模型目录挡住(routeOwnsCatalog)时,改用 CC Switch
* 生成的目录。客户端只在启动时读模型目录,成功后提示重启;还剩别的提示照样弹出。
*/
export function useAdoptCodexStackCatalog() {
+1 -3
View File
@@ -83,13 +83,11 @@ export interface ProxyStackMember {
/**
* Codex Stack 模型客户端看不到或看不全:`routeOwnsCatalog` 路由供应商使用自己的模型目录文件,
* Stack 模型不发布;`configOwnsCatalog` 用户在 config.toml 里指定了自己的模型目录,生成的目录
* 不生效;官方做路由时官方模型列表暂未取到:`officialModelsBundled` 暂用 Codex
* Stack 模型不发布;官方做路由时官方模型列表暂未取到:`officialModelsBundled` 暂用 Codex
* 自带的列表(可能缺账号专属的模型),`officialModelsUnavailable` Stack 模型暂不可用。
*/
export type ProxyStackNotice =
| "routeOwnsCatalog"
| "configOwnsCatalog"
| "officialModelsBundled"
| "officialModelsUnavailable";