From a29b1154a9bcfbaac793bbacbad0b4f828d0cf73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=8C=82=E7=A5=9E?= Date: Fri, 26 Jul 2024 11:12:09 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20WebUI=E6=94=AF=E6=8C=81=E6=94=BE?= =?UTF-8?q?=E7=BD=AE=E5=88=B0=E4=BA=8C=E7=BA=A7=E7=9B=AE=E5=BD=95=E4=B8=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/webui/index.ts | 15 ++-- src/webui/src/helper/config.ts | 89 +++++++++++++++---- src/webui/ui/NapCat.ts | 6 +- src/webui/ui/components/WebUiApiOB11Config.ts | 4 +- src/webui/webui.json | 2 + static/QQLogin.html | 8 +- static/assets/renderer.js | 38 ++++---- static/index.html | 4 +- static/login.html | 4 +- 9 files changed, 110 insertions(+), 60 deletions(-) diff --git a/src/webui/index.ts b/src/webui/index.ts index 98366e38..ed65bd81 100644 --- a/src/webui/index.ts +++ b/src/webui/index.ts @@ -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}`); }); - -} \ No newline at end of file +} diff --git a/src/webui/src/helper/config.ts b/src/webui/src/helper/config.ts index 41e6df34..a3eb92f3 100644 --- a/src/webui/src/helper/config.ts +++ b/src/webui/src/helper/config.ts @@ -12,7 +12,33 @@ const __dirname = dirname(__filename); // 限制尝试端口的次数,避免死循环 const MAX_PORT_TRY = 100; -async function tryUsePort(port: number, tryCount: number = 0): Promise { +async function tryUseHost(host: string): Promise { + 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 { return new Promise(async (resolve, reject) => { try { const server = net.createServer(); @@ -25,7 +51,7 @@ async function tryUsePort(port: number, tryCount: number = 0): Promise { 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 { }); // 尝试监听端口 - server.listen(port); + server.listen(port, host); } catch (error) { // 这里捕获到的错误应该是启动服务器时的同步错误 reject(`服务器启动时发生错误: ${error}`); @@ -44,44 +70,73 @@ async function tryUsePort(port: number, tryCount: number = 0): Promise { } export interface WebUiConfigType { + host: string; port: number; + prefix: string; token: string; loginRate: number } // 读取当前目录下名为 webui.json 的配置文件,如果不存在则创建初始化配置文件 class WebUiConfigWrapper { WebUiConfigData: WebUiConfigType | undefined = undefined; + private applyDefaults(obj: Partial, defaults: T): T { + return { ...defaults, ...obj }; + } async GetWebUIConfig(): Promise { 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, 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(); \ No newline at end of file +export const WebUiConfig = new WebUiConfigWrapper(); diff --git a/src/webui/ui/NapCat.ts b/src/webui/ui/NapCat.ts index 26a6affe..1712a62f 100644 --- a/src/webui/ui/NapCat.ts +++ b/src/webui/ui/NapCat.ts @@ -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 = ( diff --git a/src/webui/ui/components/WebUiApiOB11Config.ts b/src/webui/ui/components/WebUiApiOB11Config.ts index 1d9eee0f..492cc6bf 100644 --- a/src/webui/ui/components/WebUiApiOB11Config.ts +++ b/src/webui/ui/components/WebUiApiOB11Config.ts @@ -38,7 +38,7 @@ class WebUiApiOB11ConfigWrapper { this.retCredential = Credential; } async GetOB11Config(): Promise { - 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 { - const ConfigResponse = await fetch('/api/OB11Config/SetConfig', { + const ConfigResponse = await fetch('../api/OB11Config/SetConfig', { method: 'POST', headers: { Authorization: 'Bearer ' + this.retCredential, diff --git a/src/webui/webui.json b/src/webui/webui.json index ed5bcc3e..793b8cf1 100644 --- a/src/webui/webui.json +++ b/src/webui/webui.json @@ -1,5 +1,7 @@ { + "host": "0.0.0.0", "port": 6099, + "prefix": "", "token": "random", "loginRate": 3 diff --git a/static/QQLogin.html b/static/QQLogin.html index be6c04fd..0307263d 100644 --- a/static/QQLogin.html +++ b/static/QQLogin.html @@ -160,7 +160,7 @@