diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index d42c188d9..f6d5e2d76 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -6673,6 +6673,7 @@ dependencies = [ "proxycast-infra", "proxycast-providers", "proxycast-services", + "proxycast-terminal", "rand 0.8.5", "regex", "reqwest 0.12.28", @@ -6847,6 +6848,33 @@ dependencies = [ "zip", ] +[[package]] +name = "proxycast-terminal" +version = "0.60.0" +dependencies = [ + "async-trait", + "base64 0.22.1", + "chrono", + "dashmap 5.5.3", + "dirs 5.0.1", + "parking_lot", + "portable-pty", + "proptest", + "proxycast-core", + "rusqlite", + "scopeguard", + "serde", + "serde_json", + "sha2", + "ssh2", + "tempfile", + "thiserror 1.0.69", + "tokio", + "tracing", + "uuid", + "whoami", +] + [[package]] name = "psl-types" version = "2.0.11" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 5e370544f..8a16d5638 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -15,6 +15,7 @@ proxycast-core = { path = "crates/core" } proxycast-infra = { path = "crates/infra" } proxycast-providers = { path = "crates/providers" } proxycast-services = { path = "crates/services" } +proxycast-terminal = { path = "crates/terminal" } voice-core = { path = "crates/voice-core" } # 序列化 @@ -189,6 +190,7 @@ proxycast-core.workspace = true proxycast-infra.workspace = true proxycast-providers.workspace = true proxycast-services.workspace = true +proxycast-terminal.workspace = true voice-core.workspace = true # Tauri diff --git a/src-tauri/crates/terminal/Cargo.toml b/src-tauri/crates/terminal/Cargo.toml new file mode 100644 index 000000000..08ab88a30 --- /dev/null +++ b/src-tauri/crates/terminal/Cargo.toml @@ -0,0 +1,51 @@ +[package] +name = "proxycast-terminal" +version.workspace = true +edition.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true + +[dependencies] +# 项目内 crate +proxycast-core = { path = "../core" } + +# 序列化 +serde.workspace = true +serde_json.workspace = true + +# 异步运行时 +tokio.workspace = true +async-trait.workspace = true + +# 错误处理 +thiserror.workspace = true + +# 日志 +tracing.workspace = true + +# 数据库 +rusqlite.workspace = true + +# 时间和 UUID +chrono.workspace = true +uuid.workspace = true + +# 工具库 +dirs.workspace = true +parking_lot.workspace = true +base64.workspace = true +sha2.workspace = true +dashmap.workspace = true +whoami.workspace = true +scopeguard.workspace = true + +# 终端 +portable-pty.workspace = true + +# SSH +ssh2.workspace = true + +[dev-dependencies] +proptest.workspace = true +tempfile.workspace = true diff --git a/src-tauri/crates/terminal/README.md b/src-tauri/crates/terminal/README.md new file mode 100644 index 000000000..f46082174 --- /dev/null +++ b/src-tauri/crates/terminal/README.md @@ -0,0 +1,41 @@ +# proxycast-terminal + + + +## 架构说明 + +终端核心模块独立 crate,采用**后端预创建 PTY**架构(参考 WaveTerm)。 +通过 `TerminalEventEmit` trait 抽象事件发射,不直接依赖 Tauri。 + +**核心原则:** +- 后端是会话的唯一真相来源 +- PTY 使用默认大小 (24x80) 预创建 +- 前端连接后通过 resize 同步实际大小 +- 统一的 BlockController 抽象层支持多种连接类型 +- 通过 trait 抽象与 Tauri 解耦 + +## Trait 设计(两层抽象) + +- `TerminalEventEmit`:基础 trait(dyn 兼容,不要求 Clone) +- `TerminalEventEmitter`:扩展 trait = `TerminalEventEmit + Clone`(blanket impl) +- `DynEmitter`:`Arc` newtype,自动获得 `TerminalEventEmitter` +- `NoOpEmitter`:空实现,用于测试 + +## 文件索引 + +- `src/lib.rs` - 模块声明和类型重导出 +- `src/emitter.rs` - 事件发射器 trait 定义 +- `src/emit_helper.rs` - 事件发射辅助函数 +- `src/error.rs` - 错误类型定义 +- `src/events.rs` - 事件定义 +- `src/pty_session.rs` - PTY 会话封装 +- `src/session_manager.rs` - 会话管理器 +- `src/tests.rs` - 单元测试(187 个) +- `src/block_controller/` - 块控制器模块 +- `src/connections/` - 连接模块(本地 PTY、SSH、WSL) +- `src/integration/` - 集成模块(OSC 解析、Shell 集成、状态重同步) +- `src/persistence/` - 持久化存储模块 + +## 更新提醒 + +任何文件变更后,请更新此文档和相关的上级文档。 diff --git a/src-tauri/src/terminal/block_controller/mod.rs b/src-tauri/crates/terminal/src/block_controller/mod.rs similarity index 100% rename from src-tauri/src/terminal/block_controller/mod.rs rename to src-tauri/crates/terminal/src/block_controller/mod.rs diff --git a/src-tauri/src/terminal/block_controller/registry.rs b/src-tauri/crates/terminal/src/block_controller/registry.rs similarity index 98% rename from src-tauri/src/terminal/block_controller/registry.rs rename to src-tauri/crates/terminal/src/block_controller/registry.rs index 572243762..a1262f267 100644 --- a/src-tauri/src/terminal/block_controller/registry.rs +++ b/src-tauri/crates/terminal/src/block_controller/registry.rs @@ -129,10 +129,10 @@ impl ControllerRegistry { #[cfg(test)] mod tests { use super::*; - use crate::terminal::block_controller::traits::{ + use crate::block_controller::traits::{ BlockControllerRuntimeStatus, BlockInputUnion, BlockMeta, RuntimeOpts, }; - use crate::terminal::TerminalError; + use crate::error::TerminalError; use async_trait::async_trait; /// 测试用的 Mock 控制器 diff --git a/src-tauri/src/terminal/block_controller/shell_controller.rs b/src-tauri/crates/terminal/src/block_controller/shell_controller.rs similarity index 96% rename from src-tauri/src/terminal/block_controller/shell_controller.rs rename to src-tauri/crates/terminal/src/block_controller/shell_controller.rs index e38d8a3e8..66fcb1715 100644 --- a/src-tauri/src/terminal/block_controller/shell_controller.rs +++ b/src-tauri/crates/terminal/src/block_controller/shell_controller.rs @@ -17,15 +17,16 @@ use std::sync::atomic::{AtomicBool, AtomicI32, Ordering}; use std::sync::Arc; use async_trait::async_trait; -use tauri::Emitter; use tokio::sync::{mpsc, RwLock}; use super::traits::{ BlockController, BlockControllerRuntimeStatus, BlockInputUnion, BlockMeta, RuntimeOpts, }; -use crate::terminal::connections::ShellProc; -use crate::terminal::error::TerminalError; -use crate::terminal::persistence::BlockFile; +use crate::connections::ShellProc; +use crate::emit_helper; +use crate::emitter::TerminalEventEmitter; +use crate::error::TerminalError; +use crate::persistence::BlockFile; /// 控制器状态事件名称 pub const CONTROLLER_STATUS_EVENT: &str = "controller:status"; @@ -67,7 +68,7 @@ impl From for ControllerStatusEvent { /// - `cmd_run_once`: 仅运行一次(不自动重启) /// - `cmd_clear_on_start`: 启动前清空输出 /// - `cmd_close_on_exit`: 退出后自动关闭 -pub struct ShellController { +pub struct ShellController { /// 控制器类型: "shell" | "cmd" controller_type: String, /// Tab ID @@ -85,11 +86,11 @@ pub struct ShellController { /// 连接名称(用于 SSH/WSL) conn_name: RwLock>, /// Shell 进程 - shell_proc: RwLock>, + shell_proc: RwLock>>, /// Shell 输入发送器 shell_input_tx: RwLock>>, - /// Tauri 应用句柄 - app_handle: tauri::AppHandle, + /// 事件发射器 + app_handle: E, /// 块文件存储 block_file: Option>, /// 是否已运行过(用于 cmd:runonce) @@ -98,25 +99,20 @@ pub struct ShellController { current_meta: RwLock>, } -impl ShellController { +impl ShellController { /// 创建新的 ShellController /// /// # 参数 /// - `tab_id`: Tab ID /// - `block_id`: Block ID /// - `controller_type`: 控制器类型 ("shell" | "cmd") - /// - `app_handle`: Tauri 应用句柄 + /// - `app_handle`: 事件发射器 /// /// # 返回 /// 新的 ShellController 实例 /// /// _Requirements: 1.2, 1.3_ - pub fn new( - tab_id: String, - block_id: String, - controller_type: String, - app_handle: tauri::AppHandle, - ) -> Self { + pub fn new(tab_id: String, block_id: String, controller_type: String, app_handle: E) -> Self { tracing::info!( "[ShellController] 创建控制器: block_id={}, type={}", block_id, @@ -147,13 +143,13 @@ impl ShellController { /// - `tab_id`: Tab ID /// - `block_id`: Block ID /// - `controller_type`: 控制器类型 - /// - `app_handle`: Tauri 应用句柄 + /// - `app_handle`: 事件发射器 /// - `block_file`: 块文件存储 pub fn with_block_file( tab_id: String, block_id: String, controller_type: String, - app_handle: tauri::AppHandle, + app_handle: E, block_file: Arc, ) -> Self { let mut controller = Self::new(tab_id, block_id, controller_type, app_handle); @@ -224,7 +220,7 @@ impl ShellController { let status = self.get_runtime_status(); let event = ControllerStatusEvent::from(status); - if let Err(e) = self.app_handle.emit(CONTROLLER_STATUS_EVENT, &event) { + if let Err(e) = emit_helper::emit(&self.app_handle, CONTROLLER_STATUS_EVENT, &event) { tracing::error!( "[ShellController] 发送状态更新事件失败: block_id={}, error={}", self.block_id, @@ -347,7 +343,7 @@ impl ShellController { } #[async_trait] -impl BlockController for ShellController { +impl BlockController for ShellController { /// 启动控制器 /// /// 根据 block_meta 配置启动 Shell 或命令执行进程。 diff --git a/src-tauri/src/terminal/block_controller/traits.rs b/src-tauri/crates/terminal/src/block_controller/traits.rs similarity index 99% rename from src-tauri/src/terminal/block_controller/traits.rs rename to src-tauri/crates/terminal/src/block_controller/traits.rs index 138f2db45..bb42ed3a5 100644 --- a/src-tauri/src/terminal/block_controller/traits.rs +++ b/src-tauri/crates/terminal/src/block_controller/traits.rs @@ -15,7 +15,7 @@ use async_trait::async_trait; use serde::{Deserialize, Serialize}; use std::collections::HashMap; -use crate::terminal::TerminalError; +use crate::error::TerminalError; /// 终端大小 #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] diff --git a/src-tauri/src/terminal/connections/connection_config.rs b/src-tauri/crates/terminal/src/connections/connection_config.rs similarity index 100% rename from src-tauri/src/terminal/connections/connection_config.rs rename to src-tauri/crates/terminal/src/connections/connection_config.rs diff --git a/src-tauri/src/terminal/connections/connection_router.rs b/src-tauri/crates/terminal/src/connections/connection_router.rs similarity index 99% rename from src-tauri/src/terminal/connections/connection_router.rs rename to src-tauri/crates/terminal/src/connections/connection_router.rs index 6a766fdc2..cd3c61985 100644 --- a/src-tauri/src/terminal/connections/connection_router.rs +++ b/src-tauri/crates/terminal/src/connections/connection_router.rs @@ -14,7 +14,7 @@ use serde::{Deserialize, Serialize}; use super::{is_local_conn_name, is_ssh_conn_name, is_wsl_conn_name}; -use crate::terminal::error::TerminalError; +use crate::error::TerminalError; // ============================================================================ // 连接类型枚举 diff --git a/src-tauri/src/terminal/connections/local_pty.rs b/src-tauri/crates/terminal/src/connections/local_pty.rs similarity index 93% rename from src-tauri/src/terminal/connections/local_pty.rs rename to src-tauri/crates/terminal/src/connections/local_pty.rs index 032fe0844..7491d3bcb 100644 --- a/src-tauri/src/terminal/connections/local_pty.rs +++ b/src-tauri/crates/terminal/src/connections/local_pty.rs @@ -27,22 +27,21 @@ use std::sync::Arc; use base64::{engine::general_purpose::STANDARD as BASE64, Engine}; use parking_lot::Mutex; use portable_pty::{native_pty_system, CommandBuilder, PtySize}; -use tauri::Emitter; -use tauri::Manager; use tokio::sync::mpsc; -use crate::terminal::block_controller::{BlockInputUnion, BlockMeta}; -use crate::terminal::error::TerminalError; -use crate::terminal::events::{ - event_names, SessionStatus, TerminalOutputEvent, TerminalStatusEvent, -}; -use crate::terminal::integration::{ShellLaunchBuilder, ShellType}; -use crate::terminal::persistence::BlockFile; +use crate::emit_helper; +use crate::emitter::TerminalEventEmitter; + +use crate::block_controller::{BlockInputUnion, BlockMeta}; +use crate::error::TerminalError; +use crate::events::{event_names, SessionStatus, TerminalOutputEvent, TerminalStatusEvent}; +use crate::integration::{ShellLaunchBuilder, ShellType}; +use crate::persistence::BlockFile; /// Shell 进程封装 /// /// 封装 PTY 进程,提供输入输出和生命周期管理。 -pub struct ShellProc { +pub struct ShellProc { /// Block ID block_id: String, /// 控制器类型 @@ -57,9 +56,11 @@ pub struct ShellProc { exit_code: Arc, /// 是否已退出 exited: Arc, + /// 泛型标记 + _emitter: std::marker::PhantomData, } -impl ShellProc { +impl ShellProc { /// 创建新的 Shell 进程 /// /// # 参数 @@ -67,7 +68,7 @@ impl ShellProc { /// - `controller_type`: 控制器类型 ("shell" | "cmd") /// - `rows`: 终端行数 /// - `cols`: 终端列数 - /// - `app_handle`: Tauri 应用句柄 + /// - `app_handle`: 事件发射器 /// - `block_meta`: 块元数据配置 /// - `input_rx`: 输入接收器 /// - `block_file`: 块文件存储(可选) @@ -82,7 +83,7 @@ impl ShellProc { controller_type: String, rows: u16, cols: u16, - app_handle: tauri::AppHandle, + app_handle: E, block_meta: BlockMeta, input_rx: mpsc::Receiver, block_file: Option>, @@ -165,6 +166,7 @@ impl ShellProc { shutdown_flag, exit_code, exited, + _emitter: std::marker::PhantomData, }) } @@ -175,7 +177,7 @@ impl ShellProc { /// # 参数 /// - `controller_type`: 控制器类型 /// - `block_meta`: 块元数据 - /// - `app_handle`: Tauri 应用句柄 + /// - `app_handle`: 事件发射器 /// - `block_id`: Block ID /// /// # 返回 @@ -185,7 +187,7 @@ impl ShellProc { fn build_command( controller_type: &str, block_meta: &BlockMeta, - app_handle: &tauri::AppHandle, + app_handle: &E, block_id: &str, ) -> Result { let mut cmd = if controller_type == "cmd" { @@ -213,7 +215,7 @@ impl ShellProc { /// _Requirements: 17.5, 17.7, 17.8, 17.9, 17.10_ fn build_shell_command( block_meta: &BlockMeta, - app_handle: &tauri::AppHandle, + app_handle: &E, block_id: &str, ) -> Result { // 获取用户默认 shell @@ -222,7 +224,6 @@ impl ShellProc { // 获取应用数据目录 let app_data_dir = app_handle - .path() .app_data_dir() .map_err(|e| TerminalError::Internal(format!("获取应用数据目录失败: {e}")))?; @@ -299,7 +300,7 @@ impl ShellProc { fn spawn_output_reader( block_id: String, mut reader: Box, - app_handle: tauri::AppHandle, + app_handle: E, shutdown_flag: Arc, exit_code: Arc, exited: Arc, @@ -323,9 +324,10 @@ impl ShellProc { exited.store(true, Ordering::SeqCst); // 发送状态事件 - let _ = app_handle.emit( + let _ = emit_helper::emit( + &app_handle, event_names::TERMINAL_STATUS, - TerminalStatusEvent { + &TerminalStatusEvent { session_id: block_id.clone(), status: SessionStatus::Done, exit_code: Some(exit_code.load(Ordering::SeqCst)), @@ -350,9 +352,10 @@ impl ShellProc { // 发送输出事件 let data = BASE64.encode(output_data); - let _ = app_handle.emit( + let _ = emit_helper::emit( + &app_handle, event_names::TERMINAL_OUTPUT, - TerminalOutputEvent { + &TerminalOutputEvent { session_id: block_id.clone(), data, }, @@ -367,9 +370,10 @@ impl ShellProc { tracing::error!("[ShellProc] 读取错误: block_id={}, error={}", block_id, e); exited.store(true, Ordering::SeqCst); - let _ = app_handle.emit( + let _ = emit_helper::emit( + &app_handle, event_names::TERMINAL_STATUS, - TerminalStatusEvent { + &TerminalStatusEvent { session_id: block_id.clone(), status: SessionStatus::Error, exit_code: None, @@ -545,7 +549,7 @@ impl ShellProc { } } -impl Drop for ShellProc { +impl Drop for ShellProc { fn drop(&mut self) { // 确保关闭标志被设置 self.shutdown_flag.store(true, Ordering::SeqCst); diff --git a/src-tauri/src/terminal/connections/mod.rs b/src-tauri/crates/terminal/src/connections/mod.rs similarity index 86% rename from src-tauri/src/terminal/connections/mod.rs rename to src-tauri/crates/terminal/src/connections/mod.rs index d56a16db5..1177ea3c9 100644 --- a/src-tauri/src/terminal/connections/mod.rs +++ b/src-tauri/crates/terminal/src/connections/mod.rs @@ -9,14 +9,6 @@ //! - `wsl_connection` - WSL 连接(仅 Windows) //! - `connection_router` - 连接类型路由 //! - `connection_config` - 连接配置持久化 -//! -//! ## 功能 -//! - 本地 PTY 进程管理 -//! - SSH 远程连接和认证 -//! - SSH 远程 PTY 创建和数据转发 -//! - WSL 发行版连接 -//! - 连接类型自动路由 -//! - 连接配置存储和管理 pub mod connection_config; pub mod connection_router; diff --git a/src-tauri/src/terminal/connections/ssh_connection.rs b/src-tauri/crates/terminal/src/connections/ssh_connection.rs similarity index 99% rename from src-tauri/src/terminal/connections/ssh_connection.rs rename to src-tauri/crates/terminal/src/connections/ssh_connection.rs index 5a51e282b..f5ef75453 100644 --- a/src-tauri/src/terminal/connections/ssh_connection.rs +++ b/src-tauri/crates/terminal/src/connections/ssh_connection.rs @@ -26,12 +26,15 @@ use std::fmt; use std::net::TcpStream; use std::path::PathBuf; use std::sync::atomic::{AtomicBool, AtomicI32, AtomicI64, Ordering}; +use std::sync::Arc; use parking_lot::RwLock; use serde::{Deserialize, Serialize}; use ssh2::{KeyboardInteractivePrompt as SshKeyboardInteractivePrompt, Session}; -use crate::terminal::error::TerminalError; +use crate::emit_helper; +use crate::emitter::TerminalEventEmit; +use crate::error::TerminalError; /// 默认 SSH 端口 pub const DEFAULT_SSH_PORT: u16 = 22; @@ -574,8 +577,8 @@ pub struct SSHConn { wsh_error: RwLock>, /// 不使用 wsh 的原因 no_wsh_reason: RwLock>, - /// Tauri 应用句柄(用于事件广播) - app_handle: RwLock>, + /// 事件发射器(用于事件广播) + app_handle: RwLock>>, } impl SSHConn { @@ -598,26 +601,25 @@ impl SSHConn { } } - /// 创建带有 Tauri 应用句柄的 SSH 连接管理器 + /// 创建带有事件发射器的 SSH 连接管理器 /// /// 启用事件广播功能。 - pub fn with_app_handle(opts: SSHOpts, app_handle: tauri::AppHandle) -> Self { + pub fn with_app_handle(opts: SSHOpts, app_handle: impl TerminalEventEmit) -> Self { let conn = Self::new(opts); - *conn.app_handle.write() = Some(app_handle); + *conn.app_handle.write() = Some(Arc::new(app_handle)); conn } - /// 设置 Tauri 应用句柄 - pub fn set_app_handle(&self, app_handle: tauri::AppHandle) { - *self.app_handle.write() = Some(app_handle); + /// 设置事件发射器 + pub fn set_app_handle(&self, app_handle: impl TerminalEventEmit) { + *self.app_handle.write() = Some(Arc::new(app_handle)); } /// 广播连接状态变更事件 /// /// _Requirements: 7.3_ fn broadcast_conn_change(&self) { - use crate::terminal::events::{event_names, ConnChangeEvent}; - use tauri::Emitter; + use crate::events::{event_names, ConnChangeEvent}; if let Some(ref app_handle) = *self.app_handle.read() { let status = self.derive_conn_status(); @@ -626,7 +628,8 @@ impl SSHConn { status, }; - if let Err(e) = app_handle.emit(event_names::CONN_CHANGE, event) { + if let Err(e) = emit_helper::emit(app_handle.as_ref(), event_names::CONN_CHANGE, &event) + { tracing::warn!("[SSHConn] 广播连接状态变更事件失败: {}", e); } } diff --git a/src-tauri/src/terminal/connections/ssh_shell_proc.rs b/src-tauri/crates/terminal/src/connections/ssh_shell_proc.rs similarity index 95% rename from src-tauri/src/terminal/connections/ssh_shell_proc.rs rename to src-tauri/crates/terminal/src/connections/ssh_shell_proc.rs index b9b210113..49fc872d6 100644 --- a/src-tauri/src/terminal/connections/ssh_shell_proc.rs +++ b/src-tauri/crates/terminal/src/connections/ssh_shell_proc.rs @@ -21,15 +21,15 @@ use std::sync::Arc; use base64::{engine::general_purpose::STANDARD as BASE64, Engine}; use parking_lot::Mutex; use ssh2::{Channel, Session}; -use tauri::Emitter; use tokio::sync::mpsc; -use crate::terminal::block_controller::{BlockInputUnion, BlockMeta, TermSize}; -use crate::terminal::error::TerminalError; -use crate::terminal::events::{ - event_names, SessionStatus, TerminalOutputEvent, TerminalStatusEvent, -}; -use crate::terminal::persistence::BlockFile; +use crate::emit_helper; +use crate::emitter::TerminalEventEmitter; + +use crate::block_controller::{BlockInputUnion, BlockMeta, TermSize}; +use crate::error::TerminalError; +use crate::events::{event_names, SessionStatus, TerminalOutputEvent, TerminalStatusEvent}; +use crate::persistence::BlockFile; use super::ssh_connection::SSHConn; @@ -80,7 +80,7 @@ impl SSHShellProc { session: &Session, rows: u16, cols: u16, - app_handle: tauri::AppHandle, + app_handle: impl TerminalEventEmitter, block_meta: BlockMeta, input_rx: mpsc::Receiver, block_file: Option>, @@ -190,7 +190,7 @@ impl SSHShellProc { ssh_conn: &SSHConn, rows: u16, cols: u16, - app_handle: tauri::AppHandle, + app_handle: impl TerminalEventEmitter, block_meta: BlockMeta, input_rx: mpsc::Receiver, block_file: Option>, @@ -258,7 +258,7 @@ impl SSHShellProc { fn spawn_output_reader( block_id: String, channel: Arc>, - app_handle: tauri::AppHandle, + app_handle: impl TerminalEventEmitter, shutdown_flag: Arc, exit_code: Arc, exited: Arc, @@ -294,9 +294,10 @@ impl SSHShellProc { ); // 发送状态事件 - let _ = app_handle.emit( + let _ = emit_helper::emit( + &app_handle, event_names::TERMINAL_STATUS, - TerminalStatusEvent { + &TerminalStatusEvent { session_id: block_id.clone(), status: SessionStatus::Done, exit_code: Some(code), @@ -321,9 +322,10 @@ impl SSHShellProc { exit_code.store(code, Ordering::SeqCst); exited.store(true, Ordering::SeqCst); - let _ = app_handle.emit( + let _ = emit_helper::emit( + &app_handle, event_names::TERMINAL_STATUS, - TerminalStatusEvent { + &TerminalStatusEvent { session_id: block_id.clone(), status: SessionStatus::Done, exit_code: Some(code), @@ -353,9 +355,10 @@ impl SSHShellProc { // 发送输出事件 let data = BASE64.encode(output_data); - let _ = app_handle.emit( + let _ = emit_helper::emit( + &app_handle, event_names::TERMINAL_OUTPUT, - TerminalOutputEvent { + &TerminalOutputEvent { session_id: block_id.clone(), data, }, @@ -379,9 +382,10 @@ impl SSHShellProc { ); exited.store(true, Ordering::SeqCst); - let _ = app_handle.emit( + let _ = emit_helper::emit( + &app_handle, event_names::TERMINAL_STATUS, - TerminalStatusEvent { + &TerminalStatusEvent { session_id: block_id.clone(), status: SessionStatus::Error, exit_code: None, diff --git a/src-tauri/src/terminal/connections/wsl_connection.rs b/src-tauri/crates/terminal/src/connections/wsl_connection.rs similarity index 95% rename from src-tauri/src/terminal/connections/wsl_connection.rs rename to src-tauri/crates/terminal/src/connections/wsl_connection.rs index 03a5ebf79..3d04dc994 100644 --- a/src-tauri/src/terminal/connections/wsl_connection.rs +++ b/src-tauri/crates/terminal/src/connections/wsl_connection.rs @@ -25,15 +25,19 @@ use parking_lot::RwLock; use serde::{Deserialize, Serialize}; use tokio::sync::mpsc; -use crate::terminal::block_controller::{BlockInputUnion, BlockMeta}; -use crate::terminal::connections::{ConnStatus, ConnectionState}; -use crate::terminal::error::TerminalError; -use crate::terminal::events::event_names; +use crate::block_controller::{BlockInputUnion, BlockMeta}; +use crate::connections::{ConnStatus, ConnectionState}; +use crate::emit_helper; +use crate::emitter::TerminalEventEmit; #[cfg(target_os = "windows")] -use crate::terminal::events::{TerminalOutputEvent, TerminalStatusEvent}; -use crate::terminal::persistence::BlockFile; +use crate::emitter::TerminalEventEmitter; +use crate::error::TerminalError; +use crate::events::event_names; #[cfg(target_os = "windows")] -use crate::terminal::SessionStatus; +use crate::events::{TerminalOutputEvent, TerminalStatusEvent}; +use crate::persistence::BlockFile; +#[cfg(target_os = "windows")] +use crate::SessionStatus; #[cfg(target_os = "windows")] use base64::engine::general_purpose::STANDARD as BASE64; @@ -286,8 +290,8 @@ pub struct WSLConn { wsh_error: RwLock>, /// 不使用 wsh 的原因 no_wsh_reason: RwLock>, - /// Tauri 应用句柄(用于事件广播) - app_handle: RwLock>, + /// 事件发射器(用于事件广播) + app_handle: RwLock>>, } impl WSLConn { @@ -308,16 +312,16 @@ impl WSLConn { } } - /// 创建带有 Tauri 应用句柄的 WSL 连接管理器 - pub fn with_app_handle(opts: WSLOpts, app_handle: tauri::AppHandle) -> Self { + /// 创建带有事件发射器的 WSL 连接管理器 + pub fn with_app_handle(opts: WSLOpts, app_handle: impl TerminalEventEmit) -> Self { let conn = Self::new(opts); - *conn.app_handle.write() = Some(app_handle); + *conn.app_handle.write() = Some(Arc::new(app_handle)); conn } - /// 设置 Tauri 应用句柄 - pub fn set_app_handle(&self, app_handle: tauri::AppHandle) { - *self.app_handle.write() = Some(app_handle); + /// 设置事件发射器 + pub fn set_app_handle(&self, app_handle: impl TerminalEventEmit) { + *self.app_handle.write() = Some(Arc::new(app_handle)); } /// 从连接字符串创建 @@ -362,8 +366,7 @@ impl WSLConn { /// /// _Requirements: 7.3_ fn broadcast_conn_change(&self) { - use crate::terminal::events::ConnChangeEvent; - use tauri::Emitter; + use crate::events::ConnChangeEvent; if let Some(ref app_handle) = *self.app_handle.read() { let status = self.derive_conn_status(); @@ -372,7 +375,8 @@ impl WSLConn { status, }; - if let Err(e) = app_handle.emit(event_names::CONN_CHANGE, event) { + if let Err(e) = emit_helper::emit(app_handle.as_ref(), event_names::CONN_CHANGE, &event) + { tracing::warn!("[WSLConn] 广播连接状态变更事件失败: {}", e); } } @@ -617,7 +621,7 @@ impl WSLShellProc { opts: WSLOpts, rows: u16, cols: u16, - app_handle: tauri::AppHandle, + app_handle: impl TerminalEventEmitter, block_meta: BlockMeta, input_rx: mpsc::Receiver, block_file: Option>, @@ -712,7 +716,7 @@ impl WSLShellProc { opts: WSLOpts, _rows: u16, _cols: u16, - _app_handle: tauri::AppHandle, + _app_handle: impl TerminalEventEmit, _block_meta: BlockMeta, _input_rx: mpsc::Receiver, _block_file: Option>, @@ -790,14 +794,12 @@ impl WSLShellProc { fn spawn_output_reader( block_id: String, mut reader: Box, - app_handle: tauri::AppHandle, + app_handle: impl TerminalEventEmit, shutdown_flag: Arc, exit_code: Arc, exited: Arc, block_file: Option>, ) { - use tauri::Emitter; - std::thread::spawn(move || { let mut buffer = [0u8; 4096]; @@ -812,9 +814,10 @@ impl WSLShellProc { tracing::info!("[WSLShellProc] WSL 进程已退出: block_id={}", block_id); exited.store(true, Ordering::SeqCst); - let _ = app_handle.emit( + let _ = emit_helper::emit( + &app_handle, event_names::TERMINAL_STATUS, - TerminalStatusEvent { + &TerminalStatusEvent { session_id: block_id.clone(), status: SessionStatus::Done, exit_code: Some(exit_code.load(Ordering::SeqCst)), @@ -837,9 +840,10 @@ impl WSLShellProc { } let data = BASE64.encode(output_data); - let _ = app_handle.emit( + let _ = emit_helper::emit( + &app_handle, event_names::TERMINAL_OUTPUT, - TerminalOutputEvent { + &TerminalOutputEvent { session_id: block_id.clone(), data, }, @@ -857,9 +861,10 @@ impl WSLShellProc { ); exited.store(true, Ordering::SeqCst); - let _ = app_handle.emit( + let _ = emit_helper::emit( + &app_handle, event_names::TERMINAL_STATUS, - TerminalStatusEvent { + &TerminalStatusEvent { session_id: block_id.clone(), status: SessionStatus::Error, exit_code: None, diff --git a/src-tauri/crates/terminal/src/emit_helper.rs b/src-tauri/crates/terminal/src/emit_helper.rs new file mode 100644 index 000000000..1c121f22a --- /dev/null +++ b/src-tauri/crates/terminal/src/emit_helper.rs @@ -0,0 +1,19 @@ +//! 事件发射辅助函数 +//! +//! 提供类型安全的事件发射便捷方法,封装序列化逻辑。 + +use serde::Serialize; + +use crate::emitter::TerminalEventEmit; + +/// 发射序列化事件 +/// +/// 将 payload 序列化为 JSON 后通过 emitter 发射。 +pub fn emit( + emitter: &E, + event: &str, + payload: &T, +) -> Result<(), String> { + let value = serde_json::to_value(payload).map_err(|e| format!("序列化事件数据失败: {e}"))?; + emitter.emit_event(event, &value) +} diff --git a/src-tauri/crates/terminal/src/emitter.rs b/src-tauri/crates/terminal/src/emitter.rs new file mode 100644 index 000000000..81ec1ec65 --- /dev/null +++ b/src-tauri/crates/terminal/src/emitter.rs @@ -0,0 +1,73 @@ +//! 事件发射器抽象 +//! +//! 定义事件发射器 trait 层次,用于将 Tauri 事件发射功能抽象化, +//! 使终端模块不直接依赖 Tauri。 +//! +//! ## 设计 +//! - `TerminalEventEmit`:基础 trait(dyn 兼容,不要求 Clone) +//! - `TerminalEventEmitter`:扩展 trait(要求 Clone,用于泛型参数) +//! - `DynEmitter`:`Arc` 的 newtype,实现 `TerminalEventEmitter` + +use std::path::PathBuf; +use std::sync::Arc; + +/// 基础事件发射 trait(dyn 兼容) +/// +/// 不要求 `Clone`,可以用作 `dyn TerminalEventEmit`。 +/// 主 crate 中为 `tauri::AppHandle` 实现此 trait。 +pub trait TerminalEventEmit: Send + Sync + 'static { + /// 发射事件到前端 + fn emit_event(&self, event: &str, payload: &serde_json::Value) -> Result<(), String>; + + /// 获取应用数据目录 + fn app_data_dir(&self) -> Result; +} + +/// 扩展事件发射器 trait(要求 Clone) +/// +/// 用于泛型参数场景(如 `ShellController`、`ShellProc`)。 +/// 所有实现了 `TerminalEventEmit + Clone` 的类型自动实现此 trait。 +pub trait TerminalEventEmitter: TerminalEventEmit + Clone {} + +/// 自动实现:任何 `TerminalEventEmit + Clone` 的类型都是 `TerminalEventEmitter` +impl TerminalEventEmitter for T {} + +/// 动态事件发射器包装 +/// +/// 使用 `Arc` 包装,实现 `Clone` + `TerminalEventEmit`, +/// 从而自动获得 `TerminalEventEmitter`。 +/// +/// 用于需要存储和传递发射器但不想泛型化的场景(如 `TerminalSessionManager`)。 +#[derive(Clone)] +pub struct DynEmitter(pub Arc); + +impl DynEmitter { + /// 从实现了 TerminalEventEmit 的类型创建 + pub fn new(emitter: impl TerminalEventEmit) -> Self { + Self(Arc::new(emitter)) + } +} + +impl TerminalEventEmit for DynEmitter { + fn emit_event(&self, event: &str, payload: &serde_json::Value) -> Result<(), String> { + self.0.emit_event(event, payload) + } + + fn app_data_dir(&self) -> Result { + self.0.app_data_dir() + } +} + +/// 空事件发射器(用于测试) +#[derive(Debug, Clone)] +pub struct NoOpEmitter; + +impl TerminalEventEmit for NoOpEmitter { + fn emit_event(&self, _event: &str, _payload: &serde_json::Value) -> Result<(), String> { + Ok(()) + } + + fn app_data_dir(&self) -> Result { + Ok(PathBuf::from(".proxycast")) + } +} diff --git a/src-tauri/src/terminal/error.rs b/src-tauri/crates/terminal/src/error.rs similarity index 100% rename from src-tauri/src/terminal/error.rs rename to src-tauri/crates/terminal/src/error.rs diff --git a/src-tauri/src/terminal/events.rs b/src-tauri/crates/terminal/src/events.rs similarity index 97% rename from src-tauri/src/terminal/events.rs rename to src-tauri/crates/terminal/src/events.rs index cc2ae897a..aa7f83779 100644 --- a/src-tauri/src/terminal/events.rs +++ b/src-tauri/crates/terminal/src/events.rs @@ -11,7 +11,7 @@ use serde::{Deserialize, Serialize}; -use crate::terminal::connections::ConnStatus; +use crate::connections::ConnStatus; /// 会话状态 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] diff --git a/src-tauri/src/terminal/integration/mod.rs b/src-tauri/crates/terminal/src/integration/mod.rs similarity index 100% rename from src-tauri/src/terminal/integration/mod.rs rename to src-tauri/crates/terminal/src/integration/mod.rs diff --git a/src-tauri/src/terminal/integration/osc_parser.rs b/src-tauri/crates/terminal/src/integration/osc_parser.rs similarity index 100% rename from src-tauri/src/terminal/integration/osc_parser.rs rename to src-tauri/crates/terminal/src/integration/osc_parser.rs diff --git a/src-tauri/src/terminal/integration/resync.rs b/src-tauri/crates/terminal/src/integration/resync.rs similarity index 94% rename from src-tauri/src/terminal/integration/resync.rs rename to src-tauri/crates/terminal/src/integration/resync.rs index 299587092..69eb049fc 100644 --- a/src-tauri/src/terminal/integration/resync.rs +++ b/src-tauri/crates/terminal/src/integration/resync.rs @@ -19,15 +19,16 @@ use std::sync::Arc; use base64::{engine::general_purpose::STANDARD as BASE64, Engine}; -use tauri::Emitter; -use crate::terminal::block_controller::{ +use crate::block_controller::{ BlockController, BlockControllerRuntimeStatus, BlockMeta, ControllerRegistry, RuntimeOpts, ShellController, }; -use crate::terminal::error::TerminalError; -use crate::terminal::events::{event_names, TerminalOutputEvent}; -use crate::terminal::persistence::BlockFile; +use crate::emit_helper; +use crate::emitter::TerminalEventEmitter; +use crate::error::TerminalError; +use crate::events::{event_names, TerminalOutputEvent}; +use crate::persistence::BlockFile; /// 终端重置序列 /// @@ -134,7 +135,7 @@ impl ResyncController { block_id: &str, block_meta: BlockMeta, rt_opts: Option, - app_handle: tauri::AppHandle, + app_handle: impl TerminalEventEmitter, block_file: Option>, options: ResyncOptions, ) -> Result { @@ -338,7 +339,7 @@ impl ResyncController { controller_name: &str, tab_id: &str, block_id: &str, - app_handle: tauri::AppHandle, + app_handle: impl TerminalEventEmitter, block_file: Option>, ) -> Result, TerminalError> { match controller_name { @@ -378,7 +379,7 @@ impl ResyncController { /// /// _Requirements: 2.3_ fn send_reset_sequence( - app_handle: &tauri::AppHandle, + app_handle: &impl TerminalEventEmitter, block_id: &str, full_reset: bool, ) -> Result<(), TerminalError> { @@ -390,15 +391,15 @@ impl ResyncController { let data = BASE64.encode(reset_data); - app_handle - .emit( - event_names::TERMINAL_OUTPUT, - TerminalOutputEvent { - session_id: block_id.to_string(), - data, - }, - ) - .map_err(|e| TerminalError::Internal(format!("发送重置序列失败: {e}")))?; + emit_helper::emit( + app_handle, + event_names::TERMINAL_OUTPUT, + &TerminalOutputEvent { + session_id: block_id.to_string(), + data, + }, + ) + .map_err(|e| TerminalError::Internal(format!("发送重置序列失败: {e}")))?; tracing::debug!( "[ResyncController] 发送重置序列: block_id={}, full={}", @@ -423,7 +424,7 @@ impl ResyncController { /// /// _Requirements: 2.4_ fn restore_history( - app_handle: &tauri::AppHandle, + app_handle: &impl TerminalEventEmitter, block_id: &str, block_file: &BlockFile, ) -> Result { @@ -440,15 +441,15 @@ impl ResyncController { let data_size = history_data.len(); let data = BASE64.encode(&history_data); - app_handle - .emit( - event_names::TERMINAL_OUTPUT, - TerminalOutputEvent { - session_id: block_id.to_string(), - data, - }, - ) - .map_err(|e| TerminalError::Internal(format!("发送历史数据失败: {e}")))?; + emit_helper::emit( + app_handle, + event_names::TERMINAL_OUTPUT, + &TerminalOutputEvent { + session_id: block_id.to_string(), + data, + }, + ) + .map_err(|e| TerminalError::Internal(format!("发送历史数据失败: {e}")))?; tracing::info!( "[ResyncController] 恢复历史数据: block_id={}, size={} bytes", @@ -556,7 +557,7 @@ pub async fn resync_controller( block_id: &str, block_meta: BlockMeta, rt_opts: Option, - app_handle: tauri::AppHandle, + app_handle: impl TerminalEventEmitter, block_file: Option>, force: bool, ) -> Result { diff --git a/src-tauri/src/terminal/integration/shell_integration.rs b/src-tauri/crates/terminal/src/integration/shell_integration.rs similarity index 96% rename from src-tauri/src/terminal/integration/shell_integration.rs rename to src-tauri/crates/terminal/src/integration/shell_integration.rs index bfac6e7f6..8e7f10bf7 100644 --- a/src-tauri/src/terminal/integration/shell_integration.rs +++ b/src-tauri/crates/terminal/src/integration/shell_integration.rs @@ -22,11 +22,12 @@ use std::sync::RwLock; use std::time::{SystemTime, UNIX_EPOCH}; use serde::{Deserialize, Serialize}; -use tauri::Emitter; use super::osc_parser::{OSCParser, OSCSequence, PromptMarkType}; -use crate::terminal::error::TerminalError; -use crate::terminal::events::event_names; +use crate::emit_helper; +use crate::emitter::TerminalEventEmit; +use crate::error::TerminalError; +use crate::events::event_names; /// Shell 类型 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] @@ -161,15 +162,12 @@ pub struct ShellIntegration { current_command: RwLock>, /// 上次命令开始时间 last_command_start: AtomicI64, - /// Tauri 应用句柄(可选) - app_handle: Option, + /// 事件发射器(可选) + app_handle: Option>, } impl ShellIntegration { /// 创建新的 Shell 集成处理器 - /// - /// # 参数 - /// - `block_id`: Block ID pub fn new(block_id: String) -> Self { Self { block_id, @@ -182,12 +180,8 @@ impl ShellIntegration { } } - /// 创建带有 Tauri 应用句柄的 Shell 集成处理器 - /// - /// # 参数 - /// - `block_id`: Block ID - /// - `app_handle`: Tauri 应用句柄 - pub fn with_app_handle(block_id: String, app_handle: tauri::AppHandle) -> Self { + /// 创建带有事件发射器的 Shell 集成处理器 + pub fn with_app_handle(block_id: String, app_handle: impl TerminalEventEmit) -> Self { Self { block_id, shell_type: RwLock::new(ShellType::Unknown), @@ -195,7 +189,7 @@ impl ShellIntegration { status: RwLock::new(ShellIntegrationStatus::Unknown), current_command: RwLock::new(None), last_command_start: AtomicI64::new(0), - app_handle: Some(app_handle), + app_handle: Some(std::sync::Arc::new(app_handle)), } } @@ -343,9 +337,10 @@ impl ShellIntegration { // 发送剪贴板事件到前端 if let Some(ref app_handle) = self.app_handle { - let _ = app_handle.emit( + let _ = emit_helper::emit( + app_handle.as_ref(), event_names::CLIPBOARD_WRITE, - serde_json::json!({ + &serde_json::json!({ "block_id": self.block_id, "selection": selection, "content": content, @@ -497,7 +492,11 @@ impl ShellIntegration { command_info, }; - if let Err(e) = app_handle.emit(event_names::SHELL_INTEGRATION_STATUS, &event) { + if let Err(e) = emit_helper::emit( + app_handle.as_ref(), + event_names::SHELL_INTEGRATION_STATUS, + &event, + ) { tracing::warn!( "[ShellIntegration] 发送状态事件失败: block_id={}, error={}", self.block_id, diff --git a/src-tauri/src/terminal/integration/shell_scripts.rs b/src-tauri/crates/terminal/src/integration/shell_scripts.rs similarity index 99% rename from src-tauri/src/terminal/integration/shell_scripts.rs rename to src-tauri/crates/terminal/src/integration/shell_scripts.rs index 9d29eba2d..2a84eb9f9 100644 --- a/src-tauri/src/terminal/integration/shell_scripts.rs +++ b/src-tauri/crates/terminal/src/integration/shell_scripts.rs @@ -25,8 +25,8 @@ use std::collections::HashMap; use std::fs; use std::path::{Path, PathBuf}; -use crate::terminal::error::TerminalError; -use crate::terminal::integration::shell_integration::ShellType; +use crate::error::TerminalError; +use crate::integration::shell_integration::ShellType; /// Shell 集成脚本目录名 const SHELL_INTEGRATION_DIR: &str = "shell-integration"; diff --git a/src-tauri/crates/terminal/src/lib.rs b/src-tauri/crates/terminal/src/lib.rs new file mode 100644 index 000000000..9aacab122 --- /dev/null +++ b/src-tauri/crates/terminal/src/lib.rs @@ -0,0 +1,60 @@ +//! ProxyCast 终端模块 +//! +//! 提供 PTY 管理和会话管理能力,独立于 Tauri 框架。 +//! +//! ## 模块结构 +//! - `emitter` - 事件发射器抽象 trait +//! - `emit_helper` - 事件发射辅助函数 +//! - `error` - 错误类型定义 +//! - `events` - 事件定义 +//! - `pty_session` - PTY 会话封装 +//! - `session_manager` - 会话管理器 +//! - `persistence` - 持久化存储(块文件、会话元数据) +//! - `block_controller` - 块控制器抽象层 +//! - `connections` - 连接模块(本地 PTY、SSH、WSL) +//! - `integration` - 集成模块(Shell 集成、OSC 解析、状态重同步) + +// 核心抽象 +pub mod emit_helper; +pub mod emitter; + +// 基础类型 +pub mod error; +pub mod events; + +// 会话管理 +pub mod pty_session; +pub mod session_manager; + +// 持久化 +pub mod persistence; + +// 块控制器 +pub mod block_controller; + +// 连接 +pub mod connections; + +// 集成 +pub mod integration; + +#[cfg(test)] +mod tests; + +// 重新导出常用类型 +pub use emitter::{DynEmitter, NoOpEmitter, TerminalEventEmitter}; +pub use error::TerminalError; +pub use events::{SessionStatus, TerminalOutputEvent, TerminalStatusEvent}; +pub use pty_session::{PtySession, DEFAULT_COLS, DEFAULT_ROWS}; +pub use session_manager::{SessionMetadata, TerminalSessionManager}; + +pub use block_controller::{ + BlockController, BlockControllerRuntimeStatus, BlockInputUnion, BlockMeta, ControllerRegistry, + ControllerStatusEvent, RuntimeOpts, ShellController, TermSize, CONTROLLER_STATUS_EVENT, +}; +pub use connections::ShellProc; +pub use integration::{ + resync_controller, ResyncController, ResyncOptions, ResyncResult, TERMINAL_RESET_SEQUENCE, + TERMINAL_SOFT_RESET_SEQUENCE, +}; +pub use persistence::{BlockFile, SessionMetadataStore, SessionRecord}; diff --git a/src-tauri/src/terminal/persistence/block_file.rs b/src-tauri/crates/terminal/src/persistence/block_file.rs similarity index 99% rename from src-tauri/src/terminal/persistence/block_file.rs rename to src-tauri/crates/terminal/src/persistence/block_file.rs index 2c2347b6b..3ba19b9a1 100644 --- a/src-tauri/src/terminal/persistence/block_file.rs +++ b/src-tauri/crates/terminal/src/persistence/block_file.rs @@ -20,7 +20,7 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use parking_lot::RwLock; -use crate::terminal::error::TerminalError; +use crate::error::TerminalError; /// 默认终端块文件最大大小 (256KB) pub const DEFAULT_TERM_MAX_FILE_SIZE: usize = 256 * 1024; diff --git a/src-tauri/src/terminal/persistence/mod.rs b/src-tauri/crates/terminal/src/persistence/mod.rs similarity index 100% rename from src-tauri/src/terminal/persistence/mod.rs rename to src-tauri/crates/terminal/src/persistence/mod.rs diff --git a/src-tauri/src/terminal/persistence/session_store.rs b/src-tauri/crates/terminal/src/persistence/session_store.rs similarity index 99% rename from src-tauri/src/terminal/persistence/session_store.rs rename to src-tauri/crates/terminal/src/persistence/session_store.rs index b59ebbde7..df8bfcbad 100644 --- a/src-tauri/src/terminal/persistence/session_store.rs +++ b/src-tauri/crates/terminal/src/persistence/session_store.rs @@ -13,8 +13,8 @@ use chrono::Utc; use rusqlite::{params, OptionalExtension}; use serde::{Deserialize, Serialize}; -use crate::database::DbConnection; -use crate::terminal::error::TerminalError; +use crate::error::TerminalError; +use proxycast_core::database::DbConnection; /// 会话记录(存储在 SQLite) /// diff --git a/src-tauri/src/terminal/pty_session.rs b/src-tauri/crates/terminal/src/pty_session.rs similarity index 93% rename from src-tauri/src/terminal/pty_session.rs rename to src-tauri/crates/terminal/src/pty_session.rs index 8e71ab8a2..cf1a73c8a 100644 --- a/src-tauri/src/terminal/pty_session.rs +++ b/src-tauri/crates/terminal/src/pty_session.rs @@ -20,11 +20,12 @@ use std::sync::Arc; use base64::{engine::general_purpose::STANDARD as BASE64, Engine}; use parking_lot::Mutex; use portable_pty::{native_pty_system, CommandBuilder, PtySize}; -use tauri::Emitter; use tokio::sync::RwLock; -use super::error::TerminalError; -use super::events::{event_names, SessionStatus, TerminalOutputEvent, TerminalStatusEvent}; +use crate::emit_helper; +use crate::emitter::TerminalEventEmit; +use crate::error::TerminalError; +use crate::events::{event_names, SessionStatus, TerminalOutputEvent, TerminalStatusEvent}; /// 默认终端行数 pub const DEFAULT_ROWS: u16 = 24; @@ -99,7 +100,7 @@ impl PtySession { /// # 返回 /// - `Ok(PtySession)`: 创建成功 /// - `Err(TerminalError)`: 创建失败 - pub fn new(id: String, app_handle: tauri::AppHandle) -> Result { + pub fn new(id: String, app_handle: impl TerminalEventEmit) -> Result { Self::with_size(id, DEFAULT_ROWS, DEFAULT_COLS, app_handle) } @@ -118,7 +119,7 @@ impl PtySession { id: String, rows: u16, cols: u16, - app_handle: tauri::AppHandle, + app_handle: impl TerminalEventEmit, ) -> Result { Self::with_size_and_cwd(id, rows, cols, None, app_handle) } @@ -140,7 +141,7 @@ impl PtySession { rows: u16, cols: u16, cwd: Option, - app_handle: tauri::AppHandle, + app_handle: impl TerminalEventEmit, ) -> Result { tracing::info!( "[终端] 创建 PTY 会话 {}, 大小: {}x{}, cwd: {:?}", @@ -255,9 +256,10 @@ impl PtySession { }); // 发送状态事件 - let _ = app_handle.emit( + let _ = emit_helper::emit( + &app_handle, event_names::TERMINAL_STATUS, - TerminalStatusEvent { + &TerminalStatusEvent { session_id: id_clone.clone(), status: SessionStatus::Done, exit_code: Some(0), @@ -274,9 +276,10 @@ impl PtySession { // 发送输出事件 let data = BASE64.encode(output_data); - let _ = app_handle.emit( + let _ = emit_helper::emit( + &app_handle, event_names::TERMINAL_OUTPUT, - TerminalOutputEvent { + &TerminalOutputEvent { session_id: id_clone.clone(), data, }, @@ -293,9 +296,10 @@ impl PtySession { *status_clone.write().await = SessionStatus::Error; }); - let _ = app_handle.emit( + let _ = emit_helper::emit( + &app_handle, event_names::TERMINAL_STATUS, - TerminalStatusEvent { + &TerminalStatusEvent { session_id: id_clone.clone(), status: SessionStatus::Error, exit_code: None, diff --git a/src-tauri/src/terminal/session_manager.rs b/src-tauri/crates/terminal/src/session_manager.rs similarity index 95% rename from src-tauri/src/terminal/session_manager.rs rename to src-tauri/crates/terminal/src/session_manager.rs index 7b6fec9f8..9a08b2ae3 100644 --- a/src-tauri/src/terminal/session_manager.rs +++ b/src-tauri/crates/terminal/src/session_manager.rs @@ -26,13 +26,14 @@ use serde::{Deserialize, Serialize}; use tokio::sync::RwLock; use uuid::Uuid; -use crate::database::DbConnection; +use proxycast_core::database::DbConnection; -use super::block_controller::ControllerRegistry; -use super::error::TerminalError; -use super::events::SessionStatus; -use super::persistence::{BlockFile, SessionMetadataStore, SessionRecord}; -use super::pty_session::{PtySession, DEFAULT_COLS, DEFAULT_ROWS}; +use crate::block_controller::ControllerRegistry; +use crate::emitter::{DynEmitter, TerminalEventEmit}; +use crate::error::TerminalError; +use crate::events::SessionStatus; +use crate::persistence::{BlockFile, SessionMetadataStore, SessionRecord}; +use crate::pty_session::{PtySession, DEFAULT_COLS, DEFAULT_ROWS}; /// 会话元数据(用于前端展示) #[derive(Debug, Clone, Serialize, Deserialize)] @@ -114,16 +115,16 @@ pub struct TerminalSessionManager { session_store: Option>, /// 块文件基础目录 block_file_base_dir: PathBuf, - /// Tauri 应用句柄 - app_handle: tauri::AppHandle, + /// Tauri 应用句柄(抽象为事件发射器) + app_handle: DynEmitter, } impl TerminalSessionManager { /// 创建新的会话管理器 /// /// # 参数 - /// - `app_handle`: Tauri 应用句柄 - pub fn new(app_handle: tauri::AppHandle) -> Self { + /// - `app_handle`: 事件发射器(实现 TerminalEventEmit trait) + pub fn new(app_handle: impl TerminalEventEmit) -> Self { let block_file_base_dir = BlockFile::default_base_dir() .unwrap_or_else(|_| PathBuf::from(".proxycast/terminal_blocks")); @@ -137,19 +138,19 @@ impl TerminalSessionManager { controller_registry: Arc::new(ControllerRegistry::new()), session_store: None, block_file_base_dir, - app_handle, + app_handle: DynEmitter::new(app_handle), } } /// 创建带数据库连接的会话管理器 /// /// # 参数 - /// - `app_handle`: Tauri 应用句柄 + /// - `app_handle`: 事件发射器(实现 TerminalEventEmit trait) /// - `db`: 数据库连接 /// /// _Requirements: 3.5_ pub fn with_database( - app_handle: tauri::AppHandle, + app_handle: impl TerminalEventEmit, db: DbConnection, ) -> Result { let mut manager = Self::new(app_handle); diff --git a/src-tauri/src/terminal/tests.rs b/src-tauri/crates/terminal/src/tests.rs similarity index 100% rename from src-tauri/src/terminal/tests.rs rename to src-tauri/crates/terminal/src/tests.rs diff --git a/src-tauri/src/app/runner.rs b/src-tauri/src/app/runner.rs index f18551a5b..48fe1cf44 100644 --- a/src-tauri/src/app/runner.rs +++ b/src-tauri/src/app/runner.rs @@ -338,7 +338,7 @@ pub fn run() { // 初始化终端会话管理器 { let app_handle = app.handle().clone(); - let terminal_manager = crate::terminal::TerminalSessionManager::new(app_handle.clone()); + let terminal_manager = crate::terminal::TerminalSessionManager::new(crate::terminal::TauriEmitter(app_handle.clone())); if let Some(state) = app_handle.try_state::() { let mut guard = state.inner().0.blocking_write(); *guard = Some(terminal_manager); diff --git a/src-tauri/src/terminal/README.md b/src-tauri/src/terminal/README.md index d2fd18f85..fc0c51082 100644 --- a/src-tauri/src/terminal/README.md +++ b/src-tauri/src/terminal/README.md @@ -1,90 +1,22 @@ -# terminal +# terminal(重导出层) ## 架构说明 -终端核心模块,采用**后端预创建 PTY**架构(参考 WaveTerm)。 +本目录是终端模块的**重导出层**,实际实现已迁移至独立 crate `proxycast-terminal`(位于 `crates/terminal/`)。 -**核心原则:** -- 后端是会话的唯一真相来源 -- PTY 使用默认大小 (24x80) 预创建 -- 前端连接后通过 resize 同步实际大小 -- 通过 Tauri Commands 和 Events 暴露给前端 -- 统一的 BlockController 抽象层支持多种连接类型 - -## 核心功能 - -- **PTY 管理**: 创建和管理伪终端进程(默认大小预创建) -- **会话管理**: 多会话支持,生命周期管理 -- **实时输出**: 通过 Tauri Events 推送终端输出 -- **状态通知**: 会话状态变化事件 -- **持久化存储**: 块文件循环缓冲存储、会话元数据 SQLite 存储 -- **块控制器**: 统一的控制器抽象层(Shell、Cmd、SSH、WSL) -- **连接管理**: 本地 PTY、SSH、WSL 连接支持 -- **Shell 集成**: OSC 序列解析、状态重同步、命令跟踪 +本层职责: +- 提供 `TauriEmitter` newtype,桥接 Tauri `AppHandle` 与终端 crate 的 `TerminalEventEmit` trait +- 重导出 `proxycast-terminal` 的所有公共模块和类型,保持 `crate::terminal::xxx` 路径兼容 ## 文件索引 -- `mod.rs` - 模块入口和类型导出 -- `error.rs` - 错误类型定义 -- `events.rs` - Tauri 事件定义(terminal:output, terminal:status, terminal:shell-integration) -- `pty_session.rs` - PTY 会话封装(支持默认大小创建) -- `session_manager.rs` - 会话管理器 -- `tests.rs` - 单元测试 -- `block_controller/` - 块控制器模块 - - `mod.rs` - 模块入口 - - `traits.rs` - BlockController trait 定义 - - `registry.rs` - 控制器注册表 - - `shell_controller.rs` - Shell/Cmd 控制器实现 -- `connections/` - 连接模块 - - `mod.rs` - 模块入口 - - `local_pty.rs` - 本地 PTY 连接(ShellProc) - - `ssh_connection.rs` - SSH 远程连接(待实现) - - `wsl_connection.rs` - WSL 连接(待实现) -- `integration/` - 集成模块 - - `mod.rs` - 模块入口 - - `resync.rs` - 状态重同步控制器 - - `osc_parser.rs` - OSC 序列解析器(OSC 7/52/133/16162) - - `shell_integration.rs` - Shell 集成处理器(状态管理、命令跟踪) -- `persistence/` - 持久化存储模块 - - `mod.rs` - 模块入口 - - `block_file.rs` - 块文件循环缓冲存储 - - `session_store.rs` - 会话元数据 SQLite 存储 +- `mod.rs` - TauriEmitter 定义 + proxycast-terminal 重导出 -## 命令接口 +## 实际实现 -| 命令 | 描述 | 参数 | -|------|------|------| -| `terminal_create_session` | 创建终端会话(默认大小) | 无 | -| `terminal_write` | 向终端发送输入 | `session_id`, `data` | -| `terminal_resize` | 调整终端大小 | `session_id`, `rows`, `cols` | -| `terminal_close` | 关闭终端会话 | `session_id` | -| `terminal_list_sessions` | 获取所有会话列表 | 无 | -| `terminal_get_session` | 获取单个会话信息 | `session_id` | - -## 事件定义 - -| 事件名 | 描述 | 数据结构 | -|--------|------|----------| -| `terminal:output` | 终端输出数据 | `{ session_id, data }` | -| `terminal:status` | 会话状态变化 | `{ session_id, status, exit_code?, error? }` | -| `terminal:shell-integration` | Shell 集成状态变化 | `{ block_id, status, current_dir?, command_info? }` | -| `terminal:clipboard-write` | 剪贴板写入请求 | `{ block_id, selection, content }` | -| `controller:status` | 控制器状态变化 | `{ block_id, version, shell_proc_status, ... }` | - -## 常量 - -- `DEFAULT_ROWS`: 默认终端行数 (24) -- `DEFAULT_COLS`: 默认终端列数 (80) -- `DEFAULT_TERM_MAX_FILE_SIZE`: 默认块文件最大大小 (256KB) -- `TERMINAL_RESET_SEQUENCE`: 终端完全重置序列 -- `TERMINAL_SOFT_RESET_SEQUENCE`: 终端软重置序列 - -## 依赖 - -- `portable-pty` - 跨平台 PTY 支持 -- `rusqlite` - SQLite 数据库支持 +详见 `crates/terminal/` 目录及其 README。 ## 更新提醒 diff --git a/src-tauri/src/terminal/block_controller/README.md b/src-tauri/src/terminal/block_controller/README.md deleted file mode 100644 index 56f59968c..000000000 --- a/src-tauri/src/terminal/block_controller/README.md +++ /dev/null @@ -1,123 +0,0 @@ -# block_controller - - - -## 架构说明 - -块控制器模块,提供统一的控制器抽象层,支持不同类型的终端连接。 - -**核心原则:** -- 所有控制器类型实现统一的 BlockController trait -- 通过注册表管理控制器生命周期 -- 支持 Shell、Cmd、SSH、WSL 等多种控制器类型 - -## 核心功能 - -- **BlockController trait**: 统一的控制器接口(start、stop、send_input、get_runtime_status) -- **ShellController**: Shell/Cmd 控制器实现,管理本地和远程 Shell 进程 -- **控制器注册表**: 按 block_id 管理控制器实例 -- **运行时状态**: 提供控制器状态查询 -- **状态事件广播**: 通过 Tauri 事件系统广播状态更新 - -## 文件索引 - -- `mod.rs` - 模块入口和类型导出 -- `traits.rs` - BlockController trait 定义、BlockControllerRuntimeStatus、BlockInputUnion、BlockMeta -- `registry.rs` - 控制器注册表(HashMap + RwLock) -- `shell_controller.rs` - ShellController 实现,支持 shell 和 cmd 两种模式 - -## 数据结构 - -### BlockControllerRuntimeStatus - -```rust -pub struct BlockControllerRuntimeStatus { - pub block_id: String, - pub version: i32, - pub shell_proc_status: String, // "init" | "running" | "done" - pub shell_proc_conn_name: Option, - pub shell_proc_exit_code: i32, -} -``` - -### BlockInputUnion - -```rust -pub struct BlockInputUnion { - pub input_data: Option>, - pub sig_name: Option, - pub term_size: Option, -} -``` - -### BlockMeta - -```rust -pub struct BlockMeta { - pub controller: Option, // "shell" | "cmd" - pub connection: Option, // SSH/WSL 连接名称 - pub cmd: Option, // 命令字符串 - pub cmd_args: Option>, // 命令参数 - pub cmd_cwd: Option, // 工作目录 - pub cmd_env: Option>, // 环境变量 - pub cmd_run_on_start: Option, // 启动时自动运行 - pub cmd_run_once: Option, // 仅运行一次 - pub cmd_clear_on_start: Option, // 启动前清空输出 - pub cmd_close_on_exit: Option, // 退出后自动关闭 - // ... 其他终端配置 -} -``` - -## ShellController 功能 - -### 支持的控制器类型 - -- **shell**: 交互式 Shell 模式,启动用户默认 Shell -- **cmd**: 命令执行模式,执行指定命令 - -### Cmd 模式配置选项 - -- `cmd_run_on_start`: 启动时自动运行命令(默认 true) -- `cmd_run_once`: 仅运行一次,不自动重启 -- `cmd_clear_on_start`: 启动前清空输出历史 -- `cmd_close_on_exit`: 命令退出后自动关闭 - -### 状态事件 - -通过 `controller:status` 事件广播状态更新: - -```rust -pub struct ControllerStatusEvent { - pub block_id: String, - pub version: i32, - pub shell_proc_status: String, - pub shell_proc_conn_name: Option, - pub shell_proc_exit_code: i32, -} -``` - -## 依赖 - -- `async-trait` - 异步 trait 支持 -- `tokio` - 异步运行时(RwLock、mpsc) -- `serde` - 序列化支持 -- `scopeguard` - 作用域守卫 -- `tauri` - 事件系统 - -## Requirements 覆盖 - -- 1.1: BlockController trait 定义 -- 1.2: Shell 控制器创建 -- 1.3: Cmd 控制器创建 -- 1.6: 控制器注册表 -- 1.8: get_runtime_status 方法 -- 2.7: 状态事件广播 -- 16.5: cmd:runonstart 配置 -- 16.6: cmd:runonce 配置 -- 16.7: cmd:clearonstart 配置 -- 16.8: cmd:closeonexit 配置 -- 16.9: 重启按钮支持 - -## 更新提醒 - -任何文件变更后,请更新此文档和相关的上级文档。 diff --git a/src-tauri/src/terminal/connections/README.md b/src-tauri/src/terminal/connections/README.md deleted file mode 100644 index 81ed177e5..000000000 --- a/src-tauri/src/terminal/connections/README.md +++ /dev/null @@ -1,356 +0,0 @@ -# connections - - - -## 架构说明 - -连接模块,提供不同类型的终端连接实现。 - -**核心原则:** -- 封装 PTY 进程管理 -- 支持本地、SSH、WSL 多种连接类型 -- 异步输入输出处理 - -## 核心功能 - -- **ShellProc**: 本地 PTY 进程封装,支持 shell 和 cmd 模式 -- **SSHConn**: SSH 远程连接管理器,支持多种认证方式 -- **SSHShellProc**: SSH 远程 Shell 进程封装,支持远程 PTY 创建和数据转发 -- **WSLConn**: WSL 连接管理器(仅 Windows),支持发行版列表和 PTY 创建 -- **输出读取**: 异步读取 PTY 输出并通过 Tauri 事件推送 -- **输入处理**: 处理键盘输入、信号和终端大小调整 -- **块文件集成**: 自动保存输出到块文件 - -## 文件索引 - -- `mod.rs` - 模块入口和类型导出 -- `local_pty.rs` - 本地 PTY 连接实现(ShellProc) -- `ssh_connection.rs` - SSH 远程连接实现 -- `ssh_shell_proc.rs` - SSH 远程 Shell 进程实现 -- `wsl_connection.rs` - WSL 连接实现(仅 Windows) -- `connection_router.rs` - 连接类型路由和工厂模式 - -## ShellProc 功能 - -### 创建进程 - -```rust -let shell_proc = ShellProc::new( - block_id, - controller_type, // "shell" | "cmd" - rows, - cols, - app_handle, - block_meta, - input_rx, - block_file, -).await?; -``` - -### 支持的命令模式 - -- **shell 模式**: 启动用户默认 Shell($SHELL 或 /bin/bash) -- **cmd 模式**: 执行指定命令,支持参数和环境变量 - -### 环境变量 - -自动设置以下环境变量: -- `TERM=xterm-256color` -- `COLORTERM=truecolor` -- 自定义环境变量(通过 `cmd_env`) - -### 进程控制 - -- `terminate()`: 优雅终止(SIGTERM) -- `kill()`: 强制终止(SIGKILL) -- `resize()`: 调整终端大小 -- `write()`: 写入数据到 PTY - -## SSH 连接功能 - -### 连接字符串解析 - -```rust -// 支持多种格式 -let opts = SSHOpts::parse("user@host:port")?; -let opts = SSHOpts::parse("ssh://user@host")?; -let opts = SSHOpts::parse("[::1]:22")?; // IPv6 -``` - -### 连接状态管理 - -```rust -let conn = SSHConn::new(opts); -conn.connect(&conn_flags).await?; -conn.authenticate(&auth_methods).await?; -let status = conn.derive_conn_status(); -``` - -### 支持的认证方式 - -- 公钥认证(密钥文件) -- SSH Agent 认证 -- 密码认证 -- 键盘交互认证(待完善) - -### SSH 配置文件解析 - -支持从 `~/.ssh/config` 读取连接配置: - -```rust -// 获取主机配置(自动合并通配符配置) -let config = SSHConfigParser::get_host_config("myserver")?; - -// 从指定文件获取配置 -let config = SSHConfigParser::get_host_config_from_file(&path, "myserver")?; - -// 解析配置内容 -let hosts = SSHConfigParser::parse_config_content(content)?; -``` - -#### 支持的配置选项 - -- **基础选项**: HostName, User, Port, IdentityFile -- **认证选项**: PubkeyAuthentication, PasswordAuthentication, KbdInteractiveAuthentication, PreferredAuthentications -- **连接选项**: ConnectTimeout, ServerAliveInterval, ServerAliveCountMax, Compression -- **代理选项**: ProxyJump, ProxyCommand -- **转发选项**: LocalForward, RemoteForward, DynamicForward, ForwardAgent -- **其他选项**: BatchMode, StrictHostKeyChecking, RequestTTY, RemoteCommand, SendEnv, SetEnv - -#### 配置合并语义 - -遵循 SSH 配置文件的 "first match wins" 语义: -- 按顺序遍历所有 Host 块 -- 如果模式匹配,合并配置(第一个匹配的值优先) -- 通配符 `*` 匹配所有主机 -- IdentityFile 是累加的(不覆盖) - -#### 通配符模式匹配 - -支持以下模式: -- `*` - 匹配所有 -- `*.example.com` - 后缀匹配 -- `server*` - 前缀匹配 -- `?` - 匹配单个字符 -- `!pattern` - 否定匹配(排除) - -### ProxyJump 支持 - -支持跳板机配置,最大深度为 10: - -```rust -// 解析 ProxyJump 链 -let chain = SSHConfigParser::parse_proxy_jump_chain("jump1.com, user@jump2.com:2222"); - -// 解析单个跳板机 -let opts = SSHConfigParser::parse_proxy_jump_host("user@jump.example.com:2222")?; - -// 解析完整的 ProxyJump 链(递归解析每个跳板机的配置) -let chain = SSHConfigParser::resolve_proxy_jump_chain("bastion@jump.example.com", 0)?; -``` - -## SSH 远程 Shell 进程功能 - -### 创建远程 Shell 进程 - -```rust -// 从 SSH 会话创建 -let ssh_proc = SSHShellProc::new( - block_id, - controller_type, // "shell" | "cmd" - &session, - rows, - cols, - app_handle, - block_meta, - input_rx, - block_file, -).await?; - -// 从 SSHConn 创建(便捷方法) -let ssh_proc = SSHShellProc::from_ssh_conn( - block_id, - controller_type, - &ssh_conn, - rows, - cols, - app_handle, - block_meta, - input_rx, - block_file, -).await?; -``` - -### 远程 PTY 功能 - -- **PTY 请求**: 使用 xterm-256color 终端类型 -- **Shell 模式**: 启动远程交互式 Shell -- **Cmd 模式**: 执行远程命令,支持工作目录和环境变量 -- **数据转发**: 异步读取远程输出并推送到前端 -- **终端大小同步**: 自动同步终端大小到远程 PTY - -### 进程控制 - -- `terminate()`: 优雅终止(发送 Ctrl+C 并关闭 Channel) -- `kill()`: 强制终止(直接关闭 Channel) -- `resize()`: 调整远程 PTY 大小 -- `write()`: 写入数据到远程 PTY -- `send_eof()`: 发送 EOF 到远程 - -### 信号处理 - -由于 ssh2 crate 限制,信号通过控制字符发送: -- `SIGINT`: 发送 Ctrl+C (0x03) -- `SIGQUIT`: 发送 Ctrl+\ (0x1C) - -## WSL 连接功能(仅 Windows) - -### 连接字符串解析 - -```rust -// 支持多种格式 -let opts = WSLOpts::parse("wsl://")?; // 默认发行版 -let opts = WSLOpts::parse("wsl://Ubuntu")?; // 指定发行版 -let opts = WSLOpts::parse("wsl://Ubuntu/home/user")?; // 指定路径 -``` - -### 发行版管理 - -```rust -// 列出所有可用的 WSL 发行版 -let distros = WSLConn::list_distros()?; - -// 获取默认发行版 -let default = WSLConn::get_default_distro()?; - -// 检查 WSL 是否可用 -let available = WSLConn::is_wsl_available(); -``` - -### 连接状态管理 - -```rust -let conn = WSLConn::new(opts); -conn.connect().await?; -let status = conn.derive_conn_status(); -``` - -### WSL Shell 进程 - -```rust -let wsl_proc = WSLShellProc::new( - block_id, - opts, - rows, - cols, - app_handle, - block_meta, - input_rx, - block_file, -).await?; -``` - -### 连接类型检测 - -```rust -// 检测连接类型 -is_local_conn_name("local"); // true -is_ssh_conn_name("user@host"); // true -is_wsl_conn_name("wsl://Ubuntu"); // true -``` - -## 连接类型路由 - -### 自动路由 - -根据连接名称自动选择连接类型: - -```rust -use crate::terminal::connections::{ConnectionRouter, ConnectionType}; - -// 自动路由 -let conn_type = ConnectionRouter::route(""); // Local -let conn_type = ConnectionRouter::route("local"); // Local -let conn_type = ConnectionRouter::route("user@host"); // SSH -let conn_type = ConnectionRouter::route("wsl://Ubuntu"); // WSL -``` - -### 路由规则 - -1. 空字符串或 "local" → `ConnectionType::Local` -2. 以 "wsl://" 开头或等于 "wsl" → `ConnectionType::WSL` -3. 以 "ssh://" 开头、包含 "@" 或其他非本地/WSL 格式 → `ConnectionType::SSH` - -### 连接验证 - -```rust -// 验证连接名称格式 -let conn_type = ConnectionRouter::validate("user@host:22")?; - -// 检查连接类型是否在当前平台可用 -let available = ConnectionRouter::is_available(ConnectionType::WSL); -``` - -### 连接信息 - -```rust -// 获取完整的连接信息 -let info = ConnectionInfo::from_conn_name("user@host"); -println!("类型: {}", info.conn_type); // ssh -println!("可用: {}", info.available); // true -println!("描述: {}", info.description); // SSH 远程连接 -``` - -## 事件 - -通过 Tauri 事件系统发送: -- `terminal:output` - 终端输出数据(Base64 编码) -- `terminal:status` - 终端状态变化 -- `terminal:conn-change` - 连接状态变化 - -## 依赖 - -- `portable-pty` - 跨平台 PTY 支持 -- `ssh2` - SSH 协议支持 -- `tokio` - 异步运行时 -- `base64` - 数据编码 -- `parking_lot` - 高性能锁 - -## Requirements 覆盖 - -### 连接类型路由 (connection_router.rs) -- 1.4: 创建 SSH 终端时使用 SSH_Connection 建立远程连接 -- 1.5: 创建 WSL 终端时使用 WSL_Connection 建立连接 - -### 本地 PTY (local_pty.rs) -- 16.1: cmd 命令字符串配置 -- 16.2: cmd_args 参数配置 -- 16.3: cmd_cwd 工作目录配置 -- 17.1: Shell 进程生命周期管理 -- 17.2: 环境变量设置 -- 17.3: 优雅终止和强制终止 -- 17.4: 退出码记录 - -### SSH 连接 (ssh_connection.rs) -- 4.1: SSH 连接字符串解析 -- 4.3-4.6: 多种认证方式 -- 4.7: ProxyJump 跳板机配置支持 -- 4.10: 连接断开处理 -- 4.12: SSH 配置文件解析(~/.ssh/config) -- 7.1-7.7: 连接状态管理 - -### SSH 远程 Shell 进程 (ssh_shell_proc.rs) -- 4.2: SSH 连接建立成功时创建远程 PTY 会话 -- 4.7: 支持 ProxyJump 配置(通过 SSHConn) -- 4.11: 用户调整终端大小时同步调整远程 PTY 大小 - -### WSL 连接 (wsl_connection.rs) -- 5.1: 连接到指定的 WSL 发行版 -- 5.2: 创建 PTY 会话 -- 5.3: 列出所有可用的 WSL 发行版 -- 5.4: 连接断开处理和重连 -- 5.6: 终端大小同步 - -## 更新提醒 - -任何文件变更后,请更新此文档和相关的上级文档。 diff --git a/src-tauri/src/terminal/integration/README.md b/src-tauri/src/terminal/integration/README.md deleted file mode 100644 index 8802928fd..000000000 --- a/src-tauri/src/terminal/integration/README.md +++ /dev/null @@ -1,78 +0,0 @@ -# integration - - - -## 架构说明 - -集成模块,提供 Shell 集成、OSC 序列解析、状态重同步、Shell 集成脚本管理等功能。 - -**核心原则:** -- OSC 序列解析和处理 -- Shell 集成状态管理 -- Shell 集成脚本安装和管理 -- 终端状态重同步机制 - -## 核心功能 - -- **OSC 解析器**: 解析 OSC 7/52/133/16162 序列 -- **Shell 集成**: 目录同步、命令时间记录、状态管理 -- **Shell 脚本**: 各种 Shell 的集成脚本安装和启动配置 -- **状态重同步**: 连接恢复时重建终端状态 - -## 文件索引 - -- `mod.rs` - 模块入口,导出公共类型 -- `resync.rs` - 状态重同步控制器,实现终端状态重建 -- `osc_parser.rs` - OSC 序列解析器,支持 OSC 7/52/133/16162 -- `shell_integration.rs` - Shell 集成处理器,管理 Shell 状态和命令跟踪 -- `shell_scripts.rs` - Shell 集成脚本管理,支持 Bash/Zsh/Fish/PowerShell - -## 已实现功能 - -### 任务 6: 状态重同步控制器 ✅ -- `ResyncController` - 状态重同步控制器 -- `resync_controller` - 便捷重同步函数 -- `ResyncOptions` - 重同步选项配置 -- `ResyncResult` - 重同步结果 -- 终端重置序列发送(完全重置和软重置) -- 历史数据恢复(从 BlockFile 读取) - -### 任务 7.1: OSC 序列解析器 ✅ -- `OSCParser` - OSC 序列解析器 -- `OSCSequence` - OSC 序列类型枚举 -- `PromptMarkType` - 命令提示符标记类型 -- `ParsedOSC` - 解析结果结构 -- `strip_osc_sequences` - 过滤 OSC 序列工具函数 -- 支持 OSC 7(当前目录)、OSC 52(剪贴板)、OSC 133(命令标记)、OSC 16162(Wave 命令) - -### 任务 7.3: ShellIntegration 处理器 ✅ -- `ShellIntegration` - Shell 集成处理器 -- `ShellIntegrationStatus` - 集成状态枚举(Ready、RunningCommand、Unknown) -- `ShellType` - Shell 类型枚举(Bash、Zsh、Fish、Pwsh) -- `CommandInfo` - 命令执行信息(开始时间、结束时间、持续时间) -- `ShellIntegrationEvent` - 状态变更事件 -- 当前目录跟踪(OSC 7) -- 命令时间记录(OSC 133) -- Wave 命令处理(OSC 16162) - -### 任务 21.1: Shell 集成脚本安装 ✅ -- `ShellScripts` - Shell 集成脚本管理器 -- `ShellLaunchConfig` - Shell 启动配置 -- `ShellLaunchBuilder` - Shell 启动配置构建器 -- Bash 集成(--rcfile 参数) -- Zsh 集成(ZDOTDIR 环境变量) -- Fish 集成(-C source 参数) -- PowerShell 集成(-NoExit -Command) -- 自动安装集成脚本到应用数据目录 - -### 任务 21.2: 环境变量配置 ✅ -- `TerminalEnvConfig` - 终端环境变量配置管理器 -- 标准环境变量设置(TERM、COLORTERM) -- 块标识环境变量(PROXYCAST_BLOCKID、WAVETERM_BLOCKID) -- 版本信息环境变量(PROXYCAST_VERSION) -- 语言设置环境变量(LANG、LC_ALL) -- 自定义环境变量合并支持 - -## 更新提醒 - -任何文件变更后,请更新此文档和相关的上级文档。 diff --git a/src-tauri/src/terminal/mod.rs b/src-tauri/src/terminal/mod.rs index 5e998fe9a..eb9a4f4c9 100644 --- a/src-tauri/src/terminal/mod.rs +++ b/src-tauri/src/terminal/mod.rs @@ -1,50 +1,54 @@ -//! 终端核心模块 +//! 终端核心模块(重导出层) //! -//! 提供 PTY 管理和会话管理能力,通过 Tauri Commands 和 Events 暴露给前端。 -//! -//! ## 模块结构 -//! - `error` - 错误类型定义 -//! - `events` - Tauri 事件定义 -//! - `pty_session` - PTY 会话封装 -//! - `session_manager` - 会话管理器 -//! - `persistence` - 持久化存储(块文件、会话元数据) -//! - `block_controller` - 块控制器抽象层 -//! - `connections` - 连接模块(本地 PTY、SSH、WSL) -//! - `integration` - 集成模块(Shell 集成、OSC 解析、状态重同步) -//! -//! ## 使用示例 -//! ```ignore -//! use proxycast_lib::terminal::{TerminalSessionManager, SessionStatus}; -//! -//! let manager = TerminalSessionManager::new(app_handle); -//! let session_id = manager.create_session(24, 80).await?; -//! manager.write_to_session(&session_id, b"ls -la\n").await?; -//! ``` +//! 实际实现位于 `proxycast-terminal` crate。 +//! 本模块提供 `TauriEmitter` newtype 桥接 Tauri 与终端 crate。 -pub mod block_controller; -pub mod connections; -pub mod error; -pub mod events; -pub mod integration; -pub mod persistence; -pub mod pty_session; -pub mod session_manager; +use std::path::PathBuf; -#[cfg(test)] -mod tests; +use tauri::{Emitter, Manager}; + +use proxycast_terminal::emitter::TerminalEventEmit; + +/// Tauri AppHandle 的 newtype 包装 +/// +/// 实现 `TerminalEventEmit` trait,桥接 Tauri 框架与终端 crate。 +#[derive(Clone)] +pub struct TauriEmitter(pub tauri::AppHandle); + +impl TerminalEventEmit for TauriEmitter { + fn emit_event(&self, event: &str, payload: &serde_json::Value) -> Result<(), String> { + self.0 + .emit(event, payload.clone()) + .map_err(|e| format!("Tauri emit 失败: {e}")) + } + + fn app_data_dir(&self) -> Result { + self.0 + .path() + .app_data_dir() + .map_err(|e| format!("获取应用数据目录失败: {e}")) + } +} + +// 重新导出 proxycast-terminal 的所有公共类型 +pub use proxycast_terminal::block_controller; +pub use proxycast_terminal::connections; +pub use proxycast_terminal::emit_helper; +pub use proxycast_terminal::emitter; +pub use proxycast_terminal::error; +pub use proxycast_terminal::events; +pub use proxycast_terminal::integration; +pub use proxycast_terminal::persistence; +pub use proxycast_terminal::pty_session; +pub use proxycast_terminal::session_manager; // 重新导出常用类型 -pub use block_controller::{ - BlockController, BlockControllerRuntimeStatus, BlockInputUnion, BlockMeta, ControllerRegistry, - ControllerStatusEvent, RuntimeOpts, ShellController, TermSize, CONTROLLER_STATUS_EVENT, +pub use proxycast_terminal::{ + resync_controller, BlockController, BlockControllerRuntimeStatus, BlockFile, BlockInputUnion, + BlockMeta, ControllerRegistry, ControllerStatusEvent, DynEmitter, NoOpEmitter, PtySession, + ResyncController, ResyncOptions, ResyncResult, RuntimeOpts, SessionMetadata, + SessionMetadataStore, SessionRecord, SessionStatus, ShellController, ShellProc, TermSize, + TerminalError, TerminalEventEmitter, TerminalOutputEvent, TerminalSessionManager, + TerminalStatusEvent, CONTROLLER_STATUS_EVENT, DEFAULT_COLS, DEFAULT_ROWS, + TERMINAL_RESET_SEQUENCE, TERMINAL_SOFT_RESET_SEQUENCE, }; -pub use connections::ShellProc; -pub use error::TerminalError; -pub use events::{SessionStatus, TerminalOutputEvent, TerminalStatusEvent}; -pub use integration::{ - resync_controller, ResyncController, ResyncOptions, ResyncResult, TERMINAL_RESET_SEQUENCE, - TERMINAL_SOFT_RESET_SEQUENCE, -}; -pub use persistence::{BlockFile, SessionMetadataStore, SessionRecord}; -pub use pty_session::{PtySession, DEFAULT_COLS, DEFAULT_ROWS}; -pub use session_manager::{SessionMetadata, TerminalSessionManager}; diff --git a/src-tauri/src/terminal/persistence/README.md b/src-tauri/src/terminal/persistence/README.md deleted file mode 100644 index cd93ba5d5..000000000 --- a/src-tauri/src/terminal/persistence/README.md +++ /dev/null @@ -1,62 +0,0 @@ -# 终端持久化模块 - -提供终端会话数据的持久化存储能力。 - -## 模块结构 - -| 文件 | 说明 | -|------|------| -| `mod.rs` | 模块入口,导出公共类型 | -| `block_file.rs` | 块文件循环缓冲存储 | -| `session_store.rs` | 会话元数据 SQLite 存储 | - -## 功能 - -### BlockFile - 块文件存储 - -- 终端输出历史的文件存储 -- 循环缓冲策略(超过最大大小时覆盖旧数据) -- 默认最大大小 256KB -- 支持读取、追加、截断操作 - -### SessionMetadataStore - 会话元数据存储 - -- 会话元数据的 SQLite 存储 -- 支持 CRUD 操作 -- 支持按状态、标签页查询 -- 支持会话恢复 - -## 使用示例 - -```rust -use proxycast_lib::terminal::persistence::{BlockFile, SessionMetadataStore, SessionRecord}; - -// 创建块文件 -let base_dir = BlockFile::default_base_dir()?; -let block_file = BlockFile::with_default_size("session-123", &base_dir)?; - -// 追加数据 -block_file.append_data(b"Hello, World!")?; - -// 读取数据 -let data = block_file.read_all()?; - -// 创建会话存储 -let store = SessionMetadataStore::new(db_connection); -store.init_tables()?; - -// 保存会话记录 -let record = SessionRecord::new( - "session-123".to_string(), - "block-123".to_string(), - "tab-1".to_string(), - "shell".to_string(), - None, -); -store.save(&record)?; -``` - -## 相关需求 - -- Requirements 3.1, 3.2, 3.3, 3.4, 3.7 - 块文件存储 -- Requirements 3.5, 3.9 - 会话元数据存储