diff --git a/backend/internal/service/websearch_config.go b/backend/internal/service/websearch_config.go
index bb33368d39..d4b0823adc 100644
--- a/backend/internal/service/websearch_config.go
+++ b/backend/internal/service/websearch_config.go
@@ -3,6 +3,7 @@ package service
import (
"context"
"encoding/json"
+ "errors"
"fmt"
"log/slog"
"sync/atomic"
@@ -106,6 +107,15 @@ func (s *SettingService) loadWebSearchConfigFromDB() (*WebSearchEmulationConfig,
raw, err := s.settingRepo.GetValue(dbCtx, SettingKeyWebSearchEmulationConfig)
if err != nil {
+ // Missing key is the normal first-boot state: return empty disabled config.
+ if errors.Is(err, ErrSettingNotFound) {
+ cfg := &WebSearchEmulationConfig{}
+ webSearchEmulationCache.Store(&cachedWebSearchEmulationConfig{
+ config: cfg,
+ expiresAt: time.Now().Add(webSearchEmulationCacheTTL).UnixNano(),
+ })
+ return cfg, nil
+ }
webSearchEmulationCache.Store(&cachedWebSearchEmulationConfig{
config: &WebSearchEmulationConfig{},
expiresAt: time.Now().Add(webSearchEmulationErrorTTL).UnixNano(),
diff --git a/frontend/src/components/common/BaseDialog.vue b/frontend/src/components/common/BaseDialog.vue
index 2a0f25870b..0a025bfce3 100644
--- a/frontend/src/components/common/BaseDialog.vue
+++ b/frontend/src/components/common/BaseDialog.vue
@@ -28,7 +28,7 @@
-
+
@@ -52,6 +52,7 @@ const dialogId = `modal-title-${++dialogIdCounter}`
// 焦点管理
const dialogRef = ref
(null)
+const modalBodyRef = ref(null)
let previousActiveElement: HTMLElement | null = null
type DialogWidth = 'narrow' | 'normal' | 'wide' | 'extra-wide' | 'full'
@@ -123,6 +124,9 @@ watch(
// 等待DOM更新后设置焦点到对话框
await nextTick()
+ if (modalBodyRef.value) {
+ modalBodyRef.value.scrollTop = 0
+ }
if (dialogRef.value) {
const firstFocusable = dialogRef.value.querySelector(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
diff --git a/frontend/src/components/common/__tests__/BaseDialog.spec.ts b/frontend/src/components/common/__tests__/BaseDialog.spec.ts
new file mode 100644
index 0000000000..13ada1087e
--- /dev/null
+++ b/frontend/src/components/common/__tests__/BaseDialog.spec.ts
@@ -0,0 +1,37 @@
+import { afterEach, describe, expect, it, vi } from 'vitest'
+import { mount } from '@vue/test-utils'
+import { nextTick } from 'vue'
+import BaseDialog from '../BaseDialog.vue'
+
+vi.mock('vue-i18n', () => ({
+ useI18n: () => ({ t: (key: string) => key })
+}))
+
+describe('BaseDialog', () => {
+ afterEach(() => {
+ document.body.innerHTML = ''
+ document.body.classList.remove('modal-open')
+ })
+
+ it('resets body scroll position when reopened', async () => {
+ const wrapper = mount(BaseDialog, {
+ attachTo: document.body,
+ props: { show: false, title: 'Details' },
+ slots: { default: 'content
' },
+ global: { stubs: { Icon: true } }
+ })
+
+ await wrapper.setProps({ show: true })
+ await nextTick()
+ const body = document.body.querySelector('.modal-body')
+ expect(body).not.toBeNull()
+ body!.scrollTop = 480
+
+ await wrapper.setProps({ show: false })
+ await wrapper.setProps({ show: true })
+ await nextTick()
+
+ expect(document.body.querySelector('.modal-body')?.scrollTop).toBe(0)
+ wrapper.unmount()
+ })
+})