合并最新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
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:
@@ -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
@@ -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(),
|
||||
|
||||
+2
-1
@@ -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
Reference in New Issue
Block a user