From 8396849d0420aa488e1b61c940849efa59e142b5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E7=BB=9D=E5=B0=98?= <237809796@qq.com>
Date: Sat, 3 Oct 2026 17:57:43 +0800
Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96:=20=E8=AE=BE=E7=BD=AE?=
=?UTF-8?q?=E5=9F=9F=E4=BD=93=E9=AA=8C=E5=AE=8C=E5=96=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 校验失败红框定位字段,保存反馈改持久化成功后
- 数值越界钳制,双提示队列化不再重叠
- 搜索防抖跳分类,托盘气泡文案进多语言
---
src/App.vue | 40 ++++++---
src/components/AiChat.vue | 20 ++++-
src/components/settings/AppearanceSection.vue | 37 +++++---
.../settings/PerformanceSection.vue | 88 +++++++++++++------
src/components/settings/ProviderPanel.vue | 69 ++++++++++++---
src/components/settings/SettingRow.vue | 36 +++++---
src/components/settings/SettingsNav.vue | 50 ++++++++---
src/i18n/en/ai.ts | 13 +++
src/i18n/en/aiChat.ts | 5 ++
src/i18n/en/common.ts | 2 +
src/i18n/en/settings.ts | 8 ++
src/i18n/en/taskDetail.ts | 2 +
src/i18n/zh-CN/ai.ts | 12 +++
src/i18n/zh-CN/aiChat.ts | 5 ++
src/i18n/zh-CN/common.ts | 2 +
src/i18n/zh-CN/settings.ts | 8 ++
src/i18n/zh-CN/taskDetail.ts | 2 +
src/lib.rs | 0
src/views/Settings.vue | 23 ++++-
19 files changed, 329 insertions(+), 93 deletions(-)
create mode 100644 src/lib.rs
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 @@