Refactor bypass defaults and crash handling

Set bypass defaults to disabled and simplify loading: napcat.json default bypass flags changed to false and code now reads bypass options without merging a prior "all enabled" default. Removed the progressive bypass-disable logic and related environment variable usage, and added a log when Napi2NativeLoader enables bypasses. Web UI/backend adjustments: default NapCat config is now generated from the AJV schema; the bypass settings UI defaults to false, adds an o3HookMode toggle, and submits o3HookMode as 0/1. UX fixes: extension tabs made horizontally scrollable with fixed tab sizing, and plugin uninstall flow updated to a single confirmation dialog with an optional checkbox to remove plugin config. Overall changes aim to use safer defaults, simplify crash/restart behavior, and improve configuration and UI clarity.
This commit is contained in:
手瓜一十雪
2026-02-20 16:36:16 +08:00
parent 285d352bc8
commit 41d94cd5e2
8 changed files with 116 additions and 195 deletions

View File

@@ -7,11 +7,11 @@
"packetServer": "", "packetServer": "",
"o3HookMode": 1, "o3HookMode": 1,
"bypass": { "bypass": {
"hook": true, "hook": false,
"window": true, "window": false,
"module": true, "module": false,
"process": true, "process": false,
"container": true, "container": false,
"js": true "js": false
} }
} }

View File

