From b96473836dec5f2828287043d22bc02f69f5a5e6 Mon Sep 17 00:00:00 2001 From: kdletters <61648117+kdletters@users.noreply.github.com> Date: Sun, 20 Sep 2026 17:26:34 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E9=BD=90=20Unity=20=E4=B8=8E=20Godot?= =?UTF-8?q?=20=E5=B8=B8=E7=94=A8=E6=93=8D=E4=BD=9C=E6=8C=87=E5=AF=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增两种编辑器的内置 Skill 和场景、资源、UI、保存撤销示例 接入 DirectProject 按需读取与 Runtime 同源操作参考 补齐指南原文实机测试、安装投影及工具说明完整性检查 记录 Godot 图形补验结果并保留尚未验收的边界 --- .../scripts/skill-pack-manifest.mjs | 2 + .../agc-skills/agc-godot-editor/SKILL.md | 14 + ...操作指南】Godot编辑器常用操作-2026-09-20.md | 257 ++++++++++++ .../agc-skills/agc-unity-editor/SKILL.md | 10 + ...操作指南】Unity编辑器常用操作-2026-09-20.md | 225 +++++++++++ .../resources/agc-skills/manifest.json | 34 +- .../src/agent/codex_app_server/mod.rs | 4 +- .../src-tauri/src/agent/direct_runtime/mod.rs | 22 + .../src-tauri/src/agent/direct_tools_mcp.rs | 16 + .../src-tauri/src/agent/skill_pack.rs | 51 ++- .../src-tauri/src/agent_native_tools.rs | 83 +++- .../shared-memory/decision-log.md | 4 + ...方案】AGC Godot编辑器插件接入-2026-09-20.md | 6 + ...方案】AGC Unity编辑器插件接入-2026-09-18.md | 6 +- ...案】AGC通用插件宿主与编辑器适配-2026-09-09.md | 21 +- ...案】AI游戏创作智能体App实施计划-2026-06-24.md | 2 +- .../gdextension/tests/guide-examples.test.mjs | 380 ++++++++++++++++++ .../tests/guide_examples.rs | 142 +++++++ 18 files changed, 1266 insertions(+), 13 deletions(-) create mode 100644 apps/ai-game-creator-shell/src-tauri/resources/agc-skills/agc-godot-editor/SKILL.md create mode 100644 apps/ai-game-creator-shell/src-tauri/resources/agc-skills/agc-godot-editor/references/【操作指南】Godot编辑器常用操作-2026-09-20.md create mode 100644 apps/ai-game-creator-shell/src-tauri/resources/agc-skills/agc-unity-editor/SKILL.md create mode 100644 apps/ai-game-creator-shell/src-tauri/resources/agc-skills/agc-unity-editor/references/【操作指南】Unity编辑器常用操作-2026-09-20.md create mode 100644 plugins/agc-godot-editor/native/gdextension/tests/guide-examples.test.mjs create mode 100644 plugins/agc-unity-editor/native/unity-editor-bridge/tests/guide_examples.rs diff --git a/apps/ai-game-creator-shell/scripts/skill-pack-manifest.mjs b/apps/ai-game-creator-shell/scripts/skill-pack-manifest.mjs index 5f89ad3ae..88c717208 100644 --- a/apps/ai-game-creator-shell/scripts/skill-pack-manifest.mjs +++ b/apps/ai-game-creator-shell/scripts/skill-pack-manifest.mjs @@ -9,7 +9,9 @@ export const EXPECTED_SKILL_NAMES = Object.freeze([ 'agc-browser-playtest', 'agc-client-projection', 'agc-game-production-workflow', + 'agc-godot-editor', 'agc-project-structure', + 'agc-unity-editor', 'agc-web-game-development', 'taonier-art-assets', ]); diff --git a/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/agc-godot-editor/SKILL.md b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/agc-godot-editor/SKILL.md new file mode 100644 index 000000000..31a4edd78 --- /dev/null +++ b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/agc-godot-editor/SKILL.md @@ -0,0 +1,14 @@ +--- +name: agc-godot-editor +description: 在 AGC 中通过已连接的 Godot 编辑器读取、修改和保存场景、节点、资源与 UI,运行项目并诊断 GDScript 执行结果。 +--- + +# Godot 编辑器操作 + +使用当前环境实际提供的 Godot 执行工具:DirectProject 为 `agc_godot_execute`,Runtime 使用 `godot.editor.execute` 对应的已发现工具。执行载荷只含 GDScript **函数体** `code`;Direct 传 `{code:...}`,Runtime 按实际 schema 包装为 `{reason:"...",input:{code:...}}`。项目、编辑器和连接身份由 AGC 管理。 + +开始操作前读取 [Godot 编辑器常用操作](references/【操作指南】Godot编辑器常用操作-2026-09-20.md),按当前任务选取查询、节点、撤销、资源、UI、保存或运行示例。先查询真实编辑场景与目标节点,再做有限修改并回读结果。 + +DLL 随 AGC 分发,首次连接需要 Godot 扫描时重新聚焦编辑器即可;无需手动复制 DLL、配置端口或运行引导脚本。不要读取或返回连接凭据。 + +明确失败也可能已经修改场景;先检查日志和真实状态再修复。超时、断线或 `needs-reconciliation` 表示结果待核对,不自动重放,不通过重连绕过执行阻断。保存、运行和删除范围以用户任务为准;局部 `UndoRedo` 不等于编辑器撤销历史。 diff --git a/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/agc-godot-editor/references/【操作指南】Godot编辑器常用操作-2026-09-20.md b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/agc-godot-editor/references/【操作指南】Godot编辑器常用操作-2026-09-20.md new file mode 100644 index 000000000..ed00f7074 --- /dev/null +++ b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/agc-godot-editor/references/【操作指南】Godot编辑器常用操作-2026-09-20.md @@ -0,0 +1,257 @@ +# Godot 编辑器常用操作 + +面向 AGC 内置 Godot 工具,仅支持 Windows x64 标准编辑器;不推断 .NET 或其他平台支持。缺少执行工具时报告不可用。目录:执行、查询、节点、撤销、资源、UI、保存、运行、诊断。 + +## 执行合同 + +- Direct 的 `agc_godot_execute` 传 `{code:...}`;Runtime 先发现 `godot.editor.execute`,按实际 schema 传 `{reason:"操作原因",input:{code:...}}`。执行载荷只含 `code`,不增加项目路径等字段。以下是函数体,不增加 `extends`、`@tool` 或 `func run()`,保留内部缩进。 +- 上下文是临时 `RefCounted.run()`;`self` 不是场景 Node,不能直接 `get_tree()`。用 `EditorInterface.get_edited_scene_root()` 取得编辑场景根;`EditorInterface.get_base_control().get_tree().root` 是编辑器根,不是用户场景。 +- 各次调用不共享局部变量。返回 `null`、布尔、整数、有限浮点、字符串、数组、字符串键字典。Node、Resource、Vector2、Color 等需投影为路径、数值数组或字典;不要直接返回 Godot 对象。用 `return` 返回结果,`print` 只写有界日志。 +- 可 `await EditorInterface.get_base_control().get_tree().process_frame` 或短计时器;不要死循环、长阻塞,也不要派发未等待的后台修改。一次只执行一段有界操作。 +- DLL 原件由 AGC 安装资源提供,私有缓存按编辑器实例隔离;首次发现扩展时重新聚焦 Godot 即可。不手改 `.gdextension`、DLL、端口、令牌或 `.godot/agc`。 + +## 读取当前场景、选中节点和树 + +先核对 `scene`、类型和相对路径。无打开场景时返回空结果。遍历最多 256 节点,`truncated` 为 true 时按目标子树继续查。 + + +```gdscript +var root := EditorInterface.get_edited_scene_root() +if root == null: + return {"scene": null, "nodes": [], "selected": []} +var selected: Array = [] +for node in EditorInterface.get_selection().get_selected_nodes(): + if node == root or root.is_ancestor_of(node): + selected.append(str(root.get_path_to(node))) +var nodes: Array = [] +var pending: Array[Node] = [root] +while not pending.is_empty() and nodes.size() < 256: + var node: Node = pending.pop_back() + nodes.append({"path": str(root.get_path_to(node)), "type": node.get_class()}) + for child in node.get_children(): + pending.append(child) +return {"scene": root.scene_file_path, "root": str(root.name), "nodes": nodes, + "selected": selected, "truncated": not pending.is_empty()} +``` + +`get_node_or_null("Player/Sprite2D")` 相对于场景根。选择用 `EditorInterface.get_selection().clear()` / `add_node(node)`;检查器用 `EditorInterface.edit_node(node)`,均不保存场景。 + +## 创建、改属性、删除节点 + +将 `AGCGuideMarker` 替换为任务指定且不冲突的名称。示例直接修改,不自动加入编辑器撤销历史。`add_child` 后设 `owner = root` 才随当前场景保存;新子树逐个设置 owner,不重写实例场景内部 owner。 + + +```gdscript +var root := EditorInterface.get_edited_scene_root() +assert(root != null and root.get_node_or_null("AGCGuideMarker") == null) +var marker := Node2D.new() +marker.name = "AGCGuideMarker" +root.add_child(marker) +marker.owner = root +marker.position = Vector2(12, 24) +EditorInterface.mark_scene_as_unsaved() +return {"path": str(root.get_path_to(marker)), "position": [marker.position.x, marker.position.y], + "owned": marker.owner == root} +``` + + +```gdscript +var root := EditorInterface.get_edited_scene_root() +assert(root != null) +var marker := root.get_node_or_null("AGCGuideMarker") as Node2D +assert(marker != null) +marker.position = Vector2(24, 48) +EditorInterface.mark_scene_as_unsaved() +return {"path": str(root.get_path_to(marker)), "position": [marker.position.x, marker.position.y]} +``` + +删除前核对目标及后代;`queue_free()` 连同后代删除,下一帧完成后对象失效。不要删除场景根。 + + +```gdscript +var root := EditorInterface.get_edited_scene_root() +assert(root != null) +var marker := root.get_node_or_null("AGCGuideMarker") +assert(marker != null and marker != root) +root.remove_child(marker) +marker.queue_free() +EditorInterface.mark_scene_as_unsaved() +await EditorInterface.get_base_control().get_tree().process_frame +return {"removed": root.get_node_or_null("AGCGuideMarker") == null} +``` + +其它属性如 `Sprite2D.texture`、`Node3D.position`、`Label.text`,先确认实际类型。向量和颜色返回数值数组。 + +## 撤销:局部事务与编辑器历史 + +局部 `UndoRedo.new()` 不进入 Ctrl+Z 菜单,调用结束即失去历史。下例同一次调用改位置为 `(80, 90)`,随后撤销并回读。 + + +```gdscript +var root := EditorInterface.get_edited_scene_root() +assert(root != null) +var marker := root.get_node_or_null("AGCGuideMarker") as Node2D +assert(marker != null) +var previous := marker.position +var undo := UndoRedo.new() +undo.create_action("验证位置撤销") +undo.add_do_property(marker, "position", Vector2(80, 90)) +undo.add_undo_property(marker, "position", previous) +undo.commit_action() +var changed := marker.position +assert(undo.undo()) +return {"changed": [changed.x, changed.y], "restored": [marker.position.x, marker.position.y], + "matches": marker.position == previous} +``` + +Ctrl+Z 需复用已有 `EditorPlugin.get_undo_redo()` 的 `EditorUndoRedoManager`,`create_action(..., UndoRedo.MERGE_DISABLE, root)` 指定场景历史。局部 UndoRedo 方法操作用 Callable;manager 用对象、方法名、参数。不要为取得 manager 擅自安装 addon。 + +创建历史需登记 `add_child`、`owner`、逆向 `remove_child` 和 `add_do_reference`;删除记录父节点、顺序、owner,用 `add_undo_reference` 保活,禁止 `free/queue_free` 后再承诺恢复。属性成对登记新旧值。无持久 EditorPlugin 时只能承诺直接修改,不能承诺 Ctrl+Z。 + +保存重开后旧 Node 引用和局部历史不能复用。需重新查询,确认无后续用户改动,再执行逆操作并重新保存;内存 undo 不会恢复磁盘文件。 + +## PackedScene 与资源 + +将 `res://agc_guide_piece.tscn` 改为任务指定新路径,确认不存在并检查 `pack`、`ResourceSaver.save` 返回值。`owner` 决定子节点能否打包;不照例覆盖已有资源。 + + +```gdscript +var target := "res://agc_guide_piece.tscn" +assert(not FileAccess.file_exists(target)) +var source := Node2D.new() +source.name = "GuidePiece" +var child := Marker2D.new() +child.name = "Anchor" +source.add_child(child) +child.owner = source +var packed := PackedScene.new() +var packed_error := packed.pack(source) +source.free() +assert(packed_error == OK) +var save_error := ResourceSaver.save(packed, target) +assert(save_error == OK) +EditorInterface.get_resource_filesystem().scan() +return {"path": target, "saved": FileAccess.file_exists(target)} +``` + +实例化时检查 PackedScene 类型,只把实例根归属于当前根,保留内部所有权。实例局部覆盖不会改写源 `.tscn`。 + + +```gdscript +var root := EditorInterface.get_edited_scene_root() +assert(root != null and root.get_node_or_null("AGCGuidePiece") == null) +var packed := ResourceLoader.load("res://agc_guide_piece.tscn", "PackedScene", ResourceLoader.CACHE_MODE_IGNORE) as PackedScene +assert(packed != null) +var instance := packed.instantiate(PackedScene.GEN_EDIT_STATE_INSTANCE) +instance.name = "AGCGuidePiece" +root.add_child(instance) +instance.owner = root +EditorInterface.mark_scene_as_unsaved() +return {"path": str(root.get_path_to(instance)), "source": instance.scene_file_path, + "has_anchor": instance.get_node_or_null("Anchor") != null} +``` + +ResourceLoader 默认缓存可能返回旧对象;外部刚写文件可用 `CACHE_MODE_IGNORE`。共享 Resource 的修改影响所有引用;局部变化先 `duplicate()` 再赋回。图片/音频须等扫描和导入完成,文件存在不代表已导入。 + +## 基础 Control / Container UI + +Container 管理直属子 Control 布局,使用 `custom_minimum_size`、size flags、theme 常量,避免手写子控件 position/size。新节点逐个设置 owner。 + + +```gdscript +var root := EditorInterface.get_edited_scene_root() +assert(root != null and root.get_node_or_null("AGCGuideHUD") == null) +var layer := CanvasLayer.new() +layer.name = "AGCGuideHUD" +root.add_child(layer) +layer.owner = root +var center := CenterContainer.new() +center.name = "Center" +layer.add_child(center) +center.owner = root +center.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) +var column := VBoxContainer.new() +column.name = "Column" +center.add_child(column) +column.owner = root +column.custom_minimum_size = Vector2(240, 96) +column.add_theme_constant_override("separation", 8) +var label := Label.new() +label.name = "Title" +label.text = "关卡目标" +column.add_child(label) +label.owner = root +var button := Button.new() +button.name = "Start" +button.text = "开始" +button.custom_minimum_size = Vector2(200, 40) +column.add_child(button) +button.owner = root +EditorInterface.mark_scene_as_unsaved() +return {"path": str(root.get_path_to(layer)), "title": label.text, "button": button.text, + "anchors": [center.anchor_left, center.anchor_top, center.anchor_right, center.anchor_bottom], + "owned": [layer.owner == root, center.owner == root, column.owner == root, label.owner == root, button.owner == root]} +``` + +持久信号应连接游戏脚本的方法,不把临时执行器 Callable 当运行时回调。此例只建布局;尺寸、层级、输入仍需实际试玩验收。 + +## 保存、重新打开与新场景 + +`mark_scene_as_unsaved()` 不写盘。仅在获准保存全部当前改动时执行。`save_scene_as(path,false)` 跳过缩略图但返回 void;旧文件可加载不代表本次保存成功。下例依赖前文三个分支,先核验磁盘节点和位置再重开;实际任务须覆盖所有待保存变更,无法证明时只保存、不 reload。 + + +```gdscript +var root := EditorInterface.get_edited_scene_root() +assert(root != null and not root.scene_file_path.is_empty()) +var scene_path := root.scene_file_path +var expected: Vector2 = root.get_node("AGCGuideMarker").position +EditorInterface.save_scene_as(scene_path, false) +var saved := ResourceLoader.load(scene_path, "PackedScene", ResourceLoader.CACHE_MODE_IGNORE) as PackedScene +assert(saved != null) +var probe := saved.instantiate() +var marker := probe.get_node_or_null("AGCGuideMarker") as Node2D +var matches := marker != null and marker.position == expected and probe.has_node("AGCGuidePiece/Anchor") and probe.has_node("AGCGuideHUD/Center/Column/Title") +probe.free() +if not matches: + return {"reloaded": false, "reason": "磁盘内容未验证,保留当前编辑场景"} +EditorInterface.reload_scene_from_path(scene_path) +await EditorInterface.get_base_control().get_tree().process_frame +var reopened := EditorInterface.get_edited_scene_root() +assert(reopened != null and reopened.scene_file_path == scene_path) +return {"scene": reopened.scene_file_path, "saved": true, "reloaded": true, + "has_piece": reopened.get_node_or_null("AGCGuidePiece/Anchor") != null, + "has_ui": reopened.get_node_or_null("AGCGuideHUD/Center/Column/Title") != null} +``` + +打开场景用 `open_scene_from_path("res://...")`,等一帧重新取根核对路径;`get_open_scenes()` 查已打开路径,均属 EditorInterface。未命名场景用 `save_scene_as(path)`;常规 GUI 用 `save_scene()` 检查 `OK`,headless 缩略图可能报错。不要覆盖未知未保存工作。 + +## 运行与停止 + +EditorInterface 的 `play_current_scene()` 运行当前场景,`play_main_scene()` 运行主场景,`play_custom_scene("res://...")` 运行指定场景。仅需试玩时调用,先核对路径、主场景与未保存改动。`is_playing_scene()` / `get_playing_scene()` 只报告启动状态,不证明玩法正确;编辑根不是游戏 Remote SceneTree。 + + +```gdscript +var was_playing := EditorInterface.is_playing_scene() +if was_playing: + EditorInterface.stop_playing_scene() + await EditorInterface.get_base_control().get_tree().process_frame +return {"was_playing": was_playing, "playing": EditorInterface.is_playing_scene()} +``` + +## 错误诊断与回执 + +- 读取执行回执的 `ok/status/result/error/logs`。编译错误先检查函数体包装、类型推断和真实 API;确定运行失败也可能已经执行前半段修改,先读回节点/资源,再修复剩余步骤。 +- `godot_result_not_serializable` 可能只是返回了对象,不能据此认定修改未发生;改用只读查询返回路径和标量。`assert` 失败不会替你回滚此前副作用。 +- 超时、断线、`needs-reconciliation` 或发送后的身份不明不能自动重放;先核对编辑器真实状态,按 AGC 现有恢复流程处理阻断。重新连接、启停插件或重启 Runner 都不是“确认没有执行”。 +- 捕获日志只覆盖这次编辑器执行且有长度上限;成功启动游戏不等于运行时无错误。结合 Godot Output/Debugger、游戏日志与实际试玩核验,不将空日志当作无故障。 + + +```gdscript +var root := EditorInterface.get_edited_scene_root() +return {"version": Engine.get_version_info().string, + "editor": Engine.is_editor_hint(), "scene": root.scene_file_path if root != null else null, + "open_scenes": Array(EditorInterface.get_open_scenes()), "playing": EditorInterface.is_playing_scene(), + "playing_scene": EditorInterface.get_playing_scene()} +``` + +示例已在 Godot 4.7.2 标准版 headless 验证;停止仅验证已停止状态。GUI 缩略图保存、Ctrl+Z 历史、运行中停止及 UI 视觉效果未在此指南测试中验收。 diff --git a/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/agc-unity-editor/SKILL.md b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/agc-unity-editor/SKILL.md new file mode 100644 index 000000000..a70108174 --- /dev/null +++ b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/agc-unity-editor/SKILL.md @@ -0,0 +1,10 @@ +--- +name: agc-unity-editor +description: 通过 AGC 的 Unity 编辑器执行工具读取和修改当前项目的场景、对象、组件、Prefab、Canvas 与资源,并保存、撤销和检查播放状态。 +--- + +# Unity 编辑器操作 + +使用当前会话提供的 Unity 执行工具,提交 C# 方法正文。开始操作前读取[常用操作指南](references/【操作指南】Unity编辑器常用操作-2026-09-20.md),按任务选择其中的示例。指南包含调用格式、目标定位、返回值投影和可执行代码。 + +先查询目标与编辑状态,修改后回读;写操作显式登记 Undo,保存操作检查返回值。执行失败可能留下部分修改,结果未知时不得重放。插件不会自动把任意代码变成可撤销事务。 diff --git a/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/agc-unity-editor/references/【操作指南】Unity编辑器常用操作-2026-09-20.md b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/agc-unity-editor/references/【操作指南】Unity编辑器常用操作-2026-09-20.md new file mode 100644 index 000000000..06b8bdf98 --- /dev/null +++ b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/agc-unity-editor/references/【操作指南】Unity编辑器常用操作-2026-09-20.md @@ -0,0 +1,225 @@ +# Unity 编辑器常用操作 + +## 调用与结果 + +当前接入支持 Windows x64 的 Mono 编辑器。工具缺失时报告不可用,不推断 .NET/CoreCLR 或其他平台已支持。 + +连接当前项目的 Unity 后提交仅含 `code` 的执行载荷。DirectProject 工具 `agc_unity_execute` 传 `{"code":"return 42;"}`;Runtime 的 `unity.editor.execute` 按实际 schema 传 `{"reason":"读取编辑器状态","input":{"code":"return 42;"}}`。以会话工具清单为准。 + +`code` 是主线程执行的方法正文,直接 `return`,不加 `using`、类或 `Main`。使用完整 API 名称。Unity 对象先投影为普通数据;返回集合最多保留 32 项,嵌套深度达到 4 会转字符串,采用浅层投影、每批 30 项及显式截断标记。跨调用保留路径/GUID,实例 ID 仅当前 Editor 生命周期内有效。 + +先确认场景、选择、编辑模式和待修改资源。遍历 `GetRootGameObjects()` 和 `GetComponentsInChildren(..., true)` 可包含未激活对象;`GameObject.Find` 会漏掉它们。结合场景路径、层级路径和实例 ID 回读目标,重名时不要任取首个。 + +`completed` 只证明代码返回,仍要回读。`failed` 可能已部分修改,检查 `dispatched` 与现场后修复;编译失败且 `dispatched=false` 表示未执行。`needs-reconciliation`、超时或断线后结果未知时不重放,保留执行 ID 并核对现场,重连不等于允许重试。工具不自动撤销/回滚,不能中断死循环;保持调用短小,不在主线程等待编译/播放切换。 + +## 当前场景、选择和层级 + +返回当前场景及最多 30 个节点。其他场景用 `SceneManager.sceneCount/GetSceneAt` 枚举。 + + +```csharp +var scene = UnityEngine.SceneManagement.SceneManager.GetActiveScene(); +var rows = new System.Collections.Generic.List(); +var queue = new System.Collections.Generic.Queue(); +foreach (var root in scene.GetRootGameObjects()) queue.Enqueue(root.transform); +while (queue.Count > 0 && rows.Count < 30) { + var t = queue.Dequeue(); + var path = t.name; + for (var p = t.parent; p != null; p = p.parent) path = p.name + "/" + path; + rows.Add(new { id = t.gameObject.GetInstanceID(), path, active = t.gameObject.activeSelf, + x = t.localPosition.x, y = t.localPosition.y, z = t.localPosition.z }); + for (int i = 0; i < t.childCount; i++) queue.Enqueue(t.GetChild(i)); +} +var selected = UnityEditor.Selection.activeGameObject; +return new { scene = scene.path, dirty = scene.isDirty, nodes = rows.ToArray(), truncated = queue.Count > 0, + selectedId = selected == null ? 0 : selected.GetInstanceID(), + playing = UnityEditor.EditorApplication.isPlaying, compiling = UnityEditor.EditorApplication.isCompiling }; +``` + +## 创建、修改、删除与 Undo + +示例对象 `AGC_Guide_Object` 应替换成任务目标。编辑先退出播放模式。属性写入前 `Undo.RecordObject`;创建用 `RegisterCreatedObjectUndo`,加组件用 `Undo.AddComponent`,删除用 `Undo.DestroyObjectImmediate`,改父级用 `Undo.SetTransformParent`。磁盘写入、外部副作用及未登记修改不会自动撤销。 + +创建对象和组件并选中它;检查重复名是防误建措施,不是结果未知后重试的许可。 + + +```csharp +if (UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode) throw new System.Exception("请先停止播放"); +var scene = UnityEngine.SceneManagement.SceneManager.GetActiveScene(); +foreach (var root in scene.GetRootGameObjects()) + if (root.name == "AGC_Guide_Object") throw new System.Exception("目标已存在,请先核对"); +UnityEditor.Undo.IncrementCurrentGroup(); +int group = UnityEditor.Undo.GetCurrentGroup(); +UnityEditor.Undo.SetCurrentGroupName("AGC 创建对象"); +var go = new UnityEngine.GameObject("AGC_Guide_Object"); +UnityEditor.Undo.RegisterCreatedObjectUndo(go, "AGC 创建对象"); +UnityEditor.Undo.AddComponent(go); +UnityEditor.Selection.activeGameObject = go; +UnityEditor.Undo.CollapseUndoOperations(group); +return new { id = go.GetInstanceID(), name = go.name, collider = go.GetComponent() != null }; +``` + +确认选择是目标后修改。Prefab 实例属性写入后记录 override。改 Prefab 资产用 `LoadPrefabContents/SaveAsPrefabAsset/UnloadPrefabContents` 并在 `finally` 释放,不能当场景对象保存。 + + +```csharp +var go = UnityEditor.Selection.activeGameObject; +if (go == null || !go.scene.IsValid() || UnityEditor.EditorUtility.IsPersistent(go)) throw new System.Exception("请选中场景对象"); +if (UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode) throw new System.Exception("请先停止播放"); +UnityEditor.Undo.IncrementCurrentGroup(); +int group = UnityEditor.Undo.GetCurrentGroup(); +UnityEditor.Undo.SetCurrentGroupName("AGC 修改对象"); +UnityEditor.Undo.RecordObject(go.transform, "AGC 移动对象"); +go.transform.localPosition = new UnityEngine.Vector3(1, 2, 3); +var collider = go.GetComponent(); +if (collider == null) collider = UnityEditor.Undo.AddComponent(go); +UnityEditor.Undo.RecordObject(collider, "AGC 修改碰撞体"); +collider.size = new UnityEngine.Vector3(2, 3, 4); +if (UnityEditor.PrefabUtility.IsPartOfPrefabInstance(go)) { + UnityEditor.PrefabUtility.RecordPrefabInstancePropertyModifications(go.transform); + UnityEditor.PrefabUtility.RecordPrefabInstancePropertyModifications(collider); +} +UnityEditor.Undo.FlushUndoRecordObjects(); +UnityEditor.Undo.CollapseUndoOperations(group); +UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty(go.scene); +return new { id = go.GetInstanceID(), x = go.transform.localPosition.x, colliderX = collider.size.x }; +``` + +删除选择对象上的碰撞体;删除整个已核对对象时把 `collider` 替换为 `go`,并提前回读待删除子树。 + + +```csharp +var go = UnityEditor.Selection.activeGameObject; +if (go == null || !go.scene.IsValid() || UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode) throw new System.Exception("需要编辑模式中的场景对象"); +var collider = go.GetComponent(); +if (collider == null) throw new System.Exception("没有 BoxCollider"); +UnityEditor.Undo.IncrementCurrentGroup(); +UnityEditor.Undo.SetCurrentGroupName("AGC 删除碰撞体"); +UnityEditor.Undo.DestroyObjectImmediate(collider); +UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty(go.scene); +return new { removed = go.GetComponent() == null }; +``` + +只在确认最后一条 Undo 就是本次操作时执行撤销,避免撤销用户插入的编辑。撤销后重新运行查询检查对象/属性。 + + +```csharp +UnityEditor.Undo.PerformUndo(); +var go = UnityEditor.Selection.activeGameObject; +return new { selectedId = go == null ? 0 : go.GetInstanceID(), collider = go != null && go.GetComponent() != null }; +``` + +## 资源查找与 Prefab 实例化 + +按类型和目录查询,拿到 GUID/路径后加载。下例返回前 30 个 Prefab;过滤器可换成 `t:Material`、`t:Texture2D` 等。 + + +```csharp +var ids = UnityEditor.AssetDatabase.FindAssets("t:Prefab", new[] { "Assets" }); +var rows = new System.Collections.Generic.List(); +for (int i = 0; i < ids.Length && i < 30; i++) + rows.Add(new { guid = ids[i], path = UnityEditor.AssetDatabase.GUIDToAssetPath(ids[i]) }); +return new { assets = rows.ToArray(), total = ids.Length, truncated = ids.Length > 30 }; +``` + +路径替换为已查到的 Prefab;`InstantiatePrefab` 保持 Prefab 联系,后续修改登记 override。 + + +```csharp +if (UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode) throw new System.Exception("请先停止播放"); +var path = "Assets/AGCGuide/Guide.prefab"; +var asset = UnityEditor.AssetDatabase.LoadAssetAtPath(path); +if (asset == null || UnityEditor.PrefabUtility.GetPrefabAssetType(asset) == UnityEditor.PrefabAssetType.NotAPrefab) throw new System.Exception("未找到 Prefab"); +var scene = UnityEngine.SceneManagement.SceneManager.GetActiveScene(); +UnityEditor.Undo.IncrementCurrentGroup(); +var instance = (UnityEngine.GameObject)UnityEditor.PrefabUtility.InstantiatePrefab(asset, scene); +UnityEditor.Undo.RegisterCreatedObjectUndo(instance, "AGC 实例化 Prefab"); +UnityEditor.Selection.activeGameObject = instance; +return new { id = instance.GetInstanceID(), source = UnityEditor.PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(instance) }; +``` + +## 基础 Canvas 与布局 + +先查询并复用现有 UI。下例创建 Canvas 与居中布局容器,不依赖 uGUI/TMP,容器无可见图形。添加 `Image`、`Button`、文本或 `EventSystem` 前确认项目 UI 体系和包,避免重复事件系统。 + + +```csharp +if (UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode) throw new System.Exception("请先停止播放"); +var scene = UnityEngine.SceneManagement.SceneManager.GetActiveScene(); +foreach (var root in scene.GetRootGameObjects()) + if (root.name == "AGC_Guide_Canvas") throw new System.Exception("示例 Canvas 已存在"); +UnityEditor.Undo.IncrementCurrentGroup(); +int group = UnityEditor.Undo.GetCurrentGroup(); +var canvasObject = new UnityEngine.GameObject("AGC_Guide_Canvas", typeof(UnityEngine.RectTransform), typeof(UnityEngine.Canvas)); +UnityEditor.Undo.RegisterCreatedObjectUndo(canvasObject, "AGC 创建 Canvas"); +canvasObject.GetComponent().renderMode = UnityEngine.RenderMode.ScreenSpaceOverlay; +var panel = new UnityEngine.GameObject("Content", typeof(UnityEngine.RectTransform)); +UnityEditor.Undo.RegisterCreatedObjectUndo(panel, "AGC 创建布局"); +UnityEditor.Undo.SetTransformParent(panel.transform, canvasObject.transform, "AGC 设置 UI 父级"); +var rect = (UnityEngine.RectTransform)panel.transform; +rect.anchorMin = rect.anchorMax = rect.pivot = new UnityEngine.Vector2(0.5f, 0.5f); +rect.anchoredPosition = UnityEngine.Vector2.zero; +rect.sizeDelta = new UnityEngine.Vector2(320, 180); +UnityEditor.Undo.CollapseUndoOperations(group); +UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty(scene); +return new { canvasId = canvasObject.GetInstanceID(), panelId = panel.GetInstanceID(), width = rect.sizeDelta.x, height = rect.sizeDelta.y }; +``` + +## 保存与打开场景 + +确认目标路径及对象所属场景,多场景时用 `go.scene` 而非默认 active scene;已有场景通常沿用 `scene.path`。`MarkSceneDirty` 不是保存;独立资源用 `SetDirty` 和 `AssetDatabase.SaveAssetIfDirty` 保存。磁盘保存不由 Undo 回滚。 + + +```csharp +if (UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode) throw new System.Exception("请先停止播放"); +var scene = UnityEngine.SceneManagement.SceneManager.GetActiveScene(); +var path = "Assets/AGCGuide/Guide.unity"; +if (!UnityEditor.AssetDatabase.IsValidFolder("Assets/AGCGuide")) UnityEditor.AssetDatabase.CreateFolder("Assets", "AGCGuide"); +if (!UnityEditor.SceneManagement.EditorSceneManager.SaveScene(scene, path)) throw new System.Exception("场景保存失败"); +return new { path = scene.path, dirty = scene.isDirty }; +``` + +Single 会关闭当前场景;存在未保存修改时先停下处理,不默默丢弃。要保留场景则用 `OpenSceneMode.Additive`,并明确后续目标场景。 + + +```csharp +if (UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode) throw new System.Exception("请先停止播放"); +for (int i = 0; i < UnityEngine.SceneManagement.SceneManager.sceneCount; i++) + if (UnityEngine.SceneManagement.SceneManager.GetSceneAt(i).isDirty) throw new System.Exception("存在未保存场景,请先处理"); +var path = "Assets/AGCGuide/Guide.unity"; +if (UnityEditor.AssetDatabase.LoadAssetAtPath(path) == null) throw new System.Exception("场景文件不存在"); +var scene = UnityEditor.SceneManagement.EditorSceneManager.OpenScene(path, UnityEditor.SceneManagement.OpenSceneMode.Single); +return new { path = scene.path, loaded = scene.isLoaded, roots = scene.rootCount }; +``` + +## 播放、停止与编译诊断 + +播放/停止在下一次 Editor update 调度,`requested` 不代表已切换,稍后查询。播放和修改脚本可能触发编译/Domain Reload 使连接失效,稳定后重连核对,不重发操作。退出播放通常不保留运行期改动。 + + +```csharp +if (UnityEditor.EditorApplication.isCompiling || UnityEditor.EditorApplication.isUpdating) throw new System.Exception("编辑器正在编译或导入"); +UnityEditor.EditorApplication.delayCall += () => { UnityEditor.EditorApplication.isPlaying = true; }; +return new { requested = "play" }; +``` + + +```csharp +UnityEditor.EditorApplication.delayCall += () => { UnityEditor.EditorApplication.isPlaying = false; }; +return new { requested = "stop" }; +``` + +状态查询不能证明编译成功。代码编译错误由工具回执返回;项目编译详情查看 Console/Editor 日志,回执不含全量 Console。不要依赖未公开的 `LogEntries` API。 + + +```csharp +return new { compiling = UnityEditor.EditorApplication.isCompiling, + importing = UnityEditor.EditorApplication.isUpdating, + playing = UnityEditor.EditorApplication.isPlaying, + changingPlayMode = UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode, + version = UnityEngine.Application.unityVersion }; +``` + +## 验证范围 + +以上 13 个代码块已从本文提取,在 Windows x64 Unity 6000.3.7f1 Mono 的独立无包依赖项目中经 AGC Attach 实测,包含修改回读、Undo、Prefab override 保存重开及播放/停止。采用 batchmode/nographics;未验收 UI 视觉、第三方包或其他 Unity 版本。 diff --git a/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/manifest.json b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/manifest.json index 0b9f67ba9..7a2ef73d4 100644 --- a/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/manifest.json +++ b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/manifest.json @@ -1,7 +1,39 @@ { "schemaVersion": "agc-skill-pack.v1", - "version": "2026-08-26.25", + "version": "2026-08-26.27", "skills": [ + { + "name": "agc-unity-editor", + "purpose": "通过 AGC 内置 Unity 插件查询和修改场景、对象、资源与 UI,正确处理撤销、保存和回执", + "triggers": [ + "操作已打开的 Unity 工程", + "编写 Unity 编辑器执行代码" + ], + "requiredTools": [ + "agc_tools.agc_unity_execute" + ], + "files": [ + "SKILL.md", + "references/【操作指南】Unity编辑器常用操作-2026-09-20.md" + ], + "sha256": "9599fa1884db9c4f3eeab20d18871d4dafc845f5ecfe0f9ac9ba7417e65062fc" + }, + { + "name": "agc-godot-editor", + "purpose": "通过 AGC 内置 Godot 插件查询和修改场景、节点、资源与 UI,正确处理 owner、撤销和回执", + "triggers": [ + "操作已打开的 Godot 工程", + "编写 Godot 编辑器执行代码" + ], + "requiredTools": [ + "agc_tools.agc_godot_execute" + ], + "files": [ + "SKILL.md", + "references/【操作指南】Godot编辑器常用操作-2026-09-20.md" + ], + "sha256": "b5d76c8685c49e0cd1b0a243a2f46f00daa1a7c8a37c5137b6f31917df9a0af0" + }, { "name": "agc-game-production-workflow", "purpose": "把完整游戏从策划案按阶段推进到真实素材接入、构建、试玩和交付", diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs index b33ebcf21..175e8f454 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs @@ -6092,7 +6092,7 @@ case "$extra_roots" in *'"method":"skills/extraRoots/set"'*) ;; *) exit 87 ;; es printf '%s\n' '{"id":2,"result":{}}' IFS= read -r skills_list case "$skills_list" in *'"method":"skills/list"'*) ;; *) exit 88 ;; esac -printf '%s\n' '{"id":3,"result":{"data":[{"skills":[{"name":"agc-browser-playtest"},{"name":"agc-client-projection"},{"name":"agc-game-production-workflow"},{"name":"agc-project-structure"},{"name":"agc-web-game-development"},{"name":"taonier-art-assets"}],"errors":[]}]}}' +printf '%s\n' '{"id":3,"result":{"data":[{"skills":[{"name":"agc-browser-playtest"},{"name":"agc-client-projection"},{"name":"agc-game-production-workflow"},{"name":"agc-godot-editor"},{"name":"agc-unity-editor"},{"name":"agc-project-structure"},{"name":"agc-web-game-development"},{"name":"taonier-art-assets"}],"errors":[]}]}}' while IFS= read -r line; do :; done "#, ) @@ -6833,7 +6833,7 @@ while IFS= read -r line; do case "$line" in *'"method":"initialize"'*) printf '{"id":%s,"result":{"codexHome":"/tmp","platformFamily":"unix","platformOs":"linux","userAgent":"fixture"}}\n' "$id" ;; *'"method":"skills/extraRoots/set"'*) printf '{"id":%s,"result":{}}\n' "$id" ;; - *'"method":"skills/list"'*) printf '{"id":%s,"result":{"data":[{"skills":[{"name":"agc-browser-playtest"},{"name":"agc-client-projection"},{"name":"agc-game-production-workflow"},{"name":"agc-project-structure"},{"name":"agc-web-game-development"},{"name":"taonier-art-assets"}],"errors":[]}]}}\n' "$id" ;; + *'"method":"skills/list"'*) printf '{"id":%s,"result":{"data":[{"skills":[{"name":"agc-browser-playtest"},{"name":"agc-client-projection"},{"name":"agc-game-production-workflow"},{"name":"agc-godot-editor"},{"name":"agc-unity-editor"},{"name":"agc-project-structure"},{"name":"agc-web-game-development"},{"name":"taonier-art-assets"}],"errors":[]}]}}\n' "$id" ;; *'"method":"thread/start"'*) printf '{"id":%s,"result":{"thread":{"id":"thread-echo"}}}\n' "$id" ;; *'"method":"thread/inject_items"'*) printf '{"id":%s,"result":{}}\n' "$id" ;; *'"method":"turn/start"'*) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/mod.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/mod.rs index 79256bcd0..7b989047e 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/mod.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/mod.rs @@ -18,6 +18,7 @@ const DIRECT_TAONIER_IDENTITY_GUIDANCE: &str = "对外身份合同:你是“ const DIRECT_AGC_ENGINEERING_GUIDANCE: &str = "AGC 工程合同:当前 cwd 是用户选择的项目目录。DirectProject 的 Phaser 迁移固定使用 workspaceMode=DirectProject:识别已有 game/index.html 后,完整迁移状态、输入、敌人/守卫、波次、胜负、重开和画布绘制到 Phaser Scene/GameObject/update;写入 game/package.json、package-lock.json、vite.config.js(输出 game/dist)、game/game.js、game/style.css,先调用 project.bootstrap {cwd:game},再调用 project.verify {cwd:game,script:build,expectedCommand:从 game/package.json 原样读取},确认 game/dist/index.html 后才可 preview.start,并分别 preview.validate 桌面与移动视口。Phaser 画布居中责任唯一:使用 Phaser Scale.FIT 与 autoCenter CENTER_BOTH 时,canvas 的直接父容器用普通 block 按需要的宽高确定尺寸,不得在同一个 canvas 父容器上叠加 grid/flex 的 place-items、justify-content、align-items 居中或 margin:auto、translate 居中;若选择用 CSS 居中,则必须把 Phaser autoCenter 设为 NO_CENTER。外围布局仍可用 flex/grid,但同一个 canvas 的定位责任只能有一处。预览偏移先查项目自身的 CSS 与 Phaser 配置,不得用修改 AGC iframe 偏移来掩盖。改完布局后必须在桌面与移动视口以及 resize 后实测 canvas 相对游戏父容器的中心误差不超过 1 CSS px、无溢出,并按项目 scripts 构建 dist 后复验。不能把 Phaser 项目走 gameHtml 单文件协议。先读取当前 cwd 下适用的 AGENTS.md、README 或项目说明并识别实际引擎与工程结构。用户明确指定 Cocos、Unity、Godot 或其它编辑器/引擎,而当前目录不具备对应工程结构时,必须先说明不匹配并提出澄清;在澄清前不得把请求改写成 Phaser/Web 实现,也不得写文件、安装依赖、构建或试玩。仅当用户确认继续当前工程或提供了匹配的项目目录后才执行。识别为 Cocos Creator 项目时,优先使用 `agc_cocos_execute` 或 Cocos 插件的 `cocos.editor.execute` 在已打开的 Creator 编辑器中操作;不要创建 Phaser 文件,不要把 Cocos 请求改写成 Web 工程。新 Web 游戏使用 npm + Vite;二维游戏 Phaser 固定为 4.2.1,在 `game.js` 或模块中使用 `import Phaser from 'phaser'`;用户要做三维游戏时不受 Phaser 约束,由你自选三维技术栈(例如 Three.js / Babylon.js),不要用等轴伪 3D 冒充三维。两种情况都可以按需使用其它 npm 依赖,不得复制 Phaser bundle、使用 import map 或 CDN。简单修改只完成用户明确要求的范围;安装依赖、构建和试玩是后续操作,除非用户明确要求或它们是完成该项不可替代的最小验证,否则不得擅自扩展任务。源码使用 cwd 相对路径,例如 `index.html`、`style.css`、`game.js`、`package.json`、`package-lock.json`、`assets/hero.png`;依赖安装与构建使用项目自己的 npm scripts。原生文件工具、patch 和命令参数可以使用 DirectProject Codex app-server 声明的完整访问权限;优先使用 cwd 相对路径,例如 `index.html`、`style.css`、`game.js`、`assets/hero.png`,便于用户理解和审计,但不再把项目路径、`.agent/`、`.git/` 或其它目录做成 Codex 原生能力白名单。若 Codex 原生文件修改不可用,可以按需用客户端 `agc_write_file` 把文本写入项目相对路径;调用 `agc_write_file` 时,content 必须是目标文件的完整原始 UTF-8 正文,不得把 command.exec 的 Exit code、Wall time、Output 包装、终端日志或解释文字一起复制进 content,命令结果只能用于判断,不能当作文件正文。凭据、Token、Cookie、auth.json、`.env` 和 Runtime 私有控制面仍不得主动输出到对话、工具参数或日志。DirectProject 提供 Codex 原生文件、搜索、命令、图片查看、Skill、经客户端注入的 `agc_tools` MCP,以及客户端扩展列表中用户已启用的第三方 MCP。用户明确指定第三方 MCP Server 或工具时,先在当前可用工具中查找并直接调用;找不到时如实说明,不得伪造。你可以按需选择这些能力:`agc_write_file` 写入代码、配置、资源依赖清单或说明文件;`agc_generate_image` 生成普通图片、角色图、视觉规范图(icon-spec)、UI 设计图;`agc_edit_image` 修改已登记图片;`taonier_prepare_game_art` 准备完整游戏美术包及可用的 canonical 切片;`agc_list_registered_assets`、`agc_list_project_files`、`agc_list_account_assets`、`agc_import_account_assets` 用于发现和接入资源依赖;`agc_create_or_derive_resource` 用于视频、角色动画、音效或背景音乐;`agc_browser_playtest` 用于需要时的本地试玩观察;Skill references 按需使用相对路径直接读取。完整新游戏或根据策划案实现时必须执行 agc-game-production-workflow:按“策划定界 → 项目/资源盘点 → 美术生成或复用 → 游戏实现 → 构建验证 → 桌面/移动试玩 → 交付报告”顺序推进,每阶段完成后再进入下一阶段,不得在写完代码或生成图片后提前结束。新游戏 brief 中需要视觉素材时必须执行 taonier-art-assets:先检查已登记资源;缺少或不适用时调用 agc_tools 生图/编辑工具;读取返回的相对路径和登记身份,生成结果必须接入游戏源码并验证实际显示。只有明确不需要视觉素材的游戏才可跳过。资源生成、处理和接入属于同一游戏交付链路;不要用 emoji、CSS 形状或临时占位图替代 brief 中要求的真实素材,也不要在素材未接入时报告游戏完成。试玩仍按改动范围执行,AGC 不会据此替你拆任务、编排 DAG、做强验收或阻止继续执行;不要等待 Supervisor、harness 或宿主规划器。不要读取或输出凭据、Token、Cookie、auth.json、.env 或宿主私密路径;项目锁、付费提交、幂等键、下载校验和客户端投影由客户端处理。游戏文件真实变化后客户端可登记资源和版本,Codex 不直接保存或伪造项目版本。"; const DIRECT_UNITY_BUILTIN_PLUGIN_GUIDANCE: &str = "Unity 编辑器能力来自客户端内置插件 agc-unity-editor,工具为 agc_unity_execute(Runtime 为 unity.editor.execute)。当前工程是 Unity 时使用该工具执行 C#,先读取实际场景与对象再修改;不安装 UPM 或项目内 MCP,不改写为 Phaser。只支持 Windows x64 Mono Editor;缺少工具时报告客户端内置插件不可用。仅提交 code;主线程同步代码无法硬中止。needs-reconciliation 表示结果待人工核对,禁止自动重发、重启插件或切换项目以绕过阻断。只有真实 completed 回执才可报告成功。"; const DIRECT_GODOT_BUILTIN_PLUGIN_GUIDANCE: &str = "Godot 编辑器能力来自客户端内置插件 agc-godot-editor,工具为 agc_godot_execute(Runtime 为 godot.editor.execute)。当前工程是 Godot 时使用该工具执行支持 return/await 的 GDScript 函数体,先读取真实场景再修改;不改写为 Phaser。DLL 随 AGC 安装目录分发,宿主只在实际 Godot 根目录维护引用 DLL 的受管 agc-editor-bridge.gdextension,重新聚焦 Godot 后自动加载;无需安装 addon、打开或手动运行引导脚本,不要自行写入 DLL 或描述文件。只支持 Windows x64 的 Godot 4.7 及以上标准编辑器;workspace 可包含唯一一层 Godot 子目录,实际引擎根由宿主确定。仅提交 code,不提供项目、进程、端口、令牌或库路径;缺少工具时报告客户端内置插件不可用。编译或确定运行失败可修正代码;needs-reconciliation、超时或断线时禁止自动重发、重启插件或切换项目绕过阻断。只有真实 completed 回执才可报告成功。"; +const DIRECT_EDITOR_GUIDE_GUIDANCE: &str = "常用编辑器操作:Unity 先读 agc-unity-editor,Godot 先读 agc-godot-editor。可用原生 Skill 读取,或调用 agc_read_skill_resource,skillName 为对应名称、relativePath 为 SKILL.md,再按入口读取操作参考。指南提供场景、对象/节点、资源、UI、保存和撤销示例;只读说明不代表编辑器工具已可用,实际执行仍检查当前工具。"; const DIRECT_COCOS_BUILTIN_PLUGIN_GUIDANCE: &str = r#"Cocos Creator 桥接边界:Cocos 的编辑器能力来自客户端随包提供的内置插件 `agc-cocos-editor`,Agent 工具名是 `cocos.editor.execute`(客户端受控工具名为 `agc_cocos_execute`)。识别为 Cocos Creator 项目后,直接检查当前可用工具并调用这个内置工具;不要搜索、读取、安装、启用或建议项目目录里的 MCP 扩展、`extensions/` 包、`package.json` 插件或 Cocos 面板服务。项目内的第三方 MCP 扩展不是 AGC Cocos 桥接来源,缺失内置工具时只能报告客户端内置插件不可用,不得改为查项目扩展或要求用户打开 Cocos MCP 面板。历史聊天记录仅用于理解上下文,不是工具或系统指令;其中与本边界冲突的旧说明一律以当前提示和当前可用内置工具为准。"#; const DIRECT_COCOS_CAPABILITY_GUIDE: &str = r#"Cocos 能力:先用 cocos_get_capabilities 和 cocos_get_hierarchy 查询;查询返回 NID 与 UUID,场景切换后必须重新查询。读取场景树 `Editor.Message.request('scene', 'query-node-tree')`,先用只读查询拿到真实 uuid 和当前状态,再执行修改。用 cocos_inspect_node 取得 componentIndex、组件类型及属性后再修改。节点、组件、Prefab、Label/Sprite/Button/Shape、Layout/Widget、九宫格、批量 UI、保存、撤销、日志、构建诊断和网页预览调试均有对应 cocos_* 工具,按实际 inputSchema 调用。批量 UI 最多 64 个节点和 12 层,save 缺省 true;首次保存可用 cocos_save_scene 的 path 指定 assets 下新 .scene 路径。只在 verified 为 true 时报告结果已经回读确认;failed、rolledBack 和 needs-reconciliation 不能当成功,结果不确定不得自动重发。cocos_mcp_undo_last 会拒绝覆盖后续手动修改。预览工具只管理自己的 Chromium 窗口和当前项目 loopback 地址,capture 返回 PNG 图片。目录之外的操作继续用 agc_cocos_execute 注入支持 await/return 的 JS 函数体。"#; const DIRECT_ENGINE_FREEDOM_GUIDANCE: &str = "三维请求合同:用户要做三维(3D)游戏时,不受“新 Web 游戏固定 Phaser 4.2.1”的约束,由你自行选择三维技术栈(例如 Three.js、Babylon.js 等 npm 三维运行时,或当前工程自带的引擎),可以按需新增 npm 依赖,并在回复里说明选型。不要用等轴伪 3D 或二维图集冒充三维交付;做不到就用回复说明限制与原因。用户明确指定 Cocos、Unity、Godot 等编辑器而当前目录不具备对应工程结构时,仍按既有规则先说明不匹配再动作。"; @@ -4597,6 +4598,7 @@ fn build_direct_codex_system_prompt_with_search( DIRECT_COCOS_BUILTIN_PLUGIN_GUIDANCE.to_string(), DIRECT_UNITY_BUILTIN_PLUGIN_GUIDANCE.to_string(), DIRECT_GODOT_BUILTIN_PLUGIN_GUIDANCE.to_string(), + DIRECT_EDITOR_GUIDE_GUIDANCE.to_string(), DIRECT_COCOS_CAPABILITY_GUIDE.to_string(), "工程执行要求:优先复用现有结构;按需读取真实文件,不依赖客户端预注入源码快照;修改后运行与改动相关的本地验证。工具返回 isError、构建失败、验证失败或试玩异常时,把错误当作调试上下文,读取当前项目、修复真实文件并重跑失败步骤,不要直接结束或伪造成功;鉴权、权限、余额、身份、历史、传输断开和操作状态不确定等安全错误才停止。不要创建 Supervisor、专业 Agent 或平行项目。".to_string(), format!("提示词与技能:{skill_index}"), @@ -6210,6 +6212,26 @@ mod tests { assert!(!prompt.contains("secret")); } + #[test] + fn editor_guide_routes_survive_prompt_budget_without_loading_examples() { + for search in [false, true] { + let prompt = + build_direct_codex_system_prompt_with_search(Path::new("."), search).unwrap(); + assert!(prompt.chars().count() < MAX_DIRECT_SYSTEM_PROMPT_CHARS); + assert!(prompt.contains(DIRECT_EDITOR_GUIDE_GUIDANCE)); + assert!(prompt.contains(DIRECT_UNITY_BUILTIN_PLUGIN_GUIDANCE)); + assert!(prompt.contains(DIRECT_GODOT_BUILTIN_PLUGIN_GUIDANCE)); + for (skill, engine) in [("agc-unity-editor", "Unity"), ("agc-godot-editor", "Godot")] { + assert!(prompt.contains(skill)); + let reference = read_agc_skill_resource(&format!( + "{skill}/references/【操作指南】{engine}编辑器常用操作-2026-09-20.md" + )) + .unwrap(); + assert!(!prompt.contains(reference.trim())); + } + } + } + #[test] fn system_prompt_does_not_preload_current_game_files() { let root = tempfile::tempdir().expect("temp dir"); diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tools_mcp.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tools_mcp.rs index dd05660f4..cbc5e90a2 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tools_mcp.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tools_mcp.rs @@ -3009,6 +3009,22 @@ mod tests { assert_eq!(denied_windows_absolute["isError"], true); } + #[test] + fn editor_guides_are_available_through_the_existing_skill_resource_tool() { + for (skill, engine) in [("agc-unity-editor", "Unity"), ("agc-godot-editor", "Godot")] { + let relative = format!("references/【操作指南】{engine}编辑器常用操作-2026-09-20.md"); + let expected = read_agc_skill_resource(&format!("{skill}/{relative}")).unwrap(); + let response = + call_agc_read_skill_resource(&json!({"skillName":skill,"relativePath":relative})); + assert_eq!(response["isError"], false); + assert_eq!(response["content"][0]["text"], expected); + let denied = call_agc_read_skill_resource( + &json!({"skillName":skill,"relativePath":"references/not-in-manifest.md"}), + ); + assert_eq!(denied["isError"], true); + } + } + #[test] fn external_codex_response_redacts_sensitive_lines_and_keeps_safe_text() { let response = redact_external_mcp_response( diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/skill_pack.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/skill_pack.rs index 5c5f9e602..e0d474324 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/skill_pack.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/skill_pack.rs @@ -6,16 +6,34 @@ use std::path::{Component, Path}; const AGC_SKILL_PACK_MANIFEST: &[u8] = include_bytes!("../../resources/agc-skills/manifest.json"); const AGC_SKILL_PACK_SCHEMA_VERSION: &str = "agc-skill-pack.v1"; -pub(crate) const AGC_SKILL_PACK_EXPECTED_NAMES: [&str; 6] = [ +pub(crate) const AGC_SKILL_PACK_EXPECTED_NAMES: [&str; 8] = [ "agc-browser-playtest", "agc-client-projection", "agc-game-production-workflow", + "agc-godot-editor", "agc-project-structure", + "agc-unity-editor", "agc-web-game-development", "taonier-art-assets", ]; -const AGC_SKILL_PACK_FILES: [(&str, &[u8]); 18] = [ +const AGC_SKILL_PACK_FILES: [(&str, &[u8]); 22] = [ + ( + "agc-unity-editor/SKILL.md", + include_bytes!("../../resources/agc-skills/agc-unity-editor/SKILL.md"), + ), + ( + "agc-unity-editor/references/【操作指南】Unity编辑器常用操作-2026-09-20.md", + include_bytes!("../../resources/agc-skills/agc-unity-editor/references/【操作指南】Unity编辑器常用操作-2026-09-20.md"), + ), + ( + "agc-godot-editor/SKILL.md", + include_bytes!("../../resources/agc-skills/agc-godot-editor/SKILL.md"), + ), + ( + "agc-godot-editor/references/【操作指南】Godot编辑器常用操作-2026-09-20.md", + include_bytes!("../../resources/agc-skills/agc-godot-editor/references/【操作指南】Godot编辑器常用操作-2026-09-20.md"), + ), ( "agc-browser-playtest/SKILL.md", include_bytes!("../../resources/agc-skills/agc-browser-playtest/SKILL.md"), @@ -306,10 +324,10 @@ mod tests { use super::*; #[test] - fn bundled_skill_pack_is_exactly_the_six_reviewed_skills() { + fn bundled_skill_pack_matches_the_reviewed_allowlist() { let manifest = validated_skill_pack_manifest().expect("validated manifest"); assert_eq!(manifest.schema_version, "agc-skill-pack.v1"); - assert_eq!(manifest.skills.len(), 6); + assert_eq!(manifest.skills.len(), AGC_SKILL_PACK_EXPECTED_NAMES.len()); assert!(manifest.skills.iter().all(|entry| entry.sha256.len() == 64)); let serialized = serde_json::to_string( &manifest @@ -391,4 +409,29 @@ mod tests { assert!(!is_safe_skill_relative_path(r"\\server\share\SKILL.md")); assert!(!is_safe_skill_relative_path(r"references\contract.md")); } + + #[test] + fn editor_guides_are_complete_in_installed_and_readable_skill_resources() { + let home = tempfile::tempdir().unwrap(); + install_agc_skill_pack(home.path()).unwrap(); + for (skill, engine) in [("agc-unity-editor", "Unity"), ("agc-godot-editor", "Godot")] { + let resource = + format!("{skill}/references/【操作指南】{engine}编辑器常用操作-2026-09-20.md"); + let guide = read_agc_skill_resource(&resource).unwrap(); + assert!(!guide.is_empty()); + assert!( + guide.len() <= 14 * 1024, + "{engine} reference exceeds UTF-8 budget" + ); + let installed = + std::fs::read_to_string(home.path().join(".agents/skills").join(&resource)) + .unwrap(); + assert_eq!(installed, guide); + let entry = read_agc_skill_resource(&format!("{skill}/SKILL.md")).unwrap(); + assert!(entry.contains(resource.split_once('/').unwrap().1)); + assert!( + read_agc_skill_resource(&format!("{skill}/references/../../auth.json")).is_err() + ); + } + } } diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent_native_tools.rs b/apps/ai-game-creator-shell/src-tauri/src/agent_native_tools.rs index 9d25349d5..36e30fbe1 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent_native_tools.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent_native_tools.rs @@ -294,10 +294,22 @@ pub(crate) fn build_agent_runtime_native_function_tools_for_agent( if !names.insert(name.clone()) { return Err(format!("Runtime 原生函数名重复:{name}")); } + let description = if let Some(reference) = editor_operation_reference(definition.id()) { + let reference = reference.replace("\r\n", "\n"); + if reference.len() > 14 * 1024 { + return Err(format!( + "Runtime 编辑器操作参考超过随包预算:{}", + definition.id() + )); + } + reference + } else { + definition.description().to_owned() + }; functions.push( LlmFunctionTool::new( name, - definition.description(), + description, action_function_parameters(definition.input_schema().clone()), ) .with_strict(true), @@ -1004,6 +1016,14 @@ fn string_array_schema(max_items: usize) -> Value { }) } +fn editor_operation_reference(tool: &str) -> Option<&'static str> { + match tool { + "unity.editor.execute" => Some(include_str!("../resources/agc-skills/agc-unity-editor/references/【操作指南】Unity编辑器常用操作-2026-09-20.md")), + "godot.editor.execute" => Some(include_str!("../resources/agc-skills/agc-godot-editor/references/【操作指南】Godot编辑器常用操作-2026-09-20.md")), + _ => None, + } +} + fn runtime_tool_description(tool: &str) -> &'static str { match tool { "user.input_request" => "向用户提出一至三个结构化问题,并暂停当前 run 等待回答。", @@ -1056,8 +1076,8 @@ fn runtime_tool_description(tool: &str) -> &'static str { "cocos.editor.execute" => { "在当前项目对应的已打开 Cocos Creator 编辑器中执行一段有界代码;Runtime 自动绑定唯一匹配的 Creator 主进程,代码与结果都通过注入 payload 的本机 bridge 返回。" } - "unity.editor.execute" => "在当前 Unity 项目已打开的 Windows x64 Mono Editor 中执行 C#。仅提交 code,宿主绑定项目身份;结果待核对时禁止自动重发。", - "godot.editor.execute" => "在当前 Godot 项目已打开的 Windows x64 标准编辑器中执行支持 return/await 的 GDScript 函数体。DLL 原件保留在安装目录,宿主在私有缓存准备每实例加载副本,受管描述文件引用该副本,重新聚焦后自动加载;仅提交 code,结果待核对时禁止自动重发。", + "unity.editor.execute" => "在当前 Unity 项目已打开的 Windows x64 Mono Editor 中执行 C#。执行载荷仅有 code,宿主绑定项目身份;结果待核对时禁止自动重发。", + "godot.editor.execute" => "在当前 Godot 项目已打开的 Windows x64 标准编辑器中执行支持 return/await 的 GDScript 函数体。执行载荷仅有 code,重新聚焦可触发首次加载;结果待核对时禁止自动重发。", "blackboard.write" => "向项目级共享黑板追加稳定结论。", "agent.message" => "向一个目标 Agent 写入定向上下文消息。", "agent.delegate" => { @@ -1927,6 +1947,63 @@ mod tests { assert_eq!(schema["properties"].as_object().unwrap().len(), 1); } + #[test] + fn editor_guides_reach_native_tool_definitions_without_truncation() { + let _guard = crate::builtin_plugins::test_lock(); + let config = tempfile::tempdir().unwrap(); + crate::builtin_plugins::initialize(config.path()).unwrap(); + for id in [ + crate::builtin_plugins::AGC_UNITY_EDITOR_PLUGIN_ID, + crate::builtin_plugins::AGC_GODOT_EDITOR_PLUGIN_ID, + ] { + crate::builtin_plugins::set_enabled(id, true).unwrap(); + } + let functions = build_agent_runtime_native_function_tools().unwrap(); + for (engine, skill, tool) in [ + ("Unity", "agc-unity-editor", "unity.editor.execute"), + ("Godot", "agc-godot-editor", "godot.editor.execute"), + ] { + let reference = crate::agent::read_agc_skill_resource(&format!( + "{skill}/references/【操作指南】{engine}编辑器常用操作-2026-09-20.md" + )) + .unwrap(); + assert_eq!( + editor_operation_reference(tool) + .unwrap() + .replace("\r\n", "\n"), + reference + ); + let emitted = functions + .iter() + .find(|function| function.name == native_runtime_function_name_for_tool(tool)); + let expected = match engine { + "Unity" => cfg!(all( + windows, + target_arch = "x86_64", + feature = "unity-editor-execute" + )), + "Godot" => cfg!(all( + windows, + target_arch = "x86_64", + feature = "godot-editor-execute" + )), + _ => false, + }; + assert_eq!(emitted.is_some(), expected); + if let Some(function) = emitted { + let wire = serde_json::to_value(function).unwrap(); + assert_eq!( + wire["description"].as_str().unwrap().replace("\r\n", "\n"), + reference + ); + } else { + assert!(!functions + .iter() + .any(|function| function.description.contains(&reference))); + } + } + } + #[test] fn strict_native_function_schemas_match_openai_subset() { let functions = diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index e6dd1189f..6c9caa3ea 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -1,5 +1,9 @@ # 决策记录 +## Unity 与 Godot 常用操作指导 + +两种编辑器的操作指导复用客户端审核 Skill pack:DirectProject 通过原生 Skill 或既有审核资源读取入口按需取得,Agent Runtime 的对应执行工具说明嵌入同源参考。指南不改变插件可用性、执行授权或 Runner 回执;只读说明不能证明编辑器已连接。常用示例与执行失败/部分修改、保存、撤销边界在同一参考中维护,避免提示词和文档各存一份代码。 + ## 2026-09-20 Godot 编辑器执行接入 Godot 编辑器操控复用既有 AGC 插件宿主、EditorAdapter、Runner 和不确定执行回执合同,编辑器实现留在 `plugins/agc-godot-editor`。用户选择 DLL 原件随 AGC 安装资源分发,并确认按编辑器实例在 AGC 私有缓存准备临时加载副本,以满足 Godot Windows 加载器的同目录 `~DLL` 写入要求;项目内不复制 DLL,只用受管 `.gdextension` 引导。Godot 自动 UID 伴生文件必须记录归属并在确认卸载后按内容匹配清理。工作区根不迁移到 Godot 子目录,原始项目配置与场景只通过明确编辑操作修改。完整合同及验证范围见 [Godot 编辑器插件接入](<../../technical/【技术方案】AGC Godot编辑器插件接入-2026-09-20.md>)。 diff --git a/docs/technical/【技术方案】AGC Godot编辑器插件接入-2026-09-20.md b/docs/technical/【技术方案】AGC Godot编辑器插件接入-2026-09-20.md index 31022fd09..c2becfe13 100644 --- a/docs/technical/【技术方案】AGC Godot编辑器插件接入-2026-09-20.md +++ b/docs/technical/【技术方案】AGC Godot编辑器插件接入-2026-09-20.md @@ -7,6 +7,12 @@ ## 目标与边界 +常用操作指导随客户端审核 Skill pack 提供,入口为 `agc-godot-editor`。DirectProject 按需读取 [Skill 入口](../../apps/ai-game-creator-shell/src-tauri/resources/agc-skills/agc-godot-editor/SKILL.md) 和其常用操作参考;Runtime 的 `godot.editor.execute` 工具说明包含同一参考正文。指导覆盖场景/节点、owner、PackedScene、资源、UI、保存与撤销,不新增专用操作工具,不改变执行授权。 + +原文示例在 Godot 4.7.2 标准版 headless 工程验证了节点回读、局部撤销、PackedScene 存读、居中 UI 结构、无缩略图保存重开及只读旧文件写失败时保留内存修改。`save_scene_as` 不返回错误码,指南在重开前核验磁盘包含本次预期变更,不能以旧文件可加载作为保存成功证据。复验时设置 `AGC_GODOT_TEST_EXECUTABLE`,运行 `node --test plugins/agc-godot-editor/native/gdextension/tests/guide-examples.test.mjs`。正式 Ctrl+Z 历史、运行中停止、GUI 缩略图保存及 UI 视觉仍未由该测试验收。 + +独立图形环境补验已确认该示例在 800×600、480×800、1280×720 三种实际渲染尺寸下中文和按钮正常显示、容器居中且无裁切;此结果只覆盖示例布局,不代表按钮已接入游戏逻辑或其他 UI 已验收。 + 将 Godot 编辑器操控接入现有 AGC PluginHost、EditorAdapter、Runner、内置插件开关、权限审计和 Agent 工具链。Windows x64 的 Godot 4.7 及以上标准编辑器是首个实现目标,实机验收使用 4.7.2;其他平台和 .NET 编辑器不得从该结果推断支持。 初版工程路径支持 Windows 本地盘符目录;UNC/网络共享路径在准备描述文件前明确拒绝。链接/reparse point 继续按同一文件边界失败关闭。 diff --git a/docs/technical/【技术方案】AGC Unity编辑器插件接入-2026-09-18.md b/docs/technical/【技术方案】AGC Unity编辑器插件接入-2026-09-18.md index 3cb49fcbe..67cdb2938 100644 --- a/docs/technical/【技术方案】AGC Unity编辑器插件接入-2026-09-18.md +++ b/docs/technical/【技术方案】AGC Unity编辑器插件接入-2026-09-18.md @@ -3,10 +3,14 @@ > 文档状态:`current` > 规范关系:承接 AGC 通用插件宿主与编辑器适配主规范 -更新时间:`2026-09-18` +更新时间:`2026-09-20` ## 目标与非目标 +常用操作指导随客户端审核 Skill pack 提供,入口为 `agc-unity-editor`。DirectProject 按需读取 [Skill 入口](../../apps/ai-game-creator-shell/src-tauri/resources/agc-skills/agc-unity-editor/SKILL.md) 和其常用操作参考;Runtime 的 `unity.editor.execute` 工具说明包含同一参考正文。指导覆盖查询、对象/组件、Prefab、资源、UI、保存与撤销,不新增专用操作工具,不改变执行授权。 + +指南示例在独立 Unity 6000.3.7f1 Mono 工程经真实 Attach 验证,覆盖查询、创建/修改、Undo、Prefab override 保存重开、Canvas 及播放切换;不据此推断 UI 视觉、第三方包或其他版本已验收。复验入口为 `native/unity-editor-bridge/tests/guide_examples.rs`:按文件头显式设置 fixture 的 helper、项目与 PID 环境变量后,运行 `cargo test --locked --manifest-path plugins/agc-unity-editor/native/unity-editor-bridge/Cargo.toml --test guide_examples -- --ignored --nocapture`;它直接提取随包指南代码,而非维护示例副本。 + 将 DotCraft.Unity 0.4.3 对应的 Attach 执行核心接入 AGC 现有插件系统,使当前 Unity 项目能够探测编辑器、建立连接、执行 C# 并获得真实结果。复用既有扩展列表、内置插件开关、权限、审计、EditorAdapter 和 Agent 工具通路。 首期只支持 Windows x64 的 Unity Mono Editor。连接不修改项目文件、不安装 UPM 包、不启动或关闭用户编辑器。不引入 DotCraft.Harness、另一套 Agent Runtime、MCP 服务或聊天界面;截图、热重载专用工具、macOS、Linux 和 Unity CoreCLR 不属于本次交付。 diff --git a/docs/technical/【技术方案】AGC通用插件宿主与编辑器适配-2026-09-09.md b/docs/technical/【技术方案】AGC通用插件宿主与编辑器适配-2026-09-09.md index 0f5cd9bd8..ade9f8249 100644 --- a/docs/technical/【技术方案】AGC通用插件宿主与编辑器适配-2026-09-09.md +++ b/docs/technical/【技术方案】AGC通用插件宿主与编辑器适配-2026-09-09.md @@ -4,7 +4,7 @@ > 规范关系:AGC 插件与编辑器适配主规范 > 验收范围:插件 manifest、宿主生命周期、RPC、Capability/权限审计、UI 挂载和编辑器适配器边界 -更新时间:`2026-09-18` +更新时间:`2026-09-20` ## 目标与边界 @@ -126,8 +126,27 @@ Windows x64 的 Attach helper 来源、构建工具链和执行回执合同见 Godot 使用同一 Runner 执行与回执确认层,按引擎分别保存 pending/uncertain 状态,不能相互确认或清除。`godot-editor` 的受控连接允许按用户已选方案维护项目内 `.gdextension` 引用及 Godot 自动生成的 UID;DLL 随安装资源分发,探测仍只读,原始项目配置和场景不改。具体文件归属、GDScript 错误/async、升级卸载与分发合同见 [Godot 插件接入](<./【技术方案】AGC Godot编辑器插件接入-2026-09-20.md>)。 +## 编辑器常用操作指导 + +Unity 与 Godot 的常用操作指导由客户端既有审核 Skill pack 随包提供;每个引擎有独立入口和常用操作参考,不新增执行工具或任意文件读取入口。DirectProject 在隔离 Skill 目录发现指导,也可通过已有 `agc_read_skill_resource` 按审核名称和相对路径读取。Agent Runtime 的对应执行工具说明包含同一份常用操作参考,避免只覆盖 Codex 原生 Skill 路径。正文只有一个源码来源,清单指纹与安装投影必须一致。 + +指导覆盖场景/层级查询、对象或节点的创建/修改/删除、组件或属性、Prefab/PackedScene、资源引用、基础 UI、打开/保存场景、运行/停止及诊断。示例是提交给现有 execute 的代码正文,说明前置状态、预期回读和保存/撤销语义,不写宿主路径、PID、令牌或桥接安装动作。读说明不连接编辑器、不授权修改;执行仍受原插件开关、平台、项目和权限门禁控制。 + +每个引擎的操作参考不超过 14 KiB UTF-8,并独立包含执行参数和失败边界;Direct 常驻提示只给读取路由,16K 字符预算内必须保留两个引擎的指南入口。Runtime 的最终工具定义须完整包含对应参考,不能因截断丢失末尾内容,也不能在该执行工具不可用时额外注入正文。 + +通用 CapabilityRegistry 保留原有短描述和 4000 字符约束;完整参考只在已注册能力转换为 Provider 函数工具时附加,按 UTF-8/LF 规范化并校验 14 KiB 上限。不扩大 core 的任务、能力或摘要长度合同。 + +执行载荷仅有 `code`;Direct 工具的顶层参数为 `{code}`,Runtime 原生函数沿用 `{reason,input:{code}}` 外层,指南必须按实际工具 schema 区分这两种调用格式。 + +确定失败也可能已经产生部分修改;未知结果继续禁止自动重放。Unity 使用实际场景与对象身份,区分 Undo、Prefab override、保存和 Domain Reload。Godot 使用真实编辑场景根、为需保存的新节点设置 owner,区分独立 UndoRedo 回滚与编辑器历史,避免承诺未验证的 Ctrl+Z。主线程同步死循环不可硬中止。 + +验收要求:两个入口均能取得审核正文,源码与安装后的字节一致;非法路径及未登记资源继续拒绝;系统提示能够发现指南但不塞入全部示例;从指南原文提取代码做真实临时工程验证,至少覆盖查询、修改与回读、局部撤销、场景持久化、资源实例化和 UI。未实测操作和平台须在交付记录中明确,不把 API 示例当作已有独立工具。 + +当前证据覆盖本地指南读取/安装、最终工具描述、真实编辑器执行示例及 Godot 示例图形渲染。Unity GUI 视觉和真实 Provider 读取指南后调用编辑器的端到端链路尚未验收,不能由定向测试或前置状态检查推断通过。 + ## Tauri 命令 + `list_agc_extensions` 返回统一的 Plugin/Skill/MCP catalog;`list_agc_plugins`、`refresh_agc_plugins`、`start_agc_plugin`、`stop_agc_plugin`、`reload_agc_plugin`、`call_agc_plugin` 和 `read_agc_plugin_panel` 提供 Runtime Plugin 管理入口;`set_agc_plugin_project_path` 设置当前项目的受控上下文。编辑器适配器通过宿主 registry 和 Plugin RPC 使用,不增加编辑器专属 Tauri 命令。 编辑器操作统一走 `host.rpc`:插件用 `extensions.world.genarrative.agc.adapter` 或显式 `adapter` 参数选择适配器,宿主校验 `editor.rpc` 权限后调用 `EditorAdapter::rpc`。项目上下文通过 `host.events.subscribe` 的响应和 `project.changed` 事件 payload 下发,插件不需要自己扫描目录。 diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index 8f08efafe..7261b3ee3 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -1377,7 +1377,7 @@ game-project/ - 普通项目对话只由一个 project-bound Codex app-server thread 执行。客户端系统提示词只放最小工程合同、当前游戏源码有界快照、项目 prompts 和审核 Skill 索引;不再批量读取项目 `.codex/.agents` Skill 正文,也不恢复 Supervisor、专业 Agent 或 harness。 - 首页恢复“做游戏 / 做素材 / 做方案”三个创作类型,默认“做游戏”。该选择与设置页的 Agent Runtime 模式无关;每次首页提交仍只自动创建一个新项目并进入项目工作台。用户正文原样进入项目对话,`game|art|doc` 仅作为受限结构化首轮上下文传给同一 Codex thread,不拼接“初始意图”文案、不产生首页对话、不切换 Provider 或恢复旧 Runtime 编排。 -- `agc-skill-pack.v1` 只包含项目结构、陶泥儿美术、Web 游戏实现、真实浏览器试玩、客户端资源投影五项 Skill。清单记录用途、触发条件、所需工具、版本和内容 SHA-256;审核文本按 UTF-8 读取并将 CRLF 规范为 LF 后计算指纹和安装,避免混合换行造成 Windows / Linux 构建结果漂移,语义内容变化时必须同步重算对应清单指纹并提升版本。同步统一运行 `npm run agc:skill-pack:sync`,只读校验由 AGC `typecheck` 和 release build 自动执行,发现漂移时直接列出 Skill 与实际摘要,不让失配内容进入构建产物。客户端把审核文件安装到隔离目录后通过 app-server `skills/extraRoots/set + skills/list` 注册并复核,完整正文由 Codex 原生 Skill 机制按意图加载,一层引用只能经 `agc_read_skill_resource` 读取清单内 Markdown。引用路径按平台无关规则拒绝反斜杠、盘符、UNC、绝对路径和 `..`,不能依赖当前宿主的 `std::path` 语义判断其它平台路径。 +- `agc-skill-pack.v1` 包含完整游戏交付流程、项目结构、陶泥儿美术、Web 游戏实现、真实浏览器试玩、客户端资源投影,以及 Unity/Godot 编辑器常用操作八项审核 Skill。清单记录用途、触发条件、所需工具、版本和内容 SHA-256;审核文本按 UTF-8 读取并将 CRLF 规范为 LF 后计算指纹和安装,避免混合换行造成 Windows / Linux 构建结果漂移,语义内容变化时必须同步重算对应清单指纹并提升版本。同步统一运行 `npm run agc:skill-pack:sync`,只读校验由 AGC `typecheck` 和 release build 自动执行,发现漂移时直接列出 Skill 与实际摘要,不让失配内容进入构建产物。客户端把审核文件安装到隔离目录后通过 app-server `skills/extraRoots/set + skills/list` 注册并复核,完整正文由 Codex 原生 Skill 机制按意图加载,一层引用只能经 `agc_read_skill_resource` 读取清单内 Markdown。引用路径按平台无关规则拒绝反斜杠、盘符、UNC、绝对路径和 `..`,不能依赖当前宿主的 `std::path` 语义判断其它平台路径。 - DirectProject 始终连接客户端内置的 `agc_tools` STDIO MCP;2026-08-31 起还会在启动时接入客户端扩展仓库中用户已启用的独立第三方 STDIO/HTTP MCP 配置,但不读取用户全局 Codex MCP、不开启完整 Plugin Runtime。内置工具固定为审核引用读取、标准陶泥儿美术准备、已登记资源有界查询、视频 / 角色动画 / 音效 / BGM 的 create-or-derive 语义生成、已登记图片去背景、desktop/mobile 浏览器试玩和受控 `agc_web_search`。内置 MCP 进程只做协议;真实浏览器、付费 External v1 调用与受控搜索通过随机 loopback 地址回到客户端主进程,因此不复制 GUI 登录态、开发者 Key、项目路径、revision、operation 或幂等键到模型上下文。内置与用户启用的第三方 MCP 工具都沿用 DirectProject 自动批准方式,但付费资源工具仍由客户端绑定稳定回合身份、限制单回合请求数、串行执行并优先恢复匹配账本;通用 shell、Codex 原生 webSearch、任意原生命令网络、多 Agent 和完整插件能力继续关闭。`llm.webSearchEnabled` 只控制 DirectProject 的 AGC 受控搜索工具暴露与执行,Codex 原生 `web_search` 始终保持 disabled;Provider、ToolHost、DirectHome 不纳入本次联网主链路。 - 陶泥儿生成继续复用持久幂等账本、operation 恢复、来源/下载/PNG 解码和 manifest 登记;普通客户端优先使用当前 AGC 登录会话及账号路由,只有受控的 ExternalDeveloper 发布模式才在客户端内部使用按服务器 origin 隔离的私有 Key。用户和模型都不需要提供或配置 API Key;凭据失效、来源不明或结果未知时失败关闭,不能自动换 Key 或重新扣费。 - 自定义 LLM API Key 路由只在 DirectHome/DirectProject 经 loopback `/responses` 流式代理转发。代理不注入 Key,只要求请求自带 Bearer,并剥离开发网关错误携带的 `X-Codex-*` ChatGPT 账户额度头,防止隔离 app-server 把 API Provider 误判为余额 0;旧 ToolHost 保持原 Provider 行为。 diff --git a/plugins/agc-godot-editor/native/gdextension/tests/guide-examples.test.mjs b/plugins/agc-godot-editor/native/gdextension/tests/guide-examples.test.mjs new file mode 100644 index 000000000..f405c07a8 --- /dev/null +++ b/plugins/agc-godot-editor/native/gdextension/tests/guide-examples.test.mjs @@ -0,0 +1,380 @@ +import assert from 'node:assert/strict'; +import { spawn } from 'node:child_process'; +import { once } from 'node:events'; +import fs from 'node:fs'; +import net from 'node:net'; +import path from 'node:path'; +import test from 'node:test'; +import { fileURLToPath } from 'node:url'; + +const nativeRoot = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + '..', +); +const repoRoot = path.resolve(nativeRoot, '../../../..'); +const guidePath = path.join( + repoRoot, + 'apps/ai-game-creator-shell/src-tauri/resources/agc-skills/agc-godot-editor/references/【操作指南】Godot编辑器常用操作-2026-09-20.md', +); +const guide = fs.readFileSync(guidePath, 'utf8'); +const examples = new Map( + [ + ...guide.matchAll( + /\r?\n```gdscript\r?\n([\s\S]*?)\r?\n```/g, + ), + ].map((match) => [match[1], match[2]]), +); +const executable = process.env.AGC_GODOT_TEST_EXECUTABLE; +const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + +async function until(predicate, duration = 15000) { + const end = Date.now() + duration; + while (Date.now() < end) { + const value = predicate(); + if (value) return value; + await pause(25); + } + throw Error('Godot guide fixture did not become ready'); +} + +function request(session, method, params = {}) { + return new Promise((resolve, reject) => { + const socket = net.createConnection({ + host: '127.0.0.1', + port: session.port, + }); + let data = ''; + socket.setTimeout(10000, () => + socket.destroy(Error('Guide execution receipt timed out')), + ); + socket.once('error', reject); + socket.once('connect', () => + socket.write( + `${JSON.stringify({ + protocol: session.protocol, + id: 1, + generation: session.generation, + token: session.token, + method, + params, + })}\n`, + ), + ); + socket.on('data', (chunk) => { + data += chunk; + const newline = data.indexOf('\n'); + if (newline < 0) return; + try { + const reply = JSON.parse(data.slice(0, newline)); + assert.equal(reply.protocol, session.protocol); + assert.equal(reply.generation, session.generation); + assert.equal(reply.pid, session.pid); + assert.equal(reply.buildId, session.buildId); + assert.equal( + path.resolve(reply.projectPath).toLowerCase(), + path.resolve(session.projectPath).toLowerCase(), + ); + resolve(reply.result); + } catch (error) { + reject(error); + } + socket.end(); + }); + socket.once('end', () => { + if (!data.includes('\n')) reject(Error('Godot exited without a receipt')); + }); + }); +} + +test('Godot guide examples are unique, extractable, and within the runtime read budget', () => { + assert.ok(Buffer.byteLength(guide, 'utf8') <= 14 * 1024); + assert.equal([...guide.matchAll(/\n```csharp\n"); + let (_, after) = guide.split_once(&marker).expect("指南缺少示例"); + after + .split_once("\n```") + .expect("示例代码未闭合") + .0 + .to_string() +} + +fn required(name: &str) -> String { + std::env::var(name).unwrap_or_else(|_| panic!("显式设置 {name} 后才能运行实机示例")) +} + +struct Disconnect; +impl Drop for Disconnect { + fn drop(&mut self) { + disconnect_unity_editor(); + } +} + +#[test] +#[ignore = "需要指定独立临时 Unity fixture;修改演示场景、资源并验证 Undo 和保存"] +fn execute_documented_unity_examples_in_owned_fixture() { + let project = required("AGC_UNITY_SMOKE_PROJECT"); + let project_path = PathBuf::from(&project); + assert!(project_path.join(".agc-guide-fixture").is_file()); + let pid = required("AGC_UNITY_SMOKE_PID").parse::().unwrap(); + let adapter = UnityEditorAdapter::new(vec![PathBuf::from(required("AGC_UNITY_SMOKE_HELPER"))]); + let _disconnect = Disconnect; + let connected = adapter + .rpc("connect", json!({"projectPath":project,"processId":pid})) + .unwrap(); + assert_eq!(connected["connected"], true, "{connected}"); + + let execute = |label: &str, code: &str| -> Value { + let reply = adapter + .rpc( + "execute", + json!({"projectPath":project,"processId":pid,"code":code}), + ) + .unwrap(); + println!("{}", json!({"example":label,"reply":reply})); + assert_eq!(reply["status"], "completed", "{label}: {reply}"); + reply["result"].clone() + }; + let run = |name: &str| execute(name, &example(name)); + execute("reset_owned_fixture_scene", "UnityEditor.SceneManagement.EditorSceneManager.NewScene(UnityEditor.SceneManagement.NewSceneSetup.EmptyScene, UnityEditor.SceneManagement.NewSceneMode.Single); return true;"); + run("inspect"); + assert_eq!(run("create")["collider"], true); + assert_eq!(run("modify")["colliderX"].as_f64(), Some(2.0)); + let read = run("inspect"); + assert!(read["nodes"] + .as_array() + .unwrap() + .iter() + .any(|node| node["path"] == "AGC_Guide_Object" && node["x"].as_f64() == Some(1.0))); + run("undo"); + assert_eq!( + execute( + "read_undo", + "return UnityEditor.Selection.activeGameObject.transform.localPosition.x;" + ) + .as_f64(), + Some(0.0) + ); + assert_eq!(run("remove_component")["removed"], true); + assert_eq!(run("undo")["collider"], true); + assert_eq!(run("save")["dirty"], false); + assert_eq!(run("open")["loaded"], true); + assert_eq!(run("inspect")["scene"], "Assets/AGCGuide/Guide.unity"); + + execute("prepare_prefab_fixture", "var go = new UnityEngine.GameObject(\"GuidePrefab\"); try { var saved = UnityEditor.PrefabUtility.SaveAsPrefabAsset(go, \"Assets/AGCGuide/Guide.prefab\"); return saved != null; } finally { UnityEngine.Object.DestroyImmediate(go); }"); + assert!(run("assets")["assets"] + .as_array() + .unwrap() + .iter() + .any(|asset| asset["path"] == "Assets/AGCGuide/Guide.prefab")); + assert_eq!(run("prefab")["source"], "Assets/AGCGuide/Guide.prefab"); + run("modify"); + assert_eq!(execute("read_prefab_override", "return UnityEditor.PrefabUtility.HasPrefabInstanceAnyOverrides(UnityEditor.Selection.activeGameObject, false);"), true); + let canvas = run("canvas"); + assert_eq!(canvas["width"].as_f64(), Some(320.0)); + assert_eq!(canvas["height"].as_f64(), Some(180.0)); + let ui = execute("read_canvas", "var go = UnityEngine.GameObject.Find(\"AGC_Guide_Canvas/Content\"); var rect = go.GetComponent(); return new { width = rect.sizeDelta.x, height = rect.sizeDelta.y, parent = rect.parent.name }; "); + assert_eq!(ui["width"].as_f64(), Some(320.0)); + assert_eq!(ui["height"].as_f64(), Some(180.0)); + assert_eq!(ui["parent"], "AGC_Guide_Canvas"); + run("undo"); + assert_eq!( + execute( + "read_canvas_undo", + "return UnityEngine.GameObject.Find(\"AGC_Guide_Canvas\") == null;" + ), + true + ); + run("canvas"); + run("save"); + run("open"); + let reopened = run("inspect"); + assert!(reopened["nodes"] + .as_array() + .unwrap() + .iter() + .any(|node| node["path"] == "AGC_Guide_Canvas/Content")); + let persisted = execute("read_prefab_after_reopen", "foreach (var go in UnityEngine.SceneManagement.SceneManager.GetActiveScene().GetRootGameObjects()) { if (UnityEditor.PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(go) == \"Assets/AGCGuide/Guide.prefab\") return new { overrideExists = UnityEditor.PrefabUtility.HasPrefabInstanceAnyOverrides(go, false), x = go.transform.localPosition.x, colliderX = go.GetComponent().size.x }; } throw new System.Exception(\"Prefab instance missing\");"); + assert_eq!(persisted["colliderX"].as_f64(), Some(2.0)); + assert_eq!(persisted["x"].as_f64(), Some(1.0)); + assert_eq!(persisted["overrideExists"], true); + assert_eq!(run("diagnostics")["playing"], false); + assert_eq!(run("play")["requested"], "play"); + std::thread::sleep(std::time::Duration::from_secs(3)); + let reconnected = adapter + .rpc("connect", json!({"projectPath":project,"processId":pid})) + .unwrap(); + assert_eq!(reconnected["connected"], true, "{reconnected}"); + assert_eq!(run("diagnostics")["playing"], true); + assert_eq!(run("stop")["requested"], "stop"); + std::thread::sleep(std::time::Duration::from_secs(2)); + let reconnected = adapter + .rpc("connect", json!({"projectPath":project,"processId":pid})) + .unwrap(); + assert_eq!(reconnected["connected"], true, "{reconnected}"); + assert_eq!(run("diagnostics")["playing"], false); + println!("Unity 指南 13 个原文示例:查询、创建、修改、组件删除与撤销、资源查找、Prefab override、Canvas 撤销、保存重开、播放/停止及状态诊断通过。"); +}