合并最新master到策划Agent修复分支
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled

合入 origin/master 的平台会话、多窗口 Runner 与 Direct 三维选型更新。

保留本分支的策划 Agent 锁削弱、中断自愈和 patch_file 失败诊断决策。

解决 decision-log 双方新增头部决策记录的冲突,保留两侧内容。
This commit is contained in:
2026-09-16 10:14:21 +00:00
45 changed files with 2354 additions and 710 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@genarrative/ai-game-creator-shell",
"private": true,
"version": "0.1.45",
"version": "0.1.47",
"type": "module",
"scripts": {
"dev": "node scripts/start-tauri-dev.mjs",
+1 -1
View File
@@ -1725,7 +1725,7 @@ dependencies = [
[[package]]
name = "genarrative-ai-game-creator-shell"
version = "0.1.45"
version = "0.1.47"
dependencies = [
"agent-runtime-core",
"axum",
@@ -1,6 +1,6 @@
[package]
name = "genarrative-ai-game-creator-shell"
version = "0.1.45"
version = "0.1.47"
edition = "2021"
publish = false
@@ -1,6 +1,6 @@
{
"schemaVersion": "agc-skill-pack.v1",
"version": "2026-08-26.16",
"version": "2026-08-26.15",
"skills": [
{
"name": "agc-game-production-workflow",
@@ -22,7 +22,7 @@
"agents/openai.yaml",
"references/workflow-contract.md"
],
"sha256": "f25e5bd27e8fc82c61b08dc66366b5b253ee8d16d7fa72dbf2c94d2462f4e7fc"
"sha256": "d9d8e7e0a6bc512e0b463e0e4bd77edee1cc57f4a6965c9553e0920e38985d5c"
},
{
"name": "agc-project-structure",
@@ -98,7 +98,7 @@
"agents/openai.yaml",
"references/browser-evidence-contract.md"
],
"sha256": "92ecce42d6589e034d32b75bcd155c1fee34a8c7b843eea5780c0577300ed521"
"sha256": "4437cd8a927a1c79a5faf4bcd40e9946676c08a3b460ab171298cabf899f49ad"
},
{
"name": "agc-client-projection",
File diff suppressed because one or more lines are too long
@@ -49,7 +49,7 @@ struct ExternalMcpHttpState {
root: PathBuf,
token: String,
session_user_id: String,
session_generation: u64,
session_identity_generation: u64,
}
pub(crate) fn direct_tools_mcp_mode_requested(args: &[String]) -> bool {
@@ -1241,7 +1241,8 @@ fn external_mcp_session_id(root: &Path) -> String {
material.push('\0');
material.push_str(&session.user_id);
material.push('\0');
material.push_str(&session.generation.to_string());
// 用身份代次而不是 token:同一账号续期不得让 MCP 会话身份漂移。
material.push_str(&session.identity_generation.to_string());
}
format!("mcp-{:x}", Sha256::digest(material.as_bytes()))
}
@@ -1759,7 +1760,9 @@ async fn handle_external_mcp_http_request(
let Some(session) = current_platform_session() else {
return Err(StatusCode::UNAUTHORIZED);
};
if session.user_id != state.session_user_id || session.generation != state.session_generation {
if session.user_id != state.session_user_id
|| session.identity_generation != state.session_identity_generation
{
return Err(StatusCode::UNAUTHORIZED);
}
let response = EXTERNAL_MCP_BRIDGE_URL
@@ -1794,7 +1797,7 @@ pub(crate) async fn start_external_mcp_loopback(
root,
token: token.clone(),
session_user_id: session.user_id,
session_generation: session.generation,
session_identity_generation: session.identity_generation,
};
let app = Router::new()
.route(&route, post(handle_external_mcp_http_request))
@@ -11,6 +11,10 @@ use super::external_generation_state::{
retain_platform_art_generation_runtime_accepted_result, PlatformArtGenerationRuntimeState,
};
use super::*;
use crate::platform_session::{
acquire_platform_session_identity_lease, validate_platform_session_identity,
PlatformSessionIdentity,
};
use reqwest::multipart::{Form, Part};
const EXTERNAL_GENERATION_POLL_TIMEOUT: Duration = Duration::from_secs(35 * 60);
@@ -1510,10 +1514,7 @@ struct PreparedPlatformArtAssetSlice {
#[derive(Clone)]
struct PreparedPlatformSessionFence {
user_id: String,
api_base_url: String,
generation: u64,
access_token_sha256: String,
identity: PlatformSessionIdentity,
}
impl PreparedPlatformSessionFence {
@@ -1521,41 +1522,17 @@ impl PreparedPlatformSessionFence {
access
.frozen_platform_session()
.map(|session| PreparedPlatformSessionFence {
user_id: session.user_id.clone(),
api_base_url: session.api_base_url.clone(),
generation: session.generation,
access_token_sha256: format!(
"{:x}",
Sha256::digest(session.access_token.as_bytes())
),
identity: session.identity(),
})
}
fn validate(&self) -> Result<(), String> {
let matches = current_platform_session().is_some_and(|session| {
session.user_id == self.user_id
&& session.api_base_url == self.api_base_url
&& session.generation == self.generation
&& format!("{:x}", Sha256::digest(session.access_token.as_bytes()))
== self.access_token_sha256
});
if matches {
Ok(())
} else {
Err(
"authentication-required: 陶泥儿登录态已变化,旧账号请求已停止,请使用当前账号重试"
.to_string(),
)
}
// 只比较身份:同一账号的 access token 轮换不得让在途生成 operation 失败。
validate_platform_session_identity(&self.identity)
}
fn acquire_lease(&self) -> Result<ValidatedPlatformSessionLease, String> {
acquire_validated_platform_session_fingerprint(
&self.user_id,
&self.api_base_url,
self.generation,
&self.access_token_sha256,
)
acquire_platform_session_identity_lease(&self.identity)
}
}
@@ -10636,7 +10613,7 @@ mod canvas_generation_tests {
}
drop(owner_a_access);
drop(frozen_owner_a);
install_platform_session("durable-owner-b", "durable-owner-b-token", &base_url, 2)
install_platform_session("durable-owner-b", "durable-owner-b-token", &base_url, 2, 2)
.expect("switch to owner B");
let error = match request_platform_art_asset_with_runtime_options_at(
@@ -10761,8 +10738,14 @@ mod canvas_generation_tests {
.recv_timeout(Duration::from_secs(3))
.expect("wait for accepted response");
std::thread::sleep(Duration::from_millis(50));
install_platform_session("post-202-user-b", "post-202-token-b", &switch_base_url, 2)
.expect("switch platform account after accepted response");
install_platform_session(
"post-202-user-b",
"post-202-token-b",
&switch_base_url,
2,
2,
)
.expect("switch platform account after accepted response");
});
let runtime_context = PlatformArtGenerationRuntimeContext {
agent_id: "art-director".to_string(),
@@ -1431,12 +1431,13 @@ mod external_generation_state_tests {
base_url,
);
let frozen_a = current_platform_session().expect("freeze owner A");
validate_platform_session_snapshot(&frozen_a).expect("owner A is current before switch");
validate_frozen_platform_session(&frozen_a).expect("owner A is current before switch");
replace_platform_session_for_gui_owner(
"fingerprint-owner-b",
"fingerprint-token-b",
base_url,
2,
2,
)
.expect("switch global session to owner B");
let current_b = current_platform_session().expect("owner B is current after switch");
@@ -2,8 +2,9 @@ use super::*;
pub(super) static GAME_CREATOR_AGENT_RUNTIME_UPDATE_APP_HANDLE: OnceLock<tauri::AppHandle> =
OnceLock::new();
pub(super) static GAME_CREATOR_MANIFEST_INVALIDATION_EVENT_SINK: OnceLock<
std::sync::Mutex<Option<GameCreatorManifestInvalidationEventSink>>,
/// 同一 AppData 允许多个界面窗口同时挂载事件接收端,因此这里是按 token 去重的注册表。
pub(super) static GAME_CREATOR_MANIFEST_INVALIDATION_EVENT_SINKS: OnceLock<
std::sync::Mutex<Vec<GameCreatorManifestInvalidationEventSink>>,
> = OnceLock::new();
#[cfg(test)]
pub(super) static GAME_CREATOR_MANIFEST_INVALIDATION_EVENT_SINK_TEST_LOCK: std::sync::Mutex<()> =
@@ -293,8 +294,8 @@ pub(crate) use entrypoints::{
configure_game_creator_manifest_invalidation_event_sink, emit_direct_game_creator_progress,
emit_game_creator_agent_runtime_update, emit_game_creator_manifest_invalidated,
game_creator_agent_runtime_update_event, generate_local_game_draft_at,
install_game_creator_manifest_invalidation_event_sink, read_game_creator_agent_runtime_at,
read_game_creator_agent_runtime_for_session_at, read_game_creator_agent_runtimes_at,
read_game_creator_agent_runtime_at, read_game_creator_agent_runtime_for_session_at,
read_game_creator_agent_runtimes_at, register_game_creator_manifest_invalidation_event_sink,
set_game_creator_agent_runtime_update_app_handle,
start_game_creator_manifest_invalidation_event_sink,
validate_game_creator_manifest_invalidation_event_sink,
@@ -1,11 +1,12 @@
use super::*;
const GAME_CREATOR_MANIFEST_INVALIDATION_RELAY_MAX_BYTES: u64 = 64 * 1024;
const GAME_CREATOR_MANIFEST_INVALIDATION_EVENT_SINK_MAX: usize = 16;
fn lock_game_creator_manifest_invalidation_event_sink(
) -> std::sync::MutexGuard<'static, Option<GameCreatorManifestInvalidationEventSink>> {
GAME_CREATOR_MANIFEST_INVALIDATION_EVENT_SINK
.get_or_init(|| Mutex::new(None))
fn lock_game_creator_manifest_invalidation_event_sinks(
) -> std::sync::MutexGuard<'static, Vec<GameCreatorManifestInvalidationEventSink>> {
GAME_CREATOR_MANIFEST_INVALIDATION_EVENT_SINKS
.get_or_init(|| Mutex::new(Vec::new()))
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
@@ -219,7 +220,7 @@ pub(crate) fn configure_game_creator_manifest_invalidation_event_sink(
token: &str,
) -> Result<(), String> {
let sink = validate_game_creator_manifest_invalidation_event_sink(port, token)?;
install_game_creator_manifest_invalidation_event_sink(sink);
register_game_creator_manifest_invalidation_event_sink(sink);
Ok(())
}
@@ -240,10 +241,29 @@ pub(crate) fn validate_game_creator_manifest_invalidation_event_sink(
})
}
pub(crate) fn install_game_creator_manifest_invalidation_event_sink(
/// 登记一个界面窗口的事件接收端。
///
/// 同一窗口重复 attach 用同一个 token,按 token 覆盖旧登记;不同窗口各自持有
/// 自己的 token,注册表按登记顺序保留,最多 `GAME_CREATOR_MANIFEST_INVALIDATION_EVENT_SINK_MAX` 个。
pub(crate) fn register_game_creator_manifest_invalidation_event_sink(
sink: GameCreatorManifestInvalidationEventSink,
) {
*lock_game_creator_manifest_invalidation_event_sink() = Some(sink);
let mut sinks = lock_game_creator_manifest_invalidation_event_sinks();
if let Some(existing) = sinks
.iter_mut()
.find(|existing| existing.token == sink.token)
{
*existing = sink;
return;
}
if sinks.len() >= GAME_CREATOR_MANIFEST_INVALIDATION_EVENT_SINK_MAX {
sinks.remove(0);
}
sinks.push(sink);
}
fn remove_game_creator_manifest_invalidation_event_sink(token: &str) {
lock_game_creator_manifest_invalidation_event_sinks().retain(|sink| sink.token != token);
}
#[cfg(test)]
@@ -258,14 +278,20 @@ impl GameCreatorManifestInvalidationEventSinkTestGuard {
}
pub(crate) fn configured_sink(&self) -> Option<GameCreatorManifestInvalidationEventSink> {
lock_game_creator_manifest_invalidation_event_sink().clone()
lock_game_creator_manifest_invalidation_event_sinks()
.first()
.cloned()
}
pub(crate) fn configured_sinks(&self) -> Vec<GameCreatorManifestInvalidationEventSink> {
lock_game_creator_manifest_invalidation_event_sinks().clone()
}
}
#[cfg(test)]
impl Drop for GameCreatorManifestInvalidationEventSinkTestGuard {
fn drop(&mut self) {
*lock_game_creator_manifest_invalidation_event_sink() = None;
lock_game_creator_manifest_invalidation_event_sinks().clear();
}
}
@@ -281,16 +307,43 @@ pub(crate) fn acquire_game_creator_manifest_invalidation_event_sink_test_guard(
}
fn relay_game_creator_manifest_invalidation(root: &Path, agent_id: &str) -> Result<(), String> {
let sink = lock_game_creator_manifest_invalidation_event_sink().clone();
let Some(sink) = sink else {
let sinks = lock_game_creator_manifest_invalidation_event_sinks().clone();
if sinks.is_empty() {
return Ok(());
}
let event = GameCreatorManifestInvalidatedEvent {
project_path: root.to_string_lossy().into_owned(),
agent_id: agent_id.to_string(),
};
let mut failed_tokens = Vec::new();
let mut last_error = None;
for sink in &sinks {
match relay_game_creator_manifest_invalidation_to_sink(sink, &event) {
Ok(()) => {}
Err(error) => {
// 窗口已退出或接收端已释放时只淘汰该接收端,不能影响其它窗口。
failed_tokens.push(sink.token.clone());
last_error = Some(error);
}
}
}
if !failed_tokens.is_empty() {
lock_game_creator_manifest_invalidation_event_sinks()
.retain(|sink| !failed_tokens.contains(&sink.token));
}
match last_error {
Some(error) => Err(error),
None => Ok(()),
}
}
fn relay_game_creator_manifest_invalidation_to_sink(
sink: &GameCreatorManifestInvalidationEventSink,
event: &GameCreatorManifestInvalidatedEvent,
) -> Result<(), String> {
let envelope = GameCreatorManifestInvalidationRelayEnvelope {
token: sink.token,
event: GameCreatorManifestInvalidatedEvent {
project_path: root.to_string_lossy().into_owned(),
agent_id: agent_id.to_string(),
},
token: sink.token.clone(),
event: event.clone(),
};
let payload = serde_json::to_vec(&envelope)
.map_err(|error| format!("序列化 manifest 失效事件失败:{error}"))?;
@@ -1939,8 +1939,9 @@ pub(crate) async fn polish_local_project_prompt(
}
#[tauri::command]
pub(crate) fn read_platform_account_session_generation() -> u64 {
current_platform_session_generation()
pub(crate) fn read_platform_account_session_state(
) -> crate::platform_session::PlatformSessionWriteState {
crate::platform_session::current_platform_session_write_state()
}
#[tauri::command]
@@ -1948,28 +1949,45 @@ pub(crate) async fn install_platform_account_session(
user_id: String,
access_token: String,
api_base_url: String,
generation: u64,
identity_generation: u64,
revision: u64,
) -> Result<(), String> {
tokio::task::spawn_blocking(move || {
validate_platform_session_input(&user_id, &access_token, &api_base_url, generation)?;
validate_platform_session_input(
&user_id,
&access_token,
&api_base_url,
identity_generation,
revision,
)?;
install_external_agent_runner_platform_session(
&user_id,
&access_token,
&api_base_url,
generation,
identity_generation,
revision,
)?;
install_platform_session(&user_id, &access_token, &api_base_url, generation)
install_platform_session(
&user_id,
&access_token,
&api_base_url,
identity_generation,
revision,
)
})
.await
.map_err(|error| format!("安装本地运行时会话任务意外终止:{error}"))?
}
#[tauri::command]
pub(crate) async fn clear_platform_account_session(generation: u64) -> Result<(), String> {
pub(crate) async fn clear_platform_account_session(
identity_generation: u64,
revision: u64,
) -> Result<(), String> {
tokio::task::spawn_blocking(move || {
shutdown_game_creator_codex_app_servers()?;
clear_external_agent_runner_platform_session(generation)?;
clear_platform_session(generation);
clear_external_agent_runner_platform_session(identity_generation, revision)?;
clear_platform_session(identity_generation, revision);
Ok(())
})
.await
@@ -4247,14 +4265,7 @@ pub(crate) async fn import_account_editor_assets_for_agent(
access.validate_frozen_session()?;
let _platform_session_lease = frozen_session
.as_ref()
.map(|session| {
acquire_validated_platform_session_fingerprint(
&session.user_id,
&session.api_base_url,
session.generation,
&format!("{:x}", Sha256::digest(session.access_token.as_bytes())),
)
})
.map(|session| acquire_platform_session_identity_lease(&session.identity()))
.transpose()?;
let _lock = acquire_project_write_lock(root, "canvas.asset_import")?;
access.validate_frozen_session()?;
@@ -2219,6 +2219,8 @@ fn game_creator_gui_run_event_requests_runner_shutdown(event: &tauri::RunEvent)
enum GameCreatorGuiRunnerShutdownOutcome {
NotRequested,
Requested,
/// 仍有其它界面窗口持有参与锁,Runner 必须保留给它们。
Retained,
Failed(GameCreatorGuiRunnerShutdownFailure),
}
@@ -2256,7 +2258,8 @@ fn classify_game_creator_gui_runner_shutdown_error(
GameCreatorGuiRunnerShutdownFailure::ProcessIdentity
} else if error.contains("当前平台不支持") || error.contains("macOS 不提供") {
GameCreatorGuiRunnerShutdownFailure::PlatformUnsupported
} else if error.contains("实例锁") || error.contains("owner 锁") {
} else if error.contains("实例锁") || error.contains("参与锁") || error.contains("owner 锁")
{
GameCreatorGuiRunnerShutdownFailure::LockTimeout
} else if error.contains("endpoint") {
GameCreatorGuiRunnerShutdownFailure::EndpointUnavailable
@@ -2272,13 +2275,14 @@ fn resolve_game_creator_gui_runner_shutdown<F>(
shutdown: F,
) -> GameCreatorGuiRunnerShutdownOutcome
where
F: FnOnce() -> Result<(), String>,
F: FnOnce() -> Result<bool, String>,
{
if !game_creator_gui_run_event_requests_runner_shutdown(event) {
return GameCreatorGuiRunnerShutdownOutcome::NotRequested;
}
match shutdown() {
Ok(()) => GameCreatorGuiRunnerShutdownOutcome::Requested,
Ok(true) => GameCreatorGuiRunnerShutdownOutcome::Requested,
Ok(false) => GameCreatorGuiRunnerShutdownOutcome::Retained,
Err(error) => GameCreatorGuiRunnerShutdownOutcome::Failed(
classify_game_creator_gui_runner_shutdown_error(&error),
),
@@ -2298,11 +2302,17 @@ fn handle_game_creator_gui_run_event(event: &tauri::RunEvent) {
app_log!("agent.direct_codex.gui_exit.shutdown_failed: {error}");
}
}
match resolve_game_creator_gui_runner_shutdown(event, shutdown_external_agent_runner) {
match resolve_game_creator_gui_runner_shutdown(
event,
shutdown_external_agent_runner_for_gui_exit,
) {
GameCreatorGuiRunnerShutdownOutcome::NotRequested => {}
GameCreatorGuiRunnerShutdownOutcome::Requested => {
app_log!("agent.runner.gui_exit.shutdown_requested")
}
GameCreatorGuiRunnerShutdownOutcome::Retained => {
app_log!("agent.runner.gui_exit.retained_for_other_windows")
}
GameCreatorGuiRunnerShutdownOutcome::Failed(failure) => {
app_log!("agent.runner.gui_exit.shutdown_failed.{}", failure.code())
}
@@ -2611,27 +2621,25 @@ fn main() {
)
})?;
setup_log.append("startup.runner.configure.complete");
let gui_owner_lock = acquire_external_agent_runner_gui_owner_lock(&config_dir)
hold_external_agent_runner_gui_participant_lock(&config_dir)
.inspect_err(|error| {
let details = sanitize_diagnostic_message(error, Some(config_dir.as_path()));
setup_log.fail(&format!(
"startup.runner.owner-lock.failed details={details}"
"startup.runner.participant-lock.failed details={details}"
));
})
.map_err(|error| {
std::io::Error::new(
std::io::ErrorKind::AlreadyExists,
format!("获取 GUI owner 锁失败:{error}"),
format!("建立 AGC 界面参与锁失败:{error}"),
)
})?;
let gui_owner_epoch = gui_owner_lock.owner_epoch().to_string();
app.manage(gui_owner_lock);
setup_log.append("startup.runner.start.begin");
set_game_creator_agent_runtime_update_app_handle(app.handle().clone());
set_direct_thread_manager_app_handle(app.handle().clone());
let manifest_event_sink =
start_game_creator_manifest_invalidation_event_sink(app.handle().clone())?;
attach_external_agent_runner_gui_owner(&manifest_event_sink, &gui_owner_epoch)
attach_external_agent_runner_gui_owner(&manifest_event_sink)
.inspect_err(|error| {
let details = sanitize_diagnostic_message(error, Some(config_dir.as_path()));
setup_log.fail(&format!(
@@ -2725,7 +2733,7 @@ fn main() {
confirm_resume_game_creator_agent_runtime_tasks,
schedule_game_creator_agent_ready_tasks,
check_game_creator_llm_config,
read_platform_account_session_generation,
read_platform_account_session_state,
install_platform_account_session,
clear_platform_account_session,
read_game_creator_app_config,
File diff suppressed because it is too large Load Diff
@@ -110,11 +110,13 @@ impl<'a> ExternalEditorBindingAccess<'a> {
}
/// Call before and after every awaited remote action and immediately before installing a
/// binding. Developer-key mode has no process-global account generation to compare.
/// binding. 只比较身份:同一账号的 access token 轮换(长回合保活、401 续期)不得让
/// 在途的生成、编辑、上传、确认或下载 operation 失效;换号、退出或 origin 变化仍然
/// 失败关闭。Developer-key 模式没有进程级身份代次可比对。
pub(crate) fn validate_frozen_session(&self) -> Result<(), String> {
validate_external_editor_binding_access_shape(self)?;
if let Some(session) = self.frozen_platform_session {
validate_platform_session_snapshot(session)?;
validate_frozen_platform_session(session)?;
}
Ok(())
}
@@ -1152,7 +1154,8 @@ mod tests {
user_id: user_id.to_string(),
access_token: token.to_string(),
api_base_url: "https://dev.genarrative.world".to_string(),
generation,
identity_generation: generation,
revision: generation,
}
}
@@ -4389,14 +4389,7 @@ fn with_frozen_resource_edit_platform_session<T>(
let Some(platform_session) = platform_session else {
return action();
};
let access_token_sha256 = sha256_hex(platform_session.access_token.as_bytes());
with_validated_platform_session_fingerprint(
&platform_session.user_id,
&platform_session.api_base_url,
platform_session.generation,
&access_token_sha256,
action,
)
with_validated_platform_session_identity(&platform_session.identity(), action)
}
fn commit_resource_edit_asset_with_frozen_platform_session(
@@ -4774,14 +4767,7 @@ pub(crate) fn list_pending_local_project_resource_edits_at(
let current_platform_session = current_platform_session();
let _platform_session_lease = current_platform_session
.as_ref()
.map(|session| {
acquire_validated_platform_session_fingerprint(
&session.user_id,
&session.api_base_url,
session.generation,
&sha256_hex(session.access_token.as_bytes()),
)
})
.map(|session| acquire_platform_session_identity_lease(&session.identity()))
.transpose()?;
let directory = resolve_local_project_path(root, &format!("{RESOURCE_EDIT_ROOT}/operations"))?;
let entries = match fs::read_dir(&directory) {
@@ -5084,12 +5070,8 @@ pub(crate) async fn archive_failed_local_project_resource_edit_at(
Ok(())
};
if let Some(session) = platform_session {
let access_token_sha256 = sha256_hex(session.access_token.as_bytes());
crate::platform_session::with_validated_platform_session_fingerprint(
&session.user_id,
&session.api_base_url,
session.generation,
&access_token_sha256,
crate::platform_session::with_validated_platform_session_identity(
&session.identity(),
archive,
)?;
} else {
@@ -5500,7 +5482,8 @@ mod tests {
user_id: "gui-owner".to_string(),
access_token: "gui-token".to_string(),
api_base_url: "https://dev.genarrative.world".to_string(),
generation: 7,
identity_generation: 7,
revision: 7,
};
let developer_credentials = (
"https://dev.genarrative.world".to_string(),
@@ -5911,6 +5894,7 @@ mod tests {
"source-binding-token-b",
api_base_url,
*generation,
*generation,
)
.expect("switch account after source registration");
}
@@ -6925,7 +6909,7 @@ mod tests {
listener,
upload_url,
false,
Some((base_url.clone(), frozen_session.generation + 1)),
Some((base_url.clone(), frozen_session.identity_generation + 1)),
done_receiver,
);
let client = reqwest::Client::new();
@@ -6952,7 +6936,8 @@ mod tests {
"source-binding-owner-a",
"source-binding-token-a",
&base_url,
frozen_session.generation + 2,
frozen_session.identity_generation + 2,
frozen_session.identity_generation + 2,
)
.expect("switch back to source binding owner A");
let resumed_session = current_platform_session().expect("resumed source binding owner A");
@@ -7018,7 +7003,7 @@ mod tests {
install_test_platform_session("submission-owner-a", "submission-token-a", &base_url);
let frozen_session = current_platform_session().expect("frozen owner A session");
let switch_base_url = base_url.clone();
let switch_generation = frozen_session.generation + 1;
let switch_generation = frozen_session.identity_generation + 1;
let server = std::thread::spawn(move || {
let mut stream =
accept_resource_editor_fixture_connection(&listener, "accepted switch fixture", 0);
@@ -7032,6 +7017,7 @@ mod tests {
"submission-token-b",
&switch_base_url,
switch_generation,
switch_generation,
)
.expect("switch to owner B before returning accepted response");
write_json(
@@ -7447,8 +7433,14 @@ mod tests {
ledger.access_scheme = None;
initialize_resource_edit_access_identity(root, &mut ledger, base_url, Some(&frozen_a))
.expect("write resource ledger for owner A");
replace_platform_session_for_gui_owner("resource-owner-b", "resource-token-b", base_url, 2)
.expect("switch global resource session to owner B");
replace_platform_session_for_gui_owner(
"resource-owner-b",
"resource-token-b",
base_url,
2,
2,
)
.expect("switch global resource session to owner B");
let error = prepare_resource_edit_service_identity(
root,
@@ -7509,7 +7501,8 @@ mod tests {
user_id: "resource-identity-owner-b".to_string(),
access_token: "resource-identity-token-b".to_string(),
api_base_url: owner_a.api_base_url.clone(),
generation: owner_a.generation + 1,
identity_generation: owner_a.identity_generation + 1,
revision: owner_a.revision + 1,
};
let mut ledger = ledger_for(&request, &source, ResourceEditLedgerPhase::Prepared);
ledger.access_scheme = Some(RESOURCE_EDIT_PLATFORM_ACCESS_SCHEME.to_string());
@@ -7535,7 +7528,8 @@ mod tests {
&owner_b.user_id,
&owner_b.access_token,
&owner_b.api_base_url,
owner_b.generation,
owner_b.identity_generation,
owner_b.revision,
)
.expect("switch to resource non-owner B");
@@ -7639,7 +7633,8 @@ mod tests {
"resource-lease-owner-b",
"resource-lease-token-b",
api_base_url,
frozen_a.generation + 1,
frozen_a.identity_generation + 1,
frozen_a.revision + 1,
)
.expect("switch resource lease owner");
switched_sender.send(()).expect("signal resource switch");
@@ -8259,7 +8254,8 @@ mod tests {
"archive-owner-b",
"archive-token-b",
api_base_url,
owner_a.generation + 1,
owner_a.identity_generation + 1,
owner_a.revision + 1,
)
.expect("switch to owner B");
let error = archive_failed_local_project_resource_edit_at(
@@ -8388,7 +8384,8 @@ mod tests {
"pending-owner-b",
"pending-token-b",
api_base_url,
owner_a.generation + 1,
owner_a.identity_generation + 1,
owner_a.revision + 1,
)
.expect("switch to pending owner B");
let owner_b = current_platform_session().expect("pending owner B session");
@@ -9347,7 +9344,7 @@ mod tests {
let (attempted_sender, attempted_receiver) = mpsc::channel();
let (completed_sender, completed_receiver) = mpsc::channel();
let switch_api_base_url = api_base_url.to_string();
let switch_generation = frozen_session.generation + 1;
let switch_generation = frozen_session.identity_generation + 1;
let switch_thread = std::thread::spawn(move || {
begin_switch_receiver
.recv()
@@ -9360,6 +9357,7 @@ mod tests {
"commit-token-b",
&switch_api_base_url,
switch_generation,
switch_generation,
)
.expect("switch to commit owner B");
completed_sender
@@ -12,21 +12,20 @@ pub(crate) use client::{
clear_external_agent_runner_platform_session, compact_external_agent_runner_context,
configure_external_agent_runner, configure_external_agent_runner_read_only,
continue_external_agent_runner_action, ensure_external_agent_runner_started,
ensure_external_agent_runner_started_for_gui, install_external_agent_runner_platform_session,
ensure_external_agent_runner_started_for_gui, hold_external_agent_runner_gui_participant_lock,
install_external_agent_runner_platform_session,
interrupt_external_agent_runner_provider_for_steer_decision, notify_external_agent_runner,
pause_external_agent_runner, read_external_agent_runner_status,
require_external_agent_runner_configured_for_cli_runtime_write,
require_external_agent_runner_for_cli_runtime_write, resume_external_agent_runner,
shutdown_external_agent_runner, shutdown_external_agent_runner_for_client_exit,
shutdown_external_agent_runner_if_idle, steer_external_agent_runner,
wake_external_agent_runner_pending, wake_external_agent_runner_pending_for_run,
shutdown_external_agent_runner_for_gui_exit, shutdown_external_agent_runner_if_idle,
steer_external_agent_runner, wake_external_agent_runner_pending,
wake_external_agent_runner_pending_for_run,
};
#[cfg(windows)]
pub(crate) use endpoint::validate_windows_regular_file_handle;
pub(crate) use endpoint::{
acquire_external_agent_runner_gui_owner_lock, external_agent_runner_enabled,
external_agent_runner_is_server_process,
};
pub(crate) use endpoint::{external_agent_runner_enabled, external_agent_runner_is_server_process};
#[allow(unused_imports)]
pub(crate) use protocol::{ExternalAgentRunnerStatus, EXTERNAL_AGENT_RUNNER_PROTOCOL_VERSION};
pub(crate) use server::{
File diff suppressed because it is too large Load Diff
@@ -1,6 +1,6 @@
use super::{endpoint::*, project_owner::*, protocol::*, state::*};
use crate::{
install_game_creator_manifest_invalidation_event_sink,
register_game_creator_manifest_invalidation_event_sink,
validate_game_creator_manifest_invalidation_event_sink,
};
use serde::Deserialize;
@@ -116,9 +116,9 @@ fn apply_external_agent_runner_gui_owner_attachment(
.gui_owner_session_revision
.ok_or_else(|| "Agent Runner GUI owner 缺少 session revision".to_string())?;
let config_dir = state
.gui_owner_lock_path
.gui_participant_lock_path
.parent()
.ok_or_else(|| "Agent Runner GUI owner 锁缺少 AppData 父目录".to_string())?;
.ok_or_else(|| "AGC 界面参与锁缺少 AppData 父目录".to_string())?;
let mut active_claim = lock_unpoisoned(&state.gui_owner_platform_session_claim);
let durable_claim = read_external_agent_runner_gui_owner_claim(config_dir)?;
if durable_claim.owner_epoch != requested_epoch
@@ -127,43 +127,59 @@ fn apply_external_agent_runner_gui_owner_attachment(
return Err("Agent Runner GUI owner claim 已过期".to_string());
}
let requested_claim = (requested_epoch.to_string(), requested_revision);
let replace_claim = active_claim.as_ref() != Some(&requested_claim);
// 同一 AppData 的多个窗口共享同一个 epoch:只有 epoch 变化(本窗口发布了新的
// 登录态权威)才允许强制替换会话。同一 epoch 内的重复 attach 只做单调校验,
// 因此后开窗口的“无登录态 attach”不会清空已有会话。
let epoch_changed = match active_claim.as_ref() {
Some(active) => active.0 != requested_epoch,
None => true,
};
let replace_claim = epoch_changed;
let result = match (
params.platform_user_id.as_deref(),
params.platform_access_token.as_deref(),
params.platform_api_base_url.as_deref(),
params.platform_auth_generation,
params.platform_auth_revision,
) {
(Some(user_id), Some(access_token), Some(api_base_url), Some(generation)) => {
(
Some(user_id),
Some(access_token),
Some(api_base_url),
Some(identity_generation),
Some(revision),
) => {
if replace_claim {
crate::replace_platform_session_for_gui_owner(
user_id,
access_token,
api_base_url,
generation,
identity_generation,
revision,
)
} else {
crate::install_platform_session_checked(
user_id,
access_token,
api_base_url,
generation,
identity_generation,
revision,
)
}
}
(None, None, None, Some(generation)) => {
(None, None, None, Some(identity_generation), Some(revision)) => {
if replace_claim {
crate::clear_platform_session_for_gui_owner(generation);
crate::clear_platform_session_for_gui_owner(identity_generation, revision);
Ok(())
} else {
crate::clear_platform_session_checked(generation)
crate::clear_platform_session_checked(identity_generation, revision)
}
}
(None, None, None, None) if replace_claim => {
crate::clear_platform_session_for_gui_owner(0);
(None, None, None, None, None) if epoch_changed => {
crate::clear_platform_session_for_gui_owner(0, 0);
Ok(())
}
(None, None, None, None) => Ok(()),
(None, None, None, None, None) => Ok(()),
_ => Err("Agent Runner GUI owner 的平台登录态同步参数不完整".to_string()),
};
result?;
@@ -171,7 +187,7 @@ fn apply_external_agent_runner_gui_owner_attachment(
Ok(claim) => claim,
Err(error) => {
*active_claim = None;
crate::clear_platform_session_for_gui_owner(0);
crate::clear_platform_session_for_gui_owner(0, 0);
return Err(format!(
"Agent Runner GUI owner claim 在 attach 提交期间无法核验,平台登录态已隔离:{error}"
));
@@ -181,11 +197,11 @@ fn apply_external_agent_runner_gui_owner_attachment(
|| committed_claim.session_revision != requested_revision
{
*active_claim = None;
crate::clear_platform_session_for_gui_owner(0);
crate::clear_platform_session_for_gui_owner(0, 0);
return Err("Agent Runner GUI owner claim 在 attach 提交期间已变化".to_string());
}
if let Some(event_sink) = event_sink {
install_game_creator_manifest_invalidation_event_sink(event_sink);
register_game_creator_manifest_invalidation_event_sink(event_sink);
}
*active_claim = Some(requested_claim);
Ok(())
@@ -195,9 +211,9 @@ pub(super) fn validate_external_agent_runner_gui_owner_claim_current(
state: &ExternalAgentRunnerServerState,
) -> Result<(), String> {
let config_dir = state
.gui_owner_lock_path
.gui_participant_lock_path
.parent()
.ok_or_else(|| "Agent Runner GUI owner 锁缺少 AppData 父目录".to_string())?;
.ok_or_else(|| "AGC 界面参与锁缺少 AppData 父目录".to_string())?;
let mut active_claim = lock_unpoisoned(&state.gui_owner_platform_session_claim);
let durable_claim = read_external_agent_runner_gui_owner_claim(config_dir);
let matches = durable_claim.as_ref().is_ok_and(|claim| {
@@ -207,7 +223,7 @@ pub(super) fn validate_external_agent_runner_gui_owner_claim_current(
return Ok(());
}
*active_claim = None;
crate::clear_platform_session_for_gui_owner(0);
crate::clear_platform_session_for_gui_owner(0, 0);
match durable_claim {
Ok(_) => Err(
"authentication-required: Agent Runner GUI owner claim 已变化,平台登录态已隔离"
@@ -768,7 +784,7 @@ pub(super) fn dispatch_external_agent_runner_runtime_request_with_owner_claim(
}
}
"runner.attach_gui_owner" => {
match external_agent_runner_gui_owner_is_locked(&state.gui_owner_lock_path) {
match external_agent_runner_lock_is_held(&state.gui_participant_lock_path) {
Ok(true) => {
let event_sink = request
.params
@@ -810,7 +826,7 @@ pub(super) fn dispatch_external_agent_runner_runtime_request_with_owner_claim(
Ok(false) => ExternalAgentRunnerResponse::failure(
&request.request_id,
"gui-owner-missing",
"Agent Runner 未检测到活跃 GUI owner 锁",
"Agent Runner 未检测到活跃的 AGC 界面进程",
),
Err(error) => ExternalAgentRunnerResponse::failure(
&request.request_id,
@@ -6,7 +6,12 @@ use std::io::{self, Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
use std::sync::atomic::Ordering;
use std::sync::Mutex;
use std::time::{SystemTime, UNIX_EPOCH};
use std::thread;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
const EXTERNAL_AGENT_RUNNER_GUI_PARTICIPANT_LOCK_ACQUIRE_TIMEOUT: Duration = Duration::from_secs(5);
const EXTERNAL_AGENT_RUNNER_GUI_PARTICIPANT_LOCK_RETRY_INTERVAL: Duration =
Duration::from_millis(40);
pub(super) fn external_agent_runner_config_dir_lock() -> &'static Mutex<Option<PathBuf>> {
EXTERNAL_AGENT_RUNNER_CONFIG_DIR.get_or_init(|| Mutex::new(None))
@@ -305,8 +310,8 @@ pub(super) fn external_agent_runner_lock_path(config_dir: &Path) -> PathBuf {
config_dir.join(EXTERNAL_AGENT_RUNNER_LOCK_FILE_NAME)
}
pub(super) fn external_agent_runner_gui_owner_lock_path(config_dir: &Path) -> PathBuf {
config_dir.join(EXTERNAL_AGENT_RUNNER_GUI_OWNER_LOCK_FILE_NAME)
pub(super) fn external_agent_runner_gui_participant_lock_path(config_dir: &Path) -> PathBuf {
config_dir.join(EXTERNAL_AGENT_RUNNER_GUI_PARTICIPANT_LOCK_FILE_NAME)
}
pub(super) fn external_agent_runner_gui_owner_claim_path(config_dir: &Path) -> PathBuf {
@@ -341,8 +346,9 @@ pub(super) fn read_external_agent_runner_gui_owner_claim(
Ok(claim)
}
pub(super) fn external_agent_runner_gui_owner_is_locked(path: &Path) -> Result<bool, String> {
match try_open_external_agent_runner_lock(path, "Agent Runner GUI owner 锁")? {
/// 独占探测:返回 `true` 表示仍有界面进程持有该参与锁。
pub(super) fn external_agent_runner_lock_is_held(path: &Path) -> Result<bool, String> {
match try_open_external_agent_runner_lock(path, "AGC 界面参与锁")? {
Some(lock) => {
drop(lock);
Ok(false)
@@ -743,10 +749,21 @@ pub(super) fn read_current_external_agent_runner_endpoint(
})
}
/// 锁文件的两种打开方式。
///
/// `Exclusive` 是权威探测:能否独占取得句柄决定“还有没有存活持有者”。
/// `Shared` 是参与者持有:同一 AppData 的多个界面进程可以同时持有。
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(super) enum ExternalAgentRunnerLockMode {
Exclusive,
Shared,
}
#[cfg(unix)]
pub(super) fn try_open_external_agent_runner_lock(
pub(super) fn open_external_agent_runner_lock_file(
path: &Path,
label: &str,
mode: ExternalAgentRunnerLockMode,
) -> Result<Option<File>, String> {
use std::os::fd::AsRawFd;
use std::os::unix::fs::{MetadataExt, OpenOptionsExt, PermissionsExt};
@@ -809,14 +826,30 @@ pub(super) fn try_open_external_agent_runner_lock(
path.display()
));
}
let flock_operation = match mode {
ExternalAgentRunnerLockMode::Exclusive => libc::LOCK_EX | libc::LOCK_NB,
ExternalAgentRunnerLockMode::Shared => libc::LOCK_SH | libc::LOCK_NB,
};
// SAFETY: flock only observes the valid fd owned by `file`; `file` remains alive on success.
let result = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
let result = unsafe { libc::flock(file.as_raw_fd(), flock_operation) };
if result == 0 {
return Ok(Some(file));
}
let error = io::Error::last_os_error();
if error.kind() == io::ErrorKind::WouldBlock {
Ok(None)
return match mode {
ExternalAgentRunnerLockMode::Exclusive => Ok(None),
ExternalAgentRunnerLockMode::Shared => Err(format!(
"{label} 正被独占探测或持有,稍后重试:{}",
path.display()
)),
};
}
if mode == ExternalAgentRunnerLockMode::Shared {
Err(format!(
"以共享方式获取 {label} 失败:{}: {error}",
path.display()
))
} else {
Err(format!(
"获取 {label} 系统锁失败:{}: {error}",
@@ -825,39 +858,58 @@ pub(super) fn try_open_external_agent_runner_lock(
}
}
#[cfg(unix)]
pub(super) fn try_open_external_agent_runner_lock(
path: &Path,
label: &str,
) -> Result<Option<File>, String> {
open_external_agent_runner_lock_file(path, label, ExternalAgentRunnerLockMode::Exclusive)
}
#[cfg(windows)]
pub(super) fn windows_external_agent_runner_lock_is_busy_error(error: &io::Error) -> bool {
matches!(error.raw_os_error(), Some(32 | 33))
}
#[cfg(windows)]
pub(super) fn try_open_external_agent_runner_lock(
pub(super) fn open_external_agent_runner_lock_file(
path: &Path,
label: &str,
mode: ExternalAgentRunnerLockMode,
) -> Result<Option<File>, String> {
use std::os::windows::fs::OpenOptionsExt;
const FILE_FLAG_OPEN_REPARSE_POINT: u32 = 0x0020_0000;
const FILE_SHARE_READ: u32 = 0x0000_0001;
const FILE_SHARE_WRITE: u32 = 0x0000_0002;
const FILE_SHARE_DELETE: u32 = 0x0000_0004;
let parent = path
.parent()
.ok_or_else(|| format!("{label} 缺少 AppData 父目录:{}", path.display()))?;
let private_parent = crate::inspect_game_creator_runtime_config_dir(parent)?;
let runner_lock_path = private_parent.join(EXTERNAL_AGENT_RUNNER_LOCK_FILE_NAME);
let gui_owner_lock_path = private_parent.join(EXTERNAL_AGENT_RUNNER_GUI_OWNER_LOCK_FILE_NAME);
if path != runner_lock_path && path != gui_owner_lock_path {
let gui_participant_lock_path =
private_parent.join(EXTERNAL_AGENT_RUNNER_GUI_PARTICIPANT_LOCK_FILE_NAME);
if path != runner_lock_path && path != gui_participant_lock_path {
return Err(format!(
"{label} 必须位于已验证的私有 AppData 固定锁路径:{}{}",
runner_lock_path.display(),
gui_owner_lock_path.display()
gui_participant_lock_path.display()
));
}
let share_mode = match mode {
ExternalAgentRunnerLockMode::Exclusive => 0,
ExternalAgentRunnerLockMode::Shared => {
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE
}
};
match OpenOptions::new()
.create(true)
.read(true)
.write(true)
.share_mode(0)
.share_mode(share_mode)
.custom_flags(FILE_FLAG_OPEN_REPARSE_POINT)
.open(path)
{
@@ -875,16 +927,30 @@ pub(super) fn try_open_external_agent_runner_lock(
));
}
validate_windows_regular_file_handle(&file, label)?;
// share_mode(0) gives this process an exclusive handle. At this point the fixed
// lock path is known to be a stale, single-link, non-reparse regular file inside
// the current TokenUser's private AppData. Repairing its owner is therefore safe
// and is required when Windows creates it with TokenOwner=Administrators.
// The fixed lock path is known to be a single-link, non-reparse regular file
// inside the current TokenUser's private AppData. Repairing its owner is
// therefore safe and is required when Windows creates it with
// TokenOwner=Administrators.
crate::initialize_windows_game_creator_file_owner_for_current_user(path)?;
validate_windows_regular_file_handle(&file, label)?;
crate::secure_windows_game_creator_path_for_current_user(path, false, false)?;
Ok(Some(file))
}
Err(error) if windows_external_agent_runner_lock_is_busy_error(&error) => Ok(None),
Err(error)
if mode == ExternalAgentRunnerLockMode::Exclusive
&& windows_external_agent_runner_lock_is_busy_error(&error) =>
{
Ok(None)
}
Err(error)
if mode == ExternalAgentRunnerLockMode::Shared
&& windows_external_agent_runner_lock_is_busy_error(&error) =>
{
Err(format!(
"{label} 正被独占探测或持有,稍后重试:{}",
path.display()
))
}
Err(error) => Err(format!(
"安全打开 {label} 失败:{}: {error}",
path.display()
@@ -892,12 +958,29 @@ pub(super) fn try_open_external_agent_runner_lock(
}
}
#[cfg(windows)]
pub(super) fn try_open_external_agent_runner_lock(
path: &Path,
label: &str,
) -> Result<Option<File>, String> {
open_external_agent_runner_lock_file(path, label, ExternalAgentRunnerLockMode::Exclusive)
}
#[cfg(not(any(unix, windows)))]
pub(super) fn open_external_agent_runner_lock_file(
path: &Path,
label: &str,
_mode: ExternalAgentRunnerLockMode,
) -> Result<Option<File>, String> {
Err(format!("当前平台不支持 {label} 系统锁:{}", path.display()))
}
#[cfg(not(any(unix, windows)))]
pub(super) fn try_open_external_agent_runner_lock(
path: &Path,
label: &str,
) -> Result<Option<File>, String> {
Err(format!("当前平台不支持 {label} 系统锁:{}", path.display()))
open_external_agent_runner_lock_file(path, label, ExternalAgentRunnerLockMode::Exclusive)
}
pub(super) fn acquire_external_agent_runner_instance_lock(
@@ -929,40 +1012,90 @@ pub(super) fn acquire_external_agent_runner_instance_lock(
Ok(ExternalAgentRunnerInstanceLock { _file: file })
}
pub(crate) fn acquire_external_agent_runner_gui_owner_lock(
/// 取得本窗口在该 AppData 下的界面参与锁。
///
/// 参与锁以共享句柄打开:同一 AppData 可以同时持有任意数量的界面窗口。
/// Runner 侧用同文件的独占探测判断“是否仍有界面进程存活”,探测窗口很短,
/// 所以这里遇到瞬时冲突时按 `EXTERNAL_AGENT_RUNNER_GUI_PARTICIPANT_LOCK_RETRY_INTERVAL`
/// 重试,直到 `EXTERNAL_AGENT_RUNNER_GUI_PARTICIPANT_LOCK_ACQUIRE_TIMEOUT` 截止。
pub(crate) fn acquire_external_agent_runner_gui_participant_lock(
config_dir: &Path,
) -> Result<ExternalAgentRunnerGuiOwnerLock, String> {
let path = external_agent_runner_gui_owner_lock_path(config_dir);
let Some(mut file) = try_open_external_agent_runner_lock(&path, "Agent Runner GUI owner 锁")?
else {
return Err("AI 游戏创作界面已由同一 AppData 目录中的其他进程运行".to_string());
};
let owner_epoch = uuid::Uuid::new_v4().to_string();
let acquired_at = unix_millis();
) -> Result<ExternalAgentRunnerGuiParticipantLock, String> {
let path = external_agent_runner_gui_participant_lock_path(config_dir);
let deadline = Instant::now() + EXTERNAL_AGENT_RUNNER_GUI_PARTICIPANT_LOCK_ACQUIRE_TIMEOUT;
let mut last_error = "AGC 界面参与锁未知失败".to_string();
loop {
match open_external_agent_runner_lock_file(
&path,
"AGC 界面参与锁",
ExternalAgentRunnerLockMode::Shared,
) {
Ok(Some(mut file)) => {
write_external_agent_runner_gui_participant_diagnostic(&mut file, &path)?;
return Ok(ExternalAgentRunnerGuiParticipantLock { _file: file });
}
Ok(None) => {
last_error = format!("AGC 界面参与锁无法以共享方式取得:{}", path.display());
}
Err(error) => last_error = error,
}
if Instant::now() >= deadline {
return Err(format!("取得 AGC 界面参与锁失败:{last_error}"));
}
thread::sleep(EXTERNAL_AGENT_RUNNER_GUI_PARTICIPANT_LOCK_RETRY_INTERVAL);
}
}
/// 参与锁诊断内容只由首个窗口写入,后续窗口不覆写,避免并发写坏 JSON。
fn write_external_agent_runner_gui_participant_diagnostic(
file: &mut File,
path: &Path,
) -> Result<(), String> {
let existing_len = file.metadata().map(|metadata| metadata.len()).unwrap_or(0);
if existing_len > 0 {
return Ok(());
}
let diagnostic = serde_json::to_vec(&json!({
"protocolVersion": EXTERNAL_AGENT_RUNNER_PROTOCOL_VERSION,
"pid": std::process::id(),
"ownerEpoch": owner_epoch,
"acquiredAt": acquired_at,
"instanceId": uuid::Uuid::new_v4().to_string(),
"acquiredAt": unix_millis(),
}))
.map_err(|error| format!("生成 Agent Runner GUI owner 锁信息失败:{error}"))?;
.map_err(|error| format!("生成 AGC 界面参与锁信息失败:{error}"))?;
file.set_len(0)
.and_then(|_| file.seek(SeekFrom::Start(0)).map(|_| ()))
.and_then(|_| file.write_all(&diagnostic))
.and_then(|_| file.sync_data())
.map_err(|error| {
format!(
"写入 Agent Runner GUI owner 锁信息失败:{}: {error}",
path.display()
)
})?;
write_external_agent_runner_gui_owner_claim_atomic(config_dir, &owner_epoch, 0)?;
Ok(ExternalAgentRunnerGuiOwnerLock {
_file: file,
.map_err(|error| format!("写入 AGC 界面参与锁信息失败:{}: {error}", path.display()))
}
/// 发布新的 durable claim:新 epoch + 本次会话 revision。
///
/// 发布是“谁改动登录态谁成为新 epoch 权威”的实现;并发发布以最后一次
/// 成功写入为准,落败窗口按最新 claim 重试。
pub(crate) fn publish_external_agent_runner_gui_owner_claim(
config_dir: &Path,
session_revision: u64,
) -> Result<ExternalAgentRunnerGuiOwnerClaim, String> {
let owner_epoch = uuid::Uuid::new_v4().to_string();
write_external_agent_runner_gui_owner_claim_atomic(config_dir, &owner_epoch, session_revision)?;
Ok(ExternalAgentRunnerGuiOwnerClaim {
owner_epoch,
session_revision,
})
}
/// 采纳现有 durable claim;只有 claim 缺失或不可读时才发布新 claim。
pub(crate) fn adopt_or_publish_external_agent_runner_gui_owner_claim(
config_dir: &Path,
session_revision: u64,
) -> Result<ExternalAgentRunnerGuiOwnerClaim, String> {
match read_external_agent_runner_gui_owner_claim(config_dir) {
Ok(claim) => Ok(claim),
Err(_) => publish_external_agent_runner_gui_owner_claim(config_dir, session_revision),
}
}
pub(super) fn write_external_agent_runner_gui_owner_claim_atomic(
config_dir: &Path,
owner_epoch: &str,
@@ -13,8 +13,8 @@ pub(crate) const EXTERNAL_AGENT_RUNNER_PROTOCOL_VERSION: u32 = 7;
pub(super) const EXTERNAL_AGENT_RUNNER_ENDPOINT_FILE_NAME: &str = "agent-runner.endpoint.json";
pub(super) const EXTERNAL_AGENT_RUNNER_LOCK_FILE_NAME: &str = "agent-runner.lock";
pub(super) const EXTERNAL_AGENT_RUNNER_GUI_OWNER_LOCK_FILE_NAME: &str =
"agent-runner.gui-owner.lock";
pub(super) const EXTERNAL_AGENT_RUNNER_GUI_PARTICIPANT_LOCK_FILE_NAME: &str =
"agent-runner.gui-participant.lock";
pub(super) const EXTERNAL_AGENT_RUNNER_GUI_OWNER_CLAIM_FILE_NAME: &str =
"agent-runner.gui-owner.claim.json";
pub(super) const EXTERNAL_AGENT_RUNNER_PROJECT_OWNER_FILE_NAME: &str = "execution-owner.lock";
@@ -280,6 +280,10 @@ pub(super) struct ExternalAgentRunnerRequestParams {
pub(super) platform_api_base_url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(super) platform_auth_generation: Option<u64>,
/// 原生写入 revision:只用于 install / clear 的顺序判定。同一身份的凭据轮换会推进
/// revision,但不推进 `platform_auth_generation`(身份代次)。
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(super) platform_auth_revision: Option<u64>,
}
#[derive(Deserialize, Serialize)]
@@ -156,7 +156,7 @@ fn external_agent_runner_watchdog_tick(state: &ExternalAgentRunnerServerState) -
if !state.gui_owner_attached.load(Ordering::Acquire) {
return false;
}
match external_agent_runner_gui_owner_is_locked(&state.gui_owner_lock_path) {
match external_agent_runner_lock_is_held(&state.gui_participant_lock_path) {
Ok(true) => {
let _ = validate_external_agent_runner_gui_owner_claim_current(state);
false
@@ -224,7 +224,7 @@ pub(crate) fn run_external_agent_runner_server(
)?;
let gui_owner_present_at_start = resolve_external_agent_runner_initial_gui_owner(
gui_owner_required,
external_agent_runner_gui_owner_is_locked(&external_agent_runner_gui_owner_lock_path(
external_agent_runner_lock_is_held(&external_agent_runner_gui_participant_lock_path(
&config_dir,
))?,
)?;
@@ -16,7 +16,7 @@ pub(super) struct ExternalAgentRunnerServerState {
pub(super) draining: AtomicBool,
pub(super) active_connections: AtomicUsize,
pub(super) known_roots: Mutex<BTreeSet<PathBuf>>,
pub(super) gui_owner_lock_path: PathBuf,
pub(super) gui_participant_lock_path: PathBuf,
pub(super) project_execution_owners:
Mutex<BTreeMap<PathBuf, ExternalAgentRunnerProjectExecutionOwnerEntry>>,
project_execution_owner_recovery_changed: Condvar,
@@ -80,10 +80,10 @@ impl Drop for ExternalAgentRunnerProjectExecutionOwnerRecoveryGuard<'_> {
impl ExternalAgentRunnerServerState {
pub(super) fn new(endpoint_path: PathBuf, endpoint: ExternalAgentRunnerEndpoint) -> Self {
let gui_owner_lock_path = endpoint_path
let gui_participant_lock_path = endpoint_path
.parent()
.map(external_agent_runner_gui_owner_lock_path)
.unwrap_or_else(|| PathBuf::from(EXTERNAL_AGENT_RUNNER_GUI_OWNER_LOCK_FILE_NAME));
.map(external_agent_runner_gui_participant_lock_path)
.unwrap_or_else(|| PathBuf::from(EXTERNAL_AGENT_RUNNER_GUI_PARTICIPANT_LOCK_FILE_NAME));
Self {
endpoint_path,
endpoint: Mutex::new(endpoint),
@@ -94,7 +94,7 @@ impl ExternalAgentRunnerServerState {
draining: AtomicBool::new(false),
active_connections: AtomicUsize::new(0),
known_roots: Mutex::new(BTreeSet::new()),
gui_owner_lock_path,
gui_participant_lock_path,
project_execution_owners: Mutex::new(BTreeMap::new()),
project_execution_owner_recovery_changed: Condvar::new(),
write_request_cache: Mutex::new(ExternalAgentRunnerRequestCache::default()),
@@ -231,16 +231,10 @@ pub(super) struct ExternalAgentRunnerInstanceLock {
pub(super) _file: File,
}
/// 界面进程持有的参与锁。共享句柄,同一 AppData 可同时存在多个窗口。
#[derive(Debug)]
pub(crate) struct ExternalAgentRunnerGuiOwnerLock {
pub(crate) struct ExternalAgentRunnerGuiParticipantLock {
pub(super) _file: File,
pub(super) owner_epoch: String,
}
impl ExternalAgentRunnerGuiOwnerLock {
pub(crate) fn owner_epoch(&self) -> &str {
&self.owner_epoch
}
}
pub(super) struct ExternalAgentRunnerProjectOwnerStorage {
File diff suppressed because it is too large Load Diff
@@ -202,9 +202,13 @@ fn gui_final_exit_is_the_only_run_event_that_requests_runner_shutdown() {
GameCreatorGuiRunnerShutdownOutcome::NotRequested
);
assert_eq!(
resolve_game_creator_gui_runner_shutdown(&tauri::RunEvent::Exit, || Ok(())),
resolve_game_creator_gui_runner_shutdown(&tauri::RunEvent::Exit, || Ok(true)),
GameCreatorGuiRunnerShutdownOutcome::Requested
);
assert_eq!(
resolve_game_creator_gui_runner_shutdown(&tauri::RunEvent::Exit, || Ok(false)),
GameCreatorGuiRunnerShutdownOutcome::Retained
);
assert_eq!(
resolve_game_creator_gui_runner_shutdown(&tauri::RunEvent::Exit, || {
Err("private shutdown diagnostic".to_string())
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "陶泥儿",
"version": "0.1.45",
"version": "0.1.47",
"identifier": "world.genarrative.ai-game-creator",
"build": {
"beforeDevCommand": "npm --prefix ../.. run agc:serve",

Some files were not shown because too many files have changed in this diff Show More