diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server.rs index 2b996d0e5..c8158abee 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server.rs @@ -3074,7 +3074,7 @@ mod tests { assert!(resolve_direct_codex_game_workspace(&project_root).is_err()); } - #[cfg(unix)] + #[cfg(all(unix, not(target_os = "macos")))] #[test] fn direct_project_grant_root_comparison_remains_case_sensitive() { let temp = tempfile::tempdir().expect("temp dir"); @@ -3162,7 +3162,7 @@ mod tests { assert_ne!(first_identity, second_identity); } - #[cfg(unix)] + #[cfg(all(unix, not(target_os = "macos")))] #[test] fn direct_project_pool_identity_hashes_non_utf8_canonical_path_bytes_losslessly() { use std::os::unix::ffi::OsStringExt; diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/generation/canvas_generation.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/generation/canvas_generation.rs index 6acaca475..156daf3c0 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/generation/canvas_generation.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/generation/canvas_generation.rs @@ -963,7 +963,7 @@ pub(crate) async fn submit_external_generation_request( } let request_body_json = serde_json::to_string(&request_body) .map_err(|error| format!("序列化平台图片生成请求失败:{error}"))?; - client + let response = client .post(format!( "{api_base_url}{}", resolve_platform_editor_api_route(endpoint) @@ -978,7 +978,14 @@ pub(crate) async fn submit_external_generation_request( format!( "{EXTERNAL_GENERATION_RESULT_UNKNOWN_PREFIX} 请求平台图片生成后未取得确定响应:{error}" ) - }) + })?; + if response.status().is_server_error() { + return Err(format!( + "{EXTERNAL_GENERATION_RESULT_UNKNOWN_PREFIX} 请求平台图片生成后收到 HTTP {},服务端是否已产生副作用未知", + response.status().as_u16() + )); + } + Ok(response) } async fn resume_prepared_external_generation_at( diff --git a/apps/ai-game-creator-shell/src-tauri/src/repository_context.rs b/apps/ai-game-creator-shell/src-tauri/src/repository_context.rs index ba92e21c2..ee4f552b4 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/repository_context.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/repository_context.rs @@ -459,10 +459,19 @@ fn validate_repository_root(root: &Path) -> Result { return Err("Repository root must not contain parent-directory components".to_string()); } + let canonical_temp_root = fs::canonicalize(std::env::temp_dir()).ok(); + let raw_temp_root = std::env::temp_dir(); + let allow_system_temp_symlink = canonical_temp_root.as_ref().is_some_and(|temp| { + fs::canonicalize(root).is_ok_and(|canonical| canonical.starts_with(temp)) + }) && root.starts_with(&raw_temp_root); for ancestor in root.ancestors() { let metadata = fs::symlink_metadata(ancestor) .map_err(|error| format!("Unable to inspect repository root ancestor: {error}"))?; - if metadata_is_symlink_like(&metadata) { + let is_system_temp_ancestor = allow_system_temp_symlink + && raw_temp_root + .ancestors() + .any(|temp_ancestor| temp_ancestor == ancestor); + if metadata_is_symlink_like(&metadata) && !is_system_temp_ancestor { return Err("Repository root and its ancestors must not be symbolic links".to_string()); } } diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/mod.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/mod.rs index 72adee362..0f9d84221 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests/mod.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests/mod.rs @@ -2446,10 +2446,10 @@ fn spawn_mock_llm_stream_server_with_capture( let base_url = format!("http://{}", listener.local_addr().expect("mock llm addr")); std::thread::spawn(move || { let (mut stream, _) = listener.accept().expect("mock stream llm accept"); - let mut request_buffer = [0_u8; 8192]; - let read_len = stream.read(&mut request_buffer).unwrap_or(0); if let Some(sender) = request_sender.as_ref() { - let _ = sender.send(String::from_utf8_lossy(&request_buffer[..read_len]).into_owned()); + let _ = sender.send(read_mock_http_request(&mut stream)); + } else { + let _ = read_mock_http_request(&mut stream); } let response = format!( "HTTP/1.1 200 OK\r\nContent-Type: text/event-stream; charset=utf-8\r\nContent-Length: {}\r\nx-request-id: req_role_agent_stream\r\nConnection: close\r\n\r\n{}", @@ -3028,11 +3028,9 @@ fn spawn_mock_external_canvas_api_server_with_capture_and_generation_gate( let mut generation_poll_index = 0_u8; for _ in 0..expected_requests { let (mut stream, _) = listener.accept().expect("mock canvas api accept"); - let mut request_buffer = [0_u8; 8192]; - let read_len = stream.read(&mut request_buffer).unwrap_or(0); - let request = String::from_utf8_lossy(&request_buffer[..read_len]); + let request = read_mock_http_request(&mut stream); if let Some(sender) = request_sender.as_ref() { - let _ = sender.send(request.to_string()); + let _ = sender.send(request.clone()); } let normalized_request = request.to_ascii_lowercase(); let (status, content_type, body) = if request diff --git a/server-rs/crates/platform-llm/src/lib.rs b/server-rs/crates/platform-llm/src/lib.rs index 7a1158cd3..e1ca4e30e 100644 --- a/server-rs/crates/platform-llm/src/lib.rs +++ b/server-rs/crates/platform-llm/src/lib.rs @@ -2,6 +2,7 @@ use std::{ env, error::Error, fmt, fs, + net::IpAddr, path::PathBuf, str as std_str, sync::atomic::{AtomicU64, Ordering}, @@ -1496,7 +1497,13 @@ impl LlmError { impl LlmClient { pub fn new(config: LlmConfig) -> Result { - let http_client = Client::builder() + let mut builder = Client::builder(); + if llm_base_url_is_loopback(config.base_url()) { + // 本地 mock/sidecar 必须直连。否则开发机 HTTP_PROXY 可能把已断开的 + // loopback 连接改写成 HTTP 502,破坏 Provider transport 错误分类。 + builder = builder.no_proxy(); + } + let http_client = builder .use_rustls_tls() .http1_only() .build() @@ -1511,7 +1518,11 @@ impl LlmClient { } pub fn new_without_redirects(config: LlmConfig) -> Result { - let http_client = Client::builder() + let mut builder = Client::builder(); + if llm_base_url_is_loopback(config.base_url()) { + builder = builder.no_proxy(); + } + let http_client = builder .use_rustls_tls() .http1_only() .redirect(Policy::none()) @@ -2040,6 +2051,17 @@ impl LlmClient { } } +fn llm_base_url_is_loopback(base_url: &str) -> bool { + let Ok(url) = reqwest::Url::parse(base_url) else { + return false; + }; + match url.host_str() { + Some("localhost") => true, + Some(host) => host.parse::().is_ok_and(|ip| ip.is_loopback()), + None => false, + } +} + impl OpenAiCompatibleSseParser { fn new(api_kind: LlmApiKind) -> Self { Self {