first commit

This commit is contained in:
douboer
2026-03-21 18:57:10 +08:00
commit c49aa1a5e9
570 changed files with 107167 additions and 0 deletions

View File

@@ -0,0 +1,277 @@
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<GlobalSettings["uiLanguage"]>(["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<string>();
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<GlobalSettings> | 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<string, ThemePreset> = {
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<GlobalSettings, "gatewayUrl">): string {
// 兼容旧版持久化数据中残留的 gatewayUrl 字段
if (settings?.gatewayUrl) {
return settings.gatewayUrl;
}
return resolveDefaultGatewayUrl();
}
/**
* 根据设置推导 gatewayToken运行时不持久化到用户设置
* 优先级:构建时 VITE_GATEWAY_TOKEN > 旧版持久化数据残留 > 开发占位符。
*/
export function resolveGatewayToken(settings?: Pick<GlobalSettings, "gatewayToken">): 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";
}