feat(m159): 自更新 GUI 客户端接线 + CLI update check/apply 同一内核

D50 的「检查 → 提示 → 可拒绝 → 下载校验 → 下一次启动生效」整环接线,GUI 与
CLI 共用 core 的一条内核(`update::Job`),并顺带修掉 M157 最后一轮复审的三处 P3。

内核(core::update)
- 新增 job.rs:`Job { source, target, staging, dest }` + `check` / `apply`(复查 →
  取包 → 长度/SHA256 双检 → 原子替换),`default_source`(GitHub,`ULL_UPDATE_DIR`
  可切本地目录夹具)、`process_dest`、`staging_dir`。
- `mod.rs` 导出 job 一族;`source.rs` 为 `Box<dyn UpdateSource>` 补 trait 转发
  (壳层要运行期选源,`UpdateChecker` 是泛型的)。

presenter(宿主通道)
- `dto/update.rs`:`UpdateView` / `UpdateOutcome`(Idle/Checking/UpToDate/Available/
  MissingArtifact/Failed 六态,**失败与最新不折叠**)/ `UpdateBanner` 优先级判据。
- `host/update.rs`:Factory(生产 GitHub / 单测本地目录)+ core 结论 → DTO 的唯一映射
  + 更新域偏好(`cache/update/prefs.json`)。
- `host/update_state.rs`:Runtime(会话状态机:begin_check/adopt/begin_apply/dismiss/
  set_auto_check)+ `CoreHost` 派发与后台线程。
- `AppCommand::{CheckUpdate, ApplyUpdate, DismissUpdate, SetUpdateAutoCheck}`、
  `CoreEvent::Update`、`AppState.update`;`refresh_all` 按开关派发静默检查。

app-iced
- 设置页新增第 5 组「更新」(当前版本 / 渠道 / 自动检查开关 / 立即检查 / 上次检查与
  结果 / 下载并安装 / 就位后立即重启),走 `settings::view` 的 `update` 形参 + 按下标
  分发(与 M106 语言行同一取舍)。
- 顶部提示条(每页可见):有新版本(版本号 + 体积 + 说明摘要,可安装/可忽略)、下载中、
  已就位(立即重启);静默检查失败**不**弹条(原因进设置页)。
- `pages::Message::Update(UpdateAction)` → `app/update/update_action.rs`;「立即重启」
  拉同参数的新进程后退出(截图通道不重启)。

CLI
- `ull update check` / `ull update apply [--dest <path>]`:复用同一 `Job`;四种结论分开
  报(`--json` 的 `status`),失败退出码 1(`update_check_failed` / `update_apply_failed`)。

i18n
- 新增 `catalog/update.rs`(30 条,链尾追加)+ 金表对应 30 行。

M157 复审 P3 三件
- `app/update/instances.rs` 模块文档改成「清脏的唯一时机是回程快照等于表单」。
- `pages/instances/demo.rs` 的 `demo_form` 注释改成现状(两个参数框早已是单行输入)。
- `pages/instances/launch_args.rs`:门限不满足时只灰 `opengl`/`vulkan`,`default` 仍可点
  (它是「不下发」的安全档);判据抽成 `backend_chip_enabled` 并补 3 条用例。浅色 + 灰化
  两张图:`C:/Temp/m159-shots/instances-launch-args-{light,gated}.png`。

顺带:harness 支持 `ULL_SETTINGS_GROUP=<下标>`(拍第 5 组的验收图需要它)。

验证:cargo fmt --check 绿;cargo clippy --workspace --all-targets -D warnings 绿;
cargo test --workspace --all-targets 51 个测试二进制全绿(唯一例外:core::addon::pack
一族要造 4 GiB 稀疏文件,本机磁盘 99% 满 ⇒ StorageFull,与本次改动无关);CLI 用本地
目录夹具跑通「发现 9.9.9 → 下载 → 校验 → 替换运行中的 exe → 备份留旧文件」,并验证
摘要不符时拒绝应用、目标文件零改动、暂存目录零残留。
This commit is contained in:
AskaEth committed 2026-11-07 12:09:42 +08:00
1 parent a80362915a
commit 1c8f6aa1b3
44 files changed
+3341 -22

