From c380d5eee7fa83737cd4ce5b7d11ae2235922560 Mon Sep 17 00:00:00 2001 From: coso Date: Fri, 26 Dec 2025 01:10:34 +0800 Subject: [PATCH] =?UTF-8?q?v0.20.0:=20=E6=9C=BA=E5=99=A8=E7=A0=81=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=B7=A5=E5=85=B7=20-=20=E5=85=A8=E6=96=B0=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E5=8F=91=E5=B8=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ✨ 新增功能 • 完整的跨平台机器码管理工具(Windows/macOS/Linux) • 智能格式检测和验证(UUID/32位十六进制) • 机器码生成、修改、备份和恢复功能 • 完整的操作历史记录追踪 • 图形化文件选择对话框 • 一键恢复原始机器码功能 🔧 技术实现 • Rust 后端服务,支持注册表和文件系统操作 • React 前端界面,完整的用户体验 • 跨平台兼容性和权限管理 • 历史记录持久化存储 🚀 用户体验 • 直观的图形化操作界面 • 智能权限检测和提示 • 清晰的错误处理和用户反馈 • 完善的恢复功能和备份机制 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4 --- package.json | 2 +- src-tauri/Cargo.lock | 13 +- src-tauri/Cargo.toml | 5 +- src-tauri/src/commands/machine_id_cmd.rs | 299 +++++++ src-tauri/src/commands/mod.rs | 1 + src-tauri/src/lib.rs | 24 + src-tauri/src/models/machine_id.rs | 208 +++++ src-tauri/src/models/mod.rs | 1 + src-tauri/src/services/machine_id_service.rs | 727 ++++++++++++++++ src-tauri/src/services/mod.rs | 1 + src-tauri/tauri.conf.json | 2 +- src/App.tsx | 6 + src/components/Sidebar.tsx | 1 + src/components/tools/ToolsPage.tsx | 16 +- .../machine-id/MachineIdHistoryPanel.tsx | 225 +++++ .../tools/machine-id/MachineIdInfoPanel.tsx | 257 ++++++ .../tools/machine-id/MachineIdManagePanel.tsx | 787 ++++++++++++++++++ .../tools/machine-id/MachineIdSystemPanel.tsx | 346 ++++++++ .../tools/machine-id/MachineIdTool.tsx | 154 ++++ src/components/ui/radio-group.tsx | 64 ++ src/components/ui/separator.tsx | 31 + src/hooks/use-toast.ts | 31 + src/lib/api/machineId.ts | 255 ++++++ 23 files changed, 3449 insertions(+), 7 deletions(-) create mode 100644 src-tauri/src/commands/machine_id_cmd.rs create mode 100644 src-tauri/src/models/machine_id.rs create mode 100644 src-tauri/src/services/machine_id_service.rs create mode 100644 src/components/tools/machine-id/MachineIdHistoryPanel.tsx create mode 100644 src/components/tools/machine-id/MachineIdInfoPanel.tsx create mode 100644 src/components/tools/machine-id/MachineIdManagePanel.tsx create mode 100644 src/components/tools/machine-id/MachineIdSystemPanel.tsx create mode 100644 src/components/tools/machine-id/MachineIdTool.tsx create mode 100644 src/components/ui/radio-group.tsx create mode 100644 src/components/ui/separator.tsx create mode 100644 src/hooks/use-toast.ts create mode 100644 src/lib/api/machineId.ts diff --git a/package.json b/package.json index 26a167e36..f1a392a51 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "proxycast", "private": true, - "version": "0.19.0", + "version": "0.20.0", "type": "module", "repository": { "type": "git", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 345462805..3739891b5 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -3668,7 +3668,7 @@ dependencies = [ [[package]] name = "proxycast" -version = "0.19.0" +version = "0.20.0" dependencies = [ "anyhow", "arboard", @@ -3725,6 +3725,7 @@ dependencies = [ "uuid", "winapi", "windows 0.56.0", + "winreg 0.52.0", "zip", ] @@ -6770,6 +6771,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "winreg" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + [[package]] name = "winreg" version = "0.55.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index c50a1dccf..695c60faf 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "proxycast" -version = "0.19.0" +version = "0.20.0" description = "AI API Proxy Desktop App" authors = ["you"] edition = "2021" @@ -66,7 +66,7 @@ arboard = "3" # Platform specific dependencies for browser interceptor -# Windows specific dependencies for browser interceptor +# Windows specific dependencies for browser interceptor and machine ID management [target.'cfg(windows)'.dependencies] windows = { version = "0.56", features = [ "Win32_Foundation", @@ -89,6 +89,7 @@ winapi = { version = "0.3", features = [ "psapi", "tlhelp32", ] } +winreg = "0.52" # macOS specific dependencies for browser interceptor [target.'cfg(target_os = "macos")'.dependencies] diff --git a/src-tauri/src/commands/machine_id_cmd.rs b/src-tauri/src/commands/machine_id_cmd.rs new file mode 100644 index 000000000..ac3ce8eeb --- /dev/null +++ b/src-tauri/src/commands/machine_id_cmd.rs @@ -0,0 +1,299 @@ +use crate::models::machine_id::*; +use crate::services::machine_id_service::MachineIdService; +use std::sync::Arc; +use tauri::State; +use tokio::sync::RwLock; + +/// 机器码管理服务状态 +pub type MachineIdState = Arc>; + +/// 获取当前机器码信息 +#[tauri::command] +pub async fn get_current_machine_id( + service: State<'_, MachineIdState>, +) -> Result { + let service = service.read().await; + service.get_current_machine_id().await +} + +/// 设置新的机器码 +#[tauri::command] +pub async fn set_machine_id( + new_id: String, + service: State<'_, MachineIdState>, +) -> Result { + let service = service.read().await; + service.set_machine_id(&new_id).await +} + +/// 生成随机机器码 +#[tauri::command] +pub async fn generate_random_machine_id( + service: State<'_, MachineIdState>, +) -> Result { + let service = service.read().await; + Ok(service.generate_random_machine_id()) +} + +/// 验证机器码格式 +#[tauri::command] +pub async fn validate_machine_id( + machine_id: String, + service: State<'_, MachineIdState>, +) -> Result { + let service = service.read().await; + service.validate_machine_id(&machine_id) +} + +/// 检查管理员权限 +#[tauri::command] +pub async fn check_admin_privileges( + service: State<'_, MachineIdState>, +) -> Result { + let service = service.read().await; + service.check_admin_privileges().await +} + +/// 获取操作系统类型 +#[tauri::command] +pub async fn get_os_type() -> Result { + Ok(MachineIdService::get_os_type()) +} + +/// 备份机器码到文件 +#[tauri::command] +pub async fn backup_machine_id_to_file( + file_path: String, + service: State<'_, MachineIdState>, +) -> Result { + let service = service.read().await; + service.backup_machine_id(&file_path).await +} + +/// 从文件恢复机器码 +#[tauri::command] +pub async fn restore_machine_id_from_file( + file_path: String, + service: State<'_, MachineIdState>, +) -> Result { + let service = service.read().await; + service.restore_machine_id(&file_path).await +} + +/// 格式化机器码 +#[tauri::command] +pub async fn format_machine_id( + machine_id: String, + format_type: String, // "uuid" 或 "hex32" +) -> Result { + let format = match format_type.to_lowercase().as_str() { + "uuid" => MachineIdFormat::Uuid, + "hex32" => MachineIdFormat::Hex32, + _ => return Err("Unsupported format type. Use 'uuid' or 'hex32'".to_string()), + }; + + format.format_machine_id(&machine_id) +} + +/// 检测机器码格式 +#[tauri::command] +pub async fn detect_machine_id_format(machine_id: String) -> Result { + let format = MachineIdFormat::detect(&machine_id); + Ok(format.to_string()) +} + +/// 转换机器码格式 +#[tauri::command] +pub async fn convert_machine_id_format( + machine_id: String, + target_format: String, +) -> Result { + // 首先清理输入 + let cleaned = machine_id.replace("-", "").replace(" ", "").to_lowercase(); + + // 验证输入是否为有效的十六进制 + if cleaned.len() != 32 || !cleaned.chars().all(|c| c.is_ascii_hexdigit()) { + return Err("Invalid machine ID: must be 32 hex characters".to_string()); + } + + match target_format.to_lowercase().as_str() { + "uuid" => { + let format = MachineIdFormat::Uuid; + format.format_machine_id(&cleaned) + } + "hex32" => { + let format = MachineIdFormat::Hex32; + format.format_machine_id(&cleaned) + } + _ => Err("Unsupported target format. Use 'uuid' or 'hex32'".to_string()), + } +} + +/// 获取机器码历史记录 +#[tauri::command] +pub async fn get_machine_id_history( + service: State<'_, MachineIdState>, +) -> Result, String> { + let service = service.read().await; + service.get_history() +} + +/// 清除机器码覆盖(仅限 macOS) +#[tauri::command] +pub async fn clear_machine_id_override() -> Result { + #[cfg(target_os = "macos")] + { + use dirs; + use std::fs; + + let override_file = dirs::data_dir() + .ok_or("Failed to get app data directory")? + .join("proxycast") + .join("machine-id-override"); + + match fs::remove_file(override_file) { + Ok(_) => Ok(MachineIdResult { + success: true, + message: "Machine ID override removed successfully".to_string(), + requires_restart: false, + requires_admin: false, + new_machine_id: None, + }), + Err(e) => Ok(MachineIdResult { + success: false, + message: format!("Failed to remove override: {}", e), + requires_restart: false, + requires_admin: false, + new_machine_id: None, + }), + } + } + + #[cfg(not(target_os = "macos"))] + { + Ok(MachineIdResult { + success: false, + message: "Machine ID override is only supported on macOS".to_string(), + requires_restart: false, + requires_admin: false, + new_machine_id: None, + }) + } +} + +/// 复制机器码到剪贴板 +#[tauri::command] +pub async fn copy_machine_id_to_clipboard(machine_id: String) -> Result { + use arboard::Clipboard; + let mut clipboard = + Clipboard::new().map_err(|e| format!("Failed to access clipboard: {}", e))?; + clipboard + .set_text(machine_id) + .map_err(|e| format!("Failed to copy to clipboard: {}", e))?; + + Ok(true) +} + +/// 从剪贴板粘贴机器码 +#[tauri::command] +pub async fn paste_machine_id_from_clipboard() -> Result { + use arboard::Clipboard; + let mut clipboard = + Clipboard::new().map_err(|e| format!("Failed to access clipboard: {}", e))?; + let text = clipboard + .get_text() + .map_err(|e| format!("Failed to read from clipboard: {}", e))?; + + // 基本验证 + let cleaned = text.replace("-", "").replace(" ", "").trim().to_lowercase(); + if cleaned.len() == 32 && cleaned.chars().all(|c| c.is_ascii_hexdigit()) { + Ok(text.trim().to_string()) + } else { + Err("Clipboard does not contain a valid machine ID".to_string()) + } +} + +/// 获取系统信息(用于调试和支持) +#[tauri::command] +pub async fn get_system_info() -> Result { + let os = MachineIdService::get_os_type(); + let arch = std::env::consts::ARCH.to_string(); + let family = std::env::consts::FAMILY.to_string(); + + Ok(SystemInfo { + os, + arch, + family, + machine_id_support: get_machine_id_platform_support(), + requires_admin: match MachineIdService::get_os_type().as_str() { + "windows" | "linux" => true, + "macos" => false, + _ => true, + }, + }) +} + +/// 系统信息结构 +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct SystemInfo { + pub os: String, + pub arch: String, + pub family: String, + pub machine_id_support: PlatformSupport, + pub requires_admin: bool, +} + +/// 平台支持信息 +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct PlatformSupport { + pub can_read: bool, + pub can_write: bool, + pub format: String, + pub method: String, + pub limitations: Vec, +} + +/// 获取当前平台的机器码支持信息 +fn get_machine_id_platform_support() -> PlatformSupport { + match MachineIdService::get_os_type().as_str() { + "windows" => PlatformSupport { + can_read: true, + can_write: true, + format: "UUID".to_string(), + method: "Windows Registry (HKLM\\SOFTWARE\\Microsoft\\Cryptography\\MachineGuid)" + .to_string(), + limitations: vec![ + "Requires administrator privileges".to_string(), + "May require restart for some applications".to_string(), + ], + }, + "macos" => PlatformSupport { + can_read: true, + can_write: true, + format: "UUID".to_string(), + method: "Application-level override (ioreg IOPlatformUUID + override file)".to_string(), + limitations: vec![ + "Original system UUID cannot be modified".to_string(), + "Only affects applications that use the override".to_string(), + ], + }, + "linux" => PlatformSupport { + can_read: true, + can_write: true, + format: "32-bit Hex".to_string(), + method: "File system (/etc/machine-id)".to_string(), + limitations: vec![ + "Requires root privileges".to_string(), + "May affect system services".to_string(), + "Some services may require restart".to_string(), + ], + }, + _ => PlatformSupport { + can_read: false, + can_write: false, + format: "Unknown".to_string(), + method: "Not supported".to_string(), + limitations: vec!["Platform not supported".to_string()], + }, + } +} diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index 968d04cf9..ac2cd629b 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -3,6 +3,7 @@ pub mod browser_interceptor_cmd; pub mod config_cmd; pub mod flow_monitor_cmd; pub mod injection_cmd; +pub mod machine_id_cmd; pub mod mcp_cmd; pub mod network_cmd; pub mod oauth_cmd; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 7b5802b9f..b633480c8 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -34,6 +34,7 @@ use commands::flow_monitor_cmd::{ FlowMonitorState, FlowQueryServiceState, FlowReplayerState, QuickFilterManagerState, SessionManagerState, }; +use commands::machine_id_cmd::MachineIdState; use commands::plugin_cmd::PluginManagerState; use commands::provider_pool_cmd::{CredentialSyncServiceState, ProviderPoolServiceState}; use commands::resilience_cmd::ResilienceConfigState; @@ -1530,6 +1531,11 @@ pub fn run() { let token_cache_service = TokenCacheService::new(); let token_cache_service_state = TokenCacheServiceState(Arc::new(token_cache_service)); + // Initialize MachineIdService + let machine_id_service = services::machine_id_service::MachineIdService::new() + .expect("Failed to initialize MachineIdService"); + let machine_id_service_state: MachineIdState = Arc::new(RwLock::new(machine_id_service)); + // Initialize RouterConfigState let router_config_state = RouterConfigState::default(); @@ -1720,6 +1726,7 @@ pub fn run() { .manage(provider_pool_service_state) .manage(credential_sync_service_state) .manage(token_cache_service_state) + .manage(machine_id_service_state) .manage(router_config_state) .manage(resilience_config_state) .manage(telemetry_state) @@ -2315,6 +2322,23 @@ pub fn run() { commands::browser_interceptor_cmd::show_status_notification, // Auto fix commands commands::auto_fix_cmd::auto_fix_configuration, + // Machine ID commands + commands::machine_id_cmd::get_current_machine_id, + commands::machine_id_cmd::set_machine_id, + commands::machine_id_cmd::generate_random_machine_id, + commands::machine_id_cmd::validate_machine_id, + commands::machine_id_cmd::check_admin_privileges, + commands::machine_id_cmd::get_os_type, + commands::machine_id_cmd::backup_machine_id_to_file, + commands::machine_id_cmd::restore_machine_id_from_file, + commands::machine_id_cmd::format_machine_id, + commands::machine_id_cmd::detect_machine_id_format, + commands::machine_id_cmd::convert_machine_id_format, + commands::machine_id_cmd::get_machine_id_history, + commands::machine_id_cmd::clear_machine_id_override, + commands::machine_id_cmd::copy_machine_id_to_clipboard, + commands::machine_id_cmd::paste_machine_id_from_clipboard, + commands::machine_id_cmd::get_system_info, // Network commands commands::network_cmd::get_network_info, ]) diff --git a/src-tauri/src/models/machine_id.rs b/src-tauri/src/models/machine_id.rs new file mode 100644 index 000000000..45641d7a0 --- /dev/null +++ b/src-tauri/src/models/machine_id.rs @@ -0,0 +1,208 @@ +use serde::{Deserialize, Serialize}; + +/// 机器码信息结构 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MachineIdInfo { + /// 当前机器码 + pub current_id: String, + /// 原始机器码(如果有备份) + pub original_id: Option, + /// 操作系统平台 + pub platform: String, + /// 是否可以修改 + pub can_modify: bool, + /// 是否需要管理员权限 + pub requires_admin: bool, + /// 是否存在备份 + pub backup_exists: bool, + /// 机器码格式类型 + pub format_type: MachineIdFormat, +} + +/// 机器码操作结果 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MachineIdResult { + /// 操作是否成功 + pub success: bool, + /// 结果消息 + pub message: String, + /// 是否需要重启 + pub requires_restart: bool, + /// 是否需要管理员权限 + pub requires_admin: bool, + /// 新的机器码(如果操作成功) + pub new_machine_id: Option, +} + +/// 管理员权限状态 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AdminStatus { + /// 是否具有管理员权限 + pub is_admin: bool, + /// 操作系统平台 + pub platform: String, + /// 权限提升方法说明 + pub elevation_method: Option, + /// 权限检查是否成功 + pub check_success: bool, +} + +/// 机器码格式类型 +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum MachineIdFormat { + /// UUID 格式 (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx) + Uuid, + /// 32位十六进制格式 (xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx) + #[serde(rename = "hex32")] + Hex32, + /// 其他格式 + #[serde(rename = "unknown")] + Unknown, +} + +/// 机器码备份信息 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MachineIdBackup { + /// 备份的机器码 + pub machine_id: String, + /// 备份时间戳 + pub timestamp: i64, + /// 操作系统平台 + pub platform: String, + /// 机器码格式 + pub format: MachineIdFormat, + /// 备份描述 + pub description: Option, +} + +/// 机器码历史记录 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MachineIdHistory { + /// 记录ID + pub id: String, + /// 机器码 + pub machine_id: String, + /// 操作时间戳 + pub timestamp: String, + /// 操作系统平台 + pub platform: String, + /// 备份路径(可选) + pub backup_path: Option, +} + +/// 机器码操作类型 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum MachineIdOperation { + /// 获取当前机器码 + Get, + /// 设置新机器码 + Set, + /// 生成随机机器码 + Generate, + /// 备份机器码 + Backup, + /// 恢复机器码 + Restore, + /// 重置为原始机器码 + Reset, +} + +/// 机器码验证结果 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MachineIdValidation { + /// 是否有效 + pub is_valid: bool, + /// 检测到的格式 + pub detected_format: MachineIdFormat, + /// 验证错误信息 + pub error_message: Option, + /// 格式化后的机器码(如果有效) + pub formatted_id: Option, +} + +impl MachineIdFormat { + /// 从字符串检测机器码格式 + pub fn detect(machine_id: &str) -> Self { + let cleaned = machine_id.replace("-", "").replace(" ", "").to_lowercase(); + + // 检查UUID格式:8-4-4-4-12个十六进制字符 + if machine_id.contains("-") && machine_id.len() == 36 { + let parts: Vec<&str> = machine_id.split('-').collect(); + if parts.len() == 5 + && parts[0].len() == 8 + && parts[1].len() == 4 + && parts[2].len() == 4 + && parts[3].len() == 4 + && parts[4].len() == 12 + && cleaned.chars().all(|c| c.is_ascii_hexdigit()) + { + return MachineIdFormat::Uuid; + } + } + + // 检查32位十六进制格式 + if cleaned.len() == 32 && cleaned.chars().all(|c| c.is_ascii_hexdigit()) { + return MachineIdFormat::Hex32; + } + + MachineIdFormat::Unknown + } + + /// 格式化机器码为标准格式 + pub fn format_machine_id(&self, machine_id: &str) -> Result { + let cleaned = machine_id.replace("-", "").replace(" ", "").to_lowercase(); + + match self { + MachineIdFormat::Uuid => { + if cleaned.len() != 32 { + return Err("UUID format requires 32 hex characters".to_string()); + } + if !cleaned.chars().all(|c| c.is_ascii_hexdigit()) { + return Err("UUID format requires hex characters only".to_string()); + } + Ok(format!( + "{}-{}-{}-{}-{}", + &cleaned[0..8], + &cleaned[8..12], + &cleaned[12..16], + &cleaned[16..20], + &cleaned[20..32] + )) + } + MachineIdFormat::Hex32 => { + if cleaned.len() != 32 { + return Err("Hex32 format requires 32 hex characters".to_string()); + } + if !cleaned.chars().all(|c| c.is_ascii_hexdigit()) { + return Err("Hex32 format requires hex characters only".to_string()); + } + Ok(cleaned) + } + MachineIdFormat::Unknown => Err("Cannot format unknown machine ID format".to_string()), + } + } +} + +impl std::fmt::Display for MachineIdFormat { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MachineIdFormat::Uuid => write!(f, "uuid"), + MachineIdFormat::Hex32 => write!(f, "hex32"), + MachineIdFormat::Unknown => write!(f, "unknown"), + } + } +} + +impl std::fmt::Display for MachineIdOperation { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MachineIdOperation::Get => write!(f, "Get"), + MachineIdOperation::Set => write!(f, "Set"), + MachineIdOperation::Generate => write!(f, "Generate"), + MachineIdOperation::Backup => write!(f, "Backup"), + MachineIdOperation::Restore => write!(f, "Restore"), + MachineIdOperation::Reset => write!(f, "Reset"), + } + } +} diff --git a/src-tauri/src/models/mod.rs b/src-tauri/src/models/mod.rs index 90fb4f251..470d8decb 100644 --- a/src-tauri/src/models/mod.rs +++ b/src-tauri/src/models/mod.rs @@ -1,6 +1,7 @@ pub mod anthropic; pub mod app_type; pub mod codewhisperer; +pub mod machine_id; pub mod mcp_model; pub mod openai; pub mod prompt_model; diff --git a/src-tauri/src/services/machine_id_service.rs b/src-tauri/src/services/machine_id_service.rs new file mode 100644 index 000000000..c115562a6 --- /dev/null +++ b/src-tauri/src/services/machine_id_service.rs @@ -0,0 +1,727 @@ +use crate::models::machine_id::*; +use dirs; +use serde_json; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; +use tracing; +use uuid::Uuid; + +#[cfg(target_os = "windows")] +use std::ptr; +#[cfg(target_os = "windows")] +use winapi::um::winnt::KEY_READ; +#[cfg(target_os = "windows")] +use winreg::{enums::*, RegKey}; + +pub struct MachineIdService { + backup_dir: PathBuf, + history_file: PathBuf, +} + +impl MachineIdService { + pub fn new() -> Result { + let app_data_dir = dirs::data_dir() + .ok_or("Failed to get app data directory")? + .join("proxycast"); + + let backup_dir = app_data_dir.join("machine_id_backups"); + let history_file = app_data_dir.join("machine_id_history.json"); + + // 确保应用数据目录存在 + if let Err(e) = fs::create_dir_all(&app_data_dir) { + return Err(format!("Failed to create app data directory: {}", e)); + } + + // 确保备份目录存在 + if let Err(e) = fs::create_dir_all(&backup_dir) { + return Err(format!("Failed to create backup directory: {}", e)); + } + + Ok(MachineIdService { + backup_dir, + history_file, + }) + } + + /// 获取当前操作系统类型 + pub fn get_os_type() -> String { + std::env::consts::OS.to_string() + } + + /// 获取当前机器码信息 + pub async fn get_current_machine_id(&self) -> Result { + let os = Self::get_os_type(); + let requires_admin = self.check_requires_admin().await?; + let backup_exists = self.check_backup_exists(); + + match os.as_str() { + "windows" => { + self.get_windows_machine_id(requires_admin, backup_exists) + .await + } + "macos" => { + self.get_macos_machine_id(requires_admin, backup_exists) + .await + } + "linux" => { + self.get_linux_machine_id(requires_admin, backup_exists) + .await + } + _ => Err(format!("Unsupported operating system: {}", os)), + } + } + + /// 设置新的机器码 + pub async fn set_machine_id(&self, new_id: &str) -> Result { + let validation = self.validate_machine_id(new_id)?; + if !validation.is_valid { + return Ok(MachineIdResult { + success: false, + message: validation + .error_message + .unwrap_or("Invalid machine ID format".to_string()), + requires_restart: false, + requires_admin: false, + new_machine_id: None, + }); + } + + let formatted_id = validation.formatted_id.unwrap(); + let os = Self::get_os_type(); + + // 在设置前先备份当前机器码 + if let Err(e) = self.create_auto_backup().await { + tracing::warn!("Failed to create auto backup: {}", e); + } + + let result = match os.as_str() { + "windows" => self.set_windows_machine_id(&formatted_id).await, + "macos" => self.set_macos_machine_id(&formatted_id).await, + "linux" => self.set_linux_machine_id(&formatted_id).await, + _ => Ok(MachineIdResult { + success: false, + message: format!("Unsupported operating system: {}", os), + requires_restart: false, + requires_admin: false, + new_machine_id: None, + }), + }; + + // 添加到历史记录 + if let Ok(ref res) = result { + if res.success { + if let Err(e) = self.add_history_record(formatted_id.clone(), None) { + tracing::warn!("Failed to add history record: {}", e); + } + } + } + + result + } + + /// 生成随机机器码 + pub fn generate_random_machine_id(&self) -> String { + let uuid = Uuid::new_v4(); + uuid.to_string() + } + + /// 检查管理员权限 + pub async fn check_admin_privileges(&self) -> Result { + let os = Self::get_os_type(); + + match os.as_str() { + "windows" => self.check_windows_admin().await, + "macos" => self.check_macos_admin().await, + "linux" => self.check_linux_admin().await, + _ => Ok(AdminStatus { + is_admin: false, + platform: os, + elevation_method: None, + check_success: false, + }), + } + } + + /// 验证机器码格式 + pub fn validate_machine_id(&self, machine_id: &str) -> Result { + let detected_format = MachineIdFormat::detect(machine_id); + + match detected_format.format_machine_id(machine_id) { + Ok(formatted) => Ok(MachineIdValidation { + is_valid: true, + detected_format, + error_message: None, + formatted_id: Some(formatted), + }), + Err(error) => Ok(MachineIdValidation { + is_valid: false, + detected_format, + error_message: Some(error), + formatted_id: None, + }), + } + } + + /// 备份机器码到文件 + pub async fn backup_machine_id(&self, file_path: &str) -> Result { + let current_info = self.get_current_machine_id().await?; + + let backup = MachineIdBackup { + machine_id: current_info.current_id.clone(), + timestamp: chrono::Utc::now().timestamp(), + platform: current_info.platform, + format: current_info.format_type, + description: Some("Manual backup".to_string()), + }; + + let backup_json = serde_json::to_string_pretty(&backup) + .map_err(|e| format!("Failed to serialize backup: {}", e))?; + + let write_result = fs::write(file_path, backup_json) + .map_err(|e| format!("Failed to write backup file: {}", e)); + + // 添加到历史记录 + if write_result.is_ok() { + if let Err(e) = + self.add_history_record(current_info.current_id, Some(file_path.to_string())) + { + tracing::warn!("Failed to add backup history record: {}", e); + } + } + + write_result.map(|_| true) + } + + /// 从文件恢复机器码 + pub async fn restore_machine_id(&self, file_path: &str) -> Result { + let backup_content = fs::read_to_string(file_path) + .map_err(|e| format!("Failed to read backup file: {}", e))?; + + let backup: MachineIdBackup = serde_json::from_str(&backup_content) + .map_err(|e| format!("Failed to parse backup file: {}", e))?; + + self.set_machine_id(&backup.machine_id).await + } + + // === Windows 特定实现 === + + #[cfg(target_os = "windows")] + async fn get_windows_machine_id( + &self, + requires_admin: bool, + backup_exists: bool, + ) -> Result { + let machine_id = self.read_windows_registry_machine_id()?; + + Ok(MachineIdInfo { + current_id: machine_id, + original_id: self.get_original_backup(), + platform: "Windows".to_string(), + can_modify: true, + requires_admin, + backup_exists, + format_type: MachineIdFormat::Uuid, + }) + } + + #[cfg(not(target_os = "windows"))] + async fn get_windows_machine_id( + &self, + requires_admin: bool, + backup_exists: bool, + ) -> Result { + Err("Windows machine ID not supported on this platform".to_string()) + } + + #[cfg(target_os = "windows")] + fn read_windows_registry_machine_id(&self) -> Result { + use winreg::{enums::*, RegKey}; + + let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); + let key = hklm.open_subkey_with_flags("SOFTWARE\\Microsoft\\Cryptography", KEY_READ) + .map_err(|e| { + tracing::error!("Failed to open Windows registry key: {}", e); + format!("Failed to access Windows machine ID registry key: {}. This may indicate a system configuration issue.", e) + })?; + + let machine_guid: String = key.get_value("MachineGuid") + .map_err(|e| { + tracing::error!("Failed to read MachineGuid from registry: {}", e); + format!("Failed to read Windows MachineGuid: {}. The registry value may be missing or corrupted.", e) + })?; + + // 验证读取到的GUID格式 + let cleaned_guid = machine_guid.trim(); + if cleaned_guid.is_empty() { + return Err( + "Windows MachineGuid is empty. This indicates a system configuration problem." + .to_string(), + ); + } + + // 基本格式验证 + let format = MachineIdFormat::detect(cleaned_guid); + if matches!(format, MachineIdFormat::Unknown) { + tracing::warn!( + "Windows MachineGuid has unexpected format: {}", + cleaned_guid + ); + // 不返回错误,因为某些Windows系统可能有非标准格式 + } + + Ok(cleaned_guid.to_string()) + } + + #[cfg(target_os = "windows")] + async fn set_windows_machine_id(&self, new_id: &str) -> Result { + // Windows 需要管理员权限来修改注册表 + let admin_status = self.check_windows_admin().await?; + if !admin_status.is_admin { + return Ok(MachineIdResult { + success: false, + message: "Administrator privileges required to modify Windows machine ID" + .to_string(), + requires_restart: false, + requires_admin: true, + new_machine_id: None, + }); + } + + match self.write_windows_registry_machine_id(new_id) { + Ok(_) => Ok(MachineIdResult { + success: true, + message: "Machine ID updated successfully. Restart may be required for some applications.".to_string(), + requires_restart: true, + requires_admin: false, + new_machine_id: Some(new_id.to_string()), + }), + Err(e) => Ok(MachineIdResult { + success: false, + message: e, + requires_restart: false, + requires_admin: true, + new_machine_id: None, + }), + } + } + + #[cfg(target_os = "windows")] + fn write_windows_registry_machine_id(&self, new_id: &str) -> Result<(), String> { + let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); + let (key, _) = hklm + .create_subkey("SOFTWARE\\Microsoft\\Cryptography") + .map_err(|e| format!("Failed to create/open registry key: {}", e))?; + + key.set_value("MachineGuid", &new_id) + .map_err(|e| format!("Failed to set MachineGuid: {}", e))?; + + Ok(()) + } + + #[cfg(not(target_os = "windows"))] + fn write_windows_registry_machine_id(&self, _new_id: &str) -> Result<(), String> { + Err("Windows registry modification not supported on this platform".to_string()) + } + + #[cfg(not(target_os = "windows"))] + fn read_windows_registry_machine_id(&self) -> Result { + Err("Windows registry reading not supported on this platform".to_string()) + } + + #[cfg(target_os = "windows")] + async fn check_windows_admin(&self) -> Result { + // 尝试多种方法检查管理员权限 + + // 方法1:尝试打开需要管理员权限的注册表项 + let registry_check = { + use winreg::{enums::*, RegKey}; + let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); + hklm.open_subkey_with_flags("SOFTWARE\\Microsoft\\Cryptography", KEY_WRITE) + .is_ok() + }; + + // 方法2:运行 net session 命令作为后备 + let net_session_check = Command::new("net") + .args(&["session"]) + .output() + .map(|result| result.status.success()) + .unwrap_or(false); + + // 方法3:使用 Windows API 检查 (需要额外实现) + // 这里可以添加更复杂的 Windows API 调用 + + let is_admin = registry_check || net_session_check; + + Ok(AdminStatus { + is_admin, + platform: "Windows".to_string(), + elevation_method: Some("Right-click and select 'Run as administrator'".to_string()), + check_success: true, + }) + } + + #[cfg(not(target_os = "windows"))] + async fn check_windows_admin(&self) -> Result { + Err("Windows admin check not supported on this platform".to_string()) + } + + #[cfg(not(target_os = "windows"))] + async fn set_windows_machine_id(&self, _new_id: &str) -> Result { + Ok(MachineIdResult { + success: false, + message: "Windows machine ID modification not supported on this platform".to_string(), + requires_restart: false, + requires_admin: false, + new_machine_id: None, + }) + } + + // === macOS 特定实现 === + + #[cfg(target_os = "macos")] + async fn get_macos_machine_id( + &self, + requires_admin: bool, + backup_exists: bool, + ) -> Result { + let machine_id = self.read_macos_machine_id().await?; + + Ok(MachineIdInfo { + current_id: machine_id, + original_id: self.get_original_backup(), + platform: "macOS".to_string(), + can_modify: true, // macOS 使用应用层覆盖 + requires_admin, + backup_exists, + format_type: MachineIdFormat::Uuid, + }) + } + + #[cfg(not(target_os = "macos"))] + async fn get_macos_machine_id( + &self, + requires_admin: bool, + backup_exists: bool, + ) -> Result { + Err("macOS machine ID not supported on this platform".to_string()) + } + + #[cfg(target_os = "macos")] + async fn read_macos_machine_id(&self) -> Result { + // 首先检查是否有应用层覆盖 + if let Ok(override_id) = self.read_macos_override() { + return Ok(override_id); + } + + // 读取系统原始 UUID + let output = Command::new("ioreg") + .args(&["-rd1", "-c", "IOPlatformExpertDevice"]) + .output() + .map_err(|e| format!("Failed to execute ioreg: {}", e))?; + + let output_str = String::from_utf8_lossy(&output.stdout); + + for line in output_str.lines() { + if line.contains("IOPlatformUUID") { + if let Some(uuid_part) = line.split('"').nth(3) { + return Ok(uuid_part.to_string()); + } + } + } + + Err("Failed to find IOPlatformUUID in ioreg output".to_string()) + } + + #[cfg(target_os = "macos")] + async fn set_macos_machine_id(&self, new_id: &str) -> Result { + // macOS 使用应用层覆盖文件 + match self.write_macos_override(new_id) { + Ok(_) => Ok(MachineIdResult { + success: true, + message: "Machine ID override created successfully. Applications using this override will see the new ID.".to_string(), + requires_restart: false, + requires_admin: false, + new_machine_id: Some(new_id.to_string()), + }), + Err(e) => Ok(MachineIdResult { + success: false, + message: e, + requires_restart: false, + requires_admin: false, + new_machine_id: None, + }), + } + } + + #[cfg(target_os = "macos")] + fn read_macos_override(&self) -> Result { + let override_file = dirs::data_dir() + .ok_or("Failed to get app data directory")? + .join("proxycast") + .join("machine-id-override"); + + fs::read_to_string(override_file).map_err(|e| format!("No override file found: {}", e)) + } + + #[cfg(target_os = "macos")] + fn write_macos_override(&self, new_id: &str) -> Result<(), String> { + let override_dir = dirs::data_dir() + .ok_or("Failed to get app data directory")? + .join("proxycast"); + + fs::create_dir_all(&override_dir) + .map_err(|e| format!("Failed to create override directory: {}", e))?; + + let override_file = override_dir.join("machine-id-override"); + fs::write(override_file, new_id) + .map_err(|e| format!("Failed to write override file: {}", e))?; + + Ok(()) + } + + #[cfg(target_os = "macos")] + async fn check_macos_admin(&self) -> Result { + // macOS 不需要管理员权限(使用应用层覆盖) + Ok(AdminStatus { + is_admin: true, // 应用层覆盖不需要管理员权限 + platform: "macOS".to_string(), + elevation_method: None, + check_success: true, + }) + } + + // === Linux 特定实现 === + + #[cfg(target_os = "linux")] + async fn get_linux_machine_id( + &self, + requires_admin: bool, + backup_exists: bool, + ) -> Result { + let machine_id = self.read_linux_machine_id()?; + + Ok(MachineIdInfo { + current_id: machine_id, + original_id: self.get_original_backup(), + platform: "Linux".to_string(), + can_modify: true, + requires_admin, + backup_exists, + format_type: MachineIdFormat::Hex32, + }) + } + + #[cfg(not(target_os = "linux"))] + async fn get_linux_machine_id( + &self, + requires_admin: bool, + backup_exists: bool, + ) -> Result { + Err("Linux machine ID not supported on this platform".to_string()) + } + + #[cfg(target_os = "linux")] + fn read_linux_machine_id(&self) -> Result { + // 尝试读取 /etc/machine-id + if let Ok(content) = fs::read_to_string("/etc/machine-id") { + return Ok(content.trim().to_string()); + } + + // 尝试读取 /var/lib/dbus/machine-id + if let Ok(content) = fs::read_to_string("/var/lib/dbus/machine-id") { + return Ok(content.trim().to_string()); + } + + Err( + "Failed to read Linux machine ID from /etc/machine-id or /var/lib/dbus/machine-id" + .to_string(), + ) + } + + #[cfg(target_os = "linux")] + async fn set_linux_machine_id(&self, new_id: &str) -> Result { + let admin_status = self.check_linux_admin().await?; + if !admin_status.is_admin { + return Ok(MachineIdResult { + success: false, + message: "Root privileges required to modify Linux machine ID".to_string(), + requires_restart: false, + requires_admin: true, + new_machine_id: None, + }); + } + + // 转换为 32 位十六进制格式(去除连字符) + let hex_id = new_id.replace("-", "").to_lowercase(); + + // 尝试写入 /etc/machine-id + match fs::write("/etc/machine-id", &hex_id) { + Ok(_) => { + // 同时更新 /var/lib/dbus/machine-id(如果存在) + let _ = fs::write("/var/lib/dbus/machine-id", &hex_id); + + Ok(MachineIdResult { + success: true, + message: "Machine ID updated successfully. Some services may require restart." + .to_string(), + requires_restart: true, + requires_admin: false, + new_machine_id: Some(hex_id), + }) + } + Err(e) => Ok(MachineIdResult { + success: false, + message: format!("Failed to write machine ID: {}", e), + requires_restart: false, + requires_admin: true, + new_machine_id: None, + }), + } + } + + #[cfg(target_os = "linux")] + async fn check_linux_admin(&self) -> Result { + let output = Command::new("id") + .args(&["-u"]) + .output() + .map_err(|e| format!("Failed to check user ID: {}", e))?; + + let uid_str = String::from_utf8_lossy(&output.stdout); + let uid: u32 = uid_str + .trim() + .parse() + .map_err(|e| format!("Failed to parse user ID: {}", e))?; + + Ok(AdminStatus { + is_admin: uid == 0, + platform: "Linux".to_string(), + elevation_method: Some("sudo or run as root".to_string()), + check_success: true, + }) + } + + #[cfg(not(target_os = "linux"))] + async fn check_linux_admin(&self) -> Result { + Err("Linux admin check not supported on this platform".to_string()) + } + + #[cfg(not(target_os = "linux"))] + async fn set_linux_machine_id(&self, _new_id: &str) -> Result { + Ok(MachineIdResult { + success: false, + message: "Linux machine ID modification not supported on this platform".to_string(), + requires_restart: false, + requires_admin: false, + new_machine_id: None, + }) + } + + // === 通用辅助方法 === + + async fn check_requires_admin(&self) -> Result { + let admin_status = self.check_admin_privileges().await?; + Ok(!admin_status.is_admin && admin_status.check_success) + } + + fn check_backup_exists(&self) -> bool { + let backup_file = self.backup_dir.join("original_machine_id.json"); + backup_file.exists() + } + + fn get_original_backup(&self) -> Option { + let backup_file = self.backup_dir.join("original_machine_id.json"); + if let Ok(content) = fs::read_to_string(backup_file) { + if let Ok(backup) = serde_json::from_str::(&content) { + return Some(backup.machine_id); + } + } + None + } + + async fn create_auto_backup(&self) -> Result<(), String> { + // 如果已有备份则跳过 + if self.check_backup_exists() { + return Ok(()); + } + + let current_info = self.get_current_machine_id().await?; + let backup_file = self.backup_dir.join("original_machine_id.json"); + + let backup = MachineIdBackup { + machine_id: current_info.current_id, + timestamp: chrono::Utc::now().timestamp(), + platform: current_info.platform, + format: current_info.format_type, + description: Some("Auto backup before first modification".to_string()), + }; + + let backup_json = serde_json::to_string_pretty(&backup) + .map_err(|e| format!("Failed to serialize backup: {}", e))?; + + fs::write(backup_file, backup_json) + .map_err(|e| format!("Failed to create auto backup: {}", e))?; + + Ok(()) + } + + // === 历史记录管理 === + + /// 加载历史记录 + fn load_history(&self) -> Result, String> { + if !self.history_file.exists() { + return Ok(vec![]); + } + + let content = fs::read_to_string(&self.history_file) + .map_err(|e| format!("Failed to read history file: {}", e))?; + + let history: Vec = + serde_json::from_str(&content).unwrap_or_else(|_| vec![]); + + Ok(history) + } + + /// 保存历史记录 + fn save_history(&self, history: &[MachineIdHistory]) -> Result<(), String> { + let history_json = serde_json::to_string_pretty(history) + .map_err(|e| format!("Failed to serialize history: {}", e))?; + + fs::write(&self.history_file, history_json) + .map_err(|e| format!("Failed to save history: {}", e))?; + + Ok(()) + } + + /// 添加历史记录 + pub fn add_history_record( + &self, + machine_id: String, + backup_path: Option, + ) -> Result<(), String> { + let mut history = self.load_history().unwrap_or_else(|_| vec![]); + + let record = MachineIdHistory { + id: uuid::Uuid::new_v4().to_string(), + machine_id, + timestamp: chrono::Utc::now().to_rfc3339(), + platform: Self::get_os_type(), + backup_path, + }; + + history.push(record); + + // 保留最近100条记录 + if history.len() > 100 { + history.drain(0..history.len() - 100); + } + + self.save_history(&history)?; + Ok(()) + } + + /// 获取历史记录 + pub fn get_history(&self) -> Result, String> { + self.load_history() + } +} diff --git a/src-tauri/src/services/mod.rs b/src-tauri/src/services/mod.rs index d12511500..b34c9bbed 100644 --- a/src-tauri/src/services/mod.rs +++ b/src-tauri/src/services/mod.rs @@ -1,6 +1,7 @@ pub mod backup_service; pub mod kiro_event_service; pub mod live_sync; +pub mod machine_id_service; pub mod mcp_service; pub mod mcp_sync; pub mod prompt_service; diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index a379902f1..dc2de625d 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "ProxyCast", - "version": "0.19.0", + "version": "0.20.0", "identifier": "com.proxycast.app", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/App.tsx b/src/App.tsx index 568739326..1fc36a696 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,6 +7,8 @@ import { ConfigManagementPage } from "./components/config/ConfigManagementPage"; import { FlowMonitorPage } from "./pages"; import { ToolsPage } from "./components/tools/ToolsPage"; import { BrowserInterceptorTool } from "./components/tools/browser-interceptor/BrowserInterceptorTool"; +import { MachineIdTool } from "./components/tools/machine-id/MachineIdTool"; +import { Toaster } from "./components/ui/sonner"; import { flowEventManager } from "./lib/flowEventManager"; type Page = @@ -16,6 +18,7 @@ type Page = | "flow-monitor" | "tools" | "browser-interceptor" + | "machine-id" | "settings"; function App() { @@ -41,6 +44,8 @@ function App() { return ; case "browser-interceptor": return ; + case "machine-id": + return ; case "settings": return ; default: @@ -52,6 +57,7 @@ function App() {
{renderPage()}
+
); } diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index e0c14236d..fdadac802 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -15,6 +15,7 @@ type Page = | "flow-monitor" | "tools" | "browser-interceptor" + | "machine-id" | "settings"; interface SidebarProps { diff --git a/src/components/tools/ToolsPage.tsx b/src/components/tools/ToolsPage.tsx index cb2cf5cf1..8fe6e3ab9 100644 --- a/src/components/tools/ToolsPage.tsx +++ b/src/components/tools/ToolsPage.tsx @@ -1,5 +1,5 @@ import React, { useState } from "react"; -import { Globe, Plus, Settings, Activity } from "lucide-react"; +import { Globe, Plus, Settings, Activity, Cpu } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { Card, @@ -19,6 +19,7 @@ interface ToolsPageProps { | "flow-monitor" | "tools" | "browser-interceptor" + | "machine-id" | "settings", ) => void; } @@ -87,6 +88,10 @@ export function ToolsPage({ onNavigate }: ToolsPageProps) { onNavigate("browser-interceptor"); }; + const handleMachineIdClick = () => { + onNavigate("machine-id"); + }; + return (
@@ -96,7 +101,7 @@ export function ToolsPage({ onNavigate }: ToolsPageProps) { ProxyCast 提供的实用工具集合

- 1 个工具 + 2 个工具
@@ -108,6 +113,13 @@ export function ToolsPage({ onNavigate }: ToolsPageProps) { onClick={handleBrowserInterceptorClick} /> + } + onClick={handleMachineIdClick} + /> + {/* 未来可以添加更多工具 */} void; +} + +export function MachineIdHistoryPanel({ + _onRefresh, +}: MachineIdHistoryPanelProps) { + const { toast } = useToast(); + const [history, setHistory] = useState([]); + const [loading, setLoading] = useState(true); + + const loadHistory = useCallback(async () => { + try { + setLoading(true); + const historyData = await machineIdApi.getMachineIdHistory(); + setHistory(historyData); + } catch (error) { + console.error("加载历史记录失败:", error); + toast({ + variant: "destructive", + title: "加载失败", + description: "无法加载机器码历史记录", + }); + } finally { + setLoading(false); + } + }, [toast]); + + useEffect(() => { + loadHistory(); + }, [loadHistory]); + + const formatTimestamp = (timestamp: string) => { + try { + return new Date(timestamp).toLocaleString("zh-CN", { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + }); + } catch { + return timestamp; + } + }; + + const getPlatformIcon = (platform: string) => { + switch (platform.toLowerCase()) { + case "windows": + return "🪟"; + case "macos": + return "🍎"; + case "linux": + return "🐧"; + default: + return "💻"; + } + }; + + if (loading) { + return ( + + + + + 操作历史 + + + +
+
+ +

加载历史记录中...

+
+
+
+
+ ); + } + + return ( +
+ + +
+
+ + + 操作历史 + + 查看机器码的历史修改记录 +
+ +
+
+ + {history.length === 0 ? ( +
+
+ +
+

暂无历史记录

+

+ 当前还没有机器码操作历史记录 +

+
+

💡 提示:历史记录功能正在开发中

+

完成后将记录所有机器码修改操作,包括:

+
    +
  • 机器码修改时间
  • +
  • 修改前后的值
  • +
  • 操作平台信息
  • +
  • 备份文件路径
  • +
+
+
+ ) : ( +
+ {history.map((record, index) => ( + + +
+
+
+ + {getPlatformIcon(record.platform)} + +
+
+

机器码操作

+
+ + {record.platform} + +
+ + {formatTimestamp(record.timestamp)} +
+
+
+
+
+
+ +
+
+

+ 机器码 +

+
+ {record.machine_id} +
+
+ + {record.backup_path && ( +
+

+ 备份路径 +

+
+ + + {record.backup_path} + +
+
+ )} +
+
+
+ ))} +
+ )} +
+
+ + {/* 说明信息 */} + + + + + 关于历史记录 + + + +

历史记录功能目前处于开发阶段,暂时返回空记录。

+

完整实现后将包含:

+
    +
  • 所有机器码修改操作的时间记录
  • +
  • 修改前后的机器码值对比
  • +
  • 操作系统和平台信息
  • +
  • 相关备份文件的路径信息
  • +
  • 操作结果和状态信息
  • +
+
+
+
+ ); +} diff --git a/src/components/tools/machine-id/MachineIdInfoPanel.tsx b/src/components/tools/machine-id/MachineIdInfoPanel.tsx new file mode 100644 index 000000000..5aafd15ce --- /dev/null +++ b/src/components/tools/machine-id/MachineIdInfoPanel.tsx @@ -0,0 +1,257 @@ +import { useState } from "react"; +import { + Copy, + Shield, + ShieldCheck, + AlertTriangle, + Monitor, + HardDrive, +} from "lucide-react"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Separator } from "@/components/ui/separator"; +import { useToast } from "@/hooks/use-toast"; +import { MachineIdInfo, AdminStatus } from "@/lib/api/machineId"; +import { machineIdApi, machineIdUtils } from "@/lib/api/machineId"; + +interface MachineIdInfoPanelProps { + machineIdInfo: MachineIdInfo | null; + adminStatus: AdminStatus | null; + onRefresh: () => void; +} + +export function MachineIdInfoPanel({ + machineIdInfo, + adminStatus, + onRefresh, +}: MachineIdInfoPanelProps) { + const { toast } = useToast(); + const [copying, setCopying] = useState(false); + + const handleCopyMachineId = async () => { + if (!machineIdInfo) return; + + try { + setCopying(true); + await machineIdApi.copyMachineIdToClipboard(machineIdInfo.current_id); + toast({ + title: "复制成功", + description: "机器码已复制到剪贴板", + }); + } catch (error) { + console.error("复制失败:", error); + toast({ + variant: "destructive", + title: "复制失败", + description: "无法复制机器码到剪贴板", + }); + } finally { + setCopying(false); + } + }; + + if (!machineIdInfo || !adminStatus) { + return ( + + + + + 无法获取机器码信息 + + + +

+ 无法加载机器码信息,请检查系统状态或权限设置。 +

+ +
+
+ ); + } + + const getStatusBadge = () => { + if (!machineIdInfo.can_modify) { + return ( + + + 只读 + + ); + } + + if (machineIdInfo.requires_admin && !adminStatus.is_admin) { + return ( + + + 需要管理员权限 + + ); + } + + return ( + + + 可修改 + + ); + }; + + const getFormatBadge = () => { + const color = + machineIdInfo.format_type === "uuid" + ? "default" + : machineIdInfo.format_type === "hex32" + ? "secondary" + : "outline"; + + return ( + + {machineIdUtils.getFormatDisplayName(machineIdInfo.format_type)} + + ); + }; + + return ( +
+ {/* 主要信息卡片 */} + + +
+
+ + + 当前机器码 + + + 系统唯一标识符,用于设备识别和授权验证 + +
+ {getStatusBadge()} +
+
+ +
+
+

机器码

+

+ {machineIdInfo.current_id} +

+
+
+ {getFormatBadge()} + +
+
+ + {machineIdInfo.original_id && ( + <> + +
+
+ +

+ 检测到机器码覆盖 +

+
+

+ 原始机器码: +

+

+ {machineIdInfo.original_id} +

+
+ + )} +
+
+ + {/* 系统信息卡片 */} + + + + + 系统状态 + + + +
+
+

操作系统

+

+ {machineIdUtils.getPlatformDisplayName(machineIdInfo.platform)} +

+
+ +
+

管理员权限

+
+ {adminStatus.is_admin ? ( + + + 已获取 + + ) : ( + + + 未获取 + + )} +
+
+ +
+

备份状态

+
+ {machineIdInfo.backup_exists ? ( + 已备份 + ) : ( + 未备份 + )} +
+
+
+
+
+ + {/* 权限提醒 */} + {machineIdInfo.requires_admin && !adminStatus.is_admin && ( + + + + + 权限提醒 + + + +

+ 在当前平台({machineIdInfo.platform})上修改机器码需要管理员权限。 +

+ {adminStatus.elevation_method && ( +

+ 提升权限方法:{adminStatus.elevation_method} +

+ )} +
+
+ )} +
+ ); +} diff --git a/src/components/tools/machine-id/MachineIdManagePanel.tsx b/src/components/tools/machine-id/MachineIdManagePanel.tsx new file mode 100644 index 000000000..a56e64dd3 --- /dev/null +++ b/src/components/tools/machine-id/MachineIdManagePanel.tsx @@ -0,0 +1,787 @@ +import { useState } from "react"; +import { + Save, + Shuffle, + ClipboardPaste, + Settings, + AlertTriangle, + CheckCircle, + FileUp, + FileDown, + Trash2, + FolderOpen, + RotateCcw, +} from "lucide-react"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Badge } from "@/components/ui/badge"; +import { Separator } from "@/components/ui/separator"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import { useToast } from "@/hooks/use-toast"; +import { + MachineIdInfo, + AdminStatus, + MachineIdValidation, +} from "@/lib/api/machineId"; +import { machineIdApi, machineIdUtils } from "@/lib/api/machineId"; +import { open, save } from "@tauri-apps/plugin-dialog"; + +interface MachineIdManagePanelProps { + machineIdInfo: MachineIdInfo | null; + adminStatus: AdminStatus | null; + onRefresh: () => void; +} + +export function MachineIdManagePanel({ + machineIdInfo, + adminStatus, + onRefresh, +}: MachineIdManagePanelProps) { + const { toast } = useToast(); + + // 机器码修改 + const [newMachineId, setNewMachineId] = useState(""); + const [validationResult, setValidationResult] = + useState(null); + const [_isValidating, setIsValidating] = useState(false); + const [isApplying, setIsApplying] = useState(false); + + // 格式转换 + const [formatInput, setFormatInput] = useState(""); + const [formatTarget, setFormatTarget] = useState<"uuid" | "hex32">("uuid"); + const [formatResult, setFormatResult] = useState(""); + + // 备份恢复 + const [backupPath, setBackupPath] = useState(""); + const [restorePath, setRestorePath] = useState(""); + const [isBackingUp, setIsBackingUp] = useState(false); + const [isRestoring, setIsRestoring] = useState(false); + + const canModify = + machineIdInfo?.can_modify && + (!machineIdInfo.requires_admin || adminStatus?.is_admin); + + const handleValidateMachineId = async () => { + if (!newMachineId.trim()) { + setValidationResult(null); + return; + } + + try { + setIsValidating(true); + const result = await machineIdApi.validateMachineId(newMachineId.trim()); + setValidationResult(result); + } catch (error) { + console.error("验证失败:", error); + toast({ + variant: "destructive", + title: "验证失败", + description: "无法验证机器码格式", + }); + } finally { + setIsValidating(false); + } + }; + + const handleApplyMachineId = async () => { + if (!validationResult?.is_valid) { + toast({ + variant: "destructive", + title: "无法应用", + description: "机器码格式不正确", + }); + return; + } + + try { + setIsApplying(true); + const result = await machineIdApi.setMachineId(newMachineId.trim()); + + if (result.success) { + toast({ + title: "机器码修改成功", + description: result.message, + }); + setNewMachineId(""); + setValidationResult(null); + onRefresh(); + + if (result.requires_restart) { + toast({ + title: "重启提醒", + description: "部分应用程序可能需要重启才能识别新的机器码", + duration: 8000, + }); + } + } else { + toast({ + variant: "destructive", + title: "机器码修改失败", + description: result.message, + }); + } + } catch (error) { + console.error("应用失败:", error); + toast({ + variant: "destructive", + title: "应用失败", + description: "无法设置新的机器码", + }); + } finally { + setIsApplying(false); + } + }; + + const handleGenerateRandom = async () => { + try { + const randomId = await machineIdApi.generateRandomMachineId(); + setNewMachineId(randomId); + // 自动验证生成的机器码 + const result = await machineIdApi.validateMachineId(randomId); + setValidationResult(result); + toast({ + title: "随机机器码生成成功", + description: "已生成新的随机机器码", + }); + } catch (error) { + console.error("生成失败:", error); + toast({ + variant: "destructive", + title: "生成失败", + description: "无法生成随机机器码", + }); + } + }; + + const handlePasteFromClipboard = async () => { + try { + const clipboardText = await machineIdApi.pasteMachineIdFromClipboard(); + setNewMachineId(clipboardText); + // 自动验证粘贴的机器码 + const result = await machineIdApi.validateMachineId(clipboardText); + setValidationResult(result); + toast({ + title: "从剪贴板粘贴成功", + description: "已从剪贴板粘贴机器码", + }); + } catch (error) { + console.error("粘贴失败:", error); + toast({ + variant: "destructive", + title: "粘贴失败", + description: "剪贴板中没有有效的机器码", + }); + } + }; + + const handleFormatConvert = async () => { + if (!formatInput.trim()) return; + + try { + const result = await machineIdApi.convertMachineIdFormat( + formatInput.trim(), + formatTarget, + ); + setFormatResult(result); + toast({ + title: "格式转换成功", + description: `已转换为${formatTarget === "uuid" ? "UUID" : "32位十六进制"}格式`, + }); + } catch (error) { + console.error("转换失败:", error); + toast({ + variant: "destructive", + title: "转换失败", + description: "无法转换机器码格式,请检查输入是否正确", + }); + } + }; + + const handleBackup = async () => { + if (!backupPath.trim()) return; + + try { + setIsBackingUp(true); + const success = await machineIdApi.backupMachineIdToFile( + backupPath.trim(), + ); + + if (success) { + toast({ + title: "备份成功", + description: "机器码已备份到指定文件", + }); + setBackupPath(""); + onRefresh(); + } else { + toast({ + variant: "destructive", + title: "备份失败", + description: "无法创建备份文件", + }); + } + } catch (error) { + console.error("备份失败:", error); + toast({ + variant: "destructive", + title: "备份失败", + description: "备份操作失败", + }); + } finally { + setIsBackingUp(false); + } + }; + + const handleSelectBackupFile = async () => { + try { + const selected = await save({ + title: "保存机器码备份文件", + defaultPath: `machine_id_backup_${new Date().toISOString().slice(0, 10)}.json`, + filters: [ + { + name: "JSON文件", + extensions: ["json"], + }, + ], + }); + + if (selected && typeof selected === "string") { + setBackupPath(selected); + toast({ + title: "保存位置已选择", + description: "备份文件保存路径已设置", + }); + } + } catch (error) { + console.error("文件保存对话框失败:", error); + toast({ + variant: "destructive", + title: "文件选择失败", + description: "无法打开文件保存对话框", + }); + } + }; + + const handleSelectRestoreFile = async () => { + try { + const selected = await open({ + title: "选择机器码备份文件", + filters: [ + { + name: "JSON文件", + extensions: ["json"], + }, + ], + multiple: false, + }); + + if (selected && typeof selected === "string") { + setRestorePath(selected); + toast({ + title: "文件已选择", + description: "备份文件路径已设置", + }); + } + } catch (error) { + console.error("文件选择失败:", error); + toast({ + variant: "destructive", + title: "文件选择失败", + description: "无法打开文件选择对话框", + }); + } + }; + + const handleRestore = async () => { + if (!restorePath.trim()) return; + + try { + setIsRestoring(true); + const result = await machineIdApi.restoreMachineIdFromFile( + restorePath.trim(), + ); + + if (result.success) { + toast({ + title: "恢复成功", + description: result.message, + }); + setRestorePath(""); + onRefresh(); + + if (result.requires_restart) { + toast({ + title: "重启提醒", + description: "部分应用程序可能需要重启才能识别恢复的机器码", + duration: 8000, + }); + } + } else { + toast({ + variant: "destructive", + title: "恢复失败", + description: result.message, + }); + } + } catch (error) { + console.error("恢复失败:", error); + toast({ + variant: "destructive", + title: "恢复失败", + description: "恢复操作失败", + }); + } finally { + setIsRestoring(false); + } + }; + + const handleRestoreOriginal = async () => { + if (!machineIdInfo?.original_id) return; + + try { + setIsRestoring(true); + const result = await machineIdApi.setMachineId(machineIdInfo.original_id); + + if (result.success) { + toast({ + title: "恢复成功", + description: "已恢复到原始机器码", + }); + onRefresh(); + + if (result.requires_restart) { + toast({ + title: "重启提醒", + description: "部分应用程序可能需要重启才能识别恢复的机器码", + duration: 8000, + }); + } + } else { + toast({ + variant: "destructive", + title: "恢复失败", + description: result.message, + }); + } + } catch (error) { + console.error("恢复原始机器码失败:", error); + toast({ + variant: "destructive", + title: "恢复失败", + description: "无法恢复到原始机器码", + }); + } finally { + setIsRestoring(false); + } + }; + + const handleClearOverride = async () => { + try { + const result = await machineIdApi.clearMachineIdOverride(); + + if (result.success) { + toast({ + title: "清除成功", + description: result.message, + }); + onRefresh(); + } else { + toast({ + variant: "destructive", + title: "清除失败", + description: result.message, + }); + } + } catch (error) { + console.error("清除失败:", error); + toast({ + variant: "destructive", + title: "清除失败", + description: "清除覆盖失败", + }); + } + }; + + if (!machineIdInfo || !adminStatus) { + return ( + + + + + 无法加载管理功能 + + + +

+ 无法加载机器码管理功能,请检查系统状态。 +

+
+
+ ); + } + + return ( +
+ {/* 权限状态提醒 */} + {!canModify && ( + + + + + 权限不足 + + + +

+ {!machineIdInfo.can_modify + ? "当前平台不支持机器码修改" + : "需要管理员权限才能修改机器码"} +

+
+
+ )} + + {/* 机器码修改 */} + + + + + 修改机器码 + + + 输入新的机器码来替换当前的系统标识符 + + + +
+ +
+ { + setNewMachineId(e.target.value); + setValidationResult(null); + }} + onBlur={handleValidateMachineId} + disabled={!canModify} + className="font-mono" + /> + + +
+
+ + {validationResult && ( +
+
+ {validationResult.is_valid ? ( + + ) : ( + + )} + + {validationResult.is_valid ? "格式验证通过" : "格式验证失败"} + +
+ + {validationResult.is_valid && validationResult.formatted_id && ( +
+

格式化后:

+

+ {validationResult.formatted_id} +

+
+ )} + + {!validationResult.is_valid && validationResult.error_message && ( +

+ {validationResult.error_message} +

+ )} + +
+ + 检测格式: + {machineIdUtils.getFormatDisplayName( + validationResult.detected_format, + )} + +
+
+ )} + + +
+
+ + {/* 格式转换工具 */} + + + + + 格式转换 + + + 在UUID格式和32位十六进制格式之间转换 + + + +
+ + setFormatInput(e.target.value)} + className="font-mono" + /> +
+ +
+ + + setFormatTarget(value as "uuid" | "hex32") + } + > +
+ + +
+
+ + +
+
+
+ + + + {formatResult && ( +
+

转换结果:

+

+ {formatResult} +

+
+ )} +
+
+ + {/* 备份和恢复 */} + + + + + 备份与恢复 + + 备份当前机器码或从备份文件恢复 + + + {/* 备份 */} +
+

