fix: use deep merge for config updates and save inputs on blur

- Backend: replace shallow merge with recursive deepMerge in PUT /api/hermes/config
  to prevent nested config fields from being lost when updating partial values
- Frontend: switch all NInput fields to default-value + @change (save on blur)
  instead of :value + @update:value (save on every keystroke) in both
  PlatformSettings.vue and SettingsView.vue api_server tab
- Remove unused debounce logic and dead changeKey function

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
ekko
2026-04-17 17:43:54 +08:00
parent 3d2b1c5e47
commit b290b755c9
3 changed files with 119 additions and 106 deletions
@@ -1,29 +1,36 @@
<script setup lang="ts">
import { onMounted } from 'vue'
import { NTabs, NTabPane, NSpin, NSwitch, NInput, NInputNumber, useMessage } from 'naive-ui'
import { useI18n } from 'vue-i18n'
import { useSettingsStore } from '@/stores/hermes/settings'
import DisplaySettings from '@/components/hermes/settings/DisplaySettings.vue'
import AgentSettings from '@/components/hermes/settings/AgentSettings.vue'
import MemorySettings from '@/components/hermes/settings/MemorySettings.vue'
import SessionSettings from '@/components/hermes/settings/SessionSettings.vue'
import PrivacySettings from '@/components/hermes/settings/PrivacySettings.vue'
import SettingRow from '@/components/hermes/settings/SettingRow.vue'
import { onMounted } from "vue";
import {
NTabs,
NTabPane,
NSpin,
NSwitch,
NInput,
NInputNumber,
useMessage,
} from "naive-ui";
import { useI18n } from "vue-i18n";
import { useSettingsStore } from "@/stores/hermes/settings";
import DisplaySettings from "@/components/hermes/settings/DisplaySettings.vue";
import AgentSettings from "@/components/hermes/settings/AgentSettings.vue";
import MemorySettings from "@/components/hermes/settings/MemorySettings.vue";
import SessionSettings from "@/components/hermes/settings/SessionSettings.vue";
import PrivacySettings from "@/components/hermes/settings/PrivacySettings.vue";
import SettingRow from "@/components/hermes/settings/SettingRow.vue";
const settingsStore = useSettingsStore()
const message = useMessage()
const { t } = useI18n()
const settingsStore = useSettingsStore();
const message = useMessage();
const { t } = useI18n();
onMounted(() => {
settingsStore.fetchSettings()
})
settingsStore.fetchSettings();
});
async function saveApiServer(values: Record<string, any>) {
try {
await settingsStore.saveSection('platforms', { api_server: values })
message.success(t('settings.saved'))
await settingsStore.saveSection("platforms", { api_server: values });
message.success(t("settings.saved"));
} catch (err: any) {
message.error(t('settings.saveFailed'))
message.error(t("settings.saveFailed"));
}
}
</script>
@@ -31,11 +38,15 @@ async function saveApiServer(values: Record<string, any>) {
<template>
<div class="settings-view">
<header class="page-header">
<h2 class="header-title">{{ t('settings.title') }}</h2>
<h2 class="header-title">{{ t("settings.title") }}</h2>
</header>
<div class="settings-content">
<NSpin :show="settingsStore.loading || settingsStore.saving" size="large" :description="t('common.loading')">
<NSpin
:show="settingsStore.loading || settingsStore.saving"
size="large"
:description="t('common.loading')"
>
<NTabs type="line" animated>
<NTabPane name="display" :tab="t('settings.tabs.display')">
<DisplaySettings />
@@ -54,40 +65,66 @@ async function saveApiServer(values: Record<string, any>) {
</NTabPane>
<NTabPane name="api_server" :tab="t('settings.tabs.apiServer')">
<section class="settings-section">
<SettingRow :label="t('settings.apiServer.enable')" :hint="t('settings.apiServer.enableHint')">
<SettingRow
:label="t('settings.apiServer.enable')"
:hint="t('settings.apiServer.enableHint')"
>
<NSwitch
:value="settingsStore.platforms?.api_server?.enabled"
@update:value="v => saveApiServer({ enabled: v })"
@update:value="(v) => saveApiServer({ enabled: v })"
/>
</SettingRow>
<SettingRow :label="t('settings.apiServer.host')" :hint="t('settings.apiServer.hostHint')">
<SettingRow
:label="t('settings.apiServer.host')"
:hint="t('settings.apiServer.hostHint')"
>
<NInput
:value="settingsStore.platforms?.api_server?.host || ''"
size="small" class="input-md"
@update:value="v => saveApiServer({ host: v })"
:default-value="settingsStore.platforms?.api_server?.host || ''"
size="small"
class="input-md"
@change="(v: string) => saveApiServer({ host: v })"
/>
</SettingRow>
<SettingRow :label="t('settings.apiServer.port')" :hint="t('settings.apiServer.portHint')">
<SettingRow
:label="t('settings.apiServer.port')"
:hint="t('settings.apiServer.portHint')"
>
<NInputNumber
:value="settingsStore.platforms?.api_server?.port"
:min="1024" :max="65535"
size="small" class="input-sm"
@update:value="v => v != null && saveApiServer({ port: v })"
:default-value="settingsStore.platforms?.api_server?.port"
:min="1024"
:max="65535"
size="small"
class="input-sm"
@blur="(e: FocusEvent) => {
const val = (e.target as HTMLInputElement).value
if (val) saveApiServer({ port: Number(val) })
}"
/>
</SettingRow>
<SettingRow :label="t('settings.apiServer.key')" :hint="t('settings.apiServer.keyHint')">
<SettingRow
:label="t('settings.apiServer.key')"
:hint="t('settings.apiServer.keyHint')"
>
<NInput
:value="settingsStore.platforms?.api_server?.key || ''"
type="password" show-password-on="click"
size="small" class="input-md"
@update:value="v => saveApiServer({ key: v })"
:default-value="settingsStore.platforms?.api_server?.key || ''"
type="password"
show-password-on="click"
size="small"
class="input-md"
@change="(v: string) => saveApiServer({ key: v })"
/>
</SettingRow>
<SettingRow :label="t('settings.apiServer.cors')" :hint="t('settings.apiServer.corsHint')">
<SettingRow
:label="t('settings.apiServer.cors')"
:hint="t('settings.apiServer.corsHint')"
>
<NInput
:value="settingsStore.platforms?.api_server?.cors_origins || ''"
size="small" class="input-md"
@update:value="v => saveApiServer({ cors_origins: v })"
:default-value="
settingsStore.platforms?.api_server?.cors_origins || ''
"
size="small"
class="input-md"
@change="(v: string) => saveApiServer({ cors_origins: v })"
/>
</SettingRow>
</section>
@@ -99,7 +136,7 @@ async function saveApiServer(values: Record<string, any>) {
</template>
<style scoped lang="scss">
@use '@/styles/variables' as *;
@use "@/styles/variables" as *;
.settings-view {
height: calc(100 * var(--vh));