Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d550d7fc71 | ||
|
|
dee5aa0821 | ||
|
|
c30cd0d96d |
@@ -11,7 +11,8 @@ export default [
|
|||||||
code: 'WAIT_READY',
|
code: 'WAIT_READY',
|
||||||
name: '等待页面稳定',
|
name: '等待页面稳定',
|
||||||
action: 'wait',
|
action: 'wait',
|
||||||
locator: { type: 'text', value: '切换账号' },
|
// 仅作页面稳定门槛,不要求元素可点击,避免不同机型 elementFromPoint 命中测试误判超时
|
||||||
|
locator: { type: 'text', value: '切换账号', requireInteractable: false },
|
||||||
waitTimeout: 12000,
|
waitTimeout: 12000,
|
||||||
delay: 0
|
delay: 0
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -124,7 +124,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import appConfig from '../../config/index'
|
import appConfig from '../../config/index'
|
||||||
import { openRechargeWebview, closeRechargeWebview, clearPlatformWebviewState } from '../../services/webview'
|
import { openRechargeWebview, closeRechargeWebview, clearPlatformWebviewState } from '../../services/webview'
|
||||||
import { openWebview, closeWebview, waitForWebviewLoaded } from '../../services/webviewManager'
|
import { openWebview, closeWebview, waitForWebviewLoaded, evalJSOnce } from '../../services/webviewManager'
|
||||||
import { startTask, reportTaskResult, reportTaskStep } from '../../api/task'
|
import { startTask, reportTaskResult, reportTaskStep } from '../../api/task'
|
||||||
import { updateDeviceWorkStatus } from '../../api/device'
|
import { updateDeviceWorkStatus } from '../../api/device'
|
||||||
// dryRun 由 appConfig.dryRun 控制,true 时跳过所有后端上报
|
// dryRun 由 appConfig.dryRun 控制,true 时跳过所有后端上报
|
||||||
@@ -349,6 +349,9 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 步骤执行前先做一次 JS 桥自检,便于远程区分“元素问题”还是“取值桥失效”
|
||||||
|
await this.runBridgeSelfTest()
|
||||||
|
|
||||||
// 运行步骤引擎
|
// 运行步骤引擎
|
||||||
const context = {
|
const context = {
|
||||||
amount: task.amount,
|
amount: task.amount,
|
||||||
@@ -477,6 +480,58 @@ export default {
|
|||||||
return String(failReason || '').indexOf('账号不存在') > -1
|
return String(failReason || '').indexOf('账号不存在') > -1
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WebView JS 桥自检。
|
||||||
|
* 用不依赖页面 DOM 的探针验证 evalJS(titleUpdate 取值回路)是否可用,
|
||||||
|
* 结果同时写入本地步骤日志与后端充值日志,便于远程区分
|
||||||
|
* “元素检测全部超时”到底是元素/命中测试问题,还是取值桥本身失效。
|
||||||
|
* 仅记录日志,不阻断主流程。
|
||||||
|
*
|
||||||
|
* @returns {Promise<boolean>} 取值桥是否可用
|
||||||
|
*/
|
||||||
|
async runBridgeSelfTest() {
|
||||||
|
let ping = null
|
||||||
|
let readyState = null
|
||||||
|
try {
|
||||||
|
ping = await evalJSOnce('return "AF_PING_" + (1 + 1)', 3000)
|
||||||
|
readyState = await evalJSOnce('return document.readyState', 3000)
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[recharge] bridge self-test error:', error)
|
||||||
|
}
|
||||||
|
|
||||||
|
const bridgeOk = ping === 'AF_PING_2'
|
||||||
|
const message = bridgeOk
|
||||||
|
? `JS桥正常 (readyState=${readyState || 'unknown'})`
|
||||||
|
: `JS桥异常:未取到探针返回值 (ping=${JSON.stringify(ping)}, readyState=${JSON.stringify(readyState)})`
|
||||||
|
|
||||||
|
console.log('[recharge] bridge self-test:', message)
|
||||||
|
|
||||||
|
this.$store.commit('pushStepLog', {
|
||||||
|
stepCode: 'BRIDGE_SELFTEST',
|
||||||
|
stepName: 'JS桥自检',
|
||||||
|
stepStatus: bridgeOk ? 'SUCCESS' : 'FAILED',
|
||||||
|
message
|
||||||
|
})
|
||||||
|
|
||||||
|
if (appConfig.dryRun || !this.taskId) {
|
||||||
|
return bridgeOk
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await reportTaskStep({
|
||||||
|
taskId: this.taskId,
|
||||||
|
stepCode: 'BRIDGE_SELFTEST',
|
||||||
|
stepName: 'JS桥自检',
|
||||||
|
stepStatus: bridgeOk ? 'SUCCESS' : 'FAILED',
|
||||||
|
message
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[recharge] report bridge self-test failed:', error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return bridgeOk
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 对步骤引擎外的页面级失败做统一回传,保证加载失败也能带上截图。
|
* 对步骤引擎外的页面级失败做统一回传,保证加载失败也能带上截图。
|
||||||
*
|
*
|
||||||
|
|||||||
+33
-5
@@ -8,6 +8,7 @@ import { executeAction, waitForElement } from './actionExecutor'
|
|||||||
import { reportTaskStep, reportTaskResult } from '../api/task'
|
import { reportTaskStep, reportTaskResult } from '../api/task'
|
||||||
import appConfig from '../config/index'
|
import appConfig from '../config/index'
|
||||||
import { captureAndUploadErrorScreenshot } from './screenshot'
|
import { captureAndUploadErrorScreenshot } from './screenshot'
|
||||||
|
import { captureDomSnapshot } from './webviewManager'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 步骤阶段映射。
|
* 步骤阶段映射。
|
||||||
@@ -152,7 +153,8 @@ export async function runStepEngine(options) {
|
|||||||
stepCode: step.code,
|
stepCode: step.code,
|
||||||
filePrefix: 'step_timeout'
|
filePrefix: 'step_timeout'
|
||||||
})
|
})
|
||||||
const failPayload = buildStepFailurePayload(step, timeoutMsg, screenshotData, null, runtimeSnapshots)
|
const domData = await captureDomSnapshotSafe()
|
||||||
|
const failPayload = buildStepFailurePayload(step, timeoutMsg, screenshotData, null, runtimeSnapshots, domData)
|
||||||
await reportStep(step.code, step.name, 'FAILED', timeoutMsg, shouldReportRemoteStep, failPayload)
|
await reportStep(step.code, step.name, 'FAILED', timeoutMsg, shouldReportRemoteStep, failPayload)
|
||||||
if (onStepEnd) onStepEnd(i, step, { success: false, message: timeoutMsg })
|
if (onStepEnd) onStepEnd(i, step, { success: false, message: timeoutMsg })
|
||||||
await reportStageFailure(stageStatusMap, step, timeoutMsg, reportStep, shouldReportRemoteStep)
|
await reportStageFailure(stageStatusMap, step, timeoutMsg, reportStep, shouldReportRemoteStep)
|
||||||
@@ -240,7 +242,8 @@ export async function runStepEngine(options) {
|
|||||||
stepCode: step.code,
|
stepCode: step.code,
|
||||||
filePrefix: 'step_failed'
|
filePrefix: 'step_failed'
|
||||||
})
|
})
|
||||||
const failPayload = buildStepFailurePayload(step, result.message, screenshotData, result.payload, runtimeSnapshots)
|
const domData = await captureDomSnapshotSafe()
|
||||||
|
const failPayload = buildStepFailurePayload(step, result.message, screenshotData, result.payload, runtimeSnapshots, domData)
|
||||||
await reportStep(step.code, step.name, 'FAILED', result.message, shouldReportRemoteStep, failPayload)
|
await reportStep(step.code, step.name, 'FAILED', result.message, shouldReportRemoteStep, failPayload)
|
||||||
await reportStageFailure(stageStatusMap, step, result.message, reportStep, shouldReportRemoteStep)
|
await reportStageFailure(stageStatusMap, step, result.message, reportStep, shouldReportRemoteStep)
|
||||||
const failReason = `步骤[${step.name || step.code}]失败: ${result.message}`
|
const failReason = `步骤[${step.name || step.code}]失败: ${result.message}`
|
||||||
@@ -323,16 +326,31 @@ function sleep(ms) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 组装失败步骤的扩展上下文,保留原有 payload 并附带截图地址。
|
* 安全抓取页面 DOM 快照,失败不影响主流程上报。
|
||||||
|
*
|
||||||
|
* @returns {Promise<object|null>} DOM 快照结果
|
||||||
|
*/
|
||||||
|
async function captureDomSnapshotSafe() {
|
||||||
|
try {
|
||||||
|
return await captureDomSnapshot()
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[stepEngine] captureDomSnapshot error:', e)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 组装失败步骤的扩展上下文,保留原有 payload 并附带截图地址与 DOM 快照。
|
||||||
*
|
*
|
||||||
* @param {object} step 当前步骤
|
* @param {object} step 当前步骤
|
||||||
* @param {string} message 失败消息
|
* @param {string} message 失败消息
|
||||||
* @param {object|null} screenshotData 截图结果
|
* @param {object|null} screenshotData 截图结果
|
||||||
* @param {object|null} basePayload 原有步骤负载
|
* @param {object|null} basePayload 原有步骤负载
|
||||||
* @param {object} runtimeSnapshots 运行期账号快照
|
* @param {object} runtimeSnapshots 运行期账号快照
|
||||||
|
* @param {object|null} domData DOM 快照结果
|
||||||
* @returns {object} 上报负载
|
* @returns {object} 上报负载
|
||||||
*/
|
*/
|
||||||
function buildStepFailurePayload(step, message, screenshotData, basePayload = null, runtimeSnapshots = {}) {
|
function buildStepFailurePayload(step, message, screenshotData, basePayload = null, runtimeSnapshots = {}, domData = null) {
|
||||||
return {
|
return {
|
||||||
...(basePayload || {}),
|
...(basePayload || {}),
|
||||||
stepCode: step.code,
|
stepCode: step.code,
|
||||||
@@ -340,7 +358,17 @@ function buildStepFailurePayload(step, message, screenshotData, basePayload = nu
|
|||||||
failMessage: message,
|
failMessage: message,
|
||||||
douyinLoginAccount: runtimeSnapshots.DOUYIN_LOGIN || '',
|
douyinLoginAccount: runtimeSnapshots.DOUYIN_LOGIN || '',
|
||||||
alipayLoginAccount: runtimeSnapshots.ALIPAY_ACCOUNT || '',
|
alipayLoginAccount: runtimeSnapshots.ALIPAY_ACCOUNT || '',
|
||||||
...(screenshotData || {})
|
...(screenshotData || {}),
|
||||||
|
...(domData && domData.ok
|
||||||
|
? {
|
||||||
|
domUrl: domData.url || '',
|
||||||
|
domTitle: domData.title || '',
|
||||||
|
domReadyState: domData.readyState || '',
|
||||||
|
domTruncated: !!domData.truncated,
|
||||||
|
domLength: domData.length || 0,
|
||||||
|
domHtml: domData.html || ''
|
||||||
|
}
|
||||||
|
: {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -372,6 +372,83 @@ export function evalJSOnce(script, timeout = 3000) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 抓取当前 WebView 页面 DOM 快照,用于运行异常时回传定位。
|
||||||
|
*
|
||||||
|
* 由于 native↔WebView 取值依赖 document.title,单次能携带的长度在不同机型上
|
||||||
|
* 限制不一,这里先在页内清洗+截断 HTML 存到 window,再分片取回拼装,避免超长丢失。
|
||||||
|
* 若取值桥本身不可用(如某些机型不发 titleUpdate),会返回 null,可据此与桥自检互相印证。
|
||||||
|
*
|
||||||
|
* @param {object} [options] 抓取参数
|
||||||
|
* @param {number} [options.maxLength=20000] 清洗后 HTML 的最大保留长度
|
||||||
|
* @param {number} [options.chunkSize=1500] 单次分片取回的字符数
|
||||||
|
* @returns {Promise<object|null>} { ok, url, title, readyState, truncated, length, html }
|
||||||
|
*/
|
||||||
|
export async function captureDomSnapshot(options = {}) {
|
||||||
|
if (!webviewInstance) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxLength = Number(options.maxLength || 20000)
|
||||||
|
const chunkSize = Number(options.chunkSize || 1500)
|
||||||
|
|
||||||
|
const prepScript = `
|
||||||
|
try {
|
||||||
|
var html = document.documentElement ? document.documentElement.outerHTML : '';
|
||||||
|
html = html
|
||||||
|
.replace(/<script[\\s\\S]*?<\\/script>/gi, '<script>[stripped]</script>')
|
||||||
|
.replace(/<style[\\s\\S]*?<\\/style>/gi, '<style>[stripped]</style>')
|
||||||
|
.replace(/ on[a-z]+="[^"]*"/gi, '');
|
||||||
|
var truncated = html.length > ${maxLength};
|
||||||
|
if (truncated) { html = html.slice(0, ${maxLength}); }
|
||||||
|
window.__afDom = html;
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
total: window.__afDom.length,
|
||||||
|
truncated: truncated,
|
||||||
|
url: location.href,
|
||||||
|
title: document.title,
|
||||||
|
readyState: document.readyState
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
return { ok: false, message: String((e && e.message) || e) };
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const meta = await evalJSOnce(prepScript, 4000)
|
||||||
|
if (!meta) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (!meta.ok) {
|
||||||
|
return { ok: false, message: meta.message || 'DOM 抓取失败' }
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = ''
|
||||||
|
for (let start = 0; start < meta.total; start += chunkSize) {
|
||||||
|
const sliceScript = `return (window.__afDom || '').slice(${start}, ${start + chunkSize});`
|
||||||
|
const part = await evalJSOnce(sliceScript, 3000)
|
||||||
|
if (typeof part !== 'string') {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
html += part
|
||||||
|
if (part.length < chunkSize) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
evalJSSync('try { delete window.__afDom; } catch (e) { window.__afDom = null; }')
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
url: meta.url || '',
|
||||||
|
title: meta.title || '',
|
||||||
|
readyState: meta.readyState || '',
|
||||||
|
truncated: !!meta.truncated,
|
||||||
|
length: html.length,
|
||||||
|
html
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 在 WebView 中执行 JS(fire-and-forget),不等待返回。
|
* 在 WebView 中执行 JS(fire-and-forget),不等待返回。
|
||||||
*
|
*
|
||||||
|
|||||||
Reference in New Issue
Block a user