 q792602257andClaude Opus 5
|
b577d3ac8d
|
fix(trading): 必填选项自动填值跳过「選択してください」占位项,并把选项开放给接口
trading 自动填 choice 时取 values[0],而必填 select 的 values[0] 恒为 id=0 的
「選択してください」——等于把「请选择」当答案提交。4 份真实样本一致(真值从
id=200 起)。同时 /api/item_detail 完全不返回 options,调用方即使想显式指定
choice 也无从知道合法取值。
- purchase_contract.py:新增 ItemOption / ItemOptionValue 与 parse_options /
auto_choice_for / format_choice。占位判定以结构为主(value_id == 0),日文
文案仅作兜底。放 shared 是因为「接口声明的合法取值」与「下单实际提交的值」
必须同源,否则两边各判一次迟早再次分叉
- item.py / scrape.py:ItemDetailData 增 options、has_required_options、
unfillable_required_options;只解析一次,两个派生结果都取自同一份结果
- site_interact.py:auto_choice_for 取第一个非占位候选;必填项填不出值时
报错点名是哪些选项,让调用方知道该在 intent.choice 里补什么
- auto_choice_for 只自动填必填项:非必填项要不要选是业务决定,不是我们该替
调用方做的选择
- README / docs:补 options[] → intent.choice、variants[] → intent.variant_id
的对照,修掉 order-gateway 示例里已不存在的 "options": {} 字段
真账号验证(scripts/probe_option_choice.py,仅加购不结算不支付):两个商品
提交 確認した / 了解致しました。均被站点接受,购物车 count=2,跑完清空恢复
原状。探针刻意走生产的 add_to_cart_payload 并从其日志截获实际 payload——
probe_purchase_block_v2.py 自己抄了一遍字段构造,与生产代码同错,正是这个
bug 当初藏住的原因。
未覆盖:这两家店铺本身不校验该选项(旧的占位值当年也被收下),所以只证明新值
走得通、语义上才是真答案,证明不了旧值会被拒;必填自由文本项(
unfillable_required_options)无真实样本,仅离线测试覆盖。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
2026-08-28 16:07:33 +08:00 |
|
 q792602257andClaude Opus 5
|
8381896eeb
|
feat(observability): 失败响应与网关信封进链路,手工埋点尊重 suppressed
失败此前在 trace 里近乎不可见:异常处理器把异常吃掉换成信封响应,自动
instrumentation 只看到一个 HTTP 状态码,而 AppError 默认 400、信封里
success=false,跟正常返回分不出来。
- api.py:四个异常处理器(对外失败的唯一出口)各记一次 span;兜底处理器额外
record_error——对外只回一句无信息量的错误文案,异常类型与栈只在本地日志里
- telemetry.py:新增 record_envelope / record_parse_failure /
span_unless_suppressed;record_error 补 error.message / retryable /
status_code;snapshot 支持 extra 带上「这份 HTML 是哪来的」
- worker/client.py:_request 自建 span,活到解信封之后。httpx 那个 CLIENT span
在 request() 返回时就结束,此时信封还没解——success=false code=6002(租约
无效)在它看来是完成的 200 请求
- span_unless_suppressed:suppress_instrumentation 只被 instrumentation 库尊重,
手工 span 不看它,lease 空转长轮询会从这个口子把孤立 trace 放回来
- 两个 scraping client 的解析失败分支收拢到 record_parse_failure;shop_items
显式标 stage=delegate 且不落快照(它自己不抓页面,按 html 推断只会得出
「fetch 失败」的错误结论)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
2026-08-28 16:07:00 +08:00 |
|
 q792602257andClaude Opus 5
|
4cc30bc058
|
feat(observability): /health 不再产生 server span
容器 HEALTHCHECK 每 30 秒探一次、上游也在轮询,这些请求各自是一条孤立
trace,量大且没有信息量——和 worker 空转长轮询同一个问题,把观测后台刷满
的正是它们。
instrument_app 传 excluded_urls,由新增的 RAKUTEN_OTEL_EXCLUDED_URLS 控制
(默认 /health$)。按 search 匹配完整 URL,锚点保证不误伤 /api/health-*;
留空时传 None,回落到 OTel 自己的 OTEL_PYTHON_FASTAPI_EXCLUDED_URLS。
三个服务共用 instrument_app,因此抓取/交易/网关一并生效。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
2026-08-28 15:24:52 +08:00 |
|
 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 |
