This commit is contained in:
27942
2026-02-27 04:01:33 +08:00
parent fcb7cd38a0
commit 8fccaea689
6 changed files with 650 additions and 0 deletions

99
MIGU_README.md Normal file
View File

@@ -0,0 +1,99 @@
# 咪咕短剧 flows.cdyylkj.com/miguSM/home 自动化说明
使用 **DrissionPage** + **TgeBrowser** 实现自动化:输入手机号、点击发送验证码、填写验证码。
## 前置条件
1. **安装 TgeBrowser**:从 [https://tgebrowser.com/zh/download](https://tgebrowser.com/zh/download) 下载并安装
2. **启动 TgeBrowser 客户端**,确保本地 API 服务运行(默认端口 50326
3. **获取 API Key**TgeBrowser 客户端 → API → 生成新密钥
4. **设置环境变量**
```powershell
$env:TGEBROWSER_API_KEY = "你的API密钥"
```
## 流程说明
- **步骤一**:传入电话号码 → 新建一个 TgeBrowser 浏览器 → 打开 miguSM 页面 → 输入手机号 → 点击发送验证码 → 返回成功
- **步骤二**:传入验证码 → 在对应会话中填写验证码并提交
每个电话号码对应一个新建的浏览器实例。
## 使用方式
### 方式一API 服务
```bash
# 安装依赖
pip install -r requirements.txt
# 启动 API端口 8001
python api_migu.py
```
**接口一:提交手机号**
```http
POST http://127.0.0.1:8001/api/submit_phone
Content-Type: application/json
{
"phone": "13800138000",
"url": "https://flows.cdyylkj.com/miguSM/home"
}
```
响应示例:
```json
{
"success": true,
"session_id": "uuid-xxx",
"message": "输入电话号码成功"
}
```
**接口二:提交验证码**
```http
POST http://127.0.0.1:8001/api/submit_code
Content-Type: application/json
{
"session_id": "上一步返回的 session_id",
"code": "123456"
}
```
### 方式二:命令行
**推荐:交互模式(一步完成)**
```bash
python run_migu_cli.py --phone 13800138000 -i
# 输入手机号、点击发送验证码后,会提示「请输入短信验证码」,输入收到的验证码即可
```
**分步执行**
```bash
# 步骤一
python run_migu_cli.py --phone 13800138000
# 步骤二(仅在同一 shell 内、步骤一之后执行,因为依赖内存中的 page
python run_migu_cli.py --code 123456
```
> 跨进程/跨请求的会话请使用 API 模式(`api_migu.py`)。
## 选择器说明
若目标页面结构变化导致找不到输入框或按钮,可修改 `migu_miguSM_dp.py` 中的选择器列表(`_find_first` 所用选择器)。当前已适配常见 H5 表单结构。
## 文件说明
| 文件 | 说明 |
|------|------|
| `tgebrowser_client.py` | TgeBrowser REST API 客户端(创建/启动/停止浏览器) |
| `migu_miguSM_dp.py` | DrissionPage 自动化逻辑(输入手机号、发送验证码、填写验证码) |
| `api_migu.py` | FastAPI 接口submit_phone / submit_code |
| `run_migu_cli.py` | 命令行入口 |

135
api_migu.py Normal file
View File

@@ -0,0 +1,135 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
咪咕短剧 flows.cdyylkj.com/miguSM/home 接口:
- POST /api/submit_phone 传入电话号码 → 新建 TgeBrowser → 输入手机号、点击发送验证码 → 返回成功
- POST /api/submit_code 传入验证码 → 在对应会话中填写验证码并提交
每个电话号码对应一个新建的 TgeBrowser 浏览器会话。
"""
from __future__ import annotations
import threading
import uuid
from typing import Any, Optional
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
from migu_miguSM_dp import (
MIGU_HOME_URL,
connect_dp_to_tgebrowser,
connect_dp_to_ws,
input_code_and_submit,
input_phone_and_send_code,
)
from tgebrowser_client import TgeBrowserClient
app = FastAPI(title="咪咕短剧 miguSM 自动化 API", description="TgeBrowser + DrissionPage 手机号与验证码提交流程")
# 会话存储session_id -> {page, env_id, client}
_sessions: dict[str, dict[str, Any]] = {}
_sessions_lock = threading.Lock()
class SubmitPhoneRequest(BaseModel):
phone: str = Field(..., description="手机号码")
url: str = Field(MIGU_HOME_URL, description="目标页面 URL")
class SubmitPhoneResponse(BaseModel):
success: bool = True
session_id: str = Field(..., description="会话 IDsubmit_code 时需传入")
message: str = "输入电话号码成功"
class SubmitCodeRequest(BaseModel):
session_id: str = Field(..., description="submit_phone 返回的会话 ID")
code: str = Field(..., description="短信验证码")
class SubmitCodeResponse(BaseModel):
success: bool = True
message: str = ""
@app.post("/api/submit_phone", response_model=SubmitPhoneResponse)
def api_submit_phone(req: SubmitPhoneRequest):
"""
步骤一:传入电话号码。
新建 TgeBrowser 浏览器 → 打开 miguSM 页面 → 输入手机号 → 点击发送验证码。
返回 session_id用于后续 submit_code。
"""
try:
client = TgeBrowserClient()
# 每次输入电话号码新建一个浏览器
start_data = client.create_and_start(
browser_name=f"miguSM_{req.phone[-4:]}",
start_page_url=req.url,
)
port = start_data.get("port")
ws = start_data.get("ws")
env_id = start_data.get("envId")
# 优先用端口连接DrissionPage 对端口支持更好)
if port:
page = connect_dp_to_tgebrowser(port)
elif ws:
page = connect_dp_to_ws(ws)
else:
raise HTTPException(status_code=500, detail="TgeBrowser 未返回 port 或 ws")
input_phone_and_send_code(page, req.phone, url=req.url)
session_id = str(uuid.uuid4())
with _sessions_lock:
_sessions[session_id] = {
"page": page,
"env_id": env_id,
"client": client,
}
return SubmitPhoneResponse(success=True, session_id=session_id, message="输入电话号码成功")
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):
"""
步骤二:传入验证码。
在 submit_phone 创建的会话中填写验证码并提交。
"""
with _sessions_lock:
sess = _sessions.get(req.session_id)
if not sess:
raise HTTPException(status_code=404, detail=f"会话不存在或已过期: {req.session_id}")
page = sess["page"]
try:
result = input_code_and_submit(page, req.code)
return SubmitCodeResponse(success=True, message=result.get("message", "验证码已填写"))
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.delete("/api/session/{session_id}")
def close_session(session_id: str):
"""关闭并清理指定会话(可选,用于释放浏览器)。"""
with _sessions_lock:
sess = _sessions.pop(session_id, None)
if not sess:
return {"success": False, "message": "会话不存在"}
try:
client = sess.get("client")
env_id = sess.get("env_id")
if client and env_id is not None:
client.stop_browser(env_id=env_id)
except Exception:
pass
return {"success": True, "message": "会话已关闭"}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8001)

163
migu_miguSM_dp.py Normal file
View File

@@ -0,0 +1,163 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
咪咕短剧超级会员 flows.cdyylkj.com/miguSM/home 自动化DrissionPage + TgeBrowser
1) 新建 TgeBrowser 浏览器
2) 打开页面,输入手机号,点击发送验证码 → 返回成功
3) 接收验证码并填写、提交
"""
from __future__ import annotations
import time
from typing import Any, Optional
from DrissionPage import ChromiumOptions, ChromiumPage
MIGU_HOME_URL = "https://flows.cdyylkj.com/miguSM/home"
def _find_first(page, selectors: list[str], timeout: float = 8):
"""尝试多个选择器,返回第一个找到的元素。"""
for sel in selectors:
try:
ele = page.ele(sel, timeout=timeout)
if ele:
return ele
except Exception:
continue
return None
def _click_safe(ele) -> None:
try:
ele.click()
except Exception:
try:
ele.click(by_js=True)
except Exception:
ele.run_js("this.click()")
def input_phone_and_send_code(
page: ChromiumPage,
phone: str,
url: str = MIGU_HOME_URL,
wait_after_open: float = 1.0,
) -> dict:
"""
步骤一:输入手机号、点击发送验证码。
返回 {"success": True, "message": "输入电话号码成功"} 或抛出异常。
"""
page.get(url)
time.sleep(wait_after_open)
# 手机号输入框(按常见 H5 表单结构)
phone_input = _find_first(page, [
'x://input[@placeholder*="手机"]',
'x://input[@placeholder*="电话"]',
'x://input[@placeholder*="号码"]',
'x://input[@type="tel"]',
'x://input[@type="number"]',
"css:input[type='tel']",
"css:input[placeholder*='手机']",
"css:input[placeholder*='电话']",
"css:input.inp-txt",
"css:.phone-input input",
"css:input.phone",
], timeout=10)
if not phone_input:
raise RuntimeError("未找到手机号输入框,请根据页面调整选择器")
phone_input.input(phone, clear=True)
time.sleep(0.3)
# 可选:勾选协议(若页面有)
agree = _find_first(page, [
"css:input[type='checkbox']",
"css:.agree-checkbox",
'x://input[@type="checkbox"]',
'x://i[contains(@class,"checkbox")]',
], timeout=2)
if agree:
try:
_click_safe(agree)
time.sleep(0.2)
except Exception:
pass
# 发送验证码按钮
send_btn = _find_first(page, [
'x://button[contains(text(),"获取验证码")]',
'x://span[contains(text(),"获取验证码")]',
'x://*[contains(text(),"获取验证码")]',
'x://button[contains(text(),"发送验证码")]',
'x://*[contains(text(),"发送验证码")]',
"css:button.btn-code",
"css:.send-code",
"css:.get-code",
"css:button[class*='code']",
"css:.verify-btn",
], timeout=8)
if not send_btn:
raise RuntimeError("未找到「获取验证码」按钮,请根据页面调整选择器")
_click_safe(send_btn)
time.sleep(0.5)
return {"success": True, "message": "输入电话号码成功"}
def input_code_and_submit(
page: ChromiumPage,
code: str,
) -> dict:
"""
步骤二:填写验证码并提交(若存在提交按钮)。
返回 {"success": True, "message": "验证码已填写"}。
"""
code_input = _find_first(page, [
'x://input[@placeholder*="验证码"]',
'x://input[@placeholder*="短信"]',
'x://input[@placeholder*="验证"]',
"css:input[placeholder*='验证码']",
"css:input[placeholder*='短信']",
"css:input.code-input",
"css:input.verify-input",
"css:input[type='text']",
"css:input.inp-txt",
], timeout=8)
if not code_input:
raise RuntimeError("未找到验证码输入框")
code_input.input(code, clear=True)
time.sleep(0.2)
# 若有确认/提交按钮则点击
submit_btn = _find_first(page, [
'x://button[contains(text(),"确认")]',
'x://button[contains(text(),"提交")]',
'x://*[contains(text(),"确认")]',
'x://*[contains(text(),"登录")]',
'x://*[contains(text(),"绑定")]',
"css:button.btn-primary",
"css:button.btn-buy",
"css:.submit-btn",
"css:.confirm-btn",
"css:img.btn-buy",
], timeout=5)
if submit_btn:
_click_safe(submit_btn)
return {"success": True, "message": "验证码已填写"}
def connect_dp_to_tgebrowser(port: int) -> ChromiumPage:
"""通过调试端口连接 TgeBrowser 已启动的浏览器。"""
co = ChromiumOptions().set_local_port(port=port)
return ChromiumPage(addr_or_opts=co)
def connect_dp_to_ws(ws_url: str) -> ChromiumPage:
"""通过 WebSocket 地址连接 TgeBrowser。"""
return ChromiumPage(addr_or_opts=ws_url)

View File

@@ -1,5 +1,6 @@
# 项目依赖
DrissionPage>=4.0.0
requests>=2.28.0
Pillow>=10.0.0
numpy>=1.24.0
fastapi>=0.100.0

93
run_migu_cli.py Normal file
View File

@@ -0,0 +1,93 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
咪咕 miguSM 命令行工具TgeBrowser + DrissionPage
1. 传入手机号 → 新建浏览器、输入手机号、点击发送验证码
2. 传入验证码 → 填写并提交
用法:
set TGEBROWSER_API_KEY=你的API密钥
python run_migu_cli.py --phone 13800138000
python run_migu_cli.py --session-id <上一步返回的session_id> --code 123456
"""
from __future__ import annotations
import argparse
import os
import sys
# 确保 TgeBrowser 客户端和 migu 模块可导入
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from migu_miguSM_dp import (
MIGU_HOME_URL,
connect_dp_to_tgebrowser,
connect_dp_to_ws,
input_code_and_submit,
input_phone_and_send_code,
)
from tgebrowser_client import TgeBrowserClient
def cmd_submit_phone(phone: str, url: str) -> str:
"""步骤一:输入手机号并发送验证码,返回 session_id此处为 env_id 的字符串形式,用于后续填写验证码)。"""
client = TgeBrowserClient()
start_data = client.create_and_start(
browser_name=f"miguSM_{phone[-4:]}",
start_page_url=url,
)
port = start_data.get("port")
ws = start_data.get("ws")
env_id = start_data.get("envId")
if port:
page = connect_dp_to_tgebrowser(port)
elif ws:
page = connect_dp_to_ws(ws)
else:
raise RuntimeError("TgeBrowser 未返回 port 或 ws")
input_phone_and_send_code(page, phone, url=url)
print("输入电话号码成功,请查收短信验证码")
# 保存到全局,供 submit_code 使用CLI 模式只支持单次流程)
cmd_submit_phone._page = page # type: ignore
cmd_submit_phone._client = client # type: ignore
cmd_submit_phone._env_id = env_id # type: ignore
return str(env_id or "ok")
def cmd_submit_code(code: str) -> None:
"""步骤二:填写验证码并提交。"""
page = getattr(cmd_submit_phone, "_page", None)
if not page:
raise RuntimeError("请先执行步骤一python run_migu_cli.py --phone 13800138000")
result = input_code_and_submit(page, code)
print(result.get("message", "验证码已填写"))
def main():
parser = argparse.ArgumentParser(description="咪咕 miguSM 自动化 CLI")
parser.add_argument("--phone", help="手机号码(步骤一)")
parser.add_argument("--code", help="短信验证码(步骤二)")
parser.add_argument("--interactive", "-i", action="store_true", help="步骤一后等待输入验证码(同一进程完成两步)")
parser.add_argument("--url", default=MIGU_HOME_URL, help=f"目标页面,默认 {MIGU_HOME_URL}")
args = parser.parse_args()
if args.phone:
cmd_submit_phone(args.phone, args.url)
if args.interactive:
code = input("请输入短信验证码: ").strip()
if code:
cmd_submit_code(code)
elif args.code:
cmd_submit_code(args.code)
else:
parser.print_help()
print("\n示例:")
print(" python run_migu_cli.py --phone 13800138000 -i # 输入手机号后交互输入验证码")
print(" python run_migu_cli.py --phone 13800138000 # 仅步骤一")
print(" python run_migu_cli.py --code 123456 # 仅步骤二(需在 API 模式或同一进程的步骤一之后)")
if __name__ == "__main__":
main()

159
tgebrowser_client.py Normal file
View File

@@ -0,0 +1,159 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
TgeBrowser API 客户端:用于通过 REST API 新建/启动浏览器环境,获取调试端口。
文档: https://docs.tgebrowser.com/api.html
"""
from __future__ import annotations
import os
from typing import Any, Optional
import requests
# TgeBrowser 本地 API 默认端口
DEFAULT_API_BASE = "http://127.0.0.1:50326"
# API Key写死也可通过环境变量 TGEBROWSER_API_KEY 覆盖)
DEFAULT_API_KEY = "asp_ad11de1727c77a5d514256b3209eec52130c6e5b4f9ccd92"
ENV_API_KEY = "TGEBROWSER_API_KEY"
def get_api_key() -> str:
"""优先从环境变量获取 API Key未配置时使用默认值。"""
key = os.environ.get(ENV_API_KEY)
if key and key.strip():
return key.strip()
return DEFAULT_API_KEY
class TgeBrowserClient:
"""TgeBrowser 本地 API 客户端。"""
def __init__(self, base_url: str = DEFAULT_API_BASE, api_key: Optional[str] = None):
self.base_url = base_url.rstrip("/")
self.api_key = api_key or get_api_key()
self._headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json",
}
def _post(self, path: str, json: Optional[dict] = None) -> dict:
resp = requests.post(
f"{self.base_url}{path}",
headers=self._headers,
json=json or {},
timeout=30,
)
resp.raise_for_status()
data = resp.json()
if not data.get("success", False):
msg = data.get("message", "未知错误")
raise RuntimeError(f"TgeBrowser API 失败: {msg}")
return data
def _get(self, path: str) -> dict:
resp = requests.get(
f"{self.base_url}{path}",
headers=self._headers,
timeout=30,
)
resp.raise_for_status()
data = resp.json()
if not data.get("success", False):
msg = data.get("message", "未知错误")
raise RuntimeError(f"TgeBrowser API 失败: {msg}")
return data
def status(self) -> dict:
"""检查 API 服务状态。"""
return self._get("/api/status")
def create_browser(
self,
browser_name: str = "miguSM_automation",
start_page_url: Optional[str] = None,
**kwargs: Any,
) -> dict:
"""
创建新的浏览器环境。
返回 data.envId 用于后续 start。
"""
# 使用 TgeBrowser 文档要求的完整结构,避免 400
start_page_value = [start_page_url] if start_page_url else []
start_page_mode = "custom" if start_page_value else "none"
# 指定手机端指纹,随机生成手机 UA
mobile_fingerprint = {
"os": "Android",
"platformVersion": 12,
}
payload = {
"browserName": browser_name,
"proxy": {"protocol": "direct"},
"fingerprint": mobile_fingerprint,
"startInfo": {
"startPage": {"mode": start_page_mode, "value": start_page_value},
"otherConfig": {"openConfigPage": False, "checkPage": False},
},
**kwargs,
}
data = self._post("/api/browser/create", json=payload)
return data.get("data", {})
def start_browser(
self,
env_id: Optional[int] = None,
user_index: Optional[int] = None,
port: Optional[int] = None,
**kwargs: Any,
) -> dict:
"""
启动浏览器环境。
返回 data 含 ws、port、http 等,用于 DrissionPage 连接。
"""
payload: dict[str, Any] = {}
if env_id is not None:
payload["envId"] = env_id
elif user_index is not None:
payload["userIndex"] = user_index
else:
raise ValueError("必须指定 envId 或 userIndex")
if port is not None:
payload["port"] = port
payload.update(kwargs)
data = self._post("/api/browser/start", json=payload)
return data.get("data", {})
def stop_browser(self, env_id: Optional[int] = None, user_index: Optional[int] = None) -> dict:
"""停止浏览器环境。"""
payload: dict[str, Any] = {}
if env_id is not None:
payload["envId"] = env_id
elif user_index is not None:
payload["userIndex"] = user_index
else:
raise ValueError("必须指定 envId 或 userIndex")
return self._post("/api/browser/stop", json=payload)
def create_and_start(
self,
browser_name: str = "miguSM_automation",
start_page_url: Optional[str] = None,
) -> dict:
"""
创建并启动新浏览器,每次调用都会新建一个环境。
返回含 ws、port、envId 等,供 DrissionPage 连接。
"""
create_data = self.create_browser(
browser_name=browser_name,
start_page_url=start_page_url,
)
env_id = create_data.get("envId")
if env_id is None:
raise RuntimeError("创建浏览器失败:未返回 envId")
start_data = self.start_browser(env_id=env_id)
start_data["envId"] = env_id
return start_data