Unlucky Launcher v0.0.1-alpha
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
Guards / core 零 UI 守门 (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
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
Guards / core 零 UI 守门 (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Rust 原生单文件 Minecraft 启动器:CLI 完整可用(install/launch/accounts/java/diagnose/agent), MCP server 接入外部 Agent 工具,GUI 开发中。
This commit is contained in:
commit
bc41dfcf7d
517 files changed
+92844
No files matched your search
@@ -0,0 +1,198 @@
|
||||
//! `ull accounts list|add|remove`:账户元数据操作,**凭据永不打印**。
|
||||
//!
|
||||
//! 设计约束(D9/Q21 + D4 凭据红线):
|
||||
//! - 只开四条路径:`list` / `add offline <name>` / `add msa` / `remove <name>`;
|
||||
//! authlib 的密码登录**故意不开 CLI 通道**(密码进 argv 会直接进 shell 历史与
|
||||
//! `ps` 输出,破坏红线;留给 GUI 输入框);
|
||||
//! - 输出只经 `AccountMeta`(可公开字段),`Report`/`--json` 里没有任何 token 字段;
|
||||
//! - device-code 的 `user_code`/URL 打到 **stderr**(不管 `--quiet` 都要打:不打用户没法完成登录)。
|
||||
|
||||
use serde_json::json;
|
||||
use unlucky_core::auth::account::{AccountKind, AccountMeta, Credentials};
|
||||
use unlucky_core::auth::microsoft::{now_ms, MicrosoftAuth};
|
||||
use unlucky_core::auth::offline::offline_meta;
|
||||
use unlucky_core::auth::{AccountStore, AuthJournal};
|
||||
|
||||
use crate::cli::{AccountsAddArgs, AccountsAddKind};
|
||||
use crate::context::Ctx;
|
||||
use crate::error::CliError;
|
||||
use crate::output::{progress_line, Report};
|
||||
|
||||
/// `accounts list`:列出账户(名称/轨道/UUID/创建时间)。
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// 数据根解析失败(在 `Ctx`)或读盘 IO 失败。
|
||||
pub fn list(ctx: &Ctx) -> Result<Report, CliError> {
|
||||
let store = open_store(ctx);
|
||||
let mut human = Vec::new();
|
||||
if store.is_empty() {
|
||||
human.push("还没有账户。".to_string());
|
||||
human.push(" 离线:ull accounts add offline <玩家名>".to_string());
|
||||
human.push(" 微软:ull accounts add msa(设备码交互式登录)".to_string());
|
||||
} else {
|
||||
human.push(format!("账户({} 个):", store.accounts().len()));
|
||||
for meta in store.accounts() {
|
||||
human.push(format!(
|
||||
" {} · {} · {}",
|
||||
meta.display_name(),
|
||||
meta.kind.label(),
|
||||
meta.profile_id
|
||||
));
|
||||
}
|
||||
}
|
||||
for warning in store.warnings() {
|
||||
human.push(format!("提示: {warning}"));
|
||||
}
|
||||
let accounts: Vec<serde_json::Value> = store.accounts().iter().map(account_json).collect();
|
||||
let data = json!({
|
||||
"accounts": accounts,
|
||||
"count": accounts.len(),
|
||||
"auth_dir": store.paths().root().display().to_string(),
|
||||
"warnings": store.warnings(),
|
||||
});
|
||||
Ok(Report::new(data, human))
|
||||
}
|
||||
|
||||
/// `accounts add offline <name>`。
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// 名字不合法、同名账户已存在、落盘失败。
|
||||
pub fn add(ctx: &Ctx, args: &AccountsAddArgs) -> Result<Report, CliError> {
|
||||
match args.kind {
|
||||
AccountsAddKind::Offline => add_offline(ctx, args.name.as_deref().unwrap_or_default()),
|
||||
AccountsAddKind::Microsoft => add_microsoft(ctx),
|
||||
}
|
||||
}
|
||||
|
||||
fn add_offline(ctx: &Ctx, name: &str) -> Result<Report, CliError> {
|
||||
let meta = offline_meta(name, now_ms()).map_err(super::map_auth_error)?;
|
||||
let mut store = open_store(ctx);
|
||||
store
|
||||
.insert(meta.clone(), Credentials::Offline)
|
||||
.map_err(super::map_auth_error)?;
|
||||
store.save().map_err(super::map_auth_error)?;
|
||||
let human = vec![
|
||||
format!(
|
||||
"已添加离线账户 {}(HMCL 兼容 UUID {})",
|
||||
meta.display_name(),
|
||||
meta.profile_id
|
||||
),
|
||||
format!("账户文件:{}", store.paths().accounts_file().display()),
|
||||
];
|
||||
Ok(Report::new(added_json(&meta), human))
|
||||
}
|
||||
|
||||
/// `accounts add msa`:device-code 交互式登录。
|
||||
fn add_microsoft(ctx: &Ctx) -> Result<Report, CliError> {
|
||||
// 失败日志落**本次命令的实际数据根**(`ctx.layout().logs()`):`MicrosoftAuth::from_env()`
|
||||
// 默认挂的是便携根(exe 同级 `.ULL/logs`),`--data-dir` / launcher.json 的 standard 模式下
|
||||
// 那是别人的目录,排障时找不到 auth.jsonl(M38 遗留 finding)。
|
||||
let auth = MicrosoftAuth::from_env()
|
||||
.map_err(super::map_auth_error)?
|
||||
.with_journal(Some(AuthJournal::in_logs_dir(ctx.layout().logs())));
|
||||
// 设备码要先给用户看,再开始等:core 的 `present` 回调正好卡在这个时间点。
|
||||
let session = unlucky_core::download::blocking::block_on(auth.device_code_login(|prompt| {
|
||||
let (headline, code) = prompt.lines();
|
||||
progress_line(&headline);
|
||||
progress_line(&code);
|
||||
}))
|
||||
.map_err(super::map_auth_error)?;
|
||||
|
||||
let meta = AccountMeta::new(
|
||||
AccountKind::Microsoft,
|
||||
session.profile_id.clone(),
|
||||
session.profile_name.clone(),
|
||||
now_ms(),
|
||||
);
|
||||
let mut store = open_store(ctx);
|
||||
store
|
||||
.insert(meta.clone(), Credentials::Microsoft(session))
|
||||
.map_err(super::map_auth_error)?;
|
||||
store.save().map_err(super::map_auth_error)?;
|
||||
let human = vec![
|
||||
format!(
|
||||
"已添加微软账户 {}(档案 UUID {})",
|
||||
meta.display_name(),
|
||||
meta.profile_id
|
||||
),
|
||||
format!("账户文件:{}", store.paths().accounts_file().display()),
|
||||
];
|
||||
Ok(Report::new(added_json(&meta), human))
|
||||
}
|
||||
|
||||
/// `accounts remove <name>`。
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// 找不到该账户、落盘失败。
|
||||
pub fn remove(ctx: &Ctx, name: &str) -> Result<Report, CliError> {
|
||||
let mut store = open_store(ctx);
|
||||
let meta = store.remove(name).map_err(super::map_auth_error)?;
|
||||
store.save().map_err(super::map_auth_error)?;
|
||||
let human = vec![format!(
|
||||
"已删除账户 {}({})",
|
||||
meta.display_name(),
|
||||
meta.kind.label()
|
||||
)];
|
||||
let data = json!({
|
||||
"removed": account_json(&meta),
|
||||
"count": store.accounts().len(),
|
||||
});
|
||||
Ok(Report::new(data, human))
|
||||
}
|
||||
|
||||
fn open_store(ctx: &Ctx) -> AccountStore {
|
||||
AccountStore::open(ctx.auth_data())
|
||||
}
|
||||
|
||||
fn added_json(meta: &AccountMeta) -> serde_json::Value {
|
||||
json!({
|
||||
"account": account_json(meta),
|
||||
"account_id": meta.id.as_str(),
|
||||
})
|
||||
}
|
||||
|
||||
/// 账户元数据 → JSON(**只含可公开字段**)。
|
||||
fn account_json(meta: &AccountMeta) -> serde_json::Value {
|
||||
json!({
|
||||
"name": meta.display_name(),
|
||||
"profile_name": meta.profile_name,
|
||||
"id": meta.id.as_str(),
|
||||
"kind": meta.kind.as_str(),
|
||||
"profile_id": meta.profile_id,
|
||||
"login_name": meta.login_name,
|
||||
"server_url": meta.server_url,
|
||||
"created_at_ms": meta.created_at_ms,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::account_json;
|
||||
use unlucky_core::auth::account::{AccountKind, AccountMeta};
|
||||
|
||||
#[test]
|
||||
fn account_json_has_no_credential_fields() {
|
||||
let mut meta = AccountMeta::new(
|
||||
AccountKind::Microsoft,
|
||||
"0f8fad5bd9cb469fa16570867728950e",
|
||||
"Steve",
|
||||
7,
|
||||
);
|
||||
meta.login_name = Some("me@example.com".to_string());
|
||||
let value = account_json(&meta);
|
||||
let keys: Vec<&str> = value
|
||||
.as_object()
|
||||
.expect("对象")
|
||||
.keys()
|
||||
.map(String::as_str)
|
||||
.collect();
|
||||
for forbidden in ["access_token", "refresh_token", "token", "password"] {
|
||||
assert!(!keys.contains(&forbidden), "不该出现 {forbidden}");
|
||||
}
|
||||
assert_eq!(value["name"], "Steve");
|
||||
assert_eq!(value["kind"], "microsoft");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user