显示客户端链接的ip:port
This commit is contained in:
@@ -132,11 +132,13 @@ async fn handle_connection(
|
|||||||
match msg {
|
match msg {
|
||||||
Ok(Message::Text(text)) => {
|
Ok(Message::Text(text)) => {
|
||||||
// Parse incoming messages
|
// Parse incoming messages
|
||||||
if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(&text) {
|
if let Ok(mut json_value) = serde_json::from_str::<serde_json::Value>(&text) {
|
||||||
if json_value.get("type").and_then(|t| t.as_str()) == Some("debug_data") {
|
let msg_type = json_value.get("type").and_then(|t| t.as_str());
|
||||||
|
|
||||||
|
if msg_type == Some("debug_data") {
|
||||||
// Broadcast debug data from game client to all clients (including frontend)
|
// Broadcast debug data from game client to all clients (including frontend)
|
||||||
tx.send(text).ok();
|
tx.send(text).ok();
|
||||||
} else if json_value.get("type").and_then(|t| t.as_str()) == Some("ping") {
|
} else if msg_type == Some("ping") {
|
||||||
// Respond to ping
|
// Respond to ping
|
||||||
let _ = tx.send(
|
let _ = tx.send(
|
||||||
serde_json::json!({
|
serde_json::json!({
|
||||||
@@ -145,6 +147,12 @@ async fn handle_connection(
|
|||||||
})
|
})
|
||||||
.to_string(),
|
.to_string(),
|
||||||
);
|
);
|
||||||
|
} else if msg_type == Some("log") {
|
||||||
|
// Inject clientId into log messages
|
||||||
|
if let Some(data) = json_value.get_mut("data").and_then(|d| d.as_object_mut()) {
|
||||||
|
data.insert("clientId".to_string(), serde_json::Value::String(peer_addr.to_string()));
|
||||||
|
}
|
||||||
|
tx.send(json_value.to_string()).ok();
|
||||||
} else {
|
} else {
|
||||||
// Forward all other messages (like get_raw_entity_list, get_entity_details, etc.)
|
// Forward all other messages (like get_raw_entity_list, get_entity_details, etc.)
|
||||||
// to all connected clients (this enables frontend -> game client communication)
|
// to all connected clients (this enables frontend -> game client communication)
|
||||||
|
|||||||
@@ -148,8 +148,8 @@ function App() {
|
|||||||
|
|
||||||
// 监听远程日志事件
|
// 监听远程日志事件
|
||||||
window.addEventListener('profiler:remote-log', ((event: CustomEvent) => {
|
window.addEventListener('profiler:remote-log', ((event: CustomEvent) => {
|
||||||
const { level, message, timestamp } = event.detail;
|
const { level, message, timestamp, clientId } = event.detail;
|
||||||
logService.addRemoteLog(level, message, timestamp);
|
logService.addRemoteLog(level, message, timestamp, clientId);
|
||||||
}) as EventListener);
|
}) as EventListener);
|
||||||
|
|
||||||
Core.services.registerInstance(UIRegistry, uiRegistry);
|
Core.services.registerInstance(UIRegistry, uiRegistry);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState, useEffect, useRef } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { LogService, LogEntry } from '@esengine/editor-core';
|
import { LogService, LogEntry } from '@esengine/editor-core';
|
||||||
import { LogLevel } from '@esengine/ecs-framework';
|
import { LogLevel } from '@esengine/ecs-framework';
|
||||||
import { Trash2, AlertCircle, Info, AlertTriangle, XCircle, Bug, Search, Maximize2, ChevronRight, ChevronDown } from 'lucide-react';
|
import { Trash2, AlertCircle, Info, AlertTriangle, XCircle, Bug, Search, Maximize2, ChevronRight, ChevronDown, Wifi } from 'lucide-react';
|
||||||
import { JsonViewer } from './JsonViewer';
|
import { JsonViewer } from './JsonViewer';
|
||||||
import '../styles/ConsolePanel.css';
|
import '../styles/ConsolePanel.css';
|
||||||
|
|
||||||
@@ -19,6 +19,7 @@ export function ConsolePanel({ logService }: ConsolePanelProps) {
|
|||||||
LogLevel.Error,
|
LogLevel.Error,
|
||||||
LogLevel.Fatal
|
LogLevel.Fatal
|
||||||
]));
|
]));
|
||||||
|
const [showRemoteOnly, setShowRemoteOnly] = useState(false);
|
||||||
const [autoScroll, setAutoScroll] = useState(true);
|
const [autoScroll, setAutoScroll] = useState(true);
|
||||||
const [expandedLogs, setExpandedLogs] = useState<Set<number>>(new Set());
|
const [expandedLogs, setExpandedLogs] = useState<Set<number>>(new Set());
|
||||||
const [jsonViewerData, setJsonViewerData] = useState<any>(null);
|
const [jsonViewerData, setJsonViewerData] = useState<any>(null);
|
||||||
@@ -65,6 +66,7 @@ export function ConsolePanel({ logService }: ConsolePanelProps) {
|
|||||||
|
|
||||||
const filteredLogs = logs.filter(log => {
|
const filteredLogs = logs.filter(log => {
|
||||||
if (!levelFilter.has(log.level)) return false;
|
if (!levelFilter.has(log.level)) return false;
|
||||||
|
if (showRemoteOnly && log.source !== 'remote') return false;
|
||||||
if (filter && !log.message.toLowerCase().includes(filter.toLowerCase())) {
|
if (filter && !log.message.toLowerCase().includes(filter.toLowerCase())) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -224,6 +226,8 @@ export function ConsolePanel({ logService }: ConsolePanelProps) {
|
|||||||
[LogLevel.Error]: logs.filter(l => l.level === LogLevel.Error || l.level === LogLevel.Fatal).length
|
[LogLevel.Error]: logs.filter(l => l.level === LogLevel.Error || l.level === LogLevel.Fatal).length
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const remoteLogCount = logs.filter(l => l.source === 'remote').length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="console-panel">
|
<div className="console-panel">
|
||||||
<div className="console-toolbar">
|
<div className="console-toolbar">
|
||||||
@@ -246,6 +250,14 @@ export function ConsolePanel({ logService }: ConsolePanelProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="console-toolbar-right">
|
<div className="console-toolbar-right">
|
||||||
|
<button
|
||||||
|
className={`console-filter-btn ${showRemoteOnly ? 'active' : ''}`}
|
||||||
|
onClick={() => setShowRemoteOnly(!showRemoteOnly)}
|
||||||
|
title="Show Remote Logs Only"
|
||||||
|
>
|
||||||
|
<Wifi size={14} />
|
||||||
|
{remoteLogCount > 0 && <span>{remoteLogCount}</span>}
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
className={`console-filter-btn ${levelFilter.has(LogLevel.Debug) ? 'active' : ''}`}
|
className={`console-filter-btn ${levelFilter.has(LogLevel.Debug) ? 'active' : ''}`}
|
||||||
onClick={() => toggleLevelFilter(LogLevel.Debug)}
|
onClick={() => toggleLevelFilter(LogLevel.Debug)}
|
||||||
@@ -317,6 +329,11 @@ export function ConsolePanel({ logService }: ConsolePanelProps) {
|
|||||||
<div className={`log-entry-source ${log.source === 'remote' ? 'source-remote' : ''}`}>
|
<div className={`log-entry-source ${log.source === 'remote' ? 'source-remote' : ''}`}>
|
||||||
[{log.source === 'remote' ? '🌐 Remote' : log.source}]
|
[{log.source === 'remote' ? '🌐 Remote' : log.source}]
|
||||||
</div>
|
</div>
|
||||||
|
{log.clientId && (
|
||||||
|
<div className="log-entry-client" title={`Client: ${log.clientId}`}>
|
||||||
|
{log.clientId}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="log-entry-message">
|
<div className="log-entry-message">
|
||||||
{formatMessage(log.message, isExpanded)}
|
{formatMessage(log.message, isExpanded)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useState, useEffect } from 'react';
|
|||||||
import { Entity } from '@esengine/ecs-framework';
|
import { Entity } from '@esengine/ecs-framework';
|
||||||
import { EntityStoreService, MessageHub } from '@esengine/editor-core';
|
import { EntityStoreService, MessageHub } from '@esengine/editor-core';
|
||||||
import { useLocale } from '../hooks/useLocale';
|
import { useLocale } from '../hooks/useLocale';
|
||||||
import { Box, Layers, Wifi } from 'lucide-react';
|
import { Box, Layers, Wifi, Search } from 'lucide-react';
|
||||||
import { ProfilerService, RemoteEntity } from '../services/ProfilerService';
|
import { ProfilerService, RemoteEntity } from '../services/ProfilerService';
|
||||||
import '../styles/SceneHierarchy.css';
|
import '../styles/SceneHierarchy.css';
|
||||||
|
|
||||||
@@ -16,6 +16,7 @@ export function SceneHierarchy({ entityStore, messageHub }: SceneHierarchyProps)
|
|||||||
const [remoteEntities, setRemoteEntities] = useState<RemoteEntity[]>([]);
|
const [remoteEntities, setRemoteEntities] = useState<RemoteEntity[]>([]);
|
||||||
const [isRemoteConnected, setIsRemoteConnected] = useState(false);
|
const [isRemoteConnected, setIsRemoteConnected] = useState(false);
|
||||||
const [selectedId, setSelectedId] = useState<number | null>(null);
|
const [selectedId, setSelectedId] = useState<number | null>(null);
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
// Subscribe to local entity changes
|
// Subscribe to local entity changes
|
||||||
@@ -106,8 +107,33 @@ export function SceneHierarchy({ entityStore, messageHub }: SceneHierarchyProps)
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Filter entities based on search query
|
||||||
|
const filterEntities = <T extends Entity | RemoteEntity>(entityList: T[]): T[] => {
|
||||||
|
if (!searchQuery.trim()) return entityList;
|
||||||
|
|
||||||
|
const query = searchQuery.toLowerCase();
|
||||||
|
return entityList.filter(entity => {
|
||||||
|
const name = 'name' in entity ? entity.name : `Entity ${entity.id}`;
|
||||||
|
const id = entity.id.toString();
|
||||||
|
|
||||||
|
// Search by name or ID
|
||||||
|
if (name.toLowerCase().includes(query) || id.includes(query)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search by component types (for remote entities)
|
||||||
|
if ('componentTypes' in entity && Array.isArray(entity.componentTypes)) {
|
||||||
|
return entity.componentTypes.some(type =>
|
||||||
|
type.toLowerCase().includes(query)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// Determine which entities to display
|
// Determine which entities to display
|
||||||
const displayEntities = isRemoteConnected ? remoteEntities : entities;
|
const displayEntities = filterEntities(isRemoteConnected ? remoteEntities : entities);
|
||||||
const showRemoteIndicator = isRemoteConnected && remoteEntities.length > 0;
|
const showRemoteIndicator = isRemoteConnected && remoteEntities.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -121,6 +147,15 @@ export function SceneHierarchy({ entityStore, messageHub }: SceneHierarchyProps)
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="hierarchy-search">
|
||||||
|
<Search size={14} />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder={t('hierarchy.search') || 'Search entities...'}
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className="hierarchy-content scrollable">
|
<div className="hierarchy-content scrollable">
|
||||||
{displayEntities.length === 0 ? (
|
{displayEntities.length === 0 ? (
|
||||||
<div className="empty-state">
|
<div className="empty-state">
|
||||||
@@ -134,7 +169,7 @@ export function SceneHierarchy({ entityStore, messageHub }: SceneHierarchyProps)
|
|||||||
</div>
|
</div>
|
||||||
) : isRemoteConnected ? (
|
) : isRemoteConnected ? (
|
||||||
<ul className="entity-list">
|
<ul className="entity-list">
|
||||||
{remoteEntities.map(entity => (
|
{displayEntities.map(entity => (
|
||||||
<li
|
<li
|
||||||
key={entity.id}
|
key={entity.id}
|
||||||
className={`entity-item remote-entity ${selectedId === entity.id ? 'selected' : ''} ${!entity.enabled ? 'disabled' : ''}`}
|
className={`entity-item remote-entity ${selectedId === entity.id ? 'selected' : ''} ${!entity.enabled ? 'disabled' : ''}`}
|
||||||
@@ -143,6 +178,11 @@ export function SceneHierarchy({ entityStore, messageHub }: SceneHierarchyProps)
|
|||||||
>
|
>
|
||||||
<Box size={14} className="entity-icon" />
|
<Box size={14} className="entity-icon" />
|
||||||
<span className="entity-name">{entity.name}</span>
|
<span className="entity-name">{entity.name}</span>
|
||||||
|
{entity.tag !== 0 && (
|
||||||
|
<span className="entity-tag" title={`Tag: ${entity.tag}`}>
|
||||||
|
#{entity.tag}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{entity.componentCount > 0 && (
|
{entity.componentCount > 0 && (
|
||||||
<span className="component-count">{entity.componentCount}</span>
|
<span className="component-count">{entity.componentCount}</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ export class ProfilerService {
|
|||||||
private currentData: ProfilerData | null = null;
|
private currentData: ProfilerData | null = null;
|
||||||
private checkServerInterval: NodeJS.Timeout | null = null;
|
private checkServerInterval: NodeJS.Timeout | null = null;
|
||||||
private reconnectTimeout: NodeJS.Timeout | null = null;
|
private reconnectTimeout: NodeJS.Timeout | null = null;
|
||||||
|
private clientIdMap: Map<string, string> = new Map(); // 客户端地址 -> 客户端ID映射
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
const settings = SettingsService.getInstance();
|
const settings = SettingsService.getInstance();
|
||||||
@@ -370,11 +371,14 @@ export class ProfilerService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const clientId = data.clientId || data.client_id || 'unknown';
|
||||||
|
|
||||||
window.dispatchEvent(new CustomEvent('profiler:remote-log', {
|
window.dispatchEvent(new CustomEvent('profiler:remote-log', {
|
||||||
detail: {
|
detail: {
|
||||||
level,
|
level,
|
||||||
message,
|
message,
|
||||||
timestamp: data.timestamp ? new Date(data.timestamp) : new Date()
|
timestamp: data.timestamp ? new Date(data.timestamp) : new Date(),
|
||||||
|
clientId
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,37 +107,46 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.console-filter-btn:nth-child(1) {
|
.console-filter-btn:nth-child(1) {
|
||||||
color: #858585;
|
color: #10b981;
|
||||||
}
|
}
|
||||||
|
|
||||||
.console-filter-btn:nth-child(1).active {
|
.console-filter-btn:nth-child(1).active {
|
||||||
|
color: #34d399;
|
||||||
|
border-color: #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-filter-btn:nth-child(2) {
|
||||||
|
color: #858585;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-filter-btn:nth-child(2).active {
|
||||||
color: #a0a0a0;
|
color: #a0a0a0;
|
||||||
border-color: #858585;
|
border-color: #858585;
|
||||||
}
|
}
|
||||||
|
|
||||||
.console-filter-btn:nth-child(2) {
|
.console-filter-btn:nth-child(3) {
|
||||||
color: #4a9eff;
|
color: #4a9eff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.console-filter-btn:nth-child(2).active {
|
.console-filter-btn:nth-child(3).active {
|
||||||
color: #6eb3ff;
|
color: #6eb3ff;
|
||||||
border-color: #4a9eff;
|
border-color: #4a9eff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.console-filter-btn:nth-child(3) {
|
.console-filter-btn:nth-child(4) {
|
||||||
color: #ffc107;
|
color: #ffc107;
|
||||||
}
|
}
|
||||||
|
|
||||||
.console-filter-btn:nth-child(3).active {
|
.console-filter-btn:nth-child(4).active {
|
||||||
color: #ffd54f;
|
color: #ffd54f;
|
||||||
border-color: #ffc107;
|
border-color: #ffc107;
|
||||||
}
|
}
|
||||||
|
|
||||||
.console-filter-btn:nth-child(4) {
|
.console-filter-btn:nth-child(5) {
|
||||||
color: #f44336;
|
color: #f44336;
|
||||||
}
|
}
|
||||||
|
|
||||||
.console-filter-btn:nth-child(4).active {
|
.console-filter-btn:nth-child(5).active {
|
||||||
color: #ef5350;
|
color: #ef5350;
|
||||||
border-color: #f44336;
|
border-color: #f44336;
|
||||||
}
|
}
|
||||||
@@ -215,6 +224,19 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.log-entry-client {
|
||||||
|
color: #10b981;
|
||||||
|
font-size: 9px;
|
||||||
|
white-space: nowrap;
|
||||||
|
padding: 1px 6px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: rgba(16, 185, 129, 0.15);
|
||||||
|
border: 1px solid rgba(16, 185, 129, 0.4);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: var(--font-family-mono);
|
||||||
|
}
|
||||||
|
|
||||||
.log-entry-remote {
|
.log-entry-remote {
|
||||||
border-left: 2px solid #4a9eff;
|
border-left: 2px solid #4a9eff;
|
||||||
background: rgba(74, 158, 255, 0.05);
|
background: rgba(74, 158, 255, 0.05);
|
||||||
|
|||||||
@@ -45,6 +45,35 @@
|
|||||||
animation: pulse-green 1.5s ease-in-out infinite;
|
animation: pulse-green 1.5s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hierarchy-search {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
padding: var(--spacing-sm) var(--spacing-md);
|
||||||
|
border-bottom: 1px solid var(--color-border-default);
|
||||||
|
background-color: var(--color-bg-base);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hierarchy-search svg {
|
||||||
|
color: var(--color-text-tertiary);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hierarchy-search input {
|
||||||
|
flex: 1;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hierarchy-search input::placeholder {
|
||||||
|
color: var(--color-text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
.hierarchy-content {
|
.hierarchy-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
@@ -171,6 +200,21 @@
|
|||||||
color: var(--color-text-tertiary);
|
color: var(--color-text-tertiary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.entity-tag {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 2px 6px;
|
||||||
|
background-color: rgba(139, 92, 246, 0.15);
|
||||||
|
border: 1px solid rgba(139, 92, 246, 0.4);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
color: rgb(167, 139, 250);
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
font-family: var(--font-family-mono);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.component-count {
|
.component-count {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export interface LogEntry {
|
|||||||
source: string;
|
source: string;
|
||||||
message: string;
|
message: string;
|
||||||
args: unknown[];
|
args: unknown[];
|
||||||
|
clientId?: string; // 远程客户端ID
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LogListener = (entry: LogEntry) => void;
|
export type LogListener = (entry: LogEntry) => void;
|
||||||
@@ -124,14 +125,15 @@ export class LogService implements IService {
|
|||||||
/**
|
/**
|
||||||
* 添加远程日志(从远程游戏接收)
|
* 添加远程日志(从远程游戏接收)
|
||||||
*/
|
*/
|
||||||
public addRemoteLog(level: LogLevel, message: string, timestamp?: Date): void {
|
public addRemoteLog(level: LogLevel, message: string, timestamp?: Date, clientId?: string): void {
|
||||||
const entry: LogEntry = {
|
const entry: LogEntry = {
|
||||||
id: this.nextId++,
|
id: this.nextId++,
|
||||||
timestamp: timestamp || new Date(),
|
timestamp: timestamp || new Date(),
|
||||||
level,
|
level,
|
||||||
source: 'remote',
|
source: 'remote',
|
||||||
message,
|
message,
|
||||||
args: []
|
args: [],
|
||||||
|
clientId
|
||||||
};
|
};
|
||||||
|
|
||||||
this.logs.push(entry);
|
this.logs.push(entry);
|
||||||
|
|||||||
Reference in New Issue
Block a user