Skip to main content

futucli/cmd/
daemon.rs

1//! v1.4.32+ daemon 生命周期管理命令。
2//!
3//! 同事 2026-04-18 提议"出问题时快速重置"工具的具体化。三个命令:
4//! - `daemon-status`   — GET /api/admin/status
5//! - `daemon-shutdown` — POST /api/admin/shutdown
6//! - `daemon-reload`   — POST /api/admin/reload
7//!
8//! 区别于其他 futucli 命令走 TCP 协议到 11111 端口,这里走 REST 到 22222
9//! 端口——admin endpoint 只在 REST 层暴露,刻意不放 TCP/gRPC/MCP(后者
10//! LLM 可能误触发 shutdown)。
11
12use anyhow::{Context, Result};
13
14use crate::output::OutputFormat;
15
16/// 默认 REST 端点(对齐 deploy/examples/futu-opend.toml 里的 rest_port = 22222)
17const DEFAULT_REST_URL: &str = "http://127.0.0.1:22222";
18
19/// GET /api/admin/status — daemon 健康状态快照
20pub async fn run_status(
21    rest_url: Option<&str>,
22    api_key: Option<&str>,
23    _output: OutputFormat,
24) -> Result<()> {
25    let resp = request(
26        reqwest::Method::GET,
27        "/api/admin/status",
28        rest_url,
29        api_key,
30        5,
31    )
32    .await?;
33    print_json(resp)
34}
35
36/// POST /api/admin/shutdown — 请求 daemon 进入统一优雅退出路径
37pub async fn run_shutdown(rest_url: Option<&str>, api_key: Option<&str>) -> Result<()> {
38    // shutdown 响应表示 daemon 已收到退出请求;实际退出由 opend phase4
39    // 统一处理 surface shutdown / await / abort fallback。
40    let resp = request(
41        reqwest::Method::POST,
42        "/api/admin/shutdown",
43        rest_url,
44        api_key,
45        5,
46    )
47    .await?;
48    print_json(resp)?;
49    eprintln!("# daemon shutdown requested; `ps` / systemd 状态即可确认最终退出");
50    Ok(())
51}
52
53/// POST /api/admin/reload — 清 trade cipher 缓存
54pub async fn run_reload(rest_url: Option<&str>, api_key: Option<&str>) -> Result<()> {
55    let resp = request(
56        reqwest::Method::POST,
57        "/api/admin/reload",
58        rest_url,
59        api_key,
60        5,
61    )
62    .await?;
63    print_json(resp)?;
64    eprintln!("# 客户端应重新调 /api/unlock-trade 才能下单");
65    Ok(())
66}
67
68/// 共用的 HTTP 请求构造 + body 拉取,返回 body 字符串(已校验 status success)。
69async fn request(
70    method: reqwest::Method,
71    path: &str,
72    rest_url: Option<&str>,
73    api_key: Option<&str>,
74    timeout_secs: u64,
75) -> Result<String> {
76    let base = resolve_rest_url(rest_url);
77    let url = format!("{}{}", base.trim_end_matches('/'), path);
78    let client = reqwest::Client::builder()
79        .timeout(std::time::Duration::from_secs(timeout_secs))
80        .build()
81        .context("build reqwest client")?;
82    let mut req = client.request(method.clone(), &url);
83    if let Some(key) = api_key {
84        req = req.bearer_auth(key);
85    }
86    let resp = req
87        .send()
88        .await
89        .with_context(|| format!("{method} {url} failed"))?;
90    let status = resp.status();
91    let body = resp.text().await.context("read response body")?;
92    if !status.is_success() {
93        anyhow::bail!(
94            "{} {} failed: HTTP {} — {}",
95            method,
96            path,
97            status.as_u16(),
98            body.chars().take(400).collect::<String>()
99        );
100    }
101    Ok(body)
102}
103
104fn print_json(body: String) -> Result<()> {
105    let parsed: serde_json::Value =
106        serde_json::from_str(&body).with_context(|| format!("response not JSON: {body}"))?;
107    let pretty = serde_json::to_string_pretty(&parsed)?;
108    println!("{}", pretty);
109    Ok(())
110}
111
112/// 决定 REST URL:CLI 参数 > FUTU_REST_URL 环境变量 > 默认 127.0.0.1:22222
113fn resolve_rest_url(cli_override: Option<&str>) -> String {
114    if let Some(u) = cli_override {
115        return u.to_string();
116    }
117    if let Ok(env_u) = std::env::var("FUTU_REST_URL")
118        && !env_u.is_empty()
119    {
120        return env_u;
121    }
122    DEFAULT_REST_URL.to_string()
123}
124
125#[cfg(test)]
126mod tests;