@@ -47,28 +47,23 @@ export async function NCoreInitFramework (
const napi2nativeLoader = new Napi2NativeLoader({ logger }); // 初始化 Napi2NativeLoader 用于后续使用 const napi2nativeLoader = new Napi2NativeLoader({ logger }); // 初始化 Napi2NativeLoader 用于后续使用
//console.log('[NapCat] [Napi2NativeLoader]', napi2nativeLoader.nativeExports.enableAllBypasses?.()); //console.log('[NapCat] [Napi2NativeLoader]', napi2nativeLoader.nativeExports.enableAllBypasses?.());
if (process.env['NAPCAT_DISABLE_BYPASS'] !== '1') { if (process.env['NAPCAT_DISABLE_BYPASS'] !== '1') {
// 读取 napcat.json 配置 let bypassOptions: BypassOptions = {};
let bypassOptions: BypassOptions = {
hook: false,
window: false,
module: false,
process: false,
container: false,
js: false,
};
try { try {
const configFile = path.join(pathWrapper.configPath, 'napcat.json'); const configFile = path.join(pathWrapper.configPath, 'napcat.json');
if (fs.existsSync(configFile)) { if (fs.existsSync(configFile)) {
const content = fs.readFileSync(configFile, 'utf-8'); const content = fs.readFileSync(configFile, 'utf-8');
const config = json5.parse(content); const config = json5.parse(content);
if (config.bypass && typeof config.bypass === 'object') { if (config.bypass && typeof config.bypass === 'object') {
bypassOptions = { ...bypassOptions, ...config.bypass }; bypassOptions = { ...config.bypass };
} }
} }
} catch (e) { } catch (e) {
logger.logWarn('[NapCat] 读取 napcat.json bypass 配置失败,已全部禁用:', e); logger.logWarn('[NapCat] 读取 napcat.json bypass 配置失败,已全部禁用:', e);
} }
const bypassEnabled = napi2nativeLoader.nativeExports.enableAllBypasses?.(bypassOptions); const bypassEnabled = napi2nativeLoader.nativeExports.enableAllBypasses?.(bypassOptions);
if (bypassEnabled) {
logger.log('[NapCat] Napi2NativeLoader: 已启用Bypass');
}
logger.log('[NapCat] Napi2NativeLoader: Framework模式Bypass配置:', bypassOptions); logger.log('[NapCat] Napi2NativeLoader: Framework模式Bypass配置:', bypassOptions);
} else { } else {
logger.log('[NapCat] Napi2NativeLoader: Bypass已通过环境变量禁用'); logger.log('[NapCat] Napi2NativeLoader: Bypass已通过环境变量禁用');

View File

@@ -49,47 +49,18 @@ import { connectToNamedPipe } from './pipe';
* 3: 强制禁用全部 bypass * 3: 强制禁用全部 bypass
*/ */
function loadBypassConfig (configPath: string, logger: LogWrapper): BypassOptions { function loadBypassConfig (configPath: string, logger: LogWrapper): BypassOptions {
const defaultOptions: BypassOptions = { let options: BypassOptions = {};
hook: true,
window: true,
module: true,
process: true,
container: true,
js: true,
};
let options = { ...defaultOptions };
try { try {
const configFile = path.join(configPath, 'napcat.json'); const configFile = path.join(configPath, 'napcat.json');
if (fs.existsSync(configFile)) { if (fs.existsSync(configFile)) {
const content = fs.readFileSync(configFile, 'utf-8'); const content = fs.readFileSync(configFile, 'utf-8');
const config = json5.parse(content); const config = json5.parse(content);
if (config.bypass && typeof config.bypass === 'object') { if (config.bypass && typeof config.bypass === 'object') {
options = { ...defaultOptions, ...config.bypass }; options = { ...config.bypass };
} }
} }
} catch (e) { } catch (e) {
logger.logWarn('[NapCat] 读取 bypass 配置失败,使用默认值:', 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.window = false;
options.module = false;
options.process = false;
options.container = false;
options.js = false;
}
} }
return options; return options;
} }

View File

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

View File

@@ -5,24 +5,17 @@ import { webUiPathWrapper } from '@/napcat-webui-backend/index';
import { sendError, sendSuccess } from '@/napcat-webui-backend/src/utils/response'; import { sendError, sendSuccess } from '@/napcat-webui-backend/src/utils/response';
import json5 from 'json5'; import json5 from 'json5';
// NapCat 配置默认值 import Ajv from 'ajv';
const defaultNapcatConfig = { import { NapcatConfigSchema } from '@/napcat-core/helper/config';
fileLog: false,
consoleLog: true, // 动态获取 NapCat 配置默认值
fileLogLevel: 'debug', function getDefaultNapcatConfig (): Record<string, unknown> {
consoleLogLevel: 'info', const ajv = new Ajv({ useDefaults: true, coerceTypes: true });
packetBackend: 'auto', const validate = ajv.compile(NapcatConfigSchema);
packetServer: '', const data = {};
o3HookMode: 1, validate(data);
bypass: { return data;
hook: true, }
window: true,
module: true,
process: true,
container: true,
js: true,
},
};
/** /**
* 获取 napcat 配置文件路径 * 获取 napcat 配置文件路径
@@ -39,12 +32,12 @@ function readNapcatConfig (): Record<string, unknown> {
try { try {
if (existsSync(configPath)) { if (existsSync(configPath)) {
const content = readFileSync(configPath, 'utf-8'); const content = readFileSync(configPath, 'utf-8');
return { ...defaultNapcatConfig, ...json5.parse(content) }; return { ...getDefaultNapcatConfig(), ...json5.parse(content) };
} }
} catch (_e) { } catch (_e) {
// 读取失败,使用默认值 // 读取失败,使用默认值
} }
return { ...defaultNapcatConfig }; return { ...getDefaultNapcatConfig() };
} }
/** /**

View File

@@ -15,16 +15,9 @@ interface BypassFormData {
process: boolean; process: boolean;
container: boolean; container: boolean;
js: boolean; js: boolean;
o3HookMode: boolean;
} }
const defaultBypass: BypassFormData = {
hook: true,
window: true,
module: true,
process: true,
container: true,
js: true,
};
const BypassConfigCard = () => { const BypassConfigCard = () => {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -33,21 +26,20 @@ const BypassConfigCard = () => {
handleSubmit, handleSubmit,
formState: { isSubmitting }, formState: { isSubmitting },
setValue, setValue,
} = useForm<BypassFormData>({ } = useForm<BypassFormData>();
defaultValues: defaultBypass,
});
const loadConfig = async (showTip = false) => { const loadConfig = async (showTip = false) => {
try { try {
setLoading(true); setLoading(true);
const config = await QQManager.getNapCatConfig(); const config = await QQManager.getNapCatConfig();
const bypass = config.bypass ?? defaultBypass; const bypass = config.bypass ?? {} as Partial<BypassOptions>;
setValue('hook', bypass.hook ?? true); setValue('hook', bypass.hook ?? false);
setValue('window', bypass.window ?? true); setValue('window', bypass.window ?? false);
setValue('module', bypass.module ?? true); setValue('module', bypass.module ?? false);
setValue('process', bypass.process ?? true); setValue('process', bypass.process ?? false);
setValue('container', bypass.container ?? true); setValue('container', bypass.container ?? false);
setValue('js', bypass.js ?? true); setValue('js', bypass.js ?? false);
setValue('o3HookMode', config.o3HookMode === 1);
if (showTip) toast.success('刷新成功'); if (showTip) toast.success('刷新成功');
} catch (error) { } catch (error) {
const msg = (error as Error).message; const msg = (error as Error).message;
@@ -59,7 +51,8 @@ const BypassConfigCard = () => {
const onSubmit = handleSubmit(async (data) => { const onSubmit = handleSubmit(async (data) => {
try { try {
await QQManager.setNapCatConfig({ bypass: data }); const { o3HookMode, ...bypass } = data;
await QQManager.setNapCatConfig({ bypass, o3HookMode: o3HookMode ? 1 : 0 });
toast.success('保存成功,重启后生效'); toast.success('保存成功,重启后生效');
} catch (error) { } catch (error) {
const msg = (error as Error).message; const msg = (error as Error).message;
@@ -156,6 +149,17 @@ const BypassConfigCard = () => {
/> />
)} )}
/> />
<Controller
control={control}
name='o3HookMode'
render={({ field }) => (
<SwitchCard
{...field}
label='o3HookMode'
description='O3 Hook 模式'
/>
)}
/>
<SaveButtons <SaveButtons
onSubmit={onSubmit} onSubmit={onSubmit}
reset={onReset} reset={onReset}

View File

@@ -115,39 +115,42 @@ export default function ExtensionPage () {
</Button> </Button>
</div> </div>
{extensionPages.length > 0 && ( {extensionPages.length > 0 && (
<Tabs <div className='max-w-full overflow-x-auto overflow-y-hidden pb-1 -mb-1'>
aria-label='Extension Pages' <Tabs
className='max-w-full' aria-label='Extension Pages'
selectedKey={selectedTab} className='min-w-max'
onSelectionChange={(key) => setSelectedTab(key as string)} selectedKey={selectedTab}
classNames={{ onSelectionChange={(key) => setSelectedTab(key as string)}
tabList: 'bg-white/40 dark:bg-black/20 backdrop-blur-md', classNames={{
cursor: 'bg-white/80 dark:bg-white/10 backdrop-blur-md shadow-sm', tabList: 'bg-white/40 dark:bg-black/20 backdrop-blur-md flex-nowrap',
panel: 'hidden', cursor: 'bg-white/80 dark:bg-white/10 backdrop-blur-md shadow-sm',
}} panel: 'hidden',
> }}
{tabs.map((tab) => ( >
<Tab {tabs.map((tab) => (
key={tab.key} <Tab
title={ key={tab.key}
<div className='flex items-center gap-2'> className='shrink-0'
{tab.icon && <span>{tab.icon}</span>} title={
<span <div className='flex items-center gap-2'>
className='cursor-pointer hover:underline truncate max-w-[6rem] md:max-w-none' {tab.icon && <span>{tab.icon}</span>}
title={`插件:${tab.pluginName}\n点击在新窗口打开`} <span
onClick={(e) => { className='cursor-pointer hover:underline truncate max-w-[6rem] md:max-w-none shrink-0'
e.stopPropagation(); title={`插件:${tab.pluginName}\n点击在新窗口打开`}
openInNewWindow(tab.pluginId, tab.path); onClick={(e) => {
}} e.stopPropagation();
> openInNewWindow(tab.pluginId, tab.path);
{tab.title} }}
</span> >
<span className='text-xs text-default-400 hidden md:inline'>({tab.pluginName})</span> {tab.title}
</div> </span>
} <span className='text-xs text-default-400 hidden md:inline shrink-0'>({tab.pluginName})</span>
/> </div>
))} }
</Tabs> />
))}
</Tabs>
</div>
)} )}
</div> </div>

View File

@@ -61,54 +61,43 @@ export default function PluginPage () {
const handleUninstall = async (plugin: PluginItem) => { const handleUninstall = async (plugin: PluginItem) => {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
let cleanData = false;
dialog.confirm({ dialog.confirm({
title: '卸载插件', title: '卸载插件',
content: ( content: (
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<p>{plugin.name}? </p> <p className="text-base text-default-800"><span className="font-semibold text-danger">{plugin.name}</span>? </p>
<p className="text-small text-default-500"></p> <div className="mt-2 bg-default-100 dark:bg-default-50/10 p-3 rounded-lg flex flex-col gap-1">
<label className="flex items-center gap-2 cursor-pointer w-fit">
<input
type="checkbox"
onChange={(e) => { cleanData = e.target.checked; }}
className="w-4 h-4 cursor-pointer accent-danger"
/>
<span className="text-small font-medium text-default-700"></span>
</label>
<p className="text-xs text-default-500 pl-6 break-all w-full">配置目录: config/plugins/{plugin.id}</p>
</div>
</div> </div>
), ),
confirmText: '确定卸载',
cancelText: '取消',
onConfirm: async () => { onConfirm: async () => {
// Ask for data cleanup const loadingToast = toast.loading('卸载中...');
dialog.confirm({ try {
title: '删除配置', await PluginManager.uninstallPlugin(plugin.id, cleanData);
content: ( toast.success('卸载成功', { id: loadingToast });
<div className="flex flex-col gap-2"> loadPlugins();
<p>{plugin.name}</p> resolve();
<div className="text-small text-default-500"> } catch (e: any) {
<p>配置目录: config/plugins/{plugin.id}</p> toast.error(e.message, { id: loadingToast });
<p>"确定""取消"</p> reject(e);
</div> }
</div>
),
confirmText: '清理并卸载',
cancelText: '仅卸载',
onConfirm: async () => {
await performUninstall(true);
},
onCancel: async () => {
await performUninstall(false);
}
});
}, },
onCancel: () => { onCancel: () => {
resolve(); resolve();
} }
}); });
const performUninstall = async (cleanData: boolean) => {
const loadingToast = toast.loading('卸载中...');
try {
await PluginManager.uninstallPlugin(plugin.id, cleanData);
toast.success('卸载成功', { id: loadingToast });
loadPlugins();
resolve();
} catch (e: any) {
toast.error(e.message, { id: loadingToast });
reject(e);
}
};
}); });
}; };