拆分腾讯会话 Cookie 与复核截图辅助逻辑

This commit is contained in:
yml
2026-05-04 14:15:13 +08:00
parent ab33272cac
commit 37299a2797
5 changed files with 176 additions and 69 deletions
@@ -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)
}
@@ -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
}
}
@@ -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) {
+8 -69
View File
@@ -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 })
}
@@ -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)
})