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
5use clap::Parser;
6
7/// 账号平台(v1.4.14+)
8///
9/// 决定 HTTP 认证服务器域名。两个平台**独立账号体系**:
10/// - `futunn`(默认)—— 牛牛,用户为 CN / HK 归属,auth.futunn.com
11/// - `moomoo` —— moomoo,用户为 US / SG / AU / JP / CA 归属,auth.moomoo.com
12///
13/// 同一手机号 / 邮箱可以在两边分别注册独立账号(不同密码)。`--platform`
14/// 让用户显式选择,避免"同号两边都有"时我们默认发 futunn 登到错账号。
15///
16/// `--auth-server <url>` 显式指定 URL 时**覆盖** `--platform`(给测试环境用)。
17#[derive(Debug, Clone, Copy, clap::ValueEnum, Default, serde::Deserialize, PartialEq, Eq)]
18#[serde(rename_all = "lowercase")]
19#[clap(rename_all = "lower")]
20pub enum Platform {
21    /// 牛牛(Futubull)—— CN / HK 账号
22    #[default]
23    Futunn,
24    /// moomoo —— US / SG / AU / JP / CA 账号
25    Moomoo,
26}
27
28impl Platform {
29    pub fn auth_server(self) -> &'static str {
30        match self {
31            Self::Futunn => "https://auth.futunn.com",
32            Self::Moomoo => "https://auth.moomoo.com",
33        }
34    }
35
36    pub fn name(self) -> &'static str {
37        match self {
38            Self::Futunn => "futunn",
39            Self::Moomoo => "moomoo",
40        }
41    }
42}
43
44/// v1.4.40 #12 fix (external reviewer exhaustive report): `--login-region` 封闭 enum。
45///
46/// v1.4.39 及以前接受任意字符串(`gz` / `us` / `moomoo` / `wuhan` / 日期串都被
47/// 静默吃掉,无法反馈给用户),且**对 moomoo 账户此 flag 永远不生效**(daemon 内
48/// commconfig 按 `user_attribution` 查 `guaranteed_ip` 覆盖)。
49///
50/// v1.4.40 起:
51/// - clap `ValueEnum` 只接受 `gz` / `sh` / `hk` 三个合法值,非法值 fail-fast
52/// - 对 `--platform moomoo` 启动时若用户传了 `--login-region`,WARN log 显式说明
53///   "此 flag 仅对 futunn 生效,moomoo 账户按 user_attribution 自动路由"
54///
55/// 三个值对应 futunn 平台的广州 / 上海 / 香港数据中心标识(C++ OpenD 历史约定)。
56#[derive(Debug, Clone, Copy, clap::ValueEnum, serde::Deserialize, PartialEq, Eq)]
57#[serde(rename_all = "lowercase")]
58#[clap(rename_all = "lower")]
59pub enum LoginRegion {
60    /// futunn 广州数据中心(默认)
61    Gz,
62    /// futunn 上海数据中心
63    Sh,
64    /// futunn 香港数据中心
65    Hk,
66}
67
68impl LoginRegion {
69    pub fn as_str(self) -> &'static str {
70        match self {
71            Self::Gz => "gz",
72            Self::Sh => "sh",
73            Self::Hk => "hk",
74        }
75    }
76}
77
78/// v1.4.73 BUG-015 fix:`--log-level` silent accept 非法值(tracing-subscriber
79/// `EnvFilter::new()` 对非法值返"deny-all" filter 吞所有 log,exit=0 像正常跑
80/// 但用户看不到任何输出)。
81///
82/// 用 clap `ValueEnum` 约束有效值集合(对齐 v1.4.40 `LoginRegion` 做法),
83/// 非法值被 clap 在 parse 阶段拒绝 → exit=2 + 清晰错误输出。
84///
85/// 有效值 = `tracing` 标准 `LevelFilter` / `Level`:
86/// `trace` / `debug` / `info` / `warn` / `error` / `off`。
87#[derive(Debug, Clone, Copy, clap::ValueEnum, serde::Serialize, serde::Deserialize)]
88#[serde(rename_all = "lowercase")]
89#[clap(rename_all = "lower")]
90pub enum LogLevel {
91    /// 最细粒度(含 tracing span entry/exit)
92    Trace,
93    /// 调试信息(默认最噪)
94    Debug,
95    /// 标准运行日志(默认)
96    Info,
97    /// 警告及以上
98    Warn,
99    /// 只有错误
100    Error,
101    /// 关闭所有日志
102    Off,
103}
104
105impl LogLevel {
106    pub fn as_str(self) -> &'static str {
107        match self {
108            Self::Trace => "trace",
109            Self::Debug => "debug",
110            Self::Info => "info",
111            Self::Warn => "warn",
112            Self::Error => "error",
113            Self::Off => "off",
114        }
115    }
116
117    /// 从 config 文件的 string 解析(XML / TOML)—— 非 clap 路径使用。
118    /// 非法值返 None 让调用方 `eprintln!` + exit。
119    ///
120    /// codex 0547 F3 (P2) 之后: production 路径已改用 serde Option<LogLevel>
121    /// 直接 parse. 此 helper 只在测试里保留旧 alias 行为证据,避免把
122    /// "warning" / "silent" / "none" 重新接回 production 配置解析。
123    #[cfg(test)]
124    pub fn from_str_opt(s: &str) -> Option<Self> {
125        match s.trim().to_ascii_lowercase().as_str() {
126            "trace" => Some(Self::Trace),
127            "debug" => Some(Self::Debug),
128            "info" => Some(Self::Info),
129            "warn" | "warning" => Some(Self::Warn),
130            "error" => Some(Self::Error),
131            "off" | "none" | "silent" => Some(Self::Off),
132            _ => None,
133        }
134    }
135}
136
137/// FutuOpenD Rust Gateway — 完全替代 C++ OpenD
138#[derive(Parser)]
139#[command(
140    name = "futu-opend",
141    version,
142    about = "FutuOpenD Rust Gateway",
143    after_long_help = "Emergency controls:\n  FUTU_WEBSIG_REFRESH=0  Disable proactive WebSig refresh for emergency rollback; restart the daemon after changing it.\n  FUTU_SESSION_KEY_REFRESH=0  Disable periodic session-key refresh (enabled by default) for emergency rollback; restart the daemon after changing it."
144)]
145pub struct Args {
146    /// XML 配置文件路径 (兼容 C++ FutuOpenD.xml)
147    #[arg(long)]
148    pub cfg_file: Option<String>,
149
150    /// TOML 配置文件路径 (v1.4.2+;字段与 CLI 参数对齐, CLI 参数覆盖).
151    ///
152    /// 字段白名单 (与 `XmlConfig` schema 一致):
153    ///   - 登录: `login_account` / `login_pwd` / `login_pwd_md5` /
154    ///     `login_pwd_file` / `login_region` / `platform`
155    ///   - 监听: `ip` / `port` (alias `api_port`) / `rest_port` /
156    ///     `grpc_port` / `websocket_port` / `telnet_port` / `telnet_ip`
157    ///   - 安全: `rsa_private_key` / `rest_keys_file` / `rest_tls_cert` /
158    ///     `rest_tls_key` / `grpc_keys_file` / `ws_keys_file` / `audit_log` /
159    ///     `allow_tcp_unauthenticated`
160    ///   - 系统: `lang` / `log_level` / `tz` /
161    ///     `client_sig_proactive_refresh` / `client_sig_reactive_refresh` /
162    ///     `history_kline_cloud_sync` / `enable_v18_experimental_reads` /
163    ///     `enable_v18_kline_patterns` / `enable_v18_kline_pattern_performance` /
164    ///     `enable_v18_stock_notes` / `enable_v18_stock_note_writes` /
165    ///     `language_pack_auto_update` / `language_pack_endpoint` /
166    ///     `language_pack_cache_dir` / `language_pack_update_timeout_ms`
167    ///
168    /// **不能写 TOML 的 CLI-only 字段** (运维 / 调试 / 一次性流程):
169    ///   `device_id` / `reset_device` / `setup_only` / `verify_code` /
170    ///   `json_log` / `inject_auth_failure_every` (dev-flags feature only)
171    ///
172    /// 任何 unknown field / typo 都会触发 fatal parse error 并中止启动,
173    /// 不会静默忽略。
174    ///
175    /// 示例:
176    /// ```toml
177    /// login_account = "123456"
178    /// ip = "0.0.0.0"
179    /// port = 11111
180    /// rest_port = 22222
181    /// grpc_port = 33333
182    /// rest_keys_file = "/etc/futu/keys.json"
183    /// audit_log = "/var/log/futu-audit.jsonl"
184    /// tz = "Asia/Hong_Kong"
185    /// ```
186    #[arg(long)]
187    pub config: Option<String>,
188
189    /// API 服务监听地址
190    #[arg(short = 'i', long)]
191    pub ip: Option<String>,
192
193    /// API 服务监听端口
194    #[arg(short = 'p', long)]
195    pub port: Option<u16>,
196
197    /// 登录账号
198    #[arg(long)]
199    pub login_account: Option<String>,
200
201    /// 登录密码明文(legacy argv fallback;不建议生产使用:会暴露在 `ps` 输出
202    /// 和 shell history。优先用 `--login-pwd-file`)
203    #[arg(long)]
204    pub login_pwd: Option<String>,
205
206    /// 登录密码 MD5 (32 位小写 hex;legacy argv fallback;不建议生产使用:
207    /// MD5 可直接登录,且同样会暴露在 `ps` 输出和 shell history)
208    #[arg(long)]
209    pub login_pwd_md5: Option<String>,
210
211    /// 登录密码从**文件**读(v1.4.18+)——适用于 systemd `LoadCredential=` /
212    /// Docker secrets 场景。argv 里只有文件路径,不会泄露明文。
213    ///
214    /// 文件内容:明文密码(末尾 `\n` 会被 trim 掉)。
215    #[arg(long)]
216    pub login_pwd_file: Option<String>,
217
218    /// 后端连接区域 (gz / sh / hk) —— **仅对 `--platform futunn` 生效**
219    ///
220    /// 这三个值是 **futunn 平台**的广州 / 上海 / 香港数据中心标识。
221    ///
222    /// **对 `--platform moomoo` 账户此 flag 会被忽略**:daemon 内 commconfig 根据
223    /// `user_attribution` 查 `guaranteed_ip` 列表覆盖。想切 moomoo 各区用
224    /// `--platform moomoo` + 账号本身决定归属。
225    ///
226    /// v1.4.40 起 clap 封闭 enum 拒绝非法值(v1.4.39 及以前静默接受任意字符串)。
227    #[arg(long, value_enum)]
228    pub login_region: Option<LoginRegion>,
229
230    /// 账号平台(v1.4.14+)—— futunn=牛牛/CN/HK,moomoo=US/SG/AU/JP/CA
231    ///
232    /// 同手机号 / 邮箱可以在两边各注册独立账号(不同密码)。默认 futunn。
233    /// `--auth-server` 显式指定 URL 时覆盖 `--platform`。
234    #[arg(long, value_enum)]
235    pub platform: Option<Platform>,
236
237    /// 认证服务器 URL(覆盖 `--platform` 推导的默认值,主要给测试环境用)
238    #[arg(long)]
239    pub auth_server: Option<String>,
240
241    /// 设备 ID(16 位 hex)—— 覆盖自动生成/持久化的值。
242    ///
243    /// v1.4.17+ 默认从 `~/.futu-opend-rs/device-{hash}.dat` 读(首次随机生成
244    /// 并写入)。本参数用于显式指定,并**更新**持久化文件。
245    ///
246    /// 如果 device_id 被服务端锁定(`error_code=15/21`),可用
247    /// `--reset-device` 一键清空文件让下次启动随机生成新值。
248    #[arg(long)]
249    pub device_id: Option<String>,
250
251    /// 重置 device_id + credentials 文件后再启动(v1.4.17+)
252    ///
253    /// 当用户的 device_id 因空验证码 / 多次 SMS 输错被服务端锁定,
254    /// 所有后续请求都返回 `error_code=15 长时间没有登录` 无法恢复。
255    /// 本参数删除 `~/.futu-opend-rs/device-{hash}.dat` 和
256    /// `credentials-{hash}.json`,下次 login 重新生成随机 device_id 走
257    /// 完整首登流程。
258    #[arg(long)]
259    pub reset_device: bool,
260
261    /// 只完成首次设备验证 + 凭据缓存后退出(v1.4.17+)
262    ///
263    /// 用于 systemd / Docker / cron 场景:先在**前台终端**手动跑一次
264    /// `futu-opend --setup-only` 完成 SMS 验证,写入 credentials 文件,然后
265    /// 生产环境启动时直接走 remember-login 跳过 SMS。
266    #[arg(long)]
267    pub setup_only: bool,
268
269    /// 直接传入 SMS 验证码,跳过终端交互。用于 systemd / Docker / CI /
270    /// 远程中继等非 TTY 场景。
271    ///
272    /// 若同一 HOME 下已有仍有效、尚未完成的 SMS challenge:
273    /// - 前台 TTY 重启会在发出替换 challenge 的请求前安全提示输入验证码;
274    /// - 非 TTY 重启必须提供本参数,否则会保留原 challenge 并在发请求前退出。
275    ///
276    /// 典型用法:
277    /// ```bash
278    /// # 1. 先不带 --verify-code 启动触发 SMS(非 TTY 会退出,但 SMS 已发)
279    /// futu-opend --setup-only --login-account X --login-pwd-file /path/to/login-pwd
280    /// # 2. 收到 SMS 后使用同一 HOME,立即带验证码重新启动
281    /// futu-opend --setup-only --login-account X --login-pwd-file /path/to/login-pwd --verify-code 123456
282    /// ```
283    #[arg(long)]
284    pub verify_code: Option<String>,
285
286    /// 日志级别(trace/debug/info/warn/error/off);非法值在参数解析阶段拒绝。
287    #[arg(long, value_enum)]
288    pub log_level: Option<LogLevel>,
289
290    /// WebSocket 服务监听端口(可选,不指定则不启动 WebSocket)
291    #[arg(long)]
292    pub websocket_port: Option<u16>,
293
294    /// Telnet 管理端口(可选,不指定则不启动 Telnet)
295    #[arg(long)]
296    pub telnet_port: Option<u16>,
297
298    /// Telnet 管理口监听地址(默认 127.0.0.1;仅在 --telnet-port 启用时生效)
299    ///
300    /// 与主 `--ip` 独立:即使 FTAPI/REST/gRPC 绑定 0.0.0.0,Telnet 默认也
301    /// 只监听 loopback,避免误暴露无鉴权管理口。
302    #[arg(long)]
303    pub telnet_ip: Option<String>,
304
305    /// REST API 监听端口(可选,不指定则不启动 REST API)
306    #[arg(long)]
307    pub rest_port: Option<u16>,
308
309    /// REST listener TLS certificate chain(PEM)。
310    #[arg(long)]
311    pub rest_tls_cert: Option<std::path::PathBuf>,
312
313    /// REST listener TLS private key(PEM;必须与 certificate 成对配置)。
314    #[arg(long)]
315    pub rest_tls_key: Option<std::path::PathBuf>,
316
317    /// gRPC 服务监听端口(可选,不指定则不启动 gRPC)
318    #[arg(long)]
319    pub grpc_port: Option<u16>,
320
321    /// RSA 私钥文件路径(PEM 格式,启用后 InitConnect 使用 RSA 加解密)
322    #[arg(long)]
323    pub rsa_private_key: Option<String>,
324
325    /// JSON 格式日志
326    #[arg(long)]
327    pub json_log: bool,
328
329    /// 界面语言 (chs=简体中文, cht=繁体中文, en=英文)
330    #[arg(long)]
331    pub lang: Option<String>,
332
333    /// 后台自动更新语言包。默认 true;无 endpoint 或下载失败不会阻塞启动。
334    ///
335    /// endpoint 来源优先级: CLI `--language-pack-endpoint` > TOML/XML
336    /// `language_pack_endpoint` > `FUTU_LANGUAGE_PACK_ENDPOINT`。未配置 endpoint
337    /// 时状态为 enabled_no_endpoint,不联网。
338    #[arg(long, value_name = "BOOL", value_parser = clap::value_parser!(bool))]
339    pub language_pack_auto_update: Option<bool>,
340
341    /// 完整 manifest URL,或必须以 `/` 结尾的 HTTPS 目录 URL(自动追加 manifest.json)。
342    ///
343    /// 默认不写死任何内部 endpoint;HTTP 仅允许 localhost 测试。
344    #[arg(long, value_name = "URL")]
345    pub language_pack_endpoint: Option<String>,
346
347    /// 语言包 cache 根目录;默认按 OS cache dir 或 FUTU_LANGUAGE_PACK_CACHE_DIR。
348    #[arg(long, value_name = "DIR")]
349    pub language_pack_cache_dir: Option<std::path::PathBuf>,
350
351    /// 语言包后台更新 HTTP 超时,单位毫秒;默认 3000。
352    #[arg(long)]
353    pub language_pack_update_timeout_ms: Option<u64>,
354
355    /// REST API Bearer Token 鉴权:加载 keys.json(futucli gen-key 生成)
356    ///
357    /// 不指定时 REST API 只读接口保持 legacy 无鉴权;写交易/admin 仍要求 API key。
358    #[arg(long)]
359    pub rest_keys_file: Option<std::path::PathBuf>,
360
361    /// gRPC Bearer Token 鉴权:加载 keys.json(futucli gen-key 生成)
362    ///
363    /// 不指定时 gRPC 无鉴权。通常与 --rest-keys-file 指向同一文件。
364    #[arg(long)]
365    pub grpc_keys_file: Option<std::path::PathBuf>,
366
367    /// 核心 WebSocket Bearer Token 鉴权:加载 keys.json
368    ///
369    /// v1.0 起核心 WS(`--websocket-port`,Futu SDK 使用的 binary WS)支持
370    /// 握手 + per-message scope 鉴权。客户端用 `?token=<plaintext>` query 或
371    /// `Authorization: Bearer <plaintext>` header 传 key。不指定这个 flag 时
372    /// WS 无鉴权(legacy 保持兼容,启动 warn)。通常与 `--rest-keys-file` 指向
373    /// 同一文件。
374    #[arg(long)]
375    pub ws_keys_file: Option<std::path::PathBuf>,
376
377    /// 显式允许 native TCP(FTAPI `--port`)接受无鉴权连接。
378    ///
379    /// **背景**: native TCP FTAPI 协议 (Python SDK / C++ OpenD 用) 没有
380    /// Authorization header 概念, InitConnect proto 无 Bearer 字段. 加 keystore
381    /// 后无法做 caller-specific scope 检查.
382    ///
383    /// **默认行为 (v1.4.104+)**: 配置任一 keys file (`--rest-keys-file` /
384    /// `--grpc-keys-file` / `--ws-keys-file`) → daemon **关闭 TCP 端口**
385    /// (fail-closed,避免跨 surface 绕过鉴权)。
386    ///
387    /// 显式 opt-in 此 flag → 保留 TCP 端口, daemon 启动 loud warn 用户该
388    /// 端口完全无 auth.
389    #[arg(
390        long,
391        default_value_t = false,
392        conflicts_with = "no_allow_tcp_unauthenticated"
393    )]
394    pub allow_tcp_unauthenticated: bool,
395
396    /// 显式禁止 native TCP(FTAPI `--port`)接受无鉴权连接。
397    ///
398    /// 此负向 CLI 开关用于覆盖 TOML/XML 中持久化的
399    /// `allow_tcp_unauthenticated = true`;CLI 显式值优先于配置文件。
400    #[arg(long, default_value_t = false)]
401    pub no_allow_tcp_unauthenticated: bool,
402
403    /// 审计日志输出:JSONL 文件路径或目录
404    ///
405    /// - 带扩展名的路径(如 `/var/log/futu-audit.jsonl`)→ 单文件 append
406    /// - 不带扩展名 / 以 `/` 结尾(如 `/var/log/futu-audit/`)→ 每日滚动,
407    ///   文件名 `futu-audit.log` + 日期后缀
408    ///
409    /// 只记录 auth / 交易 事件(target = "futu_audit"),常规日志不受影响。
410    #[arg(long)]
411    pub audit_log: Option<std::path::PathBuf>,
412
413    /// 时区覆盖(IANA name,如 "Asia/Hong_Kong" / "America/New_York")
414    ///
415    /// 用于 `hours_window` 限额检查等 "local time" 语义. 不指定时用系统 `TZ`
416    /// 环境变量, 仍未设则用 UTC. 典型场景:
417    ///
418    /// - Daemon 跑在 UTC server 但想 HK 交易时段限额 → `--tz Asia/Hong_Kong`
419    /// - Daemon 跑在 local workstation 且 `TZ` 已正确 → 不用 `--tz`
420    ///
421    /// 优先级: `--tz` flag > `TZ` env var > UTC.
422    #[arg(long, value_name = "IANA_TZ")]
423    pub tz: Option<String>,
424
425    /// 长跑 daemon: 在 client_sig 到期前 1 小时主动刷新。
426    ///
427    /// 默认关闭。也可通过环境变量 `FUTU_CLIENT_SIG_PROACTIVE_REFRESH=1` 打开。
428    /// 建议先在非关键账户长跑验证后再用于生产。
429    #[arg(long, default_value_t = false)]
430    pub client_sig_proactive_refresh: bool,
431
432    /// 长跑 daemon: reconnect 连续 TCP 登录失败时尝试刷新 client_sig 后再登录。
433    ///
434    /// 默认关闭。也可通过环境变量 `FUTU_CLIENT_SIG_REACTIVE_REFRESH=1` 打开。
435    /// 该路径只在连续失败达到阈值后触发,避免把瞬时网络抖动当成凭据过期。
436    #[arg(long, default_value_t = false)]
437    pub client_sig_reactive_refresh: bool,
438
439    /// 与官方 OpenD/App 共用历史 K 线已使用额度的云端状态。
440    ///
441    /// 默认开启。可显式传 `false` 进入 local-compat 模式;该模式只关闭
442    /// CMD20175/20179/20180 网络副作用,不关闭本地 quota owner。
443    #[arg(long, value_name = "BOOL", value_parser = clap::value_parser!(bool))]
444    pub history_kline_cloud_sync: Option<bool>,
445
446    /// Enable v1.8 backend-semantic read endpoints that have not completed authorized live parity.
447    /// Disabled by default; this grants no additional auth scope.
448    #[arg(long, default_value_t = false)]
449    pub enable_v18_experimental_reads: bool,
450
451    /// Enable Desktop K-line pattern reads 9000/9001/9003/9005.
452    /// Disabled by default; current evidence is source-aligned/local-green.
453    #[arg(long, default_value_t = false)]
454    pub enable_v18_kline_patterns: bool,
455
456    /// Enable source-derived K-line pattern performance command 9004.
457    /// Independently disabled because the pinned Desktop source has no caller.
458    #[arg(long, default_value_t = false)]
459    pub enable_v18_kline_pattern_performance: bool,
460
461    /// Enable private stock-note reads and UID-bound push. Requires API-key auth.
462    #[arg(long, default_value_t = false)]
463    pub enable_v18_stock_notes: bool,
464
465    /// Enable stock-note create/update/delete. Also requires reads and qot:write.
466    #[arg(long, default_value_t = false)]
467    pub enable_v18_stock_note_writes: bool,
468
469    /// Enable guarded v1.8 MOC writes. Disabled by default and still requires
470    /// the existing real-trading authorization gates.
471    #[arg(long, default_value_t = false)]
472    pub enable_moc_order: bool,
473
474    /// Enable guarded v1.8 Bracket/OCO/Basket BFF mutations. Disabled by
475    /// default and still requires trade:real authorization and fresh capability facts.
476    #[arg(long, default_value_t = false)]
477    pub enable_group_orders: bool,
478
479    /// Enable guarded v1.8 algorithm-order mutations. Disabled by default and
480    /// still requires trade:real authorization plus fresh capability facts.
481    #[arg(long, default_value_t = false)]
482    pub enable_algo_orders: bool,
483
484    /// Enable guarded v1.8 server-atomic position actions. Disabled by default.
485    #[arg(long, default_value_t = false)]
486    pub enable_position_actions: bool,
487
488    /// Independently authorize account-wide futures clearing. This has no
489    /// effect unless position actions are also enabled and the request sets
490    /// all_positions=true.
491    #[arg(long, default_value_t = false)]
492    pub allow_all_position_clear: bool,
493
494    /// **DEV-ONLY** v1.4.97 P1-D-C: 每 N 秒强制将 qot_logined 置 false 触发
495    /// P1-D self-heal ladder, 给 tester 真机 verify ladder 4 cell 用.
496    ///
497    /// 仅在 `cargo build --features dev-flags` 编译时可见. release build
498    /// (no feature) 不暴露此 flag. 防 production 误启用 (per 坑 #50 SPKI dev
499    /// pattern).
500    ///
501    /// **典型使用** (仅 tester 用):
502    /// ```bash
503    /// FUTU_QOT_RELOGIN_BACKOFF_MS=5000,10000,20000,40000 \
504    ///   futu-opend --inject-auth-failure-every=10 --login-account ...
505    /// # 期望日志: P1-D ladder 5s → 10s → 20s → 40s 各 trigger 一次
506    /// ```
507    #[cfg(feature = "dev-flags")]
508    #[arg(long, value_name = "SECONDS", hide = false)]
509    pub inject_auth_failure_every: Option<u64>,
510}
511
512impl std::fmt::Debug for Args {
513    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
514        let login_account_fp = self
515            .login_account
516            .as_deref()
517            .map(futu_backend::auth::redact::account_log_fingerprint);
518        let login_pwd = redact_debug_option(&self.login_pwd);
519        let login_pwd_md5 = redact_debug_option(&self.login_pwd_md5);
520        let device_id_fp = self
521            .device_id
522            .as_deref()
523            .map(futu_backend::auth::redact::device_id_log_fingerprint);
524        let verify_code = redact_debug_option(&self.verify_code);
525
526        let mut debug = f.debug_struct("Args");
527        debug
528            .field("cfg_file", &self.cfg_file)
529            .field("config", &self.config)
530            .field("ip", &self.ip)
531            .field("port", &self.port)
532            .field("login_account_fp", &login_account_fp)
533            .field("login_pwd", &login_pwd)
534            .field("login_pwd_md5", &login_pwd_md5)
535            .field("login_pwd_file", &self.login_pwd_file)
536            .field("login_region", &self.login_region)
537            .field("platform", &self.platform)
538            .field("auth_server", &self.auth_server)
539            .field("device_id_fp", &device_id_fp)
540            .field("reset_device", &self.reset_device)
541            .field("setup_only", &self.setup_only)
542            .field("verify_code", &verify_code)
543            .field("log_level", &self.log_level)
544            .field("websocket_port", &self.websocket_port)
545            .field("telnet_port", &self.telnet_port)
546            .field("telnet_ip", &self.telnet_ip)
547            .field("rest_port", &self.rest_port)
548            .field("rest_tls_cert", &self.rest_tls_cert)
549            .field("rest_tls_key", &self.rest_tls_key)
550            .field("grpc_port", &self.grpc_port)
551            .field("rsa_private_key", &self.rsa_private_key)
552            .field("json_log", &self.json_log)
553            .field("lang", &self.lang)
554            .field("language_pack_auto_update", &self.language_pack_auto_update)
555            .field("language_pack_endpoint", &self.language_pack_endpoint)
556            .field("language_pack_cache_dir", &self.language_pack_cache_dir)
557            .field(
558                "language_pack_update_timeout_ms",
559                &self.language_pack_update_timeout_ms,
560            )
561            .field("rest_keys_file", &self.rest_keys_file)
562            .field("grpc_keys_file", &self.grpc_keys_file)
563            .field("ws_keys_file", &self.ws_keys_file)
564            .field("allow_tcp_unauthenticated", &self.allow_tcp_unauthenticated)
565            .field(
566                "no_allow_tcp_unauthenticated",
567                &self.no_allow_tcp_unauthenticated,
568            )
569            .field("audit_log", &self.audit_log)
570            .field("tz", &self.tz)
571            .field(
572                "client_sig_proactive_refresh",
573                &self.client_sig_proactive_refresh,
574            )
575            .field(
576                "client_sig_reactive_refresh",
577                &self.client_sig_reactive_refresh,
578            )
579            .field("history_kline_cloud_sync", &self.history_kline_cloud_sync);
580        debug.field(
581            "enable_v18_experimental_reads",
582            &self.enable_v18_experimental_reads,
583        );
584        debug.field("enable_v18_kline_patterns", &self.enable_v18_kline_patterns);
585        debug.field(
586            "enable_v18_kline_pattern_performance",
587            &self.enable_v18_kline_pattern_performance,
588        );
589        debug.field("enable_v18_stock_notes", &self.enable_v18_stock_notes);
590        debug.field(
591            "enable_v18_stock_note_writes",
592            &self.enable_v18_stock_note_writes,
593        );
594        debug.field("enable_moc_order", &self.enable_moc_order);
595        debug.field("enable_group_orders", &self.enable_group_orders);
596        debug.field("enable_algo_orders", &self.enable_algo_orders);
597        debug.field("enable_position_actions", &self.enable_position_actions);
598        debug.field("allow_all_position_clear", &self.allow_all_position_clear);
599
600        #[cfg(feature = "dev-flags")]
601        debug.field("inject_auth_failure_every", &self.inject_auth_failure_every);
602
603        debug.finish()
604    }
605}
606
607fn redact_debug_option(value: &Option<String>) -> String {
608    match value {
609        Some(value) => format!("<REDACTED len={}>", value.len()),
610        None => "<NONE>".to_string(),
611    }
612}
613
614#[cfg(test)]
615mod tests;