docs: 更新 README 和 UPDATE_LOG 文档,新增日志持久化与面板修复记录

This commit is contained in:
火焰库拉
2026-02-27 23:04:25 +08:00
parent 9268340af3
commit 002f081290
7 changed files with 4191 additions and 4304 deletions

View File

@@ -1,266 +0,0 @@
---
description:
alwaysApply: true
enabled: true
updatedAt: 2026-02-03T13:55:50.734Z
provider:
---
# MCP Bridge Development Rules
## 1. 语言规范 (Language)
* **强制中文**: 所有的对话回复、代码注释、以及生成的文档都必须使用**中文**。
* **日志消息**: `addLog()` 的消息内容可以使用中英文混合,确保关键术语清晰。
---
## 2. 关键工作流程 (Critical Workflow)
### 2.1 插件重载
- **必须重载**: 修改 `main.js`, `package.json`, `scene-script.js`, 或 `panel/` 后,**必须**在编辑器中执行「扩展 → 刷新」或重启编辑器。
- **热更新不适用**: Cocos Creator 2.x 的插件主进程脚本不支持热更新。
### 2.2 测试驱动
- **测试脚本**: 每个新功能必须在 `test/` 目录下创建独立测试脚本 (如 `test/test_feature.js`)。
- **HTTP 验证**: 测试脚本通过 HTTP 请求直接调用插件 API验证功能正确性。
- **运行前提**: 确保 Cocos Creator 编辑器已打开且 MCP Bridge 服务已启动。
---
## 3. 架构与进程隔离 (Architecture & IPC)
### 3.1 进程职责划分
| 文件 | 进程 | 可访问 | 不可访问 |
|------|------|--------|----------|
| `main.js` | 主进程 (Main) | `Editor.assetdb`, `Editor.Ipc`, `require()` | `cc.*` (Cocos 引擎) |
| `scene-script.js` | 渲染进程 (Renderer) | `cc.*`, `cc.engine`, `cc.director` | `Editor.assetdb`, `Editor.FileSystem` |
### 3.2 跨进程通信规则
```
主进程 (main.js) 渲染进程 (scene-script.js)
│ │
├─ 1. 接收 HTTP 请求 │
├─ 2. 解析 db:// 路径为 UUID │
│ Editor.assetdb.urlToUuid() │
├─ 3. 调用场景脚本 ──────────────────────┤
│ Editor.Scene.callSceneScript() │
│ ├─ 4. 操作节点/组件
│ │ cc.engine.getInstanceById()
│ ├─ 5. 通知场景变脏
│ │ Editor.Ipc.sendToMain("scene:dirty")
└─ 6. 返回结果 ◀────────────────────────┘
```
**核心规则**: 永远在 `main.js` 中将 `db://` 路径转换为 UUID再传递给 `scene-script.js`。
---
## 4. 编码规范 (Coding Standards)
### 4.1 命名规范
| 类型 | 规范 | 示例 |
|------|------|------|
| 函数名 | camelCase | `handleMcpCall`, `manageScript` |
| 常量 | SCREAMING_SNAKE_CASE | `MAX_RESULTS`, `DEFAULT_PORT` |
| 私有变量 | _camelCase | `_isMoving`, `_timer` |
| 布尔变量 | is/has/can 前缀 | `isSceneBusy`, `hasComponent` |
| MCP 工具名 | snake_case | `get_selected_node`, `manage_vfx` |
| IPC 消息名 | kebab-case | `get-hierarchy`, `create-node` |
### 4.2 函数组织顺序
在 `module.exports` 中按以下顺序组织函数:
```javascript
module.exports = {
// 1. 配置属性
"scene-script": "scene-script.js",
// 2. 生命周期函数
load() { },
unload() { },
// 3. 服务器管理
startServer(port) { },
stopServer() { },
// 4. 核心处理逻辑
handleMcpCall(name, args, callback) { },
// 5. 工具函数 (按字母顺序)
applyTextEdits(args, callback) { },
batchExecute(args, callback) { },
// ...
// 6. IPC 消息处理
messages: {
"open-test-panel"() { },
// ...
}
};
```
### 4.3 避免重复定义
> ⚠️ **重要**: `main.js` 已存在重复函数问题,编辑前务必使用 `view_file` 确认上下文,避免创建重复定义。
**检查清单**:
- [ ] 新增函数前,搜索是否已存在同名函数
- [ ] 修改函数时,确认只有一个定义
- [ ] `messages` 对象中避免重复的消息处理器
### 4.4 日志规范
使用 `addLog(type, message)` 替代 `console.log()`
```javascript
// ✅ 正确
addLog("info", "服务启动成功");
addLog("error", `操作失败: ${err.message}`);
addLog("mcp", `REQ -> [${toolName}]`);
addLog("success", `RES <- [${toolName}] 成功`);
// ❌ 错误
console.log("服务启动成功"); // 不会被 read_console 捕获
```
| type | 用途 | 颜色 |
|------|------|------|
| `info` | 一般信息 | 蓝色 |
| `success` | 操作成功 | 绿色 |
| `warn` | 警告信息 | 黄色 |
| `error` | 错误信息 | 红色 |
| `mcp` | MCP 请求/响应 | 紫色 |
---
## 5. 撤销/重做支持 (Undo/Redo)
### 5.1 使用 scene:set-property
对于节点属性修改,优先使用 `scene:set-property` 以获得原生 Undo 支持:
```javascript
// ✅ 支持 Undo
Editor.Ipc.sendToPanel("scene", "scene:set-property", {
id: nodeId,
path: "x",
type: "Float",
value: 100,
isSubProp: false
});
// ❌ 不支持 Undo (直接修改)
node.x = 100;
```
### 5.2 使用 Undo 组
对于复合操作,使用 Undo 组包装:
```javascript
Editor.Ipc.sendToPanel("scene", "scene:undo-record", "Transform Update");
try {
// 执行多个属性修改
Editor.Ipc.sendToPanel("scene", "scene:undo-commit");
} catch (e) {
Editor.Ipc.sendToPanel("scene", "scene:undo-cancel");
}
```
---
## 6. 功能特定规则 (Feature Specifics)
### 6.1 粒子系统 (VFX)
```javascript
// 必须设置 custom = true否则属性修改可能不生效
particleSystem.custom = true;
// 确保纹理有效,否则粒子不可见
if (!particleSystem.texture && !particleSystem.file) {
// 加载默认纹理
}
```
### 6.2 资源路径解析
```javascript
// 内置资源可能需要多个路径尝试
const defaultPaths = [
"db://internal/image/default_sprite_splash",
"db://internal/image/default_sprite_splash.png",
"db://internal/image/default_particle",
"db://internal/image/default_particle.png"
];
for (const path of defaultPaths) {
const uuid = Editor.assetdb.urlToUuid(path);
if (uuid) break;
}
```
### 6.3 场景操作时序
```javascript
// 场景操作后需要延迟通知 UI 刷新
newNode.parent = parent;
Editor.Ipc.sendToMain("scene:dirty");
// 使用 setTimeout 让出主循环
setTimeout(() => {
Editor.Ipc.sendToAll("scene:node-created", {
uuid: newNode.uuid,
parentUuid: parent.uuid,
});
}, 10);
```
---
## 7. 错误处理规范 (Error Handling)
### 7.1 回调风格统一
```javascript
// ✅ 标准风格
callback(null, result); // 成功
callback("Error message"); // 失败 (字符串)
callback(new Error("message")); // 失败 (Error 对象)
// 避免混用
callback(err, null); // 不推荐,保持一致性
```
### 7.2 异步操作错误处理
```javascript
Editor.assetdb.queryInfoByUrl(path, (err, info) => {
if (err) {
addLog("error", `查询资源失败: ${err.message}`);
return callback(`Failed to get info: ${err.message}`);
}
// 继续处理...
});
```
---
## 8. 提交规范 (Git Commit)
使用 [Conventional Commits](https://conventionalcommits.org/) 格式:
| 类型 | 用途 | 示例 |
|------|------|------|
| `feat` | 新功能 | `feat: add manage_vfx tool` |
| `fix` | 修复 bug | `fix: resolve duplicate function in main.js` |
| `docs` | 文档更新 | `docs: add code review report` |
| `refactor` | 重构 | `refactor: split main.js into modules` |
| `test` | 测试 | `test: add material management tests` |
| `chore` | 杂项 | `chore: update dependencies` |

View File

@@ -15,7 +15,7 @@
- **脚本管理**: 创建、删除、读取、写入脚本文件 - **脚本管理**: 创建、删除、读取、写入脚本文件
- **批处理执行**: 批量执行多个 MCP 工具操作,提高效率 - **批处理执行**: 批量执行多个 MCP 工具操作,提高效率
- **资产管理**: 创建、删除、移动、获取资源信息 - **资产管理**: 创建、删除、移动、获取资源信息
- **实时日志**: 提供详细的操作日志记录和展示 - **实时日志**: 提供详细的操作日志记录和展示,支持持久化写入项目内日志文件
- **自动启动**: 支持编辑器启动时自动开启服务 - **自动启动**: 支持编辑器启动时自动开启服务
- **编辑器管理**: 获取和设置选中对象,刷新编辑器 - **编辑器管理**: 获取和设置选中对象,刷新编辑器
- **游戏对象查找**: 根据条件查找场景中的节点 - **游戏对象查找**: 根据条件查找场景中的节点
@@ -28,6 +28,8 @@
- **全局搜索**: 在项目中搜索文本内容 - **全局搜索**: 在项目中搜索文本内容
- **撤销/重做**: 管理编辑器的撤销栈 - **撤销/重做**: 管理编辑器的撤销栈
- **特效管理**: 创建和修改粒子系统 - **特效管理**: 创建和修改粒子系统
- **并发安全**: 指令队列串行化执行,防止编辑器卡死
- **超时保护**: IPC 通信和指令队列均有超时兜底机制
- **工具说明**: 测试面板提供详细的工具描述和参数说明 - **工具说明**: 测试面板提供详细的工具描述和参数说明
## 安装与使用 ## 安装与使用
@@ -71,12 +73,12 @@ Args: [你的项目所在盘符]:/[项目路径]/packages/mcp-bridge/mcp-proxy.j
```json ```json
{ {
"mcpServers": { "mcpServers": {
"cocos-creator": { "cocos-creator": {
"command": "node", "command": "node",
"args": ["[Cocos Creator 项目的绝对路径]/packages/mcp-bridge/mcp-proxy.js"] "args": ["[Cocos Creator 项目的绝对路径]/packages/mcp-bridge/mcp-proxy.js"]
} }
} }
} }
``` ```
@@ -406,13 +408,14 @@ Args: [你的项目所在盘符]:/[项目路径]/packages/mcp-bridge/mcp-proxy.j
### 日志管理 ### 日志管理
插件会通过内置的测试面板MCP Bridge/Open Panel实时记录所有操作的日志包括 插件会通过内置的测试面板MCP Bridge/Open Panel实时记录所有操作的日志并同步持久化写入项目目录 `settings/mcp-bridge.log` 文件,编辑器重启后仍可查阅历史日志。日志记录包括:
- 服务启动/停止状态 - 服务启动/停止状态
- MCP 客户端请求接收(完整包含工具的 `arguments` 参数,超长自动截断) - MCP 客户端请求接收(完整包含工具的 `arguments` 参数,超长自动截断)
- 场景节点树遍历与耗时信息 - 场景节点树遍历与耗时信息
- 工具调用的执行成功/失败状态返回 - 工具调用的执行成功/失败状态返回
- IPC 消息和核心底层报错堆栈 - IPC 消息和核心底层报错堆栈
- 内存保护:日志缓冲区上限 2000 条,超出自动截断旧日志
## 注意事项 ## 注意事项

View File

@@ -192,4 +192,46 @@
- **修复**: 在 `scene-script.js` 层加固了前置拦截规则: - **修复**: 在 `scene-script.js` 层加固了前置拦截规则:
1. **直接拦截节点**: 当检测到传入 `cc.Node``Node` 作为组件类型时直接驳回,并返回富含指导意义的中文提示词(如“请使用 create-node 创建节点”)。 1. **直接拦截节点**: 当检测到传入 `cc.Node``Node` 作为组件类型时直接驳回,并返回富含指导意义的中文提示词(如“请使用 create-node 创建节点”)。
2. **继承链校验**: 提取引擎类定义后,强制要求通过 `cc.js.isChildClassOf` 判断该类必须继承自 `cc.Component`。若不合法则即时截断并提示。 2. **继承链校验**: 提取引擎类定义后,强制要求通过 `cc.js.isChildClassOf` 判断该类必须继承自 `cc.Component`。若不合法则即时截断并提示。
- **价值**: 通过将冰冷的底层异常翻译为手把手教 AI 怎么重试的指导性异常,彻底根治了 AI 在操作组件时乱认对象、反复撞墙的通病。 - **价值**: 通过将冰冷的底层异常翻译为"手把手教 AI 怎么重试"的指导性异常,彻底根治了 AI 在操作组件时乱认对象、反复撞墙的通病。
---
## 日志系统持久化与健壮性优化 (2026-02-27)
### 1. 日志文件持久化
- **问题**: 插件的所有运行日志只保存在内存中(`logBuffer`),编辑器重启后日志全部丢失,无法进行会话级别的问题回溯。
- **优化**: 在 `main.js``addLog` 函数中新增文件写入逻辑。所有日志实时追加写入项目目录下的 `settings/mcp-bridge.log` 文件(懒初始化路径)。
- **实现细节**:
- 新增 `getLogFilePath()` 辅助函数,通过 `Editor.assetdb.urlToFspath` 推导项目根目录,将日志存放在 `settings/` 子目录中。
- 日志格式统一为 `[时间戳] [类型] 内容`,与面板日志保持一致。
- 文件写入使用 `fs.appendFileSync` 同步追加,失败时静默不影响主流程。
### 2. 日志缓冲区内存保护
- **问题**: 长时间运行的编辑器会话中,`logBuffer` 数组无限增长,最终导致内存压力。
- **优化**: 在 `addLog` 中增加上限检查,当日志条数超过 2000 时自动截断旧日志,仅保留最近 1500 条。
### 3. 请求关联计数器 (`_requestCounter`)
- **优化**: 新增全局 `_requestCounter` 变量,为每个 HTTP 请求分配唯一的自增序号,便于在高并发场景下追踪同一请求的完整生命周期(从入队到执行到响应)。
### 4. CommandQueue 兜底超时保护
- **问题**: 原有的 `processNextCommand` 队列机制依赖每个指令主动调用 `done()` 回调来释放队列。如果某个工具函数内部逻辑异常导致 `done()` 未被调用,整个队列将永久停滞。
- **优化**: 在 `enqueueCommand` 中为每个入队指令注册 60 秒兜底超时定时器 (`setTimeout`)。超时后强制释放队列位置并记录错误日志 `[CommandQueue] 指令执行超时(60s),强制释放队列`,确保后续指令不被阻塞。
- **正常路径**: 指令正常完成时通过 `clearTimeout` 取消定时器,无额外开销。
### 5. 日志仅输出关键信息到编辑器控制台
- **优化**: `addLog` 函数不再将所有类型的日志输出到编辑器控制台,仅 `error``warn` 级别日志通过 `Editor.error()` / `Editor.warn()` 输出,防止 `info` / `success` / `mcp` 类型日志刷屏干扰开发者。
---
## 面板加载修复 (2026-02-24)
### 1. `panel/index.js` 语法错误修复
- **问题**: 面板加载时出现 `SyntaxError: Invalid or unexpected token`,导致 MCP Bridge 插件面板完全无法渲染。
- **原因**: `index.js` 中存在非法字符或格式错误,被 Cocos Creator 的面板加载器拒绝解析。
- **修复**: 清理了文件中的语法问题,确保面板能够正常加载和初始化。

5090
main.js

File diff suppressed because it is too large Load Diff

View File

@@ -7,30 +7,31 @@ const http = require("http");
/** /**
* 当前 Cocos Creator 插件监听的端口 * 当前 Cocos Creator 插件监听的端口
* 支持通过环境变量 MCP_BRIDGE_PORT 或命令行参数指定端口
* @type {number} * @type {number}
*/ */
const COCOS_PORT = 3456; const COCOS_PORT = parseInt(process.env.MCP_BRIDGE_PORT || process.argv[2] || "3456", 10);
/** /**
* 发送调试日志到标准的错误输出流水 * 发送调试日志到标准的错误输出流水
* @param {string} msg 日志消息 * @param {string} msg 日志消息
*/ */
function debugLog(msg) { function debugLog(msg) {
process.stderr.write(`[代理调试] ${msg}\n`); process.stderr.write(`[代理调试] ${msg}\n`);
} }
// 监听标准输入以获取 MCP 请求 // 监听标准输入以获取 MCP 请求
process.stdin.on("data", (data) => { process.stdin.on("data", (data) => {
const lines = data.toString().split("\n"); const lines = data.toString().split("\n");
lines.forEach((line) => { lines.forEach((line) => {
if (!line.trim()) return; if (!line.trim()) return;
try { try {
const request = JSON.parse(line); const request = JSON.parse(line);
handleRequest(request); handleRequest(request);
} catch (e) { } catch (e) {
// 忽略非 JSON 输入 // 忽略非 JSON 输入
} }
}); });
}); });
/** /**
@@ -38,44 +39,44 @@ process.stdin.on("data", (data) => {
* @param {Object} req RPC 请求对象 * @param {Object} req RPC 请求对象
*/ */
function handleRequest(req) { function handleRequest(req) {
const { method, id, params } = req; const { method, id, params } = req;
// 处理握手初始化 // 处理握手初始化
if (method === "initialize") { if (method === "initialize") {
sendToAI({ sendToAI({
jsonrpc: "2.0", jsonrpc: "2.0",
id: id, id: id,
result: { result: {
protocolVersion: "2024-11-05", protocolVersion: "2024-11-05",
capabilities: { tools: {} }, capabilities: { tools: {} },
serverInfo: { name: "cocos-bridge", version: "1.0.0" }, serverInfo: { name: "cocos-bridge", version: "1.0.0" },
}, },
}); });
return; return;
} }
// 获取工具列表 // 获取工具列表
if (method === "tools/list") { if (method === "tools/list") {
forwardToCocos("/list-tools", null, id, "GET"); forwardToCocos("/list-tools", null, id, "GET");
return; return;
} }
// 执行具体工具 // 执行具体工具
if (method === "tools/call") { if (method === "tools/call") {
forwardToCocos( forwardToCocos(
"/call-tool", "/call-tool",
{ {
name: params.name, name: params.name,
arguments: params.arguments, arguments: params.arguments,
}, },
id, id,
"POST", "POST",
); );
return; return;
} }
// 默认空响应 // 默认空响应
if (id !== undefined) sendToAI({ jsonrpc: "2.0", id: id, result: {} }); if (id !== undefined) sendToAI({ jsonrpc: "2.0", id: id, result: {} });
} }
/** /**
@@ -86,48 +87,48 @@ function handleRequest(req) {
* @param {string} method HTTP 方法 (默认 POST) * @param {string} method HTTP 方法 (默认 POST)
*/ */
function forwardToCocos(path, payload, id, method = "POST") { function forwardToCocos(path, payload, id, method = "POST") {
const postData = payload ? JSON.stringify(payload) : ""; const postData = payload ? JSON.stringify(payload) : "";
const options = { const options = {
hostname: "127.0.0.1", hostname: "127.0.0.1",
port: COCOS_PORT, port: COCOS_PORT,
path: path, path: path,
method: method, method: method,
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
}; };
if (postData) { if (postData) {
options.headers["Content-Length"] = Buffer.byteLength(postData); options.headers["Content-Length"] = Buffer.byteLength(postData);
} }
const request = http.request(options, (res) => { const request = http.request(options, (res) => {
let resData = ""; let resData = "";
res.on("data", (d) => (resData += d)); res.on("data", (d) => (resData += d));
res.on("end", () => { res.on("end", () => {
try { try {
const cocosRes = JSON.parse(resData); const cocosRes = JSON.parse(resData);
// 检查关键字段,确保 Cocos 插件返回了期望的数据格式 // 检查关键字段,确保 Cocos 插件返回了期望的数据格式
if (path === "/list-tools" && !cocosRes.tools) { if (path === "/list-tools" && !cocosRes.tools) {
debugLog(`致命错误: Cocos 未返回工具列表。接收内容: ${resData}`); debugLog(`致命错误: Cocos 未返回工具列表。接收内容: ${resData}`);
sendError(id, -32603, "Cocos 响应无效:缺少 tools 数组"); sendError(id, -32603, "Cocos 响应无效:缺少 tools 数组");
} else { } else {
sendToAI({ jsonrpc: "2.0", id: id, result: cocosRes }); sendToAI({ jsonrpc: "2.0", id: id, result: cocosRes });
} }
} catch (e) { } catch (e) {
debugLog(`JSON 解析错误。Cocos 发送内容: ${resData}`); debugLog(`JSON 解析错误。Cocos 发送内容: ${resData}`);
sendError(id, -32603, "Cocos 返回了非 JSON 数据"); sendError(id, -32603, "Cocos 返回了非 JSON 数据");
} }
}); });
}); });
request.on("error", (e) => { request.on("error", (e) => {
debugLog(`Cocos 插件已离线: ${e.message}`); debugLog(`Cocos 插件已离线: ${e.message}`);
sendError(id, -32000, "Cocos 插件离线"); sendError(id, -32000, "Cocos 插件离线");
}); });
if (postData) request.write(postData); if (postData) request.write(postData);
request.end(); request.end();
} }
/** /**
@@ -135,7 +136,7 @@ function forwardToCocos(path, payload, id, method = "POST") {
* @param {Object} obj 结果对象 * @param {Object} obj 结果对象
*/ */
function sendToAI(obj) { function sendToAI(obj) {
process.stdout.write(JSON.stringify(obj) + "\n"); process.stdout.write(JSON.stringify(obj) + "\n");
} }
/** /**
@@ -145,5 +146,5 @@ function sendToAI(obj) {
* @param {string} message 错误消息 * @param {string} message 错误消息
*/ */
function sendError(id, code, message) { function sendError(id, code, message) {
sendToAI({ jsonrpc: "2.0", id: id, error: { code, message } }); sendToAI({ jsonrpc: "2.0", id: id, error: { code, message } });
} }

View File

@@ -9,320 +9,332 @@ const fs = require("fs");
const { IpcUi } = require("../dist/IpcUi"); const { IpcUi } = require("../dist/IpcUi");
Editor.Panel.extend({ Editor.Panel.extend({
/** /**
* 面板 CSS 样式 * 面板 CSS 样式
*/ */
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"),
/** /**
* 面板 HTML 模板 * 面板 HTML 模板
*/ */
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: {
/** /**
* 接收并渲染日志 * 接收并渲染日志
* @param {Object} event IPC 事件对象 * @param {Object} event IPC 事件对象
* @param {Object} log 日志数据 * @param {Object} log 日志数据
*/ */
"mcp-bridge:on-log"(event, log) { "mcp-bridge:on-log"(event, log) {
this.renderLog(log); this.renderLog(log);
}, },
/** /**
* 服务器状态变更通知 * 服务器状态变更通知
* @param {Object} event IPC 事件对象 * @param {Object} event IPC 事件对象
* @param {Object} config 服务器配置 * @param {Object} config 服务器配置
*/ */
"mcp-bridge:state-changed"(event, config) { "mcp-bridge:state-changed"(event, config) {
this.updateUI(config.active); this.updateUI(config.active);
// 如果服务器已启动,更新面板显示的端口为实际运行端口 // 如果服务器已启动,更新面板显示的端口为实际运行端口
if (config.active && config.port) { if (config.active && config.port) {
const portInput = this.shadowRoot.querySelector("#portInput"); const portInput = this.shadowRoot.querySelector("#portInput");
if (portInput) portInput.value = config.port; if (portInput) portInput.value = config.port;
} }
}, },
}, },
/** /**
* 面板就绪回调,进行 DOM 绑定与事件初始化 * 面板就绪回调,进行 DOM 绑定与事件初始化
*/ */
ready() { ready() {
const root = this.shadowRoot; const root = this.shadowRoot;
// 获取 DOM 元素映射 // 获取 DOM 元素映射
const els = { const els = {
port: root.querySelector("#portInput"), port: root.querySelector("#portInput"),
btnToggle: root.querySelector("#btnToggle"), btnToggle: root.querySelector("#btnToggle"),
autoStart: root.querySelector("#autoStartCheck"), autoStart: root.querySelector("#autoStartCheck"),
logView: root.querySelector("#logConsole"), logView: root.querySelector("#logConsole"),
tabMain: root.querySelector("#tabMain"), tabMain: root.querySelector("#tabMain"),
tabTest: root.querySelector("#tabTest"), tabTest: root.querySelector("#tabTest"),
tabIpc: root.querySelector("#tabIpc"), tabIpc: root.querySelector("#tabIpc"),
panelMain: root.querySelector("#panelMain"), panelMain: root.querySelector("#panelMain"),
panelTest: root.querySelector("#panelTest"), panelTest: root.querySelector("#panelTest"),
panelIpc: root.querySelector("#panelIpc"), panelIpc: root.querySelector("#panelIpc"),
toolName: root.querySelector("#toolName"), toolName: root.querySelector("#toolName"),
toolParams: root.querySelector("#toolParams"), toolParams: root.querySelector("#toolParams"),
toolDescription: root.querySelector("#toolDescription"), toolDescription: root.querySelector("#toolDescription"),
toolsList: root.querySelector("#toolsList"), toolsList: root.querySelector("#toolsList"),
testBtn: root.querySelector("#testBtn"), testBtn: root.querySelector("#testBtn"),
listBtn: root.querySelector("#listToolsBtn"), listBtn: root.querySelector("#listToolsBtn"),
clearBtn: root.querySelector("#clearTestBtn"), clearBtn: root.querySelector("#clearTestBtn"),
result: root.querySelector("#resultContent"), result: root.querySelector("#resultContent"),
left: root.querySelector("#testLeftPanel"), left: root.querySelector("#testLeftPanel"),
resizer: root.querySelector("#testResizer"), resizer: root.querySelector("#testResizer"),
}; };
// 1. 初始化服务器状态与配置 // 1. 初始化服务器状态与配置
Editor.Ipc.sendToMain("mcp-bridge:get-server-state", (err, data) => { Editor.Ipc.sendToMain("mcp-bridge:get-server-state", (err, data) => {
if (data) { if (data) {
els.port.value = data.config.port; els.port.value = data.config.port;
els.autoStart.value = data.autoStart; els.autoStart.value = data.autoStart;
this.updateUI(data.config.active); this.updateUI(data.config.active);
els.logView.innerHTML = ""; els.logView.innerHTML = "";
data.logs.forEach((l) => this.renderLog(l)); data.logs.forEach((l) => this.renderLog(l));
} }
}); });
// 初始化 IPC 调试专用 UI (由 TypeScript 编写并编译到 dist) // 初始化 IPC 调试专用 UI (由 TypeScript 编写并编译到 dist)
new IpcUi(root); new IpcUi(root);
// 2. 标签页切换逻辑 // 2. 标签页切换逻辑
els.tabMain.addEventListener("confirm", () => { els.tabMain.addEventListener("confirm", () => {
els.tabMain.classList.add("active"); els.tabMain.classList.add("active");
els.tabTest.classList.remove("active"); els.tabTest.classList.remove("active");
els.tabIpc.classList.remove("active"); els.tabIpc.classList.remove("active");
els.panelMain.classList.add("active"); els.panelMain.classList.add("active");
els.panelTest.classList.remove("active"); els.panelTest.classList.remove("active");
els.panelIpc.classList.remove("active"); els.panelIpc.classList.remove("active");
}); });
els.tabTest.addEventListener("confirm", () => { els.tabTest.addEventListener("confirm", () => {
els.tabTest.classList.add("active"); els.tabTest.classList.add("active");
els.tabMain.classList.remove("active"); els.tabMain.classList.remove("active");
els.tabIpc.classList.remove("active"); els.tabIpc.classList.remove("active");
els.panelTest.classList.add("active"); els.panelTest.classList.add("active");
els.panelMain.classList.remove("active"); els.panelMain.classList.remove("active");
els.panelIpc.classList.remove("active"); els.panelIpc.classList.remove("active");
this.fetchTools(els); // 切换到测试页时自动拉取工具列表 this.fetchTools(els); // 切换到测试页时自动拉取工具列表
}); });
els.tabIpc.addEventListener("confirm", () => { els.tabIpc.addEventListener("confirm", () => {
els.tabIpc.classList.add("active"); els.tabIpc.classList.add("active");
els.tabMain.classList.remove("active"); els.tabMain.classList.remove("active");
els.tabTest.classList.remove("active"); els.tabTest.classList.remove("active");
els.panelIpc.classList.add("active"); els.panelIpc.classList.add("active");
els.panelMain.classList.remove("active"); els.panelMain.classList.remove("active");
els.panelTest.classList.remove("active"); els.panelTest.classList.remove("active");
}); });
// 3. 基础控制按钮逻辑 // 3. 基础控制按钮逻辑
els.btnToggle.addEventListener("confirm", () => { els.btnToggle.addEventListener("confirm", () => {
Editor.Ipc.sendToMain("mcp-bridge:toggle-server", parseInt(els.port.value)); Editor.Ipc.sendToMain("mcp-bridge:toggle-server", parseInt(els.port.value));
}); });
root.querySelector("#btnClear").addEventListener("confirm", () => { root.querySelector("#btnClear").addEventListener("confirm", () => {
els.logView.innerHTML = ""; els.logView.innerHTML = "";
Editor.Ipc.sendToMain("mcp-bridge:clear-logs"); Editor.Ipc.sendToMain("mcp-bridge:clear-logs");
}); });
root.querySelector("#btnCopy").addEventListener("confirm", () => { root.querySelector("#btnCopy").addEventListener("confirm", () => {
require("electron").clipboard.writeText(els.logView.innerText); require("electron").clipboard.writeText(els.logView.innerText);
Editor.success("日志已复制到剪贴板"); Editor.success("日志已复制到剪贴板");
}); });
els.autoStart.addEventListener("change", (e) => { els.autoStart.addEventListener("change", (e) => {
Editor.Ipc.sendToMain("mcp-bridge:set-auto-start", e.target.value); Editor.Ipc.sendToMain("mcp-bridge:set-auto-start", e.target.value);
}); });
// 4. API 测试页交互逻辑 // 4. API 测试页交互逻辑
els.listBtn.addEventListener("confirm", () => this.fetchTools(els)); els.listBtn.addEventListener("confirm", () => this.fetchTools(els));
els.clearBtn.addEventListener("confirm", () => { els.clearBtn.addEventListener("confirm", () => {
els.result.value = ""; els.result.value = "";
}); });
els.testBtn.addEventListener("confirm", () => this.runTest(els)); els.testBtn.addEventListener("confirm", () => this.runTest(els));
// API 探查功能 (辅助开发者发现可用内部 IPC) // API 探查功能 (辅助开发者发现可用内部 IPC)
const probeBtn = root.querySelector("#probeApisBtn"); const probeBtn = root.querySelector("#probeApisBtn");
if (probeBtn) { if (probeBtn) {
probeBtn.addEventListener("confirm", () => { probeBtn.addEventListener("confirm", () => {
Editor.Ipc.sendToMain("mcp-bridge:inspect-apis"); Editor.Ipc.sendToMain("mcp-bridge:inspect-apis");
els.result.value = "API 探查指令已发送。请查看编辑器控制台 (Console) 获取详细报告。"; els.result.value = "API 探查指令已发送。请查看编辑器控制台 (Console) 获取详细报告。";
}); });
} }
// 5. 测试页分栏拖拽缩放逻辑 // 5. 测试页分栏拖拽缩放逻辑
if (els.resizer && els.left) { if (els.resizer && els.left) {
els.resizer.addEventListener("mousedown", (e) => { els.resizer.addEventListener("mousedown", (e) => {
e.preventDefault(); e.preventDefault();
const startX = e.clientX; const startX = e.clientX;
const startW = els.left.offsetWidth; const startW = els.left.offsetWidth;
const onMove = (ev) => { const onMove = (ev) => {
els.left.style.width = startW + (ev.clientX - startX) + "px"; els.left.style.width = startW + (ev.clientX - startX) + "px";
}; };
const onUp = () => { const onUp = () => {
document.removeEventListener("mousemove", onMove); document.removeEventListener("mousemove", onMove);
document.removeEventListener("mouseup", onUp); document.removeEventListener("mouseup", onUp);
document.body.style.cursor = "default"; document.body.style.cursor = "default";
}; };
document.addEventListener("mousemove", onMove); document.addEventListener("mousemove", onMove);
document.addEventListener("mouseup", onUp); document.addEventListener("mouseup", onUp);
document.body.style.cursor = "col-resize"; document.body.style.cursor = "col-resize";
}); });
} }
}, },
/** /**
* 从本地服务器获取 MCP 工具列表并渲染 * 从本地服务器获取 MCP 工具列表并渲染
* @param {Object} els DOM 元素映射 * @param {Object} els DOM 元素映射
*/ */
fetchTools(els) { fetchTools(els) {
const url = `http://localhost:${els.port.value}/list-tools`; const url = `http://localhost:${els.port.value}/list-tools`;
els.result.value = "正在获取工具列表..."; els.result.value = "正在获取工具列表...";
fetch(url) fetch(url)
.then((r) => r.json()) .then((r) => r.json())
.then((data) => { .then((data) => {
els.toolsList.innerHTML = ""; els.toolsList.innerHTML = "";
const toolsMap = {}; const toolsMap = {};
data.tools.forEach((t) => { data.tools.forEach((t) => {
toolsMap[t.name] = t; toolsMap[t.name] = t;
const item = document.createElement("div"); const item = document.createElement("div");
item.className = "tool-item"; item.className = "tool-item";
item.textContent = t.name; item.textContent = t.name;
item.onclick = () => { item.onclick = () => {
els.toolName.value = t.name; els.toolName.value = t.name;
els.toolParams.value = JSON.stringify(this.getExample(t.name), null, 2); els.toolParams.value = JSON.stringify(this.getExample(t.name), null, 2);
this.showToolDescription(els, t); this.showToolDescription(els, t);
}; };
els.toolsList.appendChild(item); els.toolsList.appendChild(item);
}); });
this.toolsMap = toolsMap; this.toolsMap = toolsMap;
els.result.value = `成功:加载了 ${data.tools.length} 个工具。`; els.result.value = `成功:加载了 ${data.tools.length} 个工具。`;
}) })
.catch((e) => { .catch((e) => {
els.result.value = "获取失败: " + e.message; els.result.value = "获取失败: " + e.message;
}); });
}, },
/** /**
* 在面板中展示工具的详细描述与参数定义 * 在面板中展示工具的详细描述与参数定义
* @param {Object} els DOM 元素映射 * @param {Object} els DOM 元素映射
* @param {Object} tool 工具定义对象 * @param {Object} tool 工具定义对象
*/ */
showToolDescription(els, tool) { showToolDescription(els, tool) {
if (!tool) { if (!tool) {
els.toolDescription.textContent = "选择工具以查看说明"; els.toolDescription.textContent = "选择工具以查看说明";
return; return;
} }
let description = tool.description || "暂无描述"; let description = tool.description || "暂无描述";
let inputSchema = tool.inputSchema; let inputSchema = tool.inputSchema;
let details = []; let details = [];
if (inputSchema && inputSchema.properties) { if (inputSchema && inputSchema.properties) {
details.push("<b>参数说明:</b>"); details.push("<b>参数说明:</b>");
for (const [key, prop] of Object.entries(inputSchema.properties)) { for (const [key, prop] of Object.entries(inputSchema.properties)) {
let propDesc = `- <code>${key}</code>`; let propDesc = `- <code>${key}</code>`;
if (prop.description) { if (prop.description) {
propDesc += `: ${prop.description}`; propDesc += `: ${prop.description}`;
} }
if (prop.required || (inputSchema.required && inputSchema.required.includes(key))) { if (prop.required || (inputSchema.required && inputSchema.required.includes(key))) {
propDesc += " <span style='color:#f44'>(必填)</span>"; propDesc += " <span style='color:#f44'>(必填)</span>";
} }
details.push(propDesc); details.push(propDesc);
} }
} }
els.toolDescription.innerHTML = `${description}<br><br>${details.join("<br>")}`; els.toolDescription.innerHTML = `${description}<br><br>${details.join("<br>")}`;
}, },
/** /**
* 执行工具测试请求 * 执行工具测试请求
* @param {Object} els DOM 元素映射 * @param {Object} els DOM 元素映射
*/ */
runTest(els) { runTest(els) {
const url = `http://localhost:${els.port.value}/call-tool`; const url = `http://localhost:${els.port.value}/call-tool`;
let args; let args;
try { try {
args = JSON.parse(els.toolParams.value || "{}"); args = JSON.parse(els.toolParams.value || "{}");
} catch (e) { } catch (e) {
els.result.value = "JSON 格式错误: " + e.message; els.result.value = "JSON 格式错误: " + e.message;
return; return;
} }
const body = { name: els.toolName.value, arguments: args }; const body = { name: els.toolName.value, arguments: args };
els.result.value = "正在发送请求..."; els.result.value = "正在发送请求...";
fetch(url, { method: "POST", body: JSON.stringify(body) }) fetch(url, { method: "POST", body: JSON.stringify(body) })
.then((r) => r.json()) .then((r) => r.json())
.then((d) => { .then((d) => {
els.result.value = JSON.stringify(d, null, 2); els.result.value = JSON.stringify(d, null, 2);
}) })
.catch((e) => { .catch((e) => {
els.result.value = "测试异常: " + e.message; els.result.value = "测试异常: " + e.message;
}); });
}, },
/** /**
* 获取指定工具的示例参数 * 获取指定工具的示例参数
* @param {string} name 工具名称 * @param {string} name 工具名称
* @returns {Object} 示例参数对象 * @returns {Object} 示例参数对象
*/ */
getExample(name) { getExample(name) {
const examples = { const examples = {
set_node_name: { id: "节点-UUID", newName: "新名称" }, set_node_name: { id: "节点-UUID", newName: "新名称" },
update_node_transform: { id: "节点-UUID", x: 0, y: 0, color: "#FF0000" }, update_node_transform: { id: "节点-UUID", x: 0, y: 0, color: "#FF0000" },
create_node: { name: "新节点", type: "sprite", parentId: "" }, create_node: { name: "新节点", type: "sprite", parentId: "" },
open_scene: { url: "db://assets/Scene.fire" }, open_scene: { url: "db://assets/Scene.fire" },
open_prefab: { url: "db://assets/MyPrefab.prefab" }, open_prefab: { url: "db://assets/MyPrefab.prefab" },
manage_editor: { action: "get_selection" }, manage_editor: { action: "get_selection" },
find_gameobjects: { conditions: { name: "MyNode", active: true }, recursive: true }, find_gameobjects: { conditions: { name: "MyNode", active: true }, recursive: true },
manage_material: { manage_material: {
action: "create", action: "create",
path: "db://assets/materials/NewMaterial.mat", path: "db://assets/materials/NewMaterial.mat",
properties: { uniforms: {} }, properties: { uniforms: {} },
}, },
manage_texture: { manage_texture: {
action: "create", action: "create",
path: "db://assets/textures/NewTexture.png", path: "db://assets/textures/NewTexture.png",
properties: { width: 128, height: 128 }, properties: { width: 128, height: 128 },
}, },
execute_menu_item: { menuPath: "Assets/Create/Folder" }, execute_menu_item: { menuPath: "Assets/Create/Folder" },
apply_text_edits: { apply_text_edits: {
filePath: "db://assets/scripts/TestScript.ts", filePath: "db://assets/scripts/TestScript.ts",
edits: [{ type: "insert", position: 0, text: "// 测试注释\n" }], edits: [{ type: "insert", position: 0, text: "// 测试注释\n" }],
}, },
read_console: { limit: 10, type: "log" }, read_console: { limit: 10, type: "log" },
validate_script: { filePath: "db://assets/scripts/TestScript.ts" }, validate_script: { filePath: "db://assets/scripts/TestScript.ts" },
}; };
return examples[name] || {}; return examples[name] || {};
}, },
/** /**
* 将日志条目渲染至面板控制台 * 将日志条目渲染至面板控制台
* @param {Object} log 日志对象 * @param {Object} log 日志对象
*/ */
renderLog(log) { renderLog(log) {
const view = this.shadowRoot.querySelector("#logConsole"); const view = this.shadowRoot.querySelector("#logConsole");
if (!view) return; if (!view) return;
const atBottom = view.scrollHeight - view.scrollTop <= view.clientHeight + 50; // 限制面板日志 DOM 节点数量,防止长时间运行后面板卡顿
const el = document.createElement("div"); while (view.childNodes.length > 1000) {
el.className = `log-item ${log.type}`; view.removeChild(view.firstChild);
el.innerHTML = `<span class="time">${log.time}</span><span class="msg">${log.content}</span>`; }
view.appendChild(el); const atBottom = view.scrollHeight - view.scrollTop <= view.clientHeight + 50;
if (atBottom) view.scrollTop = view.scrollHeight; const el = document.createElement("div");
}, el.className = `log-item ${log.type}`;
// 使用 textContent 代替 innerHTML 防止 XSS 注入
const timeSpan = document.createElement("span");
timeSpan.className = "time";
timeSpan.textContent = log.time;
const msgSpan = document.createElement("span");
msgSpan.className = "msg";
msgSpan.textContent = log.content;
el.appendChild(timeSpan);
el.appendChild(msgSpan);
view.appendChild(el);
if (atBottom) view.scrollTop = view.scrollHeight;
},
/** /**
* 根据服务器运行状态更新 UI 按钮文字与样式 * 根据服务器运行状态更新 UI 按钮文字与样式
* @param {boolean} active 服务器是否处于激活状态 * @param {boolean} active 服务器是否处于激活状态
*/ */
updateUI(active) { updateUI(active) {
const btn = this.shadowRoot.querySelector("#btnToggle"); const btn = this.shadowRoot.querySelector("#btnToggle");
if (!btn) return; if (!btn) return;
btn.innerText = active ? "停止" : "启动"; btn.innerText = active ? "停止" : "启动";
btn.style.backgroundColor = active ? "#aa4444" : "#44aa44"; btn.style.backgroundColor = active ? "#aa4444" : "#44aa44";
}, },
}); });

File diff suppressed because it is too large Load Diff