Release v0.3.3
This commit is contained in:
+3
-276
@@ -2,7 +2,6 @@
|
||||
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const {
|
||||
clearSelection,
|
||||
deleteAsset,
|
||||
@@ -37,12 +36,14 @@ const {
|
||||
revertPrefabInstance,
|
||||
validatePrefabReferences,
|
||||
} = require('./prefabs');
|
||||
const { buildSnippet, createFileTools, refreshAssets } = require('./tools/files');
|
||||
const { captureDesktopScreenshot, captureEditorWindowScreenshot, capturePanelScreenshot } = require('./screenshots');
|
||||
const { checkForUpdate } = require('./update-checker');
|
||||
const { safeStringify } = require('./utils');
|
||||
const IMAGE_DATA_URI_PREFIX = 'data:image/png;base64,';
|
||||
|
||||
const TOOL_CATEGORY_RULES = [
|
||||
['project', /^(get_project_info|get_editor_state|get_tool_catalog)$/],
|
||||
['updates', /update/],
|
||||
['logs', /log/],
|
||||
['diagnostics', /diagnostic|validate/],
|
||||
@@ -60,7 +61,6 @@ const TOOL_CATEGORY_RULES = [
|
||||
['runtime', /runtime|time_scale|node_event|invoke_component/],
|
||||
['scene', /scene|hierarchy|node/],
|
||||
['execution', /execute_/],
|
||||
['project', /project|editor_state|tool_catalog/],
|
||||
];
|
||||
|
||||
function createSchema(properties, required) {
|
||||
@@ -306,91 +306,6 @@ function toOutput(value) {
|
||||
return safeStringify(value);
|
||||
}
|
||||
|
||||
function matchesPattern(fileName, pattern) {
|
||||
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*');
|
||||
return new RegExp(`^${escaped}$`, 'i').test(fileName);
|
||||
}
|
||||
|
||||
function searchFiles(rootDir, pattern, limit) {
|
||||
const results = [];
|
||||
if (!fs.existsSync(rootDir)) {
|
||||
return results;
|
||||
}
|
||||
|
||||
const stack = [rootDir];
|
||||
while (stack.length && results.length < limit) {
|
||||
const current = stack.pop();
|
||||
const entries = fs.readdirSync(current, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (entry.name === '.git' || entry.name === 'node_modules' || entry.name === 'temp' || entry.name === 'library') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const fullPath = path.join(current, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
stack.push(fullPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (matchesPattern(entry.name, pattern)) {
|
||||
results.push(fullPath);
|
||||
if (results.length >= limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
function readLines(filePath) {
|
||||
return fs.readFileSync(filePath, 'utf8').split(/\r?\n/);
|
||||
}
|
||||
|
||||
function buildSnippet(filePath, lineNumber, contextLines = 3) {
|
||||
const lines = readLines(filePath);
|
||||
const start = Math.max(1, Number(lineNumber || 1) - Math.max(0, contextLines));
|
||||
const end = Math.min(lines.length, Number(lineNumber || 1) + Math.max(0, contextLines));
|
||||
const snippet = [];
|
||||
for (let line = start; line <= end; line += 1) {
|
||||
const marker = line === Number(lineNumber || 1) ? '>' : ' ';
|
||||
snippet.push(`${marker} ${String(line).padStart(4, ' ')} | ${lines[line - 1]}`);
|
||||
}
|
||||
return snippet.join('\n');
|
||||
}
|
||||
|
||||
function replaceAllLiteral(content, search, replacement) {
|
||||
if (!search) {
|
||||
throw new Error('search text is required.');
|
||||
}
|
||||
return content.split(search).join(replacement);
|
||||
}
|
||||
|
||||
async function refreshAssets(projectPath, targetPath) {
|
||||
if (!global.Editor || !Editor.Message || typeof Editor.Message.request !== 'function') {
|
||||
return 'Asset refresh API is unavailable; Cocos Creator should pick up file changes automatically.';
|
||||
}
|
||||
|
||||
const relative = path.relative(path.join(projectPath, 'assets'), targetPath).replace(/\\/g, '/');
|
||||
if (!relative.startsWith('..')) {
|
||||
const dbUrl = `db://assets/${relative}`;
|
||||
try {
|
||||
await Editor.Message.request('asset-db', 'refresh-asset', dbUrl);
|
||||
return `Refreshed asset database for ${dbUrl}`;
|
||||
} catch (error) {
|
||||
try {
|
||||
await Editor.Message.request('asset-db', 'refresh-asset', 'db://assets');
|
||||
return `Refreshed asset database after writing ${dbUrl}`;
|
||||
} catch (innerError) {
|
||||
return `File written, but asset refresh failed: ${innerError.message}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 'File written outside assets directory; no asset-db refresh was needed.';
|
||||
}
|
||||
|
||||
async function resolveNodeUuid(sceneBridge, args) {
|
||||
if (args && args.uuid) {
|
||||
return String(args.uuid);
|
||||
@@ -1337,195 +1252,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
||||
),
|
||||
handler: async (args) => sceneBridge.call('stopAnimation', args),
|
||||
},
|
||||
{
|
||||
name: 'read_file',
|
||||
profile: 'full',
|
||||
description: '[core] Read a file from the Cocos project.',
|
||||
inputSchema: createSchema(
|
||||
{
|
||||
path: { type: 'string', description: 'Project-relative or absolute file path.' },
|
||||
},
|
||||
['path']
|
||||
),
|
||||
handler: async (args) => {
|
||||
const { projectPath } = getRuntimeContext();
|
||||
const fullPath = resolveProjectPath(projectPath, args.path);
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
throw new Error(`File not found: ${args.path}`);
|
||||
}
|
||||
const content = fs.readFileSync(fullPath, 'utf8');
|
||||
return content.length > 12000 ? `${content.slice(0, 12000)}\n... (truncated)` : content;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'get_file_snippet',
|
||||
profile: 'full',
|
||||
description: '[core] Read a focused snippet around a file line number.',
|
||||
inputSchema: createSchema(
|
||||
{
|
||||
path: { type: 'string', description: 'Project-relative or absolute file path.' },
|
||||
line: { type: 'number', description: 'Target line number, starting at 1.' },
|
||||
contextLines: { type: 'number', description: 'Number of surrounding context lines.' },
|
||||
},
|
||||
['path', 'line']
|
||||
),
|
||||
handler: async (args) => {
|
||||
const { projectPath } = getRuntimeContext();
|
||||
const fullPath = resolveProjectPath(projectPath, args.path);
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
throw new Error(`File not found: ${args.path}`);
|
||||
}
|
||||
return buildSnippet(fullPath, args.line, Number.isFinite(args.contextLines) ? args.contextLines : 3);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'write_file',
|
||||
profile: 'full',
|
||||
description: '[core] Write or overwrite a file in the Cocos project.',
|
||||
inputSchema: createSchema(
|
||||
{
|
||||
path: { type: 'string', description: 'Project-relative or absolute file path.' },
|
||||
content: { type: 'string', description: 'File content to write.' },
|
||||
},
|
||||
['path', 'content']
|
||||
),
|
||||
handler: async (args) => {
|
||||
const { projectPath } = getRuntimeContext();
|
||||
const fullPath = resolveProjectPath(projectPath, args.path);
|
||||
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
||||
fs.writeFileSync(fullPath, args.content, 'utf8');
|
||||
return `Wrote ${args.content.length} chars to ${args.path}\n${await refreshAssets(projectPath, fullPath)}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'replace_in_file',
|
||||
profile: 'full',
|
||||
description: '[core] Replace text in a file, useful for script auto-fix loops.',
|
||||
inputSchema: createSchema(
|
||||
{
|
||||
path: { type: 'string', description: 'Project-relative or absolute file path.' },
|
||||
search: { type: 'string', description: 'Literal text to search for.' },
|
||||
replace: { type: 'string', description: 'Replacement text.' },
|
||||
replaceAll: { type: 'boolean', description: 'Replace every occurrence instead of only the first.' },
|
||||
},
|
||||
['path', 'search', 'replace']
|
||||
),
|
||||
handler: async (args) => {
|
||||
const { projectPath } = getRuntimeContext();
|
||||
const fullPath = resolveProjectPath(projectPath, args.path);
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
throw new Error(`File not found: ${args.path}`);
|
||||
}
|
||||
|
||||
const original = fs.readFileSync(fullPath, 'utf8');
|
||||
if (!original.includes(args.search)) {
|
||||
throw new Error(`Search text was not found in ${args.path}`);
|
||||
}
|
||||
|
||||
const updated = args.replaceAll
|
||||
? replaceAllLiteral(original, args.search, args.replace)
|
||||
: original.replace(args.search, args.replace);
|
||||
|
||||
fs.writeFileSync(fullPath, updated, 'utf8');
|
||||
return `Updated ${args.path} (${args.replaceAll ? 'all matches' : 'first match'})\n${await refreshAssets(projectPath, fullPath)}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'search_files',
|
||||
profile: 'full',
|
||||
description: '[core] Search project files by simple wildcard pattern.',
|
||||
inputSchema: createSchema(
|
||||
{
|
||||
pattern: { type: 'string', description: "Wildcard file pattern such as '*.ts' or 'Player*'." },
|
||||
directory: { type: 'string', description: 'Project-relative search root. Defaults to assets.' },
|
||||
limit: { type: 'number', description: 'Maximum number of results to return.' },
|
||||
},
|
||||
['pattern']
|
||||
),
|
||||
handler: async (args) => {
|
||||
const { projectPath } = getRuntimeContext();
|
||||
const searchRoot = resolveProjectPath(projectPath, args.directory || 'assets');
|
||||
if (!fs.existsSync(searchRoot)) {
|
||||
throw new Error(`Directory not found: ${args.directory || 'assets'}`);
|
||||
}
|
||||
|
||||
const limit = Number.isFinite(args.limit) ? Math.max(1, Math.min(500, args.limit)) : 100;
|
||||
const results = searchFiles(searchRoot, args.pattern, limit).map((fullPath) =>
|
||||
path.relative(projectPath, fullPath).replace(/\\/g, '/')
|
||||
);
|
||||
return {
|
||||
count: results.length,
|
||||
files: results,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'list_directory',
|
||||
profile: 'full',
|
||||
description: '[core] List files and directories inside a project directory.',
|
||||
inputSchema: createSchema(
|
||||
{
|
||||
path: { type: 'string', description: 'Project-relative or absolute directory path.' },
|
||||
},
|
||||
['path']
|
||||
),
|
||||
handler: async (args) => {
|
||||
const { projectPath } = getRuntimeContext();
|
||||
const targetPath = resolveProjectPath(projectPath, args.path);
|
||||
if (!fs.existsSync(targetPath) || !fs.statSync(targetPath).isDirectory()) {
|
||||
throw new Error(`Directory not found: ${args.path}`);
|
||||
}
|
||||
|
||||
const entries = fs
|
||||
.readdirSync(targetPath, { withFileTypes: true })
|
||||
.filter((entry) => !entry.name.startsWith('.'))
|
||||
.map((entry) => ({
|
||||
name: entry.name,
|
||||
type: entry.isDirectory() ? 'directory' : 'file',
|
||||
}));
|
||||
return {
|
||||
path: args.path,
|
||||
entries,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'exists',
|
||||
profile: 'full',
|
||||
description: '[core] Check whether a project file or directory exists.',
|
||||
inputSchema: createSchema(
|
||||
{
|
||||
path: { type: 'string', description: 'Project-relative or absolute path.' },
|
||||
},
|
||||
['path']
|
||||
),
|
||||
handler: async (args) => {
|
||||
const { projectPath } = getRuntimeContext();
|
||||
const targetPath = resolveProjectPath(projectPath, args.path);
|
||||
return {
|
||||
path: args.path,
|
||||
exists: fs.existsSync(targetPath),
|
||||
isFile: fs.existsSync(targetPath) ? fs.statSync(targetPath).isFile() : false,
|
||||
isDirectory: fs.existsSync(targetPath) ? fs.statSync(targetPath).isDirectory() : false,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'refresh_assets',
|
||||
profile: 'full',
|
||||
description: '[core] Best-effort asset database refresh for a file or the assets root.',
|
||||
inputSchema: createSchema(
|
||||
{
|
||||
path: { type: 'string', description: 'Optional project-relative file path to refresh.' },
|
||||
},
|
||||
[]
|
||||
),
|
||||
handler: async (args) => {
|
||||
const { projectPath } = getRuntimeContext();
|
||||
const targetPath = resolveProjectPath(projectPath, args.path || 'assets');
|
||||
return await refreshAssets(projectPath, targetPath);
|
||||
},
|
||||
},
|
||||
...createFileTools({ createSchema, getRuntimeContext }),
|
||||
{
|
||||
name: 'run_script_diagnostics',
|
||||
profile: 'core',
|
||||
|
||||
Reference in New Issue
Block a user