后端迁移会话支撑模块
This commit is contained in:
+14
-3
@@ -5,8 +5,19 @@ const TENCENT_COOKIE_URLS = [
|
|||||||
'https://milo.qq.com',
|
'https://milo.qq.com',
|
||||||
]
|
]
|
||||||
|
|
||||||
export function normalizeTencentCookieMap(cookies = []) {
|
type BrowserCookie = {
|
||||||
const cookieMap = {}
|
name?: string
|
||||||
|
value?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type BrowserContextLike = {
|
||||||
|
cookies: (urls?: string[]) => Promise<BrowserCookie[]>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TencentCookieMap = Record<string, string>
|
||||||
|
|
||||||
|
export function normalizeTencentCookieMap(cookies: BrowserCookie[] = []): TencentCookieMap {
|
||||||
|
const cookieMap: TencentCookieMap = {}
|
||||||
|
|
||||||
for (const cookie of cookies) {
|
for (const cookie of cookies) {
|
||||||
if (!cookie?.name) {
|
if (!cookie?.name) {
|
||||||
@@ -32,7 +43,7 @@ export function normalizeTencentCookieMap(cookies = []) {
|
|||||||
return cookieMap
|
return cookieMap
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadCookieMapFromContext(browserContext) {
|
export async function loadCookieMapFromContext(browserContext: BrowserContextLike): Promise<TencentCookieMap> {
|
||||||
const cookies = await browserContext.cookies(TENCENT_COOKIE_URLS)
|
const cookies = await browserContext.cookies(TENCENT_COOKIE_URLS)
|
||||||
return normalizeTencentCookieMap(cookies)
|
return normalizeTencentCookieMap(cookies)
|
||||||
}
|
}
|
||||||
+40
-8
@@ -1,11 +1,33 @@
|
|||||||
// @ts-nocheck
|
|
||||||
|
|
||||||
import { runtimeConfig } from '../../config/runtime.js'
|
import { runtimeConfig } from '../../config/runtime.js'
|
||||||
import { logDebug } from '../../utils/logger.js'
|
import { logDebug } from '../../utils/logger.js'
|
||||||
|
|
||||||
const SESSION_DEBUG_ENABLED = Boolean(runtimeConfig.session.debug)
|
const SESSION_DEBUG_ENABLED = Boolean(runtimeConfig.session.debug)
|
||||||
|
|
||||||
export function logBrowserSessionDebug(scope, detail) {
|
type FrameLike = unknown
|
||||||
|
|
||||||
|
type PageWithFrames = {
|
||||||
|
frames: () => FrameLike[]
|
||||||
|
waitForTimeout: (milliseconds: number) => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
type QrDownloadResponse = {
|
||||||
|
ok: () => boolean
|
||||||
|
status: () => number
|
||||||
|
body: () => Promise<Buffer>
|
||||||
|
}
|
||||||
|
|
||||||
|
type PageWithRequestContext = {
|
||||||
|
context: () => {
|
||||||
|
request: {
|
||||||
|
get: (url: string, options?: {
|
||||||
|
failOnStatusCode?: boolean
|
||||||
|
headers?: Record<string, string>
|
||||||
|
}) => Promise<QrDownloadResponse>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logBrowserSessionDebug(scope: string, detail?: unknown): void {
|
||||||
if (!SESSION_DEBUG_ENABLED) {
|
if (!SESSION_DEBUG_ENABLED) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -13,13 +35,17 @@ export function logBrowserSessionDebug(scope, detail) {
|
|||||||
logDebug('[browser/session]', scope, detail)
|
logDebug('[browser/session]', scope, detail)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function waitForFrame(page, predicate, timeoutMs = 15_000) {
|
export async function waitForFrame<TFrame extends FrameLike>(
|
||||||
|
page: PageWithFrames,
|
||||||
|
predicate: (frame: TFrame) => boolean,
|
||||||
|
timeoutMs = 15_000,
|
||||||
|
): Promise<TFrame> {
|
||||||
const startedAt = Date.now()
|
const startedAt = Date.now()
|
||||||
|
|
||||||
while (Date.now() - startedAt < timeoutMs) {
|
while (Date.now() - startedAt < timeoutMs) {
|
||||||
const matched = page.frames().find((frame) => {
|
const matched = page.frames().find((frame): frame is TFrame => {
|
||||||
try {
|
try {
|
||||||
return predicate(frame)
|
return predicate(frame as TFrame)
|
||||||
} catch {
|
} catch {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -35,7 +61,13 @@ export async function waitForFrame(page, predicate, timeoutMs = 15_000) {
|
|||||||
throw new Error('未找到登录二维码 frame')
|
throw new Error('未找到登录二维码 frame')
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function downloadRemoteQrImage(page, qrUrl, qrImagePath, referer = '', loginType = 'unknown') {
|
export async function downloadRemoteQrImage(
|
||||||
|
page: PageWithRequestContext,
|
||||||
|
qrUrl: string,
|
||||||
|
qrImagePath: string,
|
||||||
|
referer = '',
|
||||||
|
loginType = 'unknown',
|
||||||
|
): Promise<Buffer | false | null> {
|
||||||
try {
|
try {
|
||||||
const response = await page.context().request.get(qrUrl, {
|
const response = await page.context().request.get(qrUrl, {
|
||||||
failOnStatusCode: false,
|
failOnStatusCode: false,
|
||||||
@@ -68,7 +100,7 @@ export async function downloadRemoteQrImage(page, qrUrl, qrImagePath, referer =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isRetryableQrCaptureError(error) {
|
export function isRetryableQrCaptureError(error: unknown): boolean {
|
||||||
const message = error instanceof Error ? error.message : String(error || '')
|
const message = error instanceof Error ? error.message : String(error || '')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
+25
-2
@@ -2,7 +2,30 @@ import path from 'node:path'
|
|||||||
|
|
||||||
import { logBrowserSessionDebug } from './session-login-shared.js'
|
import { logBrowserSessionDebug } from './session-login-shared.js'
|
||||||
|
|
||||||
export function resolveReviewSignature(session, activityInfo) {
|
type BrowserPageLike = {
|
||||||
|
screenshot: (options: { path: string; fullPage?: boolean }) => Promise<unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReviewSession = {
|
||||||
|
sessionId: string
|
||||||
|
loginType: string
|
||||||
|
sessionDir: string
|
||||||
|
page: BrowserPageLike
|
||||||
|
lastReview?: {
|
||||||
|
screenshotPath?: string
|
||||||
|
signature?: string
|
||||||
|
[key: string]: unknown
|
||||||
|
} | null
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActivityInfoForReview = {
|
||||||
|
role?: {
|
||||||
|
roleId?: string
|
||||||
|
roleName?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveReviewSignature(session: Pick<ReviewSession, 'loginType'>, activityInfo: ActivityInfoForReview) {
|
||||||
const roleId = String(activityInfo?.role?.roleId || '').trim()
|
const roleId = String(activityInfo?.role?.roleId || '').trim()
|
||||||
const roleName = String(activityInfo?.role?.roleName || '').trim()
|
const roleName = String(activityInfo?.role?.roleName || '').trim()
|
||||||
|
|
||||||
@@ -13,7 +36,7 @@ export function resolveReviewSignature(session, activityInfo) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function ensureReviewScreenshot(session, activityInfo) {
|
export async function ensureReviewScreenshot(session: ReviewSession, activityInfo: ActivityInfoForReview): Promise<boolean> {
|
||||||
const { roleId, roleName, signature } = resolveReviewSignature(session, activityInfo)
|
const { roleId, roleName, signature } = resolveReviewSignature(session, activityInfo)
|
||||||
|
|
||||||
if (session.lastReview?.screenshotPath && session.lastReview?.signature === signature) {
|
if (session.lastReview?.screenshotPath && session.lastReview?.signature === signature) {
|
||||||
-2
@@ -1,5 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
import { fileURLToPath } from 'node:url'
|
import { fileURLToPath } from 'node:url'
|
||||||
|
|
||||||
@@ -436,6 +436,17 @@
|
|||||||
- `npm run typecheck`
|
- `npm run typecheck`
|
||||||
- `npm run build`
|
- `npm run build`
|
||||||
- `npm test` 共 139 个用例通过
|
- `npm test` 共 139 个用例通过
|
||||||
|
86. 腾讯浏览器会话 cookie / review / 登录共享 / 服务配置模块迁移到 `.ts`:
|
||||||
|
- `src/services/session/session-cookie-map.ts`
|
||||||
|
- `src/services/session/session-review.ts`
|
||||||
|
- `src/services/session/session-login-shared.ts`
|
||||||
|
- `src/services/session/session-service-config.ts`
|
||||||
|
87. cookie map、review 签名与截图、二维码下载、frame 等待、浏览器会话配置常量已显式类型化
|
||||||
|
88. Docker 内验证通过:
|
||||||
|
- `src/services/session/session.test.js` 共 14 个用例通过
|
||||||
|
- `npm run typecheck`
|
||||||
|
- `npm run build`
|
||||||
|
- `npm test` 共 139 个用例通过
|
||||||
|
|
||||||
## 下一步建议
|
## 下一步建议
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user