Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d550d7fc71 | ||
|
|
dee5aa0821 | ||
|
|
c30cd0d96d |
@@ -11,7 +11,8 @@ export default [
|
||||
code: 'WAIT_READY',
|
||||
name: '等待页面稳定',
|
||||
action: 'wait',
|
||||
locator: { type: 'text', value: '切换账号' },
|
||||
// 仅作页面稳定门槛,不要求元素可点击,避免不同机型 elementFromPoint 命中测试误判超时
|
||||
locator: { type: 'text', value: '切换账号', requireInteractable: false },
|
||||
waitTimeout: 12000,
|
||||
delay: 0
|
||||
},
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
<script>
|
||||
import appConfig from '../../config/index'
|
||||
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 { updateDeviceWorkStatus } from '../../api/device'
|
||||
// dryRun 由 appConfig.dryRun 控制,true 时跳过所有后端上报
|
||||
@@ -349,6 +349,9 @@ export default {
|
||||
return
|
||||
}
|
||||
|
||||
// 步骤执行前先做一次 JS 桥自检,便于远程区分“元素问题”还是“取值桥失效”
|
||||
await this.runBridgeSelfTest()
|
||||
|
||||
// 运行步骤引擎
|
||||
const context = {
|
||||
amount: task.amount,
|
||||
@@ -477,6 +480,58 @@ export default {
|
||||
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 appConfig from '../config/index'
|
||||
import { captureAndUploadErrorScreenshot } from './screenshot'
|
||||
import { captureDomSnapshot } from './webviewManager'
|
||||
|
||||
/**
|
||||
* 步骤阶段映射。
|
||||
@@ -152,7 +153,8 @@ export async function runStepEngine(options) {
|
||||
stepCode: step.code,
|
||||
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)
|
||||
if (onStepEnd) onStepEnd(i, step, { success: false, message: timeoutMsg })
|
||||
await reportStageFailure(stageStatusMap, step, timeoutMsg, reportStep, shouldReportRemoteStep)
|
||||
@@ -240,7 +242,8 @@ export async function runStepEngine(options) {
|
||||
stepCode: step.code,
|
||||
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 reportStageFailure(stageStatusMap, step, result.message, reportStep, shouldReportRemoteStep)
|
||||
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 {string} message 失败消息
|
||||
* @param {object|null} screenshotData 截图结果
|
||||
* @param {object|null} basePayload 原有步骤负载
|
||||
* @param {object} runtimeSnapshots 运行期账号快照
|
||||
* @param {object|null} domData DOM 快照结果
|
||||
* @returns {object} 上报负载
|
||||
*/
|
||||
function buildStepFailurePayload(step, message, screenshotData, basePayload = null, runtimeSnapshots = {}) {
|
||||
function buildStepFailurePayload(step, message, screenshotData, basePayload = null, runtimeSnapshots = {}, domData = null) {
|
||||
return {
|
||||
...(basePayload || {}),
|
||||
stepCode: step.code,
|
||||
@@ -340,7 +358,17 @@ function buildStepFailurePayload(step, message, screenshotData, basePayload = nu
|
||||
failMessage: message,
|
||||
douyinLoginAccount: runtimeSnapshots.DOUYIN_LOGIN || '',
|
||||
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),不等待返回。
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user