新增: Phase3 src-tauri 集成 df-tunnel(跨端闭环)

Cargo.toml(df-tunnel+uuid workspace) + state.rs(tunnel 字段 Arc<WsTunnelClient>) + lib.rs spawn tunnel task:device_id(KV/UUID 持久) + relay_url(KV/默认 localhost:8080) + token 固定常量 + subscriber(ai_event_bus→send_raw_event) + on_command(remote_bridge 下行路由) + connect(失败非阻断)。打通桌面↔relay↔miniapp 全双工闭环。cargo EXIT=0。
This commit is contained in:
2026-06-22 03:31:59 +08:00
parent 612de4cd7c
commit ed8e2fc155
4 changed files with 101 additions and 0 deletions

2
Cargo.lock generated
View File

@@ -842,6 +842,7 @@ dependencies = [
"df-nodes", "df-nodes",
"df-project", "df-project",
"df-storage", "df-storage",
"df-tunnel",
"df-types", "df-types",
"df-workflow", "df-workflow",
"futures", "futures",
@@ -858,6 +859,7 @@ dependencies = [
"tauri-plugin-window-state", "tauri-plugin-window-state",
"tokio", "tokio",
"tracing", "tracing",
"uuid",
] ]
[[package]] [[package]]

View File

@@ -36,7 +36,12 @@ df-ai = { path = "../crates/df-ai" }
df-ideas = { path = "../crates/df-ideas" } df-ideas = { path = "../crates/df-ideas" }
df-project = { path = "../crates/df-project" } df-project = { path = "../crates/df-project" }
df-mcp = { path = "../crates/df-mcp" } df-mcp = { path = "../crates/df-mcp" }
# Phase3 跨端隧道:桌面端主动出站连 df-relay(/ws/device),透传 ai_event_bus → miniapp
# + 转发 miniapp 下行指令 → remote_bridge 路由 Tauri command。穿 NAT 无需端口映射。
df-tunnel = { path = "../crates/df-tunnel" }
futures = "0.3" futures = "0.3"
# Phase3 tunnel device_id:首次启动生成 UUID v4 落 settings KV 持久化(relay 按 device_id 路由配对)
uuid = { workspace = true }
# write_file base64 编码:写二进制/非 UTF-8 文件(图片/PDF/Excel 等) # write_file base64 编码:写二进制/非 UTF-8 文件(图片/PDF/Excel 等)
base64 = "0.22" base64 = "0.22"
# validate_path URL 解码:防 %2e%2e 等 URL 编码绕过路径遍历检查(BUG-260617-03) # validate_path URL 解码:防 %2e%2e 等 URL 编码绕过路径遍历检查(BUG-260617-03)

View File

