特性(MCP): 增强 AI 参数幻觉容错与全览日志

- 在 \scene-script.js\ 中为 manage_components 启用 operation 作为 action 的后备别名防范 AI 传参幻觉导致中断。
- 在 \main.js\ 的 /call-tool 请求日志中透传完整 arguments JSON 数据,并在超长时予以截断。
- 同步补全 README.md, UPDATE_LOG.md 与 注意事项.md 针对传参错写的防避规则和特性的更新记录。
This commit is contained in:
火焰库拉
2026-02-25 11:11:21 +08:00
parent 51c04eca48
commit ffa918751d
5 changed files with 3664 additions and 3617 deletions

View File

@@ -403,12 +403,13 @@ Args: [你的项目所在盘符]:/[项目路径]/packages/mcp-bridge/mcp-proxy.j
### 日志管理 ### 日志管理
插件会记录所有操作的日志,包括: 插件会通过内置的测试面板MCP Bridge/Open Panel实时记录所有操作的日志,包括:
- 服务启动/停止 - 服务启动/停止状态
- MCP 请求接收 - MCP 客户端请求接收(完整包含工具的 `arguments` 参数,超长自动截断)
- 操作成功/失败状态 - 场景节点树遍历与耗时信息
- 错误信息 - 工具调用的执行成功/失败状态返回
- IPC 消息和核心底层报错堆栈
## 注意事项 ## 注意事项

View File

@@ -170,3 +170,18 @@
- **问题**: AI 助手创建或修改脚本后,若不主动触发系统刷新,后续试图通过 `manage_components` 将该新脚本挂载为组件时,会由于缺乏有效的 `.meta` 扫描和 UUID 索引而失败。 - **问题**: AI 助手创建或修改脚本后,若不主动触发系统刷新,后续试图通过 `manage_components` 将该新脚本挂载为组件时,会由于缺乏有效的 `.meta` 扫描和 UUID 索引而失败。
- **优化**: 在 `main.js` 中的 `manage_script` 工具 `description` 提示词中,将原本建议性质的刷新语气,修改为严格指令:“**创建后必须调用 refresh_editor (务必指定 path) 生成 meta 文件,否则无法作为组件添加**”。 - **优化**: 在 `main.js` 中的 `manage_script` 工具 `description` 提示词中,将原本建议性质的刷新语气,修改为严格指令:“**创建后必须调用 refresh_editor (务必指定 path) 生成 meta 文件,否则无法作为组件添加**”。
- **效益**: 在不增加 Token 开销的前提下,强制规范了大语言模型的行为,保障了脚本创建到组件挂载工作流的健壮性。 - **效益**: 在不增加 Token 开销的前提下,强制规范了大语言模型的行为,保障了脚本创建到组件挂载工作流的健壮性。
---
## 十、 AI 幻觉容错与调试体验增强 (2026-02-25)
### 1. `manage_components` 参数容错
- **问题**: AI 客户端在调用 `manage_components` 等工具时偶尔会产生“幻觉”,将操作类型参数 `action` 错误拼写为含义相近的 `operation`,导致插件抛出“未知的组件操作类型: undefined”等错误而中断执行。
- **修复**: 在 `scene-script.js` 及其核心操作流中增加了参数别名映射逻辑,允许将 `operation` 作为 `action` 的后备别名Fallback。即使 AI 传参名称发生漂移也能顺畅执行后续流程,大幅提升了对大模型无规律输出错漏的容错率。
### 2. MCP 请求日志全览解析 (Full Arguments Logging)
- **问题**: 现有的面板调试终端在记录 AI 工具调用时,只有指令头如 `REQ -> [manage_components]`,无法透视 AI 实际上到底提交了哪些参数。致使类似参数名称写错的幽灵 Bug 极难被常规察觉。
- **优化**: 修改了 `main.js` 中的 `/call-tool` 路由逻辑。现在系统拦截不仅会记录动作名称,还会将完整的 `arguments` 以 JSON 序列化的形态连同日志一并输出在面板中:例如 `参数: {"nodeId":"...","operation":"get"}`
- **保护机制**: 为防止类似多边形顶点数据等过大的参数体撑爆编辑器控制台缓存或导致 UI 卡顿,日志处理对超过 500 个字符长度的序列化结果启用了自动截断显示 (`...[Truncated]`)。

