Initial commit: Update project structure and add new features

This commit is contained in:
2026-05-10 19:47:27 +08:00
parent b4fa80ba82
commit 675539eada
64 changed files with 6777 additions and 1133 deletions
+2943 -31
View File
File diff suppressed because it is too large Load Diff
+7 -1
View File
@@ -22,4 +22,10 @@ tauri = { version = "2", features = [] }
tauri-plugin-opener = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tauri-plugin-sql = { version = "2", features = ['sqlite'] }
tauri-plugin-notification = "2"
iroh = { version = "0.97", features = ["address-lookup-mdns"] }
iroh-tickets = "0.4"
tokio = "1.50.0"
anyhow = "1.0.102"
chrono = "0.4"
+161
View File
@@ -0,0 +1,161 @@
# LocalFinder 实现说明
## 📋 概述
已成功构建 `LocalFinder` 结构的基础框架,用于 Iroh 的局域网设备发现功能。由于 Iroh v0.97 API 发生重大变更,当前实现为占位符版本。
## ✅ 已完成的工作
### 1. 文件结构
创建了以下文件:
- **`src/iroh/local_find.rs`** - LocalFinder 核心实现
- **`src/iroh/mod.rs`** - Iroh 模块导出
- **Tauri Commands** - 在 `handlers/tauri_handlers.rs` 中添加了 5 个新的命令处理器
### 2. LocalFinder 结构
```rust
pub struct LocalFinder {
node_id: String,
}
```
#### 已实现的方法:
- `new()` - 创建局域网发现管理器(占位符)
- `local_id()` - 获取本地节点 ID
### 3. Tauri Commands
已添加以下命令供前端调用:
1. **`initialize_local_finder()`** - 初始化局域网发现管理器
2. **`start_discovery()`** - 开始设备发现(占位符)
3. **`stop_discovery()`** - 停止设备发现(占位符)
4. **`get_local_node_id()`** - 获取本地节点 ID
5. **`get_discovered_peers()`** - 获取发现的设备列表(占位符)
### 4. 集成到应用
- ✅ 更新了 `lib.rs` 导出 `LocalFinder`
- ✅ 在 Tauri 应用中注册了 `LocalFinder` 状态管理
- ✅ 将所有新 commands 添加到 invoke_handler
## ⚠️ 当前限制
### Iroh API 变更问题
Iroh v0.97 的 API 与早期版本有很大不同,主要问题包括:
1. **`Endpoint::builder()`** - 现在需要 `Preset` trait 参数
2. **`SecretKey::generate()`** - 需要特定类型的 RNG
3. **`MdnsAddressLookup`** - MDNS 发现 API 已变更
4. **依赖冲突** - `rand``rand_core` 版本兼容性问题
### 占位符实现
当前代码使用占位符实现,仅用于演示结构和接口设计。
## 🔧 下一步工作
### 需要查阅的文档
1. **Iroh v0.97 官方文档** - https://docs.rs/iroh/latest/iroh/
2. **Iroh GitHub 仓库** - https://github.com/n0-computer/iroh
3. **Iroh Examples** - 查看最新的示例代码
### 待实现的功能
1. **正确的 Endpoint 初始化**
```rust
// 需要找到正确的方式
let endpoint = Endpoint::builder(preset).bind().await?;
```
2. **SecretKey 生成**
```rust
// 需要使用正确的 RNG
let secret_key = SecretKey::generate(/* correct rng */);
```
3. **MDNS 发现集成**
```rust
// 需要查阅新的 API
let mdns = MdnsAddressLookup::new()?;
endpoint.discovery().add(mdns)?;
```
4. **设备连接和管理**
- `connect_to_peer()` - 连接到发现的设备
- `disconnect_peer()` - 断开连接
- `get_known_peers()` - 获取已知设备列表
## 📝 使用示例(当前占位符)
### Rust 代码
```rust
use crate::iroh::LocalFinder;
// 创建 LocalFinder
let finder = LocalFinder::new().await?;
// 获取节点 ID
let node_id = finder.local_id();
println!("Local node ID: {}", node_id);
```
### TypeScript/JavaScript 调用(前端)
```typescript
// 初始化
const nodeId = await invoke('initialize_local_finder');
console.log(nodeId);
// 获取本地节点 ID
const localId = await invoke('get_local_node_id');
console.log(localId);
// 开始发现(占位符)
await invoke('start_discovery', { durationSecs: 60 });
// 获取发现的设备(占位符)
const peers = await invoke('get_discovered_peers');
console.log(peers);
```
## 🎯 建议
由于 Iroh API 频繁变更,建议:
1. **暂时移除 Iroh 依赖** - 避免编译错误
2. **使用条件编译** - 仅在需要时启用 Iroh 功能
3. **关注 Iroh 更新** - 订阅 Iroh 的 release notes
4. **考虑替代方案** - 如果 Iroh 不稳定,可以考虑其他 P2P 库
## 📦 依赖配置
当前 `Cargo.toml` 中的相关依赖:
```toml
[dependencies]
iroh = { version = "0.97", features = ["address-lookup-mdns"] }
iroh-tickets = "0.4"
tokio = "1.50.0"
anyhow = "1.0.102"
```
## 📊 项目状态
-**LocalFinder 结构** - 完成
-**Tauri 集成** - 完成
-**Commands 实现** - 完成(占位符)
- ⚠️ **实际功能** - 等待 Iroh API 研究
---
**创建时间**: 2026-04-03
**最后更新**: 2026-04-03
**状态**: 占位符实现,等待 Iroh API 研究
Binary file not shown.
+7 -3
View File
@@ -2,9 +2,13 @@
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"windows": [
"main"
],
"permissions": [
"core:default",
"opener:default"
"opener:default",
"sql:default",
"notification:default"
]
}
}
Binary file not shown.
+65
View File
@@ -0,0 +1,65 @@
use tauri_plugin_sql::{Migration, MigrationKind};
/// 获取所有数据库迁移
pub fn get_migrations() -> Vec<Migration> {
vec![
// 版本 1: 创建初始表
Migration {
version: 1,
description: "create_initial_tables",
sql: r#"
-- 用户表
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 工作流表
CREATE TABLE IF NOT EXISTS workflows (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
status TEXT DEFAULT 'draft',
creator TEXT NOT NULL,
definition TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 流程节点表
CREATE TABLE IF NOT EXISTS workflow_nodes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
workflow_id INTEGER NOT NULL,
node_type TEXT NOT NULL,
position_x REAL NOT NULL,
position_y REAL NOT NULL,
data TEXT,
FOREIGN KEY (workflow_id) REFERENCES workflows(id) ON DELETE CASCADE
);
-- 流程边表
CREATE TABLE IF NOT EXISTS workflow_edges (
id INTEGER PRIMARY KEY AUTOINCREMENT,
workflow_id INTEGER NOT NULL,
source TEXT NOT NULL,
target TEXT NOT NULL,
label TEXT,
data TEXT,
FOREIGN KEY (workflow_id) REFERENCES workflows(id) ON DELETE CASCADE
);
-- 消息表
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT,
is_read BOOLEAN DEFAULT FALSE,
workflow_id INTEGER,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (workflow_id) REFERENCES workflows(id) ON DELETE SET NULL
);
"#,
kind: MigrationKind::Up,
},
]
}
+1
View File
@@ -0,0 +1 @@
pub mod migrations;
+3
View File
@@ -0,0 +1,3 @@
pub mod tauri_handlers;
pub use tauri_handlers::TauriMessageHandler;
+159
View File
@@ -0,0 +1,159 @@
use crate::router::MessageRouter;
use crate::iroh::LocalFinder;
use serde_json::Value;
use std::sync::Arc;
use tokio::sync::Mutex;
pub struct TauriMessageHandler {
#[allow(dead_code)]
router: Arc<Mutex<MessageRouter>>,
}
impl TauriMessageHandler {
pub fn new(router: MessageRouter) -> Self {
TauriMessageHandler {
router: Arc::new(Mutex::new(router)),
}
}
}
#[tauri::command]
pub async fn send_message(state: tauri::State<'_, Arc<Mutex<MessageRouter>>>, message: Value) -> Result<String, String> {
let router = state.lock().await;
match router.send(message).await {
Ok(_) => Ok("Message sent successfully".to_string()),
Err(e) => Err(format!("Failed to send message: {}", e)),
}
}
#[tauri::command]
pub fn get_supported_message_types() -> Result<Vec<String>, String> {
Ok(vec!["text".to_string(), "file".to_string()])
}
/// 初始化局域网发现
#[tauri::command]
pub async fn initialize_local_finder(
state: tauri::State<'_, Arc<Mutex<Option<LocalFinder>>>>,
) -> Result<String, String> {
let mut finder_opt = state.lock().await;
if finder_opt.is_some() {
return Ok("Local finder already initialized".to_string());
}
match LocalFinder::new().await {
Ok(finder) => {
let node_id = finder.local_id();
*finder_opt = Some(finder);
Ok(format!("Local finder initialized successfully. Node ID: {}", node_id))
}
Err(e) => Err(format!("Failed to initialize local finder: {}", e)),
}
}
/// 开始设备发现
#[tauri::command]
pub async fn start_discovery(
_state: tauri::State<'_, Arc<Mutex<Option<LocalFinder>>>>,
_duration_secs: u64,
) -> Result<String, String> {
Ok("Discovery functionality will be implemented in future versions".to_string())
}
/// 停止设备发现
#[tauri::command]
pub async fn stop_discovery(
_state: tauri::State<'_, Arc<Mutex<Option<LocalFinder>>>>,
) -> Result<String, String> {
Ok("Stop discovery functionality will be implemented in future versions".to_string())
}
/// 获取本地节点 ID
#[tauri::command]
pub async fn get_local_node_id(
state: tauri::State<'_, Arc<Mutex<Option<LocalFinder>>>>,
) -> Result<String, String> {
let finder_opt = state.lock().await;
if let Some(finder) = finder_opt.as_ref() {
Ok(finder.local_id())
} else {
Err("Local finder not initialized".to_string())
}
}
/// 获取发现的设备列表
#[tauri::command]
pub async fn get_discovered_peers(
state: tauri::State<'_, Arc<Mutex<Option<LocalFinder>>>>,
) -> Result<String, String> {
let finder_opt = state.lock().await;
if let Some(finder) = finder_opt.as_ref() {
Ok(format!("Local node ID: {}", finder.local_id()))
} else {
Err("Local finder not initialized".to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::plugins::TextMessagePlugin;
use serde_json::json;
#[tokio::test]
async fn test_send_message_success() {
let (sender, _receiver) = mpsc::channel(100);
let router = MessageRouter::builder()
.register_plugin(TextMessagePlugin::new())
.with_iroh_sender(sender)
.build();
let handler = TauriMessageHandler::new(router);
let message = json!({
"type": "text",
"to": "user123",
"from": "user456",
"content": "Test"
});
let result = handler.send_message(message).await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), "Message sent successfully");
}
#[tokio::test]
async fn test_send_message_failure() {
// 不设置 iroh_sender,导致发送失败
let router = MessageRouter::builder()
.register_plugin(TextMessagePlugin::new())
.build();
let handler = TauriMessageHandler::new(router);
let message = json!({
"type": "text",
"to": "user123",
"from": "user456",
"content": "Test"
});
let result = handler.send_message(message).await;
assert!(result.is_err());
}
#[test]
fn test_get_supported_message_types() {
let router = MessageRouter::builder().build();
let handler = TauriMessageHandler::new(router);
let result = handler.get_supported_message_types();
assert!(result.is_ok());
let types = result.unwrap();
assert!(types.contains(&"text".to_string()));
assert!(types.contains(&"file".to_string()));
}
}
+42
View File
@@ -0,0 +1,42 @@
/// 局域网发现管理器(占位符实现)
///
/// TODO: 需要查阅最新的 Iroh 文档来正确实现
/// Iroh v0.97 的 API 与早期版本有很大不同
pub struct LocalFinder {
node_id: String,
}
impl LocalFinder {
/// 创建新的局域网发现管理器
pub async fn new() -> anyhow::Result<Self> {
// 占位符实现 - 仅用于演示结构
Ok(LocalFinder {
node_id: "placeholder-node-id".to_string(),
})
}
/// 获取本地节点 ID
pub fn local_id(&self) -> String {
self.node_id.clone()
}
}
impl Drop for LocalFinder {
fn drop(&mut self) {
// 清理资源(占位符)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_local_finder_creation() {
let finder = LocalFinder::new().await;
assert!(finder.is_ok());
let finder = finder.unwrap();
assert!(!finder.local_id().is_empty());
}
}
+3
View File
@@ -0,0 +1,3 @@
pub mod local_find;
pub use local_find::LocalFinder;
+58 -25
View File
@@ -1,35 +1,68 @@
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name)
}
// #[tauri::command]
// fn something(state: tauri::State<String>) -> String {
// state.inner().clone()
// }
// #[cfg(test)]
// mod tests {
// use super::something;
// use tauri::Manager;
// #[test]
// pub fn test_something() {
// let app = tauri::test::mock_app();
// app.manage("something".to_string());
// assert_eq!(&something(app.state::<String>()), "something");
// }
// }
mod db;
mod iroh;
mod plugins;
mod router;
mod handlers;
pub use plugins::{MessagePlugin, PluginResult};
pub use router::MessageRouter;
pub use handlers::TauriMessageHandler;
pub use plugins::{FileMessagePlugin, TextMessagePlugin};
pub use iroh::LocalFinder;
use std::sync::{Arc, Mutex};
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
let migrations = db::migrations::get_migrations();
// 创建消息路由器
let (iroh_sender, iroh_receiver) = tokio::sync::mpsc::channel(100);
let router = MessageRouter::builder()
.register_plugin(FileMessagePlugin::new())
.register_plugin(TextMessagePlugin::new())
.with_iroh_sender(iroh_sender)
.build();
// 创建 Tauri 处理器和管理状态
let handler_state = Arc::new(Mutex::new(router));
// 创建局域网发现管理器
let local_finder_state: Arc<Mutex<Option<LocalFinder>>> = Arc::new(Mutex::new(None));
tauri::Builder::default()
.plugin(tauri_plugin_notification::init())
.plugin(
tauri_plugin_sql::Builder::new()
.add_migrations("sqlite:app.db", migrations)
.build(),
)
.plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![greet])
.invoke_handler(tauri::generate_handler![
handlers::tauri_handlers::send_message,
handlers::tauri_handlers::get_supported_message_types,
handlers::tauri_handlers::initialize_local_finder,
handlers::tauri_handlers::start_discovery,
handlers::tauri_handlers::stop_discovery,
handlers::tauri_handlers::get_local_node_id,
handlers::tauri_handlers::get_discovered_peers,
])
.manage(handler_state)
.manage(local_finder_state)
.setup(|app| {
// 启动 Iroh 接收处理任务
let _app_handle = app.handle();
let mut iroh_receiver = iroh_receiver;
tauri::async_runtime::spawn(async move {
while let Some(_binary_data) = iroh_receiver.recv().await {
println!("Received binary data from Iroh: {} bytes", _binary_data.len());
}
});
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
+130
View File
@@ -0,0 +1,130 @@
use super::{BinaryData, MessagePlugin, PluginResult};
use serde_json::{json, Value};
/// 文件消息插件
pub struct FileMessagePlugin;
impl FileMessagePlugin {
pub fn new() -> Self {
FileMessagePlugin {}
}
}
impl Default for FileMessagePlugin {
fn default() -> Self {
Self::new()
}
}
impl MessagePlugin for FileMessagePlugin {
fn get_plugin_type(&self) -> &str {
"file"
}
fn before_send(&self, json_data: &Value) -> PluginResult<BinaryData> {
let to = json_data.get("to")
.and_then(|v| v.as_str())
.ok_or("Missing 'to' field")?;
let from = json_data.get("from")
.and_then(|v| v.as_str())
.ok_or("Missing 'from' field")?;
let processed_data = json!({
"type": "file",
"to": to,
"from": from,
"timestamp": chrono::Utc::now().timestamp(),
});
Ok(serde_json::to_vec(&processed_data)?)
}
fn before_receive(&self, binary_data: &BinaryData) -> PluginResult<Value> {
let json_value: Value = serde_json::from_slice(binary_data)?;
Ok(json_value)
}
fn after_send(&self, _json_data: &Value) -> PluginResult<()> {
Ok(())
}
fn after_receive(&self, _binary_data: &BinaryData) -> PluginResult<()> {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_get_plugin_type() {
let plugin = FileMessagePlugin::new();
assert_eq!(plugin.get_plugin_type(), "file");
}
#[test]
fn test_before_send_valid_message() {
let plugin = FileMessagePlugin::new();
let message = json!({
"type": "file",
"to": "user123",
"from": "user456"
});
let result = plugin.before_send(&message);
assert!(result.is_ok());
let binary_data = result.unwrap();
let parsed: Value = serde_json::from_slice(&binary_data).unwrap();
assert_eq!(parsed["type"], "file");
assert_eq!(parsed["to"], "user123");
assert_eq!(parsed["from"], "user456");
assert!(parsed["timestamp"].is_number());
}
#[test]
fn test_before_send_missing_to_field() {
let plugin = FileMessagePlugin::new();
let message = json!({
"type": "file",
"from": "user456"
});
let result = plugin.before_send(&message);
assert!(result.is_err());
}
#[test]
fn test_before_send_missing_from_field() {
let plugin = FileMessagePlugin::new();
let message = json!({
"type": "file",
"to": "user123"
});
let result = plugin.before_send(&message);
assert!(result.is_err());
}
#[test]
fn test_before_receive() {
let plugin = FileMessagePlugin::new();
let original_message = json!({
"type": "file",
"to": "user123",
"from": "user456",
"timestamp": 1234567890
});
let binary_data = serde_json::to_vec(&original_message).unwrap();
let result = plugin.before_receive(&binary_data);
assert!(result.is_ok());
let received = result.unwrap();
assert_eq!(received["type"], "file");
}
}
+28
View File
@@ -0,0 +1,28 @@
use serde_json::Value;
use std::error::Error;
pub mod file_plugin;
pub mod text_plugin;
pub use file_plugin::FileMessagePlugin;
pub use text_plugin::TextMessagePlugin;
pub type PluginResult<T> = Result<T, Box<dyn Error>>;
pub type BinaryData = Vec<u8>;
pub trait MessagePlugin: Send + Sync {
/// 获取插件类型标识
fn get_plugin_type(&self) -> &str;
/// 发送消息前的处理
fn before_send(&self, json_data: &Value) -> PluginResult<BinaryData>;
/// 接收消息前的处理
fn before_receive(&self, binary_data: &BinaryData) -> PluginResult<Value>;
/// 发送消息后的处理
fn after_send(&self, json_data: &Value) -> PluginResult<()>;
/// 接收消息后的处理
fn after_receive(&self, binary_data: &BinaryData) -> PluginResult<()>;
}
+165
View File
@@ -0,0 +1,165 @@
use super::{BinaryData, MessagePlugin, PluginResult};
use serde_json::{json, Value};
/// 文本消息插件
pub struct TextMessagePlugin;
impl TextMessagePlugin {
pub fn new() -> Self {
TextMessagePlugin {}
}
}
impl Default for TextMessagePlugin {
fn default() -> Self {
Self::new()
}
}
impl MessagePlugin for TextMessagePlugin {
fn get_plugin_type(&self) -> &str {
"text"
}
fn before_send(&self, json_data: &Value) -> PluginResult<BinaryData> {
let to = json_data.get("to")
.and_then(|v| v.as_str())
.ok_or("Missing 'to' field")?;
let from = json_data.get("from")
.and_then(|v| v.as_str())
.ok_or("Missing 'from' field")?;
let content = json_data.get("content")
.and_then(|v| v.as_str())
.ok_or("Missing 'content' field")?;
let processed_data = json!({
"type": "text",
"to": to,
"from": from,
"content": content,
"timestamp": chrono::Utc::now().timestamp(),
});
Ok(serde_json::to_vec(&processed_data)?)
}
fn before_receive(&self, binary_data: &BinaryData) -> PluginResult<Value> {
let json_value: Value = serde_json::from_slice(binary_data)?;
Ok(json_value)
}
fn after_send(&self, _json_data: &Value) -> PluginResult<()> {
Ok(())
}
fn after_receive(&self, _binary_data: &BinaryData) -> PluginResult<()> {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_get_plugin_type() {
let plugin = TextMessagePlugin::new();
assert_eq!(plugin.get_plugin_type(), "text");
}
#[test]
fn test_before_send_valid_message() {
let plugin = TextMessagePlugin::new();
let message = json!({
"type": "text",
"to": "user123",
"from": "user456",
"content": "Hello, World!"
});
let result = plugin.before_send(&message);
assert!(result.is_ok());
let binary_data = result.unwrap();
let parsed: Value = serde_json::from_slice(&binary_data).unwrap();
assert_eq!(parsed["type"], "text");
assert_eq!(parsed["to"], "user123");
assert_eq!(parsed["from"], "user456");
assert_eq!(parsed["content"], "Hello, World!");
assert!(parsed["timestamp"].is_number());
}
#[test]
fn test_before_send_missing_to_field() {
let plugin = TextMessagePlugin::new();
let message = json!({
"type": "text",
"from": "user456",
"content": "Hello"
});
let result = plugin.before_send(&message);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Missing 'to' field"));
}
#[test]
fn test_before_send_missing_from_field() {
let plugin = TextMessagePlugin::new();
let message = json!({
"type": "text",
"to": "user123",
"content": "Hello"
});
let result = plugin.before_send(&message);
assert!(result.is_err());
}
#[test]
fn test_before_send_missing_content_field() {
let plugin = TextMessagePlugin::new();
let message = json!({
"type": "text",
"to": "user123",
"from": "user456"
});
let result = plugin.before_send(&message);
assert!(result.is_err());
}
#[test]
fn test_before_receive() {
let plugin = TextMessagePlugin::new();
let original_message = json!({
"type": "text",
"to": "user123",
"from": "user456",
"content": "Test message",
"timestamp": 1234567890
});
let binary_data = serde_json::to_vec(&original_message).unwrap();
let result = plugin.before_receive(&binary_data);
assert!(result.is_ok());
let received = result.unwrap();
assert_eq!(received["type"], "text");
assert_eq!(received["content"], "Test message");
}
#[test]
fn test_after_send_and_receive() {
let plugin = TextMessagePlugin::new();
let message = json!({"test": "data"});
let binary_data = vec![1, 2, 3];
assert!(plugin.after_send(&message).is_ok());
assert!(plugin.after_receive(&binary_data).is_ok());
}
}
+254
View File
@@ -0,0 +1,254 @@
use crate::plugins::{BinaryData, MessagePlugin};
use serde_json::Value;
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use tokio::sync::mpsc;
type MessageCallback = Box<dyn Fn(Value) + Send + Sync + 'static>;
/// 消息路由器
pub struct MessageRouter {
plugins: Arc<RwLock<HashMap<String, Arc<dyn MessagePlugin>>>>,
message_callback: Arc<RwLock<Option<MessageCallback>>>,
iroh_sender: Option<mpsc::Sender<BinaryData>>,
}
pub struct MessageRouterBuilder {
router: MessageRouter,
}
impl MessageRouter {
pub fn builder() -> MessageRouterBuilder {
MessageRouterBuilder {
router: MessageRouter {
plugins: Arc::new(RwLock::new(HashMap::new())),
message_callback: Arc::new(RwLock::new(None)),
iroh_sender: None,
},
}
}
/// 发送消息
pub async fn send(&self, message: Value) -> Result<(), Box<dyn std::error::Error>> {
let message_type = message.get("type")
.and_then(|v| v.as_str())
.ok_or("Message must have a 'type' field")?;
// 先获取插件并释放锁
let plugin = {
let plugins = self.plugins.read().map_err(|_| "Failed to read plugins")?;
let plugin = plugins.get(message_type)
.ok_or_else(|| format!("No plugin found for message type: {}", message_type))?;
Arc::clone(plugin)
};
// 在锁释放后处理消息
let processed_data = plugin.before_send(&message)?;
if let Some(sender) = &self.iroh_sender {
sender.send(processed_data).await?;
} else {
return Err("Iroh sender not initialized".into());
}
plugin.after_send(&message)?;
Ok(())
}
/// 处理接收到的消息
pub fn handle_received_message(&self, binary_data: BinaryData) -> Result<(), Box<dyn std::error::Error>> {
let json_value: Value = serde_json::from_slice(&binary_data)?;
let message_type = json_value.get("type")
.and_then(|v| v.as_str())
.ok_or("Received message must have a 'type' field")?;
// 先获取插件并释放锁
let plugin = {
let plugins = self.plugins.read().map_err(|_| "Failed to read plugins")?;
let plugin = plugins.get(message_type)
.ok_or_else(|| format!("No plugin found for message type: {}", message_type))?;
Arc::clone(plugin)
};
let processed_message = plugin.before_receive(&binary_data)?;
if let Some(callback) = &*self.message_callback.read().map_err(|_| "Failed to read callback")? {
callback(processed_message);
}
plugin.after_receive(&binary_data)?;
Ok(())
}
}
impl MessageRouterBuilder {
/// 注册插件
pub fn register_plugin<P>(self, plugin: P) -> Self
where
P: MessagePlugin + 'static,
{
let plugin_type = plugin.get_plugin_type().to_string();
self.router.plugins.write()
.expect("Failed to write plugins")
.insert(plugin_type, Arc::new(plugin));
self
}
/// 设置消息回调
pub fn on_message<F>(self, callback: F) -> Self
where
F: Fn(Value) + Send + Sync + 'static,
{
*self.router.message_callback.write()
.expect("Failed to write callback") = Some(Box::new(callback));
self
}
/// 设置 Iroh 发送器
pub fn with_iroh_sender(mut self, sender: mpsc::Sender<BinaryData>) -> Self {
self.router.iroh_sender = Some(sender);
self
}
/// 构建路由器
pub fn build(self) -> MessageRouter {
self.router
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::plugins::TextMessagePlugin;
use serde_json::json;
use std::sync::{Arc, Mutex};
#[test]
fn test_router_builder() {
let router = MessageRouter::builder()
.register_plugin(TextMessagePlugin::new())
.build();
let plugins = router.plugins.read().unwrap();
assert!(plugins.contains_key("text"));
}
#[test]
fn test_register_multiple_plugins() {
use crate::plugins::FileMessagePlugin;
let router = MessageRouter::builder()
.register_plugin(TextMessagePlugin::new())
.register_plugin(FileMessagePlugin::new())
.build();
let plugins = router.plugins.read().unwrap();
assert!(plugins.contains_key("text"));
assert!(plugins.contains_key("file"));
assert_eq!(plugins.len(), 2);
}
#[tokio::test]
async fn test_send_text_message() {
let (sender, _receiver) = mpsc::channel(100);
let router = MessageRouter::builder()
.register_plugin(TextMessagePlugin::new())
.with_iroh_sender(sender)
.build();
let message = json!({
"type": "text",
"to": "user123",
"from": "user456",
"content": "Test message"
});
let result = router.send(message).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_send_message_without_iroh_sender() {
let router = MessageRouter::builder()
.register_plugin(TextMessagePlugin::new())
.build();
let message = json!({
"type": "text",
"to": "user123",
"from": "user456",
"content": "Test"
});
let result = router.send(message).await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Iroh sender not initialized"));
}
#[tokio::test]
async fn test_send_message_with_unknown_type() {
let (sender, _receiver) = mpsc::channel(100);
let router = MessageRouter::builder()
.register_plugin(TextMessagePlugin::new())
.with_iroh_sender(sender)
.build();
let message = json!({
"type": "unknown",
"to": "user123",
"from": "user456"
});
let result = router.send(message).await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("No plugin found"));
}
#[test]
fn test_handle_received_message() {
let received_messages = Arc::new(Mutex::new(Vec::new()));
let received_clone = Arc::clone(&received_messages);
let router = MessageRouter::builder()
.register_plugin(TextMessagePlugin::new())
.on_message(move |msg| {
received_clone.lock().unwrap().push(msg);
})
.build();
let original_message = json!({
"type": "text",
"to": "user123",
"from": "user456",
"content": "Incoming message"
});
let binary_data = serde_json::to_vec(&original_message).unwrap();
let result = router.handle_received_message(binary_data);
assert!(result.is_ok());
let messages = received_messages.lock().unwrap();
assert_eq!(messages.len(), 1);
assert_eq!(messages[0]["type"], "text");
assert_eq!(messages[0]["content"], "Incoming message");
}
#[test]
fn test_handle_received_message_invalid_type() {
let router = MessageRouter::builder()
.register_plugin(TextMessagePlugin::new())
.build();
// 发送一个没有 type 字段的消息
let invalid_message = vec![1, 2, 3]; // 无效的 JSON
let result = router.handle_received_message(invalid_message);
assert!(result.is_err());
}
}
+3
View File
@@ -0,0 +1,3 @@
pub mod message_router;
pub use message_router::MessageRouter;
+3 -1
View File
@@ -14,7 +14,9 @@
{
"title": "rolegram",
"width": 800,
"height": 600
"height": 600,
"minWidth": 600,
"minHeight": 500
}
],
"security": {
Binary file not shown.
+213
View File
@@ -0,0 +1,213 @@
// 消息路由系统测试示例
// 这些代码展示了如何测试各个组件
#[cfg(test)]
mod integration_tests {
use serde_json::json;
use std::sync::{Arc, Mutex};
use tokio::sync::mpsc;
// 导入被测试的模块
use crate::plugins::{MessagePlugin, TextMessagePlugin, FileMessagePlugin};
use crate::router::MessageRouter;
use crate::handlers::TauriMessageHandler;
/// 集成测试:完整的消息发送流程
#[tokio::test]
async fn test_full_message_flow() {
// 1. 创建 channel
let (sender, mut receiver) = mpsc::channel(100);
// 2. 记录接收到的消息
let received_messages = Arc::new(Mutex::new(Vec::new()));
let received_clone = Arc::clone(&received_messages);
// 3. 创建路由器
let router = MessageRouter::builder()
.register_plugin(TextMessagePlugin::new())
.on_message(move |msg| {
received_clone.lock().unwrap().push(msg);
})
.with_iroh_sender(sender)
.build();
// 4. 发送消息
let message = json!({
"type": "text",
"to": "user123",
"from": "user456",
"content": "Integration test message"
});
let result = router.send(message.clone()).await;
assert!(result.is_ok());
// 5. 验证消息被发送到 channel
let sent_data = receiver.recv().await.unwrap();
let sent_json: serde_json::Value = serde_json::from_slice(&sent_data).unwrap();
assert_eq!(sent_json["type"], "text");
assert_eq!(sent_json["content"], "Integration test message");
// 6. 模拟接收消息
let binary_data = serde_json::to_vec(&sent_json).unwrap();
let result = router.handle_received_message(binary_data);
assert!(result.is_ok());
// 7. 验证回调被调用
let messages = received_messages.lock().unwrap();
assert_eq!(messages.len(), 1);
assert_eq!(messages[0]["content"], "Integration test message");
}
/// 测试多插件协作
#[tokio::test]
async fn test_multiple_plugins() {
let (sender, _receiver) = mpsc::channel(100);
let router = MessageRouter::builder()
.register_plugin(TextMessagePlugin::new())
.register_plugin(FileMessagePlugin::new())
.with_iroh_sender(sender)
.build();
// 发送文本消息
let text_msg = json!({
"type": "text",
"to": "user1",
"from": "user2",
"content": "Text"
});
assert!(router.send(text_msg).await.is_ok());
// 发送文件消息
let file_msg = json!({
"type": "file",
"to": "user1",
"from": "user2"
});
assert!(router.send(file_msg).await.is_ok());
}
/// 测试错误处理
#[tokio::test]
async fn test_error_handling() {
let (sender, _receiver) = mpsc::channel(100);
let router = MessageRouter::builder()
.register_plugin(TextMessagePlugin::new())
.with_iroh_sender(sender)
.build();
// 测试缺失必要字段
let invalid_msg = json!({
"type": "text",
"content": "Missing to and from"
});
let result = router.send(invalid_msg).await;
assert!(result.is_err());
// 测试未知消息类型
let unknown_type = json!({
"type": "unknown_type",
"to": "user1",
"from": "user2"
});
let result = router.send(unknown_type).await;
assert!(result.is_err());
}
/// 测试 Handler 层
#[tokio::test]
async fn test_handler_layer() {
let (sender, _receiver) = mpsc::channel(100);
let router = MessageRouter::builder()
.register_plugin(TextMessagePlugin::new())
.with_iroh_sender(sender)
.build();
let handler = TauriMessageHandler::new(router);
// 测试成功的命令调用
let message = json!({
"type": "text",
"to": "user1",
"from": "user2",
"content": "Handler test"
});
let result = handler.send_message(message).await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), "Message sent successfully");
// 测试获取支持的消息类型
let types = handler.get_supported_message_types().unwrap();
assert!(types.contains(&"text".to_string()));
}
/// 性能测试:大量消息发送
#[tokio::test]
async fn test_performance_many_messages() {
let (sender, mut receiver) = mpsc::channel(1000);
let router = MessageRouter::builder()
.register_plugin(TextMessagePlugin::new())
.with_iroh_sender(sender)
.build();
// 发送 100 条消息
for i in 0..100 {
let message = json!({
"type": "text",
"to": "user1",
"from": "user2",
"content": format!("Message {}", i)
});
let result = router.send(message).await;
assert!(result.is_ok());
}
// 验证所有消息都被发送
let mut count = 0;
while let Ok(_) = receiver.try_recv() {
count += 1;
}
assert_eq!(count, 100);
}
/// 测试并发安全性
#[tokio::test]
async fn test_concurrent_safety() {
let (sender, _receiver) = mpsc::channel(100);
let router = Arc::new(
MessageRouter::builder()
.register_plugin(TextMessagePlugin::new())
.with_iroh_sender(sender)
.build()
);
// 创建多个并发任务
let mut handles = vec![];
for i in 0..10 {
let router_clone = Arc::clone(&router);
let handle = tokio::spawn(async move {
let message = json!({
"type": "text",
"to": "user1",
"from": "user2",
"content": format!("Concurrent message {}", i)
});
router_clone.send(message).await
});
handles.push(handle);
}
// 等待所有任务完成
for handle in handles {
let result = handle.await.unwrap();
assert!(result.is_ok());
}
}
}