下沉浏览器会话基础运行时逻辑
This commit is contained in:
@@ -0,0 +1,17 @@
|
|||||||
|
// @ts-check
|
||||||
|
|
||||||
|
import path from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
|
||||||
|
import { runtimeConfig } from '../../config/runtime.js'
|
||||||
|
import { resolveDefaultViewport } from './session-browser-config.js'
|
||||||
|
|
||||||
|
const CURRENT_DIR = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
const PROJECT_ROOT = path.resolve(CURRENT_DIR, '../../..')
|
||||||
|
|
||||||
|
export const DATA_ROOT = path.resolve(PROJECT_ROOT, 'data/browser-sessions')
|
||||||
|
export const ACTIVITY_URL = 'https://df.qq.com/cp/a20240812cdk/index.html'
|
||||||
|
export const CHROME_PATH = String(runtimeConfig.browser.chromePath || '').trim()
|
||||||
|
export const REDEEM_SUCCESS_AUTO_CLOSE_MS = 3 * 60 * 1000
|
||||||
|
export const DEFAULT_VIEWPORT = resolveDefaultViewport()
|
||||||
|
export const SESSION_DEBUG_ENABLED = Boolean(runtimeConfig.session.debug)
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
// @ts-check
|
||||||
|
|
||||||
|
import { chromium } from 'playwright'
|
||||||
|
|
||||||
|
import { runtimeConfig } from '../../config/runtime.js'
|
||||||
|
import {
|
||||||
|
buildChromiumLaunchOptions as buildChromiumLaunchOptionsBase,
|
||||||
|
resolveBrowserLaunchOptions as resolveBrowserLaunchOptionsBase,
|
||||||
|
} from './session-browser-config.js'
|
||||||
|
import { CHROME_PATH } from './session-service-config.js'
|
||||||
|
|
||||||
|
let browserPromise = null
|
||||||
|
|
||||||
|
export 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 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
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveBrowserLaunchOptions() {
|
||||||
|
return resolveBrowserLaunchOptionsBase({
|
||||||
|
browserConfig: runtimeConfig.browser,
|
||||||
|
chromePath: CHROME_PATH,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function closeBrowserIfIdle({ activeSessionCount = 0 } = {}) {
|
||||||
|
if (activeSessionCount > 0 || !browserPromise) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resolveBrowserLaunchOptions().keepAlive) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const browser = await browserPromise
|
||||||
|
await browser.close()
|
||||||
|
} catch {
|
||||||
|
// ignore browser close failures
|
||||||
|
} finally {
|
||||||
|
browserPromise = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildChromiumLaunchOptions(launchOptions) {
|
||||||
|
return buildChromiumLaunchOptionsBase(launchOptions, { chromePath: CHROME_PATH })
|
||||||
|
}
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
// @ts-check
|
||||||
|
|
||||||
|
import crypto from 'node:crypto'
|
||||||
|
import fs from 'node:fs/promises'
|
||||||
|
import path from 'node:path'
|
||||||
|
|
||||||
|
import {
|
||||||
|
DATA_ROOT,
|
||||||
|
REDEEM_SUCCESS_AUTO_CLOSE_MS,
|
||||||
|
} from './session-service-config.js'
|
||||||
|
import { SessionServiceError } from './session-service-runtime.js'
|
||||||
|
import {
|
||||||
|
resolveInitialQrSessionState,
|
||||||
|
SESSION_TTL_MS,
|
||||||
|
} from './session-state.js'
|
||||||
|
|
||||||
|
const sessions = new Map()
|
||||||
|
|
||||||
|
export async function registerTencentBrowserSession({ loginType, browserContext, page }) {
|
||||||
|
const sessionId = createSessionId()
|
||||||
|
const sessionDir = path.join(DATA_ROOT, sessionId)
|
||||||
|
await fs.mkdir(sessionDir, { recursive: true })
|
||||||
|
|
||||||
|
const now = new Date().toISOString()
|
||||||
|
const session = {
|
||||||
|
sessionId,
|
||||||
|
loginType,
|
||||||
|
status: 'created',
|
||||||
|
notice: '浏览器会话已创建',
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
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)
|
||||||
|
return session
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listActiveTencentBrowserSessions() {
|
||||||
|
return [...sessions.values()]
|
||||||
|
}
|
||||||
|
|
||||||
|
export 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
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyInitialQrSessionState(session) {
|
||||||
|
resetTencentSessionAutoClose(session)
|
||||||
|
Object.assign(session, resolveInitialQrSessionState(session.loginType))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function armRedeemedSessionAutoClose(session, { closeSession }) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resetTencentSessionAutoClose(session) {
|
||||||
|
if (session?.autoCloseTimer) {
|
||||||
|
clearTimeout(session.autoCloseTimer)
|
||||||
|
session.autoCloseTimer = null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (session) {
|
||||||
|
session.autoCloseAt = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cleanupExpiredTencentBrowserSessions({ closeSession }) {
|
||||||
|
const expired = listActiveTencentBrowserSessions().filter((session) => session.expiresAt <= Date.now())
|
||||||
|
|
||||||
|
await Promise.all(expired.map((session) => closeSession(session, { markClosed: false })))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function closeSession(
|
||||||
|
session,
|
||||||
|
{
|
||||||
|
markClosed,
|
||||||
|
persistSessionState,
|
||||||
|
closeBrowserIfIdle,
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function persistSessionState(session, { buildSessionPayload }) {
|
||||||
|
await fs.mkdir(session.sessionDir, { recursive: true })
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(session.sessionDir, 'session.json'),
|
||||||
|
JSON.stringify(buildSessionPayload(session), null, 2),
|
||||||
|
'utf8',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSessionId() {
|
||||||
|
let sessionId = ''
|
||||||
|
|
||||||
|
do {
|
||||||
|
sessionId = `txbs-${crypto.randomBytes(6).toString('hex')}`
|
||||||
|
} while (sessions.has(sessionId))
|
||||||
|
|
||||||
|
return sessionId
|
||||||
|
}
|
||||||
@@ -1,19 +1,8 @@
|
|||||||
// @ts-nocheck
|
// @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 {
|
import {
|
||||||
buildChromiumLaunchOptions as buildChromiumLaunchOptionsBase,
|
|
||||||
getLoginTypeLabel,
|
getLoginTypeLabel,
|
||||||
normalizeLoginType,
|
normalizeLoginType,
|
||||||
resolveBrowserLaunchOptions as resolveBrowserLaunchOptionsBase,
|
|
||||||
resolveDefaultViewport,
|
|
||||||
} from './session-browser-config.js'
|
} from './session-browser-config.js'
|
||||||
import { logBrowserSessionDebug } from './session-login-shared.js'
|
import { logBrowserSessionDebug } from './session-login-shared.js'
|
||||||
import {
|
import {
|
||||||
@@ -58,63 +47,41 @@ import {
|
|||||||
resolveSessionNotice,
|
resolveSessionNotice,
|
||||||
resolveSessionStatus,
|
resolveSessionStatus,
|
||||||
resolveTencentSessionExpiresAt,
|
resolveTencentSessionExpiresAt,
|
||||||
SESSION_TTL_MS,
|
|
||||||
} from './session-state.js'
|
} from './session-state.js'
|
||||||
|
import {
|
||||||
const CURRENT_DIR = path.dirname(fileURLToPath(import.meta.url))
|
ACTIVITY_URL,
|
||||||
const PROJECT_ROOT = path.resolve(CURRENT_DIR, '../../..')
|
DEFAULT_VIEWPORT,
|
||||||
const DATA_ROOT = path.resolve(PROJECT_ROOT, 'data/browser-sessions')
|
REDEEM_SUCCESS_AUTO_CLOSE_MS,
|
||||||
const ACTIVITY_URL = 'https://df.qq.com/cp/a20240812cdk/index.html'
|
SESSION_DEBUG_ENABLED,
|
||||||
const CHROME_PATH = String(runtimeConfig.browser.chromePath || '').trim()
|
} from './session-service-config.js'
|
||||||
export const REDEEM_SUCCESS_AUTO_CLOSE_MS = 3 * 60 * 1000
|
import {
|
||||||
const DEFAULT_VIEWPORT = resolveDefaultViewport()
|
closeBrowserIfIdle,
|
||||||
const SESSION_DEBUG_ENABLED = Boolean(runtimeConfig.session.debug)
|
ensureBrowser,
|
||||||
|
resolveBrowserLaunchOptions,
|
||||||
const sessions = new Map()
|
SessionServiceError,
|
||||||
|
warmupTencentBrowser,
|
||||||
let browserPromise = null
|
} from './session-service-runtime.js'
|
||||||
|
import {
|
||||||
|
applyInitialQrSessionState,
|
||||||
|
armRedeemedSessionAutoClose,
|
||||||
|
cleanupExpiredTencentBrowserSessions,
|
||||||
|
closeSession as closeStoredSession,
|
||||||
|
getRequiredSession,
|
||||||
|
listActiveTencentBrowserSessions,
|
||||||
|
persistSessionState as persistStoredSessionState,
|
||||||
|
registerTencentBrowserSession,
|
||||||
|
resetTencentSessionAutoClose,
|
||||||
|
} from './session-service-store.js'
|
||||||
|
|
||||||
export { resolveTencentSessionExpiresAt }
|
export { resolveTencentSessionExpiresAt }
|
||||||
|
export { REDEEM_SUCCESS_AUTO_CLOSE_MS, warmupTencentBrowser }
|
||||||
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 = {}) {
|
export async function createTencentBrowserSession(payload = {}) {
|
||||||
await cleanupExpiredTencentBrowserSessions()
|
await cleanupExpiredTencentBrowserSessions({ closeSession: closeManagedSession })
|
||||||
|
|
||||||
const loginType = normalizeLoginType(payload.loginType || payload.qrType)
|
const loginType = normalizeLoginType(payload.loginType || payload.qrType)
|
||||||
|
|
||||||
const browser = await ensureBrowser()
|
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({
|
const browserContext = await browser.newContext({
|
||||||
viewport: DEFAULT_VIEWPORT,
|
viewport: DEFAULT_VIEWPORT,
|
||||||
deviceScaleFactor: 1,
|
deviceScaleFactor: 1,
|
||||||
@@ -123,30 +90,12 @@ export async function createTencentBrowserSession(payload = {}) {
|
|||||||
const page = await browserContext.newPage()
|
const page = await browserContext.newPage()
|
||||||
page.setDefaultTimeout(20_000)
|
page.setDefaultTimeout(20_000)
|
||||||
|
|
||||||
const session = {
|
const session = await registerTencentBrowserSession({
|
||||||
sessionId,
|
|
||||||
loginType,
|
loginType,
|
||||||
status: 'created',
|
|
||||||
notice: '浏览器会话已创建',
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
updatedAt: new Date().toISOString(),
|
|
||||||
expiresAt: Date.now() + SESSION_TTL_MS,
|
|
||||||
autoCloseAt: 0,
|
|
||||||
autoCloseTimer: null,
|
|
||||||
sessionDir,
|
|
||||||
browserContext,
|
browserContext,
|
||||||
page,
|
page,
|
||||||
qrImageBase64: '',
|
})
|
||||||
qrImagePath: '',
|
const sessionId = session.sessionId
|
||||||
qrUpdatedAt: '',
|
|
||||||
lastState: null,
|
|
||||||
lastRedeem: null,
|
|
||||||
lastReview: null,
|
|
||||||
lastError: '',
|
|
||||||
presentationSyncAttempts: 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
sessions.set(sessionId, session)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await page.goto(ACTIVITY_URL, { waitUntil: 'domcontentloaded' })
|
await page.goto(ACTIVITY_URL, { waitUntil: 'domcontentloaded' })
|
||||||
@@ -180,7 +129,7 @@ export async function getTencentBrowserSessionSummary(sessionId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function reloadTencentBrowserSession(sessionId) {
|
export async function reloadTencentBrowserSession(sessionId) {
|
||||||
await cleanupExpiredTencentBrowserSessions()
|
await cleanupExpiredTencentBrowserSessions({ closeSession: closeManagedSession })
|
||||||
|
|
||||||
const session = await getRequiredSession(sessionId)
|
const session = await getRequiredSession(sessionId)
|
||||||
|
|
||||||
@@ -222,7 +171,7 @@ export async function reloadTencentBrowserSession(sessionId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function refreshTencentBrowserSession(sessionId, { includeQrImage = true } = {}) {
|
export async function refreshTencentBrowserSession(sessionId, { includeQrImage = true } = {}) {
|
||||||
await cleanupExpiredTencentBrowserSessions()
|
await cleanupExpiredTencentBrowserSessions({ closeSession: closeManagedSession })
|
||||||
|
|
||||||
const session = await getRequiredSession(sessionId)
|
const session = await getRequiredSession(sessionId)
|
||||||
|
|
||||||
@@ -349,7 +298,7 @@ export async function redeemTencentBrowserSession(sessionId, payload = {}) {
|
|||||||
activityUrl: ACTIVITY_URL,
|
activityUrl: ACTIVITY_URL,
|
||||||
})
|
})
|
||||||
|
|
||||||
armRedeemedSessionAutoClose(session)
|
armRedeemedSessionAutoClose(session, { closeSession: closeManagedSession })
|
||||||
await persistSessionState(session)
|
await persistSessionState(session)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -360,7 +309,7 @@ export async function redeemTencentBrowserSession(sessionId, payload = {}) {
|
|||||||
|
|
||||||
export async function closeTencentBrowserSession(sessionId) {
|
export async function closeTencentBrowserSession(sessionId) {
|
||||||
const session = await getRequiredSession(sessionId)
|
const session = await getRequiredSession(sessionId)
|
||||||
await closeSession(session, { markClosed: true })
|
await closeManagedSession(session, { markClosed: true })
|
||||||
return {
|
return {
|
||||||
sessionId,
|
sessionId,
|
||||||
closed: true,
|
closed: true,
|
||||||
@@ -368,9 +317,9 @@ export async function closeTencentBrowserSession(sessionId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function closeAllTencentBrowserSessions({ markClosed = true } = {}) {
|
export async function closeAllTencentBrowserSessions({ markClosed = true } = {}) {
|
||||||
const activeSessions = [...sessions.values()]
|
const activeSessions = listActiveTencentBrowserSessions()
|
||||||
|
|
||||||
await Promise.all(activeSessions.map((session) => closeSession(session, { markClosed })))
|
await Promise.all(activeSessions.map((session) => closeManagedSession(session, { markClosed })))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
closedCount: activeSessions.length,
|
closedCount: activeSessions.length,
|
||||||
@@ -405,132 +354,8 @@ export async function getTencentBrowserSessionReviewScreenshotPath(sessionId) {
|
|||||||
return screenshotPath
|
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
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyInitialQrSessionState(session) {
|
|
||||||
resetTencentSessionAutoClose(session)
|
|
||||||
Object.assign(session, resolveInitialQrSessionState(session.loginType))
|
|
||||||
}
|
|
||||||
|
|
||||||
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 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) {
|
async function persistSessionState(session) {
|
||||||
await fs.mkdir(session.sessionDir, { recursive: true })
|
await persistStoredSessionState(session, { buildSessionPayload })
|
||||||
await fs.writeFile(
|
|
||||||
path.join(session.sessionDir, 'session.json'),
|
|
||||||
JSON.stringify(buildSessionPayload(session), null, 2),
|
|
||||||
'utf8',
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildSessionPayload(session, { includeQrImage = true } = {}) {
|
function buildSessionPayload(session, { includeQrImage = true } = {}) {
|
||||||
@@ -546,21 +371,12 @@ async function fillRedeemCodeInBrowser(page, code) {
|
|||||||
await prepareRedeemCodeFill(page, code, { ensureActivityInfoReady })
|
await prepareRedeemCodeFill(page, code, { ensureActivityInfoReady })
|
||||||
}
|
}
|
||||||
|
|
||||||
async function closeBrowserIfIdle() {
|
async function closeManagedSession(session, { markClosed }) {
|
||||||
if (sessions.size > 0 || !browserPromise) {
|
await closeStoredSession(session, {
|
||||||
return
|
markClosed,
|
||||||
}
|
persistSessionState,
|
||||||
|
closeBrowserIfIdle: () => closeBrowserIfIdle({
|
||||||
if (resolveBrowserLaunchOptions().keepAlive) {
|
activeSessionCount: listActiveTencentBrowserSessions().length,
|
||||||
return
|
}),
|
||||||
}
|
})
|
||||||
|
|
||||||
try {
|
|
||||||
const browser = await browserPromise
|
|
||||||
await browser.close()
|
|
||||||
} catch {
|
|
||||||
// ignore browser close failures
|
|
||||||
} finally {
|
|
||||||
browserPromise = null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user