From 2acbdad3cfb62cae877543e6d3467ec54bbb3a82 Mon Sep 17 00:00:00 2001 From: Ocean <13620113564@qq.com> Date: Mon, 5 Oct 2026 09:00:18 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20macOS=20=E5=AE=A2?= =?UTF-8?q?=E6=88=B7=E7=AB=AF=E6=97=A0=E6=B3=95=E8=8E=B7=E5=8F=96=E6=B4=BB?= =?UTF-8?q?=E5=8A=A8=E5=A5=97=E9=A4=90=E5=8F=8A=20Issue=20#30=20=E9=A2=9D?= =?UTF-8?q?=E5=BA=A6=E9=87=8D=E5=A4=8D=E6=98=BE=E7=A4=BA=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 修复 macOS 客户端版本号写死 3.11.2: - 默认兜底版本提升至 3.14.4; - 增加 macOS 动态读取 /Applications/ZCode.app/Contents/Info.plist 探测本地版本; - 统一 OAuth 流程请求头使用动态版本。 2. 修复新账号继承真实物理机器码被风控: - adopt_virtual_device_mid 一律分配全新 UUID 虚拟机器码,彻底与物理机解耦。 3. 修复 Issue #30 同源渠道额度重复显示: - pick_channels 过滤 enabled: false 的 provider; - merge_parts 移除 src != &p.source 限制,按唯一 pid 或 (tier, name, expire) 绝对去重。 --- src-tauri/src/quota.rs | 118 ++++++++++++++++++++++++++++------------- src-tauri/src/store.rs | 15 +----- 2 files changed, 81 insertions(+), 52 deletions(-) diff --git a/src-tauri/src/quota.rs b/src-tauri/src/quota.rs index 7f05dae..6a23581 100644 --- a/src-tauri/src/quota.rs +++ b/src-tauri/src/quota.rs @@ -20,7 +20,7 @@ fn no_window(prog: &str) -> std::process::Command { pub const QUOTA_LIMIT_URL: &str = "https://open.bigmodel.cn/api/monitor/usage/quota/limit"; pub const SUBSCRIPTION_URL: &str = "https://open.bigmodel.cn/api/biz/subscription/list"; pub const BILLING_BALANCE_URL: &str = "https://zcode.z.ai/api/v1/zcode-plan/billing/balance"; -pub const CLIENT_APP_VERSION: &str = "3.11.2"; +pub const CLIENT_APP_VERSION: &str = "3.14.4"; pub(crate) fn client_platform() -> String { let os = crate::zcrypto::node_platform_for(std::env::consts::OS); @@ -129,45 +129,81 @@ pub(crate) fn client_timezone() -> String { iana_time_zone::get_timezone().unwrap_or_else(|_| "unknown".to_string()) } -pub(crate) fn zcode_app_version() -> String { - static CACHE: std::sync::OnceLock = std::sync::OnceLock::new(); - CACHE - .get_or_init(|| { - for hive in [ - r"HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall", - r"HKLM\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall", - r"HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall", - ] { - let Ok(out) = no_window("reg").args(["query", hive, "/s"]).output() else { - continue; - }; - let txt = String::from_utf8_lossy(&out.stdout); - let (mut name, mut ver) = (String::new(), String::new()); - for line in txt.lines() { - let l = line.trim(); - if l.starts_with("HKEY_") { - if is_zcode_display_name(&name) && !ver.is_empty() { - return normalize_version(&ver); +#[cfg(windows)] +fn detect_installed_version() -> Option { + for hive in [ + r"HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall", + r"HKLM\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall", + r"HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall", + ] { + let Ok(out) = no_window("reg").args(["query", hive, "/s"]).output() else { + continue; + }; + let txt = String::from_utf8_lossy(&out.stdout); + let (mut name, mut ver) = (String::new(), String::new()); + for line in txt.lines() { + let l = line.trim(); + if l.starts_with("HKEY_") { + if is_zcode_display_name(&name) && !ver.is_empty() { + return Some(normalize_version(&ver)); + } + name.clear(); + ver.clear(); + continue; + } + if let Some(rest) = l.strip_prefix("DisplayName") { + name = rest.trim_start().trim_start_matches("REG_SZ").trim().to_string(); + } else if let Some(rest) = l.strip_prefix("DisplayVersion") { + ver = rest.trim_start().trim_start_matches("REG_SZ").trim().to_string(); + } + } + if is_zcode_display_name(&name) && !ver.is_empty() { + return Some(normalize_version(&ver)); + } + } + None +} + +#[cfg(target_os = "macos")] +fn detect_installed_version() -> Option { + let home = std::env::var("HOME").unwrap_or_default(); + let candidates = [ + std::path::PathBuf::from("/Applications/ZCode.app/Contents/Info.plist"), + std::path::PathBuf::from(format!("{home}/Applications/ZCode.app/Contents/Info.plist")), + ]; + for p in candidates { + if let Ok(content) = std::fs::read_to_string(&p) { + if let Some(pos) = content.find("CFBundleShortVersionString") { + let rest = &content[pos..]; + if let (Some(s), Some(e)) = (rest.find(""), rest.find("")) { + if s + 8 < e { + let ver = rest[s + 8..e].trim(); + if !ver.is_empty() { + return Some(normalize_version(ver)); } - name.clear(); - ver.clear(); - continue; } - if let Some(rest) = l.strip_prefix("DisplayName") { - name = rest.trim_start().trim_start_matches("REG_SZ").trim().to_string(); - } else if let Some(rest) = l.strip_prefix("DisplayVersion") { - ver = rest.trim_start().trim_start_matches("REG_SZ").trim().to_string(); - } - } - if is_zcode_display_name(&name) && !ver.is_empty() { - return normalize_version(&ver); } } - CLIENT_APP_VERSION.to_string() + } + } + None +} + +#[cfg(not(any(windows, target_os = "macos")))] +fn detect_installed_version() -> Option { + None +} + +pub(crate) fn zcode_app_version() -> String { + static CACHE: std::sync::OnceLock = std::sync::OnceLock::new(); + CACHE + .get_or_init(|| { + detect_installed_version().unwrap_or_else(|| CLIENT_APP_VERSION.to_string()) }) .clone() } +#[cfg(windows)] fn is_zcode_display_name(name: &str) -> bool { let l = name.to_lowercase(); l.contains("zcode") && !l.contains("switch") @@ -193,7 +229,7 @@ pub(crate) fn zai_billing_headers_with_mid(token: &str, mid: Option) -> pub(crate) struct OAuthFlowHeaders(pub Vec<(String, String)>); pub(crate) fn zai_oauth_headers_with_mid(token: &str, mid: Option) -> OAuthFlowHeaders { - OAuthFlowHeaders(zai_headers_with_version(CLIENT_APP_VERSION.to_string(), token, mid)) + OAuthFlowHeaders(zai_headers_with_version(zcode_app_version(), token, mid)) } fn zai_headers_with_version(ver: String, token: &str, mid: Option) -> Vec<(String, String)> { @@ -617,6 +653,9 @@ pub(crate) fn pick_channels(creds: &Value, config: Option<&Value>, secret: &str, (!k.starts_with("enc:") && looks_like_token(k)).then(|| k.to_string()) }; for (id, p) in ordered { + if p.get("enabled").and_then(|e| e.as_bool()) == Some(false) { + continue; + } let key = provider_key(p); if id.contains("start-plan") { let jwt = safe_decrypt( @@ -751,11 +790,14 @@ fn merge_parts(parts: Vec) -> QuotaOverview { } refreshed = refreshed.max(p.refreshed_at); for s in &p.plans { - let dup = slot_src.iter().enumerate().any(|(i, src)| { - src != &p.source - && slots[i].tier == s.tier - && slots[i].name == s.name - && !slots[i].items.is_empty() + let dup = slots.iter().any(|existing| { + if !s.pid.is_empty() && existing.pid == s.pid { + return true; + } + existing.tier == s.tier + && existing.name == s.name + && existing.expire == s.expire + && !existing.items.is_empty() && !s.items.is_empty() }); if !dup { diff --git a/src-tauri/src/store.rs b/src-tauri/src/store.rs index 108842f..1ec0016 100644 --- a/src-tauri/src/store.rs +++ b/src-tauri/src/store.rs @@ -1171,20 +1171,7 @@ fn adopt_virtual_device_mid(paths: &Paths, acc: &mut Account) -> Result<(), Stri if acc.virtual_device_mid.as_deref().map_or(false, |m| !m.trim().is_empty()) { return Ok(()); } - let live_mid: Option = fs::read_to_string(paths.live_telemetry()) - .ok() - .and_then(|s| serde_json::from_str::(&s).ok()) - .and_then(|v| v.get("deviceMid").and_then(|m| m.as_str()).map(String::from)) - .filter(|m| !m.trim().is_empty()); - let taken = |m: &str| { - list_accounts(paths) - .map(|accs| accs.iter().any(|a| a.virtual_device_mid.as_deref() == Some(m))) - .unwrap_or(false) - }; - let mid = match live_mid { - Some(m) if !taken(&m) => m, - _ => Uuid::new_v4().to_string(), - }; + let mid = Uuid::new_v4().to_string(); acc.virtual_device_mid = Some(mid); acc.updated_at = now_ts(); save_account(paths, acc)