chore: initialize qiming workspace repository

This commit is contained in:
Codex
2026-05-29 14:22:48 +08:00
commit bfd67a0f2c
10750 changed files with 1885711 additions and 0 deletions

View File

@@ -0,0 +1,430 @@
# auto-launch
## 项目概述
跨平台应用开机自启动库,支持 Windows、macOS 和 Linux。
**GitHub**: https://github.com/zzzgydi/auto-launch.git
**本地路径**: `vendors/auto-launch`
## 目录结构
```
auto-launch/
├── src/
│ ├── lib.rs # 公共 API
│ ├── windows.rs # Windows 实现
│ ├── macos.rs # macOS 实现
│ ├── linux.rs # Linux 实现
│ └── error.rs # 错误处理
├── Cargo.toml
└── README.md
```
## 核心依赖
```toml
[dependencies]
thiserror = "1.0"
os_info = "7.0"
# Windows
windows-registry = "0.2"
windows-result = "0.2"
# macOS
smappservice-rs = "0.3"
# Linux
dirs = "5.0"
```
## 核心 API
### AutoLaunchBuilder
```rust
// lib.rs
pub struct AutoLaunchBuilder {
app_name: Option<String>,
app_path: Option<String>,
macos_launch_mode: MacOSLaunchMode,
bundle_identifiers: Option<Vec<String>>,
agent_extra_config: Option<String>,
windows_enable_mode: WindowsEnableMode,
linux_launch_mode: LinuxLaunchMode,
args: Option<Vec<String>>,
}
impl AutoLaunchBuilder {
pub fn new() -> Self;
/// 设置应用名称
pub fn set_app_name(&mut self, name: &str) -> &mut Self;
/// 设置应用路径
pub fn set_app_path(&mut self, path: &str) -> &mut Self;
/// 设置 macOS 启动模式
pub fn set_macos_launch_mode(&mut self, mode: MacOSLaunchMode) -> &mut Self;
/// 设置 Windows 启用模式
pub fn set_windows_enable_mode(&mut self, mode: WindowsEnableMode) -> &mut Self;
/// 设置 Linux 启动模式
pub fn set_linux_launch_mode(&mut self, mode: LinuxLaunchMode) -> &mut Self;
/// 设置启动参数
pub fn set_args(&mut self, args: &[impl AsRef<str>]) -> &mut Self;
/// 构建 AutoLaunch
pub fn build(&self) -> Result<AutoLaunch>;
}
```
### AutoLaunch
```rust
pub struct AutoLaunch {
app_name: String,
app_path: String,
args: Vec<String>,
// 平台特定配置...
}
impl AutoLaunch {
/// 启用自启动
pub async fn enable(&self) -> Result<()>;
/// 禁用自启动
pub async fn disable(&self) -> Result<()>;
/// 检查是否已启用
pub async fn is_enabled(&self) -> Result<bool>;
}
```
### 平台特定枚举
```rust
pub enum MacOSLaunchMode {
/// 使用 SMAppService (macOS 13+)
SMAppService,
/// 使用 LaunchAgent
LaunchAgent,
/// 使用 AppleScript
AppleScript,
}
pub enum WindowsEnableMode {
/// 动态尝试(系统级 -> 用户级)
Dynamic,
/// 仅当前用户
CurrentUser,
/// 所有用户(需管理员权限)
System,
}
pub enum LinuxLaunchMode {
/// 使用 XDG Autostart
XdgAutostart,
/// 使用 systemd
Systemd,
}
```
## 使用示例
```rust
use auto_launch::AutoLaunch;
#[tokio::main]
async fn main() -> Result<()> {
let auto = AutoLaunchBuilder::new()
.set_app_name("qiming-agent")
.set_app_path("/Applications/qiming-agent.app")
.set_args(&["--hidden", "--minimize"])
.set_macos_launch_mode(MacOSLaunchMode::SMAppService)
.build()?;
// 检查是否已启用
let is_enabled = auto.is_enabled().await?;
println!("Auto launch enabled: {}", is_enabled);
// 启用自启动
if !is_enabled {
auto.enable().await?;
}
Ok(())
}
```
## 平台实现
| 平台 | 实现方式 | 配置位置 |
|------|----------|----------|
| Windows | 注册表 | `HKCU\Software\Microsoft\Windows\CurrentVersion\Run` |
| macOS | SMAppService | `~/Library/LaunchAgents/` |
| Linux | XDG | `~/.config/autostart/` |
## 与 agent-client 集成场景
### 场景1设置界面的自启动开关
```rust
// 在设置界面中集成自启动管理
use auto_launch::{AutoLaunch, AutoLaunchBuilder, MacOSLaunchMode};
pub struct AutoLaunchManager {
auto_launch: AutoLaunch,
// 当前状态(缓存,避免频繁读取)
cached_enabled: bool,
}
impl AutoLaunchManager {
pub fn new(app_name: &str, app_path: &str) -> Result<Self> {
let auto = AutoLaunchBuilder::new()
.set_app_name(app_name)
.set_app_path(app_path)
.set_macos_launch_mode(MacOSLaunchMode::SMAppService) // macOS 13+
.build()?;
Ok(Self {
auto_launch: auto,
cached_enabled: false,
})
}
/// 获取当前自启动状态
pub async fn is_enabled(&self) -> Result<bool> {
self.auto_launch.is_enabled().await
}
/// 启用自启动
pub async fn enable(&self) -> Result<()> {
self.auto_launch.enable().await
}
/// 禁用自启动
pub async fn disable(&self) -> Result<()> {
self.auto_launch.disable().await
}
/// 切换自启动状态
pub async fn toggle(&self) -> Result<bool> {
let enabled = self.is_enabled().await?;
if enabled {
self.disable().await?;
} else {
self.enable().await?;
}
Ok(!enabled)
}
}
/// 设置界面中的自启动开关组件
pub struct AutoLaunchSwitch {
manager: AutoLaunchManager,
enabled: bool,
loading: bool,
}
impl AutoLaunchSwitch {
pub fn new(manager: AutoLaunchManager) -> Self {
Self {
manager,
enabled: false,
loading: true,
}
}
/// 初始化时检查状态
pub fn refresh_status(&mut self) {
let manager = self.manager.clone();
cx.spawn(|this, mut cx| async move {
let enabled = manager.is_enabled().await.unwrap_or(false);
this.update(&mut cx, |this, _| {
this.enabled = enabled;
this.loading = false;
});
});
}
}
impl Render for AutoLaunchSwitch {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
div()
.flex()
.items_center()
.justify_between()
.p_4()
.rounded_md()
.bg(cx.theme().colors.surface)
.border_1()
.border_color(cx.theme().colors.border)
.child(
div()
.flex()
.flex_col()
.gap_1()
.child(
div()
.font_medium()
.child("Auto Launch")
)
.child(
div()
.text_sm()
.text_color(cx.theme().colors.muted)
.child("Start application when system boots")
)
)
.child(
match self.loading {
true => Loading::new().into_any_element(),
false => Switch::new(self.enabled, |enabled, _| {
// 切换自启动
let manager = self.manager.clone();
cx.spawn(|this, mut cx| async move {
let result = if enabled {
manager.disable().await
} else {
manager.enable().await
};
match result {
Ok(_) => {
this.update(&mut cx, |this, _| {
this.enabled = !enabled;
});
}
Err(e) => {
// 显示错误提示
tracing::error!("Failed to toggle auto-launch: {}", e);
}
}
});
}).into_any_element(),
}
)
}
}
```
### 场景2应用启动时检查自启动状态
```rust
// 应用启动时的初始化逻辑
pub struct AppInitializer {
auto_launch_manager: Option<AutoLaunchManager>,
}
impl AppInitializer {
pub async fn initialize(&mut self, app_path: &str) -> Result<()> {
// 1. 初始化自启动管理器
self.auto_launch_manager = Some(
AutoLaunchManager::new("qiming-agent", app_path).ok()
);
// 2. 检查自启动是否已启用
if let Some(manager) = &self.auto_launch_manager {
let is_enabled = manager.is_enabled().await.unwrap_or(false);
tracing::info!("Auto-launch enabled: {}", is_enabled);
}
// 3. 根据启动参数决定行为
let args: Vec<String> = std::env::args().collect();
// 如果是通过自启动触发的,可能需要最小化到托盘
if args.iter().any(|arg| arg == "--minimize") {
// 最小化到托盘,不显示主窗口
}
Ok(())
}
}
```
### 场景3跨平台注意事项
```rust
// 不同平台的自启动行为差异处理
#[cfg(target_os = "windows")]
mod windows_impl {
use auto_launch::WindowsEnableMode;
pub fn get_app_path() -> String {
// Windows: 使用完整路径
std::env::current_exe()
.unwrap()
.to_string_lossy()
.into_owned()
}
pub fn build_auto_launch(name: &str, path: &str) -> auto_launch::AutoLaunchBuilder {
AutoLaunchBuilder::new()
.set_app_name(name)
.set_app_path(path)
.set_windows_enable_mode(WindowsEnableMode::CurrentUser) // 当前用户
}
}
#[cfg(target_os = "macos")]
mod macos_impl {
use auto_launch::MacOSLaunchMode;
pub fn get_app_path() -> String {
// macOS: 使用 .app 路径
std::env::current_exe()
.unwrap()
.to_string_lossy()
.into_owned()
}
pub fn build_auto_launch(name: &str, path: &str) -> auto_launch::AutoLaunchBuilder {
AutoLaunchBuilder::new()
.set_app_name(name)
.set_app_path(path)
.set_macos_launch_mode(MacOSLaunchMode::SMAppService) // macOS 13+
}
}
#[cfg(target_os = "linux")]
mod linux_impl {
use auto_launch::LinuxLaunchMode;
pub fn get_app_path() -> String {
std::env::current_exe()
.unwrap()
.to_string_lossy()
.into_owned()
}
pub fn build_auto_launch(name: &str, path: &str) -> auto_launch::AutoLaunchBuilder {
AutoLaunchBuilder::new()
.set_app_name(name)
.set_app_path(path)
.set_linux_launch_mode(LinuxLaunchMode::XdgAutostart)
}
}
```
## 在本项目中的使用
用于实现 agent-client 的开机自启动功能:
```
agent-client
└── auto-launch
├── 检测自启动状态 (设置界面开关)
├── 启用自启动 (用户勾选开关)
├── 禁用自启动 (用户取消勾选)
└── 应用启动时最小化到托盘
```

View File

