拆分腾讯会话活动页采集与展示同步逻辑
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
// @ts-nocheck
|
||||
|
||||
import { extractHostState, reloadActivityPageForPresentation } from './session-page.js'
|
||||
import { shouldRefreshLoggedInPresentation } from './session-state.js'
|
||||
|
||||
export function createEmptyActivityInfo() {
|
||||
return {
|
||||
nickname: '',
|
||||
role: {
|
||||
ready: false,
|
||||
roleName: '',
|
||||
roleId: '',
|
||||
area: '',
|
||||
partition: '',
|
||||
platId: '',
|
||||
md5str: '',
|
||||
checkparam: '',
|
||||
},
|
||||
form: {
|
||||
cdkeyInputId: '',
|
||||
cdkeyValue: '',
|
||||
verifyInputId: '',
|
||||
verifyValue: '',
|
||||
verifyImgId: '',
|
||||
submitId: '',
|
||||
},
|
||||
verify: {
|
||||
visible: false,
|
||||
src: '',
|
||||
naturalWidth: 0,
|
||||
naturalHeight: 0,
|
||||
},
|
||||
popup: {
|
||||
visible: false,
|
||||
text: '',
|
||||
detail: '',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export async function ensureLoggedInPresentation(
|
||||
session,
|
||||
{
|
||||
hostState = null,
|
||||
credentialReady = false,
|
||||
activityUrl = '',
|
||||
extractHostState: extractHostStateImpl = extractHostState,
|
||||
reloadActivityPageForPresentation: reloadActivityPageForPresentationImpl = reloadActivityPageForPresentation,
|
||||
shouldRefreshLoggedInPresentation: shouldRefreshLoggedInPresentationImpl = shouldRefreshLoggedInPresentation,
|
||||
} = {},
|
||||
) {
|
||||
let nextHostState = hostState || (await extractHostStateImpl(session.page))
|
||||
|
||||
if (!credentialReady || !shouldRefreshLoggedInPresentationImpl(nextHostState)) {
|
||||
return {
|
||||
reloaded: false,
|
||||
hostState: nextHostState,
|
||||
}
|
||||
}
|
||||
|
||||
if (session.presentationSyncAttempts >= 2) {
|
||||
return {
|
||||
reloaded: false,
|
||||
hostState: nextHostState,
|
||||
}
|
||||
}
|
||||
|
||||
session.presentationSyncAttempts += 1
|
||||
await reloadActivityPageForPresentationImpl(session.page, activityUrl)
|
||||
nextHostState = await extractHostStateImpl(session.page)
|
||||
|
||||
return {
|
||||
reloaded: true,
|
||||
hostState: nextHostState,
|
||||
}
|
||||
}
|
||||
|
||||
export async function ensureActivityInfoReady(
|
||||
page,
|
||||
{
|
||||
triggerRender = false,
|
||||
timeoutMs = 8_000,
|
||||
extractActivityInfo: extractActivityInfoImpl = extractActivityInfo,
|
||||
triggerActivityRoleRender: triggerActivityRoleRenderImpl = triggerActivityRoleRender,
|
||||
now = () => Date.now(),
|
||||
} = {},
|
||||
) {
|
||||
if (triggerRender) {
|
||||
await triggerActivityRoleRenderImpl(page)
|
||||
}
|
||||
|
||||
const startedAt = now()
|
||||
let lastInfo = await extractActivityInfoImpl(page)
|
||||
|
||||
while (now() - startedAt < timeoutMs) {
|
||||
if (lastInfo?.role?.ready) {
|
||||
return lastInfo
|
||||
}
|
||||
|
||||
await page.waitForTimeout(400)
|
||||
lastInfo = await extractActivityInfoImpl(page)
|
||||
}
|
||||
|
||||
return lastInfo
|
||||
}
|
||||
|
||||
export async function triggerActivityRoleRender(page) {
|
||||
try {
|
||||
await page.evaluate(() => {
|
||||
const maybeRender = window.renderCdkey
|
||||
|
||||
if (typeof maybeRender === 'function') {
|
||||
maybeRender()
|
||||
}
|
||||
})
|
||||
} catch {
|
||||
// ignore render trigger failures; polling will still inspect the page
|
||||
}
|
||||
}
|
||||
|
||||
export async function extractActivityInfo(page) {
|
||||
try {
|
||||
return await page.evaluate(() => {
|
||||
const asString = (value) => (typeof value === 'string' ? value.trim() : '')
|
||||
const isVisible = (element) => {
|
||||
if (!element) {
|
||||
return false
|
||||
}
|
||||
|
||||
const style = window.getComputedStyle(element)
|
||||
return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0'
|
||||
}
|
||||
|
||||
const pick = (selector) => document.querySelector(selector)
|
||||
const pickBest = (selector, predicate = null) => {
|
||||
const matches = [...document.querySelectorAll(selector)]
|
||||
const preferred = matches.find((element) => {
|
||||
if (predicate && !predicate(element)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return isVisible(element)
|
||||
})
|
||||
|
||||
if (preferred) {
|
||||
return preferred
|
||||
}
|
||||
|
||||
if (predicate) {
|
||||
return matches.find((element) => predicate(element)) || null
|
||||
}
|
||||
|
||||
return matches[0] || null
|
||||
}
|
||||
const textOf = (selector) => asString(pick(selector)?.textContent || '')
|
||||
const inputOf = (selector) => {
|
||||
const element = pickBest(
|
||||
selector,
|
||||
(node) => node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement,
|
||||
)
|
||||
|
||||
if (!(element instanceof HTMLInputElement) && !(element instanceof HTMLTextAreaElement)) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return asString(element.value || '')
|
||||
}
|
||||
|
||||
const roleData = window.roleData && typeof window.roleData === 'object' ? window.roleData : {}
|
||||
const roleSelector = pick('[id^="milo_role_selector_"]')
|
||||
const roleNameText =
|
||||
textOf('#role_name') ||
|
||||
asString(roleSelector?.selectedOptions?.[0]?.textContent || '')
|
||||
const verifyImg = pickBest('[id^="milo_verifyImg_"]', (node) => node instanceof HTMLImageElement)
|
||||
const popup = pick('#pop2')
|
||||
const cdkeyInput = pickBest(
|
||||
'[id^="milo_cdkeyInfo_"]',
|
||||
(node) =>
|
||||
(node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement) &&
|
||||
!node.disabled &&
|
||||
node.type !== 'hidden',
|
||||
)
|
||||
const verifyInput = pickBest(
|
||||
'[id^="milo_verifyInput_"]',
|
||||
(node) =>
|
||||
(node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement) &&
|
||||
!node.disabled &&
|
||||
node.type !== 'hidden',
|
||||
)
|
||||
const submitButton = pickBest(
|
||||
'#milo_cdkey_submit, [id^="milo_cdkey_submit"]',
|
||||
(node) => node instanceof HTMLElement && !node.hasAttribute('disabled'),
|
||||
)
|
||||
|
||||
return {
|
||||
nickname: textOf('#login_nickname_span'),
|
||||
role: {
|
||||
ready: Boolean(window.isRole || roleNameText),
|
||||
roleName: roleNameText,
|
||||
roleId: asString(roleData.sRoleId),
|
||||
area: asString(roleData.sArea || '36'),
|
||||
partition: asString(roleData.sPartition),
|
||||
platId: asString(roleData.sPlatId),
|
||||
md5str: asString(roleData.sMd5str || roleData.md5str || roleData.sMdStr),
|
||||
checkparam: asString(roleData.sCheckparam),
|
||||
},
|
||||
form: {
|
||||
cdkeyInputId: cdkeyInput?.id || '',
|
||||
cdkeyValue: inputOf('[id^="milo_cdkeyInfo_"]'),
|
||||
verifyInputId: verifyInput?.id || '',
|
||||
verifyValue: inputOf('[id^="milo_verifyInput_"]'),
|
||||
verifyImgId: verifyImg?.id || '',
|
||||
submitId: submitButton?.id || '',
|
||||
},
|
||||
verify: {
|
||||
visible: isVisible(verifyImg),
|
||||
src: asString(verifyImg?.getAttribute('src')),
|
||||
naturalWidth: Number(verifyImg?.naturalWidth || 0),
|
||||
naturalHeight: Number(verifyImg?.naturalHeight || 0),
|
||||
},
|
||||
popup: {
|
||||
visible: isVisible(popup),
|
||||
text: textOf('#PopText'),
|
||||
detail: textOf('#PopText2'),
|
||||
},
|
||||
}
|
||||
})
|
||||
} catch {
|
||||
return createEmptyActivityInfo()
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,11 @@ import { logBrowserSessionDebug } from './session-login-shared.js'
|
||||
import {
|
||||
loadCookieMapFromContext,
|
||||
} from './session-cookie-map.js'
|
||||
import {
|
||||
ensureActivityInfoReady,
|
||||
ensureLoggedInPresentation,
|
||||
extractActivityInfo,
|
||||
} from './session-activity.js'
|
||||
import {
|
||||
buildSessionPayload as buildSessionPayloadBase,
|
||||
} from './session-payload.js'
|
||||
@@ -54,7 +59,6 @@ import {
|
||||
resolveSessionStatus,
|
||||
resolveTencentSessionExpiresAt,
|
||||
SESSION_TTL_MS,
|
||||
shouldRefreshLoggedInPresentation,
|
||||
} from './session-state.js'
|
||||
|
||||
const CURRENT_DIR = path.dirname(fileURLToPath(import.meta.url))
|
||||
@@ -235,7 +239,11 @@ export async function refreshTencentBrowserSession(sessionId, { includeQrImage =
|
||||
const credentialReady = hasRedeemCredential(cookieMap)
|
||||
|
||||
if (credentialReady) {
|
||||
const presentation = await ensureLoggedInPresentation(session, { hostState, credentialReady })
|
||||
const presentation = await ensureLoggedInPresentation(session, {
|
||||
hostState,
|
||||
credentialReady,
|
||||
activityUrl: ACTIVITY_URL,
|
||||
})
|
||||
hostState = presentation.hostState
|
||||
} else {
|
||||
session.presentationSyncAttempts = 0
|
||||
@@ -488,213 +496,6 @@ function resetTencentSessionAutoClose(session) {
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureLoggedInPresentation(
|
||||
session,
|
||||
{ hostState = null, credentialReady = false } = {},
|
||||
) {
|
||||
let nextHostState = hostState || (await extractHostState(session.page))
|
||||
|
||||
if (!credentialReady || !shouldRefreshLoggedInPresentation(nextHostState)) {
|
||||
return {
|
||||
reloaded: false,
|
||||
hostState: nextHostState,
|
||||
}
|
||||
}
|
||||
|
||||
if (session.presentationSyncAttempts >= 2) {
|
||||
return {
|
||||
reloaded: false,
|
||||
hostState: nextHostState,
|
||||
}
|
||||
}
|
||||
|
||||
session.presentationSyncAttempts += 1
|
||||
await reloadActivityPageForPresentation(session.page, ACTIVITY_URL)
|
||||
nextHostState = await extractHostState(session.page)
|
||||
|
||||
return {
|
||||
reloaded: true,
|
||||
hostState: nextHostState,
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureActivityInfoReady(page, { triggerRender = false, timeoutMs = 8_000 } = {}) {
|
||||
if (triggerRender) {
|
||||
await triggerActivityRoleRender(page)
|
||||
}
|
||||
|
||||
const startedAt = Date.now()
|
||||
let lastInfo = await extractActivityInfo(page)
|
||||
|
||||
while (Date.now() - startedAt < timeoutMs) {
|
||||
if (lastInfo?.role?.ready) {
|
||||
return lastInfo
|
||||
}
|
||||
|
||||
await page.waitForTimeout(400)
|
||||
lastInfo = await extractActivityInfo(page)
|
||||
}
|
||||
|
||||
return lastInfo
|
||||
}
|
||||
|
||||
async function triggerActivityRoleRender(page) {
|
||||
try {
|
||||
await page.evaluate(() => {
|
||||
const maybeRender = window.renderCdkey
|
||||
|
||||
if (typeof maybeRender === 'function') {
|
||||
maybeRender()
|
||||
}
|
||||
})
|
||||
} catch {
|
||||
// ignore render trigger failures; polling will still inspect the page
|
||||
}
|
||||
}
|
||||
|
||||
async function extractActivityInfo(page) {
|
||||
try {
|
||||
return await page.evaluate(() => {
|
||||
const asString = (value) => (typeof value === 'string' ? value.trim() : '')
|
||||
const isVisible = (element) => {
|
||||
if (!element) {
|
||||
return false
|
||||
}
|
||||
|
||||
const style = window.getComputedStyle(element)
|
||||
return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0'
|
||||
}
|
||||
|
||||
const pick = (selector) => document.querySelector(selector)
|
||||
const pickBest = (selector, predicate = null) => {
|
||||
const matches = [...document.querySelectorAll(selector)]
|
||||
const preferred = matches.find((element) => {
|
||||
if (predicate && !predicate(element)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return isVisible(element)
|
||||
})
|
||||
|
||||
if (preferred) {
|
||||
return preferred
|
||||
}
|
||||
|
||||
if (predicate) {
|
||||
return matches.find((element) => predicate(element)) || null
|
||||
}
|
||||
|
||||
return matches[0] || null
|
||||
}
|
||||
const textOf = (selector) => asString(pick(selector)?.textContent || '')
|
||||
const inputOf = (selector) => {
|
||||
const element = pickBest(
|
||||
selector,
|
||||
(node) => node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement,
|
||||
)
|
||||
|
||||
if (!(element instanceof HTMLInputElement) && !(element instanceof HTMLTextAreaElement)) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return asString(element.value || '')
|
||||
}
|
||||
|
||||
const roleData = window.roleData && typeof window.roleData === 'object' ? window.roleData : {}
|
||||
const roleSelector = pick('[id^="milo_role_selector_"]')
|
||||
const roleNameText =
|
||||
textOf('#role_name') ||
|
||||
asString(roleSelector?.selectedOptions?.[0]?.textContent || '')
|
||||
const verifyImg = pickBest('[id^="milo_verifyImg_"]', (node) => node instanceof HTMLImageElement)
|
||||
const popup = pick('#pop2')
|
||||
const cdkeyInput = pickBest(
|
||||
'[id^="milo_cdkeyInfo_"]',
|
||||
(node) =>
|
||||
(node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement) &&
|
||||
!node.disabled &&
|
||||
node.type !== 'hidden',
|
||||
)
|
||||
const verifyInput = pickBest(
|
||||
'[id^="milo_verifyInput_"]',
|
||||
(node) =>
|
||||
(node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement) &&
|
||||
!node.disabled &&
|
||||
node.type !== 'hidden',
|
||||
)
|
||||
const submitButton = pickBest(
|
||||
'#milo_cdkey_submit, [id^="milo_cdkey_submit"]',
|
||||
(node) => node instanceof HTMLElement && !node.hasAttribute('disabled'),
|
||||
)
|
||||
|
||||
return {
|
||||
nickname: textOf('#login_nickname_span'),
|
||||
role: {
|
||||
ready: Boolean(window.isRole || roleNameText),
|
||||
roleName: roleNameText,
|
||||
roleId: asString(roleData.sRoleId),
|
||||
area: asString(roleData.sArea || '36'),
|
||||
partition: asString(roleData.sPartition),
|
||||
platId: asString(roleData.sPlatId),
|
||||
md5str: asString(roleData.sMd5str || roleData.md5str || roleData.sMdStr),
|
||||
checkparam: asString(roleData.sCheckparam),
|
||||
},
|
||||
form: {
|
||||
cdkeyInputId: cdkeyInput?.id || '',
|
||||
cdkeyValue: inputOf('[id^="milo_cdkeyInfo_"]'),
|
||||
verifyInputId: verifyInput?.id || '',
|
||||
verifyValue: inputOf('[id^="milo_verifyInput_"]'),
|
||||
verifyImgId: verifyImg?.id || '',
|
||||
submitId: submitButton?.id || '',
|
||||
},
|
||||
verify: {
|
||||
visible: isVisible(verifyImg),
|
||||
src: asString(verifyImg?.getAttribute('src')),
|
||||
naturalWidth: Number(verifyImg?.naturalWidth || 0),
|
||||
naturalHeight: Number(verifyImg?.naturalHeight || 0),
|
||||
},
|
||||
popup: {
|
||||
visible: isVisible(popup),
|
||||
text: textOf('#PopText'),
|
||||
detail: textOf('#PopText2'),
|
||||
},
|
||||
}
|
||||
})
|
||||
} catch {
|
||||
return {
|
||||
nickname: '',
|
||||
role: {
|
||||
ready: false,
|
||||
roleName: '',
|
||||
roleId: '',
|
||||
area: '',
|
||||
partition: '',
|
||||
platId: '',
|
||||
md5str: '',
|
||||
checkparam: '',
|
||||
},
|
||||
form: {
|
||||
cdkeyInputId: '',
|
||||
cdkeyValue: '',
|
||||
verifyInputId: '',
|
||||
verifyValue: '',
|
||||
verifyImgId: '',
|
||||
submitId: '',
|
||||
},
|
||||
verify: {
|
||||
visible: false,
|
||||
src: '',
|
||||
naturalWidth: 0,
|
||||
naturalHeight: 0,
|
||||
},
|
||||
popup: {
|
||||
visible: false,
|
||||
text: '',
|
||||
detail: '',
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function cleanupExpiredTencentBrowserSessions() {
|
||||
const expired = [...sessions.values()].filter((session) => session.expiresAt <= Date.now())
|
||||
|
||||
|
||||
@@ -6,14 +6,17 @@ import {
|
||||
parseViewportSpec,
|
||||
} from './session-browser-config.js'
|
||||
import { normalizeTencentCookieMap } from './session-cookie-map.js'
|
||||
import {
|
||||
createEmptyActivityInfo,
|
||||
ensureActivityInfoReady,
|
||||
ensureLoggedInPresentation,
|
||||
extractActivityInfo,
|
||||
} from './session-activity.js'
|
||||
import {
|
||||
captureQrImageWithRetry,
|
||||
ensureLoginTab,
|
||||
} from './session-page.js'
|
||||
import {
|
||||
buildArtifactsPayload,
|
||||
buildRedeemPayload,
|
||||
} from './session-payload.js'
|
||||
import { buildArtifactsPayload, buildRedeemPayload } from './session-payload.js'
|
||||
import { ensureReviewScreenshot } from './session-review.js'
|
||||
import { resolveInitialQrSessionState } from './session-state.js'
|
||||
import {
|
||||
@@ -293,3 +296,81 @@ test('captureQrImageWithRetry retries once after retryable error', async () => {
|
||||
{ type: 'capture', attempt: 2 },
|
||||
])
|
||||
})
|
||||
|
||||
test('ensureLoggedInPresentation reloads page when logged-in host state is stale', async () => {
|
||||
const session = {
|
||||
page: { marker: 'page' },
|
||||
presentationSyncAttempts: 0,
|
||||
}
|
||||
const calls = []
|
||||
let hostCall = 0
|
||||
|
||||
const result = await ensureLoggedInPresentation(session, {
|
||||
credentialReady: true,
|
||||
activityUrl: 'https://example.com/activity',
|
||||
async extractHostState() {
|
||||
hostCall += 1
|
||||
return hostCall === 1
|
||||
? { unloginVisible: true, loginedVisible: false, loginText: '' }
|
||||
: { unloginVisible: false, loginedVisible: true, loginText: '已登录' }
|
||||
},
|
||||
async reloadActivityPageForPresentation(_page, activityUrl) {
|
||||
calls.push(activityUrl)
|
||||
},
|
||||
})
|
||||
|
||||
assert.equal(session.presentationSyncAttempts, 1)
|
||||
assert.deepEqual(calls, ['https://example.com/activity'])
|
||||
assert.deepEqual(result, {
|
||||
reloaded: true,
|
||||
hostState: { unloginVisible: false, loginedVisible: true, loginText: '已登录' },
|
||||
})
|
||||
})
|
||||
|
||||
test('ensureActivityInfoReady triggers render and polls until role becomes ready', async () => {
|
||||
const calls = []
|
||||
const page = {
|
||||
async waitForTimeout(ms) {
|
||||
calls.push({ type: 'wait', ms })
|
||||
},
|
||||
}
|
||||
const snapshots = [
|
||||
{ role: { ready: false, roleName: '' } },
|
||||
{ role: { ready: true, roleName: '测试角色' } },
|
||||
]
|
||||
let nowValue = 0
|
||||
|
||||
const result = await ensureActivityInfoReady(page, {
|
||||
triggerRender: true,
|
||||
timeoutMs: 1000,
|
||||
async triggerActivityRoleRender() {
|
||||
calls.push({ type: 'trigger' })
|
||||
},
|
||||
async extractActivityInfo() {
|
||||
calls.push({ type: 'extract' })
|
||||
return snapshots.shift()
|
||||
},
|
||||
now() {
|
||||
nowValue += 300
|
||||
return nowValue
|
||||
},
|
||||
})
|
||||
|
||||
assert.deepEqual(calls, [
|
||||
{ type: 'trigger' },
|
||||
{ type: 'extract' },
|
||||
{ type: 'wait', ms: 400 },
|
||||
{ type: 'extract' },
|
||||
])
|
||||
assert.deepEqual(result, { role: { ready: true, roleName: '测试角色' } })
|
||||
})
|
||||
|
||||
test('extractActivityInfo falls back to empty structure when page evaluation fails', async () => {
|
||||
const page = {
|
||||
async evaluate() {
|
||||
throw new Error('page closed')
|
||||
},
|
||||
}
|
||||
|
||||
assert.deepEqual(await extractActivityInfo(page), createEmptyActivityInfo())
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user