拆分腾讯会话 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 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) {
|
export function resolveTencentSessionExpiresAt(session, nowMs = Date.now(), sessionTtlMs = SESSION_TTL_MS) {
|
||||||
const autoCloseAt = Number(session?.autoCloseAt || 0)
|
const autoCloseAt = Number(session?.autoCloseAt || 0)
|
||||||
if (autoCloseAt > 0) {
|
if (autoCloseAt > 0) {
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ import {
|
|||||||
isRetryableQrCaptureError,
|
isRetryableQrCaptureError,
|
||||||
logBrowserSessionDebug,
|
logBrowserSessionDebug,
|
||||||
} from './session-login-shared.js'
|
} from './session-login-shared.js'
|
||||||
|
import {
|
||||||
|
loadCookieMapFromContext,
|
||||||
|
} from './session-cookie-map.js'
|
||||||
import {
|
import {
|
||||||
buildSessionPayload as buildSessionPayloadBase,
|
buildSessionPayload as buildSessionPayloadBase,
|
||||||
} from './session-payload.js'
|
} from './session-payload.js'
|
||||||
@@ -30,6 +33,9 @@ import {
|
|||||||
import {
|
import {
|
||||||
saveRedeemArtifacts,
|
saveRedeemArtifacts,
|
||||||
} from './session-proof.js'
|
} from './session-proof.js'
|
||||||
|
import {
|
||||||
|
ensureReviewScreenshot,
|
||||||
|
} from './session-review.js'
|
||||||
import {
|
import {
|
||||||
capturePageCaptchaForOcr,
|
capturePageCaptchaForOcr,
|
||||||
isCaptchaRejectedResult,
|
isCaptchaRejectedResult,
|
||||||
@@ -46,6 +52,7 @@ import {
|
|||||||
import { recognizeTencentCaptcha } from './ocr.js'
|
import { recognizeTencentCaptcha } from './ocr.js'
|
||||||
import {
|
import {
|
||||||
hasRedeemCredential,
|
hasRedeemCredential,
|
||||||
|
resolveInitialQrSessionState,
|
||||||
resolveSessionNotice,
|
resolveSessionNotice,
|
||||||
resolveSessionStatus,
|
resolveSessionStatus,
|
||||||
resolveTencentSessionExpiresAt,
|
resolveTencentSessionExpiresAt,
|
||||||
@@ -560,10 +567,7 @@ async function ensureLoginTab(page, loginType) {
|
|||||||
|
|
||||||
function applyInitialQrSessionState(session) {
|
function applyInitialQrSessionState(session) {
|
||||||
resetTencentSessionAutoClose(session)
|
resetTencentSessionAutoClose(session)
|
||||||
session.status = 'waiting_scan'
|
Object.assign(session, resolveInitialQrSessionState(session.loginType))
|
||||||
session.notice = `请使用${getLoginTypeLabel(session.loginType)}扫码登录`
|
|
||||||
session.updatedAt = new Date().toISOString()
|
|
||||||
session.expiresAt = Date.now() + SESSION_TTL_MS
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function armRedeemedSessionAutoClose(session) {
|
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) {
|
async function fillRedeemCodeInBrowser(page, code) {
|
||||||
await prepareRedeemCodeFill(page, code, { ensureActivityInfoReady })
|
await prepareRedeemCodeFill(page, code, { ensureActivityInfoReady })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,13 @@ import {
|
|||||||
normalizeLoginType,
|
normalizeLoginType,
|
||||||
parseViewportSpec,
|
parseViewportSpec,
|
||||||
} from './session-browser-config.js'
|
} from './session-browser-config.js'
|
||||||
|
import { normalizeTencentCookieMap } from './session-cookie-map.js'
|
||||||
import {
|
import {
|
||||||
buildArtifactsPayload,
|
buildArtifactsPayload,
|
||||||
buildRedeemPayload,
|
buildRedeemPayload,
|
||||||
} from './session-payload.js'
|
} from './session-payload.js'
|
||||||
|
import { ensureReviewScreenshot } from './session-review.js'
|
||||||
|
import { resolveInitialQrSessionState } from './session-state.js'
|
||||||
import {
|
import {
|
||||||
REDEEM_SUCCESS_AUTO_CLOSE_MS,
|
REDEEM_SUCCESS_AUTO_CLOSE_MS,
|
||||||
resolveTencentSessionExpiresAt,
|
resolveTencentSessionExpiresAt,
|
||||||
@@ -138,3 +141,68 @@ test('buildArtifactsPayload only exposes internal artifact paths in debug mode',
|
|||||||
reviewScreenshotPath: '/tmp/session/review.png',
|
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