fix(models): stop auto-filling fallback chain and add clear-all button

The fallback editor's "Add" buttons appeared to do nothing because
applyDefaultModel auto-populated defaults.model.fallbacks with every
non-primary model whenever the list was empty (and the same for
defaults.models). After any save with an empty chain, the chain got
filled with all 17+ candidates; the candidate pool became "No candidate
models available" and any subsequent Add click hit the early return
`if (modelConfig.fallbacks.includes(full)) return`.

Even worse, the auto-fill made it impossible for users to keep an empty
fallback chain on purpose: deleting all chips would silently get
replaced with every model on the next debounced autosave.

Remove the auto-fill in applyDefaultModel. An empty fallback chain is a
valid configuration that means "no implicit fallback"; Gateway already
surfaces a clear primary-model error in that case. normalizeDefaultModel
Selection still cleans up invalid/duplicate entries.

Also add a small "Clear All" button next to the active chain title so
users can drop the entire fallback list in one click instead of removing
chips one by one (especially useful for the existing bloated 17-fallback
state created by the old auto-fill path).

## Verification
- node --check src/pages/models.js
- node --check src/locales/modules/models.js
- npm run build
- Playwright repro: open /#/models → Clear All → fallbacks on disk
  go from 17 → 0 → click Add on one candidate → fallbacks on disk are
  exactly that one entry, no longer auto-expanded back to 17.
This commit is contained in:
晴天
2026-05-16 12:07:35 +08:00
parent 207b1c7c55
commit a13c9ee6ba
2 changed files with 29 additions and 14 deletions

View File

@@ -191,4 +191,7 @@ export default {
setAsPrimary: _('设为主用', 'Set as Primary', '設為主用'),
remove: _('移除', 'Remove', '移除'),
add: _('加入', 'Add', '加入'),
addAll: _('全部加入', 'Add All', '全部加入'),
clearAll: _('清空全部', 'Clear All', '清空全部'),
confirmClearAll: _('确定清空所有备选模型?主模型不会被影响。', 'Clear all fallback models? Primary model is not affected.', '確定清空所有備選模型?主模型不會被影響。'),
}

View File

@@ -301,7 +301,10 @@ function renderFallbackWaterfall(state) {
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 16px;">
<div style="background: var(--bg-tertiary); padding: 12px; border-radius: var(--radius-md); border: 1px solid var(--border-color);">
<div style="font-size: var(--font-size-xs); font-weight: bold; margin-bottom: 8px; color: var(--text-tertiary);">${t('models.activeChainTitle')}</div>
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom: 8px;">
<div style="font-size: var(--font-size-xs); font-weight: bold; color: var(--text-tertiary);">${t('models.activeChainTitle')}</div>
${currentFallbacks.length > 0 ? `<button class="btn btn-xs btn-secondary btn-clear-all-fb" style="padding:1px 6px; font-size:10px;">${t('models.clearAll')}</button>` : ''}
</div>
<div id="active-fallback-list" style="display: flex; flex-direction: column; gap: 4px; min-height: 50px;">
${currentFallbacks.map((f, i) => `
<div class="fallback-chain-item" data-id="${f}" style="display: flex; align-items: center; justify-content: space-between; background: var(--bg-primary); padding: 6px 10px; border-radius: 4px; border: 1px solid var(--border-color);">
@@ -398,6 +401,22 @@ function bindWaterfallActions(page, state) {
}
})
// 清空全部备选
const clearAllBtn = container.querySelector('.btn-clear-all-fb')
if (clearAllBtn) {
clearAllBtn.onclick = async () => {
const yes = await showConfirm(t('models.confirmClearAll'))
if (!yes) return
const modelConfig = ensureDefaultModelConfig(state)
if (!modelConfig.fallbacks.length) return
pushUndo(state)
modelConfig.fallbacks = []
renderDefaultBar(page, state)
updateUndoBtn(page, state)
autoSave(state)
}
}
// 折叠候选服务商
container.querySelectorAll('.candidate-provider-header').forEach(header => {
header.onclick = () => {
@@ -1131,20 +1150,13 @@ function applyDefaultModel(state) {
const defaults = state.config.agents.defaults
if (!defaults.model) defaults.model = {}
defaults.model.primary = primary
if (!Array.isArray(defaults.model.fallbacks)) defaults.model.fallbacks = []
if (!defaults.models || typeof defaults.models !== 'object' || Array.isArray(defaults.models)) defaults.models = {}
// fallbacks / models 仅在为空时初始化(首次安装友好),不再每次保存都覆盖
// 避免用户精心维护的精简 fallback 链被重写,且随模型增多不断膨胀 (fixes #190)
if (!defaults.model.fallbacks || defaults.model.fallbacks.length === 0) {
const allModels = collectAllModels(state.config)
defaults.model.fallbacks = allModels.filter(m => m.full !== primary).map(m => m.full)
}
if (!defaults.models || Object.keys(defaults.models).length === 0) {
const allModels = collectAllModels(state.config)
const modelsMap = {}
modelsMap[primary] = {}
for (const m of allModels) { if (m.full !== primary) modelsMap[m.full] = {} }
defaults.models = modelsMap
}
// 注意:不再在 fallbacks/models 为空时自动塞入"全部可用模型"。
// 旧逻辑会导致用户清空备选链或新增主模型后,一次保存就把所有候选自动加进 fallbacks/models,
// 用户点"加入"时模型已经全部在备选链里 → 候选池空 → "加入"按钮显示"无可用候选模型",
// 看起来就像"加入按钮没效果"。空备选链是合法状态:此时 Gateway 主模型失败直接报错,不做隐式 fallback。
normalizeDefaultModelSelection(state.config)
// 注意:不再强制同步到各 agent 的 model.primary