diff --git a/apps/backend/src/services/session/session-page.ts b/apps/backend/src/services/session/session-page.ts index 4abcd16b..bc423f5b 100644 --- a/apps/backend/src/services/session/session-page.ts +++ b/apps/backend/src/services/session/session-page.ts @@ -13,14 +13,18 @@ type LocatorLike = { count: () => Promise first: () => LocatorLike click: (options?: { timeout?: number }) => Promise + waitFor: (options?: { state?: string; timeout?: number }) => Promise + screenshot: (options: { path: string }) => Promise } type BrowserPageLike = { + frames: () => any[] locator: (selector: string) => LocatorLike waitForTimeout: (milliseconds: number) => Promise evaluate: (callback: any) => Promise reload: (options?: { waitUntil?: string }) => Promise goto: (url: string, options?: { waitUntil?: string }) => Promise + context: () => any } type SessionForQrCapture = { diff --git a/apps/backend/src/services/session/session-qq.js b/apps/backend/src/services/session/session-qq.ts similarity index 71% rename from apps/backend/src/services/session/session-qq.js rename to apps/backend/src/services/session/session-qq.ts index 79b4607b..bf96d042 100644 --- a/apps/backend/src/services/session/session-qq.js +++ b/apps/backend/src/services/session/session-qq.ts @@ -1,4 +1,4 @@ -// @ts-nocheck +/// import fs from 'node:fs/promises' @@ -6,9 +6,48 @@ import { downloadRemoteQrImage, logBrowserSessionDebug, waitForFrame } from './s const QQ_QR_TARGET_SIZE = 144 -export async function ensureQqLoginReady(page) { +type LocatorLike = { + count: () => Promise + click: (options?: { timeout?: number }) => Promise + waitFor: (options?: { state?: string; timeout?: number }) => Promise + screenshot: (options: { path: string }) => Promise + first: () => LocatorLike +} + +type FrameLike = { + url: () => string + locator: (selector: string) => LocatorLike + waitForTimeout: (milliseconds: number) => Promise + evaluate: (callback: any) => Promise +} + +type PageLike = { + frames: () => FrameLike[] + waitForTimeout: (milliseconds: number) => Promise + locator: (selector: string) => LocatorLike + context: () => { + request: { + get: (url: string, options?: any) => Promise<{ + ok: () => boolean + status: () => number + body: () => Promise + }> + } + } +} + +type QqQrState = { + visible: boolean + qrVisible?: boolean + scanned: boolean + expired: boolean + message: string + frameUrl?: string +} + +export async function ensureQqLoginReady(page: PageLike): Promise { try { - const frame = await waitForFrame( + const frame = await waitForFrame( page, (item) => item.url().includes('xui.ptlogin2.qq.com/cgi-bin/xlogin'), 10_000, @@ -26,8 +65,8 @@ export async function ensureQqLoginReady(page) { } } -export async function captureQqQrImage(page, qrImagePath) { - const frame = await waitForFrame( +export async function captureQqQrImage(page: PageLike, qrImagePath: string): Promise { + const frame = await waitForFrame( page, (item) => item.url().includes('xui.ptlogin2.qq.com/cgi-bin/xlogin'), 10_000, @@ -45,11 +84,11 @@ export async function captureQqQrImage(page, qrImagePath) { const body = await downloadRemoteQrImage(page, effectiveQrUrl, qrImagePath, frame.url(), 'qq') logBrowserSessionDebug('qq.capture.downloadResult', { qrUrl: effectiveQrUrl, - downloaded: Boolean(body?.length), + downloaded: Buffer.isBuffer(body) && body.length > 0, qrImagePath, }) - if (body?.length) { + if (Buffer.isBuffer(body) && body.length > 0) { await fs.writeFile(qrImagePath, body) return } @@ -71,10 +110,10 @@ export async function captureQqQrImage(page, qrImagePath) { } } -export async function findQqQrLocator(page, frame = null) { +export async function findQqQrLocator(page: PageLike, frame: FrameLike | null = null): Promise { const effectiveFrame = frame || - await waitForFrame(page, (item) => item.url().includes('xui.ptlogin2.qq.com/cgi-bin/xlogin')) + await waitForFrame(page, (item) => item.url().includes('xui.ptlogin2.qq.com/cgi-bin/xlogin')) const locator = effectiveFrame.locator('#qrlogin_img') await locator.waitFor({ state: 'visible', timeout: 15_000 }) logBrowserSessionDebug('qq.findQrLocator.visibleReady', { @@ -83,7 +122,7 @@ export async function findQqQrLocator(page, frame = null) { return locator } -export async function extractQqQrState(page) { +export async function extractQqQrState(page: Pick): Promise { const frame = page.frames().find((item) => item.url().includes('xui.ptlogin2.qq.com/cgi-bin/xlogin')) if (!frame) { @@ -98,7 +137,7 @@ export async function extractQqQrState(page) { try { const { qrVisible, bodyText } = await frame .evaluate(() => { - const isVisible = (element) => { + const isVisible = (element: Element | null) => { if (!(element instanceof HTMLElement)) { return false } @@ -151,7 +190,7 @@ export async function extractQqQrState(page) { } } -async function resolveQqQrImageUrl(frame) { +async function resolveQqQrImageUrl(frame: FrameLike): Promise { return frame .evaluate(() => { const qrImage = document.querySelector('#qrlogin_img') @@ -176,7 +215,7 @@ async function resolveQqQrImageUrl(frame) { .catch(() => '') } -function upgradeQqQrImageUrl(qrUrl) { +function upgradeQqQrImageUrl(qrUrl: string): string { if (!qrUrl) { return '' } diff --git a/docs/backend-typescript-migration-plan.md b/docs/backend-typescript-migration-plan.md index c87690ea..1e7f0401 100644 --- a/docs/backend-typescript-migration-plan.md +++ b/docs/backend-typescript-migration-plan.md @@ -472,6 +472,14 @@ - `npm run typecheck` - `npm run build` - `npm test` 共 139 个用例通过 +98. 腾讯浏览器 QQ 登录模块迁移到 `.ts`: + - `src/services/session/session-qq.ts` +99. QQ 登录 iframe、二维码 URL 提取 / 升级、远程下载 fallback、二维码 locator 与扫码状态已显式类型化 +100. Docker 内验证通过: + - `src/services/session/session.test.js` 共 14 个用例通过 + - `npm run typecheck` + - `npm run build` + - `npm test` 共 139 个用例通过 ## 下一步建议