diff --git a/openless-all/app/package-lock.json b/openless-all/app/package-lock.json index bafe4e7e..b4e8a01a 100644 --- a/openless-all/app/package-lock.json +++ b/openless-all/app/package-lock.json @@ -1,12 +1,12 @@ { "name": "openless-app", - "version": "1.3.4-1", + "version": "1.3.4-2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "openless-app", - "version": "1.3.4-1", + "version": "1.3.4-2", "dependencies": { "@tauri-apps/api": "^2.1.1", "@tauri-apps/plugin-autostart": "^2.5.1", diff --git a/openless-all/app/package.json b/openless-all/app/package.json index ac93264d..27ac0df4 100644 --- a/openless-all/app/package.json +++ b/openless-all/app/package.json @@ -1,7 +1,7 @@ { "name": "openless-app", "private": true, - "version": "1.3.4-1", + "version": "1.3.4-2", "type": "module", "scripts": { "dev": "vite", diff --git a/openless-all/app/src-tauri/Cargo.lock b/openless-all/app/src-tauri/Cargo.lock index f00eda50..0e5eb23b 100644 --- a/openless-all/app/src-tauri/Cargo.lock +++ b/openless-all/app/src-tauri/Cargo.lock @@ -3751,7 +3751,7 @@ dependencies = [ [[package]] name = "openless" -version = "1.3.4-1" +version = "1.3.4-2" dependencies = [ "anyhow", "arboard", diff --git a/openless-all/app/src-tauri/Cargo.toml b/openless-all/app/src-tauri/Cargo.toml index 9054e48f..0fa46bcd 100644 --- a/openless-all/app/src-tauri/Cargo.toml +++ b/openless-all/app/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "openless" -version = "1.3.4-1" +version = "1.3.4-2" description = "OpenLess — local voice input that types where your cursor is" authors = ["OpenLess"] edition = "2021" diff --git a/openless-all/app/src-tauri/src/commands.rs b/openless-all/app/src-tauri/src/commands.rs index 246a605b..57e2b3b2 100644 --- a/openless-all/app/src-tauri/src/commands.rs +++ b/openless-all/app/src-tauri/src/commands.rs @@ -271,11 +271,13 @@ pub(crate) fn activate_builtin_style_mode( // 渠道偏好的写入路径跟 set_settings 复用 persist_settings:保持热键兜底归一化 // 跟其他 prefs 写入一致,且写完后 emit "prefs:changed",让前端跨 webview 同步。 // -// 注意:plugin-updater 2.10 的 Builder 不暴露 endpoints() 运行时 API,因此切到 Beta -// 渠道**不会**改变 in-app「检查更新」的行为——它仍然只看正式版 manifest。Beta 用户 -// 通过 `fetch_latest_beta_release` 获取最新 prerelease,由前端跳浏览器手动下载, -// 物理隔离 Beta 包不会通过 auto-update 推到正式版用户。详见 PR-B-2 description 与 -// CLAUDE.md `Branch & release-channel workflow` 段落。 +// 更新:plugin-updater 2.10.1 的 Builder 现在暴露 .endpoints() runtime API(CLAUDE.md +// 当年记的"不支持"已不成立)。本节配合 `app_check_update_with_channel` 命令实现 +// Beta auto-update:Stable 渠道 → 走 tauri.conf 的默认 endpoints;Beta 渠道 → +// fetch_latest_beta_release 拿最新 prerelease tag → 拼成 -beta manifest URL → +// builder.endpoints(vec![url]).build().check()。Stable 用户绝对不会撞到 Beta 包 +// (Beta tag 的 manifest 文件名带 `-beta` 后缀,跟 Stable manifest 在 GitHub +// Release assets 里物理分离)。 #[tauri::command] pub fn get_update_channel(coord: CoordinatorState<'_>) -> UpdateChannel { @@ -377,6 +379,101 @@ fn extract_between(haystack: &str, open: &str, close: &str) -> Option { Some(haystack[start..start + end].to_string()) } +// ─────────────────────── Channel-aware updater check ──────────────────────── +// +// 替换前端原来直接 import('@tauri-apps/plugin-updater').check() 的路径: +// - Stable 渠道:builder 不动 endpoints,沿用 tauri.conf 配的 stable manifest URL。 +// - Beta 渠道:先 fetch_latest_beta_release 拿最新 prerelease tag,拼成 -beta manifest +// URL(同时给一对 mirror + direct),再 builder.endpoints(vec![url])?.build()?.check()。 +// +// 返回的 Metadata 形状与 plugin-updater 的 JS UpdateMetadata 完全一致(rid + +// currentVersion 等驼峰字段),前端可以直接 `new Update(metadata)` 复用 plugin +// 的 download / install / close 实现,无需我们自己写下载和签名校验。 +// +// 物理隔离:Beta tag 推出来的 manifest 文件名带 `-beta` 后缀(参见 release-tauri.yml +// 第 382 行注释),跟 Stable 的 `latest-{tgt}-{arch}.json` 在 GitHub Release assets +// 里是分开的两份文件 —— 即使代码逻辑写错把 Beta URL 传给 Stable 用户,HTTP 也是 +// 直接 404,绝不会拿到错档。 + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AppUpdateMetadata { + pub rid: tauri::ResourceId, + pub current_version: String, + pub version: String, + pub date: Option, + pub body: Option, + /// 原始 manifest JSON——`new Update(metadata)` 在 JS 那边会校验它存在; + /// 我们透传 plugin 自己 check 时拿到的字段。 + pub raw_json: serde_json::Value, +} + +/// 按 prefs.update_channel 决定 manifest 来源,再走 plugin-updater 的标准 check 流程。 +/// 返回 None = 当前是最新;Some(metadata) = 有新版可装。 +#[tauri::command] +pub async fn app_check_update_with_channel( + coord: CoordinatorState<'_>, + webview: tauri::Webview, + timeout_ms: Option, +) -> Result, String> { + use tauri_plugin_updater::UpdaterExt; + + let channel = coord.prefs().get().update_channel; + let mut builder = webview.updater_builder(); + if let Some(ms) = timeout_ms { + builder = builder.timeout(std::time::Duration::from_millis(ms)); + } + if matches!(channel, UpdateChannel::Beta) { + let urls = resolve_beta_manifest_endpoints().await?; + builder = builder + .endpoints(urls) + .map_err(|e| format!("set beta endpoints: {e}"))?; + } + let updater = builder.build().map_err(|e| format!("build updater: {e}"))?; + let update = updater + .check() + .await + .map_err(|e| format!("check update failed: {e}"))?; + + let Some(update) = update else { + return Ok(None); + }; + // date 字段透传需要引 time crate;前端 AutoUpdate.tsx 实际并不用 date,所以这里 + // 直接置 None,避免拉一个新 dep 进 src-tauri/Cargo.toml。 + let metadata = AppUpdateMetadata { + current_version: update.current_version.clone(), + version: update.version.clone(), + date: None, + body: update.body.clone(), + raw_json: update.raw_json.clone(), + rid: webview.resources_table().add(update), + }; + Ok(Some(metadata)) +} + +/// 把 fetch_latest_beta_release 找到的最新 prerelease tag 拼成 -beta manifest URL 对。 +/// 顺序:先镜像(fastgit.cc 代理 GitHub),后直连 —— 跟 tauri.conf 现有 Stable +/// endpoints 一致,让国内访问优先打到 CDN。 +async fn resolve_beta_manifest_endpoints() -> Result, String> { + let Some(latest) = fetch_latest_beta_release().await? else { + return Err("尚未发布过 Beta 版本".to_string()); + }; + let tag = latest.tag_name; + // {{target}} / {{arch}} 占位符由 plugin 在 check 时替换。Rust raw string 用 r#""# + // 不需要转义双花括号,比 format! 干净。 + let mirror = format!( + "https://fastgit.cc/https://github.com/appergb/openless/releases/download/{tag}/latest-{{{{target}}}}-{{{{arch}}}}-beta-mirror.json" + ); + let direct = format!( + "https://github.com/appergb/openless/releases/download/{tag}/latest-{{{{target}}}}-{{{{arch}}}}-beta.json" + ); + let mirror_url = + url::Url::parse(&mirror).map_err(|e| format!("parse beta mirror url: {e}"))?; + let direct_url = + url::Url::parse(&direct).map_err(|e| format!("parse beta direct url: {e}"))?; + Ok(vec![mirror_url, direct_url]) +} + #[tauri::command] pub fn get_hotkey_status(coord: CoordinatorState<'_>) -> HotkeyStatus { coord.hotkey_status() diff --git a/openless-all/app/src-tauri/src/lib.rs b/openless-all/app/src-tauri/src/lib.rs index 9b758258..adea1faf 100644 --- a/openless-all/app/src-tauri/src/lib.rs +++ b/openless-all/app/src-tauri/src/lib.rs @@ -270,6 +270,7 @@ pub fn run() { commands::get_update_channel, commands::set_update_channel, commands::fetch_latest_beta_release, + commands::app_check_update_with_channel, commands::get_hotkey_status, commands::get_hotkey_capability, commands::is_wayland_cli_mode, diff --git a/openless-all/app/src-tauri/tauri.conf.json b/openless-all/app/src-tauri/tauri.conf.json index cf73d5aa..c5bbfeb1 100644 --- a/openless-all/app/src-tauri/tauri.conf.json +++ b/openless-all/app/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "OpenLess", - "version": "1.3.4-1", + "version": "1.3.4-2", "identifier": "com.openless.app", "build": { "beforeDevCommand": "npm run dev", diff --git a/openless-all/app/src/components/AutoUpdate.tsx b/openless-all/app/src/components/AutoUpdate.tsx index d5c3ec58..cc23e57d 100644 --- a/openless-all/app/src/components/AutoUpdate.tsx +++ b/openless-all/app/src/components/AutoUpdate.tsx @@ -1,14 +1,31 @@ // 自动更新共用模块 — Settings 的"关于"section 和 footer 按钮共用同一套 // 状态机 + 对话框 UI。两边各自调用 useAutoUpdate(),dialog 渲染条件相同。 +// +// 渠道感知:check 不再走 plugin-updater 的 JS check()(它只看 tauri.conf 配的 +// Stable manifest URL),改为 invoke('app_check_update_with_channel')。 +// Rust 那边按 prefs.update_channel 决定 manifest URL;返回的 metadata 直接 +// `new Update(metadata)` 复用 plugin 的 download / install / close 实现, +// 我们不重复造下载和签名校验。 import { useEffect, useRef, useState } from 'react'; -import type { Update, DownloadEvent } from '@tauri-apps/plugin-updater'; +import { invoke } from '@tauri-apps/api/core'; +import type { DownloadEvent } from '@tauri-apps/plugin-updater'; +import { Update } from '@tauri-apps/plugin-updater'; import { useTranslation } from 'react-i18next'; import { isTauri, restartApp } from '../lib/ipc'; import { Btn } from '../pages/_atoms'; const UPDATE_CHECK_TIMEOUT_MS = 8_000; // 缩短超时,让镜像站慢的情况能更快 fallback +interface AppUpdateMetadata { + rid: number; + currentVersion: string; + version: string; + date?: string | null; + body?: string | null; + rawJson: Record; +} + export type UpdateStatus = | 'idle' | 'checking' @@ -79,11 +96,28 @@ export function useAutoUpdate(): UseAutoUpdate { setStatus('none'); return; } - const { check } = await import('@tauri-apps/plugin-updater'); - const next = await check({ timeout: UPDATE_CHECK_TIMEOUT_MS }); + // Rust 侧按 update_channel 拼 manifest URL:Stable → tauri.conf 默认; + // Beta → fetch_latest_beta_release 拼出 -beta manifest URL 后再 check。 + const metadata = await invoke('app_check_update_with_channel', { + timeoutMs: UPDATE_CHECK_TIMEOUT_MS, + }); + if (!metadata) { + setStatus('none'); + return; + } + // metadata 形状跟 plugin 自己 check 返回的 UpdateMetadata 完全一致; + // new Update(metadata) 直接复用 plugin 的 download/install/close 实现。 + const next = new Update({ + rid: metadata.rid, + currentVersion: metadata.currentVersion, + version: metadata.version, + date: metadata.date ?? undefined, + body: metadata.body ?? undefined, + rawJson: metadata.rawJson, + }); updateRef.current = next; - setVersion(next?.version ?? ''); - setStatus(next ? 'available' : 'none'); + setVersion(next.version); + setStatus('available'); } catch (error) { console.error('[updater] failed to check update', error); setStatus('error'); diff --git a/openless-all/app/src/components/SettingsModal.tsx b/openless-all/app/src/components/SettingsModal.tsx index 01dff267..0245f64f 100644 --- a/openless-all/app/src/components/SettingsModal.tsx +++ b/openless-all/app/src/components/SettingsModal.tsx @@ -21,8 +21,38 @@ import { type LatestBetaRelease, type UpdateChannel, } from '../lib/ipc'; +import { APP_VERSION } from '../lib/appVersion'; +import { isDialogStatus, UpdateDialog, useAutoUpdate } from './AutoUpdate'; import type { OS } from './WindowChrome'; +// 把 Beta tag 名(如 "v1.3.4-1-beta-tauri" / "v1.3.2-3-beta-tauri")解析回 semver 版本号 +// "1.3.4-1" / "1.3.2-3"。失败返回 null(让 UI fallback 到字符串相等比对)。 +function parseVersionFromBetaTag(tag: string): string | null { + const trimmed = tag.replace(/^v/, '').replace(/-beta-tauri$/, ''); + return /^\d+\.\d+\.\d+(-\d+)?$/.test(trimmed) ? trimmed : null; +} + +// 简化版 semver 比较 —— 只覆盖本仓库实际用到的两种形态:X.Y.Z 和 X.Y.Z-N。 +// 返回 true 表示 a > b。按 SemVer 规则:major→minor→patch→prerelease(none > some)。 +function semverGreater(a: string, b: string): boolean { + const parse = (v: string) => { + const [main, pre] = v.split('-'); + const [major, minor, patch] = main.split('.').map(n => Number.parseInt(n, 10)); + const preNum = pre === undefined ? null : Number.parseInt(pre, 10); + return { major, minor, patch, preNum }; + }; + const A = parse(a); + const B = parse(b); + if (A.major !== B.major) return A.major > B.major; + if (A.minor !== B.minor) return A.minor > B.minor; + if (A.patch !== B.patch) return A.patch > B.patch; + // pre: null(无 prerelease)> 任何 prerelease + if (A.preNum === B.preNum) return false; + if (A.preNum === null) return true; + if (B.preNum === null) return false; + return A.preNum > B.preNum; +} + interface SettingsModalProps { os: OS; onClose: () => void; @@ -417,10 +447,13 @@ function AboutMini() { ); } -// Beta 渠道开关:物理隔离的 opt-in,不接 auto-update。 -// - 关闭状态 = 正式版渠道,默认行为,用户从「检查更新」拿正式 release -// - 打开 = 用户主动加入 Beta;写 prefs(无重启需要)+ 拉一次最新 prerelease 信息 -// - 点"打开 GitHub"跳浏览器到具体的 Beta release 页面,用户手动下载安装 +// Beta 渠道开关:物理隔离的 opt-in,**已接 auto-update**(PR feat/beta-auto-update)。 +// - 关闭 = Stable 渠道,「检查更新」走 tauri.conf 默认 endpoints +// - 打开 = 写 prefs.update_channel = 'beta';Rust 端 app_check_update_with_channel +// 命令会自动拉最新 prerelease tag 拼成 -beta manifest URL,再走 plugin-updater +// 的 check/download/install 标准流程 +// - 这里拉一次 fetch_latest_beta_release 仅用于「告诉用户最新 Beta 是哪个版本」做 +// 信息透明;不再渲染手动下载按钮(auto-update 接管了那条路)。 // 不在 Beta 渠道时不发起 GitHub API 请求,避免空切换浪费配额。 function BetaChannelControl() { const { t } = useTranslation(); @@ -428,6 +461,18 @@ function BetaChannelControl() { const [latest, setLatest] = useState(null); const [status, setStatus] = useState<'idle' | 'fetching' | 'empty' | 'error'>('idle'); const [errorMessage, setErrorMessage] = useState(''); + const updater = useAutoUpdate(); + + // 本机 vs 最新 Beta tag 比对: + // - 'unknown' → 还没拿到 latest 或解析失败,按"未知"渲染 + // - 'up-to-date' → 本机版本 >= 远端最新 Beta,按"已是最新"渲染、隐藏更新按钮 + // - 'newer' → 远端有更新版本,渲染「立即更新」按钮触发 auto-update 流程 + const remoteVersion = latest ? parseVersionFromBetaTag(latest.tagName) : null; + const comparison: 'unknown' | 'up-to-date' | 'newer' = !remoteVersion + ? 'unknown' + : semverGreater(remoteVersion, APP_VERSION) + ? 'newer' + : 'up-to-date'; useEffect(() => { let cancelled = false; @@ -495,9 +540,24 @@ function BetaChannelControl() { {t('settings.about.betaChannelLatestPrefix')} {latest.tagName} - + {comparison === 'up-to-date' && ( + {t('settings.about.betaChannelUpToDate')} + )} + {comparison === 'newer' && ( + + )} @@ -510,6 +570,17 @@ function BetaChannelControl() { )} )} + {isDialogStatus(updater.status) && ( + void updater.installUpdate()} + onClose={() => void updater.dismissDialog()} + /> + )} ); } diff --git a/openless-all/app/src/i18n/en.ts b/openless-all/app/src/i18n/en.ts index f53312a6..2075c5c5 100644 --- a/openless-all/app/src/i18n/en.ts +++ b/openless-all/app/src/i18n/en.ts @@ -782,7 +782,7 @@ export const en: typeof zhCN = { privacyDesc: 'All transcripts stay on this device. Cloud APIs are only called for real-time transcription/polish; no recordings are retained.', localFirst: 'Local-first', betaChannelLabel: 'Join Beta channel', - betaChannelDesc: 'Stable channel is the default. Enabling this exposes a manual download link to the latest Beta below; Beta builds are NOT pushed to regular users via auto-update — you have to download and install them yourself. May be unstable, only recommended if you are willing to test pre-release builds and report issues.', + betaChannelDesc: 'Stable channel is the default. When enabled, the app’s "Check for updates" auto-fetches the latest Beta release (with features not yet promoted to Stable). Beta builds are physically isolated from Stable manifests, so regular users are never affected. May be unstable; only recommended if you are willing to test pre-release builds and report issues.', betaChannelFetching: 'Fetching the latest Beta…', betaChannelFetchBtn: 'Look up latest Beta', betaChannelLatestPrefix: 'Latest Beta:', @@ -790,6 +790,10 @@ export const en: typeof zhCN = { betaChannelRefresh: 'Refresh', betaChannelNoBeta: 'No Beta release has been published yet.', betaChannelFetchError: 'Failed to fetch Beta release info. Please try again later.', + betaChannelUpToDate: 'Up to date', + betaChannelUpdateNow: 'Update now', + betaChannelUpdateNowTitle: 'Check and download the latest Beta, then show the update dialog', + betaChannelChecking: 'Checking…', updateDialog: { available: { title: 'Update available', diff --git a/openless-all/app/src/i18n/ja.ts b/openless-all/app/src/i18n/ja.ts index 55b124c4..897866d3 100644 --- a/openless-all/app/src/i18n/ja.ts +++ b/openless-all/app/src/i18n/ja.ts @@ -784,7 +784,7 @@ export const ja: typeof zhCN = { privacyDesc: 'すべての認識結果はローカルにのみ保存されます。クラウド API はリアルタイム転写と整文にのみ使用され、録音は保持されません。', localFirst: 'ローカル優先', betaChannelLabel: 'Beta チャンネルに参加', - betaChannelDesc: '既定は正式版です。オンにすると最新 Beta 版のダウンロードリンクが下に表示されます。Beta ビルドは自動更新で配布されず、手動でダウンロード・インストールする必要があります。不安定な場合があるため、検証とフィードバック協力に同意するユーザーのみ推奨。', + betaChannelDesc: '既定は正式版です。オンにすると、アプリの「更新を確認」が最新 Beta 版(Stable 未公開機能を含む)を自動取得します。Beta ビルドは Stable とマニフェストが物理的に分離されており、一般ユーザーへ波及しません。不安定な場合があるため、検証とフィードバック協力に同意するユーザーのみ推奨。', betaChannelFetching: '最新 Beta 版を取得中…', betaChannelFetchBtn: '最新 Beta を確認', betaChannelLatestPrefix: '最新 Beta:', @@ -792,6 +792,10 @@ export const ja: typeof zhCN = { betaChannelRefresh: '再取得', betaChannelNoBeta: 'まだ Beta リリースは公開されていません。', betaChannelFetchError: 'Beta バージョン情報の取得に失敗しました。後で再試行してください。', + betaChannelUpToDate: '最新です', + betaChannelUpdateNow: '今すぐ更新', + betaChannelUpdateNowTitle: '最新 Beta を確認・ダウンロードし、更新ダイアログを表示します', + betaChannelChecking: '確認中…', updateDialog: { available: { title: '新しいバージョンがあります', diff --git a/openless-all/app/src/i18n/ko.ts b/openless-all/app/src/i18n/ko.ts index eb583181..ee92ea0a 100644 --- a/openless-all/app/src/i18n/ko.ts +++ b/openless-all/app/src/i18n/ko.ts @@ -784,7 +784,7 @@ export const ko: typeof zhCN = { privacyDesc: '모든 인식 결과는 로컬에만 저장됩니다. 클라우드 API 는 실시간 전사와 정리에만 사용되며 녹음을 보관하지 않습니다.', localFirst: '로컬 우선', betaChannelLabel: 'Beta 채널 참여', - betaChannelDesc: '기본은 정식 버전입니다. 켜면 최신 Beta 버전 다운로드 링크가 아래에 표시됩니다. Beta 빌드는 자동 업데이트로 배포되지 않으며 직접 다운로드해 설치해야 합니다. 불안정할 수 있으므로 사전 평가와 피드백을 제공할 의향이 있는 사용자에게만 권장합니다.', + betaChannelDesc: '기본은 정식 버전입니다. 켜면 앱의 "업데이트 확인"이 최신 Beta 버전(아직 Stable에 올라가지 않은 기능 포함)을 자동으로 가져옵니다. Beta 빌드는 Stable 매니페스트와 물리적으로 분리되어 있어 일반 사용자에게 영향을 주지 않습니다. 불안정할 수 있으므로 사전 평가와 피드백을 제공할 의향이 있는 사용자에게만 권장합니다.', betaChannelFetching: '최신 Beta 버전을 가져오는 중…', betaChannelFetchBtn: '최신 Beta 확인', betaChannelLatestPrefix: '최신 Beta:', @@ -792,6 +792,10 @@ export const ko: typeof zhCN = { betaChannelRefresh: '새로 고침', betaChannelNoBeta: '아직 게시된 Beta 릴리스가 없습니다.', betaChannelFetchError: 'Beta 릴리스 정보를 가져오지 못했습니다. 잠시 후 다시 시도하세요.', + betaChannelUpToDate: '최신', + betaChannelUpdateNow: '지금 업데이트', + betaChannelUpdateNowTitle: '최신 Beta를 확인·다운로드하고 업데이트 대화상자를 표시합니다', + betaChannelChecking: '확인 중…', updateDialog: { available: { title: '새 버전 발견', diff --git a/openless-all/app/src/i18n/zh-CN.ts b/openless-all/app/src/i18n/zh-CN.ts index b0cf3287..eee752ef 100644 --- a/openless-all/app/src/i18n/zh-CN.ts +++ b/openless-all/app/src/i18n/zh-CN.ts @@ -780,7 +780,7 @@ export const zhCN = { privacyDesc: '所有识别结果仅保存在本机。云端 API 仅用于实时转写与润色,不会保留你的录音。', localFirst: '本地优先', betaChannelLabel: '加入 Beta 渠道', - betaChannelDesc: '默认拿到的是正式版。打开后可在下方看到最新 Beta 版的下载入口;Beta 包不会通过自动更新推到普通用户,需要手动下载安装。可能不稳定,仅推荐愿意尝鲜与反馈问题的用户开启。', + betaChannelDesc: '默认拿到的是正式版。打开后,App 的「检查更新」会自动接收最新 Beta 版本(含未上 Stable 的功能)。Beta 包跟 Stable 渠道物理隔离,普通用户不会被波及。可能不稳定,仅推荐愿意尝鲜与反馈问题的用户开启。', betaChannelFetching: '正在获取最新 Beta 版本…', betaChannelFetchBtn: '查询最新 Beta', betaChannelLatestPrefix: '最新 Beta:', @@ -788,6 +788,10 @@ export const zhCN = { betaChannelRefresh: '重新查询', betaChannelNoBeta: '暂无已发布的 Beta 版。', betaChannelFetchError: '获取 Beta 版本信息失败,请稍后重试。', + betaChannelUpToDate: '已是最新', + betaChannelUpdateNow: '立即更新', + betaChannelUpdateNowTitle: '检查并下载最新 Beta,然后弹出更新对话框', + betaChannelChecking: '检查中…', updateDialog: { available: { title: '发现新版本', diff --git a/openless-all/app/src/i18n/zh-TW.ts b/openless-all/app/src/i18n/zh-TW.ts index 3187f6f5..f115b0b4 100644 --- a/openless-all/app/src/i18n/zh-TW.ts +++ b/openless-all/app/src/i18n/zh-TW.ts @@ -782,7 +782,7 @@ export const zhTW: typeof zhCN = { privacyDesc: '所有識別結果僅保存在本機。雲端 API 僅用於實時轉寫與潤色,不會保留你的錄音。', localFirst: '本地優先', betaChannelLabel: '加入 Beta 渠道', - betaChannelDesc: '預設拿到的是正式版。打開後可在下方看到最新 Beta 版的下載入口;Beta 包不會通過自動更新推送給普通用戶,需要手動下載安裝。可能不穩定,僅推薦願意嘗鮮並回報問題的用戶開啟。', + betaChannelDesc: '預設拿到的是正式版。打開後,App 的「檢查更新」會自動接收最新 Beta 版本(含未上 Stable 的功能)。Beta 包跟 Stable 渠道物理隔離,普通用戶不會被波及。可能不穩定,僅推薦願意嘗鮮並回報問題的用戶開啟。', betaChannelFetching: '正在獲取最新 Beta 版本…', betaChannelFetchBtn: '查詢最新 Beta', betaChannelLatestPrefix: '最新 Beta:', @@ -790,6 +790,10 @@ export const zhTW: typeof zhCN = { betaChannelRefresh: '重新查詢', betaChannelNoBeta: '尚未發佈過 Beta 版。', betaChannelFetchError: '獲取 Beta 版本資訊失敗,請稍後重試。', + betaChannelUpToDate: '已是最新', + betaChannelUpdateNow: '立即更新', + betaChannelUpdateNowTitle: '檢查並下載最新 Beta,然後彈出更新對話框', + betaChannelChecking: '檢查中…', updateDialog: { available: { title: '發現新版本',