From 2040cd042a10ea7caf3039841df01d513e748c88 Mon Sep 17 00:00:00 2001 From: "Logan.Li" Date: Tue, 27 Oct 2026 15:21:54 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20AI=20=E9=85=8D=E7=BD=AE=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E5=95=86=E4=B8=8B=E6=8B=89=E9=80=89=E4=B8=8D=E4=B8=AD?= =?UTF-8?q?=20+=20=E5=85=88=E6=B5=8B=E8=AF=95=E9=80=9A=E8=BF=87=E6=89=8D?= =?UTF-8?q?=E8=83=BD=E4=BF=9D=E5=AD=98=EF=BC=88=E5=8F=82=E8=80=83=20aiops?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 根因:settings 刷新(保存其它配置后 invalidate)触发的表单初始化 useEffect 会把正在编辑的服务商/端点/密钥打回已保存值,表现为下拉 "闪退选不中"。改为 useRef 守卫仅首次加载数据时初始化一次, 后续刷新不再覆盖用户编辑(同时修复 lark/ldap 表单同类隐患) - 先测试才能保存:启用 AI 后「保存」按钮在「测试连接」通过前禁用 (title 提示),任何字段修改重置测试状态;通过后显示绿色 "✓ 测试通过,可保存"徽标;未启用 AI 不受门槛限制 --- frontend/src/pages/SystemSettings.tsx | 45 +++++++++++++++++++++------ 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/frontend/src/pages/SystemSettings.tsx b/frontend/src/pages/SystemSettings.tsx index 4a035c3..d35af11 100644 --- a/frontend/src/pages/SystemSettings.tsx +++ b/frontend/src/pages/SystemSettings.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { Palette, Monitor, Moon, Sun, Save, RotateCcw, CheckCircle2, Globe, Bell, Loader2, ExternalLink, ChevronDown, ChevronRight @@ -89,6 +89,12 @@ export default function SystemSettings() { const [notifications, setNotifications] = useState(defaultNotifications); const [llm, setLlm] = useState({ enabled: false, endpoint: '', model: '', apiKey: '' }); const [llmProvider, setLlmProvider] = useState('custom'); + // 先测试才能保存:null=未测 true=通过;任何表单修改都会重置 + const [llmTestOk, setLlmTestOk] = useState(null); + const updateLlm = (patch: Partial) => { + setLlm((prev) => ({ ...prev, ...patch })); + setLlmTestOk(null); + }; const [lark, setLark] = useState({ enabled: false, webhook: '', secret: '' }); const [ldap, setLdap] = useState({ enabled: false, @@ -109,9 +115,12 @@ export default function SystemSettings() { const { data: settings } = useQuery({ queryKey: ['settings'], queryFn: apiClient.getSettings }); - // 用后端数据初始化表单(仅在数据变化时覆盖本地编辑态) + // 用后端数据初始化表单:仅在首次拿到数据时执行一次, + // 避免后续 settings 刷新(保存其它配置后 invalidate)把用户正在编辑的表单打回已保存值 + const initedRef = useRef(false); useEffect(() => { - if (!settings) return; + if (!settings || initedRef.current) return; + initedRef.current = true; setTheme(safeParse(settings.theme, defaultTheme)); setNotifications(safeParse(settings.notification, defaultNotifications)); setLlm((prev) => ({ @@ -238,6 +247,7 @@ export default function SystemSettings() { llmApiKey: llm.apiKey || undefined, }), onSuccess: (res) => { + setLlmTestOk(!!res.success); if (res.success) toast.success('AI 连接成功', { description: res.message }); else toast.error('AI 连接失败', { description: res.message }); }, @@ -534,7 +544,7 @@ export default function SystemSettings() {
@@ -547,7 +557,7 @@ export default function SystemSettings() { const preset = LLM_PROVIDERS.find((p) => p.id === id); if (preset && id !== 'custom') { // 非 custom:自动填入 endpoint/model(用户仍可修改) - setLlm((prev) => ({ ...prev, endpoint: preset.endpoint, model: preset.model })); + updateLlm({ endpoint: preset.endpoint, model: preset.model }); } }} className="w-full px-3 py-[6px] text-[12px] rounded-md border outline-none" @@ -562,7 +572,7 @@ export default function SystemSettings() {
- setLlm({ ...llm, endpoint: e.target.value })} placeholder="https://api.deepseek.com/v1" className="flex-1 min-w-0 px-3 py-[6px] text-[12px] rounded-md border outline-none font-mono" style={{ background: 'var(--panel-bg-secondary)', borderColor: 'var(--border-color-light)', color: 'var(--text-primary)' }} /> + updateLlm({ endpoint: e.target.value })} placeholder="https://api.deepseek.com/v1" className="flex-1 min-w-0 px-3 py-[6px] text-[12px] rounded-md border outline-none font-mono" style={{ background: 'var(--panel-bg-secondary)', borderColor: 'var(--border-color-light)', color: 'var(--text-primary)' }} /> {!!LLM_PROVIDERS.find((p) => p.id === llmProvider)?.keyUrl && ( p.id === llmProvider)!.keyUrl} @@ -579,22 +589,37 @@ export default function SystemSettings() {
- setLlm({ ...llm, model: e.target.value })} placeholder="deepseek-chat" className="w-full px-3 py-[6px] text-[12px] rounded-md border outline-none font-mono" style={{ background: 'var(--panel-bg-secondary)', borderColor: 'var(--border-color-light)', color: 'var(--text-primary)' }} /> + updateLlm({ model: e.target.value })} placeholder="deepseek-chat" className="w-full px-3 py-[6px] text-[12px] rounded-md border outline-none font-mono" style={{ background: 'var(--panel-bg-secondary)', borderColor: 'var(--border-color-light)', color: 'var(--text-primary)' }} />
- setLlm({ ...llm, apiKey: e.target.value })} placeholder={settings?.llmApiKeySet ? '已配置,留空不修改' : 'sk-...'} className="w-full px-3 py-[6px] text-[12px] rounded-md border outline-none font-mono" style={{ background: 'var(--panel-bg-secondary)', borderColor: 'var(--border-color-light)', color: 'var(--text-primary)' }} /> + updateLlm({ apiKey: e.target.value })} placeholder={settings?.llmApiKeySet ? '已配置,留空不修改' : 'sk-...'} className="w-full px-3 py-[6px] text-[12px] rounded-md border outline-none font-mono" style={{ background: 'var(--panel-bg-secondary)', borderColor: 'var(--border-color-light)', color: 'var(--text-primary)' }} />

未启用 AI 时,智能助手自动降级为本地规则模式(按服务名匹配,仍可正常发布)

- + {llm.enabled && llmTestOk === true && ( + ✓ 测试通过,可保存 + )} + {llm.enabled && llmTestOk === false && ( + 测试未通过,请检查配置 + )}
+ {llm.enabled && llmTestOk !== true && ( +

启用 AI 后需先「测试连接」通过才能保存,避免保存不可用配置

+ )}