 q792602257andClaude Opus 5
|
3c7618a1d6
|
feat(observability): 抓取去掉首页预热,交易补齐链路埋点
两个问题一起处理,都与「出站请求与可观测性」有关。
## 抓取:正常路径不再多打一次首页
site_session 原先每条通道每 30 分钟打一次 www.rakuten.co.jp/ 做预热,而且预热
返回非 2xx 时 warmed_at 不置位——那种情况下每个请求前都会再打一次首页。
Akamai 的 cookie 随任意页面响应下发,目标页自己就会带回来,专门先打一次首页除了
多一个出站请求(以及多一次被风控计数的机会)之外没有额外收益:首个请求无论打哪个
URL 都是冷的 ~11s,之后都复用 cookie。
改为 cookie 由目标页响应建立(_note_cookies)、超 TTL 主动清空
(_drop_expired_cookies)。首页只保留在失败修复路径上(_rewarm_on_home):目标页
已经吃了挑战页时,拿首页换一套干净 cookie 比继续撞同一个 URL 更安全。happy path
的出站请求数 2 → 1。
_note_cookies 刻意不在每次响应时刷新时刻:TTL 要从「这套 cookie 第一次出现」算起,
每次都刷新会让一套 cookie 被无限续命,反而绕过了 session_ttl_seconds 的本意。
profile_status() 的 warmed 字段名保留(上游健康检查看板在用),语义改为「当前有
可复用的 Akamai cookie」,不再代表「已专门预热过首页」。
## 交易:此前没有任何有意义的链路数据
根因是 trading 的实际工作两类自动埋点都覆盖不到:站点交互走 Playwright(不经
httpx),worker 主循环是后台 asyncio 任务(没有 HTTP 入口,因此没有根 span)。
于是发给网关的每次 httpx 调用各自成为孤立 trace——观测后台上只剩一堆请求记录。
新增手工埋点:
- order.task:一笔下单的根 span,一个 task_id 一条 trace,带 order.route
(execute / recovery / already_finished)与终态 order.terminal_status
- order.step.*:清车 → 加购 → 校验 → 确认 → 提交 → 付款,每步一个子 span,
带 order.evidence_ref,可从 span 直接定位落盘证据
- site.*:12 个 Playwright 交互方法(用 traced 装饰器而非 with 块——这些方法的
函数体本就很长,再加一层缩进不利于阅读)
- account_query:只读查询单的根 span,带 query.outcome
空转的长轮询(30 秒一次、绝大多数返回空)用 suppressed() 屏蔽:量大且没有信息量,
把观测后台刷满的正是它们。领到任务后的网关调用都在任务根 span 底下,不受影响。
闸门 / 风控拦截会被 _execute_with_renewal 吞掉转 needs_human,异常冒不到根 span,
被拦下的单在 trace 里跟成功下单一模一样。加 _execute_recording_errors 一层统一
记录,比每个 except 分支各写一遍省事,也不会漏掉后续新增的分支。
_report_safe 写 span 属性前判断 is_recording():付款后监控是 create_task 起的,
asyncio 在创建时就把 context 复制了进去,等它真正跑起来根 span 早已结束——
get_current_span() 拿到的仍是那个已结束的 span(不是 INVALID_SPAN),写属性会打
"Setting attribute on ended span"。当前监控路径不传 terminal_status 走不到那里,
这道判断是防以后。
## 顺带修掉:instrument_app 从未生效
instrument_app 用 _provider is None 做前置判断,但三个服务都在模块导入时执行
app = create_app(),而 setup_telemetry 要等 lifespan 才跑——那时 _provider 还是
None,照着判断直接 return。**FastAPI 从来没被打桩过,三个服务一条 server span
都没有。**
实测确认两件事:导入期打桩能出 span,lifespan 内打桩出不来(instrument_app 是加
中间件,应用开始服务后加进去不生效);provider 后设也不影响 ProxyTracer 委托到
真实 provider。所以只能在导入期装,判断条件改为 otel_enabled。
app/gateway/main.py 此前完全没接 telemetry,worker 出站请求带过来的 traceparent
没人接上,一条下单链路在网关这里断掉,只看得到 worker 侧那半截。补上
setup_telemetry(service_name="rakuten-gateway") 与 instrument_app / shutdown。
## 验证
新增 8 个用例:首页零请求、cookie 复用与过期清空、失败后用首页换 cookie、一任务
一 trace 的父子结构、闸门失败标 ERROR、空转不埋点,以及 instrument_app 调用顺序
的回归测试。全量 526 passed。
Playwright 那些 site.* 埋点只做了静态验证(测试用桩替换站点方法),没有跑真实
浏览器下单确认 span 真的落地。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
2026-08-28 14:56:49 +08:00 |
|
 q792602257andClaude Opus 5
