This is page 4 of 5. Use http://codebase.md/daxianlee/cocos-mcp-server?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .gitignore
├── @types
│ └── schema
│ └── package
│ ├── base
│ │ └── panels.json
│ ├── contributions
│ │ └── index.json
│ └── index.json
├── base.tsconfig.json
├── dist
│ ├── examples
│ │ └── prefab-instantiation-example.js
│ ├── main.js
│ ├── mcp-server.js
│ ├── panels
│ │ ├── default
│ │ │ └── index.js
│ │ └── tool-manager
│ │ └── index.js
│ ├── scene.js
│ ├── settings.js
│ ├── test
│ │ ├── manual-test.js
│ │ ├── mcp-tool-tester.js
│ │ ├── prefab-tools-test.js
│ │ └── tool-tester.js
│ ├── tools
│ │ ├── asset-advanced-tools.js
│ │ ├── broadcast-tools.js
│ │ ├── component-tools.js
│ │ ├── debug-tools.js
│ │ ├── node-tools.js
│ │ ├── prefab-tools.js
│ │ ├── preferences-tools.js
│ │ ├── project-tools.js
│ │ ├── reference-image-tools.js
│ │ ├── scene-advanced-tools.js
│ │ ├── scene-tools.js
│ │ ├── scene-view-tools.js
│ │ ├── server-tools.js
│ │ ├── tool-manager.js
│ │ └── validation-tools.js
│ └── types
│ └── index.js
├── FEATURE_GUIDE_CN.md
├── FEATURE_GUIDE_EN.md
├── i18n
│ ├── en.js
│ └── zh.js
├── image
│ ├── iamge2.png
│ └── image-20250717174157957.png
├── package-lock.json
├── package.json
├── README.EN.md
├── README.md
├── scripts
│ └── preinstall.js
├── source
│ ├── main.ts
│ ├── mcp-server.ts
│ ├── panels
│ │ ├── default
│ │ │ └── index.ts
│ │ └── tool-manager
│ │ └── index.ts
│ ├── scene.ts
│ ├── settings.ts
│ ├── test
│ │ ├── manual-test.ts
│ │ ├── mcp-tool-tester.ts
│ │ ├── prefab-tools-test.ts
│ │ └── tool-tester.ts
│ ├── tools
│ │ ├── asset-advanced-tools.ts
│ │ ├── broadcast-tools.ts
│ │ ├── component-tools.ts
│ │ ├── debug-tools.ts
│ │ ├── node-tools.ts
│ │ ├── prefab-tools.ts
│ │ ├── preferences-tools.ts
│ │ ├── project-tools.ts
│ │ ├── reference-image-tools.ts
│ │ ├── scene-advanced-tools.ts
│ │ ├── scene-tools.ts
│ │ ├── scene-view-tools.ts
│ │ ├── server-tools.ts
│ │ ├── tool-manager.ts
│ │ └── validation-tools.ts
│ └── types
│ └── index.ts
├── static
│ ├── icon.png
│ ├── style
│ │ └── default
│ │ └── index.css
│ └── template
│ ├── default
│ │ ├── index.html
│ │ └── tool-manager.html
│ └── vue
│ └── mcp-server-app.html
├── TestScript.js
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/source/tools/component-tools.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { ToolDefinition, ToolResponse, ToolExecutor, ComponentInfo } from '../types';
2 |
3 | export class ComponentTools implements ToolExecutor {
4 | getTools(): ToolDefinition[] {
5 | return [
6 | {
7 | name: 'add_component',
8 | description: 'Add a component to a specific node. IMPORTANT: You must provide the nodeUuid parameter to specify which node to add the component to.',
9 | inputSchema: {
10 | type: 'object',
11 | properties: {
12 | nodeUuid: {
13 | type: 'string',
14 | 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.'
15 | },
16 | componentType: {
17 | type: 'string',
18 | description: 'Component type (e.g., cc.Sprite, cc.Label, cc.Button)'
19 | }
20 | },
21 | required: ['nodeUuid', 'componentType']
22 | }
23 | },
24 | {
25 | name: 'remove_component',
26 | 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.',
27 | inputSchema: {
28 | type: 'object',
29 | properties: {
30 | nodeUuid: {
31 | type: 'string',
32 | description: 'Node UUID'
33 | },
34 | componentType: {
35 | type: 'string',
36 | description: 'Component cid (type field from getComponents). Do NOT use script name or class name. Example: "cc.Sprite" or "9b4a7ueT9xD6aRE+AlOusy1"'
37 | }
38 | },
39 | required: ['nodeUuid', 'componentType']
40 | }
41 | },
42 | {
43 | name: 'get_components',
44 | description: 'Get all components of a node',
45 | inputSchema: {
46 | type: 'object',
47 | properties: {
48 | nodeUuid: {
49 | type: 'string',
50 | description: 'Node UUID'
51 | }
52 | },
53 | required: ['nodeUuid']
54 | }
55 | },
56 | {
57 | name: 'get_component_info',
58 | description: 'Get specific component information',
59 | inputSchema: {
60 | type: 'object',
61 | properties: {
62 | nodeUuid: {
63 | type: 'string',
64 | description: 'Node UUID'
65 | },
66 | componentType: {
67 | type: 'string',
68 | description: 'Component type to get info for'
69 | }
70 | },
71 | required: ['nodeUuid', 'componentType']
72 | }
73 | },
74 | {
75 | name: 'set_component_property',
76 | 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.',
77 | inputSchema: {
78 | type: 'object',
79 | properties: {
80 | nodeUuid: {
81 | type: 'string',
82 | description: 'Target node UUID - Must specify the node to operate on'
83 | },
84 | componentType: {
85 | type: 'string',
86 | 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.',
87 | // 移除enum限制,允许任意组件类型包括自定义脚本
88 | },
89 | property: {
90 | type: 'string',
91 | description: 'Property name - The property to set. Common properties include:\n' +
92 | '• cc.Label: string (text content), fontSize (font size), color (text color)\n' +
93 | '• cc.Sprite: spriteFrame (sprite frame), color (tint color), sizeMode (size mode)\n' +
94 | '• cc.Button: normalColor (normal color), pressedColor (pressed color), target (target node)\n' +
95 | '• cc.UITransform: contentSize (content size), anchorPoint (anchor point)\n' +
96 | '• Custom Scripts: Based on properties defined in the script'
97 | },
98 | propertyType: {
99 | type: 'string',
100 | description: 'Property type - Must explicitly specify the property data type for correct value conversion and validation',
101 | enum: [
102 | 'string', 'number', 'boolean', 'integer', 'float',
103 | 'color', 'vec2', 'vec3', 'size',
104 | 'node', 'component', 'spriteFrame', 'prefab', 'asset',
105 | 'nodeArray', 'colorArray', 'numberArray', 'stringArray'
106 | ]
107 | },
108 |
109 | value: {
110 | description: 'Property value - Use the corresponding data format based on propertyType:\n\n' +
111 | '📝 Basic Data Types:\n' +
112 | '• string: "Hello World" (text string)\n' +
113 | '• number/integer/float: 42 or 3.14 (numeric value)\n' +
114 | '• boolean: true or false (boolean value)\n\n' +
115 | '🎨 Color Type:\n' +
116 | '• color: {"r":255,"g":0,"b":0,"a":255} (RGBA values, range 0-255)\n' +
117 | ' - Alternative: "#FF0000" (hexadecimal format)\n' +
118 | ' - Transparency: a value controls opacity, 255 = fully opaque, 0 = fully transparent\n\n' +
119 | '📐 Vector and Size Types:\n' +
120 | '• vec2: {"x":100,"y":50} (2D vector)\n' +
121 | '• vec3: {"x":1,"y":2,"z":3} (3D vector)\n' +
122 | '• size: {"width":100,"height":50} (size dimensions)\n\n' +
123 | '🔗 Reference Types (using UUID strings):\n' +
124 | '• node: "target-node-uuid" (node reference)\n' +
125 | ' How to get: Use get_all_nodes or find_node_by_name to get node UUIDs\n' +
126 | '• component: "target-node-uuid" (component reference)\n' +
127 | ' How it works: \n' +
128 | ' 1. Provide the UUID of the NODE that contains the target component\n' +
129 | ' 2. System auto-detects required component type from property metadata\n' +
130 | ' 3. Finds the component on target node and gets its scene __id__\n' +
131 | ' 4. Sets reference using the scene __id__ (not node UUID)\n' +
132 | ' Example: value="label-node-uuid" will find cc.Label and use its scene ID\n' +
133 | '• spriteFrame: "spriteframe-uuid" (sprite frame asset)\n' +
134 | ' How to get: Check asset database or use asset browser\n' +
135 | '• prefab: "prefab-uuid" (prefab asset)\n' +
136 | ' How to get: Check asset database or use asset browser\n' +
137 | '• asset: "asset-uuid" (generic asset reference)\n' +
138 | ' How to get: Check asset database or use asset browser\n\n' +
139 | '📋 Array Types:\n' +
140 | '• nodeArray: ["uuid1","uuid2"] (array of node UUIDs)\n' +
141 | '• colorArray: [{"r":255,"g":0,"b":0,"a":255}] (array of colors)\n' +
142 | '• numberArray: [1,2,3,4,5] (array of numbers)\n' +
143 | '• stringArray: ["item1","item2"] (array of strings)'
144 | }
145 | },
146 | required: ['nodeUuid', 'componentType', 'property', 'propertyType', 'value']
147 | }
148 | },
149 | {
150 | name: 'attach_script',
151 | description: 'Attach a script component to a node',
152 | inputSchema: {
153 | type: 'object',
154 | properties: {
155 | nodeUuid: {
156 | type: 'string',
157 | description: 'Node UUID'
158 | },
159 | scriptPath: {
160 | type: 'string',
161 | description: 'Script asset path (e.g., db://assets/scripts/MyScript.ts)'
162 | }
163 | },
164 | required: ['nodeUuid', 'scriptPath']
165 | }
166 | },
167 | {
168 | name: 'get_available_components',
169 | description: 'Get list of available component types',
170 | inputSchema: {
171 | type: 'object',
172 | properties: {
173 | category: {
174 | type: 'string',
175 | description: 'Component category filter',
176 | enum: ['all', 'renderer', 'ui', 'physics', 'animation', 'audio'],
177 | default: 'all'
178 | }
179 | }
180 | }
181 | }
182 | ];
183 | }
184 |
185 | async execute(toolName: string, args: any): Promise<ToolResponse> {
186 | switch (toolName) {
187 | case 'add_component':
188 | return await this.addComponent(args.nodeUuid, args.componentType);
189 | case 'remove_component':
190 | return await this.removeComponent(args.nodeUuid, args.componentType);
191 | case 'get_components':
192 | return await this.getComponents(args.nodeUuid);
193 | case 'get_component_info':
194 | return await this.getComponentInfo(args.nodeUuid, args.componentType);
195 | case 'set_component_property':
196 | return await this.setComponentProperty(args);
197 | case 'attach_script':
198 | return await this.attachScript(args.nodeUuid, args.scriptPath);
199 | case 'get_available_components':
200 | return await this.getAvailableComponents(args.category);
201 | default:
202 | throw new Error(`Unknown tool: ${toolName}`);
203 | }
204 | }
205 |
206 | private async addComponent(nodeUuid: string, componentType: string): Promise<ToolResponse> {
207 | return new Promise(async (resolve) => {
208 | // 先查找节点上是否已存在该组件
209 | const allComponentsInfo = await this.getComponents(nodeUuid);
210 | if (allComponentsInfo.success && allComponentsInfo.data?.components) {
211 | const existingComponent = allComponentsInfo.data.components.find((comp: any) => comp.type === componentType);
212 | if (existingComponent) {
213 | resolve({
214 | success: true,
215 | message: `Component '${componentType}' already exists on node`,
216 | data: {
217 | nodeUuid: nodeUuid,
218 | componentType: componentType,
219 | componentVerified: true,
220 | existing: true
221 | }
222 | });
223 | return;
224 | }
225 | }
226 | // 尝试直接使用 Editor API 添加组件
227 | Editor.Message.request('scene', 'create-component', {
228 | uuid: nodeUuid,
229 | component: componentType
230 | }).then(async (result: any) => {
231 | // 等待一段时间让Editor完成组件添加
232 | await new Promise(resolve => setTimeout(resolve, 100));
233 | // 重新查询节点信息验证组件是否真的添加成功
234 | try {
235 | const allComponentsInfo2 = await this.getComponents(nodeUuid);
236 | if (allComponentsInfo2.success && allComponentsInfo2.data?.components) {
237 | const addedComponent = allComponentsInfo2.data.components.find((comp: any) => comp.type === componentType);
238 | if (addedComponent) {
239 | resolve({
240 | success: true,
241 | message: `Component '${componentType}' added successfully`,
242 | data: {
243 | nodeUuid: nodeUuid,
244 | componentType: componentType,
245 | componentVerified: true,
246 | existing: false
247 | }
248 | });
249 | } else {
250 | resolve({
251 | success: false,
252 | error: `Component '${componentType}' was not found on node after addition. Available components: ${allComponentsInfo2.data.components.map((c: any) => c.type).join(', ')}`
253 | });
254 | }
255 | } else {
256 | resolve({
257 | success: false,
258 | error: `Failed to verify component addition: ${allComponentsInfo2.error || 'Unable to get node components'}`
259 | });
260 | }
261 | } catch (verifyError: any) {
262 | resolve({
263 | success: false,
264 | error: `Failed to verify component addition: ${verifyError.message}`
265 | });
266 | }
267 | }).catch((err: Error) => {
268 | // 备用方案:使用场景脚本
269 | const options = {
270 | name: 'cocos-mcp-server',
271 | method: 'addComponentToNode',
272 | args: [nodeUuid, componentType]
273 | };
274 | Editor.Message.request('scene', 'execute-scene-script', options).then((result: any) => {
275 | resolve(result);
276 | }).catch((err2: Error) => {
277 | resolve({ success: false, error: `Direct API failed: ${err.message}, Scene script failed: ${err2.message}` });
278 | });
279 | });
280 | });
281 | }
282 |
283 | private async removeComponent(nodeUuid: string, componentType: string): Promise<ToolResponse> {
284 | return new Promise(async (resolve) => {
285 | // 1. 查找节点上的所有组件
286 | const allComponentsInfo = await this.getComponents(nodeUuid);
287 | if (!allComponentsInfo.success || !allComponentsInfo.data?.components) {
288 | resolve({ success: false, error: `Failed to get components for node '${nodeUuid}': ${allComponentsInfo.error}` });
289 | return;
290 | }
291 | // 2. 只查找type字段等于componentType的组件(即cid)
292 | const exists = allComponentsInfo.data.components.some((comp: any) => comp.type === componentType);
293 | if (!exists) {
294 | resolve({ success: false, error: `Component cid '${componentType}' not found on node '${nodeUuid}'. 请用getComponents获取type字段(cid)作为componentType。` });
295 | return;
296 | }
297 | // 3. 官方API直接移除
298 | try {
299 | await Editor.Message.request('scene', 'remove-component', {
300 | uuid: nodeUuid,
301 | component: componentType
302 | });
303 | // 4. 再查一次确认是否移除
304 | const afterRemoveInfo = await this.getComponents(nodeUuid);
305 | const stillExists = afterRemoveInfo.success && afterRemoveInfo.data?.components?.some((comp: any) => comp.type === componentType);
306 | if (stillExists) {
307 | resolve({ success: false, error: `Component cid '${componentType}' was not removed from node '${nodeUuid}'.` });
308 | } else {
309 | resolve({
310 | success: true,
311 | message: `Component cid '${componentType}' removed successfully from node '${nodeUuid}'`,
312 | data: { nodeUuid, componentType }
313 | });
314 | }
315 | } catch (err: any) {
316 | resolve({ success: false, error: `Failed to remove component: ${err.message}` });
317 | }
318 | });
319 | }
320 |
321 | private async getComponents(nodeUuid: string): Promise<ToolResponse> {
322 | return new Promise((resolve) => {
323 | // 优先尝试直接使用 Editor API 查询节点信息
324 | Editor.Message.request('scene', 'query-node', nodeUuid).then((nodeData: any) => {
325 | if (nodeData && nodeData.__comps__) {
326 | const components = nodeData.__comps__.map((comp: any) => ({
327 | type: comp.__type__ || comp.cid || comp.type || 'Unknown',
328 | uuid: comp.uuid?.value || comp.uuid || null,
329 | enabled: comp.enabled !== undefined ? comp.enabled : true,
330 | properties: this.extractComponentProperties(comp)
331 | }));
332 |
333 | resolve({
334 | success: true,
335 | data: {
336 | nodeUuid: nodeUuid,
337 | components: components
338 | }
339 | });
340 | } else {
341 | resolve({ success: false, error: 'Node not found or no components data' });
342 | }
343 | }).catch((err: Error) => {
344 | // 备用方案:使用场景脚本
345 | const options = {
346 | name: 'cocos-mcp-server',
347 | method: 'getNodeInfo',
348 | args: [nodeUuid]
349 | };
350 |
351 | Editor.Message.request('scene', 'execute-scene-script', options).then((result: any) => {
352 | if (result.success) {
353 | resolve({
354 | success: true,
355 | data: result.data.components
356 | });
357 | } else {
358 | resolve(result);
359 | }
360 | }).catch((err2: Error) => {
361 | resolve({ success: false, error: `Direct API failed: ${err.message}, Scene script failed: ${err2.message}` });
362 | });
363 | });
364 | });
365 | }
366 |
367 | private async getComponentInfo(nodeUuid: string, componentType: string): Promise<ToolResponse> {
368 | return new Promise((resolve) => {
369 | // 优先尝试直接使用 Editor API 查询节点信息
370 | Editor.Message.request('scene', 'query-node', nodeUuid).then((nodeData: any) => {
371 | if (nodeData && nodeData.__comps__) {
372 | const component = nodeData.__comps__.find((comp: any) => {
373 | const compType = comp.__type__ || comp.cid || comp.type;
374 | return compType === componentType;
375 | });
376 |
377 | if (component) {
378 | resolve({
379 | success: true,
380 | data: {
381 | nodeUuid: nodeUuid,
382 | componentType: componentType,
383 | enabled: component.enabled !== undefined ? component.enabled : true,
384 | properties: this.extractComponentProperties(component)
385 | }
386 | });
387 | } else {
388 | resolve({ success: false, error: `Component '${componentType}' not found on node` });
389 | }
390 | } else {
391 | resolve({ success: false, error: 'Node not found or no components data' });
392 | }
393 | }).catch((err: Error) => {
394 | // 备用方案:使用场景脚本
395 | const options = {
396 | name: 'cocos-mcp-server',
397 | method: 'getNodeInfo',
398 | args: [nodeUuid]
399 | };
400 |
401 | Editor.Message.request('scene', 'execute-scene-script', options).then((result: any) => {
402 | if (result.success && result.data.components) {
403 | const component = result.data.components.find((comp: any) => comp.type === componentType);
404 | if (component) {
405 | resolve({
406 | success: true,
407 | data: {
408 | nodeUuid: nodeUuid,
409 | componentType: componentType,
410 | ...component
411 | }
412 | });
413 | } else {
414 | resolve({ success: false, error: `Component '${componentType}' not found on node` });
415 | }
416 | } else {
417 | resolve({ success: false, error: result.error || 'Failed to get component info' });
418 | }
419 | }).catch((err2: Error) => {
420 | resolve({ success: false, error: `Direct API failed: ${err.message}, Scene script failed: ${err2.message}` });
421 | });
422 | });
423 | });
424 | }
425 |
426 | private extractComponentProperties(component: any): Record<string, any> {
427 | console.log(`[extractComponentProperties] Processing component:`, Object.keys(component));
428 |
429 | // 检查组件是否有 value 属性,这通常包含实际的组件属性
430 | if (component.value && typeof component.value === 'object') {
431 | console.log(`[extractComponentProperties] Found component.value with properties:`, Object.keys(component.value));
432 | return component.value; // 直接返回 value 对象,它包含所有组件属性
433 | }
434 |
435 | // 备用方案:从组件对象中直接提取属性
436 | const properties: Record<string, any> = {};
437 | const excludeKeys = ['__type__', 'enabled', 'node', '_id', '__scriptAsset', 'uuid', 'name', '_name', '_objFlags', '_enabled', 'type', 'readonly', 'visible', 'cid', 'editor', 'extends'];
438 |
439 | for (const key in component) {
440 | if (!excludeKeys.includes(key) && !key.startsWith('_')) {
441 | console.log(`[extractComponentProperties] Found direct property '${key}':`, typeof component[key]);
442 | properties[key] = component[key];
443 | }
444 | }
445 |
446 | console.log(`[extractComponentProperties] Final extracted properties:`, Object.keys(properties));
447 | return properties;
448 | }
449 |
450 | private async findComponentTypeByUuid(componentUuid: string): Promise<string | null> {
451 | console.log(`[findComponentTypeByUuid] Searching for component type with UUID: ${componentUuid}`);
452 | if (!componentUuid) {
453 | return null;
454 | }
455 | try {
456 | const nodeTree = await Editor.Message.request('scene', 'query-node-tree');
457 | if (!nodeTree) {
458 | console.warn('[findComponentTypeByUuid] Failed to query node tree.');
459 | return null;
460 | }
461 |
462 | const queue: any[] = [nodeTree];
463 |
464 | while (queue.length > 0) {
465 | const currentNodeInfo = queue.shift();
466 | if (!currentNodeInfo || !currentNodeInfo.uuid) {
467 | continue;
468 | }
469 |
470 | try {
471 | const fullNodeData = await Editor.Message.request('scene', 'query-node', currentNodeInfo.uuid);
472 | if (fullNodeData && fullNodeData.__comps__) {
473 | for (const comp of fullNodeData.__comps__) {
474 | const compAny = comp as any; // Cast to any to access dynamic properties
475 | // The component UUID is nested in the 'value' property
476 | if (compAny.uuid && compAny.uuid.value === componentUuid) {
477 | const componentType = compAny.__type__;
478 | console.log(`[findComponentTypeByUuid] Found component type '${componentType}' for UUID ${componentUuid} on node ${fullNodeData.name?.value}`);
479 | return componentType;
480 | }
481 | }
482 | }
483 | } catch (e) {
484 | console.warn(`[findComponentTypeByUuid] Could not query node ${currentNodeInfo.uuid}:`, e);
485 | }
486 |
487 | if (currentNodeInfo.children) {
488 | for (const child of currentNodeInfo.children) {
489 | queue.push(child);
490 | }
491 | }
492 | }
493 |
494 | console.warn(`[findComponentTypeByUuid] Component with UUID ${componentUuid} not found in scene tree.`);
495 | return null;
496 | } catch (error) {
497 | console.error(`[findComponentTypeByUuid] Error while searching for component type:`, error);
498 | return null;
499 | }
500 | }
501 |
502 | private async setComponentProperty(args: any): Promise<ToolResponse> {
503 | const { nodeUuid, componentType, property, propertyType, value } = args;
504 |
505 | return new Promise(async (resolve) => {
506 | try {
507 | console.log(`[ComponentTools] Setting ${componentType}.${property} (type: ${propertyType}) = ${JSON.stringify(value)} on node ${nodeUuid}`);
508 |
509 | // Step 0: 检测是否为节点属性,如果是则重定向到对应的节点方法
510 | const nodeRedirectResult = await this.checkAndRedirectNodeProperties(args);
511 | if (nodeRedirectResult) {
512 | resolve(nodeRedirectResult);
513 | return;
514 | }
515 |
516 | // Step 1: 获取组件信息,使用与getComponents相同的方法
517 | const componentsResponse = await this.getComponents(nodeUuid);
518 | if (!componentsResponse.success || !componentsResponse.data) {
519 | resolve({
520 | success: false,
521 | error: `Failed to get components for node '${nodeUuid}': ${componentsResponse.error}`,
522 | instruction: `Please verify that node UUID '${nodeUuid}' is correct. Use get_all_nodes or find_node_by_name to get the correct node UUID.`
523 | });
524 | return;
525 | }
526 |
527 | const allComponents = componentsResponse.data.components;
528 |
529 | // Step 2: 查找目标组件
530 | let targetComponent = null;
531 | const availableTypes: string[] = [];
532 |
533 | for (let i = 0; i < allComponents.length; i++) {
534 | const comp = allComponents[i];
535 | availableTypes.push(comp.type);
536 |
537 | if (comp.type === componentType) {
538 | targetComponent = comp;
539 | break;
540 | }
541 | }
542 |
543 | if (!targetComponent) {
544 | // 提供更详细的错误信息和建议
545 | const instruction = this.generateComponentSuggestion(componentType, availableTypes, property);
546 | resolve({
547 | success: false,
548 | error: `Component '${componentType}' not found on node. Available components: ${availableTypes.join(', ')}`,
549 | instruction: instruction
550 | });
551 | return;
552 | }
553 |
554 | // Step 3: 自动检测和转换属性值
555 | let propertyInfo;
556 | try {
557 | console.log(`[ComponentTools] Analyzing property: ${property}`);
558 | propertyInfo = this.analyzeProperty(targetComponent, property);
559 | } catch (analyzeError: any) {
560 | console.error(`[ComponentTools] Error in analyzeProperty:`, analyzeError);
561 | resolve({
562 | success: false,
563 | error: `Failed to analyze property '${property}': ${analyzeError.message}`
564 | });
565 | return;
566 | }
567 |
568 | if (!propertyInfo.exists) {
569 | resolve({
570 | success: false,
571 | error: `Property '${property}' not found on component '${componentType}'. Available properties: ${propertyInfo.availableProperties.join(', ')}`
572 | });
573 | return;
574 | }
575 |
576 | // Step 4: 处理属性值和设置
577 | const originalValue = propertyInfo.originalValue;
578 | let processedValue: any;
579 |
580 | // 根据明确的propertyType处理属性值
581 | switch (propertyType) {
582 | case 'string':
583 | processedValue = String(value);
584 | break;
585 | case 'number':
586 | case 'integer':
587 | case 'float':
588 | processedValue = Number(value);
589 | break;
590 | case 'boolean':
591 | processedValue = Boolean(value);
592 | break;
593 | case 'color':
594 | if (typeof value === 'string') {
595 | // 字符串格式:支持十六进制、颜色名称、rgb()/rgba()
596 | processedValue = this.parseColorString(value);
597 | } else if (typeof value === 'object' && value !== null) {
598 | // 对象格式:验证并转换RGBA值
599 | processedValue = {
600 | r: Math.min(255, Math.max(0, Number(value.r) || 0)),
601 | g: Math.min(255, Math.max(0, Number(value.g) || 0)),
602 | b: Math.min(255, Math.max(0, Number(value.b) || 0)),
603 | a: value.a !== undefined ? Math.min(255, Math.max(0, Number(value.a))) : 255
604 | };
605 | } else {
606 | throw new Error('Color value must be an object with r, g, b properties or a hexadecimal string (e.g., "#FF0000")');
607 | }
608 | break;
609 | case 'vec2':
610 | if (typeof value === 'object' && value !== null) {
611 | processedValue = {
612 | x: Number(value.x) || 0,
613 | y: Number(value.y) || 0
614 | };
615 | } else {
616 | throw new Error('Vec2 value must be an object with x, y properties');
617 | }
618 | break;
619 | case 'vec3':
620 | if (typeof value === 'object' && value !== null) {
621 | processedValue = {
622 | x: Number(value.x) || 0,
623 | y: Number(value.y) || 0,
624 | z: Number(value.z) || 0
625 | };
626 | } else {
627 | throw new Error('Vec3 value must be an object with x, y, z properties');
628 | }
629 | break;
630 | case 'size':
631 | if (typeof value === 'object' && value !== null) {
632 | processedValue = {
633 | width: Number(value.width) || 0,
634 | height: Number(value.height) || 0
635 | };
636 | } else {
637 | throw new Error('Size value must be an object with width, height properties');
638 | }
639 | break;
640 | case 'node':
641 | if (typeof value === 'string') {
642 | processedValue = { uuid: value };
643 | } else {
644 | throw new Error('Node reference value must be a string UUID');
645 | }
646 | break;
647 | case 'component':
648 | if (typeof value === 'string') {
649 | // 组件引用需要特殊处理:通过节点UUID找到组件的__id__
650 | processedValue = value; // 先保存节点UUID,后续会转换为__id__
651 | } else {
652 | throw new Error('Component reference value must be a string (node UUID containing the target component)');
653 | }
654 | break;
655 | case 'spriteFrame':
656 | case 'prefab':
657 | case 'asset':
658 | if (typeof value === 'string') {
659 | processedValue = { uuid: value };
660 | } else {
661 | throw new Error(`${propertyType} value must be a string UUID`);
662 | }
663 | break;
664 | case 'nodeArray':
665 | if (Array.isArray(value)) {
666 | processedValue = value.map((item: any) => {
667 | if (typeof item === 'string') {
668 | return { uuid: item };
669 | } else {
670 | throw new Error('NodeArray items must be string UUIDs');
671 | }
672 | });
673 | } else {
674 | throw new Error('NodeArray value must be an array');
675 | }
676 | break;
677 | case 'colorArray':
678 | if (Array.isArray(value)) {
679 | processedValue = value.map((item: any) => {
680 | if (typeof item === 'object' && item !== null && 'r' in item) {
681 | return {
682 | r: Math.min(255, Math.max(0, Number(item.r) || 0)),
683 | g: Math.min(255, Math.max(0, Number(item.g) || 0)),
684 | b: Math.min(255, Math.max(0, Number(item.b) || 0)),
685 | a: item.a !== undefined ? Math.min(255, Math.max(0, Number(item.a))) : 255
686 | };
687 | } else {
688 | return { r: 255, g: 255, b: 255, a: 255 };
689 | }
690 | });
691 | } else {
692 | throw new Error('ColorArray value must be an array');
693 | }
694 | break;
695 | case 'numberArray':
696 | if (Array.isArray(value)) {
697 | processedValue = value.map((item: any) => Number(item));
698 | } else {
699 | throw new Error('NumberArray value must be an array');
700 | }
701 | break;
702 | case 'stringArray':
703 | if (Array.isArray(value)) {
704 | processedValue = value.map((item: any) => String(item));
705 | } else {
706 | throw new Error('StringArray value must be an array');
707 | }
708 | break;
709 | default:
710 | throw new Error(`Unsupported property type: ${propertyType}`);
711 | }
712 |
713 | console.log(`[ComponentTools] Converting value: ${JSON.stringify(value)} -> ${JSON.stringify(processedValue)} (type: ${propertyType})`);
714 | console.log(`[ComponentTools] Property analysis result: propertyInfo.type="${propertyInfo.type}", propertyType="${propertyType}"`);
715 | console.log(`[ComponentTools] Will use color special handling: ${propertyType === 'color' && processedValue && typeof processedValue === 'object'}`);
716 |
717 | // 用于验证的实际期望值(对于组件引用需要特殊处理)
718 | let actualExpectedValue = processedValue;
719 |
720 | // Step 5: 获取原始节点数据来构建正确的路径
721 | const rawNodeData = await Editor.Message.request('scene', 'query-node', nodeUuid);
722 | if (!rawNodeData || !rawNodeData.__comps__) {
723 | resolve({
724 | success: false,
725 | error: `Failed to get raw node data for property setting`
726 | });
727 | return;
728 | }
729 |
730 | // 找到原始组件的索引
731 | let rawComponentIndex = -1;
732 | for (let i = 0; i < rawNodeData.__comps__.length; i++) {
733 | const comp = rawNodeData.__comps__[i] as any;
734 | const compType = comp.__type__ || comp.cid || comp.type || 'Unknown';
735 | if (compType === componentType) {
736 | rawComponentIndex = i;
737 | break;
738 | }
739 | }
740 |
741 | if (rawComponentIndex === -1) {
742 | resolve({
743 | success: false,
744 | error: `Could not find component index for setting property`
745 | });
746 | return;
747 | }
748 |
749 | // 构建正确的属性路径
750 | let propertyPath = `__comps__.${rawComponentIndex}.${property}`;
751 |
752 | // 特殊处理资源类属性
753 | if (propertyType === 'asset' || propertyType === 'spriteFrame' || propertyType === 'prefab' ||
754 | (propertyInfo.type === 'asset' && propertyType === 'string')) {
755 |
756 | console.log(`[ComponentTools] Setting asset reference:`, {
757 | value: processedValue,
758 | property: property,
759 | propertyType: propertyType,
760 | path: propertyPath
761 | });
762 |
763 | // Determine asset type based on property name
764 | let assetType = 'cc.SpriteFrame'; // default
765 | if (property.toLowerCase().includes('texture')) {
766 | assetType = 'cc.Texture2D';
767 | } else if (property.toLowerCase().includes('material')) {
768 | assetType = 'cc.Material';
769 | } else if (property.toLowerCase().includes('font')) {
770 | assetType = 'cc.Font';
771 | } else if (property.toLowerCase().includes('clip')) {
772 | assetType = 'cc.AudioClip';
773 | } else if (propertyType === 'prefab') {
774 | assetType = 'cc.Prefab';
775 | }
776 |
777 | await Editor.Message.request('scene', 'set-property', {
778 | uuid: nodeUuid,
779 | path: propertyPath,
780 | dump: {
781 | value: processedValue,
782 | type: assetType
783 | }
784 | });
785 | } else if (componentType === 'cc.UITransform' && (property === '_contentSize' || property === 'contentSize')) {
786 | // Special handling for UITransform contentSize - set width and height separately
787 | const width = Number(value.width) || 100;
788 | const height = Number(value.height) || 100;
789 |
790 | // Set width first
791 | await Editor.Message.request('scene', 'set-property', {
792 | uuid: nodeUuid,
793 | path: `__comps__.${rawComponentIndex}.width`,
794 | dump: { value: width }
795 | });
796 |
797 | // Then set height
798 | await Editor.Message.request('scene', 'set-property', {
799 | uuid: nodeUuid,
800 | path: `__comps__.${rawComponentIndex}.height`,
801 | dump: { value: height }
802 | });
803 | } else if (componentType === 'cc.UITransform' && (property === '_anchorPoint' || property === 'anchorPoint')) {
804 | // Special handling for UITransform anchorPoint - set anchorX and anchorY separately
805 | const anchorX = Number(value.x) || 0.5;
806 | const anchorY = Number(value.y) || 0.5;
807 |
808 | // Set anchorX first
809 | await Editor.Message.request('scene', 'set-property', {
810 | uuid: nodeUuid,
811 | path: `__comps__.${rawComponentIndex}.anchorX`,
812 | dump: { value: anchorX }
813 | });
814 |
815 | // Then set anchorY
816 | await Editor.Message.request('scene', 'set-property', {
817 | uuid: nodeUuid,
818 | path: `__comps__.${rawComponentIndex}.anchorY`,
819 | dump: { value: anchorY }
820 | });
821 | } else if (propertyType === 'color' && processedValue && typeof processedValue === 'object') {
822 | // 特殊处理颜色属性,确保RGBA值正确
823 | // Cocos Creator颜色值范围是0-255
824 | const colorValue = {
825 | r: Math.min(255, Math.max(0, Number(processedValue.r) || 0)),
826 | g: Math.min(255, Math.max(0, Number(processedValue.g) || 0)),
827 | b: Math.min(255, Math.max(0, Number(processedValue.b) || 0)),
828 | a: processedValue.a !== undefined ? Math.min(255, Math.max(0, Number(processedValue.a))) : 255
829 | };
830 |
831 | console.log(`[ComponentTools] Setting color value:`, colorValue);
832 |
833 | await Editor.Message.request('scene', 'set-property', {
834 | uuid: nodeUuid,
835 | path: propertyPath,
836 | dump: {
837 | value: colorValue,
838 | type: 'cc.Color'
839 | }
840 | });
841 | } else if (propertyType === 'vec3' && processedValue && typeof processedValue === 'object') {
842 | // 特殊处理Vec3属性
843 | const vec3Value = {
844 | x: Number(processedValue.x) || 0,
845 | y: Number(processedValue.y) || 0,
846 | z: Number(processedValue.z) || 0
847 | };
848 |
849 | await Editor.Message.request('scene', 'set-property', {
850 | uuid: nodeUuid,
851 | path: propertyPath,
852 | dump: {
853 | value: vec3Value,
854 | type: 'cc.Vec3'
855 | }
856 | });
857 | } else if (propertyType === 'vec2' && processedValue && typeof processedValue === 'object') {
858 | // 特殊处理Vec2属性
859 | const vec2Value = {
860 | x: Number(processedValue.x) || 0,
861 | y: Number(processedValue.y) || 0
862 | };
863 |
864 | await Editor.Message.request('scene', 'set-property', {
865 | uuid: nodeUuid,
866 | path: propertyPath,
867 | dump: {
868 | value: vec2Value,
869 | type: 'cc.Vec2'
870 | }
871 | });
872 | } else if (propertyType === 'size' && processedValue && typeof processedValue === 'object') {
873 | // 特殊处理Size属性
874 | const sizeValue = {
875 | width: Number(processedValue.width) || 0,
876 | height: Number(processedValue.height) || 0
877 | };
878 |
879 | await Editor.Message.request('scene', 'set-property', {
880 | uuid: nodeUuid,
881 | path: propertyPath,
882 | dump: {
883 | value: sizeValue,
884 | type: 'cc.Size'
885 | }
886 | });
887 | } else if (propertyType === 'node' && processedValue && typeof processedValue === 'object' && 'uuid' in processedValue) {
888 | // 特殊处理节点引用
889 | console.log(`[ComponentTools] Setting node reference with UUID: ${processedValue.uuid}`);
890 | await Editor.Message.request('scene', 'set-property', {
891 | uuid: nodeUuid,
892 | path: propertyPath,
893 | dump: {
894 | value: processedValue,
895 | type: 'cc.Node'
896 | }
897 | });
898 | } else if (propertyType === 'component' && typeof processedValue === 'string') {
899 | // 特殊处理组件引用:通过节点UUID找到组件的__id__
900 | const targetNodeUuid = processedValue;
901 | console.log(`[ComponentTools] Setting component reference - finding component on node: ${targetNodeUuid}`);
902 |
903 | // 从当前组件的属性元数据中获取期望的组件类型
904 | let expectedComponentType = '';
905 |
906 | // 获取当前组件的详细信息,包括属性元数据
907 | const currentComponentInfo = await this.getComponentInfo(nodeUuid, componentType);
908 | if (currentComponentInfo.success && currentComponentInfo.data?.properties?.[property]) {
909 | const propertyMeta = currentComponentInfo.data.properties[property];
910 |
911 | // 从属性元数据中提取组件类型信息
912 | if (propertyMeta && typeof propertyMeta === 'object') {
913 | // 检查是否有type字段指示组件类型
914 | if (propertyMeta.type) {
915 | expectedComponentType = propertyMeta.type;
916 | } else if (propertyMeta.ctor) {
917 | // 有些属性可能使用ctor字段
918 | expectedComponentType = propertyMeta.ctor;
919 | } else if (propertyMeta.extends && Array.isArray(propertyMeta.extends)) {
920 | // 检查extends数组,通常第一个是最具体的类型
921 | for (const extendType of propertyMeta.extends) {
922 | if (extendType.startsWith('cc.') && extendType !== 'cc.Component' && extendType !== 'cc.Object') {
923 | expectedComponentType = extendType;
924 | break;
925 | }
926 | }
927 | }
928 | }
929 | }
930 |
931 | if (!expectedComponentType) {
932 | throw new Error(`Unable to determine required component type for property '${property}' on component '${componentType}'. Property metadata may not contain type information.`);
933 | }
934 |
935 | console.log(`[ComponentTools] Detected required component type: ${expectedComponentType} for property: ${property}`);
936 |
937 | try {
938 | // 获取目标节点的组件信息
939 | const targetNodeData = await Editor.Message.request('scene', 'query-node', targetNodeUuid);
940 | if (!targetNodeData || !targetNodeData.__comps__) {
941 | throw new Error(`Target node ${targetNodeUuid} not found or has no components`);
942 | }
943 |
944 | // 打印目标节点的组件概览
945 | console.log(`[ComponentTools] Target node ${targetNodeUuid} has ${targetNodeData.__comps__.length} components:`);
946 | targetNodeData.__comps__.forEach((comp: any, index: number) => {
947 | const sceneId = comp.value && comp.value.uuid && comp.value.uuid.value ? comp.value.uuid.value : 'unknown';
948 | console.log(`[ComponentTools] Component ${index}: ${comp.type} (scene_id: ${sceneId})`);
949 | });
950 |
951 | // 查找对应的组件
952 | let targetComponent = null;
953 | let componentId: string | null = null;
954 |
955 | // 在目标节点的_components数组中查找指定类型的组件
956 | // 注意:__comps__和_components的索引是对应的
957 | console.log(`[ComponentTools] Searching for component type: ${expectedComponentType}`);
958 |
959 | for (let i = 0; i < targetNodeData.__comps__.length; i++) {
960 | const comp = targetNodeData.__comps__[i] as any;
961 | console.log(`[ComponentTools] Checking component ${i}: type=${comp.type}, target=${expectedComponentType}`);
962 |
963 | if (comp.type === expectedComponentType) {
964 | targetComponent = comp;
965 | console.log(`[ComponentTools] Found matching component at index ${i}: ${comp.type}`);
966 |
967 | // 从组件的value.uuid.value中获取组件在场景中的ID
968 | if (comp.value && comp.value.uuid && comp.value.uuid.value) {
969 | componentId = comp.value.uuid.value;
970 | console.log(`[ComponentTools] Got componentId from comp.value.uuid.value: ${componentId}`);
971 | } else {
972 | console.log(`[ComponentTools] Component structure:`, {
973 | hasValue: !!comp.value,
974 | hasUuid: !!(comp.value && comp.value.uuid),
975 | hasUuidValue: !!(comp.value && comp.value.uuid && comp.value.uuid.value),
976 | uuidStructure: comp.value ? comp.value.uuid : 'No value'
977 | });
978 | throw new Error(`Unable to extract component ID from component structure`);
979 | }
980 |
981 | break;
982 | }
983 | }
984 |
985 | if (!targetComponent) {
986 | // 如果没找到,列出可用组件让用户了解,显示场景中的真实ID
987 | const availableComponents = targetNodeData.__comps__.map((comp: any, index: number) => {
988 | let sceneId = 'unknown';
989 | // 从组件的value.uuid.value获取场景ID
990 | if (comp.value && comp.value.uuid && comp.value.uuid.value) {
991 | sceneId = comp.value.uuid.value;
992 | }
993 | return `${comp.type}(scene_id:${sceneId})`;
994 | });
995 | throw new Error(`Component type '${expectedComponentType}' not found on node ${targetNodeUuid}. Available components: ${availableComponents.join(', ')}`);
996 | }
997 |
998 | console.log(`[ComponentTools] Found component ${expectedComponentType} with scene ID: ${componentId} on node ${targetNodeUuid}`);
999 |
1000 | // 更新期望值为实际的组件ID对象格式,用于后续验证
1001 | if (componentId) {
1002 | actualExpectedValue = { uuid: componentId };
1003 | }
1004 |
1005 | // 尝试使用与节点/资源引用相同的格式:{uuid: componentId}
1006 | // 测试看是否能正确设置组件引用
1007 | await Editor.Message.request('scene', 'set-property', {
1008 | uuid: nodeUuid,
1009 | path: propertyPath,
1010 | dump: {
1011 | value: { uuid: componentId }, // 使用对象格式,像节点/资源引用一样
1012 | type: expectedComponentType
1013 | }
1014 | });
1015 |
1016 | } catch (error) {
1017 | console.error(`[ComponentTools] Error setting component reference:`, error);
1018 | throw error;
1019 | }
1020 | } else if (propertyType === 'nodeArray' && Array.isArray(processedValue)) {
1021 | // 特殊处理节点数组 - 保持预处理的格式
1022 | console.log(`[ComponentTools] Setting node array:`, processedValue);
1023 |
1024 | await Editor.Message.request('scene', 'set-property', {
1025 | uuid: nodeUuid,
1026 | path: propertyPath,
1027 | dump: {
1028 | value: processedValue // 保持 [{uuid: "..."}, {uuid: "..."}] 格式
1029 | }
1030 | });
1031 | } else if (propertyType === 'colorArray' && Array.isArray(processedValue)) {
1032 | // 特殊处理颜色数组
1033 | const colorArrayValue = processedValue.map((item: any) => {
1034 | if (item && typeof item === 'object' && 'r' in item) {
1035 | return {
1036 | r: Math.min(255, Math.max(0, Number(item.r) || 0)),
1037 | g: Math.min(255, Math.max(0, Number(item.g) || 0)),
1038 | b: Math.min(255, Math.max(0, Number(item.b) || 0)),
1039 | a: item.a !== undefined ? Math.min(255, Math.max(0, Number(item.a))) : 255
1040 | };
1041 | } else {
1042 | return { r: 255, g: 255, b: 255, a: 255 };
1043 | }
1044 | });
1045 |
1046 | await Editor.Message.request('scene', 'set-property', {
1047 | uuid: nodeUuid,
1048 | path: propertyPath,
1049 | dump: {
1050 | value: colorArrayValue,
1051 | type: 'cc.Color'
1052 | }
1053 | });
1054 | } else {
1055 | // Normal property setting for non-asset properties
1056 | await Editor.Message.request('scene', 'set-property', {
1057 | uuid: nodeUuid,
1058 | path: propertyPath,
1059 | dump: { value: processedValue }
1060 | });
1061 | }
1062 |
1063 | // Step 5: 等待Editor完成更新,然后验证设置结果
1064 | await new Promise(resolve => setTimeout(resolve, 200)); // 等待200ms让Editor完成更新
1065 |
1066 | const verification = await this.verifyPropertyChange(nodeUuid, componentType, property, originalValue, actualExpectedValue);
1067 |
1068 | resolve({
1069 | success: true,
1070 | message: `Successfully set ${componentType}.${property}`,
1071 | data: {
1072 | nodeUuid,
1073 | componentType,
1074 | property,
1075 | actualValue: verification.actualValue,
1076 | changeVerified: verification.verified
1077 | }
1078 | });
1079 |
1080 | } catch (error: any) {
1081 | console.error(`[ComponentTools] Error setting property:`, error);
1082 | resolve({
1083 | success: false,
1084 | error: `Failed to set property: ${error.message}`
1085 | });
1086 | }
1087 | });
1088 | }
1089 |
1090 |
1091 | private async attachScript(nodeUuid: string, scriptPath: string): Promise<ToolResponse> {
1092 | return new Promise(async (resolve) => {
1093 | // 从脚本路径提取组件类名
1094 | const scriptName = scriptPath.split('/').pop()?.replace('.ts', '').replace('.js', '');
1095 | if (!scriptName) {
1096 | resolve({ success: false, error: 'Invalid script path' });
1097 | return;
1098 | }
1099 | // 先查找节点上是否已存在该脚本组件
1100 | const allComponentsInfo = await this.getComponents(nodeUuid);
1101 | if (allComponentsInfo.success && allComponentsInfo.data?.components) {
1102 | const existingScript = allComponentsInfo.data.components.find((comp: any) => comp.type === scriptName);
1103 | if (existingScript) {
1104 | resolve({
1105 | success: true,
1106 | message: `Script '${scriptName}' already exists on node`,
1107 | data: {
1108 | nodeUuid: nodeUuid,
1109 | componentName: scriptName,
1110 | existing: true
1111 | }
1112 | });
1113 | return;
1114 | }
1115 | }
1116 | // 首先尝试直接使用脚本名称作为组件类型
1117 | Editor.Message.request('scene', 'create-component', {
1118 | uuid: nodeUuid,
1119 | component: scriptName // 使用脚本名称而非UUID
1120 | }).then(async (result: any) => {
1121 | // 等待一段时间让Editor完成组件添加
1122 | await new Promise(resolve => setTimeout(resolve, 100));
1123 | // 重新查询节点信息验证脚本是否真的添加成功
1124 | const allComponentsInfo2 = await this.getComponents(nodeUuid);
1125 | if (allComponentsInfo2.success && allComponentsInfo2.data?.components) {
1126 | const addedScript = allComponentsInfo2.data.components.find((comp: any) => comp.type === scriptName);
1127 | if (addedScript) {
1128 | resolve({
1129 | success: true,
1130 | message: `Script '${scriptName}' attached successfully`,
1131 | data: {
1132 | nodeUuid: nodeUuid,
1133 | componentName: scriptName,
1134 | existing: false
1135 | }
1136 | });
1137 | } else {
1138 | resolve({
1139 | success: false,
1140 | error: `Script '${scriptName}' was not found on node after addition. Available components: ${allComponentsInfo2.data.components.map((c: any) => c.type).join(', ')}`
1141 | });
1142 | }
1143 | } else {
1144 | resolve({
1145 | success: false,
1146 | error: `Failed to verify script addition: ${allComponentsInfo2.error || 'Unable to get node components'}`
1147 | });
1148 | }
1149 | }).catch((err: Error) => {
1150 | // 备用方案:使用场景脚本
1151 | const options = {
1152 | name: 'cocos-mcp-server',
1153 | method: 'attachScript',
1154 | args: [nodeUuid, scriptPath]
1155 | };
1156 | Editor.Message.request('scene', 'execute-scene-script', options).then((result: any) => {
1157 | resolve(result);
1158 | }).catch(() => {
1159 | resolve({
1160 | success: false,
1161 | error: `Failed to attach script '${scriptName}': ${err.message}`,
1162 | 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.'
1163 | });
1164 | });
1165 | });
1166 | });
1167 | }
1168 |
1169 | private async getAvailableComponents(category: string = 'all'): Promise<ToolResponse> {
1170 | const componentCategories: Record<string, string[]> = {
1171 | renderer: ['cc.Sprite', 'cc.Label', 'cc.RichText', 'cc.Mask', 'cc.Graphics'],
1172 | ui: ['cc.Button', 'cc.Toggle', 'cc.Slider', 'cc.ScrollView', 'cc.EditBox', 'cc.ProgressBar'],
1173 | physics: ['cc.RigidBody2D', 'cc.BoxCollider2D', 'cc.CircleCollider2D', 'cc.PolygonCollider2D'],
1174 | animation: ['cc.Animation', 'cc.AnimationClip', 'cc.SkeletalAnimation'],
1175 | audio: ['cc.AudioSource'],
1176 | layout: ['cc.Layout', 'cc.Widget', 'cc.PageView', 'cc.PageViewIndicator'],
1177 | effects: ['cc.MotionStreak', 'cc.ParticleSystem2D'],
1178 | camera: ['cc.Camera'],
1179 | light: ['cc.Light', 'cc.DirectionalLight', 'cc.PointLight', 'cc.SpotLight']
1180 | };
1181 |
1182 | let components: string[] = [];
1183 |
1184 | if (category === 'all') {
1185 | for (const cat in componentCategories) {
1186 | components = components.concat(componentCategories[cat]);
1187 | }
1188 | } else if (componentCategories[category]) {
1189 | components = componentCategories[category];
1190 | }
1191 |
1192 | return {
1193 | success: true,
1194 | data: {
1195 | category: category,
1196 | components: components
1197 | }
1198 | };
1199 | }
1200 |
1201 | private isValidPropertyDescriptor(propData: any): boolean {
1202 | // 检查是否是有效的属性描述对象
1203 | if (typeof propData !== 'object' || propData === null) {
1204 | return false;
1205 | }
1206 |
1207 | try {
1208 | const keys = Object.keys(propData);
1209 |
1210 | // 避免遍历简单的数值对象(如 {width: 200, height: 150})
1211 | const isSimpleValueObject = keys.every(key => {
1212 | const value = propData[key];
1213 | return typeof value === 'number' || typeof value === 'string' || typeof value === 'boolean';
1214 | });
1215 |
1216 | if (isSimpleValueObject) {
1217 | return false;
1218 | }
1219 |
1220 | // 检查是否包含属性描述符的特征字段,不使用'in'操作符
1221 | const hasName = keys.includes('name');
1222 | const hasValue = keys.includes('value');
1223 | const hasType = keys.includes('type');
1224 | const hasDisplayName = keys.includes('displayName');
1225 | const hasReadonly = keys.includes('readonly');
1226 |
1227 | // 必须包含name或value字段,且通常还有type字段
1228 | const hasValidStructure = (hasName || hasValue) && (hasType || hasDisplayName || hasReadonly);
1229 |
1230 | // 额外检查:如果有default字段且结构复杂,避免深度遍历
1231 | if (keys.includes('default') && propData.default && typeof propData.default === 'object') {
1232 | const defaultKeys = Object.keys(propData.default);
1233 | if (defaultKeys.includes('value') && typeof propData.default.value === 'object') {
1234 | // 这种情况下,我们只返回顶层属性,不深入遍历default.value
1235 | return hasValidStructure;
1236 | }
1237 | }
1238 |
1239 | return hasValidStructure;
1240 | } catch (error) {
1241 | console.warn(`[isValidPropertyDescriptor] Error checking property descriptor:`, error);
1242 | return false;
1243 | }
1244 | }
1245 |
1246 | private analyzeProperty(component: any, propertyName: string): { exists: boolean; type: string; availableProperties: string[]; originalValue: any } {
1247 | // 从复杂的组件结构中提取可用属性
1248 | const availableProperties: string[] = [];
1249 | let propertyValue: any = undefined;
1250 | let propertyExists = false;
1251 |
1252 | // 尝试多种方式查找属性:
1253 | // 1. 直接属性访问
1254 | if (Object.prototype.hasOwnProperty.call(component, propertyName)) {
1255 | propertyValue = component[propertyName];
1256 | propertyExists = true;
1257 | }
1258 |
1259 | // 2. 从嵌套结构中查找 (如从测试数据看到的复杂结构)
1260 | if (!propertyExists && component.properties && typeof component.properties === 'object') {
1261 | // 首先检查properties.value是否存在(这是我们在getComponents中看到的结构)
1262 | if (component.properties.value && typeof component.properties.value === 'object') {
1263 | const valueObj = component.properties.value;
1264 | for (const [key, propData] of Object.entries(valueObj)) {
1265 | // 检查propData是否是一个有效的属性描述对象
1266 | // 确保propData是对象且包含预期的属性结构
1267 | if (this.isValidPropertyDescriptor(propData)) {
1268 | const propInfo = propData as any;
1269 | availableProperties.push(key);
1270 | if (key === propertyName) {
1271 | // 优先使用value属性,如果没有则使用propData本身
1272 | try {
1273 | const propKeys = Object.keys(propInfo);
1274 | propertyValue = propKeys.includes('value') ? propInfo.value : propInfo;
1275 | } catch (error) {
1276 | // 如果检查失败,直接使用propInfo
1277 | propertyValue = propInfo;
1278 | }
1279 | propertyExists = true;
1280 | }
1281 | }
1282 | }
1283 | } else {
1284 | // 备用方案:直接从properties查找
1285 | for (const [key, propData] of Object.entries(component.properties)) {
1286 | if (this.isValidPropertyDescriptor(propData)) {
1287 | const propInfo = propData as any;
1288 | availableProperties.push(key);
1289 | if (key === propertyName) {
1290 | // 优先使用value属性,如果没有则使用propData本身
1291 | try {
1292 | const propKeys = Object.keys(propInfo);
1293 | propertyValue = propKeys.includes('value') ? propInfo.value : propInfo;
1294 | } catch (error) {
1295 | // 如果检查失败,直接使用propInfo
1296 | propertyValue = propInfo;
1297 | }
1298 | propertyExists = true;
1299 | }
1300 | }
1301 | }
1302 | }
1303 | }
1304 |
1305 | // 3. 从直接属性中提取简单属性名
1306 | if (availableProperties.length === 0) {
1307 | for (const key of Object.keys(component)) {
1308 | if (!key.startsWith('_') && !['__type__', 'cid', 'node', 'uuid', 'name', 'enabled', 'type', 'readonly', 'visible'].includes(key)) {
1309 | availableProperties.push(key);
1310 | }
1311 | }
1312 | }
1313 |
1314 | if (!propertyExists) {
1315 | return {
1316 | exists: false,
1317 | type: 'unknown',
1318 | availableProperties,
1319 | originalValue: undefined
1320 | };
1321 | }
1322 |
1323 | let type = 'unknown';
1324 |
1325 | // 智能类型检测
1326 | if (Array.isArray(propertyValue)) {
1327 | // 数组类型检测
1328 | if (propertyName.toLowerCase().includes('node')) {
1329 | type = 'nodeArray';
1330 | } else if (propertyName.toLowerCase().includes('color')) {
1331 | type = 'colorArray';
1332 | } else {
1333 | type = 'array';
1334 | }
1335 | } else if (typeof propertyValue === 'string') {
1336 | // Check if property name suggests it's an asset
1337 | if (['spriteFrame', 'texture', 'material', 'font', 'clip', 'prefab'].includes(propertyName.toLowerCase())) {
1338 | type = 'asset';
1339 | } else {
1340 | type = 'string';
1341 | }
1342 | } else if (typeof propertyValue === 'number') {
1343 | type = 'number';
1344 | } else if (typeof propertyValue === 'boolean') {
1345 | type = 'boolean';
1346 | } else if (propertyValue && typeof propertyValue === 'object') {
1347 | try {
1348 | const keys = Object.keys(propertyValue);
1349 | if (keys.includes('r') && keys.includes('g') && keys.includes('b')) {
1350 | type = 'color';
1351 | } else if (keys.includes('x') && keys.includes('y')) {
1352 | type = propertyValue.z !== undefined ? 'vec3' : 'vec2';
1353 | } else if (keys.includes('width') && keys.includes('height')) {
1354 | type = 'size';
1355 | } else if (keys.includes('uuid') || keys.includes('__uuid__')) {
1356 | // 检查是否是节点引用(通过属性名或__id__属性判断)
1357 | if (propertyName.toLowerCase().includes('node') ||
1358 | propertyName.toLowerCase().includes('target') ||
1359 | keys.includes('__id__')) {
1360 | type = 'node';
1361 | } else {
1362 | type = 'asset';
1363 | }
1364 | } else if (keys.includes('__id__')) {
1365 | // 节点引用特征
1366 | type = 'node';
1367 | } else {
1368 | type = 'object';
1369 | }
1370 | } catch (error) {
1371 | console.warn(`[analyzeProperty] Error checking property type for: ${JSON.stringify(propertyValue)}`);
1372 | type = 'object';
1373 | }
1374 | } else if (propertyValue === null || propertyValue === undefined) {
1375 | // For null/undefined values, check property name to determine type
1376 | if (['spriteFrame', 'texture', 'material', 'font', 'clip', 'prefab'].includes(propertyName.toLowerCase())) {
1377 | type = 'asset';
1378 | } else if (propertyName.toLowerCase().includes('node') ||
1379 | propertyName.toLowerCase().includes('target')) {
1380 | type = 'node';
1381 | } else if (propertyName.toLowerCase().includes('component')) {
1382 | type = 'component';
1383 | } else {
1384 | type = 'unknown';
1385 | }
1386 | }
1387 |
1388 | return {
1389 | exists: true,
1390 | type,
1391 | availableProperties,
1392 | originalValue: propertyValue
1393 | };
1394 | }
1395 |
1396 | private smartConvertValue(inputValue: any, propertyInfo: any): any {
1397 | const { type, originalValue } = propertyInfo;
1398 |
1399 | console.log(`[smartConvertValue] Converting ${JSON.stringify(inputValue)} to type: ${type}`);
1400 |
1401 | switch (type) {
1402 | case 'string':
1403 | return String(inputValue);
1404 |
1405 | case 'number':
1406 | return Number(inputValue);
1407 |
1408 | case 'boolean':
1409 | if (typeof inputValue === 'boolean') return inputValue;
1410 | if (typeof inputValue === 'string') {
1411 | return inputValue.toLowerCase() === 'true' || inputValue === '1';
1412 | }
1413 | return Boolean(inputValue);
1414 |
1415 | case 'color':
1416 | // 优化的颜色处理,支持多种输入格式
1417 | if (typeof inputValue === 'string') {
1418 | // 字符串格式:十六进制、颜色名称、rgb()/rgba()
1419 | return this.parseColorString(inputValue);
1420 | } else if (typeof inputValue === 'object' && inputValue !== null) {
1421 | try {
1422 | const inputKeys = Object.keys(inputValue);
1423 | // 如果输入是颜色对象,验证并转换
1424 | if (inputKeys.includes('r') || inputKeys.includes('g') || inputKeys.includes('b')) {
1425 | return {
1426 | r: Math.min(255, Math.max(0, Number(inputValue.r) || 0)),
1427 | g: Math.min(255, Math.max(0, Number(inputValue.g) || 0)),
1428 | b: Math.min(255, Math.max(0, Number(inputValue.b) || 0)),
1429 | a: inputValue.a !== undefined ? Math.min(255, Math.max(0, Number(inputValue.a))) : 255
1430 | };
1431 | }
1432 | } catch (error) {
1433 | console.warn(`[smartConvertValue] Invalid color object: ${JSON.stringify(inputValue)}`);
1434 | }
1435 | }
1436 | // 如果有原值,保持原值结构并更新提供的值
1437 | if (originalValue && typeof originalValue === 'object') {
1438 | try {
1439 | const inputKeys = typeof inputValue === 'object' && inputValue ? Object.keys(inputValue) : [];
1440 | return {
1441 | r: inputKeys.includes('r') ? Math.min(255, Math.max(0, Number(inputValue.r))) : (originalValue.r || 255),
1442 | g: inputKeys.includes('g') ? Math.min(255, Math.max(0, Number(inputValue.g))) : (originalValue.g || 255),
1443 | b: inputKeys.includes('b') ? Math.min(255, Math.max(0, Number(inputValue.b))) : (originalValue.b || 255),
1444 | a: inputKeys.includes('a') ? Math.min(255, Math.max(0, Number(inputValue.a))) : (originalValue.a || 255)
1445 | };
1446 | } catch (error) {
1447 | console.warn(`[smartConvertValue] Error processing color with original value: ${error}`);
1448 | }
1449 | }
1450 | // 默认返回白色
1451 | console.warn(`[smartConvertValue] Using default white color for invalid input: ${JSON.stringify(inputValue)}`);
1452 | return { r: 255, g: 255, b: 255, a: 255 };
1453 |
1454 | case 'vec2':
1455 | if (typeof inputValue === 'object' && inputValue !== null) {
1456 | return {
1457 | x: Number(inputValue.x) || originalValue.x || 0,
1458 | y: Number(inputValue.y) || originalValue.y || 0
1459 | };
1460 | }
1461 | return originalValue;
1462 |
1463 | case 'vec3':
1464 | if (typeof inputValue === 'object' && inputValue !== null) {
1465 | return {
1466 | x: Number(inputValue.x) || originalValue.x || 0,
1467 | y: Number(inputValue.y) || originalValue.y || 0,
1468 | z: Number(inputValue.z) || originalValue.z || 0
1469 | };
1470 | }
1471 | return originalValue;
1472 |
1473 | case 'size':
1474 | if (typeof inputValue === 'object' && inputValue !== null) {
1475 | return {
1476 | width: Number(inputValue.width) || originalValue.width || 100,
1477 | height: Number(inputValue.height) || originalValue.height || 100
1478 | };
1479 | }
1480 | return originalValue;
1481 |
1482 | case 'node':
1483 | if (typeof inputValue === 'string') {
1484 | // 节点引用需要特殊处理
1485 | return inputValue;
1486 | } else if (typeof inputValue === 'object' && inputValue !== null) {
1487 | // 如果已经是对象形式,返回UUID或完整对象
1488 | return inputValue.uuid || inputValue;
1489 | }
1490 | return originalValue;
1491 |
1492 | case 'asset':
1493 | if (typeof inputValue === 'string') {
1494 | // 如果输入是字符串路径,转换为asset对象
1495 | return { uuid: inputValue };
1496 | } else if (typeof inputValue === 'object' && inputValue !== null) {
1497 | return inputValue;
1498 | }
1499 | return originalValue;
1500 |
1501 | default:
1502 | // 对于未知类型,尽量保持原有结构
1503 | if (typeof inputValue === typeof originalValue) {
1504 | return inputValue;
1505 | }
1506 | return originalValue;
1507 | }
1508 | }
1509 |
1510 | private parseColorString(colorStr: string): { r: number; g: number; b: number; a: number } {
1511 | const str = colorStr.trim();
1512 |
1513 | // 只支持十六进制格式 #RRGGBB 或 #RRGGBBAA
1514 | if (str.startsWith('#')) {
1515 | if (str.length === 7) { // #RRGGBB
1516 | const r = parseInt(str.substring(1, 3), 16);
1517 | const g = parseInt(str.substring(3, 5), 16);
1518 | const b = parseInt(str.substring(5, 7), 16);
1519 | return { r, g, b, a: 255 };
1520 | } else if (str.length === 9) { // #RRGGBBAA
1521 | const r = parseInt(str.substring(1, 3), 16);
1522 | const g = parseInt(str.substring(3, 5), 16);
1523 | const b = parseInt(str.substring(5, 7), 16);
1524 | const a = parseInt(str.substring(7, 9), 16);
1525 | return { r, g, b, a };
1526 | }
1527 | }
1528 |
1529 | // 如果不是有效的十六进制格式,返回错误提示
1530 | throw new Error(`Invalid color format: "${colorStr}". Only hexadecimal format is supported (e.g., "#FF0000" or "#FF0000FF")`);
1531 | }
1532 |
1533 | private async verifyPropertyChange(nodeUuid: string, componentType: string, property: string, originalValue: any, expectedValue: any): Promise<{ verified: boolean; actualValue: any; fullData: any }> {
1534 | console.log(`[verifyPropertyChange] Starting verification for ${componentType}.${property}`);
1535 | console.log(`[verifyPropertyChange] Expected value:`, JSON.stringify(expectedValue));
1536 | console.log(`[verifyPropertyChange] Original value:`, JSON.stringify(originalValue));
1537 |
1538 | try {
1539 | // 重新获取组件信息进行验证
1540 | console.log(`[verifyPropertyChange] Calling getComponentInfo...`);
1541 | const componentInfo = await this.getComponentInfo(nodeUuid, componentType);
1542 | console.log(`[verifyPropertyChange] getComponentInfo success:`, componentInfo.success);
1543 |
1544 | const allComponents = await this.getComponents(nodeUuid);
1545 | console.log(`[verifyPropertyChange] getComponents success:`, allComponents.success);
1546 |
1547 | if (componentInfo.success && componentInfo.data) {
1548 | console.log(`[verifyPropertyChange] Component data available, extracting property '${property}'`);
1549 | const allPropertyNames = Object.keys(componentInfo.data.properties || {});
1550 | console.log(`[verifyPropertyChange] Available properties:`, allPropertyNames);
1551 | const propertyData = componentInfo.data.properties?.[property];
1552 | console.log(`[verifyPropertyChange] Raw property data for '${property}':`, JSON.stringify(propertyData));
1553 |
1554 | // 从属性数据中提取实际值
1555 | let actualValue = propertyData;
1556 | console.log(`[verifyPropertyChange] Initial actualValue:`, JSON.stringify(actualValue));
1557 |
1558 | if (propertyData && typeof propertyData === 'object' && 'value' in propertyData) {
1559 | actualValue = propertyData.value;
1560 | console.log(`[verifyPropertyChange] Extracted actualValue from .value:`, JSON.stringify(actualValue));
1561 | } else {
1562 | console.log(`[verifyPropertyChange] No .value property found, using raw data`);
1563 | }
1564 |
1565 | // 修复验证逻辑:检查实际值是否匹配期望值
1566 | let verified = false;
1567 |
1568 | if (typeof expectedValue === 'object' && expectedValue !== null && 'uuid' in expectedValue) {
1569 | // 对于引用类型(节点/组件/资源),比较UUID
1570 | const actualUuid = actualValue && typeof actualValue === 'object' && 'uuid' in actualValue ? actualValue.uuid : '';
1571 | const expectedUuid = expectedValue.uuid || '';
1572 | verified = actualUuid === expectedUuid && expectedUuid !== '';
1573 |
1574 | console.log(`[verifyPropertyChange] Reference comparison:`);
1575 | console.log(` - Expected UUID: "${expectedUuid}"`);
1576 | console.log(` - Actual UUID: "${actualUuid}"`);
1577 | console.log(` - UUID match: ${actualUuid === expectedUuid}`);
1578 | console.log(` - UUID not empty: ${expectedUuid !== ''}`);
1579 | console.log(` - Final verified: ${verified}`);
1580 | } else {
1581 | // 对于其他类型,直接比较值
1582 | console.log(`[verifyPropertyChange] Value comparison:`);
1583 | console.log(` - Expected type: ${typeof expectedValue}`);
1584 | console.log(` - Actual type: ${typeof actualValue}`);
1585 |
1586 | if (typeof actualValue === typeof expectedValue) {
1587 | if (typeof actualValue === 'object' && actualValue !== null && expectedValue !== null) {
1588 | // 对象类型的深度比较
1589 | verified = JSON.stringify(actualValue) === JSON.stringify(expectedValue);
1590 | console.log(` - Object comparison (JSON): ${verified}`);
1591 | } else {
1592 | // 基本类型的直接比较
1593 | verified = actualValue === expectedValue;
1594 | console.log(` - Direct comparison: ${verified}`);
1595 | }
1596 | } else {
1597 | // 类型不匹配时的特殊处理(如数字和字符串)
1598 | const stringMatch = String(actualValue) === String(expectedValue);
1599 | const numberMatch = Number(actualValue) === Number(expectedValue);
1600 | verified = stringMatch || numberMatch;
1601 | console.log(` - String match: ${stringMatch}`);
1602 | console.log(` - Number match: ${numberMatch}`);
1603 | console.log(` - Type mismatch verified: ${verified}`);
1604 | }
1605 | }
1606 |
1607 | console.log(`[verifyPropertyChange] Final verification result: ${verified}`);
1608 | console.log(`[verifyPropertyChange] Final actualValue:`, JSON.stringify(actualValue));
1609 |
1610 | const result = {
1611 | verified,
1612 | actualValue,
1613 | fullData: {
1614 | // 只返回修改的属性信息,不返回完整组件数据
1615 | modifiedProperty: {
1616 | name: property,
1617 | before: originalValue,
1618 | expected: expectedValue,
1619 | actual: actualValue,
1620 | verified,
1621 | propertyMetadata: propertyData // 只包含这个属性的元数据
1622 | },
1623 | // 简化的组件信息
1624 | componentSummary: {
1625 | nodeUuid,
1626 | componentType,
1627 | totalProperties: Object.keys(componentInfo.data?.properties || {}).length
1628 | }
1629 | }
1630 | };
1631 |
1632 | console.log(`[verifyPropertyChange] Returning result:`, JSON.stringify(result, null, 2));
1633 | return result;
1634 | } else {
1635 | console.log(`[verifyPropertyChange] ComponentInfo failed or no data:`, componentInfo);
1636 | }
1637 | } catch (error) {
1638 | console.error('[verifyPropertyChange] Verification failed with error:', error);
1639 | console.error('[verifyPropertyChange] Error stack:', error instanceof Error ? error.stack : 'No stack trace');
1640 | }
1641 |
1642 | console.log(`[verifyPropertyChange] Returning fallback result`);
1643 | return {
1644 | verified: false,
1645 | actualValue: undefined,
1646 | fullData: null
1647 | };
1648 | }
1649 |
1650 | /**
1651 | * 检测是否为节点属性,如果是则重定向到对应的节点方法
1652 | */
1653 | private async checkAndRedirectNodeProperties(args: any): Promise<ToolResponse | null> {
1654 | const { nodeUuid, componentType, property, propertyType, value } = args;
1655 |
1656 | // 检测是否为节点基础属性(应该使用 set_node_property)
1657 | const nodeBasicProperties = [
1658 | 'name', 'active', 'layer', 'mobility', 'parent', 'children', 'hideFlags'
1659 | ];
1660 |
1661 | // 检测是否为节点变换属性(应该使用 set_node_transform)
1662 | const nodeTransformProperties = [
1663 | 'position', 'rotation', 'scale', 'eulerAngles', 'angle'
1664 | ];
1665 |
1666 | // Detect attempts to set cc.Node properties (common mistake)
1667 | if (componentType === 'cc.Node' || componentType === 'Node') {
1668 | if (nodeBasicProperties.includes(property)) {
1669 | return {
1670 | success: false,
1671 | error: `Property '${property}' is a node basic property, not a component property`,
1672 | instruction: `Please use set_node_property method to set node properties: set_node_property(uuid="${nodeUuid}", property="${property}", value=${JSON.stringify(value)})`
1673 | };
1674 | } else if (nodeTransformProperties.includes(property)) {
1675 | return {
1676 | success: false,
1677 | error: `Property '${property}' is a node transform property, not a component property`,
1678 | instruction: `Please use set_node_transform method to set transform properties: set_node_transform(uuid="${nodeUuid}", ${property}=${JSON.stringify(value)})`
1679 | };
1680 | }
1681 | }
1682 |
1683 | // Detect common incorrect usage
1684 | if (nodeBasicProperties.includes(property) || nodeTransformProperties.includes(property)) {
1685 | const methodName = nodeTransformProperties.includes(property) ? 'set_node_transform' : 'set_node_property';
1686 | return {
1687 | success: false,
1688 | error: `Property '${property}' is a node property, not a component property`,
1689 | 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)})`
1690 | };
1691 | }
1692 |
1693 | return null; // 不是节点属性,继续正常处理
1694 | }
1695 |
1696 | /**
1697 | * 生成组件建议信息
1698 | */
1699 | private generateComponentSuggestion(requestedType: string, availableTypes: string[], property: string): string {
1700 | // 检查是否存在相似的组件类型
1701 | const similarTypes = availableTypes.filter(type =>
1702 | type.toLowerCase().includes(requestedType.toLowerCase()) ||
1703 | requestedType.toLowerCase().includes(type.toLowerCase())
1704 | );
1705 |
1706 | let instruction = '';
1707 |
1708 | if (similarTypes.length > 0) {
1709 | instruction += `\n\n🔍 Found similar components: ${similarTypes.join(', ')}`;
1710 | instruction += `\n💡 Suggestion: Perhaps you meant to set the '${similarTypes[0]}' component?`;
1711 | }
1712 |
1713 | // Recommend possible components based on property name
1714 | const propertyToComponentMap: Record<string, string[]> = {
1715 | 'string': ['cc.Label', 'cc.RichText', 'cc.EditBox'],
1716 | 'text': ['cc.Label', 'cc.RichText'],
1717 | 'fontSize': ['cc.Label', 'cc.RichText'],
1718 | 'spriteFrame': ['cc.Sprite'],
1719 | 'color': ['cc.Label', 'cc.Sprite', 'cc.Graphics'],
1720 | 'normalColor': ['cc.Button'],
1721 | 'pressedColor': ['cc.Button'],
1722 | 'target': ['cc.Button'],
1723 | 'contentSize': ['cc.UITransform'],
1724 | 'anchorPoint': ['cc.UITransform']
1725 | };
1726 |
1727 | const recommendedComponents = propertyToComponentMap[property] || [];
1728 | const availableRecommended = recommendedComponents.filter(comp => availableTypes.includes(comp));
1729 |
1730 | if (availableRecommended.length > 0) {
1731 | instruction += `\n\n🎯 Based on property '${property}', recommended components: ${availableRecommended.join(', ')}`;
1732 | }
1733 |
1734 | // Provide operation suggestions
1735 | instruction += `\n\n📋 Suggested Actions:`;
1736 | instruction += `\n1. Use get_components(nodeUuid="${requestedType.includes('uuid') ? 'YOUR_NODE_UUID' : 'nodeUuid'}") to view all components on the node`;
1737 | instruction += `\n2. If you need to add a component, use add_component(nodeUuid="...", componentType="${requestedType}")`;
1738 | instruction += `\n3. Verify that the component type name is correct (case-sensitive)`;
1739 |
1740 | return instruction;
1741 | }
1742 |
1743 | /**
1744 | * 快速验证资源设置结果
1745 | */
1746 | private async quickVerifyAsset(nodeUuid: string, componentType: string, property: string): Promise<any> {
1747 | try {
1748 | const rawNodeData = await Editor.Message.request('scene', 'query-node', nodeUuid);
1749 | if (!rawNodeData || !rawNodeData.__comps__) {
1750 | return null;
1751 | }
1752 |
1753 | // 找到组件
1754 | const component = rawNodeData.__comps__.find((comp: any) => {
1755 | const compType = comp.__type__ || comp.cid || comp.type;
1756 | return compType === componentType;
1757 | });
1758 |
1759 | if (!component) {
1760 | return null;
1761 | }
1762 |
1763 | // 提取属性值
1764 | const properties = this.extractComponentProperties(component);
1765 | const propertyData = properties[property];
1766 |
1767 | if (propertyData && typeof propertyData === 'object' && 'value' in propertyData) {
1768 | return propertyData.value;
1769 | } else {
1770 | return propertyData;
1771 | }
1772 | } catch (error) {
1773 | console.error(`[quickVerifyAsset] Error:`, error);
1774 | return null;
1775 | }
1776 | }
1777 | }
```