From 41d94cd5e27d4cfc243f19a6c770d5345e1438c7 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: Fri, 20 Feb 2026 16:36:16 +0800 Subject: [PATCH] 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. --- packages/napcat-core/external/napcat.json | 12 ++-- packages/napcat-framework/napcat.ts | 15 ++-- packages/napcat-shell/base.ts | 35 +--------- packages/napcat-shell/napcat.ts | 44 ++---------- .../src/api/NapCatConfig.ts | 33 ++++----- .../src/pages/dashboard/config/bypass.tsx | 42 ++++++----- .../src/pages/dashboard/extension.tsx | 69 ++++++++++--------- .../src/pages/dashboard/plugin.tsx | 61 +++++++--------- 8 files changed, 116 insertions(+), 195 deletions(-) diff --git a/packages/napcat-core/external/napcat.json b/packages/napcat-core/external/napcat.json index 09ae21a1..9b8b28dd 100644 --- a/packages/napcat-core/external/napcat.json +++ b/packages/napcat-core/external/napcat.json @@ -7,11 +7,11 @@ "packetServer": "", "o3HookMode": 1, "bypass": { - "hook": true, - "window": true, - "module": true, - "process": true, - "container": true, - "js": true + "hook": false, + "window": false, + "module": false, + "process": false, + "container": false, + "js": false } } \ No newline at end of file diff --git a/packages/napcat-framework/napcat.ts b/packages/napcat-framework/napcat.ts index 1a1ac2dc..e243c342 100644 --- a/packages/napcat-framework/napcat.ts +++ b/packages/napcat-framework/napcat.ts @@ -47,28 +47,23 @@ export async function NCoreInitFramework ( const napi2nativeLoader = new Napi2NativeLoader({ logger }); // 初始化 Napi2NativeLoader 用于后续使用 //console.log('[NapCat] [Napi2NativeLoader]', napi2nativeLoader.nativeExports.enableAllBypasses?.()); if (process.env['NAPCAT_DISABLE_BYPASS'] !== '1') { - // 读取 napcat.json 配置 - let bypassOptions: BypassOptions = { - hook: false, - window: false, - module: false, - process: false, - container: false, - js: false, - }; + let bypassOptions: BypassOptions = {}; try { const configFile = path.join(pathWrapper.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') { - bypassOptions = { ...bypassOptions, ...config.bypass }; + bypassOptions = { ...config.bypass }; } } } catch (e) { logger.logWarn('[NapCat] 读取 napcat.json bypass 配置失败,已全部禁用:', e); } const bypassEnabled = napi2nativeLoader.nativeExports.enableAllBypasses?.(bypassOptions); + if (bypassEnabled) { + logger.log('[NapCat] Napi2NativeLoader: 已启用Bypass'); + } logger.log('[NapCat] Napi2NativeLoader: Framework模式Bypass配置:', bypassOptions); } else { logger.log('[NapCat] Napi2NativeLoader: Bypass已通过环境变量禁用'); diff --git a/packages/napcat-shell/base.ts b/packages/napcat-shell/base.ts index a2a6d553..ab8a6ac3 100644 --- a/packages/napcat-shell/base.ts +++ b/packages/napcat-shell/base.ts @@ -49,47 +49,18 @@ import { connectToNamedPipe } from './pipe'; * 3: 强制禁用全部 bypass */ function loadBypassConfig (configPath: string, logger: LogWrapper): BypassOptions { - const defaultOptions: BypassOptions = { - hook: true, - window: true, - module: true, - process: true, - container: true, - js: true, - }; - - let options = { ...defaultOptions }; + let options: BypassOptions = {}; 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 }; + options = { ...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.window = false; - options.module = false; - options.process = false; - options.container = false; - options.js = false; - } + logger.logWarn('[NapCat] 读取 bypass 配置失败:', e); } return options; } diff --git a/packages/napcat-shell/napcat.ts b/packages/napcat-shell/napcat.ts index fc54228a..488d25a2 100644 --- a/packages/napcat-shell/napcat.ts +++ b/packages/napcat-shell/napcat.ts @@ -65,15 +65,6 @@ 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; /** * 获取进程类型名称(用于日志) @@ -164,8 +155,6 @@ async function cleanupOrphanedProcesses (excludePids: number[]): Promise { */ export async function restartWorker (secretKey?: string, port?: number): Promise { isRestarting = true; - isLoggedIn = false; - bypassDisableLevel = 0; if (!currentWorker) { logger.logWarn('[NapCat] [Process] 没有运行中的Worker进程'); @@ -258,7 +247,6 @@ 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'], }); @@ -289,7 +277,6 @@ async function startWorker (passQuickLogin: boolean = true, secretKey?: string, logger.logError(`[NapCat] [${processType}] 重启Worker进程失败:`, e); }); } else if (message.type === 'login-success') { - isLoggedIn = true; logger.log(`[NapCat] [${processType}] Worker进程已登录成功,切换到正常重试策略`); } } @@ -313,34 +300,13 @@ async function startWorker (passQuickLogin: boolean = true, secretKey?: string, // 记录本次崩溃 recentCrashTimestamps.push(now); - // 登录前:使用分步禁用策略 - 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}),正在尝试重新拉起...`); + 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}),正在尝试重新拉起...`); + startWorker(true).catch(e => { logger.logError(`[NapCat] [${processType}] 重新拉起Worker进程失败:`, e); }); diff --git a/packages/napcat-webui-backend/src/api/NapCatConfig.ts b/packages/napcat-webui-backend/src/api/NapCatConfig.ts index 9c234545..f0adc41a 100644 --- a/packages/napcat-webui-backend/src/api/NapCatConfig.ts +++ b/packages/napcat-webui-backend/src/api/NapCatConfig.ts @@ -5,24 +5,17 @@ import { webUiPathWrapper } from '@/napcat-webui-backend/index'; import { sendError, sendSuccess } from '@/napcat-webui-backend/src/utils/response'; import json5 from 'json5'; -// NapCat 配置默认值 -const defaultNapcatConfig = { - fileLog: false, - consoleLog: true, - fileLogLevel: 'debug', - consoleLogLevel: 'info', - packetBackend: 'auto', - packetServer: '', - o3HookMode: 1, - bypass: { - hook: true, - window: true, - module: true, - process: true, - container: true, - js: true, - }, -}; +import Ajv from 'ajv'; +import { NapcatConfigSchema } from '@/napcat-core/helper/config'; + +// 动态获取 NapCat 配置默认值 +function getDefaultNapcatConfig (): Record { + const ajv = new Ajv({ useDefaults: true, coerceTypes: true }); + const validate = ajv.compile(NapcatConfigSchema); + const data = {}; + validate(data); + return data; +} /** * 获取 napcat 配置文件路径 @@ -39,12 +32,12 @@ function readNapcatConfig (): Record { try { if (existsSync(configPath)) { const content = readFileSync(configPath, 'utf-8'); - return { ...defaultNapcatConfig, ...json5.parse(content) }; + return { ...getDefaultNapcatConfig(), ...json5.parse(content) }; } } catch (_e) { // 读取失败,使用默认值 } - return { ...defaultNapcatConfig }; + return { ...getDefaultNapcatConfig() }; } /** diff --git a/packages/napcat-webui-frontend/src/pages/dashboard/config/bypass.tsx b/packages/napcat-webui-frontend/src/pages/dashboard/config/bypass.tsx index 22c71e91..be3aab3c 100644 --- a/packages/napcat-webui-frontend/src/pages/dashboard/config/bypass.tsx +++ b/packages/napcat-webui-frontend/src/pages/dashboard/config/bypass.tsx @@ -15,16 +15,9 @@ interface BypassFormData { process: boolean; container: boolean; js: boolean; + o3HookMode: boolean; } -const defaultBypass: BypassFormData = { - hook: true, - window: true, - module: true, - process: true, - container: true, - js: true, -}; const BypassConfigCard = () => { const [loading, setLoading] = useState(true); @@ -33,21 +26,20 @@ const BypassConfigCard = () => { handleSubmit, formState: { isSubmitting }, setValue, - } = useForm({ - defaultValues: defaultBypass, - }); + } = useForm(); const loadConfig = async (showTip = false) => { try { setLoading(true); const config = await QQManager.getNapCatConfig(); - const bypass = config.bypass ?? defaultBypass; - setValue('hook', bypass.hook ?? true); - setValue('window', bypass.window ?? true); - setValue('module', bypass.module ?? true); - setValue('process', bypass.process ?? true); - setValue('container', bypass.container ?? true); - setValue('js', bypass.js ?? true); + const bypass = config.bypass ?? {} as Partial; + setValue('hook', bypass.hook ?? false); + setValue('window', bypass.window ?? false); + setValue('module', bypass.module ?? false); + setValue('process', bypass.process ?? false); + setValue('container', bypass.container ?? false); + setValue('js', bypass.js ?? false); + setValue('o3HookMode', config.o3HookMode === 1); if (showTip) toast.success('刷新成功'); } catch (error) { const msg = (error as Error).message; @@ -59,7 +51,8 @@ const BypassConfigCard = () => { const onSubmit = handleSubmit(async (data) => { try { - await QQManager.setNapCatConfig({ bypass: data }); + const { o3HookMode, ...bypass } = data; + await QQManager.setNapCatConfig({ bypass, o3HookMode: o3HookMode ? 1 : 0 }); toast.success('保存成功,重启后生效'); } catch (error) { const msg = (error as Error).message; @@ -156,6 +149,17 @@ const BypassConfigCard = () => { /> )} /> + ( + + )} + /> {extensionPages.length > 0 && ( - setSelectedTab(key as string)} - classNames={{ - tabList: 'bg-white/40 dark:bg-black/20 backdrop-blur-md', - cursor: 'bg-white/80 dark:bg-white/10 backdrop-blur-md shadow-sm', - panel: 'hidden', - }} - > - {tabs.map((tab) => ( - - {tab.icon && {tab.icon}} - { - e.stopPropagation(); - openInNewWindow(tab.pluginId, tab.path); - }} - > - {tab.title} - - ({tab.pluginName}) - - } - /> - ))} - +
+ setSelectedTab(key as string)} + classNames={{ + tabList: 'bg-white/40 dark:bg-black/20 backdrop-blur-md flex-nowrap', + cursor: 'bg-white/80 dark:bg-white/10 backdrop-blur-md shadow-sm', + panel: 'hidden', + }} + > + {tabs.map((tab) => ( + + {tab.icon && {tab.icon}} + { + e.stopPropagation(); + openInNewWindow(tab.pluginId, tab.path); + }} + > + {tab.title} + + ({tab.pluginName}) +
+ } + /> + ))} + + )} diff --git a/packages/napcat-webui-frontend/src/pages/dashboard/plugin.tsx b/packages/napcat-webui-frontend/src/pages/dashboard/plugin.tsx index fc9cff2d..33c31ea2 100644 --- a/packages/napcat-webui-frontend/src/pages/dashboard/plugin.tsx +++ b/packages/napcat-webui-frontend/src/pages/dashboard/plugin.tsx @@ -61,54 +61,43 @@ export default function PluginPage () { const handleUninstall = async (plugin: PluginItem) => { return new Promise((resolve, reject) => { + let cleanData = false; dialog.confirm({ title: '卸载插件', content: (
-

确定要卸载插件「{plugin.name}」吗? 此操作不可恢复。

-

如果插件创建了配置文件,是否一并删除?

+

确定要卸载插件「{plugin.name}」吗? 此操作不可恢复。

+
+ +

配置目录: config/plugins/{plugin.id}

+
), + confirmText: '确定卸载', + cancelText: '取消', onConfirm: async () => { - // Ask for data cleanup - dialog.confirm({ - title: '删除配置', - content: ( -
-

是否同时清理插件「{plugin.name}」的配置文件?

-
-

配置目录: config/plugins/{plugin.id}

-

点击"确定"清理配置,点击"取消"仅卸载插件。

-
-
- ), - confirmText: '清理并卸载', - cancelText: '仅卸载', - onConfirm: async () => { - await performUninstall(true); - }, - onCancel: async () => { - await performUninstall(false); - } - }); + 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); + } }, onCancel: () => { 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); - } - }; }); };