import type { AppSettings, ApiMode, SavedModelCredential } from "./types"; const SETTINGS_KEY = "task-polisher.settings.v3"; const LEGACY_SETTINGS_KEY = "task-polisher.settings.v2"; export const MODEL_OPTIONS = [ "deepseek-v4-pro", "gpt-5.4", "gpt-5.3-codex", "gpt-5", "gpt-5-codex", "z-ai/glm-5.1", "custom", ]; export const DEFAULT_SETTINGS: AppSettings = { ruleProfile: "echo-05061347", polishTransport: "browser", analysisTransport: "backend", apiMode: "chat", baseURL: "https://api.deepseek.com", apiKey: "", savedModelCredentials: [], selectedModelCredentialId: "", modelPreset: "deepseek-v4-pro", customModel: "", concurrency: 3, temperature: 0.7, topP: 1, maxTokens: 32768, enableThinking: true, clearThinking: false, }; export function resolveModel(settings: AppSettings): string { return settings.modelPreset === "custom" ? settings.customModel.trim() : settings.modelPreset; } export function defaultBaseURLForMode(_apiMode: ApiMode): string { return "https://api.deepseek.com"; } export function applyDeepSeekPreset(settings: AppSettings): AppSettings { return sanitizeSettings({ ...settings, apiMode: "chat", baseURL: "https://api.deepseek.com", selectedModelCredentialId: "", modelPreset: "deepseek-v4-pro", customModel: "", temperature: 0.7, topP: 1, maxTokens: 32768, enableThinking: true, clearThinking: false, }); } export function loadSettings(): AppSettings { const stored = readJson(SETTINGS_KEY) ?? readJson(LEGACY_SETTINGS_KEY) ?? {}; return sanitizeSettings({ ...DEFAULT_SETTINGS, ...stored }); } export function saveSettings(settings: AppSettings): AppSettings { const sanitized = sanitizeSettings(settings); window.localStorage.setItem(SETTINGS_KEY, JSON.stringify(sanitized)); return sanitized; } export function sanitizeSettings(raw: Partial): AppSettings { const apiMode = raw.apiMode === "responses" ? "responses" : "chat"; const modelPreset = MODEL_OPTIONS.includes(String(raw.modelPreset)) ? String(raw.modelPreset) : DEFAULT_SETTINGS.modelPreset; const customModel = modelPreset === "custom" ? String(raw.customModel || "").trim() : ""; const baseURL = String(raw.baseURL || DEFAULT_SETTINGS.baseURL).trim() || defaultBaseURLForMode(apiMode); const apiKey = String(raw.apiKey || "").trim(); const savedModelCredentials = sanitizeSavedModelCredentials(raw.savedModelCredentials); const selectedModelCredentialId = savedModelCredentials.some((item) => item.id === raw.selectedModelCredentialId) ? String(raw.selectedModelCredentialId) : ""; return { ruleProfile: String(raw.ruleProfile || DEFAULT_SETTINGS.ruleProfile), polishTransport: "browser", analysisTransport: "backend", apiMode, baseURL, apiKey, savedModelCredentials, selectedModelCredentialId, modelPreset, customModel, concurrency: clampInteger(raw.concurrency, 1, 8, DEFAULT_SETTINGS.concurrency), temperature: clampNumber(raw.temperature, 0, 2, DEFAULT_SETTINGS.temperature), topP: clampNumber(raw.topP, 0, 1, DEFAULT_SETTINGS.topP), maxTokens: clampInteger(raw.maxTokens, 1, 131072, DEFAULT_SETTINGS.maxTokens), enableThinking: raw.enableThinking !== false, clearThinking: Boolean(raw.clearThinking), }; } function sanitizeSavedModelCredentials(value: unknown): SavedModelCredential[] { if (!Array.isArray(value)) return []; const seen = new Set(); return value .map((item, index) => { const raw = item as Partial; const baseURL = String(raw?.baseURL || "").trim(); const apiKey = String(raw?.apiKey || "").trim(); if (!baseURL || !apiKey) return null; const id = String(raw?.id || "").trim() || makeCredentialId(baseURL, index); if (seen.has(id)) return null; seen.add(id); return { id, name: String(raw?.name || "").trim() || baseURL, baseURL, apiKey, }; }) .filter((item): item is SavedModelCredential => Boolean(item)); } function makeCredentialId(baseURL: string, index: number): string { return `credential-${index}-${baseURL.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40) || "model"}`; } function clampNumber(value: unknown, min: number, max: number, fallback: number): number { const parsed = Number(value); if (!Number.isFinite(parsed)) return fallback; return Math.min(max, Math.max(min, parsed)); } function clampInteger(value: unknown, min: number, max: number, fallback: number): number { return Math.round(clampNumber(value, min, max, fallback)); } function readJson(key: string): Record | null { try { const raw = window.localStorage.getItem(key); return raw ? JSON.parse(raw) : null; } catch { return null; } }