Merge pull request #153 from Guation/main

feat: WebUI支持放置到二级目录中
This commit is contained in:
手瓜一十雪
2024-07-26 13:04:26 +08:00
committed by GitHub
9 changed files with 110 additions and 60 deletions

View File

@@ -27,18 +27,19 @@ export async function InitWebUi() {
}
app.use(express.json());
// 初始服务
app.all('/', (_req, res) => {
// WebUI只在config.prefix所示路径上提供服务可配合Nginx挂载到子目录中
app.all(config.prefix + '/', (_req, res) => {
res.json({
msg: 'NapCat WebAPI is now running!',
});
});
// 配置静态文件服务,提供./static目录下的文件服务访问路径为/webui
app.use('/webui', express.static(resolve(__dirname, './static')));
app.use(config.prefix + '/webui', express.static(resolve(__dirname, './static')));
//挂载API接口
app.use('/api', ALLRouter);
app.listen(config.port, async () => {
log(`[NapCat] [WebUi] Current WebUi is running at IP:${config.port}`);
app.use(config.prefix + '/api', ALLRouter);
app.listen(config.port, config.host, async () => {
log(`[NapCat] [WebUi] Current WebUi is running at http://${config.host}:${config.port}${config.prefix}`);
log(`[NapCat] [WebUi] Login URL is http://${config.host}:${config.port}${config.prefix}/webui`);
log(`[NapCat] [WebUi] Login Token is ${config.token}`);
});
}
}

View File

@@ -12,7 +12,33 @@ const __dirname = dirname(__filename);
// 限制尝试端口的次数,避免死循环
const MAX_PORT_TRY = 100;
async function tryUsePort(port: number, tryCount: number = 0): Promise<number> {
async function tryUseHost(host: string): Promise<string> {
return new Promise(async (resolve, reject) => {
try {
const server = net.createServer();
server.on('listening', () => {
server.close();
resolve(host);
});
server.on('error', (err: any) => {
if (err.code === 'EADDRNOTAVAIL') {
reject("主机地址验证失败,可能为非本机地址");
} else {
reject(`遇到错误: ${err.code}`);
}
});
// 尝试监听 让系统随机分配一个端口
server.listen(0, host);
} catch (error) {
// 这里捕获到的错误应该是启动服务器时的同步错误
reject(`服务器启动时发生错误: ${error}`);
}
});
}
async function tryUsePort(port: number, host: string, tryCount: number = 0): Promise<number> {
return new Promise(async (resolve, reject) => {
try {
const server = net.createServer();
@@ -25,7 +51,7 @@ async function tryUsePort(port: number, tryCount: number = 0): Promise<number> {
if (err.code === 'EADDRINUSE') {
if (tryCount < MAX_PORT_TRY) {
// 使用循环代替递归
resolve(tryUsePort(port + 1, tryCount + 1));
resolve(tryUsePort(port + 1, host, tryCount + 1));
} else {
reject(`端口尝试失败,达到最大尝试次数: ${MAX_PORT_TRY}`);
}
@@ -35,7 +61,7 @@ async function tryUsePort(port: number, tryCount: number = 0): Promise<number> {
});
// 尝试监听端口
server.listen(port);
server.listen(port, host);
} catch (error) {
// 这里捕获到的错误应该是启动服务器时的同步错误
reject(`服务器启动时发生错误: ${error}`);
@@ -44,44 +70,73 @@ async function tryUsePort(port: number, tryCount: number = 0): Promise<number> {
}
export interface WebUiConfigType {
host: string;
port: number;
prefix: string;
token: string;
loginRate: number
}
// 读取当前目录下名为 webui.json 的配置文件,如果不存在则创建初始化配置文件
class WebUiConfigWrapper {
WebUiConfigData: WebUiConfigType | undefined = undefined;
private applyDefaults<T>(obj: Partial<T>, defaults: T): T {
return { ...defaults, ...obj };
}
async GetWebUIConfig(): Promise<WebUiConfigType> {
if (this.WebUiConfigData) {
return this.WebUiConfigData;
}
const defaultconfig: WebUiConfigType = {
host: "0.0.0.0",
port: 6099,
prefix: "",
token: "", // 默认先填空,空密码无法登录
loginRate: 3
};
try {
defaultconfig.token = Math.random().toString(36).slice(2); //生成随机密码
} catch (e) {
logError('随机密码生成失败', e);
}
try {
const configPath = resolve(__dirname, './config/webui.json');
const config: WebUiConfigType = {
port: 6099,
token: Math.random().toString(36).slice(2),//生成随机密码
loginRate: 3
};
if (!existsSync(configPath)) {
writeFileSync(configPath, JSON.stringify(config, null, 4));
writeFileSync(configPath, JSON.stringify(defaultconfig, null, 4));
}
const fileContent = readFileSync(configPath, 'utf-8');
const parsedConfig = JSON.parse(fileContent) as WebUiConfigType;
// 更新配置字段后新增字段可能会缺失,同步一下
const parsedConfig = this.applyDefaults(JSON.parse(fileContent) as Partial<WebUiConfigType>, defaultconfig);
// 修正端口占用情况
const [err, data] = await tryUsePort(parsedConfig.port).then(data => [null, data as number]).catch(err => [err, null]);
parsedConfig.port = data;
if (err) {
//一般没那么离谱 如果真有这么离谱 考虑下 向外抛出异常
if (!parsedConfig.prefix.startsWith("/")) parsedConfig.prefix = "/" + parsedConfig.prefix;
if (parsedConfig.prefix.endsWith("/")) parsedConfig.prefix = parsedConfig.prefix.slice(0, -1);
// 配置已经被操作过了,还是回写一下吧,不然新配置不会出现在配置文件里
writeFileSync(configPath, JSON.stringify(parsedConfig, null, 4));
// 不希望回写的配置放后面
// 查询主机地址是否可用
const [host_err, host] = await tryUseHost(parsedConfig.host).then(data => [null, data as string]).catch(err => [err, null]);
if (host_err) {
logError("host不可用", host_err)
parsedConfig.port = 0; // 设置为0禁用WebUI
} else {
parsedConfig.host = host;
// 修正端口占用情况
const [port_err, port] = await tryUsePort(parsedConfig.port, parsedConfig.host).then(data => [null, data as number]).catch(err => [err, null]);
if (port_err) {
logError("port不可用", port_err)
parsedConfig.port = 0; // 设置为0禁用WebUI
} else {
parsedConfig.port = port;
}
}
this.WebUiConfigData = parsedConfig;
return this.WebUiConfigData;
} catch (e) {
logError('读取配置文件失败', e);
}
return {} as WebUiConfigType; // 理论上这行代码到不了,为了保持函数完整性而保留
return defaultconfig; // 理论上这行代码到不了,到了只能返回默认配置了
}
}
export const WebUiConfig = new WebUiConfigWrapper();
export const WebUiConfig = new WebUiConfigWrapper();

View File

@@ -15,7 +15,7 @@ async function onSettingWindowCreated(view: Element) {
} else if (configKey.length === 3) {
ob11Config[configKey[1]][configKey[2]] = value;
}
OB11ConfigWrapper.SetOB11Config(ob11Config);
// OB11ConfigWrapper.SetOB11Config(ob11Config); // 只有当点保存时才下发配置,而不是在修改值后立即下发
};
const parser = new DOMParser();
@@ -192,7 +192,7 @@ async function onSettingWindowCreated(view: Element) {
// 外链按钮
doc.querySelector('#open-github')?.addEventListener('click', () => {
window.open('https://napneko.github.io/', '_blank');
window.open('https://github.com/NapNeko/NapCatQQ', '_blank');
});
doc.querySelector('#open-telegram')?.addEventListener('click', () => {
window.open('https://t.me/+nLZEnpne-pQ1OWFl');
@@ -201,7 +201,7 @@ async function onSettingWindowCreated(view: Element) {
window.open('https://qm.qq.com/q/bDnHRG38aI');
});
doc.querySelector('#open-docs')?.addEventListener('click', () => {
window.open('https://github.com/NapNeko/NapCatQQ');
window.open('https://napneko.github.io/', '_blank');
});
// 生成反向地址列表
const buildHostListItem = (

View File

@@ -38,7 +38,7 @@ class WebUiApiOB11ConfigWrapper {
this.retCredential = Credential;
}
async GetOB11Config(): Promise<OB11Config> {
const ConfigResponse = await fetch('/api/OB11Config/GetConfig', {
const ConfigResponse = await fetch('../api/OB11Config/GetConfig', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + this.retCredential,
@@ -54,7 +54,7 @@ class WebUiApiOB11ConfigWrapper {
return {} as OB11Config;
}
async SetOB11Config(config: OB11Config): Promise<boolean> {
const ConfigResponse = await fetch('/api/OB11Config/SetConfig', {
const ConfigResponse = await fetch('../api/OB11Config/SetConfig', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + this.retCredential,

View File

@@ -1,5 +1,7 @@
{
"host": "0.0.0.0",
"port": 6099,
"prefix": "",
"token": "random",
"loginRate": 3