ek-station 0.4.2

Station for the ek ecosystem: headless plugin host bootstrap - assembles ek-core MainEngine from a compile-time plugin factory table, runs the svctl supervisor service (list/start/remove over bus ask) and a headless main (readiness marker + graceful close).
Documentation
//! # factory —— 编译期插件工厂表(DV-2:v1 不做 dylib 动态加载)。
//!
//! 每个登记项 = 注册名 + 文档串 + `fn() -> Arc<dyn Plugin>` 零参构造。
//! 构造固定用生态缺省配置(`DmConfig::minute("dm")`/
//! `OmsPlugin::builder().risk_gates(ek_risk::standard_gates(..))`(0.1.7 装配
//! 口径)/`CtpPlugin::new(CtpConfig::default()` → 注册名 "ctp",native 特性关 =
//! 纯逻辑驱动);参数化配置进 S3 配置面(届时 build 签名升级为
//! `fn(&SectionMap) -> Result<...>`,本表只换实现不改面)。
//!
//! 0.1.12(BD-UI-38 用户裁定):纸面网关 "sim" 退场(纸面账户功能整体撤除,
//! ctp/SimNow 两套前置满足需求);历史见 CHANGELOG 0.1.8/0.1.9。

use ek_core::plugin::Plugin;
use ek_ctp::plugin::{CtpConfig, CtpPlugin};
use ek_dm::plugin::{DmConfig, DmPlugin};
use ek_oms::plugin::OmsPlugin;

/// 零参构造器(缺省路径:注册名 = 工厂名)。测试探针需要捕获共享槽,故用
/// `Arc<dyn Fn>`;零捕获闭包同样适配。
pub type Builder = std::sync::Arc<dyn Fn() -> std::sync::Arc<dyn Plugin> + Send + Sync>;

/// 命名构造器(多实例面:按调用方给的注册名造一个实例)。
pub type NamedBuilder = std::sync::Arc<dyn Fn(&str) -> std::sync::Arc<dyn Plugin> + Send + Sync>;

/// 一个可装配的插件种类。
#[derive(Clone)]
pub struct PlugKind {
    /// 工厂注册名(svctl.start 的 `factory` 参数;**不必然**等于插件注册名,
    /// 如多网关实例同工厂 —— 命名实例走 `build_named`)。
    pub name: &'static str,
    /// 一行用途说明(观测页提示用)。
    pub doc: &'static str,
    /// 零参构造。用于 `StartReq.name` 为空的缺省路径(单实例,注册名 = 工厂名)。
    pub build: Builder,
    /// 命名构造(多实例面:`StartReq.name` 非空时走这条)。
    ///
    /// `None` = 该工厂不支持命名实例(supervisor 直接回 `Rejected`,不静默退回
    /// 零参构造 —— 否则调用方以为建出了第二个实例,实际撞名)。
    pub build_named: Option<NamedBuilder>,
    /// 命名请求的前置闸(0.3.3 边角收紧):`Some(校验器)` 时,`StartReq.name`
    /// 非空先过校验,不过 = `Rejected`(不构造、不落宿主)。工厂各自收口
    /// 自己的命名纪律 —— ctp 行 = 只收**派生名形状**(`ek_ctp::
    /// is_derived_gateway_name`),任意名/变体名/超长名在此源头拒,不留
    /// 「名字与 broker+账号脱钩」的孤儿档案开口。`None` = 不设额外闸。
    pub name_validator: Option<NamedValidator>,
}

/// 命名闸签名:通过 = `Ok(())`;拒绝 = `Err(原因)`(原样进
/// `StartRes::Rejected.reason`)。
pub type NamedValidator = std::sync::Arc<dyn Fn(&str) -> Result<(), String> + Send + Sync>;

/// 「风控参数 → ek-risk 四闸」重建器(0.4.2,「插件即 App」波)。
///
/// 映射为什么住在 station:`ek-risk → ek-oms` 是单向依赖(闸实现吃 `RiskGate`
/// 契约),ek-oms 侧只放纯数据 [`ek_oms::RiskSpec`];本 crate 同时依赖两端,
/// 映射放这里**不成环**,并让 `oms` 段风控五键在受理面改动时**运行期重建闸面**
/// (无需重启插件)。判据走 [`oms_gate_builder`] 直接喂 `OmsPlugin::builder()`。
#[must_use]
pub fn oms_gate_builder() -> ek_oms::GateRebuild {
    std::sync::Arc::new(|s: &ek_oms::RiskSpec| {
        ek_risk::standard_gates(&ek_risk::RiskLimits {
            max_volume_per_order: s.max_volume_per_order,
            max_position_per_symbol: s.max_position_per_symbol,
            price_band: s.price_band,
            require_contract: s.require_contract,
            max_notional: s.max_notional,
        })
    })
}

