Files
clawpanel/src-tauri/src/commands/pairing.rs

252 lines
8.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// 设备配对命令
/// 自动向 Gateway 注册设备,跳过手动配对流程
#[tauri::command]
pub fn auto_pair_device() -> Result<String, String> {
// 无论是否已配对,都确保 gateway.controlUi.allowedOrigins 已写入
// 必须在最前面,避免因设备密钥不存在而跳过
patch_gateway_origins();
// 获取或生成设备密钥(首次安装时自动创建)
let (device_id, public_key, _) = super::device::get_or_create_key()?;
// 读取或创建 paired.json
let paired_path = crate::commands::openclaw_dir()
.join("devices")
.join("paired.json");
let devices_dir = crate::commands::openclaw_dir().join("devices");
// 确保 devices 目录存在
if !devices_dir.exists() {
std::fs::create_dir_all(&devices_dir).map_err(|e| format!("创建 devices 目录失败: {e}"))?;
}
let mut paired: serde_json::Value = if paired_path.exists() {
let content = std::fs::read_to_string(&paired_path)
.map_err(|e| format!("读取 paired.json 失败: {e}"))?;
serde_json::from_str(&content).map_err(|e| format!("解析 paired.json 失败: {e}"))?
} else {
serde_json::json!({})
};
let os_platform = std::env::consts::OS; // "windows" | "macos" | "linux"
// 如果已配对,档查 platform 字段是否正确;不正确则覆盖更新,
// 避免 Gateway 因 metadata-upgrade 拒绝静默自动配对
if let Some(existing) = paired.get_mut(&device_id) {
let current_platform = existing
.get("platform")
.and_then(|v| v.as_str())
.unwrap_or("");
if current_platform != os_platform {
if let Some(obj) = existing.as_object_mut() {
obj.insert(
"platform".to_string(),
serde_json::Value::String(os_platform.to_string()),
);
obj.insert(
"deviceFamily".to_string(),
serde_json::Value::String("desktop".to_string()),
);
}
let new_content = serde_json::to_string_pretty(&paired)
.map_err(|e| format!("序列化 paired.json 失败: {e}"))?;
std::fs::write(&paired_path, new_content)
.map_err(|e| format!("更新 paired.json 失败: {e}"))?;
return Ok("设备已配对(已修正平台字段)".into());
}
return Ok("设备已配对".into());
}
// 添加设备到配对列表
let now_ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis() as u64;
paired[&device_id] = serde_json::json!({
"deviceId": device_id,
"publicKey": public_key,
"platform": os_platform,
"deviceFamily": "desktop",
"clientId": "openclaw-control-ui",
"clientMode": "ui",
"role": "operator",
"roles": ["operator"],
"scopes": [
"operator.admin",
"operator.approvals",
"operator.pairing",
"operator.read",
"operator.write"
],
"approvedScopes": [
"operator.admin",
"operator.approvals",
"operator.pairing",
"operator.read",
"operator.write"
],
"tokens": {},
"createdAtMs": now_ms,
"approvedAtMs": now_ms
});
// 写入 paired.json
let new_content = serde_json::to_string_pretty(&paired)
.map_err(|e| format!("序列化 paired.json 失败: {e}"))?;
std::fs::write(&paired_path, new_content).map_err(|e| format!("写入 paired.json 失败: {e}"))?;
Ok("设备配对成功".into())
}
/// 将 Tauri 应用的 origin 写入 gateway.controlUi.allowedOrigins
/// 避免 Gateway 因 origin not allowed 拒绝 WebSocket 握手
fn patch_gateway_origins() {
let Ok(mut config) = super::config::load_openclaw_json() else {
return;
};
// Tauri 应用 + 本地开发服务器必须存在的 origin
let required: Vec<String> = vec![
"tauri://localhost".into(),
"https://tauri.localhost".into(),
"http://tauri.localhost".into(),
"http://localhost:1420".into(),
"http://127.0.0.1:1420".into(),
];
if let Some(obj) = config.as_object_mut() {
let gateway = obj
.entry("gateway")
.or_insert_with(|| serde_json::json!({}));
if let Some(gw) = gateway.as_object_mut() {
let control_ui = gw
.entry("controlUi")
.or_insert_with(|| serde_json::json!({}));
if let Some(cui) = control_ui.as_object_mut() {
// 合并:保留用户已有的 origin追加缺失的 Tauri origin
let existing: Vec<String> = cui
.get("allowedOrigins")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|s| s.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
let mut merged = existing;
for r in &required {
if !merged.iter().any(|e| e == r) {
merged.push(r.clone());
}
}
cui.insert("allowedOrigins".to_string(), serde_json::json!(merged));
}
}
}
let _ = super::config::save_openclaw_json(&config);
}
#[tauri::command]
pub fn check_pairing_status() -> Result<bool, String> {
// 读取设备密钥
let device_key_path = crate::commands::openclaw_dir().join("clawpanel-device-key.json");
if !device_key_path.exists() {
return Ok(false);
}
let device_key_content =
std::fs::read_to_string(&device_key_path).map_err(|e| format!("读取设备密钥失败: {e}"))?;
let device_key: serde_json::Value =
serde_json::from_str(&device_key_content).map_err(|e| format!("解析设备密钥失败: {e}"))?;
let device_id = device_key["deviceId"].as_str().ok_or("设备 ID 不存在")?;
// 检查 paired.json
let paired_path = crate::commands::openclaw_dir()
.join("devices")
.join("paired.json");
if !paired_path.exists() {
return Ok(false);
}
let content =
std::fs::read_to_string(&paired_path).map_err(|e| format!("读取 paired.json 失败: {e}"))?;
let paired: serde_json::Value =
serde_json::from_str(&content).map_err(|e| format!("解析 paired.json 失败: {e}"))?;
Ok(paired.get(device_id).is_some())
}
async fn run_pairing_command(args: Vec<String>) -> Result<String, String> {
let mut cmd = crate::utils::openclaw_command_async();
cmd.args(args);
let output = cmd
.output()
.await
.map_err(|e| format!("执行 openclaw 失败: {e}"))?;
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
let message = match (stdout.is_empty(), stderr.is_empty()) {
(false, false) => format!("{stdout}\n{stderr}"),
(false, true) => stdout,
(true, false) => stderr,
(true, true) => String::new(),
};
if output.status.success() {
Ok(if message.is_empty() {
"操作完成".into()
} else {
message
})
} else {
Err(if message.is_empty() {
format!("命令执行失败: {}", output.status)
} else {
message
})
}
}
#[tauri::command]
pub async fn pairing_list_channel(channel: String) -> Result<String, String> {
let channel = channel.trim();
if channel.is_empty() {
return Err("channel 不能为空".into());
}
run_pairing_command(vec!["pairing".into(), "list".into(), channel.into()]).await
}
#[tauri::command]
pub async fn pairing_approve_channel(
channel: String,
code: String,
notify: bool,
) -> Result<String, String> {
let channel = channel.trim();
let code = code.trim();
if channel.is_empty() {
return Err("channel 不能为空".into());
}
if code.is_empty() {
return Err("配对码不能为空".into());
}
let mut args = vec![
"pairing".into(),
"approve".into(),
channel.into(),
code.into(),
];
if notify {
args.push("--notify".into());
}
run_pairing_command(args).await
}