fn main() {
let args: Vec<String> = std::env::args().skip(1).collect();
match args.first().map(|s| s.as_str()) {
Some("add-account") => {
if let Err(e) = cli_add_account(&args[1..]) {
eprintln!("add-account 失败: {e}");
std::process::exit(1);
}
}
Some("list-accounts") => cli_list_accounts(),
Some("tq-secret") => {
if let Err(e) = cli_tq_secret() {
eprintln!("tq-secret 失败: {e}");
std::process::exit(1);
}
}
Some(other) => {
eprintln!(
"未知子命令 `{other}`。用法:\n ekapp # 桌面主窗\n ekapp add-account …\n ekapp list-accounts"
);
std::process::exit(2);
}
None => desktop_main(),
}
}
fn desktop_main() {
if let Err(e) = ek_ui::runtime::run_desktop() {
eprintln!("ekapp: {e}");
std::process::exit(1);
}
}
fn usage() -> String {
"用法:
ekapp add-account --broker 9999 --user 278153 \
--td tcp://182.254.243.31:30001 --md tcp://182.254.243.31:30011 \
--app-id simnow_client_test --auth-code 0000000000000000 \
[--classes 1(只期货;现时唯一合法档)] \
[--md-user u --md-password-stdin]
密码在提示后从 stdin 读一行(不进 argv;进程行能被旁人看到)。"
.to_string()
}
fn cli_add_account(rest: &[String]) -> Result<(), String> {
let mut a = ek_ui::appconfig::AcctSpec {
broker_id: String::new(),
user: String::new(),
td_addr: String::new(),
md_addr: String::new(),
app_id: String::new(),
auth_code: String::new(),
classes: "1".to_string(),
md_user: String::new(),
md_password: None,
};
let mut i = 0;
while i < rest.len() {
let flag = rest[i].as_str();
let need = |i: &mut usize, rest: &[String], what: &str| -> Result<String, String> {
*i += 1;
rest.get(*i)
.cloned()
.ok_or_else(|| format!("{what} 缺值(参数后要跟值)"))
};
match flag {
"--broker" => a.broker_id = need(&mut i, rest, "--broker")?,
"--user" => a.user = need(&mut i, rest, "--user")?,
"--td" => a.td_addr = need(&mut i, rest, "--td")?,
"--md" => a.md_addr = need(&mut i, rest, "--md")?,
"--app-id" => a.app_id = need(&mut i, rest, "--app-id")?,
"--auth-code" => a.auth_code = need(&mut i, rest, "--auth-code")?,
"--classes" => a.classes = need(&mut i, rest, "--classes")?,
"--md-user" => a.md_user = need(&mut i, rest, "--md-user")?,
"--md-password-stdin" => a.md_password = Some(String::new()),
other => {
return Err(format!("未知参数 `{other}`\n{}", usage()));
}
}
i += 1;
}
let guid = ek_ui::appconfig::derived_name(&a)?;
eprint!("[ekapp] 输入 {guid} 的 CTP 密码(stdin 单行): ");
let mut buf = String::new();
std::io::stdin()
.read_line(&mut buf)
.map_err(|e| format!("stdin 读失败: {e}"))?;
let password = buf.trim().to_string();
if password.is_empty() {
return Err("密码为空:档案不接受无密码账户(受理校验同款)".to_string());
}
if a.md_password.is_some() {
eprint!("[ekapp] 输入 {guid} 的行情密码(stdin 单行;留空回落 td 密码): ");
let mut mbuf = String::new();
std::io::stdin()
.read_line(&mut mbuf)
.map_err(|e| format!("stdin 读失败: {e}"))?;
let mp = mbuf.trim().to_string();
a.md_password = (!mp.is_empty()).then_some(mp);
}
let store = ek_cfg::Store::eco().map_err(|e| format!("配置根不可用: {e:?}"))?;
let gw = ek_ui::appconfig::write_account(&store, &a, &password)?;
let md_txt = if a.md_addr.is_empty() {
"—未配—".to_string()
} else {
a.md_addr.clone()
};
println!(
"[ekapp] 已写档 ctp.{gw}.ini(td={td};md={md};密码=密库引用,值不落盘)。
下一步:启动 `ekapp`(桌面)点该行的『连接』;或起 `ek-station` 守护(自动连接先在桌面行上勾)。",
td = a.td_addr,
md = md_txt
);
Ok(())
}
fn cli_list_accounts() {
let store = match ek_cfg::Store::eco() {
Ok(s) => s,
Err(e) => {
eprintln!("[ekapp] 配置根不可用: {e:?}");
std::process::exit(1);
}
};
let nss = store.list_namespaces().unwrap_or_default();
let mut shown = 0usize;
for ns in &nss {
if !ns.starts_with("ctp.") {
continue;
}
shown += 1;
match store.load(ns) {
Ok(ek_cfg::LoadOutcome::Loaded { map, .. }) => {
let p = |k: &str| map.get_plain(k).unwrap_or("—").to_string();
let has_pw = map
.get_secret_key("td_password")
.is_some_and(|k| !k.is_empty());
println!(
"{}\n 账号 {} @ broker {}\n td = {}\n md = {}\n 凭据 = {} | 收录 = {}",
ns,
p("td_user"),
p("broker_id"),
p("td_addr"),
p("md_addr"),
if has_pw {
"密库引用 ✓"
} else {
"缺密码引用"
},
p("product_classes"),
);
}
Ok(_) => println!("{ns}(一段都读不出;该档恐已坏)"),
Err(e) => println!("{ns}(读失败:{e:?})"),
}
}
if shown == 0 {
println!("[ekapp] 档案清点:无 ctp.* 账户段(空白首启)");
}
}
fn read_stdin_line() -> Option<String> {
use std::io::BufRead;
let mut s = String::new();
let n = std::io::stdin()
.lock()
.read_line(&mut s)
.ok()
.filter(|n| *n > 0)?;
let _ = n;
Some(s)
}
fn cli_tq_secret() -> Result<(), String> {
use ek_cfg::Store;
use ek_cfg::secret::{self, SecretValue};
use ek_core::section::{Field, SectionMap};
println!("输入天勤 user(回车):");
let user = read_stdin_line()
.ok_or("stdin 缺 user 行")?
.trim()
.to_string();
println!("输入天勤 password(回车,不回显):");
let pass = read_stdin_line()
.ok_or("stdin 缺 password 行")?
.trim()
.to_string();
if user.is_empty() || pass.is_empty() {
return Err("user/password 有空行,拒收".to_string());
}
secret::set(
"tq",
"user",
SecretValue::from_bytes(user.as_bytes().to_vec()),
)
.map_err(|e| format!("密库写入 user 失败:{e}"))?;
secret::set(
"tq",
"password",
SecretValue::from_bytes(pass.as_bytes().to_vec()),
)
.map_err(|e| format!("密库写入 password 失败:{e}"))?;
let store = Store::eco().map_err(|e| format!("档案托底根不可用:{e}"))?;
let map = SectionMap::new(1)
.set("user", Field::secret("user"))
.set("password", Field::secret("password"));
store
.store("tq", &map)
.map_err(|e| format!("tq 段写入失败:{e}"))?;
println!(
"tq 凭据已入密库:user={} password={};档案段 = {}(两条引用行)",
secret::get("tq", "user").is_some(),
secret::get("tq", "password").is_some(),
store.path_of("tq").unwrap_or_default().display()
);
Ok(())
}