新增: F-04c前端Settings池编辑(enabled toggle+weight输入+IPC绑定)
This commit is contained in:
@@ -138,6 +138,15 @@ export const aiApi = {
|
|||||||
return invoke('ai_set_provider', { providerId })
|
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<void> {
|
||||||
|
return invoke('ai_update_provider_pool', { providerId, enabled, weight })
|
||||||
|
},
|
||||||
|
|
||||||
/** 删除 AI 提供商 */
|
/** 删除 AI 提供商 */
|
||||||
deleteProvider(providerId: string): Promise<void> {
|
deleteProvider(providerId: string): Promise<void> {
|
||||||
return invoke('ai_delete_provider', { providerId })
|
return invoke('ai_delete_provider', { providerId })
|
||||||
|
|||||||
@@ -187,6 +187,10 @@ export interface AiProviderConfig {
|
|||||||
config: string | null
|
config: string | null
|
||||||
created_at: string
|
created_at: string
|
||||||
updated_at: string
|
updated_at: string
|
||||||
|
/** provider 是否进入负载均衡池(F-260614-04c)。false=仅配置存在,不参与主链路由/选池。老库默认 true。 */
|
||||||
|
enabled?: boolean
|
||||||
|
/** provider 在负载均衡池中的选择权重(0-100,F-260614-04c)。高权重优先被选为主;同权重近似轮询。老库默认 50。 */
|
||||||
|
weight?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|||||||
@@ -14,6 +14,13 @@ export default {
|
|||||||
detailBaseUrl: 'Base URL',
|
detailBaseUrl: 'Base URL',
|
||||||
detailModel: 'Model',
|
detailModel: 'Model',
|
||||||
detailApiKey: 'API Key',
|
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
|
// Provider form
|
||||||
labelName: 'Name',
|
labelName: 'Name',
|
||||||
|
|||||||
@@ -14,6 +14,13 @@ export default {
|
|||||||
detailBaseUrl: 'Base URL',
|
detailBaseUrl: 'Base URL',
|
||||||
detailModel: '模型',
|
detailModel: '模型',
|
||||||
detailApiKey: 'API Key',
|
detailApiKey: 'API Key',
|
||||||
|
detailPoolEnabled: '入负载均衡池',
|
||||||
|
detailPoolWeight: '池选择权重',
|
||||||
|
badgePoolDisabled: '已停用',
|
||||||
|
descPoolEnabled: '开启后参与主链路由/选池;关闭后仅保留配置,不被选中',
|
||||||
|
descPoolWeight: '0-100,高权重优先被选为主;同权重近似轮询(后端 clamp [0,100])',
|
||||||
|
toastPoolUpdateOk: '负载均衡池配置已更新',
|
||||||
|
toastPoolUpdateFail: '更新负载均衡池配置失败:{msg}',
|
||||||
|
|
||||||
// Provider 表单
|
// Provider 表单
|
||||||
labelName: '名称',
|
labelName: '名称',
|
||||||
|
|||||||
@@ -27,11 +27,12 @@
|
|||||||
<button class="btn btn-ghost btn-sm" @click="openProviderForm()">{{ $t('settings.addProvider') }}</button>
|
<button class="btn btn-ghost btn-sm" @click="openProviderForm()">{{ $t('settings.addProvider') }}</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="provider-list" v-if="aiProviders.length > 0">
|
<div class="provider-list" v-if="aiProviders.length > 0">
|
||||||
<div class="provider-card" :class="{ 'provider-card--default': p.is_default }" v-for="p in aiProviders" :key="p.id">
|
<div class="provider-card" :class="{ 'provider-card--default': p.is_default, 'provider-card--disabled': p.enabled === false }" v-for="p in aiProviders" :key="p.id">
|
||||||
<div class="provider-top">
|
<div class="provider-top">
|
||||||
<div class="provider-info">
|
<div class="provider-info">
|
||||||
<span class="provider-name">{{ p.name }}</span>
|
<span class="provider-name">{{ p.name }}</span>
|
||||||
<span v-if="p.is_default" class="default-badge">{{ $t('settings.badgeDefault') }}</span>
|
<span v-if="p.is_default" class="default-badge">{{ $t('settings.badgeDefault') }}</span>
|
||||||
|
<span v-if="p.enabled === false" class="pool-badge">{{ $t('settings.badgePoolDisabled') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="provider-actions">
|
<div class="provider-actions">
|
||||||
<button class="btn-link" @click="setDefaultProvider(p.id)" v-if="!p.is_default">{{ $t('settings.actionSetDefault') }}</button>
|
<button class="btn-link" @click="setDefaultProvider(p.id)" v-if="!p.is_default">{{ $t('settings.actionSetDefault') }}</button>
|
||||||
@@ -52,6 +53,25 @@
|
|||||||
<span class="detail-label">{{ $t('settings.detailApiKey') }}</span>
|
<span class="detail-label">{{ $t('settings.detailApiKey') }}</span>
|
||||||
<span class="detail-value mask">{{ maskKey(p.api_key) }}</span>
|
<span class="detail-value mask">{{ maskKey(p.api_key) }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- F-260614-04c 负载均衡池:enabled toggle + weight 即时调 IPC,落库即重建 caps -->
|
||||||
|
<div class="detail-row detail-row--pool">
|
||||||
|
<span class="detail-label" :title="$t('settings.descPoolEnabled')">{{ $t('settings.detailPoolEnabled') }}</span>
|
||||||
|
<div class="detail-value detail-value--control">
|
||||||
|
<label class="toggle toggle--sm" :title="$t('settings.descPoolEnabled')">
|
||||||
|
<input type="checkbox" :checked="p.enabled !== false" @change="onPoolToggle(p, ($event.target as HTMLInputElement).checked)" />
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row detail-row--pool" v-if="p.enabled !== false">
|
||||||
|
<span class="detail-label" :title="$t('settings.descPoolWeight')">{{ $t('settings.detailPoolWeight') }}</span>
|
||||||
|
<div class="detail-value detail-value--control">
|
||||||
|
<input type="number" class="setting-input setting-input--sm" min="0" max="100"
|
||||||
|
:value="p.weight ?? 50"
|
||||||
|
@change="onPoolWeightChange(p, ($event.target as HTMLInputElement).valueAsNumber)" />
|
||||||
|
<span class="pool-weight-hint">0-100</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -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<typeof setTimeout> | 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 {
|
function maskKey(key: string): string {
|
||||||
if (!key || key.length < 8) return '••••••••'
|
if (!key || key.length < 8) return '••••••••'
|
||||||
return key.slice(0, 4) + '••••••••' + key.slice(-4)
|
return key.slice(0, 4) + '••••••••' + key.slice(-4)
|
||||||
@@ -886,6 +951,7 @@ onUnmounted(() => {
|
|||||||
if (_agentIterTimer) clearTimeout(_agentIterTimer)
|
if (_agentIterTimer) clearTimeout(_agentIterTimer)
|
||||||
if (_agentRetryTimer) clearTimeout(_agentRetryTimer)
|
if (_agentRetryTimer) clearTimeout(_agentRetryTimer)
|
||||||
if (_knowledgeSaveTimer) clearTimeout(_knowledgeSaveTimer)
|
if (_knowledgeSaveTimer) clearTimeout(_knowledgeSaveTimer)
|
||||||
|
if (_poolTimer) clearTimeout(_poolTimer)
|
||||||
if (_toastTimer) clearTimeout(_toastTimer)
|
if (_toastTimer) clearTimeout(_toastTimer)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@@ -1178,4 +1244,27 @@ onUnmounted(() => {
|
|||||||
transform: translateX(18px);
|
transform: translateX(18px);
|
||||||
background: #fff;
|
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); }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user