更新功能完善

master
cysamurai 1 month ago
parent 57b32c54aa
commit c9b2cab58b

@ -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`

@ -1,7 +1,7 @@
{
"name": "call-client",
"private": true,
"version": "0.1.1",
"version": "0.1.2",
"type": "module",
"scripts": {
"dev": "vite",

@ -392,7 +392,7 @@ dependencies = [
[[package]]
name = "call-client"
version = "0.1.1"
version = "0.1.2"
dependencies = [
"chrono",
"fs2",

@ -1,6 +1,6 @@
[package]
name = "call-client"
version = "0.1.1"
version = "0.1.2"
description = "A Tauri App"
authors = ["you"]
edition = "2021"

@ -38,32 +38,137 @@ fn bash_single_quoted(content: &str) -> String {
format!("'{}'", content.replace('\'', "'\\''"))
}
/// 执行 `pkexec bash -c '<inner>'` 并记录 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 -ydeb 行长度={} 字符",
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_setuppkexec 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
}
}

@ -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,

@ -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",

@ -350,11 +350,22 @@ async function runZyyunAptSourceSetupWithProgress(debLine: string): Promise<void
aptSetupHint.value = typeof p.message === "string" ? p.message : "";
});
const trimmed = debLine.trim();
const debPreview =
trimmed.length > 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<void> {
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<AptUpdateCheckResult> =>
invoke<AptUpdateCheckResult>("check_apt_update", {
@ -383,7 +397,7 @@ async function handleCheckUpdate(): Promise<void> {
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<void> {
},
);
} catch {
await log("info", "用户取消自动配置 apt 源");
await log("info", "检查更新(前端): 用户在「配置更新源」对话框选择取消或关闭");
const setupCmd = buildAptSourceSetupCommand(aptSourceDebLine.value);
const body = [
"已取消自动配置。若需手动配置,可将下面一行写入软件源,并在终端执行复制出的命令:",
@ -436,6 +450,7 @@ async function handleCheckUpdate(): Promise<void> {
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<void> {
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<void> {
`检测到新版本:${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}`, "检查更新", {

Loading…
Cancel
Save