feat(dlflow): M105 安装选项区(四家加载器 + 官方版本列表 + 实例名)与取消安装真停

用户实跑反馈两条,本轮各自定位到根因并修掉:

1. 「下载页没有前置加载器选项」= 功能未做:`Message::InstallVersion` 直接派
   `AppCommand::InstallVersion`(= 只装原版),core 早在 M32/M35/M45 就有四家加载器的
   安装管线。现在选中版本之后面板先给「安装选项」区(原版 / Forge / NeoForge / Fabric /
   Quilt + 该加载器的官方版本列表 + 实例名),点「安装」才派
   `AppCommand::CreateInstance`。
   - 加载器版本列表复用 core 的 `LoaderSource::loader_versions`,经新增的
     `AppCommand::ListLoaderVersions` / `CoreEvent::LoaderVersions` 回流;
     缓存键是 `(加载器, 原版版本)`,切版本不会串列表;取不到给具名原因并**禁掉确认**。
   - 不兼容组合用静态规则在**选中那一刻**说清(NeoForge 需 1.20.1+、Fabric/Quilt 需
     1.14+、远古版四家都不支持),不让用户「选完才失败」,也不为不可能的组合作无谓联网。
   - 选项草稿(选中 + 加载器 + 加载器版本 + 实例名)住在 `App`,`refresh_view` 带进页面
     模型(与分类筛选同一做法;页面自己存不住跨事件的东西)。

2. 「点取消没反应」= 真 bug,两跳各断一半:
   - 第一跳(UI 侧):取消确实生效了,但**还在跑的后台线程继续发进度**,`InstallProgress(None)`
     刚把面板收回,下一条「下载中」立刻又点亮它 —— 用户看到的就是「点了没反应」。
     修法:`CoreHost` 加安装代次 `install_epoch`(开工与取消都 +1),`HostTask::Install` /
     `InstallDone` 带代次回来,代次不符一律丢。
   - 第二跳(core 侧):`execute` 没有任何取消入参,取消只停在段边界,传输根本没停。
     新增 `execute_with_cancel(…, cancel: Option<&AtomicBool>)`(既有 `execute` 原样包装,
     CLI 一字未改):取消后不再起新文件,在飞的那个与「取消 future」`select` 赛跑,取消赢就
     drop 掉下载 future(响应流关闭、`.ullpart` 由 `PartFile::Drop` 清掉),并且不写版本清单,
     返回新的 `InstallErrorKind::Cancelled`。
   - 另外:取消后**绝不登记实例**(文件段跑完但用户已取消的那一路也拦住);取消文案把
     「哪些留着、哪些一定没发生」说清(D21)。

实测(真网络装置 `host/cancel_tests.rs`,`#[ignore]` 守着,跑法见文件头):
取消前 files_done=2 / `.ullpart` 613 字节;取消后 20 s 进度事件 **0 条**、`.ullpart` **0 字节**
(-613)、版本清单未落盘、实例目录不存在。取消前是 +201466 字节(同一个装置量的)。

文案全部进 i18n(中英,9 条新 key + 金表同步)。
This commit is contained in:
AskaEth committed 2026-11-04 22:12:22 +08:00
1 parent d690d09b84
commit 30b1ff6f48
39 files changed
+2550 -518

No files matched your search

+39 -2
View File
@@ -7,8 +7,8 @@ use crate::agent::AgentRail;
use crate::dialogs::DialogQueue;
use crate::dto::{
AccountView, AgentEvent, AppEvent, CoreEvent, DeviceLoginView, DiagnosisReport,
InstallProgressView, InstanceRuntime, InstanceSummary, JavaPageView, LogLine, LogSession,
NoticeView, Progress, SettingGroupView, VersionSummary,
InstallProgressView, InstanceRuntime, InstanceSummary, JavaPageView, LoaderKind, LogLine,
LogSession, NoticeView, Progress, SettingGroupView, VersionSummary,
};
use crate::prefs::UiPrefs;
use crate::router::{AiPanelMode, Route, Router};
@@ -39,6 +39,9 @@ pub fn grid_positions(count: usize, columns: usize) -> Vec<(usize, usize)> {
/// 实时日志最多保留的行数(滚动窗口;更多行由日志页的筛选/历史会话承担)。
pub const MAX_LIVE_LINES: usize = 2_000;
/// 一份加载器版本清单查询的结果(`Ok` = 最新在前;`Err` = 人读原因)。
pub type LoaderVersions = Result<Vec<String>, String>;
/// 应用全局状态。
#[derive(Debug)]
pub struct AppState {
@@ -78,6 +81,16 @@ pub struct AppState {
pub java: JavaPageView,
/// 安装进度(None = 没有安装任务)
pub install: Option<InstallProgressView>,
/// 加载器版本清单(下载页「安装选项」的下拉;M105)
///
/// 两段键:外层是加载器的**稳定 id**(`LoaderKind::as_str`,`LoaderKind` 没派生 `Hash`,
/// 而 `as_str` 本来就是它在 CLI/GUI 两边的身份),内层是原版版本。
/// **原版版本进键**,所以切到另一个 MC 版本时拿到的是另一条缓存,
/// 不会把 A 版本的 Fabric 列表挂到 B 版本下面(M105 复审的硬要求)。
/// 值是 `Result`:「取不到」与「官方没有版本」是两件事,前者要在页面上说清原因
/// (并禁掉确认),不许显示成空列表。
pub loader_versions:
std::collections::HashMap<&'static str, std::collections::HashMap<String, LoaderVersions>>,
/// 设置分组(设置页右栏)
pub settings_groups: Vec<SettingGroupView>,
/// 当前选中的设置分组(download / java / data / ai / mcp)
@@ -118,6 +131,7 @@ impl AppState {
device_login: DeviceLoginView::new(),
java: JavaPageView::new(),
install: None,
loader_versions: std::collections::HashMap::new(),
settings_groups: Vec::new(),
settings_tab: "download".to_string(),
notices: Vec::new(),
@@ -126,6 +140,19 @@ impl AppState {
}
}
/// 某 (加载器, 原版版本) 的加载器版本清单。
///
/// `None` = 还没查过(页面据此派一次 `AppCommand::ListLoaderVersions` 并显示「正在获取…」);
/// `Some(Err(..))` = 查过但拿不到(页面显示具名原因并**禁掉确认**)。
#[must_use]
pub fn loader_versions_for(
&self,
kind: LoaderKind,
mc_version: &str,
) -> Option<&LoaderVersions> {
self.loader_versions.get(kind.as_str())?.get(mc_version)
}
/// 当前页面。
pub fn route(&self) -> &Route {
self.router.route()
@@ -219,6 +246,16 @@ impl AppState {
None => self.install = None,
},
CoreEvent::SettingsChanged(groups) => self.settings_groups = groups,
CoreEvent::LoaderVersions {
kind,
mc_version,
result,
} => {
self.loader_versions
.entry(kind.as_str())
.or_default()
.insert(mc_version, result);
}
CoreEvent::NoticesChanged(notices) => self.notices = notices,
}
}