拆分腾讯会话 Cookie 与复核截图辅助逻辑
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user