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()}
/>