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}