Files
Unlucky-Launcher/crates/agent/src/tools/accounts.rs
T
AskaEth bc41dfcf7d
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
Unlucky Launcher v0.0.1-alpha
Rust 原生单文件 Minecraft 启动器:CLI 完整可用(install/launch/accounts/java/diagnose/agent),
MCP server 接入外部 Agent 工具,GUI 开发中。
2026-10-31 21:54:10 +08:00

111 lines
4.1 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! `list_accounts`:账户清单(**凭据永不出口**)。
//!
//! 这个工具存在的理由很具体:用户常问「我有哪几个号 / 这局用的是哪个号」。
//! 但 D4/D9 的凭据红线是硬的——工具结果会回灌模型、也可能被发往远端 LLM,
//! 所以:
//! 1. 端口只回 [`crate::port::AccountSummary`](`AccountMeta` 的公开字段);
//! 2. 本文件里**没有任何**能表达 token / 密码的类型(连字段名都不出现);
//! 3. 工具结果再过一遍 [`crate::redact`],兜住实现方把凭据塞进 `profile_name` 的意外。
use std::sync::Arc;
use serde_json::{json, Value};
use crate::port::AccountPort;
use crate::registry::schema::object;
use crate::registry::{
RegistryError, ToolCtx, ToolError, ToolFn, ToolOutput, ToolRegistry, ToolSpec,
};
/// 工具名。
pub const LIST_ACCOUNTS: &str = "list_accounts";
/// 注册 `list_accounts`。
///
/// # Errors
/// 工具名重复/schema 非法时返回 [`RegistryError`]。
pub fn register(
registry: &mut ToolRegistry,
port: Arc<dyn AccountPort>,
) -> Result<(), RegistryError> {
let handler: ToolFn = Arc::new(move |_ctx: &ToolCtx, _args: &Value| {
let accounts = port.list_accounts().map_err(ToolError::Port)?;
let count = accounts.len();
let display: Vec<String> = accounts
.iter()
.map(|a| {
if a.profile_name.trim().is_empty() {
format!("{}({})", a.id, a.kind)
} else {
format!("{}({})", a.profile_name, a.kind)
}
})
.collect();
let mut content = json!({
"count": count,
"accounts": accounts,
"credential_policy": "账户凭据(access token / 刷新令牌 / authlib 密码)永不经由 AI 工具出口",
});
// 兜底脱敏:端口实现若把敏感串塞进了公开字段,这里也不会带出去。
content = crate::redact::redact_texts(&content);
Ok(ToolOutput::ok(
content,
if count == 0 {
"还没有任何账户".to_string()
} else {
format!("账户 {count} 个:{}", display.join("、"))
},
))
});
registry.register(ToolSpec::read_only(
LIST_ACCOUNTS,
"列出启动器里已登录的账户(展示名 / 轨道 offline|microsoft|authlib / 档案 UUID / 创建时间)。**永不返回任何凭据**:令牌与密码不在工具结果的类型里,需要登录态请让用户在启动器界面查看。",
object(&[], &[]),
handler,
))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testing::MockAccounts;
fn registry(port: Arc<dyn AccountPort>) -> ToolRegistry {
let mut registry = ToolRegistry::new();
register(&mut registry, port).expect("注册应成功");
registry
}
fn call(registry: &ToolRegistry) -> Value {
let spec = registry.get(LIST_ACCOUNTS).expect("工具存在").clone();
let ctx = ToolCtx {
agent_session_id: "s1".to_string(),
confirmed: false,
};
(spec.handler)(&ctx, &json!({})).expect("应成功").content
}
#[test]
fn lists_accounts_without_credentials() {
let content = call(&registry(Arc::new(MockAccounts::new())));
assert_eq!(content["count"], json!(1));
assert_eq!(content["accounts"][0]["profile_name"], json!("Steve"));
// 只扫账户数据本身:`credential_policy` 那句说明文字里本来就写着 "token"。
let fields = content["accounts"].to_string().to_lowercase();
for forbidden in ["token", "password", "refresh", "secret"] {
assert!(
!fields.contains(forbidden),
"账户字段含 `{forbidden}`:{fields}"
);
}
}
#[test]
fn empty_list_is_not_an_error() {
let content = call(&registry(Arc::new(MockAccounts::empty())));
assert_eq!(content["count"], json!(0));
assert_eq!(content["accounts"], json!([]));
}
}