# ek-station DESIGN
设计与决策记录(docs/ek-ui-station-plan.md 的 S1a 落地面;扎钉后不放)。
## BD(边界钉)
- **BD-U1 svctl.stop ≅ svctl.remove**:ek-core 0.3.0 无"停而不摘表"导出
(`close_one` 是私有方法,公开面只有 `close_all`/`remove_plugin`)。
运行期下线 = remove(五步闭停 + 摘表 + `ePlugDown` 广播);重启用 start
(宿主 `add_plugin` 在 start 后即时 `launch_one` 上线 + `ePlugUp` 广播)。
测试 c0 全链钉死。
- **BD-U2 工厂 build 用 `Arc<dyn Fn>` 而非 `fn` 指针**:测试探针需要捕获
共享槽;S3 参数化配置也吃这个签名。行动面不变(零参调用)。
- **BD-U3 headless 的 stdin 退出策略**:Windows 控制台无 std 信号面,
`SetConsoleCtrlHandler` 需三方/unsafe → v1 记录:`q`/`quit` 行 = 优雅退出。
~~管道 EOF = 优雅退出~~ **0.1.4 文档纠错(T2 t2g 实测推翻)**:EOF 与 stdin
不可用(detached)同归**常驻 keep-alive**——launcher 的 spawn 用 stdin=null,
子进程起手即 EOF,EOF 若走优雅退则 launcher 协议即碎。五步闭停在 `q` 与
窗口关闭(ek-ui 侧)路径生效;常驻进程由进程外 kill 终止。
**0.2.12 补两件事(审核 P12 的出口,不改默认)**:① 这一档从此有测试钉 ——
`headless_exit.rs` 的 t4 钉「EOF 之后进程必须还活着」、t5 钉「`--exit-on-eof`
走优雅闭停」。此前四个测试全喂 `q\n`,EOF 分支从未被跑过:谁把它改成优雅退,
四个测试照绿而 launcher 协议当场碎,0.1.4 那条实测纠正从此没有守卫。
② 新增 `--exit-on-eof` 开关(缺省**不开**,常驻仍是默认):手跑 `ek-station < NUL`
或在管道里跑时,缺省留下的是持有 CTP 会话的孤儿进程,这条给一个优雅出口。
常驻本身仍无超时位、无退出条件 —— 0.2.11 审核把它记为按裁定保留,不加 `--keep-alive`。
- **BD-U4 `--from` 清单延迟校验**:行格式错即时报(exit 2),未知工厂名
延迟到 boot 单一硬报错点(exit 1)——避免两套重名/存在性校验的重复面。
- **BD-U5 构造期身份(多账号面已改写)**:原 v1 口径"工厂名 == 插件注册名
(dm/oms/ctp 各一实例)"**已废**。`StartReq.name` 非空 = 以该注册名建实例
(工厂须登记 `PlugKind.build_named`;ctp 已登记,dm/oms 未登记 → 显式回拒,
不静默退回零参构造);空 = 旧行为(工厂名即注册名,单实例零回归)。
`StartRes.Started.name` 一律回带**插件自报的 `name()`**,不再假设工厂名。
网关注册名由 (broker, 账号) 派生(`ek_ctp::derived_gateway_name`),
**不进配置文件**。
- **BD-U6 管制面不可自摘(0.2.11,应用级审核 P1)**:`svctl.remove(name = "svctl")`
一律回 `RemoveRes::Rejected`。两条理由:① `Supervisor` 是全系统唯一持
`Arc<MainEngine>` 的插件,而工厂表(`default_kinds`/ek-ui 的 `ui_kinds`)都不登记
"svctl" 这一项 —— 摘掉之后 `svctl.start` 也只会回 `unknown factory: svctl`,
启停面**没有恢复入口**(单向门);② 请求在本插件自己的 worker 里处理,摘自己
= `close_one` 等自己排空,只能走到 ek-core 那 2 秒超时的 detach 分支,回包也发不出。
钉在测试 c0 step5/6(拒 + 拒后 svctl 仍在场)。观测面 `ui` 的同一道闸长在 ek-ui 侧
(0.4.3):那条指令正是 ui 自己的 worker 在执行,运维窗不给它画移除钮。
- **BD-U7 回执 `name` 栏只放原请求名(0.2.11,审核 P2)**:0.2.10 之前非
`UnknownName` 的宿主错被塞进 `NoSuchName { name: format!("{e:?}") }`,而该栏文档
写明是「原请求名」—— 消费侧(ek-ui 日志)会把错误文本当名字打,原请求名丢失。
现:新增 `Rejected { name, reason }`,名字一律照请求回填,错因只进 `reason`。
## 契约面(wire.rs)
`PlugRow { name, version, state(Debug 形状), deps, spilled, panics }`;
`StartRes/RemoveRes` 用**业务回执枚举**(Started/Rejected、Removed/NoSuchName/Rejected)
而非 `ReplyErr` 承载业务错 —— 回执在总线/测试里可读可断言。
## BD-CFG 配置托底钉清单(2026-10-25;用户裁定「软集中」,单源全文)
> 配套修订:ek-ui DESIGN 的 BD-UI-19 升第三版「零配置**文件**」——宿主/界面
> 不代读、不代转发、不代解释配置值;宿主唯一配置动作 = 载入两步直调(搬运)。
> 版本锚:ek-core 0.4.0(契约) / ek-cfg 0.1.0(库) / 本 crate 0.3.0(加载单点+登记)。
- **BD-CFG-1(依赖真相)**:各模块对托底的依赖 = Cargo 里 `ek-cfg`(库,编译期);
`cfg` 插件实例 = 库在总线上的收件形貌,**非关卡**。apply 走宿主直调、
秘值走库函数、save 有库级兜底。
- **BD-CFG-2(契约单份)**:`SectionMap`/`Field`/`Configurable`/`ApplyReport`
住 ek-core,全链一份(0.2.15「两份同名类型 = 运行时静默断链」判据适用)。
ek-core 只含纯数据与 trait,**不含格式解析**。
- **BD-CFG-3(格式钉)**:行式键值对(pickfile 同源风格):`key = value`、
`#` 注释、列表逗号分列、首行 `rev = 1`;未知 key = 警告跳过不炸;
解析/序列化在 ek-cfg;文件位置 `${config_dir}/ek/cfg/<ns>.ini` 经
`dirs::config_dir()`(Win `%APPDATA%` / Linux `~/.config`,一码两平台)。
- **BD-CFG-4(落盘钉)**:原子三步写 `.new` → 旧文改 `.bak` → 新文转正 → 清
`.bak`(std rename 在 Windows 不覆盖已存在目标,必须三步);读序
current → `.bak` → Err 上抛。
- **BD-CFG-5(单写者)**:每 ns 一个写者;字段级并发 = 插件自身 worker 串行。
- **BD-CFG-6(生效三档)**:Live 即时 / NextUse 下次使用 / NextStart =
`svctl.remove`→`start`(插件级重启,秒级)——**配置永不要求重启整个应用**。
载入路径 `ApplyReport.deferred` 显形;运行期修改的重启生效提示走各插件
自己的 configure 回执;`cfg.save` 回执只报落盘成败。
- **BD-CFG-7(文件是影子)**:运行中插件状态唯一权威在内存;手改文件不热生效;
持久面只认「最后一次受理成功」——不认退出前状态、不做退出兜底写。
- **BD-CFG-8(save 触发)**:仅「配置面受理成功」一个触发时机,不受连接结果
牵引(受理 ≠ 连接成功;网络失败不阻止持久化用户意图)。
- **BD-CFG-9(缺席判定)**:`ask` 超时 ≡ `NoRoute` ≡ 实例缺席,同闸单分支,
不重试不区分。
- **BD-CFG-10(软集中/禁拉起)**:cfg 实例缺席时,调用方**直调同一库函数**
(原子纪律/秘面分流全在库;兜底不是劣化实现);**禁止任何插件为写配置而
临时拉起 cfg 插件**(写盘付不起一个插件生命周期,且管制面唯一在 svctl)。
- **BD-CFG-11(审计线)**:兜底成功必留一行 eLog「cfg 缺席,已直写落盘」,
每插件每会话一次性(内部 Mutex<bool>),禁静默。
- **BD-CFG-12(秘面落地)**:明面文件**零秘值**,只存引用串
`secret://ek/<ns>/<key>`;真值在系统凭据库或注入环境变量。
- **BD-CFG-13(秘键单点)**:`ek_cfg::secret::{set,get,delete,ref_of}` 统一收
`(ns, key)`,条目名 `ek/<ns>/<key>` 与引用串**单源生成**,永不发散;
ns 唯一性天然隔离多插件。
- **BD-CFG-14(受理三行序)**:配置受理成功的插件固定顺序
`secret::set(...)` → `dump_config()`(秘面出引用)→ `ask("cfg.save", …)`。
- **BD-CFG-15(dump 口径)**:dump 只输出要持久的字段——不输出 = 不持久
(临时态字段天然豁免);秘面字段在 dump 里只有引用形态。
- **BD-CFG-16(解析时机)**:`apply_config` 只回灌引用,不解析秘值;取秘由
插件在**自己的时机**(典型 = on_start 后自发连接前)做;
**宿主管理线程禁止调 `secret::get`**(keyring/DBus 可秒级阻塞)。
- **BD-CFG-17(秘库无枚举)**:keyring 不提供跨平台枚举——「已存/未存」以
明面文件的引用串为准;孤儿秘条容忍(幂等覆盖,残留无语义影响),
**不做全局 GC**;`cfg.delete(ns)` 只清文件可见键。
- **BD-CFG-18(脱敏)**:秘值类型 `SecretValue(Zeroizing)` Drop 即抹内存;
含秘结构**手工 Debug**(只打 `******`),禁 `derive(Debug)`;
eLog/总线广播/`cfg.list` 回包**永零秘值**(测试机器扫描钉死)。
- **BD-CFG-19(信任边界)**:同机同用户态攻击者(root/管理员/调试器)不在
防御范围;承诺面 = 静态落盘 / 日志 / 总线**零明文**。
- **BD-CFG-20(cfg 自身)**:`as_configurable() = None`(托底者不吃托底);
路径 = 构造期装配参数(`Store`,生态缺省 `config_dir()/ek/cfg`,工厂
`eco_or_local` 回落仓内);后端 = `secret::BACKEND` 编译期择型
(0.2.0:secret-windows / secret-service > secret-env > fake),不进回灌;
`build_named = None`(单实例;多实例 = 自造竞态);缺省清单必含 `cfg`,
手工摘除须显形警告。
- **后端矩阵(BD-CFG-12 的选型,ek-cfg 0.2.0 落定)**:
A = keyring 3.x(Win 凭据管理器 = `secret-windows`;Linux Secret Service =
`secret-service`,均编译期择型);B = env 注入(`EK_SEC_EK_<NS>_<KEY>`,
非字母数字 → `_`,**只读**,headless 服务器常态);A 缺席时 B 才在编译面
生效;两者皆无 → fake(内存,进程亡即失,「待手配」语义的真值缺省);
C(DPAPI 文件)不做,D(口令加密文件)argon2id+AES-GCM 暂缓至 v3;
E(明文+权限)拒绝。测试钩 `force_fake_for_tests` 把任何形态压回内存
fake —— 测试永不碰真库。探测与取值都在插件 worker 侧(BD-CFG-16 同义)。
> **环节 7/8 收口(2026-10-25,ek-cfg 0.2.0 / ek-ctp 0.10.1 / ek-station
> 0.3.2 / ek-oms 0.3.18 / ek-ui 0.6.0)**:
> - **BD-CFG-14 扩(0.10.1)**:受理三行序由**网关插件在 worker 面执行**
> (`do_configure` 成功 → `secret::set`(dump 内登记)→ `dump_config`
> → ask `cfg.save`);cfg 缺席 = 直写 `Store` 兜底(同闸 BD-CFG-9,
> 审计行 BD-CFG-11);`EK_CFG_FALLBACK_ROOT` 为部署显缝。configure
> 回执零改(UX 零变),成败走 eLog 显形。
> - **round-robin 消费者接点**:CtpPlugin `Configurable`(ns = `ctp` 或
> `ctp.<网关注册名>`;apply 只登记引用,解密在 on_start worker 面为
> BD-CFG-16 唯一许可点;`{gw}.credentials_state` 只读口);
> OmsPlugin(环节 5)同型。
> - **BD-CFG-8 的 UI 面**:运维窗三口 = `cfg.list`(零值面目录)/
> `cfg.delete`(清段,二次确认)/「重存段」(明面重读→serialize→重写,
> 值不变,rev/格式修复)。UI 宿主开局 = `boot_with_cfg` 载入接线
> (ek-ui 0.6.0)。agg:2024 版 `SecretValue(Zeroizing)` 手工 Debug
> 的机器扫描改动见 ek-cfg 0.2.0 与 ek-ctp 0.10.0 各自 CHANGELOG。
>
> **多账户唯一性边角收紧(2026-10-25 裁定,ek-ctp 0.10.4 / ek-station
> 0.3.3)**:唯一键 = **ctp + broker + 账号(字符串恒等,字段统一小写
> 归一)**,重复不得 —— ① 派生名清洗统一小写("I130"="i130",变体 =
> 同键 = 添加面重复拦截);② 导出 `is_derived_gateway_name`,工厂面
> `name_validator` 钩子挂于 ctp 行,svctl 命名请求只收派生形状(任意名/
> 撞缺省变体/大写形/只一段源头拒,孤儿档案开口关闭);③ 流文件目录
> `<flow_root>/<gw>/<Md|Td>` **大小写原样**(去 lower 展平:仅大小写不同
> 的两实例 = 两目录,CTP 私有流不再有同目录双 SDK 同开的互踩面)。