Files
furao 14c5b00f14 Initial public release: cc-3-8-x-mcp
Cocos Creator 3.8.x MCP bridge extension with a built-in offline CLI.

Components:
- Editor extension: in-process MCP server exposing scene / asset-db /
  preview / local / editor-process-control tools
- stdio router: aggregates multiple editor instances on one machine,
  with shortName dedup
- offline CLI (cocos-mcp-cli): headless prefab read/write + a wrapper
  around the Cocos CLI build

Pure Node.js, zero third-party dependencies. Licensed under Apache-2.0.
2026-06-06 11:33:19 +08:00

796 lines
38 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# cocos-mcp-cli — CC3 Prefab 离线读写工具
> 单一真相文档。覆盖:定位、命令、op 全表、配方、已知坑、源码导航。
>
> 给 AI agent 也给人类开发者。改 `.prefab` / `.anim` 文件前必读。
---
## 1. 定位与硬规则
**`.prefab` 文件必须用本 CLI 操作,禁止 `Read + Edit` 工具直接编辑。**
prefab 是 JSON 数组,`__id__` 是数组下标,字符串替换会破坏所有引用关系。唯一例外:纯文本字面量替换(如改一个类名字符串)。
**适用场景**
- 修改节点字段(`_active` / `_lpos` / `_name` / 子节点顺序)
- 修改组件字段(`cc.Label._string` / `cc.Sprite._spriteFrame` / `cc.UITransform` 锚点尺寸 / 自定义脚本字段)
- 给脚本组件 `@property` 挂节点 / 组件引用(含嵌套 prefab 内)
- 增删克隆节点 / 加组件 / 合并重复组件
- 跨多个 prefab 跑同一组 ops`--glob`
- 比较两个 prefab`diff` 子命令)
- 操作 `.anim` 文件(`anim` 子命令,与 prefab 同格式)
**不适用 / 已知限制**
- 多层嵌套 stubstub 内还有 stub):CLI 支持 `refSubNode` 字符串数组路径,但更深层场景仍建议走 tools pipeline`tools/step-3-script/bind-prefab-components.ts`
-`.anim` 文件里的 AnimationClip / Track / Curve 结构:用 `cli/src/anim-primitives.js` 在脚本中处理,不通过 op
- 脚本组件本身在 stub 内挂载(`mountedComponents`)的 @property 绑定:CLI 会抛错
---
## 2. 入口与路径约定
```bash
# 必须用 bin/cocos-mcp-cli.jssrc/index.js 是 re-export 无 CLI 入口;src/cli/main.js 不自调用)
cd <你的 Cocos 项目根目录>
node extensions/cc-3-8-x-mcp/cli/bin/cocos-mcp-cli.js <command> [args]
```
prefab 路径相对 `forest/` 项目根,不带 `forest/` 前缀:
```
assets/packages/common/setting/ui/SettingUI.prefab
assets/packages/module/sign/prefab/SignUI.prefab
```
零依赖,无需 `npm install`。可选全局链接:
```bash
cd extensions/cc-3-8-x-mcp/cli
npm link
cocos-mcp-cli <command>
```
---
## 3. 标准工作流
```bash
# 1. 查节点树,确认目标 ID 和 isStub 信息
node bin/cocos-mcp-cli.js query <prefab> --selector tree
# 2. 写 ops.json(见 §6 Op 全表)
# 3. 干跑预览改动(不写盘)
node bin/cocos-mcp-cli.js batch <prefab> ops.json --dry-run
# 4. 落盘
node bin/cocos-mcp-cli.js batch <prefab> ops.json
# 5. 类型检查
npx tsc --noEmit
```
成功输出 `{"changed": true, "opsApplied": N, "nodesAffected": [...]}`。任一 op 失败整体不落盘(原子性)。
---
## 4. 命令完整参考
```
cocos-mcp-cli query <prefab> [--selector tree|node|find|field]
[--name X] [--type cc.Label]
[--comp cc.UITransform] [--field _anchorPoint]
[--with-comps]
cocos-mcp-cli set <prefab> <nodeName> <field> <value>
cocos-mcp-cli batch <prefab> <ops.json> [--project-root <path>] [--dry-run]
cocos-mcp-cli batch <ops.json> --glob <pattern> [--project-root <path>] [--dry-run]
cocos-mcp-cli anim <subcommand> <file> [args] # subcommand: query | batch
cocos-mcp-cli diff <prefabA> <prefabB> # 字段级 diff,输出与 dry-run 同格式
cocos-mcp-cli create-prefab <out> [--name N] [--width W] [--height H]
[--add-spine <skel-uuid>] [--dry-run]
cocos-mcp-cli extract-prefab <src> <out> --node <selector> [--name X] [--dry-run]
cocos-mcp-cli compact-prefab <prefab> [--dry-run]
```
### query
| selector | 说明 | 必带 flag |
|---|---|---|
| `tree`(默认) | 精简节点树(`id` / `name` / `isStub` / `componentTypes` / `children` | — |
| `node` | 单节点详情(含 raw + overrides | `--name <节点名>` |
| `find` | 按 `__type__` 列所有匹配 element 的 id | `--type <类型>` |
| `field` | 单组件单字段值(输出原始 JSON,方便 `jq` 管道) | `--name --comp --field` |
| `overrides` | 列 stub 节点当前所有 propertyOverrides + 关联 root targetOverrides | `--id N` / `--path A/B/C` / `--name <name>` |
`--with-comps``tree` / `node` 下展开节点的所有组件字段(输出 `components: [{type, id, fields}]`)。不带这个 flag 只输出 `componentTypes` 类型名列表。
`overrides` 输出每条 override 的落点 `target.kind`
- `stub-node-field` — stub 节点自身字段(_lpos / _name / _lscale 等)
- `nested-component` — 嵌套 prefab 内某组件字段(带 `componentType` + `ownerNodeName`
- `nested-node` — 嵌套 prefab 内某子节点字段
- `unknown` — 嵌套 prefab 加载失败或 fileId 不在嵌套索引内
调 stub 字段对不上时先跑 `overrides` 看当前已写入哪些,再用 `reset-overrides` op 清单条或一键回滚。
### set(单字段快捷写入)
支持的 `field``active` / `label.text` / `position.x` / `position.y` / `position.z`
复杂操作请用 `batch`
### batch
- `--project-root <path>`:当 `<prefab>` 放在项目目录外(如 `/tmp/`)时必须显式传入含 `assets/ + package.json` 的项目根。否则 className → 压缩 classId 查表失败时会抛错(避免写入 className 字符串导致 cocos MissingScript)。同样,刚新建 .ts 但 cocos 编辑器尚未生成 .ts.meta 时也会抛错——等 .meta 出来再跑。
- `--dry-run`:跑完不写盘,输出 `{ changed: false, dryRun: true, diff: [...] }``diff` 是字段级差异 `{ "a.b.c": [old, new] }`
- `--glob <pattern>`:第一个位置参数当 ops.json,对所有匹配 pattern 的 prefab 跑同一组 ops。pattern 支持 `**` / `*` / `?`,相对 cwd。每个文件独立执行,单文件失败不阻断后续。**先用 `--dry-run` 确认匹配范围**,再去掉落盘
```bash
# glob 示例
node bin/cocos-mcp-cli.js batch /tmp/ops.json \
--glob "assets/packages/module/**/ui/*.prefab" --dry-run
```
### anim
`.anim``.prefab` 同为 JSON 数组 + `__id__` 引用格式,复用 `editPrefab`。op 主要面向 cc.Node 树。改 AnimationClip / Track / Curve 结构请用 `cli/src/anim-primitives.js` 在脚本中处理。
```bash
node bin/cocos-mcp-cli.js anim query my.anim --selector tree
node bin/cocos-mcp-cli.js anim batch my.anim ops.json --dry-run
```
### diff
字段级 diff,输出与 dry-run 同格式:
```bash
node bin/cocos-mcp-cli.js diff old.prefab new.prefab
```
适用:CI 验证转换工具产物 / 对照历史版本 / review 自动 diff。
### ops schema 预校验
`editPrefab` 跑前一次性扫所有 op,发现拼错(`comp``componentType` 友好提示)/ 缺必填字段 / 未知 op 类型,一次性报齐,不会跑到一半才发现。
### create-prefab
从零创建一个新 prefab + 配套 `.prefab.meta``output-path` 不带 `.prefab` 后缀会自动补全。
`uuid` / `fileId``deterministicUUID` / `deterministicFileId``create-prefab:<name>` 为种子推导:**同名 prefab 每次生成相同 UUID**,可重入。
| flag | 默认 | 说明 |
|---|---|---|
| `--name <N>` | 取 basename 去 `.prefab` | prefab 内部 `_name` + `meta.userData.syncNodeName` |
| `--width <W>` | 普通 750 / spine 100 | UITransform 宽 |
| `--height <H>` | 普通 200 / spine 100 | UITransform 高 |
| `--add-spine <skel-uuid>` | — | 在 root 节点多挂 `sp.Skeleton` 组件,`_skeletonData.__uuid__` 指向给定的 `.skel` 资产 UUID;条目数 5 → 7 |
| `--dry-run` | — | 不写盘,把 prefab + meta JSON 输出到 stdout |
不带 `--add-spine`5 条目):
```
0 cc.Prefab
1 cc.Node (root)
2 cc.UITransform
3 cc.CompPrefabInfo (UITransform)
4 cc.PrefabInfo (root)
```
`--add-spine`7 条目):
```
0 cc.Prefab
1 cc.Node (root, _components: [2, 4])
2 cc.UITransform
3 cc.CompPrefabInfo (UITransform)
4 sp.Skeleton
5 cc.CompPrefabInfo (sp.Skeleton)
6 cc.PrefabInfo (root)
```
**批量生成 spine prefab**(外层 shell 循环,CLI 自身不扫目录):
```bash
for meta in assets/res/<group>/<xxxN>/*.skel.meta; do
name=$(basename "$meta" .skel.meta)
uuid=$(python3 -c "import json,sys; print(json.load(open(sys.argv[1]))['uuid'])" "$meta")
node extensions/cc-3-8-x-mcp/cli/bin/cocos-mcp-cli.js create-prefab \
"assets/packages/<group>/<xxxN>/prefab/${name}.prefab" \
--add-spine "$uuid"
done
```
已存在 prefab 加 spine 组件用 `add-component`(不走本命令):
```json
{"op": "add-component", "node": "root", "componentType": "sp.Skeleton",
"props": {"_skeletonData": {"__uuid__": "<skel-uuid>", "__expectedType__": "sp.SkeletonData"}}}
```
### extract-prefab
从源 prefab 里抠出一个子节点的完整闭包,写成新独立 prefab。
| flag | 说明 |
|---|---|
| `<src>` | 源 prefab 路径 |
| `<out>` | 输出新 prefab 路径 |
| `--node <selector>` | 节点选择器(同 batch 三种:节点名 / `{"id":N}` / `{"path":"A/B"}` |
| `--name <X>` | 新 prefab 根节点 `_name`(可选,默认沿用源节点名) |
| `--dry-run` | 不写盘,把新 prefab + meta JSON 输出到 stdout |
闭包收集规则(`cli/src/cli/extract-cmd.js`):
- 从 srcNode 开始递归走所有字段,遇 `{__id__: N}` 把 N 加入闭包队列
- 跳过 `_parent`(反向引用会把父链/兄弟拖进闭包,破坏「只提子树」语义)
- 闭包内元素按原 idx 升序拷贝到新数组(srcNode 永远是 idx 1
输出语义:
- 新 root: `_parent = null``_name = newName`
- 新 root 的 PrefabInfo: `root → {__id__: 1}`, `asset → {__id__: 0}`, 清掉 `instance / targetOverrides / nestedPrefabInstanceRoots`(这些字段在源里是相对宿主的,独立 prefab 不需要)
- meta uuid: 走 `deterministicUUID(extract-prefab:<outPath>:<newName>:uuid)`**同 src+out+name 每次同 uuid**
例:从 HomeBottom 提取 btnTask 为独立 BottomEntry.prefab
```bash
node extensions/cc-3-8-x-mcp/cli/bin/cocos-mcp-cli.js \
extract-prefab \
assets/packages/.../HomeBottom.prefab \
assets/packages/module/task/prefab/BottomEntry.prefab \
--node "btnTask" \
--name "BottomEntry"
```
### compact-prefab
清 prefab `data` 数组里所有 `null` 槽位 + 重映射所有 `__id__` 引用。
**为啥需要这个 op**Cocos editor 反序列化是「宽容模式」(遇 null 跳过);但 Cocos build worker 是「严格模式」,scan 整个 data 数组撞 null 就崩 `TypeError: Cannot read properties of undefined (reading '__type__')`。早期手工生成的 prefab(比如 extract-prefab 上线前用 Read+Edit 改 elements[i] = null 这种历史操作)会留下 null 槽位;GUI 能打开 + 运行时正常,但 build 跑不通。
算法(`cli/src/cli/compact-cmd.js`):
- 跟 extract-cmd line 105-132 同款(紧凑 push + remap),但不剔除任何东西
- 收集所有 null 索引 → 构造 oldIdx → newIdx 映射(newIdx = oldIdx - 前面被删 null 数量)→ `data.filter(el => el !== null)` → 递归 `_remapIds`
| flag | 说明 |
|---|---|
| `<prefab>` | prefab 路径 |
| `--dry-run` | 不写盘,输出统计 + dangling 引用警告 |
输出格式:
```
<prefab> → <oldLen> → <newLen> (清掉 N 个 null) [dry-run]
原 null 索引: 24,25
⚠ K 个 __id__ 引用原本指向 null 槽位(已置 null):(如有)
[0].xxxRef → __id__:24
```
**dangling 引用警告**:如果 prefab 里某个 `__id__:N` 指向了被删的 null 槽位("软删 + 引用残留"场景),compact 会把它置为 `__id__: null` 并打印警告。一般情况下 null 槽位都是孤儿(没人引用),dangling = 0;如果 > 0 需要人工排查那条引用是否本来就该解开。
例:清两个历史 null prefab
```bash
node extensions/cc-3-8-x-mcp/cli/bin/cocos-mcp-cli.js compact-prefab \
assets/packages/module/task/prefab/TaskRewardItem.prefab --dry-run
# 输出: 29 → 27 (清掉 2 个 null)
# 批量扫整个项目(shell 循环 + dry-run
for f in $(find assets -name "*.prefab"); do
node extensions/cc-3-8-x-mcp/cli/bin/cocos-mcp-cli.js compact-prefab "$f" --dry-run 2>&1 | grep -v "无 null"
done
```
适用范围:只清 data 数组**顶层**的 null。Cocos build 还可能崩在「子节点字段里 null 引用」(如 `node._children[i] = null` 这种深层损坏),那种不是 compact-prefab 能修的,要靠 Cocos GUI 重新保存或 query+set 单点修复。
---
## 5. 节点定位三种形式
`node` / `parent` / `target` / `source` / `refNode` 都通用:
```js
"itemList" // 名字(首个匹配)
{ "id": 65 } // __id__(stub 节点必须用这个,见下方踩坑)
{ "path": "Canvas/Main/itemList" } // DOM-like 路径,从根逐级下钻;同名节点多时用这个
```
### 踩坑:stub 节点不能用字符串名定位
stub 节点(嵌套 prefab 实例)在 prefab JSON 里 `_name = ""`(空字符串),真实显示名挂在 `PrefabInstance.propertyOverrides` 里。**按字符串名查找会失败**
```bash
# ❌ Inspector 里看到的是 "board",但底层 _name=""
{"op": "set-label-text", "node": "board", "text": "游戏说明", "labelNode": "title"}
# → Error: 找不到节点 "board"
# ✅ 用 query --tree 先查 id,然后用 __id__ 定位
{"op": "set-label-text", "node": {"id": 2}, "text": "游戏说明", "labelNode": "title"}
```
适用于所有 op 的所有节点引用字段(`node` / `parent` / `target` 等),不限于 set-label-text。
---
## 6. Op 全表
26 个 op,按场景分组。`node` / `parent` / `source` / `target` 均支持上面三种定位形式。
### 6.1 节点字段
| op | 参数 | 说明 |
|---|---|---|
| `set-active` | `node`, `active: bool` | 切换节点显隐 |
| `rename-node` | `node`, `name` | 改 `_name`。stub 走 propertyOverrides |
| `set-position` | `node`, `x`, `y`, `z?` | 设置本地位置(绝对值) |
| `adjust-position` | `node`, `dx?`, `dy?`, `dz?` | `_lpos` 相对偏移,免去先 query 取原值;任一轴缺省视为 0 |
| `set-node-color` | `node`, `r?`, `g?`, `b?`, `a?` | 改节点 `_color` 分量(0-255),至少提供一个分量。不支持 stub |
| `reorder-children` | `node`, `order` | 调整子节点顺序,影响 UI 渲染层级。`order` 是子节点名或 `{id:N}` 数组,**必须包含全部子节点** |
### 6.2 通用组件字段
| op | 参数 | 说明 |
|---|---|---|
| `set-component-field` | `node`, `componentType`, `property`, `value` | **普通节点**改任意组件任意字段。`property` 接字符串(顶层)或字符串数组(嵌套路径,如 `["_color","r"]`)。改 cc.Vec2/Vec3/Size 时 `value` 必须带 `__type__` |
| `set-component-enabled` | `node`, `componentType`, `enabled`, `subNode?` | 改组件 `_enabled`stub 走 propertyOverrides |
| `set-nested-component-field` | `node`stub, `componentType`, `property`, `value`, `subNode?` | **stub 节点**改内部任意组件的任意字段。`property` 支持字符串或嵌套路径数组。SpriteFrame 等资源 `value` 自备 `{__uuid__,__expectedType__}` |
| `reset-overrides` | `node`stub, `property?`, `componentType?`, `subNode?`, `all?` | 清 stub 已写入的 propertyOverrides。`all: true` 清空整个数组(一键回滚到嵌套默认);`property` 单条匹配——无 componentType = 节点字段(_lpos / _name 等),有 componentType = 嵌套内组件字段。幂等 |
| `ensure-meta` | `path` | 给 `.ts` / `.json` 文件创建 `.meta`(v4 uuid + 按扩展名选模板)。`path` 可绝对或相对项目根。已存在时幂等。**典型用法**:新建脚本 → `ensure-meta``add-component` 同 batch 内联动,免等 cocos 编辑器异步 import。dry-run 时不写盘。同 batch 内自动 invalidate classid-resolver cache,让后续 op 能查到新 className |
### 6.3 cc.UITransform 便捷
| op | 参数 | 说明 |
|---|---|---|
| `set-anchor` | `node`, `x?`, `y?`, `compensatePosition?` | `_anchorPoint` 便捷写法。`compensatePosition: true` 时按 anchor 差值 × size 自动补偿 `_lpos`,保持节点视觉位置不变。**stub 节点支持**:自动走 propertyOverrides 改嵌套 UITransformcompensate 时 `oldA/size` 从嵌套 prefab 默认值读 |
| `set-size` | `node`, `width?`, `height?` | `_contentSize` 便捷写法。**stub 节点支持**:自动走 propertyOverrides 改嵌套 UITransform |
### 6.4 cc.* 引擎组件多字段快捷 op
不含 stub 支持。改 stub 内同字段请用 `set-nested-component-field`
| op | 参数 | 关键 enum |
|---|---|---|
| `set-label` | `node`, `text?`, `fontSize?`, `lineHeight?`, `overflow?`, `horizontalAlign?`, `verticalAlign?`, `bold?`, `italic?`, `underline?`, `enableWrapText?` | `overflow`: 0=NONE 1=CLAMP 2=SHRINK 3=RESIZE_HEIGHT 4=TRUNCATE |
| `set-label-text` | `node`, `text`, `labelNode?` | 改 `_string`stub 节点走 propertyOverrides`labelNode` 指定嵌套 prefab 内持有 Label 的子节点名 |
| `set-richtext` | `node`, `text?`, `maxWidth?`, `fontSize?`, `lineHeight?` | 支持 BBCode 标签 |
| `set-sprite` | `node`, `sizeMode?`, `type?`, `grayscale?`, `trim?` | `type`: 0=SIMPLE 1=SLICED 2=TILED 3=FILLED 4=MESH。**换图用 `set-sprite-frame`** |
| `set-sprite-frame` | `node`, `uuid: string`, `spriteNode?` | 替换 SpriteFrame uuidstub 走 propertyOverrides |
| `set-button` | `node`, `interactable?`, `transition?`, `zoomScale?`, `duration?` | `transition`: 0=NONE 1=COLOR 2=SPRITE 3=SCALE |
| `set-editbox` | `node`, `inputMode?`, `maxLength?`, `placeholder?`, `string?`, `inputFlag?`, `fontSize?` | `inputMode`: 0=ANY 1=EMAIL 2=NUMERIC 3=PHONE 4=URL 5=DECIMAL 6=SINGLE_LINE |
| `set-layout` | `node`, `type?`, `resizeMode?`, `paddingLeft?`, `paddingRight?`, `paddingTop?`, `paddingBottom?`, `spacingX?`, `spacingY?`, `startAxis?`, `constraint?`, `constraintNum?`, `affectedByScale?` | `type`: 0=NONE 1=HORIZONTAL 2=VERTICAL 3=GRID |
### 6.5 节点结构
| op | 参数 | 说明 |
|---|---|---|
| `add-node` | `parent`, `node: {name, lpos?, active?, components?, width?, height?, anchor?}` | 新增 cc.Nodeparent 是 stub 时走 mountedChildren。`components: ["UITransform"]` 自动建 UITransform |
| `remove-node` | `target` | 从父节点移除引用;元素本身保留(orphan),保持其他 `__id__` 稳定 |
| `sync-nested-roots` | (无) | 重建根 `PrefabInfo.nestedPrefabInstanceRoots`,剔除「删了一半」残留的悬空嵌套实例根(节点 `_parent` 已移除但根登记残留 → 残留嵌套 prefab 的 asset 仍被当依赖加载,运行时 404)。只重写该数组,不删 elements、不动其他 `__id__`、不产生 null 槽;被孤立的残留对象成为不可达 orphan。复用 remove-node 内部同名逻辑 |
| `clone-node` | `source`, `parent`, `name` | 深拷贝整棵子树,分配新 `__id__` + 新 fileId |
#### add-node 的 node 字段
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
| `name` | string | 必填 | 节点名 |
| `lpos` | [x,y,z] | [0,0,0] | 本地位置 |
| `active` | bool | true | 是否激活 |
| `components` | string[] | [] | `["UITransform"]` 自动创建 cc.UITransform |
| `width` / `height` | number | 100 | UITransform 尺寸(仅 components 含时生效) |
| `anchor` | [x,y] | [0.5,0.5] | UITransform 锚点 |
### 6.6 组件 / 引用
| op | 参数 | 说明 |
|---|---|---|
| `add-component` | `node`, `componentType`, `props?` | 在 `_components` 新挂一个组件 + 配套 CompPrefabInfo。`componentType` 支持 @ccclass 名(`"GMUI"`)、压缩 classId`"a57b6RRA21B5I70mCpu1pBP"`)、引擎类(`"cc.Button"` |
| `remove-component` | `node`, `componentType` | 从普通节点 `_components` 移除指定组件引用。组件元素与其 CompPrefabInfo 作为 orphan 保留在数组里,保持其他 `__id__` 稳定(与 `remove-node` 同策略)。**不支持 stub**——嵌套 prefab 的组件归子 prefab 拥有,外层只能用 `set-component-enabled` 禁用 |
| `set-component-ref` | `node`, `componentType`, `property`, `refNode`, `refType?`, `refSubNode?` | 给脚本组件 `@property` 挂节点 / 组件引用。详见 §6.7 |
| `dedupe-component` | `node?` | 合并同节点上同语义但重复挂载的组件条目(cli 写 className + 编辑器 reimport 写压缩 classId 形成两份的场景)。按规范化 classId 分组,留字段非空数最多的为 keeper,losers 字段并入后删除并重映射 `__id__``node` 缺省扫整 prefab |
### 6.7 set-component-ref 完整字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `op` | string | ✓ | `"set-component-ref"` |
| `node` | name / `{id}` / `{path}` | ✓ | 挂脚本的节点(持有 @property 字段) |
| `componentType` | string | ✓ | 脚本组件类型,支持 @ccclass 名、压缩 classId、`cc.Button` 等 |
| `property` | string | ✓ | @property 字段名(如 `"_role"` |
| `refNode` | name / `{id}` / `{path}` | ✓ | 要绑定的目标节点。**stub 必须用 `{id:N}`**_name 为 null |
| `refType` | string | 可选 | 目标类型。省略 = 取 refNode 第一个非引擎组件;`"cc.Node"` = 绑节点本身(localID = 嵌套 prefab 根节点 PrefabInfo.fileId |
| `refSubNode` | string \| string[] | 可选 | stub 内部子节点定位。**字符串**指定单层 stub 的子节点名;**字符串数组**走多层嵌套(每层一段名字,最后一段配合 refType 决定终点) |
**常见拼错**`"comp"``componentType``"ref"``refNode`。schema 校验会友好提示。
`refNode` 是 stub 时自动走 `cc.TargetOverrideInfo` 协议(详见 [`nested-prefab-protocol.md`](./nested-prefab-protocol.md))。
### 6.8 按场景速查
| 场景 | op |
|---|---|
| 改节点 _active / _name / _lpos | `set-active` / `rename-node` / `set-position`(普通+stub 通用) |
| _lpos 相对偏移 | `adjust-position` |
| cc.UITransform 锚点 + 自动补偿 lpos | `set-anchor`(带 `compensatePosition: true` |
| cc.UITransform 尺寸 | `set-size` |
| 改普通节点任意组件字段(含嵌套路径) | `set-component-field` |
| 改 stub 内任意组件字段 | `set-nested-component-field` |
| 启用/禁用某组件 | `set-component-enabled` |
| 改子节点渲染顺序 | `reorder-children` |
| 一次改一批节点(按组件类型 / 名前缀 / 正则) | `bulk-set` |
| 改 cc.Label / Sprite / Button / EditBox / Layout / RichText 多字段 | `set-label` / `set-sprite` / `set-button` / `set-editbox` / `set-layout` / `set-richtext` |
| 改节点 _color | `set-node-color` |
| 给脚本 @property 挂引用(节点 / 组件 / 单层 stub / 多层 stub | `set-component-ref`(多层用 `refSubNode: ["A","B"]` |
| 加 / 删 / 复制节点 | `add-node` / `remove-node` / `clone-node` |
| 加 / 删组件 | `add-component` / `remove-component` |
| 合并重复组件 | `dedupe-component` |
| 跨多 prefab 跑同一组 ops | `batch --glob` |
| 比较两个 prefab | `diff` 子命令 |
| 操作 .anim 文件 | `anim query` / `anim batch` |
---
## 7. Op 配方(按场景示例)
### 7.1 绑定 @property 到普通节点上的组件
```json
{
"op": "set-component-ref",
"node": "SettingUI",
"componentType": "SettingUI",
"property": "_btnClose",
"refNode": "btnClose",
"refType": "cc.Button"
}
```
### 7.2 绑定 @property 到嵌套 prefabstub)内部的组件
```json
{
"op": "set-component-ref",
"node": "SettingUI",
"componentType": "SettingUI",
"property": "_someLabel",
"refNode": {"id": 33},
"refType": "cc.Label"
}
```
`refNode` 必须用 `{"id": N}`stub `_name` 为 null)。CLI 自动在 `cc.PrefabInfo.targetOverrides``cc.TargetOverrideInfo`
### 7.3 绑定 @property 到 stub 根节点本身(cc.Node 类型)
```json
{
"op": "set-component-ref",
"node": "SettingUI",
"componentType": "SettingUI",
"property": "_role",
"refNode": {"id": 33},
"refType": "cc.Node"
}
```
`refType: "cc.Node"` 表示绑定节点本身。CLI 找嵌套 prefab 根节点(`_parent === null`)的 `cc.PrefabInfo.fileId`,生成 `localID: [fileId]`
### 7.4 多层嵌套 stub @property 挂载
```json
{
"op": "set-component-ref",
"node": "Main",
"componentType": "MainUI",
"property": "_innerLabel",
"refNode": {"id": 12},
"refType": "cc.Label",
"refSubNode": ["B", "C"]
}
```
主 prefab stub → 第一层嵌套内的 stub `B` → 第二层嵌套内的 `C` 节点上的 `cc.Label`。每跨一层 PrefabInstance 边界 push 一个 fileId 到 localID 链。1 层时仍可写字符串 `"B"`,向后兼容。
### 7.5 改 stub 内部某组件的字段
```json
{
"op": "set-nested-component-field",
"node": {"id": 33},
"componentType": "cc.Label",
"property": "_string",
"value": "新文字"
}
```
### 7.6 改普通节点 cc.UITransform 锚点 + 保持视觉位置不变
```json
{
"op": "set-anchor",
"node": "itemList",
"y": 1,
"compensatePosition": true
}
```
内部按 `lpos.y += height * (newAnchorY - oldAnchorY)` 自动调整位置。
### 7.7 改普通节点的组件字段(嵌套路径)
```json
{
"op": "set-component-field",
"node": "label",
"componentType": "cc.Label",
"property": ["_color", "r"],
"value": 255
}
```
`property` 接字符串(顶层)或字符串数组(嵌套路径,逐级下钻)。中间路径不是对象会报错,不会自动建中间结构。
### 7.8 bulk-set:批量改一类节点
```json
{
"op": "bulk-set",
"selector": { "byComponent": "cc.Label" },
"target": "component:cc.Label",
"property": "_isItalic",
"value": true
}
```
`selector` 可组合(AND):
- `{ byComponent: "cc.X" }`:节点上挂指定组件
- `{ byNamePrefix: "btn" }``_name` 前缀匹配
- `{ byNameRegex: "^icon_\\d+$" }`:正则
`target``"node"`(改节点字段)或 `"component:<Type>"`(改节点上某组件字段)。匹配 0 个不算错。**bulk-set 不处理 stub 节点**(避免代码路径混用)。
### 7.9 reorder-children
```json
{"op": "reorder-children", "node": "list", "order": ["item3", "item1", "item2"]}
```
`order` 必须包含全部子节点(数量校验),元素是 `string`_name)或 `{id:N}`
### 7.10 path 选择器(同名节点多 / id 不稳)
```json
{"op": "set-active", "node": {"path": "Canvas/Main/itemList"}, "active": false}
```
从根节点逐级按 `_name` 匹配 `_children`,遇 stub 不下钻。根节点名匹配第一段时可省略。同名段会取首个匹配。
### 7.11 dry-run 预览
```bash
node bin/cocos-mcp-cli.js batch <prefab> ops.json --dry-run
```
输出:
```json
{
"changed": false,
"opsApplied": 2,
"nodesAffected": ["itemList"],
"dryRun": true,
"diff": [
{ "id": 65, "type": "cc.Node", "name": "itemList",
"changes": { "_lpos.y": [-28, 412] } },
{ "id": 66, "type": "cc.UITransform", "name": "",
"changes": { "_anchorPoint.y": [0.5, 1] } }
]
}
```
`diff` 是字段级,路径用点分(嵌套对象会展平)。
### 7.12 query 单字段值(脚本管道)
```bash
node bin/cocos-mcp-cli.js query <prefab> --selector field \
--name itemList --comp cc.UITransform --field _anchorPoint
# → {"__type__": "cc.Vec2", "x": 0.5, "y": 1}
```
### 7.13 query 树带组件字段
```bash
node bin/cocos-mcp-cli.js query <prefab> --selector tree --with-comps
```
每个节点附 `components: [{type, id, fields}]``fields` 过滤掉系统字段(`__type__` / `node` / `_enabled` / `__prefab` 等)后的业务字段。
---
## 8. 已知坑
### 坑 1stub 节点 `_name` 为 null
prefab JSON 里 stub 节点的 `_name``null`(不是字符串)。名字来自 `cc.PrefabInstance.propertyOverrides`,运行时才填。
**做法**:先 `query --selector tree``id`,然后用 `{"id": N}` 定位。
### 坑 2:入口文件错
- `bin/cocos-mcp-cli.js`
- `src/index.js` ✗(只是 re-export,无 CLI 入口)
- `src/cli/main.js` ✗(导出 `main()` 但不自调用)
### 坑 3localID 链多层嵌套
CLI 的 `resolveLocalIdChain``src/editor/nested.js`)支持 `refSubNode` 字符串数组路径走多层。但每层都假定能按节点名定位 stub;不支持靠组件类型在中间层定位。极端深嵌套场景仍建议走 tools pipeline`step-3-script/bind-prefab-components.ts`)。
### 坑 4root 节点检测依赖 `_parent === null`
`getNestedNodeFileId` 通过 `el._parent === null` 判断嵌套 prefab 的根节点。不是靠 `cc.PrefabInfo.asset.__uuid__`——嵌套 prefab JSON 里所有节点的 PrefabInfo.asset 都是 `{__id__: 0}`in-file 引用),`__uuid__` 字段不存在。
### 坑 5`sourceInfo` 为 null vs cc.TargetInfo
- `source`(挂 @property 的脚本组件)在主 prefab 根节点上 → `sourceInfo: null`
- 脚本组件本身在某个 stub 内(`mountedComponents`)→ `sourceInfo` 要填 `cc.TargetInfo`。CLI 当前不支持,会抛错
### 坑 6set-component-field vs set-nested-component-field 不通用
- **普通节点**改组件字段 → `set-component-field`(直改 elements 数组)
- **stub 节点**改组件字段 → `set-nested-component-field`(写 PrefabInstance.propertyOverrides + 嵌套 prefab fileId
用错时 CLI 抛明确错误:「节点 X 是 stub 代理,请用 set-nested-component-field」。判别 stub 看 `query --selector tree` 输出的 `isStub` 字段。
### 坑 7cc.Vec2/Vec3/Size 写入必须带 `__type__`
`set-component-field` 改这类字段时 `value` 必须形如 `{"__type__": "cc.Vec2", "x": 0.5, "y": 1}`,缺 `__type__` 会被 Cocos 反序列化为普通对象,运行时 `getAnchorPoint()` 等 API 拿不到正确值。
`set-anchor` / `set-size` op 内部已带 `__type__`,无需自己处理。
### 坑 8bulk-set 跳过 stub 节点
`bulk-set` 实现不处理 stub。要批量改 stub 内字段,先 `query --selector find` 拿 stub id 列表,再逐个 `set-nested-component-field`
### 坑 9reorder-children.order 必须含全部子节点
不允许只列要前置的几个、剩下的自动补尾。order 长度 ≠ _children 长度直接抛错。避免「你以为剩下的会按原序,但 CLI 默认丢掉了」这种隐式行为。
### 坑 10:path 选择器同名段必须消歧
`{path: "A/B/C"}` 走每段时如果 `A._children` 下有 ≥2 个同名 B,CLI 直接抛错并列出候选 `__id__`**不静默取首个**。同名场景请用 `{id:N}`,或组合"父用 path、当前层用 id"。
### 坑 11schema 类型校验(已加)
schema 校验既检查"字段拼写 + 必填存在"也检查字段类型。`width: "100"` 这种类型错跑前直接报,不会进 handler 才崩。复杂值(`value` / `props` / `selector` / `refSubNode`)走 `any`,仍由 handler 报场景错。
### 坑 12className → 压缩 classId 自动规范化
Cocos 编辑器反序列化时 `__type__` 可填 @ccclass 名(`"GMUI"`)或压缩 classId`"a57b6RRA21B5I70mCpu1pBP"`),但**保存 prefab 时会 round-trip 为后者**。如果 TS 脚本注册前触发 reimport@property refs 会被丢,导致出现「字符串版 + 压缩版」两份组件。
CLI 两端防护:
1. **写入前**`add-component` / `set-component-ref``componentType` / `refType` 自动扫 `assets/scripts` 反查 `.ts.meta` uuid,转 23 字符压缩 classId 写入。引擎类(`cc.*`/`sp.*`/`dragonBones.*`)和已压缩格式原样透传
2. **写入后兜底**:用 `dedupe-component` op 合并已经被 round-trip 过的 prefab
### 坑 13stub-node-field override 的 localID 用错 fileId
`set-position` / `rename-node` / `adjust-position` / `set-active` 等 op 对 stub 节点(嵌套 prefab 代理)写入字段时,走 `setOverrideProperty`,产物形如:
```json
{ "__type__": "cc.TargetInfo", "localID": ["<某 fileId>"] }
{ "__type__": "CCPropertyOverrideInfo", "targetInfo": { ... }, "propertyPath": ["_lpos"], "value": ... }
```
**正确的 localID 是「嵌套 prefab 内部根节点 PrefabInfo.fileId」**,不是「外层主 prefab 里 stub 自己的 PrefabInfo.fileId」。Cocos 运行时 `generateTargetMap` 按嵌套 prefab 内 fileId 建 targetMap,外层 stub fileId 在 map 里查不到,override 静默失效。
早期 fgui→cc3 转出的 prefab 设计上让这两个 fileId 一致(PrefabBuilder 复用同一 fileId),所以本工具早期版本里用 stubFileId 巧合工作;手编 prefab 或重新设计的标杆 prefab(如 `common-new/button/btnClose.prefab`)两个 fileId 一般不同,必须读嵌套 prefab JSON 拿真实根 fileId。
CLI 行为(2026-05-20 修,`cli/src/overrides.js`):
1. **写入前**`setOverrideProperty` 通过 `prefabInfo.asset.__uuid__` + `resolveUuidToPath` 加载嵌套 prefab,找根节点(`_parent === null`)的 `PrefabInfo.fileId` 作为 localID。**解析失败抛错**(不再 fallback 到 stubFileId),调用方需保证嵌套 prefab 可用。
2. **历史脏数据自动矫正(一次性迁移)**:识别旧版 cli 写入的 `stubFileId` 形式条目,命中同 propertyPath 时把 localID 改写成真值。仓库里现存的 fgui→cc3 转出 prefab 跑一次新 cli 就会逐步收敛到真值,无需手工迁移。
3. **`listOverrides` / `reset-overrides`**:只识别真值 localID(已迁移完的状态)。如果手工保留旧 stubFileId 条目不跑 cli 矫正,list/reset 会忽略它们。
诊断方法:在主 prefab JSON 里 grep `CCPropertyOverrideInfo` 找到目标条目,看 `targetInfo.localID[0]` 是否等于「嵌套 prefab 内根节点的 PrefabInfo.fileId」(在嵌套 prefab JSON 里直接 grep 根节点的 `fileId` 字段,或用 `query --selector overrides --id <stubId>` 看 cli 报告)。
### 坑 14rootTargetOverrides 单字段 override 必须排在数组字段 override 之前
Cocos 加载 prefab 时遍历 `cc.PrefabInfo.targetOverrides` 数组应用 override。**实测 cocos 3.8.x 行为**:若数组前面有数组字段 override(`propertyPath = ["_items", N]`),数组后面的单字段 override(`propertyPath = ["_btnClose"]`)会被静默跳过,运行时 `ui._btnClose === null`
最小可复现:TurnUI 有 14 个 `_items[N]` override,新 `set-component-ref _btnClose` 追加到数组末尾 → cocos 加载后 `ui._btnClose` 为 null。把这条 `_btnClose` 用 Python 移到数组首位 → 立即正常。对照实验同样的文件 mtime + reimport 流程,仅位置差异就触发不同结果,确认是顺序问题不是 reimport 时机。
CLI 防护(2026-05-20 修,`cli/src/editor/nested.js` `addRootTargetOverride`):
1. **新条目按 `propertyPath.length` 分插**
- `length === 1`(单字段,如 `["_btnClose"]`):`splice` 到第一个数组字段 override 之前
- `length > 1`(数组字段,如 `["_items", 0]`):`push` 到末尾
2. 已有的单字段 override 之间相对顺序不变(每次插入都在最后一个单字段之后、第一个数组字段之前)
3. 历史脏数据需手工迁移(Python `pop(idx) + insert(0, ref)` 或者 `splice(arrayBoundary, 0, ref)`
诊断方法:用 `query --selector tree` 拿 prefab 整体结构,然后看 root `cc.PrefabInfo.targetOverrides` 数组里第一个 `propertyPath.length > 1` 条目的位置——之前的所有条目(包括目标单字段)能正常加载,之后的单字段都会被跳过。
---
## 9. 验证 targetOverrides 已写入
```bash
python3 -c "
import json
data = json.load(open('assets/packages/common/setting/ui/SettingUI.prefab'))
for el in data:
if isinstance(el, dict) and el.get('__type__') == 'cc.PrefabInfo' and el.get('rootUuid'):
print('targetOverrides count:', len(el.get('targetOverrides', [])))
break
"
```
或 jq
```bash
jq '[.[] | select(.__type__ == "cc.TargetOverrideInfo")] | length' \
assets/packages/common/setting/ui/SettingUI.prefab
```
---
## 10. 协议参考
- `cc.TargetOverrideInfo` 协议细节:[`nested-prefab-protocol.md`](./nested-prefab-protocol.md)
- prefab JSON 结构速查:[`prefab-schema.md`](./prefab-schema.md)
- offline vs 编辑器路径决策:[`prefab-direct-edit.md`](./prefab-direct-edit.md)
- `.anim` 文件结构:[`anim-schema.md`](./anim-schema.md)
多层嵌套 localID 链的 tools pipeline 实现:`tools/step-3-script/bind-prefab-components.ts``resolveLocalIdChain`
---
## 11. 源码导航
```
extensions/cc-3-8-x-mcp/cli/
├── bin/cocos-mcp-cli.js # CLI 入口(require src/cli/main.js
└── src/
├── index.js # 公开 API re-exportparsePrefab / writePrefab / editPrefab / queryPrefab / ...
├── parse.js / write.js # JSON 数组 + __id__ 引用格式的读写
├── id.js # deterministic fileId / classId 压缩
├── primitives.js # cc.Node / cc.PrefabInfo / cc.UITransform 等节点对象构造原语
├── overrides.js # cc.PrefabInstance.propertyOverrides 读写
├── classid-resolver.js # @ccclass 名 → 压缩 classId(扫 assets/scripts/*.ts.meta
├── uuid-resolver.js # uuid → 磁盘路径(扫 assets/**/*.prefab
├── anim-primitives.js # .anim 文件构造原语
├── editor/ # editPrefab 主入口 + op handler
│ ├── index.js # editPrefab + OP_HANDLERS 注册表
│ ├── helpers.js # resolveNodename/id/path/ findComponent / isStub / normalizeComponentType
│ ├── nested.js # stub / 嵌套 prefab fileId 协议(多层支持)
│ ├── id-utils.js # fileId 分配 / 子树断开 / __id__ 重映射
│ ├── diff.js # 字段级 diffdry-run + diff 子命令共用)
│ ├── op-schema.js # ops 跑前 schema 校验
│ └── ops/ # 26 个 op handler,一文件一个
├── query/ # 只读查询
│ ├── index.js / tree.js / node.js / find.js / field.js
│ └── comp-fields.js
└── cli/ # 命令行子命令分发
├── main.js / flags.js / help.js
├── query-cmd.js / set-cmd.js
├── batch-cmd.js # 含 --glob 实现
├── anim-cmd.js
└── diff-cmd.js
```
### 加新 op 流程
1. `src/editor/ops/<new-op>.js` 写 handler,导出 `execXxx`
2. `src/editor/index.js``OP_HANDLERS` 加一行 + import
3. `src/editor/op-schema.js``SCHEMAS` 登记必填 / 可选字段
4. 本文档 §6(op 全表)+ §6.8(按场景速查)补一行
5. `test/api.test.js` 加测试用例
### 测试
```bash
# 6 个测试文件 116 个用例
for f in extensions/cc-3-8-x-mcp/cli/test/*.test.js; do
node --test "$f" 2>&1 | grep -E "^# (pass|fail)"
done
```