|
3284e086ad
|
feat(trading): 浏览器掉线兜底——任务边界自愈重建,中途掉线转 needs_human
SiteInteractor 的 Chromium 是进程级单例,此前启动后即假定永远活着:全仓唯一的
is_connected() 探活在 scraping 侧,交易侧既不探活也不重启。容器里 Chromium 崩溃
是有真实前提的(/dev/shm 不足、OOM kill、seccomp 挡 sandbox,docker-compose.yml
里已有相关注释),一旦发生,进程还活着但之后每一单都会失败,且 /health 恒返回
ok,restart: unless-stopped 永远不会被触发。
更隐蔽的一条:clear_cart / enter_checkout / pay 等处的 new_page()、context.request
写在 try 之外,掉线抛的 TargetClosedError 不是 AppError,会穿过 runner 的
except AppError 落到主循环那个只记日志的兜底里——任务一次都不上报,网关侧要干等
整个 lease_ttl(默认 300s)才被 sweep 置 stale。clear_cart 是 execute() 的 step 0,
浏览器死时最先撞上的正是它。
分三层处理:
- 任务边界自愈。_launch() 从 start() 抽出,_context_options() 统一 context 参数
(重建必须与启动完全一致,指纹漂移就是一次风控事件);_refresh_context_if_stale
改名 _ensure_context_ready(),先探活重建再做原有的 storage_state mtime 检查
(顺序不可换,mtime 重建要用 self._browser)。重建前丢弃 _checkout_pages 里的
残留确认页并记 warning——那些 Page 已随浏览器一起没了。
- 中途掉线不重建,抛 BrowserDeadError(新增,5006)。新增 _new_page() 与
_request() 两个壳收口裸异常;_request() 只在确认浏览器真死了时才改写异常,
站点 5xx 这类正常业务失败原样抛出。submit_order / pay 入口用
_require_live_browser() 直接拒绝:这两步复用 enter_checkout 留存的 Page,
重建救不回服务端订单草稿,而 pay 跑的时候订单已经真的提交了。顺带修掉一个
误诊——浏览器死时 submit_order 原先报「未找到确认按钮」,把「浏览器崩了」
说成「站点改版了」,两者的处置方式完全不同。
- runner 把 BrowserDeadError 转 needs_human 而非 failed,except 分支排在
except AppError 之前(子类,顺序反了就报 failed)。掉线发生在动作中途,
站点侧生效与否无从判断,不能给上游「明确失败」的结论。
/health 暴露 browser 状态,掉线时 degraded + HTTP 503 + code 5006,让
Dockerfile.trading 的 HEALTHCHECK 探到并重启容器。重启不会导致重复下单:网关侧
任务绝不自动重投,租约过期只置 stale 等人工 reclaim(docs/order-gateway.md §5),
重启只是恢复领新任务的能力。启动窗口期 started=False 不算掉线。
README 错误码表补 5005(此前遗漏)与 5006。
新增 15 个用例覆盖探活三态、is_connected() 自身抛错、边界重建/不重建/丢弃残留页/
重建失败、两个包装壳的分支、submit/pay 拒绝、runner 转 needs_human、/health 503。
真实 Chromium 崩溃无法在离线测试里制造,用例模拟的是 is_connected() 返回 False
这个唯一可观测信号,覆盖的是代码对该信号的反应而非崩溃本身;容器 HEALTHCHECK
真的触发重启这条链路尚未实跑验证。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
2026-08-20 16:01:47 +08:00 |
|
q792602257
|
f31e127ba7
|
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/ 并目检为空车页渲染
|
2026-08-17 08:56:11 +08:00 |
|
q792602257
|
086ab82614
|
feat(trading): 清空购物车落步骤证据,补空车/有商品两场景测试
- clear_cart 返回清理后 cart 页最终 HTML(best-effort,抓取失败不掩盖清理结果)
- runner step 0 落 00-cart-clear.{html,meta.json} 并登记 evidence_index,
证据先于闸门判断落盘(清理未净被拦时这份现场就是排查依据);
仍是本机卫生步骤:不上报 gateway、不记 order_events
- /api/cart/clear 响应不携带 html(路由显式挑字段,与 cart_add 同风格)
- 单测:fake page 覆盖空车/有商品/HTML 抓取失败;runner 层覆盖证据落盘
与闸门拦截场景
- 真账号复验 scripts/verify_cart_clear.py:空车 removed=0/count=0;
加购 1 件后 clear removed=1/count=0,status 复核 101
|
2026-08-17 08:32:11 +08:00 |
|
q792602257
|
daa5555fd4
|
fix(trading): cart count status=101 识别为空车,区分空车与获取失败
|
2026-08-17 01:42:08 +08:00 |
|
q792602257
|
3b53b8f28b
|
@
feat(trading): 详情页派送到真实样本,补结构化配送状态并修 stepper 回归
用真账号实测爬取订单详情页(scripts/probe_order_detail.py,样本落盘
.probe/order_detail/),首次拿到 pageType="ph-detail" 的真实 __INITIAL_STATE__,
此前「详情页结构从未有样本、只原样透传」的缺口由此闭合:
- _parse_order_detail_status:新增,优先从 orderData.shippingList[].deliveryInfo
.deliveryStatus 结构化枚举判配送阶段;映射遵循「只映射实测值」,目前仅
CHECKING_ORDER,未识别枚举交回 stepper 兜底(防掐掉 SHIPPED/DELIVERED 上报)。
- _ORDER_STEPPER_ITEM_PATTERN:修回归——不锚定 <li class="item--3gWCU"> 时面包屑
li 会抢走进度条第0项、导致 -active-- 永远判不出当前阶段(真实样本复现)。
- OrderStatusSnapshot 增 delivery_status 字段;query_runner order_detail 透传。
- docs/order-gateway.md §11 更新实测边界;新增详情页样本回溯测试。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@
|
2026-08-16 23:04:59 +08:00 |
|
 q792602257andClaude Opus 5
