import type { GlobalSettings, ThemePreset } from "@/types/app"; import { pickShellAccentColor } from "@remoteconn/shared"; const MIN_TERMINAL_BUFFER_MAX_ENTRIES = 200; const MAX_TERMINAL_BUFFER_MAX_ENTRIES = 50_000; const MIN_TERMINAL_BUFFER_MAX_BYTES = 64 * 1024; const MAX_TERMINAL_BUFFER_MAX_BYTES = 64 * 1024 * 1024; const UI_LANGUAGE_VALUES = new Set(["zh-Hans", "zh-Hant", "en", "ja", "ko"]); const DEFAULT_SHELL_BG_COLOR = "#192b4d"; const DEFAULT_SHELL_TEXT_COLOR = "#e6f0ff"; export const DEFAULT_VOICE_RECORD_CATEGORY_FALLBACK = "未分类"; export const DEFAULT_VOICE_RECORD_CATEGORIES = ["未分类", "优化", "新需求", "问题", "灵感"] as const; export const DEFAULT_VOICE_RECORD_DEFAULT_CATEGORY = "优化"; function normalizeInteger(value: number, fallback: number, min: number, max: number): number { if (!Number.isFinite(value)) { return fallback; } const normalized = Math.round(value); if (normalized < min) { return min; } if (normalized > max) { return max; } return normalized; } /** * 界面语言归一化: * 1. Web 端当前只负责保真,不负责真正切语言; * 2. 仍需限制合法枚举,避免无效值在跨端同步中持续扩散。 */ function normalizeUiLanguage(value: unknown): GlobalSettings["uiLanguage"] { const normalized = String(value ?? "").trim() as GlobalSettings["uiLanguage"]; return UI_LANGUAGE_VALUES.has(normalized) ? normalized : "zh-Hans"; } /** * 归一化闪念分类列表: * 1. 去空白、去重、保序; * 2. 强制保留“未分类”兜底项; * 3. 限制最多 10 项,避免配置面板无限增长。 */ function normalizeVoiceRecordCategories(value: unknown): string[] { const source = Array.isArray(value) ? value : []; const seen = new Set(); const next: string[] = []; for (const entry of source) { const normalized = String(entry ?? "").trim(); if (!normalized || seen.has(normalized)) { continue; } seen.add(normalized); next.push(normalized); if (next.length >= 10) { break; } } if (!seen.has(DEFAULT_VOICE_RECORD_CATEGORY_FALLBACK)) { next.unshift(DEFAULT_VOICE_RECORD_CATEGORY_FALLBACK); } return next.slice(0, 10); } /** * 归一化默认闪念分类: * 1. 优先使用合法且存在于分类列表中的配置值; * 2. 否则回退到预设默认分类; * 3. 若预设默认分类不在列表中,则回退到分类列表首项。 */ function normalizeVoiceRecordDefaultCategory(value: unknown, categories: string[]): string { const normalized = String(value ?? "").trim(); if (normalized && categories.includes(normalized)) { return normalized; } if (categories.includes(DEFAULT_VOICE_RECORD_DEFAULT_CATEGORY)) { return DEFAULT_VOICE_RECORD_DEFAULT_CATEGORY; } return categories[0] ?? DEFAULT_VOICE_RECORD_CATEGORY_FALLBACK; } /** * 推导默认网关地址: * 1) 若显式配置了 VITE_GATEWAY_URL,优先使用; * 2) 浏览器环境下根据当前站点自动推导; * 3) 默认走同域 80/443(由反向代理承接)。 */ function resolveDefaultGatewayUrl(): string { const envGateway = import.meta.env.VITE_GATEWAY_URL?.trim(); if (envGateway) { return envGateway; } if (typeof window === "undefined") { return "ws://localhost:8787"; } const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; const hostname = window.location.hostname; return `${protocol}//${hostname}`; } export const defaultSettings: GlobalSettings = { // ── UI 外观 ────────────────────────────────────────────────────────────── uiLanguage: "zh-Hans", uiThemePreset: "tide", uiThemeMode: "dark" as "dark" | "light", uiAccentColor: "#5bd2ff", uiBgColor: "#192b4d", uiTextColor: "#e6f0ff", uiBtnColor: "#adb9cd", // ── Shell 显示 ──────────────────────────────────────────────────────────── shellThemePreset: "tide", shellThemeMode: "dark" as "dark" | "light", shellBgColor: DEFAULT_SHELL_BG_COLOR, shellTextColor: DEFAULT_SHELL_TEXT_COLOR, shellAccentColor: pickShellAccentColor(DEFAULT_SHELL_BG_COLOR, DEFAULT_SHELL_TEXT_COLOR), shellFontFamily: "JetBrains Mono", shellFontSize: 15, shellLineHeight: 1.4, unicode11: true, // ── 终端缓冲 ───────────────────────────────────────────────────────────── terminalBufferMaxEntries: 5000, terminalBufferMaxBytes: 4 * 1024 * 1024, // ── 连接策略 ───────────────────────────────────────────────────────────── autoReconnect: true, reconnectLimit: 3, hostKeyPolicy: "strict", credentialMemoryPolicy: "remember", gatewayConnectTimeoutMs: 12000, waitForConnectedTimeoutMs: 15000, // ── 服务器配置预填 ──────────────────────────────────────────────────────── defaultAuthType: "password", defaultPort: 22, defaultProjectPath: "~/workspace", defaultTimeoutSeconds: 20, defaultHeartbeatSeconds: 15, defaultTransportMode: "gateway", // ── 日志 ───────────────────────────────────────────────────────────────── logRetentionDays: 30, maskSecrets: true, voiceRecordCategories: [...DEFAULT_VOICE_RECORD_CATEGORIES], voiceRecordDefaultCategory: DEFAULT_VOICE_RECORD_DEFAULT_CATEGORY }; /** * 全局配置归一化: * - 为历史版本缺失字段补齐默认值; * - 将旧版废弃字段迁移到新域前缀字段(仅首次,不覆盖已有新字段); * - 对终端缓冲阈值做边界收敛,避免 NaN/异常值导致缓冲策略失效。 */ export function normalizeGlobalSettings(raw: Partial | null | undefined): GlobalSettings { const r = raw ?? {}; const merged: GlobalSettings = { ...defaultSettings, ...r }; // ── 旧字段迁移(取旧值兜底,不覆盖已存在的新字段)──────────────────────── // fontFamily → shellFontFamily if (!r.shellFontFamily && r.fontFamily) { merged.shellFontFamily = r.fontFamily; } // fontSize → shellFontSize if (!r.shellFontSize && r.fontSize !== undefined) { merged.shellFontSize = r.fontSize; } // lineHeight → shellLineHeight if (!r.shellLineHeight && r.lineHeight !== undefined) { merged.shellLineHeight = r.lineHeight; } // accentColor → uiAccentColor / shellAccentColor if (!r.uiAccentColor && r.accentColor) { merged.uiAccentColor = r.accentColor; } if (!r.shellAccentColor && r.accentColor) { merged.shellAccentColor = r.accentColor; } // bgColor → uiBgColor / shellBgColor if (!r.uiBgColor && r.bgColor) { merged.uiBgColor = r.bgColor; } if (!r.shellBgColor && r.bgColor) { merged.shellBgColor = r.bgColor; } // textColor → uiTextColor / shellTextColor if (!r.uiTextColor && r.textColor) { merged.uiTextColor = r.textColor; } if (!r.shellTextColor && r.textColor) { merged.shellTextColor = r.textColor; } // themePreset → uiThemePreset / shellThemePreset(仅映射合法值) const legacyThemeMap: Record = { tide: "tide", mint: "tide", // mint 无对应新预设,兜底 tide sunrise: "焰岩" // sunrise 映射到焰岩暖色系 }; if (!r.uiThemePreset && r.themePreset) { merged.uiThemePreset = legacyThemeMap[r.themePreset] ?? "tide"; } if (!r.shellThemePreset && r.themePreset) { merged.shellThemePreset = legacyThemeMap[r.themePreset] ?? "tide"; } // shellThemeMode 非法值兜底 if (merged.shellThemeMode !== "dark" && merged.shellThemeMode !== "light") { merged.shellThemeMode = "dark"; } // credentialMemoryPolicy: "session" → "forget"(旧枚举值 session 对应 forget) if ((merged.credentialMemoryPolicy as string) === "session") { merged.credentialMemoryPolicy = "forget"; } // uiThemeMode 非法值兜底 if (merged.uiThemeMode !== "dark" && merged.uiThemeMode !== "light") { merged.uiThemeMode = "dark"; } merged.uiLanguage = normalizeUiLanguage(merged.uiLanguage); // ── 数值边界收敛 ───────────────────────────────────────────────────────── merged.terminalBufferMaxEntries = normalizeInteger( merged.terminalBufferMaxEntries, defaultSettings.terminalBufferMaxEntries, MIN_TERMINAL_BUFFER_MAX_ENTRIES, MAX_TERMINAL_BUFFER_MAX_ENTRIES ); merged.terminalBufferMaxBytes = normalizeInteger( merged.terminalBufferMaxBytes, defaultSettings.terminalBufferMaxBytes, MIN_TERMINAL_BUFFER_MAX_BYTES, MAX_TERMINAL_BUFFER_MAX_BYTES ); merged.voiceRecordCategories = normalizeVoiceRecordCategories(merged.voiceRecordCategories); merged.voiceRecordDefaultCategory = normalizeVoiceRecordDefaultCategory( merged.voiceRecordDefaultCategory, merged.voiceRecordCategories ); return merged; } /** * 根据设置推导 gatewayUrl(运行时,不持久化到用户设置)。 * 优先级:构建时 VITE_GATEWAY_URL > 旧版持久化数据残留 > 自动推导同域地址。 */ export function resolveGatewayUrl(settings?: Pick): string { // 兼容旧版持久化数据中残留的 gatewayUrl 字段 if (settings?.gatewayUrl) { return settings.gatewayUrl; } return resolveDefaultGatewayUrl(); } /** * 根据设置推导 gatewayToken(运行时,不持久化到用户设置)。 * 优先级:构建时 VITE_GATEWAY_TOKEN > 旧版持久化数据残留 > 开发占位符。 */ export function resolveGatewayToken(settings?: Pick): string { const envToken = import.meta.env.VITE_GATEWAY_TOKEN?.trim(); if (envToken) { return envToken; } // 兼容旧版持久化数据中残留的 gatewayToken 字段 if (settings?.gatewayToken) { return settings.gatewayToken; } return "remoteconn-dev-token"; }