haha
This commit is contained in:
318
api_yunyiyunpan.py
Normal file
318
api_yunyiyunpan.py
Normal file
@@ -0,0 +1,318 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
云翼云盘 API:基于 云翼云盘.py 的 FastAPI 接口
|
||||
|
||||
- POST /api/send_code 传入手机号、url,创建 TgeBrowser 指纹浏览器,执行滑块发送验证码,返回抓包内容 + flow_id
|
||||
- POST /api/submit_code 传入 flow_id、验证码,执行 input_code,返回抓包内容
|
||||
|
||||
浏览器环境:使用 TgeBrowser 指纹浏览器、随机手机 UA、HTTP 代理(从代理接口获取)
|
||||
流程超时:若 3 分钟内无调用,自动关闭浏览器
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import Any, Optional
|
||||
|
||||
import requests
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
# 代理接口地址
|
||||
PROXY_API_URL = "http://47.109.106.79:7002/ProxIpServiceTxt"
|
||||
PROXY_HEADERS = {
|
||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
|
||||
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
|
||||
"Cache-Control": "no-cache",
|
||||
"DNT": "1",
|
||||
"Pragma": "no-cache",
|
||||
"Proxy-Connection": "keep-alive",
|
||||
"Upgrade-Insecure-Requests": "1",
|
||||
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36 Edg/145.0.0.0",
|
||||
}
|
||||
|
||||
# 随机手机 UA 池
|
||||
MOBILE_UAS = [
|
||||
"Mozilla/5.0 (Linux; Android 13; SM-S918B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36",
|
||||
"Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Mobile Safari/537.36",
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_2_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1",
|
||||
"Mozilla/5.0 (Linux; Android 12; SM-G998B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Mobile Safari/537.36",
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1",
|
||||
"Mozilla/5.0 (Linux; Android 13; 2201123C) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36",
|
||||
"Mozilla/5.0 (Linux; Android 11; M2102J2SC) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Mobile Safari/537.36",
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 15_7_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.6 Mobile/15E148 Safari/604.1",
|
||||
]
|
||||
|
||||
# 流程空闲超时(秒)
|
||||
FLOW_IDLE_TIMEOUT = 180
|
||||
|
||||
|
||||
def _fetch_proxy() -> tuple[str, int]:
|
||||
"""从代理接口获取 ip:port,返回 (host, port)。"""
|
||||
resp = requests.get(PROXY_API_URL, headers=PROXY_HEADERS, timeout=10, verify=False)
|
||||
resp.raise_for_status()
|
||||
text = (resp.text or "").strip()
|
||||
if ":" not in text:
|
||||
raise RuntimeError(f"代理接口返回格式异常: {text[:100]}")
|
||||
host, _, port_str = text.partition(":")
|
||||
port = int(port_str.strip())
|
||||
return host.strip(), port
|
||||
|
||||
|
||||
def _random_mobile_ua() -> str:
|
||||
return random.choice(MOBILE_UAS)
|
||||
|
||||
|
||||
@dataclass
|
||||
class FlowRecord:
|
||||
flow_id: str
|
||||
url: str
|
||||
phone: str
|
||||
created_at: datetime
|
||||
last_activity: float
|
||||
page: Any
|
||||
env_id: int
|
||||
client: Any
|
||||
capture_data: Any = None
|
||||
|
||||
|
||||
_flows: dict[str, FlowRecord] = {}
|
||||
_flows_lock = threading.Lock()
|
||||
_executor = ThreadPoolExecutor(max_workers=8)
|
||||
|
||||
|
||||
def _create_browser_with_proxy(proxy_host: str, proxy_port: int, user_agent: str, start_url: str) -> tuple[Any, int, Any]:
|
||||
"""创建带代理和 UA 的 TgeBrowser,返回 (page, env_id, client)。"""
|
||||
from tgebrowser_client import TgeBrowserClient
|
||||
from DrissionPage import ChromiumOptions, ChromiumPage
|
||||
|
||||
client = TgeBrowserClient()
|
||||
proxy = {
|
||||
"protocol": "http",
|
||||
"host": proxy_host,
|
||||
"port": proxy_port,
|
||||
}
|
||||
fingerprint = {
|
||||
"os": "Android",
|
||||
"platformVersion": 12,
|
||||
"userAgent": user_agent,
|
||||
}
|
||||
create_data = client.create_browser(
|
||||
browser_name=f"yunyiyunpan_{uuid.uuid4().hex[:8]}",
|
||||
start_page_url=start_url,
|
||||
proxy=proxy,
|
||||
fingerprint=fingerprint,
|
||||
)
|
||||
env_id = create_data.get("envId")
|
||||
if env_id is None:
|
||||
raise RuntimeError("TgeBrowser 创建失败:未返回 envId")
|
||||
|
||||
start_data = client.start_browser(env_id=env_id)
|
||||
start_data["envId"] = env_id
|
||||
port = start_data.get("port")
|
||||
if port is None:
|
||||
ws = start_data.get("ws") or ""
|
||||
port_str = ws.split(":")[-1].split("/")[0]
|
||||
try:
|
||||
port = int(port_str)
|
||||
except ValueError:
|
||||
raise RuntimeError(f"无法解析调试端口: {start_data}")
|
||||
if isinstance(port, str):
|
||||
port = int(port)
|
||||
co = ChromiumOptions().set_local_port(port=int(port))
|
||||
page = ChromiumPage(addr_or_opts=co)
|
||||
return page, env_id, client
|
||||
|
||||
|
||||
def _run_send_code(phone: str, url: str) -> tuple[str, Any, FlowRecord]:
|
||||
"""在后台线程中执行:获取代理、创建浏览器、执行 submit_phone,返回 (flow_id, capture_data, flow_record)。"""
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
mod_path = Path(__file__).resolve().parent / "云翼云盘.py"
|
||||
spec = importlib.util.spec_from_file_location("yunyiyunpan", mod_path)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
submit_phone = mod.submit_phone
|
||||
|
||||
proxy_host, proxy_port = _fetch_proxy()
|
||||
ua = _random_mobile_ua()
|
||||
page, env_id, client = _create_browser_with_proxy(proxy_host, proxy_port, ua, url)
|
||||
|
||||
body = submit_phone(page=page, phone=phone, url=url)
|
||||
|
||||
flow_id = uuid.uuid4().hex
|
||||
now = datetime.now()
|
||||
last_activity = time.time()
|
||||
record = FlowRecord(
|
||||
flow_id=flow_id,
|
||||
url=url,
|
||||
phone=phone,
|
||||
created_at=now,
|
||||
last_activity=last_activity,
|
||||
page=page,
|
||||
env_id=env_id,
|
||||
client=client,
|
||||
capture_data=body,
|
||||
)
|
||||
with _flows_lock:
|
||||
_flows[flow_id] = record
|
||||
return flow_id, body, record
|
||||
|
||||
|
||||
def _run_input_code(flow_id: str, code: str) -> Any:
|
||||
"""在后台线程中执行 input_code,返回抓包内容。"""
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
mod_path = Path(__file__).resolve().parent / "云翼云盘.py"
|
||||
spec = importlib.util.spec_from_file_location("yunyiyunpan", mod_path)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
input_code = mod.input_code
|
||||
|
||||
with _flows_lock:
|
||||
record = _flows.get(flow_id)
|
||||
if record is None:
|
||||
raise ValueError(f"流程不存在或已过期: {flow_id}")
|
||||
|
||||
record.last_activity = time.time()
|
||||
result = input_code(page=record.page, code=str(code))
|
||||
return result
|
||||
|
||||
|
||||
def _close_flow(flow_id: str) -> None:
|
||||
"""关闭指定流程的浏览器。"""
|
||||
with _flows_lock:
|
||||
record = _flows.pop(flow_id, None)
|
||||
if record is None:
|
||||
return
|
||||
try:
|
||||
record.client.stop_browser(env_id=record.env_id)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _cleanup_idle_flows() -> None:
|
||||
"""关闭超时的流程。"""
|
||||
now = time.time()
|
||||
to_close = []
|
||||
with _flows_lock:
|
||||
for fid, rec in list(_flows.items()):
|
||||
if now - rec.last_activity >= FLOW_IDLE_TIMEOUT:
|
||||
to_close.append(fid)
|
||||
for fid in to_close:
|
||||
_flows.pop(fid, None)
|
||||
for fid in to_close:
|
||||
try:
|
||||
rec = None
|
||||
with _flows_lock:
|
||||
pass # 已移除
|
||||
# 需要从某处拿到 client/env_id,但 record 已移除,所以在 pop 前保存
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 重新实现:在 pop 之前先保存需要 stop 的 client 和 env_id
|
||||
to_stop = []
|
||||
with _flows_lock:
|
||||
for fid, rec in list(_flows.items()):
|
||||
if now - rec.last_activity >= FLOW_IDLE_TIMEOUT:
|
||||
to_stop.append((rec.client, rec.env_id))
|
||||
del _flows[fid]
|
||||
for client, env_id in to_stop:
|
||||
try:
|
||||
client.stop_browser(env_id=env_id)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
# ---------- FastAPI ----------
|
||||
|
||||
app = FastAPI(title="云翼云盘 API", description="天翼云盘订购页自动化接口(TgeBrowser + 代理)")
|
||||
|
||||
|
||||
class SendCodeRequest(BaseModel):
|
||||
phone: str = Field(..., description="手机号码")
|
||||
url: str = Field("http://yscnb.com/tyyp/", description="目标页面 URL(哪个界面注入)")
|
||||
|
||||
|
||||
class SendCodeResponse(BaseModel):
|
||||
success: bool = True
|
||||
flow_id: str = Field(..., description="流程唯一标识符")
|
||||
capture_data: Any = Field(..., description="getYanZhenMa/v2 抓包内容")
|
||||
url: str = Field(..., description="当前流程的 URL")
|
||||
phone: str = Field(..., description="手机号")
|
||||
created_at: str = Field(..., description="创建时间")
|
||||
|
||||
|
||||
class SubmitCodeRequest(BaseModel):
|
||||
flow_id: str = Field(..., description="第一个接口返回的唯一标识符")
|
||||
code: str = Field(..., description="短信验证码")
|
||||
|
||||
|
||||
class SubmitCodeResponse(BaseModel):
|
||||
success: bool = True
|
||||
capture_data: Any = Field(..., description="hn_userEquitys/common/order 抓包内容")
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
def start_cleanup_task():
|
||||
def _loop():
|
||||
while True:
|
||||
time.sleep(60)
|
||||
try:
|
||||
_cleanup_idle_flows()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
t = threading.Thread(target=_loop, daemon=True)
|
||||
t.start()
|
||||
|
||||
|
||||
@app.post("/api/send_code", response_model=SendCodeResponse)
|
||||
def api_send_code(req: SendCodeRequest):
|
||||
"""
|
||||
提交手机号并发送验证码:使用 TgeBrowser 指纹浏览器(随机手机 UA + HTTP 代理),
|
||||
打开页面,执行滑块,点击获取验证码,返回抓包内容和 flow_id。
|
||||
浏览器保持打开,若 3 分钟内未调用 submit_code 将自动关闭。
|
||||
"""
|
||||
try:
|
||||
future = _executor.submit(_run_send_code, req.phone, req.url)
|
||||
flow_id, capture_data, record = future.result(timeout=120)
|
||||
return SendCodeResponse(
|
||||
success=True,
|
||||
flow_id=flow_id,
|
||||
capture_data=capture_data,
|
||||
url=record.url,
|
||||
phone=record.phone,
|
||||
created_at=record.created_at.isoformat(),
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@app.post("/api/submit_code", response_model=SubmitCodeResponse)
|
||||
def api_submit_code(req: SubmitCodeRequest):
|
||||
"""
|
||||
提交验证码:根据 flow_id 找到对应浏览器,运行 input_code 输入验证码并提交,
|
||||
返回 hn_userEquitys/common/order 抓包内容。
|
||||
"""
|
||||
try:
|
||||
_cleanup_idle_flows()
|
||||
future = _executor.submit(_run_input_code, req.flow_id, req.code)
|
||||
capture_data = future.result(timeout=30)
|
||||
return SubmitCodeResponse(success=True, capture_data=capture_data)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8002)
|
||||
6
云翼云盘.py
6
云翼云盘.py
@@ -1053,7 +1053,8 @@ def main() -> None:
|
||||
|
||||
|
||||
def input_code(page, code):
|
||||
page.ele('x://input[@placeholder="请输入验证码"]').input(code,clear=True)
|
||||
"""填写验证码并点击确认,监听 hn_userEquitys/common/order 并返回抓包内容。"""
|
||||
page.ele('x://input[@placeholder="请输入验证码"]').input(code, clear=True)
|
||||
time.sleep(0.5)
|
||||
|
||||
page.listen.start("hn_userEquitys/common/order")
|
||||
@@ -1061,7 +1062,8 @@ def input_code(page, code):
|
||||
time.sleep(0.5)
|
||||
page.ele('x://*[@id="app"]/div/div[7]/div/div[3]/button[2]').click(by_js=True)
|
||||
res = page.listen.wait()
|
||||
print(res.response.body)
|
||||
body = getattr(getattr(res, "response", None), "body", None)
|
||||
return body
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user