fix: Windows spawn, prefab path, preview API, asset deps, error messages; add execute-context resource, tool descriptions, scene/script/asset tools
Bug fixes (verified against Cocos Creator 3.8.8): - diagnostics: shell:true for .cmd/.bat on Windows (T125/T126) - prefabs: duplicatePrefab target resolves under assets/ (T420) - cocos-project: add preview.open candidate for 3.8.8 (T444) - assets-advanced: detect directory assets in inspectAssetDependencies (T110) - scene: improve component-not-found errors with compilation hint (T429) Documentation improvements: - tool-registry: add enum and injected vars to execute_javascript, path format examples, asset ref limitation note - resources: add cocos://mcp/execute-context resource with variables, patterns, pitfalls New tools (core 37->38, full 101->110): - scene-management: create_scene, query_scene_state (core), copy_paste_node, rename_node, reparent_node - scripts: create_script with component/plain templates - prefabs: create_prefab - assets-advanced: batch_asset_ops, find_unused_assets
This commit is contained in:
+5
-1
@@ -68,7 +68,11 @@ function findTsConfig(projectPath, explicitPath) {
|
||||
|
||||
function runExec(file, args, cwd) {
|
||||
return new Promise((resolve) => {
|
||||
execFile(file, args, { cwd, maxBuffer: 8 * 1024 * 1024 }, (error, stdout, stderr) => {
|
||||
const options = { cwd, maxBuffer: 8 * 1024 * 1024 };
|
||||
if (process.platform === 'win32' && /\.(cmd|bat)$/.test(file)) {
|
||||
options.shell = true;
|
||||
}
|
||||
execFile(file, args, options, (error, stdout, stderr) => {
|
||||
resolve({
|
||||
code: error && typeof error.code === 'number' ? error.code : 0,
|
||||
stdout: stdout || '',
|
||||
|
||||
+27
-1
@@ -169,7 +169,9 @@ async function duplicatePrefab(projectPath, options = {}) {
|
||||
throw new Error(`Prefab source file was not found: ${source}`);
|
||||
}
|
||||
|
||||
const targetPath = resolveProjectPath(projectPath, target.endsWith('.prefab') ? target : `${target}.prefab`);
|
||||
const rawTarget = target.endsWith('.prefab') ? target : `${target}.prefab`;
|
||||
const targetUnderAssets = rawTarget.replace(/^assets[\\/]/, '');
|
||||
const targetPath = resolveProjectPath(projectPath, path.join('assets', targetUnderAssets));
|
||||
const assetsRoot = path.join(projectPath, 'assets');
|
||||
const relativeToAssets = path.relative(assetsRoot, targetPath);
|
||||
if (relativeToAssets.startsWith('..') || path.isAbsolute(relativeToAssets)) {
|
||||
@@ -259,8 +261,32 @@ async function revertPrefabInstance(nodeUuid) {
|
||||
throw lastError || new Error('No prefab revert editor message was available.');
|
||||
}
|
||||
|
||||
async function createPrefab(options = {}) {
|
||||
const nodeUuid = String(options.nodeUuid || '').trim();
|
||||
const savePath = String(options.savePath || '').trim();
|
||||
if (!nodeUuid) {
|
||||
throw new Error('nodeUuid is required.');
|
||||
}
|
||||
if (!savePath) {
|
||||
throw new Error('savePath is required.');
|
||||
}
|
||||
if (!savePath.endsWith('.prefab')) {
|
||||
throw new Error('savePath must end with .prefab');
|
||||
}
|
||||
// Note: 'create-prefab' is not a public message in Cocos Creator 3.8.8
|
||||
// (Public: No in editor-messages-3.8.8.md). It works but may change in future versions.
|
||||
const result = await requestEditorMessage('scene', 'create-prefab', nodeUuid, savePath);
|
||||
return {
|
||||
created: true,
|
||||
nodeUuid,
|
||||
savePath,
|
||||
result,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
applyPrefabInstance,
|
||||
createPrefab,
|
||||
duplicatePrefab,
|
||||
editPrefabJson,
|
||||
inspectPrefab,
|
||||
|
||||
@@ -70,6 +70,7 @@ class ResourceProvider {
|
||||
createResource('cocos://logs/editor', `${projectName} Editor Logs`, 'Recent MCP runtime logs and tool interaction history.'),
|
||||
createResource('cocos://logs/project', `${projectName} Project Logs`, 'Recent tails from common project log files.'),
|
||||
createResource('cocos://mcp/interactions', `${projectName} MCP Interactions`, 'Recent MCP tool interaction summaries.'),
|
||||
createResource('cocos://mcp/execute-context', `${projectName} Execute Context Guide`, 'Available variables and code patterns for execute_javascript scene and editor contexts.'),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -131,6 +132,8 @@ class ResourceProvider {
|
||||
return this.getProjectLogsText(projectPath);
|
||||
case 'cocos://mcp/interactions':
|
||||
return this.interactionLog.summary();
|
||||
case 'cocos://mcp/execute-context':
|
||||
return this.getExecuteContextGuide();
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -286,6 +289,100 @@ class ResourceProvider {
|
||||
].join('\n'))
|
||||
.join('\n\n---\n\n');
|
||||
}
|
||||
|
||||
getExecuteContextGuide() {
|
||||
return [
|
||||
'Execute JavaScript Context Guide',
|
||||
'',
|
||||
'This document describes the variables and code patterns available in',
|
||||
'execute_javascript for both scene and editor contexts.',
|
||||
'',
|
||||
'=== Scene Context (context="scene") ===',
|
||||
'',
|
||||
'Injected Variables:',
|
||||
' cc - Cocos engine module. Use cc.Sprite, cc.Node, cc.Vec3, etc.',
|
||||
' scene - Active scene root node (cc.Node). Same as director.getScene().',
|
||||
' director - cc.director.',
|
||||
' require - Node.js require (with Cocos module paths).',
|
||||
' Editor - Editor global API (if running in editor).',
|
||||
' args - User-passed args object.',
|
||||
'',
|
||||
'Return Patterns:',
|
||||
' 1. Direct return: return { myResult: 42 };',
|
||||
' 2. run function: async function run(env) { return result; }',
|
||||
' 3. module.exports: module.exports = async (env) => result;',
|
||||
'',
|
||||
'Key Rules:',
|
||||
' - Use cc.Sprite, cc.Node, cc.UITransform — NOT bare Sprite/Node.',
|
||||
' - Do NOT redeclare scene, director, cc — they are already in scope.',
|
||||
' - Find nodes by traversing scene.children recursively (see pattern below).',
|
||||
' - Load assets with callback-style assetManager.loadAny (see pattern below).',
|
||||
'',
|
||||
'Pattern: Find a node by UUID',
|
||||
' let target = null;',
|
||||
' function search(n) {',
|
||||
' if (n.uuid === "NODE_UUID") { target = n; return; }',
|
||||
' for (const c of n.children) { search(c); if (target) return; }',
|
||||
' }',
|
||||
' search(scene);',
|
||||
'',
|
||||
'Pattern: Load and set an asset reference (SpriteFrame, Texture, etc.)',
|
||||
' const { Sprite, assetManager, SpriteFrame } = cc;',
|
||||
' const sf = await new Promise((resolve, reject) => {',
|
||||
' assetManager.loadAny(',
|
||||
' { uuid: "ASSET_UUID", type: SpriteFrame },',
|
||||
' (err, asset) => { if (err) reject(err); else resolve(asset); }',
|
||||
' );',
|
||||
' });',
|
||||
' sprite.spriteFrame = sf;',
|
||||
'',
|
||||
'Pattern: Create a node with components',
|
||||
' const { Node, UITransform, Sprite } = cc;',
|
||||
' const node = new Node("MyNode");',
|
||||
' node.layer = parent.layer;',
|
||||
' node.addComponent(UITransform);',
|
||||
' node.addComponent(Sprite);',
|
||||
' parent.addChild(node);',
|
||||
'',
|
||||
'=== Editor Context (context="editor") ===',
|
||||
'',
|
||||
'Injected Variables:',
|
||||
' require - Node.js require.',
|
||||
' Editor - Editor global API.',
|
||||
' args - User-passed args object.',
|
||||
' context - Runtime context (projectPath, projectName, cocosVersion, etc.).',
|
||||
' helpers - { getStatus, listTools, readResource, callTool,',
|
||||
' listClientTargets, getClientConfig, configureClient }.',
|
||||
' fs - Node.js fs module.',
|
||||
' path - Node.js path module.',
|
||||
' os - Node.js os module.',
|
||||
'',
|
||||
'Pattern: Call asset-db from editor context',
|
||||
' const info = await Editor.Message.request("asset-db", "query-asset-info", "ASSET_UUID");',
|
||||
'',
|
||||
'Pattern: Call a scene method from editor context',
|
||||
' const result = await Editor.Message.request(',
|
||||
' "scene", "execute-scene-script", { name: "funplay-cocos-mcp", method: "inspectNode", args: { uuid: "NODE_UUID" } }',
|
||||
' );',
|
||||
'',
|
||||
'=== Common Pitfalls ===',
|
||||
'',
|
||||
'1. set_component_property cannot set asset references (SpriteFrame, Texture,',
|
||||
' Material, Font, AudioClip). Use execute_javascript with loadAny instead.',
|
||||
'',
|
||||
'2. create_node only accepts parentPath (e.g. "Canvas/Player"), not parentUuid.',
|
||||
' Use get_hierarchy first to find the full path.',
|
||||
'',
|
||||
'3. assetManager.loadAny(uuid) without callback returns null in scene context.',
|
||||
' Always use callback style: loadAny({uuid, type}, (err, asset) => {}).',
|
||||
'',
|
||||
'4. Custom script components (e.g. TestComponent) require the script to be',
|
||||
' compiled before add_component or findComponent can find them.',
|
||||
'',
|
||||
'5. Node hierarchy paths are slash-separated from scene root, e.g.',
|
||||
' "should_hide_in_hierarchy/TestPrefab/Sprite". Use get_hierarchy to verify.',
|
||||
].join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
+56
-38
@@ -31,6 +31,7 @@ const {
|
||||
} = require('./project-instructions');
|
||||
const {
|
||||
applyPrefabInstance,
|
||||
createPrefab,
|
||||
duplicatePrefab,
|
||||
editPrefabJson,
|
||||
inspectPrefab,
|
||||
@@ -41,6 +42,8 @@ const { createAssetsAdvancedTools } = require('./tools/assets-advanced');
|
||||
const { createCocosProjectTools } = require('./tools/cocos-project');
|
||||
const { buildSnippet, createFileTools, refreshAssets } = require('./tools/files');
|
||||
const { createSceneEventTools } = require('./tools/scene-events');
|
||||
const { createSceneManagementTools } = require('./tools/scene-management');
|
||||
const { createScriptTools } = require('./tools/scripts');
|
||||
const { captureDesktopScreenshot, captureEditorWindowScreenshot, capturePanelScreenshot } = require('./screenshots');
|
||||
const { checkForUpdate } = require('./update-checker');
|
||||
const { assertJavascriptSafety } = require('./javascript-safety');
|
||||
@@ -358,8 +361,8 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
||||
description: '[primary] Execute JavaScript in either the scene or editor context. Use context=\"scene\" for live scene/runtime inspection and mutation, or context=\"editor\" for Editor APIs, asset-db workflows, MCP orchestration, local filesystem access, and higher-level automation. Prefer this as the main flexible tool when many narrow tools would be noisy.',
|
||||
inputSchema: createSchema(
|
||||
{
|
||||
context: { type: 'string', description: 'Execution context: scene or editor.' },
|
||||
code: { type: 'string', description: 'JavaScript code to execute. May directly return a value, define run(env), or export a function.' },
|
||||
context: { type: 'string', enum: ['scene', 'editor'], description: 'Execution context. Scene injects: cc, scene, director, require, Editor, args. Editor injects: require, Editor, args, context, helpers, fs, path, os. Read cocos://mcp/execute-context resource for details.' },
|
||||
code: { type: 'string', description: 'JavaScript code. May directly return a value, define run(env), or use module.exports. In scene context, use cc.Sprite, cc.Node (not bare Sprite/Node). Load assets via cc.assetManager.loadAny({uuid, type}, cb) callback style.' },
|
||||
args: { type: 'object', description: 'Optional JSON object passed into the script.' },
|
||||
safety_checks: { type: 'boolean', description: 'Override the project default JavaScript safety checks for this call.' },
|
||||
},
|
||||
@@ -387,7 +390,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
||||
description: '[compat] Execute JavaScript in the active Cocos scene context. Prefer execute_javascript with context="scene" as the main unified tool; use this when you specifically want the scene-only compatibility entrypoint.',
|
||||
inputSchema: createSchema(
|
||||
{
|
||||
code: { type: 'string', description: 'JavaScript code to execute inside the scene script context.' },
|
||||
code: { type: 'string', description: 'JavaScript code. Injected vars: cc, scene, director, require, Editor, args. Use cc.Sprite etc. (not bare Sprite). See cocos://mcp/execute-context resource.' },
|
||||
args: { type: 'object', description: 'Optional JSON object passed to the scene script.' },
|
||||
safety_checks: { type: 'boolean', description: 'Override the project default JavaScript safety checks for this call.' },
|
||||
},
|
||||
@@ -404,7 +407,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
||||
description: '[compat] Execute JavaScript in the editor/browser context. Prefer execute_javascript with context="editor" as the main unified tool; use this when you specifically want the editor-only compatibility entrypoint.',
|
||||
inputSchema: createSchema(
|
||||
{
|
||||
code: { type: 'string', description: 'JavaScript code to execute inside the editor context.' },
|
||||
code: { type: 'string', description: 'JavaScript code. Injected vars: require, Editor, args, context, helpers, fs, path, os. See cocos://mcp/execute-context resource.' },
|
||||
args: { type: 'object', description: 'Optional JSON object passed to the editor script.' },
|
||||
safety_checks: { type: 'boolean', description: 'Override the project default JavaScript safety checks for this call.' },
|
||||
},
|
||||
@@ -665,7 +668,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
||||
inputSchema: createSchema(
|
||||
{
|
||||
name: { type: 'string', description: 'Name of the node to create.' },
|
||||
parentPath: { type: 'string', description: 'Optional parent node path.' },
|
||||
parentPath: { type: 'string', description: 'Parent node hierarchy path, e.g. "Canvas/Player". Slash-separated from scene root. Use get_hierarchy to find the full path.' },
|
||||
position: { type: 'object', description: 'Optional position {x,y,z}.' },
|
||||
scale: { type: 'object', description: 'Optional scale {x,y,z}.' },
|
||||
eulerAngles: { type: 'object', description: 'Optional rotation {x,y,z} in degrees.' },
|
||||
@@ -681,7 +684,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
||||
description: 'Delete a node by path, uuid, or name.',
|
||||
inputSchema: createSchema(
|
||||
{
|
||||
path: { type: 'string', description: 'Hierarchy path.' },
|
||||
path: { type: 'string', description: 'Hierarchy path, e.g. "Canvas/Player".' },
|
||||
uuid: { type: 'string', description: 'Node uuid.' },
|
||||
name: { type: 'string', description: 'Fallback exact node name.' },
|
||||
},
|
||||
@@ -695,7 +698,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
||||
description: 'Update node position, rotation, scale, or active state.',
|
||||
inputSchema: createSchema(
|
||||
{
|
||||
path: { type: 'string', description: 'Hierarchy path.' },
|
||||
path: { type: 'string', description: 'Hierarchy path, e.g. "Canvas/Player".' },
|
||||
uuid: { type: 'string', description: 'Node uuid.' },
|
||||
name: { type: 'string', description: 'Fallback exact node name.' },
|
||||
position: { type: 'object', description: 'Position {x,y,z}.' },
|
||||
@@ -715,6 +718,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
||||
handler: async () => getRuntimeContext(),
|
||||
},
|
||||
...createCocosProjectTools({ createSchema }),
|
||||
...createSceneManagementTools({ createSchema, sceneBridge }),
|
||||
{
|
||||
name: 'list_scenes',
|
||||
profile: 'core',
|
||||
@@ -757,6 +761,19 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
||||
return { count: assets.length, prefabs: assets.slice(0, 200) };
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'create_prefab',
|
||||
profile: 'full',
|
||||
description: '[core] Create a prefab asset from a scene node.',
|
||||
inputSchema: createSchema(
|
||||
{
|
||||
nodeUuid: { type: 'string', description: 'UUID of the scene node to save as prefab.' },
|
||||
savePath: { type: 'string', description: 'Asset-db URL, e.g. db://assets/prefabs/Enemy.prefab.' },
|
||||
},
|
||||
['nodeUuid', 'savePath']
|
||||
),
|
||||
handler: async (args) => createPrefab(args),
|
||||
},
|
||||
{
|
||||
name: 'inspect_prefab',
|
||||
profile: 'core',
|
||||
@@ -836,7 +853,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
||||
inputSchema: createSchema(
|
||||
{
|
||||
prefabUuid: { type: 'string', description: 'Prefab asset uuid, db url, or path.' },
|
||||
parentPath: { type: 'string', description: 'Optional parent node path.' },
|
||||
parentPath: { type: 'string', description: 'Parent node hierarchy path, e.g. "Canvas/Player". Slash-separated from scene root.' },
|
||||
name: { type: 'string', description: 'Optional override node name.' },
|
||||
position: { type: 'object', description: 'Optional position {x,y,z}; fallback runtime path only.' },
|
||||
},
|
||||
@@ -881,7 +898,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
||||
description: '[specialist] Inspect whether a scene node is linked to a prefab instance and return prefab metadata when available.',
|
||||
inputSchema: createSchema(
|
||||
{
|
||||
path: { type: 'string', description: 'Node hierarchy path.' },
|
||||
path: { type: 'string', description: 'Node hierarchy path, e.g. "Canvas/Player".' },
|
||||
uuid: { type: 'string', description: 'Node uuid.' },
|
||||
name: { type: 'string', description: 'Fallback exact node name.' },
|
||||
},
|
||||
@@ -895,7 +912,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
||||
description: '[core] Apply a scene prefab instance back to its associated prefab asset using the Cocos editor scene apply-prefab message.',
|
||||
inputSchema: createSchema(
|
||||
{
|
||||
path: { type: 'string', description: 'Node hierarchy path.' },
|
||||
path: { type: 'string', description: 'Node hierarchy path, e.g. "Canvas/Player".' },
|
||||
uuid: { type: 'string', description: 'Node uuid.' },
|
||||
name: { type: 'string', description: 'Fallback exact node name.' },
|
||||
},
|
||||
@@ -909,7 +926,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
||||
description: '[core] Revert a scene prefab instance from its associated prefab asset using available Cocos editor prefab revert messages.',
|
||||
inputSchema: createSchema(
|
||||
{
|
||||
path: { type: 'string', description: 'Node hierarchy path.' },
|
||||
path: { type: 'string', description: 'Node hierarchy path, e.g. "Canvas/Player".' },
|
||||
uuid: { type: 'string', description: 'Node uuid.' },
|
||||
name: { type: 'string', description: 'Fallback exact node name.' },
|
||||
},
|
||||
@@ -924,7 +941,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
||||
inputSchema: createSchema(
|
||||
{
|
||||
prefabUuid: { type: 'string', description: 'Prefab asset uuid.' },
|
||||
parentPath: { type: 'string', description: 'Optional parent node path.' },
|
||||
parentPath: { type: 'string', description: 'Parent node hierarchy path, e.g. "Canvas/Player". Slash-separated from scene root.' },
|
||||
name: { type: 'string', description: 'Optional override node name.' },
|
||||
position: { type: 'object', description: 'Optional position {x,y,z}.' },
|
||||
},
|
||||
@@ -1034,7 +1051,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
||||
description: '[core] List components attached to a scene node.',
|
||||
inputSchema: createSchema(
|
||||
{
|
||||
path: { type: 'string', description: 'Node hierarchy path.' },
|
||||
path: { type: 'string', description: 'Node hierarchy path, e.g. "Canvas/Player".' },
|
||||
uuid: { type: 'string', description: 'Node uuid.' },
|
||||
name: { type: 'string', description: 'Fallback exact node name.' },
|
||||
},
|
||||
@@ -1048,10 +1065,10 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
||||
description: '[core] Inspect a component attached to a node.',
|
||||
inputSchema: createSchema(
|
||||
{
|
||||
path: { type: 'string', description: 'Node hierarchy path.' },
|
||||
path: { type: 'string', description: 'Node hierarchy path, e.g. "Canvas/Player".' },
|
||||
uuid: { type: 'string', description: 'Node uuid.' },
|
||||
name: { type: 'string', description: 'Fallback exact node name.' },
|
||||
componentName: { type: 'string', description: 'Component class name.' },
|
||||
componentName: { type: 'string', description: 'Component class name, e.g. Sprite, Label, Button. Custom scripts must be compiled.' },
|
||||
index: { type: 'number', description: 'Optional component index.' },
|
||||
},
|
||||
[]
|
||||
@@ -1064,10 +1081,10 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
||||
description: 'Add a component to a node by component class name.',
|
||||
inputSchema: createSchema(
|
||||
{
|
||||
path: { type: 'string', description: 'Node hierarchy path.' },
|
||||
path: { type: 'string', description: 'Node hierarchy path, e.g. "Canvas/Player".' },
|
||||
uuid: { type: 'string', description: 'Node uuid.' },
|
||||
name: { type: 'string', description: 'Fallback exact node name.' },
|
||||
componentName: { type: 'string', description: 'Component class name, for example Sprite or cc.UITransform.' },
|
||||
componentName: { type: 'string', description: 'Component class name, e.g. Sprite, Label, Button, UITransform. Custom script components require the script to be compiled first.' },
|
||||
},
|
||||
['componentName']
|
||||
),
|
||||
@@ -1079,10 +1096,10 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
||||
description: 'Remove a component from a node by name or index.',
|
||||
inputSchema: createSchema(
|
||||
{
|
||||
path: { type: 'string', description: 'Node hierarchy path.' },
|
||||
path: { type: 'string', description: 'Node hierarchy path, e.g. "Canvas/Player".' },
|
||||
uuid: { type: 'string', description: 'Node uuid.' },
|
||||
name: { type: 'string', description: 'Fallback exact node name.' },
|
||||
componentName: { type: 'string', description: 'Component class name.' },
|
||||
componentName: { type: 'string', description: 'Component class name, e.g. Sprite, Label, Button. Custom scripts must be compiled.' },
|
||||
index: { type: 'number', description: 'Optional component index.' },
|
||||
},
|
||||
[]
|
||||
@@ -1095,13 +1112,13 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
||||
description: 'Set a component property by dot path using a JSON value.',
|
||||
inputSchema: createSchema(
|
||||
{
|
||||
path: { type: 'string', description: 'Node hierarchy path.' },
|
||||
path: { type: 'string', description: 'Node hierarchy path, e.g. "Canvas/Player".' },
|
||||
uuid: { type: 'string', description: 'Node uuid.' },
|
||||
name: { type: 'string', description: 'Fallback exact node name.' },
|
||||
componentName: { type: 'string', description: 'Component class name.' },
|
||||
componentName: { type: 'string', description: 'Component class name, e.g. Sprite, Label, Button. Custom scripts must be compiled.' },
|
||||
index: { type: 'number', description: 'Optional component index.' },
|
||||
propertyPath: { type: 'string', description: 'Property path such as color.r or enabled.' },
|
||||
valueJson: { type: 'string', description: 'JSON encoded value to assign, for example true, 12, \"hero\", or {\"x\":1}.' },
|
||||
propertyPath: { type: 'string', description: 'Property path using dot notation, e.g. "color.r", "enabled", "size.width".' },
|
||||
valueJson: { type: 'string', description: 'JSON encoded value, e.g. true, 12, "hero", {"x":1}. NOTE: Cannot set asset references (SpriteFrame, Texture, Material, etc.) — use execute_javascript with cc.assetManager.loadAny({uuid, type}, cb) for those.' },
|
||||
},
|
||||
['propertyPath', 'valueJson']
|
||||
),
|
||||
@@ -1121,12 +1138,12 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
||||
description: 'Reset or clear a component property by dot path.',
|
||||
inputSchema: createSchema(
|
||||
{
|
||||
path: { type: 'string', description: 'Node hierarchy path.' },
|
||||
path: { type: 'string', description: 'Node hierarchy path, e.g. "Canvas/Player".' },
|
||||
uuid: { type: 'string', description: 'Node uuid.' },
|
||||
name: { type: 'string', description: 'Fallback exact node name.' },
|
||||
componentName: { type: 'string', description: 'Component class name.' },
|
||||
componentName: { type: 'string', description: 'Component class name, e.g. Sprite, Label, Button. Custom scripts must be compiled.' },
|
||||
index: { type: 'number', description: 'Optional component index.' },
|
||||
propertyPath: { type: 'string', description: 'Property path such as color.r or enabled.' },
|
||||
propertyPath: { type: 'string', description: 'Property path using dot notation, e.g. "color.r", "enabled", "size.width".' },
|
||||
},
|
||||
['propertyPath']
|
||||
),
|
||||
@@ -1139,7 +1156,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
||||
inputSchema: createSchema(
|
||||
{
|
||||
name: { type: 'string', description: 'Canvas node name.' },
|
||||
parentPath: { type: 'string', description: 'Optional parent node path.' },
|
||||
parentPath: { type: 'string', description: 'Parent node hierarchy path, e.g. "Canvas/Player". Slash-separated from scene root.' },
|
||||
width: { type: 'number', description: 'Canvas width.' },
|
||||
height: { type: 'number', description: 'Canvas height.' },
|
||||
position: { type: 'object', description: 'Optional position {x,y,z}.' },
|
||||
@@ -1155,7 +1172,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
||||
inputSchema: createSchema(
|
||||
{
|
||||
name: { type: 'string', description: 'Label node name.' },
|
||||
parentPath: { type: 'string', description: 'Optional parent node path.' },
|
||||
parentPath: { type: 'string', description: 'Parent node hierarchy path, e.g. "Canvas/Player". Slash-separated from scene root.' },
|
||||
text: { type: 'string', description: 'Label text.' },
|
||||
fontSize: { type: 'number', description: 'Font size.' },
|
||||
width: { type: 'number', description: 'UI width.' },
|
||||
@@ -1174,7 +1191,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
||||
inputSchema: createSchema(
|
||||
{
|
||||
name: { type: 'string', description: 'Button node name.' },
|
||||
parentPath: { type: 'string', description: 'Optional parent node path.' },
|
||||
parentPath: { type: 'string', description: 'Parent node hierarchy path, e.g. "Canvas/Player". Slash-separated from scene root.' },
|
||||
text: { type: 'string', description: 'Button text.' },
|
||||
width: { type: 'number', description: 'Button width.' },
|
||||
height: { type: 'number', description: 'Button height.' },
|
||||
@@ -1194,8 +1211,8 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
||||
inputSchema: createSchema(
|
||||
{
|
||||
name: { type: 'string', description: 'Sprite node name.' },
|
||||
parentPath: { type: 'string', description: 'Optional parent node path.' },
|
||||
spriteFrameUuid: { type: 'string', description: 'Optional SpriteFrame asset uuid.' },
|
||||
parentPath: { type: 'string', description: 'Parent node hierarchy path, e.g. "Canvas/Player". Slash-separated from scene root.' },
|
||||
spriteFrameUuid: { type: 'string', description: 'SpriteFrame asset uuid, e.g. "57520716-48c8-4a19-8acf-41c9f8777fb0@f9941". Use list_assets to find available SpriteFrames.' },
|
||||
width: { type: 'number', description: 'UI width.' },
|
||||
height: { type: 'number', description: 'UI height.' },
|
||||
color: { type: 'string', description: 'Sprite color as #RRGGBB or #RRGGBBAA.' },
|
||||
@@ -1219,7 +1236,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
||||
inputSchema: createSchema(
|
||||
{
|
||||
name: { type: 'string', description: 'Camera node name.' },
|
||||
parentPath: { type: 'string', description: 'Optional parent node path.' },
|
||||
parentPath: { type: 'string', description: 'Parent node hierarchy path, e.g. "Canvas/Player". Slash-separated from scene root.' },
|
||||
priority: { type: 'number', description: 'Camera priority.' },
|
||||
visibility: { type: 'number', description: 'Camera visibility mask.' },
|
||||
clearFlags: { type: 'number', description: 'Camera clear flags.' },
|
||||
@@ -1272,7 +1289,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
||||
description: 'Add an AnimationClip asset to a node Animation component.',
|
||||
inputSchema: createSchema(
|
||||
{
|
||||
path: { type: 'string', description: 'Node hierarchy path.' },
|
||||
path: { type: 'string', description: 'Node hierarchy path, e.g. "Canvas/Player".' },
|
||||
uuid: { type: 'string', description: 'Node uuid.' },
|
||||
name: { type: 'string', description: 'Fallback exact node name.' },
|
||||
clipUuid: { type: 'string', description: 'AnimationClip asset uuid.' },
|
||||
@@ -1288,7 +1305,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
||||
description: '[core] Play an Animation component clip on a node.',
|
||||
inputSchema: createSchema(
|
||||
{
|
||||
path: { type: 'string', description: 'Node hierarchy path.' },
|
||||
path: { type: 'string', description: 'Node hierarchy path, e.g. "Canvas/Player".' },
|
||||
uuid: { type: 'string', description: 'Node uuid.' },
|
||||
name: { type: 'string', description: 'Fallback exact node name.' },
|
||||
clipName: { type: 'string', description: 'Optional clip name.' },
|
||||
@@ -1303,7 +1320,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
||||
description: '[core] Stop an Animation component clip on a node.',
|
||||
inputSchema: createSchema(
|
||||
{
|
||||
path: { type: 'string', description: 'Node hierarchy path.' },
|
||||
path: { type: 'string', description: 'Node hierarchy path, e.g. "Canvas/Player".' },
|
||||
uuid: { type: 'string', description: 'Node uuid.' },
|
||||
name: { type: 'string', description: 'Fallback exact node name.' },
|
||||
clipName: { type: 'string', description: 'Optional clip name.' },
|
||||
@@ -1499,7 +1516,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
||||
description: '[core] Emit a custom event on a target scene node with an optional JSON payload.',
|
||||
inputSchema: createSchema(
|
||||
{
|
||||
path: { type: 'string', description: 'Node hierarchy path.' },
|
||||
path: { type: 'string', description: 'Node hierarchy path, e.g. "Canvas/Player".' },
|
||||
uuid: { type: 'string', description: 'Node uuid.' },
|
||||
name: { type: 'string', description: 'Fallback exact node name.' },
|
||||
eventName: { type: 'string', description: 'Event name to emit.' },
|
||||
@@ -1524,16 +1541,17 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
||||
handler: async (args) => sceneBridge.call('simulateButtonClick', args),
|
||||
},
|
||||
...createSceneEventTools({ createSchema, sceneBridge }),
|
||||
...createScriptTools({ createSchema, getRuntimeContext }),
|
||||
{
|
||||
name: 'invoke_component_method',
|
||||
profile: 'full',
|
||||
description: '[core] Invoke a method on a component for runtime validation and test hooks.',
|
||||
inputSchema: createSchema(
|
||||
{
|
||||
path: { type: 'string', description: 'Node hierarchy path.' },
|
||||
path: { type: 'string', description: 'Node hierarchy path, e.g. "Canvas/Player".' },
|
||||
uuid: { type: 'string', description: 'Node uuid.' },
|
||||
name: { type: 'string', description: 'Fallback exact node name.' },
|
||||
componentName: { type: 'string', description: 'Component class name.' },
|
||||
componentName: { type: 'string', description: 'Component class name, e.g. Sprite, Label, Button. Custom scripts must be compiled.' },
|
||||
index: { type: 'number', description: 'Optional component index.' },
|
||||
methodName: { type: 'string', description: 'Method name to invoke.' },
|
||||
args: { type: 'array', description: 'Optional argument array.' },
|
||||
|
||||
@@ -97,8 +97,12 @@ function collectUuidReferences(content) {
|
||||
|
||||
async function inspectAssetDependencies(projectPath, target, options = {}) {
|
||||
const info = await queryAssetInfo(target);
|
||||
const isDirectory = info && (info.isDirectory || info.type === 'folder' || (info.url && !path.extname(info.url)));
|
||||
const filePath = assetFilePath(projectPath, info);
|
||||
if (!filePath) {
|
||||
if (isDirectory) {
|
||||
throw new Error(`Cannot inspect dependencies of a directory asset: ${target}. Please specify a file asset (e.g. a .prefab, .scene, or .mat file).`);
|
||||
}
|
||||
throw new Error(`Asset file was not found: ${target}`);
|
||||
}
|
||||
|
||||
@@ -210,12 +214,190 @@ function createAssetsAdvancedTools({ createSchema, getRuntimeContext }) {
|
||||
return await validateAssetDependencies(projectPath, args);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'batch_asset_ops',
|
||||
profile: 'full',
|
||||
description: '[core] Batch import or delete assets in the Cocos asset database.',
|
||||
inputSchema: createSchema(
|
||||
{
|
||||
action: {
|
||||
type: 'string',
|
||||
enum: ['import', 'delete'],
|
||||
description: 'Batch operation.',
|
||||
},
|
||||
sourceDirectory: {
|
||||
type: 'string',
|
||||
description: 'Local filesystem source path (import only).',
|
||||
},
|
||||
targetDirectory: {
|
||||
type: 'string',
|
||||
description: 'Asset-db target URL, e.g. db://assets/textures (import only).',
|
||||
},
|
||||
urls: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'Asset URLs to delete (delete only).',
|
||||
},
|
||||
},
|
||||
['action']
|
||||
),
|
||||
handler: async (args) => {
|
||||
const action = String(args.action || '').trim();
|
||||
|
||||
if (action === 'import') {
|
||||
const sourceDirectory = String(args.sourceDirectory || '').trim();
|
||||
const targetDirectory = String(args.targetDirectory || '').trim();
|
||||
if (!sourceDirectory) {
|
||||
throw new Error('sourceDirectory is required for action: import.');
|
||||
}
|
||||
if (!targetDirectory) {
|
||||
throw new Error('targetDirectory is required for action: import.');
|
||||
}
|
||||
if (!global.Editor || !Editor.Message || typeof Editor.Message.request !== 'function') {
|
||||
throw new Error('Editor.Message.request is unavailable in the Cocos extension host.');
|
||||
}
|
||||
const result = await Editor.Message.request('asset-db', 'import-asset', sourceDirectory, targetDirectory);
|
||||
return { action, sourceDirectory, targetDirectory, result };
|
||||
}
|
||||
|
||||
if (action === 'delete') {
|
||||
const urls = Array.isArray(args.urls) ? args.urls.filter(Boolean) : [];
|
||||
if (urls.length === 0) {
|
||||
throw new Error('urls is required for action: delete.');
|
||||
}
|
||||
if (!global.Editor || !Editor.Message || typeof Editor.Message.request !== 'function') {
|
||||
throw new Error('Editor.Message.request is unavailable in the Cocos extension host.');
|
||||
}
|
||||
const results = [];
|
||||
for (const url of urls) {
|
||||
try {
|
||||
await Editor.Message.request('asset-db', 'delete-asset', url);
|
||||
results.push({ url, deleted: true });
|
||||
} catch (error) {
|
||||
results.push({ url, deleted: false, error: error.message });
|
||||
}
|
||||
}
|
||||
return { action, urls, results };
|
||||
}
|
||||
|
||||
throw new Error(`Unknown action: ${action}. Must be one of: import, delete.`);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'find_unused_assets',
|
||||
profile: 'full',
|
||||
description: '[core] Find assets not referenced by any scene, prefab, or animation in the project.',
|
||||
inputSchema: createSchema(
|
||||
{
|
||||
directory: {
|
||||
type: 'string',
|
||||
description: 'Asset-db directory to scan. Default: db://assets.',
|
||||
},
|
||||
excludeDirectories: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'Directories to exclude from the reference scan.',
|
||||
},
|
||||
assetTypes: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'Filter by Cocos asset type, e.g. ["cc.Texture2D", "cc.AudioClip"].',
|
||||
},
|
||||
limit: { type: 'number', description: 'Max results. Default 200.' },
|
||||
},
|
||||
[]
|
||||
),
|
||||
handler: async (args) => {
|
||||
const { projectPath } = getRuntimeContext();
|
||||
return await findUnusedAssets(projectPath, args);
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const SERIALIZED_EXTENSIONS = ['.scene', '.prefab', '.anim', '.mat'];
|
||||
const SKIP_UNUSED_EXTENSIONS = ['.ts', '.scene'];
|
||||
|
||||
async function findUnusedAssets(projectPath, options = {}, listAssetsFn) {
|
||||
const listAssetsRef = typeof listAssetsFn === 'function' ? listAssetsFn : listAssets;
|
||||
const directory = options.directory || 'db://assets';
|
||||
const excludeDirs = Array.isArray(options.excludeDirectories) ? options.excludeDirectories : [];
|
||||
const assetTypes = Array.isArray(options.assetTypes) ? options.assetTypes : undefined;
|
||||
const limit = Number.isFinite(options.limit) ? Math.max(1, options.limit) : 200;
|
||||
|
||||
const allAssets = await listAssetsRef({
|
||||
pattern: directory + '/**',
|
||||
ccType: assetTypes,
|
||||
});
|
||||
|
||||
const referencedUuids = new Set();
|
||||
const assetsDir = path.join(projectPath, 'assets');
|
||||
|
||||
function scanDir(dir) {
|
||||
let entries;
|
||||
try {
|
||||
entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
} catch (_) {
|
||||
return;
|
||||
}
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
if (entry.name === 'node_modules' || entry.name === 'temp' || entry.name === 'library') {
|
||||
continue;
|
||||
}
|
||||
const relativePath = path.relative(assetsDir, fullPath).replace(/\\/g, '/');
|
||||
if (excludeDirs.some((excl) => relativePath.startsWith(String(excl)))) {
|
||||
continue;
|
||||
}
|
||||
scanDir(fullPath);
|
||||
} else {
|
||||
const ext = path.extname(entry.name).toLowerCase();
|
||||
if (!SERIALIZED_EXTENSIONS.includes(ext)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const content = fs.readFileSync(fullPath, 'utf8');
|
||||
const refs = collectUuidReferences(content);
|
||||
for (const ref of refs) {
|
||||
referencedUuids.add(ref.uuid);
|
||||
}
|
||||
} catch (_) {
|
||||
// Skip unreadable files
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scanDir(assetsDir);
|
||||
|
||||
const unused = allAssets.filter((asset) => {
|
||||
if (!asset || !asset.uuid) {
|
||||
return false;
|
||||
}
|
||||
if (asset.isDirectory) {
|
||||
return false;
|
||||
}
|
||||
const url = asset.url || asset.source || '';
|
||||
const ext = path.extname(url).toLowerCase();
|
||||
if (SKIP_UNUSED_EXTENSIONS.includes(ext)) {
|
||||
return false;
|
||||
}
|
||||
return !referencedUuids.has(asset.uuid);
|
||||
});
|
||||
|
||||
return {
|
||||
totalScanned: allAssets.length,
|
||||
unusedCount: unused.length,
|
||||
referencedCount: referencedUuids.size,
|
||||
unused: unused.slice(0, limit),
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
collectUuidReferences,
|
||||
createAssetsAdvancedTools,
|
||||
findUnusedAssets,
|
||||
inspectAssetDependencies,
|
||||
validateAssetDependencies,
|
||||
};
|
||||
|
||||
@@ -174,6 +174,7 @@ function createCocosProjectTools({ createSchema }) {
|
||||
[]
|
||||
),
|
||||
handler: async (args) => await tryEditorRequests([
|
||||
{ channel: 'preview', method: 'open', args: [args || {}] },
|
||||
{ channel: 'preview', method: 'start', args: [args || {}] },
|
||||
{ channel: 'preview', method: 'open-preview', args: [args || {}] },
|
||||
{ channel: 'builder', method: 'preview', args: [args || {}] },
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { tryEditorRequests } = require('./cocos-project');
|
||||
const { openAsset } = require('../assets');
|
||||
|
||||
const TEMPLATE_PATH = path.join(__dirname, '..', '..', 'resources', 'template.scene');
|
||||
|
||||
function buildSceneContent(sceneName, templatePath) {
|
||||
const tplPath = templatePath || TEMPLATE_PATH;
|
||||
let content;
|
||||
try {
|
||||
content = fs.readFileSync(tplPath, 'utf8');
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to read scene template at ${tplPath}: ${err.message}`);
|
||||
}
|
||||
const json = JSON.parse(content);
|
||||
if (json[0] && json[0]._name !== undefined) {
|
||||
json[0]._name = sceneName;
|
||||
}
|
||||
if (json[1] && json[1]._name !== undefined) {
|
||||
json[1]._name = sceneName;
|
||||
}
|
||||
return JSON.stringify(json);
|
||||
}
|
||||
|
||||
function createSceneManagementTools({ createSchema, sceneBridge }) {
|
||||
return [
|
||||
{
|
||||
name: 'create_scene',
|
||||
profile: 'full',
|
||||
description: '[core] Create a new scene asset in the Cocos project and optionally open it.',
|
||||
inputSchema: createSchema(
|
||||
{
|
||||
sceneName: { type: 'string', description: 'Name of the new scene (without extension).' },
|
||||
savePath: { type: 'string', description: 'Asset-db URL, e.g. db://assets/scenes/NewScene.scene.' },
|
||||
open: { type: 'boolean', description: 'Whether to open the scene after creation. Default true.' },
|
||||
overwrite: { type: 'boolean', description: 'Overwrite if the asset already exists. Default false.' },
|
||||
},
|
||||
['sceneName', 'savePath']
|
||||
),
|
||||
handler: async (args) => {
|
||||
const sceneName = String(args.sceneName || '').trim();
|
||||
const savePath = String(args.savePath || '').trim();
|
||||
if (!sceneName) {
|
||||
throw new Error('sceneName is required.');
|
||||
}
|
||||
if (!savePath) {
|
||||
throw new Error('savePath is required.');
|
||||
}
|
||||
if (!savePath.endsWith('.scene')) {
|
||||
throw new Error('savePath must end with .scene');
|
||||
}
|
||||
|
||||
const content = buildSceneContent(sceneName);
|
||||
const createArgs = [savePath, content];
|
||||
if (args.overwrite === true) {
|
||||
createArgs.push({ overwrite: true });
|
||||
}
|
||||
|
||||
const result = await tryEditorRequests([
|
||||
{ channel: 'asset-db', method: 'create-asset', args: createArgs },
|
||||
]);
|
||||
|
||||
const url = (result.result && (result.result.url || result.result.source)) || savePath;
|
||||
const uuid = result.result && result.result.uuid;
|
||||
|
||||
if (args.open !== false) {
|
||||
try {
|
||||
await openAsset(url);
|
||||
} catch (_) {
|
||||
// Opening is best-effort; creation succeeded.
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
created: true,
|
||||
sceneName,
|
||||
url,
|
||||
uuid,
|
||||
opened: args.open !== false,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'query_scene_state',
|
||||
profile: 'core',
|
||||
description: '[specialist] Query scene state: dirty (unsaved changes), ready, or soft-reload.',
|
||||
inputSchema: createSchema(
|
||||
{
|
||||
action: {
|
||||
type: 'string',
|
||||
enum: ['is_dirty', 'is_ready', 'soft_reload'],
|
||||
description: 'State query or action.',
|
||||
},
|
||||
},
|
||||
['action']
|
||||
),
|
||||
handler: async (args) => {
|
||||
const action = String(args.action || '').trim();
|
||||
const IPC_MAP = {
|
||||
is_dirty: [
|
||||
{ channel: 'scene', method: 'query-dirty' },
|
||||
],
|
||||
is_ready: [
|
||||
{ channel: 'scene', method: 'query-is-ready' },
|
||||
],
|
||||
soft_reload: [
|
||||
{ channel: 'scene', method: 'soft-reload' },
|
||||
],
|
||||
};
|
||||
const candidates = IPC_MAP[action];
|
||||
if (!candidates) {
|
||||
throw new Error(`Unknown action: ${action}. Must be one of: is_dirty, is_ready, soft_reload.`);
|
||||
}
|
||||
const result = await tryEditorRequests(candidates);
|
||||
return {
|
||||
action,
|
||||
result: result.result,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'copy_paste_node',
|
||||
profile: 'full',
|
||||
description: '[core] Copy, cut, or paste scene nodes via the Cocos editor clipboard.',
|
||||
inputSchema: createSchema(
|
||||
{
|
||||
action: {
|
||||
type: 'string',
|
||||
enum: ['copy', 'cut', 'paste', 'duplicate'],
|
||||
description: 'Clipboard or duplication action.',
|
||||
},
|
||||
uuids: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'Node UUIDs to copy, cut, or duplicate.',
|
||||
},
|
||||
target: { type: 'string', description: 'Target parent node UUID for paste.' },
|
||||
keepWorldTransform: {
|
||||
type: 'boolean',
|
||||
description: 'Keep world transform on paste. Default false.',
|
||||
},
|
||||
},
|
||||
['action']
|
||||
),
|
||||
handler: async (args) => {
|
||||
const action = String(args.action || '').trim();
|
||||
|
||||
if (action === 'copy' || action === 'cut') {
|
||||
const uuids = Array.isArray(args.uuids) ? args.uuids.filter(Boolean) : [];
|
||||
if (uuids.length === 0) {
|
||||
throw new Error(`uuids is required for action: ${action}.`);
|
||||
}
|
||||
const method = action === 'copy' ? 'copy-node' : 'cut-node';
|
||||
const result = await tryEditorRequests([
|
||||
{ channel: 'scene', method, args: [uuids] },
|
||||
]);
|
||||
return { action, uuids, result: result.result };
|
||||
}
|
||||
|
||||
if (action === 'duplicate') {
|
||||
const uuids = Array.isArray(args.uuids) ? args.uuids.filter(Boolean) : [];
|
||||
if (uuids.length === 0) {
|
||||
throw new Error('uuids is required for action: duplicate.');
|
||||
}
|
||||
const result = await tryEditorRequests([
|
||||
{ channel: 'scene', method: 'duplicate-node', args: [uuids] },
|
||||
]);
|
||||
return { action, uuids, result: result.result };
|
||||
}
|
||||
|
||||
if (action === 'paste') {
|
||||
const target = String(args.target || '').trim();
|
||||
if (!target) {
|
||||
throw new Error('target is required for action: paste.');
|
||||
}
|
||||
const keepWorldTransform = args.keepWorldTransform === true;
|
||||
const pasteOpts = { target, keepWorldTransform };
|
||||
const uuids = Array.isArray(args.uuids) ? args.uuids.filter(Boolean) : [];
|
||||
if (uuids.length > 0) {
|
||||
pasteOpts.uuids = uuids;
|
||||
}
|
||||
const result = await tryEditorRequests([
|
||||
{
|
||||
channel: 'scene',
|
||||
method: 'paste-node',
|
||||
args: [pasteOpts],
|
||||
},
|
||||
]);
|
||||
return { action, target, keepWorldTransform, result: result.result };
|
||||
}
|
||||
|
||||
throw new Error(`Unknown action: ${action}. Must be one of: copy, cut, paste, duplicate.`);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'rename_node',
|
||||
profile: 'full',
|
||||
description: '[core] Rename a scene node.',
|
||||
inputSchema: createSchema(
|
||||
{
|
||||
path: { type: 'string', description: 'Node hierarchy path.' },
|
||||
uuid: { type: 'string', description: 'Node UUID.' },
|
||||
name: { type: 'string', description: 'Fallback exact node name.' },
|
||||
newName: { type: 'string', description: 'New name for the node.' },
|
||||
},
|
||||
['newName']
|
||||
),
|
||||
handler: async (args) => sceneBridge.call('renameNode', args),
|
||||
},
|
||||
{
|
||||
name: 'reparent_node',
|
||||
profile: 'full',
|
||||
description: '[core] Move a node to a new parent in the scene hierarchy.',
|
||||
inputSchema: createSchema(
|
||||
{
|
||||
path: { type: 'string', description: 'Node hierarchy path to move.' },
|
||||
uuid: { type: 'string', description: 'Node UUID to move.' },
|
||||
name: { type: 'string', description: 'Fallback exact node name.' },
|
||||
targetPath: { type: 'string', description: 'New parent node path.' },
|
||||
targetUuid: { type: 'string', description: 'New parent node UUID.' },
|
||||
targetName: { type: 'string', description: 'Fallback exact new parent name.' },
|
||||
siblingIndex: {
|
||||
type: 'number',
|
||||
description: 'Optional insert position among siblings. Default: append.',
|
||||
},
|
||||
keepWorldTransform: {
|
||||
type: 'boolean',
|
||||
description: 'Keep world transform after reparenting. Default false.',
|
||||
},
|
||||
},
|
||||
[]
|
||||
),
|
||||
handler: async (args) => sceneBridge.call('reparentNode', args),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
buildSceneContent,
|
||||
createSceneManagementTools,
|
||||
};
|
||||
@@ -0,0 +1,148 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { resolveProjectPath } = require('../path-safety');
|
||||
const { refreshAssets } = require('./files');
|
||||
|
||||
const PRIMITIVE_TYPES = new Set(['Number', 'String', 'Boolean']);
|
||||
|
||||
function generateComponentTemplate(className, properties = []) {
|
||||
const ccTypes = new Set(['Component']);
|
||||
const propLines = [];
|
||||
|
||||
for (const prop of properties) {
|
||||
const propName = String(prop.name || 'property').trim();
|
||||
const propType = String(prop.type || 'Number').trim();
|
||||
const defaultValue = prop.default !== undefined ? prop.default : getDefaultForType(propType);
|
||||
|
||||
if (!PRIMITIVE_TYPES.has(propType)) {
|
||||
ccTypes.add(propType);
|
||||
}
|
||||
|
||||
if (PRIMITIVE_TYPES.has(propType)) {
|
||||
propLines.push(` @property`);
|
||||
propLines.push(` ${propName}: ${typeToTs(propType)} = ${defaultValue};`);
|
||||
} else {
|
||||
propLines.push(` @property(${propType})`);
|
||||
propLines.push(` ${propName}: ${propType} | null = null;`);
|
||||
}
|
||||
}
|
||||
|
||||
const importTypes = Array.from(ccTypes).join(', ');
|
||||
const propsBlock = propLines.length > 0
|
||||
? propLines.join('\n')
|
||||
: ' // Add properties here';
|
||||
|
||||
return `import { _decorator, ${importTypes} } from 'cc';
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
@ccclass('${className}')
|
||||
export class ${className} extends Component {
|
||||
${propsBlock}
|
||||
|
||||
start() {
|
||||
}
|
||||
|
||||
update(deltaTime: number) {
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
function generatePlainTemplate(className) {
|
||||
return `export class ${className} {
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
function getDefaultForType(type) {
|
||||
switch (type) {
|
||||
case 'Number': return '0';
|
||||
case 'String': return "''";
|
||||
case 'Boolean': return 'false';
|
||||
default: return 'null';
|
||||
}
|
||||
}
|
||||
|
||||
function typeToTs(type) {
|
||||
switch (type) {
|
||||
case 'Number': return 'number';
|
||||
case 'String': return 'string';
|
||||
case 'Boolean': return 'boolean';
|
||||
default: return type;
|
||||
}
|
||||
}
|
||||
|
||||
function createScriptTools({ createSchema, getRuntimeContext }) {
|
||||
return [
|
||||
{
|
||||
name: 'create_script',
|
||||
profile: 'full',
|
||||
description: '[core] Create a new TypeScript component script with a standard Cocos template.',
|
||||
inputSchema: createSchema(
|
||||
{
|
||||
scriptName: { type: 'string', description: 'Class name in PascalCase, e.g. PlayerController.' },
|
||||
savePath: { type: 'string', description: 'Asset-db URL or project path, e.g. db://assets/scripts/PlayerController.ts.' },
|
||||
template: {
|
||||
type: 'string',
|
||||
enum: ['component', 'plain'],
|
||||
description: 'Script template. Default: component.',
|
||||
},
|
||||
properties: {
|
||||
type: 'array',
|
||||
items: { type: 'object' },
|
||||
description: 'Optional @property declarations. Each item: { name, type, default }. Type can be Number, String, Boolean, or a CC class name like Label, Sprite, Node.',
|
||||
},
|
||||
},
|
||||
['scriptName', 'savePath']
|
||||
),
|
||||
handler: async (args) => {
|
||||
const scriptName = String(args.scriptName || '').trim();
|
||||
let savePath = String(args.savePath || '').trim();
|
||||
if (!scriptName) {
|
||||
throw new Error('scriptName is required.');
|
||||
}
|
||||
if (!savePath) {
|
||||
throw new Error('savePath is required.');
|
||||
}
|
||||
if (!savePath.endsWith('.ts')) {
|
||||
throw new Error('savePath must end with .ts');
|
||||
}
|
||||
if (savePath.startsWith('db://')) {
|
||||
savePath = savePath.slice('db://'.length);
|
||||
}
|
||||
|
||||
const templateType = args.template === 'plain' ? 'plain' : 'component';
|
||||
const properties = Array.isArray(args.properties) ? args.properties : [];
|
||||
|
||||
const content = templateType === 'plain'
|
||||
? generatePlainTemplate(scriptName)
|
||||
: generateComponentTemplate(scriptName, properties);
|
||||
|
||||
const { projectPath } = getRuntimeContext();
|
||||
const fullPath = resolveProjectPath(projectPath, savePath);
|
||||
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
||||
fs.writeFileSync(fullPath, content, 'utf8');
|
||||
|
||||
const refreshMessage = await refreshAssets(projectPath, fullPath);
|
||||
|
||||
return {
|
||||
created: true,
|
||||
scriptName,
|
||||
savePath,
|
||||
template: templateType,
|
||||
className: scriptName,
|
||||
contentLength: content.length,
|
||||
refreshMessage,
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createScriptTools,
|
||||
generateComponentTemplate,
|
||||
generatePlainTemplate,
|
||||
};
|
||||
Reference in New Issue
Block a user