fix(settings): reconcile secret status and model drafts
This commit is contained in:
@@ -13,11 +13,13 @@ import {
|
||||
PROVIDER_OPTIONS,
|
||||
SECTION_DEFINITIONS,
|
||||
SESSION_RETENTION_OPTIONS,
|
||||
applyAuthoritativeSecretStatus,
|
||||
buildDefaultState,
|
||||
buildLlmPayload,
|
||||
buildRenderPayload,
|
||||
computeSectionStatus,
|
||||
isModelConfigReady,
|
||||
isModelSecretMask,
|
||||
isRenderSecretMask,
|
||||
maskConfiguredModelSecrets,
|
||||
maskConfiguredRenderSecret,
|
||||
@@ -106,13 +108,19 @@ export function useSettings() {
|
||||
nextState = mergeState(nextState, readStoredSettings())
|
||||
}
|
||||
|
||||
nextState = applyAuthoritativeSecretStatus(nextState, snapshot)
|
||||
|
||||
if (preserveModelApiKeys) {
|
||||
nextState.llmForm.mainApiKey = currentState.llmForm.mainApiKey
|
||||
nextState.llmForm.backupApiKey = currentState.llmForm.backupApiKey
|
||||
nextState.llmForm.embeddingApiKey = currentState.llmForm.embeddingApiKey
|
||||
nextState.llmForm.rerankerApiKey = currentState.llmForm.rerankerApiKey
|
||||
const preserveApiKeyInput = (value) => isModelSecretMask(value) ? '' : value
|
||||
nextState.llmForm.mainApiKey = preserveApiKeyInput(currentState.llmForm.mainApiKey)
|
||||
nextState.llmForm.backupApiKey = preserveApiKeyInput(currentState.llmForm.backupApiKey)
|
||||
nextState.llmForm.embeddingApiKey = preserveApiKeyInput(currentState.llmForm.embeddingApiKey)
|
||||
nextState.llmForm.rerankerApiKey = preserveApiKeyInput(currentState.llmForm.rerankerApiKey)
|
||||
const modelApiKeysBySlot = new Map(
|
||||
normalizeLlmModelRows(currentState.llmForm.models).map((row) => [row.slot, row.apiKey])
|
||||
normalizeLlmModelRows(currentState.llmForm.models).map((row) => [
|
||||
row.slot,
|
||||
preserveApiKeyInput(row.apiKey)
|
||||
])
|
||||
)
|
||||
nextState.llmForm.models = normalizeLlmModelRows(nextState.llmForm.models).map((row) => ({
|
||||
...row,
|
||||
@@ -148,7 +156,10 @@ export function useSettings() {
|
||||
async function loadSettingsSnapshot() {
|
||||
try {
|
||||
const snapshot = await fetchSettings()
|
||||
applyLoadedSnapshot(snapshot, { mergeDraft: true })
|
||||
applyLoadedSnapshot(snapshot, {
|
||||
mergeDraft: true,
|
||||
preserveModelApiKeys: true
|
||||
})
|
||||
} catch (error) {
|
||||
persistSettings(pageState.value)
|
||||
updateBrandPreviewFromState(pageState.value)
|
||||
|
||||
@@ -483,6 +483,63 @@ export function mergeState(baseState, overrideState) {
|
||||
}
|
||||
}
|
||||
|
||||
export function applyAuthoritativeSecretStatus(state, snapshot) {
|
||||
const stateModelRows = normalizeLlmModelRows(state.llmForm?.models)
|
||||
const localApiKeysBySlot = new Map(
|
||||
stateModelRows.map((row) => [
|
||||
row.slot,
|
||||
isModelSecretMask(row.apiKey) ? '' : row.apiKey
|
||||
])
|
||||
)
|
||||
const nextState = {
|
||||
...state,
|
||||
adminForm: { ...(state.adminForm || {}) },
|
||||
llmForm: {
|
||||
...(state.llmForm || {}),
|
||||
models: stateModelRows
|
||||
},
|
||||
renderForm: { ...(state.renderForm || {}) },
|
||||
mailForm: { ...(state.mailForm || {}) }
|
||||
}
|
||||
const snapshotLlmForm = snapshot?.llmForm || {}
|
||||
const configuredKeyNames = [
|
||||
'mainApiKeyConfigured',
|
||||
'backupApiKeyConfigured',
|
||||
'embeddingApiKeyConfigured',
|
||||
'rerankerApiKeyConfigured'
|
||||
]
|
||||
|
||||
for (const key of configuredKeyNames) {
|
||||
if (Object.hasOwn(snapshotLlmForm, key)) {
|
||||
nextState.llmForm[key] = Boolean(snapshotLlmForm[key])
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(snapshotLlmForm.models)) {
|
||||
const authoritativeModels = normalizeLlmModelRows(snapshotLlmForm.models).map((row) => ({
|
||||
...row,
|
||||
apiKey: localApiKeysBySlot.get(row.slot) || row.apiKey,
|
||||
apiKeyConfigured: Boolean(row.apiKeyConfigured)
|
||||
}))
|
||||
nextState.llmForm = syncLegacyModelFieldsFromRows({
|
||||
...nextState.llmForm,
|
||||
models: authoritativeModels
|
||||
})
|
||||
}
|
||||
|
||||
for (const [formKey, statusKey] of [
|
||||
['adminForm', 'adminPasswordConfigured'],
|
||||
['renderForm', 'jwtSecretConfigured'],
|
||||
['mailForm', 'passwordConfigured']
|
||||
]) {
|
||||
if (Object.hasOwn(snapshot?.[formKey] || {}, statusKey)) {
|
||||
nextState[formKey][statusKey] = Boolean(snapshot[formKey][statusKey])
|
||||
}
|
||||
}
|
||||
|
||||
return nextState
|
||||
}
|
||||
|
||||
export function sanitizeForStorage(state) {
|
||||
return {
|
||||
companyForm: { ...state.companyForm },
|
||||
|
||||
99
web/tests/settings-secret-status.test.mjs
Normal file
99
web/tests/settings-secret-status.test.mjs
Normal file
@@ -0,0 +1,99 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import {
|
||||
applyAuthoritativeSecretStatus
|
||||
} from '../src/utils/settingsModelHelper.js'
|
||||
|
||||
test('server secret status overrides stale configured flags from the session draft', () => {
|
||||
const modelRows = [
|
||||
{
|
||||
slot: 'main',
|
||||
provider: 'Ali',
|
||||
url: 'https://example.com',
|
||||
apiKey: 'unsaved-main-key',
|
||||
modelId: 'main',
|
||||
type: 'llm'
|
||||
},
|
||||
{ slot: 'backup', provider: 'GLM', url: 'https://example.com', modelId: 'backup', type: 'llm' },
|
||||
{ slot: 'embedding', provider: 'GLM', url: 'https://example.com', modelId: 'embedding', type: 'embedding' },
|
||||
{ slot: 'reranker', provider: 'Ali', url: 'https://example.com', modelId: 'reranker', type: 'rerank' }
|
||||
]
|
||||
const staleDraft = {
|
||||
companyForm: {},
|
||||
appearanceForm: {},
|
||||
adminForm: { adminPasswordConfigured: true },
|
||||
sessionForm: {},
|
||||
hermesForm: {},
|
||||
llmForm: {
|
||||
embeddingApiKeyConfigured: true,
|
||||
rerankerApiKeyConfigured: true,
|
||||
models: [
|
||||
...modelRows.map((model) => ({
|
||||
...model,
|
||||
apiKeyConfigured: true
|
||||
})),
|
||||
{
|
||||
slot: 'server_deleted',
|
||||
provider: 'Ali',
|
||||
url: 'https://stale.example.com',
|
||||
modelId: 'stale',
|
||||
type: 'llm',
|
||||
apiKeyConfigured: true
|
||||
}
|
||||
]
|
||||
},
|
||||
renderForm: { jwtSecretConfigured: true },
|
||||
logForm: {},
|
||||
mailForm: { passwordConfigured: true }
|
||||
}
|
||||
const serverSnapshot = {
|
||||
adminForm: { adminPasswordConfigured: true },
|
||||
llmForm: {
|
||||
mainApiKeyConfigured: true,
|
||||
backupApiKeyConfigured: true,
|
||||
embeddingApiKeyConfigured: false,
|
||||
rerankerApiKeyConfigured: false,
|
||||
models: [
|
||||
...modelRows.map((model) => ({
|
||||
...model,
|
||||
apiKey: '',
|
||||
apiKeyConfigured: ['main', 'backup'].includes(model.slot)
|
||||
})),
|
||||
{
|
||||
slot: 'server_only',
|
||||
provider: 'GLM',
|
||||
url: 'https://server.example.com',
|
||||
apiKey: '',
|
||||
modelId: 'server-model',
|
||||
type: 'llm',
|
||||
apiKeyConfigured: true
|
||||
}
|
||||
]
|
||||
},
|
||||
renderForm: { jwtSecretConfigured: false },
|
||||
mailForm: { passwordConfigured: false }
|
||||
}
|
||||
|
||||
const resolved = applyAuthoritativeSecretStatus(staleDraft, serverSnapshot)
|
||||
const configuredBySlot = new Map(
|
||||
resolved.llmForm.models.map((model) => [model.slot, model.apiKeyConfigured])
|
||||
)
|
||||
|
||||
assert.equal(resolved.llmForm.mainApiKeyConfigured, true)
|
||||
assert.equal(resolved.llmForm.embeddingApiKeyConfigured, false)
|
||||
assert.equal(resolved.llmForm.rerankerApiKeyConfigured, false)
|
||||
assert.equal(configuredBySlot.get('main'), true)
|
||||
assert.equal(configuredBySlot.get('embedding'), false)
|
||||
assert.equal(configuredBySlot.get('reranker'), false)
|
||||
assert.equal(configuredBySlot.get('server_only'), true)
|
||||
assert.equal(configuredBySlot.has('server_deleted'), false)
|
||||
assert.equal(
|
||||
resolved.llmForm.models.find((model) => model.slot === 'main')?.apiKey,
|
||||
'unsaved-main-key'
|
||||
)
|
||||
assert.equal(resolved.llmForm.mainApiKey, 'unsaved-main-key')
|
||||
assert.equal(resolved.adminForm.adminPasswordConfigured, true)
|
||||
assert.equal(resolved.renderForm.jwtSecretConfigured, false)
|
||||
assert.equal(resolved.mailForm.passwordConfigured, false)
|
||||
})
|
||||
Reference in New Issue
Block a user