Skip to main content

futu_mcp/
trade_pwd.rs

1//! 交易密码来源封装
2//!
3//! 优先级(对 LLM 透明,它只会调 `futu_unlock_trade` 工具,密码永不入 prompt):
4//!   1. 账号级 OS keychain —— `futucli set-trade-pwd --account <login-account>`
5//!   2. 环境变量 `FUTU_TRADE_PWD`(container / systemd EnvironmentFile 场景)
6//!   3. legacy 全局 OS keychain —— v1.4.109 前的 `trade-password` 兜底读取
7//!   4. 都没有 → 报错
8//!
9//! MD5 在 server 端计算后再发网关。LLM 从来看不到明文 / MD5。
10
11use futu_auth::{KEYRING_SERVICE, KEYRING_USERNAME_TRADE_PWD, keyring_username_for_trade_pwd};
12
13fn non_empty_trimmed(s: &str) -> Option<String> {
14    let s = s.trim();
15    (!s.is_empty()).then(|| s.to_string())
16}
17
18fn trade_pwd_account_from(
19    explicit: Option<&str>,
20    trade_pwd_account_env: Option<&str>,
21    futu_account_env: Option<&str>,
22) -> Option<String> {
23    explicit
24        .and_then(non_empty_trimmed)
25        .or_else(|| trade_pwd_account_env.and_then(non_empty_trimmed))
26        .or_else(|| futu_account_env.and_then(non_empty_trimmed))
27}
28
29fn read_keyring_password(username: &str) -> Option<String> {
30    if let Ok(entry) = keyring::Entry::new(KEYRING_SERVICE, username)
31        && let Ok(pwd) = entry.get_password()
32        && !pwd.is_empty()
33    {
34        return Some(pwd);
35    }
36    None
37}
38
39fn trade_password_from_sources(
40    account_hint: Option<&str>,
41    trade_pwd_account_env: Option<&str>,
42    futu_account_env: Option<&str>,
43    env_pwd: Option<&str>,
44    mut read_keyring: impl FnMut(&str) -> Option<String>,
45) -> Result<String, String> {
46    let env_pwd = env_pwd.and_then(non_empty_trimmed);
47    if let Some(account) =
48        trade_pwd_account_from(account_hint, trade_pwd_account_env, futu_account_env)
49    {
50        let scoped_username = keyring_username_for_trade_pwd(&account);
51        if let Some(pwd) = read_keyring(&scoped_username) {
52            return Ok(pwd);
53        }
54        if let Some(pwd) = env_pwd {
55            return Ok(pwd);
56        }
57        if let Some(pwd) = read_keyring(KEYRING_USERNAME_TRADE_PWD) {
58            return Ok(pwd);
59        }
60        return Err(format!(
61            "no trade password configured for account {account}: run `futucli set-trade-pwd \
62             --account {account}`, then start futu-mcp with `--trade-pwd-account {account}` \
63             (or FUTU_TRADE_PWD_ACCOUNT={account}); alternatively set FUTU_TRADE_PWD"
64        ));
65    }
66
67    if let Some(pwd) = read_keyring(KEYRING_USERNAME_TRADE_PWD) {
68        return Ok(pwd);
69    }
70    if let Some(pwd) = env_pwd {
71        return Ok(pwd);
72    }
73    Err(
74        "no trade password configured: run `futucli set-trade-pwd --account <login-account>` \
75         and start futu-mcp with `--trade-pwd-account <login-account>` \
76         (or set FUTU_TRADE_PWD_ACCOUNT); legacy deployments may still use FUTU_TRADE_PWD"
77            .to_string(),
78    )
79}
80
81/// 试着拿到交易密码明文。成功则返回 `Ok(pwd)`;任何来源都没找到返回 `Err`。
82pub fn get_trade_password_for_account(account_hint: Option<&str>) -> Result<String, String> {
83    let trade_pwd_account_env = std::env::var("FUTU_TRADE_PWD_ACCOUNT").ok();
84    let futu_account_env = std::env::var("FUTU_ACCOUNT").ok();
85    let env_pwd = std::env::var("FUTU_TRADE_PWD").ok();
86    trade_password_from_sources(
87        account_hint,
88        trade_pwd_account_env.as_deref(),
89        futu_account_env.as_deref(),
90        env_pwd.as_deref(),
91        read_keyring_password,
92    )
93}
94
95/// 把密码 MD5 化再返回(unlock_trade RPC 要的格式)
96pub fn get_trade_password_md5_for_account(account_hint: Option<&str>) -> Result<String, String> {
97    let pwd = get_trade_password_for_account(account_hint)?;
98    Ok(md5_hex(&pwd))
99}
100
101fn md5_hex(pwd: &str) -> String {
102    format!("{:x}", md5::compute(pwd.as_bytes()))
103}
104
105#[cfg(test)]
106mod tests;