|
q792602257
|
bba45d7f7c
|
fix(tests): isolate trading API from Playwright
|
2026-08-20 16:51:24 +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
|
48c6f2a28f
|
feat(gateway): 下单任务支持 callback_url 终结类事件异步通知
- POST /api/orders 新增可选 callback_url(仅 http/https,其余 422)
- 仅终结类事件各通知一次:terminal report 推入终态(succeeded/failed/
needs_human)、租约过期被 sweep 置 stale;中间态与终结后的监控上报不通知
- 幂等重发不更新既有任务的回调地址;投递 best-effort 单次尝试,失败只记日志
- CallbackNotifier 发后不管(create_task + 在途任务强引用),关停等在途发完;
生产路径按回调地址逐次构造客户端,满足统一出站代理策略(test_proxy.py)
- tasks 表加 callback_url 列,GatewayDB.start() 内置迁移兼容既有库
- 新增 RAKUTEN_CALLBACK_TIMEOUT_SECONDS(默认 10);文档补 §4.8;
openapi.json 重新导出(gitignore 未跟踪);新增 17 条测试,全量 492 通过
|
2026-08-17 01:05:37 +08:00 |
|
 q792602257andClaude Opus 5
|
df93bc1e63
|
fix(trading): 每单开跑前先清理购物车,杜绝上一单失败残留被新单一起买走
上一单若在提交(submit_order)之前失败(如 enter_checkout 被 session upgrade
拦截、金额守卫拦下),残留商品会留在购物车里;下一单 add_to_cart 把新商品叠
加在旧商品上,结算时会把上一单的一起买走(已实测踩到过)。
在 execute() 加购前先 clear_cart,从空车起步,保证「一单 = 只买本单商品」——
不依赖上一次失败路径是否完整执行清理,进程中途崩溃残留也没人清也能兜住。
清理后 cart_count 仍未清空(含 count API 失败返回 -1)按闸门语义转 needs_human,
绝不带残留往下加购。清理本身是本机侧卫生操作,不走证据与上报。
补 3 个用例:clear 先于 add、清理后非空/未知 → 转 needs_human 且不加购;
并为走 execute 的既有测试补 clear_cart 桩。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
2026-08-17 00:33:42 +08:00 |
|
 q792602257andClaude Opus 5
|
f6c3976c0a
|
feat(gateway): 定时下派通道——周期性盘点账号订单并回写网关编目
新增 §12 定时下派通道:网关自己定期派 order_list / order_detail 查询,把账号
真实订单沉淀进新表 account_orders(回写网关),上游可直接查 GET /api/account/orders
拿到账号里实际有哪些订单,不必自己记 order_number。
- collector.py: OrderDiscoveryCollector 常驻后台任务(与 sweep 并列)。每隔
account_discovery_interval_seconds 派 order_list,收割结果后把「编目里没有的
订单」upsert 进编目,再逐笔派 order_detail 沉淀 delivery_status/order_state。
状态无痕:不新增编排跟踪表,仅内存 _pending + _detail_dispatched_this_day;
detail query_id 按天分段(discover-detail-<order>-<日期>),同天幂等防重复派。
边界:collector 只消费 worker 结果的规范化字段,绝不解析 raw/raw_pages。
- db.py: account_orders 表 + AccountOrderRow + upsert/single/list/stale 访问;
upsert 以 order_number 为主键,重复采集只刷新,列表重扫不抹详情节点。
- 路由: GET /api/account/orders(列编目)、GET /api/account/orders/{n}(6005)、
POST /api/account/discovery/trigger(手动立即派一轮 list)。
- config: RAKUTEN_ACCOUNT_DISCOVERY_ENABLED / _INTERVAL_SECONDS / _MAX_PAGES /
RAKUTEN_ACCOUNT_DETAIL_REFRESH_SECONDS。
- 既有网关测试夹具统一关 discovery(会启动即派扫描污染查询队列语义),
新表测试在 tests/test_gateway_discovery.py(13 用例,真实 QueryQueue+DB 装配)。
472 tests passed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
2026-08-16 23:35:51 +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
|
b01c9659d1
|
fix: 避免无 cookie 时重复预热会话
|
2026-08-16 21:29:51 +08:00 |
|
q792602257
|
fabca9510d
|
feat: 为出站请求统一接入 HTTP 代理
|
2026-08-14 16:08:39 +08:00 |
|
 q792602257andClaude Opus 5
