Commit Graph
11 Commits
Author SHA1 Message Date
AskaEth b394316df4 fix(m162-r1): 叶子成因穿出引擎 + 安装路径切 core 在线源 + 详情迟到结果闸门
P1-1(p1-1items 的根因):
- download/error.rs:`DownloadError` 增 `leaf: Option<DownloadErrorKind>` 与
  `effective_kind()`;`leaf_of()` 汇总规则只此一处:全 404 ⇒ NotFound、有
  Network/Timeout ⇒ Network、全 5xx ⇒ ServerError、混合按「网络错 > 5xx > 404」
  取最可诊断的那个(**不从 suppressed 文本猜状态码**)。
- download/bytes.rs:候选源循环收集逐源 kind,收尾时把叶子挂到 AllSourcesFailed 上。
- online/transport.rs:`From<DownloadError>` 改用 `effective_kind()`,于是
  NotFound/Server/Integrity 在生产路径可达(项目被删/id 拼错/限速不再画成「未联网」)。
- import/fetch.rs:新增 `FetchErrorKind::Offline`(真正的「没联网」)并与 Network 分开;
  `From<DownloadError>`/`From<OnlineError>` 都按叶子/大类映射。
- import/complete.rs:补全只在 `Offline` 时早停;404(真实 mrpack 大多托管在 CF CDN,
  Modrinth 必然 404)与 5xx 只影响那一条,不再掐掉整轮;注记「因未联网提前停止」因此
  只会在真断网时出现。

P2-1:presenter 安装路径不再自己拼 Modrinth URL / 自己解析 JSON——改用
`core::addon::online::Modrinth::versions` + `primary_file()` + `VersionFile::sha1/sha512`;
`HostTask::InstallDone.result` 改成 `Result<String, ContentFailure>`,
`InstallProgressView` 增 `failure_kind`(离线 ≠ 查无此项)。

P2-2:`app/update/content.rs` 把被详情方法抢走的「装入闸门」注释还回
`content_install_request`,详情方法只留自己那一行。

P2-3:`ContentState::apply_detail/fail_detail` 带上 project(事件与 HostTask 都携带请求时的
项目 id),迟到结果一律丢弃;`request_detail` 记下在飞项目,`detail_pending` 不再被迟到
结果清掉。

顺手项:`usable_hash` 让形状非法的那一档摘要被**丢弃**(sha512 仍校验,不再整条跳过;
两档都不可用才拒绝);`digest_file` 的 IO 失败先删落盘文件再返回。i18n 的
ContentOfflineBody 改成不暗示「有缓存」的措辞(golden 同步)。

用例:download/error.rs 叶子四档 + effective_kind;online/tests/classify.rs 5 条
(AllSourcesFailed+leaf 404 ⇒ NotFound、无叶子 ⇒ NotConnected 保守、叶子一一映射、
单源自身 kind、导入侧 404≠Offline);import_online 增「404 不停」「真离线才停」
(共 7 条);download 增「sha1 形状非法仍用 sha512 校验成功」;presenter 增详情迟到闸门。
2026-11-07 17:28:36 +08:00
AskaEth 81278f55af feat(m162): Modrinth 在线内容源(匿名)+ 远端条目按 sha512 补全 + 离线态
core:新增 addon::online —— 按 Modrinth v2 真实形状建模(search facets / project /
project versions / version_file by hash),网络出口只有 Transport 一个 trait,生产实现
EngineTransport 走既有 DownloadEngine::download_bytes(零新依赖);错误分大类,网络类
失败映射成 NotConnected(= 未联网),不再用空列表冒充「没有结果」。

core:online::download::download_verified —— sha1 交给引擎流式校验、sha512 落盘后复核
(download::HashAlgo 没有 Sha512,改它超出 scope,已登记 finding),不符/失败删文件重试,
最多 3 次尝试且不留半截文件。

core:远端条目补全 —— PackFetcher::modrinth_file_by_sha512(默认 Unsupported = 补不出),
import 管线与新增的 scan_pack_online 都会在「有 path 但缺 hashes.sha1」时按 sha512 反查;
补出的 sha1 走 M160 同一套 judge 与合并判定,补不出仍进 unknown_remote 计数(口径未改)。
DownloadEngineFetcher 改用 download_verified,FetchRequest 增 sha512。

