use rusqlite::{Connection, OpenFlags}; use serde::{Deserialize, Serialize}; use std::path::PathBuf; #[derive(Debug, Serialize, Clone)] pub struct V2raynNode { pub index_id: String, pub config_type: i64, pub remarks: String, pub address: String, pub port: i64, pub id: Option, pub security: Option, pub network: Option, pub stream_security: Option, pub flow: Option, pub sni: Option, pub fingerprint: Option, pub public_key: Option, pub short_id: Option, pub spider_x: Option, pub request_host: Option, pub path: Option, pub alter_id: Option, pub alter_id_i: Option, } /// 自动探测 v2rayN DB 路径 fn find_v2rayn_db() -> Option { // 1. %APPDATA%\v2rayN if let Ok(appdata) = std::env::var("APPDATA") { let p = PathBuf::from(&appdata) .join("v2rayN") .join("guiConfigs") .join("guiNDB.db"); if p.exists() { return Some(p); } } // 2. Scoop 安装:扫描常见盘符 + 用户目录下的 Scoop let scoop_roots: Vec = { let mut roots = vec![]; // 用户目录下的 Scoop(默认安装位置) if let Ok(home) = std::env::var("USERPROFILE") { roots.push(PathBuf::from(&home).join("scoop")); roots.push(PathBuf::from(&home).join("Scoop")); } // 各盘符根目录 for drive in ["C", "D", "E", "F", "G"] { roots.push(PathBuf::from(format!("{drive}:\\scoop"))); roots.push(PathBuf::from(format!("{drive}:\\Scoop"))); } roots }; for root in &scoop_roots { let p = root .join("persist") .join("v2rayn") .join("guiConfigs") .join("guiNDB.db"); if p.exists() { return Some(p); } // 有些 scoop bucket 用 v2rayn 全小写或带版本 let p2 = root .join("persist") .join("v2rayN") .join("guiConfigs") .join("guiNDB.db"); if p2.exists() { return Some(p2); } } None } /// 将 DB 文件复制到临时目录,绕过文件锁 fn copy_to_temp(src: &PathBuf) -> Result { let tmp = std::env::temp_dir().join("dev_manager_v2rayn_copy.db"); std::fs::copy(src, &tmp).map_err(|e| format!("复制 DB 失败: {e}"))?; Ok(tmp) } /// 扫描 v2rayN 节点列表。db_path 为空时自动探测。 #[tauri::command] pub fn scan_v2rayn_nodes(db_path: Option) -> Result, String> { let path = match db_path { Some(p) => PathBuf::from(p), None => { find_v2rayn_db().ok_or_else(|| "未找到 v2rayN 配置文件,请手动指定路径".to_string())? } }; // 复制避免锁冲突 let tmp = copy_to_temp(&path)?; let conn = Connection::open_with_flags(&tmp, OpenFlags::SQLITE_OPEN_READ_ONLY) .map_err(|e| format!("打开 DB 失败: {e}"))?; let mut stmt = conn .prepare( "SELECT IndexId, ConfigType, Remarks, Address, Port, Id, Security, Network, StreamSecurity, Flow, Sni, Fingerprint, PublicKey, ShortId, SpiderX, RequestHost, Path, AlterId FROM ProfileItem WHERE ConfigType IN (1, 3, 5, 6, 7) ORDER BY Remarks", ) .map_err(|e| format!("查询失败: {e}"))?; let nodes = stmt .query_map([], |row| { Ok(V2raynNode { index_id: row.get(0)?, config_type: row.get(1)?, remarks: row.get::<_, Option>(2)?.unwrap_or_default(), address: row.get::<_, Option>(3)?.unwrap_or_default(), port: row.get::<_, Option>(4)?.unwrap_or(0), id: row.get(5)?, security: row.get(6)?, network: row.get(7)?, stream_security: row.get(8)?, flow: row.get(9)?, sni: row.get(10)?, fingerprint: row.get(11)?, public_key: row.get(12)?, short_id: row.get(13)?, spider_x: row.get(14)?, request_host: row.get(15)?, path: row.get(16)?, alter_id: row.get(17)?, alter_id_i: None, }) }) .map_err(|e| format!("读取失败: {e}"))? .collect::, _>>() .map_err(|e| format!("数据解析失败: {e}"))?; // 清理临时文件 let _ = std::fs::remove_file(&tmp); Ok(nodes) } /// 返回当前探测到的 v2rayN DB 路径(用于前端展示) #[tauri::command] pub fn detect_v2rayn_db_path() -> Option { find_v2rayn_db().map(|p| p.to_string_lossy().to_string()) } #[derive(Debug, Serialize, Deserialize)] pub struct ActiveV2raynNode { pub index_id: String, pub remarks: String, pub address: String, pub port: i64, pub config_type: i64, pub share_link: String, } /// 读取 v2rayN 当前激活节点,返回节点信息和 share link #[tauri::command] pub fn get_active_v2rayn_node(db_path: Option) -> Result { // 1. 找到 DB 路径 let db_file = match db_path { Some(ref p) => PathBuf::from(p), None => find_v2rayn_db().ok_or("未找到 v2rayN 数据库,请先配置路径")?, }; // 2. 找同目录的 guiNConfig.json,读取当前激活 IndexId let config_file = db_file .parent() .ok_or("无法获取 DB 目录")? .join("guiNConfig.json"); let config_str = std::fs::read_to_string(&config_file) .map_err(|e| format!("读取 guiNConfig.json 失败: {e}"))?; #[derive(Deserialize)] struct GuiConfig { #[serde(rename = "IndexId")] index_id: String, } let gui_cfg: GuiConfig = serde_json::from_str(&config_str).map_err(|e| format!("解析 guiNConfig.json 失败: {e}"))?; let active_id = gui_cfg.index_id; // 3. 从 DB 查该节点详情 let tmp = copy_to_temp(&db_file)?; let conn = Connection::open_with_flags(&tmp, OpenFlags::SQLITE_OPEN_READ_ONLY) .map_err(|e| format!("打开 DB 失败: {e}"))?; let node = conn .query_row( "SELECT IndexId, ConfigType, Remarks, Address, Port, Id, Security, Network, StreamSecurity, Flow, Sni, Fingerprint, PublicKey, ShortId, SpiderX, RequestHost, Path, AlterId FROM ProfileItem WHERE IndexId = ?1", [&active_id], |row| { Ok(V2raynNode { index_id: row.get(0)?, config_type: row.get(1)?, remarks: row.get::<_, Option>(2)?.unwrap_or_default(), address: row.get::<_, Option>(3)?.unwrap_or_default(), port: row.get::<_, Option>(4)?.unwrap_or(0), id: row.get(5)?, security: row.get(6)?, network: row.get(7)?, stream_security: row.get(8)?, flow: row.get(9)?, sni: row.get(10)?, fingerprint: row.get(11)?, public_key: row.get(12)?, short_id: row.get(13)?, spider_x: row.get(14)?, request_host: row.get(15)?, path: row.get(16)?, alter_id: row.get(17)?, alter_id_i: None, }) }, ) .map_err(|_| format!("未在 DB 中找到激活节点 {active_id}"))?; let _ = std::fs::remove_file(&tmp); // 4. 生成 share link let share_link = build_share_link_rust(&node); Ok(ActiveV2raynNode { index_id: node.index_id, remarks: node.remarks, address: node.address, port: node.port, config_type: node.config_type, share_link, }) } /// 在 Rust 侧生成 share link(与前端 buildShareLink 逻辑一致) fn build_share_link_rust(n: &V2raynNode) -> String { let remarks_enc = urlencoding::encode(&n.remarks).into_owned(); match n.config_type { // VMess 1 => { let obj = serde_json::json!({ "v": "2", "ps": n.remarks, "add": n.address, "port": n.port.to_string(), "id": n.id.as_deref().unwrap_or(""), "aid": n.alter_id.unwrap_or(0).to_string(), "net": n.network.as_deref().unwrap_or("tcp"), "tls": n.stream_security.as_deref().unwrap_or(""), "sni": n.sni.as_deref().unwrap_or(""), "host": n.request_host.as_deref().unwrap_or(""), "path": n.path.as_deref().unwrap_or(""), "type": "none", }); let b64 = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, obj.to_string()); format!("vmess://{b64}") } // VLESS 5 => { let mut params = format!( "security={}&encryption=none", n.stream_security.as_deref().unwrap_or("none") ); if let Some(f) = &n.flow { if !f.is_empty() { params += &format!("&flow={f}"); } } if let Some(s) = &n.sni { if !s.is_empty() { params += &format!("&sni={s}"); } } if let Some(fp) = &n.fingerprint { if !fp.is_empty() { params += &format!("&fp={fp}"); } } if let Some(pk) = &n.public_key { if !pk.is_empty() { params += &format!("&pbk={pk}"); } } if let Some(sid) = &n.short_id { if !sid.is_empty() { params += &format!("&sid={sid}"); } } if let Some(net) = &n.network { if !net.is_empty() { params += &format!("&type={net}"); } } format!( "vless://{}@{}:{}?{params}#{remarks_enc}", n.id.as_deref().unwrap_or(""), n.address, n.port ) } // Trojan 3 => { let mut params = String::new(); if let Some(s) = &n.sni { if !s.is_empty() { params = format!("sni={s}"); } } let q = if params.is_empty() { String::new() } else { format!("?{params}") }; format!( "trojan://{}@{}:{}{}#{remarks_enc}", n.id.as_deref().unwrap_or(""), n.address, n.port, q ) } // Shadowsocks 6 => { let method = n.security.as_deref().unwrap_or("chacha20-poly1305"); let userinfo = base64::Engine::encode( &base64::engine::general_purpose::STANDARD, format!("{method}:{}", n.id.as_deref().unwrap_or("")), ); format!("ss://{userinfo}@{}:{}#{remarks_enc}", n.address, n.port) } _ => format!("unsupported://{}:{}", n.address, n.port), } }