feat: 新增看门狗汪汪汪

This commit is contained in:
时瑾 2026-01-14 23:25:54 +08:00
parent 0918b17257
commit b97a224a14
No known key found for this signature in database
GPG Key ID: 023F70A1B8F8C196
8 changed files with 133 additions and 7 deletions

View File

@ -86,6 +86,7 @@ import { GetGroupMemberList } from './group/GetGroupMemberList';
import { GetGroupFileUrl } from '@/napcat-onebot/action/file/GetGroupFileUrl'; import { GetGroupFileUrl } from '@/napcat-onebot/action/file/GetGroupFileUrl';
import { GetPacketStatus } from '@/napcat-onebot/action/packet/GetPacketStatus'; import { GetPacketStatus } from '@/napcat-onebot/action/packet/GetPacketStatus';
import { GetCredentials } from './system/GetCredentials'; import { GetCredentials } from './system/GetCredentials';
import { SetRestart } from './system/SetRestart';
import { SendGroupSign, SetGroupSign } from './extends/SetGroupSign'; import { SendGroupSign, SetGroupSign } from './extends/SetGroupSign';
import { GoCQHTTPGetGroupAtAllRemain } from './go-cqhttp/GetGroupAtAllRemain'; import { GoCQHTTPGetGroupAtAllRemain } from './go-cqhttp/GetGroupAtAllRemain';
import { GoCQHTTPCheckUrlSafely } from './go-cqhttp/GoCQHTTPCheckUrlSafely'; import { GoCQHTTPCheckUrlSafely } from './go-cqhttp/GoCQHTTPCheckUrlSafely';
@ -266,6 +267,7 @@ export function createActionMap (obContext: NapCatOneBot11Adapter, core: NapCatC
new GetGroupFileSystemInfo(obContext, core), new GetGroupFileSystemInfo(obContext, core),
new GetGroupFilesByFolder(obContext, core), new GetGroupFilesByFolder(obContext, core),
new GetPacketStatus(obContext, core), new GetPacketStatus(obContext, core),
new SetRestart(obContext, core),
new GroupPoke(obContext, core), new GroupPoke(obContext, core),
new FriendPoke(obContext, core), new FriendPoke(obContext, core),
new GetUserStatus(obContext, core), new GetUserStatus(obContext, core),

View File

@ -81,7 +81,7 @@ export const ActionName = {
CanSendRecord: 'can_send_record', CanSendRecord: 'can_send_record',
GetStatus: 'get_status', GetStatus: 'get_status',
GetVersionInfo: 'get_version_info', GetVersionInfo: 'get_version_info',
// Reboot : 'set_restart', Reboot: 'set_restart',
CleanCache: 'clean_cache', CleanCache: 'clean_cache',
Exit: 'bot_exit', Exit: 'bot_exit',
// go-cqhttp // go-cqhttp

View File

@ -0,0 +1,15 @@
import { ActionName } from '@/napcat-onebot/action/router';
import { OneBotAction } from '../OneBotAction';
import { writeFileSync } from 'fs';
import { join } from 'path';
export class SetRestart extends OneBotAction<void, void> {
override actionName = ActionName.Reboot;
async _handle () {
setTimeout(() => {
writeFileSync(join(this.obContext.context.pathWrapper.binaryPath, 'napcat.restart'), Date.now().toString());
process.exit(51);
}, 5);
}
}

View File

@ -0,0 +1,54 @@
@echo off
chcp 65001 >nul
net session >nul 2>&1
if %ERRORLEVEL% == 0 (
echo Administrator mode detected.
) else (
echo Please run this script in administrator mode.
powershell -Command "Start-Process 'wt.exe' -ArgumentList 'cmd /k cd /d \"%cd%\" && \"%~f0\" %*' -Verb runAs"
exit
)
setlocal enabledelayedexpansion
set NAPCAT_PATCH_PACKAGE=%cd%\qqnt.json
set NAPCAT_LOAD_PATH=%cd%\loadNapCat.js
set NAPCAT_INJECT_PATH=%cd%\NapCatWinBootHook.dll
set NAPCAT_LAUNCHER_PATH=%cd%\NapCatWinBootMain.exe
set NAPCAT_MAIN_PATH=%cd%\napcat.mjs
set NAPCAT_RESTART_SIGNAL=%cd%\napcat.restart
:loop_read
for /f "tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" /v "UninstallString"') do (
set "RetString=%%~b"
goto :napcat_boot
)
:napcat_boot
for %%a in ("%RetString%") do (
set "pathWithoutUninstall=%%~dpa"
)
set "QQPath=%pathWithoutUninstall%QQ.exe"
if not exist "%QQPath%" (
echo provided QQ path is invalid
pause
exit /b
)
set "ST_MAIN_PATH=%NAPCAT_MAIN_PATH:\=/%"
echo (async () =^> {await import("file:///%ST_MAIN_PATH%")})() > "%NAPCAT_LOAD_PATH%"
if exist "%NAPCAT_RESTART_SIGNAL%" del "%NAPCAT_RESTART_SIGNAL%"
echo [%date% %time%] [Watchdog] Starting NapCat...
"%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" %*
:watchdog
timeout /t 3 /nobreak >nul
if exist "%NAPCAT_RESTART_SIGNAL%" (
echo [%date% %time%] [Watchdog] Restart signal received. Restarting...
del "%NAPCAT_RESTART_SIGNAL%"
goto napcat_boot
)
goto watchdog

View File

@ -1,9 +1,11 @@
import { RequestHandler } from 'express'; import { RequestHandler } from 'express';
import { writeFileSync } from 'fs';
import { join } from 'path';
import { WebUiDataRuntime } from '@/napcat-webui-backend/src/helper/Data'; import { WebUiDataRuntime } from '@/napcat-webui-backend/src/helper/Data';
import { webUiPathWrapper, WebUiConfig } from '@/napcat-webui-backend/index';
import { isEmpty } from '@/napcat-webui-backend/src/utils/check'; import { isEmpty } from '@/napcat-webui-backend/src/utils/check';
import { sendError, sendSuccess } from '@/napcat-webui-backend/src/utils/response'; import { sendError, sendSuccess } from '@/napcat-webui-backend/src/utils/response';
import { WebUiConfig } from '@/napcat-webui-backend/index';
// 获取QQ登录二维码 // 获取QQ登录二维码
export const QQGetQRcodeHandler: RequestHandler = async (_, res) => { export const QQGetQRcodeHandler: RequestHandler = async (_, res) => {
@ -108,3 +110,12 @@ export const QQRefreshQRcodeHandler: RequestHandler = async (_, res) => {
await WebUiDataRuntime.refreshQRCode(); await WebUiDataRuntime.refreshQRCode();
return sendSuccess(res, null); return sendSuccess(res, null);
}; };
// 退出以重启重新登录
export const QQRestartHandler: RequestHandler = async (_, res) => {
sendSuccess(res, null);
setTimeout(() => {
writeFileSync(join(webUiPathWrapper.binaryPath, 'napcat.restart'), Date.now().toString());
process.exit(51);
}, 100);
};

View File

@ -10,6 +10,7 @@ import {
getAutoLoginAccountHandler, getAutoLoginAccountHandler,
setAutoLoginAccountHandler, setAutoLoginAccountHandler,
QQRefreshQRcodeHandler, QQRefreshQRcodeHandler,
QQRestartHandler,
} from '@/napcat-webui-backend/src/api/QQLogin'; } from '@/napcat-webui-backend/src/api/QQLogin';
const router = Router(); const router = Router();
@ -31,5 +32,7 @@ router.post('/GetQuickLoginQQ', getAutoLoginAccountHandler);
router.post('/SetQuickLoginQQ', setAutoLoginAccountHandler); router.post('/SetQuickLoginQQ', setAutoLoginAccountHandler);
// router:刷新QQ登录二维码 // router:刷新QQ登录二维码
router.post('/RefreshQRcode', QQRefreshQRcodeHandler); router.post('/RefreshQRcode', QQRefreshQRcodeHandler);
// router:重启QQ
router.post('/Restart', QQRestartHandler);
export { router as QQLoginRouter }; export { router as QQLoginRouter };

View File

@ -1,3 +1,4 @@
import { AxiosRequestConfig } from 'axios';
import { serverRequest } from '@/utils/request'; import { serverRequest } from '@/utils/request';
import { SelfInfo } from '@/types/user'; import { SelfInfo } from '@/types/user';
@ -71,9 +72,11 @@ export default class QQManager {
}); });
} }
public static async getQQLoginInfo () { public static async getQQLoginInfo (config?: AxiosRequestConfig) {
const data = await serverRequest.post<ServerResponse<SelfInfo>>( const data = await serverRequest.post<ServerResponse<SelfInfo>>(
'/QQLogin/GetQQLoginInfo' '/QQLogin/GetQQLoginInfo',
{},
config
); );
return data.data.data; return data.data.data;
} }
@ -90,4 +93,8 @@ export default class QQManager {
uin, uin,
}); });
} }
public static async reboot () {
await serverRequest.post<ServerResponse<null>>('/QQLogin/Restart');
}
} }