presenter:Modrinth 检索/详情改由 core 的 online 客户端提供(不再自己拼 URL、不再读错
date_modified);ContentFailure{kind} 取代裸字符串,无网 = Offline;新增
ContentDetailRequest/ContentDetail + LoadContentDetail 命令与 CoreEvent::ContentDetail 通道;
预检改走 scan_pack_online;内容库安装也走 download_verified(sha1+sha512)。

app-iced:Status::Offline 独立一态(未联网 ≠ 空列表 ≠ 普通失败),详情面板消费新 DTO
显示可用版本;选中项目即拉详情。i18n 新增 6 个键并同步金表。

测试:online 模块 24 个离线用例(录制 JSON + 假字节:搜索/详情/版本/按摘要反查/校验失败
即删/重试/离线分类/补全),import 侧 6 个(离线计数不变、补全后判冲突、补错摘要 ⇒ Failed),
presenter/app-iced 各补失败分类与详情状态机用例。
2026-11-07 17:28:36 +08:00
AskaEth 03741ecd2d fix(m161): 收更新键/按钮/矮窗口/启动错误/导入结果五笔债
① update.auto_check / update.channel 登记进 core setting schema(1.6.0)
   - 新增 setting/keys/update.rs(两键 + UPDATE_CHANNELS,只允许 stable);
     SettingSchema 加 with_all 供域键表批量登记。
   - 新增 setting/update_prefs.rs:cache/update/prefs.json → launcher.json 的一次性迁移。
     四条硬约束:源指纹闸门(幂等)、不丢 false、新值胜出、旧文件改名 .migrated 后不再被读;
     主配置不可写时不动旧文件。收据 config/update-prefs-migration.json。
   - presenter Runtime 建会话时迁一次并把结局留在 migration();prefs 字段换成
     auto_check + 每次派发从主配置读 channel(不再硬编码 Channel::Stable)。
     保留旧字段 auto_check 的真实来源(原有测试 cases 会断言)。
   - CLI config get/set/dump 先跑同一次迁移(幂等),输出里给 migration 段与收据路径。

② presenter/dto/events.rs 的「本次会话不再弹提示条」注释改成真实语义
   (记的是版本号:同版本压住、更高版本照弹、手动检查清掉)。

③ 检查/下载进行中按钮禁用:pages/update/busy.rs 的 busy/can_check/can_apply;
   设置页两颗按钮改画禁用(不是消失),app 层 on_update_action 再挡一道
   (忙碌时 CheckNow/Apply 不派发);新增两条用例(含「忙完恢回可点」)。

④ 内容库低高度:content_layout::MIN_BODY_HEIGHT=460 + BodyMode(按固定件+三整行算出来,
   用例守着算式);frame::body_height 从 Ui::metrics 算正文可用高,pages::Model 带 window_height。
   ≥460 列表自己滚;<460 主列整列一个滚动条、列表按自然高度铺开。

⑤ 启动错误文案本地化:presenter 删掉 launch_summary / 「启动失败:」前缀 / 五条中文阶段前缀,
   改发 LaunchFailureView{stage,detail};LaunchFinished 改带 exit_code/uptime_ms/failure,
   Toast 由 app 层按 i18n key 现求(与 install_cancelled 同构)。InstancesFailed 拆成
   InstancesFailedStage(列表单行)+ LaunchStageFailed(详情/Toast 全文)。

⑥ 导入结果块:ImportReportView 加 failure_stage(NotStarted/RolledBack/RollbackIncomplete,
   由 core 的 rollback 机器位推出);标题、图标、底色三处分档,预检失败不再说「已回滚」。

顺带:六处注释/文档漂移(update.rs 的分工表与偏好落点、pack 的「已整包回滚」、
build_request 的 Errors、settings/update 的按钮禁用说明等),
以及为守 400 行把 update/cli-config/core-keys/presenter-dto/presenter-launch/i18n-golden
里贴线或越线的文件按模块拆开(纯搬移,行为不变)。
2026-11-07 15:02:13 +08:00
AskaEth 1c8f6aa1b3 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 → 备份留旧文件」,并验证
摘要不符时拒绝应用、目标文件零改动、暂存目录零残留。
2026-11-07 12:09:42 +08:00
AskaEth a183adc072 refactor(app-iced, presenter): M114 rebase 后守 400 行 —— 四处纯搬移(不动语义、不压注释、不挪断言)
主线六条合入后,四个被我碰过的文件越过 400 行;按模块拆开,只搬不改:

- `presenter/host/mod.rs` 421→366:账户刷新/皮肤三段(refresh_accounts / attach_skins /
  request_skins)搬进 `host/accounts/refresh.rs`(`impl CoreHost`,pub(in crate::host))
- `presenter/state.rs` 410→392:`TileCell` + `grid_positions` 搬进 `state/tiles.rs`,
  `state.rs` 用 `pub use` 再导出,老路径不变
- `app-iced/app/state.rs` 401→394:账户页重建那一段搬进 `app/view/account_page.rs`
  的 `App::rebuild_account_page`
- `app-iced/app/update.rs` 407→388:`ULL_START_ROUTE` 取值表搬进 `app/update/boot_env.rs`
  (纯函数、无状态)
2026-11-05 05:13:54 +08:00
AskaEth 1ff00bbe50 feat(app-iced): M114 账户页三轨登录(微软正版 / 第三方皮肤站 / 本地档案)
- 账户页:当前账户卡 + 全部账户列表 + 空态三入口 + 皮肤区;五项动作(添加/切换/删除/重新登录/校验)
- 三轨添加流程:设备码(用户码+链接+倒计时+过期重试+取消)、授权码(浏览器+可复制链接+取消)、
  本地档案(名字校验)、第三方站(服务地址 + 服务发现校验 + 密码登录,密码掩码且用完即弃)
- 导航:账户为一等页,入口是轨底头像位(未登录=描边剪影 / 已登录=首字母块+名字;窄高收成图标条)
- 凭据红线:DTO 侧形状脱敏 + 状态层二次脱敏 + 密码掩码;Inject-JWT 测试断言视图模型干净(已做变异取证)
- presenter:authlib 登录/服务发现/皮肤抓取(复用下载引擎的 HTTP 客户端,无新增依赖)、
  皮肤原图经 DTO 到 UI 裁剪成头像(既有 image 依赖,未改 Cargo.toml)
- i18n:新增 account 域 51 条中英文案(离线档案口径全量写入),金表同步
2026-11-05 05:08:35 +08:00
AskaEth 134b0a2aa4 fix(packui): M113 r2 复审整改 —— RAII 在跑句柄、per-host 代次、注释与代码对齐
复审 r2(p2-3items / fix-then-merge)逐条整改。**P2-6 与 P3-3/4/5 按塔台指示不动**,只登记。

## 必修 1:`PACK_RUNNING` 在早退路径不清零(跨功能泄漏)

改前:`fresh_cancel()` 置 bool,`super::finished()` 在 `block_on(import_pack(..))` **之后**才清;
探测失败 / 目标目录不可用两条 `return` 都在它之前 ⇒ bool 永久留在「在跑」。
后果:之后**任何**一次 `CancelInstall`(包括下载页的「取消安装」)①不再发 `InstallCancelled`、
②还给内容库发 `PackEvent::Cancelled`(一句「导入还在后台跑」的假话)。

改后:`InFlight` **RAII 句柄**(`Drop` 减一)取代手工 `finished()`;计数用「每个 host 一格」的
`in_flight: usize` 而不是 bool(连点两次 Import 各自进出)。
`InFlight::begin(&env)` 在**派发点**创建、随线程移动到出口 ⇒ 正常收尾、两条早退、`?`、panic
展开**任何**退出路径都会减一,以后再加早退分支也不用记得补一行。

## 必修 2:`dir_name` 注释与实测不一致

改前:注释与报告写「`Better MC` → `Better-MC`(不加哈希)」,而 `faithful` 只认 `[A-Za-z0-9_-]`,
空格不算 ⇒ 实际 `Better-MC-c7c9c149`;测试只钉了无空格形态。

改后:**选「空格算 faithful」这边**,注释、代码、测试三处对齐 —— 名字只含 ASCII 字母数字、
空格、`-`、`_` 时保持原样(空格折成 `-`);折过**别的**字符(中文、`.`、括号…)才补 `-<hash8>`。
测试钉住四例:`Better-MC` → 原样、`Better MC` → `Better-MC`(不加哈希)、`pack` → `pack`、
`Survival 1.20.1` → `Survival-1-20-1-<hash>`(折过 `.` 必须补)。