No files matched your search

+4
View File
@@ -125,6 +125,7 @@ mod loader;
mod logs;
mod nav;
mod settings;
mod update;
/// 拆分前的 `key -> (en, zh-CN)` **逐字快照**(金表)。
///
@@ -195,6 +196,7 @@ use settings::{
settings_ai_entries, settings_appearance_entries, settings_download_entries,
settings_general_entries, settings_java_entries,
};
use update::update_entries;
// 装配顺序 = `Key` 的声明顺序。**与拆分前逐条一致**,动它等于动 `Key` 的 `Ord` 与导出表顺序,
// 金表(`catalog/golden.rs`)会红。设置页那五个域按页面上的分组顺序排,中间三个现在是空的。
@@ -224,6 +226,8 @@ nav_entries! {
// M114:账户页。同理**追加在 main 已占住的链尾之后**(先 content 再 account),
// 金表只追加不重排。
account_entries,
// M159:自更新(设置页「更新」分组 + 顶部提示条)。同样只追加在链尾之后。
update_entries,
finish,
{}
}
+30
View File
@@ -361,4 +361,34 @@ pub(super) const ROWS: &[(&str, &str, &str)] = &[
("AccountNoticeTitleDone", "Account updated", "账户已更新"),
("AccountHintAuthlibReauth", "Enter the password again for {server}.", "请输入 {server} 的密码。"),
("AccountHintOfflineNoRelogin", "Local profiles have no server account to sign in again — create a new profile instead.", "本地档案没有可重新登录的服务器账户,请新建一个档案。"),
("SettingsGroupUpdate", "Updates", "更新"),
("SettingsUpdateCurrent", "Current version", "当前版本"),
("SettingsUpdateChannel", "Channel", "更新渠道"),
("SettingsUpdateChannelStable", "Stable", "稳定版"),
("SettingsUpdateAutoCheck", "Check for updates on startup", "启动时检查更新"),
("SettingsUpdateCheckNow", "Check now", "立即检查"),
("SettingsUpdateChecking", "Checking…", "正在检查…"),
("SettingsUpdateNever", "Not checked yet", "还没检查过"),
("SettingsUpdateUpToDate", "You are on the latest version ({version})", "已是最新({version})"),
("SettingsUpdateAvailable", "Version {version} is available ({size})", "有新版本 {version}({size})"),
("SettingsUpdateMissingArtifact", "Version {version} is out, but there is no build for this platform yet.", "{version} 已发布,但还没有本平台的安装包。"),
("SettingsUpdateFailed", "Check failed: {reason}", "检查失败:{reason}"),
("SettingsUpdateApply", "Download and install", "下载并安装"),
("SettingsUpdateApplying", "Downloading and verifying…", "正在下载并校验…"),
("SettingsUpdateReady", "Version {version} is ready — restart to use it.", "版本 {version} 已就位,重启后生效。"),
("SettingsUpdateNotes", "Release notes: {notes}", "更新说明:{notes}"),
("SettingsUpdateForced", "This update is required", "这是一次强制更新"),
("SettingsUpdateLastChecked", "Last check", "上次检查"),
("SettingsUpdateHint", "Downloads are verified with SHA256 before they touch the launcher. The running copy stays as it is until you restart.", "更新包在落到启动器之前会做 SHA256 校验;正在运行的这一份在你重启之前不会变。"),
("UpdateBannerTitleAvailable", "Version {version} is available", "发现新版本 {version}"),
("UpdateBannerBodyAvailable", "Download size {size}", "下载体积 {size}"),
("UpdateBannerTitleReady", "Update ready", "更新已就位"),
("UpdateBannerBodyReady", "Version {version} will run after a restart.", "重启后运行版本 {version}。"),
("UpdateBannerTitleFailed", "Update failed", "更新失败"),
("UpdateBannerApplying", "Downloading the update…", "正在下载更新…"),
("UpdateBannerInstall", "Download and install", "下载并安装"),
("UpdateBannerDismiss", "Skip this version", "忽略这个版本"),
("UpdateBannerRestartNow", "Restart now", "立即重启"),
("UpdateBannerRestartLater", "Not now", "稍后"),
("UpdateBannerRetry", "Try again", "重试"),
];
+60
View File
@@ -0,0 +1,60 @@
//! 自更新文案(M159):设置页「更新」分组 + 顶部提示条。
//!
//! 域文件的形状见 [`crate::catalog`] 的「域装配」一节;本域**追加在链尾**(`account_entries`
//! 之后),于是 `Key::ALL` 顺序 = 金表顺序 = 先 main 的全部域,最后 update(金表只追加不重排)。
//!
//! 三条口径:
//!
//! 1. **「检查失败」与「已是最新」是两句不同的文案**(mission 的硬要求):前者带原因,
//! 后者带版本号。别为了少一个 key 把它们合并成一条;
//! 2. 体积走 `{size}`(由页面按 `human_bytes` 格式化后传入),版本号走 `{version}`——
//! 目录里不写死任何版本号;
//! 3. 品牌/技术名词不翻:`OpenGL` / `SHA256` / `GitHub` 这类保持原样。
macro_rules! update_entries {
($next:ident, $($rest:ident,)* { $($acc:tt)* }) => {
$next! { $($rest,)* {
$($acc)*
// ── 设置页「更新」分组
SettingsGroupUpdate => ("Updates", "更新"),
SettingsUpdateCurrent => ("Current version", "当前版本"),
SettingsUpdateChannel => ("Channel", "更新渠道"),
SettingsUpdateChannelStable => ("Stable", "稳定版"),
SettingsUpdateAutoCheck => ("Check for updates on startup", "启动时检查更新"),
SettingsUpdateCheckNow => ("Check now", "立即检查"),
SettingsUpdateChecking => ("Checking…", "正在检查…"),
SettingsUpdateNever => ("Not checked yet", "还没检查过"),
SettingsUpdateUpToDate => ("You are on the latest version ({version})", "已是最新({version})"),
SettingsUpdateAvailable => ("Version {version} is available ({size})", "有新版本 {version}({size})"),
SettingsUpdateMissingArtifact => (
"Version {version} is out, but there is no build for this platform yet.",
"{version} 已发布,但还没有本平台的安装包。"
),
SettingsUpdateFailed => ("Check failed: {reason}", "检查失败:{reason}"),
SettingsUpdateApply => ("Download and install", "下载并安装"),
SettingsUpdateApplying => ("Downloading and verifying…", "正在下载并校验…"),
SettingsUpdateReady => ("Version {version} is ready — restart to use it.", "版本 {version} 已就位,重启后生效。"),
SettingsUpdateNotes => ("Release notes: {notes}", "更新说明:{notes}"),
SettingsUpdateForced => ("This update is required", "这是一次强制更新"),
SettingsUpdateLastChecked => ("Last check", "上次检查"),
SettingsUpdateHint => (
"Downloads are verified with SHA256 before they touch the launcher. The running copy stays as it is until you restart.",
"更新包在落到启动器之前会做 SHA256 校验;正在运行的这一份在你重启之前不会变。"
),
// ── 顶部提示条
UpdateBannerTitleAvailable => ("Version {version} is available", "发现新版本 {version}"),
UpdateBannerBodyAvailable => ("Download size {size}", "下载体积 {size}"),
UpdateBannerTitleReady => ("Update ready", "更新已就位"),
UpdateBannerBodyReady => ("Version {version} will run after a restart.", "重启后运行版本 {version}。"),
UpdateBannerTitleFailed => ("Update failed", "更新失败"),
UpdateBannerApplying => ("Downloading the update…", "正在下载更新…"),
UpdateBannerInstall => ("Download and install", "下载并安装"),
UpdateBannerDismiss => ("Skip this version", "忽略这个版本"),
UpdateBannerRestartNow => ("Restart now", "立即重启"),
UpdateBannerRestartLater => ("Not now", "稍后"),
UpdateBannerRetry => ("Try again", "重试"),
} }
};
}
pub(crate) use update_entries;