2026-01-29 13:47:38 +08:00
"use strict" ;
2026-02-03 19:55:51 +08:00
const { IpcManager } = require ( "./dist/IpcManager" ) ;
2026-01-29 13:47:38 +08:00
const http = require ( "http" ) ;
2026-01-29 14:26:28 +08:00
const path = require ( "path" ) ;
2026-02-02 14:34:34 +08:00
const fs = require ( "fs" ) ;
2026-01-29 14:26:28 +08:00
2026-01-29 14:53:06 +08:00
let logBuffer = [ ] ; // 存储所有日志
let mcpServer = null ;
2026-02-01 13:30:11 +08:00
let isSceneBusy = false ;
2026-01-29 14:53:06 +08:00
let serverConfig = {
port : 3456 ,
active : false ,
} ;
// 封装日志函数,同时发送给面板和编辑器控制台
function addLog ( type , message ) {
const logEntry = {
time : new Date ( ) . toLocaleTimeString ( ) ,
type : type ,
content : message ,
} ;
logBuffer . push ( logEntry ) ;
Editor . Ipc . sendToPanel ( "mcp-bridge" , "mcp-bridge:on-log" , logEntry ) ;
2026-02-02 14:34:34 +08:00
// 【修改】确保所有日志都输出到编辑器控制台,以便用户查看
2026-01-29 14:53:06 +08:00
if ( type === "error" ) {
2026-02-02 14:34:34 +08:00
Editor . error ( ` [MCP] ${ message } ` ) ;
} else if ( type === "warn" ) {
Editor . warn ( ` [MCP] ${ message } ` ) ;
} else {
2026-01-29 14:53:06 +08:00
}
}
2026-02-02 14:34:34 +08:00
function getLogContent ( ) {
return logBuffer . map ( entry => ` [ ${ entry . time } ] [ ${ entry . type } ] ${ entry . content } ` ) . join ( '\n' ) ;
}
2026-01-29 14:26:28 +08:00
const getNewSceneTemplate = ( ) => {
// 尝试获取 UUID 生成函数
let newId = "" ;
if ( Editor . Utils && Editor . Utils . uuid ) {
newId = Editor . Utils . uuid ( ) ;
} else if ( Editor . Utils && Editor . Utils . UuidUtils && Editor . Utils . UuidUtils . uuid ) {
newId = Editor . Utils . UuidUtils . uuid ( ) ;
} else {
// 兜底方案:如果找不到编辑器 API, 生成一个随机字符串
newId = Math . random ( ) . toString ( 36 ) . substring ( 2 , 15 ) ;
}
const sceneData = [
{
_ _type _ _ : "cc.SceneAsset" ,
_name : "" ,
_objFlags : 0 ,
_native : "" ,
scene : { _ _id _ _ : 1 } ,
} ,
{
_ _id _ _ : 1 ,
_ _type _ _ : "cc.Scene" ,
_name : "" ,
_objFlags : 0 ,
_parent : null ,
_children : [ ] ,
_active : true ,
_level : 0 ,
_components : [ ] ,
autoReleaseAssets : false ,
_id : newId ,
} ,
] ;
return JSON . stringify ( sceneData ) ;
} ;
2026-01-29 13:47:38 +08:00
2026-01-29 15:55:38 +08:00
const getToolsList = ( ) => {
return [
{
name : "get_selected_node" ,
description : "获取当前编辑器中选中的节点 ID" ,
inputSchema : { type : "object" , properties : { } } ,
} ,
{
name : "set_node_name" ,
description : "修改指定节点的名称" ,
inputSchema : {
type : "object" ,
properties : {
id : { type : "string" , description : "节点的 UUID" } ,
newName : { type : "string" , description : "新的节点名称" } ,
} ,
required : [ "id" , "newName" ] ,
} ,
} ,
{
name : "save_scene" ,
description : "保存当前场景的修改" ,
inputSchema : { type : "object" , properties : { } } ,
} ,
{
name : "get_scene_hierarchy" ,
description : "获取当前场景的完整节点树结构(包括 UUID、名称和层级关系) " ,
inputSchema : { type : "object" , properties : { } } ,
} ,
{
name : "update_node_transform" ,
description : "修改节点的坐标、缩放或颜色" ,
inputSchema : {
type : "object" ,
properties : {
id : { type : "string" , description : "节点 UUID" } ,
x : { type : "number" } ,
y : { type : "number" } ,
scaleX : { type : "number" } ,
scaleY : { type : "number" } ,
color : { type : "string" , description : "HEX 颜色代码如 #FF0000" } ,
} ,
required : [ "id" ] ,
} ,
} ,
{
name : "create_scene" ,
description : "在 assets 目录下创建一个新的场景文件" ,
inputSchema : {
type : "object" ,
properties : {
sceneName : { type : "string" , description : "场景名称" } ,
} ,
required : [ "sceneName" ] ,
} ,
} ,
{
name : "create_prefab" ,
description : "将场景中的某个节点保存为预制体资源" ,
inputSchema : {
type : "object" ,
properties : {
nodeId : { type : "string" , description : "节点 UUID" } ,
prefabName : { type : "string" , description : "预制体名称" } ,
} ,
required : [ "nodeId" , "prefabName" ] ,
} ,
} ,
{
name : "open_scene" ,
2026-01-31 16:48:21 +08:00
description : "打开场景文件。注意:这是一个异步且耗时的操作,打开后请等待几秒再进行节点创建或保存操作。" ,
2026-01-29 15:55:38 +08:00
inputSchema : {
type : "object" ,
properties : {
url : {
type : "string" ,
description : "场景资源路径,如 db://assets/NewScene.fire" ,
} ,
} ,
required : [ "url" ] ,
} ,
} ,
{
name : "create_node" ,
description : "在当前场景中创建一个新节点" ,
inputSchema : {
type : "object" ,
properties : {
name : { type : "string" , description : "节点名称" } ,
parentId : {
type : "string" ,
description : "父节点 UUID (可选,不传则挂在场景根部)" ,
} ,
type : {
type : "string" ,
enum : [ "empty" , "sprite" , "label" ] ,
description : "节点预设类型" ,
} ,
} ,
required : [ "name" ] ,
} ,
} ,
2026-01-31 16:48:21 +08:00
{
name : "manage_components" ,
description : "管理节点组件" ,
inputSchema : {
type : "object" ,
properties : {
nodeId : { type : "string" , description : "节点 UUID" } ,
2026-02-03 19:55:51 +08:00
action : { type : "string" , enum : [ "add" , "remove" , "update" , "get" ] , description : "操作类型 (add: 添加组件, remove: 移除组件, update: 更新组件属性, get: 获取组件列表)" } ,
componentType : { type : "string" , description : "组件类型,如 cc.Sprite (add/update 操作需要)" } ,
componentId : { type : "string" , description : "组件 ID (remove/update 操作可选)" } ,
properties : { type : "object" , description : "组件属性 (add/update 操作使用). 支持智能解析: 如果属性类型是组件但提供了节点UUID, 会自动查找对应组件。" } ,
2026-01-31 16:48:21 +08:00
} ,
required : [ "nodeId" , "action" ] ,
} ,
} ,
{
name : "manage_script" ,
2026-02-03 19:55:51 +08:00
description : "管理脚本文件。注意:创建或修改脚本后,编辑器需要时间进行编译(通常几秒钟)。新脚本在编译完成前无法作为组件添加到节点。建议在 create 后调用 refresh_editor, 或等待一段时间后再使用 manage_components。" ,
2026-01-31 16:48:21 +08:00
inputSchema : {
type : "object" ,
properties : {
action : { type : "string" , enum : [ "create" , "delete" , "read" , "write" ] , description : "操作类型" } ,
path : { type : "string" , description : "脚本路径,如 db://assets/scripts/NewScript.js" } ,
content : { type : "string" , description : "脚本内容 (用于 create 和 write 操作)" } ,
name : { type : "string" , description : "脚本名称 (用于 create 操作)" } ,
} ,
required : [ "action" , "path" ] ,
} ,
} ,
{
name : "batch_execute" ,
description : "批处理执行多个操作" ,
inputSchema : {
type : "object" ,
properties : {
operations : {
type : "array" ,
items : {
type : "object" ,
properties : {
tool : { type : "string" , description : "工具名称" } ,
params : { type : "object" , description : "工具参数" } ,
} ,
required : [ "tool" , "params" ] ,
} ,
description : "操作列表" ,
} ,
} ,
required : [ "operations" ] ,
} ,
} ,
{
name : "manage_asset" ,
description : "管理资源" ,
inputSchema : {
type : "object" ,
properties : {
action : { type : "string" , enum : [ "create" , "delete" , "move" , "get_info" ] , description : "操作类型" } ,
path : { type : "string" , description : "资源路径,如 db://assets/textures" } ,
targetPath : { type : "string" , description : "目标路径 (用于 move 操作)" } ,
content : { type : "string" , description : "资源内容 (用于 create 操作)" } ,
} ,
required : [ "action" , "path" ] ,
} ,
} ,
2026-01-31 19:36:55 +08:00
{
name : "scene_management" ,
description : "场景管理" ,
inputSchema : {
type : "object" ,
properties : {
2026-02-01 13:30:11 +08:00
action : {
type : "string" ,
enum : [ "create" , "delete" , "duplicate" , "get_info" ] ,
description : "操作类型" ,
} ,
2026-01-31 19:36:55 +08:00
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 : {
2026-02-01 13:30:11 +08:00
action : {
type : "string" ,
enum : [ "create" , "update" , "instantiate" , "get_info" ] ,
description : "操作类型" ,
} ,
2026-01-31 19:36:55 +08:00
path : { type : "string" , description : "预制体路径,如 db://assets/prefabs/NewPrefab.prefab" } ,
nodeId : { type : "string" , description : "节点 ID (用于 create 操作)" } ,
parentId : { type : "string" , description : "父节点 ID (用于 instantiate 操作)" } ,
} ,
required : [ "action" , "path" ] ,
} ,
} ,
2026-02-01 13:30:11 +08:00
{
name : "manage_editor" ,
description : "管理编辑器" ,
inputSchema : {
type : "object" ,
properties : {
action : {
type : "string" ,
enum : [ "get_selection" , "set_selection" , "refresh_editor" ] ,
description : "操作类型" ,
} ,
target : {
type : "string" ,
enum : [ "node" , "asset" ] ,
description : "目标类型 (用于 set_selection 操作)" ,
} ,
properties : { type : "object" , description : "操作属性" } ,
} ,
required : [ "action" ] ,
} ,
} ,
{
name : "find_gameobjects" ,
description : "查找游戏对象" ,
inputSchema : {
type : "object" ,
properties : {
conditions : { type : "object" , description : "查找条件" } ,
recursive : { type : "boolean" , default : true , description : "是否递归查找" } ,
} ,
required : [ "conditions" ] ,
} ,
} ,
{
name : "manage_material" ,
description : "管理材质" ,
inputSchema : {
type : "object" ,
properties : {
action : { type : "string" , enum : [ "create" , "delete" , "get_info" ] , description : "操作类型" } ,
path : { type : "string" , description : "材质路径,如 db://assets/materials/NewMaterial.mat" } ,
properties : { type : "object" , description : "材质属性" } ,
} ,
required : [ "action" , "path" ] ,
} ,
} ,
{
name : "manage_texture" ,
description : "管理纹理" ,
inputSchema : {
type : "object" ,
properties : {
action : { type : "string" , enum : [ "create" , "delete" , "get_info" ] , description : "操作类型" } ,
path : { type : "string" , description : "纹理路径,如 db://assets/textures/NewTexture.png" } ,
properties : { type : "object" , description : "纹理属性" } ,
} ,
required : [ "action" , "path" ] ,
} ,
} ,
{
name : "execute_menu_item" ,
description : "执行菜单项" ,
inputSchema : {
type : "object" ,
properties : {
menuPath : { type : "string" , description : "菜单项路径" } ,
} ,
required : [ "menuPath" ] ,
} ,
} ,
{
name : "apply_text_edits" ,
description : "应用文本编辑" ,
inputSchema : {
type : "object" ,
properties : {
filePath : { type : "string" , description : "文件路径" } ,
edits : { type : "array" , items : { type : "object" } , description : "编辑操作列表" } ,
} ,
required : [ "filePath" , "edits" ] ,
} ,
} ,
{
name : "read_console" ,
description : "读取控制台" ,
inputSchema : {
type : "object" ,
properties : {
limit : { type : "number" , description : "输出限制" } ,
type : { type : "string" , enum : [ "log" , "error" , "warn" ] , description : "输出类型" } ,
} ,
} ,
} ,
{
name : "validate_script" ,
description : "验证脚本" ,
inputSchema : {
type : "object" ,
properties : {
filePath : { type : "string" , description : "脚本路径" } ,
} ,
required : [ "filePath" ] ,
} ,
} ,
{
name : "find_in_file" ,
description : "在项目中全局搜索文本内容" ,
inputSchema : {
type : "object" ,
properties : {
query : { type : "string" , description : "搜索关键词" } ,
extensions : {
type : "array" ,
items : { type : "string" } ,
description : "文件后缀列表 (例如 ['.js', '.ts'])" ,
default : [ ".js" , ".ts" , ".json" , ".fire" , ".prefab" , ".xml" , ".txt" , ".md" ]
} ,
includeSubpackages : { type : "boolean" , default : true , description : "是否搜索子包 (暂时默认搜索 assets 目录)" }
} ,
required : [ "query" ]
}
} ,
{
name : "manage_undo" ,
description : "管理编辑器的撤销和重做历史" ,
inputSchema : {
type : "object" ,
properties : {
action : {
type : "string" ,
enum : [ "undo" , "redo" , "begin_group" , "end_group" , "cancel_group" ] ,
description : "操作类型"
} ,
description : { type : "string" , description : "撤销组的描述 (用于 begin_group)" }
} ,
required : [ "action" ]
}
} ,
{
name : "manage_vfx" ,
description : "管理全场景特效 (粒子系统)" ,
inputSchema : {
type : "object" ,
properties : {
action : {
type : "string" ,
enum : [ "create" , "update" , "get_info" ] ,
description : "操作类型"
} ,
nodeId : { type : "string" , description : "节点 UUID (用于 update/get_info)" } ,
properties : {
type : "object" ,
description : "粒子系统属性 (用于 create/update)" ,
properties : {
duration : { type : "number" , description : "发射时长" } ,
emissionRate : { type : "number" , description : "发射速率" } ,
life : { type : "number" , description : "生命周期" } ,
lifeVar : { type : "number" , description : "生命周期变化" } ,
startColor : { type : "string" , description : "起始颜色 (Hex)" } ,
endColor : { type : "string" , description : "结束颜色 (Hex)" } ,
startSize : { type : "number" , description : "起始大小" } ,
endSize : { type : "number" , description : "结束大小" } ,
speed : { type : "number" , description : "速度" } ,
angle : { type : "number" , description : "角度" } ,
gravity : { type : "object" , properties : { x : { type : "number" } , y : { type : "number" } } } ,
file : { type : "string" , description : "粒子文件路径 (plist) 或 texture 路径" }
}
} ,
name : { type : "string" , description : "节点名称 (用于 create)" } ,
parentId : { type : "string" , description : "父节点 ID (用于 create)" }
} ,
required : [ "action" ]
}
}
2026-01-29 15:55:38 +08:00
] ;
} ;
2026-02-01 13:30:11 +08:00
2026-01-29 13:47:38 +08:00
module . exports = {
"scene-script" : "scene-script.js" ,
load ( ) {
2026-01-29 14:53:06 +08:00
addLog ( "info" , "MCP Bridge Plugin Loaded" ) ;
// 读取配置
let profile = this . getProfile ( ) ;
serverConfig . port = profile . get ( "last-port" ) || 3456 ;
let autoStart = profile . get ( "auto-start" ) ;
if ( autoStart ) {
addLog ( "info" , "Auto-start is enabled. Initializing server..." ) ;
// 延迟一点启动,确保编辑器环境完全就绪
setTimeout ( ( ) => {
this . startServer ( serverConfig . port ) ;
} , 1000 ) ;
}
} ,
// 获取配置文件的辅助函数
getProfile ( ) {
// 'local' 表示存储在项目本地( local/mcp-bridge.json)
return Editor . Profile . load ( "profile://local/mcp-bridge.json" , "mcp-bridge" ) ;
2026-01-29 13:47:38 +08:00
} ,
unload ( ) {
2026-01-29 14:53:06 +08:00
this . stopServer ( ) ;
2026-01-29 13:47:38 +08:00
} ,
2026-01-29 14:53:06 +08:00
startServer ( port ) {
if ( mcpServer ) this . stopServer ( ) ;
2026-01-29 13:47:38 +08:00
2026-01-29 14:53:06 +08:00
try {
mcpServer = http . createServer ( ( req , res ) => {
res . setHeader ( "Content-Type" , "application/json" ) ;
2026-01-29 15:55:38 +08:00
res . setHeader ( "Access-Control-Allow-Origin" , "*" ) ;
2026-01-29 13:47:38 +08:00
2026-01-29 14:53:06 +08:00
let body = "" ;
req . on ( "data" , ( chunk ) => {
body += chunk ;
} ) ;
req . on ( "end" , ( ) => {
2026-01-29 15:55:38 +08:00
const url = req . url ;
if ( url === "/list-tools" ) {
const tools = getToolsList ( ) ;
addLog ( "info" , ` AI Client requested tool list ` ) ;
// 明确返回成功结构
res . writeHead ( 200 ) ;
return res . end ( JSON . stringify ( { tools : tools } ) ) ;
2026-02-02 14:34:34 +08:00
res . writeHead ( 200 ) ;
return res . end ( JSON . stringify ( { tools : tools } ) ) ;
}
if ( url === "/list-resources" ) {
const resources = this . getResourcesList ( ) ;
addLog ( "info" , ` AI Client requested resource list ` ) ;
res . writeHead ( 200 ) ;
return res . end ( JSON . stringify ( { resources : resources } ) ) ;
}
if ( url === "/read-resource" ) {
try {
const { uri } = JSON . parse ( body || "{}" ) ;
addLog ( "mcp" , ` READ -> [ ${ uri } ] ` ) ;
this . handleReadResource ( uri , ( err , content ) => {
if ( err ) {
addLog ( "error" , ` 读取失败: ${ err } ` ) ;
res . writeHead ( 500 ) ;
return res . end ( JSON . stringify ( { error : err } ) ) ;
}
addLog ( "success" , ` 读取成功: ${ uri } ` ) ;
res . writeHead ( 200 ) ;
// 返回 MCP Resource 格式: { contents: [{ uri, mimeType, text }] }
res . end ( JSON . stringify ( {
contents : [ {
uri : uri ,
mimeType : "application/json" ,
text : typeof content === 'string' ? content : JSON . stringify ( content )
} ]
} ) ) ;
} ) ;
} catch ( e ) {
res . writeHead ( 500 ) ;
res . end ( JSON . stringify ( { error : e . message } ) ) ;
}
return ;
2026-01-29 15:55:38 +08:00
}
if ( url === "/call-tool" ) {
try {
const { name , arguments : args } = JSON . parse ( body || "{}" ) ;
addLog ( "mcp" , ` REQ -> [ ${ name } ] ` ) ;
2026-01-29 14:53:06 +08:00
2026-01-29 15:55:38 +08:00
this . handleMcpCall ( name , args , ( err , result ) => {
const response = {
content : [
{
type : "text" ,
text : err
? ` Error: ${ err } `
: typeof result === "object"
? JSON . stringify ( result , null , 2 )
: result ,
} ,
] ,
} ;
2026-02-01 13:30:11 +08:00
if ( err ) {
addLog ( "error" , ` RES <- [ ${ name } ] 失败: ${ err } ` ) ;
} else {
// 成功时尝试捕获简单的结果预览(如果是字符串或简短对象)
let preview = "" ;
if ( typeof result === 'string' ) {
preview = result . length > 100 ? result . substring ( 0 , 100 ) + "..." : result ;
} else if ( typeof result === 'object' ) {
try {
const jsonStr = JSON . stringify ( result ) ;
preview = jsonStr . length > 100 ? jsonStr . substring ( 0 , 100 ) + "..." : jsonStr ;
} catch ( e ) {
preview = "Object (Circular/Unserializable)" ;
}
}
addLog ( "success" , ` RES <- [ ${ name } ] 成功 : ${ preview } ` ) ;
}
2026-01-29 15:55:38 +08:00
res . writeHead ( 200 ) ;
res . end ( JSON . stringify ( response ) ) ;
} ) ;
} catch ( e ) {
2026-02-01 13:30:11 +08:00
if ( e instanceof SyntaxError ) {
addLog ( "error" , ` JSON Parse Error: ${ e . message } ` ) ;
res . writeHead ( 400 ) ;
res . end ( JSON . stringify ( { error : "Invalid JSON" } ) ) ;
} else {
addLog ( "error" , ` Internal Server Error: ${ e . message } ` ) ;
res . writeHead ( 500 ) ;
res . end ( JSON . stringify ( { error : e . message } ) ) ;
}
2026-01-29 13:47:38 +08:00
}
2026-01-29 15:55:38 +08:00
return ;
2026-01-29 13:47:38 +08:00
}
2026-01-29 15:55:38 +08:00
// --- 兜底处理 (404) ---
res . writeHead ( 404 ) ;
res . end ( JSON . stringify ( { error : "Not Found" , url : url } ) ) ;
2026-01-29 14:53:06 +08:00
} ) ;
} ) ;
2026-01-29 15:55:38 +08:00
mcpServer . on ( "error" , ( e ) => {
addLog ( "error" , ` Server Error: ${ e . message } ` ) ;
} ) ;
2026-01-29 14:53:06 +08:00
mcpServer . listen ( port , ( ) => {
serverConfig . active = true ;
2026-01-29 15:55:38 +08:00
addLog ( "success" , ` MCP Server running at http://127.0.0.1: ${ port } ` ) ;
2026-01-29 14:53:06 +08:00
Editor . Ipc . sendToPanel ( "mcp-bridge" , "mcp-bridge:state-changed" , serverConfig ) ;
} ) ;
// 启动成功后顺便存一下端口
this . getProfile ( ) . set ( "last-port" , port ) ;
this . getProfile ( ) . save ( ) ;
} catch ( e ) {
addLog ( "error" , ` Failed to start server: ${ e . message } ` ) ;
}
} ,
stopServer ( ) {
if ( mcpServer ) {
mcpServer . close ( ) ;
mcpServer = null ;
serverConfig . active = false ;
addLog ( "warn" , "MCP Server stopped" ) ;
Editor . Ipc . sendToPanel ( "mcp-bridge" , "mcp-bridge:state-changed" , serverConfig ) ;
}
} ,
2026-02-02 14:34:34 +08:00
getResourcesList ( ) {
return [
{
uri : "cocos://hierarchy" ,
name : "Scene Hierarchy" ,
description : "当前场景层级的 JSON 快照" ,
mimeType : "application/json"
} ,
{
uri : "cocos://selection" ,
name : "Current Selection" ,
description : "当前选中节点的 UUID 列表" ,
mimeType : "application/json"
} ,
{
uri : "cocos://logs/latest" ,
name : "Editor Logs" ,
description : "最新的编辑器日志 (内存缓存)" ,
mimeType : "text/plain"
}
] ;
} ,
handleReadResource ( uri , callback ) {
let parsed ;
try {
parsed = new URL ( uri ) ;
} catch ( e ) {
return callback ( ` Invalid URI: ${ uri } ` ) ;
}
if ( parsed . protocol !== "cocos:" ) {
return callback ( ` Unsupported protocol: ${ parsed . protocol } ` ) ;
}
const type = parsed . hostname ; // hierarchy, selection, logs
switch ( type ) {
case "hierarchy" :
// 注意: query-hierarchy 是异步的
Editor . Ipc . sendToPanel ( "scene" , "scene:query-hierarchy" , ( err , sceneId , hierarchy ) => {
if ( err ) return callback ( err ) ;
callback ( null , JSON . stringify ( hierarchy , null , 2 ) ) ;
} ) ;
break ;
case "selection" :
const selection = Editor . Selection . curSelection ( "node" ) ;
callback ( null , JSON . stringify ( selection ) ) ;
break ;
case "logs" :
callback ( null , getLogContent ( ) ) ;
break ;
default :
callback ( ` Resource not found: ${ uri } ` ) ;
break ;
}
} ,
2026-01-29 14:53:06 +08:00
handleMcpCall ( name , args , callback ) {
2026-01-31 16:48:21 +08:00
if ( isSceneBusy && ( name === "save_scene" || name === "create_node" ) ) {
return callback ( "Editor is busy (Processing Scene), please wait a moment." ) ;
}
2026-01-29 14:53:06 +08:00
switch ( name ) {
case "get_selected_node" :
const ids = Editor . Selection . curSelection ( "node" ) ;
callback ( null , ids ) ;
break ;
case "set_node_name" :
2026-02-01 13:30:11 +08:00
// 使用 scene:set-property 以支持撤销
Editor . Ipc . sendToPanel ( "scene" , "scene:set-property" , {
id : args . id ,
path : "name" ,
type : "String" ,
value : args . newName ,
isSubProp : false
} ) ;
callback ( null , ` Node name updated to ${ args . newName } ` ) ;
2026-01-29 14:53:06 +08:00
break ;
case "save_scene" :
2026-01-31 16:48:21 +08:00
isSceneBusy = true ;
addLog ( "info" , "Preparing to save scene... Waiting for UI sync." ) ;
// 强制延迟保存,防止死锁
setTimeout ( ( ) => {
2026-02-02 10:14:17 +08:00
// 使用 stash-and-save 替代 save-scene, 这更接近 Ctrl+S 的行为
Editor . Ipc . sendToMain ( "scene:stash-and-save" ) ;
addLog ( "info" , "Executing Safe Save (Stash)..." ) ;
2026-01-31 16:48:21 +08:00
setTimeout ( ( ) => {
isSceneBusy = false ;
addLog ( "info" , "Safe Save completed." ) ;
callback ( null , "Scene saved successfully." ) ;
} , 1000 ) ;
} , 500 ) ;
2026-01-29 14:53:06 +08:00
break ;
case "get_scene_hierarchy" :
Editor . Scene . callSceneScript ( "mcp-bridge" , "get-hierarchy" , callback ) ;
break ;
case "update_node_transform" :
2026-02-02 14:34:34 +08:00
// 直接调用场景脚本更新属性,绕过可能导致 "Unknown object" 的复杂 Undo 系统
Editor . Scene . callSceneScript ( "mcp-bridge" , "update-node-transform" , args , ( err , result ) => {
if ( err ) {
addLog ( "error" , ` Transform update failed: ${ err } ` ) ;
callback ( err ) ;
} else {
callback ( null , "Transform updated" ) ;
2026-02-01 13:30:11 +08:00
}
2026-02-02 14:34:34 +08:00
} ) ;
2026-01-29 14:53:06 +08:00
break ;
case "create_scene" :
const sceneUrl = ` db://assets/ ${ args . sceneName } .fire ` ;
if ( Editor . assetdb . exists ( sceneUrl ) ) {
return callback ( "Scene already exists" ) ;
}
Editor . assetdb . create ( sceneUrl , getNewSceneTemplate ( ) , ( err ) => {
callback ( err , err ? null : ` Standard Scene created at ${ sceneUrl } ` ) ;
} ) ;
break ;
case "create_prefab" :
const prefabUrl = ` db://assets/ ${ args . prefabName } .prefab ` ;
Editor . Ipc . sendToMain ( "scene:create-prefab" , args . nodeId , prefabUrl ) ;
callback ( null , ` Command sent: Creating prefab ' ${ args . prefabName } ' ` ) ;
break ;
case "open_scene" :
2026-01-31 16:48:21 +08:00
isSceneBusy = true ; // 锁定
2026-01-29 14:53:06 +08:00
const openUuid = Editor . assetdb . urlToUuid ( args . url ) ;
if ( openUuid ) {
Editor . Ipc . sendToMain ( "scene:open-by-uuid" , openUuid ) ;
2026-01-31 16:48:21 +08:00
setTimeout ( ( ) => {
isSceneBusy = false ;
callback ( null , ` Success: Opening scene ${ args . url } ` ) ;
} , 2000 ) ;
2026-01-29 14:53:06 +08:00
} else {
2026-01-31 16:48:21 +08:00
isSceneBusy = false ;
2026-01-29 14:53:06 +08:00
callback ( ` Could not find asset with URL ${ args . url } ` ) ;
}
break ;
case "create_node" :
Editor . Scene . callSceneScript ( "mcp-bridge" , "create-node" , args , callback ) ;
break ;
2026-01-31 16:48:21 +08:00
case "manage_components" :
Editor . Scene . callSceneScript ( "mcp-bridge" , "manage-components" , args , callback ) ;
break ;
case "manage_script" :
this . manageScript ( args , callback ) ;
break ;
case "batch_execute" :
this . batchExecute ( args , callback ) ;
break ;
case "manage_asset" :
this . manageAsset ( args , callback ) ;
break ;
2026-01-31 19:36:55 +08:00
case "scene_management" :
this . sceneManagement ( args , callback ) ;
break ;
case "prefab_management" :
this . prefabManagement ( args , callback ) ;
break ;
2026-02-01 13:30:11 +08:00
case "manage_editor" :
this . manageEditor ( args , callback ) ;
break ;
case "find_gameobjects" :
Editor . Scene . callSceneScript ( "mcp-bridge" , "find-gameobjects" , args , callback ) ;
break ;
case "manage_material" :
this . manageMaterial ( args , callback ) ;
break ;
case "manage_texture" :
this . manageTexture ( args , callback ) ;
break ;
case "execute_menu_item" :
this . executeMenuItem ( args , callback ) ;
break ;
case "apply_text_edits" :
this . applyTextEdits ( args , callback ) ;
break ;
case "read_console" :
this . readConsole ( args , callback ) ;
break ;
case "validate_script" :
this . validateScript ( args , callback ) ;
break ;
case "find_in_file" :
this . findInFile ( args , callback ) ;
break ;
case "manage_undo" :
this . manageUndo ( args , callback ) ;
break ;
case "manage_vfx" :
// 【修复】在主进程预先解析 URL 为 UUID, 因为渲染进程(scene-script)无法访问 Editor.assetdb
if ( args . properties && args . properties . file ) {
if ( typeof args . properties . file === 'string' && args . properties . file . startsWith ( "db://" ) ) {
const uuid = Editor . assetdb . urlToUuid ( args . properties . file ) ;
if ( uuid ) {
args . properties . file = uuid ; // 替换为 UUID
} else {
console . warn ( ` Failed to resolve path to UUID: ${ args . properties . file } ` ) ;
}
}
}
// 预先获取默认贴图 UUID (尝试多个可能的路径)
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 ) {
args . defaultSpriteUuid = uuid ;
addLog ( "info" , ` [mcp-bridge] Resolved Default Sprite UUID: ${ uuid } from ${ path } ` ) ;
break ;
}
}
if ( ! args . defaultSpriteUuid ) {
addLog ( "warn" , "[mcp-bridge] Failed to resolve any default sprite UUID." ) ;
}
Editor . Scene . callSceneScript ( "mcp-bridge" , "manage-vfx" , args , callback ) ;
break ;
2026-01-29 14:53:06 +08:00
default :
callback ( ` Unknown tool: ${ name } ` ) ;
break ;
}
} ,
2026-01-31 16:48:21 +08:00
// 管理脚本文件
manageScript ( args , callback ) {
2026-02-02 14:34:34 +08:00
const { action , path : scriptPath , content } = args ;
2026-01-31 16:48:21 +08:00
switch ( action ) {
case "create" :
2026-02-02 14:34:34 +08:00
if ( Editor . assetdb . exists ( scriptPath ) ) {
return callback ( ` Script already exists at ${ scriptPath } ` ) ;
2026-01-31 16:48:21 +08:00
}
// 确保父目录存在
2026-02-02 14:34:34 +08:00
const absolutePath = Editor . assetdb . urlToFspath ( scriptPath ) ;
const dirPath = path . dirname ( absolutePath ) ;
2026-01-31 16:48:21 +08:00
if ( ! fs . existsSync ( dirPath ) ) {
fs . mkdirSync ( dirPath , { recursive : true } ) ;
}
2026-02-01 13:30:11 +08:00
Editor . assetdb . create (
2026-02-02 14:34:34 +08:00
scriptPath ,
2026-02-01 13:30:11 +08:00
content ||
` const { ccclass, property } = cc._decorator;
2026-01-31 16:48:21 +08:00
@ ccclass
export default class NewScript extends cc . Component {
@ property ( cc . Label )
label : cc . Label = null ;
@ property
text : string = 'hello' ;
// LIFE-CYCLE CALLBACKS:
onLoad ( ) { }
start ( ) { }
update ( dt ) { }
2026-02-01 13:30:11 +08:00
} ` ,
( err ) => {
2026-02-03 19:55:51 +08:00
if ( err ) {
callback ( err ) ;
} else {
// 【关键修复】创建脚本后,必须刷新 AssetDB 并等待完成,
// 否则后续立即挂载脚本的操作(manage_components)会因找不到脚本 UUID 而失败。
Editor . assetdb . refresh ( scriptPath , ( refreshErr ) => {
if ( refreshErr ) {
addLog ( "warn" , ` Refresh failed after script creation: ${ refreshErr } ` ) ;
}
callback ( null , ` Script created at ${ scriptPath } ` ) ;
} ) ;
}
2026-02-01 13:30:11 +08:00
} ,
) ;
2026-01-31 16:48:21 +08:00
break ;
case "delete" :
2026-02-02 14:34:34 +08:00
if ( ! Editor . assetdb . exists ( scriptPath ) ) {
return callback ( ` Script not found at ${ scriptPath } ` ) ;
2026-01-31 16:48:21 +08:00
}
2026-02-02 14:34:34 +08:00
Editor . assetdb . delete ( [ scriptPath ] , ( err ) => {
callback ( err , err ? null : ` Script deleted at ${ scriptPath } ` ) ;
2026-01-31 16:48:21 +08:00
} ) ;
break ;
case "read" :
2026-02-02 14:34:34 +08:00
// 使用 fs 读取,绕过 assetdb.loadAny
const readFsPath = Editor . assetdb . urlToFspath ( scriptPath ) ;
if ( ! readFsPath || ! fs . existsSync ( readFsPath ) ) {
return callback ( ` Script not found at ${ scriptPath } ` ) ;
}
try {
const content = fs . readFileSync ( readFsPath , "utf-8" ) ;
callback ( null , content ) ;
} catch ( e ) {
callback ( ` Failed to read script: ${ e . message } ` ) ;
}
2026-01-31 16:48:21 +08:00
break ;
case "write" :
2026-02-02 14:34:34 +08:00
// 使用 fs 写入 + refresh, 确保覆盖成功
const writeFsPath = Editor . assetdb . urlToFspath ( scriptPath ) ;
if ( ! writeFsPath ) {
return callback ( ` Invalid path: ${ scriptPath } ` ) ;
}
try {
fs . writeFileSync ( writeFsPath , content , "utf-8" ) ;
Editor . assetdb . refresh ( scriptPath , ( err ) => {
if ( err ) addLog ( "warn" , ` Refresh failed after write: ${ err } ` ) ;
callback ( null , ` Script updated at ${ scriptPath } ` ) ;
} ) ;
} catch ( e ) {
callback ( ` Failed to write script: ${ e . message } ` ) ;
}
2026-01-31 16:48:21 +08:00
break ;
default :
callback ( ` Unknown script action: ${ action } ` ) ;
break ;
}
} ,
// 批处理执行
batchExecute ( args , callback ) {
const { operations } = args ;
const results = [ ] ;
let completed = 0 ;
if ( ! operations || operations . length === 0 ) {
return callback ( "No operations provided" ) ;
}
operations . forEach ( ( operation , index ) => {
this . handleMcpCall ( operation . tool , operation . params , ( err , result ) => {
results [ index ] = { tool : operation . tool , error : err , result : result } ;
completed ++ ;
if ( completed === operations . length ) {
callback ( null , results ) ;
}
} ) ;
} ) ;
} ,
// 管理资源
manageAsset ( args , callback ) {
const { action , path , targetPath , content } = args ;
switch ( action ) {
case "create" :
if ( Editor . assetdb . exists ( path ) ) {
return callback ( ` Asset already exists at ${ path } ` ) ;
}
// 确保父目录存在
2026-02-01 13:30:11 +08:00
const fs = require ( "fs" ) ;
const pathModule = require ( "path" ) ;
2026-01-31 16:48:21 +08:00
const absolutePath = Editor . assetdb . urlToFspath ( path ) ;
const dirPath = pathModule . dirname ( absolutePath ) ;
if ( ! fs . existsSync ( dirPath ) ) {
fs . mkdirSync ( dirPath , { recursive : true } ) ;
}
2026-02-01 13:30:11 +08:00
Editor . assetdb . create ( path , content || "" , ( err ) => {
2026-01-31 16:48:21 +08:00
callback ( err , err ? null : ` Asset created at ${ path } ` ) ;
} ) ;
break ;
case "delete" :
if ( ! Editor . assetdb . exists ( path ) ) {
return callback ( ` Asset not found at ${ path } ` ) ;
}
Editor . assetdb . delete ( [ path ] , ( err ) => {
callback ( err , err ? null : ` Asset deleted at ${ path } ` ) ;
} ) ;
break ;
case "move" :
if ( ! Editor . assetdb . exists ( path ) ) {
return callback ( ` Asset not found at ${ path } ` ) ;
}
if ( Editor . assetdb . exists ( targetPath ) ) {
return callback ( ` Target asset already exists at ${ targetPath } ` ) ;
}
Editor . assetdb . move ( path , targetPath , ( err ) => {
callback ( err , err ? null : ` Asset moved from ${ path } to ${ targetPath } ` ) ;
} ) ;
break ;
2026-02-02 14:34:34 +08:00
2026-01-31 16:48:21 +08:00
case "get_info" :
2026-02-01 13:30:11 +08:00
try {
if ( ! Editor . assetdb . exists ( path ) ) {
return callback ( ` Asset not found: ${ path } ` ) ;
}
const uuid = Editor . assetdb . urlToUuid ( path ) ;
2026-02-02 14:34:34 +08:00
const info = Editor . assetdb . assetInfoByUuid ( uuid ) ;
if ( info ) {
callback ( null , info ) ;
} else {
// Fallback if API returns nothing but asset exists
callback ( null , { url : path , uuid : uuid , exists : true } ) ;
}
2026-02-01 13:30:11 +08:00
} catch ( e ) {
callback ( ` Error getting asset info: ${ e . message } ` ) ;
}
2026-01-31 16:48:21 +08:00
break ;
default :
callback ( ` Unknown asset action: ${ action } ` ) ;
break ;
2026-02-01 13:30:11 +08:00
}
} ,
2026-01-31 19:36:55 +08:00
2026-02-01 13:30:11 +08:00
// 场景管理
sceneManagement ( args , callback ) {
const { action , path , targetPath , name } = args ;
2026-01-31 19:36:55 +08:00
2026-02-01 13:30:11 +08:00
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 ;
2026-01-31 19:36:55 +08:00
2026-02-01 13:30:11 +08:00
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 ;
2026-01-31 19:36:55 +08:00
2026-02-01 13:30:11 +08:00
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 } ` ) ;
2026-01-31 19:36:55 +08:00
}
2026-02-01 13:30:11 +08:00
// 确保目标目录存在
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 } ) ;
2026-01-31 19:36:55 +08:00
}
2026-02-01 13:30:11 +08:00
// 创建复制的场景
Editor . assetdb . create ( targetPath , content , ( err ) => {
callback ( err , err ? null : ` Scene duplicated from ${ path } to ${ targetPath } ` ) ;
2026-01-31 19:36:55 +08:00
} ) ;
2026-02-01 13:30:11 +08:00
} ) ;
break ;
2026-01-31 19:36:55 +08:00
2026-02-01 13:30:11 +08:00
case "get_info" :
2026-02-02 14:34:34 +08:00
if ( Editor . assetdb . exists ( path ) ) {
const uuid = Editor . assetdb . urlToUuid ( path ) ;
const info = Editor . assetdb . assetInfoByUuid ( uuid ) ;
callback ( null , info || { url : path , uuid : uuid , exists : true } ) ;
} else {
callback ( ` Scene not found: ${ path } ` ) ;
}
2026-02-01 13:30:11 +08:00
break ;
2026-01-31 19:36:55 +08:00
2026-02-01 13:30:11 +08:00
default :
2026-01-31 19:36:55 +08:00
callback ( ` Unknown scene action: ${ action } ` ) ;
break ;
2026-02-01 13:30:11 +08:00
}
} ,
// 预制体管理
prefabManagement ( args , callback ) {
2026-02-02 14:34:34 +08:00
const { action , path : prefabPath , nodeId , parentId } = args ;
2026-02-01 13:30:11 +08:00
switch ( action ) {
case "create" :
if ( ! nodeId ) {
return callback ( ` Node ID is required for create operation ` ) ;
}
2026-02-02 14:34:34 +08:00
if ( Editor . assetdb . exists ( prefabPath ) ) {
return callback ( ` Prefab already exists at ${ prefabPath } ` ) ;
2026-02-01 13:30:11 +08:00
}
// 确保父目录存在
2026-02-02 14:34:34 +08:00
const absolutePath = Editor . assetdb . urlToFspath ( prefabPath ) ;
const dirPath = path . dirname ( absolutePath ) ;
2026-02-01 13:30:11 +08:00
if ( ! fs . existsSync ( dirPath ) ) {
fs . mkdirSync ( dirPath , { recursive : true } ) ;
}
2026-02-02 14:34:34 +08:00
// 解析目标目录和文件名
// db://assets/folder/PrefabName.prefab -> db://assets/folder, PrefabName
const targetDir = prefabPath . substring ( 0 , prefabPath . lastIndexOf ( '/' ) ) ;
const fileName = prefabPath . substring ( prefabPath . lastIndexOf ( '/' ) + 1 ) ;
const prefabName = fileName . replace ( '.prefab' , '' ) ;
// 1. 重命名节点以匹配预制体名称
Editor . Ipc . sendToPanel ( "scene" , "scene:set-property" , {
id : nodeId ,
path : "name" ,
type : "String" ,
value : prefabName ,
isSubProp : false
} ) ;
// 2. 发送创建命令 (参数: [uuids], dirPath)
// 注意: scene:create-prefab 第三个参数必须是 db:// 目录路径
setTimeout ( ( ) => {
Editor . Ipc . sendToPanel ( "scene" , "scene:create-prefab" , [ nodeId ] , targetDir ) ;
} , 100 ) ; // 稍微延迟以确保重命名生效
callback ( null , ` Command sent: Creating prefab from node ${ nodeId } at ${ targetDir } as ${ prefabName } ` ) ;
2026-02-01 13:30:11 +08:00
break ;
case "update" :
if ( ! nodeId ) {
return callback ( ` Node ID is required for update operation ` ) ;
}
2026-02-02 14:34:34 +08:00
if ( ! Editor . assetdb . exists ( prefabPath ) ) {
return callback ( ` Prefab not found at ${ prefabPath } ` ) ;
2026-02-01 13:30:11 +08:00
}
// 更新预制体
2026-02-02 14:34:34 +08:00
Editor . Ipc . sendToPanel ( "scene" , "scene:update-prefab" , nodeId , prefabPath ) ;
callback ( null , ` Command sent: Updating prefab ${ prefabPath } from node ${ nodeId } ` ) ;
2026-02-01 13:30:11 +08:00
break ;
case "instantiate" :
2026-02-02 14:34:34 +08:00
if ( ! Editor . assetdb . exists ( prefabPath ) ) {
return callback ( ` Prefab not found at ${ prefabPath } ` ) ;
2026-02-01 13:30:11 +08:00
}
// 实例化预制体
2026-02-02 14:34:34 +08:00
const prefabUuid = Editor . assetdb . urlToUuid ( prefabPath ) ;
2026-02-01 13:30:11 +08:00
Editor . Scene . callSceneScript (
"mcp-bridge" ,
"instantiate-prefab" ,
{
2026-02-02 14:34:34 +08:00
prefabUuid : prefabUuid ,
2026-02-01 13:30:11 +08:00
parentId : parentId ,
} ,
callback ,
) ;
break ;
case "get_info" :
2026-02-02 14:34:34 +08:00
if ( Editor . assetdb . exists ( prefabPath ) ) {
const uuid = Editor . assetdb . urlToUuid ( prefabPath ) ;
const info = Editor . assetdb . assetInfoByUuid ( uuid ) ;
// 确保返回对象包含 exists: true, 以满足测试验证
const result = info || { url : prefabPath , uuid : uuid } ;
result . exists = true ;
callback ( null , result ) ;
} else {
callback ( ` Prefab not found: ${ prefabPath } ` ) ;
}
2026-02-01 13:30:11 +08:00
break ;
default :
callback ( ` Unknown prefab action: ${ action } ` ) ;
}
} ,
// 管理编辑器
manageEditor ( args , callback ) {
const { action , target , properties } = args ;
switch ( action ) {
case "get_selection" :
// 获取当前选中的资源或节点
const nodeSelection = Editor . Selection . curSelection ( "node" ) ;
const assetSelection = Editor . Selection . curSelection ( "asset" ) ;
callback ( null , {
nodes : nodeSelection ,
assets : assetSelection ,
} ) ;
break ;
case "set_selection" :
// 设置选中状态
if ( target === "node" && properties . nodes ) {
Editor . Selection . select ( "node" , properties . nodes ) ;
} else if ( target === "asset" && properties . assets ) {
Editor . Selection . select ( "asset" , properties . assets ) ;
}
callback ( null , "Selection updated" ) ;
break ;
case "refresh_editor" :
// 刷新编辑器
2026-02-02 10:14:17 +08:00
const refreshPath = ( properties && properties . path ) ? properties . path : 'db://assets/scripts' ;
Editor . assetdb . refresh ( refreshPath , ( err ) => {
if ( err ) {
addLog ( "error" , ` Refresh failed: ${ err } ` ) ;
callback ( err ) ;
} else {
callback ( null , ` Editor refreshed: ${ refreshPath } ` ) ;
}
} ) ;
2026-02-01 13:30:11 +08:00
break ;
default :
callback ( "Unknown action" ) ;
break ;
}
} ,
// 管理材质
manageMaterial ( args , callback ) {
const { action , path , properties } = args ;
switch ( action ) {
case "create" :
if ( Editor . assetdb . exists ( path ) ) {
return callback ( ` Material 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 } ) ;
}
// 创建材质资源
const materialContent = JSON . stringify ( {
_ _type _ _ : "cc.Material" ,
_name : "" ,
_objFlags : 0 ,
_native : "" ,
effects : [
{
technique : 0 ,
defines : { } ,
uniforms : properties . uniforms || { } ,
} ,
] ,
} ) ;
Editor . assetdb . create ( path , materialContent , ( err ) => {
callback ( err , err ? null : ` Material created at ${ path } ` ) ;
} ) ;
break ;
case "delete" :
if ( ! Editor . assetdb . exists ( path ) ) {
return callback ( ` Material not found at ${ path } ` ) ;
}
Editor . assetdb . delete ( [ path ] , ( err ) => {
callback ( err , err ? null : ` Material deleted at ${ path } ` ) ;
} ) ;
break ;
case "get_info" :
2026-02-02 14:34:34 +08:00
if ( Editor . assetdb . exists ( path ) ) {
const uuid = Editor . assetdb . urlToUuid ( path ) ;
const info = Editor . assetdb . assetInfoByUuid ( uuid ) ;
callback ( null , info || { url : path , uuid : uuid , exists : true } ) ;
} else {
callback ( ` Material not found: ${ path } ` ) ;
}
2026-02-01 13:30:11 +08:00
break ;
default :
callback ( ` Unknown material action: ${ action } ` ) ;
break ;
}
} ,
// 管理纹理
manageTexture ( args , callback ) {
const { action , path , properties } = args ;
switch ( action ) {
case "create" :
if ( Editor . assetdb . exists ( path ) ) {
return callback ( ` Texture 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 } ) ;
}
// 创建纹理资源(简化版,实际需要处理纹理文件)
const textureContent = JSON . stringify ( {
_ _type _ _ : "cc.Texture2D" ,
_name : "" ,
_objFlags : 0 ,
_native : properties . native || "" ,
width : properties . width || 128 ,
height : properties . height || 128 ,
} ) ;
Editor . assetdb . create ( path , textureContent , ( err ) => {
callback ( err , err ? null : ` Texture created at ${ path } ` ) ;
} ) ;
break ;
case "delete" :
if ( ! Editor . assetdb . exists ( path ) ) {
return callback ( ` Texture not found at ${ path } ` ) ;
}
Editor . assetdb . delete ( [ path ] , ( err ) => {
callback ( err , err ? null : ` Texture deleted at ${ path } ` ) ;
} ) ;
break ;
case "get_info" :
2026-02-02 14:34:34 +08:00
if ( Editor . assetdb . exists ( path ) ) {
const uuid = Editor . assetdb . urlToUuid ( path ) ;
const info = Editor . assetdb . assetInfoByUuid ( uuid ) ;
callback ( null , info || { url : path , uuid : uuid , exists : true } ) ;
} else {
callback ( ` Texture not found: ${ path } ` ) ;
}
2026-02-01 13:30:11 +08:00
break ;
default :
callback ( ` Unknown texture action: ${ action } ` ) ;
break ;
}
} ,
// 应用文本编辑
applyTextEdits ( args , callback ) {
const { filePath , edits } = args ;
2026-02-02 14:34:34 +08:00
// 1. 获取文件系统路径
const fspath = Editor . assetdb . urlToFspath ( filePath ) ;
if ( ! fspath ) {
return callback ( ` File not found or invalid URL: ${ filePath } ` ) ;
}
2026-01-31 19:36:55 +08:00
2026-02-02 14:34:34 +08:00
const fs = require ( "fs" ) ;
if ( ! fs . existsSync ( fspath ) ) {
return callback ( ` File does not exist: ${ fspath } ` ) ;
}
try {
// 2. 读取
let updatedContent = fs . readFileSync ( fspath , "utf-8" ) ;
// 3. 应用编辑
// 必须按倒序应用编辑,否则后续编辑的位置会偏移 (假设edits未排序, 这里简单处理, 实际上LSP通常建议客户端倒序应用或计算偏移)
// 这里假设edits已经按照位置排序或者用户负责, 如果需要严谨, 应先按 start/position 倒序排序
2026-02-03 20:04:45 +08:00
// 简单排序保险:
2026-02-02 14:34:34 +08:00
const sortedEdits = [ ... edits ] . sort ( ( a , b ) => {
const posA = a . position !== undefined ? a . position : a . start ;
const posB = b . position !== undefined ? b . position : b . start ;
2026-02-03 20:04:45 +08:00
return posB - posA ; // 从大到小
2026-02-02 14:34:34 +08:00
} ) ;
sortedEdits . forEach ( ( edit ) => {
switch ( edit . type ) {
case "insert" :
updatedContent =
updatedContent . slice ( 0 , edit . position ) +
edit . text +
updatedContent . slice ( edit . position ) ;
break ;
case "delete" :
updatedContent = updatedContent . slice ( 0 , edit . start ) + updatedContent . slice ( edit . end ) ;
break ;
case "replace" :
updatedContent =
updatedContent . slice ( 0 , edit . start ) + edit . text + updatedContent . slice ( edit . end ) ;
break ;
2026-02-01 13:30:11 +08:00
}
2026-02-02 14:34:34 +08:00
} ) ;
2026-01-31 19:36:55 +08:00
2026-02-02 14:34:34 +08:00
// 4. 写入
fs . writeFileSync ( fspath , updatedContent , "utf-8" ) ;
2026-01-31 19:36:55 +08:00
2026-02-02 14:34:34 +08:00
// 5. 通知编辑器资源变化 (重要)
Editor . assetdb . refresh ( filePath , ( err ) => {
if ( err ) addLog ( "warn" , ` Refresh failed for ${ filePath } : ${ err } ` ) ;
callback ( null , ` Text edits applied to ${ filePath } ` ) ;
2026-02-01 13:30:11 +08:00
} ) ;
2026-02-02 14:34:34 +08:00
} catch ( err ) {
callback ( ` Action failed: ${ err . message } ` ) ;
}
2026-02-01 13:30:11 +08:00
} ,
2026-01-31 19:36:55 +08:00
2026-02-01 13:30:11 +08:00
// 读取控制台
readConsole ( args , callback ) {
const { limit , type } = args ;
let filteredOutput = logBuffer ;
2026-01-31 19:36:55 +08:00
2026-02-01 13:30:11 +08:00
if ( type ) {
filteredOutput = filteredOutput . filter ( ( item ) => item . type === type ) ;
}
2026-01-31 19:36:55 +08:00
2026-02-01 13:30:11 +08:00
if ( limit ) {
filteredOutput = filteredOutput . slice ( - limit ) ;
}
callback ( null , filteredOutput ) ;
} ,
2026-02-02 10:14:17 +08:00
executeMenuItem ( args , callback ) {
const { menuPath } = args ;
if ( ! menuPath ) {
return callback ( "Menu path is required" ) ;
}
addLog ( "info" , ` Executing Menu Item: ${ menuPath } ` ) ;
2026-02-03 19:55:51 +08:00
// 菜单项映射表 (Cocos Creator 2.4.x IPC)
// 参考: IPC_MESSAGES.md
const menuMap = {
'File/New Scene' : 'scene:new-scene' ,
'File/Save Scene' : 'scene:stash-and-save' ,
'File/Save' : 'scene:stash-and-save' , // 别名
'Edit/Undo' : 'scene:undo' ,
'Edit/Redo' : 'scene:redo' ,
2026-02-03 20:04:45 +08:00
'Edit/Delete' : 'scene:delete-selected' ,
'Delete' : 'scene:delete-selected' ,
'delete' : 'scene:delete-selected' ,
2026-02-03 19:55:51 +08:00
'Node/Create Empty Node' : 'scene:create-node-by-classid' , // 简化的映射,通常需要参数
'Project/Build' : 'app:build-project' ,
} ;
2026-02-03 20:04:45 +08:00
// 特殊处理 delete-node:UUID 格式
if ( menuPath . startsWith ( "delete-node:" ) ) {
const uuid = menuPath . split ( ":" ) [ 1 ] ;
if ( uuid ) {
Editor . Scene . callSceneScript ( 'mcp-bridge' , 'delete-node' , { uuid } , ( err , result ) => {
if ( err ) callback ( err ) ;
else callback ( null , result || ` Node ${ uuid } deleted via scene script ` ) ;
} ) ;
return ;
}
}
2026-02-03 19:55:51 +08:00
if ( menuMap [ menuPath ] ) {
const ipcMsg = menuMap [ menuPath ] ;
try {
Editor . Ipc . sendToMain ( ipcMsg ) ;
callback ( null , ` Menu action triggered: ${ menuPath } -> ${ ipcMsg } ` ) ;
} catch ( err ) {
callback ( ` Failed to execute IPC ${ ipcMsg } : ${ err . message } ` ) ;
}
2026-02-02 10:14:17 +08:00
} else {
2026-02-03 19:55:51 +08:00
// 对于未在映射表中的菜单,尝试通用的 menu:click (虽然不一定有效)
// 或者直接返回不支持的警告
addLog ( "warn" , ` Menu item ' ${ menuPath } ' not found in supported map. Trying legacy fallback. ` ) ;
// 尝试通用调用
try {
// 注意: Cocos Creator 2.x 的 menu:click 通常需要 Electron 菜单 ID, 而不只是路径
// 这里做个尽力而为的尝试
Editor . Ipc . sendToMain ( 'menu:click' , menuPath ) ;
callback ( null , ` Generic menu action sent: ${ menuPath } (Success guaranteed only for supported items) ` ) ;
} catch ( e ) {
callback ( ` Failed to execute menu item: ${ menuPath } ` ) ;
}
2026-02-02 10:14:17 +08:00
}
} ,
2026-02-01 13:30:11 +08:00
// 验证脚本
validateScript ( args , callback ) {
const { filePath } = args ;
2026-02-02 14:34:34 +08:00
// 1. 获取文件系统路径
const fspath = Editor . assetdb . urlToFspath ( filePath ) ;
if ( ! fspath ) {
return callback ( ` File not found or invalid URL: ${ filePath } ` ) ;
}
2026-02-01 13:30:11 +08:00
2026-02-02 14:34:34 +08:00
// 2. 检查文件是否存在
const fs = require ( "fs" ) ;
if ( ! fs . existsSync ( fspath ) ) {
return callback ( ` File does not exist: ${ fspath } ` ) ;
}
2026-02-01 13:30:11 +08:00
2026-02-02 14:34:34 +08:00
// 3. 读取内容并验证
try {
const content = fs . readFileSync ( fspath , "utf-8" ) ;
2026-02-01 13:30:11 +08:00
2026-02-03 19:55:51 +08:00
// 检查空文件
if ( ! content || content . trim ( ) . length === 0 ) {
return callback ( null , { valid : false , message : "Script is empty" } ) ;
}
// 对于 JavaScript 脚本,使用 Function 构造器进行语法验证
2026-02-02 14:34:34 +08:00
if ( filePath . endsWith ( ".js" ) ) {
const wrapper = ` (function() { ${ content } }) ` ;
try {
2026-02-03 19:55:51 +08:00
new Function ( wrapper ) ;
callback ( null , { valid : true , message : "JavaScript syntax is valid" } ) ;
2026-02-02 14:34:34 +08:00
} catch ( syntaxErr ) {
return callback ( null , { valid : false , message : syntaxErr . message } ) ;
2026-02-01 13:30:11 +08:00
}
2026-02-02 14:34:34 +08:00
}
2026-02-03 19:55:51 +08:00
// 对于 TypeScript, 由于没有内置 TS 编译器,我们进行基础的"防呆"检查
// 并明确告知用户无法进行完整编译验证
else if ( filePath . endsWith ( ".ts" ) ) {
// 简单的正则表达式检查:是否有非法字符或明显错误结构 (示例)
// 这里暂时只做简单的括号匹配检查或直接通过,但给出一个 Warning
2026-02-03 20:04:45 +08:00
// 检查是否有 class 定义 (简单的启发式检查)
2026-02-03 19:55:51 +08:00
if ( ! content . includes ( 'class ' ) && ! content . includes ( 'interface ' ) && ! content . includes ( 'enum ' ) && ! content . includes ( 'export ' ) ) {
return callback ( null , { valid : true , message : "Warning: TypeScript file seems to lack standard definitions (class/interface), but basic syntax check is skipped due to missing compiler." } ) ;
}
2026-02-02 14:34:34 +08:00
2026-02-03 19:55:51 +08:00
callback ( null , { valid : true , message : "TypeScript basic check passed. (Full compilation validation requires editor build)" } ) ;
} else {
callback ( null , { valid : true , message : "Unknown script type, validation skipped." } ) ;
}
2026-02-02 14:34:34 +08:00
} catch ( err ) {
callback ( null , { valid : false , message : ` Read Error: ${ err . message } ` } ) ;
}
2026-02-01 13:30:11 +08:00
} ,
// 暴露给 MCP 或面板的 API 封装
2026-01-29 14:53:06 +08:00
messages : {
2026-02-03 19:55:51 +08:00
"scan-ipc-messages" ( event ) {
try {
const msgs = IpcManager . getIpcMessages ( ) ;
if ( event . reply ) event . reply ( null , msgs ) ;
} catch ( e ) {
if ( event . reply ) event . reply ( e . message ) ;
}
} ,
"test-ipc-message" ( event , args ) {
const { name , params } = args ;
IpcManager . testIpcMessage ( name , params ) . then ( ( result ) => {
if ( event . reply ) event . reply ( null , result ) ;
} ) ;
} ,
2026-01-29 14:53:06 +08:00
"open-test-panel" ( ) {
Editor . Panel . open ( "mcp-bridge" ) ;
} ,
2026-02-02 14:34:34 +08:00
2026-01-29 14:53:06 +08:00
"toggle-server" ( event , port ) {
if ( serverConfig . active ) this . stopServer ( ) ;
else this . startServer ( port ) ;
} ,
"clear-logs" ( ) {
logBuffer = [ ] ;
addLog ( "info" , "Logs cleared" ) ;
} ,
// 修改场景中的节点(需要通过 scene-script)
"set-node-property" ( event , args ) {
addLog ( "mcp" , ` Creating node: ${ args . name } ( ${ args . type } ) ` ) ;
// 确保第一个参数 'mcp-bridge' 和 package.json 的 name 一致
Editor . Scene . callSceneScript ( "mcp-bridge" , "set-property" , args , ( err , result ) => {
if ( err ) {
Editor . error ( "Scene Script Error:" , err ) ;
}
if ( event && event . reply ) {
event . reply ( err , result ) ;
2026-01-29 13:47:38 +08:00
}
} ) ;
2026-01-29 14:53:06 +08:00
} ,
"create-node" ( event , args ) {
addLog ( "mcp" , ` Creating node: ${ args . name } ( ${ args . type } ) ` ) ;
Editor . Scene . callSceneScript ( "mcp-bridge" , "create-node" , args , ( err , result ) => {
if ( err ) addLog ( "error" , ` CreateNode Failed: ${ err } ` ) ;
else addLog ( "success" , ` Node Created: ${ result } ` ) ;
event . reply ( err , result ) ;
} ) ;
} ,
"get-server-state" ( event ) {
let profile = this . getProfile ( ) ;
event . reply ( null , {
config : serverConfig ,
logs : logBuffer ,
autoStart : profile . get ( "auto-start" ) , // 返回自动启动状态
} ) ;
} ,
2026-01-29 13:47:38 +08:00
2026-01-29 14:53:06 +08:00
"set-auto-start" ( event , value ) {
this . getProfile ( ) . set ( "auto-start" , value ) ;
this . getProfile ( ) . save ( ) ;
addLog ( "info" , ` Auto-start set to: ${ value } ` ) ;
} ,
2026-02-01 13:30:11 +08:00
2026-02-02 14:34:34 +08:00
"inspect-apis" ( ) {
addLog ( "info" , "[API Inspector] Starting DEEP inspection..." ) ;
2026-02-01 13:30:11 +08:00
2026-02-03 20:04:45 +08:00
// 获取函数参数的辅助函数
2026-02-02 14:34:34 +08:00
const getArgs = ( func ) => {
try {
const str = func . toString ( ) ;
const match = str . match ( /function\s.*?\(([^)]*)\)/ ) || str . match ( /.*?\(([^)]*)\)/ ) ;
if ( match ) {
return match [ 1 ] . split ( "," ) . map ( arg => arg . trim ( ) ) . filter ( a => a ) . join ( ", " ) ;
}
return ` ${ func . length } args ` ;
} catch ( e ) {
return "?" ;
}
} ;
2026-02-03 20:04:45 +08:00
// 检查对象的辅助函数
2026-02-02 14:34:34 +08:00
const inspectObj = ( name , obj ) => {
if ( ! obj ) return { name , exists : false } ;
const props = { } ;
const proto = Object . getPrototypeOf ( obj ) ;
// 组合自身属性和原型属性
const allKeys = new Set ( [ ... Object . getOwnPropertyNames ( obj ) , ... Object . getOwnPropertyNames ( proto || { } ) ] ) ;
allKeys . forEach ( key => {
2026-02-03 20:04:45 +08:00
if ( key . startsWith ( "_" ) ) return ; // 跳过私有属性
2026-02-02 14:34:34 +08:00
try {
const val = obj [ key ] ;
if ( typeof val === 'function' ) {
props [ key ] = ` func( ${ getArgs ( val ) } ) ` ;
} else {
props [ key ] = typeof val ;
}
} catch ( e ) { }
} ) ;
return { name , exists : true , props } ;
} ;
2026-02-03 20:04:45 +08:00
// 1. 检查标准对象
2026-02-02 14:34:34 +08:00
const standardObjects = {
"Editor.assetdb" : Editor . assetdb ,
"Editor.Selection" : Editor . Selection ,
"Editor.Ipc" : Editor . Ipc ,
"Editor.Panel" : Editor . Panel ,
"Editor.Scene" : Editor . Scene ,
"Editor.Utils" : Editor . Utils ,
"Editor.remote" : Editor . remote
} ;
const report = { } ;
Object . keys ( standardObjects ) . forEach ( key => {
report [ key ] = inspectObj ( key , standardObjects [ key ] ) ;
} ) ;
2026-02-01 13:30:11 +08:00
2026-02-03 20:04:45 +08:00
// 2. 检查特定论坛提到的 API
2026-02-02 14:34:34 +08:00
const forumChecklist = [
"Editor.assetdb.queryInfoByUuid" ,
"Editor.assetdb.assetInfoByUuid" ,
"Editor.assetdb.move" ,
"Editor.assetdb.createOrSave" ,
"Editor.assetdb.delete" ,
"Editor.assetdb.urlToUuid" ,
"Editor.assetdb.uuidToUrl" ,
"Editor.assetdb.fspathToUrl" ,
"Editor.assetdb.urlToFspath" ,
"Editor.remote.assetdb.uuidToUrl" ,
"Editor.Selection.select" ,
"Editor.Selection.clear" ,
"Editor.Selection.curSelection" ,
"Editor.Selection.curGlobalActivate"
] ;
const checklistResults = { } ;
forumChecklist . forEach ( path => {
const parts = path . split ( "." ) ;
2026-02-03 20:04:45 +08:00
let curr = global ; // 在主进程中, Editor 是全局的
2026-02-02 14:34:34 +08:00
let exists = true ;
for ( const part of parts ) {
if ( curr && curr [ part ] ) {
curr = curr [ part ] ;
} else {
exists = false ;
break ;
}
2026-02-01 13:30:11 +08:00
}
2026-02-02 14:34:34 +08:00
checklistResults [ path ] = exists ? ( typeof curr === 'function' ? ` Available( ${ getArgs ( curr ) } ) ` : "Available" ) : "Missing" ;
} ) ;
addLog ( "info" , ` [API Inspector] Standard Objects: \n ${ JSON . stringify ( report , null , 2 ) } ` ) ;
addLog ( "info" , ` [API Inspector] Forum Checklist: \n ${ JSON . stringify ( checklistResults , null , 2 ) } ` ) ;
2026-02-01 13:30:11 +08:00
2026-02-03 20:04:45 +08:00
// 3. 检查内置包 IPC 消息
2026-02-02 14:34:34 +08:00
const ipcReport = { } ;
const builtinPackages = [ "scene" , "builder" , "assets" ] ; // 核心内置包
const fs = require ( "fs" ) ;
builtinPackages . forEach ( pkgName => {
2026-02-01 13:30:11 +08:00
try {
2026-02-02 14:34:34 +08:00
const pkgPath = Editor . url ( ` packages:// ${ pkgName } /package.json ` ) ;
if ( pkgPath && fs . existsSync ( pkgPath ) ) {
const pkgData = JSON . parse ( fs . readFileSync ( pkgPath , "utf-8" ) ) ;
if ( pkgData . messages ) {
ipcReport [ pkgName ] = Object . keys ( pkgData . messages ) ;
} else {
ipcReport [ pkgName ] = "No messages defined" ;
}
} else {
ipcReport [ pkgName ] = "Package path not found" ;
2026-02-01 13:30:11 +08:00
}
2026-02-02 14:34:34 +08:00
} catch ( e ) {
ipcReport [ pkgName ] = ` Error: ${ e . message } ` ;
2026-02-01 13:30:11 +08:00
}
} ) ;
2026-02-02 14:34:34 +08:00
addLog ( "info" , ` [API Inspector] Built-in IPC Messages: \n ${ JSON . stringify ( ipcReport , null , 2 ) } ` ) ;
} ,
2026-02-01 13:30:11 +08:00
} ,
2026-02-02 14:34:34 +08:00
2026-02-01 13:30:11 +08:00
// 全局文件搜索
findInFile ( args , callback ) {
const { query , extensions , includeSubpackages } = args ;
const fs = require ( 'fs' ) ;
const path = require ( 'path' ) ;
const assetsPath = Editor . assetdb . urlToFspath ( "db://assets" ) ;
const validExtensions = extensions || [ ".js" , ".ts" , ".json" , ".fire" , ".prefab" , ".xml" , ".txt" , ".md" ] ;
const results = [ ] ;
const MAX _RESULTS = 500 ; // 限制返回结果数量,防止溢出
try {
// 递归遍历函数
const walk = ( dir ) => {
if ( results . length >= MAX _RESULTS ) return ;
const list = fs . readdirSync ( dir ) ;
list . forEach ( ( file ) => {
if ( results . length >= MAX _RESULTS ) return ;
// 忽略隐藏文件和 node_modules
if ( file . startsWith ( '.' ) || file === 'node_modules' || file === 'bin' || file === 'local' ) return ;
const filePath = path . join ( dir , file ) ;
const stat = fs . statSync ( filePath ) ;
if ( stat && stat . isDirectory ( ) ) {
walk ( filePath ) ;
} else {
// 检查后缀
const ext = path . extname ( file ) . toLowerCase ( ) ;
if ( validExtensions . includes ( ext ) ) {
try {
const content = fs . readFileSync ( filePath , 'utf8' ) ;
// 简单的行匹配
const lines = content . split ( '\n' ) ;
lines . forEach ( ( line , index ) => {
if ( results . length >= MAX _RESULTS ) return ;
if ( line . includes ( query ) ) {
// 转换为项目相对路径 (db://assets/...)
const relativePath = path . relative ( assetsPath , filePath ) ;
// 统一使用 forward slash
const dbPath = "db://assets/" + relativePath . split ( path . sep ) . join ( '/' ) ;
results . push ( {
filePath : dbPath ,
line : index + 1 ,
content : line . trim ( )
} ) ;
}
} ) ;
} catch ( e ) {
// 读取文件出错,跳过
}
}
}
} ) ;
} ;
walk ( assetsPath ) ;
callback ( null , results ) ;
} catch ( err ) {
callback ( ` Find in file failed: ${ err . message } ` ) ;
}
} ,
// 管理撤销/重做
manageUndo ( args , callback ) {
const { action , description } = args ;
try {
switch ( action ) {
case "undo" :
Editor . Ipc . sendToPanel ( "scene" , "scene:undo" ) ;
callback ( null , "Undo command executed" ) ;
break ;
case "redo" :
Editor . Ipc . sendToPanel ( "scene" , "scene:redo" ) ;
callback ( null , "Redo command executed" ) ;
break ;
case "begin_group" :
// scene:undo-record [id]
// 这里的 id 好像是可选的,或者用于区分不同的事务
Editor . Ipc . sendToPanel ( "scene" , "scene:undo-record" , description || "MCP Action" ) ;
callback ( null , ` Undo group started: ${ description || "MCP Action" } ` ) ;
break ;
case "end_group" :
Editor . Ipc . sendToPanel ( "scene" , "scene:undo-commit" ) ;
callback ( null , "Undo group committed" ) ;
break ;
case "cancel_group" :
Editor . Ipc . sendToPanel ( "scene" , "scene:undo-cancel" ) ;
callback ( null , "Undo group cancelled" ) ;
break ;
default :
callback ( ` Unknown undo action: ${ action } ` ) ;
}
} catch ( err ) {
callback ( ` Undo operation failed: ${ err . message } ` ) ;
}
} ,
2026-01-29 13:47:38 +08:00
} ;