feat(trading): 下单流程每步证据带整页截图(html + png + meta 三件套)
- site_interact 新增 PageSnapshot{html, screenshot} 证据载体:
add_to_cart 截商品页、verify_cart 截 cart 页、enter_checkout 截落地
确认页、pay 截付款检查后的完成页;截图全部 best-effort,失败记
warning 留空,不掩盖动作结果
- submit_order 改返回 SubmitOutcome{site_order_id, evidence}:完成页
HTML+截图首次随 step 4 落盘(此前只有 meta)
- fetch_order_detail 详情页截图挂在 OrderStatusSnapshot.screenshot,
监控步骤 write_step 带 png;只读查询通道按字段挑出,不受影响
- runner._run_step 认 PageSnapshot(旧 str 契约保留兼容),step 0/3/4
显式传 png
- 测试:桩同步新契约;新增每步三件套齐全的全流程断言;clear_cart
场景单测补截图断言
- 真账号复验 verify_cart_clear.py:两场景截图均合法 PNG,落盘
.probe/cart_clear/ 并目检为空车页渲染
This commit is contained in:
@@ -35,7 +35,7 @@ from app.trading.worker.client import GatewayClient
|
||||
from app.trading.worker.evidence import EvidenceStore
|
||||
from app.trading.worker.local_db import LocalDB
|
||||
from app.trading.worker.models import LeaseTask
|
||||
from app.trading.worker.site_interact import SiteInteractor
|
||||
from app.trading.worker.site_interact import PageSnapshot, SiteInteractor
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from app.shared.config import Settings
|
||||
@@ -260,8 +260,8 @@ class WorkerRunner:
|
||||
|
||||
每一步的顺序:动作 → 落证据 → 写本地 SQLite → 回报 gateway。
|
||||
开单前的「清购物车」是本机侧卫生步骤(step 0,见下方代码注释):落本地
|
||||
步骤证据(页面 + meta)但不上报 gateway、不记状态事件;站点交互失败会转
|
||||
_execute_with_renewal 的 except 分支上报 needs_human / failed。
|
||||
步骤证据(页面 + 截图 + meta)但不上报 gateway、不记状态事件;站点交互
|
||||
失败会转 _execute_with_renewal 的 except 分支上报 needs_human / failed。
|
||||
"""
|
||||
await self._db.ensure_started(task.task_id, task.site, task.intent)
|
||||
|
||||
@@ -277,7 +277,7 @@ class WorkerRunner:
|
||||
# (包括进程中途崩溃,残留都没人清),这里都从空车起步。
|
||||
#
|
||||
# 这条不在 gateway 上报、不记 order_events:它是本机侧的卫生操作,不是订单
|
||||
# 进度的状态迁移。但清理后的页面快照与 meta 要落本地证据并登记
|
||||
# 进度的状态迁移。但清理后的页面快照、整页截图与 meta 要落本地证据并登记
|
||||
# evidence_index——清理没跑干净被下面的闸门拦下转 needs_human 时,这份现场
|
||||
# 就是排查依据,所以证据必须先于闸门判断落盘。清理后 count 非 0(含
|
||||
# count API 拿不到结果返回 -1)说明清理没跑干净,**不能**带着残留往下
|
||||
@@ -287,6 +287,7 @@ class WorkerRunner:
|
||||
evidence_ref = self._evidence.write_step(
|
||||
task.task_id, 0, "cart-clear",
|
||||
html=cleared.get("html") or None,
|
||||
png=cleared.get("screenshot") or None,
|
||||
meta={
|
||||
"step": "cart-clear",
|
||||
"removed_count": cleared.get("removed_count"),
|
||||
@@ -319,15 +320,16 @@ class WorkerRunner:
|
||||
)
|
||||
|
||||
# 步骤 3:进入下单确认页 + 金额守卫
|
||||
checkout_html = await self._site.enter_checkout(task)
|
||||
summary = await self._site.parse_checkout(checkout_html)
|
||||
checkout = await self._site.enter_checkout(task)
|
||||
summary = await self._site.parse_checkout(checkout.html)
|
||||
self._enforce_amount_guard(task, summary.payable_yen)
|
||||
await self._run_step(
|
||||
task, step_no=3, step_name="order-confirm",
|
||||
action=self._noop(),
|
||||
state=OrderState.CREATED,
|
||||
detail=f"下单确认页已解析:应付 {summary.payable_yen} 円",
|
||||
html=checkout_html,
|
||||
html=checkout.html or None,
|
||||
png=checkout.screenshot or None,
|
||||
evidence_meta={
|
||||
"payable_yen": summary.payable_yen,
|
||||
"site_order_id": summary.site_order_id,
|
||||
@@ -336,13 +338,16 @@ class WorkerRunner:
|
||||
)
|
||||
|
||||
# 步骤 4:提交下单
|
||||
site_order_id = await self._site.submit_order(task)
|
||||
submit = await self._site.submit_order(task)
|
||||
site_order_id = submit.site_order_id
|
||||
await self._run_step(
|
||||
task, step_no=4, step_name="order-submit",
|
||||
action=self._noop(),
|
||||
state=OrderState.ORDERED,
|
||||
site_order_id=site_order_id,
|
||||
detail=f"已提交下单,站点订单号 {site_order_id}",
|
||||
html=submit.evidence.html or None,
|
||||
png=submit.evidence.screenshot or None,
|
||||
)
|
||||
|
||||
# 步骤 5:付款
|
||||
@@ -424,6 +429,7 @@ class WorkerRunner:
|
||||
evidence_ref = self._evidence.write_step(
|
||||
task.task_id, step_no, step_name,
|
||||
html=snapshot.html,
|
||||
png=snapshot.screenshot or None,
|
||||
meta={
|
||||
"step": step_name,
|
||||
"state": snapshot.order_state.value,
|
||||
@@ -485,15 +491,23 @@ class WorkerRunner:
|
||||
pay_deadline: str | None = None,
|
||||
evidence_meta: dict | None = None,
|
||||
html: str | None = None,
|
||||
png: bytes | None = None,
|
||||
) -> None:
|
||||
"""单步执行:动作 → 落证据 → 写本地 → 回报 gateway
|
||||
|
||||
`action` 的返回值若是字符串,视为该步骤要落盘的页面 HTML(add_to_cart /
|
||||
verify_cart 均已改为返回渲染后的页面);显式传入的 `html` 优先级更高,
|
||||
供 step3 等已经单独拿到页面 HTML 的调用点直接使用。
|
||||
`action` 返回 PageSnapshot(add_to_cart / verify_cart / pay 等站点方法的
|
||||
证据载体)时,其 html / screenshot 即本步骤要落盘的页面与整页截图;
|
||||
显式传入的 `html` / `png` 优先级更高,供 step3/step4 等已经单独拿到
|
||||
页面的调用点使用。
|
||||
"""
|
||||
result = await action()
|
||||
if html is None and isinstance(result, str):
|
||||
if isinstance(result, PageSnapshot):
|
||||
if html is None:
|
||||
html = result.html or None
|
||||
if png is None:
|
||||
png = result.screenshot or None
|
||||
elif html is None and isinstance(result, str):
|
||||
# 旧契约兼容:站点方法返回字符串时视为页面 HTML
|
||||
html = result
|
||||
meta = {
|
||||
"step": step_name,
|
||||
@@ -501,7 +515,7 @@ class WorkerRunner:
|
||||
**(evidence_meta or {}),
|
||||
}
|
||||
evidence_ref = self._evidence.write_step(
|
||||
task.task_id, step_no, step_name, html=html, meta=meta
|
||||
task.task_id, step_no, step_name, html=html, png=png, meta=meta
|
||||
)
|
||||
await self._db.index_evidence(task.task_id, step_no, step_name, evidence_ref)
|
||||
await self._db.record_event(
|
||||
|
||||
Reference in New Issue
Block a user