fix(presenter,app-iced,i18n): M132 缺 MSA client id 上屏成「未配置 + 怎么补」

缺 client id 原来整条链路只给 `LoginFailure::Other`:界面画「Sign-in failed /
完整原因在启动器日志里 / Code: other」,用户得翻日志才知道要设 `ULL_MSA_CLIENT_ID`。

落点定在**账户页登录面板**,不是启动流程:这个失败只发生在 `microsoft_auth()`(两条
登录流的入口);没配 id 的发布版建不出微软账户,`identity::resolve` 那条启动路径用户
撞不到(只有「有 id 的构建里登录过、之后 id 被摘掉」才可达)。

- presenter:新增 `LoginFailure::MsaNotConfigured`(`msa_not_configured`)。`microsoft_auth`
  改为回 `AuthResult`,`AuthError::Unsupported`(`from_env` 唯一的产出点,零网络)单独成码,
  其余失败照旧走 `classify`。诊断原文仍只进日志。
- i18n:账户域新增 `AccountLoginNotConfigured`(标题)/ `AccountFailureMsaNotConfigured`
  (正文:设 `ULL_MSA_CLIENT_ID=<id>` 或构建时注入,改完重启),golden 同步。
- app-iced:`failure_title_key` 给这一类专属标题;正文按码查表;这一类**不给「重试」**
  (进程内的环境变量改不了,重试必然是同一个错)。
- 测试:`a_missing_client_id_tells_the_user_what_to_configure` 断言 UI 模型里出现「未配置」
  + 变量名 + `<id>` 占位 + `msa_not_configured`,且没有那句「去翻日志」;反向锚
  `a_network_failure_does_not_look_like_a_missing_config` 与
  `only_a_missing_client_id_becomes_the_config_gap_code` 钉住别的失败不许跟着变。

为什么这一类可以上屏:上屏的是 catalog 固定文案(无凭据、无 token、无内部细节),
`DevicePanel::Failed` 里没有 `message`/`detail` 字段,原文在类型上就画不出来;
理由是它的原因本身就是用户的下一步。M117/M119 的「诊断不上屏」约定不动。
This commit is contained in:
AskaEth committed 2026-11-05 20:08:52 +08:00
1 parent 01ca8033be
commit af39efd23b
7 files changed
+223 -52

No files matched your search

