Support preferred WebUI port via environment variable
Some checks are pending
Build NapCat Artifacts / Build-Framework (push) Waiting to run
Build NapCat Artifacts / Build-Shell (push) Waiting to run

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.
This commit is contained in:
手瓜一十雪 2026-01-18 12:19:03 +08:00
parent 7216755430
commit 8a232d8c68
2 changed files with 47 additions and 16 deletions

View File

@ -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<void> {
export async function restartWorker (secretKey?: string, port?: number): Promise<void> {
isRestarting = true;
if (!currentWorker) {
@ -129,16 +137,18 @@ export async function restartWorker (secretKey?: string): Promise<void> {
// 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<void> {
async function startWorker (passQuickLogin: boolean = true, secretKey?: string, preferredPort?: number): Promise<void> {
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<void> {
// 注册重启进程函数到 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: '进程重启请求已发送' };

View File

@ -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<string> {
});
}
async function tryUsePort (port: number, host: string, tryCount: number = 0): Promise<number> {
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();
@ -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}`));
}