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
2 changes: 0 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
[workspace]
members = ["crates/wb-switch-core", "crates/wb-switch-server", "src-tauri"]
resolver = "2"

# `schemars 0.8`(tauri-build / tauri-plugin 的传递依赖,且开启了 `preserve_order`)
# 用 `indexmap 1.x` 的 `IndexMap<K, V>` 两参数形式。而 `indexmap 1.9.3` 的 `IndexMap`
# 只在 `has_std` 下有默认哈希参数,该标记由 build.rs 里的 `autocfg` 探测 sysroot 得出。
# 在新版 Rust(本机 1.98.1 实测)上该探测失败,于是 `IndexMap` 变成必须显式传三个泛型
# 参数,编译报 `E0107: struct takes 3 generic arguments but 2 were supplied`。
#
# 这里 patch 到本地副本:其 build.rs 无条件声明 `has_std`,从根上消除 E0107。
# 只动 build script 的探测逻辑,不触碰任何库代码。
#
# 注意:这是**构建层**的兼容修复,与业务逻辑无关。若你的工具链较老(autocfg 探测正常)
# 则不需要它,可以整段删除并移除 `vendor-patched/`。
[patch.crates-io]
indexmap = { path = "vendor-patched/indexmap" }
70 changes: 68 additions & 2 deletions crates/wb-switch-core/src/modules/codebuddy_cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1120,7 +1120,23 @@ pub fn sync_windows_env_for_account(
.map(clean_bearer_token)
.is_some_and(|token| token == current);
let already_synced = clean_bearer_token(updated_token) == current;
if !previous_matches && !already_synced {
// 例外:settings 里的 token 匹配不上账号库中的**任何**账号时,它是失效的孤儿值
// (典型成因:历史上某次刷新没同步成功,或该账号被重新登录换了 token)。
//
// 这种情况必须允许用活跃账号的最新 token 覆盖,否则状态**不可自愈**:
// settings 既不等于「刷新前 token」也不等于「刷新后 token」,于是每次刷新都在
// 这里被挡掉,界面永远停在「认证脱节」,只能靠用户手动点按钮。
//
// 若它匹配到的是**其它**账号,说明用户手动切过去了,仍然不覆盖(保留原保护)。
let current_is_orphan = !accounts.iter().any(|candidate| {
clean_bearer_token(
candidate
.get("access_token")
.and_then(Value::as_str)
.unwrap_or(""),
) == current
});
if !previous_matches && !already_synced && !current_is_orphan {
return Ok(false);
}
if already_synced {
Expand All @@ -1141,6 +1157,46 @@ pub fn sync_windows_env_for_account(
Ok(true)
}

/// 保活刷新后、settings 尚未同步的宽限窗口。
///
/// 账号库在此时间内被写过,说明大概率正处在「保活刚换完 token、settings 还没跟上」
/// 的正常中间态,此时不该提示用户「认证脱节」。
const ACCOUNTS_SYNC_GRACE_MS: i64 = 30_000;

/// 账号库是否在最近 `window_ms` 内被写过(按 mtime 判断)。
fn account_store_written_within(window_ms: i64) -> bool {
let path = crate::modules::config::accounts_file();
let Ok(modified) = std::fs::metadata(path).and_then(|meta| meta.modified()) else {
return false;
};
match std::time::SystemTime::now().duration_since(modified) {
Ok(age) => (age.as_millis() as i64) <= window_ms,
// 时钟回拨等异常:按「刚写过」处理,宁可少报一次也不误报脱节。
Err(_) => true,
}
}

/// 由状态位推导 `(syncPending, syncInProgress)`。
///
/// - `syncPending`:真脱节——settings 的 token 反查不到账号,且已超出同步宽限窗口,
/// 需要用户介入(点「更新 CLI 认证」)。
/// - `syncInProgress`:保活刚写完账号库、settings 尚未跟上的正常中间态,
/// 稍候会自动完成,不该催用户操作。
///
/// 抽成纯函数是为了让边界(尤其是宽限窗口的临界点)可被单测固定住。
fn classify_sync_state(
windows: bool,
env_configured: bool,
active_missing: bool,
expected_present: bool,
recently_written: bool,
) -> (bool, bool) {
let pending_raw = windows && env_configured && active_missing && expected_present;
let in_progress = pending_raw && recently_written;
let pending = pending_raw && !recently_written;
(pending, in_progress)
}

/// 返回脱敏的 CLI 轮换状态,不返回 token 或 helper 内容。
pub fn status() -> Value {
let accounts = account::load_accounts();
Expand All @@ -1165,13 +1221,23 @@ pub fn status() -> Value {
} else {
helper_migration_required()
};
// 脱节判定拆成两个互斥状态:真脱节 vs 保活刷新中的正常中间态。
// 后者若也报脱节,用户会在每次启动看到「认证脱节」并反复点按钮。
let (sync_pending, sync_in_progress) = classify_sync_state(
cfg!(windows),
env_configured,
active.is_none(),
expected_active.is_some(),
account_store_written_within(ACCOUNTS_SYNC_GRACE_MS),
);
json!({
"configured": configured,
"authMode": if cfg!(windows) { "settings-env" } else { "api-key-helper" },
"environmentOverride": environment_override,
"helperCurrent": if cfg!(windows) { env_configured } else { helper_is_current() },
"migrationRequired": migration_required,
"syncPending": cfg!(windows) && env_configured && active.is_none() && expected_active.is_some(),
"syncPending": sync_pending,
"syncInProgress": sync_in_progress,
"settingsPresent": settings_path().is_file(),
"helperPresent": helper_path().map(|path| path.is_file()).unwrap_or(false),
"helperSupportsAccountIds": helper_supports_account_ids(),
Expand Down
15 changes: 15 additions & 0 deletions crates/wb-switch-core/src/modules/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,22 @@ pub const DEFAULT_HTTP_USER_AGENT: &str = "Mozilla/5.0 (Macintosh; Intel Mac OS
// 路径
// ---------------------------------------------------------------------------

/// 覆盖家目录的环境变量名。
///
/// 设置后 `home_dir()` 返回它的值,于是 `~/.wb-switch`、`~/.codebuddy`、
/// `~/.codebuddy-rotate` 全部落在指定目录下。用途:
/// - 集成测试把家目录沙箱化,避免读写真实账号与 CLI 配置;
/// - 自定义部署位置。
///
/// 未设置时行为与之前完全一致(`dirs::home_dir()`)。
pub const HOME_ENV_VAR: &str = "WB_SWITCH_HOME";

pub fn home_dir() -> PathBuf {
if let Some(overridden) = std::env::var_os(HOME_ENV_VAR) {
if !overridden.is_empty() {
return PathBuf::from(overridden);
}
}
dirs::home_dir().unwrap_or_else(|| PathBuf::from("."))
}

Expand Down
Loading
Loading