From 5efd88cb3e2ea25abe91a37bcba38fc6d4c4eeeb Mon Sep 17 00:00:00 2001 From: Linghong Date: Tue, 8 Sep 2026 12:58:29 +0000 Subject: [PATCH 1/7] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=AE=A2=E6=88=B7?= =?UTF-8?q?=E7=AB=AF=E5=B8=B8=E7=94=A8=E8=AE=BE=E7=BD=AE=E8=AF=BB=E5=86=99?= =?UTF-8?q?=E5=8D=A1=E9=A1=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移除常用设置读取和保存后的外部诊断调用 减少配置读取时的重复权限修复和保存后的重复回读 补充常用设置无需外部诊断的原因注释 --- .../src-tauri/src/commands.rs | 5 ++++- .../src-tauri/src/config.rs | 9 ++++----- .../runtime-config/RuntimeConfigDialog.tsx | 19 +------------------ 3 files changed, 9 insertions(+), 24 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/commands.rs b/apps/ai-game-creator-shell/src-tauri/src/commands.rs index 2f564e863..e7e86ac1a 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/commands.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/commands.rs @@ -2037,7 +2037,10 @@ fn persist_game_creator_app_config( let path = writable_game_creator_config_path()?; let content = serialize_game_creator_app_config_for_renderer_write(&config)?; write_game_creator_config_atomically(&path, &format!("{content}\n"))?; - game_creator_app_config_view(load_game_creator_app_config()?) + // `config` is already normalized and is exactly what was persisted. + // Avoid reloading it here: a reload repeats the Windows private-path and + // ACL checks and made saving the settings panel appear to hang. + game_creator_app_config_view(config) } #[tauri::command] diff --git a/apps/ai-game-creator-shell/src-tauri/src/config.rs b/apps/ai-game-creator-shell/src-tauri/src/config.rs index 8fbfff37b..973ef6b67 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/config.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/config.rs @@ -3272,9 +3272,10 @@ pub(crate) fn configure_game_creator_runtime_config_dir( Ok(()) } -/// Validates a persisted AGC config file without following links. Existing -/// regular files are tightened through the same Windows owner/DACL gate used -/// by credential files before any read is allowed. +/// Validates a persisted AGC config file without following links. +/// +/// Reading configuration must remain read-only. ACL hardening is performed +/// when the file is created or replaced, not on every settings-panel read. fn validate_game_creator_config_file_entry(path: &Path) -> Result { #[cfg(windows)] validate_game_creator_private_path_ancestors_with_auto_elevation(path, "客户端配置文件")?; @@ -3293,8 +3294,6 @@ fn validate_game_creator_config_file_entry(path: &Path) -> Result if metadata.file_type().is_symlink() || !metadata.is_file() { return Err("客户端配置文件必须是普通文件,不能是链接或其他对象".to_string()); } - #[cfg(windows)] - secure_windows_game_creator_path_for_current_user_with_auto_elevation(path, false, true)?; Ok(true) } 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 b88d59449..cb90f50db 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 @@ -470,6 +470,7 @@ export function RuntimeConfigDialog({ })); } + // 常用设置仅调整运行参数,读写时无需外部诊断,避免进程探测拖慢页面加载和保存反馈。 async function readRuntimeConfig() { if (runtimeConfigBusyRef.current) { return; @@ -494,15 +495,6 @@ export function RuntimeConfigDialog({ ); setRuntimeConfigDraft(config); setRuntimeConfigStatus(`已读取:${result.path}`); - try { - const status = await invoke( - 'check_game_creator_llm_config', - ); - setEffectiveLlmConfigStatus(status); - } catch { - // Older test fixtures and non-Tauri previews may not expose the - // diagnostic command; the config read itself remains usable. - } onLog?.('runtime_config.read'); } catch (error) { setRuntimeConfigStatus( @@ -551,15 +543,6 @@ export function RuntimeConfigDialog({ ); setRuntimeConfigDraft(savedConfig); setRuntimeConfigStatus(`已保存:${result.path}`); - try { - const status = await invoke( - 'check_game_creator_llm_config', - ); - setEffectiveLlmConfigStatus(status); - } catch { - // Keep the last safe status when the optional diagnostic refresh is - // unavailable in a preview or test fixture. - } setRuntimeConfigToast({ tone: 'success', message: '保存成功,新的运行时配置已生效', From 1cf835418eea42b3c660d09d9908b4eb16dad594 Mon Sep 17 00:00:00 2001 From: Linghong Date: Tue, 8 Sep 2026 13:45:25 +0000 Subject: [PATCH 2/7] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=B8=B8=E7=94=A8?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE=E7=8A=B6=E6=80=81=E5=B1=95=E7=A4=BA=E4=B8=8E?= =?UTF-8?q?=E6=9C=AC=E5=9C=B0=E8=A6=86=E7=9B=96=E4=BF=9D=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 删除常用设置账号权限状态栏及关联状态传递 复用保存前读取结果并同步冲突覆盖项,模型切换仅同步模型字段 补充配置保存定向测试并更新设置职责文档 --- .../src-tauri/src/commands.rs | 50 ++++++++++++-- .../src-tauri/src/config.rs | 46 ++++++++++++- .../src-tauri/src/tests/configuration.rs | 43 ++++++++++++ apps/ai-game-creator-shell/src/App.tsx | 1 - .../runtime-config/RuntimeConfigDialog.tsx | 68 ------------------- ...案】AI游戏创作智能体App实施计划-2026-06-24.md | 6 ++ 6 files changed, 138 insertions(+), 76 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/commands.rs b/apps/ai-game-creator-shell/src-tauri/src/commands.rs index e7e86ac1a..74c580b0b 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/commands.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/commands.rs @@ -2006,8 +2006,9 @@ pub(crate) fn write_game_creator_app_config( let _guard = GAME_CREATOR_CONFIG_WRITE_LOCK .lock() .map_err(|_| "配置写入锁不可用")?; - config.selected_model_id = load_game_creator_app_config()?.selected_model_id; - persist_game_creator_app_config(config) + let (current, overlays) = load_game_creator_app_config_for_write()?; + config.selected_model_id = current.selected_model_id; + persist_game_creator_app_config(config, overlays, false) } #[tauri::command] @@ -2025,24 +2026,65 @@ pub(crate) fn select_game_creator_model( { return Err("模型标识无效".into()); } - let mut config = load_game_creator_app_config()?; + let (mut config, overlays) = load_game_creator_app_config_for_write()?; config.selected_model_id = model_id; - persist_game_creator_app_config(config) + persist_game_creator_app_config(config, overlays, true) } fn persist_game_creator_app_config( config: GameCreatorAppConfig, + overlays: Vec<(PathBuf, serde_json::Value)>, + model_only: bool, ) -> Result { let config = normalize_game_creator_app_config(config)?; let path = writable_game_creator_config_path()?; let content = serialize_game_creator_app_config_for_renderer_write(&config)?; write_game_creator_config_atomically(&path, &format!("{content}\n"))?; + let saved: serde_json::Value = serde_json::from_str(&content) + .map_err(|error| format!("解析已序列化客户端配置失败:{error}"))?; + for (overlay_path, mut overlay) in overlays { + let previous = overlay.clone(); + if let Some(fields) = overlay.as_object_mut() { + for (key, value) in fields.iter_mut() { + if (key == "selectedModelId") == model_only { + if let Some(saved_value) = saved.get(key) { + // 仅同步已有覆盖项;其它字段继续保留原有覆盖语义。 + if key == "agentLlm" { + *value = saved_value.clone(); + } else { + update_existing_config_overlay_fields(value, saved_value); + } + } + } + } + } + if overlay != previous { + let content = serde_json::to_string_pretty(&overlay) + .map_err(|error| format!("序列化客户端覆盖配置失败:{error}"))?; + write_game_creator_config_atomically(&overlay_path, &format!("{content}\n"))?; + } + } // `config` is already normalized and is exactly what was persisted. // Avoid reloading it here: a reload repeats the Windows private-path and // ACL checks and made saving the settings panel appear to hang. game_creator_app_config_view(config) } +fn update_existing_config_overlay_fields( + overlay: &mut serde_json::Value, + saved: &serde_json::Value, +) { + if let (Some(fields), Some(saved_fields)) = (overlay.as_object_mut(), saved.as_object()) { + for (key, value) in fields { + if let Some(saved_value) = saved_fields.get(key) { + update_existing_config_overlay_fields(value, saved_value); + } + } + } else if !overlay.is_null() { + *overlay = saved.clone(); + } +} + #[tauri::command] pub(crate) fn upload_local_asset( project_path: String, diff --git a/apps/ai-game-creator-shell/src-tauri/src/config.rs b/apps/ai-game-creator-shell/src-tauri/src/config.rs index 973ef6b67..9eaf82727 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/config.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/config.rs @@ -3351,6 +3351,31 @@ pub(crate) fn load_game_creator_app_config() -> Result Result<(GameCreatorAppConfig, Vec<(PathBuf, serde_json::Value)>), String> { + let writable_path = writable_game_creator_config_path()?; + let mut config = GameCreatorAppConfig::default(); + let mut overlays = Vec::new(); + let mut after_writable = false; + for path in game_creator_config_paths() { + if path == writable_path { + after_writable = true; + } + if let Some(content) = read_game_creator_config_file(&path)? { + merge_game_creator_config_content(&mut config, &path, &content)?; + if after_writable && path != writable_path { + let value = serde_json::from_str(&content) + .map_err(|error| format!("解析客户端覆盖配置失败:{error}"))?; + overlays.push((path, value)); + } + } + } + if game_creator_official_llm_route_locked() { + lock_game_creator_app_config_to_official_route(&mut config); + } + Ok((config, overlays)) +} + pub(crate) fn scrub_locked_game_creator_config_file(config: &mut GameCreatorAppConfigFile) -> bool { let mut changed = config.agent_mode.as_deref() != Some(GAME_CREATOR_AGENT_MODE_CODEX_APP_SERVER) @@ -3587,6 +3612,13 @@ pub(crate) fn merge_game_creator_config_file( config: &mut GameCreatorAppConfig, path: &Path, ) -> Result<(), String> { + if let Some(content) = read_game_creator_config_file(path)? { + merge_game_creator_config_content(config, path, &content)?; + } + Ok(()) +} + +fn read_game_creator_config_file(path: &Path) -> Result, String> { let backup_path = game_creator_config_backup_path(path); let path_exists = validate_game_creator_config_file_entry(path)?; let read_path = if path_exists { @@ -3594,11 +3626,19 @@ pub(crate) fn merge_game_creator_config_file( } else if validate_game_creator_config_file_entry(&backup_path)? { backup_path.as_path() } else { - return Ok(()); + return Ok(None); }; let content = read_game_creator_private_file_to_string(read_path, "客户端配置", 256 * 1024)?; - let file_config = serde_json::from_str::(&content) - .map_err(|error| format!("解析客户端配置失败:{}: {error}", read_path.display()))?; + Ok(Some(content)) +} + +fn merge_game_creator_config_content( + config: &mut GameCreatorAppConfig, + path: &Path, + content: &str, +) -> Result<(), String> { + let file_config = serde_json::from_str::(content) + .map_err(|error| format!("解析客户端配置失败:{}: {error}", path.display()))?; if let Some(agent_mode) = file_config.agent_mode { config.agent_mode = agent_mode; } diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/configuration.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/configuration.rs index 72fb58321..8bb6d2f70 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests/configuration.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests/configuration.rs @@ -775,6 +775,7 @@ fn app_config_commands_write_runtime_config_file() { .expect("read persisted runtime config"); assert!(!persisted.contains("editorApi")); assert!(!persisted.contains("editor-key")); + assert!(!root.join(GAME_CREATOR_LOCAL_CONFIG_FILE_NAME).exists()); let read_back = read_game_creator_app_config().expect("read runtime config"); assert_eq!(read_back.config.llm.model, "runtime-model"); @@ -806,6 +807,48 @@ fn app_config_commands_write_runtime_config_file() { fs::remove_dir_all(root).expect("cleanup runtime config dir"); } +#[test] +fn app_config_save_updates_conflicting_local_overlay() { + let root = unique_project_path(); + fs::create_dir_all(&root).expect("config dir"); + let _guard = use_test_runtime_config_dir(root.clone()); + let overlay_path = root.join(GAME_CREATOR_LOCAL_CONFIG_FILE_NAME); + fs::write(&overlay_path, r#"{"llm":{"reasoningEffort":"low"},"selectedModelId":"existing","custom":{"keep":true}}"#) + .expect("write overlay"); + let mut config = load_game_creator_app_config().expect("load config"); + config.llm.reasoning_effort = "high".to_string(); + let saved = write_game_creator_app_config(config).expect("save config"); + let effective = load_game_creator_app_config().expect("effective config"); + assert_eq!(saved.config.llm.reasoning_effort, "high"); + assert_eq!(effective.llm.reasoning_effort, "high"); + let overlay: serde_json::Value = serde_json::from_str( + &fs::read_to_string(&overlay_path).expect("read overlay"), + ).expect("parse overlay"); + assert_eq!(overlay["selectedModelId"], "existing"); + assert_eq!(overlay["llm"], serde_json::json!({"reasoningEffort": "high"})); + assert_eq!(overlay["custom"]["keep"], true); + fs::remove_dir_all(root).expect("cleanup config dir"); +} + +#[test] +fn app_config_model_selection_only_updates_model_overlay() { + let root = unique_project_path(); + fs::create_dir_all(&root).expect("config dir"); + let _guard = use_test_runtime_config_dir(root.clone()); + let overlay_path = root.join(GAME_CREATOR_LOCAL_CONFIG_FILE_NAME); + fs::write(&overlay_path, r#"{"selectedModelId":"old","llm":{"reasoningEffort":"low"}}"#) + .expect("write overlay"); + let saved = select_game_creator_model("new-model".to_string()).expect("select model"); + let effective = load_game_creator_app_config().expect("effective config"); + assert_eq!(saved.config.selected_model_id, "new-model"); + assert_eq!(effective.selected_model_id, "new-model"); + let overlay: serde_json::Value = serde_json::from_str( + &fs::read_to_string(&overlay_path).expect("read overlay"), + ).expect("parse overlay"); + assert_eq!(overlay["llm"], serde_json::json!({"reasoningEffort": "low"})); + fs::remove_dir_all(root).expect("cleanup config dir"); +} + #[test] fn app_config_write_rejects_invalid_api_kind() { let root = unique_project_path(); diff --git a/apps/ai-game-creator-shell/src/App.tsx b/apps/ai-game-creator-shell/src/App.tsx index 9f53b0868..d6530c5a6 100644 --- a/apps/ai-game-creator-shell/src/App.tsx +++ b/apps/ai-game-creator-shell/src/App.tsx @@ -11184,7 +11184,6 @@ export function App({ {runtimeConfigOpen ? ( setRuntimeConfigOpen(false)} onLog={(entry) => setCommandLog((current) => [...current, entry])} /> 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 cb90f50db..ea6f80329 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 @@ -28,7 +28,6 @@ import { type ClientExtensionItem, type GameCreatorAppConfig, type GameCreatorAppConfigView, - type GameCreatorLlmConfigStatus, type GameCreatorLlmReasoningEffort, gameCreatorLlmReasoningEfforts, } from '../../app/types'; @@ -59,45 +58,6 @@ const defaultRuntimeConfigDraft: GameCreatorAppConfig = { }, }; -function formatAccountCredentialState( - status: GameCreatorLlmConfigStatus | null, -) { - switch (status?.accountCredentialState) { - case 'ready': - return { label: '账号权限已就绪', tone: 'success' as const }; - case 'login_required': - return { label: '请先登录陶泥儿账号', tone: 'warning' as const }; - case 'permission_required': - case 'permission_denied': - case 'forbidden': - return { - label: '账号权限不足,请重新登录', - tone: 'warning' as const, - }; - case 'revoked': - return { - label: '账号授权已失效,请重新登录', - tone: 'warning' as const, - }; - case 'acl_repair_required': - return { label: '本机凭据权限需要修复', tone: 'warning' as const }; - case 'unavailable': - return { - label: '账号权限暂不可用,请稍后重试', - tone: 'warning' as const, - }; - case 'not_required': - return { - label: '当前发行版不需要本地凭据', - tone: 'neutral' as const, - }; - default: - return status?.configured - ? { label: '账号权限已就绪', tone: 'success' as const } - : { label: '尚未检查账号权限', tone: 'neutral' as const }; - } -} - type RuntimeSettingsSection = | 'general' | 'agents' @@ -223,13 +183,11 @@ function normalizeRuntimeConfigDraft( export function RuntimeConfigDialog({ allowAdvancedExternalEditorConfig = false, - llmConfigStatus = null, onClose, onLog, }: { projectPath?: string; allowAdvancedExternalEditorConfig?: boolean; - llmConfigStatus?: GameCreatorLlmConfigStatus | null; onClose: () => void; onLog?: (entry: string) => void; }) { @@ -248,8 +206,6 @@ export function RuntimeConfigDialog({ const [runtimeConfigDraft, setRuntimeConfigDraft] = useState(defaultRuntimeConfigDraft); const [runtimeConfigBusy, setRuntimeConfigBusy] = useState(false); - const [effectiveLlmConfigStatus, setEffectiveLlmConfigStatus] = - useState(llmConfigStatus); const [activeSection, setActiveSection] = useState('general'); const [clientExtensions, setClientExtensions] = useState< @@ -281,10 +237,6 @@ export function RuntimeConfigDialog({ }; }, []); - useEffect(() => { - setEffectiveLlmConfigStatus(llmConfigStatus); - }, [llmConfigStatus]); - useEffect(() => { void readRuntimeConfig(); void readClientExtensions(); @@ -713,26 +665,6 @@ export function RuntimeConfigDialog({ 官方账号服务(固定) 登录后自动使用当前账号权限。 - {(() => { - const credential = formatAccountCredentialState( - effectiveLlmConfigStatus, - ); - return ( -
- 账号权限 - {credential.label} - - {credential.tone === 'success' - ? '当前账号已具备智能创作权限。' - : '请返回登录页完成登录或重新登录;普通用户无需填写任何智能服务凭据。'} - -
- ); - })()} {runtimeConfigDraft.agentMode !== 'codex_cli' ? ( <>