Add configurable bypass options and UI

Introduce granular "bypass" configuration to control Napi2Native bypass features and expose it in the WebUI.

Key changes:
- Add bypass defaults to packages/napcat-core/external/napcat.json and BypassOptionsSchema in napcat-core helper config.
- Extend Napi2NativeLoader types: enableAllBypasses now accepts BypassOptions.
- Framework & Shell: load napcat.json (via json5), pass parsed bypass options to native loader, and log the applied config. Add json5 dependency.
- Shell: implement loadBypassConfig with a crash-recovery override (NAPCAT_BYPASS_DISABLE_LEVEL) and add master<->worker IPC (login-success) plus progressive bypass-disable strategy to mitigate repeated crashes before login.
- WebUI backend: add GET/Set endpoints for NapCat config (NapCatConfigRouter) with validation and JSON5-aware defaults.
- WebUI frontend: add BypassConfig page, types, and controller methods to get/set bypass config.
- Update package.json to include json5 and update pnpm lockfile; native binaries (.node / ffmpeg.dll) also updated.

This enables operators to tune bypass behavior per-installation and to have an in-UI control for toggling anti-detection features; it also adds progressive fallback behavior to help recover from crashes caused by bypasses.
This commit is contained in:
手瓜一十雪
2026-02-18 22:09:27 +08:00
parent 9998207346
commit b9f61cc0ee
20 changed files with 541 additions and 15 deletions

View File