|
c03158488b
|
feat(gateway): 账号只读查询通道——从已登录账号取真实订单
上游要的不只是网关记的任务状态镜像,还有「已登录账号在站点上的真实订单」,
但账号只在 NAT 后本地机上,只能经网关队列走。新增独立查询通道(§11):
- gateway 单开 account_queries 表 + QueryStatus 状态机,接口
POST /api/account/queries(幂等)/ lease / {id}/result / {id}
- 不复用下单任务队列:查询是只读,租约过期可安全重投(与下单「绝不自动
重投」相反),且不该被全局并发度 1 堵死、task_reports 是订单镜像不能污染
- 本地交易服务起第二条常驻循环 query_runner,领到即调 SiteInteractor 真读:
order_list 复用已实测的 list_recent_orders(规范化字段 + 站点
orderListData 原文),order_detail 复用 fetch_order_detail(配送阶段 +
页面 __INITIAL_STATE__ 原样透传,结构未经真实样本,不抽字段)
- 账号级串行仍由 SiteInteractor 的锁保证;每次执行套超时按失败回报
- 错误码 6005/6006(查询通道,可重试只读区别于 6001-6004);/health 暴露
queued_query_count;结果体积上限先丢原始 JSON
openapi.json 重导,docs/order-gateway.md §11、README、.env.example 补全
配置与实测边界。全量测试 404→454 通过。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
2026-08-16 22:46:01 +08:00 |
|
q792602257
|
fabca9510d
|
feat: 为出站请求统一接入 HTTP 代理
|
2026-08-14 16:08:39 +08:00 |
|
 q792602257andClaude Opus 5
