Commit Graph
9 Commits
Author SHA1 Message Date
AskaEth 44a0c87a11 style: cargo fmt --all(合并期整合修复)
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / unsafe 落点守门(集合必须恰好一个文件) (push) Canceled after 0s
Guards / design 零第三方依赖守门 (push) Canceled after 0s
Guards / presenter 零 UI 守门 (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Dev Guards / 架构与合规守门(同 main 口径) (push) Canceled after 0s
Dev Guards / fmt (rustfmt --check) (push) Canceled after 0s
Dev Guards / clippy (ubuntu) (push) Canceled after 0s
M187/M188 并入后全仓 fmt --check 为红(CI 的 fmt job 会红)。
本次仅 cargo fmt --all 的换行/缩进变化,无语义改动;
cargo fmt --check 与 cargo check 均已复验为绿。
2026-11-09 01:43:31 +08:00
worker-logplace 5fd5cd47dc feat(m188): launcher.log 早落盘 + 会话日志搬到 logs/game(旧家只读兼容)
用户投诉原话:「我们的启动日志放在哪?…他找半天没找到崩溃日志」「只有一个安装日志」。

启动器运行日志(.ULL/logs/launcher.log)
- presenter 构造 HostEnv 时就登记日志目录:启动早期缓冲的 200 行([probe]/[font]/[theme])
  当场刷盘,不再是「只有开过一局游戏才有日志」。
- app/** 里所有 eprintln! 诊断改走 app::diag::diag!(stderr + 文件双写,行首 [tag] 即 scope),
  含 boot/boot::fallback/state/update 各线。
- boot 那一拍补一行后端事实(request/effective/fallback)。
- 滚动上限(1 MiB → launcher.1.log,只留一份)写进 README 与 docs/logs-location.md。

会话日志搬家:data/agent/sessions → logs
- LauncherLayout: sessions_root()/game_logs() 新增,agent_sessions() 降级为只读兼容。
- host/logs 与 cli/commands/sessions 两个根合并(同 id 新根优先),event_path 回落旧家。
- 不搬不删;日志目录写一份 session-log-location.json 记录(moved=0, policy=read-only-compat)。

入口与文案
- 设置页「通用」新增「日志文件夹」行:路径 + 打开目录 + 复制路径(i18n 中英)。
- 日志页页头的「打开目录」不再是占位:走 M187 的 open_dir,失败文本带路径并送进剪贴板。
- README.md / README.zh-CN.md 新增「日志在哪」章节(含 crash-reports 只在游戏走到那一步才有),
  docs/logs-location.md 给出上限、双写判据、迁移策略与没验的部分。

守门(每条都有变异证明,见报告)
- 旧家会话仍列得出/打不开 ⇒ 只读新根变红;新会话写 logs/game ⇒ 只读旧根、写回旧家都变红。
- launcher.log 滚动 ⇒ 干掉 rename 变红。
- 打开日志文件夹指向真实目录 ⇒ 换成 cache/ 或去掉 create_dir_all 都变红。
- 日志根随三模式变化 ⇒ 把 HostEnv::logs_dir 写死成便携落点变红。
2026-11-09 00:43:45 +08:00
AskaEth b457f0acc8 feat(m179): 外观分组接真语言并拆成五段文件(只接管子,不补文案)
M172 审计:`pages/appearance/**` 中文界面下真正本地化 0/27,两个根因各断一刀。

① 拆文件(纯搬移):`section.rs` 395 行 → `section/` 五个文件,行数守卫只剩 5 行余量,
   不拆就加不了任何东西。
   - `section/mod.rs`(壳:section() + card() + 五个行控件 + 取词口 t)
   - `section/window.rs`(① 整窗 α + ② 圆角,含 notice/说明段)
   - `section/panel.rs`(③ 面板 α)、`section/bg_image.rs`(④⑤⑥⑦⑧)、
     `section/colour_source.rs`(⑨)
   取证:每段与原文件做「去空白/去尾逗号」diff,4/4 相同;只有五处机械改写
   (`rows = rows.push(…)`→`rows.push(…)`、`t(Key::X)`→`t(Key::X, lang)`、
   `super::notice`→`super::super::notice`、一处 `.into()`、rustfmt 换行)。

② 语言接到渲染层:`settings::Body` 从 `fn(&Ui, States)` 变成 `fn(&Ui, States, Lang)`,
   `form` 传 `language.effective`,`settings/appearance.rs` 收 `lang` 转给
   `pages::appearance::section(ui, state, lang)`,`section::t(key, lang)` 取词。
   选「接进唯一那个槽位」而不是再给 `form` 加一条按下标分发的 `else if`:`settings/mod.rs`
   贴着 400 行硬顶(M195 还要在此落地),这条路净增 0 行且不可能与分发表漂开。
   验收点:`pages/appearance/**` 里已经搜不到 `Lang::En`。

③ 能变红的守卫:`the_appearance_group_speaks_chinese_when_the_interface_does`
   —— 走 `settings::view` 真分发渲染一帧,断言取词记录里含汉字、英文那帧不含汉字且同一 key
   中英译文不同。取证口是 `appearance::probe`(`#[cfg(test)]` thread_local):iced 0.14
   没有可遍历控件树的公开接口(`Widget::children()` 返回状态 Tree;`operate` 要 Renderer),
   所以取证点退到「字进树之前」的唯一取词口。变异证明:把 `t()` 改回 `Lang::En.catalog()`
   ⇒ 该用例红(失败信息逐条列出四条英文);还原 ⇒ 绿。

只接语言管子,不补文案:本组其余英文仍是字面量(行号见 M179 报告)。真实窗口截图、
macOS/Linux 字体差异未验。
2026-11-08 20:42:28 +08:00
AskaEth b9009c0e61 fix(m168): 复审 r2 三项 —— 夹具不再空转、启动不再替英文用户下载、时限不再判慢线下不完
复审 r2(p2-3 / fix-then-merge)三项:

1. **P2-1 爬行服务端是空转的**(`font/tests/support.rs`,从 `tests.rs` 拆出守 400 行):
   `seen.windows(4).any(|w| w == b"\n\n")` 拿 4 字节窗口比 2 字节切片 ⇒ `PartialEq` 先比长度 ⇒
   恒 false ⇒ 请求头永远读不完、一个字节不发,用例证明的其实只是「连接挂着被掐断」。上一版
   这里是 Python 生成时把 `\r\n` 落成了真实换行字节。现在两处都是转义写法
   (`b"\r\n\r\n"` + 完整 CRLF 响应头),并且**把「夹具真的在说话」变成断言**:
   `serve_trickle()` 返回 `TrickleProof { header, dripped }`,用例断言「真的发过头」「真的挤过
   字节」——否则夹具再坏一次也会绿。拆成两条用例:`a_trickling_candidate_is_cut_by_the_attempt_deadline`
   ([爬行, 正常],300 ms 切断第一个后第二个成功,0.38 s)与
   `the_total_deadline_cuts_a_single_crawling_candidate`(单候选 + 400 ms 总预算)。
   变异证明:把服务端改成「一个字节都不发」⇒ 两条用例红在「爬行服务端必须真的发出响应头」。

2. **P2-2 启动替英文用户下 17 MiB(真行为 bug)**:
   - `fonts::resolve_with` **不再触发下载**(启动那一拍只报告判据)。唯一的触发点收在
     `fonts::cjk_for`(`ui/lang.rs::log_cjk`),那一拍的 `setting` 与 `LangChoice` 同源
     (ULL_LANG > 配置 > 系统 locale)= 「界面实际用哪种语言」。
   - `plan` 把 `is_downloading()` / `Ready` 的闸提到**所有策略之前**:上一版只在 `NeedsDownload`
     分支里,"下载中 + Unprobeable" 会把状态改回「无法探测」而线程还在飞,下完又翻回「已就绪」。
     当时那条理由(「否则界面停在正在下载」)只对僵死成立 ⇒ worker 现在用 `catch_unwind`
     兜底「任何 panic 都写终态」,闸才敢前移。
   - 出图通道:新增 `install::set_offline(true)`(`App::with_presenter` 在
     `options.screenshot.is_some()` 时调),`plan_with_offline` 在 offline 下永不 `Start`。
     于是 `pages/harness/app.rs` 与 `--screenshot` 都不会在 zh-locale 无字体机器上真下 17 MiB。
   - 被钉住的旧判据(`Unprobeable + Downloading ⇒ Apply`)改成 `Skip` 并写明旧行为为什么是错的;
     新增 `every_strategy_skips_while_a_download_is_in_flight`(四策略打表)与
     `the_screenshot_channel_never_starts_a_download`。

3. **P2-3 90 s 单次上限把「慢但能下完」变永久失败**:改 `ATTEMPT_DEADLINE=120 s` /
   `TOTAL_DEADLINE=360 s`,并且**最后一个候选拿整个剩余总预算**(前 n-1 个仍 `min(单次, 剩余)`)。
   算账写在常量文档里:17 772 300 B / 360 s ≈ 49 KB/s,比 r1 实测最慢那条 IPv6(67~77 KB/s)
   还低一档 ⇒ 慢而能下完的线路仍有一次成功机会;健康线路(1.1~16.6 s)在第一/第二个候选就结束。
   代价(最坏 6 分钟)也是刻意取舍,写在同一处。

顺手:`i18n::CjkFont::source` 的「尚未钉死」改成指向 `Source::Pinned`;`clock.rs` 与
`general_section.rs` 注释里的 17.0 MB → 16.9 MB。

验证(`CARGO_TARGET_DIR=C:/Temp/ull-target/wt-168`,全量重编;取退出码,不截断):
- `cargo test -p unlucky-core --all-targets` → **16 个 target / 1189 passed / 0 failed / exit 0**
  (lib 625 + setting 115 + 其余 14 个)
- `cargo test -p unlucky-i18n` → 31 passed / 0 failed / exit 0
- `cargo test -p unlucky-app-iced`(含 `no_hardcoded_tokens` integration)→ **693 passed / 0 failed / exit 0**
- `cargo clippy -p unlucky-i18n -p unlucky-core -p unlucky-app-iced --all-targets -- -D warnings` → exit 0
- `cargo fmt --all -- --check` 干净;全仓受检文件 ≤400(本分支最大 361)
2026-11-08 14:38:25 +08:00
AskaEth 449ded2cb7 feat(m168): 中文字体 CDN 下载真接线(D28 后半)
M167 调研的结论是「中文字体 CDN 下载至今未实装」:`i18n::font` 只有 `Fetcher` trait 与
假实现,`needs-download` 分支什么也不做。这条把它接成一条真链路。

判据 → 下载 → 注册(一条链,四个 crate 各管一段):

* `unlucky_i18n::font`(零依赖):`Source::Pinned { urls, sha256, bytes }` 把「下什么」
  钉死——google/fonts 的 `NotoSansSC[wght].ttf` 钉到 commit `2894aab…`(不是 `@main`),
  17 772 300 B / sha256 `a3041811…`。可变字体一份覆盖 wght 100–900,省掉三份静态子集的 25 MiB。
  `FetchError` 补第三档 `Disk`(网络 / 校验 / 磁盘,UI 按类别选词)。
* `unlucky_core::download::font`(新):缓存命中 → 共享引擎 `download_bytes` → 复验(sha256 +
  sfnt 魔数 + 32 MiB 上限)→ 原子落盘。**复验在写盘之前**,所以「哈希不符」时盘上什么都没有;
  缓存按 `<sha256 前 16 位>` 命名,pin 一换自动换名。
* `unlucky_app_iced::fonts::{cdn,install}`(新):`CdnFetcher` 把 pin 接进 core(`ui.font_cdn`
  可覆盖 URL,哈希不换);`install` 是状态机 + 后台线程,只有 `NeedsDownload` 才起线程
  (英文 / 系统已有 / 探测不出一律零网络)。注册走 `iced_graphics::text::FontSystem::load_font`,
  族名从字体库读回来核对。失败只降级(继续用系统字体),不阻塞、不崩。
* 接入两条路径:`fonts::resolve_with`(启动)与 `fonts::cjk_for`(设置页切语言)都调
  `install::after_probe`;设置页 General 组多一行状态(下载中 / 完成 / 失败 / 无需下载 + 三类失败
  原因),文案进 catalog 金表(链尾追加,既有顺序不动);`App::advance` 每秒比对状态快照后重建
  视图模型,所以那一行会自己刷新。

配置:新增 `ui.font_cdn`(`keys/font.rs`,schema 1.7.0),空 = 内置 jsDelivr 镜像(含备用边缘)。

守卫(三条都能变红,变异证明见报告):core 侧 8 条(缓存不联网 / 哈希不符拒落盘 / HTML 不当字体
/ 坏缓存自愈 / 上限 / pin 校验),app 侧 6 条(三种闲置策略零调用、失败不注册、类别不串、幂等、
截图钉子、单行日志),页面侧 1 条(四态 × 两语言无缺译、不写假百分比)。

测试:`cargo test -p unlucky-i18n --lib` 31 绿;`-p unlucky-core --lib` 622 绿;
`-p unlucky-app-iced --lib` 684 绿;`cargo fmt --check` 干净。截图三张见报告。
2026-11-08 14:38:23 +08:00
AskaEth 89dcb6bdf3 feat(app-iced,i18n): M106 —— 界面跟随系统语言 + 设置页真实语言行
用户原话:「我系统语言是中文 为什么打开软件是英文 并且设置页里面显示的是 English(system)」。
两处根源:①M92 的探测链一直是对的,但 `pages::Model.lang` 在 `app/view.rs` 里写死成
`Lang::En`(`pages::render` 还把下载页写死成英文),探测结果没人用;②设置页语言行是硬编码
字面量 `("Language", "English (System)")`。

新增/改动(按层):
- i18n:新域 `catalog/general.rs`(24 条中英双份 key,装配链尾追加)+ `catalog/golden.rs`
  只追加不重排;`choice.rs`(`LangChoice`:跟随系统/手选 + D28 四级优先级真值表 + 字体优先级真值表)。
- app-iced 语言入口 `src/ui.rs` + `src/ui/lang.rs`:`LangState` = 用户档位 + 探测结论;
  启动判定 `ULL_LANG` > 配置 `ui.language` > 系统 locale > 英文;两行日志(探测/生效 + 中文策略)。
- `app/state.rs`:`App` 持有 `LangState`,切语言 = 换目录 + 落盘 + 重建视图模型;
  `app/view.rs` 的 `model()` 收 `lang`,不再写死英文。
- 设置页:语言行换真控件(跟随系统 / 简体中文 / 英文,回显「界面当前使用:简体中文」);
  页头、左列分组名、逐实例空态、导航轨标签全部走 catalog。
- presenter:`settings_bridge_lang.rs`(`load_ui_language` / `persist_ui_language`)。
  「跟随系统」= **删键**(schema 允许值只有 zh_cn/en_us),读的是 `doc().get()` 而不是
  `effective_str`(后者会把默认值 `zh_cn` 当成用户手选,英文机器一启动就被切成中文)。

验证:408 条 app-iced lib 测试 + 28 条 i18n 测试 + 全仓 `cargo test --workspace --exclude
unlucky-app` 全绿;fmt / clippy `-D warnings` 干净;出图 6 张(en/zh × 浅/深 + 下载页对照)
在 `DevDocs/artifacts/m106/`(该目录被 .gitignore 忽略,按仓内惯例作为本地验收产物)。
2026-11-04 22:50:09 +08:00
AskaEth 029ece3f17 feat(app-iced): M101 r2 设置页状态漏斗 + pages/mod.rs 拆分 + r1 复审订正
r1 复审的 P1:消息漏斗只解决了「加一支消息」,一组要画真值仍要改 `settings/mod.rs`
(`Group.body` 与 `view` 只认 `appearance::State`)。本 commit 补齐与消息漏斗同构的状态漏斗:

- 新增 `pages/settings/state.rs`:`Groups`(一个组一个字段,字段类型在组自己文件里)+
  `States`(按值传给渲染器的只读视图:外观状态 + 组状态)+ `demo_groups()`(harness 的
  `'static` 默认状态)。
- `Group.body` 签名改成 `for<'a> fn(&'a Ui, States<'a>) -> Option<Element>`:每组自己从
  `States` 取自己要的数据,「没有可画的状态」返回 `None` 由 `form` 退回演示行。
  `appearance` 组照旧接 `pages/appearance/section.rs`(M93 路径行为不变)。
- `App.settings: pages::settings::Groups`(`app/state.rs` 字段列表末尾)+ `app/view.rs` 构造
  `States` 走新入口 `pages::page_with_settings`。加/改某一组的字段只改那一组的文件。
- 四个组各自的 `State` 落在各自文件;`java::Message` 留一支只有测试构造的 `Ping`,配
  `app/update/settings/tests.rs` 的 `a_group_message_reaches_its_handler_through_the_funnel`
  ——消息漏斗第一次有**运行时**证据。

r1 其余条目:
- P2-1 删掉 `only_the_appearance_group_carries_a_real_control_body`(被四条 per-group 断言蕴含),
  并在同处写明删掉之后「唯一性」没人守;三份 section 文档从「改 3 个文件」订正为 2 个。
- P2-2 `pub mod settings` → `pub(crate) mod settings`(四个消息口模块 `pub(crate)`),
  并订正两处理由(lint 按类型自身可见性判,不是可达性)。
- P2-3 `pages/mod.rs` 397 行 → 拆出 `pages/message.rs` / `pages/screen.rs`(纯搬移,`pub use`
  保证 `pages::Message` / `Action` / `Screen` 路径不变),并删掉 M101 自己那 18 行重复文档。
- P3-1 订正共享点清单(`Key` 由宏从域文件累积生成,加 key 不改 `catalog.rs`)。
- P3-2 守门改成「除已知臂外任何含 `=>` 的行都算兜底」(`other =>` 也拦),并加「恰好四臂」断言。
- P3-3 `ai.rs` → `ai_about.rs` 单独一条 R100 改名提交,`git log --follow` 不断链。
- P3-4 `message.rs` 的「加一支消息改 1 个文件」订正为 2 个。
2026-11-03 20:57:08 +08:00
AskaEth 380fac0324 fix(app-iced): M100 r1 返工 —— 一次一采样 + OS 层密闭指标 + 文档口径订正
三条复审意见 + 六条 p3,全部落在原有文件里(另加两个拆分文件)。

P2-1 截图落盘「最后写入者说了算」(真竞态)
  `Message::Frame` 以前在 `frames >= CAPTURE_AT_FRAME` 之后每帧都发一次 `window::screenshot`,
  `Captured` 每次都覆盖同一文件才 `exit()`;已派发的采样不会因 exit 撤回 ⇒ 落盘内容是调度结果
  (复审实测 12 次里 7 次写了两次;把窗口在两次采样之间改成 900×600,落盘就是 900×600)。
  现在闸门 = `should_request_capture(frames, dispatched, capture_mode)`,派发即置位、只开一次;
  `Captured` 计数落盘次数并打进日志。实测 12/12 次「[shot] -> 文件」行恒为 1、`落盘次数=1`。

P2-2 密闭证据换成 OS 层
  `platform/win32/transparency.rs` 加安全 getter `click_through`(unsafe 仍只在既有唯一落点);
  该文件因超 400 行又拆出 `transparency/exstyle.rs`(子模块继承父模块的 `#![allow(unsafe_code)]`,
  `unsafe-scope` 门禁判据不受影响)。日志三态 `[caps] passthrough_ext=on|off|unknown`:
  `None` 记 `unknown`,绝不记 `off`。实测默认 on 12/12、`--passthrough=off` 为 off 3/3。

P2-3 头注释「下放」→「新增」,并订正「接真控件改 3 个文件」
  (section 文件里的 body 绊线 + 该 section 的 `GROUP` 文件 + `tests/mod.rs` 跨 section 绊线)。

p3 六条:shell 断言说明改准(默认值那条归 cli 的测试)、harness 文档给「指针事件=0」加对照限定、
数字订正(纯搬移 234 / 新增 134)、三文件口径、ai+about 对本文件的两处必改、`Rest` 与真 Disabled
撞语义写进「抓不住」。

文档口径:「8 连跑全稳」不再当密闭证据——同 argv 开/关穿透 hash 相同(实测 0d7b4f5b 两边一样),
它只说明「帧静止 + 落盘确定」。密闭主证据是 `[caps] passthrough_ext=`。

顺带:`run()` 从 `harness.rs` 拆到 `harness/app.rs`(272 / 190 行)。搬移中丢过一条既有断言
`ensure_layered_refuses_a_missing_or_invalid_window`,被编译器 unused-import 警告抓出并复原。
2026-11-03 15:22:55 +08:00
AskaEth 2061296615 feat(app-iced,pages): M100 取证密闭 + harness 参数拆分 + 设置页测试按 section 下放
四件前置事,都是为了设置页正文四条 section 线能并行(不写任何 section 正文):

1. 取证密闭(两条通道各有各的机制,证据分开算)
   - 产品 bin:`cli::ForcedState` 加 `Rest`(静息态),给了 `--screenshot` 且没显式
     `--force-state` 时默认 `Some(Rest)`;交互模式仍是 `None`(`shell::overridden`
     与 `app/view.rs` 的 `model()` 各补一支)。
   - harness:截图窗口开 `window::enable_mouse_passthrough`(winit 的
     `set_cursor_hittest(false)`)⇒ 窗口收不到指针事件 ⇒ 全树控件恒为 Active。
     为什么用通道级而不是逐控件灌 forced:设置页「外观」区的滑块/文本框/裸 button 都不消费
     forced,逐控件做要改十几个组件文件,而新产品组件随时会漏。
   - harness 同时数指针事件并打进日志(`指针事件=N`):让「这张图见过几个鼠标事件」变成
     可读数字,替代「连跑几次 hash 一样」那种恒真的假绿。

2. harness 的外观真状态入口:`--appearance=real` 让设置页走 `appearance: Some(..)`
   (工厂态 + `ULL_APPEARANCE_*`),新入口 `pages::app_with_appearance`。
   `--screen=settings` 之前就有了,但 `appearance` 恒为 None,D30 要的那一屏根本不存在。

3. `pages/harness.rs` 400 行 → 拆 `pages/harness/request.rs`(`#[path]` 范式),
   纯搬移部分 240 行 + 新增密闭代码。拆分等价性:同一 argv 的 PNG sha256 与拆分前一致。

4. `pages/settings/tests.rs` 按 section 下放成 `tests/{general,appearance,java,download}_section.rs`,
   mod 层只留跨 section 断言(每条写清守什么、为什么不能下放)。
   给某组加一条新断言 = 只改它自己那个文件。

顺带:`shell/mod.rs` 在 HEAD 就 399 行、`cli.rs` 394 行,两处都按同一范式把 tests 搬成
`shell/tests.rs` / `cli/tests.rs`(断言一字不动,逐字节可 diff)。

变异验证:把四处实现各改坏一处(默认 Rest、passthrough 默认值、overridden 的 Rest 分支、
view 的 Rest 映射),对应 4 条断言全红(390 passed / 4 failed),恢复后 394 passed。
2026-11-03 14:22:40 +08:00