修真实结算流程三处硬伤: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>
This commit is contained in:
@@ -141,8 +141,13 @@ _CHECKOUT_BUTTON_SELECTOR = 'button[aria-label="購入手続き"]'
|
|||||||
# 点击结算按钮后落到 session upgrade 密码页的判据:URL 里出现这些片段之一
|
# 点击结算按钮后落到 session upgrade 密码页的判据:URL 里出现这些片段之一
|
||||||
_SESSION_UPGRADE_URL_MARKERS = ("session/upgrade", "sign_in/password")
|
_SESSION_UPGRADE_URL_MARKERS = ("session/upgrade", "sign_in/password")
|
||||||
|
|
||||||
# session upgrade 密码页的「次へ」提交按钮没有 type=submit,只能按文案匹配
|
# session upgrade 密码页/电话补录页的「次へ」提交按钮:2026-08-14 非无头浏览器
|
||||||
_SESSION_UPGRADE_SUBMIT_SELECTOR = 'button:has-text("次へ")'
|
# 实测抓到真实 HTML 才发现,此前的 button:has-text("次へ") 是猜的、根本不存在
|
||||||
|
# 匹配的 <button> 标签——真实控件是
|
||||||
|
# <div role="button" class="... h4k5-e2e-button__submit ..." id="cta0XX">次へ</div>,
|
||||||
|
# 不是原生 button。h4k5-e2e-button__submit 这个类名看着就是站点自己留的 e2e
|
||||||
|
# 测试钩子(class 名里直接带 "e2e"),比按文案或按自动生成的 id="cta0XX" 猜更稳
|
||||||
|
_SESSION_UPGRADE_SUBMIT_SELECTOR = '.h4k5-e2e-button__submit'
|
||||||
|
|
||||||
# 提交密码后等待页面跳转的轮询:每次 3s,共 _SESSION_UPGRADE_POLL_ROUNDS 次
|
# 提交密码后等待页面跳转的轮询:每次 3s,共 _SESSION_UPGRADE_POLL_ROUNDS 次
|
||||||
# (合计 ~30s)。前 4 次真实尝试里从未在这个窗口内看到跳转,超时即认定被拦截。
|
# (合计 ~30s)。前 4 次真实尝试里从未在这个窗口内看到跳转,超时即认定被拦截。
|
||||||
@@ -192,8 +197,12 @@ _MASKED_CARD_LAST4_PATTERN = re.compile(r"[•●\*]{2,}\s*(\d{3,4})")
|
|||||||
# 猜的(见 _fill_or_select_in_frame 的 "input, select" 兜底)。
|
# 猜的(见 _fill_or_select_in_frame 的 "input, select" 兜底)。
|
||||||
# - 只有「名義人」(持卡人姓名)是主文档里的普通 <input>,但没有 name/autocomplete
|
# - 只有「名義人」(持卡人姓名)是主文档里的普通 <input>,但没有 name/autocomplete
|
||||||
# 等属性,唯一稳定线索是 placeholder="TARO RAKUTEN",故用它做 selector。
|
# 等属性,唯一稳定线索是 placeholder="TARO RAKUTEN",故用它做 selector。
|
||||||
# - 代填后仍然**不会自动点「追加する」提交**——原则不变:注册新卡是有实际后果的
|
# - **2026-08-14 决定放开自动提交**(此前默认代填后停下等人工点「追加する」,
|
||||||
# 动作,iframe 内部字段是否真填对了也没法从外层验证,交给人工核对后自己点。
|
# 2026-08-14 用户明确要求放开):填完三个 iframe 字段 + 名義人后自动点「追加する」
|
||||||
|
# (2026-08-13 人工在真实页面上点过一次这个按钮成功过,文案不是猜的),提交后
|
||||||
|
# 必须在页面回显里看到掩码卡号+有効期限才放行,看不到就当失败转人工——
|
||||||
|
# iframe 内部字段选择器本身仍未经真实 HTML 验证,一旦这步之前填错,大概率会
|
||||||
|
# 直接体现在这条確认信号上,不会被静默当成功。
|
||||||
# - 另外发现:本次注册的卡「追加する」成功后当场展示为已选中的已保存卡(含掩码
|
# - 另外发现:本次注册的卡「追加する」成功后当场展示为已选中的已保存卡(含掩码
|
||||||
# 卡号+有効期限),但换一个全新浏览器 context(沿用同一份 storage_state 登录态)
|
# 卡号+有効期限),但换一个全新浏览器 context(沿用同一份 storage_state 登录态)
|
||||||
# 重新走到支付方式页时,这张卡**没有**出现在已保存卡列表里,需要重新走一遍新增
|
# 重新走到支付方式页时,这张卡**没有**出现在已保存卡列表里,需要重新走一遍新增
|
||||||
@@ -201,6 +210,9 @@ _MASKED_CARD_LAST4_PATTERN = re.compile(r"[•●\*]{2,}\s*(\d{3,4})")
|
|||||||
# 没有进一步验证,_select_payment_method 不应假设新卡注册跨 SiteInteractor
|
# 没有进一步验证,_select_payment_method 不应假设新卡注册跨 SiteInteractor
|
||||||
# context 重建后仍然可用。
|
# context 重建后仍然可用。
|
||||||
_ADD_CARD_LINK_TEXT = "新しいカードを追加する"
|
_ADD_CARD_LINK_TEXT = "新しいカードを追加する"
|
||||||
|
# 新卡表单的提交按钮:2026-08-13 人工在真实页面上点过一次确认过文案(见
|
||||||
|
# data/evidence/checkout-live-20260813/i-final-state.html 一带),不是猜的
|
||||||
|
_ADD_CARD_SUBMIT_TEXT = "追加する"
|
||||||
_CARD_NUMBER_MOUNT_SELECTOR = "#checkout-card-number-mount iframe"
|
_CARD_NUMBER_MOUNT_SELECTOR = "#checkout-card-number-mount iframe"
|
||||||
_CARD_MONTH_MOUNT_SELECTOR = "#checkout-card-expiration-month-mount iframe"
|
_CARD_MONTH_MOUNT_SELECTOR = "#checkout-card-expiration-month-mount iframe"
|
||||||
_CARD_YEAR_MOUNT_SELECTOR = "#checkout-card-expiration-year-mount iframe"
|
_CARD_YEAR_MOUNT_SELECTOR = "#checkout-card-expiration-year-mount iframe"
|
||||||
@@ -517,7 +529,11 @@ class SiteInteractor:
|
|||||||
|
|
||||||
self._playwright = await async_playwright().start()
|
self._playwright = await async_playwright().start()
|
||||||
self._browser = await self._playwright.chromium.launch(
|
self._browser = await self._playwright.chromium.launch(
|
||||||
headless=True,
|
# 2026-08-14 实测确认:headless=True 会让站点的购物车/结算 SPA 表现
|
||||||
|
# 异常(购入手続き点了不跳转、shopUrlList 渲染不出来),换 headless=False
|
||||||
|
# 后行为与真实下单一致(能正常触发 session upgrade)——所有会改动站点
|
||||||
|
# 状态的操作(加购/结算/支付)都必须走非无头浏览器
|
||||||
|
headless=False,
|
||||||
channel=self._settings.browser_channel or None,
|
channel=self._settings.browser_channel or None,
|
||||||
args=["--no-first-run", "--disable-blink-features=AutomationControlled"],
|
args=["--no-first-run", "--disable-blink-features=AutomationControlled"],
|
||||||
)
|
)
|
||||||
@@ -1129,6 +1145,14 @@ class SiteInteractor:
|
|||||||
f"当前 url={page.url}(可能出现了未见过的新步骤)"
|
f"当前 url={page.url}(可能出现了未见过的新步骤)"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if page.url.startswith(_CART_PAGE):
|
||||||
|
raise OrderOperationError(
|
||||||
|
"点「購入手続き」后没有跳转出购物车页(未触发 session upgrade/"
|
||||||
|
"电话补录/地址确认/支付方式任一已知中间步骤),可能是点击被站点"
|
||||||
|
f"忽略或按钮短暂失效弹回;当前仍在 url={page.url},不能当成已落地"
|
||||||
|
"确认页处理"
|
||||||
|
)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"enter_checkout 落地:task_id=%s url=%s", task.task_id, page.url
|
"enter_checkout 落地:task_id=%s url=%s", task.task_id, page.url
|
||||||
)
|
)
|
||||||
@@ -1255,9 +1279,16 @@ class SiteInteractor:
|
|||||||
async def _confirm_default_address(self, page, *, task_id: str) -> None:
|
async def _confirm_default_address(self, page, *, task_id: str) -> None:
|
||||||
"""收货地址确认页:点击默认地址卡片继续
|
"""收货地址确认页:点击默认地址卡片继续
|
||||||
|
|
||||||
**未经真实 HTML 验证**:2026-08-11 人工走一遍时观察到默认地址卡片本身可点击
|
**未经真实 HTML 验证,且目前账号的真实流程从未触发过这一步**:2026-08-11
|
||||||
(点击后直接跳到支付方式页,没有单独的「次へ」),但没拿到这页的真实 HTML,
|
人工走一遍时观察到默认地址卡片本身可点击(点击后直接跳到支付方式页,没有
|
||||||
selector 是根据可见文案「デフォルトお届け先」反推的推测。
|
单独的「次へ」),但没拿到这页的真实 HTML,selector 是根据可见文案
|
||||||
|
「デフォルトお届け先」反推的推测。2026-08-13 真实下单成功一次、2026-08-14
|
||||||
|
非无头浏览器真实探测三次(均已修复 headless 导致 SPA 异常的问题、修好了
|
||||||
|
session upgrade 提交按钮选择器),URL 里都没出现过 `/ship`——当前这个账号
|
||||||
|
(只有一个已保存地址)会直接从 session upgrade 跳到 order-confirmation,
|
||||||
|
跳过地址确认这一步。也就是说这个方法目前是真实死代码:只有账号出现多个
|
||||||
|
地址或没有默认地址时才可能触发,selector 依旧是未经验证的推测,遇到时会
|
||||||
|
直接抛错转人工,不会静默用错误的 selector 蒙混过关。
|
||||||
账号没有默认地址时(新地址、需要人工选/填)直接抛错转人工,不猜表单结构。
|
账号没有默认地址时(新地址、需要人工选/填)直接抛错转人工,不猜表单结构。
|
||||||
"""
|
"""
|
||||||
html = await page.content()
|
html = await page.content()
|
||||||
@@ -1339,11 +1370,7 @@ class SiteInteractor:
|
|||||||
"且 account.yaml 未配置 payment.credit-card,无法代填新卡,需要人工处理"
|
"且 account.yaml 未配置 payment.credit-card,无法代填新卡,需要人工处理"
|
||||||
)
|
)
|
||||||
await self._fill_new_card_form(page, card=configured_card, task_id=task_id)
|
await self._fill_new_card_form(page, card=configured_card, task_id=task_id)
|
||||||
raise CheckoutBlockedError(
|
await self._submit_new_card_form(page, task_id=task_id)
|
||||||
"已按 account.yaml 的卡信息代填新卡表单,但未自动提交/登录该卡"
|
|
||||||
"(表单从未经真实页面验证,且注册新卡是有实际后果的动作),"
|
|
||||||
"需要人工在浏览器里核对填写内容后自己点提交"
|
|
||||||
)
|
|
||||||
|
|
||||||
next_btn = page.locator(_PAYMENT_NEXT_BUTTON_SELECTOR).first
|
next_btn = page.locator(_PAYMENT_NEXT_BUTTON_SELECTOR).first
|
||||||
try:
|
try:
|
||||||
@@ -1356,7 +1383,7 @@ class SiteInteractor:
|
|||||||
logger.info("支付方式选择完成:task_id=%s url=%s", task_id, page.url)
|
logger.info("支付方式选择完成:task_id=%s url=%s", task_id, page.url)
|
||||||
|
|
||||||
async def _fill_new_card_form(self, page, *, card: "CreditCard", task_id: str) -> None:
|
async def _fill_new_card_form(self, page, *, card: "CreditCard", task_id: str) -> None:
|
||||||
"""代填新卡表单(卡号/有效期/持卡人姓名),不点提交
|
"""代填新卡表单(卡号/有效期/持卡人姓名);提交交给 _submit_new_card_form
|
||||||
|
|
||||||
**2026-08-13 真实走过一次这个表单**(人工在浏览器窗口里用 OS 级
|
**2026-08-13 真实走过一次这个表单**(人工在浏览器窗口里用 OS 级
|
||||||
SendKeys 代填,不是本方法这段 Playwright 代码本身——见
|
SendKeys 代填,不是本方法这段 Playwright 代码本身——见
|
||||||
@@ -1417,7 +1444,37 @@ class SiteInteractor:
|
|||||||
await _fill_in_vault_iframe(_CARD_YEAR_MOUNT_SELECTOR, year, "有効期限(年)")
|
await _fill_in_vault_iframe(_CARD_YEAR_MOUNT_SELECTOR, year, "有効期限(年)")
|
||||||
await _fill_in_main_doc(card.name, "名義人")
|
await _fill_in_main_doc(card.name, "名義人")
|
||||||
|
|
||||||
logger.info("新卡表单代填完成(未提交):task_id=%s url=%s", task_id, page.url)
|
logger.info("新卡表单代填完成:task_id=%s url=%s", task_id, page.url)
|
||||||
|
|
||||||
|
async def _submit_new_card_form(self, page, *, task_id: str) -> None:
|
||||||
|
"""点「追加する」提交新卡表单,确认站点回显该卡已被选中后才放行
|
||||||
|
|
||||||
|
2026-08-14 决定放开自动提交(此前默认填完停下等人工点,见本模块顶部常量
|
||||||
|
注释里的决策记录):按钮文案「追加する」是 2026-08-13 人工在真实页面上
|
||||||
|
点过一次确认过的,不是猜的。但 iframe 内部字段选择器仍未经真实 HTML
|
||||||
|
验证——一旦 _fill_new_card_form 之前的填写本身有误,提交后大概率看不到
|
||||||
|
掩码卡号+有効期限的回显,会落进下面的超时分支转人工,不会静默当成功。
|
||||||
|
"""
|
||||||
|
submit = page.locator(f'button:has-text("{_ADD_CARD_SUBMIT_TEXT}")').first
|
||||||
|
try:
|
||||||
|
await submit.click(timeout=10_000)
|
||||||
|
except Exception as exc:
|
||||||
|
raise CheckoutBlockedError(
|
||||||
|
f"新卡表单未找到或点击「{_ADD_CARD_SUBMIT_TEXT}」提交按钮失败:"
|
||||||
|
f"{type(exc).__name__}: {exc}"
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
for _ in range(_SESSION_UPGRADE_POLL_ROUNDS):
|
||||||
|
await page.wait_for_timeout(_SESSION_UPGRADE_POLL_INTERVAL_MS)
|
||||||
|
html = await page.content()
|
||||||
|
if _SAVED_CARD_SIGNAL_PATTERN.search(html):
|
||||||
|
logger.info("新卡注册并选中成功:task_id=%s url=%s", task_id, page.url)
|
||||||
|
return
|
||||||
|
|
||||||
|
raise CheckoutBlockedError(
|
||||||
|
"新卡表单提交后未看到该卡被选中的回显(掩码卡号+有効期限),"
|
||||||
|
"无法确认是否注册成功,需要人工核对"
|
||||||
|
)
|
||||||
|
|
||||||
async def _discard_checkout_page(self, task_id: str) -> None:
|
async def _discard_checkout_page(self, task_id: str) -> None:
|
||||||
"""关闭并丢弃 task_id 对应的已留存确认页 Page
|
"""关闭并丢弃 task_id 对应的已留存确认页 Page
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
"""收货地址确认页真实 HTML 探针
|
||||||
|
|
||||||
|
走 SiteInteractor 的真实生产代码路径(add_to_cart → verify_cart → enter_checkout),
|
||||||
|
在 _confirm_default_address 被调用前把该页 HTML 落盘,验证/纠正选择器是否与真实页面
|
||||||
|
一致。**不调用 submit_order / pay**——enter_checkout 落地到确认页就返回,这里直接
|
||||||
|
丢弃该页,不产生下单动作。
|
||||||
|
|
||||||
|
用法:
|
||||||
|
.venv/Scripts/python.exe scripts/probe_address_confirm.py
|
||||||
|
.venv/Scripts/python.exe scripts/probe_address_confirm.py --item-url https://item.rakuten.co.jp/...
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||||
|
|
||||||
|
from app.shared.config import get_settings # noqa: E402
|
||||||
|
from app.trading.services.auth_session import AuthSession # noqa: E402
|
||||||
|
from app.trading.worker.models import LeaseTask # noqa: E402
|
||||||
|
from app.trading.worker.site_interact import SiteInteractor # noqa: E402
|
||||||
|
|
||||||
|
PROBE_DIR = Path(__file__).resolve().parent.parent / ".probe" / "address_confirm"
|
||||||
|
PROBE_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# fafachai/10000033 已下架/不可购买;改用 2026-08-13 真实下单时用过的商品
|
||||||
|
# (data/evidence/checkout-live-20260813/i00-cart-initial.html 里的 itemUrl)
|
||||||
|
DEFAULT_ITEM_URL = "https://item.rakuten.co.jp/moccasin/ds001iwrgesaaa2/"
|
||||||
|
|
||||||
|
|
||||||
|
async def _start_visible(site: SiteInteractor, settings) -> None:
|
||||||
|
"""复制 SiteInteractor.start() 但 headless=False,供本地诊断肉眼看浏览器
|
||||||
|
|
||||||
|
不改动生产代码(生产 start() 硬编码 headless=True),直接绕过它、把结果
|
||||||
|
塞进 site 的私有属性——只在这个一次性诊断脚本里这么干。
|
||||||
|
"""
|
||||||
|
from playwright.async_api import async_playwright
|
||||||
|
|
||||||
|
from app.trading.core import auth_site
|
||||||
|
|
||||||
|
state_path = settings.auth_state_path / auth_site.profile("rakuten").state_filename
|
||||||
|
storage_state = str(state_path) if state_path.exists() else None
|
||||||
|
|
||||||
|
site._playwright = await async_playwright().start()
|
||||||
|
site._browser = await site._playwright.chromium.launch(
|
||||||
|
headless=False,
|
||||||
|
channel=settings.browser_channel or None,
|
||||||
|
args=["--no-first-run", "--disable-blink-features=AutomationControlled"],
|
||||||
|
)
|
||||||
|
site._context = await site._browser.new_context(
|
||||||
|
storage_state=storage_state,
|
||||||
|
user_agent=auth_site.RAKUTEN_USER_AGENT,
|
||||||
|
locale="ja-JP",
|
||||||
|
timezone_id="Asia/Tokyo",
|
||||||
|
viewport={"width": 390, "height": 844},
|
||||||
|
is_mobile=True,
|
||||||
|
has_touch=True,
|
||||||
|
)
|
||||||
|
print(f"[visible] 浏览器已启动(headless=False),storage_state={storage_state or '(none)'}")
|
||||||
|
|
||||||
|
|
||||||
|
async def run(item_url: str) -> int:
|
||||||
|
settings = get_settings()
|
||||||
|
auth = AuthSession(settings)
|
||||||
|
await auth.start()
|
||||||
|
site = SiteInteractor(auth_session=auth, settings=settings)
|
||||||
|
await _start_visible(site, settings)
|
||||||
|
|
||||||
|
captured: dict[str, str] = {}
|
||||||
|
orig_confirm = site._confirm_default_address
|
||||||
|
|
||||||
|
async def spy_confirm(page, *, task_id: str) -> None:
|
||||||
|
html = await page.content()
|
||||||
|
captured["html"] = html
|
||||||
|
captured["url"] = page.url
|
||||||
|
path = PROBE_DIR / "address-confirm-page.html"
|
||||||
|
path.write_text(html, encoding="utf-8")
|
||||||
|
print(f"[捕获] 收货地址确认页 -> {path} ({len(html)} bytes), url={page.url}")
|
||||||
|
await orig_confirm(page, task_id=task_id)
|
||||||
|
|
||||||
|
site._confirm_default_address = spy_confirm
|
||||||
|
|
||||||
|
orig_session_upgrade = site._complete_session_upgrade
|
||||||
|
|
||||||
|
async def spy_session_upgrade(page, *, task_id: str) -> None:
|
||||||
|
html = await page.content()
|
||||||
|
path = PROBE_DIR / "session-upgrade-before.html"
|
||||||
|
path.write_text(html, encoding="utf-8")
|
||||||
|
print(f"[捕获] session upgrade 页(点提交前)-> {path} ({len(html)} bytes), url={page.url}")
|
||||||
|
try:
|
||||||
|
await orig_session_upgrade(page, task_id=task_id)
|
||||||
|
finally:
|
||||||
|
html_after = await page.content()
|
||||||
|
(PROBE_DIR / "session-upgrade-after.html").write_text(html_after, encoding="utf-8")
|
||||||
|
|
||||||
|
site._complete_session_upgrade = spy_session_upgrade
|
||||||
|
|
||||||
|
task = LeaseTask(
|
||||||
|
task_id="probe-addr-1",
|
||||||
|
site="rakuten",
|
||||||
|
intent={"item_url": item_url, "quantity": 1},
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
print("=== add_to_cart ===")
|
||||||
|
cart_add_html = await site.add_to_cart(task)
|
||||||
|
if cart_add_html:
|
||||||
|
(PROBE_DIR / "01-cart-add.html").write_text(cart_add_html, encoding="utf-8")
|
||||||
|
print(f" cart-add 页已存盘 ({len(cart_add_html)} bytes)")
|
||||||
|
print("=== verify_cart ===")
|
||||||
|
verify_html = await site.verify_cart(task)
|
||||||
|
if verify_html:
|
||||||
|
(PROBE_DIR / "02-cart-verify.html").write_text(verify_html, encoding="utf-8")
|
||||||
|
print(f" cart-verify 页已存盘 ({len(verify_html)} bytes)")
|
||||||
|
print("=== enter_checkout(落地到确认页或撞上未捕获的中间步骤为止)===")
|
||||||
|
try:
|
||||||
|
html = await site.enter_checkout(task)
|
||||||
|
print(f"enter_checkout 返回,长度={len(html)}")
|
||||||
|
(PROBE_DIR / "03-enter-checkout-final.html").write_text(html, encoding="utf-8")
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"enter_checkout 抛出:{type(exc).__name__}: {exc}")
|
||||||
|
finally:
|
||||||
|
# 不调用 submit_order/pay;enter_checkout 若成功会把 page 留存在
|
||||||
|
# _checkout_pages,这里直接丢弃,不产生下单动作
|
||||||
|
page = site._checkout_pages.pop(task.task_id, None)
|
||||||
|
if page is not None:
|
||||||
|
print(f" 最终落地 url={page.url}")
|
||||||
|
await page.close()
|
||||||
|
|
||||||
|
if "html" not in captured:
|
||||||
|
print("\n没有触发 _confirm_default_address——账号可能没走到这一步"
|
||||||
|
"(比如没有电话补录/地址确认步骤,或选择器判断条件本身有问题)")
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
await site.clear_cart()
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
print(f"清理购物车失败(忽略):{type(exc).__name__}: {exc}")
|
||||||
|
await site.close()
|
||||||
|
await auth.close()
|
||||||
|
|
||||||
|
print(f"\n探针输出目录:{PROBE_DIR}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
async def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--item-url", default=DEFAULT_ITEM_URL)
|
||||||
|
args = parser.parse_args()
|
||||||
|
return await run(args.item_url)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(asyncio.run(main()))
|
||||||
Reference in New Issue
Block a user