|
e2875f0c00
|
自动重登修两处:并发去重读错缓存、只读订单查询掉登录被静默吞掉
- try_relogin 的并发去重原本读 status().logged_in,但重登成功后的 reload()
会把它重置成 None,排队在 site 锁上的调用方一律判「还没人登上」,N 个并发
调用会串行触发 N 次真实登录(各自最长 relogin_timeout)。改用 _relogin_epochs
计数:等锁期间 epoch 变过就真探测一次,已登录即跳过;仍未登录说明这轮站点侧
就是登不上(验证码/密码错/风控),直接失败,不在同一波并发里重复触发。
原并发测试的桩自相矛盾(login_one 返回成功、探针页始终回未登录),断言只能
松到 count >= 1;桩改为登录成功时翻转探针页,断言收紧到 count == 1。
- check_order_status / list_recent_orders 执行中掉登录此前会被静默吞掉:订单页
被踢到 SSO 后既不报错也没订单号,_parse_order_status 返回 found=False 被
_monitor_order 当成「订单还没反映出来」继续轮询(默认 3 小时一轮),
_parse_order_list 则退化成空列表让 verify_on_site 转 unknown 卡住等人工。
新增 SiteInteractor._read_with_relogin_retry 外壳:只读操作中途判定掉登录时
重登一次并整个重跑,第二次仍失败抛 NotLoggedInError。只给读操作用——写操作
中途掉登录不能重跑(上次动作可能已在站点侧生效),这条边界在两边文档里写明。
- 判据是新增的 auth_site.looks_logged_out:与探针页上权威的 is_logged_in 分开,
它是业务页上的单边启发式(返回 False 不代表登录着),只用于「判错最多多花一次
重登」的重试决策。刻意排除 session/upgrade——那是已登录时的站点风控复核密码,
不是 cookie 过期,误判会把风控当掉登录去重登。
判据里「掉登录会跳到 SSO 域」这一步没有真实探测证据(要复现得先让一份真实登录态
过期),是按站点通行行为的推断,已在常量注释标注;新增测试用替身页面,不是真实
站点 HTML。399 测试全绿(仓库未配 ruff/flake8/mypy,只跑了 pytest)。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
2026-08-14 14:05:47 +08:00 |
|
 q792602257andClaude Sonnet 5
|
295fc7ad69
|
结算失败关键节点落调试快照(HTML+截图),方便排查 needs_human 场景
enter_checkout/submit_order/pay 抛错前尽力落一份页面快照到
evidence_dir/{task_id}/debug-*,与已有编号步骤证据同目录;快照方法自身
失败只记日志,不会掩盖原始异常。之前失败时只有日志文字,要复现现场只能
靠临时探针脚本重新触发一遍真实流程。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
2026-08-14 02:07:12 +08:00 |
|
 q792602257andClaude Sonnet 5
|
e5f7da09a9
|
支付方式选择/新卡代填链路用真实站点探针验证并修复两处真实 bug
真实调用 _select_payment_method 验证「已有匹配卡→点次へ」分支成功;
强制走新卡代填分支验证 _fill_new_card_form/_submit_new_card_form,发现并
修复:1) 选中支付方式后详情面板默认折叠,需再点一次才能展开找到「新增卡」
链接;2)「追加する」提交按钮有重复 DOM 匹配,需逐个尝试真正可点的那个,
不能盲用 .first。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
2026-08-14 01:56:41 +08:00 |
|
 q792602257andClaude Sonnet 5
