import { webUiPathWrapper, getInitialWebUiToken } from '@/napcat-webui-backend/index'; import { Type, Static } from '@sinclair/typebox'; import Ajv from 'ajv'; import fs, { constants } from 'node:fs/promises'; import { resolve } from 'node:path'; import { deepMerge } from '../utils/object'; import { themeType } from '../types/theme'; import { getRandomToken } from '../utils/url'; // 限制尝试端口的次数,避免死循环 // 定义配置的类型 const WebUiConfigSchema = Type.Object({ host: Type.String({ default: '0.0.0.0' }), port: Type.Number({ default: 6099 }), token: Type.String({ default: getRandomToken(12) }), loginRate: Type.Number({ default: 10 }), autoLoginAccount: Type.String({ default: '' }), theme: themeType, // 是否关闭WebUI disableWebUI: Type.Boolean({ default: false }), // 是否关闭非局域网访问 disableNonLANAccess: Type.Boolean({ default: false }), }); export type WebUiConfigType = Static; // 读取当前目录下名为 webui.json 的配置文件,如果不存在则创建初始化配置文件 export class WebUiConfigWrapper { WebUiConfigData: WebUiConfigType | undefined = undefined; private validateAndApplyDefaults (config: Partial): WebUiConfigType { new Ajv({ coerceTypes: true, useDefaults: true }).compile(WebUiConfigSchema)(config); return config as WebUiConfigType; } private async ensureConfigFileExists (configPath: string): Promise { const configExists = await fs .access(configPath, constants.F_OK) .then(() => true) .catch(() => false); if (!configExists) { await fs.writeFile(configPath, JSON.stringify(this.validateAndApplyDefaults({}), null, 4)); } } private async readAndValidateConfig (configPath: string): Promise { const fileContent = await fs.readFile(configPath, 'utf-8'); return this.validateAndApplyDefaults(JSON.parse(fileContent)); } private async writeConfig (configPath: string, config: WebUiConfigType): Promise { const hasWritePermission = await fs .access(configPath, constants.W_OK) .then(() => true) .catch(() => false); if (hasWritePermission) { await fs.writeFile(configPath, JSON.stringify(config, null, 4)); } else { console.warn(`文件: ${configPath} 没有写入权限, 配置的更改部分可能会在重启后还原.`); } } async GetWebUIConfig (): Promise { if (this.WebUiConfigData) { return this.WebUiConfigData; } try { const configPath = resolve(webUiPathWrapper.configPath, './webui.json'); await this.ensureConfigFileExists(configPath); const parsedConfig = await this.readAndValidateConfig(configPath); // 使用内存中缓存的token进行覆盖,确保强兼容性 this.WebUiConfigData = { ...parsedConfig, // 首次读取内存中是没有token的,需要进行一层兜底 token: getInitialWebUiToken() || parsedConfig.token, }; return this.WebUiConfigData; } catch (e) { console.log('读取配置文件失败', e); const defaultConfig = this.validateAndApplyDefaults({}); this.WebUiConfigData = { ...defaultConfig, token: getInitialWebUiToken() || defaultConfig.token, }; return this.WebUiConfigData; } } async UpdateWebUIConfig (newConfig: Partial): Promise { const configPath = resolve(webUiPathWrapper.configPath, './webui.json'); // 使用原始配置进行合并,避免内存token覆盖影响配置更新 const currentConfig = await this.GetRawWebUIConfig(); const mergedConfig = deepMerge({ ...currentConfig }, newConfig); const updatedConfig = this.validateAndApplyDefaults(mergedConfig); await this.writeConfig(configPath, updatedConfig); this.WebUiConfigData = updatedConfig; } /** * 获取配置文件中实际存储的配置(不被内存token覆盖) * 主要用于配置更新和特殊场景 */ async GetRawWebUIConfig (): Promise { if (this.WebUiConfigData) { return this.WebUiConfigData; } try { const configPath = resolve(webUiPathWrapper.configPath, './webui.json'); await this.ensureConfigFileExists(configPath); const parsedConfig = await this.readAndValidateConfig(configPath); this.WebUiConfigData = parsedConfig; return this.WebUiConfigData; } catch (e) { console.log('读取配置文件失败', e); return this.validateAndApplyDefaults({}); } } async UpdateToken (oldToken: string, newToken: string): Promise { // 使用内存中缓存的token进行验证,确保强兼容性 const cachedToken = getInitialWebUiToken(); const tokenToCheck = cachedToken || (await this.GetWebUIConfig()).token; if (tokenToCheck !== oldToken) { throw new Error('旧 token 不匹配'); } await this.UpdateWebUIConfig({ token: newToken }); } // 获取日志文件夹路径 async GetLogsPath (): Promise { return resolve(webUiPathWrapper.logsPath); } // 获取日志列表 async GetLogsList (): Promise { const logsPath = resolve(webUiPathWrapper.logsPath); const logsExist = await fs .access(logsPath, constants.F_OK) .then(() => true) .catch(() => false); if (logsExist) { return (await fs.readdir(logsPath)) .filter((file) => file.endsWith('.log')) .map((file) => file.replace('.log', '')); } return []; } // 获取指定日志文件内容 async GetLogContent (filename: string): Promise { const logPath = resolve(webUiPathWrapper.logsPath, `${filename}.log`); const logExists = await fs .access(logPath, constants.R_OK) .then(() => true) .catch(() => false); if (logExists) { return await fs.readFile(logPath, 'utf-8'); } return ''; } // 获取字体文件夹内的字体列表 async GetFontList (): Promise { const fontsPath = resolve(webUiPathWrapper.configPath, './fonts'); const fontsExist = await fs .access(fontsPath, constants.F_OK) .then(() => true) .catch(() => false); if (fontsExist) { return (await fs.readdir(fontsPath)).filter((file) => file.endsWith('.ttf')); } return []; } // 判断字体是否存在(webui.woff) async CheckWebUIFontExist (): Promise { const fontsPath = resolve(webUiPathWrapper.configPath, './fonts'); return await fs .access(resolve(fontsPath, './webui.woff'), constants.F_OK) .then(() => true) .catch(() => false); } // 获取webui字体文件路径 GetWebUIFontPath (): string { return resolve(webUiPathWrapper.configPath, './fonts/webui.woff'); } getAutoLoginAccount (): string | undefined { return this.WebUiConfigData?.autoLoginAccount; } // 获取自动登录账号 async GetAutoLoginAccount (): Promise { return (await this.GetWebUIConfig()).autoLoginAccount; } // 更新自动登录账号 async UpdateAutoLoginAccount (uin: string): Promise { await this.UpdateWebUIConfig({ autoLoginAccount: uin }); } // 获取主题内容 async GetTheme (): Promise { const config = await this.GetWebUIConfig(); return config.theme; } // 更新主题内容 async UpdateTheme (theme: WebUiConfigType['theme']): Promise { await this.UpdateWebUIConfig({ theme }); } // 获取是否禁用WebUI async GetDisableWebUI (): Promise { const config = await this.GetWebUIConfig(); return config.disableWebUI; } // 更新是否禁用WebUI async UpdateDisableWebUI (disable: boolean): Promise { await this.UpdateWebUIConfig({ disableWebUI: disable }); } // 获取是否禁用非局域网访问 async GetDisableNonLANAccess (): Promise { const config = await this.GetWebUIConfig(); return config.disableNonLANAccess; } // 更新是否禁用非局域网访问 async UpdateDisableNonLANAccess (disable: boolean): Promise { await this.UpdateWebUIConfig({ disableNonLANAccess: disable }); } }