futucli/cli/commands/key.rs
1//! Key-management clap argument structs split from commands.rs.
2
3use std::path::PathBuf;
4
5use clap::Args;
6
7#[derive(Args)]
8pub struct GenKeyArgs {
9 /// 人读 ID(审计日志用,必须唯一)
10 #[arg(long)]
11 pub(crate) id: String,
12
13 /// 授予的 scope,逗号分隔(v1.4.34+ 补齐 admin;详见 --help 的
14 /// scopes 长说明)
15 #[arg(
16 long,
17 long_help = "\
18授予的 scope,逗号分隔。
19
20可选(v1.8.0+ 含 qot:write):
21
22 qot:read 行情读(订阅 / 报价 / K 线 / 摆盘)
23 qot:write 行情用户态写(自选股 / 到价提醒 / 期权异动提醒 / 股票备注)
24 acc:read 交易账户只读(资金 / 持仓 / 订单历史)
25 trade:simulate 模拟盘下单 / 改单 / 撤单
26 trade:real 实盘下单 / 改单 / 撤单(高风险)
27 trade:unlock 解锁交易(/api/unlock-trade / futu_unlock_trade)
28 admin v1.4.32+ daemon 生命周期:/api/admin/status /
29 /api/admin/reload / /api/admin/shutdown
30
31多个 scope 用逗号分隔,例如:
32 --scopes qot:read,acc:read
33 --scopes qot:read,trade:simulate
34 --scopes admin # daemon 运维专用 key"
35 )]
36 pub(crate) scopes: String,
37
38 /// keys.json 路径。默认(按 OS):macOS ~/Library/Application Support/futu/keys.json / Linux ~/.config/futu/keys.json / Windows %APPDATA%/futu/keys.json
39 #[arg(long)]
40 pub(crate) keys_file: Option<PathBuf>,
41
42 /// 过期时间: Nd / Nh / Nm 或 RFC3339(如 2026-12-31T23:59:59Z)
43 #[arg(long)]
44 pub(crate) expires: Option<String>,
45
46 /// 备注
47 #[arg(long)]
48 pub(crate) note: Option<String>,
49
50 /// 允许的市场,逗号分隔(如 HK,US)
51 #[arg(long)]
52 pub(crate) allowed_markets: Option<String>,
53
54 /// 允许的品种白名单,逗号分隔(MARKET.CODE 格式,如 HK.00700,HK.09988)
55 #[arg(long)]
56 pub(crate) allowed_symbols: Option<String>,
57
58 /// 单笔订单金额上限
59 #[arg(long)]
60 pub(crate) max_order_value: Option<f64>,
61
62 /// 单日累计金额上限
63 #[arg(long)]
64 pub(crate) max_daily_value: Option<f64>,
65
66 /// 允许交易的时间窗口(服务器本地时区)HH:MM-HH:MM,跨午夜如 22:00-04:00
67 #[arg(long)]
68 pub(crate) hours_window: Option<String>,
69
70 /// 每 60s 最多下单次数(滑动窗口速率限制)
71 ///
72 /// 单笔金额 / 日累计之外再加一层防 spray-and-pray:
73 /// 假设攻击者拿到一把 `max_order_value = 50` 的 key,没速率限制的话
74 /// 一分钟能刷 N 千单;加了这个就被钳制在 `max_orders_per_minute` 内。
75 #[arg(long)]
76 pub(crate) max_orders_per_minute: Option<u32>,
77
78 /// 允许的交易方向白名单,逗号分隔
79 ///
80 /// 典型用法:`--allowed-trd-sides SELL` → 只允许平仓 bot 卖;
81 /// 值:`BUY` / `SELL` / `SELL_SHORT` / `BUY_BACK`(大小写敏感)。
82 /// 改单 / 撤单路径不带 side,不受此限制(避免误伤运维操作)。
83 #[arg(long)]
84 pub(crate) allowed_trd_sides: Option<String>,
85
86 /// v1.4.35: 允许的 acc_id 白名单,逗号分隔(如 `10001,10002`)
87 ///
88 /// **operational safety**:该 key 只能对这些 acc_id 发 trade / unlock / query
89 /// 操作;超出列表的 acc_id 直接 403。None / 空 → 不限(向后兼容)。
90 ///
91 /// 典型用法(多 agent 隔离):
92 /// `--allowed-acc-ids 10001,10002` → bot-A 只能动 10001/10002
93 /// `--allowed-acc-ids 10003` → bot-B 只能动 10003
94 ///
95 /// 注意:这是 **operational** 隔离(防 agent bug / LLM 幻觉 / key 泄露
96 /// 爆炸半径),对**纯现金策略**用户实质等同财务隔离。真·财务隔离(融资 /
97 /// 期权组合 / 跨品种保证金)需要多 union card,见 CLAUDE.md 隔离层级。
98 #[arg(long)]
99 pub(crate) allowed_acc_ids: Option<String>,
100
101 /// v1.4.103 (B10): per-key card_num 白名单, 逗号分隔.
102 ///
103 /// 接受 **4 位 suffix** (App 显示的 "保证金综合账户(`<card-suffix>`)"
104 /// 末 4 位) 或 **16 位完整 card_num** (e.g. `<full-card-num>`).
105 /// 示例为 synthetic placeholder, 不是真实账户信息. daemon 启动后通过
106 /// GetAccList **resolve** → **合并**进 `allowed_acc_ids` (内部仍用 u64).
107 ///
108 /// **设计动机**: App 用户看到的是 card_num, 不是内部 `acc_id`. 直接
109 /// 写 4 位 / 16 位避免先调 /api/accounts 拿映射.
110 ///
111 /// 典型用法 (与 --allowed-acc-ids 等价但用 App 可见的号码):
112 /// --allowed-card-nums <card-suffix> → 末 4 位匹配的账户
113 /// --allowed-card-nums <full-card-num> → 完整 card_num
114 /// --allowed-card-nums <suffix-A>,<suffix-B> → 多个 (e.g. HK + US 各一)
115 ///
116 /// 多 suffix 撞 (短 4 位 match 多账户) → daemon 启动 log warn + skip
117 /// (loud, 不静默接受). 找不到 → log warn + skip (cache load 后可补).
118 #[arg(long)]
119 pub(crate) allowed_card_nums: Option<String>,
120
121 /// 将 key 绑定到本机(软绑定:读 machine-id 计算指纹)
122 ///
123 /// 设置后 key 只能在本机使用;复制到别的机器会被拒。
124 /// 可以和 `--bind-machines` 同时用来同时允许多台机器。
125 #[arg(long)]
126 pub(crate) bind_this_machine: bool,
127
128 /// 追加绑定其他机器(需要对方先跑 `futucli machine-id --for-key <id>` 拿指纹)
129 ///
130 /// 逗号分隔的 64 位 hex 指纹列表,如:
131 /// --bind-machines aabb...,ccdd...
132 #[arg(long)]
133 pub(crate) bind_machines: Option<String>,
134}
135
136#[derive(Args)]
137pub struct BindKeyArgs {
138 /// 要编辑的 key id
139 #[arg(long)]
140 pub(crate) id: String,
141
142 /// keys.json 路径。默认(按 OS):macOS ~/Library/Application Support/futu/keys.json / Linux ~/.config/futu/keys.json / Windows %APPDATA%/futu/keys.json
143 #[arg(long)]
144 pub(crate) keys_file: Option<PathBuf>,
145
146 /// 追加本机指纹
147 #[arg(long)]
148 pub(crate) this_machine: bool,
149
150 /// 追加指定指纹,逗号分隔的 64 位 hex
151 #[arg(long)]
152 pub(crate) machines: Option<String>,
153
154 /// 替换模式:用新指纹覆盖整个白名单(默认是追加)
155 #[arg(long, conflicts_with_all = &["clear", "freeze"])]
156 pub(crate) replace: bool,
157
158 /// 清除绑定:把 allowed_machines 置为 None(等同于未启用绑定)
159 #[arg(long, conflicts_with_all = &["freeze", "replace", "this_machine", "machines"])]
160 pub(crate) clear: bool,
161
162 /// 冻结:把 allowed_machines 置为 [](任何机器都不过)
163 #[arg(long, conflicts_with_all = &["clear", "replace", "this_machine", "machines"])]
164 pub(crate) freeze: bool,
165}
166
167#[derive(Args)]
168pub struct MachineIdArgs {
169 /// 要生成指纹的 key id(和签发方的 `gen-key --id` 保持一致)
170 #[arg(long)]
171 pub(crate) for_key: Option<String>,
172}
173
174#[derive(Args)]
175pub struct ListKeysArgs {
176 /// keys.json 路径。默认(按 OS):macOS ~/Library/Application Support/futu/keys.json / Linux ~/.config/futu/keys.json / Windows %APPDATA%/futu/keys.json
177 #[arg(long)]
178 pub(crate) keys_file: Option<PathBuf>,
179
180 /// 输出 JSON 数组而不是短表格
181 ///
182 /// JSON 字段含完整 KeyRecord (hash / created_at / allowed_machines /
183 /// allowed_acc_ids / allowed_card_nums / allowed_symbols / hours_window
184 /// 等) 适合脚本 / agent 读取. 空 keys.json 输出 `[]`.
185 #[arg(long)]
186 pub(crate) json: bool,
187}
188
189#[derive(Args)]
190pub struct RevokeKeyArgs {
191 /// 要吊销的 key id
192 #[arg(long)]
193 pub(crate) id: String,
194
195 /// keys.json 路径。默认(按 OS):macOS ~/Library/Application Support/futu/keys.json / Linux ~/.config/futu/keys.json / Windows %APPDATA%/futu/keys.json
196 #[arg(long)]
197 pub(crate) keys_file: Option<PathBuf>,
198
199 /// 确认吊销(未加 --yes 时仅 dry-run)
200 #[arg(long)]
201 pub(crate) yes: bool,
202}
203
204#[derive(Args)]
205pub struct UnlockTradeArgs {
206 /// 锁回交易(清除 gateway 侧 cipher 缓存)
207 #[arg(long)]
208 pub(crate) lock: bool,
209
210 /// 从 stdin 读一行作为密码(脚本管道友好,覆盖环境变量 / tty prompt)
211 #[arg(long)]
212 pub(crate) from_stdin: bool,
213
214 /// 交易密码所属登录账号,用于读取账号级 OS keychain 条目。
215 ///
216 /// 对应 `futucli set-trade-pwd --account <login-account>` 写入的
217 /// `trade-password.<login-account>`。未传时会依次看
218 /// `FUTU_TRADE_PWD_ACCOUNT` / `FUTU_ACCOUNT`;仍找不到时再读 legacy
219 /// 全局 keychain 条目或交互式 prompt。
220 #[arg(long, value_name = "LOGIN_ACCOUNT")]
221 pub(crate) trade_pwd_account: Option<String>,
222
223 /// OTP / 令牌动态密码明文(v1.4.31+,2FA 用户必填)
224 ///
225 /// 若账号开启了"令牌动态密码"二次验证:首次 unlock 会收到 daemon 提示
226 /// `需要令牌动态密码`,再次调用带此参数即可。无 2FA 账号留空。
227 #[arg(long)]
228 pub(crate) otp: Option<String>,
229
230 /// 只解锁该券商下的账户。不传则解锁所有 broker(默认,向后兼容)。
231 /// 对齐 C++ OpenD 的 per-broker unlock 语义。
232 ///
233 /// 接受 3 种形式(v1.4.34 验证数字和别名确实能跑;clap 默认报错
234 /// 消息只列官方名,用 `--help` 看详细用法):官方名、1-7 数字、短别名。
235 #[arg(
236 long,
237 value_enum,
238 value_name = "FIRM",
239 long_help = "\
240只解锁该券商下的账户。不传则解锁所有 broker(默认,向后兼容)。
241
242接受 3 种形式(等价):
243 1. 官方名:FutuHK FutuUS FutuSG FutuAU FutuCA FutuMY FutuJP
244 2. 数字: 1 2 3 4 5 6 7
245 (HK) (US) (SG) (AU) (CA) (MY) (JP)
246 3. 短别名:hk / us / sg / au / ca / my / jp
247 (moomoo 账号 = us;其他别名 futu-hk / futu-us / mm 等)
248
249示例:
250 --security-firm 1
251 --security-firm hk
252 --security-firm FutuHK
253 --security-firm moomoo # 等价 --security-firm us"
254 )]
255 pub(crate) security_firm: Option<crate::cmd::unlock::SecurityFirmArg>,
256
257 /// v1.4.34+ 只解锁指定 acc_id 列表(逗号分隔)。和 `--security-firm`
258 /// 同时传时取**交集**。解决同 broker 内影子账户拖垮主账户的场景——
259 /// 显式传主账户 acc_id,影子账户不进请求。
260 #[arg(long, value_delimiter = ',', value_name = "ACC_ID[,ACC_ID]...")]
261 pub(crate) acc_ids: Vec<u64>,
262}
263
264#[derive(Args)]
265pub struct SetTradePwdArgs {
266 /// 登录账号(牛牛号 / 手机号 / 邮箱)—— 和 futu-opend --login-account 完全一致
267 #[arg(long)]
268 pub(crate) account: String,
269
270 /// 从 stdin 读一行密码(脚本/CI 友好,不做二次确认)
271 #[arg(long)]
272 pub(crate) from_stdin: bool,
273}
274
275#[derive(Args)]
276pub struct ClearTradePwdArgs {
277 #[arg(long)]
278 pub(crate) account: String,
279}
280
281#[derive(Args)]
282pub struct SetLoginPwdArgs {
283 /// 账号(牛牛号 / 手机号 / 邮箱)—— 和 futu-opend --login-account 完全一致
284 #[arg(long)]
285 pub(crate) account: String,
286
287 /// 从 stdin 读一行密码(脚本/CI 友好,不做二次确认)
288 #[arg(long)]
289 pub(crate) from_stdin: bool,
290}
291
292#[derive(Args)]
293pub struct ClearLoginPwdArgs {
294 #[arg(long)]
295 pub(crate) account: String,
296}