|
8b2fd74336
|
修真实结算流程三处硬伤:headless SPA 异常、次へ按钮选择器猜错、确认页误判
实测发现 headless=True 会让购物车/结算 SPA 表现异常(购入手続き点了不跳转),
所有会改站点状态的操作改用非无头浏览器;session upgrade/电话补录页的「次へ」
提交按钮选择器一直是猜的 button:has-text,真实控件是 div[role=button] 的
e2e 测试钩子类,从未真正匹配过;enter_checkout 的中间步骤跳转循环没有确认
是否真的离开了购物车页就会把卡住的购物车页当成确认页返回,现在会显式报错。
用真实账号非无头浏览器验证过整条链路能落地到真实 order-confirmation 页(未
触发实际付款)。同时按之前的决定放开新卡表单自动提交,新增地址确认页探针
脚本备用。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
2026-08-14 01:11:18 +08:00 |
|
q792602257
|
93c1a83406
|
实现租约恢复核对:verify_on_site 接真实订单列表反查
新增 SiteInteractor.list_recent_orders(分页拉 order.my.rakuten.co.jp 订单列表,
按商品 URL 反查)+ GatewayClient.get_task,替换掉恒返回 UNKNOWN 的桩。
NOT_ORDERED 分支目前只有逻辑验证、没有真实多单数据支撑,刻意仍路由到
needs_human,不自动重新下单。全程只读查询,未触发任何真实付款操作。
|
2026-08-13 23:58:54 +08:00 |
|
 q792602257andClaude Sonnet 5
|
51e2538438
|
实现付款后订单监控:真实探测 order.my.rakuten.co.jp 配送阶段
用真实订单号 306087-20260813-0863947697 探测 order.my.rakuten.co.jp(订单列表/
详情页),拿到真实 DOM 结构后实现 SiteInteractor.check_order_status:
- 详情页 URL 可直接从 site_order_id 构造(shop_id 是订单号第一段)
- 配送阶段用「进度条」组件的 4 个固定阶段(ショップ/出荷/配達店/配達完了),
当前阶段的 class 带 -active-- 中缀,映射到 OrderState.SHIPPED/DELIVERED
- 查不到订单号、进度条解析不出新阶段都不算错误,交给轮询循环继续重试
runner.WorkerRunner 新增 _monitor_order 后台轮询:付款成功上报后以
asyncio.create_task 起后台任务(不阻塞主循环领下一单,因为 SiteInteractor 的
Playwright 操作全程持锁串行化),状态变化时用 terminal=False 追加 report;
新增 cancel_monitors() 在服务关闭时于 SiteInteractor.close() 之前收尾。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
2026-08-13 23:01:35 +08:00 |
|
 q792602257andClaude Sonnet 5
|
5f7921b788
|
结算流程用真实下单证据修正:新卡代填 iframe vault、订单号正则、登录态判据
- 新卡代填改用真实 DOM 结构:卡号/有效期分别托管在 Rakuten PCI 代付 vault 的
跨域 iframe 里,此前按 autocomplete/name 猜的 selector 在主文档里根本找不到
元素;持卡人姓名字段改用「名義人」标签相对定位,不用 placeholder 示例文案
(TARO RAKUTEN 只是示例用户名,不是稳定标识)
- 订单号正则修正 实体导致的分隔符匹配失败(2026-08-13 真实下单验证)
- login_runner._is_logged_in 改用与 AuthSession 一致的 __INITIAL_STATE__ 判据,
修掉此前误判「已登录」导致保存无效 storage_state 的问题
- 新增 CheckoutBlockedError:站点风控拦截(session upgrade/3DS)转 needs_human,
不当普通失败重试
- account.yaml 支持 phone / payment.credit-card 字段
- data/evidence/(真实 PII)加入 .gitignore
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
2026-08-13 22:36:49 +08:00 |
|
 q792602257andClaude Opus 4.6
