1use anyhow::{Context, Result};
13use serde_json::Value;
14
15use crate::output::OutputFormat;
16
17mod doctor;
18
19pub use doctor::run_doctor;
20
21#[cfg(test)]
22use doctor::{DoctorEndpoint, analyze_doctor};
23
24const DEFAULT_REST_URL: &str = "http://127.0.0.1:22222";
26
27pub async fn run_status(
29 rest_url: Option<&str>,
30 api_key: Option<&str>,
31 _output: OutputFormat,
32) -> Result<()> {
33 let resp = request(
34 reqwest::Method::GET,
35 "/api/admin/status",
36 rest_url,
37 api_key,
38 5,
39 )
40 .await?;
41 print_json(resp)
42}
43
44pub async fn run_push_subscriber_info(
46 rest_url: Option<&str>,
47 api_key: Option<&str>,
48 _output: OutputFormat,
49) -> Result<()> {
50 let resp = request(
51 reqwest::Method::GET,
52 "/api/push-subscriber-info",
53 rest_url,
54 api_key,
55 5,
56 )
57 .await?;
58 print_json(resp)
59}
60
61pub async fn run_shutdown(rest_url: Option<&str>, api_key: Option<&str>) -> Result<()> {
63 let resp = request(
66 reqwest::Method::POST,
67 "/api/admin/shutdown",
68 rest_url,
69 api_key,
70 5,
71 )
72 .await?;
73 print_json(resp)?;
74 eprintln!("# daemon shutdown requested; `ps` / systemd 状态即可确认最终退出");
75 Ok(())
76}
77
78pub async fn run_reload(rest_url: Option<&str>, api_key: Option<&str>) -> Result<()> {
80 let resp = request(
81 reqwest::Method::POST,
82 "/api/admin/reload",
83 rest_url,
84 api_key,
85 5,
86 )
87 .await?;
88 print_json(resp)?;
89 eprintln!("# 客户端应重新调 /api/unlock-trade 才能下单");
90 Ok(())
91}
92
93pub(crate) async fn request(
95 method: reqwest::Method,
96 path: &str,
97 rest_url: Option<&str>,
98 api_key: Option<&str>,
99 timeout_secs: u64,
100) -> Result<String> {
101 request_with_body(method, path, rest_url, api_key, timeout_secs, None).await
102}
103
104async fn request_with_body(
105 method: reqwest::Method,
106 path: &str,
107 rest_url: Option<&str>,
108 api_key: Option<&str>,
109 timeout_secs: u64,
110 json_body: Option<Value>,
111) -> Result<String> {
112 let base = resolve_rest_url(rest_url);
113 let client = reqwest::Client::builder()
114 .timeout(std::time::Duration::from_secs(timeout_secs))
115 .build()
116 .context("build reqwest client")?;
117 let (status, body) = request_raw(&client, &base, method.clone(), path, api_key, json_body)
118 .await
119 .with_context(|| format!("{method} {path} failed"))?;
120 if !(200..300).contains(&status) {
121 anyhow::bail!(
122 "{} {} failed: HTTP {} — {}",
123 method,
124 path,
125 status,
126 body.chars().take(400).collect::<String>()
127 );
128 }
129 Ok(body)
130}
131
132pub(crate) async fn request_raw(
133 client: &reqwest::Client,
134 base: &str,
135 method: reqwest::Method,
136 path: &str,
137 api_key: Option<&str>,
138 json_body: Option<Value>,
139) -> Result<(u16, String)> {
140 let url = format!("{}{}", base.trim_end_matches('/'), path);
141 let mut req = client.request(method.clone(), &url);
142 if let Some(key) = api_key {
143 req = req.bearer_auth(key);
144 }
145 if let Some(body) = json_body {
146 req = req.json(&body);
147 }
148 let resp = req
149 .send()
150 .await
151 .with_context(|| format!("{method} {url} failed"))?;
152 let status = resp.status().as_u16();
153 let body = resp.text().await.context("read response body")?;
154 Ok((status, body))
155}
156
157fn print_json(body: String) -> Result<()> {
158 let parsed: serde_json::Value =
159 serde_json::from_str(&body).with_context(|| format!("response not JSON: {body}"))?;
160 let pretty = serde_json::to_string_pretty(&parsed)?;
161 println!("{}", pretty);
162 Ok(())
163}
164
165pub(crate) fn resolve_rest_url(cli_override: Option<&str>) -> String {
167 if let Some(u) = cli_override {
168 return u.to_string();
169 }
170 if let Ok(env_u) = std::env::var("FUTU_REST_URL")
171 && !env_u.is_empty()
172 {
173 return env_u;
174 }
175 DEFAULT_REST_URL.to_string()
176}
177
178#[cfg(test)]
179mod tests;