feat: 完成第二阶段开发\n\n- 添加 scene_management 工具\n- 添加 prefab_management 工具\n- 优化面板布局和响应式设计\n- 添加滚动条支持\n- 移除旧的 create_scene 和 create_prefab 工具\n- 更新 README 文档
This commit is contained in:
43
README.md
43
README.md
@@ -103,26 +103,13 @@ Args: [你的项目所在盘符]:/[项目路径]/packages/mcp-bridge/mcp-proxy.j
|
|||||||
- `scaleX`, `scaleY`: 缩放值
|
- `scaleX`, `scaleY`: 缩放值
|
||||||
- `color`: HEX 颜色代码(如 #FF0000)
|
- `color`: HEX 颜色代码(如 #FF0000)
|
||||||
|
|
||||||
### 6. create_scene
|
### 6. open_scene
|
||||||
|
|
||||||
- **描述**: 在 assets 目录下创建一个新的场景文件
|
|
||||||
- **参数**:
|
|
||||||
- `sceneName`: 场景名称
|
|
||||||
|
|
||||||
### 7. create_prefab
|
|
||||||
|
|
||||||
- **描述**: 将场景中的某个节点保存为预制体资源
|
|
||||||
- **参数**:
|
|
||||||
- `nodeId`: 节点 UUID
|
|
||||||
- `prefabName`: 预制体名称
|
|
||||||
|
|
||||||
### 8. open_scene
|
|
||||||
|
|
||||||
- **描述**: 在编辑器中打开指定的场景文件
|
- **描述**: 在编辑器中打开指定的场景文件
|
||||||
- **参数**:
|
- **参数**:
|
||||||
- `url`: 场景资源路径,如 `db://assets/NewScene.fire`
|
- `url`: 场景资源路径,如 `db://assets/NewScene.fire`
|
||||||
|
|
||||||
### 9. create_node
|
### 7. create_node
|
||||||
|
|
||||||
- **描述**: 在当前场景中创建一个新节点
|
- **描述**: 在当前场景中创建一个新节点
|
||||||
- **参数**:
|
- **参数**:
|
||||||
@@ -130,7 +117,7 @@ Args: [你的项目所在盘符]:/[项目路径]/packages/mcp-bridge/mcp-proxy.j
|
|||||||
- `parentId`: 父节点 UUID (可选,不传则挂在场景根部)
|
- `parentId`: 父节点 UUID (可选,不传则挂在场景根部)
|
||||||
- `type`: 节点预设类型(`empty`, `sprite`, `label`, `canvas`)
|
- `type`: 节点预设类型(`empty`, `sprite`, `label`, `canvas`)
|
||||||
|
|
||||||
### 10. manage_components
|
### 8. manage_components
|
||||||
|
|
||||||
- **描述**: 管理节点组件
|
- **描述**: 管理节点组件
|
||||||
- **参数**:
|
- **参数**:
|
||||||
@@ -140,7 +127,7 @@ Args: [你的项目所在盘符]:/[项目路径]/packages/mcp-bridge/mcp-proxy.j
|
|||||||
- `componentId`: 组件 ID(用于 `remove` 操作)
|
- `componentId`: 组件 ID(用于 `remove` 操作)
|
||||||
- `properties`: 组件属性(用于 `add` 操作)
|
- `properties`: 组件属性(用于 `add` 操作)
|
||||||
|
|
||||||
### 11. manage_script
|
### 9. manage_script
|
||||||
|
|
||||||
- **描述**: 管理脚本文件,默认创建 TypeScript 脚本
|
- **描述**: 管理脚本文件,默认创建 TypeScript 脚本
|
||||||
- **参数**:
|
- **参数**:
|
||||||
@@ -150,7 +137,7 @@ Args: [你的项目所在盘符]:/[项目路径]/packages/mcp-bridge/mcp-proxy.j
|
|||||||
- `name`: 脚本名称(用于 `create` 操作)
|
- `name`: 脚本名称(用于 `create` 操作)
|
||||||
- **默认模板**: 当未提供 content 时,会使用 TypeScript 格式的默认模板
|
- **默认模板**: 当未提供 content 时,会使用 TypeScript 格式的默认模板
|
||||||
|
|
||||||
### 12. batch_execute
|
### 10. batch_execute
|
||||||
|
|
||||||
- **描述**: 批处理执行多个操作
|
- **描述**: 批处理执行多个操作
|
||||||
- **参数**:
|
- **参数**:
|
||||||
@@ -158,7 +145,7 @@ Args: [你的项目所在盘符]:/[项目路径]/packages/mcp-bridge/mcp-proxy.j
|
|||||||
- `tool`: 工具名称
|
- `tool`: 工具名称
|
||||||
- `params`: 工具参数
|
- `params`: 工具参数
|
||||||
|
|
||||||
### 13. manage_asset
|
### 11. manage_asset
|
||||||
|
|
||||||
- **描述**: 管理资源
|
- **描述**: 管理资源
|
||||||
- **参数**:
|
- **参数**:
|
||||||
@@ -167,6 +154,24 @@ Args: [你的项目所在盘符]:/[项目路径]/packages/mcp-bridge/mcp-proxy.j
|
|||||||
- `targetPath`: 目标路径(用于 `move` 操作)
|
- `targetPath`: 目标路径(用于 `move` 操作)
|
||||||
- `content`: 资源内容(用于 `create` 操作)
|
- `content`: 资源内容(用于 `create` 操作)
|
||||||
|
|
||||||
|
### 12. scene_management
|
||||||
|
|
||||||
|
- **描述**: 场景管理
|
||||||
|
- **参数**:
|
||||||
|
- `action`: 操作类型(`create`, `delete`, `duplicate`, `get_info`)
|
||||||
|
- `path`: 场景路径,如 `db://assets/scenes/NewScene.fire`
|
||||||
|
- `targetPath`: 目标路径(用于 `duplicate` 操作)
|
||||||
|
- `name`: 场景名称(用于 `create` 操作)
|
||||||
|
|
||||||
|
### 13. prefab_management
|
||||||
|
|
||||||
|
- **描述**: 预制体管理
|
||||||
|
- **参数**:
|
||||||
|
- `action`: 操作类型(`create`, `update`, `instantiate`, `get_info`)
|
||||||
|
- `path`: 预制体路径,如 `db://assets/prefabs/NewPrefab.prefab`
|
||||||
|
- `nodeId`: 节点 ID(用于 `create` 和 `update` 操作)
|
||||||
|
- `parentId`: 父节点 ID(用于 `instantiate` 操作)
|
||||||
|
|
||||||
## 技术实现
|
## 技术实现
|
||||||
|
|
||||||
### 架构设计
|
### 架构设计
|
||||||
|
|||||||
177
main.js
177
main.js
@@ -5,6 +5,7 @@ const path = require("path");
|
|||||||
|
|
||||||
let logBuffer = []; // 存储所有日志
|
let logBuffer = []; // 存储所有日志
|
||||||
let mcpServer = null;
|
let mcpServer = null;
|
||||||
|
let isSceneBusy = false;
|
||||||
let serverConfig = {
|
let serverConfig = {
|
||||||
port: 3456,
|
port: 3456,
|
||||||
active: false,
|
active: false,
|
||||||
@@ -230,9 +231,36 @@ const getToolsList = () => {
|
|||||||
required: ["action", "path"],
|
required: ["action", "path"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "scene_management",
|
||||||
|
description: "场景管理",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
action: { type: "string", enum: ["create", "delete", "duplicate", "get_info"], description: "操作类型" },
|
||||||
|
path: { type: "string", description: "场景路径,如 db://assets/scenes/NewScene.fire" },
|
||||||
|
targetPath: { type: "string", description: "目标路径 (用于 duplicate 操作)" },
|
||||||
|
name: { type: "string", description: "场景名称 (用于 create 操作)" },
|
||||||
|
},
|
||||||
|
required: ["action", "path"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "prefab_management",
|
||||||
|
description: "预制体管理",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
action: { type: "string", enum: ["create", "update", "instantiate", "get_info"], description: "操作类型" },
|
||||||
|
path: { type: "string", description: "预制体路径,如 db://assets/prefabs/NewPrefab.prefab" },
|
||||||
|
nodeId: { type: "string", description: "节点 ID (用于 create 操作)" },
|
||||||
|
parentId: { type: "string", description: "父节点 ID (用于 instantiate 操作)" },
|
||||||
|
},
|
||||||
|
required: ["action", "path"],
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
let isSceneBusy = false;
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
"scene-script": "scene-script.js",
|
"scene-script": "scene-script.js",
|
||||||
@@ -441,6 +469,14 @@ module.exports = {
|
|||||||
this.manageAsset(args, callback);
|
this.manageAsset(args, callback);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "scene_management":
|
||||||
|
this.sceneManagement(args, callback);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "prefab_management":
|
||||||
|
this.prefabManagement(args, callback);
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
callback(`Unknown tool: ${name}`);
|
callback(`Unknown tool: ${name}`);
|
||||||
break;
|
break;
|
||||||
@@ -592,9 +628,142 @@ export default class NewScript extends cc.Component {
|
|||||||
default:
|
default:
|
||||||
callback(`Unknown asset action: ${action}`);
|
callback(`Unknown asset action: ${action}`);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// 暴露给 MCP 或面板的 API 封装
|
|
||||||
|
// 场景管理
|
||||||
|
sceneManagement(args, callback) {
|
||||||
|
const { action, path, targetPath, name } = args;
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case "create":
|
||||||
|
if (Editor.assetdb.exists(path)) {
|
||||||
|
return callback(`Scene already exists at ${path}`);
|
||||||
|
}
|
||||||
|
// 确保父目录存在
|
||||||
|
const fs = require('fs');
|
||||||
|
const pathModule = require('path');
|
||||||
|
const absolutePath = Editor.assetdb.urlToFspath(path);
|
||||||
|
const dirPath = pathModule.dirname(absolutePath);
|
||||||
|
if (!fs.existsSync(dirPath)) {
|
||||||
|
fs.mkdirSync(dirPath, { recursive: true });
|
||||||
|
}
|
||||||
|
Editor.assetdb.create(path, getNewSceneTemplate(), (err) => {
|
||||||
|
callback(err, err ? null : `Scene created at ${path}`);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "delete":
|
||||||
|
if (!Editor.assetdb.exists(path)) {
|
||||||
|
return callback(`Scene not found at ${path}`);
|
||||||
|
}
|
||||||
|
Editor.assetdb.delete([path], (err) => {
|
||||||
|
callback(err, err ? null : `Scene deleted at ${path}`);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "duplicate":
|
||||||
|
if (!Editor.assetdb.exists(path)) {
|
||||||
|
return callback(`Scene not found at ${path}`);
|
||||||
|
}
|
||||||
|
if (!targetPath) {
|
||||||
|
return callback(`Target path is required for duplicate operation`);
|
||||||
|
}
|
||||||
|
if (Editor.assetdb.exists(targetPath)) {
|
||||||
|
return callback(`Target scene already exists at ${targetPath}`);
|
||||||
|
}
|
||||||
|
// 读取原场景内容
|
||||||
|
Editor.assetdb.loadAny(path, (err, content) => {
|
||||||
|
if (err) {
|
||||||
|
return callback(`Failed to read scene: ${err}`);
|
||||||
|
}
|
||||||
|
// 确保目标目录存在
|
||||||
|
const fs = require('fs');
|
||||||
|
const pathModule = require('path');
|
||||||
|
const targetAbsolutePath = Editor.assetdb.urlToFspath(targetPath);
|
||||||
|
const targetDirPath = pathModule.dirname(targetAbsolutePath);
|
||||||
|
if (!fs.existsSync(targetDirPath)) {
|
||||||
|
fs.mkdirSync(targetDirPath, { recursive: true });
|
||||||
|
}
|
||||||
|
// 创建复制的场景
|
||||||
|
Editor.assetdb.create(targetPath, content, (err) => {
|
||||||
|
callback(err, err ? null : `Scene duplicated from ${path} to ${targetPath}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "get_info":
|
||||||
|
Editor.assetdb.queryInfoByUuid(Editor.assetdb.urlToUuid(path), (err, info) => {
|
||||||
|
callback(err, err ? null : info);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
callback(`Unknown scene action: ${action}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 预制体管理
|
||||||
|
prefabManagement(args, callback) {
|
||||||
|
const { action, path, nodeId, parentId } = args;
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case "create":
|
||||||
|
if (!nodeId) {
|
||||||
|
return callback(`Node ID is required for create operation`);
|
||||||
|
}
|
||||||
|
if (Editor.assetdb.exists(path)) {
|
||||||
|
return callback(`Prefab already exists at ${path}`);
|
||||||
|
}
|
||||||
|
// 确保父目录存在
|
||||||
|
const fs = require('fs');
|
||||||
|
const pathModule = require('path');
|
||||||
|
const absolutePath = Editor.assetdb.urlToFspath(path);
|
||||||
|
const dirPath = pathModule.dirname(absolutePath);
|
||||||
|
if (!fs.existsSync(dirPath)) {
|
||||||
|
fs.mkdirSync(dirPath, { recursive: true });
|
||||||
|
}
|
||||||
|
// 从节点创建预制体
|
||||||
|
Editor.Ipc.sendToMain("scene:create-prefab", nodeId, path);
|
||||||
|
callback(null, `Command sent: Creating prefab from node ${nodeId} at ${path}`);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "update":
|
||||||
|
if (!nodeId) {
|
||||||
|
return callback(`Node ID is required for update operation`);
|
||||||
|
}
|
||||||
|
if (!Editor.assetdb.exists(path)) {
|
||||||
|
return callback(`Prefab not found at ${path}`);
|
||||||
|
}
|
||||||
|
// 更新预制体
|
||||||
|
Editor.Ipc.sendToMain("scene:update-prefab", nodeId, path);
|
||||||
|
callback(null, `Command sent: Updating prefab ${path} from node ${nodeId}`);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "instantiate":
|
||||||
|
if (!Editor.assetdb.exists(path)) {
|
||||||
|
return callback(`Prefab not found at ${path}`);
|
||||||
|
}
|
||||||
|
// 实例化预制体
|
||||||
|
Editor.Scene.callSceneScript("mcp-bridge", "instantiate-prefab", {
|
||||||
|
prefabPath: path,
|
||||||
|
parentId: parentId
|
||||||
|
}, callback);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "get_info":
|
||||||
|
Editor.assetdb.queryInfoByUuid(Editor.assetdb.urlToUuid(path), (err, info) => {
|
||||||
|
callback(err, err ? null : info);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
callback(`Unknown prefab action: ${action}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 暴露给 MCP 或面板的 API 封装
|
||||||
messages: {
|
messages: {
|
||||||
"open-test-panel"() {
|
"open-test-panel"() {
|
||||||
Editor.Panel.open("mcp-bridge");
|
Editor.Panel.open("mcp-bridge");
|
||||||
|
|||||||
430
panel/index.html
430
panel/index.html
@@ -1,359 +1,93 @@
|
|||||||
<div class="mcp-container">
|
<div class="mcp-container">
|
||||||
<!-- 标签页 -->
|
<div class="tabs">
|
||||||
<div class="tabs">
|
<ui-button id="tabMain" class="tab-button active">Main</ui-button>
|
||||||
<ui-button id="tabMain" class="tab-button active">Main</ui-button>
|
<ui-button id="tabTest" class="tab-button">Tool Test</ui-button>
|
||||||
<ui-button id="tabTest" class="tab-button">Tool Test</ui-button>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 主面板内容 -->
|
<div id="panelMain" class="tab-content active">
|
||||||
<div id="panelMain" class="tab-content active">
|
<div class="toolbar">
|
||||||
<div class="toolbar">
|
<div class="ctrl-group">
|
||||||
<div class="ctrl-group">
|
<span>Port:</span>
|
||||||
<span>Port:</span>
|
<ui-input id="portInput" style="width: 60px;"></ui-input>
|
||||||
<ui-input id="portInput" value="3456"></ui-input>
|
<ui-button id="btnToggle">Start</ui-button>
|
||||||
<ui-button id="btnToggle" class="green">Start</ui-button>
|
</div>
|
||||||
</div>
|
<div class="ctrl-group" style="margin-left: 10px">
|
||||||
|
<ui-checkbox id="autoStartCheck">Auto Start</ui-checkbox>
|
||||||
|
</div>
|
||||||
|
<div class="spacer"></div>
|
||||||
|
<ui-button id="btnClear" class="transparent">Clear</ui-button>
|
||||||
|
<ui-button id="btnCopy" class="transparent">Copy All</ui-button>
|
||||||
|
</div>
|
||||||
|
<div id="logConsole" class="log-view"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 新增的自动启动勾选框 -->
|
<div id="panelTest" class="tab-content">
|
||||||
<div class="ctrl-group" style="margin-left: 15px">
|
<div class="test-layout">
|
||||||
<ui-checkbox id="autoStartCheck">Auto Start</ui-checkbox>
|
<div class="left-panel" id="testLeftPanel">
|
||||||
</div>
|
<div class="form-item">
|
||||||
|
<label>工具名称:</label>
|
||||||
|
<ui-input id="toolName" placeholder="选中下方列表填充"></ui-input>
|
||||||
|
</div>
|
||||||
|
<label>可用工具列表:</label>
|
||||||
|
<div class="tools-list" id="toolsList"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="spacer"></div>
|
<!-- 拖动条 -->
|
||||||
<ui-button id="btnClear" class="transparent">Clear</ui-button>
|
<div class="resizer" id="testResizer"></div>
|
||||||
<ui-button id="btnCopy" class="transparent">Copy All</ui-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 日志区域 -->
|
<div class="right-panel">
|
||||||
<div id="logConsole" class="log-view"></div>
|
<div class="flex-v">
|
||||||
</div>
|
<label>工具参数 (JSON):</label>
|
||||||
|
<textarea id="toolParams" spellcheck="false" placeholder='{}'></textarea>
|
||||||
<!-- 测试面板内容 -->
|
</div>
|
||||||
<div id="panelTest" class="tab-content">
|
<div class="button-group">
|
||||||
<div class="test-container">
|
<ui-button id="testBtn" class="green">测试工具</ui-button>
|
||||||
<div class="test-layout">
|
<ui-button id="listToolsBtn">刷新列表</ui-button>
|
||||||
<!-- 左侧工具列表 -->
|
<ui-button id="clearTestBtn">清空结果</ui-button>
|
||||||
<div class="left-panel">
|
</div>
|
||||||
<div class="tool-name-section">
|
<div class="flex-v">
|
||||||
<label for="toolName">工具名称:</label>
|
<label>测试结果:</label>
|
||||||
<ui-input id="toolName" placeholder="例如: manage_components"></ui-input>
|
<textarea id="resultContent" readonly spellcheck="false"></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div class="tools-list" id="toolsList"></div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<!-- 右侧输入输出 -->
|
|
||||||
<div class="right-panel">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="toolParams">工具参数 (JSON格式):</label>
|
|
||||||
<textarea id="toolParams" placeholder="例如: {
|
|
||||||
\"nodeId\": \"节点UUID\",
|
|
||||||
\"action\": \"add\",
|
|
||||||
\"componentType\": \"cc.Button\"
|
|
||||||
}"></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="button-group">
|
|
||||||
<ui-button id="testBtn" class="primary">测试工具</ui-button>
|
|
||||||
<ui-button id="listToolsBtn" class="secondary">获取工具列表</ui-button>
|
|
||||||
<ui-button id="clearBtn" class="secondary">清空结果</ui-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="result">
|
|
||||||
<h2>测试结果:</h2>
|
|
||||||
<textarea id="resultContent" placeholder="点击\"测试工具\"按钮开始测试"></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
:host {
|
:host { height: 100%; display: flex; background-color: #2d2d2d; overflow: hidden; }
|
||||||
height: 100%;
|
.mcp-container { display: flex; flex-direction: column; width: 100%; height: 100%; padding: 5px; box-sizing: border-box; }
|
||||||
display: flex;
|
.tabs { display: flex; border-bottom: 1px solid #444; margin-bottom: 5px; flex-shrink: 0; }
|
||||||
background-color: #2d2d2d;
|
.tab-button { padding: 4px 12px; margin-right: 2px; background: #333; }
|
||||||
}
|
.tab-button.active { background: #4CAF50; color: white; }
|
||||||
.mcp-container {
|
.tab-content { display: none; flex: 1; flex-direction: column; min-height: 0; overflow: hidden; }
|
||||||
display: flex;
|
.tab-content.active { display: flex; }
|
||||||
flex-direction: column;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
padding: 5px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 标签页样式 */
|
/* Main Panel */
|
||||||
.tabs {
|
.toolbar { display: flex; align-items: center; padding: 5px 0; gap: 5px; flex-shrink: 0; }
|
||||||
display: flex;
|
.spacer { flex: 1; }
|
||||||
border-bottom: 1px solid #444;
|
.log-view { flex: 1; background: #1a1a1a; margin-top: 5px; overflow-y: auto; padding: 8px; font-family: monospace; font-size: 11px; -webkit-user-select: text; min-height: 0; }
|
||||||
margin-bottom: 10px;
|
.log-item { border-left: 4px solid #555; padding-left: 8px; margin-bottom: 3px; white-space: pre-wrap; word-break: break-all; }
|
||||||
flex-shrink: 0;
|
.log-item.info { border-left-color: #61afef; color: #abb2bf; }
|
||||||
}
|
.log-item.success { border-left-color: #98c379; color: #98c379; }
|
||||||
.tab-button {
|
.log-item.warn { border-left-color: #e5c07b; color: #e5c07b; }
|
||||||
padding: 8px 16px;
|
.log-item.error { border-left-color: #e06c75; color: #e06c75; }
|
||||||
margin-right: 2px;
|
.log-item.mcp { border-left-color: #c678dd; color: #d19a66; background: rgba(198, 120, 221, 0.05); }
|
||||||
border: none;
|
.time { color: #5c6370; margin-right: 8px; }
|
||||||
background-color: #333;
|
|
||||||
color: #ccc;
|
|
||||||
cursor: pointer;
|
|
||||||
border-bottom: 2px solid transparent;
|
|
||||||
}
|
|
||||||
.tab-button.active {
|
|
||||||
background-color: #2d2d2d;
|
|
||||||
color: #fff;
|
|
||||||
border-bottom-color: #4CAF50;
|
|
||||||
}
|
|
||||||
.tab-content {
|
|
||||||
display: none;
|
|
||||||
flex: 1;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
.tab-content.active {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 主面板样式 */
|
/* Test Panel */
|
||||||
.toolbar {
|
.test-layout { display: flex; flex: 1; min-height: 0; }
|
||||||
display: flex;
|
.left-panel { width: 250px; min-width: 150px; max-width: 500px; display: flex; flex-direction: column; flex-shrink: 0; min-height: 0; }
|
||||||
align-items: center;
|
.resizer { width: 6px; cursor: col-resize; background: #1a1a1a; flex-shrink: 0; }
|
||||||
padding-bottom: 5px;
|
.resizer:hover { background: #4CAF50; }
|
||||||
border-bottom: 1px solid #444;
|
.right-panel { flex: 1; display: flex; flex-direction: column; min-width: 0; padding-left: 5px; }
|
||||||
flex-shrink: 0; /* 禁止头部压缩 */
|
.tools-list { flex: 1; background: #222; border: 1px solid #444; overflow-y: auto; margin-top: 5px; }
|
||||||
}
|
.tool-item { padding: 6px; border-bottom: 1px solid #333; cursor: pointer; font-size: 11px; }
|
||||||
.ctrl-group {
|
.tool-item:hover { background: #444; }
|
||||||
display: flex;
|
.flex-v { display: flex; flex-direction: column; flex: 1; min-height: 0; }
|
||||||
align-items: center;
|
textarea { width: 100%; background: #222; color: #ccc; border: 1px solid #444; padding: 5px; font-family: monospace; resize: none; }
|
||||||
gap: 5px;
|
#toolParams { height: 120px; flex-shrink: 0; }
|
||||||
}
|
#resultContent { flex: 1; }
|
||||||
.spacer {
|
.button-group { display: flex; gap: 5px; padding: 5px 0; }
|
||||||
flex-grow: 1;
|
label { font-size: 11px; color: #888; margin: 4px 0; }
|
||||||
}
|
|
||||||
|
|
||||||
.log-view {
|
|
||||||
flex: 1; /* 自动撑满剩余空间 */
|
|
||||||
background: #1a1a1a;
|
|
||||||
margin-top: 5px;
|
|
||||||
border-radius: 2px;
|
|
||||||
overflow-y: auto;
|
|
||||||
overflow-x: hidden;
|
|
||||||
padding: 8px;
|
|
||||||
font-family: "Consolas", "Monaco", monospace;
|
|
||||||
font-size: 12px;
|
|
||||||
|
|
||||||
/* 【关键】允许鼠标选中文字 */
|
|
||||||
-webkit-user-select: text;
|
|
||||||
cursor: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
.log-item {
|
|
||||||
margin-bottom: 3px;
|
|
||||||
line-height: 1.4;
|
|
||||||
border-left: 4px solid #444;
|
|
||||||
padding-left: 8px;
|
|
||||||
word-wrap: break-word;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 颜色修正 */
|
|
||||||
.time {
|
|
||||||
color: #5c6370;
|
|
||||||
margin-right: 8px;
|
|
||||||
font-weight: normal;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
.info {
|
|
||||||
border-left-color: #61afef;
|
|
||||||
color: #abb2bf;
|
|
||||||
}
|
|
||||||
.success {
|
|
||||||
border-left-color: #98c379;
|
|
||||||
color: #98c379;
|
|
||||||
}
|
|
||||||
.warn {
|
|
||||||
border-left-color: #e5c07b;
|
|
||||||
color: #e5c07b;
|
|
||||||
}
|
|
||||||
.error {
|
|
||||||
border-left-color: #e06c75;
|
|
||||||
color: #e06c75;
|
|
||||||
}
|
|
||||||
.mcp {
|
|
||||||
border-left-color: #c678dd;
|
|
||||||
color: #d19a66;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 测试面板样式 */
|
|
||||||
.test-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100%;
|
|
||||||
padding: 10px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.test-layout {
|
|
||||||
display: flex;
|
|
||||||
height: 100%;
|
|
||||||
gap: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.left-panel {
|
|
||||||
width: 300px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100%;
|
|
||||||
background-color: #333;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.right-panel {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 15px;
|
|
||||||
height: 100%;
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.left-panel .tool-name-section {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.left-panel .tools-list {
|
|
||||||
flex: 1;
|
|
||||||
border: 1px solid #444;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 5px;
|
|
||||||
background-color: #222;
|
|
||||||
overflow-y: auto;
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.left-panel label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
color: #ccc;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.left-panel ui-input {
|
|
||||||
width: 100%;
|
|
||||||
padding: 8px;
|
|
||||||
border: 1px solid #444;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
background-color: #222;
|
|
||||||
color: #fff;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.right-panel .form-group {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.right-panel .result {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
min-height: 0;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.right-panel .result textarea {
|
|
||||||
flex: 1;
|
|
||||||
min-height: 100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.right-panel .button-group {
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
.form-group {
|
|
||||||
margin-bottom: 15px;
|
|
||||||
background-color: #333;
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
.form-group label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
color: #ccc;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
.form-group ui-input,
|
|
||||||
.form-group textarea {
|
|
||||||
width: 100%;
|
|
||||||
padding: 8px;
|
|
||||||
border: 1px solid #444;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
background-color: #222;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
.form-group textarea {
|
|
||||||
height: 150px;
|
|
||||||
resize: vertical;
|
|
||||||
font-family: 'Courier New', monospace;
|
|
||||||
}
|
|
||||||
.tool-item {
|
|
||||||
padding: 5px;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
background-color: #333;
|
|
||||||
border-radius: 3px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #ccc;
|
|
||||||
}
|
|
||||||
.tool-item:hover {
|
|
||||||
background-color: #444;
|
|
||||||
}
|
|
||||||
.button-group {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
.button-group ui-button {
|
|
||||||
padding: 8px 16px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.button-group ui-button.primary {
|
|
||||||
background-color: #4CAF50;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
.button-group ui-button.secondary {
|
|
||||||
background-color: #2196F3;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
.result {
|
|
||||||
flex: 1;
|
|
||||||
background-color: #333;
|
|
||||||
padding: 15px;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
.result h2 {
|
|
||||||
color: #ccc;
|
|
||||||
font-size: 14px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
.result textarea {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background-color: #222;
|
|
||||||
color: #fff;
|
|
||||||
border: 1px solid #444;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 10px;
|
|
||||||
font-family: 'Courier New', monospace;
|
|
||||||
font-size: 12px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
resize: vertical;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
451
panel/index.js
451
panel/index.js
@@ -1,333 +1,152 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
|
|
||||||
Editor.Panel.extend({
|
Editor.Panel.extend({
|
||||||
style: fs.readFileSync(Editor.url("packages://mcp-bridge/panel/index.html"), "utf-8"),
|
style: fs.readFileSync(Editor.url("packages://mcp-bridge/panel/index.html"), "utf-8"),
|
||||||
template: fs.readFileSync(Editor.url("packages://mcp-bridge/panel/index.html"), "utf-8"),
|
template: fs.readFileSync(Editor.url("packages://mcp-bridge/panel/index.html"), "utf-8"),
|
||||||
|
|
||||||
messages: {
|
messages: {
|
||||||
"mcp-bridge:on-log"(event, log) {
|
"mcp-bridge:on-log"(event, log) { this.renderLog(log); },
|
||||||
this.renderLog(log);
|
"mcp-bridge:state-changed"(event, config) { this.updateUI(config.active); }
|
||||||
},
|
},
|
||||||
"mcp-bridge:state-changed"(event, config) {
|
|
||||||
this.updateUI(config.active);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
ready() {
|
ready() {
|
||||||
const portInput = this.shadowRoot.querySelector("#portInput");
|
const root = this.shadowRoot;
|
||||||
const btnToggle = this.shadowRoot.querySelector("#btnToggle");
|
// 获取元素
|
||||||
const autoStartCheck = this.shadowRoot.querySelector("#autoStartCheck");
|
const els = {
|
||||||
const btnClear = this.shadowRoot.querySelector("#btnClear");
|
port: root.querySelector("#portInput"),
|
||||||
const btnCopy = this.shadowRoot.querySelector("#btnCopy");
|
btnToggle: root.querySelector("#btnToggle"),
|
||||||
const logView = this.shadowRoot.querySelector("#logConsole");
|
autoStart: root.querySelector("#autoStartCheck"),
|
||||||
|
logView: root.querySelector("#logConsole"),
|
||||||
|
tabMain: root.querySelector("#tabMain"),
|
||||||
|
tabTest: root.querySelector("#tabTest"),
|
||||||
|
panelMain: root.querySelector("#panelMain"),
|
||||||
|
panelTest: root.querySelector("#panelTest"),
|
||||||
|
toolName: root.querySelector("#toolName"),
|
||||||
|
toolParams: root.querySelector("#toolParams"),
|
||||||
|
toolsList: root.querySelector("#toolsList"),
|
||||||
|
testBtn: root.querySelector("#testBtn"),
|
||||||
|
listBtn: root.querySelector("#listToolsBtn"),
|
||||||
|
clearBtn: root.querySelector("#clearTestBtn"),
|
||||||
|
result: root.querySelector("#resultContent"),
|
||||||
|
left: root.querySelector("#testLeftPanel"),
|
||||||
|
resizer: root.querySelector("#testResizer")
|
||||||
|
};
|
||||||
|
|
||||||
// 标签页元素
|
// 1. 初始化状态
|
||||||
const tabMain = this.shadowRoot.querySelector("#tabMain");
|
Editor.Ipc.sendToMain("mcp-bridge:get-server-state", (err, data) => {
|
||||||
const tabTest = this.shadowRoot.querySelector("#tabTest");
|
if (data) {
|
||||||
const panelMain = this.shadowRoot.querySelector("#panelMain");
|
els.port.value = data.config.port;
|
||||||
const panelTest = this.shadowRoot.querySelector("#panelTest");
|
els.autoStart.value = data.autoStart;
|
||||||
|
this.updateUI(data.config.active);
|
||||||
|
els.logView.innerHTML = "";
|
||||||
|
data.logs.forEach(l => this.renderLog(l));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 测试面板元素
|
// 2. 标签切换
|
||||||
const toolNameInput = this.shadowRoot.querySelector("#toolName");
|
els.tabMain.addEventListener("confirm", () => {
|
||||||
const toolParamsTextarea = this.shadowRoot.querySelector("#toolParams");
|
els.tabMain.classList.add("active"); els.tabTest.classList.remove("active");
|
||||||
const toolsList = this.shadowRoot.querySelector("#toolsList");
|
els.panelMain.classList.add("active"); els.panelTest.classList.remove("active");
|
||||||
const testBtn = this.shadowRoot.querySelector("#testBtn");
|
});
|
||||||
const listToolsBtn = this.shadowRoot.querySelector("#listToolsBtn");
|
els.tabTest.addEventListener("confirm", () => {
|
||||||
const clearBtn = this.shadowRoot.querySelector("#clearBtn");
|
els.tabTest.classList.add("active"); els.tabMain.classList.remove("active");
|
||||||
const resultContent = this.shadowRoot.querySelector("#resultContent");
|
els.panelTest.classList.add("active"); els.panelMain.classList.remove("active");
|
||||||
|
this.fetchTools(els);
|
||||||
|
});
|
||||||
|
|
||||||
let tools = [];
|
// 3. 基础功能
|
||||||
const API_BASE = 'http://localhost:3456';
|
els.btnToggle.addEventListener("confirm", () => {
|
||||||
|
Editor.Ipc.sendToMain("mcp-bridge:toggle-server", parseInt(els.port.value));
|
||||||
|
});
|
||||||
|
root.querySelector("#btnClear").addEventListener("confirm", () => {
|
||||||
|
els.logView.innerHTML = ""; Editor.Ipc.sendToMain("mcp-bridge:clear-logs");
|
||||||
|
});
|
||||||
|
root.querySelector("#btnCopy").addEventListener("confirm", () => {
|
||||||
|
require("electron").clipboard.writeText(els.logView.innerText);
|
||||||
|
Editor.success("Logs Copied");
|
||||||
|
});
|
||||||
|
els.autoStart.addEventListener("change", (e) => {
|
||||||
|
Editor.Ipc.sendToMain("mcp-bridge:set-auto-start", e.target.value);
|
||||||
|
});
|
||||||
|
|
||||||
// 初始化
|
// 4. 测试页功能
|
||||||
Editor.Ipc.sendToMain("mcp-bridge:get-server-state", (err, data) => {
|
els.listBtn.addEventListener("confirm", () => this.fetchTools(els));
|
||||||
if (data) {
|
els.clearBtn.addEventListener("confirm", () => { els.result.value = ""; });
|
||||||
portInput.value = data.config.port;
|
els.testBtn.addEventListener("confirm", () => this.runTest(els));
|
||||||
this.updateUI(data.config.active);
|
|
||||||
data.logs.forEach((log) => this.renderLog(log));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 标签页切换
|
// 5. 【修复】拖拽逻辑
|
||||||
tabMain.addEventListener("confirm", () => {
|
if (els.resizer && els.left) {
|
||||||
tabMain.classList.add("active");
|
els.resizer.addEventListener('mousedown', (e) => {
|
||||||
tabTest.classList.remove("active");
|
e.preventDefault();
|
||||||
panelMain.classList.add("active");
|
const startX = e.clientX;
|
||||||
panelTest.classList.remove("active");
|
const startW = els.left.offsetWidth;
|
||||||
});
|
const onMove = (ev) => { els.left.style.width = (startW + (ev.clientX - startX)) + "px"; };
|
||||||
|
const onUp = () => {
|
||||||
|
document.removeEventListener('mousemove', onMove);
|
||||||
|
document.removeEventListener('mouseup', onUp);
|
||||||
|
document.body.style.cursor = 'default';
|
||||||
|
};
|
||||||
|
document.addEventListener('mousemove', onMove);
|
||||||
|
document.addEventListener('mouseup', onUp);
|
||||||
|
document.body.style.cursor = 'col-resize';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
tabTest.addEventListener("confirm", () => {
|
fetchTools(els) {
|
||||||
tabTest.classList.add("active");
|
const url = `http://localhost:${els.port.value}/list-tools`;
|
||||||
tabMain.classList.remove("active");
|
fetch(url).then(r => r.json()).then(data => {
|
||||||
panelTest.classList.add("active");
|
els.toolsList.innerHTML = "";
|
||||||
panelMain.classList.remove("active");
|
data.tools.forEach(t => {
|
||||||
// 自动获取工具列表
|
const item = document.createElement('div');
|
||||||
this.getToolsList();
|
item.className = 'tool-item';
|
||||||
});
|
item.textContent = t.name;
|
||||||
|
item.onclick = () => {
|
||||||
|
els.toolName.value = t.name;
|
||||||
|
els.toolParams.value = JSON.stringify(this.getExample(t.name), null, 2);
|
||||||
|
};
|
||||||
|
els.toolsList.appendChild(item);
|
||||||
|
});
|
||||||
|
els.result.value = `Loaded ${data.tools.length} tools.`;
|
||||||
|
}).catch(e => { els.result.value = "Error: " + e.message; });
|
||||||
|
},
|
||||||
|
|
||||||
btnToggle.addEventListener("confirm", () => {
|
runTest(els) {
|
||||||
Editor.Ipc.sendToMain("mcp-bridge:toggle-server", parseInt(portInput.value));
|
const url = `http://localhost:${els.port.value}/call-tool`;
|
||||||
});
|
const body = { name: els.toolName.value, arguments: JSON.parse(els.toolParams.value || "{}") };
|
||||||
|
els.result.value = "Testing...";
|
||||||
|
fetch(url, { method: 'POST', body: JSON.stringify(body) })
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(d => { els.result.value = JSON.stringify(d, null, 2); })
|
||||||
|
.catch(e => { els.result.value = "Error: " + e.message; });
|
||||||
|
},
|
||||||
|
|
||||||
btnClear.addEventListener("confirm", () => {
|
getExample(name) {
|
||||||
logView.innerHTML = "";
|
const examples = {
|
||||||
Editor.Ipc.sendToMain("mcp-bridge:clear-logs");
|
"set_node_name": { "id": "UUID", "newName": "Hello" },
|
||||||
});
|
"update_node_transform": { "id": "UUID", "x": 0, "y": 0, "color": "#FF0000" },
|
||||||
|
"create_node": { "name": "Node", "type": "sprite", "parentId": "" },
|
||||||
|
"open_scene": { "url": "db://assets/Scene.fire" }
|
||||||
|
};
|
||||||
|
return examples[name] || {};
|
||||||
|
},
|
||||||
|
|
||||||
btnCopy.addEventListener("confirm", () => {
|
renderLog(log) {
|
||||||
require("electron").clipboard.writeText(logView.innerText);
|
const view = this.shadowRoot.querySelector("#logConsole");
|
||||||
Editor.success("All logs copied!");
|
if (!view) return;
|
||||||
});
|
const atBottom = view.scrollHeight - view.scrollTop <= view.clientHeight + 50;
|
||||||
|
const el = document.createElement("div");
|
||||||
|
el.className = `log-item ${log.type}`;
|
||||||
|
el.innerHTML = `<span class="time">${log.time}</span><span class="msg">${log.content}</span>`;
|
||||||
|
view.appendChild(el);
|
||||||
|
if (atBottom) view.scrollTop = view.scrollHeight;
|
||||||
|
},
|
||||||
|
|
||||||
Editor.Ipc.sendToMain("mcp-bridge:get-server-state", (err, data) => {
|
updateUI(active) {
|
||||||
if (data) {
|
const btn = this.shadowRoot.querySelector("#btnToggle");
|
||||||
portInput.value = data.config.port;
|
if (!btn) return;
|
||||||
this.updateUI(data.config.active);
|
btn.innerText = active ? "Stop" : "Start";
|
||||||
|
btn.style.backgroundColor = active ? "#aa4444" : "#44aa44";
|
||||||
// 设置自动启动复选框状态
|
}
|
||||||
autoStartCheck.value = data.autoStart;
|
|
||||||
|
|
||||||
data.logs.forEach((log) => this.renderLog(log));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
autoStartCheck.addEventListener("change", (event) => {
|
|
||||||
// event.target.value 在 ui-checkbox 中是布尔值
|
|
||||||
Editor.Ipc.sendToMain("mcp-bridge:set-auto-start", event.target.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 测试面板事件
|
|
||||||
testBtn.addEventListener("confirm", () => this.testTool());
|
|
||||||
listToolsBtn.addEventListener("confirm", () => this.getToolsList());
|
|
||||||
clearBtn.addEventListener("confirm", () => this.clearResult());
|
|
||||||
|
|
||||||
// 获取工具列表
|
|
||||||
this.getToolsList = function() {
|
|
||||||
this.showResult('获取工具列表中...');
|
|
||||||
|
|
||||||
fetch(`${API_BASE}/list-tools`)
|
|
||||||
.then(response => {
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
}
|
|
||||||
return response.json();
|
|
||||||
})
|
|
||||||
.then(data => {
|
|
||||||
if (data.tools) {
|
|
||||||
tools = data.tools;
|
|
||||||
this.displayToolsList(tools);
|
|
||||||
this.showResult(`成功获取 ${tools.length} 个工具`, 'success');
|
|
||||||
} else {
|
|
||||||
this.showResult('获取工具列表失败:未找到工具数据', 'error');
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
this.showResult(`获取工具列表失败:${error.message}`, 'error');
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 显示工具列表
|
|
||||||
this.displayToolsList = function(tools) {
|
|
||||||
toolsList.innerHTML = '';
|
|
||||||
|
|
||||||
tools.forEach(tool => {
|
|
||||||
const toolItem = document.createElement('div');
|
|
||||||
toolItem.className = 'tool-item';
|
|
||||||
toolItem.textContent = `${tool.name} - ${tool.description}`;
|
|
||||||
toolItem.addEventListener('click', () => {
|
|
||||||
toolNameInput.value = tool.name;
|
|
||||||
// 尝试填充示例参数
|
|
||||||
this.fillExampleParams(tool);
|
|
||||||
});
|
|
||||||
toolsList.appendChild(toolItem);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 填充示例参数
|
|
||||||
this.fillExampleParams = function(tool) {
|
|
||||||
let exampleParams = {};
|
|
||||||
|
|
||||||
switch (tool.name) {
|
|
||||||
case 'get_selected_node':
|
|
||||||
case 'save_scene':
|
|
||||||
case 'get_scene_hierarchy':
|
|
||||||
exampleParams = {};
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'set_node_name':
|
|
||||||
exampleParams = {
|
|
||||||
"id": "节点UUID",
|
|
||||||
"newName": "新节点名称"
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'update_node_transform':
|
|
||||||
exampleParams = {
|
|
||||||
"id": "节点UUID",
|
|
||||||
"x": 100,
|
|
||||||
"y": 100,
|
|
||||||
"scaleX": 1,
|
|
||||||
"scaleY": 1
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'create_scene':
|
|
||||||
exampleParams = {
|
|
||||||
"sceneName": "NewScene"
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'create_prefab':
|
|
||||||
exampleParams = {
|
|
||||||
"nodeId": "节点UUID",
|
|
||||||
"prefabName": "NewPrefab"
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'open_scene':
|
|
||||||
exampleParams = {
|
|
||||||
"url": "db://assets/NewScene.fire"
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'create_node':
|
|
||||||
exampleParams = {
|
|
||||||
"name": "NewNode",
|
|
||||||
"parentId": "父节点UUID",
|
|
||||||
"type": "empty"
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'manage_components':
|
|
||||||
exampleParams = {
|
|
||||||
"nodeId": "节点UUID",
|
|
||||||
"action": "add",
|
|
||||||
"componentType": "cc.Button"
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'manage_script':
|
|
||||||
exampleParams = {
|
|
||||||
"action": "create",
|
|
||||||
"path": "db://assets/scripts/TestScript.ts",
|
|
||||||
"content": "const { ccclass, property } = cc._decorator;\n\n@ccclass\nexport default class TestScript extends cc.Component {\n // LIFE-CYCLE CALLBACKS:\n\n onLoad () {}\n\n start () {}\n\n update (dt) {}\n}"
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'batch_execute':
|
|
||||||
exampleParams = {
|
|
||||||
"operations": [
|
|
||||||
{
|
|
||||||
"tool": "get_selected_node",
|
|
||||||
"params": {}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'manage_asset':
|
|
||||||
exampleParams = {
|
|
||||||
"action": "create",
|
|
||||||
"path": "db://assets/test.txt",
|
|
||||||
"content": "Hello, MCP!"
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
toolParamsTextarea.value = JSON.stringify(exampleParams, null, 2);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 测试工具
|
|
||||||
this.testTool = function() {
|
|
||||||
const toolName = toolNameInput.value.trim();
|
|
||||||
const toolParamsStr = toolParamsTextarea.value.trim();
|
|
||||||
|
|
||||||
if (!toolName) {
|
|
||||||
this.showResult('请输入工具名称', 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let toolParams;
|
|
||||||
try {
|
|
||||||
toolParams = toolParamsStr ? JSON.parse(toolParamsStr) : {};
|
|
||||||
} catch (error) {
|
|
||||||
this.showResult(`参数格式错误:${error.message}`, 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.showResult('测试工具中...');
|
|
||||||
|
|
||||||
fetch(`${API_BASE}/call-tool`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
name: toolName,
|
|
||||||
arguments: toolParams
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.then(response => {
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
}
|
|
||||||
return response.json();
|
|
||||||
})
|
|
||||||
.then(data => {
|
|
||||||
if (data.error) {
|
|
||||||
this.showResult(`测试失败:${data.error}`, 'error');
|
|
||||||
} else {
|
|
||||||
this.showResult(JSON.stringify(data, null, 2), 'success');
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
this.showResult(`测试失败:${error.message}`, 'error');
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 显示结果
|
|
||||||
this.showResult = function(message, type = 'info') {
|
|
||||||
resultContent.value = message;
|
|
||||||
|
|
||||||
// 移除旧样式
|
|
||||||
resultContent.className = '';
|
|
||||||
|
|
||||||
// 添加新样式
|
|
||||||
if (type === 'error' || type === 'success') {
|
|
||||||
resultContent.className = type;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 清空结果
|
|
||||||
this.clearResult = function() {
|
|
||||||
this.showResult('点击"测试工具"按钮开始测试');
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
renderLog(log) {
|
|
||||||
const logView = this.shadowRoot.querySelector("#logConsole");
|
|
||||||
if (!logView) return;
|
|
||||||
|
|
||||||
// 记录当前滚动条位置
|
|
||||||
const isAtBottom = logView.scrollHeight - logView.scrollTop <= logView.clientHeight + 50;
|
|
||||||
|
|
||||||
const el = document.createElement("div");
|
|
||||||
el.className = `log-item ${log.type}`;
|
|
||||||
el.innerHTML = `<span class="time">${log.time}</span><span class="msg">${log.content}</span>`;
|
|
||||||
logView.appendChild(el);
|
|
||||||
|
|
||||||
// 如果用户正在向上翻看,不自动滚动;否则自动滚到底部
|
|
||||||
if (isAtBottom) {
|
|
||||||
logView.scrollTop = logView.scrollHeight;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
updateUI(isActive) {
|
|
||||||
const btnToggle = this.shadowRoot.querySelector("#btnToggle");
|
|
||||||
if (!btnToggle) return;
|
|
||||||
btnToggle.innerText = isActive ? "Stop" : "Start";
|
|
||||||
btnToggle.style.backgroundColor = isActive ? "#aa4444" : "#44aa44";
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
@@ -268,4 +268,50 @@ module.exports = {
|
|||||||
|
|
||||||
return properties;
|
return properties;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"instantiate-prefab": function (event, args) {
|
||||||
|
const { prefabPath, parentId } = args;
|
||||||
|
const scene = cc.director.getScene();
|
||||||
|
|
||||||
|
if (!scene || !cc.director.getRunningScene()) {
|
||||||
|
if (event.reply) event.reply(new Error("Scene not ready or loading."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载预制体资源
|
||||||
|
cc.loader.loadRes(prefabPath.replace("db://assets/", "").replace(".prefab", ""), cc.Prefab, (err, prefab) => {
|
||||||
|
if (err) {
|
||||||
|
if (event.reply) event.reply(new Error(`Failed to load prefab: ${err.message}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 实例化预制体
|
||||||
|
const instance = cc.instantiate(prefab);
|
||||||
|
if (!instance) {
|
||||||
|
if (event.reply) event.reply(new Error("Failed to instantiate prefab"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置父节点
|
||||||
|
let parent = parentId ? cc.engine.getInstanceById(parentId) : scene;
|
||||||
|
if (parent) {
|
||||||
|
instance.parent = parent;
|
||||||
|
|
||||||
|
// 通知场景变脏
|
||||||
|
Editor.Ipc.sendToMain("scene:dirty");
|
||||||
|
|
||||||
|
// 通知 UI 刷新
|
||||||
|
setTimeout(() => {
|
||||||
|
Editor.Ipc.sendToAll("scene:node-created", {
|
||||||
|
uuid: instance.uuid,
|
||||||
|
parentUuid: parent.uuid,
|
||||||
|
});
|
||||||
|
}, 10);
|
||||||
|
|
||||||
|
if (event.reply) event.reply(null, `Prefab instantiated successfully with UUID: ${instance.uuid}`);
|
||||||
|
} else {
|
||||||
|
if (event.reply) event.reply(new Error("Parent node not found"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user