mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-03-01 16:20:25 +00:00
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:
12
packages/napcat-core/external/napcat.json
vendored
12
packages/napcat-core/external/napcat.json
vendored
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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已通过环境变量禁用');
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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() };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user