futu_mcp/
card_num_expand.rs1use 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 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 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
56pub(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
103async fn expand_card_nums_via_daemon(state: &ServerState, key_store: &Arc<KeyStore>) -> Result<()> {
110 let client = state
112 .client()
113 .await
114 .with_context(|| "connect to daemon for card_num expand")?;
115
116 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 let resolver = build_card_num_resolver(accs);
133
134 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
166pub(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}