|
|
|
|
@@ -27,11 +27,12 @@
|
|
|
|
|
<button class="btn btn-ghost btn-sm" @click="openProviderForm()">{{ $t('settings.addProvider') }}</button>
|
|
|
|
|
</div>
|
|
|
|
|
<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-info">
|
|
|
|
|
<span class="provider-name">{{ p.name }}</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 class="provider-actions">
|
|
|
|
|
<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-value mask">{{ maskKey(p.api_key) }}</span>
|
|
|
|
|
</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>
|
|
|
|
|
@@ -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 {
|
|
|
|
|
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)
|
|
|
|
|
})
|
|
|
|
|
</script>
|
|
|
|
|
@@ -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); }
|
|
|
|
|
</style>
|
|
|
|
|
|