优化: 设置域体验完善
- 校验失败红框定位字段,保存反馈改持久化成功后 - 数值越界钳制,双提示队列化不再重叠 - 搜索防抖跳分类,托盘气泡文案进多语言
This commit is contained in:
+17
-5
@@ -29,6 +29,9 @@
|
|||||||
</template>
|
</template>
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
|
|
||||||
|
<!-- 走查 P3-16:全局 toast 统一收进同一纵向堆叠容器。原错误 toast 与托盘提示气泡
|
||||||
|
各自 fixed 同位(bottom:24px 居中),两者同现时互相压盖、后弹者盖住先弹者。 -->
|
||||||
|
<div class="df-toast-stack">
|
||||||
<!-- 全局错误 toast(根级,主窗口与分离窗口共用)-->
|
<!-- 全局错误 toast(根级,主窗口与分离窗口共用)-->
|
||||||
<transition name="toast">
|
<transition name="toast">
|
||||||
<div v-if="errorMsg" class="df-toast">⚠ {{ errorMsg }}</div>
|
<div v-if="errorMsg" class="df-toast">⚠ {{ errorMsg }}</div>
|
||||||
@@ -39,6 +42,7 @@
|
|||||||
<transition name="toast">
|
<transition name="toast">
|
||||||
<div v-if="trayNoticeMsg" class="df-toast df-toast--info">{{ trayNoticeMsg }}</div>
|
<div v-if="trayNoticeMsg" class="df-toast df-toast--info">{{ trayNoticeMsg }}</div>
|
||||||
</transition>
|
</transition>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 审批待办浮层(应用内右上角,窗口聚焦/可见时作快速提示)。
|
<!-- 审批待办浮层(应用内右上角,窗口聚焦/可见时作快速提示)。
|
||||||
仅在主窗口与 ai-detached 窗口挂载(file-explorer / approval-popup 自身不挂)。
|
仅在主窗口与 ai-detached 窗口挂载(file-explorer / approval-popup 自身不挂)。
|
||||||
@@ -63,6 +67,7 @@ import { useKnowledgeStore } from './stores/knowledge'
|
|||||||
import { useProjectStore } from './stores/project'
|
import { useProjectStore } from './stores/project'
|
||||||
import { useAppSettingsStore } from './stores/appSettings'
|
import { useAppSettingsStore } from './stores/appSettings'
|
||||||
import i18n from './i18n'
|
import i18n from './i18n'
|
||||||
|
import { t as ti18n } from './i18n/i18n-helpers'
|
||||||
import AiChat from './components/AiChat.vue'
|
import AiChat from './components/AiChat.vue'
|
||||||
import ApprovalOverlay from './components/ai/ApprovalOverlay.vue'
|
import ApprovalOverlay from './components/ai/ApprovalOverlay.vue'
|
||||||
import ApprovalDialog from './components/project/ApprovalDialog.vue'
|
import ApprovalDialog from './components/project/ApprovalDialog.vue'
|
||||||
@@ -227,7 +232,8 @@ onMounted(async () => {
|
|||||||
_unlistenTrayHideNotice = await listen<{ running: number }>('tray-hide-notice', (e) => {
|
_unlistenTrayHideNotice = await listen<{ running: number }>('tray-hide-notice', (e) => {
|
||||||
const running = e.payload?.running ?? 0
|
const running = e.payload?.running ?? 0
|
||||||
if (running <= 0) return
|
if (running <= 0) return
|
||||||
trayNoticeMsg.value = `${running} 个会话将继续在后台运行,托盘图标可随时唤起`
|
// 走查 P3-15:气泡文案硬编码中文 → 入 i18n(运行数经命名参数注入)
|
||||||
|
trayNoticeMsg.value = ti18n('common.trayNoticeRunning', { n: running })
|
||||||
if (trayNoticeTimer) clearTimeout(trayNoticeTimer)
|
if (trayNoticeTimer) clearTimeout(trayNoticeTimer)
|
||||||
trayNoticeTimer = setTimeout(() => { trayNoticeMsg.value = '' }, 3500)
|
trayNoticeTimer = setTimeout(() => { trayNoticeMsg.value = '' }, 3500)
|
||||||
})
|
})
|
||||||
@@ -313,16 +319,22 @@ onUnmounted(() => {
|
|||||||
.page-enter-from { opacity: 0; transform: translateY(6px); }
|
.page-enter-from { opacity: 0; transform: translateY(6px); }
|
||||||
.page-leave-to { opacity: 0; transform: translateY(-4px); }
|
.page-leave-to { opacity: 0; transform: translateY(-4px); }
|
||||||
|
|
||||||
/* ═══ 全局错误 toast ═══ */
|
/* ═══ 全局 toast 纵向堆叠容器(走查 P3-16)═══
|
||||||
.df-toast {
|
容器独占 fixed 定位,内部 flex 纵排 + 底对齐,多条 toast 依次向上堆叠不再同位互盖。
|
||||||
|
子项保持 .df-toast 原形态(定宽自适应),定位字段从子项上移到容器。 */
|
||||||
|
.df-toast-stack {
|
||||||
position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%);
|
position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%);
|
||||||
|
display: flex; flex-direction: column; align-items: center; gap: 8px;
|
||||||
|
z-index: 9999; max-width: 90vw; pointer-events: none;
|
||||||
|
}
|
||||||
|
.df-toast {
|
||||||
background: var(--df-danger); color: #fff;
|
background: var(--df-danger); color: #fff;
|
||||||
padding: 10px 18px; border-radius: var(--df-radius-sm);
|
padding: 10px 18px; border-radius: var(--df-radius-sm);
|
||||||
font-size: 13px; z-index: 9999; max-width: 90vw;
|
font-size: 13px; max-width: 90vw;
|
||||||
box-shadow: 0 4px 16px rgba(0,0,0,0.3);
|
box-shadow: 0 4px 16px rgba(0,0,0,0.3);
|
||||||
}
|
}
|
||||||
.toast-enter-active, .toast-leave-active { transition: all 0.25s var(--df-ease); }
|
.toast-enter-active, .toast-leave-active { transition: all 0.25s var(--df-ease); }
|
||||||
.toast-enter-from, .toast-leave-to { opacity: 0; transform: translate(-50%, 10px); }
|
.toast-enter-from, .toast-leave-to { opacity: 0; transform: translateY(10px); }
|
||||||
/* 后台运行提示:同 df-toast 形态,信息语义用 accent 底色与错误红区分 */
|
/* 后台运行提示:同 df-toast 形态,信息语义用 accent 底色与错误红区分 */
|
||||||
.df-toast--info { background: var(--df-accent); }
|
.df-toast--info { background: var(--df-accent); }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -289,6 +289,10 @@ const projectsStore = useProjectStore()
|
|||||||
// 滚动边沿/跟随意图重置随 MessageList 迁移,由子组件自管。
|
// 滚动边沿/跟随意图重置随 MessageList 迁移,由子组件自管。
|
||||||
watch(() => store.state.activeConversationId, () => {
|
watch(() => store.state.activeConversationId, () => {
|
||||||
if (editingMsgId.value) cancelEdit()
|
if (editingMsgId.value) cancelEdit()
|
||||||
|
// UX-P3: 队列编辑态(editingQueueIdx/editingQueueText)绑定旧会话队列在 activeQueue
|
||||||
|
// 视图内的下标,切会话后 activeQueue 已换成新会话的集合,残留 idx/text 会错挂到
|
||||||
|
// 新会话同下标队列项上,随消息编辑态一并重置。
|
||||||
|
if (editingQueueIdx.value !== null) cancelEditQueued()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -331,7 +335,14 @@ function onGlobalKeydown(e: KeyboardEvent) {
|
|||||||
// Ctrl+R: 重新生成末条回复(替代浏览器刷新,Tauri 下 preventDefault 可靠)
|
// Ctrl+R: 重新生成末条回复(替代浏览器刷新,Tauri 下 preventDefault 可靠)
|
||||||
if (ctrl && (e.key === 'r' || e.key === 'R')) {
|
if (ctrl && (e.key === 'r' || e.key === 'R')) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (!store.state.streaming) store.regenerate()
|
// UX-P3: 末条非 assistant 等场景 regenerate() 会 reject(regenerateUnavailable),原样裸调
|
||||||
|
// 成 unhandled rejection 用户无感;对齐 MessageList.handleRetry 的 catch + toast。
|
||||||
|
if (!store.state.streaming) {
|
||||||
|
store.regenerate().catch((e: unknown) => {
|
||||||
|
const msg = e instanceof Error ? e.message : String(e)
|
||||||
|
showToast(t('aiChat.regenerateFailed', { msg }), 'error')
|
||||||
|
})
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Ctrl+L: 清空当前对话(带 confirm;生成中跳过避免删流式中消息)
|
// Ctrl+L: 清空当前对话(带 confirm;生成中跳过避免删流式中消息)
|
||||||
@@ -510,9 +521,14 @@ async function handleSendQueuedNow(idx: number) {
|
|||||||
* cancelQueued 内部经 activeQueueRealIndex 做过滤视图下标映射,越界 no-op;
|
* cancelQueued 内部经 activeQueueRealIndex 做过滤视图下标映射,越界 no-op;
|
||||||
* 项携带技能时按名找回 SkillInfo 回挂 chip,找不到(技能已卸载)只回填文本,
|
* 项携带技能时按名找回 SkillInfo 回挂 chip,找不到(技能已卸载)只回填文本,
|
||||||
* 与队列原地编辑只保 text 同一取舍。 */
|
* 与队列原地编辑只保 text 同一取舍。 */
|
||||||
function backfillQueued(idx: number) {
|
async function backfillQueued(idx: number) {
|
||||||
const item = activeQueue.value[idx]
|
const item = activeQueue.value[idx]
|
||||||
if (!item) return
|
if (!item) return
|
||||||
|
// UX-P3: 输入框已有未发送草稿(文本/图片/技能 chip 任一非空)时回填会静默覆盖,
|
||||||
|
// 先 confirm 确认;取消则直接返回,队列项原样保留(尚未 cancelQueued)。
|
||||||
|
if (chatInputRef.value?.hasUnsentInput()) {
|
||||||
|
if (!await confirmDialog(t('aiChat.backfillOverwriteConfirm'))) return
|
||||||
|
}
|
||||||
const skill = item.skill
|
const skill = item.skill
|
||||||
? store.state.skills.find(s => s.name === item.skill) ?? null
|
? store.state.skills.find(s => s.name === item.skill) ?? null
|
||||||
: null
|
: null
|
||||||
|
|||||||
@@ -6,20 +6,21 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="general-settings">
|
<div class="general-settings">
|
||||||
<SettingRow ref="rowTheme" :label="$t('settings.labelTheme')" :desc="$t('settings.descTheme')">
|
<SettingRow ref="rowTheme" :label="$t('settings.labelTheme')" :desc="$t('settings.descTheme')">
|
||||||
<select v-model="settings.theme" class="setting-select" @change="markSaved('theme')">
|
<!-- 走查 P3-11:不再 @change 即显「已保存」,反馈移到 persist 完成后(防假成功) -->
|
||||||
|
<select v-model="settings.theme" class="setting-select">
|
||||||
<option value="dark">{{ $t('settings.themeDark') }}</option>
|
<option value="dark">{{ $t('settings.themeDark') }}</option>
|
||||||
<option value="light">{{ $t('settings.themeLight') }}</option>
|
<option value="light">{{ $t('settings.themeLight') }}</option>
|
||||||
<option value="system">{{ $t('settings.themeSystem') }}</option>
|
<option value="system">{{ $t('settings.themeSystem') }}</option>
|
||||||
</select>
|
</select>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
<SettingRow ref="rowLanguage" :label="$t('settings.labelLanguage')" :desc="$t('settings.descLanguage')">
|
<SettingRow ref="rowLanguage" :label="$t('settings.labelLanguage')" :desc="$t('settings.descLanguage')">
|
||||||
<select v-model="settings.language" class="setting-select" @change="markSaved('language')">
|
<select v-model="settings.language" class="setting-select">
|
||||||
<option value="zh-CN">{{ $t('settings.aiLanguageZh') }}</option>
|
<option value="zh-CN">{{ $t('settings.aiLanguageZh') }}</option>
|
||||||
<option value="en">{{ $t('settings.aiLanguageEn') }}</option>
|
<option value="en">{{ $t('settings.aiLanguageEn') }}</option>
|
||||||
</select>
|
</select>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
<SettingRow ref="rowAiLanguage" :label="$t('settings.labelAiLanguage')" :desc="$t('settings.descAiLanguage')">
|
<SettingRow ref="rowAiLanguage" :label="$t('settings.labelAiLanguage')" :desc="$t('settings.descAiLanguage')">
|
||||||
<select v-model="settings.aiLanguage" class="setting-select" @change="markSaved('aiLanguage')">
|
<select v-model="settings.aiLanguage" class="setting-select">
|
||||||
<option value="auto">{{ $t('settings.aiLanguageAuto') }}</option>
|
<option value="auto">{{ $t('settings.aiLanguageAuto') }}</option>
|
||||||
<option value="zh-CN">{{ $t('settings.aiLanguageZh') }}</option>
|
<option value="zh-CN">{{ $t('settings.aiLanguageZh') }}</option>
|
||||||
<option value="en">{{ $t('settings.aiLanguageEn') }}</option>
|
<option value="en">{{ $t('settings.aiLanguageEn') }}</option>
|
||||||
@@ -27,13 +28,13 @@
|
|||||||
</SettingRow>
|
</SettingRow>
|
||||||
<SettingRow ref="rowToken" :label="$t('settings.labelShowTokenUsage')" :desc="$t('settings.descShowTokenUsage')">
|
<SettingRow ref="rowToken" :label="$t('settings.labelShowTokenUsage')" :desc="$t('settings.descShowTokenUsage')">
|
||||||
<label class="toggle">
|
<label class="toggle">
|
||||||
<input type="checkbox" v-model="settings.showTokenUsage" @change="markSaved('token')" />
|
<input type="checkbox" v-model="settings.showTokenUsage" />
|
||||||
<span class="toggle-slider"></span>
|
<span class="toggle-slider"></span>
|
||||||
</label>
|
</label>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
<SettingRow ref="rowStreamingMd" :label="$t('settings.labelStreamingMd')" :desc="$t('settings.descStreamingMd')">
|
<SettingRow ref="rowStreamingMd" :label="$t('settings.labelStreamingMd')" :desc="$t('settings.descStreamingMd')">
|
||||||
<label class="toggle">
|
<label class="toggle">
|
||||||
<input type="checkbox" v-model="settings.streamingMd" @change="markSaved('streamingMd')" />
|
<input type="checkbox" v-model="settings.streamingMd" />
|
||||||
<span class="toggle-slider"></span>
|
<span class="toggle-slider"></span>
|
||||||
</label>
|
</label>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
@@ -91,32 +92,42 @@ const rowLanguage = useTemplateRef<InstanceType<typeof SettingRow>>('rowLanguage
|
|||||||
const rowAiLanguage = useTemplateRef<InstanceType<typeof SettingRow>>('rowAiLanguage')
|
const rowAiLanguage = useTemplateRef<InstanceType<typeof SettingRow>>('rowAiLanguage')
|
||||||
const rowToken = useTemplateRef<InstanceType<typeof SettingRow>>('rowToken')
|
const rowToken = useTemplateRef<InstanceType<typeof SettingRow>>('rowToken')
|
||||||
const rowStreamingMd = useTemplateRef<InstanceType<typeof SettingRow>>('rowStreamingMd')
|
const rowStreamingMd = useTemplateRef<InstanceType<typeof SettingRow>>('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 }
|
// 走查 P3-11 假成功族:原 @change 立即 markSaved,先于持久化完成,失败无人知晓。
|
||||||
map[key]?.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) => {
|
watch(() => settings.theme, (val) => {
|
||||||
applyTheme(val)
|
applyTheme(val)
|
||||||
void appSettings.set('df-theme', val)
|
void persist('df-theme', val, rowTheme)
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(() => settings.aiLanguage, (val) => {
|
watch(() => settings.aiLanguage, (val) => {
|
||||||
void appSettings.set('df-ai-language', val)
|
void persist('df-ai-language', val, rowAiLanguage)
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(() => settings.showTokenUsage, (val) => {
|
watch(() => settings.showTokenUsage, (val) => {
|
||||||
void appSettings.set('df-show-token-usage', val)
|
void persist('df-show-token-usage', val, rowToken)
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(() => settings.streamingMd, (val) => {
|
watch(() => settings.streamingMd, (val) => {
|
||||||
void appSettings.set('df-ai-streaming-md', val)
|
void persist('df-ai-streaming-md', val, rowStreamingMd)
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(() => settings.language, (val) => {
|
watch(() => settings.language, (val) => {
|
||||||
void appSettings.set('df-language', val)
|
|
||||||
// 同步 i18n 实例,让界面立即跟随语言切换
|
// 同步 i18n 实例,让界面立即跟随语言切换
|
||||||
locale.value = val as 'zh-CN' | 'en'
|
locale.value = val as 'zh-CN' | 'en'
|
||||||
|
void persist('df-language', val, rowLanguage)
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|||||||
@@ -10,7 +10,8 @@
|
|||||||
next-round
|
next-round
|
||||||
:label="$t('settings.labelGlobalConcurrency')"
|
:label="$t('settings.labelGlobalConcurrency')"
|
||||||
:desc="$t('settings.descGlobalConcurrency')">
|
:desc="$t('settings.descGlobalConcurrency')">
|
||||||
<input type="number" class="setting-number" min="1" style="width:80px"
|
<!-- 走查 P3-14:max=64 前端 sanity 上界(原生 min/max 只约束步进按钮,越界拦截在 JS clamp) -->
|
||||||
|
<input type="number" class="setting-number" min="1" max="64" style="width:80px"
|
||||||
v-model.number="settings.llmGlobalConcurrency"
|
v-model.number="settings.llmGlobalConcurrency"
|
||||||
@change="onConcurrencyChange" />
|
@change="onConcurrencyChange" />
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
@@ -19,7 +20,7 @@
|
|||||||
next-round
|
next-round
|
||||||
:label="$t('settings.labelPerConvConcurrency')"
|
:label="$t('settings.labelPerConvConcurrency')"
|
||||||
:desc="$t('settings.descPerConvConcurrency')">
|
:desc="$t('settings.descPerConvConcurrency')">
|
||||||
<input type="number" class="setting-number" min="1" style="width:80px"
|
<input type="number" class="setting-number" min="1" max="64" style="width:80px"
|
||||||
v-model.number="settings.llmPerConvConcurrency"
|
v-model.number="settings.llmPerConvConcurrency"
|
||||||
@change="onConcurrencyChange" />
|
@change="onConcurrencyChange" />
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
@@ -29,7 +30,8 @@
|
|||||||
:label="$t('settings.labelAgentMaxIterations')"
|
:label="$t('settings.labelAgentMaxIterations')"
|
||||||
:desc="$t('settings.descAgentMaxIterations')">
|
:desc="$t('settings.descAgentMaxIterations')">
|
||||||
<label class="unlimited-toggle">
|
<label class="unlimited-toggle">
|
||||||
<input type="checkbox" v-model="unlimited" @change="markSaved('iter')" />
|
<!-- 走查 P3-11:勾选反馈不再即时显,由 syncAgentMaxIterations 的 IPC 成功回调点亮 -->
|
||||||
|
<input type="checkbox" v-model="unlimited" />
|
||||||
<span>{{ $t('settings.labelUnlimited') }}</span>
|
<span>{{ $t('settings.labelUnlimited') }}</span>
|
||||||
</label>
|
</label>
|
||||||
<input type="number" class="setting-number" min="1" max="50" style="width:80px"
|
<input type="number" class="setting-number" min="1" max="50" style="width:80px"
|
||||||
@@ -52,6 +54,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { reactive, watch, computed, onMounted, onUnmounted, useTemplateRef } from 'vue'
|
import { reactive, watch, computed, onMounted, onUnmounted, useTemplateRef } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
import { aiApi } from '@/api'
|
import { aiApi } from '@/api'
|
||||||
import { useAppSettingsStore } from '@/stores/appSettings'
|
import { useAppSettingsStore } from '@/stores/appSettings'
|
||||||
import SettingRow from './SettingRow.vue'
|
import SettingRow from './SettingRow.vue'
|
||||||
@@ -59,10 +62,16 @@ import SettingRow from './SettingRow.vue'
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
// 性能域 — 本地设置(localStorage):LLM 并发 / 单对话并发 / Agent 轮次 / 流式重试
|
// 性能域 — 本地设置(localStorage):LLM 并发 / 单对话并发 / Agent 轮次 / 流式重试
|
||||||
// 拆自 GeneralPanel,本 Section 仅持有性能相关 key + 各自 debounce IPC 同步
|
// 拆自 GeneralPanel,本 Section 仅持有性能相关 key + 各自 debounce IPC 同步
|
||||||
// 阶段5:SettingRow 封装行布局 + nextRound badge(后端快照锁定边界,下次对话生效);
|
// 阶段5:SettingRow 封装行布局 + nextRound badge(后端快照锁定边界,下次对话生效)
|
||||||
// 4 项变更后调 markSaved 显"已保存"
|
// 走查 P3-11:「已保存」改为 IPC 成功回调后点亮,失败点亮行内红字(防假成功)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
const appSettings = useAppSettingsStore()
|
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({
|
const settings = reactive({
|
||||||
llmGlobalConcurrency: appSettings.get<number>('df-llm-global-concurrency', 3),
|
llmGlobalConcurrency: appSettings.get<number>('df-llm-global-concurrency', 3),
|
||||||
@@ -73,7 +82,7 @@ const settings = reactive({
|
|||||||
agentMaxRetries: appSettings.get<number>('df-ai-agent-max-retries', 3),
|
agentMaxRetries: appSettings.get<number>('df-ai-agent-max-retries', 3),
|
||||||
})
|
})
|
||||||
|
|
||||||
// 即时生效反馈:SettingRow refs
|
// 即时生效反馈:SettingRow refs(成功/失败两态,走查 P3-11)
|
||||||
const rowGlobalConcurrency = useTemplateRef<InstanceType<typeof SettingRow>>('rowGlobalConcurrency')
|
const rowGlobalConcurrency = useTemplateRef<InstanceType<typeof SettingRow>>('rowGlobalConcurrency')
|
||||||
const rowPerConvConcurrency = useTemplateRef<InstanceType<typeof SettingRow>>('rowPerConvConcurrency')
|
const rowPerConvConcurrency = useTemplateRef<InstanceType<typeof SettingRow>>('rowPerConvConcurrency')
|
||||||
const rowMaxIterations = useTemplateRef<InstanceType<typeof SettingRow>>('rowMaxIterations')
|
const rowMaxIterations = useTemplateRef<InstanceType<typeof SettingRow>>('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 }
|
const map = { global: rowGlobalConcurrency.value, perConv: rowPerConvConcurrency.value, iter: rowMaxIterations.value, retry: rowMaxRetries.value }
|
||||||
map[key]?.markSaved()
|
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) => {
|
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) => {
|
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) => {
|
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) => {
|
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 不应触发"已保存"反馈(那是启动恢复,非用户改动)
|
// onMounted 期间调 sync 不应触发"已保存"反馈(那是启动恢复,非用户改动)
|
||||||
let _userInteracting = false
|
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)
|
// 同步 LLM 并发上限到后端(debounce 300ms,防快速连续调整时频繁 IPC)
|
||||||
// 持久化已由上方 watch 写 appSettings(SQLite) 完成;此处只负责同步内存限流器
|
// 持久化已由上方 watch 写 appSettings(SQLite) 完成;此处只负责同步内存限流器
|
||||||
let _concurrencyTimer: ReturnType<typeof setTimeout> | null = null
|
let _concurrencyTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
function syncConcurrencyConfig() {
|
function syncConcurrencyConfig() {
|
||||||
// clamp 到合理区间,防越界输入(缓存被篡改 / 手动输超限)致限流失效;
|
// clamp 到合理区间,防越界输入(缓存被篡改 / 手动输超限)致限流失效;
|
||||||
// 修正后值同步回 v-model(input 显示) + watch 持久化
|
// 修正后值同步回 v-model(input 显示) + watch 持久化
|
||||||
// 下限 1,无上限;约束单对话并发不超过全局并发
|
// 下限 1,上界 64(前端 sanity);约束单对话并发不超过全局并发
|
||||||
settings.llmGlobalConcurrency = Math.max(1, settings.llmGlobalConcurrency || 3)
|
let clamped = false
|
||||||
settings.llmPerConvConcurrency = Math.max(1, settings.llmPerConvConcurrency || 2)
|
const g = clampConcurrency(settings.llmGlobalConcurrency, 3)
|
||||||
if (settings.llmPerConvConcurrency > settings.llmGlobalConcurrency) {
|
if (g !== settings.llmGlobalConcurrency) clamped = true
|
||||||
settings.llmPerConvConcurrency = settings.llmGlobalConcurrency
|
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)
|
if (_concurrencyTimer) clearTimeout(_concurrencyTimer)
|
||||||
|
// 走查 P3-11:interactive 在排程时捕获(debounce 回调执行时 _userInteracting 已复位 false)
|
||||||
|
const interactive = _userInteracting
|
||||||
_concurrencyTimer = setTimeout(async () => {
|
_concurrencyTimer = setTimeout(async () => {
|
||||||
try {
|
try {
|
||||||
await aiApi.setConcurrencyConfig(settings.llmGlobalConcurrency, settings.llmPerConvConcurrency)
|
await aiApi.setConcurrencyConfig(g, p)
|
||||||
|
// IPC 成功才显「已保存」,失败显行内红字(原实现先显成功、失败仅 console = 假成功)
|
||||||
|
if (interactive) { markSaved('global'); markSaved('perConv') }
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('同步 LLM 并发配置失败:', e)
|
console.error('同步 LLM 并发配置失败:', e)
|
||||||
|
if (interactive) { markFailed('global'); markFailed('perConv') }
|
||||||
}
|
}
|
||||||
}, 300)
|
}, 300)
|
||||||
if (_userInteracting) {
|
|
||||||
markSaved('global')
|
|
||||||
markSaved('perConv')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// wrapper:并发变更(@change 用户触发)同时反馈两项(global/perConv 任一变动都同步两项)
|
// wrapper:并发变更(@change 用户触发)同时反馈两项(global/perConv 任一变动都同步两项)
|
||||||
@@ -158,28 +190,32 @@ function syncAgentMaxIterations() {
|
|||||||
settings.agentMaxIterations = Math.min(50, Math.max(1, settings.agentMaxIterations || 10))
|
settings.agentMaxIterations = Math.min(50, Math.max(1, settings.agentMaxIterations || 10))
|
||||||
}
|
}
|
||||||
if (_agentIterTimer) clearTimeout(_agentIterTimer)
|
if (_agentIterTimer) clearTimeout(_agentIterTimer)
|
||||||
|
const interactive = _userInteracting
|
||||||
_agentIterTimer = setTimeout(async () => {
|
_agentIterTimer = setTimeout(async () => {
|
||||||
try {
|
try {
|
||||||
await aiApi.setAgentMaxIterations(settings.agentMaxIterations)
|
await aiApi.setAgentMaxIterations(settings.agentMaxIterations)
|
||||||
|
if (interactive) markSaved('iter')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('同步 Agent 最大轮次失败:', e)
|
console.error('同步 Agent 最大轮次失败:', e)
|
||||||
|
if (interactive) markFailed('iter')
|
||||||
}
|
}
|
||||||
}, 300)
|
}, 300)
|
||||||
if (_userInteracting) {
|
|
||||||
markSaved('iter')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 「不限」勾选:agentMaxIterations===0 即不限(后端 0→usize::MAX,loop 靠 stop/收敛/审批退出)
|
// 「不限」勾选:agentMaxIterations===0 即不限(后端 0→usize::MAX,loop 靠 stop/收敛/审批退出)
|
||||||
const unlimited = computed({
|
const unlimited = computed({
|
||||||
get: () => settings.agentMaxIterations === 0,
|
get: () => settings.agentMaxIterations === 0,
|
||||||
set(v: boolean) {
|
set(v: boolean) {
|
||||||
|
// 走查 P3-11:勾选属用户交互,标记 interactive 让「已保存」等 IPC 成功后再显
|
||||||
|
// (原实现在模板 @change 即时 markSaved,先于持久化 = 假成功)
|
||||||
|
_userInteracting = true
|
||||||
if (v) {
|
if (v) {
|
||||||
settings.agentMaxIterations = 0
|
settings.agentMaxIterations = 0
|
||||||
} else if (settings.agentMaxIterations === 0) {
|
} else if (settings.agentMaxIterations === 0) {
|
||||||
settings.agentMaxIterations = 10
|
settings.agentMaxIterations = 10
|
||||||
}
|
}
|
||||||
syncAgentMaxIterations()
|
syncAgentMaxIterations()
|
||||||
|
_userInteracting = false
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -191,16 +227,16 @@ function syncAgentMaxRetries() {
|
|||||||
// clamp 0-10,防越界
|
// clamp 0-10,防越界
|
||||||
settings.agentMaxRetries = Math.min(10, Math.max(0, settings.agentMaxRetries ?? 3))
|
settings.agentMaxRetries = Math.min(10, Math.max(0, settings.agentMaxRetries ?? 3))
|
||||||
if (_agentRetryTimer) clearTimeout(_agentRetryTimer)
|
if (_agentRetryTimer) clearTimeout(_agentRetryTimer)
|
||||||
|
const interactive = _userInteracting
|
||||||
_agentRetryTimer = setTimeout(async () => {
|
_agentRetryTimer = setTimeout(async () => {
|
||||||
try {
|
try {
|
||||||
await aiApi.setAgentMaxRetries(settings.agentMaxRetries)
|
await aiApi.setAgentMaxRetries(settings.agentMaxRetries)
|
||||||
|
if (interactive) markSaved('retry')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('同步流式重试次数失败:', e)
|
console.error('同步流式重试次数失败:', e)
|
||||||
|
if (interactive) markFailed('retry')
|
||||||
}
|
}
|
||||||
}, 300)
|
}, 300)
|
||||||
if (_userInteracting) {
|
|
||||||
markSaved('retry')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|||||||
@@ -67,7 +67,10 @@
|
|||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label class="form-label">{{ $t('settings.labelName') }}</label>
|
<label class="form-label">{{ $t('settings.labelName') }}</label>
|
||||||
<input v-model="providerForm.name" class="setting-input" :placeholder="$t('settings.phProviderName')" />
|
<!-- 走查 P3-10:校验失败定位字段——红框 + @input 清错(示范级,仅本表单) -->
|
||||||
|
<input ref="inputName" v-model="providerForm.name" class="setting-input"
|
||||||
|
:class="{ 'setting-input--invalid': fieldErrors.name }"
|
||||||
|
:placeholder="$t('settings.phProviderName')" @input="fieldErrors.name = false" />
|
||||||
</div>
|
</div>
|
||||||
<!-- 项 D:厂商预设下拉 — 选中自动填充 providerType/baseUrl/defaultModel(用户可改覆盖)。
|
<!-- 项 D:厂商预设下拉 — 选中自动填充 providerType/baseUrl/defaultModel(用户可改覆盖)。
|
||||||
新建态独立"快捷选择";编辑态回填后切预设仍可覆盖(等价快捷填充),不阻塞手改。
|
新建态独立"快捷选择";编辑态回填后切预设仍可覆盖(等价快捷填充),不阻塞手改。
|
||||||
@@ -88,12 +91,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label class="form-label">{{ $t('settings.labelBaseUrl') }}</label>
|
<label class="form-label">{{ $t('settings.labelBaseUrl') }}</label>
|
||||||
<input v-model="providerForm.baseUrl" class="setting-input" :placeholder="$t('settings.phBaseUrl')" />
|
<input ref="inputBaseUrl" v-model="providerForm.baseUrl" class="setting-input"
|
||||||
|
:class="{ 'setting-input--invalid': fieldErrors.baseUrl }"
|
||||||
|
:placeholder="$t('settings.phBaseUrl')" @input="fieldErrors.baseUrl = false" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label class="form-label">{{ $t('settings.labelApiKey') }}</label>
|
<label class="form-label">{{ $t('settings.labelApiKey') }}</label>
|
||||||
<!-- 走查 P3-9:type 恒 password 无法核对已粘贴密钥,加显隐切换 -->
|
<!-- 走查 P3-9:type 恒 password 无法核对已粘贴密钥,加显隐切换 -->
|
||||||
<input v-model="providerForm.apiKey" class="setting-input" :type="showApiKey ? 'text' : 'password'" :placeholder="$t('settings.phApiKey')" />
|
<input ref="inputApiKey" v-model="providerForm.apiKey" class="setting-input" :type="showApiKey ? 'text' : 'password'"
|
||||||
|
:class="{ 'setting-input--invalid': fieldErrors.apiKey }"
|
||||||
|
:placeholder="$t('settings.phApiKey')" @input="fieldErrors.apiKey = false" />
|
||||||
<button type="button" class="btn-link" :title="$t(showApiKey ? 'settings.apiKeyHide' : 'settings.apiKeyShow')"
|
<button type="button" class="btn-link" :title="$t(showApiKey ? 'settings.apiKeyHide' : 'settings.apiKeyShow')"
|
||||||
@click="showApiKey = !showApiKey">
|
@click="showApiKey = !showApiKey">
|
||||||
{{ $t(showApiKey ? 'settings.apiKeyHide' : 'settings.apiKeyShow') }}
|
{{ $t(showApiKey ? 'settings.apiKeyHide' : 'settings.apiKeyShow') }}
|
||||||
@@ -104,7 +111,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label class="form-label">{{ $t('settings.labelDefaultModel') }}</label>
|
<label class="form-label">{{ $t('settings.labelDefaultModel') }}</label>
|
||||||
<input v-model="providerForm.defaultModel" class="setting-input" :placeholder="$t('settings.phDefaultModel')" />
|
<input ref="inputDefaultModel" v-model="providerForm.defaultModel" class="setting-input"
|
||||||
|
:class="{ 'setting-input--invalid': fieldErrors.defaultModel }"
|
||||||
|
:placeholder="$t('settings.phDefaultModel')" @input="fieldErrors.defaultModel = false" />
|
||||||
<!-- 方向 A(轻量):defaultModel 仅作"建议起点"——预设 model id 易过时(DeepSeek chat 停用 /
|
<!-- 方向 A(轻量):defaultModel 仅作"建议起点"——预设 model id 易过时(DeepSeek chat 停用 /
|
||||||
Gemini 2.5→3.5 等),提示用户点「测试连接」拉真实模型列表覆盖,降低踩过时默认值的困惑。 -->
|
Gemini 2.5→3.5 等),提示用户点「测试连接」拉真实模型列表覆盖,降低踩过时默认值的困惑。 -->
|
||||||
<div class="form-field-hint">{{ $t('settings.defaultModelHint') }}</div>
|
<div class="form-field-hint">{{ $t('settings.defaultModelHint') }}</div>
|
||||||
@@ -112,6 +121,7 @@
|
|||||||
<!-- 供应商级采样温度覆盖:部分受限模型(如 Kimi 系)仅接受 temperature=1,留空走默认 0.7 -->
|
<!-- 供应商级采样温度覆盖:部分受限模型(如 Kimi 系)仅接受 temperature=1,留空走默认 0.7 -->
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label class="form-label">{{ $t('settings.labelSamplingTemp') }}</label>
|
<label class="form-label">{{ $t('settings.labelSamplingTemp') }}</label>
|
||||||
|
<!-- 走查 P3-14:原生 min/max 只约束步进按钮,手输越界由 saveProvider clamp + toast 提示 -->
|
||||||
<input type="number" v-model.number="providerForm.temperature" class="setting-input"
|
<input type="number" v-model.number="providerForm.temperature" class="setting-input"
|
||||||
:placeholder="$t('settings.phSamplingTemp')" step="0.1" min="0" max="2" />
|
:placeholder="$t('settings.phSamplingTemp')" step="0.1" min="0" max="2" />
|
||||||
</div>
|
</div>
|
||||||
@@ -192,7 +202,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { reactive, ref, computed, onMounted } from 'vue'
|
import { reactive, ref, computed, onMounted, useTemplateRef } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { aiApi } from '@/api'
|
import { aiApi } from '@/api'
|
||||||
import type { AiProviderConfig, ModelConfig, Modality, Capability, ProbeSource } from '@/api/types'
|
import type { AiProviderConfig, ModelConfig, Modality, Capability, ProbeSource } from '@/api/types'
|
||||||
@@ -228,6 +238,25 @@ const aiProviders = ref<AiProviderConfig[]>([])
|
|||||||
// 密钥显隐切换(走查 P3-9):默认隐藏;重开表单时复位,防上一家 provider 的显钥状态延续
|
// 密钥显隐切换(走查 P3-9):默认隐藏;重开表单时复位,防上一家 provider 的显钥状态延续
|
||||||
const showApiKey = ref(false)
|
const showApiKey = ref(false)
|
||||||
|
|
||||||
|
// 走查 P3-10:校验失败只 toast 不定位字段——补字段级红框 + 滚动聚焦首个缺失字段。
|
||||||
|
// 仅保存必填四项(name/baseUrl/apiKey/defaultModel)做示范级实现;红框在对应输入框
|
||||||
|
// @input 时即清(用户开始修正就不该继续红着)。
|
||||||
|
const fieldErrors = reactive({ name: false, baseUrl: false, apiKey: false, defaultModel: false })
|
||||||
|
const inputName = useTemplateRef<HTMLInputElement>('inputName')
|
||||||
|
const inputBaseUrl = useTemplateRef<HTMLInputElement>('inputBaseUrl')
|
||||||
|
const inputApiKey = useTemplateRef<HTMLInputElement>('inputApiKey')
|
||||||
|
const inputDefaultModel = useTemplateRef<HTMLInputElement>('inputDefaultModel')
|
||||||
|
|
||||||
|
/** 聚焦首个红框字段(focus 默认行为即滚动到该元素,无需额外 scrollIntoView) */
|
||||||
|
function focusFirstError() {
|
||||||
|
const el = fieldErrors.name ? inputName.value
|
||||||
|
: fieldErrors.baseUrl ? inputBaseUrl.value
|
||||||
|
: fieldErrors.apiKey ? inputApiKey.value
|
||||||
|
: fieldErrors.defaultModel ? inputDefaultModel.value
|
||||||
|
: null
|
||||||
|
el?.focus()
|
||||||
|
}
|
||||||
|
|
||||||
function errMsg(e: unknown): string { return e instanceof Error ? e.message : String(e) }
|
function errMsg(e: unknown): string { return e instanceof Error ? e.message : String(e) }
|
||||||
|
|
||||||
const providerForm = reactive({
|
const providerForm = reactive({
|
||||||
@@ -433,6 +462,7 @@ function openProviderForm(p?: AiProviderConfig) {
|
|||||||
}
|
}
|
||||||
providerForm.fetching = false
|
providerForm.fetching = false
|
||||||
providerForm.saveError = null // 重开表单清旧错误横幅
|
providerForm.saveError = null // 重开表单清旧错误横幅
|
||||||
|
fieldErrors.name = fieldErrors.baseUrl = fieldErrors.apiKey = fieldErrors.defaultModel = false // 重开表单清字段红框
|
||||||
showApiKey.value = false // 密钥默认隐藏,不延续上次表单的显钥状态(防窥)
|
showApiKey.value = false // 密钥默认隐藏,不延续上次表单的显钥状态(防窥)
|
||||||
providerForm.visible = true
|
providerForm.visible = true
|
||||||
// L19/M32: 记初始快照(在 visible=true、字段回填完成后),isFormDirty 据此比对。
|
// L19/M32: 记初始快照(在 visible=true、字段回填完成后),isFormDirty 据此比对。
|
||||||
@@ -547,10 +577,28 @@ function classifySaveError(raw: string): { title: string; detail: string } {
|
|||||||
async function saveProvider() {
|
async function saveProvider() {
|
||||||
// apiKey:新建必填;编辑可留空(不改,FR-S1 后端保留原 DB 值)
|
// apiKey:新建必填;编辑可留空(不改,FR-S1 后端保留原 DB 值)
|
||||||
const needKey = !providerForm.editId
|
const needKey = !providerForm.editId
|
||||||
if (!providerForm.name || !providerForm.baseUrl || (needKey && !providerForm.apiKey) || !providerForm.defaultModel) {
|
// 走查 P3-10:逐字段判定缺失,红框标出并滚动聚焦首个缺失字段(toast 保留,双通道反馈)
|
||||||
|
fieldErrors.name = !providerForm.name
|
||||||
|
fieldErrors.baseUrl = !providerForm.baseUrl
|
||||||
|
fieldErrors.apiKey = needKey && !providerForm.apiKey
|
||||||
|
fieldErrors.defaultModel = !providerForm.defaultModel
|
||||||
|
if (fieldErrors.name || fieldErrors.baseUrl || fieldErrors.apiKey || fieldErrors.defaultModel) {
|
||||||
emit('toast', t('settings.toastSaveIncomplete'), 'warning')
|
emit('toast', t('settings.toastSaveIncomplete'), 'warning')
|
||||||
|
focusFirstError()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// 走查 P3-14:采样温度钳制 [0,2](原生 min/max 只约束步进按钮,手输 99 照存)。
|
||||||
|
// 越界有限值 → clamp + toast 告知修正值;NaN/清空残留 → 归一 null(走默认 0.7,同步清显示)
|
||||||
|
let temperatureOverride: number | undefined
|
||||||
|
if (typeof providerForm.temperature === 'number' && Number.isFinite(providerForm.temperature)) {
|
||||||
|
if (providerForm.temperature < 0 || providerForm.temperature > 2) {
|
||||||
|
providerForm.temperature = Math.min(2, Math.max(0, providerForm.temperature))
|
||||||
|
emit('toast', t('settings.toastTempClamped', { v: providerForm.temperature }), 'warning')
|
||||||
|
}
|
||||||
|
temperatureOverride = providerForm.temperature
|
||||||
|
} else {
|
||||||
|
providerForm.temperature = null
|
||||||
|
}
|
||||||
providerForm.saving = true
|
providerForm.saving = true
|
||||||
try {
|
try {
|
||||||
await aiApi.saveProvider({
|
await aiApi.saveProvider({
|
||||||
@@ -561,11 +609,7 @@ async function saveProvider() {
|
|||||||
apiKey: providerForm.apiKey,
|
apiKey: providerForm.apiKey,
|
||||||
defaultModel: providerForm.defaultModel,
|
defaultModel: providerForm.defaultModel,
|
||||||
modelConfigs: providerForm.models,
|
modelConfigs: providerForm.models,
|
||||||
// 采样温度非空且为有效数字才带(清空输入 v-model.number 可能残留 ''/NaN,不过滤会污染后端 config)
|
temperatureOverride,
|
||||||
temperatureOverride:
|
|
||||||
typeof providerForm.temperature === 'number' && Number.isFinite(providerForm.temperature)
|
|
||||||
? providerForm.temperature
|
|
||||||
: undefined,
|
|
||||||
})
|
})
|
||||||
// 成功明确反馈——清错误横幅 + 关表单 + 绿色「已保存」toast
|
// 成功明确反馈——清错误横幅 + 关表单 + 绿色「已保存」toast
|
||||||
providerForm.saveError = null
|
providerForm.saveError = null
|
||||||
@@ -788,6 +832,9 @@ onMounted(loadProviders)
|
|||||||
/* 输入/下拉在 form-field 内 flex 填满(基础样式走全局 settings.css .setting-input/.setting-select) */
|
/* 输入/下拉在 form-field 内 flex 填满(基础样式走全局 settings.css .setting-input/.setting-select) */
|
||||||
.form-field .setting-input,
|
.form-field .setting-input,
|
||||||
.form-field .setting-select { flex: 1; min-width: 0; }
|
.form-field .setting-select { flex: 1; min-width: 0; }
|
||||||
|
/* 走查 P3-10:校验缺失字段红框(@input 即清,用户开始修正就不再红) */
|
||||||
|
.setting-input--invalid { border-color: var(--df-danger) !important; }
|
||||||
|
.setting-input--invalid:focus { border-color: var(--df-danger) !important; box-shadow: 0 0 0 1px var(--df-danger); }
|
||||||
.setting-input--sm { flex: 0 0 auto; min-width: 0; width: 70px; padding: 4px 8px; font-size: 12px; }
|
.setting-input--sm { flex: 0 0 auto; min-width: 0; width: 70px; padding: 4px 8px; font-size: 12px; }
|
||||||
|
|
||||||
/* Toggle 小号变体(本面板负载均衡池用;.toggle 基础走全局) */
|
/* Toggle 小号变体(本面板负载均衡池用;.toggle 基础走全局) */
|
||||||
|
|||||||
@@ -11,7 +11,11 @@
|
|||||||
<div class="setting-control">
|
<div class="setting-control">
|
||||||
<slot />
|
<slot />
|
||||||
<Transition name="saved-fade">
|
<Transition name="saved-fade">
|
||||||
<span v-if="savedVisible" class="saved-hint">{{ $t('settings.savedHint') }}</span>
|
<!-- 走查 P3-11 假成功族:持久化/IPC 有成功/失败两态,反馈位同理补失败红字,
|
||||||
|
防止「已保存」承诺落空后用户无从察觉 -->
|
||||||
|
<span v-if="hintState !== 'none'" class="saved-hint" :class="{ 'saved-hint--fail': hintState === 'failed' }">
|
||||||
|
{{ hintState === 'saved' ? $t('settings.savedHint') : $t('settings.saveFailedHint') }}
|
||||||
|
</span>
|
||||||
</Transition>
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -21,9 +25,10 @@
|
|||||||
import { shallowRef } from 'vue'
|
import { shallowRef } from 'vue'
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 统一设置行:label/desc 入 props,控件走默认 slot,变更后父级调 markSaved() 显反馈。
|
// 统一设置行:label/desc 入 props,控件走默认 slot,变更后父级调 markSaved() 显反馈,
|
||||||
|
// 持久化失败时调 markFailed() 显红字(走查 P3-11:只报成功不报失败 = 半个反馈位)。
|
||||||
// nextRound=true 显"下次对话生效" badge(性能域并发/轮次/重试白名单项)。
|
// nextRound=true 显"下次对话生效" badge(性能域并发/轮次/重试白名单项)。
|
||||||
// savedVisible 用 shallowRef(布尔翻转无深响应需求);timer ref 持有供连续触发时清旧。
|
// hintState 用 shallowRef(字符串翻转无深响应需求);timer ref 持有供连续触发时清旧。
|
||||||
// ============================================================
|
// ============================================================
|
||||||
defineProps<{
|
defineProps<{
|
||||||
/** 设置项标题 */
|
/** 设置项标题 */
|
||||||
@@ -34,30 +39,41 @@ defineProps<{
|
|||||||
nextRound?: boolean
|
nextRound?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const savedVisible = shallowRef(false)
|
const hintState = shallowRef<'none' | 'saved' | 'failed'>('none')
|
||||||
const timer = shallowRef<ReturnType<typeof setTimeout> | null>(null)
|
const timer = shallowRef<ReturnType<typeof setTimeout> | null>(null)
|
||||||
|
|
||||||
/** 显"✓已保存"行内提示,2s 后消失。连续触发清旧 timer 防提前消失。 */
|
/** 点亮行内反馈提示,2s 后消失。连续触发清旧 timer 防提前消失。 */
|
||||||
function markSaved() {
|
function flashHint(state: 'saved' | 'failed') {
|
||||||
savedVisible.value = true
|
hintState.value = state
|
||||||
if (timer.value) clearTimeout(timer.value)
|
if (timer.value) clearTimeout(timer.value)
|
||||||
timer.value = setTimeout(() => {
|
timer.value = setTimeout(() => {
|
||||||
savedVisible.value = false
|
hintState.value = 'none'
|
||||||
timer.value = null
|
timer.value = null
|
||||||
}, 2000)
|
}, 2000)
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({ markSaved })
|
/** 显"✓已保存"行内提示(持久化/IPC 成功后才调,防假成功) */
|
||||||
|
function markSaved() {
|
||||||
|
flashHint('saved')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 显"✗ 保存失败"行内提示(持久化/IPC 失败路径调用) */
|
||||||
|
function markFailed() {
|
||||||
|
flashHint('failed')
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ markSaved, markFailed })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* 反馈位"✓已保存" — 行内紧贴控件右侧,2s 淡出 */
|
/* 反馈位"✓已保存" — 行内紧贴控件右侧,2s 淡出;失败态红字区分 */
|
||||||
.saved-hint {
|
.saved-hint {
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--df-accent);
|
color: var(--df-accent);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
.saved-hint--fail { color: var(--df-danger); }
|
||||||
/* badge"下次对话生效" — label 右侧小标 */
|
/* badge"下次对话生效" — label 右侧小标 */
|
||||||
.next-round-badge {
|
.next-round-badge {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- ═══ 设置左导航(master-detail 左栏)═══
|
<!-- ═══ 设置左导航(master-detail 左栏)═══
|
||||||
阶段3 UX 重构:6 类导航(外观/AI模型/知识库/性能/安全/高级)+ emoji 图标。
|
阶段3 UX 重构:6 类导航(外观/AI模型/知识库/性能/安全/高级)+ emoji 图标。
|
||||||
阶段4 UX 重构:顶部搜索框 → 匹配 label/desc → 高亮匹配类 + 选中首匹配类
|
阶段4 UX 重构:顶部搜索框 → 匹配 label/desc → 即时高亮匹配类 + 防抖后跳首匹配类
|
||||||
+ emit scroll-target(父 Settings.vue 滚动到对应 setting-row)。
|
+ emit scroll-target(父 Settings.vue 滚动到对应 setting-row)。
|
||||||
|
走查 P3-17:跳转防抖 350ms + 仅命中才跳,避免每键入强制切分类打断输入。
|
||||||
activeCategory 受控(双向),v-model 由 Settings.vue 绑定 + appSettings 持久化。
|
activeCategory 受控(双向),v-model 由 Settings.vue 绑定 + appSettings 持久化。
|
||||||
底部留导入导出占位(阶段6 接入)。 -->
|
底部留导入导出占位(阶段6 接入)。 -->
|
||||||
<nav class="settings-nav" :class="{ 'is-horizontal': horizontal }">
|
<nav class="settings-nav" :class="{ 'is-horizontal': horizontal }">
|
||||||
@@ -52,7 +53,7 @@
|
|||||||
// advanced 高级(autoExecuteMode/logLevel + 连接)
|
// advanced 高级(autoExecuteMode/logLevel + 连接)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch, onUnmounted } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { SETTINGS_INDEX, type SettingsCategory, type SettingIndexEntry } from './searchIndex'
|
import { SETTINGS_INDEX, type SettingsCategory, type SettingIndexEntry } from './searchIndex'
|
||||||
|
|
||||||
@@ -126,12 +127,28 @@ function isMatchedCategory(cat: SettingsCategory): boolean {
|
|||||||
return matchedCategories.value.includes(cat)
|
return matchedCategories.value.includes(cat)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 关键词变化 → 选中首匹配类 + 上抛首匹配项 key(触发右侧滚动定位)。
|
/** 关键词变化 → (防抖后)选中首匹配类 + 上抛首匹配项 key(触发右侧滚动定位)。
|
||||||
* 空关键词时不改 activeCategory(用户清空搜索应留在当前类),也不上抛。 */
|
* 空关键词时不改 activeCategory(用户清空搜索应留在当前类),也不上抛。
|
||||||
|
* 走查 P3-17:原实现每敲一键立即切分类+滚动,浏览 A 类打字被反复拽到 B 类打断输入。
|
||||||
|
* 改为 350ms 防抖——连续键入只更新左侧命中高亮(即时,computed),停顿后才跳转定位;
|
||||||
|
* 且仅当有命中时才跳(无命中保持原地,不打断)。 */
|
||||||
|
const JUMP_DEBOUNCE_MS = 350
|
||||||
|
let _jumpTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
function cancelPendingJump() {
|
||||||
|
if (_jumpTimer) {
|
||||||
|
clearTimeout(_jumpTimer)
|
||||||
|
_jumpTimer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
watch(
|
watch(
|
||||||
() => searchKeyword.value,
|
() => searchKeyword.value,
|
||||||
(kw) => {
|
() => {
|
||||||
const trimmed = kw.trim()
|
cancelPendingJump()
|
||||||
|
if (!searchKeyword.value.trim()) return
|
||||||
|
_jumpTimer = setTimeout(() => {
|
||||||
|
_jumpTimer = null
|
||||||
|
// 防抖窗口后关键词可能又变了,重读当前值;无命中不跳(仅命中时跳)
|
||||||
|
const trimmed = searchKeyword.value.trim()
|
||||||
if (!trimmed) return
|
if (!trimmed) return
|
||||||
const first = matchedEntries.value[0]
|
const first = matchedEntries.value[0]
|
||||||
if (!first) return
|
if (!first) return
|
||||||
@@ -140,12 +157,17 @@ watch(
|
|||||||
}
|
}
|
||||||
// 分类切换后需等右侧 Section 挂载,父组件 watch scroll-target 时自带 nextTick
|
// 分类切换后需等右侧 Section 挂载,父组件 watch scroll-target 时自带 nextTick
|
||||||
emit('scroll-target', first.key)
|
emit('scroll-target', first.key)
|
||||||
|
}, JUMP_DEBOUNCE_MS)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 卸载清防抖 timer,防 unmount 后旧 timer fire 时 emit 到已销毁组件
|
||||||
|
onUnmounted(cancelPendingJump)
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
/** 清空搜索框(父组件切分类时可调用,但当前保留搜索状态便于用户继续搜) */
|
/** 清空搜索框(父组件切分类时可调用,但当前保留搜索状态便于用户继续搜) */
|
||||||
clearSearch() {
|
clearSearch() {
|
||||||
|
cancelPendingJump()
|
||||||
searchKeyword.value = ''
|
searchKeyword.value = ''
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -89,6 +89,19 @@ export default {
|
|||||||
truncated: 'truncated',
|
truncated: 'truncated',
|
||||||
failed: 'failed',
|
failed: 'failed',
|
||||||
meta: '{rounds} rounds · {tokens} tokens',
|
meta: '{rounds} rounds · {tokens} tokens',
|
||||||
|
// S3: finished card is clickable to open the read-only trace (hover hint)
|
||||||
|
viewHint: 'Click to view execution trace',
|
||||||
|
},
|
||||||
|
// S3 sub-agent trace overlay (opened from the finished card, read-only simple render,
|
||||||
|
// discarded on close)
|
||||||
|
agentTrace: {
|
||||||
|
title: 'Sub-agent Trace',
|
||||||
|
empty: 'No messages recorded',
|
||||||
|
loadFail: 'Failed to load sub-agent trace',
|
||||||
|
roleUser: 'User',
|
||||||
|
roleAssistant: 'Assistant',
|
||||||
|
roleTool: 'Tool result',
|
||||||
|
roleSystem: 'System',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -292,5 +292,10 @@ export default {
|
|||||||
sendErrRateLimit: 'Rate limit reached, please retry later (error: {msg})',
|
sendErrRateLimit: 'Rate limit reached, please retry later (error: {msg})',
|
||||||
sendErrQuota: 'AI quota or balance insufficient, check your provider account (error: {msg})',
|
sendErrQuota: 'AI quota or balance insufficient, check your provider account (error: {msg})',
|
||||||
sendErrNetwork: 'Network error or timeout, check your connection and retry (error: {msg})',
|
sendErrNetwork: 'Network error or timeout, check your connection and retry (error: {msg})',
|
||||||
|
|
||||||
|
// ── UX-P3 chat polish additions ──
|
||||||
|
editEmptyBlocked: 'Edit content is empty, submission cancelled',
|
||||||
|
skillNoMatchQuery: 'No skill matches "{q}", send blocked',
|
||||||
|
backfillOverwriteConfirm: 'The input box has an unsent draft; backfilling will overwrite it. Continue?',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,5 +24,7 @@ export default {
|
|||||||
perPage: 'Per page',
|
perPage: 'Per page',
|
||||||
total: '{n} total',
|
total: '{n} total',
|
||||||
},
|
},
|
||||||
|
// Close-to-tray notice bubble (App.vue consumes the tray-hide-notice event; moved from hardcoded Chinese)
|
||||||
|
trayNoticeRunning: '{n} conversation(s) keep running in the background — the tray icon brings the window back anytime',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,15 @@ export default {
|
|||||||
|
|
||||||
// ===== Instant feedback (phase 5 SettingRow) =====
|
// ===== Instant feedback (phase 5 SettingRow) =====
|
||||||
savedHint: '✓ Saved',
|
savedHint: '✓ Saved',
|
||||||
|
// False-success family: red inline feedback for persist/IPC failure
|
||||||
|
saveFailedHint: '✗ Save failed',
|
||||||
nextRoundBadge: 'Next message',
|
nextRoundBadge: 'Next message',
|
||||||
|
// Out-of-range clamp notices (concurrency 1-64 + per-conv ≤ global; temperature reports the corrected value)
|
||||||
|
toastConcurrencyClamped: 'Value out of range, corrected automatically (concurrency 1-{max}; per-conversation capped at global)',
|
||||||
|
toastTempClamped: 'Sampling temperature must be within 0-2, corrected to {v}',
|
||||||
|
// Prompt caching toggle save feedback (previously silent)
|
||||||
|
toastAnthropicCacheSaved: 'Prompt caching setting saved',
|
||||||
|
toastAnthropicCacheFail: 'Failed to save the prompt caching setting',
|
||||||
|
|
||||||
// ===== Connection mgmt (phase 7: experimental badge, not wired to AI yet) =====
|
// ===== Connection mgmt (phase 7: experimental badge, not wired to AI yet) =====
|
||||||
experimentalBadge: 'Experimental · not wired to AI tools',
|
experimentalBadge: 'Experimental · not wired to AI tools',
|
||||||
|
|||||||
@@ -91,5 +91,7 @@ export default {
|
|||||||
descSaveFailed: 'Failed to save description: {msg}',
|
descSaveFailed: 'Failed to save description: {msg}',
|
||||||
saveDesc: 'Save',
|
saveDesc: 'Save',
|
||||||
cancelEdit: 'Cancel',
|
cancelEdit: 'Cancel',
|
||||||
|
// S4 related conversations (reverse lookup of main sessions that ran task tools, click to jump)
|
||||||
|
relatedConversations: 'Conversations',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,6 +88,18 @@ export default {
|
|||||||
truncated: '已截断',
|
truncated: '已截断',
|
||||||
failed: '失败',
|
failed: '失败',
|
||||||
meta: '{rounds} 轮 · {tokens} tokens',
|
meta: '{rounds} 轮 · {tokens} tokens',
|
||||||
|
// S3:完成卡可点击查看只读轨迹(hover 提示)
|
||||||
|
viewHint: '点击查看执行轨迹',
|
||||||
|
},
|
||||||
|
// S3 子代理轨迹浮层(完成卡点击展开,只读简版渲染,关即弃)
|
||||||
|
agentTrace: {
|
||||||
|
title: '子代理执行轨迹',
|
||||||
|
empty: '暂无消息记录',
|
||||||
|
loadFail: '拉取子代理轨迹失败',
|
||||||
|
roleUser: '用户',
|
||||||
|
roleAssistant: '助手',
|
||||||
|
roleTool: '工具结果',
|
||||||
|
roleSystem: '系统',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -293,5 +293,10 @@ export default {
|
|||||||
sendErrRateLimit: '发送频率超限,请稍后重试(原始错误:{msg})',
|
sendErrRateLimit: '发送频率超限,请稍后重试(原始错误:{msg})',
|
||||||
sendErrQuota: 'AI 服务额度或余额不足,请检查 Provider 账户(原始错误:{msg})',
|
sendErrQuota: 'AI 服务额度或余额不足,请检查 Provider 账户(原始错误:{msg})',
|
||||||
sendErrNetwork: '网络异常或请求超时,请检查网络后重试(原始错误:{msg})',
|
sendErrNetwork: '网络异常或请求超时,请检查网络后重试(原始错误:{msg})',
|
||||||
|
|
||||||
|
// ── UX-P3 对话域打磨追加 ──
|
||||||
|
editEmptyBlocked: '编辑内容为空,已取消提交',
|
||||||
|
skillNoMatchQuery: '未找到技能「{q}」,已阻止发送',
|
||||||
|
backfillOverwriteConfirm: '输入框已有未发送草稿,回填将覆盖,确定继续?',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,5 +24,7 @@ export default {
|
|||||||
perPage: '每页',
|
perPage: '每页',
|
||||||
total: '共 {n} 条',
|
total: '共 {n} 条',
|
||||||
},
|
},
|
||||||
|
// 关窗后台运行气泡(App.vue 消费 tray-hide-notice 事件;走查 P3-15 由硬编码中文迁入)
|
||||||
|
trayNoticeRunning: '{n} 个会话将继续在后台运行,托盘图标可随时唤起',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,15 @@ export default {
|
|||||||
|
|
||||||
// ===== 即时生效反馈(阶段5 SettingRow) =====
|
// ===== 即时生效反馈(阶段5 SettingRow) =====
|
||||||
savedHint: '✓ 已保存',
|
savedHint: '✓ 已保存',
|
||||||
|
// 走查 P3-11 假成功族:持久化/IPC 失败的红字反馈位
|
||||||
|
saveFailedHint: '✗ 保存失败',
|
||||||
nextRoundBadge: '下次对话生效',
|
nextRoundBadge: '下次对话生效',
|
||||||
|
// 走查 P3-14 越界钳制提示(并发 1-64 + 单对话不超全局;采样温度单独提示修正值)
|
||||||
|
toastConcurrencyClamped: '输入超出有效范围,已自动修正(并发 1-{max},单对话不超过全局)',
|
||||||
|
toastTempClamped: '采样温度须在 0-2 范围内,已修正为 {v}',
|
||||||
|
// 走查 P3-11:Prompt 缓存开关保存反馈(原切换零反馈)
|
||||||
|
toastAnthropicCacheSaved: 'Prompt 缓存设置已保存',
|
||||||
|
toastAnthropicCacheFail: 'Prompt 缓存设置保存失败',
|
||||||
|
|
||||||
// ===== 连接管理(阶段7:实验功能 badge 明示暂未接入 AI) =====
|
// ===== 连接管理(阶段7:实验功能 badge 明示暂未接入 AI) =====
|
||||||
experimentalBadge: '实验功能·暂未接入 AI 工具',
|
experimentalBadge: '实验功能·暂未接入 AI 工具',
|
||||||
|
|||||||
@@ -91,5 +91,7 @@ export default {
|
|||||||
descSaveFailed: '保存描述失败: {msg}',
|
descSaveFailed: '保存描述失败: {msg}',
|
||||||
saveDesc: '保存',
|
saveDesc: '保存',
|
||||||
cancelEdit: '取消',
|
cancelEdit: '取消',
|
||||||
|
// S4 关联会话(审计表反查发起过任务工具的主会话,点击跳转)
|
||||||
|
relatedConversations: '关联会话',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-3
@@ -44,7 +44,7 @@
|
|||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:checked="anthropicCache"
|
:checked="anthropicCache"
|
||||||
@change="anthropicCache = ($event.target as HTMLInputElement).checked"
|
@change="onAnthropicCacheChange(($event.target as HTMLInputElement).checked)"
|
||||||
/>
|
/>
|
||||||
<span class="toggle-slider"></span>
|
<span class="toggle-slider"></span>
|
||||||
</label>
|
</label>
|
||||||
@@ -57,8 +57,11 @@
|
|||||||
:ai-providers="aiProviders"
|
:ai-providers="aiProviders"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 性能:global/per-conv concurrency/max-iterations/max-retries -->
|
<!-- 性能:global/per-conv concurrency/max-iterations/max-retries(@toast 接走查 P3-14 越界修正提示) -->
|
||||||
<PerformanceSection v-else-if="activeCategory === 'performance'" />
|
<PerformanceSection
|
||||||
|
v-else-if="activeCategory === 'performance'"
|
||||||
|
@toast="showToast"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- 安全:allowed_dirs -->
|
<!-- 安全:allowed_dirs -->
|
||||||
<AllowedDirsPanel
|
<AllowedDirsPanel
|
||||||
@@ -153,6 +156,20 @@ onBeforeRouteLeave(async () => {
|
|||||||
// 值落库为 JSON 布尔,后端 "true"/"1" 判定兼容)
|
// 值落库为 JSON 布尔,后端 "true"/"1" 判定兼容)
|
||||||
const anthropicCache = appSettings.useSetting<boolean>('df-ai-anthropic-cache', false)
|
const anthropicCache = appSettings.useSetting<boolean>('df-ai-anthropic-cache', false)
|
||||||
|
|
||||||
|
// 走查 P3-11:原开关切换零反馈,用户无从得知是否生效。改为 set 完成后 toast 反馈成败。
|
||||||
|
// 注:appSettings.set 当前落库失败仅 console 内吞(降级内存态),catch 是 store 上抛错误后
|
||||||
|
// 的失败通道,今日实际不触发(登记项见批次报告),成功反馈先补齐。
|
||||||
|
async function onAnthropicCacheChange(checked: boolean) {
|
||||||
|
anthropicCache.value = checked
|
||||||
|
try {
|
||||||
|
await appSettings.set('df-ai-anthropic-cache', checked)
|
||||||
|
showToast(ti18n('settings.toastAnthropicCacheSaved'), 'success')
|
||||||
|
} catch (e) {
|
||||||
|
console.error('保存 Prompt 缓存开关失败:', e)
|
||||||
|
showToast(ti18n('settings.toastAnthropicCacheFail'), 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 窄屏响应式:监听窗口宽度,nav 折叠为横向 tab(避免依赖仅 CSS 的 :has,
|
// 窄屏响应式:监听窗口宽度,nav 折叠为横向 tab(避免依赖仅 CSS 的 :has,
|
||||||
// 并让 SettingsNav 按横向模式渲染 list)
|
// 并让 SettingsNav 按横向模式渲染 list)
|
||||||
const isNarrowScreen = ref(false)
|
const isNarrowScreen = ref(false)
|
||||||
|
|||||||
Reference in New Issue
Block a user