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())} > + + +