新增: 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-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]]
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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![
|
||||||
|
|||||||
@@ -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())),
|
||||||
|
|||||||
Reference in New Issue
Block a user