fix(settings): reconcile secret status and model drafts

This commit is contained in:
caoxiaozhu
2026-07-20 10:29:49 +08:00
parent 044a5669fe
commit 15da295963
9 changed files with 377 additions and 23 deletions

View File

@@ -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)

View File

@@ -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 },

View 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)
})