Skip to main content
Glama
DaxianLee

Cocos Creator MCP Server Plugin

by DaxianLee
component-tools.ts93.3 kB
import { ToolDefinition, ToolResponse, ToolExecutor, ComponentInfo } from '../types'; export class ComponentTools implements ToolExecutor { getTools(): ToolDefinition[] { return [ { name: 'add_component', description: 'Add a component to a specific node. IMPORTANT: You must provide the nodeUuid parameter to specify which node to add the component to.', inputSchema: { type: 'object', properties: { nodeUuid: { type: 'string', description: 'Target node UUID. REQUIRED: You must specify the exact node to add the component to. Use get_all_nodes or find_node_by_name to get the UUID of the desired node.' }, componentType: { type: 'string', description: 'Component type (e.g., cc.Sprite, cc.Label, cc.Button)' } }, required: ['nodeUuid', 'componentType'] } }, { name: 'remove_component', description: 'Remove a component from a node. componentType must be the component\'s classId (cid, i.e. the type field from getComponents), not the script name or class name. Use getComponents to get the correct cid.', inputSchema: { type: 'object', properties: { nodeUuid: { type: 'string', description: 'Node UUID' }, componentType: { type: 'string', description: 'Component cid (type field from getComponents). Do NOT use script name or class name. Example: "cc.Sprite" or "9b4a7ueT9xD6aRE+AlOusy1"' } }, required: ['nodeUuid', 'componentType'] } }, { name: 'get_components', description: 'Get all components of a node', inputSchema: { type: 'object', properties: { nodeUuid: { type: 'string', description: 'Node UUID' } }, required: ['nodeUuid'] } }, { name: 'get_component_info', description: 'Get specific component information', inputSchema: { type: 'object', properties: { nodeUuid: { type: 'string', description: 'Node UUID' }, componentType: { type: 'string', description: 'Component type to get info for' } }, required: ['nodeUuid', 'componentType'] } }, { name: 'set_component_property', description: 'Set component property values for UI components or custom script components. Supports setting properties of built-in UI components (e.g., cc.Label, cc.Sprite) and custom script components. Note: For node basic properties (name, active, layer, etc.), use set_node_property. For node transform properties (position, rotation, scale, etc.), use set_node_transform.', inputSchema: { type: 'object', properties: { nodeUuid: { type: 'string', description: 'Target node UUID - Must specify the node to operate on' }, componentType: { type: 'string', description: 'Component type - Can be built-in components (e.g., cc.Label) or custom script components (e.g., MyScript). If unsure about component type, use get_components first to retrieve all components on the node.', // 移除enum限制,允许任意组件类型包括自定义脚本 }, property: { type: 'string', description: 'Property name - The property to set. Common properties include:\n' + '• cc.Label: string (text content), fontSize (font size), color (text color)\n' + '• cc.Sprite: spriteFrame (sprite frame), color (tint color), sizeMode (size mode)\n' + '• cc.Button: normalColor (normal color), pressedColor (pressed color), target (target node)\n' + '• cc.UITransform: contentSize (content size), anchorPoint (anchor point)\n' + '• Custom Scripts: Based on properties defined in the script' }, propertyType: { type: 'string', description: 'Property type - Must explicitly specify the property data type for correct value conversion and validation', enum: [ 'string', 'number', 'boolean', 'integer', 'float', 'color', 'vec2', 'vec3', 'size', 'node', 'component', 'spriteFrame', 'prefab', 'asset', 'nodeArray', 'colorArray', 'numberArray', 'stringArray' ] }, value: { description: 'Property value - Use the corresponding data format based on propertyType:\n\n' + '📝 Basic Data Types:\n' + '• string: "Hello World" (text string)\n' + '• number/integer/float: 42 or 3.14 (numeric value)\n' + '• boolean: true or false (boolean value)\n\n' + '🎨 Color Type:\n' + '• color: {"r":255,"g":0,"b":0,"a":255} (RGBA values, range 0-255)\n' + ' - Alternative: "#FF0000" (hexadecimal format)\n' + ' - Transparency: a value controls opacity, 255 = fully opaque, 0 = fully transparent\n\n' + '📐 Vector and Size Types:\n' + '• vec2: {"x":100,"y":50} (2D vector)\n' + '• vec3: {"x":1,"y":2,"z":3} (3D vector)\n' + '• size: {"width":100,"height":50} (size dimensions)\n\n' + '🔗 Reference Types (using UUID strings):\n' + '• node: "target-node-uuid" (node reference)\n' + ' How to get: Use get_all_nodes or find_node_by_name to get node UUIDs\n' + '• component: "target-node-uuid" (component reference)\n' + ' How it works: \n' + ' 1. Provide the UUID of the NODE that contains the target component\n' + ' 2. System auto-detects required component type from property metadata\n' + ' 3. Finds the component on target node and gets its scene __id__\n' + ' 4. Sets reference using the scene __id__ (not node UUID)\n' + ' Example: value="label-node-uuid" will find cc.Label and use its scene ID\n' + '• spriteFrame: "spriteframe-uuid" (sprite frame asset)\n' + ' How to get: Check asset database or use asset browser\n' + '• prefab: "prefab-uuid" (prefab asset)\n' + ' How to get: Check asset database or use asset browser\n' + '• asset: "asset-uuid" (generic asset reference)\n' + ' How to get: Check asset database or use asset browser\n\n' + '📋 Array Types:\n' + '• nodeArray: ["uuid1","uuid2"] (array of node UUIDs)\n' + '• colorArray: [{"r":255,"g":0,"b":0,"a":255}] (array of colors)\n' + '• numberArray: [1,2,3,4,5] (array of numbers)\n' + '• stringArray: ["item1","item2"] (array of strings)' } }, required: ['nodeUuid', 'componentType', 'property', 'propertyType', 'value'] } }, { name: 'attach_script', description: 'Attach a script component to a node', inputSchema: { type: 'object', properties: { nodeUuid: { type: 'string', description: 'Node UUID' }, scriptPath: { type: 'string', description: 'Script asset path (e.g., db://assets/scripts/MyScript.ts)' } }, required: ['nodeUuid', 'scriptPath'] } }, { name: 'get_available_components', description: 'Get list of available component types', inputSchema: { type: 'object', properties: { category: { type: 'string', description: 'Component category filter', enum: ['all', 'renderer', 'ui', 'physics', 'animation', 'audio'], default: 'all' } } } } ]; } async execute(toolName: string, args: any): Promise<ToolResponse> { switch (toolName) { case 'add_component': return await this.addComponent(args.nodeUuid, args.componentType); case 'remove_component': return await this.removeComponent(args.nodeUuid, args.componentType); case 'get_components': return await this.getComponents(args.nodeUuid); case 'get_component_info': return await this.getComponentInfo(args.nodeUuid, args.componentType); case 'set_component_property': return await this.setComponentProperty(args); case 'attach_script': return await this.attachScript(args.nodeUuid, args.scriptPath); case 'get_available_components': return await this.getAvailableComponents(args.category); default: throw new Error(`Unknown tool: ${toolName}`); } } private async addComponent(nodeUuid: string, componentType: string): Promise<ToolResponse> { return new Promise(async (resolve) => { // 先查找节点上是否已存在该组件 const allComponentsInfo = await this.getComponents(nodeUuid); if (allComponentsInfo.success && allComponentsInfo.data?.components) { const existingComponent = allComponentsInfo.data.components.find((comp: any) => comp.type === componentType); if (existingComponent) { resolve({ success: true, message: `Component '${componentType}' already exists on node`, data: { nodeUuid: nodeUuid, componentType: componentType, componentVerified: true, existing: true } }); return; } } // 尝试直接使用 Editor API 添加组件 Editor.Message.request('scene', 'create-component', { uuid: nodeUuid, component: componentType }).then(async (result: any) => { // 等待一段时间让Editor完成组件添加 await new Promise(resolve => setTimeout(resolve, 100)); // 重新查询节点信息验证组件是否真的添加成功 try { const allComponentsInfo2 = await this.getComponents(nodeUuid); if (allComponentsInfo2.success && allComponentsInfo2.data?.components) { const addedComponent = allComponentsInfo2.data.components.find((comp: any) => comp.type === componentType); if (addedComponent) { resolve({ success: true, message: `Component '${componentType}' added successfully`, data: { nodeUuid: nodeUuid, componentType: componentType, componentVerified: true, existing: false } }); } else { resolve({ success: false, error: `Component '${componentType}' was not found on node after addition. Available components: ${allComponentsInfo2.data.components.map((c: any) => c.type).join(', ')}` }); } } else { resolve({ success: false, error: `Failed to verify component addition: ${allComponentsInfo2.error || 'Unable to get node components'}` }); } } catch (verifyError: any) { resolve({ success: false, error: `Failed to verify component addition: ${verifyError.message}` }); } }).catch((err: Error) => { // 备用方案:使用场景脚本 const options = { name: 'cocos-mcp-server', method: 'addComponentToNode', args: [nodeUuid, componentType] }; 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 removeComponent(nodeUuid: string, componentType: string): Promise<ToolResponse> { return new Promise(async (resolve) => { // 1. 查找节点上的所有组件 const allComponentsInfo = await this.getComponents(nodeUuid); if (!allComponentsInfo.success || !allComponentsInfo.data?.components) { resolve({ success: false, error: `Failed to get components for node '${nodeUuid}': ${allComponentsInfo.error}` }); return; } // 2. 只查找type字段等于componentType的组件(即cid) const exists = allComponentsInfo.data.components.some((comp: any) => comp.type === componentType); if (!exists) { resolve({ success: false, error: `Component cid '${componentType}' not found on node '${nodeUuid}'. 请用getComponents获取type字段(cid)作为componentType。` }); return; } // 3. 官方API直接移除 try { await Editor.Message.request('scene', 'remove-component', { uuid: nodeUuid, component: componentType }); // 4. 再查一次确认是否移除 const afterRemoveInfo = await this.getComponents(nodeUuid); const stillExists = afterRemoveInfo.success && afterRemoveInfo.data?.components?.some((comp: any) => comp.type === componentType); if (stillExists) { resolve({ success: false, error: `Component cid '${componentType}' was not removed from node '${nodeUuid}'.` }); } else { resolve({ success: true, message: `Component cid '${componentType}' removed successfully from node '${nodeUuid}'`, data: { nodeUuid, componentType } }); } } catch (err: any) { resolve({ success: false, error: `Failed to remove component: ${err.message}` }); } }); } private async getComponents(nodeUuid: string): Promise<ToolResponse> { return new Promise((resolve) => { // 优先尝试直接使用 Editor API 查询节点信息 Editor.Message.request('scene', 'query-node', nodeUuid).then((nodeData: any) => { if (nodeData && nodeData.__comps__) { const components = nodeData.__comps__.map((comp: any) => ({ type: comp.__type__ || comp.cid || comp.type || 'Unknown', uuid: comp.uuid?.value || comp.uuid || null, enabled: comp.enabled !== undefined ? comp.enabled : true, properties: this.extractComponentProperties(comp) })); resolve({ success: true, data: { nodeUuid: nodeUuid, components: components } }); } else { resolve({ success: false, error: 'Node not found or no components data' }); } }).catch((err: Error) => { // 备用方案:使用场景脚本 const options = { name: 'cocos-mcp-server', method: 'getNodeInfo', args: [nodeUuid] }; Editor.Message.request('scene', 'execute-scene-script', options).then((result: any) => { if (result.success) { resolve({ success: true, data: result.data.components }); } else { resolve(result); } }).catch((err2: Error) => { resolve({ success: false, error: `Direct API failed: ${err.message}, Scene script failed: ${err2.message}` }); }); }); }); } private async getComponentInfo(nodeUuid: string, componentType: string): Promise<ToolResponse> { return new Promise((resolve) => { // 优先尝试直接使用 Editor API 查询节点信息 Editor.Message.request('scene', 'query-node', nodeUuid).then((nodeData: any) => { if (nodeData && nodeData.__comps__) { const component = nodeData.__comps__.find((comp: any) => { const compType = comp.__type__ || comp.cid || comp.type; return compType === componentType; }); if (component) { resolve({ success: true, data: { nodeUuid: nodeUuid, componentType: componentType, enabled: component.enabled !== undefined ? component.enabled : true, properties: this.extractComponentProperties(component) } }); } else { resolve({ success: false, error: `Component '${componentType}' not found on node` }); } } else { resolve({ success: false, error: 'Node not found or no components data' }); } }).catch((err: Error) => { // 备用方案:使用场景脚本 const options = { name: 'cocos-mcp-server', method: 'getNodeInfo', args: [nodeUuid] }; Editor.Message.request('scene', 'execute-scene-script', options).then((result: any) => { if (result.success && result.data.components) { const component = result.data.components.find((comp: any) => comp.type === componentType); if (component) { resolve({ success: true, data: { nodeUuid: nodeUuid, componentType: componentType, ...component } }); } else { resolve({ success: false, error: `Component '${componentType}' not found on node` }); } } else { resolve({ success: false, error: result.error || 'Failed to get component info' }); } }).catch((err2: Error) => { resolve({ success: false, error: `Direct API failed: ${err.message}, Scene script failed: ${err2.message}` }); }); }); }); } private extractComponentProperties(component: any): Record<string, any> { console.log(`[extractComponentProperties] Processing component:`, Object.keys(component)); // 检查组件是否有 value 属性,这通常包含实际的组件属性 if (component.value && typeof component.value === 'object') { console.log(`[extractComponentProperties] Found component.value with properties:`, Object.keys(component.value)); return component.value; // 直接返回 value 对象,它包含所有组件属性 } // 备用方案:从组件对象中直接提取属性 const properties: Record<string, any> = {}; const excludeKeys = ['__type__', 'enabled', 'node', '_id', '__scriptAsset', 'uuid', 'name', '_name', '_objFlags', '_enabled', 'type', 'readonly', 'visible', 'cid', 'editor', 'extends']; for (const key in component) { if (!excludeKeys.includes(key) && !key.startsWith('_')) { console.log(`[extractComponentProperties] Found direct property '${key}':`, typeof component[key]); properties[key] = component[key]; } } console.log(`[extractComponentProperties] Final extracted properties:`, Object.keys(properties)); return properties; } private async findComponentTypeByUuid(componentUuid: string): Promise<string | null> { console.log(`[findComponentTypeByUuid] Searching for component type with UUID: ${componentUuid}`); if (!componentUuid) { return null; } try { const nodeTree = await Editor.Message.request('scene', 'query-node-tree'); if (!nodeTree) { console.warn('[findComponentTypeByUuid] Failed to query node tree.'); return null; } const queue: any[] = [nodeTree]; while (queue.length > 0) { const currentNodeInfo = queue.shift(); if (!currentNodeInfo || !currentNodeInfo.uuid) { continue; } try { const fullNodeData = await Editor.Message.request('scene', 'query-node', currentNodeInfo.uuid); if (fullNodeData && fullNodeData.__comps__) { for (const comp of fullNodeData.__comps__) { const compAny = comp as any; // Cast to any to access dynamic properties // The component UUID is nested in the 'value' property if (compAny.uuid && compAny.uuid.value === componentUuid) { const componentType = compAny.__type__; console.log(`[findComponentTypeByUuid] Found component type '${componentType}' for UUID ${componentUuid} on node ${fullNodeData.name?.value}`); return componentType; } } } } catch (e) { console.warn(`[findComponentTypeByUuid] Could not query node ${currentNodeInfo.uuid}:`, e); } if (currentNodeInfo.children) { for (const child of currentNodeInfo.children) { queue.push(child); } } } console.warn(`[findComponentTypeByUuid] Component with UUID ${componentUuid} not found in scene tree.`); return null; } catch (error) { console.error(`[findComponentTypeByUuid] Error while searching for component type:`, error); return null; } } private async setComponentProperty(args: any): Promise<ToolResponse> { const { nodeUuid, componentType, property, propertyType, value } = args; return new Promise(async (resolve) => { try { console.log(`[ComponentTools] Setting ${componentType}.${property} (type: ${propertyType}) = ${JSON.stringify(value)} on node ${nodeUuid}`); // Step 0: 检测是否为节点属性,如果是则重定向到对应的节点方法 const nodeRedirectResult = await this.checkAndRedirectNodeProperties(args); if (nodeRedirectResult) { resolve(nodeRedirectResult); return; } // Step 1: 获取组件信息,使用与getComponents相同的方法 const componentsResponse = await this.getComponents(nodeUuid); if (!componentsResponse.success || !componentsResponse.data) { resolve({ success: false, error: `Failed to get components for node '${nodeUuid}': ${componentsResponse.error}`, instruction: `Please verify that node UUID '${nodeUuid}' is correct. Use get_all_nodes or find_node_by_name to get the correct node UUID.` }); return; } const allComponents = componentsResponse.data.components; // Step 2: 查找目标组件 let targetComponent = null; const availableTypes: string[] = []; for (let i = 0; i < allComponents.length; i++) { const comp = allComponents[i]; availableTypes.push(comp.type); if (comp.type === componentType) { targetComponent = comp; break; } } if (!targetComponent) { // 提供更详细的错误信息和建议 const instruction = this.generateComponentSuggestion(componentType, availableTypes, property); resolve({ success: false, error: `Component '${componentType}' not found on node. Available components: ${availableTypes.join(', ')}`, instruction: instruction }); return; } // Step 3: 自动检测和转换属性值 let propertyInfo; try { console.log(`[ComponentTools] Analyzing property: ${property}`); propertyInfo = this.analyzeProperty(targetComponent, property); } catch (analyzeError: any) { console.error(`[ComponentTools] Error in analyzeProperty:`, analyzeError); resolve({ success: false, error: `Failed to analyze property '${property}': ${analyzeError.message}` }); return; } if (!propertyInfo.exists) { resolve({ success: false, error: `Property '${property}' not found on component '${componentType}'. Available properties: ${propertyInfo.availableProperties.join(', ')}` }); return; } // Step 4: 处理属性值和设置 const originalValue = propertyInfo.originalValue; let processedValue: any; // 根据明确的propertyType处理属性值 switch (propertyType) { case 'string': processedValue = String(value); break; case 'number': case 'integer': case 'float': processedValue = Number(value); break; case 'boolean': processedValue = Boolean(value); break; case 'color': if (typeof value === 'string') { // 字符串格式:支持十六进制、颜色名称、rgb()/rgba() processedValue = this.parseColorString(value); } else if (typeof value === 'object' && value !== null) { // 对象格式:验证并转换RGBA值 processedValue = { r: Math.min(255, Math.max(0, Number(value.r) || 0)), g: Math.min(255, Math.max(0, Number(value.g) || 0)), b: Math.min(255, Math.max(0, Number(value.b) || 0)), a: value.a !== undefined ? Math.min(255, Math.max(0, Number(value.a))) : 255 }; } else { throw new Error('Color value must be an object with r, g, b properties or a hexadecimal string (e.g., "#FF0000")'); } break; case 'vec2': if (typeof value === 'object' && value !== null) { processedValue = { x: Number(value.x) || 0, y: Number(value.y) || 0 }; } else { throw new Error('Vec2 value must be an object with x, y properties'); } break; case 'vec3': if (typeof value === 'object' && value !== null) { processedValue = { x: Number(value.x) || 0, y: Number(value.y) || 0, z: Number(value.z) || 0 }; } else { throw new Error('Vec3 value must be an object with x, y, z properties'); } break; case 'size': if (typeof value === 'object' && value !== null) { processedValue = { width: Number(value.width) || 0, height: Number(value.height) || 0 }; } else { throw new Error('Size value must be an object with width, height properties'); } break; case 'node': if (typeof value === 'string') { processedValue = { uuid: value }; } else { throw new Error('Node reference value must be a string UUID'); } break; case 'component': if (typeof value === 'string') { // 组件引用需要特殊处理:通过节点UUID找到组件的__id__ processedValue = value; // 先保存节点UUID,后续会转换为__id__ } else { throw new Error('Component reference value must be a string (node UUID containing the target component)'); } break; case 'spriteFrame': case 'prefab': case 'asset': if (typeof value === 'string') { processedValue = { uuid: value }; } else { throw new Error(`${propertyType} value must be a string UUID`); } break; case 'nodeArray': if (Array.isArray(value)) { processedValue = value.map((item: any) => { if (typeof item === 'string') { return { uuid: item }; } else { throw new Error('NodeArray items must be string UUIDs'); } }); } else { throw new Error('NodeArray value must be an array'); } break; case 'colorArray': if (Array.isArray(value)) { processedValue = value.map((item: any) => { if (typeof item === 'object' && item !== null && 'r' in item) { return { r: Math.min(255, Math.max(0, Number(item.r) || 0)), g: Math.min(255, Math.max(0, Number(item.g) || 0)), b: Math.min(255, Math.max(0, Number(item.b) || 0)), a: item.a !== undefined ? Math.min(255, Math.max(0, Number(item.a))) : 255 }; } else { return { r: 255, g: 255, b: 255, a: 255 }; } }); } else { throw new Error('ColorArray value must be an array'); } break; case 'numberArray': if (Array.isArray(value)) { processedValue = value.map((item: any) => Number(item)); } else { throw new Error('NumberArray value must be an array'); } break; case 'stringArray': if (Array.isArray(value)) { processedValue = value.map((item: any) => String(item)); } else { throw new Error('StringArray value must be an array'); } break; default: throw new Error(`Unsupported property type: ${propertyType}`); } console.log(`[ComponentTools] Converting value: ${JSON.stringify(value)} -> ${JSON.stringify(processedValue)} (type: ${propertyType})`); console.log(`[ComponentTools] Property analysis result: propertyInfo.type="${propertyInfo.type}", propertyType="${propertyType}"`); console.log(`[ComponentTools] Will use color special handling: ${propertyType === 'color' && processedValue && typeof processedValue === 'object'}`); // 用于验证的实际期望值(对于组件引用需要特殊处理) let actualExpectedValue = processedValue; // Step 5: 获取原始节点数据来构建正确的路径 const rawNodeData = await Editor.Message.request('scene', 'query-node', nodeUuid); if (!rawNodeData || !rawNodeData.__comps__) { resolve({ success: false, error: `Failed to get raw node data for property setting` }); return; } // 找到原始组件的索引 let rawComponentIndex = -1; for (let i = 0; i < rawNodeData.__comps__.length; i++) { const comp = rawNodeData.__comps__[i] as any; const compType = comp.__type__ || comp.cid || comp.type || 'Unknown'; if (compType === componentType) { rawComponentIndex = i; break; } } if (rawComponentIndex === -1) { resolve({ success: false, error: `Could not find component index for setting property` }); return; } // 构建正确的属性路径 let propertyPath = `__comps__.${rawComponentIndex}.${property}`; // 特殊处理资源类属性 if (propertyType === 'asset' || propertyType === 'spriteFrame' || propertyType === 'prefab' || (propertyInfo.type === 'asset' && propertyType === 'string')) { console.log(`[ComponentTools] Setting asset reference:`, { value: processedValue, property: property, propertyType: propertyType, path: propertyPath }); // Determine asset type based on property name let assetType = 'cc.SpriteFrame'; // default if (property.toLowerCase().includes('texture')) { assetType = 'cc.Texture2D'; } else if (property.toLowerCase().includes('material')) { assetType = 'cc.Material'; } else if (property.toLowerCase().includes('font')) { assetType = 'cc.Font'; } else if (property.toLowerCase().includes('clip')) { assetType = 'cc.AudioClip'; } else if (propertyType === 'prefab') { assetType = 'cc.Prefab'; } await Editor.Message.request('scene', 'set-property', { uuid: nodeUuid, path: propertyPath, dump: { value: processedValue, type: assetType } }); } else if (componentType === 'cc.UITransform' && (property === '_contentSize' || property === 'contentSize')) { // Special handling for UITransform contentSize - set width and height separately const width = Number(value.width) || 100; const height = Number(value.height) || 100; // Set width first await Editor.Message.request('scene', 'set-property', { uuid: nodeUuid, path: `__comps__.${rawComponentIndex}.width`, dump: { value: width } }); // Then set height await Editor.Message.request('scene', 'set-property', { uuid: nodeUuid, path: `__comps__.${rawComponentIndex}.height`, dump: { value: height } }); } else if (componentType === 'cc.UITransform' && (property === '_anchorPoint' || property === 'anchorPoint')) { // Special handling for UITransform anchorPoint - set anchorX and anchorY separately const anchorX = Number(value.x) || 0.5; const anchorY = Number(value.y) || 0.5; // Set anchorX first await Editor.Message.request('scene', 'set-property', { uuid: nodeUuid, path: `__comps__.${rawComponentIndex}.anchorX`, dump: { value: anchorX } }); // Then set anchorY await Editor.Message.request('scene', 'set-property', { uuid: nodeUuid, path: `__comps__.${rawComponentIndex}.anchorY`, dump: { value: anchorY } }); } else if (propertyType === 'color' && processedValue && typeof processedValue === 'object') { // 特殊处理颜色属性,确保RGBA值正确 // Cocos Creator颜色值范围是0-255 const colorValue = { r: Math.min(255, Math.max(0, Number(processedValue.r) || 0)), g: Math.min(255, Math.max(0, Number(processedValue.g) || 0)), b: Math.min(255, Math.max(0, Number(processedValue.b) || 0)), a: processedValue.a !== undefined ? Math.min(255, Math.max(0, Number(processedValue.a))) : 255 }; console.log(`[ComponentTools] Setting color value:`, colorValue); await Editor.Message.request('scene', 'set-property', { uuid: nodeUuid, path: propertyPath, dump: { value: colorValue, type: 'cc.Color' } }); } else if (propertyType === 'vec3' && processedValue && typeof processedValue === 'object') { // 特殊处理Vec3属性 const vec3Value = { x: Number(processedValue.x) || 0, y: Number(processedValue.y) || 0, z: Number(processedValue.z) || 0 }; await Editor.Message.request('scene', 'set-property', { uuid: nodeUuid, path: propertyPath, dump: { value: vec3Value, type: 'cc.Vec3' } }); } else if (propertyType === 'vec2' && processedValue && typeof processedValue === 'object') { // 特殊处理Vec2属性 const vec2Value = { x: Number(processedValue.x) || 0, y: Number(processedValue.y) || 0 }; await Editor.Message.request('scene', 'set-property', { uuid: nodeUuid, path: propertyPath, dump: { value: vec2Value, type: 'cc.Vec2' } }); } else if (propertyType === 'size' && processedValue && typeof processedValue === 'object') { // 特殊处理Size属性 const sizeValue = { width: Number(processedValue.width) || 0, height: Number(processedValue.height) || 0 }; await Editor.Message.request('scene', 'set-property', { uuid: nodeUuid, path: propertyPath, dump: { value: sizeValue, type: 'cc.Size' } }); } else if (propertyType === 'node' && processedValue && typeof processedValue === 'object' && 'uuid' in processedValue) { // 特殊处理节点引用 console.log(`[ComponentTools] Setting node reference with UUID: ${processedValue.uuid}`); await Editor.Message.request('scene', 'set-property', { uuid: nodeUuid, path: propertyPath, dump: { value: processedValue, type: 'cc.Node' } }); } else if (propertyType === 'component' && typeof processedValue === 'string') { // 特殊处理组件引用:通过节点UUID找到组件的__id__ const targetNodeUuid = processedValue; console.log(`[ComponentTools] Setting component reference - finding component on node: ${targetNodeUuid}`); // 从当前组件的属性元数据中获取期望的组件类型 let expectedComponentType = ''; // 获取当前组件的详细信息,包括属性元数据 const currentComponentInfo = await this.getComponentInfo(nodeUuid, componentType); if (currentComponentInfo.success && currentComponentInfo.data?.properties?.[property]) { const propertyMeta = currentComponentInfo.data.properties[property]; // 从属性元数据中提取组件类型信息 if (propertyMeta && typeof propertyMeta === 'object') { // 检查是否有type字段指示组件类型 if (propertyMeta.type) { expectedComponentType = propertyMeta.type; } else if (propertyMeta.ctor) { // 有些属性可能使用ctor字段 expectedComponentType = propertyMeta.ctor; } else if (propertyMeta.extends && Array.isArray(propertyMeta.extends)) { // 检查extends数组,通常第一个是最具体的类型 for (const extendType of propertyMeta.extends) { if (extendType.startsWith('cc.') && extendType !== 'cc.Component' && extendType !== 'cc.Object') { expectedComponentType = extendType; break; } } } } } if (!expectedComponentType) { throw new Error(`Unable to determine required component type for property '${property}' on component '${componentType}'. Property metadata may not contain type information.`); } console.log(`[ComponentTools] Detected required component type: ${expectedComponentType} for property: ${property}`); try { // 获取目标节点的组件信息 const targetNodeData = await Editor.Message.request('scene', 'query-node', targetNodeUuid); if (!targetNodeData || !targetNodeData.__comps__) { throw new Error(`Target node ${targetNodeUuid} not found or has no components`); } // 打印目标节点的组件概览 console.log(`[ComponentTools] Target node ${targetNodeUuid} has ${targetNodeData.__comps__.length} components:`); targetNodeData.__comps__.forEach((comp: any, index: number) => { const sceneId = comp.value && comp.value.uuid && comp.value.uuid.value ? comp.value.uuid.value : 'unknown'; console.log(`[ComponentTools] Component ${index}: ${comp.type} (scene_id: ${sceneId})`); }); // 查找对应的组件 let targetComponent = null; let componentId: string | null = null; // 在目标节点的_components数组中查找指定类型的组件 // 注意:__comps__和_components的索引是对应的 console.log(`[ComponentTools] Searching for component type: ${expectedComponentType}`); for (let i = 0; i < targetNodeData.__comps__.length; i++) { const comp = targetNodeData.__comps__[i] as any; console.log(`[ComponentTools] Checking component ${i}: type=${comp.type}, target=${expectedComponentType}`); if (comp.type === expectedComponentType) { targetComponent = comp; console.log(`[ComponentTools] Found matching component at index ${i}: ${comp.type}`); // 从组件的value.uuid.value中获取组件在场景中的ID if (comp.value && comp.value.uuid && comp.value.uuid.value) { componentId = comp.value.uuid.value; console.log(`[ComponentTools] Got componentId from comp.value.uuid.value: ${componentId}`); } else { console.log(`[ComponentTools] Component structure:`, { hasValue: !!comp.value, hasUuid: !!(comp.value && comp.value.uuid), hasUuidValue: !!(comp.value && comp.value.uuid && comp.value.uuid.value), uuidStructure: comp.value ? comp.value.uuid : 'No value' }); throw new Error(`Unable to extract component ID from component structure`); } break; } } if (!targetComponent) { // 如果没找到,列出可用组件让用户了解,显示场景中的真实ID const availableComponents = targetNodeData.__comps__.map((comp: any, index: number) => { let sceneId = 'unknown'; // 从组件的value.uuid.value获取场景ID if (comp.value && comp.value.uuid && comp.value.uuid.value) { sceneId = comp.value.uuid.value; } return `${comp.type}(scene_id:${sceneId})`; }); throw new Error(`Component type '${expectedComponentType}' not found on node ${targetNodeUuid}. Available components: ${availableComponents.join(', ')}`); } console.log(`[ComponentTools] Found component ${expectedComponentType} with scene ID: ${componentId} on node ${targetNodeUuid}`); // 更新期望值为实际的组件ID对象格式,用于后续验证 if (componentId) { actualExpectedValue = { uuid: componentId }; } // 尝试使用与节点/资源引用相同的格式:{uuid: componentId} // 测试看是否能正确设置组件引用 await Editor.Message.request('scene', 'set-property', { uuid: nodeUuid, path: propertyPath, dump: { value: { uuid: componentId }, // 使用对象格式,像节点/资源引用一样 type: expectedComponentType } }); } catch (error) { console.error(`[ComponentTools] Error setting component reference:`, error); throw error; } } else if (propertyType === 'nodeArray' && Array.isArray(processedValue)) { // 特殊处理节点数组 - 保持预处理的格式 console.log(`[ComponentTools] Setting node array:`, processedValue); await Editor.Message.request('scene', 'set-property', { uuid: nodeUuid, path: propertyPath, dump: { value: processedValue // 保持 [{uuid: "..."}, {uuid: "..."}] 格式 } }); } else if (propertyType === 'colorArray' && Array.isArray(processedValue)) { // 特殊处理颜色数组 const colorArrayValue = processedValue.map((item: any) => { if (item && typeof item === 'object' && 'r' in item) { return { r: Math.min(255, Math.max(0, Number(item.r) || 0)), g: Math.min(255, Math.max(0, Number(item.g) || 0)), b: Math.min(255, Math.max(0, Number(item.b) || 0)), a: item.a !== undefined ? Math.min(255, Math.max(0, Number(item.a))) : 255 }; } else { return { r: 255, g: 255, b: 255, a: 255 }; } }); await Editor.Message.request('scene', 'set-property', { uuid: nodeUuid, path: propertyPath, dump: { value: colorArrayValue, type: 'cc.Color' } }); } else { // Normal property setting for non-asset properties await Editor.Message.request('scene', 'set-property', { uuid: nodeUuid, path: propertyPath, dump: { value: processedValue } }); } // Step 5: 等待Editor完成更新,然后验证设置结果 await new Promise(resolve => setTimeout(resolve, 200)); // 等待200ms让Editor完成更新 const verification = await this.verifyPropertyChange(nodeUuid, componentType, property, originalValue, actualExpectedValue); resolve({ success: true, message: `Successfully set ${componentType}.${property}`, data: { nodeUuid, componentType, property, actualValue: verification.actualValue, changeVerified: verification.verified } }); } catch (error: any) { console.error(`[ComponentTools] Error setting property:`, error); resolve({ success: false, error: `Failed to set property: ${error.message}` }); } }); } private async attachScript(nodeUuid: string, scriptPath: string): Promise<ToolResponse> { return new Promise(async (resolve) => { // 从脚本路径提取组件类名 const scriptName = scriptPath.split('/').pop()?.replace('.ts', '').replace('.js', ''); if (!scriptName) { resolve({ success: false, error: 'Invalid script path' }); return; } // 先查找节点上是否已存在该脚本组件 const allComponentsInfo = await this.getComponents(nodeUuid); if (allComponentsInfo.success && allComponentsInfo.data?.components) { const existingScript = allComponentsInfo.data.components.find((comp: any) => comp.type === scriptName); if (existingScript) { resolve({ success: true, message: `Script '${scriptName}' already exists on node`, data: { nodeUuid: nodeUuid, componentName: scriptName, existing: true } }); return; } } // 首先尝试直接使用脚本名称作为组件类型 Editor.Message.request('scene', 'create-component', { uuid: nodeUuid, component: scriptName // 使用脚本名称而非UUID }).then(async (result: any) => { // 等待一段时间让Editor完成组件添加 await new Promise(resolve => setTimeout(resolve, 100)); // 重新查询节点信息验证脚本是否真的添加成功 const allComponentsInfo2 = await this.getComponents(nodeUuid); if (allComponentsInfo2.success && allComponentsInfo2.data?.components) { const addedScript = allComponentsInfo2.data.components.find((comp: any) => comp.type === scriptName); if (addedScript) { resolve({ success: true, message: `Script '${scriptName}' attached successfully`, data: { nodeUuid: nodeUuid, componentName: scriptName, existing: false } }); } else { resolve({ success: false, error: `Script '${scriptName}' was not found on node after addition. Available components: ${allComponentsInfo2.data.components.map((c: any) => c.type).join(', ')}` }); } } else { resolve({ success: false, error: `Failed to verify script addition: ${allComponentsInfo2.error || 'Unable to get node components'}` }); } }).catch((err: Error) => { // 备用方案:使用场景脚本 const options = { name: 'cocos-mcp-server', method: 'attachScript', args: [nodeUuid, scriptPath] }; Editor.Message.request('scene', 'execute-scene-script', options).then((result: any) => { resolve(result); }).catch(() => { resolve({ success: false, error: `Failed to attach script '${scriptName}': ${err.message}`, instruction: 'Please ensure the script is properly compiled and exported as a Component class. You can also manually attach the script through the Properties panel in the editor.' }); }); }); }); } private async getAvailableComponents(category: string = 'all'): Promise<ToolResponse> { const componentCategories: Record<string, string[]> = { renderer: ['cc.Sprite', 'cc.Label', 'cc.RichText', 'cc.Mask', 'cc.Graphics'], ui: ['cc.Button', 'cc.Toggle', 'cc.Slider', 'cc.ScrollView', 'cc.EditBox', 'cc.ProgressBar'], physics: ['cc.RigidBody2D', 'cc.BoxCollider2D', 'cc.CircleCollider2D', 'cc.PolygonCollider2D'], animation: ['cc.Animation', 'cc.AnimationClip', 'cc.SkeletalAnimation'], audio: ['cc.AudioSource'], layout: ['cc.Layout', 'cc.Widget', 'cc.PageView', 'cc.PageViewIndicator'], effects: ['cc.MotionStreak', 'cc.ParticleSystem2D'], camera: ['cc.Camera'], light: ['cc.Light', 'cc.DirectionalLight', 'cc.PointLight', 'cc.SpotLight'] }; let components: string[] = []; if (category === 'all') { for (const cat in componentCategories) { components = components.concat(componentCategories[cat]); } } else if (componentCategories[category]) { components = componentCategories[category]; } return { success: true, data: { category: category, components: components } }; } private isValidPropertyDescriptor(propData: any): boolean { // 检查是否是有效的属性描述对象 if (typeof propData !== 'object' || propData === null) { return false; } try { const keys = Object.keys(propData); // 避免遍历简单的数值对象(如 {width: 200, height: 150}) const isSimpleValueObject = keys.every(key => { const value = propData[key]; return typeof value === 'number' || typeof value === 'string' || typeof value === 'boolean'; }); if (isSimpleValueObject) { return false; } // 检查是否包含属性描述符的特征字段,不使用'in'操作符 const hasName = keys.includes('name'); const hasValue = keys.includes('value'); const hasType = keys.includes('type'); const hasDisplayName = keys.includes('displayName'); const hasReadonly = keys.includes('readonly'); // 必须包含name或value字段,且通常还有type字段 const hasValidStructure = (hasName || hasValue) && (hasType || hasDisplayName || hasReadonly); // 额外检查:如果有default字段且结构复杂,避免深度遍历 if (keys.includes('default') && propData.default && typeof propData.default === 'object') { const defaultKeys = Object.keys(propData.default); if (defaultKeys.includes('value') && typeof propData.default.value === 'object') { // 这种情况下,我们只返回顶层属性,不深入遍历default.value return hasValidStructure; } } return hasValidStructure; } catch (error) { console.warn(`[isValidPropertyDescriptor] Error checking property descriptor:`, error); return false; } } private analyzeProperty(component: any, propertyName: string): { exists: boolean; type: string; availableProperties: string[]; originalValue: any } { // 从复杂的组件结构中提取可用属性 const availableProperties: string[] = []; let propertyValue: any = undefined; let propertyExists = false; // 尝试多种方式查找属性: // 1. 直接属性访问 if (Object.prototype.hasOwnProperty.call(component, propertyName)) { propertyValue = component[propertyName]; propertyExists = true; } // 2. 从嵌套结构中查找 (如从测试数据看到的复杂结构) if (!propertyExists && component.properties && typeof component.properties === 'object') { // 首先检查properties.value是否存在(这是我们在getComponents中看到的结构) if (component.properties.value && typeof component.properties.value === 'object') { const valueObj = component.properties.value; for (const [key, propData] of Object.entries(valueObj)) { // 检查propData是否是一个有效的属性描述对象 // 确保propData是对象且包含预期的属性结构 if (this.isValidPropertyDescriptor(propData)) { const propInfo = propData as any; availableProperties.push(key); if (key === propertyName) { // 优先使用value属性,如果没有则使用propData本身 try { const propKeys = Object.keys(propInfo); propertyValue = propKeys.includes('value') ? propInfo.value : propInfo; } catch (error) { // 如果检查失败,直接使用propInfo propertyValue = propInfo; } propertyExists = true; } } } } else { // 备用方案:直接从properties查找 for (const [key, propData] of Object.entries(component.properties)) { if (this.isValidPropertyDescriptor(propData)) { const propInfo = propData as any; availableProperties.push(key); if (key === propertyName) { // 优先使用value属性,如果没有则使用propData本身 try { const propKeys = Object.keys(propInfo); propertyValue = propKeys.includes('value') ? propInfo.value : propInfo; } catch (error) { // 如果检查失败,直接使用propInfo propertyValue = propInfo; } propertyExists = true; } } } } } // 3. 从直接属性中提取简单属性名 if (availableProperties.length === 0) { for (const key of Object.keys(component)) { if (!key.startsWith('_') && !['__type__', 'cid', 'node', 'uuid', 'name', 'enabled', 'type', 'readonly', 'visible'].includes(key)) { availableProperties.push(key); } } } if (!propertyExists) { return { exists: false, type: 'unknown', availableProperties, originalValue: undefined }; } let type = 'unknown'; // 智能类型检测 if (Array.isArray(propertyValue)) { // 数组类型检测 if (propertyName.toLowerCase().includes('node')) { type = 'nodeArray'; } else if (propertyName.toLowerCase().includes('color')) { type = 'colorArray'; } else { type = 'array'; } } else if (typeof propertyValue === 'string') { // Check if property name suggests it's an asset if (['spriteFrame', 'texture', 'material', 'font', 'clip', 'prefab'].includes(propertyName.toLowerCase())) { type = 'asset'; } else { type = 'string'; } } else if (typeof propertyValue === 'number') { type = 'number'; } else if (typeof propertyValue === 'boolean') { type = 'boolean'; } else if (propertyValue && typeof propertyValue === 'object') { try { const keys = Object.keys(propertyValue); if (keys.includes('r') && keys.includes('g') && keys.includes('b')) { type = 'color'; } else if (keys.includes('x') && keys.includes('y')) { type = propertyValue.z !== undefined ? 'vec3' : 'vec2'; } else if (keys.includes('width') && keys.includes('height')) { type = 'size'; } else if (keys.includes('uuid') || keys.includes('__uuid__')) { // 检查是否是节点引用(通过属性名或__id__属性判断) if (propertyName.toLowerCase().includes('node') || propertyName.toLowerCase().includes('target') || keys.includes('__id__')) { type = 'node'; } else { type = 'asset'; } } else if (keys.includes('__id__')) { // 节点引用特征 type = 'node'; } else { type = 'object'; } } catch (error) { console.warn(`[analyzeProperty] Error checking property type for: ${JSON.stringify(propertyValue)}`); type = 'object'; } } else if (propertyValue === null || propertyValue === undefined) { // For null/undefined values, check property name to determine type if (['spriteFrame', 'texture', 'material', 'font', 'clip', 'prefab'].includes(propertyName.toLowerCase())) { type = 'asset'; } else if (propertyName.toLowerCase().includes('node') || propertyName.toLowerCase().includes('target')) { type = 'node'; } else if (propertyName.toLowerCase().includes('component')) { type = 'component'; } else { type = 'unknown'; } } return { exists: true, type, availableProperties, originalValue: propertyValue }; } private smartConvertValue(inputValue: any, propertyInfo: any): any { const { type, originalValue } = propertyInfo; console.log(`[smartConvertValue] Converting ${JSON.stringify(inputValue)} to type: ${type}`); switch (type) { case 'string': return String(inputValue); case 'number': return Number(inputValue); case 'boolean': if (typeof inputValue === 'boolean') return inputValue; if (typeof inputValue === 'string') { return inputValue.toLowerCase() === 'true' || inputValue === '1'; } return Boolean(inputValue); case 'color': // 优化的颜色处理,支持多种输入格式 if (typeof inputValue === 'string') { // 字符串格式:十六进制、颜色名称、rgb()/rgba() return this.parseColorString(inputValue); } else if (typeof inputValue === 'object' && inputValue !== null) { try { const inputKeys = Object.keys(inputValue); // 如果输入是颜色对象,验证并转换 if (inputKeys.includes('r') || inputKeys.includes('g') || inputKeys.includes('b')) { return { r: Math.min(255, Math.max(0, Number(inputValue.r) || 0)), g: Math.min(255, Math.max(0, Number(inputValue.g) || 0)), b: Math.min(255, Math.max(0, Number(inputValue.b) || 0)), a: inputValue.a !== undefined ? Math.min(255, Math.max(0, Number(inputValue.a))) : 255 }; } } catch (error) { console.warn(`[smartConvertValue] Invalid color object: ${JSON.stringify(inputValue)}`); } } // 如果有原值,保持原值结构并更新提供的值 if (originalValue && typeof originalValue === 'object') { try { const inputKeys = typeof inputValue === 'object' && inputValue ? Object.keys(inputValue) : []; return { r: inputKeys.includes('r') ? Math.min(255, Math.max(0, Number(inputValue.r))) : (originalValue.r || 255), g: inputKeys.includes('g') ? Math.min(255, Math.max(0, Number(inputValue.g))) : (originalValue.g || 255), b: inputKeys.includes('b') ? Math.min(255, Math.max(0, Number(inputValue.b))) : (originalValue.b || 255), a: inputKeys.includes('a') ? Math.min(255, Math.max(0, Number(inputValue.a))) : (originalValue.a || 255) }; } catch (error) { console.warn(`[smartConvertValue] Error processing color with original value: ${error}`); } } // 默认返回白色 console.warn(`[smartConvertValue] Using default white color for invalid input: ${JSON.stringify(inputValue)}`); return { r: 255, g: 255, b: 255, a: 255 }; case 'vec2': if (typeof inputValue === 'object' && inputValue !== null) { return { x: Number(inputValue.x) || originalValue.x || 0, y: Number(inputValue.y) || originalValue.y || 0 }; } return originalValue; case 'vec3': if (typeof inputValue === 'object' && inputValue !== null) { return { x: Number(inputValue.x) || originalValue.x || 0, y: Number(inputValue.y) || originalValue.y || 0, z: Number(inputValue.z) || originalValue.z || 0 }; } return originalValue; case 'size': if (typeof inputValue === 'object' && inputValue !== null) { return { width: Number(inputValue.width) || originalValue.width || 100, height: Number(inputValue.height) || originalValue.height || 100 }; } return originalValue; case 'node': if (typeof inputValue === 'string') { // 节点引用需要特殊处理 return inputValue; } else if (typeof inputValue === 'object' && inputValue !== null) { // 如果已经是对象形式,返回UUID或完整对象 return inputValue.uuid || inputValue; } return originalValue; case 'asset': if (typeof inputValue === 'string') { // 如果输入是字符串路径,转换为asset对象 return { uuid: inputValue }; } else if (typeof inputValue === 'object' && inputValue !== null) { return inputValue; } return originalValue; default: // 对于未知类型,尽量保持原有结构 if (typeof inputValue === typeof originalValue) { return inputValue; } return originalValue; } } private parseColorString(colorStr: string): { r: number; g: number; b: number; a: number } { const str = colorStr.trim(); // 只支持十六进制格式 #RRGGBB 或 #RRGGBBAA if (str.startsWith('#')) { if (str.length === 7) { // #RRGGBB const r = parseInt(str.substring(1, 3), 16); const g = parseInt(str.substring(3, 5), 16); const b = parseInt(str.substring(5, 7), 16); return { r, g, b, a: 255 }; } else if (str.length === 9) { // #RRGGBBAA const r = parseInt(str.substring(1, 3), 16); const g = parseInt(str.substring(3, 5), 16); const b = parseInt(str.substring(5, 7), 16); const a = parseInt(str.substring(7, 9), 16); return { r, g, b, a }; } } // 如果不是有效的十六进制格式,返回错误提示 throw new Error(`Invalid color format: "${colorStr}". Only hexadecimal format is supported (e.g., "#FF0000" or "#FF0000FF")`); } private async verifyPropertyChange(nodeUuid: string, componentType: string, property: string, originalValue: any, expectedValue: any): Promise<{ verified: boolean; actualValue: any; fullData: any }> { console.log(`[verifyPropertyChange] Starting verification for ${componentType}.${property}`); console.log(`[verifyPropertyChange] Expected value:`, JSON.stringify(expectedValue)); console.log(`[verifyPropertyChange] Original value:`, JSON.stringify(originalValue)); try { // 重新获取组件信息进行验证 console.log(`[verifyPropertyChange] Calling getComponentInfo...`); const componentInfo = await this.getComponentInfo(nodeUuid, componentType); console.log(`[verifyPropertyChange] getComponentInfo success:`, componentInfo.success); const allComponents = await this.getComponents(nodeUuid); console.log(`[verifyPropertyChange] getComponents success:`, allComponents.success); if (componentInfo.success && componentInfo.data) { console.log(`[verifyPropertyChange] Component data available, extracting property '${property}'`); const allPropertyNames = Object.keys(componentInfo.data.properties || {}); console.log(`[verifyPropertyChange] Available properties:`, allPropertyNames); const propertyData = componentInfo.data.properties?.[property]; console.log(`[verifyPropertyChange] Raw property data for '${property}':`, JSON.stringify(propertyData)); // 从属性数据中提取实际值 let actualValue = propertyData; console.log(`[verifyPropertyChange] Initial actualValue:`, JSON.stringify(actualValue)); if (propertyData && typeof propertyData === 'object' && 'value' in propertyData) { actualValue = propertyData.value; console.log(`[verifyPropertyChange] Extracted actualValue from .value:`, JSON.stringify(actualValue)); } else { console.log(`[verifyPropertyChange] No .value property found, using raw data`); } // 修复验证逻辑:检查实际值是否匹配期望值 let verified = false; if (typeof expectedValue === 'object' && expectedValue !== null && 'uuid' in expectedValue) { // 对于引用类型(节点/组件/资源),比较UUID const actualUuid = actualValue && typeof actualValue === 'object' && 'uuid' in actualValue ? actualValue.uuid : ''; const expectedUuid = expectedValue.uuid || ''; verified = actualUuid === expectedUuid && expectedUuid !== ''; console.log(`[verifyPropertyChange] Reference comparison:`); console.log(` - Expected UUID: "${expectedUuid}"`); console.log(` - Actual UUID: "${actualUuid}"`); console.log(` - UUID match: ${actualUuid === expectedUuid}`); console.log(` - UUID not empty: ${expectedUuid !== ''}`); console.log(` - Final verified: ${verified}`); } else { // 对于其他类型,直接比较值 console.log(`[verifyPropertyChange] Value comparison:`); console.log(` - Expected type: ${typeof expectedValue}`); console.log(` - Actual type: ${typeof actualValue}`); if (typeof actualValue === typeof expectedValue) { if (typeof actualValue === 'object' && actualValue !== null && expectedValue !== null) { // 对象类型的深度比较 verified = JSON.stringify(actualValue) === JSON.stringify(expectedValue); console.log(` - Object comparison (JSON): ${verified}`); } else { // 基本类型的直接比较 verified = actualValue === expectedValue; console.log(` - Direct comparison: ${verified}`); } } else { // 类型不匹配时的特殊处理(如数字和字符串) const stringMatch = String(actualValue) === String(expectedValue); const numberMatch = Number(actualValue) === Number(expectedValue); verified = stringMatch || numberMatch; console.log(` - String match: ${stringMatch}`); console.log(` - Number match: ${numberMatch}`); console.log(` - Type mismatch verified: ${verified}`); } } console.log(`[verifyPropertyChange] Final verification result: ${verified}`); console.log(`[verifyPropertyChange] Final actualValue:`, JSON.stringify(actualValue)); const result = { verified, actualValue, fullData: { // 只返回修改的属性信息,不返回完整组件数据 modifiedProperty: { name: property, before: originalValue, expected: expectedValue, actual: actualValue, verified, propertyMetadata: propertyData // 只包含这个属性的元数据 }, // 简化的组件信息 componentSummary: { nodeUuid, componentType, totalProperties: Object.keys(componentInfo.data?.properties || {}).length } } }; console.log(`[verifyPropertyChange] Returning result:`, JSON.stringify(result, null, 2)); return result; } else { console.log(`[verifyPropertyChange] ComponentInfo failed or no data:`, componentInfo); } } catch (error) { console.error('[verifyPropertyChange] Verification failed with error:', error); console.error('[verifyPropertyChange] Error stack:', error instanceof Error ? error.stack : 'No stack trace'); } console.log(`[verifyPropertyChange] Returning fallback result`); return { verified: false, actualValue: undefined, fullData: null }; } /** * 检测是否为节点属性,如果是则重定向到对应的节点方法 */ private async checkAndRedirectNodeProperties(args: any): Promise<ToolResponse | null> { const { nodeUuid, componentType, property, propertyType, value } = args; // 检测是否为节点基础属性(应该使用 set_node_property) const nodeBasicProperties = [ 'name', 'active', 'layer', 'mobility', 'parent', 'children', 'hideFlags' ]; // 检测是否为节点变换属性(应该使用 set_node_transform) const nodeTransformProperties = [ 'position', 'rotation', 'scale', 'eulerAngles', 'angle' ]; // Detect attempts to set cc.Node properties (common mistake) if (componentType === 'cc.Node' || componentType === 'Node') { if (nodeBasicProperties.includes(property)) { return { success: false, error: `Property '${property}' is a node basic property, not a component property`, instruction: `Please use set_node_property method to set node properties: set_node_property(uuid="${nodeUuid}", property="${property}", value=${JSON.stringify(value)})` }; } else if (nodeTransformProperties.includes(property)) { return { success: false, error: `Property '${property}' is a node transform property, not a component property`, instruction: `Please use set_node_transform method to set transform properties: set_node_transform(uuid="${nodeUuid}", ${property}=${JSON.stringify(value)})` }; } } // Detect common incorrect usage if (nodeBasicProperties.includes(property) || nodeTransformProperties.includes(property)) { const methodName = nodeTransformProperties.includes(property) ? 'set_node_transform' : 'set_node_property'; return { success: false, error: `Property '${property}' is a node property, not a component property`, instruction: `Property '${property}' should be set using ${methodName} method, not set_component_property. Please use: ${methodName}(uuid="${nodeUuid}", ${nodeTransformProperties.includes(property) ? property : `property="${property}"`}=${JSON.stringify(value)})` }; } return null; // 不是节点属性,继续正常处理 } /** * 生成组件建议信息 */ private generateComponentSuggestion(requestedType: string, availableTypes: string[], property: string): string { // 检查是否存在相似的组件类型 const similarTypes = availableTypes.filter(type => type.toLowerCase().includes(requestedType.toLowerCase()) || requestedType.toLowerCase().includes(type.toLowerCase()) ); let instruction = ''; if (similarTypes.length > 0) { instruction += `\n\n🔍 Found similar components: ${similarTypes.join(', ')}`; instruction += `\n💡 Suggestion: Perhaps you meant to set the '${similarTypes[0]}' component?`; } // Recommend possible components based on property name const propertyToComponentMap: Record<string, string[]> = { 'string': ['cc.Label', 'cc.RichText', 'cc.EditBox'], 'text': ['cc.Label', 'cc.RichText'], 'fontSize': ['cc.Label', 'cc.RichText'], 'spriteFrame': ['cc.Sprite'], 'color': ['cc.Label', 'cc.Sprite', 'cc.Graphics'], 'normalColor': ['cc.Button'], 'pressedColor': ['cc.Button'], 'target': ['cc.Button'], 'contentSize': ['cc.UITransform'], 'anchorPoint': ['cc.UITransform'] }; const recommendedComponents = propertyToComponentMap[property] || []; const availableRecommended = recommendedComponents.filter(comp => availableTypes.includes(comp)); if (availableRecommended.length > 0) { instruction += `\n\n🎯 Based on property '${property}', recommended components: ${availableRecommended.join(', ')}`; } // Provide operation suggestions instruction += `\n\n📋 Suggested Actions:`; instruction += `\n1. Use get_components(nodeUuid="${requestedType.includes('uuid') ? 'YOUR_NODE_UUID' : 'nodeUuid'}") to view all components on the node`; instruction += `\n2. If you need to add a component, use add_component(nodeUuid="...", componentType="${requestedType}")`; instruction += `\n3. Verify that the component type name is correct (case-sensitive)`; return instruction; } /** * 快速验证资源设置结果 */ private async quickVerifyAsset(nodeUuid: string, componentType: string, property: string): Promise<any> { try { const rawNodeData = await Editor.Message.request('scene', 'query-node', nodeUuid); if (!rawNodeData || !rawNodeData.__comps__) { return null; } // 找到组件 const component = rawNodeData.__comps__.find((comp: any) => { const compType = comp.__type__ || comp.cid || comp.type; return compType === componentType; }); if (!component) { return null; } // 提取属性值 const properties = this.extractComponentProperties(component); const propertyData = properties[property]; if (propertyData && typeof propertyData === 'object' && 'value' in propertyData) { return propertyData.value; } else { return propertyData; } } catch (error) { console.error(`[quickVerifyAsset] Error:`, error); return null; } } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/DaxianLee/cocos-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server