feat: 完成项目全量汉化、文档同步及 Undo 系统稳定性修复

This commit is contained in:
火焰库拉
2026-02-11 01:09:42 +08:00
parent 7d5e943ab1
commit 09817ac79d
5 changed files with 133 additions and 107 deletions

View File

@@ -101,6 +101,14 @@ startServer(port) {
} }
``` ```
## 4. 开发历程与里程碑
### 2026-02-10: Undo 系统深度修复与规范化
- **问题分析**: 修复了 `TypeError: Cannot read property '_name' of null`。该错误是由于直接修改节点属性(绕过 Undo 系统)与分组事务混用导致的。
- **重构要点**: 将 `update-node-transform` 中所有的直接赋值替换为 `scene:set-property` IPC 调用,确保所有变换修改均受撤销系统监控。
- **缺陷修正**: 修复了 `manage_undo``begin_group` 时传递错误参数导致 "Unknown object to record" 的问题。
- **全量汉化与文档同步**: 完成了 `main.js``scene-script.js` 的 100% 简体中文翻译。同步更新了 `README.md``DEVELOPMENT.md``注意事项.md`
### 3.2 MCP 工具注册 ### 3.2 MCP 工具注册
`/list-tools` 接口中注册工具: `/list-tools` 接口中注册工具:

View File

@@ -317,12 +317,11 @@ Args: [你的项目所在盘符]:/[项目路径]/packages/mcp-bridge/mcp-proxy.j
} }
``` ```
### 23. manage_undo
- **描述**: 撤销/重做管理 - **描述**: 撤销/重做管理
- **参数**: - **参数**:
- `action`: 操作类型 (`undo`, `redo`, `begin_group`, `end_group`, `cancel_group`) - `action`: 操作类型 (`undo`, `redo`, `begin_group`, `end_group`, `cancel_group`)
- `description`: 撤销组描述 (用于 `begin_group`) - `description`: 撤销组描述 (用于 `begin_group`)
- `id`: 节点 UUID (用于 `begin_group` 时的 `undo-record`,可选)
### 24. manage_vfx ### 24. manage_vfx

145
main.js
View File

