//! df-im-gateway 主入口:装配 config + store + throttle + commander + adapters //! //! 骨架版(P0):Web 适配器真实可跑,事件流全链路打通。 //! 用法: //! ```text //! df-im-gateway.exe # 默认配置(env) //! GATEWAY_RELAY_URL=... GATEWAY_RELAY_TOKEN=... \ //! GATEWAY_DEFAULT_CHANNEL=chan-work df-im-gateway.exe //! ``` mod adapters; mod commander; mod config; mod feishu; mod store; mod throttle; use std::path::PathBuf; use tokio::sync::mpsc; use adapters::{InboundEvent, OutboundAction, PlatformAdapter}; use config::GatewayConfig; use store::SessionStore; use throttle::DeltaThrottle; #[tokio::main] async fn main() -> anyhow::Result<()> { tracing_subscriber::fmt() .with_env_filter( tracing_subscriber::EnvFilter::try_from_default_env() .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")), ) .init(); let cfg = GatewayConfig::load()?; if cfg.channels.is_empty() { anyhow::bail!("未配置任何 channel 路由(GATEWAY_DEFAULT_CHANNEL),无法启动"); } let data_dir = std::env::var("GATEWAY_DATA_DIR") .map(PathBuf::from) .unwrap_or_else(|_| PathBuf::from("data")); std::fs::create_dir_all(&data_dir)?; let store = SessionStore::open(&data_dir.join("sessions.db"))?; info_banner(&cfg); // ── 核心事件总线:适配器 → 核心 ───────────────────────────── let (inbound_tx, mut inbound_rx) = mpsc::channel::(256); // Web 适配器(四端首个,P0) let web_adapter = adapters::web::WebAdapter::new(); { let web_run = web_adapter.clone(); let ev_tx = inbound_tx.clone(); tokio::spawn(async move { if let Err(e) = web_run.run(ev_tx).await { tracing::error!("[web] 适配器退出: {e}"); } }); } // 飞书适配器(P2):FEISHU_APP_ID/SECRET 配置时启用长连接 let feishu_adapter = cfg.feishu.as_ref().map(|f| match feishu::FeishuAdapter::new(f) { Ok(adapter) => { let feishu_run = adapter.clone(); let ev_tx = inbound_tx.clone(); tokio::spawn(async move { if let Err(e) = feishu_run.run(ev_tx).await { tracing::error!("[feishu] 适配器退出: {e}"); } }); tracing::info!("[feishu] 适配器已启动(长连接)"); adapter } Err(e) => { tracing::error!("[feishu] 适配器构造失败(跳过): {e}"); feishu::FeishuAdapter::offline() } }); // ── Commander 连接(u-relay)──────────────────────────────── let (shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(false); let default_channel = cfg .channels .iter() .find(|c| c.prefix.is_empty()) .map(|c| c.channel_id.clone()) .unwrap_or_else(|| cfg.channels[0].channel_id.clone()); // 事件流:commander 任务 → 核心循环 let (ev_tx, mut ev_rx) = mpsc::channel::(256); // 指令流:核心循环 → commander 任务(透传到 WS) let (cmd_tx, cmd_rx) = mpsc::channel::(256); { let relay_url = cfg.relay_url.clone(); let token = cfg.relay_token.clone(); let channel = default_channel.clone(); tokio::spawn(async move { if let Err(e) = commander::run_commander(relay_url, channel, token, ev_tx, cmd_rx, shutdown_rx).await { tracing::error!("[commander] 连接循环退出: {e}"); } }); } // ── 核心分发循环(单 select,持有全部状态,无跨线程共享)───── let mut throttle = DeltaThrottle::new(cfg.throttle_ms); // 审批幂等表:approval_id → 已处理标记(P0 必验项 ②:防 pending→approved 二次点击重复转发) let mut handled_approvals: std::collections::HashSet = std::collections::HashSet::new(); // 平台注册表(走查 #4):显式 platform → 下标映射,替代硬编码 "feishu"=>1 与 // adapters vec 构建顺序的隐式耦合。新增平台(企微/钉钉)只在此处注册一次。 let mut adapters: Vec> = vec![Box::new(web_adapter.clone())]; let mut platform_index: std::collections::HashMap = std::collections::HashMap::from([("web".to_string(), 0usize)]); if let Some(fs) = feishu_adapter { platform_index.insert("feishu".to_string(), adapters.len()); adapters.push(Box::new(fs)); } let platform_of = |platform_or_conv: &str| -> usize { // conv_id 形如 "platform:chat_id",前缀即平台;未知平台回退 web(0) let platform = platform_or_conv.split(':').next().unwrap_or("web"); *platform_index.get(platform).unwrap_or(&0) }; // 停止信号 let shutdown_ctrl = tokio::signal::ctrl_c(); tokio::pin!(shutdown_ctrl); let cfg_loop = cfg.clone(); loop { tokio::select! { _ = &mut shutdown_ctrl => { tracing::info!("收到 Ctrl+C,优雅退出"); let _ = shutdown_tx.send(true); break; } // ── TunnelEvent(桌面端 → 网关)── ev = ev_rx.recv() => { let Some(ev) = ev else { break }; match ev { df_tunnel::TunnelEvent::TextDelta { conv_id, delta } => { // 出站按 conv_id 前缀(platform:chat_id)路由回原平台 let flush = throttle.feed(&conv_id, &delta); if let Some(text) = flush { let idx = platform_of(&conv_id); let _ = adapters[idx] .dispatch(OutboundAction::StreamText { chat_id: conv_id, text, done: false }) .await; } } df_tunnel::TunnelEvent::Completed { conv_id, .. } => { let tail = throttle.finish(&conv_id); let full = tail.unwrap_or_default(); let idx = platform_of(&conv_id); let _ = adapters[idx] .dispatch(OutboundAction::StreamText { chat_id: conv_id, text: full, done: true }) .await; } df_tunnel::TunnelEvent::Approval { conv_id, approval_id, description } => { let idx = platform_of(&conv_id); let _ = adapters[idx] .dispatch(OutboundAction::ApprovalCard { chat_id: conv_id, approval_id, title: "工具审批请求".into(), detail: description, }) .await; } df_tunnel::TunnelEvent::ToolCall { .. } => { // P0 骨架不转发 ToolCall 到 IM(卡片噪音),仅日志 tracing::debug!("[core] ToolCall 事件(骨架忽略)"); } } } // ── InboundEvent(IM → 网关)── inbound = inbound_rx.recv() => { let Some(ev) = inbound else { break }; match ev { InboundEvent::Message { platform, chat_id, user_id, text } => { // 回复适配器:按来源平台回路由 let reply_idx = platform_of(&platform); if !cfg_loop.whitelist.is_empty() && !cfg_loop.whitelist.contains(&user_id) { let _ = adapters[reply_idx] .dispatch(OutboundAction::Notice { chat_id, text: "⛔ 你不在白名单内".into() }) .await; continue; } let Some(channel_id) = cfg_loop.route(&text) else { let _ = adapters[reply_idx] .dispatch(OutboundAction::Notice { chat_id, text: "⚠️ 无可用 channel".into() }) .await; continue; }; // 会话映射:无则新建(conv_id = platform:chat_id,Web 端 chat_id 同源) let conv_id = match store.get_conv(&platform, &chat_id) { Some(m) => m.conv_id, None => { let conv = format!("{platform}:{chat_id}"); let _ = store.set_conv(&platform, &chat_id, &conv, channel_id); // 走查 #3:新建会话时清理流式残留(上轮异常中止无 // Completed 事件,streams entry 永驻会错覆写旧消息) adapters[reply_idx].reset_stream(&chat_id).await; conv } }; let _ = cmd_tx .send(df_tunnel::TunnelCommand::Send { conv_id, content: text }) .await; } InboundEvent::NewSession { platform, chat_id } => { let _ = store.remove(&platform, &chat_id); let reply_idx = platform_of(&platform); let _ = adapters[reply_idx] .dispatch(OutboundAction::Notice { chat_id, text: "🔄 已重置会话".into() }) .await; } InboundEvent::Approval { platform, chat_id, approval_id, approved } => { // 幂等:同 approval_id 只处理首次(P0 必验项 ②) if handled_approvals.contains(&approval_id) { tracing::debug!("[core] 审批 {approval_id} 已处理,忽略重复点击"); continue; } handled_approvals.insert(approval_id.clone()); let conv_id = store .get_conv(&platform, &chat_id) .map(|m| m.conv_id) .unwrap_or_else(|| format!("{platform}:{chat_id}")); let _ = cmd_tx .send(df_tunnel::TunnelCommand::Approve { conv_id, approval_id, accepted: approved }) .await; } } } } } Ok(()) } fn info_banner(cfg: &GatewayConfig) { println!("╔══════════════════════════════════════════╗"); println!("║ df-im-gateway v0.1.0 (P0 骨架) ║"); println!("╚══════════════════════════════════════════╝"); println!(" relay : {}", cfg.relay_url); println!(" channels : {} 条路由", cfg.channels.len()); println!(" throttle : {}ms", cfg.throttle_ms); println!(" whitelist : {} 用户", cfg.whitelist.len()); if cfg.whitelist.is_empty() { println!(" ⚠️ 白名单为空:任何人可用本网关(开发默认)。生产部署请设置 GATEWAY_WHITELIST!"); } println!(" feishu : {}", if cfg.feishu.is_some() { "已配置" } else { "未配置(跳过)" }); println!(" web ws : 0.0.0.0:9191"); }