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

@@ -178,5 +178,23 @@ export default class QQManager {
public static async resetLinuxDeviceID () {
await serverRequest.post<ServerResponse<null>>('/QQLogin/ResetLinuxDeviceID');
}
// ============================================================
// NapCat 配置管理
// ============================================================
public static async getNapCatConfig () {
const { data } = await serverRequest.get<ServerResponse<NapCatConfig>>(
'/NapCatConfig/GetConfig'
);
return data.data;
}
public static async setNapCatConfig (config: Partial<NapCatConfig>) {
await serverRequest.post<ServerResponse<null>>(
'/NapCatConfig/SetConfig',
config
);
}
}

View File

@@ -0,0 +1,169 @@
import { useEffect, useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
import toast from 'react-hot-toast';
import SaveButtons from '@/components/button/save_buttons';
import PageLoading from '@/components/page_loading';
import SwitchCard from '@/components/switch_card';
import QQManager from '@/controllers/qq_manager';
interface BypassFormData {
hook: boolean;
module: boolean;
window: boolean;
js: boolean;
container: boolean;
maps: boolean;
}
const defaultBypass: BypassFormData = {
hook: true,
module: true,
window: true,
js: true,
container: true,
maps: true,
};
const BypassConfigCard = () => {
const [loading, setLoading] = useState(true);
const {
control,
handleSubmit,
formState: { isSubmitting },
setValue,
} = useForm<BypassFormData>({
defaultValues: defaultBypass,
});
const loadConfig = async (showTip = false) => {
try {
setLoading(true);
const config = await QQManager.getNapCatConfig();
const bypass = config.bypass ?? defaultBypass;
setValue('hook', bypass.hook ?? true);
setValue('module', bypass.module ?? true);
setValue('window', bypass.window ?? true);
setValue('js', bypass.js ?? true);
setValue('container', bypass.container ?? true);
setValue('maps', bypass.maps ?? true);
if (showTip) toast.success('刷新成功');
} catch (error) {
const msg = (error as Error).message;
toast.error(`获取配置失败: ${msg}`);
} finally {
setLoading(false);
}
};
const onSubmit = handleSubmit(async (data) => {
try {
await QQManager.setNapCatConfig({ bypass: data });
toast.success('保存成功,重启后生效');
} catch (error) {
const msg = (error as Error).message;
toast.error(`保存失败: ${msg}`);
}
});
const onReset = () => {
loadConfig();
};
const onRefresh = async () => {
await loadConfig(true);
};
useEffect(() => {
loadConfig();
}, []);
if (loading) return <PageLoading loading />;
return (
<>
<title> - NapCat WebUI</title>
<div className='flex flex-col gap-1 mb-2'>
<h3 className='text-lg font-semibold text-default-700'></h3>
<p className='text-sm text-default-500'>
Napi2Native
</p>
</div>
<Controller
control={control}
name='hook'
render={({ field }) => (
<SwitchCard
{...field}
label='Hook'
description='hook特征隐藏'
/>
)}
/>
<Controller
control={control}
name='module'
render={({ field }) => (
<SwitchCard
{...field}
label='Module'
description='加载模块隐藏'
/>
)}
/>
<Controller
control={control}
name='window'
render={({ field }) => (
<SwitchCard
{...field}
label='Window'
description='窗口伪造'
/>
)}
/>
<Controller
control={control}
name='js'
render={({ field }) => (
<SwitchCard
{...field}
label='JS'
description='JS Bypass保留'
/>
)}
/>
<Controller
control={control}
name='container'
render={({ field }) => (
<SwitchCard
{...field}
label='Container'
description='容器反检测'
/>
)}
/>
<Controller
control={control}
name='maps'
render={({ field }) => (
<SwitchCard
{...field}
label='Maps'
description='linux maps反检测'
/>
)}
/>
<SaveButtons
onSubmit={onSubmit}
reset={onReset}
isSubmitting={isSubmitting}
refresh={onRefresh}
/>
</>
);
};
export default BypassConfigCard;

View File

@@ -14,6 +14,7 @@ import SSLConfigCard from './ssl';
import ThemeConfigCard from './theme';
import WebUIConfigCard from './webui';
import BackupConfigCard from './backup';
import BypassConfigCard from './bypass';
export interface ConfigPageProps {
children?: React.ReactNode;
@@ -114,6 +115,11 @@ export default function ConfigPage () {
<BackupConfigCard />
</ConfigPageItem>
</Tab>
<Tab title='Bypass配置' key='bypass'>
<ConfigPageItem>
<BypassConfigCard />
</ConfigPageItem>
</Tab>
</Tabs>
</section>
);

View File

@@ -0,0 +1,19 @@
interface BypassOptions {
hook: boolean;
module: boolean;
window: boolean;
js: boolean;
container: boolean;
maps: boolean;
}
interface NapCatConfig {
fileLog: boolean;
consoleLog: boolean;
fileLogLevel: string;
consoleLogLevel: string;
packetBackend: string;
packetServer: string;
o3HookMode: number;
bypass?: BypassOptions;
}