## 必修 3:两处注释与代码相反

- `host/job.rs` 的 `HostTask::Pack`:原写「代次与 `HostTask::Install` 共用同一计数器」,改为
  「pack 自己的一条(`PACK_EPOCH`),不与 `install_epoch` 共用」并写明为什么(P2-7 的教训)。
- `host/addon/pack/import.rs` 的 `target_of`:`<slug>` → `<dir_name>`,并注明中文名不会被折成同一个 `pack`。

## 必修 4:两条 P3

- `04` 图里「Back」出现两次:有预检结果时字段区那行只留「预检」,`Back` 只由预览块给一个
  (没有预检时才在字段区给 `Back`)。
- 导入侧 `rejected` 列表补 `bounded`(与 missing / conflicts 同一口径)。

## 必修 5:app 级用例(P2-1 的护栏)

新增 `a_failed_import_releases_the_in_flight_flag_and_keeps_install_cancel_intact`:
`CoreHost::headless` + 真派 `PackCommand::Import`(包不存在 ⇒ 探测早退)→ 泵到 `Probe` →
断言 `in_flight == 0` → 再派 `CancelInstall` → 断言**仍会**发 `InstallCancelled` 且**不发**
`PackEvent::Cancelled`。

## P3-6(裸静态量):按塔台要求正面回答并已改掉

不再用裸静态量:`PACK_EPOCH` / `PACK_RUNNING` / `PACK_CANCEL` 换成**按 host 键控**的一张表
(键 = `LauncherLayout::home()`),理由是实测撞到过串扰 —— `host/cancel_tests` 派一次
`CancelInstall` 就 bump 了全局代次,我这条在飞的导入事件被按代次丢掉(新用例因此红过)。
为什么不挪进 `CoreHost`:那要给 `host/mod.rs` 加字段(不在 scope)。这张表等价,`host/mod.rs`
一旦进 scope,搬过去只是把表删掉 —— 报告里也这么写了。
新增用例 `pack_epoch_is_per_host_and_is_its_own_counter` 钉住「另一个 host 的代次不受影响」。

## 顺手的两条

- `pack/tests.rs` 加完用例后 429 行超线 ⇒ 夹具(临时目录 / 写文件 / 泵 host)拆到
  `pack/tests/harness.rs`,`tests.rs` 回到 3xx 行。
- clippy `-D warnings` 抓到 `== false`、两处多余的 `&`、一个未用的再导出,都改掉了。

门禁:`cargo fmt --all --check` 通过;clippy 三 crate `--all-targets -- -D warnings` EXIT=0;
`cargo test -p unlucky-app-iced --lib` **507 passed**;`cargo test -p unlucky-presenter --lib`
**185 passed**(新增的 app 级用例在内,且是在**全量并行**下跑的);改动文件全部 ≤400 行。
2026-11-05 04:12:39 +08:00
AskaEth 5f09b8113a feat(packui): M113 phase 2 —— 导入导出真正接线(三个共享枚举各只多一个变体)
搬到 M109 rebase 后的新 main(26e764e)之后按**新 main 的形状**写一次,不写两遍。

## 接线

- `dto/pack/event.rs`(新):`PackCommand` / `PackEvent` / `PackState::apply`。
  整条流程在三个最挤的共享枚举里各只占**一个**变体(`AppCommand::Pack` / `CoreEvent::Pack` /
  `HostTask::Pack{event, epoch}`),子枚举承载细节 —— rebase 时不用替别人定序。
- `events.rs` / `job.rs` / `handle.rs` / `dispatch.rs` / `state.rs`:变体与分支,全部带
  `// M113: import/export` 锚点。`state.rs` 只多 1 个字段 + 1 条 apply 分支(399 行)。
