后端迁移会话状态载荷模块

This commit is contained in:
yml
2026-05-21 15:22:05 +08:00
parent a77439874b
commit 73b0ee211f
4 changed files with 176 additions and 22 deletions
@@ -1,8 +1,39 @@
import process from 'node:process'
export const DEFAULT_LOGIN_TYPE = 'qq'
export type LoginType = 'qq' | 'wx'
export function resolveBrowserLaunchOptions(options = {}) {
export const DEFAULT_LOGIN_TYPE: LoginType = 'qq'
type BrowserConfigInput = {
headless?: boolean
devtools?: boolean
keepAlive?: boolean
prewarm?: boolean
slowMoMs?: number
}
type ResolveBrowserLaunchOptionsInput = {
browserConfig?: BrowserConfigInput
chromePath?: string
nodeEnv?: string
ci?: string
}
export type BrowserLaunchOptions = {
headless: boolean
devtools: boolean
usesBundledChromium: boolean
keepAlive: boolean
prewarm: boolean
slowMoMs: number
}
export type ViewportSize = {
width: number
height: number
}
export function resolveBrowserLaunchOptions(options: ResolveBrowserLaunchOptionsInput = {}): BrowserLaunchOptions {
const browserConfig = options.browserConfig || {}
const chromePath = options.chromePath || ''
const nodeEnv = options.nodeEnv ?? process.env.NODE_ENV
@@ -37,7 +68,10 @@ export function resolveBrowserLaunchOptions(options = {}) {
}
}
export function buildChromiumLaunchOptions(launchOptions, { chromePath = '' } = {}) {
export function buildChromiumLaunchOptions(
launchOptions: BrowserLaunchOptions,
{ chromePath = '' }: { chromePath?: string } = {},
) {
const options = {
headless: launchOptions.headless,
devtools: launchOptions.devtools,
@@ -54,7 +88,7 @@ export function buildChromiumLaunchOptions(launchOptions, { chromePath = '' } =
return options
}
export function resolveDefaultViewport(rawValue = process.env.TENCENT_BROWSER_VNC_RESOLUTION) {
export function resolveDefaultViewport(rawValue = process.env.TENCENT_BROWSER_VNC_RESOLUTION): ViewportSize {
const fromVncResolution = parseViewportSpec(rawValue)
if (fromVncResolution) {
return fromVncResolution
@@ -63,7 +97,7 @@ export function resolveDefaultViewport(rawValue = process.env.TENCENT_BROWSER_VN
return { width: 1600, height: 900 }
}
export function parseViewportSpec(rawValue) {
export function parseViewportSpec(rawValue: unknown): ViewportSize | null {
const text = String(rawValue || '').trim()
if (!text) {
@@ -85,7 +119,7 @@ export function parseViewportSpec(rawValue) {
return { width, height }
}
export function normalizeLoginType(value, fallback = DEFAULT_LOGIN_TYPE) {
export function normalizeLoginType(value: unknown, fallback: LoginType = DEFAULT_LOGIN_TYPE): LoginType {
const normalized = String(value || fallback).trim().toLowerCase()
if (['qq', 'qc'].includes(normalized)) {
return 'qq'
@@ -96,6 +130,6 @@ export function normalizeLoginType(value, fallback = DEFAULT_LOGIN_TYPE) {
return fallback
}
export function getLoginTypeLabel(loginType) {
export function getLoginTypeLabel(loginType: LoginType | string): string {
return loginType === 'wx' ? '微信' : 'QQ'
}
@@ -1,14 +1,14 @@
export function buildSessionPayload(
session,
session: TencentBrowserSessionPayloadSource,
{
activityUrl = '',
includeQrImage = true,
sessionDebugEnabled = false,
browserDebug = null,
} = {},
) {
}: BuildSessionPayloadOptions = {},
): any {
const activityInfo = session.lastState?.activityInfo || null
const payload = {
const payload: Record<string, any> = {
sessionId: session.sessionId,
loginType: session.loginType,
activityUrl,
@@ -45,7 +45,65 @@ export function buildSessionPayload(
}
}
export function buildReviewPayload(review, { includeInternalPaths = false } = {}) {
type BuildSessionPayloadOptions = {
activityUrl?: string
includeQrImage?: boolean
sessionDebugEnabled?: boolean
browserDebug?: unknown
}
type InternalPathOptions = {
includeInternalPaths?: boolean
}
type TencentBrowserSessionPayloadSource = {
sessionId: string
loginType?: string
status?: string
notice?: string
createdAt?: string
updatedAt?: string
expiresAt: string | number | Date
qrUpdatedAt?: string
qrImageBase64?: string
qrImagePath?: string
sessionDir?: string
lastState?: {
credentialReady?: boolean
cookieKeys?: string[]
activityInfo?: any
[key: string]: unknown
} | null
lastReview?: ReviewPayloadSource | null
lastRedeem?: RedeemPayloadSource | null
}
type ReviewPayloadSource = {
capturedAt?: string
roleId?: string
roleName?: string
screenshotPath?: string
signature?: string
}
type RedeemAttemptPayloadSource = {
attempt?: number
redeem?: unknown
}
type RedeemPayloadSource = {
code?: string
area?: string
proofMode?: string
attempts?: RedeemAttemptPayloadSource[]
final?: RedeemAttemptPayloadSource | null
finishedAt?: string
screenshotPath?: string
htmlPath?: string
resultPath?: string
}
export function buildReviewPayload(review: ReviewPayloadSource | null | undefined, { includeInternalPaths = false }: InternalPathOptions = {}) {
if (!review) {
return null
}
@@ -67,7 +125,7 @@ export function buildReviewPayload(review, { includeInternalPaths = false } = {}
}
}
export function buildRedeemPayload(redeem, { includeInternalPaths = false } = {}) {
export function buildRedeemPayload(redeem: RedeemPayloadSource | null | undefined, { includeInternalPaths = false }: InternalPathOptions = {}) {
if (!redeem) {
return null
}
@@ -103,7 +161,13 @@ export function buildRedeemPayload(redeem, { includeInternalPaths = false } = {}
}
}
export function buildArtifactsPayload(session, { includeInternalPaths = false } = {}) {
export function buildArtifactsPayload(
session: Pick<
TencentBrowserSessionPayloadSource,
'sessionDir' | 'qrImagePath' | 'qrImageBase64' | 'lastReview' | 'lastRedeem'
>,
{ includeInternalPaths = false }: InternalPathOptions = {},
) {
const payload = {
hasQrImage: Boolean(session.qrImageBase64),
hasReviewScreenshot: Boolean(session.lastReview?.screenshotPath),
@@ -1,15 +1,57 @@
import { getLoginTypeLabel } from './session-browser-config.js'
import { getLoginTypeLabel, type LoginType } from './session-browser-config.js'
export const SESSION_TTL_MS = 30 * 60 * 1000
type InitialQrSessionStateOptions = {
updatedAt?: string
nowMs?: number
sessionTtlMs?: number
loginTypeLabel?: string
}
type HostState = {
loginText?: string
unloginVisible?: boolean
loginedVisible?: boolean
}
type QrState = {
expired?: boolean
scanned?: boolean
}
type ActivityInfo = {
role?: {
ready?: boolean
roleName?: string
}
}
type SessionStatusInput = {
hostState: HostState
qrState: QrState
credentialReady?: boolean
activityInfo?: ActivityInfo | null
}
type SessionNoticeInput = SessionStatusInput & {
loginType: LoginType | string
}
type CookieMap = {
openid?: string
access_token?: string
appid?: string
}
export function resolveInitialQrSessionState(
loginType,
loginType: LoginType | string,
{
updatedAt = new Date().toISOString(),
nowMs = Date.now(),
sessionTtlMs = SESSION_TTL_MS,
loginTypeLabel = getLoginTypeLabel(loginType),
} = {},
}: InitialQrSessionStateOptions = {},
) {
return {
status: 'waiting_scan',
@@ -19,7 +61,11 @@ export function resolveInitialQrSessionState(
}
}
export function resolveTencentSessionExpiresAt(session, nowMs = Date.now(), sessionTtlMs = SESSION_TTL_MS) {
export function resolveTencentSessionExpiresAt(
session: { autoCloseAt?: number | string } | null | undefined,
nowMs = Date.now(),
sessionTtlMs = SESSION_TTL_MS,
): number {
const autoCloseAt = Number(session?.autoCloseAt || 0)
if (autoCloseAt > 0) {
return autoCloseAt
@@ -28,13 +74,13 @@ export function resolveTencentSessionExpiresAt(session, nowMs = Date.now(), sess
return nowMs + sessionTtlMs
}
export function shouldRefreshLoggedInPresentation(hostState) {
export function shouldRefreshLoggedInPresentation(hostState: HostState): boolean {
const loginText = String(hostState?.loginText || '').trim()
return Boolean(hostState?.unloginVisible || !hostState?.loginedVisible || !loginText)
}
export function resolveSessionStatus({ hostState, qrState, credentialReady, activityInfo }) {
export function resolveSessionStatus({ hostState, qrState, credentialReady, activityInfo }: SessionStatusInput): string {
if (hostState.loginedVisible || credentialReady) {
if (credentialReady && activityInfo?.role?.ready) {
return 'ready_to_redeem'
@@ -60,7 +106,7 @@ export function resolveSessionNotice({
credentialReady,
activityInfo,
loginType,
}) {
}: SessionNoticeInput): string {
const loginTypeLabel = getLoginTypeLabel(loginType)
if (credentialReady) {
@@ -86,6 +132,6 @@ export function resolveSessionNotice({
return `请使用${loginTypeLabel}扫码登录`
}
export function hasRedeemCredential(cookieMap) {
export function hasRedeemCredential(cookieMap: CookieMap): boolean {
return Boolean(cookieMap.openid && cookieMap.access_token && cookieMap.appid)
}