修复 Cocos 内置桥接与客户端项目打开链路
修复 Cocos 内置插件提示词,禁止回退到项目内 MCP 扩展 移除 agc_cocos_execute 对项目写锁的无关依赖 将项目对话读取移到 blocking worker,避免客户端窗口阻塞 为 Windows 开发与发行构建默认启用 Cocos 编辑器执行能力 补充 Cocos Inspector 引导、桥接协议和客户端回归验证
This commit is contained in:
@@ -4,6 +4,11 @@ import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import {
|
||||
defaultEditorFeatures,
|
||||
withDefaultCargoFeatures,
|
||||
} from './cargo-features.mjs';
|
||||
|
||||
const appRoot = fileURLToPath(new URL('..', import.meta.url));
|
||||
const defaultReleaseTarget = 'x86_64-pc-windows-msvc';
|
||||
const releaseTarget =
|
||||
@@ -159,10 +164,30 @@ export async function prepareReleaseVersion() {
|
||||
return nextVersion;
|
||||
}
|
||||
|
||||
export function runTauriBuild(args = []) {
|
||||
export function buildTauriBuildArguments(
|
||||
args = [],
|
||||
target = releaseTarget,
|
||||
platform = process.platform,
|
||||
) {
|
||||
const noBundle = args.includes('--no-bundle');
|
||||
const hasTarget = args.includes('--target');
|
||||
const targetArgs = noBundle || hasTarget ? [] : ['--target', releaseTarget];
|
||||
const targetIndex = args.indexOf('--target');
|
||||
const explicitTarget =
|
||||
targetIndex >= 0
|
||||
? args[targetIndex + 1]
|
||||
: args
|
||||
.find((value) => value.startsWith('--target='))
|
||||
?.slice('--target='.length);
|
||||
const targetArgs = noBundle || explicitTarget ? [] : ['--target', target];
|
||||
const features = defaultEditorFeatures(
|
||||
explicitTarget || (noBundle ? platform : target),
|
||||
);
|
||||
return [
|
||||
'build',
|
||||
...withDefaultCargoFeatures([...targetArgs, ...args], features),
|
||||
];
|
||||
}
|
||||
|
||||
export function runTauriBuild(args = []) {
|
||||
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
||||
const result = spawnSync(
|
||||
npmCommand,
|
||||
@@ -172,9 +197,7 @@ export function runTauriBuild(args = []) {
|
||||
'exec',
|
||||
'tauri',
|
||||
'--',
|
||||
'build',
|
||||
...targetArgs,
|
||||
...args,
|
||||
...buildTauriBuildArguments(args),
|
||||
],
|
||||
{ cwd: appRoot, stdio: 'inherit', shell: process.platform === 'win32' },
|
||||
);
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/** 默认桌面能力;显式 feature 参数优先,不把应用参数当 Cargo 参数。 */
|
||||
export function withDefaultCargoFeatures(argv, features) {
|
||||
const separator = argv.indexOf('--');
|
||||
const cargoArgs = separator < 0 ? argv : argv.slice(0, separator);
|
||||
if (
|
||||
!features.length ||
|
||||
cargoArgs.some(
|
||||
(value) =>
|
||||
value === '--features' ||
|
||||
value === '-f' ||
|
||||
value.startsWith('--features=') ||
|
||||
/^-f.+/u.test(value),
|
||||
)
|
||||
) {
|
||||
return argv;
|
||||
}
|
||||
return [`--features=${features.join(',')}`, ...argv];
|
||||
}
|
||||
|
||||
export function defaultEditorFeatures(target) {
|
||||
return target === 'win32' || target.includes('windows')
|
||||
? ['cocos-editor-execute']
|
||||
: [];
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { test } from 'node:test';
|
||||
|
||||
import { buildTauriBuildArguments } from './build-release.mjs';
|
||||
import { withDefaultCargoFeatures } from './cargo-features.mjs';
|
||||
|
||||
test('Windows release includes the same editor feature as development', () => {
|
||||
assert.deepEqual(
|
||||
buildTauriBuildArguments([], 'x86_64-pc-windows-msvc', 'win32'),
|
||||
[
|
||||
'build',
|
||||
'--features=cocos-editor-execute',
|
||||
'--target',
|
||||
'x86_64-pc-windows-msvc',
|
||||
],
|
||||
);
|
||||
assert.deepEqual(
|
||||
buildTauriBuildArguments(
|
||||
['--no-bundle'],
|
||||
'x86_64-pc-windows-msvc',
|
||||
'linux',
|
||||
),
|
||||
['build', '--no-bundle'],
|
||||
);
|
||||
assert.deepEqual(
|
||||
buildTauriBuildArguments(['--target=aarch64-apple-darwin']),
|
||||
['build', '--target=aarch64-apple-darwin'],
|
||||
);
|
||||
});
|
||||
test('explicit Cargo features override defaults in every supported spelling', () => {
|
||||
for (const args of [
|
||||
['--features', 'custom'],
|
||||
['--features=custom'],
|
||||
['-f', 'custom'],
|
||||
['-fcustom'],
|
||||
]) {
|
||||
assert.deepEqual(
|
||||
withDefaultCargoFeatures(args, ['cocos-editor-execute']),
|
||||
args,
|
||||
);
|
||||
}
|
||||
assert.deepEqual(
|
||||
withDefaultCargoFeatures(
|
||||
['--', '--features=app'],
|
||||
['cocos-editor-execute'],
|
||||
),
|
||||
['--features=cocos-editor-execute', '--', '--features=app'],
|
||||
);
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import { resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { withDefaultCargoFeatures } from './cargo-features.mjs';
|
||||
import {
|
||||
readAgcDevEndpoint,
|
||||
resolveAgcDevEndpoint,
|
||||
@@ -58,11 +59,7 @@ function readDevCargoFeatures(env = process.env) {
|
||||
}
|
||||
|
||||
function withDevCargoFeatures(argv, features = readDevCargoFeatures()) {
|
||||
if (features.length === 0) return argv;
|
||||
if (argv.some((value) => value === '--features' || value === '-f')) {
|
||||
return argv;
|
||||
}
|
||||
return [`--features=${features.join(',')}`, ...argv];
|
||||
return withDefaultCargoFeatures(argv, features);
|
||||
}
|
||||
|
||||
function spawnTauriCli(argv, { env = process.env } = {}) {
|
||||
|
||||
+3
@@ -739,9 +739,11 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"editor-adapter-api",
|
||||
"reqwest 0.12.28",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"tungstenite",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
@@ -4303,6 +4305,7 @@ dependencies = [
|
||||
"cookie",
|
||||
"cookie_store",
|
||||
"encoding_rs",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2",
|
||||
|
||||
@@ -7,7 +7,7 @@ publish = false
|
||||
[features]
|
||||
default = []
|
||||
cocos-editor = ["cocos-editor-bridge/process-discovery"]
|
||||
cocos-editor-execute = ["cocos-editor", "cocos-editor-bridge/windows-transport"]
|
||||
cocos-editor-execute = ["cocos-editor", "cocos-editor-bridge/windows-bootstrap"]
|
||||
cocos-editor-injection = ["cocos-editor-execute", "cocos-editor-bridge/windows-injection"]
|
||||
|
||||
[build-dependencies]
|
||||
|
||||
@@ -305,6 +305,35 @@ fn game_creator_codex_app_server_error_detail_indicates_auth_failure(
|
||||
|| detail.contains("http 403")
|
||||
}
|
||||
|
||||
fn game_creator_codex_app_server_error_detail(error: &serde_json::Value) -> String {
|
||||
let Some(error) = error.as_object() else {
|
||||
return String::new();
|
||||
};
|
||||
["message", "additionalDetails", "code"]
|
||||
.into_iter()
|
||||
.filter_map(|field| error.get(field).and_then(serde_json::Value::as_str))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
.to_ascii_lowercase()
|
||||
}
|
||||
|
||||
fn game_creator_codex_app_server_error_detail_indicates_stream_requirement(
|
||||
error: &serde_json::Value,
|
||||
) -> bool {
|
||||
let detail = game_creator_codex_app_server_error_detail(error);
|
||||
detail.contains("stream must be set to true")
|
||||
|| detail.contains("stream=true")
|
||||
|| detail.contains("stream is required")
|
||||
}
|
||||
|
||||
fn game_creator_codex_app_server_error_detail_indicates_timeout(error: &serde_json::Value) -> bool {
|
||||
let detail = game_creator_codex_app_server_error_detail(error);
|
||||
detail.contains("timed out")
|
||||
|| detail.contains("timeout")
|
||||
|| detail.contains("request deadline exceeded")
|
||||
|| detail.contains("deadline exceeded")
|
||||
}
|
||||
|
||||
fn game_creator_codex_app_server_error_detail_indicates_request_too_large(
|
||||
error: &serde_json::Value,
|
||||
) -> bool {
|
||||
@@ -362,6 +391,15 @@ fn game_creator_codex_app_server_failed_turn_error(
|
||||
if game_creator_codex_app_server_error_detail_indicates_request_too_large(error) {
|
||||
return game_creator_codex_app_server_error_kind("request-too-large");
|
||||
}
|
||||
if game_creator_codex_app_server_error_detail_indicates_stream_requirement(error) {
|
||||
return game_creator_codex_app_server_error_kind("stream-required");
|
||||
}
|
||||
if game_creator_codex_app_server_error_detail_indicates_timeout(error) {
|
||||
return platform_llm::LlmError::Connectivity {
|
||||
attempts: 1,
|
||||
message: "Codex app-server 上游请求超时".to_string(),
|
||||
};
|
||||
}
|
||||
if game_creator_codex_app_server_error_detail_indicates_auth_failure(error) {
|
||||
return game_creator_codex_app_server_error_kind("unauthorized");
|
||||
}
|
||||
@@ -2464,10 +2502,12 @@ impl CodexAppServerConnection {
|
||||
// Codex's default provider in Debug builds.
|
||||
self.inner._provider_proxy.is_some() || !llm.api_key.trim().is_empty(),
|
||||
);
|
||||
let result = self
|
||||
.request("thread/start", params)
|
||||
.await
|
||||
.map_err(platform_llm::LlmError::Transport)?;
|
||||
let result = match self.request("thread/start", params).await {
|
||||
Ok(result) => result,
|
||||
Err(error) => {
|
||||
return Err(platform_llm::LlmError::Transport(error));
|
||||
}
|
||||
};
|
||||
let thread_id = result
|
||||
.pointer("/thread/id")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
@@ -4810,6 +4850,37 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codex_app_server_failed_turn_maps_stream_and_timeout_details() {
|
||||
let stream_error = game_creator_codex_app_server_failed_turn_error(&serde_json::json!({
|
||||
"status": "failed",
|
||||
"error": {
|
||||
"message": "Stream must be set to true",
|
||||
"codexErrorInfo": "other"
|
||||
}
|
||||
}));
|
||||
assert_eq!(
|
||||
stream_error,
|
||||
platform_llm::LlmError::InvalidRequest(
|
||||
"codex-app-server-error:stream-required".to_string()
|
||||
)
|
||||
);
|
||||
let timeout_error = game_creator_codex_app_server_failed_turn_error(&serde_json::json!({
|
||||
"status": "failed",
|
||||
"error": {
|
||||
"message": "provider request timed out",
|
||||
"codexErrorInfo": "other"
|
||||
}
|
||||
}));
|
||||
assert_eq!(
|
||||
timeout_error,
|
||||
platform_llm::LlmError::Connectivity {
|
||||
attempts: 1,
|
||||
message: "Codex app-server 上游请求超时".to_string()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codex_app_server_failed_turn_maps_insufficient_mud_points_to_stable_upstream_error() {
|
||||
for detail in [
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -2386,8 +2386,10 @@ async fn bridge_cocos_execute(state: &DirectToolBridgeState, arguments: &Value)
|
||||
}
|
||||
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 execute talks to the already-open Creator process through its
|
||||
// validated Inspector/pipe bridge. It does not mutate AGC's project
|
||||
// files or manifest, so it must not wait on `.agent/project.lock`.
|
||||
// File-writing tools keep their own project lock separately.
|
||||
if !crate::builtin_plugins::cocos_editor_agent_tool_available() {
|
||||
return Err(cocos_editor_bridge::BridgeError::InvalidInput(
|
||||
"Cocos Creator 插件已禁用".to_string(),
|
||||
@@ -2511,14 +2513,11 @@ pub(crate) async fn start_direct_tool_bridge(
|
||||
.route(&route, post(handle_direct_tool_bridge))
|
||||
.layer(DefaultBodyLimit::max(DIRECT_TOOL_BRIDGE_MAX_REQUEST_BYTES))
|
||||
.with_state(Arc::clone(&state));
|
||||
let url = format!("http://127.0.0.1:{}{route}", address.port());
|
||||
let task = tokio::spawn(async move {
|
||||
let _ = axum::serve(listener, app).await;
|
||||
});
|
||||
Ok(DirectToolBridge {
|
||||
url: format!("http://127.0.0.1:{}{route}", address.port()),
|
||||
state,
|
||||
task,
|
||||
})
|
||||
Ok(DirectToolBridge { url, state, task })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -4740,23 +4740,31 @@ pub(crate) fn archive_game_creator_agent_session(
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) fn read_local_conversation(
|
||||
pub(crate) async fn read_local_conversation(
|
||||
project_path: String,
|
||||
agent_id: Option<String>,
|
||||
session_id: Option<String>,
|
||||
) -> Result<LocalConversationResult, String> {
|
||||
let root = Path::new(project_path.trim());
|
||||
enforce_project_permission_policy(root, "conversation.read")?;
|
||||
read_local_conversation_for_session_at(root, agent_id.as_deref(), session_id.as_deref())
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
let root = Path::new(project_path.trim());
|
||||
enforce_project_permission_policy(root, "conversation.read")?;
|
||||
read_local_conversation_for_session_at(root, agent_id.as_deref(), session_id.as_deref())
|
||||
})
|
||||
.await
|
||||
.map_err(|error| format!("读取项目对话后台任务失败:{error}"))?
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) fn read_direct_project_conversation(
|
||||
pub(crate) async fn read_direct_project_conversation(
|
||||
project_path: String,
|
||||
) -> Result<LocalConversationResult, String> {
|
||||
let root = Path::new(project_path.trim());
|
||||
enforce_project_permission_policy(root, "conversation.read")?;
|
||||
read_direct_project_chat_history_at(root)
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
let root = Path::new(project_path.trim());
|
||||
enforce_project_permission_policy(root, "conversation.read")?;
|
||||
read_direct_project_chat_history_at(root)
|
||||
})
|
||||
.await
|
||||
.map_err(|error| format!("读取 DirectProject 历史后台任务失败:{error}"))?
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
||||
@@ -34,6 +34,8 @@ const PLUGIN_PROTOCOL_VERSION: &str = "agc.plugin.v1";
|
||||
const PLUGIN_API_VERSION: &str = "v1";
|
||||
const AUDIT_FILE_NAME: &str = "audit.jsonl";
|
||||
const RPC_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
// 编辑器连接可包含受控引导;外层必须覆盖引导和编辑器命令的完整期限。
|
||||
const EDITOR_RPC_TIMEOUT: Duration = Duration::from_secs(90);
|
||||
const MAX_MANIFEST_BYTES: u64 = 1024 * 1024;
|
||||
const MAX_RPC_BYTES: usize = 2 * 1024 * 1024;
|
||||
|
||||
@@ -1134,7 +1136,7 @@ impl PluginHost {
|
||||
}
|
||||
|
||||
pub(crate) fn call(&self, id: &str, method: String, params: Value) -> Result<Value, String> {
|
||||
let (root, request_id, response_receiver, pending, writer) = {
|
||||
let (root, request_id, response_receiver, pending, writer, response_timeout) = {
|
||||
let mut state = self
|
||||
.state
|
||||
.lock()
|
||||
@@ -1143,10 +1145,18 @@ impl PluginHost {
|
||||
.root
|
||||
.clone()
|
||||
.ok_or_else(|| "插件宿主尚未初始化".to_string())?;
|
||||
let running = state
|
||||
let record = state
|
||||
.plugins
|
||||
.get_mut(id)
|
||||
.and_then(|record| record.running.as_mut())
|
||||
.ok_or_else(|| "插件不存在".to_string())?;
|
||||
let response_timeout = if record.manifest.adapter.is_some() {
|
||||
EDITOR_RPC_TIMEOUT
|
||||
} else {
|
||||
RPC_TIMEOUT
|
||||
};
|
||||
let running = record
|
||||
.running
|
||||
.as_mut()
|
||||
.ok_or_else(|| "插件尚未启动".to_string())?;
|
||||
let request_id = running.next_request_id;
|
||||
running.next_request_id = request_id
|
||||
@@ -1169,9 +1179,10 @@ impl PluginHost {
|
||||
receiver,
|
||||
Arc::clone(&running.pending),
|
||||
Arc::clone(&running.stdin),
|
||||
response_timeout,
|
||||
)
|
||||
};
|
||||
let deadline = Instant::now() + RPC_TIMEOUT;
|
||||
let deadline = Instant::now() + response_timeout;
|
||||
let (write_sender, write_receiver) = mpsc::channel();
|
||||
thread::spawn(move || {
|
||||
let _ = write_sender.send(write_rpc_shared(
|
||||
|
||||
@@ -2696,20 +2696,10 @@ fn local_conversation_command_message_id_is_idempotent() {
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_conversation_read_respects_project_policy() {
|
||||
#[tokio::test]
|
||||
async fn local_conversation_read_respects_project_policy() {
|
||||
let root = unique_project_path();
|
||||
init_local_game_project_at(&root, "project-1", "像素动作原型").expect("project init");
|
||||
append_local_conversation_message_at(
|
||||
&root,
|
||||
None,
|
||||
LocalConversationMessage {
|
||||
role: "user".to_string(),
|
||||
content: "hello".to_string(),
|
||||
agent_id: None,
|
||||
},
|
||||
)
|
||||
.expect("append conversation");
|
||||
write_project_permission_policy_at(
|
||||
&root,
|
||||
ProjectPermissionPolicy {
|
||||
@@ -2721,6 +2711,7 @@ fn local_conversation_read_respects_project_policy() {
|
||||
.expect("write policy");
|
||||
|
||||
let error = read_local_conversation(root.to_string_lossy().into_owned(), None, None)
|
||||
.await
|
||||
.expect_err("conversation read denied");
|
||||
assert!(error.contains("项目权限策略拒绝执行:conversation.read"));
|
||||
|
||||
|
||||
@@ -10942,6 +10942,7 @@ export function App({
|
||||
const nextProjectPath = localProject?.projectPath ?? null;
|
||||
if (
|
||||
!projectSupervisorOnly ||
|
||||
directCodexProductRuntime ||
|
||||
!invoke ||
|
||||
!nextProjectPath ||
|
||||
professionalResultCandidates.length === 0
|
||||
@@ -11023,6 +11024,7 @@ export function App({
|
||||
// The semantic candidate key replaces the freshly allocated candidates array.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
directCodexProductRuntime,
|
||||
localProject?.projectPath,
|
||||
professionalResultCandidateKey,
|
||||
projectSupervisorOnly,
|
||||
|
||||
@@ -6061,6 +6061,16 @@ export function registerProjectSupervisorSurfaceTests() {
|
||||
await Promise.resolve();
|
||||
});
|
||||
expect(professionalRuntimeReadCount).toBe(0);
|
||||
await waitFor(() =>
|
||||
expect(invoke).toHaveBeenCalledWith('read_direct_project_conversation', {
|
||||
projectPath,
|
||||
}),
|
||||
);
|
||||
// 默认任务占位行也不能触发专业 Agent 历史的批量读取。
|
||||
expect(invoke).not.toHaveBeenCalledWith(
|
||||
'read_local_conversation',
|
||||
expect.anything(),
|
||||
);
|
||||
expect(invoke).not.toHaveBeenCalledWith(
|
||||
'read_game_creator_agent_runtimes',
|
||||
expect.anything(),
|
||||
|
||||
@@ -8206,11 +8206,11 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
|
||||
|
||||
## 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 或项目扩展自动写入。
|
||||
- Cocos Creator bridge 核心位于 `plugins/agc-cocos-editor/native/cocos-editor-bridge`,与 Tauri、Agent Runtime 和服务端解耦;crate 默认 feature 关闭,Windows 标准客户端开发/发行脚本均启用 `cocos-editor-execute`,包含 `windows-bootstrap`。
|
||||
- 进程发现只用于把 CocosCreator 主进程 PID 与 `--project` 和 Creator 版本绑定,排除 Electron 子进程;主进程查询一次后复用已验证目标,不在单次执行链重复枚举。
|
||||
- execute/connect 复用就绪 pipe,否则通过目标 PID 的回环 Node Inspector 安装编译内置 bootstrap;检查 debug handler 的映像归属及 Inspector 的进程、项目、版本,握手成功后才发布连接和发送业务代码。只关闭本次开启的 Inspector,不写项目扩展。
|
||||
- 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 技术方案。
|
||||
- DirectProject 的 `agc_tools` 对应入口是 `agc_cocos_execute`,只接收 code,并沿用当前项目权限。引导在普通线程运行;执行发送后的未知结果禁止自动重放,Direct bridge 会阻断后续 execute。不能把插件进程启动、PID 识别或 DLL 加载当作握手成功。详细版本/fuse、端口与验收边界见 Cocos bridge 技术方案。
|
||||
|
||||
## 2026-09-10 Cocos 直连模块改为插件包与独立插件工作区
|
||||
|
||||
@@ -8229,6 +8229,11 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
|
||||
- 决策:禁用时先停止运行中的插件进程并让 `start_agc_plugin` 失败;同时把对应 Runtime 工具从 `agent_runtime_executable_tools()` 移除,使其不再进入工具策略快照、原生函数目录和系统提示词工具目录,DirectProject 的 `agc_tools` 规格与 bridge 执行入口同步拒绝。启用后立即恢复,不需要重启客户端。
|
||||
- 边界:导入扩展的启用状态仍走既有 `set_client_extension_enabled` 和扩展索引,不并入内置插件开关文件;内置插件开关不改变 manifest、权限或审计协议。
|
||||
- 验证:`builtin_plugins` 单测覆盖默认值、持久化往返、坏文件失败关闭和“禁用后工具目录不再出现该工具”;`plugin_host` 单测覆盖禁用后不能启动、导入 id 被拒绝、启用后回到 stopped。
|
||||
|
||||
## 2026-09-12 Cocos execute 不获取 AGC 项目写锁
|
||||
|
||||
- 背景:早期 Cocos 直连模块让 `agc_cocos_execute` 复用 `.agent/project.lock`,意图是把可能通过 Creator API 修改工程的执行与文件写入串行化;实际的控制台执行通过已校验的 Inspector / pipe 进入 Creator,`console.log` 等操作不写 AGC 文件,导致历史、manifest 或 revision 写入期间被错误拒绝。
|
||||
- 决策:`agc_cocos_execute` 保留 `cocos.editor.execute` 权限、Creator PID / 项目 / 版本校验和不确定结果阻断,但不获取 `.agent/project.lock`;真正的 `agc_write_file`、manifest、revision 和资源持久化继续使用项目写锁。Cocos 执行与文件写入的并发安全由 Creator 自身事件循环和各写入入口分别负责。
|
||||
## 2026-09-10 Direct 写通道纳入统一项目锁等待窗口并补齐持锁方可诊断
|
||||
|
||||
- 背景:Issue #318。`agc_write_file` 是用户直接触发、失败即整轮无法落盘的项目写入通道,却用零等待取锁,任何重叠都在 24-42ms 内被投影成“项目正在被其他写操作占用”;同一形状已在 2026-07-22 由 `file.write / file.patch / file.delete` 用有界等待修过,本项目技术方案的 2026-08-13 一节也已规定这类争用结果“统一投影为争用并进入既有有界等待”。现场取证还缺 `commandId / pid / createdAt / ownerIsSelf`,无法回答“谁在持锁”,加上 `create_new` 把 ACL 拒绝、delete-pending 和真实跨进程争用压成同一句话,排障被引向“残留锁”。
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
# 踩坑与排障记录
|
||||
|
||||
## 2026-09-12 Cocos 请求不得回退到项目内 MCP 扩展
|
||||
|
||||
AGC 的 Cocos 能力来自随客户端分发的 `agc-cocos-editor` 内置插件,工具名为 `cocos.editor.execute` / `agc_cocos_execute`。Cocos 项目中的 `extensions/`、`package.json` 插件声明和第三方 MCP 包不是桥接来源;内置工具不可用时必须报告客户端插件状态,不能扫描、安装、启用或要求用户打开项目内 MCP 面板。
|
||||
|
||||
## 2026-09-12 打开 DirectProject 时批量读取专业 Agent 历史导致窗口无响应
|
||||
|
||||
- 工作区的专业 Agent 文本回执 effect 不能只检查 `projectSupervisorOnly`:DirectProject 同样使用这个工作区壳,默认任务占位行会触发无关的 `read_local_conversation` 批量调用。
|
||||
- 同步 Tauri command 内的权限校验、会话目录扫描和锁等待会占用窗口线程。DirectProject 必须跳过专业 Agent 历史;开发入口仍需要的对话读取在 blocking worker 中执行,权限校验保留在同一后台闭包内。
|
||||
- 排障测量完整 IPC 链路并同步采样原生窗口响应。某命令的调用端耗时可能包含前面的主线程队列等待,不能仅凭调用端耗时认定插件启动或上游请求本身缓慢。
|
||||
|
||||
> 当前口径:本文件保留可复用的排障经验;历史条目的旧路由、旧版本和已删除文档仅作根因背景,不得据此恢复退役入口。当前命令、路由和 schema 以代码与 `docs/README.md` 为准。
|
||||
|
||||
## 2026-09-05 Planning V2 审批和续跑必须等过项目锁瞬时争用
|
||||
@@ -41,6 +51,12 @@
|
||||
首页命名回合成功后创建命令失败且不会留下项目目录。用户通过目录选择器创建的
|
||||
项目仍走 user-selected 权限范围。
|
||||
|
||||
## 2026-09-12 Cocos 项目识别不等于编辑器桥就绪
|
||||
|
||||
- 现象:能发现正确 Creator PID、Agent 也有 `agc_cocos_execute`,但首次执行报 pipe 不存在;仅登记目标的 `connect` 会误报成功。
|
||||
- 处理:Windows execute/connect 先统一复用 pipe 或通过目标 PID 的 Inspector 引导,握手通过后才发布连接或发送代码;开发与发布脚本均默认带 `cocos-editor-execute`。Node 规范化前要处理 Rust 扩展路径;成功安装后只关闭本次开启的 Inspector。
|
||||
- 验证:必须分别跑自有进程冷启动/已有 Inspector/超时回归与真实 Creator 首次连接;只跑 fixture 或开启 feature 不能证明真实链路可用。
|
||||
|
||||
## 2026-09-11 Cocos 项目必须走独立导入分支
|
||||
|
||||
Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/` 目录识别;
|
||||
@@ -4884,6 +4900,12 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/`
|
||||
- 处理:同时隔离 `HOME / USERPROFILE / APPDATA / LOCALAPPDATA`,并在临时 workspace 创建空 `.git` 作为仓库发现边界,防止继续向父目录(例如 `/tmp`)发现 `.codex/.agents`;启动前设置 `web_search="disabled"`、`agents.enabled=false`,并关闭 shell/unified exec/browser/plugin/image/workspace dependency 等原生 feature;接收 `item/started` 时只允许消息、计划、推理和压缩等被动 item,其余立即 interrupt。配置中的 `webSearchEnabled=true` 必须失败关闭并提示切 `provider`。
|
||||
- 验证:fake app-server 检查 argv 不含 Key、专用 Key 只在环境、继承 `CODEX_API_KEY` 被移除、HOME 指向临时目录、web/multi-agent/shell 关闭;另覆盖 turn-start 回包前 drop 最终只发一次对应 interrupt。
|
||||
|
||||
## 2026-09-12 app-server `other` 不代表 dev 上游故障
|
||||
|
||||
- 现象:客户端显示 `codex-app-server-error:other`,但 DirectProject 的项目历史没有 `agc_cocos_execute` item。
|
||||
- 证据边界:dev `/api/llm/models`、流式 `/api/llm/responses` 和带 `agc_cocos_execute` 工具的 Responses 探针均可返回成功;这只能证明 dev 契约和模型路由可用,不能证明客户端本次请求已经到达 dev。
|
||||
- 处理:app-server 失败分类必须优先读取安全的 `message` / `additionalDetails` / `codexErrorInfo`,把 `stream must be true`、超时、鉴权、请求过大和连接断开投影为稳定类别;禁止把上游正文、token、URL 查询参数写入日志。DirectProject 工具调用只有在 `turn/start` 成功后才会出现,不能用“没有工具 item”反推 Cocos bridge 失败。
|
||||
|
||||
## 多 Agent 共享一个 Codex app-server 会放大单点终态丢失(2026-08-10)
|
||||
|
||||
- 现象:多个节点最初已有 `started -> completed`,随后一个 app-server stdio 连接关闭,同一秒多个仍在途节点一起进入 `needs-reconciliation`;单看 threadId 不同会误以为节点已经进程隔离。
|
||||
|
||||
@@ -2,7 +2,17 @@
|
||||
|
||||
## 目标
|
||||
|
||||
目标是在用户已打开 Cocos Creator 项目时,由 AGC 识别正确的 Creator 主进程,并在进程内注入随包 JavaScript bootstrap;用户不需要在 Cocos 项目中手动安装扩展。桥接核心独立于 Tauri,随 Cocos 插件包分发,位于 `plugins/agc-cocos-editor/native/cocos-editor-bridge`。2026-09-10 已验证通过 Node 自带的运行中 Inspector 激活入口完成引导,具体见本文“Inspector 注入调研”;这条路径尚未替换当前 crate 的 DLL 实现。
|
||||
用户打开 Cocos Creator 项目后,AGC 在正确的 Creator 主进程内安装随包 JavaScript bootstrap;用户无需手动安装项目扩展。核心位于 `plugins/agc-cocos-editor/native/cocos-editor-bridge`,通过 Node Inspector 引导、通过 named pipe 执行业务代码。
|
||||
|
||||
## 2026-09-12 冷启动连接合同
|
||||
|
||||
`cocos-editor-execute` 同时启用 `windows-bootstrap`:DirectProject、Runtime 和插件适配器的 execute 在发送业务代码前统一验证项目/PID,复用已就绪 pipe,否则通过目标进程的 Node Inspector 安装编译进 crate 的 bootstrap。引导只允许目标 PID 所有的回环 Inspector;再次检查 Inspector 返回的 PID、可执行文件、项目路径和 Creator 版本。Windows 调试 handler 必须位于目标可执行映像的可执行内存中,不允许任意地址、外部端口或 DLL 输入。
|
||||
|
||||
bootstrap 安装与业务 execute 分离,安装只发送一次,随后等待带正确身份的 `ping` 回执。`connect` 仅在该握手成功后保存连接并返回 `connected: true`。本次开启的 Inspector 在断开 WebSocket 后通过 pipe 关闭;已有 Inspector 保持原状。bootstrap 超时或握手失败不得发送业务代码;已发送业务代码的超时仍按原有 `needs-reconciliation` 合同禁止重放。
|
||||
|
||||
Windows 开发和发行构建均默认启用 `cocos-editor-execute`,明确的 feature 参数或开发 feature 环境覆盖仍受尊重。验收必须分别覆盖默认/启用 feature 编译、协议回归、真实 Creator 无预装桥的首次连接与后续执行;fixture 通过不等同于真实编辑器验收。
|
||||
|
||||
插件宿主的普通 RPC 保持 10 秒写入/响应期限;声明编辑器适配器的插件响应期限为 90 秒,写入仍限 10 秒。Cocos 插件内部期限为 85 秒,覆盖最长 15 秒引导与 60 秒命令,先于宿主截止。DirectProject 与 Runtime 继续使用自身的工具期限和不确定结果阻断。
|
||||
|
||||
## 边界
|
||||
|
||||
@@ -10,10 +20,10 @@ Cocos Creator 3.x 是 Electron/Node 编辑器,不能复用 Unity Mono 的 Core
|
||||
|
||||
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)`,不写入项目扩展目录。
|
||||
3. 在 `windows-bootstrap` 下,按内核 TCP 表选择目标 PID 的回环 Inspector,必要时激活目标主映像内的 Node debug handler。跨进程命名互斥锁串行化同一 PID 的引导,避免 GUI、Runner 和插件同时安装。
|
||||
4. 提供 `ping/status/execute` 协议、Windows pipe 客户端和编译进 crate 的 `payload/bootstrap.cjs`。bootstrap 在 Node 主上下文调用 `install(Editor)`,不写入项目扩展目录。Rust 的 `\\?\` 盘符和 UNC 路径在进入 Node `realpathSync` 前转换为普通路径。
|
||||
|
||||
当前 DLL 实现仍只返回 `injected-unverified`,不能作为可交付的注入入口。它在 `windows-injection` 下构建 C++ payload 并尝试通过窗口钩子调用 V8;其中 `RequestInterrupt → run_bootstrap → Script::Run` 违反 V8 的中断回调约束,不能因编译或 DLL 加载成功而认定安全可用。`HandleScope` 的存储大小和 C++ Local/MaybeLocal 的调用约定也不能通过裸指针替代来推断。后续接入采用下述 Inspector 引导,不再依赖这条未经验证的 native 路径。
|
||||
单独的 `windows-injection` DLL 实验入口仍只返回 `injected-unverified`,不能作为正式连接或验收依据。其中 `RequestInterrupt → Script::Run` 不满足 V8 中断回调约束;标准开发、发行、connect 和 execute 均使用 Inspector 引导,不启用该实验入口。
|
||||
|
||||
## Feature 开关
|
||||
|
||||
@@ -25,13 +35,14 @@ cocos-editor-bridge = { path = ".../plugins/agc-cocos-editor/native/cocos-editor
|
||||
|
||||
- `process-discovery`:启用 Windows Creator 主进程发现;不加载 Windows 注入 API。
|
||||
- `windows-transport`:在已注入 payload 后启用本机 named pipe 的 `ping/status/execute` 命令传输。
|
||||
- `windows-bootstrap`:启用 `windows-transport` 和 Windows x64 Node Inspector 引导;仅桌面目标引入 HTTP/WebSocket 和进程 API。
|
||||
- `windows-injection`:隐含启用 `windows-transport`,并启用 Windows native DLL 注入实现。
|
||||
|
||||
AGC 或其它桌面宿主应将 `windows-injection` 作为单独的发行构建开关,服务端和非桌面构建保持 `default-features = false`。
|
||||
标准 Windows 客户端使用 `windows-bootstrap`;服务端和非桌面构建保持 `default-features = false`。
|
||||
|
||||
AGC 不再内置 Cocos 专属 Tauri 命令。适配器 `cocos-editor` 由插件包 `plugins/agc-cocos-editor` 提供,实现通用 `EditorAdapter`(`prepare` / `inject` / `ping` / `status` / `execute` / `detect` / `connect` / `disconnect`),由宿主按 manifest 的 `adapter` 字段注册;插件入口通过 `host.rpc` 触发这些操作,宿主校验 `editor.rpc` 权限后路由到 native 模块。
|
||||
|
||||
Runtime 只广告一个 `cocos.editor.execute` 工具,代码输入使用当前项目根,目标 PID 由 crate 内部唯一匹配;默认命令权限为 confirm,具体运行档沿用已有 Runtime 策略。进程发现留在 crate 内部作为目标校验步骤,不建立客户端扫描服务或独立发现入口。默认 AGC 构建不启用 Cocos 集成;桌面构建需显式传 `--features cocos-editor`,命令执行需传 `--features cocos-editor-execute`,注入构建再传 `--features cocos-editor-injection`。注入只加载插件包 payload 目录里的 `cocos-editor-bridge.dll`(打包后位于 `<resource_dir>/plugins/agc-cocos-editor/native/payload`),适配器拒绝任何其它路径,避免变成任意 DLL 注入器。
|
||||
Runtime 只广告 `cocos.editor.execute`,代码输入使用当前项目根,目标 PID 由 crate 内部唯一匹配;具体权限沿用已有 Runtime 策略。Windows 的标准 dev/release 脚本统一默认选择 `cocos-editor-execute`,它包含 `windows-bootstrap`;显式 feature 参数优先。纯 Cargo 默认 feature 仍为空。进程发现只用于内部目标校验,不建立客户端扫描服务,不向模型公开 PID、端口或 Inspector。
|
||||
|
||||
## 插件包形态
|
||||
|
||||
@@ -59,7 +70,7 @@ Cocos Creator 项目,并将其标记为 `cocos` 项目类型。选择目录后
|
||||
|
||||
## 第一阶段命令协议
|
||||
|
||||
DirectProject 的现役 `agc_tools` 目录通过 Windows `cocos-editor-execute` feature 注册 `agc_cocos_execute`,参数只有 `code`。客户端在 blocking worker 内调用插件 native 模块,保留项目锁和现有项目权限;当前 bridge 出现执行结果不确定后拒绝后续 execute。旧 Runtime 的对应工具名为 `cocos.editor.execute`,继续使用它已有的 pending action、权限和恢复语义;插件入口注册的同名命令走宿主 `host.rpc` → `EditorAdapter` 路径,两条路径共享同一 native 实现和不确定结果阻断语义。
|
||||
DirectProject 的现役 `agc_tools` 目录通过 Windows `cocos-editor-execute` feature 注册 `agc_cocos_execute`,参数只有 `code`。客户端在 blocking worker 内调用插件 native 模块,执行前检查现有项目权限但不获取 `.agent/project.lock`;该入口只通过 Inspector / pipe 操作已打开的 Creator,文件写入工具仍独立使用项目锁。当前 bridge 出现执行结果不确定后拒绝后续 execute。旧 Runtime 的对应工具名为 `cocos.editor.execute`,继续使用它已有的 pending action、权限和恢复语义;插件入口注册的同名命令走宿主 `host.rpc` → `EditorAdapter` 路径,两条路径共享同一 native 实现和不确定结果阻断语义。
|
||||
|
||||
注入 payload 在目标 Creator 主进程内监听 `\\.\pipe\genarrative-cocos-editor-{pid}`,使用换行分隔的 JSON。crate 只生成三种操作:
|
||||
|
||||
@@ -83,11 +94,11 @@ Rust 客户端在写入前通过 `GetNamedPipeServerProcessId` 验证 pipe 属
|
||||
- 注入超时不会释放仍可能被远程线程使用的内存,并返回人工核对错误,避免在不确定状态下破坏目标进程。
|
||||
- 非 Windows、feature 未启用、目标不存在、身份不匹配或 payload 不合规均直接失败,不启动 Cocos、不关闭 Cocos、不修改项目文件。
|
||||
|
||||
## 后续接入
|
||||
## 验收
|
||||
|
||||
下一步是在独立 crate 内实现 Inspector 引导并替换 DLL 装载入口,保留 feature 开关、项目/PID 绑定和 `ping/status/execute` 三条命令。进程发现仍只是内部目标校验,不增加客户端扫描服务。隔离验证脚本使用 Node 的 `_debugProcess`;正式 Rust 实现可直接调用同一组 Win32 API,无需附带额外 Node 运行时。源码接入、默认/启用 feature 编译和 AGC 发行包验收仍未完成。
|
||||
正式引导由 Rust 调用 Win32、HTTP 和 WebSocket 完成,不额外启动 Node 助手。执行 `cargo test --manifest-path plugins/agc-cocos-editor/native/cocos-editor-bridge/Cargo.toml --features windows-bootstrap` 验证身份、路径、状态与协议。追加 `-- --ignored` 实际运行自有 Node 进程的冷启动、保留已有 Inspector 和 pipe 超时回归;fixture 进程由测试独占并回收。
|
||||
|
||||
当前验证:crate 全 feature 单元测试、Node bootstrap fixture、Rust 到 Node bootstrap 的真实命名管道回环(含 execute 超时不确定结果)通过;AGC feature 编译使用临时工作树与 Codex 资源替身,只属于编译检查,不代表发布包或真实 Creator 验收。当前工作区完整 AGC 检查受缺失内置 Codex CLI 阻断,TypeScript 契约测试受缺失 Vitest 阻断。
|
||||
真实 Creator 使用 `cargo run --manifest-path plugins/agc-cocos-editor/native/cocos-editor-bridge/Cargo.toml --features windows-bootstrap --example creator_smoke -- <已打开的项目绝对路径>`。该入口只返回 PID、项目、版本和 Inspector 状态,验证首次执行、复用和 connect/ping,不写项目文件。初始 `before=NotReady` 才证明冷启动;后续只有 warm 成功不能替代冷启动验收。源码编译、真实 Creator smoke、发行包构建与安装包 smoke 分别报告。
|
||||
|
||||
## Inspector 注入调研(2026-09-10)
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ OpenAI 的标准模型是“Plugin 作为可安装包,组合 Skills、可选 M
|
||||
|
||||
宿主以已安装插件目录为 cwd 启动入口;JavaScript 入口使用系统 `node` 执行,其它入口直接执行。环境先清空,再保留 PATH、Windows 系统目录和临时目录等必要变量,并注入插件身份和协议版本;不继承客户端凭据。Windows 复用进程模块的 Job Object,Unix 使用独立进程组,停止/卸载时回收自有进程。
|
||||
|
||||
stdin/stdout 使用一行一个 JSON-RPC 2.0 消息,单条消息限制 2 MiB,队列和并发请求有上限;独立消息循环持续处理注册请求、事件和响应。写入与响应共享 10 秒期限,写入阻塞只终止对应运行实例。宿主 API 权限用于约束 `host.*` 调用;Runtime Plugin 是用户主动启动的本地程序,这不是 OS 沙箱。
|
||||
stdin/stdout 使用一行一个 JSON-RPC 2.0 消息,单条消息限制 2 MiB,队列和并发请求有上限;独立消息循环持续处理注册请求、事件和响应。普通 RPC 写入与响应共享 10 秒期限;声明编辑器适配器的插件允许 90 秒响应期限以覆盖连接引导与编辑器执行,写入仍限 10 秒。写入阻塞只终止对应运行实例。宿主 API 权限用于约束 `host.*` 调用;Runtime Plugin 是用户主动启动的本地程序,这不是 OS 沙箱。
|
||||
|
||||
SDK 对插件暴露稳定的通用 API:
|
||||
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# AI 游戏创作智能体 App 实施计划
|
||||
|
||||
## 2026-09-12 已有项目打开响应性
|
||||
|
||||
DirectProject 工作区只恢复自身对话,不按专业 Agent 默认任务占位行批量读取旧会话或生成专业 Agent 文本回执。专业 Agent 结果加载 effect 必须以当前 Runtime 模式为边界,并在模式切换时清空旧结果。仍供开发入口使用的 `read_local_conversation` 在 blocking worker 内完整执行权限校验、会话目录解析和历史读取,避免文件访问或锁等待阻塞 Tauri 窗口线程。
|
||||
|
||||
DirectProject 自身的 `read_direct_project_conversation` 也必须在 blocking worker 中执行权限校验、JSONL 历史解析和消息投影,不能因为它只读取一份项目历史就保留同步 Tauri command。
|
||||
|
||||
验收覆盖实际 Launcher 打开已有项目:恢复 DirectProject 对话且不调用 `read_local_conversation`;后台读取仍遵守项目权限策略。原生客户端重复打开同一已有项目时验证窗口响应,IPC 测量只记录命令名和耗时,不记录会话内容。
|
||||
|
||||
## 图片生成恢复与测试边界
|
||||
|
||||
已有持久生成账本的 Provider 待执行动作恢复时,若动作省略了旧视觉 Agent 自动补齐的参数,只在 Agent、动作身份、生成种类和冻结提示词均匹配旧合同后补齐缺省参数;显式参数不得被覆盖。新请求继续按当前自由图片合同执行,不能重新引入固定视觉产物门禁。恢复复用原 operation 与幂等账本,不因默认值变化重复提交已受理请求。
|
||||
|
||||
@@ -7,6 +7,10 @@ RPC、权限和能力注册全部由通用宿主负责,只有“如何连接 C
|
||||
“是否可用”,禁用后插件不能启动,`cocos.editor.execute` / `agc_cocos_execute` 也会
|
||||
从 Agent 工具列表、工具策略快照和上下文里消失;重新启用后立即恢复。
|
||||
|
||||
Cocos Creator 项目目录中的 `extensions/`、`package.json` 插件声明或第三方 MCP 包不属于
|
||||
AGC Cocos 桥接来源。Agent 处理 Cocos 请求时只使用客户端登记的 `agc-cocos-editor`
|
||||
内置插件;内置工具不可用时报告客户端插件状态,不扫描或要求用户启动项目内 MCP 扩展。
|
||||
|
||||
```text
|
||||
plugin.json Agent Plugins 清单 + AGC Runtime 扩展
|
||||
src/entry.mjs 运行时入口(注册命令 / 能力 / 面板,转发 host.rpc)
|
||||
@@ -54,6 +58,8 @@ execute 不接受并发积压。结果不确定时返回 `needs-reconciliation`
|
||||
- `windows-transport`:注入后通过 named pipe 提供 `ping/status/execute`,执行结果
|
||||
不确定时返回 `ExecutionUncertain` 并禁止自动重放。
|
||||
- `windows-injection`:随包 DLL 的 Windows 注入实现,默认关闭。
|
||||
- `windows-bootstrap`:在首次 connect/execute 前经目标 PID 的 Node Inspector
|
||||
安装内置 bootstrap,验证 pipe 握手并关闭本次开启的 Inspector;保留已有调试会话。
|
||||
|
||||
AGC 客户端当前在编译期链接本 crate(Cargo path 依赖),由通用宿主按 manifest 的
|
||||
`adapter` 字段注册;宿主源码里没有 Cocos 进程名、注入或 Editor.Message 逻辑。
|
||||
@@ -62,6 +68,7 @@ AGC 客户端当前在编译期链接本 crate(Cargo path 依赖),由通
|
||||
|
||||
```bash
|
||||
cargo test --manifest-path plugins/agc-cocos-editor/native/cocos-editor-bridge/Cargo.toml
|
||||
cargo test --manifest-path plugins/agc-cocos-editor/native/cocos-editor-bridge/Cargo.toml --features windows-bootstrap -- --include-ignored
|
||||
node --test plugins/agc-cocos-editor/src/entry.test.mjs
|
||||
```
|
||||
|
||||
|
||||
@@ -33,6 +33,16 @@ windows-injection = [
|
||||
"windows-sys/Win32_System_LibraryLoader",
|
||||
"windows-sys/Win32_System_Memory",
|
||||
]
|
||||
windows-bootstrap = [
|
||||
"windows-transport",
|
||||
"dep:reqwest",
|
||||
"dep:tungstenite",
|
||||
"windows-sys/Win32_NetworkManagement_IpHelper",
|
||||
"windows-sys/Win32_Networking_WinSock",
|
||||
"windows-sys/Win32_System_Memory",
|
||||
"windows-sys/Win32_System_ProcessStatus",
|
||||
"windows-sys/Win32_System_SystemInformation",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
editor-adapter-api = { path = "../../../../server-rs/crates/editor-adapter-api" }
|
||||
@@ -42,6 +52,8 @@ sha2 = { version = "0.10", optional = true }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows-sys = { version = "0.61", optional = true, default-features = false }
|
||||
reqwest = { version = "0.12", optional = true, default-features = false, features = ["blocking", "json"] }
|
||||
tungstenite = { version = "0.28", optional = true, default-features = false, features = ["handshake"] }
|
||||
|
||||
[build-dependencies]
|
||||
cc = "1"
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
//! 对用户指定的已打开 Creator 项目执行只读冷启动/复用验证,不写项目文件。
|
||||
use cocos_editor_bridge::{
|
||||
execute_cocos_editor_code_for_project, ping_cocos_editor, CocosEditorAdapter,
|
||||
};
|
||||
use editor_adapter_api::EditorAdapter;
|
||||
use std::path::Path;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let project = std::env::args()
|
||||
.nth(1)
|
||||
.ok_or("需要显式提供已打开的 Cocos 项目目录")?;
|
||||
let mut adapter = CocosEditorAdapter::default();
|
||||
let target = adapter.detect(Path::new(&project))?;
|
||||
let pid = target.pid.ok_or("没有匹配的 Creator")?;
|
||||
println!("detected={}", serde_json::to_string(&target)?);
|
||||
println!("before={:?}", ping_cocos_editor(pid, &project, 40));
|
||||
for phase in ["first", "warm"] {
|
||||
let response = execute_cocos_editor_code_for_project(&project,
|
||||
"return {pid:process.pid,version:Editor.App.version,project:Editor.Project.path,inspectorOpen:!!require('node:inspector').url()};", 3000)?;
|
||||
if !response.ok {
|
||||
return Err(format!("{phase}: {:?}", response.error).into());
|
||||
}
|
||||
println!("{phase}={}", serde_json::to_string(&response)?);
|
||||
}
|
||||
let connected = adapter.connect(
|
||||
pid,
|
||||
Path::new(&project),
|
||||
target.version.as_deref().unwrap_or(""),
|
||||
)?;
|
||||
assert!(connected.connected);
|
||||
assert!(ping_cocos_editor(pid, &project, 500)?.ok);
|
||||
println!("connected={}", serde_json::to_string(&connected)?);
|
||||
Ok(())
|
||||
}
|
||||
@@ -17,7 +17,11 @@ function normalizeProjectPath(value) {
|
||||
if (typeof value !== 'string' || !path.isAbsolute(value)) {
|
||||
throw new Error('projectPath must be absolute');
|
||||
}
|
||||
const canonical = fs.realpathSync(value).replace(/^\\\\\?\\/, '');
|
||||
// Rust canonicalize 的扩展路径必须先转回 Node 可解析的盘符/UNC 形式。
|
||||
const ordinary = value
|
||||
.replace(/^\\\\\?\\UNC\\/i, '\\\\')
|
||||
.replace(/^\\\\\?\\/, '');
|
||||
const canonical = fs.realpathSync(ordinary);
|
||||
return process.platform === 'win32' ? canonical.toLowerCase() : canonical;
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,13 @@ test('Creator bootstrap executes async Editor code and keeps a three-command sur
|
||||
}
|
||||
try {
|
||||
assert.equal((await request({ op: 'ping' })).result.ready, true);
|
||||
if (process.platform === 'win32') {
|
||||
assert.equal(
|
||||
(await request({ op: 'ping' }, { projectPath: `\\\\?\\${__dirname}` }))
|
||||
.ok,
|
||||
true,
|
||||
);
|
||||
}
|
||||
assert.equal(
|
||||
(await request({ op: 'status' })).result.creatorVersion,
|
||||
'test',
|
||||
|
||||
@@ -120,6 +120,37 @@ impl CocosEditorAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
fn connect_with(
|
||||
&self,
|
||||
process: crate::CocosEditorProcess,
|
||||
handshake: impl FnOnce() -> Result<(), String>,
|
||||
) -> Result<EditorConnectionInfo, String> {
|
||||
let mut connection = self
|
||||
.connection
|
||||
.lock()
|
||||
.map_err(|_| "Cocos 适配器连接状态锁已损坏".to_string())?;
|
||||
*connection = None;
|
||||
handshake()?;
|
||||
let info = self.connected_info(&process);
|
||||
*connection = Some(CocosEditorConnection {
|
||||
process_id: process.process_id,
|
||||
project_path: process
|
||||
.project_path
|
||||
.clone()
|
||||
.ok_or_else(|| "目标缺少项目路径".to_string())?,
|
||||
creator_version: process.creator_version.clone(),
|
||||
});
|
||||
Ok(info)
|
||||
}
|
||||
|
||||
fn connect_target(&self, pid: u32, project_path: &str) -> Result<EditorConnectionInfo, String> {
|
||||
let target =
|
||||
validate_injection_target(pid, project_path).map_err(|error| error.to_string())?;
|
||||
self.connect_with(target.clone(), || {
|
||||
crate::ensure_target_ready(&target).map_err(|error| error.to_string())
|
||||
})
|
||||
}
|
||||
|
||||
/// 只允许加载插件包随包目录里的 payload,避免适配器变成任意 DLL 注入器。
|
||||
fn payload_path(&self, requested: Option<&str>) -> Result<PathBuf, String> {
|
||||
match requested {
|
||||
@@ -286,22 +317,7 @@ impl EditorAdapter for CocosEditorAdapter {
|
||||
project_path: &Path,
|
||||
_version: &str,
|
||||
) -> Result<EditorConnectionInfo, String> {
|
||||
let process = validate_injection_target(pid, &project_path.to_string_lossy())
|
||||
.map_err(|error| error.to_string())?;
|
||||
let connection = CocosEditorConnection {
|
||||
process_id: process.process_id,
|
||||
project_path: process
|
||||
.project_path
|
||||
.clone()
|
||||
.unwrap_or_else(|| project_path.to_string_lossy().into_owned()),
|
||||
creator_version: process.creator_version.clone(),
|
||||
};
|
||||
let info = self.connected_info(&process);
|
||||
*self
|
||||
.connection
|
||||
.lock()
|
||||
.map_err(|_| "Cocos 适配器连接状态锁已损坏".to_string())? = Some(connection);
|
||||
Ok(info)
|
||||
self.connect_target(pid, &project_path.to_string_lossy())
|
||||
}
|
||||
|
||||
fn disconnect(&mut self) {
|
||||
@@ -328,21 +344,7 @@ impl EditorAdapter for CocosEditorAdapter {
|
||||
}
|
||||
"connect" => {
|
||||
let project_path = params.project_path()?;
|
||||
let process = validate_injection_target(params.process_id()?, &project_path)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let connection = CocosEditorConnection {
|
||||
process_id: process.process_id,
|
||||
project_path: process
|
||||
.project_path
|
||||
.clone()
|
||||
.unwrap_or_else(|| project_path.clone()),
|
||||
creator_version: process.creator_version.clone(),
|
||||
};
|
||||
let info = self.connected_info(&process);
|
||||
*self
|
||||
.connection
|
||||
.lock()
|
||||
.map_err(|_| "Cocos 适配器连接状态锁已损坏".to_string())? = Some(connection);
|
||||
let info = self.connect_target(params.process_id()?, &project_path)?;
|
||||
serde_json::to_value(info).map_err(|error| error.to_string())
|
||||
}
|
||||
"disconnect" => {
|
||||
@@ -375,6 +377,25 @@ impl EditorAdapter for CocosEditorAdapter {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn connection_is_not_published_before_a_successful_handshake() {
|
||||
let adapter = CocosEditorAdapter::default();
|
||||
let target = crate::CocosEditorProcess {
|
||||
process_id: 42,
|
||||
parent_process_id: 1,
|
||||
executable_path: "CocosCreator.exe".into(),
|
||||
command_line: None,
|
||||
project_path: Some("C:/project".into()),
|
||||
creator_version: Some("3.8.8".into()),
|
||||
};
|
||||
assert!(adapter
|
||||
.connect_with(target.clone(), || Err("pipe unavailable".into()))
|
||||
.is_err());
|
||||
assert!(adapter.connection().unwrap().is_none());
|
||||
assert!(adapter.connect_with(target, || Ok(())).unwrap().connected);
|
||||
assert_eq!(adapter.connection().unwrap().unwrap().process_id, 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn uncertain_execute_blocks_subsequent_dispatch_even_after_disconnect() {
|
||||
let mut adapter = CocosEditorAdapter::default();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user