diff --git a/packages/napcat-onebot/action/index.ts b/packages/napcat-onebot/action/index.ts index b0c6842a..22fc68b6 100644 --- a/packages/napcat-onebot/action/index.ts +++ b/packages/napcat-onebot/action/index.ts @@ -86,6 +86,7 @@ import { GetGroupMemberList } from './group/GetGroupMemberList'; import { GetGroupFileUrl } from '@/napcat-onebot/action/file/GetGroupFileUrl'; import { GetPacketStatus } from '@/napcat-onebot/action/packet/GetPacketStatus'; import { GetCredentials } from './system/GetCredentials'; +import { SetRestart } from './system/SetRestart'; import { SendGroupSign, SetGroupSign } from './extends/SetGroupSign'; import { GoCQHTTPGetGroupAtAllRemain } from './go-cqhttp/GetGroupAtAllRemain'; import { GoCQHTTPCheckUrlSafely } from './go-cqhttp/GoCQHTTPCheckUrlSafely'; @@ -266,6 +267,7 @@ export function createActionMap (obContext: NapCatOneBot11Adapter, core: NapCatC new GetGroupFileSystemInfo(obContext, core), new GetGroupFilesByFolder(obContext, core), new GetPacketStatus(obContext, core), + new SetRestart(obContext, core), new GroupPoke(obContext, core), new FriendPoke(obContext, core), new GetUserStatus(obContext, core), diff --git a/packages/napcat-onebot/action/router.ts b/packages/napcat-onebot/action/router.ts index fe56d1a8..9fb2ff14 100644 --- a/packages/napcat-onebot/action/router.ts +++ b/packages/napcat-onebot/action/router.ts @@ -81,7 +81,7 @@ export const ActionName = { CanSendRecord: 'can_send_record', GetStatus: 'get_status', GetVersionInfo: 'get_version_info', - // Reboot : 'set_restart', + Reboot: 'set_restart', CleanCache: 'clean_cache', Exit: 'bot_exit', // go-cqhttp diff --git a/packages/napcat-onebot/action/system/SetRestart.ts b/packages/napcat-onebot/action/system/SetRestart.ts new file mode 100644 index 00000000..8ce33292 --- /dev/null +++ b/packages/napcat-onebot/action/system/SetRestart.ts @@ -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 { + override actionName = ActionName.Reboot; + + async _handle () { + setTimeout(() => { + writeFileSync(join(this.obContext.context.pathWrapper.binaryPath, 'napcat.restart'), Date.now().toString()); + process.exit(51); + }, 5); + } +} diff --git a/packages/napcat-shell/launcher-win.bat b/packages/napcat-shell/launcher-win.bat new file mode 100644 index 00000000..3113df5f --- /dev/null +++ b/packages/napcat-shell/launcher-win.bat @@ -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 diff --git a/packages/napcat-webui-backend/src/api/QQLogin.ts b/packages/napcat-webui-backend/src/api/QQLogin.ts index 0aaf79dd..b97afc73 100644 --- a/packages/napcat-webui-backend/src/api/QQLogin.ts +++ b/packages/napcat-webui-backend/src/api/QQLogin.ts @@ -1,9 +1,11 @@ import { RequestHandler } from 'express'; +import { writeFileSync } from 'fs'; +import { join } from 'path'; 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 { sendError, sendSuccess } from '@/napcat-webui-backend/src/utils/response'; -import { WebUiConfig } from '@/napcat-webui-backend/index'; // 获取QQ登录二维码 export const QQGetQRcodeHandler: RequestHandler = async (_, res) => { @@ -108,3 +110,12 @@ export const QQRefreshQRcodeHandler: RequestHandler = async (_, res) => { await WebUiDataRuntime.refreshQRCode(); 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); +}; diff --git a/packages/napcat-webui-backend/src/router/QQLogin.ts b/packages/napcat-webui-backend/src/router/QQLogin.ts index d5adf94d..8802915e 100644 --- a/packages/napcat-webui-backend/src/router/QQLogin.ts +++ b/packages/napcat-webui-backend/src/router/QQLogin.ts @@ -10,6 +10,7 @@ import { getAutoLoginAccountHandler, setAutoLoginAccountHandler, QQRefreshQRcodeHandler, + QQRestartHandler, } from '@/napcat-webui-backend/src/api/QQLogin'; const router = Router(); @@ -31,5 +32,7 @@ router.post('/GetQuickLoginQQ', getAutoLoginAccountHandler); router.post('/SetQuickLoginQQ', setAutoLoginAccountHandler); // router:刷新QQ登录二维码 router.post('/RefreshQRcode', QQRefreshQRcodeHandler); +// router:重启QQ +router.post('/Restart', QQRestartHandler); export { router as QQLoginRouter }; diff --git a/packages/napcat-webui-frontend/src/controllers/qq_manager.ts b/packages/napcat-webui-frontend/src/controllers/qq_manager.ts index 1b341a85..e82d0d14 100644 --- a/packages/napcat-webui-frontend/src/controllers/qq_manager.ts +++ b/packages/napcat-webui-frontend/src/controllers/qq_manager.ts @@ -1,3 +1,4 @@ +import { AxiosRequestConfig } from 'axios'; import { serverRequest } from '@/utils/request'; 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>( - '/QQLogin/GetQQLoginInfo' + '/QQLogin/GetQQLoginInfo', + {}, + config ); return data.data.data; } @@ -90,4 +93,8 @@ export default class QQManager { uin, }); } + + public static async reboot () { + await serverRequest.post>('/QQLogin/Restart'); + } } diff --git a/packages/napcat-webui-frontend/src/layouts/default.tsx b/packages/napcat-webui-frontend/src/layouts/default.tsx index 0213c7f5..da10614a 100644 --- a/packages/napcat-webui-frontend/src/layouts/default.tsx +++ b/packages/napcat-webui-frontend/src/layouts/default.tsx @@ -3,7 +3,7 @@ import { Button } from '@heroui/button'; import { useLocalStorage } from '@uidotdev/usehooks'; import clsx from 'clsx'; 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 { MdMenu, MdMenuOpen } from 'react-icons/md'; import { useLocation, useNavigate } from 'react-router-dom'; @@ -11,7 +11,7 @@ import { useLocation, useNavigate } from 'react-router-dom'; import key from '@/const/key'; import errorFallbackRender from '@/components/error_fallback'; -// import PageLoading from "@/components/Loading/PageLoading"; +import PageLoading from '@/components/page_loading'; import SideBar from '@/components/sidebar'; import useAuth from '@/hooks/auth'; @@ -52,6 +52,7 @@ const Layout: React.FC<{ children: React.ReactNode; }> = ({ children }) => { const { isAuth, revokeAuth } = useAuth(); const dialog = useDialog(); const isOnlineRef = useRef(true); + const [isRestarting, setIsRestarting] = useState(false); // 定期检查 QQ 在线状态,掉线时弹窗提示 useEffect(() => { @@ -68,7 +69,39 @@ const Layout: React.FC<{ children: React.ReactNode; }> = ({ children }) => { content: '您的 QQ 账号已下线,请重新登录。', confirmText: '重新登陆', 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: () => { revokeAuth(); navigate('/web_login'); @@ -123,6 +156,7 @@ const Layout: React.FC<{ children: React.ReactNode; }> = ({ children }) => { backgroundPosition: 'center', }} > +