Skip to main content

ek_ui/
appconfig.rs

1//! 无界面一次性写档共用面(0.9.0;BD-APP-2/4):`ekapp add-account` CLI 与
2//! **等效判据**(tests/ui_app_contract.rs)共用同一构造函数 —— 判据钉的
3//! 就是「CLI 写出的段,喂给**真受理方法** `apply_config` 必被全采纳」。
4//!
5//! 段形对齐真实受理面(`ek-cta::dump_config`):明文键 + `secret://` 引用行,
6//! 密码**值**先进密库(Windows 凭据管理器)再出引用;值永不落文件。
7//! 形态常量(`gw_td_password`/`gw_md_password`)与 ek-cta 单源同拼写 —— 改名
8//! 必须双动(判据钉 willcatch 漂移)。
9
10use ek_core::{Field, SectionMap};
11
12/// CLI 一次提交的账户档要素(密码不入结构面:与本进程 stdin 通道分开)。
13#[derive(Debug, Clone)]
14pub struct AcctSpec {
15    pub broker_id: String,
16    pub user: String,
17    pub td_addr: String,
18    pub md_addr: String,
19    pub app_id: String,
20    pub auth_code: String,
21    /// 收录产品类(单字符 CTP 码逗号分隔;空 = 不写档,交给缺省)。
22    pub classes: String,
23    pub md_user: String,
24    /// 行情侧独立密码(缺省 = None → 段里不出 md_password,行情会话回落 td 凭据)。
25    pub md_password: Option<String>,
26}
27
28/// 派生注册名(ek-cta 单源;形 `ctp_<broker>_<账号>`)。
29pub fn derived_name(spec: &AcctSpec) -> Result<String, String> {
30    ek_ctp::derived_gateway_name(&spec.broker_id, &spec.user).ok_or_else(|| {
31        format!(
32            "派生注册名失败(broker={:?} 账号={:?}):字符不合名纪律",
33            spec.broker_id, spec.user
34        )
35    })
36}
37
38/// 组段(校验 + 秘面登记 + 段构造;**store 由调用方给**,判据用临时根,
39/// CLI 用生态缺省根 —— 同一函数,一字不差)。
40///
41/// 返回:网关派生名;调用方即将其拼 `ns = "ctp.<gw>"` 存档。
42///
43/// # Errors
44/// 派生名/必填项/product_classes 形制不合;密库登记失败(缺后端面)。
45pub fn write_account(
46    store: &ek_cfg::Store,
47    spec: &AcctSpec,
48    password: &str,
49) -> Result<String, String> {
50    if spec.broker_id.is_empty()
51        || spec.user.is_empty()
52        || spec.td_addr.is_empty()
53        || spec.app_id.is_empty()
54        || spec.auth_code.is_empty()
55    {
56        return Err("缺必填要素(broker/user/td 前置/app_id/auth_code)".to_string());
57    }
58    if password.trim().is_empty() {
59        return Err("密码为空:档案不接受无密码账户(受理校验同款)".to_string());
60    }
61    let gw = derived_name(spec)?;
62    let ns = format!("ctp.{gw}");
63    ek_cfg::secret::set(
64        &ns,
65        "gw_td_password",
66        ek_cfg::SecretValue::from_bytes(password.trim().to_owned()),
67    )
68    .map_err(|e| format!("密码登记密库失败: {e:?}"))?;
69    let mut map = SectionMap::new(1)
70        .set("broker_id", Field::plain(spec.broker_id.trim()))
71        .set("td_addr", Field::plain(spec.td_addr.trim()))
72        .set("td_user", Field::plain(spec.user.trim()))
73        .set("td_app_id", Field::plain(spec.app_id.trim()))
74        .set("td_auth_code", Field::plain(spec.auth_code.trim()))
75        .set("td_password", Field::secret("gw_td_password"));
76    if !spec.md_addr.trim().is_empty() {
77        map = map.set("md_addr", Field::plain(spec.md_addr.trim()));
78        if !spec.md_user.trim().is_empty() {
79            map = map.set("md_user", Field::plain(spec.md_user.trim()));
80        }
81        if let Some(mp) = spec.md_password.as_deref() {
82            let mp = mp.trim();
83            if !mp.is_empty() {
84                ek_cfg::secret::set(&ns, "gw_md_password", ek_cfg::SecretValue::from_bytes(mp))
85                    .map_err(|e| format!("行情密码登记密库失败: {e:?}"))?;
86                map = map.set("md_password", Field::secret("gw_md_password"));
87            }
88        }
89    }
90    let classes = spec.classes.trim();
91    if !classes.is_empty() {
92        for c in classes.split(',').map(str::trim) {
93            if c.chars().count() != 1 {
94                return Err(format!(
95                    "product_classes 项 {c:?} 非单字符 CTP 码(D-PC-FILTER)"
96                ));
97            }
98        }
99        map = map.set("product_classes", Field::plain(classes));
100    }
101    store
102        .store(&ns, &map)
103        .map_err(|e| format!("档案落盘失败: {e:?}"))?;
104    Ok(gw)
105}