备份机器码

+
+ setBackupPath(e.target.value)} + disabled={!canModify} + className="flex-1" + /> + + +
+
+ + + + {/* 恢复 */} +
+

恢复机器码

+ + {/* 恢复到原始机器码 */} + {machineIdInfo?.original_id && ( +
+
+
+
+ 恢复到原始机器码 +
+

+ 将机器码恢复到首次备份的原始值 +

+

+ {machineIdInfo.original_id} +

+
+ +
+
+ )} + + {/* 从文件恢复 */} +
+
+ 从备份文件恢复 +
+
+ setRestorePath(e.target.value)} + disabled={!canModify} + className="flex-1" + /> + + +
+
+
+
+
+ + {/* macOS 特殊操作 */} + {machineIdInfo.platform.toLowerCase() === "macos" && + machineIdInfo.original_id && ( + + + + + macOS 特殊操作 + + + 清除机器码覆盖,恢复原始系统机器码 + + + +
+
+ +

+ 注意:此操作将删除当前的机器码覆盖 +

+
+

+ 这将使系统恢复到原始机器码:{machineIdInfo.original_id} +

+ +
+
+
+ )} +
+ ); +} diff --git a/src/components/tools/machine-id/MachineIdSystemPanel.tsx b/src/components/tools/machine-id/MachineIdSystemPanel.tsx new file mode 100644 index 000000000..eb6b7b6dc --- /dev/null +++ b/src/components/tools/machine-id/MachineIdSystemPanel.tsx @@ -0,0 +1,346 @@ +import { + Cpu, + Shield, + Server, + AlertTriangle, + CheckCircle, + Info, +} from "lucide-react"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Separator } from "@/components/ui/separator"; +import { SystemInfo, AdminStatus } from "@/lib/api/machineId"; +import { machineIdUtils } from "@/lib/api/machineId"; + +interface MachineIdSystemPanelProps { + systemInfo: SystemInfo | null; + adminStatus: AdminStatus | null; + onRefresh: () => void; +} + +export function MachineIdSystemPanel({ + systemInfo, + adminStatus, +}: MachineIdSystemPanelProps) { + if (!systemInfo || !adminStatus) { + return ( + + + + + 无法获取系统信息 + + + +

+ 无法加载系统信息,请检查系统状态。 +

+
+
+ ); + } + + const getPlatformIcon = (os: string) => { + switch (os.toLowerCase()) { + case "windows": + return "🪟"; + case "macos": + return "🍎"; + case "linux": + return "🐧"; + default: + return "💻"; + } + }; + + const getArchIcon = (arch: string) => { + switch (arch.toLowerCase()) { + case "x86_64": + case "amd64": + return "🖥️"; + case "aarch64": + case "arm64": + return "📱"; + case "x86": + case "i386": + return "🖧"; + default: + return "🔧"; + } + }; + + const getSupportBadge = (canRead: boolean, canWrite: boolean) => { + if (canRead && canWrite) { + return ( + + + 完全支持 + + ); + } else if (canRead) { + return ( + + + 只读支持 + + ); + } else { + return ( + + + 不支持 + + ); + } + }; + + return ( +
+ {/* 系统基本信息 */} + + + + + 系统信息 + + 当前系统的基本信息和配置 + + +
+
+
+ + {getPlatformIcon(systemInfo.os)} + +
+

操作系统

+

+ {machineIdUtils.getPlatformDisplayName(systemInfo.os)} +

+

+ 系列: {systemInfo.family} +

+
+
+ +
+ {getArchIcon(systemInfo.arch)} +
+

系统架构

+

{systemInfo.arch}

+
+
+
+ +
+
+ +
+

权限状态

+
+ {adminStatus.is_admin ? ( + + + 管理员权限 + + ) : ( + + + 普通用户 + + )} +
+ {adminStatus.elevation_method && ( +

+ 提升方法:{adminStatus.elevation_method} +

+ )} +
+
+ +
+ +
+

机器码支持

+ {getSupportBadge( + systemInfo.machine_id_support.can_read, + systemInfo.machine_id_support.can_write, + )} + {systemInfo.requires_admin && ( +

+ ⚠️ 修改需要管理员权限 +

+ )} +
+
+
+
+
+
+ + {/* 平台支持详情 */} + + + + + 平台支持详情 + + 当前平台对机器码操作的支持情况 + + +
+
+
+

支持格式

+

+ {systemInfo.machine_id_support.format} +

+
+ +
+

实现方法

+

+ {systemInfo.machine_id_support.method} +

+
+
+ +
+
+

操作权限

+
+
+ {systemInfo.machine_id_support.can_read ? ( + + ) : ( + + )} + + 读取机器码{" "} + {systemInfo.machine_id_support.can_read ? "✓" : "✗"} + +
+
+ {systemInfo.machine_id_support.can_write ? ( + + ) : ( + + )} + + 修改机器码{" "} + {systemInfo.machine_id_support.can_write ? "✓" : "✗"} + +
+
+
+
+
+ + {systemInfo.machine_id_support.limitations.length > 0 && ( + <> + +
+
+ +

平台限制

+
+
+ {systemInfo.machine_id_support.limitations.map( + (limitation, index) => ( +
+ + {limitation} +
+ ), + )} +
+
+ + )} +
+
+ + {/* 平台特定信息 */} + + + + + 平台特定说明 + + + + {systemInfo.os.toLowerCase() === "windows" && ( +
+

Windows 平台说明

+
    +
  • + 机器码存储在注册表中: + + HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\MachineGuid + +
  • +
  • 修改机器码需要管理员权限
  • +
  • 某些应用程序可能需要重启才能识别新的机器码
  • +
  • 支持标准 UUID 格式
  • +
+
+ )} + + {systemInfo.os.toLowerCase() === "macos" && ( +
+

macOS 平台说明

+
    +
  • 使用应用层覆盖机制,不修改系统原始 UUID
  • +
  • + 原始机器码通过{" "} + ioreg 命令获取 +
  • +
  • 覆盖文件存储在用户数据目录
  • +
  • 不需要管理员权限,但只影响使用覆盖的应用
  • +
  • 支持清除覆盖恢复原始状态
  • +
+
+ )} + + {systemInfo.os.toLowerCase() === "linux" && ( +
+

Linux 平台说明

+
    +
  • + 机器码存储在{" "} + /etc/machine-id{" "} + 文件中 +
  • +
  • 修改需要 root 权限
  • +
  • 使用 32 位十六进制格式
  • +
  • 某些系统服务可能需要重启
  • +
  • 修改可能影响系统服务的正常运行
  • +
+
+ )} + + {!["windows", "macos", "linux"].includes( + systemInfo.os.toLowerCase(), + ) && ( +
+ +

不支持的平台

+

+ 当前平台({systemInfo.os})暂不支持机器码管理功能 +

+
+ )} +
+
+
+ ); +} diff --git a/src/components/tools/machine-id/MachineIdTool.tsx b/src/components/tools/machine-id/MachineIdTool.tsx new file mode 100644 index 000000000..1998568f8 --- /dev/null +++ b/src/components/tools/machine-id/MachineIdTool.tsx @@ -0,0 +1,154 @@ +import { useState, useEffect } from "react"; +import { ArrowLeft, Cpu, Settings, History, Info } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { MachineIdInfoPanel } from "./MachineIdInfoPanel"; +import { MachineIdManagePanel } from "./MachineIdManagePanel"; +import { MachineIdHistoryPanel } from "./MachineIdHistoryPanel"; +import { MachineIdSystemPanel } from "./MachineIdSystemPanel"; +import { MachineIdInfo, SystemInfo, AdminStatus } from "@/lib/api/machineId"; +import { machineIdApi } from "@/lib/api/machineId"; + +interface MachineIdToolProps { + onNavigate: ( + page: + | "provider-pool" + | "config-management" + | "api-server" + | "flow-monitor" + | "tools" + | "browser-interceptor" + | "machine-id" + | "settings", + ) => void; +} + +export function MachineIdTool({ onNavigate }: MachineIdToolProps) { + const [machineIdInfo, setMachineIdInfo] = useState( + null, + ); + const [systemInfo, setSystemInfo] = useState(null); + const [adminStatus, setAdminStatus] = useState(null); + const [loading, setLoading] = useState(true); + const [refreshKey, setRefreshKey] = useState(0); + + useEffect(() => { + loadAllInfo(); + }, [refreshKey]); + + const loadAllInfo = async () => { + try { + setLoading(true); + const [machineId, system, admin] = await Promise.all([ + machineIdApi.getCurrentMachineId(), + machineIdApi.getSystemInfo(), + machineIdApi.checkAdminPrivileges(), + ]); + + setMachineIdInfo(machineId); + setSystemInfo(system); + setAdminStatus(admin); + } catch (error) { + console.error("加载机器码信息失败:", error); + } finally { + setLoading(false); + } + }; + + const handleRefresh = () => { + setRefreshKey((prev) => prev + 1); + }; + + if (loading) { + return ( +
+
+ +

加载中...

+
+
+ ); + } + + return ( +
+ {/* 页面头部 */} +
+
+ +
+ +
+

机器码管理工具

+

+ 查看、修改和管理系统机器码,支持跨平台操作 +

+
+
+
+ + {/* 刷新按钮 */} + +
+ + {/* 主要功能标签页 */} + + + + + 机器码信息 + + + + 管理操作 + + + + 操作历史 + + + + 系统信息 + + + + + + + + + + + + + + + + + + + +
+ ); +} diff --git a/src/components/ui/radio-group.tsx b/src/components/ui/radio-group.tsx new file mode 100644 index 000000000..5ec3a2bfa --- /dev/null +++ b/src/components/ui/radio-group.tsx @@ -0,0 +1,64 @@ +import * as React from "react"; +import { cn } from "@/lib/utils"; + +interface RadioGroupProps { + value?: string; + onValueChange?: (value: string) => void; + className?: string; + children?: React.ReactNode; +} + +interface RadioGroupItemProps { + value: string; + id?: string; + className?: string; + children?: React.ReactNode; +} + +const RadioGroupContext = React.createContext<{ + value?: string; + onValueChange?: (value: string) => void; +}>({}); + +const RadioGroup = React.forwardRef( + ({ className, value, onValueChange, children, ...props }, ref) => { + return ( + +
+ {children} +
+
+ ); + }, +); +RadioGroup.displayName = "RadioGroup"; + +const RadioGroupItem = React.forwardRef( + ({ className, value, id, ...props }, ref) => { + const context = React.useContext(RadioGroupContext); + + return ( + context.onValueChange?.(value)} + className={cn( + "aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50", + className, + )} + {...props} + /> + ); + }, +); +RadioGroupItem.displayName = "RadioGroupItem"; + +export { RadioGroup, RadioGroupItem }; diff --git a/src/components/ui/separator.tsx b/src/components/ui/separator.tsx new file mode 100644 index 000000000..3898edad6 --- /dev/null +++ b/src/components/ui/separator.tsx @@ -0,0 +1,31 @@ +import * as React from "react"; +import { cn } from "@/lib/utils"; + +const Separator = React.forwardRef< + React.ElementRef<"div">, + React.ComponentPropsWithoutRef<"div"> & { + orientation?: "horizontal" | "vertical"; + decorative?: boolean; + } +>( + ( + { className, orientation = "horizontal", decorative = true, ...props }, + ref, + ) => ( +
+ ), +); + +Separator.displayName = "Separator"; + +export { Separator }; diff --git a/src/hooks/use-toast.ts b/src/hooks/use-toast.ts new file mode 100644 index 000000000..5ab9e8d6a --- /dev/null +++ b/src/hooks/use-toast.ts @@ -0,0 +1,31 @@ +import { toast as sonnerToast } from "sonner"; + +export interface ToastProps { + title: string; + description?: string; + variant?: "default" | "destructive"; + duration?: number; +} + +export function useToast() { + const toast = ({ + title, + description, + variant = "default", + duration, + }: ToastProps) => { + if (variant === "destructive") { + sonnerToast.error(title, { + description, + duration, + }); + } else { + sonnerToast.success(title, { + description, + duration, + }); + } + }; + + return { toast }; +} diff --git a/src/lib/api/machineId.ts b/src/lib/api/machineId.ts new file mode 100644 index 000000000..71ed99c09 --- /dev/null +++ b/src/lib/api/machineId.ts @@ -0,0 +1,255 @@ +import { invoke } from "@tauri-apps/api/core"; + +// Machine ID types matching Rust backend + +export type MachineIdFormat = "uuid" | "hex32" | "unknown"; + +export interface MachineIdInfo { + current_id: string; + original_id?: string; + platform: string; + can_modify: boolean; + requires_admin: boolean; + backup_exists: boolean; + format_type: MachineIdFormat; +} + +export interface MachineIdResult { + success: boolean; + message: string; + requires_restart: boolean; + requires_admin: boolean; + new_machine_id?: string; +} + +export interface AdminStatus { + is_admin: boolean; + platform: string; + elevation_method?: string; + check_success: boolean; +} + +export interface MachineIdValidation { + is_valid: boolean; + error_message?: string; + formatted_id?: string; + detected_format: MachineIdFormat; +} + +export interface MachineIdHistory { + id: string; + machine_id: string; + timestamp: string; + platform: string; + backup_path?: string; +} + +export interface SystemInfo { + os: string; + arch: string; + family: string; + machine_id_support: PlatformSupport; + requires_admin: boolean; +} + +export interface PlatformSupport { + can_read: boolean; + can_write: boolean; + format: string; + method: string; + limitations: string[]; +} + +export const machineIdApi = { + /** + * 获取当前机器码信息 + */ + async getCurrentMachineId(): Promise { + return invoke("get_current_machine_id"); + }, + + /** + * 设置新的机器码 + */ + async setMachineId(newId: string): Promise { + return invoke("set_machine_id", { newId }); + }, + + /** + * 生成随机机器码 + */ + async generateRandomMachineId(): Promise { + return invoke("generate_random_machine_id"); + }, + + /** + * 验证机器码格式 + */ + async validateMachineId(machineId: string): Promise { + return invoke("validate_machine_id", { machineId }); + }, + + /** + * 检查管理员权限 + */ + async checkAdminPrivileges(): Promise { + return invoke("check_admin_privileges"); + }, + + /** + * 获取操作系统类型 + */ + async getOsType(): Promise { + return invoke("get_os_type"); + }, + + /** + * 备份机器码到文件 + */ + async backupMachineIdToFile(filePath: string): Promise { + return invoke("backup_machine_id_to_file", { filePath }); + }, + + /** + * 从文件恢复机器码 + */ + async restoreMachineIdFromFile(filePath: string): Promise { + return invoke("restore_machine_id_from_file", { filePath }); + }, + + /** + * 格式化机器码 + * @param machineId 机器码 + * @param formatType 格式类型:"uuid" 或 "hex32" + */ + async formatMachineId( + machineId: string, + formatType: "uuid" | "hex32", + ): Promise { + return invoke("format_machine_id", { machineId, formatType }); + }, + + /** + * 检测机器码格式 + */ + async detectMachineIdFormat(machineId: string): Promise { + return invoke("detect_machine_id_format", { machineId }); + }, + + /** + * 转换机器码格式 + * @param machineId 机器码 + * @param targetFormat 目标格式:"uuid" 或 "hex32" + */ + async convertMachineIdFormat( + machineId: string, + targetFormat: "uuid" | "hex32", + ): Promise { + return invoke("convert_machine_id_format", { machineId, targetFormat }); + }, + + /** + * 获取机器码历史记录 + */ + async getMachineIdHistory(): Promise { + return invoke("get_machine_id_history"); + }, + + /** + * 清除机器码覆盖(仅限 macOS) + */ + async clearMachineIdOverride(): Promise { + return invoke("clear_machine_id_override"); + }, + + /** + * 复制机器码到剪贴板 + */ + async copyMachineIdToClipboard(machineId: string): Promise { + return invoke("copy_machine_id_to_clipboard", { machineId }); + }, + + /** + * 从剪贴板粘贴机器码 + */ + async pasteMachineIdFromClipboard(): Promise { + return invoke("paste_machine_id_from_clipboard"); + }, + + /** + * 获取系统信息 + */ + async getSystemInfo(): Promise { + return invoke("get_system_info"); + }, +}; + +// Helper functions for common operations +export const machineIdUtils = { + /** + * 检查机器码是否为有效的UUID格式 + */ + isValidUuid(machineId: string): boolean { + const uuidRegex = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + return uuidRegex.test(machineId); + }, + + /** + * 检查机器码是否为有效的32位十六进制格式 + */ + isValidHex32(machineId: string): boolean { + const hex32Regex = /^[0-9a-f]{32}$/i; + return hex32Regex.test(machineId); + }, + + /** + * 清理机器码字符串(移除连字符和空格) + */ + cleanMachineId(machineId: string): string { + return machineId.replace(/[-\s]/g, "").toLowerCase(); + }, + + /** + * 格式化机器码为UUID格式 + */ + formatAsUuid(machineId: string): string { + const cleaned = this.cleanMachineId(machineId); + if (cleaned.length !== 32) { + throw new Error("Invalid machine ID length"); + } + return `${cleaned.slice(0, 8)}-${cleaned.slice(8, 12)}-${cleaned.slice(12, 16)}-${cleaned.slice(16, 20)}-${cleaned.slice(20)}`; + }, + + /** + * 获取机器码的显示名称 + */ + getFormatDisplayName(format: MachineIdFormat): string { + switch (format) { + case "uuid": + return "UUID格式"; + case "hex32": + return "32位十六进制"; + case "unknown": + return "未知格式"; + default: + return "未知格式"; + } + }, + + /** + * 获取平台的显示名称 + */ + getPlatformDisplayName(platform: string): string { + switch (platform.toLowerCase()) { + case "windows": + return "Windows"; + case "macos": + return "macOS"; + case "linux": + return "Linux"; + default: + return platform; + } + }, +};