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' type BrowserContextLike = { close?: () => Promise } export type TencentBrowserSession = Record & { sessionId: string loginType: string status: string notice: string createdAt: string updatedAt: string expiresAt: number autoCloseAt: number autoCloseTimer: ReturnType | null sessionDir: string browserContext?: BrowserContextLike | null page?: unknown browserContextClosed?: boolean } type RegisterSessionInput = { loginType: string browserContext: BrowserContextLike page: unknown } type CloseSessionDependency = { closeSession: (session: TencentBrowserSession, options: { markClosed: boolean }) => Promise } type CloseSessionOptions = { markClosed: boolean persistSessionState: (session: TencentBrowserSession) => Promise closeBrowserIfIdle: () => Promise } type PersistSessionStateOptions = { buildSessionPayload: (session: TencentBrowserSession) => unknown } const sessions = new Map() export async function registerTencentBrowserSession({ loginType, browserContext, page, }: RegisterSessionInput): Promise { 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(): TencentBrowserSession[] { return [...sessions.values()] } export async function getRequiredSession(sessionId: unknown): Promise { 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: TencentBrowserSession): void { resetTencentSessionAutoClose(session) Object.assign(session, resolveInitialQrSessionState(session.loginType)) } export function armRedeemedSessionAutoClose(session: TencentBrowserSession, { closeSession }: CloseSessionDependency): void { 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?: TencentBrowserSession | null): void { if (session?.autoCloseTimer) { clearTimeout(session.autoCloseTimer) session.autoCloseTimer = null } if (session) { session.autoCloseAt = 0 } } export async function cleanupExpiredTencentBrowserSessions({ closeSession }: CloseSessionDependency): Promise { const expired = listActiveTencentBrowserSessions().filter((session) => session.expiresAt <= Date.now()) await Promise.all(expired.map((session) => closeSession(session, { markClosed: false }))) } export async function closeSession( session: TencentBrowserSession, { markClosed, persistSessionState, closeBrowserIfIdle, }: CloseSessionOptions, ): Promise { 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: TencentBrowserSession, { buildSessionPayload }: PersistSessionStateOptions, ): Promise { 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(): string { let sessionId = '' do { sessionId = `txbs-${crypto.randomBytes(6).toString('hex')}` } while (sessions.has(sessionId)) return sessionId }