显示客户端链接的ip:port

This commit is contained in:
YHH
2025-10-16 17:33:43 +08:00
parent 43bdd7e43b
commit 3cda3c2238
8 changed files with 156 additions and 19 deletions

View File

@@ -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)

View File

@@ -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);

View File

@@ -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>

View File

@@ -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>
)} )}

View File

@@ -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
} }
})); }));
} }

View File

@@ -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);

View File

@@ -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;

View File

@@ -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);