diff --git a/apps/backend/src/services/session/session-wx.js b/apps/backend/src/services/session/session-wx.ts similarity index 78% rename from apps/backend/src/services/session/session-wx.js rename to apps/backend/src/services/session/session-wx.ts index 09a38583..e66fcab7 100644 --- a/apps/backend/src/services/session/session-wx.js +++ b/apps/backend/src/services/session/session-wx.ts @@ -1,12 +1,64 @@ -// @ts-nocheck +/// import fs from 'node:fs/promises' import { downloadRemoteQrImage, logBrowserSessionDebug, waitForFrame } from './session-login-shared.js' -export async function ensureWxLoginReady(page) { +type LocatorLike = { + first: () => LocatorLike + waitFor: (options?: { state?: string; timeout?: number }) => Promise + screenshot: (options: { path: string }) => Promise + isVisible: () => Promise + click: (options?: { timeout?: number }) => Promise + evaluate: (callback: (element: Element) => T | Promise) => Promise +} + +type FrameLike = { + url: () => string + locator: (selector: string) => LocatorLike + waitForTimeout: (milliseconds: number) => Promise + evaluate: (callback: any) => Promise +} + +type PageLike = { + frames: () => FrameLike[] + waitForTimeout: (milliseconds: number) => Promise + context: () => { + request: { + get: (url: string, options?: any) => Promise<{ + ok: () => boolean + status: () => number + body: () => Promise + }> + } + } +} + +type WxLoginViewState = { + qrVisible: boolean + quickLoginVisible: boolean + switchToNormalVisible: boolean + qrCandidates: Array<{ + index: number + visible: boolean + src: string + naturalWidth: number + }> + bodyText: string +} + +type WxQrState = { + visible: boolean + qrVisible?: boolean + scanned: boolean + expired: boolean + message: string + frameUrl?: string +} + +export async function ensureWxLoginReady(page: PageLike): Promise { try { - const frame = await waitForFrame( + const frame = await waitForFrame( page, (item) => item.url().includes('open.weixin.qq.com/connect/qrconnect'), 10_000, @@ -52,9 +104,9 @@ export async function ensureWxLoginReady(page) { } } -export async function captureWxQrImage(page, qrImagePath) { +export async function captureWxQrImage(page: PageLike, qrImagePath: string): Promise { const qrLocator = await findWxQrLocator(page) - const frame = await waitForFrame( + const frame = await waitForFrame( page, (item) => item.url().includes('open.weixin.qq.com/connect/qrconnect'), 10_000, @@ -70,11 +122,11 @@ export async function captureWxQrImage(page, qrImagePath) { const downloaded = await downloadRemoteQrImage(page, qrUrl, qrImagePath, frame.url(), 'wx') logBrowserSessionDebug('wx.capture.downloadResult', { qrUrl, - downloaded: Boolean(downloaded?.length), + downloaded: Buffer.isBuffer(downloaded) && downloaded.length > 0, qrImagePath, }) - if (downloaded?.length) { + if (Buffer.isBuffer(downloaded) && downloaded.length > 0) { await fs.writeFile(qrImagePath, downloaded) return } @@ -86,10 +138,10 @@ export async function captureWxQrImage(page, qrImagePath) { await qrLocator.screenshot({ path: qrImagePath }) } -export async function findWxQrLocator(page) { +export async function findWxQrLocator(page: PageLike): Promise { await ensureWxLoginReady(page) - const frame = await waitForFrame( + const frame = await waitForFrame( page, (item) => item.url().includes('open.weixin.qq.com/connect/qrconnect'), ) @@ -109,7 +161,7 @@ export async function findWxQrLocator(page) { return locator } -export async function extractWxQrState(page) { +export async function extractWxQrState(page: Pick): Promise { const frame = page.frames().find((item) => item.url().includes('open.weixin.qq.com/connect/qrconnect')) if (!frame) { @@ -146,10 +198,10 @@ export async function extractWxQrState(page) { } } -async function inspectWxLoginView(frame) { +async function inspectWxLoginView(frame: FrameLike): Promise { return frame .evaluate(() => { - const isVisible = (element) => { + const isVisible = (element: Element | null) => { if (!(element instanceof HTMLElement)) { return false } @@ -203,7 +255,7 @@ async function inspectWxLoginView(frame) { })) } -async function switchWxQuickLoginToQr(frame) { +async function switchWxQuickLoginToQr(frame: FrameLike): Promise { const switcherCandidates = [ frame.locator('.js_switchToNormal:visible').first(), frame.locator('.js_switchToNormal').first(), @@ -279,10 +331,10 @@ async function switchWxQuickLoginToQr(frame) { .catch(() => false) } -async function resolveWxQrImageUrl(frame) { +async function resolveWxQrImageUrl(frame: FrameLike): Promise { return frame .evaluate(() => { - const isVisible = (element) => { + const isVisible = (element: Element | null) => { if (!(element instanceof HTMLElement)) { return false } diff --git a/docs/backend-typescript-migration-plan.md b/docs/backend-typescript-migration-plan.md index 1e7f0401..0caa7fe7 100644 --- a/docs/backend-typescript-migration-plan.md +++ b/docs/backend-typescript-migration-plan.md @@ -480,6 +480,14 @@ - `npm run typecheck` - `npm run build` - `npm test` 共 139 个用例通过 +101. 腾讯浏览器 WX 登录模块迁移到 `.ts`: + - `src/services/session/session-wx.ts` +102. WX 登录 iframe、快捷登录切换、二维码候选状态、远程下载 fallback、二维码 locator 与扫码状态已显式类型化 +103. Docker 内验证通过: + - `src/services/session/session.test.js` 共 14 个用例通过 + - `npm run typecheck` + - `npm run build` + - `npm test` 共 139 个用例通过 ## 下一步建议