mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-02-04 06:31:13 +00:00
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<QuickLoginResult>) and minor formatting fixes.
This commit is contained in:
parent
78ac36f670
commit
74b1da67d8
@ -21,7 +21,7 @@ export interface PasswordLoginRetType {
|
||||
jumpWord: string;
|
||||
tipsTitle: string;
|
||||
tipsContent: string;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export interface PasswordLoginArgType {
|
||||
@ -55,7 +55,7 @@ export interface QuickLoginResult {
|
||||
jumpUrl: string,
|
||||
jumpWord: string,
|
||||
tipsTitle: string,
|
||||
tipsContent: string
|
||||
tipsContent: string;
|
||||
};
|
||||
}
|
||||
|
||||
@ -76,16 +76,16 @@ export interface NodeIKernelLoginService {
|
||||
|
||||
initConfig (config: LoginInitConfig): void;
|
||||
|
||||
getLoginMiscData(data: string): Promise<GeneralCallResult & { value: string }>;
|
||||
getLoginMiscData (data: string): Promise<GeneralCallResult & { value: string; }>;
|
||||
|
||||
getLoginList (): Promise<{
|
||||
result: number, // 0是ok
|
||||
LocalLoginInfoList: LoginListItem[]
|
||||
LocalLoginInfoList: LoginListItem[];
|
||||
}>;
|
||||
|
||||
quickLoginWithUin (uin: string): Promise<QuickLoginResult>;
|
||||
|
||||
passwordLogin(param: PasswordLoginArgType): Promise<unknown>;
|
||||
passwordLogin (param: PasswordLoginArgType): Promise<QuickLoginResult>;
|
||||
|
||||
getQRCodePicture (): boolean;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
};
|
||||
|
||||
@ -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;
|
||||
},
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -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<void>;
|
||||
onRestartProcessRequested: () => Promise<{ result: boolean; message: string; }>;
|
||||
QQLoginList: string[];
|
||||
|
||||
122
packages/napcat-webui-frontend/src/components/password_login.tsx
Normal file
122
packages/napcat-webui-frontend/src/components/password_login.tsx
Normal file
@ -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<PasswordLoginProps> = ({ 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 (
|
||||
<div className='flex flex-col gap-8'>
|
||||
<div className='flex justify-center'>
|
||||
<Image
|
||||
className='shadow-lg'
|
||||
height={100}
|
||||
radius='full'
|
||||
src={`https://q1.qlogo.cn/g?b=qq&nk=${uin || '0'}&s=100`}
|
||||
width={100}
|
||||
alt="QQ Avatar"
|
||||
/>
|
||||
</div>
|
||||
<div className='flex flex-col gap-4'>
|
||||
<Input
|
||||
type="text"
|
||||
label="QQ账号"
|
||||
placeholder="请输入QQ号"
|
||||
value={uin}
|
||||
onValueChange={setUin}
|
||||
variant="bordered"
|
||||
size='lg'
|
||||
autoComplete="off"
|
||||
endContent={
|
||||
<Dropdown>
|
||||
<DropdownTrigger>
|
||||
<Button isIconOnly variant="light" size="sm" radius="full">
|
||||
<IoChevronDown size={16} />
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu
|
||||
aria-label="QQ Login History"
|
||||
items={qqList}
|
||||
onAction={(key) => setUin(key.toString())}
|
||||
>
|
||||
{(item) => (
|
||||
<DropdownItem key={item.uin} textValue={item.uin}>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Avatar
|
||||
alt={item.uin}
|
||||
className='flex-shrink-0'
|
||||
size='sm'
|
||||
src={
|
||||
isQQQuickNewItem(item)
|
||||
? item.faceUrl
|
||||
: `https://q1.qlogo.cn/g?b=qq&nk=${item.uin}&s=1`
|
||||
}
|
||||
/>
|
||||
<div className='flex flex-col'>
|
||||
{isQQQuickNewItem(item)
|
||||
? `${item.nickName}(${item.uin})`
|
||||
: item.uin}
|
||||
</div>
|
||||
</div>
|
||||
</DropdownItem>
|
||||
)}
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
type="password"
|
||||
label="密码"
|
||||
placeholder="请输入密码"
|
||||
value={password}
|
||||
onValueChange={setPassword}
|
||||
variant="bordered"
|
||||
size='lg'
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
<div className='flex justify-center mt-5'>
|
||||
<Button
|
||||
className='w-64 max-w-full'
|
||||
color='primary'
|
||||
isLoading={isLoading}
|
||||
radius='full'
|
||||
size='lg'
|
||||
variant='shadow'
|
||||
onPress={handleSubmit}
|
||||
>
|
||||
登录
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PasswordLogin;
|
||||
@ -93,4 +93,11 @@ export default class QQManager {
|
||||
uin,
|
||||
});
|
||||
}
|
||||
|
||||
public static async passwordLogin (uin: string, passwordMd5: string) {
|
||||
await serverRequest.post<ServerResponse<null>>('/QQLogin/PasswordLogin', {
|
||||
uin,
|
||||
passwordMd5,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<string>('');
|
||||
const [qqList, setQQList] = useState<(QQItem | LoginListItem)[]>([]);
|
||||
const [refresh, setRefresh] = useState<boolean>(false);
|
||||
const [activeTab, setActiveTab] = useState<string>('shortcut');
|
||||
const firstLoad = useRef<boolean>(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);
|
||||
|
||||
// 仅在扫码登录 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())}
|
||||
>
|
||||
<Tab key='shortcut' title='快速登录'>
|
||||
<QuickLogin
|
||||
@ -209,6 +235,13 @@ export default function QQLoginPage () {
|
||||
onUpdateQQList={onUpdateQQList}
|
||||
/>
|
||||
</Tab>
|
||||
<Tab key='password' title='密码登录'>
|
||||
<PasswordLogin
|
||||
isLoading={isLoading}
|
||||
onSubmit={onPasswordSubmit}
|
||||
qqList={qqList}
|
||||
/>
|
||||
</Tab>
|
||||
<Tab key='qrcode' title='扫码登录'>
|
||||
<QrCodeLogin
|
||||
loginError={parseLoginError(loginError)}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user