新增 AGC Cocos 直连模块
新增独立 crate server-rs/crates/cocos-editor-bridge:Creator 主进程发现与项目/PID 身份校验、named pipe 命令协议、可选 Windows DLL 注入,默认 feature 全部关闭。
新增 Creator 主进程内 payload:在 \\.\pipe\genarrative-cocos-editor-{pid} 上串行提供 ping/status/execute,execute 只接受有界 JavaScript 函数体,结果不确定时返回 ExecutionUncertain 并禁止自动重放。
AGC 新增 cocos_editor 模块与 prepare_cocos_editor_injection、inject_cocos_editor、ping_cocos_editor、status_cocos_editor、execute_cocos_editor_code 五条命令,注入只加载随包资源目录中的 DLL。
AGC 新增 cocos-editor、cocos-editor-execute、cocos-editor-injection 三层 feature 与资源映射,Windows 构建把 bridge DLL 暂存到 resources/cocos-editor-bridge,并改为按 Cargo profile 目录查找产物 DLL。
Runtime 新增 cocos.editor.execute:同步工具策略快照、并行账本、隔离 Agent 拒绝表、工具描述与动作审计,审计只记录代码长度与 SHA-256。
DirectProject 的 agc_tools 新增 agc_cocos_execute,只接收 code,复用项目写锁和当前项目权限,结果不确定时阻断后续调用。
共享契约新增 cocos.editor.execute(confirm 权限),当前分支命令契约从 64 条更新为 65 条。
新增技术方案文档与决策记录,并在文档索引登记;resources/cocos-editor-bridge 仅提交占位与 .gitignore,生成的 DLL 不入库。
移植到 codex/agc-agent-plugins 的适配:命令契约长度、Tauri 未调用命令 allowlist 与 Windows 资源清单校验随当前分支基线同步更新。
This commit is contained in:
@@ -126,6 +126,11 @@ const allowedUncalledTauriCommands = [
|
||||
'call_agc_plugin',
|
||||
'read_agc_plugin_panel',
|
||||
'set_agc_plugin_project_path',
|
||||
'prepare_cocos_editor_injection',
|
||||
'ping_cocos_editor',
|
||||
'status_cocos_editor',
|
||||
'execute_cocos_editor_code',
|
||||
'inject_cocos_editor',
|
||||
];
|
||||
const sourceExtensions = new Set([
|
||||
'.json',
|
||||
@@ -1294,7 +1299,7 @@ if (tauriConfig.identifier !== 'world.genarrative.ai-game-creator') {
|
||||
throw new Error('AI game creator shell identifier drifted');
|
||||
}
|
||||
|
||||
const expectedBundledCodexResources = {
|
||||
const expectedBundledWindowsResources = {
|
||||
'resources/codex/win-x64/bin/codex.exe': 'codex/win-x64/bin/codex.exe',
|
||||
'resources/codex/win-x64/bin/codex-code-mode-host.exe':
|
||||
'codex/win-x64/bin/codex-code-mode-host.exe',
|
||||
@@ -1308,6 +1313,7 @@ const expectedBundledCodexResources = {
|
||||
'codex/win-x64/codex-package.json',
|
||||
'resources/codex/win-x64/NOTICE.md': 'codex/win-x64/NOTICE.md',
|
||||
'resources/codex/win-x64/manifest.json': 'codex/win-x64/manifest.json',
|
||||
'resources/cocos-editor-bridge': 'cocos-editor-bridge',
|
||||
};
|
||||
if (tauriConfig.bundle?.resources !== undefined) {
|
||||
throw new Error(
|
||||
@@ -1316,8 +1322,8 @@ if (tauriConfig.bundle?.resources !== undefined) {
|
||||
}
|
||||
assert.deepEqual(
|
||||
windowsTauriConfig.bundle?.resources,
|
||||
expectedBundledCodexResources,
|
||||
'AI game creator shell Windows Tauri config must bundle the complete pinned Codex resource set',
|
||||
expectedBundledWindowsResources,
|
||||
'AI game creator shell Windows Tauri config must bundle the complete pinned Codex resource set and Cocos bridge payload directory',
|
||||
);
|
||||
if (windowsTauriConfig.bundle?.useLocalToolsDir !== true) {
|
||||
throw new Error(
|
||||
|
||||
+12
@@ -733,6 +733,17 @@ dependencies = [
|
||||
"error-code",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cocos-editor-bridge"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "combine"
|
||||
version = "4.6.7"
|
||||
@@ -1709,6 +1720,7 @@ dependencies = [
|
||||
"axum",
|
||||
"base64 0.22.1",
|
||||
"chromiumoxide",
|
||||
"cocos-editor-bridge",
|
||||
"futures",
|
||||
"getrandom 0.3.4",
|
||||
"http",
|
||||
|
||||
@@ -6,6 +6,9 @@ publish = false
|
||||
|
||||
[features]
|
||||
default = []
|
||||
cocos-editor = ["cocos-editor-bridge/process-discovery"]
|
||||
cocos-editor-execute = ["cocos-editor", "cocos-editor-bridge/windows-transport"]
|
||||
cocos-editor-injection = ["cocos-editor-execute", "cocos-editor-bridge/windows-injection"]
|
||||
|
||||
[build-dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
@@ -19,6 +22,7 @@ ts-rs = "12.0.1"
|
||||
typed_floats = { version = "1.0.7", features = ["serde"] }
|
||||
nalgebra = { version = "0.35.0", features = ["serde-serialize"] }
|
||||
agent-runtime-core = { path = "../../../server-rs/crates/agent-runtime-core" }
|
||||
cocos-editor-bridge = { path = "../../../server-rs/crates/cocos-editor-bridge", default-features = false }
|
||||
base64 = "0.22"
|
||||
axum = "0.8"
|
||||
chromiumoxide = "0.9.1"
|
||||
|
||||
@@ -182,6 +182,7 @@ fn main() {
|
||||
);
|
||||
let manifest_path = manifest_dir.join("prompts/runtime/manifest.json");
|
||||
stage_bundled_codex_cli(&manifest_dir);
|
||||
stage_cocos_editor_payload(&manifest_dir);
|
||||
let compiled = runtime_prompt_bundle::compile_manifest(&manifest_path)
|
||||
.unwrap_or_else(|error| panic!("Prompt Bundle 编译失败:{error}"));
|
||||
validate_seed_task_catalog(&compiled);
|
||||
@@ -203,3 +204,32 @@ fn main() {
|
||||
}
|
||||
tauri_build::build()
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn stage_cocos_editor_payload(manifest_dir: &std::path::Path) {
|
||||
if std::env::var_os("CARGO_FEATURE_COCOS_EDITOR_INJECTION").is_none() {
|
||||
return;
|
||||
}
|
||||
let out_dir = std::path::PathBuf::from(std::env::var_os("OUT_DIR").expect("OUT_DIR"));
|
||||
let profile_dir = out_dir
|
||||
.ancestors()
|
||||
.find(|path| path.file_name().is_some_and(|name| name == "build"))
|
||||
.and_then(|build_dir| build_dir.parent())
|
||||
.expect("AGC Cargo profile directory not found");
|
||||
let candidates = [
|
||||
profile_dir.join("deps/cocos_editor_bridge.dll"),
|
||||
profile_dir.join("cocos_editor_bridge.dll"),
|
||||
];
|
||||
let source = candidates
|
||||
.iter()
|
||||
.find(|path| path.is_file())
|
||||
.unwrap_or_else(|| panic!("Cocos bridge native payload 未构建:{}", candidates.iter().map(|p| p.display().to_string()).collect::<Vec<_>>().join(";")));
|
||||
let destination = manifest_dir.join("resources/cocos-editor-bridge/cocos-editor-bridge.dll");
|
||||
std::fs::create_dir_all(destination.parent().expect("payload resource parent"))
|
||||
.expect("创建 Cocos bridge 资源目录失败");
|
||||
std::fs::copy(source, &destination).expect("复制 Cocos bridge native payload 失败");
|
||||
println!("cargo:rerun-if-changed={}", source.display());
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn stage_cocos_editor_payload(_manifest_dir: &std::path::Path) {}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
*.dll
|
||||
@@ -54,6 +54,8 @@ struct DirectToolBridgeState {
|
||||
regeneration_gate: tokio::sync::Mutex<()>,
|
||||
resource_generation_gate: tokio::sync::Mutex<()>,
|
||||
image_generation_gate: tokio::sync::Mutex<()>,
|
||||
#[cfg(all(windows, feature = "cocos-editor-execute"))]
|
||||
cocos_execute_uncertain: tokio::sync::Mutex<bool>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -687,6 +689,8 @@ fn direct_tool_bridge_state_with_search(
|
||||
regeneration_gate: tokio::sync::Mutex::new(()),
|
||||
resource_generation_gate: tokio::sync::Mutex::new(()),
|
||||
image_generation_gate: tokio::sync::Mutex::new(()),
|
||||
#[cfg(all(windows, feature = "cocos-editor-execute"))]
|
||||
cocos_execute_uncertain: tokio::sync::Mutex::new(false),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2295,6 +2299,67 @@ async fn bridge_web_search_at(root: &Path, arguments: &Value, search_url: &str)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(windows, feature = "cocos-editor-execute"))]
|
||||
async fn bridge_cocos_execute(state: &DirectToolBridgeState, arguments: &Value) -> Value {
|
||||
let prepared = (|| {
|
||||
bridge_reject_unknown_fields(arguments, &["code"])?;
|
||||
enforce_project_permission_policy(&state.root, "cocos.editor.execute")?;
|
||||
let code = arguments.get("code").and_then(Value::as_str)
|
||||
.ok_or_else(|| "code 必须是 JavaScript 函数体".to_string())?;
|
||||
cocos_editor_bridge::validate_execute_code(code).map_err(|error| error.to_string())?;
|
||||
Ok::<_, String>(code.to_string())
|
||||
})();
|
||||
let code = match prepared {
|
||||
Ok(code) => code,
|
||||
Err(error) => return bridge_tool_result(
|
||||
redact_agent_runtime_error(&state.root, &error, 480), Vec::new(), true,
|
||||
),
|
||||
};
|
||||
let mut uncertain = state.cocos_execute_uncertain.lock().await;
|
||||
if *uncertain {
|
||||
return bridge_tool_result(json!({
|
||||
"status": "needs-reconciliation", "retryAllowed": false,
|
||||
"message": "先前 Cocos execute 结果待核对,当前 bridge 不再发送执行命令"
|
||||
}).to_string(), Vec::new(), true);
|
||||
}
|
||||
let root = state.root.clone();
|
||||
let result = tokio::task::spawn_blocking(move || {
|
||||
let _lock = acquire_project_write_lock(&root, "direct-cocos.execute")
|
||||
.map_err(cocos_editor_bridge::BridgeError::InvalidInput)?;
|
||||
cocos_editor_bridge::execute_cocos_editor_code_for_project(
|
||||
root.to_string_lossy().as_ref(), &code, cocos_editor_bridge::DEFAULT_COMMAND_TIMEOUT_MS,
|
||||
)
|
||||
}).await;
|
||||
match result {
|
||||
Ok(Ok(response)) => {
|
||||
let is_error = !response.ok;
|
||||
let text = json!({
|
||||
"status": if response.ok { "completed" } else { "failed" },
|
||||
"requestId": response.request_id,
|
||||
"result": response.result,
|
||||
"error": response.error,
|
||||
}).to_string();
|
||||
bridge_tool_result(redact_agent_runtime_project_paths(&state.root, &text, 32_000), Vec::new(), is_error)
|
||||
}
|
||||
failed => {
|
||||
let (is_uncertain, error) = match failed {
|
||||
Ok(Err(error)) => (
|
||||
matches!(&error, cocos_editor_bridge::BridgeError::ExecutionUncertain(_)),
|
||||
error.to_string(),
|
||||
),
|
||||
Err(_) => (true, "Cocos execute worker 退出,执行结果需要核对".to_string()),
|
||||
Ok(Ok(_)) => unreachable!(),
|
||||
};
|
||||
*uncertain = is_uncertain;
|
||||
bridge_tool_result(json!({
|
||||
"status": if is_uncertain { "needs-reconciliation" } else { "failed" },
|
||||
"retryAllowed": !is_uncertain,
|
||||
"message": redact_agent_runtime_error(&state.root, &error, 480),
|
||||
}).to_string(), Vec::new(), true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_direct_tool_bridge(
|
||||
State(state): State<Arc<DirectToolBridgeState>>,
|
||||
Json(request): Json<DirectToolBridgeRequest>,
|
||||
@@ -2308,6 +2373,8 @@ async fn handle_direct_tool_bridge(
|
||||
}
|
||||
"agc_list_project_files" => bridge_list_project_files(&state.root, &request.arguments),
|
||||
"agc_write_file" => bridge_write_file(&state.root, &request.arguments),
|
||||
#[cfg(all(windows, feature = "cocos-editor-execute"))]
|
||||
"agc_cocos_execute" => bridge_cocos_execute(&state, &request.arguments).await,
|
||||
"agc_list_account_assets" => bridge_list_account_assets(&state, &request.arguments).await,
|
||||
"agc_import_account_assets" => {
|
||||
bridge_import_account_assets(&state, &request.arguments).await
|
||||
|
||||
@@ -431,6 +431,17 @@ fn direct_tools_mcp_specs_for(controlled_web_search: bool) -> Value {
|
||||
}),
|
||||
];
|
||||
let mut tools = tools;
|
||||
#[cfg(all(windows, feature = "cocos-editor-execute"))]
|
||||
tools.push(json!({
|
||||
"name": "agc_cocos_execute",
|
||||
"description": "在当前项目已连接的 Cocos Creator 主进程执行 JavaScript 函数体,支持 await 和 return。宿主绑定项目和目标进程,只提交 code。结果待核对或超时后禁止自动重发;使用 Editor.Message 调用 Creator API。",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": { "code": { "type": "string", "minLength": 1, "maxLength": cocos_editor_bridge::MAX_EXECUTE_CODE_BYTES } },
|
||||
"required": ["code"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}));
|
||||
if controlled_web_search {
|
||||
tools.push(json!({
|
||||
"name": "agc_web_search",
|
||||
@@ -503,6 +514,19 @@ fn validate_write_file_arguments(arguments: &Value) -> Result<(), String> {
|
||||
normalize_relative_path(&path).map(|_| ())
|
||||
}
|
||||
|
||||
#[cfg(all(windows, feature = "cocos-editor-execute"))]
|
||||
async fn call_agc_cocos_execute(arguments: &Value) -> Value {
|
||||
let validated = validate_tool_object_fields(arguments, &["code"]).and_then(|()| {
|
||||
let code = arguments.get("code").and_then(Value::as_str)
|
||||
.ok_or_else(|| "code 必须是 JavaScript 函数体".to_string())?;
|
||||
cocos_editor_bridge::validate_execute_code(code).map_err(|error| error.to_string())
|
||||
});
|
||||
if let Err(error) = validated {
|
||||
return mcp_tool_result(error, Vec::new(), true);
|
||||
}
|
||||
call_client_tool_bridge("agc_cocos_execute", arguments).await
|
||||
}
|
||||
|
||||
fn mcp_success(id: Value, result: Value) -> Value {
|
||||
json!({ "jsonrpc": "2.0", "id": id, "result": result })
|
||||
}
|
||||
@@ -1575,6 +1599,8 @@ async fn handle_direct_tools_mcp_request(root: &Path, request: Value) -> Option<
|
||||
"conversation.read" => external_mcp_conversation_read(root, &arguments),
|
||||
"agc_read_skill_resource" => call_agc_read_skill_resource(&arguments),
|
||||
"agc_write_file" => call_agc_write_file(&arguments).await,
|
||||
#[cfg(all(windows, feature = "cocos-editor-execute"))]
|
||||
"agc_cocos_execute" => call_agc_cocos_execute(&arguments).await,
|
||||
"taonier_prepare_game_art" => call_taonier_prepare_game_art(&arguments).await,
|
||||
"agc_generate_image" => call_agc_generate_image(&arguments).await,
|
||||
"agc_edit_image" => call_agc_edit_image(&arguments).await,
|
||||
@@ -1869,6 +1895,9 @@ mod tests {
|
||||
"agc_remove_background",
|
||||
"agc_browser_playtest",
|
||||
]
|
||||
.into_iter()
|
||||
.chain(cfg!(all(windows, feature = "cocos-editor-execute")).then_some("agc_cocos_execute"))
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
let serialized = specs.to_string();
|
||||
assert!(!serialized.contains("agc_web_search"));
|
||||
|
||||
@@ -1606,6 +1606,17 @@ pub(crate) fn agent_runtime_tool_action_input_summary(
|
||||
.unwrap_or(160)
|
||||
),
|
||||
"command.run_limited" => format!("commandId={}", text(&["commandId", "command_id", "id"])),
|
||||
"cocos.editor.execute" => format!(
|
||||
"codeChars={} · codeSha256={:x}",
|
||||
chars(&["code"]),
|
||||
Sha256::digest(
|
||||
input
|
||||
.get("code")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or_default()
|
||||
.as_bytes()
|
||||
)
|
||||
),
|
||||
"preview.validate" => {
|
||||
let viewports = input
|
||||
.get("viewports")
|
||||
|
||||
@@ -362,6 +362,16 @@ pub(crate) async fn execute_game_creator_agent_runtime_tool_action_with_pending_
|
||||
observe_agent_runtime_limited_command(root, agent_id, run_id, &action.input)
|
||||
}
|
||||
"preview.start" => observe_agent_runtime_preview_start(root, agent_id, run_id),
|
||||
"cocos.editor.execute" => observe_agent_runtime_project_snapshot_with_lock(
|
||||
root,
|
||||
agent_id,
|
||||
run_id,
|
||||
action,
|
||||
&action_fingerprint,
|
||||
pending_action,
|
||||
true,
|
||||
|| observe_agent_runtime_cocos_editor_execute(root, action, pending_action),
|
||||
),
|
||||
"preview.validate" => {
|
||||
observe_agent_runtime_preview_validate(
|
||||
root,
|
||||
|
||||
@@ -95,6 +95,8 @@ pub(in crate::agent) fn game_creator_agent_runtime_tool_command_id(
|
||||
"command.stdin" => Some("command.stdin"),
|
||||
"command.terminate" => Some("command.terminate"),
|
||||
"command.run_limited" => Some("command.run_limited"),
|
||||
#[cfg(feature = "cocos-editor-execute")]
|
||||
"cocos.editor.execute" => Some("cocos.editor.execute"),
|
||||
"preview.start" => Some("preview.start"),
|
||||
"preview.validate" => Some("preview.validate"),
|
||||
"image.inspect" => Some("image.inspect"),
|
||||
|
||||
+6
-2
@@ -17,7 +17,7 @@ mod canvas_asset_kind_contract_tests {
|
||||
}
|
||||
|
||||
pub(crate) fn agent_runtime_executable_tools() -> Vec<&'static str> {
|
||||
vec![
|
||||
let tools = vec![
|
||||
GAME_CREATOR_USER_INPUT_REQUEST_TOOL,
|
||||
"memory.read",
|
||||
"memory.write",
|
||||
@@ -63,7 +63,11 @@ pub(crate) fn agent_runtime_executable_tools() -> Vec<&'static str> {
|
||||
"agent.schedule_ready",
|
||||
"agent.action_history",
|
||||
"agent.run_status",
|
||||
]
|
||||
];
|
||||
tools
|
||||
.into_iter()
|
||||
.chain(cfg!(feature = "cocos-editor-execute").then_some("cocos.editor.execute"))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn agent_runtime_native_executable_tools() -> Vec<&'static str> {
|
||||
|
||||
@@ -2,6 +2,7 @@ use super::*;
|
||||
|
||||
mod action_history;
|
||||
mod command_ops;
|
||||
mod cocos_editor;
|
||||
mod context;
|
||||
mod delegation;
|
||||
mod delivery;
|
||||
@@ -21,6 +22,7 @@ mod ui_workflow;
|
||||
|
||||
pub(in crate::agent) use action_history::*;
|
||||
pub(in crate::agent) use command_ops::*;
|
||||
pub(in crate::agent) use cocos_editor::*;
|
||||
pub(in crate::agent) use context::*;
|
||||
pub(in crate::agent) use delegation::*;
|
||||
pub(in crate::agent) use delivery::*;
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||
struct CocosEditorExecuteInput {
|
||||
code: String,
|
||||
}
|
||||
|
||||
pub(in crate::agent) fn observe_agent_runtime_cocos_editor_execute(
|
||||
root: &Path,
|
||||
action: &AgentRuntimeToolAction,
|
||||
pending_action: Option<&AgentRuntimePendingToolAction>,
|
||||
) -> AgentRuntimeToolObservation {
|
||||
let input = match serde_json::from_value::<CocosEditorExecuteInput>(action.input.clone()) {
|
||||
Ok(input) => input,
|
||||
Err(error) => {
|
||||
return AgentRuntimeToolObservation {
|
||||
tool: "cocos.editor.execute".to_string(),
|
||||
status: "failed".to_string(),
|
||||
summary: sanitize_agent_runtime_text(
|
||||
&format!("cocos.editor.execute 输入无效:{error}"),
|
||||
240,
|
||||
),
|
||||
detail: None,
|
||||
};
|
||||
}
|
||||
};
|
||||
if pending_action.is_none() {
|
||||
return AgentRuntimeToolObservation {
|
||||
tool: "cocos.editor.execute".to_string(),
|
||||
status: "failed".to_string(),
|
||||
summary: "cocos.editor.execute 必须绑定 durable pending action".to_string(),
|
||||
detail: None,
|
||||
};
|
||||
}
|
||||
let response = match cocos_editor_bridge::execute_cocos_editor_code_for_project(
|
||||
root.to_string_lossy().as_ref(),
|
||||
&input.code,
|
||||
cocos_editor_bridge::DEFAULT_COMMAND_TIMEOUT_MS,
|
||||
) {
|
||||
Ok(response) => response,
|
||||
Err(error) => {
|
||||
let uncertain = matches!(
|
||||
&error,
|
||||
cocos_editor_bridge::BridgeError::ExecutionUncertain(_)
|
||||
);
|
||||
return AgentRuntimeToolObservation {
|
||||
tool: "cocos.editor.execute".to_string(),
|
||||
status: if uncertain {
|
||||
AGENT_RUNTIME_TOOL_OBSERVATION_STATUS_NEEDS_RECONCILIATION
|
||||
} else {
|
||||
"failed"
|
||||
}
|
||||
.to_string(),
|
||||
summary: redact_agent_runtime_project_paths(root, &error.to_string(), 240),
|
||||
detail: Some(
|
||||
serde_json::json!({
|
||||
"codeChars": input.code.chars().count(),
|
||||
"error": redact_agent_runtime_project_paths(root, &error.to_string(), 500),
|
||||
})
|
||||
.to_string(),
|
||||
),
|
||||
};
|
||||
}
|
||||
};
|
||||
let ok = response.ok;
|
||||
let detail = serde_json::json!({
|
||||
"processId": response.process_id,
|
||||
"requestId": response.request_id,
|
||||
"ok": ok,
|
||||
"result": response.result.clone(),
|
||||
"error": response.error.clone(),
|
||||
"codeChars": input.code.chars().count(),
|
||||
})
|
||||
.to_string();
|
||||
AgentRuntimeToolObservation {
|
||||
tool: "cocos.editor.execute".to_string(),
|
||||
status: if ok { "ok" } else { "failed" }.to_string(),
|
||||
summary: if ok {
|
||||
format!(
|
||||
"已在 Cocos Creator 执行 {} 字符代码",
|
||||
input.code.chars().count()
|
||||
)
|
||||
} else {
|
||||
"Cocos Creator execute 返回失败".to_string()
|
||||
},
|
||||
detail: Some(detail),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn execute_requires_durable_pending_action_before_touching_editor() {
|
||||
let action = AgentRuntimeToolAction {
|
||||
tool: "cocos.editor.execute".to_string(),
|
||||
reason: Some("test".to_string()),
|
||||
input: serde_json::json!({"code": "return 1 + 1;"}),
|
||||
};
|
||||
let observation =
|
||||
observe_agent_runtime_cocos_editor_execute(Path::new("C:\\cocos"), &action, None);
|
||||
assert_eq!(observation.status, "failed");
|
||||
assert!(observation
|
||||
.summary
|
||||
.contains("必须绑定 durable pending action"));
|
||||
}
|
||||
}
|
||||
@@ -1027,6 +1027,9 @@ fn runtime_tool_description(tool: &str) -> &'static str {
|
||||
"ui.workflow.run" => {
|
||||
"先用 discover 从受控 game/ui-pages.json 或页面声明标记自动发现全部功能页面,再把已登记 ui-prototype 与每个页面的设计图桥接成独立 UI JSON State;可同时载入已登记图片、图标和项目字体,执行 Provider 结构识别、多树合并与分批组件绑定、回读阶段,并且只有所有页面已绑定且已应用到 game/ 后才允许 finalize。项目根目录由 Runtime 注入,模型不得传入宿主路径。"
|
||||
}
|
||||
"cocos.editor.execute" => {
|
||||
"在当前项目对应的已打开 Cocos Creator 编辑器中执行一段有界代码;Runtime 自动绑定唯一匹配的 Creator 主进程,代码与结果都通过注入 payload 的本机 bridge 返回。"
|
||||
}
|
||||
"blackboard.write" => "向项目级共享黑板追加稳定结论。",
|
||||
"agent.message" => "向一个目标 Agent 写入定向上下文消息。",
|
||||
"agent.delegate" => {
|
||||
@@ -1216,6 +1219,14 @@ fn runtime_tool_input_schema(tool: &str) -> Value {
|
||||
}
|
||||
}),
|
||||
"command.exec" | "command.start" => command_start_input_schema(),
|
||||
"cocos.editor.execute" => json!({
|
||||
"type": "object",
|
||||
"required": ["code"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"code": { "type": "string", "minLength": 1, "maxLength": 131072 }
|
||||
}
|
||||
}),
|
||||
"command.output_read" => json!({
|
||||
"type": "object", "required": ["actionId", "startLine", "maxLines"], "additionalProperties": false,
|
||||
"properties": {
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
#[cfg(feature = "cocos-editor-injection")]
|
||||
use cocos_editor_bridge::CocosEditorInjectionRequest;
|
||||
use cocos_editor_bridge::{
|
||||
CocosEditorCommandResponse, CocosEditorInjectionResult, CocosEditorProcess,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
#[cfg(feature = "cocos-editor-injection")]
|
||||
use tauri::Manager;
|
||||
#[cfg(feature = "cocos-editor-injection")]
|
||||
const BUNDLED_COCOS_BRIDGE_PAYLOAD: &str = "cocos-editor-bridge/cocos-editor-bridge.dll";
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||
pub(crate) struct CocosEditorTargetRequest {
|
||||
process_id: u32,
|
||||
project_path: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||
pub(crate) struct CocosEditorCommandInput {
|
||||
process_id: u32,
|
||||
project_path: String,
|
||||
#[serde(default = "default_command_timeout_ms")]
|
||||
timeout_ms: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||
pub(crate) struct CocosEditorInjectionInput {
|
||||
process_id: u32,
|
||||
project_path: String,
|
||||
#[serde(default = "default_injection_timeout_ms")]
|
||||
timeout_ms: u32,
|
||||
}
|
||||
|
||||
fn default_command_timeout_ms() -> u32 {
|
||||
cocos_editor_bridge::DEFAULT_COMMAND_TIMEOUT_MS
|
||||
}
|
||||
|
||||
fn default_injection_timeout_ms() -> u32 {
|
||||
cocos_editor_bridge::DEFAULT_INJECTION_TIMEOUT_MS
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||
pub(crate) struct CocosEditorExecuteInput {
|
||||
process_id: u32,
|
||||
project_path: String,
|
||||
code: String,
|
||||
#[serde(default = "default_command_timeout_ms")]
|
||||
timeout_ms: u32,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) fn prepare_cocos_editor_injection(
|
||||
request: CocosEditorTargetRequest,
|
||||
) -> Result<CocosEditorProcess, String> {
|
||||
#[cfg(feature = "cocos-editor")]
|
||||
{
|
||||
return cocos_editor_bridge::validate_injection_target(
|
||||
request.process_id,
|
||||
&request.project_path,
|
||||
)
|
||||
.map_err(|error| error.to_string());
|
||||
}
|
||||
#[cfg(not(feature = "cocos-editor"))]
|
||||
{
|
||||
let _ = (request.process_id, request.project_path);
|
||||
Err("Cocos Editor bridge feature 未启用".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) fn ping_cocos_editor(
|
||||
request: CocosEditorCommandInput,
|
||||
) -> Result<CocosEditorCommandResponse, String> {
|
||||
#[cfg(feature = "cocos-editor-execute")]
|
||||
{
|
||||
return cocos_editor_bridge::ping_cocos_editor(
|
||||
request.process_id,
|
||||
&request.project_path,
|
||||
request.timeout_ms,
|
||||
)
|
||||
.map_err(|error| error.to_string());
|
||||
}
|
||||
#[cfg(not(feature = "cocos-editor-execute"))]
|
||||
{
|
||||
let _ = (request.process_id, request.project_path, request.timeout_ms);
|
||||
Err("Cocos Editor execute feature 未启用".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) fn status_cocos_editor(
|
||||
request: CocosEditorCommandInput,
|
||||
) -> Result<CocosEditorCommandResponse, String> {
|
||||
#[cfg(feature = "cocos-editor-execute")]
|
||||
{
|
||||
return cocos_editor_bridge::status_cocos_editor(
|
||||
request.process_id,
|
||||
&request.project_path,
|
||||
request.timeout_ms,
|
||||
)
|
||||
.map_err(|error| error.to_string());
|
||||
}
|
||||
#[cfg(not(feature = "cocos-editor-execute"))]
|
||||
{
|
||||
let _ = (request.process_id, request.project_path, request.timeout_ms);
|
||||
Err("Cocos Editor execute feature 未启用".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) fn execute_cocos_editor_code(
|
||||
request: CocosEditorExecuteInput,
|
||||
) -> Result<CocosEditorCommandResponse, String> {
|
||||
#[cfg(feature = "cocos-editor-execute")]
|
||||
{
|
||||
return cocos_editor_bridge::execute_cocos_editor_code(
|
||||
request.process_id,
|
||||
&request.project_path,
|
||||
&request.code,
|
||||
request.timeout_ms,
|
||||
)
|
||||
.map_err(|error| error.to_string());
|
||||
}
|
||||
#[cfg(not(feature = "cocos-editor-execute"))]
|
||||
{
|
||||
let _ = (
|
||||
request.process_id,
|
||||
request.project_path,
|
||||
request.code,
|
||||
request.timeout_ms,
|
||||
);
|
||||
Err("Cocos Editor execute feature 未启用".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) fn inject_cocos_editor(
|
||||
app: tauri::AppHandle,
|
||||
input: CocosEditorInjectionInput,
|
||||
) -> Result<CocosEditorInjectionResult, String> {
|
||||
#[cfg(feature = "cocos-editor-injection")]
|
||||
{
|
||||
let payload = app
|
||||
.path()
|
||||
.resource_dir()
|
||||
.map_err(|error| format!("解析 AGC 资源目录失败:{error}"))?
|
||||
.join(BUNDLED_COCOS_BRIDGE_PAYLOAD);
|
||||
if !payload.is_file() {
|
||||
return Err(format!(
|
||||
"AGC 未随包提供 Cocos bridge payload:{}",
|
||||
BUNDLED_COCOS_BRIDGE_PAYLOAD
|
||||
));
|
||||
}
|
||||
let request = CocosEditorInjectionRequest {
|
||||
process_id: input.process_id,
|
||||
project_path: input.project_path,
|
||||
bridge_dll_path: payload.to_string_lossy().into_owned(),
|
||||
timeout_ms: input.timeout_ms,
|
||||
};
|
||||
return cocos_editor_bridge::inject_bridge_dll(&request).map_err(|error| error.to_string());
|
||||
}
|
||||
#[cfg(not(feature = "cocos-editor-injection"))]
|
||||
{
|
||||
let _ = app;
|
||||
let _ = (input.process_id, input.project_path, input.timeout_ms);
|
||||
Err("Cocos Editor injection feature 未启用;当前构建只支持目标预检".to_string())
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,7 @@ pub(crate) const ISOLATED_AGENT_UNSCOPED_DENIED_COMMAND_IDS: &[&str] = &[
|
||||
"command.exec",
|
||||
"command.start",
|
||||
"command.stdin",
|
||||
"cocos.editor.execute",
|
||||
"preview.start",
|
||||
"agent.delegate",
|
||||
"agent.spawn_isolated",
|
||||
@@ -62,6 +63,7 @@ pub(crate) const ISOLATED_AGENT_UNSCOPED_DENIED_TOOLS: &[&str] = &[
|
||||
"command.exec",
|
||||
"command.start",
|
||||
"command.stdin",
|
||||
"cocos.editor.execute",
|
||||
"preview.start",
|
||||
"agent.delegate",
|
||||
"agent.spawn_isolated",
|
||||
|
||||
@@ -251,6 +251,7 @@ mod command_exec;
|
||||
mod command_output;
|
||||
mod command_sandbox;
|
||||
mod command_sandbox_trampoline;
|
||||
mod cocos_editor;
|
||||
mod commands;
|
||||
mod config;
|
||||
mod context_compaction;
|
||||
@@ -293,6 +294,7 @@ use collaboration::*;
|
||||
use command_exec::*;
|
||||
use command_output::*;
|
||||
use command_sandbox::*;
|
||||
use cocos_editor::*;
|
||||
use commands::*;
|
||||
use config::*;
|
||||
use context_compaction::*;
|
||||
@@ -2728,7 +2730,12 @@ fn main() {
|
||||
read_diagnostic_logs,
|
||||
report_client_error,
|
||||
get_pending_error_reports,
|
||||
ack_error_reports
|
||||
ack_error_reports,
|
||||
prepare_cocos_editor_injection,
|
||||
ping_cocos_editor,
|
||||
status_cocos_editor,
|
||||
execute_cocos_editor_code,
|
||||
inject_cocos_editor,
|
||||
])
|
||||
.build(tauri_context);
|
||||
let app = match app {
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
"resources/codex/win-x64/codex-resources/codex-windows-sandbox-setup.exe": "codex/win-x64/codex-resources/codex-windows-sandbox-setup.exe",
|
||||
"resources/codex/win-x64/codex-package.json": "codex/win-x64/codex-package.json",
|
||||
"resources/codex/win-x64/NOTICE.md": "codex/win-x64/NOTICE.md",
|
||||
"resources/codex/win-x64/manifest.json": "codex/win-x64/manifest.json"
|
||||
"resources/codex/win-x64/manifest.json": "codex/win-x64/manifest.json",
|
||||
"resources/cocos-editor-bridge": "cocos-editor-bridge"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
- [DirectProject Codex 原始历史与异常恢复](./technical/【技术方案】DirectProject%20Codex原始历史与异常恢复-2026-09-04.md):原始 Responses item 持久化、线程注入与异常回合收尾。
|
||||
- [DirectProject 客户端 Skill 与 MCP 扩展导入方案](./technical/【技术方案】DirectProject客户端Skill与MCP扩展导入方案-2026-08-31.md):客户端扩展导入、按独立 Skill/MCP 拆分、命名、启用和启动时注入边界。
|
||||
- [AGC 通用插件宿主与编辑器适配](./technical/【技术方案】AGC通用插件宿主与编辑器适配-2026-09-09.md):通用插件宿主、SDK、权限审计、UI 挂载和 Cocos 编辑器适配边界。
|
||||
- [AGC Cocos Creator 编辑器桥接模块](<./technical/【技术方案】AGC Cocos Creator 编辑器桥接模块-2026-09-09.md>):独立 crate、feature 开关、目标校验与 Windows 注入边界。
|
||||
- [AGC 客户端更新检查与下载](./technical/【技术方案】AGC客户端更新检查与下载-2026-08-31.md):启动版本检测、OSS 清单格式和下载约定。
|
||||
- [DirectProject 本轮附件路径映射](./technical/【技术方案】DirectProject本轮附件路径映射-2026-08-31.md):Direct 首轮只映射附件原名与项目相对路径,不灌正文、不区别 GDD。
|
||||
- [Direct 回合行为审计账本](./technical/【技术方案】Direct回合行为审计账本-2026-08-31.md):Direct GUI 回合把 native 读 / MCP / 写文件落成项目内有界时间线,用于判断有没有打开本轮附件。
|
||||
|
||||
@@ -8203,3 +8203,11 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
|
||||
- 用户侧 AGC Plugin 按 OpenAI Agent Plugins 组合模型吸收现有 Skill/MCP:统一 catalog、来源和审计,但 Skill 仍由 Codex 原生读取、MCP 仍由 MCP transport 启动。新增通用 `plugin_host` 和 `@genarrative/agc-plugin-sdk`;扫描、manifest 校验、Runtime Plugin 子进程启停/热重载、行分隔 JSON-RPC、UI/Capability 注册、权限和审计统一由宿主负责。
|
||||
- 插件 manifest 使用 Agent Plugins 根目录 `plugin.json` 和标准 schema,兼容 `.codex-plugin/plugin.json`;AGC Runtime 字段放在 `extensions.world.genarrative.agc`,来源和父/子项统一保存到既有 `extensions` 索引。入口和面板资源只能是插件目录内普通文件;权限采用白名单,进程继承最小系统环境,不接收客户端凭据。
|
||||
- 目标编辑器只实现 `EditorAdapter` 的查找、PID/项目/版本校验、连接和请求转换;本次仅保留通用 registry 和 trait,不随标准 Plugin 核心内置具体编辑器适配器。
|
||||
|
||||
## 2026-09-09 AGC Cocos Creator 编辑器桥接独立 crate
|
||||
|
||||
- Cocos Creator bridge 核心位于 `server-rs/crates/cocos-editor-bridge`,与 Tauri、Agent Runtime 和服务端解耦;默认 feature 关闭,桌面宿主按需启用 `process-discovery` 或 `windows-injection`。
|
||||
- 进程发现只用于把 CocosCreator 主进程 PID 与 `--project` 和 Creator 版本绑定,排除 Electron 子进程;AGC 默认不加载该能力。
|
||||
- 注入仅加载随 AGC 资源目录提供的 DLL,结果先标记 `injected-unverified`,必须由 payload 完成握手后才可开放有限 Cocos 操作;Runtime 的 execute 代码有界并受确认策略保护,不开放未受限 eval 或项目扩展自动写入。
|
||||
- Runtime 第一阶段只广告 `cocos.editor.execute`,代码长度有界、默认走确认策略,项目根和目标 PID 不交给模型;`ping/status` 先作为宿主命令保留,不扩大全局 Agent 工具面。
|
||||
- DirectProject 的 `agc_tools` 对应入口是 `agc_cocos_execute`,同样只接收 code,并沿用当前项目权限。2026-09-10 已通过临时真实 Creator 3.8.8 验证 Node 的 Windows 调试 handler 激活 Inspector、注入 bootstrap、pipe execute 及关闭 Inspector 后继续执行;此路线尚未替换当前 native DLL 源码。现有 `RequestInterrupt` 回调不能调用 JavaScript,不能把 DLL 加载和窗口线程钩子当作可用握手。执行发送后的未知结果禁止自动重放,Direct bridge 会阻断后续 execute。详细步骤、版本/fuse 和端口边界见 Cocos bridge 技术方案。
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
# AGC Cocos Creator 编辑器桥接模块
|
||||
|
||||
## 目标
|
||||
|
||||
目标是在用户已打开 Cocos Creator 项目时,由 AGC 识别正确的 Creator 主进程,并在进程内注入随包 JavaScript bootstrap;用户不需要在 Cocos 项目中手动安装扩展。桥接核心独立于 Tauri,位于 `server-rs/crates/cocos-editor-bridge`。2026-09-10 已验证通过 Node 自带的运行中 Inspector 激活入口完成引导,具体见本文“Inspector 注入调研”;这条路径尚未替换当前 crate 的 DLL 实现。
|
||||
|
||||
## 边界
|
||||
|
||||
Cocos Creator 3.x 是 Electron/Node 编辑器,不能复用 Unity Mono 的 CoreCLR/Roslyn 进程内调用方式。Core crate 只负责:
|
||||
|
||||
1. 读取 Creator 主进程的 PID、父 PID、可执行文件和 `--project` 参数;Electron renderer、GPU、utility、crashpad 子进程被排除。
|
||||
2. 对 PID、项目目录和 payload 做同一目标校验。项目目录必须是绝对路径、可解析目录并包含 `package.json`;Creator 版本只从 `package.json.creator.version` 读取。
|
||||
3. 在启用 `windows-injection` feature 时,通过 `OpenProcess`、`VirtualAllocEx`、`WriteProcessMemory` 和 `CreateRemoteThread(LoadLibraryW)` 加载受信任 DLL。
|
||||
4. 提供 `ping/status/execute` 协议、Windows pipe 客户端和 `payload/bootstrap.cjs`。native payload 只在具备受支持的 Node/V8 上下文调度时尝试送入 Creator 主进程并调用 `install(Editor)`,不写入项目扩展目录。
|
||||
|
||||
当前 DLL 实现仍只返回 `injected-unverified`,不能作为可交付的注入入口。它在 `windows-injection` 下构建 C++ payload 并尝试通过窗口钩子调用 V8;其中 `RequestInterrupt → run_bootstrap → Script::Run` 违反 V8 的中断回调约束,不能因编译或 DLL 加载成功而认定安全可用。`HandleScope` 的存储大小和 C++ Local/MaybeLocal 的调用约定也不能通过裸指针替代来推断。后续接入采用下述 Inspector 引导,不再依赖这条未经验证的 native 路径。
|
||||
|
||||
## Feature 开关
|
||||
|
||||
crate 默认不启用任何宿主集成:
|
||||
|
||||
```toml
|
||||
cocos-editor-bridge = { path = ".../server-rs/crates/cocos-editor-bridge", default-features = false, features = ["process-discovery"] }
|
||||
```
|
||||
|
||||
- `process-discovery`:启用 Windows Creator 主进程发现;不加载 Windows 注入 API。
|
||||
- `windows-transport`:在已注入 payload 后启用本机 named pipe 的 `ping/status/execute` 命令传输。
|
||||
- `windows-injection`:隐含启用 `windows-transport`,并启用 Windows native DLL 注入实现。
|
||||
|
||||
AGC 或其它桌面宿主应将 `windows-injection` 作为单独的发行构建开关,服务端和非桌面构建保持 `default-features = false`。
|
||||
|
||||
当前 AGC Tauri adapter 只暴露 `prepare_cocos_editor_injection`(确认前预检)、`inject_cocos_editor`(确认后注入)、`ping_cocos_editor`、`status_cocos_editor` 和 `execute_cocos_editor_code`。Runtime 只广告一个 `cocos.editor.execute` 工具,代码输入使用当前项目根,目标 PID 由 crate 内部唯一匹配;默认命令权限为 confirm,具体运行档沿用已有 Runtime 策略。进程发现留在 crate 内部作为目标校验步骤,不建立客户端扫描服务或独立发现入口。默认 AGC 构建不启用 Cocos 集成;桌面构建需显式传 `--features cocos-editor`,命令执行需传 `--features cocos-editor-execute`,注入构建再传 `--features cocos-editor-injection`。注入命令不接收 DLL 路径,只加载资源目录中的 `cocos-editor-bridge/cocos-editor-bridge.dll`,避免把 Tauri command 变成任意 DLL 注入器。
|
||||
|
||||
## 第一阶段命令协议
|
||||
|
||||
DirectProject 的现役 `agc_tools` 目录通过 Windows `cocos-editor-execute` feature 注册 `agc_cocos_execute`,参数只有 `code`。客户端在 blocking worker 内调用 crate,保留项目锁和现有项目权限;当前 bridge 出现执行结果不确定后拒绝后续 execute。旧 Runtime 的对应工具名为 `cocos.editor.execute`,继续使用它已有的 pending action、权限和恢复语义。
|
||||
|
||||
注入 payload 在目标 Creator 主进程内监听 `\\.\pipe\genarrative-cocos-editor-{pid}`,使用换行分隔的 JSON。crate 只生成三种操作:
|
||||
|
||||
```json
|
||||
{"schemaVersion":"game-creator-cocos-editor-bridge.v1","requestId":"cocos-42-...","processId":42,"projectPath":"C:\\demo","command":{"op":"ping"}}
|
||||
{"schemaVersion":"game-creator-cocos-editor-bridge.v1","requestId":"cocos-42-...","processId":42,"projectPath":"C:\\demo","command":{"op":"status"}}
|
||||
{"schemaVersion":"game-creator-cocos-editor-bridge.v1","requestId":"cocos-42-...","processId":42,"projectPath":"C:\\demo","command":{"op":"execute","code":"return Editor.Project.path"}}
|
||||
```
|
||||
|
||||
回执必须回传相同的 `schemaVersion/requestId/processId`。`execute.code` 是支持 `await` 和 `return` 的 JavaScript 函数体,上限为 128 KiB,单次回执上限为 2 MiB;bootstrap 通过 `AsyncFunction('Editor', 'require', code)` 在 Creator 主进程事件循环串行执行,并返回可序列化 JSON。它使用 Creator/Node 的现有权限,不是代码沙箱;AGC 不维护额外的 Cocos 业务 API 名单。
|
||||
|
||||
Rust 客户端在写入前通过 `GetNamedPipeServerProcessId` 验证 pipe 属于目标 PID,读写使用 overlapped I/O 和 deadline。execute 开始写入后遇到断线、超时或无可信回执,返回 `ExecutionUncertain`,Runtime 进入 `needs-reconciliation`,不得自动重放。客户端超时不等于 JavaScript 已取消,bootstrap 保持同一串行队列直到原执行结束;同步死循环仍可能阻塞 Creator,需要真实集成阶段提供运行时中断方案。
|
||||
|
||||
## 安全与失败关闭
|
||||
|
||||
- PowerShell 查询脚本为固定常量,用户输入不拼接进 shell。
|
||||
- 目标 PID 必须是没有 Electron `--type` 参数的 `CocosCreator.exe`,且其 `--project` 与请求目录规范化后相同;不能仅凭进程名注入。
|
||||
- bridge DLL 必须是绝对路径、普通文件、非符号链接、`.dll` 扩展名且大小不超过 64 MiB。AGC adapter 还必须把路径限制在签名/随包资源目录;core 不接受任意下载 URL。
|
||||
- 注入超时不会释放仍可能被远程线程使用的内存,并返回人工核对错误,避免在不确定状态下破坏目标进程。
|
||||
- 非 Windows、feature 未启用、目标不存在、身份不匹配或 payload 不合规均直接失败,不启动 Cocos、不关闭 Cocos、不修改项目文件。
|
||||
|
||||
## 后续接入
|
||||
|
||||
下一步是在独立 crate 内实现 Inspector 引导并替换 DLL 装载入口,保留 feature 开关、项目/PID 绑定和 `ping/status/execute` 三条命令。进程发现仍只是内部目标校验,不增加客户端扫描服务。隔离验证脚本使用 Node 的 `_debugProcess`;正式 Rust 实现可直接调用同一组 Win32 API,无需附带额外 Node 运行时。源码接入、默认/启用 feature 编译和 AGC 发行包验收仍未完成。
|
||||
|
||||
当前验证:crate 全 feature 单元测试、Node bootstrap fixture、Rust 到 Node bootstrap 的真实命名管道回环(含 execute 超时不确定结果)通过;AGC feature 编译使用临时工作树与 Codex 资源替身,只属于编译检查,不代表发布包或真实 Creator 验收。当前工作区完整 AGC 检查受缺失内置 Codex CLI 阻断,TypeScript 契约测试受缺失 Vitest 阻断。
|
||||
|
||||
## Inspector 注入调研(2026-09-10)
|
||||
|
||||
### 入口与执行链
|
||||
|
||||
Node 20.15.1 的 Windows 调试信号初始化会创建 `node-debug-handler-{pid}` 命名共享内存,其中存放目标进程自身 `StartIoThreadProc` 的函数地址。`process._debugProcess(pid)` 的内部实现是 `OpenProcess → OpenFileMappingW(FILE_MAP_READ) → MapViewOfFile → CreateRemoteThread(目标 handler)`。handler 通过 Node 自己的 libuv/interrupt 调度启动 Inspector,宿主无需写入 DLL、修改机器指令或猜测 V8 对象布局。
|
||||
|
||||
接入次序:
|
||||
|
||||
1. 校验 Creator 主进程、项目身份、位数和调试入口是否可用;共享内存给出的入口应与目标进程已加载的可执行映像匹配,不能仅信任一个带 PID 的对象名。
|
||||
2. 如已有目标 Inspector,复用且记录原有状态;否则激活目标 handler。只连接属于该 PID 的回环监听端口,随后再次核对 `process.pid`、`Editor.Project.path` 和运行版本。已有进程通常使用默认调试端口;端口被其它进程占用时应失败,不连接其它目标。AGC 自己启动的 Creator 可预设 `--inspect-port=127.0.0.1:0`,该参数本身不启用 Inspector。
|
||||
3. 使用 Inspector `Runtime.evaluate` 在 Node 主上下文求值 CommonJS 包装器,把 `require` 显式传给 bootstrap,并调用 `install(Editor)`。不增加业务工具列表。
|
||||
4. 等待 bootstrap 的 pipe 就绪,按既有身份合同验证 `ping`,再开放 `execute`。
|
||||
5. 如 Inspector 是本次引导开启的,先断开 Inspector WebSocket,再从 pipe 执行 `require('node:inspector').close()`;已有用户调试会话保持原状态。之后持续命令全部走 pipe。
|
||||
|
||||
### 已验证与限制
|
||||
|
||||
- 使用本机 Creator 3.8.8 的真实 GUI 主进程和由官方 Empty(3D) 模板创建的临时项目,未安装项目扩展。运行时为 Electron 31.3.1、Node 20.15.1、V8 12.6.228.28-electron.0。
|
||||
- 在进程启动后激活 Inspector,注入当前 `payload/bootstrap.cjs`,实际 `ping`、`execute` 返回目标 PID、真实项目路径和 Creator 版本。
|
||||
- 编辑器就绪后,`Editor.Message.request('scene', 'query-node-tree')` 成功返回 `null`:空白项目未打开场景。这证明消息调用链可用,不代表已验证非空场景的读取或修改。
|
||||
- 关闭 Inspector 后,pipe 的 `execute` 仍成功;验证结束正常退出临时 Creator。用户原有 Creator 仅做共享内存入口和监听端口的只读检查。
|
||||
- 本次真实 Electron Inspector 对 `awaitPromise: true` 曾返回 `Promise was collected`。验证改为同步 evaluate 启动安装、保存就绪状态并轮询;异步业务代码由 pipe 内的 Node 队列执行。不要把单次 Inspector 异常当作 bootstrap 未执行并自动重放。
|
||||
- Inspector 的命令行 API `require` 在求值之外不保证可访问;异步闭包须捕获或显式传入该函数,不能假设 `globalThis.require` 存在。
|
||||
- 本机二进制的 `EnableNodeCliInspectArguments` fuse 开启,当前已打开的 Creator 也存在调试 handler。其它版本或 fuse 被关闭的发行包不能推断支持;入口缺失应失败,不能修改 fuse 或回退到未验证的函数打补丁。
|
||||
|
||||
### 上游依据
|
||||
|
||||
- [Node 20.15.1:Windows DebugProcess](https://github.com/nodejs/node/blob/v20.15.1/src/node_process_methods.cc#L377-L448)
|
||||
- [Node 20.15.1:注册目标调试 handler](https://github.com/nodejs/node/blob/v20.15.1/src/inspector_agent.cc#L143-L197)
|
||||
- [Electron 31.3.1:NodeBindings 与 Inspector fuse](https://github.com/electron/electron/blob/v31.3.1/shell/common/node_bindings.cc)
|
||||
- [Electron Inspector fuse 说明](https://github.com/electron/electron/blob/v31.3.1/docs/tutorial/fuses.md#nodecliinspect)
|
||||
- [V8 12.6:RequestInterrupt 的回调禁止重入 isolate](https://github.com/v8/v8/blob/12.6.228/include/v8-isolate.h)
|
||||
- [Node inspector.close:等待现有连接关闭后停用 Inspector](https://github.com/nodejs/node/blob/v20.15.1/doc/api/inspector.md#inspectorclose)
|
||||
@@ -183,7 +183,7 @@ Supervisor 认领该回执后,由父 run 自己为每个原 delivery 逐一创
|
||||
|
||||
## 目标
|
||||
|
||||
在 Genarrative 内建设独立桌面 App:普通用户通过项目开发工作台中的陶泥儿对话、资源画布、运行状态和确认操作,让平台生成保存在本地的可运行 Web 游戏原型,并通过本地 HTTP server 预览;主窗口提供运行时配置入口,用于保存发布版 AppData / Tauri 配置目录里的 LLM 配置及受控开发者 External Editor 配置,设置弹窗同时提供独立“关于”页并显示从客户端构建版本注入的版本号。普通客户素材画布使用平台登录态调用内部编辑器 API,不展示或要求填写画板 Base URL / API Key。任务明细、原始文件、命令日志和专业 Agent 调试控制只通过显式开发调试入口查看,不随普通客户端启动额外打开窗口。v1 的生成闭环仍以 Web 小游戏为主,同时允许用户打开已有 Godot 项目:用户选择的目录始终作为工作区根,`.agent/`、Session、Runtime、文件工具和外围资料都留在该根;客户端检查根目录及一层直接子目录中的普通文件 `project.godot`,将唯一命中的实际目录以工作区相对 `godotProjectRoot` 记录到 manifest。Agent 使用标准运行档继续修改,不创建 `game/`、`assets/`、`memory/`、`exports/` 平行目录;本期不扩展 Unity、Godot 内嵌预览、云同步或插件市场。
|
||||
在 Genarrative 内建设独立桌面 App:普通用户通过项目开发工作台中的陶泥儿对话、资源画布、运行状态和确认操作,让平台生成保存在本地的可运行 Web 游戏原型,并通过本地 HTTP server 预览;主窗口提供运行时配置入口,用于保存发布版 AppData / Tauri 配置目录里的 LLM 配置及受控开发者 External Editor 配置,设置弹窗同时提供独立“关于”页并显示从客户端构建版本注入的版本号。普通客户素材画布使用平台登录态调用内部编辑器 API,不展示或要求填写画板 Base URL / API Key。任务明细、原始文件、命令日志和专业 Agent 调试控制只通过显式开发调试入口查看,不随普通客户端启动额外打开窗口。v1 的生成闭环仍以 Web 小游戏为主,同时允许用户打开已有 Godot 项目:用户选择的目录始终作为工作区根,`.agent/`、Session、Runtime、文件工具和外围资料都留在该根;客户端检查根目录及一层直接子目录中的普通文件 `project.godot`,将唯一命中的实际目录以工作区相对 `godotProjectRoot` 记录到 manifest。Agent 使用标准运行档继续修改,不创建 `game/`、`assets/`、`memory/`、`exports/` 平行目录;本期不扩展 Unity、Godot 内嵌预览、云同步或插件市场。新增的 Cocos Creator bridge 核心独立为 `server-rs/crates/cocos-editor-bridge`,AGC 仅通过 feature 转发桌面进程发现、受控 execute 和 Windows 注入能力;它不改变服务端路线,也不把原始 pipe、句柄或未绑定项目身份的代码执行面暴露给 Agent。
|
||||
|
||||
## 技术选择
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ describe('AI 游戏创作 App 共享契约', () => {
|
||||
it('keeps command permissions explicit', () => {
|
||||
const commandIds = GAME_CREATION_APP_COMMANDS.map((command) => command.id);
|
||||
|
||||
expect(GAME_CREATION_APP_COMMANDS).toHaveLength(64);
|
||||
expect(GAME_CREATION_APP_COMMANDS).toHaveLength(65);
|
||||
expect(commandIds).toContain('project.git_inspect');
|
||||
expect(commandIds).toContain('project.git_commit');
|
||||
expect(commandIds).toContain('project.patchset');
|
||||
@@ -29,6 +29,7 @@ describe('AI 游戏创作 App 共享契约', () => {
|
||||
expect(commandIds).toContain('command.poll');
|
||||
expect(commandIds).toContain('command.stdin');
|
||||
expect(commandIds).toContain('command.terminate');
|
||||
expect(commandIds).toContain('cocos.editor.execute');
|
||||
expect(commandIds).toContain('mcp.call');
|
||||
expect(commandIds.indexOf('command.exec')).toBe(
|
||||
commandIds.indexOf('command.run_limited') + 1,
|
||||
|
||||
@@ -67,6 +67,7 @@ export const GAME_CREATION_APP_COMMANDS = [
|
||||
{ id: 'command.poll', permission: 'auto' },
|
||||
{ id: 'command.stdin', permission: 'confirm' },
|
||||
{ id: 'command.terminate', permission: 'confirm' },
|
||||
{ id: 'cocos.editor.execute', permission: 'confirm' },
|
||||
{ id: 'canvas.project_open', permission: 'confirm' },
|
||||
{ id: 'canvas.project_sync', permission: 'confirm' },
|
||||
{ id: 'canvas.asset_import', permission: 'confirm' },
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user