feat(content): 安装失败的两档分组与「只重试这些」(M191)
M186 把「可只重试失败项」算到了 core/presenter(DependencyPlan::retry_request()), 但 DTO 没字段、界面没按钮,用户拿不到。这一轮补最后一公里: - DTO:ContentFailureKind 加 Incompatible 一档(不再借 Unsupported,与「缺密钥」分开); InstallProgressView 加 retry: Vec<InstallFailureView>(project + 档位 + core 原话), 档位是专门的 InstallFailureKind(Incompatible / Unreachable,穷尽映射 core 的 FailureKind); ContentInstallRequest 加 retry: bool(重试路径唯一读的字段是 instance)。 - presenter:plan.rs 把上一轮失败的计划按 host+实例记住(与 addon/pack.rs 同一手法, host/mod.rs 不在 scope);retry_request()/retry_plan() 就是「只重试这些」—— 调 core 的 retry_request()、只请求失败的那几个项目,present 重新扫目录。 install.rs 的重试入口没有记录时回一条失败,不退化成整单重装;成功则 forget + 清名单。 - 界面:内容库页插入按档分组的失败面板(两段文案、各自条目)+「只重试这 N 个」实心按钮; 判据只有一条且在模型里(stage==Failed && 名单非空),全成功时连同按钮一起不渲染。 - i18n:新增 6 条中英文案(不兼容 / 取不到 / 只重试这 N 个 / 标题 / 提示 / 关闭),金表同步。 守门(每条都做过变异验证,见报告): ① 把 retry_request() 换成根项目请求 ⇒ retrying_asks_for_the_failed_projects_only 红(roots 变 ["sodium"]); ② 两档合并 ⇒ the_retry_list_keeps_the_two_tiers_apart / the_two_tiers_are_two_groups_with_two_labels 红; ③ 恒渲染 ⇒ a_fully_successful_install_has_no_retry_surface_at_all 红。 数据包(datapack)本轮仍未做:ContentKind 没有这一项,界面上也没有半成品入口。
This commit is contained in:
1 parent
5505ea0f91
commit
5a52f38f35
24 files changed
+1072
-24
No files matched your search
@@ -14,12 +14,18 @@
|
||||
//! | 整合包 | 一律 `Failed` + 一句原因 | **未做**:core 没有「远程整合包装进实例」的管线(M111) |
|
||||
//! | CurseForge 文件解析 | 一律 `Failed` + 一句原因 | **未做**:需要带 key 的文件端点,本轮只做了 CF 的**检索** |
|
||||
//!
|
||||
//! ## 前置的两档失败与重试名单(M186)
|
||||
//! ## 前置的两档失败与重试名单(M186 / M191)
|
||||
//!
|
||||
//! 解析在 [`super::plan`] + `core::addon::online::resolve`:某个前置「不兼容所选版本/加载器」
|
||||
//! 与「取不到(网络 / 项目不存在)」是**两条**记录,本文件把两档分开写进失败文案,并把失败项
|
||||
//! 列成「可只重试这 N 个」。**这不是自动重试**:重试要用户(或将来 UI 的按钮)再发一次,
|
||||
//! `DependencyPlan::retry_request()` 已经把请求备好了。
|
||||
//! 列成「可只重试这 N 个」。**这不是自动重试**:重试要用户点(M191 的界面入口)。
|
||||
//!
|
||||
//! ## 「只重试这些」真的只重试这些(M191)
|
||||
//!
|
||||
//! 上一轮失败时,那份 `DependencyPlan` 被 [`plan::remember`] 记下来;`ContentInstallRequest.retry`
|
||||
//! 为真的第二次请求走 [`run`] 的重试入口——它调 core 的 `DependencyPlan::retry_request()`,
|
||||
//! 于是重解**只请求失败的那几个项目**(`present` 重新扫一遍目录,已装好的不会被重下)。
|
||||
//! 没有记录时回一条失败,**不**退化成「整单重装」:那会把已装好的再下一遍、请求数放大。
|
||||
//!
|
||||
//! 下载阶段的失败**也进同一套名单**(M186 r1):以前 `download_verified` 一失败就整单中断,
|
||||
//! 用户既看不到「主文件已装、缺 fabric-api」,也拿不到可重试的项目。现在逐个文件下、失败记进
|
||||
@@ -40,11 +46,11 @@
|
||||
//! 面板在「进度未知」时本来就只画一行说明、不画条(`download/install.rs` 的既定口径)。
|
||||
//! 已登记为后续项:presenter 补 `futures-util` 之后换成字节级。
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
use unlucky_core::addon::online::{
|
||||
resolve, EngineArtifactFetcher, EngineTransport, Modrinth, PlannedNode,
|
||||
resolve, DependencyPlan, EngineArtifactFetcher, EngineTransport, Modrinth, PlannedNode,
|
||||
};
|
||||
use unlucky_core::download::blocking::block_on;
|
||||
use unlucky_core::game::install::instance::read_instance_config;
|
||||
@@ -78,7 +84,8 @@ pub(super) fn install(
|
||||
|
||||
// 整合包:core 没有这条管线(M111),**不**假装能装。UI 侧按钮本来就是禁用的,
|
||||
// 这里再挡一次是为了「绕过 UI 直接发指令」也不会静默做半件事。
|
||||
if request.kind.dir().is_none() {
|
||||
// 重试模式不看 `kind`:那种模式下落盘目录来自上一次的计划(见 `run`)。
|
||||
if !request.retry && request.kind.dir().is_none() {
|
||||
fail(
|
||||
tx,
|
||||
progress,
|
||||
@@ -134,6 +141,13 @@ fn fail(tx: &JobSender, mut progress: InstallProgressView, failure: ContentFailu
|
||||
///
|
||||
/// 返回「装了哪些 / 跳过了哪些」的人读描述;解析阶段的两档失败由
|
||||
/// [`plan::failure_of_plan`] 变成一条带大类的失败(**前置缺了不算装成**)。
|
||||
///
|
||||
/// 两条解析入口,别混:
|
||||
///
|
||||
/// | 入口 | 请求从哪来 | 发几次 |
|
||||
/// |---|---|---|
|
||||
/// | 首次装入 | 根项目 + 过滤条件([`plan::request`]) | 一次,从根开始 BFS |
|
||||
/// | **只重试失败项** | 上一轮计划的 `retry_request()`([`plan::retry_plan`]) | 一次,**只含失败的那几个项目** |
|
||||
fn run(
|
||||
env: &HostEnv,
|
||||
request: &ContentInstallRequest,
|
||||
@@ -143,6 +157,38 @@ fn run(
|
||||
epoch: u64,
|
||||
) -> Result<String, ContentFailure> {
|
||||
let instance = find_instance(env, &request.instance)?;
|
||||
|
||||
// 「只重试这些」:输入是上一轮那份计划,**不**重新发起根项目安装。
|
||||
// 没有记录时(换了实例 / 进程重启过)回一条失败,而不是退化成「整单重装」。
|
||||
if request.retry {
|
||||
let Some((target_dir, previous)) = plan::remembered(env, &request.instance) else {
|
||||
return Err(local(RETRY_LOST));
|
||||
};
|
||||
progress.version = previous
|
||||
.request
|
||||
.roots
|
||||
.first()
|
||||
.cloned()
|
||||
.unwrap_or_else(|| request.instance.clone());
|
||||
let present = plan::present_files(&target_dir);
|
||||
let core_engine = super::super::engine::engine(env);
|
||||
let dependency_plan = plan::retry_plan(&previous, present, |retry| {
|
||||
let transport = EngineTransport::new(&core_engine);
|
||||
let client = Modrinth::new(&transport);
|
||||
block_on(resolve(&client, retry))
|
||||
});
|
||||
return collect(
|
||||
env,
|
||||
request,
|
||||
dependency_plan,
|
||||
&target_dir,
|
||||
cancel,
|
||||
progress,
|
||||
tx,
|
||||
epoch,
|
||||
);
|
||||
}
|
||||
|
||||
let game_dir = game_dir_of(env, &instance)?;
|
||||
let dir = request
|
||||
.kind
|
||||
@@ -165,12 +211,41 @@ fn run(
|
||||
plan::present_files(&target_dir),
|
||||
);
|
||||
let core_engine = super::super::engine::engine(env);
|
||||
let mut dependency_plan = {
|
||||
let dependency_plan = {
|
||||
let transport = EngineTransport::new(&core_engine);
|
||||
let client = Modrinth::new(&transport);
|
||||
block_on(resolve(&client, &resolve_request))
|
||||
};
|
||||
collect(
|
||||
env,
|
||||
request,
|
||||
dependency_plan,
|
||||
&target_dir,
|
||||
cancel,
|
||||
progress,
|
||||
tx,
|
||||
epoch,
|
||||
)
|
||||
}
|
||||
|
||||
/// 解析之后**两条入口共用**的尾巴:进度 → 逐个下载 → 收口。
|
||||
///
|
||||
/// 收口的两件事在同一个地方做,是因为它们必须同时发生:
|
||||
///
|
||||
/// | 结果 | 计划 | 可重试名单 |
|
||||
/// |---|---|---|
|
||||
/// | 有失败 | 记下(`plan::remember`,重试的输入) | 填(界面据此画「只重试这 N 个」) |
|
||||
/// | 全成功 | 忘掉(`plan::forget`) | 清空(界面**不渲染**那条入口) |
|
||||
fn collect(
|
||||
env: &HostEnv,
|
||||
request: &ContentInstallRequest,
|
||||
mut dependency_plan: DependencyPlan,
|
||||
target_dir: &Path,
|
||||
cancel: &AtomicBool,
|
||||
progress: &mut InstallProgressView,
|
||||
tx: &JobSender,
|
||||
epoch: u64,
|
||||
) -> Result<String, ContentFailure> {
|
||||
progress.stage = InstallStage::Downloading;
|
||||
progress.files_total = dependency_plan.nodes.len() as u64;
|
||||
progress.files_done = 0;
|
||||
@@ -192,11 +267,12 @@ fn run(
|
||||
epoch,
|
||||
});
|
||||
|
||||
let core_engine = super::super::engine::engine(env);
|
||||
let fetcher = EngineArtifactFetcher::new(&core_engine);
|
||||
// 下载阶段在 `downloads.rs`:可测(假下载器能驱动),失败进名单而不中断。
|
||||
let installed = downloads::download_stage(
|
||||
&mut dependency_plan,
|
||||
&target_dir,
|
||||
target_dir,
|
||||
&fetcher,
|
||||
cancel,
|
||||
progress,
|
||||
@@ -207,11 +283,19 @@ fn run(
|
||||
// 解析与下载两段的失败在这里一起收口:两档分成两句话(不合并成「下载失败」),
|
||||
// 并列出「可只重试这 N 个」。
|
||||
if let Some(failure) = plan::failure_of_plan(&dependency_plan, installed.len()) {
|
||||
progress.retry = plan::retry_items(&dependency_plan);
|
||||
plan::remember(env, &request.instance, target_dir, &dependency_plan);
|
||||
return Err(failure);
|
||||
}
|
||||
progress.retry.clear();
|
||||
plan::forget(env, &request.instance);
|
||||
Ok(plan::summary(&dependency_plan, &installed))
|
||||
}
|
||||
|
||||
/// 没有可重试的记录时那句话(**不**退化成「整单重装」)。
|
||||
const RETRY_LOST: &str =
|
||||
"没有可重试的记录:这次失败发生在别的实例上、或进程重启过。请重新装入那一次。";
|
||||
|
||||
/// 完成态:`files_done` 对齐 `files_total`。
|
||||
///
|
||||
/// 别在这里另写数字:以前写死 `1`,装 3 个文件后终态是 `1/3`,下载页只要「已知」就画确定态
|
||||
|
||||
Reference in new issue
Block a user