2 Commits
Author SHA1 Message Date
mingyuansi 9e06ba79d8 docs: add AI agent skill guide for efficient MCP tool usage
CI / validate (push) Has been cancelled
2026-06-30 22:01:37 +08:00
mingyuansi b65a4f22c3 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
2026-06-30 21:53:06 +08:00
22 changed files with 5533 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. 6. Add clear input schema descriptions so AI clients can choose the tool correctly.
7. Test the tool with `tools/list` and `tools/call`. 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 ## Documentation
The documentation is bilingual: The documentation is bilingual:
+260
View File
@@ -0,0 +1,260 @@
# Funplay Cocos MCP — AI Agent Skill
> Use this skill when operating a Cocos Creator 3.8+ project through the Funplay Cocos MCP server.
## What This Is
An MCP server embedded inside the Cocos Creator editor. It exposes tools, resources, and prompts that let you inspect and manipulate a live Cocos project — scene hierarchy, nodes, components, assets, prefabs, scripts, screenshots, runtime state, and more.
- **Server URL**: `http://127.0.0.1:8765/` (default; may auto-shift if port is taken)
- **MCP server name**: `funplay_cocos`
- **Tool profiles**: `core` (38 tools, default), `full` (110 tools), `custom`
- **Health check**: `GET /health` — verify the server is alive before doing anything
## Core Philosophy: execute_javascript First
**`execute_javascript` is the primary tool.** It handles ~80% of all workflows in fewer, higher-leverage calls than chaining many narrow tools.
**Decision rule**: Start with `execute_javascript`. Only switch to a specialist tool when it is *strictly better* for the task:
| Task | Best tool | Why |
|---|---|---|
| Scene/node/component inspection & mutation | `execute_javascript` (scene) | One call can inspect, decide, and act |
| Editor automation, file ops, asset-db | `execute_javascript` (editor) | Full access to `Editor`, `fs`, `path`, `helpers` |
| TypeScript compile errors | `run_script_diagnostics` / `get_script_diagnostic_context` | Parsed diagnostics with source snippets |
| Visual verification | `capture_scene_screenshot` / `capture_preview_screenshot` | Image proof of results |
| Exact asset discovery | `list_assets` | Structured asset-db query with type filtering |
| Opening a specific scene | `open_scene` | Direct scene switching |
| Compact editor summary | `get_editor_state` | One-call snapshot of project + selection + windows |
| Validate scene health | `validate_scene` | Combined scene + runtime + diagnostics + logs check |
**Anti-pattern**: Calling `get_hierarchy``inspect_node``list_components``inspect_component` in 4 separate calls when one `execute_javascript` with `context="scene"` can do all of it and mutate in the same call.
## The Two Execution Contexts
### context="scene" — Live scene/runtime
Runs inside the active Cocos scene via the scene bridge.
**Injected variables:**
- `cc` — Cocos engine (use `cc.Sprite`, `cc.Node`, `cc.Vec3`, `cc.UITransform`, 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 (if running in editor)
- `args` — User-passed args object
**Return patterns (any one):**
```javascript
// 1. Direct return
return { sceneName: scene.name, childCount: scene.children.length };
// 2. run function (async supported)
async function run(env) { return result; }
// 3. module.exports
module.exports = async (env) => result;
```
**Critical rules:**
- Always use `cc.Sprite`, `cc.Node` — never bare `Sprite`, `Node`
- Do NOT redeclare `scene`, `director`, `cc` — they are already in scope
- `assetManager.loadAny` must use callback style — it returns `null` without a callback
### context="editor" — Editor/browser context
Runs in the Cocos editor main process with full Node.js access.
**Injected variables:**
- `require`, `Editor`, `args`, `context`, `helpers`, `fs`, `path`, `os`
**`helpers` object:**
- `helpers.getStatus()` — server status
- `helpers.listTools()` — all exposed MCP tools
- `helpers.readResource(uri)` — read an MCP resource
- `helpers.callTool(name, args)` — call another MCP tool
- `helpers.configureClient(targetId)` — configure an MCP client
**Common editor-context patterns:**
```javascript
// Query asset-db
const info = await Editor.Message.request("asset-db", "query-asset-info", "ASSET_UUID");
// Call a scene method
const result = await Editor.Message.request(
"scene", "execute-scene-script",
{ name: "funplay-cocos-mcp", method: "inspectNode", args: { uuid: "NODE_UUID" } }
);
// Read project context via helper
const ctx = await helpers.readResource("cocos://project/context");
// Call another tool from editor context
const diag = await helpers.callTool("run_script_diagnostics", {});
```
## Safety Checks
`execute_javascript` has safety checks **enabled by default**. The following patterns are blocked:
- `fs.rm`, `fs.unlink`, `fs.truncate`, `fs.rmdir` and sync variants — **delete/truncate**
- `fs.createWriteStream`, `fs.openSync`**raw write streams**
- `require("child_process")`, `exec`, `spawn`, etc. — **shell execution**
- `~`, `$HOME`, `%USERPROFILE%`**user-home path literals**
- `../`**path traversal**
- Absolute paths outside the Cocos project root
**To bypass** (only after reviewing the risk): pass `"safety_checks": false` in the tool args.
File tools and `cocos://asset/path/...` resources are sandboxed to the project root.
## Common Workflows
### 1. Orient: Understand the current project state
```json
// One call for a compact snapshot
{ "context": "editor", "code": "return await helpers.readResource('cocos://project/context');" }
```
Or use the specialist tool:
```json
{}
```
`get_editor_state` returns project info, runtime status, selection, scene summary, and windows.
### 2. Inspect scene hierarchy
```json
{
"context": "scene",
"code": "function walk(n, d=0) { const r = { name: n.name, active: n.active, components: n.components.map(c=>c.constructor.name) }; if (d < 3 && n.children.length) r.children = n.children.map(c => walk(c, d+1)); return r; } return walk(scene);"
}
```
Or use `get_hierarchy` with `{ "rootPath": "Canvas", "maxDepth": 3, "includeComponents": true }` for a structured snapshot.
### 3. Create UI (login page example)
```json
{
"context": "scene",
"code": "const { Node, UITransform, Sprite, Label, Button, Color, Vec3, find } = cc; const canvas = find('Canvas'); const bg = new Node('LoginBG'); bg.layer = canvas.layer; bg.addComponent(UITransform).setContentSize(720, 1280); bg.addComponent(Sprite).color = new Color(40, 40, 50, 255); canvas.addChild(bg); const title = new Node('Title'); title.layer = canvas.layer; title.addComponent(UITransform).setContentSize(400, 60); const tLabel = title.addComponent(Label); tLabel.string = 'Login'; tLabel.fontSize = 48; tLabel.color = Color.WHITE; title.setPosition(0, 400, 0); bg.addChild(title); return { created: ['LoginBG', 'Title'], canvas: canvas.name };"
}
```
After creation, verify:
1. `get_hierarchy` or `execute_javascript` to confirm structure
2. `capture_scene_screenshot` for visual proof
### 4. Set an asset reference (SpriteFrame, Texture, etc.)
**Cannot use `set_component_property` for asset references.** Use `execute_javascript` with `loadAny`:
```json
{
"context": "scene",
"code": "const { find, assetManager, SpriteFrame } = cc; const node = find('Canvas/Player/Sprite'); const sprite = node.getComponent('cc.Sprite'); const sf = await new Promise((resolve, reject) => { assetManager.loadAny({ uuid: 'SPRITEFRAME_UUID', type: SpriteFrame }, (err, asset) => { if (err) reject(err); else resolve(asset); }); }); sprite.spriteFrame = sf; return { node: node.name, spriteFrame: sf.name };"
}
```
### 5. Fix script errors
1. Run diagnostics: `run_script_diagnostics` → get error list
2. Get context: `get_script_diagnostic_context` → errors with source snippets
3. Fix the file: `execute_javascript` with `context="editor"` using `fs`/`path`, or use `replace_in_file`
4. Refresh assets: `refresh_assets`
5. Re-run diagnostics to confirm clean
### 6. Prefab workflow
1. Inspect: `inspect_prefab` with `{ "target": "db://assets/prefabs/Enemy.prefab" }`
2. Validate references: `validate_prefab_references`
3. Edit JSON directly: `edit_prefab_json` with `jsonPath` + `valueJson` or `search`/`replace`
4. Instantiate in scene: `create_prefab_instance` (linked) or `instantiate_prefab` (runtime)
5. After scene edits, apply back: `apply_prefab_instance`
### 7. Visual verification
```json
// Scene panel screenshot
{ "fileName": "after-ui-creation" }
```
`capture_scene_screenshot`
```json
// Preview/game screenshot
{ "fileName": "gameplay-check" }
```
`capture_preview_screenshot`
## MCP Resources Quick Reference
| Resource URI | What it gives you |
|---|---|
| `cocos://project/context` | Full project + editor context (project path, version, selection, scene) |
| `cocos://project/summary` | Project path, folder listing, script/prefab/scene counts |
| `cocos://scene/active` | Active scene summary (depth 3) |
| `cocos://selection/current` | Current node + asset selection |
| `cocos://errors/scripts` | Latest TypeScript diagnostics |
| `cocos://logs/editor` | Recent MCP runtime logs + tool interactions |
| `cocos://logs/project` | Tails of common project log files |
| `cocos://mcp/interactions` | Recent MCP tool call history |
| `cocos://mcp/execute-context` | Full execute_javascript variable & pattern guide |
**Resource templates:**
- `cocos://scene/node/{path}` — inspect a node by hierarchy path
- `cocos://asset/path/{relative_path}` — read a text/asset file by project-relative path
- `cocos://asset/info/{uuid_or_path}` — asset info + metadata by uuid or path
## MCP Prompts
Built-in workflow prompts (call via `prompts/get`):
| Prompt | Use when |
|---|---|
| `fix_script_errors` | Diagnosing and repairing TypeScript compile errors |
| `create_playable_prototype` | Building a gameplay prototype from scratch |
| `scene_validation` | Validating scene health, references, and runtime state |
| `auto_wire_scene` | Wiring up node references, event bindings, and component connections |
All prompts reinforce the `execute_javascript`-first philosophy.
## Key Pitfalls
1. **`set_component_property` cannot set asset references** (SpriteFrame, Texture, Material, Font, AudioClip). Use `execute_javascript` with `cc.assetManager.loadAny({uuid, type}, cb)`.
2. **`assetManager.loadAny(uuid)` without callback returns `null`** in scene context. Always use callback style:
```javascript
assetManager.loadAny({ uuid, type: SpriteFrame }, (err, asset) => { ... });
```
3. **Custom script components require compilation** before `add_component` or `find` can locate them. If a script was just created or modified, wait for Cocos to recompile or trigger a refresh first.
4. **Node hierarchy paths are slash-separated from scene root**, e.g. `"Canvas/Player/Sprite"`. Use `get_hierarchy` to verify paths before passing them to tools.
5. **`create_node` accepts `parentPath`, not `parentUuid`**. Resolve the path first with `get_hierarchy` or `find_nodes`.
6. **Always use `cc.` prefix** in scene context: `cc.Sprite`, `cc.Node`, `cc.UITransform`, `cc.Label`, `cc.Button`, etc. Bare names like `Sprite` or `Node` will fail.
7. **New nodes need `.layer` set** to match their parent, otherwise they won't render. Set `node.layer = parent.layer` before adding to hierarchy.
8. **Safety checks block absolute paths outside the project**. If you need to read an external file, use `execute_javascript` with `context="editor"` and pass `safety_checks: false` only after verifying the code is safe.
## Tool Profile Management
- **`core`** (default): 38 high-signal tools. Start here.
- **`full`**: All 110 tools including UI creation, node editing, components, animation, camera, input simulation, file I/O, runtime control, and more.
- **`custom`**: Start from `core`, add categories/tools via `enabledToolCategories` / `enabledTools`, remove via `disabledToolCategories` / `disabledTools`.
Check current exposure: `get_tool_catalog` → see every tool with profile, category, and enabled state.
If you need a `full`-only tool (e.g. `create_node`, `add_component`, `write_file`), tell the user to switch to `full` profile in the MCP panel (`Funplay > MCP Server`).
## Quick Start Checklist for Agents
1. **Verify connection** — call `get_project_info` or `get_editor_state`
2. **Read context** — `cocos://project/context` resource or `get_editor_state`
3. **Do the work** — prefer `execute_javascript` (scene for runtime, editor for automation)
4. **Verify results** — `validate_scene`, `run_script_diagnostics`, or screenshot tools
5. **Report** — summarize what was done, what was verified, and any issues found
+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. --> <!-- 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 Summary
| Profile | Tool Count | Purpose | | Profile | Tool Count | Purpose |
|---|---:|---| |---|---:|---|
| `core` | 37 | Focused default surface for common editor automation. | | `core` | 38 | Focused default surface for common editor automation. |
| `full` | 101 | All built-in tools, including destructive and low-level helpers. | | `full` | 110 | All built-in tools, including destructive and low-level helpers. |
## Core Tools ## 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 ## Tools By Category
@@ -30,7 +30,10 @@ Generated from `lib/tool-registry.js`. The default `core` profile exposes 37 too
| Tool | Profiles | Access | Description | | 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. | | `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` | `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. | | `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. | | `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 | | 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. | | `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. | | `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 | | 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. | | `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. | | `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. | | `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. | | `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 | | 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. | | `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. | | `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. | | `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_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. | | `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. | | `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. | | `set_node_transform` | `full` | mutating | Update node position, rotation, scale, or active state. |
### Screenshots ### 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) { function runExec(file, args, cwd) {
return new Promise((resolve) => { 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({ resolve({
code: error && typeof error.code === 'number' ? error.code : 0, code: error && typeof error.code === 'number' ? error.code : 0,
stdout: stdout || '', 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}`); 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 assetsRoot = path.join(projectPath, 'assets');
const relativeToAssets = path.relative(assetsRoot, targetPath); const relativeToAssets = path.relative(assetsRoot, targetPath);
if (relativeToAssets.startsWith('..') || path.isAbsolute(relativeToAssets)) { 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.'); 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 = { module.exports = {
applyPrefabInstance, applyPrefabInstance,
createPrefab,
duplicatePrefab, duplicatePrefab,
editPrefabJson, editPrefabJson,
inspectPrefab, 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/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://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/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); return this.getProjectLogsText(projectPath);
case 'cocos://mcp/interactions': case 'cocos://mcp/interactions':
return this.interactionLog.summary(); return this.interactionLog.summary();
case 'cocos://mcp/execute-context':
return this.getExecuteContextGuide();
default: default:
break; break;
} }
@@ -286,6 +289,100 @@ class ResourceProvider {
].join('\n')) ].join('\n'))
.join('\n\n---\n\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 = { module.exports = {
+56 -38
View File
@@ -31,6 +31,7 @@ const {
} = require('./project-instructions'); } = require('./project-instructions');
const { const {
applyPrefabInstance, applyPrefabInstance,
createPrefab,
duplicatePrefab, duplicatePrefab,
editPrefabJson, editPrefabJson,
inspectPrefab, inspectPrefab,
@@ -41,6 +42,8 @@ const { createAssetsAdvancedTools } = require('./tools/assets-advanced');
const { createCocosProjectTools } = require('./tools/cocos-project'); const { createCocosProjectTools } = require('./tools/cocos-project');
const { buildSnippet, createFileTools, refreshAssets } = require('./tools/files'); const { buildSnippet, createFileTools, refreshAssets } = require('./tools/files');
const { createSceneEventTools } = require('./tools/scene-events'); const { createSceneEventTools } = require('./tools/scene-events');
const { createSceneManagementTools } = require('./tools/scene-management');
const { createScriptTools } = require('./tools/scripts');
const { captureDesktopScreenshot, captureEditorWindowScreenshot, capturePanelScreenshot } = require('./screenshots'); const { captureDesktopScreenshot, captureEditorWindowScreenshot, capturePanelScreenshot } = require('./screenshots');
const { checkForUpdate } = require('./update-checker'); const { checkForUpdate } = require('./update-checker');
const { assertJavascriptSafety } = require('./javascript-safety'); 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.', 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( inputSchema: createSchema(
{ {
context: { type: 'string', description: 'Execution context: scene or editor.' }, 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 to execute. May directly return a value, define run(env), or export a function.' }, 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.' }, 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.' }, 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.', 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( 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.' }, 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.' }, 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.', 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( 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.' }, 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.' }, 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( inputSchema: createSchema(
{ {
name: { type: 'string', description: 'Name of the node to create.' }, 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}.' }, position: { type: 'object', description: 'Optional position {x,y,z}.' },
scale: { type: 'object', description: 'Optional scale {x,y,z}.' }, scale: { type: 'object', description: 'Optional scale {x,y,z}.' },
eulerAngles: { type: 'object', description: 'Optional rotation {x,y,z} in degrees.' }, 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.', description: 'Delete a node by path, uuid, or name.',
inputSchema: createSchema( inputSchema: createSchema(
{ {
path: { type: 'string', description: 'Hierarchy path.' }, path: { type: 'string', description: 'Hierarchy path, e.g. "Canvas/Player".' },
uuid: { type: 'string', description: 'Node uuid.' }, uuid: { type: 'string', description: 'Node uuid.' },
name: { type: 'string', description: 'Fallback exact node name.' }, 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.', description: 'Update node position, rotation, scale, or active state.',
inputSchema: createSchema( inputSchema: createSchema(
{ {
path: { type: 'string', description: 'Hierarchy path.' }, path: { type: 'string', description: 'Hierarchy path, e.g. "Canvas/Player".' },
uuid: { type: 'string', description: 'Node uuid.' }, uuid: { type: 'string', description: 'Node uuid.' },
name: { type: 'string', description: 'Fallback exact node name.' }, name: { type: 'string', description: 'Fallback exact node name.' },
position: { type: 'object', description: 'Position {x,y,z}.' }, position: { type: 'object', description: 'Position {x,y,z}.' },
@@ -715,6 +718,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
handler: async () => getRuntimeContext(), handler: async () => getRuntimeContext(),
}, },
...createCocosProjectTools({ createSchema }), ...createCocosProjectTools({ createSchema }),
...createSceneManagementTools({ createSchema, sceneBridge }),
{ {
name: 'list_scenes', name: 'list_scenes',
profile: 'core', profile: 'core',
@@ -757,6 +761,19 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
return { count: assets.length, prefabs: assets.slice(0, 200) }; 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', name: 'inspect_prefab',
profile: 'core', profile: 'core',
@@ -836,7 +853,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
inputSchema: createSchema( inputSchema: createSchema(
{ {
prefabUuid: { type: 'string', description: 'Prefab asset uuid, db url, or path.' }, 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.' }, name: { type: 'string', description: 'Optional override node name.' },
position: { type: 'object', description: 'Optional position {x,y,z}; fallback runtime path only.' }, 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.', description: '[specialist] Inspect whether a scene node is linked to a prefab instance and return prefab metadata when available.',
inputSchema: createSchema( 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.' }, uuid: { type: 'string', description: 'Node uuid.' },
name: { type: 'string', description: 'Fallback exact node name.' }, 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.', description: '[core] Apply a scene prefab instance back to its associated prefab asset using the Cocos editor scene apply-prefab message.',
inputSchema: createSchema( 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.' }, uuid: { type: 'string', description: 'Node uuid.' },
name: { type: 'string', description: 'Fallback exact node name.' }, 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.', description: '[core] Revert a scene prefab instance from its associated prefab asset using available Cocos editor prefab revert messages.',
inputSchema: createSchema( 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.' }, uuid: { type: 'string', description: 'Node uuid.' },
name: { type: 'string', description: 'Fallback exact node name.' }, name: { type: 'string', description: 'Fallback exact node name.' },
}, },
@@ -924,7 +941,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
inputSchema: createSchema( inputSchema: createSchema(
{ {
prefabUuid: { type: 'string', description: 'Prefab asset uuid.' }, 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.' }, name: { type: 'string', description: 'Optional override node name.' },
position: { type: 'object', description: 'Optional position {x,y,z}.' }, 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.', description: '[core] List components attached to a scene node.',
inputSchema: createSchema( 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.' }, uuid: { type: 'string', description: 'Node uuid.' },
name: { type: 'string', description: 'Fallback exact node name.' }, 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.', description: '[core] Inspect a component attached to a node.',
inputSchema: createSchema( 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.' }, uuid: { type: 'string', description: 'Node uuid.' },
name: { type: 'string', description: 'Fallback exact node name.' }, 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.' }, 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.', description: 'Add a component to a node by component class name.',
inputSchema: createSchema( 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.' }, uuid: { type: 'string', description: 'Node uuid.' },
name: { type: 'string', description: 'Fallback exact node name.' }, 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'] ['componentName']
), ),
@@ -1079,10 +1096,10 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
description: 'Remove a component from a node by name or index.', description: 'Remove a component from a node by name or index.',
inputSchema: createSchema( 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.' }, uuid: { type: 'string', description: 'Node uuid.' },
name: { type: 'string', description: 'Fallback exact node name.' }, 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.' }, 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.', description: 'Set a component property by dot path using a JSON value.',
inputSchema: createSchema( 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.' }, uuid: { type: 'string', description: 'Node uuid.' },
name: { type: 'string', description: 'Fallback exact node name.' }, 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.' }, 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".' },
valueJson: { type: 'string', description: 'JSON encoded value to assign, for example true, 12, \"hero\", or {\"x\":1}.' }, 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'] ['propertyPath', 'valueJson']
), ),
@@ -1121,12 +1138,12 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
description: 'Reset or clear a component property by dot path.', description: 'Reset or clear a component property by dot path.',
inputSchema: createSchema( 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.' }, uuid: { type: 'string', description: 'Node uuid.' },
name: { type: 'string', description: 'Fallback exact node name.' }, 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.' }, 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'] ['propertyPath']
), ),
@@ -1139,7 +1156,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
inputSchema: createSchema( inputSchema: createSchema(
{ {
name: { type: 'string', description: 'Canvas node name.' }, 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.' }, width: { type: 'number', description: 'Canvas width.' },
height: { type: 'number', description: 'Canvas height.' }, height: { type: 'number', description: 'Canvas height.' },
position: { type: 'object', description: 'Optional position {x,y,z}.' }, position: { type: 'object', description: 'Optional position {x,y,z}.' },
@@ -1155,7 +1172,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
inputSchema: createSchema( inputSchema: createSchema(
{ {
name: { type: 'string', description: 'Label node name.' }, 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.' }, text: { type: 'string', description: 'Label text.' },
fontSize: { type: 'number', description: 'Font size.' }, fontSize: { type: 'number', description: 'Font size.' },
width: { type: 'number', description: 'UI width.' }, width: { type: 'number', description: 'UI width.' },
@@ -1174,7 +1191,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
inputSchema: createSchema( inputSchema: createSchema(
{ {
name: { type: 'string', description: 'Button node name.' }, 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.' }, text: { type: 'string', description: 'Button text.' },
width: { type: 'number', description: 'Button width.' }, width: { type: 'number', description: 'Button width.' },
height: { type: 'number', description: 'Button height.' }, height: { type: 'number', description: 'Button height.' },
@@ -1194,8 +1211,8 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
inputSchema: createSchema( inputSchema: createSchema(
{ {
name: { type: 'string', description: 'Sprite node name.' }, name: { type: 'string', description: 'Sprite 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.' },
spriteFrameUuid: { type: 'string', description: 'Optional SpriteFrame asset uuid.' }, 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.' }, width: { type: 'number', description: 'UI width.' },
height: { type: 'number', description: 'UI height.' }, height: { type: 'number', description: 'UI height.' },
color: { type: 'string', description: 'Sprite color as #RRGGBB or #RRGGBBAA.' }, color: { type: 'string', description: 'Sprite color as #RRGGBB or #RRGGBBAA.' },
@@ -1219,7 +1236,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
inputSchema: createSchema( inputSchema: createSchema(
{ {
name: { type: 'string', description: 'Camera node name.' }, 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.' }, priority: { type: 'number', description: 'Camera priority.' },
visibility: { type: 'number', description: 'Camera visibility mask.' }, visibility: { type: 'number', description: 'Camera visibility mask.' },
clearFlags: { type: 'number', description: 'Camera clear flags.' }, 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.', description: 'Add an AnimationClip asset to a node Animation component.',
inputSchema: createSchema( 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.' }, uuid: { type: 'string', description: 'Node uuid.' },
name: { type: 'string', description: 'Fallback exact node name.' }, name: { type: 'string', description: 'Fallback exact node name.' },
clipUuid: { type: 'string', description: 'AnimationClip asset uuid.' }, 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.', description: '[core] Play an Animation component clip on a node.',
inputSchema: createSchema( 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.' }, uuid: { type: 'string', description: 'Node uuid.' },
name: { type: 'string', description: 'Fallback exact node name.' }, name: { type: 'string', description: 'Fallback exact node name.' },
clipName: { type: 'string', description: 'Optional clip 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.', description: '[core] Stop an Animation component clip on a node.',
inputSchema: createSchema( 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.' }, uuid: { type: 'string', description: 'Node uuid.' },
name: { type: 'string', description: 'Fallback exact node name.' }, name: { type: 'string', description: 'Fallback exact node name.' },
clipName: { type: 'string', description: 'Optional clip 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.', description: '[core] Emit a custom event on a target scene node with an optional JSON payload.',
inputSchema: createSchema( 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.' }, uuid: { type: 'string', description: 'Node uuid.' },
name: { type: 'string', description: 'Fallback exact node name.' }, name: { type: 'string', description: 'Fallback exact node name.' },
eventName: { type: 'string', description: 'Event name to emit.' }, 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), handler: async (args) => sceneBridge.call('simulateButtonClick', args),
}, },
...createSceneEventTools({ createSchema, sceneBridge }), ...createSceneEventTools({ createSchema, sceneBridge }),
...createScriptTools({ createSchema, getRuntimeContext }),
{ {
name: 'invoke_component_method', name: 'invoke_component_method',
profile: 'full', profile: 'full',
description: '[core] Invoke a method on a component for runtime validation and test hooks.', description: '[core] Invoke a method on a component for runtime validation and test hooks.',
inputSchema: createSchema( 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.' }, uuid: { type: 'string', description: 'Node uuid.' },
name: { type: 'string', description: 'Fallback exact node name.' }, 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.' }, index: { type: 'number', description: 'Optional component index.' },
methodName: { type: 'string', description: 'Method name to invoke.' }, methodName: { type: 'string', description: 'Method name to invoke.' },
args: { type: 'array', description: 'Optional argument array.' }, args: { type: 'array', description: 'Optional argument array.' },
+182
View File
@@ -97,8 +97,12 @@ function collectUuidReferences(content) {
async function inspectAssetDependencies(projectPath, target, options = {}) { async function inspectAssetDependencies(projectPath, target, options = {}) {
const info = await queryAssetInfo(target); const info = await queryAssetInfo(target);
const isDirectory = info && (info.isDirectory || info.type === 'folder' || (info.url && !path.extname(info.url)));
const filePath = assetFilePath(projectPath, info); const filePath = assetFilePath(projectPath, info);
if (!filePath) { 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}`); throw new Error(`Asset file was not found: ${target}`);
} }
@@ -210,12 +214,190 @@ function createAssetsAdvancedTools({ createSchema, getRuntimeContext }) {
return await validateAssetDependencies(projectPath, args); 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 = { module.exports = {
collectUuidReferences, collectUuidReferences,
createAssetsAdvancedTools, createAssetsAdvancedTools,
findUnusedAssets,
inspectAssetDependencies, inspectAssetDependencies,
validateAssetDependencies, validateAssetDependencies,
}; };
+1
View File
@@ -174,6 +174,7 @@ function createCocosProjectTools({ createSchema }) {
[] []
), ),
handler: async (args) => await tryEditorRequests([ handler: async (args) => await tryEditorRequests([
{ channel: 'preview', method: 'open', args: [args || {}] },
{ channel: 'preview', method: 'start', args: [args || {}] }, { channel: 'preview', method: 'start', args: [args || {}] },
{ channel: 'preview', method: 'open-preview', args: [args || {}] }, { channel: 'preview', method: 'open-preview', args: [args || {}] },
{ channel: 'builder', method: '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/", "docs/",
"lib/", "lib/",
"panel/", "panel/",
"resources/",
"browser.js", "browser.js",
"scene.js", "scene.js",
"server.json", "server.json",
@@ -49,7 +50,7 @@
"access": "public" "access": "public"
}, },
"scripts": { "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", "test": "node --test",
"docs:generate": "node scripts/generate-tool-docs.js", "docs:generate": "node scripts/generate-tool-docs.js",
"docs:check": "node scripts/generate-tool-docs.js --check", "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 = {}) { async setNodeTransform(options = {}) {
const node = findNode(options); const node = findNode(options);
if (!node) { if (!node) {
@@ -847,7 +923,7 @@ exports.methods = {
const component = findComponent(node, options); const component = findComponent(node, options);
if (!component) { 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'; const componentName = component.constructor ? component.constructor.name : 'UnknownComponent';
@@ -867,7 +943,7 @@ exports.methods = {
const component = findComponent(node, options); const component = findComponent(node, options);
if (!component) { 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 { return {
@@ -892,7 +968,7 @@ exports.methods = {
const component = findComponent(node, options); const component = findComponent(node, options);
if (!component) { 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); setValueByPath(component, options.propertyPath, options.value);
@@ -913,7 +989,7 @@ exports.methods = {
const component = findComponent(node, options); const component = findComponent(node, options);
if (!component) { 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); resetValueByPath(component, options.propertyPath);
@@ -1410,7 +1486,7 @@ exports.methods = {
const component = findComponent(target, { componentName }); const component = findComponent(target, { componentName });
if (!component) { 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') { if (typeof component[handlerName] !== 'function') {
throw new Error(`Target component method was not found: ${componentName}.${handlerName}`); throw new Error(`Target component method was not found: ${componentName}.${handlerName}`);
@@ -1465,7 +1541,7 @@ exports.methods = {
} }
const component = findComponent(node, options); const component = findComponent(node, options);
if (!component) { 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(); const methodName = String(options.methodName || '').trim();
if (!methodName || typeof component[methodName] !== 'function') { 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', () => { test('core profile exposes the documented focused tool set', () => {
const tools = createRegistry('core').listTools(); 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 === 'execute_javascript'), true);
assert.equal(tools.some((tool) => tool.name === 'get_editor_state'), true); assert.equal(tools.some((tool) => tool.name === 'get_editor_state'), true);
assert.equal(tools.some((tool) => tool.name === 'get_tool_catalog'), 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 === 'get_performance_snapshot'), true);
assert.equal(tools.some((tool) => tool.name === 'list_project_instructions'), 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 === 'set_selection'), true);
assert.equal(tools.some((tool) => tool.name === 'query_scene_state'), true);
assert.equal(tools.some((tool) => tool.name === 'write_file'), false); assert.equal(tools.some((tool) => tool.name === 'write_file'), false);
}); });
test('full profile exposes all built-in tools', () => { test('full profile exposes all built-in tools', () => {
const tools = createRegistry('full').listTools(); 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 === 'write_file'), true);
assert.equal(tools.some((tool) => tool.name === 'edit_prefab_json'), true); assert.equal(tools.some((tool) => tool.name === 'edit_prefab_json'), true);
assert.equal(tools.some((tool) => tool.name === 'create_project_skill'), true); assert.equal(tools.some((tool) => tool.name === 'create_project_skill'), true);