From 8a232d8c68051d39a3a98db6f9faa4fa5426df0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=89=8B=E7=93=9C=E4=B8=80=E5=8D=81=E9=9B=AA?= Date: Sun, 18 Jan 2026 12:19:03 +0800 Subject: [PATCH] Support preferred WebUI port via environment variable Adds support for specifying a preferred WebUI port using the NAPCAT_WEBUI_PREFERRED_PORT environment variable. The shell and backend now coordinate to pass and honor this port during worker restarts, falling back to the default port if the preferred one is unavailable. --- packages/napcat-shell/napcat.ts | 38 ++++++++++++++++++-------- packages/napcat-webui-backend/index.ts | 25 +++++++++++++---- 2 files changed, 47 insertions(+), 16 deletions(-) diff --git a/packages/napcat-shell/napcat.ts b/packages/napcat-shell/napcat.ts index 298ba60a..2b513f1c 100644 --- a/packages/napcat-shell/napcat.ts +++ b/packages/napcat-shell/napcat.ts @@ -4,6 +4,7 @@ import { LogWrapper } from '@/napcat-core/helper/log'; import { connectToNamedPipe } from './pipe'; import { WebUiDataRuntime } from '@/napcat-webui-backend/src/helper/Data'; import { AuthHelper } from '@/napcat-webui-backend/src/helper/SignToken'; +import { webUiRuntimePort } from '@/napcat-webui-backend/index'; import { createProcessManager, type IProcessManager, type IWorkerProcess } from './process-api'; import path from 'path'; import { fileURLToPath } from 'url'; @@ -19,6 +20,13 @@ const ENV = { isPipeDisabled: process.env['NAPCAT_DISABLE_PIPE'] === '1', } as const; +// Worker 消息类型 +interface WorkerMessage { + type: 'restart' | 'restart-prepare' | 'shutdown'; + secretKey?: string; + port?: number; +} + // 初始化日志 const pathWrapper = new NapCatPathWrapper(); const logger = new LogWrapper(pathWrapper.logsPath); @@ -81,7 +89,7 @@ function forceKillProcess (pid: number): void { /** * 重启 Worker 进程 */ -export async function restartWorker (secretKey?: string): Promise { +export async function restartWorker (secretKey?: string, port?: number): Promise { isRestarting = true; if (!currentWorker) { @@ -129,16 +137,18 @@ export async function restartWorker (secretKey?: string): Promise { // 4. 等待后启动新进程 await new Promise(resolve => setTimeout(resolve, 3000)); - // 5. 启动新进程(重启模式不传递快速登录参数,传递密钥) - await startWorker(false, secretKey); + // 5. 启动新进程(重启模式不传递快速登录参数,传递密钥和端口) + await startWorker(false, secretKey, port); isRestarting = false; } /** * 启动 Worker 进程 * @param passQuickLogin 是否传递快速登录参数,默认为 true,重启时为 false + * @param secretKey WebUI JWT 密钥 + * @param preferredPort 优先使用的 WebUI 端口 */ -async function startWorker (passQuickLogin: boolean = true, secretKey?: string): Promise { +async function startWorker (passQuickLogin: boolean = true, secretKey?: string, preferredPort?: number): Promise { if (!processManager) { throw new Error('进程管理器未初始化'); } @@ -165,6 +175,7 @@ async function startWorker (passQuickLogin: boolean = true, secretKey?: string): ...process.env, NAPCAT_WORKER_PROCESS: '1', ...(secretKey ? { NAPCAT_WEBUI_JWT_SECRET_KEY: secretKey } : {}), + ...(preferredPort ? { NAPCAT_WEBUI_PREFERRED_PORT: String(preferredPort) } : {}), }, stdio: isElectron ? 'pipe' : ['inherit', 'pipe', 'pipe', 'ipc'], }); @@ -188,11 +199,13 @@ async function startWorker (passQuickLogin: boolean = true, secretKey?: string): // 监听子进程消息 child.on('message', (msg: unknown) => { // 处理重启请求 - if (typeof msg === 'object' && msg !== null && 'type' in msg && msg.type === 'restart') { - const secretKey = 'secretKey' in msg ? (msg as any).secretKey : undefined; - restartWorker(secretKey).catch(e => { - logger.logError(`[NapCat] [${processType}] 重启Worker进程失败:`, e); - }); + if (typeof msg === 'object' && msg !== null && 'type' in msg) { + const message = msg as WorkerMessage; + if (message.type === 'restart') { + restartWorker(message.secretKey, message.port).catch(e => { + logger.logError(`[NapCat] [${processType}] 重启Worker进程失败:`, e); + }); + } } }); @@ -282,8 +295,11 @@ async function startWorkerProcess (): Promise { // 注册重启进程函数到 WebUI WebUiDataRuntime.setRestartProcessCall(async () => { try { - const success = processManager!.sendToParent({ type: 'restart', secretKey: AuthHelper.getSecretKey() }); - + const success = processManager!.sendToParent({ + type: 'restart', + secretKey: AuthHelper.getSecretKey(), + port: webUiRuntimePort, + }); if (success) { return { result: true, message: '进程重启请求已发送' }; diff --git a/packages/napcat-webui-backend/index.ts b/packages/napcat-webui-backend/index.ts index eb1e71db..872fa615 100644 --- a/packages/napcat-webui-backend/index.ts +++ b/packages/napcat-webui-backend/index.ts @@ -72,7 +72,19 @@ export function setPendingTokenToSend (token: string | null) { export async function InitPort (parsedConfig: WebUiConfigType): Promise<[string, number, string]> { try { await tryUseHost(parsedConfig.host); - const port = await tryUsePort(parsedConfig.port, 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); @@ -356,7 +368,7 @@ async function tryUseHost (host: string): Promise { }); } -async function tryUsePort (port: number, host: string, tryCount: number = 0): Promise { +async function tryUsePort (port: number, host: string, tryCount: number = 0, singleTry: boolean = false): Promise { return new Promise((resolve, reject) => { try { const server = net.createServer(); @@ -367,9 +379,12 @@ async function tryUsePort (port: number, host: string, tryCount: number = 0): Pr server.on('error', (err: any) => { if (err.code === 'EADDRINUSE') { - if (tryCount < MAX_PORT_TRY) { - // 使用循环代替递归 - resolve(tryUsePort(port + 1, host, tryCount + 1)); + 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}`)); }