From 3f90aad2d662e516d7d2cd9b37eb06fee1798282 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BB=9D=E5=B0=98?= <237809796@qq.com> Date: Wed, 17 Jun 2026 02:36:54 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E:=20F-04c=E5=89=8D=E7=AB=AFSe?= =?UTF-8?q?ttings=E6=B1=A0=E7=BC=96=E8=BE=91(enabled=20toggle+weight?= =?UTF-8?q?=E8=BE=93=E5=85=A5+IPC=E7=BB=91=E5=AE=9A)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/ai.ts | 9 ++++ src/api/types.ts | 4 ++ src/i18n/en/settings.ts | 7 +++ src/i18n/zh-CN/settings.ts | 7 +++ src/views/Settings.vue | 91 +++++++++++++++++++++++++++++++++++++- 5 files changed, 117 insertions(+), 1 deletion(-) diff --git a/src/api/ai.ts b/src/api/ai.ts index 208863d..ce8f4a0 100644 --- a/src/api/ai.ts +++ b/src/api/ai.ts @@ -138,6 +138,15 @@ export const aiApi = { return invoke('ai_set_provider', { providerId }) }, + /** + * 更新 provider 负载均衡池配置(F-260614-04c):仅 UPDATE enabled/weight + 重建 per_provider caps。 + * 走轻量分支,不经 ai_save_provider 全量 INSERT OR REPLACE(避免空 api_key 触发 R-PD-1 密钥迁移)。 + * 后端 weight clamp [0,100];落库后立即 reload_provider_caps,下条消息即按新配置 acquire。 + */ + updateProviderPool(providerId: string, enabled: boolean, weight: number): Promise { + return invoke('ai_update_provider_pool', { providerId, enabled, weight }) + }, + /** 删除 AI 提供商 */ deleteProvider(providerId: string): Promise { return invoke('ai_delete_provider', { providerId }) diff --git a/src/api/types.ts b/src/api/types.ts index eb102d3..6ec5f14 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -187,6 +187,10 @@ export interface AiProviderConfig { config: string | null created_at: string updated_at: string + /** provider 是否进入负载均衡池(F-260614-04c)。false=仅配置存在,不参与主链路由/选池。老库默认 true。 */ + enabled?: boolean + /** provider 在负载均衡池中的选择权重(0-100,F-260614-04c)。高权重优先被选为主;同权重近似轮询。老库默认 50。 */ + weight?: number } // ============================================================ diff --git a/src/i18n/en/settings.ts b/src/i18n/en/settings.ts index 8dfd9e6..f335d1b 100644 --- a/src/i18n/en/settings.ts +++ b/src/i18n/en/settings.ts @@ -14,6 +14,13 @@ export default { detailBaseUrl: 'Base URL', detailModel: 'Model', detailApiKey: 'API Key', + detailPoolEnabled: 'In load-balance pool', + detailPoolWeight: 'Pool selection weight', + badgePoolDisabled: 'Disabled', + descPoolEnabled: 'When on, participates in main-chain routing/pool selection; off = config retained but never selected', + descPoolWeight: '0-100, higher weight preferred as primary; equal weights ~ round-robin (backend clamps [0,100])', + toastPoolUpdateOk: 'Load-balance pool config updated', + toastPoolUpdateFail: 'Failed to update pool config: {msg}', // Provider form labelName: 'Name', diff --git a/src/i18n/zh-CN/settings.ts b/src/i18n/zh-CN/settings.ts index 38925bf..438232f 100644 --- a/src/i18n/zh-CN/settings.ts +++ b/src/i18n/zh-CN/settings.ts @@ -14,6 +14,13 @@ export default { detailBaseUrl: 'Base URL', detailModel: '模型', detailApiKey: 'API Key', + detailPoolEnabled: '入负载均衡池', + detailPoolWeight: '池选择权重', + badgePoolDisabled: '已停用', + descPoolEnabled: '开启后参与主链路由/选池;关闭后仅保留配置,不被选中', + descPoolWeight: '0-100,高权重优先被选为主;同权重近似轮询(后端 clamp [0,100])', + toastPoolUpdateOk: '负载均衡池配置已更新', + toastPoolUpdateFail: '更新负载均衡池配置失败:{msg}', // Provider 表单 labelName: '名称', diff --git a/src/views/Settings.vue b/src/views/Settings.vue index 10732de..d7936c6 100644 --- a/src/views/Settings.vue +++ b/src/views/Settings.vue @@ -27,11 +27,12 @@
-
+
{{ p.name }} {{ $t('settings.badgeDefault') }} + {{ $t('settings.badgePoolDisabled') }}
@@ -52,6 +53,25 @@ {{ $t('settings.detailApiKey') }} {{ maskKey(p.api_key) }}
+ +
+ {{ $t('settings.detailPoolEnabled') }} +
+ +
+
+
+ {{ $t('settings.detailPoolWeight') }} +
+ + 0-100 +
+
@@ -591,6 +611,51 @@ async function setDefaultProvider(id: string) { } } +// ============================================================ +// F-260614-04c: 负载均衡池可编辑层(enabled / weight) +// 卡片即时调 IPC(对齐 syncConcurrencyConfig 模式);落库后后端 reload_provider_caps +// 重建 per_provider caps,下条消息即按新配置 acquire。 +// 不走 saveProvider(全量 INSERT OR REPLACE 会触发 R-PD-1 密钥迁移分支,与 F-04c 轻量 UPDATE 路径冲突)。 +// ============================================================ +let _poolTimer: ReturnType | null = null + +/** + * toggle 即时生效(开关语义=立即入/出池,无 debounce 必要;但失败需回滚视图)。 + * 乐观更新本地数组,失败 revert + toast。 + */ +async function onPoolToggle(p: AiProviderConfig, enabled: boolean) { + const prev = p.enabled + p.enabled = enabled + try { + await aiApi.updateProviderPool(p.id, enabled, p.weight ?? 50) + showToast(t('settings.toastPoolUpdateOk'), 'info') + } catch (e) { + p.enabled = prev // revert + console.error(t('settings.toastPoolUpdateFail', { msg: errMsg(e) }), e) + showToast(t('settings.toastPoolUpdateFail', { msg: errMsg(e) }), 'error') + } +} + +/** + * weight 变更 debounce 300ms(防快速连续输入触发频繁 IPC),clamp [0,100] 对齐后端。 + * 双 clamp:前端 clamp 视觉输入 + 后端 command 再 clamp(防御性)。 + */ +function onPoolWeightChange(p: AiProviderConfig, raw: number) { + // NaN 兜底(清空输入):回退 50,防 IPC 传 NaN 后端 serde u32 失败 + const clamped = Math.min(100, Math.max(0, Number.isFinite(raw) ? Math.trunc(raw) : 50)) + p.weight = clamped + if (_poolTimer) clearTimeout(_poolTimer) + _poolTimer = setTimeout(async () => { + try { + await aiApi.updateProviderPool(p.id, p.enabled !== false, clamped) + showToast(t('settings.toastPoolUpdateOk'), 'info') + } catch (e) { + console.error(t('settings.toastPoolUpdateFail', { msg: errMsg(e) }), e) + showToast(t('settings.toastPoolUpdateFail', { msg: errMsg(e) }), 'error') + } + }, 300) +} + function maskKey(key: string): string { if (!key || key.length < 8) return '••••••••' return key.slice(0, 4) + '••••••••' + key.slice(-4) @@ -886,6 +951,7 @@ onUnmounted(() => { if (_agentIterTimer) clearTimeout(_agentIterTimer) if (_agentRetryTimer) clearTimeout(_agentRetryTimer) if (_knowledgeSaveTimer) clearTimeout(_knowledgeSaveTimer) + if (_poolTimer) clearTimeout(_poolTimer) if (_toastTimer) clearTimeout(_toastTimer) }) @@ -1178,4 +1244,27 @@ onUnmounted(() => { transform: translateX(18px); background: #fff; } +/* 小号 toggle(model 行 / 池开关) */ +.toggle--sm { width: 32px; height: 18px; } +.toggle--sm .toggle-slider::before { width: 12px; height: 12px; left: 3px; bottom: 3px; } +.toggle--sm input:checked + .toggle-slider::before { transform: translateX(14px); } + +/* 小号输入框(model 权重 / 池权重) */ +.setting-input--sm { min-width: 0; width: 70px; padding: 4px 8px; font-size: 12px; } + +/* Provider 卡片 disabled 态(F-260614-04c:enabled=false 视觉弱化) */ +.provider-card--disabled { opacity: 0.6; } +.pool-badge { + font-size: 10px; + padding: 1px 6px; + border-radius: var(--df-radius-xs); + background: rgba(255,107,107,0.15); + color: var(--df-danger); + font-weight: 500; +} + +/* 负载均衡池行(toggle / weight 控件) */ +.detail-row--pool { align-items: center; } +.detail-value--control { display: flex; align-items: center; gap: 8px; font-family: var(--df-font-mono); } +.pool-weight-hint { font-size: 11px; color: var(--df-text-dim); }