From 74b1da67d8ef98f6f022e96336aca3dc2192c1d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=89=8B=E7=93=9C=E4=B8=80=E5=8D=81=E9=9B=AA?= Date: Mon, 2 Feb 2026 19:48:31 +0800 Subject: [PATCH] Add password login support to web UI and backend Implement password-based QQ login across the stack: add a PasswordLogin React component, integrate it into the QQ login page, and add a frontend controller method to call a new /QQLogin/PasswordLogin API. On the backend, add QQPasswordLoginHandler, router entry, and WebUiDataRuntime hooks (setPasswordLoginCall / requestPasswordLogin) plus a default handler. Register a password login callback in the shell (base.ts) that calls the kernel login service, handles common error cases and falls back to QR code when needed. Update types to include onPasswordLoginRequested and adjust NodeIKernelLoginService method signatures (including passwordLogin return type changed to Promise) and minor formatting fixes. --- .../services/NodeIKernelLoginService.ts | 30 ++--- packages/napcat-shell/base.ts | 46 +++++++ .../napcat-webui-backend/src/api/QQLogin.ts | 26 ++++ .../napcat-webui-backend/src/helper/Data.ts | 11 ++ .../src/router/QQLogin.ts | 3 + .../napcat-webui-backend/src/types/index.ts | 1 + .../src/components/password_login.tsx | 122 ++++++++++++++++++ .../src/controllers/qq_manager.ts | 7 + .../src/pages/qq_login.tsx | 43 +++++- 9 files changed, 269 insertions(+), 20 deletions(-) create mode 100644 packages/napcat-webui-frontend/src/components/password_login.tsx diff --git a/packages/napcat-core/services/NodeIKernelLoginService.ts b/packages/napcat-core/services/NodeIKernelLoginService.ts index fbcf19e8..a611bb7e 100644 --- a/packages/napcat-core/services/NodeIKernelLoginService.ts +++ b/packages/napcat-core/services/NodeIKernelLoginService.ts @@ -21,7 +21,7 @@ export interface PasswordLoginRetType { jumpWord: string; tipsTitle: string; tipsContent: string; - } + }; } export interface PasswordLoginArgType { @@ -55,37 +55,37 @@ export interface QuickLoginResult { jumpUrl: string, jumpWord: string, tipsTitle: string, - tipsContent: string + tipsContent: string; }; } export interface NodeIKernelLoginService { getMsfStatus: () => number; - setLoginMiscData(arg0: string, value: string): unknown; + setLoginMiscData (arg0: string, value: string): unknown; - getMachineGuid(): string; + getMachineGuid (): string; - get(): NodeIKernelLoginService; + get (): NodeIKernelLoginService; - connect(): boolean; + connect (): boolean; - addKernelLoginListener(listener: NodeIKernelLoginListener): number; + addKernelLoginListener (listener: NodeIKernelLoginListener): number; - removeKernelLoginListener(listener: number): void; + removeKernelLoginListener (listener: number): void; - initConfig(config: LoginInitConfig): void; + initConfig (config: LoginInitConfig): void; - getLoginMiscData(data: string): Promise; + getLoginMiscData (data: string): Promise; - getLoginList(): Promise<{ + getLoginList (): Promise<{ result: number, // 0是ok - LocalLoginInfoList: LoginListItem[] + LocalLoginInfoList: LoginListItem[]; }>; - quickLoginWithUin(uin: string): Promise; + quickLoginWithUin (uin: string): Promise; - passwordLogin(param: PasswordLoginArgType): Promise; + passwordLogin (param: PasswordLoginArgType): Promise; - getQRCodePicture(): boolean; + getQRCodePicture (): boolean; } diff --git a/packages/napcat-shell/base.ts b/packages/napcat-shell/base.ts index 7bce0fac..f9be199f 100644 --- a/packages/napcat-shell/base.ts +++ b/packages/napcat-shell/base.ts @@ -220,6 +220,52 @@ async function handleLoginInner (context: { isLogined: boolean; }, logger: LogWr } }); }); + + // 注册密码登录回调 + WebUiDataRuntime.setPasswordLoginCall(async (uin: string, passwordMd5: string) => { + return await new Promise((resolve) => { + if (uin && passwordMd5) { + logger.log('正在密码登录 ', uin); + loginService.passwordLogin({ + uin, + passwordMd5, + step: 0, + newDeviceLoginSig: '', + proofWaterSig: '', + proofWaterRand: '', + proofWaterSid: '', + }).then(res => { + if (res.result === '140022008') { + const errMsg = '需要验证码,暂不支持'; + WebUiDataRuntime.setQQLoginError(errMsg); + loginService.getQRCodePicture(); + resolve({ result: false, message: errMsg }); + } else if (res.result === '140022010') { + const errMsg = '新设备需要扫码登录,暂不支持'; + WebUiDataRuntime.setQQLoginError(errMsg); + loginService.getQRCodePicture(); + resolve({ result: false, message: errMsg }); + } else if (res.result !== '0') { + const errMsg = res.loginErrorInfo?.errMsg || '密码登录失败'; + WebUiDataRuntime.setQQLoginError(errMsg); + loginService.getQRCodePicture(); + resolve({ result: false, message: errMsg }); + } else { + WebUiDataRuntime.setQQLoginStatus(true); + WebUiDataRuntime.setQQLoginError(''); + resolve({ result: true, message: '' }); + } + }).catch((e) => { + logger.logError(e); + WebUiDataRuntime.setQQLoginError('密码登录发生错误'); + loginService.getQRCodePicture(); + resolve({ result: false, message: '密码登录发生错误' }); + }); + } else { + resolve({ result: false, message: '密码登录失败:参数不完整' }); + } + }); + }); if (quickLoginUin) { if (historyLoginList.some(u => u.uin === quickLoginUin)) { logger.log('正在快速登录 ', quickLoginUin); diff --git a/packages/napcat-webui-backend/src/api/QQLogin.ts b/packages/napcat-webui-backend/src/api/QQLogin.ts index 423a899f..4eb1d832 100644 --- a/packages/napcat-webui-backend/src/api/QQLogin.ts +++ b/packages/napcat-webui-backend/src/api/QQLogin.ts @@ -108,3 +108,29 @@ export const QQRefreshQRcodeHandler: RequestHandler = async (_, res) => { await WebUiDataRuntime.refreshQRCode(); return sendSuccess(res, null); }; + +// 密码登录 +export const QQPasswordLoginHandler: RequestHandler = async (req, res) => { + // 获取QQ号和密码MD5 + const { uin, passwordMd5 } = req.body; + // 判断是否已经登录 + const isLogin = WebUiDataRuntime.getQQLoginStatus(); + if (isLogin) { + return sendError(res, 'QQ Is Logined'); + } + // 判断QQ号是否为空 + if (isEmpty(uin)) { + return sendError(res, 'uin is empty'); + } + // 判断密码MD5是否为空 + if (isEmpty(passwordMd5)) { + return sendError(res, 'passwordMd5 is empty'); + } + + // 执行密码登录 + const { result, message } = await WebUiDataRuntime.requestPasswordLogin(uin, passwordMd5); + if (!result) { + return sendError(res, message); + } + return sendSuccess(res, null); +}; diff --git a/packages/napcat-webui-backend/src/helper/Data.ts b/packages/napcat-webui-backend/src/helper/Data.ts index 8fb4a615..cc8092a7 100644 --- a/packages/napcat-webui-backend/src/helper/Data.ts +++ b/packages/napcat-webui-backend/src/helper/Data.ts @@ -33,6 +33,9 @@ const LoginRuntime: LoginRuntimeType = { onQuickLoginRequested: async () => { return { result: false, message: '' }; }, + onPasswordLoginRequested: async () => { + return { result: false, message: '密码登录功能未初始化' }; + }, onRestartProcessRequested: async () => { return { result: false, message: '重启功能未初始化' }; }, @@ -136,6 +139,14 @@ export const WebUiDataRuntime = { return LoginRuntime.NapCatHelper.onQuickLoginRequested(uin); } as LoginRuntimeType['NapCatHelper']['onQuickLoginRequested'], + setPasswordLoginCall (func: LoginRuntimeType['NapCatHelper']['onPasswordLoginRequested']): void { + LoginRuntime.NapCatHelper.onPasswordLoginRequested = func; + }, + + requestPasswordLogin: function (uin: string, passwordMd5: string) { + return LoginRuntime.NapCatHelper.onPasswordLoginRequested(uin, passwordMd5); + } as LoginRuntimeType['NapCatHelper']['onPasswordLoginRequested'], + setOnOB11ConfigChanged (func: LoginRuntimeType['NapCatHelper']['onOB11ConfigChanged']): void { LoginRuntime.NapCatHelper.onOB11ConfigChanged = func; }, diff --git a/packages/napcat-webui-backend/src/router/QQLogin.ts b/packages/napcat-webui-backend/src/router/QQLogin.ts index 3ceb63b6..81b0fe91 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, + QQPasswordLoginHandler, } from '@/napcat-webui-backend/src/api/QQLogin'; const router: Router = Router(); @@ -31,5 +32,7 @@ router.post('/GetQuickLoginQQ', getAutoLoginAccountHandler); router.post('/SetQuickLoginQQ', setAutoLoginAccountHandler); // router:刷新QQ登录二维码 router.post('/RefreshQRcode', QQRefreshQRcodeHandler); +// router:密码登录 +router.post('/PasswordLogin', QQPasswordLoginHandler); export { router as QQLoginRouter }; diff --git a/packages/napcat-webui-backend/src/types/index.ts b/packages/napcat-webui-backend/src/types/index.ts index 96f09fe2..f19259be 100644 --- a/packages/napcat-webui-backend/src/types/index.ts +++ b/packages/napcat-webui-backend/src/types/index.ts @@ -56,6 +56,7 @@ export interface LoginRuntimeType { OneBotContext: any | null; // OneBot 上下文,用于调试功能 NapCatHelper: { onQuickLoginRequested: (uin: string) => Promise<{ result: boolean; message: string; }>; + onPasswordLoginRequested: (uin: string, passwordMd5: string) => Promise<{ result: boolean; message: string; }>; onOB11ConfigChanged: (ob11: OneBotConfig) => Promise; onRestartProcessRequested: () => Promise<{ result: boolean; message: string; }>; QQLoginList: string[]; diff --git a/packages/napcat-webui-frontend/src/components/password_login.tsx b/packages/napcat-webui-frontend/src/components/password_login.tsx new file mode 100644 index 00000000..0f46f6ea --- /dev/null +++ b/packages/napcat-webui-frontend/src/components/password_login.tsx @@ -0,0 +1,122 @@ +import { Avatar } from '@heroui/avatar'; +import { Button } from '@heroui/button'; +import { Dropdown, DropdownItem, DropdownMenu, DropdownTrigger } from '@heroui/dropdown'; +import { Image } from '@heroui/image'; +import { Input } from '@heroui/input'; +import { useState } from 'react'; +import { toast } from 'react-hot-toast'; +import { IoChevronDown } from 'react-icons/io5'; + +import type { QQItem } from '@/components/quick_login'; +import { isQQQuickNewItem } from '@/utils/qq'; + +interface PasswordLoginProps { + onSubmit: (uin: string, password: string) => void; + isLoading: boolean; + qqList: (QQItem | LoginListItem)[]; +} + +const PasswordLogin: React.FC = ({ onSubmit, isLoading, qqList }) => { + const [uin, setUin] = useState(''); + const [password, setPassword] = useState(''); + + const handleSubmit = () => { + if (!uin) { + toast.error('请输入QQ号'); + return; + } + if (!password) { + toast.error('请输入密码'); + return; + } + onSubmit(uin, password); + }; + + return ( +
+
+ QQ Avatar +
+
+ + + + + setUin(key.toString())} + > + {(item) => ( + +
+ +
+ {isQQQuickNewItem(item) + ? `${item.nickName}(${item.uin})` + : item.uin} +
+
+
+ )} +
+ + } + /> + +
+
+ +
+
+ ); +}; + +export default PasswordLogin; diff --git a/packages/napcat-webui-frontend/src/controllers/qq_manager.ts b/packages/napcat-webui-frontend/src/controllers/qq_manager.ts index 2fe03eed..db3826fc 100644 --- a/packages/napcat-webui-frontend/src/controllers/qq_manager.ts +++ b/packages/napcat-webui-frontend/src/controllers/qq_manager.ts @@ -93,4 +93,11 @@ export default class QQManager { uin, }); } + + public static async passwordLogin (uin: string, passwordMd5: string) { + await serverRequest.post>('/QQLogin/PasswordLogin', { + uin, + passwordMd5, + }); + } } diff --git a/packages/napcat-webui-frontend/src/pages/qq_login.tsx b/packages/napcat-webui-frontend/src/pages/qq_login.tsx index e9d1471c..d9a43a03 100644 --- a/packages/napcat-webui-frontend/src/pages/qq_login.tsx +++ b/packages/napcat-webui-frontend/src/pages/qq_login.tsx @@ -5,11 +5,13 @@ import { Tab, Tabs } from '@heroui/tabs'; import { useEffect, useRef, useState } from 'react'; import { toast } from 'react-hot-toast'; import { useNavigate } from 'react-router-dom'; +import CryptoJS from 'crypto-js'; import logo from '@/assets/images/logo.png'; import HoverEffectCard from '@/components/effect_card'; import { title } from '@/components/primitives'; +import PasswordLogin from '@/components/password_login'; import QrCodeLogin from '@/components/qr_code_login'; import QuickLogin from '@/components/quick_login'; import type { QQItem } from '@/components/quick_login'; @@ -51,6 +53,7 @@ export default function QQLoginPage () { const lastErrorRef = useRef(''); const [qqList, setQQList] = useState<(QQItem | LoginListItem)[]>([]); const [refresh, setRefresh] = useState(false); + const [activeTab, setActiveTab] = useState('shortcut'); const firstLoad = useRef(true); const onSubmit = async () => { if (!uinValue) { @@ -72,6 +75,21 @@ export default function QQLoginPage () { } }; + const onPasswordSubmit = async (uin: string, password: string) => { + setIsLoading(true); + try { + // 计算密码的MD5值 + const passwordMd5 = CryptoJS.MD5(password).toString(); + await QQManager.passwordLogin(uin, passwordMd5); + toast.success('密码登录请求已发送'); + } catch (error) { + const msg = (error as Error).message; + toast.error(`密码登录失败: ${msg}`); + } finally { + setIsLoading(false); + } + }; + const onUpdateQrCode = async () => { if (firstLoad.current) setIsLoading(true); try { @@ -91,11 +109,17 @@ export default function QQLoginPage () { setLoginError(data.loginError); const friendlyMsg = parseLoginError(data.loginError); - dialog.alert({ - title: '登录失败', - content: friendlyMsg, - confirmText: '确定', - }); + // 仅在扫码登录 Tab 下才弹窗,或者错误不是"二维码已过期" + // 如果是 "二维码已过期",且不在 qrcode tab,则不弹窗 + const isQrCodeExpired = friendlyMsg.includes('二维码') && (friendlyMsg.includes('过期') || friendlyMsg.includes('失效')); + + if (!isQrCodeExpired || activeTab === 'qrcode') { + dialog.alert({ + title: '登录失败', + content: friendlyMsg, + confirmText: '确定', + }); + } } else if (!data.loginError) { lastErrorRef.current = ''; setLoginError(''); @@ -197,6 +221,8 @@ export default function QQLoginPage () { }} isDisabled={isLoading} size='lg' + selectedKey={activeTab} + onSelectionChange={(key) => setActiveTab(key.toString())} > + + +