+69
View File
@@ -89,6 +89,15 @@ macro_rules! account_entries {
AccountFailureNoProfile => ("The account has no Minecraft profile on this server yet. Create the character on the server first.", "这个账号在该服务器上还没有角色:先在皮肤站里创建角色。"),
AccountFailureStore => ("Could not save the account to disk.", "账户没能写到磁盘上。"),
AccountFailureOther => ("The sign-in failed. The full reason is in the launcher log.", "登录失败,完整原因在启动器日志里。"),
// M132:缺 client id 是**配置项缺失**,原因就是用户能执行的下一步,所以这一条
// 必须自己说清「缺什么、怎么补」,不许退回上面那句「完整原因在日志里」。
// 纪律:只写环境变量名与 `<id>` 占位,**不许出现任何真实 client id**;
// 也不许写成「未支持 / unsupported」——登录链路是完整的。
AccountLoginNotConfigured => ("Microsoft sign-in is not configured", "微软登录未配置"),
AccountFailureMsaNotConfigured => (
"This build has no Azure public client id, so Microsoft sign-in cannot start (the sign-in flow itself is implemented). Set the environment variable ULL_MSA_CLIENT_ID=<id> before starting the launcher, or build with ULL_MSA_CLIENT_ID=<id>. Then restart the launcher: a running process does not pick up a new environment variable.",
"本启动器没有配置 Azure 公共客户端 id,微软登录无法开始(登录功能已实现,只差这一项配置)。启动前设置环境变量 ULL_MSA_CLIENT_ID=<id>,或构建时注入:ULL_MSA_CLIENT_ID=<id> cargo build。改完请重启启动器——已在运行的进程读不到新设的环境变量。"
),
AccountDeviceRequesting => ("Contacting Microsoft…", "正在联系微软…"),
AccountLoginSucceeded => ("Signed in as {name}", "已登录 {name}"),
// ── 账户页横幅(添加/删除/切换的结果;页面上真实可见,不是 toast)
@@ -111,3 +120,63 @@ macro_rules! account_entries {
}
pub(crate) use account_entries;
#[cfg(test)]
mod tests {
use crate::{Catalog, Key, Lang};
/// **M132**:缺 client id 这条是本页唯一「说清原因」的失败文案,它的纪律比别的正文更硬。
///
/// 抓得住:文案退回「登录失败 / 完整原因在日志里」、退化成「未支持」、或者只说缺 id 却
/// 不给要设的变量名;也能抓住有人把某个真实 client id 写进词条。
/// 抓不住:读起来顺不顺(那靠 review 与截图)。
#[test]
fn the_missing_client_id_copy_says_what_to_configure_in_both_languages() {
for &lang in Lang::ALL {
let catalog = Catalog::of(lang);
let title = catalog.text(Key::AccountLoginNotConfigured);
let body = catalog.text(Key::AccountFailureMsaNotConfigured);
let all = format!("{title}\n{body}");
assert!(body.contains("ULL_MSA_CLIENT_ID"), "{}: {all}", lang.tag());
assert!(
body.contains("<id>"),
"示例只许用占位:{}: {all}",
lang.tag()
);
// 面板上是「标题 + 正文」一起:结论(未配置)给标题,怎么做给正文。
assert!(
all.to_lowercase().contains("not configured") || all.contains("未配置"),
"要说清「未配置」:{}: {all}",
lang.tag()
);
assert!(!body.to_lowercase().contains("unsupported"), "{all}");
assert!(
!body.contains("未支持") && !body.contains("未实现"),
"{all}"
);
// 不许退回「完整原因在日志里」那句(那正是用户报的 bug)
assert!(!body.contains("launcher log"), "{all}");
assert!(!body.contains("启动器日志"), "{all}");
// 硬约束:`ULL_MSA_CLIENT_ID=` 后面只许跟 `<id>` 占位——写进来一个真 id 就红。
assert_eq!(
body.matches("ULL_MSA_CLIENT_ID=").count(),
body.matches("ULL_MSA_CLIENT_ID=<id>").count(),
"示例里出现了真 id:{all}"
);
}
for &lang in Lang::ALL {
let catalog = Catalog::of(lang);
let title = catalog.text(Key::AccountLoginNotConfigured);
assert!(
title.to_lowercase().contains("not configured") || title.contains("未配置"),
"{}: {title}",
lang.tag()
);
assert!(
!title.contains('=') && !title.contains("ULL_MSA_CLIENT_ID"),
"标题只报结论,变量名在正文里:{title}"
);
}
}
}
+4 -4
View File
@@ -1,7 +1,5 @@
//! 金表的**表数据**(`Key::ALL` 顺序 × (en, zh-CN) 的逐字快照)。「为什么有这张表、它抓得住
//! 什么」全在 `catalog.rs` 的 `mod golden;` 文档里——本文件只放数据,是因为每加一条 key 就多一行。
/// 拆分前 `Key::ALL` 的顺序 × (en, zh-CN)。第一列是变体名(`Key::name()` 的返回值)。
//! 金表的**表数据**(`Key::ALL` 顺序 × (en, zh-CN) 逐字快照,第一列是 `Key::name()`)。「为什么
//! 有这张表、它抓得住什么」全在 `catalog.rs` 的 `mod golden;` 里;本文件只放数据。
pub(super) const GOLDEN: &[(&str, &str, &str)] = &[
("NavInstances", "Instances", "实例"),
("NavDownload", "Versions", "版本管理"),
@@ -381,6 +379,8 @@ pub(super) const GOLDEN: &[(&str, &str, &str)] = &[
("AccountFailureNoProfile", "The account has no Minecraft profile on this server yet. Create the character on the server first.", "这个账号在该服务器上还没有角色:先在皮肤站里创建角色。"),
("AccountFailureStore", "Could not save the account to disk.", "账户没能写到磁盘上。"),
("AccountFailureOther", "The sign-in failed. The full reason is in the launcher log.", "登录失败,完整原因在启动器日志里。"),
("AccountLoginNotConfigured", "Microsoft sign-in is not configured", "微软登录未配置"),
("AccountFailureMsaNotConfigured", "This build has no Azure public client id, so Microsoft sign-in cannot start (the sign-in flow itself is implemented). Set the environment variable ULL_MSA_CLIENT_ID=<id> before starting the launcher, or build with ULL_MSA_CLIENT_ID=<id>. Then restart the launcher: a running process does not pick up a new environment variable.", "本启动器没有配置 Azure 公共客户端 id,微软登录无法开始(登录功能已实现,只差这一项配置)。启动前设置环境变量 ULL_MSA_CLIENT_ID=<id>,或构建时注入:ULL_MSA_CLIENT_ID=<id> cargo build。改完请重启启动器——已在运行的进程读不到新设的环境变量。"),
("AccountDeviceRequesting", "Contacting Microsoft…", "正在联系微软…"),
("AccountLoginSucceeded", "Signed in as {name}", "已登录 {name}"),
("AccountNoticeAdded", "Added {name}", "已添加 {name}"),