后端迁移会话兑换服务

This commit is contained in:
yml
2026-05-21 15:13:22 +08:00
parent 0da7666aa0
commit 2360323581
3 changed files with 181 additions and 24 deletions
@@ -1,7 +1,137 @@
// @ts-nocheck
/// <reference lib="dom" />
import fs from 'node:fs/promises'
type JsonObject = Record<string, any>
type BrowserResponseLike = {
url: () => string
request: () => { method: () => string }
text: () => Promise<string>
status: () => number
}
type BrowserLocatorLike = {
screenshot: (options: { path: string }) => Promise<void>
getAttribute: (name: string) => Promise<string | null>
click: (options?: JsonObject) => Promise<void>
evaluate: <T = unknown>(callback: (element: Element) => T | Promise<T>) => Promise<T>
}
type BrowserPageLike = {
waitForFunction: (callback: any, argOrOptions?: any, options?: JsonObject) => Promise<unknown>
waitForResponse: (
predicate: (response: BrowserResponseLike) => boolean,
options?: JsonObject,
) => Promise<BrowserResponseLike>
waitForTimeout: (milliseconds: number) => Promise<void>
locator: (selector: string) => BrowserLocatorLike
evaluate: <T = any>(callback: any, arg?: any) => Promise<T>
}
type ActivityInfo = {
role?: JsonObject & {
ready?: boolean
area?: string
}
form?: {
cdkeyInputId?: string
verifyInputId?: string
verifyImgId?: string
submitId?: string
}
}
type TencentRedeemPopup = {
visible?: boolean
text?: string
detail?: string
}
type TencentRedeemResult = JsonObject & {
iRet?: number | string
sMsg?: string
msg?: string
popup?: TencentRedeemPopup | null
}
type TencentRedeemOutcome = 'success' | 'captcha_rejected' | 'code_used' | 'code_invalid' | 'failed'
type TencentRedeemClassification = {
outcome: TencentRedeemOutcome
success: boolean
retryWithSameCode: boolean
retryWithReplacementCode: boolean
retCode: number | null
message: string
}
type RedeemAttemptRecord = {
attempt: number
verifyCode: string
verifysession: string
ocrSample: unknown
redeem: TencentRedeemResult
classification: TencentRedeemClassification
role: ActivityInfo['role']
}
type TencentBrowserRedeemSession = JsonObject & {
status: string
notice: string
updatedAt: string
lastError?: string
lastRedeem?: JsonObject
page: BrowserPageLike
browserContext: unknown
sessionDir: string
sessionId: string
}
type RunTencentBrowserRedeemOptions = {
session: TencentBrowserRedeemSession
code: string
maxAttempts: number
ensureLoggedInPresentation: (session: TencentBrowserRedeemSession, options?: JsonObject) => Promise<unknown>
ensureActivityInfoReady: (page: BrowserPageLike, options?: JsonObject) => Promise<ActivityInfo>
fillRedeemCodeInBrowser: (page: BrowserPageLike, code: string, options?: JsonObject) => Promise<unknown>
capturePageCaptchaForOcr: (
page: BrowserPageLike,
options: { sessionDir: string; attempt: number },
) => Promise<{
imageBuffer: Buffer
imagePath: string
imageExtension: string
contentType: string
verifyImgId: string
verifysession: string
}>
recognizeTencentCaptcha: (payload: JsonObject) => Promise<JsonObject>
submitRedeemInBrowser: (
page: BrowserPageLike,
payload: { code: string; verifyCode: string },
) => Promise<TencentRedeemResult>
isCaptchaRejectedResult: (result: TencentRedeemResult) => boolean
refreshPageCaptcha: (page: BrowserPageLike, verifyImgId?: string) => Promise<void>
persistSessionState: (session: TencentBrowserRedeemSession) => Promise<unknown>
buildSessionPayload: (session: TencentBrowserRedeemSession) => JsonObject
saveRedeemArtifacts: (payload: JsonObject) => Promise<JsonObject>
activityUrl: string
}
type ActivityInfoProvider = {
ensureActivityInfoReady: (page: BrowserPageLike, options?: JsonObject) => Promise<ActivityInfo>
}
declare global {
interface Window {
Milo?: {
get?: (key: string) => unknown
}
closeDialog?: () => void
}
}
export async function runTencentBrowserRedeem({
session,
code,
@@ -18,14 +148,14 @@ export async function runTencentBrowserRedeem({
buildSessionPayload,
saveRedeemArtifacts,
activityUrl,
}) {
}: RunTencentBrowserRedeemOptions): Promise<JsonObject> {
session.status = 'redeeming'
session.notice = '正在识别验证码并提交兑换'
session.updatedAt = new Date().toISOString()
await persistSessionState(session)
const attempts = []
let finalResult = null
const attempts: RedeemAttemptRecord[] = []
let finalResult: RedeemAttemptRecord | null = null
try {
await ensureLoggedInPresentation(session, { credentialReady: true })
@@ -133,7 +263,10 @@ export async function runTencentBrowserRedeem({
}
}
export async function capturePageCaptchaForOcr(page, { sessionDir, attempt, ensureActivityInfoReady }) {
export async function capturePageCaptchaForOcr(
page: BrowserPageLike,
{ sessionDir, attempt, ensureActivityInfoReady }: { sessionDir: string; attempt: number } & ActivityInfoProvider,
) {
const activityInfo = await ensureActivityInfoReady(page, { timeoutMs: 5_000 })
if (!activityInfo?.form?.verifyImgId) {
@@ -187,7 +320,11 @@ export async function capturePageCaptchaForOcr(page, { sessionDir, attempt, ensu
}
}
export async function submitRedeemInBrowser(page, payload, { ensureActivityInfoReady }) {
export async function submitRedeemInBrowser(
page: BrowserPageLike,
payload: { code: string; verifyCode: string },
{ ensureActivityInfoReady }: ActivityInfoProvider,
): Promise<TencentRedeemResult> {
const activityInfo = await ensureActivityInfoReady(page, { timeoutMs: 2_000 })
if (!activityInfo?.form?.verifyInputId || !activityInfo?.form?.submitId) {
@@ -237,7 +374,11 @@ export async function submitRedeemInBrowser(page, payload, { ensureActivityInfoR
}
}
export function fillRedeemCodeInBrowser(page, code, { activityInfo }) {
export function fillRedeemCodeInBrowser(
page: BrowserPageLike,
code: string,
{ activityInfo }: { activityInfo?: ActivityInfo } = {},
) {
const cdkeySelector = activityInfo?.form?.cdkeyInputId
? `#${activityInfo.form.cdkeyInputId}`
: '[id^="milo_cdkeyInfo_"]'
@@ -252,12 +393,16 @@ export function fillRedeemCodeInBrowser(page, code, { activityInfo }) {
])
}
export async function prepareRedeemCodeFill(page, code, { ensureActivityInfoReady }) {
export async function prepareRedeemCodeFill(
page: BrowserPageLike,
code: string,
{ ensureActivityInfoReady }: ActivityInfoProvider,
) {
const activityInfo = await ensureActivityInfoReady(page, { triggerRender: true, timeoutMs: 2_000 })
await fillRedeemCodeInBrowser(page, code, { activityInfo })
}
export async function refreshPageCaptcha(page, verifyImgId) {
export async function refreshPageCaptcha(page: BrowserPageLike, verifyImgId?: string) {
if (!verifyImgId) {
return
}
@@ -280,7 +425,7 @@ export async function refreshPageCaptcha(page, verifyImgId) {
).catch(() => null)
}
export function isCaptchaRejectedResult(result) {
export function isCaptchaRejectedResult(result: TencentRedeemResult): boolean {
if (classifyTencentRedeemResult(result).outcome === 'captcha_rejected') {
return true
}
@@ -288,7 +433,7 @@ export function isCaptchaRejectedResult(result) {
return false
}
export function classifyTencentRedeemResult(result) {
export function classifyTencentRedeemResult(result: TencentRedeemResult): TencentRedeemClassification {
const retCode = Number(result?.iRet)
const message = resolveTencentRedeemMessage(result)
const normalized = normalizeTencentRedeemText(message)
@@ -323,7 +468,7 @@ export function classifyTencentRedeemResult(result) {
return buildTencentRedeemClassification('failed', retCode, message || '兑换失败')
}
function resetRedeemPopup(page) {
function resetRedeemPopup(page: BrowserPageLike) {
return page.evaluate(() => {
const popup = document.querySelector('#pop2')
const popupText = document.querySelector('#PopText')
@@ -344,7 +489,7 @@ function resetRedeemPopup(page) {
})
}
async function dismissRedeemRetryPopup(page) {
async function dismissRedeemRetryPopup(page: BrowserPageLike) {
await page.evaluate(() => {
try {
if (typeof window.closeDialog === 'function') {
@@ -372,7 +517,7 @@ async function dismissRedeemRetryPopup(page) {
await page.waitForTimeout(150)
}
async function normalizeRedeemOverlay(page) {
async function normalizeRedeemOverlay(page: BrowserPageLike) {
await page.evaluate(() => {
const overlayIds = ['_overlay_', 'overlay_mask', 'overlay']
@@ -390,7 +535,7 @@ async function normalizeRedeemOverlay(page) {
})
}
async function clickRedeemSubmit(page, submitSelector) {
async function clickRedeemSubmit(page: BrowserPageLike, submitSelector: string) {
const locator = page.locator(submitSelector)
try {
@@ -412,12 +557,12 @@ async function clickRedeemSubmit(page, submitSelector) {
}
}
async function setFormControlValue(page, selector, value) {
async function setFormControlValue(page: BrowserPageLike, selector: string, value: unknown) {
const nextValue = String(value ?? '')
const appliedValue = await page.evaluate(
({ targetSelector, targetValue }) => {
const isVisible = (element) => {
const isVisible = (element: Element) => {
if (!(element instanceof HTMLElement)) {
return false
}
@@ -491,7 +636,7 @@ async function setFormControlValue(page, selector, value) {
}
}
async function waitForRedeemPopup(page) {
async function waitForRedeemPopup(page: BrowserPageLike): Promise<TencentRedeemPopup> {
await page.waitForFunction(() => {
const popup = document.querySelector('#pop2')
const popupText = document.querySelector('#PopText')
@@ -511,7 +656,7 @@ async function waitForRedeemPopup(page) {
}))
}
function tryParseJson(text) {
function tryParseJson(text: string): any {
try {
return JSON.parse(text)
} catch {
@@ -519,7 +664,7 @@ function tryParseJson(text) {
}
}
function inferRedeemCodeFromPopup(popup) {
function inferRedeemCodeFromPopup(popup: TencentRedeemPopup) {
const text = `${String(popup?.text || '')} ${String(popup?.detail || '')}`.trim()
if (!text) {
@@ -537,7 +682,7 @@ function inferRedeemCodeFromPopup(popup) {
return 1
}
export function resolveTencentRedeemMessage(result) {
export function resolveTencentRedeemMessage(result: TencentRedeemResult): string {
const candidates = [
result?.popup?.detail,
result?.popup?.text,
@@ -550,7 +695,11 @@ export function resolveTencentRedeemMessage(result) {
return candidates[0] || '兑换完成'
}
function buildTencentRedeemClassification(outcome, retCode, message) {
function buildTencentRedeemClassification(
outcome: TencentRedeemOutcome,
retCode: number,
message: string,
): TencentRedeemClassification {
return {
outcome,
success: outcome === 'success',
@@ -561,7 +710,7 @@ function buildTencentRedeemClassification(outcome, retCode, message) {
}
}
function normalizeTencentRedeemText(value) {
function normalizeTencentRedeemText(value: unknown): string {
return String(value || '')
.replace(/\s+/g, '')
.trim()
+1 -1
View File
@@ -19,7 +19,7 @@
"node_modules",
"data",
"src/**/*.test.js",
"src/services/session/**",
"src/services/session/**/*.js",
"src/services/platforms/agiso/**/*.js"
]
}