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