diff --git a/openless-all/app/src-tauri/src/lib.rs b/openless-all/app/src-tauri/src/lib.rs index f2e28569..0b449b71 100644 --- a/openless-all/app/src-tauri/src/lib.rs +++ b/openless-all/app/src-tauri/src/lib.rs @@ -288,6 +288,7 @@ macro_rules! app_invoke_handler_desktop { commands::sherpa_onnx_asr_reveal_model_dir, commands::export_error_log, restart_app, + log_client_error, set_windows_caption_theme, ] }; @@ -378,6 +379,7 @@ macro_rules! app_invoke_handler_mobile { $crate::commands::app_check_update_with_channel, $crate::commands::app_download_and_install_android_update, $crate::restart_app, + $crate::log_client_error, ] }; } @@ -1140,6 +1142,26 @@ fn restart_app(app: AppHandle) { app.restart(); } +/// 把前端的关键错误(如自动更新 install 失败)转发到 Rust 文件日志(openless.log)。 +/// webview 的 console.error 不会进 openless.log,单独留一个 IPC,便于用户「导出日志」 +/// 后我们拿到自动更新失败的真实原因。 +#[tauri::command] +fn log_client_error(message: String) { + // message 由前端 webview 可控,可能很长或含换行(伪造日志行)。先把换行折成空格、 + // 再按 UTF-8 字符边界截断,避免单条日志过大或污染日志格式。 + const MAX_LEN: usize = 2048; + let mut sanitized = message.replace(['\n', '\r'], " "); + if sanitized.len() > MAX_LEN { + let mut end = MAX_LEN; + while !sanitized.is_char_boundary(end) { + end -= 1; + } + sanitized.truncate(end); + sanitized.push_str("…(truncated)"); + } + log::error!("[client] {sanitized}"); +} + #[cfg(target_os = "macos")] fn reset_tcc_for_beta_restart() { if !is_beta_build() { diff --git a/openless-all/app/src/components/AutoUpdate.tsx b/openless-all/app/src/components/AutoUpdate.tsx index ac9a008e..ec321d03 100644 --- a/openless-all/app/src/components/AutoUpdate.tsx +++ b/openless-all/app/src/components/AutoUpdate.tsx @@ -15,6 +15,8 @@ import { appDownloadAndInstallAndroidUpdate, isAndroid, isTauri, + logClientError, + openExternal, restartApp, type AppUpdateMetadata, type UpdateChannel, @@ -23,6 +25,9 @@ import { Btn } from '../pages/_atoms'; const UPDATE_CHECK_TIMEOUT_MS = 15_000; +// 自动更新失败时的手动下载兜底:直达 GitHub Releases(与「关于」页 RELEASE_NOTES_URL 一致)。 +const RELEASE_DOWNLOAD_URL = 'https://github.com/appergb/openless/releases'; + export type UpdateStatus = | 'idle' | 'checking' @@ -31,7 +36,11 @@ export type UpdateStatus = | 'downloading' | 'installing' | 'downloaded' - | 'error'; + | 'error' + // installError:下载/安装这一步失败(区别于 'error' 的「检查失败」)。 + // 检查失败沿用 'error',在 CheckUpdateButton 里只做按钮内轻提示、不弹框, + // 后台自动检查失败也不弹框;只有 installError 才让弹框留在原地显示错误 + 手动下载兜底。 + | 'installError'; export interface UseAutoUpdate { status: UpdateStatus; @@ -162,6 +171,7 @@ export function useAutoUpdate(): UseAutoUpdate { } catch (error) { console.error('[updater] failed to check update', error); const msg = error instanceof Error ? error.message : String(error); + void logClientError(`[updater] check failed: ${msg}`); setErrorMessage(msg); setStatus('error'); } @@ -180,8 +190,9 @@ export function useAutoUpdate(): UseAutoUpdate { } catch (error) { console.error('[updater] failed to install android update', error); const msg = error instanceof Error ? error.message : String(error); + void logClientError(`[updater] android install failed (v${payload.version}): ${msg}`); setErrorMessage(msg); - setStatus('error'); + setStatus('installError'); } return; } @@ -208,9 +219,10 @@ export function useAutoUpdate(): UseAutoUpdate { } catch (error) { console.error('[updater] failed to install update', error); const msg = error instanceof Error ? error.message : String(error); + void logClientError(`[updater] install failed (v${update.version}): ${msg}`); setErrorMessage(msg); await closeUpdate(); - setStatus('error'); + setStatus('installError'); } }; @@ -237,8 +249,8 @@ export function useAutoUpdate(): UseAutoUpdate { }; } -export function isDialogStatus(status: UpdateStatus): status is 'available' | 'downloading' | 'installing' | 'downloaded' { - return status === 'available' || status === 'downloading' || status === 'installing' || status === 'downloaded'; +export function isDialogStatus(status: UpdateStatus): status is 'available' | 'downloading' | 'installing' | 'downloaded' | 'installError' { + return status === 'available' || status === 'downloading' || status === 'installing' || status === 'downloaded' || status === 'installError'; } export function UpdateDialog({ @@ -247,29 +259,34 @@ export function UpdateDialog({ progress, downloaded, contentLength, + errorMessage, onInstall, onClose, }: { - status: 'available' | 'downloading' | 'installing' | 'downloaded'; + status: 'available' | 'downloading' | 'installing' | 'downloaded' | 'installError'; version: string; progress: number | null; downloaded: number; contentLength: number | null; + errorMessage?: string | null; onInstall: () => void; onClose: () => void; }) { const { t } = useTranslation(); const downloading = status === 'downloading'; const installing = status === 'installing'; + const installError = status === 'installError'; const androidInstalled = isAndroid() && status === 'downloaded'; return (
{t(`settings.about.updateDialog.${status}.title`)}
-
+
{androidInstalled ? t('settings.about.updateDialog.androidInstalled.desc', { version, defaultValue: '系统安装器已打开,请按提示完成安装。安装后重新打开 OpenLess 即可使用 {{version}}。' }) - : t(`settings.about.updateDialog.${status}.desc`, { version })} + : installError + ? t('settings.about.updateDialog.installError.desc', { error: errorMessage || t('settings.about.updateError') }) + : t(`settings.about.updateDialog.${status}.desc`, { version })}
{(downloading || installing || status === 'downloaded') && (
@@ -291,6 +308,8 @@ export function UpdateDialog({ {(downloading || installing) && {installing ? t('settings.about.updateDialog.installingLabel') : t('settings.about.updateDialog.downloadingLabel')}} {status === 'downloaded' && {t('settings.about.updateDialog.later')}} {status === 'downloaded' && !androidInstalled && {t('settings.about.updateDialog.restartNow')}} + {installError && {t('common.cancel')}} + {installError && void openExternal(RELEASE_DOWNLOAD_URL)}>{t('settings.about.updateDialog.manualDownload')}}
diff --git a/openless-all/app/src/components/AutoUpdateGate.tsx b/openless-all/app/src/components/AutoUpdateGate.tsx index 7ff1b6b7..a5a3dbc2 100644 --- a/openless-all/app/src/components/AutoUpdateGate.tsx +++ b/openless-all/app/src/components/AutoUpdateGate.tsx @@ -60,6 +60,7 @@ export function AutoUpdateGate() { progress={u.progress} downloaded={u.downloaded} contentLength={u.contentLength} + errorMessage={u.errorMessage} onInstall={u.installUpdate} onClose={u.dismissDialog} /> diff --git a/openless-all/app/src/i18n/en.ts b/openless-all/app/src/i18n/en.ts index fcf71ca3..0e7f57b2 100644 --- a/openless-all/app/src/i18n/en.ts +++ b/openless-all/app/src/i18n/en.ts @@ -1041,6 +1041,11 @@ export const en: typeof zhCN = { restartNow: 'Restart now', progress: '{{progress}}% · {{downloaded}} / {{total}}', progressUnknown: '{{downloaded}} downloaded', + installError: { + title: 'Update failed', + desc: "The automatic update couldn't finish: {{error}}. You can download and install the latest version manually.", + }, + manualDownload: 'Download manually', }, }, }, diff --git a/openless-all/app/src/i18n/ja.ts b/openless-all/app/src/i18n/ja.ts index 257bfb7f..98cc54c8 100644 --- a/openless-all/app/src/i18n/ja.ts +++ b/openless-all/app/src/i18n/ja.ts @@ -1009,6 +1009,11 @@ export const ja: typeof zhCN = { restartNow: '今すぐ再起動', progress: '{{progress}}% · {{downloaded}} / {{total}}', progressUnknown: 'ダウンロード済み {{downloaded}}', + installError: { + title: '更新に失敗しました', + desc: '自動更新を完了できませんでした:{{error}}。ダウンロードページから手動で最新版を入手できます。', + }, + manualDownload: '手動でダウンロード', }, }, }, diff --git a/openless-all/app/src/i18n/ko.ts b/openless-all/app/src/i18n/ko.ts index 395f92e1..0ec2d055 100644 --- a/openless-all/app/src/i18n/ko.ts +++ b/openless-all/app/src/i18n/ko.ts @@ -1009,6 +1009,11 @@ export const ko: typeof zhCN = { restartNow: '지금 재시작', progress: '{{progress}}% · {{downloaded}} / {{total}}', progressUnknown: '다운로드됨 {{downloaded}}', + installError: { + title: '업데이트 실패', + desc: '자동 업데이트를 완료하지 못했습니다: {{error}}. 다운로드 페이지에서 최신 버전을 직접 받아 설치할 수 있습니다.', + }, + manualDownload: '수동 다운로드', }, }, }, diff --git a/openless-all/app/src/i18n/zh-CN.ts b/openless-all/app/src/i18n/zh-CN.ts index 3147b416..890004e4 100644 --- a/openless-all/app/src/i18n/zh-CN.ts +++ b/openless-all/app/src/i18n/zh-CN.ts @@ -1039,6 +1039,11 @@ export const zhCN = { restartNow: '现在重启', progress: '{{progress}}% · {{downloaded}} / {{total}}', progressUnknown: '已下载 {{downloaded}}', + installError: { + title: '更新失败', + desc: '自动更新没能完成:{{error}}。你可以前往下载页手动下载安装最新版本。', + }, + manualDownload: '手动下载', }, }, }, diff --git a/openless-all/app/src/i18n/zh-TW.ts b/openless-all/app/src/i18n/zh-TW.ts index 2fe58825..25324a6e 100644 --- a/openless-all/app/src/i18n/zh-TW.ts +++ b/openless-all/app/src/i18n/zh-TW.ts @@ -1007,6 +1007,11 @@ export const zhTW: typeof zhCN = { restartNow: '現在重啓', progress: '{{progress}}% · {{downloaded}} / {{total}}', progressUnknown: '已下載 {{downloaded}}', + installError: { + title: '更新失敗', + desc: '自動更新未能完成:{{error}}。你可以前往下載頁手動下載安裝最新版本。', + }, + manualDownload: '手動下載', }, }, }, diff --git a/openless-all/app/src/lib/ipc/index.ts b/openless-all/app/src/lib/ipc/index.ts index b0afaf42..f83f7ef8 100644 --- a/openless-all/app/src/lib/ipc/index.ts +++ b/openless-all/app/src/lib/ipc/index.ts @@ -195,4 +195,4 @@ export { } from "./marketplace-cache" // utils -export { openExternal, exportErrorLog } from "./utils" +export { openExternal, exportErrorLog, logClientError } from "./utils" diff --git a/openless-all/app/src/lib/ipc/utils.ts b/openless-all/app/src/lib/ipc/utils.ts index 3f2ef98f..233d1733 100644 --- a/openless-all/app/src/lib/ipc/utils.ts +++ b/openless-all/app/src/lib/ipc/utils.ts @@ -47,3 +47,16 @@ export async function exportErrorLog( ) return target } + +/** + * 把前端关键错误(如自动更新 install 失败)转发到 Rust 文件日志(openless.log)。 + * webview 的 console.error 不会落进 openless.log,单独走 IPC,便于用户「导出日志」 + * 后我们拿到失败的真实原因。永不抛错——日志失败不应再影响调用方的错误处理。 + */ +export async function logClientError(message: string): Promise { + try { + await invokeOrMock("log_client_error", { message }, () => undefined) + } catch (error) { + console.warn("[log-client-error] failed to forward error to app log", error) + } +} diff --git a/openless-all/app/src/pages/settings/CheckUpdateButton.tsx b/openless-all/app/src/pages/settings/CheckUpdateButton.tsx index 19106fd2..73cb3132 100644 --- a/openless-all/app/src/pages/settings/CheckUpdateButton.tsx +++ b/openless-all/app/src/pages/settings/CheckUpdateButton.tsx @@ -62,6 +62,7 @@ export function CheckUpdateButton({ channel }: { channel: UpdateChannel }) { progress={updater.progress} downloaded={updater.downloaded} contentLength={updater.contentLength} + errorMessage={updater.errorMessage} onInstall={() => void updater.installUpdate()} onClose={() => void updater.dismissDialog()} />