@@ -6,6 +6,8 @@ mod state;
use tauri::{Emitter, Listener, Manager}; use tauri::{Emitter, Listener, Manager};
use state::AppState; use state::AppState;
// Phase3 跨端隧道:TunnelClient trait(connect/send_raw_event/disconnect 方法)需在作用域内
use df_tunnel::TunnelClient;
#[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() { pub fn run() {
@@ -86,6 +88,90 @@ pub fn run() {
}); });
}); });
// Phase3 跨端隧道集成:tunnel connect relay + subscriber 透传 ai_event_bus → miniapp
// + on_command 下行路由(miniap 指令 → remote_bridge → Tauri command)。
// spawn 后台 task:device_id/relay_url 解析(持久 KV,无则默认)→ subscriber 透传
// → on_command 回调注册 → connect(失败非阻断,可后续手动重连)。
let tunnel_app = app.handle().clone();
tauri::async_runtime::spawn(async move {
let state = tunnel_app.state::<AppState>();
// device_id:KV 取,无则生成 UUID v4 落库(持久,relay 按 device_id 路由配对)。
let device_id = match state.settings.get("device_id").await {
Ok(Some(id)) if !id.is_empty() => id,
_ => {
let id = uuid::Uuid::new_v4().to_string();
let _ = state.settings.set("device_id", &id).await;
id
}
};
// relay_url:KV 取,无则默认本地(df-miniapp config.ts:33 同源 localhost:8080)。
let relay_url = state
.settings
.get("relay_url")
.await
.ok()
.flatten()
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "ws://localhost:8080/ws/device".to_string());
// token:固定常量(三端同源,对齐 df-relay relay.rs:37 DEFAULT_TOKEN)
let token = "devflow-relay-default-token".to_string();
// subscriber task:subscribe ai_event_bus → tunnel.send_raw_event 透传 miniapp
let mut rx = state.ai_event_bus.subscribe();
let tunnel_for_sub = state.tunnel.clone();
tauri::async_runtime::spawn(async move {
tracing::info!("[tunnel-sub] subscriber task 启动,透传 ai_event_bus → relay");
while let Ok(value) = rx.recv().await {
if let Err(e) = tunnel_for_sub.send_raw_event(value).await {
tracing::warn!("[tunnel-sub] send_raw_event 失败(可能未连接): {}", e);
}
}
tracing::info!("[tunnel-sub] subscriber task 退出(rx 关闭)");
});
// on_command 回调:miniapp 下行指令 → remote_bridge 路由 Tauri command。
// CommandHandler = Arc<dyn Fn(Value) -> BoxFuture<'static, ()> + Send + Sync>
// (tunnel.rs:45)。回调返 BoxFuture,Box::pin 构造 + 类型标注对齐签名。
//
// 借用处理:`handle_remote_command(payload, app, state)` 的 state 是
// `State<'_, AppState>`(newtype `&AppState`),借用 app。若先 `let state =
// app.state()` 再 move app 入函数,E0505(借用的 app 被 move)。
// 正解:同一表达式内 `app.clone()` move clone、`app.state()` 借用原 app,
// 原 app 不 move(仅被 state 借用到调用结束),借用检查通过。
let cmd_app = tunnel_app.clone();
let on_command: df_tunnel::tunnel::CommandHandler = std::sync::Arc::new(
move |payload: serde_json::Value| {
let app = cmd_app.clone();
Box::pin(async move {
crate::commands::ai::remote_bridge::handle_remote_command(
payload,
app.clone(),
app.state::<AppState>(),
)
.await;
})
as std::pin::Pin<
Box<dyn std::future::Future<Output = ()> + Send>,
>
},
);
// connect(失败非阻断,tunnel 可后续手动重连)。收发循环在 client 内部 spawn。
match state
.tunnel
.connect(&relay_url, &device_id, &token, on_command)
.await
{
Ok(()) => tracing::info!(
"[tunnel] 连接 relay 成功 device_id={} url={}",
device_id,
relay_url
),
Err(e) => tracing::warn!("[tunnel] 连接 relay 失败(非阻断): {}", e),
}
});
Ok(()) Ok(())
}) })
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![

View File

@@ -412,6 +412,12 @@ pub struct AppState {
/// workspace_root 始终在白名单(向后兼容)。 /// workspace_root 始终在白名单(向后兼容)。
/// Phase B 将扩展为 persistent + session(会话临时授权) + 弹窗挂起。 /// Phase B 将扩展为 persistent + session(会话临时授权) + 弹窗挂起。
pub allowed_dirs: Arc<RwLock<AllowedDirs>>, pub allowed_dirs: Arc<RwLock<AllowedDirs>>,
// ── 跨端隧道(Phase3 Layer1)──
/// 桌面端出站 WS 隧道客户端,连 df-relay(/ws/device)透传 ai_event_bus → miniapp
/// + 转发 miniapp 下行指令 → remote_bridge。lib.rs setup spawn task 解析
/// device_id/relay_url 后 connect(失败非阻断,可后续手动重连)。
/// Arc 包裹:setup task(subscriber + on_command 回调)与 AppState 共享同一客户端。
pub tunnel: std::sync::Arc<df_tunnel::WsTunnelClient>,
} }
/// F-260619-03 Phase A/B/C: AI 工具文件访问授权目录白名单 /// F-260619-03 Phase A/B/C: AI 工具文件访问授权目录白名单
@@ -661,6 +667,8 @@ impl AppState {
ai_session: Arc::new(Mutex::new(AiSession::new())), ai_session: Arc::new(Mutex::new(AiSession::new())),
// L3 批2b:AI 事件总线入 AppState(独立于工作流 event_bus)。emit 双写留批3。 // L3 批2b:AI 事件总线入 AppState(独立于工作流 event_bus)。emit 双写留批3。
ai_event_bus: crate::commands::ai::event_bus::EventBus::new(), ai_event_bus: crate::commands::ai::event_bus::EventBus::new(),
// Phase3 Layer1:tunnel 客户端未连接实例,lib.rs setup 内 connect
tunnel: std::sync::Arc::new(df_tunnel::WsTunnelClient::new()),
knowledge: KnowledgeRepo::new(&db), knowledge: KnowledgeRepo::new(&db),
knowledge_events: KnowledgeEventsRepo::new(&db), knowledge_events: KnowledgeEventsRepo::new(&db),
knowledge_config: Arc::new(Mutex::new(KnowledgeConfig::default())), knowledge_config: Arc::new(Mutex::new(KnowledgeConfig::default())),