后端迁移会话兑换服务
This commit is contained in:
+172
-23
@@ -1,7 +1,137 @@
|
|||||||
// @ts-nocheck
|
/// <reference lib="dom" />
|
||||||
|
|
||||||
import fs from 'node:fs/promises'
|
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({
|
export async function runTencentBrowserRedeem({
|
||||||
session,
|
session,
|
||||||
code,
|
code,
|
||||||
@@ -18,14 +148,14 @@ export async function runTencentBrowserRedeem({
|
|||||||
buildSessionPayload,
|
buildSessionPayload,
|
||||||
saveRedeemArtifacts,
|
saveRedeemArtifacts,
|
||||||
activityUrl,
|
activityUrl,
|
||||||
}) {
|
}: RunTencentBrowserRedeemOptions): Promise<JsonObject> {
|
||||||
session.status = 'redeeming'
|
session.status = 'redeeming'
|
||||||
session.notice = '正在识别验证码并提交兑换'
|
session.notice = '正在识别验证码并提交兑换'
|
||||||
session.updatedAt = new Date().toISOString()
|
session.updatedAt = new Date().toISOString()
|
||||||
await persistSessionState(session)
|
await persistSessionState(session)
|
||||||
|
|
||||||
const attempts = []
|
const attempts: RedeemAttemptRecord[] = []
|
||||||
let finalResult = null
|
let finalResult: RedeemAttemptRecord | null = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await ensureLoggedInPresentation(session, { credentialReady: true })
|
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 })
|
const activityInfo = await ensureActivityInfoReady(page, { timeoutMs: 5_000 })
|
||||||
|
|
||||||
if (!activityInfo?.form?.verifyImgId) {
|
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 })
|
const activityInfo = await ensureActivityInfoReady(page, { timeoutMs: 2_000 })
|
||||||
|
|
||||||
if (!activityInfo?.form?.verifyInputId || !activityInfo?.form?.submitId) {
|
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
|
const cdkeySelector = activityInfo?.form?.cdkeyInputId
|
||||||
? `#${activityInfo.form.cdkeyInputId}`
|
? `#${activityInfo.form.cdkeyInputId}`
|
||||||
: '[id^="milo_cdkeyInfo_"]'
|
: '[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 })
|
const activityInfo = await ensureActivityInfoReady(page, { triggerRender: true, timeoutMs: 2_000 })
|
||||||
await fillRedeemCodeInBrowser(page, code, { activityInfo })
|
await fillRedeemCodeInBrowser(page, code, { activityInfo })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function refreshPageCaptcha(page, verifyImgId) {
|
export async function refreshPageCaptcha(page: BrowserPageLike, verifyImgId?: string) {
|
||||||
if (!verifyImgId) {
|
if (!verifyImgId) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -280,7 +425,7 @@ export async function refreshPageCaptcha(page, verifyImgId) {
|
|||||||
).catch(() => null)
|
).catch(() => null)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isCaptchaRejectedResult(result) {
|
export function isCaptchaRejectedResult(result: TencentRedeemResult): boolean {
|
||||||
if (classifyTencentRedeemResult(result).outcome === 'captcha_rejected') {
|
if (classifyTencentRedeemResult(result).outcome === 'captcha_rejected') {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -288,7 +433,7 @@ export function isCaptchaRejectedResult(result) {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
export function classifyTencentRedeemResult(result) {
|
export function classifyTencentRedeemResult(result: TencentRedeemResult): TencentRedeemClassification {
|
||||||
const retCode = Number(result?.iRet)
|
const retCode = Number(result?.iRet)
|
||||||
const message = resolveTencentRedeemMessage(result)
|
const message = resolveTencentRedeemMessage(result)
|
||||||
const normalized = normalizeTencentRedeemText(message)
|
const normalized = normalizeTencentRedeemText(message)
|
||||||
@@ -323,7 +468,7 @@ export function classifyTencentRedeemResult(result) {
|
|||||||
return buildTencentRedeemClassification('failed', retCode, message || '兑换失败')
|
return buildTencentRedeemClassification('failed', retCode, message || '兑换失败')
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetRedeemPopup(page) {
|
function resetRedeemPopup(page: BrowserPageLike) {
|
||||||
return page.evaluate(() => {
|
return page.evaluate(() => {
|
||||||
const popup = document.querySelector('#pop2')
|
const popup = document.querySelector('#pop2')
|
||||||
const popupText = document.querySelector('#PopText')
|
const popupText = document.querySelector('#PopText')
|
||||||
@@ -344,7 +489,7 @@ function resetRedeemPopup(page) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function dismissRedeemRetryPopup(page) {
|
async function dismissRedeemRetryPopup(page: BrowserPageLike) {
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
try {
|
try {
|
||||||
if (typeof window.closeDialog === 'function') {
|
if (typeof window.closeDialog === 'function') {
|
||||||
@@ -372,7 +517,7 @@ async function dismissRedeemRetryPopup(page) {
|
|||||||
await page.waitForTimeout(150)
|
await page.waitForTimeout(150)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function normalizeRedeemOverlay(page) {
|
async function normalizeRedeemOverlay(page: BrowserPageLike) {
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
const overlayIds = ['_overlay_', 'overlay_mask', 'overlay']
|
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)
|
const locator = page.locator(submitSelector)
|
||||||
|
|
||||||
try {
|
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 nextValue = String(value ?? '')
|
||||||
|
|
||||||
const appliedValue = await page.evaluate(
|
const appliedValue = await page.evaluate(
|
||||||
({ targetSelector, targetValue }) => {
|
({ targetSelector, targetValue }) => {
|
||||||
const isVisible = (element) => {
|
const isVisible = (element: Element) => {
|
||||||
if (!(element instanceof HTMLElement)) {
|
if (!(element instanceof HTMLElement)) {
|
||||||
return false
|
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(() => {
|
await page.waitForFunction(() => {
|
||||||
const popup = document.querySelector('#pop2')
|
const popup = document.querySelector('#pop2')
|
||||||
const popupText = document.querySelector('#PopText')
|
const popupText = document.querySelector('#PopText')
|
||||||
@@ -511,7 +656,7 @@ async function waitForRedeemPopup(page) {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
function tryParseJson(text) {
|
function tryParseJson(text: string): any {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(text)
|
return JSON.parse(text)
|
||||||
} catch {
|
} catch {
|
||||||
@@ -519,7 +664,7 @@ function tryParseJson(text) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function inferRedeemCodeFromPopup(popup) {
|
function inferRedeemCodeFromPopup(popup: TencentRedeemPopup) {
|
||||||
const text = `${String(popup?.text || '')} ${String(popup?.detail || '')}`.trim()
|
const text = `${String(popup?.text || '')} ${String(popup?.detail || '')}`.trim()
|
||||||
|
|
||||||
if (!text) {
|
if (!text) {
|
||||||
@@ -537,7 +682,7 @@ function inferRedeemCodeFromPopup(popup) {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveTencentRedeemMessage(result) {
|
export function resolveTencentRedeemMessage(result: TencentRedeemResult): string {
|
||||||
const candidates = [
|
const candidates = [
|
||||||
result?.popup?.detail,
|
result?.popup?.detail,
|
||||||
result?.popup?.text,
|
result?.popup?.text,
|
||||||
@@ -550,7 +695,11 @@ export function resolveTencentRedeemMessage(result) {
|
|||||||
return candidates[0] || '兑换完成'
|
return candidates[0] || '兑换完成'
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildTencentRedeemClassification(outcome, retCode, message) {
|
function buildTencentRedeemClassification(
|
||||||
|
outcome: TencentRedeemOutcome,
|
||||||
|
retCode: number,
|
||||||
|
message: string,
|
||||||
|
): TencentRedeemClassification {
|
||||||
return {
|
return {
|
||||||
outcome,
|
outcome,
|
||||||
success: outcome === 'success',
|
success: outcome === 'success',
|
||||||
@@ -561,7 +710,7 @@ function buildTencentRedeemClassification(outcome, retCode, message) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeTencentRedeemText(value) {
|
function normalizeTencentRedeemText(value: unknown): string {
|
||||||
return String(value || '')
|
return String(value || '')
|
||||||
.replace(/\s+/g, '')
|
.replace(/\s+/g, '')
|
||||||
.trim()
|
.trim()
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
"node_modules",
|
"node_modules",
|
||||||
"data",
|
"data",
|
||||||
"src/**/*.test.js",
|
"src/**/*.test.js",
|
||||||
"src/services/session/**",
|
"src/services/session/**/*.js",
|
||||||
"src/services/platforms/agiso/**/*.js"
|
"src/services/platforms/agiso/**/*.js"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -389,6 +389,14 @@
|
|||||||
- `npm run typecheck`
|
- `npm run typecheck`
|
||||||
- `npm run build`
|
- `npm run build`
|
||||||
- `npm test` 共 139 个用例通过
|
- `npm test` 共 139 个用例通过
|
||||||
|
71. 腾讯浏览器会话兑换服务迁移到 `.ts`:
|
||||||
|
- `src/services/session/session-redeem.ts`
|
||||||
|
72. 会话兑换编排、验证码截图 / OCR payload、页面提交、弹窗结果解析、兑换结果分类已补轻量结构类型;`tsconfig.json` 已将 session 新增 `.ts` 文件纳入 `typecheck`,旧 session `.js` 文件继续暂时排除
|
||||||
|
73. Docker 内验证通过:
|
||||||
|
- `src/services/session/session-redeem.test.js` 共 6 个用例通过
|
||||||
|
- `npm run typecheck`
|
||||||
|
- `npm run build`
|
||||||
|
- `npm test` 共 139 个用例通过
|
||||||
|
|
||||||
## 下一步建议
|
## 下一步建议
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user