diff --git a/src/server/routes/api/settings.events.test.ts b/src/server/routes/api/settings.events.test.ts index db4fb3c1..7810f0a1 100644 --- a/src/server/routes/api/settings.events.test.ts +++ b/src/server/routes/api/settings.events.test.ts @@ -81,6 +81,8 @@ describe('settings and auth events', () => { (config as any).telegramChatId = ''; (config as any).telegramUseSystemProxy = false; (config as any).telegramMessageThreadId = ''; + config.globalBlockedBrands = []; + config.globalAllowedModels = []; }); afterAll(async () => { @@ -745,6 +747,32 @@ describe('settings and auth events', () => { expect(runtime.proxyEmptyContentFailEnabled).toBe(true); }); + it('persists global model and brand filters as JSON arrays', async () => { + const updateResponse = await app.inject({ + method: 'PUT', + url: '/api/settings/runtime', + payload: { + globalAllowedModels: ['model-alpha', ' model-beta ', 'model-alpha', 'model-gamma'], + globalBlockedBrands: ['Codex', ' Codex ', 'Gemini'], + }, + }); + + expect(updateResponse.statusCode).toBe(200); + const updated = updateResponse.json() as { + globalAllowedModels?: string[]; + globalBlockedBrands?: string[]; + }; + expect(updated.globalAllowedModels).toEqual(['model-alpha', 'model-beta', 'model-gamma']); + expect(updated.globalBlockedBrands).toEqual(['Codex', 'Gemini']); + + const rows = await db.select().from(schema.settings).all(); + const settingsMap = new Map(rows.map((row) => [row.key, row.value])); + expect(settingsMap.get('global_allowed_models')).toBe(JSON.stringify(['model-alpha', 'model-beta', 'model-gamma'])); + expect(JSON.parse(settingsMap.get('global_allowed_models') || 'null')).toEqual(['model-alpha', 'model-beta', 'model-gamma']); + expect(settingsMap.get('global_blocked_brands')).toBe(JSON.stringify(['Codex', 'Gemini'])); + expect(JSON.parse(settingsMap.get('global_blocked_brands') || 'null')).toEqual(['Codex', 'Gemini']); + }); + it('persists and returns log cleanup settings from runtime settings', async () => { const updateResponse = await app.inject({ method: 'PUT', diff --git a/src/server/routes/api/settings.ts b/src/server/routes/api/settings.ts index 41863971..e6273f50 100644 --- a/src/server/routes/api/settings.ts +++ b/src/server/routes/api/settings.ts @@ -1425,7 +1425,7 @@ export async function settingsRoutes(app: FastifyInstance) { changedLabels.push('全局品牌屏蔽'); } config.globalBlockedBrands = uniqueBrands; - upsertSetting('global_blocked_brands', JSON.stringify(uniqueBrands)); + upsertSetting('global_blocked_brands', uniqueBrands); if (prev !== next) { startBackgroundTask( { @@ -1450,7 +1450,7 @@ export async function settingsRoutes(app: FastifyInstance) { changedLabels.push('全局模型白名单'); } config.globalAllowedModels = uniqueModels; - upsertSetting('global_allowed_models', JSON.stringify(uniqueModels)); + upsertSetting('global_allowed_models', uniqueModels); if (prev !== next) { startBackgroundTask( { diff --git a/src/server/runtimeSettingsHydration.test.ts b/src/server/runtimeSettingsHydration.test.ts index f9068309..893b3464 100644 --- a/src/server/runtimeSettingsHydration.test.ts +++ b/src/server/runtimeSettingsHydration.test.ts @@ -44,4 +44,14 @@ describe('applyRuntimeSettings', () => { expect(config.smtpPort).toBe(587); }); + + it('hydrates legacy double-encoded global model allowlist values', () => { + config.globalAllowedModels = []; + + applyRuntimeSettings(new Map([ + ['global_allowed_models', JSON.stringify(JSON.stringify(['model-alpha', ' model-beta ', 'model-gamma']))], + ])); + + expect(config.globalAllowedModels).toEqual(['model-alpha', 'model-beta', 'model-gamma']); + }); }); diff --git a/src/server/runtimeSettingsHydration.ts b/src/server/runtimeSettingsHydration.ts index 25a7060d..1d60a47a 100644 --- a/src/server/runtimeSettingsHydration.ts +++ b/src/server/runtimeSettingsHydration.ts @@ -22,6 +22,14 @@ function toStringList(value: unknown): string[] { .filter((item) => item.length > 0); } if (typeof value === 'string') { + try { + const parsed = JSON.parse(value); + if (Array.isArray(parsed)) { + return toStringList(parsed); + } + } catch { + // Fall back to comma splitting for legacy plain-string lists. + } return value .split(',') .map((item) => item.trim())