@@ -0,0 +1,516 @@
# cargo-packager
## 项目概述
应用打包工具,支持生成多种平台安装包。是从 Tauri 框架剥离出来的独立打包工具。
**本地路径**: `vendors/cargo-packager`
## 目录结构
```
cargo-packager/
├── Cargo.toml # workspace 配置
├── packager/ # 打包核心
│ └── src/
│ ├── lib.rs
│ ├── config.rs # 配置
│ ├── package.rs # 打包逻辑
│ └── utils.rs # 工具函数
├── config/ # 配置定义
│ └── src/
│ └── lib.rs
├── resource-resolver/ # 资源解析
│ └── src/
│ └── lib.rs
├── updater/ # 自动更新
│ └── src/
│ └── lib.rs
├── codesign/ # 代码签名
│ └── src/
│ └── lib.rs
├── package/
│ ├── deb/ # Debian 包
│ ├── appimage/ # AppImage
│ ├── dmg/ # macOS DMG
│ ├── msi/ # Windows MSI
│ └── nsis/ # Windows NSIS
└── Cargo.toml
```
## 核心依赖
```toml
[dependencies]
serde = "1.0"
serde_json = "1.0"
toml = "0.7"
clap = { version = "4.0", features = ["derive"] }
schemars = "0.8"
tracing = "0.1"
tar = "0.4"
zip = "0.6"
napi = { version = "2.0", optional = true }
dirs = "5.0"
semver = "1.0"
url = "2.4"
thiserror = "1.0"
```
## 核心 API
### Config
```rust
// config/src/lib.rs
pub struct PackageConfig {
pub product_name: String,
pub version: String,
pub description: Option<String>,
pub homepage: Option<String>,
pub default_author: Option<String>,
pub icons: Vec<IconConfig>,
pub resources: Vec<ResourceConfig>,
pub bin: Vec<BinConfig>,
pub i18n: Vec<I18nConfig>,
}
pub struct Config {
pub package: PackageConfig,
pub formats: Vec<PackageFormat>,
pub signing: Option<SigningConfig>,
pub windows: Option<WindowsConfig>,
pub macos: Option<MacosConfig>,
pub linux: Option<LinuxConfig>,
}
```
### PackageFormat
```rust
pub enum PackageFormat {
/// Windows MSI
Msi(WindowsMsiConfig),
/// Windows NSIS
Nsis(WindowsNsisConfig),
/// macOS DMG
Dmg(MacosDmgConfig),
/// macOS Bundle
Bundle(MacosBundleConfig),
/// Linux Deb
Deb(DebConfig),
/// Linux AppImage
AppImage(AppImageConfig),
}
```
### 使用示例
```rust
use cargo_packager::{Config, PackageFormat};
#[tokio::main]
async fn main() -> Result<()> {
let config = Config {
package: PackageConfig {
product_name: "qiming-agent".to_string(),
version: "1.0.0".to_string(),
description: Some("Agent Client".to_string()),
homepage: Some("https://example.com".to_string()),
default_author: Some("Author <author@example.com>".to_string()),
icons: vec![IconConfig::Path {
path: "icons/icon.png".into(),
}],
resources: vec![ResourceConfig {
patterns: vec!["assets/**/*".into()],
target: None,
}],
bin: vec![],
i18n: vec![],
},
formats: vec![
PackageFormat::Msi(WindowsMsiConfig::default()),
PackageFormat::Dmg(MacosDmgConfig::default()),
PackageFormat::Deb(DebConfig::default()),
PackageFormat::AppImage(AppImageConfig::default()),
],
signing: Some(SigningConfig {
identity: None,
digest: Some(DigestAlgorithm::Sha256),
timestamp: Some(TimestampConfig::default()),
..Default::default()
}),
windows: Some(WindowsConfig {
signing_config: Some(SigningConfig::default()),
..Default::default()
}),
macos: Some(MacosConfig {
signing_config: Some(SigningConfig::default()),
provider: None,
}),
linux: Some(LinuxConfig {
package: LinuxPackageFormatConfig::default(),
..Default::default()
}),
};
// 执行打包
cargo_packager::package(&config).await?;
Ok(())
}
```
## 自动更新配置
```rust
pub struct UpdaterConfig {
/// 启用自动更新
pub active: bool,
/// 更新端点 URL 模板
pub endpoints: Option<Vec<String>>,
/// 公钥
pub pubkey: String,
/// 苹果开发者 ID
pub apple_id: Option<String>,
/// Windows 证书
pub windows: Option<WindowsUpdaterConfig>,
}
impl UpdaterConfig {
/// 默认端点模板
pub fn default_endpoints(&self, target: &str) -> Vec<String> {
vec![
format!("https://updates.example.com/update/{target}/{version}"),
format!("https://updates.example.com/update/{target}/{version}-{target}.{extension}"),
]
}
}
```
## 与 agent-client 集成场景
### 场景1完整的打包配置
```rust
// build.rs 或打包脚本
use cargo_packager::{Config, PackageFormat, PackageConfig};
use cargo_packager::config::{MacosDmgConfig, WindowsMsiConfig, DebConfig, AppImageConfig};
pub async fn build_packages() -> Result<()> {
let version = env!("CARGO_PKG_VERSION");
let app_name = "qiming-agent";
let config = Config {
package: PackageConfig {
product_name: app_name.to_string(),
version: version.to_string(),
description: Some("Cross-platform AI Agent Client".to_string()),
homepage: Some("https://example.com".to_string()),
default_author: Some("qiming <support@example.com>".to_string()),
icons: vec![
IconConfig::Path {
path: "assets/icons/icon.png".into(),
},
IconConfig::Path {
path: "assets/icons/icon.ico".into(),
},
IconConfig::Path {
path: "assets/icons/icon.icns".into(),
},
],
resources: vec![
ResourceConfig {
patterns: vec![
"assets/**/*".into(),
"locales/**/*".into(),
],
target: None,
},
],
bin: vec![],
i18n: vec![],
},
formats: vec![
PackageFormat::Msi(WindowsMsiConfig {
// MSI 配置
database_path: Some("assets/wix/database.wxs".into()),
..Default::default()
}),
PackageFormat::Dmg(MacosDmgConfig {
// DMG 配置
background: Some("assets/dmg/background.png".into()),
..Default::default()
}),
PackageFormat::Deb(DebConfig {
// Debian 包配置
priority: Some("optional".to_string()),
section: Some("utils".to_string()),
..Default::default()
}),
PackageFormat::AppImage(AppImageConfig {
// AppImage 配置
..Default::default()
}),
],
signing: Some(SigningConfig {
identity: std::env::var("SIGNING_IDENTITY").ok(),
digest: Some(DigestAlgorithm::Sha256),
timestamp: Some(TimestampConfig {
server: Some("http://timestamp.digicert.com".to_string()),
}),
..Default::default()
}),
windows: Some(WindowsConfig {
signing_config: Some(SigningConfig {
identity: std::env::var("WINDOWS_CERT").ok(),
..Default::default()
}),
..Default::default()
}),
macos: Some(MacosConfig {
signing_config: Some(SigningConfig {
identity: std::env::var("APPLE_SIGNING_ID").ok(),
provider: std::env::var("APPLE_PROVIDER").ok(),
}),
..Default::default()
}),
linux: Some(LinuxConfig {
package: LinuxPackageFormatConfig::default(),
..Default::default()
}),
};
// 执行打包
cargo_packager::package(&config).await?;
Ok(())
}
```
### 场景2CI/CD 集成
```yaml
# .github/workflows/release.yml
name: Release
on:
release:
types: [created]
jobs:
build:
strategy:
matrix:
include:
- os: ubuntu-latest
targets: deb,appimage
- os: macos-latest
targets: dmg
- os: windows-latest
targets: msi
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Setup Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
target: ${{ matrix.target == 'deb' && 'x86_64-unknown-linux-gnu' || 'x86_64-apple-darwin' }}
- name: Build
run: cargo build --release
- name: Package
run: |
cargo run --package cargo-packager -- --package-format ${{ matrix.targets }}
- name: Sign macOS
if: matrix.os == 'macos-latest'
run: |
echo "${{ secrets.APPLE_CERT }}" | base64 -d > certificate.p12
security create-keychain -p password build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p password build.keychain
security import certificate.p12 -P "" -A -t cert -f pkcs12 -k build.keychain
codesign --deep --force --sign "${{ secrets.APPLE_SIGNING_ID }}" target/release/qiming-agent
- name: Sign Windows
if: matrix.os == 'windows-latest'
run: |
& "C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x64\signtool.exe" sign /f certificate.pfx /p ${{ secrets.WINDOWS_CERT_PASSWORD }} /fd SHA256 /tr http://timestamp.digicert.com /td SHA256 /v target/release/qiming-agent-*.msi
- name: Upload Assets
uses: softprops/action-gh-release@v1
with:
files: |
target/release/qiming-agent-*.msi
target/release/qiming-agent-*.dmg
target/release/qiming-agent-*.deb
target/release/qiming-agent-*.AppImage
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```
### 场景3自动更新配置
```rust
// 打包时生成更新配置
pub struct UpdaterConfig {
/// 更新服务器地址
pub update_url: String,
/// 公钥(用于签名验证)
pub public_key: String,
/// 是否启用自动更新
pub enable_auto_update: bool,
}
impl UpdaterConfig {
/// 生成更新配置模板
pub fn generate_app_config(&self) -> String {
format!(r#"
[updater]
enabled = {}
url = "{}"
"#, self.enable_auto_update, self.update_url)
}
/// 获取更新端点
pub fn get_update_endpoints(&self, version: &str, target: &str) -> Vec<String> {
vec![
format!("{}/{}/{}/qiming-agent_{}_{}.{}",
self.update_url, version, target, version, target, self.extension(target)),
]
}
fn extension(target: &str) -> &str {
match target {
_ if target.contains("windows") => "msi",
_ if target.contains("darwin") => "dmg",
_ if target.contains("linux") => if target.contains("deb") { "deb" } else { "AppImage" },
_ => "tar.gz",
}
}
}
```
### 场景4资源文件处理
```rust
// 打包时的资源处理
pub struct ResourceProcessor {
// 资源目录
resource_dir: PathBuf,
// 输出目录
output_dir: PathBuf,
}
impl ResourceProcessor {
pub fn new(resource_dir: &str, output_dir: &str) -> Self {
Self {
resource_dir: PathBuf::from(resource_dir),
output_dir: PathBuf::from(output_dir),
}
}
/// 处理所有资源
pub async fn process(&self) -> Result<Vec<ResourceFile>> {
let mut resources = Vec::new();
// 1. 图标资源
resources.extend(self.process_icons().await?);
// 2. 翻译文件
resources.extend(self.process_locales().await?);
// 3. 配置文件
resources.extend(self.process_configs().await?);
Ok(resources)
}
async fn process_icons(&self) -> Result<Vec<ResourceFile>> {
let icons_dir = self.resource_dir.join("icons");
let mut icons = Vec::new();
for entry in fs::read_dir(&icons_dir)? {
let entry = entry?;
let path = entry.path();
if path.extension().map(|e| e == "png" || e == "ico" || e == "icns").unwrap_or(false) {
icons.push(ResourceFile {
source: path.clone(),
target: format!("icons/{}", path.file_name().unwrap()),
});
}
}
Ok(icons)
}
async fn process_locales(&self) -> Result<Vec<ResourceFile>> {
let locales_dir = self.resource_dir.join("locales");
let mut locales = Vec::new();
for entry in fs::read_dir(&locales_dir)? {
let entry = entry?;
let path = entry.path();
if path.extension().map(|e| e == "json").unwrap_or(false) {
locales.push(ResourceFile {
source: path.clone(),
target: format!("locales/{}", path.file_name().unwrap()),
});
}
}
Ok(locales)
}
}
```
## 在本项目中的使用
用于打包 agent-client 为各平台安装包:
```
agent-client
├── cargo-packager
│ │
│ ├── Windows MSI/NSIS 安装包
│ ├── macOS DMG 磁盘镜像
│ ├── Linux Deb 包
│ └── Linux AppImage
└── 资源文件
├── icons/ (应用图标)
├── locales/ (多语言)
└── assets/ (其他资源)
```
## 打包命令
```bash
# CLI 打包
cargo-packager --package-format msi,dmg,deb,appimage
# 代码调用
cargo_packager::package(&config).await?;
# 单独平台打包
cargo-packager --package-format msi # Windows
cargo-packager --package-format dmg # macOS
cargo-packager --package-format deb # Linux Deb
cargo-packager --package-format appimage # Linux AppImage
```

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,240 @@
# mcp-proxy
## 项目概述
MCP (Model Context Protocol) 代理服务集合,包含 MCP 代理、文档解析、语音处理、OSS 客户端等功能。
**本地路径**: `vendors/mcp-proxy`
## 目录结构
```
mcp-proxy/
├── Cargo.toml # workspace 配置
├── mcp-proxy/ # MCP 代理服务
│ └── src/
│ ├── main.rs
│ ├── server.rs # HTTP 服务
│ ├── handler.rs # 请求处理
│ └── state.rs # 状态管理
├── document-parser/ # 文档解析
│ └── src/
│ ├── lib.rs
│ ├── pdf.rs
│ ├── office.rs
│ └── parser.rs
├── voice-cli/ # 语音服务
│ └── src/
│ ├── lib.rs
│ ├── recognize.rs
│ └── synthesize.rs
├── oss-client/ # OSS 客户端
│ └── src/
│ ├── lib.rs
│ ├── client.rs
│ └── error.rs
├── mcp-common/ # 共享模块
│ └── src/
│ ├── lib.rs
│ ├── types.rs
│ └── util.rs
└── Cargo.toml
```
## 核心依赖
```toml
[dependencies]
# Web 框架
axum = "0.7"
tokio = { version = "1.0", features = ["full"] }
tower-http = { version = "0.6", features = ["cors", "auth"] }
# MCP 协议
rmcp = "1.0"
# 任务队列
apalis = "1.0"
# 数据库
sqlx = "0.7"
sled = "1.0"
# 网络
reqwest = { version = "0.11", features = ["json"] }
tokio-tungstenite = "0.23"
# 并发
dashmap = "6.0"
# OpenTelemetry
opentelemetry = "0.30"
opentelemetry-otlp = "0.27"
# 文档处理
pdf = "0.8"
calamine = "0.5" # Excel
```
## 核心模块
### MCP 代理服务
```rust
// mcp-proxy/src/server.rs
pub struct McpProxyServer {
// MCP 工具注册表
tools: DashMap<String, Box<dyn McpTool>>,
// 连接管理
connections: ConnectionManager,
// 状态
state: Arc<McpState>,
}
impl McpProxyServer {
/// 注册 MCP 工具
pub fn register_tool<T: McpTool>(&self, tool: T);
/// 列出所有工具
pub fn list_tools(&self) -> Vec<ToolInfo>;
/// 调用工具
pub async fn call_tool(&self, name: &str, params: Value) -> Result<Value>;
}
```
### 文档解析
```rust
// document-parser/src/parser.rs
pub enum Document {
Pdf(PdfDocument),
Word(WordDocument),
Excel(ExcelDocument),
Text(TextDocument),
}
pub struct ParseOptions {
pub max_pages: Option<usize>,
pub extract_images: bool,
pub extract_tables: bool,
}
impl DocumentParser {
/// 解析文档
pub async fn parse(
&self,
path: &Path: ParseOptions,
) -> Result,
options<Document>;
/// 提取文本
pub async fn extract_text(&self, doc: &Document) -> Result<String>;
}
```
### 任务队列 (Apolis)
```rust
use apalis::prelude::*;
pub struct JobQueue {
storage: SqliteStorage<JobData>,
worker: Worker<SqliteStorage<JobData>>,
}
impl JobQueue {
pub async fn new(db_path: &str) -> Result<Self> {
let storage = SqliteStorage::new(db_path);
storage.setup().await?;
Ok(Self {
storage,
worker: Worker::new(),
})
}
/// 添加任务
pub async fn enqueue(&self, job: JobData) -> Result<()>;
/// 处理任务
pub async fn process_jobs(&self);
}
#[derive(Debug, Serialize, Deserialize)]
pub struct JobData {
pub id: String,
pub job_type: JobType,
pub payload: Value,
pub created_at: DateTime<Utc>,
}
pub enum JobType {
DocumentParse,
VoiceRecognize,
VoiceSynthesize,
McpRequest,
}
```
## 使用示例
```rust
use mcp_proxy::McpProxyServer;
#[tokio::main]
async fn main() -> Result<()> {
let server = McpProxyServer::new();
// 注册自定义工具
server.register_tool(SearchTool);
server.register_tool(FileTool);
// 启动 HTTP 服务
let app = axum::Router::new()
.route("/tools", axum::routing::get(list_tools))
.route("/tools/:name/call", axum::routing::post(call_tool))
.with_state(server);
axum::Server::bind(&"0.0.0.0:3000".parse()?)
.serve(app.into_make_service())
.await?;
Ok(())
}
```
## 在本项目中的使用
用于提供 MCP 服务能力,支持管理端的 MCP 工具调用:
```
agent-server-admin (管理端)
├── MCP 工具调用 --> mcp-proxy
│ │
│ ├── 文档解析
│ ├── 语音处理
│ └── OSS 存储
└── SSE 响应 <---------
```
## gRPC 服务定义
```protobuf
service McpProxyService {
// 列出可用工具
rpc ListTools(ListToolsRequest) returns (ListToolsResponse);
// 调用工具
rpc CallTool(CallToolRequest) returns (CallToolResponse);
// 订阅工具执行进度
rpc Subscribe(SubscribeRequest) returns (stream ProgressEvent);
}
```