|
55c01ae4f5
|
加 /api/cart/* 接口;加购链路完全归 trading
trading 新增 4 条购物车接口(POST /api/cart/{add,status,clear,remove}),
全部 Bearer 鉴权、走 SiteInteractor(Playwright + storage_state)。同步把
SiteInteractor 从 gateway URL 解耦——lifespan 总是构造与启停,container
字段 worker_site → site,加 asyncio.Lock 让 HTTP 与 worker 共用同一把锁
(同账号串行硬约束)。clear/remove 用 UI 点击 button[aria-label="削除"],
探针回报这是稳定 selector;真账号实测前先用此路径。
抽 ichiba 加购字段解析到 app/shared/purchase_contract.py(常量 +
inventory_flag_for + basket_domain_of + base_form_fields),原本 scraping
与 trading 重复实现同一段 __INITIAL_STATE__.purchase 解析。进一步发现
README 写的「purchase 块是两服务契约」实际未落地——trading 必须 Playwright
开页(httpx 被 TLS 指纹拦死),本地抽比再调 /api/item_detail 更快更新鲜。
删除 scraping 端 PurchaseInfo/PurchaseOption/PurchaseOptionValue 模型、
各站 _purchase_info 函数、tests/test_purchase.py。ItemDetailData 保留
purchase_condition / is_sold_out / purchase_unit / sku 等商品状态字段。
README「加购与下单」段重写。
328 测试全绿(含架构测试守住三方互不 import)。
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
2026-07-28 17:18:00 +08:00 |
|
q792602257
|
6247d68fb5
|
账号
|
2026-07-27 21:29:36 +08:00 |
|
 q792602257andClaude Opus 4.6
|
18cf7ae079
|
实现 site_interact 加购与购物车校验(Playwright 通道)
探针实测发现 httpx 因 TLS/HTTP2 指纹被 Rakuten 拒认(cookie 有效但
站点不识别 session),site_interact 改为全程 Playwright + storage_state。
add_to_cart 与 verify_cart 已实测可用:从商品页 __INITIAL_STATE__ 抽
purchase 块、调用方补 variant_id 与 choice、POST basketDomain、用 cart
count JSONP API 与 SPA 渲染后的 cart 页校验。enter_checkout 及之后仍
NotImplementedError:Rakuten 对 checkout 要求 session upgrade(重输密码),
是自动 checkout 的硬墙,未实测过墙方案前留接口缝。
SiteInteractor 类持有 Playwright BrowserContext,由 WorkerRunner 实例
持有、container/main 接线、lifespan 管理生命周期。测试侧 23 个用例,
全套件 333 passed,架构边界守住。
附 5 个探针脚本(scripts/probe_*.py),记录实测路径与响应结构。
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
2026-07-27 17:59:30 +08:00 |
|
 q792602257andClaude Opus 4.6
|
07107094a7
|
实现下单任务网关与本地 worker
按 docs/order-gateway.md 落地:第三个部署单元 app.gateway(:31109)承担任务队列
+ 状态镜像;本地 worker 在 app.trading.worker 内,按 RAKUTEN_ORDER_GATEWAY_URL
决定是否启动。规格 §5 最关键约束已守:租约过期绝不自动重投,恢复只能 reclaim,
worker 收到 lease_count>1 时先核对站点订单。
站点交互(加购/下单/付款/订单列表反查)按规格 §10 留接口缝,site_interact.py
全部 NotImplementedError,verify.py 恒返回 unknown——等真实账号实测后再填,
不写猜测的提交逻辑。
310 个测试全绿,覆盖规格 §9 验收清单 12 条;架构测试守住三方互不 import。
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
2026-07-27 16:25:20 +08:00 |
|