You've already forked pruchase_jhw
适配common-0.3.0
This commit is contained in:
+2
-2
@@ -9,8 +9,8 @@ authors = [
|
|||||||
{ name = "Antigravity Development Team" }
|
{ name = "Antigravity Development Team" }
|
||||||
]
|
]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
# playwright 由 surugaya-common 的 [browser] extra 提供(>=1.54.0,<2.0.0),不再直接声明。
|
# playwright / playwright-stealth 由 surugaya-common 的 [browser] extra 提供,不再直接声明。
|
||||||
"surugaya-common[browser]>=0.2.0",
|
"surugaya-common[browser]>=0.3.0",
|
||||||
"pydantic>=2.7.0",
|
"pydantic>=2.7.0",
|
||||||
"pydantic-settings>=2.2.0",
|
"pydantic-settings>=2.2.0",
|
||||||
"loguru>=0.7.2",
|
"loguru>=0.7.2",
|
||||||
|
|||||||
+8
-18
@@ -4,6 +4,7 @@ from pathlib import Path
|
|||||||
from typing import Dict
|
from typing import Dict
|
||||||
from playwright.async_api import async_playwright, Playwright, BrowserContext
|
from playwright.async_api import async_playwright, Playwright, BrowserContext
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
from surugaya_common.browser_profile import DEFAULT_USER_AGENT, LOCALE, TIMEZONE_ID, stealth_launch_args
|
||||||
from src.config.settings import get_settings
|
from src.config.settings import get_settings
|
||||||
from src.config.accounts import AccountConfig, load_accounts_config
|
from src.config.accounts import AccountConfig, load_accounts_config
|
||||||
|
|
||||||
@@ -239,17 +240,9 @@ class BrowserManager:
|
|||||||
logger.error(f"创建账号 '{account.account_id}' 的持久化目录失败: {e}")
|
logger.error(f"创建账号 '{account.account_id}' 的持久化目录失败: {e}")
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
# 2. 配置隐身 Chromium 启动参数
|
# 2. 配置隐身 Chromium 启动参数(共享反检测参数 + 本进程追加项)
|
||||||
chrome_args = [
|
chrome_args = stealth_launch_args() + [
|
||||||
"--disable-blink-features=AutomationControlled", # 浏览器层面移除 navigator.webdriver
|
"--start-maximized", # 启动时全屏(最大化)
|
||||||
"--disable-infobars", # 隐藏「Chrome 正受到自动测试软件控制」提示条
|
|
||||||
"--no-sandbox", # Docker/服务器环境兼容
|
|
||||||
"--disable-setuid-sandbox",
|
|
||||||
"--disable-dev-shm-usage", # 内存受限环境避免资源崩溃
|
|
||||||
"--start-maximized", # 启动时全屏(最大化)
|
|
||||||
"--disable-gpu", # 服务器无头模式稳定渲染
|
|
||||||
"--use-mock-keychain", # 避免本地密钥环弹窗(macOS/Linux)
|
|
||||||
"--password-store=basic",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
executable_path_raw = self.settings.browser_executable_path
|
executable_path_raw = self.settings.browser_executable_path
|
||||||
@@ -265,11 +258,8 @@ class BrowserManager:
|
|||||||
)
|
)
|
||||||
headless_mode = self.settings.browser_headless
|
headless_mode = self.settings.browser_headless
|
||||||
|
|
||||||
# 使用较新的 Windows Chrome User-Agent
|
# 使用与 API 服务一致的 Windows Chrome User-Agent(共享指纹配置)
|
||||||
user_agent = (
|
user_agent = DEFAULT_USER_AGENT
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
|
||||||
"(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info(f"正在为账号 {account.account_id} 启动 Playwright | 无头模式: {headless_mode}")
|
logger.info(f"正在为账号 {account.account_id} 启动 Playwright | 无头模式: {headless_mode}")
|
||||||
|
|
||||||
@@ -289,8 +279,8 @@ class BrowserManager:
|
|||||||
"args": chrome_args,
|
"args": chrome_args,
|
||||||
"user_agent": user_agent,
|
"user_agent": user_agent,
|
||||||
"no_viewport": True, # 配合 --start-maximized 使得网页视口也铺满全屏
|
"no_viewport": True, # 配合 --start-maximized 使得网页视口也铺满全屏
|
||||||
"locale": "ja-JP", # 区域语言一致
|
"locale": LOCALE, # 区域语言一致(ja-JP)
|
||||||
"timezone_id": "Asia/Tokyo", # 日本本地时区
|
"timezone_id": TIMEZONE_ID, # 日本本地时区(Asia/Tokyo)
|
||||||
"accept_downloads": True, # 允许自动下载(如需要)
|
"accept_downloads": True, # 允许自动下载(如需要)
|
||||||
"proxy": proxy_config # 住宅代理等
|
"proxy": proxy_config # 住宅代理等
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-75
@@ -17,12 +17,13 @@ from playwright.async_api import TimeoutError as PlaywrightTimeoutError
|
|||||||
from notifications import NotificationEvent
|
from notifications import NotificationEvent
|
||||||
from src.tasks.base import BasePurchaseTask
|
from src.tasks.base import BasePurchaseTask
|
||||||
from src.config.accounts import get_account_config
|
from src.config.accounts import get_account_config
|
||||||
from src.utils.page_utils import format_japanese_price
|
|
||||||
from src.task_queue.redis_manager import get_redis_client
|
from src.task_queue.redis_manager import get_redis_client
|
||||||
from surugaya_common.models import SurugayaOrderDetail
|
from surugaya_common.models import SurugayaOrderDetail
|
||||||
from surugaya_common.parsers.surugaya.order_detail import SurugayaOrderDetailParser
|
from surugaya_common.parsers.surugaya.order_detail import SurugayaOrderDetailParser
|
||||||
from surugaya_common.browser_utils import human_scroll
|
from surugaya_common.browser_utils import clear_cart, human_scroll
|
||||||
|
from surugaya_common.parse_utils import format_japanese_price
|
||||||
from surugaya_common.app_utils import is_valid_str
|
from surugaya_common.app_utils import is_valid_str
|
||||||
|
from surugaya_common.urls import CARGO_DETAIL_URL, MYPAGE_URL, product_detail_url, trade_detail_url
|
||||||
|
|
||||||
|
|
||||||
class TemplateMatchResult(TypedDict):
|
class TemplateMatchResult(TypedDict):
|
||||||
@@ -57,7 +58,7 @@ class SurugayaPurchaseTask(BasePurchaseTask):
|
|||||||
|
|
||||||
logger.info(f"[任务 {self.task_id}] 购买任务开始,商品:{item_list}")
|
logger.info(f"[任务 {self.task_id}] 购买任务开始,商品:{item_list}")
|
||||||
# 打开购物车
|
# 打开购物车
|
||||||
await page.goto("https://www.suruga-ya.jp/cargo/detail", wait_until="domcontentloaded", timeout=30000)
|
await page.goto(CARGO_DETAIL_URL, wait_until="domcontentloaded", timeout=30000)
|
||||||
|
|
||||||
# 检查登录状态
|
# 检查登录状态
|
||||||
logger.debug(f"[任务 {self.task_id}] 正在校验账号 '{self.account_id}' 的登录状态...")
|
logger.debug(f"[任务 {self.task_id}] 正在校验账号 '{self.account_id}' 的登录状态...")
|
||||||
@@ -70,7 +71,7 @@ class SurugayaPurchaseTask(BasePurchaseTask):
|
|||||||
raise RuntimeError(f"账号 '{self.account_id}' 登录失败,无法继续执行采购流程。")
|
raise RuntimeError(f"账号 '{self.account_id}' 登录失败,无法继续执行采购流程。")
|
||||||
|
|
||||||
# 重新进入购物车
|
# 重新进入购物车
|
||||||
await page.goto("https://www.suruga-ya.jp/cargo/detail", wait_until="domcontentloaded", timeout=30000)
|
await page.goto(CARGO_DETAIL_URL, wait_until="domcontentloaded", timeout=30000)
|
||||||
|
|
||||||
# 判断购物车是否有商品
|
# 判断购物车是否有商品
|
||||||
cart_goods_num = await page.locator("table.item_box tbody tr.item").count()
|
cart_goods_num = await page.locator("table.item_box tbody tr.item").count()
|
||||||
@@ -79,7 +80,7 @@ class SurugayaPurchaseTask(BasePurchaseTask):
|
|||||||
# 清空购物车
|
# 清空购物车
|
||||||
await asyncio.sleep(random.uniform(1, 3)) # 加入随机延迟,模拟人工操作间隔
|
await asyncio.sleep(random.uniform(1, 3)) # 加入随机延迟,模拟人工操作间隔
|
||||||
if cart_goods_num > 0:
|
if cart_goods_num > 0:
|
||||||
await self.clear_cart(page)
|
await clear_cart(page)
|
||||||
|
|
||||||
# 将待采购的商品加入购物车
|
# 将待采购的商品加入购物车
|
||||||
for index, item in enumerate(item_list):
|
for index, item in enumerate(item_list):
|
||||||
@@ -96,9 +97,7 @@ class SurugayaPurchaseTask(BasePurchaseTask):
|
|||||||
logger.warning(f"[任务 {self.task_id}] 索引 {index} 的商品数量为 0,跳过。")
|
logger.warning(f"[任务 {self.task_id}] 索引 {index} 的商品数量为 0,跳过。")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
detail_url = f"https://www.suruga-ya.jp/product/detail/{product_id}"
|
detail_url = product_detail_url(product_id, tenpo_cd)
|
||||||
if tenpo_cd:
|
|
||||||
detail_url += f"?tenpo_cd={tenpo_cd}"
|
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"[任务 {self.task_id}] 进入商品 {product_id} 详情页,购买数量: {number}, URL: {detail_url}")
|
f"[任务 {self.task_id}] 进入商品 {product_id} 详情页,购买数量: {number}, URL: {detail_url}")
|
||||||
@@ -195,7 +194,7 @@ class SurugayaPurchaseTask(BasePurchaseTask):
|
|||||||
|
|
||||||
# 再次进入购物车,确认商品是否加入成功
|
# 再次进入购物车,确认商品是否加入成功
|
||||||
await asyncio.sleep(random.uniform(1, 3))
|
await asyncio.sleep(random.uniform(1, 3))
|
||||||
await page.goto("https://www.suruga-ya.jp/cargo/detail", wait_until="domcontentloaded", timeout=30000)
|
await page.goto(CARGO_DETAIL_URL, wait_until="domcontentloaded", timeout=30000)
|
||||||
cart_goods_num = await page.locator("table.item_box tbody tr.item").count()
|
cart_goods_num = await page.locator("table.item_box tbody tr.item").count()
|
||||||
logger.debug(f"[任务 {self.task_id}] 再次进入购物车页面,购物车商品数量:{cart_goods_num}")
|
logger.debug(f"[任务 {self.task_id}] 再次进入购物车页面,购物车商品数量:{cart_goods_num}")
|
||||||
if cart_goods_num < 1:
|
if cart_goods_num < 1:
|
||||||
@@ -304,7 +303,7 @@ class SurugayaPurchaseTask(BasePurchaseTask):
|
|||||||
解析交易详情页内容,提取交易详情。
|
解析交易详情页内容,提取交易详情。
|
||||||
"""
|
"""
|
||||||
await page.goto(
|
await page.goto(
|
||||||
f"https://www.suruga-ya.jp/pcmypage/action_sell_search/detail?trade_code={trade_code}",
|
trade_detail_url(trade_code),
|
||||||
wait_until="domcontentloaded",
|
wait_until="domcontentloaded",
|
||||||
timeout=30000,
|
timeout=30000,
|
||||||
)
|
)
|
||||||
@@ -345,7 +344,7 @@ class SurugayaPurchaseTask(BasePurchaseTask):
|
|||||||
password = account_config.password
|
password = account_config.password
|
||||||
|
|
||||||
logger.info(f"[任务 {self.task_id}] 正在初始化骏河屋登录流程...")
|
logger.info(f"[任务 {self.task_id}] 正在初始化骏河屋登录流程...")
|
||||||
await page.goto("https://www.suruga-ya.jp/pcmypage", wait_until="domcontentloaded", timeout=30000)
|
await page.goto(MYPAGE_URL, wait_until="domcontentloaded", timeout=30000)
|
||||||
|
|
||||||
# 1. 确保表单和输入框在画面上完全可见
|
# 1. 确保表单和输入框在画面上完全可见
|
||||||
# 骏河屋的账号框 id 是 'edit-mail'
|
# 骏河屋的账号框 id 是 'edit-mail'
|
||||||
@@ -390,70 +389,6 @@ class SurugayaPurchaseTask(BasePurchaseTask):
|
|||||||
await self.take_screenshot(scene="surugaya_login_failed")
|
await self.take_screenshot(scene="surugaya_login_failed")
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
async def clear_cart(self, page: Page) -> None:
|
|
||||||
"""
|
|
||||||
清空购物车
|
|
||||||
"""
|
|
||||||
logger.debug(f"[任务 {self.task_id}] 正在清空购物车...")
|
|
||||||
cart_goods_num = await page.locator("table.item_box tbody tr.item").count()
|
|
||||||
if cart_goods_num < 1:
|
|
||||||
return
|
|
||||||
|
|
||||||
is_delete = 0
|
|
||||||
# 购物车商品数量超过3个后会出现"全削除"按钮
|
|
||||||
if cart_goods_num > 3:
|
|
||||||
try:
|
|
||||||
delete_button = page.locator("#total_box .delbtn-all").get_by_role("button", name="全削除")
|
|
||||||
# 3. 显式等待按钮在页面上可见(容错处理:比如给它最多 3000 毫秒的加载时间)
|
|
||||||
# 如果按钮是一开始就在静态 HTML 里的,这一步也可以省略,直接用下面的 is_visible()
|
|
||||||
await delete_button.wait_for(state="visible", timeout=3000)
|
|
||||||
if await delete_button.is_visible():
|
|
||||||
logger.debug("检测到‘全削除’按钮存在,正在执行点击...")
|
|
||||||
await delete_button.click()
|
|
||||||
is_delete = 1
|
|
||||||
except Exception:
|
|
||||||
logger.debug("未检测到‘全削除’按钮。")
|
|
||||||
|
|
||||||
if is_delete == 1:
|
|
||||||
return
|
|
||||||
|
|
||||||
# 遍历删除
|
|
||||||
await page.wait_for_timeout(random.randint(1000, 3000))
|
|
||||||
item_selector = "table.item_box tbody tr.item"
|
|
||||||
remove_selector = "table.item_box a.remove_product"
|
|
||||||
|
|
||||||
while True:
|
|
||||||
cart_goods_num = await page.locator(item_selector).count()
|
|
||||||
if cart_goods_num == 0:
|
|
||||||
break
|
|
||||||
|
|
||||||
remove_link = page.locator(remove_selector).first
|
|
||||||
href = await remove_link.get_attribute("href")
|
|
||||||
# logger.debug(f"[任务 {self.task_id}] 删除购物车商品:{href or '(no href)'},当前购物车数量:{cart_goods_num}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
await remove_link.click()
|
|
||||||
# 删除按钮不会触发弹窗,直接等待购物车条目数减少即可。
|
|
||||||
await page.wait_for_function(
|
|
||||||
"""({selector, expectedCount}) => {
|
|
||||||
return document.querySelectorAll(selector).length < expectedCount;
|
|
||||||
}""",
|
|
||||||
arg={"selector": item_selector, "expectedCount": cart_goods_num},
|
|
||||||
timeout=15_000,
|
|
||||||
)
|
|
||||||
await page.wait_for_timeout(random.randint(800, 1500))
|
|
||||||
except PlaywrightTimeoutError:
|
|
||||||
latest_count = await page.locator(item_selector).count()
|
|
||||||
if latest_count < cart_goods_num:
|
|
||||||
logger.debug(f"[任务 {self.task_id}] 购物车商品已删除,当前购物车数量:{latest_count}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
logger.warning(f"[任务 {self.task_id}] 删除商品后数量未变化,等待后重试")
|
|
||||||
await page.wait_for_timeout(1000)
|
|
||||||
|
|
||||||
cart_goods_num = await page.locator(item_selector).count()
|
|
||||||
# logger.debug(f"[任务 {self.task_id}] 购物车已清空。")
|
|
||||||
|
|
||||||
async def get_show_payment_methods(self, page: Page) -> list:
|
async def get_show_payment_methods(self, page: Page) -> list:
|
||||||
"""
|
"""
|
||||||
获取所有显示的支付方式
|
获取所有显示的支付方式
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from typing import Any
|
|||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from surugaya_common.parsers.surugaya import SurugayaOrderDetailParser
|
from surugaya_common.parsers.surugaya import SurugayaOrderDetailParser
|
||||||
|
from surugaya_common.urls import trade_detail_url
|
||||||
from src.tasks.base import BasePurchaseTask
|
from src.tasks.base import BasePurchaseTask
|
||||||
from src.task_queue.redis_manager import get_redis_client
|
from src.task_queue.redis_manager import get_redis_client
|
||||||
|
|
||||||
@@ -37,7 +38,7 @@ class SurugayaTradeMonitorTask(BasePurchaseTask):
|
|||||||
|
|
||||||
page = self.page
|
page = self.page
|
||||||
|
|
||||||
await page.goto(f"https://www.suruga-ya.jp/pcmypage/action_sell_search/detail?trade_code={trade_code}",
|
await page.goto(trade_detail_url(trade_code),
|
||||||
wait_until="domcontentloaded", timeout=30000)
|
wait_until="domcontentloaded", timeout=30000)
|
||||||
|
|
||||||
order_detail_model = await SurugayaOrderDetailParser.parse_page(page)
|
order_detail_model = await SurugayaOrderDetailParser.parse_page(page)
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
import re
|
|
||||||
|
|
||||||
|
|
||||||
def format_japanese_price(price_str: str | None) -> int:
|
|
||||||
"""
|
|
||||||
将日本电商网站的价格字符串格式化为标准整数
|
|
||||||
支持的格式示例: "600円 (税込)", "1,250円", "¥ 3,000", "300"
|
|
||||||
"""
|
|
||||||
if not price_str:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
digits = re.findall(r'\d', price_str)
|
|
||||||
if not digits:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
return int("".join(digits))
|
|
||||||
@@ -187,7 +187,7 @@ requires-dist = [
|
|||||||
{ name = "pyscreeze", specifier = ">=1.0.1" },
|
{ name = "pyscreeze", specifier = ">=1.0.1" },
|
||||||
{ name = "pyyaml", specifier = ">=6.0.1" },
|
{ name = "pyyaml", specifier = ">=6.0.1" },
|
||||||
{ name = "redis", specifier = "==8.0.0" },
|
{ name = "redis", specifier = "==8.0.0" },
|
||||||
{ name = "surugaya-common", extras = ["browser"], specifier = ">=0.2.0", index = "https://git.jerryyan.net/api/packages/jp/pypi/simple" },
|
{ name = "surugaya-common", extras = ["browser"], specifier = ">=0.3.0", index = "https://git.jerryyan.net/api/packages/jp/pypi/simple" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -381,6 +381,18 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/80/c8/210f282d278e4709cdd71b12a31af45a30a22ab3207b387e29b37e478713/playwright-1.60.0-py3-none-win_arm64.whl", hash = "sha256:6e4f6700a4c2250efff8e690a81d66e3855754fb587b6b87cf5c784014f91537", size = 34037981, upload-time = "2026-05-18T12:00:57.584Z" },
|
{ url = "https://files.pythonhosted.org/packages/80/c8/210f282d278e4709cdd71b12a31af45a30a22ab3207b387e29b37e478713/playwright-1.60.0-py3-none-win_arm64.whl", hash = "sha256:6e4f6700a4c2250efff8e690a81d66e3855754fb587b6b87cf5c784014f91537", size = 34037981, upload-time = "2026-05-18T12:00:57.584Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "playwright-stealth"
|
||||||
|
version = "2.0.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "playwright" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a4/db/6ade5d539c7d151b9defc78fafa8b65aa52352617d0e7699b47008bd801f/playwright_stealth-2.0.3.tar.gz", hash = "sha256:1d8e488fbdd8f190f1269ea8cf5d57d14df3a9f1af1001c41ee3588b2aac3133", size = 25751, upload-time = "2026-04-04T02:50:33.88Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b5/10/607c409712c02a26c4cb794820514cb7fdaaeac15fb05bed917fb8a354b3/playwright_stealth-2.0.3-py3-none-any.whl", hash = "sha256:1887ade423ab7ff8ae16d363a30a38de0b5817e1e4a29d47b74bf3a0e3dbfcb4", size = 34385, upload-time = "2026-04-04T02:50:35.246Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyautogui"
|
name = "pyautogui"
|
||||||
version = "0.9.54"
|
version = "0.9.54"
|
||||||
@@ -685,20 +697,21 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "surugaya-common"
|
name = "surugaya-common"
|
||||||
version = "0.2.0"
|
version = "0.3.0"
|
||||||
source = { registry = "https://git.jerryyan.net/api/packages/jp/pypi/simple" }
|
source = { registry = "https://git.jerryyan.net/api/packages/jp/pypi/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "pydantic" },
|
{ name = "pydantic" },
|
||||||
{ name = "redis" },
|
{ name = "redis" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://git.jerryyan.net/api/packages/jp/pypi/files/surugaya-common/0.2.0/surugaya_common-0.2.0.tar.gz", hash = "sha256:168a5810672f5ead82957a9491611b64bf439f489b4eeaa287b302206712e2ab" }
|
sdist = { url = "https://git.jerryyan.net/api/packages/jp/pypi/files/surugaya-common/0.3.0/surugaya_common-0.3.0.tar.gz", hash = "sha256:578757409e8e062fd9bac32244775f4db76ba1dd8d22cb5fe4eaeab8f068c85f" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://git.jerryyan.net/api/packages/jp/pypi/files/surugaya-common/0.2.0/surugaya_common-0.2.0-py3-none-any.whl", hash = "sha256:140587df6b60abf1f1fc3b592d0668a096ddd42f5216ee16d4b83a95783b58cd" },
|
{ url = "https://git.jerryyan.net/api/packages/jp/pypi/files/surugaya-common/0.3.0/surugaya_common-0.3.0-py3-none-any.whl", hash = "sha256:281ae05a4996e4100d40abd6e5c6ffe0953cec5142745d287e1b21082c2bad1f" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.optional-dependencies]
|
[package.optional-dependencies]
|
||||||
browser = [
|
browser = [
|
||||||
{ name = "playwright" },
|
{ name = "playwright" },
|
||||||
|
{ name = "playwright-stealth" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
Reference in New Issue
Block a user