View File

@@ -0,0 +1,700 @@
# qiming-rustdesk
## 项目概述
RustDesk 远程桌面客户端的定制版本支持远程控制、文件传输、音视频服务、剪贴板同步等功能。作为本项目的核心通信库提供双向通信、P2P 直连和中继传输能力。
**本地路径**: `vendors/qiming-rustdesk`
## 目录结构
```
qiming-rustdesk/
├── src/
│ ├── lib.rs # 库入口导出公共API
│ ├── client.rs # 客户端连接处理核心模块
│ ├── server.rs # 服务端音视频/剪贴板/输入服务
│ ├── common.rs # 公共工具函数和类型定义
│ ├── rendezvous_mediator.rs # 远程连接中介(核心)
│ ├── ipc.rs # 进程间通信
│ └── platform/ # 平台特定代码
│ ├── linux/
│ ├── macos/
│ └── windows/
├── libs/
│ ├── hbb_common/ # 通用工具库(最重要)
│ │ ├── src/
│ │ │ ├── lib.rs
│ │ │ ├── message_proto/ # protobuf 消息定义
│ │ │ ├── net/ # 网络相关TCP/KCP/WebSocket
│ │ │ ├── crypto/ # 加密模块
│ │ │ ├── config/ # 配置管理
│ │ │ └── util/ # 工具函数
│ │ └── Cargo.toml
│ ├── scrap/ # 屏幕捕获库
│ ├── enigo/ # 键盘鼠标模拟库
│ ├── clipboard/ # 剪贴板库
│ └── virtual_display/ # 虚拟显示驱动
├── proto/ # protobuf 协议定义
└── Cargo.toml # workspace 配置
```
## 核心通信架构
### 整体架构
```
┌─────────────────────────────────────────────────────────────────┐
│ agent-client │
│ │
│ ┌─────────────────┐ ┌─────────────────────────────────┐ │
│ │ UI 界面 │ │ 核心通信模块 │ │
│ │ - 聊天窗口 │ │ - rendezvous_mediator (连接管理)│ │
│ │ - 设置界面 │ │ - client (数据收发) │ │
│ │ - 依赖管理 │ │ - server (音视频服务) │ │
│ └─────────────────┘ └─────────────────────────────────┘ │
│ │ │
└────────────────────────────────┼────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ data-server (rustdesk-server) │
│ │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ hbbs │ │ hbbr │ │
│ │ (信令服务器) │ │ (中继服务器) │ │
│ │ - 端口: 21116 │ │ - 端口: 21117 │ │
│ │ - ID 注册 │ │ - 数据中继 │ │
│ │ - 连接协商 │ │ - 带宽限制 │ │
│ │ - NAT 穿透 │ │ │ │
│ └─────────────────────┘ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ agent-server-admin (管理端) │
└─────────────────────────────────────────────────────────────────┘
```
## 连接管理 (rendezvous_mediator.rs)
### 核心结构体
```rust
// src/rendezvous_mediator.rs
pub struct RendezvousMediator {
addr: TargetAddr<'static>,
host: String,
host_prefix: String,
keep_alive: i32,
}
impl RendezvousMediator {
/// 启动连接管理器(根据配置选择 UDP 或 TCP 模式)
pub async fn start(server: ServerPtr, host: String) -> ResultType<()> {
log::info!("start rendezvous mediator of {}", host);
// 根据配置选择通信模式
if cfg!(debug_assertions) && option_env!("TEST_TCP").is_some()
|| Config::is_proxy()
|| use_ws()
|| crate::is_udp_disabled()
{
// WebSocket/代理模式
Self::start_tcp(server, host).await
} else {
// UDP 直连模式
Self::start_udp(server, host).await
}
}
}
```
### 双协议支持
```rust
// UDP 直连模式(优先使用,性能更好)
async fn start_udp(server: ServerPtr, host: String) -> ResultType<()> {
// 注册超时管理
const MIN_REG_TIMEOUT: i64 = 3_000;
const MAX_REG_TIMEOUT: i64 = 30_000;
let mut reg_timeout = MIN_REG_TIMEOUT;
loop {
// 心跳延迟计算EMA 指数移动平均)
if ema_latency == 0 {
ema_latency = latency;
} else {
ema_latency = latency / 30 + (ema_latency * 29 / 30);
}
// 动态调整注册间隔
let mut n = latency / 5;
if n < 3000 { n = 3000; }
// 心跳保活
if !send_heartbeat(...).await {
reg_timeout = (reg_timeout * 2).min(MAX_REG_TIMEOUT);
}
}
}
// TCP/WebSocket 模式(代理环境或 UDP 被禁用时)
async fn start_tcp(server: ServerPtr用于, host: String) -> ResultType<()> {
// 建立 TCP 连接
let mut socket = connect_tcp(&*self.host, CONNECT_TIMEOUT).await?;
// 发送注册消息
let mut msg_out = Message::new();
let mut rr = RegisterRelay {
socket_addr: socket_addr.into(),
version: crate::VERSION.to_owned(),
..Default::default()
};
msg_out.set_register_relay(rr);
socket.send(&msg_out).await?;
// 处理中继响应
if let Some(msg) = socket.next().await {
let response = msg?.payload;
// 建立中继连接...
}
}
```
## P2P 直连流程 (打洞)
### 打洞请求处理
```rust
// src/rendezvous_mediator.rs
async fn handle_punch_hole(&self, ph: PunchHole, server: ServerPtr) -> ResultType<()> {
// 1. 解析对端地址
let mut peer_addr = AddrMangle::decode(&ph.socket_addr);
// 2. 去重处理100ms 内重复消息忽略)
let last = *LAST_MSG.lock().await;
if last.0 == peer_addr && last.1.elapsed().as_millis() < 100 {
return Ok(()); // 忽略重复消息
}
// 3. 判断是否需要中继
let relay = use_ws() || Config::is_proxy() || ph.force_relay;
let nat_type = NatType::from_i32(Config::get_nat_type())?;
// 对称型 NAT 或强制中继时降级到中继模式
if nat_type == NatType::SYMMETRIC || relay || (config::is_disable_tcp_listen() && ph.udp_port <= 0) {
return self.create_relay(...).await;
}
// 4. UDP 打洞
if ph.udp_port > 0 {
peer_addr.set_port(ph.udp_port as u16);
return self.punch_udp_hole(peer_addr, server, msg_punch, control_permissions).await;
}
// 5. TCP 打洞
let mut socket = connect_tcp(&*self.host, CONNECT_TIMEOUT).await?;
allow_err!(socket_client::connect_tcp_local(peer_addr, Some(local_addr), 30).await);
}
```
### NAT 类型与连接策略
```rust
// NAT 类型决定连接策略
enum NatType {
NO_NAT, // 无 NAT优先 P2P 直连
FULL_CONE, // 完全锥形 NAT优先 P2P 直连
RESTRICTED, // 限制锥形 NAT可能需要中继
SYMMETRIC, // 对称型 NAT必须中继
}
// 连接策略选择
fn select_connection_strategy(nat_type: NatType, force_relay: bool) -> ConnectionMode {
if force_relay || nat_type == NatType::SYMMETRIC {
ConnectionMode::Relay // 必须中继
} else if nat_type == NatType::RESTRICTED {
ConnectionMode::TryP2PThenRelay // 尝试 P2P失败则中继
} else {
ConnectionMode::P2P // 优先 P2P
}
}
```
## 中继模式 (Relay)
### 创建中继连接
```rust
// src/rendezvous_mediator.rs
async fn create_relay(
&self,
socket_addr: Vec<u8>,
relay_server: String,
uuid: String,
server: ServerPtr,
secure: bool,
initiate: bool,
socket_addr_v6: bytes::Bytes,
control_permissions: Option<ControlPermissions>,
) -> ResultType<()> {
// 1. 建立到 rendezvous server 的 TCP 连接
let mut socket = connect_tcp(&*self.host, CONNECT_TIMEOUT).await?;
// 2. 发送中继请求
let mut msg_out = Message::new();
let mut rr = RelayResponse {
socket_addr: socket_addr.into(),
version: crate::VERSION.to_owned(),
socket_addr_v6,
..Default::default()
};
msg_out.set_relay_response(rr);
socket.send(&msg_out).await?;
// 3. 建立 relay 连接
crate::create_relay_connection(
server,
relay_server,
uuid,
peer_addr,
secure,
is_ipv4(&self.addr),
control_permissions,
).await;
Ok(())
}
```
### 心跳保活机制
```rust
// 心跳机制确保连接活跃
async fn send_heartbeat(&self, socket: &mut impl FramedWrite) -> bool {
let mut msg = Message::new();
msg.set_ping(Ping {
timestamp: get_time().await,
..Default::default()
});
if let Err(e) = socket.send(&msg).await {
log::error!("Heartbeat failed: {}", e);
return false;
}
true
}
// 心跳超时检测
fn check_heartbeat_timeout(last_heartbeat: Option<DateTime<Utc>>, timeout_secs: i64) -> bool {
if let Some(timestamp) = last_heartbeat {
let elapsed = Utc::now() - timestamp;
elapsed.num_seconds() > timeout_secs
} else {
false
}
}
```
## Protocol Buffers 消息定义
### 主消息类型
```protobuf
// libs/hbb_common/src/message_proto/mod.rs
// 主消息类型oneof 模式支持多种消息)
message Message {
int32 id = 1; // 消息 ID
MessageType type = 2; // 消息类型
oneof payload {
LoginRequest login_request = 3;
LoginResponse login_response = 4;
AudioFrame audio_frame = 5;
VideoFrame video_frame = 6;
InputEvent input_event = 7;
ClipboardData clipboard_data = 8;
FileTransfer file_transfer = 9;
CloseReason close_reason = 10;
Ping ping = 11;
Pong pong = 12;
PunchHole punch_hole = 13; // 打洞消息
RelayResponse relay_response = 14; // 中继响应
}
}
enum MessageType {
NONE = 0;
LOGIN_REQUEST = 1;
LOGIN_RESPONSE = 2;
AUDIO_FRAME = 3;
VIDEO_FRAME = 4;
INPUT_EVENT = 5;
CLIPBOARD = 6;
FILE_TRANSFER = 7;
CLOSE = 8;
PING = 9;
PONG = 10;
PUNCH_HOLE = 11;
RELAY_RESPONSE = 12;
}
```
### 打洞消息
```protobuf
message PunchHole {
string peer_id = 1; // 对端 ID
bytes socket_addr = 2; // 序列化地址
int32 socket_addr_version = 3; // 地址版本
int32 peer_port = 4; // 对端端口
bool force_relay = 5; // 强制中继
int32 nat_type = 6; // NAT 类型
bytes local_addr = 7; // 本地地址
int32 local_port = 8; // 本地端口
int32 udp_port = 9; // UDP 端口
int32 conn_type = 10; // 连接类型
bytes socket_addr_v6 = 11; // IPv6 地址
bytes local_addr_v6 = 12; // IPv6 本地地址
}
```
### 文件传输(支持分片和断点续传)
```protobuf
message FileTransfer {
string id = 1; // 传输 ID
FileTransferAction action = 2;
bytes data = 3; // 文件数据
int64 offset = 4; // 文件偏移(支持断点续传)
bool compressed = 5; // 是否压缩
}
enum FileTransferAction {
FILE_TRANSFER_NONE = 0;
FILE_TRANSFER_REQUEST = 1; // 请求传输
FILE_TRANSFER_RESPONSE = 2; // 响应
FILE_TRANSFER_DATA = 3; // 数据块
FILE_TRANSFER_CANCEL = 4; // 取消
FILE_TRANSFER_DONE = 5; // 完成
}
// 文件元数据
message FileMeta {
string name = 1;
int64 size = 2;
int64 modified = 3;
bool is_directory = 4;
}
// 文件块
message FileChunk {
int32 id = 1;
int32 file_num = 2;
bytes data = 3;
bool compressed = 4;
int64 offset = 5;
}
```
### 输入事件
```protobuf
message InputEvent {
oneof event {
KeyEvent key = 1;
MouseEvent mouse = 2;
WheelEvent wheel = 3;
ClipboardEvent clipboard = 4;
}
}
message KeyEvent {
Direction direction = 1;
int32 code = 2; // 扫描码
int32 key = 3; // 虚拟键码
bool scan = 4;
bool raw = 5;
}
message MouseEvent {
Direction direction = 1;
int32 button = 2;
int32 x = 3;
int32 y = 4;
}
message WheelEvent {
Axis axis = 1;
int32 delta = 2;
}
enum Direction {
DOWN = 0;
UP = 1;
}
enum Axis {
HORIZONTAL = 0;
VERTICAL = 1;
}
```
## 在本项目中的使用
### 1. 客户端连接管理
```rust
// agent-client/src/connection/mod.rs
use hbb_common::{rendezvous_proto::*, ResultType};
use std::sync::Arc;
pub struct ConnectionManager {
mediator: Arc<RendezvousMediator>,
state: ConnectionState,
}
pub enum ConnectionState {
Disconnected,
Connecting,
Connected(PeerConnection),
Relay(RelayConnection),
}
impl ConnectionManager {
/// 连接到远程客户端
pub async fn connect(
&self,
peer_id: &str,
key: &str,
) -> ResultType<PeerConnection> {
// 1. 从 hbbs 获取对端信息
let peer_info = self.mediator.get_peer_info(peer_id).await?;
// 2. 尝试 P2P 直连
if let Some(conn) = self.try_p2p_connect(&peer_info, key).await {
return Ok(conn);
}
// 3. P2P 失败,降级到中继
self.connect_via_relay(&peer_info, key).await
}
/// 监听远程连接
pub async fn listen(&self, id: &str, key: &str) -> ResultType<()> {
self.mediator.start(id, key).await
}
}
```
### 2. 消息收发
```rust
// agent-client/src/connection/message.rs
use hbb_common::message_proto::{Message, MessageType};
pub struct MessageChannel {
stream: Box<dyn FramedStream>,
}
impl MessageChannel {
/// 发送消息
pub async fn send(&mut self, msg: &Message) -> ResultType<()> {
self.stream.send(msg).await?;
Ok(())
}
/// 接收消息
pub async fn recv(&mut self) -> ResultType<Option<Message>> {
match self.stream.next().await {
Some(Ok(msg)) => Ok(Some(msg)),
Some(Err(e)) => Err(e.into()),
None => Ok(None),
}
}
/// 发送聊天消息
pub async fn send_chat(&mut self, content: &str) -> ResultType<()> {
let msg = Message {
id: generate_id(),
type_: MessageType::MESSAGE_CHAT as i32,
payload: Some(ChatMessage {
content: content.to_string(),
timestamp: get_timestamp(),
}.into()),
..Default::default()
};
self.send(&msg).await
}
}
```
### 3. 连接状态指示
```rust
// agent-client/src/ui/status_bar.rs
use hbb_common::rendezvous_proto::PunchHole;
pub struct ConnectionIndicator {
state: ConnectionState,
last_heartbeat: Option<DateTime<Utc>>,
}
impl Render for ConnectionIndicator {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let status = match self.state {
ConnectionState::Connected => Status::Online,
ConnectionState::Relay => Status::Relay, // 中继模式
ConnectionState::Connecting => Status::Connecting,
ConnectionState::Disconnected => Status::Offline,
};
let color = match status {
Status::Online => cx.theme().colors.success,
Status::Relay => cx.theme().colors.warning,
Status::Connecting => cx.theme().colors.info,
Status::Offline => cx.theme().colors.error,
};
div()
.flex()
.items_center()
.gap_2()
.child(
div()
.w_3()
.h_3()
.rounded_full()
.bg(color)
)
.child(match status {
Status::Online => "已连接 (P2P)",
Status::Relay => "已连接 (中继)",
Status::Connecting => "连接中...",
Status::Offline => "未连接",
})
}
}
```
## 文件传输功能
### 文件分片传输
```rust
// agent-client/src/connection/file_transfer.rs
use hbb_common::message_proto::{FileTransfer, FileTransferAction};
const CHUNK_SIZE: usize = 64 * 1024; // 64KB 分片
pub struct FileTransferManager {
upload_jobs: HashMap<String, UploadJob>,
download_jobs: HashMap<String, DownloadJob>,
}
pub struct UploadJob {
file: File,
path: PathBuf,
offset: i64,
peer_id: String,
}
impl FileTransferManager {
/// 上传文件(支持断点续传)
pub async fn upload(&mut self, path: &Path, peer_id: &str) -> ResultType<()> {
let file = File::open(path).await?;
let metadata = file.metadata().await?;
let file_name = path.file_name().unwrap().to_string_lossy();
// 检查断点续传
let offset = self.get_checkpoint(&peer_id, &file_name).unwrap_or(0);
let job = UploadJob {
file,
path: path.to_path_buf(),
offset,
peer_id: peer_id.to_string(),
};
// 开始分片传输
self.upload_with_offset(job).await
}
async fn upload_with_offset(&mut self, mut job: UploadJob) -> ResultType<()> {
job.file.seek(SeekFrom::Start(job.offset as u64)).await?;
let mut buffer = vec![0u8; CHUNK_SIZE];
let mut offset = job.offset;
while let Ok(n) = job.file.read(&mut buffer).await {
if n == 0 { break; }
// 发送分片
let chunk = FileTransfer {
id: job.peer_id.clone(),
action: FileTransferAction::FILE_TRANSFER_DATA as i32,
data: buffer[..n].to_vec(),
offset,
compressed: false,
};
self.send_chunk(&chunk).await?;
// 保存断点
self.save_checkpoint(&job.peer_id, &job.path, offset);
offset += n as i64;
}
// 传输完成
self.send_done(&job.peer_id).await?;
Ok(())
}
}
```
## 可复用代码
### 核心复用模块
| 模块 | 路径 | 用途 |
|------|------|------|
| **hbb_common** | `libs/hbb_common/` | 消息编解码、网络工具、配置管理 |
| **scrap** | `libs/scrap/` | 屏幕捕获、视频编码 |
| **enigo** | `libs/enigo/` | 键盘鼠标输入模拟 |
| **clipboard** | `libs/clipboard/` | 跨平台剪贴板同步 |
### 推荐集成方式
```toml
[dependencies]
hbb_common = { path = "vendors/qiming-rustdesk/libs/hbb_common" }
```
## 关键设计模式
1. **双协议支持**UDP 直连优先,失败降级 TCP/WebSocket 中继
2. **NAT 打洞**:根据 NAT 类型自动选择连接策略
3. **心跳保活**EMA 动态调整心跳间隔
4. **断点续传**:支持文件传输中断后继续
5. **分片传输**:大文件分块传输,提高可靠性
## 与本项目的集成点
| 功能 | 使用模块 | 说明 |
|------|----------|------|
| 双向通信 | `hbb_common::message_proto` | 消息编解码 |
| P2P 直连 | `rendezvous_mediator` | NAT 穿透 |
| 中继传输 | `relay_connection` | 数据中转 |
| 文件传输 | `file_transfer` | 分片传输 |
| 输入模拟 | `enigo` | 远程控制 |
| 屏幕捕获 | `scrap` | 远程桌面 |