- `host/addon/**`(新目录,父文件只加 `mod pack;` + 再导出):
  - `pack.rs`:`CoreHost::pack` 落点 + 取消标记 + 实例目录/快照/格式/缺省路径解析
  - `pack/import.rs`:探测 → 目标目录 → `import_pack` → 报告(默认 KeepUser,显式才 Overwrite)
  - `pack/export.rs`:预检(`scan_instance`,**不写盘**)与写盘(`export_instance`)
  - `pack/convert.rs`:core 报告 → DTO,逐字段照搬(三态/五阶段/四处置/三成因/十一原因)
  - `pack/diagnose.rs`:探测诊断「像 X 缺 Y」(按标记文件的有无,不看文件名)
- `app/update/content.rs`:导入 / 以包为准 / 预检 / 确认写盘 / 关面板真的派指令;
  预检时先记下那份请求,确认时**原样发回**(防「预检 A、写 B」)。
- `pages/content/model.rs`:读 `state.pack`(不再是默认空态)。
- 面板接口按新形状改:`install_panel_model(&InstallProgressView, catalog) -> Option<Download>`
  (`InstallView` 不再从 `pages/download` 导出)。

## 取消(如实)

`CancelInstall` 现在同时置导入的取消标记 + 代次 +1:面板立刻收回、旧事件按代次丢弃。
但 core 的 `import_pack` 没有取消令牌、进度回调也拿不到控制流 ⇒ **后台那次导入会跑完**。
所以「取消」的真实含义是「不再显示 + 结果作废」,已在模块文档与报告里写清,并单开 finding。
取消标记用 `Arc<AtomicBool>` 而不是裸静态量:裸静态量会在并行测试间泄漏取消状态(实测到过)。

## 图看出来的三处(塔台要求)

1. `no download source (CurseForge file API not wired)` → `no download source available yet`,
   API 名只留在 DTO 的 detail 与日志里;顺带把 `sha512` / `projectID/fileID` 也改成用户话。
2. 右栏 `Installing modpacks is not available yet` → `Installing modpacks from the library into
   an instance is not available yet`(那句在 i18n 词表里,不在 M109 页面代码里,所以我自己改)。
3. 结果面板接线后可以关:`Dismiss` / `AbortExport` 发 `PackCommand::Dismiss` → host 回
   `PackEvent::Cleared` → 回到入口/浏览态,不再常驻。

## 证据(三条都是真跑,不是假数据)

- `bad_packs_get_a_named_diagnosis`:把文本改名成 .mrpack → 有具名诊断(不是静默、不 panic);
  目录形态有 overrides/ 无 manifest.json → 说得出「缺哪个标记」。
- `a_real_pack_runs_through_the_pipeline_end_to_end`:造一个真 MultiMC 实例目录当包,
  走 `import::run` 真路径,断言事件流里有进度 + 结果、格式=MutliMC、报告有写盘文件、文件真落地、
  代次原样带回。
- `preview_counts_match_the_written_report`:同一实例上跑预检与 `export_instance`,
  inline / skipped / remote **三项计数逐项相等**,且逐条路径的归类一致。

门禁:cargo check ✓ / clippy -D warnings(三 crate)EXIT=0 /
cargo test(app-iced 504 + 6 守卫,i18n 24,presenter 181)全绿 / rustfmt --check ✓ /
所有改动文件 ≤400 行(`state.rs` 399、`addon.rs` 106,pack 子目录最大 338)。
2026-11-05 04:06:39 +08:00
AskaEth 6b161227d8 feat(app-iced): M109 CONTENTDL —— 内容库页(整合包/Mod/资源包/光影)
用户令「下载页只负责下载整合包和Mod/光影」:新开一页做**内容发现与装入**,
检索走 Modrinth(优先,无需 key)/ CurseForge(`ULL_CF_API_KEY` 环境变量,日志只打
set/unset),装入走 core 的下载引擎真下到实例目录,进度/取消/失败复用下载页那块安装面板。

- **入口(B1)**:`Route::Content` 是新顶层路由,但 `Route::nav()` 复用 `NavSection::Download`
  ——导航轨仍五项,页头右上是「版本管理 | 内容库」一级切换(版本管理页也加了同一个切换,
  否则内容库只有 `ULL_START_ROUTE` 能到)
- **页内四条一级侧栏**(`pages/content/layout.rs` 四档几何:992/856/648 三个门槛都是
  「Σ栏宽 + 间距」算出来的):整合包 / Mod / 资源包 / 光影;标签栏 200(`Resource packs`
  比 M107 的版本类型名长,140 会切字,已登记偏离)
