diff --git a/.gitea/workflows/project-ci.yml b/.gitea/workflows/project-ci.yml index 2de097ee1..0b20e28e2 100644 --- a/.gitea/workflows/project-ci.yml +++ b/.gitea/workflows/project-ci.yml @@ -69,23 +69,8 @@ jobs: - name: Install npm dependencies run: bash scripts/ci-npm-ci-with-retry.sh - - name: Run repository lint gates - run: npm run lint - - - name: Build web applications - run: npm run build - - - name: Validate content data - run: npm run check:content - - - name: Check committed whitespace - shell: bash - run: | - set -euo pipefail - base_ref="${SPACETIME_SCHEMA_BASE_REF:-}" - test -n "${base_ref}" - git cat-file -e "${base_ref}^{commit}" - git diff --check "${base_ref}"...HEAD + - name: Run repository checks + run: npm run check:repository-ci frontend-tests: name: Frontend tests diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100755 index 000000000..fdb72ecc2 --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1 @@ +npm run check:pre-push-master -- "$@" 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 eeedc2dfd..08348c123 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 @@ -20,6 +20,8 @@ const GAME_CREATOR_CODEX_APP_SERVER_THREAD_MAX: usize = 128; const GAME_CREATOR_CODEX_APP_SERVER_RPC_TIMEOUT_MS: u64 = 30_000; pub(in crate::agent) const GAME_CREATOR_CODEX_APP_SERVER_TERMINAL_UNKNOWN_PREFIX: &str = "codex-app-server-terminal-unknown:"; +pub(in crate::agent) const GAME_CREATOR_CODEX_APP_SERVER_ERROR_KIND_PREFIX: &str = + "codex-app-server-error:"; type RpcResult = Result; @@ -177,6 +179,92 @@ fn game_creator_codex_app_server_terminal_unknown( )) } +fn game_creator_codex_app_server_error_kind(kind: &str) -> platform_llm::LlmError { + platform_llm::LlmError::InvalidRequest(format!( + "{GAME_CREATOR_CODEX_APP_SERVER_ERROR_KIND_PREFIX}{kind}" + )) +} + +fn game_creator_codex_app_server_error_http_status( + info: &serde_json::Value, + field: &str, +) -> Option { + info.get(field)? + .get("httpStatusCode")? + .as_u64() + .and_then(|status| u16::try_from(status).ok()) + .filter(|status| (100..=599).contains(status)) +} + +fn game_creator_codex_app_server_connection_error( + info: &serde_json::Value, + field: &str, +) -> platform_llm::LlmError { + match game_creator_codex_app_server_error_http_status(info, field) { + Some(401 | 403) => game_creator_codex_app_server_error_kind("unauthorized"), + Some(status_code) => platform_llm::LlmError::Upstream { + status_code, + message: "Codex app-server 连接上游失败".to_string(), + }, + None => platform_llm::LlmError::Connectivity { + attempts: 1, + message: "Codex app-server 连接失败".to_string(), + }, + } +} + +fn game_creator_codex_app_server_failed_turn_error( + turn: &serde_json::Value, +) -> platform_llm::LlmError { + let Some(info) = turn + .get("error") + .and_then(|error| error.get("codexErrorInfo")) + .filter(|info| !info.is_null()) + else { + return game_creator_codex_app_server_error_kind("other"); + }; + if let Some(kind) = info.as_str() { + return match kind { + "contextWindowExceeded" => { + game_creator_codex_app_server_error_kind("context-window-exceeded") + } + "sessionBudgetExceeded" => { + game_creator_codex_app_server_error_kind("session-budget-exceeded") + } + "usageLimitExceeded" => { + game_creator_codex_app_server_error_kind("usage-limit-exceeded") + } + "serverOverloaded" | "internalServerError" => platform_llm::LlmError::Upstream { + status_code: 503, + message: "Codex app-server 上游服务暂时不可用".to_string(), + }, + "cyberPolicy" => game_creator_codex_app_server_error_kind("cyber-policy"), + "unauthorized" => game_creator_codex_app_server_error_kind("unauthorized"), + "badRequest" => game_creator_codex_app_server_error_kind("bad-request"), + "threadRollbackFailed" => { + game_creator_codex_app_server_error_kind("thread-rollback-failed") + } + "sandboxError" => game_creator_codex_app_server_error_kind("sandbox-error"), + "other" => game_creator_codex_app_server_error_kind("other"), + _ => game_creator_codex_app_server_error_kind("other"), + }; + } + for field in [ + "httpConnectionFailed", + "responseStreamConnectionFailed", + "responseStreamDisconnected", + "responseTooManyFailedAttempts", + ] { + if info.get(field).is_some() { + return game_creator_codex_app_server_connection_error(info, field); + } + } + if info.get("activeTurnNotSteerable").is_some() { + return game_creator_codex_app_server_error_kind("active-turn-not-steerable"); + } + game_creator_codex_app_server_error_kind("other") +} + async fn isolate_game_creator_codex_app_server_terminal_unknown( inner: &Arc, detail: impl Into, @@ -1012,9 +1100,7 @@ impl CodexAppServerConnection { )) } "failed" => { - return Err(platform_llm::LlmError::InvalidRequest( - "Codex app-server turn 执行失败".to_string(), - )) + return Err(game_creator_codex_app_server_failed_turn_error(turn)) } status => { return Err(platform_llm::LlmError::Deserialize(format!( @@ -1577,6 +1663,81 @@ mod tests { assert!(response.text.is_empty()); } + #[test] + fn codex_app_server_failed_turn_uses_structured_error_info_without_raw_details() { + let secret = ["sk", "turn-secret"].join("-"); + let failed_turn = serde_json::json!({ + "status": "failed", + "error": { + "message": format!("private message {secret}"), + "additionalDetails": "https://provider.example/private C:\\Users\\victim\\project", + "codexErrorInfo": "contextWindowExceeded" + } + }); + let error = game_creator_codex_app_server_failed_turn_error(&failed_turn); + assert_eq!( + error, + platform_llm::LlmError::InvalidRequest( + "codex-app-server-error:context-window-exceeded".to_string() + ) + ); + let visible = error.to_string(); + assert!(!visible.contains(&secret)); + assert!(!visible.contains("provider.example")); + assert!(!visible.contains("victim")); + } + + #[test] + fn codex_app_server_failed_turn_maps_stable_categories_and_http_status() { + for (info, expected) in [ + ( + serde_json::json!("usageLimitExceeded"), + platform_llm::LlmError::InvalidRequest( + "codex-app-server-error:usage-limit-exceeded".to_string(), + ), + ), + ( + serde_json::json!("unauthorized"), + platform_llm::LlmError::InvalidRequest( + "codex-app-server-error:unauthorized".to_string(), + ), + ), + ( + serde_json::json!({"httpConnectionFailed":{"httpStatusCode":429}}), + platform_llm::LlmError::Upstream { + status_code: 429, + message: "Codex app-server 连接上游失败".to_string(), + }, + ), + ( + serde_json::json!({"responseStreamDisconnected":{"httpStatusCode":null}}), + platform_llm::LlmError::Connectivity { + attempts: 1, + message: "Codex app-server 连接失败".to_string(), + }, + ), + ] { + let turn = serde_json::json!({ + "status": "failed", + "error": { + "message": "private upstream body", + "additionalDetails": "private diagnostics", + "codexErrorInfo": info + } + }); + assert_eq!( + game_creator_codex_app_server_failed_turn_error(&turn), + expected + ); + } + assert_eq!( + game_creator_codex_app_server_failed_turn_error( + &serde_json::json!({"status":"failed","error":null}) + ), + platform_llm::LlmError::InvalidRequest("codex-app-server-error:other".to_string()) + ); + } + #[test] fn codex_app_server_rejects_non_responses_key_mapping() { let mut llm = test_llm(); diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/generation/loop_orchestration.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/generation/loop_orchestration.rs index dd84942a3..43a6aa0ed 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/generation/loop_orchestration.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/generation/loop_orchestration.rs @@ -547,7 +547,13 @@ pub(crate) fn game_creator_agent_llm_error_public_summary( (format!("upstream-{status_code}"), Some(*status_code)) } platform_llm::LlmError::InvalidConfig(_) => ("invalid-config".to_string(), None), - platform_llm::LlmError::InvalidRequest(_) => ("invalid-request".to_string(), None), + platform_llm::LlmError::InvalidRequest(message) => ( + message + .strip_prefix(GAME_CREATOR_CODEX_APP_SERVER_ERROR_KIND_PREFIX) + .map(|kind| format!("codex-app-server-{kind}")) + .unwrap_or_else(|| "invalid-request".to_string()), + None, + ), platform_llm::LlmError::StreamUnavailable => ("stream-unavailable".to_string(), None), platform_llm::LlmError::EmptyResponse => ("empty-response".to_string(), None), platform_llm::LlmError::Deserialize(_) => ("deserialize".to_string(), None), diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop.rs index 3f31ca775..fa6ecd3fc 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop.rs @@ -60,6 +60,25 @@ pub(super) fn game_creator_agent_background_final_reply_fallback( } } +pub(super) fn game_creator_agent_final_reply_error_allows_fallback(error: &str) -> bool { + const KIND_PREFIX: &str = "kind="; + let Some(start) = error.rfind(KIND_PREFIX) else { + return false; + }; + if error[..start] + .chars() + .next_back() + .is_some_and(|boundary| !boundary.is_whitespace() && boundary != ':' && boundary != ':') + { + return false; + } + let kind = error[start + KIND_PREFIX.len()..] + .chars() + .take_while(|character| character.is_ascii_lowercase() || *character == '-') + .collect::(); + matches!(kind.as_str(), "empty-response" | "deserialize") +} + fn requested_game_chat_fast_path_plan_at( root: &Path, plan: AgentRuntimeToolPlan, @@ -3799,7 +3818,10 @@ async fn run_game_creator_agent_background_task_pass_without_deadline( { return AgentBackgroundTaskOutcome::NeedsReconciliation; } - Err(_) if final_reply_fallback.is_some() => { + Err(error) + if final_reply_fallback.is_some() + && game_creator_agent_final_reply_error_allows_fallback(&error) => + { final_reply_fallback.expect("checked final reply fallback") } Err(error) => { diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop_tests.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop_tests.rs index 5f3d126cb..ecee106d9 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop_tests.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop_tests.rs @@ -4529,6 +4529,36 @@ fn plan_response_precedes_autonomous_supervisor_deterministic_fallback() { ); } +#[test] +fn autonomous_final_reply_fallback_only_accepts_safe_response_shape_failures() { + let fingerprint = "a".repeat(64); + for allowed in ["empty-response", "deserialize"] { + assert!(game_creator_agent_final_reply_error_allows_fallback( + &format!("后台 Agent 最终回复调用 LLM 失败:kind={allowed} fingerprint={fingerprint} chars=12") + )); + } + for rejected in [ + "codex-app-server-unauthorized", + "codex-app-server-usage-limit-exceeded", + "codex-app-server-context-window-exceeded", + "codex-app-server-cyber-policy", + "codex-app-server-sandbox-error", + "invalid-config", + "transport", + "upstream-503", + ] { + assert!( + !game_creator_agent_final_reply_error_allows_fallback(&format!( + "后台 Agent 最终回复调用 LLM 失败:kind={rejected} fingerprint={fingerprint} chars=12" + )), + "final reply fallback must reject {rejected}" + ); + } + assert!(!game_creator_agent_final_reply_error_allows_fallback( + "上游自由文本kind=deserialize fingerprint=private" + )); +} + #[tokio::test] async fn autonomous_supervisor_converged_final_reply_deserialize_commits_fallback_once() { const RUN_ID: &str = "autonomous-final-reply-fallback-run"; diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/provider_retry.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/provider_retry.rs index 0a15a6860..ef3faa7bc 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/provider_retry.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/provider_retry.rs @@ -204,6 +204,40 @@ pub(in crate::agent) fn game_creator_agent_runtime_failure_conversation_message( } else { "专业 Agent" }; + let public_kind_prefix = "kind=codex-app-server-"; + let codex_error_kind = error + .rfind(public_kind_prefix) + .map(|start| &error[start + public_kind_prefix.len()..]) + .or_else(|| { + error + .rfind(GAME_CREATOR_CODEX_APP_SERVER_ERROR_KIND_PREFIX) + .map(|start| { + &error[start + GAME_CREATOR_CODEX_APP_SERVER_ERROR_KIND_PREFIX.len()..] + }) + }) + .map(|kind| { + kind.chars() + .take_while(|character| character.is_ascii_lowercase() || *character == '-') + .collect::() + }) + .filter(|kind| !kind.is_empty()); + if let Some(kind) = codex_error_kind { + let detail = match kind + .trim_matches(|character: char| !character.is_ascii_lowercase() && character != '-') + { + "context-window-exceeded" => "模型上下文已超限,请缩小任务范围后重试", + "session-budget-exceeded" => "本次会话预算已耗尽,请缩小任务范围或新建任务", + "usage-limit-exceeded" => "Codex 用量已达上限,请检查账户额度后重试", + "unauthorized" => "Codex 鉴权失败,请重新登录或检查 API Key", + "bad-request" => "Codex 请求无效,请检查模型与运行时配置", + "cyber-policy" => "Codex 安全策略拒绝了本次请求,请调整任务内容", + "sandbox-error" => "Codex 隔离环境启动失败,请重试或检查本机环境", + "thread-rollback-failed" => "Codex 会话恢复失败,请新建任务后重试", + "active-turn-not-steerable" => "当前 Codex 任务无法追加指令,请等待结束后重试", + _ => "Codex 执行失败,请查看运行详情后重试", + }; + return format!("{subject} {detail}"); + } if let Some((http_status, retry_attempt, max_retries)) = game_creator_agent_runtime_exhausted_upstream_retry_fields(error) { @@ -1956,4 +1990,43 @@ mod tests { assert!(!unrelated_visible.contains("absolute-path")); assert!(!unrelated_visible.contains("redacted-secret")); } + + #[test] + fn codex_app_server_failure_kind_has_actionable_safe_public_summary() { + let private_error = format!( + "agentLlm.code-prototype 调用 LLM 失败:kind=codex-app-server-context-window-exceeded fingerprint={} chars=999", + "a".repeat(64) + ); + assert_eq!( + game_creator_agent_runtime_failure_conversation_message( + "code-prototype", + &private_error, + ), + "专业 Agent 模型上下文已超限,请缩小任务范围后重试" + ); + let unauthorized = format!( + "kind=codex-app-server-unauthorized fingerprint={} chars=32", + "b".repeat(64) + ); + assert_eq!( + game_creator_agent_runtime_failure_conversation_message( + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + &unauthorized, + ), + "项目总控 Agent Codex 鉴权失败,请重新登录或检查 API Key" + ); + for visible in [ + game_creator_agent_runtime_failure_conversation_message( + "code-prototype", + &private_error, + ), + game_creator_agent_runtime_failure_conversation_message( + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + &unauthorized, + ), + ] { + assert!(!visible.contains("fingerprint")); + assert!(!visible.contains("chars=")); + } + } } diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_state.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_state.rs index 820ea4827..8ca059907 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_state.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_state.rs @@ -2599,6 +2599,21 @@ pub(super) fn append_game_creator_agent_runtime_event_with_action( ) })?; } + let failure_detail = matches!( + event_type, + "error" | "turn.failed" | "turn.budget_exhausted" + ) + .then(|| { + detail.map(|value| game_creator_agent_runtime_public_failure_detail(&state.agent_id, value)) + }) + .flatten(); + let public_text = if matches!(event_type, "turn.failed" | "turn.budget_exhausted") { + failure_detail + .clone() + .or_else(|| game_creator_agent_runtime_public_event_text(root, event_type, summary)) + } else { + game_creator_agent_runtime_public_event_text(root, event_type, summary) + }; let event = AgentRuntimeEvent { schema_version: AGENT_RUNTIME_SCHEMA_VERSION.to_string(), agent_id: state.agent_id.clone(), @@ -2612,7 +2627,7 @@ pub(super) fn append_game_creator_agent_runtime_event_with_action( status: status.to_string(), phase: phase.to_string(), summary: summary.to_string(), - public_text: game_creator_agent_runtime_public_event_text(root, event_type, summary), + public_text, detail: detail .filter(|_| { !(event_type == "observation" @@ -2624,10 +2639,9 @@ pub(super) fn append_game_creator_agent_runtime_event_with_action( event_type, "error" | "turn.failed" | "turn.budget_exhausted" ) { - return game_creator_agent_runtime_public_failure_detail( - &state.agent_id, - value, - ); + return failure_detail.clone().unwrap_or_else(|| { + game_creator_agent_runtime_public_failure_detail(&state.agent_id, value) + }); } let max_chars = if event_type == "observation" && summary.starts_with("agent.action_history:") 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 d0fe0840b..8bf085900 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 @@ -1628,17 +1628,23 @@ pub(crate) fn spawn_mock_llm_tool_plan_then_invalid_final_reply( .write_all(planning_response.as_bytes()) .expect("mock tool plan response"); - let (mut final_stream, _) = listener.accept().expect("mock final reply accept"); - drop(read_mock_http_request(&mut final_stream)); - let invalid_body = "{invalid-json"; - let final_response = format!( - "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", - invalid_body.len(), - invalid_body - ); - final_stream - .write_all(final_response.as_bytes()) - .expect("mock invalid final reply response"); + // Autonomous runs enforce a 12-retry floor. Return the same malformed + // response for the initial final-reply request and every retry so this + // fixture tests deserialize exhaustion rather than an accidental + // connection-refused fallback after the first malformed response. + for _ in 0..=12 { + let (mut final_stream, _) = listener.accept().expect("mock final reply accept"); + drop(read_mock_http_request(&mut final_stream)); + let invalid_body = "{invalid-json"; + let final_response = format!( + "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", + invalid_body.len(), + invalid_body + ); + final_stream + .write_all(final_response.as_bytes()) + .expect("mock invalid final reply response"); + } }); base_url } diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/response_stream.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/response_stream.rs index b9d345fde..b0dadfb3f 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests/response_stream.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests/response_stream.rs @@ -617,7 +617,8 @@ async fn response_stream_private_process_output_is_never_published_or_committed_ } #[tokio::test] -async fn response_stream_final_failure_with_retry_disabled_commits_planning_fallback() { +async fn response_stream_final_disconnect_with_retry_disabled_fails_without_committing_planning_fallback( +) { let root = unique_project_path(); init_local_game_project_at( &root, @@ -686,32 +687,28 @@ async fn response_stream_final_failure_with_retry_disabled_commits_planning_fall ); drop(finalization_lock); - let completed = wait_for_agent_runtime_idle(&root, "design-director"); - assert_eq!(completed.phase, "completed"); - assert_eq!(completed.last_response.as_deref(), Some(planning_fallback)); - let committed = wait_for_response_stream_status( - &root, - "design-director", - run_id, - "committed", - ready.sequence.saturating_add(1), - ); - assert_eq!(committed.accumulated_text, planning_fallback); + let failed = wait_for_agent_runtime_phase(&root, "design-director", "failed"); + assert_eq!(failed.status, "failed"); + assert!(failed + .error + .as_deref() + .is_some_and(|error| error.contains("kind=transport"))); + assert_eq!(failed.last_response, None); let conversation = read_local_conversation_for_session_at( &root, Some("design-director"), Some(&started.state.session_id), ) - .expect("read committed fallback conversation"); - assert_eq!( - conversation - .messages - .iter() - .filter(|message| message.role == "assistant") - .map(|message| message.content.as_str()) - .collect::>(), - vec![planning_fallback] - ); + .expect("read failed final stream conversation"); + let assistant_messages = conversation + .messages + .iter() + .filter(|message| message.role == "assistant") + .map(|message| message.content.as_str()) + .collect::>(); + assert_eq!(assistant_messages.len(), 1); + assert_ne!(assistant_messages[0], planning_fallback); + assert!(assistant_messages[0].contains("失败")); let requests = mock.stop_and_collect(); assert_eq!( @@ -760,11 +757,21 @@ async fn response_stream_final_failure_with_retry_disabled_commits_planning_fall final_lifecycle[0]["requestSlot"], final_lifecycle[1]["requestSlot"] ); - assert_response_stream_completion_event_details( - &root, - "design-director", - run_id, - planning_fallback, + let mut final_reply_failed_audit = false; + for _ in 0..250 { + final_reply_failed_audit = read_agent_db_records_for_test(&root).iter().any(|record| { + record["recordType"] == "agent.runtime.background_task.failed" + && record["runId"] == run_id + && record["failureKind"] == "final-reply-failed" + }); + if final_reply_failed_audit { + break; + } + std::thread::sleep(Duration::from_millis(20)); + } + assert!( + final_reply_failed_audit, + "final reply transport failure audit must eventually persist" ); assert_response_stream_public_surfaces_exclude(&root, "design-director", &[planning_fallback]); diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_state.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_state.rs index 4d013debf..d77a1e92c 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_state.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_state.rs @@ -147,12 +147,23 @@ fn runtime_events_expose_stable_ids_and_backend_owned_public_text_only() { "public-event-action-1", ) .expect("repeat public action idempotently"); + append_game_creator_agent_runtime_action_event( + &root, + &state, + "turn.failed", + "failed", + "failed", + "Agent Runtime 本轮处理失败。", + Some("kind=codex-app-server-context-window-exceeded fingerprint=private"), + "public-event-failure-1", + ) + .expect("append classified public failure"); let events = read_recent_game_creator_agent_runtime_events( &game_creator_agent_runtime_event_path(&root, "code-prototype"), ) .expect("read public runtime events"); - assert_eq!(events.len(), 4); + assert_eq!(events.len(), 5); assert!(events.iter().all(|event| !event.event_id.trim().is_empty())); let mut event_ids = events .iter() @@ -176,6 +187,16 @@ fn runtime_events_expose_stable_ids_and_backend_owned_public_text_only() { .as_deref() .unwrap_or_default() .contains("raw tool input must stay private")); + assert_eq!( + events[4].public_text.as_deref(), + Some("专业 Agent 模型上下文已超限,请缩小任务范围后重试") + ); + assert_eq!(events[4].detail, events[4].public_text); + assert!(!events[4] + .public_text + .as_deref() + .unwrap_or_default() + .contains("fingerprint")); fs::remove_dir_all(root).ok(); } diff --git a/apps/ai-game-creator-shell/src/app/types.ts b/apps/ai-game-creator-shell/src/app/types.ts index 6cf04d7c7..d97a3ca63 100644 --- a/apps/ai-game-creator-shell/src/app/types.ts +++ b/apps/ai-game-creator-shell/src/app/types.ts @@ -846,6 +846,7 @@ export interface AgentStatusCard { runtimeWaitingOn: string | null; runtimeNextStep: string | null; runtimeTask: string | null; + runtimeError?: string | null; runtimeRunId: string | null; runtimeLoopIteration: number | null; runtimeMaxLoopIterations: number | null; diff --git a/apps/ai-game-creator-shell/src/features/agent-runtime/model.ts b/apps/ai-game-creator-shell/src/features/agent-runtime/model.ts index 9113fecc6..4e7e13d4e 100644 --- a/apps/ai-game-creator-shell/src/features/agent-runtime/model.ts +++ b/apps/ai-game-creator-shell/src/features/agent-runtime/model.ts @@ -709,8 +709,16 @@ export function agentRuntimeStartedRunId( export function isAgentRuntimeTerminalState(runtime: AgentRuntimeState) { return ( - ['completed', 'failed', 'cancelled'].includes(runtime.phase) || - ['completed', 'failed', 'cancelled', 'idle'].includes(runtime.status) + ['completed', 'failed', 'cancelled', 'needs-reconciliation'].includes( + runtime.phase, + ) || + [ + 'completed', + 'failed', + 'cancelled', + 'idle', + 'needs-reconciliation', + ].includes(runtime.status) ); } @@ -979,6 +987,12 @@ function agentRuntimeProviderRetryStatus(runtime: AgentRuntimeState) { export function agentRuntimeConversationStatus(runtime: AgentRuntimeState) { if (isAgentRuntimeTerminalState(runtime)) { + if ( + runtime.status === 'needs-reconciliation' || + runtime.phase === 'needs-reconciliation' + ) { + return 'Agent 运行状态需要核对,正在同步记录'; + } if (runtime.status === 'failed' || runtime.phase === 'failed') { return 'Agent 运行失败,正在同步错误记录'; } @@ -1023,6 +1037,14 @@ export function projectSupervisorChatRuntimeStatus(runtime: AgentRuntimeState) { if (runtime.status === 'idle' || runtime.phase === 'idle') { return '等待输入'; } + if ( + runtime.status === 'needs-reconciliation' || + runtime.phase === 'needs-reconciliation' + ) { + return runtime.error + ? projectRuntimeVisibleError(runtime.error, '项目总控 Agent', true) + : '项目总控 Agent 运行状态需要核对,请打开运行详情后重试'; + } if (isAgentRuntimeTerminalState(runtime)) { if (runtime.status === 'failed' || runtime.phase === 'failed') { return runtime.error @@ -1043,15 +1065,13 @@ export function formatAgentRuntimeEvent(event: AgentRuntimeEventRecord) { 'turn.failed', 'turn.budget_exhausted', ].includes(event.eventType); - const containsInternalFailureDiagnostics = Boolean( - event.detail && - /(?:errorSha256|errorChars|fingerprint|chars|retryAttempt|retryState)=|<(?:absolute-path|redacted-url)>|\[redacted(?:[- ]secret| sensitive context)\]/i.test( - event.detail, - ), - ); - const visibleDetail = - isFailureEvent && containsInternalFailureDiagnostics ? null : event.detail; + const visibleDetail = isFailureEvent ? null : event.detail; + const publicFailureText = + isFailureEvent && typeof event.publicText === 'string' + ? event.publicText.trim() + : ''; const summary = + publicFailureText || event.summary || visibleDetail || (isFailureEvent ? 'Agent Runtime 本轮处理失败。' : event.runId); @@ -1371,11 +1391,16 @@ export function projectSupervisorRuntimeStatusLabel( runtime: AgentRuntimeState | null, runtimeError: string, ) { + if ( + runtime?.status === 'needs-reconciliation' || + runtime?.phase === 'needs-reconciliation' + ) { + return '待核对'; + } if ( runtimeError || runtime?.status === 'failed' || - runtime?.phase === 'failed' || - runtime?.phase === 'needs-reconciliation' + runtime?.phase === 'failed' ) { return '失败'; } @@ -1641,6 +1666,27 @@ export function projectRuntimeVisibleError( if (isMudPointInsufficientRuntimeError(message)) { return MUD_POINT_INSUFFICIENT_INTERRUPTION_MESSAGE; } + const codexAppServerKind = visibleMessage.match( + /(?:^|[\s::])kind=codex-app-server-([a-z-]+)(?=\s|$)/, + )?.[1]; + if (codexAppServerKind) { + const detail = { + 'context-window-exceeded': '模型上下文已超限,请缩小任务范围后重试', + 'session-budget-exceeded': '本次会话预算已耗尽,请缩小任务范围或新建任务', + 'usage-limit-exceeded': 'Codex 用量已达上限,请检查账户额度后重试', + unauthorized: 'Codex 鉴权失败,请重新登录或检查 API Key', + 'bad-request': 'Codex 请求无效,请检查模型与运行时配置', + 'cyber-policy': 'Codex 安全策略拒绝了本次请求,请调整任务内容', + 'sandbox-error': 'Codex 隔离环境启动失败,请重试或检查本机环境', + 'thread-rollback-failed': 'Codex 会话恢复失败,请新建任务后重试', + 'active-turn-not-steerable': + '当前 Codex 任务无法追加指令,请等待结束后重试', + other: 'Codex 执行失败,请查看运行详情后重试', + }[codexAppServerKind]; + if (detail) { + return `${subject} ${detail}`; + } + } const exhaustedUpstreamRetry = visibleMessage.match( /(?:^|[\s::])kind=upstream-(\d{3}) httpStatus=(\d{3}) fingerprint=[0-9a-f]{64} chars=\d+ retryAttempt=(\d+) maxRetries=(\d+) retryState=exhausted\s*$/, ); @@ -1698,6 +1744,54 @@ export function projectRuntimeVisibleError( ) { return `${subject} 服务繁忙,请稍后重试`; } + if ( + normalized.includes('needs-reconciliation') || + normalized.includes('result-unknown') || + normalized.includes('终态未知') || + normalized.includes('需要人工核对') + ) { + return `${subject} 运行状态需要核对,请打开运行详情后重试`; + } + if ( + normalized.includes('budget-exhausted') || + normalized.includes('预算耗尽') || + normalized.includes('预算已耗尽') + ) { + return `${subject} 本轮预算已耗尽,请缩小任务范围后重试`; + } + if ( + normalized.includes('missing expected artifact') || + normalized.includes('expected artifact') || + normalized.includes('缺少预期产物') || + normalized.includes('缺少 expected artifact') + ) { + return `${subject} 未生成要求的产物,请查看任务要求后重试`; + } + if ( + normalized.includes('verification') || + normalized.includes('project.verify') || + normalized.includes('preview.validate') || + normalized.includes('验证未通过') || + normalized.includes('验证失败') + ) { + return `${subject} 项目验证未通过,请查看运行详情并修复后重试`; + } + if ( + normalized.includes('policy') || + normalized.includes('permission') || + normalized.includes('拒绝') || + normalized.includes('禁止') || + normalized.includes('不允许') + ) { + return `${subject} 被项目权限或安全策略阻止,请检查审批配置`; + } + if ( + normalized.includes('落盘失败') || + normalized.includes('持久化失败') || + normalized.includes('写入失败') + ) { + return `${subject} 保存运行记录失败,请检查项目目录后重试`; + } const containsInternalDiagnostics = normalized.includes('agentllm.') || /(?:^|[\s::])kind=/.test(normalized) || @@ -1725,7 +1819,8 @@ export function projectRuntimeVisibleError( export function projectSupervisorVisibleConversationText( message: string, - role: ChatMessage['role'] = 'assistant', + role: ChatMessage['role'] | 'tool' = 'assistant', + subject = '项目总控 Agent', ) { const failurePrefix = '后台任务失败:'; if (role !== 'assistant' || !message.startsWith(failurePrefix)) { @@ -1733,7 +1828,7 @@ export function projectSupervisorVisibleConversationText( } return projectRuntimeVisibleError( message.slice(failurePrefix.length), - '项目总控 Agent', + subject, true, ); } @@ -1848,9 +1943,22 @@ export function formatAgentRecentRuntimeTask(task: AgentRuntimeTaskRecord) { const goalSource = task.goalId ? `Goal ${task.goalStatus ?? '-'} · revision ${task.goalRevision ?? 0}` : null; + const failed = + task.status === 'failed' || + ['failed', 'budget-exhausted', 'needs-reconciliation'].includes(task.phase); + const failureDetail = failed + ? task.terminalDetail?.trim() || task.error?.trim() || null + : null; + const failureSummary = failureDetail + ? projectRuntimeVisibleError( + failureDetail, + projectProfessionalAgentLabel(task.agentId), + true, + ) + : null; return `${task.status} / ${task.phase} · ${ task.task || task.currentAction || task.runId }${goalSource ? ` · ${goalSource}` : ''}${ delegationSource ? ` · ${delegationSource}` : '' - }`; + }${failureSummary ? ` · 失败原因:${failureSummary}` : ''}`; } diff --git a/apps/ai-game-creator-shell/src/features/project-summary/agentPresentation.ts b/apps/ai-game-creator-shell/src/features/project-summary/agentPresentation.ts index 4e7824c0e..9a23973e1 100644 --- a/apps/ai-game-creator-shell/src/features/project-summary/agentPresentation.ts +++ b/apps/ai-game-creator-shell/src/features/project-summary/agentPresentation.ts @@ -41,7 +41,9 @@ import { isAgentRuntimeTerminalState, isGameChatSupervisorRoot, projectCurrentGameChatRuntimeLineage, + projectProfessionalAgentLabel, projectRuntimeVisibleCurrentWork, + projectRuntimeVisibleError, taskRowsFromManifest, } from '../agent-runtime'; import { @@ -157,6 +159,26 @@ export function taskRowsForAgentStatus( return mergedTasks; } +function agentRuntimeFailureDetail(runtime: AgentRuntimeState) { + const runtimeError = runtime.error?.trim(); + if (runtimeError) { + return runtimeError; + } + const terminalTask = [...(runtime.recentTasks ?? [])] + .reverse() + .find( + (task) => + task.runId === runtime.runId && + (task.status === 'failed' || + ['failed', 'budget-exhausted', 'needs-reconciliation'].includes( + task.phase, + )), + ); + return ( + terminalTask?.terminalDetail?.trim() || terminalTask?.error?.trim() || null + ); +} + export function deriveAgentStatusCards( nextManifest: GameCreationAppManifest, trace: GameCreationAgentRunTrace | null, @@ -208,6 +230,7 @@ export function deriveAgentStatusCards( ? (runtime.nextStep ?? agentRuntimeNextStepFromPhase(runtime.phase)) : null, runtimeTask: runtime?.currentTask ?? null, + runtimeError: runtime ? agentRuntimeFailureDetail(runtime) : null, runtimeRunId: runtime?.runId ?? null, runtimeLoopIteration: runtime?.loopIteration ?? null, runtimeMaxLoopIterations: runtime?.maxLoopIterations ?? null, @@ -263,7 +286,12 @@ export function projectAgentRuntimeSummaries( ) { return 4; } - if (runtime.status === 'failed' || runtime.phase === 'failed') { + if ( + runtime.status === 'failed' || + runtime.phase === 'failed' || + runtime.status === 'needs-reconciliation' || + runtime.phase === 'needs-reconciliation' + ) { return 3; } if (!isAgentRuntimeTerminalState(runtime)) { @@ -319,6 +347,9 @@ export function projectAgentRuntimeSummaries( runtime.phase === 'waiting-for-confirmation', ); const failed = runtime.status === 'failed' || runtime.phase === 'failed'; + const needsReconciliation = + runtime.status === 'needs-reconciliation' || + runtime.phase === 'needs-reconciliation'; const completed = runtime.status === 'completed' || runtime.phase === 'completed'; const cancelled = @@ -327,7 +358,7 @@ export function projectAgentRuntimeSummaries( const status: ProjectAgentRuntimeSummary['status'] = waitingForInput || waitingForConfirmation ? 'waiting' - : failed + : failed || needsReconciliation ? 'failed' : completed ? 'completed' @@ -345,17 +376,28 @@ export function projectAgentRuntimeSummaries( ? '待回答' : waitingForConfirmation ? '待确认' - : failed - ? '失败' - : completed - ? '已完成' - : cancelled - ? '已取消' - : idle - ? '等待中' - : runtime.phase === 'planning' - ? '分析中' - : '工作中'; + : needsReconciliation + ? '待核对' + : failed + ? '失败' + : completed + ? '已完成' + : cancelled + ? '已取消' + : idle + ? '等待中' + : runtime.phase === 'planning' + ? '分析中' + : '工作中'; + const failureDetail = agentRuntimeFailureDetail(runtime); + const failureSummary = + (failed || needsReconciliation) && failureDetail + ? projectRuntimeVisibleError( + failureDetail, + projectProfessionalAgentLabel(runtime.agentId), + true, + ) + : null; return [ { @@ -363,6 +405,7 @@ export function projectAgentRuntimeSummaries( label, status, statusLabel, + failureSummary, currentTask: (activePlanStep && agentRuntimePlanStepText(activePlanStep)) || projectRuntimeVisibleCurrentWork(runtime), @@ -410,6 +453,7 @@ export function sameAgentStatusCard( left.runtimeWaitingOn === right.runtimeWaitingOn && left.runtimeNextStep === right.runtimeNextStep && left.runtimeTask === right.runtimeTask && + left.runtimeError === right.runtimeError && left.runtimeRunId === right.runtimeRunId && left.runtimeLoopIteration === right.runtimeLoopIteration && left.runtimeMaxLoopIterations === right.runtimeMaxLoopIterations && @@ -478,6 +522,7 @@ export function sameAgentRuntimeTasks( task.task === other.task && task.currentAction === other.currentAction && task.terminalDetail === other.terminalDetail && + task.error === other.error && task.updatedAt === other.updatedAt ); }) @@ -924,8 +969,22 @@ export function formatAgentCardRuntimeStatus(agent: AgentStatusCard) { agent.runtimeMaxLoopIterations ?? 3 }` : null; + const needsAttention = + agent.runtimeStatus === 'failed' || + agent.runtimePhase === 'failed' || + agent.runtimeStatus === 'needs-reconciliation' || + agent.runtimePhase === 'needs-reconciliation'; + const failureSummary = + needsAttention && agent.runtimeError + ? projectRuntimeVisibleError( + agent.runtimeError, + projectProfessionalAgentLabel(agent.id), + true, + ) + : null; return [ `Runtime:${agent.runtimeStatus} / ${agent.runtimePhase ?? '-'}`, + failureSummary, loopProgress, agent.runtimeAction, agent.runtimeWaitingOn ? `等待 ${agent.runtimeWaitingOn}` : null, diff --git a/apps/ai-game-creator-shell/src/features/project-workspace/AgentConversationOverlay.tsx b/apps/ai-game-creator-shell/src/features/project-workspace/AgentConversationOverlay.tsx index d823404ca..5b6db7c21 100644 --- a/apps/ai-game-creator-shell/src/features/project-workspace/AgentConversationOverlay.tsx +++ b/apps/ai-game-creator-shell/src/features/project-workspace/AgentConversationOverlay.tsx @@ -16,7 +16,11 @@ import type { AgentStatusCard, LocalConversationMessageRecord, } from '../../app/types'; -import { AgentRuntimeStatusPanel } from '../agent-runtime'; +import { + AgentRuntimeStatusPanel, + projectProfessionalAgentLabel, + projectSupervisorVisibleConversationText, +} from '../agent-runtime'; import { commandDraftFromSuggestedToolCall } from '../project-summary/agentPresentation'; import { agentTaskGraphStateLabels, @@ -268,7 +272,11 @@ export function AgentConversationOverlay({ key={`${message.updatedAt}-${index}`} className={`message message--${message.role}`} > - {message.content} + {projectSupervisorVisibleConversationText( + message.content, + message.role, + projectProfessionalAgentLabel(selectedAgent.id), + )}

))} diff --git a/apps/ai-game-creator-shell/src/features/project-workspace/ProjectSupervisorView.tsx b/apps/ai-game-creator-shell/src/features/project-workspace/ProjectSupervisorView.tsx index 7cc9d262d..220a31edf 100644 --- a/apps/ai-game-creator-shell/src/features/project-workspace/ProjectSupervisorView.tsx +++ b/apps/ai-game-creator-shell/src/features/project-workspace/ProjectSupervisorView.tsx @@ -13,7 +13,10 @@ import type { PendingUiConfirmation, } from '../../app/types'; import { + projectProfessionalAgentLabel, + projectRuntimeVisibleError, ProjectSupervisorRuntimePanel, + projectSupervisorVisibleConversationText, projectWorkspaceStatusForDisplay, } from '../agent-runtime'; import { formatAgentCardRuntimeStatus } from '../project-summary/agentPresentation'; @@ -77,7 +80,10 @@ export function ProjectSupervisorView({ key={message.messageId ?? `${message.role}-${index}`} className={`message message--${message.role}`} > - {message.text} + {projectSupervisorVisibleConversationText( + message.text, + message.role, + )}

))} {transientReply ? ( @@ -151,6 +157,15 @@ export function ProjectSupervisorView({ taskStatusLabels[agent.status]} {agent.runtimeTask ? {agent.runtimeTask} : null} + {agent.runtimeError ? ( + + {projectRuntimeVisibleError( + agent.runtimeError, + projectProfessionalAgentLabel(agent.id), + true, + )} + + ) : null} ))} diff --git a/apps/ai-game-creator-shell/src/features/project-workspace/SupervisorChatOnlyView.tsx b/apps/ai-game-creator-shell/src/features/project-workspace/SupervisorChatOnlyView.tsx index cc52bccb3..5dd8eca5f 100644 --- a/apps/ai-game-creator-shell/src/features/project-workspace/SupervisorChatOnlyView.tsx +++ b/apps/ai-game-creator-shell/src/features/project-workspace/SupervisorChatOnlyView.tsx @@ -303,12 +303,20 @@ export function formatGameChatStageRecord( ) { const terminalStatus = runtime.status === 'failed' || runtime.phase === 'failed' - ? progress.interruptionText || '本轮失败' - : runtime.status === 'cancelled' || runtime.phase === 'cancelled' - ? '本轮已取消' - : runtime.status === 'completed' || runtime.phase === 'completed' - ? '本轮已完成' - : projectSupervisorChatRuntimeStatus(runtime); + ? progress.interruptionText || + (runtime.error + ? projectRuntimeVisibleError(runtime.error, '项目总控 Agent', true) + : '本轮失败') + : runtime.status === 'needs-reconciliation' || + runtime.phase === 'needs-reconciliation' + ? runtime.error + ? projectRuntimeVisibleError(runtime.error, '项目总控 Agent', true) + : '项目总控 Agent 运行状态需要核对,请打开运行详情后重试' + : runtime.status === 'cancelled' || runtime.phase === 'cancelled' + ? '本轮已取消' + : runtime.status === 'completed' || runtime.phase === 'completed' + ? '本轮已完成' + : projectSupervisorChatRuntimeStatus(runtime); const lines = [ GAME_CHAT_STAGE_RECORD_PREFIX, `${progress.title.replace('Supervisor 进度播报 · ', '')} · ${terminalStatus}`, @@ -990,7 +998,10 @@ export function SupervisorChatOnlyView({ const running = Boolean( chatAgentBusy || synchronizingAcceptedRun || - (projectedRuntime && (!runtimeTerminal || descendantsStillActive)), + (projectedRuntime && + projectedRuntime.status !== 'needs-reconciliation' && + projectedRuntime.phase !== 'needs-reconciliation' && + (!runtimeTerminal || descendantsStillActive)), ); const gameChatInterruptionText = gameChatMode && projectedRuntime @@ -1123,6 +1134,12 @@ export function SupervisorChatOnlyView({ if (runtimeError) { return '运行异常'; } + if ( + runtime?.status === 'needs-reconciliation' || + runtime?.phase === 'needs-reconciliation' + ) { + return '待人工核对'; + } if (synchronizingAcceptedRun) { return '正在启动'; } diff --git a/apps/ai-game-creator-shell/src/features/runtime-config/RuntimeConfigDialog.tsx b/apps/ai-game-creator-shell/src/features/runtime-config/RuntimeConfigDialog.tsx index c8c9fdd23..df6e2469e 100644 --- a/apps/ai-game-creator-shell/src/features/runtime-config/RuntimeConfigDialog.tsx +++ b/apps/ai-game-creator-shell/src/features/runtime-config/RuntimeConfigDialog.tsx @@ -1,5 +1,19 @@ -import { Plus, Trash2, Zap } from 'lucide-react'; -import { type FormEvent, Fragment, useEffect, useRef, useState } from 'react'; +import { + Bot, + Cable, + CheckCircle2, + CircleAlert, + LoaderCircle, + Plus, + RotateCcw, + Save, + Settings2, + SlidersHorizontal, + Trash2, + X, + Zap, +} from 'lucide-react'; +import { type FormEvent, useEffect, useRef, useState } from 'react'; import { createGameCreationAppSeedTasks } from '../../../../../packages/shared/src/contracts/gameCreationApp'; import { @@ -82,6 +96,40 @@ const runtimeAgentModes = new Set([ 'provider', ]); +type RuntimeSettingsSection = 'general' | 'agents' | 'connections' | 'advanced'; + +const runtimeSettingsSections = [ + { + id: 'general', + label: '常用设置', + description: '运行方式与默认模型', + icon: Settings2, + }, + { + id: 'agents', + label: 'Agent 模型', + description: '按角色覆盖默认模型', + icon: Bot, + }, + { + id: 'connections', + label: '连接与工具', + description: 'MCP 与外部服务', + icon: Cable, + }, + { + id: 'advanced', + label: '高级参数', + description: '上下文、超时与重试', + icon: SlidersHorizontal, + }, +] as const satisfies ReadonlyArray<{ + id: RuntimeSettingsSection; + label: string; + description: string; + icon: typeof Settings2; +}>; + const defaultRuntimeMcpServerConfig: GameCreatorMcpServerConfig = { enabled: true, required: false, @@ -534,6 +582,9 @@ export function RuntimeConfigDialog({ const [runtimeConfigDraft, setRuntimeConfigDraft] = useState(defaultRuntimeConfigDraft); const [runtimeConfigBusy, setRuntimeConfigBusy] = useState(false); + const [activeSection, setActiveSection] = + useState('general'); + const [expandedAgentIds, setExpandedAgentIds] = useState([]); const [newMcpServerId, setNewMcpServerId] = useState(''); const [mcpStructuredDrafts, setMcpStructuredDrafts] = useState< Record @@ -858,6 +909,24 @@ export function RuntimeConfigDialog({ } } + const selectedSection = + runtimeSettingsSections.find((section) => section.id === activeSection) ?? + runtimeSettingsSections[0]; + const configuredAgentCount = Object.values( + runtimeConfigDraft.agentLlm ?? {}, + ).filter((config) => + Object.values(config).some((value) => value !== undefined && value !== ''), + ).length; + const runtimeConfigStatusTone = runtimeConfigBusy + ? 'busy' + : /^(已保存|已读取|已恢复默认|已添加|已移除|MCP 已连接)/.test( + runtimeConfigStatus, + ) + ? 'success' + : runtimeConfigStatus === '未读取' + ? 'neutral' + : 'warning'; + return (
closeDialogOnBackdropMouseDown(event, onClose)} >
closeDialogOnEscape(event, onClose)} > -
-

运行时配置

-
+
+
+ AI GAME CREATOR +

Agent 设置

+
+ +
+
+ +
+
+
+

{selectedSection.label}

+

{selectedSection.description}

+
+ {activeSection === 'agents' ? ( + {configuredAgentCount} 个角色已覆盖 + ) : activeSection === 'connections' ? ( + + {Object.keys(runtimeConfigDraft.mcpServers).length} 个 MCP + + ) : null} +
+
+ {activeSection === 'general' ? ( + <> + + {runtimeConfigDraft.agentMode !== 'codex_cli' ? ( + <> + + + + + + + + + + ) : null} + + ) : null} + {activeSection === 'advanced' && + runtimeConfigDraft.agentMode !== 'codex_cli' ? ( + <> + + + + + + + + ) : null} + {activeSection === 'agents' && + runtimeConfigDraft.agentMode !== 'codex_cli' ? ( +
+ {runtimeAgentLlmRows.map((agent) => { + const agentLlm = + runtimeConfigDraft.agentLlm?.[agent.id] ?? {}; + const defaultReasoningEffort = + runtimeAgentReasoningEffortDefaults[ + agent.id as keyof typeof runtimeAgentReasoningEffortDefaults + ]; + return ( +
+ + {expandedAgentIds.includes(agent.id) ? ( +
+ + + + + + + + + + + +
+ ) : null} +
+ ); + })} +
+ ) : null} + {activeSection === 'connections' ? ( + <> + + + + ) : null} + {activeSection === 'connections' ? ( +
+
+
+

MCP servers

+ {`${Object.keys(runtimeConfigDraft.mcpServers).length} 个配置`} +
+ +
+
+ + +
+ {Object.entries(runtimeConfigDraft.mcpServers).length === + 0 ? ( +

尚未配置 MCP server

+ ) : ( +
+ {Object.entries(runtimeConfigDraft.mcpServers).map( + ([serverId, server]) => { + const structuredDraft = + mcpStructuredDrafts[serverId] ?? + runtimeMcpStructuredDraft(server); + const serverStatus = mcpCatalog?.servers.find( + (candidate) => candidate.serverId === serverId, + ); + return ( +
+
+
+ {serverId} + + {!server.enabled + ? '已停用' + : serverStatus + ? serverStatus.connected + ? `已连接 · ${serverStatus.toolCount} 个工具` + : '连接失败' + : server.transport === 'stdio' + ? 'STDIO · 未测试' + : 'HTTP · 未测试'} + +
+ +
+
+ 配置 +
+ + + + + {server.transport === 'stdio' ? ( + <> + + +