mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-06-25 17:54:10 +08:00
fix(dashboard): preserve agents.defaults.models entries during model self-heal
Dashboard normalization rebuilt defaults.models from only primary+fallbacks, dropping valid per-model overrides for other configured models (unlike the models page normalizeDefaultModelMap). Extract shared pure helpers and merge orphan valid keys after rebuilding the chain. Co-authored-by: 晴天 <1186258278@users.noreply.github.com>
This commit is contained in:
74
src/lib/agent-default-model-normalize.js
Normal file
74
src/lib/agent-default-model-normalize.js
Normal file
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Pure helpers for repairing agents.defaults.model + agents.defaults.models
|
||||
* when loading the dashboard. Kept in a standalone module so unit tests can
|
||||
* cover normalization without pulling in the full dashboard page.
|
||||
*/
|
||||
|
||||
export 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
|
||||
}
|
||||
|
||||
export 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))
|
||||
}
|
||||
|
||||
/**
|
||||
* Align primary / fallbacks with configured providers and rebuild defaults.models.
|
||||
* Must preserve per-model blocks for any still-valid model id not on the chain
|
||||
* (same rule as models.js normalizeDefaultModelMap); otherwise dashboard
|
||||
* self-heal drops unrelated overrides when it only meant to fix a bad primary.
|
||||
*/
|
||||
export 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] : {}
|
||||
}
|
||||
for (const [key, value] of Object.entries(currentMap)) {
|
||||
if (validModels.has(key) && !nextMap[key]) {
|
||||
nextMap[key] = value && typeof value === 'object' && !Array.isArray(value) ? value : {}
|
||||
}
|
||||
}
|
||||
config.agents.defaults.models = nextMap
|
||||
return modelConfig.primary
|
||||
}
|
||||
@@ -11,6 +11,10 @@ import { t } from '../lib/i18n.js'
|
||||
import { wsClient } from '../lib/ws-client.js'
|
||||
import { attachCliConflictBanner } from '../components/cli-conflict-banner.js'
|
||||
import { icon } from '../lib/icons.js'
|
||||
import {
|
||||
defaultModelNeedsNormalization,
|
||||
normalizeDefaultModelConfig,
|
||||
} from '../lib/agent-default-model-normalize.js'
|
||||
|
||||
let _unsubGw = null
|
||||
let _loadInFlight = false
|
||||
@@ -139,64 +143,6 @@ 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
|
||||
|
||||
56
tests/agent-default-model-normalize.test.js
Normal file
56
tests/agent-default-model-normalize.test.js
Normal file
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* 运行:node --test tests/agent-default-model-normalize.test.js
|
||||
*/
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
import { normalizeDefaultModelConfig } from '../src/lib/agent-default-model-normalize.js'
|
||||
|
||||
test('normalizeDefaultModelConfig keeps valid per-model blocks off the fallback chain', () => {
|
||||
const config = {
|
||||
models: {
|
||||
providers: {
|
||||
openai: { models: [{ id: 'gpt-4' }, { id: 'gpt-4o-mini' }] },
|
||||
anthropic: { models: [{ id: 'claude-3-5-sonnet' }] },
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
defaults: {
|
||||
model: {
|
||||
primary: 'openai/deleted-model',
|
||||
fallbacks: [],
|
||||
},
|
||||
models: {
|
||||
'openai/deleted-model': { temperature: 0.1 },
|
||||
'anthropic/claude-3-5-sonnet': { temperature: 0.7 },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
normalizeDefaultModelConfig(config)
|
||||
assert.equal(config.agents.defaults.model.primary, 'openai/gpt-4')
|
||||
assert.deepEqual(config.agents.defaults.models['anthropic/claude-3-5-sonnet'], { temperature: 0.7 })
|
||||
assert.equal(config.agents.defaults.models['openai/deleted-model'], undefined)
|
||||
})
|
||||
|
||||
test('normalizeDefaultModelConfig still strips invalid model keys', () => {
|
||||
const config = {
|
||||
models: {
|
||||
providers: {
|
||||
openai: { models: [{ id: 'gpt-4' }] },
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: 'openai/gpt-4', fallbacks: [] },
|
||||
models: {
|
||||
'openai/gpt-4': {},
|
||||
'ghost/missing': { temperature: 1 },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
normalizeDefaultModelConfig(config)
|
||||
assert.equal(config.agents.defaults.models['ghost/missing'], undefined)
|
||||
assert.ok(config.agents.defaults.models['openai/gpt-4'])
|
||||
})
|
||||
Reference in New Issue
Block a user