Files
order_site/apps/backend/src/services/session/session.js
T

974 lines
26 KiB
JavaScript

// @ts-nocheck
import crypto from 'node:crypto'
import fs from 'node:fs/promises'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { chromium } from 'playwright'
import { runtimeConfig } from '../../config/runtime.js'
import {
buildChromiumLaunchOptions as buildChromiumLaunchOptionsBase,
getLoginTypeLabel,
normalizeLoginType,
resolveBrowserLaunchOptions as resolveBrowserLaunchOptionsBase,
resolveDefaultViewport,
} from './session-browser-config.js'
import {
isRetryableQrCaptureError,
logBrowserSessionDebug,
} from './session-login-shared.js'
import {
buildSessionPayload as buildSessionPayloadBase,
} from './session-payload.js'
import {
captureQqQrImage,
ensureQqLoginReady,
extractQqQrState,
} from './session-qq.js'
import {
saveRedeemArtifacts,
} from './session-proof.js'
import {
capturePageCaptchaForOcr,
isCaptchaRejectedResult,
prepareRedeemCodeFill,
refreshPageCaptcha,
runTencentBrowserRedeem,
submitRedeemInBrowser,
} from './session-redeem.js'
import {
captureWxQrImage,
ensureWxLoginReady,
extractWxQrState,
} from './session-wx.js'
import { recognizeTencentCaptcha } from './ocr.js'
import {
hasRedeemCredential,
resolveSessionNotice,
resolveSessionStatus,
resolveTencentSessionExpiresAt,
SESSION_TTL_MS,
shouldRefreshLoggedInPresentation,
} from './session-state.js'
const CURRENT_DIR = path.dirname(fileURLToPath(import.meta.url))
const PROJECT_ROOT = path.resolve(CURRENT_DIR, '../../..')
const DATA_ROOT = path.resolve(PROJECT_ROOT, 'data/browser-sessions')
const ACTIVITY_URL = 'https://df.qq.com/cp/a20240812cdk/index.html'
const CHROME_PATH = String(runtimeConfig.browser.chromePath || '').trim()
export const REDEEM_SUCCESS_AUTO_CLOSE_MS = 3 * 60 * 1000
const DEFAULT_VIEWPORT = resolveDefaultViewport()
const SESSION_DEBUG_ENABLED = Boolean(runtimeConfig.session.debug)
const sessions = new Map()
let browserPromise = null
export { resolveTencentSessionExpiresAt }
class SessionServiceError extends Error {
constructor(message, { statusCode = 500, errorCode = 'session_error' } = {}) {
super(message)
this.name = 'SessionServiceError'
this.statusCode = statusCode
this.errorCode = errorCode
}
}
export async function warmupTencentBrowser() {
const browserLaunch = resolveBrowserLaunchOptions()
if (!browserLaunch.prewarm) {
return {
warmed: false,
reason: 'disabled',
browserLaunch,
}
}
await ensureBrowser()
return {
warmed: true,
reason: 'ready',
browserLaunch,
}
}
export async function createTencentBrowserSession(payload = {}) {
await cleanupExpiredTencentBrowserSessions()
const loginType = normalizeLoginType(payload.loginType || payload.qrType)
const browser = await ensureBrowser()
const sessionId = createSessionId()
const sessionDir = path.join(DATA_ROOT, sessionId)
await fs.mkdir(sessionDir, { recursive: true })
const browserContext = await browser.newContext({
viewport: DEFAULT_VIEWPORT,
deviceScaleFactor: 1,
})
const page = await browserContext.newPage()
page.setDefaultTimeout(20_000)
const session = {
sessionId,
loginType,
status: 'created',
notice: '浏览器会话已创建',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
expiresAt: Date.now() + SESSION_TTL_MS,
autoCloseAt: 0,
autoCloseTimer: null,
sessionDir,
browserContext,
page,
qrImageBase64: '',
qrImagePath: '',
qrUpdatedAt: '',
lastState: null,
lastRedeem: null,
lastReview: null,
lastError: '',
presentationSyncAttempts: 0,
}
sessions.set(sessionId, session)
try {
await page.goto(ACTIVITY_URL, { waitUntil: 'domcontentloaded' })
await ensureLoginTab(page, loginType)
await captureSessionQr(session, { ensureLoginReady: false })
applyInitialQrSessionState(session)
await persistSessionState(session)
void refreshTencentBrowserSession(sessionId).catch((error) => {
logBrowserSessionDebug('createSession.backgroundRefresh.error', {
sessionId,
message: error instanceof Error ? error.message : String(error),
})
})
return buildSessionPayload(session)
} catch (error) {
session.status = 'failed'
session.notice = error instanceof Error ? error.message : `${getLoginTypeLabel(loginType)} 浏览器会话初始化失败`
session.lastError = session.notice
session.updatedAt = new Date().toISOString()
await persistSessionState(session)
throw error
}
}
export async function getTencentBrowserSession(sessionId, options = {}) {
return refreshTencentBrowserSession(sessionId, options)
}
export async function getTencentBrowserSessionSummary(sessionId) {
return refreshTencentBrowserSession(sessionId, { includeQrImage: false })
}
export async function reloadTencentBrowserSession(sessionId) {
await cleanupExpiredTencentBrowserSessions()
const session = await getRequiredSession(sessionId)
if (session.browserContextClosed) {
throw new SessionServiceError('浏览器会话已关闭', {
statusCode: 410,
errorCode: 'session_closed',
})
}
try {
await reloadActivityPageForPresentation(session.page)
const cookieMap = await loadCookieMapFromContext(session.browserContext)
const credentialReady = hasRedeemCredential(cookieMap)
if (!credentialReady) {
session.qrImageBase64 = ''
session.qrImagePath = ''
session.qrUpdatedAt = ''
await ensureLoginTab(session.page, session.loginType)
await captureSessionQr(session)
}
session.notice = credentialReady
? '后端页面已刷新,正在同步最新状态'
: `已刷新${getLoginTypeLabel(session.loginType)}登录页,准备重新抓取二维码`
session.updatedAt = new Date().toISOString()
return await refreshTencentBrowserSession(sessionId)
} catch (error) {
session.status = 'failed'
session.notice = error instanceof Error ? error.message : '刷新后端页面失败'
session.lastError = session.notice
session.updatedAt = new Date().toISOString()
await persistSessionState(session)
throw error
}
}
export async function refreshTencentBrowserSession(sessionId, { includeQrImage = true } = {}) {
await cleanupExpiredTencentBrowserSessions()
const session = await getRequiredSession(sessionId)
if (session.browserContextClosed) {
throw new SessionServiceError('浏览器会话已关闭', {
statusCode: 410,
errorCode: 'session_closed',
})
}
try {
let hostState = await extractHostState(session.page)
const cookieMap = await loadCookieMapFromContext(session.browserContext)
const credentialReady = hasRedeemCredential(cookieMap)
if (credentialReady) {
const presentation = await ensureLoggedInPresentation(session, { hostState, credentialReady })
hostState = presentation.hostState
} else {
session.presentationSyncAttempts = 0
}
if (!credentialReady && session.loginType === 'wx') {
await ensureWxLoginReady(session.page)
}
const qrState = await extractQrState(session.page, session.loginType)
const activityInfo = credentialReady
? await ensureActivityInfoReady(session.page, { triggerRender: true })
: await extractActivityInfo(session.page)
session.lastState = {
hostState,
qrState,
credentialReady,
activityInfo,
cookieKeys: Object.keys(cookieMap),
}
session.status = resolveSessionStatus({ hostState, qrState, credentialReady, activityInfo })
session.notice = resolveSessionNotice({
hostState,
qrState,
credentialReady,
activityInfo,
loginType: session.loginType,
})
if (credentialReady && activityInfo?.role?.ready) {
await ensureReviewScreenshot(session, activityInfo)
}
session.updatedAt = new Date().toISOString()
session.expiresAt = resolveTencentSessionExpiresAt(session)
if (!session.qrImageBase64 || session.status === 'expired') {
if (session.status === 'expired') {
await reloadActivityPageForPresentation(session.page)
await ensureLoginTab(session.page, session.loginType)
}
await captureSessionQr(session)
if (session.status === 'expired') {
session.notice = '二维码已刷新,请重新扫码'
session.status = 'waiting_scan'
}
}
await persistSessionState(session)
return buildSessionPayload(session, { includeQrImage })
} catch (error) {
session.status = 'failed'
session.notice = error instanceof Error ? error.message : `查询 ${getLoginTypeLabel(session.loginType)} 浏览器会话状态失败`
session.lastError = session.notice
session.updatedAt = new Date().toISOString()
await persistSessionState(session)
return buildSessionPayload(session, { includeQrImage })
}
}
export async function redeemTencentBrowserSession(sessionId, payload = {}) {
const session = await getRequiredSession(sessionId)
const code = String(payload.code || payload.sCdKey || '').trim()
const maxAttempts = Math.max(1, Math.min(Number(payload.maxAttempts || 6), 12))
if (!code) {
throw new SessionServiceError('缺少兑换码', {
statusCode: 400,
errorCode: 'missing_redeem_code',
})
}
const refreshed = await refreshTencentBrowserSession(sessionId)
if (!['logged_in', 'ready_to_redeem', 'redeemed'].includes(refreshed.status)) {
throw new SessionServiceError(`当前会话还不能兑换,状态为 ${refreshed.status}`, {
statusCode: 409,
errorCode: 'session_not_ready',
})
}
resetTencentSessionAutoClose(session)
await runTencentBrowserRedeem({
session,
code,
maxAttempts,
ensureLoggedInPresentation,
ensureActivityInfoReady,
fillRedeemCodeInBrowser,
capturePageCaptchaForOcr: (page, options) =>
capturePageCaptchaForOcr(page, { ...options, ensureActivityInfoReady }),
recognizeTencentCaptcha,
submitRedeemInBrowser: (page, redeemPayload) =>
submitRedeemInBrowser(page, redeemPayload, { ensureActivityInfoReady }),
isCaptchaRejectedResult,
refreshPageCaptcha,
persistSessionState,
buildSessionPayload,
saveRedeemArtifacts,
activityUrl: ACTIVITY_URL,
})
armRedeemedSessionAutoClose(session)
await persistSessionState(session)
return {
...buildSessionPayload(session),
redeem: session.lastRedeem,
}
}
export async function closeTencentBrowserSession(sessionId) {
const session = await getRequiredSession(sessionId)
await closeSession(session, { markClosed: true })
return {
sessionId,
closed: true,
}
}
export async function closeAllTencentBrowserSessions({ markClosed = true } = {}) {
const activeSessions = [...sessions.values()]
await Promise.all(activeSessions.map((session) => closeSession(session, { markClosed })))
return {
closedCount: activeSessions.length,
}
}
export async function getTencentBrowserSessionScreenshotPath(sessionId) {
const session = await getRequiredSession(sessionId)
const screenshotPath = session.lastRedeem?.screenshotPath
if (!screenshotPath) {
throw new SessionServiceError('当前会话还没有兑换截图', {
statusCode: 404,
errorCode: 'screenshot_not_ready',
})
}
return screenshotPath
}
export async function getTencentBrowserSessionReviewScreenshotPath(sessionId) {
const session = await getRequiredSession(sessionId)
const screenshotPath = session.lastReview?.screenshotPath
if (!screenshotPath) {
throw new SessionServiceError('当前会话还没有客服复核截图', {
statusCode: 404,
errorCode: 'review_screenshot_not_ready',
})
}
return screenshotPath
}
async function ensureBrowser() {
if (!browserPromise) {
const launchOptions = resolveBrowserLaunchOptions()
browserPromise = chromium.launch(buildChromiumLaunchOptions(launchOptions))
.then((browser) => {
browser.on('disconnected', () => {
browserPromise = null
})
return browser
})
.catch((error) => {
browserPromise = null
throw error
})
}
return browserPromise
}
function resolveBrowserLaunchOptions() {
return resolveBrowserLaunchOptionsBase({
browserConfig: runtimeConfig.browser,
chromePath: CHROME_PATH,
})
}
function buildChromiumLaunchOptions(launchOptions) {
return buildChromiumLaunchOptionsBase(launchOptions, { chromePath: CHROME_PATH })
}
function createSessionId() {
let sessionId = ''
do {
sessionId = `txbs-${crypto.randomBytes(6).toString('hex')}`
} while (sessions.has(sessionId))
return sessionId
}
async function getRequiredSession(sessionId) {
const key = String(sessionId || '').trim()
if (!key) {
throw new SessionServiceError('缺少 sessionId', {
statusCode: 400,
errorCode: 'missing_session_id',
})
}
const session = sessions.get(key)
if (!session) {
throw new SessionServiceError('浏览器会话不存在或已过期', {
statusCode: 404,
errorCode: 'session_not_found',
})
}
return session
}
async function captureSessionQr(session, { ensureLoginReady = true } = {}) {
if (ensureLoginReady) {
await ensureLoginTab(session.page, session.loginType)
}
const qrImagePath = path.join(session.sessionDir, `${session.loginType}-qr.png`)
await captureQrImageWithRetry(session.page, session.loginType, qrImagePath)
session.qrImagePath = qrImagePath
session.qrImageBase64 = await fs.readFile(qrImagePath, 'base64')
session.qrUpdatedAt = new Date().toISOString()
session.updatedAt = new Date().toISOString()
}
async function captureQrImageWithRetry(page, loginType, qrImagePath) {
const maxAttempts = loginType === 'qq' ? 4 : 3
let lastError = null
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
try {
logBrowserSessionDebug('captureQrImageWithRetry.start', {
loginType,
attempt,
maxAttempts,
qrImagePath,
})
if (loginType === 'qq') {
await captureQqQrImage(page, qrImagePath)
} else {
await captureWxQrImage(page, qrImagePath)
}
logBrowserSessionDebug('captureQrImageWithRetry.success', {
loginType,
attempt,
qrImagePath,
})
return
} catch (error) {
lastError = error
logBrowserSessionDebug('captureQrImageWithRetry.error', {
loginType,
attempt,
message: error instanceof Error ? error.message : String(error),
})
if (!isRetryableQrCaptureError(error) || attempt === maxAttempts) {
throw error
}
await page.waitForTimeout(500)
await ensureLoginTab(page, loginType)
}
}
if (lastError) {
throw lastError
}
throw new Error('二维码截图失败')
}
async function ensureLoginTab(page, loginType) {
const tabSelector = loginType === 'wx' ? '.wx-tab' : '.qc-tab'
const loginButton = page.locator('#unlogin')
let interacted = false
if (await loginButton.count()) {
try {
await loginButton.first().click({ timeout: 2_000 })
interacted = true
} catch {
// ignore when the login layer is already open
}
}
const tab = page.locator(tabSelector).first()
if (await tab.count()) {
try {
await tab.click({ timeout: 2_000 })
interacted = true
} catch {
// ignore tab click failures
}
}
if (interacted) {
await page.waitForTimeout(250)
}
if (loginType === 'qq') {
await ensureQqLoginReady(page)
return
}
if (loginType === 'wx') {
await ensureWxLoginReady(page)
}
}
function applyInitialQrSessionState(session) {
resetTencentSessionAutoClose(session)
session.status = 'waiting_scan'
session.notice = `请使用${getLoginTypeLabel(session.loginType)}扫码登录`
session.updatedAt = new Date().toISOString()
session.expiresAt = Date.now() + SESSION_TTL_MS
}
function armRedeemedSessionAutoClose(session) {
resetTencentSessionAutoClose(session)
const autoCloseAt = Date.now() + REDEEM_SUCCESS_AUTO_CLOSE_MS
session.autoCloseAt = autoCloseAt
session.expiresAt = autoCloseAt
session.autoCloseTimer = setTimeout(() => {
void closeSession(session, { markClosed: true }).catch(() => {
// ignore delayed auto-close failures
})
}, REDEEM_SUCCESS_AUTO_CLOSE_MS)
}
function resetTencentSessionAutoClose(session) {
if (session?.autoCloseTimer) {
clearTimeout(session.autoCloseTimer)
session.autoCloseTimer = null
}
if (session) {
session.autoCloseAt = 0
}
}
async function extractHostState(page) {
return page.evaluate(() => {
const isVisible = (element) => {
if (!element) {
return false
}
const style = window.getComputedStyle(element)
return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0'
}
const unlogin = document.querySelector('#unlogin')
const logined = document.querySelector('#logined')
return {
unloginVisible: isVisible(unlogin),
loginedVisible: isVisible(logined),
loginText: String(logined?.textContent || '').trim(),
}
})
}
async function ensureLoggedInPresentation(
session,
{ hostState = null, credentialReady = false } = {},
) {
let nextHostState = hostState || (await extractHostState(session.page))
if (!credentialReady || !shouldRefreshLoggedInPresentation(nextHostState)) {
return {
reloaded: false,
hostState: nextHostState,
}
}
if (session.presentationSyncAttempts >= 2) {
return {
reloaded: false,
hostState: nextHostState,
}
}
session.presentationSyncAttempts += 1
await reloadActivityPageForPresentation(session.page)
nextHostState = await extractHostState(session.page)
return {
reloaded: true,
hostState: nextHostState,
}
}
async function reloadActivityPageForPresentation(page) {
try {
await page.reload({ waitUntil: 'domcontentloaded' })
} catch {
await page.goto(ACTIVITY_URL, { waitUntil: 'domcontentloaded' })
}
await page.waitForTimeout(2_500)
}
async function extractQrState(page, loginType) {
if (loginType === 'qq') {
return extractQqQrState(page)
}
return extractWxQrState(page)
}
async function ensureActivityInfoReady(page, { triggerRender = false, timeoutMs = 8_000 } = {}) {
if (triggerRender) {
await triggerActivityRoleRender(page)
}
const startedAt = Date.now()
let lastInfo = await extractActivityInfo(page)
while (Date.now() - startedAt < timeoutMs) {
if (lastInfo?.role?.ready) {
return lastInfo
}
await page.waitForTimeout(400)
lastInfo = await extractActivityInfo(page)
}
return lastInfo
}
async function triggerActivityRoleRender(page) {
try {
await page.evaluate(() => {
const maybeRender = window.renderCdkey
if (typeof maybeRender === 'function') {
maybeRender()
}
})
} catch {
// ignore render trigger failures; polling will still inspect the page
}
}
async function extractActivityInfo(page) {
try {
return await page.evaluate(() => {
const asString = (value) => (typeof value === 'string' ? value.trim() : '')
const isVisible = (element) => {
if (!element) {
return false
}
const style = window.getComputedStyle(element)
return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0'
}
const pick = (selector) => document.querySelector(selector)
const pickBest = (selector, predicate = null) => {
const matches = [...document.querySelectorAll(selector)]
const preferred = matches.find((element) => {
if (predicate && !predicate(element)) {
return false
}
return isVisible(element)
})
if (preferred) {
return preferred
}
if (predicate) {
return matches.find((element) => predicate(element)) || null
}
return matches[0] || null
}
const textOf = (selector) => asString(pick(selector)?.textContent || '')
const inputOf = (selector) => {
const element = pickBest(
selector,
(node) => node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement,
)
if (!(element instanceof HTMLInputElement) && !(element instanceof HTMLTextAreaElement)) {
return ''
}
return asString(element.value || '')
}
const roleData = window.roleData && typeof window.roleData === 'object' ? window.roleData : {}
const roleSelector = pick('[id^="milo_role_selector_"]')
const roleNameText =
textOf('#role_name') ||
asString(roleSelector?.selectedOptions?.[0]?.textContent || '')
const verifyImg = pickBest('[id^="milo_verifyImg_"]', (node) => node instanceof HTMLImageElement)
const popup = pick('#pop2')
const cdkeyInput = pickBest(
'[id^="milo_cdkeyInfo_"]',
(node) =>
(node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement) &&
!node.disabled &&
node.type !== 'hidden',
)
const verifyInput = pickBest(
'[id^="milo_verifyInput_"]',
(node) =>
(node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement) &&
!node.disabled &&
node.type !== 'hidden',
)
const submitButton = pickBest(
'#milo_cdkey_submit, [id^="milo_cdkey_submit"]',
(node) => node instanceof HTMLElement && !node.hasAttribute('disabled'),
)
return {
nickname: textOf('#login_nickname_span'),
role: {
ready: Boolean(window.isRole || roleNameText),
roleName: roleNameText,
roleId: asString(roleData.sRoleId),
area: asString(roleData.sArea || '36'),
partition: asString(roleData.sPartition),
platId: asString(roleData.sPlatId),
md5str: asString(roleData.sMd5str || roleData.md5str || roleData.sMdStr),
checkparam: asString(roleData.sCheckparam),
},
form: {
cdkeyInputId: cdkeyInput?.id || '',
cdkeyValue: inputOf('[id^="milo_cdkeyInfo_"]'),
verifyInputId: verifyInput?.id || '',
verifyValue: inputOf('[id^="milo_verifyInput_"]'),
verifyImgId: verifyImg?.id || '',
submitId: submitButton?.id || '',
},
verify: {
visible: isVisible(verifyImg),
src: asString(verifyImg?.getAttribute('src')),
naturalWidth: Number(verifyImg?.naturalWidth || 0),
naturalHeight: Number(verifyImg?.naturalHeight || 0),
},
popup: {
visible: isVisible(popup),
text: textOf('#PopText'),
detail: textOf('#PopText2'),
},
}
})
} catch {
return {
nickname: '',
role: {
ready: false,
roleName: '',
roleId: '',
area: '',
partition: '',
platId: '',
md5str: '',
checkparam: '',
},
form: {
cdkeyInputId: '',
cdkeyValue: '',
verifyInputId: '',
verifyValue: '',
verifyImgId: '',
submitId: '',
},
verify: {
visible: false,
src: '',
naturalWidth: 0,
naturalHeight: 0,
},
popup: {
visible: false,
text: '',
detail: '',
},
}
}
}
async function cleanupExpiredTencentBrowserSessions() {
const expired = [...sessions.values()].filter((session) => session.expiresAt <= Date.now())
await Promise.all(expired.map((session) => closeSession(session, { markClosed: false })))
}
async function closeSession(session, { markClosed }) {
resetTencentSessionAutoClose(session)
sessions.delete(session.sessionId)
try {
await session.browserContext?.close()
} catch {
// ignore browser context close failures
}
session.browserContextClosed = true
session.updatedAt = new Date().toISOString()
if (markClosed) {
session.status = 'closed'
session.notice = '浏览器会话已关闭'
await persistSessionState(session)
}
await closeBrowserIfIdle()
}
async function persistSessionState(session) {
await fs.mkdir(session.sessionDir, { recursive: true })
await fs.writeFile(
path.join(session.sessionDir, 'session.json'),
JSON.stringify(buildSessionPayload(session), null, 2),
'utf8',
)
}
function buildSessionPayload(session, { includeQrImage = true } = {}) {
return buildSessionPayloadBase(session, {
activityUrl: ACTIVITY_URL,
includeQrImage,
sessionDebugEnabled: SESSION_DEBUG_ENABLED,
browserDebug: SESSION_DEBUG_ENABLED ? resolveBrowserLaunchOptions() : null,
})
}
async function ensureReviewScreenshot(session, activityInfo) {
const roleId = String(activityInfo?.role?.roleId || '').trim()
const roleName = String(activityInfo?.role?.roleName || '').trim()
const signature = `${session.loginType}|${roleId}|${roleName}`
if (session.lastReview?.screenshotPath && session.lastReview?.signature === signature) {
return
}
const screenshotPath = path.join(session.sessionDir, 'role-review.png')
try {
await session.page.screenshot({
path: screenshotPath,
fullPage: true,
})
session.lastReview = {
screenshotPath,
capturedAt: new Date().toISOString(),
roleId,
roleName,
signature,
}
} catch (error) {
logBrowserSessionDebug('reviewScreenshot.capture.error', {
sessionId: session.sessionId,
message: error instanceof Error ? error.message : String(error),
})
}
}
async function loadCookieMapFromContext(browserContext) {
const cookies = await browserContext.cookies([
'https://df.qq.com',
'https://ssl.captcha.qq.com',
'https://graph.qq.com',
'https://milo.qq.com',
])
const cookieMap = {}
for (const cookie of cookies) {
if (!cookie?.name) {
continue
}
cookieMap[String(cookie.name)] = String(cookie.value || '')
}
if (!cookieMap.acctype) {
cookieMap.acctype = 'qc'
}
if (!cookieMap.appid) {
cookieMap.appid = '101491592'
}
if (cookieMap.openid && cookieMap.access_token && !cookieMap.iegams_milo_proxylogin_qc) {
cookieMap.iegams_milo_proxylogin_qc =
`${cookieMap.appid}_$$_${cookieMap.openid}_$$_${cookieMap.access_token}`
}
return cookieMap
}
async function fillRedeemCodeInBrowser(page, code) {
await prepareRedeemCodeFill(page, code, { ensureActivityInfoReady })
}
async function closeBrowserIfIdle() {
if (sessions.size > 0 || !browserPromise) {
return
}
if (resolveBrowserLaunchOptions().keepAlive) {
return
}
try {
const browser = await browserPromise
await browser.close()
} catch {
// ignore browser close failures
} finally {
browserPromise = null
}
}