From c9b2cab58b1ec714b078e982e7ca0b8257d629c7 Mon Sep 17 00:00:00 2001 From: cysamurai Date: Wed, 13 May 2026 17:31:31 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=8A=9F=E8=83=BD=E5=AE=8C?= =?UTF-8?q?=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 56 +++- call-client/package.json | 2 +- call-client/src-tauri/Cargo.lock | 2 +- call-client/src-tauri/Cargo.toml | 2 +- call-client/src-tauri/src/commands/update.rs | 330 +++++++++++++++++-- call-client/src-tauri/src/lib.rs | 3 +- call-client/src-tauri/tauri.conf.json | 2 +- call-client/src/views/LoginView.vue | 75 ++++- 8 files changed, 417 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index c349a1b..4637f77 100644 --- a/README.md +++ b/README.md @@ -59,33 +59,59 @@ Tauri 原始输出目录: ## 3. 配置文件目录与日志目录 +以下与各自 `src-tauri/tauri.conf.json` 里的 **`bundle.identifier`** 一致(当前为 `com.ziyun.callclient`、`com.ziyun.broadcastclient`)。若改过 identifier,中间目录名会随之变化。 + +**路径中的用户名**:下文以 Linux 用户 `alice`、Windows 用户 `Alice` 为例;请把 `alice` / `Alice` 换成本机实际登录名(Linux 一般为 `/home/<登录名>` 中的 `<登录名>`;Windows 一般为 `C:\Users\<登录名>` 中的 `<登录名>`)。 + ### 3.1 call-client -配置文件(运行时): +Bundle ID:`com.ziyun.callclient`。 + +**默认环境(未设置 `XDG_CONFIG_HOME`、`XDG_DATA_HOME`)** + +| 用途 | Linux 完整路径示例 | Windows 完整路径示例 | +|------|----------------------|----------------------| +| 应用配置 | `/home/alice/.config/com.ziyun.callclient/config.json` | `C:\Users\Alice\AppData\Roaming\com.ziyun.callclient\config.json` | +| 主程序日志 | `/home/alice/.config/com.ziyun.callclient/app.log`(轮转文件形如 `/home/alice/.config/com.ziyun.callclient/app-20260513120000.log`) | `C:\Users\Alice\AppData\Roaming\com.ziyun.callclient\app.log`(同级目录下 `app-*.log`) | +| 运行时会话(多窗口共享) | `/home/alice/.local/share/com.ziyun.callclient/runtime_session.json` | `C:\Users\Alice\AppData\Local\com.ziyun.callclient\runtime_session.json` | +| 单实例锁 | `/home/alice/.local/share/com.ziyun.callclient/single-instance.lock` | `C:\Users\Alice\AppData\Local\com.ziyun.callclient\single-instance.lock` | + +**若设置了 XDG 环境变量(Linux)** -- Linux: `~/.config/call-client/config.json` -- Windows: `%APPDATA%/call-client/config.json` +- 当 `XDG_CONFIG_HOME` 为 `/data/my-config` 时,配置文件与 `app.log` 在: + `/data/my-config/com.ziyun.callclient/config.json` + `/data/my-config/com.ziyun.callclient/app.log` +- 当 `XDG_DATA_HOME` 为 `/data/my-data` 时,`runtime_session.json` 与 `single-instance.lock` 在: + `/data/my-data/com.ziyun.callclient/runtime_session.json` + `/data/my-data/com.ziyun.callclient/single-instance.lock` -日志输出(运行时): +实现依据:`call-client/src-tauri/src/commands/config.rs`、`logger.rs`、`session.rs`。 -- Linux: `~/.local/state/call-client/app.log` -- Windows: `%LOCALAPPDATA%/call-client/state/app.log` +> 旧版本若使用独立目录名 `call-client` 或日志放在 `local/state` 等路径,已废弃;当前统一为上述 `com.ziyun.callclient` 目录。 ### 3.2 broadcast-client -配置存储(运行时): +Bundle ID:`com.ziyun.broadcastclient`。 + +**默认环境(未设置 `XDG_CONFIG_HOME`)** + +| 用途 | Linux 完整路径示例 | Windows 完整路径示例 | +|------|----------------------|----------------------| +| 持久化配置 | `/home/alice/.config/com.ziyun.broadcastclient/broadcast-config.json` | `C:\Users\Alice\AppData\Roaming\com.ziyun.broadcastclient\broadcast-config.json` | +| Socket 服务日志(文件名含时间戳) | `/home/alice/.config/com.ziyun.broadcastclient/socket-service-20260513-120000-000.log` | `C:\Users\Alice\AppData\Roaming\com.ziyun.broadcastclient\socket-service-20260513-120000-000.log` | + +**若 `XDG_CONFIG_HOME` 为 `/data/my-config`(Linux)** + +- `/data/my-config/com.ziyun.broadcastclient/broadcast-config.json` +- `/data/my-config/com.ziyun.broadcastclient/socket-service-<时间戳>.log` -- 当前使用浏览器存储(`localStorage`),无独立磁盘配置文件 -- 关键键名: - - `runtime_broadcast_config` - - `broadcast_config_local_fallback` +**浏览器侧回退**(`localStorage` 键名,非磁盘路径):`runtime_broadcast_config`、`broadcast_config_local_fallback`。 -日志输出(运行时,socket 服务): +实现依据:`broadcast-client/src/services/configStore.ts`、`broadcast-client/src-tauri/src/lib.rs`。 -- Linux: `~/.config/broadcast-client/socket-service-*.log` -- Windows: `%APPDATA%/broadcast-client/socket-service-*.log` +> 旧版本若使用 `broadcast-client` 作为配置目录名,已废弃;当前为 `com.ziyun.broadcastclient`。 -### 3.3 两个项目仓库内 Tauri 配置文件 +### 3.3 两个项目仓库内 Tauri 打包配置(源码路径) - `call-client/src-tauri/tauri.conf.json` - `broadcast-client/src-tauri/tauri.conf.json` diff --git a/call-client/package.json b/call-client/package.json index c44da7f..a7d45be 100644 --- a/call-client/package.json +++ b/call-client/package.json @@ -1,7 +1,7 @@ { "name": "call-client", "private": true, - "version": "0.1.1", + "version": "0.1.2", "type": "module", "scripts": { "dev": "vite", diff --git a/call-client/src-tauri/Cargo.lock b/call-client/src-tauri/Cargo.lock index ee1b1e7..7eff3b8 100644 --- a/call-client/src-tauri/Cargo.lock +++ b/call-client/src-tauri/Cargo.lock @@ -392,7 +392,7 @@ dependencies = [ [[package]] name = "call-client" -version = "0.1.1" +version = "0.1.2" dependencies = [ "chrono", "fs2", diff --git a/call-client/src-tauri/Cargo.toml b/call-client/src-tauri/Cargo.toml index 591bb52..99b3d7c 100644 --- a/call-client/src-tauri/Cargo.toml +++ b/call-client/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "call-client" -version = "0.1.1" +version = "0.1.2" description = "A Tauri App" authors = ["you"] edition = "2021" diff --git a/call-client/src-tauri/src/commands/update.rs b/call-client/src-tauri/src/commands/update.rs index 7185f46..0b783af 100644 --- a/call-client/src-tauri/src/commands/update.rs +++ b/call-client/src-tauri/src/commands/update.rs @@ -38,32 +38,137 @@ fn bash_single_quoted(content: &str) -> String { format!("'{}'", content.replace('\'', "'\\''")) } +/// 执行 `pkexec bash -c ''` 并记录 stdout/stderr;`log_topic` 用于日志前缀(如 `[apt-setup]`)。 #[cfg(target_os = "linux")] -fn run_privileged_apt_setup(deb_line: &str) -> Result<(), String> { - let quoted = bash_single_quoted(deb_line.trim()); - let inner = format!( - "set -e; install -d /etc/apt/sources.list.d; printf '%s\\n' {quoted} > /etc/apt/sources.list.d/zyyun.list; apt-get update -y" +fn run_pkexec_bash_c(inner: &str, log_topic: &str, error_prefix_zh: &str) -> Result<(), String> { + let _ = app_log( + "info".to_string(), + format!( + "{log_topic} pkexec 内联 bash -c 脚本长度={} 字符", + inner.chars().count() + ), ); let output = Command::new("pkexec") - .args(["bash", "-c", &inner]) + .args(["bash", "-c", inner]) .output() - .map_err(|e| format!("无法启动 pkexec(请确认已安装 policykit-1):{e}"))?; + .map_err(|e| { + let _ = app_log( + "error".to_string(), + format!("{log_topic} 无法启动 pkexec: {e}(请确认已安装 policykit-1 且当前为图形会话)"), + ); + format!("无法启动 pkexec(请确认已安装 policykit-1):{e}") + })?; + + let code = output.status.code(); + let stdout_len = output.stdout.len(); + let stderr_len = output.stderr.len(); + let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); + let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); if output.status.success() { + let _ = app_log( + "info".to_string(), + format!( + "{log_topic} pkexec/bash 已结束: success=true exitCode={code:?} stdout字节={stdout_len} stderr字节={stderr_len}" + ), + ); + if stdout.is_empty() && stderr.is_empty() { + let _ = app_log( + "info".to_string(), + format!("{log_topic} pkexec 未产生 stdout/stderr(常见于图形授权器吞掉子进程输出)"), + ); + } else { + if !stdout.is_empty() { + log_preview(&format!("{log_topic} pkexec stdout"), &stdout, 8000); + } + if !stderr.is_empty() { + log_preview(&format!("{log_topic} pkexec stderr"), &stderr, 8000); + } + } return Ok(()); } - let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); - let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); + let _ = app_log( + "error".to_string(), + format!( + "{log_topic} pkexec/bash 已结束: success=false exitCode={code:?} stdout字节={stdout_len} stderr字节={stderr_len}" + ), + ); + if !stdout.is_empty() { + log_preview(&format!("{log_topic} 失败时 pkexec stdout"), &stdout, 12000); + } else { + let _ = app_log( + "error".to_string(), + format!("{log_topic} 失败时 pkexec stdout 为空"), + ); + } + if !stderr.is_empty() { + log_preview(&format!("{log_topic} 失败时 pkexec stderr"), &stderr, 12000); + } else { + let _ = app_log( + "error".to_string(), + format!("{log_topic} 失败时 pkexec stderr 为空"), + ); + } + Err(format!( - "配置更新源失败(退出码 {:?})。stderr={} stdout={}", - output.status.code(), + "{error_prefix_zh}失败(退出码 {code:?})。stderr={} stdout={}", if stderr.is_empty() { "(空)" } else { &stderr }, if stdout.is_empty() { "(空)" } else { &stdout } )) } +#[cfg(target_os = "linux")] +fn log_preview(label: &str, text: &str, max_chars: usize) { + let body: String = text.chars().take(max_chars).collect(); + let collapsed = body.replace('\r', "").replace('\n', " | "); + let suffix = if text.chars().count() > max_chars { + format!(" …(共 {} 字符,已截断)", text.chars().count()) + } else { + String::new() + }; + let _ = app_log( + "info".to_string(), + format!("{label}{suffix}: {collapsed}"), + ); +} + +#[cfg(target_os = "linux")] +fn run_privileged_apt_setup(deb_line: &str) -> Result<(), String> { + let trimmed = deb_line.trim(); + let quoted = bash_single_quoted(trimmed); + let inner = format!( + "set -e; install -d /etc/apt/sources.list.d; printf '%s\\n' {quoted} > /etc/apt/sources.list.d/zyyun.list; apt-get update -y" + ); + + let _ = app_log( + "info".to_string(), + format!( + "[apt-setup] 特权脚本摘要: 使用 pkexec 启动 bash -c;步骤为 install -d /etc/apt/sources.list.d → printf deb 行到 /etc/apt/sources.list.d/zyyun.list → apt-get update -y;deb 行长度={} 字符", + trimmed.chars().count() + ), + ); + log_preview("[apt-setup] deb 行预览(前 220 字符)", trimmed, 220); + let _ = app_log( + "info".to_string(), + "[apt-setup] deb 行已按单引号规则嵌入 bash -c 脚本(长度见下一条 pkexec 日志)".to_string(), + ); + + run_pkexec_bash_c(&inner, "[apt-setup]", "配置更新源") +} + +/// 受控升级:仅允许固定包名 `call-client`,不接受前端拼接 shell。 +#[cfg(target_os = "linux")] +fn run_privileged_upgrade_call_client() -> Result<(), String> { + const INNER: &str = "set -e; apt-get update -y && apt-get install -y --only-upgrade call-client"; + let _ = app_log( + "info".to_string(), + "[apt-upgrade] 受控特权脚本摘要: pkexec bash -c → apt-get update -y → apt-get install -y --only-upgrade call-client(包名写死在 Rust,无用户输入)".to_string(), + ); + run_pkexec_bash_c(INNER, "[apt-upgrade]", "应用内升级(apt)") +} + #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct AptUpdateCheckResult { @@ -78,11 +183,30 @@ pub struct AptUpdateCheckResult { #[cfg(target_os = "linux")] fn extract_policy_value(output: &str, key: &str) -> String { - output - .lines() - .find_map(|line| line.trim().strip_prefix(key).map(|value| value.trim().to_string())) - .filter(|value| !value.is_empty()) - .unwrap_or_else(|| "(unknown)".to_string()) + let label = key.trim_end_matches(':').trim(); + if label.is_empty() { + return "(unknown)".to_string(); + } + let needle = format!("{label}:"); + for raw in output.lines() { + let line = raw + .trim() + .trim_start_matches('\u{feff}'); + if let Some(rest) = line.strip_prefix(&needle) { + let value = rest.trim(); + if !value.is_empty() { + return value.to_string(); + } + } + // 行内任意位置出现「Installed:」「Candidate:」(兼容非常规格式/BOM/缩进) + if let Some(idx) = line.find(&needle) { + let rest = line[idx + needle.len()..].trim(); + if !rest.is_empty() { + return rest.to_string(); + } + } + } + "(unknown)".to_string() } #[tauri::command] @@ -94,7 +218,7 @@ pub fn check_apt_update( let _ = app_log( "info".to_string(), format!( - "检查更新开始: package={}, current_version={}", + "检查更新开始: package={}, currentVersion={}", package_name, current_version ), ); @@ -122,18 +246,38 @@ pub fn check_apt_update( let message = format!("执行 apt-cache 失败: {error}"); let _ = app_log( "error".to_string(), - format!("检查更新失败: package={}, {}", package_name, message), + format!( + "检查更新失败: package={}, 无法启动子进程 apt-cache policy, err={}", + package_name, message + ), ); message })?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); + let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); let message = if stderr.is_empty() { "apt-cache policy 执行失败".to_string() } else { format!("apt-cache policy 执行失败: {stderr}") }; + let _ = app_log( + "error".to_string(), + format!( + "检查更新失败: package={}, exitCode={:?} stderr字节={} stdout字节={}", + package_name, + output.status.code(), + output.stderr.len(), + output.stdout.len() + ), + ); + if !stdout.is_empty() { + log_preview("检查更新: apt-cache 失败 stdout", &stdout, 4000); + } + if !stderr.is_empty() { + log_preview("检查更新: apt-cache 失败 stderr", &stderr, 4000); + } let _ = app_log( "error".to_string(), format!("检查更新失败: package={}, {}", package_name, message), @@ -142,8 +286,87 @@ pub fn check_apt_update( } let stdout = String::from_utf8_lossy(&output.stdout).to_string(); - let installed_version = extract_policy_value(&stdout, "Installed:"); + let stderr = String::from_utf8_lossy(&output.stderr).to_string(); + let _ = app_log( + "info".to_string(), + format!( + "检查更新: apt-cache policy 子进程结束 success=true package={} exitCode={:?} stdout字节={} stderr字节={}", + package_name, + output.status.code(), + stdout.len(), + stderr.len() + ), + ); + if !stderr.trim().is_empty() { + log_preview("检查更新: apt-cache policy stderr(非致命,仍可能解析 stdout)", &stderr, 2000); + } + let mut installed_version = extract_policy_value(&stdout, "Installed:"); let candidate_version = extract_policy_value(&stdout, "Candidate:"); + + if installed_version == "(unknown)" { + let dpkg_cmd = format!("dpkg-query -W -f=${{Version}} {}", package_name); + let _ = app_log( + "info".to_string(), + format!("检查更新: 尝试补全已安装版本: {dpkg_cmd}"), + ); + if let Ok(dpkg_out) = Command::new("dpkg-query") + .args(["-W", "-f=${Version}", &package_name]) + .output() + { + let dpkg_ok = dpkg_out.status.success(); + let dpkg_raw = String::from_utf8_lossy(&dpkg_out.stdout).trim().to_string(); + let dpkg_err = String::from_utf8_lossy(&dpkg_out.stderr).trim().to_string(); + let _ = app_log( + "info".to_string(), + format!( + "检查更新: dpkg-query 结束 success={} stdout={} stderr_len={}", + dpkg_ok, + if dpkg_raw.is_empty() { "(空)".to_string() } else { dpkg_raw.clone() }, + dpkg_err.len() + ), + ); + if !dpkg_err.is_empty() { + log_preview("检查更新: dpkg-query stderr", &dpkg_err, 1500); + } + if dpkg_ok { + let v = dpkg_raw; + if !v.is_empty() { + installed_version = v; + let _ = app_log( + "info".to_string(), + format!( + "检查更新: 已从 dpkg-query 补全 installedVersion={} (apt-cache 未解析出 Installed 行)", + installed_version + ), + ); + } + } + } else { + let _ = app_log( + "warn".to_string(), + "检查更新: 无法启动 dpkg-query 子进程(忽略,继续使用 apt 解析结果)".to_string(), + ); + } + } + + log_preview( + "检查更新: apt-cache policy stdout 全文预览(截断)", + &stdout, + 4000, + ); + + if installed_version == "(unknown)" || candidate_version == "(unknown)" { + let preview: String = stdout.chars().take(1200).collect(); + let sanitized = preview.replace('\r', "").replace('\n', " | "); + let _ = app_log( + "warn".to_string(), + format!( + "检查更新: 版本或候选源解析异常 package={},apt-cache policy 输出预览(截断): {}", + package_name, sanitized + ), + ); + } + let baseline_version = if installed_version == "(none)" || installed_version == "(unknown)" { current_version.clone() } else { @@ -171,12 +394,15 @@ pub fn check_apt_update( let _ = app_log( "info".to_string(), format!( - "检查更新完成: package={}, installed_version={}, candidate_version={}, has_update={}, source_available={}", + "检查更新完成: package={}, currentVersion={}, installedVersion={}, candidateVersion={}, baselineVersion={}, hasUpdate={}, sourceAvailable={}, updateCommand={}", result.package_name, + result.current_version, result.installed_version, result.candidate_version, + baseline_version, result.has_update, - result.source_available + result.source_available, + result.update_command ), ); @@ -197,9 +423,26 @@ pub fn setup_zyyun_apt_source(app: AppHandle, deb_line: String) -> Result<(), St { let line = deb_line.trim(); if line.is_empty() { + let _ = app_log( + "error".to_string(), + "[apt-setup] setup_zyyun_apt_source 中止: deb 源行为空".to_string(), + ); return Err("deb 源行不能为空".to_string()); } + let _ = app_log( + "info".to_string(), + format!( + "[apt-setup] setup_zyyun_apt_source 入口: deb 行长度={} 字符", + line.chars().count() + ), + ); + log_preview("[apt-setup] setup_zyyun_apt_source 入口 deb 行预览", line, 260); + let _ = app_log( + "info".to_string(), + "[apt-setup] 即将调用 run_privileged_apt_setup(pkexec bash -c …)".to_string(), + ); + emit_apt_setup_progress(&app, 5, "准备配置紫云 apt 软件源…"); emit_apt_setup_progress( &app, @@ -209,6 +452,14 @@ pub fn setup_zyyun_apt_source(app: AppHandle, deb_line: String) -> Result<(), St let result = run_privileged_apt_setup(line); + let _ = app_log( + "info".to_string(), + format!( + "[apt-setup] run_privileged_apt_setup 返回: success={}", + result.is_ok() + ), + ); + match &result { Ok(()) => { emit_apt_setup_progress(&app, 92, "正在完成索引刷新…"); @@ -230,3 +481,42 @@ pub fn setup_zyyun_apt_source(app: AppHandle, deb_line: String) -> Result<(), St result } } + +/// 受控应用内升级:通过 `pkexec` 执行固定的 `apt-get update` + `apt-get install --only-upgrade call-client`。 +/// 包名写死在 Rust 中,不接受前端传入 shell,以降低注入风险。 +#[tauri::command] +pub fn upgrade_call_client_via_apt(_app: AppHandle) -> Result<(), String> { + let _ = app_log( + "info".to_string(), + "[apt-upgrade] 命令入口 upgrade_call_client_via_apt(仅固定包 call-client)".to_string(), + ); + + #[cfg(not(target_os = "linux"))] + { + let _ = app_log( + "warn".to_string(), + "[apt-upgrade] 中止: 当前目标平台非 Linux".to_string(), + ); + return Err("当前系统不支持通过 apt 在应用内升级。".to_string()); + } + + #[cfg(target_os = "linux")] + { + let result = run_privileged_upgrade_call_client(); + match &result { + Ok(()) => { + let _ = app_log( + "info".to_string(), + "[apt-upgrade] pkexec/apt 升级子流程已成功结束(若界面版本未刷新,请重启应用)".to_string(), + ); + } + Err(err) => { + let _ = app_log( + "error".to_string(), + format!("[apt-upgrade] 升级失败: {err}"), + ); + } + } + result + } +} diff --git a/call-client/src-tauri/src/lib.rs b/call-client/src-tauri/src/lib.rs index 7c4e134..abc4f25 100644 --- a/call-client/src-tauri/src/lib.rs +++ b/call-client/src-tauri/src/lib.rs @@ -12,7 +12,7 @@ use commands::{ logger::{app_log, get_log_paths}, session::{session_clear, session_get, session_set}, sync::{start_screen_sync, stop_screen_sync}, - update::{check_apt_update, setup_zyyun_apt_source}, + update::{check_apt_update, setup_zyyun_apt_source, upgrade_call_client_via_apt}, window::{ close_taxer_info_window, close_ticket_window, ensure_main_window, focus_window, open_login_window, open_main_window, open_taxer_info_window, open_ticket_window, quit_app, @@ -84,6 +84,7 @@ pub fn run() { list_windows, check_apt_update, setup_zyyun_apt_source, + upgrade_call_client_via_apt, open_ticket_window, close_ticket_window, close_taxer_info_window, diff --git a/call-client/src-tauri/tauri.conf.json b/call-client/src-tauri/tauri.conf.json index 0650345..1282e2d 100644 --- a/call-client/src-tauri/tauri.conf.json +++ b/call-client/src-tauri/tauri.conf.json @@ -2,7 +2,7 @@ "$schema": "../node_modules/@tauri-apps/cli/schema.json", "package": { "productName": "call-client", - "version": "0.1.1" + "version": "0.1.2" }, "build": { "beforeDevCommand": "npm run dev", diff --git a/call-client/src/views/LoginView.vue b/call-client/src/views/LoginView.vue index 251070c..09b039d 100644 --- a/call-client/src/views/LoginView.vue +++ b/call-client/src/views/LoginView.vue @@ -350,11 +350,22 @@ async function runZyyunAptSourceSetupWithProgress(debLine: string): Promise 220 ? `${trimmed.slice(0, 220)}…(共 ${trimmed.length} 字符)` : trimmed; + await log( + "info", + `[apt-setup][前端] 即将 invoke setup_zyyun_apt_source, debLen=${trimmed.length}, debPreview=${debPreview}`, + ); + try { - await invoke("setup_zyyun_apt_source", { debLine: debLine.trim() }); + await invoke("setup_zyyun_apt_source", { debLine: trimmed }); aptSetupProgressStatus.value = "success"; + await log("info", "[apt-setup][前端] setup_zyyun_apt_source invoke 成功结束"); } catch (error) { aptSetupProgressStatus.value = "exception"; + const message = error instanceof Error ? error.message : String(error); + await log("error", `[apt-setup][前端] setup_zyyun_apt_source invoke 失败: ${message}`); throw error; } finally { unlisten(); @@ -372,7 +383,10 @@ async function handleCheckUpdate(): Promise { checkingUpdate.value = true; try { - await log("info", `检查更新开始: package=call-client, currentVersion=${appVersion.value}`); + await log( + "info", + `检查更新(前端): 开始, package=call-client, currentVersion=${appVersion.value}`, + ); const invokeCheck = (): Promise => invoke("check_apt_update", { @@ -383,7 +397,7 @@ async function handleCheckUpdate(): Promise { let result = await invokeCheck(); await log( "info", - `检查更新结果: package=${result.packageName}, installed=${result.installedVersion}, candidate=${result.candidateVersion}, hasUpdate=${result.hasUpdate}, sourceAvailable=${result.sourceAvailable}`, + `检查更新(前端): 首次 check_apt_update 返回 installed=${result.installedVersion}, candidate=${result.candidateVersion}, sourceAvailable=${result.sourceAvailable}, hasUpdate=${result.hasUpdate}`, ); if (!result.sourceAvailable) { @@ -414,7 +428,7 @@ async function handleCheckUpdate(): Promise { }, ); } catch { - await log("info", "用户取消自动配置 apt 源"); + await log("info", "检查更新(前端): 用户在「配置更新源」对话框选择取消或关闭"); const setupCmd = buildAptSourceSetupCommand(aptSourceDebLine.value); const body = [ "已取消自动配置。若需手动配置,可将下面一行写入软件源,并在终端执行复制出的命令:", @@ -436,6 +450,7 @@ async function handleCheckUpdate(): Promise { return; } + await log("info", "检查更新(前端): 用户确认自动配置 apt 源,开始 runZyyunAptSourceSetupWithProgress"); await runZyyunAptSourceSetupWithProgress(aptSourceDebLine.value); await mergeConfig({ zyyun_apt_source_configured: true }); await log("info", "已写入配置: zyyun_apt_source_configured=true"); @@ -443,7 +458,7 @@ async function handleCheckUpdate(): Promise { result = await invokeCheck(); await log( "info", - `自动配置后再次检查: installed=${result.installedVersion}, candidate=${result.candidateVersion}, sourceAvailable=${result.sourceAvailable}`, + `检查更新(前端): 自动配置后再次 check_apt_update 返回 installed=${result.installedVersion}, candidate=${result.candidateVersion}, sourceAvailable=${result.sourceAvailable}, hasUpdate=${result.hasUpdate}`, ); if (!result.sourceAvailable) { @@ -481,19 +496,49 @@ async function handleCheckUpdate(): Promise { `检测到新版本:${result.candidateVersion}`, `当前版本:${result.installedVersion}`, "", - "点击「复制命令」后,在终端执行升级:", + "可选:", + "· 点击「应用内升级」将弹出管理员授权(pkexec),执行固定的 apt 更新与仅升级 call-client(与终端命令等价,包名写死在程序内)。", + "· 点击「复制终端命令」可在终端自行执行 sudo。", + "", + "终端命令:", result.updateCommand, ].join("\n"); - const copy = await confirmNative({ - title: "发现新版本", - message: body, - okLabel: "复制命令", - cancelLabel: "关闭", - }); - if (copy) { - await writeText(result.updateCommand); - showMessage("success", "升级命令已复制到剪贴板"); + try { + await ElMessageBox.confirm(body, "发现新版本", { + confirmButtonText: "应用内升级(需管理员密码)", + cancelButtonText: "复制终端命令", + distinguishCancelAndClose: true, + type: "info", + }); + } catch (action: unknown) { + if (action === "cancel") { + await writeText(result.updateCommand); + await log("info", "检查更新(前端): 用户选择复制终端升级命令"); + showMessage("success", "升级命令已复制到剪贴板"); + } else { + await log( + "info", + `检查更新(前端): 关闭「发现新版本」或未应用内升级 action=${String(action)}`, + ); + } + return; + } + + await log( + "info", + "检查更新(前端): 用户选择应用内 apt 升级(invoke upgrade_call_client_via_apt)", + ); + await invoke("upgrade_call_client_via_apt"); + await log("info", "检查更新(前端): upgrade_call_client_via_apt 已成功返回"); + try { + appVersion.value = await getVersion(); + } catch { + // 忽略版本读取失败 } + showMessage( + "success", + `升级命令已执行完毕。界面显示版本:${appVersion.value}。若仍为旧版本,请重启本客户端后再试。`, + ); } catch (error) { const message = error instanceof Error ? error.message : String(error); await showErrorNative(`检查更新失败:${message}`, "检查更新", {