修复常用设置状态展示与本地覆盖保存
删除常用设置账号权限状态栏及关联状态传递 复用保存前读取结果并同步冲突覆盖项,模型切换仅同步模型字段 补充配置保存定向测试并更新设置职责文档
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user