diff --git a/src/App.vue b/src/App.vue index 109576b..dcbdc81 100644 --- a/src/App.vue +++ b/src/App.vue @@ -29,16 +29,20 @@ - - -
⚠ {{ errorMsg }}
-
+ +
+ + +
⚠ {{ errorMsg }}
+
- - -
{{ trayNoticeMsg }}
-
+ + +
{{ trayNoticeMsg }}
+
+
+ - - @@ -27,13 +28,13 @@ @@ -91,32 +92,42 @@ const rowLanguage = useTemplateRef>('rowLanguage const rowAiLanguage = useTemplateRef>('rowAiLanguage') const rowToken = useTemplateRef>('rowToken') const rowStreamingMd = useTemplateRef>('rowStreamingMd') -function markSaved(key: 'theme' | 'language' | 'aiLanguage' | 'token' | 'streamingMd') { - const map = { theme: rowTheme.value, language: rowLanguage.value, aiLanguage: rowAiLanguage.value, token: rowToken.value, streamingMd: rowStreamingMd.value } - map[key]?.markSaved() + +// 走查 P3-11 假成功族:原 @change 立即 markSaved,先于持久化完成,失败无人知晓。 +// 改为 set 完成后才点亮「已保存」,失败点亮行内红字。注:appSettings.set 当前对落库 +// 失败仅 console 内吞(stores/appSettings.ts 降级内存态设计),catch 实际不会触发; +// 待 store 层把失败上抛后本处失败通道即自动生效(登记项,见批次报告)。 +async function persist(key: string, value: unknown, row: { value: { markSaved: () => void; markFailed: () => void } | null }) { + try { + await appSettings.set(key, value) + row.value?.markSaved() + } catch (e) { + console.error(`保存设置 ${key} 失败:`, e) + row.value?.markFailed() + } } watch(() => settings.theme, (val) => { applyTheme(val) - void appSettings.set('df-theme', val) + void persist('df-theme', val, rowTheme) }) watch(() => settings.aiLanguage, (val) => { - void appSettings.set('df-ai-language', val) + void persist('df-ai-language', val, rowAiLanguage) }) watch(() => settings.showTokenUsage, (val) => { - void appSettings.set('df-show-token-usage', val) + void persist('df-show-token-usage', val, rowToken) }) watch(() => settings.streamingMd, (val) => { - void appSettings.set('df-ai-streaming-md', val) + void persist('df-ai-streaming-md', val, rowStreamingMd) }) watch(() => settings.language, (val) => { - void appSettings.set('df-language', val) // 同步 i18n 实例,让界面立即跟随语言切换 locale.value = val as 'zh-CN' | 'en' + void persist('df-language', val, rowLanguage) }) onMounted(() => { diff --git a/src/components/settings/PerformanceSection.vue b/src/components/settings/PerformanceSection.vue index 90b1d33..f3d4daa 100644 --- a/src/components/settings/PerformanceSection.vue +++ b/src/components/settings/PerformanceSection.vue @@ -10,7 +10,8 @@ next-round :label="$t('settings.labelGlobalConcurrency')" :desc="$t('settings.descGlobalConcurrency')"> - + @@ -19,7 +20,7 @@ next-round :label="$t('settings.labelPerConvConcurrency')" :desc="$t('settings.descPerConvConcurrency')"> - @@ -29,7 +30,8 @@ :label="$t('settings.labelAgentMaxIterations')" :desc="$t('settings.descAgentMaxIterations')"> import { reactive, watch, computed, onMounted, onUnmounted, useTemplateRef } from 'vue' +import { useI18n } from 'vue-i18n' import { aiApi } from '@/api' import { useAppSettingsStore } from '@/stores/appSettings' import SettingRow from './SettingRow.vue' @@ -59,10 +62,16 @@ import SettingRow from './SettingRow.vue' // ============================================================ // 性能域 — 本地设置(localStorage):LLM 并发 / 单对话并发 / Agent 轮次 / 流式重试 // 拆自 GeneralPanel,本 Section 仅持有性能相关 key + 各自 debounce IPC 同步 -// 阶段5:SettingRow 封装行布局 + nextRound badge(后端快照锁定边界,下次对话生效); -// 4 项变更后调 markSaved 显"已保存" +// 阶段5:SettingRow 封装行布局 + nextRound badge(后端快照锁定边界,下次对话生效) +// 走查 P3-11:「已保存」改为 IPC 成功回调后点亮,失败点亮行内红字(防假成功) // ============================================================ const appSettings = useAppSettingsStore() +const { t } = useI18n() + +// 走查 P3-14:并发越界钳制需要上抛 warning toast,由壳(Settings.vue)的 showToast 消费 +const emit = defineEmits<{ + (e: 'toast', msg: string, type: 'error' | 'warning' | 'info' | 'success'): void +}>() const settings = reactive({ llmGlobalConcurrency: appSettings.get('df-llm-global-concurrency', 3), @@ -73,7 +82,7 @@ const settings = reactive({ agentMaxRetries: appSettings.get('df-ai-agent-max-retries', 3), }) -// 即时生效反馈:SettingRow refs +// 即时生效反馈:SettingRow refs(成功/失败两态,走查 P3-11) const rowGlobalConcurrency = useTemplateRef>('rowGlobalConcurrency') const rowPerConvConcurrency = useTemplateRef>('rowPerConvConcurrency') const rowMaxIterations = useTemplateRef>('rowMaxIterations') @@ -82,50 +91,73 @@ function markSaved(key: 'global' | 'perConv' | 'iter' | 'retry') { const map = { global: rowGlobalConcurrency.value, perConv: rowPerConvConcurrency.value, iter: rowMaxIterations.value, retry: rowMaxRetries.value } map[key]?.markSaved() } +function markFailed(key: 'global' | 'perConv' | 'iter' | 'retry') { + const map = { global: rowGlobalConcurrency.value, perConv: rowPerConvConcurrency.value, iter: rowMaxIterations.value, retry: rowMaxRetries.value } + map[key]?.markFailed() +} +// SQLite 持久化(watch 触发):落库失败点亮行内红字。注:appSettings.set 当前对落库失败 +// 仅 console 内吞(降级内存态设计,stores/appSettings.ts),catch 是 store 上抛错误后的 +// 失败通道,今日实际不触发(登记项见批次报告);IPC 同步失败通道在下方 timer 内,是真实信号。 watch(() => settings.llmGlobalConcurrency, (v) => { - void appSettings.set('df-llm-global-concurrency', v) + appSettings.set('df-llm-global-concurrency', v).catch(() => markFailed('global')) }) watch(() => settings.llmPerConvConcurrency, (v) => { - void appSettings.set('df-llm-per-conv-concurrency', v) + appSettings.set('df-llm-per-conv-concurrency', v).catch(() => markFailed('perConv')) }) watch(() => settings.agentMaxIterations, (v) => { - void appSettings.set('df-ai-agent-max-iterations', v) + appSettings.set('df-ai-agent-max-iterations', v).catch(() => markFailed('iter')) }) watch(() => settings.agentMaxRetries, (v) => { - void appSettings.set('df-ai-agent-max-retries', v) + appSettings.set('df-ai-agent-max-retries', v).catch(() => markFailed('retry')) }) // onMounted 期间调 sync 不应触发"已保存"反馈(那是启动恢复,非用户改动) let _userInteracting = false +// 走查 P3-14:并发上界 64 为前端 sanity 值(后端 ai_set_concurrency_config 仅 max(1) 无上界, +// 超大并发会让限流器形同虚设、放大上游限流/超时风险);越界时静默修正 + toast 告知修正结果 +const MAX_CONCURRENCY = 64 + +/** 非法输入(NaN/清空)回退默认值,合法值 clamp [1,64] */ +function clampConcurrency(v: number | undefined, fallback: number): number { + const n = typeof v === 'number' && Number.isFinite(v) ? Math.trunc(v) : fallback + return Math.min(MAX_CONCURRENCY, Math.max(1, n)) +} + // 同步 LLM 并发上限到后端(debounce 300ms,防快速连续调整时频繁 IPC) // 持久化已由上方 watch 写 appSettings(SQLite) 完成;此处只负责同步内存限流器 let _concurrencyTimer: ReturnType | null = null function syncConcurrencyConfig() { // clamp 到合理区间,防越界输入(缓存被篡改 / 手动输超限)致限流失效; // 修正后值同步回 v-model(input 显示) + watch 持久化 - // 下限 1,无上限;约束单对话并发不超过全局并发 - settings.llmGlobalConcurrency = Math.max(1, settings.llmGlobalConcurrency || 3) - settings.llmPerConvConcurrency = Math.max(1, settings.llmPerConvConcurrency || 2) - if (settings.llmPerConvConcurrency > settings.llmGlobalConcurrency) { - settings.llmPerConvConcurrency = settings.llmGlobalConcurrency + // 下限 1,上界 64(前端 sanity);约束单对话并发不超过全局并发 + let clamped = false + const g = clampConcurrency(settings.llmGlobalConcurrency, 3) + if (g !== settings.llmGlobalConcurrency) clamped = true + settings.llmGlobalConcurrency = g + let p = clampConcurrency(settings.llmPerConvConcurrency, 2) + if (p > g) { p = g; clamped = true } + settings.llmPerConvConcurrency = p + if (clamped && _userInteracting) { + emit('toast', t('settings.toastConcurrencyClamped', { max: MAX_CONCURRENCY }), 'warning') } if (_concurrencyTimer) clearTimeout(_concurrencyTimer) + // 走查 P3-11:interactive 在排程时捕获(debounce 回调执行时 _userInteracting 已复位 false) + const interactive = _userInteracting _concurrencyTimer = setTimeout(async () => { try { - await aiApi.setConcurrencyConfig(settings.llmGlobalConcurrency, settings.llmPerConvConcurrency) + await aiApi.setConcurrencyConfig(g, p) + // IPC 成功才显「已保存」,失败显行内红字(原实现先显成功、失败仅 console = 假成功) + if (interactive) { markSaved('global'); markSaved('perConv') } } catch (e) { console.error('同步 LLM 并发配置失败:', e) + if (interactive) { markFailed('global'); markFailed('perConv') } } }, 300) - if (_userInteracting) { - markSaved('global') - markSaved('perConv') - } } // wrapper:并发变更(@change 用户触发)同时反馈两项(global/perConv 任一变动都同步两项) @@ -158,28 +190,32 @@ function syncAgentMaxIterations() { settings.agentMaxIterations = Math.min(50, Math.max(1, settings.agentMaxIterations || 10)) } if (_agentIterTimer) clearTimeout(_agentIterTimer) + const interactive = _userInteracting _agentIterTimer = setTimeout(async () => { try { await aiApi.setAgentMaxIterations(settings.agentMaxIterations) + if (interactive) markSaved('iter') } catch (e) { console.error('同步 Agent 最大轮次失败:', e) + if (interactive) markFailed('iter') } }, 300) - if (_userInteracting) { - markSaved('iter') - } } // 「不限」勾选:agentMaxIterations===0 即不限(后端 0→usize::MAX,loop 靠 stop/收敛/审批退出) const unlimited = computed({ get: () => settings.agentMaxIterations === 0, set(v: boolean) { + // 走查 P3-11:勾选属用户交互,标记 interactive 让「已保存」等 IPC 成功后再显 + // (原实现在模板 @change 即时 markSaved,先于持久化 = 假成功) + _userInteracting = true if (v) { settings.agentMaxIterations = 0 } else if (settings.agentMaxIterations === 0) { settings.agentMaxIterations = 10 } syncAgentMaxIterations() + _userInteracting = false }, }) @@ -191,16 +227,16 @@ function syncAgentMaxRetries() { // clamp 0-10,防越界 settings.agentMaxRetries = Math.min(10, Math.max(0, settings.agentMaxRetries ?? 3)) if (_agentRetryTimer) clearTimeout(_agentRetryTimer) + const interactive = _userInteracting _agentRetryTimer = setTimeout(async () => { try { await aiApi.setAgentMaxRetries(settings.agentMaxRetries) + if (interactive) markSaved('retry') } catch (e) { console.error('同步流式重试次数失败:', e) + if (interactive) markFailed('retry') } }, 300) - if (_userInteracting) { - markSaved('retry') - } } onMounted(() => { diff --git a/src/components/settings/ProviderPanel.vue b/src/components/settings/ProviderPanel.vue index 2457cb3..2555ceb 100644 --- a/src/components/settings/ProviderPanel.vue +++ b/src/components/settings/ProviderPanel.vue @@ -67,7 +67,10 @@
- + +
- +
- +
{{ $t('settings.defaultModelHint') }}
@@ -112,6 +121,7 @@
+
@@ -192,7 +202,7 @@