diff --git a/apps/backend/src/services/session/session-browser-config.js b/apps/backend/src/services/session/session-browser-config.ts similarity index 66% rename from apps/backend/src/services/session/session-browser-config.js rename to apps/backend/src/services/session/session-browser-config.ts index 7ccbe485..d0ea749b 100644 --- a/apps/backend/src/services/session/session-browser-config.js +++ b/apps/backend/src/services/session/session-browser-config.ts @@ -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' } diff --git a/apps/backend/src/services/session/session-payload.js b/apps/backend/src/services/session/session-payload.ts similarity index 61% rename from apps/backend/src/services/session/session-payload.js rename to apps/backend/src/services/session/session-payload.ts index 9df357e0..595e09ad 100644 --- a/apps/backend/src/services/session/session-payload.js +++ b/apps/backend/src/services/session/session-payload.ts @@ -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 = { 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), diff --git a/apps/backend/src/services/session/session-state.js b/apps/backend/src/services/session/session-state.ts similarity index 59% rename from apps/backend/src/services/session/session-state.js rename to apps/backend/src/services/session/session-state.ts index dcdcc393..9fa29631 100644 --- a/apps/backend/src/services/session/session-state.js +++ b/apps/backend/src/services/session/session-state.ts @@ -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) } diff --git a/docs/backend-typescript-migration-plan.md b/docs/backend-typescript-migration-plan.md index 95321827..0972ccc5 100644 --- a/docs/backend-typescript-migration-plan.md +++ b/docs/backend-typescript-migration-plan.md @@ -426,6 +426,16 @@ - `npm run typecheck` - `npm run build` - `npm test` 共 139 个用例通过 +83. 腾讯浏览器会话配置 / 状态 / payload 纯逻辑模块迁移到 `.ts`: + - `src/services/session/session-browser-config.ts` + - `src/services/session/session-state.ts` + - `src/services/session/session-payload.ts` +84. 登录类型、浏览器启动参数、默认视口、会话状态 / notice、二维码 TTL、redeem/review/artifact payload 已显式类型化;`buildSessionPayload` 对外暂保留动态返回以兼容仍未迁移的 JS 路由与 session 编排 +85. Docker 内验证通过: + - `src/services/session/session.test.js` 共 14 个用例通过 + - `npm run typecheck` + - `npm run build` + - `npm test` 共 139 个用例通过 ## 下一步建议