Implement P2P connection features, including local IP retrieval, P2P address exchange, and connection readiness handling. Enhance input control testing functionality and improve WebSocket message handling for P2P events. Update UI to support local input control testing and display results. Adjust connection state management for P2P scenarios.
This commit is contained in:
parent
fd34c415f5
commit
0ad21a24b0
@ -225,6 +225,31 @@ pub struct TurnConfig {
|
|||||||
pub struct NatTraversal;
|
pub struct NatTraversal;
|
||||||
|
|
||||||
impl NatTraversal {
|
impl NatTraversal {
|
||||||
|
/// 获取所有本地 IP 地址
|
||||||
|
pub fn get_local_ips() -> Vec<String> {
|
||||||
|
let mut ips = Vec::new();
|
||||||
|
|
||||||
|
// 尝试使用 socket 连接来获取本地 IP
|
||||||
|
if let Ok(socket) = std::net::UdpSocket::bind("0.0.0.0:0") {
|
||||||
|
// 连接到公网地址来获取本机实际使用的 IP
|
||||||
|
if let Ok(()) = socket.connect("8.8.8.8:80") {
|
||||||
|
if let Ok(addr) = socket.local_addr() {
|
||||||
|
let ip = addr.ip().to_string();
|
||||||
|
if !ip.starts_with("127.") && ip != "0.0.0.0" {
|
||||||
|
ips.push(ip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有获取到,添加 localhost
|
||||||
|
if ips.is_empty() {
|
||||||
|
ips.push("127.0.0.1".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
ips
|
||||||
|
}
|
||||||
|
|
||||||
/// 从服务器获取 ICE 服务器配置
|
/// 从服务器获取 ICE 服务器配置
|
||||||
pub async fn fetch_ice_servers(server_url: &str) -> Result<IceServersConfig> {
|
pub async fn fetch_ice_servers(server_url: &str) -> Result<IceServersConfig> {
|
||||||
// 将 ws:// 或 wss:// 转换为 http:// 或 https://
|
// 将 ws:// 或 wss:// 转换为 http:// 或 https://
|
||||||
|
|||||||
@ -19,6 +19,14 @@ pub struct DisplayInfoMsg {
|
|||||||
pub name: String,
|
pub name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// TURN 服务器信息(用于 P2P 中继)
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct TurnServerInfo {
|
||||||
|
pub url: String,
|
||||||
|
pub username: String,
|
||||||
|
pub credential: String,
|
||||||
|
}
|
||||||
|
|
||||||
/// 信令消息
|
/// 信令消息
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[serde(tag = "type")]
|
#[serde(tag = "type")]
|
||||||
@ -162,6 +170,38 @@ pub enum SignalMessage {
|
|||||||
fps: u32,
|
fps: u32,
|
||||||
quality: u32,
|
quality: u32,
|
||||||
},
|
},
|
||||||
|
/// P2P 地址交换请求
|
||||||
|
#[serde(rename = "p2p_exchange")]
|
||||||
|
P2PExchange {
|
||||||
|
session_id: String,
|
||||||
|
from_device: String,
|
||||||
|
to_device: String,
|
||||||
|
/// 本地地址列表 (局域网)
|
||||||
|
local_addrs: Vec<String>,
|
||||||
|
/// 公网地址 (通过 STUN 获取)
|
||||||
|
public_addr: Option<String>,
|
||||||
|
/// UDP 监听端口
|
||||||
|
udp_port: u16,
|
||||||
|
/// TURN 服务器信息(用于中继)
|
||||||
|
turn_server: Option<TurnServerInfo>,
|
||||||
|
},
|
||||||
|
/// P2P 连接就绪
|
||||||
|
#[serde(rename = "p2p_ready")]
|
||||||
|
P2PReady {
|
||||||
|
session_id: String,
|
||||||
|
from_device: String,
|
||||||
|
to_device: String,
|
||||||
|
/// 成功连接的地址
|
||||||
|
connected_addr: String,
|
||||||
|
},
|
||||||
|
/// P2P 连接失败,回退到服务器中转
|
||||||
|
#[serde(rename = "p2p_fallback")]
|
||||||
|
P2PFallback {
|
||||||
|
session_id: String,
|
||||||
|
from_device: String,
|
||||||
|
to_device: String,
|
||||||
|
reason: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 信令客户端
|
/// 信令客户端
|
||||||
@ -182,6 +222,11 @@ impl SignalClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 获取服务器 URL
|
||||||
|
pub fn server_url(&self) -> Option<String> {
|
||||||
|
Some(self.server_url.clone())
|
||||||
|
}
|
||||||
|
|
||||||
/// 连接到信令服务器
|
/// 连接到信令服务器
|
||||||
pub async fn connect(
|
pub async fn connect(
|
||||||
&mut self,
|
&mut self,
|
||||||
@ -222,14 +267,42 @@ impl SignalClient {
|
|||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
while let Some(Ok(msg)) = read.next().await {
|
while let Some(Ok(msg)) = read.next().await {
|
||||||
if let Message::Text(text) = msg {
|
if let Message::Text(text) = msg {
|
||||||
println!("★★★ [客户端] 收到WebSocket消息: {}", &text[..text.len().min(200)]);
|
// 检测消息类型
|
||||||
|
let is_heartbeat = text.contains("heartbeat");
|
||||||
|
let is_mouse = text.contains("mouse_event");
|
||||||
|
let is_keyboard = text.contains("keyboard_event");
|
||||||
|
let is_screen = text.contains("screen_frame");
|
||||||
|
|
||||||
|
// 打印控制消息(鼠标、键盘)
|
||||||
|
if is_mouse || is_keyboard {
|
||||||
|
println!("★★★ [被控端] 收到控制消息: {}", &text[..text.len().min(200)]);
|
||||||
|
} else if !is_heartbeat && !is_screen {
|
||||||
|
println!("★★★ [客户端] 收到WebSocket消息: {}", &text[..text.len().min(300)]);
|
||||||
|
}
|
||||||
|
|
||||||
match serde_json::from_str::<SignalMessage>(&text) {
|
match serde_json::from_str::<SignalMessage>(&text) {
|
||||||
Ok(signal_msg) => {
|
Ok(signal_msg) => {
|
||||||
println!("★★★ [客户端] 解析成功: {:?}", std::mem::discriminant(&signal_msg));
|
// 打印非心跳、非屏幕帧消息
|
||||||
|
match &signal_msg {
|
||||||
|
SignalMessage::HeartbeatAck => {},
|
||||||
|
SignalMessage::Heartbeat { .. } => {},
|
||||||
|
SignalMessage::ScreenFrame { .. } => {},
|
||||||
|
SignalMessage::MouseEvent { x, y, event_type, .. } => {
|
||||||
|
if event_type != "move" {
|
||||||
|
println!("★★★ [被控端] 解析鼠标事件成功: type={}, x={:.0}, y={:.0}", event_type, x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SignalMessage::KeyboardEvent { key, event_type, .. } => {
|
||||||
|
println!("★★★ [被控端] 解析键盘事件成功: key={}, type={}", key, event_type);
|
||||||
|
}
|
||||||
|
other => {
|
||||||
|
println!("★★★ [客户端] 解析成功: {:?}", std::mem::discriminant(other));
|
||||||
|
}
|
||||||
|
}
|
||||||
on_message(signal_msg);
|
on_message(signal_msg);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
println!("★★★ [客户端] 解析失败: {}, 原始消息: {}", e, &text[..text.len().min(100)]);
|
println!("★★★ [客户端] 解析失败: {}, 原始消息: {}", e, &text[..text.len().min(200)]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,7 +11,7 @@ easyremote-common = { path = "../common" }
|
|||||||
easyremote-client-core = { path = "../client-core" }
|
easyremote-client-core = { path = "../client-core" }
|
||||||
|
|
||||||
# Tauri
|
# Tauri
|
||||||
tauri = { version = "1.5", features = ["shell-open", "dialog-all", "clipboard-all", "window-all", "global-shortcut-all", "process-all", "system-tray"] }
|
tauri = { version = "1.5", features = ["shell-open", "dialog-all", "clipboard-all", "window-all", "global-shortcut-all", "process-all", "system-tray", "window-data-url"] }
|
||||||
|
|
||||||
# Windows single instance
|
# Windows single instance
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
@ -46,6 +46,8 @@ thiserror = { workspace = true }
|
|||||||
dirs = "5.0"
|
dirs = "5.0"
|
||||||
image = { workspace = true }
|
image = { workspace = true }
|
||||||
once_cell = "1.19"
|
once_cell = "1.19"
|
||||||
|
urlencoding = "2.1"
|
||||||
|
url = "2.5"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["custom-protocol"]
|
default = ["custom-protocol"]
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -176,11 +176,16 @@ fn main() {
|
|||||||
_ => {}
|
_ => {}
|
||||||
})
|
})
|
||||||
.on_window_event(|event| {
|
.on_window_event(|event| {
|
||||||
// 关闭窗口时最小化到托盘而不是退出
|
// 关闭窗口时的处理
|
||||||
if let WindowEvent::CloseRequested { api, .. } = event.event() {
|
if let WindowEvent::CloseRequested { api, .. } = event.event() {
|
||||||
event.window().hide().unwrap();
|
let window = event.window();
|
||||||
|
// 只有主窗口最小化到托盘,其他窗口(如远程控制窗口)直接关闭
|
||||||
|
if window.label() == "main" {
|
||||||
|
window.hide().unwrap();
|
||||||
api.prevent_close();
|
api.prevent_close();
|
||||||
}
|
}
|
||||||
|
// 远程控制窗口直接关闭(不阻止)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
// 设置窗口标题以便单例检测
|
// 设置窗口标题以便单例检测
|
||||||
@ -224,6 +229,7 @@ fn main() {
|
|||||||
commands::connect_to_device,
|
commands::connect_to_device,
|
||||||
commands::disconnect,
|
commands::disconnect,
|
||||||
commands::get_connection_state,
|
commands::get_connection_state,
|
||||||
|
commands::open_remote_window,
|
||||||
// 历史记录
|
// 历史记录
|
||||||
commands::get_history,
|
commands::get_history,
|
||||||
// 配置
|
// 配置
|
||||||
@ -233,6 +239,8 @@ fn main() {
|
|||||||
// 开机启动
|
// 开机启动
|
||||||
commands::get_autostart,
|
commands::get_autostart,
|
||||||
commands::set_autostart,
|
commands::set_autostart,
|
||||||
|
// 测试
|
||||||
|
commands::test_local_input,
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
|||||||
@ -4,6 +4,24 @@ use easyremote_common::types::{DeviceId, VerificationCode};
|
|||||||
use easyremote_client_core::ClientConfig;
|
use easyremote_client_core::ClientConfig;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
use tokio::net::UdpSocket;
|
||||||
|
|
||||||
|
/// P2P 连接信息
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct P2PConnection {
|
||||||
|
/// 会话 ID
|
||||||
|
pub session_id: String,
|
||||||
|
/// 对方设备 ID
|
||||||
|
pub peer_device_id: String,
|
||||||
|
/// 本地 UDP 端口
|
||||||
|
pub local_port: u16,
|
||||||
|
/// 对方地址
|
||||||
|
pub peer_addr: Option<std::net::SocketAddr>,
|
||||||
|
/// 是否已连接
|
||||||
|
pub connected: bool,
|
||||||
|
}
|
||||||
|
|
||||||
/// 应用状态
|
/// 应用状态
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
@ -21,6 +39,10 @@ pub struct AppState {
|
|||||||
pub connection_state: ConnectionState,
|
pub connection_state: ConnectionState,
|
||||||
/// 当前会话ID
|
/// 当前会话ID
|
||||||
pub current_session_id: Option<String>,
|
pub current_session_id: Option<String>,
|
||||||
|
/// P2P 连接(用于被控端接收直连数据)
|
||||||
|
pub p2p_socket: Option<Arc<UdpSocket>>,
|
||||||
|
/// P2P 连接信息
|
||||||
|
pub p2p_connection: Option<P2PConnection>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 当前用户信息
|
/// 当前用户信息
|
||||||
@ -38,15 +60,20 @@ pub enum ConnectionState {
|
|||||||
Disconnected,
|
Disconnected,
|
||||||
/// 正在连接
|
/// 正在连接
|
||||||
Connecting,
|
Connecting,
|
||||||
|
/// 正在进行 P2P 握手
|
||||||
|
P2PHandshaking {
|
||||||
|
target_device_id: String,
|
||||||
|
},
|
||||||
/// 已连接到设备
|
/// 已连接到设备
|
||||||
Connected {
|
Connected {
|
||||||
target_device_id: String,
|
target_device_id: String,
|
||||||
target_device_name: String,
|
target_device_name: String,
|
||||||
connection_type: String,
|
connection_type: String, // "p2p" 或 "relay"
|
||||||
},
|
},
|
||||||
/// 被控制中
|
/// 被控制中
|
||||||
BeingControlled {
|
BeingControlled {
|
||||||
controller_device_id: String,
|
controller_device_id: String,
|
||||||
|
connection_type: String, // "p2p" 或 "relay"
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,6 +102,8 @@ impl AppState {
|
|||||||
auth_token,
|
auth_token,
|
||||||
connection_state: ConnectionState::Disconnected,
|
connection_state: ConnectionState::Disconnected,
|
||||||
current_session_id: None,
|
current_session_id: None,
|
||||||
|
p2p_socket: None,
|
||||||
|
p2p_connection: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -83,6 +83,7 @@
|
|||||||
},
|
},
|
||||||
"windows": [
|
"windows": [
|
||||||
{
|
{
|
||||||
|
"label": "main",
|
||||||
"fullscreen": false,
|
"fullscreen": false,
|
||||||
"height": 700,
|
"height": 700,
|
||||||
"resizable": true,
|
"resizable": true,
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, computed } from 'vue'
|
import { ref, onMounted, computed } from 'vue'
|
||||||
import { invoke } from '@tauri-apps/api/tauri'
|
import { invoke } from '@tauri-apps/api/tauri'
|
||||||
|
import { WebviewWindow } from '@tauri-apps/api/window'
|
||||||
import type { DeviceInfo, CurrentUser, Device, HistoryItem, ConnectRequest, ConnectionState, ClientConfig } from './types'
|
import type { DeviceInfo, CurrentUser, Device, HistoryItem, ConnectRequest, ConnectionState, ClientConfig } from './types'
|
||||||
|
|
||||||
// 状态
|
// 状态
|
||||||
@ -32,6 +33,9 @@ const configForm = ref({
|
|||||||
server_url: ''
|
server_url: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 测试结果
|
||||||
|
const testResult = ref('')
|
||||||
|
|
||||||
// 标签页
|
// 标签页
|
||||||
const activeTab = ref<'local' | 'remote' | 'devices' | 'history' | 'settings'>('local')
|
const activeTab = ref<'local' | 'remote' | 'devices' | 'history' | 'settings'>('local')
|
||||||
|
|
||||||
@ -100,6 +104,19 @@ async function toggleAutostart() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 测试本地输入控制
|
||||||
|
async function testLocalInput() {
|
||||||
|
try {
|
||||||
|
testResult.value = '正在测试...'
|
||||||
|
const result = await invoke('test_local_input')
|
||||||
|
testResult.value = result as string
|
||||||
|
showSuccess('测试完成,请查看结果')
|
||||||
|
} catch (e: any) {
|
||||||
|
testResult.value = '测试失败: ' + e
|
||||||
|
showError('测试失败: ' + e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function loadConfig() {
|
async function loadConfig() {
|
||||||
try {
|
try {
|
||||||
config.value = await invoke('get_config')
|
config.value = await invoke('get_config')
|
||||||
@ -109,6 +126,14 @@ async function loadConfig() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadDeviceInfo() {
|
||||||
|
try {
|
||||||
|
deviceInfo.value = await invoke('get_device_info')
|
||||||
|
} catch (e) {
|
||||||
|
console.error('加载设备信息失败:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function saveConfig() {
|
async function saveConfig() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
@ -238,34 +263,49 @@ async function connectToDevice(targetDeviceId?: string, targetCode?: string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
loading.value = true
|
// 创建一个临时设备对象
|
||||||
error.value = ''
|
const tempDevice: Device = {
|
||||||
try {
|
id: '',
|
||||||
const request: ConnectRequest = {
|
|
||||||
device_id: deviceId.replace(/\s/g, ''),
|
device_id: deviceId.replace(/\s/g, ''),
|
||||||
verification_code: code
|
name: `设备 ${deviceId}`,
|
||||||
}
|
os_type: 'Unknown',
|
||||||
await invoke('connect_to_device', { request })
|
os_version: '',
|
||||||
connectionState.value = await invoke('get_connection_state')
|
is_online: true,
|
||||||
showSuccess('连接成功')
|
last_seen: ''
|
||||||
} catch (e: any) {
|
|
||||||
error.value = e.toString()
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 打开远程控制窗口
|
||||||
|
openRemoteWindow(tempDevice)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function quickConnect(device: Device) {
|
async function quickConnect(device: Device) {
|
||||||
loading.value = true
|
// 打开新窗口进行远程控制
|
||||||
error.value = ''
|
openRemoteWindow(device)
|
||||||
try {
|
}
|
||||||
await invoke('connect_to_device', {
|
|
||||||
request: { device_id: device.device_id, verification_code: '' }
|
// 打开远程控制窗口
|
||||||
|
async function openRemoteWindow(device: Device) {
|
||||||
|
const serverUrl = config.value.server_url
|
||||||
|
|
||||||
|
console.log('打开远程控制窗口:', {
|
||||||
|
deviceId: device.device_id,
|
||||||
|
deviceName: device.name,
|
||||||
|
serverUrl: serverUrl
|
||||||
})
|
})
|
||||||
connectionState.value = await invoke('get_connection_state')
|
|
||||||
showSuccess('正在连接...')
|
// 调用后端命令打开远程控制窗口
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
await invoke('open_remote_window', {
|
||||||
|
deviceId: device.device_id,
|
||||||
|
deviceName: device.name,
|
||||||
|
serverUrl: serverUrl
|
||||||
|
})
|
||||||
|
console.log('远程控制窗口命令已发送')
|
||||||
|
showSuccess('正在打开远程控制窗口...')
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
error.value = e.toString()
|
console.error('打开远程控制窗口失败:', e)
|
||||||
|
showError('打开远程控制窗口失败: ' + e.toString())
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
@ -749,6 +789,24 @@ function getDeviceIcon(osType: string): string {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 调试工具 -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">调试工具</h3>
|
||||||
|
</div>
|
||||||
|
<div class="settings-list">
|
||||||
|
<div class="settings-item">
|
||||||
|
<span class="settings-label">测试本地输入控制</span>
|
||||||
|
<button class="btn btn-secondary btn-sm" @click="testLocalInput">
|
||||||
|
测试鼠标移动和点击
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-if="testResult" class="test-result">
|
||||||
|
<pre>{{ testResult }}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 关于 -->
|
<!-- 关于 -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
@ -831,6 +889,7 @@ function getDeviceIcon(osType: string): string {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -1258,6 +1317,30 @@ function getDeviceIcon(osType: string): string {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 测试结果 */
|
||||||
|
.test-result {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border-radius: 8px;
|
||||||
|
font-family: 'Consolas', 'Monaco', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-result pre {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
/* 错误信息 */
|
/* 错误信息 */
|
||||||
.error-message {
|
.error-message {
|
||||||
color: var(--error);
|
color: var(--error);
|
||||||
@ -1439,4 +1522,5 @@ function getDeviceIcon(osType: string): string {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -89,7 +89,14 @@ pub async fn list_all_devices(
|
|||||||
|
|
||||||
match repo.find_all_with_username(params.offset(), params.limit()).await {
|
match repo.find_all_with_username(params.offset(), params.limit()).await {
|
||||||
Ok((devices, total)) => {
|
Ok((devices, total)) => {
|
||||||
let items: Vec<DeviceResponse> = devices.into_iter().map(Into::into).collect();
|
// 使用实时 WebSocket 连接状态覆盖数据库中的状态
|
||||||
|
let mut items: Vec<DeviceResponse> = Vec::with_capacity(devices.len());
|
||||||
|
for device in devices {
|
||||||
|
let mut device_resp: DeviceResponse = device.into();
|
||||||
|
// 检查实际的 WebSocket 连接状态
|
||||||
|
device_resp.is_online = state.is_device_online(&device_resp.device_id).await;
|
||||||
|
items.push(device_resp);
|
||||||
|
}
|
||||||
let total_pages = ((total as f64) / (params.limit() as f64)).ceil() as u32;
|
let total_pages = ((total as f64) / (params.limit() as f64)).ceil() as u32;
|
||||||
|
|
||||||
let response = PaginatedResponse {
|
let response = PaginatedResponse {
|
||||||
@ -144,7 +151,8 @@ pub async fn get_stats(
|
|||||||
|
|
||||||
let total_users = user_repo.find_all(0, 1).await.map(|(_, t)| t).unwrap_or(0);
|
let total_users = user_repo.find_all(0, 1).await.map(|(_, t)| t).unwrap_or(0);
|
||||||
let (_, total_devices) = device_repo.find_all(0, 1).await.unwrap_or((vec![], 0));
|
let (_, total_devices) = device_repo.find_all(0, 1).await.unwrap_or((vec![], 0));
|
||||||
let online_devices = device_repo.count_online().await.unwrap_or(0);
|
// 使用实时 WebSocket 连接数作为在线设备数
|
||||||
|
let online_devices = state.count_online_devices().await as i64;
|
||||||
let active_sessions = session_repo.count_active().await.unwrap_or(0);
|
let active_sessions = session_repo.count_active().await.unwrap_or(0);
|
||||||
let (_, total_sessions) = session_repo.find_all(0, 1).await.unwrap_or((vec![], 0));
|
let (_, total_sessions) = session_repo.find_all(0, 1).await.unwrap_or((vec![], 0));
|
||||||
|
|
||||||
|
|||||||
@ -55,7 +55,14 @@ pub async fn list_devices(
|
|||||||
|
|
||||||
match repo.find_by_user(&user.user_id).await {
|
match repo.find_by_user(&user.user_id).await {
|
||||||
Ok(devices) => {
|
Ok(devices) => {
|
||||||
let response: Vec<DeviceResponse> = devices.into_iter().map(Into::into).collect();
|
// 使用实时 WebSocket 连接状态覆盖数据库中的状态
|
||||||
|
let mut response: Vec<DeviceResponse> = Vec::with_capacity(devices.len());
|
||||||
|
for device in devices {
|
||||||
|
let mut device_resp: DeviceResponse = device.into();
|
||||||
|
// 检查实际的 WebSocket 连接状态
|
||||||
|
device_resp.is_online = state.is_device_online(&device_resp.device_id).await;
|
||||||
|
response.push(device_resp);
|
||||||
|
}
|
||||||
(StatusCode::OK, Json(ApiResponse::ok(response))).into_response()
|
(StatusCode::OK, Json(ApiResponse::ok(response))).into_response()
|
||||||
}
|
}
|
||||||
Err(e) => api_error(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
|
Err(e) => api_error(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
|
||||||
|
|||||||
@ -63,4 +63,10 @@ impl AppState {
|
|||||||
let connections = self.connections.read().await;
|
let connections = self.connections.read().await;
|
||||||
connections.contains_key(device_id)
|
connections.contains_key(device_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 获取在线设备数量
|
||||||
|
pub async fn count_online_devices(&self) -> usize {
|
||||||
|
let connections = self.connections.read().await;
|
||||||
|
connections.len()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -169,6 +169,33 @@ pub enum SignalMessage {
|
|||||||
to_device: String,
|
to_device: String,
|
||||||
display_index: usize,
|
display_index: usize,
|
||||||
},
|
},
|
||||||
|
/// P2P 地址交换
|
||||||
|
#[serde(rename = "p2p_exchange")]
|
||||||
|
P2PExchange {
|
||||||
|
session_id: String,
|
||||||
|
from_device: String,
|
||||||
|
to_device: String,
|
||||||
|
local_addrs: Vec<String>,
|
||||||
|
public_addr: Option<String>,
|
||||||
|
udp_port: u16,
|
||||||
|
turn_server: Option<TurnServerInfoMsg>,
|
||||||
|
},
|
||||||
|
/// P2P 连接就绪
|
||||||
|
#[serde(rename = "p2p_ready")]
|
||||||
|
P2PReady {
|
||||||
|
session_id: String,
|
||||||
|
from_device: String,
|
||||||
|
to_device: String,
|
||||||
|
connected_addr: String,
|
||||||
|
},
|
||||||
|
/// P2P 回退到服务器中转
|
||||||
|
#[serde(rename = "p2p_fallback")]
|
||||||
|
P2PFallback {
|
||||||
|
session_id: String,
|
||||||
|
from_device: String,
|
||||||
|
to_device: String,
|
||||||
|
reason: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 显示器信息
|
/// 显示器信息
|
||||||
@ -181,6 +208,14 @@ pub struct DisplayInfoMsg {
|
|||||||
pub name: String,
|
pub name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// TURN 服务器信息
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct TurnServerInfoMsg {
|
||||||
|
pub url: String,
|
||||||
|
pub username: String,
|
||||||
|
pub credential: String,
|
||||||
|
}
|
||||||
|
|
||||||
/// 信令WebSocket处理器
|
/// 信令WebSocket处理器
|
||||||
pub async fn signal_handler(
|
pub async fn signal_handler(
|
||||||
ws: WebSocketUpgrade,
|
ws: WebSocketUpgrade,
|
||||||
@ -418,6 +453,24 @@ async fn handle_signal_message(state: &Arc<AppState>, device_id: &str, text: &st
|
|||||||
let _ = state.send_to_device(&to_device, text).await;
|
let _ = state.send_to_device(&to_device, text).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// P2P 地址交换 - 转发给对方
|
||||||
|
SignalMessage::P2PExchange { to_device, .. } => {
|
||||||
|
tracing::info!("★★★ P2P 地址交换: 转发到 {}", to_device);
|
||||||
|
let _ = state.send_to_device(&to_device, text).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
// P2P 连接就绪 - 转发给对方
|
||||||
|
SignalMessage::P2PReady { to_device, .. } => {
|
||||||
|
tracing::info!("★★★ P2P 连接就绪: 转发到 {}", to_device);
|
||||||
|
let _ = state.send_to_device(&to_device, text).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
// P2P 回退 - 转发给对方
|
||||||
|
SignalMessage::P2PFallback { to_device, .. } => {
|
||||||
|
tracing::info!("★★★ P2P 回退到服务器中转: 转发到 {}", to_device);
|
||||||
|
let _ = state.send_to_device(&to_device, text).await;
|
||||||
|
}
|
||||||
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -494,6 +547,11 @@ async fn handle_remote_socket(socket: WebSocket, state: Arc<AppState>, device_id
|
|||||||
let recv_task = tokio::spawn(async move {
|
let recv_task = tokio::spawn(async move {
|
||||||
while let Some(Ok(msg)) = receiver.next().await {
|
while let Some(Ok(msg)) = receiver.next().await {
|
||||||
if let Message::Text(text) = msg {
|
if let Message::Text(text) = msg {
|
||||||
|
// 打印调试信息(仅非移动事件)
|
||||||
|
if !text.contains("\"event_type\":\"move\"") {
|
||||||
|
tracing::info!("★★★ [Remote] 收到消息: {}", &text[..text.len().min(200)]);
|
||||||
|
}
|
||||||
|
|
||||||
// 替换 from_device 为正确的浏览器ID
|
// 替换 from_device 为正确的浏览器ID
|
||||||
let modified_text = if let Ok(mut json) = serde_json::from_str::<serde_json::Value>(&text) {
|
let modified_text = if let Ok(mut json) = serde_json::from_str::<serde_json::Value>(&text) {
|
||||||
if let Some(obj) = json.as_object_mut() {
|
if let Some(obj) = json.as_object_mut() {
|
||||||
@ -505,8 +563,12 @@ async fn handle_remote_socket(socket: WebSocket, state: Arc<AppState>, device_id
|
|||||||
} else {
|
} else {
|
||||||
text
|
text
|
||||||
};
|
};
|
||||||
|
|
||||||
// 转发消息到目标设备
|
// 转发消息到目标设备
|
||||||
let _ = state_clone.send_to_device(&target_device_id, &modified_text).await;
|
let result = state_clone.send_to_device(&target_device_id, &modified_text).await;
|
||||||
|
if !text.contains("\"event_type\":\"move\"") {
|
||||||
|
tracing::info!("★★★ [Remote] 转发到 {} 结果: {}", target_device_id, result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Binary file not shown.
Loading…
Reference in New Issue
Block a user