mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-02-04 06:31:13 +00:00
Introduce support for plugin API routes that do not require WebUI authentication. Updates include:
- napcat-onebot: Add apiNoAuth route storage and helpers (apiNoAuth, getNoAuth, postNoAuth, putNoAuth, deleteNoAuth), hasApiNoAuthRoutes, buildApiNoAuthRouter, and clear handling in PluginRouterRegistryImpl.
- napcat-onebot types: Extend PluginRouterRegistry interface with no-auth API methods and document that authenticated APIs remain separate.
- napcat-webui-backend: Mount a new unauthenticated plugin route handler at /plugin/:pluginId/api that looks up the plugin router and dispatches requests to the plugin's no-auth router, returning appropriate errors when context or routes are missing.
- napcat-plugin-builtin: Add example no-auth endpoints (public/info and health) and update logger messages to reflect both auth and no-auth API paths.
- Bump napcat-types version to 0.0.16 and update napcat-plugin-builtin dependency accordingly.
These changes enable plugins to expose public endpoints (e.g. health checks or public metadata) under /plugin/{pluginId}/api/ while keeping existing authenticated APIs under /api/Plugin/ext/{pluginId}/.
541 lines
21 KiB
TypeScript
541 lines
21 KiB
TypeScript
/**
|
||
* @file WebUI服务入口文件
|
||
*/
|
||
|
||
import express from 'express';
|
||
import type { WebUiConfigType } from './src/types';
|
||
import { createServer } from 'http';
|
||
import { randomUUID } from 'node:crypto';
|
||
import { createServer as createHttpsServer } from 'https';
|
||
import { NapCatPathWrapper } from 'napcat-common/src/path';
|
||
import { WebUiConfigWrapper } from '@/napcat-webui-backend/src/helper/config';
|
||
import { ALLRouter } from '@/napcat-webui-backend/src/router';
|
||
import { cors } from '@/napcat-webui-backend/src/middleware/cors';
|
||
import { createUrl, getRandomToken } from '@/napcat-webui-backend/src/utils/url';
|
||
import { sendError } from '@/napcat-webui-backend/src/utils/response';
|
||
import { join } from 'node:path';
|
||
import { terminalManager } from '@/napcat-webui-backend/src/terminal/terminal_manager';
|
||
import multer from 'multer';
|
||
import * as net from 'node:net';
|
||
import { WebUiDataRuntime } from './src/helper/Data';
|
||
import { existsSync, readFileSync } from 'node:fs'; // 引入multer用于错误捕获
|
||
import { ILogWrapper } from 'napcat-common/src/log-interface';
|
||
import { ISubscription } from 'napcat-common/src/subscription-interface';
|
||
import { IStatusHelperSubscription } from '@/napcat-common/src/status-interface';
|
||
import { handleDebugWebSocket } from '@/napcat-webui-backend/src/api/Debug';
|
||
import compression from 'compression';
|
||
import { napCatVersion } from 'napcat-common/src/version';
|
||
import { fileURLToPath } from 'node:url';
|
||
import { dirname, resolve } from 'node:path';
|
||
import { NapCatOneBot11Adapter } from '@/napcat-onebot/index';
|
||
import { OB11PluginMangerAdapter } from '@/napcat-onebot/network/plugin-manger';
|
||
|
||
const __filename = fileURLToPath(import.meta.url);
|
||
const __dirname = dirname(__filename);
|
||
// 实例化Express
|
||
const app = express();
|
||
/**
|
||
* 初始化并启动WebUI服务。
|
||
* 该函数配置了Express服务器以支持JSON解析和静态文件服务,并监听6099端口。
|
||
* 无需参数。
|
||
* @returns {Promise<void>} 无返回值。
|
||
*/
|
||
export let WebUiConfig: WebUiConfigWrapper;
|
||
export let webUiPathWrapper: NapCatPathWrapper;
|
||
export let logSubscription: ISubscription;
|
||
export let statusHelperSubscription: IStatusHelperSubscription;
|
||
export let webUiLogger: ILogWrapper | null = null;
|
||
const MAX_PORT_TRY = 100;
|
||
|
||
export let webUiRuntimePort = 6099;
|
||
// 全局变量:存储需要在QQ登录成功后发送的新token
|
||
export let pendingTokenToSend: string | null = null;
|
||
|
||
/**
|
||
* 存储WebUI启动时的初始token,用于鉴权
|
||
* - 无论是否在运行时修改密码,都应该使用此token进行鉴权
|
||
* - 运行时手动修改的密码将会在下次napcat重启后生效
|
||
* - 如果需要在运行时修改密码并立即生效,则需要在前端调用路由进行修改
|
||
*/
|
||
let initialWebUiToken: string = '';
|
||
|
||
export function setInitialWebUiToken (token: string) {
|
||
initialWebUiToken = token;
|
||
}
|
||
|
||
export function getInitialWebUiToken (): string {
|
||
return initialWebUiToken;
|
||
}
|
||
|
||
export function setPendingTokenToSend (token: string | null) {
|
||
pendingTokenToSend = token;
|
||
}
|
||
|
||
export async function InitPort (parsedConfig: WebUiConfigType): Promise<[string, number, string]> {
|
||
try {
|
||
await tryUseHost(parsedConfig.host);
|
||
const preferredPort = parseInt(process.env['NAPCAT_WEBUI_PREFERRED_PORT'] || '', 10);
|
||
|
||
let port: number;
|
||
if (preferredPort > 0) {
|
||
try {
|
||
port = await tryUsePort(preferredPort, parsedConfig.host, 0, true);
|
||
} catch {
|
||
port = await tryUsePort(parsedConfig.port, parsedConfig.host);
|
||
}
|
||
} else {
|
||
port = await tryUsePort(parsedConfig.port, parsedConfig.host);
|
||
}
|
||
|
||
return [parsedConfig.host, port, parsedConfig.token];
|
||
} catch (error) {
|
||
console.log('host或port不可用', error);
|
||
return ['', 0, randomUUID()];
|
||
}
|
||
}
|
||
|
||
async function checkCertificates (logger: ILogWrapper): Promise<{ key: string, cert: string; } | null> {
|
||
try {
|
||
const certPath = join(webUiPathWrapper.configPath, 'cert.pem');
|
||
const keyPath = join(webUiPathWrapper.configPath, 'key.pem');
|
||
|
||
if (existsSync(certPath) && existsSync(keyPath)) {
|
||
const cert = readFileSync(certPath, 'utf8');
|
||
const key = readFileSync(keyPath, 'utf8');
|
||
logger.log('[NapCat] [WebUi] 找到SSL证书,将启用HTTPS模式');
|
||
return { cert, key };
|
||
}
|
||
return null;
|
||
} catch (error) {
|
||
logger.log('[NapCat] [WebUi] 检查SSL证书时出错: ' + error);
|
||
return null;
|
||
}
|
||
}
|
||
export async function InitWebUi (logger: ILogWrapper, pathWrapper: NapCatPathWrapper, Subscription: ISubscription, statusSubscription: IStatusHelperSubscription) {
|
||
webUiPathWrapper = pathWrapper;
|
||
logSubscription = Subscription;
|
||
statusHelperSubscription = statusSubscription;
|
||
webUiLogger = logger;
|
||
WebUiConfig = new WebUiConfigWrapper();
|
||
let config = await WebUiConfig.GetWebUIConfig();
|
||
|
||
// 检查是否禁用WebUI(若禁用则不进行密码检测)
|
||
if (config.disableWebUI) {
|
||
logger.log('[NapCat] [WebUi] WebUI is disabled by configuration.');
|
||
return;
|
||
}
|
||
|
||
// 优先使用环境变量覆盖 Token
|
||
if (process.env['NAPCAT_WEBUI_SECRET_KEY'] && config.token !== process.env['NAPCAT_WEBUI_SECRET_KEY']) {
|
||
await WebUiConfig.UpdateWebUIConfig({ token: process.env['NAPCAT_WEBUI_SECRET_KEY'] });
|
||
logger.log(`[NapCat] [WebUi] 检测到环境变量配置,已更新 WebUI Token 为 ${process.env['NAPCAT_WEBUI_SECRET_KEY']}`);
|
||
config = await WebUiConfig.GetWebUIConfig();
|
||
} else if (config.token === 'napcat' || !config.token) {
|
||
// 只有没设置环境变量,且是默认密码时,才生成随机密码
|
||
const randomToken = getRandomToken(8);
|
||
await WebUiConfig.UpdateWebUIConfig({ token: randomToken });
|
||
logger.log('[NapCat] [WebUi] 检测到默认密码,已自动更新为安全密码');
|
||
|
||
// 存储token到全局变量,等待QQ登录成功后发送
|
||
setPendingTokenToSend(randomToken);
|
||
logger.log('[NapCat] [WebUi] 新密码将在QQ登录成功后发送给用户');
|
||
|
||
// 重新获取更新后的配置
|
||
config = await WebUiConfig.GetWebUIConfig();
|
||
}
|
||
|
||
// 存储启动时的初始token用于鉴权
|
||
setInitialWebUiToken(config.token);
|
||
|
||
const [host, port, token] = await InitPort(config);
|
||
webUiRuntimePort = port;
|
||
if (port === 0) {
|
||
logger.log('[NapCat] [WebUi] Current WebUi is not run.');
|
||
return;
|
||
}
|
||
WebUiDataRuntime.setWebUiConfigQuickFunction(
|
||
async () => {
|
||
const autoLoginAccount = process.env['NAPCAT_QUICK_ACCOUNT'] || WebUiConfig.getAutoLoginAccount();
|
||
if (autoLoginAccount) {
|
||
try {
|
||
const { result, message } = await WebUiDataRuntime.requestQuickLogin(autoLoginAccount);
|
||
if (!result) {
|
||
throw new Error(message);
|
||
}
|
||
console.log(`[NapCat] [WebUi] Auto login account: ${autoLoginAccount}`);
|
||
} catch (error) {
|
||
console.log('[NapCat] [WebUi] Auto login account failed.' + error);
|
||
}
|
||
}
|
||
});
|
||
// ------------注册中间件------------
|
||
// 使用express的json中间件
|
||
app.use(express.json());
|
||
// 启用gzip压缩(对所有响应启用,阈值1KB)
|
||
app.use(compression({
|
||
level: 6, // 压缩级别 1-9,6 是性能和压缩率的平衡点
|
||
threshold: 1024, // 只压缩大于 1KB 的响应
|
||
filter: (req, res) => {
|
||
// 不压缩 SSE 和 WebSocket 升级请求
|
||
if (req.headers['accept'] === 'text/event-stream') {
|
||
return false;
|
||
}
|
||
// 使用默认过滤器
|
||
return compression.filter(req, res);
|
||
},
|
||
}));
|
||
|
||
// CORS中间件
|
||
// TODO:
|
||
app.use(cors);
|
||
|
||
// 自定义字体文件路由 - 返回用户上传的字体文件
|
||
app.use('/webui/fonts/CustomFont.woff', async (_req, res) => {
|
||
const fontPath = await WebUiConfig.GetWebUIFontPath();
|
||
if (fontPath) {
|
||
res.sendFile(fontPath);
|
||
} else {
|
||
res.status(404).send('Custom font not found');
|
||
}
|
||
});
|
||
|
||
// 如果是自定义色彩,构建一个css文件
|
||
app.use('/files/theme.css', async (_req, res) => {
|
||
const theme = await WebUiConfig.GetTheme();
|
||
const fontMode = theme.fontMode || 'system';
|
||
|
||
let css = '';
|
||
|
||
// 生成字体 @font-face
|
||
if (fontMode === 'aacute') {
|
||
css += `
|
||
@font-face {
|
||
font-family: 'Aa偷吃可爱长大的';
|
||
src: url('/webui/fonts/AaCute.woff') format('woff');
|
||
font-display: swap;
|
||
}
|
||
`;
|
||
} else if (fontMode === 'custom') {
|
||
css += `
|
||
@font-face {
|
||
font-family: 'CustomFont';
|
||
src: url('/webui/fonts/CustomFont.woff') format('woff');
|
||
font-display: swap;
|
||
}
|
||
`;
|
||
}
|
||
|
||
// 生成颜色主题和字体变量
|
||
css += ':root, .light, [data-theme="light"] {';
|
||
for (const key in theme.light) {
|
||
css += `${key}: ${theme.light[key]};`;
|
||
}
|
||
// 添加字体变量
|
||
if (fontMode === 'aacute') {
|
||
css += "--font-family-base: 'Aa偷吃可爱长大的', var(--font-family-fallbacks) !important;";
|
||
css += "--font-family-mono: 'Aa偷吃可爱长大的', var(--font-family-fallbacks) !important;";
|
||
} else if (fontMode === 'custom') {
|
||
css += "--font-family-base: 'CustomFont', var(--font-family-fallbacks) !important;";
|
||
css += "--font-family-mono: 'CustomFont', var(--font-family-fallbacks) !important;";
|
||
} else {
|
||
css += '--font-family-base: var(--font-family-fallbacks) !important;';
|
||
css += '--font-family-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace !important;';
|
||
}
|
||
css += '}';
|
||
|
||
css += '.dark, [data-theme="dark"] {';
|
||
for (const key in theme.dark) {
|
||
css += `${key}: ${theme.dark[key]};`;
|
||
}
|
||
// 添加字体变量
|
||
if (fontMode === 'aacute') {
|
||
css += "--font-family-base: 'Aa偷吃可爱长大的', var(--font-family-fallbacks) !important;";
|
||
css += "--font-family-mono: 'Aa偷吃可爱长大的', var(--font-family-fallbacks) !important;";
|
||
} else if (fontMode === 'custom') {
|
||
css += "--font-family-base: 'CustomFont', var(--font-family-fallbacks) !important;";
|
||
css += "--font-family-mono: 'CustomFont', var(--font-family-fallbacks) !important;";
|
||
} else {
|
||
css += '--font-family-base: var(--font-family-fallbacks) !important;';
|
||
css += '--font-family-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace !important;';
|
||
}
|
||
css += '}';
|
||
|
||
res.send(css);
|
||
});
|
||
|
||
// 动态生成 sw.js
|
||
app.get('/webui/sw.js', async (_req, res) => {
|
||
try {
|
||
// 读取模板文件
|
||
let templatePath = resolve(__dirname, 'static', 'sw_template.js');
|
||
if (!existsSync(templatePath)) {
|
||
templatePath = resolve(__dirname, 'src', 'assets', 'sw_template.js');
|
||
}
|
||
|
||
let swContent = readFileSync(templatePath, 'utf-8');
|
||
|
||
// 替换版本号
|
||
// 使用 napCatVersion,如果为 alpha 则尝试加上时间戳或其他标识以避免缓存冲突,或者直接使用
|
||
// 用户要求控制 sw.js 版本,napCatVersion 是核心控制点
|
||
swContent = swContent.replace('{{VERSION}}', napCatVersion);
|
||
|
||
res.header('Content-Type', 'application/javascript');
|
||
res.header('Service-Worker-Allowed', '/webui/');
|
||
res.header('Cache-Control', 'no-cache, no-store, must-revalidate');
|
||
res.send(swContent);
|
||
} catch (error) {
|
||
console.error('[NapCat] [WebUi] Error generating sw.js', error);
|
||
res.status(500).send('Error generating service worker');
|
||
}
|
||
});
|
||
|
||
// ------------中间件结束------------
|
||
|
||
// ------------挂载路由------------
|
||
// 挂载静态路由(前端),路径为 /webui
|
||
app.use('/webui', express.static(pathWrapper.staticPath, {
|
||
maxAge: '1d',
|
||
}));
|
||
|
||
// 插件内存静态资源路由(不需要鉴权)
|
||
// 路径格式: /plugin/:pluginId/mem/:urlPath/*
|
||
app.use('/plugin/:pluginId/mem', async (req, res) => {
|
||
const { pluginId } = req.params;
|
||
if (!pluginId) return res.status(400).json({ code: -1, message: 'Plugin ID is required' });
|
||
|
||
const ob11 = WebUiDataRuntime.getOneBotContext() as NapCatOneBot11Adapter | null;
|
||
if (!ob11) return res.status(503).json({ code: -1, message: 'OneBot context not available' });
|
||
|
||
const pluginManager = ob11.networkManager.findSomeAdapter('plugin_manager') as OB11PluginMangerAdapter | undefined;
|
||
if (!pluginManager) return res.status(503).json({ code: -1, message: 'Plugin manager not available' });
|
||
|
||
const routerRegistry = pluginManager.getPluginRouter(pluginId);
|
||
const memoryRoutes = routerRegistry?.getMemoryStaticRoutes() || [];
|
||
|
||
for (const { urlPath, files } of memoryRoutes) {
|
||
const prefix = urlPath.startsWith('/') ? urlPath : '/' + urlPath;
|
||
if (req.path.startsWith(prefix)) {
|
||
const filePath = '/' + (req.path.substring(prefix.length).replace(/^\//, '') || '');
|
||
const memFile = files.find(f => ('/' + f.path.replace(/^\//, '')) === filePath);
|
||
if (memFile) {
|
||
try {
|
||
const content = typeof memFile.content === 'function' ? await memFile.content() : memFile.content;
|
||
res.setHeader('Content-Type', memFile.contentType || 'application/octet-stream');
|
||
return res.send(content);
|
||
} catch (err) {
|
||
console.error(`[Plugin: ${pluginId}] Error serving memory file:`, err);
|
||
return res.status(500).json({ code: -1, message: 'Error serving memory file' });
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return res.status(404).json({ code: -1, message: 'Memory file not found' });
|
||
});
|
||
|
||
// 插件无认证 API 路由(不需要鉴权)
|
||
// 路径格式: /plugin/:pluginId/api/*
|
||
app.use('/plugin/:pluginId/api', (req, res, next) => {
|
||
const { pluginId } = req.params;
|
||
if (!pluginId) return res.status(400).json({ code: -1, message: 'Plugin ID is required' });
|
||
|
||
const ob11 = WebUiDataRuntime.getOneBotContext() as NapCatOneBot11Adapter | null;
|
||
if (!ob11) return res.status(503).json({ code: -1, message: 'OneBot context not available' });
|
||
|
||
const pluginManager = ob11.networkManager.findSomeAdapter('plugin_manager') as OB11PluginMangerAdapter | undefined;
|
||
if (!pluginManager) return res.status(503).json({ code: -1, message: 'Plugin manager not available' });
|
||
|
||
const routerRegistry = pluginManager.getPluginRouter(pluginId);
|
||
if (!routerRegistry || !routerRegistry.hasApiNoAuthRoutes()) {
|
||
return res.status(404).json({ code: -1, message: `Plugin '${pluginId}' has no registered no-auth API routes` });
|
||
}
|
||
|
||
// 构建并执行插件无认证 API 路由
|
||
const pluginRouter = routerRegistry.buildApiNoAuthRouter();
|
||
return pluginRouter(req, res, next);
|
||
});
|
||
|
||
// 插件页面路由(不需要鉴权)
|
||
// 路径格式: /plugin/:pluginId/page/:pagePath
|
||
app.get('/plugin/:pluginId/page/:pagePath', (req, res) => {
|
||
const { pluginId, pagePath } = req.params;
|
||
if (!pluginId) return res.status(400).json({ code: -1, message: 'Plugin ID is required' });
|
||
|
||
const ob11 = WebUiDataRuntime.getOneBotContext() as NapCatOneBot11Adapter | null;
|
||
if (!ob11) return res.status(503).json({ code: -1, message: 'OneBot context not available' });
|
||
|
||
const pluginManager = ob11.networkManager.findSomeAdapter('plugin_manager') as OB11PluginMangerAdapter | undefined;
|
||
if (!pluginManager) return res.status(503).json({ code: -1, message: 'Plugin manager not available' });
|
||
|
||
const routerRegistry = pluginManager.getPluginRouter(pluginId);
|
||
if (!routerRegistry || !routerRegistry.hasPages()) {
|
||
return res.status(404).json({ code: -1, message: `Plugin '${pluginId}' has no registered pages` });
|
||
}
|
||
|
||
const pages = routerRegistry.getPages();
|
||
const page = pages.find(p => p.path === '/' + pagePath || p.path === pagePath);
|
||
if (!page) {
|
||
return res.status(404).json({ code: -1, message: `Page '${pagePath}' not found in plugin '${pluginId}'` });
|
||
}
|
||
|
||
const pluginPath = routerRegistry.getPluginPath();
|
||
if (!pluginPath) {
|
||
return res.status(500).json({ code: -1, message: 'Plugin path not available' });
|
||
}
|
||
|
||
const htmlFilePath = join(pluginPath, page.htmlFile);
|
||
if (!existsSync(htmlFilePath)) {
|
||
return res.status(404).json({ code: -1, message: `HTML file not found: ${page.htmlFile}` });
|
||
}
|
||
|
||
return res.sendFile(htmlFilePath);
|
||
});
|
||
|
||
// 插件文件系统静态资源路由(不需要鉴权)
|
||
// 路径格式: /plugin/:pluginId/files/*
|
||
app.use('/plugin/:pluginId/files', (req, res, next) => {
|
||
const { pluginId } = req.params;
|
||
if (!pluginId) return res.status(400).json({ code: -1, message: 'Plugin ID is required' });
|
||
|
||
const ob11 = WebUiDataRuntime.getOneBotContext() as NapCatOneBot11Adapter | null;
|
||
if (!ob11) return res.status(503).json({ code: -1, message: 'OneBot context not available' });
|
||
|
||
const pluginManager = ob11.networkManager.findSomeAdapter('plugin_manager') as OB11PluginMangerAdapter | undefined;
|
||
if (!pluginManager) return res.status(503).json({ code: -1, message: 'Plugin manager not available' });
|
||
|
||
const routerRegistry = pluginManager.getPluginRouter(pluginId);
|
||
const staticRoutes = routerRegistry?.getStaticRoutes() || [];
|
||
|
||
for (const { urlPath, localPath } of staticRoutes) {
|
||
const prefix = urlPath.startsWith('/') ? urlPath : '/' + urlPath;
|
||
if (req.path.startsWith(prefix) || req.path === prefix.slice(0, -1)) {
|
||
const staticMiddleware = express.static(localPath, { maxAge: '1d' });
|
||
const originalUrl = req.url;
|
||
req.url = '/' + (req.path.substring(prefix.length).replace(/^\//, '') || '');
|
||
return staticMiddleware(req, res, (err) => {
|
||
req.url = originalUrl;
|
||
err ? next(err) : next();
|
||
});
|
||
}
|
||
}
|
||
res.status(404).json({ code: -1, message: 'Static resource not found' });
|
||
});
|
||
|
||
// 初始化WebSocket服务器
|
||
const sslCerts = await checkCertificates(logger);
|
||
const isHttps = !!sslCerts;
|
||
const server = isHttps && sslCerts ? createHttpsServer(sslCerts, app) : createServer(app);
|
||
server.on('upgrade', (request, socket, head) => {
|
||
const url = new URL(request.url || '', `http://${request.headers.host}`);
|
||
|
||
// 检查是否是调试 WebSocket 连接
|
||
if (url.pathname.startsWith('/api/Debug/ws')) {
|
||
handleDebugWebSocket(request, socket, head);
|
||
} else {
|
||
// 默认为终端 WebSocket
|
||
terminalManager.initialize(request, socket, head, logger);
|
||
}
|
||
});
|
||
// 挂载API接口
|
||
app.use('/api', ALLRouter);
|
||
// 所有剩下的请求都转到静态页面
|
||
const indexFile = join(pathWrapper.staticPath, 'index.html');
|
||
|
||
app.all(/\/webui\/(.*)/, (_req, res) => {
|
||
res.sendFile(indexFile);
|
||
});
|
||
|
||
// 初始服务(先放个首页)
|
||
app.all('/', (_req, res) => {
|
||
res.status(301).header('Location', '/webui').send();
|
||
});
|
||
|
||
// 错误处理中间件,捕获multer的错误
|
||
app.use((err: Error, _: express.Request, res: express.Response, next: express.NextFunction) => {
|
||
if (err instanceof multer.MulterError) {
|
||
return sendError(res, err.message, true);
|
||
}
|
||
next(err);
|
||
});
|
||
|
||
// 全局错误处理中间件(非multer错误)
|
||
app.use((_: Error, __: express.Request, res: express.Response, ___: express.NextFunction) => {
|
||
sendError(res, 'An unknown error occurred.', true);
|
||
});
|
||
|
||
// ------------启动服务------------
|
||
server.listen(port, host, async () => {
|
||
const searchParams = { token };
|
||
logger.log(`[NapCat] [WebUi] WebUi Token: ${token}`);
|
||
logger.log(
|
||
`[NapCat] [WebUi] WebUi User Panel Url: ${createUrl('127.0.0.1', port.toString(), '/webui', searchParams)}`
|
||
);
|
||
if (host !== '') {
|
||
logger.log(
|
||
`[NapCat] [WebUi] WebUi User Panel Url: ${createUrl(host, port.toString(), '/webui', searchParams)}`
|
||
);
|
||
}
|
||
});
|
||
|
||
// ------------Over!------------
|
||
}
|
||
|
||
async function tryUseHost (host: string): Promise<string> {
|
||
return new Promise((resolve, reject) => {
|
||
try {
|
||
const server = net.createServer();
|
||
server.on('listening', () => {
|
||
server.close();
|
||
resolve(host);
|
||
});
|
||
|
||
server.on('error', (err: any) => {
|
||
if (err.code === 'EADDRNOTAVAIL') {
|
||
reject(new Error('主机地址验证失败,可能为非本机地址'));
|
||
} else {
|
||
reject(new Error(`遇到错误: ${err.code}`));
|
||
}
|
||
});
|
||
|
||
// 尝试监听 让系统随机分配一个端口
|
||
server.listen(0, host);
|
||
} catch (error) {
|
||
// 这里捕获到的错误应该是启动服务器时的同步错误
|
||
reject(new Error(`服务器启动时发生错误: ${error}`));
|
||
}
|
||
});
|
||
}
|
||
|
||
async function tryUsePort (port: number, host: string, tryCount: number = 0, singleTry: boolean = false): Promise<number> {
|
||
return new Promise((resolve, reject) => {
|
||
try {
|
||
const server = net.createServer();
|
||
server.on('listening', () => {
|
||
server.close();
|
||
resolve(port);
|
||
});
|
||
|
||
server.on('error', (err: any) => {
|
||
if (err.code === 'EADDRINUSE') {
|
||
if (singleTry) {
|
||
// 只尝试一次,端口被占用则直接失败
|
||
reject(new Error(`端口 ${port} 已被占用`));
|
||
} else if (tryCount < MAX_PORT_TRY) {
|
||
// 递归尝试下一个端口
|
||
resolve(tryUsePort(port + 1, host, tryCount + 1, false));
|
||
} else {
|
||
reject(new Error(`端口尝试失败,达到最大尝试次数: ${MAX_PORT_TRY}`));
|
||
}
|
||
} else {
|
||
reject(new Error(`遇到错误: ${err.code}`));
|
||
}
|
||
});
|
||
|
||
// 尝试监听端口
|
||
server.listen(port, host);
|
||
} catch (error) {
|
||
// 这里捕获到的错误应该是启动服务器时的同步错误
|
||
reject(new Error(`服务器启动时发生错误: ${error}`));
|
||
}
|
||
});
|
||
}
|