优化了组件属性的设置的接口,使AI调用起来更简单,更准确。每次更新场景信息、组件信息或者节点信息,端口将会返回最新的信息供AI进行追踪和比对。
This commit is contained in:
155
dist/mcp-server.js
vendored
155
dist/mcp-server.js
vendored
File diff suppressed because one or more lines are too long
481
dist/tools/component-tools.js
vendored
481
dist/tools/component-tools.js
vendored
File diff suppressed because one or more lines are too long
420
dist/tools/node-tools.js
vendored
420
dist/tools/node-tools.js
vendored
File diff suppressed because one or more lines are too long
182
dist/tools/project-tools.js
vendored
182
dist/tools/project-tools.js
vendored
File diff suppressed because one or more lines are too long
35
dist/tools/scene-tools.js
vendored
35
dist/tools/scene-tools.js
vendored
File diff suppressed because one or more lines are too long
250
dist/tools/validation-tools.js
vendored
Normal file
250
dist/tools/validation-tools.js
vendored
Normal file
File diff suppressed because one or more lines are too long
2
dist/types/index.js
vendored
2
dist/types/index.js
vendored
@@ -1,3 +1,3 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zb3VyY2UvdHlwZXMvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IiIsInNvdXJjZXNDb250ZW50IjpbImV4cG9ydCBpbnRlcmZhY2UgTUNQU2VydmVyU2V0dGluZ3Mge1xuICAgIHBvcnQ6IG51bWJlcjtcbiAgICBhdXRvU3RhcnQ6IGJvb2xlYW47XG4gICAgZW5hYmxlRGVidWdMb2c6IGJvb2xlYW47XG4gICAgYWxsb3dlZE9yaWdpbnM6IHN0cmluZ1tdO1xuICAgIG1heENvbm5lY3Rpb25zOiBudW1iZXI7XG59XG5cbmV4cG9ydCBpbnRlcmZhY2UgU2VydmVyU3RhdHVzIHtcbiAgICBydW5uaW5nOiBib29sZWFuO1xuICAgIHBvcnQ6IG51bWJlcjtcbiAgICBjbGllbnRzOiBudW1iZXI7XG59XG5cbmV4cG9ydCBpbnRlcmZhY2UgVG9vbERlZmluaXRpb24ge1xuICAgIG5hbWU6IHN0cmluZztcbiAgICBkZXNjcmlwdGlvbjogc3RyaW5nO1xuICAgIGlucHV0U2NoZW1hOiBhbnk7XG59XG5cbmV4cG9ydCBpbnRlcmZhY2UgVG9vbFJlc3BvbnNlIHtcbiAgICBzdWNjZXNzOiBib29sZWFuO1xuICAgIGRhdGE/OiBhbnk7XG4gICAgbWVzc2FnZT86IHN0cmluZztcbiAgICBlcnJvcj86IHN0cmluZztcbiAgICBpbnN0cnVjdGlvbj86IHN0cmluZztcbn1cblxuZXhwb3J0IGludGVyZmFjZSBOb2RlSW5mbyB7XG4gICAgdXVpZDogc3RyaW5nO1xuICAgIG5hbWU6IHN0cmluZztcbiAgICBhY3RpdmU6IGJvb2xlYW47XG4gICAgcG9zaXRpb24/OiB7IHg6IG51bWJlcjsgeTogbnVtYmVyOyB6OiBudW1iZXIgfTtcbiAgICByb3RhdGlvbj86IHsgeDogbnVtYmVyOyB5OiBudW1iZXI7IHo6IG51bWJlciB9O1xuICAgIHNjYWxlPzogeyB4OiBudW1iZXI7IHk6IG51bWJlcjsgejogbnVtYmVyIH07XG4gICAgcGFyZW50Pzogc3RyaW5nO1xuICAgIGNoaWxkcmVuPzogc3RyaW5nW107XG4gICAgY29tcG9uZW50cz86IENvbXBvbmVudEluZm9bXTtcbiAgICBsYXllcj86IG51bWJlcjtcbiAgICBtb2JpbGl0eT86IG51bWJlcjtcbn1cblxuZXhwb3J0IGludGVyZmFjZSBDb21wb25lbnRJbmZvIHtcbiAgICB0eXBlOiBzdHJpbmc7XG4gICAgZW5hYmxlZDogYm9vbGVhbjtcbiAgICBwcm9wZXJ0aWVzPzogUmVjb3JkPHN0cmluZywgYW55Pjtcbn1cblxuZXhwb3J0IGludGVyZmFjZSBTY2VuZUluZm8ge1xuICAgIG5hbWU6IHN0cmluZztcbiAgICB1dWlkOiBzdHJpbmc7XG4gICAgcGF0aDogc3RyaW5nO1xufVxuXG5leHBvcnQgaW50ZXJmYWNlIFByZWZhYkluZm8ge1xuICAgIG5hbWU6IHN0cmluZztcbiAgICB1dWlkOiBzdHJpbmc7XG4gICAgcGF0aDogc3RyaW5nO1xuICAgIGZvbGRlcjogc3RyaW5nO1xuICAgIGNyZWF0ZVRpbWU/OiBzdHJpbmc7XG4gICAgbW9kaWZ5VGltZT86IHN0cmluZztcbiAgICBkZXBlbmRlbmNpZXM/OiBzdHJpbmdbXTtcbn1cblxuZXhwb3J0IGludGVyZmFjZSBBc3NldEluZm8ge1xuICAgIG5hbWU6IHN0cmluZztcbiAgICB1dWlkOiBzdHJpbmc7XG4gICAgcGF0aDogc3RyaW5nO1xuICAgIHR5cGU6IHN0cmluZztcbiAgICBzaXplPzogbnVtYmVyO1xuICAgIGlzRGlyZWN0b3J5OiBib29sZWFuO1xuICAgIG1ldGE/OiB7XG4gICAgICAgIHZlcjogc3RyaW5nO1xuICAgICAgICBpbXBvcnRlcjogc3RyaW5nO1xuICAgIH07XG59XG5cbmV4cG9ydCBpbnRlcmZhY2UgUHJvamVjdEluZm8ge1xuICAgIG5hbWU6IHN0cmluZztcbiAgICBwYXRoOiBzdHJpbmc7XG4gICAgdXVpZDogc3RyaW5nO1xuICAgIHZlcnNpb246IHN0cmluZztcbiAgICBjb2Nvc1ZlcnNpb246IHN0cmluZztcbn1cblxuZXhwb3J0IGludGVyZmFjZSBDb25zb2xlTWVzc2FnZSB7XG4gICAgdGltZXN0YW1wOiBzdHJpbmc7XG4gICAgdHlwZTogJ2xvZycgfCAnd2FybicgfCAnZXJyb3InIHwgJ2luZm8nO1xuICAgIG1lc3NhZ2U6IHN0cmluZztcbiAgICBzdGFjaz86IHN0cmluZztcbn1cblxuZXhwb3J0IGludGVyZmFjZSBQZXJmb3JtYW5jZVN0YXRzIHtcbiAgICBub2RlQ291bnQ6IG51bWJlcjtcbiAgICBjb21wb25lbnRDb3VudDogbnVtYmVyO1xuICAgIGRyYXdDYWxsczogbnVtYmVyO1xuICAgIHRyaWFuZ2xlczogbnVtYmVyO1xuICAgIG1lbW9yeTogUmVjb3JkPHN0cmluZywgYW55Pjtcbn1cblxuZXhwb3J0IGludGVyZmFjZSBWYWxpZGF0aW9uSXNzdWUge1xuICAgIHR5cGU6ICdlcnJvcicgfCAnd2FybmluZycgfCAnaW5mbyc7XG4gICAgY2F0ZWdvcnk6IHN0cmluZztcbiAgICBtZXNzYWdlOiBzdHJpbmc7XG4gICAgZGV0YWlscz86IGFueTtcbiAgICBzdWdnZXN0aW9uPzogc3RyaW5nO1xufVxuXG5leHBvcnQgaW50ZXJmYWNlIFZhbGlkYXRpb25SZXN1bHQge1xuICAgIHZhbGlkOiBib29sZWFuO1xuICAgIGlzc3VlQ291bnQ6IG51bWJlcjtcbiAgICBpc3N1ZXM6IFZhbGlkYXRpb25Jc3N1ZVtdO1xufVxuXG5leHBvcnQgaW50ZXJmYWNlIE1DUENsaWVudCB7XG4gICAgaWQ6IHN0cmluZztcbiAgICBsYXN0QWN0aXZpdHk6IERhdGU7XG4gICAgdXNlckFnZW50Pzogc3RyaW5nO1xufVxuXG5leHBvcnQgaW50ZXJmYWNlIFRvb2xFeGVjdXRvciB7XG4gICAgZ2V0VG9vbHMoKTogVG9vbERlZmluaXRpb25bXTtcbiAgICBleGVjdXRlKHRvb2xOYW1lOiBzdHJpbmcsIGFyZ3M6IGFueSk6IFByb21pc2U8VG9vbFJlc3BvbnNlPjtcbn0iXX0=
|
||||
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zb3VyY2UvdHlwZXMvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IiIsInNvdXJjZXNDb250ZW50IjpbImV4cG9ydCBpbnRlcmZhY2UgTUNQU2VydmVyU2V0dGluZ3Mge1xuICAgIHBvcnQ6IG51bWJlcjtcbiAgICBhdXRvU3RhcnQ6IGJvb2xlYW47XG4gICAgZW5hYmxlRGVidWdMb2c6IGJvb2xlYW47XG4gICAgYWxsb3dlZE9yaWdpbnM6IHN0cmluZ1tdO1xuICAgIG1heENvbm5lY3Rpb25zOiBudW1iZXI7XG59XG5cbmV4cG9ydCBpbnRlcmZhY2UgU2VydmVyU3RhdHVzIHtcbiAgICBydW5uaW5nOiBib29sZWFuO1xuICAgIHBvcnQ6IG51bWJlcjtcbiAgICBjbGllbnRzOiBudW1iZXI7XG59XG5cbmV4cG9ydCBpbnRlcmZhY2UgVG9vbERlZmluaXRpb24ge1xuICAgIG5hbWU6IHN0cmluZztcbiAgICBkZXNjcmlwdGlvbjogc3RyaW5nO1xuICAgIGlucHV0U2NoZW1hOiBhbnk7XG59XG5cbmV4cG9ydCBpbnRlcmZhY2UgVG9vbFJlc3BvbnNlIHtcbiAgICBzdWNjZXNzOiBib29sZWFuO1xuICAgIGRhdGE/OiBhbnk7XG4gICAgbWVzc2FnZT86IHN0cmluZztcbiAgICBlcnJvcj86IHN0cmluZztcbiAgICBpbnN0cnVjdGlvbj86IHN0cmluZztcbiAgICB3YXJuaW5nPzogc3RyaW5nO1xuICAgIHZlcmlmaWNhdGlvbkRhdGE/OiBhbnk7XG4gICAgdXBkYXRlZFByb3BlcnRpZXM/OiBzdHJpbmdbXTtcbn1cblxuZXhwb3J0IGludGVyZmFjZSBOb2RlSW5mbyB7XG4gICAgdXVpZDogc3RyaW5nO1xuICAgIG5hbWU6IHN0cmluZztcbiAgICBhY3RpdmU6IGJvb2xlYW47XG4gICAgcG9zaXRpb24/OiB7IHg6IG51bWJlcjsgeTogbnVtYmVyOyB6OiBudW1iZXIgfTtcbiAgICByb3RhdGlvbj86IHsgeDogbnVtYmVyOyB5OiBudW1iZXI7IHo6IG51bWJlciB9O1xuICAgIHNjYWxlPzogeyB4OiBudW1iZXI7IHk6IG51bWJlcjsgejogbnVtYmVyIH07XG4gICAgcGFyZW50Pzogc3RyaW5nO1xuICAgIGNoaWxkcmVuPzogc3RyaW5nW107XG4gICAgY29tcG9uZW50cz86IENvbXBvbmVudEluZm9bXTtcbiAgICBsYXllcj86IG51bWJlcjtcbiAgICBtb2JpbGl0eT86IG51bWJlcjtcbn1cblxuZXhwb3J0IGludGVyZmFjZSBDb21wb25lbnRJbmZvIHtcbiAgICB0eXBlOiBzdHJpbmc7XG4gICAgZW5hYmxlZDogYm9vbGVhbjtcbiAgICBwcm9wZXJ0aWVzPzogUmVjb3JkPHN0cmluZywgYW55Pjtcbn1cblxuZXhwb3J0IGludGVyZmFjZSBTY2VuZUluZm8ge1xuICAgIG5hbWU6IHN0cmluZztcbiAgICB1dWlkOiBzdHJpbmc7XG4gICAgcGF0aDogc3RyaW5nO1xufVxuXG5leHBvcnQgaW50ZXJmYWNlIFByZWZhYkluZm8ge1xuICAgIG5hbWU6IHN0cmluZztcbiAgICB1dWlkOiBzdHJpbmc7XG4gICAgcGF0aDogc3RyaW5nO1xuICAgIGZvbGRlcjogc3RyaW5nO1xuICAgIGNyZWF0ZVRpbWU/OiBzdHJpbmc7XG4gICAgbW9kaWZ5VGltZT86IHN0cmluZztcbiAgICBkZXBlbmRlbmNpZXM/OiBzdHJpbmdbXTtcbn1cblxuZXhwb3J0IGludGVyZmFjZSBBc3NldEluZm8ge1xuICAgIG5hbWU6IHN0cmluZztcbiAgICB1dWlkOiBzdHJpbmc7XG4gICAgcGF0aDogc3RyaW5nO1xuICAgIHR5cGU6IHN0cmluZztcbiAgICBzaXplPzogbnVtYmVyO1xuICAgIGlzRGlyZWN0b3J5OiBib29sZWFuO1xuICAgIG1ldGE/OiB7XG4gICAgICAgIHZlcjogc3RyaW5nO1xuICAgICAgICBpbXBvcnRlcjogc3RyaW5nO1xuICAgIH07XG59XG5cbmV4cG9ydCBpbnRlcmZhY2UgUHJvamVjdEluZm8ge1xuICAgIG5hbWU6IHN0cmluZztcbiAgICBwYXRoOiBzdHJpbmc7XG4gICAgdXVpZDogc3RyaW5nO1xuICAgIHZlcnNpb246IHN0cmluZztcbiAgICBjb2Nvc1ZlcnNpb246IHN0cmluZztcbn1cblxuZXhwb3J0IGludGVyZmFjZSBDb25zb2xlTWVzc2FnZSB7XG4gICAgdGltZXN0YW1wOiBzdHJpbmc7XG4gICAgdHlwZTogJ2xvZycgfCAnd2FybicgfCAnZXJyb3InIHwgJ2luZm8nO1xuICAgIG1lc3NhZ2U6IHN0cmluZztcbiAgICBzdGFjaz86IHN0cmluZztcbn1cblxuZXhwb3J0IGludGVyZmFjZSBQZXJmb3JtYW5jZVN0YXRzIHtcbiAgICBub2RlQ291bnQ6IG51bWJlcjtcbiAgICBjb21wb25lbnRDb3VudDogbnVtYmVyO1xuICAgIGRyYXdDYWxsczogbnVtYmVyO1xuICAgIHRyaWFuZ2xlczogbnVtYmVyO1xuICAgIG1lbW9yeTogUmVjb3JkPHN0cmluZywgYW55Pjtcbn1cblxuZXhwb3J0IGludGVyZmFjZSBWYWxpZGF0aW9uSXNzdWUge1xuICAgIHR5cGU6ICdlcnJvcicgfCAnd2FybmluZycgfCAnaW5mbyc7XG4gICAgY2F0ZWdvcnk6IHN0cmluZztcbiAgICBtZXNzYWdlOiBzdHJpbmc7XG4gICAgZGV0YWlscz86IGFueTtcbiAgICBzdWdnZXN0aW9uPzogc3RyaW5nO1xufVxuXG5leHBvcnQgaW50ZXJmYWNlIFZhbGlkYXRpb25SZXN1bHQge1xuICAgIHZhbGlkOiBib29sZWFuO1xuICAgIGlzc3VlQ291bnQ6IG51bWJlcjtcbiAgICBpc3N1ZXM6IFZhbGlkYXRpb25Jc3N1ZVtdO1xufVxuXG5leHBvcnQgaW50ZXJmYWNlIE1DUENsaWVudCB7XG4gICAgaWQ6IHN0cmluZztcbiAgICBsYXN0QWN0aXZpdHk6IERhdGU7XG4gICAgdXNlckFnZW50Pzogc3RyaW5nO1xufVxuXG5leHBvcnQgaW50ZXJmYWNlIFRvb2xFeGVjdXRvciB7XG4gICAgZ2V0VG9vbHMoKTogVG9vbERlZmluaXRpb25bXTtcbiAgICBleGVjdXRlKHRvb2xOYW1lOiBzdHJpbmcsIGFyZ3M6IGFueSk6IFByb21pc2U8VG9vbFJlc3BvbnNlPjtcbn0iXX0=
|
||||
@@ -15,6 +15,7 @@ import { SceneAdvancedTools } from './tools/scene-advanced-tools';
|
||||
import { SceneViewTools } from './tools/scene-view-tools';
|
||||
import { ReferenceImageTools } from './tools/reference-image-tools';
|
||||
import { AssetAdvancedTools } from './tools/asset-advanced-tools';
|
||||
import { ValidationTools } from './tools/validation-tools';
|
||||
|
||||
export class MCPServer {
|
||||
private settings: MCPServerSettings;
|
||||
@@ -44,6 +45,7 @@ export class MCPServer {
|
||||
this.tools.sceneView = new SceneViewTools();
|
||||
this.tools.referenceImage = new ReferenceImageTools();
|
||||
this.tools.assetAdvanced = new AssetAdvancedTools();
|
||||
this.tools.validation = new ValidationTools();
|
||||
console.log('[MCPServer] Tools initialized successfully');
|
||||
} catch (error) {
|
||||
console.error('[MCPServer] Error initializing tools:', error);
|
||||
@@ -146,6 +148,11 @@ export class MCPServer {
|
||||
} else if (pathname === '/health' && req.method === 'GET') {
|
||||
res.writeHead(200);
|
||||
res.end(JSON.stringify({ status: 'ok', tools: this.toolsList.length }));
|
||||
} else if (pathname?.startsWith('/api/') && req.method === 'POST') {
|
||||
await this.handleSimpleAPIRequest(req, res, pathname);
|
||||
} else if (pathname === '/api/tools' && req.method === 'GET') {
|
||||
res.writeHead(200);
|
||||
res.end(JSON.stringify({ tools: this.getSimplifiedToolsList() }));
|
||||
} else {
|
||||
res.writeHead(404);
|
||||
res.end(JSON.stringify({ error: 'Not found' }));
|
||||
@@ -166,11 +173,25 @@ export class MCPServer {
|
||||
|
||||
req.on('end', async () => {
|
||||
try {
|
||||
const message = JSON.parse(body);
|
||||
// Enhanced JSON parsing with better error handling
|
||||
let message;
|
||||
try {
|
||||
message = JSON.parse(body);
|
||||
} catch (parseError: any) {
|
||||
// Try to fix common JSON issues
|
||||
const fixedBody = this.fixCommonJsonIssues(body);
|
||||
try {
|
||||
message = JSON.parse(fixedBody);
|
||||
console.log('[MCPServer] Fixed JSON parsing issue');
|
||||
} catch (secondError) {
|
||||
throw new Error(`JSON parsing failed: ${parseError.message}. Original body: ${body.substring(0, 500)}...`);
|
||||
}
|
||||
}
|
||||
|
||||
const response = await this.handleMessage(message);
|
||||
res.writeHead(200);
|
||||
res.end(JSON.stringify(response));
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error('Error handling MCP request:', error);
|
||||
res.writeHead(400);
|
||||
res.end(JSON.stringify({
|
||||
@@ -178,7 +199,7 @@ export class MCPServer {
|
||||
id: null,
|
||||
error: {
|
||||
code: -32700,
|
||||
message: 'Parse error'
|
||||
message: `Parse error: ${error.message}`
|
||||
}
|
||||
}));
|
||||
}
|
||||
@@ -234,6 +255,27 @@ export class MCPServer {
|
||||
}
|
||||
}
|
||||
|
||||
private fixCommonJsonIssues(jsonStr: string): string {
|
||||
let fixed = jsonStr;
|
||||
|
||||
// Fix common escape character issues
|
||||
fixed = fixed
|
||||
// Fix unescaped quotes in strings
|
||||
.replace(/([^\\])"([^"]*[^\\])"([^,}\]:])/g, '$1\\"$2\\"$3')
|
||||
// Fix unescaped backslashes
|
||||
.replace(/([^\\])\\([^"\\\/bfnrt])/g, '$1\\\\$2')
|
||||
// Fix trailing commas
|
||||
.replace(/,(\s*[}\]])/g, '$1')
|
||||
// Fix single quotes (should be double quotes)
|
||||
.replace(/'/g, '"')
|
||||
// Fix common control characters
|
||||
.replace(/\n/g, '\\n')
|
||||
.replace(/\r/g, '\\r')
|
||||
.replace(/\t/g, '\\t');
|
||||
|
||||
return fixed;
|
||||
}
|
||||
|
||||
public stop(): void {
|
||||
if (this.httpServer) {
|
||||
this.httpServer.close();
|
||||
@@ -252,6 +294,123 @@ export class MCPServer {
|
||||
};
|
||||
}
|
||||
|
||||
private async handleSimpleAPIRequest(req: http.IncomingMessage, res: http.ServerResponse, pathname: string): Promise<void> {
|
||||
let body = '';
|
||||
|
||||
req.on('data', (chunk) => {
|
||||
body += chunk.toString();
|
||||
});
|
||||
|
||||
req.on('end', async () => {
|
||||
try {
|
||||
// Extract tool name from path like /api/node/set_position
|
||||
const pathParts = pathname.split('/').filter(p => p);
|
||||
if (pathParts.length < 3) {
|
||||
res.writeHead(400);
|
||||
res.end(JSON.stringify({ error: 'Invalid API path. Use /api/{category}/{tool_name}' }));
|
||||
return;
|
||||
}
|
||||
|
||||
const category = pathParts[1];
|
||||
const toolName = pathParts[2];
|
||||
const fullToolName = `${category}_${toolName}`;
|
||||
|
||||
// Parse parameters with enhanced error handling
|
||||
let params;
|
||||
try {
|
||||
params = body ? JSON.parse(body) : {};
|
||||
} catch (parseError: any) {
|
||||
// Try to fix JSON issues
|
||||
const fixedBody = this.fixCommonJsonIssues(body);
|
||||
try {
|
||||
params = JSON.parse(fixedBody);
|
||||
console.log('[MCPServer] Fixed API JSON parsing issue');
|
||||
} catch (secondError: any) {
|
||||
res.writeHead(400);
|
||||
res.end(JSON.stringify({
|
||||
error: 'Invalid JSON in request body',
|
||||
details: parseError.message,
|
||||
receivedBody: body.substring(0, 200)
|
||||
}));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Execute tool
|
||||
const result = await this.executeToolCall(fullToolName, params);
|
||||
|
||||
res.writeHead(200);
|
||||
res.end(JSON.stringify({
|
||||
success: true,
|
||||
tool: fullToolName,
|
||||
result: result
|
||||
}));
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Simple API error:', error);
|
||||
res.writeHead(500);
|
||||
res.end(JSON.stringify({
|
||||
success: false,
|
||||
error: error.message,
|
||||
tool: pathname
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private getSimplifiedToolsList(): any[] {
|
||||
return this.toolsList.map(tool => {
|
||||
const parts = tool.name.split('_');
|
||||
const category = parts[0];
|
||||
const toolName = parts.slice(1).join('_');
|
||||
|
||||
return {
|
||||
name: tool.name,
|
||||
category: category,
|
||||
toolName: toolName,
|
||||
description: tool.description,
|
||||
apiPath: `/api/${category}/${toolName}`,
|
||||
curlExample: this.generateCurlExample(category, toolName, tool.inputSchema)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private generateCurlExample(category: string, toolName: string, schema: any): string {
|
||||
// Generate sample parameters based on schema
|
||||
const sampleParams = this.generateSampleParams(schema);
|
||||
const jsonString = JSON.stringify(sampleParams, null, 2);
|
||||
|
||||
return `curl -X POST http://127.0.0.1:8585/api/${category}/${toolName} \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '${jsonString}'`;
|
||||
}
|
||||
|
||||
private generateSampleParams(schema: any): any {
|
||||
if (!schema || !schema.properties) return {};
|
||||
|
||||
const sample: any = {};
|
||||
for (const [key, prop] of Object.entries(schema.properties as any)) {
|
||||
const propSchema = prop as any;
|
||||
switch (propSchema.type) {
|
||||
case 'string':
|
||||
sample[key] = propSchema.default || 'example_string';
|
||||
break;
|
||||
case 'number':
|
||||
sample[key] = propSchema.default || 42;
|
||||
break;
|
||||
case 'boolean':
|
||||
sample[key] = propSchema.default || true;
|
||||
break;
|
||||
case 'object':
|
||||
sample[key] = propSchema.default || { x: 0, y: 0, z: 0 };
|
||||
break;
|
||||
default:
|
||||
sample[key] = 'example_value';
|
||||
}
|
||||
}
|
||||
return sample;
|
||||
}
|
||||
|
||||
public updateSettings(settings: MCPServerSettings) {
|
||||
this.settings = settings;
|
||||
if (this.httpServer) {
|
||||
|
||||
@@ -73,24 +73,25 @@ export class ComponentTools implements ToolExecutor {
|
||||
},
|
||||
{
|
||||
name: 'set_component_property',
|
||||
description: 'Set component property value',
|
||||
description: 'Set component property value - AI只需提供4个简单参数:节点UUID、组件名称、属性名称、属性值',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
nodeUuid: {
|
||||
type: 'string',
|
||||
description: 'Node UUID'
|
||||
description: 'Node UUID - 节点的UUID'
|
||||
},
|
||||
componentType: {
|
||||
type: 'string',
|
||||
description: 'Component type'
|
||||
description: 'Component type - 组件类型',
|
||||
enum: ['cc.Label', 'cc.Sprite', 'cc.Button', 'cc.Toggle', 'cc.Slider', 'cc.ScrollView', 'cc.EditBox', 'cc.ProgressBar', 'cc.RichText', 'cc.Mask', 'cc.Graphics', 'cc.Layout', 'cc.Widget', 'cc.UITransform']
|
||||
},
|
||||
property: {
|
||||
type: 'string',
|
||||
description: 'Property name'
|
||||
description: 'Property name - 属性名称,常见值: string(文本), color(颜色), fontSize(字体大小), spriteFrame(精灵帧), enabled(启用状态), position(位置), scale(缩放), rotation(旋转)'
|
||||
},
|
||||
value: {
|
||||
description: 'Property value'
|
||||
description: 'Property value - 属性值,支持的类型:\n• 字符串: "Hello World"\n• 数字: 32, 1.5\n• 布尔值: true, false\n• 颜色对象: {"r":255,"g":0,"b":0,"a":255} 或 "#FF0000"\n• 向量对象: {"x":100,"y":50} 或 {"x":1,"y":2,"z":3}\n• 尺寸对象: {"width":100,"height":50}\n• 资源UUID: "asset-uuid-string"'
|
||||
}
|
||||
},
|
||||
required: ['nodeUuid', 'componentType', 'property', 'value']
|
||||
@@ -160,12 +161,41 @@ export class ComponentTools implements ToolExecutor {
|
||||
uuid: nodeUuid,
|
||||
component: componentType
|
||||
}).then((result: any) => {
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
componentId: result,
|
||||
message: `Component '${componentType}' added successfully`
|
||||
}
|
||||
// Get comprehensive verification data including node info and all components
|
||||
Promise.all([
|
||||
this.getComponents(nodeUuid),
|
||||
this.getComponentInfo(nodeUuid, componentType)
|
||||
]).then(([allComponentsInfo, newComponentInfo]) => {
|
||||
const addedComponent = allComponentsInfo.data?.components?.find((comp: any) => comp.type === componentType);
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
componentId: result,
|
||||
nodeUuid: nodeUuid,
|
||||
componentType: componentType,
|
||||
message: `Component '${componentType}' added successfully`,
|
||||
componentVerified: !!addedComponent
|
||||
},
|
||||
verificationData: {
|
||||
addedComponent: newComponentInfo.data,
|
||||
allNodeComponents: allComponentsInfo.data,
|
||||
componentCount: allComponentsInfo.data?.components?.length || 0,
|
||||
verificationStatus: {
|
||||
componentExists: !!addedComponent,
|
||||
componentDetails: addedComponent || null
|
||||
}
|
||||
}
|
||||
});
|
||||
}).catch(() => {
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
componentId: result,
|
||||
nodeUuid: nodeUuid,
|
||||
componentType: componentType,
|
||||
message: `Component '${componentType}' added successfully (verification failed)`
|
||||
}
|
||||
});
|
||||
});
|
||||
}).catch((err: Error) => {
|
||||
// 备用方案:使用场景脚本
|
||||
@@ -206,7 +236,7 @@ export class ComponentTools implements ToolExecutor {
|
||||
Editor.Message.request('scene', 'query-node', nodeUuid).then((nodeData: any) => {
|
||||
if (nodeData && nodeData.__comps__) {
|
||||
const components = nodeData.__comps__.map((comp: any) => ({
|
||||
type: comp.__type__ || 'Unknown',
|
||||
type: comp.__type__ || comp.cid || comp.type || 'Unknown',
|
||||
enabled: comp.enabled !== undefined ? comp.enabled : true,
|
||||
properties: this.extractComponentProperties(comp)
|
||||
}));
|
||||
@@ -250,7 +280,10 @@ export class ComponentTools implements ToolExecutor {
|
||||
// 优先尝试直接使用 Editor API 查询节点信息
|
||||
Editor.Message.request('scene', 'query-node', nodeUuid).then((nodeData: any) => {
|
||||
if (nodeData && nodeData.__comps__) {
|
||||
const component = nodeData.__comps__.find((comp: any) => comp.__type__ === componentType);
|
||||
const component = nodeData.__comps__.find((comp: any) => {
|
||||
const compType = comp.__type__ || comp.cid || comp.type;
|
||||
return compType === componentType;
|
||||
});
|
||||
|
||||
if (component) {
|
||||
resolve({
|
||||
@@ -315,58 +348,182 @@ export class ComponentTools implements ToolExecutor {
|
||||
}
|
||||
|
||||
private async setComponentProperty(args: any): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
// 首先获取节点信息以找到正确的组件索引
|
||||
Editor.Message.request('scene', 'query-node', args.nodeUuid).then((nodeData: any) => {
|
||||
if (!nodeData || !nodeData.__comps__) {
|
||||
throw new Error('Node not found or no components data');
|
||||
const { nodeUuid, componentType, property, value } = args;
|
||||
|
||||
return new Promise(async (resolve) => {
|
||||
try {
|
||||
console.log(`[ComponentTools] Setting ${componentType}.${property} = ${JSON.stringify(value)} on node ${nodeUuid}`);
|
||||
|
||||
// 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}`
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 查找组件索引
|
||||
let componentIndex = -1;
|
||||
for (let i = 0; i < nodeData.__comps__.length; i++) {
|
||||
const comp = nodeData.__comps__[i];
|
||||
if (comp.__type__ === args.componentType) {
|
||||
componentIndex = i;
|
||||
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 (componentIndex === -1) {
|
||||
throw new Error(`Component '${args.componentType}' not found on node`);
|
||||
if (!targetComponent) {
|
||||
resolve({
|
||||
success: false,
|
||||
error: `Component '${componentType}' not found on node. Available components: ${availableTypes.join(', ')}`
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用正确的组件索引路径
|
||||
const propertyPath = `__comps__.${componentIndex}.${args.property}`;
|
||||
// Step 3: 自动检测和转换属性值
|
||||
const propertyInfo = this.analyzeProperty(targetComponent, property);
|
||||
if (!propertyInfo.exists) {
|
||||
resolve({
|
||||
success: false,
|
||||
error: `Property '${property}' not found on component '${componentType}'. Available properties: ${propertyInfo.availableProperties.join(', ')}`
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
return Editor.Message.request('scene', 'set-property', {
|
||||
uuid: args.nodeUuid,
|
||||
path: propertyPath,
|
||||
dump: {
|
||||
value: args.value
|
||||
const processedValue = this.smartConvertValue(value, propertyInfo);
|
||||
const originalValue = propertyInfo.originalValue;
|
||||
|
||||
console.log(`[ComponentTools] Converting value: ${JSON.stringify(value)} -> ${JSON.stringify(processedValue)} (type: ${propertyInfo.type})`);
|
||||
|
||||
// Step 4: 设置属性值
|
||||
// 需要重新获取原始节点数据来构建正确的路径
|
||||
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;
|
||||
}
|
||||
});
|
||||
}).then(() => {
|
||||
}
|
||||
|
||||
if (rawComponentIndex === -1) {
|
||||
resolve({
|
||||
success: false,
|
||||
error: `Could not find component index for setting property`
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 构建正确的属性路径
|
||||
let propertyPath = `__comps__.${rawComponentIndex}.${property}`;
|
||||
|
||||
// 特殊处理资源类属性
|
||||
if (propertyInfo.type === 'asset') {
|
||||
// 对于资源类属性,需要特殊处理
|
||||
const assetValue = typeof processedValue === 'string' ?
|
||||
{ uuid: processedValue } : processedValue;
|
||||
|
||||
// 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';
|
||||
}
|
||||
|
||||
// Try multiple approaches for setting asset properties
|
||||
try {
|
||||
// Approach 1: Direct property setting with asset structure
|
||||
await Editor.Message.request('scene', 'set-property', {
|
||||
uuid: nodeUuid,
|
||||
path: propertyPath,
|
||||
dump: {
|
||||
value: assetValue,
|
||||
type: assetType
|
||||
}
|
||||
});
|
||||
} catch (error1) {
|
||||
try {
|
||||
// Approach 2: Try with different structure
|
||||
await Editor.Message.request('scene', 'set-property', {
|
||||
uuid: nodeUuid,
|
||||
path: propertyPath,
|
||||
dump: {
|
||||
value: {
|
||||
__uuid__: assetValue.uuid || assetValue
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error2) {
|
||||
// Approach 3: Try direct UUID assignment
|
||||
await Editor.Message.request('scene', 'set-property', {
|
||||
uuid: nodeUuid,
|
||||
path: propertyPath,
|
||||
dump: { value: assetValue.uuid || assetValue }
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Normal property setting for non-asset properties
|
||||
await Editor.Message.request('scene', 'set-property', {
|
||||
uuid: nodeUuid,
|
||||
path: propertyPath,
|
||||
dump: { value: processedValue }
|
||||
});
|
||||
}
|
||||
|
||||
// Step 5: 验证设置结果
|
||||
const verification = await this.verifyPropertyChange(nodeUuid, componentType, property, originalValue, processedValue);
|
||||
|
||||
resolve({
|
||||
success: true,
|
||||
message: `Component property '${args.property}' updated successfully`
|
||||
message: `Successfully set ${componentType}.${property} = ${JSON.stringify(processedValue)}`,
|
||||
data: {
|
||||
nodeUuid,
|
||||
componentType,
|
||||
property,
|
||||
originalValue,
|
||||
newValue: processedValue,
|
||||
actualValue: verification.actualValue,
|
||||
changeVerified: verification.verified
|
||||
},
|
||||
verificationData: verification.fullData
|
||||
});
|
||||
}).catch((err: Error) => {
|
||||
// 备用方案:使用场景脚本
|
||||
const options = {
|
||||
name: 'cocos-mcp-server',
|
||||
method: 'setComponentProperty',
|
||||
args: [args.nodeUuid, args.componentType, args.property, args.value]
|
||||
};
|
||||
Editor.Message.request('scene', 'execute-scene-script', options).then((result: any) => {
|
||||
resolve(result);
|
||||
}).catch((err2: Error) => {
|
||||
resolve({ success: false, error: `Direct API failed: ${err.message}, Scene script failed: ${err2.message}` });
|
||||
|
||||
} 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((resolve) => {
|
||||
// 从脚本路径提取组件类名
|
||||
@@ -400,7 +557,7 @@ export class ComponentTools implements ToolExecutor {
|
||||
|
||||
Editor.Message.request('scene', 'execute-scene-script', options).then((result: any) => {
|
||||
resolve(result);
|
||||
}).catch((err2: Error) => {
|
||||
}).catch(() => {
|
||||
resolve({
|
||||
success: false,
|
||||
error: `Failed to attach script '${scriptName}': ${err.message}`,
|
||||
@@ -442,4 +599,245 @@ export class ComponentTools implements ToolExecutor {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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 (propertyName in component) {
|
||||
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)) {
|
||||
if (typeof propData === 'object' && propData && 'value' in propData) {
|
||||
const propInfo = propData as any;
|
||||
availableProperties.push(key);
|
||||
if (key === propertyName) {
|
||||
propertyValue = propInfo.value;
|
||||
propertyExists = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 备用方案:直接从properties查找
|
||||
for (const [key, propData] of Object.entries(component.properties)) {
|
||||
if (typeof propData === 'object' && propData && 'value' in propData) {
|
||||
const propInfo = propData as any;
|
||||
availableProperties.push(key);
|
||||
if (key === propertyName) {
|
||||
propertyValue = propInfo.value;
|
||||
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 (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') {
|
||||
if ('r' in propertyValue && 'g' in propertyValue && 'b' in propertyValue) {
|
||||
type = 'color';
|
||||
} else if ('x' in propertyValue && 'y' in propertyValue) {
|
||||
type = propertyValue.z !== undefined ? 'vec3' : 'vec2';
|
||||
} else if ('width' in propertyValue && 'height' in propertyValue) {
|
||||
type = 'size';
|
||||
} else if ('uuid' in propertyValue || '__uuid__' in propertyValue) {
|
||||
type = 'asset';
|
||||
} else {
|
||||
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 {
|
||||
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 === 'object' && inputValue !== null) {
|
||||
// 如果输入是颜色对象,直接使用
|
||||
if ('r' in inputValue || 'g' in inputValue || 'b' in inputValue) {
|
||||
return {
|
||||
r: Number(inputValue.r) || 0,
|
||||
g: Number(inputValue.g) || 0,
|
||||
b: Number(inputValue.b) || 0,
|
||||
a: Number(inputValue.a) !== undefined ? Number(inputValue.a) : 255
|
||||
};
|
||||
}
|
||||
} else if (typeof inputValue === 'string') {
|
||||
// 如果是字符串,尝试解析为十六进制颜色
|
||||
return this.parseColorString(inputValue);
|
||||
}
|
||||
// 保持原值结构,只更新提供的值
|
||||
return {
|
||||
r: Number(inputValue.r) || originalValue.r || 255,
|
||||
g: Number(inputValue.g) || originalValue.g || 255,
|
||||
b: Number(inputValue.b) || originalValue.b || 255,
|
||||
a: Number(inputValue.a) !== undefined ? Number(inputValue.a) : (originalValue.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 '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 } {
|
||||
// 简单的颜色字符串解析(支持#RRGGBB格式) // cSpell:ignore RRGGBB
|
||||
if (colorStr.startsWith('#') && colorStr.length === 7) {
|
||||
const r = parseInt(colorStr.substring(1, 3), 16);
|
||||
const g = parseInt(colorStr.substring(3, 5), 16);
|
||||
const b = parseInt(colorStr.substring(5, 7), 16);
|
||||
return { r, g, b, a: 255 };
|
||||
}
|
||||
// 默认返回白色
|
||||
return { r: 255, g: 255, b: 255, a: 255 };
|
||||
}
|
||||
|
||||
private async verifyPropertyChange(nodeUuid: string, componentType: string, property: string, originalValue: any, expectedValue: any): Promise<{ verified: boolean; actualValue: any; fullData: any }> {
|
||||
try {
|
||||
// 重新获取组件信息进行验证
|
||||
const componentInfo = await this.getComponentInfo(nodeUuid, componentType);
|
||||
const allComponents = await this.getComponents(nodeUuid);
|
||||
|
||||
if (componentInfo.success && componentInfo.data) {
|
||||
const actualValue = componentInfo.data.properties?.[property];
|
||||
const verified = JSON.stringify(actualValue) !== JSON.stringify(originalValue);
|
||||
|
||||
return {
|
||||
verified,
|
||||
actualValue,
|
||||
fullData: {
|
||||
updatedComponent: componentInfo.data,
|
||||
allNodeComponents: allComponents.data,
|
||||
changeDetails: {
|
||||
property,
|
||||
before: originalValue,
|
||||
expected: expectedValue,
|
||||
actual: actualValue,
|
||||
verified
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[verifyPropertyChange] Verification failed:', error);
|
||||
}
|
||||
|
||||
return {
|
||||
verified: false,
|
||||
actualValue: undefined,
|
||||
fullData: null
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -89,7 +89,7 @@ export class NodeTools implements ToolExecutor {
|
||||
},
|
||||
{
|
||||
name: 'set_node_property',
|
||||
description: 'Set node property value',
|
||||
description: 'Set node property value (prefer using set_node_transform for position/rotation/scale)',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -99,7 +99,7 @@ export class NodeTools implements ToolExecutor {
|
||||
},
|
||||
property: {
|
||||
type: 'string',
|
||||
description: 'Property name (e.g., position, rotation, scale, active)'
|
||||
description: 'Property name (e.g., active, name, layer)'
|
||||
},
|
||||
value: {
|
||||
description: 'Property value'
|
||||
@@ -108,6 +108,47 @@ export class NodeTools implements ToolExecutor {
|
||||
required: ['uuid', 'property', 'value']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'set_node_transform',
|
||||
description: 'Set node transform properties (position, rotation, scale) with unified interface. Automatically handles 2D/3D node differences.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
uuid: {
|
||||
type: 'string',
|
||||
description: 'Node UUID'
|
||||
},
|
||||
position: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
x: { type: 'number' },
|
||||
y: { type: 'number' },
|
||||
z: { type: 'number', description: 'Z coordinate (ignored for 2D nodes)' }
|
||||
},
|
||||
description: 'Node position. For 2D nodes, only x,y are used; z is ignored. For 3D nodes, all coordinates are used.'
|
||||
},
|
||||
rotation: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
x: { type: 'number', description: 'X rotation (ignored for 2D nodes)' },
|
||||
y: { type: 'number', description: 'Y rotation (ignored for 2D nodes)' },
|
||||
z: { type: 'number', description: 'Z rotation (main rotation axis for 2D nodes)' }
|
||||
},
|
||||
description: 'Node rotation in euler angles. For 2D nodes, only z rotation is used. For 3D nodes, all axes are used.'
|
||||
},
|
||||
scale: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
x: { type: 'number' },
|
||||
y: { type: 'number' },
|
||||
z: { type: 'number', description: 'Z scale (usually 1 for 2D nodes)' }
|
||||
},
|
||||
description: 'Node scale. For 2D nodes, z is typically 1. For 3D nodes, all axes are used.'
|
||||
}
|
||||
},
|
||||
required: ['uuid']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'delete_node',
|
||||
description: 'Delete a node from scene',
|
||||
@@ -163,6 +204,20 @@ export class NodeTools implements ToolExecutor {
|
||||
},
|
||||
required: ['uuid']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'detect_node_type',
|
||||
description: 'Detect if a node is 2D or 3D based on its components and properties',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
uuid: {
|
||||
type: 'string',
|
||||
description: 'Node UUID to analyze'
|
||||
}
|
||||
},
|
||||
required: ['uuid']
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -181,12 +236,16 @@ export class NodeTools implements ToolExecutor {
|
||||
return await this.getAllNodes();
|
||||
case 'set_node_property':
|
||||
return await this.setNodeProperty(args.uuid, args.property, args.value);
|
||||
case 'set_node_transform':
|
||||
return await this.setNodeTransform(args);
|
||||
case 'delete_node':
|
||||
return await this.deleteNode(args.uuid);
|
||||
case 'move_node':
|
||||
return await this.moveNode(args.nodeUuid, args.newParentUuid, args.siblingIndex);
|
||||
case 'duplicate_node':
|
||||
return await this.duplicateNode(args.uuid, args.includeChildren);
|
||||
case 'detect_node_type':
|
||||
return await this.detectNodeType(args.uuid);
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${toolName}`);
|
||||
}
|
||||
@@ -254,44 +313,67 @@ export class NodeTools implements ToolExecutor {
|
||||
|
||||
Editor.Message.request('scene', 'create-node', createNodeOptions).then((nodeUuid: any) => {
|
||||
// 如果需要设置特定的兄弟索引,使用set-parent API
|
||||
// 添加延迟以避免内部状态竞争
|
||||
if (args.siblingIndex !== undefined && args.siblingIndex >= 0 && nodeUuid) {
|
||||
Editor.Message.request('scene', 'set-parent', {
|
||||
parent: targetParentUuid,
|
||||
uuids: [nodeUuid],
|
||||
keepWorldTransform: false
|
||||
}).then(() => {
|
||||
setTimeout(() => {
|
||||
Editor.Message.request('scene', 'set-parent', {
|
||||
parent: targetParentUuid,
|
||||
uuids: [nodeUuid],
|
||||
keepWorldTransform: false
|
||||
}).then(() => {
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
uuid: nodeUuid,
|
||||
name: args.name,
|
||||
parentUuid: targetParentUuid,
|
||||
message: args.parentUuid
|
||||
? `Node '${args.name}' created under specified parent`
|
||||
: `Node '${args.name}' created at scene root (no parent specified)`
|
||||
}
|
||||
});
|
||||
}).catch(() => {
|
||||
// 即使移动失败,节点已创建,返回成功但带警告
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
uuid: nodeUuid,
|
||||
name: args.name,
|
||||
message: `Node '${args.name}' created but may not be under intended parent`,
|
||||
warning: 'Failed to move node to specified parent'
|
||||
}
|
||||
});
|
||||
});
|
||||
}, 100); // 100ms延迟
|
||||
} else {
|
||||
// Get complete node info for verification
|
||||
this.getNodeInfo(nodeUuid).then((nodeInfo) => {
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
uuid: nodeUuid,
|
||||
name: args.name,
|
||||
parentUuid: targetParentUuid,
|
||||
message: args.parentUuid
|
||||
? `Node '${args.name}' created under specified parent`
|
||||
: `Node '${args.name}' created at scene root (no parent specified)`
|
||||
message: `Node '${args.name}' created successfully`
|
||||
},
|
||||
verificationData: {
|
||||
nodeInfo: nodeInfo.data,
|
||||
creationDetails: {
|
||||
parentUuid: targetParentUuid,
|
||||
nodeType: args.nodeType || 'Node',
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
}
|
||||
});
|
||||
}).catch(() => {
|
||||
// 即使移动失败,节点已创建,返回成功但带警告
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
uuid: nodeUuid,
|
||||
name: args.name,
|
||||
message: `Node '${args.name}' created but may not be under intended parent`,
|
||||
warning: 'Failed to move node to specified parent'
|
||||
message: `Node '${args.name}' created successfully (verification failed)`
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
uuid: nodeUuid,
|
||||
name: args.name,
|
||||
message: `Node '${args.name}' created successfully`
|
||||
}
|
||||
});
|
||||
}
|
||||
}).catch((err: Error) => {
|
||||
resolve({ success: false, error: err.message });
|
||||
@@ -527,9 +609,30 @@ export class NodeTools implements ToolExecutor {
|
||||
value: value
|
||||
}
|
||||
}).then(() => {
|
||||
resolve({
|
||||
success: true,
|
||||
message: `Property '${property}' updated successfully`
|
||||
// Get comprehensive verification data including updated node info
|
||||
this.getNodeInfo(uuid).then((nodeInfo) => {
|
||||
resolve({
|
||||
success: true,
|
||||
message: `Property '${property}' updated successfully`,
|
||||
data: {
|
||||
nodeUuid: uuid,
|
||||
property: property,
|
||||
newValue: value
|
||||
},
|
||||
verificationData: {
|
||||
nodeInfo: nodeInfo.data,
|
||||
changeDetails: {
|
||||
property: property,
|
||||
value: value,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
}
|
||||
});
|
||||
}).catch(() => {
|
||||
resolve({
|
||||
success: true,
|
||||
message: `Property '${property}' updated successfully (verification failed)`
|
||||
});
|
||||
});
|
||||
}).catch((err: Error) => {
|
||||
// 如果直接设置失败,尝试使用场景脚本
|
||||
@@ -548,6 +651,215 @@ export class NodeTools implements ToolExecutor {
|
||||
});
|
||||
}
|
||||
|
||||
private async setNodeTransform(args: any): Promise<ToolResponse> {
|
||||
return new Promise(async (resolve) => {
|
||||
const { uuid, position, rotation, scale } = args;
|
||||
const updatePromises: Promise<any>[] = [];
|
||||
const updates: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
|
||||
try {
|
||||
// First get node info to determine if it's 2D or 3D
|
||||
const nodeInfoResponse = await this.getNodeInfo(uuid);
|
||||
if (!nodeInfoResponse.success || !nodeInfoResponse.data) {
|
||||
resolve({ success: false, error: 'Failed to get node information' });
|
||||
return;
|
||||
}
|
||||
|
||||
const nodeInfo = nodeInfoResponse.data;
|
||||
const is2DNode = this.is2DNode(nodeInfo);
|
||||
|
||||
if (position) {
|
||||
const normalizedPosition = this.normalizeTransformValue(position, 'position', is2DNode);
|
||||
if (normalizedPosition.warning) {
|
||||
warnings.push(normalizedPosition.warning);
|
||||
}
|
||||
|
||||
updatePromises.push(
|
||||
Editor.Message.request('scene', 'set-property', {
|
||||
uuid: uuid,
|
||||
path: 'position',
|
||||
dump: { value: normalizedPosition.value }
|
||||
})
|
||||
);
|
||||
updates.push('position');
|
||||
}
|
||||
|
||||
if (rotation) {
|
||||
const normalizedRotation = this.normalizeTransformValue(rotation, 'rotation', is2DNode);
|
||||
if (normalizedRotation.warning) {
|
||||
warnings.push(normalizedRotation.warning);
|
||||
}
|
||||
|
||||
updatePromises.push(
|
||||
Editor.Message.request('scene', 'set-property', {
|
||||
uuid: uuid,
|
||||
path: 'rotation',
|
||||
dump: { value: normalizedRotation.value }
|
||||
})
|
||||
);
|
||||
updates.push('rotation');
|
||||
}
|
||||
|
||||
if (scale) {
|
||||
const normalizedScale = this.normalizeTransformValue(scale, 'scale', is2DNode);
|
||||
if (normalizedScale.warning) {
|
||||
warnings.push(normalizedScale.warning);
|
||||
}
|
||||
|
||||
updatePromises.push(
|
||||
Editor.Message.request('scene', 'set-property', {
|
||||
uuid: uuid,
|
||||
path: 'scale',
|
||||
dump: { value: normalizedScale.value }
|
||||
})
|
||||
);
|
||||
updates.push('scale');
|
||||
}
|
||||
|
||||
if (updatePromises.length === 0) {
|
||||
resolve({ success: false, error: 'No transform properties specified' });
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.all(updatePromises);
|
||||
|
||||
// Verify the changes by getting updated node info
|
||||
const updatedNodeInfo = await this.getNodeInfo(uuid);
|
||||
const response: any = {
|
||||
success: true,
|
||||
message: `Transform properties updated: ${updates.join(', ')} ${is2DNode ? '(2D node)' : '(3D node)'}`,
|
||||
updatedProperties: updates,
|
||||
data: {
|
||||
nodeUuid: uuid,
|
||||
nodeType: is2DNode ? '2D' : '3D',
|
||||
appliedChanges: updates,
|
||||
transformConstraints: {
|
||||
position: is2DNode ? 'x, y only (z ignored)' : 'x, y, z all used',
|
||||
rotation: is2DNode ? 'z only (x, y ignored)' : 'x, y, z all used',
|
||||
scale: is2DNode ? 'x, y main, z typically 1' : 'x, y, z all used'
|
||||
}
|
||||
},
|
||||
verificationData: {
|
||||
nodeInfo: updatedNodeInfo.data,
|
||||
transformDetails: {
|
||||
originalNodeType: is2DNode ? '2D' : '3D',
|
||||
appliedTransforms: updates,
|
||||
timestamp: new Date().toISOString()
|
||||
},
|
||||
beforeAfterComparison: {
|
||||
before: nodeInfo,
|
||||
after: updatedNodeInfo.data
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (warnings.length > 0) {
|
||||
response.warning = warnings.join('; ');
|
||||
}
|
||||
|
||||
resolve(response);
|
||||
|
||||
} catch (err: any) {
|
||||
resolve({
|
||||
success: false,
|
||||
error: `Failed to update transform: ${err.message}`
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private is2DNode(nodeInfo: any): boolean {
|
||||
// Check if node has 2D-specific components or is under Canvas
|
||||
const components = nodeInfo.components || [];
|
||||
|
||||
// Check for common 2D components
|
||||
const has2DComponents = components.some((comp: any) =>
|
||||
comp.type && (
|
||||
comp.type.includes('cc.Sprite') ||
|
||||
comp.type.includes('cc.Label') ||
|
||||
comp.type.includes('cc.Button') ||
|
||||
comp.type.includes('cc.Layout') ||
|
||||
comp.type.includes('cc.Widget') ||
|
||||
comp.type.includes('cc.Mask') ||
|
||||
comp.type.includes('cc.Graphics')
|
||||
)
|
||||
);
|
||||
|
||||
if (has2DComponents) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for 3D-specific components
|
||||
const has3DComponents = components.some((comp: any) =>
|
||||
comp.type && (
|
||||
comp.type.includes('cc.MeshRenderer') ||
|
||||
comp.type.includes('cc.Camera') ||
|
||||
comp.type.includes('cc.Light') ||
|
||||
comp.type.includes('cc.DirectionalLight') ||
|
||||
comp.type.includes('cc.PointLight') ||
|
||||
comp.type.includes('cc.SpotLight')
|
||||
)
|
||||
);
|
||||
|
||||
if (has3DComponents) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Default heuristic: if z position is 0 and hasn't been changed, likely 2D
|
||||
const position = nodeInfo.position;
|
||||
if (position && Math.abs(position.z) < 0.001) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Default to 3D if uncertain
|
||||
return false;
|
||||
}
|
||||
|
||||
private normalizeTransformValue(value: any, type: 'position' | 'rotation' | 'scale', is2D: boolean): { value: any, warning?: string } {
|
||||
const result = { ...value };
|
||||
let warning: string | undefined;
|
||||
|
||||
if (is2D) {
|
||||
switch (type) {
|
||||
case 'position':
|
||||
if (value.z !== undefined && Math.abs(value.z) > 0.001) {
|
||||
warning = `2D node: z position (${value.z}) ignored, set to 0`;
|
||||
result.z = 0;
|
||||
} else if (value.z === undefined) {
|
||||
result.z = 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'rotation':
|
||||
if ((value.x !== undefined && Math.abs(value.x) > 0.001) ||
|
||||
(value.y !== undefined && Math.abs(value.y) > 0.001)) {
|
||||
warning = `2D node: x,y rotations ignored, only z rotation applied`;
|
||||
result.x = 0;
|
||||
result.y = 0;
|
||||
} else {
|
||||
result.x = result.x || 0;
|
||||
result.y = result.y || 0;
|
||||
}
|
||||
result.z = result.z || 0;
|
||||
break;
|
||||
|
||||
case 'scale':
|
||||
if (value.z === undefined) {
|
||||
result.z = 1; // Default scale for 2D
|
||||
}
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// 3D node - ensure all axes are defined
|
||||
result.x = result.x !== undefined ? result.x : (type === 'scale' ? 1 : 0);
|
||||
result.y = result.y !== undefined ? result.y : (type === 'scale' ? 1 : 0);
|
||||
result.z = result.z !== undefined ? result.z : (type === 'scale' ? 1 : 0);
|
||||
}
|
||||
|
||||
return { value: result, warning };
|
||||
}
|
||||
|
||||
private async deleteNode(uuid: string): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
Editor.Message.request('scene', 'remove-node', { uuid: uuid }).then(() => {
|
||||
@@ -595,4 +907,113 @@ export class NodeTools implements ToolExecutor {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async detectNodeType(uuid: string): Promise<ToolResponse> {
|
||||
return new Promise(async (resolve) => {
|
||||
try {
|
||||
const nodeInfoResponse = await this.getNodeInfo(uuid);
|
||||
if (!nodeInfoResponse.success || !nodeInfoResponse.data) {
|
||||
resolve({ success: false, error: 'Failed to get node information' });
|
||||
return;
|
||||
}
|
||||
|
||||
const nodeInfo = nodeInfoResponse.data;
|
||||
const is2D = this.is2DNode(nodeInfo);
|
||||
const components = nodeInfo.components || [];
|
||||
|
||||
// Collect detection reasons
|
||||
const detectionReasons: string[] = [];
|
||||
|
||||
// Check for 2D components
|
||||
const twoDComponents = components.filter((comp: any) =>
|
||||
comp.type && (
|
||||
comp.type.includes('cc.Sprite') ||
|
||||
comp.type.includes('cc.Label') ||
|
||||
comp.type.includes('cc.Button') ||
|
||||
comp.type.includes('cc.Layout') ||
|
||||
comp.type.includes('cc.Widget') ||
|
||||
comp.type.includes('cc.Mask') ||
|
||||
comp.type.includes('cc.Graphics')
|
||||
)
|
||||
);
|
||||
|
||||
// Check for 3D components
|
||||
const threeDComponents = components.filter((comp: any) =>
|
||||
comp.type && (
|
||||
comp.type.includes('cc.MeshRenderer') ||
|
||||
comp.type.includes('cc.Camera') ||
|
||||
comp.type.includes('cc.Light') ||
|
||||
comp.type.includes('cc.DirectionalLight') ||
|
||||
comp.type.includes('cc.PointLight') ||
|
||||
comp.type.includes('cc.SpotLight')
|
||||
)
|
||||
);
|
||||
|
||||
if (twoDComponents.length > 0) {
|
||||
detectionReasons.push(`Has 2D components: ${twoDComponents.map((c: any) => c.type).join(', ')}`);
|
||||
}
|
||||
|
||||
if (threeDComponents.length > 0) {
|
||||
detectionReasons.push(`Has 3D components: ${threeDComponents.map((c: any) => c.type).join(', ')}`);
|
||||
}
|
||||
|
||||
// Check position for heuristic
|
||||
const position = nodeInfo.position;
|
||||
if (position && Math.abs(position.z) < 0.001) {
|
||||
detectionReasons.push('Z position is ~0 (likely 2D)');
|
||||
} else if (position && Math.abs(position.z) > 0.001) {
|
||||
detectionReasons.push(`Z position is ${position.z} (likely 3D)`);
|
||||
}
|
||||
|
||||
if (detectionReasons.length === 0) {
|
||||
detectionReasons.push('No specific indicators found, defaulting based on heuristics');
|
||||
}
|
||||
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
nodeUuid: uuid,
|
||||
nodeName: nodeInfo.name,
|
||||
nodeType: is2D ? '2D' : '3D',
|
||||
detectionReasons: detectionReasons,
|
||||
components: components.map((comp: any) => ({
|
||||
type: comp.type,
|
||||
category: this.getComponentCategory(comp.type)
|
||||
})),
|
||||
position: nodeInfo.position,
|
||||
transformConstraints: {
|
||||
position: is2D ? 'x, y only (z ignored)' : 'x, y, z all used',
|
||||
rotation: is2D ? 'z only (x, y ignored)' : 'x, y, z all used',
|
||||
scale: is2D ? 'x, y main, z typically 1' : 'x, y, z all used'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
} catch (err: any) {
|
||||
resolve({
|
||||
success: false,
|
||||
error: `Failed to detect node type: ${err.message}`
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private getComponentCategory(componentType: string): string {
|
||||
if (!componentType) return 'unknown';
|
||||
|
||||
if (componentType.includes('cc.Sprite') || componentType.includes('cc.Label') ||
|
||||
componentType.includes('cc.Button') || componentType.includes('cc.Layout') ||
|
||||
componentType.includes('cc.Widget') || componentType.includes('cc.Mask') ||
|
||||
componentType.includes('cc.Graphics')) {
|
||||
return '2D';
|
||||
}
|
||||
|
||||
if (componentType.includes('cc.MeshRenderer') || componentType.includes('cc.Camera') ||
|
||||
componentType.includes('cc.Light') || componentType.includes('cc.DirectionalLight') ||
|
||||
componentType.includes('cc.PointLight') || componentType.includes('cc.SpotLight')) {
|
||||
return '3D';
|
||||
}
|
||||
|
||||
return 'generic';
|
||||
}
|
||||
}
|
||||
@@ -331,6 +331,62 @@ export class ProjectTools implements ToolExecutor {
|
||||
},
|
||||
required: ['uuid']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'find_asset_by_name',
|
||||
description: 'Find assets by name (supports partial matching and multiple results)',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Asset name to search for (supports partial matching)'
|
||||
},
|
||||
exactMatch: {
|
||||
type: 'boolean',
|
||||
description: 'Whether to use exact name matching',
|
||||
default: false
|
||||
},
|
||||
assetType: {
|
||||
type: 'string',
|
||||
description: 'Filter by asset type',
|
||||
enum: ['all', 'scene', 'prefab', 'script', 'texture', 'material', 'mesh', 'audio', 'animation', 'spriteFrame'],
|
||||
default: 'all'
|
||||
},
|
||||
folder: {
|
||||
type: 'string',
|
||||
description: 'Folder to search in',
|
||||
default: 'db://assets'
|
||||
},
|
||||
maxResults: {
|
||||
type: 'number',
|
||||
description: 'Maximum number of results to return',
|
||||
default: 20,
|
||||
minimum: 1,
|
||||
maximum: 100
|
||||
}
|
||||
},
|
||||
required: ['name']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'get_asset_details',
|
||||
description: 'Get detailed asset information including spriteFrame sub-assets',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
assetPath: {
|
||||
type: 'string',
|
||||
description: 'Asset path (db://assets/...)'
|
||||
},
|
||||
includeSubAssets: {
|
||||
type: 'boolean',
|
||||
description: 'Include sub-assets like spriteFrame, texture',
|
||||
default: true
|
||||
}
|
||||
},
|
||||
required: ['assetPath']
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -381,6 +437,10 @@ export class ProjectTools implements ToolExecutor {
|
||||
return await this.queryAssetUuid(args.url);
|
||||
case 'query_asset_url':
|
||||
return await this.queryAssetUrl(args.uuid);
|
||||
case 'find_asset_by_name':
|
||||
return await this.findAssetByName(args);
|
||||
case 'get_asset_details':
|
||||
return await this.getAssetDetails(args.assetPath, args.includeSubAssets);
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${toolName}`);
|
||||
}
|
||||
@@ -896,4 +956,143 @@ export class ProjectTools implements ToolExecutor {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async findAssetByName(args: any): Promise<ToolResponse> {
|
||||
const { name, exactMatch = false, assetType = 'all', folder = 'db://assets', maxResults = 20 } = args;
|
||||
|
||||
return new Promise(async (resolve) => {
|
||||
try {
|
||||
// Get all assets in the specified folder
|
||||
const allAssetsResponse = await this.getAssets(assetType, folder);
|
||||
if (!allAssetsResponse.success || !allAssetsResponse.data) {
|
||||
resolve({
|
||||
success: false,
|
||||
error: `Failed to get assets: ${allAssetsResponse.error}`
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const allAssets = allAssetsResponse.data.assets as any[];
|
||||
let matchedAssets: any[] = [];
|
||||
|
||||
// Search for matching assets
|
||||
for (const asset of allAssets) {
|
||||
const assetName = asset.name;
|
||||
let matches = false;
|
||||
|
||||
if (exactMatch) {
|
||||
matches = assetName === name;
|
||||
} else {
|
||||
matches = assetName.toLowerCase().includes(name.toLowerCase());
|
||||
}
|
||||
|
||||
if (matches) {
|
||||
// Get detailed asset info if needed
|
||||
try {
|
||||
const detailResponse = await this.getAssetInfo(asset.path);
|
||||
if (detailResponse.success) {
|
||||
matchedAssets.push({
|
||||
...asset,
|
||||
details: detailResponse.data
|
||||
});
|
||||
} else {
|
||||
matchedAssets.push(asset);
|
||||
}
|
||||
} catch {
|
||||
matchedAssets.push(asset);
|
||||
}
|
||||
|
||||
if (matchedAssets.length >= maxResults) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
searchTerm: name,
|
||||
exactMatch,
|
||||
assetType,
|
||||
folder,
|
||||
totalFound: matchedAssets.length,
|
||||
maxResults,
|
||||
assets: matchedAssets,
|
||||
message: `Found ${matchedAssets.length} assets matching '${name}'`
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
resolve({
|
||||
success: false,
|
||||
error: `Asset search failed: ${error.message}`
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async getAssetDetails(assetPath: string, includeSubAssets: boolean = true): Promise<ToolResponse> {
|
||||
return new Promise(async (resolve) => {
|
||||
try {
|
||||
// Get basic asset info
|
||||
const assetInfoResponse = await this.getAssetInfo(assetPath);
|
||||
if (!assetInfoResponse.success) {
|
||||
resolve(assetInfoResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
const assetInfo = assetInfoResponse.data;
|
||||
const detailedInfo: any = {
|
||||
...assetInfo,
|
||||
subAssets: []
|
||||
};
|
||||
|
||||
if (includeSubAssets && assetInfo) {
|
||||
// For image assets, try to get spriteFrame and texture sub-assets
|
||||
if (assetInfo.type === 'cc.ImageAsset' || assetPath.match(/\.(png|jpg|jpeg|gif|tga|bmp|psd)$/i)) {
|
||||
// Generate common sub-asset UUIDs
|
||||
const baseUuid = assetInfo.uuid;
|
||||
const possibleSubAssets = [
|
||||
{ type: 'spriteFrame', uuid: `${baseUuid}@f9941`, suffix: '@f9941' },
|
||||
{ type: 'texture', uuid: `${baseUuid}@6c48a`, suffix: '@6c48a' },
|
||||
{ type: 'texture2D', uuid: `${baseUuid}@6c48a`, suffix: '@6c48a' }
|
||||
];
|
||||
|
||||
for (const subAsset of possibleSubAssets) {
|
||||
try {
|
||||
// Try to get URL for the sub-asset to verify it exists
|
||||
const subAssetUrl = await Editor.Message.request('asset-db', 'query-url', subAsset.uuid);
|
||||
if (subAssetUrl) {
|
||||
detailedInfo.subAssets.push({
|
||||
type: subAsset.type,
|
||||
uuid: subAsset.uuid,
|
||||
url: subAssetUrl,
|
||||
suffix: subAsset.suffix
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// Sub-asset doesn't exist, skip it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
assetPath,
|
||||
includeSubAssets,
|
||||
...detailedInfo,
|
||||
message: `Asset details retrieved. Found ${detailedInfo.subAssets.length} sub-assets.`
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
resolve({
|
||||
success: false,
|
||||
error: `Failed to get asset details: ${error.message}`
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -364,14 +364,30 @@ export class SceneTools implements ToolExecutor {
|
||||
], null, 2);
|
||||
|
||||
Editor.Message.request('asset-db', 'create-asset', fullPath, sceneContent).then((result: any) => {
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
uuid: result.uuid,
|
||||
url: result.url,
|
||||
name: sceneName,
|
||||
message: `Scene '${sceneName}' created successfully`
|
||||
}
|
||||
// Verify scene creation by checking if it exists
|
||||
this.getSceneList().then((sceneList) => {
|
||||
const createdScene = sceneList.data?.find((scene: any) => scene.uuid === result.uuid);
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
uuid: result.uuid,
|
||||
url: result.url,
|
||||
name: sceneName,
|
||||
message: `Scene '${sceneName}' created successfully`,
|
||||
sceneVerified: !!createdScene
|
||||
},
|
||||
verificationData: createdScene
|
||||
});
|
||||
}).catch(() => {
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
uuid: result.uuid,
|
||||
url: result.url,
|
||||
name: sceneName,
|
||||
message: `Scene '${sceneName}' created successfully (verification failed)`
|
||||
}
|
||||
});
|
||||
});
|
||||
}).catch((err: Error) => {
|
||||
resolve({ success: false, error: err.message });
|
||||
|
||||
263
source/tools/validation-tools.ts
Normal file
263
source/tools/validation-tools.ts
Normal file
@@ -0,0 +1,263 @@
|
||||
import { ToolDefinition, ToolResponse, ToolExecutor } from '../types';
|
||||
|
||||
export class ValidationTools implements ToolExecutor {
|
||||
getTools(): ToolDefinition[] {
|
||||
return [
|
||||
{
|
||||
name: 'validate_json_params',
|
||||
description: 'Validate and fix JSON parameters before sending to other tools',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
jsonString: {
|
||||
type: 'string',
|
||||
description: 'JSON string to validate and fix'
|
||||
},
|
||||
expectedSchema: {
|
||||
type: 'object',
|
||||
description: 'Expected parameter schema (optional)'
|
||||
}
|
||||
},
|
||||
required: ['jsonString']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'safe_string_value',
|
||||
description: 'Create a safe string value that won\'t cause JSON parsing issues',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
value: {
|
||||
type: 'string',
|
||||
description: 'String value to make safe'
|
||||
}
|
||||
},
|
||||
required: ['value']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'format_mcp_request',
|
||||
description: 'Format a complete MCP request with proper JSON escaping',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
toolName: {
|
||||
type: 'string',
|
||||
description: 'Tool name to call'
|
||||
},
|
||||
arguments: {
|
||||
type: 'object',
|
||||
description: 'Tool arguments'
|
||||
}
|
||||
},
|
||||
required: ['toolName', 'arguments']
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
async execute(toolName: string, args: any): Promise<ToolResponse> {
|
||||
switch (toolName) {
|
||||
case 'validate_json_params':
|
||||
return await this.validateJsonParams(args.jsonString, args.expectedSchema);
|
||||
case 'safe_string_value':
|
||||
return await this.createSafeStringValue(args.value);
|
||||
case 'format_mcp_request':
|
||||
return await this.formatMcpRequest(args.toolName, args.arguments);
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${toolName}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async validateJsonParams(jsonString: string, expectedSchema?: any): Promise<ToolResponse> {
|
||||
try {
|
||||
// First try to parse as-is
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(jsonString);
|
||||
} catch (error: any) {
|
||||
// Try to fix common issues
|
||||
const fixed = this.fixJsonString(jsonString);
|
||||
try {
|
||||
parsed = JSON.parse(fixed);
|
||||
} catch (secondError) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Cannot fix JSON: ${error.message}`,
|
||||
data: {
|
||||
originalJson: jsonString,
|
||||
fixedAttempt: fixed,
|
||||
suggestions: this.getJsonFixSuggestions(jsonString)
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Validate against schema if provided
|
||||
if (expectedSchema) {
|
||||
const validation = this.validateAgainstSchema(parsed, expectedSchema);
|
||||
if (!validation.valid) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Schema validation failed',
|
||||
data: {
|
||||
parsedJson: parsed,
|
||||
validationErrors: validation.errors,
|
||||
suggestions: validation.suggestions
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
parsedJson: parsed,
|
||||
fixedJson: JSON.stringify(parsed, null, 2),
|
||||
isValid: true
|
||||
}
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async createSafeStringValue(value: string): Promise<ToolResponse> {
|
||||
const safeValue = this.escapJsonString(value);
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
originalValue: value,
|
||||
safeValue: safeValue,
|
||||
jsonReady: JSON.stringify(safeValue),
|
||||
usage: `Use "${safeValue}" in your JSON parameters`
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private async formatMcpRequest(toolName: string, toolArgs: any): Promise<ToolResponse> {
|
||||
try {
|
||||
const mcpRequest = {
|
||||
jsonrpc: '2.0',
|
||||
id: Date.now(),
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: toolName,
|
||||
arguments: toolArgs
|
||||
}
|
||||
};
|
||||
|
||||
const formattedJson = JSON.stringify(mcpRequest, null, 2);
|
||||
const compactJson = JSON.stringify(mcpRequest);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
request: mcpRequest,
|
||||
formattedJson: formattedJson,
|
||||
compactJson: compactJson,
|
||||
curlCommand: this.generateCurlCommand(compactJson)
|
||||
}
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Failed to format MCP request: ${error.message}`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private fixJsonString(jsonStr: string): string {
|
||||
let fixed = jsonStr;
|
||||
|
||||
// Fix common escape character issues
|
||||
fixed = fixed
|
||||
// Fix unescaped quotes in string values
|
||||
.replace(/(\{[^}]*"[^"]*":\s*")([^"]*")([^"]*")([^}]*\})/g, (match, prefix, content, suffix, end) => {
|
||||
const escapedContent = content.replace(/"/g, '\\"');
|
||||
return prefix + escapedContent + suffix + end;
|
||||
})
|
||||
// Fix unescaped backslashes
|
||||
.replace(/([^\\])\\([^"\\\/bfnrtu])/g, '$1\\\\$2')
|
||||
// Fix trailing commas
|
||||
.replace(/,(\s*[}\]])/g, '$1')
|
||||
// Fix control characters
|
||||
.replace(/\n/g, '\\n')
|
||||
.replace(/\r/g, '\\r')
|
||||
.replace(/\t/g, '\\t')
|
||||
// Fix single quotes to double quotes
|
||||
.replace(/'/g, '"');
|
||||
|
||||
return fixed;
|
||||
}
|
||||
|
||||
private escapJsonString(str: string): string {
|
||||
return str
|
||||
.replace(/\\/g, '\\\\') // Escape backslashes first
|
||||
.replace(/"/g, '\\"') // Escape quotes
|
||||
.replace(/\n/g, '\\n') // Escape newlines
|
||||
.replace(/\r/g, '\\r') // Escape carriage returns
|
||||
.replace(/\t/g, '\\t') // Escape tabs
|
||||
.replace(/\f/g, '\\f') // Escape form feeds
|
||||
.replace(/\b/g, '\\b'); // Escape backspaces
|
||||
}
|
||||
|
||||
private validateAgainstSchema(data: any, schema: any): { valid: boolean; errors: string[]; suggestions: string[] } {
|
||||
const errors: string[] = [];
|
||||
const suggestions: string[] = [];
|
||||
|
||||
// Basic type checking
|
||||
if (schema.type) {
|
||||
const actualType = Array.isArray(data) ? 'array' : typeof data;
|
||||
if (actualType !== schema.type) {
|
||||
errors.push(`Expected type ${schema.type}, got ${actualType}`);
|
||||
suggestions.push(`Convert value to ${schema.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Required fields checking
|
||||
if (schema.required && Array.isArray(schema.required)) {
|
||||
for (const field of schema.required) {
|
||||
if (!(field in data)) {
|
||||
errors.push(`Missing required field: ${field}`);
|
||||
suggestions.push(`Add required field "${field}"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors,
|
||||
suggestions
|
||||
};
|
||||
}
|
||||
|
||||
private getJsonFixSuggestions(jsonStr: string): string[] {
|
||||
const suggestions: string[] = [];
|
||||
|
||||
if (jsonStr.includes('\\"')) {
|
||||
suggestions.push('Check for improperly escaped quotes');
|
||||
}
|
||||
if (jsonStr.includes("'")) {
|
||||
suggestions.push('Replace single quotes with double quotes');
|
||||
}
|
||||
if (jsonStr.includes('\n') || jsonStr.includes('\t')) {
|
||||
suggestions.push('Escape newlines and tabs properly');
|
||||
}
|
||||
if (jsonStr.match(/,\s*[}\]]/)) {
|
||||
suggestions.push('Remove trailing commas');
|
||||
}
|
||||
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
private generateCurlCommand(jsonStr: string): string {
|
||||
const escapedJson = jsonStr.replace(/'/g, "'\"'\"'");
|
||||
return `curl -X POST http://127.0.0.1:8585/mcp \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '${escapedJson}'`;
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,9 @@ export interface ToolResponse {
|
||||
message?: string;
|
||||
error?: string;
|
||||
instruction?: string;
|
||||
warning?: string;
|
||||
verificationData?: any;
|
||||
updatedProperties?: string[];
|
||||
}
|
||||
|
||||
export interface NodeInfo {
|
||||
|
||||
Reference in New Issue
Block a user