import { ToolDefinition, ToolResponse, ToolExecutor, ConsoleMessage, PerformanceStats, ValidationResult, ValidationIssue } from '../types';
import * as fs from 'fs';
import * as path from 'path';
export class DebugTools implements ToolExecutor {
private consoleMessages: ConsoleMessage[] = [];
private readonly maxMessages = 1000;
constructor() {
this.setupConsoleCapture();
}
private setupConsoleCapture(): void {
// Intercept Editor console messages
// Note: Editor.Message.addBroadcastListener may not be available in all versions
// This is a placeholder for console capture implementation
console.log('Console capture setup - implementation depends on Editor API availability');
}
private addConsoleMessage(message: any): void {
this.consoleMessages.push({
timestamp: new Date().toISOString(),
...message
});
// Keep only latest messages
if (this.consoleMessages.length > this.maxMessages) {
this.consoleMessages.shift();
}
}
getTools(): ToolDefinition[] {
return [
{
name: 'get_console_logs',
description: 'Get editor console logs',
inputSchema: {
type: 'object',
properties: {
limit: {
type: 'number',
description: 'Number of recent logs to retrieve',
default: 100
},
filter: {
type: 'string',
description: 'Filter logs by type',
enum: ['all', 'log', 'warn', 'error', 'info'],
default: 'all'
}
}
}
},
{
name: 'clear_console',
description: 'Clear editor console',
inputSchema: {
type: 'object',
properties: {}
}
},
{
name: 'execute_script',
description: 'Execute JavaScript in scene context',
inputSchema: {
type: 'object',
properties: {
script: {
type: 'string',
description: 'JavaScript code to execute'
}
},
required: ['script']
}
},
{
name: 'get_node_tree',
description: 'Get detailed node tree for debugging',
inputSchema: {
type: 'object',
properties: {
rootUuid: {
type: 'string',
description: 'Root node UUID (optional, uses scene root if not provided)'
},
maxDepth: {
type: 'number',
description: 'Maximum tree depth',
default: 10
}
}
}
},
{
name: 'get_performance_stats',
description: 'Get performance statistics',
inputSchema: {
type: 'object',
properties: {}
}
},
{
name: 'validate_scene',
description: 'Validate current scene for issues',
inputSchema: {
type: 'object',
properties: {
checkMissingAssets: {
type: 'boolean',
description: 'Check for missing asset references',
default: true
},
checkPerformance: {
type: 'boolean',
description: 'Check for performance issues',
default: true
}
}
}
},
{
name: 'get_editor_info',
description: 'Get editor and environment information',
inputSchema: {
type: 'object',
properties: {}
}
},
{
name: 'get_project_logs',
description: 'Get project logs from temp/logs/project.log file',
inputSchema: {
type: 'object',
properties: {
lines: {
type: 'number',
description: 'Number of lines to read from the end of the log file (default: 100)',
default: 100,
minimum: 1,
maximum: 10000
},
filterKeyword: {
type: 'string',
description: 'Filter logs containing specific keyword (optional)'
},
logLevel: {
type: 'string',
description: 'Filter by log level',
enum: ['ERROR', 'WARN', 'INFO', 'DEBUG', 'TRACE', 'ALL'],
default: 'ALL'
}
}
}
},
{
name: 'get_log_file_info',
description: 'Get information about the project log file',
inputSchema: {
type: 'object',
properties: {}
}
},
{
name: 'search_project_logs',
description: 'Search for specific patterns or errors in project logs',
inputSchema: {
type: 'object',
properties: {
pattern: {
type: 'string',
description: 'Search pattern (supports regex)'
},
maxResults: {
type: 'number',
description: 'Maximum number of matching results',
default: 20,
minimum: 1,
maximum: 100
},
contextLines: {
type: 'number',
description: 'Number of context lines to show around each match',
default: 2,
minimum: 0,
maximum: 10
}
},
required: ['pattern']
}
}
];
}
async execute(toolName: string, args: any): Promise<ToolResponse> {
switch (toolName) {
case 'get_console_logs':
return await this.getConsoleLogs(args.limit, args.filter);
case 'clear_console':
return await this.clearConsole();
case 'execute_script':
return await this.executeScript(args.script);
case 'get_node_tree':
return await this.getNodeTree(args.rootUuid, args.maxDepth);
case 'get_performance_stats':
return await this.getPerformanceStats();
case 'validate_scene':
return await this.validateScene(args);
case 'get_editor_info':
return await this.getEditorInfo();
case 'get_project_logs':
return await this.getProjectLogs(args.lines, args.filterKeyword, args.logLevel);
case 'get_log_file_info':
return await this.getLogFileInfo();
case 'search_project_logs':
return await this.searchProjectLogs(args.pattern, args.maxResults, args.contextLines);
default:
throw new Error(`Unknown tool: ${toolName}`);
}
}
private async getConsoleLogs(limit: number = 100, filter: string = 'all'): Promise<ToolResponse> {
let logs = this.consoleMessages;
if (filter !== 'all') {
logs = logs.filter(log => log.type === filter);
}
const recentLogs = logs.slice(-limit);
return {
success: true,
data: {
total: logs.length,
returned: recentLogs.length,
logs: recentLogs
}
};
}
private async clearConsole(): Promise<ToolResponse> {
this.consoleMessages = [];
try {
// Note: Editor.Message.send may not return a promise in all versions
Editor.Message.send('console', 'clear');
return {
success: true,
message: 'Console cleared successfully'
};
} catch (err: any) {
return { success: false, error: err.message };
}
}
private async executeScript(script: string): Promise<ToolResponse> {
return new Promise((resolve) => {
Editor.Message.request('scene', 'execute-scene-script', {
name: 'console',
method: 'eval',
args: [script]
}).then((result: any) => {
resolve({
success: true,
data: {
result: result,
message: 'Script executed successfully'
}
});
}).catch((err: Error) => {
resolve({ success: false, error: err.message });
});
});
}
private async getNodeTree(rootUuid?: string, maxDepth: number = 10): Promise<ToolResponse> {
return new Promise((resolve) => {
const buildTree = async (nodeUuid: string, depth: number = 0): Promise<any> => {
if (depth >= maxDepth) {
return { truncated: true };
}
try {
const nodeData = await Editor.Message.request('scene', 'query-node', nodeUuid);
const tree = {
uuid: nodeData.uuid,
name: nodeData.name,
active: nodeData.active,
components: (nodeData as any).components ? (nodeData as any).components.map((c: any) => c.__type__) : [],
childCount: nodeData.children ? nodeData.children.length : 0,
children: [] as any[]
};
if (nodeData.children && nodeData.children.length > 0) {
for (const childId of nodeData.children) {
const childTree = await buildTree(childId, depth + 1);
tree.children.push(childTree);
}
}
return tree;
} catch (err: any) {
return { error: err.message };
}
};
if (rootUuid) {
buildTree(rootUuid).then(tree => {
resolve({ success: true, data: tree });
});
} else {
Editor.Message.request('scene', 'query-hierarchy').then(async (hierarchy: any) => {
const trees = [];
for (const rootNode of hierarchy.children) {
const tree = await buildTree(rootNode.uuid);
trees.push(tree);
}
resolve({ success: true, data: trees });
}).catch((err: Error) => {
resolve({ success: false, error: err.message });
});
}
});
}
private async getPerformanceStats(): Promise<ToolResponse> {
return new Promise((resolve) => {
Editor.Message.request('scene', 'query-performance').then((stats: any) => {
const perfStats: PerformanceStats = {
nodeCount: stats.nodeCount || 0,
componentCount: stats.componentCount || 0,
drawCalls: stats.drawCalls || 0,
triangles: stats.triangles || 0,
memory: stats.memory || {}
};
resolve({ success: true, data: perfStats });
}).catch(() => {
// Fallback to basic stats
resolve({
success: true,
data: {
message: 'Performance stats not available in edit mode'
}
});
});
});
}
private async validateScene(options: any): Promise<ToolResponse> {
const issues: ValidationIssue[] = [];
try {
// Check for missing assets
if (options.checkMissingAssets) {
const assetCheck = await Editor.Message.request('scene', 'check-missing-assets');
if (assetCheck && assetCheck.missing) {
issues.push({
type: 'error',
category: 'assets',
message: `Found ${assetCheck.missing.length} missing asset references`,
details: assetCheck.missing
});
}
}
// Check for performance issues
if (options.checkPerformance) {
const hierarchy = await Editor.Message.request('scene', 'query-hierarchy');
const nodeCount = this.countNodes(hierarchy.children);
if (nodeCount > 1000) {
issues.push({
type: 'warning',
category: 'performance',
message: `High node count: ${nodeCount} nodes (recommended < 1000)`,
suggestion: 'Consider using object pooling or scene optimization'
});
}
}
const result: ValidationResult = {
valid: issues.length === 0,
issueCount: issues.length,
issues: issues
};
return { success: true, data: result };
} catch (err: any) {
return { success: false, error: err.message };
}
}
private countNodes(nodes: any[]): number {
let count = nodes.length;
for (const node of nodes) {
if (node.children) {
count += this.countNodes(node.children);
}
}
return count;
}
private async getEditorInfo(): Promise<ToolResponse> {
const info = {
editor: {
version: (Editor as any).versions?.editor || 'Unknown',
cocosVersion: (Editor as any).versions?.cocos || 'Unknown',
platform: process.platform,
arch: process.arch,
nodeVersion: process.version
},
project: {
name: Editor.Project.name,
path: Editor.Project.path,
uuid: Editor.Project.uuid
},
memory: process.memoryUsage(),
uptime: process.uptime()
};
return { success: true, data: info };
}
private async getProjectLogs(lines: number = 100, filterKeyword?: string, logLevel: string = 'ALL'): Promise<ToolResponse> {
try {
// Try multiple possible project paths
let logFilePath = '';
const possiblePaths = [
Editor.Project ? Editor.Project.path : null,
'/Users/lizhiyong/NewProject_3',
process.cwd(),
].filter(p => p !== null);
for (const basePath of possiblePaths) {
const testPath = path.join(basePath, 'temp/logs/project.log');
if (fs.existsSync(testPath)) {
logFilePath = testPath;
break;
}
}
if (!logFilePath) {
return {
success: false,
error: `Project log file not found. Tried paths: ${possiblePaths.map(p => path.join(p, 'temp/logs/project.log')).join(', ')}`
};
}
// Read the file content
const logContent = fs.readFileSync(logFilePath, 'utf8');
const logLines = logContent.split('\n').filter(line => line.trim() !== '');
// Get the last N lines
const recentLines = logLines.slice(-lines);
// Apply filters
let filteredLines = recentLines;
// Filter by log level if not 'ALL'
if (logLevel !== 'ALL') {
filteredLines = filteredLines.filter(line =>
line.includes(`[${logLevel}]`) || line.includes(logLevel.toLowerCase())
);
}
// Filter by keyword if provided
if (filterKeyword) {
filteredLines = filteredLines.filter(line =>
line.toLowerCase().includes(filterKeyword.toLowerCase())
);
}
return {
success: true,
data: {
totalLines: logLines.length,
requestedLines: lines,
filteredLines: filteredLines.length,
logLevel: logLevel,
filterKeyword: filterKeyword || null,
logs: filteredLines,
logFilePath: logFilePath
}
};
} catch (error: any) {
return {
success: false,
error: `Failed to read project logs: ${error.message}`
};
}
}
private async getLogFileInfo(): Promise<ToolResponse> {
try {
// Try multiple possible project paths
let logFilePath = '';
const possiblePaths = [
Editor.Project ? Editor.Project.path : null,
'/Users/lizhiyong/NewProject_3',
process.cwd(),
].filter(p => p !== null);
for (const basePath of possiblePaths) {
const testPath = path.join(basePath, 'temp/logs/project.log');
if (fs.existsSync(testPath)) {
logFilePath = testPath;
break;
}
}
if (!logFilePath) {
return {
success: false,
error: `Project log file not found. Tried paths: ${possiblePaths.map(p => path.join(p, 'temp/logs/project.log')).join(', ')}`
};
}
const stats = fs.statSync(logFilePath);
const logContent = fs.readFileSync(logFilePath, 'utf8');
const lineCount = logContent.split('\n').filter(line => line.trim() !== '').length;
return {
success: true,
data: {
filePath: logFilePath,
fileSize: stats.size,
fileSizeFormatted: this.formatFileSize(stats.size),
lastModified: stats.mtime.toISOString(),
lineCount: lineCount,
created: stats.birthtime.toISOString(),
accessible: fs.constants.R_OK
}
};
} catch (error: any) {
return {
success: false,
error: `Failed to get log file info: ${error.message}`
};
}
}
private async searchProjectLogs(pattern: string, maxResults: number = 20, contextLines: number = 2): Promise<ToolResponse> {
try {
// Try multiple possible project paths
let logFilePath = '';
const possiblePaths = [
Editor.Project ? Editor.Project.path : null,
'/Users/lizhiyong/NewProject_3',
process.cwd(),
].filter(p => p !== null);
for (const basePath of possiblePaths) {
const testPath = path.join(basePath, 'temp/logs/project.log');
if (fs.existsSync(testPath)) {
logFilePath = testPath;
break;
}
}
if (!logFilePath) {
return {
success: false,
error: `Project log file not found. Tried paths: ${possiblePaths.map(p => path.join(p, 'temp/logs/project.log')).join(', ')}`
};
}
const logContent = fs.readFileSync(logFilePath, 'utf8');
const logLines = logContent.split('\n');
// Create regex pattern (support both string and regex patterns)
let regex: RegExp;
try {
regex = new RegExp(pattern, 'gi');
} catch {
// If pattern is not valid regex, treat as literal string
regex = new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
}
const matches: any[] = [];
let resultCount = 0;
for (let i = 0; i < logLines.length && resultCount < maxResults; i++) {
const line = logLines[i];
if (regex.test(line)) {
// Get context lines
const contextStart = Math.max(0, i - contextLines);
const contextEnd = Math.min(logLines.length - 1, i + contextLines);
const contextLinesArray = [];
for (let j = contextStart; j <= contextEnd; j++) {
contextLinesArray.push({
lineNumber: j + 1,
content: logLines[j],
isMatch: j === i
});
}
matches.push({
lineNumber: i + 1,
matchedLine: line,
context: contextLinesArray
});
resultCount++;
// Reset regex lastIndex for global search
regex.lastIndex = 0;
}
}
return {
success: true,
data: {
pattern: pattern,
totalMatches: matches.length,
maxResults: maxResults,
contextLines: contextLines,
logFilePath: logFilePath,
matches: matches
}
};
} catch (error: any) {
return {
success: false,
error: `Failed to search project logs: ${error.message}`
};
}
}
private formatFileSize(bytes: number): string {
const units = ['B', 'KB', 'MB', 'GB'];
let size = bytes;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return `${size.toFixed(2)} ${units[unitIndex]}`;
}
}