From 37299a2797abb40498ed0135105c3808ef07518b Mon Sep 17 00:00:00 2001 From: yml Date: Mon, 4 May 2026 14:15:13 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8B=86=E5=88=86=E8=85=BE=E8=AE=AF=E4=BC=9A?= =?UTF-8?q?=E8=AF=9D=20Cookie=20=E4=B8=8E=E5=A4=8D=E6=A0=B8=E6=88=AA?= =?UTF-8?q?=E5=9B=BE=E8=BE=85=E5=8A=A9=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../services/session/session-cookie-map.js | 38 +++++++++ .../src/services/session/session-review.js | 45 +++++++++++ .../src/services/session/session-state.js | 17 ++++ apps/backend/src/services/session/session.js | 77 ++----------------- .../src/services/session/session.test.js | 68 ++++++++++++++++ 5 files changed, 176 insertions(+), 69 deletions(-) create mode 100644 apps/backend/src/services/session/session-cookie-map.js create mode 100644 apps/backend/src/services/session/session-review.js diff --git a/apps/backend/src/services/session/session-cookie-map.js b/apps/backend/src/services/session/session-cookie-map.js new file mode 100644 index 00000000..b4957c31 --- /dev/null +++ b/apps/backend/src/services/session/session-cookie-map.js @@ -0,0 +1,38 @@ +const TENCENT_COOKIE_URLS = [ + 'https://df.qq.com', + 'https://ssl.captcha.qq.com', + 'https://graph.qq.com', + 'https://milo.qq.com', +] + +export function normalizeTencentCookieMap(cookies = []) { + 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 +} + +export async function loadCookieMapFromContext(browserContext) { + const cookies = await browserContext.cookies(TENCENT_COOKIE_URLS) + return normalizeTencentCookieMap(cookies) +} diff --git a/apps/backend/src/services/session/session-review.js b/apps/backend/src/services/session/session-review.js new file mode 100644 index 00000000..5c6cd30a --- /dev/null +++ b/apps/backend/src/services/session/session-review.js @@ -0,0 +1,45 @@ +import path from 'node:path' + +import { logBrowserSessionDebug } from './session-login-shared.js' + +export function resolveReviewSignature(session, activityInfo) { + const roleId = String(activityInfo?.role?.roleId || '').trim() + const roleName = String(activityInfo?.role?.roleName || '').trim() + + return { + roleId, + roleName, + signature: `${session.loginType}|${roleId}|${roleName}`, + } +} + +export async function ensureReviewScreenshot(session, activityInfo) { + const { roleId, roleName, signature } = resolveReviewSignature(session, activityInfo) + + if (session.lastReview?.screenshotPath && session.lastReview?.signature === signature) { + return false + } + + 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, + } + return true + } catch (error) { + logBrowserSessionDebug('reviewScreenshot.capture.error', { + sessionId: session.sessionId, + message: error instanceof Error ? error.message : String(error), + }) + return false + } +} diff --git a/apps/backend/src/services/session/session-state.js b/apps/backend/src/services/session/session-state.js index da9e4638..dcdcc393 100644 --- a/apps/backend/src/services/session/session-state.js +++ b/apps/backend/src/services/session/session-state.js @@ -2,6 +2,23 @@ import { getLoginTypeLabel } from './session-browser-config.js' export const SESSION_TTL_MS = 30 * 60 * 1000 +export function resolveInitialQrSessionState( + loginType, + { + updatedAt = new Date().toISOString(), + nowMs = Date.now(), + sessionTtlMs = SESSION_TTL_MS, + loginTypeLabel = getLoginTypeLabel(loginType), + } = {}, +) { + return { + status: 'waiting_scan', + notice: `请使用${loginTypeLabel}扫码登录`, + updatedAt, + expiresAt: nowMs + sessionTtlMs, + } +} + export function resolveTencentSessionExpiresAt(session, nowMs = Date.now(), sessionTtlMs = SESSION_TTL_MS) { const autoCloseAt = Number(session?.autoCloseAt || 0) if (autoCloseAt > 0) { diff --git a/apps/backend/src/services/session/session.js b/apps/backend/src/services/session/session.js index 86122a74..b04be7c1 100644 --- a/apps/backend/src/services/session/session.js +++ b/apps/backend/src/services/session/session.js @@ -19,6 +19,9 @@ import { isRetryableQrCaptureError, logBrowserSessionDebug, } from './session-login-shared.js' +import { + loadCookieMapFromContext, +} from './session-cookie-map.js' import { buildSessionPayload as buildSessionPayloadBase, } from './session-payload.js' @@ -30,6 +33,9 @@ import { import { saveRedeemArtifacts, } from './session-proof.js' +import { + ensureReviewScreenshot, +} from './session-review.js' import { capturePageCaptchaForOcr, isCaptchaRejectedResult, @@ -46,6 +52,7 @@ import { import { recognizeTencentCaptcha } from './ocr.js' import { hasRedeemCredential, + resolveInitialQrSessionState, resolveSessionNotice, resolveSessionStatus, resolveTencentSessionExpiresAt, @@ -560,10 +567,7 @@ async function ensureLoginTab(page, loginType) { 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 + Object.assign(session, resolveInitialQrSessionState(session.loginType)) } function armRedeemedSessionAutoClose(session) { @@ -884,71 +888,6 @@ function buildSessionPayload(session, { includeQrImage = true } = {}) { }) } -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 }) } diff --git a/apps/backend/src/services/session/session.test.js b/apps/backend/src/services/session/session.test.js index fb6593e2..1cba3ecd 100644 --- a/apps/backend/src/services/session/session.test.js +++ b/apps/backend/src/services/session/session.test.js @@ -5,10 +5,13 @@ import { normalizeLoginType, parseViewportSpec, } from './session-browser-config.js' +import { normalizeTencentCookieMap } from './session-cookie-map.js' import { buildArtifactsPayload, buildRedeemPayload, } from './session-payload.js' +import { ensureReviewScreenshot } from './session-review.js' +import { resolveInitialQrSessionState } from './session-state.js' import { REDEEM_SUCCESS_AUTO_CLOSE_MS, resolveTencentSessionExpiresAt, @@ -138,3 +141,68 @@ test('buildArtifactsPayload only exposes internal artifact paths in debug mode', reviewScreenshotPath: '/tmp/session/review.png', }) }) + +test('normalizeTencentCookieMap fills required defaults and derived proxy cookie', () => { + assert.deepEqual( + normalizeTencentCookieMap([ + { name: 'openid', value: 'openid-1' }, + { name: 'access_token', value: 'token-1' }, + { name: '', value: 'ignored' }, + ]), + { + openid: 'openid-1', + access_token: 'token-1', + acctype: 'qc', + appid: '101491592', + iegams_milo_proxylogin_qc: '101491592_$$_openid-1_$$_token-1', + }, + ) +}) + +test('resolveInitialQrSessionState creates waiting-scan snapshot with ttl', () => { + assert.deepEqual( + resolveInitialQrSessionState('wx', { + updatedAt: '2026-04-14T09:30:00.000Z', + nowMs: 1_700_000_000_000, + sessionTtlMs: 1_800_000, + }), + { + status: 'waiting_scan', + notice: '请使用微信扫码登录', + updatedAt: '2026-04-14T09:30:00.000Z', + expiresAt: 1_700_001_800_000, + }, + ) +}) + +test('ensureReviewScreenshot writes screenshot state and skips unchanged signature', async () => { + const screenshotCalls = [] + const session = { + sessionId: 'txbs-demo', + loginType: 'qq', + sessionDir: '/tmp/session-demo', + page: { + async screenshot(options) { + screenshotCalls.push(options) + }, + }, + lastReview: null, + } + + const activityInfo = { + role: { + roleId: '1001', + roleName: '测试角色', + }, + } + + assert.equal(await ensureReviewScreenshot(session, activityInfo), true) + assert.equal(screenshotCalls.length, 1) + assert.equal(session.lastReview.roleId, '1001') + assert.equal(session.lastReview.roleName, '测试角色') + assert.equal(session.lastReview.signature, 'qq|1001|测试角色') + assert.match(session.lastReview.screenshotPath, /role-review\.png$/) + + assert.equal(await ensureReviewScreenshot(session, activityInfo), false) + assert.equal(screenshotCalls.length, 1) +})