账号
This commit is contained in:
+9
-116
@@ -1,135 +1,28 @@
|
||||
"""人工登录:起一个有头浏览器,由人完成登录,落盘 cookie 供服务复用
|
||||
"""自动登录 CLI 入口:按 account.yaml 填账号密码,撞验证码则人工接管
|
||||
|
||||
为什么必须人工:乐天与 ラクマ 的登录都带 reCAPTCHA 与设备验证(短信/邮箱 OTP),
|
||||
自动填表的成功率既低又不稳定,还要在本地存明文密码。这里的取舍是——**密码只经过
|
||||
你和站点,不经过本服务**:脚本只负责把浏览器打开、等你登录完,然后把 cookie 存下来。
|
||||
实现已移到 app/trading/services/login_runner.py,本文件保留为命令行入口,
|
||||
让 AuthSession(运行时重登)与 CLI 共用同一份登录逻辑,避免漂移。
|
||||
|
||||
用法:
|
||||
|
||||
.venv/Scripts/python.exe scripts/login.py --site rakuten
|
||||
.venv/Scripts/python.exe scripts/login.py --site rakuma
|
||||
.venv/Scripts/python.exe scripts/login.py --site all
|
||||
.venv/Scripts/python.exe scripts/login.py --site rakuten --account trdian022
|
||||
.venv/Scripts/python.exe scripts/login.py --site all --all-accounts
|
||||
|
||||
浏览器窗口打开后手动完成登录,脚本会自动轮询登录态;检测到已登录即保存并退出。
|
||||
也可以登录完成后回到终端按回车立即保存。
|
||||
|
||||
产物落在 settings.auth_state_dir(默认 .auth/),内含可直接冒充账号的 cookie,
|
||||
已在 .gitignore 里排除,不要提交、不要外传。
|
||||
凭据安全:account.yaml 含明文密码,已在 .gitignore 排除;账号密码只经过
|
||||
account.yaml → 浏览器 → 站点,不入代码/日志/记忆/commit。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# 允许以 `python scripts/login.py` 直接运行
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
from app.shared.config import get_settings # noqa: E402
|
||||
from app.trading.core import auth_site # noqa: E402
|
||||
|
||||
# 轮询间隔与总时长:给足人工过验证码、收短信的时间
|
||||
_POLL_INTERVAL_SECONDS = 5
|
||||
_POLL_TIMEOUT_SECONDS = 600
|
||||
|
||||
|
||||
async def _is_logged_in(page, site: str) -> bool:
|
||||
"""在浏览器里探测登录态
|
||||
|
||||
判据与 AuthSession 共用 auth_site.is_logged_in,只是这里喂浏览器页面内容、
|
||||
那里喂 httpx 响应,避免两条链路对「算不算登录」各判各的。
|
||||
"""
|
||||
await page.goto(
|
||||
auth_site.profile(site).probe_url, wait_until="domcontentloaded", timeout=60_000
|
||||
)
|
||||
return auth_site.is_logged_in(site, final_url=page.url, body=await page.content())
|
||||
|
||||
|
||||
async def login(site: str) -> bool:
|
||||
"""打开浏览器让用户登录指定站点,成功则保存 storage_state"""
|
||||
from playwright.async_api import async_playwright
|
||||
|
||||
settings = get_settings()
|
||||
profile = auth_site.profile(site)
|
||||
state_path = settings.auth_state_path / profile.state_filename
|
||||
|
||||
print(f"\n=== {profile.label}({site})登录 ===")
|
||||
print(f"即将打开浏览器:{profile.login_url}")
|
||||
print("请在浏览器窗口里完成登录(账号密码只在浏览器与站点之间传递,本脚本不读取)。")
|
||||
print(f"登录完成后脚本会自动检测,最长等待 {_POLL_TIMEOUT_SECONDS // 60} 分钟。\n")
|
||||
|
||||
async with async_playwright() as playwright:
|
||||
browser = await playwright.chromium.launch(
|
||||
headless=False, # 人工登录必须有头
|
||||
channel=settings.browser_channel or None,
|
||||
proxy=settings.playwright_proxy,
|
||||
args=["--no-first-run", "--disable-blink-features=AutomationControlled"],
|
||||
)
|
||||
try:
|
||||
context = await browser.new_context(
|
||||
# UA 取自 auth_site:服务端后续用同一个 UA 发请求,换了可能触发
|
||||
# 站点的设备校验,让这次辛苦登来的 cookie 提前失效。
|
||||
user_agent=profile.user_agent,
|
||||
locale="ja-JP",
|
||||
timezone_id="Asia/Tokyo",
|
||||
viewport=(
|
||||
{"width": 390, "height": 844}
|
||||
if profile.mobile
|
||||
else {"width": 1440, "height": 900}
|
||||
),
|
||||
is_mobile=profile.mobile,
|
||||
has_touch=profile.mobile,
|
||||
)
|
||||
page = await context.new_page()
|
||||
await page.goto(profile.login_url, wait_until="domcontentloaded", timeout=60_000)
|
||||
|
||||
waited = 0
|
||||
while waited < _POLL_TIMEOUT_SECONDS:
|
||||
await asyncio.sleep(_POLL_INTERVAL_SECONDS)
|
||||
waited += _POLL_INTERVAL_SECONDS
|
||||
try:
|
||||
if await _is_logged_in(page, site):
|
||||
break
|
||||
except Exception as exc: # 页面正在跳转时探测可能失败,继续等
|
||||
print(f" 探测中({waited}s):{type(exc).__name__}")
|
||||
continue
|
||||
print(f" 等待登录中…({waited}s)")
|
||||
else:
|
||||
print(f"✗ {profile.label} 等待超时,未检测到登录态。未保存。")
|
||||
return False
|
||||
|
||||
state = await context.storage_state()
|
||||
state_path.write_text(
|
||||
json.dumps(state, ensure_ascii=False, indent=2), encoding="utf-8"
|
||||
)
|
||||
print(f"✓ {profile.label} 登录成功,已保存 {len(state.get('cookies', []))} 条 cookie")
|
||||
print(f" → {state_path}")
|
||||
return True
|
||||
finally:
|
||||
await browser.close()
|
||||
|
||||
|
||||
async def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="人工登录并保存乐天/ラクマ 登录态")
|
||||
parser.add_argument(
|
||||
"--site",
|
||||
choices=[*auth_site.SITES, "all"],
|
||||
default="all",
|
||||
help="要登录的站点,默认两站都登录",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
targets = list(auth_site.SITES) if args.site == "all" else [args.site]
|
||||
results = {site: await login(site) for site in targets}
|
||||
|
||||
print("\n=== 结果 ===")
|
||||
for site, ok in results.items():
|
||||
print(f" {site}: {'已登录' if ok else '失败'}")
|
||||
print("\n登录态已就绪,可启动服务并用 POST /api/auth/status 复核。")
|
||||
return 0 if all(results.values()) else 1
|
||||
from app.trading.services.login_runner import cli_main # noqa: E402
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(asyncio.run(main()))
|
||||
raise SystemExit(asyncio.run(cli_main()))
|
||||
|
||||
Reference in New Issue
Block a user