feat: 优化离线重连机制,支持通过前端实现重新登录

* feat: 优化离线重连机制,增加前端登录错误提示与二维码刷新功能

- 增加全局掉线检测弹窗
- 增强登录错误解析,支持显示 serverErrorCode 和 message
- 优化二维码登录 UI,错误时显示详细原因并提供大按钮重新获取
- 核心层解耦,通过事件抛出 KickedOffLine 通知
- 支持前端点击刷新二维码接口

* feat: 新增看门狗汪汪汪

* cp napcat-shell-loader/launcher-win.bat

* refactor: 重构重启流程,移除旧的重启逻辑,新增基于 WebUI 的重启请求处理

* fix: 刷新二维码清楚错误信息
This commit is contained in:
时瑾
2026-01-17 15:38:24 +08:00
committed by GitHub
parent 3cde32dd9f
commit 59d1ea28ab
21 changed files with 374 additions and 59 deletions

View File

@@ -10,6 +10,7 @@ import PageLoading from '@/components/page_loading';
import QQManager from '@/controllers/qq_manager';
import ProcessManager from '@/controllers/process_manager';
import { waitForBackendReady } from '@/utils/process_utils';
const LoginConfigCard = () => {
const [isRestarting, setIsRestarting] = useState(false);
@@ -60,11 +61,24 @@ const LoginConfigCard = () => {
setIsRestarting(true);
try {
const result = await ProcessManager.restartProcess();
toast.success(result.message || '进程重启成功');
// 等待 5 秒后刷新页面
setTimeout(() => {
window.location.reload();
}, 5000);
toast.success(result.message || '进程重启请求已发送');
// 轮询探测后端是否恢复
const isReady = await waitForBackendReady(
30000, // 30秒超时
() => {
setIsRestarting(false);
toast.success('进程重启完成');
},
() => {
setIsRestarting(false);
toast.error('后端在 30 秒内未响应,请检查 NapCat 运行日志');
}
);
if (!isReady) {
setIsRestarting(false);
}
} catch (error) {
const msg = (error as Error).message;
toast.error(`进程重启失败: ${msg}`);
@@ -114,7 +128,7 @@ const LoginConfigCard = () => {
{isRestarting ? '正在重启进程...' : '重启进程'}
</Button>
<div className='mt-2 text-xs text-default-500'>
Worker 3 5
Worker 3
</div>
</div>
</>

View File

@@ -16,14 +16,39 @@ import type { QQItem } from '@/components/quick_login';
import { ThemeSwitch } from '@/components/theme-switch';
import QQManager from '@/controllers/qq_manager';
import useDialog from '@/hooks/use-dialog';
import PureLayout from '@/layouts/pure';
import { motion } from 'motion/react';
const parseLoginError = (errorStr: string) => {
if (errorStr.startsWith('登录失败: ')) {
const jsonPart = errorStr.substring('登录失败: '.length);
try {
const parsed = JSON.parse(jsonPart);
if (Array.isArray(parsed) && parsed[1]) {
const info = parsed[1];
const codeStr = info.serverErrorCode ? ` (错误码: ${info.serverErrorCode})` : '';
return `${info.message || errorStr}${codeStr}`;
}
} catch (e) {
// 忽略解析错误
}
}
return errorStr;
};
export default function QQLoginPage () {
const navigate = useNavigate();
const dialog = useDialog();
const [uinValue, setUinValue] = useState<string>('');
const [isLoading, setIsLoading] = useState<boolean>(false);
const [qrcode, setQrcode] = useState<string>('');
const [loginError, setLoginError] = useState<string>('');
const lastErrorRef = useRef<string>('');
const [qqList, setQQList] = useState<(QQItem | LoginListItem)[]>([]);
const [refresh, setRefresh] = useState<boolean>(false);
const firstLoad = useRef<boolean>(true);
@@ -61,6 +86,20 @@ export default function QQLoginPage () {
navigate('/', { replace: true });
} else {
setQrcode(data.qrcodeurl);
if (data.loginError && data.loginError !== lastErrorRef.current) {
lastErrorRef.current = data.loginError;
setLoginError(data.loginError);
const friendlyMsg = parseLoginError(data.loginError);
dialog.alert({
title: '登录失败',
content: friendlyMsg,
confirmText: '确定',
});
} else if (!data.loginError) {
lastErrorRef.current = '';
setLoginError('');
}
}
} catch (error) {
const msg = (error as Error).message;
@@ -99,6 +138,18 @@ export default function QQLoginPage () {
setUinValue(e.target.value);
};
const onRefreshQRCode = async () => {
try {
lastErrorRef.current = '';
setLoginError('');
await QQManager.refreshQRCode();
toast.success('已发送刷新请求');
} catch (error) {
const msg = (error as Error).message;
toast.error(`刷新二维码失败: ${msg}`);
}
};
useEffect(() => {
const timer = setInterval(() => {
onUpdateQrCode();
@@ -159,7 +210,11 @@ export default function QQLoginPage () {
/>
</Tab>
<Tab key='qrcode' title='扫码登录'>
<QrCodeLogin qrcode={qrcode} />
<QrCodeLogin
loginError={parseLoginError(loginError)}
qrcode={qrcode}
onRefresh={onRefreshQRCode}
/>
</Tab>
</Tabs>
<Button