task-93: Similar ASIN、店铺抓取和采集数据页面 E2E 核心路径执行器

This commit is contained in:
2026-08-30 23:08:56 +08:00
parent 77bd83743b
commit 85c8c66c4d
2 changed files with 273 additions and 0 deletions
@@ -0,0 +1,99 @@
/**
* 页面 E2E 核心路径执行器(Task 93)。
*
* 把 Similar ASIN / 店铺抓取 / 采集数据三个页面的 E2E 核心路径收敛为
* 可注入依赖的确定性流程:解析文件(parse)→ 建任务(createTask)→
* 轮询到终态(poll)→ 刷新历史(refreshHistory),各步由 deps 提供实现,
* 页面接入时传真实接口即可复用同一套语义。
*
* 流程语义:
* - parse 返回 0 表示无可处理数据:安全跳过后续步骤,不创建无效资源;
* - poll 返回非终态时按最大次数轮询,超过 maxPollAttempts 降级返回当前
* 状态(不抛错、不无限轮询、不刷新历史);
* - 任一步抛错:执行立即终止,cleanup 必被调用(失败与成功路径都执行),
* 调用方可重新执行同一 deps 恢复;
* - 输入校验 fail-fastdeps 缺函数、parse 返回非法 taskId、maxPollAttempts
* 非法均抛错且零状态变更。
*/
export interface PageE2EDeps {
/** 解析文件/参数,返回 taskId;返回 0 表示无可处理数据 */
parse: () => Promise<number>
/** 创建任务 */
createTask: (taskId: number) => Promise<void>
/** 单次轮询任务进度,返回状态字符串 */
poll: (taskId: number) => Promise<string>
/** 任务到达终态后刷新历史列表(可选) */
refreshHistory?: () => Promise<void>
/** 释放临时资源(URL、控制器、计时器等),失败与成功路径都会调用(可选) */
cleanup?: () => Promise<void>
}
export interface PageE2EOptions {
/** 轮询次数上限,必须为正数,默认 60 */
maxPollAttempts?: number
/** 判定终态,默认 SUCCESS/FAILED */
isTerminal?: (status: string) => boolean
}
export interface PageE2EResult {
taskId: number
status: string
/** 已完成的步骤(按执行顺序) */
completedSteps: string[]
/** 实际轮询次数 */
attempts: number
}
export async function runPageE2ECorePath(
deps: PageE2EDeps,
options: PageE2EOptions = {},
): Promise<PageE2EResult> {
if (typeof deps !== 'object' || deps == null) {
throw new Error('deps 必须是对象')
}
if (typeof deps.parse !== 'function') {
throw new Error('parse 必须是函数')
}
if (typeof deps.createTask !== 'function') {
throw new Error('createTask 必须是函数')
}
if (typeof deps.poll !== 'function') {
throw new Error('poll 必须是函数')
}
const maxPollAttempts = options.maxPollAttempts ?? 60
if (!(maxPollAttempts > 0)) {
throw new Error('maxPollAttempts 必须为正数: ' + maxPollAttempts)
}
const isTerminal = options.isTerminal ?? ((status: string) => status === 'SUCCESS' || status === 'FAILED')
const completedSteps: string[] = []
let status = ''
let attempts = 0
try {
const taskId = await deps.parse()
if (taskId === 0) {
completedSteps.push('parse')
return { taskId: 0, status: '', completedSteps, attempts: 0 }
}
if (typeof taskId !== 'number' || !Number.isFinite(taskId) || taskId <= 0) {
throw new Error('parse 必须返回正整数 taskId: ' + taskId)
}
completedSteps.push('parse')
await deps.createTask(taskId)
completedSteps.push('create-task')
while (attempts < maxPollAttempts) {
attempts += 1
status = await deps.poll(taskId)
completedSteps.push('poll')
if (isTerminal(status)) {
await deps.refreshHistory?.()
completedSteps.push('refresh-history')
return { taskId, status, completedSteps, attempts }
}
}
return { taskId, status, completedSteps, attempts }
} finally {
await deps.cleanup?.()
}
}