修复常用设置状态展示与本地覆盖保存
Project CI / Backend tests (pull_request) Successful in 5m43s
Project CI / Repository checks (pull_request) Failing after 24m21s
Project CI / Native shell tests (pull_request) Failing after 14m11s
Project CI / Frontend tests (pull_request) Successful in 28m31s

删除常用设置账号权限状态栏及关联状态传递
复用保存前读取结果并同步冲突覆盖项,模型切换仅同步模型字段
补充配置保存定向测试并更新设置职责文档
This commit is contained in:
2026-09-08 13:45:25 +00:00
parent 5efd88cb3e
commit 1cf835418e
6 changed files with 138 additions and 76 deletions
@@ -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<GameCreatorAppConfigView, String> {
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,
@@ -3351,6 +3351,31 @@ pub(crate) fn load_game_creator_app_config() -> Result<GameCreatorAppConfig, Str
Ok(config)
}
pub(crate) fn load_game_creator_app_config_for_write(
) -> 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<Option<String>, 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::<GameCreatorAppConfigFile>(&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::<GameCreatorAppConfigFile>(content)
.map_err(|error| format!("解析客户端配置失败:{}: {error}", path.display()))?;
if let Some(agent_mode) = file_config.agent_mode {
config.agent_mode = agent_mode;
}
@@ -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();
-1
View File
@@ -11184,7 +11184,6 @@ export function App({
{runtimeConfigOpen ? (
<RuntimeConfigDialog
projectPath={localProject?.projectPath}
llmConfigStatus={llmConfigStatus}
onClose={() => setRuntimeConfigOpen(false)}
onLog={(entry) => setCommandLog((current) => [...current, entry])}
/>
@@ -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<GameCreatorAppConfig>(defaultRuntimeConfigDraft);
const [runtimeConfigBusy, setRuntimeConfigBusy] = useState(false);
const [effectiveLlmConfigStatus, setEffectiveLlmConfigStatus] =
useState<GameCreatorLlmConfigStatus | null>(llmConfigStatus);
const [activeSection, setActiveSection] =
useState<RuntimeSettingsSection>('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({
<strong></strong>
<small>使</small>
</div>
{(() => {
const credential = formatAccountCredentialState(
effectiveLlmConfigStatus,
);
return (
<div
className="runtime-settings-readonly-field"
data-tone={credential.tone}
data-testid="runtime-settings-account-credential-state"
>
<span></span>
<strong>{credential.label}</strong>
<small>
{credential.tone === 'success'
? '当前账号已具备智能创作权限。'
: '请返回登录页完成登录或重新登录;普通用户无需填写任何智能服务凭据。'}
</small>
</div>
);
})()}
{runtimeConfigDraft.agentMode !== 'codex_cli' ? (
<>
<label>