# Agent Client 实现计划文档 > 版本:v2.1.0 > 创建日期:2026-01-29 > 基于设计文档:agent-client-spec.md > 状态:深度实现计划 --- ## 开发阶段说明 ### 阶段一:核心通信与 UI(当前优先级) **目标**:验证 `agent-server-admin` 与 `agent-client` 之间的双向通信能力,确保 `data-server` 的 P2P/TCP/WebSocket 通信正常工作。 **包含 Phase**: - Phase 1:基础框架(UI、脚手架、托盘、自启动、协议版本、安全存储) - Phase 2:核心功能(连接管理、业务通道、设置界面、客户端信息、安全机制) - Phase 3:依赖管理(Node.js、npm 工具) - Phase 3.5:agent-server-admin 最小实现(用于通信验证) - Phase 3.6:跨平台打包(cargo-packager、CI/CD) **验证里程碑**: - [ ] 客户端可在 macOS/Windows/Linux 打包安装 - [ ] 客户端能连接 data-server(获取 ID) - [ ] 管理端能发现并连接客户端 - [ ] 双向通信正常 - [ ] 支持 P2P/Relay 模式 ### 阶段二:Agent 运行时集成(后续开发) **目标**:集成 agent_runner,实现 Agent 任务执行等功能。 **包含 Phase**: - Phase 4:Agent 运行时(AgentManager、任务执行、进度回传) - Phase 5:增强功能(权限管理、日志、文件传输、远程桌面) - Phase 6:收尾完善(关于、聊天、升级、国际化) --- ## 文档说明 本文档是 agent-client 技术设计的深度实现计划,专注于回答"如何实现"的问题,包含: - 完整的代码片段和实现模式 - 详细的架构设计和技术选型 - 跨平台适配的具体方案 - 错误处理和边界条件 --- ## 目录 1. [项目结构](#1-项目结构) 2. [Phase 1:基础框架](#phase-1基础框架p0) 3. [Phase 2:核心功能](#phase-2核心功能p0) 4. [Phase 3:依赖管理](#phase-3依赖管理p0) 5. [Phase 4:Agent 运行时](#phase-4agent-运行时p1) *后续开发* 6. [Phase 5:增强功能](#phase-5增强功能p1) *后续开发* 7. [Phase 6:收尾完善](#phase-6收尾完善p2) *后续开发* 8. [安全与网络实现](#8安全与网络实现) 9. [国际化与会话管理](#9国际化与会话管理) 10. [系统集成](#10系统集成) 11. [验收标准](#验收标准) --- ## 1. 项目结构 ### 1.1 完整目录结构 ``` qiming-agent/ # 项目根目录 ├── Cargo.toml # Workspace 配置 ├── Cargo.lock │ ├── crates/ # Crates 目录 │ ├── agent-client/ # 主应用 crate │ │ ├── Cargo.toml │ │ ├── src/ │ │ │ ├── main.rs # 程序入口 (约 80 行) │ │ │ ├── lib.rs # 库入口,导出公共 API │ │ │ ├── app.rs # 应用状态管理 (约 150 行) │ │ │ │ │ │ │ ├── components/ # UI 组件模块 │ │ │ │ ├── mod.rs # 组件模块导出 │ │ │ │ ├── root.rs # 根组件,主布局 (约 200 行) │ │ │ │ ├── status_bar.rs # 状态栏组件 (约 150 行) │ │ │ │ ├── client_info.rs # 客户端信息 Tab (ID/密码) │ │ │ │ ├── settings.rs # 设置界面 │ │ │ │ │ ├── mod.rs # 设置模块入口 │ │ │ │ │ ├── server.rs # 服务器配置子页面 │ │ │ │ │ ├── security.rs # 安全设置子页面 │ │ │ │ │ ├── general.rs # 常规设置子页面 │ │ │ │ │ ├── appearance.rs # 外观设置子页面 │ │ │ │ │ └── logging.rs # 日志设置子页面 │ │ │ │ ├── dependency_manager.rs # 依赖管理 Tab │ │ │ │ ├── permissions.rs # 权限设置 Tab │ │ │ │ ├── about.rs # 关于界面 │ │ │ │ ├── remote_desktop.rs # 远程桌面 Tab │ │ │ │ └── chat.rs # 聊天界面 │ │ │ │ │ │ │ ├── core/ # 核心逻辑模块 │ │ │ │ ├── mod.rs # 核心模块导出 │ │ │ │ ├── config.rs # 配置管理 (约 200 行) │ │ │ │ ├── connection.rs # 连接管理 (约 300 行) │ │ │ │ ├── agent.rs # Agent 管理 (约 250 行) │ │ │ │ ├── dependency.rs # 依赖管理核心 (约 300 行) │ │ │ │ ├── file_transfer.rs # 文件传输 │ │ │ │ ├── business_channel.rs # 业务通道 │ │ │ │ ├── logger.rs # 日志管理 │ │ │ │ │ │ │ │ │ └── platform/ # 平台适配层 │ │ │ │ ├── mod.rs # 平台模块导出 │ │ │ │ ├── permissions.rs # 权限检测 │ │ │ │ ├── auto_launch.rs # 自启动管理 │ │ │ │ ├── directories.rs # 跨平台目录 │ │ │ │ ├── windows.rs # Windows 特定实现 │ │ │ │ ├── macos.rs # macOS 特定实现 │ │ │ │ └── linux.rs # Linux 特定实现 │ │ │ │ │ │ │ ├── message/ # 消息协议模块 │ │ │ │ ├── mod.rs # 消息模块导出 │ │ │ │ ├── proto.rs # Protobuf 定义 │ │ │ │ ├── converter.rs # 消息转换器 │ │ │ │ └── codec.rs # 消息编解码 │ │ │ │ │ │ │ ├── tray/ # 系统托盘模块 │ │ │ │ ├── mod.rs # 托盘模块导出 │ │ │ │ ├── menu.rs # 托盘菜单 │ │ │ │ └── icon.rs # 图标加载 │ │ │ │ │ │ │ └── utils/ # 工具函数 │ │ │ ├── mod.rs │ │ │ ├── clipboard.rs # 剪贴板操作 │ │ │ ├── version.rs # 版本处理 │ │ │ ├── path.rs # 路径处理 │ │ │ └── notification.rs # 通知工具 │ │ │ │ │ └── assets/ # 资源文件 │ │ ├── icons/ # 图标资源 │ │ │ ├── app.icns # macOS 图标 │ │ │ ├── app.ico # Windows 图标 │ │ │ ├── app.png # Linux 图标 │ │ │ ├── tray_macos.png # macOS 托盘图标 │ │ │ ├── tray_windows.ico # Windows 托盘图标 │ │ │ └── tray_linux.png # Linux 托盘图标 │ │ │ │ │ └── locales/ # 国际化资源 │ │ ├── en.json # 英文翻译 │ │ └── zh.json # 中文翻译 │ │ │ ├── agent-server-admin/ # 服务器管理端 │ │ ├── Cargo.toml │ │ ├── src/ │ │ │ ├── main.rs │ │ │ ├── api/ # HTTP API │ │ │ │ ├── mod.rs │ │ │ │ ├── client.rs # 客户端管理 API │ │ │ │ └── task.rs # 任务管理 API │ │ │ ├── sse/ # SSE 推送 │ │ │ │ ├── mod.rs │ │ │ │ └── handler.rs # SSE 处理器 │ │ │ ├── client_manager/ # 客户端管理 │ │ │ │ ├── mod.rs │ │ │ │ └── manager.rs # 客户端管理器 │ │ │ ├── ws/ # WebSocket │ │ │ │ ├── mod.rs │ │ │ │ └── handler.rs # WebSocket 处理器 │ │ │ └── config.rs # 服务配置 │ │ └── tests/ │ │ └── integration_tests.rs │ │ │ ├── data-server/ # 数据中转服务器 │ │ ├── Cargo.toml │ │ ├── src/ │ │ │ ├── main.rs # 基于 rustdesk-server 封装 │ │ │ ├── relay.rs # 中继服务 │ │ │ ├── rendezvous.rs # 信令服务 │ │ │ └── router.rs # 消息路由 │ │ └── tests/ │ │ └── relay_tests.rs │ │ │ └── agent-protocol/ # 共享协议 crate │ ├── Cargo.toml │ ├── src/ │ │ ├── lib.rs # 库入口 │ │ ├── proto/ # Protobuf 定义 │ │ │ ├── mod.rs # 模块导出 │ │ │ ├── message.proto # 消息协议定义 │ │ │ ├── common.proto # 公共消息定义 │ │ │ └── business.proto # 业务消息定义 │ │ └── generated/ # 生成的代码 │ │ └── mod.rs # 生成代码导出 │ ├── build.rs # Proto 编译脚本 │ └── tests/ │ └── proto_tests.rs │ ├── vendors/ # 第三方库(路径依赖) │ ├── qiming-rustdesk/ # RustDesk fork │ ├── rustdesk-server/ # RustDesk Server fork │ ├── gpui-component/ # UI 组件库 │ ├── rcoder/ # AI Agent 运行时 │ ├── auto-launch/ # 开机自启动库 │ ├── tray-icon/ # 系统托盘库 │ ├── muda/ # 菜单库 │ ├── enigo/ # 输入模拟库 │ └── cargo-packager/ # 打包工具 │ └── specs/ # 规格文档 ├── agent-client.md # 需求文档 ├── agent-client-spec.md # 技术规格 ├── agent-client-plan.md # 实现计划 ├── agent-client-task.md # 任务分解 └── vendorsdoc/ # 第三方库文档 ``` ### 1.2 Workspace Cargo.toml 完整配置 ```toml # Cargo.toml [workspace] members = [ "crates/agent-client", "crates/agent-server-admin", "crates/data-server", "crates/agent-protocol", ] resolver = "2" # 排除不需要构建的目录 exclude = [ "vendors", "specs", "assets", ] # 统一版本管理 [workspace.package] version = "1.0.0" edition = "2021" rust-version = "1.75" authors = ["qiming "] # 依赖版本锁定 [workspace.dependencies] # Async 运行时 tokio = { version = "1.38", features = ["full"] } async-trait = "0.1" # 序列化 serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" toml = "0.8" # 错误处理 thiserror = "2.0" anyhow = "1.0" # 日志 tracing = "0.1" tracing-subscriber = "0.3" tracing-appender = "0.2" # 工具库 dirs = "5.0" url = "2.5" uuid = { version = "1.10", features = ["v4"] } chrono = { version = "0.4", features = ["serde"] } regex = "1.10" # 并发 dashmap = "6.1" tokio-stream = "0.1" futures = "0.3" # 网络 reqwest = { version = "0.12", features = ["json"] } tokio-tar = "0.3" [patch.crates-io] # 本地路径补丁 gpui = { git = "https://github.com/zed-industries/gpui", branch = "main" } ``` ### 1.3 agent-client/Cargo.toml 完整配置 ```toml # agent-client/Cargo.toml [package] name = "qiming-agent" version.workspace = true edition.workspace = true rust-version.workspace = true authors.workspace = true description = "跨平台 Agent 客户端,支持远程桌面和 Agent 任务执行" repository = "https://github.com/qiming/qiming-agent" license = "MIT" [lib] name = "qiming_agent" crate-type = ["cdylib", "rlib"] [[bin]] name = "qiming-agent" path = "src/main.rs" # Feature 控制表 [features] default = ["tray", "auto-launch", "dependency-management"] # 系统托盘 tray = ["dep:tray-icon", "dep:muda", "dep:image"] # 开机自启动 auto-launch = ["dep:auto-launch"] # 依赖管理界面 dependency-management = ["dep:reqwest", "dep:tokio-tar"] # 远程桌面功能 remote-desktop = ["dep:scrap", "dep:enigo"] # 聊天界面 chat-ui = [] # 文件传输 file-transfer = [] # 开发者模式日志 dev-mode = ["dep:tracing-subscriber/dev-filter", "dep:env_filter"] [dependencies] # Workspace 依赖 agent-protocol = { path = "../agent-protocol" } tokio = { workspace = true, features = ["full"] } async-trait = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } toml = { workspace = true } thiserror = { workspace = true } anyhow = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } tracing-appender = { workspace = true } dashmap = { workspace = true } tokio-stream = { workspace = true } futures = { workspace = true } dirs = { workspace = true } url = { workspace = true } uuid = { workspace = true, features = ["v4"] } chrono = { workspace = true } regex = { workspace = true } # UI 框架 gpui = { git = "https://github.com/zed-industries/gpui", branch = "main" } gpui-component = { path = "../../vendors/gpui-component" } # 系统托盘 (可选) tray-icon = { path = "../../vendors/tray-icon", optional = true } muda = { path = "../../vendors/muda", optional = true } image = { version = "0.24", optional = true } # 开机自启动 (可选) auto-launch = { path = "../../vendors/auto-launch", optional = true } # 通信库 - qiming-rustdesk librustdesk = { path = "../../vendors/qiming-rustdesk" } # Agent 运行时 - rcoder/agent_runner agent_runner = { path = "../../vendors/rcoder/crates/agent_runner" } # 远程桌面 scrap = { path = "../../vendors/qiming-rustdesk/libs/scrap", optional = true } enigo = { version = "0.1", optional = true } # 依赖管理 reqwest = { version = "0.12", optional = true } tokio-tar = { version = "0.3", optional = true } zip = { version = "0.6" } # 平台特定 [target.'cfg(target_os = "macos")'.dependencies] core-foundation = "0.9" security-framework = "2" [target.'cfg(target_os = "windows")'.dependencies] winreg = "0.52" [build-dependencies] prost-build = "0.12" tonic-build = "0.10" vergen = { version = "9", features = ["build", "git"] } [profile.release] opt-level = "z" lto = true codegen-units = 1 strip = true [profile.dev] debug = true split-debuginfo = "unpacked" [package.metadata] # cargo-binstall 支持 bins = [ { name = "qiming-agent", src = "src/main.rs" } ] ``` ### 1.4 agent-protocol/Cargo.toml 完整配置 ```toml # agent-protocol/Cargo.toml [package] name = "agent-protocol" version.workspace = true edition.workspace = true authors.workspace = true description = "Agent 客户端通信协议定义" repository.workspace = true license.workspace = true [dependencies] prost = "0.12" prost-types = "0.12" tokio = { version = "1.38", features = ["sync"] } bytes = "1.7" serde = { version = "1.0", features = ["derive", "rc"] } serde_json = "1.0" thiserror = "2.0" anyhow = "1.0" uuid = { version = "1.10", features = ["v4"] } chrono = { version = "0.4", features = ["serde"] } [dev-dependencies] prost-build = "0.12" tempfile = "4.0" [build-dependencies] prost-build = "0.12" [features] default = [] # 开发模式:包含测试数据生成 dev-mode = [] [package.metadata.protobuf] # Proto 文件路径 proto-path = "src/proto" # 输出目录 out-dir = "src/generated" ``` ### 1.5 build.rs 完整实现 ```rust // agent-protocol/build.rs use std::env; use std::path::PathBuf; fn main() { let proto_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()) .join("src/proto"); // 查找所有 proto 文件 let mut proto_files = Vec::new(); collect_proto_files(&proto_dir, &mut proto_files); if proto_files.is_empty() { eprintln!("No proto files found in {:?}", proto_dir); return; } println!("Found proto files: {:?}", proto_files); // 配置 prost-build let mut config = prost_build::Config::new(); // 启用 bytes 支持 config.bytes(["."]); // 启用 wasm 支持(如果需要) #[cfg(target_arch = "wasm32")] config.enable_wasm_protobuf(); // 编译 proto 文件 config .compile_protos(&proto_files, &[&proto_dir]) .expect("Failed to compile proto files"); // 生成版本信息 generate_version(); } fn collect_proto_files(dir: &PathBuf, files: &mut Vec) { if let Ok(entries) = std::fs::read_dir(dir) { for entry in entries.flatten() { let path = entry.path(); if path.is_file() && path.extension().map(|e| e == "proto").unwrap_or(false) { files.push(path); } else if path.is_dir() { collect_proto_files(&path, files); } } } } fn generate_version() { // 使用 vergen 生成版本信息 println!("cargo:rerun-if-changed=.git/HEAD"); println!("cargo:rerun-if-changed=.git/index"); // 手动生成版本信息(不使用 vergen) let git_sha = std::process::Command::new("git") .args(&["rev-parse", "HEAD"]) .output() .ok() .and_then(|o| String::from_utf8(o.stdout).ok()) .unwrap_or_else(|| "unknown".to_string()); let git_describe = std::process::Command::new("git") .args(&["describe", "--tags", "--always"]) .output() .ok() .and_then(|o| String::from_utf8(o.stdout).ok()) .unwrap_or_else(|| "unknown".to_string()); println!("cargo:rustc-env=GIT_SHA={}", git_sha.trim()); println!("cargo:rustc-env=GIT_DESCRIBE={}", git_describe.trim()); } ``` --- ## Phase 1:基础框架(P0) ### 1.6 项目脚手架 #### 1.6.1 main.rs 完整实现(约 80 行) ```rust // crates/agent-client/src/main.rs use std::sync::Arc; use std::time::Duration; use tokio::sync::{mpsc, RwLock}; use tokio::signal; use qiming_agent::{App, AppConfig}; use qiming_agent::core::logger::Logger; use qiming_agent::core::config::ConfigManager; const APP_NAME: &str = "qiming-agent"; const VERSION: &str = env!("CARGO_PKG_VERSION"); #[tokio::main(flavor = "multi_thread", worker_threads = 4)] async fn main() -> anyhow::Result<()> { // 1. 初始化日志系统 Logger::init(APP_NAME, VERSION)?; tracing::info!("Starting {} v{}", APP_NAME, VERSION); tracing::info!("Platform: {}", std::env::consts::OS); tracing::info!("Architecture: {}", std::env::consts::ARCH); // 2. 加载配置 let config_manager = Arc::new(ConfigManager::load().await?); tracing::info!("Configuration loaded"); // 3. 应用配置到日志 if let Some(log_config) = &config_manager.config.logging { Logger::configure_level(log_config.level); } // 4. 创建应用状态 let (shutdown_tx, shutdown_rx) = mpsc::channel(1); let app_state = Arc::new(RwLock::new(App::new(config_manager.clone()).await?)); // 5. 启动状态同步任务 let app_state_clone = app_state.clone(); tokio::spawn(async move { sync_app_state(app_state_clone, shutdown_rx).await; }); // 6. 处理信号 let shutdown_tx_clone = shutdown_tx.clone(); tokio::spawn(async move { handle_signals(shutdown_tx_clone).await; }); // 7. 运行应用主循环 let mut app = App::new(config_manager).await?; app.run().await?; // 8. 清理 shutdown_tx.send(()).await.ok(); tracing::info!("Application shutdown complete"); Ok(()) } /// 同步应用状态 async fn sync_app_state(app_state: Arc>, mut shutdown_rx: mpsc::Receiver<()>) { let mut interval = tokio::time::interval(Duration::from_secs(5)); loop { tokio::select! { _ = interval.tick() => { // 定期同步状态 let mut state = app_state.write().await; state.sync_connection_state().await; } _ = shutdown_rx.recv() => { tracing::debug!("State sync task received shutdown signal"); break; } } } } /// 处理系统信号 async fn handle_signals(mut shutdown_tx: mpsc::Sender<()>) { let ctrl_c = async { signal::ctrl_c().await.expect("Failed to install Ctrl+C handler"); }; #[cfg(unix)] let terminate = async { signal::unix::signal(signal::UnixSignalKind::terminate()) .expect("Failed to install terminate handler") .recv() .await; }; #[cfg(not(unix))] let terminate = std::future::pending::<()>(); tokio::select! { _ = ctrl_c => { tracing::info!("Received Ctrl+C, initiating shutdown..."); } _ = terminate => { tracing::info!("Received terminate signal, initiating shutdown..."); } } // 发送关闭信号 shutdown_tx.send(()).await.ok(); } ``` #### 1.6.2 lib.rs 完整实现(约 100 行) ```rust // crates/agent-client/src/lib.rs // 公开所有公共 API pub use app::App; pub use core::config::AppConfig; pub use core::connection::ConnectionManager; pub use core::agent::AgentManager; pub use core::dependency::DependencyManager; // Re-exports pub use gpui; pub use gpui_component; // 内部模块 mod app; mod components; mod core; mod message; mod tray; mod utils; // 库入口 #[cfg(test)] mod tests { use super::*; use rstest::rstest; #[tokio::test] async fn test_config_load() { // 测试配置加载 } #[rstest] #[case("test_value")] fn test_config_value(#[case] value: &str) { // 参数化测试 } } ``` ### 1.7 应用状态管理 #### 1.7.1 app.rs 完整实现(约 150 行) ```rust // crates/agent-client/src/app.rs use std::sync::Arc; use std::time::Duration; use tokio::sync::{mpsc, RwLock, broadcast}; use dashmap::DashMap; use crate::core::config::{AppConfig, ConfigManager}; use crate::core::connection::ConnectionManager; use crate::core::agent::AgentManager; use crate::core::dependency::DependencyManager; use crate::components::{Root, ActiveTab, ConnectionState, AgentState}; use crate::utils::Notification; /// 应用状态 pub struct App { /// 配置管理器 pub config: Arc, /// 连接管理器 pub connection: Arc>, /// Agent 管理器 pub agent: Arc>, /// 依赖管理器 pub dependency: Arc>, /// UI 状态 pub ui_state: AppUiState, /// 通知通道 pub notifications: broadcast::Sender, /// 活跃任务映射 pub active_tasks: DashMap, } pub struct AppUiState { /// 当前激活的 Tab pub active_tab: ActiveTab, /// 设置子页面 pub settings_subtab: SettingsSubTab, /// 状态栏状态 pub status_bar: StatusBarState, /// 模态框状态 pub modal: Option, } pub struct StatusBarState { pub connection: ConnectionState, pub agent: AgentState, pub dependency_ok: bool, pub current_time: String, } pub enum ModalState { PasswordChange(PasswordChangeModal), FileTransfer(FileTransferModal), Notification(String), } pub struct TaskInfo { pub task_id: String, pub session_id: String, pub started_at: chrono::DateTime, } impl App { /// 创建新应用实例 pub async fn new(config: Arc) -> Result { let notifications = broadcast::channel(100).0; Ok(Self { config: config.clone(), connection: Arc::new(RwLock::new(ConnectionManager::new())), agent: Arc::new(RwLock::new(AgentManager::new())), dependency: Arc::new(RwLock::new(DependencyManager::new(config))), ui_state: AppUiState { active_tab: ActiveTab::ClientInfo, settings_subtab: SettingsSubTab::Server, status_bar: StatusBarState { connection: ConnectionState::Disconnected, agent: AgentState::Idle, dependency_ok: false, current_time: String::new(), }, modal: None, }, notifications, active_tasks: DashMap::new(), }) } /// 运行应用主循环 pub async fn run(&mut self) -> Result<(), anyhow::Error> { tracing::info!("Application starting..."); // 初始化连接 self.initialize_connection().await?; // 创建 UI let window = self.create_window().await?; // 运行窗口事件循环 window.run().await; Ok(()) } /// 初始化连接 async fn initialize_connection(&mut self) -> Result<(), anyhow::Error> { let config = self.config.config.read().await; if let Some(server) = &config.server { let mut connection = self.connection.write().await; connection.connect(&server.hbbs_addr, &server.hbbr_addr).await?; } Ok(()) } /// 创建窗口 async fn create_window(&mut self) -> Result { let bounds = gpui::Bounds::centered( gpui::Size::new(1024.0, 768.0), None, ); let window = gpui::WindowBuilder::new() .bounds(bounds) .title("qiming-agent") .window_id("main".into()) .resizable(true) .min_size(gpui::Size::new(800.0, 600.0)) .app_id("qiming-agent") .decorations(true) .transparent(false) .build()?; // 初始化根组件 let root = Root::new(); window.set_root(root); Ok(window) } /// 切换 Tab pub fn switch_tab(&mut self, tab: ActiveTab) { self.ui_state.active_tab = tab; } /// 切换设置子页面 pub fn switch_settings_subtab(&mut self, subtab: SettingsSubTab) { self.ui_state.settings_subtab = subtab; } /// 显示通知 pub fn show_notification(&mut self, notification: Notification) { let _ = self.notifications.send(notification); } } // 辅助类型 pub enum SettingsSubTab { Server, Security, General, Appearance, Logging, } ``` ### 1.8 UI 组件实现 #### 1.8.1 Root 组件完整实现(约 200 行) ```rust // crates/agent-client/src/components/root.rs use gpui::{div, Component, Context, Element, IntoElement, Render, div}; use gpui::prelude::*; use gpui_component::{TabPanel, TabItem, Button, IconButton, IconName}; use crate::components::{ StatusBar, ConnectionState, AgentState, ClientInfo, Settings, DependencyManager, Permissions, About, RemoteDesktop, Chat, }; use crate::app::{App, ActiveTab, SettingsSubTab}; /// 根组件 - 主布局容器 pub struct Root { /// 当前激活的 Tab active_tab: ActiveTab, /// 当前激活的设置子页面 settings_subtab: SettingsSubTab, /// 连接状态 connection_state: ConnectionState, /// Agent 状态 agent_state: AgentState, /// 依赖状态 dependency_ok: bool, /// 是否显示模态框 show_modal: bool, } impl Root { /// 创建新根组件 pub fn new(cx: &mut Context) -> Self { Self { active_tab: ActiveTab::ClientInfo, settings_subtab: SettingsSubTab::Server, connection_state: ConnectionState::Disconnected, agent_state: AgentState::Idle, dependency_ok: true, show_modal: false, } } /// 设置连接状态 pub fn set_connection_state(&mut self, state: ConnectionState, cx: &mut Context) { self.connection_state = state; cx.notify(); } /// 设置 Agent 状态 pub fn set_agent_state(&mut self, state: AgentState, cx: &mut Context) { self.agent_state = state; cx.notify(); } /// 设置依赖状态 pub fn set_dependency_ok(&mut self, ok: bool, cx: &mut Context) { self.dependency_ok = ok; cx.notify(); } /// 切换 Tab pub fn switch_tab(&mut self, tab: ActiveTab, cx: &mut Context) { self.active_tab = tab; cx.notify(); } /// 切换设置子页面 pub fn switch_settings_subtab(&mut self, subtab: SettingsSubTab, cx: &mut Context) { self.settings_subtab = subtab; cx.notify(); } } impl Component for Root { fn render(&mut self, cx: &mut gpui::Context) -> impl IntoElement { div() .id("root") .size_full() .bg(cx.theme().background) .flex() .col() .children(self.render_header(cx)) .children(self.render_tabs(cx)) .children(self.render_content(cx)) .children(self.render_status_bar(cx)) } } impl Root { /// 渲染顶部标题栏 fn render_header(&mut self, cx: &mut Context) -> impl IntoElement { div() .id("header") .h(px(48.0)) .bg(cx.theme().surface) .border_b_1() .border_color(cx.theme().border) .px_4() .flex() .items_center() .justify_between() // 左侧:应用图标和标题 .child( div() .flex() .items_center() .gap_2() .child( div() .w(px(32.0)) .h(px(32.0)) .bg_blue_500() .rounded_md() ) .child( div() .text_lg() .font_bold() .text_color(cx.theme().text) .child("qiming-agent") ) ) // 右侧:设置按钮 .child( IconButton::new("settings-btn", IconName::Settings) .tooltip("设置") .on_click(|_, cx| { // TODO: 打开设置 }) ) } /// 渲染 Tab 面板 fn render_tabs(&mut self, cx: &mut Context) -> impl IntoElement { let tabs = vec![ TabItem::new("客户端信息", ActiveTab::ClientInfo), TabItem::new("依赖管理", ActiveTab::Dependencies), TabItem::new("权限设置", ActiveTab::Permissions), #[cfg(feature = "remote-desktop")] TabItem::new("远程桌面", ActiveTab::RemoteDesktop), #[cfg(feature = "chat-ui")] TabItem::new("Agent 聊天", ActiveTab::Chat), TabItem::new("关于", ActiveTab::About), ] .into_iter() .filter(|tab| { // 根据 feature 过滤 Tab #[cfg(not(feature = "chat-ui"))] if tab.id == ActiveTab::Chat { return false; } true }) .collect(); TabPanel::new() .id("main-tabs") .tabs(tabs) .active_tab(self.active_tab) .height(px(40.0)) .on_change(|new_tab, cx| { if let Some(tab) = new_tab.downcast_ref::() { // 处理 Tab 切换 } }) } /// 渲染主内容区域 fn render_content(&mut self, cx: &mut Context) -> impl IntoElement { div() .id("content") .flex_1() .overflow_hidden() .child(match self.active_tab { ActiveTab::ClientInfo => ClientInfo::render(cx), ActiveTab::Settings => Settings::render(self.settings_subtab, cx), ActiveTab::Dependencies => DependencyManager::render(cx), ActiveTab::Permissions => Permissions::render(cx), ActiveTab::About => About::render(cx), #[cfg(feature = "remote-desktop")] ActiveTab::RemoteDesktop => RemoteDesktop::render(cx), #[cfg(feature = "chat-ui")] ActiveTab::Chat => Chat::render(cx), }) } /// 渲染状态栏 fn render_status_bar(&mut self, cx: &mut Context) -> impl IntoElement { StatusBar::new() .height(px(32.0)) .connection_state(self.connection_state) .agent_state(self.agent_state) .dependency_ok(self.dependency_ok) } } ``` #### 1.8.2 StatusBar 组件完整实现(约 150 行) ```rust // crates/agent-client/src/components/status_bar.rs use gpui::{div, Component, Context, Element, IntoElement, Render}; use gpui::prelude::*; use gpui_component::{StatusBar, StatusItem, Icon, IconName, Badge}; use crate::components::ConnectionState; use crate::app::AgentState; /// 状态栏组件 pub struct StatusBarComponent { /// 连接状态 connection_state: ConnectionState, /// Agent 状态 agent_state: AgentState, /// 依赖是否正常 dependency_ok: bool, /// 当前时间 current_time: String, } impl StatusBarComponent { pub fn new() -> Self { Self { connection_state: ConnectionState::Disconnected, agent_state: AgentState::Idle, dependency_ok: true, current_time: String::new(), } } pub fn set_connection_state(&mut self, state: ConnectionState) { self.connection_state = state; } pub fn set_agent_state(&mut self, state: AgentState) { self.agent_state = state; } pub fn set_dependency_ok(&mut self, ok: bool) { self.dependency_ok = ok; } pub fn set_time(&mut self, time: String) { self.current_time = time; } } impl Component for StatusBarComponent { fn render(&mut self, cx: &mut gpui::Context) -> impl IntoElement { StatusBar::new() .height(px(32.0)) .items(vec![ // 左侧:连接状态 StatusItem::new() .content(self.render_connection_status(cx)) .min_width(px(200.0)), // 中左:Agent 状态 StatusItem::new() .content(self.render_agent_status(cx)) .flex_grow(1.0) .justify_center(), // 中右:依赖状态 StatusItem::new() .content(self.render_dependency_status(cx)) .min_width(px(120.0)), // 右侧:时间 StatusItem::new() .content(self.render_time(cx)) .min_width(px(100.0)) .justify_end(), ]) } } impl StatusBarComponent { /// 渲染连接状态 fn render_connection_status(&mut self, cx: &mut Context) -> impl IntoElement { let (icon, color, text, latency) = match self.connection_state { ConnectionState::Connected(mode, latency_ms) => { let (icon, color, mode_text) = match mode { ConnectionMode::P2P => ("●", "#22c55e", "P2P"), ConnectionMode::Relay => ("◑", "#eab308", "Relay"), }; (icon, color, format!("已连接 ({})", mode_text), Some(latency_ms)) } ConnectionState::Connecting => ("●", "#3b82f6", "连接中...", None), ConnectionState::Disconnected => ("●", "#ef4444", "未连接", None), ConnectionState::Error(ref msg) => ("⚠️", "#ef4444", format!("错误: {}", msg), None), }; div() .flex() .items_center() .gap_2() .child( div() .text_color(color) .child(icon) ) .child( div() .text_color(color) .child(text) ) .child(if let Some(latency) = latency { div() .text_sm() .text_gray() .child(format!("- {}ms", latency)) } else { div() }) } /// 渲染 Agent 状态 fn render_agent_status(&mut self, cx: &mut Context) -> impl IntoElement { let (icon, color, text) = match self.agent_state { AgentState::Idle => ("✓", "#6b7280", "Agent: 空闲"), AgentState::Active(count) => ("⟳", "#3b82f6", format!("Agent: {} 运行中", count)), AgentState::Executing(current, total) => { ("⟳", "#3b82f6", format!("Agent: 执行中 ({}/{})", current, total)) } AgentState::Error => ("✗", "#ef4444", "Agent: 错误"), }; div() .flex() .items_center() .gap_2() .child( div() .text_color(color) .child(icon) ) .child(text) } /// 渲染依赖状态 fn render_dependency_status(&mut self, cx: &mut Context) -> impl IntoElement { let (icon, color, text) = if self.dependency_ok { ("✓", "#22c55e", "依赖正常") } else { ("⚠️", "#ef4444", "依赖异常") }; div() .flex() .items_center() .gap_2() .child( div() .text_color(color) .child(icon) ) .child(text) } /// 渲染时间 fn render_time(&mut self, cx: &mut Context) -> impl IntoElement { div() .text_sm() .text_gray() .child(&self.current_time) } } /// 连接模式 pub enum ConnectionMode { P2P, Relay, } ``` ### 1.9 系统托盘 #### 1.9.1 TrayManager 完整实现(约 200 行) ```rust // crates/agent-client/src/tray/mod.rs use std::sync::Arc; use std::path::PathBuf; use std::fmt::Debug; use tokio::sync::{mpsc, RwLock, broadcast}; use tracing::error; use tray_icon::{TrayIconBuilder, icon::Icon, TrayIconEvent}; use muda::{Menu, MenuItem, ContextMenu, IconMenuItem, SeparatorMenuItem, AboutItem}; use crate::utils::Notification; use crate::components::ConnectionState; use crate::app::AgentState; /// 托盘图标路径配置 #[cfg(target_os = "macos")] const TRAY_ICON: &[u8] = include_bytes!("../assets/icons/tray_macos.png"); #[cfg(target_os = "windows")] const TRAY_ICON: &[u8] = include_bytes!("../assets/icons/tray_windows.ico"); #[cfg(target_os = "linux")] const TRAY_ICON: &[u8] = include_bytes!("../assets/icons/tray_linux.png"); /// 托盘管理器 pub struct TrayManager { /// 托盘图标实例 tray_icon: Option, /// 托盘菜单 menu: ContextMenu, /// 通知发送器 notification_tx: broadcast::Sender, /// 连接状态 connection_state: Arc>, /// Agent 状态 agent_state: Arc>, } impl TrayManager { /// 创建新的托盘管理器 pub fn new( notification_tx: broadcast::Sender, connection_state: Arc>, agent_state: Arc>, ) -> Self { let icon = Self::load_icon(); let menu = Self::create_menu(¬ification_tx); let tray_icon = TrayIconBuilder::new() .with_icon(icon) .with_tooltip("qiming-agent") .with_menu(Box::new(menu.clone())) .with_menu_on_left_click(true) .build() .ok(); Self { tray_icon, menu, notification_tx, connection_state, agent_state, } } /// 加载托盘图标 fn load_icon() -> Icon { Icon::from_rgba(TRAY_ICON.to_vec(), 32, 32) .unwrap_or_else(|e| { error!("Failed to load tray icon: {:?}", e); // 返回一个默认的空白图标 Icon::from_rgba(vec![0; 32 * 32 * 4], 32, 32).unwrap() }) } /// 创建托盘菜单 fn create_menu(notification_tx: &broadcast::Sender) -> ContextMenu { ContextMenu::new() // 显示窗口 .add_item(&MenuItem::with_id( "show", "显示窗口", true, None, ).unwrap()) .add_separator() // 状态信息(动态更新) .add_item(&IconMenuItem::with_id( "status", "状态: 已连接", true, None, None, ).unwrap()) .add_separator() // 功能菜单 .add_item(&MenuItem::with_id( "settings", "设置...", true, None, ).unwrap()) .add_item(&MenuItem::with_id( "dependencies", "依赖管理", true, None, ).unwrap()) .add_item(&MenuItem::with_id( "permissions", "权限设置", true, None, ).unwrap()) .add_separator() // 关于和退出 .add_item(&MenuItem::with_id( "about", "关于", true, None, ).unwrap()) .add_separator() .add_item(&MenuItem::with_id( "quit", "退出", true, None, ).unwrap()) } /// 处理托盘事件 pub fn handle_event(&self, event: TrayIconEvent) { match event { TrayIconEvent::Click { button, id: _ } => { match button { tray_icon::ClickButton::Left => { self.show_window(); } tray_icon::ClickButton::Right => { // 右键点击,显示菜单 } _ => {} } } TrayIconEvent::MenuEvent { id } => { self.handle_menu_event(id.as_str()); } _ => {} } } /// 处理菜单事件 fn handle_menu_event(&self, id: &str) { match id { "show" => { self.show_window(); } "settings" => { self.open_settings(); } "dependencies" => { self.open_dependencies(); } "permissions" => { self.open_permissions(); } "about" => { self.open_about(); } "quit" => { self.quit(); } _ => { error!("Unknown tray menu event: {}", id); } } } fn show_window(&self) { // TODO: 显示主窗口 let _ = self.notification_tx.send(Notification::info("显示窗口")); } fn open_settings(&self) { let _ = self.notification_tx.send(Notification::info("打开设置")); } fn open_dependencies(&self) { let _ = self.notification_tx.send(Notification::info("打开依赖管理")); } fn open_permissions(&self) { let _ = self.notification_tx.send(Notification::info("打开权限设置")); } fn open_about(&self) { let _ = self.notification_tx.send(Notification::info("打开关于")); } fn quit(&self) { let _ = self.notification_tx.send(Notification::info("退出应用")); // TODO: 发送退出信号 } /// 更新状态显示 pub async fn update_status(&self) { let connection = self.connection_state.read().await; let agent = self.agent_state.read().await; let status_text = match *connection { ConnectionState::Connected(_, latency) => { format!("已连接 (延迟: {}ms)", latency) } ConnectionState::Connecting => "连接中...".to_string(), ConnectionState::Disconnected => "未连接".to_string(), ConnectionState::Error(ref msg) => format!("错误: {}", msg), }; // 更新菜单项 // TODO: 使用 muda 的 API 更新动态菜单项 } } ``` ### 1.10 开机自启动 #### 1.10.1 AutoLaunchManager 完整实现(约 150 行) ```rust // crates/agent-client/src/core/platform/auto_launch.rs use std::path::PathBuf; use std::fmt; use auto_launch::{AutoLaunch, AutoLaunchBuilder, MacOSLaunchMode}; use thiserror::Error; #[derive(Error, Debug)] pub enum AutoLaunchError { #[error("Failed to get app path: {0}")] GetPathError(String), #[error("AutoLaunch not supported on this platform")] NotSupported, #[error("Failed to enable auto-launch: {0}")] EnableError(String), #[error("Failed to check auto-launch status: {0}")] CheckError(String), } pub struct AutoLaunchManager { auto_launch: Option, app_name: String, app_path: PathBuf, } impl AutoLaunchManager { /// 创建新的自启动管理器 pub fn new() -> Result { let app_name = "qiming-agent".to_string(); let app_path = Self::get_app_path()?; let auto_launch = Self::build_auto_launch(&app_name, &app_path)?; Ok(Self { auto_launch, app_name, app_path, }) } /// 获取应用路径(跨平台) fn get_app_path() -> Result { #[cfg(target_os = "macos")] { // macOS: /Applications/qiming-agent.app Ok(PathBuf::from("/Applications/qiming-agent.app")) } #[cfg(target_os = "windows")] { // Windows: 当前可执行文件路径 std::env::current_exe() .map_err(|e| AutoLaunchError::GetPathError(e.to_string())) } #[cfg(target_os = "linux")] { // Linux: 当前可执行文件路径 std::env::current_exe() .map_err(|e| AutoLaunchError::GetPathError(e.to_string())) } } /// 构建 AutoLaunch 实例 fn build_auto_launch(app_name: &str, app_path: &PathBuf) -> Result, AutoLaunchError> { #[cfg(target_os = "macos")] { let auto = AutoLaunchBuilder::new() .set_app_name(app_name) .set_app_path(app_path.to_str().ok_or_else(|| { AutoLaunchError::GetPathError("Invalid path".to_string()) })?) .set_macos_launch_mode(MacOSLaunchMode::SMAppService) .build() .map_err(|e| AutoLaunchError::EnableError(e.to_string()))?; Ok(Some(auto)) } #[cfg(target_os = "windows")] { let auto = AutoLaunchBuilder::new() .set_app_name(app_name) .set_app_path(app_path.to_str().ok_or_else(|| { AutoLaunchError::GetPathError("Invalid path".to_string()) })?) .build() .map_err(|e| AutoLaunchError::EnableError(e.to_string()))?; Ok(Some(auto)) } #[cfg(target_os = "linux")] { let auto = AutoLaunchBuilder::new() .set_app_name(app_name) .set_app_path(app_path.to_str().ok_or_else(|| { AutoLaunchError::GetPathError("Invalid path".to_string()) })?) .set_app_arguments(&["--hidden"]) .build() .map_err(|e| AutoLaunchError::EnableError(e.to_string()))?; Ok(Some(auto)) } #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))] { Err(AutoLaunchError::NotSupported) } } /// 检查是否已启用 pub async fn is_enabled(&self) -> Result { match &self.auto_launch { Some(auto) => { auto.is_enabled() .await .map_err(|e| AutoLaunchError::CheckError(e.to_string())) } None => Ok(false), } } /// 启用自启动 pub async fn enable(&self) -> Result<(), AutoLaunchError> { match &self.auto_launch { Some(auto) => { auto.enable() .await .map_err(|e| AutoLaunchError::EnableError(e.to_string()))?; Ok(()) } None => Err(AutoLaunchError::NotSupported), } } /// 禁用自启动 pub async fn disable(&self) -> Result<(), AutoLaunchError> { match &self.auto_launch { Some(auto) => { auto.disable() .await .map_err(|e| AutoLaunchError::EnableError(e.to_string()))?; Ok(()) } None => Err(AutoLaunchError::NotSupported), } } /// 切换自启动状态 pub async fn toggle(&self) -> Result { let is_enabled = self.is_enabled().await?; if is_enabled { self.disable().await?; Ok(false) } else { self.enable().await?; Ok(true) } } } ``` --- ## Phase 2:核心功能(P0) ### 2.1 qiming-rustdesk 集成 #### 2.1.1 ConnectionManager 完整实现(约 300 行) ```rust // crates/agent-client/src/core/connection.rs use std::sync::Arc; use std::time::{Duration, Instant}; use tokio::sync::{mpsc, RwLock, broadcast}; use dashmap::DashMap; use tracing::{info, warn, error, debug}; use librustdesk::{RendezvousMediator, ConnectionType}; use crate::utils::Notification; /// 连接管理器 pub struct ConnectionManager { /// RendezvousMediator 实例 mediator: Option>, /// 当前连接状态 state: RwLock, /// 连接事件广播 event_tx: broadcast::Sender, /// 心跳定时器 heartbeat_interval: Duration, /// 重连配置 reconnect_config: ReconnectConfig, /// 统计信息 stats: ConnectionStats, } #[derive(Clone)] pub struct ConnectionState { pub status: ConnectionStatus, pub mode: Option, pub latency_ms: u64, pub last_error: Option, pub connected_at: Option>, } #[derive(Clone, Debug, PartialEq)] pub enum ConnectionStatus { Disconnected, Connecting, Connected, Reconnecting, Error, } #[derive(Clone, Debug, PartialEq)] pub enum ConnectionMode { P2P, Relay, } pub struct ReconnectConfig { /// 最大重试次数 pub max_retries: u32, /// 重试间隔(秒) pub retry_interval: u64, /// 指数退避因子 pub backoff_factor: f64, /// 最大重试间隔(秒) pub max_backoff: u64, } pub struct ConnectionStats { /// 总连接次数 pub total_connections: u64, /// P2P 连接成功次数 pub p2p_successes: u64, /// Relay 连接成功次数 pub relay_successes: u64, /// 断开连接次数 pub disconnections: u64, /// 平均延迟(毫秒) pub avg_latency_ms: f64, /// 最后活动时间 pub last_activity: Option, } #[derive(Clone, Debug)] pub enum ConnectionEvent { Connected(ConnectionMode, u64), Disconnected, LatencyUpdated(u64), Error(String), StateChanged(ConnectionStatus), } impl Default for ConnectionState { fn default() -> Self { Self { status: ConnectionStatus::Disconnected, mode: None, latency_ms: 0, last_error: None, connected_at: None, } } } impl Default for ReconnectConfig { fn default() -> Self { Self { max_retries: 10, retry_interval: 5, backoff_factor: 2.0, max_backoff: 300, } } } impl ConnectionManager { /// 创建新的连接管理器 pub fn new() -> Self { let (event_tx, _) = broadcast::channel(100); Self { mediator: None, state: RwLock::new(ConnectionState::default()), event_tx, heartbeat_interval: Duration::from_secs(30), reconnect_config: ReconnectConfig::default(), stats: ConnectionStats { total_connections: 0, p2p_successes: 0, relay_successes: 0, disconnections: 0, avg_latency_ms: 0.0, last_activity: None, }, } } /// 连接到服务器 pub async fn connect( &mut self, hbbs_addr: &str, hbbr_addr: &str, ) -> Result<(), ConnectionError> { info!("Connecting to hbbs: {}, hbbr: {}", hbbs_addr, hbbr_addr); // 更新状态为连接中 self.set_status(ConnectionStatus::Connecting).await; self.state.write().await.last_error = None; // 创建 RendezvousMediator let mediator = Arc::new(RendezvousMediator::new()); // 配置服务器地址 mediator.set_id_server(hbbs_addr); mediator.set_relay_server(hbbr_addr); // 登录 let login_result = mediator.login().await .map_err(|e| ConnectionError::LoginFailed(e.to_string()))?; // 处理连接结果 let (mode, latency) = match login_result.conn_type { ConnectionType::P2P => { self.stats.p2p_successes += 1; (ConnectionMode::P2P, login_result.latency) } ConnectionType::Relay => { self.stats.relay_successes += 1; (ConnectionMode::Relay, login_result.latency) } _ => { return Err(ConnectionError::UnknownConnectionType); } }; // 更新状态 self.state.write().await.mode = Some(mode); self.state.write().await.latency_ms = latency; self.state.write().await.connected_at = Some(chrono::Utc::now()); self.set_status(ConnectionStatus::Connected).await; // 保存 mediator self.mediator = Some(mediator.clone()); // 更新统计 self.stats.total_connections += 1; self.stats.last_activity = Some(Instant::now()); // 发送连接事件 let _ = self.event_tx.send(ConnectionEvent::Connected(mode, latency)); // 启动心跳和状态监听 self.start_heartbeat(mediator.clone()).await; self.start_connection_monitor(mediator.clone()).await; info!("Connected successfully, mode: {:?}, latency: {}ms", mode, latency); Ok(()) } /// 断开连接 pub async fn disconnect(&mut self) { if let Some(mediator) = &self.mediator { mediator.logout().await.ok(); } self.mediator = None; self.set_status(ConnectionStatus::Disconnected).await; self.stats.disconnections += 1; info!("Disconnected from server"); } /// 设置连接状态 async fn set_status(&self, status: ConnectionStatus) { self.state.write().await.status = status.clone(); let _ = self.event_tx.send(ConnectionEvent::StateChanged(status)); } /// 启动心跳 async fn start_heartbeat(&self, mediator: Arc) { let event_tx = self.event_tx.clone(); let mut interval = tokio::time::interval(self.heartbeat_interval); tokio::spawn(async move { loop { interval.tick().await; // 发送心跳 match mediator.send_ping().await { Ok(latency) => { let _ = event_tx.send(ConnectionEvent::LatencyUpdated(latency)); } Err(e) => { error!("Heartbeat failed: {:?}", e); let _ = event_tx.send(ConnectionEvent::Error(format!("Heartbeat failed: {}", e))); } } } }); } /// 启动连接监控 async fn start_connection_monitor(&self, mediator: Arc) { let event_tx = self.event_tx.clone(); tokio::spawn(async move { while let Ok(state) = mediator.on_connection_state_changed().await { match state { ConnectionType::P2P | ConnectionType::Relay => { let _ = event_tx.send(ConnectionEvent::Connected(state.into(), 0)); } _ => { let _ = event_tx.send(ConnectionEvent::Disconnected); } } } }); } /// 获取当前状态 pub async fn state(&self) -> ConnectionState { self.state.read().await.clone() } /// 获取事件接收器 pub fn subscribe(&self) -> broadcast::Receiver { self.event_tx.subscribe() } /// 获取 Stream pub fn get_stream(&self) -> Option> { self.mediator.as_ref()?.get_stream() } /// 获取统计数据 pub fn stats(&self) -> &ConnectionStats { &self.stats } } #[derive(Error, Debug)] pub enum ConnectionError { #[error("Login failed: {0}")] LoginFailed(String), #[error("Unknown connection type")] UnknownConnectionType, #[error("Not connected")] NotConnected, #[error("Connection timeout")] Timeout, #[error("Connection refused")] Refused, } ``` ### 2.2 业务数据通道 #### 2.2.1 BusinessChannel 完整实现(200 行) ```rust // crates/agent-client/src/core/business_channel.rs use std::sync::Arc; use std::time::Duration; use tokio::sync::{mpsc, RwLock, broadcast}; use dashmap::DashMap; use tracing::{info, warn, error, debug}; use librustdesk::{Stream, BusinessChannel, BusinessChannelConfig, BUSINESS_CHANNEL_ID}; use crate::message::proto::{BusinessEnvelope, BusinessMessageType}; /// 业务通道管理器 pub struct BusinessChannelManager { /// 业务通道实例 channel: Option, /// 配置 config: BusinessChannelConfig, /// 消息发送器 message_tx: broadcast::Sender, /// 订阅者 subscribers: Arc>>, /// 是否已连接 connected: RwLock, } #[derive(Clone, Debug)] pub struct BusinessMessage { pub message_type: BusinessMessageType, pub payload: Vec, pub timestamp: i64, pub source_id: Option, } impl BusinessChannelManager { /// 创建新的业务通道管理器 pub fn new() -> Self { let (message_tx, _) = broadcast::channel(100); Self { channel: None, config: BusinessChannelConfig::default(), message_tx, subscribers: Arc::new(DashMap::new()), connected: RwLock::new(false), } } /// 从 qiming-rustdesk 的 Stream 创建业务通道 pub async fn create_from_stream( &mut self, stream: Arc, ) -> Result<(), BusinessChannelError> { info!("Creating business channel from stream"); let channel = BusinessChannel::from_stream( stream, Some(self.config.clone()), ).await?; self.channel = Some(channel); *self.connected.write().await = true; // 启动消息处理循环 self.start_message_handler().await; info!("Business channel created successfully"); Ok(()) } /// 发送 Agent 任务请求 pub async fn send_agent_task(&self, task: &[u8]) -> Result<(), BusinessChannelError> { self.send_message(BusinessMessageType::AgentTask, task).await } /// 发送 Agent 取消请求 pub async fn send_agent_cancel(&self, task_id: &str) -> Result<(), BusinessChannelError> { let payload = serde_json::json!({ "task_id": task_id, "timestamp": chrono::Utc::now().timestamp_millis(), }); self.send_message( BusinessMessageType::AgentCancel, serde_json::to_vec(&payload)?.as_slice(), ).await } /// 发送业务消息 async fn send_message( &self, message_type: BusinessMessageType, payload: &[u8], ) -> Result<(), BusinessChannelError> { let channel = self.channel.as_ref() .ok_or(BusinessChannelError::NotConnected)?; channel.send(message_type, payload).await .map_err(|e| BusinessChannelError::SendFailed(e.to_string()))?; Ok(()) } /// 启动消息处理循环 async fn start_message_handler(&self) { let mut channel = self.channel.clone(); let message_tx = self.message_tx.clone(); tokio::spawn(async move { loop { let result = match &mut channel { Some(ch) => ch.recv().await, None => break, }; match result { Ok(Some((msg_type, payload))) => { let message = BusinessMessage { message_type: msg_type, payload, timestamp: chrono::Utc::now().timestamp_millis(), source_id: None, }; // 广播给所有订阅者 let _ = message_tx.send(message.clone()); // 发送给内部处理器 Self::handle_message(message).await; } Ok(None) => { warn!("Business channel received empty message"); } Err(e) => { error!("Business channel error: {:?}", e); break; } } } }); } /// 处理接收到的消息 async fn handle_message(message: BusinessMessage) { debug!("Handling business message: {:?}", message.message_type); match message.message_type { BusinessMessageType::AgentTask => { // TODO任务并执行: 解析 } BusinessMessageType::AgentResponse => { // TODO: 处理响应 } BusinessMessageType::AgentProgress => { // TODO: 处理进度 } BusinessMessageType::AgentCancel => { // TODO: 处理取消 } _ => { warn!("Unknown message type: {:?}", message.message_type); } } } /// 订阅业务消息 pub fn subscribe(&self, key: &str) -> broadcast::Receiver { let (tx, rx) = mpsc::unbounded_channel(); self.subscribers.insert(key.to_string(), tx); self.message_tx.subscribe() } /// 取消订阅 pub fn unsubscribe(&self, key: &str) { self.subscribers.remove(key); } /// 检查是否已连接 pub async fn is_connected(&self) -> bool { *self.connected.read().await } } #[derive(Error, Debug)] pub enum BusinessChannelError { #[error("Not connected")] NotConnected, #[error("Send failed: {0}")] SendFailed(String), #[error("Receive failed: {0}")] ReceiveFailed(String), #[error("Parse failed: {0}")] ParseFailed(String), } ``` ### 2.3 设置界面 #### 2.3.1 Settings 主组件完整实现(300 行) ```rust // crates/agent-client/src/components/settings.rs use std::sync::Arc; use tokio::sync::RwLock; use gpui::{div, Component, Context, Element, IntoElement, Render}; use gpui::prelude::*; use gpui_component::{TabPanel, TabItem, Button, Input, IconButton, IconName}; use crate::core::config::{ServerConfig, SecurityConfig, AppConfig}; use crate::core::platform::AutoLaunchManager; use crate::app::SettingsSubTab; /// 设置组件 pub struct Settings { /// 当前子页面 active_subtab: SettingsSubTab, /// 服务器配置 server_config: ServerConfig, /// 安全配置 security_config: SecurityConfig, /// 是否正在保存 is_saving: bool, /// 保存结果 save_result: Option, /// 自动启动管理器 auto_launch: Option>, } pub struct SaveResult { success: bool, message: String, } impl Settings { pub fn new(cx: &mut Context) -> Self { let config = AppConfig::load().unwrap_or_default(); let server_config = config.server.unwrap_or_default(); let security_config = config.security.unwrap_or_default(); Self { active_subtab: SettingsSubTab::Server, server_config, security_config, is_saving: false, save_result: None, auto_launch: None, } } } impl Component for Settings { fn render(&mut self, cx: &mut gpui::Context) -> impl IntoElement { div() .id("settings") .size_full() .p_4() .flex() .col() .gap_4() .child(self.render_header(cx)) .child(self.render_content(cx)) } } impl Settings { /// 渲染标题栏 fn render_header(&mut self, cx: &mut Context) -> impl IntoElement { div() .flex() .items_center() .justify_between() .child( div() .text_xl() .font_bold() .child("设置") ) .child( div() .flex() .gap_2() .child( Button::new("save-btn", cx) .label("保存") .disabled(self.is_saving) .on_click(|_, cx| { self.save(cx); }) ) .child( Button::new("reset-btn", cx) .label("恢复默认") .variant(gpui_component::ButtonVariant::Ghost) .on_click(|_, cx| { self.reset_to_default(cx); }) ) ) } /// 渲染子页面导航 fn render_content(&mut self, cx: &mut Context) -> impl IntoElement { div() .flex_1() .overflow_hidden() .child( div() .flex() .size_full() .child(self.render_sidebar(cx)) .child(self.render_main(cx)) ) } /// 渲染侧边栏 fn render_sidebar(&mut self, cx: &mut Context) -> impl IntoElement { div() .w(px(200.0)) .h_full() .bg(cx.theme().surface) .border_r_1() .border_color(cx.theme().border) .p_2() .flex() .col() .gap_1() .child(self.render_nav_item("服务器", SettingsSubTab::Server, cx)) .child(self.render_nav_item("安全", SettingsSubTab::Security, cx)) .child(self.render_nav_item("常规", SettingsSubTab::General, cx)) .child(self.render_nav_item("外观", SettingsSubTab::Appearance, cx)) .child(self.render_nav_item("日志", SettingsSubTab::Logging, cx)) } /// 渲染导航项 fn render_nav_item( &mut self, label: &'static str, tab: SettingsSubTab, cx: &mut Context, ) -> impl IntoElement { let is_active = self.active_subtab == tab; div() .p_2() .rounded-md() .bg(if is_active { cx.theme().primary } else { gpui::Transparent }) .text_color(if is_active { cx.theme().on_primary } else { cx.theme().text }) .cursor_pointer() .on_click(move |_, cx| { self.active_subtab = tab; cx.notify(); }) .child(label) } /// 渲染主内容区 fn render_main(&mut self, cx: &mut Context) -> impl IntoElement { div() .flex_1() .p_4() .overflow_auto() .child(match self.active_subtab { SettingsSubTab::Server => self.render_server_config(cx), SettingsSubTab::Security => self.render_security_config(cx), SettingsSubTab::General => self.render_general_config(cx), SettingsSubTab::Appearance => self.render_appearance_config(cx), SettingsSubTab::Logging => self.render_logging_config(cx), }) } /// 渲染服务器配置 fn render_server_config(&mut self, cx: &mut Context) -> impl IntoElement { div() .flex() .col() .gap_4() .child( div() .text_lg() .font_bold() .child("服务器配置") ) .child( div() .text_sm() .text_gray() .child("配置连接到 agent-server-admin 的服务器地址") ) // 信令服务器 .child(self.render_section("信令服务器 (hbbs)", cx, |cx| { div() .flex() .col() .gap_2() .child( Input::new(&mut self.server_config.hbbs_host, cx) .placeholder("例如: hbbs.example.com") .label("主机地址") ) .child( Input::new(&mut self.server_config.hbbs_port, cx) .placeholder("21116") .label("端口") .validator(|s| { s.parse::().map_err(|_| "端口必须是 1-65535".to_string()) }) ) })) // 中继服务器 .child(self.render_section("中继服务器 (hbbr)", cx, |cx| { div() .flex() .col() .gap_2() .child( Input::new(&mut self.server_config.hbbr_host, cx) .placeholder("例如: relay.example.com") .label("主机地址") ) .child( Input::new(&mut self.server_config.hbbr_port, cx) .placeholder("21117") .label("端口") .validator(|s| { s.parse::().map_err(|_| "端口必须是 1-65535".to_string()) }) ) })) // API 服务器 .child(self.render_section("API 服务器", cx, |cx| { div() .flex() .col() .gap_2() .child( Input::new(&mut self.server_config.api_host, cx) .placeholder("例如: api.example.com") .label("主机地址") ) .child( Input::new(&mut self.server_config.api_port, cx) .placeholder("8080") .label("端口") .validator(|s| { s.parse::().map_err(|_| "端口必须是 1-65535".to_string()) }) ) })) // 连接测试按钮 .child( Button::new("test-connection", cx) .label("测试连接") .on_click(|_, cx| { self.test_connection(cx); }) ) } /// 渲染配置区块 fn render_section( &mut self, title: &'static str, _cx: &mut Context, content: F, ) -> impl IntoElement where F: FnOnce(&mut Context) -> impl IntoElement + 'static, { div() .p_4() .border_1() .border_gray_300() .rounded-lg() .child( div() .font_bold() .mb_2() .child(title) ) .child(content(_cx)) } /// 保存配置 fn save(&mut self, cx: &mut Context) { self.is_saving = true; cx.notify(); let config = AppConfig { server: Some(self.server_config.clone()), security: Some(self.security_config.clone()), ..Default::default() }; cx.spawn(async move { // 保存配置 if let Err(e) = config.save().await { // 显示错误 } }); } /// 重置为默认 fn reset_to_default(&mut self, cx: &mut Context) { self.server_config = ServerConfig::default(); cx.notify(); } /// 测试连接 fn test_connection(&mut self, cx: &mut Context) { // TODO: 实现连接测试 } } ``` --- ## Phase 3:依赖管理(P0) ### 3.1 Node.js 自动安装 #### 3.1.1 NodeDetector 完整实现(200 行) ```rust // crates/agent-client/src/core/dependency/node.rs use std::path::PathBuf; use std::process::Command; use tokio::fs; use regex::Regex; use thiserror::Error; use crate::core::platform::directories::AppDirectories; const MIN_NODE_VERSION: &str = "18.0.0"; /// Node.js 检测器 pub struct NodeDetector; #[derive(Debug, Clone)] pub struct NodeInfo { /// 安装来源 pub source: NodeSource, /// 版本号 pub version: Option, /// 可执行文件路径 pub path: PathBuf, /// 是否有效 pub is_valid: bool, /// 错误信息(如果无效) pub error: Option, } #[derive(Debug, Clone, PartialEq)] pub enum NodeSource { /// 系统全局安装 SystemGlobal(PathBuf), /// 客户端隔离目录安装 ClientDirectory(PathBuf), /// 未安装 NotInstalled, } #[derive(Error, Debug)] pub enum NodeDetectionError { #[error("Path is not executable: {0}")] NotExecutable(String), #[error("Failed to get version: {0}")] VersionParseError(String), #[error("Version too old: {0}, minimum required: {1}")] VersionTooOld(String, String), #[error("Node not found")] NotFound, } impl NodeDetector { /// 检测系统全局 Node.js pub async fn detect_system_node(&self) -> Result, NodeDetectionError> { // 1. 检查 PATH 中的 node if let Some(path) = self.find_in_path("node") { if let Some(info) = self.check_node(&path).await? { return Ok(Some(info)); } } // 2. 检查平台特定的系统路径 #[cfg(target_os = "macos")] { let path = PathBuf::from("/usr/local/bin/node"); if path.exists() { if let Some(info) = self.check_node(&path).await? { return Ok(Some(info)); } } } #[cfg(target_os = "windows")] { let paths = vec![ PathBuf::from(r"C:\Program Files\nodejs\node.exe"), PathBuf::from(r"C:\Program Files (x86)\nodejs\node.exe"), ]; for path in paths { if path.exists() { if let Some(info) = self.check_node(&path).await? { return Ok(Some(info)); } } } } Ok(None) } /// 检测客户端目录中的 Node.js pub async fn detect_client_node(&self) -> Result, NodeDetectionError> { let install_dir = self.get_client_install_dir(); #[cfg(target_os = "macos")] let node_path = install_dir.join("bin/node"); #[cfg(target_os = "windows")] let node_path = install_dir.join("node.exe"); #[cfg(target_os = "linux")] let node_path = install_dir.join("bin/node"); if node_path.exists() { if let Some(info) = self.check_node(&node_path).await? { return Ok(Some(info)); } } Ok(None) } /// 检测并返回最佳可用的 Node.js pub async fn detect_best(&self) -> Result, NodeDetectionError> { // 1. 优先使用系统全局 if let Some(info) = self.detect_system_node().await? { return Ok(Some(info)); } // 2. 检查客户端目录 if let Some(info) = self.detect_client_node().await? { return Ok(Some(info)); } Ok(None) } /// 检查 Node.js 是否有效 async fn check_node(&self, path: &PathBuf) -> Result, NodeDetectionError> { // 检查文件是否存在且可执行 if !path.exists() { return Ok(None); } // 获取版本 let version = match self.get_version(path).await { Ok(v) => v, Err(e) => { return Ok(Some(NodeInfo { source: NodeSource::SystemGlobal(path.clone()), version: None, path: path.clone(), is_valid: false, error: Some(e.to_string()), })); } }; // 检查版本是否满足最低要求 if let Some(v) = &version { if v < &semver::Version::parse(MIN_NODE_VERSION).unwrap() { return Ok(Some(NodeInfo { source: NodeSource::SystemGlobal(path.clone()), version: Some(v.clone()), path: path.clone(), is_valid: false, error: Some(format!( "Version {} too old, minimum required: {}", v, MIN_NODE_VERSION )), })); } } Ok(Some(NodeInfo { source: NodeSource::SystemGlobal(path.clone()), version, path: path.clone(), is_valid: true, error: None, })) } /// 从 PATH 中查找可执行文件 fn find_in_path(&self, command: &str) -> Option { std::env::var_os("PATH").and_then(|paths| { paths.to_str().map(|paths_str| { paths_str.split(std::path::PATH_SEPARATOR) .filter_map(|dir| { let path = PathBuf::from(dir).join(command); // 检查文件是否存在且不是目录 if path.exists() && path.is_file() { #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; if let Ok(metadata) = path.metadata() { if metadata.permissions().mode() & 0o111 != 0 { return Some(path); } } } #[cfg(windows)] { return Some(path); } } None }) .next() }) }) } /// 获取 Node.js 版本 async fn get_version(&self, path: &PathBuf) -> Result, NodeDetectionError> { let output = Command::new(path) .arg("--version") .output() .map_err(|e| NodeDetectionError::VersionParseError(e.to_string()))?; if !output.status.success() { return Err(NodeDetectionError::VersionParseError( String::from_utf8_lossy(&output.stderr).to_string() )); } let version_str = String::from_utf8_lossy(&output.stdout) .trim() .to_string(); // 解析版本号(去除 'v' 前缀) let version_str = version_str.strip_prefix('v').unwrap_or(&version_str); // 解析语义化版本 let version = semver::Version::parse(version_str) .map_err(|e| NodeDetectionError::VersionParseError(e.to_string()))?; Ok(Some(version)) } /// 获取客户端安装目录 fn get_client_install_dir(&self) -> PathBuf { let dirs = AppDirectories::new(); dirs.data_dir.join("tools/node") } } /// 检查命令是否可用 pub fn is_command_available(command: &str) -> bool { if let Some(path) = PathBuf::from(command).canonicalize().ok() { return path.exists() && path.is_file(); } if let Some(path) = std::env::var_os("PATH") { return path.to_str() .unwrap_or("") .split(std::path::PATH_SEPARATOR) .any(|dir| { let path = PathBuf::from(dir).join(command); #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; if let Ok(metadata) = path.metadata() { return metadata.permissions().mode() & 0o111 != 0; } } #[cfg(windows)] { return path.exists() && path.is_file(); } false }); } false } ``` #### 3.1.2 NodeInstaller 完整实现(250 行) ```rust // crates/agent-client/src/core/dependency/node_installer.rs use std::path::{PathBuf, Path}; use std::fs::{self, Permissions}; use std::os::unix::fs::PermissionsExt; use tokio::sync::mpsc; use reqwest; use tracing::{info, warn, error, debug}; use thiserror::Error; use super::node::NodeDetector; use crate::core::platform::directories::AppDirectories; use crate::utils::notification::Notification; const NODE_VERSION: &str = "20.10.0"; /// Node.js 安装器 pub struct NodeInstaller { /// 安装目录 install_dir: PathBuf, /// 版本 version: String, /// 下载 URL download_url: String, /// 通知发送器 notification_tx: mpsc::Sender, } #[derive(Error, Debug)] pub enum NodeInstallError { #[error("Download failed: {0}")] DownloadFailed(String), #[error("Extract failed: {0}")] ExtractFailed(String), #[error("Verification failed: {0}")] VerificationFailed(String), #[error("IO error: {0}")] IoError(String), #[error("Network error: {0}")] NetworkError(String), } pub enum InstallProgress { Downloading { progress: u8 }, Extracting, Configuring, Verifying, Completed, Failed(String), } impl NodeInstaller { /// 创建新的安装器 pub fn new(notification_tx: mpsc::Sender) -> Self { let version = NODE_VERSION.to_string(); let install_dir = Self::get_install_dir(); let download_url = Self::get_download_url(&version); Self { install_dir, version, download_url, notification_tx, } } /// 获取安装目录 fn get_install_dir() -> PathBuf { let dirs = AppDirectories::new(); dirs.data_dir.join("tools/node") } /// 获取下载 URL(跨平台) fn get_download_url(version: &str) -> String { #[cfg(target_os = "macos")] { if std::env::consts::ARCH == "aarch64" { format!( "https://nodejs.org/dist/{}/node-{}-darwin-arm64.tar.xz", version, version ) } else { format!( "https://nodejs.org/dist/{}/node-{}-darwin-x64.tar.xz", version, version ) } } #[cfg(target_os = "windows")] { format!( "https://nodejs.org/dist/{}/node-{}-win-x64.zip", version, version ) } #[cfg(target_os = "linux")] { format!( "https://nodejs.org/dist/{}/node-{}-linux-x64.tar.xz", version, version ) } } /// 安装 Node.js pub async fn install(&self) -> Result<(), NodeInstallError> { info!("Starting Node.js {} installation", self.version); self.send_notification("开始下载 Node.js...").await; // 1. 创建安装目录 fs::create_dir_all(&self.install_dir) .map_err(|e| NodeInstallError::IoError(e.to_string()))?; // 2. 下载压缩包 let archive_path = self.install_dir.join(format!("node-{}.tar.xz", self.version)); self.download(&archive_path).await?; // 3. 解压 self.send_notification("正在解压...").await; self.extract(&archive_path).await?; // 4. 记录版本 self.record_version().await?; // 5. 验证安装 self.send_notification("正在验证安装...").await; self.verify().await?; self.send_notification(&format!("Node.js {} 安装完成", self.version)).await; Ok(()) } /// 下载 Node.js async fn download(&self, archive_path: &PathBuf) -> Result<(), NodeInstallError> { info!("Downloading Node.js from {}", self.download_url); let response = reqwest::Client::new() .get(&self.download_url) .send() .await .map_err(|e| NodeInstallError::NetworkError(e.to_string()))?; if !response.status().is_success() { return Err(NodeInstallError::DownloadFailed( format!("HTTP {}", response.status()) )); } let total_size = response .content_length() .unwrap_or(0); let mut file = fs::File::create(archive_path) .map_err(|e| NodeInstallError::IoError(e.to_string()))?; let mut downloaded: u64 = 0; let mut stream = response.bytes_stream(); while let Some(chunk) = stream.next().await { let chunk = chunk .map_err(|e| NodeInstallError::DownloadFailed(e.to_string()))?; file.write_all(&chunk) .map_err(|e| NodeInstallError::IoError(e.to_string()))?; downloaded += chunk.len() as u64; // 发送下载进度 if total_size > 0 { let progress = (downloaded as f64 / total_size as f64 * 100.0) as u8; self.send_progress(InstallProgress::Downloading { progress }).await; } } info!("Download completed: {} bytes", downloaded); Ok(()) } /// 解压压缩包 async fn extract(&self, archive_path: &PathBuf) -> Result<(), NodeInstallError> { info!("Extracting archive"); // 解压到临时目录 let temp_dir = self.install_dir.join("temp"); fs::create_dir_all(&temp_dir) .map_err(|e| NodeInstallError::IoError(e.to_string()))?; // 使用 tar 解压 #[cfg(unix)] { let output = Command::new("tar") .args(&[ "-xf", archive_path.to_str().unwrap(), "-C", temp_dir.to_str().unwrap(), ]) .output() .await .map_err(|e| NodeInstallError::ExtractFailed(e.to_string()))?; if !output.status.success() { return Err(NodeInstallError::ExtractFailed( String::from_utf8_lossy(&output.stderr).to_string() )); } } #[cfg(windows)] { // Windows 使用解压工具 let output = Command::new("powershell") .args(&[ "-Command", &format!("Expand-Archive -Path '{}' -DestinationPath '{}'", archive_path.display(), temp_dir.display()) ]) .output() .await .map_err(|e| NodeInstallError::ExtractFailed(e.to_string()))?; if !output.status.success() { return Err(NodeInstallError::ExtractFailed( String::from_utf8_lossy(&output.stderr).to_string() )); } } // 移动内容到安装目录 let extracted_dir = temp_dir.join(format!("node-{}", self.version)); if extracted_dir.exists() { for entry in fs::read_dir(&extracted_dir)? { let entry = entry?; let dest = self.install_dir.join(entry.file_name()); fs::rename(entry.path(), &dest) .map_err(|e| NodeInstallError::IoError(e.to_string()))?; } } // 清理 fs::remove_dir_all(&temp_dir).ok(); fs::remove_file(archive_path).ok(); self.send_progress(InstallProgress::Extracting).await; Ok(()) } /// 记录安装版本 async fn record_version(&self) -> Result<(), NodeInstallError> { let version_file = self.install_dir.join("versions.json"); #[cfg(unix)] { // 设置 bin 目录权限 let bin_dir = self.install_dir.join("bin"); if bin_dir.exists() { fs::set_permissions(&bin_dir, fs::Permissions::from_mode(0o755)) .map_err(|e| NodeInstallError::IoError(e.to_string()))?; } } // 写入版本信息 let version_info = serde_json::json!({ "name": "node", "version": self.version, "installed_at": chrono::Utc::now().to_rfc3339(), }); fs::write(&version_file, version_info.to_string()) .map_err(|e| NodeInstallError::IoError(e.to_string()))?; Ok(()) } /// 验证安装 async fn verify(&self) -> Result<(), NodeInstallError> { #[cfg(target_os = "macos")] let node_path = self.install_dir.join("bin/node"); #[cfg(target_os = "windows")] let node_path = self.install_dir.join("node.exe"); #[cfg(target_os = "linux")] let node_path = self.install_dir.join("bin/node"); if !node_path.exists() { return Err(NodeInstallError::VerificationFailed( "Node executable not found".to_string() )); } // 检查版本 let output = Command::new(&node_path) .arg("--version") .output() .await .map_err(|e| NodeInstallError::VerificationFailed(e.to_string()))?; if !output.status.success() { return Err(NodeInstallError::VerificationFailed( "Version check failed".to_string() )); } self.send_progress(InstallProgress::Verifying).await; Ok(()) } /// 发送通知 async fn send_notification(&self, message: &str) { let _ = self.notification_tx.send(Notification::info(message)).await; } /// 发送进度 async fn send_progress(&self, progress: InstallProgress) { let message = match &progress { InstallProgress::Downloading { progress } => format!("下载中... {}%", progress), InstallProgress::Extracting => "正在解压...", InstallProgress::Configuring => "正在配置...", InstallProgress::Verifying => "正在验证...", InstallProgress::Completed => "安装完成", InstallProgress::Failed(e) => return, }; let _ = self.notification_tx.send(Notification::info(message)).await; } } ``` --- ## Phase 4:Agent 运行时(P1) ### 4.1 AgentManager 完整实现(250 行) ```rust // crates/agent-client/src/core/agent.rs use std::path::PathBuf; use std::time::Duration; use std::sync::Arc; use tokio::sync::{mpsc, RwLock, broadcast}; use dashmap::DashMap; use tracing::{info, warn, error, debug}; use agent_runner::{ start_agent, cancel_agent, get_agent_status, AgentRequest, AgentOutput, AgentStatus, }; use crate::message::proto::{AgentTaskRequest, AgentTaskOutput}; use crate::utils::Notification; /// Agent 任务信息 pub struct AgentTaskInfo { pub task_id: String, pub session_id: String, pub status: AgentTaskStatus, pub output_tx: mpsc::Sender, pub started_at: chrono::DateTime, } #[derive(Clone, Copy, Debug, PartialEq)] pub enum AgentTaskStatus { Pending, Active, Completing, Completed, Error, } /// Agent 管理器 pub struct AgentManager { /// 活跃任务映射 tasks: DashMap, /// 消息通道 message_tx: mpsc::Sender, /// 消息广播 event_tx: broadcast::Sender, /// 工作目录 work_dir: PathBuf, } #[derive(Clone, Debug)] pub enum AgentMessage { Progress { task_id: String, output: AgentOutput, }, Completed { task_id: String, output: Option, }, Error { task_id: String, error: String, }, } #[derive(Clone, Debug)] pub enum AgentEvent { TaskStarted(String), TaskProgress(String, String), TaskCompleted(String), TaskFailed(String, String), TaskCancelled(String), } #[derive(Error, Debug)] pub enum AgentError { #[error("Task not found: {0}")] NotFound(String), #[error("Start failed: {0}")] StartFailed(String), #[error("Cancel failed: {0}")] CancelFailed(String), #[error("Worker stopped")] WorkerStopped, } impl AgentManager { /// 创建新的 Agent 管理器 pub fn new() -> Self { let (message_tx, _) = mpsc::channel(100); let (event_tx, _) = broadcast::channel(100); let work_dir = dirs::data_dir() .unwrap_or_else(|| PathBuf::from(".")) .join("qiming-agent/workspace"); Self { tasks: DashMap::new(), message_tx, event_tx, work_dir, } } /// 执行 Agent 任务 pub async fn execute_task( &self, request: AgentTaskRequest, ) -> Result { let task_id = request.task_id.clone() .unwrap_or_else(|| uuid::Uuid::new_v4().to_string()); let session_id = uuid::Uuid::new_v4().to_string(); info!("Starting agent task: {}", task_id); // 1. 转换请求 let agent_request = self.convert_request(request).await; // 2. 创建输出通道 let (output_tx, output_rx) = mpsc::channel(100); // 3. 启动任务 let mut output_stream = start_agent(agent_request) .await .map_err(|e| AgentError::StartFailed(e.to_string()))?; // 4. 注册任务 let task_info = AgentTaskInfo { task_id: task_id.clone(), session_id: session_id.clone(), status: AgentTaskStatus::Active, output_tx: output_tx.clone(), started_at: chrono::Utc::now(), }; self.tasks.insert(task_id.clone(), task_info); // 5. 发送任务开始事件 let _ = self.event_tx.send(AgentEvent::TaskStarted(task_id.clone())); // 6. 在后台处理输出流 let tasks = self.tasks.clone(); let message_tx = self.message_tx.clone(); let event_tx = self.event_tx.clone(); let task_id_clone = task_id.clone(); tokio::spawn(async move { while let Some(output) = output_stream.next().await { // 发送输出到通道 let _ = output_tx.send(output.clone()).await; // 发送进度消息 let _ = message_tx.send(AgentMessage::Progress { task_id: task_id_clone.clone(), output: output.clone(), }).await; // 发送事件 let content = output.content.clone().unwrap_or_default(); let _ = event_tx.send(AgentEvent::TaskProgress( task_id_clone.clone(), content, )).await; } // 任务完成 tasks.remove(&task_id_clone); let _ = message_tx.send(AgentMessage::Completed { task_id: task_id_clone.clone(), output: None, }).await; let _ = event_tx.send(AgentEvent::TaskCompleted(task_id_clone)).await; }); Ok(task_id) } /// 取消任务 pub async fn cancel_task(&self, task_id: &str) -> Result<(), AgentError> { info!("Cancelling task: {}", task_id); if let Some(info) = self.tasks.get(task_id) { // 更新状态 let mut info = info.clone(); info.status = AgentTaskStatus::Completing; // 调用取消 cancel_agent(&info.session_id) .await .map_err(|e| AgentError::CancelFailed(e.to_string()))?; // 发送事件 let _ = self.event_tx.send(AgentEvent::TaskCancelled(task_id.to_string())); Ok(()) } else { Err(AgentError::NotFound(task_id.to_string())) } } /// 获取任务状态 pub async fn get_task_status(&self, task_id: &str) -> Option { self.tasks.get(task_id).map(|info| info.status) } /// 获取所有活跃任务 pub fn get_active_tasks(&self) -> Vec<(String, AgentTaskStatus)> { self.tasks .iter() .map(|info| (info.key().clone(), info.status)) .collect() } /// 订阅事件 pub fn subscribe(&self) -> broadcast::Receiver { self.event_tx.subscribe() } /// 转换请求 async fn convert_request(&self, request: AgentTaskRequest) -> AgentRequest { let work_dir = request.work_dir .map(PathBuf::from) .unwrap_or_else(|| self.work_dir.clone()); let timeout = if request.timeout_seconds > 0 { Some(Duration::from_secs(request.timeout_seconds as u64)) } else { None }; AgentRequest { prompt: request.prompt, work_dir, env: request.env.into_iter().collect(), timeout, // ... 其他字段 } } /// 设置工作目录 pub fn set_work_dir(&mut self, path: PathBuf) { self.work_dir = path; } } ``` --- ## Phase 5:增强功能(P1) ### 5.1 权限检测 #### 5.1.1 PermissionManager 完整实现(200 行) ```rust // crates/agent-client/src/core/platform/permissions.rs use std::path::PathBuf; use thiserror::Error; #[cfg(target_os = "macos")] use std::process::Command; #[cfg(target_os = "windows")] use winreg::enums::*; #[cfg(target_os = "windows")] use winreg::RegKey; /// 权限类型 #[derive(Clone, Copy, Debug, PartialEq)] pub enum PermissionType { /// 辅助功能(键盘鼠标模拟) Accessibility, /// 屏幕录制(远程桌面) ScreenRecording, /// 磁盘访问(文件传输) FullDiskAccess, /// 麦克风 Microphone, #[cfg(target_os = "linux")] Wayland, } /// 权限状态 #[derive(Clone, Copy, Debug, PartialEq)] pub enum PermissionStatus { /// 已授权 Granted, /// 被拒绝 Denied, /// 尚未确定(首次请求) NotDetermined, /// 受限制 Restricted, } /// 权限检查结果 pub struct PermissionCheck { pub permission_type: PermissionType, pub status: PermissionStatus, pub description: String, pub setting_url: String, } #[derive(Error, Debug)] pub enum PermissionError { #[error("Platform not supported")] NotSupported, #[error("Check failed: {0}")] CheckFailed(String), #[error("Open settings failed: {0}")] OpenSettingsFailed(String), } /// 权限管理器 pub struct PermissionManager; impl PermissionManager { /// 检查所有权限 pub async fn check_all(&self) -> Vec { vec![ self.check_accessibility().await, self.check_screen_recording().await, self.check_full_disk_access().await, self.check_microphone().await, ] } /// 检查辅助功能权限 pub async fn check_accessibility(&self) -> PermissionCheck { #[cfg(target_os = "macos")] { let status = Self::check_macos_permission("Accessibility"); PermissionCheck { permission_type: PermissionType::Accessibility, status, description: "用于远程控制时模拟键盘鼠标输入".to_string(), setting_url: "x-apple.systempreferences:com.apple.SecurityPrivacy_Accessibility".to_string(), } } #[cfg(target_os = "windows")] { let status = Self::check_windows_permission("Accessibility"); PermissionCheck { permission_type: PermissionType::Accessibility, status, description: "用于远程控制时模拟键盘鼠标输入".to_string(), setting_url: "ms-settings:privacy-keyboard".to_string(), } } #[cfg(target_os = "linux")] { PermissionCheck { permission_type: PermissionType::Accessibility, status: PermissionStatus::NotDetermined, description: "用于远程控制时模拟键盘鼠标输入".to_string(), setting_url: "".to_string(), } } } /// 检查屏幕录制权限 pub async fn check_screen_recording(&self) -> PermissionCheck { #[cfg(target_os = "macos")] { let status = Self::check_macos_permission("Screen Recording"); PermissionCheck { permission_type: PermissionType::ScreenRecording, status, description: "用于远程桌面实时画面传输".to_string(), setting_url: "x-apple.systempreferences:com.apple.SecurityPrivacy_ScreenRecording".to_string(), } } #[cfg(target_os = "windows")] { PermissionCheck { permission_type: PermissionType::ScreenRecording, status: PermissionStatus::Granted, description: "用于远程桌面实时画面传输".to_string(), setting_url: "ms-settings:privacy-webcam".to_string(), } } #[cfg(target_os = "linux")] { PermissionCheck { permission_type: PermissionType::ScreenRecording, status: PermissionStatus::NotDetermined, description: "用于远程桌面实时画面传输".to_string(), setting_url: "".to_string(), } } } /// 打开系统设置页面 pub fn open_settings(&self, setting_url: &str) -> Result<(), PermissionError> { #[cfg(target_os = "macos")] { open::that(setting_url) .map_err(|e| PermissionError::OpenSettingsFailed(e.to_string()))?; } #[cfg(target_os = "windows")] { if setting_url.starts_with("ms-settings:") { Command::new("cmd") .args(&["/c", "start", setting_url]) .spawn() .map_err(|e| PermissionError::OpenSettingsFailed(e.to_string()))?; } else { open::that(setting_url) .map_err(|e| PermissionError::OpenSettingsFailed(e.to_string()))?; } } #[cfg(target_os = "linux")] { open::that(setting_url) .map_err(|e| PermissionError::OpenSettingsFailed(e.to_string()))?; } Ok(()) } /// 检查 macOS 权限(使用 tccutil 或直接检查) #[cfg(target_os = "macos")] fn check_macos_permission(name: &str) -> PermissionStatus { // 尝试通过命令行检查 let output = Command::new("sqlite3") .args(&[ "/Library/Application Support/com.apple.TCC/TCC.db", &format!("SELECT auth_value FROM access WHERE client='{}' AND service='{}'", "qiming-agent", name) ]) .output(); match output { Ok(output) => { let result = String::from_utf8_lossy(&output.stdout); match result.trim() { "2" => PermissionStatus::Granted, // kTCCAuthResultAuthorized "1" => PermissionStatus::NotDetermined, "0" => PermissionStatus::Denied, _ => PermissionStatus::NotDetermined, } } Err(_) => PermissionStatus::NotDetermined, } } /// 检查 Windows 权限 #[cfg(target_os = "windows")] fn check_windows_permission(_name: &str) -> PermissionStatus { // Windows 权限检查比较复杂,这里简化处理 // 实际实现需要检查注册表或使用 Windows API PermissionStatus::Granted } } ``` --- ## Phase 6:收尾完善(P2) ### 6.1 About 组件完整实现(120 行) ```rust // crates/agent-client/src/components/about.rs use gpui::{div, Component, Context, Element, IntoElement, Render}; use gpui::prelude::*; use gpui_component::{Button, Modal, Link}; use crate::VERSION; use crate::BUILD_INFO; /// 关于组件 pub struct About; impl About { pub fn render(&mut self, cx: &mut Context) -> impl IntoElement { div() .id("about") .size_full() .p_8() .flex() .col() .items_center() .gap_6() // 应用图标 .child( div() .w(px(96.0)) .h(px(96.0)) .bg_blue_500() .rounded_full() .flex() .items_center() .justify_center() .child( div() .text_4xl() .text_white() .font_bold() .child("A") ) ) // 应用名称 .child( div() .text_2xl() .font_bold() .text_color(cx.theme().text) .child("qiming-agent") ) // 版本信息 .child( div() .text_lg() .text_gray() .child(format!("版本 {} ({})", VERSION, BUILD_INFO.git_sha)) ) // 构建信息 .child( div() .text_sm() .text_gray_500() .child(format!( "Rust {}, 构建时间: {}", env!("RUST_VERSION"), BUILD_INFO.build_date )) ) // 许可证 .child( div() .text_sm() .text_gray_500() .child("MIT License") ) // 链接 .child( div() .flex() .gap_4() .mt_4() .child( Link::new("https://docs.example.com") .label("文档") ) .child( Link::new("https://github.com/example/issues") .label("问题反馈") ) .child( Link::new("https://example.com") .label("官网") ) ) // 操作按钮 .child( div() .flex() .gap_4() .mt_8() .child( Button::new("export-logs", cx) .label("导出日志") .on_click(|_, cx| { self.export_logs(cx); }) ) .child( Button::new("check-update", cx) .label("检查更新") .on_click(|_, cx| { self.check_update(cx); }) ) ) } fn export_logs(&self, cx: &mut Context) { // TODO: 实现日志导出 } fn check_update(&self, cx: &mut Context) { // TODO: 实现更新检查 } } /// 构建信息 pub struct BuildInfo { pub git_sha: String, pub git_date: String, pub build_date: String, pub platform: String, pub arch: String, } impl BuildInfo { pub fn new() -> Self { Self { git_sha: env!("GIT_SHA").to_string(), git_date: env!("GIT_DESCRIBE").to_string(), build_date: chrono::Utc::now().to_rfc3339(), platform: std::env::consts::OS.to_string(), arch: std::env::consts::ARCH.to_string(), } } } ``` --- ## 8. 安全与网络实现 ### 8.0 协议版本协商 ```rust // crates/agent-protocol/src/version.rs use semver::Version; use thiserror::Error; /// 协议版本 pub const PROTOCOL_VERSION: &str = "1.0.0"; /// 支持的最低版本 pub const MIN_SUPPORTED_VERSION: &str = "1.0.0"; /// 版本协商错误 #[derive(Error, Debug)] pub enum VersionError { #[error("协议版本不兼容: 需要 >= {min}, 实际 {actual}")] Incompatible { min: String, actual: String }, #[error("无效的版本格式: {0}")] InvalidFormat(String), } /// 版本协商器 pub struct VersionNegotiator { /// 当前协议版本 current: Version, /// 最低支持版本 min_supported: Version, } impl VersionNegotiator { pub fn new() -> Self { Self { current: Version::parse(PROTOCOL_VERSION).unwrap(), min_supported: Version::parse(MIN_SUPPORTED_VERSION).unwrap(), } } /// 检查版本兼容性 pub fn check_compatibility(&self, remote_version: &str) -> Result { let remote = Version::parse(remote_version) .map_err(|_| VersionError::InvalidFormat(remote_version.to_string()))?; if remote < self.min_supported { return Err(VersionError::Incompatible { min: self.min_supported.to_string(), actual: remote.to_string(), }); } Ok(true) } /// 获取当前版本 pub fn current_version(&self) -> &str { PROTOCOL_VERSION } /// 协商版本(返回双方都支持的最高版本) pub fn negotiate(&self, remote_version: &str) -> Result { let remote = Version::parse(remote_version) .map_err(|_| VersionError::InvalidFormat(remote_version.to_string()))?; // 检查兼容性 self.check_compatibility(remote_version)?; // 返回较低的版本作为协商结果 let negotiated = if remote < self.current { remote } else { self.current.clone() }; Ok(negotiated.to_string()) } } /// 在握手消息中使用 impl HandshakeRequest { pub fn with_version() -> Self { Self { protocol_version: PROTOCOL_VERSION.to_string(), // ... 其他字段 } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_compatible_version() { let negotiator = VersionNegotiator::new(); assert!(negotiator.check_compatibility("1.0.0").is_ok()); assert!(negotiator.check_compatibility("1.1.0").is_ok()); assert!(negotiator.check_compatibility("2.0.0").is_ok()); } #[test] fn test_incompatible_version() { let negotiator = VersionNegotiator::new(); assert!(negotiator.check_compatibility("0.9.0").is_err()); } #[test] fn test_negotiate() { let negotiator = VersionNegotiator::new(); // 远程版本更高,使用本地版本 assert_eq!(negotiator.negotiate("2.0.0").unwrap(), "1.0.0"); // 远程版本相同 assert_eq!(negotiator.negotiate("1.0.0").unwrap(), "1.0.0"); } } ``` ### 8.0.1 配置文件加密存储 ```rust // crates/agent-client/src/core/security/config_encryption.rs use aes_gcm::{ aead::{Aead, KeyInit, OsRng}, Aes256Gcm, Nonce, }; use argon2::Argon2; use rand::RngCore; use std::fs; use std::path::Path; use thiserror::Error; /// 加密错误 #[derive(Error, Debug)] pub enum EncryptionError { #[error("加密失败: {0}")] EncryptionFailed(String), #[error("解密失败: {0}")] DecryptionFailed(String), #[error("密钥派生失败")] KeyDerivationFailed, #[error("IO 错误: {0}")] IoError(#[from] std::io::Error), } /// 配置加密器 pub struct ConfigEncryptor { /// 机器唯一标识(用于派生密钥) machine_id: String, } impl ConfigEncryptor { pub fn new() -> Result { let machine_id = Self::get_machine_id()?; Ok(Self { machine_id }) } /// 获取机器唯一标识 #[cfg(target_os = "macos")] fn get_machine_id() -> Result { use std::process::Command; let output = Command::new("ioreg") .args(["-rd1", "-c", "IOPlatformExpertDevice"]) .output()?; let stdout = String::from_utf8_lossy(&output.stdout); // 解析 IOPlatformUUID for line in stdout.lines() { if line.contains("IOPlatformUUID") { if let Some(uuid) = line.split('"').nth(3) { return Ok(uuid.to_string()); } } } Err(EncryptionError::KeyDerivationFailed) } #[cfg(target_os = "windows")] fn get_machine_id() -> Result { use winreg::enums::*; use winreg::RegKey; let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); let key = hklm.open_subkey("SOFTWARE\\Microsoft\\Cryptography")?; let guid: String = key.get_value("MachineGuid")?; Ok(guid) } #[cfg(target_os = "linux")] fn get_machine_id() -> Result { let id = fs::read_to_string("/etc/machine-id") .or_else(|_| fs::read_to_string("/var/lib/dbus/machine-id"))?; Ok(id.trim().to_string()) } /// 派生加密密钥 fn derive_key(&self, salt: &[u8]) -> Result<[u8; 32], EncryptionError> { let mut key = [0u8; 32]; Argon2::default() .hash_password_into(self.machine_id.as_bytes(), salt, &mut key) .map_err(|_| EncryptionError::KeyDerivationFailed)?; Ok(key) } /// 加密配置 pub fn encrypt(&self, plaintext: &[u8]) -> Result, EncryptionError> { // 生成随机盐和 nonce let mut salt = [0u8; 16]; let mut nonce_bytes = [0u8; 12]; OsRng.fill_bytes(&mut salt); OsRng.fill_bytes(&mut nonce_bytes); // 派生密钥 let key = self.derive_key(&salt)?; let cipher = Aes256Gcm::new_from_slice(&key) .map_err(|e| EncryptionError::EncryptionFailed(e.to_string()))?; // 加密 let nonce = Nonce::from_slice(&nonce_bytes); let ciphertext = cipher .encrypt(nonce, plaintext) .map_err(|e| EncryptionError::EncryptionFailed(e.to_string()))?; // 格式: salt (16) + nonce (12) + ciphertext let mut result = Vec::with_capacity(16 + 12 + ciphertext.len()); result.extend_from_slice(&salt); result.extend_from_slice(&nonce_bytes); result.extend_from_slice(&ciphertext); Ok(result) } /// 解密配置 pub fn decrypt(&self, encrypted: &[u8]) -> Result, EncryptionError> { if encrypted.len() < 28 { return Err(EncryptionError::DecryptionFailed("数据太短".to_string())); } // 解析 salt、nonce 和密文 let salt = &encrypted[..16]; let nonce_bytes = &encrypted[16..28]; let ciphertext = &encrypted[28..]; // 派生密钥 let key = self.derive_key(salt)?; let cipher = Aes256Gcm::new_from_slice(&key) .map_err(|e| EncryptionError::DecryptionFailed(e.to_string()))?; // 解密 let nonce = Nonce::from_slice(nonce_bytes); let plaintext = cipher .decrypt(nonce, ciphertext) .map_err(|e| EncryptionError::DecryptionFailed(e.to_string()))?; Ok(plaintext) } /// 加密并保存到文件 pub fn encrypt_to_file(&self, plaintext: &[u8], path: &Path) -> Result<(), EncryptionError> { let encrypted = self.encrypt(plaintext)?; fs::write(path, encrypted)?; Ok(()) } /// 从文件读取并解密 pub fn decrypt_from_file(&self, path: &Path) -> Result, EncryptionError> { let encrypted = fs::read(path)?; self.decrypt(&encrypted) } } /// 敏感配置字段标记 #[derive(serde::Serialize, serde::Deserialize)] pub struct SensitiveConfig { /// 连接密码(加密存储) #[serde(skip_serializing_if = "Option::is_none")] pub password_hash: Option, /// API Token(加密存储) #[serde(skip_serializing_if = "Option::is_none")] pub api_token: Option, } #[cfg(test)] mod tests { use super::*; #[test] fn test_encrypt_decrypt() { let encryptor = ConfigEncryptor::new().unwrap(); let plaintext = b"sensitive data"; let encrypted = encryptor.encrypt(plaintext).unwrap(); let decrypted = encryptor.decrypt(&encrypted).unwrap(); assert_eq!(plaintext.to_vec(), decrypted); } #[test] fn test_different_encryption_each_time() { let encryptor = ConfigEncryptor::new().unwrap(); let plaintext = b"same data"; let encrypted1 = encryptor.encrypt(plaintext).unwrap(); let encrypted2 = encryptor.encrypt(plaintext).unwrap(); // 每次加密结果不同(因为随机 salt 和 nonce) assert_ne!(encrypted1, encrypted2); // 但解密结果相同 assert_eq!( encryptor.decrypt(&encrypted1).unwrap(), encryptor.decrypt(&encrypted2).unwrap() ); } } ``` ### 8.1 密码存储与认证 ```rust // crates/agent-client/src/core/security/password.rs use argon2::{Argon2, PasswordHash, PasswordVerifier}; use rand::rngs::OsRng; use rand::RngCore; use std::sync::Arc; use thiserror::Error; /// 密码存储错误 #[derive(Error, Debug)] pub enum PasswordError { #[error("密码哈希失败")] HashError(#[from] argon2::Error), #[error("密码验证失败")] VerificationError, #[error("密码强度不足")] WeakPassword, } /// 密码强度 pub enum PasswordStrength { Weak, // < 8 字符,红色 Medium, // >= 8 字符,无特殊字符,黄色 Strong, // >= 8 字符 + 特殊字符,绿色 } /// 密码管理器 pub struct PasswordManager { /// Argon2 实例 argon2: Arc>, /// 密码盐长度 salt_len: usize, } impl PasswordManager { pub fn new() -> Self { Self { argon2: Arc::new(Argon2::default()), salt_len: 16, } } /// 生成密码盐 fn generate_salt(&self) -> Vec { let mut salt = vec![0u8; self.salt_len]; OsRng.fill_bytes(&mut salt); salt } /// 哈希密码 pub fn hash_password(&self, password: &str) -> Result { let salt = self.generate_salt(); let hash = argon2::Argon2::default() .hash_password_into( password.as_bytes(), &salt, &mut [0u8; 32], // 32 字节输出 ) .map_err(PasswordError::HashError)?; // 格式: $argon2id$v=19$m=65536,t=3,p=4$salt$hash Ok(format!( "${}$v=19$m=65536,t=3,p=4${}${}$hash", argon2::Algorithm::Argon2id.variant_name().unwrap(), base64::encode(&salt), base64::encode(&hash) )) } /// 验证密码 pub fn verify_password(&self, password: &str, hash: &str) -> Result { let parsed_hash = PasswordHash::new(hash) .map_err(PasswordError::HashError)?; self.argon2 .verify_password(password.as_bytes(), &parsed_hash) .map(|_| true) .map_err(|_| PasswordError::VerificationError) } /// 检查密码强度 pub fn check_strength(&self, password: &str) -> PasswordStrength { let has_letter = password.chars().any(|c| c.is_alphabetic()); let has_digit = password.chars().any(|c| c.is_digit(10)); let has_special = password.chars().any(|c| !c.is_alphanumeric()); if password.len() < 8 { PasswordStrength::Weak } else if has_letter && has_digit && has_special { PasswordStrength::Strong } else { PasswordStrength::Medium } } } /// Token 管理器 pub struct TokenManager { /// Token 有效期(秒) const TOKEN_TTL_SECS: u64 = 86400; // 24 小时 /// 刷新提前量(秒) const REFRESH_AHEAD_SECS: u64 = 3600; // 1 小时 /// 密钥(用于 HMAC) secret_key: [u8; 32], /// 已发放的 Token active_tokens: DashMap, } struct TokenData { client_id: String, expires_at: DateTime, } impl TokenManager { pub fn new() -> Self { Self { secret_key: Self::generate_secret_key(), active_tokens: DashMap::new(), } } fn generate_secret_key() -> [u8; 32] { let mut key = [0u8; 32]; OsRng.fill_bytes(&mut key); key } /// 生成 Token pub fn generate_token(&self, client_id: &str) -> String { let now = Utc::now(); let expires_at = now + Duration::seconds(Self::TOKEN_TTL_SECS as i64); let payload = json!({ "client_id": client_id, "exp": expires_at.timestamp(), "iat": now.timestamp(), }); let token = base64::encode(payload.to_string()); let signature = self.sign(&token); format!("{}.{}", token, signature) } /// 签名 fn sign(&self, data: &str) -> String { use hmac::{Hmac, Mac}; type HmacSha256 = Hmac; let mut mac = HmacSha256::new_from_slice(&self.secret_key) .expect("HMAC initialization failed"); mac.update(data.as_bytes()); let result = mac.finalize().into_bytes(); base64::encode(result) } /// 验证 Token pub fn validate_token(&self, token: &str) -> Option<(String, DateTime)> { let parts: Vec<&str> = token.split('.').collect(); if parts.len() != 2 { return None; } let signature = self.sign(parts[0]); if signature != parts[1] { return None; } let payload: serde_json::Value = serde_json::from_str( &base64::decode(parts[0]).ok()?.to_string() ).ok()?; let client_id = payload["client_id"].as_str()?.to_string(); let expires_at = Utc.timestamp_opt(payload["exp"].as_i64()?, 0).single()?; if Utc::now() > expires_at { return None; } Some((client_id, expires_at)) } } ``` ### 8.2 重连机制实现 ```rust // crates/agent-client/src/core/connection/reconnect.rs use tokio::time::{sleep, Duration}; use std::sync::Arc; use tokio::sync::RwLock; /// 重连策略配置 #[derive(Clone)] pub struct ReconnectConfig { /// 初始延迟(毫秒) pub initial_delay_ms: u64, /// 最大延迟(毫秒) pub max_delay_ms: u64, /// 指数退避基数 pub backoff_base: f64, /// 最大重试次数(0 表示无限) pub max_retries: u32, /// 是否随机抖动 pub jitter: bool, } impl Default for ReconnectConfig { fn default() -> Self { Self { initial_delay_ms: 1000, max_delay_ms: 30000, backoff_base: 2.0, max_retries: 0, // 无限重试 jitter: true, } } } /// 重连管理器 pub struct ReconnectManager { config: ReconnectConfig, retry_count: Arc>, last_attempt: Arc>>, } impl ReconnectManager { pub fn new(config: Option) -> Self { Self { config: config.unwrap_or_default(), retry_count: Arc::new(RwLock::new(0)), last_attempt: Arc::new(RwLock::new(Utc::now())), } } /// 计算重连延迟 async fn calculate_delay(&self) -> Duration { let count = *self.retry_count.read().await; let mut delay = (self.config.initial_delay_ms as f64) * (self.config.backoff_base.powi(count as i32)) as u64; // 限制最大延迟 if delay > self.config.max_delay_ms { delay = self.config.max_delay_ms; } // 添加随机抖动 if self.config.jitter { let jitter_range = (delay as f64 * 0.1) as u64; let jitter = rand::thread_rng().gen_range(0..jitter_range); delay += jitter; } Duration::from_millis(delay) } /// 等待重连 pub async fn wait_for_reconnect(&self, mut connect_fn: F) -> bool where F: FnMut() -> Fut, Fut: std::future::Future>, { loop { // 检查是否达到最大重试次数 { let count = *self.retry_count.read().await; if self.config.max_retries > 0 && count >= self.config.max_retries { warn!("已达到最大重试次数 {},停止重连", count); return false; } } // 计算并等待延迟 let delay = self.calculate_delay(); sleep(delay).await; // 更新重试计数 { let mut count = self.retry_count.write().await; *count += 1; *self.last_attempt.write().await = Utc::now(); } info!("尝试重连(第 {} 次)", self.retry_count.read().await); // 尝试连接 match connect_fn().await { Ok(true) => { info!("重连成功"); // 重置重试计数 *self.retry_count.write().await = 0; return true; } Ok(false) | Err(()) => { warn!("重连失败,继续重试..."); } } } } /// 重置重试计数 pub fn reset(&self) { *self.retry_count.write().await = 0; } /// 获取当前重试次数 pub fn retry_count(&self) -> u32 { *self.retry_count.blocking_read() } } /// 离线消息队列 pub struct OfflineMessageQueue { /// 队列容量 capacity: usize, /// 消息保留时间 ttl: Duration, /// 待发送消息 pending: Arc>>, } struct QueuedMessage { /// 消息内容 payload: Vec, /// 消息类型 message_type: u32, /// 入队时间 enqueued_at: DateTime, /// 重试次数 retry_count: u32, } impl OfflineMessageQueue { pub fn new(capacity: usize, ttl_hours: u64) -> Self { Self { capacity, ttl: Duration::hours(ttl_hours as i64), pending: Arc::new(RwLock::new(Vec::new())), } } /// 入队 pub async fn enqueue(&self, message_type: u32, payload: Vec) -> bool { let mut queue = self.pending.write().await; if queue.len() >= self.capacity { warn!("离线消息队列已满,丢弃消息"); return false; } queue.push(QueuedMessage { payload, message_type, enqueued_at: Utc::now(), retry_count: 0, }); true } /// 出队 pub async fn dequeue(&self) -> Option { let mut queue = self.pending.write().await; // 移除过期消息 let now = Utc::now(); queue.retain(|msg| now.signed_duration_since(msg.enqueued_at) < self.ttl); // 按重试次数排序(优先发送重试次数少的) queue.sort_by_key(|msg| msg.retry_count); queue.pop() } /// 增加重试次数 pub async fn increment_retry(&self, index: usize) { let mut queue = self.pending.write().await; if let Some(msg) = queue.get_mut(index) { msg.retry_count += 1; } } /// 清空队列 pub async fn clear(&self) { self.pending.write().await.clear(); } /// 获取队列长度 pub async fn len(&self) -> usize { self.pending.read().await.len() } } ``` ### 8.3 审计日志 ```rust // crates/agent-client/src/core/audit.rs use serde_json::Value; /// 审计日志结构 pub struct AuditLog { pub timestamp: DateTime, pub event_type: AuditEventType, pub client_id: String, pub user_id: String, pub action: String, pub details: Value, pub ip_address: Option, pub result: AuditResult, } pub enum AuditEventType { Connection, Authentication, TaskExecution, FileTransfer, ConfigChange, SecurityEvent, } pub enum AuditResult { Success, Failure(String), } /// 审计日志管理器 pub struct AuditManager { /// 审计日志文件路径 log_path: PathBuf, /// 当前日志文件 current_file: RwLock>, } impl AuditManager { pub fn new(log_dir: &Path) -> Result { let log_path = log_dir.to_path_buf(); std::fs::create_dir_all_all(&log_path)?; Ok(Self { log_path, current_file: RwLock::new(None), }) } /// 记录审计日志 pub async fn log(&self, audit: AuditLog) -> Result<()> { let timestamp = audit.timestamp.to_rfc3339(); let event_type = format!("{:?}", audit.event_type); let result = match &audit.result { AuditResult::Success => "success".to_string(), AuditResult::Failure(e) => format!("failure: {}", e), }; let log_line = format!( r#"{{"timestamp":"{}","event_type":"{}","client_id":"{}","user_id":"{}","action":"{}","details":{},"ip_address":{:?},"result":"{}"}}"#, timestamp, event_type, audit.client_id, audit.user_id, audit.action, audit.details, audit.ip_address, result ); // 写入文件 self.write_to_file(&log_line).await?; // 同时发送到服务器 self.send_to_server(&audit).await?; Ok(()) } async fn write_to_file(&self, line: &str) -> Result<()> { let mut file = self.current_file.write().await; let date = Utc::now().format("%Y-%m-%d").to_string(); let filename = format!("audit_{}.log", date); let filepath = self.log_path.join(&filename); let mut open_options = OpenOptions::new(); open_options.create(true).append(true).write(true); if let Some(ref mut f) = *file { // 检查是否需要切换文件 let current_metadata = f.metadata()?; if current_metadata.len() > 10 * 1024 * 1024 { // 10MB drop(file); let mut new_file = open_options.open(&filepath)?; new_file.write_all(line.as_bytes())?; new_file.write_all(b"\n")?; *file = Some(new_file); return Ok(()); } } else { *file = Some(open_options.open(&filepath)?); } if let Some(ref mut f) = *file { f.write_all(line.as_bytes())?; f.write_all(b"\n")?; } Ok(()) } } /// 敏感信息脱敏 pub struct SensitiveDataRedactor; impl SensitiveDataRedactor { /// 脱敏敏感数据 pub fn redact(data: &str) -> String { let patterns = [ (r"(?i)password\s*[:=]\s*([^\s,}]+)", "password: ******"), (r"(?i)token\s*[:=]\s*([^\s,}]+)", |caps| { let token = caps.get(1).unwrap().as_str(); if token.len() > 8 { format!("token: {}...{}", &token[..4], &token[token.len()-4..]) } else { "token: ******".to_string() } }), (r"(?i)api[_-]?key\s*[:=]\s*([^\s,}]+)", |caps| { let key = caps.get(1).unwrap().as_str(); if key.len() > 8 { format!("api_key: {}...{}", &key[..4], &key[key.len()-4..]) } else { "api_key: ******".to_string() } }), (r"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b", "***.***.***.***"), ]; let mut result = data.to_string(); for pattern in &patterns { result = regex::Regex::new(pattern.0) .unwrap() .replace_all(&result, |caps: ®ex::Captures| { match pattern.1 { regex::Replacer::Static(s) => s.to_string(), regex::Replacer::Fn(f) => f(caps).to_string(), } }) .to_string(); } result } } ``` --- ## 9. 国际化与会话管理 ### 9.1 国际化基础设施 ```rust // crates/agent-client/src/i18n/mod.rs use serde::Deserialize; use std::collections::HashMap; use std::sync::Arc; use tokio::sync::RwLock; /// 支持的语言 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum Language { ZhCN, EnUS, JaJP, } impl Language { pub fn code(&self) -> &'static str { match self { Language::ZhCN => "zh-CN", Language::EnUS => "en-US", Language::JaJP => "ja-JP", } } pub fn from_code(code: &str) -> Option { match code { "zh-CN" | "zh" => Some(Language::ZhCN), "en-US" | "en" => Some(Language::EnUS), "ja-JP" | "ja" => Some(Language::JaJP), _ => None, } } } /// 国际化资源 #[derive(Deserialize, Default)] struct MessageResource { #[serde(flatten)] entries: HashMap, } impl MessageResource { fn get(&self, key: &str) -> Option<&str> { self.entries.get(key).map(|s| s.as_str()) } } /// 国际化管理器 pub struct I18nManager { /// 当前语言 current_lang: RwLock, /// 语言资源缓存 resources: RwLock>, /// 资源文件目录 resource_dir: PathBuf, } impl I18nManager { pub fn new(resource_dir: &Path) -> Self { Self { current_lang: RwLock::new(Language::ZhCN), // 默认中文 resources: RwLock::new(HashMap::new()), resource_dir: resource_dir.to_path_buf(), } } /// 加载语言资源 pub async fn load_language(&self, lang: Language) -> Result<()> { let resource_file = self.resource_dir.join(format!("{}/messages.toml", lang.code())); if !resource_file.exists() { warn!("语言资源文件不存在: {:?}", resource_file); return Ok(()); } let content = tokio::fs::read_to_string(&resource_file).await?; let resource: MessageResource = toml::from_str(&content)?; self.resources.write().await.insert(lang, resource); Ok(()) } /// 设置当前语言 pub async fn set_language(&self, lang: Language) { *self.current_lang.write().await = lang; self.load_language(lang).await.ok(); } /// 获取翻译 pub fn t(&self, key: &str) -> String { let lang = *self.current_lang.blocking_read(); self.resources .blocking_read() .get(&lang) .and_then(|r| r.get(key)) .map(|s| s.to_string()) .unwrap_or_else(|| key.to_string()) } /// 格式化带参数的消息 pub fn t_with_args(&self, key: &str, args: &[(&str, &str)]) -> String { let mut result = self.t(key); for (name, value) in args { result = result.replace(&format!("{{{}}}", name), value); } result } } /// 区域格式化器 pub struct LocaleFormatter { locale: Language, } impl LocaleFormatter { pub fn new(locale: Language) -> Self { Self { locale } } /// 格式化日期 pub fn format_date(&self, date: &DateTime) -> String { match self.locale { Language::ZhCN => date.format("%Y年%m月%d日").to_string(), Language::JaJP => date.format("%Y年%m月%d日").to_string(), _ => date.format("%B %d, %Y").to_string(), } } /// 格式化时间 pub fn format_time(&self, time: &DateTime) -> String { match self.locale { Language::ZhCN | Language::JaJP => time.format("%H:%M:%S").to_string(), _ => time.format("%H:%M:%S").to_string(), } } /// 格式化文件大小 pub fn format_file_size(&self, bytes: u64) -> String { const UNITS: [&str; 5] = ["B", "KB", "MB", "GB", "TB"]; let mut size = bytes as f64; let mut unit_index = 0; while size >= 1024.0 && unit_index < UNITS.len() - 1 { size /= 1024.0; unit_index += 1; } match self.locale { Language::ZhCN => format!("{:.1} {}", size, UNITS[unit_index]), _ => format!("{} {:.1} {}", size, UNITS[unit_index]), } } } ``` ### 9.2 会话管理 ```rust // crates/agent-client/src/core/session.rs /// Agent 任务会话状态 pub enum AgentSessionState { /// 空闲 Idle, /// 等待中 Pending { received_at: DateTime, task_id: String, }, /// 执行中 Active { started_at: DateTime, session_id: String, task_id: String, }, /// 正在停止 Terminating { started_at: DateTime, reason: String, }, /// 出错 Error { error_type: ErrorType, message: String, }, } pub enum ErrorType { Timeout, PermissionDenied, WorkerStopped, Unknown, } /// 会话管理器 pub struct SessionManager { /// 任务超时 task_timeout: Duration, /// 空闲超时 idle_timeout: Duration, /// 最大并发任务数 max_concurrent: usize, /// 活跃会话 active_sessions: DashMap, /// 等待队列 waiting_queue: Arc>>, } impl SessionManager { pub fn new( task_timeout_mins: u64, idle_timeout_mins: u64, max_concurrent: usize, ) -> Self { Self { task_timeout: Duration::minutes(task_timeout_mins as i64), idle_timeout: Duration::minutes(idle_timeout_mins as i64), max_concurrent, active_sessions: DashMap::new(), waiting_queue: Arc::new(RwLock::new(Vec::new())), } } /// 提交任务 pub async fn submit(&self, request: TaskRequest) -> Result { let running_count = self.active_sessions .iter() .filter(|(_, state)| matches!(state, AgentSessionState::Active { .. })) .count(); if running_count >= self.max_concurrent { self.waiting_queue.write().await.push(request); return Ok(SubmissionResult::Queued); } self.start_session(request).await?; Ok(SubmissionResult::Accepted) } /// 开始会话 async fn start_session(&self, request: TaskRequest) -> Result<()> { let session_id = self.generate_session_id(); let state = AgentSessionState::Active { started_at: Utc::now(), session_id: session_id.clone(), task_id: request.task_id.clone(), }; self.active_sessions.insert(session_id.clone(), state); // TODO: 启动实际的任务执行 Ok(()) } fn generate_session_id(&self) -> String { uuid::Uuid::new_v4().to_string() } /// 完成任务 pub fn complete_session(&self, session_id: &str) { self.active_sessions.remove(session_id); // 检查等待队列 let next = self.waiting_queue.write().await.pop(); if let Some(request) = next { tokio::spawn(async move { // self.start_session(request).await?; Ok(()) }); } } /// 检查超时 pub fn check_timeouts(&self) -> Vec { let mut timed_out = Vec::new(); let now = Utc::now(); for (session_id, state) in self.active_sessions.iter() { if let AgentSessionState::Active { started_at, .. } = state { if now.signed_duration_since(*started_at) > self.task_timeout { timed_out.push(session_id.clone()); } } } timed_out } } pub enum SubmissionResult { Accepted, Queued, } pub struct TaskRequest { pub task_id: String, pub prompt: String, pub attachments: Vec, pub timeout: Duration, } ``` --- ## 10. 系统集成 ### 10.1 全局快捷键 ```rust // crates/agent-client/src/system/shortcuts.rs use std::collections::HashMap; use std::sync::Arc; use tokio::sync::RwLock; /// 快捷键定义 pub struct Shortcut { pub modifiers: Vec, pub key_code: KeyCode, } pub enum ModifierKey { Ctrl, Alt, Shift, Cmd, // macOS Command, Windows Meta } pub enum KeyCode { A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z, Function(u8), // F1-F12 Escape, Enter, Space, } /// 全局快捷键管理器 pub struct GlobalShortcutManager { /// 快捷键注册表 shortcuts: Arc>>, /// 平台特定实现 platform_impl: Box, } impl GlobalShortcutManager { pub fn new() -> Result { Ok(Self { shortcuts: Arc::new(RwLock::new(HashMap::new())), platform_impl: create_platform_impl()?, }) } /// 注册快捷键 pub fn register( &self, shortcut: Shortcut, handler: ShortcutHandler, ) -> Result<()> { self.platform_impl.register(&shortcut)?; self.shortcuts.write().await.insert(shortcut, handler); Ok(()) } /// 取消注册 pub fn unregister(&self, shortcut: &Shortcut) -> Result<()> { self.platform_impl.unregister(shortcut)?; self.shortcuts.write().await.remove(shortcut); Ok(()) } } /// 默认快捷键 pub const DEFAULT_SHORTCUTS: &[(Shortcut, &str)] = &[ ( Shortcut { modifiers: vec![ModifierKey::Ctrl, ModifierKey::Alt], key_code: KeyCode::S, }, "show_window", ), ( Shortcut { modifiers: vec![ModifierKey::Ctrl, ModifierKey::Alt], key_code: KeyCode::H, }, "hide_window", ), ( Shortcut { modifiers: vec![ModifierKey::Ctrl, ModifierKey::Alt], key_code: KeyCode::Q, }, "quick_connect", ), ]; ``` ### 10.2 系统通知 ```rust // crates/agent-client/src/system/notification.rs use std::path::PathBuf; /// 系统通知管理器 pub struct SystemNotificationManager { /// 平台特定实现 platform_impl: Box, } impl SystemNotificationManager { pub fn new() -> Result { Ok(Self { platform_impl: create_platform_notification()?, }) } /// 发送通知 pub async fn send(&self, request: NotificationRequest) -> Result { self.platform_impl.send(request).await } /// 取消通知 pub async fn cancel(&self, id: NotificationId) { self.platform_impl.cancel(id).await; } } pub struct NotificationRequest { pub title: String, pub body: String, pub icon: Option, pub urgency: NotificationUrgency, pub timeout: Duration, pub actions: Vec, } pub enum NotificationUrgency { Low, Normal, Critical, } pub struct NotificationAction { pub id: String, pub label: String, } /// 通知 ID 类型 pub struct NotificationId(u32); ``` ### 10.3 系统事件处理 ```rust // crates/agent-client/src/system/events.rs /// 系统事件监听器 #[async_trait::async_trait] pub trait SystemEventListener: Send + Sync { async fn on_shutdown(&self); async fn on_sleep(&self); async fn on_wake(&self); async fn on_screen_lock(&self); async fn on_screen_unlock(&self); async fn on_network_change(&self, status: NetworkStatus); } /// 系统事件管理器 pub struct SystemEventManager { /// 事件监听器 listeners: Arc>>>>, } impl SystemEventManager { pub fn new() -> Self { Self { listeners: Arc::new(RwLock::new(Vec::new())), } } /// 添加监听器 pub fn add_listener(&self, listener: Arc) { self.listeners.write().push(listener); } /// 广播关机事件 pub async fn broadcast_shutdown(&self) { for listener in self.listeners.read().iter() { listener.on_shutdown().await; } } /// 广播睡眠事件 pub async fn broadcast_sleep(&self) { for listener in self.listeners.read().iter() { listener.on_sleep().await; } } /// 广播唤醒事件 pub async fn broadcast_wake(&self) { for listener in self.listeners.read().iter() { listener.on_wake().await; } } } ``` --- ## 11. 验收标准 ### P0 功能(必须实现) | 功能 | 验收标准 | |------|----------| | 跨平台运行 | macOS/Windows/Linux 均可编译运行 | | 系统托盘 | 右下角显示图标,左键显示窗口 | | 开机自启动 | 可在设置中开启/关闭 | | 连接 data-server | 成功连接到 hbbs/hbbr,显示 P2P/Relay 状态 | | 显示客户端 ID | 客户端信息 Tab 显示 8 位 ID | | 显示/修改密码 | 可切换显示/隐藏,支持修改 | | 服务器配置 | 可配置 hbbs/hbbr/api 地址,保存后重连 | | Node.js 自动安装 | 检测系统 Node.js,未安装时自动下载 | | 权限检测 | 显示各权限状态,提供授权入口 | ### P1 功能(应该实现) | 功能 | 验收标准 | |------|----------| | Agent 任务执行 | 接收任务,执行并返回结果 | | 进度实时回传 | 任务执行时实时显示输出 | | 状态栏显示 | 连接状态、Agent 状态、依赖状态 | | 文件传输 | 支持发送/接收文件 | | 日志导出 | 可导出日志文件(脱敏) | | 远程桌面 | 可查看远程桌面画面 | ### P2 功能(可选实现) | 功能 | 验收标准 | |------|----------| | 客户端升级 | 检查更新,下载并安装 | | 聊天界面 | 完整的聊天 UI | | 关于界面 | 版本信息完整,链接可点击 |