修复 Cocos 内置桥接与客户端项目打开链路

修复 Cocos 内置插件提示词,禁止回退到项目内 MCP 扩展

移除 agc_cocos_execute 对项目写锁的无关依赖

将项目对话读取移到 blocking worker,避免客户端窗口阻塞

为 Windows 开发与发行构建默认启用 Cocos 编辑器执行能力

补充 Cocos Inspector 引导、桥接协议和客户端回归验证
This commit is contained in:
2026-09-12 23:24:45 +08:00
parent fd904ea144
commit 1be6e6756c
29 changed files with 1127 additions and 103 deletions
@@ -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
View File
@@ -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"));
+2
View File
@@ -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
View File
@@ -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