Unlucky Launcher v0.2.0
CI / runtime smoke (macos arm64, probe) (push) Skipped
CI / fmt (rustfmt --check) (push) Canceled after 0s
CI / clippy (ubuntu-latest) (push) Canceled after 0s
CI / clippy (windows-latest) (push) Canceled after 0s
CI / test (ubuntu-latest) (push) Canceled after 0s
CI / test (windows-latest) (push) Canceled after 0s
CI / build release (ubuntu-latest) (push) Canceled after 0s
CI / build release (windows-latest) (push) Canceled after 0s
CI / runtime smoke (ubuntu x64, xvfb) (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Guards / presenter 零 UI 守门 (push) Canceled after 0s
Guards / design 零第三方依赖守门 (push) Canceled after 0s
Guards / unsafe 落点守门(集合必须恰好一个文件) (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s

首个 iced + Material 3 Expressive 版本的源码快照。
完整历史与日常提交在 dev 分支;main 只放阶段性快照。
This commit is contained in:
AskaEth committed 2026-11-05 14:22:11 +08:00
commit 8a8e5ca0c0
1079 files changed
+208035

No files matched your search

@@ -0,0 +1,126 @@
//! 账户写操作的失败分类 + 离线名字契约(M114 r2)。
//!
//! 分类**只看 `AuthError` 的变体与 ascii 码**,不看中文文案——文案随时会改,
//! 而页面据 `AccountNoticeCode` 选 i18n,两边必须能独立演进。
use unlucky_core::auth::AuthError;
use unlucky_core::auth::BodyKind;
use crate::dto::{AccountNoticeCode, LoginFailure};
/// 账户写操作的失败:**结构化码 + 原文**(原文只进日志/详细信息,不上屏)。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AccountFailure {
/// 结果码(页面据此选 i18n)
pub code: AccountNoticeCode,
/// 原始诊断(已脱敏)
pub detail: String,
}
impl AccountFailure {
/// 按 core 的错误类型分类(不看中文文案,只看变体)。
pub(super) fn classify(error: AuthError, fallback: AccountNoticeCode) -> Self {
let code = match &error {
unlucky_core::auth::AuthError::Conflict { .. } => AccountNoticeCode::AccountExists,
unlucky_core::auth::AuthError::NotFound { .. } => AccountNoticeCode::NotFound,
unlucky_core::auth::AuthError::Malformed(_) => AccountNoticeCode::OfflineNameInvalid,
unlucky_core::auth::AuthError::Store { .. } => AccountNoticeCode::StoreFailed,
_ => fallback,
};
Self {
code,
detail: error.to_string(),
}
}
}
/// `AuthError` → 原因码(**只看变体与 ascii 码,不看中文文案**)。
pub(super) fn classify(error: &AuthError) -> LoginFailure {
match error {
AuthError::Network { .. } | AuthError::Timeout { .. } => LoginFailure::Unreachable,
AuthError::Denied { code, description } => {
if mentions_whitelist(code) || mentions_whitelist(description) {
LoginFailure::NotWhitelisted
} else {
LoginFailure::Denied
}
}
// 只看 `ResponseDetail` 的**结构化字段**(status / kind / server_message),
// 不把整条错误 Debug 成字符串再去扫——那会把中文 `what`/`digest` 也算进判据里。
AuthError::Response(detail) => {
let server = detail.server_message.as_deref().unwrap_or_default();
let digest = detail.digest.as_str();
if mentions_whitelist(server) || mentions_whitelist(digest) {
LoginFailure::NotWhitelisted
} else if detail.status >= 500 || matches!(detail.kind, BodyKind::Html | BodyKind::Text)
{
// 5xx / 拦截页:服务器或链路出了问题,不是「它拒绝了你」。
LoginFailure::Unreachable
} else {
LoginFailure::Denied
}
}
AuthError::Xbox { .. } | AuthError::CredentialExpired => LoginFailure::Denied,
AuthError::Store { .. } => LoginFailure::StoreFailed,
_ => LoginFailure::Other,
}
}
/// 是否属于「应用注册未获批」这一类(AADSTS7000218 / consent required / whitelist)。
pub(super) fn mentions_whitelist(text: &str) -> bool {
let lowered = text.to_lowercase();
lowered.contains("aadsts7000218")
|| lowered.contains("aadsts65001")
|| lowered.contains("consent")
|| lowered.contains("whitelist")
|| lowered.contains("not approved")
}
#[cfg(test)]
mod tests {
use super::*;
use unlucky_core::auth::body::ResponseDetail;
fn response(status: u16, kind: BodyKind, server_message: Option<&str>) -> AuthError {
AuthError::Response(Box::new(ResponseDetail {
step: "xsts".to_string(),
what: "XSTS 授权".to_string(),
status,
content_type: None,
kind,
server_message: server_message.map(str::to_string),
digest: "digest".to_string(),
}))
}
/// 403 带 AADSTS 码 → 白名单未批;403 不带 → 一般性拒绝。
#[test]
fn aadsts_markers_win_over_the_generic_denial() {
assert_eq!(
classify(&response(
403,
BodyKind::Json,
Some("AADSTS7000218: app not approved")
)),
LoginFailure::NotWhitelisted
);
assert_eq!(
classify(&response(403, BodyKind::Json, Some("invalid_grant"))),
LoginFailure::Denied
);
}
/// 5xx 与「拦截页」(HTML/纯文本)归「连不上」,不是「服务器拒绝了你」。
#[test]
fn upstream_failures_and_intercept_pages_are_unreachable() {
assert_eq!(
classify(&response(503, BodyKind::Json, None)),
LoginFailure::Unreachable
);
assert_eq!(
classify(&response(200, BodyKind::Html, None)),
LoginFailure::Unreachable
);
}
}