View File

@ -3,7 +3,7 @@ import { Button } from '@heroui/button';
import { useLocalStorage } from '@uidotdev/usehooks'; import { useLocalStorage } from '@uidotdev/usehooks';
import clsx from 'clsx'; import clsx from 'clsx';
import { AnimatePresence, motion } from 'motion/react'; import { AnimatePresence, motion } from 'motion/react';
import { useEffect, useMemo, useRef } from 'react'; import { useEffect, useMemo, useRef, useState } from 'react';
import { ErrorBoundary } from 'react-error-boundary'; import { ErrorBoundary } from 'react-error-boundary';
import { MdMenu, MdMenuOpen } from 'react-icons/md'; import { MdMenu, MdMenuOpen } from 'react-icons/md';
import { useLocation, useNavigate } from 'react-router-dom'; import { useLocation, useNavigate } from 'react-router-dom';
@ -11,7 +11,7 @@ import { useLocation, useNavigate } from 'react-router-dom';
import key from '@/const/key'; import key from '@/const/key';
import errorFallbackRender from '@/components/error_fallback'; import errorFallbackRender from '@/components/error_fallback';
// import PageLoading from "@/components/Loading/PageLoading"; import PageLoading from '@/components/page_loading';
import SideBar from '@/components/sidebar'; import SideBar from '@/components/sidebar';
import useAuth from '@/hooks/auth'; import useAuth from '@/hooks/auth';
@ -52,6 +52,7 @@ const Layout: React.FC<{ children: React.ReactNode; }> = ({ children }) => {
const { isAuth, revokeAuth } = useAuth(); const { isAuth, revokeAuth } = useAuth();
const dialog = useDialog(); const dialog = useDialog();
const isOnlineRef = useRef(true); const isOnlineRef = useRef(true);
const [isRestarting, setIsRestarting] = useState(false);
// 定期检查 QQ 在线状态,掉线时弹窗提示 // 定期检查 QQ 在线状态,掉线时弹窗提示
useEffect(() => { useEffect(() => {
@ -68,7 +69,39 @@ const Layout: React.FC<{ children: React.ReactNode; }> = ({ children }) => {
content: '您的 QQ 账号已下线,请重新登录。', content: '您的 QQ 账号已下线,请重新登录。',
confirmText: '重新登陆', confirmText: '重新登陆',
cancelText: '退出账户', cancelText: '退出账户',
onConfirm: () => navigate('/qq_login'), onConfirm: async () => {
setIsRestarting(true);
try {
await QQManager.reboot();
} catch (_e) {
// 忽略错误,因为后端正在重启关闭连接
}
// 开始轮询探测后端是否启动
const startTime = Date.now();
const maxWaitTime = 15000; // 15秒总超时
const timer = setInterval(async () => {
try {
// 尝试请求后端,设置一个较短的请求超时避免挂起
await QQManager.getQQLoginInfo({ timeout: 500 });
// 如果能走到这一步说明请求成功了
clearInterval(timer);
setIsRestarting(false);
window.location.reload();
} catch (_e) {
// 如果请求失败(后端没起来),检查是否超时
if (Date.now() - startTime > maxWaitTime) {
clearInterval(timer);
setIsRestarting(false);
dialog.alert({
title: '启动超时',
content: '后端在 15 秒内未响应,请检查 NapCat 运行日志或手动重启。',
});
}
}
}, 500); // 每 500ms 探测一次
},
onCancel: () => { onCancel: () => {
revokeAuth(); revokeAuth();
navigate('/web_login'); navigate('/web_login');
@ -123,6 +156,7 @@ const Layout: React.FC<{ children: React.ReactNode; }> = ({ children }) => {
backgroundPosition: 'center', backgroundPosition: 'center',
}} }}
> >
<PageLoading loading={isRestarting} />
<SideBar <SideBar
items={menus} items={menus}
open={openSideBar} open={openSideBar}