新增: 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:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -842,6 +842,7 @@ dependencies = [
|
||||
"df-nodes",
|
||||
"df-project",
|
||||
"df-storage",
|
||||
"df-tunnel",
|
||||
"df-types",
|
||||
"df-workflow",
|
||||
"futures",
|
||||
@@ -858,6 +859,7 @@ dependencies = [
|
||||
"tauri-plugin-window-state",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -36,7 +36,12 @@ df-ai = { path = "../crates/df-ai" }
|
||||
df-ideas = { path = "../crates/df-ideas" }
|
||||
df-project = { path = "../crates/df-project" }
|
||||
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"
|
||||
# Phase3 tunnel device_id:首次启动生成 UUID v4 落 settings KV 持久化(relay 按 device_id 路由配对)
|
||||
uuid = { workspace = true }
|
||||
# write_file base64 编码:写二进制/非 UTF-8 文件(图片/PDF/Excel 等)
|
||||
base64 = "0.22"
|
||||
# validate_path URL 解码:防 %2e%2e 等 URL 编码绕过路径遍历检查(BUG-260617-03)
|
||||
|
||||
@@ -6,6 +6,8 @@ mod state;
|
||||
use tauri::{Emitter, Listener, Manager};
|
||||
|
||||
use state::AppState;
|
||||
// Phase3 跨端隧道:TunnelClient trait(connect/send_raw_event/disconnect 方法)需在作用域内
|
||||
use df_tunnel::TunnelClient;
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
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(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
|
||||
@@ -412,6 +412,12 @@ pub struct AppState {
|
||||
/// workspace_root 始终在白名单(向后兼容)。
|
||||
/// Phase B 将扩展为 persistent + session(会话临时授权) + 弹窗挂起。
|
||||
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 工具文件访问授权目录白名单
|
||||
@@ -661,6 +667,8 @@ impl AppState {
|
||||
ai_session: Arc::new(Mutex::new(AiSession::new())),
|
||||
// L3 批2b:AI 事件总线入 AppState(独立于工作流 event_bus)。emit 双写留批3。
|
||||
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_events: KnowledgeEventsRepo::new(&db),
|
||||
knowledge_config: Arc::new(Mutex::new(KnowledgeConfig::default())),
|
||||
|
||||
Reference in New Issue
Block a user