Skip to main content

futu_opend/
hints.rs

1//! v1.4.110 P1-2: auth error hint messages (抽自 main.rs lines 1416-1604).
2//!
3//! v1.4.97 P1-D: error message classification + user-facing recovery hints.
4//! ret_type=2/15/21/45 等错误码 → 不同恢复建议 (device_id 重置 / 反刷 sleep /
5//! 防火墙开放 9595 / 平台 + 客户端版本切换).
6
7#![allow(unused_imports)]
8
9use std::io::IsTerminal;
10
11use crate::config::RuntimeConfig;
12
13/// 解析 auth error string + 给出对应 recovery hint (打到 stderr + tracing).
14///
15/// 调用方在 `Err(e) => { ... }` match arm 内调用此 fn (替代 inline if-chain).
16/// 之后调用方按需做 `if config.setup_only { return Err(...) }` 控制流.
17pub fn print_auth_error_hint(e: &futu_core::error::FutuError, config: &RuntimeConfig) {
18    let err_str = format!("{e}");
19    let hint_21 = err_str.contains("ret_type=21") || err_str.contains("验证码错");
20    let hint_15 = err_str.contains("ret_type=15") || err_str.contains("长时间没有登录");
21    // v1.4.19:识别"所有 Platform IP 都连不上"—— 几乎一定是本机
22    // 出站防火墙挡了 9595 端口(腾讯云 Lighthouse 默认不放 9595;
23    // 企业内网 / 云厂商安全组也常见)
24    let hint_firewall =
25        err_str.contains("Platform IP pool exhausted") || err_str.contains("timed out");
26    // v1.4.92 D1: 更多细分 hint —— error_code=2 / 45 / 网络 DNS / SMS 非交互
27    let hint_2 = err_str.contains("ret_type=2,")
28        || err_str.contains("ret_type=2 ")
29        || err_str.contains("账号密码不匹配");
30    let hint_45 = err_str.contains("ret_type=45") || err_str.contains("当前应用版本过低");
31    let hint_dns = err_str.contains("dns error")
32        || err_str.contains("failed to lookup")
33        || err_str.contains("Name or service not known")
34        || err_str.contains("nodename nor servname");
35    // SMS 非交互:error 是 SMS 流程相关 + stdin 不是 TTY
36    let hint_sms_noninteractive = (err_str.contains("ret_type=20")
37        || err_str.contains("require_device_verify")
38        || err_str.contains("device_verify_sig"))
39        && !std::io::IsTerminal::is_terminal(&std::io::stdin())
40        && config.verify_code.is_none();
41    if hint_21 {
42        tracing::error!(
43            error = %e,
44            "gateway init failed: SMS code mismatch (ret_type=21). \
45             Device_id may be locked after multiple wrong codes — try \
46             `futu-opend --reset-device --setup-only ...` to regenerate \
47             and re-verify via SMS."
48        );
49        // v1.4.72 BUG-009 Fix 9b (eli v1.4.69 P1): setup-only + TTY
50        // 场景保持前台 wait,让用户读完错误 + 手动决定下一步(不要
51        // 立即退出让 supervisor 重启 → 重 POST /authority → 新 SMS
52        // 失效旧码 → 累计失败触发账户锁)。
53        //
54        // 判断条件:setup_only + stdin 是 tty(可交互)+ --verify-code
55        // 用过(说明这次 SMS 输错)。非交互 daemon (systemd / Docker)
56        // 保持旧行为 return Err → supervisor 决定重启策略。
57        if config.setup_only
58            && std::io::IsTerminal::is_terminal(&std::io::stdin())
59            && config.verify_code.is_some()
60        {
61            eprintln!();
62            eprintln!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
63            eprintln!("⚠️  v1.4.72 BUG-009 Fix 9b: SMS 验证码错 (ret_type=21)");
64            eprintln!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
65            eprintln!();
66            eprintln!("   daemon 不立即退出,让你有时间读完错误 + 决定下一步。");
67            eprintln!();
68            eprintln!("   下一步建议(不要盲目重跑 daemon,避免新 SMS + 账户锁):");
69            eprintln!("   1. 等 30 秒,等服务端限流过");
70            eprintln!("   2. 检查手机上最新收到的 SMS(可能有多条,用最新那条)");
71            eprintln!("   3. 重跑 daemon 带新码:");
72            eprintln!("      futu-opend --setup-only --verify-code <新 SMS 码> ...");
73            eprintln!();
74            eprintln!("   按 Enter 退出(Ctrl+C 也可)...");
75            let _ = std::io::stdin().read_line(&mut String::new());
76        }
77    } else if hint_15 {
78        // v1.4.74 A3 BUG-003 fix(eli v1.4.71 AI tester §4.2 Layer 2):
79        // error_code=15 原本用一大段 inline text 列 5 因,用户 skim 读
80        // 很难 discharge 每一条 cause。改为结构化 stderr 输出 + tracing
81        // log 保留引用;让用户能**按优先级逐条排查**。
82        eprintln!();
83        eprintln!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
84        eprintln!("⚠️  Gateway init failed: server returned ret_type=15");
85        eprintln!("    (\"请重新输入密码\" — 这是服务端状态机错乱的统称)");
86        eprintln!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
87        eprintln!();
88        eprintln!("这个错误有 **5 种可能根因**,按优先级逐条排查:");
89        eprintln!();
90        eprintln!("  [0] **有另一个 Futu OpenD 正在用这个账号**(v1.4.52 新增)");
91        eprintln!("      → 检查所有机器:`lsof -i :11111` + `ps aux | grep -i opend`");
92        eprintln!("      → 停掉另一个 OpenD(C++ 或 Rust),然后重试");
93        eprintln!();
94        eprintln!("  [1] **服务端反刷限流**(最常见,连续重启触发)");
95        eprintln!("      → 等 60 秒后再重试");
96        eprintln!();
97        eprintln!("  [2] **SMS 超限 + device_id 被封**(v1.4.57 新增)");
98        eprintln!("      → 等 3-5 分钟让限流过");
99        eprintln!("      → 手机上登录富途/moomoo App 一次清账号状态");
100        eprintln!("      → 重启 daemon 带 `--verify-code <CODE>` 避免 stdin 延迟");
101        eprintln!();
102        eprintln!("  [3] **device_id 毒化**(空 SMS 提交 / 长时间不用)");
103        eprintln!("      → 先试 `--reset-device --setup-only`");
104        eprintln!("      → 注意:破坏性操作,会触发二次 SMS 验证");
105        eprintln!();
106        eprintln!("  [4] **账号级风控 / 状态异常**(最难)");
107        eprintln!("      → 手机上登录富途/moomoo App 一次清账号状态");
108        eprintln!("      → 若账号从未在 App 登录激活,必须先激活");
109        eprintln!();
110        eprintln!("  建议试序:[0] → [1] → [2] → [3] → [4]");
111        eprintln!();
112        tracing::error!(
113            error = %e,
114            "ret_type=15 — see stderr for 5-cause diagnostic checklist"
115        );
116    } else if hint_firewall {
117        tracing::error!(
118            error = %e,
119            "gateway init failed: all Platform IPs are unreachable on port 9595. \
120             This is almost always an outbound firewall issue on your host, NOT \
121             a Futu server problem. Quick check: \
122             `timeout 5 bash -c '</dev/tcp/119.29.48.17/9595'` — if this also \
123             fails (it's Tencent Cloud in Guangzhou), your host is blocking \
124             outbound TCP 9595. Fix: open 9595 in your cloud security group \
125             (Tencent Cloud Lighthouse / CVM / AWS / Aliyun all default to \
126             blocking non-standard ports)."
127        );
128    } else if hint_2 {
129        // v1.4.92 D1: error_code=2 "账号密码不匹配"
130        // 真因 4 类(按出现概率排序):
131        //   1. 密码真错(最常见)
132        //   2. --platform 选错(auth.futunn.com 收 client_type=60 / 反之 → 直接拒)
133        //   3. account 拆区号错(+86-xxx 整串发 → 服务端按号码本体查 hash 不匹配)
134        //   4. 同号在 futunn / moomoo 各注册了一个,登错了那个
135        eprintln!();
136        eprintln!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
137        eprintln!("⚠️  ret_type=2 — 账号密码不匹配");
138        eprintln!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
139        eprintln!();
140        eprintln!("💡 Hint: 这个错误可能 4 种根因(按概率):");
141        eprintln!("  1. 密码真错 → 检查 --login-pwd 或 --login-pwd-md5");
142        eprintln!("  2. --platform 选错 → futunn 系账号必须 --platform futunn,");
143        eprintln!("     moomoo (US/SG/AU/JP/CA/MY) 系账号必须 --platform moomoo");
144        eprintln!("  3. 同号双注册 → futunn / moomoo 同手机号 / 邮箱可各注册一个,");
145        eprintln!("     检查 `futu-opend --login-account ... --platform <对侧>` 是否能登");
146        eprintln!("  4. 区号写错 → 手机号格式 +86-13900000000 (区号 + dash + 号码本体)");
147        eprintln!();
148        tracing::error!(error = %e, "ret_type=2 — see stderr 4-cause hint");
149    } else if hint_45 {
150        // v1.4.92 D1: error_code=45 "当前应用版本过低"
151        eprintln!();
152        eprintln!("⚠️  ret_type=45 — 当前应用版本过低");
153        eprintln!();
154        eprintln!("💡 Hint: 服务端对海外账号 (user_attribution != 1) 严格校验,");
155        eprintln!("        客户端版本号需 ≥ 800(X-Futu-Client-Version 字段)。");
156        eprintln!("        本 Rust daemon 使用 backend 可识别的 Rust 版本号 1030。");
157        eprintln!("        若仍报 45,可能是 backend 升级了最低版本要求,");
158        eprintln!("        升级 daemon: brew upgrade futuleaf/tap/futu-opend-rs");
159        eprintln!();
160        tracing::error!(error = %e, "ret_type=45 — version too low, see stderr hint");
161    } else if hint_dns {
162        // v1.4.92 D1: DNS 解析失败 (auth.futunn.com / auth.moomoo.com)
163        eprintln!();
164        eprintln!("⚠️  网络错: DNS 解析 auth server 失败");
165        eprintln!();
166        eprintln!("💡 Hint: 检查域名解析 + 网络连通性:");
167        eprintln!("  1. ping auth.futunn.com / ping auth.moomoo.com");
168        eprintln!("  2. 若公司 / 校园 VPN 限制了海外域名 → 切换 VPN 或开放 auth.* 解析");
169        eprintln!("  3. 中国大陆 ISP 偶尔抽风 auth.moomoo.com → 试 8.8.8.8 DNS");
170        eprintln!("  4. 检查 /etc/hosts 是否有错误的 auth server override");
171        eprintln!();
172        tracing::error!(error = %e, "DNS lookup failed for auth server");
173    } else if hint_sms_noninteractive {
174        // v1.4.92 D1: SMS 验证需要交互终端 + stdin 不是 TTY (nohup / docker -d / systemd)
175        eprintln!();
176        eprintln!("⚠️  SMS 验证码需要交互式终端 (stdin 不是 TTY)");
177        eprintln!();
178        eprintln!("💡 Hint: 后台 / 守护进程模式下 SMS 输入会立即返回空字符串,");
179        eprintln!("        毒化 device_id 触发 ret_type=15 / 21。");
180        eprintln!();
181        eprintln!("  正确部署姿势 (systemd / Docker):");
182        eprintln!("  1. 前台一次完成 SMS:");
183        eprintln!("     futu-opend --setup-only --login-account X --login-pwd Y \\");
184        eprintln!("                --platform <futunn|moomoo>");
185        eprintln!("     (或用 --verify-code <已收到的码> 跳过 stdin)");
186        eprintln!("  2. 完成后凭据写入 ~/.futu-opend-rs/,daemon 后续自动跳 SMS");
187        eprintln!("  3. 启动后台 daemon:");
188        eprintln!("     futu-opend --login-account X --login-pwd Y --platform ...");
189        eprintln!();
190        tracing::error!(error = %e, "SMS required but stdin is not a TTY");
191    } else {
192        tracing::error!(error = %e, "gateway initialization failed, starting in offline mode");
193    }
194}