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

38 KiB
Raw Permalink Blame History

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
  • 比较两个 prefabdiff 子命令)
  • 操作 .anim 文件(anim 子命令,与 prefab 同格式)

不适用 / 已知限制

  • 多层嵌套 stubstub 内还有 stub):CLI 支持 refSubNode 字符串数组路径,但更深层场景仍建议走 tools pipelinetools/step-3-script/bind-prefab-components.ts
  • .anim 文件里的 AnimationClip / Track / Curve 结构:用 cli/src/anim-primitives.js 在脚本中处理,不通过 op
  • 脚本组件本身在 stub 内挂载(mountedComponents)的 @property 绑定:CLI 会抛错

2. 入口与路径约定

# 必须用 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。可选全局链接:

cd extensions/cc-3-8-x-mcp/cli
npm link
cocos-mcp-cli <command>

3. 标准工作流

# 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-compstree / 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(单字段快捷写入)

支持的 fieldactive / 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 确认匹配范围,再去掉落盘
# 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 在脚本中处理。

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 同格式:

node bin/cocos-mcp-cli.js diff old.prefab new.prefab

适用:CI 验证转换工具产物 / 对照历史版本 / review 自动 diff。

ops schema 预校验

editPrefab 跑前一次性扫所有 op,发现拼错(compcomponentType 友好提示)/ 缺必填字段 / 未知 op 类型,一次性报齐,不会跑到一半才发现。

create-prefab

从零创建一个新 prefab + 配套 .prefab.metaoutput-path 不带 .prefab 后缀会自动补全。

uuid / fileIddeterministicUUID / deterministicFileIdcreate-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-spine5 条目):

0  cc.Prefab
1  cc.Node (root)
2  cc.UITransform
3  cc.CompPrefabInfo (UITransform)
4  cc.PrefabInfo     (root)

--add-spine7 条目):

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 自身不扫目录):

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(不走本命令):

{"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

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__ 引用。

为啥需要这个 opCocos 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

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 都通用:

"itemList"                                    // 名字(首个匹配)
{ "id": 65 }                                  // __id__(stub 节点必须用这个,见下方踩坑)
{ "path": "Canvas/Main/itemList" }            // DOM-like 路径,从根逐级下钻;同名节点多时用这个

踩坑:stub 节点不能用字符串名定位

stub 节点(嵌套 prefab 实例)在 prefab JSON 里 _name = ""(空字符串),真实显示名挂在 PrefabInstance.propertyOverrides 里。按字符串名查找会失败

# ❌ 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? 改组件 _enabledstub 走 propertyOverrides
set-nested-component-field nodestub, componentType, property, value, subNode? stub 节点改内部任意组件的任意字段。property 支持字符串或嵌套路径数组。SpriteFrame 等资源 value 自备 {__uuid__,__expectedType__}
reset-overrides nodestub, property?, componentType?, subNode?, all? 清 stub 已写入的 propertyOverrides。all: true 清空整个数组(一键回滚到嵌套默认);property 单条匹配——无 componentType = 节点字段(_lpos / _name 等),有 componentType = 嵌套内组件字段。幂等
ensure-meta path .ts / .json 文件创建 .meta(v4 uuid + 按扩展名选模板)。path 可绝对或相对项目根。已存在时幂等。典型用法:新建脚本 → ensure-metaadd-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? _stringstub 节点走 propertyOverrideslabelNode 指定嵌套 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)。

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 到普通节点上的组件

{
  "op": "set-component-ref",
  "node": "SettingUI",
  "componentType": "SettingUI",
  "property": "_btnClose",
  "refNode": "btnClose",
  "refType": "cc.Button"
}

7.2 绑定 @property 到嵌套 prefabstub)内部的组件

{
  "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.targetOverridescc.TargetOverrideInfo

7.3 绑定 @property 到 stub 根节点本身(cc.Node 类型)

{
  "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 挂载

{
  "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 内部某组件的字段

{
  "op": "set-nested-component-field",
  "node": {"id": 33},
  "componentType": "cc.Label",
  "property": "_string",
  "value": "新文字"
}

7.6 改普通节点 cc.UITransform 锚点 + 保持视觉位置不变

{
  "op": "set-anchor",
  "node": "itemList",
  "y": 1,
  "compensatePosition": true
}

内部按 lpos.y += height * (newAnchorY - oldAnchorY) 自动调整位置。

7.7 改普通节点的组件字段(嵌套路径)

{
  "op": "set-component-field",
  "node": "label",
  "componentType": "cc.Label",
  "property": ["_color", "r"],
  "value": 255
}

property 接字符串(顶层)或字符串数组(嵌套路径,逐级下钻)。中间路径不是对象会报错,不会自动建中间结构。

7.8 bulk-set:批量改一类节点

{
  "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

{"op": "reorder-children", "node": "list", "order": ["item3", "item1", "item2"]}

order 必须包含全部子节点(数量校验),元素是 string_name)或 {id:N}

7.10 path 选择器(同名节点多 / id 不稳)

{"op": "set-active", "node": {"path": "Canvas/Main/itemList"}, "active": false}

从根节点逐级按 _name 匹配 _children,遇 stub 不下钻。根节点名匹配第一段时可省略。同名段会取首个匹配。

7.11 dry-run 预览

node bin/cocos-mcp-cli.js batch <prefab> ops.json --dry-run

输出:

{
  "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 单字段值(脚本管道)

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 树带组件字段

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 节点的 _namenull(不是字符串)。名字来自 cc.PrefabInstance.propertyOverrides,运行时才填。

做法:先 query --selector treeid,然后用 {"id": N} 定位。

坑 2:入口文件错

  • bin/cocos-mcp-cli.js
  • src/index.js ✗(只是 re-export,无 CLI 入口)
  • src/cli/main.js ✗(导出 main() 但不自调用)

坑 3localID 链多层嵌套

CLI 的 resolveLocalIdChainsrc/editor/nested.js)支持 refSubNode 字符串数组路径走多层。但每层都假定能按节点名定位 stub;不支持靠组件类型在中间层定位。极端深嵌套场景仍建议走 tools pipelinestep-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__ 字段不存在。

坑 5sourceInfo 为 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"。

坑 11:schema 类型校验(已加)

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-refcomponentType / 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,产物形如:

{ "__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 已写入

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

jq '[.[] | select(.__type__ == "cc.TargetOverrideInfo")] | length' \
  assets/packages/common/setting/ui/SettingUI.prefab

10. 协议参考

多层嵌套 localID 链的 tools pipeline 实现:tools/step-3-script/bind-prefab-components.tsresolveLocalIdChain


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.jsOP_HANDLERS 加一行 + import
  3. src/editor/op-schema.jsSCHEMAS 登记必填 / 可选字段
  4. 本文档 §6(op 全表)+ §6.8(按场景速查)补一行
  5. test/api.test.js 加测试用例

测试

# 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