598 lines
24 KiB
TypeScript
598 lines
24 KiB
TypeScript
import { ToolDefinition, ToolResponse, ToolExecutor, NodeInfo } from '../types';
|
||
|
||
export class NodeTools implements ToolExecutor {
|
||
getTools(): ToolDefinition[] {
|
||
return [
|
||
{
|
||
name: 'create_node',
|
||
description: 'Create a new node in the scene. IMPORTANT: You should always provide parentUuid to specify where to create the node. If parentUuid is not provided, the node will be created at the scene root.',
|
||
inputSchema: {
|
||
type: 'object',
|
||
properties: {
|
||
name: {
|
||
type: 'string',
|
||
description: 'Node name'
|
||
},
|
||
parentUuid: {
|
||
type: 'string',
|
||
description: 'Parent node UUID. STRONGLY RECOMMENDED: Always provide this parameter. Use get_current_scene or get_all_nodes to find parent UUIDs. If not provided, node will be created at scene root.'
|
||
},
|
||
nodeType: {
|
||
type: 'string',
|
||
description: 'Node type: Node, 2DNode, 3DNode',
|
||
enum: ['Node', '2DNode', '3DNode'],
|
||
default: 'Node'
|
||
},
|
||
siblingIndex: {
|
||
type: 'number',
|
||
description: 'Sibling index for ordering (-1 means append at end)',
|
||
default: -1
|
||
}
|
||
},
|
||
required: ['name']
|
||
}
|
||
},
|
||
{
|
||
name: 'get_node_info',
|
||
description: 'Get node information by UUID',
|
||
inputSchema: {
|
||
type: 'object',
|
||
properties: {
|
||
uuid: {
|
||
type: 'string',
|
||
description: 'Node UUID'
|
||
}
|
||
},
|
||
required: ['uuid']
|
||
}
|
||
},
|
||
{
|
||
name: 'find_nodes',
|
||
description: 'Find nodes by name pattern',
|
||
inputSchema: {
|
||
type: 'object',
|
||
properties: {
|
||
pattern: {
|
||
type: 'string',
|
||
description: 'Name pattern to search'
|
||
},
|
||
exactMatch: {
|
||
type: 'boolean',
|
||
description: 'Exact match or partial match',
|
||
default: false
|
||
}
|
||
},
|
||
required: ['pattern']
|
||
}
|
||
},
|
||
{
|
||
name: 'find_node_by_name',
|
||
description: 'Find first node by exact name',
|
||
inputSchema: {
|
||
type: 'object',
|
||
properties: {
|
||
name: {
|
||
type: 'string',
|
||
description: 'Node name to find'
|
||
}
|
||
},
|
||
required: ['name']
|
||
}
|
||
},
|
||
{
|
||
name: 'get_all_nodes',
|
||
description: 'Get all nodes in the scene with their UUIDs',
|
||
inputSchema: {
|
||
type: 'object',
|
||
properties: {}
|
||
}
|
||
},
|
||
{
|
||
name: 'set_node_property',
|
||
description: 'Set node property value',
|
||
inputSchema: {
|
||
type: 'object',
|
||
properties: {
|
||
uuid: {
|
||
type: 'string',
|
||
description: 'Node UUID'
|
||
},
|
||
property: {
|
||
type: 'string',
|
||
description: 'Property name (e.g., position, rotation, scale, active)'
|
||
},
|
||
value: {
|
||
description: 'Property value'
|
||
}
|
||
},
|
||
required: ['uuid', 'property', 'value']
|
||
}
|
||
},
|
||
{
|
||
name: 'delete_node',
|
||
description: 'Delete a node from scene',
|
||
inputSchema: {
|
||
type: 'object',
|
||
properties: {
|
||
uuid: {
|
||
type: 'string',
|
||
description: 'Node UUID to delete'
|
||
}
|
||
},
|
||
required: ['uuid']
|
||
}
|
||
},
|
||
{
|
||
name: 'move_node',
|
||
description: 'Move node to new parent',
|
||
inputSchema: {
|
||
type: 'object',
|
||
properties: {
|
||
nodeUuid: {
|
||
type: 'string',
|
||
description: 'Node UUID to move'
|
||
},
|
||
newParentUuid: {
|
||
type: 'string',
|
||
description: 'New parent node UUID'
|
||
},
|
||
siblingIndex: {
|
||
type: 'number',
|
||
description: 'Sibling index in new parent',
|
||
default: -1
|
||
}
|
||
},
|
||
required: ['nodeUuid', 'newParentUuid']
|
||
}
|
||
},
|
||
{
|
||
name: 'duplicate_node',
|
||
description: 'Duplicate a node',
|
||
inputSchema: {
|
||
type: 'object',
|
||
properties: {
|
||
uuid: {
|
||
type: 'string',
|
||
description: 'Node UUID to duplicate'
|
||
},
|
||
includeChildren: {
|
||
type: 'boolean',
|
||
description: 'Include children nodes',
|
||
default: true
|
||
}
|
||
},
|
||
required: ['uuid']
|
||
}
|
||
}
|
||
];
|
||
}
|
||
|
||
async execute(toolName: string, args: any): Promise<ToolResponse> {
|
||
switch (toolName) {
|
||
case 'create_node':
|
||
return await this.createNode(args);
|
||
case 'get_node_info':
|
||
return await this.getNodeInfo(args.uuid);
|
||
case 'find_nodes':
|
||
return await this.findNodes(args.pattern, args.exactMatch);
|
||
case 'find_node_by_name':
|
||
return await this.findNodeByName(args.name);
|
||
case 'get_all_nodes':
|
||
return await this.getAllNodes();
|
||
case 'set_node_property':
|
||
return await this.setNodeProperty(args.uuid, args.property, args.value);
|
||
case 'delete_node':
|
||
return await this.deleteNode(args.uuid);
|
||
case 'move_node':
|
||
return await this.moveNode(args.nodeUuid, args.newParentUuid, args.siblingIndex);
|
||
case 'duplicate_node':
|
||
return await this.duplicateNode(args.uuid, args.includeChildren);
|
||
default:
|
||
throw new Error(`Unknown tool: ${toolName}`);
|
||
}
|
||
}
|
||
|
||
private async createNode(args: any): Promise<ToolResponse> {
|
||
return new Promise(async (resolve) => {
|
||
let targetParentUuid = args.parentUuid;
|
||
|
||
// 如果没有提供父节点UUID,获取场景根节点
|
||
if (!targetParentUuid) {
|
||
try {
|
||
const sceneInfo = await Editor.Message.request('scene', 'query-node-tree');
|
||
if (sceneInfo && typeof sceneInfo === 'object' && 'uuid' in sceneInfo) {
|
||
targetParentUuid = sceneInfo.uuid;
|
||
console.log(`No parent specified, using scene root: ${targetParentUuid}`);
|
||
} else if (Array.isArray(sceneInfo) && sceneInfo.length > 0 && sceneInfo[0].uuid) {
|
||
// 如果返回的是数组,使用第一个元素(通常是场景根节点)
|
||
targetParentUuid = sceneInfo[0].uuid;
|
||
console.log(`No parent specified, using scene root: ${targetParentUuid}`);
|
||
} else {
|
||
// 备用方案:尝试获取当前场景
|
||
const currentScene = await Editor.Message.request('scene', 'query-current-scene');
|
||
if (currentScene && currentScene.uuid) {
|
||
targetParentUuid = currentScene.uuid;
|
||
}
|
||
}
|
||
} catch (err) {
|
||
console.warn('Failed to get scene root, will use default behavior');
|
||
}
|
||
}
|
||
|
||
// 如果指定了父节点,先验证父节点是否存在
|
||
if (targetParentUuid) {
|
||
try {
|
||
const parentNode = await Editor.Message.request('scene', 'query-node', targetParentUuid);
|
||
if (!parentNode) {
|
||
resolve({
|
||
success: false,
|
||
error: `Parent node with UUID '${targetParentUuid}' not found`
|
||
});
|
||
return;
|
||
}
|
||
} catch (err) {
|
||
resolve({
|
||
success: false,
|
||
error: `Failed to verify parent node: ${err}`
|
||
});
|
||
return;
|
||
}
|
||
}
|
||
|
||
const nodeData: any = {
|
||
name: args.name,
|
||
type: args.nodeType || 'cc.Node'
|
||
};
|
||
|
||
// 使用正确的create-node API参数结构
|
||
if (targetParentUuid) {
|
||
const createNodeOptions = {
|
||
parent: targetParentUuid,
|
||
name: args.name,
|
||
components: args.nodeType && args.nodeType !== 'Node' ? [args.nodeType] : undefined
|
||
};
|
||
|
||
Editor.Message.request('scene', 'create-node', createNodeOptions).then((nodeUuid: any) => {
|
||
// 如果需要设置特定的兄弟索引,使用set-parent API
|
||
if (args.siblingIndex !== undefined && args.siblingIndex >= 0 && nodeUuid) {
|
||
Editor.Message.request('scene', 'set-parent', {
|
||
parent: targetParentUuid,
|
||
uuids: [nodeUuid],
|
||
keepWorldTransform: false
|
||
}).then(() => {
|
||
resolve({
|
||
success: true,
|
||
data: {
|
||
uuid: nodeUuid,
|
||
name: args.name,
|
||
parentUuid: targetParentUuid,
|
||
message: args.parentUuid
|
||
? `Node '${args.name}' created under specified parent`
|
||
: `Node '${args.name}' created at scene root (no parent specified)`
|
||
}
|
||
});
|
||
}).catch(() => {
|
||
// 即使移动失败,节点已创建,返回成功但带警告
|
||
resolve({
|
||
success: true,
|
||
data: {
|
||
uuid: nodeUuid,
|
||
name: args.name,
|
||
message: `Node '${args.name}' created but may not be under intended parent`,
|
||
warning: 'Failed to move node to specified parent'
|
||
}
|
||
});
|
||
});
|
||
} else {
|
||
resolve({
|
||
success: true,
|
||
data: {
|
||
uuid: nodeUuid,
|
||
name: args.name,
|
||
message: `Node '${args.name}' created successfully`
|
||
}
|
||
});
|
||
}
|
||
}).catch((err: Error) => {
|
||
resolve({ success: false, error: err.message });
|
||
});
|
||
} else {
|
||
// 没有找到场景根节点,使用默认行为(创建在场景根节点)
|
||
const createNodeOptions = {
|
||
name: args.name,
|
||
components: args.nodeType && args.nodeType !== 'Node' ? [args.nodeType] : undefined
|
||
};
|
||
|
||
Editor.Message.request('scene', 'create-node', createNodeOptions).then((result: any) => {
|
||
resolve({
|
||
success: true,
|
||
data: {
|
||
uuid: result,
|
||
name: args.name,
|
||
message: `Node '${args.name}' created at default location (scene root not found)`,
|
||
warning: 'Could not determine scene root, node created at default location'
|
||
}
|
||
});
|
||
}).catch((err: Error) => {
|
||
resolve({ success: false, error: err.message });
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
private async getNodeInfo(uuid: string): Promise<ToolResponse> {
|
||
return new Promise((resolve) => {
|
||
Editor.Message.request('scene', 'query-node', uuid).then((nodeData: any) => {
|
||
if (!nodeData) {
|
||
resolve({
|
||
success: false,
|
||
error: 'Node not found or invalid response'
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 根据实际返回的数据结构解析节点信息
|
||
const info: NodeInfo = {
|
||
uuid: nodeData.uuid?.value || uuid,
|
||
name: nodeData.name?.value || 'Unknown',
|
||
active: nodeData.active?.value !== undefined ? nodeData.active.value : true,
|
||
position: nodeData.position?.value || { x: 0, y: 0, z: 0 },
|
||
rotation: nodeData.rotation?.value || { x: 0, y: 0, z: 0 },
|
||
scale: nodeData.scale?.value || { x: 1, y: 1, z: 1 },
|
||
parent: nodeData.parent?.value?.uuid || null,
|
||
children: nodeData.children || [],
|
||
components: (nodeData.__comps__ || []).map((comp: any) => ({
|
||
type: comp.__type__ || 'Unknown',
|
||
enabled: comp.enabled !== undefined ? comp.enabled : true
|
||
})),
|
||
layer: nodeData.layer?.value || 1073741824,
|
||
mobility: nodeData.mobility?.value || 0
|
||
};
|
||
resolve({ success: true, data: info });
|
||
}).catch((err: Error) => {
|
||
resolve({ success: false, error: err.message });
|
||
});
|
||
});
|
||
}
|
||
|
||
private async findNodes(pattern: string, exactMatch: boolean = false): Promise<ToolResponse> {
|
||
return new Promise((resolve) => {
|
||
// Note: 'query-nodes-by-name' API doesn't exist in official documentation
|
||
// Using tree traversal as primary approach
|
||
Editor.Message.request('scene', 'query-node-tree').then((tree: any) => {
|
||
const nodes: any[] = [];
|
||
|
||
const searchTree = (node: any, currentPath: string = '') => {
|
||
const nodePath = currentPath ? `${currentPath}/${node.name}` : node.name;
|
||
|
||
const matches = exactMatch ?
|
||
node.name === pattern :
|
||
node.name.toLowerCase().includes(pattern.toLowerCase());
|
||
|
||
if (matches) {
|
||
nodes.push({
|
||
uuid: node.uuid,
|
||
name: node.name,
|
||
path: nodePath
|
||
});
|
||
}
|
||
|
||
if (node.children) {
|
||
for (const child of node.children) {
|
||
searchTree(child, nodePath);
|
||
}
|
||
}
|
||
};
|
||
|
||
if (tree) {
|
||
searchTree(tree);
|
||
}
|
||
|
||
resolve({ success: true, data: nodes });
|
||
}).catch((err: Error) => {
|
||
// 备用方案:使用场景脚本
|
||
const options = {
|
||
name: 'cocos-mcp-server',
|
||
method: 'findNodes',
|
||
args: [pattern, exactMatch]
|
||
};
|
||
|
||
Editor.Message.request('scene', 'execute-scene-script', options).then((result: any) => {
|
||
resolve(result);
|
||
}).catch((err2: Error) => {
|
||
resolve({ success: false, error: `Tree search failed: ${err.message}, Scene script failed: ${err2.message}` });
|
||
});
|
||
});
|
||
});
|
||
}
|
||
|
||
private async findNodeByName(name: string): Promise<ToolResponse> {
|
||
return new Promise((resolve) => {
|
||
// 优先尝试使用 Editor API 查询节点树并搜索
|
||
Editor.Message.request('scene', 'query-node-tree').then((tree: any) => {
|
||
const foundNode = this.searchNodeInTree(tree, name);
|
||
if (foundNode) {
|
||
resolve({
|
||
success: true,
|
||
data: {
|
||
uuid: foundNode.uuid,
|
||
name: foundNode.name,
|
||
path: this.getNodePath(foundNode)
|
||
}
|
||
});
|
||
} else {
|
||
resolve({ success: false, error: `Node '${name}' not found` });
|
||
}
|
||
}).catch((err: Error) => {
|
||
// 备用方案:使用场景脚本
|
||
const options = {
|
||
name: 'cocos-mcp-server',
|
||
method: 'findNodeByName',
|
||
args: [name]
|
||
};
|
||
|
||
Editor.Message.request('scene', 'execute-scene-script', options).then((result: any) => {
|
||
resolve(result);
|
||
}).catch((err2: Error) => {
|
||
resolve({ success: false, error: `Direct API failed: ${err.message}, Scene script failed: ${err2.message}` });
|
||
});
|
||
});
|
||
});
|
||
}
|
||
|
||
private searchNodeInTree(node: any, targetName: string): any {
|
||
if (node.name === targetName) {
|
||
return node;
|
||
}
|
||
|
||
if (node.children) {
|
||
for (const child of node.children) {
|
||
const found = this.searchNodeInTree(child, targetName);
|
||
if (found) {
|
||
return found;
|
||
}
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
private async getAllNodes(): Promise<ToolResponse> {
|
||
return new Promise((resolve) => {
|
||
// 尝试查询场景节点树
|
||
Editor.Message.request('scene', 'query-node-tree').then((tree: any) => {
|
||
const nodes: any[] = [];
|
||
|
||
const traverseTree = (node: any) => {
|
||
nodes.push({
|
||
uuid: node.uuid,
|
||
name: node.name,
|
||
type: node.type,
|
||
active: node.active,
|
||
path: this.getNodePath(node)
|
||
});
|
||
|
||
if (node.children) {
|
||
for (const child of node.children) {
|
||
traverseTree(child);
|
||
}
|
||
}
|
||
};
|
||
|
||
if (tree && tree.children) {
|
||
traverseTree(tree);
|
||
}
|
||
|
||
resolve({
|
||
success: true,
|
||
data: {
|
||
totalNodes: nodes.length,
|
||
nodes: nodes
|
||
}
|
||
});
|
||
}).catch((err: Error) => {
|
||
// 备用方案:使用场景脚本
|
||
const options = {
|
||
name: 'cocos-mcp-server',
|
||
method: 'getAllNodes',
|
||
args: []
|
||
};
|
||
|
||
Editor.Message.request('scene', 'execute-scene-script', options).then((result: any) => {
|
||
resolve(result);
|
||
}).catch((err2: Error) => {
|
||
resolve({ success: false, error: `Direct API failed: ${err.message}, Scene script failed: ${err2.message}` });
|
||
});
|
||
});
|
||
});
|
||
}
|
||
|
||
private getNodePath(node: any): string {
|
||
const path = [node.name];
|
||
let current = node.parent;
|
||
while (current && current.name !== 'Canvas') {
|
||
path.unshift(current.name);
|
||
current = current.parent;
|
||
}
|
||
return path.join('/');
|
||
}
|
||
|
||
private async setNodeProperty(uuid: string, property: string, value: any): Promise<ToolResponse> {
|
||
return new Promise((resolve) => {
|
||
// 尝试直接使用 Editor API 设置节点属性
|
||
Editor.Message.request('scene', 'set-property', {
|
||
uuid: uuid,
|
||
path: property,
|
||
dump: {
|
||
value: value
|
||
}
|
||
}).then(() => {
|
||
resolve({
|
||
success: true,
|
||
message: `Property '${property}' updated successfully`
|
||
});
|
||
}).catch((err: Error) => {
|
||
// 如果直接设置失败,尝试使用场景脚本
|
||
const options = {
|
||
name: 'cocos-mcp-server',
|
||
method: 'setNodeProperty',
|
||
args: [uuid, property, value]
|
||
};
|
||
|
||
Editor.Message.request('scene', 'execute-scene-script', options).then((result: any) => {
|
||
resolve(result);
|
||
}).catch((err2: Error) => {
|
||
resolve({ success: false, error: `Direct API failed: ${err.message}, Scene script failed: ${err2.message}` });
|
||
});
|
||
});
|
||
});
|
||
}
|
||
|
||
private async deleteNode(uuid: string): Promise<ToolResponse> {
|
||
return new Promise((resolve) => {
|
||
Editor.Message.request('scene', 'remove-node', { uuid: uuid }).then(() => {
|
||
resolve({
|
||
success: true,
|
||
message: 'Node deleted successfully'
|
||
});
|
||
}).catch((err: Error) => {
|
||
resolve({ success: false, error: err.message });
|
||
});
|
||
});
|
||
}
|
||
|
||
private async moveNode(nodeUuid: string, newParentUuid: string, siblingIndex: number = -1): Promise<ToolResponse> {
|
||
return new Promise((resolve) => {
|
||
// Use correct set-parent API instead of move-node
|
||
Editor.Message.request('scene', 'set-parent', {
|
||
parent: newParentUuid,
|
||
uuids: [nodeUuid],
|
||
keepWorldTransform: false
|
||
}).then(() => {
|
||
resolve({
|
||
success: true,
|
||
message: 'Node moved successfully'
|
||
});
|
||
}).catch((err: Error) => {
|
||
resolve({ success: false, error: err.message });
|
||
});
|
||
});
|
||
}
|
||
|
||
private async duplicateNode(uuid: string, includeChildren: boolean = true): Promise<ToolResponse> {
|
||
return new Promise((resolve) => {
|
||
// Note: includeChildren parameter is accepted for future use but not currently implemented
|
||
Editor.Message.request('scene', 'duplicate-node', uuid).then((result: any) => {
|
||
resolve({
|
||
success: true,
|
||
data: {
|
||
newUuid: result.uuid,
|
||
message: 'Node duplicated successfully'
|
||
}
|
||
});
|
||
}).catch((err: Error) => {
|
||
resolve({ success: false, error: err.message });
|
||
});
|
||
});
|
||
}
|
||
} |