Add captcha & new-device QQ login flows

Introduce multi-step QQ password login support (captcha and new-device verification) and related OIDB QR handling.

- Change login signature fields in NodeIKernelLoginService to binary (Uint8Array) and add unusualDeviceCheckSig.
- Update shell base to handle additional result codes (captcha required, new-device, abnormal-device), set login status on success, and register three callbacks: captcha, new-device, and password flows. Use TextEncoder for encoding ticket/randstr/sid and newDevicePullQrCodeSig.
- Extend backend WebUiDataRuntime (types and runtime) with set/request methods for captcha and new-device login calls and adjust LoginRuntime types to return richer metadata (needCaptcha, proofWaterUrl, needNewDevice, jumpUrl, newDevicePullQrCodeSig).
- Add backend API handlers: CaptchaLogin, NewDeviceLogin, GetNewDeviceQRCode and PollNewDeviceQR; add oidbRequest helper using https to query oidb.tim.qq.com for QR generation and polling.
- Wire new handlers into QQLogin router and return structured success responses when further steps are required.
- Add frontend components and pages for captcha and new-device verification (new files: 1.html, new_device_verify.tsx, tencent_captcha.tsx) and update existing frontend controllers/pages to integrate the new flows.
- Improve error logging and user-facing messages for the new flows.

This change enables handling of password-login scenarios requiring captcha or device attestation and provides endpoints to obtain and poll OIDB QR codes for new-device verification.
This commit is contained in:
手瓜一十雪
2026-02-21 13:03:40 +08:00
parent f961830836
commit b71a4913eb
11 changed files with 957 additions and 102 deletions

View File

@@ -234,21 +234,43 @@ async function handleLoginInner (context: { isLogined: boolean; }, logger: LogWr
uin,
passwordMd5,
step: 0,
newDeviceLoginSig: '',
proofWaterSig: '',
proofWaterRand: '',
proofWaterSid: '',
newDeviceLoginSig: new Uint8Array(),
proofWaterSig: new Uint8Array(),
proofWaterRand: new Uint8Array(),
proofWaterSid: new Uint8Array(),
unusualDeviceCheckSig: new Uint8Array(),
}).then(res => {
if (res.result === '140022008') {
const errMsg = '需要验证码,暂不支持';
WebUiDataRuntime.setQQLoginError(errMsg);
loginService.getQRCodePicture();
resolve({ result: false, message: errMsg });
const proofWaterUrl = res.loginErrorInfo?.proofWaterUrl || '';
logger.log('需要验证码, proofWaterUrl: ', proofWaterUrl);
resolve({
result: false,
message: '需要验证码',
needCaptcha: true,
proofWaterUrl,
});
} else if (res.result === '140022010') {
const errMsg = '新设备需要扫码登录,暂不支持';
WebUiDataRuntime.setQQLoginError(errMsg);
loginService.getQRCodePicture();
resolve({ result: false, message: errMsg });
const jumpUrl = res.loginErrorInfo?.jumpUrl || '';
const newDevicePullQrCodeSig = res.loginErrorInfo?.newDevicePullQrCodeSig || '';
logger.log('新设备需要扫码验证, jumpUrl: ', jumpUrl);
resolve({
result: false,
message: '新设备需要扫码验证',
needNewDevice: true,
jumpUrl,
newDevicePullQrCodeSig,
});
} else if (res.result === '140022011') {
const jumpUrl = res.loginErrorInfo?.jumpUrl || '';
const newDevicePullQrCodeSig = res.loginErrorInfo?.newDevicePullQrCodeSig || '';
logger.log('异常设备需要验证, jumpUrl: ', jumpUrl);
resolve({
result: false,
message: '异常设备需要验证',
needNewDevice: true,
jumpUrl,
newDevicePullQrCodeSig,
});
} else if (res.result !== '0') {
const errMsg = res.loginErrorInfo?.errMsg || '密码登录失败';
WebUiDataRuntime.setQQLoginError(errMsg);
@@ -270,6 +292,114 @@ async function handleLoginInner (context: { isLogined: boolean; }, logger: LogWr
}
});
});
// 注册验证码登录回调(密码登录需要验证码时的第二步)
WebUiDataRuntime.setCaptchaLoginCall(async (uin: string, passwordMd5: string, ticket: string, randstr: string, sid: string) => {
return await new Promise((resolve) => {
if (uin && passwordMd5 && ticket) {
logger.log('正在验证码登录 ', uin);
loginService.passwordLogin({
uin,
passwordMd5,
step: 1,
newDeviceLoginSig: new Uint8Array(),
proofWaterSig: new TextEncoder().encode(ticket),
proofWaterRand: new TextEncoder().encode(randstr),
proofWaterSid: new TextEncoder().encode(sid),
unusualDeviceCheckSig: new Uint8Array(),
}).then(res => {
console.log('验证码登录结果: ', res);
if (res.result === '140022010') {
const jumpUrl = res.loginErrorInfo?.jumpUrl || '';
const newDevicePullQrCodeSig = res.loginErrorInfo?.newDevicePullQrCodeSig || '';
logger.log('验证码登录后需要新设备验证, jumpUrl: ', jumpUrl);
resolve({
result: false,
message: '新设备需要扫码验证',
needNewDevice: true,
jumpUrl,
newDevicePullQrCodeSig,
});
} else if (res.result === '140022011') {
const jumpUrl = res.loginErrorInfo?.jumpUrl || '';
const newDevicePullQrCodeSig = res.loginErrorInfo?.newDevicePullQrCodeSig || '';
logger.log('验证码登录后需要异常设备验证, jumpUrl: ', jumpUrl);
resolve({
result: false,
message: '异常设备需要验证',
needNewDevice: true,
jumpUrl,
newDevicePullQrCodeSig,
});
} 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: '验证码登录失败:参数不完整' });
}
});
});
// 注册新设备登录回调(密码登录需要新设备验证时的第二步)
WebUiDataRuntime.setNewDeviceLoginCall(async (uin: string, passwordMd5: string, newDevicePullQrCodeSig: string) => {
return await new Promise((resolve) => {
if (uin && passwordMd5 && newDevicePullQrCodeSig) {
logger.log('正在新设备验证登录 ', uin);
loginService.passwordLogin({
uin,
passwordMd5,
step: 2,
newDeviceLoginSig: new TextEncoder().encode(newDevicePullQrCodeSig),
proofWaterSig: new Uint8Array(),
proofWaterRand: new Uint8Array(),
proofWaterSid: new Uint8Array(),
unusualDeviceCheckSig: new Uint8Array(),
}).then(res => {
if (res.result === '140022011') {
const jumpUrl = res.loginErrorInfo?.jumpUrl || '';
const newDevicePullQrCodeSig = res.loginErrorInfo?.newDevicePullQrCodeSig || '';
logger.log('新设备验证后需要异常设备验证, jumpUrl: ', jumpUrl);
resolve({
result: false,
message: '异常设备需要验证',
needNewDevice: true,
jumpUrl,
newDevicePullQrCodeSig,
});
} 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);