mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-25 13:45:03 +08:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7e1c85feb6 | |||
| 7d0eacac33 |
@@ -1,6 +1,7 @@
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use tauri::AppHandle;
|
||||
use tauri_plugin_updater::UpdaterExt;
|
||||
|
||||
fn merge_settings_for_save(
|
||||
mut incoming: crate::settings::AppSettings,
|
||||
@@ -79,17 +80,74 @@ pub async fn restart_app(app: AppHandle) -> Result<bool, String> {
|
||||
// 在后台延迟重启,让函数有时间返回响应
|
||||
tauri::async_runtime::spawn(async move {
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||||
// app.restart() 走 RESTART_EXIT_CODE 路径,ExitRequested 处理器会直接
|
||||
// 放行给 Tauri 默认 re-exec,不执行代理/Live 清理。但本命令用于
|
||||
// app_config_dir 变更后的重启:新实例会切到新数据库,拿不到旧库里的
|
||||
// Live 备份,无法恢复被接管的 Live 配置。因此必须趁旧实例的事件循环
|
||||
// 仍存活,在这里同步完成恢复(保留代理状态,新实例启动时自动重新接管)。
|
||||
crate::cleanup_before_exit(&app).await;
|
||||
app.restart();
|
||||
});
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// 下载并安装应用更新,然后由后端直接重启应用。
|
||||
///
|
||||
/// macOS 更新会原地替换 `.app` bundle。如果先返回前端、再让旧 WebView 调
|
||||
/// `process.relaunch()`,旧进程可能已经处在 bundle 被替换后的不稳定窗口期。
|
||||
/// 这里把退出清理、安装和重启串在同一个后端流程中,避免依赖旧前端继续执行。
|
||||
#[tauri::command]
|
||||
pub async fn install_update_and_restart(app: AppHandle) -> Result<bool, String> {
|
||||
let updater = app
|
||||
.updater_builder()
|
||||
.build()
|
||||
.map_err(|e| format!("初始化更新器失败: {e}"))?;
|
||||
|
||||
let Some(update) = updater
|
||||
.check()
|
||||
.await
|
||||
.map_err(|e| format!("检查更新失败: {e}"))?
|
||||
else {
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
log::info!("开始下载应用更新: {}", update.version);
|
||||
let bytes = update
|
||||
.download(|_, _| {}, || {})
|
||||
.await
|
||||
.map_err(|e| format!("下载更新失败: {e}"))?;
|
||||
|
||||
log::info!("开始安装应用更新: {}", update.version);
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
// Windows updater 会在 install() 内启动安装器并直接退出当前进程
|
||||
// (插件内部 std::process::exit(0),绕过 TrayIcon::drop、不发
|
||||
// NIM_DELETE,会残留死图标——与托盘"退出"路径相同的问题)。
|
||||
// 因此清理只能放在 install 前执行,且必须显式移除托盘图标。
|
||||
crate::save_window_state_before_exit(&app);
|
||||
crate::cleanup_before_exit(&app).await;
|
||||
crate::remove_tray_icon_before_exit(&app);
|
||||
crate::destroy_single_instance_lock(&app);
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||||
update.install(bytes).map_err(|e| {
|
||||
format!(
|
||||
"Windows 更新安装失败: {e}。已执行退出前清理,代理或 Live 接管可能已暂停;请重启应用或重新开启代理后再试。"
|
||||
)
|
||||
})?;
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
// macOS/Linux install() 会返回;先安装,避免安装失败时误停代理/撤回接管。
|
||||
update
|
||||
.install(bytes)
|
||||
.map_err(|e| format!("安装更新失败: {e}"))?;
|
||||
|
||||
crate::save_window_state_before_exit(&app);
|
||||
crate::cleanup_before_exit(&app).await;
|
||||
|
||||
log::info!("应用更新安装完成,正在重启应用");
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||||
crate::restart_process(&app);
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取 app_config_dir 覆盖配置 (从 Store)
|
||||
#[tauri::command]
|
||||
pub async fn get_app_config_dir_override(app: AppHandle) -> Result<Option<String>, String> {
|
||||
|
||||
+28
-1
@@ -1165,6 +1165,7 @@ pub fn run() {
|
||||
commands::get_log_config,
|
||||
commands::set_log_config,
|
||||
commands::restart_app,
|
||||
commands::install_update_and_restart,
|
||||
commands::check_for_updates,
|
||||
commands::is_portable_mode,
|
||||
commands::copy_text_to_clipboard,
|
||||
@@ -1645,7 +1646,7 @@ pub async fn cleanup_before_exit(app_handle: &tauri::AppHandle) {
|
||||
/// 触发 tray-icon 内部的 `remove_tray_icon` → `Shell_NotifyIconW(NIM_DELETE)`,
|
||||
/// 在进程结束前干净地把图标摘掉。其它平台 `set_visible(false)` 也是
|
||||
/// 正常的隐藏/移除语义,作为跨平台兜底也安全。
|
||||
fn remove_tray_icon_before_exit(app_handle: &tauri::AppHandle) {
|
||||
pub(crate) fn remove_tray_icon_before_exit(app_handle: &tauri::AppHandle) {
|
||||
if let Some(tray) = app_handle.tray_by_id(tray::TRAY_ID) {
|
||||
if let Err(e) = tray.set_visible(false) {
|
||||
log::warn!("退出时移除托盘图标失败: {e}");
|
||||
@@ -1962,6 +1963,32 @@ pub fn save_window_state_before_exit(app_handle: &tauri::AppHandle) {
|
||||
}
|
||||
}
|
||||
|
||||
/// 主动释放 single-instance 锁。
|
||||
///
|
||||
/// macOS single-instance 使用 `/tmp/{identifier}.sock`。我们有若干路径会直接
|
||||
/// `std::process::exit(0)`,不会触发插件挂在 `RunEvent::Exit` 上的清理钩子。
|
||||
/// 重启前主动 destroy 可以避免新进程误连旧 listener 后自行退出。
|
||||
pub fn destroy_single_instance_lock(app_handle: &tauri::AppHandle) {
|
||||
#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
|
||||
tauri_plugin_single_instance::destroy(app_handle);
|
||||
}
|
||||
|
||||
/// 清理托盘图标、释放 single-instance 锁后重启当前应用。
|
||||
///
|
||||
/// 直接走 `tauri::process::restart`(spawn 新进程 + `exit(0)`),不经过事件
|
||||
/// 循环退出,因此 Tauri 内部的 `cleanup_before_exit` 和各插件的
|
||||
/// `RunEvent::Exit` 钩子都不会执行。需要的清理由调用方与本函数显式补偿:
|
||||
/// 窗口状态、代理/Live 恢复(调用方);托盘图标、single-instance 锁(本函数)。
|
||||
///
|
||||
/// 有意不调 `AppHandle::cleanup_before_exit()`:它会在调用线程上 Drop 托盘
|
||||
/// 图标,而 macOS 的 NSStatusItem 操作要求主线程;`set_visible(false)` 走
|
||||
/// `run_item_main_thread` 代理,跨线程安全(见 `remove_tray_icon_before_exit`)。
|
||||
pub fn restart_process(app_handle: &tauri::AppHandle) -> ! {
|
||||
remove_tray_icon_before_exit(app_handle);
|
||||
destroy_single_instance_lock(app_handle);
|
||||
tauri::process::restart(&app_handle.env());
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{classify_exit_request, ExitRequestAction};
|
||||
|
||||
@@ -1133,22 +1133,6 @@ pub fn import_default_config(state: &AppState, app_type: AppType) -> Result<bool
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// 拒绝把"被代理接管的 Live"导入为供应商:接管期间 Live 里只有
|
||||
// PROXY_MANAGED 占位符和本地代理地址,不是用户的真实配置。一旦导入,
|
||||
// 它会成为 current provider(SSOT),后续"无备份恢复"路径会把占位符
|
||||
// 当真实配置写回 Live,永久卡在已失效的本地代理上。
|
||||
// 典型触发场景:代理接管开启时切换 app_config_dir 并重启,新数据库首启导入。
|
||||
if state
|
||||
.proxy_service
|
||||
.detect_takeover_in_live_config_for_app(&app_type)
|
||||
{
|
||||
return Err(AppError::localized(
|
||||
"provider.import.live_taken_over",
|
||||
"Live 配置当前处于代理接管状态(包含占位符),不能导入为供应商。请先关闭代理接管或恢复 Live 配置后重试。",
|
||||
"The live config is currently taken over by the proxy (contains placeholders) and cannot be imported as a provider. Disable proxy takeover or restore the live config first.",
|
||||
));
|
||||
}
|
||||
|
||||
let settings_config = match app_type {
|
||||
AppType::Codex => crate::codex_config::read_codex_live_settings()?,
|
||||
AppType::Claude => {
|
||||
|
||||
@@ -1632,7 +1632,7 @@ impl ProxyService {
|
||||
///
|
||||
/// 返回值:
|
||||
/// - Ok(true):已成功写回
|
||||
/// - Ok(false):缺少当前供应商/供应商不存在/供应商本身含占位符,无法写回
|
||||
/// - Ok(false):缺少当前供应商/供应商不存在,无法写回
|
||||
fn restore_live_from_ssot_for_app(&self, app_type: &AppType) -> Result<bool, String> {
|
||||
let current_id = crate::settings::get_effective_current_provider(&self.db, app_type)
|
||||
.map_err(|e| format!("获取 {app_type:?} 当前供应商失败: {e}"))?;
|
||||
@@ -1650,16 +1650,6 @@ impl ProxyService {
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
// 供应商配置本身含接管占位符时不可写回(历史异常:接管期间 Live 被
|
||||
// 误导入成了供应商)。写回只会把占位符固化进 Live;返回 Ok(false)
|
||||
// 让调用方落到"清理占位符"兜底。
|
||||
if Self::live_has_proxy_placeholder_for_app(app_type, &provider.settings_config) {
|
||||
log::warn!(
|
||||
"{app_type:?} 当前供应商配置含代理接管占位符(疑似接管期间被导入的残留),跳过 SSOT 写回,改走占位符清理"
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
write_live_with_common_config(self.db.as_ref(), app_type, provider)
|
||||
.map_err(|e| format!("写入 {app_type:?} Live 配置失败: {e}"))?;
|
||||
|
||||
|
||||
@@ -588,30 +588,3 @@ fn switch_provider_codex_missing_auth_returns_error_and_keeps_state() {
|
||||
"current provider should remain empty or be the attempted id on failure, got: {current_id:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_refuses_live_config_under_proxy_takeover() {
|
||||
let _guard = test_mutex().lock().expect("acquire test mutex");
|
||||
reset_test_fs();
|
||||
ensure_test_home();
|
||||
|
||||
// 接管态 Codex Live:auth 是 PROXY_MANAGED 占位符,不是用户真实配置
|
||||
let auth = json!({"OPENAI_API_KEY": "PROXY_MANAGED"});
|
||||
let config = r#"model = "gpt-5"
|
||||
"#;
|
||||
write_codex_live_atomic(&auth, Some(config)).expect("seed taken-over codex live");
|
||||
|
||||
let state = create_test_state().expect("create test state");
|
||||
|
||||
import_default_config_test_hook(&state, AppType::Codex)
|
||||
.expect_err("importing a taken-over live config must fail");
|
||||
|
||||
let providers = state
|
||||
.db
|
||||
.get_all_providers(AppType::Codex.as_str())
|
||||
.expect("get codex providers");
|
||||
assert!(
|
||||
providers.is_empty(),
|
||||
"taken-over live import must not create providers"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1911,62 +1911,3 @@ fn provider_service_delete_current_provider_returns_error() {
|
||||
other => panic!("expected Config/Message error, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recover_from_crash_without_backup_cleans_placeholder_instead_of_writing_it_back() {
|
||||
let _guard = test_mutex().lock().expect("acquire test mutex");
|
||||
reset_test_fs();
|
||||
let _home = ensure_test_home();
|
||||
|
||||
// 接管态 Claude Live,且 DB 中无备份(模拟切换 app_config_dir 后新库首启的场景)
|
||||
let taken_over_live = json!({
|
||||
"env": {
|
||||
"ANTHROPIC_BASE_URL": "http://127.0.0.1:15721",
|
||||
"ANTHROPIC_AUTH_TOKEN": "PROXY_MANAGED"
|
||||
}
|
||||
});
|
||||
let settings_path = get_claude_settings_path();
|
||||
std::fs::create_dir_all(settings_path.parent().expect("settings dir")).expect("create dir");
|
||||
std::fs::write(
|
||||
&settings_path,
|
||||
serde_json::to_string_pretty(&taken_over_live).expect("serialize taken over live"),
|
||||
)
|
||||
.expect("write taken over live");
|
||||
|
||||
let state = create_test_state().expect("create test state");
|
||||
|
||||
// 模拟历史异常:接管态 Live 已被导入成 current provider(SSOT 被污染)
|
||||
let provider = Provider::with_id(
|
||||
"default".to_string(),
|
||||
"default".to_string(),
|
||||
taken_over_live.clone(),
|
||||
None,
|
||||
);
|
||||
state
|
||||
.db
|
||||
.save_provider(AppType::Claude.as_str(), &provider)
|
||||
.expect("save placeholder provider");
|
||||
state
|
||||
.db
|
||||
.set_current_provider(AppType::Claude.as_str(), "default")
|
||||
.expect("set current provider");
|
||||
|
||||
futures::executor::block_on(state.proxy_service.recover_from_crash())
|
||||
.expect("recover from crash");
|
||||
|
||||
let live_after: serde_json::Value =
|
||||
read_json_file(&settings_path).expect("read live settings after recovery");
|
||||
let env = live_after.get("env").cloned().unwrap_or_else(|| json!({}));
|
||||
assert_ne!(
|
||||
env.get("ANTHROPIC_AUTH_TOKEN").and_then(|v| v.as_str()),
|
||||
Some("PROXY_MANAGED"),
|
||||
"recovery must not write the placeholder back to live"
|
||||
);
|
||||
assert!(
|
||||
env.get("ANTHROPIC_BASE_URL")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|url| !url.starts_with("http://127.0.0.1"))
|
||||
.unwrap_or(true),
|
||||
"recovery must drop the local proxy base URL"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@ import type {
|
||||
ToolInstallationReport,
|
||||
} from "@/lib/api/settings";
|
||||
import { useUpdate } from "@/contexts/UpdateContext";
|
||||
import { relaunchApp } from "@/lib/updater";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { motion } from "framer-motion";
|
||||
import appIcon from "@/assets/icons/app-icon.png";
|
||||
@@ -195,14 +194,8 @@ export function AboutSection({ isPortable }: AboutSectionProps) {
|
||||
);
|
||||
const [showInstallCommands, setShowInstallCommands] = useState(false);
|
||||
|
||||
const {
|
||||
hasUpdate,
|
||||
updateInfo,
|
||||
updateHandle,
|
||||
checkUpdate,
|
||||
resetDismiss,
|
||||
isChecking,
|
||||
} = useUpdate();
|
||||
const { hasUpdate, updateInfo, checkUpdate, resetDismiss, isChecking } =
|
||||
useUpdate();
|
||||
|
||||
const [wslShellByTool, setWslShellByTool] = useState<
|
||||
Record<string, WslShellPreference>
|
||||
@@ -392,7 +385,7 @@ export function AboutSection({ isPortable }: AboutSectionProps) {
|
||||
}, [t, updateInfo?.availableVersion, version]);
|
||||
|
||||
const handleCheckUpdate = useCallback(async () => {
|
||||
if (hasUpdate && updateHandle) {
|
||||
if (hasUpdate) {
|
||||
if (isPortable) {
|
||||
try {
|
||||
await settingsApi.checkUpdates();
|
||||
@@ -405,11 +398,16 @@ export function AboutSection({ isPortable }: AboutSectionProps) {
|
||||
setIsDownloading(true);
|
||||
try {
|
||||
resetDismiss();
|
||||
await updateHandle.downloadAndInstall();
|
||||
await relaunchApp();
|
||||
const installed = await settingsApi.installUpdateAndRestart();
|
||||
if (!installed) {
|
||||
toast.success(t("settings.upToDate"), { closeButton: true });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[AboutSection] Update failed", error);
|
||||
toast.error(t("settings.updateFailed"));
|
||||
toast.error(t("settings.updateFailed"), {
|
||||
description: extractErrorMessage(error) || undefined,
|
||||
closeButton: true,
|
||||
});
|
||||
try {
|
||||
await settingsApi.checkUpdates();
|
||||
} catch (fallbackError) {
|
||||
@@ -433,7 +431,7 @@ export function AboutSection({ isPortable }: AboutSectionProps) {
|
||||
console.error("[AboutSection] Check update failed", error);
|
||||
toast.error(t("settings.checkUpdateFailed"));
|
||||
}
|
||||
}, [checkUpdate, hasUpdate, isPortable, resetDismiss, t, updateHandle]);
|
||||
}, [checkUpdate, hasUpdate, isPortable, resetDismiss, t]);
|
||||
|
||||
const handleCopyInstallCommands = useCallback(async () => {
|
||||
try {
|
||||
|
||||
@@ -6,14 +6,13 @@ import React, {
|
||||
useCallback,
|
||||
useRef,
|
||||
} from "react";
|
||||
import type { UpdateInfo, UpdateHandle } from "../lib/updater";
|
||||
import type { UpdateInfo } from "../lib/updater";
|
||||
import { checkForUpdate } from "../lib/updater";
|
||||
|
||||
interface UpdateContextValue {
|
||||
// 更新状态
|
||||
hasUpdate: boolean;
|
||||
updateInfo: UpdateInfo | null;
|
||||
updateHandle: UpdateHandle | null;
|
||||
isChecking: boolean;
|
||||
error: string | null;
|
||||
|
||||
@@ -34,7 +33,6 @@ export function UpdateProvider({ children }: { children: React.ReactNode }) {
|
||||
|
||||
const [hasUpdate, setHasUpdate] = useState(false);
|
||||
const [updateInfo, setUpdateInfo] = useState<UpdateInfo | null>(null);
|
||||
const [updateHandle, setUpdateHandle] = useState<UpdateHandle | null>(null);
|
||||
const [isChecking, setIsChecking] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isDismissed, setIsDismissed] = useState(false);
|
||||
@@ -72,7 +70,6 @@ export function UpdateProvider({ children }: { children: React.ReactNode }) {
|
||||
if (result.status === "available") {
|
||||
setHasUpdate(true);
|
||||
setUpdateInfo(result.info);
|
||||
setUpdateHandle(result.update);
|
||||
|
||||
// 检查是否已经关闭过这个版本的提醒
|
||||
let dismissedVersion = localStorage.getItem(DISMISSED_VERSION_KEY);
|
||||
@@ -89,7 +86,6 @@ export function UpdateProvider({ children }: { children: React.ReactNode }) {
|
||||
} else {
|
||||
setHasUpdate(false);
|
||||
setUpdateInfo(null);
|
||||
setUpdateHandle(null);
|
||||
setIsDismissed(false);
|
||||
return false; // 已是最新
|
||||
}
|
||||
@@ -132,7 +128,6 @@ export function UpdateProvider({ children }: { children: React.ReactNode }) {
|
||||
const value: UpdateContextValue = {
|
||||
hasUpdate,
|
||||
updateInfo,
|
||||
updateHandle,
|
||||
isChecking,
|
||||
error,
|
||||
isDismissed,
|
||||
|
||||
@@ -36,6 +36,10 @@ export const settingsApi = {
|
||||
return await invoke("restart_app");
|
||||
},
|
||||
|
||||
async installUpdateAndRestart(): Promise<boolean> {
|
||||
return await invoke("install_update_and_restart");
|
||||
},
|
||||
|
||||
async checkUpdates(): Promise<void> {
|
||||
await invoke("check_for_updates");
|
||||
},
|
||||
|
||||
+5
-83
@@ -1,22 +1,7 @@
|
||||
import { getVersion } from "@tauri-apps/api/app";
|
||||
|
||||
// 可选导入:在未注册插件或非 Tauri 环境下,调用时会抛错,外层需做兜底
|
||||
// 我们按需加载并在运行时捕获错误,避免构建期类型问题
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||
import type { Update } from "@tauri-apps/plugin-updater";
|
||||
|
||||
export type UpdateChannel = "stable" | "beta";
|
||||
|
||||
export type UpdaterPhase =
|
||||
| "idle"
|
||||
| "checking"
|
||||
| "available"
|
||||
| "downloading"
|
||||
| "installing"
|
||||
| "restarting"
|
||||
| "upToDate"
|
||||
| "error";
|
||||
|
||||
export interface UpdateInfo {
|
||||
currentVersion: string;
|
||||
availableVersion: string;
|
||||
@@ -24,64 +9,11 @@ export interface UpdateInfo {
|
||||
pubDate?: string;
|
||||
}
|
||||
|
||||
export interface UpdateProgressEvent {
|
||||
event: "Started" | "Progress" | "Finished";
|
||||
total?: number;
|
||||
downloaded?: number;
|
||||
}
|
||||
|
||||
export interface UpdateHandle {
|
||||
version: string;
|
||||
notes?: string;
|
||||
date?: string;
|
||||
downloadAndInstall: (
|
||||
onProgress?: (e: UpdateProgressEvent) => void,
|
||||
) => Promise<void>;
|
||||
download?: () => Promise<void>;
|
||||
install?: () => Promise<void>;
|
||||
}
|
||||
|
||||
export interface CheckOptions {
|
||||
timeout?: number;
|
||||
channel?: UpdateChannel;
|
||||
}
|
||||
|
||||
function mapUpdateHandle(raw: Update): UpdateHandle {
|
||||
return {
|
||||
version: (raw as any).version ?? "",
|
||||
notes: (raw as any).notes,
|
||||
date: (raw as any).date,
|
||||
async downloadAndInstall(onProgress?: (e: UpdateProgressEvent) => void) {
|
||||
await (raw as any).downloadAndInstall((evt: any) => {
|
||||
if (!onProgress) return;
|
||||
const mapped: UpdateProgressEvent = {
|
||||
event: evt?.event,
|
||||
};
|
||||
if (evt?.event === "Started") {
|
||||
mapped.total = evt?.data?.contentLength ?? 0;
|
||||
mapped.downloaded = 0;
|
||||
} else if (evt?.event === "Progress") {
|
||||
mapped.downloaded = evt?.data?.chunkLength ?? 0; // 累积由调用方完成
|
||||
}
|
||||
onProgress(mapped);
|
||||
});
|
||||
},
|
||||
// 透传可选 API(若插件版本支持)
|
||||
download: (raw as any).download
|
||||
? async () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
await (raw as any).download();
|
||||
}
|
||||
: undefined,
|
||||
install: (raw as any).install
|
||||
? async () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
await (raw as any).install();
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getCurrentVersion(): Promise<string> {
|
||||
try {
|
||||
return await getVersion();
|
||||
@@ -93,8 +25,7 @@ export async function getCurrentVersion(): Promise<string> {
|
||||
export async function checkForUpdate(
|
||||
opts: CheckOptions = {},
|
||||
): Promise<
|
||||
| { status: "up-to-date" }
|
||||
| { status: "available"; info: UpdateInfo; update: UpdateHandle }
|
||||
{ status: "up-to-date" } | { status: "available"; info: UpdateInfo }
|
||||
> {
|
||||
// 动态引入,避免在未安装插件时导致打包期问题
|
||||
const { check } = await import("@tauri-apps/plugin-updater");
|
||||
@@ -106,21 +37,12 @@ export async function checkForUpdate(
|
||||
return { status: "up-to-date" };
|
||||
}
|
||||
|
||||
const mapped = mapUpdateHandle(update);
|
||||
const info: UpdateInfo = {
|
||||
currentVersion,
|
||||
availableVersion: mapped.version,
|
||||
notes: mapped.notes,
|
||||
pubDate: mapped.date,
|
||||
availableVersion: (update as any).version ?? "",
|
||||
notes: (update as any).notes,
|
||||
pubDate: (update as any).date,
|
||||
};
|
||||
|
||||
return { status: "available", info, update: mapped };
|
||||
return { status: "available", info };
|
||||
}
|
||||
|
||||
export async function relaunchApp(): Promise<void> {
|
||||
const { relaunch } = await import("@tauri-apps/plugin-process");
|
||||
await relaunch();
|
||||
}
|
||||
|
||||
// 旧的聚合更新流程已由调用方直接使用 updateHandle 取代
|
||||
// 如需单函数封装,可在需要时基于 checkForUpdate + updateHandle 复合调用
|
||||
|
||||
Reference in New Issue
Block a user