- **筛选**:关键词(草稿 + 显式「搜索」按钮,不逐字打网络)、来源、游戏版本、加载器
  (Fabric/Forge/NeoForge/Quilt)、排序;**分页**(「加载更多」)而不是无限滚动——快照只有一个槽、
  翻页要看得见、状态要可截图复现
- **四态**:加载 / 空 / 失败 / 有数据 + 「没有更多了」;筛选条在任何状态下都在(加载中藏筛选条
  等于把入口收走)
- **一键装入**:`Content::blockers()` 是纯函数——版本不匹配、加载器不匹配、没有目标实例、
  整合包缺管线**四条原因逐条**给出,按钮点之前就禁用;判定与画法分开(model/tests 直接钉)
- **整合包今天不可装**:`ContentKind::Modpack.dir()` 是 `None`,UI 禁用 + 说明,host 侧再挡
  一次(绕过 UI 也只会回一条 Failed)。真流程是 M111 的导入管线
- **零 UI 网络**:`presenter/host/addon.rs` + `addon/{sources,http,install}.rs`;无 header 的请求走
  core 的 `download_bytes`(信号量限流 + 健康度 + 退避全现成),带 `X-API-KEY` 的只有 CF 一条,
  自带 429/5xx 退避(优先吃 `Retry-After`,封顶 30s);密钥不进 DTO/日志/仓库
- 文案 41 条中英进 `crates/i18n`(新域接在装配链末尾 ⇒ `Key::ALL` 与金表只追加)
- 演示数据 `ContentHit → Hit::of` 与产品路径同源(格式化不会两套),`ULL_CONTENT_KIND` /
  `ULL_CONTENT_STATE` 供出图矩阵选类型与状态

取证:`DevDocs/artifacts/m109/`(10 张:四类内容 × 浅/深 + 搜索无结果 + 加载器不兼容禁用)

门禁:fmt ✓ / clippy `-D warnings`(三 crate)✓ / app-iced 450+6、presenter 170、i18n 24 全绿;
`cargo tree` 两条零 UI 门禁仍空;全部改动文件 ≤400 行。
2026-11-05 03:20:21 +08:00
AskaEth 30b1ff6f48 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 + 金表同步)。
2026-11-04 22:12:22 +08:00
AskaEth f159529936 feat(presenter): 新建 unlucky-presenter,把 host/状态/DTO/设置桥从 app 迁出(M85 第 1 块)
D26(iced 重写)的关键一次性动作:UI 框架无关的宿主层独立成 crate,
Slint 版(crates/app)与 iced 版(crates/app-iced)共用,Slint 退役后不再重写一遍。

- 新建 crates/presenter(package `unlucky-presenter` / lib `unlucky_presenter`),
  workspace 登记 + crate 文档头写清「这层为什么存在」、对外表面、UI 侧允许/禁止、
  iced 接线骨架、以及哪些东西留在了 crates/app 及为什么。
- `git mv`(保留文件历史)迁入:host/(25 文件)、dto/(11 文件)、state.rs(+tests)、
  prefs.rs、settings_bridge.rs、toast.rs、router.rs、dialogs.rs、wizard.rs、
  mock.rs、mock_data.rs、backend.rs。模块路径与公开 API 名称不变(`host::install` 等),
  内部实现零改动。
- 新增 `theme`:把原本混在 ui_bind/theme_runtime 里的**纯判据**(CONFIG_POLL_TICKS /
  SYSTEM_THEME_POLL_TICKS / TILE_REGENERATE_STEP / BackgroundKey / tile_viewport /
  system_theme_value / 两个 poll 判据)抽出来,附 5 个单测;app 侧改成调用它们。
- crates/app 改为消费者:删掉迁走的文件,`crate::host|state|dto|prefs|settings_bridge|
  toast|router|dialogs|wizard|mock|mock_data|backend` 全部改写为 `unlucky_presenter::*`
  (64 处),backend.rs 的 config_path/build_backend 提为 pub。
- crates/app-iced 只登记 unlucky-presenter 依赖,本 mission 不接线。
- prefs.rs 的 2 处 slint 文档引用改写;`grep -rn slint crates/presenter` 现在无输出。
2026-11-02 09:29:31 +08:00