/// 缺省工厂表:ek 生态当前已发布插件的全部登记。
#[must_use]
pub fn default_kinds() -> Vec<PlugKind> {
    vec![
        PlugKind {
            // 0.3.0(BD-CFG-20):配置托底收件壳,缺省表**必含**(手工摘除
            // 需显形警告,boot_with_cfg 的 warn_if_cfg_missing 管)。
            // eco_or_local:config_dir 不可用 → 仓内 ./ekcfg-local 显形回落,
            // 拒临时目录(BD-CFG-20)。
            // 承重耦合(0.3.6 补齐,与 ctp 行同式):缺省路径「注册名 = 工厂名」
            // 的等式两端都锚 `ek_cfg::NAME`(CfgPlugin 自报;托底者不吃托底;
            // 0.3.7 随 ek-cfg 0.2.2 根面直名切齐,不走 plugin:: 路径)。
            name: ek_cfg::NAME,
            doc: "配置托底(save/list/delete;ek-cfg,零状态收件形貌)",
            build: std::sync::Arc::new(|| std::sync::Arc::new(ek_cfg::CfgPlugin::eco_or_local())),
            build_named: None, // 单实例裁定:多实例 = 自造磁盘竞态
            name_validator: None,
        },
        PlugKind {
            // 承重耦合(0.3.6 补齐):`ek_dm::NAME` = DmPlugin 自报注册名;
            // 缺省路径下 DmConfig::minute(NAME) 与本行同名 —— 改名两端同一步。
            name: ek_dm::NAME,
            doc: "数据管理器(KlineManager;eTick.→eBar. 单源批)",
            build: std::sync::Arc::new(|| {
                std::sync::Arc::new(DmPlugin::new(DmConfig::minute(ek_dm::NAME)))
            }),
            build_named: None,
            name_validator: None,
        },
        PlugKind {
            // 承重耦合(0.3.6 补齐):`ek_oms::NAME` = OmsPlugin 自报注册名
            // (风险闸装配走 builder;零参路径与 svctl remove 的点名端同源)。
            name: ek_oms::NAME,
            doc: "OMS 中枢(入账 + 查询 + 五段管线;地板三件 + ek-risk 四闸装配:合约预检 / 单笔手数 / 品种持仓限 / 价带)",
            build: std::sync::Arc::new(|| {
                // 0.1.7 装配口径:缺省限额(DESIGN ek-risk §6「不阻塞但保守」)。
                // 0.4.2「插件即 App」波:注入闸面重建器 —— `oms` 段风控五键在
                // 受理面改动时**运行期重建闸面**(映射住在 station,见
                // [`oms_gate_builder`]:本 crate 同时依赖 ek-oms 与 ek-risk,
                // ek-oms 侧只放纯数据 RiskSpec,依赖方向不成环)。
                std::sync::Arc::new(
                    OmsPlugin::builder()
                        .risk_spec(ek_oms::RiskSpec::default())
                        .gate_rebuild(oms_gate_builder())
                        .risk_gates(ek_risk::standard_gates(&ek_risk::RiskLimits::default()))
                        .build(),
                )
            }),
            build_named: None,
            name_validator: None,
        },
        PlugKind {
            name: "ctp", // 工厂名;注册名经 gateway_name_or_default 承重耦合(见下方 build_ctp 文档)
            doc: "CTP 网关(native 关 = 纯逻辑驱动;账号经 {gw}.configure 手工注入,插件不读配置文件)",
            build: std::sync::Arc::new(|| build_ctp("")),
            // 多账号面:同一工厂按不同注册名建多个实例(名字由调用方派生)。
            build_named: Some(std::sync::Arc::new(build_ctp)),
            // 命名闸(0.3.3 边角收紧):只收派生名形状 —— 唯一键 =
            // ctp+broker+账号;任意名/大小写变体/超长名一律 `Rejected`。
            name_validator: Some(std::sync::Arc::new(|name| {
                if ek_ctp::plugin::is_derived_gateway_name(name) {
                    Ok(())
                } else {
                    Err(format!(
                        "ctp 命名实例须为派生名 ctp_<broker>_<账号>(仅小写字母数字,≤32);收到 {name:?}"
                    ))
                }
            })),
        },
    ]
}