@@ -20,6 +20,7 @@ import { hostname, systemVersion } from 'napcat-common/src/system';
import path from 'path';
import fs from 'fs';
import os from 'os';
import json5 from 'json5';
import { LoginListItem, NodeIKernelLoginService } from 'napcat-core/services';
import qrcode from 'napcat-qrcode/lib/main';
import { NapCatAdapterManager } from 'napcat-adapter';
@@ -30,13 +31,72 @@ import { NodeIO3MiscListener } from 'napcat-core/listeners/NodeIO3MiscListener';
import { sleep } from 'napcat-common/src/helper';
import { FFmpegService } from '@/napcat-core/helper/ffmpeg/ffmpeg';
import { NativePacketHandler } from 'napcat-core/packet/handler/client';
import { Napi2NativeLoader } from 'napcat-core/packet/handler/napi2nativeLoader';
import { Napi2NativeLoader, BypassOptions } from 'napcat-core/packet/handler/napi2nativeLoader';
import { logSubscription, LogWrapper } from '@/napcat-core/helper/log';
import { proxiedListenerOf } from '@/napcat-core/helper/proxy-handler';
import { QQBasicInfoWrapper } from '@/napcat-core/helper/qq-basic-info';
import { statusHelperSubscription } from '@/napcat-core/helper/status';
import { applyPendingUpdates } from '@/napcat-webui-backend/src/api/UpdateNapCat';
import { connectToNamedPipe } from './pipe';
/**
* 读取 napcat.json 配置中的 bypass 选项,并根据分步禁用级别覆盖
*
* 分步禁用级别 (NAPCAT_BYPASS_DISABLE_LEVEL):
* 0: 使用配置文件原始值(全部启用或用户自定义)
* 1: 强制禁用 hook
* 2: 强制禁用 hook + module
* 3: 强制禁用全部 bypass
*/
function loadBypassConfig (configPath: string, logger: LogWrapper): BypassOptions {
const defaultOptions: BypassOptions = {
hook: true,
module: true,
window: true,
js: true,
container: true,
maps: true,
};
let options = { ...defaultOptions };
try {
const configFile = path.join(configPath, 'napcat.json');
if (fs.existsSync(configFile)) {
const content = fs.readFileSync(configFile, 'utf-8');
const config = json5.parse(content);
if (config.bypass && typeof config.bypass === 'object') {
options = { ...defaultOptions, ...config.bypass };
}
}
} catch (e) {
logger.logWarn('[NapCat] 读取 bypass 配置失败,使用默认值:', e);
}
// 根据分步禁用级别覆盖配置
const disableLevel = parseInt(process.env['NAPCAT_BYPASS_DISABLE_LEVEL'] || '0', 10);
if (disableLevel > 0) {
const levelDescriptions = ['全部启用', '禁用 hook', '禁用 hook + module', '全部禁用 bypass'];
logger.logWarn(`[NapCat] 崩溃恢复:当前 bypass 禁用级别 ${disableLevel} (${levelDescriptions[disableLevel] ?? '未知'})`);
if (disableLevel >= 1) {
options.hook = false;
}
if (disableLevel >= 2) {
options.module = false;
}
if (disableLevel >= 3) {
options.hook = false;
options.module = false;
options.window = false;
options.js = false;
options.container = false;
options.maps = false;
}
}
return options;
}
// NapCat Shell App ES 入口文件
async function handleUncaughtExceptions (logger: LogWrapper) {
process.on('uncaughtException', (err) => {
@@ -406,7 +466,9 @@ export async function NCoreInitShell () {
}
// wrapper.node 加载后立刻启用 Bypass可通过环境变量禁用
if (process.env['NAPCAT_DISABLE_BYPASS'] !== '1') {
const bypassEnabled = napi2nativeLoader.nativeExports.enableAllBypasses?.();
const bypassOptions = loadBypassConfig(pathWrapper.configPath, logger);
logger.logDebug('[NapCat] Bypass 配置:', bypassOptions);
const bypassEnabled = napi2nativeLoader.nativeExports.enableAllBypasses?.(bypassOptions);
if (bypassEnabled) {
logger.log('[NapCat] Napi2NativeLoader: 已启用Bypass');
}
@@ -463,6 +525,13 @@ export async function NCoreInitShell () {
o3Service.reportAmgomWeather('login', 'a1', [dataTimestape, '0', '0']);
const selfInfo = await handleLogin(loginService, logger, pathWrapper, quickLoginUin, historyLoginList);
// 登录成功后通知 Master 进程(用于切换崩溃重试策略)
if (typeof process.send === 'function') {
process.send({ type: 'login-success' });
logger.log('[NapCat] 已通知主进程登录成功');
}
const amgomDataPiece = 'eb1fd6ac257461580dc7438eb099f23aae04ca679f4d88f53072dc56e3bb1129';
o3Service.setAmgomDataPiece(basicInfoWrapper.QQVersionAppid, new Uint8Array(Buffer.from(amgomDataPiece, 'hex')));

View File

@@ -45,7 +45,7 @@ const ENV = {
// Worker 消息类型
interface WorkerMessage {
type: 'restart' | 'restart-prepare' | 'shutdown';
type: 'restart' | 'restart-prepare' | 'shutdown' | 'login-success';
secretKey?: string;
port?: number;
}
@@ -65,6 +65,16 @@ const recentCrashTimestamps: number[] = [];
const CRASH_TIME_WINDOW = 10000; // 10秒时间窗口
const MAX_CRASHES_IN_WINDOW = 3; // 最大崩溃次数
// 分步禁用策略:记录当前禁用级别 (0-3)
// 0: 全部启用
// 1: 禁用 hook
// 2: 禁用 hook + module
// 3: 全部禁用
let bypassDisableLevel = 0;
// 是否已登录成功(登录后不再使用分步禁用策略)
let isLoggedIn = false;
/**
* 获取进程类型名称(用于日志)
*/
@@ -154,6 +164,8 @@ async function cleanupOrphanedProcesses (excludePids: number[]): Promise<void> {
*/
export async function restartWorker (secretKey?: string, port?: number): Promise<void> {
isRestarting = true;
isLoggedIn = false;
bypassDisableLevel = 0;
if (!currentWorker) {
logger.logWarn('[NapCat] [Process] 没有运行中的Worker进程');
@@ -246,6 +258,7 @@ async function startWorker (passQuickLogin: boolean = true, secretKey?: string,
NAPCAT_WORKER_PROCESS: '1',
...(secretKey ? { NAPCAT_WEBUI_JWT_SECRET_KEY: secretKey } : {}),
...(preferredPort ? { NAPCAT_WEBUI_PREFERRED_PORT: String(preferredPort) } : {}),
...(bypassDisableLevel > 0 ? { NAPCAT_BYPASS_DISABLE_LEVEL: String(bypassDisableLevel) } : {}),
},
stdio: isElectron ? 'pipe' : ['inherit', 'pipe', 'pipe', 'ipc'],
});
@@ -275,6 +288,9 @@ async function startWorker (passQuickLogin: boolean = true, secretKey?: string,
restartWorker(message.secretKey, message.port).catch(e => {
logger.logError(`[NapCat] [${processType}] 重启Worker进程失败:`, e);
});
} else if (message.type === 'login-success') {
isLoggedIn = true;
logger.log(`[NapCat] [${processType}] Worker进程已登录成功切换到正常重试策略`);
}
}
});
@@ -297,13 +313,34 @@ async function startWorker (passQuickLogin: boolean = true, secretKey?: string,
// 记录本次崩溃
recentCrashTimestamps.push(now);
// 检查是否超过崩溃阈值
if (recentCrashTimestamps.length >= MAX_CRASHES_IN_WINDOW) {
logger.logError(`[NapCat] [${processType}] Worker进程在 ${CRASH_TIME_WINDOW / 1000} 秒内异常退出 ${MAX_CRASHES_IN_WINDOW} 次,主进程退出`);
process.exit(1);
// 登录前:使用分步禁用策略
if (!isLoggedIn) {
// 每次崩溃提升禁用级别
bypassDisableLevel = Math.min(bypassDisableLevel + 1, 3);
const levelDescriptions = [
'全部启用',
'禁用 hook',
'禁用 hook + module',
'全部禁用 bypass'
];
if (bypassDisableLevel >= 3 && recentCrashTimestamps.length >= MAX_CRASHES_IN_WINDOW) {
logger.logError(`[NapCat] [${processType}] Worker进程在 ${CRASH_TIME_WINDOW / 1000} 秒内异常退出 ${MAX_CRASHES_IN_WINDOW} 次,已尝试全部禁用策略,主进程退出`);
process.exit(1);
}
logger.logWarn(`[NapCat] [${processType}] Worker进程意外退出 (${recentCrashTimestamps.length}/${MAX_CRASHES_IN_WINDOW}),切换到禁用级别 ${bypassDisableLevel}: ${levelDescriptions[bypassDisableLevel]},正在尝试重新拉起...`);
} else {
// 登录后:使用正常重试策略
if (recentCrashTimestamps.length >= MAX_CRASHES_IN_WINDOW) {
logger.logError(`[NapCat] [${processType}] Worker进程在 ${CRASH_TIME_WINDOW / 1000} 秒内异常退出 ${MAX_CRASHES_IN_WINDOW} 次,主进程退出`);
process.exit(1);
}
logger.logWarn(`[NapCat] [${processType}] Worker进程意外退出 (${recentCrashTimestamps.length}/${MAX_CRASHES_IN_WINDOW}),正在尝试重新拉起...`);
}
logger.logWarn(`[NapCat] [${processType}] Worker进程意外退出 (${recentCrashTimestamps.length}/${MAX_CRASHES_IN_WINDOW}),正在尝试重新拉起...`);
startWorker(true).catch(e => {
logger.logError(`[NapCat] [${processType}] 重新拉起Worker进程失败:`, e);
});

View File

@@ -25,6 +25,7 @@
"napcat-qrcode": "workspace:*"
},
"devDependencies": {
"json5": "^2.2.3",
"@types/node": "^22.0.1",
"napcat-vite": "workspace:*"
},