godot-cli-control 0.4.2__tar.gz → 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/PKG-INFO +3 -3
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/addons/godot_cli_control/CHANGELOG.md +10 -1
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/addons/godot_cli_control/README.md +5 -1
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/addons/godot_cli_control/bridge/error_codes.gd +87 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/addons/godot_cli_control/bridge/game_bridge.gd +7 -1
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/addons/godot_cli_control/bridge/low_level_api.gd +86 -10
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/addons/godot_cli_control/plugin.cfg +1 -1
- godot_cli_control-0.5.0/addons/godot_cli_control/tests/gut/test_error_codes.gd +38 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/addons/godot_cli_control/tests/gut/test_game_bridge.gd +17 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/addons/godot_cli_control/tests/gut/test_low_level_api.gd +66 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/python/README.md +2 -2
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/python/godot_cli_control/_version.py +2 -2
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/python/godot_cli_control/bridge.py +22 -3
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/python/godot_cli_control/cli.py +294 -75
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/python/godot_cli_control/client.py +39 -3
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/python/godot_cli_control/init_cmd.py +4 -8
- godot_cli_control-0.5.0/python/godot_cli_control/skills_install.py +98 -0
- godot_cli_control-0.5.0/python/godot_cli_control/templates/skill/SKILL.md +159 -0
- godot_cli_control-0.5.0/python/godot_cli_control/templates/skill/references/commands.md +221 -0
- godot_cli_control-0.5.0/python/godot_cli_control/templates/skill/references/daemon-multi-instance.md +111 -0
- godot_cli_control-0.5.0/python/godot_cli_control/templates/skill/references/error-codes.md +47 -0
- godot_cli_control-0.5.0/python/godot_cli_control/templates/skill/references/pitfalls.md +78 -0
- godot_cli_control-0.5.0/python/godot_cli_control/templates/skill/references/python-and-pytest.md +121 -0
- godot_cli_control-0.5.0/python/godot_cli_control/templates/skill/references/recording.md +48 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/python/tests/test_bridge.py +37 -3
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/python/tests/test_broadcast.py +6 -3
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/python/tests/test_cli.py +234 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/python/tests/test_cli_helpers.py +4 -1
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/python/tests/test_client.py +56 -0
- godot_cli_control-0.5.0/python/tests/test_error_hints.py +148 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/python/tests/test_init.py +4 -1
- godot_cli_control-0.5.0/python/tests/test_skills_install.py +250 -0
- godot_cli_control-0.4.2/python/godot_cli_control/skills_install.py +0 -51
- godot_cli_control-0.4.2/python/godot_cli_control/templates/skill/SKILL.md +0 -664
- godot_cli_control-0.4.2/python/tests/test_skills_install.py +0 -198
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/.gitignore +0 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/LICENSE +0 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/addons/godot_cli_control/LICENSE +0 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/addons/godot_cli_control/bin/run_cli_control.ps1 +0 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/addons/godot_cli_control/bin/run_cli_control.sh +0 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/addons/godot_cli_control/bridge/capture_logger.gd +0 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/addons/godot_cli_control/bridge/diagnostics_api.gd +0 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/addons/godot_cli_control/bridge/game_bridge.gd.uid +0 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/addons/godot_cli_control/bridge/input_simulation_api.gd +0 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/addons/godot_cli_control/bridge/input_simulation_api.gd.uid +0 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/addons/godot_cli_control/bridge/low_level_api.gd.uid +0 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/addons/godot_cli_control/bridge/render_api.gd +0 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/addons/godot_cli_control/bridge/scene_api.gd +0 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/addons/godot_cli_control/bridge/time_api.gd +0 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/addons/godot_cli_control/bridge/variant_codec.gd +0 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/addons/godot_cli_control/bridge/wait_api.gd +0 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/addons/godot_cli_control/plugin.gd +0 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/addons/godot_cli_control/plugin.gd.uid +0 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/addons/godot_cli_control/tests/gut/test_diagnostics_api.gd +0 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/addons/godot_cli_control/tests/gut/test_input_simulation_api.gd +0 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/addons/godot_cli_control/tests/gut/test_render_api.gd +0 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/addons/godot_cli_control/tests/gut/test_scene_api.gd +0 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/addons/godot_cli_control/tests/gut/test_time_api.gd +0 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/addons/godot_cli_control/tests/gut/test_variant_codec.gd +0 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/addons/godot_cli_control/tests/gut/test_wait_api.gd +0 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/addons/godot_cli_control/tests/run_gut.py +0 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/addons/godot_cli_control/tests/run_gut.sh +0 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/addons/godot_cli_control/tests/test_init_walkthrough.py +0 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/addons/godot_cli_control/tests/test_walkthrough.sh +0 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/pyproject.toml +0 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/python/godot_cli_control/__init__.py +0 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/python/godot_cli_control/__main__.py +0 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/python/godot_cli_control/_duration.py +0 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/python/godot_cli_control/daemon.py +0 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/python/godot_cli_control/pytest_plugin.py +0 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/python/godot_cli_control/registry.py +0 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/python/godot_cli_control/runner.py +0 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/python/godot_cli_control/templates/__init__.py +0 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/python/godot_cli_control/templates/skill/__init__.py +0 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/python/tests/__init__.py +0 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/python/tests/conftest.py +0 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/python/tests/test_daemon.py +0 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/python/tests/test_duration.py +0 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/python/tests/test_e2e_client_direct.py +0 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/python/tests/test_e2e_diag.py +0 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/python/tests/test_e2e_example.py +0 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/python/tests/test_e2e_input.py +0 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/python/tests/test_e2e_multi_instance.py +0 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/python/tests/test_e2e_record.py +0 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/python/tests/test_e2e_render.py +0 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/python/tests/test_e2e_scene.py +0 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/python/tests/test_e2e_screenshot_gui.py +0 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/python/tests/test_e2e_time.py +0 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/python/tests/test_issue_fixes.py +0 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/python/tests/test_pytest_plugin.py +0 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/python/tests/test_registry.py +0 -0
- {godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/python/tests/test_runner.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: godot-cli-control
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: WebSocket bridge for headless / scripted control of Godot scenes.
|
|
5
5
|
Project-URL: Homepage, https://github.com/ClaymanTwinkle/godot-cli-control
|
|
6
6
|
Project-URL: Repository, https://github.com/ClaymanTwinkle/godot-cli-control
|
|
@@ -62,9 +62,9 @@ godot-cli-control tree 3
|
|
|
62
62
|
godot-cli-control daemon stop
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
-
Re-running `init` refreshes both `addons/godot_cli_control/` and the
|
|
65
|
+
Re-running `init` refreshes both `addons/godot_cli_control/` and the skill files to match the installed CLI version (the plugin directory is wiped and re-copied; `project.godot` patching stays idempotent). Pass `--keep-addon` to keep an existing `addons/godot_cli_control/` untouched.
|
|
66
66
|
|
|
67
|
-
`init` also writes `.claude/skills/godot-cli-control
|
|
67
|
+
`init` also writes `.claude/skills/godot-cli-control/` and `.codex/skills/godot-cli-control/` (a lean `SKILL.md` core + `references/*.md` detail files) so AI agents working in your Godot project can pick up this CLI surface automatically without flooding their context. Use `--no-skills` to skip, or `--skills-only` to refresh just those files after a CLI upgrade. See the [top-level README](../README.md#agent-integration) for details.
|
|
68
68
|
|
|
69
69
|
## Async API
|
|
70
70
|
|
|
@@ -6,7 +6,16 @@
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
-
## [0.
|
|
9
|
+
## [0.5.0] - 2026-07-08
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- **`daemon restart`——改启动 flags 一步到位**:容忍未运行的 stop + 以本次给出的 flags 启动(与 `daemon start` 同一套参数,共享注册函数防漂移;**不记忆**上次 start 的 flags)。选靶走 stop 的自动语义(0 个在跑 → default;1 个 → 它;≥2 → 须 `--name`)。stop 硬失败(进程在但停不掉)中止不起新实例(exit 2);仅 ffmpeg 转码失败(AVI 保留)不阻断重启、最终 exit 4 透出(对齐 `run` 语义)。信封 `{"restarted", "was_running", "stop_rc", "instance", "port", "pid"}`。
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- **顶层 `--help` 换成分组命令总览,从 130 行降到 ~87 行**:40+ 子命令原先平铺全量描述;现按 Daemon/读/写/输入/等待/场景时间/渲染诊断 7 组展示、每命令一句话摘要(取自各命令描述首句,自动生成)。旧的手维护清单曾静默漂移掉 `find`/`wait-prop`/`scene-*`/`drag` 等十余条——新版由 `RPC_SPECS` 生成并有测试锁「每个命令必须归入恰好一组」,不再漂移。各命令完整参数照旧 `<cmd> -h`。
|
|
16
|
+
- **`click` 支持 find 同款过滤器定位——「点击文案为 X 的按钮」一条命令原子完成**:`click --contains 开始 [--type BaseButton] [--exact/--name-pattern/--from]`(Python:`client.click(text_contains=...)` / `bridge.click(...)` 同参)。服务端同帧内 find+click,替代 `find` → 解析 `matches[0].path` → `click` 三步两次 RPC,并消灭两步之间的 stale-path 竞态。过滤器必须恰好命中 1 个节点:0 个报 `1001`;≥2 个报**新错误码 `1017 AMBIGUOUS_MATCH`**(message 列出候选路径,收窄过滤器再试——BFS 序不可预期,宁可 fail-loud 也不静默点第一个)。成功信封带 `path`(实际点到的节点)。path 与过滤器互斥(CLI preflight `-1003`/64,服务端 `-32602` 兜裸 RPC)。需要新 addon 行为——老项目跑一次 `init` 同步。
|
|
17
|
+
- **错误信封新增可选 `error.hint` 字段——错误发生点当场给「下一步怎么办」**:此前「拿到 1009 该先 `pause`」「拿到 1004 该 `combo-cancel`」这类指引只存在于 SKILL.md 错误码表里,没读过表的 agent 只能瞎重试。现服务端业务码(`1xxx` 全部 16 个 + `-32601`)由 addon 侧 `CliControlErrorCodes.hint_for` 随响应下发(GUT 锁「新增业务码必须登记 hint」),客户端码(`-1001`/`-1002`/`-1004`/`-1006`/`-1099`)由 CLI 在发射点补齐;服务端下发的 hint 优先于客户端映射。message 已足够具体的码(`-32602`/`-1003`/`-1005`)不带 hint,无 hint 的错误不带空字段。`--text` 模式在 stderr 尾部追加「(提示:...)」;`--instance all` 广播的 per-entry error 同样带 hint;`RpcError` 新增 `.hint` 属性供 Python 调用方使用。服务端 hint 需要新 addon——老项目跑一次 `init` 同步(未同步只是没有 hint,无兼容问题)。
|
|
18
|
+
- **SKILL.md 拆成多文件渐进披露结构,触发时上下文占用从 ~1850 行降到 ~160 行**:旧版单文件 SKILL.md 渲染后 1854 行 / ~142KB(其中 1195 行是 `{{cli_help}}` argparse 全量帮助注入),agent 每次触发 skill 都整体灌进上下文——违背本项目「不让大 payload 撑爆 agent 上下文」的 AI 友好契约。现拆为:核心 `SKILL.md`(quickstart / 退出码 / 单行命令目录 / 高频 pitfalls / 路由表,≤400 行有测试锁)+ `references/*.md` 六个主题文件(commands / error-codes / daemon-multi-instance / recording / python-and-pytest / pitfalls,agent 按需 Read)。`{{cli_help}}` 注入删除——agent 现场跑 `godot-cli-control <cmd> -h` 拿永远最新的帮助,顺带消灭「重渲染必须 COLUMNS=80 + Python 3.12」的 argparse 折行漂移坑(CI skill-render-drift 改整目录 diff,新增 `python -m godot_cli_control.skills_install` 一键重渲染入口)。`init` / `init --skills-only` 现在写整个 skill 目录;`--skills-no-clobber` 逐文件跳过已存在的(老单文件安装升级时缺失的 references/ 仍会补上)。旧项目跑一次 `godot-cli-control init --skills-only` 即升级。API 变更:`skills_install.render_skill(version)` 不再收 `cli_help` 参数,新增 `skills_install.skill_files(version)`。
|
|
10
19
|
|
|
11
20
|
### Fixed
|
|
12
21
|
- **`call` 对错类型「标量」实参不再假成功(#167 的标量侧补集)**:#167 只关了 `call` 的 **JSON Array → 复合 Variant** 侧假成功;错类型的标量实参(`set_name 42` 数字→StringName、`set_z_index '"abc"'` String→int 等)此前仍直接喂 `callv`,引擎喷 `Cannot convert argument` 返回 `null`,RPC 却假 `ok:true`/`result:null`——调用方以为方法生效实则没跑(与 #164/#167 同级数据风险,且 README/SKILL 已宣称「typed 方法假成功不再可能」,文档与实现脱节)。现连 daemon 前按方法声明签名对非 Array 实参也做类型守卫:镜像 Godot `Variant::can_convert_strict`,命中安全互转组(数值组 `bool`/`int`/`float`、字符串组 `String`/`StringName`/`NodePath`)透传,跨组(必 `Cannot convert`)`-32602` fail-loud。数值宽化(`float`→`int`)、`bool`→`int`、`String`→`String` 等合法调用零回归;`null` 实参保守透传(不在本 issue 新增拦截)。纯 addon 服务端行为改动,老项目跑一次 `init` 同步。
|
|
@@ -82,7 +82,7 @@ All methods callable via `godot-cli-control <method>` or `from godot_cli_control
|
|
|
82
82
|
|
|
83
83
|
| Method | Example |
|
|
84
84
|
|---|---|
|
|
85
|
-
| `click(path)` | `await client.click("/root/MyScene/Button")` |
|
|
85
|
+
| `click(path=None, node_type=None, text=None, text_contains=None, name_pattern=None, from_path=None)` | `await client.click("/root/MyScene/Button")`, or locate-and-click atomically with find-style filters: `await client.click(text_contains="开始", node_type="BaseButton")` — must match exactly one node (0 → `1001`, ≥ 2 → `1017` listing candidates); success envelope carries the clicked `path`. `path` and filters are mutually exclusive (`-32602`). CLI: `click <path>` \| `click --contains <substr> [--exact/--type/--name-pattern/--from]` |
|
|
86
86
|
| `click_at(x, y, node=None, button="left", double=False)` | `await client.click_at(320, 240)` or `client.click_at(node="/root/Slot", button="right")` — coordinate-level mouse click via the real event pipeline (viewport physical px); CLI: `click-at <x> <y> \| --node <path> [--button] [--double]` |
|
|
87
87
|
| `mouse_move(x, y, node=None)` | `await client.mouse_move(400, 300)` — inject one mouse-motion event (with `relative`); CLI: `mouse-move <x> <y> \| --node <path>` |
|
|
88
88
|
| `drag(x1, y1, x2, y2, from_node=None, to_node=None, button="left", duration=0.3, steps=10)` | `await client.drag(100, 100, 300, 200)` — press→interpolated move→release (slider/drag-and-drop/swipe); each end may use `from_node`/`to_node` for a node center; game-time `duration`, one drag at a time (`1014` otherwise); CLI: `drag <x1> <y1> <x2> <y2> \| --from-node/--to-node <path> [--button] [--duration] [--steps]` |
|
|
@@ -137,6 +137,8 @@ All methods callable via `godot-cli-control <method>` or `from godot_cli_control
|
|
|
137
137
|
|
|
138
138
|
Three numeric ranges share `error.code`; they never overlap, so a single field is unambiguous.
|
|
139
139
|
|
|
140
|
+
Most errors also carry an optional **`error.hint`** field — a one-line "what to do next" pointer (e.g. `1009` hints "call `pause` first, then `step-frames`"). Server-side hints (`1xxx` / `-32601`) come from the addon (`CliControlErrorCodes.hint_for`) and require a synced addon (re-run `init` on old projects); client-side hints (`-1xxx`) are added by the CLI itself. Codes whose `message` is already case-specific (`-32602`, `-1003`, `-1005`) carry no hint. Follow the hint before consulting the table below.
|
|
141
|
+
|
|
140
142
|
| Code | Source | Meaning |
|
|
141
143
|
|---|---|---|
|
|
142
144
|
| `1001` | server | Node not found at the given path |
|
|
@@ -155,6 +157,7 @@ Three numeric ranges share `error.code`; they never overlap, so a single field i
|
|
|
155
157
|
| `1014` | server | DRAG_IN_PROGRESS: a `drag` was issued while another is still interpolating. One mouse drag in flight at a time — wait for it (or `release-all` to cancel) before the next. State-class (like `1004`). |
|
|
156
158
|
| `1015` | server | EMIT_SIGNAL_DISABLED: `emit-signal` called but daemon was not started with `--allow-emit-signal`. Restart the daemon with that flag (debug-build + localhost + explicit opt-in). `emit_signal` remains in the method blacklist — `call <node> emit_signal` is always rejected regardless of this flag. |
|
|
157
159
|
| `1016` | server | RESPONSE_TOO_LARGE: a single response exceeded the outbound WebSocket buffer (default 10 MB, `godot_cli_control/outbound_buffer_mb`) and was replaced with this error instead of hanging the call to a `-1002` timeout. Usually a bytes-API `screenshot` of a hiDPI frame — pass a path (daemon writes to disk) or raise the buffer. |
|
|
160
|
+
| `1017` | server | AMBIGUOUS_MATCH: `click` with finder filters matched ≥ 2 nodes (message lists candidates). Narrow the filters (`--exact`/`--type`/`--from`/`--name-pattern`) until exactly one matches, or `find` and click the path directly. |
|
|
158
161
|
| `-32600` | server | Malformed JSON-RPC request |
|
|
159
162
|
| `-32601` | server | Unknown method name |
|
|
160
163
|
| `-32602` | server | Invalid params (incl. blocked methods/properties from the security blacklist, `set` value-type mismatch — e.g. `Vector2` property given an array of wrong length / non-numeric elements; `call` typed-arg coercion failure / argument-count mismatch — wrong array length, non-numeric element, a JSON array given to a scalar/Object parameter, a scalar of the wrong type (a string for an `int` param, a number for a `String`/`StringName`), or wrong arg count (#167/#183); or `hold` given `duration ≤ 0` — use `press` for an indefinite hold; also out-of-range values like a scene `timeout` outside `(0, 3600]` sent directly via `GameClient`, and `find_nodes` called with no filter at all or with both `text` and `text_contains`) |
|
|
@@ -175,6 +178,7 @@ The daemon is the long-lived Godot process the CLI talks to (one per project roo
|
|
|
175
178
|
| Command | What it does |
|
|
176
179
|
|---|---|
|
|
177
180
|
| `daemon start [--headless\|--gui] [--record] [--time-scale N] [--name <inst>] [--allow-emit-signal]` | Launch Godot with the bridge listening. `--name` sets the instance name (default `default`); use different names to run multiple Godot daemons for the same project in parallel. Headless vs GUI is auto-detected from the TTY; `--record` forces GUI (Movie Maker needs a real renderer) and is **rejected with `--headless`** (exit 2). `--time-scale N` sets `Engine.time_scale` from frame 0 (range `(0, 100]`). `--allow-emit-signal` unlocks the `emit-signal` subcommand (debug-build + localhost + explicit opt-in three gates); without it any `emit-signal` call returns `1015`. |
|
|
181
|
+
| `daemon restart [same flags as start]` | Tolerant stop (a stopped daemon is fine) + start with the flags you pass **now** — it does not remember the previous start's flags. The one-step way to change daemon flags (e.g. add `--allow-emit-signal`). Hard stop failure aborts before starting (exit 2); a transcode-only stop failure still restarts and exits `4`. |
|
|
178
182
|
| `daemon stop [--name <inst>]` | Stop this project's daemon. With `--name`, stop the named instance. Exit 2 if it stopped cleanly but the `.avi`→`.mp4` ffmpeg transcode failed (raw `.avi` kept; see `.cli_control/ffmpeg.log`). |
|
|
179
183
|
| `daemon stop --all` | Stop every registered daemon across all projects. **Exit 3** if at least one failed; per-record `rc` (and an `error` field on failures) is in `result.stopped[]`. |
|
|
180
184
|
| `daemon stop --all --project <path>` | Stop all instances of one specific project. |
|
{godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/addons/godot_cli_control/bridge/error_codes.gd
RENAMED
|
@@ -62,7 +62,94 @@ const EMIT_SIGNAL_DISABLED: int = 1015
|
|
|
62
62
|
# 容量/资源类永久错——同一响应重试必再超;agent 应改用 path 落盘(screenshot)
|
|
63
63
|
# 或调大 buffer。daemon 用它替换发不出去的大响应,避免 client 干等到 -1002 假超时。
|
|
64
64
|
const RESPONSE_TOO_LARGE: int = 1016
|
|
65
|
+
# click 过滤器定位命中多于一个节点(find+click 原子化):BFS 序对 agent 不可
|
|
66
|
+
# 预期,静默点第一个可能点错按钮(比报错贵得多)——宁可 fail-loud 列出候选
|
|
67
|
+
# 让 agent 收窄。状态类错(与 1004/1014 同族):过滤器本身合法,是场景里
|
|
68
|
+
# 恰好有多个匹配。
|
|
69
|
+
const AMBIGUOUS_MATCH: int = 1017
|
|
65
70
|
|
|
66
71
|
const INVALID_PARAMS: int = -32602
|
|
67
72
|
const INVALID_REQUEST: int = -32600
|
|
68
73
|
const METHOD_UNKNOWN: int = -32601
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
## 错误码 → 「下一步怎么办」提示。空串 = 无提示(该码的 message 已足够具体,
|
|
77
|
+
## 如 -32602 每个 case 有专属文案)。唯一注入点是 GameBridge._send_error:
|
|
78
|
+
## 响应 error 对象带上可选 "hint" 字段,CLI 信封原样透传给 agent——让 agent
|
|
79
|
+
## 在错误发生点当场拿到指引,不依赖它读过 SKILL.md 错误码表。
|
|
80
|
+
## 客户端 -1xxx 的提示在 Python 侧 cli.py 的 _CLIENT_HINTS,两段各管各的
|
|
81
|
+
## (与错误码三段制同构)。hint 只补「下一步动作」,不复述错误本身。
|
|
82
|
+
static func hint_for(code: int) -> String:
|
|
83
|
+
match code:
|
|
84
|
+
NODE_NOT_FOUND:
|
|
85
|
+
return (
|
|
86
|
+
"path must start with /root; locate by text/type with `find`,"
|
|
87
|
+
+ " or `wait-node <path>` if it may not be loaded yet"
|
|
88
|
+
)
|
|
89
|
+
PROPERTY_NOT_FOUND:
|
|
90
|
+
return "inspect the node's properties with `tree <path> 1`"
|
|
91
|
+
METHOD_NOT_FOUND:
|
|
92
|
+
return (
|
|
93
|
+
"inspect node methods with `tree`; for input actions run"
|
|
94
|
+
+ " `actions` (or `actions --all`)"
|
|
95
|
+
)
|
|
96
|
+
COMBO_IN_PROGRESS:
|
|
97
|
+
return "run `combo-cancel` (or `release-all`), then retry"
|
|
98
|
+
SCENE_TREE_TOO_LARGE:
|
|
99
|
+
return (
|
|
100
|
+
"pass --max-nodes N, or query a subtree:"
|
|
101
|
+
+ " `tree <path>` / `children <path>`"
|
|
102
|
+
)
|
|
103
|
+
RESOURCE_UNAVAILABLE:
|
|
104
|
+
return (
|
|
105
|
+
"usually transient - retry after `wait-time 0.05`; on a"
|
|
106
|
+
+ " headless daemon screenshots never work (restart with --gui)"
|
|
107
|
+
)
|
|
108
|
+
SIGNAL_NOT_FOUND:
|
|
109
|
+
return "list the node's signals with `tree`; check the spelling"
|
|
110
|
+
SCENE_UNAVAILABLE:
|
|
111
|
+
return (
|
|
112
|
+
"check the res:// path; if loading hangs, inspect"
|
|
113
|
+
+ " `daemon logs --tail 50`"
|
|
114
|
+
)
|
|
115
|
+
NOT_PAUSED:
|
|
116
|
+
return "call `pause` first, then `step-frames`"
|
|
117
|
+
UNSUPPORTED_NODE_TYPE:
|
|
118
|
+
return (
|
|
119
|
+
"aim at a drawable node (often the child sprite),"
|
|
120
|
+
+ " or use another command"
|
|
121
|
+
)
|
|
122
|
+
NODE_NOT_ON_SCREEN:
|
|
123
|
+
return "move the camera/node into view (or wait for it), then retry"
|
|
124
|
+
FEATURE_UNAVAILABLE:
|
|
125
|
+
return (
|
|
126
|
+
"requires a newer Godot (errors capture needs 4.5+);"
|
|
127
|
+
+ " upgrade the engine or drop this call"
|
|
128
|
+
)
|
|
129
|
+
WRITE_FAILED:
|
|
130
|
+
return "fix the destination path/permissions (daemon-side), then retry"
|
|
131
|
+
DRAG_IN_PROGRESS:
|
|
132
|
+
return (
|
|
133
|
+
"wait for the in-flight drag to finish,"
|
|
134
|
+
+ " or `release-all` to cancel it"
|
|
135
|
+
)
|
|
136
|
+
EMIT_SIGNAL_DISABLED:
|
|
137
|
+
return "restart the daemon with --allow-emit-signal"
|
|
138
|
+
RESPONSE_TOO_LARGE:
|
|
139
|
+
return (
|
|
140
|
+
"pass a file path so the daemon writes to disk,"
|
|
141
|
+
+ " or raise godot_cli_control/outbound_buffer_mb"
|
|
142
|
+
)
|
|
143
|
+
AMBIGUOUS_MATCH:
|
|
144
|
+
return (
|
|
145
|
+
"narrow the filters (--exact / --type / --from / --name-pattern)"
|
|
146
|
+
+ " until exactly one node matches, or run `find` and click"
|
|
147
|
+
+ " the path directly"
|
|
148
|
+
)
|
|
149
|
+
METHOD_UNKNOWN:
|
|
150
|
+
return (
|
|
151
|
+
"client and addon versions drifted - re-run"
|
|
152
|
+
+ " `godot-cli-control init` to sync the addon"
|
|
153
|
+
)
|
|
154
|
+
_:
|
|
155
|
+
return ""
|
{godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/addons/godot_cli_control/bridge/game_bridge.gd
RENAMED
|
@@ -388,7 +388,13 @@ func _send_response(id: String, result: Dictionary) -> void:
|
|
|
388
388
|
|
|
389
389
|
|
|
390
390
|
func _send_error(id: String, code: int, message: String) -> void:
|
|
391
|
-
|
|
391
|
+
# 所有服务端错误响应的唯一出口:按码附加可选 "hint"(下一步指引),
|
|
392
|
+
# 让 agent 在错误发生点当场拿到动作建议(hint 表在 CliControlErrorCodes)。
|
|
393
|
+
var err: Dictionary = {"code": code, "message": message}
|
|
394
|
+
var hint: String = CliControlErrorCodes.hint_for(code)
|
|
395
|
+
if not hint.is_empty():
|
|
396
|
+
err["hint"] = hint
|
|
397
|
+
var response: Dictionary = {"id": id, "error": err}
|
|
392
398
|
_send_json(response)
|
|
393
399
|
|
|
394
400
|
|
{godot_cli_control-0.4.2 → godot_cli_control-0.5.0}/addons/godot_cli_control/bridge/low_level_api.gd
RENAMED
|
@@ -146,9 +146,57 @@ func _merge_extra(base: PackedStringArray, setting_key: String) -> PackedStringA
|
|
|
146
146
|
|
|
147
147
|
|
|
148
148
|
func handle_click(params: Dictionary) -> Dictionary:
|
|
149
|
+
# 两种定位方式:绝对 path(原始形态),或 find 同款过滤器(type / text /
|
|
150
|
+
# text_contains / name_pattern / from)——「点击文案为 X 的按钮」此前要
|
|
151
|
+
# find → 解析 matches[0].path → click 三步两次 RPC,中间还有 stale-path
|
|
152
|
+
# 竞态窗口;现在服务端同帧内 find+click 原子完成。
|
|
153
|
+
if _has_find_filters(params):
|
|
154
|
+
var path_given: String = params.get("path", "") as String
|
|
155
|
+
if not path_given.is_empty():
|
|
156
|
+
return _err(
|
|
157
|
+
CliControlErrorCodes.INVALID_PARAMS,
|
|
158
|
+
"click: give either a node path or finder filters, not both",
|
|
159
|
+
)
|
|
160
|
+
return _click_by_filters(params)
|
|
149
161
|
var node: Node = _get_node_or_error(params)
|
|
150
162
|
if node == null:
|
|
151
163
|
return _node_not_found(params.get("path", "") as String)
|
|
164
|
+
return _click_node(node)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
## 过滤器定位 + 点击。恰好 1 个匹配才点:0 个 → 1001;≥2 个 → 1017 列出
|
|
168
|
+
## 匹配路径让 agent 收窄(宁可 fail-loud 也不静默点第一个——BFS 序对 agent
|
|
169
|
+
## 不可预期,点错按钮比报错贵得多)。成功信封带 "path"(实际点到的节点)。
|
|
170
|
+
func _click_by_filters(params: Dictionary) -> Dictionary:
|
|
171
|
+
# 取 1 个足以点击、多取 4 个用于歧义报错时展示候选(够收窄用)
|
|
172
|
+
var found: Dictionary = _collect_find_matches(params, 5)
|
|
173
|
+
if found.has("error"):
|
|
174
|
+
return found
|
|
175
|
+
var nodes: Array = found["nodes"] as Array
|
|
176
|
+
if nodes.is_empty():
|
|
177
|
+
return _err(
|
|
178
|
+
CliControlErrorCodes.NODE_NOT_FOUND,
|
|
179
|
+
"click: no node matches the given filters",
|
|
180
|
+
)
|
|
181
|
+
if nodes.size() > 1:
|
|
182
|
+
var paths: PackedStringArray = PackedStringArray()
|
|
183
|
+
for entry: Dictionary in found["matches"] as Array:
|
|
184
|
+
paths.append(str(entry["path"]))
|
|
185
|
+
var more: String = ", ..." if (found.get("truncated", false) as bool) else ""
|
|
186
|
+
return _err(
|
|
187
|
+
CliControlErrorCodes.AMBIGUOUS_MATCH,
|
|
188
|
+
"click: %d nodes match the filters: %s%s"
|
|
189
|
+
% [nodes.size(), ", ".join(paths), more],
|
|
190
|
+
)
|
|
191
|
+
var result: Dictionary = _click_node(nodes[0] as Node)
|
|
192
|
+
if not result.has("error"):
|
|
193
|
+
var entry: Dictionary = (found["matches"] as Array)[0] as Dictionary
|
|
194
|
+
result["path"] = str(entry["path"])
|
|
195
|
+
return result
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
## click 的实际派发(path 定位与过滤器定位共用)。
|
|
199
|
+
func _click_node(node: Node) -> Dictionary:
|
|
152
200
|
if node is BaseButton:
|
|
153
201
|
(node as BaseButton).emit_signal("pressed")
|
|
154
202
|
return {"success": true}
|
|
@@ -694,9 +742,39 @@ func handle_find_nodes(params: Dictionary) -> Dictionary:
|
|
|
694
742
|
# 服务端节点搜索(issue #153):程序化匿名 UI(@Button@12,编号跨运行不稳定)
|
|
695
743
|
# 只能按文本/类型定位,客户端 children+get_text 逐层 BFS 在录制模式下
|
|
696
744
|
# 每个 RPC 等帧渲染(50-150ms/次),一次全树遍历曾拖出 57s 死时间——
|
|
697
|
-
#
|
|
698
|
-
#
|
|
699
|
-
|
|
745
|
+
# 这里把几十次往返折成一次。遍历本体抽在 _collect_find_matches,与
|
|
746
|
+
# click 的过滤器定位(原子 find+click)共用。
|
|
747
|
+
var limit: int = params.get("limit", 20) as int
|
|
748
|
+
if limit <= 0:
|
|
749
|
+
limit = 20
|
|
750
|
+
limit = mini(limit, _FIND_MAX_MATCHES)
|
|
751
|
+
var found: Dictionary = _collect_find_matches(params, limit)
|
|
752
|
+
if found.has("error"):
|
|
753
|
+
return found
|
|
754
|
+
var response: Dictionary = {"matches": found["matches"]}
|
|
755
|
+
if found.get("truncated", false) as bool:
|
|
756
|
+
response["truncated"] = true
|
|
757
|
+
return response
|
|
758
|
+
|
|
759
|
+
|
|
760
|
+
## params 里是否带任一 find 过滤器(click 用它区分 path 定位 / 过滤器定位)。
|
|
761
|
+
## 空字符串 = 过滤器未启用(与 get_children 的 type_filter 约定一致)。
|
|
762
|
+
func _has_find_filters(params: Dictionary) -> bool:
|
|
763
|
+
return (
|
|
764
|
+
not (params.get("type", "") as String).is_empty()
|
|
765
|
+
or not (params.get("name_pattern", "") as String).is_empty()
|
|
766
|
+
or not (params.get("text", "") as String).is_empty()
|
|
767
|
+
or not (params.get("text_contains", "") as String).is_empty()
|
|
768
|
+
or not (params.get("from", "") as String).is_empty()
|
|
769
|
+
)
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
## find 遍历本体(handle_find_nodes 与 click 过滤器定位共用):校验过滤器 →
|
|
773
|
+
## 解析 from 子树 → 迭代 BFS。返回 {"matches": Array[Dictionary](序列化 entry),
|
|
774
|
+
## "nodes": Array[Node](与 matches 一一对应,供 click 直接派发), "truncated"?}
|
|
775
|
+
## 或 {"error": ...}。不用 Node.find_children:它先建全量匹配数组没法 limit
|
|
776
|
+
## 短路,且手写 BFS 才有「浅层优先」的稳定排序(UI 搜索友好)。
|
|
777
|
+
func _collect_find_matches(params: Dictionary, limit: int) -> Dictionary:
|
|
700
778
|
var type_filter: String = params.get("type", "") as String
|
|
701
779
|
var name_pattern: String = params.get("name_pattern", "") as String
|
|
702
780
|
var text_exact: String = params.get("text", "") as String
|
|
@@ -714,10 +792,6 @@ func handle_find_nodes(params: Dictionary) -> Dictionary:
|
|
|
714
792
|
CliControlErrorCodes.INVALID_PARAMS,
|
|
715
793
|
"find_nodes: 'text' (exact) and 'text_contains' (substring) are mutually exclusive",
|
|
716
794
|
)
|
|
717
|
-
var limit: int = params.get("limit", 20) as int
|
|
718
|
-
if limit <= 0:
|
|
719
|
-
limit = 20
|
|
720
|
-
limit = mini(limit, _FIND_MAX_MATCHES)
|
|
721
795
|
# 默认搜全树(root 而非 current_scene):弹窗 / autoload 常直接挂 root,
|
|
722
796
|
# 「按文本找按钮」必须能命中它们。
|
|
723
797
|
var start: Node = get_tree().root
|
|
@@ -729,6 +803,7 @@ func handle_find_nodes(params: Dictionary) -> Dictionary:
|
|
|
729
803
|
# 迭代 BFS(数组 + 头指针):浅层匹配排前;不递归 → 深树不爆栈;
|
|
730
804
|
# 与 find_children(owned=true) 不同,代码创建的无 owner 节点照常命中。
|
|
731
805
|
var matches: Array[Dictionary] = []
|
|
806
|
+
var match_nodes: Array[Node] = []
|
|
732
807
|
var truncated: bool = false
|
|
733
808
|
var queue: Array[Node] = []
|
|
734
809
|
for child: Node in start.get_children():
|
|
@@ -743,12 +818,13 @@ func handle_find_nodes(params: Dictionary) -> Dictionary:
|
|
|
743
818
|
truncated = true
|
|
744
819
|
break
|
|
745
820
|
matches.append(_node_entry(node))
|
|
821
|
+
match_nodes.append(node)
|
|
746
822
|
for child: Node in node.get_children():
|
|
747
823
|
queue.append(child)
|
|
748
|
-
var
|
|
824
|
+
var result: Dictionary = {"matches": matches, "nodes": match_nodes}
|
|
749
825
|
if truncated:
|
|
750
|
-
|
|
751
|
-
return
|
|
826
|
+
result["truncated"] = true
|
|
827
|
+
return result
|
|
752
828
|
|
|
753
829
|
|
|
754
830
|
## find_nodes 的过滤判定(AND 语义)。text 两档共享前置:节点必须有 text 属性。
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
## GUT 单元测试:CliControlErrorCodes.hint_for —— 错误码 → 下一步提示。
|
|
2
|
+
##
|
|
3
|
+
## 契约:每个 1xxx 业务码都必须登记 hint(agent 在错误发生点当场拿到指引,
|
|
4
|
+
## 相当于把 SKILL.md 错误码表的「下一步」列下沉到运行时)。新增业务码忘
|
|
5
|
+
## 登记 → test_every_business_code_has_hint 红,与 error_codes.gd 头注释的
|
|
6
|
+
## 「新加业务码必须在这里登记」同一守护思路。
|
|
7
|
+
extends GutTest
|
|
8
|
+
|
|
9
|
+
const ErrorCodes := preload("res://addons/godot_cli_control/bridge/error_codes.gd")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
func test_every_business_code_has_hint() -> void:
|
|
13
|
+
# get_script_constant_map 是 Script 的实例方法:经 preload 常量(类命名空间)
|
|
14
|
+
# 直呼会被 4.7 解析器判「对类调非静态函数」拒载,先落到 Script 类型变量再调。
|
|
15
|
+
var script_res: Script = ErrorCodes
|
|
16
|
+
var consts: Dictionary = script_res.get_script_constant_map()
|
|
17
|
+
for key: String in consts:
|
|
18
|
+
var value: Variant = consts[key]
|
|
19
|
+
if value is int and int(value) >= 1000 and int(value) < 2000:
|
|
20
|
+
assert_ne(
|
|
21
|
+
ErrorCodes.hint_for(int(value)),
|
|
22
|
+
"",
|
|
23
|
+
"业务码 %s(%d) 未登记 hint —— 新码必须同时登记下一步提示" % [key, value]
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
func test_unknown_code_returns_empty() -> void:
|
|
28
|
+
assert_eq(ErrorCodes.hint_for(4242), "")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
func test_method_unknown_hints_init_resync() -> void:
|
|
32
|
+
# -32601 = client/addon 版本漂移,指引重跑 init 同步
|
|
33
|
+
assert_string_contains(ErrorCodes.hint_for(-32601), "init")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
func test_invalid_params_has_no_hint() -> void:
|
|
37
|
+
# -32602 每个 case 的 message 已带专属文案,不加统一 hint(避免复述)
|
|
38
|
+
assert_eq(ErrorCodes.hint_for(-32602), "")
|
|
@@ -338,6 +338,23 @@ func test_sync_handler_error_dict_emits_error_frame() -> void:
|
|
|
338
338
|
assert_eq(str(f.error.message), "Node not found")
|
|
339
339
|
|
|
340
340
|
|
|
341
|
+
func test_error_frame_carries_hint_for_registered_code() -> void:
|
|
342
|
+
# _send_error 是所有错误响应的唯一出口:已登记的码必须带 "hint"(下一步指引)
|
|
343
|
+
_low.click_return = {"error": {"code": 1001, "message": "Node not found"}}
|
|
344
|
+
_send('{"id": "x", "method": "click", "params": {"path": "/missing"}}')
|
|
345
|
+
var f: Dictionary = _last_frame()
|
|
346
|
+
assert_has(f.error, "hint")
|
|
347
|
+
assert_string_contains(str(f.error.hint), "find")
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
func test_error_frame_omits_hint_for_unregistered_code() -> void:
|
|
351
|
+
# -32600 未登记 hint —— 不带空字段占位(信封字段有值才出现)
|
|
352
|
+
_send('not json at all')
|
|
353
|
+
var f: Dictionary = _last_frame()
|
|
354
|
+
assert_eq(int(f.error.code), -32600)
|
|
355
|
+
assert_does_not_have(f.error, "hint")
|
|
356
|
+
|
|
357
|
+
|
|
341
358
|
# ── async 路径 ────────────────────────────────────────────────────
|
|
342
359
|
|
|
343
360
|
func test_async_handler_success_emits_result_frame() -> void:
|
|
@@ -1395,6 +1395,72 @@ func test_find_nodes_from_scopes_search_to_subtree() -> void:
|
|
|
1395
1395
|
assert_string_contains(str((matches[0] as Dictionary).get("name")), "@Button@")
|
|
1396
1396
|
|
|
1397
1397
|
|
|
1398
|
+
# ── handle_click 过滤器定位(原子 find+click) ────────────────────
|
|
1399
|
+
|
|
1400
|
+
|
|
1401
|
+
func _find_fixture_button(text: String) -> Button:
|
|
1402
|
+
for child: Node in (_target.get_node("Panel") as Node).get_children():
|
|
1403
|
+
if child is Button and (child as Button).text == text:
|
|
1404
|
+
return child as Button
|
|
1405
|
+
return null
|
|
1406
|
+
|
|
1407
|
+
|
|
1408
|
+
func test_click_by_text_clicks_unique_match() -> void:
|
|
1409
|
+
_build_find_fixture()
|
|
1410
|
+
var btn: Button = _find_fixture_button("开始游戏")
|
|
1411
|
+
assert_not_null(btn)
|
|
1412
|
+
watch_signals(btn)
|
|
1413
|
+
var result: Dictionary = _api.handle_click({
|
|
1414
|
+
"from": str(_target.get_path()), "text": "开始游戏",
|
|
1415
|
+
})
|
|
1416
|
+
assert_does_not_have(result, "error")
|
|
1417
|
+
assert_eq(result.get("success"), true)
|
|
1418
|
+
assert_eq(
|
|
1419
|
+
str(result.get("path")), str(btn.get_path()),
|
|
1420
|
+
"信封应带实际点到的节点路径(agent 可缓存复用)"
|
|
1421
|
+
)
|
|
1422
|
+
assert_signal_emitted(btn, "pressed")
|
|
1423
|
+
|
|
1424
|
+
|
|
1425
|
+
func test_click_by_filters_zero_matches_is_1001() -> void:
|
|
1426
|
+
_build_find_fixture()
|
|
1427
|
+
var result: Dictionary = _api.handle_click({
|
|
1428
|
+
"from": str(_target.get_path()), "text": "不存在的按钮文案",
|
|
1429
|
+
})
|
|
1430
|
+
assert_has(result, "error")
|
|
1431
|
+
assert_eq(int(result.error.code), 1001)
|
|
1432
|
+
|
|
1433
|
+
|
|
1434
|
+
func test_click_by_filters_multiple_matches_is_1017_listing_paths() -> void:
|
|
1435
|
+
_build_find_fixture()
|
|
1436
|
+
# type=BaseButton 命中两个按钮 → 歧义必须 fail-loud(BFS 序不可预期,
|
|
1437
|
+
# 静默点第一个可能点错),message 列出候选路径供 agent 收窄
|
|
1438
|
+
var result: Dictionary = _api.handle_click({
|
|
1439
|
+
"from": str(_target.get_path()), "type": "BaseButton",
|
|
1440
|
+
})
|
|
1441
|
+
assert_has(result, "error")
|
|
1442
|
+
assert_eq(int(result.error.code), 1017)
|
|
1443
|
+
assert_string_contains(str(result.error.message), "@Button@")
|
|
1444
|
+
|
|
1445
|
+
|
|
1446
|
+
func test_click_path_plus_filters_is_invalid_params() -> void:
|
|
1447
|
+
_build_find_fixture()
|
|
1448
|
+
var result: Dictionary = _api.handle_click({
|
|
1449
|
+
"path": str(_target.get_path()), "text": "开始游戏",
|
|
1450
|
+
})
|
|
1451
|
+
assert_has(result, "error")
|
|
1452
|
+
assert_eq(int(result.error.code), -32602)
|
|
1453
|
+
|
|
1454
|
+
|
|
1455
|
+
func test_click_by_filters_invalid_combo_propagates_find_validation() -> void:
|
|
1456
|
+
# 过滤器校验复用 find 侧:text 与 text_contains 互斥(-32602)
|
|
1457
|
+
var result: Dictionary = _api.handle_click({
|
|
1458
|
+
"text": "a", "text_contains": "b",
|
|
1459
|
+
})
|
|
1460
|
+
assert_has(result, "error")
|
|
1461
|
+
assert_eq(int(result.error.code), -32602)
|
|
1462
|
+
|
|
1463
|
+
|
|
1398
1464
|
# ── _mark_screenshot_stale_check (#156 子问题 B / B3) ────────────────
|
|
1399
1465
|
|
|
1400
1466
|
func test_stale_check_first_call_not_stale() -> void:
|
|
@@ -25,9 +25,9 @@ godot-cli-control tree 3
|
|
|
25
25
|
godot-cli-control daemon stop
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
Re-running `init` refreshes both `addons/godot_cli_control/` and the
|
|
28
|
+
Re-running `init` refreshes both `addons/godot_cli_control/` and the skill files to match the installed CLI version (the plugin directory is wiped and re-copied; `project.godot` patching stays idempotent). Pass `--keep-addon` to keep an existing `addons/godot_cli_control/` untouched.
|
|
29
29
|
|
|
30
|
-
`init` also writes `.claude/skills/godot-cli-control
|
|
30
|
+
`init` also writes `.claude/skills/godot-cli-control/` and `.codex/skills/godot-cli-control/` (a lean `SKILL.md` core + `references/*.md` detail files) so AI agents working in your Godot project can pick up this CLI surface automatically without flooding their context. Use `--no-skills` to skip, or `--skills-only` to refresh just those files after a CLI upgrade. See the [top-level README](../README.md#agent-integration) for details.
|
|
31
31
|
|
|
32
32
|
## Async API
|
|
33
33
|
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.
|
|
22
|
-
__version_tuple__ = version_tuple = (0,
|
|
21
|
+
__version__ = version = '0.5.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 5, 0)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -139,9 +139,28 @@ class GameBridge:
|
|
|
139
139
|
|
|
140
140
|
# ── UI 交互 ──
|
|
141
141
|
|
|
142
|
-
def click(
|
|
143
|
-
|
|
144
|
-
|
|
142
|
+
def click(
|
|
143
|
+
self,
|
|
144
|
+
path: str | None = None,
|
|
145
|
+
*,
|
|
146
|
+
node_type: str | None = None,
|
|
147
|
+
text: str | None = None,
|
|
148
|
+
text_contains: str | None = None,
|
|
149
|
+
name_pattern: str | None = None,
|
|
150
|
+
from_path: str | None = None,
|
|
151
|
+
) -> dict:
|
|
152
|
+
"""点击 UI 节点:给 ``path`` 直接定位,或给 find 同款过滤器由服务端
|
|
153
|
+
同帧原子 find+click(恰好 1 个匹配才点:0 个 → 1001,≥2 个 → 1017)。"""
|
|
154
|
+
return self._run(
|
|
155
|
+
self._client.click(
|
|
156
|
+
path,
|
|
157
|
+
node_type=node_type,
|
|
158
|
+
text=text,
|
|
159
|
+
text_contains=text_contains,
|
|
160
|
+
name_pattern=name_pattern,
|
|
161
|
+
from_path=from_path,
|
|
162
|
+
)
|
|
163
|
+
)
|
|
145
164
|
|
|
146
165
|
def click_at(
|
|
147
166
|
self,
|