fix(content): 重试面板画可读名,不再只有站点 id (M191 r1 P2-1)
复审 P2-1:面板原来只画 `P7dR8mSH` 这种项目键,用户拿到的是一串哈希。
可读名其实都在手边,只是没进 DTO——现在补上,按复审给的**最小修法**做,没扩散:
- `InstallFailureView` 加 `label: String`(**语言中立**:站点标题或文件名),
配 `line()` 统一决定「名字 (id)」还是「只有 id」(名字为空时不编造)。
`detail`(core 中文散文)**依旧不进界面**——那会踩 i18n R2。
- `plan::label_of` 三个来源,零额外请求:
根项目 → 请求里的 `title`;计划里有节点(下载失败)→ `node.file.filename`;
都没有(解析 404 / 不兼容的前置)→ 空,界面退回只画 id(复审认这条兜底)。
- 计划被记住时连显示名一起记(`plan::remember` 多一个 `title`),于是重试那一轮
面板头与失败名单沿用的是同一个名字(顺手收掉复审 N5 的「重试时面板头显示 id」)。
- N2:删掉新加的 `ContentInstallDismiss`,复用既有的 `DownloadDismiss`(同一句话不立两条 key)。
- 行数/clippy:`collect` 的参数捆成 `Placement{dir,title}`(9 个参数超限);
`plan.rs` 加完 `label_of` 到 429 行,把「记住的计划」表纯搬移到 `plan/remembered.rs`(363 + 82)。
守门(真跑红再还原):
- 把 `label_of` 置空 ⇒ `the_retry_list_shows_a_readable_name_not_just_the_project_id` 红
(三行全变成 `["AANobbMI","P7dR8mSH","1eAoo2KR"]`);
- 审阅指出的另一半判据也补上了:`Done` + 残留名单 ⇒ 必须不画;现在**只去掉**
`stage != Failed` 那一半就红(之前只去掉它仍是全绿,报告的表述不准,已改)。
This commit is contained in:
1 parent
5a52f38f35
commit
961ff453a0
11 files changed
+326
-104
No files matched your search
@@ -40,21 +40,26 @@ fn retry_failure(instance: &str, catalog: Catalog) -> Option<RetryPanel> {
|
||||
files_total: 4,
|
||||
..InstallProgressView::new()
|
||||
};
|
||||
// 三种可显示性各一条,与 host 侧 `plan::label_of` 的三种情况同形:
|
||||
// 根项目 = 请求里的标题、下载失败 = 文件名、解析 404 = 没有名字(只画 id)。
|
||||
progress.retry = vec![
|
||||
InstallFailureView::new(
|
||||
"AANobbMI",
|
||||
"Sodium",
|
||||
InstallFailureKind::Unreachable,
|
||||
"下载 sodium-fabric-0.6.0.jar 失败(online_not_connected):连不上 cdn.modrinth.com",
|
||||
),
|
||||
InstallFailureView::new(
|
||||
"P7dR8mSH",
|
||||
"",
|
||||
InstallFailureKind::Incompatible,
|
||||
"没有匹配所选游戏版本/加载器(1.20.1 / fabric)的版本",
|
||||
),
|
||||
InstallFailureView::new(
|
||||
"1eAoo2KR",
|
||||
"fabric-api-0.92.2+1.20.1.jar",
|
||||
InstallFailureKind::Unreachable,
|
||||
"取版本失败(online_not_found):HTTP 404",
|
||||
),
|
||||
InstallFailureView::new(
|
||||
"AANobbMI",
|
||||
InstallFailureKind::Unreachable,
|
||||
"下载 fabric-api.jar 失败(online_not_connected):连不上 cdn.modrinth.com",
|
||||
"下载 fabric-api-0.92.2+1.20.1.jar 失败(online_not_connected):连不上 cdn.modrinth.com",
|
||||
),
|
||||
];
|
||||
RetryPanel::of(&progress, catalog)
|
||||
@@ -321,7 +326,20 @@ mod tests {
|
||||
assert!(panel.has_both_tiers(), "{:?}", panel.groups);
|
||||
assert_eq!(
|
||||
panel.items_of(unlucky_presenter::dto::InstallFailureKind::Unreachable),
|
||||
Some(["1eAoo2KR".to_string(), "AANobbMI".to_string()].as_slice())
|
||||
Some(
|
||||
[
|
||||
// 根项目:名字来自请求标题
|
||||
"Sodium (AANobbMI)".to_string(),
|
||||
// 下载失败:名字来自文件名
|
||||
"fabric-api-0.92.2+1.20.1.jar (1eAoo2KR)".to_string(),
|
||||
]
|
||||
.as_slice()
|
||||
)
|
||||
);
|
||||
// 拿不到名的那一档(解析 404 / 不兼容):只画 id,不编名字
|
||||
assert_eq!(
|
||||
panel.items_of(unlucky_presenter::dto::InstallFailureKind::Incompatible),
|
||||
Some(["P7dR8mSH".to_string()].as_slice())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -7,12 +7,16 @@
|
||||
//! ```text
|
||||
//! 安装未完成 —— 3 项没装上
|
||||
//! 不兼容所选版本/加载器
|
||||
//! P7dR8mSH
|
||||
//! P7dR8mSH ← 拿不到名字时只画 id(合理兜底)
|
||||
//! 取不到(网络 / 项目不存在)
|
||||
//! 1eAoo2KR、AANobbMI
|
||||
//! Sodium (AANobbMI) ← 根项目:名字来自请求标题
|
||||
//! fabric-api-0.92.2.jar (1eAoo2KR) ← 下载失败:名字来自文件名
|
||||
//! [只重试这 3 个] [关闭]
|
||||
//! ```
|
||||
//!
|
||||
//! 每行是 `InstallFailureView::line()` 的结果(名字优先、id 次要)。名字是**语言中立**的站点
|
||||
//! 标题或文件名,不是 core 的中文散文——那条线(i18n R2)踩了就回不去。
|
||||
//!
|
||||
//! ## 三条判据(都落在 [`RetryPanel::of`],纯函数,用例直接钉)
|
||||
//!
|
||||
//! 1. **全成功不渲染**:`retry` 名单空 ⇒ `None` ⇒ 视图里连按钮都不存在(不是置灰);
|
||||
@@ -27,8 +31,8 @@
|
||||
|
||||
use unlucky_i18n::{Catalog, Key};
|
||||
use unlucky_presenter::dto::{
|
||||
ContentInstallRequest, ContentSource, InstallFailureKind, InstallProgressView, InstallStage,
|
||||
LoaderKind,
|
||||
ContentInstallRequest, ContentSource, InstallFailureKind, InstallFailureView,
|
||||
InstallProgressView, InstallStage, LoaderKind,
|
||||
};
|
||||
|
||||
/// 面板上的一个档位分组。
|
||||
@@ -38,7 +42,7 @@ pub struct RetryGroup {
|
||||
pub kind: InstallFailureKind,
|
||||
/// 这一档的标题 key(不硬编码中文)
|
||||
pub title: Key,
|
||||
/// 这一档的项目键(顺序 = 计划里的顺序)
|
||||
/// 这一档要画的每一行(`名字 (id)`;拿不到名字时就只有 id,顺序 = 计划里的顺序)
|
||||
pub items: Vec<String>,
|
||||
}
|
||||
|
||||
@@ -80,7 +84,7 @@ impl RetryPanel {
|
||||
title: title_of(kind),
|
||||
items: progress
|
||||
.retry_of(kind)
|
||||
.map(|item| item.project.clone())
|
||||
.map(InstallFailureView::line)
|
||||
.collect(),
|
||||
})
|
||||
.filter(|group| !group.items.is_empty())
|
||||
|
||||
@@ -20,14 +20,22 @@ fn failed_progress() -> unlucky_presenter::dto::InstallProgressView {
|
||||
files_total: 4,
|
||||
..InstallProgressView::new()
|
||||
};
|
||||
// 三种可显示性各一条(与 `plan::label_of` 的三种情况同形):
|
||||
// 根项目用请求里的标题、下载失败用文件名、拿不到名的只画 id。
|
||||
progress.retry = vec![
|
||||
InstallFailureView::new(
|
||||
"AANobbMI",
|
||||
"Sodium",
|
||||
InstallFailureKind::Unreachable,
|
||||
"连不上 CDN",
|
||||
),
|
||||
InstallFailureView::new(
|
||||
"P7dR8mSH",
|
||||
"fabric-api.jar",
|
||||
InstallFailureKind::Incompatible,
|
||||
"没有匹配的版本",
|
||||
),
|
||||
InstallFailureView::new("1eAoo2KR", InstallFailureKind::Unreachable, "HTTP 404"),
|
||||
InstallFailureView::new("AANobbMI", InstallFailureKind::Unreachable, "连不上 CDN"),
|
||||
InstallFailureView::new("1eAoo2KR", "", InstallFailureKind::Unreachable, "HTTP 404"),
|
||||
];
|
||||
progress
|
||||
}
|
||||
@@ -41,13 +49,21 @@ fn the_two_tiers_are_two_groups_with_two_labels() {
|
||||
let panel = RetryPanel::of(&failed_progress(), Lang::En.catalog()).expect("有失败项就有面板");
|
||||
assert_eq!(panel.total, 3);
|
||||
assert!(panel.has_both_tiers(), "{:?}", panel.groups);
|
||||
// **P2-1 的守门**:有名字的画「名字 (id)」,没有名字的才只画 id
|
||||
assert_eq!(
|
||||
panel.items_of(InstallFailureKind::Incompatible),
|
||||
Some(["P7dR8mSH".to_string()].as_slice())
|
||||
Some(["fabric-api.jar (P7dR8mSH)".to_string()].as_slice())
|
||||
);
|
||||
assert_eq!(
|
||||
panel.items_of(InstallFailureKind::Unreachable),
|
||||
Some(["1eAoo2KR".to_string(), "AANobbMI".to_string()].as_slice())
|
||||
Some(["Sodium (AANobbMI)".to_string(), "1eAoo2KR".to_string()].as_slice())
|
||||
);
|
||||
// 变异守门:把标签置空 ⇒ 这一行退回裸 id,上面两条立刻红
|
||||
assert!(
|
||||
panel
|
||||
.items_of(InstallFailureKind::Unreachable)
|
||||
.is_some_and(|items| items[0].starts_with("Sodium")),
|
||||
"根项目必须显示请求里的标题,不是 id"
|
||||
);
|
||||
let titles: Vec<Key> = panel.groups.iter().map(|group| group.title).collect();
|
||||
assert_ne!(titles[0], titles[1], "两档的标题 key 不许是同一个");
|
||||
@@ -71,6 +87,7 @@ fn an_empty_tier_does_not_get_a_group() {
|
||||
};
|
||||
progress.retry = vec![InstallFailureView::new(
|
||||
"P7dR8mSH",
|
||||
"fabric-api.jar",
|
||||
InstallFailureKind::Incompatible,
|
||||
"没有匹配的版本",
|
||||
)];
|
||||
@@ -101,7 +118,19 @@ fn a_fully_successful_install_has_no_retry_surface_at_all() {
|
||||
assert!(model.panel.is_none());
|
||||
assert!(model.retry_install_request().is_none());
|
||||
|
||||
// 失败但没有名单(例如整合包那条本地失败)同样不画:没有可重试的东西
|
||||
// 判据是**两半**:`Failed` **且** 名单非空。第一半单独钉住——就算快照里残留了名单
|
||||
// (不该发生,host 成功时会清),`Done` 也不画(r1 复审指出:只去掉这一半曾全绿)。
|
||||
let mut stale = InstallProgressView {
|
||||
stage: InstallStage::Done,
|
||||
..InstallProgressView::new()
|
||||
};
|
||||
stale.retry = failed_progress().retry;
|
||||
assert!(
|
||||
RetryPanel::of(&stale, Lang::En.catalog()).is_none(),
|
||||
"Done 态不许画重试面板(判据是「失败且有名单」,不是「有名单」)"
|
||||
);
|
||||
|
||||
// 第二半:失败但没有名单(例如整合包那条本地失败)同样不画:没有可重试的东西
|
||||
let mut failed = InstallProgressView {
|
||||
stage: InstallStage::Failed,
|
||||
..InstallProgressView::new()
|
||||
|
||||
@@ -101,8 +101,9 @@ pub(crate) fn retry_panel<'a>(
|
||||
),
|
||||
buttons::text(
|
||||
ui,
|
||||
// 复用下载页那条现有的「关闭」:同一句话不另立一条要同步的 key(r1 复审 N2)
|
||||
Button::new(
|
||||
catalog.text(Key::ContentInstallDismiss),
|
||||
catalog.text(Key::DownloadDismiss),
|
||||
PageMessage::CancelInstall
|
||||
),
|
||||
),
|
||||
|
||||
@@ -118,7 +118,6 @@ macro_rules! content_entries {
|
||||
"Retry asks again only for the items above; files that already landed stay put.",
|
||||
"重试只重新解析上面这几项,已落盘的文件不动。"
|
||||
),
|
||||
ContentInstallDismiss => ("Dismiss", "关闭"),
|
||||
// --- M113: pack import / export(PACKUI)---
|
||||
PackTitle => ("Modpack import & export", "整合包导入 / 导出"),
|
||||
PackImportTitle => ("Import a modpack", "导入整合包"),
|
||||
|
||||
@@ -391,7 +391,6 @@ pub(super) const ROWS: &[(&str, &str, &str)] = &[
|
||||
("ContentInstallRetryUnreachable", "Could not fetch (network or missing project)", "取不到(网络 / 项目不存在)"),
|
||||
("ContentInstallRetryOnly", "Retry just these {count}", "只重试这 {count} 个"),
|
||||
("ContentInstallRetryHint", "Retry asks again only for the items above; files that already landed stay put.", "重试只重新解析上面这几项,已落盘的文件不动。"),
|
||||
("ContentInstallDismiss", "Dismiss", "关闭"),
|
||||
("PackTitle", "Modpack import & export", "整合包导入 / 导出"),
|
||||
("PackImportTitle", "Import a modpack", "导入整合包"),
|
||||
("PackDropHint", "Drop a .mrpack, .zip or a MultiMC/Prism instance folder here", "把 .mrpack / .zip 或 MultiMC·Prism 实例目录拖到这里"),
|
||||
|
||||
@@ -76,17 +76,29 @@ impl InstallFailureKind {
|
||||
pub const ALL: [Self; 2] = [Self::Incompatible, Self::Unreachable];
|
||||
}
|
||||
|
||||
/// 一条「可只重试」的失败项(M191):项目键 + 档位 + core 给的原话。
|
||||
/// 一条「可只重试」的失败项(M191):项目键 + 可读标签 + 档位 + core 给的原话。
|
||||
///
|
||||
/// 这是界面画「可只重试这 N 个」和分组所需的**全部**信息:项目键就是重试时发出去的那个键
|
||||
/// (`DependencyPlan::retry_request()` 用的同一份)。
|
||||
///
|
||||
/// ## 为什么还要 `label`(M191 r1 复审 P2-1)
|
||||
///
|
||||
/// `project` 是站点 id(`P7dR8mSH`),直接画在界面上等于给用户一串哈希。可读名其实都在手边,
|
||||
/// 只是没进 DTO:根项目的名字就在请求的 `title` 里;失败文件的名字在 `PlannedNode.file.filename`
|
||||
/// 里(下载失败那种)。`label` 收的正是这两个**语言中立**的字符串(站点标题 / 文件名),
|
||||
/// **不是** `detail` 那种 core 中文散文——那会踩 i18n 的 R2(core 中文不进界面)。
|
||||
///
|
||||
/// `label` 为空是合法且**必须**保留的一档:项目不存在(404)时确实没有名字可显示,
|
||||
/// 那时界面只画 id(评审认这条兜底是合理的)。
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct InstallFailureView {
|
||||
/// 请求它用的项目键(slug 或 id;重试也用它)
|
||||
pub project: String,
|
||||
/// 语言中立的可读标签(站点给的项目标题 / 文件名;空 = 拿不到名字)
|
||||
pub label: String,
|
||||
/// 哪一档
|
||||
pub kind: InstallFailureKind,
|
||||
/// core 给的人读说明(引擎原话 + 上下文,**不是**界面文案)
|
||||
/// core 给的人读说明(引擎原话 + 上下文,**不是**界面文案;**不要**画到界面上)
|
||||
pub detail: String,
|
||||
}
|
||||
|
||||
@@ -95,15 +107,31 @@ impl InstallFailureView {
|
||||
#[must_use]
|
||||
pub fn new(
|
||||
project: impl Into<String>,
|
||||
label: impl Into<String>,
|
||||
kind: InstallFailureKind,
|
||||
detail: impl Into<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
project: project.into(),
|
||||
label: label.into(),
|
||||
kind,
|
||||
detail: detail.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 界面该画的那一行:有名字就「名字 (id)」,没有就只画 id。
|
||||
///
|
||||
/// 界面上**名字优先、id 次要**:名字让人认出是哪个项目,括号里的 id 是给「名字对不上时」
|
||||
/// 或排查用的。定在这个方法里而不是视图里,是因为「有名字用名字」这条要有用例钉住。
|
||||
#[must_use]
|
||||
pub fn line(&self) -> String {
|
||||
let label = self.label.trim();
|
||||
if label.is_empty() || label == self.project {
|
||||
self.project.clone()
|
||||
} else {
|
||||
format!("{label} ({})", self.project)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 一次安装的进度快照。
|
||||
|
||||
@@ -161,15 +161,11 @@ fn run(
|
||||
// 「只重试这些」:输入是上一轮那份计划,**不**重新发起根项目安装。
|
||||
// 没有记录时(换了实例 / 进程重启过)回一条失败,而不是退化成「整单重装」。
|
||||
if request.retry {
|
||||
let Some((target_dir, previous)) = plan::remembered(env, &request.instance) else {
|
||||
let Some((target_dir, title, 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());
|
||||
// 面板头继续显示那次的显示名(不是项目 id:r1 复审 N5)
|
||||
progress.version = title.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| {
|
||||
@@ -181,7 +177,10 @@ fn run(
|
||||
env,
|
||||
request,
|
||||
dependency_plan,
|
||||
&target_dir,
|
||||
Placement {
|
||||
dir: &target_dir,
|
||||
title: &title,
|
||||
},
|
||||
cancel,
|
||||
progress,
|
||||
tx,
|
||||
@@ -220,7 +219,10 @@ fn run(
|
||||
env,
|
||||
request,
|
||||
dependency_plan,
|
||||
&target_dir,
|
||||
Placement {
|
||||
dir: &target_dir,
|
||||
title: &request.title,
|
||||
},
|
||||
cancel,
|
||||
progress,
|
||||
tx,
|
||||
@@ -228,6 +230,14 @@ fn run(
|
||||
)
|
||||
}
|
||||
|
||||
/// 「装到哪、那次叫什么」——这两样总是一起出现,捆成一个参数(否则 `collect` 顶破 8 个)。
|
||||
struct Placement<'a> {
|
||||
/// 落盘目录(`mods/` / `resourcepacks/` / `shaderpacks/`)
|
||||
dir: &'a Path,
|
||||
/// 那次装入的显示名(失败名单里根项目的可读名就是它)
|
||||
title: &'a str,
|
||||
}
|
||||
|
||||
/// 解析之后**两条入口共用**的尾巴:进度 → 逐个下载 → 收口。
|
||||
///
|
||||
/// 收口的两件事在同一个地方做,是因为它们必须同时发生:
|
||||
@@ -240,7 +250,7 @@ fn collect(
|
||||
env: &HostEnv,
|
||||
request: &ContentInstallRequest,
|
||||
mut dependency_plan: DependencyPlan,
|
||||
target_dir: &Path,
|
||||
placement: Placement<'_>,
|
||||
cancel: &AtomicBool,
|
||||
progress: &mut InstallProgressView,
|
||||
tx: &JobSender,
|
||||
@@ -272,7 +282,7 @@ fn collect(
|
||||
// 下载阶段在 `downloads.rs`:可测(假下载器能驱动),失败进名单而不中断。
|
||||
let installed = downloads::download_stage(
|
||||
&mut dependency_plan,
|
||||
target_dir,
|
||||
placement.dir,
|
||||
&fetcher,
|
||||
cancel,
|
||||
progress,
|
||||
@@ -283,8 +293,14 @@ fn collect(
|
||||
// 解析与下载两段的失败在这里一起收口:两档分成两句话(不合并成「下载失败」),
|
||||
// 并列出「可只重试这 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);
|
||||
progress.retry = plan::retry_items(&dependency_plan, placement.title);
|
||||
plan::remember(
|
||||
env,
|
||||
&request.instance,
|
||||
placement.dir,
|
||||
placement.title,
|
||||
&dependency_plan,
|
||||
);
|
||||
return Err(failure);
|
||||
}
|
||||
progress.retry.clear();
|
||||
|
||||
@@ -19,18 +19,20 @@
|
||||
//!
|
||||
//! # 「可只重试这些」的名单也在这里(M191)
|
||||
//!
|
||||
//! [`retry_items`] 把计划里的失败项摊成 DTO 的 [`InstallFailureView`](项目键 + 档位 + 原话),
|
||||
//! 界面据此分组画两段;[`retry_plan`] 是**真正重试**那一步——它调 core 的
|
||||
//! [`retry_items`] 把计划里的失败项摊成 DTO 的 [`InstallFailureView`](项目键 + 可读标签 +
|
||||
//! 档位 + 原话),界面据此分组画两段;[`retry_plan`] 是**真正重试**那一步——它调 core 的
|
||||
//! `DependencyPlan::retry_request()`,于是重试只请求失败的那几个项目,**不是**重新发起根项目安装。
|
||||
//!
|
||||
//! 可读标签由 [`label_of`] 定(根项目用请求标题、下载失败用文件名、解析失败留空),
|
||||
//! **不许**拿 `failure.message` 顶上——那是 core 的中文散文,界面照画会踩 i18n R2。
|
||||
//!
|
||||
//! # 不兼容有自己的一档(M191 起)
|
||||
//!
|
||||
//! M186 把它映成 `Unsupported`(当时 DTO 没有这一档,与「缺密钥」共用),于是界面没法单独说
|
||||
//! 「换个版本/加载器」。M191 给 `ContentFailureKind` 加了 `Incompatible`,[`kind_of`] 直接映射,
|
||||
//! **不再借 `Unsupported`**。
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Mutex;
|
||||
use std::path::Path;
|
||||
|
||||
use unlucky_core::addon::online::{
|
||||
DependencyFailure, DependencyPlan, FailureKind, OnlineError, PlannedNode, PresentFile,
|
||||
@@ -39,9 +41,12 @@ use unlucky_core::addon::online::{
|
||||
|
||||
use crate::dto::{ContentFailureKind, InstallFailureKind, InstallFailureView};
|
||||
|
||||
use super::super::env::HostEnv;
|
||||
use super::sources::failure_of;
|
||||
|
||||
mod remembered;
|
||||
|
||||
pub(super) use remembered::{forget, remember, remembered};
|
||||
|
||||
/// 扫目标目录(不递归)里已存在的文件名。
|
||||
///
|
||||
/// 只给名字,不算摘要:为了「跳过」把整个 `mods/` 哈希一遍不划算,同名就是同一份 jar 的
|
||||
@@ -121,13 +126,18 @@ fn kind_of(failure: &DependencyFailure) -> ContentFailureKind {
|
||||
///
|
||||
/// 档位**一一映射** core 的 [`FailureKind`]:两档在 DTO 上分开,界面才有两段文案。
|
||||
/// 顺序沿用计划里的顺序(`record_failure` 已按项目键去重,同一项目只出现一次)。
|
||||
///
|
||||
/// `root_title` 是**用户点的那次装入的显示名**(`ContentInstallRequest.title`,来自检索命中的
|
||||
/// 项目标题)。它只用来给「根项目自己失败」那条垫一个可读名(M191 r1 P2-1:面板原来只画
|
||||
/// `P7dR8mSH` 这种站点 id,用户拿到的是一串哈希)。
|
||||
#[must_use]
|
||||
pub(super) fn retry_items(plan: &DependencyPlan) -> Vec<InstallFailureView> {
|
||||
pub(super) fn retry_items(plan: &DependencyPlan, root_title: &str) -> Vec<InstallFailureView> {
|
||||
plan.failures
|
||||
.iter()
|
||||
.map(|failure| {
|
||||
InstallFailureView::new(
|
||||
failure.requested_as.clone(),
|
||||
label_of(plan, failure, root_title),
|
||||
failure_kind_of(failure.kind),
|
||||
failure.message.clone(),
|
||||
)
|
||||
@@ -135,6 +145,34 @@ pub(super) fn retry_items(plan: &DependencyPlan) -> Vec<InstallFailureView> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// 一条失败的可读标签(**语言中立**:站点标题或文件名;拿不到就是空串)。
|
||||
///
|
||||
/// 两个来源,都在手边、零额外请求:
|
||||
///
|
||||
/// | 情况 | 标签 | 为什么 |
|
||||
/// |---|---|---|
|
||||
/// | 根项目自己失败 | 请求里的 `title` | 用户刚才点的就是它,界面其它地方用的也是这个名字 |
|
||||
/// | 计划里有这个节点(下载失败) | `node.file.filename` | 「哪个文件没下来」比项目名更具体 |
|
||||
/// | 都没有(解析阶段 404 / 不兼容) | 空 | 站点没给过名字;界面退回只画 id(合理兜底) |
|
||||
///
|
||||
/// **不许**拿 `failure.message` 顶:那是 core 的中文散文,画到界面上就是 i18n R2 的违规。
|
||||
#[must_use]
|
||||
fn label_of(plan: &DependencyPlan, failure: &DependencyFailure, root_title: &str) -> String {
|
||||
if plan
|
||||
.request
|
||||
.roots
|
||||
.first()
|
||||
.is_some_and(|root| root == &failure.requested_as)
|
||||
{
|
||||
return root_title.to_string();
|
||||
}
|
||||
plan.nodes
|
||||
.iter()
|
||||
.find(|node| node.requested_as == failure.requested_as)
|
||||
.map(|node| node.file.filename.clone())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// core 的档位 → DTO 的档位(**穷尽 match**:谁把两档合并,这条会报错或让用例红)。
|
||||
const fn failure_kind_of(kind: FailureKind) -> InstallFailureKind {
|
||||
match kind {
|
||||
@@ -262,63 +300,6 @@ fn unresolved_note(plan: &DependencyPlan) -> Option<String> {
|
||||
))
|
||||
}
|
||||
|
||||
/// 「上一次装入留下的计划」表(M191):`只重试这些` 的输入。
|
||||
///
|
||||
/// ## 为什么要一张静态表(说清楚这笔债)
|
||||
///
|
||||
/// 重试要的输入是**上一轮那份 `DependencyPlan`**(`retry_request()` 是它的方法)。它现在活在
|
||||
/// `CoreHost`(`host/dispatch.rs`)之外,因为那个文件不在本 mission 的 scope;与
|
||||
/// `addon/pack.rs` 的 `PACK_STATE` 同一手法:每 host 一格(键 = 数据根),于是测试里两个 host
|
||||
/// 互不干扰。`host/mod.rs` 一旦进 scope,把这张表搬成一个字段即可(这表就是那个字段)。
|
||||
///
|
||||
/// 键里带 `instance`:同一台机器上往两个实例装过东西,重试只该重试这个实例那一次的计划。
|
||||
/// 成功之后由 [`forget`] 清掉——「全成功」时不该留下任何可重试的东西。
|
||||
struct Remembered {
|
||||
host: PathBuf,
|
||||
instance: String,
|
||||
/// 落盘目录(重试要按它重新扫 `present`,且**不许**换目录)
|
||||
dir: PathBuf,
|
||||
plan: DependencyPlan,
|
||||
}
|
||||
|
||||
static REMEMBERED: Mutex<Vec<Remembered>> = Mutex::new(Vec::new());
|
||||
|
||||
/// 记下这一轮的计划(**覆盖**同一个 host + 实例的上一份)。
|
||||
pub(super) fn remember(env: &HostEnv, instance: &str, dir: &Path, plan: &DependencyPlan) {
|
||||
let host = env.layout.home().to_path_buf();
|
||||
let mut table = REMEMBERED
|
||||
.lock()
|
||||
.unwrap_or_else(|poison| poison.into_inner());
|
||||
table.retain(|item| !(item.host == host && item.instance == instance));
|
||||
table.push(Remembered {
|
||||
host,
|
||||
instance: instance.to_string(),
|
||||
dir: dir.to_path_buf(),
|
||||
plan: plan.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
/// 忘掉这个实例的计划(装成功 / 没有失败项时调:没有可重试的东西了)。
|
||||
pub(super) fn forget(env: &HostEnv, instance: &str) {
|
||||
let host = env.layout.home().to_path_buf();
|
||||
let mut table = REMEMBERED
|
||||
.lock()
|
||||
.unwrap_or_else(|poison| poison.into_inner());
|
||||
table.retain(|item| !(item.host == host && item.instance == instance));
|
||||
}
|
||||
|
||||
/// 这个实例上一次失败的计划(+ 当时的落盘目录);`None` = 没有可重试的记录。
|
||||
pub(super) fn remembered(env: &HostEnv, instance: &str) -> Option<(PathBuf, DependencyPlan)> {
|
||||
let host = env.layout.home().to_path_buf();
|
||||
let table = REMEMBERED
|
||||
.lock()
|
||||
.unwrap_or_else(|poison| poison.into_inner());
|
||||
table
|
||||
.iter()
|
||||
.find(|item| item.host == host && item.instance == instance)
|
||||
.map(|item| (item.dir.clone(), item.plan.clone()))
|
||||
}
|
||||
|
||||
/// 全部装成之后的落盘描述(含跳过与前置的条数、「为什么装」的父项目)。
|
||||
///
|
||||
/// `installed` 是**真落盘**的那些(下载失败的不在里面),所以条数与文件名都从它来,
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
//! 「上一次装入留下的计划」表(M191):`retry_plan()` 的输入。
|
||||
//!
|
||||
//! 从 `plan.rs` 拆出(父文件加了 `label_of` 之后顶到 429 行,越过 400 行硬线)。
|
||||
//! 拆法与本仓既有 `-p1/-p2` 一致:**纯搬移**,一行不改语义;调用方路径不变
|
||||
//! (`plan.rs` 仍然 `pub(super) use` 这三个函数)。
|
||||
//!
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Mutex;
|
||||
|
||||
use unlucky_core::addon::online::DependencyPlan;
|
||||
|
||||
use super::super::super::env::HostEnv;
|
||||
|
||||
/// 「上一次装入留下的计划」表(M191):`只重试这些` 的输入。
|
||||
///
|
||||
/// ## 为什么要一张静态表(说清楚这笔债)
|
||||
///
|
||||
/// 重试要的输入是**上一轮那份 `DependencyPlan`**(`retry_request()` 是它的方法)。它现在活在
|
||||
/// `CoreHost`(`host/dispatch.rs`)之外,因为那个文件不在本 mission 的 scope;与
|
||||
/// `addon/pack.rs` 的 `PACK_STATE` 同一手法:每 host 一格(键 = 数据根),于是测试里两个 host
|
||||
/// 互不干扰。`host/mod.rs` 一旦进 scope,把这张表搬成一个字段即可(这表就是那个字段)。
|
||||
///
|
||||
/// 键里带 `instance`:同一台机器上往两个实例装过东西,重试只该重试这个实例那一次的计划。
|
||||
/// 成功之后由 [`forget`] 清掉——「全成功」时不该留下任何可重试的东西。
|
||||
struct Remembered {
|
||||
host: PathBuf,
|
||||
instance: String,
|
||||
/// 落盘目录(重试要按它重新扫 `present`,且**不许**换目录)
|
||||
dir: PathBuf,
|
||||
/// 那次装入的显示名(重试时面板头与失败名单的可读名都沿用它,不再退回项目 id)
|
||||
title: String,
|
||||
plan: DependencyPlan,
|
||||
}
|
||||
|
||||
static REMEMBERED: Mutex<Vec<Remembered>> = Mutex::new(Vec::new());
|
||||
|
||||
/// 记下这一轮的计划(**覆盖**同一个 host + 实例的上一份)。
|
||||
pub(in crate::host::addon) fn remember(
|
||||
env: &HostEnv,
|
||||
instance: &str,
|
||||
dir: &Path,
|
||||
title: &str,
|
||||
plan: &DependencyPlan,
|
||||
) {
|
||||
let host = env.layout.home().to_path_buf();
|
||||
let mut table = REMEMBERED
|
||||
.lock()
|
||||
.unwrap_or_else(|poison| poison.into_inner());
|
||||
table.retain(|item| !(item.host == host && item.instance == instance));
|
||||
table.push(Remembered {
|
||||
host,
|
||||
instance: instance.to_string(),
|
||||
dir: dir.to_path_buf(),
|
||||
title: title.to_string(),
|
||||
plan: plan.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
/// 忘掉这个实例的计划(装成功 / 没有失败项时调:没有可重试的东西了)。
|
||||
pub(in crate::host::addon) fn forget(env: &HostEnv, instance: &str) {
|
||||
let host = env.layout.home().to_path_buf();
|
||||
let mut table = REMEMBERED
|
||||
.lock()
|
||||
.unwrap_or_else(|poison| poison.into_inner());
|
||||
table.retain(|item| !(item.host == host && item.instance == instance));
|
||||
}
|
||||
|
||||
/// 这个实例上一次失败的计划(+ 当时的落盘目录 + 显示名);`None` = 没有可重试的记录。
|
||||
pub(in crate::host::addon) fn remembered(
|
||||
env: &HostEnv,
|
||||
instance: &str,
|
||||
) -> Option<(PathBuf, String, DependencyPlan)> {
|
||||
let host = env.layout.home().to_path_buf();
|
||||
let table = REMEMBERED
|
||||
.lock()
|
||||
.unwrap_or_else(|poison| poison.into_inner());
|
||||
table
|
||||
.iter()
|
||||
.find(|item| item.host == host && item.instance == instance)
|
||||
.map(|item| (item.dir.clone(), item.title.clone(), item.plan.clone()))
|
||||
}
|
||||
@@ -36,7 +36,7 @@ fn plan_with_two_tier_failures() -> DependencyPlan {
|
||||
/// 抓得住:把 `Incompatible` 并进 `Unreachable`(两档合成一档)⇒ 这条立刻红。
|
||||
#[test]
|
||||
fn the_retry_list_keeps_the_two_tiers_apart() {
|
||||
let items = retry_items(&plan_with_two_tier_failures());
|
||||
let items = retry_items(&plan_with_two_tier_failures(), "Sodium");
|
||||
assert_eq!(items.len(), 2);
|
||||
assert_eq!(
|
||||
items
|
||||
@@ -63,7 +63,7 @@ fn a_clean_plan_offers_nothing_to_retry() {
|
||||
vec![node("AANobbMI", "sodium.jar", 0, &[])],
|
||||
Vec::new(),
|
||||
);
|
||||
assert!(retry_items(&clean).is_empty());
|
||||
assert!(retry_items(&clean, "Sodium").is_empty());
|
||||
assert!(failure_of_plan(&clean, 1).is_none());
|
||||
}
|
||||
|
||||
@@ -116,9 +116,11 @@ fn the_last_failed_plan_is_remembered_per_instance() {
|
||||
|
||||
let plan = plan_with_two_tier_failures();
|
||||
let dir = root.join(".minecraft/mods");
|
||||
remember(&env, "survival", &dir, &plan);
|
||||
let (saved_dir, saved) = remembered(&env, "survival").expect("记住了这一次");
|
||||
remember(&env, "survival", &dir, "Sodium", &plan);
|
||||
let (saved_dir, saved_title, saved) = remembered(&env, "survival").expect("记住了这一次");
|
||||
assert_eq!(saved_dir, dir);
|
||||
// 显示名跟着计划一起记住:重试时面板头与失败名单的可读名都沿用它(不再退回项目 id)
|
||||
assert_eq!(saved_title, "Sodium");
|
||||
assert_eq!(saved.failures.len(), 2);
|
||||
// 别的实例不受影响(同一台机器上装过两个实例)
|
||||
assert!(remembered(&env, "modded").is_none());
|
||||
@@ -138,7 +140,7 @@ fn the_last_failed_plan_is_remembered_per_instance() {
|
||||
#[test]
|
||||
fn the_dto_list_is_exactly_what_the_retry_request_asks_for() {
|
||||
let plan = plan_with_two_tier_failures();
|
||||
let items: Vec<InstallFailureView> = retry_items(&plan);
|
||||
let items: Vec<InstallFailureView> = retry_items(&plan, "Sodium");
|
||||
let request = retry_request(&plan, Vec::new());
|
||||
assert_eq!(
|
||||
items
|
||||
@@ -148,3 +150,66 @@ fn the_dto_list_is_exactly_what_the_retry_request_asks_for() {
|
||||
request.roots
|
||||
);
|
||||
}
|
||||
|
||||
/// **P2-1 的守门**:面板上写的是可读名,不是裸项目 id。
|
||||
///
|
||||
/// 三个来源各一条(`plan::label_of` 的三种情况),而且都必须**语言中立**(站点标题 / 文件名,
|
||||
/// 不是 core 的中文散文):
|
||||
///
|
||||
/// | 项 | 情况 | 期望那一行 |
|
||||
/// |---|---|---|
|
||||
/// | 根项目 | 它自己失败 | `Sodium (AANobbMI)` —— 名字来自请求的 `title` |
|
||||
/// | 前置 | 下载失败(计划里有节点) | `fabric-api.jar (P7dR8mSH)` —— 名字来自文件名 |
|
||||
/// | 前置 | 解析阶段 404(没有节点) | `1eAoo2KR` —— 拿不到名,只画 id(合理兜底) |
|
||||
///
|
||||
/// 变异守门:把 `label_of` 的标签置空(或 `retry_items` 传空串)⇒ 这条红。
|
||||
#[test]
|
||||
fn the_retry_list_shows_a_readable_name_not_just_the_project_id() {
|
||||
let mut plan = plan(
|
||||
vec![
|
||||
// 根项目自己失败(下载失败):requested_as 与 roots[0] 相同
|
||||
failure(
|
||||
"AANobbMI",
|
||||
FailureKind::Unreachable,
|
||||
Some(OnlineErrorKind::NotConnected),
|
||||
"下载 sodium.jar 失败(online_not_connected):连不上 cdn.modrinth.com",
|
||||
),
|
||||
// 前置下载失败:计划里有它的节点 ⇒ 用文件名
|
||||
failure(
|
||||
"P7dR8mSH",
|
||||
FailureKind::Unreachable,
|
||||
Some(OnlineErrorKind::NotConnected),
|
||||
"下载 fabric-api.jar 失败(online_not_connected):连不上 cdn.modrinth.com",
|
||||
),
|
||||
// 前置解析 404:没有节点、也不是根 ⇒ 没有名字可显示
|
||||
failure(
|
||||
"1eAoo2KR",
|
||||
FailureKind::Unreachable,
|
||||
Some(OnlineErrorKind::NotFound),
|
||||
"取版本失败(online_not_found):HTTP 404",
|
||||
),
|
||||
],
|
||||
vec![
|
||||
node("AANobbMI", "sodium-fabric-0.6.0.jar", 0, &[]),
|
||||
node("P7dR8mSH", "fabric-api.jar", 1, &["AANobbMI"]),
|
||||
],
|
||||
Vec::new(),
|
||||
);
|
||||
// 根必须是请求里的那个键(`plan::request` 只放一个根)
|
||||
plan.request = unlucky_core::addon::online::ResolveRequest::single("AANobbMI");
|
||||
|
||||
let items = retry_items(&plan, "Sodium");
|
||||
let lines: Vec<String> = items.iter().map(InstallFailureView::line).collect();
|
||||
assert_eq!(
|
||||
lines,
|
||||
vec![
|
||||
"Sodium (AANobbMI)".to_string(),
|
||||
"fabric-api.jar (P7dR8mSH)".to_string(),
|
||||
// 拿不到名的那一档:只画 id,不编造、也不拿 core 的中文 message 顶上
|
||||
"1eAoo2KR".to_string(),
|
||||
]
|
||||
);
|
||||
// 标签是语言中立的:站点标题/文件名,不是 core 的中文散文
|
||||
assert!(!items[0].label.contains("下载"), "{}", items[0].label);
|
||||
assert!(!items[1].label.contains("失败"), "{}", items[1].label);
|
||||
}
|
||||
Reference in new issue
Block a user