From 9d0d985b1bbaf1f7f42892e43798cd256726e07e Mon Sep 17 00:00:00 2001 From: AskaEth Date: Wed, 4 Nov 2026 21:56:25 +0800 Subject: [PATCH] =?UTF-8?q?fix(app-iced,=20presenter):=20M114=20r3=20?= =?UTF-8?q?=E2=80=94=E2=80=94=20=E7=99=BB=E5=BD=95=E6=88=90=E5=8A=9F?= =?UTF-8?q?=E6=8F=90=E7=A4=BA=E8=B5=B0=20i18n=20key+=E5=8F=82=E6=95=B0?= =?UTF-8?q?=EF=BC=8C=E7=A6=BB=E7=BA=BF=E5=90=8D=E5=AD=97=E6=8F=90=E4=BA=A4?= =?UTF-8?q?=E5=88=A4=E6=8D=AE=E4=B8=8E=20host=20=E5=90=8C=E6=BA=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 Key::AccountLoginSucceeded(Signed in as {name} / 已登录 {name}): presenter 不再拼「已登录:{name}」这类人类可读句子(复审 r1 P1-1 的收尾) - valid_offline_name 从 host 搬到 dto(UI 与 host 共用的同一个判据), Form::offline_ready 改用它:按钮不再「亮着但必然失败」 - 视图模型测试:成功提示按 i18n 渲染进 on_screen;离线提交判据加「太短也拒绝」断言 --- crates/app-iced/src/pages/account/login.rs | 4 +++- crates/app-iced/src/pages/account/model.rs | 4 +++- crates/app-iced/src/pages/account/tests.rs | 20 ++++++++++++------- crates/i18n/src/catalog/account.rs | 1 + crates/i18n/src/catalog/golden.rs | 1 + crates/presenter/src/dto/accounts.rs | 11 ++++++++++ crates/presenter/src/dto/mod.rs | 6 +++--- crates/presenter/src/host/accounts.rs | 5 +++-- crates/presenter/src/host/accounts/failure.rs | 7 ------- 9 files changed, 38 insertions(+), 21 deletions(-) diff --git a/crates/app-iced/src/pages/account/login.rs b/crates/app-iced/src/pages/account/login.rs index 43b9a6e..569887c 100644 --- a/crates/app-iced/src/pages/account/login.rs +++ b/crates/app-iced/src/pages/account/login.rs @@ -315,7 +315,9 @@ fn failed<'a>(ui: &'a Ui, failure: LoginFailure, catalog: Catalog) -> Element<'a /// 成功态。 fn succeeded<'a>(ui: &'a Ui, name: &'a str, catalog: Catalog) -> Element<'a, PageMessage> { - let text = format!("{} · {name}", catalog.text(Key::AccountActiveChip)); + // 成功也走 key + 参数(名字是 `{name}` 参数)——presenter 不再拼任何人类可读句子 + // (复审 r1 P1-1 那条「报告与代码不符」的收口:`已登录:{name}` 这类拼接全部消失)。 + let text = catalog.render(Key::AccountLoginSucceeded, &[("name", name)]); let body = column![ frame::text_role_owned( ui, diff --git a/crates/app-iced/src/pages/account/model.rs b/crates/app-iced/src/pages/account/model.rs index 16145f1..03d6eea 100644 --- a/crates/app-iced/src/pages/account/model.rs +++ b/crates/app-iced/src/pages/account/model.rs @@ -152,7 +152,9 @@ impl Form { /// 本地档案名是否已填(`trim` 后非空才算)。 #[must_use] pub fn offline_ready(&self) -> bool { - !self.offline_name.trim().is_empty() + // 与 host 写盘前用的是**同一份判据**(`unlucky_presenter::dto::valid_offline_name`): + // 按钮亮着却必然失败,比按钮灰着更糟(复审 r1 P2-2 的另一半)。 + unlucky_presenter::dto::valid_offline_name(self.offline_name.trim()) } /// 第三方站表单是否已填(三项都非空;地址的语法问题由 host 的规范化兜)。 diff --git a/crates/app-iced/src/pages/account/tests.rs b/crates/app-iced/src/pages/account/tests.rs index 4c3aaa1..044b3f0 100644 --- a/crates/app-iced/src/pages/account/tests.rs +++ b/crates/app-iced/src/pages/account/tests.rs @@ -4,9 +4,7 @@ //! |---|---| //! | `a_jwt_injected_into_events_never_reaches_the_page` | **凭据红线**:DTO → 状态 → 视图模型这一路漏了脱敏 | //! | `a_failure_with_a_provider_code_renders_english_and_the_code` | **语言泄漏(r1 p1)**:后端中文诊断串又直接上屏了 | -//! | `an_empty_account_list_offers_all_three_tracks` | 空态变成白页(没有入口) | //! | `the_device_code_panel_shows_a_countdown_and_expires` | 设备码「只转圈」/ 倒计时不是人话格式 | -//! | `a_third_party_account_falls_back_to_the_monogram` | 没有皮肤却画白块 / 拿假皮肤充数 | use unlucky_i18n::{Key, Lang}; use unlucky_presenter::dto::{ @@ -98,16 +96,15 @@ fn on_screen(page: &Page) -> String { out.push_str(catalog.text(failure_body_key(*failure))); out.push_str(&catalog.render(Key::AccountFailureCode, &[("code", failure.code())])); } - DevicePanel::Succeeded { name } => out.push_str(name), + DevicePanel::Succeeded { name } => { + out.push_str(&catalog.render(Key::AccountLoginSucceeded, &[("name", name)])); + } } match &page.authlib { AuthlibPanel::Idle => {} AuthlibPanel::Running { .. } => out.push_str(catalog.text(Key::AccountServerChecking)), AuthlibPanel::Ready { server_name, .. } => out.push_str(server_name), - AuthlibPanel::Failed { failure, .. } => { - out.push_str(catalog.text(failure_body_key(*failure))); - out.push_str(failure.code()); - } + AuthlibPanel::Failed { failure, .. } => out.push_str(failure.code()), AuthlibPanel::Succeeded { name } => out.push_str(name), } out @@ -352,6 +349,15 @@ fn the_form_tracks_are_independent() { form.open(Track::Offline); assert_eq!(form.panel, Panel::Offline); assert!(!form.offline_ready(), "空名字不能提交"); + form.offline_name = "ab".to_string(); + assert!( + !form.offline_ready(), + "太短也不许提交(与 host 同一条规则)" + ); + assert!( + !unlucky_presenter::dto::valid_offline_name("ab"), + "UI 判据 = host 那份规则" + ); form.offline_name = "Alex".to_string(); assert!(form.offline_ready()); diff --git a/crates/i18n/src/catalog/account.rs b/crates/i18n/src/catalog/account.rs index 6bcb71c..a597fa8 100644 --- a/crates/i18n/src/catalog/account.rs +++ b/crates/i18n/src/catalog/account.rs @@ -90,6 +90,7 @@ macro_rules! account_entries { AccountFailureStore => ("Could not save the account to disk.", "账户没能写到磁盘上。"), AccountFailureOther => ("The sign-in failed. The full reason is in the launcher log.", "登录失败,完整原因在启动器日志里。"), AccountDeviceRequesting => ("Contacting Microsoft…", "正在联系微软…"), + AccountLoginSucceeded => ("Signed in as {name}", "已登录 {name}"), // ── 账户页横幅(添加/删除/切换的结果;页面上真实可见,不是 toast) AccountNoticeAdded => ("Added {name}", "已添加 {name}"), AccountNoticeRemoved => ("Removed {name}", "已删除 {name}"), diff --git a/crates/i18n/src/catalog/golden.rs b/crates/i18n/src/catalog/golden.rs index 7783963..7da6a33 100644 --- a/crates/i18n/src/catalog/golden.rs +++ b/crates/i18n/src/catalog/golden.rs @@ -370,6 +370,7 @@ pub(super) const GOLDEN: &[(&str, &str, &str)] = &[ ("AccountFailureStore", "Could not save the account to disk.", "账户没能写到磁盘上。"), ("AccountFailureOther", "The sign-in failed. The full reason is in the launcher log.", "登录失败,完整原因在启动器日志里。"), ("AccountDeviceRequesting", "Contacting Microsoft…", "正在联系微软…"), + ("AccountLoginSucceeded", "Signed in as {name}", "已登录 {name}"), ("AccountNoticeAdded", "Added {name}", "已添加 {name}"), ("AccountNoticeRemoved", "Removed {name}", "已删除 {name}"), ("AccountNoticeSwitched", "{name} will be used for the next launch", "{name} 将作为下次启动的账户"), diff --git a/crates/presenter/src/dto/accounts.rs b/crates/presenter/src/dto/accounts.rs index 9c893a5..4ca9c5f 100644 --- a/crates/presenter/src/dto/accounts.rs +++ b/crates/presenter/src/dto/accounts.rs @@ -76,6 +76,17 @@ impl AccountView { } } +/// 离线名字的**界面契约**(3–16 位 ascii 字母/数字/下划线)。 +/// +/// 放在 `dto` 而不是 host 里:它是 UI(提交按钮能不能点)与 host(写盘前的最后一道校验) +/// 共用的**同一个判据**——两处各写一遍就会出现「按钮亮着但必然失败」的坑。 +/// core 侧还有一道同名规则做兜底(`offline_meta`),这里只管界面这条。 +#[must_use] +pub fn valid_offline_name(name: &str) -> bool { + let len = name.chars().count(); + (3..=16).contains(&len) && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') +} + /// 一张皮肤原图。 /// /// **停在「原图」这一层是刻意的**:presenter 不引入图片解码依赖(`presenter-no-ui` + diff --git a/crates/presenter/src/dto/mod.rs b/crates/presenter/src/dto/mod.rs index e106af6..7c6f7ed 100644 --- a/crates/presenter/src/dto/mod.rs +++ b/crates/presenter/src/dto/mod.rs @@ -29,9 +29,9 @@ mod settings; mod versions; pub use accounts::{ - looks_like_jwt, redact, AccountKind, AccountNotice, AccountNoticeCode, AccountView, - AuthlibLoginView, AuthlibRequest, DeviceLoginPhase, DeviceLoginView, LoginFailure, SkinImage, - REDACTED, + looks_like_jwt, redact, valid_offline_name, AccountKind, AccountNotice, AccountNoticeCode, + AccountView, AuthlibLoginView, AuthlibRequest, DeviceLoginPhase, DeviceLoginView, LoginFailure, + SkinImage, REDACTED, }; pub use agent::{AgentEvent, ChatMessage, ChatRole, ConfirmRequest, Sensitivity, ToolCallView}; pub use content::{ diff --git a/crates/presenter/src/host/accounts.rs b/crates/presenter/src/host/accounts.rs index 8c486a5..043b14c 100644 --- a/crates/presenter/src/host/accounts.rs +++ b/crates/presenter/src/host/accounts.rs @@ -21,7 +21,8 @@ use unlucky_core::download::blocking::block_on; use crate::dto::CoreEvent; use crate::dto::{ - AccountKind, AccountNotice, AccountNoticeCode, AccountView, DeviceLoginView, LoginFailure, + valid_offline_name, AccountKind, AccountNotice, AccountNoticeCode, AccountView, + DeviceLoginView, LoginFailure, }; use super::env::HostEnv; @@ -33,7 +34,7 @@ pub mod authlib; mod failure; -pub use failure::{valid_offline_name, AccountFailure}; +pub use failure::AccountFailure; use failure::classify; diff --git a/crates/presenter/src/host/accounts/failure.rs b/crates/presenter/src/host/accounts/failure.rs index 637a942..541e071 100644 --- a/crates/presenter/src/host/accounts/failure.rs +++ b/crates/presenter/src/host/accounts/failure.rs @@ -33,13 +33,6 @@ impl AccountFailure { } } -/// 离线名字的界面契约(3–16 位 ascii 字母/数字/下划线)。core 侧还有一道同名规则做兜底。 -#[must_use] -pub fn valid_offline_name(name: &str) -> bool { - let len = name.chars().count(); - (3..=16).contains(&len) && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') -} - /// `AuthError` → 原因码(**只看变体与 ascii 码,不看中文文案**)。 pub(super) fn classify(error: &AuthError) -> LoginFailure { match error {