docs: 深度优化 Token 消耗,精简查找与层级获取载荷,补充相关文档与安全守则
This commit is contained in:
18
README.md
18
README.md
@@ -105,8 +105,11 @@ Args: [你的项目所在盘符]:/[项目路径]/packages/mcp-bridge/mcp-proxy.j
|
||||
|
||||
### 4. get_scene_hierarchy
|
||||
|
||||
- **描述**: 获取当前场景的完整节点树结构(包括 UUID、名称和层级关系)
|
||||
- **参数**: 无
|
||||
- **描述**: 获取当前场景的完整节点树结构(支持分页避免长数据截断)。如果要查询具体组件属性请配合 manage_components。
|
||||
- **参数**:
|
||||
- `nodeId`: 指定的根节点 UUID。如果不传则获取整个场景的根 (可选)。
|
||||
- `depth`: 遍历的深度限制,默认为 2。用来防止过大场景导致返回数据超长 (可选)。
|
||||
- `includeDetails`: 是否包含坐标、缩放等杂项详情,默认为 false (可选)。
|
||||
|
||||
### 5. update_node_transform
|
||||
|
||||
@@ -223,14 +226,13 @@ Args: [你的项目所在盘符]:/[项目路径]/packages/mcp-bridge/mcp-proxy.j
|
||||
|
||||
### 15. find_gameobjects
|
||||
|
||||
- **描述**: 查找游戏对象
|
||||
- **描述**: 按条件在场景中搜索游戏对象。返回匹配节点的轻量级结构 (UUID, name, active, components 等)。若要获取完整的详细组件属性,请进一步对目标使用 `manage_components`。
|
||||
- **参数**:
|
||||
- `conditions`: 查找条件
|
||||
- `name`: 节点名称(包含匹配)
|
||||
- `tag`: 节点标签
|
||||
- `component`: 组件类型
|
||||
- `active`: 激活状态
|
||||
- `recursive`: 是否递归查找(默认:true)
|
||||
- `name`: 节点名称(包含模糊匹配)
|
||||
- `component`: 包含的组件类名(如 `cc.Sprite`)
|
||||
- `active`: 布尔值,节点的激活状态
|
||||
- `recursive`: 是否递归查找所有的子节点(默认:true)
|
||||
|
||||
### 16. manage_material
|
||||
|
||||
|
||||
@@ -120,3 +120,32 @@
|
||||
- **优化预制体创建稳定性 (`create_node` + `prefab_management`)**:
|
||||
- 在创建物理目录后强制执行 `Editor.assetdb.refresh`,确保 AssetDB 即时同步。
|
||||
- 将节点重命名与预制体创建指令之间的安全延迟从 100ms 增加至 300ms,消除了重命名未完成导致创建失败的竞态条件。
|
||||
|
||||
---
|
||||
|
||||
## 八、 Token 消耗深度优化 (2026-02-24)
|
||||
|
||||
### 1. 工具描述精简 (`main.js`)
|
||||
- **问题**: `globalPrecautions` (AI 安全守则) 被硬编码到所有工具的 `description` 中,导致每次环境初始化或查阅工具列表时浪费约 2200 个 CJK Token。
|
||||
- **优化**: 收束安全守则的广播范围。目前仅针对高风险的**写操作**(如 `manage_components`, `update_node_transform`, `manage_material`, `create_node` 等)保留警告,低风险或只读分析类工具(如 `get_scene_hierarchy`, `get_selected_node`)已悉数移除该文本。
|
||||
- **效果**: `/list-tools` 整体负载字符数缩减近 40%。
|
||||
|
||||
### 2. 长数据截断保护 (`scene-script.js`)
|
||||
- **问题**: `manage_components(get)` 会完整序列化多边形坐标集、曲线数据数组以及 Base64 图片,产生极其庞大且对 AI 无用的 JSON 负载。
|
||||
- **优化**:
|
||||
- **数组截断**: 长度超过 10 的数组直接返回 `[Array(length)]`,彻底杜绝数据雪崩。
|
||||
- **字符串截断**: 长度超过 200 的字符串限制为截断显示并附带 `...[Truncated, total length: X]` 提示。
|
||||
|
||||
### 3. 层级树获取瘦身与分页 (`get_scene_hierarchy`)
|
||||
- **问题**: 请求场景层级时会一次性返回完整 1000+ 节点的深层结构,包括所有变换矩阵。
|
||||
- **优化**:
|
||||
- 支持 `depth` 深度限制(默认 2 层)。
|
||||
- 支持 `nodeId` 参数,允许 AI 缩小作用域,从指定根节点向下探测。
|
||||
- 添加 `includeDetails` 参数。默认关闭,此时剥离坐标、缩放与尺寸指标,且将冗长的组件详细结构浓缩成简化的名称数组(如 `["Sprite", "Button"]`)。
|
||||
|
||||
### 4. 查找结果精简 (`find_gameobjects`)
|
||||
- **优化**: 将原本包含 Transform(位移/缩放/尺寸)全量数据的匹配回传,精简为仅包含核心识别特征的基础集 (`uuid`, `name`, `active`, `components`, `childrenCount`),极大释放了同名大批量查找时的 Token 压力。
|
||||
|
||||
### 5. 底层鲁棒性大修
|
||||
- **问题**: 上述优化在应用过程中暴露出遍历未命名根节点(如 `cc.Scene`)时遭遇 `undefined.startsWith` 报错并引发 IPC 悬挂的致命隐患。
|
||||
- **修复**: 在 `dumpNodes` 与 `searchNode` 中增设前置安全屏障,并修复 `cc.js.getClassName(c)` 替代底层的 `__typename` 来兼容 2.4 获取有效类名。修复了 `main.js` 中关于 `get_scene_hierarchy` 的参数传递脱节问题。
|
||||
|
||||
40
main.js
40
main.js
@@ -173,7 +173,7 @@ const getToolsList = () => {
|
||||
return [
|
||||
{
|
||||
name: "get_selected_node",
|
||||
description: `${globalPrecautions} 获取当前编辑器中选中的节点 ID。建议获取后立即调用 get_scene_hierarchy 确认该节点是否仍存在于当前场景中。`,
|
||||
description: `获取当前编辑器中选中的节点 ID。建议获取后立即调用 get_scene_hierarchy 确认该节点是否仍存在于当前场景中。`,
|
||||
inputSchema: { type: "object", properties: {} },
|
||||
},
|
||||
{
|
||||
@@ -190,13 +190,20 @@ const getToolsList = () => {
|
||||
},
|
||||
{
|
||||
name: "save_scene",
|
||||
description: `${globalPrecautions} 保存当前场景的修改`,
|
||||
description: `保存当前场景的修改`,
|
||||
inputSchema: { type: "object", properties: {} },
|
||||
},
|
||||
{
|
||||
name: "get_scene_hierarchy",
|
||||
description: `${globalPrecautions} 获取当前场景的完整节点树结构(包括 UUID、名称和层级关系)`,
|
||||
inputSchema: { type: "object", properties: {} },
|
||||
description: `获取当前场景的节点树结构(包含 UUID、名称、子节点数)。若要查询节点组件详情等,请使用 manage_components。`,
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
nodeId: { type: "string", description: "指定的根节点 UUID。如果不传则获取整个场景的根。" },
|
||||
depth: { type: "number", description: "遍历的深度限制,默认为 2。用来防止过大场景导致返回数据超长。" },
|
||||
includeDetails: { type: "boolean", description: "是否包含坐标、缩放等杂项详情,默认为 false。" }
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "update_node_transform",
|
||||
@@ -218,7 +225,7 @@ const getToolsList = () => {
|
||||
},
|
||||
{
|
||||
name: "create_scene",
|
||||
description: `${globalPrecautions} 在 assets 目录下创建一个新的场景文件。创建并通过 open_scene 打开后,请务必初始化基础节点(如 Canvas 和 Camera)。`,
|
||||
description: `在 assets 目录下创建一个新的场景文件。创建并通过 open_scene 打开后,请务必初始化基础节点(如 Canvas 和 Camera)。`,
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
@@ -241,7 +248,7 @@ const getToolsList = () => {
|
||||
},
|
||||
{
|
||||
name: "open_scene",
|
||||
description: `${globalPrecautions} 打开场景文件。注意:这是一个异步且耗时的操作,打开后请等待几秒。重要:如果是新创建或空的场景,请务必先创建并初始化基础节点(Canvas/Camera)。`,
|
||||
description: `打开场景文件。注意:这是一个异步且耗时的操作,打开后请等待几秒。重要:如果是新创建或空的场景,请务必先创建并初始化基础节点(Canvas/Camera)。`,
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
@@ -255,7 +262,7 @@ const getToolsList = () => {
|
||||
},
|
||||
{
|
||||
name: "open_prefab",
|
||||
description: `${globalPrecautions} 在编辑器中打开预制体文件进入编辑模式。注意:这是一个异步操作,打开后请等待几秒。`,
|
||||
description: `在编辑器中打开预制体文件进入编辑模式。注意:这是一个异步操作,打开后请等待几秒。`,
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
@@ -424,12 +431,15 @@ const getToolsList = () => {
|
||||
},
|
||||
{
|
||||
name: "find_gameobjects",
|
||||
description: `${globalPrecautions} 查找游戏对象`,
|
||||
description: `按条件在场景中搜索游戏对象。返回匹配节点的轻量级结构 (UUID, name, active, components 等)。若要获取完整的详细组件属性,请进一步对目标使用 manage_components。`,
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
conditions: { type: "object", description: "查找条件" },
|
||||
recursive: { type: "boolean", default: true, description: "是否递归查找" },
|
||||
conditions: {
|
||||
type: "object",
|
||||
description: "查找条件。支持的属性:name (节点名称,支持模糊匹配), component (包含的组件类名,如 'cc.Sprite'), active (布尔值,节点的激活状态)。"
|
||||
},
|
||||
recursive: { type: "boolean", default: true, description: "是否递归查找所有子节点" },
|
||||
},
|
||||
required: ["conditions"],
|
||||
},
|
||||
@@ -538,7 +548,7 @@ const getToolsList = () => {
|
||||
},
|
||||
{
|
||||
name: "read_console",
|
||||
description: `${globalPrecautions} 读取控制台`,
|
||||
description: `读取控制台`,
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
@@ -553,7 +563,7 @@ const getToolsList = () => {
|
||||
},
|
||||
{
|
||||
name: "validate_script",
|
||||
description: `${globalPrecautions} 验证脚本`,
|
||||
description: `验证脚本`,
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
@@ -564,7 +574,7 @@ const getToolsList = () => {
|
||||
},
|
||||
{
|
||||
name: "search_project",
|
||||
description: `${globalPrecautions} 搜索项目文件。支持三种模式:1. 'content' (默认): 搜索文件内容,支持正则表达式;2. 'file_name': 在指定目录下搜索匹配的文件名;3. 'dir_name': 在指定目录下搜索匹配的文件夹名。`,
|
||||
description: `搜索项目文件。支持三种模式:1. 'content' (默认): 搜索文件内容,支持正则表达式;2. 'file_name': 在指定目录下搜索匹配的文件名;3. 'dir_name': 在指定目录下搜索匹配的文件夹名。`,
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
@@ -650,7 +660,7 @@ const getToolsList = () => {
|
||||
},
|
||||
{
|
||||
name: "get_sha",
|
||||
description: `${globalPrecautions} 获取指定文件的 SHA-256 哈希值`,
|
||||
description: `获取指定文件的 SHA-256 哈希值`,
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
@@ -999,7 +1009,7 @@ module.exports = {
|
||||
break;
|
||||
|
||||
case "get_scene_hierarchy":
|
||||
callSceneScriptWithTimeout("mcp-bridge", "get-hierarchy", null, callback);
|
||||
callSceneScriptWithTimeout("mcp-bridge", "get-hierarchy", args, callback);
|
||||
break;
|
||||
|
||||
case "update_node_transform":
|
||||
|
||||
@@ -64,41 +64,71 @@ module.exports = {
|
||||
/**
|
||||
* 获取当前场景的完整层级树
|
||||
* @param {Object} event IPC 事件对象
|
||||
* @param {Object} args 参数 (nodeId, depth, includeDetails)
|
||||
*/
|
||||
"get-hierarchy": function (event) {
|
||||
"get-hierarchy": function (event, args) {
|
||||
const { nodeId = null, depth = 2, includeDetails = false } = args || {};
|
||||
const scene = cc.director.getScene();
|
||||
|
||||
let rootNode = scene;
|
||||
if (nodeId) {
|
||||
rootNode = findNode(nodeId);
|
||||
if (!rootNode) {
|
||||
if (event.reply) event.reply(new Error(`找不到指定的起始节点: ${nodeId}`));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归遍历并序列化节点树
|
||||
* @param {cc.Node} node 目标节点
|
||||
* @param {number} currentDepth 当前深度
|
||||
* @returns {Object|null} 序列化后的节点数据
|
||||
*/
|
||||
function dumpNodes(node) {
|
||||
function dumpNodes(node, currentDepth) {
|
||||
// 【优化】跳过编辑器内部的私有节点,减少数据量
|
||||
if (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;
|
||||
}
|
||||
|
||||
let nodeData = {
|
||||
name: node.name,
|
||||
uuid: node.uuid,
|
||||
active: node.active,
|
||||
position: { x: Math.round(node.x), y: Math.round(node.y) },
|
||||
scale: { x: node.scaleX, y: node.scaleY },
|
||||
size: { width: node.width, height: node.height },
|
||||
// 记录组件类型,让 AI 知道这是个什么节点
|
||||
components: node._components.map((c) => c.__typename),
|
||||
children: [],
|
||||
childrenCount: node.childrenCount,
|
||||
};
|
||||
|
||||
const comps = node._components || [];
|
||||
|
||||
// 根据是否需要详情来决定附加哪些数据以节省 Token
|
||||
if (includeDetails) {
|
||||
nodeData.active = node.active;
|
||||
nodeData.position = { x: Math.round(node.x), y: Math.round(node.y) };
|
||||
nodeData.scale = { x: node.scaleX, y: node.scaleY };
|
||||
nodeData.size = { width: node.width, height: node.height };
|
||||
nodeData.components = comps.map((c) => cc.js.getClassName(c));
|
||||
} else {
|
||||
// 简略模式下如果存在组件,至少提供一个极简列表让 AI 知道节点的作用
|
||||
if (comps.length > 0) {
|
||||
nodeData.components = comps.map(c => {
|
||||
const parts = (cc.js.getClassName(c) || "").split('.');
|
||||
return parts[parts.length - 1]; // 只取类名,例如 cc.Sprite -> Sprite
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 如果未超出深度限制,继续递归子树
|
||||
if (currentDepth < depth && node.childrenCount > 0) {
|
||||
nodeData.children = [];
|
||||
for (let i = 0; i < node.childrenCount; i++) {
|
||||
let childData = dumpNodes(node.children[i]);
|
||||
let childData = dumpNodes(node.children[i], currentDepth + 1);
|
||||
if (childData) nodeData.children.push(childData);
|
||||
}
|
||||
}
|
||||
|
||||
return nodeData;
|
||||
}
|
||||
|
||||
const hierarchy = dumpNodes(scene);
|
||||
const hierarchy = dumpNodes(rootNode, 0);
|
||||
if (event.reply) event.reply(null, hierarchy);
|
||||
},
|
||||
|
||||
@@ -643,7 +673,12 @@ module.exports = {
|
||||
|
||||
// 基础类型是安全的
|
||||
if (typeof val !== "object") {
|
||||
// 【优化】对于超长字符串进行截断
|
||||
if (typeof val === "string" && val.length > 200) {
|
||||
properties[key] = val.substring(0, 50) + `...[Truncated, total length: ${val.length}]`;
|
||||
} else {
|
||||
properties[key] = val;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -658,11 +693,21 @@ module.exports = {
|
||||
properties[key] = `组件(${val.name}<${val.__typename}>)`;
|
||||
} else {
|
||||
// 数组和普通对象
|
||||
// 【优化】对于超长数组直接截断并提示,防止返回巨大的坐标或点集
|
||||
if (Array.isArray(val) && val.length > 10) {
|
||||
properties[key] = `[Array(${val.length})]`;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 尝试转换为纯 JSON 数据以避免 IPC 错误(如包含原生对象/循环引用)
|
||||
try {
|
||||
const jsonStr = JSON.stringify(val);
|
||||
if (jsonStr && jsonStr.length > 500) {
|
||||
properties[key] = `[Large JSON Object, length: ${jsonStr.length}]`;
|
||||
} else {
|
||||
// 确保不传递原始对象引用
|
||||
properties[key] = JSON.parse(jsonStr);
|
||||
}
|
||||
} catch (e) {
|
||||
// 如果 JSON 失败(例如循环引用),格式化为字符串
|
||||
properties[key] =
|
||||
@@ -772,8 +817,7 @@ module.exports = {
|
||||
const scene = cc.director.getScene();
|
||||
|
||||
function searchNode(node) {
|
||||
// 跳过编辑器内部的私有节点
|
||||
if (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;
|
||||
}
|
||||
|
||||
@@ -806,14 +850,16 @@ module.exports = {
|
||||
}
|
||||
|
||||
if (match) {
|
||||
const comps = node._components || [];
|
||||
result.push({
|
||||
uuid: node.uuid,
|
||||
name: node.name,
|
||||
active: node.active,
|
||||
position: { x: node.x, y: node.y },
|
||||
scale: { x: node.scaleX, y: node.scaleY },
|
||||
size: { width: node.width, height: node.height },
|
||||
components: node._components.map((c) => c.__typename),
|
||||
components: comps.map((c) => {
|
||||
const parts = (cc.js.getClassName(c) || "").split('.');
|
||||
return parts[parts.length - 1]; // 简化的组件名
|
||||
}),
|
||||
childrenCount: node.childrenCount
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
17
注意事项.md
17
注意事项.md
@@ -128,3 +128,20 @@
|
||||
- **背景**:`Editor.Scene.callSceneScript` 的回调依赖 Scene 面板响应 IPC 消息。如果主线程阻塞,Scene 面板无法处理消息,导致 callback 永远不返回,HTTP 连接堆积。
|
||||
- **解决方案**:所有 `callSceneScript` 调用均通过 `callSceneScriptWithTimeout` 包装,默认 15 秒超时。超时后自动返回错误,释放 HTTP 连接和队列位置。
|
||||
- **日志标识**:超时会记录 `[超时] callSceneScript "方法名" 超过 15000ms 未响应`。
|
||||
|
||||
---
|
||||
|
||||
## 10. Token 消耗与长数据保护防爆机制
|
||||
|
||||
### 10.1 `get_scene_hierarchy` 深度与层级限制
|
||||
- **背景**:在一两千个节点的大型 UI 场景中,无限制地获取全场景树会瞬间消耗十万以上的 Token,导致 AI 丢失上下文甚至触发截断报错。
|
||||
- **最佳实践**:
|
||||
- **默认使用 `depth: 2`** (默认限制) 来逐步探查。
|
||||
- **结合 `nodeId` 参数**:找到关键模块(例如 `Canvas/LoginPanel`)的 UUID 后,再单独向该 `nodeId` 请求下一层的结构,而非每次从根部拉取。
|
||||
|
||||
### 10.2 大对象与长数组截断
|
||||
- **背景**:在读取某些特定组件数据(如多边形顶点坐标、Sprite 曲线数据或序列化的内联 Base64 图片)时,JSON 可能会异常庞大。
|
||||
- **保护机制**:
|
||||
- `scene-script.js` 内部在执行 `manage_components(get)` 序列化时,对于**长度超过 10 的 Array** 会强制截断,返回字面量字符串 `"[Array(X)]"`。
|
||||
- 对于**长度大于 200 的长字符串**,也会强制缩略并追加 `...[Truncated, total length: X]`。
|
||||
- **应对策略**:如果 AI 看到截断提示,这意味着此处为海量无语义数据,**请勿**尝试盲目通过 `update` 覆盖或还原被截断的字段,极易导致源数据被破坏。请仅修改自己能够完全看清的轻量级属性(如 `name`, `x`, `scale` 等)。
|
||||
|
||||
Reference in New Issue
Block a user