Files
cocos-mcp/source/tools/component-tools.ts
root 1286dbc5c8 - **完全修复预制体创建功能**: 彻底解决了预制体创建时组件/节点/资源类型引用丢失的问题
- **正确的引用处理**: 实现了与手动创建预制体完全一致的引用格式
  - **内部引用**: 预制体内部的节点和组件引用正确转换为 `{"__id__": x}` 格式
  - **外部引用**: 预制体外部的节点和组件引用正确设置为 `null`
  - **资源引用**: 预制体、纹理、精灵帧等资源引用完整保留UUID格式
2025-07-30 12:55:50 +08:00

1777 lines
91 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
}
}
}