refactor:优化WebUI后端代码格式(无新功能添加)

This commit is contained in:
bietiaop
2024-11-25 21:56:57 +08:00
parent 876bfbd3cb
commit 4eeabcc9e0
25 changed files with 483 additions and 377 deletions

View File

@@ -20,6 +20,7 @@
"@eslint/js": "^9.14.0",
"@log4js-node/log4js-api": "^1.0.2",
"@napneko/nap-proto-core": "^0.0.4",
"@protobuf-ts/runtime": "^2.9.4",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-typescript": "^11.1.6",
"@types/cors": "^2.8.17",
@@ -52,9 +53,9 @@
"dependencies": {
"express": "^5.0.0",
"fluent-ffmpeg": "^2.1.2",
"piscina": "^4.7.0",
"qrcode-terminal": "^0.12.0",
"silk-wasm": "^3.6.1",
"ws": "^8.18.0",
"piscina": "^4.7.0"
"ws": "^8.18.0"
}
}

View File

@@ -1,11 +1,20 @@
/**
* @file WebUI服务入口文件
*/
import express from 'express';
import { ALLRouter } from './src/router';
import { LogWrapper } from '@/common/log';
import { NapCatPathWrapper } from '@/common/path';
import { WebUiConfigWrapper } from './src/helper/config';
import { RequestUtil } from '@/common/request';
import { isIP } from "node:net";
import { WebUiConfigWrapper } from '@webapi/helper/config';
import { ALLRouter } from '@webapi/router';
import { cors } from '@webapi/middleware/cors';
import { createUrl } from '@webapi/utils/url';
import { sendSuccess } from '@webapi/utils/response';
// 实例化Express
const app = express();
/**
@@ -26,49 +35,51 @@ export async function InitWebUi(logger: LogWrapper, pathWrapper: NapCatPathWrapp
log('[NapCat] [WebUi] Current WebUi is not run.');
return;
}
// ------------注册中间件------------
// 使用express的json中间件
app.use(express.json());
// 初始服务
// CORS中间件
// TODO:
app.use(cors);
// ------------中间件结束------------
// ------------挂载路由------------
// 挂载静态路由(前端),路径为 [/前缀]/webui
app.use(config.prefix + '/webui', express.static(pathWrapper.staticPath));
// 挂载API接口
app.use(config.prefix + '/api', ALLRouter);
// 初始服务(先放个首页)
// WebUI只在config.prefix所示路径上提供服务可配合Nginx挂载到子目录中
app.all(config.prefix + '/', (_req, res) => {
res.json({
msg: 'NapCat WebAPI is now running!',
sendSuccess(res, null, 'NapCat WebAPI is now running!');
});
});
// 配置静态文件服务,提供./static目录下的文件服务访问路径为/webui
app.use(config.prefix + '/webui', express.static(pathWrapper.staticPath));
//挂载API接口
// 添加CORS支持
// TODO:
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
next();
});
app.use(config.prefix + '/api', ALLRouter);
// ------------路由挂载结束------------
// ------------启动服务------------
app.listen(config.port, config.host, async () => {
const normalizeHost = (host: string) => {
if (host === '0.0.0.0') return '127.0.0.1';
if (isIP(host) === 6) return `[${host}]`;
return host;
};
const createUrl = (host: string, path: string, token: string) => {
const url = new URL(`http://${normalizeHost(host)}`);
url.port = config.port.toString();
url.pathname = `${config.prefix}${path}`;
url.searchParams.set('token', token);
return url.toString();
};
// 启动后打印出相关地址
const port = config.port.toString(),
searchParams = { token: config.token },
path = `${config.prefix}/webui`;
// 打印日志地址、token
log(`[NapCat] [WebUi] Current WebUi is running at http://${config.host}:${config.port}${config.prefix}`);
log(`[NapCat] [WebUi] Login Token is ${config.token}`);
log(`[NapCat] [WebUi] WebUi User Panel Url: ${createUrl(config.host, '/webui', config.token)}`);
log(`[NapCat] [WebUi] WebUi Local Panel Url: ${createUrl('127.0.0.1', '/webui', config.token)}`);
log(`[NapCat] [WebUi] WebUi User Panel Url: ${createUrl(config.host, port, path, searchParams)}`);
log(`[NapCat] [WebUi] WebUi Local Panel Url: ${createUrl('127.0.0.1', port, path, searchParams)}`);
// 获取公网地址
try {
const publishUrl = 'https://ip.011102.xyz/';
const data = await RequestUtil.HttpGetJson<{ IP: { IP: string } }>(publishUrl, 'GET', {}, {}, true, true);
log(`[NapCat] [WebUi] WebUi Publish Panel Url: ${createUrl(data.IP.IP, '/webui', config.token)}`);
log(`[NapCat] [WebUi] WebUi Publish Panel Url: ${createUrl(data.IP.IP, port, path, searchParams)}`);
} catch (err) {
logger.logError(`[NapCat] [WebUi] Get Publish Panel Url Error: ${err}`);
}
});
// ------------Over------------
}

View File

@@ -1,69 +1,64 @@
import { RequestHandler } from 'express';
import { AuthHelper } from '../helper/SignToken';
import { WebUiDataRuntime } from '../helper/Data';
import { WebUiConfig } from '@/webui';
const isEmpty = (data: any) => data === undefined || data === null || data === '';
import { AuthHelper } from '@webapi/helper/SignToken';
import { WebUiDataRuntime } from '@webapi/helper/Data';
import { sendSuccess, sendError } from '@webapi/utils/response';
import { isEmpty } from '@webapi/utils/check';
// 登录
export const LoginHandler: RequestHandler = async (req, res) => {
// 获取WebUI配置
const WebUiConfigData = await WebUiConfig.GetWebUIConfig();
// 获取请求体中的token
const { token } = req.body;
// 如果token为空返回错误信息
if (isEmpty(token)) {
res.json({
code: -1,
message: 'token is empty',
});
return;
return sendError(res, 'token is empty');
}
if (!await WebUiDataRuntime.checkLoginRate(WebUiConfigData.loginRate)) {
res.json({
code: -1,
message: 'login rate limit',
});
return;
// 检查登录频率
if (!(await WebUiDataRuntime.checkLoginRate(WebUiConfigData.loginRate))) {
return sendError(res, 'login rate limit');
}
//验证config.token是否等于token
if (WebUiConfigData.token !== token) {
res.json({
code: -1,
message: 'token is invalid',
});
return;
return sendError(res, 'token is invalid');
}
const signCredential = Buffer.from(JSON.stringify(await AuthHelper.signCredential(WebUiConfigData.token))).toString('base64');
res.json({
code: 0,
message: 'success',
data: {
'Credential': signCredential,
},
// 签发凭证
const signCredential = Buffer.from(JSON.stringify(await AuthHelper.signCredential(WebUiConfigData.token))).toString(
'base64'
);
// 返回成功信息
return sendSuccess(res, {
Credential: signCredential,
});
return;
};
export const LogoutHandler: RequestHandler = (req, res) => {
// 这玩意无状态销毁个灯 得想想办法
res.json({
code: 0,
message: 'success',
});
return;
// 退出登录
export const LogoutHandler: RequestHandler = (_, res) => {
// TODO: 这玩意无状态销毁个灯 得想想办法
return sendSuccess(res, null);
};
// 检查登录状态
export const checkHandler: RequestHandler = async (req, res) => {
// 获取WebUI配置
const WebUiConfigData = await WebUiConfig.GetWebUIConfig();
// 获取请求头中的Authorization
const authorization = req.headers.authorization;
// 检查凭证
try {
// 从Authorization中获取凭证
const CredentialBase64: string = authorization?.split(' ')[1] as string;
// 解析凭证
const Credential = JSON.parse(Buffer.from(CredentialBase64, 'base64').toString());
// 验证凭证是否在一小时内有效
await AuthHelper.validateCredentialWithinOneHour(WebUiConfigData.token, Credential);
res.json({
code: 0,
message: 'success',
});
return;
// 返回成功信息
return sendSuccess(res, null);
} catch (e) {
res.json({
code: -1,
message: 'failed',
});
// 返回错误信息
return sendError(res, 'Authorization Faild');
}
return;
};

View File

@@ -1,14 +1,15 @@
import { RequestHandler } from 'express';
export const LogFileListHandler: RequestHandler = async (req, res) => {
res.send({
code: 0,
data: {
import { sendSuccess } from '@webapi/utils/response';
// TODO: Implement LogFileListHandler
export const LogFileListHandler: RequestHandler = async (_, res) => {
const fakeData = {
uin: 0,
nick: 'NapCat',
avatar: 'https://q1.qlogo.cn/g?b=qq&nk=0&s=640',
status: 'online',
boottime: Date.now()
}
});
boottime: Date.now(),
};
sendSuccess(res, fakeData);
};

View File

@@ -1,79 +1,58 @@
import { RequestHandler } from 'express';
import { WebUiDataRuntime } from '../helper/Data';
import { existsSync, readFileSync } from 'node:fs';
import { OneBotConfig } from '@/onebot/config/config';
import { resolve } from 'node:path';
import { webUiPathWrapper } from '@/webui';
const isEmpty = (data: any) => data === undefined || data === null || data === '';
export const OB11GetConfigHandler: RequestHandler = async (req, res) => {
import { OneBotConfig } from '@/onebot/config/config';
import { webUiPathWrapper } from '@/webui';
import { WebUiDataRuntime } from '@webapi/helper/Data';
import { sendError, sendSuccess } from '@webapi/utils/response';
import { isEmpty } from '@webapi/utils/check';
// 获取OneBot11配置
export const OB11GetConfigHandler: RequestHandler = async (_, res) => {
// 获取QQ登录状态
const isLogin = await WebUiDataRuntime.getQQLoginStatus();
// 如果未登录,返回错误
if (!isLogin) {
res.send({
code: -1,
message: 'Not Login',
});
return;
return sendError(res, 'Not Login');
}
// 获取登录的QQ号
const uin = await WebUiDataRuntime.getQQLoginUin();
// 读取配置文件
const configFilePath = resolve(webUiPathWrapper.configPath, `./onebot11_${uin}.json`);
//console.log(configFilePath);
let data: OneBotConfig;
// 尝试解析配置文件
try {
data = JSON.parse(
// 读取配置文件
const data = JSON.parse(
existsSync(configFilePath)
? readFileSync(configFilePath).toString()
: readFileSync(resolve(webUiPathWrapper.configPath, './onebot11.json')).toString()
);
) as OneBotConfig;
// 返回配置文件
return sendSuccess(res, data);
} catch (e) {
data = {} as OneBotConfig;
res.send({
code: -1,
message: 'Config Get Error',
});
return;
return sendError(res, 'Config Get Error');
}
res.send({
code: 0,
message: 'success',
data: data,
});
return;
};
// 写入OneBot11配置
export const OB11SetConfigHandler: RequestHandler = async (req, res) => {
// 获取QQ登录状态
const isLogin = await WebUiDataRuntime.getQQLoginStatus();
// 如果未登录,返回错误
if (!isLogin) {
res.send({
code: -1,
message: 'Not Login',
});
return;
return sendError(res, 'Not Login');
}
// 如果配置为空,返回错误
if (isEmpty(req.body.config)) {
res.send({
code: -1,
message: 'config is empty',
});
return;
return sendError(res, 'config is empty');
}
let SetResult;
// 写入配置
try {
await WebUiDataRuntime.setOB11Config(JSON.parse(req.body.config));
SetResult = true;
return sendSuccess(res, null);
} catch (e) {
SetResult = false;
return sendError(res, 'Config Set Error');
}
if (SetResult) {
res.send({
code: 0,
message: 'success',
});
} else {
res.send({
code: -1,
message: 'Config Set Error',
});
}
return;
};

View File

@@ -1,78 +1,64 @@
import { RequestHandler } from 'express';
import { WebUiDataRuntime } from '../helper/Data';
const isEmpty = (data: any) => data === undefined || data === null || data === '';
import { WebUiDataRuntime } from '@webapi/helper/Data';
import { isEmpty } from '@webapi/utils/check';
import { sendError, sendSuccess } from '@webapi/utils/response';
// 获取QQ登录二维码
export const QQGetQRcodeHandler: RequestHandler = async (req, res) => {
// 判断是否已经登录
if (await WebUiDataRuntime.getQQLoginStatus()) {
res.send({
code: -1,
message: 'QQ Is Logined',
});
return;
// 已经登录
return sendError(res, 'QQ Is Logined');
}
// 获取二维码
const qrcodeUrl = await WebUiDataRuntime.getQQLoginQrcodeURL();
// 判断二维码是否为空
if (isEmpty(qrcodeUrl)) {
res.send({
code: -1,
message: 'QRCode Get Error',
});
return;
return sendError(res, 'QRCode Get Error');
}
res.send({
code: 0,
message: 'success',
data: {
// 返回二维码URL
const data = {
qrcode: qrcodeUrl,
},
});
return;
};
return sendSuccess(res, data);
};
// 获取QQ登录状态
export const QQCheckLoginStatusHandler: RequestHandler = async (req, res) => {
res.send({
code: 0,
message: 'success',
data: {
const data = {
isLogin: await WebUiDataRuntime.getQQLoginStatus(),
qrcodeurl: await WebUiDataRuntime.getQQLoginQrcodeURL()
},
});
qrcodeurl: await WebUiDataRuntime.getQQLoginQrcodeURL(),
};
return sendSuccess(res, data);
};
// 快速登录
export const QQSetQuickLoginHandler: RequestHandler = async (req, res) => {
// 获取QQ号
const { uin } = req.body;
// 判断是否已经登录
const isLogin = await WebUiDataRuntime.getQQLoginStatus();
if (isLogin) {
res.send({
code: -1,
message: 'QQ Is Logined',
});
return;
return sendError(res, 'QQ Is Logined');
}
// 判断QQ号是否为空
if (isEmpty(uin)) {
res.send({
code: -1,
message: 'uin is empty',
});
return;
return sendError(res, 'uin is empty');
}
// 获取快速登录状态
const { result, message } = await WebUiDataRuntime.requestQuickLogin(uin);
if (!result) {
res.send({
code: -1,
message: message,
});
return;
return sendError(res, message);
}
//本来应该验证 但是http不宜这么搞 建议前端验证
//isLogin = await WebUiDataRuntime.getQQLoginStatus();
res.send({
code: 0,
message: 'success',
});
return sendSuccess(res, null);
};
export const QQGetQuickLoginListHandler: RequestHandler = async (req, res) => {
// 获取快速登录列表
export const QQGetQuickLoginListHandler: RequestHandler = async (_, res) => {
const quickLoginList = await WebUiDataRuntime.getQQQuickLoginList();
res.send({
code: 0,
data: quickLoginList,
});
return sendSuccess(res, quickLoginList);
};

View File

@@ -0,0 +1,13 @@
export enum HttpStatusCode {
OK = 200,
BadRequest = 400,
Unauthorized = 401,
Forbidden = 403,
NotFound = 404,
InternalServerError = 500,
}
export enum ResponseCode {
Success = 0,
Error = -1,
}

View File

@@ -1,18 +1,5 @@
import { OneBotConfig } from '@/onebot/config/config';
interface LoginRuntimeType {
LoginCurrentTime: number;
LoginCurrentRate: number;
QQLoginStatus: boolean;
QQQRCodeURL: string;
QQLoginUin: string;
NapCatHelper: {
onQuickLoginRequested: (uin: string) => Promise<{ result: boolean; message: string }>;
onOB11ConfigChanged: (ob11: OneBotConfig) => Promise<void>;
QQLoginList: string[];
};
}
const LoginRuntime: LoginRuntimeType = {
LoginCurrentTime: Date.now(),
LoginCurrentRate: 0,

View File

@@ -1,15 +1,5 @@
import crypto from 'crypto';
interface WebUiCredentialInnerJson {
CreatedTime: number;
TokenEncoded: string;
}
interface WebUiCredentialJson {
Data: WebUiCredentialInnerJson;
Hmac: string;
}
export class AuthHelper {
private static readonly secretKey = Math.random().toString(36).slice(2);
@@ -24,9 +14,7 @@ export class AuthHelper {
TokenEncoded: token,
};
const jsonString = JSON.stringify(innerJson);
const hmac = crypto.createHmac('sha256', AuthHelper.secretKey)
.update(jsonString, 'utf8')
.digest('hex');
const hmac = crypto.createHmac('sha256', AuthHelper.secretKey).update(jsonString, 'utf8').digest('hex');
return { Data: innerJson, Hmac: hmac };
}
@@ -38,7 +26,8 @@ export class AuthHelper {
public static async checkCredential(credentialJson: WebUiCredentialJson): Promise<boolean> {
try {
const jsonString = JSON.stringify(credentialJson.Data);
const calculatedHmac = crypto.createHmac('sha256', AuthHelper.secretKey)
const calculatedHmac = crypto
.createHmac('sha256', AuthHelper.secretKey)
.update(jsonString, 'utf8')
.digest('hex');
return calculatedHmac === credentialJson.Hmac;
@@ -53,7 +42,10 @@ export class AuthHelper {
* @param credentialJson 已签名的凭证JSON对象。
* @returns 布尔值表示凭证是否有效且token匹配。
*/
public static async validateCredentialWithinOneHour(token: string, credentialJson: WebUiCredentialJson): Promise<boolean> {
public static async validateCredentialWithinOneHour(
token: string,
credentialJson: WebUiCredentialJson
): Promise<boolean> {
const isValid = await AuthHelper.checkCredential(credentialJson);
if (!isValid) {
return false;

View File

@@ -3,7 +3,6 @@ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import * as net from 'node:net';
import { resolve } from 'node:path';
// 限制尝试端口的次数,避免死循环
const MAX_PORT_TRY = 100;
@@ -64,14 +63,6 @@ async function tryUsePort(port: number, host: string, tryCount: number = 0): Pro
});
}
export interface WebUiConfigType {
host: string;
port: number;
prefix: string;
token: string;
loginRate: number;
}
// 读取当前目录下名为 webui.json 的配置文件,如果不存在则创建初始化配置文件
export class WebUiConfigWrapper {
WebUiConfigData: WebUiConfigType | undefined = undefined;
@@ -114,14 +105,18 @@ export class WebUiConfigWrapper {
// 不希望回写的配置放后面
// 查询主机地址是否可用
const [host_err, host] = await tryUseHost(parsedConfig.host).then(data => [null, data]).catch(err => [err, null]);
const [host_err, host] = await tryUseHost(parsedConfig.host)
.then((data) => [null, data])
.catch((err) => [err, null]);
if (host_err) {
console.log('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]).catch(err => [err, null]);
const [port_err, port] = await tryUsePort(parsedConfig.port, parsedConfig.host)
.then((data) => [null, data])
.catch((err) => [err, null]);
if (port_err) {
console.log('port不可用', port_err);
parsedConfig.port = 0; // 设置为0禁用WebUI
@@ -137,4 +132,3 @@ export class WebUiConfigWrapper {
return defaultconfig; // 理论上这行代码到不了,到了只能返回默认配置了
}
}

View File

@@ -0,0 +1,46 @@
import { NextFunction, Request, Response } from 'express';
import { WebUiConfig } from '@/webui';
import { AuthHelper } from '@webapi/helper/SignToken';
import { sendError } from '@webapi/utils/response';
// 鉴权中间件
export async function auth(req: Request, res: Response, next: NextFunction) {
// 判断当前url是否为/login 如果是跳过鉴权
if (req.url == '/auth/login') {
return next();
}
// 判断是否有Authorization头
if (req.headers?.authorization) {
// 切割参数以获取token
const authorization = req.headers.authorization.split(' ');
// 当Bearer后面没有参数时
if (authorization.length < 2) {
return sendError(res, 'Unauthorized');
}
// 获取token
const token = authorization[1];
// 解析token
let Credential: WebUiCredentialJson;
try {
Credential = JSON.parse(Buffer.from(token, 'base64').toString('utf-8'));
} catch (e) {
return sendError(res, 'Unauthorized');
}
// 获取配置
const config = await WebUiConfig.GetWebUIConfig();
// 验证凭证在1小时内有效且token与原始token相同
const credentialJson = await AuthHelper.validateCredentialWithinOneHour(config.token, Credential);
if (credentialJson) {
// 通过验证
return next();
}
// 验证失败
return sendError(res, 'Unauthorized');
}
// 没有Authorization头
return sendError(res, 'Unauthorized');
}

View File

@@ -0,0 +1,9 @@
import type { RequestHandler } from 'express';
// CORS 中间件,跨域用
export const cors: RequestHandler = (_, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
next();
};

View File

@@ -1,7 +1,11 @@
import { Router } from 'express';
import { OB11GetConfigHandler, OB11SetConfigHandler } from '../api/OB11Config';
import { OB11GetConfigHandler, OB11SetConfigHandler } from '@webapi/api/OB11Config';
const router = Router();
// router:读取配置
router.post('/GetConfig', OB11GetConfigHandler);
// router:写入配置
router.post('/SetConfig', OB11SetConfigHandler);
export { router as OB11ConfigRouter };

View File

@@ -1,14 +1,20 @@
import { Router } from 'express';
import {
QQCheckLoginStatusHandler,
QQGetQRcodeHandler,
QQGetQuickLoginListHandler,
QQSetQuickLoginHandler,
} from '../api/QQLogin';
} from '@webapi/api/QQLogin';
const router = Router();
// router:获取快速登录列表
router.all('/GetQuickLoginList', QQGetQuickLoginListHandler);
// router:检查QQ登录状态
router.post('/CheckLoginStatus', QQCheckLoginStatusHandler);
// router:获取QQ登录二维码
router.post('/GetQQLoginQrcode', QQGetQRcodeHandler);
// router:设置QQ快速登录
router.post('/SetQuickLogin', QQSetQuickLoginHandler);
export { router as QQLoginRouter };

View File

@@ -1,9 +1,13 @@
import { Router } from 'express';
import { checkHandler, LoginHandler, LogoutHandler } from '../api/Auth';
import { checkHandler, LoginHandler, LogoutHandler } from '@webapi/api/Auth';
const router = Router();
// router:登录
router.post('/login', LoginHandler);
// router:检查登录状态
router.post('/check', checkHandler);
// router:注销
router.post('/logout', LogoutHandler);
export { router as AuthRouter };

View File

@@ -1,67 +1,30 @@
import { NextFunction, Request, Response, Router } from 'express';
import { AuthHelper } from '../../src/helper/SignToken';
import { QQLoginRouter } from './QQLogin';
import { AuthRouter } from './auth';
import { OB11ConfigRouter } from './OB11Config';
import { WebUiConfig } from '@/webui';
/**
* @file 所有路由的入口文件
*/
import { Router } from 'express';
import { OB11ConfigRouter } from '@webapi/router/OB11Config';
import { auth } from '@webapi/middleware/auth';
import { sendSuccess } from '@webapi/utils/response';
import { QQLoginRouter } from '@webapi/router/QQLogin';
import { AuthRouter } from '@webapi/router/auth';
const router = Router();
export async function AuthApi(req: Request, res: Response, next: NextFunction) {
//判断当前url是否为/login 如果是跳过鉴权
if (req.url == '/auth/login') {
next();
return;
}
if (req.headers?.authorization) {
const authorization = req.headers.authorization.split(' ');
if (authorization.length < 2) {
res.json({
code: -1,
msg: 'Unauthorized',
});
return;
}
const token = authorization[1];
let Credential: any;
try {
Credential = JSON.parse(Buffer.from(token, 'base64').toString('utf-8'));
} catch (e) {
res.json({
code: -1,
msg: 'Unauthorized',
});
return;
}
const config = await WebUiConfig.GetWebUIConfig();
const credentialJson = await AuthHelper.validateCredentialWithinOneHour(config.token, Credential);
if (credentialJson) {
//通过验证
next();
return;
}
res.json({
code: -1,
msg: 'Unauthorized',
});
return;
}
// 鉴权中间件
router.use(auth);
res.json({
code: -1,
msg: 'Server Error',
});
return;
}
router.use(AuthApi);
router.all('/test', (req, res) => {
res.json({
code: 0,
msg: 'ok',
});
// router:测试用
router.all('/test', (_, res) => {
return sendSuccess(res);
});
// router:WebUI登录相关路由
router.use('/auth', AuthRouter);
// router:QQ登录相关路由
router.use('/QQLogin', QQLoginRouter);
// router:OB11配置相关路由
router.use('/OB11Config', OB11ConfigRouter);
export { router as ALLRouter };

7
src/webui/src/types/config.d.ts vendored Normal file
View File

@@ -0,0 +1,7 @@
interface WebUiConfigType {
host: string;
port: number;
prefix: string;
token: string;
loginRate: number;
}

12
src/webui/src/types/data.d.ts vendored Normal file
View File

@@ -0,0 +1,12 @@
interface LoginRuntimeType {
LoginCurrentTime: number;
LoginCurrentRate: number;
QQLoginStatus: boolean;
QQQRCodeURL: string;
QQLoginUin: string;
NapCatHelper: {
onQuickLoginRequested: (uin: string) => Promise<{ result: boolean; message: string }>;
onOB11ConfigChanged: (ob11: OneBotConfig) => Promise<void>;
QQLoginList: string[];
};
}

7
src/webui/src/types/server.d.ts vendored Normal file
View File

@@ -0,0 +1,7 @@
interface APIResponse<T> {
code: number;
message: string;
data: T;
}
type Protocol = 'http' | 'https' | 'ws' | 'wss';

9
src/webui/src/types/sign_token.d.ts vendored Normal file
View File

@@ -0,0 +1,9 @@
interface WebUiCredentialInnerJson {
CreatedTime: number;
TokenEncoded: string;
}
interface WebUiCredentialJson {
Data: WebUiCredentialInnerJson;
Hmac: string;
}

View File

@@ -0,0 +1 @@
export const isEmpty = <T>(data: T) => data === undefined || data === null || data === '';

View File

@@ -0,0 +1,26 @@
import type { Response } from 'express';
import { ResponseCode, HttpStatusCode } from '@webapi/const/status';
export const sendResponse = <T>(res: Response, data?: T, code: ResponseCode = 0, message = 'success') => {
res.status(HttpStatusCode.OK).json({
code,
message,
data,
});
};
export const sendError = (res: Response, message = 'error') => {
res.status(HttpStatusCode.OK).json({
code: ResponseCode.Error,
message,
});
};
export const sendSuccess = <T>(res: Response, data?: T, message = 'success') => {
res.status(HttpStatusCode.OK).json({
code: ResponseCode.Success,
data,
message,
});
};

View File

@@ -0,0 +1,47 @@
/**
* @file URL工具
*/
import { isIP } from 'node:net';
/**
* 将 host主机地址 转换为标准格式
* @param host 主机地址
* @returns 标准格式的IP地址
* @example normalizeHost('10.0.3.2') => '10.0.3.2'
* @example normalizeHost('0.0.0.0') => '127.0.0.1'
* @example normalizeHost('2001:4860:4801:51::27') => '[2001:4860:4801:51::27]'
*/
export const normalizeHost = (host: string) => {
if (host === '0.0.0.0') return '127.0.0.1';
if (isIP(host) === 6) return `[${host}]`;
return host;
};
/**
* 创建URL
* @param host 主机地址
* @param port 端口
* @param path URL路径
* @param search URL参数
* @returns 完整URL
* @example createUrl('127.0.0.1', '8080', '/api', { token: '123456' }) => 'http://127.0.0.1:8080/api?token=123456'
* @example createUrl('baidu.com', '80', void 0, void 0, 'https') => 'https://baidu.com:80/'
*/
export const createUrl = (
host: string,
port: string,
path = '/',
search?: Record<string, any>,
protocol: Protocol = 'http'
) => {
const url = new URL(`${protocol}://${normalizeHost(host)}`);
url.port = port;
url.pathname = path;
if (search) {
for (const key in search) {
url.searchParams.set(key, search[key]);
}
}
return url.toString();
};

View File

@@ -23,9 +23,12 @@
"noFallthroughCasesInSwitch": true,
"sourceMap": true,
"paths": {
"@*": [
"./src*"
]
"@/*": [
"./src/*"
],
"@webapi/*": [
"./src/webui/src/*"
],
}
},
"include": [

View File

@@ -50,13 +50,15 @@ const ShellBaseConfigPlugin: PluginOption[] = [
nodeResolve(),
];
const ShellBaseConfig = () => defineConfig({
const ShellBaseConfig = () =>
defineConfig({
resolve: {
conditions: ['node', 'default'],
alias: {
'@/core': resolve(__dirname, './src/core'),
'@': resolve(__dirname, './src'),
'./lib-cov/fluent-ffmpeg': './lib/fluent-ffmpeg',
'@webapi': resolve(__dirname, './src/webui/src'),
},
},
build: {
@@ -65,7 +67,7 @@ const ShellBaseConfig = () => defineConfig({
minify: false,
lib: {
entry: {
'napcat': 'src/shell/napcat.ts',
napcat: 'src/shell/napcat.ts',
'audio-worker': 'src/common/audio-worker.ts',
},
formats: ['es'],
@@ -75,9 +77,10 @@ const ShellBaseConfig = () => defineConfig({
external: [...nodeModules, ...external],
},
},
});
});
const FrameworkBaseConfig = () => defineConfig({
const FrameworkBaseConfig = () =>
defineConfig({
resolve: {
conditions: ['node', 'default'],
alias: {
@@ -92,7 +95,7 @@ const FrameworkBaseConfig = () => defineConfig({
minify: false,
lib: {
entry: {
'napcat': 'src/framework/napcat.ts',
napcat: 'src/framework/napcat.ts',
'audio-worker': 'src/common/audio-worker.ts',
},
formats: ['es'],
@@ -102,7 +105,7 @@ const FrameworkBaseConfig = () => defineConfig({
external: [...nodeModules, ...external],
},
},
});
});
export default defineConfig(({ mode }): UserConfig => {
if (mode === 'shell') {