后端迁移会话页面模块

This commit is contained in:
yml
2026-05-21 15:30:25 +08:00
parent 3176880b2e
commit c7726dde32
2 changed files with 81 additions and 21 deletions
@@ -1,4 +1,4 @@
// @ts-nocheck /// <reference lib="dom" />
import fs from 'node:fs/promises' import fs from 'node:fs/promises'
import path from 'node:path' import path from 'node:path'
@@ -7,16 +7,68 @@ import { isRetryableQrCaptureError, logBrowserSessionDebug } from './session-log
import { captureQqQrImage, ensureQqLoginReady, extractQqQrState } from './session-qq.js' import { captureQqQrImage, ensureQqLoginReady, extractQqQrState } from './session-qq.js'
import { captureWxQrImage, ensureWxLoginReady, extractWxQrState } from './session-wx.js' import { captureWxQrImage, ensureWxLoginReady, extractWxQrState } from './session-wx.js'
type LoginType = 'qq' | 'wx' | string
type LocatorLike = {
count: () => Promise<number>
first: () => LocatorLike
click: (options?: { timeout?: number }) => Promise<void>
}
type BrowserPageLike = {
locator: (selector: string) => LocatorLike
waitForTimeout: (milliseconds: number) => Promise<void>
evaluate: <T = any>(callback: any) => Promise<T>
reload: (options?: { waitUntil?: string }) => Promise<unknown>
goto: (url: string, options?: { waitUntil?: string }) => Promise<unknown>
}
type SessionForQrCapture = {
page: BrowserPageLike
loginType: LoginType
sessionDir: string
qrImagePath?: string
qrImageBase64?: string
qrUpdatedAt?: string
updatedAt?: string
}
type CaptureSessionQrOptions = {
ensureLoginReady?: boolean
ensureLoginTab?: typeof ensureLoginTab
captureQrImageWithRetry?: typeof captureQrImageWithRetry
readFile?: (path: string, encoding: BufferEncoding) => Promise<string>
now?: () => string
}
type CaptureQrImageWithRetryOptions = {
captureQqQrImage?: (page: BrowserPageLike, qrImagePath: string) => Promise<unknown>
captureWxQrImage?: (page: BrowserPageLike, qrImagePath: string) => Promise<unknown>
ensureLoginTab?: typeof ensureLoginTab
isRetryableQrCaptureError?: (error: unknown) => boolean
logBrowserSessionDebug?: (scope: string, detail?: unknown) => void
}
type EnsureLoginTabOptions = {
ensureQqLoginReady?: (page: BrowserPageLike) => Promise<unknown>
ensureWxLoginReady?: (page: BrowserPageLike) => Promise<unknown>
}
type ExtractQrStateOptions = {
extractQqQrState?: (page: BrowserPageLike) => Promise<unknown>
extractWxQrState?: (page: BrowserPageLike) => Promise<unknown>
}
export async function captureSessionQr( export async function captureSessionQr(
session, session: SessionForQrCapture,
{ {
ensureLoginReady = true, ensureLoginReady = true,
ensureLoginTab: ensureLoginTabImpl = ensureLoginTab, ensureLoginTab: ensureLoginTabImpl = ensureLoginTab,
captureQrImageWithRetry: captureQrImageWithRetryImpl = captureQrImageWithRetry, captureQrImageWithRetry: captureQrImageWithRetryImpl = captureQrImageWithRetry,
readFile = fs.readFile, readFile = fs.readFile,
now = () => new Date().toISOString(), now = () => new Date().toISOString(),
} = {}, }: CaptureSessionQrOptions = {},
) { ): Promise<void> {
if (ensureLoginReady) { if (ensureLoginReady) {
await ensureLoginTabImpl(session.page, session.loginType) await ensureLoginTabImpl(session.page, session.loginType)
} }
@@ -33,19 +85,19 @@ export async function captureSessionQr(
} }
export async function captureQrImageWithRetry( export async function captureQrImageWithRetry(
page, page: BrowserPageLike,
loginType, loginType: LoginType,
qrImagePath, qrImagePath: string,
{ {
captureQqQrImage: captureQqQrImageImpl = captureQqQrImage, captureQqQrImage: captureQqQrImageImpl = captureQqQrImage,
captureWxQrImage: captureWxQrImageImpl = captureWxQrImage, captureWxQrImage: captureWxQrImageImpl = captureWxQrImage,
ensureLoginTab: ensureLoginTabImpl = ensureLoginTab, ensureLoginTab: ensureLoginTabImpl = ensureLoginTab,
isRetryableQrCaptureError: isRetryableQrCaptureErrorImpl = isRetryableQrCaptureError, isRetryableQrCaptureError: isRetryableQrCaptureErrorImpl = isRetryableQrCaptureError,
logBrowserSessionDebug: logBrowserSessionDebugImpl = logBrowserSessionDebug, logBrowserSessionDebug: logBrowserSessionDebugImpl = logBrowserSessionDebug,
} = {}, }: CaptureQrImageWithRetryOptions = {},
) { ): Promise<void> {
const maxAttempts = loginType === 'qq' ? 4 : 3 const maxAttempts = loginType === 'qq' ? 4 : 3
let lastError = null let lastError: unknown = null
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
try { try {
@@ -94,13 +146,13 @@ export async function captureQrImageWithRetry(
} }
export async function ensureLoginTab( export async function ensureLoginTab(
page, page: BrowserPageLike,
loginType, loginType: LoginType,
{ {
ensureQqLoginReady: ensureQqLoginReadyImpl = ensureQqLoginReady, ensureQqLoginReady: ensureQqLoginReadyImpl = ensureQqLoginReady,
ensureWxLoginReady: ensureWxLoginReadyImpl = ensureWxLoginReady, ensureWxLoginReady: ensureWxLoginReadyImpl = ensureWxLoginReady,
} = {}, }: EnsureLoginTabOptions = {},
) { ): Promise<void> {
const tabSelector = loginType === 'wx' ? '.wx-tab' : '.qc-tab' const tabSelector = loginType === 'wx' ? '.wx-tab' : '.qc-tab'
const loginButton = page.locator('#unlogin') const loginButton = page.locator('#unlogin')
let interacted = false let interacted = false
@@ -139,9 +191,9 @@ export async function ensureLoginTab(
} }
} }
export async function extractHostState(page) { export async function extractHostState(page: BrowserPageLike) {
return page.evaluate(() => { return page.evaluate(() => {
const isVisible = (element) => { const isVisible = (element: Element | null) => {
if (!element) { if (!element) {
return false return false
} }
@@ -162,7 +214,7 @@ export async function extractHostState(page) {
}) })
} }
export async function reloadActivityPageForPresentation(page, activityUrl) { export async function reloadActivityPageForPresentation(page: BrowserPageLike, activityUrl: string): Promise<void> {
try { try {
await page.reload({ waitUntil: 'domcontentloaded' }) await page.reload({ waitUntil: 'domcontentloaded' })
} catch { } catch {
@@ -173,13 +225,13 @@ export async function reloadActivityPageForPresentation(page, activityUrl) {
} }
export async function extractQrState( export async function extractQrState(
page, page: BrowserPageLike,
loginType, loginType: LoginType,
{ {
extractQqQrState: extractQqQrStateImpl = extractQqQrState, extractQqQrState: extractQqQrStateImpl = extractQqQrState,
extractWxQrState: extractWxQrStateImpl = extractWxQrState, extractWxQrState: extractWxQrStateImpl = extractWxQrState,
} = {}, }: ExtractQrStateOptions = {},
) { ): Promise<unknown> {
if (loginType === 'qq') { if (loginType === 'qq') {
return extractQqQrStateImpl(page) return extractQqQrStateImpl(page)
} }
@@ -464,6 +464,14 @@
- `npm run typecheck` - `npm run typecheck`
- `npm run build` - `npm run build`
- `npm test` 共 139 个用例通过 - `npm test` 共 139 个用例通过
95. 腾讯浏览器会话页面模块迁移到 `.ts`
- `src/services/session/session-page.ts`
96. 二维码截图重试、登录 tab 切换、host 状态提取、页面刷新、QQ/WX 二维码状态分发已显式类型化
97. Docker 内验证通过:
- `src/services/session/session.test.js` 共 14 个用例通过
- `npm run typecheck`
- `npm run build`
- `npm test` 共 139 个用例通过
## 下一步建议 ## 下一步建议