fix: 接入NapCat验证码提交
This commit is contained in:
parent
ab43fe8632
commit
68e593237e
@ -47,7 +47,7 @@ pnpm run build:antdv-next
|
|||||||
## 业务页面
|
## 业务页面
|
||||||
|
|
||||||
- 系统管理 / 站内信是日志级通知列表,只展示 API 错误、QQBot 下线、NapCat 离线等后端自动捕获事件;页面提供筛选、处理/重新打开、置顶和删除,不提供人工新增或编辑。
|
- 系统管理 / 站内信是日志级通知列表,只展示 API 错误、QQBot 下线、NapCat 离线等后端自动捕获事件;页面提供筛选、处理/重新打开、置顶和删除,不提供人工新增或编辑。
|
||||||
- QQBot / 账号连接页拆分 OneBot 连接、QQ 登录、NapCat 运行和运行说明列;更新登录通过 SSE 展示 quick / password / qrcode 每步进度。
|
- QQBot / 账号连接页拆分 OneBot 连接、QQ 登录、NapCat 运行和运行说明列;更新登录通过 SSE 展示 quick / password / captcha / qrcode 每步进度,密码登录触发 QQ 安全验证时在弹窗内完成腾讯验证码并回交 API。
|
||||||
|
|
||||||
## 部署说明
|
## 部署说明
|
||||||
|
|
||||||
|
|||||||
@ -85,6 +85,7 @@ export namespace QqbotApi {
|
|||||||
|
|
||||||
export interface AccountScanResult {
|
export interface AccountScanResult {
|
||||||
accountId?: string;
|
accountId?: string;
|
||||||
|
captchaUrl?: string;
|
||||||
containerId?: string;
|
containerId?: string;
|
||||||
containerName?: string;
|
containerName?: string;
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
@ -105,6 +106,13 @@ export namespace QqbotApi {
|
|||||||
step: string;
|
step: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AccountScanCaptchaBody {
|
||||||
|
randstr: string;
|
||||||
|
sessionId: string;
|
||||||
|
sid?: string;
|
||||||
|
ticket: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Rule {
|
export interface Rule {
|
||||||
cooldownMs: number;
|
cooldownMs: number;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@ -388,6 +396,15 @@ export function refreshQqbotAccountScanQrcode(sessionId: string) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function submitQqbotAccountScanCaptcha(
|
||||||
|
data: QqbotApi.AccountScanCaptchaBody,
|
||||||
|
) {
|
||||||
|
return requestClient.post<QqbotApi.AccountScanResult>(
|
||||||
|
'/qqbot/account/scan/captcha/submit',
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function cancelQqbotAccountScan(sessionId: string) {
|
export function cancelQqbotAccountScan(sessionId: string) {
|
||||||
return requestClient.post<boolean>(
|
return requestClient.post<boolean>(
|
||||||
`/qqbot/account/scan/cancel?sessionId=${sessionId}`,
|
`/qqbot/account/scan/cancel?sessionId=${sessionId}`,
|
||||||
|
|||||||
@ -36,6 +36,7 @@ import {
|
|||||||
refreshQqbotAccountScanQrcode,
|
refreshQqbotAccountScanQrcode,
|
||||||
startQqbotAccountScanCreate,
|
startQqbotAccountScanCreate,
|
||||||
startQqbotAccountScanRefresh,
|
startQqbotAccountScanRefresh,
|
||||||
|
submitQqbotAccountScanCaptcha,
|
||||||
updateQqbotAccount,
|
updateQqbotAccount,
|
||||||
} from '#/api/qqbot';
|
} from '#/api/qqbot';
|
||||||
import { KtTable, useKtTable } from '#/components/ktTable';
|
import { KtTable, useKtTable } from '#/components/ktTable';
|
||||||
@ -46,6 +47,32 @@ const ASteps = Steps as any;
|
|||||||
const ATypographyLink = Typography.Link as any;
|
const ATypographyLink = Typography.Link as any;
|
||||||
const ATypographyText = Typography.Text as any;
|
const ATypographyText = Typography.Text as any;
|
||||||
|
|
||||||
|
type TencentCaptchaResult = {
|
||||||
|
appid?: string;
|
||||||
|
errorCode?: number;
|
||||||
|
errorMessage?: string;
|
||||||
|
randstr?: string;
|
||||||
|
ret: number;
|
||||||
|
ticket?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TencentCaptchaInstance = {
|
||||||
|
destroy: () => void;
|
||||||
|
show: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
TencentCaptcha?: new (
|
||||||
|
appid: string,
|
||||||
|
callback: (res: TencentCaptchaResult) => void,
|
||||||
|
options?: Record<string, unknown>,
|
||||||
|
) => TencentCaptchaInstance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let tencentCaptchaScriptPromise: Promise<void> | undefined;
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'QqBotAccountList',
|
name: 'QqBotAccountList',
|
||||||
setup() {
|
setup() {
|
||||||
@ -57,6 +84,7 @@ export default defineComponent({
|
|||||||
const scanQrcodeText = ref('');
|
const scanQrcodeText = ref('');
|
||||||
const scanEvents = ref<QqbotApi.AccountScanEvent[]>([]);
|
const scanEvents = ref<QqbotApi.AccountScanEvent[]>([]);
|
||||||
const scanState = reactive<{
|
const scanState = reactive<{
|
||||||
|
captchaUrl?: string;
|
||||||
containerId?: string;
|
containerId?: string;
|
||||||
containerName?: string;
|
containerName?: string;
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
@ -102,6 +130,9 @@ export default defineComponent({
|
|||||||
Math.max(scanProgressItems.value.length - 1, 0),
|
Math.max(scanProgressItems.value.length - 1, 0),
|
||||||
);
|
);
|
||||||
const scanQrcodePlaceholderText = computed(() => {
|
const scanQrcodePlaceholderText = computed(() => {
|
||||||
|
if (scanState.captchaUrl) {
|
||||||
|
return '等待安全验证';
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
scanState.mode === 'refresh' &&
|
scanState.mode === 'refresh' &&
|
||||||
scanState.errorMessage?.includes('正在尝试快速登录')
|
scanState.errorMessage?.includes('正在尝试快速登录')
|
||||||
@ -395,6 +426,7 @@ export default defineComponent({
|
|||||||
result: QqbotApi.AccountScanResult,
|
result: QqbotApi.AccountScanResult,
|
||||||
options: { reloadQrcode?: boolean } = {},
|
options: { reloadQrcode?: boolean } = {},
|
||||||
) {
|
) {
|
||||||
|
scanState.captchaUrl = result.captchaUrl;
|
||||||
scanState.containerId = result.containerId;
|
scanState.containerId = result.containerId;
|
||||||
scanState.containerName = result.containerName;
|
scanState.containerName = result.containerName;
|
||||||
scanState.errorMessage = result.errorMessage;
|
scanState.errorMessage = result.errorMessage;
|
||||||
@ -456,6 +488,37 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function submitScanCaptcha() {
|
||||||
|
const sessionId = scanState.sessionId;
|
||||||
|
const captchaUrl = scanState.captchaUrl;
|
||||||
|
if (!sessionId || !captchaUrl || scanLoading.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
scanLoading.value = true;
|
||||||
|
try {
|
||||||
|
const captcha = await requestTencentCaptcha(captchaUrl);
|
||||||
|
if (
|
||||||
|
scanState.sessionId !== sessionId ||
|
||||||
|
scanState.captchaUrl !== captchaUrl
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await applyScanResult(
|
||||||
|
await submitQqbotAccountScanCaptcha({
|
||||||
|
...captcha,
|
||||||
|
sessionId,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
const text = getErrorMessage(error);
|
||||||
|
if (text !== '已取消安全验证') {
|
||||||
|
message.error(text);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
scanLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function startScanPolling() {
|
function startScanPolling() {
|
||||||
if (scanTimer) return;
|
if (scanTimer) return;
|
||||||
scanTimer = window.setInterval(() => {
|
scanTimer = window.setInterval(() => {
|
||||||
@ -521,6 +584,7 @@ export default defineComponent({
|
|||||||
stopScanPolling();
|
stopScanPolling();
|
||||||
stopScanEvents();
|
stopScanEvents();
|
||||||
Object.assign(scanState, {
|
Object.assign(scanState, {
|
||||||
|
captchaUrl: undefined,
|
||||||
containerId: undefined,
|
containerId: undefined,
|
||||||
containerName: undefined,
|
containerName: undefined,
|
||||||
errorMessage: undefined,
|
errorMessage: undefined,
|
||||||
@ -965,6 +1029,28 @@ export default defineComponent({
|
|||||||
type="info"
|
type="info"
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
{scanState.captchaUrl ? (
|
||||||
|
<Alert
|
||||||
|
description={
|
||||||
|
<Space>
|
||||||
|
<ATypographyText>
|
||||||
|
请在当前页面完成腾讯安全验证,验证结果会自动提交到对应
|
||||||
|
NapCat 容器。
|
||||||
|
</ATypographyText>
|
||||||
|
<AButton
|
||||||
|
loading={scanLoading.value}
|
||||||
|
onClick={submitScanCaptcha}
|
||||||
|
type="primary"
|
||||||
|
>
|
||||||
|
完成安全验证
|
||||||
|
</AButton>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
message="QQ 密码登录需要安全验证"
|
||||||
|
showIcon
|
||||||
|
type="warning"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
{scanProgressItems.value.length > 0 ? (
|
{scanProgressItems.value.length > 0 ? (
|
||||||
<ASteps
|
<ASteps
|
||||||
current={scanProgressCurrent.value}
|
current={scanProgressCurrent.value}
|
||||||
@ -1021,3 +1107,146 @@ export default defineComponent({
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function requestTencentCaptcha(
|
||||||
|
proofWaterUrl: string,
|
||||||
|
): Promise<Omit<QqbotApi.AccountScanCaptchaBody, 'sessionId'>> {
|
||||||
|
const params = parseUrlParams(proofWaterUrl);
|
||||||
|
const appid = params.aid || '2081081773';
|
||||||
|
const sid = params.sid || '';
|
||||||
|
|
||||||
|
return loadTencentCaptchaScript().then(
|
||||||
|
() =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
if (!window.TencentCaptcha) {
|
||||||
|
reject(new Error('腾讯验证码组件加载失败'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let captcha: TencentCaptchaInstance | undefined;
|
||||||
|
let settled = false;
|
||||||
|
const finish = (
|
||||||
|
error?: Error,
|
||||||
|
value?: Omit<QqbotApi.AccountScanCaptchaBody, 'sessionId'>,
|
||||||
|
) => {
|
||||||
|
if (settled) return;
|
||||||
|
settled = true;
|
||||||
|
try {
|
||||||
|
captcha?.destroy();
|
||||||
|
} catch {
|
||||||
|
// The captcha SDK may already have cleaned up its popup.
|
||||||
|
}
|
||||||
|
if (error) {
|
||||||
|
reject(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!value) {
|
||||||
|
reject(new Error('腾讯验证码未返回验证结果'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
captcha = new window.TencentCaptcha(
|
||||||
|
appid,
|
||||||
|
(res) => {
|
||||||
|
if (res.ret === 0 && res.ticket && res.randstr) {
|
||||||
|
finish(undefined, {
|
||||||
|
randstr: res.randstr,
|
||||||
|
sid,
|
||||||
|
ticket: res.ticket,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
finish(new Error('已取消安全验证'));
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enableAged: true,
|
||||||
|
login_appid: params.login_appid,
|
||||||
|
showHeader: false,
|
||||||
|
sid: params.sid,
|
||||||
|
type: 'popup',
|
||||||
|
uin: params.uin,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
captcha.show();
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadTencentCaptchaScript() {
|
||||||
|
if (window.TencentCaptcha) return;
|
||||||
|
tencentCaptchaScriptPromise =
|
||||||
|
tencentCaptchaScriptPromise ||
|
||||||
|
loadScriptWithFallback([
|
||||||
|
'https://captcha.gtimg.com/TCaptcha.js',
|
||||||
|
'https://ssl.captcha.qq.com/TCaptcha.js',
|
||||||
|
]);
|
||||||
|
try {
|
||||||
|
await tencentCaptchaScriptPromise;
|
||||||
|
} catch (error) {
|
||||||
|
tencentCaptchaScriptPromise = undefined;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadScriptWithFallback(sources: string[]) {
|
||||||
|
let lastError: unknown;
|
||||||
|
for (const source of sources) {
|
||||||
|
try {
|
||||||
|
await loadScript(source);
|
||||||
|
return;
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw lastError instanceof Error
|
||||||
|
? lastError
|
||||||
|
: new Error('腾讯验证码脚本加载失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadScript(source: string) {
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
const existing = document.querySelector<HTMLScriptElement>(
|
||||||
|
`script[src="${source}"]`,
|
||||||
|
);
|
||||||
|
if (existing) {
|
||||||
|
existing.addEventListener('load', () => resolve(), { once: true });
|
||||||
|
existing.addEventListener(
|
||||||
|
'error',
|
||||||
|
() => reject(new Error(`腾讯验证码脚本加载失败:${source}`)),
|
||||||
|
{ once: true },
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.async = true;
|
||||||
|
script.src = source;
|
||||||
|
script.addEventListener('load', () => resolve(), { once: true });
|
||||||
|
script.addEventListener(
|
||||||
|
'error',
|
||||||
|
() => reject(new Error(`腾讯验证码脚本加载失败:${source}`)),
|
||||||
|
{ once: true },
|
||||||
|
);
|
||||||
|
document.head.append(script);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseUrlParams(url: string) {
|
||||||
|
const params: Record<string, string> = {};
|
||||||
|
try {
|
||||||
|
const parsed = new URL(url);
|
||||||
|
parsed.searchParams.forEach((value, key) => {
|
||||||
|
params[key] = value;
|
||||||
|
});
|
||||||
|
return params;
|
||||||
|
} catch {
|
||||||
|
const query = url.split('?')[1] || '';
|
||||||
|
query.split('&').forEach((pair) => {
|
||||||
|
const [key, value = ''] = pair.split('=');
|
||||||
|
if (key) params[key] = decodeURIComponent(value);
|
||||||
|
});
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user