futu_mcp/
state.rs

1//! 共享状态:网关连接 + 订阅状态 + 授权
2
3use std::sync::Arc;
4
5use anyhow::{anyhow, Context, Result};
6use futu_auth::{KeyRecord, KeyStore, RuntimeCounters};
7use futu_net::client::{ClientConfig, FutuClient, ReconnectingClient};
8use futu_qot::types::{QotMarket, Security};
9use tokio::sync::Mutex;
10
11/// MCP server 运行时状态
12#[derive(Clone)]
13pub struct ServerState {
14    pub inner: Arc<Mutex<Inner>>,
15    /// 是否启用交易写工具(place/modify/cancel)。默认 false。旧开关,仅当
16    /// `key_store.is_configured() == false` 时生效。
17    pub enable_trading: bool,
18    /// 是否允许对 real 环境下单。默认 false。旧开关,同上。
19    pub allow_real_trading: bool,
20    /// keys.json 加载的 KeyStore。`is_configured()` 为 true 时走 scope 授权模式。
21    pub key_store: Arc<KeyStore>,
22    /// 调用方传入的 API Key 对应的记录;None 表示未提供 key。
23    pub authed_key: Option<Arc<KeyRecord>>,
24    /// 限额运行时(日累计计数器)
25    pub counters: Arc<RuntimeCounters>,
26}
27
28pub struct Inner {
29    pub gateway: String,
30    pub client: Option<Arc<FutuClient>>,
31}
32
33impl ServerState {
34    pub fn new(gateway: String) -> Self {
35        Self {
36            inner: Arc::new(Mutex::new(Inner {
37                gateway,
38                client: None,
39            })),
40            enable_trading: false,
41            allow_real_trading: false,
42            key_store: Arc::new(KeyStore::empty()),
43            authed_key: None,
44            counters: Arc::new(RuntimeCounters::new()),
45        }
46    }
47
48    /// 启用交易写工具(构造器式链式设置)
49    pub fn with_trading(mut self, enable_trading: bool, allow_real_trading: bool) -> Self {
50        self.enable_trading = enable_trading;
51        self.allow_real_trading = allow_real_trading;
52        self
53    }
54
55    /// 设置 KeyStore(新授权模式)
56    pub fn with_key_store(mut self, store: Arc<KeyStore>) -> Self {
57        self.key_store = store;
58        self
59    }
60
61    /// 设置已通过验证的 API Key 记录
62    pub fn with_authed_key(mut self, key: Option<Arc<KeyRecord>>) -> Self {
63        self.authed_key = key;
64        self
65    }
66
67    /// 是否启用了 scope 授权模式
68    pub fn is_scope_mode(&self) -> bool {
69        self.key_store.is_configured()
70    }
71
72    /// 获取(或懒加载)网关客户端
73    pub async fn client(&self) -> Result<Arc<FutuClient>> {
74        let mut guard = self.inner.lock().await;
75        if let Some(c) = &guard.client {
76            return Ok(c.clone());
77        }
78
79        let config = ClientConfig {
80            addr: guard.gateway.clone(),
81            client_ver: env!("CARGO_PKG_VERSION").to_string(),
82            client_id: "futu-mcp".to_string(),
83            recv_notify: false,
84            rsa_key: None,
85        };
86        let mut reconnector = ReconnectingClient::new(config);
87        let (client, mut push_rx, _info) = reconnector
88            .connect()
89            .await
90            .with_context(|| format!("connect to futu gateway at {}", guard.gateway))?;
91
92        // 为了不阻塞订阅,推送通道必须被持续消费。MVP 直接丢弃。
93        tokio::spawn(async move {
94            while push_rx.recv().await.is_some() {
95                // drop
96            }
97        });
98
99        let arc = Arc::new(client);
100        guard.client = Some(arc.clone());
101        Ok(arc)
102    }
103}
104
105// ========== symbol 解析 ==========
106
107/// 解析 "MARKET.CODE" 格式的 symbol
108pub fn parse_symbol(s: &str) -> Result<Security> {
109    let (market_str, code) = s.split_once('.').ok_or_else(|| {
110        anyhow!("invalid symbol {s:?}: expected MARKET.CODE (e.g. HK.00700, US.AAPL, SH.600519)")
111    })?;
112    if code.is_empty() {
113        return Err(anyhow!("invalid symbol {s:?}: code part is empty"));
114    }
115    let market = match market_str.to_ascii_uppercase().as_str() {
116        "HK" => QotMarket::HkSecurity,
117        "HK_FUTURE" => QotMarket::HkFuture,
118        "US" => QotMarket::UsSecurity,
119        "SH" => QotMarket::CnshSecurity,
120        "SZ" => QotMarket::CnszSecurity,
121        other => {
122            return Err(anyhow!(
123                "invalid symbol {s:?}: unknown market {other:?} (HK|HK_FUTURE|US|SH|SZ)"
124            ))
125        }
126    };
127    Ok(Security::new(market, code))
128}
129
130/// 格式化 Security 为 "MARKET.CODE"
131pub fn format_symbol(sec: &Security) -> String {
132    let m = match sec.market {
133        QotMarket::HkSecurity => "HK",
134        QotMarket::HkFuture => "HK_FUTURE",
135        QotMarket::UsSecurity => "US",
136        QotMarket::CnshSecurity => "SH",
137        QotMarket::CnszSecurity => "SZ",
138        QotMarket::Unknown => "UNKNOWN",
139    };
140    format!("{m}.{}", sec.code)
141}