chore(mcp): 换用 cocos-mcp-server 扩展, .mcp.json 改 HTTP 直连
旧扩展 @iflow-mcp/harady-cocos-creator-mcp(164 个细粒度工具 + stdio-bridge
转发)整个删除, 换成 cocos-mcp-server v1.7.9(16 个聚合工具, 靠 action 分派)。
新扩展自身起 streamable-http 服务, 不再需要 stdio 桥, 故 .mcp.json 从
node + stdio-bridge.js 改为 { type: "http", url: "http://127.0.0.1:3000/mcp" }。
原配置指向的 client/stdio-bridge.js 已随旧扩展删除, MCP 因此加载失败。
服务器名保留 cocos-creator-mcp(而非 INSTALL.md 建议的 cocos-creator), 以免
打断 CLAUDE.md、cocos-mcp 技能与 settings.local.json 里的
mcp__cocos-creator-mcp__* 允许规则。
gitignore: 新扩展是商业付费产品(vberai), 非 npm 可重装, dist/sidecar/static
共约 16M 发行产物+加密载荷+平台二进制不入库; 顺带忽略 Claude Code 个人本地
配置与编辑器运行时端口文件。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+13
-4
@@ -31,10 +31,19 @@ yarn-error.log*
|
|||||||
cocoscreator_projects/games/*/assets/framework
|
cocoscreator_projects/games/*/assets/framework
|
||||||
cocoscreator_projects/games/*/assets/framework.meta
|
cocoscreator_projects/games/*/assets/framework.meta
|
||||||
|
|
||||||
# ---------- cocos-creator-mcp 扩展:本地依赖与构建产物 ----------
|
# ---------- cocos-mcp-server 扩展:构建产物与二进制 ----------
|
||||||
# clone 后在该目录跑 `npm install && npm run build` 重新生成
|
# 商业付费扩展(vberai),非 npm 可重装:dist/sidecar/static 是发行产物+加密载荷+
|
||||||
cocoscreator_projects/YouleNexus/extensions/cocos-creator-mcp/node_modules/
|
# 平台二进制,约 16M,不入库。换机器时从购买渠道重新拷贝整个扩展目录。
|
||||||
cocoscreator_projects/YouleNexus/extensions/cocos-creator-mcp/dist/
|
cocoscreator_projects/YouleNexus/extensions/cocos-mcp-server/dist/
|
||||||
|
cocoscreator_projects/YouleNexus/extensions/cocos-mcp-server/sidecar/
|
||||||
|
cocoscreator_projects/YouleNexus/extensions/cocos-mcp-server/static/
|
||||||
|
|
||||||
|
# ---------- Claude Code 个人本地配置 ----------
|
||||||
|
# .claude/skills/ 入库(团队共享),本地权限覆盖不入库
|
||||||
|
.claude/settings.local.json
|
||||||
|
|
||||||
|
# 运行时状态(编辑器每次启动重写)
|
||||||
|
cocoscreator_projects/YouleNexus/settings/.mcp-port.json
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# 说明:以下内容【应当】入库,勿忽略——
|
# 说明:以下内容【应当】入库,勿忽略——
|
||||||
|
|||||||
@@ -1,13 +1,8 @@
|
|||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"cocos-creator-mcp": {
|
"cocos-creator-mcp": {
|
||||||
"command": "node",
|
"type": "http",
|
||||||
"args": [
|
"url": "http://127.0.0.1:3000/mcp"
|
||||||
"G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/extensions/cocos-creator-mcp/client/stdio-bridge.js"
|
|
||||||
],
|
|
||||||
"env": {
|
|
||||||
"COCOS_MCP_URL": "http://127.0.0.1:3000/mcp"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2026 Yusuke Harada
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@@ -1,539 +0,0 @@
|
|||||||
# Cocos Creator MCP
|
|
||||||
|
|
||||||
[MCP (Model Context Protocol)](https://modelcontextprotocol.io/) server extension for Cocos Creator 3.8+.
|
|
||||||
|
|
||||||
AI assistants like Claude can control Cocos Creator editor through this extension — creating nodes, editing scenes, managing prefabs, building projects, and more.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- **164 Tools** across 13 categories — comprehensive editor automation
|
|
||||||
- **Streamable HTTP (SSE)** — Native support for MCP's Streamable HTTP transport
|
|
||||||
- **JSON-RPC 2.0** — Standard MCP protocol compliance
|
|
||||||
- **Prefab Property Persistence** — Component properties are correctly preserved when saving prefabs
|
|
||||||
- **Preview in Editor** — Start editor preview programmatically (no manual button click needed)
|
|
||||||
- **Screenshot Capture** — Capture editor window and game preview screenshots (WebP / PNG)
|
|
||||||
- **Video Recording** — Record game preview canvas to video (MP4 / WebM) via Preview Recorder panel
|
|
||||||
- **Game Command Control** — Send commands to running game preview (screenshot, click, navigate, state, inspect)
|
|
||||||
- **Client Scripts** — Drop-in TypeScript files for game preview integration (`client/`)
|
|
||||||
- **Auto Start** — Server starts automatically when the extension loads
|
|
||||||
- **Tool Call Logging** — All tool invocations logged with timing for debugging
|
|
||||||
- **UUID Validation** — Input validation helpers for better error messages
|
|
||||||
- **i18n** — English, Japanese, Chinese
|
|
||||||
- **Regression Tests** — 200+ assertions covering core tool flows
|
|
||||||
|
|
||||||
## Quick Start
|
|
||||||
|
|
||||||
### 1. Install
|
|
||||||
|
|
||||||
Copy or symlink this extension into your Cocos Creator project's `extensions/` directory:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Windows (Junction — no admin required)
|
|
||||||
mklink /J "your-project\extensions\cocos-creator-mcp" "path\to\cocos-creator-mcp"
|
|
||||||
|
|
||||||
# macOS / Linux
|
|
||||||
ln -s /path/to/cocos-creator-mcp your-project/extensions/cocos-creator-mcp
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Build
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd cocos-creator-mcp
|
|
||||||
npm install
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Enable in Cocos Creator
|
|
||||||
|
|
||||||
1. Open your project in Cocos Creator
|
|
||||||
2. Go to **Extension > Extension Manager**
|
|
||||||
3. Enable **Cocos Creator MCP**
|
|
||||||
4. Open the panel: **Extension > Cocos Creator MCP > Open Panel**
|
|
||||||
5. Click **Start Server** (or set `autoStart: true` in config)
|
|
||||||
|
|
||||||
### 4. Connect from Claude Code
|
|
||||||
|
|
||||||
Pick one of the two transports below.
|
|
||||||
|
|
||||||
#### Option A — stdio bridge (recommended for Claude Code VSCode extension)
|
|
||||||
|
|
||||||
The Claude Code VSCode extension currently has a bug where it unconditionally
|
|
||||||
tries OAuth Dynamic Client Registration for HTTP-type MCP servers and fails
|
|
||||||
with `SDK auth failed` (see upstream issues
|
|
||||||
[#26917](https://github.com/anthropics/claude-code/issues/26917),
|
|
||||||
[#38102](https://github.com/anthropics/claude-code/issues/38102),
|
|
||||||
[#29697](https://github.com/anthropics/claude-code/issues/29697)).
|
|
||||||
To avoid it entirely, use the bundled stdio bridge. It speaks JSON-RPC on
|
|
||||||
stdin/stdout and forwards to the HTTP server internally.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"cocos-creator-mcp": {
|
|
||||||
"command": "node",
|
|
||||||
"args": [
|
|
||||||
"<ABSOLUTE_PATH_TO>/cocos-creator-mcp/client/stdio-bridge.js"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Optional env var: `COCOS_MCP_URL` (default `http://127.0.0.1:3000/mcp`).
|
|
||||||
|
|
||||||
#### Option B — direct HTTP
|
|
||||||
|
|
||||||
Works with Claude Code CLI, Cursor, Cline, and other clients that don't force
|
|
||||||
OAuth on HTTP MCP. The server ships minimal dummy OAuth endpoints
|
|
||||||
(`/.well-known/oauth-*`, `/oauth/register|authorize|token`) so OAuth-requiring
|
|
||||||
clients can still complete a pro-forma flow on localhost.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"cocos-creator-mcp": {
|
|
||||||
"type": "http",
|
|
||||||
"url": "http://127.0.0.1:3000/mcp"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
> The dummy OAuth endpoints will be removed once upstream issues
|
|
||||||
> ([#26917](https://github.com/anthropics/claude-code/issues/26917),
|
|
||||||
> [#38102](https://github.com/anthropics/claude-code/issues/38102))
|
|
||||||
> are resolved or real authentication is introduced.
|
|
||||||
|
|
||||||
### 5. Verify
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl http://127.0.0.1:3000/health
|
|
||||||
# {"status":"ok","tools":164}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Available Tools (164)
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary><strong>Scene (6)</strong> — Scene lifecycle and hierarchy</summary>
|
|
||||||
|
|
||||||
| Tool | Description |
|
|
||||||
|------|-------------|
|
|
||||||
| `scene_get_hierarchy` | Get the node tree (with optional component info) |
|
|
||||||
| `scene_open` | Open a scene by UUID or db:// path |
|
|
||||||
| `scene_save` | Save the current scene |
|
|
||||||
| `scene_get_list` | List all .scene files |
|
|
||||||
| `scene_close` | Close the current scene |
|
|
||||||
| `scene_get_current` | Get name and UUID of the current scene |
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary><strong>Scene Advanced (30)</strong> — Undo, clipboard, queries, property manipulation</summary>
|
|
||||||
|
|
||||||
| Tool | Description |
|
|
||||||
|------|-------------|
|
|
||||||
| `scene_execute_script` | Execute a custom scene script method |
|
|
||||||
| `scene_snapshot` | Take a snapshot for undo |
|
|
||||||
| `scene_snapshot_abort` | Abort the current undo snapshot |
|
|
||||||
| `scene_begin_undo` | Begin recording undo operations |
|
|
||||||
| `scene_end_undo` | End undo recording |
|
|
||||||
| `scene_cancel_undo` | Cancel undo recording |
|
|
||||||
| `scene_query_dirty` | Check if scene has unsaved changes |
|
|
||||||
| `scene_query_ready` | Check if scene is fully loaded |
|
|
||||||
| `scene_query_classes` | List all available component classes |
|
|
||||||
| `scene_query_components` | Query components for a node |
|
|
||||||
| `scene_query_component_has_script` | Check if a component has a script file |
|
|
||||||
| `scene_query_node_tree` | Get raw node tree from editor |
|
|
||||||
| `scene_query_node` | Get full property dump of a node |
|
|
||||||
| `scene_query_component` | Get full property dump of a component |
|
|
||||||
| `scene_query_nodes_by_asset` | Find nodes referencing an asset |
|
|
||||||
| `scene_query_scene_bounds` | Get scene bounding rect |
|
|
||||||
| `scene_soft_reload` | Soft reload scene |
|
|
||||||
| `scene_reset_node_transform` | Reset transform to default |
|
|
||||||
| `scene_reset_property` | Reset a specific property to default |
|
|
||||||
| `scene_reset_component` | Reset a component to defaults |
|
|
||||||
| `scene_copy_node` | Copy node to clipboard |
|
|
||||||
| `scene_paste_node` | Paste node from clipboard |
|
|
||||||
| `scene_cut_node` | Cut node to clipboard |
|
|
||||||
| `scene_create` | Create a new empty scene |
|
|
||||||
| `scene_save_as` | Save scene to a new file |
|
|
||||||
| `scene_set_parent` | Reparent node(s) with official API |
|
|
||||||
| `scene_restore_prefab` | Restore prefab node to original state |
|
|
||||||
| `scene_execute_component_method` | Call a method on a component |
|
|
||||||
| `scene_move_array_element` | Reorder array property element |
|
|
||||||
| `scene_remove_array_element` | Remove array property element |
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary><strong>Scene View (19)</strong> — Gizmo, camera, grid, viewport</summary>
|
|
||||||
|
|
||||||
| Tool | Description |
|
|
||||||
|------|-------------|
|
|
||||||
| `view_change_gizmo_tool` | Switch gizmo tool (move/rotate/scale/rect) |
|
|
||||||
| `view_query_gizmo_tool` | Get current gizmo tool |
|
|
||||||
| `view_change_gizmo_pivot` | Change pivot mode (center/pivot) |
|
|
||||||
| `view_query_gizmo_pivot` | Get current pivot mode |
|
|
||||||
| `view_change_gizmo_coordinate` | Change coordinate system (local/global) |
|
|
||||||
| `view_query_gizmo_coordinate` | Get current coordinate system |
|
|
||||||
| `view_change_mode_2d_3d` | Switch 2D/3D view |
|
|
||||||
| `view_query_mode_2d_3d` | Get current view mode |
|
|
||||||
| `view_set_grid_visible` | Show/hide grid |
|
|
||||||
| `view_query_grid_visible` | Check grid visibility |
|
|
||||||
| `view_set_icon_gizmo_3d` | Toggle 3D icon gizmos |
|
|
||||||
| `view_query_icon_gizmo_3d` | Check 3D icon gizmo state |
|
|
||||||
| `view_set_icon_gizmo_size` | Set icon gizmo size |
|
|
||||||
| `view_query_icon_gizmo_size` | Get icon gizmo size |
|
|
||||||
| `view_focus_on_node` | Focus camera on node(s) |
|
|
||||||
| `view_align_with_view` | Align node with camera view |
|
|
||||||
| `view_align_view_with_node` | Align camera with node |
|
|
||||||
| `view_get_status` | Get all view settings at once |
|
|
||||||
| `view_reset` | Reset scene view to default |
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary><strong>Node (14)</strong> — Create, edit, move, delete nodes</summary>
|
|
||||||
|
|
||||||
| Tool | Description |
|
|
||||||
|------|-------------|
|
|
||||||
| `node_create` | Create node (with optional components) |
|
|
||||||
| `node_get_info` | Get node details (position, scale, components) |
|
|
||||||
| `node_find_by_name` | Find nodes by name |
|
|
||||||
| `node_set_property` | Set node property |
|
|
||||||
| `node_set_transform` | Set position/rotation/scale at once |
|
|
||||||
| `node_set_active` | Set node visibility |
|
|
||||||
| `node_set_layer` | Set node layer |
|
|
||||||
| `node_delete` | Delete node |
|
|
||||||
| `node_move` | Move node to new parent |
|
|
||||||
| `node_duplicate` | Duplicate node |
|
|
||||||
| `node_get_all` | List all nodes |
|
|
||||||
| `node_detect_type` | Detect node type (2D/3D/Node) |
|
|
||||||
| `node_create_tree` | Create a node hierarchy in one call (v1.6) |
|
|
||||||
| `node_set_layout` | Set UITransform + Widget + color/opacity at once (v1.13) |
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary><strong>Component (8)</strong> — Add, remove, configure components</summary>
|
|
||||||
|
|
||||||
| Tool | Description |
|
|
||||||
|------|-------------|
|
|
||||||
| `component_add` | Add component (e.g. `cc.Label`, `cc.Sprite`) |
|
|
||||||
| `component_remove` | Remove component |
|
|
||||||
| `component_get_components` | List components on node |
|
|
||||||
| `component_set_property` | Set component property (Label.string, fontSize, etc.) |
|
|
||||||
| `component_get_info` | Get full component dump by UUID |
|
|
||||||
| `component_get_available` | List all available component classes |
|
|
||||||
| `component_auto_bind` | Auto-match `@property` fields to nodes by name (v1.12) |
|
|
||||||
| `component_query_enum` | Query enum values of a component property (v1.6) |
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary><strong>Prefab (12)</strong> — Prefab lifecycle and validation</summary>
|
|
||||||
|
|
||||||
| Tool | Description |
|
|
||||||
|------|-------------|
|
|
||||||
| `prefab_list` | List all prefabs |
|
|
||||||
| `prefab_create` | Create prefab from node (properties preserved) |
|
|
||||||
| `prefab_instantiate` | Instantiate prefab into scene |
|
|
||||||
| `prefab_get_info` | Get prefab asset info |
|
|
||||||
| `prefab_update` | Apply prefab changes |
|
|
||||||
| `prefab_revert` | Revert prefab instance to original |
|
|
||||||
| `prefab_duplicate` | Copy prefab to new path |
|
|
||||||
| `prefab_validate` | Validate prefab for broken references |
|
|
||||||
| `prefab_open` | Open prefab for editing (v1.5) |
|
|
||||||
| `prefab_close` | Close prefab editing mode and return to scene (v1.5) |
|
|
||||||
| `prefab_create_and_replace` | Create prefab and replace instance in one call (v1.5) |
|
|
||||||
| `prefab_create_from_spec` | Create node tree + auto-bind + prefab_create in one call (v1.12) |
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary><strong>Asset (18)</strong> — CRUD, queries, metadata, dependencies</summary>
|
|
||||||
|
|
||||||
| Tool | Description |
|
|
||||||
|------|-------------|
|
|
||||||
| `asset_create` | Create new asset |
|
|
||||||
| `asset_delete` | Delete asset |
|
|
||||||
| `asset_move` | Move/rename asset |
|
|
||||||
| `asset_copy` | Copy asset |
|
|
||||||
| `asset_save` | Save asset |
|
|
||||||
| `asset_reimport` | Re-import asset |
|
|
||||||
| `asset_import` | Import external file into project |
|
|
||||||
| `asset_query_path` | Get file path for UUID |
|
|
||||||
| `asset_query_uuid` | Get UUID for path |
|
|
||||||
| `asset_query_url` | Get URL for UUID |
|
|
||||||
| `asset_get_details` | Get asset metadata |
|
|
||||||
| `asset_get_dependencies` | Get asset dependencies |
|
|
||||||
| `asset_open_external` | Open in external editor |
|
|
||||||
| `asset_save_meta` | Save asset meta/importer settings |
|
|
||||||
| `asset_generate_available_url` | Generate non-conflicting asset path |
|
|
||||||
| `asset_query_ready` | Check if asset DB is ready |
|
|
||||||
| `asset_query_users` | Find assets that reference this asset |
|
|
||||||
| `asset_query_missing` | Check for missing references |
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary><strong>Project (8)</strong> — Project info, settings, engine</summary>
|
|
||||||
|
|
||||||
| Tool | Description |
|
|
||||||
|------|-------------|
|
|
||||||
| `project_get_info` | Get project name and path |
|
|
||||||
| `project_refresh_assets` | Refresh asset database |
|
|
||||||
| `project_get_asset_info` | Get asset info by UUID |
|
|
||||||
| `project_find_asset` | Find assets by glob pattern |
|
|
||||||
| `project_get_settings` | Get project settings |
|
|
||||||
| `project_set_settings` | Set a project setting |
|
|
||||||
| `project_get_engine_info` | Get engine version and paths |
|
|
||||||
| `project_query_scripts` | Query all script plugins |
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary><strong>Preferences (4)</strong> — Editor preferences</summary>
|
|
||||||
|
|
||||||
| Tool | Description |
|
|
||||||
|------|-------------|
|
|
||||||
| `preferences_get` | Get preference value |
|
|
||||||
| `preferences_set` | Set preference value |
|
|
||||||
| `preferences_get_all` | Get all preferences for a protocol |
|
|
||||||
| `preferences_reset` | Reset preference to default |
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary><strong>Debug (22)</strong> — Editor info, logs, preview, screenshots, recording, game control</summary>
|
|
||||||
|
|
||||||
| Tool | Description |
|
|
||||||
|------|-------------|
|
|
||||||
| `debug_get_editor_info` | Get editor version and environment |
|
|
||||||
| `debug_list_messages` | List available Editor messages |
|
|
||||||
| `debug_execute_script` | Execute scene script method |
|
|
||||||
| `debug_get_console_logs` | Get console log entries (scene + game preview) |
|
|
||||||
| `debug_clear_console` | Clear editor console and log buffers |
|
|
||||||
| `debug_preview` | Start Preview in Editor (play button) |
|
|
||||||
| `debug_clear_code_cache` | Clear code cache (Developer > Cache) |
|
|
||||||
| `debug_screenshot` | Capture editor window screenshot |
|
|
||||||
| `debug_game_command` | Send command to game preview (screenshot/click/navigate/state/inspect) |
|
|
||||||
| `debug_batch_screenshot` | Navigate to multiple pages and screenshot each |
|
|
||||||
| `debug_record_start` | Start recording game preview canvas (webm/mp4) |
|
|
||||||
| `debug_record_stop` | Stop recording and save video file |
|
|
||||||
| `debug_reload_extension` | Reload this MCP extension (after build) |
|
|
||||||
| `debug_list_extensions` | List installed extensions |
|
|
||||||
| `debug_get_extension_info` | Get extension details |
|
|
||||||
| `debug_get_project_logs` | Read project log entries |
|
|
||||||
| `debug_search_project_logs` | Search patterns in project logs |
|
|
||||||
| `debug_get_log_file_info` | Get log file metadata |
|
|
||||||
| `debug_validate_scene` | Validate scene for common issues |
|
|
||||||
| `debug_query_devices` | List connected devices |
|
|
||||||
| `debug_open_url` | Open URL in system browser |
|
|
||||||
| `debug_wait_compile` | Wait for TypeScript compile to finish (v1.12) |
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary><strong>Server (7)</strong> — Editor server and network</summary>
|
|
||||||
|
|
||||||
| Tool | Description |
|
|
||||||
|------|-------------|
|
|
||||||
| `server_query_ip_list` | Get editor server IPs |
|
|
||||||
| `server_query_port` | Get editor server port |
|
|
||||||
| `server_get_status` | Get full server status |
|
|
||||||
| `server_check_connectivity` | Check if editor server is reachable |
|
|
||||||
| `server_get_network_interfaces` | Get network interface details |
|
|
||||||
| `server_get_build_hash` | Get build hash of MCP dist files (v1.6) |
|
|
||||||
| `server_check_code_sync` | Check if runtime matches dist hash (v1.6) |
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary><strong>Builder (5)</strong> — Build and preview</summary>
|
|
||||||
|
|
||||||
| Tool | Description |
|
|
||||||
|------|-------------|
|
|
||||||
| `builder_open_panel` | Open Build panel |
|
|
||||||
| `builder_get_settings` | Get build configuration |
|
|
||||||
| `builder_query_tasks` | Query active build tasks |
|
|
||||||
| `builder_run_preview` | Start preview server |
|
|
||||||
| `builder_stop_preview` | Stop preview server |
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary><strong>Reference Image (11)</strong> — Scene overlay images</summary>
|
|
||||||
|
|
||||||
| Tool | Description |
|
|
||||||
|------|-------------|
|
|
||||||
| `refimage_add` | Add a reference image |
|
|
||||||
| `refimage_remove` | Remove a reference image |
|
|
||||||
| `refimage_list` | List all reference images |
|
|
||||||
| `refimage_clear_all` | Remove all reference images |
|
|
||||||
| `refimage_switch` | Switch active reference image |
|
|
||||||
| `refimage_set_position` | Set image position |
|
|
||||||
| `refimage_set_scale` | Set image scale |
|
|
||||||
| `refimage_set_opacity` | Set image opacity |
|
|
||||||
| `refimage_query_config` | Get reference image config |
|
|
||||||
| `refimage_query_current` | Get current active image info |
|
|
||||||
| `refimage_refresh` | Refresh image display |
|
|
||||||
</details>
|
|
||||||
|
|
||||||
## Client Scripts
|
|
||||||
|
|
||||||
The `client/` directory contains TypeScript files for runtime communication between the game preview and the MCP server. Since the extension is installed in `extensions/`, these files can be imported directly — no copying needed.
|
|
||||||
|
|
||||||
### McpConsoleCapture
|
|
||||||
|
|
||||||
Captures `console.log/warn/error` from the game preview and sends them to the MCP server.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Import from extensions/ (adjust relative path as needed)
|
|
||||||
import { initMcpConsoleCapture } from "../../extensions/cocos-creator-mcp/client/McpConsoleCapture";
|
|
||||||
initMcpConsoleCapture();
|
|
||||||
```
|
|
||||||
|
|
||||||
### McpDebugClient
|
|
||||||
|
|
||||||
Enables AI-driven game control: screenshots, node clicking, and custom commands.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { initMcpDebugClient } from "../../extensions/cocos-creator-mcp/client/McpDebugClient";
|
|
||||||
|
|
||||||
initMcpDebugClient({
|
|
||||||
customCommands: {
|
|
||||||
// Add project-specific commands
|
|
||||||
state: () => ({ success: true, data: { dump: MyDb.dump() } }),
|
|
||||||
navigate: async (args) => {
|
|
||||||
await MyRouter.goTo(args.page);
|
|
||||||
return { success: true };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
**Built-in commands** (no setup needed):
|
|
||||||
- `screenshot` — Capture game screen via RenderTexture
|
|
||||||
- `click` — Click a node by name
|
|
||||||
|
|
||||||
**Custom commands** (project-specific):
|
|
||||||
- Register any handler via `customCommands` option
|
|
||||||
- Called via `debug_game_command` MCP tool
|
|
||||||
|
|
||||||
Both scripts silently ignore when the MCP server is not running, so they are safe to leave in development builds.
|
|
||||||
|
|
||||||
## Console Log Capture (Details)
|
|
||||||
|
|
||||||
`debug_get_console_logs` captures logs from two sources:
|
|
||||||
|
|
||||||
### Scene Process Logs (automatic)
|
|
||||||
|
|
||||||
Console output from scene scripts (`console.log/warn/error` in the scene renderer process) is automatically captured. No setup required.
|
|
||||||
|
|
||||||
### Game Preview Logs (opt-in)
|
|
||||||
|
|
||||||
Game code runs in a browser during preview, which is a separate process. To capture game preview logs, your game code needs to send logs to the MCP server's `/log` endpoint.
|
|
||||||
|
|
||||||
**Setup:**
|
|
||||||
|
|
||||||
Add a console capture script to your game project:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const MCP_LOG_URL = "http://127.0.0.1:3000/log";
|
|
||||||
const FLUSH_INTERVAL = 500;
|
|
||||||
|
|
||||||
let buffer: Array<{ timestamp: string; level: string; message: string }> = [];
|
|
||||||
|
|
||||||
function hook(level: string, original: (...args: any[]) => void) {
|
|
||||||
return function (...args: any[]) {
|
|
||||||
original.apply(console, args);
|
|
||||||
buffer.push({
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
level,
|
|
||||||
message: args.map(a => typeof a === "string" ? a : JSON.stringify(a)).join(" "),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log = hook("log", console.log);
|
|
||||||
console.warn = hook("warn", console.warn);
|
|
||||||
console.error = hook("error", console.error);
|
|
||||||
|
|
||||||
setInterval(() => {
|
|
||||||
if (buffer.length === 0) return;
|
|
||||||
const entries = buffer.splice(0, 50);
|
|
||||||
fetch(MCP_LOG_URL, {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify(entries),
|
|
||||||
}).catch(() => {}); // silently ignore if MCP server is not running
|
|
||||||
}, FLUSH_INTERVAL);
|
|
||||||
```
|
|
||||||
|
|
||||||
**`POST /log` format:**
|
|
||||||
|
|
||||||
```json
|
|
||||||
[
|
|
||||||
{ "timestamp": "2026-03-26T12:00:00.000Z", "level": "log", "message": "Hello" },
|
|
||||||
{ "timestamp": "2026-03-26T12:00:01.000Z", "level": "error", "message": "Something failed" }
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
Both scene and game logs are merged chronologically when retrieved via `debug_get_console_logs`. Each log entry includes a `source` field (`"scene"` or `"game"`) to distinguish the origin.
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
Settings are stored in `{project}/settings/cocos-creator-mcp.json`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"port": 3000,
|
|
||||||
"autoStart": true
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
| Option | Default | Description |
|
|
||||||
|--------|---------|-------------|
|
|
||||||
| `port` | `3000` | HTTP server port |
|
|
||||||
| `autoStart` | `false` | Start server automatically when extension loads |
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
```bash
|
|
||||||
node test/regression.mjs # default port 3000
|
|
||||||
node test/regression.mjs 3001 # custom port
|
|
||||||
```
|
|
||||||
|
|
||||||
## Version History
|
|
||||||
|
|
||||||
- **v0.1** — MCP server + scene/node tools (13 tools)
|
|
||||||
- **v0.5** — Component, prefab, project, debug tools (27 tools)
|
|
||||||
- **v1.0** — Full tool coverage (145 tools, 13 categories, 224 test assertions)
|
|
||||||
- **v1.1** — Console log capture (scene process auto-capture + game preview via `/log` endpoint)
|
|
||||||
- **v1.2** — AI autonomous development: Preview in Editor, screenshot capture, game command control, code cache clear, scene save fix. Client scripts for game preview integration (`client/`)
|
|
||||||
- **v1.3** — `scene:set-property` for prefab save support, prefab_create overwrite guard, param alias (`component` → `componentType`)
|
|
||||||
- **v1.5** — `prefab_create_and_replace`, batch `set_property`, `prefab_open`
|
|
||||||
- **v1.6** — `debug_batch_screenshot`, widget support in `create_tree`, `component_query_enum`, `server_check_code_sync`
|
|
||||||
- **v1.8.0** — Preview Recorder panel: `debug_record_start` / `debug_record_stop` (MediaRecorder via canvas.captureStream, MP4/WebM, quality presets)
|
|
||||||
- **v1.8.1** — Fix: `component_set_property` cc.Asset references (cc.Font etc.) falling back to cc.Node when type is unspecified
|
|
||||||
- **v1.8.2** — Preview Recorder: screenshot button (webp/png toggle, max width), section-based UI layout
|
|
||||||
- **v1.9.0** — Preview Recorder auto-archive of old recordings + preflight "preview not running" check
|
|
||||||
- **v1.10.0** — `scene_create` asset-db fallback, stringified args preventive validation, test coverage expansion
|
|
||||||
- **v1.11.0** — HTTP MCP OAuth workaround (stdio bridge + dummy OAuth endpoints for Claude Code VSCode upstream bug) + dialog prevention for scene switching tools (`force` param, `ensureSceneSafeToSwitch`, `safeSaveScene`) + regression tests for both
|
|
||||||
- **v1.12.0** — Prefab authoring efficiency: `component_auto_bind` (auto-match `@property` fields to node names), `debug_wait_compile` (wait for TS compile to finish), `prefab_create_from_spec` (create node tree + auto-bind + prefab_create in one call)
|
|
||||||
- **v1.13.0** — `nodeName` parameter on component/get_components/auto_bind (no UUID required), `screenshot` auto-return option on `component_set_property` / `node_set_layout`, `node_set_layout` unified tool (UITransform + Widget + color/opacity in one call), dialog auto-response for untitled+dirty scenes, shared screenshot / node-resolve utilities
|
|
||||||
- **v1.14.0** — Widget `_alignFlags` auto-recalc bug fix: `setProperty` / `setProperties` / `node_set_layout` now re-query `isAlign*` values from scene and rebuild `_alignFlags` bitmask after isAlign updates (Editor bug where bitmask was not updated automatically, causing prefabs to save with `_alignFlags: 45` stuck state). Also `node_create` component addition now waits for editor reflection (`waitForComponent`) to fix flaky tests
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run watch # Watch mode
|
|
||||||
npm run build # One-time build
|
|
||||||
```
|
|
||||||
|
|
||||||
After building, reload the extension in Cocos Creator:
|
|
||||||
- **Extension Manager** — disable then re-enable
|
|
||||||
- **Developer > Reload** — reloads main process
|
|
||||||
- **Full restart** — required for scene script or new category changes
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
- Cocos Creator 3.8+
|
|
||||||
- Node.js 18+
|
|
||||||
|
|
||||||
## Known Limitations
|
|
||||||
|
|
||||||
- **`scene_create`**: Does not work on Cocos Creator 3.8.x because the underlying `scene:new-scene` Editor message is not exposed on that version. As a workaround, create the `.scene` JSON file directly under `db://assets/` and call `project_refresh_assets` so the editor picks it up. See [#13](https://github.com/harady/cocos-creator-mcp/issues/13) for details.
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
MIT
|
|
||||||
-82
File diff suppressed because one or more lines are too long
-91
@@ -1,91 +0,0 @@
|
|||||||
/**
|
|
||||||
* McpConsoleCapture — ゲームプレビュー時のconsole.log/warn/errorをMCPサーバーに送信する。
|
|
||||||
*
|
|
||||||
* MCPサーバーの POST /log エンドポイントにバッチ送信する。
|
|
||||||
* MCPサーバー未起動時は黙って無視(本番影響なし)。
|
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
* import { initMcpConsoleCapture } from "./McpConsoleCapture";
|
|
||||||
* initMcpConsoleCapture(); // or initMcpConsoleCapture({ mcpBaseUrl: "http://127.0.0.1:3001" });
|
|
||||||
*/
|
|
||||||
|
|
||||||
export interface McpConsoleCaptureConfig {
|
|
||||||
/** MCPサーバーのベースURL (default: "http://127.0.0.1:3000") */
|
|
||||||
mcpBaseUrl?: string;
|
|
||||||
/** バッチ送信間隔ms (default: 500) */
|
|
||||||
flushInterval?: number;
|
|
||||||
/** 1回の送信最大件数 (default: 50) */
|
|
||||||
maxBatchSize?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface LogEntry {
|
|
||||||
timestamp: string;
|
|
||||||
level: "log" | "warn" | "error";
|
|
||||||
message: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
let _buffer: LogEntry[] = [];
|
|
||||||
let _timer: any = null;
|
|
||||||
let _initialized = false;
|
|
||||||
let _config: Required<McpConsoleCaptureConfig>;
|
|
||||||
|
|
||||||
function formatArgs(args: any[]): string {
|
|
||||||
return args.map(a => {
|
|
||||||
if (typeof a === "string") return a;
|
|
||||||
if (a instanceof Error) return `${a.message}\n${a.stack || ""}`;
|
|
||||||
try { return JSON.stringify(a); } catch { return String(a); }
|
|
||||||
}).join(" ");
|
|
||||||
}
|
|
||||||
|
|
||||||
function flush(): void {
|
|
||||||
if (_buffer.length === 0) return;
|
|
||||||
const entries = _buffer.splice(0, _config.maxBatchSize);
|
|
||||||
fetch(`${_config.mcpBaseUrl}/log`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify(entries),
|
|
||||||
}).catch(() => {});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** ゲーム起動時に1回呼ぶ。console.log/warn/errorをフックしてMCPに送信開始 */
|
|
||||||
export function initMcpConsoleCapture(config?: McpConsoleCaptureConfig): void {
|
|
||||||
if (_initialized) return;
|
|
||||||
_initialized = true;
|
|
||||||
_config = {
|
|
||||||
mcpBaseUrl: config?.mcpBaseUrl ?? "http://127.0.0.1:3000",
|
|
||||||
flushInterval: config?.flushInterval ?? 500,
|
|
||||||
maxBatchSize: config?.maxBatchSize ?? 50,
|
|
||||||
};
|
|
||||||
|
|
||||||
const originalLog = console.log;
|
|
||||||
const originalWarn = console.warn;
|
|
||||||
const originalError = console.error;
|
|
||||||
|
|
||||||
function hook(level: LogEntry["level"], original: (...args: any[]) => void) {
|
|
||||||
return function (...args: any[]) {
|
|
||||||
original.apply(console, args);
|
|
||||||
_buffer.push({
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
level,
|
|
||||||
message: formatArgs(args),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log = hook("log", originalLog);
|
|
||||||
console.warn = hook("warn", originalWarn);
|
|
||||||
console.error = hook("error", originalError);
|
|
||||||
|
|
||||||
_timer = setInterval(flush, _config.flushInterval);
|
|
||||||
console.log("[McpConsoleCapture] initialized");
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 停止 */
|
|
||||||
export function stopMcpConsoleCapture(): void {
|
|
||||||
if (_timer) {
|
|
||||||
clearInterval(_timer);
|
|
||||||
_timer = null;
|
|
||||||
}
|
|
||||||
flush();
|
|
||||||
_initialized = false;
|
|
||||||
}
|
|
||||||
-176
File diff suppressed because one or more lines are too long
-315
@@ -1,315 +0,0 @@
|
|||||||
/**
|
|
||||||
* McpDebugClient — MCPサーバーとCocosCreatorプレビューの双方向通信
|
|
||||||
*
|
|
||||||
* MCPサーバーのコマンドキューをポーリングし、コマンドを実行して結果を返す。
|
|
||||||
* MCPサーバー未起動時は黙って無視(本番影響なし)。
|
|
||||||
*
|
|
||||||
* ビルトインコマンド:
|
|
||||||
* - screenshot: RenderTexture経由でゲーム画面キャプチャ
|
|
||||||
* - click: ノード名指定でクリックイベント発火
|
|
||||||
*
|
|
||||||
* カスタムコマンドを追加可能(プロジェクト固有の機能):
|
|
||||||
* initMcpDebugClient({
|
|
||||||
* customCommands: {
|
|
||||||
* state: () => ({ success: true, data: myDb.dump() }),
|
|
||||||
* navigate: async (args) => { await myRouter.go(args.page); return { success: true }; },
|
|
||||||
* },
|
|
||||||
* });
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { director, Node, Button, Camera, RenderTexture, gfx } from "cc";
|
|
||||||
|
|
||||||
export interface McpDebugClientConfig {
|
|
||||||
/** MCPサーバーのベースURL (default: "http://127.0.0.1:3000") */
|
|
||||||
mcpBaseUrl?: string;
|
|
||||||
/** ポーリング間隔ms (default: 500) */
|
|
||||||
pollInterval?: number;
|
|
||||||
/** プロジェクト固有のコマンドハンドラー */
|
|
||||||
customCommands?: Record<string, (args: any) => any | Promise<any>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface McpCommand {
|
|
||||||
id: string;
|
|
||||||
type: string;
|
|
||||||
args?: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
let _polling = false;
|
|
||||||
let _timer: any = null;
|
|
||||||
let _config: Required<Pick<McpDebugClientConfig, "mcpBaseUrl" | "pollInterval">> & { customCommands: Record<string, (args: any) => any | Promise<any>> };
|
|
||||||
|
|
||||||
async function pollCommand(): Promise<void> {
|
|
||||||
if (_polling) return;
|
|
||||||
_polling = true;
|
|
||||||
try {
|
|
||||||
const res = await fetch(`${_config.mcpBaseUrl}/game/command`);
|
|
||||||
if (!res.ok) return;
|
|
||||||
const cmd: McpCommand | null = await res.json();
|
|
||||||
if (!cmd) return;
|
|
||||||
|
|
||||||
console.log(`[McpDebugClient] command: ${cmd.type}`, cmd.args || "");
|
|
||||||
const result = await executeCommand(cmd);
|
|
||||||
|
|
||||||
await fetch(`${_config.mcpBaseUrl}/game/result`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify(result),
|
|
||||||
}).catch(() => {});
|
|
||||||
} catch {
|
|
||||||
// MCP server not running
|
|
||||||
} finally {
|
|
||||||
_polling = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function executeCommand(cmd: McpCommand): Promise<any> {
|
|
||||||
try {
|
|
||||||
// ビルトインコマンド
|
|
||||||
switch (cmd.type) {
|
|
||||||
case "screenshot":
|
|
||||||
return { id: cmd.id, ...takeScreenshot() };
|
|
||||||
case "click":
|
|
||||||
return { id: cmd.id, ...clickNode(cmd.args?.name) };
|
|
||||||
case "record_start":
|
|
||||||
return { id: cmd.id, ...startRecording(cmd.args) };
|
|
||||||
case "record_stop":
|
|
||||||
return { id: cmd.id, ...(await stopRecording()) };
|
|
||||||
}
|
|
||||||
// カスタムコマンド
|
|
||||||
const handler = _config.customCommands[cmd.type];
|
|
||||||
if (handler) {
|
|
||||||
const result = await handler(cmd.args);
|
|
||||||
return { id: cmd.id, ...result };
|
|
||||||
}
|
|
||||||
return { id: cmd.id, success: false, error: `Unknown command: ${cmd.type}` };
|
|
||||||
} catch (e: any) {
|
|
||||||
return { id: cmd.id, success: false, error: e.message || String(e) };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── ビルトインコマンド ───
|
|
||||||
|
|
||||||
function takeScreenshot(): { success: boolean; data?: any; error?: string } {
|
|
||||||
try {
|
|
||||||
const scene = director.getScene();
|
|
||||||
if (!scene) return { success: false, error: "No active scene" };
|
|
||||||
|
|
||||||
let camera: Camera | null = null;
|
|
||||||
const findCamera = (node: Node) => {
|
|
||||||
const cam = node.getComponent(Camera);
|
|
||||||
if (cam && cam.enabled) { camera = cam; return; }
|
|
||||||
for (const child of node.children) {
|
|
||||||
if (camera) return;
|
|
||||||
findCamera(child);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
findCamera(scene);
|
|
||||||
if (!camera) return { success: false, error: "No camera found" };
|
|
||||||
|
|
||||||
const cam = camera as Camera;
|
|
||||||
const width = Math.floor(cam.camera.width);
|
|
||||||
const height = Math.floor(cam.camera.height);
|
|
||||||
|
|
||||||
const rt = new RenderTexture();
|
|
||||||
rt.reset({ width, height });
|
|
||||||
|
|
||||||
const prevTarget = cam.targetTexture;
|
|
||||||
cam.targetTexture = rt;
|
|
||||||
director.root!.frameMove(0);
|
|
||||||
cam.targetTexture = prevTarget;
|
|
||||||
|
|
||||||
const region = new gfx.BufferTextureCopy();
|
|
||||||
region.texOffset.x = 0;
|
|
||||||
region.texOffset.y = 0;
|
|
||||||
region.texExtent.width = width;
|
|
||||||
region.texExtent.height = height;
|
|
||||||
|
|
||||||
const buffer = new Uint8Array(width * height * 4);
|
|
||||||
const gfxTex = rt.getGFXTexture()!;
|
|
||||||
director.root!.device.copyTextureToBuffers(gfxTex, [buffer], [region]);
|
|
||||||
|
|
||||||
const cvs = document.createElement("canvas");
|
|
||||||
cvs.width = width;
|
|
||||||
cvs.height = height;
|
|
||||||
const ctx = cvs.getContext("2d")!;
|
|
||||||
const imageData = ctx.createImageData(width, height);
|
|
||||||
|
|
||||||
for (let y = 0; y < height; y++) {
|
|
||||||
const srcRow = (height - 1 - y) * width * 4;
|
|
||||||
const dstRow = y * width * 4;
|
|
||||||
for (let x = 0; x < width * 4; x++) {
|
|
||||||
imageData.data[dstRow + x] = buffer[srcRow + x];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.putImageData(imageData, 0, 0);
|
|
||||||
const dataUrl = cvs.toDataURL("image/png");
|
|
||||||
rt.destroy();
|
|
||||||
|
|
||||||
return { success: true, data: { dataUrl, width, height } };
|
|
||||||
} catch (e: any) {
|
|
||||||
return { success: false, error: e.message };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function clickNode(name?: string): { success: boolean; error?: string } {
|
|
||||||
if (!name) return { success: false, error: "name argument required" };
|
|
||||||
const scene = director.getScene();
|
|
||||||
if (!scene) return { success: false, error: "No active scene" };
|
|
||||||
|
|
||||||
const found = findNodeByName(scene, name);
|
|
||||||
if (!found) return { success: false, error: `Node '${name}' not found` };
|
|
||||||
|
|
||||||
found.emit(Button.EventType.CLICK, found);
|
|
||||||
return { success: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── 録画(MediaRecorder経由) ───
|
|
||||||
|
|
||||||
let _mediaRecorder: MediaRecorder | null = null;
|
|
||||||
let _recordChunks: Blob[] = [];
|
|
||||||
let _recordStream: MediaStream | null = null;
|
|
||||||
let _recordId: string | null = null;
|
|
||||||
|
|
||||||
const QUALITY_PRESETS: Record<string, number> = {
|
|
||||||
low: 0.15,
|
|
||||||
medium: 0.25,
|
|
||||||
high: 0.40,
|
|
||||||
ultra: 0.60,
|
|
||||||
};
|
|
||||||
|
|
||||||
function startRecording(args?: { fps?: number; videoBitsPerSecond?: number; quality?: string; coefficient?: number; format?: "webm" | "mp4" }): { success: boolean; error?: string; data?: any } {
|
|
||||||
// 前回の録画状態が残っていたら強制クリア
|
|
||||||
if (_mediaRecorder) {
|
|
||||||
try { if (_mediaRecorder.state !== "inactive") _mediaRecorder.stop(); } catch {}
|
|
||||||
_recordStream?.getTracks().forEach(t => { try { t.stop(); } catch {} });
|
|
||||||
_mediaRecorder = null;
|
|
||||||
_recordStream = null;
|
|
||||||
_recordChunks = [];
|
|
||||||
_recordId = null;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
// GameView canvas取得
|
|
||||||
const canvas = document.getElementById("GameCanvas") as HTMLCanvasElement
|
|
||||||
|| document.querySelector("canvas") as HTMLCanvasElement;
|
|
||||||
if (!canvas) return { success: false, error: "canvas not found" };
|
|
||||||
|
|
||||||
const fps = args?.fps ?? 30;
|
|
||||||
const quality = args?.quality ?? "medium";
|
|
||||||
const coef = args?.coefficient ?? QUALITY_PRESETS[quality] ?? QUALITY_PRESETS.medium;
|
|
||||||
const autoBps = Math.round(canvas.width * canvas.height * fps * coef);
|
|
||||||
const bps = args?.videoBitsPerSecond ?? autoBps;
|
|
||||||
const format = args?.format ?? "mp4";
|
|
||||||
_recordStream = canvas.captureStream(fps);
|
|
||||||
_recordChunks = [];
|
|
||||||
_recordId = `rec_${new Date().toISOString().replace(/[:.]/g, "-").replace("T", "_").substring(0, 19)}`;
|
|
||||||
|
|
||||||
// 指定フォーマット優先、非対応ならwebmにfallback
|
|
||||||
const mp4Candidates = [
|
|
||||||
"video/mp4;codecs=h264",
|
|
||||||
"video/mp4;codecs=avc1.42E01E",
|
|
||||||
"video/mp4",
|
|
||||||
];
|
|
||||||
const webmCandidates = [
|
|
||||||
"video/webm;codecs=vp9",
|
|
||||||
"video/webm;codecs=vp8",
|
|
||||||
"video/webm",
|
|
||||||
];
|
|
||||||
const candidates = format === "mp4"
|
|
||||||
? [...mp4Candidates, ...webmCandidates]
|
|
||||||
: webmCandidates;
|
|
||||||
let mimeType = "";
|
|
||||||
for (const c of candidates) {
|
|
||||||
if (typeof MediaRecorder !== "undefined" && MediaRecorder.isTypeSupported(c)) {
|
|
||||||
mimeType = c;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!mimeType) return { success: false, error: "No supported MediaRecorder mimeType" };
|
|
||||||
|
|
||||||
_mediaRecorder = new MediaRecorder(_recordStream, { mimeType, videoBitsPerSecond: bps });
|
|
||||||
_mediaRecorder.ondataavailable = (e: BlobEvent) => {
|
|
||||||
if (e.data.size > 0) _recordChunks.push(e.data);
|
|
||||||
};
|
|
||||||
_mediaRecorder.start();
|
|
||||||
return { success: true, data: { id: _recordId, mimeType, fps, videoBitsPerSecond: bps, quality, canvasWidth: canvas.width, canvasHeight: canvas.height } };
|
|
||||||
} catch (e: any) {
|
|
||||||
_mediaRecorder = null;
|
|
||||||
_recordStream = null;
|
|
||||||
return { success: false, error: e.message || String(e) };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function stopRecording(): Promise<{ success: boolean; error?: string; data?: any }> {
|
|
||||||
if (!_mediaRecorder || !_recordId) return { success: false, error: "not recording" };
|
|
||||||
const id = _recordId;
|
|
||||||
const mimeType = _mediaRecorder.mimeType;
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
_mediaRecorder!.onstop = async () => {
|
|
||||||
try {
|
|
||||||
const blob = new Blob(_recordChunks, { type: mimeType });
|
|
||||||
_recordStream?.getTracks().forEach(t => t.stop());
|
|
||||||
_mediaRecorder = null;
|
|
||||||
_recordStream = null;
|
|
||||||
_recordChunks = [];
|
|
||||||
_recordId = null;
|
|
||||||
|
|
||||||
// Blob → base64 → POST /game/recording
|
|
||||||
const base64 = await blobToBase64(blob);
|
|
||||||
const uploadRes = await fetch(`${_config.mcpBaseUrl}/game/recording`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ id, base64, mimeType }),
|
|
||||||
});
|
|
||||||
const uploadData = await uploadRes.json();
|
|
||||||
resolve({ success: true, data: { id, size: blob.size, ...uploadData } });
|
|
||||||
} catch (e: any) {
|
|
||||||
resolve({ success: false, error: e.message || String(e) });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
_mediaRecorder!.stop();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function blobToBase64(blob: Blob): Promise<string> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onloadend = () => {
|
|
||||||
const result = reader.result as string;
|
|
||||||
// "data:video/webm;base64,XXX" → XXX だけ抽出
|
|
||||||
const commaIdx = result.indexOf(",");
|
|
||||||
resolve(commaIdx >= 0 ? result.substring(commaIdx + 1) : result);
|
|
||||||
};
|
|
||||||
reader.onerror = reject;
|
|
||||||
reader.readAsDataURL(blob);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function findNodeByName(root: Node, name: string): Node | null {
|
|
||||||
if (root.name === name) return root;
|
|
||||||
for (const child of root.children) {
|
|
||||||
const found = findNodeByName(child, name);
|
|
||||||
if (found) return found;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── 初期化・停止 ───
|
|
||||||
|
|
||||||
export function initMcpDebugClient(config?: McpDebugClientConfig): void {
|
|
||||||
if (_timer) return;
|
|
||||||
_config = {
|
|
||||||
mcpBaseUrl: config?.mcpBaseUrl ?? "http://127.0.0.1:3000",
|
|
||||||
pollInterval: config?.pollInterval ?? 500,
|
|
||||||
customCommands: config?.customCommands ?? {},
|
|
||||||
};
|
|
||||||
_timer = setInterval(pollCommand, _config.pollInterval);
|
|
||||||
console.log("[McpDebugClient] initialized");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function stopMcpDebugClient(): void {
|
|
||||||
if (_timer) {
|
|
||||||
clearInterval(_timer);
|
|
||||||
_timer = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,171 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
/**
|
|
||||||
* cocos-creator-mcp stdio bridge
|
|
||||||
*
|
|
||||||
* stdin で line-delimited JSON-RPC を読み取り、HTTP MCP エンドポイントに転送する。
|
|
||||||
* レスポンスは stdout に 1 行ずつ書き出す。
|
|
||||||
*
|
|
||||||
* Claude Code VSCode 拡張の HTTP MCP OAuth 強制バグ (#26917 等) を回避するための
|
|
||||||
* 代替トランスポート。ユーザーは .mcp.json で stdio 型として設定する:
|
|
||||||
*
|
|
||||||
* {
|
|
||||||
* "mcpServers": {
|
|
||||||
* "cocos-creator-mcp": {
|
|
||||||
* "command": "node",
|
|
||||||
* "args": [
|
|
||||||
* "C:/path/to/cocos-creator-mcp/client/stdio-bridge.js"
|
|
||||||
* ]
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* Optional 環境変数:
|
|
||||||
* COCOS_MCP_URL - MCP サーバーの URL (default: http://127.0.0.1:3000/mcp)
|
|
||||||
*/
|
|
||||||
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
const http = require("http");
|
|
||||||
const { URL } = require("url");
|
|
||||||
const readline = require("readline");
|
|
||||||
|
|
||||||
const DEFAULT_URL = "http://127.0.0.1:3000/mcp";
|
|
||||||
const serverUrl = new URL(process.env.COCOS_MCP_URL || DEFAULT_URL);
|
|
||||||
|
|
||||||
let sessionId = null;
|
|
||||||
|
|
||||||
function log(...args) {
|
|
||||||
// デバッグログは stderr に出す(stdout は JSON-RPC 応答専用)
|
|
||||||
console.error("[stdio-bridge]", ...args);
|
|
||||||
}
|
|
||||||
|
|
||||||
function postJsonRpc(body) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const headers = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Accept": "application/json",
|
|
||||||
"Content-Length": Buffer.byteLength(body),
|
|
||||||
};
|
|
||||||
if (sessionId) headers["Mcp-Session-Id"] = sessionId;
|
|
||||||
|
|
||||||
const req = http.request(
|
|
||||||
{
|
|
||||||
hostname: serverUrl.hostname,
|
|
||||||
port: serverUrl.port || 80,
|
|
||||||
path: serverUrl.pathname,
|
|
||||||
method: "POST",
|
|
||||||
headers,
|
|
||||||
},
|
|
||||||
(res) => {
|
|
||||||
const chunks = [];
|
|
||||||
res.on("data", (c) => chunks.push(c));
|
|
||||||
res.on("end", () => {
|
|
||||||
const responseSession = res.headers["mcp-session-id"];
|
|
||||||
if (responseSession && !sessionId) {
|
|
||||||
sessionId = responseSession;
|
|
||||||
log("session established:", sessionId);
|
|
||||||
}
|
|
||||||
const text = Buffer.concat(chunks).toString("utf8");
|
|
||||||
resolve({ status: res.statusCode || 0, body: text });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
req.on("error", (err) => reject(err));
|
|
||||||
req.write(body);
|
|
||||||
req.end();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function writeResponse(obj) {
|
|
||||||
process.stdout.write(JSON.stringify(obj) + "\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleLine(line) {
|
|
||||||
const trimmed = line.trim();
|
|
||||||
if (!trimmed) return;
|
|
||||||
|
|
||||||
let request;
|
|
||||||
try {
|
|
||||||
request = JSON.parse(trimmed);
|
|
||||||
} catch (e) {
|
|
||||||
writeResponse({
|
|
||||||
jsonrpc: "2.0",
|
|
||||||
id: null,
|
|
||||||
error: { code: -32700, message: "Parse error", data: e.message },
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { status, body } = await postJsonRpc(trimmed);
|
|
||||||
// Notifications (no id) may yield empty body
|
|
||||||
if (!body) {
|
|
||||||
if (request.id !== undefined) {
|
|
||||||
writeResponse({
|
|
||||||
jsonrpc: "2.0",
|
|
||||||
id: request.id,
|
|
||||||
error: { code: -32603, message: "Empty response from server", data: { status } },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Pass through server response (single JSON object per line)
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(body);
|
|
||||||
writeResponse(parsed);
|
|
||||||
} catch {
|
|
||||||
// サーバーが SSE 等で返した場合のフォールバック
|
|
||||||
process.stdout.write(body + (body.endsWith("\n") ? "" : "\n"));
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
writeResponse({
|
|
||||||
jsonrpc: "2.0",
|
|
||||||
id: request.id !== undefined ? request.id : null,
|
|
||||||
error: {
|
|
||||||
code: -32603,
|
|
||||||
message: "HTTP bridge error",
|
|
||||||
data: err && err.message ? err.message : String(err),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let inflight = 0;
|
|
||||||
let stdinClosed = false;
|
|
||||||
|
|
||||||
function maybeExit() {
|
|
||||||
if (stdinClosed && inflight === 0) {
|
|
||||||
log("stdin closed, no inflight, exiting");
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function main() {
|
|
||||||
log(`starting, target=${serverUrl.href}`);
|
|
||||||
|
|
||||||
const rl = readline.createInterface({
|
|
||||||
input: process.stdin,
|
|
||||||
terminal: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
rl.on("line", (line) => {
|
|
||||||
inflight++;
|
|
||||||
handleLine(line)
|
|
||||||
.catch((e) => log("handler error:", e))
|
|
||||||
.finally(() => {
|
|
||||||
inflight--;
|
|
||||||
maybeExit();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
rl.on("close", () => {
|
|
||||||
stdinClosed = true;
|
|
||||||
maybeExit();
|
|
||||||
});
|
|
||||||
|
|
||||||
process.on("SIGINT", () => process.exit(0));
|
|
||||||
process.on("SIGTERM", () => process.exit(0));
|
|
||||||
}
|
|
||||||
|
|
||||||
main();
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
description: "MCP (Model Context Protocol) server for Cocos Creator",
|
|
||||||
panel_title: "Cocos Creator MCP",
|
|
||||||
open_panel: "Open Panel",
|
|
||||||
recorder_title: "Preview Recorder",
|
|
||||||
open_recorder: "Open Recorder",
|
|
||||||
};
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
description: "Cocos Creator用 MCP (Model Context Protocol) サーバー",
|
|
||||||
panel_title: "Cocos Creator MCP",
|
|
||||||
open_panel: "パネルを開く",
|
|
||||||
recorder_title: "Preview Recorder",
|
|
||||||
open_recorder: "Previewレコーダー",
|
|
||||||
};
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
description: "Cocos Creator MCP (模型上下文协议) 服务器",
|
|
||||||
panel_title: "Cocos Creator MCP",
|
|
||||||
open_panel: "打开面板",
|
|
||||||
recorder_title: "Preview Recorder",
|
|
||||||
open_recorder: "打开录制器",
|
|
||||||
};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
nodejs
|
|
||||||
-310
@@ -1,310 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@iflow-mcp/harady-cocos-creator-mcp",
|
|
||||||
"version": "1.14.0",
|
|
||||||
"lockfileVersion": 3,
|
|
||||||
"requires": true,
|
|
||||||
"packages": {
|
|
||||||
"": {
|
|
||||||
"name": "@iflow-mcp/harady-cocos-creator-mcp",
|
|
||||||
"version": "1.14.0",
|
|
||||||
"dependencies": {
|
|
||||||
"vue": "^3.1.4",
|
|
||||||
"wasm-vips": "^0.0.17"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"iflow-mcp_harady-cocos-creator-mcp": "client/stdio-bridge.js"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@cocos/creator-types": "^3.8.6",
|
|
||||||
"@types/node": "^18.17.1",
|
|
||||||
"typescript": "^5.8.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@babel/helper-string-parser": {
|
|
||||||
"version": "7.29.7",
|
|
||||||
"resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
|
|
||||||
"integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.9.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@babel/helper-validator-identifier": {
|
|
||||||
"version": "7.29.7",
|
|
||||||
"resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
|
|
||||||
"integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.9.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@babel/parser": {
|
|
||||||
"version": "7.29.7",
|
|
||||||
"resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.7.tgz",
|
|
||||||
"integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/types": "^7.29.7"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"parser": "bin/babel-parser.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@babel/types": {
|
|
||||||
"version": "7.29.7",
|
|
||||||
"resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.7.tgz",
|
|
||||||
"integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/helper-string-parser": "^7.29.7",
|
|
||||||
"@babel/helper-validator-identifier": "^7.29.7"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.9.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@cocos/creator-types": {
|
|
||||||
"version": "3.8.7",
|
|
||||||
"resolved": "https://registry.npmmirror.com/@cocos/creator-types/-/creator-types-3.8.7.tgz",
|
|
||||||
"integrity": "sha512-QTtuC+w1s1yQ8DPlgFSiIGS+Pko6v2QdrrQRN8/SttEBo53JjyXEWHXpRkg1lGiHq0u/aadm0cN/Y4jtEmis/w==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/@jridgewell/sourcemap-codec": {
|
|
||||||
"version": "1.5.5",
|
|
||||||
"resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
|
||||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="
|
|
||||||
},
|
|
||||||
"node_modules/@types/node": {
|
|
||||||
"version": "18.19.130",
|
|
||||||
"resolved": "https://registry.npmmirror.com/@types/node/-/node-18.19.130.tgz",
|
|
||||||
"integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"undici-types": "~5.26.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@vue/compiler-core": {
|
|
||||||
"version": "3.5.39",
|
|
||||||
"resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.39.tgz",
|
|
||||||
"integrity": "sha512-16KBTEXAJCpDr0mwlw+AZyhu8iyC7R3S2vBwsI7QnWJU6X3WKc9VKeNEZpiMdZ569qWhz9574L3vV55qRL0Vtw==",
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/parser": "^7.29.7",
|
|
||||||
"@vue/shared": "3.5.39",
|
|
||||||
"entities": "^7.0.1",
|
|
||||||
"estree-walker": "^2.0.2",
|
|
||||||
"source-map-js": "^1.2.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@vue/compiler-dom": {
|
|
||||||
"version": "3.5.39",
|
|
||||||
"resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.39.tgz",
|
|
||||||
"integrity": "sha512-oQPigALqYbNxTNPvNgSOe+czwVExfbVF02lz8jP0S3AXJiu3jxYDygNUiqSep4ezzW8XgnubqH63My2A7JR/vg==",
|
|
||||||
"dependencies": {
|
|
||||||
"@vue/compiler-core": "3.5.39",
|
|
||||||
"@vue/shared": "3.5.39"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@vue/compiler-sfc": {
|
|
||||||
"version": "3.5.39",
|
|
||||||
"resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.39.tgz",
|
|
||||||
"integrity": "sha512-d0ki86iOyN8LoZPBmk5SJWNwHP19CnDDCfuo//+2WJa2g5Ke0Jay983PIBIcSSzldC68I8DrD5GrHV3OSDfodg==",
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/parser": "^7.29.7",
|
|
||||||
"@vue/compiler-core": "3.5.39",
|
|
||||||
"@vue/compiler-dom": "3.5.39",
|
|
||||||
"@vue/compiler-ssr": "3.5.39",
|
|
||||||
"@vue/shared": "3.5.39",
|
|
||||||
"estree-walker": "^2.0.2",
|
|
||||||
"magic-string": "^0.30.21",
|
|
||||||
"postcss": "^8.5.15",
|
|
||||||
"source-map-js": "^1.2.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@vue/compiler-ssr": {
|
|
||||||
"version": "3.5.39",
|
|
||||||
"resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.39.tgz",
|
|
||||||
"integrity": "sha512-Ce7/wvwMHai74bdszfXExdazFigYnlF9zgCmEQUcM1j0fOymlouZ7XilTYNo8oUjhlnjYOZbGrcYKuqjz89Ucw==",
|
|
||||||
"dependencies": {
|
|
||||||
"@vue/compiler-dom": "3.5.39",
|
|
||||||
"@vue/shared": "3.5.39"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@vue/reactivity": {
|
|
||||||
"version": "3.5.39",
|
|
||||||
"resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.39.tgz",
|
|
||||||
"integrity": "sha512-TpsuBJ9gGlZa5d23XcM2y8EXanz9dZeVDQBXRwzy46ItgvM+rWpzs+UVM0wcRLxGvcav0HE5jz2gNL53xlRAog==",
|
|
||||||
"dependencies": {
|
|
||||||
"@vue/shared": "3.5.39"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@vue/runtime-core": {
|
|
||||||
"version": "3.5.39",
|
|
||||||
"resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.39.tgz",
|
|
||||||
"integrity": "sha512-9GLtNyRvPAUMbX+7ono0RC2j0guo2LXVi8LvcmAooImACUKm0oFf0jjwbX8/H0AE/t1nxhAkn8RSl9PMCzzxZw==",
|
|
||||||
"dependencies": {
|
|
||||||
"@vue/reactivity": "3.5.39",
|
|
||||||
"@vue/shared": "3.5.39"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@vue/runtime-dom": {
|
|
||||||
"version": "3.5.39",
|
|
||||||
"resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.39.tgz",
|
|
||||||
"integrity": "sha512-7Y6aAGboKcXAZ3ECuUy7RrS5yy2r47dhTp2SKaJmYxjopImaVFaNa5Ne66NwGovsrxVAl5S5rwc7m22UG7Lmww==",
|
|
||||||
"dependencies": {
|
|
||||||
"@vue/reactivity": "3.5.39",
|
|
||||||
"@vue/runtime-core": "3.5.39",
|
|
||||||
"@vue/shared": "3.5.39",
|
|
||||||
"csstype": "^3.2.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@vue/server-renderer": {
|
|
||||||
"version": "3.5.39",
|
|
||||||
"resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.39.tgz",
|
|
||||||
"integrity": "sha512-yZSakiAGw85rZfG7UM8akMnIF+FmeiNk47uvHf2nVBBSe+dIKUhZuZq9+XgJhbV3nS5Z4ALH23/MpXofW+mbcw==",
|
|
||||||
"dependencies": {
|
|
||||||
"@vue/compiler-ssr": "3.5.39",
|
|
||||||
"@vue/shared": "3.5.39"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"vue": "3.5.39"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@vue/shared": {
|
|
||||||
"version": "3.5.39",
|
|
||||||
"resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.39.tgz",
|
|
||||||
"integrity": "sha512-l1rrBtBfTnmxvtsvdQDXltUUy8S1Y+ZaqdfUzmAnJkTd8Z8rv5v/ytW+TKiqEOWyHPoqtPlNFSs0lhRmYVSHVA=="
|
|
||||||
},
|
|
||||||
"node_modules/csstype": {
|
|
||||||
"version": "3.2.3",
|
|
||||||
"resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz",
|
|
||||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="
|
|
||||||
},
|
|
||||||
"node_modules/entities": {
|
|
||||||
"version": "7.0.1",
|
|
||||||
"resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz",
|
|
||||||
"integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.12"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/estree-walker": {
|
|
||||||
"version": "2.0.2",
|
|
||||||
"resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz",
|
|
||||||
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
|
|
||||||
},
|
|
||||||
"node_modules/magic-string": {
|
|
||||||
"version": "0.30.21",
|
|
||||||
"resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz",
|
|
||||||
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/nanoid": {
|
|
||||||
"version": "3.3.15",
|
|
||||||
"resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.15.tgz",
|
|
||||||
"integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==",
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/ai"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"bin": {
|
|
||||||
"nanoid": "bin/nanoid.cjs"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/picocolors": {
|
|
||||||
"version": "1.1.1",
|
|
||||||
"resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
|
|
||||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
|
|
||||||
},
|
|
||||||
"node_modules/postcss": {
|
|
||||||
"version": "8.5.15",
|
|
||||||
"resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.15.tgz",
|
|
||||||
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/postcss/"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "tidelift",
|
|
||||||
"url": "https://tidelift.com/funding/github/npm/postcss"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/ai"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"dependencies": {
|
|
||||||
"nanoid": "^3.3.12",
|
|
||||||
"picocolors": "^1.1.1",
|
|
||||||
"source-map-js": "^1.2.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^10 || ^12 || >=14"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/source-map-js": {
|
|
||||||
"version": "1.2.1",
|
|
||||||
"resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
|
|
||||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/typescript": {
|
|
||||||
"version": "5.9.3",
|
|
||||||
"resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz",
|
|
||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
|
||||||
"devOptional": true,
|
|
||||||
"bin": {
|
|
||||||
"tsc": "bin/tsc",
|
|
||||||
"tsserver": "bin/tsserver"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.17"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/undici-types": {
|
|
||||||
"version": "5.26.5",
|
|
||||||
"resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-5.26.5.tgz",
|
|
||||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/vue": {
|
|
||||||
"version": "3.5.39",
|
|
||||||
"resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.39.tgz",
|
|
||||||
"integrity": "sha512-xmZCYabFGcirU8r0fTuvl/LICc1OU620rnqepaJDL/a141ZigkG7AyaxQLdqJ02ZRYzWe6YPaDHeQx7MfknQfA==",
|
|
||||||
"dependencies": {
|
|
||||||
"@vue/compiler-dom": "3.5.39",
|
|
||||||
"@vue/compiler-sfc": "3.5.39",
|
|
||||||
"@vue/runtime-dom": "3.5.39",
|
|
||||||
"@vue/server-renderer": "3.5.39",
|
|
||||||
"@vue/shared": "3.5.39"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"typescript": "*"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"typescript": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/wasm-vips": {
|
|
||||||
"version": "0.0.17",
|
|
||||||
"resolved": "https://registry.npmmirror.com/wasm-vips/-/wasm-vips-0.0.17.tgz",
|
|
||||||
"integrity": "sha512-nhkqUNJDUymImoXGrVfImC4wzIFTb9KfBpAngb7dcEQNPP1gVTx4+WL3VVVDSXQpMsyeacsQDOx0+DM33Rpurg==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=16.4.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
{
|
|
||||||
"package_version": 2,
|
|
||||||
"name": "cocos-creator-mcp",
|
|
||||||
"version": "1.14.0",
|
|
||||||
"author": "harady",
|
|
||||||
"editor": ">=3.8.0",
|
|
||||||
"scripts": {
|
|
||||||
"build": "tsc && node scripts/postbuild.js",
|
|
||||||
"watch": "tsc -w"
|
|
||||||
},
|
|
||||||
"description": "MCP Server for Cocos Creator [2ade3d2fa124]",
|
|
||||||
"main": "./dist/main.js",
|
|
||||||
"bin": {
|
|
||||||
"iflow-mcp_harady-cocos-creator-mcp": "client/stdio-bridge.js"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"vue": "^3.1.4",
|
|
||||||
"wasm-vips": "^0.0.17"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@cocos/creator-types": "^3.8.6",
|
|
||||||
"@types/node": "^18.17.1",
|
|
||||||
"typescript": "^5.8.2"
|
|
||||||
},
|
|
||||||
"panels": {
|
|
||||||
"default": {
|
|
||||||
"title": "i18n:cocos-creator-mcp.panel_title",
|
|
||||||
"type": "dockable",
|
|
||||||
"main": "dist/panels/default",
|
|
||||||
"size": {
|
|
||||||
"min-width": 350,
|
|
||||||
"min-height": 200,
|
|
||||||
"width": 450,
|
|
||||||
"height": 350
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"recorder": {
|
|
||||||
"title": "i18n:cocos-creator-mcp.recorder_title",
|
|
||||||
"type": "dockable",
|
|
||||||
"main": "dist/panels/recorder",
|
|
||||||
"size": {
|
|
||||||
"min-width": 400,
|
|
||||||
"min-height": 400,
|
|
||||||
"width": 500,
|
|
||||||
"height": 600
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"contributions": {
|
|
||||||
"menu": [
|
|
||||||
{
|
|
||||||
"path": "i18n:menu.extension/Cocos Creator MCP",
|
|
||||||
"label": "i18n:cocos-creator-mcp.open_panel",
|
|
||||||
"message": "open-panel"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "i18n:menu.extension/Cocos Creator MCP",
|
|
||||||
"label": "i18n:cocos-creator-mcp.open_recorder",
|
|
||||||
"message": "open-recorder"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"messages": {
|
|
||||||
"open-panel": {
|
|
||||||
"methods": [
|
|
||||||
"openPanel"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"open-recorder": {
|
|
||||||
"methods": [
|
|
||||||
"openRecorder"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"start-server": {
|
|
||||||
"methods": [
|
|
||||||
"startServer"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"stop-server": {
|
|
||||||
"methods": [
|
|
||||||
"stopServer"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"get-server-status": {
|
|
||||||
"methods": [
|
|
||||||
"getServerStatus"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"update-port": {
|
|
||||||
"methods": [
|
|
||||||
"updatePort"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"scene": {
|
|
||||||
"script": "./dist/scene.js",
|
|
||||||
"methods": [
|
|
||||||
"getNodeInfo",
|
|
||||||
"getAllNodes",
|
|
||||||
"findNodesByName",
|
|
||||||
"setNodeProperty",
|
|
||||||
"setComponentProperty",
|
|
||||||
"getSceneHierarchy",
|
|
||||||
"moveNode",
|
|
||||||
"addComponentToNode",
|
|
||||||
"removeComponentFromNode"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
@iflow-mcp/harady-cocos-creator-mcp
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"push_platform": "github",
|
|
||||||
"fork_url": "https://github.com/iflow-mcp/harady-cocos-creator-mcp",
|
|
||||||
"fork_branch": "iflow"
|
|
||||||
}
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
const fs = require("fs");
|
|
||||||
const crypto = require("crypto");
|
|
||||||
const path = require("path");
|
|
||||||
|
|
||||||
// dist/ 配下の全 .js ファイルの内容からハッシュを生成
|
|
||||||
// コードが同じなら常に同じ値になる
|
|
||||||
const distDir = path.join(__dirname, "..", "dist");
|
|
||||||
const hash = crypto.createHash("sha256");
|
|
||||||
|
|
||||||
// dist/ 配下を再帰的に走査して全 .js ファイルを収集
|
|
||||||
function collectJsFiles(dir, prefix = "") {
|
|
||||||
let files = [];
|
|
||||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
||||||
const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
||||||
if (entry.isDirectory()) {
|
|
||||||
files = files.concat(collectJsFiles(path.join(dir, entry.name), relPath));
|
|
||||||
} else if (entry.name.endsWith(".js")) {
|
|
||||||
files.push(relPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return files;
|
|
||||||
}
|
|
||||||
|
|
||||||
const jsFiles = collectJsFiles(distDir).sort();
|
|
||||||
|
|
||||||
for (const file of jsFiles) {
|
|
||||||
const content = fs.readFileSync(path.join(distDir, file), "utf8");
|
|
||||||
// __BUILD_HASH__ プレースホルダーは除外して計算(自己参照を避ける)
|
|
||||||
hash.update(content.replace(/__BUILD_HASH__/g, ""));
|
|
||||||
}
|
|
||||||
|
|
||||||
const buildHash = hash.digest("hex").substring(0, 12);
|
|
||||||
|
|
||||||
// mcp-server.js にハッシュを埋め込む
|
|
||||||
const serverFile = path.join(distDir, "mcp-server.js");
|
|
||||||
const serverContent = fs.readFileSync(serverFile, "utf8");
|
|
||||||
fs.writeFileSync(serverFile, serverContent.replace("__BUILD_HASH__", buildHash));
|
|
||||||
|
|
||||||
// package.json の description にハッシュを付与
|
|
||||||
const pkgFile = path.join(__dirname, "..", "package.json");
|
|
||||||
const pkg = JSON.parse(fs.readFileSync(pkgFile, "utf8"));
|
|
||||||
const baseDesc = pkg.description.replace(/\s*\[[a-f0-9]+\]$/, ""); // 既存ハッシュを除去
|
|
||||||
pkg.description = `${baseDesc} [${buildHash}]`;
|
|
||||||
fs.writeFileSync(pkgFile, JSON.stringify(pkg, null, 4) + "\n");
|
|
||||||
|
|
||||||
console.log("BUILD_HASH:", buildHash);
|
|
||||||
|
|
||||||
// ── dist自動同期: ゲームプロジェクトの extensions/ にコピー ──
|
|
||||||
// SYNC_TARGET 環境変数 or settings ファイルで同期先を指定
|
|
||||||
const settingsFile = path.join(__dirname, "..", "sync-targets.json");
|
|
||||||
let syncTargets = [];
|
|
||||||
|
|
||||||
if (process.env.SYNC_TARGET) {
|
|
||||||
syncTargets = process.env.SYNC_TARGET.split(",").map(s => s.trim());
|
|
||||||
} else if (fs.existsSync(settingsFile)) {
|
|
||||||
syncTargets = JSON.parse(fs.readFileSync(settingsFile, "utf8")).targets || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (syncTargets.length > 0) {
|
|
||||||
const rootDir = path.join(__dirname, "..");
|
|
||||||
const filesToSync = ["dist", "client", "i18n", "static", "package.json", "package-lock.json", "node_modules"];
|
|
||||||
|
|
||||||
for (const target of syncTargets) {
|
|
||||||
const targetDir = path.resolve(target);
|
|
||||||
if (!fs.existsSync(path.dirname(targetDir))) {
|
|
||||||
console.warn(`SYNC SKIP: parent dir not found: ${targetDir}`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
console.log(`SYNC: ${targetDir}`);
|
|
||||||
for (const entry of filesToSync) {
|
|
||||||
const src = path.join(rootDir, entry);
|
|
||||||
const dst = path.join(targetDir, entry);
|
|
||||||
if (!fs.existsSync(src)) continue;
|
|
||||||
if (fs.statSync(src).isDirectory()) {
|
|
||||||
copyDirSync(src, dst);
|
|
||||||
} else {
|
|
||||||
fs.mkdirSync(path.dirname(dst), { recursive: true });
|
|
||||||
fs.copyFileSync(src, dst);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
console.log(`SYNC DONE: ${targetDir}`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log("SYNC: no targets configured (set SYNC_TARGET or create sync-targets.json)");
|
|
||||||
}
|
|
||||||
|
|
||||||
function copyDirSync(src, dst) {
|
|
||||||
fs.mkdirSync(dst, { recursive: true });
|
|
||||||
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
||||||
const srcPath = path.join(src, entry.name);
|
|
||||||
const dstPath = path.join(dst, entry.name);
|
|
||||||
if (entry.isDirectory()) {
|
|
||||||
copyDirSync(srcPath, dstPath);
|
|
||||||
} else {
|
|
||||||
fs.copyFileSync(srcPath, dstPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import fs from "fs";
|
|
||||||
import path from "path";
|
|
||||||
|
|
||||||
const HOURS_24 = 24 * 60 * 60 * 1000;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 24時間以上前のファイルを OLD_yyyyMM フォルダに移動する.
|
|
||||||
*/
|
|
||||||
export function archiveOldFiles(dir: string): void {
|
|
||||||
try {
|
|
||||||
let files: string[];
|
|
||||||
try {
|
|
||||||
files = fs.readdirSync(dir);
|
|
||||||
} catch {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const now = Date.now();
|
|
||||||
let archived = 0;
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
const filePath = path.join(dir, file);
|
|
||||||
const stat = fs.statSync(filePath);
|
|
||||||
|
|
||||||
if (stat.isDirectory()) continue;
|
|
||||||
if (now - stat.mtimeMs < HOURS_24) continue;
|
|
||||||
|
|
||||||
const fileDate = new Date(stat.mtimeMs);
|
|
||||||
const yyyy = fileDate.getFullYear();
|
|
||||||
const mm = String(fileDate.getMonth() + 1).padStart(2, "0");
|
|
||||||
const archiveDir = path.join(dir, `OLD_${yyyy}${mm}`);
|
|
||||||
fs.mkdirSync(archiveDir, { recursive: true });
|
|
||||||
|
|
||||||
fs.renameSync(filePath, path.join(archiveDir, file));
|
|
||||||
archived++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (archived > 0) {
|
|
||||||
console.log(`[cocos-creator-mcp] ${archived}件のファイルを月別フォルダにアーカイブ (${dir})`);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("[cocos-creator-mcp] アーカイブ失敗:", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
import { McpServer } from "./mcp-server";
|
|
||||||
import { SceneTools } from "./tools/scene-tools";
|
|
||||||
import { NodeTools } from "./tools/node-tools";
|
|
||||||
import { ComponentTools } from "./tools/component-tools";
|
|
||||||
import { PrefabTools } from "./tools/prefab-tools";
|
|
||||||
import { ProjectTools } from "./tools/project-tools";
|
|
||||||
import { DebugTools } from "./tools/debug-tools";
|
|
||||||
import { SceneAdvancedTools } from "./tools/scene-advanced-tools";
|
|
||||||
import { SceneViewTools } from "./tools/scene-view-tools";
|
|
||||||
import { AssetTools } from "./tools/asset-tools";
|
|
||||||
import { PreferencesTools } from "./tools/preferences-tools";
|
|
||||||
import { ServerTools } from "./tools/server-tools";
|
|
||||||
import { BuilderTools } from "./tools/builder-tools";
|
|
||||||
import { ReferenceImageTools } from "./tools/reference-image-tools";
|
|
||||||
import { ServerConfig, DEFAULT_CONFIG } from "./types";
|
|
||||||
import path from "path";
|
|
||||||
import fs from "fs";
|
|
||||||
|
|
||||||
let server: McpServer | null = null;
|
|
||||||
|
|
||||||
function getSettingsPath(): string {
|
|
||||||
return path.join(Editor.Project.path, "settings", "cocos-creator-mcp.json");
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadConfig(): ServerConfig {
|
|
||||||
try {
|
|
||||||
const p = getSettingsPath();
|
|
||||||
if (fs.existsSync(p)) {
|
|
||||||
const data = JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
||||||
return { ...DEFAULT_CONFIG, ...data };
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("[cocos-creator-mcp] Failed to load settings, using defaults");
|
|
||||||
}
|
|
||||||
return { ...DEFAULT_CONFIG };
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveConfig(config: ServerConfig): void {
|
|
||||||
try {
|
|
||||||
const p = getSettingsPath();
|
|
||||||
const dir = path.dirname(p);
|
|
||||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
||||||
fs.writeFileSync(p, JSON.stringify(config, null, 2), "utf-8");
|
|
||||||
} catch (e) {
|
|
||||||
console.error("[cocos-creator-mcp] Failed to save settings:", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function createServer(config: ServerConfig): McpServer {
|
|
||||||
const s = new McpServer(config);
|
|
||||||
s.register(new SceneTools());
|
|
||||||
s.register(new NodeTools());
|
|
||||||
const componentTools = new ComponentTools();
|
|
||||||
s.register(componentTools);
|
|
||||||
s.register(new PrefabTools(componentTools));
|
|
||||||
s.register(new ProjectTools());
|
|
||||||
s.register(new DebugTools());
|
|
||||||
s.register(new SceneAdvancedTools());
|
|
||||||
s.register(new SceneViewTools());
|
|
||||||
s.register(new AssetTools());
|
|
||||||
s.register(new PreferencesTools());
|
|
||||||
s.register(new ServerTools());
|
|
||||||
s.register(new BuilderTools());
|
|
||||||
s.register(new ReferenceImageTools());
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const methods: Record<string, (...args: any[]) => any> = {
|
|
||||||
openPanel() {
|
|
||||||
Editor.Panel.open("cocos-creator-mcp");
|
|
||||||
},
|
|
||||||
|
|
||||||
openRecorder() {
|
|
||||||
Editor.Panel.open("cocos-creator-mcp.recorder");
|
|
||||||
},
|
|
||||||
|
|
||||||
async startServer() {
|
|
||||||
if (server?.isRunning) return { running: true, port: server.port };
|
|
||||||
const config = loadConfig();
|
|
||||||
server = createServer(config);
|
|
||||||
await server.start();
|
|
||||||
return { running: true, port: server.port };
|
|
||||||
},
|
|
||||||
|
|
||||||
async stopServer() {
|
|
||||||
if (server) {
|
|
||||||
await server.stop();
|
|
||||||
server = null;
|
|
||||||
}
|
|
||||||
return { running: false };
|
|
||||||
},
|
|
||||||
|
|
||||||
async updatePort(_port: number) {
|
|
||||||
const config = loadConfig();
|
|
||||||
config.port = _port;
|
|
||||||
saveConfig(config);
|
|
||||||
// Restart server if running
|
|
||||||
if (server?.isRunning) {
|
|
||||||
await server.stop();
|
|
||||||
server = createServer(config);
|
|
||||||
await server.start();
|
|
||||||
}
|
|
||||||
return { port: _port, running: server?.isRunning ?? false };
|
|
||||||
},
|
|
||||||
|
|
||||||
getServerStatus() {
|
|
||||||
return {
|
|
||||||
running: server?.isRunning ?? false,
|
|
||||||
port: server?.port ?? loadConfig().port,
|
|
||||||
toolCount: server?.getAllTools().length ?? 0,
|
|
||||||
toolNames: server?.getAllTools().map((t) => t.name).sort() ?? [],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function load() {
|
|
||||||
console.log("[cocos-creator-mcp] Extension loaded");
|
|
||||||
const config = loadConfig();
|
|
||||||
server = createServer(config);
|
|
||||||
|
|
||||||
if (config.autoStart) {
|
|
||||||
try {
|
|
||||||
await server.start();
|
|
||||||
console.log(`[cocos-creator-mcp] Auto-started on port ${config.port}`);
|
|
||||||
} catch (e) {
|
|
||||||
console.error("[cocos-creator-mcp] Auto-start failed:", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function unload() {
|
|
||||||
if (server) {
|
|
||||||
await server.stop();
|
|
||||||
server = null;
|
|
||||||
}
|
|
||||||
console.log("[cocos-creator-mcp] Extension unloaded");
|
|
||||||
}
|
|
||||||
@@ -1,542 +0,0 @@
|
|||||||
import http from "http";
|
|
||||||
import { ToolCategory, ToolDefinition, JsonRpcRequest, JsonRpcResponse, ServerConfig, DEFAULT_CONFIG } from "./types";
|
|
||||||
import { archiveOldFiles } from "./archive";
|
|
||||||
|
|
||||||
const MCP_PROTOCOL_VERSION = "2024-11-05";
|
|
||||||
const SESSION_ID = `cocos-mcp-${Date.now()}-${Math.random().toString(36).substring(2, 10)}`;
|
|
||||||
|
|
||||||
/** ビルド時にコードベースのSHA256ハッシュが埋め込まれる */
|
|
||||||
export const BUILD_HASH = "__BUILD_HASH__";
|
|
||||||
|
|
||||||
// ─── Game Preview Log Buffer ───
|
|
||||||
|
|
||||||
interface GameLogEntry {
|
|
||||||
timestamp: string;
|
|
||||||
level: "log" | "warn" | "error";
|
|
||||||
message: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const MAX_GAME_LOG_BUFFER = 500;
|
|
||||||
const _gameLogs: GameLogEntry[] = [];
|
|
||||||
|
|
||||||
/** Access game preview log buffer from debug-tools */
|
|
||||||
export function getGameLogs(count: number, level?: string): { logs: GameLogEntry[]; total: number } {
|
|
||||||
let logs = _gameLogs;
|
|
||||||
if (level) {
|
|
||||||
logs = logs.filter(l => l.level === level);
|
|
||||||
}
|
|
||||||
return { logs: logs.slice(-count), total: _gameLogs.length };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function clearGameLogs(): void {
|
|
||||||
_gameLogs.length = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Game Debug Command Queue ───
|
|
||||||
|
|
||||||
interface GameCommand {
|
|
||||||
id: string;
|
|
||||||
type: string;
|
|
||||||
args?: any;
|
|
||||||
timestamp: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GameCommandResult {
|
|
||||||
id: string;
|
|
||||||
success: boolean;
|
|
||||||
data?: any;
|
|
||||||
error?: string;
|
|
||||||
timestamp: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
let _pendingCommand: GameCommand | null = null;
|
|
||||||
let _commandResult: GameCommandResult | null = null;
|
|
||||||
let _commandIdCounter = 0;
|
|
||||||
|
|
||||||
/** Queue a command for the game to execute */
|
|
||||||
export function queueGameCommand(type: string, args?: any): string {
|
|
||||||
const id = `cmd_${++_commandIdCounter}_${Date.now()}`;
|
|
||||||
_pendingCommand = { id, type, args, timestamp: new Date().toISOString() };
|
|
||||||
_commandResult = null;
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get the result of the last command (poll until available) */
|
|
||||||
export function getCommandResult(): GameCommandResult | null {
|
|
||||||
return _commandResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Clear command state */
|
|
||||||
export function clearCommandState(): void {
|
|
||||||
_pendingCommand = null;
|
|
||||||
_commandResult = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Recording Storage ───
|
|
||||||
|
|
||||||
interface RecordingInfo {
|
|
||||||
path: string;
|
|
||||||
size: number;
|
|
||||||
createdAt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const _recordings = new Map<string, RecordingInfo>();
|
|
||||||
|
|
||||||
/** Get completed recording info by id */
|
|
||||||
export function getRecording(id: string): RecordingInfo | undefined {
|
|
||||||
return _recordings.get(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setRecording(id: string, info: RecordingInfo): void {
|
|
||||||
_recordings.set(id, info);
|
|
||||||
}
|
|
||||||
|
|
||||||
export class McpServer {
|
|
||||||
private server: http.Server | null = null;
|
|
||||||
private tools: Map<string, ToolCategory> = new Map();
|
|
||||||
private toolIndex: Map<string, ToolCategory> = new Map(); // toolName -> category
|
|
||||||
private config: ServerConfig;
|
|
||||||
|
|
||||||
constructor(config?: Partial<ServerConfig>) {
|
|
||||||
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Register a tool category */
|
|
||||||
register(category: ToolCategory): void {
|
|
||||||
this.tools.set(category.categoryName, category);
|
|
||||||
for (const tool of category.getTools()) {
|
|
||||||
this.toolIndex.set(tool.name, category);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get all tool definitions */
|
|
||||||
getAllTools(): ToolDefinition[] {
|
|
||||||
const all: ToolDefinition[] = [];
|
|
||||||
for (const cat of this.tools.values()) {
|
|
||||||
all.push(...cat.getTools());
|
|
||||||
}
|
|
||||||
return all;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Start HTTP server */
|
|
||||||
start(): Promise<void> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
if (this.server) {
|
|
||||||
resolve();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.server = http.createServer((req, res) => this.handleRequest(req, res));
|
|
||||||
this.server.listen(this.config.port, "127.0.0.1", () => {
|
|
||||||
console.log(`[cocos-creator-mcp] Server started on http://127.0.0.1:${this.config.port}/mcp`);
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
this.server.on("error", (e) => {
|
|
||||||
console.error(`[cocos-creator-mcp] Server error:`, e);
|
|
||||||
reject(e);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Stop HTTP server */
|
|
||||||
stop(): Promise<void> {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
if (!this.server) {
|
|
||||||
resolve();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.server.close(() => {
|
|
||||||
this.server = null;
|
|
||||||
console.log("[cocos-creator-mcp] Server stopped");
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
get isRunning(): boolean {
|
|
||||||
return this.server !== null;
|
|
||||||
}
|
|
||||||
|
|
||||||
get port(): number {
|
|
||||||
return this.config.port;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async handleRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
|
||||||
// CORS
|
|
||||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
||||||
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
||||||
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Accept");
|
|
||||||
|
|
||||||
if (req.method === "OPTIONS") {
|
|
||||||
res.writeHead(204);
|
|
||||||
res.end();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = req.url || "/";
|
|
||||||
const origin = `http://127.0.0.1:${this.config.port}`;
|
|
||||||
|
|
||||||
// ─── OAuth endpoints (MCP spec 2025-06-18 / RFC 9728 / RFC 8414 / RFC 7591) ───
|
|
||||||
//
|
|
||||||
// Claude Code の VSCode 拡張は HTTP トランスポートの MCP サーバーに対して
|
|
||||||
// 無条件で OAuth discovery / DCR を試みる (#26917 等の既知バグ)。
|
|
||||||
// cocos-creator-mcp は localhost-only のローカル開発ツールで本物の認証は不要だが、
|
|
||||||
// クライアントを満足させるため OAuth エンドポイント群をダミー実装して常時許可する。
|
|
||||||
//
|
|
||||||
// TODO: 以下のいずれかが発生したら削除する
|
|
||||||
// 1. anthropics/claude-code #26917 / #38102 等の HTTP OAuth バグが修正される
|
|
||||||
// 2. 本物の認証機構を実装する必要が出る(偽 OAuth と衝突するため)
|
|
||||||
// 3. MCP spec が PKCE 検証・トークンローテーション必須等に更新される
|
|
||||||
// 4. stdio ブリッジが十分定着して HTTP transport 自体を deprecate する
|
|
||||||
|
|
||||||
// RFC 9728 Protected Resource Metadata
|
|
||||||
if (url === "/.well-known/oauth-protected-resource" && req.method === "GET") {
|
|
||||||
res.writeHead(200, { "Content-Type": "application/json" });
|
|
||||||
res.end(JSON.stringify({
|
|
||||||
resource: `${origin}/mcp`,
|
|
||||||
authorization_servers: [origin],
|
|
||||||
bearer_methods_supported: ["header"],
|
|
||||||
scopes_supported: ["mcp"],
|
|
||||||
}));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// RFC 8414 Authorization Server Metadata
|
|
||||||
if (url === "/.well-known/oauth-authorization-server" && req.method === "GET") {
|
|
||||||
res.writeHead(200, { "Content-Type": "application/json" });
|
|
||||||
res.end(JSON.stringify({
|
|
||||||
issuer: origin,
|
|
||||||
authorization_endpoint: `${origin}/oauth/authorize`,
|
|
||||||
token_endpoint: `${origin}/oauth/token`,
|
|
||||||
registration_endpoint: `${origin}/oauth/register`,
|
|
||||||
response_types_supported: ["code"],
|
|
||||||
grant_types_supported: ["authorization_code"],
|
|
||||||
code_challenge_methods_supported: ["S256", "plain"],
|
|
||||||
token_endpoint_auth_methods_supported: ["none"],
|
|
||||||
scopes_supported: ["mcp"],
|
|
||||||
}));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// RFC 7591 Dynamic Client Registration — accept anything, return dummy client
|
|
||||||
if (url === "/oauth/register" && req.method === "POST") {
|
|
||||||
const body = await readBody(req);
|
|
||||||
let reg: any = {};
|
|
||||||
try { reg = JSON.parse(body); } catch { /* ignore */ }
|
|
||||||
const clientId = `cocos-mcp-client-${Date.now()}`;
|
|
||||||
res.writeHead(200, { "Content-Type": "application/json" });
|
|
||||||
res.end(JSON.stringify({
|
|
||||||
client_id: clientId,
|
|
||||||
client_id_issued_at: Math.floor(Date.now() / 1000),
|
|
||||||
client_name: reg.client_name || "cocos-creator-mcp client",
|
|
||||||
redirect_uris: reg.redirect_uris || [],
|
|
||||||
token_endpoint_auth_method: "none",
|
|
||||||
grant_types: ["authorization_code"],
|
|
||||||
response_types: ["code"],
|
|
||||||
}));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// OAuth authorization endpoint — auto-consent, redirect immediately with code
|
|
||||||
if (url.startsWith("/oauth/authorize") && req.method === "GET") {
|
|
||||||
const parsed = new URL(url, origin);
|
|
||||||
const redirectUri = parsed.searchParams.get("redirect_uri") || "";
|
|
||||||
const state = parsed.searchParams.get("state") || "";
|
|
||||||
if (!redirectUri) {
|
|
||||||
res.writeHead(400, { "Content-Type": "application/json" });
|
|
||||||
res.end(JSON.stringify({ error: "invalid_request", error_description: "redirect_uri required" }));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const code = `cocos-mcp-code-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
||||||
const location = `${redirectUri}${redirectUri.includes("?") ? "&" : "?"}code=${encodeURIComponent(code)}&state=${encodeURIComponent(state)}`;
|
|
||||||
res.writeHead(302, { Location: location });
|
|
||||||
res.end();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// OAuth token endpoint — always issue a dummy token
|
|
||||||
if (url === "/oauth/token" && req.method === "POST") {
|
|
||||||
await readBody(req); // drain
|
|
||||||
res.writeHead(200, {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Cache-Control": "no-store",
|
|
||||||
});
|
|
||||||
res.end(JSON.stringify({
|
|
||||||
access_token: "cocos-mcp-public-token",
|
|
||||||
token_type: "Bearer",
|
|
||||||
expires_in: 86400,
|
|
||||||
scope: "mcp",
|
|
||||||
}));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Health check
|
|
||||||
if (url === "/health" && req.method === "GET") {
|
|
||||||
res.writeHead(200, { "Content-Type": "application/json" });
|
|
||||||
res.end(JSON.stringify({ status: "ok", tools: this.getAllTools().length }));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Game debug command queue — game polls for commands
|
|
||||||
if (url === "/game/command" && req.method === "GET") {
|
|
||||||
const cmd = _pendingCommand;
|
|
||||||
_pendingCommand = null; // consume
|
|
||||||
res.writeHead(200, { "Content-Type": "application/json" });
|
|
||||||
res.end(JSON.stringify(cmd));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Game debug command result — game posts result
|
|
||||||
if (url === "/game/result" && req.method === "POST") {
|
|
||||||
const body = await readBody(req);
|
|
||||||
try {
|
|
||||||
_commandResult = JSON.parse(body);
|
|
||||||
} catch { /* ignore */ }
|
|
||||||
res.writeHead(204);
|
|
||||||
res.end();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Game preview recording receiver
|
|
||||||
if (url === "/game/recording" && req.method === "POST") {
|
|
||||||
const body = await readBody(req);
|
|
||||||
try {
|
|
||||||
const { id, base64, mimeType, savePath } = JSON.parse(body);
|
|
||||||
if (!id || !base64) throw new Error("id/base64 required");
|
|
||||||
|
|
||||||
const fs = require("fs");
|
|
||||||
const path = require("path");
|
|
||||||
const buffer = Buffer.from(base64, "base64");
|
|
||||||
|
|
||||||
// savePath指定があればそこに保存(絶対パスまたはプロジェクト相対パス)
|
|
||||||
const projectPath = (global as any).Editor?.Project?.path
|
|
||||||
|| process.cwd();
|
|
||||||
let dir: string;
|
|
||||||
if (savePath) {
|
|
||||||
dir = path.isAbsolute(savePath) ? savePath : path.join(projectPath, savePath);
|
|
||||||
} else {
|
|
||||||
dir = path.join(projectPath, "temp", "recordings");
|
|
||||||
}
|
|
||||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
||||||
const mt = (mimeType || "").toLowerCase();
|
|
||||||
const ext = mt.includes("webm") ? "webm"
|
|
||||||
: mt.includes("mp4") ? "mp4"
|
|
||||||
: "bin";
|
|
||||||
const fileName = `${id}.${ext}`;
|
|
||||||
const filePath = path.join(dir, fileName);
|
|
||||||
fs.writeFileSync(filePath, buffer);
|
|
||||||
|
|
||||||
setRecording(id, {
|
|
||||||
path: filePath,
|
|
||||||
size: buffer.length,
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
});
|
|
||||||
if (this.config.autoArchiveRecordings) {
|
|
||||||
archiveOldFiles(dir);
|
|
||||||
}
|
|
||||||
res.writeHead(200, { "Content-Type": "application/json" });
|
|
||||||
res.end(JSON.stringify({ success: true, path: filePath, size: buffer.length }));
|
|
||||||
} catch (e: any) {
|
|
||||||
res.writeHead(400, { "Content-Type": "application/json" });
|
|
||||||
res.end(JSON.stringify({ success: false, error: e.message }));
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Game preview log receiver
|
|
||||||
if (url === "/log" && req.method === "POST") {
|
|
||||||
const body = await readBody(req);
|
|
||||||
try {
|
|
||||||
const entries: GameLogEntry[] = JSON.parse(body);
|
|
||||||
for (const entry of (Array.isArray(entries) ? entries : [entries])) {
|
|
||||||
_gameLogs.push({
|
|
||||||
timestamp: entry.timestamp || new Date().toISOString(),
|
|
||||||
level: entry.level || "log",
|
|
||||||
message: entry.message || "",
|
|
||||||
});
|
|
||||||
// __debug_state__ ログから userId を debug-menu.json に保存
|
|
||||||
try {
|
|
||||||
const msg = JSON.parse(entry.message || "");
|
|
||||||
if (msg.__debug_state__ && msg.userId) {
|
|
||||||
const _fs = require("fs");
|
|
||||||
const _path = require("path");
|
|
||||||
const projectPath = (global as any).Editor?.Project?.path || process.cwd();
|
|
||||||
const settingsPath = _path.join(projectPath, "settings", "debug-menu.json");
|
|
||||||
_fs.writeFileSync(settingsPath, JSON.stringify({ userId: msg.userId }, null, 2), "utf-8");
|
|
||||||
}
|
|
||||||
} catch { /* not debug_state */ }
|
|
||||||
}
|
|
||||||
if (_gameLogs.length > MAX_GAME_LOG_BUFFER) {
|
|
||||||
_gameLogs.splice(0, _gameLogs.length - MAX_GAME_LOG_BUFFER);
|
|
||||||
}
|
|
||||||
} catch { /* ignore malformed */ }
|
|
||||||
res.writeHead(204);
|
|
||||||
res.end();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// MCP endpoint
|
|
||||||
if (url === "/mcp") {
|
|
||||||
if (req.method === "GET") {
|
|
||||||
// SSE keepalive stream
|
|
||||||
res.writeHead(200, {
|
|
||||||
"Content-Type": "text/event-stream",
|
|
||||||
"Cache-Control": "no-cache",
|
|
||||||
"Connection": "keep-alive",
|
|
||||||
});
|
|
||||||
// Send initial comment to keep connection alive
|
|
||||||
res.write(": connected\n\n");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (req.method === "POST") {
|
|
||||||
await this.handleMcpPost(req, res);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (req.method === "DELETE") {
|
|
||||||
res.writeHead(200, { "Content-Type": "application/json" });
|
|
||||||
res.end(JSON.stringify({ ok: true }));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 404
|
|
||||||
res.writeHead(404, { "Content-Type": "application/json" });
|
|
||||||
res.end(JSON.stringify({ error: "Not found" }));
|
|
||||||
}
|
|
||||||
|
|
||||||
private async handleMcpPost(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
|
||||||
const body = await readBody(req);
|
|
||||||
let rpc: JsonRpcRequest;
|
|
||||||
try {
|
|
||||||
rpc = JSON.parse(body);
|
|
||||||
} catch {
|
|
||||||
this.sendJsonRpc(res, { jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error" } });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const accept = req.headers["accept"] || "";
|
|
||||||
const wantSse = accept.includes("text/event-stream");
|
|
||||||
|
|
||||||
let response: JsonRpcResponse;
|
|
||||||
|
|
||||||
switch (rpc.method) {
|
|
||||||
case "initialize":
|
|
||||||
response = {
|
|
||||||
jsonrpc: "2.0",
|
|
||||||
id: rpc.id,
|
|
||||||
result: {
|
|
||||||
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
||||||
capabilities: { tools: {} },
|
|
||||||
serverInfo: {
|
|
||||||
name: "cocos-creator-mcp",
|
|
||||||
version: "1.0.0",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "notifications/initialized":
|
|
||||||
// No response needed for notification
|
|
||||||
res.writeHead(204, { "Mcp-Session-Id": SESSION_ID });
|
|
||||||
res.end();
|
|
||||||
return;
|
|
||||||
|
|
||||||
case "tools/list":
|
|
||||||
response = {
|
|
||||||
jsonrpc: "2.0",
|
|
||||||
id: rpc.id,
|
|
||||||
result: { tools: this.getAllTools() },
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "tools/call": {
|
|
||||||
const toolName = rpc.params?.name;
|
|
||||||
const args = rpc.params?.arguments || {};
|
|
||||||
const category = this.toolIndex.get(toolName);
|
|
||||||
|
|
||||||
if (!category) {
|
|
||||||
response = {
|
|
||||||
jsonrpc: "2.0",
|
|
||||||
id: rpc.id,
|
|
||||||
error: { code: -32602, message: `Unknown tool: ${toolName}` },
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
const start = Date.now();
|
|
||||||
console.log(`[cocos-creator-mcp] ▶ ${toolName}`, Object.keys(args).length > 0 ? JSON.stringify(args).substring(0, 200) : "");
|
|
||||||
const timeoutMs = (toolName.startsWith("prefab_") || toolName === "scene_open") ? 120000 : 30000;
|
|
||||||
const result = await withTimeout(category.execute(toolName, args), timeoutMs, `Tool ${toolName} timed out`);
|
|
||||||
console.log(`[cocos-creator-mcp] ✓ ${toolName} (${Date.now() - start}ms)`);
|
|
||||||
response = {
|
|
||||||
jsonrpc: "2.0",
|
|
||||||
id: rpc.id,
|
|
||||||
result,
|
|
||||||
};
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error(`[cocos-creator-mcp] ✗ ${toolName}:`, e.message || String(e));
|
|
||||||
response = {
|
|
||||||
jsonrpc: "2.0",
|
|
||||||
id: rpc.id,
|
|
||||||
error: { code: -32603, message: e.message || String(e) },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
response = {
|
|
||||||
jsonrpc: "2.0",
|
|
||||||
id: rpc.id,
|
|
||||||
error: { code: -32601, message: `Method not found: ${rpc.method}` },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (wantSse) {
|
|
||||||
this.sendSse(res, [response]);
|
|
||||||
} else {
|
|
||||||
this.sendJsonRpc(res, response);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private sendJsonRpc(res: http.ServerResponse, data: JsonRpcResponse): void {
|
|
||||||
res.writeHead(200, {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Mcp-Session-Id": SESSION_ID,
|
|
||||||
});
|
|
||||||
res.end(JSON.stringify(data));
|
|
||||||
}
|
|
||||||
|
|
||||||
private sendSse(res: http.ServerResponse, messages: JsonRpcResponse[]): void {
|
|
||||||
res.writeHead(200, {
|
|
||||||
"Content-Type": "text/event-stream",
|
|
||||||
"Cache-Control": "no-cache",
|
|
||||||
"Mcp-Session-Id": SESSION_ID,
|
|
||||||
});
|
|
||||||
for (const msg of messages) {
|
|
||||||
res.write(`event: message\ndata: ${JSON.stringify(msg)}\n\n`);
|
|
||||||
}
|
|
||||||
res.end();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function withTimeout<T>(promise: Promise<T>, ms: number, message: string): Promise<T> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const timer = setTimeout(() => reject(new Error(message)), ms);
|
|
||||||
promise.then(
|
|
||||||
(v) => { clearTimeout(timer); resolve(v); },
|
|
||||||
(e) => { clearTimeout(timer); reject(e); },
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function readBody(req: http.IncomingMessage): Promise<string> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const chunks: Buffer[] = [];
|
|
||||||
req.on("data", (c) => chunks.push(c));
|
|
||||||
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
|
|
||||||
req.on("error", reject);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
/**
|
|
||||||
* Shared node UUID resolution — resolves nodeName to UUID via scene script.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const EXT_NAME = "cocos-creator-mcp";
|
|
||||||
|
|
||||||
export interface ResolvedNode {
|
|
||||||
uuid: string;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve a node UUID from either `uuid` or `nodeName` parameter.
|
|
||||||
* If `uuid` is provided, it is returned as-is.
|
|
||||||
* If `nodeName` is provided, searches the scene for a matching node.
|
|
||||||
* Throws if neither is provided or no node is found.
|
|
||||||
*/
|
|
||||||
export async function resolveNodeUuid(args: { uuid?: string; nodeName?: string }): Promise<ResolvedNode> {
|
|
||||||
if (args.uuid) {
|
|
||||||
return { uuid: args.uuid, name: "" };
|
|
||||||
}
|
|
||||||
if (!args.nodeName) {
|
|
||||||
throw new Error("Either 'uuid' or 'nodeName' is required");
|
|
||||||
}
|
|
||||||
const result = await Editor.Message.request("scene", "execute-scene-script", {
|
|
||||||
name: EXT_NAME,
|
|
||||||
method: "findNodesByName",
|
|
||||||
args: [args.nodeName],
|
|
||||||
});
|
|
||||||
if (!result?.success || !result?.data?.length) {
|
|
||||||
throw new Error(`Node not found: "${args.nodeName}"`);
|
|
||||||
}
|
|
||||||
// 最初のマッチを使用
|
|
||||||
const node = result.data[0];
|
|
||||||
return { uuid: node.uuid, name: node.name || args.nodeName };
|
|
||||||
}
|
|
||||||
-117
@@ -1,117 +0,0 @@
|
|||||||
const { createApp } = require("vue");
|
|
||||||
|
|
||||||
const panelDataMap = new WeakMap<any, any>();
|
|
||||||
|
|
||||||
module.exports = Editor.Panel.define({
|
|
||||||
template: `
|
|
||||||
<div id="app">
|
|
||||||
<h2>Cocos Creator MCP</h2>
|
|
||||||
<div class="status">
|
|
||||||
<span>Status: <strong :class="running ? 'on' : 'off'">{{ running ? 'Running' : 'Stopped' }}</strong></span>
|
|
||||||
</div>
|
|
||||||
<div class="port-row">
|
|
||||||
<label>Port:</label>
|
|
||||||
<input type="number" v-model.number="editPort" :disabled="running" min="1024" max="65535" />
|
|
||||||
<ui-button v-if="!running && editPort !== port" @confirm="applyPort" class="small-btn">Apply</ui-button>
|
|
||||||
</div>
|
|
||||||
<div class="actions">
|
|
||||||
<ui-button v-if="!running" @confirm="start">Start Server</ui-button>
|
|
||||||
<ui-button v-if="running" @confirm="stop">Stop Server</ui-button>
|
|
||||||
</div>
|
|
||||||
<div v-if="running" class="info">
|
|
||||||
<p>Endpoint: <code>http://127.0.0.1:{{ port }}/mcp</code></p>
|
|
||||||
<p>Tools: <strong>{{ toolCount }}</strong></p>
|
|
||||||
</div>
|
|
||||||
<div v-if="error" class="error">{{ error }}</div>
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
style: `
|
|
||||||
#app { padding: 12px; font-family: sans-serif; color: #ccc; }
|
|
||||||
h2 { margin: 0 0 8px 0; font-size: 16px; }
|
|
||||||
.status { margin: 8px 0; }
|
|
||||||
.on { color: #4f4; }
|
|
||||||
.off { color: #f66; }
|
|
||||||
.port-row { margin: 8px 0; display: flex; align-items: center; gap: 8px; }
|
|
||||||
.port-row label { font-size: 12px; }
|
|
||||||
.port-row input { width: 80px; padding: 3px 6px; background: #222; color: #ccc; border: 1px solid #444; border-radius: 3px; font-size: 12px; }
|
|
||||||
.port-row input:disabled { opacity: 0.5; }
|
|
||||||
.actions { margin: 8px 0; }
|
|
||||||
.info { margin: 8px 0; padding: 8px; background: var(--color-normal-fill-emphasis); border-radius: 4px; }
|
|
||||||
.info p { margin: 4px 0; font-size: 12px; }
|
|
||||||
.info code { background: #333; padding: 2px 6px; border-radius: 3px; font-size: 11px; }
|
|
||||||
.error { margin: 8px 0; color: #f66; font-size: 12px; }
|
|
||||||
`,
|
|
||||||
$: { app: "#app" },
|
|
||||||
ready() {
|
|
||||||
if (!this.$.app) return;
|
|
||||||
const app = createApp({
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
running: false,
|
|
||||||
port: 3000,
|
|
||||||
editPort: 3000,
|
|
||||||
toolCount: 0,
|
|
||||||
error: "",
|
|
||||||
};
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
async start(this: any) {
|
|
||||||
try {
|
|
||||||
this.error = "";
|
|
||||||
const result = await Editor.Message.request("cocos-creator-mcp", "start-server");
|
|
||||||
this.running = result.running;
|
|
||||||
this.port = result.port;
|
|
||||||
this.editPort = result.port;
|
|
||||||
await this.refresh();
|
|
||||||
} catch (e: any) {
|
|
||||||
this.error = e.message || String(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async stop(this: any) {
|
|
||||||
try {
|
|
||||||
await Editor.Message.request("cocos-creator-mcp", "stop-server");
|
|
||||||
this.running = false;
|
|
||||||
this.toolCount = 0;
|
|
||||||
} catch (e: any) {
|
|
||||||
this.error = e.message || String(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async applyPort(this: any) {
|
|
||||||
try {
|
|
||||||
this.error = "";
|
|
||||||
const result = await Editor.Message.request("cocos-creator-mcp", "update-port", this.editPort);
|
|
||||||
this.port = result.port;
|
|
||||||
this.running = result.running;
|
|
||||||
if (this.running) await this.refresh();
|
|
||||||
} catch (e: any) {
|
|
||||||
this.error = e.message || String(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async refresh(this: any) {
|
|
||||||
try {
|
|
||||||
const status = await Editor.Message.request("cocos-creator-mcp", "get-server-status");
|
|
||||||
this.running = status.running;
|
|
||||||
this.port = status.port;
|
|
||||||
this.editPort = status.port;
|
|
||||||
this.toolCount = status.toolCount;
|
|
||||||
} catch (e: any) {
|
|
||||||
console.warn("[cocos-creator-mcp] refresh failed:", e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
async mounted(this: any) {
|
|
||||||
try {
|
|
||||||
await this.refresh();
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("[cocos-creator-mcp] mounted refresh failed:", e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
app.mount(this.$.app);
|
|
||||||
panelDataMap.set(this, app);
|
|
||||||
},
|
|
||||||
close() {
|
|
||||||
const app = panelDataMap.get(this);
|
|
||||||
if (app) app.unmount();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
-540
@@ -1,540 +0,0 @@
|
|||||||
const { createApp: createAppRec } = require("vue");
|
|
||||||
|
|
||||||
const panelDataMapRec = new WeakMap<any, any>();
|
|
||||||
|
|
||||||
module.exports = Editor.Panel.define({
|
|
||||||
template: `
|
|
||||||
<div id="app">
|
|
||||||
<h2>Preview Recorder</h2>
|
|
||||||
|
|
||||||
<div class="controls">
|
|
||||||
<button v-if="!recording" @click="start" class="btn btn-start">● 録画開始</button>
|
|
||||||
<button v-else @click="stop" class="btn btn-stop" :disabled="stopping">■ 録画停止{{ stopping ? '中...' : '' }}</button>
|
|
||||||
<button @click="screenshot" class="btn btn-shot" :disabled="shooting">📸 スクショ{{ shooting ? '中...' : '' }}</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="recording" class="status-row">
|
|
||||||
<span class="rec-dot">●</span> REC <strong>{{ elapsed }}s</strong>
|
|
||||||
<span class="info">{{ recordingInfo }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="section-title">録画設定</div>
|
|
||||||
<div class="row">
|
|
||||||
<label>FPS:</label>
|
|
||||||
<input type="number" v-model.number="fps" :disabled="recording" min="10" max="60" />
|
|
||||||
<label>形式:</label>
|
|
||||||
<select v-model="format" :disabled="recording">
|
|
||||||
<option value="mp4">MP4</option>
|
|
||||||
<option value="webm">WebM</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<label>品質:</label>
|
|
||||||
<select v-model="quality" @change="onQualityChange" :disabled="recording">
|
|
||||||
<option value="low">低</option>
|
|
||||||
<option value="medium">中</option>
|
|
||||||
<option value="high">高</option>
|
|
||||||
<option value="ultra">最高</option>
|
|
||||||
<option value="custom">カスタム</option>
|
|
||||||
</select>
|
|
||||||
<label title="ビットレート = 幅 × 高さ × FPS × 品質係数">品質係数:</label>
|
|
||||||
<input type="number" v-model.number="coefficient" @input="onCoefChange"
|
|
||||||
:disabled="recording" min="0.01" max="2" step="0.01" class="custom-bitrate"
|
|
||||||
title="ビットレート = 幅 × 高さ × FPS × 品質係数" />
|
|
||||||
<button @click="resetQuality" class="btn btn-small" :disabled="recording" title="録画設定を初期値に戻す">↺</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="section-title">スクショ設定</div>
|
|
||||||
<div class="row">
|
|
||||||
<label>形式:</label>
|
|
||||||
<select v-model="shotFormat" :disabled="shooting">
|
|
||||||
<option value="png">PNG</option>
|
|
||||||
<option value="webp">WebP</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="section-title">保存先</div>
|
|
||||||
<div class="row">
|
|
||||||
<input type="text" v-model="savePath" :disabled="recording" class="path-input" placeholder="temp/recordings" />
|
|
||||||
<button @click="selectSaveFolder" class="btn btn-small" :disabled="recording">📁 選択</button>
|
|
||||||
<button @click="resetSavePath" class="btn btn-small" :disabled="recording" title="保存先を初期値に戻す">↺</button>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<button @click="openSaveFolder" class="btn btn-small">📂 保存フォルダを開く</button>
|
|
||||||
<label class="checkbox-label" title="ONにすると、録画/スクショ保存時に24時間以上前のファイルを OLD_yyyyMM フォルダに自動移動します"><input type="checkbox" v-model="autoArchive" @change="onAutoArchiveChange" /> 古いファイルを自動整理</label>
|
|
||||||
<button class="btn btn-help" @click="showArchiveHelp = !showArchiveHelp" title="自動整理の説明">?</button>
|
|
||||||
</div>
|
|
||||||
<div v-if="showArchiveHelp" class="help-box">
|
|
||||||
<strong>自動整理について</strong><br>
|
|
||||||
ONにすると、録画やスクショを保存するたびに、24時間以上前の古いファイルを<br>
|
|
||||||
<code>OLD_yyyyMM/</code> フォルダ(例: OLD_202604/)に自動で移動します。<br>
|
|
||||||
保存フォルダ直下には直近のファイルだけが残り、整理された状態を保てます。
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="lastResult" class="result" :class="lastError ? 'error' : 'success'">
|
|
||||||
<div v-if="!lastError">
|
|
||||||
<strong>✓ {{ lastResult.kind === 'shot' ? 'スクショ保存' : '録画完了' }}</strong><br>
|
|
||||||
<code>{{ lastResult.path }}</code><br>
|
|
||||||
{{ (lastResult.size / 1024).toFixed(1) }} KB
|
|
||||||
</div>
|
|
||||||
<div v-else>
|
|
||||||
<strong>✗ エラー:</strong> {{ lastResult.error || lastResult.message || 'unknown' }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="note">
|
|
||||||
※ ゲームプレビュー実行中に使ってください(GameDebugClientが必要)
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
style: `
|
|
||||||
#app { padding: 16px; font-family: sans-serif; color: #ccc; font-size: 12px; }
|
|
||||||
h2 { margin: 0 0 12px 0; font-size: 18px; }
|
|
||||||
.controls { margin: 12px 0; }
|
|
||||||
.btn {
|
|
||||||
padding: 10px 20px;
|
|
||||||
color: #fff;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.btn:disabled { background: #555; color: #999; cursor: not-allowed; }
|
|
||||||
.btn-start { background: #d44; }
|
|
||||||
.btn-start:hover { background: #e55; }
|
|
||||||
.btn-stop { background: #888; }
|
|
||||||
.btn-stop:hover { background: #999; }
|
|
||||||
.btn-shot { background: #468; margin-left: 8px; }
|
|
||||||
.btn-shot:hover { background: #579; }
|
|
||||||
.section-title {
|
|
||||||
margin-top: 14px;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
padding: 4px 0 3px 0;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #8af;
|
|
||||||
border-bottom: 1px solid #333;
|
|
||||||
}
|
|
||||||
.btn-small {
|
|
||||||
padding: 4px 10px;
|
|
||||||
background: #4a8;
|
|
||||||
font-size: 11px;
|
|
||||||
margin-left: 8px;
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
.btn-small:hover { background: #5b9; }
|
|
||||||
.status-row { margin: 10px 0; font-size: 14px; }
|
|
||||||
.rec-dot { color: #f44; animation: blink 1s infinite; }
|
|
||||||
@keyframes blink { 50% { opacity: 0.3; } }
|
|
||||||
.info { color: #888; font-size: 11px; margin-left: 8px; }
|
|
||||||
.row { margin: 10px 0; display: flex; align-items: center; gap: 8px; flex-wrap: nowrap; }
|
|
||||||
.row label { font-size: 12px; }
|
|
||||||
.row input { width: 60px; padding: 4px 8px; background: #222; color: #ccc; border: 1px solid #444; border-radius: 3px; }
|
|
||||||
.row select { padding: 4px 8px; background: #222; color: #ccc; border: 1px solid #444; border-radius: 3px; }
|
|
||||||
.path-input { flex: 1; min-width: 200px; width: auto !important; font-family: monospace; }
|
|
||||||
.custom-bitrate { width: 70px !important; }
|
|
||||||
.unit { font-size: 11px; color: #888; }
|
|
||||||
.result { margin: 12px 0; padding: 10px; border-radius: 4px; font-size: 12px; line-height: 1.5; }
|
|
||||||
.result.success { background: #1a3a1a; color: #afa; }
|
|
||||||
.result.error { background: #3a1a1a; color: #faa; }
|
|
||||||
.result code { font-size: 10px; word-break: break-all; background: #000; padding: 2px 4px; border-radius: 2px; }
|
|
||||||
.checkbox-label { font-size: 12px; display: flex; align-items: center; gap: 4px; margin-left: auto; cursor: pointer; }
|
|
||||||
.checkbox-label input[type="checkbox"] { cursor: pointer; }
|
|
||||||
.btn-help { padding: 2px 7px; background: #555; font-size: 11px; font-weight: bold; border-radius: 50%; min-width: 20px; margin-left: 4px; }
|
|
||||||
.btn-help:hover { background: #777; }
|
|
||||||
.help-box { margin: 8px 0; padding: 10px; background: #1a2a3a; border: 1px solid #345; border-radius: 4px; font-size: 11px; line-height: 1.6; color: #bcd; }
|
|
||||||
.help-box code { background: #000; padding: 1px 4px; border-radius: 2px; font-size: 10px; }
|
|
||||||
.note { margin-top: 16px; padding-top: 8px; border-top: 1px solid #333; font-size: 10px; color: #888; }
|
|
||||||
`,
|
|
||||||
$: { app: "#app" },
|
|
||||||
ready() {
|
|
||||||
if (!this.$.app) return;
|
|
||||||
const MCP_BASE = "http://127.0.0.1:3000";
|
|
||||||
const STORAGE_KEY = "cocos-mcp-recorder-settings";
|
|
||||||
const PERSISTED_KEYS = ["fps", "quality", "coefficient", "format", "savePath", "shotFormat"];
|
|
||||||
// localStorage から設定を読み込み
|
|
||||||
const loadSettings = () => {
|
|
||||||
try {
|
|
||||||
const raw = localStorage.getItem(STORAGE_KEY);
|
|
||||||
return raw ? JSON.parse(raw) : {};
|
|
||||||
} catch { return {}; }
|
|
||||||
};
|
|
||||||
const saved = loadSettings();
|
|
||||||
// プロジェクト設定から autoArchiveRecordings を読み込み
|
|
||||||
const loadProjectConfig = () => {
|
|
||||||
try {
|
|
||||||
const fs = require("fs");
|
|
||||||
const path = require("path");
|
|
||||||
const p = path.join(Editor.Project.path, "settings", "cocos-creator-mcp.json");
|
|
||||||
if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
||||||
} catch { /* ignore */ }
|
|
||||||
return {};
|
|
||||||
};
|
|
||||||
const projectCfg = loadProjectConfig();
|
|
||||||
const app = createAppRec({
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
recording: false,
|
|
||||||
stopping: false,
|
|
||||||
shooting: false,
|
|
||||||
elapsed: "0.0",
|
|
||||||
recordingInfo: "",
|
|
||||||
fps: saved.fps ?? 30,
|
|
||||||
quality: saved.quality ?? "medium",
|
|
||||||
coefficient: saved.coefficient ?? 0.25,
|
|
||||||
format: saved.format ?? "mp4",
|
|
||||||
savePath: saved.savePath ?? "temp/recordings",
|
|
||||||
shotFormat: saved.shotFormat ?? "png",
|
|
||||||
autoArchive: projectCfg.autoArchiveRecordings ?? false,
|
|
||||||
showArchiveHelp: false,
|
|
||||||
lastResult: null as any,
|
|
||||||
lastError: false,
|
|
||||||
_startTime: 0,
|
|
||||||
_timer: null as any,
|
|
||||||
_aliveCheckTimer: null as any,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
fps(this: any) { this.saveSettings(); },
|
|
||||||
quality(this: any) { this.saveSettings(); },
|
|
||||||
coefficient(this: any) { this.saveSettings(); },
|
|
||||||
format(this: any) { this.saveSettings(); },
|
|
||||||
savePath(this: any) { this.saveSettings(); },
|
|
||||||
shotFormat(this: any) { this.saveSettings(); },
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
saveSettings(this: any) {
|
|
||||||
const data: any = {};
|
|
||||||
for (const key of PERSISTED_KEYS) data[key] = this[key];
|
|
||||||
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); } catch { /* ignore */ }
|
|
||||||
},
|
|
||||||
async isPreviewRunning(this: any): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
const res = await fetch(`${MCP_BASE}/mcp`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
jsonrpc: "2.0", id: 98, method: "tools/call",
|
|
||||||
params: {
|
|
||||||
name: "debug_game_command",
|
|
||||||
arguments: { type: "inspect", args: { name: "Canvas" }, timeout: 1500 },
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
const json = await res.json();
|
|
||||||
const content = json.result?.content?.[0]?.text;
|
|
||||||
const parsed = content ? JSON.parse(content) : null;
|
|
||||||
return !!parsed?.success;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async start(this: any) {
|
|
||||||
this.lastResult = null;
|
|
||||||
if (!await this.isPreviewRunning()) {
|
|
||||||
this.lastResult = { error: "ゲームプレビューが実行されていません。プレビューを開始してから録画してください。" };
|
|
||||||
this.lastError = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const res = await fetch(`${MCP_BASE}/mcp`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
jsonrpc: "2.0",
|
|
||||||
id: 1,
|
|
||||||
method: "tools/call",
|
|
||||||
params: {
|
|
||||||
name: "debug_record_start",
|
|
||||||
arguments: {
|
|
||||||
fps: this.fps,
|
|
||||||
coefficient: this.coefficient,
|
|
||||||
format: this.format,
|
|
||||||
savePath: this.savePath,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
const json = await res.json();
|
|
||||||
const content = json.result?.content?.[0]?.text;
|
|
||||||
const parsed = content ? JSON.parse(content) : null;
|
|
||||||
if (parsed?.success && parsed.data?.id) {
|
|
||||||
this.recording = true;
|
|
||||||
const d = parsed.data;
|
|
||||||
const mbps = d?.videoBitsPerSecond ? (d.videoBitsPerSecond / 1_000_000).toFixed(2) : "?";
|
|
||||||
this.recordingInfo = `${d?.canvasWidth || "?"}x${d?.canvasHeight || "?"} @ ${d?.fps || "?"}fps / ${mbps}Mbps / ${d?.mimeType || ""}`;
|
|
||||||
this._startTime = Date.now();
|
|
||||||
this._timer = setInterval(() => {
|
|
||||||
this.elapsed = ((Date.now() - this._startTime) / 1000).toFixed(1);
|
|
||||||
}, 100);
|
|
||||||
// プレビュー停止検知用ポーリング(2秒毎)
|
|
||||||
this._aliveCheckTimer = setInterval(() => {
|
|
||||||
this.checkPreviewAlive();
|
|
||||||
}, 2000);
|
|
||||||
} else {
|
|
||||||
// 可能な限り詳細なエラー情報を表示
|
|
||||||
const errDetail = parsed?.data?.error
|
|
||||||
|| parsed?.error
|
|
||||||
|| (parsed?.data ? JSON.stringify(parsed.data) : null)
|
|
||||||
|| (parsed ? JSON.stringify(parsed).substring(0, 200) : "no response")
|
|
||||||
|| "録画開始失敗";
|
|
||||||
this.lastResult = { error: errDetail };
|
|
||||||
this.lastError = true;
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
this.lastResult = { error: `通信エラー: ${e.message}` };
|
|
||||||
this.lastError = true;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async stop(this: any) {
|
|
||||||
this.stopping = true;
|
|
||||||
try {
|
|
||||||
const res = await fetch(`${MCP_BASE}/mcp`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
jsonrpc: "2.0",
|
|
||||||
id: 2,
|
|
||||||
method: "tools/call",
|
|
||||||
params: {
|
|
||||||
name: "debug_record_stop",
|
|
||||||
arguments: { timeout: 60000 },
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
const json = await res.json();
|
|
||||||
const content = json.result?.content?.[0]?.text;
|
|
||||||
const parsed = content ? JSON.parse(content) : null;
|
|
||||||
if (parsed?.success && parsed.data?.path) {
|
|
||||||
this.lastResult = { path: parsed.data.path, size: parsed.data.size };
|
|
||||||
this.lastError = false;
|
|
||||||
} else {
|
|
||||||
const errDetail = parsed?.data?.error
|
|
||||||
|| parsed?.error
|
|
||||||
|| (parsed?.data ? JSON.stringify(parsed.data) : null)
|
|
||||||
|| "録画停止失敗";
|
|
||||||
this.lastResult = { error: errDetail };
|
|
||||||
this.lastError = true;
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
this.lastResult = { error: `通信エラー: ${e.message}` };
|
|
||||||
this.lastError = true;
|
|
||||||
} finally {
|
|
||||||
this.recording = false;
|
|
||||||
this.stopping = false;
|
|
||||||
if (this._timer) { clearInterval(this._timer); this._timer = null; }
|
|
||||||
if (this._aliveCheckTimer) { clearInterval(this._aliveCheckTimer); this._aliveCheckTimer = null; }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onQualityChange(this: any) {
|
|
||||||
const map: Record<string, number> = { low: 0.15, medium: 0.25, high: 0.40, ultra: 0.60 };
|
|
||||||
if (this.quality !== "custom") this.coefficient = map[this.quality];
|
|
||||||
},
|
|
||||||
onCoefChange(this: any) {
|
|
||||||
const map: Record<number, string> = { 0.15: "low", 0.25: "medium", 0.40: "high", 0.60: "ultra" };
|
|
||||||
this.quality = map[this.coefficient] || "custom";
|
|
||||||
},
|
|
||||||
resetQuality(this: any) {
|
|
||||||
this.fps = 30;
|
|
||||||
this.quality = "medium";
|
|
||||||
this.coefficient = 0.25;
|
|
||||||
this.format = "mp4";
|
|
||||||
},
|
|
||||||
resetSavePath(this: any) {
|
|
||||||
this.savePath = "temp/recordings";
|
|
||||||
},
|
|
||||||
onAutoArchiveChange(this: any) {
|
|
||||||
try {
|
|
||||||
const fs = require("fs");
|
|
||||||
const path = require("path");
|
|
||||||
const p = path.join(Editor.Project.path, "settings", "cocos-creator-mcp.json");
|
|
||||||
let cfg: any = {};
|
|
||||||
if (fs.existsSync(p)) cfg = JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
||||||
cfg.autoArchiveRecordings = this.autoArchive;
|
|
||||||
const dir = path.dirname(p);
|
|
||||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
||||||
fs.writeFileSync(p, JSON.stringify(cfg, null, 2), "utf-8");
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("[recorder] 設定保存失敗:", e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async screenshot(this: any) {
|
|
||||||
this.shooting = true;
|
|
||||||
if (!await this.isPreviewRunning()) {
|
|
||||||
this.lastResult = { error: "ゲームプレビューが実行されていません。プレビューを開始してからスクショしてください。" };
|
|
||||||
this.lastError = true;
|
|
||||||
this.shooting = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const res = await fetch(`${MCP_BASE}/mcp`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
jsonrpc: "2.0",
|
|
||||||
id: 3,
|
|
||||||
method: "tools/call",
|
|
||||||
params: {
|
|
||||||
name: "debug_game_command",
|
|
||||||
arguments: { type: "screenshot", args: {}, timeout: 5000, maxWidth: 0, imageFormat: this.shotFormat },
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
const json = await res.json();
|
|
||||||
const content = json.result?.content?.[0]?.text;
|
|
||||||
const parsed = content ? JSON.parse(content) : null;
|
|
||||||
if (parsed?.success && parsed.path) {
|
|
||||||
// savePath 配下にコピー
|
|
||||||
const fs = require("fs");
|
|
||||||
const path = require("path");
|
|
||||||
const projectPath = Editor.Project.path;
|
|
||||||
let destDir = this.savePath || "temp/recordings";
|
|
||||||
if (!path.isAbsolute(destDir)) destDir = path.join(projectPath, destDir);
|
|
||||||
if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
|
|
||||||
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
|
||||||
const ext = path.extname(parsed.path) || ".png";
|
|
||||||
const destPath = path.join(destDir, `screenshot_${ts}${ext}`);
|
|
||||||
fs.copyFileSync(parsed.path, destPath);
|
|
||||||
// 設定に応じて古いファイルをアーカイブ
|
|
||||||
try {
|
|
||||||
const settingsPath = path.join(projectPath, "settings", "cocos-creator-mcp.json");
|
|
||||||
if (fs.existsSync(settingsPath)) {
|
|
||||||
const cfg = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
|
|
||||||
if (cfg.autoArchiveRecordings) {
|
|
||||||
const { archiveOldFiles } = require("../../archive");
|
|
||||||
archiveOldFiles(destDir);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch { /* ignore */ }
|
|
||||||
this.lastResult = { kind: "shot", path: destPath, size: parsed.size };
|
|
||||||
this.lastError = false;
|
|
||||||
} else {
|
|
||||||
const errDetail = parsed?.error || parsed?.message || "スクショ失敗";
|
|
||||||
this.lastResult = { error: errDetail };
|
|
||||||
this.lastError = true;
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
this.lastResult = { error: `通信エラー: ${e.message}` };
|
|
||||||
this.lastError = true;
|
|
||||||
} finally {
|
|
||||||
this.shooting = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async checkPreviewAlive(this: any) {
|
|
||||||
if (!this.recording) return;
|
|
||||||
try {
|
|
||||||
// MCP経由で軽量なinspectコマンドを送る
|
|
||||||
const res = await fetch(`${MCP_BASE}/mcp`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
jsonrpc: "2.0", id: 99, method: "tools/call",
|
|
||||||
params: {
|
|
||||||
name: "debug_game_command",
|
|
||||||
arguments: { type: "inspect", args: { name: "Canvas" }, timeout: 1500 },
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
const json = await res.json();
|
|
||||||
const content = json.result?.content?.[0]?.text;
|
|
||||||
const parsed = content ? JSON.parse(content) : null;
|
|
||||||
if (parsed?.success) {
|
|
||||||
// プレビュー生存
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 応答なし → プレビュー停止とみなして録画停止状態に
|
|
||||||
console.warn("[PreviewRecorder] preview not responding, stopping recording state");
|
|
||||||
this.recording = false;
|
|
||||||
if (this._timer) { clearInterval(this._timer); this._timer = null; }
|
|
||||||
if (this._aliveCheckTimer) { clearInterval(this._aliveCheckTimer); this._aliveCheckTimer = null; }
|
|
||||||
this.lastResult = { error: "プレビューが停止したため録画を中断しました(動画は保存されません)" };
|
|
||||||
this.lastError = true;
|
|
||||||
} catch (e) {
|
|
||||||
// ネットワークエラーは通信エラーとして無視(一時的かもしれない)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async selectSaveFolder(this: any) {
|
|
||||||
try {
|
|
||||||
const result = await (Editor.Dialog as any).select({
|
|
||||||
title: "保存先フォルダを選択",
|
|
||||||
type: "directory",
|
|
||||||
multi: false,
|
|
||||||
});
|
|
||||||
if (result?.filePaths?.length) {
|
|
||||||
const path = require("path");
|
|
||||||
const projectPath = Editor.Project.path;
|
|
||||||
const absPath = result.filePaths[0];
|
|
||||||
// プロジェクト配下なら相対パスで保持
|
|
||||||
const relPath = path.relative(projectPath, absPath);
|
|
||||||
if (!relPath.startsWith("..") && !path.isAbsolute(relPath)) {
|
|
||||||
this.savePath = relPath.replace(/\\/g, "/");
|
|
||||||
} else {
|
|
||||||
this.savePath = absPath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error("[PreviewRecorder] selectSaveFolder failed:", e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
openSaveFolder(this: any) {
|
|
||||||
const path = require("path");
|
|
||||||
const fs = require("fs");
|
|
||||||
const projectPath = Editor.Project.path;
|
|
||||||
let dir = this.savePath || "temp/recordings";
|
|
||||||
if (!path.isAbsolute(dir)) dir = path.join(projectPath, dir);
|
|
||||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
||||||
try {
|
|
||||||
const { spawn } = require("child_process");
|
|
||||||
const platform = process.platform;
|
|
||||||
const [cmd, ...args] = platform === "win32"
|
|
||||||
? ["explorer.exe", dir.replace(/\//g, "\\")]
|
|
||||||
: platform === "darwin"
|
|
||||||
? ["open", dir]
|
|
||||||
: ["xdg-open", dir];
|
|
||||||
const p = spawn(cmd, args, { detached: true, stdio: "ignore" });
|
|
||||||
p.unref();
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error("[PreviewRecorder] openSaveFolder failed:", e);
|
|
||||||
this.lastResult = { error: `フォルダを開けませんでした: ${e.message}` };
|
|
||||||
this.lastError = true;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
openFolder(this: any) {
|
|
||||||
if (!this.lastResult?.path) return;
|
|
||||||
const filePath = this.lastResult.path;
|
|
||||||
try {
|
|
||||||
const { shell } = require("electron");
|
|
||||||
if (shell?.showItemInFolder) {
|
|
||||||
shell.showItemInFolder(filePath);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (e) { /* fallback */ }
|
|
||||||
// フォールバック: OS別コマンド
|
|
||||||
try {
|
|
||||||
const { exec } = require("child_process");
|
|
||||||
const platform = process.platform;
|
|
||||||
if (platform === "win32") {
|
|
||||||
exec(`explorer.exe /select,"${filePath.replace(/\//g, "\\")}"`);
|
|
||||||
} else if (platform === "darwin") {
|
|
||||||
exec(`open -R "${filePath}"`);
|
|
||||||
} else {
|
|
||||||
const dir = require("path").dirname(filePath);
|
|
||||||
exec(`xdg-open "${dir}"`);
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error("[PreviewRecorder] openFolder failed:", e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
app.mount(this.$.app);
|
|
||||||
panelDataMapRec.set(this, app);
|
|
||||||
},
|
|
||||||
beforeClose() { },
|
|
||||||
close() {
|
|
||||||
const app = panelDataMapRec.get(this);
|
|
||||||
if (app) app.unmount();
|
|
||||||
panelDataMapRec.delete(this);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,509 +0,0 @@
|
|||||||
/**
|
|
||||||
* Scene script — runs inside CocosCreator's scene renderer process.
|
|
||||||
* These methods are called via Editor.Message.request('scene', 'execute-scene-script', ...)
|
|
||||||
* and have access to the `cc` module (engine runtime).
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { join } from "path";
|
|
||||||
module.paths.push(join(Editor.App.path, "node_modules"));
|
|
||||||
|
|
||||||
// ─── Console Log Buffer ───
|
|
||||||
|
|
||||||
interface ConsoleLogEntry {
|
|
||||||
timestamp: string;
|
|
||||||
level: "log" | "warn" | "error";
|
|
||||||
message: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const MAX_LOG_BUFFER = 500;
|
|
||||||
const _consoleLogs: ConsoleLogEntry[] = [];
|
|
||||||
|
|
||||||
const _originalLog = console.log;
|
|
||||||
const _originalWarn = console.warn;
|
|
||||||
const _originalError = console.error;
|
|
||||||
|
|
||||||
function formatArgs(args: any[]): string {
|
|
||||||
return args.map(a => {
|
|
||||||
if (typeof a === "string") return a;
|
|
||||||
try { return JSON.stringify(a); } catch { return String(a); }
|
|
||||||
}).join(" ");
|
|
||||||
}
|
|
||||||
|
|
||||||
function pushLog(level: ConsoleLogEntry["level"], args: any[]): void {
|
|
||||||
_consoleLogs.push({
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
level,
|
|
||||||
message: formatArgs(args),
|
|
||||||
});
|
|
||||||
if (_consoleLogs.length > MAX_LOG_BUFFER) {
|
|
||||||
_consoleLogs.splice(0, _consoleLogs.length - MAX_LOG_BUFFER);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log = function (...args: any[]) {
|
|
||||||
_originalLog.apply(console, args);
|
|
||||||
pushLog("log", args);
|
|
||||||
};
|
|
||||||
|
|
||||||
console.warn = function (...args: any[]) {
|
|
||||||
_originalWarn.apply(console, args);
|
|
||||||
pushLog("warn", args);
|
|
||||||
};
|
|
||||||
|
|
||||||
console.error = function (...args: any[]) {
|
|
||||||
_originalError.apply(console, args);
|
|
||||||
pushLog("error", args);
|
|
||||||
};
|
|
||||||
|
|
||||||
function getScene() {
|
|
||||||
const { director } = require("cc");
|
|
||||||
return director.getScene();
|
|
||||||
}
|
|
||||||
|
|
||||||
function findNode(uuid: string) {
|
|
||||||
const scene = getScene();
|
|
||||||
if (!scene) return null;
|
|
||||||
|
|
||||||
// Recursive search — getChildByUuid is not recursive in cc
|
|
||||||
const queue = [...scene.children];
|
|
||||||
while (queue.length > 0) {
|
|
||||||
const node = queue.shift()!;
|
|
||||||
if (node.uuid === uuid) return node;
|
|
||||||
if (node.children) queue.push(...node.children);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recursively build a node tree from a JSON spec.
|
|
||||||
* Returns { uuid, name, children: [...] }
|
|
||||||
*/
|
|
||||||
function buildNodeRecursive(parent: any, spec: any): any {
|
|
||||||
const { Node, js, Vec3 } = require("cc");
|
|
||||||
|
|
||||||
const node = new Node(spec.name || "Node");
|
|
||||||
parent.addChild(node);
|
|
||||||
|
|
||||||
// Add components
|
|
||||||
if (spec.components && Array.isArray(spec.components)) {
|
|
||||||
const { Sprite, Label } = require("cc");
|
|
||||||
for (const compName of spec.components) {
|
|
||||||
const CompClass = js.getClassByName(compName);
|
|
||||||
if (CompClass) {
|
|
||||||
if (!node.getComponent(CompClass)) {
|
|
||||||
const comp = node.addComponent(CompClass);
|
|
||||||
// Sprite: sizeMode=CUSTOM でUITransformサイズの上書きを防ぐ
|
|
||||||
if (comp instanceof Sprite) {
|
|
||||||
comp.sizeMode = 0; // SizeMode.CUSTOM
|
|
||||||
}
|
|
||||||
// Label: useSystemFont + 色を黒に(白パネル上で見えるように)
|
|
||||||
if (comp instanceof Label) {
|
|
||||||
comp.useSystemFont = true;
|
|
||||||
const { Color } = require("cc");
|
|
||||||
comp.color = new Color(51, 51, 51, 255);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set component properties
|
|
||||||
// Format: { "cc.UITransform.contentSize": {width:720, height:1280} }
|
|
||||||
if (spec.properties) {
|
|
||||||
for (const [key, value] of Object.entries(spec.properties)) {
|
|
||||||
const dotIdx = key.lastIndexOf(".");
|
|
||||||
if (dotIdx < 0) continue;
|
|
||||||
const compName = key.substring(0, dotIdx);
|
|
||||||
const propName = key.substring(dotIdx + 1);
|
|
||||||
|
|
||||||
const CompClass = js.getClassByName(compName);
|
|
||||||
if (!CompClass) continue;
|
|
||||||
const comp = node.getComponent(CompClass);
|
|
||||||
if (!comp) continue;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// contentSize needs special handling (Size type)
|
|
||||||
if (propName === "contentSize" && value && typeof value === "object") {
|
|
||||||
comp.setContentSize((value as any).width ?? 0, (value as any).height ?? 0);
|
|
||||||
} else {
|
|
||||||
comp[propName] = value;
|
|
||||||
}
|
|
||||||
} catch { /* skip invalid property */ }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set UITransform anchorPoint if specified
|
|
||||||
if (spec.anchorPoint) {
|
|
||||||
const { UITransform } = require("cc");
|
|
||||||
const ut = node.getComponent(UITransform);
|
|
||||||
if (ut) ut.setAnchorPoint(spec.anchorPoint.x ?? 0.5, spec.anchorPoint.y ?? 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set node properties (position, scale, active)
|
|
||||||
if (spec.active === false) node.active = false;
|
|
||||||
if (spec.position) node.setPosition(spec.position.x || 0, spec.position.y || 0, spec.position.z || 0);
|
|
||||||
if (spec.scale) node.setScale(spec.scale.x ?? 1, spec.scale.y ?? 1, spec.scale.z ?? 1);
|
|
||||||
|
|
||||||
// Set Widget properties if specified
|
|
||||||
// Format: { top: 0, bottom: 0, left: 0, right: 0 } — each field enables the corresponding alignment
|
|
||||||
if (spec.widget) {
|
|
||||||
const { Widget } = require("cc");
|
|
||||||
let w = node.getComponent(Widget);
|
|
||||||
if (!w) w = node.addComponent(Widget);
|
|
||||||
const wSpec = spec.widget;
|
|
||||||
if (wSpec.top !== undefined) { w.isAlignTop = true; w.top = wSpec.top; }
|
|
||||||
if (wSpec.bottom !== undefined) { w.isAlignBottom = true; w.bottom = wSpec.bottom; }
|
|
||||||
if (wSpec.left !== undefined) { w.isAlignLeft = true; w.left = wSpec.left; }
|
|
||||||
if (wSpec.right !== undefined) { w.isAlignRight = true; w.right = wSpec.right; }
|
|
||||||
if (wSpec.horizontalCenter !== undefined) { w.isAlignHorizontalCenter = true; w.horizontalCenter = wSpec.horizontalCenter; }
|
|
||||||
if (wSpec.verticalCenter !== undefined) { w.isAlignVerticalCenter = true; w.verticalCenter = wSpec.verticalCenter; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build children
|
|
||||||
const childResults: any[] = [];
|
|
||||||
if (spec.children && Array.isArray(spec.children)) {
|
|
||||||
for (const childSpec of spec.children) {
|
|
||||||
childResults.push(buildNodeRecursive(node, childSpec));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { uuid: node.uuid, name: node.name, children: childResults };
|
|
||||||
}
|
|
||||||
|
|
||||||
function collectNodeInfo(node: any, includeComponents: boolean = false): any {
|
|
||||||
const info: any = {
|
|
||||||
uuid: node.uuid,
|
|
||||||
name: node.name,
|
|
||||||
active: node.active,
|
|
||||||
position: { x: node.position.x, y: node.position.y, z: node.position.z },
|
|
||||||
scale: { x: node.scale.x, y: node.scale.y, z: node.scale.z },
|
|
||||||
parent: node.parent?.uuid || null,
|
|
||||||
childCount: node.children?.length || 0,
|
|
||||||
};
|
|
||||||
if (includeComponents && node.components) {
|
|
||||||
info.components = node.components.map((c: any) => ({
|
|
||||||
type: c.constructor.name,
|
|
||||||
uuid: c.uuid,
|
|
||||||
enabled: c.enabled,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
return info;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const methods: Record<string, (...args: any[]) => any> = {
|
|
||||||
getSceneHierarchy(includeComponents: boolean = false) {
|
|
||||||
try {
|
|
||||||
const scene = getScene();
|
|
||||||
if (!scene) return { success: false, error: "No active scene" };
|
|
||||||
|
|
||||||
const walk = (node: any): any => {
|
|
||||||
const item: any = {
|
|
||||||
uuid: node.uuid,
|
|
||||||
name: node.name,
|
|
||||||
active: node.active,
|
|
||||||
children: [],
|
|
||||||
};
|
|
||||||
if (includeComponents && node.components) {
|
|
||||||
item.components = node.components.map((c: any) => ({
|
|
||||||
type: c.constructor.name,
|
|
||||||
uuid: c.uuid,
|
|
||||||
enabled: c.enabled,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
if (node.children) {
|
|
||||||
item.children = node.children.map((ch: any) => walk(ch));
|
|
||||||
}
|
|
||||||
return item;
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
sceneName: scene.name,
|
|
||||||
sceneUuid: scene.uuid,
|
|
||||||
hierarchy: scene.children.map((ch: any) => walk(ch)),
|
|
||||||
};
|
|
||||||
} catch (e: any) {
|
|
||||||
return { success: false, error: e.message };
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
getNodeInfo(uuid: string) {
|
|
||||||
try {
|
|
||||||
const node = findNode(uuid);
|
|
||||||
if (!node) return { success: false, error: `Node ${uuid} not found` };
|
|
||||||
return { success: true, data: collectNodeInfo(node, true) };
|
|
||||||
} catch (e: any) {
|
|
||||||
return { success: false, error: e.message };
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
getAllNodes() {
|
|
||||||
try {
|
|
||||||
const scene = getScene();
|
|
||||||
if (!scene) return { success: false, error: "No active scene" };
|
|
||||||
|
|
||||||
const nodes: any[] = [];
|
|
||||||
const walk = (node: any) => {
|
|
||||||
nodes.push(collectNodeInfo(node));
|
|
||||||
if (node.children) node.children.forEach(walk);
|
|
||||||
};
|
|
||||||
scene.children.forEach(walk);
|
|
||||||
return { success: true, data: nodes };
|
|
||||||
} catch (e: any) {
|
|
||||||
return { success: false, error: e.message };
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 指定ノードの子孫から名前で検索する。auto_bind 用。
|
|
||||||
* rootUuid が空の場合はシーン全体を検索(後方互換)。
|
|
||||||
*/
|
|
||||||
findDescendantsByName(rootUuid: string, name: string) {
|
|
||||||
try {
|
|
||||||
let root: any;
|
|
||||||
if (rootUuid) {
|
|
||||||
root = findNode(rootUuid);
|
|
||||||
if (!root) return { success: false, error: `Root node ${rootUuid} not found` };
|
|
||||||
} else {
|
|
||||||
root = getScene();
|
|
||||||
if (!root) return { success: false, error: "No active scene" };
|
|
||||||
}
|
|
||||||
|
|
||||||
const results: any[] = [];
|
|
||||||
const walk = (node: any) => {
|
|
||||||
if (node.name === name) results.push(collectNodeInfo(node));
|
|
||||||
if (node.children) node.children.forEach(walk);
|
|
||||||
};
|
|
||||||
if (root.children) root.children.forEach(walk);
|
|
||||||
return { success: true, data: results };
|
|
||||||
} catch (e: any) {
|
|
||||||
return { success: false, error: e.message };
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 指定ノードの全子孫を depth 付きで返す。auto_bind の一括検索用。
|
|
||||||
*/
|
|
||||||
getAllDescendants(rootUuid: string) {
|
|
||||||
try {
|
|
||||||
const root = findNode(rootUuid);
|
|
||||||
if (!root) return { success: false, error: `Node ${rootUuid} not found` };
|
|
||||||
|
|
||||||
const results: Array<{uuid: string, name: string, depth: number}> = [];
|
|
||||||
const walk = (node: any, depth: number) => {
|
|
||||||
results.push({ uuid: node.uuid, name: node.name, depth });
|
|
||||||
if (node.children) node.children.forEach((c: any) => walk(c, depth + 1));
|
|
||||||
};
|
|
||||||
if (root.children) root.children.forEach((c: any) => walk(c, 1));
|
|
||||||
return { success: true, data: results };
|
|
||||||
} catch (e: any) {
|
|
||||||
return { success: false, error: e.message };
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
findNodesByName(name: string) {
|
|
||||||
try {
|
|
||||||
const scene = getScene();
|
|
||||||
if (!scene) return { success: false, error: "No active scene" };
|
|
||||||
|
|
||||||
const results: any[] = [];
|
|
||||||
const walk = (node: any) => {
|
|
||||||
if (node.name === name) results.push(collectNodeInfo(node));
|
|
||||||
if (node.children) node.children.forEach(walk);
|
|
||||||
};
|
|
||||||
scene.children.forEach(walk);
|
|
||||||
return { success: true, data: results };
|
|
||||||
} catch (e: any) {
|
|
||||||
return { success: false, error: e.message };
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
findNodeByPath(path: string) {
|
|
||||||
try {
|
|
||||||
const scene = getScene();
|
|
||||||
if (!scene) return { success: false, error: "No active scene" };
|
|
||||||
|
|
||||||
const parts = path.split("/");
|
|
||||||
let current: any = null;
|
|
||||||
|
|
||||||
const walk = (node: any): any => {
|
|
||||||
if (node.name === parts[0]) return node;
|
|
||||||
if (node.children) {
|
|
||||||
for (const child of node.children) {
|
|
||||||
const found = walk(child);
|
|
||||||
if (found) return found;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
for (const child of scene.children) {
|
|
||||||
current = walk(child);
|
|
||||||
if (current) break;
|
|
||||||
}
|
|
||||||
if (!current) return { success: false, error: `Node "${parts[0]}" not found` };
|
|
||||||
|
|
||||||
for (let i = 1; i < parts.length; i++) {
|
|
||||||
const child = current.children?.find((c: any) => c.name === parts[i]);
|
|
||||||
if (!child) return { success: false, error: `Child "${parts[i]}" not found in "${current.name}"` };
|
|
||||||
current = child;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: true, data: collectNodeInfo(current) };
|
|
||||||
} catch (e: any) {
|
|
||||||
return { success: false, error: e.message };
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
setNodeProperty(uuid: string, property: string, value: any) {
|
|
||||||
try {
|
|
||||||
const node = findNode(uuid);
|
|
||||||
if (!node) return { success: false, error: `Node ${uuid} not found` };
|
|
||||||
|
|
||||||
switch (property) {
|
|
||||||
case "position":
|
|
||||||
node.setPosition(value.x ?? 0, value.y ?? 0, value.z ?? 0);
|
|
||||||
break;
|
|
||||||
case "rotation":
|
|
||||||
node.setRotationFromEuler(value.x ?? 0, value.y ?? 0, value.z ?? 0);
|
|
||||||
break;
|
|
||||||
case "scale":
|
|
||||||
node.setScale(value.x ?? 1, value.y ?? 1, value.z ?? 1);
|
|
||||||
break;
|
|
||||||
case "active":
|
|
||||||
node.active = !!value;
|
|
||||||
break;
|
|
||||||
case "name":
|
|
||||||
node.name = String(value);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
(node as any)[property] = value;
|
|
||||||
}
|
|
||||||
return { success: true };
|
|
||||||
} catch (e: any) {
|
|
||||||
return { success: false, error: e.message };
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
setComponentProperty(uuid: string, componentType: string, property: string, value: any) {
|
|
||||||
try {
|
|
||||||
const { js } = require("cc");
|
|
||||||
const node = findNode(uuid);
|
|
||||||
if (!node) return { success: false, error: `Node ${uuid} not found` };
|
|
||||||
|
|
||||||
const CompClass = js.getClassByName(componentType);
|
|
||||||
if (!CompClass) return { success: false, error: `Component class ${componentType} not found` };
|
|
||||||
|
|
||||||
const comp = node.getComponent(CompClass);
|
|
||||||
if (!comp) return { success: false, error: `Component ${componentType} not on node ${uuid}` };
|
|
||||||
|
|
||||||
comp[property] = value;
|
|
||||||
return { success: true };
|
|
||||||
} catch (e: any) {
|
|
||||||
return { success: false, error: e.message };
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
addComponentToNode(uuid: string, componentType: string) {
|
|
||||||
try {
|
|
||||||
const { js } = require("cc");
|
|
||||||
const node = findNode(uuid);
|
|
||||||
if (!node) return { success: false, error: `Node ${uuid} not found` };
|
|
||||||
|
|
||||||
const CompClass = js.getClassByName(componentType);
|
|
||||||
if (!CompClass) return { success: false, error: `Component class ${componentType} not found` };
|
|
||||||
|
|
||||||
const comp = node.addComponent(CompClass);
|
|
||||||
return { success: true, data: { uuid: comp.uuid, type: componentType } };
|
|
||||||
} catch (e: any) {
|
|
||||||
return { success: false, error: e.message };
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
moveNode(uuid: string, parentUuid: string) {
|
|
||||||
try {
|
|
||||||
const node = findNode(uuid);
|
|
||||||
if (!node) return { success: false, error: `Node ${uuid} not found` };
|
|
||||||
const parent = findNode(parentUuid);
|
|
||||||
if (!parent) return { success: false, error: `Parent ${parentUuid} not found` };
|
|
||||||
node.setParent(parent);
|
|
||||||
return { success: true };
|
|
||||||
} catch (e: any) {
|
|
||||||
return { success: false, error: e.message };
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build a node tree from a JSON spec in one call.
|
|
||||||
* Spec format:
|
|
||||||
* {
|
|
||||||
* name: string,
|
|
||||||
* components?: string[], // e.g. ["cc.UITransform", "cc.Layout"]
|
|
||||||
* properties?: Record<string, any>, // e.g. { "cc.UITransform.contentSize": {width:720,height:1280} }
|
|
||||||
* children?: NodeSpec[]
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
buildNodeTree(parentUuid: string, spec: any) {
|
|
||||||
try {
|
|
||||||
const { Node, js, UITransform } = require("cc");
|
|
||||||
const parent = findNode(parentUuid);
|
|
||||||
if (!parent) return { success: false, error: `Parent ${parentUuid} not found` };
|
|
||||||
|
|
||||||
const result = buildNodeRecursive(parent, spec);
|
|
||||||
return { success: true, data: result };
|
|
||||||
} catch (e: any) {
|
|
||||||
return { success: false, error: e.message };
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
testLog(message: string = "test message") {
|
|
||||||
console.log("[testLog]", message);
|
|
||||||
const methods = Object.keys(exports.methods || {});
|
|
||||||
return { success: true, bufferSize: _consoleLogs.length, methods };
|
|
||||||
},
|
|
||||||
|
|
||||||
getConsoleLogs(count: number = 50, level?: string) {
|
|
||||||
let logs = _consoleLogs;
|
|
||||||
if (level) {
|
|
||||||
logs = logs.filter(l => l.level === level);
|
|
||||||
}
|
|
||||||
return { success: true, logs: logs.slice(-count), total: _consoleLogs.length };
|
|
||||||
},
|
|
||||||
|
|
||||||
clearConsoleLogs() {
|
|
||||||
_consoleLogs.length = 0;
|
|
||||||
return { success: true };
|
|
||||||
},
|
|
||||||
|
|
||||||
async setPropertyViaEditor(nodeUuid: string, path: string, dump: any) {
|
|
||||||
try {
|
|
||||||
// scene:set-property API
|
|
||||||
// uuid: ノードUUID(コンポーネントUUIDではない)
|
|
||||||
// path: __comps__.{index}.{property} 形式
|
|
||||||
// dump: { value, type } 形式
|
|
||||||
const opts = { uuid: nodeUuid, path, dump };
|
|
||||||
const result = await (Editor as any).Message.request("scene", "set-property", opts);
|
|
||||||
return { success: true, result };
|
|
||||||
} catch (e: any) {
|
|
||||||
return { success: false, error: e.message || String(e) };
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
removeComponentFromNode(uuid: string, componentType: string) {
|
|
||||||
try {
|
|
||||||
const { js } = require("cc");
|
|
||||||
const node = findNode(uuid);
|
|
||||||
if (!node) return { success: false, error: `Node ${uuid} not found` };
|
|
||||||
|
|
||||||
const CompClass = js.getClassByName(componentType);
|
|
||||||
if (!CompClass) return { success: false, error: `Component class ${componentType} not found` };
|
|
||||||
|
|
||||||
const comp = node.getComponent(CompClass);
|
|
||||||
if (!comp) return { success: false, error: `Component ${componentType} not on node` };
|
|
||||||
|
|
||||||
node.removeComponent(comp);
|
|
||||||
return { success: true };
|
|
||||||
} catch (e: any) {
|
|
||||||
return { success: false, error: e.message };
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
/**
|
|
||||||
* Shared screenshot utility — used by debug-tools and other tools that need screenshots.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export interface ProcessedImage {
|
|
||||||
buffer: Buffer;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
format: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ScreenshotResult {
|
|
||||||
success: boolean;
|
|
||||||
path: string;
|
|
||||||
size: number;
|
|
||||||
format: string;
|
|
||||||
originalSize: string;
|
|
||||||
savedSize: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function processImage(pngBuffer: Buffer, maxWidth: number, desiredFormat?: string): Promise<ProcessedImage> {
|
|
||||||
try {
|
|
||||||
const Vips = require("wasm-vips");
|
|
||||||
const vips = await Vips();
|
|
||||||
let image = vips.Image.newFromBuffer(pngBuffer);
|
|
||||||
if (maxWidth > 0 && image.width > maxWidth) {
|
|
||||||
image = image.thumbnailImage(maxWidth);
|
|
||||||
}
|
|
||||||
if (desiredFormat === "png") {
|
|
||||||
const pngOut = image.pngsaveBuffer();
|
|
||||||
const result = { buffer: Buffer.from(pngOut), width: image.width, height: image.height, format: "png" };
|
|
||||||
image.delete();
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
const outBuf = image.webpsaveBuffer({ Q: 85 });
|
|
||||||
const result = { buffer: Buffer.from(outBuf), width: image.width, height: image.height, format: "webp" };
|
|
||||||
image.delete();
|
|
||||||
return result;
|
|
||||||
} catch {
|
|
||||||
// Fallback: NativeImage resize + JPEG
|
|
||||||
const electron = require("electron");
|
|
||||||
let image = electron.nativeImage.createFromBuffer(pngBuffer);
|
|
||||||
if (maxWidth > 0) {
|
|
||||||
const size = image.getSize();
|
|
||||||
if (size.width > maxWidth) {
|
|
||||||
const ratio = maxWidth / size.width;
|
|
||||||
image = image.resize({ width: Math.round(size.width * ratio), height: Math.round(size.height * ratio) });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const size = image.getSize();
|
|
||||||
const buffer = image.toJPEG(85);
|
|
||||||
return { buffer, width: size.width, height: size.height, format: "jpeg" };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function takeEditorScreenshot(savePath?: string | undefined, maxWidth?: number): Promise<ScreenshotResult> {
|
|
||||||
const fs = require("fs");
|
|
||||||
const path = require("path");
|
|
||||||
const electron = require("electron");
|
|
||||||
const windows = electron.BrowserWindow.getAllWindows();
|
|
||||||
if (!windows || windows.length === 0) {
|
|
||||||
throw new Error("No editor window found");
|
|
||||||
}
|
|
||||||
// Find the main (largest) window
|
|
||||||
let win = windows[0];
|
|
||||||
let maxArea = 0;
|
|
||||||
for (const w of windows) {
|
|
||||||
const bounds = w.getBounds();
|
|
||||||
const area = bounds.width * bounds.height;
|
|
||||||
if (area > maxArea) {
|
|
||||||
maxArea = area;
|
|
||||||
win = w;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Bring to front and wait for render
|
|
||||||
win.show();
|
|
||||||
await new Promise(r => setTimeout(r, 300));
|
|
||||||
const nativeImage = await win.webContents.capturePage();
|
|
||||||
const originalSize = nativeImage.getSize();
|
|
||||||
const pngBuffer = nativeImage.toPNG();
|
|
||||||
|
|
||||||
const effectiveMaxWidth = maxWidth !== undefined ? maxWidth : 960;
|
|
||||||
const { buffer, width, height, format } = await processImage(pngBuffer, effectiveMaxWidth);
|
|
||||||
|
|
||||||
const ext = format === "webp" ? "webp" : format === "jpeg" ? "jpg" : "png";
|
|
||||||
if (!savePath) {
|
|
||||||
const dir = path.join(Editor.Project.tmpDir, "screenshots");
|
|
||||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
||||||
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
||||||
savePath = path.join(dir, `screenshot_${timestamp}.${ext}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const finalPath = savePath!;
|
|
||||||
fs.writeFileSync(finalPath, buffer);
|
|
||||||
return {
|
|
||||||
success: true, path: finalPath, size: buffer.length, format,
|
|
||||||
originalSize: `${originalSize.width}x${originalSize.height}`,
|
|
||||||
savedSize: `${width}x${height}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import { ToolResult } from "./types";
|
|
||||||
|
|
||||||
/** Helper to create a successful text result */
|
|
||||||
export function ok(data: any): ToolResult {
|
|
||||||
return {
|
|
||||||
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Helper to create an error result */
|
|
||||||
export function err(message: string): ToolResult {
|
|
||||||
return {
|
|
||||||
content: [{ type: "text", text: JSON.stringify({ error: message }) }],
|
|
||||||
isError: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Validate that a string looks like a CocosCreator UUID (not empty, reasonable format) */
|
|
||||||
export function validateUuid(uuid: string, label: string = "uuid"): string | null {
|
|
||||||
if (!uuid || typeof uuid !== "string") {
|
|
||||||
return `${label} is required`;
|
|
||||||
}
|
|
||||||
if (uuid.trim().length === 0) {
|
|
||||||
return `${label} cannot be empty`;
|
|
||||||
}
|
|
||||||
// CocosCreator UUIDs: either standard format (8-4-4-4-12) or compressed (22 chars with +/=)
|
|
||||||
// Be permissive — just reject obviously wrong values
|
|
||||||
if (uuid.length < 10) {
|
|
||||||
return `${label} "${uuid}" is too short to be a valid UUID`;
|
|
||||||
}
|
|
||||||
return null; // valid
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Validate UUID and return err() if invalid, null if valid */
|
|
||||||
export function checkUuid(uuid: string, label: string = "uuid"): ToolResult | null {
|
|
||||||
const error = validateUuid(uuid, label);
|
|
||||||
return error ? err(error) : null;
|
|
||||||
}
|
|
||||||
-290
@@ -1,290 +0,0 @@
|
|||||||
import { ToolCategory, ToolDefinition, ToolResult } from "../types";
|
|
||||||
import { ok, err } from "../tool-base";
|
|
||||||
|
|
||||||
export class AssetTools implements ToolCategory {
|
|
||||||
readonly categoryName = "asset";
|
|
||||||
|
|
||||||
getTools(): ToolDefinition[] {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
name: "asset_create",
|
|
||||||
description: "Create a new asset file in the project.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
path: { type: "string", description: "db:// path for the new asset" },
|
|
||||||
content: { type: "string", description: "File content (for text-based assets)" },
|
|
||||||
},
|
|
||||||
required: ["path"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "asset_delete",
|
|
||||||
description: "Delete an asset from the project.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
path: { type: "string", description: "db:// path of the asset to delete" },
|
|
||||||
},
|
|
||||||
required: ["path"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "asset_move",
|
|
||||||
description: "Move/rename an asset.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
source: { type: "string", description: "Current db:// path" },
|
|
||||||
destination: { type: "string", description: "New db:// path" },
|
|
||||||
},
|
|
||||||
required: ["source", "destination"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "asset_copy",
|
|
||||||
description: "Copy an asset to a new location.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
source: { type: "string", description: "Source db:// path" },
|
|
||||||
destination: { type: "string", description: "Destination db:// path" },
|
|
||||||
},
|
|
||||||
required: ["source", "destination"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "asset_save",
|
|
||||||
description: "Save an asset by UUID.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
uuid: { type: "string", description: "Asset UUID" },
|
|
||||||
},
|
|
||||||
required: ["uuid"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "asset_reimport",
|
|
||||||
description: "Re-import an asset to refresh it.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
path: { type: "string", description: "db:// path of the asset" },
|
|
||||||
},
|
|
||||||
required: ["path"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "asset_query_path",
|
|
||||||
description: "Get the file path for an asset UUID.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
uuid: { type: "string", description: "Asset UUID" },
|
|
||||||
},
|
|
||||||
required: ["uuid"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "asset_query_uuid",
|
|
||||||
description: "Get the UUID for an asset path.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
path: { type: "string", description: "db:// path of the asset" },
|
|
||||||
},
|
|
||||||
required: ["path"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "asset_query_url",
|
|
||||||
description: "Get the URL for an asset UUID.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
uuid: { type: "string", description: "Asset UUID" },
|
|
||||||
},
|
|
||||||
required: ["uuid"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "asset_get_details",
|
|
||||||
description: "Get detailed metadata for an asset (type, size, importer, etc.).",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
uuid: { type: "string", description: "Asset UUID" },
|
|
||||||
},
|
|
||||||
required: ["uuid"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "asset_get_dependencies",
|
|
||||||
description: "Get all assets that a given asset depends on.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
uuid: { type: "string", description: "Asset UUID" },
|
|
||||||
},
|
|
||||||
required: ["uuid"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "asset_open_external",
|
|
||||||
description: "Open an asset in the system's default external editor.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
path: { type: "string", description: "db:// path of the asset" },
|
|
||||||
},
|
|
||||||
required: ["path"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "asset_import",
|
|
||||||
description: "Import an external file into the project assets.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
source: { type: "string", description: "Source file path on disk" },
|
|
||||||
target: { type: "string", description: "Target db:// path in project" },
|
|
||||||
},
|
|
||||||
required: ["source", "target"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "asset_save_meta",
|
|
||||||
description: "Save asset meta information (importer settings, etc.).",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
uuid: { type: "string", description: "Asset UUID" },
|
|
||||||
meta: { type: "string", description: "Meta JSON string" },
|
|
||||||
},
|
|
||||||
required: ["uuid", "meta"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "asset_generate_available_url",
|
|
||||||
description: "Generate a non-conflicting asset URL (avoids name collisions).",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
url: { type: "string", description: "Desired db:// path" },
|
|
||||||
},
|
|
||||||
required: ["url"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "asset_query_ready",
|
|
||||||
description: "Check if the asset database is ready.",
|
|
||||||
inputSchema: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
// ── 以下、既存MCP未対応のEditor API ──
|
|
||||||
{
|
|
||||||
name: "asset_query_users",
|
|
||||||
description: "Find all assets that use/reference a given asset.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: { uuid: { type: "string", description: "Asset UUID" } },
|
|
||||||
required: ["uuid"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "asset_query_missing",
|
|
||||||
description: "Check if an asset has missing references.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: { uuid: { type: "string", description: "Asset UUID" } },
|
|
||||||
required: ["uuid"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
async execute(toolName: string, args: Record<string, any>): Promise<ToolResult> {
|
|
||||||
try {
|
|
||||||
switch (toolName) {
|
|
||||||
case "asset_create":
|
|
||||||
await (Editor.Message.request as any)("asset-db", "create-asset", args.path, args.content || null);
|
|
||||||
return ok({ success: true, path: args.path });
|
|
||||||
case "asset_delete":
|
|
||||||
await (Editor.Message.request as any)("asset-db", "delete-asset", args.path);
|
|
||||||
return ok({ success: true, path: args.path });
|
|
||||||
case "asset_move":
|
|
||||||
await (Editor.Message.request as any)("asset-db", "move-asset", args.source, args.destination);
|
|
||||||
return ok({ success: true, source: args.source, destination: args.destination });
|
|
||||||
case "asset_copy":
|
|
||||||
await (Editor.Message.request as any)("asset-db", "copy-asset", args.source, args.destination);
|
|
||||||
return ok({ success: true, source: args.source, destination: args.destination });
|
|
||||||
case "asset_save":
|
|
||||||
await (Editor.Message.request as any)("asset-db", "save-asset", args.uuid);
|
|
||||||
return ok({ success: true, uuid: args.uuid });
|
|
||||||
case "asset_reimport":
|
|
||||||
await (Editor.Message.request as any)("asset-db", "reimport-asset", args.path);
|
|
||||||
return ok({ success: true, path: args.path });
|
|
||||||
case "asset_query_path": {
|
|
||||||
const path = await (Editor.Message.request as any)("asset-db", "query-path", args.uuid);
|
|
||||||
return ok({ success: true, uuid: args.uuid, path });
|
|
||||||
}
|
|
||||||
case "asset_query_uuid": {
|
|
||||||
const uuid = await (Editor.Message.request as any)("asset-db", "query-uuid", args.path);
|
|
||||||
return ok({ success: true, path: args.path, uuid });
|
|
||||||
}
|
|
||||||
case "asset_query_url": {
|
|
||||||
const url = await (Editor.Message.request as any)("asset-db", "query-url", args.uuid);
|
|
||||||
return ok({ success: true, uuid: args.uuid, url });
|
|
||||||
}
|
|
||||||
case "asset_get_details": {
|
|
||||||
const info = await (Editor.Message.request as any)("asset-db", "query-asset-info", args.uuid);
|
|
||||||
const meta = await (Editor.Message.request as any)("asset-db", "query-asset-meta", args.uuid).catch(() => null);
|
|
||||||
return ok({ success: true, info, meta });
|
|
||||||
}
|
|
||||||
case "asset_get_dependencies": {
|
|
||||||
// Try multiple API names as they vary by CocosCreator version
|
|
||||||
let deps;
|
|
||||||
try {
|
|
||||||
deps = await (Editor.Message.request as any)("asset-db", "query-depends", args.uuid);
|
|
||||||
} catch {
|
|
||||||
try {
|
|
||||||
deps = await (Editor.Message.request as any)("asset-db", "query-asset-depends", args.uuid);
|
|
||||||
} catch {
|
|
||||||
deps = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ok({ success: true, uuid: args.uuid, dependencies: deps });
|
|
||||||
}
|
|
||||||
case "asset_open_external":
|
|
||||||
await (Editor.Message.request as any)("asset-db", "open-asset", args.path);
|
|
||||||
return ok({ success: true, path: args.path });
|
|
||||||
case "asset_import":
|
|
||||||
await (Editor.Message.request as any)("asset-db", "import-asset", args.source, args.target);
|
|
||||||
return ok({ success: true, source: args.source, target: args.target });
|
|
||||||
case "asset_save_meta":
|
|
||||||
await (Editor.Message.request as any)("asset-db", "save-asset-meta", args.uuid, args.meta);
|
|
||||||
return ok({ success: true, uuid: args.uuid });
|
|
||||||
case "asset_generate_available_url": {
|
|
||||||
const url = await (Editor.Message.request as any)("asset-db", "generate-available-url", args.url);
|
|
||||||
return ok({ success: true, url });
|
|
||||||
}
|
|
||||||
case "asset_query_ready": {
|
|
||||||
const ready = await (Editor.Message.request as any)("asset-db", "query-ready");
|
|
||||||
return ok({ success: true, ready });
|
|
||||||
}
|
|
||||||
case "asset_query_users": {
|
|
||||||
const users = await (Editor.Message.request as any)("asset-db", "query-asset-users", args.uuid).catch(() => []);
|
|
||||||
return ok({ success: true, uuid: args.uuid, users });
|
|
||||||
}
|
|
||||||
case "asset_query_missing": {
|
|
||||||
const missing = await (Editor.Message.request as any)("asset-db", "query-missing-asset-info", args.uuid).catch(() => null);
|
|
||||||
return ok({ success: true, uuid: args.uuid, missing, hasMissing: !!missing });
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return err(`Unknown tool: ${toolName}`);
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
return err(e.message || String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-64
@@ -1,64 +0,0 @@
|
|||||||
import { ToolCategory, ToolDefinition, ToolResult } from "../types";
|
|
||||||
import { ok, err } from "../tool-base";
|
|
||||||
|
|
||||||
export class BuilderTools implements ToolCategory {
|
|
||||||
readonly categoryName = "builder";
|
|
||||||
|
|
||||||
getTools(): ToolDefinition[] {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
name: "builder_open_panel",
|
|
||||||
description: "Open the Build panel in the editor.",
|
|
||||||
inputSchema: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "builder_get_settings",
|
|
||||||
description: "Get the current build settings/configuration.",
|
|
||||||
inputSchema: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "builder_query_tasks",
|
|
||||||
description: "Query active build tasks.",
|
|
||||||
inputSchema: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "builder_run_preview",
|
|
||||||
description: "Start the preview server for browser testing.",
|
|
||||||
inputSchema: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "builder_stop_preview",
|
|
||||||
description: "Stop the preview server.",
|
|
||||||
inputSchema: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
async execute(toolName: string, _args: Record<string, any>): Promise<ToolResult> {
|
|
||||||
try {
|
|
||||||
switch (toolName) {
|
|
||||||
case "builder_open_panel":
|
|
||||||
Editor.Panel.open("builder");
|
|
||||||
return ok({ success: true });
|
|
||||||
case "builder_get_settings": {
|
|
||||||
const settings = await (Editor.Message.request as any)("builder", "query-build-options").catch(() => null);
|
|
||||||
return ok({ success: true, settings });
|
|
||||||
}
|
|
||||||
case "builder_query_tasks": {
|
|
||||||
const tasks = await (Editor.Message.request as any)("builder", "query-tasks").catch(() => []);
|
|
||||||
return ok({ success: true, tasks });
|
|
||||||
}
|
|
||||||
case "builder_run_preview":
|
|
||||||
await (Editor.Message.request as any)("preview", "start");
|
|
||||||
return ok({ success: true, message: "Preview started" });
|
|
||||||
case "builder_stop_preview":
|
|
||||||
await (Editor.Message.request as any)("preview", "stop");
|
|
||||||
return ok({ success: true, message: "Preview stopped" });
|
|
||||||
default:
|
|
||||||
return err(`Unknown tool: ${toolName}`);
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
return err(e.message || String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-687
@@ -1,687 +0,0 @@
|
|||||||
import { ToolCategory, ToolDefinition, ToolResult } from "../types";
|
|
||||||
import { ok, err } from "../tool-base";
|
|
||||||
import { parseMaybeJson } from "../utils";
|
|
||||||
import { resolveNodeUuid } from "../node-resolve";
|
|
||||||
import { takeEditorScreenshot } from "../screenshot";
|
|
||||||
|
|
||||||
const EXT_NAME = "cocos-creator-mcp";
|
|
||||||
|
|
||||||
export class ComponentTools implements ToolCategory {
|
|
||||||
readonly categoryName = "component";
|
|
||||||
|
|
||||||
getTools(): ToolDefinition[] {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
name: "component_add",
|
|
||||||
description: "Add a component to a node. Use cc.XXX format (e.g. 'cc.Label', 'cc.Sprite', 'cc.Button').",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
uuid: { type: "string", description: "Node UUID" },
|
|
||||||
componentType: { type: "string", description: "Component class name (e.g. 'cc.Label')" },
|
|
||||||
},
|
|
||||||
required: ["uuid", "componentType"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "component_remove",
|
|
||||||
description: "Remove a component from a node.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
uuid: { type: "string", description: "Node UUID" },
|
|
||||||
componentType: { type: "string", description: "Component class name to remove" },
|
|
||||||
},
|
|
||||||
required: ["uuid", "componentType"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "component_get_components",
|
|
||||||
description: "Get all components on a node with their properties.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
uuid: { type: "string", description: "Node UUID (either uuid or nodeName required)" },
|
|
||||||
nodeName: { type: "string", description: "Node name to find (alternative to uuid)" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "component_set_property",
|
|
||||||
description: "Set one or more properties on a component. For single: use property+value. For batch: use properties array. Use nodeName instead of uuid to find node by name. Set screenshot=true to capture editor screenshot after changes. Examples: Label.string, Label.fontSize, Sprite.color, UITransform.contentSize.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
uuid: { type: "string", description: "Node UUID (either uuid or nodeName required)" },
|
|
||||||
nodeName: { type: "string", description: "Node name to find (alternative to uuid — avoids UUID lookup)" },
|
|
||||||
componentType: { type: "string", description: "Component class name (e.g. 'cc.Label')" },
|
|
||||||
property: { type: "string", description: "Property name (single mode)" },
|
|
||||||
value: { description: "Value to set (single mode)" },
|
|
||||||
properties: {
|
|
||||||
type: "array",
|
|
||||||
description: "Batch mode: array of {property, value} objects to set multiple properties at once",
|
|
||||||
items: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
property: { type: "string", description: "Property name" },
|
|
||||||
value: { description: "Value to set" },
|
|
||||||
},
|
|
||||||
required: ["property", "value"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
screenshot: { type: "boolean", description: "If true, capture editor screenshot after setting properties and return the file path (default: false)" },
|
|
||||||
},
|
|
||||||
required: ["componentType"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "component_get_info",
|
|
||||||
description: "Get detailed dump of a specific component by its UUID.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
componentUuid: { type: "string", description: "Component UUID (not node UUID)" },
|
|
||||||
},
|
|
||||||
required: ["componentUuid"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "component_get_available",
|
|
||||||
description: "List all available component classes that can be added to nodes.",
|
|
||||||
inputSchema: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "component_auto_bind",
|
|
||||||
description: "Automatically bind @property references by matching property names to descendant node names. Searches only descendants of the target node. Validates component type existence. Supports array properties (Slot_0, Slot_1...). Mode: 'fuzzy' (default) tries exact match first, then case-insensitive; 'strict' requires exact match only.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
uuid: { type: "string", description: "Node UUID (either uuid or nodeName required)" },
|
|
||||||
nodeName: { type: "string", description: "Node name to find (alternative to uuid)" },
|
|
||||||
componentType: { type: "string", description: "Script component class name (e.g. 'QuestReadyPageView')" },
|
|
||||||
force: { type: "boolean", description: "If true, rebind even already-bound properties (default: false)" },
|
|
||||||
mode: { type: "string", enum: ["fuzzy", "strict"], description: "Matching mode: 'fuzzy' (default) or 'strict'" },
|
|
||||||
},
|
|
||||||
required: ["componentType"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "component_query_enum",
|
|
||||||
description: "Get enum values for a component property. Useful for knowing what values Layout.type, Layout.resizeMode, etc. accept.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
uuid: { type: "string", description: "Node UUID" },
|
|
||||||
componentType: { type: "string", description: "Component class (e.g. 'cc.Layout')" },
|
|
||||||
property: { type: "string", description: "Property name (e.g. 'type', 'resizeMode')" },
|
|
||||||
},
|
|
||||||
required: ["uuid", "componentType", "property"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
async execute(toolName: string, args: Record<string, any>): Promise<ToolResult> {
|
|
||||||
// パラメータエイリアス: component → componentType
|
|
||||||
const compType = args.componentType || args.component;
|
|
||||||
|
|
||||||
// nodeName → uuid 解決(対応ツールのみ)
|
|
||||||
const needsResolve = ["component_set_property", "component_get_components", "component_auto_bind"];
|
|
||||||
if (needsResolve.includes(toolName) && !args.uuid && args.nodeName) {
|
|
||||||
try {
|
|
||||||
const resolved = await resolveNodeUuid({ nodeName: args.nodeName });
|
|
||||||
args.uuid = resolved.uuid;
|
|
||||||
} catch (e: any) {
|
|
||||||
return err(e.message || String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (toolName) {
|
|
||||||
case "component_add":
|
|
||||||
return this.addComponent(args.uuid, compType);
|
|
||||||
case "component_remove":
|
|
||||||
return this.removeComponent(args.uuid, compType);
|
|
||||||
case "component_get_components":
|
|
||||||
return this.getComponents(args.uuid);
|
|
||||||
case "component_set_property": {
|
|
||||||
const properties = parseMaybeJson(args.properties);
|
|
||||||
let result: ToolResult;
|
|
||||||
if (properties && Array.isArray(properties)) {
|
|
||||||
const parsed = properties.map((p: any) => ({ ...p, value: parseMaybeJson(p.value) }));
|
|
||||||
result = await this.setProperties(args.uuid, compType, parsed);
|
|
||||||
} else {
|
|
||||||
result = await this.setProperty(args.uuid, compType, args.property, parseMaybeJson(args.value));
|
|
||||||
}
|
|
||||||
// screenshot オプション
|
|
||||||
if (args.screenshot) {
|
|
||||||
try {
|
|
||||||
const ss = await takeEditorScreenshot();
|
|
||||||
const data = JSON.parse(result.content[0].text);
|
|
||||||
data.screenshot = { path: ss.path, size: ss.savedSize };
|
|
||||||
return ok(data);
|
|
||||||
} catch (ssErr: any) {
|
|
||||||
// スクショ失敗してもプロパティ設定結果は返す
|
|
||||||
const data = JSON.parse(result.content[0].text);
|
|
||||||
data.screenshotError = ssErr.message || String(ssErr);
|
|
||||||
return ok(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
case "component_get_info": {
|
|
||||||
try {
|
|
||||||
const dump = await (Editor.Message.request as any)("scene", "query-component", args.componentUuid);
|
|
||||||
return ok({ success: true, component: dump });
|
|
||||||
} catch (e: any) { return err(e.message || String(e)); }
|
|
||||||
}
|
|
||||||
case "component_get_available": {
|
|
||||||
try {
|
|
||||||
const classes = await (Editor.Message.request as any)("scene", "query-classes");
|
|
||||||
return ok({ success: true, classes });
|
|
||||||
} catch (e: any) { return err(e.message || String(e)); }
|
|
||||||
}
|
|
||||||
case "component_auto_bind":
|
|
||||||
return this.autoBind(args.uuid, compType, args.force ?? false, args.mode ?? "fuzzy");
|
|
||||||
case "component_query_enum":
|
|
||||||
return this.queryEnum(args.uuid, compType, args.property);
|
|
||||||
default:
|
|
||||||
return err(`Unknown tool: ${toolName}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async addComponent(uuid: string, componentType: string): Promise<ToolResult> {
|
|
||||||
try {
|
|
||||||
const result = await this.sceneScript("addComponentToNode", [uuid, componentType]);
|
|
||||||
return ok(result);
|
|
||||||
} catch (e: any) {
|
|
||||||
return err(e.message || String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async queryEnum(nodeUuid: string, componentType: string, property: string): Promise<ToolResult> {
|
|
||||||
try {
|
|
||||||
const nodeDump = await (Editor.Message.request as any)("scene", "query-node", nodeUuid);
|
|
||||||
if (!nodeDump) return err("Node not found");
|
|
||||||
|
|
||||||
const comps = nodeDump.__comps__ || [];
|
|
||||||
for (const comp of comps) {
|
|
||||||
const compType = comp.type;
|
|
||||||
if (!compType) continue;
|
|
||||||
// Match by cc.XXX format
|
|
||||||
const normalizedType = componentType.startsWith("cc.") ? componentType.substring(3) : componentType;
|
|
||||||
if (compType !== `cc.${normalizedType}` && compType !== componentType) continue;
|
|
||||||
|
|
||||||
const propDump = comp.value?.[property];
|
|
||||||
if (!propDump) return err(`Property '${property}' not found on ${componentType}`);
|
|
||||||
if (propDump.type !== "Enum") {
|
|
||||||
return ok({ success: true, property, type: propDump.type, note: "Not an enum property", currentValue: propDump.value });
|
|
||||||
}
|
|
||||||
return ok({
|
|
||||||
success: true,
|
|
||||||
property,
|
|
||||||
currentValue: propDump.value,
|
|
||||||
enumList: propDump.enumList,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return err(`Component ${componentType} not found on node`);
|
|
||||||
} catch (e: any) {
|
|
||||||
return err(e.message || String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async removeComponent(uuid: string, componentType: string): Promise<ToolResult> {
|
|
||||||
try {
|
|
||||||
const result = await this.sceneScript("removeComponentFromNode", [uuid, componentType]);
|
|
||||||
return ok(result);
|
|
||||||
} catch (e: any) {
|
|
||||||
return err(e.message || String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getComponents(uuid: string): Promise<ToolResult> {
|
|
||||||
try {
|
|
||||||
const result = await this.sceneScript("getNodeInfo", [uuid]);
|
|
||||||
if (!result.success) return ok(result);
|
|
||||||
return ok({
|
|
||||||
success: true,
|
|
||||||
uuid,
|
|
||||||
name: result.data?.name,
|
|
||||||
components: result.data?.components || [],
|
|
||||||
});
|
|
||||||
} catch (e: any) {
|
|
||||||
return err(e.message || String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @property 名とノード名を自動マッチングしてバインドする。
|
|
||||||
*
|
|
||||||
* - 検索スコープ: 対象ノードの子孫のみ
|
|
||||||
* - 複数ヒット時: 階層の浅いノード(直接の子)を優先
|
|
||||||
* - 型検証: Component 参照型の場合、該当コンポーネントの存在を確認
|
|
||||||
* - 配列対応: @property([Node]) → 連番ノード名 (Slots_0, Slots_1...)
|
|
||||||
* - mode:
|
|
||||||
* - "fuzzy" (default): 完全一致 → case-insensitive → not_found+候補
|
|
||||||
* - "strict": 完全一致のみ → not_found+候補
|
|
||||||
*/
|
|
||||||
private async autoBind(nodeUuid: string, componentType: string, force: boolean, mode: string): Promise<ToolResult> {
|
|
||||||
try {
|
|
||||||
const nodeDump = await (Editor.Message.request as any)("scene", "query-node", nodeUuid);
|
|
||||||
if (!nodeDump) return err("Node not found");
|
|
||||||
|
|
||||||
const comps = nodeDump.__comps__ || [];
|
|
||||||
const compName = componentType.replace("cc.", "");
|
|
||||||
const compIndex = comps.findIndex((c: any) => {
|
|
||||||
const t = c.type || "";
|
|
||||||
return t === compName || t === `cc.${compName}`;
|
|
||||||
});
|
|
||||||
if (compIndex < 0) return err(`Component ${componentType} not found on node`);
|
|
||||||
|
|
||||||
// 子孫ノード一覧を一括取得(検索効率化)
|
|
||||||
const allDescendants = await this.sceneScript("getAllDescendants", [nodeUuid]);
|
|
||||||
const descendantList: Array<{uuid: string, name: string, depth: number}> =
|
|
||||||
allDescendants?.success ? allDescendants.data : [];
|
|
||||||
|
|
||||||
const compDump = comps[compIndex];
|
|
||||||
const properties = compDump.value || {};
|
|
||||||
const skipKeys = new Set(["uuid", "name", "enabled", "node", "__scriptAsset", "__prefab", "_name", "_objFlags", "_enabled"]);
|
|
||||||
|
|
||||||
const results: any[] = [];
|
|
||||||
|
|
||||||
for (const [propName, propDumpRaw] of Object.entries(properties)) {
|
|
||||||
if (skipKeys.has(propName) || propName.startsWith("_")) continue;
|
|
||||||
|
|
||||||
const propDump = propDumpRaw as any;
|
|
||||||
const propType = propDump.type as string;
|
|
||||||
if (!propType) continue;
|
|
||||||
|
|
||||||
const extendsArr = (propDump.extends || []) as string[];
|
|
||||||
|
|
||||||
// 配列型の判定
|
|
||||||
const isArray = propType === "Array" || Array.isArray(propDump.value);
|
|
||||||
if (isArray) {
|
|
||||||
const arrayResult = await this.autoBindArray(nodeUuid, compIndex, propName, propDump, descendantList, mode);
|
|
||||||
results.push(arrayResult);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isNodeRef = propType === "cc.Node";
|
|
||||||
const isComponentRef = extendsArr.includes("cc.Component");
|
|
||||||
if (!isNodeRef && !isComponentRef) continue;
|
|
||||||
|
|
||||||
// 既にバインド済みならスキップ
|
|
||||||
const currentValue = propDump.value;
|
|
||||||
if (!force && currentValue?.uuid) {
|
|
||||||
results.push({ property: propName, status: "already_bound" });
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 名前マッチ: 完全一致 → fuzzy時は case-insensitive
|
|
||||||
const matchResult = this.findMatchingNode(propName, descendantList, mode);
|
|
||||||
|
|
||||||
if (matchResult && isComponentRef) {
|
|
||||||
// 型検証: コンポーネントが存在するか
|
|
||||||
const hasComp = await this.nodeHasComponent(matchResult.uuid, propType);
|
|
||||||
if (!hasComp) {
|
|
||||||
results.push({ property: propName, type: propType, status: "type_mismatch",
|
|
||||||
nodeName: matchResult.name, message: `Node "${matchResult.name}" has no ${propType} component` });
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!matchResult) {
|
|
||||||
// 候補サジェスト
|
|
||||||
const suggestions = this.getSuggestions(propName, descendantList);
|
|
||||||
results.push({ property: propName, type: propType, status: "not_found", suggestions });
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const path = `__comps__.${compIndex}.${propName}`;
|
|
||||||
const dump = await this.buildDumpWithTypeInfo(nodeUuid, path, matchResult.uuid);
|
|
||||||
const setResult = await this.sceneScript("setPropertyViaEditor", [nodeUuid, path, dump]);
|
|
||||||
const status = matchResult.exact ? "bound" : "fuzzy_bound";
|
|
||||||
results.push({ property: propName, status, nodeName: matchResult.name, success: setResult?.success !== false });
|
|
||||||
}
|
|
||||||
|
|
||||||
const boundCount = results.filter(r => r.status === "bound" || r.status === "fuzzy_bound").length;
|
|
||||||
const fuzzyCount = results.filter(r => r.status === "fuzzy_bound").length;
|
|
||||||
const notFoundCount = results.filter(r => r.status === "not_found").length;
|
|
||||||
return ok({ success: true, boundCount, fuzzyCount, notFoundCount, results });
|
|
||||||
} catch (e: any) {
|
|
||||||
return err(e.message || String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 子孫リストからプロパティ名にマッチするノードを検索。
|
|
||||||
* 完全一致を優先、fuzzy モードでは case-insensitive もフォールバック。
|
|
||||||
* 複数ヒット時は階層の浅い(depth が小さい)ものを優先。
|
|
||||||
*/
|
|
||||||
private findMatchingNode(
|
|
||||||
propName: string, descendants: Array<{uuid: string, name: string, depth: number}>, mode: string
|
|
||||||
): { uuid: string; name: string; exact: boolean } | null {
|
|
||||||
const candidates = this.propertyNameToNodeNames(propName);
|
|
||||||
|
|
||||||
// 1. 完全一致
|
|
||||||
for (const candidate of candidates) {
|
|
||||||
const matches = descendants
|
|
||||||
.filter(d => d.name === candidate)
|
|
||||||
.sort((a, b) => a.depth - b.depth);
|
|
||||||
if (matches.length > 0) {
|
|
||||||
return { uuid: matches[0].uuid, name: matches[0].name, exact: true };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. fuzzy: case-insensitive
|
|
||||||
if (mode === "fuzzy") {
|
|
||||||
const lowerCandidates = candidates.map(c => c.toLowerCase());
|
|
||||||
const matches = descendants
|
|
||||||
.filter(d => lowerCandidates.includes(d.name.toLowerCase()))
|
|
||||||
.sort((a, b) => a.depth - b.depth);
|
|
||||||
if (matches.length > 0) {
|
|
||||||
return { uuid: matches[0].uuid, name: matches[0].name, exact: false };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* not_found 時に似た名前のノードをサジェストする。
|
|
||||||
*/
|
|
||||||
private getSuggestions(propName: string, descendants: Array<{uuid: string, name: string, depth: number}>): string[] {
|
|
||||||
const lower = propName.toLowerCase();
|
|
||||||
return descendants
|
|
||||||
.filter(d => d.name.toLowerCase().includes(lower) || lower.includes(d.name.toLowerCase()))
|
|
||||||
.map(d => d.name)
|
|
||||||
.slice(0, 5);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ノードに指定型のコンポーネントが存在するか確認。
|
|
||||||
*/
|
|
||||||
private async nodeHasComponent(nodeUuid: string, propType: string): Promise<boolean> {
|
|
||||||
const typeName = propType.replace("cc.", "");
|
|
||||||
const info = await this.sceneScript("getNodeInfo", [nodeUuid]);
|
|
||||||
if (!info?.success || !info?.data?.components) return false;
|
|
||||||
return info.data.components.some((c: any) => c.type === typeName);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 配列 @property の自動バインド。
|
|
||||||
* プロパティ名 "slots" → "Slots_0", "Slots_1", ... の連番ノードを検索。
|
|
||||||
*/
|
|
||||||
private async autoBindArray(
|
|
||||||
nodeUuid: string, compIndex: number, propName: string, propDump: any,
|
|
||||||
descendants: Array<{uuid: string, name: string, depth: number}>, mode: string
|
|
||||||
): Promise<any> {
|
|
||||||
const elementType = propDump.value?.[0]?.type as string | undefined;
|
|
||||||
if (!elementType) {
|
|
||||||
return { property: propName, status: "skip", reason: "empty array or unknown element type" };
|
|
||||||
}
|
|
||||||
|
|
||||||
const pascal = propName.charAt(0).toUpperCase() + propName.slice(1);
|
|
||||||
const foundElements: any[] = [];
|
|
||||||
let index = 0;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
const candidateName = `${pascal}_${index}`;
|
|
||||||
// 完全一致 or case-insensitive
|
|
||||||
let match = descendants.find(d => d.name === candidateName);
|
|
||||||
if (!match && mode === "fuzzy") {
|
|
||||||
const lower = candidateName.toLowerCase();
|
|
||||||
match = descendants.find(d => d.name.toLowerCase() === lower);
|
|
||||||
}
|
|
||||||
if (!match) break;
|
|
||||||
|
|
||||||
const elementPath = `__comps__.${compIndex}.${propName}.${index}`;
|
|
||||||
const dump = await this.buildDumpWithTypeInfo(nodeUuid, elementPath, match.uuid);
|
|
||||||
const setResult = await this.sceneScript("setPropertyViaEditor", [nodeUuid, elementPath, dump]);
|
|
||||||
const exact = match.name === candidateName;
|
|
||||||
foundElements.push({ index, nodeName: match.name, exact, success: setResult?.success !== false });
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (foundElements.length === 0) {
|
|
||||||
return { property: propName, status: "not_found", type: "Array", candidates: [`${pascal}_0`, `${pascal}_1`, "..."] };
|
|
||||||
}
|
|
||||||
const hasFuzzy = foundElements.some(e => !e.exact);
|
|
||||||
return { property: propName, status: hasFuzzy ? "fuzzy_bound" : "bound", type: "Array", count: foundElements.length, elements: foundElements };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* camelCase プロパティ名からノード名の候補を生成。
|
|
||||||
* closeButton → ["CloseButton", "closeButton"]
|
|
||||||
*/
|
|
||||||
private propertyNameToNodeNames(propName: string): string[] {
|
|
||||||
const pascal = propName.charAt(0).toUpperCase() + propName.slice(1);
|
|
||||||
const names = [pascal];
|
|
||||||
if (pascal !== propName) names.push(propName);
|
|
||||||
return names;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async setProperty(uuid: string, componentType: string, property: string, value: any): Promise<ToolResult> {
|
|
||||||
try {
|
|
||||||
// コンポーネントのインデックスを取得
|
|
||||||
const nodeInfo = await this.sceneScript("getNodeInfo", [uuid]);
|
|
||||||
if (!nodeInfo?.success || !nodeInfo?.data?.components) {
|
|
||||||
return err(`Node ${uuid} not found or has no components`);
|
|
||||||
}
|
|
||||||
const compName = componentType.replace("cc.", "");
|
|
||||||
const compIndex = nodeInfo.data.components.findIndex((c: any) => c.type === compName);
|
|
||||||
if (compIndex < 0) {
|
|
||||||
return err(`Component ${componentType} not found on node ${uuid}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// scene:set-property でプロパティ変更(Prefab保存時にも反映される)
|
|
||||||
// パス形式: __comps__.{index}.{property}
|
|
||||||
const path = `__comps__.${compIndex}.${property}`;
|
|
||||||
|
|
||||||
// プロパティの型情報をquery-nodeから取得して、適切なdump形式を構築
|
|
||||||
const dump = await this.buildDumpWithTypeInfo(uuid, path, value);
|
|
||||||
|
|
||||||
const result = await this.sceneScript("setPropertyViaEditor", [uuid, path, dump]);
|
|
||||||
|
|
||||||
// cc.Widget の isAlign* 設定後は _alignFlags を再計算する
|
|
||||||
// (Editor が isAlign* 変更時に _alignFlags を自動更新しないバグの対処)
|
|
||||||
if (componentType === "cc.Widget" && property.startsWith("isAlign")) {
|
|
||||||
await this.recalcWidgetAlignFlags(uuid, compIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ok({ success: true, path, dump, result });
|
|
||||||
} catch (e: any) {
|
|
||||||
return err(e.message || String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async setProperties(uuid: string, componentType: string, properties: Array<{property: string, value: any}>): Promise<ToolResult> {
|
|
||||||
try {
|
|
||||||
if (!componentType) return err("componentType is required");
|
|
||||||
if (!properties.length) return err("properties array is empty");
|
|
||||||
|
|
||||||
// コンポーネントのインデックスを取得(1回だけ)
|
|
||||||
const nodeInfo = await this.sceneScript("getNodeInfo", [uuid]);
|
|
||||||
if (!nodeInfo?.success || !nodeInfo?.data?.components) {
|
|
||||||
return err(`Node ${uuid} not found or has no components`);
|
|
||||||
}
|
|
||||||
const compName = componentType.replace("cc.", "");
|
|
||||||
const compIndex = nodeInfo.data.components.findIndex((c: any) => c.type === compName);
|
|
||||||
if (compIndex < 0) {
|
|
||||||
return err(`Component ${componentType} not found on node ${uuid}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const results: any[] = [];
|
|
||||||
for (const { property, value } of properties) {
|
|
||||||
const path = `__comps__.${compIndex}.${property}`;
|
|
||||||
const dump = await this.buildDumpWithTypeInfo(uuid, path, value);
|
|
||||||
const result = await this.sceneScript("setPropertyViaEditor", [uuid, path, dump]);
|
|
||||||
results.push({ property, success: result?.success !== false, path });
|
|
||||||
}
|
|
||||||
|
|
||||||
const allOk = results.every(r => r.success);
|
|
||||||
|
|
||||||
// cc.Widget の isAlign* 設定後は _alignFlags を再計算する
|
|
||||||
// (Editor が isAlign* 変更時に _alignFlags を自動更新しないバグの対処)
|
|
||||||
if (componentType === "cc.Widget" && properties.some(p => p.property.startsWith("isAlign"))) {
|
|
||||||
await this.recalcWidgetAlignFlags(uuid, compIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ok({ success: allOk, results });
|
|
||||||
} catch (e: any) {
|
|
||||||
return err(e.message || String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* cc.Widget の isAlign* プロパティ現在値から _alignFlags ビットマスクを再計算して設定する。
|
|
||||||
*
|
|
||||||
* CocosCreator Editor は isAlign* を setPropertyViaEditor で変更しても
|
|
||||||
* _alignFlags を自動更新しないバグがある。このヘルパーで明示的に同期する。
|
|
||||||
*
|
|
||||||
* _alignFlags ビット定義:
|
|
||||||
* isAlignLeft=1, isAlignRight=2, isAlignTop=4, isAlignBottom=8,
|
|
||||||
* isAlignHorizontalCenter=16, isAlignVerticalCenter=32
|
|
||||||
*/
|
|
||||||
private async recalcWidgetAlignFlags(uuid: string, wIdx: number): Promise<void> {
|
|
||||||
const ALIGN_BITS: Record<string, number> = {
|
|
||||||
isAlignLeft: 1, isAlignRight: 2, isAlignTop: 4, isAlignBottom: 8,
|
|
||||||
isAlignHorizontalCenter: 16, isAlignVerticalCenter: 32,
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
const nodeDump = await (Editor.Message.request as any)("scene", "query-node", uuid);
|
|
||||||
if (!nodeDump) return;
|
|
||||||
const wCompDump = nodeDump.__comps__?.[wIdx];
|
|
||||||
if (!wCompDump) return;
|
|
||||||
let alignFlags = 0;
|
|
||||||
for (const [key, bit] of Object.entries(ALIGN_BITS)) {
|
|
||||||
if (wCompDump.value?.[key]?.value === true) alignFlags |= bit;
|
|
||||||
}
|
|
||||||
const path = `__comps__.${wIdx}._alignFlags`;
|
|
||||||
await this.sceneScript("setPropertyViaEditor", [uuid, path, { value: alignFlags, type: "Number" }]);
|
|
||||||
} catch (_e) {
|
|
||||||
// _alignFlags 再計算の失敗は致命的でないため無視
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* プロパティの型情報をEditor APIから取得し、適切なdump形式を構築する。
|
|
||||||
*
|
|
||||||
* UUID文字列が渡された場合、プロパティの型に応じて:
|
|
||||||
* - Node/Component参照型 → {type: propType, value: {uuid: nodeUuid}}
|
|
||||||
* - Asset参照型(cc.Prefab等) → {type: propType, value: {uuid: assetUuid}}
|
|
||||||
* - String型 → {value, type: "String"}
|
|
||||||
*/
|
|
||||||
private async buildDumpWithTypeInfo(nodeUuid: string, path: string, value: any): Promise<any> {
|
|
||||||
// プリミティブ型はそのまま
|
|
||||||
if (typeof value === "number") return { value, type: "Number" };
|
|
||||||
if (typeof value === "boolean") return { value, type: "Boolean" };
|
|
||||||
|
|
||||||
// オブジェクト形式 {uuid: "xxx", type: "cc.Node"} はそのまま
|
|
||||||
// type 指定なしの {uuid: "xxx"} はプロパティの実際の型を解決するため文字列扱いに変換する
|
|
||||||
if (value !== null && typeof value === "object" && typeof value.uuid === "string") {
|
|
||||||
if (typeof value.type === "string") {
|
|
||||||
return { type: value.type, value: { uuid: value.uuid } };
|
|
||||||
}
|
|
||||||
// type 未指定: 文字列として処理してプロパティ型から解決
|
|
||||||
value = value.uuid;
|
|
||||||
}
|
|
||||||
|
|
||||||
// @path: プレフィックスの場合: パスからノードUUIDを解決
|
|
||||||
if (typeof value === "string" && value.startsWith("@path:")) {
|
|
||||||
const nodePath = value.slice(6);
|
|
||||||
const result = await this.sceneScript("findNodeByPath", [nodePath]);
|
|
||||||
if (result?.success && result.data?.uuid) {
|
|
||||||
value = result.data.uuid;
|
|
||||||
} else {
|
|
||||||
throw new Error(`Node not found at path: ${nodePath}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 文字列の場合: プロパティの型情報を取得して判定
|
|
||||||
if (typeof value === "string") {
|
|
||||||
try {
|
|
||||||
const nodeDump = await (Editor.Message.request as any)("scene", "query-node", nodeUuid);
|
|
||||||
if (nodeDump) {
|
|
||||||
const propDump = this.resolveDumpPath(nodeDump, path);
|
|
||||||
if (propDump?.type) {
|
|
||||||
const propType = propDump.type as string;
|
|
||||||
const extendsArr = (propDump.extends || []) as string[];
|
|
||||||
const isComponentRef = extendsArr.includes("cc.Component");
|
|
||||||
const isNodeRef = propType === "cc.Node";
|
|
||||||
const isAssetRef = extendsArr.includes("cc.Asset");
|
|
||||||
|
|
||||||
if (isComponentRef) {
|
|
||||||
// コンポーネント参照: ノードUUIDからコンポーネントUUIDを解決
|
|
||||||
const compUuid = await this.resolveComponentUuid(value, propType);
|
|
||||||
return { type: propType, value: { uuid: compUuid || value } };
|
|
||||||
}
|
|
||||||
if (isNodeRef) {
|
|
||||||
return { type: propType, value: { uuid: value } };
|
|
||||||
}
|
|
||||||
if (isAssetRef) {
|
|
||||||
return { type: propType, value: { uuid: value } };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (_e) {
|
|
||||||
// query-node失敗時はフォールバック
|
|
||||||
}
|
|
||||||
return { value, type: "String" };
|
|
||||||
}
|
|
||||||
|
|
||||||
// その他のオブジェクト(contentSize, color等の構造体)
|
|
||||||
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
||||||
const wrapped: any = {};
|
|
||||||
for (const [k, v] of Object.entries(value)) {
|
|
||||||
wrapped[k] = { value: v };
|
|
||||||
}
|
|
||||||
return { value: wrapped };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { value };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* query-nodeのdumpからドットパスでプロパティを解決する。
|
|
||||||
* 例: "__comps__.2.scrollView" → nodeDump.__comps__[2].value.scrollView
|
|
||||||
*/
|
|
||||||
private resolveDumpPath(nodeDump: any, path: string): any {
|
|
||||||
const parts = path.split(".");
|
|
||||||
let current = nodeDump;
|
|
||||||
for (const part of parts) {
|
|
||||||
if (!current) return null;
|
|
||||||
if (part === "__comps__") {
|
|
||||||
current = current.__comps__;
|
|
||||||
} else if (/^\d+$/.test(part)) {
|
|
||||||
current = current[parseInt(part)]?.value;
|
|
||||||
} else {
|
|
||||||
current = current?.[part];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return current;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ノードUUIDからコンポーネントUUIDを解決する。
|
|
||||||
* propType(例: "cc.ScrollView", "MissionListPanel")に一致するコンポーネントを探す。
|
|
||||||
*/
|
|
||||||
private async resolveComponentUuid(nodeUuid: string, propType: string): Promise<string | null> {
|
|
||||||
try {
|
|
||||||
const nodeInfo = await this.sceneScript("getNodeInfo", [nodeUuid]);
|
|
||||||
if (!nodeInfo?.success || !nodeInfo?.data?.components) return null;
|
|
||||||
const typeName = propType.replace("cc.", "");
|
|
||||||
const comp = nodeInfo.data.components.find((c: any) => c.type === typeName);
|
|
||||||
return comp?.uuid || null;
|
|
||||||
} catch (_e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async sceneScript(method: string, args: any[]): Promise<any> {
|
|
||||||
return Editor.Message.request("scene", "execute-scene-script", {
|
|
||||||
name: EXT_NAME,
|
|
||||||
method,
|
|
||||||
args,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-804
@@ -1,804 +0,0 @@
|
|||||||
import { ToolCategory, ToolDefinition, ToolResult } from "../types";
|
|
||||||
import { ok, err } from "../tool-base";
|
|
||||||
import { getGameLogs, clearGameLogs, queueGameCommand, getCommandResult } from "../mcp-server";
|
|
||||||
import { parseMaybeJson } from "../utils";
|
|
||||||
import { ensureSceneSafeToSwitch } from "./scene-tools";
|
|
||||||
import { processImage, takeEditorScreenshot } from "../screenshot";
|
|
||||||
|
|
||||||
export class DebugTools implements ToolCategory {
|
|
||||||
readonly categoryName = "debug";
|
|
||||||
|
|
||||||
getTools(): ToolDefinition[] {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
name: "debug_get_editor_info",
|
|
||||||
description: "Get Cocos Creator editor information (version, platform, language).",
|
|
||||||
inputSchema: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "debug_list_messages",
|
|
||||||
description: "List available Editor messages for a given extension or built-in module.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
target: { type: "string", description: "Message target (e.g. 'scene', 'asset-db', 'extension')" },
|
|
||||||
},
|
|
||||||
required: ["target"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "debug_execute_script",
|
|
||||||
description: "Execute a custom scene script method. The method must be registered in scene.ts.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
method: { type: "string", description: "Method name from scene.ts" },
|
|
||||||
args: { type: "array", description: "Arguments to pass", items: {} },
|
|
||||||
},
|
|
||||||
required: ["method"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "debug_get_console_logs",
|
|
||||||
description: "Get recent console log entries. Automatically captures scene process logs (console.log/warn/error in scene scripts). Game preview logs can also be captured by sending POST requests to /log endpoint — see README for setup.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
count: { type: "number", description: "Max number of entries (default 50)" },
|
|
||||||
level: { type: "string", description: "Filter by level: 'log', 'warn', or 'error'" },
|
|
||||||
source: { type: "string", description: "Filter by source: 'scene' or 'game'. Returns both if omitted." },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "debug_clear_console",
|
|
||||||
description: "Clear the editor console.",
|
|
||||||
inputSchema: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "debug_list_extensions",
|
|
||||||
description: "List all installed extensions.",
|
|
||||||
inputSchema: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "debug_get_project_logs",
|
|
||||||
description: "Read recent project log entries from the log file.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
lines: { type: "number", description: "Number of lines to read (default 100)" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "debug_search_project_logs",
|
|
||||||
description: "Search for a pattern in project logs.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
pattern: { type: "string", description: "Search pattern (regex supported)" },
|
|
||||||
},
|
|
||||||
required: ["pattern"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "debug_get_log_file_info",
|
|
||||||
description: "Get information about the project log file (size, path, last modified).",
|
|
||||||
inputSchema: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
// ── 以下、既存MCP未対応のEditor API ──
|
|
||||||
{
|
|
||||||
name: "debug_query_devices",
|
|
||||||
description: "List connected devices (for native debugging).",
|
|
||||||
inputSchema: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "debug_open_url",
|
|
||||||
description: "Open a URL in the system browser from the editor.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: { url: { type: "string", description: "URL to open" } },
|
|
||||||
required: ["url"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "debug_validate_scene",
|
|
||||||
description: "Validate the current scene for common issues.",
|
|
||||||
inputSchema: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "debug_game_command",
|
|
||||||
description: "Send a command to the running game preview. Requires GameDebugClient in the game. Commands: 'screenshot' (capture game canvas), 'state' (dump GameDb), 'navigate' (go to a page), 'click' (click a node by name), 'inspect' (get runtime node info: UITransform sizes, Widget, Layout, position). Returns the result from the game.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
type: { type: "string", description: "Command type: 'screenshot', 'state', 'navigate', 'click', 'inspect'" },
|
|
||||||
args: { type: "object", description: "Command arguments (e.g. {page: 'HomePageView'} for navigate, {name: 'ButtonName'} for click)" },
|
|
||||||
timeout: { type: "number", description: "Max wait time in ms (default 5000)" },
|
|
||||||
maxWidth: { type: "number", description: "Max width for screenshot resize (default: 960, 0 = no resize)" },
|
|
||||||
imageFormat: { type: "string", description: "Screenshot output format: 'webp' (default, Q=85) or 'png' (lossless)" },
|
|
||||||
},
|
|
||||||
required: ["type"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "debug_screenshot",
|
|
||||||
description: "Take a screenshot of the editor window and save to a file. Returns the file path of the saved PNG.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
savePath: { type: "string", description: "File path to save the PNG (default: temp/screenshots/screenshot_<timestamp>.png)" },
|
|
||||||
maxWidth: { type: "number", description: "Max width in pixels for resize (default: 960, 0 = no resize). Aspect ratio is preserved." },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "debug_preview",
|
|
||||||
description: "Start or stop the game preview. Uses Preview in Editor (auto-opens MainScene if needed). Falls back to browser preview if editor preview fails.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
action: { type: "string", description: "'start' (default) or 'stop'" },
|
|
||||||
waitForReady: { type: "boolean", description: "If true, wait until GameDebugClient connects after start (default: false)" },
|
|
||||||
waitTimeout: { type: "number", description: "Max wait time in ms for waitForReady (default: 15000)" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "debug_clear_code_cache",
|
|
||||||
description: "Clear the code cache (equivalent to Developer > Cache > Clear code cache) and soft-reload the scene.",
|
|
||||||
inputSchema: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "debug_reload_extension",
|
|
||||||
description: "Reload the MCP extension itself. Use after npm run build to apply code changes without restarting CocosCreator. Response is sent before reload starts.",
|
|
||||||
inputSchema: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "debug_get_extension_info",
|
|
||||||
description: "Get detailed information about a specific extension.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
name: { type: "string", description: "Extension name" },
|
|
||||||
},
|
|
||||||
required: ["name"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "debug_record_start",
|
|
||||||
description: "Start recording the game preview canvas to a video file. Uses MediaRecorder on the game side. Bitrate is auto-calculated from canvas resolution × fps × quality coefficient unless videoBitsPerSecond is set explicitly.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
fps: { type: "number", description: "Frames per second (default: 30)" },
|
|
||||||
quality: { type: "string", description: "'low'/'medium'/'high'/'ultra' (default: medium). Coefficients: 0.15/0.25/0.40/0.60" },
|
|
||||||
coefficient: { type: "number", description: "Custom bitrate coefficient (width × height × fps × coefficient). Overrides quality." },
|
|
||||||
videoBitsPerSecond: { type: "number", description: "Explicit bitrate in bps. Overrides quality-based calculation." },
|
|
||||||
format: { type: "string", description: "'mp4' (default) or 'webm'. mp4 falls back to webm if not supported." },
|
|
||||||
savePath: { type: "string", description: "Save directory (project-relative or absolute). Default: temp/recordings" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "debug_record_stop",
|
|
||||||
description: "Stop recording started by debug_record_start. Returns the saved WebM file path and size. Video is saved to project's temp/recordings/rec_<datetime>.webm.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
timeout: { type: "number", description: "Max wait time in ms for file upload (default: 30000)" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "debug_batch_screenshot",
|
|
||||||
description: "Navigate to multiple pages and take a screenshot of each. Requires game preview running with GameDebugClient. Returns an array of screenshot file paths.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
pages: {
|
|
||||||
type: "array",
|
|
||||||
items: { type: "string" },
|
|
||||||
description: "List of page names to screenshot (e.g. ['HomePageView', 'ShopPageView'])",
|
|
||||||
},
|
|
||||||
delay: { type: "number", description: "Delay in ms between navigate and screenshot (default: 1000)" },
|
|
||||||
maxWidth: { type: "number", description: "Max width for screenshot resize (default: 960)" },
|
|
||||||
},
|
|
||||||
required: ["pages"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "debug_wait_compile",
|
|
||||||
description: "Wait for TypeScript compilation to complete. Monitors the packer-driver debug log for 'Target(editor) ends' message. Use after modifying .ts files to ensure changes are compiled before operating on Prefabs. With clean=true, deletes compiled output first to force a fresh recompile (slower but guaranteed).",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
timeout: { type: "number", description: "Max wait time in ms (default: 15000)" },
|
|
||||||
clean: { type: "boolean", description: "If true, delete compiled output first to force fresh recompile (default: false)" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
async execute(toolName: string, args: Record<string, any>): Promise<ToolResult> {
|
|
||||||
try {
|
|
||||||
switch (toolName) {
|
|
||||||
case "debug_get_editor_info":
|
|
||||||
return this.getEditorInfo();
|
|
||||||
case "debug_list_messages":
|
|
||||||
return this.listMessages(args.target);
|
|
||||||
case "debug_execute_script":
|
|
||||||
return this.executeScript(args.method, args.args || []);
|
|
||||||
case "debug_get_console_logs":
|
|
||||||
return this.getConsoleLogs(args.count || 50, args.level, args.source);
|
|
||||||
case "debug_clear_console":
|
|
||||||
Editor.Message.send("console", "clear");
|
|
||||||
// Clear scene process log buffer
|
|
||||||
await Editor.Message.request("scene", "execute-scene-script", {
|
|
||||||
name: "cocos-creator-mcp",
|
|
||||||
method: "clearConsoleLogs",
|
|
||||||
args: [],
|
|
||||||
}).catch(() => {});
|
|
||||||
// Clear game preview log buffer
|
|
||||||
clearGameLogs();
|
|
||||||
return ok({ success: true });
|
|
||||||
case "debug_list_extensions":
|
|
||||||
return this.listExtensions();
|
|
||||||
case "debug_get_project_logs":
|
|
||||||
return this.getProjectLogs(args.lines || 100);
|
|
||||||
case "debug_search_project_logs":
|
|
||||||
return this.searchProjectLogs(args.pattern);
|
|
||||||
case "debug_get_log_file_info":
|
|
||||||
return this.getLogFileInfo();
|
|
||||||
case "debug_query_devices": {
|
|
||||||
const devices = await (Editor.Message.request as any)("device", "query").catch(() => []);
|
|
||||||
return ok({ success: true, devices });
|
|
||||||
}
|
|
||||||
case "debug_open_url":
|
|
||||||
await (Editor.Message.request as any)("program", "open-url", args.url);
|
|
||||||
return ok({ success: true, url: args.url });
|
|
||||||
case "debug_game_command":
|
|
||||||
return this.gameCommand(args.type || args.command, parseMaybeJson(args.args), args.timeout || 5000, args.maxWidth, args.imageFormat);
|
|
||||||
case "debug_screenshot":
|
|
||||||
return this.takeScreenshot(args.savePath, args.maxWidth);
|
|
||||||
case "debug_preview":
|
|
||||||
return this.handlePreview(args.action || "start", args.waitForReady, args.waitTimeout || 15000);
|
|
||||||
case "debug_clear_code_cache":
|
|
||||||
return this.clearCodeCache();
|
|
||||||
case "debug_reload_extension":
|
|
||||||
return this.reloadExtension();
|
|
||||||
case "debug_validate_scene":
|
|
||||||
return this.validateScene();
|
|
||||||
case "debug_get_extension_info":
|
|
||||||
return this.getExtensionInfo(args.name);
|
|
||||||
case "debug_batch_screenshot":
|
|
||||||
return this.batchScreenshot(args.pages, args.delay || 1000, args.maxWidth);
|
|
||||||
case "debug_record_start":
|
|
||||||
return this.gameCommand("record_start", { fps: args.fps, quality: args.quality, coefficient: args.coefficient, videoBitsPerSecond: args.videoBitsPerSecond, format: args.format, savePath: args.savePath }, 5000);
|
|
||||||
case "debug_record_stop":
|
|
||||||
return this.gameCommand("record_stop", undefined, args.timeout || 30000);
|
|
||||||
case "debug_wait_compile":
|
|
||||||
return this.waitCompile(args.timeout || 15000, args.clean ?? false);
|
|
||||||
default:
|
|
||||||
return err(`Unknown tool: ${toolName}`);
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
return err(e.message || String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getEditorInfo(): Promise<ToolResult> {
|
|
||||||
return ok({
|
|
||||||
success: true,
|
|
||||||
version: Editor.App.version,
|
|
||||||
path: Editor.App.path,
|
|
||||||
home: Editor.App.home,
|
|
||||||
language: Editor.I18n?.getLanguage?.() || "unknown",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async listMessages(target: string): Promise<ToolResult> {
|
|
||||||
try {
|
|
||||||
const info = await (Editor.Message.request as any)("extension", "query-info", target);
|
|
||||||
return ok({ success: true, target, info });
|
|
||||||
} catch (e: any) {
|
|
||||||
const knownMessages: Record<string, string[]> = {
|
|
||||||
"scene": [
|
|
||||||
"query-node-tree", "create-node", "remove-node", "duplicate-node",
|
|
||||||
"set-property", "create-prefab", "save-scene", "execute-scene-script",
|
|
||||||
"query-is-dirty", "query-classes", "soft-reload", "snapshot",
|
|
||||||
"change-gizmo-tool", "query-gizmo-tool-name", "focus-camera-on-nodes",
|
|
||||||
],
|
|
||||||
"asset-db": [
|
|
||||||
"query-assets", "query-asset-info", "query-asset-meta",
|
|
||||||
"refresh-asset", "save-asset", "create-asset", "delete-asset",
|
|
||||||
"move-asset", "copy-asset", "open-asset", "reimport-asset",
|
|
||||||
"query-path", "query-uuid", "query-url", "query-asset-depends",
|
|
||||||
],
|
|
||||||
};
|
|
||||||
const messages = knownMessages[target];
|
|
||||||
if (messages) {
|
|
||||||
return ok({ success: true, target, messages, note: "Static list (query failed)" });
|
|
||||||
}
|
|
||||||
return err(e.message || String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async executeScript(method: string, args: any[]): Promise<ToolResult> {
|
|
||||||
const result = await Editor.Message.request("scene", "execute-scene-script", {
|
|
||||||
name: "cocos-creator-mcp",
|
|
||||||
method,
|
|
||||||
args,
|
|
||||||
});
|
|
||||||
return ok(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getConsoleLogs(count: number, level?: string, source?: string): Promise<ToolResult> {
|
|
||||||
// 1. Try Editor's native console API first (may be supported in future CocosCreator versions)
|
|
||||||
if (!source) {
|
|
||||||
try {
|
|
||||||
const logs = await (Editor.Message.request as any)("console", "query-last-logs", count);
|
|
||||||
if (Array.isArray(logs) && logs.length > 0) {
|
|
||||||
return ok({ success: true, logs, source: "editor-api", note: "Using native Editor console API" });
|
|
||||||
}
|
|
||||||
} catch { /* Not supported in this version — use fallback */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Fallback: collect from scene process buffer + game preview buffer
|
|
||||||
let sceneLogs: any[] = [];
|
|
||||||
let gameLogs: any[] = [];
|
|
||||||
|
|
||||||
// 2a. Scene process logs (console wrapper in scene.ts)
|
|
||||||
if (!source || source === "scene") {
|
|
||||||
try {
|
|
||||||
const result = await Editor.Message.request("scene", "execute-scene-script", {
|
|
||||||
name: "cocos-creator-mcp",
|
|
||||||
method: "getConsoleLogs",
|
|
||||||
args: [count * 2, level], // request more, will trim after merge
|
|
||||||
});
|
|
||||||
if (result?.logs) {
|
|
||||||
sceneLogs = result.logs.map((l: any) => ({ ...l, source: "scene" }));
|
|
||||||
}
|
|
||||||
} catch { /* scene not available */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2b. Game preview logs (received via POST /log endpoint)
|
|
||||||
if (!source || source === "game") {
|
|
||||||
const gameResult = getGameLogs(count * 2, level);
|
|
||||||
gameLogs = gameResult.logs.map((l: any) => ({ ...l, source: "game" }));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge and sort by timestamp, take last `count`
|
|
||||||
const merged = [...sceneLogs, ...gameLogs]
|
|
||||||
.sort((a, b) => a.timestamp.localeCompare(b.timestamp))
|
|
||||||
.slice(-count);
|
|
||||||
|
|
||||||
return ok({
|
|
||||||
success: true,
|
|
||||||
logs: merged,
|
|
||||||
total: { scene: sceneLogs.length, game: (source === "scene" ? 0 : gameLogs.length) },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async listExtensions(): Promise<ToolResult> {
|
|
||||||
try {
|
|
||||||
const list = await (Editor.Message.request as any)("extension", "query-all");
|
|
||||||
return ok({ success: true, extensions: list });
|
|
||||||
} catch {
|
|
||||||
return ok({ success: true, extensions: [], note: "Extension query not supported" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getExtensionInfo(name: string): Promise<ToolResult> {
|
|
||||||
try {
|
|
||||||
const info = await (Editor.Message.request as any)("extension", "query-info", name);
|
|
||||||
return ok({ success: true, name, info });
|
|
||||||
} catch (e: any) {
|
|
||||||
return err(e.message || String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getProjectLogs(lines: number): Promise<ToolResult> {
|
|
||||||
try {
|
|
||||||
const fs = require("fs");
|
|
||||||
const path = require("path");
|
|
||||||
const logPath = path.join(Editor.Project.tmpDir, "logs", "project.log");
|
|
||||||
if (!fs.existsSync(logPath)) return ok({ success: true, logs: [], note: "Log file not found" });
|
|
||||||
const content = fs.readFileSync(logPath, "utf-8");
|
|
||||||
const allLines = content.split("\n");
|
|
||||||
const recent = allLines.slice(-lines);
|
|
||||||
return ok({ success: true, lines: recent.length, logs: recent });
|
|
||||||
} catch (e: any) {
|
|
||||||
return err(e.message || String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async searchProjectLogs(pattern: string): Promise<ToolResult> {
|
|
||||||
try {
|
|
||||||
const fs = require("fs");
|
|
||||||
const path = require("path");
|
|
||||||
const logPath = path.join(Editor.Project.tmpDir, "logs", "project.log");
|
|
||||||
if (!fs.existsSync(logPath)) return ok({ success: true, matches: [] });
|
|
||||||
const content = fs.readFileSync(logPath, "utf-8");
|
|
||||||
const regex = new RegExp(pattern, "gi");
|
|
||||||
const matches = content.split("\n").filter((line: string) => regex.test(line));
|
|
||||||
return ok({ success: true, pattern, count: matches.length, matches: matches.slice(0, 100) });
|
|
||||||
} catch (e: any) {
|
|
||||||
return err(e.message || String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getLogFileInfo(): Promise<ToolResult> {
|
|
||||||
try {
|
|
||||||
const fs = require("fs");
|
|
||||||
const path = require("path");
|
|
||||||
const logPath = path.join(Editor.Project.tmpDir, "logs", "project.log");
|
|
||||||
if (!fs.existsSync(logPath)) return ok({ success: true, exists: false });
|
|
||||||
const stat = fs.statSync(logPath);
|
|
||||||
return ok({ success: true, exists: true, path: logPath, size: stat.size, modified: stat.mtime });
|
|
||||||
} catch (e: any) {
|
|
||||||
return err(e.message || String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async handlePreview(action: string, waitForReady?: boolean, waitTimeout?: number): Promise<ToolResult> {
|
|
||||||
if (action === "stop") {
|
|
||||||
return this.stopPreview();
|
|
||||||
}
|
|
||||||
const result = await this.startPreview();
|
|
||||||
if (waitForReady) {
|
|
||||||
const resultData = JSON.parse(result.content[0].text);
|
|
||||||
if (resultData.success) {
|
|
||||||
const ready = await this.waitForGameReady(waitTimeout || 15000);
|
|
||||||
resultData.gameReady = ready;
|
|
||||||
if (!ready) {
|
|
||||||
resultData.note = (resultData.note || "") + " GameDebugClient did not connect within timeout.";
|
|
||||||
}
|
|
||||||
return ok(resultData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async waitForGameReady(timeout: number): Promise<boolean> {
|
|
||||||
const start = Date.now();
|
|
||||||
while (Date.now() - start < timeout) {
|
|
||||||
// Check if game has sent any log or command result recently
|
|
||||||
const gameResult = getGameLogs(1);
|
|
||||||
if (gameResult.total > 0) return true;
|
|
||||||
await new Promise(r => setTimeout(r, 500));
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async startPreview(): Promise<ToolResult> {
|
|
||||||
try {
|
|
||||||
await this.ensureMainSceneOpen();
|
|
||||||
|
|
||||||
// ツールバーのVueインスタンス経由でplay()を呼ぶ(UI状態も同期される)
|
|
||||||
const played = await this.executeOnToolbar("start");
|
|
||||||
if (played) {
|
|
||||||
return ok({ success: true, action: "start", mode: "editor" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// フォールバック: 直接API
|
|
||||||
const isPlaying = await (Editor.Message.request as any)("scene", "editor-preview-set-play", true);
|
|
||||||
return ok({ success: true, isPlaying, action: "start", mode: "editor", note: "direct API (toolbar UI may not sync)" });
|
|
||||||
} catch (e: any) {
|
|
||||||
try {
|
|
||||||
const electron = require("electron");
|
|
||||||
await electron.shell.openExternal("http://127.0.0.1:7456");
|
|
||||||
return ok({ success: true, action: "start", mode: "browser" });
|
|
||||||
} catch (e2: any) {
|
|
||||||
return err(e2.message || String(e2));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async stopPreview(): Promise<ToolResult> {
|
|
||||||
try {
|
|
||||||
// ツールバー経由で停止(UI同期)
|
|
||||||
const stopped = await this.executeOnToolbar("stop");
|
|
||||||
if (!stopped) {
|
|
||||||
// フォールバック: 直接API
|
|
||||||
await (Editor.Message.request as any)("scene", "editor-preview-set-play", false);
|
|
||||||
}
|
|
||||||
// scene:preview-stop ブロードキャストでツールバーUI状態をリセット
|
|
||||||
Editor.Message.broadcast("scene:preview-stop");
|
|
||||||
// シーンビューに戻す
|
|
||||||
await new Promise(r => setTimeout(r, 500));
|
|
||||||
await this.ensureMainSceneOpen();
|
|
||||||
return ok({ success: true, action: "stop" });
|
|
||||||
} catch (e: any) {
|
|
||||||
return err(e.message || String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async executeOnToolbar(action: "start" | "stop"): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
const electron = require("electron");
|
|
||||||
const allContents = electron.webContents.getAllWebContents();
|
|
||||||
for (const wc of allContents) {
|
|
||||||
try {
|
|
||||||
// play()をawaitしない — プレビュー完了を待つとタイムアウトするため
|
|
||||||
if (action === "start") {
|
|
||||||
const result = await wc.executeJavaScript(
|
|
||||||
`(function() { if (window.xxx && window.xxx.play && !window.xxx.gameView.isPlay) { window.xxx.play(); return true; } return false; })()`
|
|
||||||
);
|
|
||||||
if (result) return true;
|
|
||||||
} else {
|
|
||||||
const result = await wc.executeJavaScript(
|
|
||||||
`(function() { if (window.xxx && window.xxx.gameView.isPlay) { window.xxx.play(); return true; } return false; })()`
|
|
||||||
);
|
|
||||||
if (result) return true;
|
|
||||||
}
|
|
||||||
} catch { /* not the toolbar webContents */ }
|
|
||||||
}
|
|
||||||
} catch { /* electron API not available */ }
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async ensureMainSceneOpen(): Promise<void> {
|
|
||||||
const hierarchy = await Editor.Message.request("scene", "execute-scene-script", {
|
|
||||||
name: "cocos-creator-mcp",
|
|
||||||
method: "getSceneHierarchy",
|
|
||||||
args: [false],
|
|
||||||
}).catch(() => null);
|
|
||||||
|
|
||||||
if (!hierarchy?.sceneName || hierarchy.sceneName === "scene-2d") {
|
|
||||||
// プロジェクト設定のStart Sceneを参照
|
|
||||||
let sceneUuid: string | null = null;
|
|
||||||
try {
|
|
||||||
sceneUuid = await (Editor as any).Profile.getConfig("preview", "general.start_scene", "local");
|
|
||||||
} catch { /* ignore */ }
|
|
||||||
|
|
||||||
// Start Sceneが未設定 or "current_scene" の場合、最初のシーンを使う
|
|
||||||
if (!sceneUuid || sceneUuid === "current_scene") {
|
|
||||||
const scenes = await Editor.Message.request("asset-db", "query-assets", {
|
|
||||||
ccType: "cc.SceneAsset",
|
|
||||||
pattern: "db://assets/**/*",
|
|
||||||
});
|
|
||||||
if (Array.isArray(scenes) && scenes.length > 0) {
|
|
||||||
sceneUuid = scenes[0].uuid;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sceneUuid) {
|
|
||||||
// debug_preview 内部の自動遷移は preview を優先して force=true
|
|
||||||
// (dialog 出るより preview 開始を優先する運用)
|
|
||||||
await ensureSceneSafeToSwitch(true);
|
|
||||||
await (Editor.Message.request as any)("scene", "open-scene", sceneUuid);
|
|
||||||
await new Promise(r => setTimeout(r, 1500));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async clearCodeCache(): Promise<ToolResult> {
|
|
||||||
try {
|
|
||||||
const electron = require("electron");
|
|
||||||
const menu = electron.Menu.getApplicationMenu();
|
|
||||||
if (!menu) return err("Application menu not found");
|
|
||||||
|
|
||||||
const findMenuItem = (items: any[], path: string[]): any => {
|
|
||||||
for (const item of items) {
|
|
||||||
if (item.label === path[0]) {
|
|
||||||
if (path.length === 1) return item;
|
|
||||||
if (item.submenu?.items) return findMenuItem(item.submenu.items, path.slice(1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const cacheItem = findMenuItem(menu.items, ["Developer", "Cache", "Clear code cache"]);
|
|
||||||
if (!cacheItem) return err("Menu item 'Developer > Cache > Clear code cache' not found");
|
|
||||||
|
|
||||||
cacheItem.click();
|
|
||||||
await new Promise(r => setTimeout(r, 1000));
|
|
||||||
return ok({ success: true, note: "Code cache cleared via menu" });
|
|
||||||
} catch (e: any) {
|
|
||||||
return err(e.message || String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async gameCommand(type: string, args: any, timeout: number, maxWidth?: number, imageFormat?: string): Promise<ToolResult> {
|
|
||||||
const cmdId = queueGameCommand(type, args);
|
|
||||||
|
|
||||||
// Poll for result
|
|
||||||
const start = Date.now();
|
|
||||||
while (Date.now() - start < timeout) {
|
|
||||||
const result = getCommandResult();
|
|
||||||
if (result && result.id === cmdId) {
|
|
||||||
// If screenshot, save to file and return path
|
|
||||||
if (type === "screenshot" && result.success && result.data?.dataUrl) {
|
|
||||||
try {
|
|
||||||
const fs = require("fs");
|
|
||||||
const path = require("path");
|
|
||||||
const dir = path.join(Editor.Project.tmpDir, "screenshots");
|
|
||||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
||||||
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
||||||
const base64 = result.data.dataUrl.replace(/^data:image\/png;base64,/, "");
|
|
||||||
const pngBuffer = Buffer.from(base64, "base64");
|
|
||||||
const effectiveMaxWidth = maxWidth !== undefined ? maxWidth : 960;
|
|
||||||
const electron = require("electron");
|
|
||||||
const origImage = electron.nativeImage.createFromBuffer(pngBuffer);
|
|
||||||
const originalSize = origImage.getSize();
|
|
||||||
const { buffer, width, height, format } = await processImage(pngBuffer, effectiveMaxWidth, imageFormat);
|
|
||||||
const ext = format === "webp" ? "webp" : format === "jpeg" ? "jpg" : "png";
|
|
||||||
const filePath = path.join(dir, `game_${timestamp}.${ext}`);
|
|
||||||
fs.writeFileSync(filePath, buffer);
|
|
||||||
return ok({
|
|
||||||
success: true, path: filePath, size: buffer.length, format,
|
|
||||||
originalSize: `${originalSize.width}x${originalSize.height}`,
|
|
||||||
savedSize: `${width}x${height}`,
|
|
||||||
});
|
|
||||||
} catch (e: any) {
|
|
||||||
return ok({ success: true, note: "Screenshot captured but file save failed", error: e.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ok(result);
|
|
||||||
}
|
|
||||||
await new Promise(r => setTimeout(r, 200));
|
|
||||||
}
|
|
||||||
return err(`Game did not respond within ${timeout}ms. Is GameDebugClient running in the preview?`);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async takeScreenshot(savePath?: string, maxWidth?: number): Promise<ToolResult> {
|
|
||||||
try {
|
|
||||||
const result = await takeEditorScreenshot(savePath, maxWidth);
|
|
||||||
return ok(result);
|
|
||||||
} catch (e: any) {
|
|
||||||
return err(e.message || String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async reloadExtension(): Promise<ToolResult> {
|
|
||||||
// Schedule reload after response is sent
|
|
||||||
setTimeout(async () => {
|
|
||||||
try {
|
|
||||||
await (Editor.Message.request as any)("extension", "reload", "cocos-creator-mcp");
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error("[MCP] Extension reload failed:", e.message);
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
return ok({ success: true, note: "Extension reload scheduled. MCP server will restart in ~1s. NOTE: Adding new tool definitions or modifying scene.ts requires a full CocosCreator restart (reload is not sufficient)." });
|
|
||||||
}
|
|
||||||
|
|
||||||
private async batchScreenshot(pages: string[], delay: number, maxWidth?: number): Promise<ToolResult> {
|
|
||||||
const results: any[] = [];
|
|
||||||
const timeout = 10000;
|
|
||||||
|
|
||||||
for (const page of pages) {
|
|
||||||
// Navigate
|
|
||||||
const navResult = await this.gameCommand("navigate", { page }, timeout, maxWidth);
|
|
||||||
const navData = JSON.parse(navResult.content[0].text);
|
|
||||||
if (!navData.success) {
|
|
||||||
results.push({ page, success: false, error: "navigate failed" });
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for page to render
|
|
||||||
await new Promise(r => setTimeout(r, delay));
|
|
||||||
|
|
||||||
// Screenshot
|
|
||||||
const ssResult = await this.gameCommand("screenshot", {}, timeout, maxWidth);
|
|
||||||
const ssData = JSON.parse(ssResult.content[0].text);
|
|
||||||
results.push({
|
|
||||||
page,
|
|
||||||
success: ssData.success || false,
|
|
||||||
path: ssData.path,
|
|
||||||
error: ssData.success ? undefined : (ssData.error || ssData.message),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const succeeded = results.filter(r => r.success).length;
|
|
||||||
return ok({
|
|
||||||
success: true,
|
|
||||||
total: pages.length,
|
|
||||||
succeeded,
|
|
||||||
failed: pages.length - succeeded,
|
|
||||||
results,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async validateScene(): Promise<ToolResult> {
|
|
||||||
try {
|
|
||||||
const tree = await Editor.Message.request("scene", "query-node-tree");
|
|
||||||
const issues: string[] = [];
|
|
||||||
const checkNodes = (nodes: any[]) => {
|
|
||||||
if (!nodes) return;
|
|
||||||
for (const node of nodes) {
|
|
||||||
if (!node.name) issues.push(`Node ${node.uuid} has no name`);
|
|
||||||
if (node.children) checkNodes(node.children);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if (Array.isArray(tree)) checkNodes(tree);
|
|
||||||
return ok({ success: true, issueCount: issues.length, issues });
|
|
||||||
} catch (e: any) {
|
|
||||||
return err(e.message || String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TypeScript コンパイル完了を待つ。
|
|
||||||
* packer-driver の debug.log に "Target(editor) ends" が現れるのを監視する。
|
|
||||||
* 既にコンパイル済み(直近数秒以内に完了ログあり)なら即座に返す。
|
|
||||||
*/
|
|
||||||
private async waitCompile(timeout: number, clean: boolean): Promise<ToolResult> {
|
|
||||||
try {
|
|
||||||
const fs = require("fs");
|
|
||||||
const path = require("path");
|
|
||||||
const logPath = path.join(Editor.Project.path, "temp", "programming", "packer-driver", "logs", "debug.log");
|
|
||||||
const chunksDir = path.join(Editor.Project.path, "temp", "programming", "packer-driver", "targets", "editor", "chunks");
|
|
||||||
|
|
||||||
if (!fs.existsSync(logPath)) {
|
|
||||||
return err(`Compile log not found: ${logPath}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const MARKER = "Target(editor) ends";
|
|
||||||
|
|
||||||
// clean モード: コードキャッシュクリア + soft-reload で再コンパイルを強制
|
|
||||||
if (clean) {
|
|
||||||
// Developer > Cache > Clear code cache をクリック
|
|
||||||
try {
|
|
||||||
const electron = require("electron");
|
|
||||||
const menu = electron.Menu.getApplicationMenu();
|
|
||||||
const findMenuItem = (items: any[], labels: string[]): any => {
|
|
||||||
for (const item of items) {
|
|
||||||
if (item.label === labels[0]) {
|
|
||||||
if (labels.length === 1) return item;
|
|
||||||
if (item.submenu?.items) return findMenuItem(item.submenu.items, labels.slice(1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
const cacheItem = menu ? findMenuItem(menu.items, ["Developer", "Cache", "Clear code cache"]) : null;
|
|
||||||
if (cacheItem) cacheItem.click();
|
|
||||||
} catch (_e) { /* ignore */ }
|
|
||||||
await new Promise(r => setTimeout(r, 500));
|
|
||||||
// soft-reload でシーンを再読み込み → コンパイルトリガー
|
|
||||||
await (Editor.Message.request as any)("scene", "soft-reload").catch(() => {});
|
|
||||||
}
|
|
||||||
|
|
||||||
// refresh-asset でファイル変更を CC に通知してコンパイルをトリガー
|
|
||||||
await (Editor.Message.request as any)("asset-db", "refresh-asset", "db://assets").catch(() => {});
|
|
||||||
|
|
||||||
const initialSize = fs.statSync(logPath).size;
|
|
||||||
const startTime = Date.now();
|
|
||||||
const POLL_INTERVAL = 200;
|
|
||||||
const DETECT_GRACE_MS = 2000; // CC がファイル変更を検知するまでの猶予
|
|
||||||
|
|
||||||
while (Date.now() - startTime < timeout) {
|
|
||||||
await new Promise(r => setTimeout(r, POLL_INTERVAL));
|
|
||||||
|
|
||||||
const currentSize = fs.statSync(logPath).size;
|
|
||||||
|
|
||||||
// ログが成長していない
|
|
||||||
if (currentSize <= initialSize) {
|
|
||||||
// clean モードでは必ずコンパイルが走るので猶予判定しない
|
|
||||||
if (clean) continue;
|
|
||||||
// 猶予期間内はまだ待つ (CC の検知が遅い可能性)
|
|
||||||
if (Date.now() - startTime < DETECT_GRACE_MS) continue;
|
|
||||||
// 猶予期間を過ぎてもログが成長しない → コンパイル不要
|
|
||||||
return ok({ success: true, compiled: true, waitedMs: Date.now() - startTime, note: "No compilation triggered (no changes detected)" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// ログが成長した → 新しい部分にマーカーがあるか確認
|
|
||||||
const fd = fs.openSync(logPath, "r");
|
|
||||||
const newBytes = currentSize - initialSize;
|
|
||||||
const buffer = Buffer.alloc(newBytes);
|
|
||||||
fs.readSync(fd, buffer, 0, newBytes, initialSize);
|
|
||||||
fs.closeSync(fd);
|
|
||||||
const newContent = buffer.toString("utf8");
|
|
||||||
|
|
||||||
if (newContent.includes(MARKER)) {
|
|
||||||
return ok({ success: true, compiled: true, waitedMs: Date.now() - startTime });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ok({ success: true, compiled: false, timeout: true, waitedMs: timeout });
|
|
||||||
} catch (e: any) {
|
|
||||||
return err(e.message || String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-624
@@ -1,624 +0,0 @@
|
|||||||
import { ToolCategory, ToolDefinition, ToolResult } from "../types";
|
|
||||||
import { ok, err } from "../tool-base";
|
|
||||||
import { parseMaybeJson } from "../utils";
|
|
||||||
import { resolveNodeUuid } from "../node-resolve";
|
|
||||||
import { takeEditorScreenshot } from "../screenshot";
|
|
||||||
|
|
||||||
const EXT_NAME = "cocos-creator-mcp";
|
|
||||||
|
|
||||||
export class NodeTools implements ToolCategory {
|
|
||||||
readonly categoryName = "node";
|
|
||||||
|
|
||||||
getTools(): ToolDefinition[] {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
name: "node_create",
|
|
||||||
description: "Create a new node in the scene.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
name: { type: "string", description: "Node name" },
|
|
||||||
parent: { type: "string", description: "Parent node UUID (optional, defaults to scene root)" },
|
|
||||||
components: {
|
|
||||||
type: "array",
|
|
||||||
items: { type: "string" },
|
|
||||||
description: "Component class names to add (e.g. ['cc.Label', 'cc.Sprite'])",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ["name"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "node_get_info",
|
|
||||||
description: "Get detailed information about a node by UUID, including components.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
uuid: { type: "string", description: "Node UUID" },
|
|
||||||
},
|
|
||||||
required: ["uuid"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "node_find_by_name",
|
|
||||||
description: "Find all nodes matching a given name.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
name: { type: "string", description: "Node name to search" },
|
|
||||||
},
|
|
||||||
required: ["name"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "node_set_property",
|
|
||||||
description: "Set a property on a node (name, active, position, rotation, scale, etc.).",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
uuid: { type: "string", description: "Node UUID" },
|
|
||||||
property: { type: "string", description: "Property name (e.g. 'name', 'active', 'position')" },
|
|
||||||
value: { description: "Value to set. For position/rotation/scale use {x,y,z}." },
|
|
||||||
},
|
|
||||||
required: ["uuid", "property", "value"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "node_set_transform",
|
|
||||||
description: "Set position, rotation, and/or scale of a node at once.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
uuid: { type: "string", description: "Node UUID" },
|
|
||||||
position: {
|
|
||||||
type: "object",
|
|
||||||
properties: { x: { type: "number" }, y: { type: "number" }, z: { type: "number" } },
|
|
||||||
description: "Position {x,y,z}",
|
|
||||||
},
|
|
||||||
rotation: {
|
|
||||||
type: "object",
|
|
||||||
properties: { x: { type: "number" }, y: { type: "number" }, z: { type: "number" } },
|
|
||||||
description: "Euler rotation {x,y,z}",
|
|
||||||
},
|
|
||||||
scale: {
|
|
||||||
type: "object",
|
|
||||||
properties: { x: { type: "number" }, y: { type: "number" }, z: { type: "number" } },
|
|
||||||
description: "Scale {x,y,z}",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ["uuid"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "node_delete",
|
|
||||||
description: "Delete a node by UUID.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
uuid: { type: "string", description: "Node UUID" },
|
|
||||||
},
|
|
||||||
required: ["uuid"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "node_move",
|
|
||||||
description: "Move a node to a new parent.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
uuid: { type: "string", description: "Node UUID" },
|
|
||||||
parentUuid: { type: "string", description: "New parent node UUID" },
|
|
||||||
},
|
|
||||||
required: ["uuid", "parentUuid"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "node_duplicate",
|
|
||||||
description: "Duplicate a node.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
uuid: { type: "string", description: "Node UUID to duplicate" },
|
|
||||||
},
|
|
||||||
required: ["uuid"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "node_get_all",
|
|
||||||
description: "Get a flat list of all nodes in the current scene.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "node_set_active",
|
|
||||||
description: "Set a node's active (visible) state.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
uuid: { type: "string", description: "Node UUID" },
|
|
||||||
active: { type: "boolean", description: "Whether the node is active" },
|
|
||||||
},
|
|
||||||
required: ["uuid", "active"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "node_set_layer",
|
|
||||||
description: "Set a node's layer.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
uuid: { type: "string", description: "Node UUID" },
|
|
||||||
layer: { type: "number", description: "Layer value" },
|
|
||||||
},
|
|
||||||
required: ["uuid", "layer"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "node_detect_type",
|
|
||||||
description: "Detect node type (2D, 3D, or regular Node) based on its components.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
uuid: { type: "string", description: "Node UUID" },
|
|
||||||
},
|
|
||||||
required: ["uuid"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "node_create_tree",
|
|
||||||
description: "Create a full node tree from a JSON spec in one call. Much faster than creating nodes one by one. Spec format: { name, components?: ['cc.UITransform'], properties?: {'cc.UITransform.contentSize': {width:720,height:1280}}, widget?: {top:0, bottom:0, left:0, right:0}, active?: bool, position?: {x,y,z}, children?: [...] }",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
parent: { type: "string", description: "Parent node UUID" },
|
|
||||||
spec: { description: "Node tree specification (JSON object with name, components, properties, children)" },
|
|
||||||
},
|
|
||||||
required: ["parent", "spec"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "node_set_layout",
|
|
||||||
description: "Set UITransform (contentSize, anchorPoint) and Widget (margins) on a node in one call. Much faster than calling component_set_property multiple times for layout adjustments. Set screenshot=true to capture the editor after changes.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
uuid: { type: "string", description: "Node UUID (either uuid or nodeName required)" },
|
|
||||||
nodeName: { type: "string", description: "Node name to find (alternative to uuid)" },
|
|
||||||
contentSize: {
|
|
||||||
type: "object",
|
|
||||||
properties: { width: { type: "number" }, height: { type: "number" } },
|
|
||||||
description: "UITransform contentSize {width, height}",
|
|
||||||
},
|
|
||||||
anchorPoint: {
|
|
||||||
type: "object",
|
|
||||||
properties: { x: { type: "number" }, y: { type: "number" } },
|
|
||||||
description: "UITransform anchorPoint {x, y} (0-1)",
|
|
||||||
},
|
|
||||||
widget: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
top: { type: "number" }, bottom: { type: "number" },
|
|
||||||
left: { type: "number" }, right: { type: "number" },
|
|
||||||
horizontalCenter: { type: "number" }, verticalCenter: { type: "number" },
|
|
||||||
isAlignTop: { type: "boolean" }, isAlignBottom: { type: "boolean" },
|
|
||||||
isAlignLeft: { type: "boolean" }, isAlignRight: { type: "boolean" },
|
|
||||||
isAlignHorizontalCenter: { type: "boolean" }, isAlignVerticalCenter: { type: "boolean" },
|
|
||||||
},
|
|
||||||
description: "Widget alignment margins. Setting a value (e.g. top:0) automatically enables the corresponding alignment (isAlignTop:true).",
|
|
||||||
},
|
|
||||||
color: {
|
|
||||||
type: "object",
|
|
||||||
properties: { r: { type: "number" }, g: { type: "number" }, b: { type: "number" }, a: { type: "number" } },
|
|
||||||
description: "Node color {r,g,b,a} (0-255)",
|
|
||||||
},
|
|
||||||
opacity: { type: "number", description: "Node opacity (0-255)" },
|
|
||||||
screenshot: { type: "boolean", description: "If true, capture editor screenshot after changes (default: false)" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
async execute(toolName: string, args: Record<string, any>): Promise<ToolResult> {
|
|
||||||
const rejected = await this.rejectIfPreviewRunning(toolName);
|
|
||||||
if (rejected) return rejected;
|
|
||||||
|
|
||||||
switch (toolName) {
|
|
||||||
case "node_create":
|
|
||||||
return this.createNode(args.name, args.parent, args.components);
|
|
||||||
case "node_get_info":
|
|
||||||
return this.getNodeInfo(args.uuid);
|
|
||||||
case "node_find_by_name":
|
|
||||||
return this.findByName(args.name);
|
|
||||||
case "node_set_property":
|
|
||||||
return this.setProperty(args.uuid, args.property, parseMaybeJson(args.value));
|
|
||||||
case "node_set_transform":
|
|
||||||
return this.setTransform(args.uuid, args.position, args.rotation, args.scale);
|
|
||||||
case "node_delete":
|
|
||||||
return this.deleteNode(args.uuid);
|
|
||||||
case "node_move":
|
|
||||||
return this.moveNode(args.uuid, args.parentUuid);
|
|
||||||
case "node_duplicate":
|
|
||||||
return this.duplicateNode(args.uuid);
|
|
||||||
case "node_get_all":
|
|
||||||
return this.getAllNodes();
|
|
||||||
case "node_set_active":
|
|
||||||
return this.setProperty(args.uuid, "active", args.active);
|
|
||||||
case "node_set_layer":
|
|
||||||
return this.setProperty(args.uuid, "layer", args.layer);
|
|
||||||
case "node_create_tree":
|
|
||||||
return this.createNodeTree(args.parent, parseMaybeJson(args.spec));
|
|
||||||
case "node_set_layout":
|
|
||||||
return this.setLayout(args);
|
|
||||||
case "node_detect_type": {
|
|
||||||
try {
|
|
||||||
const info = await this.sceneScript("getNodeInfo", [args.uuid]);
|
|
||||||
if (!info.success) return ok(info);
|
|
||||||
const comps = info.data?.components || [];
|
|
||||||
const compTypes = comps.map((c: any) => c.type);
|
|
||||||
let nodeType = "Node";
|
|
||||||
if (compTypes.includes("UITransform")) nodeType = "2D";
|
|
||||||
else if (compTypes.includes("MeshRenderer") || compTypes.includes("Camera")) nodeType = "3D";
|
|
||||||
return ok({ success: true, uuid: args.uuid, nodeType, components: compTypes });
|
|
||||||
} catch (e: any) { return err(e.message || String(e)); }
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return err(`Unknown tool: ${toolName}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Scene editing tools that must not run during preview */
|
|
||||||
private static readonly SCENE_EDIT_TOOLS = new Set([
|
|
||||||
"node_create", "node_delete", "node_move", "node_duplicate",
|
|
||||||
"node_set_property", "node_set_transform", "node_set_active", "node_set_layer",
|
|
||||||
"node_create_tree", "node_set_layout",
|
|
||||||
]);
|
|
||||||
|
|
||||||
private async rejectIfPreviewRunning(toolName: string): Promise<ToolResult | null> {
|
|
||||||
if (!NodeTools.SCENE_EDIT_TOOLS.has(toolName)) return null;
|
|
||||||
try {
|
|
||||||
const state = await Editor.Message.request("preview", "query-info");
|
|
||||||
if (state && (state as any).running) {
|
|
||||||
return err(`"${toolName}" はプレビュー中に実行できません。先にプレビューを停止してください。`);
|
|
||||||
}
|
|
||||||
} catch { /* query failed — allow execution */ }
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async createNode(name: string, parent?: string, components?: string[]): Promise<ToolResult> {
|
|
||||||
try {
|
|
||||||
// Use Editor API to create node
|
|
||||||
const uuid = await Editor.Message.request("scene", "create-node", {
|
|
||||||
parent: parent || undefined,
|
|
||||||
name,
|
|
||||||
assetUuid: undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Wait until the node is queryable in the scene process
|
|
||||||
await this.waitForNode(uuid);
|
|
||||||
|
|
||||||
// Add components if specified
|
|
||||||
if (components && components.length > 0) {
|
|
||||||
for (const comp of components) {
|
|
||||||
await this.sceneScript("addComponentToNode", [uuid, comp]);
|
|
||||||
// Wait until the component is reflected in query-node
|
|
||||||
await this.waitForComponent(uuid, comp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ok({ success: true, uuid, name });
|
|
||||||
} catch (e: any) {
|
|
||||||
return err(e.message || String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wait until a node becomes queryable in the scene process.
|
|
||||||
* Editor.Message.request("scene", "create-node") returns before the node
|
|
||||||
* is fully registered in the scene hierarchy, so subsequent scene script
|
|
||||||
* calls (findNode) may fail without this wait.
|
|
||||||
*/
|
|
||||||
private async waitForNode(uuid: string, maxRetries = 10, intervalMs = 100): Promise<void> {
|
|
||||||
for (let i = 0; i < maxRetries; i++) {
|
|
||||||
try {
|
|
||||||
const result = await this.sceneScript("getNodeInfo", [uuid]);
|
|
||||||
if (result?.success) return;
|
|
||||||
} catch { /* not ready yet */ }
|
|
||||||
await new Promise(resolve => setTimeout(resolve, intervalMs));
|
|
||||||
}
|
|
||||||
// Don't throw — let the caller proceed and get a more specific error if needed
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wait until a component added via addComponentToNode is reflected in query-node.
|
|
||||||
* sceneScript returns before the Editor API (query-node) reflects the change,
|
|
||||||
* so polling is needed to avoid race conditions in subsequent tool calls.
|
|
||||||
*/
|
|
||||||
private async waitForComponent(nodeUuid: string, componentType: string, maxRetries = 10, intervalMs = 100): Promise<void> {
|
|
||||||
const normalizedType = componentType.startsWith("cc.") ? componentType.substring(3) : componentType;
|
|
||||||
for (let i = 0; i < maxRetries; i++) {
|
|
||||||
try {
|
|
||||||
const nodeDump = await (Editor.Message.request as any)("scene", "query-node", nodeUuid);
|
|
||||||
const comps: any[] = nodeDump?.__comps__ || [];
|
|
||||||
const found = comps.some((c) =>
|
|
||||||
c.type === componentType || c.type === `cc.${normalizedType}` || c.type === normalizedType
|
|
||||||
);
|
|
||||||
if (found) return;
|
|
||||||
} catch { /* not ready yet */ }
|
|
||||||
await new Promise(resolve => setTimeout(resolve, intervalMs));
|
|
||||||
}
|
|
||||||
// Don't throw — component may still work; let caller get a specific error if needed
|
|
||||||
}
|
|
||||||
|
|
||||||
private async createNodeTree(parentUuid: string, spec: any): Promise<ToolResult> {
|
|
||||||
try {
|
|
||||||
const result = await this.sceneScript("buildNodeTree", [parentUuid, spec]);
|
|
||||||
if (!result?.success) return err(result?.error || "buildNodeTree failed");
|
|
||||||
return ok(result);
|
|
||||||
} catch (e: any) {
|
|
||||||
return err(e.message || String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getNodeInfo(uuid: string): Promise<ToolResult> {
|
|
||||||
try {
|
|
||||||
const result = await this.sceneScript("getNodeInfo", [uuid]);
|
|
||||||
return ok(result);
|
|
||||||
} catch (e: any) {
|
|
||||||
return err(e.message || String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async findByName(name: string): Promise<ToolResult> {
|
|
||||||
try {
|
|
||||||
const result = await this.sceneScript("findNodesByName", [name]);
|
|
||||||
return ok(result);
|
|
||||||
} catch (e: any) {
|
|
||||||
return err(e.message || String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async setProperty(uuid: string, property: string, value: any): Promise<ToolResult> {
|
|
||||||
try {
|
|
||||||
const result = await this.sceneScript("setNodeProperty", [uuid, property, value]);
|
|
||||||
return ok(result);
|
|
||||||
} catch (e: any) {
|
|
||||||
return err(e.message || String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async setTransform(uuid: string, position?: any, rotation?: any, scale?: any): Promise<ToolResult> {
|
|
||||||
try {
|
|
||||||
const results: any[] = [];
|
|
||||||
if (position) {
|
|
||||||
results.push(await this.sceneScript("setNodeProperty", [uuid, "position", position]));
|
|
||||||
}
|
|
||||||
if (rotation) {
|
|
||||||
results.push(await this.sceneScript("setNodeProperty", [uuid, "rotation", rotation]));
|
|
||||||
}
|
|
||||||
if (scale) {
|
|
||||||
results.push(await this.sceneScript("setNodeProperty", [uuid, "scale", scale]));
|
|
||||||
}
|
|
||||||
const anyFailed = results.find((r) => !r.success);
|
|
||||||
if (anyFailed) return ok(anyFailed);
|
|
||||||
return ok({ success: true, uuid });
|
|
||||||
} catch (e: any) {
|
|
||||||
return err(e.message || String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async deleteNode(uuid: string): Promise<ToolResult> {
|
|
||||||
try {
|
|
||||||
await Editor.Message.request("scene", "remove-node", { uuid });
|
|
||||||
return ok({ success: true, uuid });
|
|
||||||
} catch (e: any) {
|
|
||||||
return err(e.message || String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async moveNode(uuid: string, parentUuid: string): Promise<ToolResult> {
|
|
||||||
try {
|
|
||||||
await (Editor.Message.request as any)("scene", "set-property", {
|
|
||||||
uuid,
|
|
||||||
path: "parent",
|
|
||||||
dump: { type: "cc.Node", value: { uuid: parentUuid } },
|
|
||||||
});
|
|
||||||
return ok({ success: true, uuid, parentUuid });
|
|
||||||
} catch (e: any) {
|
|
||||||
// Fallback: try scene script
|
|
||||||
try {
|
|
||||||
const result = await this.sceneScript("moveNode", [uuid, parentUuid]);
|
|
||||||
return ok(result);
|
|
||||||
} catch (e2: any) {
|
|
||||||
return err(e.message || String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async duplicateNode(uuid: string): Promise<ToolResult> {
|
|
||||||
try {
|
|
||||||
const result = await Editor.Message.request("scene", "duplicate-node", uuid);
|
|
||||||
// duplicate-node returns an array of UUIDs
|
|
||||||
const newUuid = Array.isArray(result) ? result[0] : result;
|
|
||||||
return ok({ success: true, sourceUuid: uuid, newUuid });
|
|
||||||
} catch (e: any) {
|
|
||||||
return err(e.message || String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getAllNodes(): Promise<ToolResult> {
|
|
||||||
try {
|
|
||||||
const result = await this.sceneScript("getAllNodes", []);
|
|
||||||
return ok(result);
|
|
||||||
} catch (e: any) {
|
|
||||||
return err(e.message || String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* UITransform + Widget + color/opacity をまとめて設定する。
|
|
||||||
* Widget の値を指定すると、対応する isAlign* フラグを自動で true にする。
|
|
||||||
*/
|
|
||||||
private async setLayout(args: Record<string, any>): Promise<ToolResult> {
|
|
||||||
try {
|
|
||||||
// nodeName → uuid 解決
|
|
||||||
let uuid = args.uuid;
|
|
||||||
if (!uuid && args.nodeName) {
|
|
||||||
const resolved = await resolveNodeUuid({ nodeName: args.nodeName });
|
|
||||||
uuid = resolved.uuid;
|
|
||||||
}
|
|
||||||
if (!uuid) return err("Either 'uuid' or 'nodeName' is required");
|
|
||||||
|
|
||||||
const results: any[] = [];
|
|
||||||
|
|
||||||
// UITransform の設定
|
|
||||||
const contentSize = parseMaybeJson(args.contentSize);
|
|
||||||
const anchorPoint = parseMaybeJson(args.anchorPoint);
|
|
||||||
if (contentSize || anchorPoint) {
|
|
||||||
const nodeInfo = await this.sceneScript("getNodeInfo", [uuid]);
|
|
||||||
if (!nodeInfo?.success) return err(`Node ${uuid} not found`);
|
|
||||||
const comps = nodeInfo.data?.components || [];
|
|
||||||
const utIdx = comps.findIndex((c: any) => c.type === "UITransform");
|
|
||||||
if (utIdx < 0) return err("Node has no UITransform component");
|
|
||||||
|
|
||||||
if (contentSize) {
|
|
||||||
const path = `__comps__.${utIdx}.contentSize`;
|
|
||||||
const dump = { value: { width: { value: contentSize.width }, height: { value: contentSize.height } } };
|
|
||||||
const r = await this.sceneScript("setPropertyViaEditor", [uuid, path, dump]);
|
|
||||||
results.push({ property: "contentSize", success: r?.success !== false });
|
|
||||||
}
|
|
||||||
if (anchorPoint) {
|
|
||||||
const path = `__comps__.${utIdx}.anchorPoint`;
|
|
||||||
const dump = { value: { x: { value: anchorPoint.x }, y: { value: anchorPoint.y } } };
|
|
||||||
const r = await this.sceneScript("setPropertyViaEditor", [uuid, path, dump]);
|
|
||||||
results.push({ property: "anchorPoint", success: r?.success !== false });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Widget の設定
|
|
||||||
const widget = parseMaybeJson(args.widget);
|
|
||||||
if (widget) {
|
|
||||||
// Widget コンポーネントを探す(なければ追加)
|
|
||||||
let nodeInfo = await this.sceneScript("getNodeInfo", [uuid]);
|
|
||||||
if (!nodeInfo?.success) return err(`Node ${uuid} not found`);
|
|
||||||
let comps = nodeInfo.data?.components || [];
|
|
||||||
let wIdx = comps.findIndex((c: any) => c.type === "Widget");
|
|
||||||
if (wIdx < 0) {
|
|
||||||
await this.sceneScript("addComponentToNode", [uuid, "cc.Widget"]);
|
|
||||||
// 再取得
|
|
||||||
nodeInfo = await this.sceneScript("getNodeInfo", [uuid]);
|
|
||||||
comps = nodeInfo.data?.components || [];
|
|
||||||
wIdx = comps.findIndex((c: any) => c.type === "Widget");
|
|
||||||
if (wIdx < 0) return err("Failed to add Widget component");
|
|
||||||
results.push({ property: "Widget", action: "added" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// isAlign* を自動設定(値があれば true にする)
|
|
||||||
const alignMap: Record<string, string> = {
|
|
||||||
top: "isAlignTop", bottom: "isAlignBottom",
|
|
||||||
left: "isAlignLeft", right: "isAlignRight",
|
|
||||||
horizontalCenter: "isAlignHorizontalCenter",
|
|
||||||
verticalCenter: "isAlignVerticalCenter",
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(widget)) {
|
|
||||||
// isAlign* を明示指定した場合はそのまま設定
|
|
||||||
const path = `__comps__.${wIdx}.${key}`;
|
|
||||||
if (typeof value === "boolean") {
|
|
||||||
const dump = { value, type: "Boolean" };
|
|
||||||
await this.sceneScript("setPropertyViaEditor", [uuid, path, dump]);
|
|
||||||
results.push({ property: `Widget.${key}`, success: true });
|
|
||||||
} else if (typeof value === "number") {
|
|
||||||
// まず対応する isAlign* を true にする
|
|
||||||
const alignKey = alignMap[key];
|
|
||||||
if (alignKey && widget[alignKey] === undefined) {
|
|
||||||
const alignPath = `__comps__.${wIdx}.${alignKey}`;
|
|
||||||
await this.sceneScript("setPropertyViaEditor", [uuid, alignPath, { value: true, type: "Boolean" }]);
|
|
||||||
}
|
|
||||||
const dump = { value, type: "Number" };
|
|
||||||
await this.sceneScript("setPropertyViaEditor", [uuid, path, dump]);
|
|
||||||
results.push({ property: `Widget.${key}`, success: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// _alignFlags を isAlign* 現在値から再計算して設定
|
|
||||||
// (Editor が isAlign* 変更時に _alignFlags を自動更新しないバグの対処)
|
|
||||||
try {
|
|
||||||
const ALIGN_BITS: Record<string, number> = {
|
|
||||||
isAlignLeft: 1, isAlignRight: 2, isAlignTop: 4, isAlignBottom: 8,
|
|
||||||
isAlignHorizontalCenter: 16, isAlignVerticalCenter: 32,
|
|
||||||
};
|
|
||||||
const nodeDump = await (Editor.Message.request as any)("scene", "query-node", uuid);
|
|
||||||
if (nodeDump) {
|
|
||||||
const wCompDump = nodeDump.__comps__?.[wIdx];
|
|
||||||
if (wCompDump) {
|
|
||||||
let alignFlags = 0;
|
|
||||||
for (const [key, bit] of Object.entries(ALIGN_BITS)) {
|
|
||||||
if (wCompDump.value?.[key]?.value === true) alignFlags |= bit;
|
|
||||||
}
|
|
||||||
const flagPath = `__comps__.${wIdx}._alignFlags`;
|
|
||||||
await this.sceneScript("setPropertyViaEditor", [uuid, flagPath, { value: alignFlags, type: "Number" }]);
|
|
||||||
results.push({ property: "Widget._alignFlags", value: alignFlags });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (_e) {
|
|
||||||
// _alignFlags 再計算の失敗は致命的でないため無視
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// color
|
|
||||||
const color = parseMaybeJson(args.color);
|
|
||||||
if (color) {
|
|
||||||
const r = await this.sceneScript("setNodeProperty", [uuid, "color", color]);
|
|
||||||
results.push({ property: "color", success: r?.success !== false });
|
|
||||||
}
|
|
||||||
|
|
||||||
// opacity
|
|
||||||
if (args.opacity !== undefined) {
|
|
||||||
// cc.UIOpacity を使う(なければ color.a で設定)
|
|
||||||
const nodeInfo = await this.sceneScript("getNodeInfo", [uuid]);
|
|
||||||
const comps = nodeInfo?.data?.components || [];
|
|
||||||
const opIdx = comps.findIndex((c: any) => c.type === "UIOpacity");
|
|
||||||
if (opIdx >= 0) {
|
|
||||||
const path = `__comps__.${opIdx}.opacity`;
|
|
||||||
await this.sceneScript("setPropertyViaEditor", [uuid, path, { value: args.opacity, type: "Number" }]);
|
|
||||||
results.push({ property: "UIOpacity.opacity", success: true });
|
|
||||||
} else {
|
|
||||||
// UIOpacity がない場合は color.a を直接設定
|
|
||||||
const currentColor = nodeInfo?.data?.color || { r: 255, g: 255, b: 255, a: 255 };
|
|
||||||
currentColor.a = args.opacity;
|
|
||||||
const r = await this.sceneScript("setNodeProperty", [uuid, "color", currentColor]);
|
|
||||||
results.push({ property: "color.a", success: r?.success !== false });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const allOk = results.every(r => r.success !== false);
|
|
||||||
let response: any = { success: allOk, uuid, results };
|
|
||||||
|
|
||||||
// screenshot
|
|
||||||
if (args.screenshot) {
|
|
||||||
try {
|
|
||||||
const ss = await takeEditorScreenshot();
|
|
||||||
response.screenshot = { path: ss.path, size: ss.savedSize };
|
|
||||||
} catch (ssErr: any) {
|
|
||||||
response.screenshotError = ssErr.message || String(ssErr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ok(response);
|
|
||||||
} catch (e: any) {
|
|
||||||
return err(e.message || String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Call a scene script method */
|
|
||||||
private async sceneScript(method: string, args: any[]): Promise<any> {
|
|
||||||
return Editor.Message.request("scene", "execute-scene-script", {
|
|
||||||
name: EXT_NAME,
|
|
||||||
method,
|
|
||||||
args,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-680
@@ -1,680 +0,0 @@
|
|||||||
import { ToolCategory, ToolDefinition, ToolResult } from "../types";
|
|
||||||
import { ok, err } from "../tool-base";
|
|
||||||
import { ensureSceneSafeToSwitch, safeSaveScene } from "./scene-tools";
|
|
||||||
import { parseMaybeJson } from "../utils";
|
|
||||||
import type { ComponentTools } from "./component-tools";
|
|
||||||
import fs from "fs";
|
|
||||||
import path from "path";
|
|
||||||
import crypto from "crypto";
|
|
||||||
|
|
||||||
const EXT_NAME = "cocos-creator-mcp";
|
|
||||||
|
|
||||||
/** prefab_instantiate で配置したネスト Prefab 情報を記憶 */
|
|
||||||
interface NestedPrefabEntry {
|
|
||||||
nodeUuid: string;
|
|
||||||
prefabAssetUuid: string;
|
|
||||||
parentUuid: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class PrefabTools implements ToolCategory {
|
|
||||||
readonly categoryName = "prefab";
|
|
||||||
private _pendingNestedPrefabs: NestedPrefabEntry[] = [];
|
|
||||||
/** prefab_open で開いた Prefab アセット UUID */
|
|
||||||
private _currentPrefabUuid: string | null = null;
|
|
||||||
private _componentTools: ComponentTools | null;
|
|
||||||
|
|
||||||
constructor(componentTools?: ComponentTools) {
|
|
||||||
this._componentTools = componentTools ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
getTools(): ToolDefinition[] {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
name: "prefab_list",
|
|
||||||
description: "List all prefab files in the project.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "prefab_create",
|
|
||||||
description: "Create a prefab from an existing node in the scene. The node remains in the scene.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
uuid: { type: "string", description: "Node UUID to create prefab from" },
|
|
||||||
path: { type: "string", description: "db:// path for the prefab (e.g. 'db://assets/prefabs/MyPrefab.prefab')" },
|
|
||||||
},
|
|
||||||
required: ["uuid", "path"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "prefab_instantiate",
|
|
||||||
description: "Instantiate a prefab into the scene.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
prefabUuid: { type: "string", description: "Prefab asset UUID" },
|
|
||||||
parent: { type: "string", description: "Parent node UUID (optional, defaults to scene root)" },
|
|
||||||
},
|
|
||||||
required: ["prefabUuid"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "prefab_get_info",
|
|
||||||
description: "Get information about a prefab asset (name, path, UUID).",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
uuid: { type: "string", description: "Prefab asset UUID" },
|
|
||||||
},
|
|
||||||
required: ["uuid"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "prefab_update",
|
|
||||||
description: "Update (re-save) a prefab from its instance node in the scene.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
uuid: { type: "string", description: "Node UUID of the prefab instance in the scene" },
|
|
||||||
},
|
|
||||||
required: ["uuid"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "prefab_duplicate",
|
|
||||||
description: "Duplicate a prefab asset to a new path.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
source: { type: "string", description: "Source prefab db:// path" },
|
|
||||||
destination: { type: "string", description: "Destination db:// path" },
|
|
||||||
},
|
|
||||||
required: ["source", "destination"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "prefab_validate",
|
|
||||||
description: "Validate a prefab for missing references or broken links.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
uuid: { type: "string", description: "Prefab asset UUID" },
|
|
||||||
},
|
|
||||||
required: ["uuid"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "prefab_revert",
|
|
||||||
description: "Revert a prefab instance node to its original prefab state.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
uuid: { type: "string", description: "Node UUID of the prefab instance" },
|
|
||||||
},
|
|
||||||
required: ["uuid"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "prefab_create_and_replace",
|
|
||||||
description: "Create a prefab from a node AND replace the original node with a prefab instance. This is the recommended way to extract a nested prefab — one command instead of create → delete → instantiate.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
uuid: { type: "string", description: "Node UUID to create prefab from" },
|
|
||||||
path: { type: "string", description: "db:// path for the prefab (e.g. 'db://assets/prefabs/MyPrefab.prefab')" },
|
|
||||||
},
|
|
||||||
required: ["uuid", "path"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "prefab_open",
|
|
||||||
description: "Open a prefab in editing mode. Equivalent to double-clicking the prefab in CocosCreator. Returns an error if the current scene is dirty and untitled (to avoid modal save dialog); pass force=true to bypass.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
uuid: { type: "string", description: "Prefab asset UUID" },
|
|
||||||
path: { type: "string", description: "Prefab db:// path (alternative to uuid)" },
|
|
||||||
force: { type: "boolean", description: "Skip dirty-scene preflight check (may trigger modal save dialog)" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "prefab_close",
|
|
||||||
description: "Save and close the current prefab editing mode, then return to the main scene. Returns an error if the current prefab is dirty and untitled (to avoid modal save dialog); pass force=true to bypass.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
save: { type: "boolean", description: "Save prefab before closing (default true)" },
|
|
||||||
sceneUuid: { type: "string", description: "Scene UUID to return to (default: project's start scene or first scene)" },
|
|
||||||
force: { type: "boolean", description: "Skip dirty-scene preflight check (may trigger modal save dialog)" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "prefab_create_from_spec",
|
|
||||||
description: "Create a prefab from a JSON spec in one call. Combines node_create_tree + component_auto_bind + prefab_create into a single operation. Spec extends node_create_tree format with optional autoBind field. Example: { name: 'MyPopup', components: ['cc.UITransform', 'MyPopupView'], autoBind: 'MyPopupView', children: [{ name: 'CloseButton', components: ['cc.Button'] }] }",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
path: { type: "string", description: "db:// path for the prefab (e.g. 'db://assets/prefabs/MyPrefab.prefab')" },
|
|
||||||
spec: { description: "Node tree specification with optional autoBind field (string for component type to auto-bind)" },
|
|
||||||
autoBindMode: { type: "string", enum: ["fuzzy", "strict"], description: "Auto-bind matching mode (default: fuzzy)" },
|
|
||||||
},
|
|
||||||
required: ["path", "spec"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
async execute(toolName: string, args: Record<string, any>): Promise<ToolResult> {
|
|
||||||
switch (toolName) {
|
|
||||||
case "prefab_list":
|
|
||||||
return this.listPrefabs();
|
|
||||||
case "prefab_create":
|
|
||||||
return this.createPrefab(args.uuid, args.path);
|
|
||||||
case "prefab_instantiate":
|
|
||||||
return this.instantiatePrefab(args.prefabUuid, args.parent);
|
|
||||||
case "prefab_get_info":
|
|
||||||
return this.getPrefabInfo(args.uuid);
|
|
||||||
case "prefab_update":
|
|
||||||
return this.updatePrefab(args.uuid);
|
|
||||||
case "prefab_duplicate": {
|
|
||||||
try {
|
|
||||||
await (Editor.Message.request as any)("asset-db", "copy-asset", args.source, args.destination);
|
|
||||||
return ok({ success: true, source: args.source, destination: args.destination });
|
|
||||||
} catch (e: any) { return err(e.message || String(e)); }
|
|
||||||
}
|
|
||||||
case "prefab_validate": {
|
|
||||||
try {
|
|
||||||
const info = await (Editor.Message.request as any)("asset-db", "query-asset-info", args.uuid);
|
|
||||||
const deps = await (Editor.Message.request as any)("asset-db", "query-depends", args.uuid).catch(() => []);
|
|
||||||
return ok({ success: true, uuid: args.uuid, info, dependencies: deps, valid: !!info });
|
|
||||||
} catch (e: any) { return err(e.message || String(e)); }
|
|
||||||
}
|
|
||||||
case "prefab_revert":
|
|
||||||
return this.revertPrefab(args.uuid);
|
|
||||||
case "prefab_create_and_replace":
|
|
||||||
return this.createAndReplace(args.uuid, args.path);
|
|
||||||
case "prefab_open":
|
|
||||||
return this.openPrefab(args.uuid, args.path, !!args.force);
|
|
||||||
case "prefab_close":
|
|
||||||
return this.closePrefab(args.save !== false, args.sceneUuid, !!args.force);
|
|
||||||
case "prefab_create_from_spec":
|
|
||||||
return this.createFromSpec(args.path, parseMaybeJson(args.spec), args.autoBindMode ?? "fuzzy");
|
|
||||||
default:
|
|
||||||
return err(`Unknown tool: ${toolName}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async listPrefabs(): Promise<ToolResult> {
|
|
||||||
try {
|
|
||||||
const results = await Editor.Message.request("asset-db", "query-assets", {
|
|
||||||
pattern: "db://assets/**/*.prefab",
|
|
||||||
});
|
|
||||||
const prefabs = (results || []).map((a: any) => ({
|
|
||||||
uuid: a.uuid,
|
|
||||||
path: a.path || a.url,
|
|
||||||
name: a.name,
|
|
||||||
}));
|
|
||||||
return ok({ success: true, prefabs });
|
|
||||||
} catch (e: any) {
|
|
||||||
return err(e.message || String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async createPrefab(nodeUuid: string, path: string): Promise<ToolResult> {
|
|
||||||
try {
|
|
||||||
// 既存Prefabがある場合は警告を返す(上書きダイアログでタイムアウトするため)
|
|
||||||
const existing = await this.assetExists(path);
|
|
||||||
if (existing) {
|
|
||||||
return err(
|
|
||||||
`Prefab already exists at "${path}". Use prefab_update instead to update an existing prefab. ` +
|
|
||||||
`Workflow: 1) prefab_instantiate to place in scene, 2) modify properties, 3) prefab_update to save.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const result = await (Editor.Message.request as any)("scene", "create-prefab", nodeUuid, path);
|
|
||||||
return ok({ success: true, nodeUuid, path, result });
|
|
||||||
} catch (e: any) {
|
|
||||||
return err(e.message || String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async assetExists(path: string): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
const pattern = path.replace(/\.prefab$/, "") + ".*";
|
|
||||||
const results = await Editor.Message.request("asset-db", "query-assets", { pattern });
|
|
||||||
return (results || []).length > 0;
|
|
||||||
} catch {
|
|
||||||
try {
|
|
||||||
const info = await (Editor.Message.request as any)("asset-db", "query-asset-info", path);
|
|
||||||
return !!info;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async instantiatePrefab(prefabUuid: string, parent?: string): Promise<ToolResult> {
|
|
||||||
try {
|
|
||||||
const nodeUuid = await Editor.Message.request("scene", "create-node", {
|
|
||||||
parent: parent || undefined,
|
|
||||||
assetUuid: prefabUuid,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Prefab 編集モード中の場合、ネスト Prefab 情報を記憶
|
|
||||||
// prefab_update 時に JSON 後処理で asset/instance/nestedPrefabInstanceRoots を設定
|
|
||||||
if (parent) {
|
|
||||||
this._pendingNestedPrefabs.push({
|
|
||||||
nodeUuid,
|
|
||||||
prefabAssetUuid: prefabUuid,
|
|
||||||
parentUuid: parent,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return ok({ success: true, nodeUuid, prefabUuid });
|
|
||||||
} catch (e: any) {
|
|
||||||
return err(e.message || String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private async updatePrefab(nodeUuid: string): Promise<ToolResult> {
|
|
||||||
try {
|
|
||||||
const result = await (Editor.Message.request as any)("scene", "apply-prefab", nodeUuid);
|
|
||||||
|
|
||||||
// ネスト Prefab の JSON 後処理
|
|
||||||
if (this._pendingNestedPrefabs.length > 0) {
|
|
||||||
await this._fixNestedPrefabJson(nodeUuid);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ok({ success: true, nodeUuid, result });
|
|
||||||
} catch (e: any) {
|
|
||||||
return err(e.message || String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* prefab_update 後に Prefab JSON を後処理して、ネスト Prefab 参照を正しく設定する.
|
|
||||||
*/
|
|
||||||
private async _fixNestedPrefabJson(_rootNodeUuid: string): Promise<void> {
|
|
||||||
if (this._pendingNestedPrefabs.length === 0) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// シーンを保存して Prefab JSON を書き出す
|
|
||||||
// 現在シーンが untitled (scene-2d) の場合、save-scene はダイアログを出すので
|
|
||||||
// safeSaveScene でスキップする。untitled でシーンにインスタンスが居るケースは
|
|
||||||
// 本来 prefab_open モードでのみ発生するので save が効く想定だが、
|
|
||||||
// テスト等で直接呼ばれた場合の保護として skip する。
|
|
||||||
const saved = await safeSaveScene();
|
|
||||||
if (!saved) {
|
|
||||||
console.warn(
|
|
||||||
"[PrefabTools] _fixNestedPrefabJson: save-scene skipped (untitled scene). " +
|
|
||||||
"Nested prefab JSON post-processing may be incomplete."
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await new Promise(r => setTimeout(r, 1500));
|
|
||||||
|
|
||||||
// prefab_open で記憶した UUID からファイルパスを取得
|
|
||||||
if (!this._currentPrefabUuid) return;
|
|
||||||
|
|
||||||
const prefabPath = await (Editor.Message.request as any)(
|
|
||||||
"asset-db", "query-path", this._currentPrefabUuid
|
|
||||||
);
|
|
||||||
if (!prefabPath) return;
|
|
||||||
if (!fs.existsSync(prefabPath)) return;
|
|
||||||
|
|
||||||
const data = JSON.parse(fs.readFileSync(prefabPath, "utf-8"));
|
|
||||||
|
|
||||||
// 各ネスト Prefab エントリを処理
|
|
||||||
for (const entry of this._pendingNestedPrefabs) {
|
|
||||||
// fileId でノードを検索(nodeUuid はシーン内 UUID、Prefab JSON 内では fileId)
|
|
||||||
let flpNodeIdx = -1;
|
|
||||||
for (let i = 0; i < data.length; i++) {
|
|
||||||
if (data[i].__type__ === "cc.PrefabInfo" && data[i].fileId === entry.nodeUuid) {
|
|
||||||
// この PrefabInfo を持つノードを探す
|
|
||||||
for (let j = 0; j < data.length; j++) {
|
|
||||||
if (data[j]._prefab?.__id__ === i) {
|
|
||||||
flpNodeIdx = j;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// fileId で見つからない場合、ノード名で検索
|
|
||||||
if (flpNodeIdx < 0) {
|
|
||||||
// Prefab アセット名を取得
|
|
||||||
const assetInfo = await (Editor.Message.request as any)("asset-db", "query-asset-info", entry.prefabAssetUuid);
|
|
||||||
const assetName = assetInfo?.name?.replace(".prefab", "") || "";
|
|
||||||
for (let i = 0; i < data.length; i++) {
|
|
||||||
if (data[i].__type__ === "cc.Node" && (data[i]._name === assetName || data[i]._name === undefined)) {
|
|
||||||
const prefabIdx = data[i]._prefab?.__id__;
|
|
||||||
if (prefabIdx != null && data[prefabIdx]?.asset?.__id__ === 0 && !data[prefabIdx]?.instance) {
|
|
||||||
flpNodeIdx = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (flpNodeIdx < 0) continue;
|
|
||||||
|
|
||||||
const prefabInfoIdx = data[flpNodeIdx]._prefab?.__id__;
|
|
||||||
if (prefabInfoIdx == null) continue;
|
|
||||||
|
|
||||||
// PrefabInfo を修正
|
|
||||||
const prefabInfo = data[prefabInfoIdx];
|
|
||||||
prefabInfo.root = { __id__: flpNodeIdx };
|
|
||||||
prefabInfo.asset = {
|
|
||||||
__uuid__: entry.prefabAssetUuid,
|
|
||||||
__expectedType__: "cc.Prefab",
|
|
||||||
};
|
|
||||||
|
|
||||||
// PrefabInstance を追加
|
|
||||||
if (!prefabInfo.instance) {
|
|
||||||
const instanceIdx = data.length;
|
|
||||||
data.push({
|
|
||||||
__type__: "cc.PrefabInstance",
|
|
||||||
fileId: crypto.randomBytes(16).toString("base64").replace(/[+/=]/g, "").substring(0, 22),
|
|
||||||
prefabRootNode: { __id__: 1 }, // Prefab 編集モードのルート
|
|
||||||
mountedChildren: [],
|
|
||||||
mountedComponents: [],
|
|
||||||
propertyOverrides: [],
|
|
||||||
removedComponents: [],
|
|
||||||
});
|
|
||||||
prefabInfo.instance = { __id__: instanceIdx };
|
|
||||||
}
|
|
||||||
|
|
||||||
// 子ノード・コンポーネントをクリア(Prefab アセットから復元される)
|
|
||||||
data[flpNodeIdx]._children = [];
|
|
||||||
data[flpNodeIdx]._components = [];
|
|
||||||
|
|
||||||
// ルートの nestedPrefabInstanceRoots に追加
|
|
||||||
const rootPrefabIdx = data[1]._prefab?.__id__;
|
|
||||||
if (rootPrefabIdx != null) {
|
|
||||||
const rootPrefab = data[rootPrefabIdx];
|
|
||||||
if (!rootPrefab.nestedPrefabInstanceRoots) {
|
|
||||||
rootPrefab.nestedPrefabInstanceRoots = [];
|
|
||||||
}
|
|
||||||
const alreadyNested = rootPrefab.nestedPrefabInstanceRoots.some(
|
|
||||||
(r: any) => r?.__id__ === flpNodeIdx
|
|
||||||
);
|
|
||||||
if (!alreadyNested) {
|
|
||||||
rootPrefab.nestedPrefabInstanceRoots.push({ __id__: flpNodeIdx });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.writeFileSync(prefabPath, JSON.stringify(data, null, 2), "utf-8");
|
|
||||||
this._pendingNestedPrefabs = [];
|
|
||||||
} catch (e: any) {
|
|
||||||
console.warn("[PrefabTools] _fixNestedPrefabJson failed:", e.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async revertPrefab(nodeUuid: string): Promise<ToolResult> {
|
|
||||||
try {
|
|
||||||
const result = await (Editor.Message.request as any)("scene", "revert-prefab", nodeUuid);
|
|
||||||
return ok({ success: true, nodeUuid, result });
|
|
||||||
} catch (e: any) {
|
|
||||||
return err(e.message || String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getPrefabInfo(uuid: string): Promise<ToolResult> {
|
|
||||||
try {
|
|
||||||
const info = await (Editor.Message.request as any)("asset-db", "query-asset-info", uuid);
|
|
||||||
return ok({ success: true, info });
|
|
||||||
} catch (e: any) {
|
|
||||||
return err(e.message || String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async createAndReplace(nodeUuid: string, path: string): Promise<ToolResult> {
|
|
||||||
try {
|
|
||||||
// 1. Check if prefab already exists
|
|
||||||
const existing = await this.assetExists(path);
|
|
||||||
if (existing) {
|
|
||||||
return err(
|
|
||||||
`Prefab already exists at "${path}". Delete it first or use a different path.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Get node info (parent, sibling index, transform) before creating prefab
|
|
||||||
const nodeInfo = await this.sceneScript("getNodeInfo", [nodeUuid]);
|
|
||||||
if (!nodeInfo?.success) {
|
|
||||||
return err(`Node ${nodeUuid} not found`);
|
|
||||||
}
|
|
||||||
const parentUuid = nodeInfo.data?.parent;
|
|
||||||
|
|
||||||
// 3. Create prefab from the node
|
|
||||||
const prefabAssetUuid = await (Editor.Message.request as any)("scene", "create-prefab", nodeUuid, path);
|
|
||||||
if (!prefabAssetUuid) {
|
|
||||||
return err("create-prefab returned no asset UUID");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Delete the original node
|
|
||||||
await (Editor.Message.request as any)("scene", "remove-node", { uuid: nodeUuid });
|
|
||||||
|
|
||||||
// 5. Instantiate the prefab at the same parent
|
|
||||||
const newNodeUuid = await Editor.Message.request("scene", "create-node", {
|
|
||||||
parent: parentUuid || undefined,
|
|
||||||
assetUuid: prefabAssetUuid,
|
|
||||||
});
|
|
||||||
|
|
||||||
return ok({
|
|
||||||
success: true,
|
|
||||||
prefabAssetUuid,
|
|
||||||
prefabPath: path,
|
|
||||||
originalNodeUuid: nodeUuid,
|
|
||||||
newInstanceUuid: newNodeUuid,
|
|
||||||
});
|
|
||||||
} catch (e: any) {
|
|
||||||
return err(e.message || String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async openPrefab(uuid?: string, path?: string, force: boolean = false): Promise<ToolResult> {
|
|
||||||
try {
|
|
||||||
// Prefab を開くときも内部的にシーン切替が発生するので dirty untitled チェック
|
|
||||||
await ensureSceneSafeToSwitch(force);
|
|
||||||
|
|
||||||
// Resolve UUID from path if needed
|
|
||||||
let assetUuid = uuid;
|
|
||||||
if (!assetUuid && path) {
|
|
||||||
const info = await (Editor.Message.request as any)("asset-db", "query-asset-info", path);
|
|
||||||
assetUuid = info?.uuid;
|
|
||||||
}
|
|
||||||
if (!assetUuid) {
|
|
||||||
return err("Either uuid or path is required");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open prefab in editing mode (equivalent to double-click)
|
|
||||||
await (Editor.Message.request as any)("asset-db", "open-asset", assetUuid);
|
|
||||||
// Wait for prefab editing mode to initialize
|
|
||||||
await new Promise(r => setTimeout(r, 1000));
|
|
||||||
|
|
||||||
this._currentPrefabUuid = assetUuid;
|
|
||||||
this._pendingNestedPrefabs = [];
|
|
||||||
|
|
||||||
return ok({ success: true, uuid: assetUuid, mode: "prefab-edit" });
|
|
||||||
} catch (e: any) {
|
|
||||||
return err(e.message || String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async closePrefab(save: boolean, sceneUuid?: string, force: boolean = false): Promise<ToolResult> {
|
|
||||||
try {
|
|
||||||
// 1. Save prefab if requested
|
|
||||||
// prefab edit モード中は "current scene" = 編集中 Prefab なので save-scene は Prefab を保存する。
|
|
||||||
// ただし untitled フォールバックに当たった場合はダイアログ回避のためスキップする。
|
|
||||||
if (save) {
|
|
||||||
await safeSaveScene();
|
|
||||||
await new Promise(r => setTimeout(r, 500));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Determine which scene to return to
|
|
||||||
let targetScene = sceneUuid;
|
|
||||||
if (!targetScene) {
|
|
||||||
// Try project's start scene
|
|
||||||
try {
|
|
||||||
targetScene = await (Editor as any).Profile.getConfig("preview", "general.start_scene", "local");
|
|
||||||
} catch { /* ignore */ }
|
|
||||||
|
|
||||||
// Fallback to first scene
|
|
||||||
if (!targetScene || targetScene === "current_scene") {
|
|
||||||
const scenes = await Editor.Message.request("asset-db", "query-assets", {
|
|
||||||
ccType: "cc.SceneAsset",
|
|
||||||
pattern: "db://assets/**/*",
|
|
||||||
});
|
|
||||||
if (Array.isArray(scenes) && scenes.length > 0) {
|
|
||||||
targetScene = scenes[0].uuid;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Open the scene
|
|
||||||
if (targetScene) {
|
|
||||||
// prefab edit モードから戻る遷移もダイアログが出うる
|
|
||||||
await ensureSceneSafeToSwitch(force);
|
|
||||||
await (Editor.Message.request as any)("scene", "open-scene", targetScene);
|
|
||||||
await new Promise(r => setTimeout(r, 1000));
|
|
||||||
}
|
|
||||||
|
|
||||||
return ok({ success: true, returnedToScene: targetScene });
|
|
||||||
} catch (e: any) {
|
|
||||||
return err(e.message || String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async createFromSpec(prefabPath: string, spec: any, autoBindMode: string): Promise<ToolResult> {
|
|
||||||
try {
|
|
||||||
// 1. 既存 Prefab チェック
|
|
||||||
const existing = await this.assetExists(prefabPath);
|
|
||||||
if (existing) {
|
|
||||||
return err(
|
|
||||||
`Prefab already exists at "${prefabPath}". Delete it first or use a different path.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. シーンの最初の cc.Node UUID を取得(Scene UUID ではなく Canvas 等)
|
|
||||||
const hier = await this.sceneScript("getSceneHierarchy", [false]);
|
|
||||||
const hierarchy = hier?.hierarchy || [];
|
|
||||||
const firstNode = hierarchy[0];
|
|
||||||
if (!firstNode?.uuid) return err("Could not find a node in the current scene to use as parent");
|
|
||||||
const parentUuid = firstNode.uuid;
|
|
||||||
|
|
||||||
// 3. ノードツリーを構築
|
|
||||||
const autoBind = spec.autoBind;
|
|
||||||
const cleanSpec = { ...spec };
|
|
||||||
delete cleanSpec.autoBind;
|
|
||||||
|
|
||||||
const treeResult = await this.sceneScript("buildNodeTree", [parentUuid, cleanSpec]);
|
|
||||||
if (!treeResult?.success) return err(treeResult?.error || "buildNodeTree failed");
|
|
||||||
const nodeUuid = treeResult.data?.uuid;
|
|
||||||
if (!nodeUuid) return err("buildNodeTree returned no root node UUID");
|
|
||||||
|
|
||||||
// 4. フォント・SpriteFrame を Editor API 経由で設定(アセット依存追跡のため)
|
|
||||||
await this._applyDefaultAssets(nodeUuid);
|
|
||||||
|
|
||||||
// 5. autoBind 実行 (旧4)
|
|
||||||
let autoBindResult: any = null;
|
|
||||||
if (autoBind) {
|
|
||||||
if (!this._componentTools) {
|
|
||||||
return err("autoBind requires ComponentTools dependency (internal configuration error)");
|
|
||||||
}
|
|
||||||
const bindToolResult = await this._componentTools.execute("component_auto_bind", {
|
|
||||||
uuid: nodeUuid,
|
|
||||||
componentType: autoBind,
|
|
||||||
force: false,
|
|
||||||
mode: autoBindMode,
|
|
||||||
});
|
|
||||||
try {
|
|
||||||
autoBindResult = JSON.parse(bindToolResult.content[0].text);
|
|
||||||
} catch { autoBindResult = bindToolResult; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. Prefab 作成
|
|
||||||
const prefabAssetUuid = await (Editor.Message.request as any)(
|
|
||||||
"scene", "create-prefab", nodeUuid, prefabPath
|
|
||||||
);
|
|
||||||
if (!prefabAssetUuid) {
|
|
||||||
await (Editor.Message.request as any)("scene", "remove-node", { uuid: nodeUuid });
|
|
||||||
return err("create-prefab returned no asset UUID");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 7. 一時ノードを削除
|
|
||||||
await (Editor.Message.request as any)("scene", "remove-node", { uuid: nodeUuid });
|
|
||||||
|
|
||||||
return ok({
|
|
||||||
success: true,
|
|
||||||
prefabAssetUuid,
|
|
||||||
path: prefabPath,
|
|
||||||
nodeTree: treeResult.data,
|
|
||||||
autoBind: autoBindResult,
|
|
||||||
});
|
|
||||||
} catch (e: any) {
|
|
||||||
return err(e.message || String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* buildNodeTree で作成したノードツリーの Label にプロジェクトフォントを設定する。
|
|
||||||
* Editor API (scene:set-property) 経由で設定することでアセット依存が正しく追跡される。
|
|
||||||
*/
|
|
||||||
private async _applyDefaultAssets(rootUuid: string): Promise<void> {
|
|
||||||
// プロジェクトのデフォルトフォントを検索(resources/fonts/ 配下の TTFFont)
|
|
||||||
let fontUuid: string | null = null;
|
|
||||||
try {
|
|
||||||
const assets = await Editor.Message.request("asset-db", "query-assets", {
|
|
||||||
pattern: "db://assets/resources/fonts/**",
|
|
||||||
ccType: "cc.TTFFont",
|
|
||||||
});
|
|
||||||
if (Array.isArray(assets) && assets.length > 0) {
|
|
||||||
fontUuid = assets[0].uuid;
|
|
||||||
}
|
|
||||||
} catch { /* ignore */ }
|
|
||||||
|
|
||||||
if (!fontUuid) return;
|
|
||||||
|
|
||||||
// 全子孫ノードを取得
|
|
||||||
const descendants = await this.sceneScript("getAllDescendants", [rootUuid]);
|
|
||||||
if (!descendants?.success) return;
|
|
||||||
const allNodes = [{ uuid: rootUuid, name: "root" }, ...descendants.data];
|
|
||||||
|
|
||||||
for (const node of allNodes) {
|
|
||||||
try {
|
|
||||||
const nodeDump = await (Editor.Message.request as any)("scene", "query-node", node.uuid);
|
|
||||||
if (!nodeDump) continue;
|
|
||||||
const comps = nodeDump.__comps__ || [];
|
|
||||||
for (let i = 0; i < comps.length; i++) {
|
|
||||||
const compType = comps[i].type || "";
|
|
||||||
// Label にフォント設定
|
|
||||||
if (compType === "cc.Label") {
|
|
||||||
const fontDump = comps[i].value?.font;
|
|
||||||
if (!fontDump?.value?.uuid) {
|
|
||||||
await (Editor.Message.request as any)("scene", "set-property", {
|
|
||||||
uuid: node.uuid,
|
|
||||||
path: `__comps__.${i}.font`,
|
|
||||||
dump: { type: "cc.TTFFont", value: { uuid: fontUuid } },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch { /* skip nodes that can't be queried */ }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async sceneScript(method: string, args: any[]): Promise<any> {
|
|
||||||
return Editor.Message.request("scene", "execute-scene-script", {
|
|
||||||
name: "cocos-creator-mcp",
|
|
||||||
method,
|
|
||||||
args,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-84
@@ -1,84 +0,0 @@
|
|||||||
import { ToolCategory, ToolDefinition, ToolResult } from "../types";
|
|
||||||
import { ok, err } from "../tool-base";
|
|
||||||
|
|
||||||
export class PreferencesTools implements ToolCategory {
|
|
||||||
readonly categoryName = "preferences";
|
|
||||||
|
|
||||||
getTools(): ToolDefinition[] {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
name: "preferences_get",
|
|
||||||
description: "Get a preference value by key.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
protocol: { type: "string", description: "Protocol name (e.g. 'general', 'builder', 'engine')" },
|
|
||||||
key: { type: "string", description: "Preference key" },
|
|
||||||
},
|
|
||||||
required: ["protocol", "key"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "preferences_set",
|
|
||||||
description: "Set a preference value.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
protocol: { type: "string", description: "Protocol name" },
|
|
||||||
key: { type: "string", description: "Preference key" },
|
|
||||||
value: { description: "Value to set" },
|
|
||||||
},
|
|
||||||
required: ["protocol", "key", "value"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "preferences_get_all",
|
|
||||||
description: "Get all preferences for a given protocol.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
protocol: { type: "string", description: "Protocol name (e.g. 'general')" },
|
|
||||||
},
|
|
||||||
required: ["protocol"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "preferences_reset",
|
|
||||||
description: "Reset a preference to its default value.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
protocol: { type: "string", description: "Protocol name" },
|
|
||||||
key: { type: "string", description: "Preference key to reset" },
|
|
||||||
},
|
|
||||||
required: ["protocol", "key"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
async execute(toolName: string, args: Record<string, any>): Promise<ToolResult> {
|
|
||||||
try {
|
|
||||||
switch (toolName) {
|
|
||||||
case "preferences_get": {
|
|
||||||
const value = Editor.Profile.getConfig(args.protocol, args.key);
|
|
||||||
return ok({ success: true, protocol: args.protocol, key: args.key, value });
|
|
||||||
}
|
|
||||||
case "preferences_set":
|
|
||||||
Editor.Profile.setConfig(args.protocol, args.key, args.value);
|
|
||||||
return ok({ success: true, protocol: args.protocol, key: args.key });
|
|
||||||
case "preferences_get_all": {
|
|
||||||
const config = Editor.Profile.getConfig(args.protocol);
|
|
||||||
return ok({ success: true, protocol: args.protocol, config });
|
|
||||||
}
|
|
||||||
case "preferences_reset":
|
|
||||||
Editor.Profile.removeConfig(args.protocol, args.key);
|
|
||||||
return ok({ success: true, protocol: args.protocol, key: args.key });
|
|
||||||
default:
|
|
||||||
return err(`Unknown tool: ${toolName}`);
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
return err(e.message || String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-167
@@ -1,167 +0,0 @@
|
|||||||
import { ToolCategory, ToolDefinition, ToolResult } from "../types";
|
|
||||||
import { ok, err } from "../tool-base";
|
|
||||||
|
|
||||||
export class ProjectTools implements ToolCategory {
|
|
||||||
readonly categoryName = "project";
|
|
||||||
|
|
||||||
getTools(): ToolDefinition[] {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
name: "project_get_info",
|
|
||||||
description: "Get project information (name, path, engine version).",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "project_refresh_assets",
|
|
||||||
description: "Refresh the asset database to detect file changes.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "project_get_asset_info",
|
|
||||||
description: "Get information about an asset by UUID.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
uuid: { type: "string", description: "Asset UUID" },
|
|
||||||
},
|
|
||||||
required: ["uuid"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "project_find_asset",
|
|
||||||
description: "Find assets by name pattern (glob). Returns matching asset paths and UUIDs.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
pattern: { type: "string", description: "Glob pattern (e.g. 'db://assets/**/*.ts', 'db://assets/**/Button*')" },
|
|
||||||
},
|
|
||||||
required: ["pattern"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "project_get_settings",
|
|
||||||
description: "Get project settings for a given protocol.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
protocol: { type: "string", description: "Settings protocol (e.g. 'general', 'engine')" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "project_set_settings",
|
|
||||||
description: "Set a project setting.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
protocol: { type: "string" },
|
|
||||||
key: { type: "string" },
|
|
||||||
value: {},
|
|
||||||
},
|
|
||||||
required: ["protocol", "key", "value"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "project_get_engine_info",
|
|
||||||
description: "Get engine version and path information.",
|
|
||||||
inputSchema: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "project_query_scripts",
|
|
||||||
description: "Query all script plugins in the project.",
|
|
||||||
inputSchema: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
async execute(toolName: string, args: Record<string, any>): Promise<ToolResult> {
|
|
||||||
switch (toolName) {
|
|
||||||
case "project_get_info":
|
|
||||||
return this.getInfo();
|
|
||||||
case "project_refresh_assets":
|
|
||||||
return this.refreshAssets();
|
|
||||||
case "project_get_asset_info":
|
|
||||||
return this.getAssetInfo(args.uuid);
|
|
||||||
case "project_find_asset":
|
|
||||||
return this.findAsset(args.pattern);
|
|
||||||
case "project_get_settings": {
|
|
||||||
try {
|
|
||||||
const config = await (Editor.Message.request as any)("project", "query-config", args.protocol || "general");
|
|
||||||
return ok({ success: true, config });
|
|
||||||
} catch (e: any) { return err(e.message || String(e)); }
|
|
||||||
}
|
|
||||||
case "project_set_settings": {
|
|
||||||
try {
|
|
||||||
await (Editor.Message.request as any)("project", "set-config", args.protocol, args.key, args.value);
|
|
||||||
return ok({ success: true });
|
|
||||||
} catch (e: any) { return err(e.message || String(e)); }
|
|
||||||
}
|
|
||||||
case "project_get_engine_info": {
|
|
||||||
try {
|
|
||||||
const info = await (Editor.Message.request as any)("engine", "query-info");
|
|
||||||
return ok({ success: true, info });
|
|
||||||
} catch (e: any) { return err(e.message || String(e)); }
|
|
||||||
}
|
|
||||||
case "project_query_scripts": {
|
|
||||||
try {
|
|
||||||
const scripts = await (Editor.Message.request as any)("programming", "query-sorted-plugins");
|
|
||||||
return ok({ success: true, scripts });
|
|
||||||
} catch (e: any) { return err(e.message || String(e)); }
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return err(`Unknown tool: ${toolName}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getInfo(): Promise<ToolResult> {
|
|
||||||
try {
|
|
||||||
return ok({
|
|
||||||
success: true,
|
|
||||||
name: Editor.Project.name,
|
|
||||||
path: Editor.Project.path,
|
|
||||||
tmpDir: Editor.Project.tmpDir,
|
|
||||||
});
|
|
||||||
} catch (e: any) {
|
|
||||||
return err(e.message || String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async refreshAssets(): Promise<ToolResult> {
|
|
||||||
try {
|
|
||||||
await Editor.Message.request("asset-db", "refresh-asset", "db://assets");
|
|
||||||
return ok({ success: true });
|
|
||||||
} catch (e: any) {
|
|
||||||
return err(e.message || String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getAssetInfo(uuid: string): Promise<ToolResult> {
|
|
||||||
try {
|
|
||||||
const info = await (Editor.Message.request as any)("asset-db", "query-asset-info", uuid);
|
|
||||||
return ok({ success: true, info });
|
|
||||||
} catch (e: any) {
|
|
||||||
return err(e.message || String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async findAsset(pattern: string): Promise<ToolResult> {
|
|
||||||
try {
|
|
||||||
const results = await Editor.Message.request("asset-db", "query-assets", { pattern });
|
|
||||||
const assets = (results || []).map((a: any) => ({
|
|
||||||
uuid: a.uuid,
|
|
||||||
path: a.path || a.url,
|
|
||||||
name: a.name,
|
|
||||||
type: a.type,
|
|
||||||
}));
|
|
||||||
return ok({ success: true, assets });
|
|
||||||
} catch (e: any) {
|
|
||||||
return err(e.message || String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-150
@@ -1,150 +0,0 @@
|
|||||||
import { ToolCategory, ToolDefinition, ToolResult } from "../types";
|
|
||||||
import { ok, err } from "../tool-base";
|
|
||||||
|
|
||||||
export class ReferenceImageTools implements ToolCategory {
|
|
||||||
readonly categoryName = "referenceImage";
|
|
||||||
|
|
||||||
getTools(): ToolDefinition[] {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
name: "refimage_add",
|
|
||||||
description: "Add a reference image to the scene view.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
path: { type: "string", description: "Image file path or db:// path" },
|
|
||||||
},
|
|
||||||
required: ["path"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "refimage_remove",
|
|
||||||
description: "Remove a reference image by index.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
index: { type: "number", description: "Image index" },
|
|
||||||
},
|
|
||||||
required: ["index"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "refimage_list",
|
|
||||||
description: "List all reference images in the scene view.",
|
|
||||||
inputSchema: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "refimage_clear_all",
|
|
||||||
description: "Remove all reference images.",
|
|
||||||
inputSchema: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "refimage_switch",
|
|
||||||
description: "Switch to a specific reference image by index.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
index: { type: "number", description: "Image index to switch to" },
|
|
||||||
},
|
|
||||||
required: ["index"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "refimage_set_position",
|
|
||||||
description: "Set the position of the current reference image.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
x: { type: "number" },
|
|
||||||
y: { type: "number" },
|
|
||||||
},
|
|
||||||
required: ["x", "y"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "refimage_set_scale",
|
|
||||||
description: "Set the scale of the current reference image.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
scale: { type: "number", description: "Scale factor" },
|
|
||||||
},
|
|
||||||
required: ["scale"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "refimage_set_opacity",
|
|
||||||
description: "Set the opacity of the current reference image.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
opacity: { type: "number", description: "Opacity (0-255)" },
|
|
||||||
},
|
|
||||||
required: ["opacity"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "refimage_query_config",
|
|
||||||
description: "Get the current reference image configuration.",
|
|
||||||
inputSchema: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "refimage_query_current",
|
|
||||||
description: "Get info about the currently active reference image.",
|
|
||||||
inputSchema: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "refimage_refresh",
|
|
||||||
description: "Refresh the reference image display.",
|
|
||||||
inputSchema: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
async execute(toolName: string, args: Record<string, any>): Promise<ToolResult> {
|
|
||||||
try {
|
|
||||||
switch (toolName) {
|
|
||||||
case "refimage_add":
|
|
||||||
await (Editor.Message.request as any)("scene", "add-reference-image", args.path);
|
|
||||||
return ok({ success: true, path: args.path });
|
|
||||||
case "refimage_remove":
|
|
||||||
await (Editor.Message.request as any)("scene", "remove-reference-image", args.index);
|
|
||||||
return ok({ success: true, index: args.index });
|
|
||||||
case "refimage_list": {
|
|
||||||
const config = await (Editor.Message.request as any)("scene", "query-reference-image-config").catch(() => null);
|
|
||||||
return ok({ success: true, config });
|
|
||||||
}
|
|
||||||
case "refimage_clear_all":
|
|
||||||
await (Editor.Message.request as any)("scene", "clear-all-reference-images");
|
|
||||||
return ok({ success: true });
|
|
||||||
case "refimage_switch":
|
|
||||||
await (Editor.Message.request as any)("scene", "switch-reference-image", args.index);
|
|
||||||
return ok({ success: true, index: args.index });
|
|
||||||
case "refimage_set_position":
|
|
||||||
await (Editor.Message.request as any)("scene", "set-reference-image-position", args.x, args.y);
|
|
||||||
return ok({ success: true, x: args.x, y: args.y });
|
|
||||||
case "refimage_set_scale":
|
|
||||||
await (Editor.Message.request as any)("scene", "set-reference-image-scale", args.scale);
|
|
||||||
return ok({ success: true, scale: args.scale });
|
|
||||||
case "refimage_set_opacity":
|
|
||||||
await (Editor.Message.request as any)("scene", "set-reference-image-opacity", args.opacity);
|
|
||||||
return ok({ success: true, opacity: args.opacity });
|
|
||||||
case "refimage_query_config": {
|
|
||||||
const config = await (Editor.Message.request as any)("scene", "query-reference-image-config").catch(() => null);
|
|
||||||
return ok({ success: true, config });
|
|
||||||
}
|
|
||||||
case "refimage_query_current": {
|
|
||||||
const current = await (Editor.Message.request as any)("scene", "query-current-reference-image").catch(() => null);
|
|
||||||
return ok({ success: true, current });
|
|
||||||
}
|
|
||||||
case "refimage_refresh":
|
|
||||||
await (Editor.Message.request as any)("scene", "refresh-reference-image");
|
|
||||||
return ok({ success: true });
|
|
||||||
default:
|
|
||||||
return err(`Unknown tool: ${toolName}`);
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
return err(e.message || String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-635
@@ -1,635 +0,0 @@
|
|||||||
import { ToolCategory, ToolDefinition, ToolResult } from "../types";
|
|
||||||
import { ok, err } from "../tool-base";
|
|
||||||
import { ensureSceneSafeToSwitch } from "./scene-tools";
|
|
||||||
|
|
||||||
const EXT_NAME = "cocos-creator-mcp";
|
|
||||||
|
|
||||||
export class SceneAdvancedTools implements ToolCategory {
|
|
||||||
readonly categoryName = "sceneAdvanced";
|
|
||||||
|
|
||||||
getTools(): ToolDefinition[] {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
name: "scene_execute_script",
|
|
||||||
description: "Execute a scene script method by name with arguments.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
method: { type: "string", description: "Scene script method name" },
|
|
||||||
args: { type: "array", description: "Arguments to pass", items: {} },
|
|
||||||
},
|
|
||||||
required: ["method"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "scene_snapshot",
|
|
||||||
description: "Take a snapshot of the current scene state (for undo).",
|
|
||||||
inputSchema: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "scene_query_dirty",
|
|
||||||
description: "Check if the current scene has unsaved changes.",
|
|
||||||
inputSchema: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "scene_query_classes",
|
|
||||||
description: "Query all available component classes in the scene.",
|
|
||||||
inputSchema: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "scene_query_components",
|
|
||||||
description: "Query available components for a given node.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
uuid: { type: "string", description: "Node UUID" },
|
|
||||||
},
|
|
||||||
required: ["uuid"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "scene_query_node_tree",
|
|
||||||
description: "Query the raw node tree from the editor (alternative to scene_get_hierarchy).",
|
|
||||||
inputSchema: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "scene_query_nodes_by_asset",
|
|
||||||
description: "Find all nodes that reference a given asset UUID.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
assetUuid: { type: "string", description: "Asset UUID to search for" },
|
|
||||||
},
|
|
||||||
required: ["assetUuid"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "scene_soft_reload",
|
|
||||||
description: "Soft reload the current scene without losing state.",
|
|
||||||
inputSchema: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "scene_reset_node_transform",
|
|
||||||
description: "Reset a node's transform to default (position 0,0,0 / rotation 0,0,0 / scale 1,1,1).",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
uuid: { type: "string", description: "Node UUID" },
|
|
||||||
},
|
|
||||||
required: ["uuid"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "scene_copy_node",
|
|
||||||
description: "Copy a node to clipboard.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
uuid: { type: "string", description: "Node UUID" },
|
|
||||||
},
|
|
||||||
required: ["uuid"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "scene_paste_node",
|
|
||||||
description: "Paste node from clipboard under a parent.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
parentUuid: { type: "string", description: "Parent node UUID" },
|
|
||||||
},
|
|
||||||
required: ["parentUuid"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "scene_create",
|
|
||||||
description: "Create a new empty 2D scene. If path is omitted, uses the editor's built-in new-scene command (may not work on CC 3.8.x). If path is specified, creates a .scene file via asset-db as a fallback. Returns an error if the current scene is dirty and untitled (to avoid modal save dialog); pass force=true to bypass.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
path: { type: "string", description: "Scene asset path (e.g. 'db://assets/scenes/NewScene.scene'). If omitted, uses editor's new-scene command." },
|
|
||||||
force: { type: "boolean", description: "Skip dirty-scene preflight check (may trigger modal save dialog)" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "scene_cut_node",
|
|
||||||
description: "Cut a node to clipboard (removes from scene).",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: { uuid: { type: "string", description: "Node UUID" } },
|
|
||||||
required: ["uuid"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "scene_reset_property",
|
|
||||||
description: "Reset a specific property on a node or component to its default value.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
uuid: { type: "string", description: "Node or component UUID" },
|
|
||||||
path: { type: "string", description: "Property path (e.g. 'position', 'color')" },
|
|
||||||
},
|
|
||||||
required: ["uuid", "path"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "scene_reset_component",
|
|
||||||
description: "Reset a component to its default state.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: { uuid: { type: "string", description: "Component UUID" } },
|
|
||||||
required: ["uuid"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "scene_execute_component_method",
|
|
||||||
description: "Call a method on a component at edit-time.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
uuid: { type: "string", description: "Component UUID" },
|
|
||||||
method: { type: "string", description: "Method name" },
|
|
||||||
args: { type: "array", description: "Method arguments", items: {} },
|
|
||||||
},
|
|
||||||
required: ["uuid", "method"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "scene_move_array_element",
|
|
||||||
description: "Move an array element to a new position in a property.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
uuid: { type: "string", description: "Node or component UUID" },
|
|
||||||
path: { type: "string", description: "Array property path" },
|
|
||||||
target: { type: "number", description: "Current index" },
|
|
||||||
offset: { type: "number", description: "Move offset (+1 = down, -1 = up)" },
|
|
||||||
},
|
|
||||||
required: ["uuid", "path", "target", "offset"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "scene_remove_array_element",
|
|
||||||
description: "Remove an element from an array property by index.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
uuid: { type: "string", description: "Node or component UUID" },
|
|
||||||
path: { type: "string", description: "Array property path" },
|
|
||||||
index: { type: "number", description: "Index to remove" },
|
|
||||||
},
|
|
||||||
required: ["uuid", "path", "index"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "scene_snapshot_abort",
|
|
||||||
description: "Abort the current undo snapshot.",
|
|
||||||
inputSchema: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "scene_query_ready",
|
|
||||||
description: "Check if the scene is fully loaded and ready.",
|
|
||||||
inputSchema: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "scene_query_component_has_script",
|
|
||||||
description: "Check if a component has an associated script file.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: { name: { type: "string", description: "Component class name" } },
|
|
||||||
required: ["name"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "scene_restore_prefab",
|
|
||||||
description: "Restore a prefab node to its original prefab state.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: { uuid: { type: "string", description: "Node UUID" } },
|
|
||||||
required: ["uuid"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "scene_begin_undo",
|
|
||||||
description: "Begin recording undo operations.",
|
|
||||||
inputSchema: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "scene_end_undo",
|
|
||||||
description: "End undo recording and save the undo step.",
|
|
||||||
inputSchema: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "scene_cancel_undo",
|
|
||||||
description: "Cancel the current undo recording.",
|
|
||||||
inputSchema: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "scene_save_as",
|
|
||||||
description: "Save the current scene to a new file (shows save dialog).",
|
|
||||||
inputSchema: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
// ── 以下、既存MCP未対応のEditor API ──
|
|
||||||
{
|
|
||||||
name: "scene_set_parent",
|
|
||||||
description: "Reparent node(s) using the official Editor API (alternative to node_move).",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
uuids: { type: "array", items: { type: "string" }, description: "Node UUID(s) to move" },
|
|
||||||
parent: { type: "string", description: "New parent node UUID" },
|
|
||||||
keepWorldTransform: { type: "boolean", description: "Keep world position (default false)" },
|
|
||||||
},
|
|
||||||
required: ["uuids", "parent"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "scene_query_node",
|
|
||||||
description: "Get a full property dump of a node (all serialized data).",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: { uuid: { type: "string", description: "Node UUID" } },
|
|
||||||
required: ["uuid"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "scene_query_component",
|
|
||||||
description: "Get a full property dump of a component by its UUID.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: { uuid: { type: "string", description: "Component UUID" } },
|
|
||||||
required: ["uuid"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "scene_query_scene_bounds",
|
|
||||||
description: "Get the bounding rect of the current scene.",
|
|
||||||
inputSchema: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
async execute(toolName: string, args: Record<string, any>): Promise<ToolResult> {
|
|
||||||
try {
|
|
||||||
switch (toolName) {
|
|
||||||
case "scene_execute_script":
|
|
||||||
return ok(await this.sceneScript(args.method, args.args || []));
|
|
||||||
case "scene_snapshot":
|
|
||||||
return ok(await (Editor.Message.request as any)("scene", "snapshot"));
|
|
||||||
case "scene_query_dirty": {
|
|
||||||
const dirty = await (Editor.Message.request as any)("scene", "query-dirty");
|
|
||||||
return ok({ success: true, dirty });
|
|
||||||
}
|
|
||||||
case "scene_query_classes": {
|
|
||||||
const classes = await (Editor.Message.request as any)("scene", "query-classes");
|
|
||||||
return ok({ success: true, classes });
|
|
||||||
}
|
|
||||||
case "scene_query_components": {
|
|
||||||
const comps = await (Editor.Message.request as any)("scene", "query-components", args.uuid);
|
|
||||||
return ok({ success: true, components: comps });
|
|
||||||
}
|
|
||||||
case "scene_query_node_tree": {
|
|
||||||
const tree = await Editor.Message.request("scene", "query-node-tree");
|
|
||||||
return ok({ success: true, tree });
|
|
||||||
}
|
|
||||||
case "scene_query_nodes_by_asset": {
|
|
||||||
const nodes = await (Editor.Message.request as any)("scene", "query-nodes-by-asset-uuid", args.assetUuid);
|
|
||||||
return ok({ success: true, nodes });
|
|
||||||
}
|
|
||||||
case "scene_soft_reload":
|
|
||||||
await (Editor.Message.request as any)("scene", "soft-reload");
|
|
||||||
return ok({ success: true });
|
|
||||||
case "scene_reset_node_transform":
|
|
||||||
return await this.resetTransform(args.uuid);
|
|
||||||
case "scene_copy_node":
|
|
||||||
await (Editor.Message.request as any)("scene", "copy-node", args.uuid);
|
|
||||||
return ok({ success: true, uuid: args.uuid });
|
|
||||||
case "scene_paste_node": {
|
|
||||||
const result = await (Editor.Message.request as any)("scene", "paste-node", args.parentUuid);
|
|
||||||
return ok({ success: true, result });
|
|
||||||
}
|
|
||||||
case "scene_create":
|
|
||||||
return this.createScene(args.path, !!args.force);
|
|
||||||
case "scene_cut_node":
|
|
||||||
await (Editor.Message.request as any)("scene", "cut-node", args.uuid);
|
|
||||||
return ok({ success: true, uuid: args.uuid });
|
|
||||||
case "scene_reset_property":
|
|
||||||
await (Editor.Message.request as any)("scene", "reset-property", { uuid: args.uuid, path: args.path });
|
|
||||||
return ok({ success: true });
|
|
||||||
case "scene_reset_component":
|
|
||||||
await (Editor.Message.request as any)("scene", "reset-component", { uuid: args.uuid });
|
|
||||||
return ok({ success: true });
|
|
||||||
case "scene_execute_component_method": {
|
|
||||||
const result = await (Editor.Message.request as any)("scene", "execute-component-method", { uuid: args.uuid, name: args.method, args: args.args || [] });
|
|
||||||
return ok({ success: true, result });
|
|
||||||
}
|
|
||||||
case "scene_move_array_element":
|
|
||||||
await (Editor.Message.request as any)("scene", "move-array-element", { uuid: args.uuid, path: args.path, target: args.target, offset: args.offset });
|
|
||||||
return ok({ success: true });
|
|
||||||
case "scene_remove_array_element":
|
|
||||||
await (Editor.Message.request as any)("scene", "remove-array-element", { uuid: args.uuid, path: args.path, index: args.index });
|
|
||||||
return ok({ success: true });
|
|
||||||
case "scene_snapshot_abort":
|
|
||||||
await (Editor.Message.request as any)("scene", "snapshot-abort");
|
|
||||||
return ok({ success: true });
|
|
||||||
case "scene_query_ready": {
|
|
||||||
const ready = await (Editor.Message.request as any)("scene", "query-is-ready");
|
|
||||||
return ok({ success: true, ready });
|
|
||||||
}
|
|
||||||
case "scene_query_component_has_script": {
|
|
||||||
const hasScript = await (Editor.Message.request as any)("scene", "query-component-has-script", args.name);
|
|
||||||
return ok({ success: true, name: args.name, hasScript });
|
|
||||||
}
|
|
||||||
case "scene_restore_prefab":
|
|
||||||
await (Editor.Message.request as any)("scene", "restore-prefab", { uuid: args.uuid });
|
|
||||||
return ok({ success: true, uuid: args.uuid });
|
|
||||||
case "scene_begin_undo":
|
|
||||||
await (Editor.Message.request as any)("scene", "begin-recording");
|
|
||||||
return ok({ success: true });
|
|
||||||
case "scene_end_undo":
|
|
||||||
await (Editor.Message.request as any)("scene", "end-recording");
|
|
||||||
return ok({ success: true });
|
|
||||||
case "scene_cancel_undo":
|
|
||||||
await (Editor.Message.request as any)("scene", "cancel-recording");
|
|
||||||
return ok({ success: true });
|
|
||||||
case "scene_save_as": {
|
|
||||||
const result = await (Editor.Message.request as any)("scene", "save-as-scene");
|
|
||||||
return ok({ success: true, result });
|
|
||||||
}
|
|
||||||
case "scene_set_parent":
|
|
||||||
await (Editor.Message.request as any)("scene", "set-parent", {
|
|
||||||
parent: args.parent,
|
|
||||||
uuids: args.uuids,
|
|
||||||
keepWorldTransform: args.keepWorldTransform || false,
|
|
||||||
});
|
|
||||||
return ok({ success: true });
|
|
||||||
case "scene_query_node": {
|
|
||||||
const dump = await (Editor.Message.request as any)("scene", "query-node", args.uuid);
|
|
||||||
return ok({ success: true, node: dump });
|
|
||||||
}
|
|
||||||
case "scene_query_component": {
|
|
||||||
const dump = await (Editor.Message.request as any)("scene", "query-component", args.uuid);
|
|
||||||
return ok({ success: true, component: dump });
|
|
||||||
}
|
|
||||||
case "scene_query_scene_bounds": {
|
|
||||||
const bounds = await (Editor.Message.request as any)("scene", "query-scene-bounds");
|
|
||||||
return ok({ success: true, bounds });
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return err(`Unknown tool: ${toolName}`);
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
return err(e.message || String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async resetTransform(uuid: string): Promise<ToolResult> {
|
|
||||||
const result = await this.sceneScript("setNodeProperty", [uuid, "position", { x: 0, y: 0, z: 0 }]);
|
|
||||||
await this.sceneScript("setNodeProperty", [uuid, "rotation", { x: 0, y: 0, z: 0 }]);
|
|
||||||
await this.sceneScript("setNodeProperty", [uuid, "scale", { x: 1, y: 1, z: 1 }]);
|
|
||||||
return ok({ success: true, uuid });
|
|
||||||
}
|
|
||||||
|
|
||||||
private async createScene(path?: string, force: boolean = false): Promise<ToolResult> {
|
|
||||||
// ダイアログ割り込み防止: 現在シーンが dirty かつ untitled の場合は事前エラー
|
|
||||||
try { await ensureSceneSafeToSwitch(force); }
|
|
||||||
catch (e: any) { return err(e.message || String(e)); }
|
|
||||||
|
|
||||||
// まず scene:new-scene を試行(path 未指定時のみ)
|
|
||||||
if (!path) {
|
|
||||||
try {
|
|
||||||
await (Editor.Message.request as any)("scene", "new-scene");
|
|
||||||
return ok({ success: true });
|
|
||||||
} catch (e: any) {
|
|
||||||
const msg = e?.message || String(e);
|
|
||||||
if (msg.includes("Message does not exist") || msg.includes("scene - new-scene")) {
|
|
||||||
// CC 3.8.x → asset-db fallback にフォール
|
|
||||||
const fallbackPath = await this.generateAvailableScenePath();
|
|
||||||
return this.createSceneViaAssetDb(fallbackPath);
|
|
||||||
}
|
|
||||||
return err(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// path 指定 → asset-db fallback
|
|
||||||
return this.createSceneViaAssetDb(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async generateAvailableScenePath(): Promise<string> {
|
|
||||||
const basePath = "db://assets/NewScene.scene";
|
|
||||||
try {
|
|
||||||
const result = await (Editor.Message.request as any)("asset-db", "generate-available-url", basePath);
|
|
||||||
if (result) return result;
|
|
||||||
} catch { /* fallback */ }
|
|
||||||
return `db://assets/NewScene_${Date.now()}.scene`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async createSceneViaAssetDb(path: string): Promise<ToolResult> {
|
|
||||||
try {
|
|
||||||
if (!path.endsWith(".scene")) path += ".scene";
|
|
||||||
|
|
||||||
const sceneName = path.split("/").pop()!.replace(".scene", "");
|
|
||||||
const uid = () => crypto.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
||||||
const sid = () => {
|
|
||||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
||||||
let s = "";
|
|
||||||
for (let i = 0; i < 21; i++) s += chars[Math.floor(Math.random() * chars.length)];
|
|
||||||
return s;
|
|
||||||
};
|
|
||||||
|
|
||||||
const sceneJson = this.buildMinimalSceneJson(sceneName, uid, sid);
|
|
||||||
const content = JSON.stringify(sceneJson, null, 2);
|
|
||||||
|
|
||||||
await (Editor.Message.request as any)("asset-db", "create-asset", path, content);
|
|
||||||
|
|
||||||
// シーンを開く
|
|
||||||
try {
|
|
||||||
// ensureSceneSafeToSwitch は createScene 入口で既に通過済みなのでここでは再チェックしない
|
|
||||||
const queryResult = await (Editor.Message.request as any)("asset-db", "query-uuid", path);
|
|
||||||
if (queryResult) {
|
|
||||||
await (Editor.Message.request as any)("scene", "open-scene", queryResult);
|
|
||||||
}
|
|
||||||
} catch { /* open failure is not critical */ }
|
|
||||||
|
|
||||||
return ok({ success: true, path, method: "asset-db-fallback" });
|
|
||||||
} catch (e: any) {
|
|
||||||
return err(e.message || String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private buildMinimalSceneJson(name: string, uid: () => string, sid: () => string): any[] {
|
|
||||||
const sceneId = uid();
|
|
||||||
const canvasNodeId = sid();
|
|
||||||
const cameraNodeId = sid();
|
|
||||||
|
|
||||||
const vec3 = (x: number, y: number, z: number) => ({ __type__: "cc.Vec3", x, y, z });
|
|
||||||
const quat = () => ({ __type__: "cc.Quat", x: 0, y: 0, z: 0, w: 1 });
|
|
||||||
|
|
||||||
return [
|
|
||||||
// [0] SceneAsset
|
|
||||||
{
|
|
||||||
__type__: "cc.SceneAsset",
|
|
||||||
_name: name,
|
|
||||||
_objFlags: 0,
|
|
||||||
__editorExtras__: {},
|
|
||||||
_native: "",
|
|
||||||
scene: { __id__: 1 },
|
|
||||||
},
|
|
||||||
// [1] Scene
|
|
||||||
{
|
|
||||||
__type__: "cc.Scene",
|
|
||||||
_name: name,
|
|
||||||
_objFlags: 0,
|
|
||||||
__editorExtras__: {},
|
|
||||||
_parent: null,
|
|
||||||
_children: [{ __id__: 2 }],
|
|
||||||
_active: true,
|
|
||||||
_components: [],
|
|
||||||
_prefab: null,
|
|
||||||
_lpos: vec3(0, 0, 0),
|
|
||||||
_lrot: quat(),
|
|
||||||
_lscale: vec3(1, 1, 1),
|
|
||||||
_mobility: 0,
|
|
||||||
_layer: 1073741824,
|
|
||||||
_euler: vec3(0, 0, 0),
|
|
||||||
autoReleaseAssets: false,
|
|
||||||
_globals: { __id__: 10 },
|
|
||||||
_id: sceneId,
|
|
||||||
},
|
|
||||||
// [2] Canvas node
|
|
||||||
{
|
|
||||||
__type__: "cc.Node",
|
|
||||||
_name: "Canvas",
|
|
||||||
_objFlags: 0,
|
|
||||||
__editorExtras__: {},
|
|
||||||
_parent: { __id__: 1 },
|
|
||||||
_children: [{ __id__: 3 }],
|
|
||||||
_active: true,
|
|
||||||
_components: [{ __id__: 5 }, { __id__: 6 }, { __id__: 7 }],
|
|
||||||
_prefab: null,
|
|
||||||
_lpos: vec3(0, 0, 0),
|
|
||||||
_lrot: quat(),
|
|
||||||
_lscale: vec3(1, 1, 1),
|
|
||||||
_mobility: 0,
|
|
||||||
_layer: 33554432,
|
|
||||||
_euler: vec3(0, 0, 0),
|
|
||||||
_id: canvasNodeId,
|
|
||||||
},
|
|
||||||
// [3] Camera node
|
|
||||||
{
|
|
||||||
__type__: "cc.Node",
|
|
||||||
_name: "Camera",
|
|
||||||
_objFlags: 0,
|
|
||||||
__editorExtras__: {},
|
|
||||||
_parent: { __id__: 2 },
|
|
||||||
_children: [],
|
|
||||||
_active: true,
|
|
||||||
_components: [{ __id__: 4 }],
|
|
||||||
_prefab: null,
|
|
||||||
_lpos: vec3(0, 0, 1000),
|
|
||||||
_lrot: quat(),
|
|
||||||
_lscale: vec3(1, 1, 1),
|
|
||||||
_mobility: 0,
|
|
||||||
_layer: 1073741824,
|
|
||||||
_euler: vec3(0, 0, 0),
|
|
||||||
_id: cameraNodeId,
|
|
||||||
},
|
|
||||||
// [4] Camera component
|
|
||||||
{
|
|
||||||
__type__: "cc.Camera",
|
|
||||||
_name: "",
|
|
||||||
_objFlags: 0,
|
|
||||||
__editorExtras__: {},
|
|
||||||
node: { __id__: 3 },
|
|
||||||
_enabled: true,
|
|
||||||
_projection: 1,
|
|
||||||
_priority: 0,
|
|
||||||
_fov: 45,
|
|
||||||
_fovAxis: 0,
|
|
||||||
_orthoHeight: 10,
|
|
||||||
_near: 1,
|
|
||||||
_far: 2000,
|
|
||||||
_color: { __type__: "cc.Color", r: 0, g: 0, b: 0, a: 255 },
|
|
||||||
_depth: 1,
|
|
||||||
_stencil: 0,
|
|
||||||
_clearFlags: 6,
|
|
||||||
_rect: { __type__: "cc.Rect", x: 0, y: 0, width: 1, height: 1 },
|
|
||||||
_visibility: 1108344832,
|
|
||||||
_id: "",
|
|
||||||
},
|
|
||||||
// [5] UITransform on Canvas
|
|
||||||
{
|
|
||||||
__type__: "cc.UITransform",
|
|
||||||
_name: "",
|
|
||||||
_objFlags: 0,
|
|
||||||
__editorExtras__: {},
|
|
||||||
node: { __id__: 2 },
|
|
||||||
_enabled: true,
|
|
||||||
_contentSize: { __type__: "cc.Size", width: 720, height: 1280 },
|
|
||||||
_anchorPoint: { __type__: "cc.Vec2", x: 0.5, y: 0.5 },
|
|
||||||
_id: "",
|
|
||||||
},
|
|
||||||
// [6] Canvas component
|
|
||||||
{
|
|
||||||
__type__: "cc.Canvas",
|
|
||||||
_name: "",
|
|
||||||
_objFlags: 0,
|
|
||||||
__editorExtras__: {},
|
|
||||||
node: { __id__: 2 },
|
|
||||||
_enabled: true,
|
|
||||||
_cameraComponent: { __id__: 4 },
|
|
||||||
_alignCanvasWithScreen: true,
|
|
||||||
_id: "",
|
|
||||||
},
|
|
||||||
// [7] Widget on Canvas (fullscreen)
|
|
||||||
{
|
|
||||||
__type__: "cc.Widget",
|
|
||||||
_name: "",
|
|
||||||
_objFlags: 0,
|
|
||||||
__editorExtras__: {},
|
|
||||||
node: { __id__: 2 },
|
|
||||||
_enabled: true,
|
|
||||||
_alignFlags: 15,
|
|
||||||
_target: null,
|
|
||||||
_left: 0,
|
|
||||||
_right: 0,
|
|
||||||
_top: 0,
|
|
||||||
_bottom: 0,
|
|
||||||
_isAbsLeft: true,
|
|
||||||
_isAbsRight: true,
|
|
||||||
_isAbsTop: true,
|
|
||||||
_isAbsBottom: true,
|
|
||||||
_originalWidth: 0,
|
|
||||||
_originalHeight: 0,
|
|
||||||
_id: "",
|
|
||||||
},
|
|
||||||
// [8] cc.PrefabInfo for scene
|
|
||||||
// [9] (reserved)
|
|
||||||
// [10] SceneGlobals
|
|
||||||
{
|
|
||||||
__type__: "cc.SceneGlobals",
|
|
||||||
ambient: { __id__: 11 },
|
|
||||||
shadows: { __id__: 12 },
|
|
||||||
_skybox: { __id__: 13 },
|
|
||||||
fog: { __id__: 14 },
|
|
||||||
},
|
|
||||||
// [11] AmbientInfo
|
|
||||||
{ __type__: "cc.AmbientInfo", _skyLightingColor: { __type__: "cc.Vec4", x: 0.2, y: 0.2, z: 0.2, w: 1 } },
|
|
||||||
// [12] ShadowsInfo
|
|
||||||
{ __type__: "cc.ShadowsInfo" },
|
|
||||||
// [13] SkyboxInfo
|
|
||||||
{ __type__: "cc.SkyboxInfo" },
|
|
||||||
// [14] FogInfo
|
|
||||||
{ __type__: "cc.FogInfo" },
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
private async sceneScript(method: string, args: any[]): Promise<any> {
|
|
||||||
return Editor.Message.request("scene", "execute-scene-script", {
|
|
||||||
name: EXT_NAME,
|
|
||||||
method,
|
|
||||||
args,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-361
@@ -1,361 +0,0 @@
|
|||||||
import { ToolCategory, ToolDefinition, ToolResult } from "../types";
|
|
||||||
import { ok, err } from "../tool-base";
|
|
||||||
|
|
||||||
const EXT_NAME = "cocos-creator-mcp";
|
|
||||||
|
|
||||||
/** シーン名から「untitled (保存先なし)」かどうかを判定 */
|
|
||||||
const UNTITLED_SCENE_NAMES = new Set(["scene-2d", "scene-3d", "Untitled", "NewScene", ""]);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 現在のシーンの dirty 状態を取得。
|
|
||||||
* CC バージョンによって query-dirty / query-is-dirty のどちらか(または両方)が存在するので両方試行。
|
|
||||||
*/
|
|
||||||
export async function queryCurrentSceneDirty(): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
const r = await (Editor.Message.request as any)("scene", "query-dirty");
|
|
||||||
return !!r;
|
|
||||||
} catch { /* try alternate */ }
|
|
||||||
try {
|
|
||||||
const r = await (Editor.Message.request as any)("scene", "query-is-dirty");
|
|
||||||
return !!r;
|
|
||||||
} catch { /* assume clean if both fail */ }
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 現在のシーン情報(名前・UUID)を取得。
|
|
||||||
*/
|
|
||||||
export async function queryCurrentSceneInfo(): Promise<{ sceneName: string; sceneUuid: string }> {
|
|
||||||
try {
|
|
||||||
const result: any = await Editor.Message.request(
|
|
||||||
"scene",
|
|
||||||
"execute-scene-script",
|
|
||||||
{ name: EXT_NAME, method: "getSceneHierarchy", args: [false] }
|
|
||||||
);
|
|
||||||
return { sceneName: result?.sceneName || "", sceneUuid: result?.sceneUuid || "" };
|
|
||||||
} catch {
|
|
||||||
return { sceneName: "", sceneUuid: "" };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 現在のシーンが untitled (scene-2d 等) かを判定。
|
|
||||||
*/
|
|
||||||
export async function isCurrentSceneUntitled(): Promise<boolean> {
|
|
||||||
const { sceneName } = await queryCurrentSceneInfo();
|
|
||||||
return UNTITLED_SCENE_NAMES.has(sceneName);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* save-scene をダイアログ安全に呼び出す。
|
|
||||||
*
|
|
||||||
* - 現在のシーンが untitled (scene-2d 等) → no-op(ダイアログ防止)
|
|
||||||
* - それ以外 → save-scene を実行
|
|
||||||
*
|
|
||||||
* 任意の MCP ツール内部で save-scene を呼ぶ場合は必ずこれ経由にする。
|
|
||||||
* 直接 `Editor.Message.request("scene", "save-scene")` を呼ぶと、
|
|
||||||
* 現在シーンが untitled のときにモーダルダイアログが出て MCP が固まる。
|
|
||||||
*
|
|
||||||
* @returns 実際に保存を実行したか
|
|
||||||
*/
|
|
||||||
export async function safeSaveScene(): Promise<boolean> {
|
|
||||||
if (await isCurrentSceneUntitled()) {
|
|
||||||
console.warn(
|
|
||||||
"[cocos-creator-mcp] safeSaveScene: current scene is untitled, " +
|
|
||||||
"skipping save-scene to avoid modal dialog."
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
await (Editor.Message.request as any)("scene", "save-scene");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* シーン切替系ツール(scene_open/close/new など)の前処理。
|
|
||||||
*
|
|
||||||
* - clean → OK
|
|
||||||
* - dirty + 保存先ありのシーン → save-scene で自動保存
|
|
||||||
* - dirty + untitled シーン → ダイアログを自動応答("Don't Save")して変更を破棄
|
|
||||||
*
|
|
||||||
* 目的: "Save changes?" ダイアログで MCP がブロックされるのを防ぐ。
|
|
||||||
*/
|
|
||||||
export async function ensureSceneSafeToSwitch(force: boolean = false): Promise<void> {
|
|
||||||
const isDirty = await queryCurrentSceneDirty();
|
|
||||||
if (!isDirty) return;
|
|
||||||
|
|
||||||
const { sceneName } = await queryCurrentSceneInfo();
|
|
||||||
const isUntitled = UNTITLED_SCENE_NAMES.has(sceneName);
|
|
||||||
|
|
||||||
if (isUntitled) {
|
|
||||||
// untitled + dirty: ダイアログが出るとMCPがブロックされるので、
|
|
||||||
// Electron dialog をパッチして「保存しない」を自動応答する。
|
|
||||||
// パッチは次回のシーン切替(呼び出し元の scene_open 等)でダイアログが出る瞬間に効く。
|
|
||||||
patchDialogForDiscard();
|
|
||||||
console.warn(
|
|
||||||
`[cocos-creator-mcp] ensureSceneSafeToSwitch: untitled scene "${sceneName}" ` +
|
|
||||||
`is dirty — dialog will auto-respond "Don't Save" to avoid blocking MCP.`
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// ここに来る時点で untitled ではない(上で判定済み)ので直接 save-scene OK
|
|
||||||
await (Editor.Message.request as any)("scene", "save-scene");
|
|
||||||
} catch (e: any) {
|
|
||||||
if (force) {
|
|
||||||
console.warn(
|
|
||||||
`[cocos-creator-mcp] ensureSceneSafeToSwitch: save-scene failed but force=true — proceeding. ` +
|
|
||||||
`Error: ${e.message || e}`
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
throw new Error(
|
|
||||||
`Failed to auto-save dirty scene "${sceneName}" before switch: ${e.message || e}. ` +
|
|
||||||
`Save manually and retry, or pass force=true to bypass.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Electron の dialog.showMessageBox(Sync) を一時的にパッチして、
|
|
||||||
* 次回のダイアログで「保存しない (Don't Save)」を自動選択する。
|
|
||||||
*
|
|
||||||
* CocosCreator の "Save changes?" ダイアログは通常:
|
|
||||||
* buttons: ["Save", "Cancel", "Don't Save"] → index 2 = Don't Save
|
|
||||||
* または:
|
|
||||||
* buttons: ["Save", "Don't Save", "Cancel"] → index 1 = Don't Save
|
|
||||||
*
|
|
||||||
* ボタンテキストから "Don't Save" / "not" / "discard" を探し、
|
|
||||||
* 見つからなければ最後のボタン(通常 Don't Save)を選択する。
|
|
||||||
*/
|
|
||||||
function patchDialogForDiscard(): void {
|
|
||||||
try {
|
|
||||||
const electron = require("electron");
|
|
||||||
const dialog = electron.dialog;
|
|
||||||
const origSync = dialog.showMessageBoxSync;
|
|
||||||
const origAsync = dialog.showMessageBox;
|
|
||||||
|
|
||||||
function findDiscardIndex(buttons: string[]): number {
|
|
||||||
if (!buttons || buttons.length === 0) return 0;
|
|
||||||
const idx = buttons.findIndex((b: string) => {
|
|
||||||
const lower = b.toLowerCase();
|
|
||||||
return lower.includes("don't save") || lower.includes("not")
|
|
||||||
|| lower.includes("discard") || lower.includes("保存しない");
|
|
||||||
});
|
|
||||||
return idx >= 0 ? idx : buttons.length - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sync version
|
|
||||||
dialog.showMessageBoxSync = function (...args: any[]) {
|
|
||||||
dialog.showMessageBoxSync = origSync; // 1回で復元
|
|
||||||
const options = args.length > 1 ? args[1] : args[0];
|
|
||||||
const buttons = options?.buttons || [];
|
|
||||||
const result = findDiscardIndex(buttons);
|
|
||||||
console.warn(`[cocos-creator-mcp] dialog auto-responded: button[${result}]="${buttons[result] || "?"}" (buttons: ${JSON.stringify(buttons)})`);
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Async version
|
|
||||||
dialog.showMessageBox = function (...args: any[]) {
|
|
||||||
dialog.showMessageBox = origAsync; // 1回で復元
|
|
||||||
const options = args.length > 1 ? args[1] : args[0];
|
|
||||||
const buttons = options?.buttons || [];
|
|
||||||
const result = findDiscardIndex(buttons);
|
|
||||||
console.warn(`[cocos-creator-mcp] dialog auto-responded (async): button[${result}]="${buttons[result] || "?"}" (buttons: ${JSON.stringify(buttons)})`);
|
|
||||||
return Promise.resolve({ response: result, checkboxChecked: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
// 安全策: 5秒後に未使用なら復元(次のダイアログが出なかったケース)
|
|
||||||
setTimeout(() => {
|
|
||||||
if (dialog.showMessageBoxSync !== origSync) {
|
|
||||||
dialog.showMessageBoxSync = origSync;
|
|
||||||
}
|
|
||||||
if (dialog.showMessageBox !== origAsync) {
|
|
||||||
dialog.showMessageBox = origAsync;
|
|
||||||
}
|
|
||||||
}, 5000);
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error(`[cocos-creator-mcp] patchDialogForDiscard failed: ${e.message || e}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class SceneTools implements ToolCategory {
|
|
||||||
readonly categoryName = "scene";
|
|
||||||
|
|
||||||
getTools(): ToolDefinition[] {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
name: "scene_get_hierarchy",
|
|
||||||
description: "Get the node hierarchy of the current scene. Returns tree structure with uuid, name, active, and optionally components.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
includeComponents: {
|
|
||||||
type: "boolean",
|
|
||||||
description: "Include component info for each node",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "scene_open",
|
|
||||||
description: "Open a scene by its asset UUID or database path (e.g. 'db://assets/scenes/Main.scene'). If the current scene is dirty and untitled, returns an error instead of triggering a modal save dialog that would block MCP. Pass force=true to bypass this guard.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
scene: {
|
|
||||||
type: "string",
|
|
||||||
description: "Scene UUID or db:// path",
|
|
||||||
},
|
|
||||||
force: {
|
|
||||||
type: "boolean",
|
|
||||||
description: "Skip dirty-scene preflight check (may trigger modal save dialog)",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ["scene"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "scene_save",
|
|
||||||
description: "Save the currently open scene.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "scene_get_list",
|
|
||||||
description: "List all scene files in the project.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "scene_close",
|
|
||||||
description: "Close the current scene. If the current scene is dirty and untitled, returns an error instead of triggering a modal save dialog. Pass force=true to bypass.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
force: {
|
|
||||||
type: "boolean",
|
|
||||||
description: "Skip dirty-scene preflight check (may trigger modal save dialog)",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "scene_get_current",
|
|
||||||
description: "Get the name and UUID of the currently open scene.",
|
|
||||||
inputSchema: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
async execute(toolName: string, args: Record<string, any>): Promise<ToolResult> {
|
|
||||||
switch (toolName) {
|
|
||||||
case "scene_get_hierarchy":
|
|
||||||
return this.getHierarchy(args.includeComponents ?? false);
|
|
||||||
case "scene_open":
|
|
||||||
return this.openScene(args.scene, !!args.force);
|
|
||||||
case "scene_save":
|
|
||||||
return this.saveScene();
|
|
||||||
case "scene_get_list":
|
|
||||||
return this.getSceneList();
|
|
||||||
case "scene_close":
|
|
||||||
try {
|
|
||||||
await ensureSceneSafeToSwitch(!!args.force);
|
|
||||||
await (Editor.Message.request as any)("scene", "close-scene");
|
|
||||||
return ok({ success: true });
|
|
||||||
} catch (e: any) { return err(e.message || String(e)); }
|
|
||||||
case "scene_get_current":
|
|
||||||
return this.getHierarchy(false).then((r) => {
|
|
||||||
const parsed = JSON.parse(r.content[0].text);
|
|
||||||
return ok({ success: true, sceneName: parsed.sceneName, sceneUuid: parsed.sceneUuid });
|
|
||||||
}).catch((e) => err(String(e)));
|
|
||||||
default:
|
|
||||||
return err(`Unknown tool: ${toolName}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getHierarchy(includeComponents: boolean): Promise<ToolResult> {
|
|
||||||
try {
|
|
||||||
const result = await Editor.Message.request(
|
|
||||||
"scene",
|
|
||||||
"execute-scene-script",
|
|
||||||
{
|
|
||||||
name: EXT_NAME,
|
|
||||||
method: "getSceneHierarchy",
|
|
||||||
args: [includeComponents],
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return ok(result);
|
|
||||||
} catch (e: any) {
|
|
||||||
// Fallback: use query-node-tree
|
|
||||||
try {
|
|
||||||
const tree = await Editor.Message.request("scene", "query-node-tree");
|
|
||||||
return ok({ success: true, hierarchy: tree });
|
|
||||||
} catch (e2: any) {
|
|
||||||
return err(e2.message || String(e2));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async openScene(scene: string, force: boolean): Promise<ToolResult> {
|
|
||||||
try {
|
|
||||||
await ensureSceneSafeToSwitch(force);
|
|
||||||
await Editor.Message.request("asset-db", "open-asset", scene);
|
|
||||||
return ok({ success: true, scene });
|
|
||||||
} catch (e: any) {
|
|
||||||
return err(e.message || String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async saveScene(): Promise<ToolResult> {
|
|
||||||
try {
|
|
||||||
// 現在のシーンが保存済みファイルか確認(新規シーンの場合はダイアログが出るのでスキップ)
|
|
||||||
const scenes = await Editor.Message.request("asset-db", "query-assets", {
|
|
||||||
pattern: "db://assets/**/*.scene",
|
|
||||||
}).catch(() => []);
|
|
||||||
|
|
||||||
// シーン名を取得して既存シーンか判定
|
|
||||||
const hierarchy = await Editor.Message.request(
|
|
||||||
"scene", "execute-scene-script",
|
|
||||||
{ name: "cocos-creator-mcp", method: "getSceneHierarchy", args: [false] }
|
|
||||||
).catch(() => null);
|
|
||||||
|
|
||||||
const sceneName = hierarchy?.sceneName;
|
|
||||||
const isNewScene = !sceneName || sceneName === "scene-2d" || sceneName === "Untitled";
|
|
||||||
if (isNewScene) {
|
|
||||||
return ok({ success: true, note: "New/untitled scene, skip save to avoid dialog" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// シーンがdirtyでない場合は保存不要
|
|
||||||
const isDirty = await (Editor.Message.request as any)("scene", "query-is-dirty").catch(() => true);
|
|
||||||
if (!isDirty) {
|
|
||||||
return ok({ success: true, note: "Scene not dirty, skip save" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await (Editor.Message.request as any)("scene", "save-scene", false);
|
|
||||||
return ok({ success: true, result });
|
|
||||||
} catch (e: any) {
|
|
||||||
return err(e.message || String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getSceneList(): Promise<ToolResult> {
|
|
||||||
try {
|
|
||||||
const results = await Editor.Message.request("asset-db", "query-assets", {
|
|
||||||
pattern: "db://assets/**/*.scene",
|
|
||||||
});
|
|
||||||
const scenes = (results || []).map((a: any) => ({
|
|
||||||
uuid: a.uuid,
|
|
||||||
path: a.path || a.url,
|
|
||||||
name: a.name,
|
|
||||||
}));
|
|
||||||
return ok({ success: true, scenes });
|
|
||||||
} catch (e: any) {
|
|
||||||
return err(e.message || String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-232
@@ -1,232 +0,0 @@
|
|||||||
import { ToolCategory, ToolDefinition, ToolResult } from "../types";
|
|
||||||
import { ok, err } from "../tool-base";
|
|
||||||
|
|
||||||
export class SceneViewTools implements ToolCategory {
|
|
||||||
readonly categoryName = "sceneView";
|
|
||||||
|
|
||||||
getTools(): ToolDefinition[] {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
name: "view_change_gizmo_tool",
|
|
||||||
description: "Change the gizmo tool (move, rotate, scale, rect).",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
tool: { type: "string", enum: ["move", "rotate", "scale", "rect"], description: "Gizmo tool name" },
|
|
||||||
},
|
|
||||||
required: ["tool"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "view_query_gizmo_tool",
|
|
||||||
description: "Get the currently active gizmo tool.",
|
|
||||||
inputSchema: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "view_change_gizmo_pivot",
|
|
||||||
description: "Change gizmo pivot mode (center or pivot).",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
pivot: { type: "string", enum: ["center", "pivot"], description: "Pivot mode" },
|
|
||||||
},
|
|
||||||
required: ["pivot"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "view_query_gizmo_pivot",
|
|
||||||
description: "Get the current gizmo pivot mode.",
|
|
||||||
inputSchema: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "view_change_gizmo_coordinate",
|
|
||||||
description: "Change gizmo coordinate system (local or global).",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
coordinate: { type: "string", enum: ["local", "global"], description: "Coordinate system" },
|
|
||||||
},
|
|
||||||
required: ["coordinate"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "view_query_gizmo_coordinate",
|
|
||||||
description: "Get the current gizmo coordinate system.",
|
|
||||||
inputSchema: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "view_change_mode_2d_3d",
|
|
||||||
description: "Switch between 2D and 3D view mode.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
mode: { type: "string", enum: ["2d", "3d"], description: "View mode" },
|
|
||||||
},
|
|
||||||
required: ["mode"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "view_query_mode_2d_3d",
|
|
||||||
description: "Get the current 2D/3D view mode.",
|
|
||||||
inputSchema: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "view_set_grid_visible",
|
|
||||||
description: "Show or hide the scene grid.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
visible: { type: "boolean", description: "Whether to show the grid" },
|
|
||||||
},
|
|
||||||
required: ["visible"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "view_query_grid_visible",
|
|
||||||
description: "Check if the scene grid is visible.",
|
|
||||||
inputSchema: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "view_focus_on_node",
|
|
||||||
description: "Focus the scene camera on specific node(s).",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
uuids: { type: "array", items: { type: "string" }, description: "Node UUIDs to focus on" },
|
|
||||||
},
|
|
||||||
required: ["uuids"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "view_get_status",
|
|
||||||
description: "Get the current scene view status (gizmo tool, pivot, coordinate, grid, etc.).",
|
|
||||||
inputSchema: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "view_set_icon_gizmo_3d",
|
|
||||||
description: "Toggle 3D icon gizmos on/off.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: { enabled: { type: "boolean" } },
|
|
||||||
required: ["enabled"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "view_query_icon_gizmo_3d",
|
|
||||||
description: "Check if 3D icon gizmos are enabled.",
|
|
||||||
inputSchema: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "view_set_icon_gizmo_size",
|
|
||||||
description: "Set the icon gizmo size.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: { size: { type: "number" } },
|
|
||||||
required: ["size"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "view_query_icon_gizmo_size",
|
|
||||||
description: "Get the current icon gizmo size.",
|
|
||||||
inputSchema: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "view_align_with_view",
|
|
||||||
description: "Align the selected node with the current camera view.",
|
|
||||||
inputSchema: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "view_align_view_with_node",
|
|
||||||
description: "Align the camera view with the selected node.",
|
|
||||||
inputSchema: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "view_reset",
|
|
||||||
description: "Reset the scene view to default state.",
|
|
||||||
inputSchema: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
async execute(toolName: string, args: Record<string, any>): Promise<ToolResult> {
|
|
||||||
try {
|
|
||||||
switch (toolName) {
|
|
||||||
case "view_change_gizmo_tool":
|
|
||||||
await (Editor.Message.request as any)("scene", "change-gizmo-tool", args.tool);
|
|
||||||
return ok({ success: true, tool: args.tool });
|
|
||||||
case "view_query_gizmo_tool": {
|
|
||||||
const tool = await (Editor.Message.request as any)("scene", "query-gizmo-tool-name");
|
|
||||||
return ok({ success: true, tool });
|
|
||||||
}
|
|
||||||
case "view_change_gizmo_pivot":
|
|
||||||
await (Editor.Message.request as any)("scene", "change-gizmo-pivot", args.pivot);
|
|
||||||
return ok({ success: true, pivot: args.pivot });
|
|
||||||
case "view_query_gizmo_pivot": {
|
|
||||||
const pivot = await (Editor.Message.request as any)("scene", "query-gizmo-pivot");
|
|
||||||
return ok({ success: true, pivot });
|
|
||||||
}
|
|
||||||
case "view_change_gizmo_coordinate":
|
|
||||||
await (Editor.Message.request as any)("scene", "change-gizmo-coordinate", args.coordinate);
|
|
||||||
return ok({ success: true, coordinate: args.coordinate });
|
|
||||||
case "view_query_gizmo_coordinate": {
|
|
||||||
const coord = await (Editor.Message.request as any)("scene", "query-gizmo-coordinate");
|
|
||||||
return ok({ success: true, coordinate: coord });
|
|
||||||
}
|
|
||||||
case "view_change_mode_2d_3d":
|
|
||||||
await (Editor.Message.request as any)("scene", "change-view-mode-2d-3d", args.mode);
|
|
||||||
return ok({ success: true, mode: args.mode });
|
|
||||||
case "view_query_mode_2d_3d": {
|
|
||||||
const mode = await (Editor.Message.request as any)("scene", "query-view-mode-2d-3d");
|
|
||||||
return ok({ success: true, mode });
|
|
||||||
}
|
|
||||||
case "view_set_grid_visible":
|
|
||||||
await (Editor.Message.request as any)("scene", "set-grid-visible", args.visible);
|
|
||||||
return ok({ success: true, visible: args.visible });
|
|
||||||
case "view_query_grid_visible": {
|
|
||||||
const visible = await (Editor.Message.request as any)("scene", "query-grid-visible");
|
|
||||||
return ok({ success: true, visible });
|
|
||||||
}
|
|
||||||
case "view_focus_on_node":
|
|
||||||
await (Editor.Message.request as any)("scene", "focus-camera-on-nodes", args.uuids);
|
|
||||||
return ok({ success: true, uuids: args.uuids });
|
|
||||||
case "view_get_status": {
|
|
||||||
const [tool, pivot, coord, mode, grid] = await Promise.all([
|
|
||||||
(Editor.Message.request as any)("scene", "query-gizmo-tool-name").catch(() => null),
|
|
||||||
(Editor.Message.request as any)("scene", "query-gizmo-pivot").catch(() => null),
|
|
||||||
(Editor.Message.request as any)("scene", "query-gizmo-coordinate").catch(() => null),
|
|
||||||
(Editor.Message.request as any)("scene", "query-view-mode-2d-3d").catch(() => null),
|
|
||||||
(Editor.Message.request as any)("scene", "query-grid-visible").catch(() => null),
|
|
||||||
]);
|
|
||||||
return ok({ success: true, tool, pivot, coordinate: coord, mode, gridVisible: grid });
|
|
||||||
}
|
|
||||||
case "view_set_icon_gizmo_3d":
|
|
||||||
await (Editor.Message.request as any)("scene", "set-icon-gizmo-3d", args.enabled);
|
|
||||||
return ok({ success: true, enabled: args.enabled });
|
|
||||||
case "view_query_icon_gizmo_3d": {
|
|
||||||
const enabled = await (Editor.Message.request as any)("scene", "query-is-icon-gizmo-3d");
|
|
||||||
return ok({ success: true, enabled });
|
|
||||||
}
|
|
||||||
case "view_set_icon_gizmo_size":
|
|
||||||
await (Editor.Message.request as any)("scene", "set-icon-gizmo-size", args.size);
|
|
||||||
return ok({ success: true, size: args.size });
|
|
||||||
case "view_query_icon_gizmo_size": {
|
|
||||||
const size = await (Editor.Message.request as any)("scene", "query-icon-gizmo-size");
|
|
||||||
return ok({ success: true, size });
|
|
||||||
}
|
|
||||||
case "view_align_with_view":
|
|
||||||
await (Editor.Message.request as any)("scene", "align-with-view");
|
|
||||||
return ok({ success: true });
|
|
||||||
case "view_align_view_with_node":
|
|
||||||
await (Editor.Message.request as any)("scene", "align-view-with-node");
|
|
||||||
return ok({ success: true });
|
|
||||||
case "view_reset":
|
|
||||||
await (Editor.Message.request as any)("scene", "reset-scene-view");
|
|
||||||
return ok({ success: true });
|
|
||||||
default:
|
|
||||||
return err(`Unknown tool: ${toolName}`);
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
return err(e.message || String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-130
@@ -1,130 +0,0 @@
|
|||||||
import { ToolCategory, ToolDefinition, ToolResult } from "../types";
|
|
||||||
import { ok, err } from "../tool-base";
|
|
||||||
import { BUILD_HASH } from "../mcp-server";
|
|
||||||
|
|
||||||
export class ServerTools implements ToolCategory {
|
|
||||||
readonly categoryName = "server";
|
|
||||||
|
|
||||||
getTools(): ToolDefinition[] {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
name: "server_query_ip_list",
|
|
||||||
description: "Get the list of IP addresses the editor server is listening on.",
|
|
||||||
inputSchema: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "server_query_port",
|
|
||||||
description: "Get the port number of the editor's built-in server.",
|
|
||||||
inputSchema: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "server_get_status",
|
|
||||||
description: "Get the editor server status (IP, port, connectivity).",
|
|
||||||
inputSchema: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "server_get_build_hash",
|
|
||||||
description: "Get the build hash of the MCP server. The hash is derived from the code content, so identical code always produces the same hash.",
|
|
||||||
inputSchema: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "server_check_connectivity",
|
|
||||||
description: "Check if the editor server is reachable.",
|
|
||||||
inputSchema: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "server_get_network_interfaces",
|
|
||||||
description: "Get detailed network interface information.",
|
|
||||||
inputSchema: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "server_check_code_sync",
|
|
||||||
description: "Check if the running MCP extension code matches the latest build. Compares the runtime BUILD_HASH with the hash of dist/ files. Returns whether extension reload or CC restart is needed.",
|
|
||||||
inputSchema: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
async execute(toolName: string, _args: Record<string, any>): Promise<ToolResult> {
|
|
||||||
try {
|
|
||||||
switch (toolName) {
|
|
||||||
case "server_query_ip_list": {
|
|
||||||
const ips = await (Editor.Message.request as any)("server", "query-ip-list");
|
|
||||||
return ok({ success: true, ips });
|
|
||||||
}
|
|
||||||
case "server_query_port": {
|
|
||||||
const port = await (Editor.Message.request as any)("server", "query-port");
|
|
||||||
return ok({ success: true, port });
|
|
||||||
}
|
|
||||||
case "server_get_status": {
|
|
||||||
const [ips, port] = await Promise.all([
|
|
||||||
(Editor.Message.request as any)("server", "query-ip-list").catch(() => []),
|
|
||||||
(Editor.Message.request as any)("server", "query-port").catch(() => null),
|
|
||||||
]);
|
|
||||||
return ok({ success: true, ips, port, buildId: BUILD_HASH });
|
|
||||||
}
|
|
||||||
case "server_get_build_hash": {
|
|
||||||
return ok({ success: true, buildHash: BUILD_HASH });
|
|
||||||
}
|
|
||||||
case "server_check_connectivity": {
|
|
||||||
try {
|
|
||||||
const port = await (Editor.Message.request as any)("server", "query-port");
|
|
||||||
return ok({ success: true, reachable: true, port });
|
|
||||||
} catch {
|
|
||||||
return ok({ success: true, reachable: false });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case "server_get_network_interfaces": {
|
|
||||||
const os = require("os");
|
|
||||||
const interfaces = os.networkInterfaces();
|
|
||||||
return ok({ success: true, interfaces });
|
|
||||||
}
|
|
||||||
case "server_check_code_sync": {
|
|
||||||
try {
|
|
||||||
const fs = require("fs");
|
|
||||||
const path = require("path");
|
|
||||||
const crypto = require("crypto");
|
|
||||||
const extDir = path.join(Editor.Project.path, "extensions", "cocos-creator-mcp", "dist");
|
|
||||||
if (!fs.existsSync(extDir)) {
|
|
||||||
return ok({ success: true, synced: false, note: "Extension dist/ not found", runtimeHash: BUILD_HASH });
|
|
||||||
}
|
|
||||||
const hash = crypto.createHash("sha256");
|
|
||||||
const collectJs = (dir: string, prefix = ""): string[] => {
|
|
||||||
let files: string[] = [];
|
|
||||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
||||||
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
||||||
if (entry.isDirectory()) files = files.concat(collectJs(path.join(dir, entry.name), rel));
|
|
||||||
else if (entry.name.endsWith(".js")) files.push(rel);
|
|
||||||
}
|
|
||||||
return files;
|
|
||||||
};
|
|
||||||
for (const file of collectJs(extDir).sort()) {
|
|
||||||
let content = fs.readFileSync(path.join(extDir, file), "utf8");
|
|
||||||
// mcp-server.js の baked hash を postbuild 前と同じプレースホルダに逆置換
|
|
||||||
// (postbuild は __BUILD_HASH__ が残った状態でハッシュ計算しているため、同じ入力にする)
|
|
||||||
if (file === "mcp-server.js") {
|
|
||||||
content = content.replace(/exports\.BUILD_HASH = "[a-f0-9]{12}"/, 'exports.BUILD_HASH = "__BUILD_HASH__"');
|
|
||||||
}
|
|
||||||
hash.update(content.replace(/__BUILD_HASH__/g, ""));
|
|
||||||
}
|
|
||||||
const diskHash = hash.digest("hex").substring(0, 12);
|
|
||||||
const synced = diskHash === BUILD_HASH;
|
|
||||||
return ok({
|
|
||||||
success: true,
|
|
||||||
synced,
|
|
||||||
runtimeHash: BUILD_HASH,
|
|
||||||
diskHash,
|
|
||||||
action: synced ? "none" : "Extension reload or CC restart needed",
|
|
||||||
});
|
|
||||||
} catch (e: any) {
|
|
||||||
return ok({ success: true, synced: false, error: e.message, runtimeHash: BUILD_HASH });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return err(`Unknown tool: ${toolName}`);
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
return err(e.message || String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
/** MCP Tool definition (JSON Schema based) */
|
|
||||||
export interface ToolDefinition {
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
inputSchema: {
|
|
||||||
type: "object";
|
|
||||||
properties: Record<string, any>;
|
|
||||||
required?: string[];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Result returned from tool execution */
|
|
||||||
export interface ToolResult {
|
|
||||||
content: Array<{ type: "text"; text: string }>;
|
|
||||||
isError?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Interface that all tool categories must implement */
|
|
||||||
export interface ToolCategory {
|
|
||||||
readonly categoryName: string;
|
|
||||||
getTools(): ToolDefinition[];
|
|
||||||
execute(toolName: string, args: Record<string, any>): Promise<ToolResult>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** JSON-RPC 2.0 request */
|
|
||||||
export interface JsonRpcRequest {
|
|
||||||
jsonrpc: "2.0";
|
|
||||||
id?: string | number;
|
|
||||||
method: string;
|
|
||||||
params?: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** JSON-RPC 2.0 response */
|
|
||||||
export interface JsonRpcResponse {
|
|
||||||
jsonrpc: "2.0";
|
|
||||||
id?: string | number | null;
|
|
||||||
result?: any;
|
|
||||||
error?: {
|
|
||||||
code: number;
|
|
||||||
message: string;
|
|
||||||
data?: any;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Server configuration */
|
|
||||||
export interface ServerConfig {
|
|
||||||
port: number;
|
|
||||||
autoStart: boolean;
|
|
||||||
autoArchiveRecordings: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DEFAULT_CONFIG: ServerConfig = {
|
|
||||||
port: 3000,
|
|
||||||
autoStart: false,
|
|
||||||
autoArchiveRecordings: false,
|
|
||||||
};
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
/**
|
|
||||||
* MCP クライアントが JSON オブジェクトを文字列化して送信する問題への共通対策.
|
|
||||||
* スキーマに type が未宣言のオブジェクト引数は文字列で届く場合がある.
|
|
||||||
*/
|
|
||||||
export function parseMaybeJson<T = any>(value: any): T {
|
|
||||||
if (typeof value === "string") {
|
|
||||||
try { return JSON.parse(value); } catch { /* string のまま */ }
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
/**
|
|
||||||
* Prefab保存テスト — プロパティがPrefabに保持されるか検証
|
|
||||||
*
|
|
||||||
* 前提: CocosCreatorでcocos-creator-mcpサーバーが起動中、MainScene開いてる
|
|
||||||
*/
|
|
||||||
|
|
||||||
const PORT = process.argv[2] || 3001;
|
|
||||||
const BASE = `http://127.0.0.1:${PORT}`;
|
|
||||||
let rpcId = 0;
|
|
||||||
|
|
||||||
async function rpc(method, params = {}) {
|
|
||||||
const res = await fetch(`${BASE}/mcp`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ jsonrpc: "2.0", id: ++rpcId, method, params }),
|
|
||||||
});
|
|
||||||
return res.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function callTool(name, args = {}) {
|
|
||||||
const res = await rpc("tools/call", { name, arguments: args });
|
|
||||||
if (res.error) return { _rpcError: res.error };
|
|
||||||
const text = res.result?.content?.[0]?.text;
|
|
||||||
return text ? JSON.parse(text) : res.result;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function sceneScript(method, args) {
|
|
||||||
// Call execute-scene-script through a raw JSON-RPC (not via tool)
|
|
||||||
// Actually we need to go through the MCP server... let's use node_set_property hack
|
|
||||||
// Or better: add a scene_script tool call
|
|
||||||
// For now, use the existing setComponentProperty that IS registered in scene.ts
|
|
||||||
|
|
||||||
// We can't call execute-scene-script directly from outside.
|
|
||||||
// But we CAN call the node_set_property tool which internally calls setNodeProperty scene script.
|
|
||||||
// For component property, we need a dedicated tool.
|
|
||||||
|
|
||||||
// Workaround: Use curl to call the Editor's Messages API if available
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
console.log("\n🧪 Prefab Property Persistence Test\n");
|
|
||||||
|
|
||||||
// Step 1: Create test node with Label
|
|
||||||
console.log("1. Creating test node with Label...");
|
|
||||||
const created = await callTool("node_create", {
|
|
||||||
name: "PrefabPropertyTest",
|
|
||||||
parent: "83A39G7PtBqLLEoVjvUo4h", // Canvas
|
|
||||||
components: ["cc.Label"],
|
|
||||||
});
|
|
||||||
console.log(` Node UUID: ${created.uuid}`);
|
|
||||||
const nodeUuid = created.uuid;
|
|
||||||
|
|
||||||
// Step 2: Set position (this should persist)
|
|
||||||
console.log("2. Setting position to (100, 200, 0)...");
|
|
||||||
await callTool("node_set_transform", {
|
|
||||||
uuid: nodeUuid,
|
|
||||||
position: { x: 100, y: 200, z: 0 },
|
|
||||||
});
|
|
||||||
|
|
||||||
// Step 3: Verify position was set
|
|
||||||
const info1 = await callTool("node_get_info", { uuid: nodeUuid });
|
|
||||||
console.log(` Position: (${info1.data?.position?.x}, ${info1.data?.position?.y})`);
|
|
||||||
console.log(` Components: ${info1.data?.components?.map(c => c.type).join(", ")}`);
|
|
||||||
|
|
||||||
// Step 4: Try to create prefab via Editor API
|
|
||||||
console.log("3. Attempting to create prefab via Editor.Message...");
|
|
||||||
|
|
||||||
// We'll try the create-prefab scene message
|
|
||||||
// This requires calling: Editor.Message.request("scene", "create-prefab", nodeUuid, url)
|
|
||||||
// Since we don't have a tool for this, let's try different approaches:
|
|
||||||
|
|
||||||
// Approach A: Try "create-prefab" message (3 args: nodeUuid, url, ?)
|
|
||||||
const prefabUrl = "db://assets/test/PrefabPropertyTest.prefab";
|
|
||||||
|
|
||||||
// We need to add this capability. For now, let's check if we can use
|
|
||||||
// asset-db to create an asset
|
|
||||||
console.log(` Target: ${prefabUrl}`);
|
|
||||||
|
|
||||||
// Actually, let me just check the scene script createPrefabFromNode
|
|
||||||
// which IS registered in package.json scene methods
|
|
||||||
// But wait - our own MCP's scene.ts doesn't have createPrefabFromNode
|
|
||||||
|
|
||||||
console.log("\n⚠️ Prefab creation tool not yet implemented in our MCP.");
|
|
||||||
console.log(" This is what needs to be built for v0.5.\n");
|
|
||||||
|
|
||||||
// Step 5: Test what the Editor API returns for prefab-related messages
|
|
||||||
console.log("4. Probing available prefab-related Editor Messages...");
|
|
||||||
|
|
||||||
const messages = [
|
|
||||||
["scene", "create-prefab"],
|
|
||||||
["scene", "save-prefab"],
|
|
||||||
["asset-db", "create-asset"],
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const [target, msg] of messages) {
|
|
||||||
try {
|
|
||||||
const res = await fetch(`${BASE}/mcp`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
jsonrpc: "2.0", id: ++rpcId, method: "tools/call",
|
|
||||||
params: { name: "node_get_info", arguments: { uuid: "test" } }
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
console.log(` ${target}:${msg} — needs testing`);
|
|
||||||
} catch (e) {
|
|
||||||
console.log(` ${target}:${msg} — error: ${e.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
console.log("\n5. Cleaning up test node...");
|
|
||||||
await callTool("node_delete", { uuid: nodeUuid });
|
|
||||||
console.log(" Deleted.\n");
|
|
||||||
|
|
||||||
console.log("═".repeat(50));
|
|
||||||
console.log(" Summary:");
|
|
||||||
console.log(" - Node creation with components: ✅");
|
|
||||||
console.log(" - Position/transform setting: ✅");
|
|
||||||
console.log(" - Component property setting: ❓ (needs component tool)");
|
|
||||||
console.log(" - Prefab creation: ❓ (needs prefab tool)");
|
|
||||||
console.log(" - Property persistence in prefab: ❓ (blocked by above)");
|
|
||||||
console.log("═".repeat(50));
|
|
||||||
console.log("\n→ v0.5 で component + prefab ツール実装後に再検証");
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch(e => { console.error("Fatal:", e); process.exit(1); });
|
|
||||||
File diff suppressed because it is too large
Load Diff
-128
@@ -1,128 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
/**
|
|
||||||
* 独立测试服务器 - 模拟Cocos Creator MCP服务器的最小化实现
|
|
||||||
* 用于测试stdio-bridge.js的基本功能
|
|
||||||
*/
|
|
||||||
|
|
||||||
const http = require('http');
|
|
||||||
|
|
||||||
const PORT = 3000;
|
|
||||||
const MCP_PROTOCOL_VERSION = "2024-11-05";
|
|
||||||
|
|
||||||
// 模拟的工具列表(从README中提取的164个工具中的几个示例)
|
|
||||||
const MOCK_TOOLS = [
|
|
||||||
{
|
|
||||||
name: "scene_get_hierarchy",
|
|
||||||
description: "Get the node tree (with optional component info)",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
includeComponents: { type: "boolean", description: "Include component info in hierarchy" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "node_create",
|
|
||||||
description: "Create node (with optional components)",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
name: { type: "string", description: "Node name" },
|
|
||||||
parentUuid: { type: "string", description: "Parent node UUID" }
|
|
||||||
},
|
|
||||||
required: ["name"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "scene_save",
|
|
||||||
description: "Save the current scene",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const server = http.createServer((req, res) => {
|
|
||||||
if (req.method === 'POST' && req.url === '/mcp') {
|
|
||||||
let body = '';
|
|
||||||
req.on('data', chunk => body += chunk);
|
|
||||||
req.on('end', () => {
|
|
||||||
try {
|
|
||||||
const request = JSON.parse(body);
|
|
||||||
console.log('[TEST SERVER] Received:', request.method);
|
|
||||||
|
|
||||||
const response = {
|
|
||||||
jsonrpc: "2.0",
|
|
||||||
id: request.id
|
|
||||||
};
|
|
||||||
|
|
||||||
switch (request.method) {
|
|
||||||
case 'initialize':
|
|
||||||
response.result = {
|
|
||||||
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
||||||
capabilities: {
|
|
||||||
tools: {}
|
|
||||||
},
|
|
||||||
serverInfo: {
|
|
||||||
name: "cocos-creator-mcp",
|
|
||||||
version: "1.14.0"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'tools/list':
|
|
||||||
response.result = {
|
|
||||||
tools: MOCK_TOOLS
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'tools/call':
|
|
||||||
const toolName = request.params.name;
|
|
||||||
response.result = {
|
|
||||||
content: [{
|
|
||||||
type: "text",
|
|
||||||
text: JSON.stringify({
|
|
||||||
success: true,
|
|
||||||
tool: toolName,
|
|
||||||
message: `Called ${toolName} successfully`
|
|
||||||
})
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
response.error = {
|
|
||||||
code: -32601,
|
|
||||||
message: "Method not found"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
res.setHeader('Content-Type', 'application/json');
|
|
||||||
res.end(JSON.stringify(response));
|
|
||||||
} catch (e) {
|
|
||||||
res.statusCode = 400;
|
|
||||||
res.end(JSON.stringify({ error: e.message }));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (req.method === 'GET' && req.url === '/health') {
|
|
||||||
res.setHeader('Content-Type', 'application/json');
|
|
||||||
res.end(JSON.stringify({ status: 'ok', tools: MOCK_TOOLS.length }));
|
|
||||||
} else {
|
|
||||||
res.statusCode = 404;
|
|
||||||
res.end('Not found');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
server.listen(PORT, '127.0.0.1', () => {
|
|
||||||
console.log(`[TEST SERVER] Started on http://127.0.0.1:${PORT}/mcp`);
|
|
||||||
console.log(`[TEST SERVER] Health check: http://127.0.0.1:${PORT}/health`);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 优雅关闭
|
|
||||||
process.on('SIGINT', () => {
|
|
||||||
console.log('[TEST SERVER] Shutting down...');
|
|
||||||
server.close(() => {
|
|
||||||
process.exit(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "ES2017",
|
|
||||||
"module": "CommonJS",
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"inlineSourceMap": true,
|
|
||||||
"inlineSources": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"strict": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"outDir": "./dist",
|
|
||||||
"rootDir": "./source",
|
|
||||||
"types": [
|
|
||||||
"node",
|
|
||||||
"@cocos/creator-types/editor"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"exclude": ["client", "test", "dist"]
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# Cocos Creator MCP Server v1.7.9
|
||||||
|
|
||||||
|
## 安装说明
|
||||||
|
|
||||||
|
1. 将整个文件夹复制到 Cocos Creator 项目的 `extensions` 目录
|
||||||
|
2. 重启 Cocos Creator 或刷新扩展 (Ctrl+R / Cmd+R)
|
||||||
|
3. 在扩展菜单中启用 "Cocos MCP Server"
|
||||||
|
4. 打开扩展面板配置服务器设置
|
||||||
|
|
||||||
|
## 使用配置
|
||||||
|
|
||||||
|
### Claude Desktop
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"cocos-creator": {
|
||||||
|
"type": "http",
|
||||||
|
"url": "http://127.0.0.1:3000/mcp"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cursor
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"cocos-creator": {
|
||||||
|
"url": "http://127.0.0.1:3000/mcp"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 技术支持
|
||||||
|
|
||||||
|
- 📧 邮箱: a549623165@gmail.com
|
||||||
|
- 💬 微信: lidaxian-AI | 客服微信: vberai-support
|
||||||
|
- 🐙 GitHub: https://github.com/DaxianLee
|
||||||
|
- 🛒 购买/续费: https://www.vberai.com/game-engines/cocos
|
||||||
|
|
||||||
|
---
|
||||||
|
构建信息: v1.7.9 (production) - 2026-07-18 16:06:14
|
||||||
|
作者: 李大仙 (LiDaxian)
|
||||||
@@ -0,0 +1,243 @@
|
|||||||
|
"use strict";
|
||||||
|
module.exports = {
|
||||||
|
"extension_name": "خادم Cocos MCP",
|
||||||
|
"description": "خادم AI MCP لـ Cocos Creator 3.8",
|
||||||
|
"panel_title": "خادم MCP",
|
||||||
|
"open_panel": "فتح لوحة MCP",
|
||||||
|
"start_server": "بدء الخادم",
|
||||||
|
"stop_server": "إيقاف الخادم",
|
||||||
|
"server_status": "حالة الخادم",
|
||||||
|
"port": "المنفذ",
|
||||||
|
"settings": "الإعدادات",
|
||||||
|
"connected": "متصل",
|
||||||
|
"disconnected": "غير متصل",
|
||||||
|
"server_running": "الخادم يعمل على المنفذ {0}",
|
||||||
|
"server_stopped": "تم إيقاف الخادم",
|
||||||
|
"auto_start": "بدء تلقائي",
|
||||||
|
"debug_log": "سجل التصحيح",
|
||||||
|
"max_connections": "الحد الأقصى للاتصالات",
|
||||||
|
"connection_info": "معلومات الاتصال",
|
||||||
|
"http_url": "رابط HTTP",
|
||||||
|
"copy": "نسخ",
|
||||||
|
"save_settings": "حفظ",
|
||||||
|
"settings_saved": "تم حفظ الإعدادات بنجاح",
|
||||||
|
"server_started": "تم بدء خادم MCP",
|
||||||
|
"server_stopped_msg": "تم إيقاف خادم MCP",
|
||||||
|
"failed_to_start": "فشل في بدء الخادم",
|
||||||
|
"failed_to_stop": "فشل في إيقاف الخادم",
|
||||||
|
"failed_to_save": "فشل في حفظ الإعدادات",
|
||||||
|
"url_copied": "تم نسخ رابط HTTP إلى الحافظة",
|
||||||
|
"tool_manager": "مدير الأدوات",
|
||||||
|
"open_tool_manager": "فتح مدير الأدوات",
|
||||||
|
"create_config": "إنشاء تكوين",
|
||||||
|
"edit_config": "تعديل التكوين",
|
||||||
|
"delete_config": "حذف التكوين",
|
||||||
|
"import_config": "استيراد التكوين",
|
||||||
|
"export_config": "تصدير التكوين",
|
||||||
|
"apply_config": "تطبيق التكوين",
|
||||||
|
"select_all": "تحديد الكل",
|
||||||
|
"deselect_all": "إلغاء تحديد الكل",
|
||||||
|
"save_changes": "حفظ التغييرات",
|
||||||
|
"config_name": "اسم التكوين",
|
||||||
|
"config_description": "وصف التكوين",
|
||||||
|
"current_config": "التكوين الحالي",
|
||||||
|
"tool_management": "إدارة الأدوات",
|
||||||
|
"total_tools": "إجمالي الأدوات",
|
||||||
|
"enabled_tools": "مفعّلة",
|
||||||
|
"disabled_tools": "معطّلة",
|
||||||
|
"no_config_selected": "لم يتم تحديد تكوين",
|
||||||
|
"select_config_first": "يرجى تحديد أو إنشاء تكوين أولاً",
|
||||||
|
"config_created": "تم إنشاء التكوين بنجاح",
|
||||||
|
"config_updated": "تم تحديث التكوين بنجاح",
|
||||||
|
"config_deleted": "تم حذف التكوين بنجاح",
|
||||||
|
"config_applied": "تم تطبيق التكوين بنجاح",
|
||||||
|
"config_exported": "تم تصدير التكوين بنجاح",
|
||||||
|
"config_imported": "تم استيراد التكوين بنجاح",
|
||||||
|
"confirm_delete": "تأكيد الحذف",
|
||||||
|
"delete_config_confirm": "هل أنت متأكد من حذف التكوين \"{0}\"؟ لا يمكن التراجع عن هذا الإجراء.",
|
||||||
|
"max_config_slots_reached": "تم الوصول إلى الحد الأقصى لعدد التكوينات ({0})",
|
||||||
|
"invalid_config_format": "تنسيق التكوين غير صالح",
|
||||||
|
"invalid_json_format": "تنسيق JSON أو بنية التكوين غير صالحة",
|
||||||
|
"server_tab": "الخادم",
|
||||||
|
"tools_tab": "إدارة الأدوات",
|
||||||
|
"available_tools": "الأدوات المتاحة",
|
||||||
|
"scene_tools": "أدوات المشهد",
|
||||||
|
"node_tools": "أدوات العقد",
|
||||||
|
"component_tools": "أدوات المكونات",
|
||||||
|
"prefab_tools": "أدوات القوالب",
|
||||||
|
"project_tools": "أدوات المشروع",
|
||||||
|
"debug_tools": "أدوات التصحيح",
|
||||||
|
"preferences_tools": "التفضيلات",
|
||||||
|
"server_tools": "أدوات الخادم",
|
||||||
|
"broadcast_tools": "أدوات البث",
|
||||||
|
"scene_advanced_tools": "أدوات المشهد المتقدمة",
|
||||||
|
"scene_view_tools": "أدوات عرض المشهد",
|
||||||
|
"reference_image_tools": "أدوات الصور المرجعية",
|
||||||
|
"asset_advanced_tools": "أدوات الأصول المتقدمة",
|
||||||
|
"validation_tools": "أدوات التحقق",
|
||||||
|
"brand_name": "LiDaxian MCP Series",
|
||||||
|
"brand_subtitle": "LiDaxian MCP Series",
|
||||||
|
"contact_email": "البريد الإلكتروني",
|
||||||
|
"contact_wechat": "WeChat",
|
||||||
|
"contact_github": "GitHub",
|
||||||
|
"machine_id_label": "رمز الجهاز",
|
||||||
|
"purchase_renew": "شراء / تجديد",
|
||||||
|
"purchase_url": "https://www.vberai.com/game-engines/cocos",
|
||||||
|
"auth_title": "اختر طريقة تسجيل الدخول",
|
||||||
|
"auth_subtitle": "أدخل بريدك الإلكتروني ورمز التفعيل",
|
||||||
|
"email_label": "عنوان البريد الإلكتروني",
|
||||||
|
"activation_code_label": "رمز التفعيل",
|
||||||
|
"email_placeholder": "أدخل البريد الإلكتروني",
|
||||||
|
"code_placeholder": "أدخل الرمز (XXXXX-XXXXX-XXXXX-XXXXX-XXXXX)",
|
||||||
|
"activate_button": "تفعيل",
|
||||||
|
"deactivate_button": "إلغاء التفعيل",
|
||||||
|
"activating": "جارٍ التفعيل...",
|
||||||
|
"deactivating": "جارٍ إلغاء التفعيل...",
|
||||||
|
"license_status": "حالة الترخيص",
|
||||||
|
"license_type": "نوع الترخيص",
|
||||||
|
"expiry_date": "تاريخ الانتهاء",
|
||||||
|
"days_remaining": "الأيام المتبقية",
|
||||||
|
"days_unit": "ي",
|
||||||
|
"permanent": "دائم",
|
||||||
|
"expiry_warning_title": "الترخيص على وشك الانتهاء",
|
||||||
|
"expiry_warning_message": "سينتهي ترخيصك خلال {0} أيام. يرجى التجديد لتجنب انقطاع الخدمة.",
|
||||||
|
"renew_now": "تجديد الآن",
|
||||||
|
"i_understand": "فهمت",
|
||||||
|
"activation_success": "تم تفعيل الترخيص بنجاح!",
|
||||||
|
"activation_failed": "فشل التفعيل: {0}",
|
||||||
|
"invalid_code": "تنسيق رمز التفعيل غير صالح",
|
||||||
|
"purchase_license": "شراء ترخيص",
|
||||||
|
"renew_license": "تجديد الترخيص",
|
||||||
|
"contact_support": "الدعم",
|
||||||
|
"language_switch": "العربية",
|
||||||
|
"version_label": "الإصدار",
|
||||||
|
"license_required": "مطلوب ترخيص لبدء الخادم",
|
||||||
|
"license_check_failed": "فشل التحقق من الترخيص",
|
||||||
|
|
||||||
|
"status_stopped": "متوقف",
|
||||||
|
"status_running": "يعمل",
|
||||||
|
"status_error": "فشل الاتصال",
|
||||||
|
"config_tab": "إعداد سريع",
|
||||||
|
"status_label": "الحالة",
|
||||||
|
"connections_count": "الاتصالات",
|
||||||
|
"transport_protocol": "بروتوكول النقل",
|
||||||
|
"transport_type": "نوع النقل",
|
||||||
|
"mcp_url": "رابط MCP",
|
||||||
|
"port_fallback_title": "المنفذ {0} غير متاح — تم التبديل إلى {1}",
|
||||||
|
"port_fallback_reason": "المنفذ {0} محجوز من قبل النظام أو محظور بواسطة جدار الحماية/برنامج مكافحة الفيروسات (شائع في نظام Windows المثبت عليه Hyper-V أو WSL2 أو Docker). الخادم يعمل بشكل طبيعي على المنفذ الجديد.",
|
||||||
|
"port_fallback_resynced": "تم تحديث إعدادات العملاء التالية: {0}. أعد تشغيلها ليصبح المنفذ الجديد ساري المفعول.",
|
||||||
|
"port_fallback_no_clients": "إذا سبق أن أعددت عملاء الذكاء الاصطناعي، فأعد إعدادهم على المنفذ الجديد من خلال الإعداد بنقرة واحدة.",
|
||||||
|
"port_fallback_dismiss": "حسنًا",
|
||||||
|
"port_label": "المنفذ",
|
||||||
|
"auto_start_label": "بدء تلقائي",
|
||||||
|
"debug_log_label": "سجل التصحيح",
|
||||||
|
"max_connections_label": "الحد الأقصى للاتصالات",
|
||||||
|
"server_settings_title": "إعدادات الخادم",
|
||||||
|
"server_status_title": "حالة الخادم",
|
||||||
|
"connection_info_title": "معلومات الاتصال",
|
||||||
|
"tools_count": "{0} أدوات",
|
||||||
|
"tools_stats": "{0} أدوات ({1} مفعّلة / {2} معطّلة)",
|
||||||
|
"quick_config_title": "إعداد سريع لعملاء MCP",
|
||||||
|
"quick_config_desc": "تكوين خادم MCP تلقائيًا في محررات AI وأدوات CLI",
|
||||||
|
"server_info": "معلومات الخادم",
|
||||||
|
"server_name_label": "اسم الخادم",
|
||||||
|
"server_address": "عنوان الخادم",
|
||||||
|
"timeout_ms": "مهلة الانتظار (مللي ثانية)",
|
||||||
|
"auto_config_title": "إعداد تلقائي (IDE + Codex CLI)",
|
||||||
|
"add_all": "إضافة الكل",
|
||||||
|
"remove_all": "إزالة الكل",
|
||||||
|
"auto_config_note": "انقر لتعديل ملفات التكوين تلقائيًا (Cursor, Windsurf, Trae, Codex CLI)",
|
||||||
|
"configured": "مُعدّ",
|
||||||
|
"not_configured": "غير مُعدّ",
|
||||||
|
"auto_config_btn": "إعداد تلقائي",
|
||||||
|
"delete_btn": "حذف",
|
||||||
|
"copy_config": "نسخ التكوين",
|
||||||
|
"open_config": "فتح التكوين",
|
||||||
|
"manual_config_title": "إعداد يدوي (Claude CLI + Gemini CLI)",
|
||||||
|
"refresh_commands": "تحديث الأوامر",
|
||||||
|
"manual_config_note": "أدوات CLI التالية تتطلب تنفيذًا يدويًا في الطرفية",
|
||||||
|
"config_scope": "نطاق التكوين:",
|
||||||
|
"scope_user": "مستوى المستخدم",
|
||||||
|
"scope_project": "مستوى المشروع",
|
||||||
|
"operation_log": "سجل العمليات",
|
||||||
|
"no_logs": "لا توجد سجلات",
|
||||||
|
"copyright_line": "© 2025 LiDaxian - Cocos Creator MCP Server v{0}",
|
||||||
|
"protocol_line": "مبني على بروتوكول Streamable HTTP (MCP 2025-03-26)",
|
||||||
|
"tools_copyright_line1": "نظام الأدوات - 13 أداة intent-level، أكثر من 150 عملية محرر، توزيع أكواد الإجراءات",
|
||||||
|
"config_copyright_line1": "إعداد سريع - يدعم Cursor, Windsurf, Trae, Codex CLI, Claude CLI, Gemini CLI",
|
||||||
|
"email_required": "يرجى إدخال البريد الإلكتروني ورمز التفعيل",
|
||||||
|
"permanent_label": "دائم",
|
||||||
|
"logout_button": "تسجيل الخروج",
|
||||||
|
"invite_code_label": "رمز الدعوة",
|
||||||
|
"copy_button": "نسخ",
|
||||||
|
"change_code_button": "تغيير الرمز",
|
||||||
|
"change_code_title": "تغيير رمز التفعيل",
|
||||||
|
"change_code_desc": "أدخل رمز التفعيل الجديد. سيتم إلغاء ربط الجهاز الحالي أولاً.",
|
||||||
|
"confirm_change": "تأكيد",
|
||||||
|
"cancel": "إلغاء",
|
||||||
|
"code_required": "يرجى إدخال رمز التفعيل",
|
||||||
|
"expiry_inline_warning": "ينتهي قريبًا!",
|
||||||
|
"store_purchase_hint": "إذا اشتريت هذا الإضافة من Cocos Store، يرجى الاتصال بي للحصول على رمز التفعيل.",
|
||||||
|
"license_expired_title": "انتهت صلاحية الترخيص",
|
||||||
|
"license_expired_message": "انتهت صلاحية ترخيصك ولا يمكن بدء الخادم. يرجى التجديد أو إعادة التفعيل للمتابعة.",
|
||||||
|
"go_to_login": "الذهاب لتسجيل الدخول",
|
||||||
|
|
||||||
|
"log_load_config_failed": "فشل في تحميل حالة التكوين: {0}",
|
||||||
|
"log_gen_cli_failed": "فشل في إنشاء أوامر CLI: {0}",
|
||||||
|
"log_add_client_failed": "فشل في الإضافة إلى {0}: {1}",
|
||||||
|
"log_remove_client_failed": "فشل في الإزالة من {0}: {1}",
|
||||||
|
"quick_config_dialog_title": "تكوين بنقرة واحدة",
|
||||||
|
"config_operation_failed": "فشلت عملية التكوين",
|
||||||
|
"config_file_not_found_changed_path": "فشل التكوين: لم يتم العثور على ملف التكوين. ربما تم تثبيت {0} في مسار مخصص.",
|
||||||
|
"log_batch_add_failed": "فشل الإضافة الجماعية: {0}",
|
||||||
|
"log_batch_remove_failed": "فشل الإزالة الجماعية: {0}",
|
||||||
|
"log_copied_config": "تم نسخ تكوين {0} إلى الحافظة",
|
||||||
|
"log_gen_config_failed": "فشل في إنشاء التكوين",
|
||||||
|
"log_copy_config_failed": "فشل في نسخ التكوين: {0}",
|
||||||
|
"log_copied_command": "تم نسخ أمر {0} إلى الحافظة",
|
||||||
|
"log_copy_command_failed": "فشل في نسخ الأمر: {0}",
|
||||||
|
"log_opened_config_file": "تم فتح ملف التكوين: {0}",
|
||||||
|
"log_open_config_failed": "فشل في فتح ملف التكوين",
|
||||||
|
"log_open_config_error": "فشل في فتح ملف التكوين: {0}",
|
||||||
|
|
||||||
|
"update_available_title": "تحديث متاح",
|
||||||
|
"update_new_version": "الإصدار الجديد v{0} متاح!",
|
||||||
|
"update_now": "التحديث الآن",
|
||||||
|
"update_not_now": "ليس الآن",
|
||||||
|
"update_skip_version": "تخطي v{0}",
|
||||||
|
"check_for_updates": "التحقق من التحديثات",
|
||||||
|
"update_checking": "جارٍ التحقق...",
|
||||||
|
"update_no_update": "أنت تستخدم أحدث إصدار",
|
||||||
|
"update_urgency_critical": "تحديث عاجل",
|
||||||
|
"update_urgency_high": "تحديث مهم",
|
||||||
|
"update_current_version": "الإصدار الحالي: v{0}",
|
||||||
|
"update_changelog": "سجل التغييرات",
|
||||||
|
"auth_choose_subtitle": "اختر طريقة تسجيل الدخول للمتابعة",
|
||||||
|
"update_changelog_empty": "لم يتم توفير ملاحظات إصدار لهذه النسخة.",
|
||||||
|
"subscription_login_title": "حساب اشتراك VberAI",
|
||||||
|
"subscription_password_label": "كلمة المرور",
|
||||||
|
"subscription_password_placeholder": "أدخل كلمة المرور",
|
||||||
|
"subscription_login_button": "تسجيل الدخول باستخدام VberAI",
|
||||||
|
"subscription_google_button": "تسجيل الدخول باستخدام Google",
|
||||||
|
"subscription_google_pending": "تم فتح المتصفح. أكمل تفويض Google ثم عد إلى Cocos Creator.",
|
||||||
|
"subscription_google_pending_button": "في انتظار تفويض Google...",
|
||||||
|
"subscription_register_button": "إنشاء حساب VberAI",
|
||||||
|
"subscription_logging_in": "جارٍ تسجيل الدخول...",
|
||||||
|
"subscription_credentials_required": "يرجى إدخال البريد الإلكتروني وكلمة المرور",
|
||||||
|
"subscription_login_success": "تم تسجيل الدخول إلى حساب الاشتراك",
|
||||||
|
"subscription_login_failed": "فشل تسجيل الدخول إلى الاشتراك",
|
||||||
|
"subscription_not_entitled": "اشتراكك الحالي لا يتضمن Cocos 3.x MCP. قم بالترقية للمتابعة.",
|
||||||
|
"subscription_upgrade_to": "قم بالترقية إلى «{0}» لاستخدام هذا المنتج",
|
||||||
|
"subscription_upgrade_action": "ترقية الاشتراك",
|
||||||
|
"subscription_logged_in": "الحساب المسجّل دخوله",
|
||||||
|
"subscription_choose_button": "حساب اشتراك VberAI",
|
||||||
|
"license_choose_button": "رمز التفعيل",
|
||||||
|
"subscription_method_label": "اشتراك",
|
||||||
|
"back_to_login_choices": "← اختر طريقة تسجيل دخول أخرى",
|
||||||
|
"subscription_signed_out_other_device": "تم تسجيل الخروج: تم استخدام حسابك على جهاز آخر.",
|
||||||
|
"subscription_google_cancelled": "تم إلغاء تسجيل الدخول عبر Google.",
|
||||||
|
"subscription_request_timeout": "انتهت مهلة الطلب. تحقق من شبكتك وحاول مرة أخرى.",
|
||||||
|
"subscription_network_error": "فشل الاتصال بالشبكة. تحقق من شبكتك وحاول مرة أخرى.",
|
||||||
|
"subscription_server_error": "أرجع الخادم استجابة غير صالحة. حاول مرة أخرى لاحقًا.",
|
||||||
|
"subscription_google_authorization_failed": "لم يكتمل تفويض Google. يرجى المحاولة مرة أخرى."
|
||||||
|
};
|
||||||
@@ -0,0 +1,243 @@
|
|||||||
|
"use strict";
|
||||||
|
module.exports = {
|
||||||
|
"extension_name": "Cocos MCP Server",
|
||||||
|
"description": "AI MCP Server für Cocos Creator 3.8",
|
||||||
|
"panel_title": "MCP Server",
|
||||||
|
"open_panel": "MCP-Panel öffnen",
|
||||||
|
"start_server": "Server starten",
|
||||||
|
"stop_server": "Server stoppen",
|
||||||
|
"server_status": "Serverstatus",
|
||||||
|
"port": "Port",
|
||||||
|
"settings": "Einstellungen",
|
||||||
|
"connected": "Verbunden",
|
||||||
|
"disconnected": "Getrennt",
|
||||||
|
"server_running": "Server läuft auf Port {0}",
|
||||||
|
"server_stopped": "Server wurde gestoppt",
|
||||||
|
"auto_start": "Autostart",
|
||||||
|
"debug_log": "Debug-Protokoll",
|
||||||
|
"max_connections": "Max. Verbindungen",
|
||||||
|
"connection_info": "Verbindungsinfo",
|
||||||
|
"http_url": "HTTP-URL",
|
||||||
|
"copy": "Kopieren",
|
||||||
|
"save_settings": "Speichern",
|
||||||
|
"settings_saved": "Einstellungen erfolgreich gespeichert",
|
||||||
|
"server_started": "MCP Server gestartet",
|
||||||
|
"server_stopped_msg": "MCP Server gestoppt",
|
||||||
|
"failed_to_start": "Server konnte nicht gestartet werden",
|
||||||
|
"failed_to_stop": "Server konnte nicht gestoppt werden",
|
||||||
|
"failed_to_save": "Einstellungen konnten nicht gespeichert werden",
|
||||||
|
"url_copied": "HTTP-URL in Zwischenablage kopiert",
|
||||||
|
"tool_manager": "Tool-Manager",
|
||||||
|
"open_tool_manager": "Tool-Manager öffnen",
|
||||||
|
"create_config": "Konfiguration erstellen",
|
||||||
|
"edit_config": "Konfiguration bearbeiten",
|
||||||
|
"delete_config": "Konfiguration löschen",
|
||||||
|
"import_config": "Konfiguration importieren",
|
||||||
|
"export_config": "Konfiguration exportieren",
|
||||||
|
"apply_config": "Konfiguration anwenden",
|
||||||
|
"select_all": "Alle auswählen",
|
||||||
|
"deselect_all": "Alle abwählen",
|
||||||
|
"save_changes": "Änderungen speichern",
|
||||||
|
"config_name": "Konfigurationsname",
|
||||||
|
"config_description": "Konfigurationsbeschreibung",
|
||||||
|
"current_config": "Aktuelle Konfiguration",
|
||||||
|
"tool_management": "Tool-Verwaltung",
|
||||||
|
"total_tools": "Gesamt",
|
||||||
|
"enabled_tools": "Aktiviert",
|
||||||
|
"disabled_tools": "Deaktiviert",
|
||||||
|
"no_config_selected": "Keine Konfiguration ausgewählt",
|
||||||
|
"select_config_first": "Bitte wählen Sie zuerst eine Konfiguration oder erstellen Sie eine neue",
|
||||||
|
"config_created": "Konfiguration erfolgreich erstellt",
|
||||||
|
"config_updated": "Konfiguration erfolgreich aktualisiert",
|
||||||
|
"config_deleted": "Konfiguration erfolgreich gelöscht",
|
||||||
|
"config_applied": "Konfiguration erfolgreich angewendet",
|
||||||
|
"config_exported": "Konfiguration erfolgreich exportiert",
|
||||||
|
"config_imported": "Konfiguration erfolgreich importiert",
|
||||||
|
"confirm_delete": "Löschen bestätigen",
|
||||||
|
"delete_config_confirm": "Möchten Sie die Konfiguration \"{0}\" wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||||
|
"max_config_slots_reached": "Maximale Anzahl von Konfigurationen erreicht ({0})",
|
||||||
|
"invalid_config_format": "Ungültiges Konfigurationsformat",
|
||||||
|
"invalid_json_format": "Ungültiges JSON-Format oder Konfigurationsstruktur",
|
||||||
|
"server_tab": "Server",
|
||||||
|
"tools_tab": "Tool-Verwaltung",
|
||||||
|
"available_tools": "Verfügbare Tools",
|
||||||
|
"scene_tools": "Szenen-Tools",
|
||||||
|
"node_tools": "Knoten-Tools",
|
||||||
|
"component_tools": "Komponenten-Tools",
|
||||||
|
"prefab_tools": "Prefab-Tools",
|
||||||
|
"project_tools": "Projekt-Tools",
|
||||||
|
"debug_tools": "Debug-Tools",
|
||||||
|
"preferences_tools": "Einstellungen",
|
||||||
|
"server_tools": "Server-Tools",
|
||||||
|
"broadcast_tools": "Broadcast-Tools",
|
||||||
|
"scene_advanced_tools": "Erweiterte Szenen-Tools",
|
||||||
|
"scene_view_tools": "Szenenansicht-Tools",
|
||||||
|
"reference_image_tools": "Referenzbild-Tools",
|
||||||
|
"asset_advanced_tools": "Erweiterte Asset-Tools",
|
||||||
|
"validation_tools": "Validierungs-Tools",
|
||||||
|
"brand_name": "LiDaxian MCP Series",
|
||||||
|
"brand_subtitle": "LiDaxian MCP Series",
|
||||||
|
"contact_email": "E-Mail",
|
||||||
|
"contact_wechat": "WeChat",
|
||||||
|
"contact_github": "GitHub",
|
||||||
|
"machine_id_label": "Maschinencode",
|
||||||
|
"purchase_renew": "Kaufen / Verlängern",
|
||||||
|
"purchase_url": "https://www.vberai.com/game-engines/cocos",
|
||||||
|
"auth_title": "Anmeldemethode auswählen",
|
||||||
|
"auth_subtitle": "Geben Sie Ihre E-Mail und Ihren Aktivierungscode ein",
|
||||||
|
"email_label": "E-Mail-Adresse",
|
||||||
|
"activation_code_label": "Aktivierungscode",
|
||||||
|
"email_placeholder": "E-Mail eingeben",
|
||||||
|
"code_placeholder": "Code eingeben (XXXXX-XXXXX-XXXXX-XXXXX-XXXXX)",
|
||||||
|
"activate_button": "Aktivieren",
|
||||||
|
"deactivate_button": "Deaktivieren",
|
||||||
|
"activating": "Aktivierung...",
|
||||||
|
"deactivating": "Deaktivierung...",
|
||||||
|
"license_status": "Lizenzstatus",
|
||||||
|
"license_type": "Lizenztyp",
|
||||||
|
"expiry_date": "Ablaufdatum",
|
||||||
|
"days_remaining": "Verbleibende Tage",
|
||||||
|
"days_unit": "T",
|
||||||
|
"permanent": "Permanent",
|
||||||
|
"expiry_warning_title": "Lizenz läuft bald ab",
|
||||||
|
"expiry_warning_message": "Ihre Lizenz läuft in {0} Tagen ab. Bitte verlängern Sie, um Unterbrechungen zu vermeiden.",
|
||||||
|
"renew_now": "Jetzt verlängern",
|
||||||
|
"i_understand": "Verstanden",
|
||||||
|
"activation_success": "Lizenz erfolgreich aktiviert!",
|
||||||
|
"activation_failed": "Aktivierung fehlgeschlagen: {0}",
|
||||||
|
"invalid_code": "Ungültiges Aktivierungscode-Format",
|
||||||
|
"purchase_license": "Lizenz kaufen",
|
||||||
|
"renew_license": "Lizenz verlängern",
|
||||||
|
"contact_support": "Support",
|
||||||
|
"language_switch": "Deutsch",
|
||||||
|
"version_label": "Version",
|
||||||
|
"license_required": "Zum Starten des Servers ist eine Lizenz erforderlich",
|
||||||
|
"license_check_failed": "Lizenzprüfung fehlgeschlagen",
|
||||||
|
|
||||||
|
"status_stopped": "Gestoppt",
|
||||||
|
"status_running": "Läuft",
|
||||||
|
"status_error": "Verbindung fehlgeschlagen",
|
||||||
|
"config_tab": "Schnellkonfig",
|
||||||
|
"status_label": "Status",
|
||||||
|
"connections_count": "Verbindungen",
|
||||||
|
"transport_protocol": "Transportprotokoll",
|
||||||
|
"transport_type": "Transporttyp",
|
||||||
|
"mcp_url": "MCP-URL",
|
||||||
|
"port_fallback_title": "Port {0} nicht verfügbar – zu {1} gewechselt",
|
||||||
|
"port_fallback_reason": "Port {0} ist vom System reserviert oder wird von einer Firewall/Antivirensoftware blockiert (häufig unter Windows mit Hyper-V, WSL2 oder Docker). Der Server läuft auf dem neuen Port normal.",
|
||||||
|
"port_fallback_resynced": "Konfiguration aktualisiert für: {0}. Starten Sie diese Clients neu, damit der neue Port wirksam wird.",
|
||||||
|
"port_fallback_no_clients": "Falls Sie bereits AI-Clients eingerichtet haben, konfigurieren Sie sie über die Ein-Klick-Konfiguration erneut auf den neuen Port.",
|
||||||
|
"port_fallback_dismiss": "Verstanden",
|
||||||
|
"port_label": "Port",
|
||||||
|
"auto_start_label": "Autostart",
|
||||||
|
"debug_log_label": "Debug-Protokoll",
|
||||||
|
"max_connections_label": "Max. Verbindungen",
|
||||||
|
"server_settings_title": "Servereinstellungen",
|
||||||
|
"server_status_title": "Serverstatus",
|
||||||
|
"connection_info_title": "Verbindungsinfo",
|
||||||
|
"tools_count": "{0} Tools",
|
||||||
|
"tools_stats": "{0} Tools ({1} aktiviert / {2} deaktiviert)",
|
||||||
|
"quick_config_title": "MCP-Clients schnell konfigurieren",
|
||||||
|
"quick_config_desc": "MCP-Server automatisch in AI-Editoren und CLI-Tools konfigurieren",
|
||||||
|
"server_info": "Serverinfo",
|
||||||
|
"server_name_label": "Servername",
|
||||||
|
"server_address": "Serveradresse",
|
||||||
|
"timeout_ms": "Zeitlimit (ms)",
|
||||||
|
"auto_config_title": "Auto-Konfig (IDE-Editoren + Codex CLI)",
|
||||||
|
"add_all": "Alle hinzufügen",
|
||||||
|
"remove_all": "Alle entfernen",
|
||||||
|
"auto_config_note": "Klicken Sie, um Konfigurationsdateien automatisch zu ändern (Cursor, Windsurf, Trae, Codex CLI)",
|
||||||
|
"configured": "Konfiguriert",
|
||||||
|
"not_configured": "Nicht konfiguriert",
|
||||||
|
"auto_config_btn": "Auto-Konfig",
|
||||||
|
"delete_btn": "Löschen",
|
||||||
|
"copy_config": "Konfig kopieren",
|
||||||
|
"open_config": "Konfig öffnen",
|
||||||
|
"manual_config_title": "Manuelle Konfig (Claude CLI + Gemini CLI)",
|
||||||
|
"refresh_commands": "Befehle aktualisieren",
|
||||||
|
"manual_config_note": "Die folgenden CLI-Tools erfordern manuelle Ausführung im Terminal",
|
||||||
|
"config_scope": "Konfig-Bereich:",
|
||||||
|
"scope_user": "Benutzerebene",
|
||||||
|
"scope_project": "Projektebene",
|
||||||
|
"operation_log": "Operationsprotokoll",
|
||||||
|
"no_logs": "Keine Einträge",
|
||||||
|
"copyright_line": "© 2025 LiDaxian - Cocos Creator MCP Server v{0}",
|
||||||
|
"protocol_line": "Basierend auf dem Streamable HTTP Protokoll (MCP 2025-03-26)",
|
||||||
|
"tools_copyright_line1": "Tool-System - 13 Intent-Level-Tools, 150+ Editor-Operationen, Action-Code-Dispatch",
|
||||||
|
"config_copyright_line1": "Schnellkonfig - Unterstützt Cursor, Windsurf, Trae, Codex CLI, Claude CLI, Gemini CLI",
|
||||||
|
"email_required": "Bitte geben Sie E-Mail und Aktivierungscode ein",
|
||||||
|
"permanent_label": "Permanent",
|
||||||
|
"logout_button": "Abmelden",
|
||||||
|
"invite_code_label": "Einladungscode",
|
||||||
|
"copy_button": "Kopieren",
|
||||||
|
"change_code_button": "Code ändern",
|
||||||
|
"change_code_title": "Aktivierungscode ändern",
|
||||||
|
"change_code_desc": "Geben Sie Ihren neuen Aktivierungscode ein. Die aktuelle Gerätebindung wird zuerst freigegeben.",
|
||||||
|
"confirm_change": "Bestätigen",
|
||||||
|
"cancel": "Abbrechen",
|
||||||
|
"code_required": "Bitte geben Sie einen Aktivierungscode ein",
|
||||||
|
"expiry_inline_warning": "Läuft bald ab!",
|
||||||
|
"store_purchase_hint": "Wenn Sie dieses Plugin im Cocos Store gekauft haben, kontaktieren Sie mich bitte, um Ihren Aktivierungscode zu erhalten.",
|
||||||
|
"license_expired_title": "Lizenz abgelaufen",
|
||||||
|
"license_expired_message": "Ihre Lizenz ist abgelaufen und der Server kann nicht gestartet werden. Bitte verlängern oder reaktivieren Sie, um fortzufahren.",
|
||||||
|
"go_to_login": "Zum Login",
|
||||||
|
|
||||||
|
"log_load_config_failed": "Konfigurationsstatus konnte nicht geladen werden: {0}",
|
||||||
|
"log_gen_cli_failed": "CLI-Befehle konnten nicht generiert werden: {0}",
|
||||||
|
"log_add_client_failed": "Hinzufügen zu {0} fehlgeschlagen: {1}",
|
||||||
|
"log_remove_client_failed": "Entfernen von {0} fehlgeschlagen: {1}",
|
||||||
|
"quick_config_dialog_title": "Ein-Klick-Konfiguration",
|
||||||
|
"config_operation_failed": "Konfigurationsvorgang fehlgeschlagen",
|
||||||
|
"config_file_not_found_changed_path": "Konfiguration fehlgeschlagen: Konfigurationsdatei nicht gefunden. {0} wurde möglicherweise in einem benutzerdefinierten Pfad installiert.",
|
||||||
|
"log_batch_add_failed": "Stapel-Hinzufügen fehlgeschlagen: {0}",
|
||||||
|
"log_batch_remove_failed": "Stapel-Entfernen fehlgeschlagen: {0}",
|
||||||
|
"log_copied_config": "{0}-Konfiguration in Zwischenablage kopiert",
|
||||||
|
"log_gen_config_failed": "Konfiguration konnte nicht generiert werden",
|
||||||
|
"log_copy_config_failed": "Konfiguration konnte nicht kopiert werden: {0}",
|
||||||
|
"log_copied_command": "{0}-Befehl in Zwischenablage kopiert",
|
||||||
|
"log_copy_command_failed": "Befehl konnte nicht kopiert werden: {0}",
|
||||||
|
"log_opened_config_file": "Konfigurationsdatei geöffnet: {0}",
|
||||||
|
"log_open_config_failed": "Konfigurationsdatei konnte nicht geöffnet werden",
|
||||||
|
"log_open_config_error": "Konfigurationsdatei konnte nicht geöffnet werden: {0}",
|
||||||
|
|
||||||
|
"update_available_title": "Update verfügbar",
|
||||||
|
"update_new_version": "Neue Version v{0} ist verfügbar!",
|
||||||
|
"update_now": "Jetzt aktualisieren",
|
||||||
|
"update_not_now": "Später",
|
||||||
|
"update_skip_version": "v{0} überspringen",
|
||||||
|
"check_for_updates": "Nach Updates suchen",
|
||||||
|
"update_checking": "Prüfe...",
|
||||||
|
"update_no_update": "Sie verwenden die neueste Version",
|
||||||
|
"update_urgency_critical": "Kritisches Update",
|
||||||
|
"update_urgency_high": "Wichtiges Update",
|
||||||
|
"update_current_version": "Aktuelle Version: v{0}",
|
||||||
|
"update_changelog": "Änderungsprotokoll",
|
||||||
|
"auth_choose_subtitle": "Wählen Sie eine Anmeldemethode, um fortzufahren",
|
||||||
|
"update_changelog_empty": "Für diese Version wurden keine Versionshinweise bereitgestellt.",
|
||||||
|
"subscription_login_title": "VberAI-Abonnementkonto",
|
||||||
|
"subscription_password_label": "Passwort",
|
||||||
|
"subscription_password_placeholder": "Passwort eingeben",
|
||||||
|
"subscription_login_button": "Mit VberAI anmelden",
|
||||||
|
"subscription_google_button": "Mit Google anmelden",
|
||||||
|
"subscription_google_pending": "Browser geöffnet. Schließen Sie die Google-Autorisierung ab und kehren Sie dann zu Cocos Creator zurück.",
|
||||||
|
"subscription_google_pending_button": "Warte auf Google-Autorisierung...",
|
||||||
|
"subscription_register_button": "VberAI-Konto erstellen",
|
||||||
|
"subscription_logging_in": "Anmeldung läuft...",
|
||||||
|
"subscription_credentials_required": "Bitte E-Mail und Passwort eingeben",
|
||||||
|
"subscription_login_success": "Bei Abonnementkonto angemeldet",
|
||||||
|
"subscription_login_failed": "Abonnement-Anmeldung fehlgeschlagen",
|
||||||
|
"subscription_not_entitled": "Ihr aktuelles Abonnement enthält Cocos 3.x MCP nicht. Führen Sie ein Upgrade durch, um fortzufahren.",
|
||||||
|
"subscription_upgrade_to": "Führen Sie ein Upgrade auf „{0}“ durch, um dieses Produkt zu verwenden",
|
||||||
|
"subscription_upgrade_action": "Abonnement upgraden",
|
||||||
|
"subscription_logged_in": "Angemeldetes Konto",
|
||||||
|
"subscription_choose_button": "VberAI-Abonnementkonto",
|
||||||
|
"license_choose_button": "Aktivierungscode",
|
||||||
|
"subscription_method_label": "Abonnement",
|
||||||
|
"back_to_login_choices": "← Andere Anmeldemethode wählen",
|
||||||
|
"subscription_signed_out_other_device": "Abgemeldet: Ihr Konto wurde auf einem anderen Gerät verwendet.",
|
||||||
|
"subscription_google_cancelled": "Google-Anmeldung wurde abgebrochen.",
|
||||||
|
"subscription_request_timeout": "Zeitüberschreitung der Anfrage. Überprüfen Sie Ihr Netzwerk und versuchen Sie es erneut.",
|
||||||
|
"subscription_network_error": "Netzwerkverbindung fehlgeschlagen. Überprüfen Sie Ihr Netzwerk und versuchen Sie es erneut.",
|
||||||
|
"subscription_server_error": "Der Server hat eine ungültige Antwort zurückgegeben. Versuchen Sie es später erneut.",
|
||||||
|
"subscription_google_authorization_failed": "Die Google-Autorisierung wurde nicht abgeschlossen. Bitte versuchen Sie es erneut."
|
||||||
|
};
|
||||||
@@ -0,0 +1,244 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
"extension_name": "Cocos MCP Server",
|
||||||
|
"description": "AI MCP Server for Cocos Creator 3.8",
|
||||||
|
"panel_title": "MCP Server",
|
||||||
|
"open_panel": "Open MCP Panel",
|
||||||
|
"start_server": "Start Server",
|
||||||
|
"stop_server": "Stop Server",
|
||||||
|
"server_status": "Server Status",
|
||||||
|
"port": "Port",
|
||||||
|
"settings": "Settings",
|
||||||
|
"connected": "Connected",
|
||||||
|
"disconnected": "Disconnected",
|
||||||
|
"server_running": "Server is running on port {0}",
|
||||||
|
"server_stopped": "Server has stopped",
|
||||||
|
"auto_start": "Auto Start",
|
||||||
|
"debug_log": "Debug Logging",
|
||||||
|
"max_connections": "Max Connections",
|
||||||
|
"connection_info": "Connection Info",
|
||||||
|
"http_url": "HTTP URL",
|
||||||
|
"copy": "Copy",
|
||||||
|
"save_settings": "Save Settings",
|
||||||
|
"settings_saved": "Settings saved successfully",
|
||||||
|
"server_started": "MCP Server Started",
|
||||||
|
"server_stopped_msg": "MCP Server Stopped",
|
||||||
|
"failed_to_start": "Failed to start server",
|
||||||
|
"failed_to_stop": "Failed to stop server",
|
||||||
|
"failed_to_save": "Failed to save settings",
|
||||||
|
"url_copied": "HTTP URL copied to clipboard",
|
||||||
|
"tool_manager": "Tool Manager",
|
||||||
|
"open_tool_manager": "Open Tool Manager",
|
||||||
|
"create_config": "Create Configuration",
|
||||||
|
"edit_config": "Edit Configuration",
|
||||||
|
"delete_config": "Delete Configuration",
|
||||||
|
"import_config": "Import Configuration",
|
||||||
|
"export_config": "Export Configuration",
|
||||||
|
"apply_config": "Apply Configuration",
|
||||||
|
"select_all": "Select All",
|
||||||
|
"deselect_all": "Deselect All",
|
||||||
|
"save_changes": "Save Changes",
|
||||||
|
"config_name": "Configuration Name",
|
||||||
|
"config_description": "Configuration Description",
|
||||||
|
"current_config": "Current Configuration",
|
||||||
|
"tool_management": "Tool Management",
|
||||||
|
"total_tools": "Total Tools",
|
||||||
|
"enabled_tools": "Enabled",
|
||||||
|
"disabled_tools": "Disabled",
|
||||||
|
"no_config_selected": "No Configuration Selected",
|
||||||
|
"select_config_first": "Please select a configuration or create a new one first",
|
||||||
|
"config_created": "Configuration created successfully",
|
||||||
|
"config_updated": "Configuration updated successfully",
|
||||||
|
"config_deleted": "Configuration deleted successfully",
|
||||||
|
"config_applied": "Configuration applied successfully",
|
||||||
|
"config_exported": "Configuration exported successfully",
|
||||||
|
"config_imported": "Configuration imported successfully",
|
||||||
|
"confirm_delete": "Confirm Delete",
|
||||||
|
"delete_config_confirm": "Are you sure you want to delete configuration \"{0}\"? This action cannot be undone.",
|
||||||
|
"max_config_slots_reached": "Maximum configuration slots reached ({0})",
|
||||||
|
"invalid_config_format": "Invalid configuration format",
|
||||||
|
"invalid_json_format": "Invalid JSON format or configuration structure",
|
||||||
|
"server_tab": "Server",
|
||||||
|
"tools_tab": "Tool Management",
|
||||||
|
"available_tools": "Available Tools",
|
||||||
|
"scene_tools": "Scene Tools",
|
||||||
|
"node_tools": "Node Tools",
|
||||||
|
"component_tools": "Component Tools",
|
||||||
|
"prefab_tools": "Prefab Tools",
|
||||||
|
"project_tools": "Project Tools",
|
||||||
|
"debug_tools": "Debug Tools",
|
||||||
|
"preferences_tools": "Preferences",
|
||||||
|
"server_tools": "Server Tools",
|
||||||
|
"broadcast_tools": "Broadcast Tools",
|
||||||
|
"scene_advanced_tools": "Advanced Scene Tools",
|
||||||
|
"scene_view_tools": "Scene View Tools",
|
||||||
|
"reference_image_tools": "Reference Image Tools",
|
||||||
|
"asset_advanced_tools": "Advanced Asset Tools",
|
||||||
|
"validation_tools": "Validation Tools",
|
||||||
|
"brand_name": "LiDaxian MCP Series",
|
||||||
|
"brand_subtitle": "LiDaxian MCP Series",
|
||||||
|
"contact_email": "Email",
|
||||||
|
"contact_wechat": "WeChat",
|
||||||
|
"contact_github": "GitHub",
|
||||||
|
"machine_id_label": "Machine ID",
|
||||||
|
"purchase_renew": "Purchase / Renew",
|
||||||
|
"purchase_url": "https://www.vberai.com/game-engines/cocos",
|
||||||
|
"auth_title": "Choose a sign-in method",
|
||||||
|
"auth_subtitle": "Enter your email and activation code",
|
||||||
|
"auth_choose_subtitle": "Choose a sign-in method to continue",
|
||||||
|
"email_label": "Email Address",
|
||||||
|
"activation_code_label": "Activation Code",
|
||||||
|
"email_placeholder": "Enter email",
|
||||||
|
"code_placeholder": "Enter code (XXXXX-XXXXX-XXXXX-XXXXX-XXXXX)",
|
||||||
|
"activate_button": "Activate",
|
||||||
|
"deactivate_button": "Deactivate",
|
||||||
|
"activating": "Activating...",
|
||||||
|
"deactivating": "Deactivating...",
|
||||||
|
"license_status": "License Status",
|
||||||
|
"license_type": "License Type",
|
||||||
|
"expiry_date": "Expiry Date",
|
||||||
|
"days_remaining": "Days Remaining",
|
||||||
|
"days_unit": "d",
|
||||||
|
"permanent": "Permanent",
|
||||||
|
"expiry_warning_title": "License Expiring Soon",
|
||||||
|
"expiry_warning_message": "Your license will expire in {0} days. Please renew to avoid service interruption.",
|
||||||
|
"renew_now": "Renew Now",
|
||||||
|
"i_understand": "I Understand",
|
||||||
|
"activation_success": "License activated successfully!",
|
||||||
|
"activation_failed": "Activation failed: {0}",
|
||||||
|
"invalid_code": "Invalid activation code format",
|
||||||
|
"purchase_license": "Purchase License",
|
||||||
|
"renew_license": "Renew License",
|
||||||
|
"contact_support": "Support",
|
||||||
|
"language_switch": "English",
|
||||||
|
"version_label": "Version",
|
||||||
|
"license_required": "License required to start server",
|
||||||
|
"license_check_failed": "License check failed",
|
||||||
|
|
||||||
|
"status_stopped": "Stopped",
|
||||||
|
"status_running": "Running",
|
||||||
|
"status_error": "Connection Failed",
|
||||||
|
"config_tab": "Quick Config",
|
||||||
|
"status_label": "Status",
|
||||||
|
"connections_count": "Connections",
|
||||||
|
"transport_protocol": "Transport Protocol",
|
||||||
|
"transport_type": "Transport Type",
|
||||||
|
"mcp_url": "MCP URL",
|
||||||
|
"port_fallback_title": "Port {0} unavailable — switched to {1}",
|
||||||
|
"port_fallback_reason": "Port {0} is reserved by the system or blocked by a firewall/antivirus (common on Windows with Hyper-V, WSL2 or Docker installed). The server is running normally on the new port.",
|
||||||
|
"port_fallback_resynced": "Updated the config for: {0}. Restart these clients for the new port to take effect.",
|
||||||
|
"port_fallback_no_clients": "If you have already configured AI clients, re-run the one-click setup to point them at the new port.",
|
||||||
|
"port_fallback_dismiss": "Got it",
|
||||||
|
"port_label": "Port",
|
||||||
|
"auto_start_label": "Auto Start",
|
||||||
|
"debug_log_label": "Debug Log",
|
||||||
|
"max_connections_label": "Max Connections",
|
||||||
|
"server_settings_title": "Server Settings",
|
||||||
|
"server_status_title": "Server Status",
|
||||||
|
"connection_info_title": "Connection Info",
|
||||||
|
"tools_count": "{0} tools",
|
||||||
|
"tools_stats": "{0} tools ({1} enabled / {2} disabled)",
|
||||||
|
"quick_config_title": "Quick Config MCP Clients",
|
||||||
|
"quick_config_desc": "Auto-configure the current MCP server into mainstream AI editors and CLI tools",
|
||||||
|
"server_info": "Server Info",
|
||||||
|
"server_name_label": "Server Name",
|
||||||
|
"server_address": "Server Address",
|
||||||
|
"timeout_ms": "Timeout (ms)",
|
||||||
|
"auto_config_title": "Auto Config (IDE Editors + Codex CLI)",
|
||||||
|
"add_all": "Add All",
|
||||||
|
"remove_all": "Remove All",
|
||||||
|
"auto_config_note": "Click button to auto-modify config files (Cursor, Windsurf, Trae, Codex CLI)",
|
||||||
|
"configured": "Configured",
|
||||||
|
"not_configured": "Not Configured",
|
||||||
|
"auto_config_btn": "Auto Config",
|
||||||
|
"delete_btn": "Delete",
|
||||||
|
"copy_config": "Copy Config",
|
||||||
|
"open_config": "Open Config",
|
||||||
|
"manual_config_title": "Manual Config (Claude CLI + Gemini CLI)",
|
||||||
|
"refresh_commands": "Refresh Commands",
|
||||||
|
"manual_config_note": "The following CLI tools need manual terminal command execution",
|
||||||
|
"config_scope": "Config Scope:",
|
||||||
|
"scope_user": "User Level",
|
||||||
|
"scope_project": "Project Level",
|
||||||
|
"operation_log": "Operation Log",
|
||||||
|
"no_logs": "No operation records",
|
||||||
|
"copyright_line": "© 2025 LiDaxian - Cocos Creator MCP Server v{0}",
|
||||||
|
"protocol_line": "Based on Streamable HTTP Protocol (MCP 2025-03-26)",
|
||||||
|
"tools_copyright_line1": "Tool System - 13 intent-level tools, 150+ editor operations, action code dispatch",
|
||||||
|
"config_copyright_line1": "Quick Config - Supports Cursor, Windsurf, Trae, Codex CLI, Claude CLI, Gemini CLI",
|
||||||
|
"email_required": "Please enter email and activation code",
|
||||||
|
"permanent_label": "Permanent",
|
||||||
|
"logout_button": "Logout",
|
||||||
|
"invite_code_label": "Invite Code",
|
||||||
|
"copy_button": "Copy",
|
||||||
|
"change_code_button": "Change Code",
|
||||||
|
"change_code_title": "Change Activation Code",
|
||||||
|
"change_code_desc": "Enter your new activation code. The current device binding will be released first.",
|
||||||
|
"confirm_change": "Confirm",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"code_required": "Please enter an activation code",
|
||||||
|
"expiry_inline_warning": "Expiring soon!",
|
||||||
|
"store_purchase_hint": "If you purchased this plugin from Cocos Store, please contact me to get your activation code.",
|
||||||
|
"license_expired_title": "License Expired",
|
||||||
|
"license_expired_message": "Your license has expired and the server cannot be started. Please renew or reactivate to continue.",
|
||||||
|
"go_to_login": "Go to Login",
|
||||||
|
|
||||||
|
"log_load_config_failed": "Failed to load config status: {0}",
|
||||||
|
"log_gen_cli_failed": "Failed to generate CLI commands: {0}",
|
||||||
|
"log_add_client_failed": "Failed to add to {0}: {1}",
|
||||||
|
"log_remove_client_failed": "Failed to remove from {0}: {1}",
|
||||||
|
"quick_config_dialog_title": "One-click Configuration",
|
||||||
|
"config_operation_failed": "Configuration operation failed",
|
||||||
|
"config_file_not_found_changed_path": "Configuration failed: could not find the config file. {0} may be installed in a custom path.",
|
||||||
|
"log_batch_add_failed": "Batch add failed: {0}",
|
||||||
|
"log_batch_remove_failed": "Batch remove failed: {0}",
|
||||||
|
"log_copied_config": "Copied {0} config to clipboard",
|
||||||
|
"log_gen_config_failed": "Failed to generate config",
|
||||||
|
"log_copy_config_failed": "Failed to copy config: {0}",
|
||||||
|
"log_copied_command": "Copied {0} command to clipboard",
|
||||||
|
"log_copy_command_failed": "Failed to copy command: {0}",
|
||||||
|
"log_opened_config_file": "Opened config file: {0}",
|
||||||
|
"log_open_config_failed": "Failed to open config file",
|
||||||
|
"log_open_config_error": "Failed to open config file: {0}",
|
||||||
|
|
||||||
|
"update_available_title": "Update Available",
|
||||||
|
"update_new_version": "New version v{0} is available!",
|
||||||
|
"update_now": "Update Now",
|
||||||
|
"update_not_now": "Not Now",
|
||||||
|
"update_skip_version": "Skip v{0}",
|
||||||
|
"check_for_updates": "Check for Updates",
|
||||||
|
"update_checking": "Checking...",
|
||||||
|
"update_no_update": "You are using the latest version",
|
||||||
|
"update_urgency_critical": "Critical Update",
|
||||||
|
"update_urgency_high": "Important Update",
|
||||||
|
"update_current_version": "Current version: v{0}",
|
||||||
|
"update_changelog": "Changelog"
|
||||||
|
,"update_changelog_empty": "No release notes were provided for this version."
|
||||||
|
,"subscription_login_title": "VberAI subscription account"
|
||||||
|
,"subscription_password_label": "Password"
|
||||||
|
,"subscription_password_placeholder": "Enter password"
|
||||||
|
,"subscription_login_button": "Sign in with VberAI"
|
||||||
|
,"subscription_google_button": "Sign in with Google"
|
||||||
|
,"subscription_google_pending": "Browser opened. Complete Google authorization, then return to Cocos Creator."
|
||||||
|
,"subscription_google_pending_button": "Waiting for Google authorization..."
|
||||||
|
,"subscription_register_button": "Create a VberAI account"
|
||||||
|
,"subscription_logging_in": "Signing in..."
|
||||||
|
,"subscription_credentials_required": "Please enter email and password"
|
||||||
|
,"subscription_login_success": "Subscription account signed in"
|
||||||
|
,"subscription_login_failed": "Subscription sign-in failed"
|
||||||
|
,"subscription_not_entitled": "Your current subscription does not include Cocos 3.x MCP. Upgrade to continue."
|
||||||
|
,"subscription_upgrade_to": "Upgrade to {0} to use this product"
|
||||||
|
,"subscription_upgrade_action": "Upgrade subscription"
|
||||||
|
,"subscription_logged_in": "Signed-in account"
|
||||||
|
,"subscription_choose_button": "VberAI subscription account"
|
||||||
|
,"license_choose_button": "Activation code"
|
||||||
|
,"subscription_method_label": "Subscription"
|
||||||
|
,"back_to_login_choices": "← Choose another sign-in method"
|
||||||
|
,"subscription_signed_out_other_device": "Signed out: your account was used on another device."
|
||||||
|
,"subscription_google_cancelled": "Google sign-in was cancelled."
|
||||||
|
,"subscription_request_timeout": "The request timed out. Check your network and try again."
|
||||||
|
,"subscription_network_error": "Network connection failed. Check your network and try again."
|
||||||
|
,"subscription_server_error": "The server returned an invalid response. Try again later."
|
||||||
|
,"subscription_google_authorization_failed": "Google authorization was not completed. Please try again."
|
||||||
|
};
|
||||||
@@ -0,0 +1,243 @@
|
|||||||
|
"use strict";
|
||||||
|
module.exports = {
|
||||||
|
"extension_name": "Cocos MCP Servidor",
|
||||||
|
"description": "Servidor AI MCP para Cocos Creator 3.8",
|
||||||
|
"panel_title": "Servidor MCP",
|
||||||
|
"open_panel": "Abrir panel MCP",
|
||||||
|
"start_server": "Iniciar servidor",
|
||||||
|
"stop_server": "Detener servidor",
|
||||||
|
"server_status": "Estado del servidor",
|
||||||
|
"port": "Puerto",
|
||||||
|
"settings": "Ajustes",
|
||||||
|
"connected": "Conectado",
|
||||||
|
"disconnected": "Desconectado",
|
||||||
|
"server_running": "El servidor está ejecutándose en el puerto {0}",
|
||||||
|
"server_stopped": "El servidor se ha detenido",
|
||||||
|
"auto_start": "Inicio automático",
|
||||||
|
"debug_log": "Registro de depuración",
|
||||||
|
"max_connections": "Conexiones máx.",
|
||||||
|
"connection_info": "Info de conexión",
|
||||||
|
"http_url": "URL HTTP",
|
||||||
|
"copy": "Copiar",
|
||||||
|
"save_settings": "Guardar",
|
||||||
|
"settings_saved": "Ajustes guardados correctamente",
|
||||||
|
"server_started": "Servidor MCP iniciado",
|
||||||
|
"server_stopped_msg": "Servidor MCP detenido",
|
||||||
|
"failed_to_start": "Error al iniciar el servidor",
|
||||||
|
"failed_to_stop": "Error al detener el servidor",
|
||||||
|
"failed_to_save": "Error al guardar los ajustes",
|
||||||
|
"url_copied": "URL HTTP copiada al portapapeles",
|
||||||
|
"tool_manager": "Gestor de herramientas",
|
||||||
|
"open_tool_manager": "Abrir gestor de herramientas",
|
||||||
|
"create_config": "Crear configuración",
|
||||||
|
"edit_config": "Editar configuración",
|
||||||
|
"delete_config": "Eliminar configuración",
|
||||||
|
"import_config": "Importar configuración",
|
||||||
|
"export_config": "Exportar configuración",
|
||||||
|
"apply_config": "Aplicar configuración",
|
||||||
|
"select_all": "Seleccionar todo",
|
||||||
|
"deselect_all": "Deseleccionar todo",
|
||||||
|
"save_changes": "Guardar cambios",
|
||||||
|
"config_name": "Nombre de configuración",
|
||||||
|
"config_description": "Descripción de configuración",
|
||||||
|
"current_config": "Configuración actual",
|
||||||
|
"tool_management": "Gestión de herramientas",
|
||||||
|
"total_tools": "Total de herramientas",
|
||||||
|
"enabled_tools": "Activadas",
|
||||||
|
"disabled_tools": "Desactivadas",
|
||||||
|
"no_config_selected": "Ninguna configuración seleccionada",
|
||||||
|
"select_config_first": "Por favor, seleccione o cree una configuración primero",
|
||||||
|
"config_created": "Configuración creada correctamente",
|
||||||
|
"config_updated": "Configuración actualizada correctamente",
|
||||||
|
"config_deleted": "Configuración eliminada correctamente",
|
||||||
|
"config_applied": "Configuración aplicada correctamente",
|
||||||
|
"config_exported": "Configuración exportada correctamente",
|
||||||
|
"config_imported": "Configuración importada correctamente",
|
||||||
|
"confirm_delete": "Confirmar eliminación",
|
||||||
|
"delete_config_confirm": "¿Está seguro de eliminar la configuración \"{0}\"? Esta acción no se puede deshacer.",
|
||||||
|
"max_config_slots_reached": "Se alcanzó el máximo de configuraciones ({0})",
|
||||||
|
"invalid_config_format": "Formato de configuración no válido",
|
||||||
|
"invalid_json_format": "Formato JSON o estructura de configuración no válidos",
|
||||||
|
"server_tab": "Servidor",
|
||||||
|
"tools_tab": "Gestión de herramientas",
|
||||||
|
"available_tools": "Herramientas disponibles",
|
||||||
|
"scene_tools": "Herramientas de escena",
|
||||||
|
"node_tools": "Herramientas de nodo",
|
||||||
|
"component_tools": "Herramientas de componente",
|
||||||
|
"prefab_tools": "Herramientas de prefab",
|
||||||
|
"project_tools": "Herramientas de proyecto",
|
||||||
|
"debug_tools": "Herramientas de depuración",
|
||||||
|
"preferences_tools": "Preferencias",
|
||||||
|
"server_tools": "Herramientas de servidor",
|
||||||
|
"broadcast_tools": "Herramientas de difusión",
|
||||||
|
"scene_advanced_tools": "Herramientas de escena avanzadas",
|
||||||
|
"scene_view_tools": "Herramientas de vista de escena",
|
||||||
|
"reference_image_tools": "Herramientas de imagen de referencia",
|
||||||
|
"asset_advanced_tools": "Herramientas de assets avanzadas",
|
||||||
|
"validation_tools": "Herramientas de validación",
|
||||||
|
"brand_name": "LiDaxian MCP Series",
|
||||||
|
"brand_subtitle": "LiDaxian MCP Series",
|
||||||
|
"contact_email": "Correo",
|
||||||
|
"contact_wechat": "WeChat",
|
||||||
|
"contact_github": "GitHub",
|
||||||
|
"machine_id_label": "Código de máquina",
|
||||||
|
"purchase_renew": "Comprar / Renovar",
|
||||||
|
"purchase_url": "https://www.vberai.com/game-engines/cocos",
|
||||||
|
"auth_title": "Elegir método de inicio de sesión",
|
||||||
|
"auth_subtitle": "Ingrese su correo y código de activación",
|
||||||
|
"email_label": "Correo electrónico",
|
||||||
|
"activation_code_label": "Código de activación",
|
||||||
|
"email_placeholder": "Ingrese correo",
|
||||||
|
"code_placeholder": "Ingrese código (XXXXX-XXXXX-XXXXX-XXXXX-XXXXX)",
|
||||||
|
"activate_button": "Activar",
|
||||||
|
"deactivate_button": "Desactivar",
|
||||||
|
"activating": "Activando...",
|
||||||
|
"deactivating": "Desactivando...",
|
||||||
|
"license_status": "Estado de licencia",
|
||||||
|
"license_type": "Tipo de licencia",
|
||||||
|
"expiry_date": "Fecha de vencimiento",
|
||||||
|
"days_remaining": "Días restantes",
|
||||||
|
"days_unit": "d",
|
||||||
|
"permanent": "Permanente",
|
||||||
|
"expiry_warning_title": "La licencia expira pronto",
|
||||||
|
"expiry_warning_message": "Su licencia expirará en {0} días. Por favor, renueve para evitar interrupciones.",
|
||||||
|
"renew_now": "Renovar ahora",
|
||||||
|
"i_understand": "Entendido",
|
||||||
|
"activation_success": "¡Licencia activada correctamente!",
|
||||||
|
"activation_failed": "Error de activación: {0}",
|
||||||
|
"invalid_code": "Formato de código de activación no válido",
|
||||||
|
"purchase_license": "Comprar licencia",
|
||||||
|
"renew_license": "Renovar licencia",
|
||||||
|
"contact_support": "Soporte",
|
||||||
|
"language_switch": "Español",
|
||||||
|
"version_label": "Versión",
|
||||||
|
"license_required": "Se requiere licencia para iniciar el servidor",
|
||||||
|
"license_check_failed": "Error en la verificación de licencia",
|
||||||
|
|
||||||
|
"status_stopped": "Detenido",
|
||||||
|
"status_running": "En ejecución",
|
||||||
|
"status_error": "Error de conexión",
|
||||||
|
"config_tab": "Config rápida",
|
||||||
|
"status_label": "Estado",
|
||||||
|
"connections_count": "Conexiones",
|
||||||
|
"transport_protocol": "Protocolo de transporte",
|
||||||
|
"transport_type": "Tipo de transporte",
|
||||||
|
"mcp_url": "URL MCP",
|
||||||
|
"port_fallback_title": "Puerto {0} no disponible: se cambió a {1}",
|
||||||
|
"port_fallback_reason": "El puerto {0} está reservado por el sistema o bloqueado por un firewall/antivirus (habitual en Windows con Hyper-V, WSL2 o Docker). El servidor funciona con normalidad en el nuevo puerto.",
|
||||||
|
"port_fallback_resynced": "Se actualizó la configuración de: {0}. Reinicie estos clientes para que el nuevo puerto surta efecto.",
|
||||||
|
"port_fallback_no_clients": "Si ya configuró clientes de IA, vuelva a configurarlos en el nuevo puerto desde la configuración con un clic.",
|
||||||
|
"port_fallback_dismiss": "Entendido",
|
||||||
|
"port_label": "Puerto",
|
||||||
|
"auto_start_label": "Inicio auto",
|
||||||
|
"debug_log_label": "Registro de depuración",
|
||||||
|
"max_connections_label": "Conexiones máx.",
|
||||||
|
"server_settings_title": "Ajustes del servidor",
|
||||||
|
"server_status_title": "Estado del servidor",
|
||||||
|
"connection_info_title": "Info de conexión",
|
||||||
|
"tools_count": "{0} herramientas",
|
||||||
|
"tools_stats": "{0} herramientas ({1} activadas / {2} desactivadas)",
|
||||||
|
"quick_config_title": "Config rápida de clientes MCP",
|
||||||
|
"quick_config_desc": "Configurar automáticamente el servidor MCP en editores AI y herramientas CLI",
|
||||||
|
"server_info": "Info del servidor",
|
||||||
|
"server_name_label": "Nombre del servidor",
|
||||||
|
"server_address": "Dirección del servidor",
|
||||||
|
"timeout_ms": "Tiempo de espera (ms)",
|
||||||
|
"auto_config_title": "Config auto (IDE + Codex CLI)",
|
||||||
|
"add_all": "Añadir todo",
|
||||||
|
"remove_all": "Eliminar todo",
|
||||||
|
"auto_config_note": "Haga clic para modificar automáticamente los archivos de config (Cursor, Windsurf, Trae, Codex CLI)",
|
||||||
|
"configured": "Configurado",
|
||||||
|
"not_configured": "No configurado",
|
||||||
|
"auto_config_btn": "Config auto",
|
||||||
|
"delete_btn": "Eliminar",
|
||||||
|
"copy_config": "Copiar config",
|
||||||
|
"open_config": "Abrir config",
|
||||||
|
"manual_config_title": "Config manual (Claude CLI + Gemini CLI)",
|
||||||
|
"refresh_commands": "Actualizar comandos",
|
||||||
|
"manual_config_note": "Las siguientes herramientas CLI requieren ejecución manual en terminal",
|
||||||
|
"config_scope": "Ámbito de config:",
|
||||||
|
"scope_user": "Nivel de usuario",
|
||||||
|
"scope_project": "Nivel de proyecto",
|
||||||
|
"operation_log": "Registro de operaciones",
|
||||||
|
"no_logs": "Sin registros",
|
||||||
|
"copyright_line": "© 2025 LiDaxian - Cocos Creator MCP Server v{0}",
|
||||||
|
"protocol_line": "Basado en el protocolo Streamable HTTP (MCP 2025-03-26)",
|
||||||
|
"tools_copyright_line1": "Sistema de herramientas - 13 herramientas intent-level, 150+ operaciones de editor, despacho de código de acción",
|
||||||
|
"config_copyright_line1": "Config rápida - Soporta Cursor, Windsurf, Trae, Codex CLI, Claude CLI, Gemini CLI",
|
||||||
|
"email_required": "Por favor ingrese correo y código de activación",
|
||||||
|
"permanent_label": "Permanente",
|
||||||
|
"logout_button": "Cerrar sesión",
|
||||||
|
"invite_code_label": "Código de invitación",
|
||||||
|
"copy_button": "Copiar",
|
||||||
|
"change_code_button": "Cambiar código",
|
||||||
|
"change_code_title": "Cambiar código de activación",
|
||||||
|
"change_code_desc": "Ingrese su nuevo código de activación. El enlace del dispositivo actual se liberará primero.",
|
||||||
|
"confirm_change": "Confirmar",
|
||||||
|
"cancel": "Cancelar",
|
||||||
|
"code_required": "Por favor ingrese un código de activación",
|
||||||
|
"expiry_inline_warning": "¡Expira pronto!",
|
||||||
|
"store_purchase_hint": "Si compró este plugin en Cocos Store, contácteme para obtener su código de activación.",
|
||||||
|
"license_expired_title": "Licencia expirada",
|
||||||
|
"license_expired_message": "Su licencia ha expirado y el servidor no puede iniciarse. Renueve o reactive para continuar.",
|
||||||
|
"go_to_login": "Ir a inicio de sesión",
|
||||||
|
|
||||||
|
"log_load_config_failed": "Error al cargar el estado de config: {0}",
|
||||||
|
"log_gen_cli_failed": "Error al generar comandos CLI: {0}",
|
||||||
|
"log_add_client_failed": "Error al añadir a {0}: {1}",
|
||||||
|
"log_remove_client_failed": "Error al eliminar de {0}: {1}",
|
||||||
|
"quick_config_dialog_title": "Configuración con un clic",
|
||||||
|
"config_operation_failed": "Error en la operación de configuración",
|
||||||
|
"config_file_not_found_changed_path": "Error de configuración: no se encontró el archivo de configuración. Es posible que {0} se haya instalado en una ruta personalizada.",
|
||||||
|
"log_batch_add_failed": "Error en la adición masiva: {0}",
|
||||||
|
"log_batch_remove_failed": "Error en la eliminación masiva: {0}",
|
||||||
|
"log_copied_config": "Config de {0} copiada al portapapeles",
|
||||||
|
"log_gen_config_failed": "Error al generar la config",
|
||||||
|
"log_copy_config_failed": "Error al copiar la config: {0}",
|
||||||
|
"log_copied_command": "Comando {0} copiado al portapapeles",
|
||||||
|
"log_copy_command_failed": "Error al copiar el comando: {0}",
|
||||||
|
"log_opened_config_file": "Archivo de config abierto: {0}",
|
||||||
|
"log_open_config_failed": "Error al abrir archivo de config",
|
||||||
|
"log_open_config_error": "Error al abrir archivo de config: {0}",
|
||||||
|
|
||||||
|
"update_available_title": "Actualización disponible",
|
||||||
|
"update_new_version": "¡La nueva versión v{0} está disponible!",
|
||||||
|
"update_now": "Actualizar ahora",
|
||||||
|
"update_not_now": "Ahora no",
|
||||||
|
"update_skip_version": "Omitir v{0}",
|
||||||
|
"check_for_updates": "Buscar actualizaciones",
|
||||||
|
"update_checking": "Comprobando...",
|
||||||
|
"update_no_update": "Está usando la última versión",
|
||||||
|
"update_urgency_critical": "Actualización crítica",
|
||||||
|
"update_urgency_high": "Actualización importante",
|
||||||
|
"update_current_version": "Versión actual: v{0}",
|
||||||
|
"update_changelog": "Registro de cambios",
|
||||||
|
"auth_choose_subtitle": "Elige un método de inicio de sesión para continuar",
|
||||||
|
"update_changelog_empty": "No se proporcionaron notas de la versión para esta versión.",
|
||||||
|
"subscription_login_title": "Cuenta de suscripción de VberAI",
|
||||||
|
"subscription_password_label": "Contraseña",
|
||||||
|
"subscription_password_placeholder": "Introduce la contraseña",
|
||||||
|
"subscription_login_button": "Iniciar sesión con VberAI",
|
||||||
|
"subscription_google_button": "Iniciar sesión con Google",
|
||||||
|
"subscription_google_pending": "Navegador abierto. Completa la autorización de Google y vuelve a Cocos Creator.",
|
||||||
|
"subscription_google_pending_button": "Esperando la autorización de Google...",
|
||||||
|
"subscription_register_button": "Crear una cuenta de VberAI",
|
||||||
|
"subscription_logging_in": "Iniciando sesión...",
|
||||||
|
"subscription_credentials_required": "Introduce el correo electrónico y la contraseña",
|
||||||
|
"subscription_login_success": "Sesión iniciada en la cuenta de suscripción",
|
||||||
|
"subscription_login_failed": "Error al iniciar sesión en la suscripción",
|
||||||
|
"subscription_not_entitled": "Tu suscripción actual no incluye Cocos 3.x MCP. Actualiza para continuar.",
|
||||||
|
"subscription_upgrade_to": "Actualiza a «{0}» para usar este producto",
|
||||||
|
"subscription_upgrade_action": "Mejorar suscripción",
|
||||||
|
"subscription_logged_in": "Cuenta con sesión iniciada",
|
||||||
|
"subscription_choose_button": "Cuenta de suscripción de VberAI",
|
||||||
|
"license_choose_button": "Código de activación",
|
||||||
|
"subscription_method_label": "Suscripción",
|
||||||
|
"back_to_login_choices": "← Elegir otro método de inicio de sesión",
|
||||||
|
"subscription_signed_out_other_device": "Sesión cerrada: tu cuenta se usó en otro dispositivo.",
|
||||||
|
"subscription_google_cancelled": "Se canceló el inicio de sesión con Google.",
|
||||||
|
"subscription_request_timeout": "Se agotó el tiempo de la solicitud. Comprueba tu red e inténtalo de nuevo.",
|
||||||
|
"subscription_network_error": "Falló la conexión de red. Comprueba tu red e inténtalo de nuevo.",
|
||||||
|
"subscription_server_error": "El servidor devolvió una respuesta no válida. Inténtalo más tarde.",
|
||||||
|
"subscription_google_authorization_failed": "No se completó la autorización de Google. Inténtalo de nuevo."
|
||||||
|
};
|
||||||
@@ -0,0 +1,243 @@
|
|||||||
|
"use strict";
|
||||||
|
module.exports = {
|
||||||
|
"extension_name": "Cocos MCP Serveur",
|
||||||
|
"description": "Serveur AI MCP pour Cocos Creator 3.8",
|
||||||
|
"panel_title": "Serveur MCP",
|
||||||
|
"open_panel": "Ouvrir le panneau MCP",
|
||||||
|
"start_server": "Démarrer le serveur",
|
||||||
|
"stop_server": "Arrêter le serveur",
|
||||||
|
"server_status": "État du serveur",
|
||||||
|
"port": "Port",
|
||||||
|
"settings": "Paramètres",
|
||||||
|
"connected": "Connecté",
|
||||||
|
"disconnected": "Déconnecté",
|
||||||
|
"server_running": "Le serveur fonctionne sur le port {0}",
|
||||||
|
"server_stopped": "Le serveur est arrêté",
|
||||||
|
"auto_start": "Démarrage automatique",
|
||||||
|
"debug_log": "Journal de débogage",
|
||||||
|
"max_connections": "Connexions max",
|
||||||
|
"connection_info": "Infos de connexion",
|
||||||
|
"http_url": "URL HTTP",
|
||||||
|
"copy": "Copier",
|
||||||
|
"save_settings": "Enregistrer",
|
||||||
|
"settings_saved": "Paramètres enregistrés avec succès",
|
||||||
|
"server_started": "Serveur MCP démarré",
|
||||||
|
"server_stopped_msg": "Serveur MCP arrêté",
|
||||||
|
"failed_to_start": "Échec du démarrage du serveur",
|
||||||
|
"failed_to_stop": "Échec de l'arrêt du serveur",
|
||||||
|
"failed_to_save": "Échec de l'enregistrement",
|
||||||
|
"url_copied": "URL HTTP copiée dans le presse-papiers",
|
||||||
|
"tool_manager": "Gestionnaire d'outils",
|
||||||
|
"open_tool_manager": "Ouvrir le gestionnaire d'outils",
|
||||||
|
"create_config": "Créer une configuration",
|
||||||
|
"edit_config": "Modifier la configuration",
|
||||||
|
"delete_config": "Supprimer la configuration",
|
||||||
|
"import_config": "Importer la configuration",
|
||||||
|
"export_config": "Exporter la configuration",
|
||||||
|
"apply_config": "Appliquer la configuration",
|
||||||
|
"select_all": "Tout sélectionner",
|
||||||
|
"deselect_all": "Tout désélectionner",
|
||||||
|
"save_changes": "Enregistrer les modifications",
|
||||||
|
"config_name": "Nom de configuration",
|
||||||
|
"config_description": "Description de configuration",
|
||||||
|
"current_config": "Configuration actuelle",
|
||||||
|
"tool_management": "Gestion des outils",
|
||||||
|
"total_tools": "Total des outils",
|
||||||
|
"enabled_tools": "Activés",
|
||||||
|
"disabled_tools": "Désactivés",
|
||||||
|
"no_config_selected": "Aucune configuration sélectionnée",
|
||||||
|
"select_config_first": "Veuillez d'abord sélectionner ou créer une configuration",
|
||||||
|
"config_created": "Configuration créée avec succès",
|
||||||
|
"config_updated": "Configuration mise à jour avec succès",
|
||||||
|
"config_deleted": "Configuration supprimée avec succès",
|
||||||
|
"config_applied": "Configuration appliquée avec succès",
|
||||||
|
"config_exported": "Configuration exportée avec succès",
|
||||||
|
"config_imported": "Configuration importée avec succès",
|
||||||
|
"confirm_delete": "Confirmer la suppression",
|
||||||
|
"delete_config_confirm": "Voulez-vous vraiment supprimer la configuration \"{0}\" ? Cette action est irréversible.",
|
||||||
|
"max_config_slots_reached": "Nombre maximal de configurations atteint ({0})",
|
||||||
|
"invalid_config_format": "Format de configuration invalide",
|
||||||
|
"invalid_json_format": "Format JSON ou structure de configuration invalide",
|
||||||
|
"server_tab": "Serveur",
|
||||||
|
"tools_tab": "Gestion des outils",
|
||||||
|
"available_tools": "Outils disponibles",
|
||||||
|
"scene_tools": "Outils de scène",
|
||||||
|
"node_tools": "Outils de nœud",
|
||||||
|
"component_tools": "Outils de composant",
|
||||||
|
"prefab_tools": "Outils de prefab",
|
||||||
|
"project_tools": "Outils de projet",
|
||||||
|
"debug_tools": "Outils de débogage",
|
||||||
|
"preferences_tools": "Préférences",
|
||||||
|
"server_tools": "Outils serveur",
|
||||||
|
"broadcast_tools": "Outils de diffusion",
|
||||||
|
"scene_advanced_tools": "Outils de scène avancés",
|
||||||
|
"scene_view_tools": "Outils de vue de scène",
|
||||||
|
"reference_image_tools": "Outils d'image de référence",
|
||||||
|
"asset_advanced_tools": "Outils d'assets avancés",
|
||||||
|
"validation_tools": "Outils de validation",
|
||||||
|
"brand_name": "LiDaxian MCP Series",
|
||||||
|
"brand_subtitle": "LiDaxian MCP Series",
|
||||||
|
"contact_email": "E-mail",
|
||||||
|
"contact_wechat": "WeChat",
|
||||||
|
"contact_github": "GitHub",
|
||||||
|
"machine_id_label": "Code machine",
|
||||||
|
"purchase_renew": "Acheter / Renouveler",
|
||||||
|
"purchase_url": "https://www.vberai.com/game-engines/cocos",
|
||||||
|
"auth_title": "Choisir une méthode de connexion",
|
||||||
|
"auth_subtitle": "Entrez votre e-mail et votre code d'activation",
|
||||||
|
"email_label": "Adresse e-mail",
|
||||||
|
"activation_code_label": "Code d'activation",
|
||||||
|
"email_placeholder": "Entrez l'e-mail",
|
||||||
|
"code_placeholder": "Entrez le code (XXXXX-XXXXX-XXXXX-XXXXX-XXXXX)",
|
||||||
|
"activate_button": "Activer",
|
||||||
|
"deactivate_button": "Désactiver",
|
||||||
|
"activating": "Activation...",
|
||||||
|
"deactivating": "Désactivation...",
|
||||||
|
"license_status": "État de la licence",
|
||||||
|
"license_type": "Type de licence",
|
||||||
|
"expiry_date": "Date d'expiration",
|
||||||
|
"days_remaining": "Jours restants",
|
||||||
|
"days_unit": "j",
|
||||||
|
"permanent": "Permanent",
|
||||||
|
"expiry_warning_title": "Licence bientôt expirée",
|
||||||
|
"expiry_warning_message": "Votre licence expirera dans {0} jours. Veuillez renouveler pour éviter toute interruption.",
|
||||||
|
"renew_now": "Renouveler",
|
||||||
|
"i_understand": "J'ai compris",
|
||||||
|
"activation_success": "Licence activée avec succès !",
|
||||||
|
"activation_failed": "Échec de l'activation : {0}",
|
||||||
|
"invalid_code": "Format de code d'activation invalide",
|
||||||
|
"purchase_license": "Acheter une licence",
|
||||||
|
"renew_license": "Renouveler la licence",
|
||||||
|
"contact_support": "Support",
|
||||||
|
"language_switch": "Français",
|
||||||
|
"version_label": "Version",
|
||||||
|
"license_required": "Une licence est requise pour démarrer le serveur",
|
||||||
|
"license_check_failed": "Échec de la vérification de licence",
|
||||||
|
|
||||||
|
"status_stopped": "Arrêté",
|
||||||
|
"status_running": "En cours",
|
||||||
|
"status_error": "Échec de connexion",
|
||||||
|
"config_tab": "Config rapide",
|
||||||
|
"status_label": "État",
|
||||||
|
"connections_count": "Connexions",
|
||||||
|
"transport_protocol": "Protocole de transport",
|
||||||
|
"transport_type": "Type de transport",
|
||||||
|
"mcp_url": "URL MCP",
|
||||||
|
"port_fallback_title": "Port {0} indisponible — basculé vers {1}",
|
||||||
|
"port_fallback_reason": "Le port {0} est réservé par le système ou bloqué par un pare-feu/antivirus (fréquent sous Windows avec Hyper-V, WSL2 ou Docker). Le serveur fonctionne normalement sur le nouveau port.",
|
||||||
|
"port_fallback_resynced": "Configuration mise à jour pour : {0}. Redémarrez ces clients pour que le nouveau port prenne effet.",
|
||||||
|
"port_fallback_no_clients": "Si vous avez déjà configuré des clients IA, reconfigurez-les sur le nouveau port via la configuration en un clic.",
|
||||||
|
"port_fallback_dismiss": "Compris",
|
||||||
|
"port_label": "Port",
|
||||||
|
"auto_start_label": "Démarrage auto",
|
||||||
|
"debug_log_label": "Journal de débogage",
|
||||||
|
"max_connections_label": "Connexions max",
|
||||||
|
"server_settings_title": "Paramètres du serveur",
|
||||||
|
"server_status_title": "État du serveur",
|
||||||
|
"connection_info_title": "Infos de connexion",
|
||||||
|
"tools_count": "{0} outils",
|
||||||
|
"tools_stats": "{0} outils ({1} activés / {2} désactivés)",
|
||||||
|
"quick_config_title": "Config rapide des clients MCP",
|
||||||
|
"quick_config_desc": "Configurer automatiquement le serveur MCP dans les éditeurs AI et outils CLI",
|
||||||
|
"server_info": "Infos serveur",
|
||||||
|
"server_name_label": "Nom du serveur",
|
||||||
|
"server_address": "Adresse du serveur",
|
||||||
|
"timeout_ms": "Délai d'attente (ms)",
|
||||||
|
"auto_config_title": "Config auto (IDE + Codex CLI)",
|
||||||
|
"add_all": "Tout ajouter",
|
||||||
|
"remove_all": "Tout supprimer",
|
||||||
|
"auto_config_note": "Cliquez pour modifier automatiquement les fichiers de config (Cursor, Windsurf, Trae, Codex CLI)",
|
||||||
|
"configured": "Configuré",
|
||||||
|
"not_configured": "Non configuré",
|
||||||
|
"auto_config_btn": "Config auto",
|
||||||
|
"delete_btn": "Supprimer",
|
||||||
|
"copy_config": "Copier la config",
|
||||||
|
"open_config": "Ouvrir la config",
|
||||||
|
"manual_config_title": "Config manuelle (Claude CLI + Gemini CLI)",
|
||||||
|
"refresh_commands": "Actualiser les commandes",
|
||||||
|
"manual_config_note": "Les outils CLI suivants nécessitent une exécution manuelle dans le terminal",
|
||||||
|
"config_scope": "Portée de config :",
|
||||||
|
"scope_user": "Niveau utilisateur",
|
||||||
|
"scope_project": "Niveau projet",
|
||||||
|
"operation_log": "Journal des opérations",
|
||||||
|
"no_logs": "Aucun enregistrement",
|
||||||
|
"copyright_line": "© 2025 LiDaxian - Cocos Creator MCP Server v{0}",
|
||||||
|
"protocol_line": "Basé sur le protocole Streamable HTTP (MCP 2025-03-26)",
|
||||||
|
"tools_copyright_line1": "Système d'outils - 13 outils intent-level, 150+ opérations éditeur, dispatch par code d'action",
|
||||||
|
"config_copyright_line1": "Config rapide - Supporte Cursor, Windsurf, Trae, Codex CLI, Claude CLI, Gemini CLI",
|
||||||
|
"email_required": "Veuillez entrer l'e-mail et le code d'activation",
|
||||||
|
"permanent_label": "Permanent",
|
||||||
|
"logout_button": "Déconnexion",
|
||||||
|
"invite_code_label": "Code d'invitation",
|
||||||
|
"copy_button": "Copier",
|
||||||
|
"change_code_button": "Changer le code",
|
||||||
|
"change_code_title": "Changer le code d'activation",
|
||||||
|
"change_code_desc": "Entrez votre nouveau code d'activation. La liaison de l'appareil actuel sera d'abord libérée.",
|
||||||
|
"confirm_change": "Confirmer",
|
||||||
|
"cancel": "Annuler",
|
||||||
|
"code_required": "Veuillez entrer un code d'activation",
|
||||||
|
"expiry_inline_warning": "Expiration imminente !",
|
||||||
|
"store_purchase_hint": "Si vous avez acheté ce plugin sur Cocos Store, veuillez me contacter pour obtenir votre code d'activation.",
|
||||||
|
"license_expired_title": "Licence expirée",
|
||||||
|
"license_expired_message": "Votre licence a expiré et le serveur ne peut pas être démarré. Veuillez renouveler ou réactiver pour continuer.",
|
||||||
|
"go_to_login": "Se connecter",
|
||||||
|
|
||||||
|
"log_load_config_failed": "Échec du chargement de la config : {0}",
|
||||||
|
"log_gen_cli_failed": "Échec de la génération des commandes CLI : {0}",
|
||||||
|
"log_add_client_failed": "Échec de l'ajout à {0} : {1}",
|
||||||
|
"log_remove_client_failed": "Échec de la suppression de {0} : {1}",
|
||||||
|
"quick_config_dialog_title": "Configuration en un clic",
|
||||||
|
"config_operation_failed": "Échec de l'opération de configuration",
|
||||||
|
"config_file_not_found_changed_path": "Échec de la configuration : fichier de configuration introuvable. {0} a peut-être été installé dans un chemin personnalisé.",
|
||||||
|
"log_batch_add_failed": "Échec de l'ajout groupé : {0}",
|
||||||
|
"log_batch_remove_failed": "Échec de la suppression groupée : {0}",
|
||||||
|
"log_copied_config": "Config {0} copiée dans le presse-papiers",
|
||||||
|
"log_gen_config_failed": "Échec de la génération de la config",
|
||||||
|
"log_copy_config_failed": "Échec de la copie de la config : {0}",
|
||||||
|
"log_copied_command": "Commande {0} copiée dans le presse-papiers",
|
||||||
|
"log_copy_command_failed": "Échec de la copie de la commande : {0}",
|
||||||
|
"log_opened_config_file": "Fichier de config ouvert : {0}",
|
||||||
|
"log_open_config_failed": "Échec de l'ouverture du fichier de config",
|
||||||
|
"log_open_config_error": "Échec de l'ouverture du fichier de config : {0}",
|
||||||
|
|
||||||
|
"update_available_title": "Mise à jour disponible",
|
||||||
|
"update_new_version": "La nouvelle version v{0} est disponible !",
|
||||||
|
"update_now": "Mettre à jour",
|
||||||
|
"update_not_now": "Plus tard",
|
||||||
|
"update_skip_version": "Ignorer v{0}",
|
||||||
|
"check_for_updates": "Vérifier les mises à jour",
|
||||||
|
"update_checking": "Vérification...",
|
||||||
|
"update_no_update": "Vous utilisez la dernière version",
|
||||||
|
"update_urgency_critical": "Mise à jour critique",
|
||||||
|
"update_urgency_high": "Mise à jour importante",
|
||||||
|
"update_current_version": "Version actuelle : v{0}",
|
||||||
|
"update_changelog": "Journal des modifications",
|
||||||
|
"auth_choose_subtitle": "Choisissez une méthode de connexion pour continuer",
|
||||||
|
"update_changelog_empty": "Aucune note de version n'a été fournie pour cette version.",
|
||||||
|
"subscription_login_title": "Compte d'abonnement VberAI",
|
||||||
|
"subscription_password_label": "Mot de passe",
|
||||||
|
"subscription_password_placeholder": "Saisissez le mot de passe",
|
||||||
|
"subscription_login_button": "Se connecter avec VberAI",
|
||||||
|
"subscription_google_button": "Se connecter avec Google",
|
||||||
|
"subscription_google_pending": "Navigateur ouvert. Terminez l'autorisation Google, puis revenez à Cocos Creator.",
|
||||||
|
"subscription_google_pending_button": "En attente de l'autorisation Google...",
|
||||||
|
"subscription_register_button": "Créer un compte VberAI",
|
||||||
|
"subscription_logging_in": "Connexion en cours...",
|
||||||
|
"subscription_credentials_required": "Veuillez saisir l'e-mail et le mot de passe",
|
||||||
|
"subscription_login_success": "Connecté au compte d'abonnement",
|
||||||
|
"subscription_login_failed": "Échec de la connexion à l'abonnement",
|
||||||
|
"subscription_not_entitled": "Votre abonnement actuel n'inclut pas Cocos 3.x MCP. Effectuez une mise à niveau pour continuer.",
|
||||||
|
"subscription_upgrade_to": "Passez à « {0} » pour utiliser ce produit",
|
||||||
|
"subscription_upgrade_action": "Mettre à niveau l'abonnement",
|
||||||
|
"subscription_logged_in": "Compte connecté",
|
||||||
|
"subscription_choose_button": "Compte d'abonnement VberAI",
|
||||||
|
"license_choose_button": "Code d'activation",
|
||||||
|
"subscription_method_label": "Abonnement",
|
||||||
|
"back_to_login_choices": "← Choisir une autre méthode de connexion",
|
||||||
|
"subscription_signed_out_other_device": "Déconnecté : votre compte a été utilisé sur un autre appareil.",
|
||||||
|
"subscription_google_cancelled": "La connexion Google a été annulée.",
|
||||||
|
"subscription_request_timeout": "La requête a expiré. Vérifiez votre réseau et réessayez.",
|
||||||
|
"subscription_network_error": "Échec de la connexion réseau. Vérifiez votre réseau et réessayez.",
|
||||||
|
"subscription_server_error": "Le serveur a renvoyé une réponse non valide. Réessayez plus tard.",
|
||||||
|
"subscription_google_authorization_failed": "L'autorisation Google n'a pas été effectuée. Veuillez réessayer."
|
||||||
|
};
|
||||||
@@ -0,0 +1,243 @@
|
|||||||
|
"use strict";
|
||||||
|
module.exports = {
|
||||||
|
"extension_name": "Cocos MCP サーバー",
|
||||||
|
"description": "Cocos Creator 3.8 用 AI MCP サーバー",
|
||||||
|
"panel_title": "MCP サーバー",
|
||||||
|
"open_panel": "MCP パネルを開く",
|
||||||
|
"start_server": "サーバー起動",
|
||||||
|
"stop_server": "サーバー停止",
|
||||||
|
"server_status": "サーバー状態",
|
||||||
|
"port": "ポート",
|
||||||
|
"settings": "設定",
|
||||||
|
"connected": "接続済み",
|
||||||
|
"disconnected": "未接続",
|
||||||
|
"server_running": "サーバーはポート {0} で実行中",
|
||||||
|
"server_stopped": "サーバーは停止しました",
|
||||||
|
"auto_start": "自動起動",
|
||||||
|
"debug_log": "デバッグログ",
|
||||||
|
"max_connections": "最大接続数",
|
||||||
|
"connection_info": "接続情報",
|
||||||
|
"http_url": "HTTP URL",
|
||||||
|
"copy": "コピー",
|
||||||
|
"save_settings": "設定を保存",
|
||||||
|
"settings_saved": "設定の保存に成功しました",
|
||||||
|
"server_started": "MCP サーバーが起動しました",
|
||||||
|
"server_stopped_msg": "MCP サーバーが停止しました",
|
||||||
|
"failed_to_start": "サーバーの起動に失敗しました",
|
||||||
|
"failed_to_stop": "サーバーの停止に失敗しました",
|
||||||
|
"failed_to_save": "設定の保存に失敗しました",
|
||||||
|
"url_copied": "HTTP URL をクリップボードにコピーしました",
|
||||||
|
"tool_manager": "ツールマネージャー",
|
||||||
|
"open_tool_manager": "ツールマネージャーを開く",
|
||||||
|
"create_config": "設定を作成",
|
||||||
|
"edit_config": "設定を編集",
|
||||||
|
"delete_config": "設定を削除",
|
||||||
|
"import_config": "設定をインポート",
|
||||||
|
"export_config": "設定をエクスポート",
|
||||||
|
"apply_config": "設定を適用",
|
||||||
|
"select_all": "全選択",
|
||||||
|
"deselect_all": "全解除",
|
||||||
|
"save_changes": "変更を保存",
|
||||||
|
"config_name": "設定名",
|
||||||
|
"config_description": "設定の説明",
|
||||||
|
"current_config": "現在の設定",
|
||||||
|
"tool_management": "ツール管理",
|
||||||
|
"total_tools": "ツール合計",
|
||||||
|
"enabled_tools": "有効",
|
||||||
|
"disabled_tools": "無効",
|
||||||
|
"no_config_selected": "設定が選択されていません",
|
||||||
|
"select_config_first": "先に設定を選択するか、新しく作成してください",
|
||||||
|
"config_created": "設定の作成に成功しました",
|
||||||
|
"config_updated": "設定の更新に成功しました",
|
||||||
|
"config_deleted": "設定の削除に成功しました",
|
||||||
|
"config_applied": "設定の適用に成功しました",
|
||||||
|
"config_exported": "設定のエクスポートに成功しました",
|
||||||
|
"config_imported": "設定のインポートに成功しました",
|
||||||
|
"confirm_delete": "削除の確認",
|
||||||
|
"delete_config_confirm": "設定 \"{0}\" を削除してもよろしいですか?この操作は元に戻せません。",
|
||||||
|
"max_config_slots_reached": "設定スロットの上限に達しました ({0})",
|
||||||
|
"invalid_config_format": "無効な設定形式",
|
||||||
|
"invalid_json_format": "無効なJSON形式または設定構造",
|
||||||
|
"server_tab": "サーバー",
|
||||||
|
"tools_tab": "ツール管理",
|
||||||
|
"available_tools": "利用可能なツール",
|
||||||
|
"scene_tools": "シーンツール",
|
||||||
|
"node_tools": "ノードツール",
|
||||||
|
"component_tools": "コンポーネントツール",
|
||||||
|
"prefab_tools": "プレハブツール",
|
||||||
|
"project_tools": "プロジェクトツール",
|
||||||
|
"debug_tools": "デバッグツール",
|
||||||
|
"preferences_tools": "環境設定",
|
||||||
|
"server_tools": "サーバーツール",
|
||||||
|
"broadcast_tools": "ブロードキャストツール",
|
||||||
|
"scene_advanced_tools": "高度なシーンツール",
|
||||||
|
"scene_view_tools": "シーンビューツール",
|
||||||
|
"reference_image_tools": "参照画像ツール",
|
||||||
|
"asset_advanced_tools": "高度なアセットツール",
|
||||||
|
"validation_tools": "検証ツール",
|
||||||
|
"brand_name": "LiDaxian MCP シリーズ",
|
||||||
|
"brand_subtitle": "LiDaxian MCP Series",
|
||||||
|
"contact_email": "メール",
|
||||||
|
"contact_wechat": "WeChat",
|
||||||
|
"contact_github": "GitHub",
|
||||||
|
"machine_id_label": "マシンID",
|
||||||
|
"purchase_renew": "購入 / 更新",
|
||||||
|
"purchase_url": "https://www.vberai.com/game-engines/cocos",
|
||||||
|
"auth_title": "ログイン方法を選択",
|
||||||
|
"auth_subtitle": "メールアドレスとアクティベーションコードを入力してください",
|
||||||
|
"email_label": "メールアドレス",
|
||||||
|
"activation_code_label": "アクティベーションコード",
|
||||||
|
"email_placeholder": "メールを入力",
|
||||||
|
"code_placeholder": "コードを入力 (XXXXX-XXXXX-XXXXX-XXXXX-XXXXX)",
|
||||||
|
"activate_button": "アクティベート",
|
||||||
|
"deactivate_button": "無効化",
|
||||||
|
"activating": "アクティベート中...",
|
||||||
|
"deactivating": "無効化中...",
|
||||||
|
"license_status": "ライセンス状態",
|
||||||
|
"license_type": "ライセンス種別",
|
||||||
|
"expiry_date": "有効期限",
|
||||||
|
"days_remaining": "残り日数",
|
||||||
|
"days_unit": "日",
|
||||||
|
"permanent": "永久",
|
||||||
|
"expiry_warning_title": "ライセンスの有効期限が近づいています",
|
||||||
|
"expiry_warning_message": "ライセンスはあと {0} 日で期限切れになります。サービスの中断を防ぐため、更新してください。",
|
||||||
|
"renew_now": "今すぐ更新",
|
||||||
|
"i_understand": "了解しました",
|
||||||
|
"activation_success": "ライセンスのアクティベーションに成功しました!",
|
||||||
|
"activation_failed": "アクティベーション失敗: {0}",
|
||||||
|
"invalid_code": "無効なアクティベーションコード形式",
|
||||||
|
"purchase_license": "ライセンスを購入",
|
||||||
|
"renew_license": "ライセンスを更新",
|
||||||
|
"contact_support": "サポート",
|
||||||
|
"language_switch": "日本語",
|
||||||
|
"version_label": "バージョン",
|
||||||
|
"license_required": "サーバーの起動にはライセンスが必要です",
|
||||||
|
"license_check_failed": "ライセンスチェックに失敗しました",
|
||||||
|
|
||||||
|
"status_stopped": "停止中",
|
||||||
|
"status_running": "実行中",
|
||||||
|
"status_error": "接続失敗",
|
||||||
|
"config_tab": "クイック設定",
|
||||||
|
"status_label": "状態",
|
||||||
|
"connections_count": "接続数",
|
||||||
|
"transport_protocol": "トランスポートプロトコル",
|
||||||
|
"transport_type": "トランスポートタイプ",
|
||||||
|
"mcp_url": "MCP URL",
|
||||||
|
"port_fallback_title": "ポート {0} が使用できないため、{1} に切り替えました",
|
||||||
|
"port_fallback_reason": "ポート {0} はシステムに予約されているか、ファイアウォール/ウイルス対策ソフトにブロックされています(Hyper-V、WSL2、Docker を導入した Windows でよく起こります)。サーバーは新しいポートで正常に動作しています。",
|
||||||
|
"port_fallback_resynced": "次のクライアントの設定を更新しました:{0}。新しいポートを有効にするには再起動してください。",
|
||||||
|
"port_fallback_no_clients": "AI クライアントを設定済みの場合は、「ワンクリック設定」で新しいポートに設定し直してください。",
|
||||||
|
"port_fallback_dismiss": "了解",
|
||||||
|
"port_label": "ポート",
|
||||||
|
"auto_start_label": "自動起動",
|
||||||
|
"debug_log_label": "デバッグログ",
|
||||||
|
"max_connections_label": "最大接続数",
|
||||||
|
"server_settings_title": "サーバー設定",
|
||||||
|
"server_status_title": "サーバー状態",
|
||||||
|
"connection_info_title": "接続情報",
|
||||||
|
"tools_count": "{0} ツール",
|
||||||
|
"tools_stats": "{0} ツール ({1} 有効 / {2} 無効)",
|
||||||
|
"quick_config_title": "MCP クライアントのクイック設定",
|
||||||
|
"quick_config_desc": "現在のMCPサーバーを主要AIエディターやCLIツールに自動設定します",
|
||||||
|
"server_info": "サーバー情報",
|
||||||
|
"server_name_label": "サーバー名",
|
||||||
|
"server_address": "サーバーアドレス",
|
||||||
|
"timeout_ms": "タイムアウト (ms)",
|
||||||
|
"auto_config_title": "自動設定 (IDE エディター + Codex CLI)",
|
||||||
|
"add_all": "すべて追加",
|
||||||
|
"remove_all": "すべて削除",
|
||||||
|
"auto_config_note": "ボタンをクリックして設定ファイルを自動変更 (Cursor, Windsurf, Trae, Codex CLI)",
|
||||||
|
"configured": "設定済み",
|
||||||
|
"not_configured": "未設定",
|
||||||
|
"auto_config_btn": "自動設定",
|
||||||
|
"delete_btn": "削除",
|
||||||
|
"copy_config": "設定をコピー",
|
||||||
|
"open_config": "設定を開く",
|
||||||
|
"manual_config_title": "手動設定 (Claude CLI + Gemini CLI)",
|
||||||
|
"refresh_commands": "コマンドを更新",
|
||||||
|
"manual_config_note": "以下のCLIツールはターミナルで手動実行が必要です",
|
||||||
|
"config_scope": "設定範囲:",
|
||||||
|
"scope_user": "ユーザーレベル",
|
||||||
|
"scope_project": "プロジェクトレベル",
|
||||||
|
"operation_log": "操作ログ",
|
||||||
|
"no_logs": "操作記録なし",
|
||||||
|
"copyright_line": "© 2025 LiDaxian - Cocos Creator MCP Server v{0}",
|
||||||
|
"protocol_line": "Streamable HTTP プロトコル (MCP 2025-03-26) 準拠",
|
||||||
|
"tools_copyright_line1": "ツールシステム - 13のインテントレベルツール、150+のエディター操作、アクションコードディスパッチ",
|
||||||
|
"config_copyright_line1": "クイック設定 - Cursor, Windsurf, Trae, Codex CLI, Claude CLI, Gemini CLI 対応",
|
||||||
|
"email_required": "メールとアクティベーションコードを入力してください",
|
||||||
|
"permanent_label": "永久",
|
||||||
|
"logout_button": "ログアウト",
|
||||||
|
"invite_code_label": "招待コード",
|
||||||
|
"copy_button": "コピー",
|
||||||
|
"change_code_button": "コード変更",
|
||||||
|
"change_code_title": "アクティベーションコードの変更",
|
||||||
|
"change_code_desc": "新しいアクティベーションコードを入力してください。現在のデバイスバインドは先に解除されます。",
|
||||||
|
"confirm_change": "確認",
|
||||||
|
"cancel": "キャンセル",
|
||||||
|
"code_required": "アクティベーションコードを入力してください",
|
||||||
|
"expiry_inline_warning": "まもなく期限切れ!",
|
||||||
|
"store_purchase_hint": "Cocos Store で本プラグインを購入済みの場合は、アクティベーションコードの取得にご連絡ください。",
|
||||||
|
"license_expired_title": "ライセンスの有効期限切れ",
|
||||||
|
"license_expired_message": "ライセンスの有効期限が切れたため、サーバーを起動できません。続行するには更新または再アクティベートしてください。",
|
||||||
|
"go_to_login": "ログインへ",
|
||||||
|
|
||||||
|
"log_load_config_failed": "設定状態の読み込みに失敗: {0}",
|
||||||
|
"log_gen_cli_failed": "CLIコマンドの生成に失敗: {0}",
|
||||||
|
"log_add_client_failed": "{0} への追加に失敗: {1}",
|
||||||
|
"log_remove_client_failed": "{0} からの削除に失敗: {1}",
|
||||||
|
"quick_config_dialog_title": "ワンクリック設定",
|
||||||
|
"config_operation_failed": "設定操作に失敗しました",
|
||||||
|
"config_file_not_found_changed_path": "設定に失敗しました: 設定ファイルが見つかりません。{0} がカスタムパスにインストールされている可能性があります。",
|
||||||
|
"log_batch_add_failed": "一括追加に失敗: {0}",
|
||||||
|
"log_batch_remove_failed": "一括削除に失敗: {0}",
|
||||||
|
"log_copied_config": "{0} の設定をクリップボードにコピーしました",
|
||||||
|
"log_gen_config_failed": "設定の生成に失敗しました",
|
||||||
|
"log_copy_config_failed": "設定のコピーに失敗: {0}",
|
||||||
|
"log_copied_command": "{0} コマンドをクリップボードにコピーしました",
|
||||||
|
"log_copy_command_failed": "コマンドのコピーに失敗: {0}",
|
||||||
|
"log_opened_config_file": "設定ファイルを開きました: {0}",
|
||||||
|
"log_open_config_failed": "設定ファイルを開けませんでした",
|
||||||
|
"log_open_config_error": "設定ファイルを開けませんでした: {0}",
|
||||||
|
|
||||||
|
"update_available_title": "アップデートがあります",
|
||||||
|
"update_new_version": "新バージョン v{0} がリリースされました!",
|
||||||
|
"update_now": "今すぐ更新",
|
||||||
|
"update_not_now": "後で",
|
||||||
|
"update_skip_version": "v{0} をスキップ",
|
||||||
|
"check_for_updates": "アップデートを確認",
|
||||||
|
"update_checking": "確認中...",
|
||||||
|
"update_no_update": "最新バージョンです",
|
||||||
|
"update_urgency_critical": "緊急アップデート",
|
||||||
|
"update_urgency_high": "重要なアップデート",
|
||||||
|
"update_current_version": "現在のバージョン: v{0}",
|
||||||
|
"update_changelog": "変更履歴",
|
||||||
|
"auth_choose_subtitle": "サインイン方法を選択して続行してください",
|
||||||
|
"update_changelog_empty": "このバージョンのリリースノートはありません。",
|
||||||
|
"subscription_login_title": "VberAI サブスクリプションアカウント",
|
||||||
|
"subscription_password_label": "パスワード",
|
||||||
|
"subscription_password_placeholder": "パスワードを入力",
|
||||||
|
"subscription_login_button": "VberAI アカウントでログイン",
|
||||||
|
"subscription_google_button": "Google でログイン",
|
||||||
|
"subscription_google_pending": "ブラウザを開きました。Google 認証を完了してから Cocos Creator に戻ってください。",
|
||||||
|
"subscription_google_pending_button": "Google 認証を待っています...",
|
||||||
|
"subscription_register_button": "VberAI アカウントを作成",
|
||||||
|
"subscription_logging_in": "ログイン中...",
|
||||||
|
"subscription_credentials_required": "メールアドレスとパスワードを入力してください",
|
||||||
|
"subscription_login_success": "サブスクリプションアカウントにログインしました",
|
||||||
|
"subscription_login_failed": "サブスクリプションのログインに失敗しました",
|
||||||
|
"subscription_not_entitled": "現在のサブスクリプションには Cocos 3.x MCP が含まれていません。続行するにはアップグレードしてください。",
|
||||||
|
"subscription_upgrade_to": "このプロダクトを使用するには「{0}」にアップグレードしてください",
|
||||||
|
"subscription_upgrade_action": "サブスクリプションをアップグレード",
|
||||||
|
"subscription_logged_in": "ログイン中のアカウント",
|
||||||
|
"subscription_choose_button": "VberAI サブスクリプションアカウント",
|
||||||
|
"license_choose_button": "アクティベーションコード",
|
||||||
|
"subscription_method_label": "サブスク",
|
||||||
|
"back_to_login_choices": "← 別のサインイン方法を選択",
|
||||||
|
"subscription_signed_out_other_device": "サインアウトしました:アカウントが別のデバイスで使用されました。",
|
||||||
|
"subscription_google_cancelled": "Google ログインがキャンセルされました。",
|
||||||
|
"subscription_request_timeout": "リクエストがタイムアウトしました。ネットワークを確認して再試行してください。",
|
||||||
|
"subscription_network_error": "ネットワーク接続に失敗しました。ネットワークを確認して再試行してください。",
|
||||||
|
"subscription_server_error": "サーバーから無効な応答が返されました。後でもう一度お試しください。",
|
||||||
|
"subscription_google_authorization_failed": "Google 認証が完了しませんでした。もう一度お試しください。"
|
||||||
|
};
|
||||||
@@ -0,0 +1,243 @@
|
|||||||
|
"use strict";
|
||||||
|
module.exports = {
|
||||||
|
"extension_name": "Cocos MCP 서버",
|
||||||
|
"description": "Cocos Creator 3.8용 AI MCP 서버",
|
||||||
|
"panel_title": "MCP 서버",
|
||||||
|
"open_panel": "MCP 패널 열기",
|
||||||
|
"start_server": "서버 시작",
|
||||||
|
"stop_server": "서버 중지",
|
||||||
|
"server_status": "서버 상태",
|
||||||
|
"port": "포트",
|
||||||
|
"settings": "설정",
|
||||||
|
"connected": "연결됨",
|
||||||
|
"disconnected": "연결 안 됨",
|
||||||
|
"server_running": "서버가 포트 {0}에서 실행 중입니다",
|
||||||
|
"server_stopped": "서버가 중지되었습니다",
|
||||||
|
"auto_start": "자동 시작",
|
||||||
|
"debug_log": "디버그 로그",
|
||||||
|
"max_connections": "최대 연결 수",
|
||||||
|
"connection_info": "연결 정보",
|
||||||
|
"http_url": "HTTP URL",
|
||||||
|
"copy": "복사",
|
||||||
|
"save_settings": "설정 저장",
|
||||||
|
"settings_saved": "설정이 저장되었습니다",
|
||||||
|
"server_started": "MCP 서버가 시작되었습니다",
|
||||||
|
"server_stopped_msg": "MCP 서버가 중지되었습니다",
|
||||||
|
"failed_to_start": "서버 시작 실패",
|
||||||
|
"failed_to_stop": "서버 중지 실패",
|
||||||
|
"failed_to_save": "설정 저장 실패",
|
||||||
|
"url_copied": "HTTP URL이 클립보드에 복사되었습니다",
|
||||||
|
"tool_manager": "도구 관리자",
|
||||||
|
"open_tool_manager": "도구 관리자 열기",
|
||||||
|
"create_config": "구성 생성",
|
||||||
|
"edit_config": "구성 편집",
|
||||||
|
"delete_config": "구성 삭제",
|
||||||
|
"import_config": "구성 가져오기",
|
||||||
|
"export_config": "구성 내보내기",
|
||||||
|
"apply_config": "구성 적용",
|
||||||
|
"select_all": "전체 선택",
|
||||||
|
"deselect_all": "전체 해제",
|
||||||
|
"save_changes": "변경 사항 저장",
|
||||||
|
"config_name": "구성 이름",
|
||||||
|
"config_description": "구성 설명",
|
||||||
|
"current_config": "현재 구성",
|
||||||
|
"tool_management": "도구 관리",
|
||||||
|
"total_tools": "전체 도구",
|
||||||
|
"enabled_tools": "활성화",
|
||||||
|
"disabled_tools": "비활성화",
|
||||||
|
"no_config_selected": "선택된 구성 없음",
|
||||||
|
"select_config_first": "먼저 구성을 선택하거나 새로 만들어 주세요",
|
||||||
|
"config_created": "구성이 생성되었습니다",
|
||||||
|
"config_updated": "구성이 업데이트되었습니다",
|
||||||
|
"config_deleted": "구성이 삭제되었습니다",
|
||||||
|
"config_applied": "구성이 적용되었습니다",
|
||||||
|
"config_exported": "구성이 내보내기되었습니다",
|
||||||
|
"config_imported": "구성이 가져오기되었습니다",
|
||||||
|
"confirm_delete": "삭제 확인",
|
||||||
|
"delete_config_confirm": "구성 \"{0}\"을(를) 삭제하시겠습니까? 이 작업은 취소할 수 없습니다.",
|
||||||
|
"max_config_slots_reached": "최대 구성 슬롯에 도달했습니다 ({0})",
|
||||||
|
"invalid_config_format": "잘못된 구성 형식",
|
||||||
|
"invalid_json_format": "잘못된 JSON 형식 또는 구성 구조",
|
||||||
|
"server_tab": "서버",
|
||||||
|
"tools_tab": "도구 관리",
|
||||||
|
"available_tools": "사용 가능한 도구",
|
||||||
|
"scene_tools": "씬 도구",
|
||||||
|
"node_tools": "노드 도구",
|
||||||
|
"component_tools": "컴포넌트 도구",
|
||||||
|
"prefab_tools": "프리팹 도구",
|
||||||
|
"project_tools": "프로젝트 도구",
|
||||||
|
"debug_tools": "디버그 도구",
|
||||||
|
"preferences_tools": "환경설정",
|
||||||
|
"server_tools": "서버 도구",
|
||||||
|
"broadcast_tools": "브로드캐스트 도구",
|
||||||
|
"scene_advanced_tools": "고급 씬 도구",
|
||||||
|
"scene_view_tools": "씬 뷰 도구",
|
||||||
|
"reference_image_tools": "참조 이미지 도구",
|
||||||
|
"asset_advanced_tools": "고급 에셋 도구",
|
||||||
|
"validation_tools": "검증 도구",
|
||||||
|
"brand_name": "LiDaxian MCP 시리즈",
|
||||||
|
"brand_subtitle": "LiDaxian MCP Series",
|
||||||
|
"contact_email": "이메일",
|
||||||
|
"contact_wechat": "WeChat",
|
||||||
|
"contact_github": "GitHub",
|
||||||
|
"machine_id_label": "머신 코드",
|
||||||
|
"purchase_renew": "구매 / 갱신",
|
||||||
|
"purchase_url": "https://www.vberai.com/game-engines/cocos",
|
||||||
|
"auth_title": "로그인 방식 선택",
|
||||||
|
"auth_subtitle": "이메일과 활성화 코드를 입력하세요",
|
||||||
|
"email_label": "이메일 주소",
|
||||||
|
"activation_code_label": "활성화 코드",
|
||||||
|
"email_placeholder": "이메일 입력",
|
||||||
|
"code_placeholder": "코드 입력 (XXXXX-XXXXX-XXXXX-XXXXX-XXXXX)",
|
||||||
|
"activate_button": "활성화",
|
||||||
|
"deactivate_button": "비활성화",
|
||||||
|
"activating": "활성화 중...",
|
||||||
|
"deactivating": "비활성화 중...",
|
||||||
|
"license_status": "라이선스 상태",
|
||||||
|
"license_type": "라이선스 유형",
|
||||||
|
"expiry_date": "만료일",
|
||||||
|
"days_remaining": "남은 일수",
|
||||||
|
"days_unit": "일",
|
||||||
|
"permanent": "영구",
|
||||||
|
"expiry_warning_title": "라이선스 만료 임박",
|
||||||
|
"expiry_warning_message": "라이선스가 {0}일 후 만료됩니다. 서비스 중단을 방지하려면 갱신해 주세요.",
|
||||||
|
"renew_now": "지금 갱신",
|
||||||
|
"i_understand": "확인했습니다",
|
||||||
|
"activation_success": "라이선스 활성화에 성공했습니다!",
|
||||||
|
"activation_failed": "활성화 실패: {0}",
|
||||||
|
"invalid_code": "잘못된 활성화 코드 형식",
|
||||||
|
"purchase_license": "라이선스 구매",
|
||||||
|
"renew_license": "라이선스 갱신",
|
||||||
|
"contact_support": "지원",
|
||||||
|
"language_switch": "한국어",
|
||||||
|
"version_label": "버전",
|
||||||
|
"license_required": "서버를 시작하려면 라이선스가 필요합니다",
|
||||||
|
"license_check_failed": "라이선스 확인 실패",
|
||||||
|
|
||||||
|
"status_stopped": "중지됨",
|
||||||
|
"status_running": "실행 중",
|
||||||
|
"status_error": "연결 실패",
|
||||||
|
"config_tab": "빠른 설정",
|
||||||
|
"status_label": "상태",
|
||||||
|
"connections_count": "연결 수",
|
||||||
|
"transport_protocol": "전송 프로토콜",
|
||||||
|
"transport_type": "전송 유형",
|
||||||
|
"mcp_url": "MCP URL",
|
||||||
|
"port_fallback_title": "포트 {0}을(를) 사용할 수 없어 {1}(으)로 전환했습니다",
|
||||||
|
"port_fallback_reason": "포트 {0}이(가) 시스템에 예약되어 있거나 방화벽/백신에 의해 차단되었습니다(Hyper-V, WSL2 또는 Docker가 설치된 Windows에서 흔합니다). 서버는 새 포트에서 정상 실행 중입니다.",
|
||||||
|
"port_fallback_resynced": "다음 클라이언트의 설정을 업데이트했습니다: {0}. 새 포트를 적용하려면 다시 시작하세요.",
|
||||||
|
"port_fallback_no_clients": "AI 클라이언트를 설정한 적이 있다면 \"원클릭 설정\"에서 새 포트로 다시 설정하세요.",
|
||||||
|
"port_fallback_dismiss": "확인",
|
||||||
|
"port_label": "포트",
|
||||||
|
"auto_start_label": "자동 시작",
|
||||||
|
"debug_log_label": "디버그 로그",
|
||||||
|
"max_connections_label": "최대 연결 수",
|
||||||
|
"server_settings_title": "서버 설정",
|
||||||
|
"server_status_title": "서버 상태",
|
||||||
|
"connection_info_title": "연결 정보",
|
||||||
|
"tools_count": "{0}개 도구",
|
||||||
|
"tools_stats": "{0}개 도구 ({1} 활성화 / {2} 비활성화)",
|
||||||
|
"quick_config_title": "MCP 클라이언트 빠른 설정",
|
||||||
|
"quick_config_desc": "현재 MCP 서버를 주요 AI 에디터 및 CLI 도구에 자동 구성합니다",
|
||||||
|
"server_info": "서버 정보",
|
||||||
|
"server_name_label": "서버 이름",
|
||||||
|
"server_address": "서버 주소",
|
||||||
|
"timeout_ms": "타임아웃 (ms)",
|
||||||
|
"auto_config_title": "자동 구성 (IDE 에디터 + Codex CLI)",
|
||||||
|
"add_all": "전체 추가",
|
||||||
|
"remove_all": "전체 삭제",
|
||||||
|
"auto_config_note": "버튼을 클릭하여 구성 파일을 자동 수정합니다 (Cursor, Windsurf, Trae, Codex CLI)",
|
||||||
|
"configured": "구성됨",
|
||||||
|
"not_configured": "미구성",
|
||||||
|
"auto_config_btn": "자동 구성",
|
||||||
|
"delete_btn": "삭제",
|
||||||
|
"copy_config": "구성 복사",
|
||||||
|
"open_config": "구성 열기",
|
||||||
|
"manual_config_title": "수동 구성 (Claude CLI + Gemini CLI)",
|
||||||
|
"refresh_commands": "명령 새로고침",
|
||||||
|
"manual_config_note": "다음 CLI 도구는 터미널에서 수동 실행이 필요합니다",
|
||||||
|
"config_scope": "구성 범위:",
|
||||||
|
"scope_user": "사용자 수준",
|
||||||
|
"scope_project": "프로젝트 수준",
|
||||||
|
"operation_log": "작업 로그",
|
||||||
|
"no_logs": "작업 기록 없음",
|
||||||
|
"copyright_line": "© 2025 LiDaxian - Cocos Creator MCP Server v{0}",
|
||||||
|
"protocol_line": "Streamable HTTP 프로토콜 (MCP 2025-03-26) 기반",
|
||||||
|
"tools_copyright_line1": "도구 시스템 - 13개 인텐트 레벨 도구, 150+ 에디터 작업, 액션 코드 디스패치",
|
||||||
|
"config_copyright_line1": "빠른 설정 - Cursor, Windsurf, Trae, Codex CLI, Claude CLI, Gemini CLI 지원",
|
||||||
|
"email_required": "이메일과 활성화 코드를 입력하세요",
|
||||||
|
"permanent_label": "영구",
|
||||||
|
"logout_button": "로그아웃",
|
||||||
|
"invite_code_label": "초대 코드",
|
||||||
|
"copy_button": "복사",
|
||||||
|
"change_code_button": "코드 변경",
|
||||||
|
"change_code_title": "활성화 코드 변경",
|
||||||
|
"change_code_desc": "새 활성화 코드를 입력하세요. 현재 기기 바인딩이 먼저 해제됩니다.",
|
||||||
|
"confirm_change": "확인",
|
||||||
|
"cancel": "취소",
|
||||||
|
"code_required": "활성화 코드를 입력하세요",
|
||||||
|
"expiry_inline_warning": "곧 만료됩니다!",
|
||||||
|
"store_purchase_hint": "Cocos Store에서 본 플러그인을 구매하셨다면, 활성화 코드를 받으시려면 연락해 주세요.",
|
||||||
|
"license_expired_title": "라이선스 만료",
|
||||||
|
"license_expired_message": "라이선스가 만료되어 서버를 시작할 수 없습니다. 계속 사용하려면 갱신하거나 다시 활성화하세요.",
|
||||||
|
"go_to_login": "로그인으로 이동",
|
||||||
|
|
||||||
|
"log_load_config_failed": "구성 상태 로드 실패: {0}",
|
||||||
|
"log_gen_cli_failed": "CLI 명령 생성 실패: {0}",
|
||||||
|
"log_add_client_failed": "{0}에 추가 실패: {1}",
|
||||||
|
"log_remove_client_failed": "{0}에서 삭제 실패: {1}",
|
||||||
|
"quick_config_dialog_title": "원클릭 구성",
|
||||||
|
"config_operation_failed": "구성 작업 실패",
|
||||||
|
"config_file_not_found_changed_path": "구성 실패: 구성 파일을 찾을 수 없습니다. {0}이 사용자 지정 경로에 설치되었을 수 있습니다.",
|
||||||
|
"log_batch_add_failed": "일괄 추가 실패: {0}",
|
||||||
|
"log_batch_remove_failed": "일괄 삭제 실패: {0}",
|
||||||
|
"log_copied_config": "{0} 구성을 클립보드에 복사했습니다",
|
||||||
|
"log_gen_config_failed": "구성 생성 실패",
|
||||||
|
"log_copy_config_failed": "구성 복사 실패: {0}",
|
||||||
|
"log_copied_command": "{0} 명령을 클립보드에 복사했습니다",
|
||||||
|
"log_copy_command_failed": "명령 복사 실패: {0}",
|
||||||
|
"log_opened_config_file": "구성 파일을 열었습니다: {0}",
|
||||||
|
"log_open_config_failed": "구성 파일을 열 수 없습니다",
|
||||||
|
"log_open_config_error": "구성 파일을 열 수 없습니다: {0}",
|
||||||
|
|
||||||
|
"update_available_title": "업데이트 가능",
|
||||||
|
"update_new_version": "새 버전 v{0}이 출시되었습니다!",
|
||||||
|
"update_now": "지금 업데이트",
|
||||||
|
"update_not_now": "나중에",
|
||||||
|
"update_skip_version": "v{0} 건너뛰기",
|
||||||
|
"check_for_updates": "업데이트 확인",
|
||||||
|
"update_checking": "확인 중...",
|
||||||
|
"update_no_update": "최신 버전입니다",
|
||||||
|
"update_urgency_critical": "긴급 업데이트",
|
||||||
|
"update_urgency_high": "중요 업데이트",
|
||||||
|
"update_current_version": "현재 버전: v{0}",
|
||||||
|
"update_changelog": "변경 사항",
|
||||||
|
"auth_choose_subtitle": "계속하려면 로그인 방법을 선택하세요",
|
||||||
|
"update_changelog_empty": "이 버전에 대한 릴리스 노트가 없습니다.",
|
||||||
|
"subscription_login_title": "VberAI 구독 계정",
|
||||||
|
"subscription_password_label": "비밀번호",
|
||||||
|
"subscription_password_placeholder": "비밀번호 입력",
|
||||||
|
"subscription_login_button": "VberAI 계정으로 로그인",
|
||||||
|
"subscription_google_button": "Google로 로그인",
|
||||||
|
"subscription_google_pending": "브라우저가 열렸습니다. Google 인증을 완료한 후 Cocos Creator로 돌아오세요.",
|
||||||
|
"subscription_google_pending_button": "Google 인증을 기다리는 중...",
|
||||||
|
"subscription_register_button": "VberAI 계정 만들기",
|
||||||
|
"subscription_logging_in": "로그인 중...",
|
||||||
|
"subscription_credentials_required": "이메일과 비밀번호를 입력하세요",
|
||||||
|
"subscription_login_success": "구독 계정에 로그인했습니다",
|
||||||
|
"subscription_login_failed": "구독 로그인에 실패했습니다",
|
||||||
|
"subscription_not_entitled": "현재 구독에는 Cocos 3.x MCP가 포함되어 있지 않습니다. 계속하려면 업그레이드하세요.",
|
||||||
|
"subscription_upgrade_to": "이 제품을 사용하려면 「{0}」(으)로 업그레이드하세요",
|
||||||
|
"subscription_upgrade_action": "구독 업그레이드",
|
||||||
|
"subscription_logged_in": "로그인된 계정",
|
||||||
|
"subscription_choose_button": "VberAI 구독 계정",
|
||||||
|
"license_choose_button": "활성화 코드",
|
||||||
|
"subscription_method_label": "구독 계정",
|
||||||
|
"back_to_login_choices": "← 다른 로그인 방법 선택",
|
||||||
|
"subscription_signed_out_other_device": "로그아웃됨: 계정이 다른 기기에서 사용되었습니다.",
|
||||||
|
"subscription_google_cancelled": "Google 로그인이 취소되었습니다.",
|
||||||
|
"subscription_request_timeout": "요청 시간이 초과되었습니다. 네트워크를 확인하고 다시 시도하세요.",
|
||||||
|
"subscription_network_error": "네트워크 연결에 실패했습니다. 네트워크를 확인하고 다시 시도하세요.",
|
||||||
|
"subscription_server_error": "서버가 잘못된 응답을 반환했습니다. 나중에 다시 시도하세요.",
|
||||||
|
"subscription_google_authorization_failed": "Google 인증이 완료되지 않았습니다. 다시 시도하세요."
|
||||||
|
};
|
||||||
@@ -0,0 +1,243 @@
|
|||||||
|
"use strict";
|
||||||
|
module.exports = {
|
||||||
|
"extension_name": "Cocos MCP Servidor",
|
||||||
|
"description": "Servidor AI MCP para Cocos Creator 3.8",
|
||||||
|
"panel_title": "Servidor MCP",
|
||||||
|
"open_panel": "Abrir painel MCP",
|
||||||
|
"start_server": "Iniciar servidor",
|
||||||
|
"stop_server": "Parar servidor",
|
||||||
|
"server_status": "Estado do servidor",
|
||||||
|
"port": "Porta",
|
||||||
|
"settings": "Configurações",
|
||||||
|
"connected": "Conectado",
|
||||||
|
"disconnected": "Desconectado",
|
||||||
|
"server_running": "O servidor está em execução na porta {0}",
|
||||||
|
"server_stopped": "O servidor foi parado",
|
||||||
|
"auto_start": "Início automático",
|
||||||
|
"debug_log": "Log de depuração",
|
||||||
|
"max_connections": "Conexões máx.",
|
||||||
|
"connection_info": "Info de conexão",
|
||||||
|
"http_url": "URL HTTP",
|
||||||
|
"copy": "Copiar",
|
||||||
|
"save_settings": "Salvar",
|
||||||
|
"settings_saved": "Configurações salvas com sucesso",
|
||||||
|
"server_started": "Servidor MCP iniciado",
|
||||||
|
"server_stopped_msg": "Servidor MCP parado",
|
||||||
|
"failed_to_start": "Falha ao iniciar o servidor",
|
||||||
|
"failed_to_stop": "Falha ao parar o servidor",
|
||||||
|
"failed_to_save": "Falha ao salvar configurações",
|
||||||
|
"url_copied": "URL HTTP copiada para a área de transferência",
|
||||||
|
"tool_manager": "Gerenciador de ferramentas",
|
||||||
|
"open_tool_manager": "Abrir gerenciador de ferramentas",
|
||||||
|
"create_config": "Criar configuração",
|
||||||
|
"edit_config": "Editar configuração",
|
||||||
|
"delete_config": "Excluir configuração",
|
||||||
|
"import_config": "Importar configuração",
|
||||||
|
"export_config": "Exportar configuração",
|
||||||
|
"apply_config": "Aplicar configuração",
|
||||||
|
"select_all": "Selecionar tudo",
|
||||||
|
"deselect_all": "Desmarcar tudo",
|
||||||
|
"save_changes": "Salvar alterações",
|
||||||
|
"config_name": "Nome da configuração",
|
||||||
|
"config_description": "Descrição da configuração",
|
||||||
|
"current_config": "Configuração atual",
|
||||||
|
"tool_management": "Gestão de ferramentas",
|
||||||
|
"total_tools": "Total de ferramentas",
|
||||||
|
"enabled_tools": "Ativadas",
|
||||||
|
"disabled_tools": "Desativadas",
|
||||||
|
"no_config_selected": "Nenhuma configuração selecionada",
|
||||||
|
"select_config_first": "Por favor, selecione ou crie uma configuração primeiro",
|
||||||
|
"config_created": "Configuração criada com sucesso",
|
||||||
|
"config_updated": "Configuração atualizada com sucesso",
|
||||||
|
"config_deleted": "Configuração excluída com sucesso",
|
||||||
|
"config_applied": "Configuração aplicada com sucesso",
|
||||||
|
"config_exported": "Configuração exportada com sucesso",
|
||||||
|
"config_imported": "Configuração importada com sucesso",
|
||||||
|
"confirm_delete": "Confirmar exclusão",
|
||||||
|
"delete_config_confirm": "Tem certeza de que deseja excluir a configuração \"{0}\"? Esta ação não pode ser desfeita.",
|
||||||
|
"max_config_slots_reached": "Número máximo de configurações atingido ({0})",
|
||||||
|
"invalid_config_format": "Formato de configuração inválido",
|
||||||
|
"invalid_json_format": "Formato JSON ou estrutura de configuração inválidos",
|
||||||
|
"server_tab": "Servidor",
|
||||||
|
"tools_tab": "Gestão de ferramentas",
|
||||||
|
"available_tools": "Ferramentas disponíveis",
|
||||||
|
"scene_tools": "Ferramentas de cena",
|
||||||
|
"node_tools": "Ferramentas de nó",
|
||||||
|
"component_tools": "Ferramentas de componente",
|
||||||
|
"prefab_tools": "Ferramentas de prefab",
|
||||||
|
"project_tools": "Ferramentas de projeto",
|
||||||
|
"debug_tools": "Ferramentas de depuração",
|
||||||
|
"preferences_tools": "Preferências",
|
||||||
|
"server_tools": "Ferramentas de servidor",
|
||||||
|
"broadcast_tools": "Ferramentas de broadcast",
|
||||||
|
"scene_advanced_tools": "Ferramentas de cena avançadas",
|
||||||
|
"scene_view_tools": "Ferramentas de visualização de cena",
|
||||||
|
"reference_image_tools": "Ferramentas de imagem de referência",
|
||||||
|
"asset_advanced_tools": "Ferramentas de assets avançadas",
|
||||||
|
"validation_tools": "Ferramentas de validação",
|
||||||
|
"brand_name": "LiDaxian MCP Series",
|
||||||
|
"brand_subtitle": "LiDaxian MCP Series",
|
||||||
|
"contact_email": "E-mail",
|
||||||
|
"contact_wechat": "WeChat",
|
||||||
|
"contact_github": "GitHub",
|
||||||
|
"machine_id_label": "Código da máquina",
|
||||||
|
"purchase_renew": "Comprar / Renovar",
|
||||||
|
"purchase_url": "https://www.vberai.com/game-engines/cocos",
|
||||||
|
"auth_title": "Escolher método de login",
|
||||||
|
"auth_subtitle": "Digite seu e-mail e código de ativação",
|
||||||
|
"email_label": "Endereço de e-mail",
|
||||||
|
"activation_code_label": "Código de ativação",
|
||||||
|
"email_placeholder": "Digite o e-mail",
|
||||||
|
"code_placeholder": "Digite o código (XXXXX-XXXXX-XXXXX-XXXXX-XXXXX)",
|
||||||
|
"activate_button": "Ativar",
|
||||||
|
"deactivate_button": "Desativar",
|
||||||
|
"activating": "Ativando...",
|
||||||
|
"deactivating": "Desativando...",
|
||||||
|
"license_status": "Estado da licença",
|
||||||
|
"license_type": "Tipo de licença",
|
||||||
|
"expiry_date": "Data de expiração",
|
||||||
|
"days_remaining": "Dias restantes",
|
||||||
|
"days_unit": "d",
|
||||||
|
"permanent": "Permanente",
|
||||||
|
"expiry_warning_title": "Licença expirando em breve",
|
||||||
|
"expiry_warning_message": "Sua licença expirará em {0} dias. Por favor, renove para evitar interrupções.",
|
||||||
|
"renew_now": "Renovar agora",
|
||||||
|
"i_understand": "Entendido",
|
||||||
|
"activation_success": "Licença ativada com sucesso!",
|
||||||
|
"activation_failed": "Falha na ativação: {0}",
|
||||||
|
"invalid_code": "Formato de código de ativação inválido",
|
||||||
|
"purchase_license": "Comprar licença",
|
||||||
|
"renew_license": "Renovar licença",
|
||||||
|
"contact_support": "Suporte",
|
||||||
|
"language_switch": "Português",
|
||||||
|
"version_label": "Versão",
|
||||||
|
"license_required": "É necessária uma licença para iniciar o servidor",
|
||||||
|
"license_check_failed": "Falha na verificação de licença",
|
||||||
|
|
||||||
|
"status_stopped": "Parado",
|
||||||
|
"status_running": "Em execução",
|
||||||
|
"status_error": "Falha na conexão",
|
||||||
|
"config_tab": "Config rápida",
|
||||||
|
"status_label": "Estado",
|
||||||
|
"connections_count": "Conexões",
|
||||||
|
"transport_protocol": "Protocolo de transporte",
|
||||||
|
"transport_type": "Tipo de transporte",
|
||||||
|
"mcp_url": "URL MCP",
|
||||||
|
"port_fallback_title": "Porta {0} indisponível — alterada para {1}",
|
||||||
|
"port_fallback_reason": "A porta {0} está reservada pelo sistema ou bloqueada por firewall/antivírus (comum no Windows com Hyper-V, WSL2 ou Docker). O servidor está funcionando normalmente na nova porta.",
|
||||||
|
"port_fallback_resynced": "Configuração atualizada para: {0}. Reinicie esses clientes para que a nova porta tenha efeito.",
|
||||||
|
"port_fallback_no_clients": "Se você já configurou clientes de IA, configure-os novamente para a nova porta pela configuração com um clique.",
|
||||||
|
"port_fallback_dismiss": "Entendi",
|
||||||
|
"port_label": "Porta",
|
||||||
|
"auto_start_label": "Início auto",
|
||||||
|
"debug_log_label": "Log de depuração",
|
||||||
|
"max_connections_label": "Conexões máx.",
|
||||||
|
"server_settings_title": "Configurações do servidor",
|
||||||
|
"server_status_title": "Estado do servidor",
|
||||||
|
"connection_info_title": "Info de conexão",
|
||||||
|
"tools_count": "{0} ferramentas",
|
||||||
|
"tools_stats": "{0} ferramentas ({1} ativadas / {2} desativadas)",
|
||||||
|
"quick_config_title": "Config rápida de clientes MCP",
|
||||||
|
"quick_config_desc": "Configurar automaticamente o servidor MCP em editores AI e ferramentas CLI",
|
||||||
|
"server_info": "Info do servidor",
|
||||||
|
"server_name_label": "Nome do servidor",
|
||||||
|
"server_address": "Endereço do servidor",
|
||||||
|
"timeout_ms": "Tempo limite (ms)",
|
||||||
|
"auto_config_title": "Config auto (IDE + Codex CLI)",
|
||||||
|
"add_all": "Adicionar tudo",
|
||||||
|
"remove_all": "Remover tudo",
|
||||||
|
"auto_config_note": "Clique para modificar automaticamente os arquivos de config (Cursor, Windsurf, Trae, Codex CLI)",
|
||||||
|
"configured": "Configurado",
|
||||||
|
"not_configured": "Não configurado",
|
||||||
|
"auto_config_btn": "Config auto",
|
||||||
|
"delete_btn": "Excluir",
|
||||||
|
"copy_config": "Copiar config",
|
||||||
|
"open_config": "Abrir config",
|
||||||
|
"manual_config_title": "Config manual (Claude CLI + Gemini CLI)",
|
||||||
|
"refresh_commands": "Atualizar comandos",
|
||||||
|
"manual_config_note": "As seguintes ferramentas CLI requerem execução manual no terminal",
|
||||||
|
"config_scope": "Escopo de config:",
|
||||||
|
"scope_user": "Nível de usuário",
|
||||||
|
"scope_project": "Nível de projeto",
|
||||||
|
"operation_log": "Log de operações",
|
||||||
|
"no_logs": "Sem registros",
|
||||||
|
"copyright_line": "© 2025 LiDaxian - Cocos Creator MCP Server v{0}",
|
||||||
|
"protocol_line": "Baseado no protocolo Streamable HTTP (MCP 2025-03-26)",
|
||||||
|
"tools_copyright_line1": "Sistema de ferramentas - 13 ferramentas intent-level, 150+ operações de editor, despacho de código de ação",
|
||||||
|
"config_copyright_line1": "Config rápida - Suporta Cursor, Windsurf, Trae, Codex CLI, Claude CLI, Gemini CLI",
|
||||||
|
"email_required": "Por favor, digite e-mail e código de ativação",
|
||||||
|
"permanent_label": "Permanente",
|
||||||
|
"logout_button": "Sair",
|
||||||
|
"invite_code_label": "Código de convite",
|
||||||
|
"copy_button": "Copiar",
|
||||||
|
"change_code_button": "Alterar código",
|
||||||
|
"change_code_title": "Alterar código de ativação",
|
||||||
|
"change_code_desc": "Digite seu novo código de ativação. A vinculação do dispositivo atual será liberada primeiro.",
|
||||||
|
"confirm_change": "Confirmar",
|
||||||
|
"cancel": "Cancelar",
|
||||||
|
"code_required": "Por favor, digite um código de ativação",
|
||||||
|
"expiry_inline_warning": "Expirando em breve!",
|
||||||
|
"store_purchase_hint": "Se você comprou este plugin na Cocos Store, entre em contato comigo para obter seu código de ativação.",
|
||||||
|
"license_expired_title": "Licença expirada",
|
||||||
|
"license_expired_message": "Sua licença expirou e o servidor não pode ser iniciado. Renove ou reative para continuar.",
|
||||||
|
"go_to_login": "Ir para login",
|
||||||
|
|
||||||
|
"log_load_config_failed": "Falha ao carregar estado da config: {0}",
|
||||||
|
"log_gen_cli_failed": "Falha ao gerar comandos CLI: {0}",
|
||||||
|
"log_add_client_failed": "Falha ao adicionar a {0}: {1}",
|
||||||
|
"log_remove_client_failed": "Falha ao remover de {0}: {1}",
|
||||||
|
"quick_config_dialog_title": "Configuração com um clique",
|
||||||
|
"config_operation_failed": "Falha na operação de configuração",
|
||||||
|
"config_file_not_found_changed_path": "Falha na configuração: arquivo de configuração não encontrado. {0} pode ter sido instalado em um caminho personalizado.",
|
||||||
|
"log_batch_add_failed": "Falha na adição em lote: {0}",
|
||||||
|
"log_batch_remove_failed": "Falha na remoção em lote: {0}",
|
||||||
|
"log_copied_config": "Config de {0} copiada para a área de transferência",
|
||||||
|
"log_gen_config_failed": "Falha ao gerar config",
|
||||||
|
"log_copy_config_failed": "Falha ao copiar config: {0}",
|
||||||
|
"log_copied_command": "Comando {0} copiado para a área de transferência",
|
||||||
|
"log_copy_command_failed": "Falha ao copiar comando: {0}",
|
||||||
|
"log_opened_config_file": "Arquivo de config aberto: {0}",
|
||||||
|
"log_open_config_failed": "Falha ao abrir arquivo de config",
|
||||||
|
"log_open_config_error": "Falha ao abrir arquivo de config: {0}",
|
||||||
|
|
||||||
|
"update_available_title": "Atualização disponível",
|
||||||
|
"update_new_version": "A nova versão v{0} está disponível!",
|
||||||
|
"update_now": "Atualizar agora",
|
||||||
|
"update_not_now": "Agora não",
|
||||||
|
"update_skip_version": "Pular v{0}",
|
||||||
|
"check_for_updates": "Verificar atualizações",
|
||||||
|
"update_checking": "Verificando...",
|
||||||
|
"update_no_update": "Você está usando a versão mais recente",
|
||||||
|
"update_urgency_critical": "Atualização crítica",
|
||||||
|
"update_urgency_high": "Atualização importante",
|
||||||
|
"update_current_version": "Versão atual: v{0}",
|
||||||
|
"update_changelog": "Registro de alterações",
|
||||||
|
"auth_choose_subtitle": "Escolha um método de login para continuar",
|
||||||
|
"update_changelog_empty": "Nenhuma nota de versão foi fornecida para esta versão.",
|
||||||
|
"subscription_login_title": "Conta de assinatura VberAI",
|
||||||
|
"subscription_password_label": "Senha",
|
||||||
|
"subscription_password_placeholder": "Digite a senha",
|
||||||
|
"subscription_login_button": "Entrar com VberAI",
|
||||||
|
"subscription_google_button": "Entrar com o Google",
|
||||||
|
"subscription_google_pending": "Navegador aberto. Conclua a autorização do Google e volte ao Cocos Creator.",
|
||||||
|
"subscription_google_pending_button": "Aguardando a autorização do Google...",
|
||||||
|
"subscription_register_button": "Criar uma conta VberAI",
|
||||||
|
"subscription_logging_in": "Entrando...",
|
||||||
|
"subscription_credentials_required": "Digite o e-mail e a senha",
|
||||||
|
"subscription_login_success": "Conectado à conta de assinatura",
|
||||||
|
"subscription_login_failed": "Falha ao entrar na assinatura",
|
||||||
|
"subscription_not_entitled": "Sua assinatura atual não inclui o Cocos 3.x MCP. Faça upgrade para continuar.",
|
||||||
|
"subscription_upgrade_to": "Faça upgrade para \"{0}\" para usar este produto",
|
||||||
|
"subscription_upgrade_action": "Fazer upgrade da assinatura",
|
||||||
|
"subscription_logged_in": "Conta conectada",
|
||||||
|
"subscription_choose_button": "Conta de assinatura VberAI",
|
||||||
|
"license_choose_button": "Código de ativação",
|
||||||
|
"subscription_method_label": "Assinatura",
|
||||||
|
"back_to_login_choices": "← Escolher outro método de login",
|
||||||
|
"subscription_signed_out_other_device": "Desconectado: sua conta foi usada em outro dispositivo.",
|
||||||
|
"subscription_google_cancelled": "O login com o Google foi cancelado.",
|
||||||
|
"subscription_request_timeout": "A solicitação expirou. Verifique sua rede e tente novamente.",
|
||||||
|
"subscription_network_error": "Falha na conexão de rede. Verifique sua rede e tente novamente.",
|
||||||
|
"subscription_server_error": "O servidor retornou uma resposta inválida. Tente novamente mais tarde.",
|
||||||
|
"subscription_google_authorization_failed": "A autorização do Google não foi concluída. Tente novamente."
|
||||||
|
};
|
||||||
@@ -0,0 +1,243 @@
|
|||||||
|
"use strict";
|
||||||
|
module.exports = {
|
||||||
|
"extension_name": "Cocos MCP Сервер",
|
||||||
|
"description": "AI MCP сервер для Cocos Creator 3.8",
|
||||||
|
"panel_title": "MCP Сервер",
|
||||||
|
"open_panel": "Открыть панель MCP",
|
||||||
|
"start_server": "Запустить сервер",
|
||||||
|
"stop_server": "Остановить сервер",
|
||||||
|
"server_status": "Состояние сервера",
|
||||||
|
"port": "Порт",
|
||||||
|
"settings": "Настройки",
|
||||||
|
"connected": "Подключено",
|
||||||
|
"disconnected": "Отключено",
|
||||||
|
"server_running": "Сервер работает на порту {0}",
|
||||||
|
"server_stopped": "Сервер остановлен",
|
||||||
|
"auto_start": "Автозапуск",
|
||||||
|
"debug_log": "Журнал отладки",
|
||||||
|
"max_connections": "Макс. подключений",
|
||||||
|
"connection_info": "Информация о подключении",
|
||||||
|
"http_url": "HTTP URL",
|
||||||
|
"copy": "Копировать",
|
||||||
|
"save_settings": "Сохранить",
|
||||||
|
"settings_saved": "Настройки успешно сохранены",
|
||||||
|
"server_started": "MCP сервер запущен",
|
||||||
|
"server_stopped_msg": "MCP сервер остановлен",
|
||||||
|
"failed_to_start": "Не удалось запустить сервер",
|
||||||
|
"failed_to_stop": "Не удалось остановить сервер",
|
||||||
|
"failed_to_save": "Не удалось сохранить настройки",
|
||||||
|
"url_copied": "HTTP URL скопирован в буфер обмена",
|
||||||
|
"tool_manager": "Менеджер инструментов",
|
||||||
|
"open_tool_manager": "Открыть менеджер инструментов",
|
||||||
|
"create_config": "Создать конфигурацию",
|
||||||
|
"edit_config": "Редактировать конфигурацию",
|
||||||
|
"delete_config": "Удалить конфигурацию",
|
||||||
|
"import_config": "Импортировать конфигурацию",
|
||||||
|
"export_config": "Экспортировать конфигурацию",
|
||||||
|
"apply_config": "Применить конфигурацию",
|
||||||
|
"select_all": "Выбрать все",
|
||||||
|
"deselect_all": "Снять все",
|
||||||
|
"save_changes": "Сохранить изменения",
|
||||||
|
"config_name": "Имя конфигурации",
|
||||||
|
"config_description": "Описание конфигурации",
|
||||||
|
"current_config": "Текущая конфигурация",
|
||||||
|
"tool_management": "Управление инструментами",
|
||||||
|
"total_tools": "Всего инструментов",
|
||||||
|
"enabled_tools": "Включено",
|
||||||
|
"disabled_tools": "Отключено",
|
||||||
|
"no_config_selected": "Конфигурация не выбрана",
|
||||||
|
"select_config_first": "Пожалуйста, сначала выберите или создайте конфигурацию",
|
||||||
|
"config_created": "Конфигурация успешно создана",
|
||||||
|
"config_updated": "Конфигурация успешно обновлена",
|
||||||
|
"config_deleted": "Конфигурация успешно удалена",
|
||||||
|
"config_applied": "Конфигурация успешно применена",
|
||||||
|
"config_exported": "Конфигурация успешно экспортирована",
|
||||||
|
"config_imported": "Конфигурация успешно импортирована",
|
||||||
|
"confirm_delete": "Подтвердить удаление",
|
||||||
|
"delete_config_confirm": "Вы уверены, что хотите удалить конфигурацию \"{0}\"? Это действие нельзя отменить.",
|
||||||
|
"max_config_slots_reached": "Достигнуто максимальное количество конфигураций ({0})",
|
||||||
|
"invalid_config_format": "Недопустимый формат конфигурации",
|
||||||
|
"invalid_json_format": "Недопустимый формат JSON или структура конфигурации",
|
||||||
|
"server_tab": "Сервер",
|
||||||
|
"tools_tab": "Управление инструментами",
|
||||||
|
"available_tools": "Доступные инструменты",
|
||||||
|
"scene_tools": "Инструменты сцены",
|
||||||
|
"node_tools": "Инструменты узлов",
|
||||||
|
"component_tools": "Инструменты компонентов",
|
||||||
|
"prefab_tools": "Инструменты префабов",
|
||||||
|
"project_tools": "Инструменты проекта",
|
||||||
|
"debug_tools": "Инструменты отладки",
|
||||||
|
"preferences_tools": "Настройки",
|
||||||
|
"server_tools": "Серверные инструменты",
|
||||||
|
"broadcast_tools": "Инструменты трансляции",
|
||||||
|
"scene_advanced_tools": "Расширенные инструменты сцены",
|
||||||
|
"scene_view_tools": "Инструменты просмотра сцены",
|
||||||
|
"reference_image_tools": "Инструменты эталонных изображений",
|
||||||
|
"asset_advanced_tools": "Расширенные инструменты ресурсов",
|
||||||
|
"validation_tools": "Инструменты валидации",
|
||||||
|
"brand_name": "LiDaxian MCP Series",
|
||||||
|
"brand_subtitle": "LiDaxian MCP Series",
|
||||||
|
"contact_email": "Эл. почта",
|
||||||
|
"contact_wechat": "WeChat",
|
||||||
|
"contact_github": "GitHub",
|
||||||
|
"machine_id_label": "Код машины",
|
||||||
|
"purchase_renew": "Купить / Продлить",
|
||||||
|
"purchase_url": "https://www.vberai.com/game-engines/cocos",
|
||||||
|
"auth_title": "Выберите способ входа",
|
||||||
|
"auth_subtitle": "Введите email и код активации",
|
||||||
|
"email_label": "Адрес электронной почты",
|
||||||
|
"activation_code_label": "Код активации",
|
||||||
|
"email_placeholder": "Введите email",
|
||||||
|
"code_placeholder": "Введите код (XXXXX-XXXXX-XXXXX-XXXXX-XXXXX)",
|
||||||
|
"activate_button": "Активировать",
|
||||||
|
"deactivate_button": "Деактивировать",
|
||||||
|
"activating": "Активация...",
|
||||||
|
"deactivating": "Деактивация...",
|
||||||
|
"license_status": "Состояние лицензии",
|
||||||
|
"license_type": "Тип лицензии",
|
||||||
|
"expiry_date": "Дата истечения",
|
||||||
|
"days_remaining": "Осталось дней",
|
||||||
|
"days_unit": "д",
|
||||||
|
"permanent": "Бессрочная",
|
||||||
|
"expiry_warning_title": "Срок лицензии скоро истекает",
|
||||||
|
"expiry_warning_message": "Ваша лицензия истекает через {0} дней. Пожалуйста, продлите, чтобы избежать прерывания.",
|
||||||
|
"renew_now": "Продлить сейчас",
|
||||||
|
"i_understand": "Понятно",
|
||||||
|
"activation_success": "Лицензия успешно активирована!",
|
||||||
|
"activation_failed": "Ошибка активации: {0}",
|
||||||
|
"invalid_code": "Неверный формат кода активации",
|
||||||
|
"purchase_license": "Купить лицензию",
|
||||||
|
"renew_license": "Продлить лицензию",
|
||||||
|
"contact_support": "Поддержка",
|
||||||
|
"language_switch": "Русский",
|
||||||
|
"version_label": "Версия",
|
||||||
|
"license_required": "Для запуска сервера требуется лицензия",
|
||||||
|
"license_check_failed": "Ошибка проверки лицензии",
|
||||||
|
|
||||||
|
"status_stopped": "Остановлен",
|
||||||
|
"status_running": "Работает",
|
||||||
|
"status_error": "Ошибка подключения",
|
||||||
|
"config_tab": "Быстрая настройка",
|
||||||
|
"status_label": "Состояние",
|
||||||
|
"connections_count": "Подключения",
|
||||||
|
"transport_protocol": "Транспортный протокол",
|
||||||
|
"transport_type": "Тип транспорта",
|
||||||
|
"mcp_url": "MCP URL",
|
||||||
|
"port_fallback_title": "Порт {0} недоступен — выполнен переход на {1}",
|
||||||
|
"port_fallback_reason": "Порт {0} зарезервирован системой или заблокирован брандмауэром/антивирусом (часто встречается в Windows с Hyper-V, WSL2 или Docker). Сервер нормально работает на новом порту.",
|
||||||
|
"port_fallback_resynced": "Обновлена конфигурация для: {0}. Перезапустите эти клиенты, чтобы новый порт вступил в силу.",
|
||||||
|
"port_fallback_no_clients": "Если вы уже настраивали ИИ-клиенты, перенастройте их на новый порт через конфигурацию в один клик.",
|
||||||
|
"port_fallback_dismiss": "Понятно",
|
||||||
|
"port_label": "Порт",
|
||||||
|
"auto_start_label": "Автозапуск",
|
||||||
|
"debug_log_label": "Журнал отладки",
|
||||||
|
"max_connections_label": "Макс. подключений",
|
||||||
|
"server_settings_title": "Настройки сервера",
|
||||||
|
"server_status_title": "Состояние сервера",
|
||||||
|
"connection_info_title": "Информация о подключении",
|
||||||
|
"tools_count": "{0} инструментов",
|
||||||
|
"tools_stats": "{0} инструментов ({1} включено / {2} отключено)",
|
||||||
|
"quick_config_title": "Быстрая настройка клиентов MCP",
|
||||||
|
"quick_config_desc": "Автоматическая настройка MCP сервера в AI-редакторах и CLI-инструментах",
|
||||||
|
"server_info": "Информация о сервере",
|
||||||
|
"server_name_label": "Имя сервера",
|
||||||
|
"server_address": "Адрес сервера",
|
||||||
|
"timeout_ms": "Тайм-аут (мс)",
|
||||||
|
"auto_config_title": "Автонастройка (IDE + Codex CLI)",
|
||||||
|
"add_all": "Добавить все",
|
||||||
|
"remove_all": "Удалить все",
|
||||||
|
"auto_config_note": "Нажмите для автоматического изменения файлов конфигурации (Cursor, Windsurf, Trae, Codex CLI)",
|
||||||
|
"configured": "Настроено",
|
||||||
|
"not_configured": "Не настроено",
|
||||||
|
"auto_config_btn": "Автонастройка",
|
||||||
|
"delete_btn": "Удалить",
|
||||||
|
"copy_config": "Копировать конфиг",
|
||||||
|
"open_config": "Открыть конфиг",
|
||||||
|
"manual_config_title": "Ручная настройка (Claude CLI + Gemini CLI)",
|
||||||
|
"refresh_commands": "Обновить команды",
|
||||||
|
"manual_config_note": "Следующие CLI-инструменты требуют ручного выполнения в терминале",
|
||||||
|
"config_scope": "Область конфигурации:",
|
||||||
|
"scope_user": "Уровень пользователя",
|
||||||
|
"scope_project": "Уровень проекта",
|
||||||
|
"operation_log": "Журнал операций",
|
||||||
|
"no_logs": "Нет записей",
|
||||||
|
"copyright_line": "© 2025 LiDaxian - Cocos Creator MCP Server v{0}",
|
||||||
|
"protocol_line": "На основе протокола Streamable HTTP (MCP 2025-03-26)",
|
||||||
|
"tools_copyright_line1": "Система инструментов - 13 intent-level инструментов, 150+ операций редактора, диспетчеризация action-кодов",
|
||||||
|
"config_copyright_line1": "Быстрая настройка - Поддержка Cursor, Windsurf, Trae, Codex CLI, Claude CLI, Gemini CLI",
|
||||||
|
"email_required": "Пожалуйста, введите email и код активации",
|
||||||
|
"permanent_label": "Бессрочная",
|
||||||
|
"logout_button": "Выйти",
|
||||||
|
"invite_code_label": "Код приглашения",
|
||||||
|
"copy_button": "Копировать",
|
||||||
|
"change_code_button": "Сменить код",
|
||||||
|
"change_code_title": "Смена кода активации",
|
||||||
|
"change_code_desc": "Введите новый код активации. Привязка текущего устройства будет сначала снята.",
|
||||||
|
"confirm_change": "Подтвердить",
|
||||||
|
"cancel": "Отмена",
|
||||||
|
"code_required": "Пожалуйста, введите код активации",
|
||||||
|
"expiry_inline_warning": "Скоро истекает!",
|
||||||
|
"store_purchase_hint": "Если вы приобрели этот плагин в Cocos Store, свяжитесь со мной для получения кода активации.",
|
||||||
|
"license_expired_title": "Лицензия истекла",
|
||||||
|
"license_expired_message": "Ваша лицензия истекла, сервер не может быть запущен. Пожалуйста, продлите или повторно активируйте для продолжения.",
|
||||||
|
"go_to_login": "Перейти к входу",
|
||||||
|
|
||||||
|
"log_load_config_failed": "Не удалось загрузить состояние конфигурации: {0}",
|
||||||
|
"log_gen_cli_failed": "Не удалось сгенерировать команды CLI: {0}",
|
||||||
|
"log_add_client_failed": "Не удалось добавить в {0}: {1}",
|
||||||
|
"log_remove_client_failed": "Не удалось удалить из {0}: {1}",
|
||||||
|
"quick_config_dialog_title": "Настройка в один клик",
|
||||||
|
"config_operation_failed": "Операция настройки не удалась",
|
||||||
|
"config_file_not_found_changed_path": "Ошибка настройки: файл конфигурации не найден. Возможно, {0} установлен в пользовательский путь.",
|
||||||
|
"log_batch_add_failed": "Пакетное добавление не удалось: {0}",
|
||||||
|
"log_batch_remove_failed": "Пакетное удаление не удалось: {0}",
|
||||||
|
"log_copied_config": "Конфигурация {0} скопирована в буфер обмена",
|
||||||
|
"log_gen_config_failed": "Не удалось сгенерировать конфигурацию",
|
||||||
|
"log_copy_config_failed": "Не удалось скопировать конфигурацию: {0}",
|
||||||
|
"log_copied_command": "Команда {0} скопирована в буфер обмена",
|
||||||
|
"log_copy_command_failed": "Не удалось скопировать команду: {0}",
|
||||||
|
"log_opened_config_file": "Файл конфигурации открыт: {0}",
|
||||||
|
"log_open_config_failed": "Не удалось открыть файл конфигурации",
|
||||||
|
"log_open_config_error": "Не удалось открыть файл конфигурации: {0}",
|
||||||
|
|
||||||
|
"update_available_title": "Доступно обновление",
|
||||||
|
"update_new_version": "Новая версия v{0} доступна!",
|
||||||
|
"update_now": "Обновить сейчас",
|
||||||
|
"update_not_now": "Позже",
|
||||||
|
"update_skip_version": "Пропустить v{0}",
|
||||||
|
"check_for_updates": "Проверить обновления",
|
||||||
|
"update_checking": "Проверка...",
|
||||||
|
"update_no_update": "У вас последняя версия",
|
||||||
|
"update_urgency_critical": "Критическое обновление",
|
||||||
|
"update_urgency_high": "Важное обновление",
|
||||||
|
"update_current_version": "Текущая версия: v{0}",
|
||||||
|
"update_changelog": "Список изменений",
|
||||||
|
"auth_choose_subtitle": "Выберите способ входа, чтобы продолжить",
|
||||||
|
"update_changelog_empty": "Для этой версии не указаны примечания к выпуску.",
|
||||||
|
"subscription_login_title": "Аккаунт подписки VberAI",
|
||||||
|
"subscription_password_label": "Пароль",
|
||||||
|
"subscription_password_placeholder": "Введите пароль",
|
||||||
|
"subscription_login_button": "Войти через VberAI",
|
||||||
|
"subscription_google_button": "Войти через Google",
|
||||||
|
"subscription_google_pending": "Браузер открыт. Завершите авторизацию Google, затем вернитесь в Cocos Creator.",
|
||||||
|
"subscription_google_pending_button": "Ожидание авторизации Google...",
|
||||||
|
"subscription_register_button": "Создать аккаунт VberAI",
|
||||||
|
"subscription_logging_in": "Вход...",
|
||||||
|
"subscription_credentials_required": "Введите адрес электронной почты и пароль",
|
||||||
|
"subscription_login_success": "Вход в аккаунт подписки выполнен",
|
||||||
|
"subscription_login_failed": "Не удалось войти в аккаунт подписки",
|
||||||
|
"subscription_not_entitled": "Ваша текущая подписка не включает Cocos 3.x MCP. Оформите повышение, чтобы продолжить.",
|
||||||
|
"subscription_upgrade_to": "Перейдите на «{0}», чтобы использовать этот продукт",
|
||||||
|
"subscription_upgrade_action": "Повысить подписку",
|
||||||
|
"subscription_logged_in": "Вошедший аккаунт",
|
||||||
|
"subscription_choose_button": "Аккаунт подписки VberAI",
|
||||||
|
"license_choose_button": "Код активации",
|
||||||
|
"subscription_method_label": "Подписка",
|
||||||
|
"back_to_login_choices": "← Выбрать другой способ входа",
|
||||||
|
"subscription_signed_out_other_device": "Выполнен выход: ваш аккаунт использовался на другом устройстве.",
|
||||||
|
"subscription_google_cancelled": "Вход через Google отменён.",
|
||||||
|
"subscription_request_timeout": "Время ожидания запроса истекло. Проверьте сеть и повторите попытку.",
|
||||||
|
"subscription_network_error": "Сбой сетевого подключения. Проверьте сеть и повторите попытку.",
|
||||||
|
"subscription_server_error": "Сервер вернул недопустимый ответ. Повторите попытку позже.",
|
||||||
|
"subscription_google_authorization_failed": "Авторизация Google не завершена. Повторите попытку."
|
||||||
|
};
|
||||||
@@ -0,0 +1,244 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
"extension_name": "Cocos MCP Server",
|
||||||
|
"description": "Máy chủ AI MCP cho Cocos Creator 3.8",
|
||||||
|
"panel_title": "Máy chủ MCP",
|
||||||
|
"open_panel": "Mở bảng điều khiển MCP",
|
||||||
|
"start_server": "Khởi động máy chủ",
|
||||||
|
"stop_server": "Dừng máy chủ",
|
||||||
|
"server_status": "Trạng thái máy chủ",
|
||||||
|
"port": "Cổng",
|
||||||
|
"settings": "Cài đặt",
|
||||||
|
"connected": "Đã kết nối",
|
||||||
|
"disconnected": "Đã ngắt kết nối",
|
||||||
|
"server_running": "Máy chủ đang chạy trên cổng {0}",
|
||||||
|
"server_stopped": "Máy chủ đã dừng",
|
||||||
|
"auto_start": "Tự động khởi động",
|
||||||
|
"debug_log": "Ghi nhật ký gỡ lỗi",
|
||||||
|
"max_connections": "Số kết nối tối đa",
|
||||||
|
"connection_info": "Thông tin kết nối",
|
||||||
|
"http_url": "HTTP URL",
|
||||||
|
"copy": "Sao chép",
|
||||||
|
"save_settings": "Lưu cài đặt",
|
||||||
|
"settings_saved": "Đã lưu cài đặt thành công",
|
||||||
|
"server_started": "Máy chủ MCP đã khởi động",
|
||||||
|
"server_stopped_msg": "Máy chủ MCP đã dừng",
|
||||||
|
"failed_to_start": "Không thể khởi động máy chủ",
|
||||||
|
"failed_to_stop": "Không thể dừng máy chủ",
|
||||||
|
"failed_to_save": "Không thể lưu cài đặt",
|
||||||
|
"url_copied": "Đã sao chép HTTP URL vào bảng nhớ tạm",
|
||||||
|
"tool_manager": "Trình quản lý công cụ",
|
||||||
|
"open_tool_manager": "Mở trình quản lý công cụ",
|
||||||
|
"create_config": "Tạo cấu hình",
|
||||||
|
"edit_config": "Chỉnh sửa cấu hình",
|
||||||
|
"delete_config": "Xóa cấu hình",
|
||||||
|
"import_config": "Nhập cấu hình",
|
||||||
|
"export_config": "Xuất cấu hình",
|
||||||
|
"apply_config": "Áp dụng cấu hình",
|
||||||
|
"select_all": "Chọn tất cả",
|
||||||
|
"deselect_all": "Bỏ chọn tất cả",
|
||||||
|
"save_changes": "Lưu thay đổi",
|
||||||
|
"config_name": "Tên cấu hình",
|
||||||
|
"config_description": "Mô tả cấu hình",
|
||||||
|
"current_config": "Cấu hình hiện tại",
|
||||||
|
"tool_management": "Quản lý công cụ",
|
||||||
|
"total_tools": "Tổng số công cụ",
|
||||||
|
"enabled_tools": "Đã bật",
|
||||||
|
"disabled_tools": "Đã tắt",
|
||||||
|
"no_config_selected": "Chưa chọn cấu hình",
|
||||||
|
"select_config_first": "Vui lòng chọn một cấu hình hoặc tạo cấu hình mới trước",
|
||||||
|
"config_created": "Đã tạo cấu hình thành công",
|
||||||
|
"config_updated": "Đã cập nhật cấu hình thành công",
|
||||||
|
"config_deleted": "Đã xóa cấu hình thành công",
|
||||||
|
"config_applied": "Đã áp dụng cấu hình thành công",
|
||||||
|
"config_exported": "Đã xuất cấu hình thành công",
|
||||||
|
"config_imported": "Đã nhập cấu hình thành công",
|
||||||
|
"confirm_delete": "Xác nhận xóa",
|
||||||
|
"delete_config_confirm": "Bạn có chắc chắn muốn xóa cấu hình \"{0}\" không? Hành động này không thể hoàn tác.",
|
||||||
|
"max_config_slots_reached": "Đã đạt số lượng vị trí cấu hình tối đa ({0})",
|
||||||
|
"invalid_config_format": "Định dạng cấu hình không hợp lệ",
|
||||||
|
"invalid_json_format": "Định dạng JSON hoặc cấu trúc cấu hình không hợp lệ",
|
||||||
|
"server_tab": "Máy chủ",
|
||||||
|
"tools_tab": "Quản lý công cụ",
|
||||||
|
"available_tools": "Công cụ khả dụng",
|
||||||
|
"scene_tools": "Công cụ cảnh",
|
||||||
|
"node_tools": "Công cụ nút",
|
||||||
|
"component_tools": "Công cụ thành phần",
|
||||||
|
"prefab_tools": "Công cụ Prefab",
|
||||||
|
"project_tools": "Công cụ dự án",
|
||||||
|
"debug_tools": "Công cụ gỡ lỗi",
|
||||||
|
"preferences_tools": "Tùy chọn",
|
||||||
|
"server_tools": "Công cụ máy chủ",
|
||||||
|
"broadcast_tools": "Công cụ quảng bá",
|
||||||
|
"scene_advanced_tools": "Công cụ cảnh nâng cao",
|
||||||
|
"scene_view_tools": "Công cụ khung nhìn cảnh",
|
||||||
|
"reference_image_tools": "Công cụ ảnh tham chiếu",
|
||||||
|
"asset_advanced_tools": "Công cụ tài nguyên nâng cao",
|
||||||
|
"validation_tools": "Công cụ kiểm tra",
|
||||||
|
"brand_name": "LiDaxian MCP Series",
|
||||||
|
"brand_subtitle": "LiDaxian MCP Series",
|
||||||
|
"contact_email": "Email",
|
||||||
|
"contact_wechat": "WeChat",
|
||||||
|
"contact_github": "GitHub",
|
||||||
|
"machine_id_label": "Mã máy",
|
||||||
|
"purchase_renew": "Mua / Gia hạn",
|
||||||
|
"purchase_url": "https://www.vberai.com/game-engines/cocos",
|
||||||
|
"auth_title": "Chọn phương thức đăng nhập",
|
||||||
|
"auth_subtitle": "Nhập email và mã kích hoạt của bạn",
|
||||||
|
"email_label": "Địa chỉ email",
|
||||||
|
"activation_code_label": "Mã kích hoạt",
|
||||||
|
"email_placeholder": "Nhập email",
|
||||||
|
"code_placeholder": "Nhập mã (XXXXX-XXXXX-XXXXX-XXXXX-XXXXX)",
|
||||||
|
"activate_button": "Kích hoạt",
|
||||||
|
"deactivate_button": "Hủy kích hoạt",
|
||||||
|
"activating": "Đang kích hoạt...",
|
||||||
|
"deactivating": "Đang hủy kích hoạt...",
|
||||||
|
"license_status": "Trạng thái giấy phép",
|
||||||
|
"license_type": "Loại giấy phép",
|
||||||
|
"expiry_date": "Ngày hết hạn",
|
||||||
|
"days_remaining": "Số ngày còn lại",
|
||||||
|
"days_unit": "ngày",
|
||||||
|
"permanent": "Vĩnh viễn",
|
||||||
|
"expiry_warning_title": "Giấy phép sắp hết hạn",
|
||||||
|
"expiry_warning_message": "Giấy phép của bạn sẽ hết hạn sau {0} ngày. Vui lòng gia hạn để tránh gián đoạn dịch vụ.",
|
||||||
|
"renew_now": "Gia hạn ngay",
|
||||||
|
"i_understand": "Tôi đã hiểu",
|
||||||
|
"activation_success": "Đã kích hoạt giấy phép thành công!",
|
||||||
|
"activation_failed": "Kích hoạt thất bại: {0}",
|
||||||
|
"invalid_code": "Định dạng mã kích hoạt không hợp lệ",
|
||||||
|
"purchase_license": "Mua giấy phép",
|
||||||
|
"renew_license": "Gia hạn giấy phép",
|
||||||
|
"contact_support": "Hỗ trợ",
|
||||||
|
"language_switch": "Tiếng Việt",
|
||||||
|
"version_label": "Phiên bản",
|
||||||
|
"license_required": "Cần có giấy phép để khởi động máy chủ",
|
||||||
|
"license_check_failed": "Kiểm tra giấy phép thất bại",
|
||||||
|
|
||||||
|
"status_stopped": "Đã dừng",
|
||||||
|
"status_running": "Đang chạy",
|
||||||
|
"status_error": "Kết nối thất bại",
|
||||||
|
"config_tab": "Cấu hình nhanh",
|
||||||
|
"status_label": "Trạng thái",
|
||||||
|
"connections_count": "Số kết nối",
|
||||||
|
"transport_protocol": "Giao thức truyền tải",
|
||||||
|
"transport_type": "Loại truyền tải",
|
||||||
|
"mcp_url": "MCP URL",
|
||||||
|
"port_fallback_title": "Cổng {0} không khả dụng — đã chuyển sang {1}",
|
||||||
|
"port_fallback_reason": "Cổng {0} bị hệ thống dành riêng hoặc bị tường lửa/phần mềm diệt virus chặn (thường gặp trên Windows có cài Hyper-V, WSL2 hoặc Docker). Máy chủ đang chạy bình thường trên cổng mới.",
|
||||||
|
"port_fallback_resynced": "Đã cập nhật cấu hình cho: {0}. Hãy khởi động lại các client này để cổng mới có hiệu lực.",
|
||||||
|
"port_fallback_no_clients": "Nếu bạn đã cấu hình client AI, hãy cấu hình lại sang cổng mới bằng thiết lập một chạm.",
|
||||||
|
"port_fallback_dismiss": "Đã hiểu",
|
||||||
|
"port_label": "Cổng",
|
||||||
|
"auto_start_label": "Tự động khởi động",
|
||||||
|
"debug_log_label": "Nhật ký gỡ lỗi",
|
||||||
|
"max_connections_label": "Số kết nối tối đa",
|
||||||
|
"server_settings_title": "Cài đặt máy chủ",
|
||||||
|
"server_status_title": "Trạng thái máy chủ",
|
||||||
|
"connection_info_title": "Thông tin kết nối",
|
||||||
|
"tools_count": "{0} công cụ",
|
||||||
|
"tools_stats": "{0} công cụ ({1} đã bật / {2} đã tắt)",
|
||||||
|
"quick_config_title": "Cấu hình nhanh máy khách MCP",
|
||||||
|
"quick_config_desc": "Tự động cấu hình máy chủ MCP hiện tại vào các trình soạn thảo AI phổ biến và công cụ CLI",
|
||||||
|
"server_info": "Thông tin máy chủ",
|
||||||
|
"server_name_label": "Tên máy chủ",
|
||||||
|
"server_address": "Địa chỉ máy chủ",
|
||||||
|
"timeout_ms": "Thời gian chờ (ms)",
|
||||||
|
"auto_config_title": "Cấu hình tự động (Trình soạn thảo IDE + Codex CLI)",
|
||||||
|
"add_all": "Thêm tất cả",
|
||||||
|
"remove_all": "Xóa tất cả",
|
||||||
|
"auto_config_note": "Nhấp nút để tự động sửa đổi tệp cấu hình (Cursor, Windsurf, Trae, Codex CLI)",
|
||||||
|
"configured": "Đã cấu hình",
|
||||||
|
"not_configured": "Chưa cấu hình",
|
||||||
|
"auto_config_btn": "Cấu hình tự động",
|
||||||
|
"delete_btn": "Xóa",
|
||||||
|
"copy_config": "Sao chép cấu hình",
|
||||||
|
"open_config": "Mở cấu hình",
|
||||||
|
"manual_config_title": "Cấu hình thủ công (Claude CLI + Gemini CLI)",
|
||||||
|
"refresh_commands": "Làm mới lệnh",
|
||||||
|
"manual_config_note": "Các công cụ CLI sau đây cần thực thi lệnh thủ công trong terminal",
|
||||||
|
"config_scope": "Phạm vi cấu hình:",
|
||||||
|
"scope_user": "Cấp người dùng",
|
||||||
|
"scope_project": "Cấp dự án",
|
||||||
|
"operation_log": "Nhật ký thao tác",
|
||||||
|
"no_logs": "Không có bản ghi thao tác",
|
||||||
|
"copyright_line": "© 2025 LiDaxian - Cocos Creator MCP Server v{0}",
|
||||||
|
"protocol_line": "Dựa trên giao thức Streamable HTTP (MCP 2025-03-26)",
|
||||||
|
"tools_copyright_line1": "Hệ thống công cụ - 13 công cụ cấp ý định, 150+ thao tác trình soạn thảo, điều phối mã action",
|
||||||
|
"config_copyright_line1": "Cấu hình nhanh - Hỗ trợ Cursor, Windsurf, Trae, Codex CLI, Claude CLI, Gemini CLI",
|
||||||
|
"email_required": "Vui lòng nhập email và mã kích hoạt",
|
||||||
|
"permanent_label": "Vĩnh viễn",
|
||||||
|
"logout_button": "Đăng xuất",
|
||||||
|
"invite_code_label": "Mã mời",
|
||||||
|
"copy_button": "Sao chép",
|
||||||
|
"change_code_button": "Thay đổi mã",
|
||||||
|
"change_code_title": "Thay đổi mã kích hoạt",
|
||||||
|
"change_code_desc": "Nhập mã kích hoạt mới của bạn. Liên kết thiết bị hiện tại sẽ được giải phóng trước.",
|
||||||
|
"confirm_change": "Xác nhận",
|
||||||
|
"cancel": "Hủy",
|
||||||
|
"code_required": "Vui lòng nhập mã kích hoạt",
|
||||||
|
"expiry_inline_warning": "Sắp hết hạn!",
|
||||||
|
"store_purchase_hint": "Nếu bạn đã mua plugin này từ Cocos Store, vui lòng liên hệ với tôi để nhận mã kích hoạt.",
|
||||||
|
"license_expired_title": "Giấy phép đã hết hạn",
|
||||||
|
"license_expired_message": "Giấy phép của bạn đã hết hạn và không thể khởi động máy chủ. Vui lòng gia hạn hoặc kích hoạt lại để tiếp tục.",
|
||||||
|
"go_to_login": "Đi đến đăng nhập",
|
||||||
|
|
||||||
|
"log_load_config_failed": "Không thể tải trạng thái cấu hình: {0}",
|
||||||
|
"log_gen_cli_failed": "Không thể tạo lệnh CLI: {0}",
|
||||||
|
"log_add_client_failed": "Không thể thêm vào {0}: {1}",
|
||||||
|
"log_remove_client_failed": "Không thể xóa khỏi {0}: {1}",
|
||||||
|
"quick_config_dialog_title": "Cấu hình một chạm",
|
||||||
|
"config_operation_failed": "Thao tác cấu hình thất bại",
|
||||||
|
"config_file_not_found_changed_path": "Cấu hình thất bại: không tìm thấy tệp cấu hình. {0} có thể được cài đặt trong một đường dẫn tùy chỉnh.",
|
||||||
|
"log_batch_add_failed": "Thêm hàng loạt thất bại: {0}",
|
||||||
|
"log_batch_remove_failed": "Xóa hàng loạt thất bại: {0}",
|
||||||
|
"log_copied_config": "Đã sao chép cấu hình {0} vào bảng nhớ tạm",
|
||||||
|
"log_gen_config_failed": "Không thể tạo cấu hình",
|
||||||
|
"log_copy_config_failed": "Không thể sao chép cấu hình: {0}",
|
||||||
|
"log_copied_command": "Đã sao chép lệnh {0} vào bảng nhớ tạm",
|
||||||
|
"log_copy_command_failed": "Không thể sao chép lệnh: {0}",
|
||||||
|
"log_opened_config_file": "Đã mở tệp cấu hình: {0}",
|
||||||
|
"log_open_config_failed": "Không thể mở tệp cấu hình",
|
||||||
|
"log_open_config_error": "Không thể mở tệp cấu hình: {0}",
|
||||||
|
|
||||||
|
"update_available_title": "Có bản cập nhật",
|
||||||
|
"update_new_version": "Đã có phiên bản mới v{0}!",
|
||||||
|
"update_now": "Cập nhật ngay",
|
||||||
|
"update_not_now": "Để sau",
|
||||||
|
"update_skip_version": "Bỏ qua v{0}",
|
||||||
|
"check_for_updates": "Kiểm tra cập nhật",
|
||||||
|
"update_checking": "Đang kiểm tra...",
|
||||||
|
"update_no_update": "Bạn đang sử dụng phiên bản mới nhất",
|
||||||
|
"update_urgency_critical": "Cập nhật khẩn cấp",
|
||||||
|
"update_urgency_high": "Cập nhật quan trọng",
|
||||||
|
"update_current_version": "Phiên bản hiện tại: v{0}",
|
||||||
|
"update_changelog": "Nhật ký thay đổi",
|
||||||
|
"auth_choose_subtitle": "Chọn một phương thức đăng nhập để tiếp tục",
|
||||||
|
"update_changelog_empty": "Không có ghi chú phát hành cho phiên bản này.",
|
||||||
|
"subscription_login_title": "Tài khoản đăng ký VberAI",
|
||||||
|
"subscription_password_label": "Mật khẩu",
|
||||||
|
"subscription_password_placeholder": "Nhập mật khẩu",
|
||||||
|
"subscription_login_button": "Đăng nhập bằng VberAI",
|
||||||
|
"subscription_google_button": "Đăng nhập bằng Google",
|
||||||
|
"subscription_google_pending": "Đã mở trình duyệt. Hoàn tất ủy quyền Google rồi quay lại Cocos Creator.",
|
||||||
|
"subscription_google_pending_button": "Đang chờ ủy quyền Google...",
|
||||||
|
"subscription_register_button": "Tạo tài khoản VberAI",
|
||||||
|
"subscription_logging_in": "Đang đăng nhập...",
|
||||||
|
"subscription_credentials_required": "Vui lòng nhập email và mật khẩu",
|
||||||
|
"subscription_login_success": "Đã đăng nhập tài khoản đăng ký",
|
||||||
|
"subscription_login_failed": "Đăng nhập đăng ký thất bại",
|
||||||
|
"subscription_not_entitled": "Gói đăng ký hiện tại của bạn không bao gồm Cocos 3.x MCP. Nâng cấp để tiếp tục.",
|
||||||
|
"subscription_upgrade_to": "Nâng cấp lên \"{0}\" để sử dụng sản phẩm này",
|
||||||
|
"subscription_upgrade_action": "Nâng cấp gói đăng ký",
|
||||||
|
"subscription_logged_in": "Tài khoản đã đăng nhập",
|
||||||
|
"subscription_choose_button": "Tài khoản đăng ký VberAI",
|
||||||
|
"license_choose_button": "Mã kích hoạt",
|
||||||
|
"subscription_method_label": "Đăng ký",
|
||||||
|
"back_to_login_choices": "← Chọn phương thức đăng nhập khác",
|
||||||
|
"subscription_signed_out_other_device": "Đã đăng xuất: tài khoản của bạn đã được dùng trên thiết bị khác.",
|
||||||
|
"subscription_google_cancelled": "Đã hủy đăng nhập Google.",
|
||||||
|
"subscription_request_timeout": "Yêu cầu đã hết thời gian. Kiểm tra mạng và thử lại.",
|
||||||
|
"subscription_network_error": "Kết nối mạng thất bại. Kiểm tra mạng và thử lại.",
|
||||||
|
"subscription_server_error": "Máy chủ trả về phản hồi không hợp lệ. Thử lại sau.",
|
||||||
|
"subscription_google_authorization_failed": "Ủy quyền Google chưa hoàn tất. Vui lòng thử lại."
|
||||||
|
};
|
||||||
@@ -0,0 +1,243 @@
|
|||||||
|
"use strict";
|
||||||
|
module.exports = {
|
||||||
|
"extension_name": "Cocos MCP 服务器",
|
||||||
|
"description": "适用于 Cocos Creator 3.8 的 AI MCP 服务器",
|
||||||
|
"panel_title": "MCP 服务器",
|
||||||
|
"open_panel": "打开 MCP 面板",
|
||||||
|
"start_server": "启动服务器",
|
||||||
|
"stop_server": "停止服务器",
|
||||||
|
"server_status": "服务器状态",
|
||||||
|
"port": "端口",
|
||||||
|
"settings": "设置",
|
||||||
|
"connected": "已连接",
|
||||||
|
"disconnected": "未连接",
|
||||||
|
"server_running": "服务器正在端口 {0} 上运行",
|
||||||
|
"server_stopped": "服务器已停止",
|
||||||
|
"auto_start": "自动启动",
|
||||||
|
"debug_log": "调试日志",
|
||||||
|
"max_connections": "最大连接数",
|
||||||
|
"connection_info": "连接信息",
|
||||||
|
"http_url": "HTTP 地址",
|
||||||
|
"copy": "复制",
|
||||||
|
"save_settings": "保存设置",
|
||||||
|
"settings_saved": "设置保存成功",
|
||||||
|
"server_started": "MCP 服务器已启动",
|
||||||
|
"server_stopped_msg": "MCP 服务器已停止",
|
||||||
|
"failed_to_start": "启动服务器失败",
|
||||||
|
"failed_to_stop": "停止服务器失败",
|
||||||
|
"failed_to_save": "保存设置失败",
|
||||||
|
"url_copied": "HTTP 地址已复制到剪贴板",
|
||||||
|
"tool_manager": "工具管理器",
|
||||||
|
"open_tool_manager": "打开工具管理器",
|
||||||
|
"create_config": "新建配置",
|
||||||
|
"edit_config": "编辑配置",
|
||||||
|
"delete_config": "删除配置",
|
||||||
|
"import_config": "导入配置",
|
||||||
|
"export_config": "导出配置",
|
||||||
|
"apply_config": "应用配置",
|
||||||
|
"select_all": "全选",
|
||||||
|
"deselect_all": "取消全选",
|
||||||
|
"save_changes": "保存更改",
|
||||||
|
"config_name": "配置名称",
|
||||||
|
"config_description": "配置描述",
|
||||||
|
"current_config": "当前配置",
|
||||||
|
"tool_management": "工具管理",
|
||||||
|
"total_tools": "总工具数",
|
||||||
|
"enabled_tools": "已启用",
|
||||||
|
"disabled_tools": "已禁用",
|
||||||
|
"no_config_selected": "没有选择配置",
|
||||||
|
"select_config_first": "请先选择一个配置或创建新配置",
|
||||||
|
"config_created": "配置创建成功",
|
||||||
|
"config_updated": "配置更新成功",
|
||||||
|
"config_deleted": "配置删除成功",
|
||||||
|
"config_applied": "配置应用成功",
|
||||||
|
"config_exported": "配置导出成功",
|
||||||
|
"config_imported": "配置导入成功",
|
||||||
|
"confirm_delete": "确认删除",
|
||||||
|
"delete_config_confirm": "确定要删除配置 \"{0}\" 吗?此操作不可撤销。",
|
||||||
|
"max_config_slots_reached": "已达到最大配置槽位数量 ({0})",
|
||||||
|
"invalid_config_format": "无效的配置格式",
|
||||||
|
"invalid_json_format": "无效的JSON格式或配置结构",
|
||||||
|
"server_tab": "服务器",
|
||||||
|
"tools_tab": "工具管理",
|
||||||
|
"available_tools": "可用工具",
|
||||||
|
"scene_tools": "场景工具",
|
||||||
|
"node_tools": "节点工具",
|
||||||
|
"component_tools": "组件工具",
|
||||||
|
"prefab_tools": "预制体工具",
|
||||||
|
"project_tools": "项目工具",
|
||||||
|
"debug_tools": "调试工具",
|
||||||
|
"preferences_tools": "偏好设置",
|
||||||
|
"server_tools": "服务器工具",
|
||||||
|
"broadcast_tools": "广播工具",
|
||||||
|
"scene_advanced_tools": "高级场景工具",
|
||||||
|
"scene_view_tools": "场景视图工具",
|
||||||
|
"reference_image_tools": "参考图片工具",
|
||||||
|
"asset_advanced_tools": "高级资源工具",
|
||||||
|
"validation_tools": "验证工具",
|
||||||
|
"brand_name": "李大仙 MCP 系列",
|
||||||
|
"brand_subtitle": "LiDaxian MCP Series",
|
||||||
|
"contact_email": "邮箱",
|
||||||
|
"contact_wechat": "微信",
|
||||||
|
"contact_github": "GitHub",
|
||||||
|
"machine_id_label": "机器码",
|
||||||
|
"purchase_renew": "购买/续费",
|
||||||
|
"purchase_url": "https://www.vberai.com/game-engines/cocos",
|
||||||
|
"auth_title": "选择登录方式",
|
||||||
|
"auth_subtitle": "请输入您的邮箱和激活码",
|
||||||
|
"auth_choose_subtitle": "请选择一种登录方式继续",
|
||||||
|
"email_label": "邮箱地址",
|
||||||
|
"activation_code_label": "激活码",
|
||||||
|
"email_placeholder": "请输入邮箱",
|
||||||
|
"code_placeholder": "请输入激活码 (XXXXX-XXXXX-XXXXX-XXXXX-XXXXX)",
|
||||||
|
"activate_button": "激活",
|
||||||
|
"deactivate_button": "取消激活",
|
||||||
|
"activating": "激活中...",
|
||||||
|
"deactivating": "取消激活中...",
|
||||||
|
"license_status": "授权状态",
|
||||||
|
"license_type": "授权类型",
|
||||||
|
"expiry_date": "到期日期",
|
||||||
|
"days_remaining": "剩余天数",
|
||||||
|
"days_unit": "天",
|
||||||
|
"permanent": "永久",
|
||||||
|
"expiry_warning_title": "授权即将到期",
|
||||||
|
"expiry_warning_message": "您的授权将在 {0} 天后到期,请及时续费以确保服务不中断。",
|
||||||
|
"renew_now": "立即续费",
|
||||||
|
"i_understand": "我知道了",
|
||||||
|
"activation_success": "授权激活成功!",
|
||||||
|
"activation_failed": "激活失败:{0}",
|
||||||
|
"invalid_code": "无效的激活码格式",
|
||||||
|
"purchase_license": "购买授权",
|
||||||
|
"renew_license": "续费授权",
|
||||||
|
"contact_support": "技术支持",
|
||||||
|
"language_switch": "中文",
|
||||||
|
"version_label": "版本",
|
||||||
|
"license_required": "需要授权才能启动服务器",
|
||||||
|
"license_check_failed": "授权检查失败",
|
||||||
|
|
||||||
|
"status_stopped": "已停止",
|
||||||
|
"status_running": "运行中",
|
||||||
|
"status_error": "连接失败",
|
||||||
|
"config_tab": "一键配置",
|
||||||
|
"status_label": "状态",
|
||||||
|
"connections_count": "连接数",
|
||||||
|
"transport_protocol": "传输协议",
|
||||||
|
"transport_type": "传输类型",
|
||||||
|
"mcp_url": "MCP URL",
|
||||||
|
"port_fallback_title": "端口 {0} 不可用,已自动切换到 {1}",
|
||||||
|
"port_fallback_reason": "端口 {0} 被系统保留或被防火墙/杀毒软件拦截(安装了 Hyper-V、WSL2 或 Docker 的 Windows 上很常见)。服务已在新端口上正常运行。",
|
||||||
|
"port_fallback_resynced": "已自动更新这些客户端的配置:{0}。请重启它们,新端口才会生效。",
|
||||||
|
"port_fallback_no_clients": "如果已配置过 AI 客户端,请在\"一键配置\"里重新配置到新端口。",
|
||||||
|
"port_fallback_dismiss": "知道了",
|
||||||
|
"port_label": "端口",
|
||||||
|
"auto_start_label": "自动启动",
|
||||||
|
"debug_log_label": "调试日志",
|
||||||
|
"max_connections_label": "最大连接数",
|
||||||
|
"server_settings_title": "服务器设置",
|
||||||
|
"server_status_title": "服务器状态",
|
||||||
|
"connection_info_title": "连接信息",
|
||||||
|
"tools_count": "{0} 个工具",
|
||||||
|
"tools_stats": "{0} 个工具 ({1} 启用 / {2} 禁用)",
|
||||||
|
"quick_config_title": "一键配置 MCP 客户端",
|
||||||
|
"quick_config_desc": "自动将当前MCP服务器配置到主流AI编辑器和CLI工具中",
|
||||||
|
"server_info": "服务器信息",
|
||||||
|
"server_name_label": "服务器名称",
|
||||||
|
"server_address": "服务器地址",
|
||||||
|
"timeout_ms": "超时时间(ms)",
|
||||||
|
"auto_config_title": "自动配置 (IDE编辑器 + Codex CLI)",
|
||||||
|
"add_all": "全部添加",
|
||||||
|
"remove_all": "全部删除",
|
||||||
|
"auto_config_note": "点击按钮将自动修改配置文件(包括Cursor、Windsurf、Trae、Codex CLI)",
|
||||||
|
"configured": "已配置",
|
||||||
|
"not_configured": "未配置",
|
||||||
|
"auto_config_btn": "自动配置",
|
||||||
|
"delete_btn": "删除",
|
||||||
|
"copy_config": "复制配置",
|
||||||
|
"open_config": "打开配置",
|
||||||
|
"manual_config_title": "手动配置 (Claude CLI + Gemini CLI)",
|
||||||
|
"refresh_commands": "刷新命令",
|
||||||
|
"manual_config_note": "以下CLI工具需要在终端中手动执行命令配置",
|
||||||
|
"config_scope": "配置范围:",
|
||||||
|
"scope_user": "用户级别",
|
||||||
|
"scope_project": "项目级别",
|
||||||
|
"operation_log": "操作日志",
|
||||||
|
"no_logs": "暂无操作记录",
|
||||||
|
"copyright_line": "© 2025 李大仙 (LiDaxian) - Cocos Creator MCP Server v{0}",
|
||||||
|
"protocol_line": "基于 Streamable HTTP 协议 (MCP 2025-03-26)",
|
||||||
|
"tools_copyright_line1": "工具系统 - 13个意图级工具,150+编辑器操作,action操作码分发",
|
||||||
|
"config_copyright_line1": "一键配置 - 支持 Cursor、Windsurf、Trae、Codex CLI、Claude CLI、Gemini CLI",
|
||||||
|
"email_required": "请输入邮箱和激活码",
|
||||||
|
"permanent_label": "永久",
|
||||||
|
"logout_button": "退出登录",
|
||||||
|
"invite_code_label": "邀请码",
|
||||||
|
"copy_button": "复制",
|
||||||
|
"change_code_button": "变更激活码",
|
||||||
|
"change_code_title": "变更激活码",
|
||||||
|
"change_code_desc": "输入新的激活码,当前设备的激活占位将被释放后重新激活。",
|
||||||
|
"confirm_change": "确认变更",
|
||||||
|
"cancel": "取消",
|
||||||
|
"code_required": "请输入激活码",
|
||||||
|
"expiry_inline_warning": "即将到期!",
|
||||||
|
"store_purchase_hint": "如果您已在 Cocos Store 购买了本插件,请联系我获取激活码。",
|
||||||
|
"license_expired_title": "授权已过期",
|
||||||
|
"license_expired_message": "您的授权已过期,无法启动服务。请续费或重新激活以继续使用。",
|
||||||
|
"go_to_login": "重新登录",
|
||||||
|
|
||||||
|
"log_load_config_failed": "加载配置状态失败: {0}",
|
||||||
|
"log_gen_cli_failed": "生成CLI命令失败: {0}",
|
||||||
|
"log_add_client_failed": "添加到{0}失败: {1}",
|
||||||
|
"log_remove_client_failed": "从{0}删除失败: {1}",
|
||||||
|
"quick_config_dialog_title": "一键配置",
|
||||||
|
"config_operation_failed": "配置操作失败",
|
||||||
|
"config_file_not_found_changed_path": "配置失败:找不到配置文件,{0} 安装时可能更改了路径",
|
||||||
|
"log_batch_add_failed": "批量添加失败: {0}",
|
||||||
|
"log_batch_remove_failed": "批量删除失败: {0}",
|
||||||
|
"log_copied_config": "已复制{0}配置到剪贴板",
|
||||||
|
"log_gen_config_failed": "生成配置失败",
|
||||||
|
"log_copy_config_failed": "复制配置失败: {0}",
|
||||||
|
"log_copied_command": "已复制{0}命令到剪贴板",
|
||||||
|
"log_copy_command_failed": "复制命令失败: {0}",
|
||||||
|
"log_opened_config_file": "已打开配置文件: {0}",
|
||||||
|
"log_open_config_failed": "打开配置文件失败",
|
||||||
|
"log_open_config_error": "打开配置文件失败: {0}",
|
||||||
|
|
||||||
|
"update_available_title": "发现新版本",
|
||||||
|
"update_new_version": "新版本 v{0} 已发布!",
|
||||||
|
"update_now": "立即更新",
|
||||||
|
"update_not_now": "暂不更新",
|
||||||
|
"update_skip_version": "跳过 v{0}",
|
||||||
|
"check_for_updates": "检查更新",
|
||||||
|
"update_checking": "检查中...",
|
||||||
|
"update_no_update": "当前已是最新版本",
|
||||||
|
"update_urgency_critical": "紧急更新",
|
||||||
|
"update_urgency_high": "重要更新",
|
||||||
|
"update_current_version": "当前版本: v{0}",
|
||||||
|
"update_changelog": "更新日志"
|
||||||
|
,"update_changelog_empty": "该版本暂未填写更新内容。"
|
||||||
|
,"subscription_login_title": "VberAI 官网订阅账号"
|
||||||
|
,"subscription_password_label": "密码"
|
||||||
|
,"subscription_password_placeholder": "请输入密码"
|
||||||
|
,"subscription_login_button": "使用 VberAI 账号登录"
|
||||||
|
,"subscription_google_button": "使用 Google 登录"
|
||||||
|
,"subscription_google_pending": "已打开浏览器,请完成 Google 授权后返回 Cocos Creator。"
|
||||||
|
,"subscription_google_pending_button": "正在等待 Google 授权..."
|
||||||
|
,"subscription_register_button": "注册 VberAI 账号"
|
||||||
|
,"subscription_logging_in": "登录中..."
|
||||||
|
,"subscription_credentials_required": "请输入邮箱和密码"
|
||||||
|
,"subscription_login_success": "订阅账号登录成功"
|
||||||
|
,"subscription_login_failed": "订阅账号登录失败"
|
||||||
|
,"subscription_not_entitled": "当前订阅计划不包含 Cocos 3.x MCP,请升级后使用。"
|
||||||
|
,"subscription_upgrade_to": "升级到「{0}」即可使用"
|
||||||
|
,"subscription_upgrade_action": "前往升级订阅"
|
||||||
|
,"subscription_logged_in": "当前登录账号"
|
||||||
|
,"subscription_choose_button": "VberAI 官网订阅账号"
|
||||||
|
,"license_choose_button": "激活码授权"
|
||||||
|
,"subscription_method_label": "订阅账号"
|
||||||
|
,"back_to_login_choices": "← 返回选择登录方式"
|
||||||
|
,"subscription_signed_out_other_device": "账号已在另一台设备登录,当前设备已退出。"
|
||||||
|
,"subscription_google_cancelled": "已取消 Google 登录"
|
||||||
|
,"subscription_request_timeout": "请求超时,请检查网络后重试。"
|
||||||
|
,"subscription_network_error": "网络连接失败,请检查网络后重试。"
|
||||||
|
,"subscription_server_error": "服务器响应异常,请稍后重试。"
|
||||||
|
,"subscription_google_authorization_failed": "Google 授权未完成,请重试。"
|
||||||
|
};
|
||||||
@@ -0,0 +1,259 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./@types/schema/package/index.json",
|
||||||
|
"package_version": 2,
|
||||||
|
"name": "cocos-mcp-server",
|
||||||
|
"version": "1.7.9",
|
||||||
|
"author": "LiDaxian",
|
||||||
|
"editor": ">=3.7.0",
|
||||||
|
"description": "i18n:cocos-mcp-server.description",
|
||||||
|
"main": "./dist/main.js",
|
||||||
|
"dependencies": {
|
||||||
|
"@iarna/toml": "^2.2.5",
|
||||||
|
"fs-extra": "^11.3.0",
|
||||||
|
"puppeteer-core": "^25.1.0",
|
||||||
|
"uuid": "^9.0.1",
|
||||||
|
"vue": "^3.1.4"
|
||||||
|
},
|
||||||
|
"panels": {
|
||||||
|
"default": {
|
||||||
|
"title": "i18n:cocos-mcp-server.panel_title",
|
||||||
|
"type": "dockable",
|
||||||
|
"main": "dist/panels/default",
|
||||||
|
"icon": "./static/icon.png",
|
||||||
|
"size": {
|
||||||
|
"min-width": 560,
|
||||||
|
"min-height": 680,
|
||||||
|
"width": 760,
|
||||||
|
"height": 820
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contributions": {
|
||||||
|
"menu": [
|
||||||
|
{
|
||||||
|
"path": "i18n:menu.extension/Cocos MCP Server",
|
||||||
|
"label": "i18n:cocos-mcp-server.open_panel",
|
||||||
|
"message": "open-panel"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"messages": {
|
||||||
|
"open-panel": {
|
||||||
|
"methods": [
|
||||||
|
"openPanel"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"open-tool-manager": {
|
||||||
|
"methods": [
|
||||||
|
"openToolManager"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"start-server": {
|
||||||
|
"methods": [
|
||||||
|
"startServer"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"stop-server": {
|
||||||
|
"methods": [
|
||||||
|
"stopServer"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"get-server-status": {
|
||||||
|
"methods": [
|
||||||
|
"getServerStatus"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"update-settings": {
|
||||||
|
"methods": [
|
||||||
|
"updateSettings"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"getToolsList": {
|
||||||
|
"methods": [
|
||||||
|
"getToolsList"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"get-server-settings": {
|
||||||
|
"methods": [
|
||||||
|
"getServerSettings"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"getToolManagerState": {
|
||||||
|
"methods": [
|
||||||
|
"getToolManagerState"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"createToolConfiguration": {
|
||||||
|
"methods": [
|
||||||
|
"createToolConfiguration"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"updateToolConfiguration": {
|
||||||
|
"methods": [
|
||||||
|
"updateToolConfiguration"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"deleteToolConfiguration": {
|
||||||
|
"methods": [
|
||||||
|
"deleteToolConfiguration"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"setCurrentToolConfiguration": {
|
||||||
|
"methods": [
|
||||||
|
"setCurrentToolConfiguration"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"updateToolStatus": {
|
||||||
|
"methods": [
|
||||||
|
"updateToolStatus"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"updateToolStatusBatch": {
|
||||||
|
"methods": [
|
||||||
|
"updateToolStatusBatch"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"exportToolConfiguration": {
|
||||||
|
"methods": [
|
||||||
|
"exportToolConfiguration"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"importToolConfiguration": {
|
||||||
|
"methods": [
|
||||||
|
"importToolConfiguration"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"getEnabledTools": {
|
||||||
|
"methods": [
|
||||||
|
"getEnabledTools"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"get-config-status": {
|
||||||
|
"methods": [
|
||||||
|
"getConfigStatus"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"generate-cli-commands": {
|
||||||
|
"methods": [
|
||||||
|
"generateCLICommands"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"generate-client-config": {
|
||||||
|
"methods": [
|
||||||
|
"generateClientConfig"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"add-to-client": {
|
||||||
|
"methods": [
|
||||||
|
"addToClient"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"remove-from-client": {
|
||||||
|
"methods": [
|
||||||
|
"removeFromClient"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"add-to-all-clients": {
|
||||||
|
"methods": [
|
||||||
|
"addToAllClients"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"remove-from-all-clients": {
|
||||||
|
"methods": [
|
||||||
|
"removeFromAllClients"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"open-config-file": {
|
||||||
|
"methods": [
|
||||||
|
"openConfigFile"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"check-license": {
|
||||||
|
"methods": [
|
||||||
|
"checkLicense"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"subscription-status": {
|
||||||
|
"methods": [
|
||||||
|
"subscriptionStatus"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"login-subscription": {
|
||||||
|
"methods": [
|
||||||
|
"loginSubscription"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"login-subscription-google": {
|
||||||
|
"methods": [
|
||||||
|
"loginSubscriptionGoogle"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"logout-subscription": {
|
||||||
|
"methods": [
|
||||||
|
"logoutSubscription"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"activate-license": {
|
||||||
|
"methods": [
|
||||||
|
"activateLicense"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"deactivate-license": {
|
||||||
|
"methods": [
|
||||||
|
"deactivateLicense"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"get-license-info": {
|
||||||
|
"methods": [
|
||||||
|
"getLicenseInfo"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"check-update": {
|
||||||
|
"methods": [
|
||||||
|
"checkUpdate"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"skip-update": {
|
||||||
|
"methods": [
|
||||||
|
"skipUpdate"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"get-machine-id": {
|
||||||
|
"methods": [
|
||||||
|
"getMachineId"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"preview-play": {
|
||||||
|
"methods": [
|
||||||
|
"default.onPreviewPlay"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"preview-stop": {
|
||||||
|
"methods": [
|
||||||
|
"default.onPreviewStop"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"device-removed": {
|
||||||
|
"methods": [
|
||||||
|
"default.device-removed"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scene": {
|
||||||
|
"script": "./dist/scene.js",
|
||||||
|
"methods": [
|
||||||
|
"createNewScene",
|
||||||
|
"addComponentToNode",
|
||||||
|
"removeComponentFromNode",
|
||||||
|
"getNodeInfo",
|
||||||
|
"getAllNodes",
|
||||||
|
"findNodeByName",
|
||||||
|
"setNodeProperty",
|
||||||
|
"setComponentProperty",
|
||||||
|
"getCurrentSceneInfo",
|
||||||
|
"getSceneHierarchy",
|
||||||
|
"createPrefabFromNode",
|
||||||
|
"captureContent"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+94
@@ -0,0 +1,94 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Batch auto_knowledge via MCP HTTP — gets runtime defaults from editor
|
||||||
|
* Requires: editor running with MCP on port 3000
|
||||||
|
*/
|
||||||
|
|
||||||
|
import http from 'http';
|
||||||
|
|
||||||
|
const MCP_URL = 'http://127.0.0.1:3000/mcp';
|
||||||
|
const ALL = [
|
||||||
|
'cc.Label','cc.Sprite','cc.Button','cc.Widget','cc.Layout',
|
||||||
|
'cc.UITransform','cc.RichText','cc.ScrollView','cc.Toggle',
|
||||||
|
'cc.ToggleContainer','cc.ProgressBar','cc.Slider','cc.PageView',
|
||||||
|
'cc.Mask','cc.Graphics','cc.UIOpacity','cc.SafeArea',
|
||||||
|
'cc.BlockInputEvents','cc.LabelOutline','cc.LabelShadow',
|
||||||
|
'cc.PageViewIndicator','cc.EditBox',
|
||||||
|
'cc.AudioSource','cc.VideoPlayer','cc.WebView',
|
||||||
|
'cc.ParticleSystem2D',
|
||||||
|
'cc.Camera','cc.Canvas','cc.Animation','cc.SkeletalAnimation',
|
||||||
|
'cc.MeshRenderer','cc.SkinnedMeshRenderer',
|
||||||
|
'cc.DirectionalLight','cc.PointLight','cc.SpotLight','cc.SphereLight',
|
||||||
|
'cc.RigidBody2D','cc.BoxCollider2D','cc.CircleCollider2D','cc.PolygonCollider2D',
|
||||||
|
'cc.RigidBody','cc.BoxCollider','cc.SphereCollider','cc.CapsuleCollider','cc.MeshCollider',
|
||||||
|
'cc.MotionStreak','cc.Billboard','cc.Line',
|
||||||
|
'cc.TiledMap','cc.TiledLayer',
|
||||||
|
'cc.HingeJoint2D','cc.DistanceJoint2D','cc.SpringJoint2D',
|
||||||
|
'cc.FixedJoint2D','cc.WheelJoint2D','cc.SliderJoint2D',
|
||||||
|
'cc.RelativeJoint2D','cc.MouseJoint2D',
|
||||||
|
'cc.HingeConstraint','cc.FixedConstraint','cc.PointToPointConstraint',
|
||||||
|
'cc.Terrain','cc.LODGroup','cc.ReflectionProbe',
|
||||||
|
'sp.Skeleton','dragonBones.ArmatureDisplay',
|
||||||
|
];
|
||||||
|
|
||||||
|
function callMcp(toolName, args) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const body = JSON.stringify({
|
||||||
|
jsonrpc: '2.0', id: Date.now(), method: 'tools/call',
|
||||||
|
params: { name: toolName, arguments: args }
|
||||||
|
});
|
||||||
|
const req = http.request(MCP_URL, {
|
||||||
|
method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
|
||||||
|
timeout: 30000,
|
||||||
|
}, (res) => {
|
||||||
|
const chunks = [];
|
||||||
|
res.on('data', c => chunks.push(c));
|
||||||
|
res.on('end', () => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(Buffer.concat(chunks).toString());
|
||||||
|
const content = data.result?.content || [];
|
||||||
|
const text = content.find(c => c.type === 'text')?.text;
|
||||||
|
resolve(text ? JSON.parse(text) : {});
|
||||||
|
} catch (e) { reject(e); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
req.on('error', reject);
|
||||||
|
req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
|
||||||
|
req.write(body);
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check connectivity
|
||||||
|
try {
|
||||||
|
await callMcp('cocos_editor', { action: 'project_info' });
|
||||||
|
} catch {
|
||||||
|
console.error('❌ Cannot connect to MCP at', MCP_URL);
|
||||||
|
console.error(' Make sure editor is running and plugin is loaded.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Connected. Running auto_knowledge(save:true) for ${ALL.length} components...\n`);
|
||||||
|
|
||||||
|
let ok = 0, fail = 0, withDefaults = 0;
|
||||||
|
|
||||||
|
for (const comp of ALL) {
|
||||||
|
try {
|
||||||
|
const r = await callMcp('cocos_devtools', { action: 'auto_knowledge', componentType: comp, save: true });
|
||||||
|
if (r.success) {
|
||||||
|
ok++;
|
||||||
|
const hasRuntime = r.data?.hasRuntimeDefaults;
|
||||||
|
if (hasRuntime) withDefaults++;
|
||||||
|
const propCount = Object.keys(r.data?.recommendedKnowledge?.properties || {}).length;
|
||||||
|
console.log(` ✅ ${comp.padEnd(35)} ${propCount} props ${hasRuntime ? '(+defaults)' : '(dts only)'}`);
|
||||||
|
} else {
|
||||||
|
fail++;
|
||||||
|
console.log(` ❌ ${comp}: ${r.error}`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
fail++;
|
||||||
|
console.log(` ❌ ${comp}: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\nDone: ${ok} ok, ${fail} fail, ${withDefaults} with runtime defaults`);
|
||||||
+83
@@ -0,0 +1,83 @@
|
|||||||
|
/**
|
||||||
|
* Batch auto_knowledge — generate complete COMPONENT_PROPERTIES from d.ts
|
||||||
|
* Output: knowledge JSON files + combined TS snippet for knowledge-handler.ts
|
||||||
|
*/
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
const mod = require(path.resolve(__dirname, '../dist/tools/cocos/utils/component-knowledge'));
|
||||||
|
|
||||||
|
const ALL_COMPONENTS = [
|
||||||
|
'cc.Label', 'cc.Sprite', 'cc.Button', 'cc.Widget', 'cc.Layout',
|
||||||
|
'cc.UITransform', 'cc.RichText', 'cc.ScrollView', 'cc.Toggle',
|
||||||
|
'cc.ToggleContainer', 'cc.ProgressBar', 'cc.Slider', 'cc.PageView',
|
||||||
|
'cc.Mask', 'cc.Graphics', 'cc.UIOpacity', 'cc.SafeArea',
|
||||||
|
'cc.BlockInputEvents', 'cc.LabelOutline', 'cc.LabelShadow',
|
||||||
|
'cc.PageViewIndicator', 'cc.EditBox',
|
||||||
|
'cc.AudioSource', 'cc.VideoPlayer', 'cc.WebView',
|
||||||
|
'cc.ParticleSystem2D', 'cc.ParticleSystem',
|
||||||
|
'cc.Camera', 'cc.Canvas', 'cc.Animation', 'cc.SkeletalAnimation',
|
||||||
|
'cc.MeshRenderer', 'cc.SkinnedMeshRenderer',
|
||||||
|
'cc.DirectionalLight', 'cc.PointLight', 'cc.SpotLight', 'cc.SphereLight',
|
||||||
|
'cc.RigidBody2D', 'cc.BoxCollider2D', 'cc.CircleCollider2D', 'cc.PolygonCollider2D',
|
||||||
|
'cc.RigidBody', 'cc.BoxCollider', 'cc.SphereCollider', 'cc.CapsuleCollider', 'cc.MeshCollider',
|
||||||
|
'cc.MotionStreak', 'cc.Billboard', 'cc.Line',
|
||||||
|
'cc.TiledMap', 'cc.TiledLayer',
|
||||||
|
'cc.HingeJoint2D', 'cc.DistanceJoint2D', 'cc.SpringJoint2D',
|
||||||
|
'cc.FixedJoint2D', 'cc.WheelJoint2D', 'cc.SliderJoint2D',
|
||||||
|
'cc.RelativeJoint2D', 'cc.MouseJoint2D',
|
||||||
|
'cc.HingeConstraint', 'cc.FixedConstraint', 'cc.PointToPointConstraint',
|
||||||
|
'cc.Terrain', 'cc.LODGroup', 'cc.ReflectionProbe',
|
||||||
|
'sp.Skeleton', 'dragonBones.ArmatureDisplay',
|
||||||
|
];
|
||||||
|
|
||||||
|
const snippets = [];
|
||||||
|
let saved = 0, failed = 0;
|
||||||
|
|
||||||
|
for (const comp of ALL_COMPONENTS) {
|
||||||
|
const className = mod.componentTypeToClassName(comp);
|
||||||
|
const dtsInfo = mod.parseDtsClass(className, { recursive: true });
|
||||||
|
|
||||||
|
if (!dtsInfo) {
|
||||||
|
console.log(` ❌ ${comp}: not found in d.ts`);
|
||||||
|
failed++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const classified = mod.classifyProperties(dtsInfo, null);
|
||||||
|
const knowledge = mod.buildRecommendedKnowledge(comp, dtsInfo, classified);
|
||||||
|
|
||||||
|
// Save knowledge file
|
||||||
|
mod.saveKnowledgeFile(comp, knowledge);
|
||||||
|
|
||||||
|
// Generate TS snippet
|
||||||
|
const snippet = mod.generateTsSnippet(comp, knowledge);
|
||||||
|
snippets.push(snippet);
|
||||||
|
|
||||||
|
const propCount = Object.keys(knowledge.properties).length;
|
||||||
|
console.log(` ✅ ${comp}: ${propCount} props`);
|
||||||
|
saved++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write combined snippets to file
|
||||||
|
const outputPath = path.resolve(__dirname, '../dev-knowledge/COMPONENT_PROPERTIES_GENERATED.ts');
|
||||||
|
const outputDir = path.dirname(outputPath);
|
||||||
|
if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });
|
||||||
|
|
||||||
|
const header = `/**
|
||||||
|
* AUTO-GENERATED by batch-auto-knowledge.js
|
||||||
|
* Date: ${new Date().toISOString()}
|
||||||
|
* Components: ${saved}
|
||||||
|
*
|
||||||
|
* Paste this into COMPONENT_PROPERTIES in knowledge-handler.ts
|
||||||
|
*/
|
||||||
|
|
||||||
|
const COMPONENT_PROPERTIES: Record<string, any> = {\n`;
|
||||||
|
|
||||||
|
const footer = `\n};\n`;
|
||||||
|
|
||||||
|
fs.writeFileSync(outputPath, header + snippets.join('\n') + footer, 'utf-8');
|
||||||
|
|
||||||
|
console.log(`\n Saved: ${saved} | Failed: ${failed}`);
|
||||||
|
console.log(` Knowledge files: dev-knowledge/components/`);
|
||||||
|
console.log(` TS output: ${outputPath}`);
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const { spawnSync } = require("child_process");
|
||||||
|
|
||||||
|
const PATH = {
|
||||||
|
packageJSON: path.join(__dirname, "../package.json")
|
||||||
|
};
|
||||||
|
|
||||||
|
function checkCreatorTypesVersion(version) {
|
||||||
|
try {
|
||||||
|
// 根据平台选择合适的npm命令
|
||||||
|
const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
|
||||||
|
|
||||||
|
// 检查npm命令是否可用
|
||||||
|
const npmCheck = spawnSync(npmCmd, ["--version"], {
|
||||||
|
stdio: 'pipe',
|
||||||
|
shell: process.platform === "win32"
|
||||||
|
});
|
||||||
|
|
||||||
|
if (npmCheck.error || npmCheck.status !== 0) {
|
||||||
|
console.warn("Warning: npm command not available, skipping version check");
|
||||||
|
return true; // 如果npm不可用,跳过检查
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取版本列表
|
||||||
|
const result = spawnSync(npmCmd, ["view", "@cocos/creator-types", "versions"], {
|
||||||
|
stdio: 'pipe',
|
||||||
|
shell: process.platform === "win32"
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.error || result.status !== 0) {
|
||||||
|
console.warn("Warning: Failed to fetch @cocos/creator-types versions, skipping check");
|
||||||
|
return true; // 如果获取失败,跳过检查
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = result.stdout.toString().trim();
|
||||||
|
|
||||||
|
// 尝试解析JSON
|
||||||
|
try {
|
||||||
|
const versions = JSON.parse(output);
|
||||||
|
if (Array.isArray(versions)) {
|
||||||
|
return versions.includes(version);
|
||||||
|
} else if (typeof versions === 'string') {
|
||||||
|
return versions.includes(version);
|
||||||
|
}
|
||||||
|
} catch (parseError) {
|
||||||
|
// 如果JSON解析失败,尝试作为字符串处理
|
||||||
|
return output.includes(version);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Warning: Version check failed:", error.message);
|
||||||
|
return true; // 出错时跳过检查
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCreatorTypesVersion() {
|
||||||
|
try {
|
||||||
|
// 检查package.json文件是否存在
|
||||||
|
if (!fs.existsSync(PATH.packageJSON)) {
|
||||||
|
console.warn("Warning: package.json not found");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const packageContent = fs.readFileSync(PATH.packageJSON, "utf8");
|
||||||
|
const packageJson = JSON.parse(packageContent);
|
||||||
|
|
||||||
|
// 检查devDependencies是否存在
|
||||||
|
if (!packageJson.devDependencies || !packageJson.devDependencies["@cocos/creator-types"]) {
|
||||||
|
console.warn("Warning: @cocos/creator-types not found in devDependencies");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const versionString = packageJson.devDependencies["@cocos/creator-types"];
|
||||||
|
return versionString.replace(/^[^\d]+/, "");
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Warning: Failed to read package.json:", error.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
try {
|
||||||
|
const creatorTypesVersion = getCreatorTypesVersion();
|
||||||
|
|
||||||
|
if (!creatorTypesVersion) {
|
||||||
|
console.log("Skipping @cocos/creator-types version check");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!checkCreatorTypesVersion(creatorTypesVersion)) {
|
||||||
|
console.log("\x1b[33mWarning:\x1b[0m");
|
||||||
|
console.log(" @en");
|
||||||
|
console.log(" Version check of @cocos/creator-types failed.");
|
||||||
|
console.log(` The definition of ${creatorTypesVersion} has not been released yet. Please export the definition to the ./node_modules directory by selecting "Developer -> Export Interface Definition" in the menu of the Creator editor.`);
|
||||||
|
console.log(" The definition of the corresponding version will be released on npm after the editor is officially released.");
|
||||||
|
console.log(" @zh");
|
||||||
|
console.log(" @cocos/creator-types 版本检查失败。");
|
||||||
|
console.log(` ${creatorTypesVersion} 定义还未发布,请先通过 Creator 编辑器菜单 "开发者 -> 导出接口定义",导出定义到 ./node_modules 目录。`);
|
||||||
|
console.log(" 对应版本的定义会在编辑器正式发布后同步发布到 npm 上。");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Preinstall script error:", error.message);
|
||||||
|
// 不要抛出错误,让安装继续进行
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行主函数
|
||||||
|
main();
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* Generate COMPONENT_PROPERTIES from saved dev-knowledge JSON files.
|
||||||
|
* Does NOT re-parse d.ts — uses the saved knowledge (which has runtime defaults).
|
||||||
|
*/
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
const mod = require(path.resolve(__dirname, '../dist/tools/cocos/utils/component-knowledge'));
|
||||||
|
|
||||||
|
const files = mod.listKnowledgeFiles();
|
||||||
|
const snippets = [];
|
||||||
|
|
||||||
|
for (const comp of files) {
|
||||||
|
const knowledge = mod.loadKnowledgeFile(comp);
|
||||||
|
if (!knowledge) continue;
|
||||||
|
snippets.push(mod.generateTsSnippet(comp, knowledge));
|
||||||
|
const propCount = Object.keys(knowledge.properties).length;
|
||||||
|
const defaultCount = Object.values(knowledge.properties).filter(p => p.default !== undefined).length;
|
||||||
|
console.log(` ${comp.padEnd(35)} ${propCount} props, ${defaultCount} with defaults`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const outputPath = path.resolve(__dirname, '../dev-knowledge/COMPONENT_PROPERTIES_GENERATED.txt');
|
||||||
|
const header = `const COMPONENT_PROPERTIES: Record<string, any> = {\n`;
|
||||||
|
const footer = `\n};\n`;
|
||||||
|
fs.writeFileSync(outputPath, header + snippets.join('\n') + footer, 'utf-8');
|
||||||
|
console.log(`\nGenerated ${files.length} components → ${outputPath}`);
|
||||||
+126
@@ -0,0 +1,126 @@
|
|||||||
|
/**
|
||||||
|
* validate-schemas.js
|
||||||
|
* 构建后校验所有 MCP 工具的 inputSchema,检测常见 JSON Schema 错误:
|
||||||
|
* - array 类型缺少 items
|
||||||
|
* - object 类型缺少 properties
|
||||||
|
* - enum 为空数组
|
||||||
|
* - type 字段缺失
|
||||||
|
*/
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
// 颜色
|
||||||
|
const RED = '\x1b[31m';
|
||||||
|
const YELLOW = '\x1b[33m';
|
||||||
|
const GREEN = '\x1b[32m';
|
||||||
|
const RESET = '\x1b[0m';
|
||||||
|
|
||||||
|
let errors = 0;
|
||||||
|
let warnings = 0;
|
||||||
|
|
||||||
|
function validateSchema(schema, context, toolName) {
|
||||||
|
if (!schema || typeof schema !== 'object') return;
|
||||||
|
|
||||||
|
// 检查 array 缺少 items
|
||||||
|
if (schema.type === 'array' && !schema.items) {
|
||||||
|
console.error(`${RED}ERROR${RESET} [${toolName}] ${context}: type "array" missing "items"`);
|
||||||
|
errors++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 items 但不是 array
|
||||||
|
if (schema.items && schema.type !== 'array') {
|
||||||
|
console.warn(`${YELLOW}WARN${RESET} [${toolName}] ${context}: has "items" but type is "${schema.type}", not "array"`);
|
||||||
|
warnings++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 enum 为空
|
||||||
|
if (schema.enum && Array.isArray(schema.enum) && schema.enum.length === 0) {
|
||||||
|
console.warn(`${YELLOW}WARN${RESET} [${toolName}] ${context}: empty "enum" array`);
|
||||||
|
warnings++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 递归检查 properties
|
||||||
|
if (schema.properties && typeof schema.properties === 'object') {
|
||||||
|
for (const [key, value] of Object.entries(schema.properties)) {
|
||||||
|
validateSchema(value, `${context}.${key}`, toolName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 递归检查 items
|
||||||
|
if (schema.items && typeof schema.items === 'object') {
|
||||||
|
validateSchema(schema.items, `${context}.items`, toolName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
const distDir = path.join(__dirname, '..', 'dist');
|
||||||
|
if (!fs.existsSync(distDir)) {
|
||||||
|
console.error(`${RED}ERROR${RESET} dist/ not found. Run "npm run build" first.`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 收集所有工具定义
|
||||||
|
const tools = [];
|
||||||
|
|
||||||
|
// 1. 加载 CocosTools
|
||||||
|
try {
|
||||||
|
const { CocosTools } = require(path.join(distDir, 'tools', 'cocos', 'cocos-tools.js'));
|
||||||
|
const cocosTools = new CocosTools();
|
||||||
|
tools.push(...cocosTools.getTools());
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`${YELLOW}WARN${RESET} Could not load CocosTools: ${e.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 加载独立工具集
|
||||||
|
const toolFiles = [
|
||||||
|
'tools/scene-tools.js',
|
||||||
|
'tools/node-tools.js',
|
||||||
|
'tools/component-tools.js',
|
||||||
|
'tools/prefab-tools.js',
|
||||||
|
'tools/project-tools.js',
|
||||||
|
'tools/scene-view-tools.js',
|
||||||
|
'tools/reference-image-tools.js',
|
||||||
|
'tools/preferences-tools.js',
|
||||||
|
'tools/asset-advanced-tools.js',
|
||||||
|
'tools/debug-tools.js',
|
||||||
|
'tools/server-tools.js',
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const file of toolFiles) {
|
||||||
|
try {
|
||||||
|
const fullPath = path.join(distDir, file);
|
||||||
|
if (!fs.existsSync(fullPath)) continue;
|
||||||
|
const mod = require(fullPath);
|
||||||
|
// 找到导出的类并实例化
|
||||||
|
for (const [, ExportedClass] of Object.entries(mod)) {
|
||||||
|
if (typeof ExportedClass === 'function' && ExportedClass.prototype && typeof ExportedClass.prototype.getTools === 'function') {
|
||||||
|
const instance = new ExportedClass();
|
||||||
|
tools.push(...instance.getTools());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`${YELLOW}WARN${RESET} Could not load ${file}: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\nValidating ${tools.length} tool schemas...\n`);
|
||||||
|
|
||||||
|
for (const tool of tools) {
|
||||||
|
if (tool.inputSchema) {
|
||||||
|
validateSchema(tool.inputSchema, 'inputSchema', tool.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
if (errors > 0) {
|
||||||
|
console.error(`${RED}FAILED${RESET}: ${errors} error(s), ${warnings} warning(s)`);
|
||||||
|
process.exit(1);
|
||||||
|
} else if (warnings > 0) {
|
||||||
|
console.log(`${YELLOW}PASSED${RESET} with ${warnings} warning(s)`);
|
||||||
|
} else {
|
||||||
|
console.log(`${GREEN}PASSED${RESET}: All schemas valid`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
1.7.9
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"port": 3000,
|
||||||
|
"autoStart": false,
|
||||||
|
"enableDebugLog": false,
|
||||||
|
"allowedOrigins": [
|
||||||
|
"*"
|
||||||
|
],
|
||||||
|
"maxConnections": 10
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
{
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"id": "1a189805-4f21-4147-91ab-461fe9ff5f28",
|
||||||
|
"name": "默认配置",
|
||||||
|
"description": "自动创建的默认工具配置",
|
||||||
|
"tools": [
|
||||||
|
{
|
||||||
|
"category": "cocos",
|
||||||
|
"name": "scene",
|
||||||
|
"enabled": true,
|
||||||
|
"description": "Scene management — open/save/create scenes, get hierarchy, undo, detect mode, list_components, validate_scene.\nCommon: hierarchy, get_info, open, save, create, validate_scene, list_components.\nNote: validate_scene checks broken refs IN this tool (NOT cocos_validate). list_components lists all available component types.\nExample: {action:\"hierarchy\"} or {action:\"validate_scene\"}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "cocos",
|
||||||
|
"name": "node",
|
||||||
|
"enabled": true,
|
||||||
|
"description": "⚡ BATCH: When modifying 3+ nodes (position/size/anchor), use \"batch_modify\" instead of calling \"modify\" repeatedly — it's faster and atomic.\nNode CRUD — create/find/modify/delete nodes, list all nodes, set transform, mount/remove scripts, reorder siblings.\nActions: find, info, list, tree, create, delete, modify, move, reorder, duplicate, copy, paste, cut, mount_script, remove_script, reset, detect_type, batch_modify.\nNote: \"list\" returns all nodes flat list, \"tree\" returns subtree of a node. Use \"info\" (NOT get_info), \"reset\" (NOT reset_transform). \"remove_script\" for scripts (NOT component.remove). \"reorder\" to change sibling order within same parent. \"batch_modify\" to modify multiple nodes in one call.\nExample: {action:\"list\"} or {action:\"tree\", node:\"Canvas\"} or {action:\"batch_modify\", nodes:[{node:\"Player\", position:{x:0,y:0,z:0}}]}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "cocos",
|
||||||
|
"name": "component",
|
||||||
|
"enabled": true,
|
||||||
|
"description": "⚡ BATCH: When binding 3+ click events, use \"batch_click_event\" instead of calling \"click_event\" repeatedly.\nComponent management — add/remove/configure components on nodes.\nActions: add, remove, list, info, set_property, available_types, click_event, batch_click_event.\nNote: use \"info\" to view component properties (NOT get_properties). To remove scripts, use cocos_node(action:\"remove_script\"). \"batch_click_event\" binds click events on multiple buttons in one call.\nExample: {action:\"info\", node:\"Canvas/Btn\", componentType:\"cc.Button\"}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "cocos",
|
||||||
|
"name": "prefab",
|
||||||
|
"enabled": true,
|
||||||
|
"description": "Prefab management — list/info/delete/create/instantiate/edit prefabs, apply/revert changes.\nAll prefab operations (including delete) belong here, NOT cocos_asset.\nCommon: instantiate, create, delete, apply, edit_enter, edit_save, edit_exit.\nExample: {action:\"instantiate\", prefabPath:\"db://assets/prefabs/Enemy.prefab\", parent:\"Canvas\"}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "cocos",
|
||||||
|
"name": "asset",
|
||||||
|
"enabled": true,
|
||||||
|
"description": "Asset operations — query/search/create/delete assets, path-UUID conversion.\nCommon: query_uuid, find_by_name, search, create, delete.\nExample: {action:\"query_uuid\", url:\"db://assets/textures/bg.png\"}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "cocos",
|
||||||
|
"name": "editor",
|
||||||
|
"enabled": true,
|
||||||
|
"description": "Editor utilities — project info, console logs, preferences, build, preview, run/stop, reload.\nGroups: project_*, console_*, log_*, pref_*, build*, server_*, run, stop, reload.\n\"run\" launches preview (platform: browser/simulator/editor). \"stop\" stops editor preview. \"reload\" reloads editor window (Developer → 重新加载).\nExample: {action:\"run\", platform:\"editor\"} then {action:\"stop\"}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "cocos",
|
||||||
|
"name": "view",
|
||||||
|
"enabled": true,
|
||||||
|
"description": "Viewport control — gizmo tools, 2D/3D mode, camera focus/align, reference images.\nCommon: gizmo_tool, camera_focus, camera_align_view (sync camera→view), camera_align_node (sync view→node).\nExample: {action:\"camera_focus\", nodes:[\"Player\"]} or {action:\"camera_align_node\", nodes:[\"Camera\"]}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "cocos",
|
||||||
|
"name": "composite",
|
||||||
|
"enabled": true,
|
||||||
|
"description": "One-call UI creation — create complete Button/Label/Image, mount script with bindings.\nActions: create_button, create_label, create_image, mount_and_bind, setup_widget, batch, batch_create_button, batch_create_label, batch_create_image.\nNote: mount_and_bind mounts script AND binds properties in one call (NOT node.mount_script which only mounts). create_button supports optional clickEvent to bind click handler in one call.\nBatch hint: 3+ buttons → batch_create_button, 3+ labels → batch_create_label, 3+ images → batch_create_image.\nExample: {action:\"mount_and_bind\", node:\"Hero\", scriptPath:\"db://assets/scripts/Game.ts\", bindings:{...}}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "cocos",
|
||||||
|
"name": "knowledge",
|
||||||
|
"enabled": true,
|
||||||
|
"description": "Cocos Creator reference — component properties, UI rules, layout patterns, animation recipes, best practices, tool guides.\nUse topic:\"tool_guide\" with query:\"tool.action\" before calling unfamiliar tools.\nExample: {topic:\"component_properties\", query:\"cc.Label\"}, {topic:\"animation_patterns\", query:\"scale_bounce\"}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "cocos",
|
||||||
|
"name": "validate",
|
||||||
|
"enabled": true,
|
||||||
|
"description": "Advanced validation rules — layout overlap/offscreen, reference consistency, hierarchy depth.\nNote: Use this for deep inspection (NOT scene.validate_scene which only checks broken refs).\nActions: layout (overlap/offscreen), references (broken asset refs), hierarchy (depth/naming).\nExample: {action:\"layout\"} or {action:\"references\"}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "cocos",
|
||||||
|
"name": "template",
|
||||||
|
"enabled": true,
|
||||||
|
"description": "UI templates — apply pre-built structures (dialog, scroll_list, nav_bar, settings_page).\nNote: Use this (NOT composite/builder) when user wants a standard UI pattern. Use list to show available templates.\nActions: list, apply.\nExample: {action:\"list\"} or {action:\"apply\", template:\"dialog\", parent:\"Canvas\"}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "cocos",
|
||||||
|
"name": "capture",
|
||||||
|
"enabled": true,
|
||||||
|
"description": "Capture — scene layout JSON + real screenshot.\nActions: scene_snapshot (full scene JSON), node_snapshot (focused subtree JSON), screenshot (PNG).\nscreenshot has TWO modes:\n • mode:\"content\" (RECOMMENDED for pixel diffs) — returns a REAL engine render of ONLY the Canvas content at design resolution with a transparent (or chosen) background: NO editor panels/grid/gizmos/FPS overlay. Runs the built web runtime headlessly internally (one MCP call → content PNG). Requires scenePath. Renders the LAST BUILT (saved) scene; reuses build/web-mobile (auto-builds if missing; rebuild:true forces a fresh build, ~60s). Deterministic. Returns pngBase64.\n • mode:\"window\" (default, legacy) — PNG of the Creator editor window via OS capture; includes panels/grid. Returns contentRect (the scene-viewport box, when detectable) so you can crop.\nUse scene/node_snapshot for position/size/component data; use screenshot for a real visual.\nExamples: {action:\"screenshot\", mode:\"content\", scenePath:\"db://assets/scenes/Main.scene\"} or {action:\"screenshot\", mode:\"content\", scenePath:\"Sprite\", width:1280, height:720, background:{r:0,g:0,b:0,a:0}, rebuild:true} or legacy {action:\"screenshot\"}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "cocos",
|
||||||
|
"name": "builder",
|
||||||
|
"enabled": true,
|
||||||
|
"description": "Build node tree from JSON — create complete hierarchies with components in one call.\nNote: Use this (NOT composite/node) when building complex multi-level UI structures. Query tool_guide for JSON format.\nPrefab shortcut: use \"type\":\"Button\" (or Label, Sprite, ScrollView, etc.) instead of components[] to create complete UI controls from engine prefab templates.\nExample: {action:\"build\", parent:\"Canvas\", tree:{name:\"Panel\", components:[\"cc.Sprite\"], children:[{name:\"MyBtn\", type:\"Button\"}]}}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "cocos",
|
||||||
|
"name": "animation",
|
||||||
|
"enabled": true,
|
||||||
|
"description": "Animation editing — clips, keyframes, tracks, events, presets, batch operations.\nRecommended: batch_file (no edit mode needed), fallback to batch. Use preset for common animations.\nExample: {action:\"preset\", clipUuid:\"...\", preset:\"sprite_fade_in\"} or {action:\"add_event\", clipUuid:\"...\", frame:10, eventData:{func:\"onHit\"}}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "cocos",
|
||||||
|
"name": "spine",
|
||||||
|
"enabled": true,
|
||||||
|
"description": "Spine (sp.Skeleton) — query animations/skins, set animation/skin, change properties, swap skeleton data, manage sockets.\nActions: info, list_animations, list_skins, set_animation, set_skin, set_property, set_data, add_socket, remove_socket.\nNote: \"node\" is the node path or UUID with sp.Skeleton component.\nExample: {action:\"list_animations\", node:\"spinNode\"} or {action:\"set_animation\", node:\"spinNode\", animation:\"idle\"}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "cocos",
|
||||||
|
"name": "label",
|
||||||
|
"enabled": true,
|
||||||
|
"description": "Label & Text — manage cc.Label, cc.RichText, cc.EditBox. Query/set text, font, style, outline, shadow. Batch operations.\nActions: info, list, set_text, set_font, set_style, set_outline, set_shadow, batch_set_font, batch_set_style.\nSupports: cc.Label (full), cc.RichText (text/font/fontSize/color), cc.EditBox (text/placeholder).\nExample: {action:\"set_text\", node:\"Title\", text:\"Hello\"}\n{action:\"set_style\", node:\"Title\", fontSize:32, isBold:true, color:{r:255,g:0,b:0,a:255}}\n{action:\"batch_set_font\", font:\"db://assets/fonts/myfont.ttf\"}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"createdAt": "2026-08-18T23:28:53.590Z",
|
||||||
|
"updatedAt": "2026-08-18T23:28:53.590Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"currentConfigId": "1a189805-4f21-4147-91ab-461fe9ff5f28",
|
||||||
|
"maxConfigSlots": 5
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user