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": "",
"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
}
}

View File

@@ -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已通过环境变量禁用');

View File

@@ -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;
}

View File

@@ -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<void> {
*/
export async function restartWorker (secretKey?: string, port?: number): Promise<void> {
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,33 +300,12 @@ 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}),正在尝试重新拉起...`);
}
startWorker(true).catch(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 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<string, unknown> {
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<string, unknown> {
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() };
}
/**

View File

@@ -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<BypassFormData>({
defaultValues: defaultBypass,
});
} = useForm<BypassFormData>();
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<BypassOptions>;
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 = () => {
/>
)}
/>
<Controller
control={control}
name='o3HookMode'
render={({ field }) => (
<SwitchCard
{...field}
label='o3HookMode'
description='O3 Hook 模式'
/>
)}
/>
<SaveButtons
onSubmit={onSubmit}
reset={onReset}

View File

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

View File

@@ -61,43 +61,28 @@ export default function PluginPage () {
const handleUninstall = async (plugin: PluginItem) => {
return new Promise<void>((resolve, reject) => {
let cleanData = false;
dialog.confirm({
title: '卸载插件',
content: (
<div className="flex flex-col gap-2">
<p>{plugin.name}? </p>
<p className="text-small text-default-500"></p>
</div>
),
onConfirm: async () => {
// Ask for data cleanup
dialog.confirm({
title: '删除配置',
content: (
<div className="flex flex-col gap-2">
<p>{plugin.name}</p>
<div className="text-small text-default-500">
<p>配置目录: config/plugins/{plugin.id}</p>
<p>"确定""取消"</p>
<p className="text-base text-default-800"><span className="font-semibold text-danger">{plugin.name}</span>? </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>
),
confirmText: '清理并卸载',
cancelText: '仅卸载',
confirmText: '确定卸载',
cancelText: '取消',
onConfirm: async () => {
await performUninstall(true);
},
onCancel: async () => {
await performUninstall(false);
}
});
},
onCancel: () => {
resolve();
}
});
const performUninstall = async (cleanData: boolean) => {
const loadingToast = toast.loading('卸载中...');
try {
await PluginManager.uninstallPlugin(plugin.id, cleanData);
@@ -108,7 +93,11 @@ export default function PluginPage () {
toast.error(e.message, { id: loadingToast });
reject(e);
}
};
},
onCancel: () => {
resolve();
}
});
});
};