@@ -1040,9 +1040,9 @@ export default class NewScript extends cc.Component {
// 否则后续立即挂载脚本的操作(manage_components)会因找不到脚本 UUID 而失败。 // 否则后续立即挂载脚本的操作(manage_components)会因找不到脚本 UUID 而失败。
Editor.assetdb.refresh(scriptPath, (refreshErr) => { Editor.assetdb.refresh(scriptPath, (refreshErr) => {
if (refreshErr) { if (refreshErr) {
addLog("warn", `Refresh failed after script creation: ${refreshErr}`); addLog("warn", `脚本创建后刷新失败: ${refreshErr}`);
} }
callback(null, `Script created at ${scriptPath}`); callback(null, `脚本已创建: ${scriptPath}`);
}); });
} }
}, },
@@ -1076,7 +1076,7 @@ export default class NewScript extends cc.Component {
// 使用 fs 写入 + refresh确保覆盖成功 // 使用 fs 写入 + refresh确保覆盖成功
const writeFsPath = Editor.assetdb.urlToFspath(scriptPath); const writeFsPath = Editor.assetdb.urlToFspath(scriptPath);
if (!writeFsPath) { if (!writeFsPath) {
return callback(`Invalid path: ${scriptPath}`); return callback(`路径无效: ${scriptPath}`);
} }
try { try {
@@ -1133,7 +1133,7 @@ export default class NewScript extends cc.Component {
switch (action) { switch (action) {
case "create": case "create":
if (Editor.assetdb.exists(path)) { if (Editor.assetdb.exists(path)) {
return callback(`Asset already exists at ${path}`); return callback(`资源已存在: ${path}`);
} }
// 确保父目录存在 // 确保父目录存在
const fs = require("fs"); const fs = require("fs");
@@ -1144,16 +1144,16 @@ export default class NewScript extends cc.Component {
fs.mkdirSync(dirPath, { recursive: true }); fs.mkdirSync(dirPath, { recursive: true });
} }
Editor.assetdb.create(path, content || "", (err) => { Editor.assetdb.create(path, content || "", (err) => {
callback(err, err ? null : `Asset created at ${path}`); callback(err, err ? null : `资源已创建: ${path}`);
}); });
break; break;
case "delete": case "delete":
if (!Editor.assetdb.exists(path)) { if (!Editor.assetdb.exists(path)) {
return callback(`Asset not found at ${path}`); return callback(`找不到资源: ${path}`);
} }
Editor.assetdb.delete([path], (err) => { Editor.assetdb.delete([path], (err) => {
callback(err, err ? null : `Asset deleted at ${path}`); callback(err, err ? null : `资源已删除: ${path}`);
}); });
break; break;
@@ -1173,7 +1173,7 @@ export default class NewScript extends cc.Component {
case "get_info": case "get_info":
try { try {
if (!Editor.assetdb.exists(path)) { if (!Editor.assetdb.exists(path)) {
return callback(`Asset not found: ${path}`); return callback(`找不到资源: ${path}`);
} }
const uuid = Editor.assetdb.urlToUuid(path); const uuid = Editor.assetdb.urlToUuid(path);
const info = Editor.assetdb.assetInfoByUuid(uuid); const info = Editor.assetdb.assetInfoByUuid(uuid);
@@ -1184,7 +1184,7 @@ export default class NewScript extends cc.Component {
callback(null, { url: path, uuid: uuid, exists: true }); callback(null, { url: path, uuid: uuid, exists: true });
} }
} catch (e) { } catch (e) {
callback(`Error getting asset info: ${e.message}`); callback(`获取资源信息失败: ${e.message}`);
} }
break; break;
@@ -1205,7 +1205,7 @@ export default class NewScript extends cc.Component {
switch (action) { switch (action) {
case "create": case "create":
if (Editor.assetdb.exists(path)) { if (Editor.assetdb.exists(path)) {
return callback(`Scene already exists at ${path}`); return callback(`场景已存在: ${path}`);
} }
// 确保父目录存在 // 确保父目录存在
const absolutePath = Editor.assetdb.urlToFspath(path); const absolutePath = Editor.assetdb.urlToFspath(path);
@@ -1214,35 +1214,35 @@ export default class NewScript extends cc.Component {
fs.mkdirSync(dirPath, { recursive: true }); fs.mkdirSync(dirPath, { recursive: true });
} }
Editor.assetdb.create(path, getNewSceneTemplate(), (err) => { Editor.assetdb.create(path, getNewSceneTemplate(), (err) => {
callback(err, err ? null : `Scene created at ${path}`); callback(err, err ? null : `场景已创建: ${path}`);
}); });
break; break;
case "delete": case "delete":
if (!Editor.assetdb.exists(path)) { if (!Editor.assetdb.exists(path)) {
return callback(`Scene not found at ${path}`); return callback(`找不到场景: ${path}`);
} }
Editor.assetdb.delete([path], (err) => { Editor.assetdb.delete([path], (err) => {
callback(err, err ? null : `Scene deleted at ${path}`); callback(err, err ? null : `场景已删除: ${path}`);
}); });
break; break;
case "duplicate": case "duplicate":
if (!Editor.assetdb.exists(path)) { if (!Editor.assetdb.exists(path)) {
return callback(`Scene not found at ${path}`); return callback(`找不到场景: ${path}`);
} }
if (!targetPath) { if (!targetPath) {
return callback(`Target path is required for duplicate operation`); return callback("复制操作需要目标路径");
} }
if (Editor.assetdb.exists(targetPath)) { if (Editor.assetdb.exists(targetPath)) {
return callback(`Target scene already exists at ${targetPath}`); return callback(`目标场景已存在: ${targetPath}`);
} }
// 【修复】Cocos 2.4.x 主进程中 Editor.assetdb 没有 loadAny // 【修复】Cocos 2.4.x 主进程中 Editor.assetdb 没有 loadAny
// 直接使用 fs 读取物理文件 // 直接使用 fs 读取物理文件
try { try {
const sourceFsPath = Editor.assetdb.urlToFspath(path); const sourceFsPath = Editor.assetdb.urlToFspath(path);
if (!sourceFsPath || !fs.existsSync(sourceFsPath)) { if (!sourceFsPath || !fs.existsSync(sourceFsPath)) {
return callback(`Failed to locate source scene file: ${path}`); return callback(`定位源场景文件失败: ${path}`);
} }
const content = fs.readFileSync(sourceFsPath, "utf-8"); const content = fs.readFileSync(sourceFsPath, "utf-8");
@@ -1257,7 +1257,7 @@ export default class NewScript extends cc.Component {
if (err) return callback(err); if (err) return callback(err);
// 【增加】关键刷新,确保数据库能查到新文件 // 【增加】关键刷新,确保数据库能查到新文件
Editor.assetdb.refresh(targetPath, (refreshErr) => { Editor.assetdb.refresh(targetPath, (refreshErr) => {
callback(refreshErr, refreshErr ? null : `Scene duplicated from ${path} to ${targetPath}`); callback(refreshErr, refreshErr ? null : `场景已从 ${path} 复制到 ${targetPath}`);
}); });
}); });
} catch (e) { } catch (e) {
@@ -1271,7 +1271,7 @@ export default class NewScript extends cc.Component {
const info = Editor.assetdb.assetInfoByUuid(uuid); const info = Editor.assetdb.assetInfoByUuid(uuid);
callback(null, info || { url: path, uuid: uuid, exists: true }); callback(null, info || { url: path, uuid: uuid, exists: true });
} else { } else {
callback(`Scene not found: ${path}`); return callback(`找不到场景: ${path}`);
} }
break; break;
@@ -1288,10 +1288,10 @@ export default class NewScript extends cc.Component {
switch (action) { switch (action) {
case "create": case "create":
if (!nodeId) { if (!nodeId) {
return callback(`Node ID is required for create operation`); return callback("创建预制体需要节点 ID");
} }
if (Editor.assetdb.exists(prefabPath)) { if (Editor.assetdb.exists(prefabPath)) {
return callback(`Prefab already exists at ${prefabPath}`); return callback(`预制体已存在: ${prefabPath}`);
} }
// 确保父目录存在 // 确保父目录存在
const absolutePath = Editor.assetdb.urlToFspath(prefabPath); const absolutePath = Editor.assetdb.urlToFspath(prefabPath);
@@ -1321,19 +1321,19 @@ export default class NewScript extends cc.Component {
Editor.Ipc.sendToPanel("scene", "scene:create-prefab", [nodeId], targetDir); Editor.Ipc.sendToPanel("scene", "scene:create-prefab", [nodeId], targetDir);
}, 100); // 稍微延迟以确保重命名生效 }, 100); // 稍微延迟以确保重命名生效
callback(null, `Command sent: Creating prefab from node ${nodeId} at ${targetDir} as ${prefabName}`); callback(null, `指令已发送: 从节点 ${nodeId} 在目录 ${targetDir} 创建名为 ${prefabName} 的预制体`);
break; break;
case "update": case "update":
if (!nodeId) { if (!nodeId) {
return callback(`Node ID is required for update operation`); return callback("更新预制体需要节点 ID");
} }
if (!Editor.assetdb.exists(prefabPath)) { if (!Editor.assetdb.exists(prefabPath)) {
return callback(`Prefab not found at ${prefabPath}`); return callback(`找不到预制体: ${prefabPath}`);
} }
// 更新预制体 // 更新预制体
Editor.Ipc.sendToPanel("scene", "scene:update-prefab", nodeId, prefabPath); Editor.Ipc.sendToPanel("scene", "scene:update-prefab", nodeId, prefabPath);
callback(null, `Command sent: Updating prefab ${prefabPath} from node ${nodeId}`); callback(null, `指令已发送: 从节点 ${nodeId} 更新预制体 ${prefabPath}`);
break; break;
case "instantiate": case "instantiate":
@@ -1362,7 +1362,7 @@ export default class NewScript extends cc.Component {
result.exists = true; result.exists = true;
callback(null, result); callback(null, result);
} else { } else {
callback(`Prefab not found: ${prefabPath}`); return callback(`找不到预制体: ${prefabPath}`);
} }
break; break;
@@ -1405,10 +1405,10 @@ export default class NewScript extends cc.Component {
const refreshPath = (properties && properties.path) ? properties.path : 'db://assets/scripts'; const refreshPath = (properties && properties.path) ? properties.path : 'db://assets/scripts';
Editor.assetdb.refresh(refreshPath, (err) => { Editor.assetdb.refresh(refreshPath, (err) => {
if (err) { if (err) {
addLog("error", `Refresh failed: ${err}`); addLog("error", `刷新失败: ${err}`);
callback(err); callback(err);
} else { } else {
callback(null, `Editor refreshed: ${refreshPath}`); callback(null, `编辑器已刷新: ${refreshPath}`);
} }
}); });
break; break;
@@ -1425,7 +1425,7 @@ export default class NewScript extends cc.Component {
switch (action) { switch (action) {
case "create": case "create":
if (Editor.assetdb.exists(effectPath)) { if (Editor.assetdb.exists(effectPath)) {
return callback(`Effect already exists at ${effectPath}`); return callback(`Effect 已存在: ${effectPath}`);
} }
// 确保父目录存在 // 确保父目录存在
const absolutePath = Editor.assetdb.urlToFspath(effectPath); const absolutePath = Editor.assetdb.urlToFspath(effectPath);
@@ -1476,21 +1476,21 @@ CCProgram fs %{
Editor.assetdb.create(effectPath, content || defaultEffect, (err) => { Editor.assetdb.create(effectPath, content || defaultEffect, (err) => {
if (err) return callback(err); if (err) return callback(err);
Editor.assetdb.refresh(effectPath, (refreshErr) => { Editor.assetdb.refresh(effectPath, (refreshErr) => {
callback(refreshErr, refreshErr ? null : `Effect created at ${effectPath}`); callback(refreshErr, refreshErr ? null : `Effect 已创建: ${effectPath}`);
}); });
}); });
break; break;
case "read": case "read":
if (!Editor.assetdb.exists(effectPath)) { if (!Editor.assetdb.exists(effectPath)) {
return callback(`Effect not found: ${effectPath}`); return callback(`找不到 Effect: ${effectPath}`);
} }
const fspath = Editor.assetdb.urlToFspath(effectPath); const fspath = Editor.assetdb.urlToFspath(effectPath);
try { try {
const data = fs.readFileSync(fspath, "utf-8"); const data = fs.readFileSync(fspath, "utf-8");
callback(null, data); callback(null, data);
} catch (e) { } catch (e) {
callback(`Failed to read effect: ${e.message}`); callback(`读取 Effect 失败: ${e.message}`);
} }
break; break;
@@ -1502,19 +1502,19 @@ CCProgram fs %{
try { try {
fs.writeFileSync(writeFsPath, content, "utf-8"); fs.writeFileSync(writeFsPath, content, "utf-8");
Editor.assetdb.refresh(effectPath, (err) => { Editor.assetdb.refresh(effectPath, (err) => {
callback(err, err ? null : `Effect updated at ${effectPath}`); callback(err, err ? null : `Effect 已更新: ${effectPath}`);
}); });
} catch (e) { } catch (e) {
callback(`Failed to write effect: ${e.message}`); callback(`更新 Effect 失败: ${e.message}`);
} }
break; break;
case "delete": case "delete":
if (!Editor.assetdb.exists(effectPath)) { if (!Editor.assetdb.exists(effectPath)) {
return callback(`Effect not found: ${effectPath}`); return callback(`找不到 Effect: ${effectPath}`);
} }
Editor.assetdb.delete([effectPath], (err) => { Editor.assetdb.delete([effectPath], (err) => {
callback(err, err ? null : `Effect deleted: ${effectPath}`); callback(err, err ? null : `Effect 已删除: ${effectPath}`);
}); });
break; break;
@@ -1524,7 +1524,7 @@ CCProgram fs %{
const info = Editor.assetdb.assetInfoByUuid(uuid); const info = Editor.assetdb.assetInfoByUuid(uuid);
callback(null, info || { url: effectPath, uuid: uuid, exists: true }); callback(null, info || { url: effectPath, uuid: uuid, exists: true });
} else { } else {
callback(`Effect not found: ${effectPath}`); callback(`找不到 Effect: ${effectPath}`);
} }
break; break;
@@ -1541,7 +1541,7 @@ CCProgram fs %{
switch (action) { switch (action) {
case "create": case "create":
if (Editor.assetdb.exists(matPath)) { if (Editor.assetdb.exists(matPath)) {
return callback(`Material already exists at ${matPath}`); return callback(`材质已存在: ${matPath}`);
} }
// 确保父目录存在 // 确保父目录存在
const absolutePath = Editor.assetdb.urlToFspath(matPath); const absolutePath = Editor.assetdb.urlToFspath(matPath);
@@ -1569,14 +1569,14 @@ CCProgram fs %{
Editor.assetdb.create(matPath, JSON.stringify(materialData, null, 2), (err) => { Editor.assetdb.create(matPath, JSON.stringify(materialData, null, 2), (err) => {
if (err) return callback(err); if (err) return callback(err);
Editor.assetdb.refresh(matPath, (refreshErr) => { Editor.assetdb.refresh(matPath, (refreshErr) => {
callback(refreshErr, refreshErr ? null : `Material created at ${matPath}`); callback(refreshErr, refreshErr ? null : `材质已创建: ${matPath}`);
}); });
}); });
break; break;
case "update": case "update":
if (!Editor.assetdb.exists(matPath)) { if (!Editor.assetdb.exists(matPath)) {
return callback(`Material not found at ${matPath}`); return callback(`找不到材质: ${matPath}`);
} }
const fspath = Editor.assetdb.urlToFspath(matPath); const fspath = Editor.assetdb.urlToFspath(matPath);
try { try {
@@ -1605,19 +1605,19 @@ CCProgram fs %{
fs.writeFileSync(fspath, JSON.stringify(matData, null, 2), "utf-8"); fs.writeFileSync(fspath, JSON.stringify(matData, null, 2), "utf-8");
Editor.assetdb.refresh(matPath, (err) => { Editor.assetdb.refresh(matPath, (err) => {
callback(err, err ? null : `Material updated at ${matPath}`); callback(err, err ? null : `材质已更新: ${matPath}`);
}); });
} catch (e) { } catch (e) {
callback(`Failed to update material: ${e.message}`); callback(`更新材质失败: ${e.message}`);
} }
break; break;
case "delete": case "delete":
if (!Editor.assetdb.exists(matPath)) { if (!Editor.assetdb.exists(matPath)) {
return callback(`Material not found at ${matPath}`); return callback(`找不到材质: ${matPath}`);
} }
Editor.assetdb.delete([matPath], (err) => { Editor.assetdb.delete([matPath], (err) => {
callback(err, err ? null : `Material deleted at ${matPath}`); callback(err, err ? null : `材质已删除: ${matPath}`);
}); });
break; break;
@@ -1627,7 +1627,7 @@ CCProgram fs %{
const info = Editor.assetdb.assetInfoByUuid(uuid); const info = Editor.assetdb.assetInfoByUuid(uuid);
callback(null, info || { url: matPath, uuid: uuid, exists: true }); callback(null, info || { url: matPath, uuid: uuid, exists: true });
} else { } else {
callback(`Material not found: ${matPath}`); callback(`找不到材质: ${matPath}`);
} }
break; break;
@@ -1644,7 +1644,7 @@ CCProgram fs %{
switch (action) { switch (action) {
case "create": case "create":
if (Editor.assetdb.exists(path)) { if (Editor.assetdb.exists(path)) {
return callback(`Texture already exists at ${path}`); return callback(`纹理已存在: ${path}`);
} }
// 确保父目录存在 // 确保父目录存在
const absolutePath = Editor.assetdb.urlToFspath(path); const absolutePath = Editor.assetdb.urlToFspath(path);
@@ -1671,7 +1671,7 @@ CCProgram fs %{
// 4. 如果有 9-slice 设置,更新 Meta // 4. 如果有 9-slice 设置,更新 Meta
if (properties && (properties.border || properties.type)) { if (properties && (properties.border || properties.type)) {
const uuid = Editor.assetdb.urlToUuid(path); const uuid = Editor.assetdb.urlToUuid(path);
if (!uuid) return callback(null, `Texture created but UUID not found immediately.`); if (!uuid) return callback(null, `纹理已创建,但未能立即获取 UUID。`);
// 稍微延迟确保 Meta 已生成 // 稍微延迟确保 Meta 已生成
setTimeout(() => { setTimeout(() => {
@@ -1700,28 +1700,28 @@ CCProgram fs %{
if (changed) { if (changed) {
Editor.assetdb.saveMeta(uuid, JSON.stringify(meta), (err) => { Editor.assetdb.saveMeta(uuid, JSON.stringify(meta), (err) => {
if (err) Editor.warn(`Failed to save meta for ${path}: ${err}`); if (err) Editor.warn(`保存资源 Meta 失败 ${path}: ${err}`);
callback(null, `Texture created and meta updated at ${path}`); callback(null, `纹理已创建并更新 Meta: ${path}`);
}); });
return; return;
} }
} }
callback(null, `Texture created at ${path}`); callback(null, `纹理已创建: ${path}`);
}, 100); }, 100);
} else { } else {
callback(null, `Texture created at ${path}`); callback(null, `纹理已创建: ${path}`);
} }
}); });
} catch (e) { } catch (e) {
callback(`Failed to write texture file: ${e.message}`); callback(`写入纹理文件失败: ${e.message}`);
} }
break; break;
case "delete": case "delete":
if (!Editor.assetdb.exists(path)) { if (!Editor.assetdb.exists(path)) {
return callback(`Texture not found at ${path}`); return callback(`找不到纹理: ${path}`);
} }
Editor.assetdb.delete([path], (err) => { Editor.assetdb.delete([path], (err) => {
callback(err, err ? null : `Texture deleted at ${path}`); callback(err, err ? null : `纹理已删除: ${path}`);
}); });
break; break;
case "get_info": case "get_info":
@@ -1730,12 +1730,12 @@ CCProgram fs %{
const info = Editor.assetdb.assetInfoByUuid(uuid); const info = Editor.assetdb.assetInfoByUuid(uuid);
callback(null, info || { url: path, uuid: uuid, exists: true }); callback(null, info || { url: path, uuid: uuid, exists: true });
} else { } else {
callback(`Texture not found: ${path}`); callback(`找不到纹理: ${path}`);
} }
break; break;
case "update": case "update":
if (!Editor.assetdb.exists(path)) { if (!Editor.assetdb.exists(path)) {
return callback(`Texture not found at ${path}`); return callback(`找不到纹理: ${path}`);
} }
const uuid = Editor.assetdb.urlToUuid(path); const uuid = Editor.assetdb.urlToUuid(path);
let meta = Editor.assetdb.loadMeta(uuid); let meta = Editor.assetdb.loadMeta(uuid);
@@ -1756,7 +1756,7 @@ CCProgram fs %{
} }
if (!meta) { if (!meta) {
return callback(`Failed to load meta for ${path}`); return callback(`加载资源 Meta 失败: ${path}`);
} }
let changed = false; let changed = false;
@@ -1838,7 +1838,7 @@ CCProgram fs %{
} }
break; break;
default: default:
callback(`Unknown texture action: ${action}`); callback(`未知的纹理操作类型: ${action}`);
break; break;
} }
}, },
@@ -2002,12 +2002,12 @@ CCProgram fs %{
// 1. 获取文件系统路径 // 1. 获取文件系统路径
const fspath = Editor.assetdb.urlToFspath(filePath); const fspath = Editor.assetdb.urlToFspath(filePath);
if (!fspath) { if (!fspath) {
return callback(`File not found or invalid URL: ${filePath}`); return callback(`找不到文件或 URL 无效: ${filePath}`);
} }
// 2. 检查文件是否存在 // 2. 检查文件是否存在
if (!fs.existsSync(fspath)) { if (!fs.existsSync(fspath)) {
return callback(`File does not exist: ${fspath}`); return callback(`文件不存在: ${fspath}`);
} }
// 3. 读取内容并验证 // 3. 读取内容并验证
@@ -2016,7 +2016,7 @@ CCProgram fs %{
// 检查空文件 // 检查空文件
if (!content || content.trim().length === 0) { if (!content || content.trim().length === 0) {
return callback(null, { valid: false, message: "Script is empty" }); return callback(null, { valid: false, message: "脚本内容为空" });
} }
// 对于 JavaScript 脚本,使用 Function 构造器进行语法验证 // 对于 JavaScript 脚本,使用 Function 构造器进行语法验证
@@ -2024,7 +2024,7 @@ CCProgram fs %{
const wrapper = `(function() { ${content} })`; const wrapper = `(function() { ${content} })`;
try { try {
new Function(wrapper); new Function(wrapper);
callback(null, { valid: true, message: "JavaScript syntax is valid" }); callback(null, { valid: true, message: "JavaScript 语法验证通过" });
} catch (syntaxErr) { } catch (syntaxErr) {
return callback(null, { valid: false, message: syntaxErr.message }); return callback(null, { valid: false, message: syntaxErr.message });
} }
@@ -2037,7 +2037,7 @@ CCProgram fs %{
// 检查是否有 class 定义 (简单的启发式检查) // 检查是否有 class 定义 (简单的启发式检查)
if (!content.includes('class ') && !content.includes('interface ') && !content.includes('enum ') && !content.includes('export ')) { 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." }); return callback(null, { valid: true, message: "警告: TypeScript 文件似乎缺少标准定义 (class/interface/export),但由于缺少编译器,已跳过基础语法检查。" });
} }
callback(null, { valid: true, message: "TypeScript 基础检查通过。(完整编译验证需要通过编辑器构建流程)" }); callback(null, { valid: true, message: "TypeScript 基础检查通过。(完整编译验证需要通过编辑器构建流程)" });
@@ -2079,7 +2079,7 @@ CCProgram fs %{
// 修改场景中的节点(需要通过 scene-script // 修改场景中的节点(需要通过 scene-script
"set-node-property"(event, args) { "set-node-property"(event, args) {
addLog("mcp", `Creating node: ${args.name} (${args.type})`); addLog("mcp", `设置节点属性: ${args.name} (${args.type})`);
// 确保第一个参数 'mcp-bridge' 和 package.json 的 name 一致 // 确保第一个参数 'mcp-bridge' 和 package.json 的 name 一致
Editor.Scene.callSceneScript("mcp-bridge", "set-property", args, (err, result) => { Editor.Scene.callSceneScript("mcp-bridge", "set-property", args, (err, result) => {
if (err) { if (err) {
@@ -2091,10 +2091,10 @@ CCProgram fs %{
}); });
}, },
"create-node"(event, args) { "create-node"(event, args) {
addLog("mcp", `Creating node: ${args.name} (${args.type})`); addLog("mcp", `创建节点: ${args.name} (${args.type})`);
Editor.Scene.callSceneScript("mcp-bridge", "create-node", args, (err, result) => { Editor.Scene.callSceneScript("mcp-bridge", "create-node", args, (err, result) => {
if (err) addLog("error", `CreateNode Failed: ${err}`); if (err) addLog("error", `创建节点失败: ${err}`);
else addLog("success", `Node Created: ${result}`); else addLog("success", `节点已创建: ${result}`);
event.reply(err, result); event.reply(err, result);
}); });
}, },
@@ -2376,9 +2376,14 @@ CCProgram fs %{
break; break;
case "begin_group": case "begin_group":
// scene:undo-record [id] // scene:undo-record [id]
// 这里的 id 好像是可选的,或者用于区分不同的事务 // 注意:在 2.4.x 中undo-record 通常需要一个有效的 uuid
Editor.Ipc.sendToPanel("scene", "scene:undo-record", description || "MCP Action"); // 如果没有提供 uuid不应将 description 作为 ID 发送,否则会报 Unknown object to record
callback(null, `Undo group started: ${description || "MCP Action"}`); addLog("info", `开始撤销组: ${description || "MCP 动作"}`);
// 如果有参数包含 id则记录该节点
if (args.id) {
Editor.Ipc.sendToPanel("scene", "scene:undo-record", args.id);
}
callback(null, `撤销组已启动: ${description || "MCP 动作"}`);
break; break;
case "end_group": case "end_group":
Editor.Ipc.sendToPanel("scene", "scene:undo-commit"); Editor.Ipc.sendToPanel("scene", "scene:undo-commit");

View File

@@ -109,44 +109,42 @@ module.exports = {
let node = findNode(id); let node = findNode(id);
if (node) { if (node) {
Editor.log(`[scene-script] Node found: ${node.name}, Current Pos: (${node.x}, ${node.y})`); Editor.log(`[scene-script] 找到节点: ${node.name}, 当前坐标: (${node.x}, ${node.y})`);
// 使用 scene:set-property 实现支持 Undo 的属性修改
// 注意IPC 消息需要发送到 'scene' 面板
if (x !== undefined) { if (x !== undefined) {
node.x = Number(x); Editor.Ipc.sendToPanel("scene", "scene:set-property", { id, path: "x", type: "Number", value: Number(x) });
Editor.log(`[scene-script] Set x to ${node.x}`);
} }
if (y !== undefined) { if (y !== undefined) {
node.y = Number(y); Editor.Ipc.sendToPanel("scene", "scene:set-property", { id, path: "y", type: "Number", value: Number(y) });
Editor.log(`[scene-script] Set y to ${node.y}`);
} }
// [新增] 支持设置节点宽高 (用于测试 9-slice 等需要调整尺寸的情况)
if (args.width !== undefined) { if (args.width !== undefined) {
node.width = Number(args.width); Editor.Ipc.sendToPanel("scene", "scene:set-property", { id, path: "width", type: "Number", value: Number(args.width) });
Editor.log(`[scene-script] Set width to ${node.width}`);
} }
if (args.height !== undefined) { if (args.height !== undefined) {
node.height = Number(args.height); Editor.Ipc.sendToPanel("scene", "scene:set-property", { id, path: "height", type: "Number", value: Number(args.height) });
Editor.log(`[scene-script] Set height to ${node.height}`); }
if (scaleX !== undefined) {
Editor.Ipc.sendToPanel("scene", "scene:set-property", { id, path: "scaleX", type: "Number", value: Number(scaleX) });
}
if (scaleY !== undefined) {
Editor.Ipc.sendToPanel("scene", "scene:set-property", { id, path: "scaleY", type: "Number", value: Number(scaleY) });
} }
if (scaleX !== undefined) node.scaleX = Number(scaleX);
if (scaleY !== undefined) node.scaleY = Number(scaleY);
if (color) { if (color) {
const c = new cc.Color().fromHEX(color); const c = new cc.Color().fromHEX(color);
// 使用 scene:set-property 实现支持 Undo 的颜色修改
// 注意IPC 消息需要发送到场景面板
Editor.Ipc.sendToPanel("scene", "scene:set-property", { Editor.Ipc.sendToPanel("scene", "scene:set-property", {
id: id, id: id,
path: "color", path: "color",
type: "Color", type: "Color",
value: { r: c.r, g: c.g, b: c.b, a: c.a } value: { r: c.r, g: c.g, b: c.b, a: c.a }
}); });
// 既然走了 IPC就不需要手动 set node.color 了,也不需要重复 dirty
} }
Editor.Ipc.sendToMain("scene:dirty"); Editor.Ipc.sendToMain("scene:dirty");
Editor.Ipc.sendToAll("scene:node-changed", { uuid: id }); Editor.Ipc.sendToAll("scene:node-changed", { uuid: id });
Editor.log(`[scene-script] Update complete. New Pos: (${node.x}, ${node.y})`); Editor.log(`[scene-script] 更新完成。新坐标: (${node.x}, ${node.y})`);
if (event.reply) event.reply(null, "变换信息已更新"); if (event.reply) event.reply(null, "变换信息已更新");
} else { } else {
if (event.reply) event.reply(new Error("找不到节点")); if (event.reply) event.reply(new Error("找不到节点"));
@@ -349,9 +347,9 @@ module.exports = {
loadedCount++; loadedCount++;
if (!err && asset) { if (!err && asset) {
loadedAssets[idx] = asset; loadedAssets[idx] = asset;
Editor.log(`[scene-script] Successfully loaded asset for ${key}[${idx}]: ${asset.name}`); Editor.log(`[scene-script] 成功为 ${key}[${idx}] 加载资源: ${asset.name}`);
} else { } else {
Editor.warn(`[scene-script] Failed to load asset ${uuid} for ${key}[${idx}]: ${err}`); Editor.warn(`[scene-script] 未能为 ${key}[${idx}] 加载资源 ${uuid}: ${err}`);
} }
if (loadedCount === uuids.length) { if (loadedCount === uuids.length) {
@@ -391,13 +389,13 @@ module.exports = {
if (targetComp) { if (targetComp) {
finalValue = targetComp; finalValue = targetComp;
} else { } else {
Editor.warn(`[scene-script] Component ${propertyType.name} not found on node ${targetNode.name}`); Editor.warn(`[scene-script] 在节点 ${targetNode.name} 上找不到组件 ${propertyType.name}`);
} }
} }
Editor.log(`[scene-script] Applied Reference for ${key}: ${targetNode.name}`); Editor.log(`[scene-script] 已应用 ${key} 的引用: ${targetNode.name}`);
} else if (value && value.length > 20) { } else if (value && value.length > 20) {
// 如果明确是组件/节点类型但找不到,才报错 // 如果明确是组件/节点类型但找不到,才报错
Editor.warn(`[scene-script] Failed to resolve target node/comp for ${key}: ${value}`); Editor.warn(`[scene-script] 无法解析 ${key} 的目标节点/组件: ${value}`);
} }
} else { } else {
// 3. 通用启发式 (找不到类型时的 fallback) // 3. 通用启发式 (找不到类型时的 fallback)
@@ -405,7 +403,7 @@ module.exports = {
const targetNode = findNode(value); const targetNode = findNode(value);
if (targetNode) { if (targetNode) {
finalValue = targetNode; finalValue = targetNode;
Editor.log(`[scene-script] Heuristic resolved Node for ${key}: ${targetNode.name}`); Editor.log(`[scene-script] 启发式解析 ${key} 的节点: ${targetNode.name}`);
} else { } else {
// 找不到节点且是 UUID -> 视为资源 // 找不到节点且是 UUID -> 视为资源
const compIndex = node._components.indexOf(component); const compIndex = node._components.indexOf(component);
@@ -417,14 +415,14 @@ module.exports = {
value: { uuid: value }, value: { uuid: value },
isSubProp: false isSubProp: false
}); });
Editor.log(`[scene-script] Heuristic resolved Asset via IPC for ${key}: ${value}`); Editor.log(`[scene-script] 通过 IPC 启发式解析 ${key} 的资源: ${value}`);
} }
return; return;
} }
} }
} }
} catch (e) { } catch (e) {
Editor.warn(`[scene-script] Property resolution failed for ${key}: ${e.message}`); Editor.warn(`[scene-script] 解析属性 ${key} 失败: ${e.message}`);
} }
component[key] = finalValue; component[key] = finalValue;

View File

@@ -48,7 +48,7 @@
1. **真实加载**:使用 `cc.AssetLibrary.loadAsset(uuid, callback)` 在场景进程中异步加载真实的资源实例。 1. **真实加载**:使用 `cc.AssetLibrary.loadAsset(uuid, callback)` 在场景进程中异步加载真实的资源实例。
2. **实例赋值**:在回调中将加载到的 `asset` 对象赋予组件(`component[key] = asset`),这确保了场景保存时能生成正确的序列化对象。 2. **实例赋值**:在回调中将加载到的 `asset` 对象赋予组件(`component[key] = asset`),这确保了场景保存时能生成正确的序列化对象。
3. **UI 同步**:同步发送 IPC `scene:set-property`,使用 `{ uuid: value }` 格式通知编辑器面板更新 Inspector UI。 3. **UI 同步**:同步发送 IPC `scene:set-property`,使用 `{ uuid: value }` 格式通知编辑器面板更新 Inspector UI。
* **Node/Component**: 对于节点组件引用,通过 `findNode` 查找实例并直接赋值实例对象,而非 UUID 字符串。 * **Node/Component**: 对于节点 or 组件引用,通过 `findNode` 查找实例并直接赋值实例对象,而非 UUID 字符串。
--- ---
@@ -68,13 +68,29 @@
* **资产识别启发式**:当通过 `manage_components` 赋值时,如果属性名包含以下关键字,插件会尝试将其作为 UUID 资源处理: * **资产识别启发式**:当通过 `manage_components` 赋值时,如果属性名包含以下关键字,插件会尝试将其作为 UUID 资源处理:
`prefab`, `sprite`, `texture`, `material`, `skeleton`, `spine`, `atlas`, `font`, `audio`, `data` `prefab`, `sprite`, `texture`, `material`, `skeleton`, `spine`, `atlas`, `font`, `audio`, `data`
* **建议**:如果资源未正确加载,请检查属性名是否包含以上关键字,或手动确认该 UUID 不属于任何节点。 * **建议**:如果资源未正确加载,请检查属性名是否包含以上关键字,或手动确认该 UUID 不属于任何节点。
69:
70: --- ---
71:
72: ## 7. 搜索工具 (search_project) 使用建议 ## 7. 搜索工具 (search_project) 使用建议
73: * **性能建议**:尽量指定 `path` 参数缩小搜索范围(如 `db://assets/scripts`),避免全项目大面积搜索,尤其是在包含大量旧资源的 Library 目录(虽然插件已过滤)。 * **性能建议**:尽量指定 `path` 参数缩小搜索范围(如 `db://assets/scripts`),避免全项目大面积搜索,尤其是在包含大量旧资源的 Library 目录(虽然插件已过滤)。
74: * **正则表达式**:在使用 `useRegex` 时,确保正则模式的语法正确。如果正则匹配失败,工具会返回详细的错误提示。 * **正则表达式**:在使用 `useRegex` 时,确保正则模式的语法正确。如果正则匹配失败,工具会返回详细的错误提示。
75: * **模式选择** * **模式选择**
76: * 查找具体逻辑代码:使用 `matchType: "content"` * 查找具体逻辑代码:使用 `matchType: "content"`
77: * 定位资源文件:使用 `matchType: "file_name"` 并配合 `extensions` 过滤。 * 定位资源文件:使用 `matchType: "file_name"` 并配合 `extensions` 过滤。
78: * 重构目录结构前:使用 `matchType: "dir_name"` 检查目录名冲突。 * 重构目录结构前:使用 `matchType: "dir_name"` 检查目录名冲突。
---
## 8. Undo/Redo (撤销/重做) 使用指南
### 8.1 事务分组
* **背景**:连续执行多次修改(如同时移动并缩放)时,通常希望一次“撤销”能回滚所有更改。
* **最佳实践**
1. 调用 `manage_undo(action: 'begin_group', description: '操作描述')`
2. 执行多次修改(如调用 `update_node_transform`)。
3. 调用 `manage_undo(action: 'end_group')`
* **注意**`undo-record` 需要在 `begin_group` 时明确关联节点 ID否则可能导致撤销栈无法精准匹配对象。
### 8.2 属性修改方式
* **核心规则**:在 `scene-script.js` 中严禁直接使用 `node.x = 100`
* **正确做法**:必须通过 `Editor.Ipc.sendToPanel('scene', 'scene:set-property', ...)`。只有这样,修改才会被 Cocos Creator 的 UndoManager 捕获,从而支持撤销。