ai项目
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

472 lines
18 KiB

# yuanbao_sms_auto.py
"""
Playwright 自动登录(sms_auto 完整实现模板)
流程:
1. 向接码平台下单获取一个手机号(得到 order_id / phone / phone_id 等)
2. 将手机号填写到页面并触发发送验证码
3. 轮询接码平台查询该订单的短信列表并抽取验证码
4. 把验证码填入页面并完成登录
5. 登录成功后导出 cookie 并上传到 /cookies/add
注意:需要你把 PROVIDER_* 常量替换为真实接码平台的 API 信息或实现 provider adapter。
"""
import os
import re
import requests
from typing import Optional, Dict, Any, List
from playwright.sync_api import sync_playwright, TimeoutError as PWTimeoutError
from utlit.update_db import upsert_token
# ========== 配置区(按照你要对接的接码平台修改) ==========
COOKIES_ADD_URL = os.getenv("COOKIES_ADD_URL", "http://127.0.0.1:5001/cookies/add")
API_KEY = os.getenv("COOKIES_ADD_API_KEY", "144defc3b314ef9f37d45651e6b1228f")
TARGET = os.getenv("TARGET", "https://metaso.cn/")
HEADLESS = False
# 接码平台通用配置(下面是示例占位)
LUBAN_API_BASE = os.getenv("LUBAN_API_BASE", "https://lubansms.com/v2/api")
LUBAN_API_KEY = os.getenv("LUBAN_API_KEY", "5ebc10c8e8c35797de15c9af46063b36")
DEFAULT_TIMEOUT = 15 # seconds
DEFAULT_CARD_TYPE = os.getenv("LUBAN_CARDTYPE", "1") # 根据平台说明填写默认卡种
# 超时与轮询参数
ORDER_TIMEOUT = 60 # 等待平台分配号码的超时(秒)
SMS_POLL_TIMEOUT = 180 # 等待短信到达的超时(秒)
SMS_POLL_INTERVAL = 3 # 轮询间隔(秒)
CODE_REGEX = re.compile(r"(\d{4,8})") # 匹配 4~8 位数字验证码
# ========== provider adapter 模式(必须实现这些函数) ==========
# 这些函数为示例实现,你需要根据真实接码平台 API 调整请求及解析逻辑。
# Helper to build URL
def _url(path: str) -> str:
return LUBAN_API_BASE.rstrip("/") + "/" + path.lstrip("/")
# ---------- provider_request_number ----------
def provider_request_number(card_type: Optional[str] = None, **kwargs) -> Dict[str, Any]:
"""
请求/租用一个带关键字的号码(getKeywordNumber)
返回示例:
{"ok": True, "order_id": "<id>", "phone": "13800138000", "raw": <original_response>}
失败返回:
{"ok": False, "error": "...", "raw": <original_response_or_text>}
参数:
- country: 国家码(字符串),lubansms 的 API 若无 country 字段可忽略
- card_type: 卡种/套餐类型(平台文档里的 cardType 值)
"""
card_type = card_type or DEFAULT_CARD_TYPE
apikey = LUBAN_API_KEY
if not apikey or apikey == "YOUR_APIKEY":
return {"ok": False, "error": "no_api_key_configured"}
params = {
"apikey": apikey,
"phone": "", # 有些接口要求 phone param 空或占位,保留
"cardType": card_type
}
try:
resp = requests.get(_url("getKeywordNumber"), params=params, timeout=DEFAULT_TIMEOUT)
except Exception as e:
return {"ok": False, "error": f"request_failed: {e}"}
# 尝试解析 JSON
try:
j = resp.json()
if j.get("code") == 0:
return j
except Exception:
return {"ok": False, "error": "invalid_json", "raw": resp.text}
# ---------- provider_get_messages ----------
def provider_get_messages(phone: str, keyword: Optional[str] = None, **kwargs) -> Dict[str, Any]:
"""
拉取短信(getKeywordSms)。
返回:
收到短信: {"ok": True, "messages": [{"text": "...", "ts": 1234567890}], "raw": ...}
等待短信: {"ok": False, "error": "waiting_sms", "raw": ...}
API 错误: {"ok": False, "error": "api_error", "raw": ...}
"""
apikey = LUBAN_API_KEY
if not apikey or apikey == "YOUR_APIKEY":
return {"ok": False, "error": "no_api_key_configured"}
params = {"apikey": apikey, "phone": phone}
if keyword:
params["keyword"] = keyword
try:
resp = requests.get(_url("getKeywordSms"), params=params, timeout=DEFAULT_TIMEOUT)
resp.raise_for_status()
except Exception as e:
return {"ok": False, "error": f"request_failed: {e}"}
try:
j = resp.json()
except Exception:
return {"ok": False, "error": "invalid_json", "raw": resp.text}
code = j.get("code")
msg = j.get("msg", "")
if code == 0:
# 收到短信
return {"ok": True, "messages": j.get("msg", ""), "raw": j}
elif code == 400 and "尚未收到短信" in msg:
# 等待短信
return {"ok": False, "error": "waiting_sms", "raw": j}
else:
# 其他 API 错误
return {"ok": False, "error": "api_error", "raw": j}
# ---------- provider_cancel_order ----------
def provider_cancel_order( phone: str = None, **kwargs) -> Dict[str, Any]:
"""
释放/取消号码(delKeywordNumber)。
返回 {"ok": True} 或 {"ok": False, "error": "..."}
"""
apikey = LUBAN_API_KEY
if not apikey or apikey == "YOUR_APIKEY":
return {"ok": False, "error": "no_api_key_configured"}
params = {"apikey": apikey, "phone": phone}
try:
resp = requests.get(_url("delKeywordNumber"), params=params, timeout=DEFAULT_TIMEOUT)
except Exception as e:
return {"ok": False, "error": f"request_failed: {e}"}
try:
j = resp.json()
except Exception:
return {"ok": False, "error": "invalid_json", "raw": resp.text}
# 成功判断:根据平台文档调整条件(常见 code==0 或 success==True)
if isinstance(j, dict) and (j.get("code") in (0, "0") or j.get("success") in (True, "true")):
return {"ok": True, "raw": j}
# 仍返回原始响应以便调试
return {"ok": False, "error": "provider_error", "raw": j}
# ========== 工具函数 ==========
def extract_code_from_text(text: str) -> Optional[str]:
m = CODE_REGEX.search(text or "")
return m.group(1) if m else None
def cookies_to_string(cookies):
return "; ".join(f"{c['name']}={c['value']}" for c in cookies)
def upload_cookie(cookie_str):
headers = {"X-API-Key": API_KEY, "Content-Type": "application/json"}
resp = requests.post(COOKIES_ADD_URL, headers=headers, json={"cookie": cookie_str}, timeout=30)
resp.raise_for_status()
return resp.json()
from playwright.sync_api import Page, TimeoutError
import time
def ensure_logged_in_ui(page: Page, wait_for_login_selector: str = None, timeout: int = 30):
"""
检查页面是否显示未登录(基于 .nick-info-name 包含 '未登录')。
若未登录则点击触发器(.t-trigger 或 avatar 容器),并等待登录页面/弹窗出现。
参数:
page: Playwright Page 对象
wait_for_login_selector: 登录弹窗/页面可见后用于等待的选择器(可选)
timeout: 等待登录出现的超时(秒)
返回:
True if we clicked/opened login (or already logged in False if already logged?)
"""
try:
# 方法一:用按钮文本(最简单、最稳)
btn = page.get_by_role("button", name="登录/注册")
btn.wait_for(state="visible", timeout=5000)
btn.click()
return True
except PWTimeoutError:
# 备选:更精确的 CSS 选择器(你贴的 HTML 中有很多 class)
try:
btn2 = page.locator("button.css-veatkv", has_text="登录/注册")
btn2.wait_for(state="visible", timeout=3000)
btn2.click()
except Exception as e:
print("点击登录按钮失败:", e)
page.screenshot(path="fail_login.png")
return False
from playwright.sync_api import Page, TimeoutError
import time
def click_phone_tab(page: Page, timeout: int = 10) -> bool:
"""
尝试点击“手机”选项(返回 True 表示已点击并选中)
"""
try:
btn = page.locator("p.css-141qk0f", has_text="手机验证")
btn.wait_for(state="visible", timeout=5000)
btn.click()
print("已点击:手机验证")
return True
except Exception as e:
print("点击手机验证失败:", e)
# page.screenshot(path="fail_login.png")
return False
import re
from playwright.sync_api import Page, TimeoutError
import time
def click_agree_span(page: Page, timeout: float = 5.0) -> bool:
"""
尝试点击页面中显示文本 '我已阅读并同意 ' 的 <span> 元素。
返回 True 表示已点击(或已勾选),False 表示失败。
"""
try:
# 首选:直接 check input(安全且会跳过已选状态)
checkbox = page.locator("#desktop-login-policy")
checkbox.wait_for(state="attached", timeout=3000)
checkbox.check(timeout=3000) # 如果已经选中不会抛错
print("使用 input#desktop-login-policy.check() 成功")
except Exception as e:
print("直接 check 失败,尝试点击包含的 label/span ->", e)
try:
# 备选:点击包含 checkbox 的 label(对自定义样式友好)
page.locator("label:has(#desktop-login-policy)").first.click(timeout=3000)
print("通过点击 label 成功")
except Exception as e2:
print("label 点击失败,尝试强制点击渲染的 checkbox span ->", e2)
try:
page.locator("input#desktop-login-policy").first.click(force=True)
print("force click 成功")
except Exception as e3:
print("所有方法失败:", e3)
page.screenshot(path="checkbox_click_fail.png")
# 验证:检查是否真的被选中
try:
is_checked = page.locator("#desktop-login-policy").is_checked()
print("当前 checked 状态:", is_checked)
except Exception:
print("无法读取 checked 状态,可能是自定义组件(需检查 aria- 属性或 class)")
def fill_phone_and_send(page: Page, phone: str, wait_for_sent: float = 10.0) -> bool:
"""
填手机号并点击“获取验证码”。
phone: e.g. "13800138000" or "+8613800138000" or "8613800138000"
wait_for_sent: 点击后等待发送按钮变为 loading 或不可见的超时时间(秒)
返回 True 表示已触发发送;False 表示未触发(或出错)
"""
# 规范化号码:去掉空白,保留前导 + 或数字
phone = (phone or "").strip()
m = re.match(r'^\+?(\d+)$', phone)
if not m:
raise ValueError("phone 格式不合法: " + repr(phone))
num = m.group(1)
# 如果传入带国家码且是中国 86,可以把前缀去掉用于填写(页面通常期望输入国内号)
# 你可按目标页面需求修改:这里默认若以86开头则去掉
if num.startswith("86") and len(num) > 8:
num_to_fill = num[2:]
else:
num_to_fill = num
# 1) 先确保手机选项已选(若你在页面切换到手机登录后再调用可跳过)
# (可选) 点击国家码下拉以切换国家
# try:
# # 点击区号标签以打开可能的面板(若不需要可跳过)
# if page.query_selector(".yuanbao-oversea-input__wrap__formitem .yuanbao-oversea-input__wrap__formitem__areaCode"):
# try:
# page.click(".yuanbao-oversea-input__wrap__formitem .yuanbao-oversea-input__wrap__formitem__areaCode")
# except Exception:
# pass
# except Exception:
# pass
# 2) 填手机号输入框
phone_selectors = [
"#desktop-login-phone input[type='tel']",
"input[placeholder*='手机号']",
]
phone_filled = False
for sel in phone_selectors:
try:
el = page.query_selector(sel)
if el:
el.fill(num_to_fill)
phone_filled = True
break
except Exception:
pass
if not phone_filled:
raise RuntimeError("无法找到手机号输入框,检查选择器或页面是否加载完成")
# 3) 点击“获取验证码”按钮(优先点击可见的 .hyc-phone-login__send-code)
send_selectors = [
"button:has-text('发送验证码')",
"text=发送验证码"
]
clicked = False
for s in send_selectors:
try:
el = page.query_selector(s)
if not el:
continue
# 如果元素存在但看起来是禁用/灰色(可能用 style 或 aria-disabled 标示),尝试等待变为可点
# 尝试点击
try:
el.click()
clicked = True
break
except Exception:
# fallback: use page.click(selector)
try:
page.click(s)
clicked = True
break
except Exception:
# 可能被禁用,继续检测
pass
except Exception:
pass
if not clicked:
return False
return True
def click_login_and_wait(page: Page, timeout: float = 10.0) -> bool:
"""
点击登录按钮并等待登录完成或超时。
- page: Playwright Page
- timeout: 等待登录成功的最长时间(秒)
返回 True 表示检测到登录成功(或提交已触发并可能成功),False 表示超时或失败。
"""
btn2 = page.locator("button.css-l7o7jf", has_text="登录/注册")
btn2.wait_for(state="visible", timeout=3000)
btn2.click()
return True
# 用法示例(在你的 Playwright 脚本里)
# page = context.new_page()
# page.goto(TARGET)
# opened = ensure_logged_in_ui(page, wait_for_login_selector="css=div.login-modal, form#loginForm", timeout=20)
# if opened:
# print("尝试触发登录弹窗,等待用户或自动化完成登录")
# else:
# print("未触发登录:可能已经登录或页面结构不同")
# ========== 主流程:下单→填写→轮询→登录 ==========
def run_sms_auto_login():
# 1) 向接码平台请求号码
print("[*] 向接码平台请求号码...")
order = provider_request_number()
if not order.get("phone"):
raise RuntimeError("Request number failed: " + str(order.get("error")))
phone = order["phone"]
print(f"[+] 获取到手机号: {phone} ")
# phone = "18751835986"
with sync_playwright() as p:
browser = p.chromium.launch(headless=HEADLESS,executable_path= r'C:\Program Files\Google\Chrome\Application\chrome.exe')
context = browser.new_context()
page = context.new_page()
page.goto(TARGET)
print("[*] 已打开目标页面,请等待页面加载并自动填写手机号...")
ensure_logged_in_ui(page)
time.sleep(2)
click_phone_tab(page)
click_agree_span(page)
# === 将号码输入到页面的手机号输入框 ===
# 请根据目标页面调整选择器
fill_phone_and_send(page,phone)
# 2) 轮询接码平台获取短信
print("[*] 轮询接码平台获取短信,超时 %s s ..." % SMS_POLL_TIMEOUT)
start = time.time()
last_err = None
code = None
while True:
if time.time() - start > SMS_POLL_TIMEOUT:
last_err = "sms_poll_timeout"
break
try:
resp = provider_get_messages(phone, "秘塔")
print(f"获取短信响应{resp}")
except Exception as e:
last_err = str(e)
resp = {"ok": False, "error": last_err}
if not resp.get("ok"):
# 可打印 provider 返回信息,便于排错
print("[...] provider no messages yet:", resp.get("error"))
else:
if "已被当前号码提供方屏蔽" in resp.get("messages",""):
code = None
break
#{"ok": True, "messages": [{"text": "12345", "ts": 12345}, ...]}
msgs = resp.get("messages") or []
# 按时间顺序遍历最新消息,尝试抽码
code = extract_code_from_text(msgs)
if code:
print("[+] 抽取到验证码:", code)
break
if code:
break
time.sleep(SMS_POLL_INTERVAL)
if not code:
print("[-] 未收到验证码或匹配失败:", last_err)
try:
data = provider_cancel_order(phone)
print(f"释放号码响应{data}")
except Exception:
pass
browser.close()
raise RuntimeError("Failed to obtain SMS code: " + str(last_err))
# 3) 把验证码填入页面并提交
sms_input_candidates = [
"input[placeholder*='短信验证码']"
]
filled_sms = False
for sel in sms_input_candidates:
try:
el = page.query_selector(sel)
if el:
el.fill(code)
filled_sms = True
print("[+] 已把验证码填入选择器:", sel)
break
except Exception:
pass
if not filled_sms:
# 允许用户手工粘贴
print("[!] 脚本无法定位验证码输入框,请在浏览器手动粘贴验证码后提交")
print("验证码:", code)
input("粘贴并提交后按回车继续...")
click_login_and_wait(page)
# 4) 等待登录成功并导出 cookie
# 等待一定时间观察页面或 cookie
# input("[*] 等待登录完成(若页面需手动点登录请操作)...")
time.sleep(5)
cookies = context.cookies()
cookie_str = cookies_to_string(cookies)
DB = "./kimi_tokens.db"
TABLE = "tokens"
# 单条 upsert
ok = upsert_token(DB, TABLE, phone=phone, platform_name="mita", token_value=cookie_str, status="active")
print("upsert single:", ok)
browser.close()
return cookie_str
# ========== CLI ==========
if __name__ == "__main__":
while 1:
try:
c = run_sms_auto_login()
print("[DONE] cookie length:", len(c or ""))
except Exception as e:
print("[ERROR]", e)