View File

@@ -0,0 +1,879 @@
# rcoder
## 项目概述
基于 ACP (Agent Protocol) 协议的 AI 代理开发平台,提供 Docker 容器化运行时和反向代理。本项目的核心模块 `agent_runner` 提供了完整的 Agent 生命周期管理能力,包括 Docker 容器管理、消息收发、超时自动销毁等功能。
**本地路径**: `vendors/rcoder`
## 目录结构
```
rcoder/
├── Cargo.toml # workspace 配置
├── crates/
│ ├── rcoder/ # 主应用服务
│ │ ├── src/
│ │ │ ├── lib.rs
│ │ │ ├── main.rs
│ │ │ ├── app.rs # 应用主逻辑
│ │ │ ├── router.rs # HTTP 路由
│ │ │ ├── state.rs # 应用状态
│ │ │ ├── error.rs # 错误处理
│ │ │ └── middlewares/ # 中间件
│ │ └── Cargo.toml
│ │
│ ├── agent_runner/ # Agent 运行时核心(最重要)
│ │ ├── src/
│ │ │ ├── lib.rs
│ │ │ ├── manager.rs # AgentWorkerManager
│ │ │ ├── worker.rs # Agent Worker
│ │ │ ├── adapter.rs # ACP 协议适配器
│ │ │ ├── types.rs # 类型定义
│ │ │ ├── message.rs # 消息处理
│ │ │ └── docker.rs # Docker 集成
│ │ └── Cargo.toml
│ │
│ ├── docker_manager/ # Docker 容器管理
│ │ ├── src/
│ │ │ ├── lib.rs
│ │ │ ├── manager.rs # DockerManager
│ │ │ ├── actor.rs # Actor 模式状态
│ │ │ ├── container.rs # 容器操作
│ │ │ └── network.rs # 网络管理
│ │ └── Cargo.toml
│ │
│ ├── acp_adapter/ # ACP 协议适配器
│ │ ├── src/
│ │ │ ├── lib.rs
│ │ │ ├── types.rs # ACP 类型定义
│ │ │ ├── client.rs # ACP 客户端
│ │ │ └── handler.rs # 消息处理
│ │ └── Cargo.toml
│ │
│ ├── shared_types/ # 共享类型定义
│ │ ├── src/
│ │ │ ├── lib.rs
│ │ │ ├── proto/ # gRPC proto 文件
│ │ │ │ └── agent.proto
│ │ │ └── types.rs
│ │ └── Cargo.toml
│ │
│ └── rcoder-proxy/ # 反向代理
│ ├── src/
│ │ ├── lib.rs
│ │ ├── proxy.rs
│ │ └── upstream.rs
│ └── Cargo.toml
└── Cargo.lock
```
## Agent Worker Manager (agent_runner)
这是 rcoder 的核心模块,负责管理 agent 的生命周期。
### 核心结构体
```rust
// crates/agent_runner/src/manager.rs
use tokio::sync::{mpsc, watch};
use std::sync::{Arc, Mutex};
use chrono::DateTime;
use dashmap::DashMap;
pub struct AgentWorkerManager {
// 任务发送器(使用 ArcSwap 支持热更新)
sender: ArcSwap<Option<mpsc::UnboundedSender<LocalSetAgentRequest>>>,
// 状态广播
state_tx: watch::Sender<WorkerState>,
// 最后心跳时间
last_heartbeat: Arc<Mutex<Option<DateTime<Utc>>>>,
// 状态变化时间
last_state_change: Arc<Mutex<DateTime<Utc>>>,
// 活跃请求追踪
active_requests: Arc<DashMap<String, DateTime<Utc>>>,
// 配置
config: AgentRunnerConfig,
}
// Worker 状态
pub enum WorkerState {
Starting,
Running,
Stopping,
Stopped,
}
impl Default for WorkerState {
fn default() -> Self {
WorkerState::Stopped
}
}
```
### 心跳超时检测
```rust
// crates/agent_runner/src/manager.rs
impl AgentWorkerManager {
/// 检查心跳是否超时
pub fn check_heartbeat_timeout(&self) -> bool {
let last_heartbeat_opt = {
let last = self.last_heartbeat.lock().expect("mutex poisoned");
*last // Copy 数据,立即释放锁
};
if let Some(timestamp) = last_heartbeat_opt {
// 正常运行时15秒无心跳视为超时
let elapsed = Utc::now() - timestamp;
elapsed.num_seconds() > 15
} else {
// 首次启动30秒内未收到心跳视为超时
let state_change = self.last_state_change.lock().expect("mutex poisoned");
let elapsed = Utc::now() - *state_change;
elapsed.num_seconds() > 30
}
}
/// 更新心跳时间
pub fn update_heartbeat(&self) {
let mut last = self.last_heartbeat.lock().expect("mutex poisoned");
*last = Some(Utc::now());
}
}
```
### Agent Worker 核心逻辑
```rust
// crates/agent_runner/src/worker.rs
pub struct AgentWorker {
config: AgentRunnerConfig,
receiver: mpsc::UnboundedReceiver<LocalSetAgentRequest>,
state_tx: watch::Sender<WorkerState>,
docker: DockerManager,
adapter: ACPAdapter,
}
impl AgentWorker {
pub async fn run(&mut self) {
tracing::info!("AgentWorker started");
self.state_tx.send(WorkerState::Running).unwrap();
loop {
tokio::select! {
Some(request) = self.receiver.recv() => {
self.handle_request(request).await;
}
_ = self.docker.health_check() => {
// Docker 健康检查
}
}
}
}
async fn handle_request(&mut self, request: LocalSetAgentRequest) {
match request {
LocalSetAgentRequest::Message { session_id, content } => {
self.handle_message(&session_id, &content).await;
}
LocalSetAgentRequest::Stop => {
self.shutdown().await;
return;
}
LocalSetAgentRequest::Spawn { config } => {
self.spawn_container(&config).await;
}
}
}
/// 启动 Docker 容器
async fn spawn_container(&mut self, config: &AgentConfig) -> Result<(), AgentError> {
// 1. 创建容器
let container_id = self.docker.create_container(CreateContainerOptions {
image: config.image.clone(),
env: config.env_vars.clone(),
Cmd: config.command.clone().unwrap_or_else(|| vec!["/bin/sh"]),
..Default::default()
}).await?;
// 2. 启动容器
self.docker.start_container(&container_id).await?;
// 3. 等待容器就绪
self.docker.wait_until_ready(&container_id).await?;
// 4. 建立 ACP 连接
self.adapter.connect(&container_id, config.acp_url).await?;
Ok(())
}
}
```
### ACP 协议消息类型
```rust
// crates/agent_runner/src/types.rs
use agent_client_protocol::{Message, MessageChunk};
// ACP 流更新事件
pub enum StreamUpdate {
UserMessageChunk {
session_id: String,
content: String,
},
AgentMessageChunk {
session_id: String,
content: String,
},
ToolCall {
session_id: String,
tool_call: ToolCall,
},
ToolCallUpdate {
session_id: String,
tool_call_update: ToolCallUpdate,
},
SessionStateChanged {
session_id: String,
new_state: SessionState,
message: String,
},
Plan {
session_id: String,
plan: Plan,
},
StepFinished {
session_id: String,
step_id: String,
},
SessionFinished {
session_id: String,
reason: String,
},
}
pub enum SessionState {
Initializing,
Connected,
Prompting,
Paused,
Closed,
Error(String),
}
pub struct Plan {
pub entries: Vec<PlanEntry>,
pub created_at: SystemTime,
pub status: PlanStatus,
}
pub enum PlanStatus {
Pending,
Approved,
Executing,
Finished,
Cancelled,
}
pub struct ToolCall {
pub id: String,
pub name: String,
pub arguments: serde_json::Value,
}
pub struct ToolCallUpdate {
pub id: String,
pub status: ToolCallStatus,
pub result: Option<serde_json::Value>,
pub error: Option<String>,
}
pub enum ToolCallStatus {
Pending,
Running,
Completed,
Failed,
Cancelled,
}
```
## Docker Manager (docker_manager)
Docker 容器管理模块,使用 Actor 模式管理容器状态,避免并发问题。
### 核心结构体
```rust
// crates/docker_manager/src/manager.rs
use bollard::{Docker, APIError};
use std::sync::Arc;
use tokio::sync::RwLock;
pub struct DockerManager {
docker: Docker, // Docker 客户端
config: DockerManagerConfig, // 配置
containers: ContainerStateHandle, // Actor 模式状态
main_network_name: Arc<RwLock<String>>, // 网络名称
}
pub struct DockerManagerConfig {
pub socket_path: Option<String>, // Docker socket 路径
pub network_name: String, // 网络名称
pub max_containers: usize, // 最大容器数
pub container_ttl: Duration, // 容器存活时间
pub auto_remove: bool, // 是否自动删除
}
```
### Actor 模式状态管理
```rust
// crates/docker_manager/src/container_state_actor.rs
// 使用 Actor 模式避免 DashMap 跨 await 持有锁导致的死锁
pub struct ContainerStateActor {
containers: HashMap<String, DockerContainerInfo>,
receiver: mpsc::Receiver<ContainerStateCommand>,
}
pub enum ContainerStateCommand {
Get { key: String, reply: oneshot::Sender<Option<DockerContainerInfo>> },
Insert { key: String, info: DockerContainerInfo },
Remove { key: String, reply: oneshot::Sender<Option<DockerContainerInfo>> },
List { reply: oneshot::Sender<Vec<DockerContainerInfo>> },
Keys { reply: oneshot::Sender<Vec<String>> },
RemoveIfContainerId { key: String, container_id: String, reply: oneshot::Sender<Option<DockerContainerInfo>> },
}
impl ContainerStateActor {
pub async fn run(&mut self) {
while let Some(cmd) = self.receiver.recv().await {
match cmd {
ContainerStateCommand::Get { key, reply } => {
let info = self.containers.get(&key).cloned();
reply.send(info).ok();
}
ContainerStateCommand::Insert { key, info } => {
self.containers.insert(key, info);
}
// ... 其他命令处理
}
}
}
}
```
### 容器创建流程
```rust
// crates/docker_manager/src/manager.rs
impl DockerManager {
pub async fn create_container(
&self,
config: DockerContainerConfig,
) -> DockerResult<DockerContainerInfo> {
let container_name = format!("{}-{}", self.config.network_name, config.name);
// 1. 清理同名旧容器
if let Ok(Some((existing_id, _, status, is_running))) =
self.find_container_realtime(&container_name).await
{
if is_running {
self.stop_container_by_id(&existing_id).await?;
}
self.remove_container_by_id(&existing_id).await?;
}
// 2. 拉取镜像
self.ensure_image_exists(&config.image).await?;
// 3. 构建挂载点
let mut mounts = Vec::new();
if !config.host_path.is_empty() {
mounts.push(Mount {
target: Some(config.container_path.clone()),
source: Some(config.host_path.clone()),
typ: Some(MountTypeEnum::BIND),
read_only: Some(false),
..Default::default()
});
}
// 4. 构建环境变量
let mut env_vars: Vec<String> = config.env_vars
.into_iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect();
// 5. 构建网络配置
let (networking_config, container_network_name) = if config.network_mode != "host" {
let main_network = self.get_main_network_name().await;
let mut endpoints = HashMap::new();
endpoints.insert(main_network.clone(), EndpointSettings {
aliases: Some(vec![container_name.clone()]),
..Default::default()
});
(Some(NetworkingConfig { endpoints_config: Some(endpoints) }), main_network)
} else {
(None, "host".to_string())
};
// 6. 创建并启动容器
let container_config = ContainerConfig {
image: config.image.clone(),
env: Some(env_vars),
cmd: config.command.map(|c| vec![c]),
host_config: Some(HostConfig {
mounts: Some(mounts),
network_mode: Some(container_network_name),
auto_remove: Some(self.config.auto_remove),
..Default::default()
}),
..Default::default()
};
let response = self.docker.create_container(
Some(CreateContainerOptions {
name: container_name.clone(),
..Default::default()
}),
container_config,
).await?;
self.docker.start_container(&response.id, None::<StartContainerOptions>).await?;
// 7. 健康检查
self.check_container_health(&response.id).await?;
// 8. 获取容器信息
let inspect = self.docker.inspect_container(&response.id, None).await?;
Ok(DockerContainerInfo {
id: response.id,
name: container_name,
image: config.image,
status: "running".to_string(),
ip_address: inspect.network_settings?.ip_address,
created_at: Utc::now(),
})
}
/// 等待容器就绪
pub async fn wait_until_ready(&self, container_id: &str) -> Result<(), DockerError> {
let timeout = Duration::from_secs(60);
let start_time = Utc::now();
loop {
// 检查容器状态
let info = self.inspect_container(container_id).await?;
if info.state.as_ref().map(|s| &s.status) == Some(&ContainerStateStatusEnum::RUNNING) {
// 检查健康检查
if let Some(health) = &info.state.as_ref().unwrap().healthcheck {
if health.status == ContainerHealthStatusEnum::HEALTHY {
return Ok(());
}
} else {
return Ok(());
}
}
// 超时检查
if (Utc::now() - start_time).num_seconds() > timeout.as_secs() as i64 {
return Err(DockerError::Timeout);
}
tokio::time::sleep(Duration::from_millis(500)).await;
}
}
}
```
## 超时自动销毁机制
```rust
// crates/agent_runner/src/proxy_agent/cleanup_task.rs
pub struct CleanupConfig {
pub idle_timeout: Duration, // 闲置超时默认3分钟
pub cleanup_interval: Duration, // 清理间隔默认30秒
}
impl Default for CleanupConfig {
fn default() -> Self {
Self {
idle_timeout: Duration::from_secs(3 * 60), // 3分钟
cleanup_interval: Duration::from_secs(30), // 30秒
}
}
}
// 清理逻辑:基于 RAII 模式
async fn cleanup_idle_agents(&mut self) -> Result<CleanupStats> {
for entry in AGENT_REGISTRY.iter_agents() {
let agent_info = entry.value();
// 只清理 Idle 状态的 agent
if agent_info.status == AgentStatus::Idle
&& self.is_agent_idle_timeout(agent_info.last_activity, current_time)
{
// 从 Registry 中移除AgentLifecycleGuard 自动清理资源
AGENT_REGISTRY.remove_by_project(project_id);
}
}
}
```
## 环境变量和 PATH 管理
```rust
// crates/agent_runner/src/docker.rs
pub fn build_environment(
node_path: &Path,
npm_global_path: &Path,
extra_env: &HashMap<String, String>,
) -> Vec<String> {
let mut env = vec![
// Node.js 环境
format!("PATH={}:{}:{}",
node_path.join("bin").to_string_lossy(),
npm_global_path.join("bin").to_string_lossy(),
std::env::var("PATH").unwrap_or_default()
),
format!("NODE_PATH={}", node_path.join("lib").to_string_lossy()),
format!("NPM_CONFIG_PREFIX={}", npm_global_path.to_string_lossy()),
];
// 添加额外的环境变量
for (k, v) in extra_env {
env.push(format!("{}={}", k, v));
}
env
}
// 在容器启动时设置环境变量
pub async fn start_container_with_env(
&self,
config: &AgentConfig,
) -> Result<String, AgentError> {
let env = build_environment(
&config.node_path,
&config.npm_global_path,
&config.extra_env,
);
let container_id = self.docker.create_container(CreateContainerOptions {
image: config.image.clone(),
env: Some(env),
Cmd: config.command.clone().unwrap_or_default(),
..Default::default()
}).await?;
self.docker.start_container(&container_id, None).await?;
Ok(container_id)
}
```
## Node.js 自动安装
```rust
// crates/agent_runner/src/installer/node_installer.rs
pub struct NodeInstaller {
install_dir: PathBuf,
platform: Platform,
}
pub enum Platform {
Windows,
MacOSIntel,
MacOSAppleSilicon,
Linux,
}
impl NodeInstaller {
/// 检测系统是否已有 Node.js
pub async fn detect_system_node(&self) -> Option<Version> {
// 检查系统 PATH
if let Ok(path) = which("node") {
if let Ok(output) = Command::new(&path)
.arg("--version")
.output()
.await
{
if output.status.success() {
let version_str = String::from_utf8_lossy(&output.stdout);
return Version::parse(&version_str.trim_start_matches('v')).ok();
}
}
}
None
}
/// 下载并安装 Node.js
pub async fn download_and_install(&self, version: &Version) -> Result<(), InstallationError> {
let url = self.get_download_url(version);
let archive_path = self.install_dir.join("cache").join(format!("node-{}.tar.xz", version));
// 1. 下载
if !archive_path.exists() {
self.download(&url, &archive_path).await?;
}
// 2. 解压
let extract_dir = self.install_dir.join("node").join(version.to_string());
self.extract(&archive_path, &extract_dir).await?;
// 3. 创建 symlink
let bin_path = extract_dir.join("bin");
let symlink_path = self.install_dir.join("bin").join("node");
if symlink_path.exists() {
std::fs::remove_file(&symlink_path)?;
}
std::os::unix::fs::symlink(&bin_path.join("node"), &symlink_path)?;
Ok(())
}
fn get_download_url(&self, version: &Version) -> String {
match self.platform {
Platform::Windows => format!(
"https://nodejs.org/dist/v{}/node-v{}-win-x64.zip",
version, version
),
Platform::MacOSIntel => format!(
"https://nodejs.org/dist/v{}/node-v{}-darwin-x64.tar.gz",
version, version
),
Platform::MacOSAppleSilicon => format!(
"https://nodejs.org/dist/v{}/node-v{}-darwin-arm64.tar.gz",
version, version
),
Platform::Linux => format!(
"https://nodejs.org/dist/v{}/node-v{}-linux-x64.tar.xz",
version, version
),
}
}
}
```
## ACP 协议适配器
```rust
// crates/acp_adapter/src/client.rs
use agent_client_protocol::{Client, Message as AcpMessage};
pub struct ACPAdapter {
client: Option<Client<WebSocketStream<Upgraded>>>,
session_id: String,
pending_requests: Arc<DashMap<String, oneshot::Sender<Result<String>>>>,
}
impl ACPAdapter {
/// 连接到 Agent 容器
pub async fn connect(
&mut self,
container_id: &str,
url: &str,
) -> Result<(), AdapterError> {
// 获取容器 IP
let container_ip = self.get_container_ip(container_id).await?;
// 建立 WebSocket 连接
let stream = WebSocket::connect(url).await?;
let (ws_stream, _) = stream.split();
let (write, read) = ws_stream.split();
// 创建 ACP 客户端
self.client = Some(Client::new(read, write));
// 生成会话 ID
self.session_id = uuid::Uuid::new_v4().to_string();
Ok(())
}
/// 发送消息
pub async fn send_message(&self, session_id: &str, content: &str) {
if let Some(client) = &self.client {
let message = AcpMessage::user_message_chunk(session_id, content);
client.send(message).await;
}
}
/// 接收消息
pub async fn receive(&self) -> Option<StreamUpdate> {
if let Some(client) = &self.client {
if let Some(msg) = client.recv().await {
Some(self.convert_message(msg))
} else {
None
}
} else {
None
}
}
}
```
## 在本项目中的使用
### Agent 依赖管理界面
```rust
// agent-client/src/ui/dependency_management.rs
use crate::agent::AgentManager;
pub struct DependencyManager {
agent_manager: Arc<AgentManager>,
node_installer: Arc<NodeInstaller>,
}
impl DependencyManager {
/// 检查所有依赖状态
pub async fn check_all_dependencies(&self) -> Vec<DependencyStatus> {
let mut statuses = vec![];
// 检查 Node.js
statuses.push(self.check_node().await);
// 检查 npm
statuses.push(self.check_npm().await);
// 检查 opencode
statuses.push(self.check_agent("opencode").await);
// 检查 claude-code
statuses.push(self.check_agent("@anthropic-ai/claude-code").await);
statuses
}
async fn check_node(&self) -> DependencyStatus {
// 1. 先检查系统全局
if let Some(version) = self.node_installer.detect_system_node().await {
return DependencyStatus {
name: "Node.js",
version: Some(version.to_string()),
source: DependencySource::System,
status: DependencyStatus::Installed,
action: None,
};
}
// 2. 检查客户端目录
if let Some(version) = self.detect_local_node().await {
return DependencyStatus {
name: "Node.js",
version: Some(version.to_string()),
source: DependencySource::Local,
status: DependencyStatus::Installed,
action: Some(DependencyAction::Update),
};
}
// 3. 未安装
DependencyStatus {
name: "Node.js",
version: None,
source: DependencySource::None,
status: DependencyStatus::NotInstalled,
action: Some(DependencyAction::Install),
}
}
}
pub enum DependencySource {
System,
Local,
None,
}
pub enum DependencyStatus {
Installed,
Installing,
Failed,
NotInstalled,
NeedsUpdate,
}
```
### 安装目录结构
```
<APP_DATA_DIR>/ # 客户端应用数据目录
├── tools/
│ ├── node/ # Node.js 安装目录
│ │ ├── v20.10.0/
│ │ │ ├── bin/
│ │ │ │ ├── node
│ │ │ │ └── npm
│ │ │ └── lib/
│ │ └── current -> v20.10.0 # 当前版本 symlink
│ ├── npm-global/ # npm 全局安装目录(隔离)
│ │ ├── bin/
│ │ │ ├── opencode
│ │ │ └── claude
│ │ └── lib/
│ └── versions.json # 已安装工具的版本记录
├── config/ # 配置文件
├── logs/ # 日志文件
└── cache/ # 缓存文件(下载的安装包等)
```
## 关键设计模式
1. **Actor 模式**: DockerManager 使用 Actor 模式管理容器状态,避免死锁
2. **ArcSwap**: 热更新配置而不需要重启
3. **DashMap**: 高并发场景下的线程安全 HashMap
4. **Watch 通道**: 状态变更广播
5. **RAII 清理**: Agent 闲置超时自动销毁
6. **隔离安装**: Node.js 和 Agent 工具安装到独立目录,不污染全局环境
## 可复用代码
| 模块 | 路径 | 用途 |
|------|------|------|
| **agent_runner** | `crates/agent_runner/` | Agent 生命周期管理 |
| **docker_manager** | `crates/docker_manager/` | Docker 容器操作 |
| **acp_adapter** | `crates/acp_adapter/` | ACP 协议客户端 |
## 与本项目的集成
```
agent-client
├── UI 层
│ ├── 依赖管理界面 (DataTable, Badge, Button)
│ ├── 安装进度弹窗 (Dialog, Progress)
│ └── 设置界面 (Form, Switch, Input)
└── Agent 运行时
├── rcoder/agent_runner # Agent 生命周期管理
│ │
│ ├── Docker Manager # 容器管理
│ │ └── opencode 容器
│ │
│ └── ACP Adapter # ACP 协议通信
│ │
│ └── opencode ACP 端口
└── Node.js 安装器 # 环境隔离安装
```

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,523 @@
# tray-icon
## 项目概述
跨平台系统托盘图标库,基于 muda 库实现,支持 Windows、macOS 和 Linux。
**GitHub**: https://github.com/tauri-apps/tray-icon.git
**本地路径**: `vendors/tray-icon`
## 目录结构
```
tray-icon/
├── src/
│ ├── lib.rs # 公共 API
│ ├── icon.rs # 图标处理
│ ├── menu.rs # 菜单构建
│ └── platform_impl/ # 平台特定实现
│ ├── mod.rs
│ ├── windows.rs
│ ├── macos.rs
│ └── linux.rs
├── Cargo.toml
└── README.md
```
## 核心依赖
```toml
[dependencies]
muda = "1.0" # 跨平台菜单库
crossbeam-channel = "0.5"
once_cell = "1.18"
thiserror = "1.0"
# 平台依赖
objc2 = { version = "0.5", optional = true }
gtk = { version = "0.18", optional = true, features = ["v3_22"] }
libappindicator = { version = "0.8", optional = true }
windows = { version = "0.48", optional = true }
```
## 核心 API
### TrayIconBuilder
```rust
// lib.rs
pub struct TrayIconBuilder {
id: TrayIconId,
attrs: TrayIconAttributes,
}
impl TrayIconBuilder {
pub fn new() -> Self;
/// 设置托盘图标
pub fn with_icon(mut self, icon: Icon) -> Self;
/// 设置提示文本
pub fn with_tooltip<S: AsRef<str>>(self, s: S) -> Self;
/// 设置标题macOS
pub fn with_title<S: AsRef<str>>(self, title: S) -> Self;
/// 设置上下文菜单
pub fn with_menu(mut self, menu: Box<dyn menu::ContextMenu>) -> Self;
/// 左键点击显示菜单(默认右键)
pub fn with_menu_on_left_click(self, enable: bool) -> Self;
/// 设置临时目录路径
pub fn with_temp_dir_path<P: AsRef<Path>>(self, s: P) -> Self;
/// 构建托盘图标
pub fn build(self) -> Result<TrayIcon>;
}
```
### TrayIcon
```rust
pub struct TrayIcon {
id: TrayIconId,
tray: Rc<RefCell<platform_impl::TrayIcon>>,
}
impl TrayIcon {
/// 设置图标
pub fn set_icon(&self, icon: Option<Icon>) -> Result<()>;
/// 设置菜单
pub fn set_menu(&self, menu: Option<Box<dyn menu::ContextMenu>>);
/// 设置提示文本
pub fn set_tooltip<S: AsRef<str>>(&self, tooltip: Option<S>) -> Result<()>;
/// 设置可见性
pub fn set_visible(&self, visible: bool) -> Result<()>;
/// 获取托盘区域矩形
pub fn rect(&self) -> Option<Rect>;
}
```
### TrayIconEvent
```rust
pub enum TrayIconEvent {
Click {
id: TrayIconId,
position: Position,
rect: Rect,
button: MouseButton,
button_state: ButtonState,
},
DoubleClick {
id: TrayIconId,
position: Position,
rect: Rect,
button: MouseButton,
},
Enter {
id: TrayIconId,
position: Position,
rect: Rect,
},
Move {
id: TrayIconId,
position: Position,
rect: Rect,
},
Leave {
id: TrayIconId,
position: Position,
rect: Rect,
},
}
impl TrayIconEvent {
/// 获取事件接收器
pub fn receiver() -> &'a TrayIconEventReceiver;
/// 设置事件处理函数
pub fn set_event_handler<F>(f: Option<F>)
where
F: Fn(TrayIconEvent, &dyn TrayIconHandle) + Send + 'static;
}
```
## 使用示例
```rust
use tray_icon::{TrayIconBuilder, icon::Icon, menu::MenuBuilder};
fn main() -> Result<()> {
// 加载图标
let icon = Icon::from_resource(1, None);
// 创建菜单
let menu = MenuBuilder::new()
.item("Show Window", "show_window", true, None)?
.separator()
.item("Settings", "settings", true, None)?
.separator()
.quit("Quit")
.build()?;
// 创建托盘图标
let _tray_icon = TrayIconBuilder::new()
.with_icon(icon)
.with_tooltip("My App")
.with_menu(Box::new(menu))
.with_on_tray_event(|event, tray| {
match event {
TrayIconEvent::Click { button, .. } => {
if button == MouseButton::Left {
show_window();
}
}
TrayIconEvent::MenuItemClick { id, .. } => {
match id.as_ref() {
"quit" => std::process::exit(0),
"settings" => open_settings(),
"show_window" => show_window(),
_ => {}
}
}
_ => {}
}
})
.build()?;
Ok(())
}
```
## 与 agent-client 集成场景
### 场景1完整的托盘管理器
```rust
// agent-client 托盘管理器
use tray_icon::{TrayIconBuilder, icon::Icon, menu::{Menu, MenuItem, MenuBuilder}};
use std::sync::atomic::{AtomicBool, Ordering};
pub struct TrayManager {
// 托盘图标
tray_icon: Option<TrayIcon>,
// 是否显示主窗口
window_visible: AtomicBool,
// 连接状态
connection_state: Arc<dyn ConnectionStatusProvider>,
// 应用退出标志
exit_flag: Arc<AtomicBool>,
}
impl TrayManager {
pub fn new(
connection_state: Arc<dyn ConnectionStatusProvider>,
exit_flag: Arc<AtomicBool>,
) -> Result<Self> {
let icon = Self::load_icon()?;
let menu = MenuBuilder::new()
.item("Show Window", "show_window", true, None)?
.separator()
.item("Agent Status", "status", true, None)?
.item("Open Settings", "settings", true, None)?
.separator()
.item("Quit", "quit", true, None)?
.build()?;
let tray_icon = TrayIconBuilder::new()
.with_icon(icon)
.with_tooltip("qiming-agent")
.with_menu(Box::new(menu))
.with_menu_on_left_click(true) // 左键也显示菜单
.build()?;
Ok(Self {
tray_icon: Some(tray_icon),
window_visible: AtomicBool::new(true),
connection_state,
exit_flag,
})
}
fn load_icon() -> Result<Icon> {
// 从资源文件加载图标
let icon_data = include_bytes!("../../assets/tray_icon.png");
let image = image::load_from_memory(icon_data)?
.to_rgba8()
.into_raw();
Icon::from_rgba(icon_data.to_vec(), 32, 32)
}
/// 设置托盘事件处理
pub fn setup_event_handler(&self) {
TrayIconEvent::set_event_handler(Some(move |event, _tray| {
match event {
TrayIconEvent::Click { button, .. } => {
if button == MouseButton::Left {
// 左键点击切换窗口可见性
// 发送事件到主窗口
}
}
TrayIconEvent::MenuItemClick { id, .. } => {
match id.as_ref() {
"show_window" => {
// 显示主窗口
}
"settings" => {
// 打开设置窗口
}
"quit" => {
// 退出应用
}
_ => {}
}
}
_ => {}
}
}));
}
/// 更新托盘图标(根据连接状态)
pub fn update_icon(&self, status: &ConnectionState) {
let icon = match status {
ConnectionState::Connected { .. } => self.load_connected_icon(),
ConnectionState::Connecting => self.load_loading_icon(),
ConnectionState::Disconnected { .. } => self.load_disconnected_icon(),
ConnectionState::Error { .. } => self.load_error_icon(),
};
if let Some(tray) = &self.tray_icon {
tray.set_icon(Some(icon)).ok();
}
}
/// 更新托盘提示文本
pub fn update_tooltip(&self, text: &str) {
if let Some(tray) = &self.tray_icon {
tray.set_tooltip(Some(text)).ok();
}
}
/// 销毁托盘
pub fn destroy(&mut self) {
if let Some(tray) = self.tray_icon.take() {
tray.set_visible(false).ok();
}
}
}
```
### 场景2托盘菜单与主窗口通信
```rust
// 使用 channel 进行托盘事件通信
use tokio::sync::mpsc;
pub enum TrayEvent {
ShowWindow,
OpenSettings,
Quit,
}
pub struct TrayEventChannel {
sender: mpsc::UnboundedSender<TrayEvent>,
receiver: Arc<Mutex<mpsc::UnboundedReceiver<TrayEvent>>>,
}
impl TrayEventChannel {
pub fn new() -> Self {
let (sender, receiver) = mpsc::unbounded_channel();
Self {
sender,
receiver: Arc::new(Mutex::new(receiver)),
}
}
pub fn sender(&self) -> mpsc::UnboundedSender<TrayEvent> {
self.sender.clone()
}
pub fn receiver(&self) -> Arc<Mutex<mpsc::UnboundedReceiver<TrayEvent>>> {
self.receiver.clone()
}
}
/// 在托盘事件中发送消息
fn setup_tray_events(channel: &TrayEventChannel) {
TrayIconEvent::set_event_handler(Some(move |event, _tray| {
if let TrayIconEvent::MenuItemClick { id, .. } = event {
let event = match id.as_ref() {
"show_window" => TrayEvent::ShowWindow,
"settings" => TrayEvent::OpenSettings,
"quit" => TrayEvent::Quit,
_ => return,
};
channel.sender().send(event).ok();
}
}));
}
/// 在主窗口中监听托盘事件
async fn handle_tray_events(channel: &TrayEventChannel, window: &WindowHandle) {
let mut receiver = channel.receiver().lock().await;
while let Some(event) = receiver.recv().await {
match event {
TrayEvent::ShowWindow => {
window.activate().ok();
}
TrayEvent::OpenSettings => {
// 打开设置窗口
}
TrayEvent::Quit => {
// 退出应用
std::process::exit(0);
}
}
}
}
```
### 场景3动态托盘菜单
```rust
// 根据状态动态更新菜单
pub struct DynamicTrayMenu {
menu: Menu,
status_item: MenuItem,
settings_item: MenuItem,
}
impl DynamicTrayMenu {
pub fn new() -> Result<Self> {
let menu = MenuBuilder::new()
.item("Show Window", "show_window", true, None)?
.separator()
.item("Status: Disconnected", "status", true, None)?
.item("Open Settings", "settings", true, None)?
.separator()
.item("Quit", "quit", true, None)?
.build()?;
// 获取子菜单项的引用
let status_item = menu.items().find(|item| item.id() == "status").unwrap();
let settings_item = menu.items().find(|item| item.id() == "settings").unwrap();
Ok(Self {
menu,
status_item,
settings_item,
})
}
/// 更新状态文本
pub fn update_status(&mut self, status: &ConnectionState) {
let text = match status {
ConnectionState::Connected { mode, latency } => {
match mode {
ConnectionMode::P2P => format!("Status: Connected (P2P) - {}ms", latency),
ConnectionMode::Relay => format!("Status: Connected (Relay) - {}ms", latency),
}
}
ConnectionState::Connecting => "Status: Connecting...".into(),
ConnectionState::Disconnected { .. } => "Status: Disconnected".into(),
ConnectionState::Error { .. } => "Status: Error".into(),
};
// 更新菜单项文本(如果支持)
// self.status_item.set_text(Some(&text));
}
/// 根据连接状态启用/禁用菜单项
pub fn update_items(&mut self, connected: bool) {
// 如果已连接,可能禁用某些选项
// 如果未连接,可能启用重连选项
}
}
```
### 场景4托盘图标动画
```rust
// 连接状态动画图标
pub struct AnimatedTrayIcon {
frames: Vec<Icon>,
current_frame: usize,
timer: Option<JoinHandle<()>>,
}
impl AnimatedTrayIcon {
pub fn new_loading_animation() -> Result<Self> {
// 创建加载动画帧
let frames = (0..8).map(|i| {
let degrees = i * 45;
Self::create_rotated_icon(degrees)
}).collect::<Result<Vec<_>>>()?;
Ok(Self {
frames,
current_frame: 0,
timer: None,
})
}
/// 开始动画
pub fn start_animation(&mut self, tray: &TrayIcon) {
let tray = tray.clone();
self.timer = Some(tokio::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_millis(100));
loop {
interval.tick().await;
// 下一帧
}
}));
}
/// 停止动画
pub fn stop_animation(&mut self) {
if let Some(timer) = self.timer.take() {
timer.abort();
}
}
fn create_rotated_icon(degrees: u32) -> Result<Icon> {
// 创建旋转后的图标
// 使用图像处理库生成旋转帧
Ok(Icon::from_rgba(vec![], 32, 32).unwrap())
}
}
```
## 在本项目中的使用
用于实现 agent-client 的任务栏常驻和托盘菜单功能:
```
agent-client
├── tray-icon (托盘图标)
│ │
│ ├── 托盘图标显示 (根据连接状态变化)
│ ├── 右键菜单 (显示窗口、设置、退出)
│ ├── 左键点击 (切换窗口可见性)
│ └── 动态提示文本 (显示连接状态)
└── muda (菜单系统)
├── Show Window
├── Agent Status (只读)
├── Open Settings
└── Quit
```