/// 以指定注册名建一个 ctp 实例(空名 = 缺省 `"ctp"`,单账号旧行为)。
///
/// 账号/前置一律留空:`connect` 空地址守卫跳过,等 `{gw}.configure` 手工注入
/// (0.3.0 D-CONFIGURE 定盘)。**凭据不进 start 通道**(密码只走 configure)。
///
/// 承重耦合(0.2.11 审核点名,BD-UI-64):空名这一路**不在本仓折成 "ctp"**,
/// 靠的是 ek-ctp `CtpConfig::gateway_name_or_default()`(`plugin.rs:352-358`)
/// 把空串兜成 `"ctp"`。这是一条跨 crate 的隐式约定:ek-ctp 若改那个兜底,
/// 本工厂零参路径的注册名、`{gw}.*` 那七个服务名、运维窗里的行名会一起静默变。
/// 命名路径(`build_named`)不受影响 —— 名字由调用方派生(`derived_gateway_name`)。
fn build_ctp(name: &str) -> std::sync::Arc<dyn Plugin> {
    let cfg = CtpConfig {
        gateway_name: name.to_string(),
        ..CtpConfig::default()
    };
    // ctp-native 特性开 → 随包 SDK 传输 + 流目录 target/ctp-flow(按 gw 分子目录);
    // 关 → 纯逻辑驱动(现行为)。
    #[cfg(feature = "ctp-native")]
    {
        CtpPlugin::with_native(cfg, "target/ctp-flow")
    }
    #[cfg(not(feature = "ctp-native"))]
    {
        std::sync::Arc::new(CtpPlugin::new(cfg))
    }
}

/// 委托式包装:把异构的 `Arc<dyn Plugin>` 抬成 `add_plugin` 泛型点可用的具体类型
/// (ek-core 0.3.0 的 `impl Plugin` 是 `Sized` 泛型面 —— BD-U6:ek-core 已发布不可改)。
///
/// 纪律钉:`get_typed` 的 downcast 不穿透本包装(nt 语义拿内层裸 Arc),
/// 类型面一律走各插件自报的 ask 契约(生态铁律:插件不直调)。
pub struct DynPlugin {
    inner: std::sync::Arc<dyn Plugin>,
}

impl DynPlugin {
    /// 包装已建好的插件对象(直接给出 `Arc<Self>`,调用点即 add_plugin 形参)。
    pub fn wrap(inner: std::sync::Arc<dyn Plugin>) -> std::sync::Arc<Self> {
        std::sync::Arc::new(Self { inner })
    }
}

impl Plugin for DynPlugin {
    fn name(&self) -> &str {
        self.inner.name()
    }

    fn deps(&self) -> &[&str] {
        self.inner.deps()
    }

    fn routes(&self) -> Vec<std::borrow::Cow<'static, str>> {
        self.inner.routes()
    }

    fn mailbox_soft_cap(&self) -> u64 {
        self.inner.mailbox_soft_cap()
    }

    fn version(&self) -> std::borrow::Cow<'static, str> {
        self.inner.version()
    }

    fn services(&self) -> Vec<ek_core::service::ServiceDesc> {
        self.inner.services()
    }

    fn on_init(&self, ctx: &ek_core::context::PluginContext) {
        self.inner.on_init(ctx);
    }

    fn on_start(&self) {
        self.inner.on_start();
    }

    fn on_stop(&self) {
        self.inner.on_stop();
    }

    fn on_mail(&self, event: &ek_event::Event) {
        self.inner.on_mail(event);
    }

    fn as_configurable(&self) -> Option<&dyn ek_core::section::Configurable> {
        // 0.3.0(BD-CFG-1 环节自审抓出的坑):缺省方法在包装下会**被吞成
        // None** —— 不转发则 boot/svctl.start 两路的配置探查永远落空。
        self.inner.as_configurable()
    }
}