mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-29 04:10:00 +08:00
fix(models): normalize invalid primary model
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 设置不同模型)
|
||||
|
||||
Reference in New Issue
Block a user