fix(models): normalize invalid primary model

This commit is contained in:
晴天
2026-05-16 00:16:39 +08:00
parent c318ff3b37
commit 9742786f8c
2 changed files with 159 additions and 31 deletions

View File

@@ -123,6 +123,7 @@ let _dashboardInitialized = false
let _dashboardVersionCache = null
let _dashboardStatusSummaryCache = null
let _dashboardInstanceId = ''
let _dashboardLoadSeq = 0
function syncDashboardInstanceScope() {
const instanceId = getActiveInstance()?.id || 'local'
@@ -138,14 +139,73 @@ function versionInfoIncomplete(version) {
return !version || !version.current || !version.source || version.source === 'unknown'
}
function collectConfigModels(config) {
const result = []
const providers = config?.models?.providers || {}
for (const [providerKey, provider] of Object.entries(providers)) {
for (const model of (provider?.models || [])) {
const id = typeof model === 'string' ? model : model?.id
if (id) result.push(`${providerKey}/${id}`)
}
}
return result
}
function defaultModelNeedsNormalization(config) {
const validModels = new Set(collectConfigModels(config))
const modelConfig = config?.agents?.defaults?.model || {}
const primary = modelConfig.primary || ''
const fallbacks = Array.isArray(modelConfig.fallbacks) ? modelConfig.fallbacks : []
if (!validModels.size) return !!primary || fallbacks.length > 0 || Object.keys(config?.agents?.defaults?.models || {}).length > 0
if (!validModels.has(primary)) return true
if (fallbacks.some(f => f === primary || !validModels.has(f))) return true
return Object.keys(config?.agents?.defaults?.models || {}).some(key => !validModels.has(key))
}
function normalizeDefaultModelConfig(config) {
const allModels = collectConfigModels(config)
const validModels = new Set(allModels)
if (!config.agents) config.agents = {}
if (!config.agents.defaults) config.agents.defaults = {}
if (!config.agents.defaults.model) config.agents.defaults.model = {}
const modelConfig = config.agents.defaults.model
if (!Array.isArray(modelConfig.fallbacks)) modelConfig.fallbacks = []
if (!allModels.length) {
modelConfig.primary = ''
modelConfig.fallbacks = []
config.agents.defaults.models = {}
return ''
}
if (!validModels.has(modelConfig.primary || '')) {
modelConfig.primary = modelConfig.fallbacks.find(f => validModels.has(f)) || allModels[0]
}
const seen = new Set([modelConfig.primary])
modelConfig.fallbacks = modelConfig.fallbacks
.filter(f => validModels.has(f))
.filter(f => {
if (seen.has(f)) return false
seen.add(f)
return true
})
const currentMap = config.agents.defaults.models && typeof config.agents.defaults.models === 'object' && !Array.isArray(config.agents.defaults.models) ? config.agents.defaults.models : {}
const nextMap = {}
nextMap[modelConfig.primary] = currentMap[modelConfig.primary] && typeof currentMap[modelConfig.primary] === 'object' && !Array.isArray(currentMap[modelConfig.primary]) ? currentMap[modelConfig.primary] : {}
for (const fallback of modelConfig.fallbacks) {
nextMap[fallback] = currentMap[fallback] && typeof currentMap[fallback] === 'object' && !Array.isArray(currentMap[fallback]) ? currentMap[fallback] : {}
}
config.agents.defaults.models = nextMap
return modelConfig.primary
}
async function loadDashboardData(page, fullRefresh = false) {
// 并发保护如果上一次加载仍在进行跳过本次fullRefresh 除外)
if (_loadInFlight && !fullRefresh) return
const loadSeq = ++_dashboardLoadSeq
_loadInFlight = true
try { await _loadDashboardDataInner(page, fullRefresh) } finally { _loadInFlight = false }
try { await _loadDashboardDataInner(page, fullRefresh, loadSeq) } finally { if (loadSeq === _dashboardLoadSeq) _loadInFlight = false }
}
async function _loadDashboardDataInner(page, fullRefresh) {
async function _loadDashboardDataInner(page, fullRefresh, loadSeq) {
syncDashboardInstanceScope()
// 分波加载:关键数据先渲染,次要数据后填充,减少白屏等待
// 轻量调用读文件每次都做重量调用spawn CLI/网络请求)只在首次或手动刷新时做
@@ -168,7 +228,7 @@ async function _loadDashboardDataInner(page, fullRefresh) {
const [servicesRes, configRes, panelConfigRes] = await coreP
const services = servicesRes.status === 'fulfilled' ? servicesRes.value : []
let version = _dashboardVersionCache || {}
const config = configRes.status === 'fulfilled' ? configRes.value : null
let config = configRes.status === 'fulfilled' ? configRes.value : null
const panelConfig = panelConfigRes.status === 'fulfilled' ? panelConfigRes.value : null
const gw = services.find(s => s.label === 'ai.openclaw.gateway')
let agents = []
@@ -189,6 +249,7 @@ async function _loadDashboardDataInner(page, fullRefresh) {
if (!config.gateway?.mode) needsPatch = true
if (config.mode) needsPatch = true
if (!config.tools || config.tools.profile !== 'full') needsPatch = true
if (defaultModelNeedsNormalization(config)) needsPatch = true
if (needsPatch) {
try {
const freshConfig = await api.readOpenclawConfig()
@@ -203,11 +264,19 @@ async function _loadDashboardDataInner(page, fullRefresh) {
freshConfig.tools.sessions.visibility = 'all'
patched = true
}
if (patched) api.writeOpenclawConfig(freshConfig).catch(() => {})
if (defaultModelNeedsNormalization(freshConfig)) {
normalizeDefaultModelConfig(freshConfig)
patched = true
}
if (patched) {
config = freshConfig
api.writeOpenclawConfig(freshConfig).catch(() => {})
}
} catch {}
}
}
if (loadSeq !== _dashboardLoadSeq || !page.isConnected) return
renderStatCards(page, services, version, [], config, panelConfig)
renderLogs(page, '')
if (gw) {
@@ -229,7 +298,7 @@ async function _loadDashboardDataInner(page, fullRefresh) {
})
: Promise.resolve(_dashboardVersionCache || {})
versionP.then(v => {
if (!page.isConnected) return
if (loadSeq !== _dashboardLoadSeq || !page.isConnected) return
version = v || {}
renderStatCards(page, services, version, agents, config, panelConfig)
})
@@ -247,6 +316,7 @@ async function _loadDashboardDataInner(page, fullRefresh) {
// 第二波Agent、MCP、备份 → 更新卡片 + 渲染总览
const [agentsRes, mcpRes, backupsRes, channelsRes] = await secondaryP
if (loadSeq !== _dashboardLoadSeq || !page.isConnected) return
agents = agentsRes.status === 'fulfilled' ? agentsRes.value : []
const mcpConfig = mcpRes.status === 'fulfilled' ? mcpRes.value : null
const backups = backupsRes.status === 'fulfilled' ? backupsRes.value : []
@@ -269,6 +339,7 @@ async function _loadDashboardDataInner(page, fullRefresh) {
// 第三波:日志(最低优先级)
const logs = await logsP
if (loadSeq !== _dashboardLoadSeq || !page.isConnected) return
renderLogs(page, logs)
_dashboardInitialized = true

View File

@@ -89,10 +89,13 @@ async function loadConfig(page, state) {
const before = JSON.stringify(state.config?.models?.providers || {})
normalizeProviderUrls(state.config)
const after = JSON.stringify(state.config?.models?.providers || {})
if (before !== after) {
console.log('[models] 自动修复了服务商 baseUrl,正在保存...')
const oldPrimary = getCurrentPrimary(state.config)
const normalizedModel = normalizeDefaultModelSelection(state.config)
if (before !== after || normalizedModel.changed) {
console.log('[models] 自动修复了模型配置,正在保存...')
await api.writeOpenclawConfig(state.config)
toast(t('models.autoFixUrl'), 'info')
if (oldPrimary !== normalizedModel.primary) toast(t('models.primaryAutoSwitch', { model: normalizedModel.primary || t('models.notConfigured') }), 'info')
else if (before !== after) toast(t('models.autoFixUrl'), 'info')
}
renderDefaultBar(page, state)
renderProviders(page, state)
@@ -127,14 +130,18 @@ function getCurrentPrimary(config) {
return config?.agents?.defaults?.model?.primary || ''
}
function ensureDefaultModelConfig(state) {
if (!state.config.agents) state.config.agents = {}
if (!state.config.agents.defaults) state.config.agents.defaults = {}
if (!state.config.agents.defaults.model) state.config.agents.defaults.model = {}
if (!Array.isArray(state.config.agents.defaults.model.fallbacks)) {
state.config.agents.defaults.model.fallbacks = []
function ensureConfigDefaultModelConfig(config) {
if (!config.agents) config.agents = {}
if (!config.agents.defaults) config.agents.defaults = {}
if (!config.agents.defaults.model) config.agents.defaults.model = {}
if (!Array.isArray(config.agents.defaults.model.fallbacks)) {
config.agents.defaults.model.fallbacks = []
}
return state.config.agents.defaults.model
return config.agents.defaults.model
}
function ensureDefaultModelConfig(state) {
return ensureConfigDefaultModelConfig(state.config)
}
function collectAllModels(config) {
@@ -149,6 +156,68 @@ function collectAllModels(config) {
return result
}
function normalizeDefaultModelMap(config, validModels, primary, fallbacks) {
const defaults = config?.agents?.defaults
if (!defaults) return false
const current = defaults.models && typeof defaults.models === 'object' && !Array.isArray(defaults.models) ? defaults.models : {}
const next = {}
if (primary) next[primary] = current[primary] && typeof current[primary] === 'object' && !Array.isArray(current[primary]) ? current[primary] : {}
for (const f of fallbacks || []) {
if (validModels.has(f) && !next[f]) next[f] = current[f] && typeof current[f] === 'object' && !Array.isArray(current[f]) ? current[f] : {}
}
for (const [key, value] of Object.entries(current)) {
if (validModels.has(key) && !next[key]) next[key] = value && typeof value === 'object' && !Array.isArray(value) ? value : {}
}
const changed = JSON.stringify(current) !== JSON.stringify(next)
defaults.models = next
return changed
}
function dedupeValidFallbacks(fallbacks, validModels, primary) {
const seen = new Set()
if (primary) seen.add(primary)
return (Array.isArray(fallbacks) ? fallbacks : [])
.filter(f => validModels.has(f))
.filter(f => {
if (seen.has(f)) return false
seen.add(f)
return true
})
}
function normalizeDefaultModelSelection(config) {
const allModels = collectAllModels(config)
const validModels = new Set(allModels.map(m => m.full))
const modelConfig = ensureConfigDefaultModelConfig(config)
let changed = false
if (!allModels.length) {
if (modelConfig.primary) {
modelConfig.primary = ''
changed = true
}
if (modelConfig.fallbacks.length) {
modelConfig.fallbacks = []
changed = true
}
changed = normalizeDefaultModelMap(config, validModels, '', []) || changed
return { changed, primary: '' }
}
let primary = modelConfig.primary || ''
if (!validModels.has(primary)) {
const fallbackPrimary = modelConfig.fallbacks.find(f => validModels.has(f))
primary = fallbackPrimary || allModels[0].full
modelConfig.primary = primary
changed = true
}
const nextFallbacks = dedupeValidFallbacks(modelConfig.fallbacks, validModels, primary)
if (JSON.stringify(nextFallbacks) !== JSON.stringify(modelConfig.fallbacks)) {
modelConfig.fallbacks = nextFallbacks
changed = true
}
changed = normalizeDefaultModelMap(config, validModels, primary, modelConfig.fallbacks) || changed
return { changed, primary }
}
function getApiTypeLabel(apiType) {
return API_TYPES.find(at => at.value === apiType)?.label || apiType || t('common.unknown')
}
@@ -1050,22 +1119,9 @@ function rotateFallbackChain(state, oldPrimary, newPrimary) {
// 应用默认模型:primary + 其余自动成为备选
// 确保 primary 指向的模型仍然存在,不存在则自动切到第一个可用模型
function ensureValidPrimary(state) {
const primary = getCurrentPrimary(state.config)
const allModels = collectAllModels(state.config)
if (allModels.length === 0) {
// 所有模型都没了,清空 primary
if (state.config.agents?.defaults?.model) {
state.config.agents.defaults.model.primary = ''
}
return
}
const exists = allModels.some(m => m.full === primary)
if (!exists) {
// primary 指向已删除的模型,自动切到第一个
const newPrimary = allModels[0].full
setPrimary(state, newPrimary)
toast(t('models.primaryAutoSwitch', { model: newPrimary }), 'info')
}
const current = getCurrentPrimary(state.config)
const normalized = normalizeDefaultModelSelection(state.config)
if (normalized.changed && current !== normalized.primary) toast(t('models.primaryAutoSwitch', { model: normalized.primary || t('models.notConfigured') }), 'info')
}
function applyDefaultModel(state) {
@@ -1089,6 +1145,7 @@ function applyDefaultModel(state) {
for (const m of allModels) { if (m.full !== primary) modelsMap[m.full] = {} }
defaults.models = modelsMap
}
normalizeDefaultModelSelection(state.config)
// 注意:不再强制同步到各 agent 的 model.primary
// 子 Agent 的模型覆盖是 OpenClaw 正常功能(用户可通过对话为不同 Agent 设置不同模型)