From 49d0425334a8aa3ee7a03c0baf2c9dde36b8a041 Mon Sep 17 00:00:00 2001 From: ddrwode <34234@3来 34> Date: Fri, 27 Feb 2026 18:29:02 +0800 Subject: [PATCH] haha --- api_yunyiyunpan.py | 318 +++++++++++++++++++++++++++++++++++++++++++++ 云翼云盘.py | 6 +- 2 files changed, 322 insertions(+), 2 deletions(-) create mode 100644 api_yunyiyunpan.py diff --git a/api_yunyiyunpan.py b/api_yunyiyunpan.py new file mode 100644 index 0000000..6cfdf3c --- /dev/null +++ b/api_yunyiyunpan.py @@ -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) diff --git a/云翼云盘.py b/云翼云盘.py index de02476..7cda813 100644 --- a/云翼云盘.py +++ b/云翼云盘.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()