Skip to main content

futu_mcp/handlers/trade/
accounts.rs

1//! mcp/handlers/trade/accounts — AccountOut + list_accounts_filtered + visible_card_num helpers
2//! (v1.4.110 CC Batch O: 拆自 trade.rs L113-237)
3
4use std::collections::HashSet;
5use std::sync::Arc;
6
7use anyhow::Result;
8use futu_net::client::FutuClient;
9use serde::Serialize;
10
11#[derive(Serialize)]
12pub struct AccountOut {
13    pub acc_id: String,
14    pub trd_env: i32,
15    pub env_label: &'static str,
16    pub trd_market_auth_list: Vec<i32>,
17    // v1.4.108 account-discovery fix: MCP 是用户/agent 可见 surface, 不暴露
18    // C++ 的 `cardNum` / `uniCardNum` 双字段差异。对外只有一个 `card_num`:
19    // App 可见综合卡号优先, 普通账户退回 raw cardNum。内部 TrdAcc 仍保留两
20    // 个字段供 backend 语义判断。
21    //
22    // 同时 acc_id 用 string 序列化,避免 JSON/JS 消费端把 u64 大账户号四舍五入。
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pub card_num: Option<String>,
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub security_firm: Option<i32>,
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub acc_type: Option<i32>,
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub acc_status: Option<i32>,
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub acc_role: Option<i32>,
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub acc_label: Option<String>,
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub sim_acc_type: Option<i32>,
37    #[serde(skip_serializing_if = "Vec::is_empty")]
38    pub jp_acc_type: Vec<i32>,
39}
40
41pub fn visible_card_num_for_account(a: &futu_trd::account::TrdAcc) -> Option<String> {
42    futu_core::account_locator::visible_card_num(a).map(ToOwned::to_owned)
43}
44
45pub fn unique_acc_ids_from_allowed_card_nums(
46    accs: &[futu_trd::account::TrdAcc],
47    allowed_card_nums: Option<&[String]>,
48) -> HashSet<u64> {
49    let mut ids = HashSet::new();
50    let Some(allowed_card_nums) = allowed_card_nums else {
51        return ids;
52    };
53    for card_num in allowed_card_nums
54        .iter()
55        .map(|s| s.trim())
56        .filter(|s| !s.is_empty())
57    {
58        if let Ok(futu_core::account_locator::CardNumResolution::Resolved(acc_id)) =
59            futu_core::account_locator::resolve_card_num_in_records(accs, card_num, None)
60        {
61            ids.insert(acc_id);
62        }
63    }
64    ids
65}
66
67pub fn caller_visible_accounts<'a>(
68    accs: &'a [futu_trd::account::TrdAcc],
69    caller_allowed: Option<&HashSet<u64>>,
70    allowed_card_nums: Option<&[String]>,
71) -> Vec<&'a futu_trd::account::TrdAcc> {
72    let allowed_ids_active = caller_allowed.is_some_and(|s| !s.is_empty());
73    let allowed_cards_active = allowed_card_nums.is_some_and(|v| !v.is_empty());
74    if !allowed_ids_active && !allowed_cards_active {
75        return accs.iter().collect();
76    }
77
78    let allowed_by_card = unique_acc_ids_from_allowed_card_nums(accs, allowed_card_nums);
79    accs.iter()
80        .filter(|a| {
81            let allowed_by_id =
82                caller_allowed.is_some_and(|allowed| a.acc_id != 0 && allowed.contains(&a.acc_id));
83            allowed_by_id || allowed_by_card.contains(&a.acc_id)
84        })
85        .collect()
86}
87
88/// v1.4.103 codex F2.5 (P2): 按 caller 的 allowed_acc_ids filter list.
89///
90/// 之前 `list_accounts` 返全部账户列表 — 受限 key (allowed_acc_ids 非空) 仍能
91/// enumerate 所有 acc_id 用于跨 surface discovery (即使 read 操作被拦, 信息
92/// 已泄漏). 本版 filter 让受限 key 只看到自己 allowed 的账户.
93///
94/// `caller_allowed = None` 或空集 → 不 filter (legacy / 无限制 key, 与原行为
95/// 兼容).
96pub async fn list_accounts_filtered(
97    client: &Arc<FutuClient>,
98    caller_allowed: Option<&std::collections::HashSet<u64>>,
99    allowed_card_nums: Option<&[String]>,
100) -> Result<String> {
101    let accs = futu_trd::account::app_visible_accounts(
102        futu_trd::account::get_acc_list_for_account_discovery(client).await?,
103    );
104    let visible = caller_visible_accounts(&accs, caller_allowed, allowed_card_nums);
105    let out: Vec<AccountOut> = visible
106        .into_iter()
107        .map(|a| AccountOut {
108            acc_id: a.acc_id.to_string(),
109            trd_env: a.trd_env,
110            env_label: match a.trd_env {
111                0 => "simulate",
112                1 => "real",
113                _ => "unknown",
114            },
115            trd_market_auth_list: a.trd_market_auth_list.clone(),
116            card_num: visible_card_num_for_account(a),
117            security_firm: a.security_firm,
118            acc_type: a.acc_type,
119            acc_status: a.acc_status,
120            acc_role: a.acc_role,
121            acc_label: a.acc_label.clone(),
122            sim_acc_type: a.sim_acc_type,
123            jp_acc_type: a.jp_acc_type.clone(),
124        })
125        .collect();
126    Ok(serde_json::to_string_pretty(&out)?)
127}