Skip to main content

futu_opend/
cli.rs

1//! v1.4.110 P1-2: CLI 类型抽自 main.rs lines 125-496.
2//!
3//! Platform / LoginRegion / LogLevel enums + Args (clap derive).
4
5#![allow(unused_imports)]
6
7use clap::{Parser, ValueEnum};
8use std::path::PathBuf;
9
10/// 账号平台(v1.4.14+)
11///
12/// 决定 HTTP 认证服务器域名。两个平台**独立账号体系**:
13/// - `futunn`(默认)—— 牛牛,用户为 CN / HK 归属,auth.futunn.com
14/// - `moomoo` —— moomoo,用户为 US / SG / AU / JP / CA 归属,auth.moomoo.com
15///
16/// 同一手机号 / 邮箱可以在两边分别注册独立账号(不同密码)。`--platform`
17/// 让用户显式选择,避免"同号两边都有"时我们默认发 futunn 登到错账号。
18///
19/// `--auth-server <url>` 显式指定 URL 时**覆盖** `--platform`(给测试环境用)。
20#[derive(Debug, Clone, Copy, clap::ValueEnum, Default, serde::Deserialize, PartialEq, Eq)]
21#[serde(rename_all = "lowercase")]
22#[clap(rename_all = "lower")]
23pub enum Platform {
24    /// 牛牛(Futubull)—— CN / HK 账号
25    #[default]
26    Futunn,
27    /// moomoo —— US / SG / AU / JP / CA 账号
28    Moomoo,
29}
30
31impl Platform {
32    pub fn auth_server(self) -> &'static str {
33        match self {
34            Self::Futunn => "https://auth.futunn.com",
35            Self::Moomoo => "https://auth.moomoo.com",
36        }
37    }
38
39    pub fn name(self) -> &'static str {
40        match self {
41            Self::Futunn => "futunn",
42            Self::Moomoo => "moomoo",
43        }
44    }
45}
46
47/// v1.4.40 #12 fix (eli exhaustive report): `--login-region` 封闭 enum。
48///
49/// v1.4.39 及以前接受任意字符串(`gz` / `us` / `moomoo` / `wuhan` / 日期串都被
50/// 静默吃掉,无法反馈给用户),且**对 moomoo 账户此 flag 永远不生效**(daemon 内
51/// commconfig 按 `user_attribution` 查 `guaranteed_ip` 覆盖)。
52///
53/// v1.4.40 起:
54/// - clap `ValueEnum` 只接受 `gz` / `sh` / `hk` 三个合法值,非法值 fail-fast
55/// - 对 `--platform moomoo` 启动时若用户传了 `--login-region`,WARN log 显式说明
56///   "此 flag 仅对 futunn 生效,moomoo 账户按 user_attribution 自动路由"
57///
58/// 三个值对应 futunn 平台的广州 / 上海 / 香港数据中心标识(C++ OpenD 历史约定)。
59#[derive(Debug, Clone, Copy, clap::ValueEnum, serde::Deserialize, PartialEq, Eq)]
60#[serde(rename_all = "lowercase")]
61#[clap(rename_all = "lower")]
62pub enum LoginRegion {
63    /// futunn 广州数据中心(默认)
64    Gz,
65    /// futunn 上海数据中心
66    Sh,
67    /// futunn 香港数据中心
68    Hk,
69}
70
71impl LoginRegion {
72    pub fn as_str(self) -> &'static str {
73        match self {
74            Self::Gz => "gz",
75            Self::Sh => "sh",
76            Self::Hk => "hk",
77        }
78    }
79}
80
81/// v1.4.73 BUG-015 fix:`--log-level` silent accept 非法值(tracing-subscriber
82/// `EnvFilter::new()` 对非法值返"deny-all" filter 吞所有 log,exit=0 像正常跑
83/// 但用户看不到任何输出)。
84///
85/// 用 clap `ValueEnum` 约束有效值集合(对齐 v1.4.40 `LoginRegion` 做法),
86/// 非法值被 clap 在 parse 阶段拒绝 → exit=2 + 清晰错误输出。
87///
88/// 有效值 = `tracing` 标准 `LevelFilter` / `Level`:
89/// `trace` / `debug` / `info` / `warn` / `error` / `off`。
90#[derive(Debug, Clone, Copy, clap::ValueEnum, serde::Serialize, serde::Deserialize)]
91#[serde(rename_all = "lowercase")]
92#[clap(rename_all = "lower")]
93pub enum LogLevel {
94    /// 最细粒度(含 tracing span entry/exit)
95    Trace,
96    /// 调试信息(默认最噪)
97    Debug,
98    /// 标准运行日志(默认)
99    Info,
100    /// 警告及以上
101    Warn,
102    /// 只有错误
103    Error,
104    /// 关闭所有日志
105    Off,
106}
107
108impl LogLevel {
109    pub fn as_str(self) -> &'static str {
110        match self {
111            Self::Trace => "trace",
112            Self::Debug => "debug",
113            Self::Info => "info",
114            Self::Warn => "warn",
115            Self::Error => "error",
116            Self::Off => "off",
117        }
118    }
119
120    /// 从 config 文件的 string 解析(XML / TOML)—— 非 clap 路径使用。
121    /// 非法值返 None 让调用方 `eprintln!` + exit。
122    ///
123    /// codex 0547 F3 (P2) 之后: production 路径已改用 serde Option<LogLevel>
124    /// 直接 parse. 此 helper 只在测试里保留旧 alias 行为证据,避免把
125    /// "warning" / "silent" / "none" 重新接回 production 配置解析。
126    #[cfg(test)]
127    pub fn from_str_opt(s: &str) -> Option<Self> {
128        match s.trim().to_ascii_lowercase().as_str() {
129            "trace" => Some(Self::Trace),
130            "debug" => Some(Self::Debug),
131            "info" => Some(Self::Info),
132            "warn" | "warning" => Some(Self::Warn),
133            "error" => Some(Self::Error),
134            "off" | "none" | "silent" => Some(Self::Off),
135            _ => None,
136        }
137    }
138}
139
140/// FutuOpenD Rust Gateway — 完全替代 C++ OpenD
141#[derive(Parser, Debug)]
142#[command(name = "futu-opend", version, about = "FutuOpenD Rust Gateway")]
143pub struct Args {
144    /// XML 配置文件路径 (兼容 C++ FutuOpenD.xml)
145    #[arg(long)]
146    pub cfg_file: Option<String>,
147
148    /// TOML 配置文件路径 (v1.4.2+;字段与 CLI 参数对齐, CLI 参数覆盖).
149    ///
150    /// codex 0547 F6 (P3): 字段白名单 (与 `XmlConfig` schema 一致):
151    ///   - 登录: `login_account` / `login_pwd` / `login_pwd_md5` /
152    ///     `login_pwd_file` / `login_region` / `platform`
153    ///   - 监听: `ip` / `port` (alias `api_port`) / `rest_port` /
154    ///     `grpc_port` / `websocket_port` / `telnet_port`
155    ///   - 安全: `rsa_private_key` / `rest_keys_file` / `grpc_keys_file` /
156    ///     `ws_keys_file` / `audit_log` / `allow_tcp_unauthenticated`
157    ///   - 系统: `lang` / `log_level` / `tz`
158    ///
159    /// **不能写 TOML 的 CLI-only 字段** (运维 / 调试 / 一次性流程):
160    ///   `device_id` / `reset_device` / `setup_only` / `verify_code` /
161    ///   `json_log` / `inject_auth_failure_every` (dev-flags feature only)
162    ///
163    /// 任何 unknown field / typo 触发 fatal parse error (BUG-006
164    /// `deny_unknown_fields`), daemon abort. 不再 silent drop.
165    ///
166    /// 示例:
167    /// ```toml
168    /// login_account = "123456"
169    /// ip = "0.0.0.0"
170    /// port = 11111
171    /// rest_port = 22222
172    /// grpc_port = 33333
173    /// rest_keys_file = "/etc/futu/keys.json"
174    /// audit_log = "/var/log/futu-audit.jsonl"
175    /// tz = "Asia/Hong_Kong"
176    /// ```
177    #[arg(long)]
178    pub config: Option<String>,
179
180    /// API 服务监听地址
181    #[arg(short = 'i', long)]
182    pub ip: Option<String>,
183
184    /// API 服务监听端口
185    #[arg(short = 'p', long)]
186    pub port: Option<u16>,
187
188    /// 登录账号
189    #[arg(long)]
190    pub login_account: Option<String>,
191
192    /// 登录密码明文
193    #[arg(long)]
194    pub login_pwd: Option<String>,
195
196    /// 登录密码 MD5 (32 位小写 hex)
197    #[arg(long)]
198    pub login_pwd_md5: Option<String>,
199
200    /// 登录密码从**文件**读(v1.4.18+)——适用于 systemd `LoadCredential=` /
201    /// Docker secrets 场景。argv 里只有文件路径,不会泄露明文。
202    ///
203    /// 文件内容:明文密码(末尾 `\n` 会被 trim 掉)。
204    #[arg(long)]
205    pub login_pwd_file: Option<String>,
206
207    /// 后端连接区域 (gz / sh / hk) —— **仅对 `--platform futunn` 生效**
208    ///
209    /// 这三个值是 **futunn 平台**的广州 / 上海 / 香港数据中心标识。
210    ///
211    /// **对 `--platform moomoo` 账户此 flag 会被忽略**:daemon 内 commconfig 根据
212    /// `user_attribution` 查 `guaranteed_ip` 列表覆盖。想切 moomoo 各区用
213    /// `--platform moomoo` + 账号本身决定归属。
214    ///
215    /// v1.4.40 起 clap 封闭 enum 拒绝非法值(v1.4.39 及以前静默接受任意字符串)。
216    #[arg(long, value_enum)]
217    pub login_region: Option<LoginRegion>,
218
219    /// 账号平台(v1.4.14+)—— futunn=牛牛/CN/HK,moomoo=US/SG/AU/JP/CA
220    ///
221    /// 同手机号 / 邮箱可以在两边各注册独立账号(不同密码)。默认 futunn。
222    /// `--auth-server` 显式指定 URL 时覆盖 `--platform`。
223    #[arg(long, value_enum)]
224    pub platform: Option<Platform>,
225
226    /// 认证服务器 URL(覆盖 `--platform` 推导的默认值,主要给测试环境用)
227    #[arg(long)]
228    pub auth_server: Option<String>,
229
230    /// 设备 ID(16 位 hex)—— 覆盖自动生成/持久化的值。
231    ///
232    /// v1.4.17+ 默认从 `~/.futu-opend-rs/device-{hash}.dat` 读(首次随机生成
233    /// 并写入)。本参数用于显式指定,并**更新**持久化文件。
234    ///
235    /// 如果 device_id 被服务端锁定(`error_code=15/21`),可用
236    /// `--reset-device` 一键清空文件让下次启动随机生成新值。
237    #[arg(long)]
238    pub device_id: Option<String>,
239
240    /// 重置 device_id + credentials 文件后再启动(v1.4.17+)
241    ///
242    /// 当用户的 device_id 因空验证码 / 多次 SMS 输错被服务端锁定,
243    /// 所有后续请求都返回 `error_code=15 长时间没有登录` 无法恢复。
244    /// 本参数删除 `~/.futu-opend-rs/device-{hash}.dat` 和
245    /// `credentials-{hash}.json`,下次 login 重新生成随机 device_id 走
246    /// 完整首登流程。
247    #[arg(long)]
248    pub reset_device: bool,
249
250    /// 只完成首次设备验证 + 凭据缓存后退出(v1.4.17+)
251    ///
252    /// 用于 systemd / Docker / cron 场景:先在**前台终端**手动跑一次
253    /// `futu-opend --setup-only` 完成 SMS 验证,写入 credentials 文件,然后
254    /// 生产环境启动时直接走 remember-login 跳过 SMS。
255    #[arg(long)]
256    pub setup_only: bool,
257
258    /// **v1.4.57 外部 UX-04**(加拿大同事 SMS + Telegram 中继场景必需):
259    /// 直接传入 SMS 验证码,跳过 stdin prompt。用于 agent / CI / 远程中继场景
260    /// (SMS 验证码通过 Telegram/IM 转发,60 秒失效,直接用 stdin 输入来不及)。
261    ///
262    /// 典型用法:
263    /// ```bash
264    /// # 1. 先不带 --verify-code 启动触发 SMS(会 fail + 退出,但 SMS 已发)
265    /// futu-opend --setup-only --login-account X --login-pwd Y
266    /// # 2. 收到 SMS 后立即带验证码重新启动
267    /// futu-opend --setup-only --login-account X --login-pwd Y --verify-code 123456
268    /// ```
269    #[arg(long)]
270    pub verify_code: Option<String>,
271
272    /// 日志级别(v1.4.73 BUG-015: clap ValueEnum 约束有效值 trace/debug/info/warn/error/off,
273    /// 非法值 parse 阶段拒绝 + 清晰错误,不再 silent 吞 log)
274    #[arg(long, value_enum)]
275    pub log_level: Option<LogLevel>,
276
277    /// WebSocket 服务监听端口(可选,不指定则不启动 WebSocket)
278    #[arg(long)]
279    pub websocket_port: Option<u16>,
280
281    /// Telnet 管理端口(可选,不指定则不启动 Telnet)
282    #[arg(long)]
283    pub telnet_port: Option<u16>,
284
285    /// REST API 监听端口(可选,不指定则不启动 REST API)
286    #[arg(long)]
287    pub rest_port: Option<u16>,
288
289    /// gRPC 服务监听端口(可选,不指定则不启动 gRPC)
290    #[arg(long)]
291    pub grpc_port: Option<u16>,
292
293    /// RSA 私钥文件路径(PEM 格式,启用后 InitConnect 使用 RSA 加解密)
294    #[arg(long)]
295    pub rsa_private_key: Option<String>,
296
297    /// JSON 格式日志
298    #[arg(long)]
299    pub json_log: bool,
300
301    /// 界面语言 (chs=简体中文, cht=繁体中文, en=英文)
302    #[arg(long)]
303    pub lang: Option<String>,
304
305    /// REST API Bearer Token 鉴权:加载 keys.json(futucli gen-key 生成)
306    ///
307    /// 不指定时 REST API 无鉴权(保持旧行为,启动 warn)。
308    #[arg(long)]
309    pub rest_keys_file: Option<std::path::PathBuf>,
310
311    /// gRPC Bearer Token 鉴权:加载 keys.json(futucli gen-key 生成)
312    ///
313    /// 不指定时 gRPC 无鉴权。通常与 --rest-keys-file 指向同一文件。
314    #[arg(long)]
315    pub grpc_keys_file: Option<std::path::PathBuf>,
316
317    /// 核心 WebSocket Bearer Token 鉴权:加载 keys.json
318    ///
319    /// v1.0 起核心 WS(`--websocket-port`,Futu SDK 使用的 binary WS)支持
320    /// 握手 + per-message scope 鉴权。客户端用 `?token=<plaintext>` query 或
321    /// `Authorization: Bearer <plaintext>` header 传 key。不指定这个 flag 时
322    /// WS 无鉴权(legacy 保持兼容,启动 warn)。通常与 `--rest-keys-file` 指向
323    /// 同一文件。
324    #[arg(long)]
325    pub ws_keys_file: Option<std::path::PathBuf>,
326
327    /// v1.4.104 eli S-001 (P0) fix: native TCP (FTAPI port `--port`) 显式
328    /// 允许无 auth 接受连接.
329    ///
330    /// **背景**: native TCP FTAPI 协议 (Python SDK / C++ OpenD 用) 没有
331    /// Authorization header 概念, InitConnect proto 无 Bearer 字段. 加 keystore
332    /// 后无法做 caller-specific scope 检查.
333    ///
334    /// **默认行为 (v1.4.104+)**: 配置任一 keys file (`--rest-keys-file` /
335    /// `--grpc-keys-file` / `--ws-keys-file`) → daemon **关闭 TCP 端口**
336    /// (fail-closed, 防 v1.4.103 eli S-001 跨 surface bypass).
337    ///
338    /// 显式 opt-in 此 flag → 保留 TCP 端口, daemon 启动 loud warn 用户该
339    /// 端口完全无 auth.
340    #[arg(long, default_value_t = false)]
341    pub allow_tcp_unauthenticated: bool,
342
343    /// 审计日志输出:JSONL 文件路径或目录
344    ///
345    /// - 带扩展名的路径(如 `/var/log/futu-audit.jsonl`)→ 单文件 append
346    /// - 不带扩展名 / 以 `/` 结尾(如 `/var/log/futu-audit/`)→ 每日滚动,
347    ///   文件名 `futu-audit.log` + 日期后缀
348    ///
349    /// 只记录 auth / 交易 事件(target = "futu_audit"),常规日志不受影响。
350    #[arg(long)]
351    pub audit_log: Option<std::path::PathBuf>,
352
353    /// v1.4.87 #3 G1: 时区覆盖 (IANA name, 如 "Asia/Hong_Kong" / "America/New_York")
354    ///
355    /// 用于 `hours_window` 限额检查等 "local time" 语义. 不指定时用系统 `TZ`
356    /// 环境变量, 仍未设则用 UTC. 典型场景:
357    ///
358    /// - Daemon 跑在 UTC server 但想 HK 交易时段限额 → `--tz Asia/Hong_Kong`
359    /// - Daemon 跑在 local workstation 且 `TZ` 已正确 → 不用 `--tz`
360    ///
361    /// 优先级: `--tz` flag > `TZ` env var > UTC.
362    #[arg(long, value_name = "IANA_TZ")]
363    pub tz: Option<String>,
364
365    /// **DEV-ONLY** v1.4.97 P1-D-C: 每 N 秒强制将 qot_logined 置 false 触发
366    /// P1-D self-heal ladder, 给 tester 真机 verify ladder 4 cell 用.
367    ///
368    /// 仅在 `cargo build --features dev-flags` 编译时可见. release build
369    /// (no feature) 不暴露此 flag. 防 production 误启用 (per 坑 #50 SPKI dev
370    /// pattern).
371    ///
372    /// **典型使用** (仅 tester 用):
373    /// ```bash
374    /// FUTU_QOT_RELOGIN_BACKOFF_MS=5000,10000,20000,40000 \
375    ///   futu-opend --inject-auth-failure-every=10 --login-account ...
376    /// # 期望日志: P1-D ladder 5s → 10s → 20s → 40s 各 trigger 一次
377    /// ```
378    #[cfg(feature = "dev-flags")]
379    #[arg(long, value_name = "SECONDS", hide = false)]
380    pub inject_auth_failure_every: Option<u64>,
381}