fix: Windows spawn, prefab path, preview API, asset deps, error messages; add execute-context resource, tool descriptions, scene/script/asset tools

Bug fixes (verified against Cocos Creator 3.8.8):
- diagnostics: shell:true for .cmd/.bat on Windows (T125/T126)
- prefabs: duplicatePrefab target resolves under assets/ (T420)
- cocos-project: add preview.open candidate for 3.8.8 (T444)
- assets-advanced: detect directory assets in inspectAssetDependencies (T110)
- scene: improve component-not-found errors with compilation hint (T429)

Documentation improvements:
- tool-registry: add enum and injected vars to execute_javascript, path format examples, asset ref limitation note
- resources: add cocos://mcp/execute-context resource with variables, patterns, pitfalls

New tools (core 37->38, full 101->110):
- scene-management: create_scene, query_scene_state (core), copy_paste_node, rename_node, reparent_node
- scripts: create_script with component/plain templates
- prefabs: create_prefab
- assets-advanced: batch_asset_ops, find_unused_assets
This commit is contained in:
mingyuansi
2026-06-30 21:53:06 +08:00
parent a9b539ca9c
commit b65a4f22c3
21 changed files with 5273 additions and 53 deletions
+583
View File
@@ -0,0 +1,583 @@
# Implementation Plan: High-Frequency Editor Operations
> **Goal**: Fill the 9 highest-impact tool gaps identified in the Cocos Creator business-development workflow analysis.
>
> **Current state**: 101 tools (core=37, full=101), version 0.4.0
> **Target state**: 110 tools (core=38, full=110)
## Tool Summary
| # | Tool | Profile | Side | Phase | New file? |
|---|---|---|---|---|---|
| 1 | `create_scene` | `full` | editor | 1 | `lib/tools/scene-management.js` |
| 2 | `query_scene_state` | `core` | editor | 1 | same |
| 3 | `copy_paste_node` | `full` | editor | 1 | same |
| 4 | `rename_node` | `full` | scene | 2 | scene.js + scene-management.js |
| 5 | `reparent_node` | `full` | scene | 2 | same |
| 6 | `create_prefab` | `full` | editor | 3 | prefabs.js + tool-registry.js |
| 7 | `create_script` | `full` | editor | 4 | `lib/tools/scripts.js` |
| 8 | `batch_asset_ops` | `full` | editor | 5 | assets-advanced.js (append) |
| 9 | `find_unused_assets` | `full` | editor | 5 | same |
## Files Changed
| File | Action | Phase |
|---|---|---|
| `lib/tools/scene-management.js` | **New** — factory for tools 1-5 | 1, 2 |
| `scene.js` | **Modify** — add `renameNode`, `reparentNode` methods | 2 |
| `lib/prefabs.js` | **Modify** — add `createPrefab` helper | 3 |
| `lib/tool-registry.js` | **Modify** — import + spread new factories, add `create_prefab` inline | 1-5 |
| `lib/tools/scripts.js` | **New** — factory for tool 7 | 4 |
| `lib/tools/assets-advanced.js` | **Modify** — append tools 8-9 | 5 |
| `docs/TOOLS.md` | **Regenerate** via `npm run docs:generate` | final |
| `README.md` / `README_CN.md` | **Modify** — add new tool categories | final |
| `CHANGELOG.md` | **Modify** — add `## [0.5.0]` entry | final |
| `test/*.test.js` | **New** — unit tests for pure-logic helpers | each phase |
---
## Phase 1: Scene Management Tools
**File**: `lib/tools/scene-management.js` (new)
**Dependencies**: `{ createSchema, getRuntimeContext }` — uses `Editor.Message.request` directly (editor-side), no scene bridge needed for tools 1-3.
**Imports**: Reuse `tryEditorRequests` from `./cocos-project.js`.
### Tool 1: `create_scene`
```js
{
name: 'create_scene',
profile: 'full',
description: '[core] Create a new scene asset in the Cocos project and open it.',
inputSchema: createSchema(
{
sceneName: { type: 'string', description: 'Name of the new scene (without extension).' },
savePath: { type: 'string', description: 'Asset-db URL, e.g. db://assets/scenes/NewScene.scene.' },
open: { type: 'boolean', description: 'Whether to open the scene after creation. Default true.' },
},
['sceneName', 'savePath']
),
handler: async (args) => { ... }
}
```
**Implementation approach**:
Use `Editor.Message.request('asset-db', 'create-asset', savePath, '')` to create an empty `.scene` file. Cocos Creator's asset-db will generate a valid scene template automatically when the file extension is `.scene`. Then optionally `openAsset(savePath)`.
Fallback: if `create-asset` doesn't auto-generate scene content, write a minimal scene JSON template to disk and let asset-db import it.
```js
// Pseudocode
const result = await Editor.Message.request('asset-db', 'create-asset', args.savePath, '');
// result contains { uuid, url, ... }
if (args.open !== false) {
await openAsset(result.url || args.savePath);
}
return { created: true, sceneName: args.sceneName, url: result.url, uuid: result.uuid };
```
**IPC mapping**:
- `asset-db: create-asset` (primary)
- `asset-db: import-asset` (fallback if create-asset doesn't work for scenes)
**Error handling**: Throw if `savePath` doesn't end with `.scene`. Throw if asset already exists.
---
### Tool 2: `query_scene_state`
```js
{
name: 'query_scene_state',
profile: 'core',
description: '[specialist] Query scene state: dirty (unsaved changes), ready, or soft-reload.',
inputSchema: createSchema(
{
action: { type: 'string', enum: ['is_dirty', 'is_ready', 'soft_reload'], description: 'State query or action.' },
},
['action']
),
handler: async (args) => { ... }
}
```
**Why `core`**: `is_dirty` is a critical signal for AI to decide whether to save before making changes. This is the only new tool promoted to `core`.
**IPC mapping**:
- `is_dirty``Editor.Message.request('scene', 'query-is-dirty')` → returns boolean
- `is_ready``Editor.Message.request('scene', 'query-ready')` → returns boolean
- `soft_reload``Editor.Message.request('scene', 'soft-reload')` → reloads scene from disk
**Implementation**:
```js
const IPC_MAP = {
is_dirty: { channel: 'scene', method: 'query-is-dirty' },
is_ready: { channel: 'scene', method: 'query-ready' },
soft_reload: { channel: 'scene', method: 'soft-reload' },
};
const candidate = IPC_MAP[args.action];
const result = await tryEditorRequests([candidate]);
return { action: args.action, result: result.result };
```
---
### Tool 3: `copy_paste_node`
```js
{
name: 'copy_paste_node',
profile: 'full',
description: '[core] Copy, cut, or paste scene nodes via the Cocos editor clipboard.',
inputSchema: createSchema(
{
action: { type: 'string', enum: ['copy', 'cut', 'paste'], description: 'Clipboard action.' },
uuids: { type: 'array', items: { type: 'string' }, description: 'Node UUIDs to copy or cut.' },
target: { type: 'string', description: 'Target parent node UUID for paste.' },
keepWorldTransform: { type: 'boolean', description: 'Keep world transform on paste. Default false.' },
},
['action']
),
handler: async (args) => { ... }
}
```
**IPC mapping**:
- `copy``Editor.Message.request('scene', 'copy-node', args.uuids)`
- `cut``Editor.Message.request('scene', 'cut-node', args.uuids)`
- `paste``Editor.Message.request('scene', 'paste-node', { target: args.target, keepWorldTransform: args.keepWorldTransform })`
**Validation**:
- `copy`/`cut` requires `uuids` non-empty
- `paste` requires `target`
---
## Phase 2: Node Operations
**Files**: `scene.js` (add methods) + `lib/tools/scene-management.js` (add tool definitions)
### Tool 4: `rename_node` (scene-side)
**scene.js method** — add to `exports.methods`:
```js
async renameNode(options = {}) {
const node = findNode(options);
if (!node) {
throw new Error('Target node was not found.');
}
const oldName = node.name;
const oldPath = getNodePath(node);
const newName = String(options.newName || '').trim();
if (!newName) {
throw new Error('newName is required.');
}
node.name = newName;
return {
renamed: true,
oldName,
newName,
oldPath,
newPath: getNodePath(node),
uuid: node.uuid,
};
},
```
**Tool definition** (in `scene-management.js`):
```js
{
name: 'rename_node',
profile: 'full',
description: '[core] Rename a scene node.',
inputSchema: createSchema(
{
path: { type: 'string', description: 'Node hierarchy path.' },
uuid: { type: 'string', description: 'Node UUID.' },
name: { type: 'string', description: 'Fallback exact node name.' },
newName: { type: 'string', description: 'New name for the node.' },
},
['newName']
),
handler: async (args) => sceneBridge.call('renameNode', args),
}
```
**Dependencies**: `{ createSchema, sceneBridge }` — add `sceneBridge` to the factory's dependency bag.
---
### Tool 5: `reparent_node` (scene-side)
**scene.js method** — add to `exports.methods`:
```js
async reparentNode(options = {}) {
const node = findNode(options);
if (!node) {
throw new Error('Target node was not found.');
}
const oldPath = getNodePath(node);
const oldParent = node.parent;
let newParent;
if (options.targetUuid) {
newParent = findNodeByUuid(options.targetUuid);
} else if (options.targetPath) {
newParent = findNodeByPath(options.targetPath);
} else if (options.targetName) {
newParent = findNodeByName(options.targetName);
}
if (!newParent) {
throw new Error('Target parent node was not found.');
}
const siblingIndex = Number.isFinite(options.siblingIndex) ? options.siblingIndex : -1;
if (siblingIndex >= 0 && siblingIndex <= newParent.children.length) {
newParent.insertChild(node, siblingIndex);
} else {
node.parent = newParent;
}
return {
reparented: true,
uuid: node.uuid,
oldPath,
newPath: getNodePath(node),
oldParent: oldParent ? oldParent.name : null,
newParent: newParent.name,
siblingIndex: node.getSiblingIndex(),
};
},
```
**Tool definition** (in `scene-management.js`):
```js
{
name: 'reparent_node',
profile: 'full',
description: '[core] Move a node to a new parent in the scene hierarchy.',
inputSchema: createSchema(
{
path: { type: 'string', description: 'Node hierarchy path to move.' },
uuid: { type: 'string', description: 'Node UUID to move.' },
name: { type: 'string', description: 'Fallback exact node name.' },
targetPath: { type: 'string', description: 'New parent node path.' },
targetUuid: { type: 'string', description: 'New parent node UUID.' },
targetName: { type: 'string', description: 'Fallback exact new parent name.' },
siblingIndex: { type: 'number', description: 'Optional insert position among siblings. Default: append.' },
},
[]
),
handler: async (args) => sceneBridge.call('reparentNode', args),
}
```
---
## Phase 3: Prefab Creation
**Files**: `lib/prefabs.js` (add helper) + `lib/tool-registry.js` (add inline tool)
### Tool 6: `create_prefab`
**lib/prefabs.js** — add `createPrefab` helper:
```js
async function createPrefab(options = {}) {
const nodeUuid = String(options.nodeUuid || '').trim();
const savePath = String(options.savePath || '').trim();
if (!nodeUuid) throw new Error('nodeUuid is required.');
if (!savePath) throw new Error('savePath is required.');
const result = await Editor.Message.request('scene', 'create-prefab', nodeUuid, savePath);
return {
created: true,
nodeUuid,
savePath,
result,
};
}
```
**Tool definition** (inline in `tool-registry.js`, near existing prefab tools ~line 760):
```js
{
name: 'create_prefab',
profile: 'full',
description: '[core] Create a prefab asset from a scene node.',
inputSchema: createSchema(
{
nodeUuid: { type: 'string', description: 'UUID of the scene node to save as prefab.' },
savePath: { type: 'string', description: 'Asset-db URL, e.g. db://assets/prefabs/Enemy.prefab.' },
},
['nodeUuid', 'savePath']
),
handler: async (args) => createPrefab(args),
},
```
**IPC mapping**: `Editor.Message.request('scene', 'create-prefab', nodeUuid, savePath)`
**Notes**: The `create-prefab` scene message is well-documented in Cocos Creator 3.8+. It takes a node UUID and a target `.prefab` path, creates the prefab asset, and links the scene node as a prefab instance.
---
## Phase 4: Script Creation
**File**: `lib/tools/scripts.js` (new)
**Dependencies**: `{ createSchema, getRuntimeContext }` — uses `fs` for file writing + `Editor.Message.request('asset-db', 'refresh-asset')` for refresh.
### Tool 7: `create_script`
```js
{
name: 'create_script',
profile: 'full',
description: '[core] Create a new TypeScript component script with a standard Cocos template.',
inputSchema: createSchema(
{
scriptName: { type: 'string', description: 'Class name in PascalCase, e.g. PlayerController.' },
savePath: { type: 'string', description: 'Asset-db URL, e.g. db://assets/scripts/PlayerController.ts.' },
template: { type: 'string', enum: ['component', 'plain'], description: 'Script template. Default: component.' },
properties: { type: 'array', items: { type: 'object' }, description: 'Optional @property declarations.' },
},
['scriptName', 'savePath']
),
handler: async (args) => { ... }
}
```
**Template generation** (pure logic, unit-testable):
```js
function generateComponentTemplate(className, properties = []) {
const props = properties.map(p => {
const type = p.type || 'number';
const name = p.name || 'property';
return ` @property({ type: ${type} })\n ${name}: ${type} = ${p.default || '0'};`;
}).join('\n\n');
return `import { _decorator, Component, ${properties.map(p => p.type).filter(t => t && t !== 'number' && t !== 'string' && t !== 'boolean').join(', ')} } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('${className}')
export class ${className} extends Component {
${props || ' // Add properties here'}
}`;
```
**Implementation**:
1. Generate template string from `scriptName` + `template` + `properties`
2. Write to project filesystem via `resolveProjectPath` + `fs.writeFileSync`
3. Refresh asset-db: `Editor.Message.request('asset-db', 'refresh-asset', savePath)`
4. Return `{ created: true, scriptName, savePath, className }`
**Unit tests** (test/scripts.test.js):
- `generateComponentTemplate('PlayerController')` → contains `@ccclass`, `export class PlayerController extends Component`
- `generateComponentTemplate('Enemy', [{name:'speed',type:'number',default:'5'}])` → contains `@property({ type: number })` and `speed: number = 5`
- `generatePlainTemplate('Utils')` → no `@ccclass`, just `export class Utils`
---
## Phase 5: Asset Batch Operations
**File**: `lib/tools/assets-advanced.js` (append to existing)
**Dependencies**: `{ createSchema, getRuntimeContext }` — uses `Editor.Message.request('asset-db', ...)` and `listAssets`.
### Tool 8: `batch_asset_ops`
```js
{
name: 'batch_asset_ops',
profile: 'full',
description: '[core] Batch import or delete assets in the Cocos asset database.',
inputSchema: createSchema(
{
action: { type: 'string', enum: ['import', 'delete'], description: 'Batch operation.' },
sourceDirectory: { type: 'string', description: 'Local filesystem source (import only).' },
targetDirectory: { type: 'string', description: 'Asset-db target URL, e.g. db://assets/textures (import only).' },
urls: { type: 'array', items: { type: 'string' }, description: 'Asset URLs to delete (delete only).' },
fileFilter: { type: 'array', items: { type: 'string' }, description: 'File extensions, e.g. [".png", ".jpg"].' },
recursive: { type: 'boolean', description: 'Import recursively. Default false.' },
overwrite: { type: 'boolean', description: 'Overwrite existing assets. Default false.' },
},
['action']
),
handler: async (args) => { ... }
}
```
**IPC mapping**:
- `import``Editor.Message.request('asset-db', 'import', targetDirectory, sourceDirectory)`
- `delete``Editor.Message.request('asset-db', 'delete-assets', urls)`
**Validation**:
- `import` requires `sourceDirectory` + `targetDirectory`
- `delete` requires `urls` non-empty
### Tool 9: `find_unused_assets`
```js
{
name: 'find_unused_assets',
profile: 'full',
description: '[core] Find assets not referenced by any scene, prefab, or script in the project.',
inputSchema: createSchema(
{
directory: { type: 'string', description: 'Asset-db directory to scan. Default: db://assets.' },
excludeDirectories: { type: 'array', items: { type: 'string' }, description: 'Directories to exclude.' },
assetTypes: { type: 'array', items: { type: 'string' }, description: 'Filter by asset type, e.g. ["cc.Texture2D", "cc.AudioClip"].' },
limit: { type: 'number', description: 'Max results. Default 200.' },
},
[]
),
handler: async (args) => { ... }
}
```
**Implementation** (pure-logic core, unit-testable):
```js
async function findUnusedAssets(projectPath, options = {}) {
// 1. Query all assets in the directory
const allAssets = await listAssets({
pattern: options.directory || 'db://assets/**',
ccType: options.assetTypes,
});
// 2. Build a set of all referenced UUIDs by scanning:
// - All .scene files
// - All .prefab files
// - All .ts/.js files (for dynamic asset references)
// - All .anim files
// - All .meta files (for sub-asset references)
const referencedUuids = new Set();
// ... scan files, collect UUIDs using collectUuidReferences from assets-advanced.js
// 3. Filter: assets whose UUID is NOT in referencedUuids
const unused = allAssets.filter(asset => !referencedUuids.has(asset.uuid));
return {
totalScanned: allAssets.length,
unusedCount: unused.length,
unused: unused.slice(0, options.limit || 200),
};
}
```
**Unit tests** (test/find-unused-assets.test.js):
- Mock `listAssets` + file scanning → verify correct filtering
- Test exclude directories
- Test asset type filtering
---
## Registration
### `lib/tool-registry.js` changes
**1. Add imports** (top of file):
```js
const { createSceneManagementTools } = require('./tools/scene-management');
const { createScriptTools } = require('./tools/scripts');
const { createPrefab } = require('./prefabs'); // add to existing destructure
```
**2. Spread factories** into the `tools` array:
```js
// After existing cocos-project tools (~line 717)
...createSceneManagementTools({ createSchema, sceneBridge, getRuntimeContext }),
// After existing prefab tools (~line 780)
{
name: 'create_prefab',
// ... inline definition
handler: async (args) => createPrefab(args),
},
// After existing file tools (~line 1315)
...createScriptTools({ createSchema, getRuntimeContext }),
// After existing assets-advanced tools (~line 1023)
// batch_asset_ops and find_unused_assets are appended inside
// createAssetsAdvancedTools via the existing factory
```
**3. Pass `sceneBridge` to `createSceneManagementTools`** — currently only `createSceneEventTools` receives `sceneBridge`. Add it to the scene-management factory call.
### `lib/tools/assets-advanced.js` changes
Append `batch_asset_ops` and `find_unused_assets` tool definitions to the `createAssetsAdvancedTools` return array. Add `getRuntimeContext` to the factory's dependency bag (already received).
### `lib/tools/scene-management.js` factory signature
```js
function createSceneManagementTools({ createSchema, sceneBridge, getRuntimeContext }) {
return [
// Tool 1: create_scene (editor-side, uses getRuntimeContext for projectPath)
// Tool 2: query_scene_state (editor-side, uses tryEditorRequests)
// Tool 3: copy_paste_node (editor-side, uses tryEditorRequests)
// Tool 4: rename_node (scene-side, uses sceneBridge.call)
// Tool 5: reparent_node (scene-side, uses sceneBridge.call)
];
}
module.exports = { createSceneManagementTools };
```
---
## Implementation Order
Execute phases sequentially. Each phase ends with `npm run check && npm test` before proceeding.
```
Phase 1 (tools 1-3) ──▶ npm run check && npm test
Phase 2 (tools 4-5) ──▶ npm run check && npm test
Phase 3 (tool 6) ──▶ npm run check && npm test
Phase 4 (tool 7) ──▶ npm run check && npm test
Phase 5 (tools 8-9) ──▶ npm run check && npm test
Final: docs:generate ──▶ docs:check ──▶ release:check ──▶ pack:dry-run
```
## Verification Checklist (per phase)
- [ ] `npm run check` — syntax check all files
- [ ] `npm test` — unit tests pass
- [ ] `npm run docs:generate` — regenerate TOOLS.md (final phase only)
- [ ] `npm run docs:check` — docs in sync (final phase only)
- [ ] `npm run release:check` — release metadata valid (final phase only)
- [ ] `npm run pack:dry-run` — package builds (final phase only)
## Version Bump
- `package.json` version: `0.4.0``0.5.0`
- `server.json` version(s): update to match
- `CHANGELOG.md`: add `## [0.5.0] - 2026-06-30` entry
- Git tag: `v0.5.0`
## Expected Outcome
| Metric | Before | After |
|---|---|---|
| `full` profile tools | 101 | 110 (+9) |
| `core` profile tools | 37 | 38 (+1: `query_scene_state`) |
| New files | — | `lib/tools/scene-management.js`, `lib/tools/scripts.js` |
| Modified files | — | `scene.js`, `lib/prefabs.js`, `lib/tool-registry.js`, `lib/tools/assets-advanced.js` |
| New scene.js methods | — | `renameNode`, `reparentNode` |
| New unit test files | — | `test/scene-management.test.js`, `test/scripts.test.js`, `test/find-unused-assets.test.js` |
| Gaps filled | Scene creation, node clipboard, node rename/reparent, prefab creation, script creation, batch assets, unused asset scan | All high-frequency gaps covered |