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 写死成便携落点变红。
This commit is contained in:
worker-logplace committed 2026-11-09 00:43:45 +08:00
1 parent efc663a3cb
commit 5fd5cd47dc
47 files changed
+1371 -119

No files matched your search

@@ -196,3 +196,48 @@ fn the_general_group_does_not_use_the_appearance_body_slot() {
"General 的真控件走 pages::Language 入口(见 settings/mod.rs 的 GENERAL_GROUP),不是 body"
);
}
/// **M188:日志文件夹那一行**的文案必须与实现一致(用户投诉「找不到崩溃日志」的直接判据)。
///
/// 抓得住:①文案里的路径写错(写成 `data/logs`、`cache/game`);②滚动上限与
/// `core::launch::launcher_log::MAX_BYTES` 脱钩(文案说 1 MiB、实现改成 4 MiB);
/// ③旧目录被静默改成「已迁移」(那会让用户以为旧日志没了);
/// ④某条 key 只进了英文表(中文界面上是 `⟪missing⟫`)。
/// 抓不住:界面画出来长什么样(截图验收管那一层)。
#[test]
fn the_logs_row_names_the_real_paths_and_the_real_limit() {
use unlucky_core::launch::launcher_log;
use unlucky_core::platform::layout::{LauncherLayout, GAME_LOG_DIR, LOGS_DIR};
// 实现侧的真值:分区名与上限都只从 core 来。
assert_eq!(LOGS_DIR, "logs");
assert_eq!(GAME_LOG_DIR, "game");
assert_eq!(launcher_log::MAX_BYTES, 1024 * 1024, "文案说 1 MiB");
let layout = LauncherLayout::new(std::path::Path::new("/tmp/ull-home"));
assert_eq!(
layout.game_logs(),
std::path::Path::new("/tmp/ull-home/logs/game")
);
for lang in Lang::ALL {
let catalog = lang.catalog();
let hint = catalog.text(Key::SettingsLogsFolderHint);
for needle in [
"launcher.log",
"1 MiB",
&format!("{LOGS_DIR}/{GAME_LOG_DIR}/"),
"data/agent/sessions",
] {
assert!(hint.contains(needle), "{} 缺 `{needle}`:{hint}", lang.tag());
}
assert!(!hint.contains('⟪'), "{} 目录里缺词:{hint}", lang.tag());
assert!(
!catalog.text(Key::SettingsLogsFolder).contains('⟪'),
"{} 缺「日志文件夹」:{}",
lang.tag(),
catalog.text(Key::SettingsLogsFolder)
);
assert!(!catalog.text(Key::SettingsLogsCopy).contains('⟪'));
assert!(!catalog.text(Key::SettingsLogsUnknown).contains('⟪'));
}
}