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
+29
View File
@@ -88,6 +88,35 @@ When adding a new tool:
6. Add clear input schema descriptions so AI clients can choose the tool correctly.
7. Test the tool with `tools/list` and `tools/call`.
### Asset Creation via Template Files
When a tool creates a new Cocos asset with a complex serialized format
(scene, prefab, animation clip, material, etc.), **store a template file in
`resources/` and pass its content to `asset-db: create-asset`** rather than
hard-coding JSON strings in source.
Why:
- Cocos serialized formats are version-specific and contain many required
sub-objects (e.g. a scene needs `cc.SceneGlobals``cc.AmbientInfo`,
`cc.ShadowsInfo`, `cc.SkyboxInfo`, `cc.FogInfo`, `cc.OctreeInfo`,
`cc.SkinInfo`, `cc.LightProbeInfo`, `cc.PostSettingsInfo`). Missing or
misnamed classes cause silent import failures or runtime errors.
- A real template file can be created/verified in the Cocos editor and
updated by simply replacing the file.
- `asset-db: create-asset(url, content)` handles `.meta` generation and
import in one step — no need for `fs.writeFileSync` + `refreshAssets`.
Pattern:
1. Create the template file at `resources/template.<ext>` (e.g.
`resources/template.scene`).
2. Add `"resources/"` to the `files` array in `package.json` so it ships
in the npm package.
3. In the tool handler, read the template with `fs.readFileSync`, optionally
replace `_name` fields, then call `create-asset` with the content string.
4. Export a pure helper (e.g. `buildSceneContent`) for unit testing.
## Documentation
The documentation is bilingual:
+338
View File
@@ -0,0 +1,338 @@
# Feature Absorption Plan: From cocos-mcp-server to funplay-cocos-mcp
This document identifies features in `cocos-mcp-server` that are missing from `funplay-cocos-mcp`, evaluates which ones are worth porting as dedicated tools, and provides concrete implementation guidance for each.
## Design Principles
Follow the existing funplay tool-design philosophy (`CONTRIBUTING.md`):
- **`execute_javascript` can already handle it → skip.** No dedicated tool needed.
- **High-frequency, complex parameters, or benefits from structured schema → dedicated tool.**
- **Multiple narrow tools → consolidate into 1-2 tools with an `action` parameter.**
- New tools default to `full` profile; only high-signal tools enter `core`.
## Priority Tiers
| Tier | Rationale | Tool count |
|---|---|---:|
| P1 — Fill real gaps | Capabilities funplay completely lacks and cannot trivially do via `execute_javascript` | 5 new tools |
| P2 — Enhance existing | Strengthen tools funplay already has, or add consolidated versions of cocos-mcp-server's fragmented tools | 3 enhancements |
| P3 — Optional | Useful but lower frequency; implement on demand | 6 items |
---
## P1 — Fill Real Gaps
### 1. `create_scene` — Scene creation
**Gap**: funplay has `open_scene` and `list_scenes` but cannot create a new scene.
**cocos-mcp-server source**: `source/tools/scene-tools.ts:205` — constructs a full `cc.Scene` JSON template (~400 lines) and writes it to disk, then imports via asset-db.
**Implementation**:
- **File**: `lib/tools/scene-management.js` (new)
- **Dependencies**: `sceneBridge` (add `createScene` method to `scene.js`) + `Editor.Message.request('asset-db', 'create-asset', ...)`
- **Schema**:
```json
{
"sceneName": { "type": "string", "description": "Name of the new scene" },
"savePath": { "type": "string", "description": "Asset path, e.g. db://assets/scenes/NewScene.scene" }
}
```
- **Required**: `["sceneName", "savePath"]`
- **Profile**: `full`
- **Notes**: Do NOT port the 400-line JSON template. Instead, use `Editor.Message.request('scene', 'create-scene', ...)` if available in the target Cocos version, or call `director.runScene(new Scene())` in the scene script then `Editor.Message.request('scene', 'save-scene')`. The hand-written JSON approach in cocos-mcp-server is fragile across Cocos versions.
### 2. `copy_paste_node` — Node clipboard
**Gap**: funplay has `create_node`, `delete_node`, `set_node_transform` but no copy/paste/cut.
**cocos-mcp-server source**: `source/tools/scene-advanced-tools.ts:463` — `Editor.Message.request('scene', 'copy-node', uuids)`, `paste-node`, `cut-node`.
**Implementation**:
- **File**: `lib/tools/scene-management.js` (same file as above)
- **Dependencies**: `Editor.Message.request('scene', ...)` — these are editor-side IPC, no scene script needed.
- **Schema** (consolidated with `action`):
```json
{
"action": { "type": "string", "enum": ["copy", "paste", "cut"] },
"uuids": { "type": "array", "items": { "type": "string" }, "description": "Node UUIDs to copy or cut" },
"target": { "type": "string", "description": "Target parent node UUID (paste only)" },
"keepWorldTransform": { "type": "boolean", "default": false }
}
```
- **Required**: `["action"]`
- **Profile**: `full`
- **IPC mapping**:
- `copy` → `Editor.Message.request('scene', 'copy-node', uuids)`
- `paste` → `Editor.Message.request('scene', 'paste-node', { target, uuids, keepWorldTransform })`
- `cut` → `Editor.Message.request('scene', 'cut-node', uuids)`
### 3. `batch_asset_ops` — Batch asset import/delete
**Gap**: funplay has `delete_asset` (single) but no batch import or batch delete.
**cocos-mcp-server source**: `source/tools/asset-advanced-tools.ts:61` — `batch_import_assets` (source dir → target dir + filter + recursive + overwrite), `batch_delete_assets` (URL array).
**Implementation**:
- **File**: `lib/tools/assets-advanced.js` (existing, append)
- **Dependencies**: `Editor.Message.request('asset-db', ...)`
- **Schema** (consolidated with `action`):
```json
{
"action": { "type": "string", "enum": ["import", "delete"] },
"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", "default": false },
"overwrite": { "type": "boolean", "default": false }
}
```
- **Required**: `["action"]`
- **Profile**: `full`
- **IPC mapping**:
- `import` → `Editor.Message.request('asset-db', 'import', targetDirectory, sourceDirectory, { ... })`
- `delete` → `Editor.Message.request('asset-db', 'delete-assets', urls)`
### 4. `query_scene_state` — Scene state queries
**Gap**: funplay cannot check if the scene has unsaved changes, check scene readiness, or soft-reload.
**cocos-mcp-server source**: `source/tools/scene-advanced-tools.ts:283-305` — `query_scene_dirty`, `soft_reload_scene`, `query_scene_ready`.
**Implementation**:
- **File**: `lib/tools/scene-management.js`
- **Dependencies**: `Editor.Message.request('scene', ...)`
- **Schema** (consolidated with `action`):
```json
{
"action": { "type": "string", "enum": ["is_dirty", "is_ready", "soft_reload"] }
}
```
- **Required**: `["action"]`
- **Profile**: `core` — `is_dirty` is a critical signal for AI to decide whether to save before making changes.
- **IPC mapping**:
- `is_dirty` → `Editor.Message.request('scene', 'query-is-dirty')`
- `is_ready` → `Editor.Message.request('scene', 'query-ready')`
- `soft_reload` → `Editor.Message.request('scene', 'soft-reload')`
### 5. `find_unused_assets` — Find unused assets
**Gap**: funplay has `inspect_asset_dependencies` and `validate_asset_dependencies` for individual assets, but no project-wide unused-asset scan.
**cocos-mcp-server source**: `source/tools/asset-advanced-tools.ts:144` — scans a directory, cross-references the dependency graph.
**Implementation**:
- **File**: `lib/tools/assets-advanced.js` (append)
- **Dependencies**: `Editor.Message.request('asset-db', 'query-assets', ...)` + dependency analysis
- **Schema**:
```json
{
"directory": { "type": "string", "default": "db://assets" },
"excludeDirectories": { "type": "array", "items": { "type": "string" }, "default": [] }
}
```
- **Required**: `[]`
- **Profile**: `full`
- **Notes**: Implementation requires querying all assets, building a reverse dependency map, then filtering. Medium complexity.
---
## P2 — Enhance Existing Capabilities
### 6. Enhance `inspect_asset_dependencies` — Add reverse direction
**Gap**: funplay only supports forward dependencies. cocos-mcp-server supports `dependents` (reverse) and `both`.
**cocos-mcp-server source**: `source/tools/asset-advanced-tools.ts:124` — `direction` parameter with enum `["dependents", "dependencies", "both"]`.
**Implementation**:
- **File**: `lib/tools/assets-advanced.js` (modify existing tool)
- **Change**: Add `direction` parameter to the existing `inspect_asset_dependencies` tool schema.
- **IPC**: `Editor.Message.request('asset-db', 'query-dependency', ...)` + reverse traversal for `dependents`.
### 7. `manage_scene_view` — Scene view / Gizmo control (consolidate 20 → 3)
**Gap**: funplay has zero scene-view control tools. cocos-mcp-server has 20 separate tools.
**cocos-mcp-server source**: `source/tools/scene-view-tools.ts` — gizmo tool/pivot/coordinate, 2D/3D mode, grid, icon gizmo, focus camera, align camera/view, get/reset status.
**Implementation**:
- **File**: `lib/tools/scene-view.js` (new)
- **Dependencies**: `Editor.Message.request('scene', ...)`
- **Consolidate into 3 tools**:
**`set_scene_view`**:
```json
{
"action": { "type": "string", "enum": ["gizmo_tool", "gizmo_pivot", "gizmo_coordinate", "view_mode", "grid", "icon_gizmo_3d", "icon_gizmo_size", "reset"] },
"value": { "description": "Value depends on action: gizmo_tool→\"position\"|\"rotation\"|\"scale\"|\"rect\", gizmo_pivot→\"pivot\"|\"center\", gizmo_coordinate→\"local\"|\"global\", view_mode→boolean(is2D), grid→boolean, icon_gizmo_3d→boolean, icon_gizmo_size→number" }
}
```
**`get_scene_view`**:
```json
{
"action": { "type": "string", "enum": ["gizmo_tool", "gizmo_pivot", "gizmo_coordinate", "view_mode", "grid", "icon_gizmo_3d", "icon_gizmo_size", "full_status"] }
}
```
**`focus_on_nodes`**:
```json
{
"uuids": { "type": "array", "items": { "type": "string" }, "description": "Node UUIDs to focus on (null/empty for all)" }
}
```
- **Profile**: `full`
- **IPC mapping** (all `Editor.Message.request('scene', ...)`):
- `change-gizmo-tool`, `change-gizmo-pivot`, `change-gizmo-coordinate`
- `change-view-mode`, `set-grid-visible`, `set-icon-gizmo-3d`, `set-icon-gizmo-size`
- `focus-camera-on-nodes`, `align-camera-with-view`, `align-view-with-node`
- `query-gizmo-tool-name`, `query-gizmo-pivot`, `query-gizmo-coordinate`, etc.
### 8. `manage_reference_image` — Reference images (consolidate 12 → 2)
**Gap**: funplay has zero reference-image tools. cocos-mcp-server has 12.
**cocos-mcp-server source**: `source/tools/reference-image-tools.ts` — add/remove/switch/clear/refresh, set position/scale/opacity/data, query config/current, list.
**Implementation**:
- **File**: `lib/tools/reference-image.js` (new)
- **Dependencies**: `Editor.Message.request('reference-image', ...)`
- **Consolidate into 2 tools**:
**`manage_reference_image`**:
```json
{
"action": { "type": "string", "enum": ["add", "remove", "switch", "clear_all", "refresh", "list", "query_current", "query_config"] },
"paths": { "type": "array", "items": { "type": "string" }, "description": "Image paths (add/remove)" },
"path": { "type": "string", "description": "Single image path (switch)" },
"sceneUUID": { "type": "string", "description": "Optional scene UUID (switch)" }
}
```
**`set_reference_image_property`**:
```json
{
"action": { "type": "string", "enum": ["position", "scale", "opacity", "data"] },
"x": { "type": "number" }, "y": { "type": "number" },
"sx": { "type": "number" }, "sy": { "type": "number" },
"opacity": { "type": "number", "minimum": 0, "maximum": 1 },
"key": { "type": "string", "enum": ["path", "x", "y", "sx", "sy", "opacity"] },
"value": { "description": "Value for the given key (data action)" }
}
```
- **Profile**: `full`
- **IPC mapping**: `Editor.Message.request('reference-image', 'add-image'|'remove-image'|'switch-image'|'set-image-data'|'query-config'|'query-current-image'|'refresh'|'clear-all', ...)`
---
## P3 — Optional Enhancements
Implement on demand. All are `full` profile.
| # | Tool name | Source | Consolidation | Key IPC |
|---|---|---|---|---|
| 9 | `manage_undo` | `scene-advanced-tools.ts:241` | `begin`/`end`/`cancel` → 1 tool + action | `scene: begin-undo-recording` etc. |
| 10 | `manage_asset_crud` | `project-tools.ts:178` | `create`/`copy`/`move`/`save`/`reimport` → 1 tool + action | `asset-db: create-asset` etc. |
| 11 | `manage_preview_server` | `project-tools.ts:156` | `start`/`stop` → 1 tool + action | `preview-server: start`/`stop` |
| 12 | `get_project_settings` | `project-tools.ts:52` | Single tool, category param | `project: query-settings` |
| 13 | `validate_asset_references` | `asset-advanced-tools.ts:110` | Directory-wide scan | `asset-db: query-assets` + analysis |
| 14 | `build_project` | `project-tools.ts:24` | Platform enum param | `builder: build` |
---
## Not Recommended for Absorption
These cocos-mcp-server features are adequately covered by `execute_javascript` and do not warrant dedicated tools:
| Feature | Reason to skip |
|---|---|
| Broadcast message listening (`listen_broadcast`, `stop_listening`) | Low frequency; `execute_javascript` with `Editor.Message.broadcast` is sufficient |
| Preferences panel opening (`open_preferences_settings`) | One-liner: `Editor.Panel.open('preferences')` |
| Console log capture (`get_console_logs`, `clear_console`) | funplay's `get_recent_logs` + `clear_logs` already cover this |
| Texture compression (`compress_textures`) | Very low frequency; better as a project build setting |
| Asset manifest export (`export_asset_manifest`) | Low frequency; `execute_javascript` can iterate asset-db |
| Array element manipulation (`move_array_element`, `remove_array_element`) | `execute_javascript` in scene context is more flexible |
| Node property reset (`reset_node_property`) | `execute_javascript` one-liner via `Editor.Message.request('scene', 'reset-property', ...)` |
| Scene snapshot (`scene_snapshot`) | Advanced internal feature; AI rarely needs it directly |
| `validate_json_params` / `safe_string_value` / `format_mcp_request` | Workarounds for cocos-mcp-server's own JSON parsing bugs; funplay's server handles JSON correctly |
---
## Implementation Steps
### Step 1: Create `lib/tools/scene-management.js`
New file with `createSceneManagementTools({ createSchema, sceneBridge })` exporting:
- `create_scene`
- `copy_paste_node`
- `query_scene_state`
### Step 2: Extend `lib/tools/assets-advanced.js`
Append to existing `createAssetsAdvancedTools`:
- `batch_asset_ops`
- `find_unused_assets`
- Add `direction` parameter to existing `inspect_asset_dependencies`
### Step 3: Create `lib/tools/scene-view.js`
New file with `createSceneViewTools({ createSchema })` exporting:
- `set_scene_view`
- `get_scene_view`
- `focus_on_nodes`
### Step 4: Create `lib/tools/reference-image.js`
New file with `createReferenceImageTools({ createSchema })` exporting:
- `manage_reference_image`
- `set_reference_image_property`
### Step 5: Register in `lib/tool-registry.js`
```javascript
const { createSceneManagementTools } = require('./tools/scene-management');
const { createSceneViewTools } = require('./tools/scene-view');
const { createReferenceImageTools } = require('./tools/reference-image');
// Inside createToolRegistry, in the tools array:
...createSceneManagementTools({ createSchema, sceneBridge }),
...createSceneViewTools({ createSchema }),
...createReferenceImageTools({ createSchema }),
```
### Step 6: Add scene-side methods to `scene.js`
Add `createScene` method to the scene script's `methods` export for scene creation support.
### Step 7: Run verification chain
```bash
npm run check # syntax check all files
npm test # run unit tests
npm run docs:generate # regenerate docs/TOOLS.md
npm run docs:check # verify docs in sync
npm run release:check # release metadata
```
### Step 8: Update documentation
- `README.md` — add new tool categories to the Built-in Tools table
- `README_CN.md` — mirror changes
- `CHANGELOG.md` — add entry under next version
---
## Expected Outcome
| Metric | Before | After |
|---|---|---|
| `full` profile tools | 101 | ~115 (net +14 after consolidation) |
| `core` profile tools | 37 | 38 (+`query_scene_state`) |
| Blank areas | Scene creation, view control, reference images, clipboard, batch assets | All filled |
| New files | — | `lib/tools/scene-management.js`, `lib/tools/scene-view.js`, `lib/tools/reference-image.js` |
| Modified files | — | `lib/tools/assets-advanced.js`, `lib/tool-registry.js`, `scene.js` |
+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 |
File diff suppressed because it is too large Load Diff
+13 -4
View File
@@ -2,18 +2,18 @@
<!-- This file is generated by `npm run docs:generate`. Do not edit by hand. -->
Generated from `lib/tool-registry.js`. The default `core` profile exposes 37 tools; the `full` profile exposes 101 tools.
Generated from `lib/tool-registry.js`. The default `core` profile exposes 38 tools; the `full` profile exposes 110 tools.
## Profile Summary
| Profile | Tool Count | Purpose |
|---|---:|---|
| `core` | 37 | Focused default surface for common editor automation. |
| `full` | 101 | All built-in tools, including destructive and low-level helpers. |
| `core` | 38 | Focused default surface for common editor automation. |
| `full` | 110 | All built-in tools, including destructive and low-level helpers. |
## Core Tools
`capture_editor_screenshot`, `capture_preview_screenshot`, `capture_scene_screenshot`, `check_for_updates`, `clear_logs`, `execute_editor_script`, `execute_javascript`, `execute_scene_script`, `get_build_status`, `get_editor_state`, `get_hierarchy`, `get_performance_snapshot`, `get_project_info`, `get_recent_logs`, `get_runtime_state`, `get_scene_info`, `get_script_diagnostic_context`, `get_selection`, `get_tool_catalog`, `inspect_asset`, `inspect_asset_dependencies`, `inspect_prefab`, `inspect_prefab_instance`, `list_assets`, `list_editor_windows`, `list_project_instructions`, `list_scenes`, `open_asset`, `open_scene`, `read_project_instruction`, `run_script_diagnostics`, `search_project_logs`, `select_asset`, `set_selection`, `validate_asset_dependencies`, `validate_prefab_references`, `validate_scene`
`capture_editor_screenshot`, `capture_preview_screenshot`, `capture_scene_screenshot`, `check_for_updates`, `clear_logs`, `execute_editor_script`, `execute_javascript`, `execute_scene_script`, `get_build_status`, `get_editor_state`, `get_hierarchy`, `get_performance_snapshot`, `get_project_info`, `get_recent_logs`, `get_runtime_state`, `get_scene_info`, `get_script_diagnostic_context`, `get_selection`, `get_tool_catalog`, `inspect_asset`, `inspect_asset_dependencies`, `inspect_prefab`, `inspect_prefab_instance`, `list_assets`, `list_editor_windows`, `list_project_instructions`, `list_scenes`, `open_asset`, `open_scene`, `query_scene_state`, `read_project_instruction`, `run_script_diagnostics`, `search_project_logs`, `select_asset`, `set_selection`, `validate_asset_dependencies`, `validate_prefab_references`, `validate_scene`
## Tools By Category
@@ -30,7 +30,10 @@ Generated from `lib/tool-registry.js`. The default `core` profile exposes 37 too
| Tool | Profiles | Access | Description |
|---|---|---|---|
| `batch_asset_ops` | `full` | mutating | [core] Batch import or delete assets in the Cocos asset database. |
| `create_scene` | `full` | stateful | [core] Create a new scene asset in the Cocos project and optionally open it. |
| `delete_asset` | `full` | mutating | Delete an asset from asset-db by uuid, db url, or path. |
| `find_unused_assets` | `full` | read-only | [core] Find assets not referenced by any scene, prefab, or animation in the project. |
| `inspect_asset` | `core`, `full` | read-only | [specialist] Inspect asset-db info, metadata, and serialized asset data by uuid or path. Prefer this when you need a precise structured asset read. |
| `inspect_asset_dependencies` | `core`, `full` | read-only | [specialist] Inspect UUID-style dependencies referenced by a serialized Cocos asset. |
| `list_assets` | `core`, `full` | read-only | [specialist] Query project assets from asset-db by pattern or asset type. Prefer this when you need exact asset discovery; otherwise use execute_javascript for broader automation. |
@@ -146,6 +149,7 @@ Generated from `lib/tool-registry.js`. The default `core` profile exposes 37 too
| Tool | Profiles | Access | Description |
|---|---|---|---|
| `create_script` | `full` | stateful | [core] Create a new TypeScript component script with a standard Cocos template. |
| `get_performance_snapshot` | `core`, `full` | read-only | [specialist] Return scene scale and runtime performance-oriented counters such as node/component counts, UI counts, depth, memory, and warnings. |
| `list_editor_windows` | `core`, `full` | read-only | [specialist] List available Electron windows so screenshots or input-targeting can choose the correct window. Use this when window targeting is the explicit problem. |
@@ -154,6 +158,7 @@ Generated from `lib/tool-registry.js`. The default `core` profile exposes 37 too
| Tool | Profiles | Access | Description |
|---|---|---|---|
| `apply_prefab_instance` | `full` | stateful | [core] Apply a scene prefab instance back to its associated prefab asset using the Cocos editor scene apply-prefab message. |
| `create_prefab` | `full` | stateful | [core] Create a prefab asset from a scene node. |
| `create_prefab_instance` | `full` | stateful | [core] Create a linked prefab instance in the editor hierarchy using Cocos scene create-node when available. |
| `duplicate_prefab` | `full` | stateful | [core] Create a new prefab asset by duplicating an existing prefab file without copying its .meta UUID. |
| `edit_prefab_json` | `full` | stateful | [core] Edit a prefab JSON file by JSON path assignment or literal search/replace, then validate references. |
@@ -191,6 +196,7 @@ Generated from `lib/tool-registry.js`. The default `core` profile exposes 37 too
| Tool | Profiles | Access | Description |
|---|---|---|---|
| `copy_paste_node` | `full` | stateful | [core] Copy, cut, or paste scene nodes via the Cocos editor clipboard. |
| `create_node` | `full` | stateful | Create a new node under the active scene or a specified parent path. |
| `delete_node` | `full` | mutating | Delete a node by path, uuid, or name. |
| `execute_scene_script` | `core`, `full` | mutating | [compat] Execute JavaScript in the active Cocos scene context. Prefer execute_javascript with context="scene" as the main unified tool; use this when you specifically want the scene-only compatibility entrypoint. |
@@ -198,6 +204,9 @@ Generated from `lib/tool-registry.js`. The default `core` profile exposes 37 too
| `get_hierarchy` | `core`, `full` | read-only | [specialist] Return a structured hierarchy tree from the active scene or a specific node path. Prefer execute_javascript for broader reasoning or repair; use this when you want a predictable hierarchy snapshot. |
| `get_scene_info` | `core`, `full` | read-only | [specialist] Return a structured summary of the active Cocos scene. Prefer execute_javascript for multi-step inspection or mutation; use this when you specifically want a compact scene snapshot. |
| `inspect_node` | `full` | read-only | [core] Inspect a specific node by path, uuid, or name. |
| `query_scene_state` | `core`, `full` | stateful | [specialist] Query scene state: dirty (unsaved changes), ready, or soft-reload. |
| `rename_node` | `full` | stateful | [core] Rename a scene node. |
| `reparent_node` | `full` | stateful | [core] Move a node to a new parent in the scene hierarchy. |
| `set_node_transform` | `full` | mutating | Update node position, rotation, scale, or active state. |
### Screenshots
+722
View File
@@ -0,0 +1,722 @@
# Cocos Creator 3.8.8 编辑器 Message 列表
> 数据来源: Cocos Creator 3.8.8 编辑器 app.asar 内置扩展包
> 提取时间: 2026-06-30
> i18n 已解析为中文文本
## 目录
1. [animation-graph](#animation-graph) (10 messages)
2. [animator](#animator) (38 messages)
3. [asset-db](#asset-db) (66 messages)
4. [builder](#builder) (59 messages)
5. [engine](#engine) (12 messages)
6. [information](#information) (4 messages)
7. [menu](#menu) (3 messages)
8. [messages](#messages) (11 messages)
9. [metrics](#metrics) (11 messages)
10. [placeholder](#placeholder) (1 messages)
11. [preferences](#preferences) (10 messages)
12. [preview](#preview) (22 messages)
13. [program](#program) (5 messages)
14. [programming](#programming) (12 messages)
15. [project](#project) (19 messages)
16. [scene](#scene) (218 messages)
17. [server](#server) (7 messages)
18. [shortcuts](#shortcuts) (8 messages)
19. [tester](#tester) (4 messages)
20. [utils](#utils) (2 messages)
21. [window](#window) (4 messages)
22. [cocos-service](#cocos-service) (16 messages)
23. [im-plugin](#im-plugin) (11 messages)
**总计: 553 条 message**
---
## animation-graph
| Message | Methods | Public | Description | Doc | Example |
|---------|---------|--------|-------------|-----|---------|
| open | open | No | | | |
| dialog-warn | dialogWarn | No | | | |
| apply | default.apply | No | | | |
| unselect | default.unselect | No | | | |
| delete | default.delete | No | | | |
| copy | default.copy | No | | | |
| duplicate | default.duplicate | No | | | |
| paste | default.paste | No | | | |
| scene:ready | profileBindWatchWhenSceneReady, default.refresh | No | | | |
| animation-graph:changed | default.refresh | No | | | |
## animator
| Message | Methods | Public | Description | Doc | Example |
|---------|---------|--------|-------------|-----|---------|
| open | open | No | | | |
| scene:ready | default.scene:ready | No | | | |
| scene:close | default.scene:close | No | | | |
| scene:change-node | default.scene:change-node | No | | | |
| scene:animation-start | default.scene:animation-start | No | | | |
| scene:animation-end | default.scene:animation-end | No | | | |
| scene:animation-change | default.scene:animation-change | No | | | |
| scene:animation-state-change | default.scene:animation-state-change | No | | | |
| scene:change-mode | default.scene:change-mode | No | | | |
| scene:animation-clip-change | default.scene:animation-clip-change | No | | | |
| selection:activated | default.selection:activated | No | | | |
| asset-db:asset-change | default.asset-db:asset-change | No | | | |
| asset-db:asset-delete | default.asset-db:asset-delete | No | | | |
| inspector-drop-animation | dropClipToNode | No | | | |
| change-debug-mode | default.change-debug-mode | No | | | |
| copy | default.copy | No | | | |
| paste | default.paste | No | | | |
| select-all | default.selectAll | No | | | |
| delete | default.deleteSelected | No | | | |
| create | default.createKey | No | | | |
| focus | default.showAllKeys | No | | | |
| show-selected-keys | default.showSelectedKeys | No | | | |
| next-step | default.nextStep | No | | | |
| prev-step | default.prevStep | No | | | |
| jump-to-next-key | default.jumpToNextKey | No | | | |
| jump-to-prev-key | default.jumpToPrevKey | No | | | |
| jump-to-first-frame | default.jumpFirstFrame | No | | | |
| jump-to-last-frame | default.jumpLastFrame | No | | | |
| play-or-pause | default.playOrPause | No | | | |
| stop | default.stop | No | | | |
| clear-selected | default.clearSelect | No | | | |
| switch-animation-mode | default.changeRecordState | No | | | |
| open-docs | openDocs | No | | | |
| query-last-clip-cache | queryLatestClipCache | No | | | |
| save-clip-cache | saveClipCacheToFile | No | | | |
| update-cache-config | updateCacheConfig | No | | | |
| enable-embedded-player | default.enableEmbeddedPlayer | No | | | |
| enable-auxiliary-curve | default.enableAuxiliaryCurve | No | | | |
## asset-db
| Message | Methods | Public | Description | Doc | Example |
|---------|---------|--------|-------------|-----|---------|
| asset-db:ready | ready | Yes | 资源数据库准备就绪时的广播 | | Editor.Message.broadcast('asset-db:ready'); |
| asset-db:close | close | Yes | 资源数据库关闭时的广播 | | Editor.Message.broadcast('asset-db:close'); |
| asset-db:asset-add | | Yes | 当资源数据库准备就绪后,再新增资源时的广播 | | Editor.Message.broadcast('asset-db:asset-add'); |
| asset-db:asset-change | | Yes | 当一个资源被修改时的广播 | | Editor.Message.broadcast('asset-db:asset-change'); |
| asset-db:asset-delete | | Yes | 当一个资源被删除时的广播 | | Editor.Message.broadcast('asset-db:asset-delete'); |
| project:change-high-quality | projectChangeHighQuality | No | | | |
| query-ready | queryReady | Yes | 检查资源数据库是否启动完毕 | @returns {boolean} 是否准备就绪 | await Editor.Message.request('asset-db', 'query-ready'); |
| create-asset | createAsset | Yes | 新建一个资源 | - url {string} 资源的 URL,例如 db://assets/abc.json<br>- content {string \| null} 写入文件的 string,为 null 则新建文件夹<br>- option {AssetOperationOption}<br> - option.overwrite {boolean} 是否强制覆盖,默认 false<br> - option.rename {boolean} 冲突是否自动更名,默认 false<br><br>@returns {AssetInfo} 返回一个资源信息 | await Editor.Message.request('asset-db', 'create-asset', url, content); |
| new-asset | newAsset | No | 新建一个资源 | | |
| import-asset | importAsset | Yes | 将一个文件或文件夹导入到资源数据库内 | - source {string} 本地的文件绝对地址<br>- target {string} 导入到数据库的 url 地址<br>- option {AssetOperationOption}<br> - option.overwrite {boolean} 是否强制覆盖,默认 false<br> - option.rename {boolean} 冲突是否自动更名,默认 false<br><br>@returns {AssetInfo} 返回一个资源信息 | await Editor.Message.request('asset-db', 'import-asset', path, url, { overwrite: true, rename: true }); |
| copy-asset | copyAsset | Yes | 复制某个资源 | - source {string} 源资源的 URL 路径,例如 db://assets/abc.json<br>- target {string} 复制到的目标位置 URL<br>- option {AssetOperationOption}<br> - option.overwrite {boolean} 是否强制覆盖,默认 false<br> - option.rename {boolean} 冲突是否自动更名,默认 false<br><br>@returns {AssetInfo} 返回一个资源信息 | await Editor.Message.request('asset-db', 'copy-asset', sourceUrl, targetUrl, { overwrite: true, rename: true }); |
| move-asset | moveAsset | Yes | 将一个资源移动到某个地方 | - source {string} 需要移动的源资源 URL 路径,例如 db://assets/abc.json<br>- target {string} 移动到的目标位置 URL<br>- option {AssetOperationOption}<br> - option.overwrite {boolean} 是否强制覆盖,默认 false<br> - option.rename {boolean} 冲突是否自动更名,默认 false<br><br>@returns {AssetInfo} 返回一个资源信息 | await Editor.Message.request('asset-db', 'move-asset', sourceUrl, targetUrl); |
| rename-asset | renameAsset | No | 重命名指定资源 | | |
| delete-asset | deleteAsset | Yes | 删除一个资源 | - url {string} 资源的 URL<br><br>@returns {AssetInfo} 返回一个资源信息 | await Editor.Message.request('asset-db', 'delete-asset', pathOrUrlOrUUID); |
| open-asset | openAsset | Yes | 尝试使用记录的打开程序打开一个资源 | - urlOrUUID {string} 尝试打开的资源 URL 或者 UUID | await Editor.Message.request('asset-db', 'open-asset', urlOrUUID); |
| save-asset | saveAsset | Yes | 保存资源 | - urlOrUUID {string} 资源的 URL 或者 UUID<br>- content {string \| Buffer} 资源的内容字符串,如果是 typeArray,请使用 Buffer.from 转换。<br><br>@returns {AssetInfo} 返回一个资源信息 | await Editor.Message.request('asset-db', 'save-asset', urlOrUUID, content); |
| save-asset-meta | saveAssetMeta | Yes | 保存资源的 meta 信息 | - urlOrUUID {string} 资源的 URL 或者 UUID<br>- content {string} 资源 meta 序列化后的内容字符串<br><br>@returns {AssetInfo} 返回一个资源信息 | await Editor.Message.request('asset-db', 'save-asset-meta', urlOrUUID, content); |
| reimport-asset | reimportAsset | Yes | 重新导入资源 | - urlOrUUID {string} 资源的 URL 或者 UUID | await Editor.Message.request('asset-db', 'reimport-asset', urlOrUUID); |
| refresh-asset | refreshAsset | Yes | 刷新一个资源所在的 url 位置,删除资源会被销毁,新增资源会导入 | - urlOrUUID {string} 资源的 URL 或者 UUID | await Editor.Message.request('asset-db', 'refresh-asset', urlOrUUID); |
| refresh-all-effect | refreshAllEffect | No | | | |
| query-path | queryPath | Yes | 查询一个资源的路径 | - urlOrUUID {string} 资源的 URL 或者 UUID<br><br>@returns {string} 返回一个资源的磁盘绝对路径 | await Editor.Message.request('asset-db', 'query-path', urlOrUUID); |
| query-url | queryUrl | Yes | 查询一个资源的 URL | - uuid {string} 资源的 UUID<br><br>@returns {string} 返回一个资源的 url | await Editor.Message.request('asset-db', 'query-url', uuidOrPath); |
| query-uuid | queryUUID | Yes | 查询一个资源的 UUID | - url {string} 资源的 URL<br><br>@returns {string} 返回一个资源的 uuid | await Editor.Message.request('asset-db', 'query-uuid', urlOrUUID); |
| query-assets | queryAssets | Yes | 根据条件查询资源数组 | - pattern? {string} 路径匹配模式,glob 格式 (db://**)<br>- ccType? {string} 资源类型,例如 cc.Texture2D<br>- importer? {string} 资源导入器类型,例如 texture<br><br>@returns {AssetInfo[]} 返回资源数组 | await Editor.Message.request('asset-db', 'query-assets', { ccType: 'cc.Script' }); |
| query-asset-info | queryAssetInfo | Yes | 查询一个资源的基本信息 | - urlOrUUID {string} 资源的 url 地址或者 uuid<br><br>@returns {AssetInfo} 返回一个资源信息 | await Editor.Message.request('asset-db', 'query-asset-info', urlOrUUIDOrPath); |
| query-missing-asset-info | queryMissingAssetInfo | No | | | |
| query-asset-meta | queryAssetMeta | Yes | 查询一个资源的 META 信息 | - urlOrUUID {string} 资源的 url 地址或者 uuid<br><br>@returns {AssetMeta} 返回一个资源 meta 信息 | await Editor.Message.request('asset-db', 'query-asset-meta', urlOrUUID); |
| query-asset-dependinces | queryAssetDependencies | No | | | |
| query-asset-used | queryAssetUsed | No | | | |
| query-asset-dependencies | queryAssetDependencies | Yes | 查询一个资源依赖的资源或脚本 uuid 数组 | - urlOrUUID {string} 资源的 url 地址或者 uuid<br>- type? {QueryAssetType} 查询的资源类型,默认 asset, 可选值:asset, script, all<br><br>@returns {string[]} 返回一个资源依赖的资源或脚本 uuid 数组 | await Editor.Message.request('asset-db', 'query-asset-dependencies', urlOrUUID, type); |
| query-asset-users | queryAssetUsers | Yes | 查询一个资源被哪些资源或脚本直接使用到 | - urlOrUUID {string} 资源的 url 地址或者 uuid<br>- type? {QueryAssetType} 查询的资源类型,默认 asset, 可选值:asset, script, all<br><br>@returns {string[]} 返回一个资源被哪些资源或脚本直接使用到 | await Editor.Message.request('asset-db', 'query-asset-users', urlOrUUID, type); |
| query-asset-data | queryAssetData | No | | | |
| generate-available-url | generateAvailableUrl | Yes | 根据传入的 url 生成一个可用的新 url | - url {string} 资源的 url<br><br>@returns {string} 返回一个新的或者和传入参数一样的 url | await Editor.Message.request('asset-db', 'generate-available-url', url); |
| query-asset-mtime | queryAssetMtime | No | | | |
| refresh | refresh | No | | | |
| is-busy | isBusy | No | | | |
| pause | pause | No | | | |
| resume | resume | No | | | |
| open-devtools | open-devtools | No | | | |
| query-db-info | query-db-info | No | | | |
| query-db-infos | query-db-infos | No | | | |
| query-db-list | query-db-list | No | | | |
| create-asset-dialog | create-asset-dialog | No | | | |
| create-asset-template | create-asset-template | No | | | |
| init-asset | init-asset | No | | | |
| query-all-importer | query-all-importer | No | | | |
| query-all-asset-types | query-all-asset-types | No | | | |
| execute-script | executeScript | No | | | |
| execute-custom-operation | executeCustomOperation | No | | | |
| start-db | startDatabase | No | | | |
| stop-db | stopDatabase | No | | | |
| notice-reload-editor | noticeReloadEditor | No | | | |
| notice-config-changed | noticeConfigChanged | No | | | |
| update-config | updateConfig | No | | | |
| refresh-all-database | refreshAllDatabase | No | | | |
| refresh-default-user-data-config | refreshDefaultUserDataConfig | No | | | |
| update-default-user-data | updateDefaultUserData | No | | | |
| query-create-list | queryCreateList | No | | | |
| query-icon-config-map | queryIconConfigMap | No | | | |
| query-asset-config-map | queryAssetConfigMap | No | | | |
| query-create-menu-list | queryCreateMenuList | No | | | |
| query-asset-userData-config | queryAssetUserDataConfig | No | | | |
| query-asset-thumbnail | queryAssetThumbnail | No | | | |
| batch-message-handler | batchMessageHandler | No | | | |
| show-asset-template-dir | showAssetTemplateDir | No | | | |
| query-global-internal-library | query-global-internal-library | No | | | |
## builder
| Message | Methods | Public | Description | Doc | Example |
|---------|---------|--------|-------------|-----|---------|
| open | open | Yes | 打开构建面板 | @returns {null} | await Editor.Message.request('builder', 'open'); |
| open-bundle | openBundle | No | | | |
| change-build-bundle | build-bundle.changeBuildBundle | No | | | |
| open-page | default.open-page | No | | | |
| query-worker-ready | query-worker-ready | Yes | 查询构建进程是否启动 | @returns {boolean} - 构建进程是否准备好 | await Editor.Message.request('builder', 'query-worker-ready'); |
| execute-build-stage | execute-build-stage | No | | | |
| open-platform-debug-tools | open-platform-debug-tools | No | | | |
| create-build-plugin-template | createBuildPluginTemplate | No | | | |
| create-build-template | create-build-template | No | | | |
| create-application-template | createApplicationTemplate | No | | | |
| open-devtools | openWorkerDevTool | No | | | |
| generate-preview-setting | generate-preview-setting | No | | | |
| query-tasks-info | query-tasks-info | No | | | |
| add-task | add-task | No | | | |
| select-all-task | default.select-all-task | No | | | |
| clear-selected-task | default.clear-selected-task | No | | | |
| add-bundle-task | add-bundle-task | No | | | |
| recompile-task | recompile-task | No | | | |
| remove-task | remove-task | No | | | |
| break-task | break-task | No | | | |
| query-task | query-task | No | | | |
| update-task | update-task | No | | | |
| save-task | save-task | No | | | |
| preview-pac | preview-pac | No | | | |
| query-atlas-files | query-atlas-files | No | | | |
| command-build | command-build | No | | | |
| asset-db:ready | asset-db:ready, default.asset-db:ready | No | | | |
| asset-db:close | asset-db:close | No | | | |
| asset-db:asset-delete | default.asset-db:asset-delete, build-bundle.asset-db:asset-delete | No | | | |
| asset-db:asset-add | default.asset-db:asset-add, build-bundle.asset-db:asset-add | No | | | |
| asset-db:asset-change | default.asset-db:asset-change, build-bundle.asset-db:asset-change | No | | | |
| request-to-build-worker | request-to-build-worker | No | | | |
| execute-hook-task | execute-hook-task | No | | | |
| preferences-changed | preferences-changed | No | | | |
| query-compress-config | query-compress-config | No | | | |
| query-platform-config | query-platform-config | No | | | |
| query-bundle-config | query-bundle-config | No | | | |
| migrate-options | migrateOptions | No | | | |
| builder:task-changed | default.builder:task-changed, build-bundle.builder:task-changed | No | | | |
| builder:task-add | build-bundle.builder:task-add | No | | | |
| builder:task-delete | default.builder:task-delete, build-bundle.builder:task-delete | No | | | |
| builder:bundle-task-changed | build-bundle.bundle-task:changed | No | | | |
| change-debug-mode | default.change-debug-mode | No | | | |
| build-worker:ready | default.onBuildWorkerReady, build-bundle.onBuildWorkerReady | Yes | 构建进程启动 | | Editor.Message.broadcast('build-worker:ready'); |
| build-worker:closed | default.onBuildWorkerClosed, build-bundle.onBuildWorkerClosed | Yes | 构建进程关闭 | | Editor.Message.broadcast('build-worker:closed'); |
| build-by-shortcut | default.buildByShortcut | No | | | |
| console:update-log-level | console:update-log-level | No | | | |
| register-package | registerPackage | No | | | |
| unregister-package | unRegisterPackage | No | | | |
| programming:pack-build-end | programming:pack-build-end | No | | | |
| open-docs | openDocs | No | | | |
| check-and-complete-options | check-and-complete-options | No | | | |
| open-panel-devtools | openPanelDevTools | No | | | |
| open-worker-devtools | openWorkerDevTool | No | | | |
| clear-all-cache | clearAllCache | No | | | |
| clear-assets-cache | clearProjectAssetsCache | No | | | |
| clear-engine-cache | clearEngineCache | No | | | |
| copy-build-notice | copy-build-notice | No | | | |
| export-bundle-config | build-bundle.exportBundleBuildConfig | No | | | |
## engine
| Message | Methods | Public | Description | Doc | Example |
|---------|---------|--------|-------------|-----|---------|
| rebuild | rebuild | No | | | |
| relaunch | relaunch | No | | | |
| import-engine-error | importEngineError | No | | | |
| pipeline-config-change | onPipelineConfigChange | No | | | |
| query-info | query-info | No | | | |
| query-engine-info | query-engine-info | No | | | |
| query-modules-config | query-modules-config | No | | | |
| change-custom-engine-config | changeCustomEngineConfig | No | | | |
| engine:engine-modules-global-config-changed | onEngineModulesChanged | No | | | |
| engine-custom-macro-changed | onCustomMacroChanged | No | | | |
| query-engine-modules-profile | queryEngineModulesProfile | No | | | |
| filter-engine-modules | filterEngineModules | No | | | |
## information
| Message | Methods | Public | Description | Doc | Example |
|---------|---------|--------|-------------|-----|---------|
| query-information | queryInformation | No | | | |
| open-information-dialog | openInformationDialog | No | | | |
| has-dialog | hasDialog | No | | | |
| close-dialog | closeDialog | No | | | |
## menu
| Message | Methods | Public | Description | Doc | Example |
|---------|---------|--------|-------------|-----|---------|
| engine:engine-modules-global-config-changed | engineModuleChanged | No | | | |
| edit-mode:enter | modeChanged | No | | | |
| shortcuts:change | onShortcutChange | No | | | |
## messages
| Message | Methods | Public | Description | Doc | Example |
|---------|---------|--------|-------------|-----|---------|
| start-record | addListener | No | | | |
| stop-record | removeListener | No | | | |
| start-auto-save | startAutoSave | No | | | |
| stop-auto-save | stopAutoSave | No | | | |
| open | open | No | | | |
| open-debug | openDebug | No | | | |
| broadcast | debug.broadcast | No | | | |
| request | debug.request | No | | | |
| send | debug.send | No | | | |
| reply | debug.reply | No | | | |
| query-message-state | queryMessageState | No | | | |
## metrics
| Message | Methods | Public | Description | Doc | Example |
|---------|---------|--------|-------------|-----|---------|
| metrics:init | init | No | | | |
| metrics:trackEvent | trackEvent | No | | | |
| metrics:trackException | trackException | No | | | |
| metrics:trackProcessMemory | trackProcessMemory | No | | | |
| metrics:trackTimeStart | trackTimeStart | No | | | |
| metrics:trackTimeEnd | trackTimeEnd | No | | | |
| metrics:_trackEventWithTimer | _trackEventWithTimer | No | | | |
| metrics:_sendEventGroup | _sendEventGroup | No | | | |
| metrics:_trackCrashEvent | _trackCrashEvent | No | | | |
| query-google-v4-data | query-google-v4-data | No | | | |
| query-google-metrics-v4-html | query-google-metrics-v4-html | No | | | |
## placeholder
| Message | Methods | Public | Description | Doc | Example |
|---------|---------|--------|-------------|-----|---------|
| install-extension | installExtension | No | | | |
## preferences
| Message | Methods | Public | Description | Doc | Example |
|---------|---------|--------|-------------|-----|---------|
| open-settings | openSettings | Yes | 打开偏好设置面板 | - tab {string} 需要打开的选项卡(功能插件的名称)<br>- ...args: {any[]} 打开选项卡带的其他参数<br><br>@returns {null} | await Editor.Message.request('preferences', 'open-settings'); |
| change-settings-tab | settings.changeTab | No | | | |
| refresh-settings-tab | settings.refreshTab | No | | | |
| query-settings-tab | settings.queryTab | No | | | |
| query-preferences-configs | queryPreferencesConfigs | No | | | |
| query-configs-from-path | queryConfigsFromPath | No | | | |
| query-config | queryConfig | Yes | 查询偏好配置 | - name {string} 插件或分类名<br>- path? {string} 配置路径<br>- type? {'default' \| 'global' \| 'local'} 配置类型<br><br>@returns {any} 返回配置数据 | await Editor.Message.request('preferences', 'query-config', 'preview', 'general', 'global'); |
| set-config | setConfig | Yes | 设置偏好配置 | - name {string} 插件名<br>- path {string} 配置路径<br>- value {any} 配置数据<br>- type? {'default' \| 'global' \| 'local'} 配置类型<br><br>@returns {boolean} 是否设置成功 | await Editor.Message.request('preferences', 'set-config', 'preview', 'general.auto_refresh', false, 'global'); |
| import-config | importConfig | No | | | |
| export-config | exportConfig | No | | | |
## preview
| Message | Methods | Public | Description | Doc | Example |
|---------|---------|--------|-------------|-----|---------|
| open | open | No | | | |
| ready | ready | No | | | |
| generate-settings | generateSettings | No | | | |
| query-preview-url | queryPreviewUrl | No | | | |
| query-connect-num | queryConnectNum | No | | | |
| scene:save | currentSceneSave | No | | | |
| programming:compiled | programming:compiled | No | | | |
| programming:compile-start | programming:compile-start | No | | | |
| open-terminal | open-terminal | No | | | |
| restart-simulator | restart-simulator | No | | | |
| reload-terminal | reload-terminal | No | | | |
| get-preview-ip | get-preview-ip | No | | | |
| set-preview-ip | set-preview-ip | No | | | |
| create-template | create-template | No | | | |
| change-platform | change-platform | No | | | |
| programming:pack-build-end | on-pack-build-end | No | | | |
| build-simulator-engine-ts | buildSimulatorEngineTS | No | | | |
| preview-scene-in-browser | previewSceneInBrowser | No | | | |
| write-setting-file | writeSettingFile | No | | | |
| asset-db:asset-change | asset-db:asset-change | No | | | |
| build-worker:ready | build-worker:ready | No | | | |
| build-worker:closed | build-worker:closed | No | | | |
## program
| Message | Methods | Public | Description | Doc | Example |
|---------|---------|--------|-------------|-----|---------|
| query-program-info | queryProgramInfo | No | | | |
| query-program-config | queryProgramConfig | No | | | |
| open-program | openProgram | No | | | |
| execute-program | executeProgram | No | | | |
| open-url | openUrl | No | | | |
## programming
| Message | Methods | Public | Description | Doc | Example |
|---------|---------|--------|-------------|-----|---------|
| packer-driver/start | packer-driver/start | No | | | |
| packer-driver/get-loader-context | packer-driver/get-loader-context | No | | | |
| packer-driver/ready | packer-driver/ready | No | | | |
| packer-driver/update-auto-update-import-config | packer-driver/update-auto-update-import-config | No | | | |
| packer-driver/query-script-deps | packer-driver/query-script-deps | No | | | |
| packer-driver/query-script-users | packer-driver/query-script-users | No | | | |
| packer-driver/query-cc-editor-module-map | packer-driver/query-cc-editor-module-map | No | | | |
| query-shared-settings | query-shared-settings | No | | | |
| clear-code-cache | clear-code-cache | No | | | |
| open-dev-tools | open-dev-tools | No | | | |
| engine:engine-custom-macro-changed | custom-macro-changed | No | | | |
| query-sorted-plugins | querySortedPlugins | No | | | |
## project
| Message | Methods | Public | Description | Doc | Example |
|---------|---------|--------|-------------|-----|---------|
| asset-db:ready | settings.assetDbReady | No | | | |
| open-settings | openSettings | Yes | 打开项目设置面板 | - name {string} 要打开的选项卡所属的插件注册名称<br>- tab {string} 在注册功能时使用的键<br>- ...args: {any[]} 打开选项卡时附带的其他参数<br><br>@returns {null} | await Editor.Message.request('project', 'open-settings'); |
| open-joint | openJoint | No | | | |
| change-settings-tab | settings.changeTab | No | | | |
| refresh-settings-tab | settings.refreshTab | No | | | |
| query-settings-tab | settings.queryTab | No | | | |
| change-script-config | changeScriptConfig | No | | | |
| change-design-resolution | changeDesignResolution | No | | | |
| query-design-resolution | queryDesignResolution | No | | | |
| change-custom-layer | changeCustomLayer | No | | | |
| change-sorting-layer | changeSortingLayer | No | | | |
| change-hight-quality | changeHighQuality | No | | | |
| calc-joint-layout | calcJointLayouts | No | | | |
| query-project-configs | queryProjectConfigs | No | | | |
| query-configs-from-path | queryConfigsFromPath | No | | | |
| query-config | queryConfig | Yes | 查询项目配置 | - name {string} 插件名<br>- path? {string} 配置路径<br>- type? {'default' \| 'project'} 配置类型<br><br>@returns {any} 返回配置数据 | await Editor.Message.request('project', 'query-config', 'engine', 'modules'); |
| set-config | setConfig | Yes | 设置项目配置 | - name {string} 插件名<br>- path {string} 配置路径<br>- value {any} 配置数据<br><br>@returns {boolean} 设置成功与否 | await Editor.Message.request('project', 'set-config', 'project', 'general.downloadMaxConcurrency', 10); |
| import-config | importConfig | No | | | |
| export-config | exportConfig | No | | | |
## scene
| Message | Methods | Public | Description | Doc | Example |
|---------|---------|--------|-------------|-----|---------|
| query-scene-bounds | default.query-scene-bounds | No | | | |
| change-native-config | default.change-native-config | No | | | |
| debug-view | default.change-debug-view-option | No | | | |
| quit-editor | default.quit-editor | No | | | |
| scene:preview-stop | preview.onEditorPreviewStop | No | | | |
| open | open | No | | | |
| full-screen | default.fullScreen | No | | | |
| i18n:change | default.i18n:change | No | | | |
| scene:ready | onSceneReady, default.scene:ready | Yes | 场景打开通知 | - uuid {string} uuid of scene | Editor.Message.broadcast('scene:ready', assetUuid); |
| scene:close | default.scene:close, onSceneClosed | Yes | 场景关闭通知 | | Editor.Message.broadcast('scene:close'); |
| asset-db:ready | default.asset-db:ready | No | | | |
| asset-db:close | default.asset-db:close | No | | | |
| selection:hover | default.selection:hover | No | | | |
| selection:select | default.selection:select | No | | | |
| selection:unselect | default.selection:unselect | No | | | |
| asset-db:asset-add | default.asset-db:asset-add | No | | | |
| asset-db:asset-change | default.asset-db:asset-change | No | | | |
| asset-db:asset-delete | default.asset-db:asset-delete | No | | | |
| programming:pack-build-end | default.programming:pack-build-end | No | | | |
| project:change-design-resolution | default.project:change-design-resolution | No | | | |
| project:change-custom-layer | default.project:change-custom-layer | No | | | |
| project:change-sorting-layer | default.project:change-sorting-layer | No | | | |
| project:update-physics-group | default.project:update-physics-group | No | | | |
| project:change-high-quality | default.project:change-high-quality | No | | | |
| engine:engine-modules-global-config-changed | default.engine:engine-modules-global-config-changed | No | | | |
| open-devtools | default.open-devtools | No | | | |
| open-preview-devtools | default.open-preview-devtools | No | | | |
| graphical-tools | default.graphicalTools | No | | | |
| open-scene | default.open-scene | Yes | 打开场景 | - uuid {string} 场景资源的 UUID | await Editor.Message.request('scene', 'open-scene', sceneUuid); |
| load-empty-scene | default.load-empty-scene | No | | | |
| save-scene | default.save-scene | Yes | 保存场景 | | await Editor.Message.request('scene', 'save-scene'); |
| save-as-scene | default.save-as-scene | Yes | 场景另存为 | | await Editor.Message.request('scene', 'save-as-scene'); |
| close-scene | default.close-scene | Yes | 关闭场景 | | await Editor.Message.request('scene', 'close-scene'); |
| set-property | default.set-property | Yes | 设置某个元素内的属性 | - options {SetPropertyOptions}<br> - uuid {string} 修改属性的对象的 uuid<br> - path {string} 属性挂载对象的搜索路径<br> - dump {IProperty} 属性 dump 出来的数据 | await Editor.Message.request('scene', 'set-property', {<br> uuid: nodeUuid,<br> path: '__comps__.1.defaultClip',<br> dump: {<br> type: 'cc.AnimationClip',<br> value: {<br> uuid: animClipUuid,<br> },<br> },<br>}); |
| reset-property | default.reset-property | Yes | 重置元素属性到默认值 | - options {SetPropertyOptions}<br> - uuid {string} 修改属性的对象的 uuid<br> - path {string} 属性挂载对象的搜索路径 | await Editor.Message.request('scene', 'reset-property', {<br> uuid: nodeUuid,<br> path: 'position',<br>}); |
| preview-set-property | default.preview-set-property | No | | | |
| cancel-preview-set-property | default.cancel-preview-set-property | No | | | |
| update-property-from-null | default.update-property-from-null | No | | | |
| set-node-and-children-layer | default.set-node-and-children-layer | No | | | |
| move-array-element | default.move-array-element | Yes | 移动数组内某个元素的位置 | - options {MoveArrayOptions}<br> - uuid {string} 节点的 uuid<br> - path {string} 数组的搜索路径<br> - target {number} 目标 item 原来的索引<br> - offset {number} 偏移量<br> <br> @returns {boolean} 操作是否成功 | await Editor.Message.request('scene', 'move-array-element', {<br> uuid: nodeUuid,<br> path: '__comps__',<br> target: 1,<br> offset: -1,<br>}); |
| remove-array-element | default.remove-array-element | Yes | 删除数组内某个元素的位置 | - options {MoveArrayOptions}<br> - uuid {string} 节点的 uuid<br> - path {string} 数组的搜索路径<br> - index {number} 目标 item 的索引<br> <br> @returns {boolean} 操作是否成功 | await Editor.Message.request('scene', 'remove-array-element', {<br> uuid: nodeUuid,<br> path: '__comps__',<br> index: 0,<br>}); |
| select-all-nodes | default.select-all-nodes | No | | | |
| copy-node | default.copy-node | Yes | 拷贝节点,给下一步粘贴(创建)节点准备数据 | - uuids {string \| string[]} 节点的 uuid<br> <br> @returns {string \| string[]} 返回节点的 uuid | await Editor.Message.request('scene', 'copy-node', uuids); |
| duplicate-node | default.duplicate-node | Yes | 复制节点 | - uuids {string \| string[]} 节点的 uuid<br> <br> @returns {string \| string[]} 返回新节点的 uuid | await Editor.Message.request('scene', 'duplicate-node', uuids); |
| paste-node | default.paste-node | Yes | 粘贴节点 | - options {PasteNodeOptions}<br> - target {string} 目标节点 uuid<br> - uuids {string \| string[]} 被复制的节点 uuid<br> - keepWorldTransform {boolean} 是否保持新节点的世界坐标不变<br> <br> @returns {string \| string[]} 返回新节点的 uuid | await Editor.Message.request('scene', 'paste-node', {<br> target: nodeUuid,<br> uuids: nodeUuids,<br>}); |
| cut-node | default.cut-node | Yes | 剪切节点 | - uuids {string \| string[]} 节点的 uuid<br> <br> @returns {string \| string[]} 返回节点的 uuid | await Editor.Message.request('scene', 'cut-node', uuids); |
| set-parent | default.set-parent | Yes | 设置节点父级 | - options {CutNodeOptions}<br> - parent {string} 父节点 uuid<br> - uuids {string\|string[]} 需要设置的子节点 uuid<br> - keepWorldTransform {boolean} 是否保持新节点的世界坐标不变<br> <br> @returns {string \| string[]} 返回节点的 uuid | await Editor.Message.request('scene','set-parent', {<br> parent: nodeUuid,<br> uuids: nodeUuids,<br>}); |
| create-node | default.create-node | Yes | 创建节点 | - options {CreateNodeOptions}<br> - parent {string} 父节点 uuid<br> - components? {string[]} 组件名字<br> <br> - name? {string} 节点名字<br> - dump? {INode \| IScene} node 初始化应用的 dump 数据<br> - keepWorldTransform? {boolean} 是否保持新节点的世界坐标不变<br> - type? {string} 资源类型<br> - canvasRequired? {boolean} 是否需要有 cc.Canvas<br> - unlinkPrefab? {boolean} 是否要解绑为普通节点<br> - assetUuid? {string} asset uuid,从资源实例化节点<br> <br> 注意: 使用 assetUuid 从预制体创建节点时,无论 unlinkPrefab 传 false 还是不传,都不会自动建立 Prefab 关联。需额外调用 link-prefab 消息建立关联。<br> <br> @returns {string \| string[]} 返回新节点的 uuid | await Editor.Message.request('scene', 'create-node', {<br> name: 'New Node'<br> parent: nodeUuid,<br>}); |
| reset-node | default.reset-node | Yes | 重置节点的位置, 角度和缩放 | - uuid {string} 节点的 uuid<br> <br> @returns {boolean} 操作是否成功 | await Editor.Message.request('scene', 'reset-node', {<br> uuid: nodeUuid,<br>}); |
| reset-component | default.reset-component | Yes | 重置组件 | - options {ResetComponentOptions}<br> - uuid {string} 组件的 uuid<br> <br> @returns {boolean} 操作是否成功 | await Editor.Message.request('scene', 'reset-component', {<br> uuid: componentUuid,<br>}); |
| restore-prefab | default.restore-prefab | Yes | 使用预制体资源还原对应预制件节点(内置撤销记录) | - uuid {string} 节点的 uuid<br> - assetUuid {string} 资源的 uuid<br> <br> @returns {boolean} 操作是否成功 | await Editor.Message.request('scene', 'restore-prefab', nodeUuid, assetUuid); |
| remove-node | default.remove-node | Yes | 删除节点 | - options {RemoveNodeOptions}<br> - uuid: {string \| string[]} 节点的 uuid | await Editor.Message.request('scene', 'remove-node', { <br> uuid: nodeUuid<br>}); |
| create-component | default.create-component | Yes | 创建组件 | - options {CreateComponentOptions}<br> - uuid {string} 节点的 uuid<br> - component {string} 组件 classId cid)(推荐方式) 或者 className 类名 | Editor.Message.request('scene', 'create-component', { <br> uuid: nodeUuid,<br> component: 'cc.Sprite'<br>}); |
| remove-component | default.remove-component | Yes | 删除组件 | - options {CreateComponentOptions}<br> - uuid {string} 节点的 uuid<br> - component {string} 组件 classId cid)(推荐方式) 或者 className 类名 | await Editor.Message.request('scene', 'remove-component', { <br> uuid: componentUuid,<br>}); |
| execute-component-method | default.execute-component-method | Yes | 执行组件上的方法 | - options {ExecuteComponentMethodOptions}<br> - uuid {string} 组件的 uuid<br> - name {string} 方法名<br> - args {any[]} 参数 | await Editor.Message.request('scene', 'execute-component-method', {<br> uuid: componentUuid,<br> name: 'getNoisePreview',<br> args: [100, 100],<br>}); |
| execute-scene-script | default.execute-scene-script | Yes | 执行某个插件注册的方法 | - options {ExecuteSceneScriptMethodsOptions}<br> - name {string} 注册进来的插件名字<br> - method {string} 执行的方法名字<br> - args {any[]} 参数数组 | await Editor.Message.request('scene', 'execute-scene-script', {<br> name: 'animation-graph',<br> method: 'query',<br> args: [],<br>}); |
| snapshot | default.snapshot | Yes | 快照当前场景状态 | | await Editor.Message.request('scene', 'snapshot'); |
| snapshot-abort | default.snapshot-abort | Yes | 中止快照 | | await Editor.Message.request('scene', 'snapshot-abort'); |
| begin-recording | default.begin-recording | Yes | 开始记录节点 Undo 数据 | | const undoID = await Editor.Message.request('scene', 'begin-recording', nodeUuid); |
| end-recording | default.end-recording | Yes | 结束记录节点 Undo 数据 | | await Editor.Message.request('scene', 'end-recording', undoID); |
| cancel-recording | default.cancel-recording | Yes | 取消记录节点 Undo 数据 | | await Editor.Message.request('scene', 'cancel-recording', undoID); |
| undo | default.undo | No | | | |
| redo | default.redo | No | | | |
| soft-reload | default.soft-reload | Yes | 软刷新场景 | | await Editor.Message.request('scene', 'soft-reload'); |
| preview-material | default.preview-material | No | | | |
| light-probe-update-tetrahedron | default.light-probe-update-tetrahedron | No | | | |
| change-gizmo-tool | default.change-gizmo-tool | Yes | 更改 Gizmo 工具 | - name {string} 工具名字 'position' \| 'rotation' \| 'scale'\| 'rect' | await Editor.Message.request('scene', 'change-gizmo-tool', 'position'); |
| query-gizmo-tool-name | default.query-gizmo-tool-name | Yes | 获取当前 Gizmo 工具的名字 | @returns {string} 'position' \| 'rotation' \| 'scale' \| 'rect' | await Editor.Message.request('scene', 'query-gizmo-tool-name'); |
| change-gizmo-pivot | default.change-gizmo-pivot | Yes | 更改变换基准点 | - name {string} 变换基准点 'pivot' \| 'center' | await Editor.Message.request('scene', 'query-gizmo-pivot'); |
| query-gizmo-pivot | default.query-gizmo-pivot | Yes | 获取当前 Gizmo 基准点名字 | @returns {string} 'pivot' \| 'center' | await Editor.Message.request('scene', 'query-gizmo-pivot'); |
| query-gizmo-view-mode | default.query-gizmo-view-mode | Yes | 查询视图模式(查看/选择) | @return {string} 'view' \| 'select' | await Editor.Message.request('scene', 'query-gizmo-view-mode'); |
| change-gizmo-coordinate | default.change-gizmo-coordinate | Yes | 更改坐标系 | - type {string} 坐标系 'local' \| 'global' | await Editor.Message.request('scene', 'change-gizmo-coordinate', 'global'); |
| query-gizmo-coordinate | default.query-gizmo-coordinate | Yes | 获取当前坐标系名字 | @returns {string} 'local' \| 'global' | await Editor.Message.request('scene', 'query-gizmo-coordinate'); |
| change-is2D | default.change-is2D | Yes | 更改2D/3D视图模式 | - is2D {boolean} 2D/3D视图 | await Editor.Message.request('scene', 'change-is2D', true); |
| query-is2D | default.query-is2D | Yes | 获取当前视图模式 | @returns {boolean} true:2D, false:3D | await Editor.Message.request('scene', 'query-is2D'); |
| set-grid-visible | default.set-grid-visible | Yes | 显示/隐藏网格 | - visible {boolean} 显示/隐藏网格 | await Editor.Message.request('scene', 'set-grid-visible', false); |
| query-is-grid-visible | default.query-is-grid-visible | Yes | 查询网格显示状态 | @returns {boolean} true: visible, false: invisible | await Editor.Message.request('scene', 'query-is-grid-visible'); |
| set-icon-gizmo-3d | default.set-icon-gizmo-3d | Yes | 设置 IconGizmo 为 3D 或 2D 模式 | - is3D {boolean} 3D/2D IconGizmo | await Editor.Message.request('scene', 'set-icon-gizmo-3d', false); |
| query-is-icon-gizmo-3d | default.query-is-icon-gizmo-3d | Yes | 查询 IconGizmo 模式 | @returns {boolean} true: 3D, false: 2D | await Editor.Message.request('scene', 'query-is-icon-gizmo-3d'); |
| set-icon-gizmo-size | default.set-icon-gizmo-size | Yes | 设置 IconGizmo 的大小 | - size {number} IconGizmo 的大小 | await Editor.Message.request('scene', 'set-icon-gizmo-size', 60); |
| query-icon-gizmo-size | default.query-icon-gizmo-size | Yes | 查询 IconGizmo 的大小 | @returns {number} IconGizmo 的大小 | await Editor.Message.request('scene', 'query-icon-gizmo-size'); |
| query-transform-snap-configs | default.query-transform-snap-configs | No | | | |
| set-transform-snap-configs | default.set-transform-snap-configs | No | | | |
| query-rect-snapping-configs | default.query-rect-snapping-configs | No | | | |
| set-rect-snapping-configs | default.set-rect-snapping-configs | No | | | |
| focus-camera | default.focus-camera | Yes | 聚焦场景相机到节点上 | - uuids {string[] \| null} 节点 uuid | await Editor.Message.request('scene', 'focus-camera', nodeUuids); |
| align-with-view | default.align-with-view | Yes | 将场景相机位置与角度应用到选中节点上 | @returns {null} | await Editor.Message.request('scene', 'align-with-view'); |
| align-view-with-node | default.align-view-with-node | Yes | 将选中节点位置与角度应用到当前视角 | @returns {null} | await Editor.Message.request('scene', 'align-with-view-node'); |
| zoom-scene-view-down | default.zoom-scene-view-down | No | | | |
| zoom-scene-view-up | default.zoom-scene-view-up | No | | | |
| toggle-active-selected-node | default.toggle-active-selected-node | No | | | |
| toggle-active-unselected-node | default.toggle-active-unselected-node | No | | | |
| toggle-active-all-nodes | default.toggle-active-all-nodes | No | | | |
| apply-material | default.apply-material | No | | | |
| query-physics-material | default.query-physics-material | No | | | |
| change-physics-material | default.change-physics-material | No | | | |
| apply-physics-material | default.apply-physics-material | No | | | |
| query-animation-graph-variant | default.query-animation-graph-variant | No | | | |
| change-animation-graph-variant | default.change-animation-graph-variant | No | | | |
| apply-animation-graph-variant | default.apply-animation-graph-variant | No | | | |
| query-animation-mask | default.query-animation-mask | No | | | |
| change-animation-mask | default.change-animation-mask | No | | | |
| apply-animation-mask | default.apply-animation-mask | No | | | |
| apply-render-texture | default.apply-render-texture | No | | | |
| copy-camera-data-to-nodes | default.copy-camera-data-to-nodes | No | | | |
| record-animation | default.record-animation | No | | | |
| change-animation-root | default.change-animation-root | No | | | |
| set-edit-time | default.set-edit-time | No | | | |
| change-clip-state | default.change-clip-state | No | | | |
| change-edit-clip | default.change-edit-clip | No | | | |
| save-clip | default.save-clip | No | | | |
| animation-operation | default.animation-operation | No | | | |
| create-prefab | default.create-prefab | No | 创建预制体资源(内置撤销记录) | | |
| getdata-prefab | default.getdata-prefab | No | | | |
| link-prefab | default.link-prefab | No | 将节点关联到预制体资源,建立 Prefab 关联关系 | - uuid {string} 节点的 uuid<br> - assetUuid {string} 预制体资源的 uuid<br> <br> @returns {boolean} 操作是否成功 | await Editor.Message.request('scene', 'link-prefab', nodeUuid, assetUuid); |
| unlink-prefab | default.unlink-prefab | No | | | |
| apply-prefab | default.apply-prefab | No | 应用预制体节点修改到对应资源(内置撤销记录) | | |
| apply-removed-component | default.apply-removed-component | No | 应用预制体删除组件的修改到对应资源(内置撤销记录) | | |
| revert-removed-component | default.revert-removed-component | No | 还原预制体节点被移除的组件(内置撤销记录) | | |
| query-is-ready | default.query-is-ready | Yes | 查询当前场景是否准备就绪 | | await Editor.Message.request('scene', 'query-is-ready'); |
| query-node | default.query-node | Yes | 查询一个节点的数据 | - uuid {string} 节点的 uuid<br> <br> @returns {Object} 节点的 dump 数据 | await Editor.Message.request('scene', 'query-node', nodeUuid); |
| query-component | default.query-component | Yes | 查询一个组件的数据 | - uuid {string} 组件的 uuid<br> <br> @returns {Object} 组件的 dump 数据 | await Editor.Message.request('scene', 'query-component', nodeUuid); |
| query-node-tree | default.query-node-tree | Yes | 查询节点树的信息 | - uuid? {string} 根节点 uuid,不传入则以场景节点为根节点<br> <br> @returns {Object}<br> - name {string} 节点名字或者 'scene'<br> - active {boolean} 节点激活状态 <br> - type {string} cc.Scene or cc.Node<br> - uuid {string} 节点的 uuid<br> - children {[]} 子节点数组<br> - prefab {number} prefab状态, 1 表示是 prefab, 2 表示是 prefab 但丢失资源<br> - isScene {boolean} 是否是场景节点<br> - components {[Object]} 组件数组<br> - type {string} 组件类型<br> - value {string} 组件的 uuid <br> - extends {[string]} 组件的继承链数组 | await Editor.Message.request('scene', 'query-node-tree', nodeUuid); |
| query-nodes-by-asset-uuid | default.query-nodes-by-asset-uuid | Yes | 查询使用了资源 UUID 的节点 | - 查询使用了资源 UUID 的节点<br> <br> @returns {string[]} 节点的 uuid | await Editor.Message.request('scene', 'query-nodes-by-asset-uuid', assetUuid); |
| query-nodes-miss-assets | default.query-nodes-miss-assets | No | | | |
| query-component-function-of-node | default.query-component-function-of-node | No | | | |
| query-all-effects | default.query-all-effects | No | | | |
| query-material | default.query-material | No | | | |
| query-effect | default.query-effect | No | | | |
| query-serialized-material | default.query-serialized-material | No | | | |
| query-render-pipeline | default.query-render-pipeline | No | | | |
| change-render-pipeline | default.change-render-pipeline | No | | | |
| apply-render-pipeline | default.apply-render-pipeline | No | | | |
| query-creatable-asset-types | default.query-creatable-asset-types | No | | | |
| query-scene-json | default.query-scene-json | No | | | |
| query-current-scene | default.query-current-scene | No | | | |
| query-dirty | default.query-dirty | Yes | 查询当前场景是否有修改 | | await Editor.Message.request('scene', 'query-dirty'); |
| query-classes | default.query-classes | Yes | 查询所有在引擎中注册的类 | @returns {[Object]}<br> - extends? {string} 过滤出基于此类名扩展而来的类 | await Editor.Message.request('scene', 'query-classes'); |
| query-components | default.query-components | Yes | 查询当前场景的所有组件 | @returns {[Object]}<br> - name {string} 组件名字<br> - path {string} 菜单路径 | await Editor.Message.request('scene', 'query-components'); |
| query-component-has-script | default.query-component-has-script | Yes | 查询引擎组件列表是否含有指定类名的脚本 | - name 脚本的类名 Class<br> <br> @returns {boolean} 存在 true, 不存在 false | await Editor.Message.request('scene', 'query-component-has-script', 'cc.Sprite'); |
| query-script-name | default.query-script-name | No | | | |
| query-script-cid | default.query-script-cid | No | | | |
| query-layer-builtin | default.query-layer-builtin | No | | | |
| query-sorting-layer-builtin | default.query-sorting-layer-builtin | No | | | |
| query-scene-mode | default.query-scene-mode | No | | | |
| query-animation-state | default.query-animation-state | No | | | |
| query-current-animation-info | default.query-current-animation-info | No | | | |
| query-animation-root | default.query-animation-root | No | | | |
| query-animation-root-info | default.query-animation-root-info | No | | | |
| query-animation-edit-info | default.query-animation-edit-info | No | | | |
| query-animation-clip | default.query-animation-clip | No | | | |
| query-animation-properties | default.query-animation-properties | No | | | |
| query-animation-clips-info | default.query-animation-clips-info | No | | | |
| query-animation-clips-time | default.query-animation-clips-time | No | | | |
| query-property-value-at-frame | default.query-property-value-at-frame | No | | | |
| query-aux-curve-value-at-frame | default.query-aux-curve-value-at-frame | No | | | |
| query-auxiliary-curves | default.query-auxiliary-curves | No | | | |
| generate-available-name | default.generate-available-name | No | | | |
| editor-preview-set-play | default.editor-preview-set-play | No | | | |
| editor-refresh-gizmo-config | default.editor-refresh-gizmo-config | No | | | |
| editor-preview-call-method | default.editor-preview-call-method | No | | | |
| editor-preview-change-style-web | default.editor-preview-change-style | No | | | |
| editor-preview-change-style-native | preview.editorPreviewChangeStyle | No | | | |
| editor-preview-change-config | default.editor-preview-change-config, preview.editorPreviewChangeConfig | No | | | |
| scene:show-loading | default.scene:show-loading | No | | | |
| scene:hide-loading | default.scene:hide-loading | No | | | |
| execute-model-preview-animation-operation | default.execute-model-preview-animation-operation | No | | | |
| regenerate-polygon-2d-points | default.regenerate-polygon-2d-points | No | | | |
| export-particle-plist | default.export-particle-plist | No | | | |
| change-node-lock | default.change-node-lock | No | | | |
| unit-test | default.unit-test | No | | | |
| change-target-resolution | default.change-target-resolution | No | | | |
| query-latest-cache | queryLatestCache | No | | | |
| clear-scene-cache | clearSceneCache | No | | | |
| save-scene-cache-to-file | saveSceneCacheToFile | No | | | |
| update-cache-config | updateCacheConfig | No | | | |
| query-enum-list-with-path | default.query-enum-list-with-path | No | | | |
| query-wrap-mode-enum-list | default.query-wrap-mode-enum-list | No | | | |
| set-scene-light-on | default.set-scene-light-on | No | | | |
| query-scene-light-on | default.query-scene-light-on | No | | | |
| native-ipc | default.native-ipc | No | | | |
| native-scene | default.native-scene | No | | | |
| panel-browser | panelToBrowser | No | | | |
| browser-panel | default.on-browser-panel | No | | | |
| is-native | default.query-is-native | No | | | |
| query-preview-data | default.query-preview-data | No | | | |
| call-preview-function | default.call-preview-function | No | | | |
| query-thumbnail | default.query-thumbnail | No | | | |
| query-preview-game-view-data | preview.getGameViewData | No | | | |
| set-preview-window-visible | preview.setPreviewWindowVisible, setPreviewWindowVisible | No | | | |
| create-preview-window | preview.createPreviewWindow | No | | | |
| resize-native-window | resizeNativeWindow | No | | | |
| create-native-window | createNativeWindow | No | | | |
| preview-panel-ready | onPreviewPanelReady | No | | | |
| preview-panel-stop | onPreviewPanelStop | No | | | |
| toggle-light-probe-edit-mode | default.toggle-light-probe-edit-mode | No | | | |
| query-light-probe-edit-mode | default.query-light-probe-edit-mode | No | | | |
| toggle-light-probe-bounding-box-edit-mode | default.toggle-light-probe-bounding-box-edit-mode | No | | | |
| query-light-probe-bounding-box-edit-mode | default.query-light-probe-bounding-box-edit-mode | No | | | |
| scene:light-probe-edit-mode-changed | | Yes | 光照探针编辑模式切换通知 | - mode {boolean} 切换后的探针编辑模式 | Editor.Message.broadcast('scene:light-probe-edit-mode-changed', true); |
| scene:light-probe-bounding-box-edit-mode-changed | | Yes | 光照探针组件包围盒编辑模式切换通知 | - mode {boolean} 切换后的探针组件包围盒编辑模式 | Editor.Message.broadcast('scene:light-probe-bounding-box-edit-mode-changed', true); |
| lod-apply-current-camera-size | default.lod-apply-current-camera-size | No | | | |
| lod-insert | default.lod-insert | No | | | |
| lod-erase | default.lod-erase | No | | | |
| shortcuts:change | default.shortcuts:change | No | | | |
| window:zoom-level-change | default.window:zoom-level-change | No | | | |
| window:focus-zoom-level-change | default.window:focus-zoom-level-change | No | | | |
| multi-open-scene | default.multi-open-scene | No | | | |
| multi-close-scene | default.multi-close-scene | No | | | |
| multi-scene-dirty | default.multi-scene-dirty | No | | | |
| multi-scene-query | default.multi-scene-query | No | | | |
| multi-scene-focus | default.multi-scene-focus | No | | | |
| multi-scene-focus-query | default.multi-scene-focus-query | No | | | |
| multi-save-all-scene | default.multi-save-all-scene | No | | | |
| multi-is-multi-edit-mode | default.multi-is-multi-edit-mode | No | | | |
| multi-close-to-the-right | default.multi-close-to-the-right | No | | | |
| multi-move-tabs-to | default.multi-move-tabs-to | No | | | |
| multi-close-others | default.multi-close-others | No | | | |
## server
| Message | Methods | Public | Description | Doc | Example |
|---------|---------|--------|-------------|-----|---------|
| query-ip-list | queryIPList | Yes | 查询 IP 列表 | @returns {string[]} - IP 列表 | await Editor.Message.request('server', 'query-ip-list'); |
| query-https-enabled | queryHTTPSEnabled | No | | | |
| query-sort-ip-list | querySortIpList | Yes | 获取排序后的 ip 列表 | @returns {string[]} - 排序后的 IP 列表 | await Editor.Message.request('server', 'query-sort-ip-list'); |
| query-port | queryPort | Yes | 查询编辑器服务器当前启动的端口号 | @returns {number} - 端口号 | await Editor.Message.request('server', 'query-port'); |
| scan-lan | scanLAN | No | | | |
| change-preview-port | change-preview-port | No | | | |
| change-https-options | change-https-options | No | | | |
## shortcuts
| Message | Methods | Public | Description | Doc | Example |
|---------|---------|--------|-------------|-----|---------|
| open | open | No | | | |
| query-shortcut-map | queryShortcutMap | No | | | |
| change-shortcut | changeShortcut | No | | | |
| change-tab | default.changeTab | No | | | |
| reset-shortcut | resetShortcut | No | | | |
| query-packages-shortcut-list | queryPackagesShortcutList | No | | | |
| remove-custom-shortcut | removeCustomShortcut | No | | | |
| shortcuts:change | default.onShortcutChanged | No | | | |
## tester
| Message | Methods | Public | Description | Doc | Example |
|---------|---------|--------|-------------|-----|---------|
| open | open | No | | | |
| forwarding-to-window | forwarding-to-window | No | | | |
| * | panel.* | No | | | |
| auto-test | auto-test | No | | | |
## utils
| Message | Methods | Public | Description | Doc | Example |
|---------|---------|--------|-------------|-----|---------|
| export-dts | exportDTS | No | | | |
| tester-tag | testerTag | No | | | |
## window
| Message | Methods | Public | Description | Doc | Example |
|---------|---------|--------|-------------|-----|---------|
| focus-window-zoom-in | onFocusWindowZoomIn | No | | | |
| focus-window-zoom-out | onFocusWindowZoomOut | No | | | |
| focus-window-zoom-to-initial | onFocusWindowZoomToInitial | No | | | |
| window-zoom-level-change | onWindowZoomLevelChange | No | | | |
## cocos-service
| Message | Methods | Public | Description | Doc | Example |
|---------|---------|--------|-------------|-----|---------|
| open-service-panel | openServicePanel | No | | | |
| enable-service | enableService | No | | | |
| open-popup | openPopup | No | | | |
| close-popup | closePopup | No | | | |
| print-log | printLog | No | | | |
| api-request | apiRequest | No | | | |
| popup-show | popupShow | No | | | |
| plugin-msg | pluginMsg | No | | | |
| plugin-msg-service | default.pluginMsg | No | | | |
| plugin-msg-popup | simple.pluginMsg, dockable.pluginMsg | No | | | |
| service-ready | serviceReady | No | | | |
| i18n:change | i18nChange | No | | | |
| service-status-changed | onServiceStatusChanged | No | | | |
| drop-scene | dropScene | No | | | |
| drop-hierarchy | dropHierarchy | No | | | |
| drop-assets | dropAssets | No | | | |
## im-plugin
| Message | Methods | Public | Description | Doc | Example |
|---------|---------|--------|-------------|-----|---------|
| show-panel | showPanel | No | | | |
| silent-check-and-update | silentCheckAndUpdate | No | | | |
| im-plugin:check | default.check | No | | | |
| silent-check | silentCheck | No | | | |
| close-panel | closePanel | No | | | |
| whether-close | whetherClose | No | | | |
| get-sign-in-url | getSignInUrl | No | | | |
| get-redirect-url | getRedirectUrl | No | | | |
| update-toolbar | updateToolbarMain | No | | | |
| save-toolbar-info | saveToolbarInfo | No | | | |
| get-saved-toolbar-info | getSavedToolbarInfo | No | | | |
---
## Message 调用方式
在扩展中可以通过以下方式发送 message:
```typescript
// 发送消息 (等待返回结果)
const result = await Editor.Message.request(packageName, messageName, ...args);
// 发送消息 (不等待返回结果)
Editor.Message.send(packageName, messageName, ...args);
// 广播消息
Editor.Message.broadcast(messageName, ...args);
```
其中 `packageName` 为上述各表格的扩展包名称 (如 `asset-db`, `scene`, `builder` 等)。
+5 -1
View File
@@ -68,7 +68,11 @@ function findTsConfig(projectPath, explicitPath) {
function runExec(file, args, cwd) {
return new Promise((resolve) => {
execFile(file, args, { cwd, maxBuffer: 8 * 1024 * 1024 }, (error, stdout, stderr) => {
const options = { cwd, maxBuffer: 8 * 1024 * 1024 };
if (process.platform === 'win32' && /\.(cmd|bat)$/.test(file)) {
options.shell = true;
}
execFile(file, args, options, (error, stdout, stderr) => {
resolve({
code: error && typeof error.code === 'number' ? error.code : 0,
stdout: stdout || '',
+27 -1
View File
@@ -169,7 +169,9 @@ async function duplicatePrefab(projectPath, options = {}) {
throw new Error(`Prefab source file was not found: ${source}`);
}
const targetPath = resolveProjectPath(projectPath, target.endsWith('.prefab') ? target : `${target}.prefab`);
const rawTarget = target.endsWith('.prefab') ? target : `${target}.prefab`;
const targetUnderAssets = rawTarget.replace(/^assets[\\/]/, '');
const targetPath = resolveProjectPath(projectPath, path.join('assets', targetUnderAssets));
const assetsRoot = path.join(projectPath, 'assets');
const relativeToAssets = path.relative(assetsRoot, targetPath);
if (relativeToAssets.startsWith('..') || path.isAbsolute(relativeToAssets)) {
@@ -259,8 +261,32 @@ async function revertPrefabInstance(nodeUuid) {
throw lastError || new Error('No prefab revert editor message was available.');
}
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.');
}
if (!savePath.endsWith('.prefab')) {
throw new Error('savePath must end with .prefab');
}
// Note: 'create-prefab' is not a public message in Cocos Creator 3.8.8
// (Public: No in editor-messages-3.8.8.md). It works but may change in future versions.
const result = await requestEditorMessage('scene', 'create-prefab', nodeUuid, savePath);
return {
created: true,
nodeUuid,
savePath,
result,
};
}
module.exports = {
applyPrefabInstance,
createPrefab,
duplicatePrefab,
editPrefabJson,
inspectPrefab,
+97
View File
@@ -70,6 +70,7 @@ class ResourceProvider {
createResource('cocos://logs/editor', `${projectName} Editor Logs`, 'Recent MCP runtime logs and tool interaction history.'),
createResource('cocos://logs/project', `${projectName} Project Logs`, 'Recent tails from common project log files.'),
createResource('cocos://mcp/interactions', `${projectName} MCP Interactions`, 'Recent MCP tool interaction summaries.'),
createResource('cocos://mcp/execute-context', `${projectName} Execute Context Guide`, 'Available variables and code patterns for execute_javascript scene and editor contexts.'),
];
}
@@ -131,6 +132,8 @@ class ResourceProvider {
return this.getProjectLogsText(projectPath);
case 'cocos://mcp/interactions':
return this.interactionLog.summary();
case 'cocos://mcp/execute-context':
return this.getExecuteContextGuide();
default:
break;
}
@@ -286,6 +289,100 @@ class ResourceProvider {
].join('\n'))
.join('\n\n---\n\n');
}
getExecuteContextGuide() {
return [
'Execute JavaScript Context Guide',
'',
'This document describes the variables and code patterns available in',
'execute_javascript for both scene and editor contexts.',
'',
'=== Scene Context (context="scene") ===',
'',
'Injected Variables:',
' cc - Cocos engine module. Use cc.Sprite, cc.Node, cc.Vec3, etc.',
' scene - Active scene root node (cc.Node). Same as director.getScene().',
' director - cc.director.',
' require - Node.js require (with Cocos module paths).',
' Editor - Editor global API (if running in editor).',
' args - User-passed args object.',
'',
'Return Patterns:',
' 1. Direct return: return { myResult: 42 };',
' 2. run function: async function run(env) { return result; }',
' 3. module.exports: module.exports = async (env) => result;',
'',
'Key Rules:',
' - Use cc.Sprite, cc.Node, cc.UITransform — NOT bare Sprite/Node.',
' - Do NOT redeclare scene, director, cc — they are already in scope.',
' - Find nodes by traversing scene.children recursively (see pattern below).',
' - Load assets with callback-style assetManager.loadAny (see pattern below).',
'',
'Pattern: Find a node by UUID',
' let target = null;',
' function search(n) {',
' if (n.uuid === "NODE_UUID") { target = n; return; }',
' for (const c of n.children) { search(c); if (target) return; }',
' }',
' search(scene);',
'',
'Pattern: Load and set an asset reference (SpriteFrame, Texture, etc.)',
' const { Sprite, assetManager, SpriteFrame } = cc;',
' const sf = await new Promise((resolve, reject) => {',
' assetManager.loadAny(',
' { uuid: "ASSET_UUID", type: SpriteFrame },',
' (err, asset) => { if (err) reject(err); else resolve(asset); }',
' );',
' });',
' sprite.spriteFrame = sf;',
'',
'Pattern: Create a node with components',
' const { Node, UITransform, Sprite } = cc;',
' const node = new Node("MyNode");',
' node.layer = parent.layer;',
' node.addComponent(UITransform);',
' node.addComponent(Sprite);',
' parent.addChild(node);',
'',
'=== Editor Context (context="editor") ===',
'',
'Injected Variables:',
' require - Node.js require.',
' Editor - Editor global API.',
' args - User-passed args object.',
' context - Runtime context (projectPath, projectName, cocosVersion, etc.).',
' helpers - { getStatus, listTools, readResource, callTool,',
' listClientTargets, getClientConfig, configureClient }.',
' fs - Node.js fs module.',
' path - Node.js path module.',
' os - Node.js os module.',
'',
'Pattern: Call asset-db from editor context',
' const info = await Editor.Message.request("asset-db", "query-asset-info", "ASSET_UUID");',
'',
'Pattern: Call a scene method from editor context',
' const result = await Editor.Message.request(',
' "scene", "execute-scene-script", { name: "funplay-cocos-mcp", method: "inspectNode", args: { uuid: "NODE_UUID" } }',
' );',
'',
'=== Common Pitfalls ===',
'',
'1. set_component_property cannot set asset references (SpriteFrame, Texture,',
' Material, Font, AudioClip). Use execute_javascript with loadAny instead.',
'',
'2. create_node only accepts parentPath (e.g. "Canvas/Player"), not parentUuid.',
' Use get_hierarchy first to find the full path.',
'',
'3. assetManager.loadAny(uuid) without callback returns null in scene context.',
' Always use callback style: loadAny({uuid, type}, (err, asset) => {}).',
'',
'4. Custom script components (e.g. TestComponent) require the script to be',
' compiled before add_component or findComponent can find them.',
'',
'5. Node hierarchy paths are slash-separated from scene root, e.g.',
' "should_hide_in_hierarchy/TestPrefab/Sprite". Use get_hierarchy to verify.',
].join('\n');
}
}
module.exports = {
+56 -38
View File
@@ -31,6 +31,7 @@ const {
} = require('./project-instructions');
const {
applyPrefabInstance,
createPrefab,
duplicatePrefab,
editPrefabJson,
inspectPrefab,
@@ -41,6 +42,8 @@ const { createAssetsAdvancedTools } = require('./tools/assets-advanced');
const { createCocosProjectTools } = require('./tools/cocos-project');
const { buildSnippet, createFileTools, refreshAssets } = require('./tools/files');
const { createSceneEventTools } = require('./tools/scene-events');
const { createSceneManagementTools } = require('./tools/scene-management');
const { createScriptTools } = require('./tools/scripts');
const { captureDesktopScreenshot, captureEditorWindowScreenshot, capturePanelScreenshot } = require('./screenshots');
const { checkForUpdate } = require('./update-checker');
const { assertJavascriptSafety } = require('./javascript-safety');
@@ -358,8 +361,8 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
description: '[primary] Execute JavaScript in either the scene or editor context. Use context=\"scene\" for live scene/runtime inspection and mutation, or context=\"editor\" for Editor APIs, asset-db workflows, MCP orchestration, local filesystem access, and higher-level automation. Prefer this as the main flexible tool when many narrow tools would be noisy.',
inputSchema: createSchema(
{
context: { type: 'string', description: 'Execution context: scene or editor.' },
code: { type: 'string', description: 'JavaScript code to execute. May directly return a value, define run(env), or export a function.' },
context: { type: 'string', enum: ['scene', 'editor'], description: 'Execution context. Scene injects: cc, scene, director, require, Editor, args. Editor injects: require, Editor, args, context, helpers, fs, path, os. Read cocos://mcp/execute-context resource for details.' },
code: { type: 'string', description: 'JavaScript code. May directly return a value, define run(env), or use module.exports. In scene context, use cc.Sprite, cc.Node (not bare Sprite/Node). Load assets via cc.assetManager.loadAny({uuid, type}, cb) callback style.' },
args: { type: 'object', description: 'Optional JSON object passed into the script.' },
safety_checks: { type: 'boolean', description: 'Override the project default JavaScript safety checks for this call.' },
},
@@ -387,7 +390,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
description: '[compat] Execute JavaScript in the active Cocos scene context. Prefer execute_javascript with context="scene" as the main unified tool; use this when you specifically want the scene-only compatibility entrypoint.',
inputSchema: createSchema(
{
code: { type: 'string', description: 'JavaScript code to execute inside the scene script context.' },
code: { type: 'string', description: 'JavaScript code. Injected vars: cc, scene, director, require, Editor, args. Use cc.Sprite etc. (not bare Sprite). See cocos://mcp/execute-context resource.' },
args: { type: 'object', description: 'Optional JSON object passed to the scene script.' },
safety_checks: { type: 'boolean', description: 'Override the project default JavaScript safety checks for this call.' },
},
@@ -404,7 +407,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
description: '[compat] Execute JavaScript in the editor/browser context. Prefer execute_javascript with context="editor" as the main unified tool; use this when you specifically want the editor-only compatibility entrypoint.',
inputSchema: createSchema(
{
code: { type: 'string', description: 'JavaScript code to execute inside the editor context.' },
code: { type: 'string', description: 'JavaScript code. Injected vars: require, Editor, args, context, helpers, fs, path, os. See cocos://mcp/execute-context resource.' },
args: { type: 'object', description: 'Optional JSON object passed to the editor script.' },
safety_checks: { type: 'boolean', description: 'Override the project default JavaScript safety checks for this call.' },
},
@@ -665,7 +668,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
inputSchema: createSchema(
{
name: { type: 'string', description: 'Name of the node to create.' },
parentPath: { type: 'string', description: 'Optional parent node path.' },
parentPath: { type: 'string', description: 'Parent node hierarchy path, e.g. "Canvas/Player". Slash-separated from scene root. Use get_hierarchy to find the full path.' },
position: { type: 'object', description: 'Optional position {x,y,z}.' },
scale: { type: 'object', description: 'Optional scale {x,y,z}.' },
eulerAngles: { type: 'object', description: 'Optional rotation {x,y,z} in degrees.' },
@@ -681,7 +684,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
description: 'Delete a node by path, uuid, or name.',
inputSchema: createSchema(
{
path: { type: 'string', description: 'Hierarchy path.' },
path: { type: 'string', description: 'Hierarchy path, e.g. "Canvas/Player".' },
uuid: { type: 'string', description: 'Node uuid.' },
name: { type: 'string', description: 'Fallback exact node name.' },
},
@@ -695,7 +698,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
description: 'Update node position, rotation, scale, or active state.',
inputSchema: createSchema(
{
path: { type: 'string', description: 'Hierarchy path.' },
path: { type: 'string', description: 'Hierarchy path, e.g. "Canvas/Player".' },
uuid: { type: 'string', description: 'Node uuid.' },
name: { type: 'string', description: 'Fallback exact node name.' },
position: { type: 'object', description: 'Position {x,y,z}.' },
@@ -715,6 +718,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
handler: async () => getRuntimeContext(),
},
...createCocosProjectTools({ createSchema }),
...createSceneManagementTools({ createSchema, sceneBridge }),
{
name: 'list_scenes',
profile: 'core',
@@ -757,6 +761,19 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
return { count: assets.length, prefabs: assets.slice(0, 200) };
},
},
{
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),
},
{
name: 'inspect_prefab',
profile: 'core',
@@ -836,7 +853,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
inputSchema: createSchema(
{
prefabUuid: { type: 'string', description: 'Prefab asset uuid, db url, or path.' },
parentPath: { type: 'string', description: 'Optional parent node path.' },
parentPath: { type: 'string', description: 'Parent node hierarchy path, e.g. "Canvas/Player". Slash-separated from scene root.' },
name: { type: 'string', description: 'Optional override node name.' },
position: { type: 'object', description: 'Optional position {x,y,z}; fallback runtime path only.' },
},
@@ -881,7 +898,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
description: '[specialist] Inspect whether a scene node is linked to a prefab instance and return prefab metadata when available.',
inputSchema: createSchema(
{
path: { type: 'string', description: 'Node hierarchy path.' },
path: { type: 'string', description: 'Node hierarchy path, e.g. "Canvas/Player".' },
uuid: { type: 'string', description: 'Node uuid.' },
name: { type: 'string', description: 'Fallback exact node name.' },
},
@@ -895,7 +912,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
description: '[core] Apply a scene prefab instance back to its associated prefab asset using the Cocos editor scene apply-prefab message.',
inputSchema: createSchema(
{
path: { type: 'string', description: 'Node hierarchy path.' },
path: { type: 'string', description: 'Node hierarchy path, e.g. "Canvas/Player".' },
uuid: { type: 'string', description: 'Node uuid.' },
name: { type: 'string', description: 'Fallback exact node name.' },
},
@@ -909,7 +926,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
description: '[core] Revert a scene prefab instance from its associated prefab asset using available Cocos editor prefab revert messages.',
inputSchema: createSchema(
{
path: { type: 'string', description: 'Node hierarchy path.' },
path: { type: 'string', description: 'Node hierarchy path, e.g. "Canvas/Player".' },
uuid: { type: 'string', description: 'Node uuid.' },
name: { type: 'string', description: 'Fallback exact node name.' },
},
@@ -924,7 +941,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
inputSchema: createSchema(
{
prefabUuid: { type: 'string', description: 'Prefab asset uuid.' },
parentPath: { type: 'string', description: 'Optional parent node path.' },
parentPath: { type: 'string', description: 'Parent node hierarchy path, e.g. "Canvas/Player". Slash-separated from scene root.' },
name: { type: 'string', description: 'Optional override node name.' },
position: { type: 'object', description: 'Optional position {x,y,z}.' },
},
@@ -1034,7 +1051,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
description: '[core] List components attached to a scene node.',
inputSchema: createSchema(
{
path: { type: 'string', description: 'Node hierarchy path.' },
path: { type: 'string', description: 'Node hierarchy path, e.g. "Canvas/Player".' },
uuid: { type: 'string', description: 'Node uuid.' },
name: { type: 'string', description: 'Fallback exact node name.' },
},
@@ -1048,10 +1065,10 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
description: '[core] Inspect a component attached to a node.',
inputSchema: createSchema(
{
path: { type: 'string', description: 'Node hierarchy path.' },
path: { type: 'string', description: 'Node hierarchy path, e.g. "Canvas/Player".' },
uuid: { type: 'string', description: 'Node uuid.' },
name: { type: 'string', description: 'Fallback exact node name.' },
componentName: { type: 'string', description: 'Component class name.' },
componentName: { type: 'string', description: 'Component class name, e.g. Sprite, Label, Button. Custom scripts must be compiled.' },
index: { type: 'number', description: 'Optional component index.' },
},
[]
@@ -1064,10 +1081,10 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
description: 'Add a component to a node by component class name.',
inputSchema: createSchema(
{
path: { type: 'string', description: 'Node hierarchy path.' },
path: { type: 'string', description: 'Node hierarchy path, e.g. "Canvas/Player".' },
uuid: { type: 'string', description: 'Node uuid.' },
name: { type: 'string', description: 'Fallback exact node name.' },
componentName: { type: 'string', description: 'Component class name, for example Sprite or cc.UITransform.' },
componentName: { type: 'string', description: 'Component class name, e.g. Sprite, Label, Button, UITransform. Custom script components require the script to be compiled first.' },
},
['componentName']
),
@@ -1079,10 +1096,10 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
description: 'Remove a component from a node by name or index.',
inputSchema: createSchema(
{
path: { type: 'string', description: 'Node hierarchy path.' },
path: { type: 'string', description: 'Node hierarchy path, e.g. "Canvas/Player".' },
uuid: { type: 'string', description: 'Node uuid.' },
name: { type: 'string', description: 'Fallback exact node name.' },
componentName: { type: 'string', description: 'Component class name.' },
componentName: { type: 'string', description: 'Component class name, e.g. Sprite, Label, Button. Custom scripts must be compiled.' },
index: { type: 'number', description: 'Optional component index.' },
},
[]
@@ -1095,13 +1112,13 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
description: 'Set a component property by dot path using a JSON value.',
inputSchema: createSchema(
{
path: { type: 'string', description: 'Node hierarchy path.' },
path: { type: 'string', description: 'Node hierarchy path, e.g. "Canvas/Player".' },
uuid: { type: 'string', description: 'Node uuid.' },
name: { type: 'string', description: 'Fallback exact node name.' },
componentName: { type: 'string', description: 'Component class name.' },
componentName: { type: 'string', description: 'Component class name, e.g. Sprite, Label, Button. Custom scripts must be compiled.' },
index: { type: 'number', description: 'Optional component index.' },
propertyPath: { type: 'string', description: 'Property path such as color.r or enabled.' },
valueJson: { type: 'string', description: 'JSON encoded value to assign, for example true, 12, \"hero\", or {\"x\":1}.' },
propertyPath: { type: 'string', description: 'Property path using dot notation, e.g. "color.r", "enabled", "size.width".' },
valueJson: { type: 'string', description: 'JSON encoded value, e.g. true, 12, "hero", {"x":1}. NOTE: Cannot set asset references (SpriteFrame, Texture, Material, etc.) — use execute_javascript with cc.assetManager.loadAny({uuid, type}, cb) for those.' },
},
['propertyPath', 'valueJson']
),
@@ -1121,12 +1138,12 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
description: 'Reset or clear a component property by dot path.',
inputSchema: createSchema(
{
path: { type: 'string', description: 'Node hierarchy path.' },
path: { type: 'string', description: 'Node hierarchy path, e.g. "Canvas/Player".' },
uuid: { type: 'string', description: 'Node uuid.' },
name: { type: 'string', description: 'Fallback exact node name.' },
componentName: { type: 'string', description: 'Component class name.' },
componentName: { type: 'string', description: 'Component class name, e.g. Sprite, Label, Button. Custom scripts must be compiled.' },
index: { type: 'number', description: 'Optional component index.' },
propertyPath: { type: 'string', description: 'Property path such as color.r or enabled.' },
propertyPath: { type: 'string', description: 'Property path using dot notation, e.g. "color.r", "enabled", "size.width".' },
},
['propertyPath']
),
@@ -1139,7 +1156,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
inputSchema: createSchema(
{
name: { type: 'string', description: 'Canvas node name.' },
parentPath: { type: 'string', description: 'Optional parent node path.' },
parentPath: { type: 'string', description: 'Parent node hierarchy path, e.g. "Canvas/Player". Slash-separated from scene root.' },
width: { type: 'number', description: 'Canvas width.' },
height: { type: 'number', description: 'Canvas height.' },
position: { type: 'object', description: 'Optional position {x,y,z}.' },
@@ -1155,7 +1172,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
inputSchema: createSchema(
{
name: { type: 'string', description: 'Label node name.' },
parentPath: { type: 'string', description: 'Optional parent node path.' },
parentPath: { type: 'string', description: 'Parent node hierarchy path, e.g. "Canvas/Player". Slash-separated from scene root.' },
text: { type: 'string', description: 'Label text.' },
fontSize: { type: 'number', description: 'Font size.' },
width: { type: 'number', description: 'UI width.' },
@@ -1174,7 +1191,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
inputSchema: createSchema(
{
name: { type: 'string', description: 'Button node name.' },
parentPath: { type: 'string', description: 'Optional parent node path.' },
parentPath: { type: 'string', description: 'Parent node hierarchy path, e.g. "Canvas/Player". Slash-separated from scene root.' },
text: { type: 'string', description: 'Button text.' },
width: { type: 'number', description: 'Button width.' },
height: { type: 'number', description: 'Button height.' },
@@ -1194,8 +1211,8 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
inputSchema: createSchema(
{
name: { type: 'string', description: 'Sprite node name.' },
parentPath: { type: 'string', description: 'Optional parent node path.' },
spriteFrameUuid: { type: 'string', description: 'Optional SpriteFrame asset uuid.' },
parentPath: { type: 'string', description: 'Parent node hierarchy path, e.g. "Canvas/Player". Slash-separated from scene root.' },
spriteFrameUuid: { type: 'string', description: 'SpriteFrame asset uuid, e.g. "57520716-48c8-4a19-8acf-41c9f8777fb0@f9941". Use list_assets to find available SpriteFrames.' },
width: { type: 'number', description: 'UI width.' },
height: { type: 'number', description: 'UI height.' },
color: { type: 'string', description: 'Sprite color as #RRGGBB or #RRGGBBAA.' },
@@ -1219,7 +1236,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
inputSchema: createSchema(
{
name: { type: 'string', description: 'Camera node name.' },
parentPath: { type: 'string', description: 'Optional parent node path.' },
parentPath: { type: 'string', description: 'Parent node hierarchy path, e.g. "Canvas/Player". Slash-separated from scene root.' },
priority: { type: 'number', description: 'Camera priority.' },
visibility: { type: 'number', description: 'Camera visibility mask.' },
clearFlags: { type: 'number', description: 'Camera clear flags.' },
@@ -1272,7 +1289,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
description: 'Add an AnimationClip asset to a node Animation component.',
inputSchema: createSchema(
{
path: { type: 'string', description: 'Node hierarchy path.' },
path: { type: 'string', description: 'Node hierarchy path, e.g. "Canvas/Player".' },
uuid: { type: 'string', description: 'Node uuid.' },
name: { type: 'string', description: 'Fallback exact node name.' },
clipUuid: { type: 'string', description: 'AnimationClip asset uuid.' },
@@ -1288,7 +1305,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
description: '[core] Play an Animation component clip on a node.',
inputSchema: createSchema(
{
path: { type: 'string', description: 'Node hierarchy path.' },
path: { type: 'string', description: 'Node hierarchy path, e.g. "Canvas/Player".' },
uuid: { type: 'string', description: 'Node uuid.' },
name: { type: 'string', description: 'Fallback exact node name.' },
clipName: { type: 'string', description: 'Optional clip name.' },
@@ -1303,7 +1320,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
description: '[core] Stop an Animation component clip on a node.',
inputSchema: createSchema(
{
path: { type: 'string', description: 'Node hierarchy path.' },
path: { type: 'string', description: 'Node hierarchy path, e.g. "Canvas/Player".' },
uuid: { type: 'string', description: 'Node uuid.' },
name: { type: 'string', description: 'Fallback exact node name.' },
clipName: { type: 'string', description: 'Optional clip name.' },
@@ -1499,7 +1516,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
description: '[core] Emit a custom event on a target scene node with an optional JSON payload.',
inputSchema: createSchema(
{
path: { type: 'string', description: 'Node hierarchy path.' },
path: { type: 'string', description: 'Node hierarchy path, e.g. "Canvas/Player".' },
uuid: { type: 'string', description: 'Node uuid.' },
name: { type: 'string', description: 'Fallback exact node name.' },
eventName: { type: 'string', description: 'Event name to emit.' },
@@ -1524,16 +1541,17 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
handler: async (args) => sceneBridge.call('simulateButtonClick', args),
},
...createSceneEventTools({ createSchema, sceneBridge }),
...createScriptTools({ createSchema, getRuntimeContext }),
{
name: 'invoke_component_method',
profile: 'full',
description: '[core] Invoke a method on a component for runtime validation and test hooks.',
inputSchema: createSchema(
{
path: { type: 'string', description: 'Node hierarchy path.' },
path: { type: 'string', description: 'Node hierarchy path, e.g. "Canvas/Player".' },
uuid: { type: 'string', description: 'Node uuid.' },
name: { type: 'string', description: 'Fallback exact node name.' },
componentName: { type: 'string', description: 'Component class name.' },
componentName: { type: 'string', description: 'Component class name, e.g. Sprite, Label, Button. Custom scripts must be compiled.' },
index: { type: 'number', description: 'Optional component index.' },
methodName: { type: 'string', description: 'Method name to invoke.' },
args: { type: 'array', description: 'Optional argument array.' },
+182
View File
@@ -97,8 +97,12 @@ function collectUuidReferences(content) {
async function inspectAssetDependencies(projectPath, target, options = {}) {
const info = await queryAssetInfo(target);
const isDirectory = info && (info.isDirectory || info.type === 'folder' || (info.url && !path.extname(info.url)));
const filePath = assetFilePath(projectPath, info);
if (!filePath) {
if (isDirectory) {
throw new Error(`Cannot inspect dependencies of a directory asset: ${target}. Please specify a file asset (e.g. a .prefab, .scene, or .mat file).`);
}
throw new Error(`Asset file was not found: ${target}`);
}
@@ -210,12 +214,190 @@ function createAssetsAdvancedTools({ createSchema, getRuntimeContext }) {
return await validateAssetDependencies(projectPath, args);
},
},
{
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 path (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).',
},
},
['action']
),
handler: async (args) => {
const action = String(args.action || '').trim();
if (action === 'import') {
const sourceDirectory = String(args.sourceDirectory || '').trim();
const targetDirectory = String(args.targetDirectory || '').trim();
if (!sourceDirectory) {
throw new Error('sourceDirectory is required for action: import.');
}
if (!targetDirectory) {
throw new Error('targetDirectory is required for action: import.');
}
if (!global.Editor || !Editor.Message || typeof Editor.Message.request !== 'function') {
throw new Error('Editor.Message.request is unavailable in the Cocos extension host.');
}
const result = await Editor.Message.request('asset-db', 'import-asset', sourceDirectory, targetDirectory);
return { action, sourceDirectory, targetDirectory, result };
}
if (action === 'delete') {
const urls = Array.isArray(args.urls) ? args.urls.filter(Boolean) : [];
if (urls.length === 0) {
throw new Error('urls is required for action: delete.');
}
if (!global.Editor || !Editor.Message || typeof Editor.Message.request !== 'function') {
throw new Error('Editor.Message.request is unavailable in the Cocos extension host.');
}
const results = [];
for (const url of urls) {
try {
await Editor.Message.request('asset-db', 'delete-asset', url);
results.push({ url, deleted: true });
} catch (error) {
results.push({ url, deleted: false, error: error.message });
}
}
return { action, urls, results };
}
throw new Error(`Unknown action: ${action}. Must be one of: import, delete.`);
},
},
{
name: 'find_unused_assets',
profile: 'full',
description: '[core] Find assets not referenced by any scene, prefab, or animation 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 from the reference scan.',
},
assetTypes: {
type: 'array',
items: { type: 'string' },
description: 'Filter by Cocos asset type, e.g. ["cc.Texture2D", "cc.AudioClip"].',
},
limit: { type: 'number', description: 'Max results. Default 200.' },
},
[]
),
handler: async (args) => {
const { projectPath } = getRuntimeContext();
return await findUnusedAssets(projectPath, args);
},
},
];
}
const SERIALIZED_EXTENSIONS = ['.scene', '.prefab', '.anim', '.mat'];
const SKIP_UNUSED_EXTENSIONS = ['.ts', '.scene'];
async function findUnusedAssets(projectPath, options = {}, listAssetsFn) {
const listAssetsRef = typeof listAssetsFn === 'function' ? listAssetsFn : listAssets;
const directory = options.directory || 'db://assets';
const excludeDirs = Array.isArray(options.excludeDirectories) ? options.excludeDirectories : [];
const assetTypes = Array.isArray(options.assetTypes) ? options.assetTypes : undefined;
const limit = Number.isFinite(options.limit) ? Math.max(1, options.limit) : 200;
const allAssets = await listAssetsRef({
pattern: directory + '/**',
ccType: assetTypes,
});
const referencedUuids = new Set();
const assetsDir = path.join(projectPath, 'assets');
function scanDir(dir) {
let entries;
try {
entries = fs.readdirSync(dir, { withFileTypes: true });
} catch (_) {
return;
}
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
if (entry.name === 'node_modules' || entry.name === 'temp' || entry.name === 'library') {
continue;
}
const relativePath = path.relative(assetsDir, fullPath).replace(/\\/g, '/');
if (excludeDirs.some((excl) => relativePath.startsWith(String(excl)))) {
continue;
}
scanDir(fullPath);
} else {
const ext = path.extname(entry.name).toLowerCase();
if (!SERIALIZED_EXTENSIONS.includes(ext)) {
continue;
}
try {
const content = fs.readFileSync(fullPath, 'utf8');
const refs = collectUuidReferences(content);
for (const ref of refs) {
referencedUuids.add(ref.uuid);
}
} catch (_) {
// Skip unreadable files
}
}
}
}
scanDir(assetsDir);
const unused = allAssets.filter((asset) => {
if (!asset || !asset.uuid) {
return false;
}
if (asset.isDirectory) {
return false;
}
const url = asset.url || asset.source || '';
const ext = path.extname(url).toLowerCase();
if (SKIP_UNUSED_EXTENSIONS.includes(ext)) {
return false;
}
return !referencedUuids.has(asset.uuid);
});
return {
totalScanned: allAssets.length,
unusedCount: unused.length,
referencedCount: referencedUuids.size,
unused: unused.slice(0, limit),
};
}
module.exports = {
collectUuidReferences,
createAssetsAdvancedTools,
findUnusedAssets,
inspectAssetDependencies,
validateAssetDependencies,
};
+1
View File
@@ -174,6 +174,7 @@ function createCocosProjectTools({ createSchema }) {
[]
),
handler: async (args) => await tryEditorRequests([
{ channel: 'preview', method: 'open', args: [args || {}] },
{ channel: 'preview', method: 'start', args: [args || {}] },
{ channel: 'preview', method: 'open-preview', args: [args || {}] },
{ channel: 'builder', method: 'preview', args: [args || {}] },
+244
View File
@@ -0,0 +1,244 @@
'use strict';
const fs = require('fs');
const path = require('path');
const { tryEditorRequests } = require('./cocos-project');
const { openAsset } = require('../assets');
const TEMPLATE_PATH = path.join(__dirname, '..', '..', 'resources', 'template.scene');
function buildSceneContent(sceneName, templatePath) {
const tplPath = templatePath || TEMPLATE_PATH;
let content;
try {
content = fs.readFileSync(tplPath, 'utf8');
} catch (err) {
throw new Error(`Failed to read scene template at ${tplPath}: ${err.message}`);
}
const json = JSON.parse(content);
if (json[0] && json[0]._name !== undefined) {
json[0]._name = sceneName;
}
if (json[1] && json[1]._name !== undefined) {
json[1]._name = sceneName;
}
return JSON.stringify(json);
}
function createSceneManagementTools({ createSchema, sceneBridge }) {
return [
{
name: 'create_scene',
profile: 'full',
description: '[core] Create a new scene asset in the Cocos project and optionally 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.' },
overwrite: { type: 'boolean', description: 'Overwrite if the asset already exists. Default false.' },
},
['sceneName', 'savePath']
),
handler: async (args) => {
const sceneName = String(args.sceneName || '').trim();
const savePath = String(args.savePath || '').trim();
if (!sceneName) {
throw new Error('sceneName is required.');
}
if (!savePath) {
throw new Error('savePath is required.');
}
if (!savePath.endsWith('.scene')) {
throw new Error('savePath must end with .scene');
}
const content = buildSceneContent(sceneName);
const createArgs = [savePath, content];
if (args.overwrite === true) {
createArgs.push({ overwrite: true });
}
const result = await tryEditorRequests([
{ channel: 'asset-db', method: 'create-asset', args: createArgs },
]);
const url = (result.result && (result.result.url || result.result.source)) || savePath;
const uuid = result.result && result.result.uuid;
if (args.open !== false) {
try {
await openAsset(url);
} catch (_) {
// Opening is best-effort; creation succeeded.
}
}
return {
created: true,
sceneName,
url,
uuid,
opened: args.open !== false,
};
},
},
{
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) => {
const action = String(args.action || '').trim();
const IPC_MAP = {
is_dirty: [
{ channel: 'scene', method: 'query-dirty' },
],
is_ready: [
{ channel: 'scene', method: 'query-is-ready' },
],
soft_reload: [
{ channel: 'scene', method: 'soft-reload' },
],
};
const candidates = IPC_MAP[action];
if (!candidates) {
throw new Error(`Unknown action: ${action}. Must be one of: is_dirty, is_ready, soft_reload.`);
}
const result = await tryEditorRequests(candidates);
return {
action,
result: result.result,
};
},
},
{
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', 'duplicate'],
description: 'Clipboard or duplication action.',
},
uuids: {
type: 'array',
items: { type: 'string' },
description: 'Node UUIDs to copy, cut, or duplicate.',
},
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) => {
const action = String(args.action || '').trim();
if (action === 'copy' || action === 'cut') {
const uuids = Array.isArray(args.uuids) ? args.uuids.filter(Boolean) : [];
if (uuids.length === 0) {
throw new Error(`uuids is required for action: ${action}.`);
}
const method = action === 'copy' ? 'copy-node' : 'cut-node';
const result = await tryEditorRequests([
{ channel: 'scene', method, args: [uuids] },
]);
return { action, uuids, result: result.result };
}
if (action === 'duplicate') {
const uuids = Array.isArray(args.uuids) ? args.uuids.filter(Boolean) : [];
if (uuids.length === 0) {
throw new Error('uuids is required for action: duplicate.');
}
const result = await tryEditorRequests([
{ channel: 'scene', method: 'duplicate-node', args: [uuids] },
]);
return { action, uuids, result: result.result };
}
if (action === 'paste') {
const target = String(args.target || '').trim();
if (!target) {
throw new Error('target is required for action: paste.');
}
const keepWorldTransform = args.keepWorldTransform === true;
const pasteOpts = { target, keepWorldTransform };
const uuids = Array.isArray(args.uuids) ? args.uuids.filter(Boolean) : [];
if (uuids.length > 0) {
pasteOpts.uuids = uuids;
}
const result = await tryEditorRequests([
{
channel: 'scene',
method: 'paste-node',
args: [pasteOpts],
},
]);
return { action, target, keepWorldTransform, result: result.result };
}
throw new Error(`Unknown action: ${action}. Must be one of: copy, cut, paste, duplicate.`);
},
},
{
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),
},
{
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.',
},
keepWorldTransform: {
type: 'boolean',
description: 'Keep world transform after reparenting. Default false.',
},
},
[]
),
handler: async (args) => sceneBridge.call('reparentNode', args),
},
];
}
module.exports = {
buildSceneContent,
createSceneManagementTools,
};
+148
View File
@@ -0,0 +1,148 @@
'use strict';
const fs = require('fs');
const path = require('path');
const { resolveProjectPath } = require('../path-safety');
const { refreshAssets } = require('./files');
const PRIMITIVE_TYPES = new Set(['Number', 'String', 'Boolean']);
function generateComponentTemplate(className, properties = []) {
const ccTypes = new Set(['Component']);
const propLines = [];
for (const prop of properties) {
const propName = String(prop.name || 'property').trim();
const propType = String(prop.type || 'Number').trim();
const defaultValue = prop.default !== undefined ? prop.default : getDefaultForType(propType);
if (!PRIMITIVE_TYPES.has(propType)) {
ccTypes.add(propType);
}
if (PRIMITIVE_TYPES.has(propType)) {
propLines.push(` @property`);
propLines.push(` ${propName}: ${typeToTs(propType)} = ${defaultValue};`);
} else {
propLines.push(` @property(${propType})`);
propLines.push(` ${propName}: ${propType} | null = null;`);
}
}
const importTypes = Array.from(ccTypes).join(', ');
const propsBlock = propLines.length > 0
? propLines.join('\n')
: ' // Add properties here';
return `import { _decorator, ${importTypes} } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('${className}')
export class ${className} extends Component {
${propsBlock}
start() {
}
update(deltaTime: number) {
}
}
`;
}
function generatePlainTemplate(className) {
return `export class ${className} {
}
`;
}
function getDefaultForType(type) {
switch (type) {
case 'Number': return '0';
case 'String': return "''";
case 'Boolean': return 'false';
default: return 'null';
}
}
function typeToTs(type) {
switch (type) {
case 'Number': return 'number';
case 'String': return 'string';
case 'Boolean': return 'boolean';
default: return type;
}
}
function createScriptTools({ createSchema, getRuntimeContext }) {
return [
{
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 or project path, 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. Each item: { name, type, default }. Type can be Number, String, Boolean, or a CC class name like Label, Sprite, Node.',
},
},
['scriptName', 'savePath']
),
handler: async (args) => {
const scriptName = String(args.scriptName || '').trim();
let savePath = String(args.savePath || '').trim();
if (!scriptName) {
throw new Error('scriptName is required.');
}
if (!savePath) {
throw new Error('savePath is required.');
}
if (!savePath.endsWith('.ts')) {
throw new Error('savePath must end with .ts');
}
if (savePath.startsWith('db://')) {
savePath = savePath.slice('db://'.length);
}
const templateType = args.template === 'plain' ? 'plain' : 'component';
const properties = Array.isArray(args.properties) ? args.properties : [];
const content = templateType === 'plain'
? generatePlainTemplate(scriptName)
: generateComponentTemplate(scriptName, properties);
const { projectPath } = getRuntimeContext();
const fullPath = resolveProjectPath(projectPath, savePath);
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
fs.writeFileSync(fullPath, content, 'utf8');
const refreshMessage = await refreshAssets(projectPath, fullPath);
return {
created: true,
scriptName,
savePath,
template: templateType,
className: scriptName,
contentLength: content.length,
refreshMessage,
};
},
},
];
}
module.exports = {
createScriptTools,
generateComponentTemplate,
generatePlainTemplate,
};
+2 -1
View File
@@ -34,6 +34,7 @@
"docs/",
"lib/",
"panel/",
"resources/",
"browser.js",
"scene.js",
"server.json",
@@ -49,7 +50,7 @@
"access": "public"
},
"scripts": {
"check": "node --check browser.js && node --check scene.js && node --check panel/index.js && node --check bin/funplay-cocos-mcp.js && node --check lib/assets.js && node --check lib/client-config.js && node --check lib/config.js && node --check lib/diagnostics.js && node --check lib/electron-tools.js && node --check lib/input.js && node --check lib/interaction-log.js && node --check lib/javascript-safety.js && node --check lib/logs.js && node --check lib/path-safety.js && node --check lib/prefabs.js && node --check lib/project-instructions.js && node --check lib/prompts.js && node --check lib/resources.js && node --check lib/runtime-log.js && node --check lib/screenshots.js && node --check lib/server.js && node --check lib/tool-profiles.js && node --check lib/tool-registry.js && node --check lib/tools/assets-advanced.js && node --check lib/tools/cocos-project.js && node --check lib/tools/files.js && node --check lib/tools/scene-events.js && node --check lib/update-checker.js && node --check lib/utils.js && node --check scripts/generate-tool-docs.js && node --check scripts/release.js",
"check": "node --check browser.js && node --check scene.js && node --check panel/index.js && node --check bin/funplay-cocos-mcp.js && node --check lib/assets.js && node --check lib/client-config.js && node --check lib/config.js && node --check lib/diagnostics.js && node --check lib/electron-tools.js && node --check lib/input.js && node --check lib/interaction-log.js && node --check lib/javascript-safety.js && node --check lib/logs.js && node --check lib/path-safety.js && node --check lib/prefabs.js && node --check lib/project-instructions.js && node --check lib/prompts.js && node --check lib/resources.js && node --check lib/runtime-log.js && node --check lib/screenshots.js && node --check lib/server.js && node --check lib/tool-profiles.js && node --check lib/tool-registry.js && node --check lib/tools/assets-advanced.js && node --check lib/tools/cocos-project.js && node --check lib/tools/files.js && node --check lib/tools/scene-events.js && node --check lib/tools/scene-management.js && node --check lib/tools/scripts.js && node --check lib/update-checker.js && node --check lib/utils.js && node --check scripts/generate-tool-docs.js && node --check scripts/release.js",
"test": "node --test",
"docs:generate": "node scripts/generate-tool-docs.js",
"docs:check": "node scripts/generate-tool-docs.js --check",
+106
View File
@@ -0,0 +1,106 @@
[
{
"__type__": "cc.SceneAsset",
"_name": "Template",
"scene": { "__id__": 1 }
},
{
"__type__": "cc.Scene",
"_name": "Template",
"_children": [],
"_globals": { "__id__": 2 }
},
{
"__type__": "cc.SceneGlobals",
"ambient": { "__id__": 3 },
"shadows": { "__id__": 4 },
"_skybox": { "__id__": 5 },
"fog": { "__id__": 6 },
"octree": { "__id__": 7 },
"skin": { "__id__": 8 },
"lightProbeInfo": { "__id__": 9 },
"postSettings": { "__id__": 10 },
"bakedWithStationaryMainLight": false,
"bakedWithHighpLightmap": false
},
{
"__type__": "cc.AmbientInfo",
"_skyColorHDR": { "__type__": "cc.Vec4", "x": 0, "y": 0, "z": 0, "w": 0.520833125 },
"_skyColor": { "__type__": "cc.Vec4", "x": 0, "y": 0, "z": 0, "w": 0.520833125 },
"_skyIllumHDR": 20000,
"_skyIllum": 20000,
"_groundAlbedoHDR": { "__type__": "cc.Vec4", "x": 0, "y": 0, "z": 0, "w": 0 },
"_groundAlbedo": { "__type__": "cc.Vec4", "x": 0, "y": 0, "z": 0, "w": 0 },
"_skyColorLDR": { "__type__": "cc.Vec4", "x": 0.2, "y": 0.5, "z": 0.8, "w": 1 },
"_skyIllumLDR": 20000,
"_groundAlbedoLDR": { "__type__": "cc.Vec4", "x": 0.2, "y": 0.2, "z": 0.2, "w": 1 }
},
{
"__type__": "cc.ShadowsInfo",
"_enabled": false,
"_type": 0,
"_normal": { "__type__": "cc.Vec3", "x": 0, "y": 1, "z": 0 },
"_distance": 0,
"_planeBias": 1,
"_shadowColor": { "__type__": "cc.Color", "r": 76, "g": 76, "b": 76, "a": 255 },
"_maxReceived": 4,
"_size": { "__type__": "cc.Vec2", "x": 512, "y": 512 }
},
{
"__type__": "cc.SkyboxInfo",
"_envLightingType": 0,
"_envmapHDR": null,
"_envmap": null,
"_envmapLDR": null,
"_diffuseMapHDR": null,
"_diffuseMapLDR": null,
"_enabled": false,
"_useHDR": true,
"_editableMaterial": null,
"_reflectionHDR": null,
"_reflectionLDR": null,
"_rotationAngle": 0
},
{
"__type__": "cc.FogInfo",
"_type": 0,
"_fogColor": { "__type__": "cc.Color", "r": 200, "g": 200, "b": 200, "a": 255 },
"_enabled": false,
"_fogDensity": 0.3,
"_fogStart": 0.5,
"_fogEnd": 300,
"_fogAtten": 5,
"_fogTop": 1.5,
"_fogRange": 1.2,
"_accurate": false
},
{
"__type__": "cc.OctreeInfo",
"_enabled": false,
"_minPos": { "__type__": "cc.Vec3", "x": -1024, "y": -1024, "z": -1024 },
"_maxPos": { "__type__": "cc.Vec3", "x": 1024, "y": 1024, "z": 1024 },
"_depth": 8
},
{
"__type__": "cc.SkinInfo",
"_enabled": false,
"_blurRadius": 0.01,
"_sssIntensity": 3
},
{
"__type__": "cc.LightProbeInfo",
"_giScale": 1,
"_giSamples": 1024,
"_bounces": 2,
"_reduceRinging": 0,
"_showProbe": true,
"_showWireframe": true,
"_showConvex": false,
"_data": null,
"_lightProbeSphereVolume": 1
},
{
"__type__": "cc.PostSettingsInfo",
"_toneMappingType": 0
}
]
+82 -6
View File
@@ -761,6 +761,82 @@ exports.methods = {
};
},
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,
};
},
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;
const keepWorldTransform = options.keepWorldTransform === true;
const oldWorldPos = keepWorldTransform ? node.worldPosition.clone() : null;
const oldWorldRot = keepWorldTransform ? node.worldRotation.clone() : null;
const oldWorldScale = keepWorldTransform ? node.worldScale.clone() : null;
let newParent = null;
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;
}
if (keepWorldTransform) {
node.worldPosition = oldWorldPos;
node.worldRotation = oldWorldRot;
node.worldScale = oldWorldScale;
}
return {
reparented: true,
uuid: node.uuid,
oldPath,
newPath: getNodePath(node),
oldParent: oldParent ? oldParent.name : null,
newParent: newParent.name,
siblingIndex: node.getSiblingIndex(),
keepWorldTransform,
};
},
async setNodeTransform(options = {}) {
const node = findNode(options);
if (!node) {
@@ -847,7 +923,7 @@ exports.methods = {
const component = findComponent(node, options);
if (!component) {
throw new Error('Target component was not found.');
throw new Error(`Target component was not found${options.componentName ? ': ' + options.componentName : ''}. Ensure the script is compiled and the component is attached to the node.`);
}
const componentName = component.constructor ? component.constructor.name : 'UnknownComponent';
@@ -867,7 +943,7 @@ exports.methods = {
const component = findComponent(node, options);
if (!component) {
throw new Error('Target component was not found.');
throw new Error(`Target component was not found${options.componentName ? ': ' + options.componentName : ''}. Ensure the script is compiled and the component is attached to the node.`);
}
return {
@@ -892,7 +968,7 @@ exports.methods = {
const component = findComponent(node, options);
if (!component) {
throw new Error('Target component was not found.');
throw new Error(`Target component was not found${options.componentName ? ': ' + options.componentName : ''}. Ensure the script is compiled and the component is attached to the node.`);
}
setValueByPath(component, options.propertyPath, options.value);
@@ -913,7 +989,7 @@ exports.methods = {
const component = findComponent(node, options);
if (!component) {
throw new Error('Target component was not found.');
throw new Error(`Target component was not found${options.componentName ? ': ' + options.componentName : ''}. Ensure the script is compiled and the component is attached to the node.`);
}
resetValueByPath(component, options.propertyPath);
@@ -1410,7 +1486,7 @@ exports.methods = {
const component = findComponent(target, { componentName });
if (!component) {
throw new Error(`Target component was not found: ${componentName}`);
throw new Error(`Target component was not found: ${componentName}. Ensure the script is compiled and the component is attached to the target node.`);
}
if (typeof component[handlerName] !== 'function') {
throw new Error(`Target component method was not found: ${componentName}.${handlerName}`);
@@ -1465,7 +1541,7 @@ exports.methods = {
}
const component = findComponent(node, options);
if (!component) {
throw new Error('Target component was not found.');
throw new Error(`Target component was not found${options.componentName ? ': ' + options.componentName : ''}. Ensure the script is compiled and the component is attached to the node.`);
}
const methodName = String(options.methodName || '').trim();
if (!methodName || typeof component[methodName] !== 'function') {
+136
View File
@@ -0,0 +1,136 @@
'use strict';
const assert = require('node:assert/strict');
const test = require('node:test');
const fs = require('node:fs');
const path = require('node:path');
const os = require('node:os');
const { findUnusedAssets, collectUuidReferences } = require('../lib/tools/assets-advanced');
function createMockAssetsDir() {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'funplay-unused-test-'));
const assetsDir = path.join(tmpDir, 'assets');
fs.mkdirSync(assetsDir, { recursive: true });
return { tmpDir, assetsDir };
}
function writeFile(dir, relPath, content) {
const fullPath = path.join(dir, relPath);
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
fs.writeFileSync(fullPath, content, 'utf8');
}
test('findUnusedAssets returns assets not referenced by any serialized file', async () => {
const { tmpDir, assetsDir } = createMockAssetsDir();
try {
const usedUuid = 'aaa11111-bbbb-cccc-dddd-eeeeeeee1111';
writeFile(assetsDir, 'textures/used.png', 'fake-png');
writeFile(assetsDir, 'textures/used.png.meta', JSON.stringify({ uuid: usedUuid }));
const unusedUuid = 'aaa22222-bbbb-cccc-dddd-eeeeeeee2222';
writeFile(assetsDir, 'textures/unused.png', 'fake-png');
writeFile(assetsDir, 'textures/unused.png.meta', JSON.stringify({ uuid: unusedUuid }));
writeFile(assetsDir, 'scenes/game.scene', JSON.stringify({
__type__: 'cc.SceneAsset',
scene: { __uuid__: usedUuid },
}));
const mockListAssets = async () => [
{ uuid: usedUuid, url: 'db://assets/textures/used.png', type: 'cc.Texture2D' },
{ uuid: unusedUuid, url: 'db://assets/textures/unused.png', type: 'cc.Texture2D' },
];
const result = await findUnusedAssets(tmpDir, {}, mockListAssets);
assert.equal(result.totalScanned, 2);
assert.equal(result.unusedCount, 1);
assert.equal(result.unused[0].uuid, unusedUuid);
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
test('findUnusedAssets skips .ts and .scene files from unused list', async () => {
const { tmpDir, assetsDir } = createMockAssetsDir();
try {
const scriptUuid = 'bbb11111-bbbb-cccc-dddd-eeeeeeee3333';
const sceneUuid = 'bbb22222-bbbb-cccc-dddd-eeeeeeee4444';
writeFile(assetsDir, 'scripts/MyScript.ts', 'export class MyScript {}');
writeFile(assetsDir, 'scripts/MyScript.ts.meta', JSON.stringify({ uuid: scriptUuid }));
writeFile(assetsDir, 'scenes/level.scene', '{}');
writeFile(assetsDir, 'scenes/level.scene.meta', JSON.stringify({ uuid: sceneUuid }));
const mockListAssets = async () => [
{ uuid: scriptUuid, url: 'db://assets/scripts/MyScript.ts', type: 'cc.ScriptAsset' },
{ uuid: sceneUuid, url: 'db://assets/scenes/level.scene', type: 'cc.SceneAsset' },
];
const result = await findUnusedAssets(tmpDir, {}, mockListAssets);
assert.equal(result.totalScanned, 2);
assert.equal(result.unusedCount, 0);
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
test('findUnusedAssets respects excludeDirectories', async () => {
const { tmpDir, assetsDir } = createMockAssetsDir();
try {
const referencedUuid = 'ccc11111-bbbb-cccc-dddd-eeeeeeee5555';
const unreferencedUuid = 'ccc22222-bbbb-cccc-dddd-eeeeeeee6666';
writeFile(assetsDir, 'textures/a.png', 'fake');
writeFile(assetsDir, 'textures/a.png.meta', JSON.stringify({ uuid: referencedUuid }));
writeFile(assetsDir, 'internal/b.png', 'fake');
writeFile(assetsDir, 'internal/b.png.meta', JSON.stringify({ uuid: unreferencedUuid }));
writeFile(assetsDir, 'internal/hidden.scene', JSON.stringify({
__type__: 'cc.SceneAsset',
tex: { __uuid__: referencedUuid },
}));
const mockListAssets = async () => [
{ uuid: referencedUuid, url: 'db://assets/textures/a.png', type: 'cc.Texture2D' },
{ uuid: unreferencedUuid, url: 'db://assets/internal/b.png', type: 'cc.Texture2D' },
];
const result = await findUnusedAssets(tmpDir, {
excludeDirectories: ['internal'],
}, mockListAssets);
assert.equal(result.totalScanned, 2);
assert.equal(result.unusedCount, 2);
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
test('findUnusedAssets respects limit option', async () => {
const { tmpDir, assetsDir } = createMockAssetsDir();
try {
const assets = [];
for (let i = 0; i < 5; i++) {
const uuid = `ddd${i}1111-bbbb-cccc-dddd-eeeeeeee777${i}`;
writeFile(assetsDir, `textures/tex${i}.png`, 'fake');
writeFile(assetsDir, `textures/tex${i}.png.meta`, JSON.stringify({ uuid }));
assets.push({ uuid, url: `db://assets/textures/tex${i}.png`, type: 'cc.Texture2D' });
}
const mockListAssets = async () => assets;
const result = await findUnusedAssets(tmpDir, { limit: 3 }, mockListAssets);
assert.equal(result.totalScanned, 5);
assert.equal(result.unusedCount, 5);
assert.equal(result.unused.length, 3);
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
test('collectUuidReferences is re-exported and works', () => {
const refs = collectUuidReferences(JSON.stringify({
__uuid__: 'abc12345-def6-7890-abcd-ef1234567890',
}));
assert.ok(refs.some((r) => r.uuid === 'abc12345-def6-7890-abcd-ef1234567890'));
});
+359
View File
@@ -0,0 +1,359 @@
'use strict';
const assert = require('node:assert/strict');
const test = require('node:test');
const { createSceneManagementTools, buildSceneContent } = require('../lib/tools/scene-management');
function makeTools(overrides = {}) {
const createSchema = (properties, required) => {
const schema = { type: 'object', properties };
if (required && required.length) {
schema.required = required;
}
return schema;
};
const calls = [];
const sceneBridge = {
call: async (method, args) => {
calls.push({ method, args });
return { ok: true, method, args };
},
};
const tools = createSceneManagementTools({ createSchema, sceneBridge, ...overrides });
const byName = {};
for (const tool of tools) {
byName[tool.name] = tool;
}
return { tools, byName, calls, sceneBridge, createSchema };
}
function mockEditor(requestImpl) {
global.Editor = {
Message: {
request: requestImpl,
},
};
}
function unmockEditor() {
delete global.Editor;
}
test('create_scene requires sceneName and savePath', async () => {
const { byName } = makeTools();
await assert.rejects(() => byName.create_scene.handler({ savePath: 'db://assets/x.scene' }), /sceneName is required/);
await assert.rejects(() => byName.create_scene.handler({ sceneName: 'X' }), /savePath is required/);
});
test('create_scene rejects non-.scene savePath', async () => {
const { byName } = makeTools();
await assert.rejects(
() => byName.create_scene.handler({ sceneName: 'X', savePath: 'db://assets/x.prefab' }),
/must end with \.scene/
);
});
test('buildSceneContent reads template and replaces scene name', () => {
const content = buildSceneContent('MyNewScene');
const json = JSON.parse(content);
assert.equal(json[0]._name, 'MyNewScene');
assert.equal(json[1]._name, 'MyNewScene');
assert.equal(json[0].__type__, 'cc.SceneAsset');
assert.equal(json[1].__type__, 'cc.Scene');
assert.ok(Array.isArray(json[1]._children), 'Scene should have _children array');
assert.equal(json[1]._children.length, 0, 'Template scene should have no children');
const globalTypes = json.slice(2).map((o) => o.__type__);
assert.ok(globalTypes.includes('cc.SceneGlobals'));
assert.ok(globalTypes.includes('cc.AmbientInfo'));
assert.ok(globalTypes.includes('cc.ShadowsInfo'));
assert.ok(globalTypes.includes('cc.SkyboxInfo'));
assert.ok(globalTypes.includes('cc.FogInfo'));
assert.ok(globalTypes.includes('cc.OctreeInfo'));
assert.ok(globalTypes.includes('cc.SkinInfo'));
assert.ok(globalTypes.includes('cc.LightProbeInfo'));
assert.ok(globalTypes.includes('cc.PostSettingsInfo'));
});
test('buildSceneContent throws on missing template file', () => {
assert.throws(
() => buildSceneContent('Test', '/nonexistent/path/template.scene'),
/Failed to read scene template/
);
});
test('create_scene creates asset with template content and returns result', async () => {
const requests = [];
mockEditor(async (channel, method, ...args) => {
requests.push({ channel, method, args });
return { uuid: 'scene-uuid-1', url: 'db://assets/scenes/NewScene.scene' };
});
try {
const { byName } = makeTools();
const result = await byName.create_scene.handler({
sceneName: 'NewScene',
savePath: 'db://assets/scenes/NewScene.scene',
open: false,
});
assert.equal(result.created, true);
assert.equal(result.sceneName, 'NewScene');
assert.equal(result.uuid, 'scene-uuid-1');
assert.equal(result.opened, false);
assert.equal(requests.length, 1);
assert.equal(requests[0].method, 'create-asset');
assert.equal(requests[0].args[0], 'db://assets/scenes/NewScene.scene');
const content = requests[0].args[1];
assert.ok(typeof content === 'string' && content.length > 0, 'content should be non-empty string');
const json = JSON.parse(content);
assert.equal(json[0]._name, 'NewScene');
assert.equal(json[1]._name, 'NewScene');
assert.equal(json[0].__type__, 'cc.SceneAsset');
assert.equal(json[1].__type__, 'cc.Scene');
} finally {
unmockEditor();
}
});
test('create_scene with overwrite passes option to create-asset', async () => {
const requests = [];
mockEditor(async (channel, method, ...args) => {
requests.push({ channel, method, args });
return { uuid: 'scene-uuid-2', url: 'db://assets/scenes/OverwriteScene.scene' };
});
try {
const { byName } = makeTools();
await byName.create_scene.handler({
sceneName: 'OverwriteScene',
savePath: 'db://assets/scenes/OverwriteScene.scene',
open: false,
overwrite: true,
});
assert.equal(requests[0].args.length, 3, 'should have 3 args: url, content, option');
assert.deepEqual(requests[0].args[2], { overwrite: true });
} finally {
unmockEditor();
}
});
test('query_scene_state is_dirty calls query-dirty', async () => {
const requests = [];
mockEditor(async (channel, method) => {
requests.push({ channel, method });
return true;
});
try {
const { byName } = makeTools();
const result = await byName.query_scene_state.handler({ action: 'is_dirty' });
assert.equal(result.action, 'is_dirty');
assert.equal(result.result, true);
assert.equal(requests[0].method, 'query-dirty');
} finally {
unmockEditor();
}
});
test('query_scene_state is_ready calls query-is-ready', async () => {
const requests = [];
mockEditor(async (channel, method) => {
requests.push({ channel, method });
return true;
});
try {
const { byName } = makeTools();
const result = await byName.query_scene_state.handler({ action: 'is_ready' });
assert.equal(result.action, 'is_ready');
assert.equal(requests[0].method, 'query-is-ready');
} finally {
unmockEditor();
}
});
test('query_scene_state soft_reload calls soft-reload', async () => {
const requests = [];
mockEditor(async (channel, method) => {
requests.push({ channel, method });
return null;
});
try {
const { byName } = makeTools();
const result = await byName.query_scene_state.handler({ action: 'soft_reload' });
assert.equal(result.action, 'soft_reload');
assert.equal(requests[0].method, 'soft-reload');
} finally {
unmockEditor();
}
});
test('query_scene_state rejects unknown action', async () => {
const { byName } = makeTools();
await assert.rejects(() => byName.query_scene_state.handler({ action: 'unknown' }), /Unknown action/);
});
test('copy_paste_node copy requires uuids', async () => {
const { byName } = makeTools();
await assert.rejects(() => byName.copy_paste_node.handler({ action: 'copy' }), /uuids is required/);
});
test('copy_paste_node copy calls copy-node', async () => {
const requests = [];
mockEditor(async (channel, method, ...args) => {
requests.push({ channel, method, args });
return { ok: true };
});
try {
const { byName } = makeTools();
const result = await byName.copy_paste_node.handler({
action: 'copy',
uuids: ['uuid-1', 'uuid-2'],
});
assert.equal(result.action, 'copy');
assert.deepEqual(result.uuids, ['uuid-1', 'uuid-2']);
assert.equal(requests[0].method, 'copy-node');
assert.deepEqual(requests[0].args[0], ['uuid-1', 'uuid-2']);
} finally {
unmockEditor();
}
});
test('copy_paste_node cut calls cut-node', async () => {
const requests = [];
mockEditor(async (channel, method, ...args) => {
requests.push({ channel, method, args });
return { ok: true };
});
try {
const { byName } = makeTools();
const result = await byName.copy_paste_node.handler({
action: 'cut',
uuids: ['uuid-1'],
});
assert.equal(result.action, 'cut');
assert.equal(requests[0].method, 'cut-node');
} finally {
unmockEditor();
}
});
test('copy_paste_node paste requires target', async () => {
const { byName } = makeTools();
await assert.rejects(() => byName.copy_paste_node.handler({ action: 'paste' }), /target is required/);
});
test('copy_paste_node paste calls paste-node with target and keepWorldTransform', async () => {
const requests = [];
mockEditor(async (channel, method, ...args) => {
requests.push({ channel, method, args });
return { ok: true };
});
try {
const { byName } = makeTools();
const result = await byName.copy_paste_node.handler({
action: 'paste',
target: 'parent-uuid',
keepWorldTransform: true,
});
assert.equal(result.action, 'paste');
assert.equal(result.target, 'parent-uuid');
assert.equal(result.keepWorldTransform, true);
assert.equal(requests[0].method, 'paste-node');
assert.deepEqual(requests[0].args[0], { target: 'parent-uuid', keepWorldTransform: true });
} finally {
unmockEditor();
}
});
test('copy_paste_node paste with uuids passes them in options', async () => {
const requests = [];
mockEditor(async (channel, method, ...args) => {
requests.push({ channel, method, args });
return { ok: true };
});
try {
const { byName } = makeTools();
await byName.copy_paste_node.handler({
action: 'paste',
target: 'parent-uuid',
uuids: ['copied-uuid-1'],
});
assert.equal(requests[0].method, 'paste-node');
assert.deepEqual(requests[0].args[0], {
target: 'parent-uuid',
keepWorldTransform: false,
uuids: ['copied-uuid-1'],
});
} finally {
unmockEditor();
}
});
test('copy_paste_node duplicate calls duplicate-node', async () => {
const requests = [];
mockEditor(async (channel, method, ...args) => {
requests.push({ channel, method, args });
return ['new-uuid-1'];
});
try {
const { byName } = makeTools();
const result = await byName.copy_paste_node.handler({
action: 'duplicate',
uuids: ['uuid-1'],
});
assert.equal(result.action, 'duplicate');
assert.deepEqual(result.uuids, ['uuid-1']);
assert.equal(requests[0].method, 'duplicate-node');
assert.deepEqual(requests[0].args[0], ['uuid-1']);
} finally {
unmockEditor();
}
});
test('copy_paste_node duplicate requires uuids', async () => {
const { byName } = makeTools();
await assert.rejects(() => byName.copy_paste_node.handler({ action: 'duplicate' }), /uuids is required/);
});
test('copy_paste_node rejects unknown action', async () => {
const { byName } = makeTools();
await assert.rejects(() => byName.copy_paste_node.handler({ action: 'invalid' }), /Unknown action/);
});
test('rename_node calls sceneBridge with renameNode', async () => {
const { byName, calls } = makeTools();
await byName.rename_node.handler({ path: 'Canvas/TestNode', newName: 'RenamedNode' });
assert.equal(calls.length, 1);
assert.equal(calls[0].method, 'renameNode');
assert.equal(calls[0].args.path, 'Canvas/TestNode');
assert.equal(calls[0].args.newName, 'RenamedNode');
});
test('reparent_node calls sceneBridge with reparentNode', async () => {
const { byName, calls } = makeTools();
await byName.reparent_node.handler({
path: 'Canvas/TestNode',
targetPath: 'Canvas/OtherParent',
siblingIndex: 2,
keepWorldTransform: true,
});
assert.equal(calls.length, 1);
assert.equal(calls[0].method, 'reparentNode');
assert.equal(calls[0].args.path, 'Canvas/TestNode');
assert.equal(calls[0].args.targetPath, 'Canvas/OtherParent');
assert.equal(calls[0].args.siblingIndex, 2);
assert.equal(calls[0].args.keepWorldTransform, true);
});
test('all 5 tools have correct profiles', () => {
const { byName } = makeTools();
assert.equal(byName.create_scene.profile, 'full');
assert.equal(byName.query_scene_state.profile, 'core');
assert.equal(byName.copy_paste_node.profile, 'full');
assert.equal(byName.rename_node.profile, 'full');
assert.equal(byName.reparent_node.profile, 'full');
});
test('all 5 tools have inputSchema with type object', () => {
const { byName } = makeTools();
for (const name of Object.keys(byName)) {
assert.equal(byName[name].inputSchema.type, 'object', `${name} should have object schema`);
assert.ok(byName[name].inputSchema.properties, `${name} should have properties`);
}
});
+139
View File
@@ -0,0 +1,139 @@
'use strict';
const assert = require('node:assert/strict');
const test = require('node:test');
const fs = require('node:fs');
const path = require('node:path');
const os = require('node:os');
const {
createScriptTools,
generateComponentTemplate,
generatePlainTemplate,
} = require('../lib/tools/scripts');
test('generateComponentTemplate produces valid component with start and update', () => {
const code = generateComponentTemplate('PlayerController');
assert.ok(code.includes("import { _decorator, Component } from 'cc'"));
assert.ok(code.includes("@ccclass('PlayerController')"));
assert.ok(code.includes('export class PlayerController extends Component'));
assert.ok(code.includes('start()'));
assert.ok(code.includes('update(deltaTime: number)'));
});
test('generateComponentTemplate includes primitive @property', () => {
const code = generateComponentTemplate('Enemy', [
{ name: 'speed', type: 'Number', default: 5 },
{ name: 'label', type: 'String', default: "'Enemy'" },
]);
assert.ok(code.includes('@property'));
assert.ok(code.includes('speed: number = 5'));
assert.ok(code.includes("label: string = 'Enemy'"));
assert.ok(!code.includes('Label'));
});
test('generateComponentTemplate includes CC type @property', () => {
const code = generateComponentTemplate('Weapon', [
{ name: 'target', type: 'Label' },
{ name: 'sprite', type: 'Sprite' },
]);
assert.ok(code.includes('@property(Label)'));
assert.ok(code.includes('target: Label | null = null'));
assert.ok(code.includes('@property(Sprite)'));
assert.ok(code.includes('sprite: Sprite | null = null'));
assert.ok(code.includes('import { _decorator, Component, Label, Sprite }'));
});
test('generateComponentTemplate uses defaults when property default is omitted', () => {
const code = generateComponentTemplate('Item', [
{ name: 'count', type: 'Number' },
{ name: 'flag', type: 'Boolean' },
]);
assert.ok(code.includes('count: number = 0'));
assert.ok(code.includes('flag: boolean = false'));
});
test('generateComponentTemplate with no properties has placeholder comment', () => {
const code = generateComponentTemplate('Empty');
assert.ok(code.includes('// Add properties here'));
});
test('generatePlainTemplate produces minimal class', () => {
const code = generatePlainTemplate('Utils');
assert.ok(code.includes('export class Utils'));
assert.ok(!code.includes('@ccclass'));
assert.ok(!code.includes('@property'));
});
test('create_script writes file and returns metadata', async () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'funplay-script-test-'));
try {
const createSchema = (properties, required) => {
const schema = { type: 'object', properties };
if (required && required.length) {
schema.required = required;
}
return schema;
};
const getRuntimeContext = () => ({ projectPath: tmpDir });
const tools = createScriptTools({ createSchema, getRuntimeContext });
const tool = tools[0];
const result = await tool.handler({
scriptName: 'TestComp',
savePath: 'assets/scripts/TestComp.ts',
template: 'component',
properties: [{ name: 'speed', type: 'Number', default: 10 }],
});
assert.equal(result.created, true);
assert.equal(result.scriptName, 'TestComp');
assert.equal(result.template, 'component');
assert.equal(result.className, 'TestComp');
const written = fs.readFileSync(path.join(tmpDir, 'assets', 'scripts', 'TestComp.ts'), 'utf8');
assert.ok(written.includes('export class TestComp extends Component'));
assert.ok(written.includes('speed: number = 10'));
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
test('create_script rejects non-.ts savePath', async () => {
const createSchema = () => ({ type: 'object', properties: {} });
const getRuntimeContext = () => ({ projectPath: '/tmp' });
const tools = createScriptTools({ createSchema, getRuntimeContext });
await assert.rejects(
() => tools[0].handler({ scriptName: 'X', savePath: 'assets/scripts/X.js' }),
/must end with \.ts/
);
});
test('create_script requires scriptName and savePath', async () => {
const createSchema = () => ({ type: 'object', properties: {} });
const getRuntimeContext = () => ({ projectPath: '/tmp' });
const tools = createScriptTools({ createSchema, getRuntimeContext });
await assert.rejects(() => tools[0].handler({ savePath: 'X.ts' }), /scriptName is required/);
await assert.rejects(() => tools[0].handler({ scriptName: 'X' }), /savePath is required/);
});
test('create_script plain template writes minimal class', async () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'funplay-script-test-'));
try {
const createSchema = () => ({ type: 'object', properties: {} });
const getRuntimeContext = () => ({ projectPath: tmpDir });
const tools = createScriptTools({ createSchema, getRuntimeContext });
const result = await tools[0].handler({
scriptName: 'Helper',
savePath: 'assets/scripts/Helper.ts',
template: 'plain',
});
assert.equal(result.template, 'plain');
const written = fs.readFileSync(path.join(tmpDir, 'assets', 'scripts', 'Helper.ts'), 'utf8');
assert.ok(written.includes('export class Helper'));
assert.ok(!written.includes('@ccclass'));
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
+3 -2
View File
@@ -21,7 +21,7 @@ function createRegistry(profile, projectPath = path.resolve('/tmp/funplay-cocos-
test('core profile exposes the documented focused tool set', () => {
const tools = createRegistry('core').listTools();
assert.equal(tools.length, 37);
assert.equal(tools.length, 38);
assert.equal(tools.some((tool) => tool.name === 'execute_javascript'), true);
assert.equal(tools.some((tool) => tool.name === 'get_editor_state'), true);
assert.equal(tools.some((tool) => tool.name === 'get_tool_catalog'), true);
@@ -31,12 +31,13 @@ test('core profile exposes the documented focused tool set', () => {
assert.equal(tools.some((tool) => tool.name === 'get_performance_snapshot'), true);
assert.equal(tools.some((tool) => tool.name === 'list_project_instructions'), true);
assert.equal(tools.some((tool) => tool.name === 'set_selection'), true);
assert.equal(tools.some((tool) => tool.name === 'query_scene_state'), true);
assert.equal(tools.some((tool) => tool.name === 'write_file'), false);
});
test('full profile exposes all built-in tools', () => {
const tools = createRegistry('full').listTools();
assert.equal(tools.length, 101);
assert.equal(tools.length, 110);
assert.equal(tools.some((tool) => tool.name === 'write_file'), true);
assert.equal(tools.some((tool) => tool.name === 'edit_prefab_json'), true);
assert.equal(tools.some((tool) => tool.name === 'create_project_skill'), true);