mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-02-06 21:10:23 +00:00
Updated the default value of the 'host' field in WebUiConfigSchema from '0.0.0.0' (IPv4) to '::' (IPv6) to support IPv6 by default.
302 lines
10 KiB
TypeScript
302 lines
10 KiB
TypeScript
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: '::' }),
|
||
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 }),
|
||
// 网络访问控制模式: 'none' | 'whitelist' | 'blacklist'
|
||
accessControlMode: Type.Union([
|
||
Type.Literal('none'),
|
||
Type.Literal('whitelist'),
|
||
Type.Literal('blacklist'),
|
||
], { default: 'none' }),
|
||
// IP白名单列表
|
||
ipWhitelist: Type.Array(Type.String(), { default: [] }),
|
||
// IP黑名单列表
|
||
ipBlacklist: Type.Array(Type.String(), { default: [] }),
|
||
// 是否启用 X-Forwarded-For 获取真实IP
|
||
enableXForwardedFor: Type.Boolean({ default: false }),
|
||
});
|
||
|
||
export type WebUiConfigType = Static<typeof WebUiConfigSchema>;
|
||
|
||
// 读取当前目录下名为 webui.json 的配置文件,如果不存在则创建初始化配置文件
|
||
export class WebUiConfigWrapper {
|
||
WebUiConfigData: WebUiConfigType | undefined = undefined;
|
||
|
||
private validateAndApplyDefaults (config: Partial<WebUiConfigType>): WebUiConfigType {
|
||
new Ajv({ coerceTypes: true, useDefaults: true }).compile(WebUiConfigSchema)(config);
|
||
return config as WebUiConfigType;
|
||
}
|
||
|
||
private async ensureConfigFileExists (configPath: string): Promise<void> {
|
||
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<WebUiConfigType> {
|
||
const fileContent = await fs.readFile(configPath, 'utf-8');
|
||
return this.validateAndApplyDefaults(JSON.parse(fileContent));
|
||
}
|
||
|
||
private async writeConfig (configPath: string, config: WebUiConfigType): Promise<void> {
|
||
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<WebUiConfigType> {
|
||
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<WebUiConfigType>): Promise<void> {
|
||
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<WebUiConfigType> {
|
||
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<void> {
|
||
// 使用内存中缓存的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<string> {
|
||
return resolve(webUiPathWrapper.logsPath);
|
||
}
|
||
|
||
// 获取日志列表
|
||
async GetLogsList (): Promise<string[]> {
|
||
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<string> {
|
||
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<string[]> {
|
||
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 [];
|
||
}
|
||
|
||
// 判断字体是否存在(支持多种格式)
|
||
async CheckWebUIFontExist (): Promise<boolean> {
|
||
const fontPath = await this.GetWebUIFontPath();
|
||
if (!fontPath) return false;
|
||
return await fs
|
||
.access(fontPath, constants.F_OK)
|
||
.then(() => true)
|
||
.catch(() => false);
|
||
}
|
||
|
||
// 获取webui字体文件路径(支持多种格式)
|
||
async GetWebUIFontPath (): Promise<string | null> {
|
||
const fontsPath = resolve(webUiPathWrapper.configPath, './fonts');
|
||
const extensions = ['.woff', '.woff2', '.ttf', '.otf'];
|
||
for (const ext of extensions) {
|
||
const fontPath = resolve(fontsPath, `webui${ext}`);
|
||
const exists = await fs
|
||
.access(fontPath, constants.F_OK)
|
||
.then(() => true)
|
||
.catch(() => false);
|
||
if (exists) {
|
||
return fontPath;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
// 同步版本,用于 multer 配置
|
||
GetWebUIFontPathSync (): string {
|
||
return resolve(webUiPathWrapper.configPath, './fonts/webui.woff');
|
||
}
|
||
|
||
getAutoLoginAccount (): string | undefined {
|
||
return this.WebUiConfigData?.autoLoginAccount;
|
||
}
|
||
|
||
// 获取自动登录账号
|
||
async GetAutoLoginAccount (): Promise<string> {
|
||
return (await this.GetWebUIConfig()).autoLoginAccount;
|
||
}
|
||
|
||
// 更新自动登录账号
|
||
async UpdateAutoLoginAccount (uin: string): Promise<void> {
|
||
await this.UpdateWebUIConfig({ autoLoginAccount: uin });
|
||
}
|
||
|
||
// 获取主题内容
|
||
async GetTheme (): Promise<WebUiConfigType['theme']> {
|
||
const config = await this.GetWebUIConfig();
|
||
|
||
return config.theme;
|
||
}
|
||
|
||
// 更新主题内容
|
||
async UpdateTheme (theme: WebUiConfigType['theme']): Promise<void> {
|
||
await this.UpdateWebUIConfig({ theme });
|
||
}
|
||
|
||
// 获取是否禁用WebUI
|
||
async GetDisableWebUI (): Promise<boolean> {
|
||
const config = await this.GetWebUIConfig();
|
||
return config.disableWebUI;
|
||
}
|
||
|
||
// 更新是否禁用WebUI
|
||
async UpdateDisableWebUI (disable: boolean): Promise<void> {
|
||
await this.UpdateWebUIConfig({ disableWebUI: disable });
|
||
}
|
||
|
||
// 获取访问控制模式
|
||
async GetAccessControlMode (): Promise<'none' | 'whitelist' | 'blacklist'> {
|
||
const config = await this.GetWebUIConfig();
|
||
return config.accessControlMode;
|
||
}
|
||
|
||
// 更新访问控制模式
|
||
async UpdateAccessControlMode (mode: 'none' | 'whitelist' | 'blacklist'): Promise<void> {
|
||
await this.UpdateWebUIConfig({ accessControlMode: mode });
|
||
}
|
||
|
||
// 获取IP白名单
|
||
async GetIpWhitelist (): Promise<string[]> {
|
||
const config = await this.GetWebUIConfig();
|
||
return config.ipWhitelist;
|
||
}
|
||
|
||
// 更新IP白名单
|
||
async UpdateIpWhitelist (whitelist: string[]): Promise<void> {
|
||
await this.UpdateWebUIConfig({ ipWhitelist: whitelist });
|
||
}
|
||
|
||
// 获取IP黑名单
|
||
async GetIpBlacklist (): Promise<string[]> {
|
||
const config = await this.GetWebUIConfig();
|
||
return config.ipBlacklist;
|
||
}
|
||
|
||
// 更新IP黑名单
|
||
async UpdateIpBlacklist (blacklist: string[]): Promise<void> {
|
||
await this.UpdateWebUIConfig({ ipBlacklist: blacklist });
|
||
}
|
||
|
||
// 获取是否启用 X-Forwarded-For
|
||
async GetEnableXForwardedFor (): Promise<boolean> {
|
||
const config = await this.GetWebUIConfig();
|
||
return config.enableXForwardedFor || false;
|
||
}
|
||
|
||
// 更新是否启用 X-Forwarded-For
|
||
async UpdateEnableXForwardedFor (enable: boolean): Promise<void> {
|
||
await this.UpdateWebUIConfig({ enableXForwardedFor: enable });
|
||
}
|
||
}
|