|
63c41b61e7
|
自动登录接口 + 有状态端容器化部署 + 三服务合并 openapi 导出
自动登录(此前只能人工跑 scripts/login.py 再 /api/auth/reload):
- 新增 POST /api/auth/login:动作顺序与下单前的 require_logged_in 一致(探测 →
未登录则按 account.yaml 登一次 → 再探测),已登录直接跳过不白起浏览器。
刻意**不抛 5001**:失败以 logged_in=false + 各站 detail 正常返回,调用方自己
决定是人工接管还是换账号。
- 新增 RAKUTEN_AUTO_LOGIN_ON_START(默认 false):启动即准备登录态,为容器部署
而存在(镜像里没有落盘的 storage_state)。做成后台任务而非启动阻塞——登录最长
等 relogin_timeout_seconds(默认 300s,撞验证码时在等人工),阻塞会让 /health
在这段时间里连端口都不通;关服务时 cancel 掉在途的那次。
- 自动登录不绕过站点校验:凭据是用户自己配在 account.yaml 里的,代填进站点自己的
登录表单,撞 reCAPTCHA / 设备验证会停在有头浏览器等人工,等不到就超时失败。
容器化部署(新增 Dockerfile.trading + docker-compose.yml):
- 有状态端单独出镜像不是为了整洁:下单/结算必须用**有头** Chromium(headless 会让
结算 SPA 失灵),镜像要带 Xvfb + 日文字体 + 给人工接管用的可选 x11vnc,抓取镜像
没有这些。网关复用同一镜像只换 command。
- Jenkinsfile 一条流水线产出两个镜像,BUILD_SCRAPING / BUILD_TRADING 两个开关控制。
- .dockerignore 补上 account.yaml / .auth/ / .browser-data/ / data/:明文密码+卡号、
可直接冒充账号的 cookie、带登录态的浏览器 profile、含真实 PII 的证据快照,都不该
进镜像也不该进 build context,运行时一律走挂载。
- .env.example 里 RAKUTEN_AUTO_LOGIN_ON_START 刻意留成注释:compose 的变量插值与
env_file 读的是同一个 ./.env,这里写成显式值会让 compose 的 `${...:-true}` 失效,
按 compose 文件头「cp .env.example .env」走反而不会自动登录。
openapi 导出(scripts/export_openapi.py):三服务合并成一份可直接导入 Apifox /
Postman 的文档,每条接口带 operation 级 servers(不必手动切端口)。鉴权标注是遍历
FastAPI 依赖树认出真的挂了 require_bearer_token 的接口,不按路径猜。
openapi.json 本身仍是 gitignore 的本地生成物,因此 tests/test_openapi_export.py
只在内存里校验合并逻辑(三服务覆盖、operation 级 servers、除 /health 外全部标鉴权、
operationId 唯一、$ref 可解析),不断言「文件内容 == 当前导出结果」——CI 的全新
clone 里没有这个文件,那种断言必然失败。代价是「改了接口忘了重新导出」没有自动
兜底,得手动跑 --check,已在 README 里点明。
398 测试全绿;另单独验证过缺 openapi.json 时该文件 5 个用例仍通过(CI 场景)。
compose 的变量插值行为只按文档核对,本机没有 docker 未能实测。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
2026-08-14 14:27:38 +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 |
|
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 |
|
 q792602257andClaude Opus 4.6
|
65e3ed31f8
|
交易服务收敛为仅 rakuten:移除 rakuma 登录态与下单入口
trading 不再管理 ラクマ 的购物车购买、支付与订单监控。删除 auth_site /
auth_session / login_runner / models.AuthSite 中的 rakuma 分支与常量、
account.yaml.example 的 rakuma 段,并清理相关测试。scraping 侧的
ラクマ 抓取 API(/api/rakuma/*)保留不动。
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
2026-07-28 11:50:07 +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 |
|
 q792602257andClaude Opus 4.6
|
2b7db521c4
|
接入 OTel 链路追踪 + 镜像默认启用
抓取-解析链路原本只有日志,出现"抓回内容但解析不出预期字段"时定位慢。
接入 OpenTelemetry traces(FastAPI/httpx 自动 + 手写 fetch/parse span),
解析失败时把页面 HTML 作为 span event 上报,便于事后复现。
Dockerfile 默认开启,镜像一启动即导出到自建 OTLP endpoint。
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
2026-07-27 15:43:19 +08:00 |
|
 q792602257andClaude Opus 5
|
104d7fef6b
|
拆分抓取与交易服务
把需要账号登录态的链路从抓取服务里拆出成独立进程。分界线不是「要不要登录」,
而是抓取无状态、幂等、可多开实例,而交易的写操作不可逆、登录态全局唯一、
订单监控是常驻轮询——同进程时抓取一扩容就会复制出 N 份登录态与 N 个轮询,
同一账号会被并发操作。
- app/shared:配置、错误码、日志、ApiResponse 信封 + Bearer 鉴权 + 异常处理器、
导航请求头构造器
- app/scraping:站点常量、会话、解析器与 10 个抓取接口,:31107,可多开
- app/trading:登录态查询/重载与健康检查,:31108,只能单实例
- 依赖方向锁为 scraping→shared、trading→shared,两侧互不 import;
tests/test_architecture.py 用 AST 检查 import 并校验两个 app 的路径不串
- 登录态 UA 在 trading 独立持有:与抓取 UA 值相同但变更理由不同,抓取 UA 为绕
反爬可随时调整,登录 UA 一改可能触发设备校验使已落盘 cookie 失效
- scripts/login.py 与 AuthSession 共用 auth_site.PROFILES 与 is_logged_in,判据只写一遍
- 同一镜像两个启动命令,交易容器覆盖 command 并设 RAKUTEN_HEALTH_PORT
同时带上此前未提交的 ラクマ 分类接口与登录态基础设施。
验证:239 个离线用例全绿;两个入口真实启动,/health 与鉴权正常。
未验证:真实探测登录态(当前开发机无外网,对站点的连接全部超时)。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
2026-07-27 15:05:01 +08:00 |
|
q792602257
|
3527975794
|
Init
|
2026-07-27 10:34:53 +08:00 |
|