use std::sync::Arc;
use std::time::Duration;
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(other) => {
eprintln!(
"未知子命令 `{other}`。用法:\n ekapp # 桌面主窗\n ekapp add-account …\n ekapp list-accounts"
);
std::process::exit(2);
}
None => desktop_main(),
}
}
fn desktop_main() {
let engine = Arc::new(ek_core::main_engine::MainEngine::new(None));
let shared = Arc::new(ek_ui::UiShared::new());
ek_station::boot(
&engine,
&ek_ui::ui_kinds(&shared, true),
&ek_station::StationConfig {
plugins: vec!["ui".to_string(), "dm".to_string(), "oms".to_string()],
},
)
.expect("boot");
engine.start().expect("start");
let watcher = ek_ui::spawn_host_watcher(&engine, &shared);
let shared_restore = Arc::clone(&shared);
std::thread::spawn(move || {
let rows = shared_restore.restore_saved_accounts();
if rows.is_empty() {
println!("[ekapp] 账户档案清点:0 行(空白首启:窗口「账户面板」→「添加账户」)");
} else {
println!(
"[ekapp] 账户档案清点:恢复 {} 行(点『连接』即按存档起连)",
rows.len()
);
}
});
struct DesktopApp {
shared: Arc<ek_ui::UiShared>,
}
impl eframe::App for DesktopApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
ek_ui::render_body(ctx, &self.shared);
ctx.request_repaint_after(Duration::from_millis(500));
}
}
let opts = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_inner_size([1280.0, 860.0])
.with_title("ek-station 桌面"),
..Default::default()
};
let shared_main = Arc::clone(&shared);
let result = eframe::run_native(
"ek-station 桌面",
opts,
Box::new(|cc| {
ek_ui::apply_cjk_fonts(&cc.egui_ctx);
Ok(Box::new(DesktopApp {
shared: Arc::clone(&shared_main),
}) as Box<dyn eframe::App>)
}),
);
shared.request_stop();
let _ = watcher.join();
if let Err(e) = result {
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.* 账户段(空白首启)");
}
}