Skip to main content

futu_mcp/
card_num_expand.rs

1//! Standalone MCP allowed_card_nums expansion helpers.
2
3use std::sync::Arc;
4
5use anyhow::{Context, Result};
6use futu_auth::KeyStore;
7
8use crate::state::ServerState;
9
10#[cfg(unix)]
11pub(crate) fn spawn_sighup_reload(store: Arc<KeyStore>, state: ServerState) {
12    if !store.is_configured() {
13        return;
14    }
15    use tokio::signal::unix::{SignalKind, signal};
16    tokio::spawn(async move {
17        let mut sig = match signal(SignalKind::hangup()) {
18            Ok(s) => s,
19            Err(e) => {
20                tracing::error!(error = %e, "failed to install SIGHUP handler");
21                return;
22            }
23        };
24        tracing::info!("SIGHUP handler installed; send `kill -HUP <pid>` to reload keys");
25        while sig.recv().await.is_some() {
26            // (1) reload phase — KeyStore::load_file 重新读盘 + 重新注入
27            // fail-closed sentinel for any new allowed_card_nums entries
28            match store.reload() {
29                Ok(()) => tracing::warn!(keys_loaded = store.len(), "keys file reloaded on SIGHUP"),
30                Err(e) => {
31                    tracing::error!(error = %e, "SIGHUP reload failed; keeping old keys");
32                    continue;
33                }
34            }
35            // (2) expand phase — v1.4.105 external reviewer #4: reload 后 raw allowed_card_nums
36            // 已经回到字符串状态 (sentinel acc_id=0 已重新写入), 必须再跑一次
37            // expand 把 card_num resolve 成 acc_ids; 否则 keys.json 修改后受
38            // 影响的 key 全部回到 fail-closed reject (即使 daemon 已起 +
39            // GetAccList 缓存可用). 与 daemon 的 unified SIGHUP handler 同语义.
40            if store.has_any_card_num_restrictions() {
41                let store_clone = store.clone();
42                let state_clone = state.clone();
43                tokio::spawn(async move {
44                    if let Err(e) = expand_card_nums_via_daemon(&state_clone, &store_clone).await {
45                        tracing::warn!(
46                            error = %e,
47                            "v1.4.105 external report #4: SIGHUP re-expand failed; sentinel 仍生效保护"
48                        );
49                    }
50                });
51            }
52        }
53    });
54}
55
56/// v1.4.105 external reviewer #4 fix: 启动时后台 retry 把 KeyStore 的
57/// `allowed_card_nums` resolve 成 `allowed_acc_ids`. 与 futu-opend daemon
58/// `card_num_reload_and_expand_fn` 启动 retry loop 同节奏 (6 × 10s).
59///
60/// 失败模式:
61/// - daemon 未起来 → connect error → 等 10s 重试
62/// - daemon 起来但 GetAccList 失败 (尚未登录 / 无账户) → 等 10s 重试
63/// - 6 次都失败 → 放弃 (sentinel `{0}` 仍生效保护)
64pub(crate) fn spawn_card_num_expand_retry(state: ServerState, key_store: Arc<KeyStore>) {
65    tokio::spawn(async move {
66        const MAX_ATTEMPTS: u32 = 6;
67        const RETRY_INTERVAL_SECS: u64 = 10;
68        for attempt in 1..=MAX_ATTEMPTS {
69            match expand_card_nums_via_daemon(&state, &key_store).await {
70                Ok(()) => {
71                    tracing::info!(
72                        attempt,
73                        "v1.4.105 external report #4: standalone MCP allowed_card_nums expanded \
74                         (与 daemon expand 路径 byte-identical)"
75                    );
76                    return;
77                }
78                Err(e) => {
79                    if attempt < MAX_ATTEMPTS {
80                        tracing::warn!(
81                            attempt,
82                            max = MAX_ATTEMPTS,
83                            error = %e,
84                            "v1.4.105 external report #4: card_num expand 失败, {RETRY_INTERVAL_SECS}s 后重试"
85                        );
86                        tokio::time::sleep(std::time::Duration::from_secs(RETRY_INTERVAL_SECS))
87                            .await;
88                    } else {
89                        tracing::error!(
90                            attempt,
91                            error = %e,
92                            "v1.4.105 external report #4: card_num expand 在 {MAX_ATTEMPTS} × \
93                             {RETRY_INTERVAL_SECS}s 后仍失败; 受限 key 走 fail-closed \
94                             sentinel reject 直到下次 SIGHUP / 手动 reload"
95                        );
96                    }
97                }
98            }
99        }
100    });
101}
102
103/// v1.4.105 external reviewer #4 fix: 连 daemon → call GetAccList → 用 acc_list 构造 resolver →
104/// 调用 [`KeyStore::expand_allowed_card_nums`].
105///
106/// 这是 daemon `card_num_reload_and_expand_fn` 在 standalone MCP 进程的镜像.
107/// daemon 那边是从 `Arc<TrdCache>` 取已缓存的账户列表; MCP 没有 cache, 必须
108/// 主动调 `TRD_GetAccList` 拿 fresh data.
109async fn expand_card_nums_via_daemon(state: &ServerState, key_store: &Arc<KeyStore>) -> Result<()> {
110    // 1. 连 daemon (state.client() 懒加载 — 第一次会建立 TCP + InitConnect)
111    let client = state
112        .client()
113        .await
114        .with_context(|| "connect to daemon for card_num expand")?;
115
116    // 2. 拉 GetAccList — daemon 必须已成功登录拿到账户. 若 user 还没 unlock /
117    //    daemon 还在 establish session, 这里会返 server error (ret_type ≠ 0)
118    let accs = futu_trd::account::get_acc_list_for_account_discovery(&client)
119        .await
120        .with_context(|| "GetAccList for card_num expand")?;
121
122    if accs.is_empty() {
123        return Err(anyhow::anyhow!(
124            "GetAccList returned empty list (daemon 已起但无账户?)"
125        ));
126    }
127
128    // 3. 用 acc_list 构造 resolver. 行为与
129    //    `crates/futu-cache/src/trd_cache.rs::find_acc_ids_by_card_num`
130    //    byte-identical (4-suffix / 16-exact 双匹配 card_num + uni_card_num,
131    //    sort + dedup).
132    let resolver = build_card_num_resolver(accs);
133
134    // 4. expand — 与 daemon `card_num_reload_and_expand_fn` 用同一套 callback
135    //    语义 (warn unresolved / ambiguous, 写 sentinel 0 让 fail-closed)
136    let (resolved, unresolved, ambiguous) = key_store.expand_allowed_card_nums(
137        &resolver,
138        |key_id, cn| {
139            tracing::warn!(
140                key_id = %key_id,
141                card_num = %cn,
142                "v1.4.105 external report #4 fail-closed: card_num not found in daemon GetAccList; \
143                 sentinel acc_id=0 让限额引擎 reject 真账户 (写完整 16 位 / specific 4 位)"
144            );
145        },
146        |key_id, cn, candidates| {
147            tracing::warn!(
148                key_id = %key_id,
149                card_num = %cn,
150                candidates = ?candidates,
151                "v1.4.105 external report #4 fail-closed: ambiguous card_num suffix matched 多账户 \
152                 (skipped; 写完整 16 位 / specific 4 位)"
153            );
154        },
155    );
156
157    tracing::info!(
158        resolved,
159        unresolved,
160        ambiguous,
161        "v1.4.105 external report #4: standalone MCP allowed_card_nums expanded into allowed_acc_ids"
162    );
163    Ok(())
164}
165
166/// v1.4.105 external reviewer #4 fix: 用 `Vec<TrdAcc>` 构造 card_num resolver closure.
167///
168/// v1.4.109 Phase C: 解析规则下沉到 `futu_core::account_locator`,MCP
169/// standalone / REST / CLI 不再各自手写 4 位 suffix、16 位完整卡号和
170/// `card_num` / `uni_card_num` OR 关系。
171pub(crate) fn build_card_num_resolver(accs: Vec<futu_trd::TrdAcc>) -> impl Fn(&str) -> Vec<u64> {
172    move |input: &str| -> Vec<u64> {
173        futu_core::account_locator::match_card_num_in_records(&accs, input, None)
174            .unwrap_or_default()
175    }
176}