feat(updater): Beta 渠道接通 auto-update(Builder.endpoints runtime 切换) (#465)

* feat(updater): Beta 渠道接通 auto-update(Builder.endpoints runtime 切换)

CLAUDE.md 之前的论断「tauri-plugin-updater 2.10 Builder 不暴露 endpoints()」在
2.10.1 已不成立 —— Builder 现在有 .endpoints(Vec<Url>) -> Result<Self>,build()
里 self.endpoints.unwrap_or_else(|| self.config.endpoints.clone()) 走 runtime 覆盖。

实现 channel-aware 自动更新(不 fork plugin、不写自定义下载/签名校验):

Rust(~100 行新增)
- commands.rs::app_check_update_with_channel:
  Stable → webview.updater_builder().build()?.check()(默认 endpoints 从 tauri.conf)
  Beta   → fetch_latest_beta_release() → 拼镜像 + 直连两份 -beta manifest URL
         → builder.endpoints(urls)?.build()?.check()
- AppUpdateMetadata 字段形状对齐 plugin 的 UpdateMetadata(rid + currentVersion
  + version + body + rawJson),前端 new Update(metadata) 即可复用 plugin 的
  download / install / close。
- lib.rs 注册新 command。

前端(~30 行改动)
- AutoUpdate.tsx::checkForUpdates 从 import('@tauri-apps/plugin-updater').check
  改为 invoke('app_check_update_with_channel');其余 download / install / close
  全走 plugin 原生 JS API(new Update(metadata) 后链式调用)。
- SettingsModal::BetaChannelControl 删掉「打开 GitHub 下载」按钮 ——
  auto-update 接管了,手动下载路径冗余;保留「最新 Beta 版本号 + 刷新」做信息透明。
- i18n 5 个 locale 改 betaChannelDesc 文案,把「需要手动下载安装」改成
  「自动接收最新 Beta 版本」。betaChannelDownloadBtn 等 i18n key 暂留(无用但无害)。

物理隔离仍生效:Beta tag 的 manifest 文件名带 -beta 后缀,跟 Stable 的
latest-{tgt}-{arch}.json 在 GitHub Release assets 里是分开两份;即使代码逻辑
错把 Beta URL 给 Stable 用户也是 HTTP 404,不会拿到错档。

验证:cargo test --lib serial 270 / 0 / 0;tsc clean;vite build clean。

* feat(settings): BetaChannelControl 加版本比对 + 「立即更新」按钮

之前 BetaChannelControl 在 toggle 打开后,只机械显示「最新 Beta: vX-beta-tauri」,
不管本机版本是不是已经等于那个 tag。用户报告:「显示有最新 Beta,但应该比对版本号」。

修复:
- parseVersionFromBetaTag("v1.3.4-1-beta-tauri") → "1.3.4-1"
- semverGreater(remote, local) 做 SemVer 风格比对(支持 X.Y.Z 和 X.Y.Z-N,覆盖
  本仓库实际版本格式;prerelease 按 SemVer 规则 lower than no-prerelease)
- 本机 >= 最新 → 「最新 Beta: vX」 + 蓝色「已是最新」chip
- 本机 < 最新 → 「最新 Beta: vX」 + 「立即更新」按钮(蓝色高亮)
- 「立即更新」点击 → useAutoUpdate.checkForUpdates → 走和全局「检查更新」按钮
  完全相同的 download/install 流程,UpdateDialog 直接挂在 BetaChannelControl 内部
- 用户的第二个 feature 诉求:「在 Beta 渠道旁边一键检查 + 自动更新」 = 这个按钮

i18n 5 个 locale 加 4 个 key:betaChannelUpToDate / betaChannelUpdateNow /
betaChannelUpdateNowTitle / betaChannelChecking。

* chore(release): bump 1.3.4-1 → 1.3.4-2 for Beta channel

`-2` 后缀让 appVersion.ts 的 IS_BETA_BUILD 仍判为 Beta,UI BETA 标签照常显示。
配本 PR 的 Beta auto-update + BetaChannelControl「立即更新」按钮,1.3.4-1 用户
切到 Beta 渠道后点检查会发现 1.3.4-2 并走 auto-update 流程一键升上来。

发布 tag:v1.3.4-2-beta-tauri。

---------

Co-authored-by: baiqing <lbx12309@icloud.com>
This commit is contained in:
TRIPandbaiqing authored and GitHub committed 2026-05-17 11:15:06 +08:00
1 parent 061558a0fa
commit 859fd5370f
14 files changed
+251 -28

No files matched your search

+2 -2
View File
@@ -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",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "openless-app",
"private": true,
"version": "1.3.4-1",
"version": "1.3.4-2",
"type": "module",
"scripts": {
"dev": "vite",
+1 -1
View File
@@ -3751,7 +3751,7 @@ dependencies = [
[[package]]
name = "openless"
version = "1.3.4-1"
version = "1.3.4-2"
dependencies = [
"anyhow",
"arboard",
+1 -1
View File
@@ -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"
+102 -5
View File
@@ -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<String> {
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<String>,
pub body: Option<String>,
/// 原始 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<R: tauri::Runtime>(
coord: CoordinatorState<'_>,
webview: tauri::Webview<R>,
timeout_ms: Option<u64>,
) -> Result<Option<AppUpdateMetadata>, 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<Vec<url::Url>, 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()
+1
View File
@@ -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,
+1 -1
View File
@@ -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",
+39 -5
View File
@@ -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<string, unknown>;
}
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<AppUpdateMetadata | null>('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');
@@ -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<LatestBetaRelease | null>(null);
const [status, setStatus] = useState<'idle' | 'fetching' | 'empty' | 'error'>('idle');
const [errorMessage, setErrorMessage] = useState<string>('');
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() {
<span>
{t('settings.about.betaChannelLatestPrefix')} <code style={{ fontFamily: 'var(--ol-font-mono)' }}>{latest.tagName}</code>
</span>
<button style={btnGhost} onClick={() => openExternal(latest.htmlUrl)}>
{t('settings.about.betaChannelDownloadBtn')}
</button>
{comparison === 'up-to-date' && (
<span style={{
fontSize: 11, padding: '2px 8px', borderRadius: 999,
background: 'var(--ol-blue-soft)', color: 'var(--ol-blue)', fontWeight: 500,
}}>{t('settings.about.betaChannelUpToDate')}</span>
)}
{comparison === 'newer' && (
<button
style={{ ...btnGhost, borderColor: 'var(--ol-blue)', color: 'var(--ol-blue)' }}
onClick={() => void updater.checkForUpdates()}
disabled={updater.checking || updater.busy}
title={t('settings.about.betaChannelUpdateNowTitle')}
>
{updater.checking
? t('settings.about.betaChannelChecking')
: t('settings.about.betaChannelUpdateNow')}
</button>
)}
<button style={btnGhost} onClick={fetchBeta} title={t('settings.about.betaChannelRefresh')}>
<Icon name="refresh" size={12} />
</button>
@@ -510,6 +570,17 @@ function BetaChannelControl() {
)}
</div>
)}
{isDialogStatus(updater.status) && (
<UpdateDialog
status={updater.status}
version={updater.version}
progress={updater.progress}
downloaded={updater.downloaded}
contentLength={updater.contentLength}
onInstall={() => void updater.installUpdate()}
onClose={() => void updater.dismissDialog()}
/>
)}
</>
);
}
+5 -1
View File
@@ -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',
+5 -1
View File
@@ -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: '新しいバージョンがあります',
+5 -1
View File
@@ -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: '새 버전 발견',
+5 -1
View File
@@ -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: '发现新版本',
+5 -1
View File
@@ -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: '發現新版本',