13
main.js
View File

@@ -836,7 +836,18 @@ module.exports = {
if (url === "/call-tool") { if (url === "/call-tool") {
try { try {
const { name, arguments: args } = JSON.parse(body || "{}"); const { name, arguments: args } = JSON.parse(body || "{}");
addLog("mcp", `REQ -> [${name}] (队列长度: ${commandQueue.length})`); let argsPreview = "";
if (args) {
try {
argsPreview = typeof args === "object" ? JSON.stringify(args) : String(args);
if (argsPreview.length > 500) {
argsPreview = argsPreview.substring(0, 500) + "...[Truncated]";
}
} catch (e) {
argsPreview = "[无法序列化的参数]";
}
}
addLog("mcp", `REQ -> [${name}] (队列长度: ${commandQueue.length}) 参数: ${argsPreview}`);
enqueueCommand((done) => { enqueueCommand((done) => {
this.handleMcpCall(name, args, (err, result) => { this.handleMcpCall(name, args, (err, result) => {

View File

@@ -87,7 +87,11 @@ module.exports = {
*/ */
function dumpNodes(node, currentDepth) { function dumpNodes(node, currentDepth) {
// 【优化】跳过编辑器内部的私有节点,减少数据量 // 【优化】跳过编辑器内部的私有节点,减少数据量
if (!node || !node.name || (typeof node.name === 'string' && (node.name.startsWith("Editor Scene") || node.name === "gizmoRoot"))) { if (
!node ||
!node.name ||
(typeof node.name === "string" && (node.name.startsWith("Editor Scene") || node.name === "gizmoRoot"))
) {
return null; return null;
} }
@@ -109,8 +113,8 @@ module.exports = {
} else { } else {
// 简略模式下如果存在组件,至少提供一个极简列表让 AI 知道节点的作用 // 简略模式下如果存在组件,至少提供一个极简列表让 AI 知道节点的作用
if (comps.length > 0) { if (comps.length > 0) {
nodeData.components = comps.map(c => { nodeData.components = comps.map((c) => {
const parts = (cc.js.getClassName(c) || "").split('.'); const parts = (cc.js.getClassName(c) || "").split(".");
return parts[parts.length - 1]; // 只取类名,例如 cc.Sprite -> Sprite return parts[parts.length - 1]; // 只取类名,例如 cc.Sprite -> Sprite
}); });
} }
@@ -320,7 +324,10 @@ module.exports = {
* @param {Object} args 参数 (nodeId, action, componentType, componentId, properties) * @param {Object} args 参数 (nodeId, action, componentType, componentId, properties)
*/ */
"manage-components": function (event, args) { "manage-components": function (event, args) {
const { nodeId, action, componentType, componentId, properties } = args; let { nodeId, action, operation, componentType, componentId, properties } = args;
// 兼容 AI 幻觉带来的传参错误
action = action || operation;
let node = findNode(nodeId); let node = findNode(nodeId);
/** /**
@@ -675,7 +682,8 @@ module.exports = {
if (typeof val !== "object") { if (typeof val !== "object") {
// 【优化】对于超长字符串进行截断 // 【优化】对于超长字符串进行截断
if (typeof val === "string" && val.length > 200) { if (typeof val === "string" && val.length > 200) {
properties[key] = val.substring(0, 50) + `...[Truncated, total length: ${val.length}]`; properties[key] =
val.substring(0, 50) + `...[Truncated, total length: ${val.length}]`;
} else { } else {
properties[key] = val; properties[key] = val;
} }
@@ -817,7 +825,11 @@ module.exports = {
const scene = cc.director.getScene(); const scene = cc.director.getScene();
function searchNode(node) { function searchNode(node) {
if (!node || !node.name || (typeof node.name === 'string' && (node.name.startsWith("Editor Scene") || node.name === "gizmoRoot"))) { if (
!node ||
!node.name ||
(typeof node.name === "string" && (node.name.startsWith("Editor Scene") || node.name === "gizmoRoot"))
) {
return; return;
} }
@@ -856,10 +868,10 @@ module.exports = {
name: node.name, name: node.name,
active: node.active, active: node.active,
components: comps.map((c) => { components: comps.map((c) => {
const parts = (cc.js.getClassName(c) || "").split('.'); const parts = (cc.js.getClassName(c) || "").split(".");
return parts[parts.length - 1]; // 简化的组件名 return parts[parts.length - 1]; // 简化的组件名
}), }),
childrenCount: node.childrenCount childrenCount: node.childrenCount,
}); });
} }

View File

@@ -69,6 +69,12 @@
3. **属性校验**:严禁猜测属性名。在 `update` 前,应通过读取脚本定义或 `get` 返回的现有属性列表来确定准确的属性名称。 3. **属性校验**:严禁猜测属性名。在 `update` 前,应通过读取脚本定义或 `get` 返回的现有属性列表来确定准确的属性名称。
- **禁止行为**:禁止基于假设进行盲目赋值或删除。如果发现对象不存在,应立即报错或尝试重建,而非继续尝试修改。 - **禁止行为**:禁止基于假设进行盲目赋值或删除。如果发现对象不存在,应立即报错或尝试重建,而非继续尝试修改。
### 5.2 传参精准与别名容错
- **传参风险**:大语言模型在生成 JSON 时可能会出现操作类型字段名“幻觉”(例如在调用 `manage_components` 时将本应是 `action` 的参数写为含义相近的 `operation`)。
- **优化机制**:底层脚本 (`scene-script.js`) 已经全面引入参数别名回落机制(如 `action = action || operation`)。
- **提示开发**:尽管底层具备一定的容错率,在维护 MCP 工具说明书Schema仍应严格要求 AI 书写标准参数名,避免纵容产生更大的幻觉偏移。
--- ---
## 6. 常见资源关键字 ## 6. 常见资源关键字
@@ -134,12 +140,14 @@
## 10. Token 消耗与长数据保护防爆机制 ## 10. Token 消耗与长数据保护防爆机制
### 10.1 `get_scene_hierarchy` 深度与层级限制 ### 10.1 `get_scene_hierarchy` 深度与层级限制
- **背景**:在一两千个节点的大型 UI 场景中,无限制地获取全场景树会瞬间消耗十万以上的 Token导致 AI 丢失上下文甚至触发截断报错。 - **背景**:在一两千个节点的大型 UI 场景中,无限制地获取全场景树会瞬间消耗十万以上的 Token导致 AI 丢失上下文甚至触发截断报错。
- **最佳实践** - **最佳实践**
- **默认使用 `depth: 2`** (默认限制) 来逐步探查。 - **默认使用 `depth: 2`** (默认限制) 来逐步探查。
- **结合 `nodeId` 参数**:找到关键模块(例如 `Canvas/LoginPanel`)的 UUID 后,再单独向该 `nodeId` 请求下一层的结构,而非每次从根部拉取。 - **结合 `nodeId` 参数**:找到关键模块(例如 `Canvas/LoginPanel`)的 UUID 后,再单独向该 `nodeId` 请求下一层的结构,而非每次从根部拉取。
### 10.2 大对象与长数组截断 ### 10.2 大对象与长数组截断
- **背景**在读取某些特定组件数据如多边形顶点坐标、Sprite 曲线数据或序列化的内联 Base64 图片JSON 可能会异常庞大。 - **背景**在读取某些特定组件数据如多边形顶点坐标、Sprite 曲线数据或序列化的内联 Base64 图片JSON 可能会异常庞大。
- **保护机制** - **保护机制**
- `scene-script.js` 内部在执行 `manage_components(get)` 序列化时,对于**长度超过 10 的 Array** 会强制截断,返回字面量字符串 `"[Array(X)]"` - `scene-script.js` 内部在执行 `manage_components(get)` 序列化时,对于**长度超过 10 的 Array** 会强制截断,返回字面量字符串 `"[Array(X)]"`