Commit ·
de31fe0
1
Parent(s): ea88387
feat: 无头打码与YesCaptcha打码
Browse files- Dockerfile +23 -0
- README.md +7 -4
- config/setting.toml +5 -0
- config/setting_warp.toml +5 -0
- requirements.txt +2 -1
- src/api/admin.py +3 -0
- src/core/config.py +35 -0
- src/core/database.py +105 -3
- src/core/models.py +12 -0
- src/main.py +17 -0
- src/services/browser_captcha.py +245 -0
- src/services/flow_client.py +104 -5
Dockerfile
CHANGED
|
@@ -2,9 +2,32 @@ FROM python:3.11-slim
|
|
| 2 |
|
| 3 |
WORKDIR /app
|
| 4 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
COPY requirements.txt .
|
| 6 |
RUN pip install --no-cache-dir -r requirements.txt
|
| 7 |
|
|
|
|
|
|
|
|
|
|
| 8 |
COPY . .
|
| 9 |
|
| 10 |
EXPOSE 8000
|
|
|
|
| 2 |
|
| 3 |
WORKDIR /app
|
| 4 |
|
| 5 |
+
# 安装 Playwright 所需的系统依赖
|
| 6 |
+
RUN apt-get update && apt-get install -y \
|
| 7 |
+
libnss3 \
|
| 8 |
+
libnspr4 \
|
| 9 |
+
libatk1.0-0 \
|
| 10 |
+
libatk-bridge2.0-0 \
|
| 11 |
+
libcups2 \
|
| 12 |
+
libdrm2 \
|
| 13 |
+
libxkbcommon0 \
|
| 14 |
+
libxcomposite1 \
|
| 15 |
+
libxdamage1 \
|
| 16 |
+
libxfixes3 \
|
| 17 |
+
libxrandr2 \
|
| 18 |
+
libgbm1 \
|
| 19 |
+
libasound2 \
|
| 20 |
+
libpango-1.0-0 \
|
| 21 |
+
libcairo2 \
|
| 22 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 23 |
+
|
| 24 |
+
# 安装 Python 依赖
|
| 25 |
COPY requirements.txt .
|
| 26 |
RUN pip install --no-cache-dir -r requirements.txt
|
| 27 |
|
| 28 |
+
# 安装 Playwright 浏览器(仅 Chromium)
|
| 29 |
+
RUN playwright install chromium --with-deps
|
| 30 |
+
|
| 31 |
COPY . .
|
| 32 |
|
| 33 |
EXPOSE 8000
|
README.md
CHANGED
|
@@ -21,6 +21,7 @@
|
|
| 21 |
- 🚀 **负载均衡** - 多 Token 轮询和并发控制
|
| 22 |
- 🌐 **代理支持** - 支持 HTTP/SOCKS5 代理
|
| 23 |
- 📱 **Web 管理界面** - 直观的 Token 和配置管理
|
|
|
|
| 24 |
|
| 25 |
## 🚀 快速开始
|
| 26 |
|
|
@@ -29,6 +30,9 @@
|
|
| 29 |
- Docker 和 Docker Compose(推荐)
|
| 30 |
- 或 Python 3.8+
|
| 31 |
|
|
|
|
|
|
|
|
|
|
| 32 |
### 方式一:Docker 部署(推荐)
|
| 33 |
|
| 34 |
#### 标准模式(不使用代理)
|
|
@@ -80,13 +84,11 @@ python main.py
|
|
| 80 |
|
| 81 |
### 首次访问
|
| 82 |
|
| 83 |
-
服务启动后,访问管理后台: **http://localhost:8000**
|
| 84 |
|
| 85 |
- **用户名**: `admin`
|
| 86 |
- **密码**: `admin`
|
| 87 |
|
| 88 |
-
⚠️ **重要**: 首次登录后请立即修改密码!
|
| 89 |
-
|
| 90 |
## 📋 支持的模型
|
| 91 |
|
| 92 |
### 图片生成
|
|
@@ -246,6 +248,8 @@ curl -X POST "http://localhost:8000/v1/chat/completions" \
|
|
| 246 |
|
| 247 |
## 🙏 致谢
|
| 248 |
|
|
|
|
|
|
|
| 249 |
感谢所有贡献者和使用者的支持!
|
| 250 |
|
| 251 |
---
|
|
@@ -253,7 +257,6 @@ curl -X POST "http://localhost:8000/v1/chat/completions" \
|
|
| 253 |
## 📞 联系方式
|
| 254 |
|
| 255 |
- 提交 Issue:[GitHub Issues](https://github.com/TheSmallHanCat/flow2api/issues)
|
| 256 |
-
- 讨论:[GitHub Discussions](https://github.com/TheSmallHanCat/flow2api/discussions)
|
| 257 |
|
| 258 |
---
|
| 259 |
|
|
|
|
| 21 |
- 🚀 **负载均衡** - 多 Token 轮询和并发控制
|
| 22 |
- 🌐 **代理支持** - 支持 HTTP/SOCKS5 代理
|
| 23 |
- 📱 **Web 管理界面** - 直观的 Token 和配置管理
|
| 24 |
+
- 🎨 **图片生成连续对话**
|
| 25 |
|
| 26 |
## 🚀 快速开始
|
| 27 |
|
|
|
|
| 30 |
- Docker 和 Docker Compose(推荐)
|
| 31 |
- 或 Python 3.8+
|
| 32 |
|
| 33 |
+
- 由于Flow增加了额外的验证码,你可以自行选择使用浏览器打码或第三发打码:
|
| 34 |
+
注册[YesCaptcha](https://yescaptcha.com/i/13Xd8K)并获取api key,将其填入系统配置页面```YesCaptcha API密钥```区域
|
| 35 |
+
|
| 36 |
### 方式一:Docker 部署(推荐)
|
| 37 |
|
| 38 |
#### 标准模式(不使用代理)
|
|
|
|
| 84 |
|
| 85 |
### 首次访问
|
| 86 |
|
| 87 |
+
服务启动后,访问管理后台: **http://localhost:8000**,首次登录后请立即修改密码!
|
| 88 |
|
| 89 |
- **用户名**: `admin`
|
| 90 |
- **密码**: `admin`
|
| 91 |
|
|
|
|
|
|
|
| 92 |
## 📋 支持的模型
|
| 93 |
|
| 94 |
### 图片生成
|
|
|
|
| 248 |
|
| 249 |
## 🙏 致谢
|
| 250 |
|
| 251 |
+
- [PearNoDec](https://github.com/PearNoDec) 提供的YesCaptcha打码方案
|
| 252 |
+
- [raomaiping](https://github.com/raomaiping) 提供的无头打码方案
|
| 253 |
感谢所有贡献者和使用者的支持!
|
| 254 |
|
| 255 |
---
|
|
|
|
| 257 |
## 📞 联系方式
|
| 258 |
|
| 259 |
- 提交 Issue:[GitHub Issues](https://github.com/TheSmallHanCat/flow2api/issues)
|
|
|
|
| 260 |
|
| 261 |
---
|
| 262 |
|
config/setting.toml
CHANGED
|
@@ -35,3 +35,8 @@ error_ban_threshold = 3
|
|
| 35 |
enabled = false
|
| 36 |
timeout = 7200 # 缓存超时时间(秒), 默认2小时
|
| 37 |
base_url = "" # 缓存文件访问的基础URL, 留空则使用服务器地址
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
enabled = false
|
| 36 |
timeout = 7200 # 缓存超时时间(秒), 默认2小时
|
| 37 |
base_url = "" # 缓存文件访问的基础URL, 留空则使用服务器地址
|
| 38 |
+
|
| 39 |
+
[captcha]
|
| 40 |
+
captcha_method = "browser" # 打码方式: yescaptcha 或 browser
|
| 41 |
+
yescaptcha_api_key = "" # YesCaptcha API密钥
|
| 42 |
+
yescaptcha_base_url = "https://api.yescaptcha.com"
|
config/setting_warp.toml
CHANGED
|
@@ -35,3 +35,8 @@ error_ban_threshold = 3
|
|
| 35 |
enabled = false
|
| 36 |
timeout = 7200 # 缓存超时时间(秒), 默认2小时
|
| 37 |
base_url = "" # 缓存文件访问的基础URL, 留空则使用服务器地址
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
enabled = false
|
| 36 |
timeout = 7200 # 缓存超时时间(秒), 默认2小时
|
| 37 |
base_url = "" # 缓存文件访问的基础URL, 留空则使用服务器地址
|
| 38 |
+
|
| 39 |
+
[captcha]
|
| 40 |
+
captcha_method = "browser" # 打码方式: yescaptcha 或 browser
|
| 41 |
+
yescaptcha_api_key = "" # YesCaptcha API密钥
|
| 42 |
+
yescaptcha_base_url = "https://api.yescaptcha.com"
|
requirements.txt
CHANGED
|
@@ -2,8 +2,9 @@ fastapi==0.119.0
|
|
| 2 |
uvicorn[standard]==0.32.1
|
| 3 |
aiosqlite==0.20.0
|
| 4 |
pydantic==2.10.4
|
| 5 |
-
curl-cffi
|
| 6 |
tomli==2.2.1
|
| 7 |
bcrypt==4.2.1
|
| 8 |
python-multipart==0.0.20
|
| 9 |
python-dateutil==2.8.2
|
|
|
|
|
|
| 2 |
uvicorn[standard]==0.32.1
|
| 3 |
aiosqlite==0.20.0
|
| 4 |
pydantic==2.10.4
|
| 5 |
+
curl-cffi==0.7.3
|
| 6 |
tomli==2.2.1
|
| 7 |
bcrypt==4.2.1
|
| 8 |
python-multipart==0.0.20
|
| 9 |
python-dateutil==2.8.2
|
| 10 |
+
playwright==1.48.0
|
src/api/admin.py
CHANGED
|
@@ -839,10 +839,12 @@ async def update_captcha_config(
|
|
| 839 |
token: str = Depends(verify_admin_token)
|
| 840 |
):
|
| 841 |
"""Update captcha configuration"""
|
|
|
|
| 842 |
yescaptcha_api_key = request.get("yescaptcha_api_key")
|
| 843 |
yescaptcha_base_url = request.get("yescaptcha_base_url")
|
| 844 |
|
| 845 |
await db.update_captcha_config(
|
|
|
|
| 846 |
yescaptcha_api_key=yescaptcha_api_key,
|
| 847 |
yescaptcha_base_url=yescaptcha_base_url
|
| 848 |
)
|
|
@@ -858,6 +860,7 @@ async def get_captcha_config(token: str = Depends(verify_admin_token)):
|
|
| 858 |
"""Get captcha configuration"""
|
| 859 |
captcha_config = await db.get_captcha_config()
|
| 860 |
return {
|
|
|
|
| 861 |
"yescaptcha_api_key": captcha_config.yescaptcha_api_key,
|
| 862 |
"yescaptcha_base_url": captcha_config.yescaptcha_base_url
|
| 863 |
}
|
|
|
|
| 839 |
token: str = Depends(verify_admin_token)
|
| 840 |
):
|
| 841 |
"""Update captcha configuration"""
|
| 842 |
+
captcha_method = request.get("captcha_method")
|
| 843 |
yescaptcha_api_key = request.get("yescaptcha_api_key")
|
| 844 |
yescaptcha_base_url = request.get("yescaptcha_base_url")
|
| 845 |
|
| 846 |
await db.update_captcha_config(
|
| 847 |
+
captcha_method=captcha_method,
|
| 848 |
yescaptcha_api_key=yescaptcha_api_key,
|
| 849 |
yescaptcha_base_url=yescaptcha_base_url
|
| 850 |
)
|
|
|
|
| 860 |
"""Get captcha configuration"""
|
| 861 |
captcha_config = await db.get_captcha_config()
|
| 862 |
return {
|
| 863 |
+
"captcha_method": captcha_config.captcha_method,
|
| 864 |
"yescaptcha_api_key": captcha_config.yescaptcha_api_key,
|
| 865 |
"yescaptcha_base_url": captcha_config.yescaptcha_base_url
|
| 866 |
}
|
src/core/config.py
CHANGED
|
@@ -179,5 +179,40 @@ class Config:
|
|
| 179 |
self._config["cache"] = {}
|
| 180 |
self._config["cache"]["base_url"] = base_url
|
| 181 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
# Global config instance
|
| 183 |
config = Config()
|
|
|
|
| 179 |
self._config["cache"] = {}
|
| 180 |
self._config["cache"]["base_url"] = base_url
|
| 181 |
|
| 182 |
+
# Captcha configuration
|
| 183 |
+
@property
|
| 184 |
+
def captcha_method(self) -> str:
|
| 185 |
+
"""Get captcha method"""
|
| 186 |
+
return self._config.get("captcha", {}).get("captcha_method", "yescaptcha")
|
| 187 |
+
|
| 188 |
+
def set_captcha_method(self, method: str):
|
| 189 |
+
"""Set captcha method"""
|
| 190 |
+
if "captcha" not in self._config:
|
| 191 |
+
self._config["captcha"] = {}
|
| 192 |
+
self._config["captcha"]["captcha_method"] = method
|
| 193 |
+
|
| 194 |
+
@property
|
| 195 |
+
def yescaptcha_api_key(self) -> str:
|
| 196 |
+
"""Get YesCaptcha API key"""
|
| 197 |
+
return self._config.get("captcha", {}).get("yescaptcha_api_key", "")
|
| 198 |
+
|
| 199 |
+
def set_yescaptcha_api_key(self, api_key: str):
|
| 200 |
+
"""Set YesCaptcha API key"""
|
| 201 |
+
if "captcha" not in self._config:
|
| 202 |
+
self._config["captcha"] = {}
|
| 203 |
+
self._config["captcha"]["yescaptcha_api_key"] = api_key
|
| 204 |
+
|
| 205 |
+
@property
|
| 206 |
+
def yescaptcha_base_url(self) -> str:
|
| 207 |
+
"""Get YesCaptcha base URL"""
|
| 208 |
+
return self._config.get("captcha", {}).get("yescaptcha_base_url", "https://api.yescaptcha.com")
|
| 209 |
+
|
| 210 |
+
def set_yescaptcha_base_url(self, base_url: str):
|
| 211 |
+
"""Set YesCaptcha base URL"""
|
| 212 |
+
if "captcha" not in self._config:
|
| 213 |
+
self._config["captcha"] = {}
|
| 214 |
+
self._config["captcha"]["yescaptcha_base_url"] = base_url
|
| 215 |
+
|
| 216 |
+
|
| 217 |
# Global config instance
|
| 218 |
config = Config()
|
src/core/database.py
CHANGED
|
@@ -4,7 +4,7 @@ import json
|
|
| 4 |
from datetime import datetime
|
| 5 |
from typing import Optional, List
|
| 6 |
from pathlib import Path
|
| 7 |
-
from .models import Token, TokenStats, Task, RequestLog, AdminConfig, ProxyConfig, GenerationConfig, CacheConfig, Project
|
| 8 |
|
| 9 |
|
| 10 |
class Database:
|
|
@@ -148,6 +148,25 @@ class Database:
|
|
| 148 |
VALUES (1, ?, ?, ?, ?)
|
| 149 |
""", (debug_enabled, log_requests, log_responses, mask_token))
|
| 150 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
async def check_and_migrate_db(self, config_dict: dict = None):
|
| 152 |
"""Check database integrity and perform migrations if needed
|
| 153 |
|
|
@@ -179,6 +198,22 @@ class Database:
|
|
| 179 |
)
|
| 180 |
""")
|
| 181 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
# ========== Step 2: Add missing columns to existing tables ==========
|
| 183 |
# Check and add missing columns to tokens table
|
| 184 |
if await self._table_exists(db, "tokens"):
|
|
@@ -234,8 +269,8 @@ class Database:
|
|
| 234 |
|
| 235 |
# ========== Step 3: Ensure all config tables have default rows ==========
|
| 236 |
# Note: This will NOT overwrite existing config rows
|
| 237 |
-
# It only ensures missing rows are created with default values
|
| 238 |
-
await self._ensure_config_rows(db, config_dict=
|
| 239 |
|
| 240 |
await db.commit()
|
| 241 |
print("Database migration check completed.")
|
|
@@ -395,6 +430,20 @@ class Database:
|
|
| 395 |
)
|
| 396 |
""")
|
| 397 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 398 |
# Create indexes
|
| 399 |
await db.execute("CREATE INDEX IF NOT EXISTS idx_task_id ON tasks(task_id)")
|
| 400 |
await db.execute("CREATE INDEX IF NOT EXISTS idx_token_st ON tokens(st)")
|
|
@@ -944,6 +993,13 @@ class Database:
|
|
| 944 |
if debug_config:
|
| 945 |
config.set_debug_enabled(debug_config.enabled)
|
| 946 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 947 |
# Cache config operations
|
| 948 |
async def get_cache_config(self) -> CacheConfig:
|
| 949 |
"""Get cache configuration"""
|
|
@@ -1046,3 +1102,49 @@ class Database:
|
|
| 1046 |
""", (new_enabled, new_log_requests, new_log_responses, new_mask_token))
|
| 1047 |
|
| 1048 |
await db.commit()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
from datetime import datetime
|
| 5 |
from typing import Optional, List
|
| 6 |
from pathlib import Path
|
| 7 |
+
from .models import Token, TokenStats, Task, RequestLog, AdminConfig, ProxyConfig, GenerationConfig, CacheConfig, Project, CaptchaConfig
|
| 8 |
|
| 9 |
|
| 10 |
class Database:
|
|
|
|
| 148 |
VALUES (1, ?, ?, ?, ?)
|
| 149 |
""", (debug_enabled, log_requests, log_responses, mask_token))
|
| 150 |
|
| 151 |
+
# Ensure captcha_config has a row
|
| 152 |
+
cursor = await db.execute("SELECT COUNT(*) FROM captcha_config")
|
| 153 |
+
count = await cursor.fetchone()
|
| 154 |
+
if count[0] == 0:
|
| 155 |
+
captcha_method = "browser"
|
| 156 |
+
yescaptcha_api_key = ""
|
| 157 |
+
yescaptcha_base_url = "https://api.yescaptcha.com"
|
| 158 |
+
|
| 159 |
+
if config_dict:
|
| 160 |
+
captcha_config = config_dict.get("captcha", {})
|
| 161 |
+
captcha_method = captcha_config.get("captcha_method", "browser")
|
| 162 |
+
yescaptcha_api_key = captcha_config.get("yescaptcha_api_key", "")
|
| 163 |
+
yescaptcha_base_url = captcha_config.get("yescaptcha_base_url", "https://api.yescaptcha.com")
|
| 164 |
+
|
| 165 |
+
await db.execute("""
|
| 166 |
+
INSERT INTO captcha_config (id, captcha_method, yescaptcha_api_key, yescaptcha_base_url)
|
| 167 |
+
VALUES (1, ?, ?, ?)
|
| 168 |
+
""", (captcha_method, yescaptcha_api_key, yescaptcha_base_url))
|
| 169 |
+
|
| 170 |
async def check_and_migrate_db(self, config_dict: dict = None):
|
| 171 |
"""Check database integrity and perform migrations if needed
|
| 172 |
|
|
|
|
| 198 |
)
|
| 199 |
""")
|
| 200 |
|
| 201 |
+
# Check and create captcha_config table if missing
|
| 202 |
+
if not await self._table_exists(db, "captcha_config"):
|
| 203 |
+
print(" ✓ Creating missing table: captcha_config")
|
| 204 |
+
await db.execute("""
|
| 205 |
+
CREATE TABLE captcha_config (
|
| 206 |
+
id INTEGER PRIMARY KEY DEFAULT 1,
|
| 207 |
+
captcha_method TEXT DEFAULT 'browser',
|
| 208 |
+
yescaptcha_api_key TEXT DEFAULT '',
|
| 209 |
+
yescaptcha_base_url TEXT DEFAULT 'https://api.yescaptcha.com',
|
| 210 |
+
website_key TEXT DEFAULT '6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV',
|
| 211 |
+
page_action TEXT DEFAULT 'FLOW_GENERATION',
|
| 212 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 213 |
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
| 214 |
+
)
|
| 215 |
+
""")
|
| 216 |
+
|
| 217 |
# ========== Step 2: Add missing columns to existing tables ==========
|
| 218 |
# Check and add missing columns to tokens table
|
| 219 |
if await self._table_exists(db, "tokens"):
|
|
|
|
| 269 |
|
| 270 |
# ========== Step 3: Ensure all config tables have default rows ==========
|
| 271 |
# Note: This will NOT overwrite existing config rows
|
| 272 |
+
# It only ensures missing rows are created with default values from setting.toml
|
| 273 |
+
await self._ensure_config_rows(db, config_dict=config_dict)
|
| 274 |
|
| 275 |
await db.commit()
|
| 276 |
print("Database migration check completed.")
|
|
|
|
| 430 |
)
|
| 431 |
""")
|
| 432 |
|
| 433 |
+
# Captcha config table
|
| 434 |
+
await db.execute("""
|
| 435 |
+
CREATE TABLE IF NOT EXISTS captcha_config (
|
| 436 |
+
id INTEGER PRIMARY KEY DEFAULT 1,
|
| 437 |
+
captcha_method TEXT DEFAULT 'browser',
|
| 438 |
+
yescaptcha_api_key TEXT DEFAULT '',
|
| 439 |
+
yescaptcha_base_url TEXT DEFAULT 'https://api.yescaptcha.com',
|
| 440 |
+
website_key TEXT DEFAULT '6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV',
|
| 441 |
+
page_action TEXT DEFAULT 'FLOW_GENERATION',
|
| 442 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 443 |
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
| 444 |
+
)
|
| 445 |
+
""")
|
| 446 |
+
|
| 447 |
# Create indexes
|
| 448 |
await db.execute("CREATE INDEX IF NOT EXISTS idx_task_id ON tasks(task_id)")
|
| 449 |
await db.execute("CREATE INDEX IF NOT EXISTS idx_token_st ON tokens(st)")
|
|
|
|
| 993 |
if debug_config:
|
| 994 |
config.set_debug_enabled(debug_config.enabled)
|
| 995 |
|
| 996 |
+
# Reload captcha config
|
| 997 |
+
captcha_config = await self.get_captcha_config()
|
| 998 |
+
if captcha_config:
|
| 999 |
+
config.set_captcha_method(captcha_config.captcha_method)
|
| 1000 |
+
config.set_yescaptcha_api_key(captcha_config.yescaptcha_api_key)
|
| 1001 |
+
config.set_yescaptcha_base_url(captcha_config.yescaptcha_base_url)
|
| 1002 |
+
|
| 1003 |
# Cache config operations
|
| 1004 |
async def get_cache_config(self) -> CacheConfig:
|
| 1005 |
"""Get cache configuration"""
|
|
|
|
| 1102 |
""", (new_enabled, new_log_requests, new_log_responses, new_mask_token))
|
| 1103 |
|
| 1104 |
await db.commit()
|
| 1105 |
+
|
| 1106 |
+
# Captcha config operations
|
| 1107 |
+
async def get_captcha_config(self) -> CaptchaConfig:
|
| 1108 |
+
"""Get captcha configuration"""
|
| 1109 |
+
async with aiosqlite.connect(self.db_path) as db:
|
| 1110 |
+
db.row_factory = aiosqlite.Row
|
| 1111 |
+
cursor = await db.execute("SELECT * FROM captcha_config WHERE id = 1")
|
| 1112 |
+
row = await cursor.fetchone()
|
| 1113 |
+
if row:
|
| 1114 |
+
return CaptchaConfig(**dict(row))
|
| 1115 |
+
return CaptchaConfig()
|
| 1116 |
+
|
| 1117 |
+
async def update_captcha_config(
|
| 1118 |
+
self,
|
| 1119 |
+
captcha_method: str = None,
|
| 1120 |
+
yescaptcha_api_key: str = None,
|
| 1121 |
+
yescaptcha_base_url: str = None
|
| 1122 |
+
):
|
| 1123 |
+
"""Update captcha configuration"""
|
| 1124 |
+
async with aiosqlite.connect(self.db_path) as db:
|
| 1125 |
+
db.row_factory = aiosqlite.Row
|
| 1126 |
+
cursor = await db.execute("SELECT * FROM captcha_config WHERE id = 1")
|
| 1127 |
+
row = await cursor.fetchone()
|
| 1128 |
+
|
| 1129 |
+
if row:
|
| 1130 |
+
current = dict(row)
|
| 1131 |
+
new_method = captcha_method if captcha_method is not None else current.get("captcha_method", "yescaptcha")
|
| 1132 |
+
new_api_key = yescaptcha_api_key if yescaptcha_api_key is not None else current.get("yescaptcha_api_key", "")
|
| 1133 |
+
new_base_url = yescaptcha_base_url if yescaptcha_base_url is not None else current.get("yescaptcha_base_url", "https://api.yescaptcha.com")
|
| 1134 |
+
|
| 1135 |
+
await db.execute("""
|
| 1136 |
+
UPDATE captcha_config
|
| 1137 |
+
SET captcha_method = ?, yescaptcha_api_key = ?, yescaptcha_base_url = ?, updated_at = CURRENT_TIMESTAMP
|
| 1138 |
+
WHERE id = 1
|
| 1139 |
+
""", (new_method, new_api_key, new_base_url))
|
| 1140 |
+
else:
|
| 1141 |
+
new_method = captcha_method if captcha_method is not None else "yescaptcha"
|
| 1142 |
+
new_api_key = yescaptcha_api_key if yescaptcha_api_key is not None else ""
|
| 1143 |
+
new_base_url = yescaptcha_base_url if yescaptcha_base_url is not None else "https://api.yescaptcha.com"
|
| 1144 |
+
|
| 1145 |
+
await db.execute("""
|
| 1146 |
+
INSERT INTO captcha_config (id, captcha_method, yescaptcha_api_key, yescaptcha_base_url)
|
| 1147 |
+
VALUES (1, ?, ?, ?)
|
| 1148 |
+
""", (new_method, new_api_key, new_base_url))
|
| 1149 |
+
|
| 1150 |
+
await db.commit()
|
src/core/models.py
CHANGED
|
@@ -144,6 +144,18 @@ class DebugConfig(BaseModel):
|
|
| 144 |
updated_at: Optional[datetime] = None
|
| 145 |
|
| 146 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
# OpenAI Compatible Request Models
|
| 148 |
class ChatMessage(BaseModel):
|
| 149 |
"""Chat message"""
|
|
|
|
| 144 |
updated_at: Optional[datetime] = None
|
| 145 |
|
| 146 |
|
| 147 |
+
class CaptchaConfig(BaseModel):
|
| 148 |
+
"""Captcha configuration"""
|
| 149 |
+
id: int = 1
|
| 150 |
+
captcha_method: str = "browser" # yescaptcha 或 browser
|
| 151 |
+
yescaptcha_api_key: str = ""
|
| 152 |
+
yescaptcha_base_url: str = "https://api.yescaptcha.com"
|
| 153 |
+
website_key: str = "6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV"
|
| 154 |
+
page_action: str = "FLOW_GENERATION"
|
| 155 |
+
created_at: Optional[datetime] = None
|
| 156 |
+
updated_at: Optional[datetime] = None
|
| 157 |
+
|
| 158 |
+
|
| 159 |
# OpenAI Compatible Request Models
|
| 160 |
class ChatMessage(BaseModel):
|
| 161 |
"""Chat message"""
|
src/main.py
CHANGED
|
@@ -66,6 +66,19 @@ async def lifespan(app: FastAPI):
|
|
| 66 |
debug_config = await db.get_debug_config()
|
| 67 |
config.set_debug_enabled(debug_config.enabled)
|
| 68 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
# Initialize concurrency manager
|
| 70 |
tokens = await token_manager.get_all_tokens()
|
| 71 |
await concurrency_manager.initialize(tokens)
|
|
@@ -106,6 +119,10 @@ async def lifespan(app: FastAPI):
|
|
| 106 |
await auto_unban_task_handle
|
| 107 |
except asyncio.CancelledError:
|
| 108 |
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
print("✓ File cache cleanup task stopped")
|
| 110 |
print("✓ 429 auto-unban task stopped")
|
| 111 |
|
|
|
|
| 66 |
debug_config = await db.get_debug_config()
|
| 67 |
config.set_debug_enabled(debug_config.enabled)
|
| 68 |
|
| 69 |
+
# Load captcha configuration from database
|
| 70 |
+
captcha_config = await db.get_captcha_config()
|
| 71 |
+
config.set_captcha_method(captcha_config.captcha_method)
|
| 72 |
+
config.set_yescaptcha_api_key(captcha_config.yescaptcha_api_key)
|
| 73 |
+
config.set_yescaptcha_base_url(captcha_config.yescaptcha_base_url)
|
| 74 |
+
|
| 75 |
+
# Initialize browser captcha service if needed
|
| 76 |
+
browser_service = None
|
| 77 |
+
if captcha_config.captcha_method == "browser":
|
| 78 |
+
from .services.browser_captcha import BrowserCaptchaService
|
| 79 |
+
browser_service = await BrowserCaptchaService.get_instance(proxy_manager)
|
| 80 |
+
print("✓ Browser captcha service initialized (headless mode)")
|
| 81 |
+
|
| 82 |
# Initialize concurrency manager
|
| 83 |
tokens = await token_manager.get_all_tokens()
|
| 84 |
await concurrency_manager.initialize(tokens)
|
|
|
|
| 119 |
await auto_unban_task_handle
|
| 120 |
except asyncio.CancelledError:
|
| 121 |
pass
|
| 122 |
+
# Close browser if initialized
|
| 123 |
+
if browser_service:
|
| 124 |
+
await browser_service.close()
|
| 125 |
+
print("✓ Browser captcha service closed")
|
| 126 |
print("✓ File cache cleanup task stopped")
|
| 127 |
print("✓ 429 auto-unban task stopped")
|
| 128 |
|
src/services/browser_captcha.py
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
浏览器自动化获取 reCAPTCHA token
|
| 3 |
+
使用 Playwright 访问页面并执行 reCAPTCHA 验证
|
| 4 |
+
"""
|
| 5 |
+
import asyncio
|
| 6 |
+
import time
|
| 7 |
+
from typing import Optional
|
| 8 |
+
from playwright.async_api import async_playwright, Browser, BrowserContext
|
| 9 |
+
|
| 10 |
+
from ..core.logger import debug_logger
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class BrowserCaptchaService:
|
| 14 |
+
"""浏览器自动化获取 reCAPTCHA token(单例模式)"""
|
| 15 |
+
|
| 16 |
+
_instance: Optional['BrowserCaptchaService'] = None
|
| 17 |
+
_lock = asyncio.Lock()
|
| 18 |
+
|
| 19 |
+
def __init__(self, proxy_manager=None):
|
| 20 |
+
"""初始化服务(始终使用无头模式)"""
|
| 21 |
+
self.headless = True # 始终无头
|
| 22 |
+
self.playwright = None
|
| 23 |
+
self.browser: Optional[Browser] = None
|
| 24 |
+
self._initialized = False
|
| 25 |
+
self.website_key = "6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV"
|
| 26 |
+
self.proxy_manager = proxy_manager
|
| 27 |
+
|
| 28 |
+
@classmethod
|
| 29 |
+
async def get_instance(cls, proxy_manager=None) -> 'BrowserCaptchaService':
|
| 30 |
+
"""获取单例实例"""
|
| 31 |
+
if cls._instance is None:
|
| 32 |
+
async with cls._lock:
|
| 33 |
+
if cls._instance is None:
|
| 34 |
+
cls._instance = cls(proxy_manager)
|
| 35 |
+
await cls._instance.initialize()
|
| 36 |
+
return cls._instance
|
| 37 |
+
|
| 38 |
+
async def initialize(self):
|
| 39 |
+
"""初始化浏览器(启动一次)"""
|
| 40 |
+
if self._initialized:
|
| 41 |
+
return
|
| 42 |
+
|
| 43 |
+
try:
|
| 44 |
+
# 获取代理配置
|
| 45 |
+
proxy_url = None
|
| 46 |
+
if self.proxy_manager:
|
| 47 |
+
proxy_url = await self.proxy_manager.get_proxy_url()
|
| 48 |
+
|
| 49 |
+
debug_logger.log_info(f"[BrowserCaptcha] 正在启动浏览器... (proxy={proxy_url or 'None'})")
|
| 50 |
+
self.playwright = await async_playwright().start()
|
| 51 |
+
|
| 52 |
+
# 配置浏览器启动参数
|
| 53 |
+
launch_options = {
|
| 54 |
+
'headless': self.headless,
|
| 55 |
+
'args': [
|
| 56 |
+
'--disable-blink-features=AutomationControlled',
|
| 57 |
+
'--disable-dev-shm-usage',
|
| 58 |
+
'--no-sandbox',
|
| 59 |
+
'--disable-setuid-sandbox'
|
| 60 |
+
]
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
# 如果有代理,添加代理配置
|
| 64 |
+
if proxy_url:
|
| 65 |
+
launch_options['proxy'] = {'server': proxy_url}
|
| 66 |
+
|
| 67 |
+
self.browser = await self.playwright.chromium.launch(**launch_options)
|
| 68 |
+
self._initialized = True
|
| 69 |
+
debug_logger.log_info(f"[BrowserCaptcha] ✅ 浏览器已启动 (headless={self.headless}, proxy={proxy_url or 'None'})")
|
| 70 |
+
except Exception as e:
|
| 71 |
+
debug_logger.log_error(f"[BrowserCaptcha] ❌ 浏览器启动失败: {str(e)}")
|
| 72 |
+
raise
|
| 73 |
+
|
| 74 |
+
async def get_token(self, project_id: str) -> Optional[str]:
|
| 75 |
+
"""获取 reCAPTCHA token
|
| 76 |
+
|
| 77 |
+
Args:
|
| 78 |
+
project_id: Flow项目ID
|
| 79 |
+
|
| 80 |
+
Returns:
|
| 81 |
+
reCAPTCHA token字符串,如果获取失败返回None
|
| 82 |
+
"""
|
| 83 |
+
if not self._initialized:
|
| 84 |
+
await self.initialize()
|
| 85 |
+
|
| 86 |
+
start_time = time.time()
|
| 87 |
+
context = None
|
| 88 |
+
|
| 89 |
+
try:
|
| 90 |
+
# 创建新的上下文
|
| 91 |
+
context = await self.browser.new_context(
|
| 92 |
+
viewport={'width': 1920, 'height': 1080},
|
| 93 |
+
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
| 94 |
+
locale='en-US',
|
| 95 |
+
timezone_id='America/New_York'
|
| 96 |
+
)
|
| 97 |
+
page = await context.new_page()
|
| 98 |
+
|
| 99 |
+
website_url = f"https://labs.google/fx/tools/flow/project/{project_id}"
|
| 100 |
+
|
| 101 |
+
debug_logger.log_info(f"[BrowserCaptcha] 访问页面: {website_url}")
|
| 102 |
+
|
| 103 |
+
# 访问页面
|
| 104 |
+
try:
|
| 105 |
+
await page.goto(website_url, wait_until="domcontentloaded", timeout=30000)
|
| 106 |
+
except Exception as e:
|
| 107 |
+
debug_logger.log_warning(f"[BrowserCaptcha] 页面加载超时或失败: {str(e)}")
|
| 108 |
+
|
| 109 |
+
# 检查并注入 reCAPTCHA v3 脚本
|
| 110 |
+
debug_logger.log_info("[BrowserCaptcha] 检查并加载 reCAPTCHA v3 脚本...")
|
| 111 |
+
script_loaded = await page.evaluate("""
|
| 112 |
+
() => {
|
| 113 |
+
if (window.grecaptcha && typeof window.grecaptcha.execute === 'function') {
|
| 114 |
+
return true;
|
| 115 |
+
}
|
| 116 |
+
return false;
|
| 117 |
+
}
|
| 118 |
+
""")
|
| 119 |
+
|
| 120 |
+
if not script_loaded:
|
| 121 |
+
# 注入脚本
|
| 122 |
+
debug_logger.log_info("[BrowserCaptcha] 注入 reCAPTCHA v3 脚本...")
|
| 123 |
+
await page.evaluate(f"""
|
| 124 |
+
() => {{
|
| 125 |
+
return new Promise((resolve) => {{
|
| 126 |
+
const script = document.createElement('script');
|
| 127 |
+
script.src = 'https://www.google.com/recaptcha/api.js?render={self.website_key}';
|
| 128 |
+
script.async = true;
|
| 129 |
+
script.defer = true;
|
| 130 |
+
script.onload = () => resolve(true);
|
| 131 |
+
script.onerror = () => resolve(false);
|
| 132 |
+
document.head.appendChild(script);
|
| 133 |
+
}});
|
| 134 |
+
}}
|
| 135 |
+
""")
|
| 136 |
+
|
| 137 |
+
# 等待reCAPTCHA加载和初始化
|
| 138 |
+
debug_logger.log_info("[BrowserCaptcha] 等待reCAPTCHA初始化...")
|
| 139 |
+
for i in range(20):
|
| 140 |
+
grecaptcha_ready = await page.evaluate("""
|
| 141 |
+
() => {
|
| 142 |
+
return window.grecaptcha &&
|
| 143 |
+
typeof window.grecaptcha.execute === 'function';
|
| 144 |
+
}
|
| 145 |
+
""")
|
| 146 |
+
if grecaptcha_ready:
|
| 147 |
+
debug_logger.log_info(f"[BrowserCaptcha] reCAPTCHA 已准备好(等待了 {i*0.5} 秒)")
|
| 148 |
+
break
|
| 149 |
+
await asyncio.sleep(0.5)
|
| 150 |
+
else:
|
| 151 |
+
debug_logger.log_warning("[BrowserCaptcha] reCAPTCHA 初始化超时,继续尝试执行...")
|
| 152 |
+
|
| 153 |
+
# 额外等待确保完全初始化
|
| 154 |
+
await page.wait_for_timeout(1000)
|
| 155 |
+
|
| 156 |
+
# 执行reCAPTCHA并获取token
|
| 157 |
+
debug_logger.log_info("[BrowserCaptcha] 执行reCAPTCHA验证...")
|
| 158 |
+
token = await page.evaluate("""
|
| 159 |
+
async (websiteKey) => {
|
| 160 |
+
try {
|
| 161 |
+
if (!window.grecaptcha) {
|
| 162 |
+
console.error('[BrowserCaptcha] window.grecaptcha 不存在');
|
| 163 |
+
return null;
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
if (typeof window.grecaptcha.execute !== 'function') {
|
| 167 |
+
console.error('[BrowserCaptcha] window.grecaptcha.execute 不是函数');
|
| 168 |
+
return null;
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
// 确保grecaptcha已准备好
|
| 172 |
+
await new Promise((resolve, reject) => {
|
| 173 |
+
const timeout = setTimeout(() => {
|
| 174 |
+
reject(new Error('reCAPTCHA加载超时'));
|
| 175 |
+
}, 15000);
|
| 176 |
+
|
| 177 |
+
if (window.grecaptcha && window.grecaptcha.ready) {
|
| 178 |
+
window.grecaptcha.ready(() => {
|
| 179 |
+
clearTimeout(timeout);
|
| 180 |
+
resolve();
|
| 181 |
+
});
|
| 182 |
+
} else {
|
| 183 |
+
clearTimeout(timeout);
|
| 184 |
+
resolve();
|
| 185 |
+
}
|
| 186 |
+
});
|
| 187 |
+
|
| 188 |
+
// 执行reCAPTCHA v3
|
| 189 |
+
const token = await window.grecaptcha.execute(websiteKey, {
|
| 190 |
+
action: 'FLOW_GENERATION'
|
| 191 |
+
});
|
| 192 |
+
|
| 193 |
+
return token;
|
| 194 |
+
} catch (error) {
|
| 195 |
+
console.error('[BrowserCaptcha] reCAPTCHA执行错误:', error);
|
| 196 |
+
return null;
|
| 197 |
+
}
|
| 198 |
+
}
|
| 199 |
+
""", self.website_key)
|
| 200 |
+
|
| 201 |
+
duration_ms = (time.time() - start_time) * 1000
|
| 202 |
+
|
| 203 |
+
if token:
|
| 204 |
+
debug_logger.log_info(f"[BrowserCaptcha] ✅ Token获取成功(耗时 {duration_ms:.0f}ms)")
|
| 205 |
+
return token
|
| 206 |
+
else:
|
| 207 |
+
debug_logger.log_error("[BrowserCaptcha] Token获取失败(返回null)")
|
| 208 |
+
return None
|
| 209 |
+
|
| 210 |
+
except Exception as e:
|
| 211 |
+
debug_logger.log_error(f"[BrowserCaptcha] 获取token异常: {str(e)}")
|
| 212 |
+
return None
|
| 213 |
+
finally:
|
| 214 |
+
# 关闭上下文
|
| 215 |
+
if context:
|
| 216 |
+
try:
|
| 217 |
+
await context.close()
|
| 218 |
+
except:
|
| 219 |
+
pass
|
| 220 |
+
|
| 221 |
+
async def close(self):
|
| 222 |
+
"""关闭浏览器"""
|
| 223 |
+
try:
|
| 224 |
+
if self.browser:
|
| 225 |
+
try:
|
| 226 |
+
await self.browser.close()
|
| 227 |
+
except Exception as e:
|
| 228 |
+
# 忽略连接关闭错误(正常关闭场景)
|
| 229 |
+
if "Connection closed" not in str(e):
|
| 230 |
+
debug_logger.log_warning(f"[BrowserCaptcha] 关闭浏览器时出现异常: {str(e)}")
|
| 231 |
+
finally:
|
| 232 |
+
self.browser = None
|
| 233 |
+
|
| 234 |
+
if self.playwright:
|
| 235 |
+
try:
|
| 236 |
+
await self.playwright.stop()
|
| 237 |
+
except Exception:
|
| 238 |
+
pass # 静默处理 playwright 停止异常
|
| 239 |
+
finally:
|
| 240 |
+
self.playwright = None
|
| 241 |
+
|
| 242 |
+
self._initialized = False
|
| 243 |
+
debug_logger.log_info("[BrowserCaptcha] 浏览器已关闭")
|
| 244 |
+
except Exception as e:
|
| 245 |
+
debug_logger.log_error(f"[BrowserCaptcha] 关闭浏览器异常: {str(e)}")
|
src/services/flow_client.py
CHANGED
|
@@ -308,10 +308,17 @@ class FlowClient:
|
|
| 308 |
"""
|
| 309 |
url = f"{self.api_base_url}/projects/{project_id}/flowMedia:batchGenerateImages"
|
| 310 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 311 |
# 构建请求
|
| 312 |
request_data = {
|
| 313 |
"clientContext": {
|
| 314 |
-
"
|
|
|
|
|
|
|
|
|
|
| 315 |
},
|
| 316 |
"seed": random.randint(1, 99999),
|
| 317 |
"imageModelName": model_name,
|
|
@@ -321,6 +328,10 @@ class FlowClient:
|
|
| 321 |
}
|
| 322 |
|
| 323 |
json_data = {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 324 |
"requests": [request_data]
|
| 325 |
}
|
| 326 |
|
|
@@ -367,11 +378,15 @@ class FlowClient:
|
|
| 367 |
"""
|
| 368 |
url = f"{self.api_base_url}/video:batchAsyncGenerateVideoText"
|
| 369 |
|
|
|
|
|
|
|
|
|
|
| 370 |
scene_id = str(uuid.uuid4())
|
| 371 |
|
| 372 |
json_data = {
|
| 373 |
"clientContext": {
|
| 374 |
-
"
|
|
|
|
| 375 |
"projectId": project_id,
|
| 376 |
"tool": "PINHOLE",
|
| 377 |
"userPaygateTier": user_paygate_tier
|
|
@@ -425,11 +440,15 @@ class FlowClient:
|
|
| 425 |
"""
|
| 426 |
url = f"{self.api_base_url}/video:batchAsyncGenerateVideoReferenceImages"
|
| 427 |
|
|
|
|
|
|
|
|
|
|
| 428 |
scene_id = str(uuid.uuid4())
|
| 429 |
|
| 430 |
json_data = {
|
| 431 |
"clientContext": {
|
| 432 |
-
"
|
|
|
|
| 433 |
"projectId": project_id,
|
| 434 |
"tool": "PINHOLE",
|
| 435 |
"userPaygateTier": user_paygate_tier
|
|
@@ -486,11 +505,15 @@ class FlowClient:
|
|
| 486 |
"""
|
| 487 |
url = f"{self.api_base_url}/video:batchAsyncGenerateVideoStartAndEndImage"
|
| 488 |
|
|
|
|
|
|
|
|
|
|
| 489 |
scene_id = str(uuid.uuid4())
|
| 490 |
|
| 491 |
json_data = {
|
| 492 |
"clientContext": {
|
| 493 |
-
"
|
|
|
|
| 494 |
"projectId": project_id,
|
| 495 |
"tool": "PINHOLE",
|
| 496 |
"userPaygateTier": user_paygate_tier
|
|
@@ -550,11 +573,15 @@ class FlowClient:
|
|
| 550 |
"""
|
| 551 |
url = f"{self.api_base_url}/video:batchAsyncGenerateVideoStartAndEndImage"
|
| 552 |
|
|
|
|
|
|
|
|
|
|
| 553 |
scene_id = str(uuid.uuid4())
|
| 554 |
|
| 555 |
json_data = {
|
| 556 |
"clientContext": {
|
| 557 |
-
"
|
|
|
|
| 558 |
"projectId": project_id,
|
| 559 |
"tool": "PINHOLE",
|
| 560 |
"userPaygateTier": user_paygate_tier
|
|
@@ -655,3 +682,75 @@ class FlowClient:
|
|
| 655 |
def _generate_scene_id(self) -> str:
|
| 656 |
"""生成sceneId: UUID"""
|
| 657 |
return str(uuid.uuid4())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 308 |
"""
|
| 309 |
url = f"{self.api_base_url}/projects/{project_id}/flowMedia:batchGenerateImages"
|
| 310 |
|
| 311 |
+
# 获取 reCAPTCHA token
|
| 312 |
+
recaptcha_token = await self._get_recaptcha_token(project_id) or ""
|
| 313 |
+
session_id = self._generate_session_id()
|
| 314 |
+
|
| 315 |
# 构建请求
|
| 316 |
request_data = {
|
| 317 |
"clientContext": {
|
| 318 |
+
"recaptchaToken": recaptcha_token,
|
| 319 |
+
"projectId": project_id,
|
| 320 |
+
"sessionId": session_id,
|
| 321 |
+
"tool": "PINHOLE"
|
| 322 |
},
|
| 323 |
"seed": random.randint(1, 99999),
|
| 324 |
"imageModelName": model_name,
|
|
|
|
| 328 |
}
|
| 329 |
|
| 330 |
json_data = {
|
| 331 |
+
"clientContext": {
|
| 332 |
+
"recaptchaToken": recaptcha_token,
|
| 333 |
+
"sessionId": session_id
|
| 334 |
+
},
|
| 335 |
"requests": [request_data]
|
| 336 |
}
|
| 337 |
|
|
|
|
| 378 |
"""
|
| 379 |
url = f"{self.api_base_url}/video:batchAsyncGenerateVideoText"
|
| 380 |
|
| 381 |
+
# 获取 reCAPTCHA token
|
| 382 |
+
recaptcha_token = await self._get_recaptcha_token(project_id) or ""
|
| 383 |
+
session_id = self._generate_session_id()
|
| 384 |
scene_id = str(uuid.uuid4())
|
| 385 |
|
| 386 |
json_data = {
|
| 387 |
"clientContext": {
|
| 388 |
+
"recaptchaToken": recaptcha_token,
|
| 389 |
+
"sessionId": session_id,
|
| 390 |
"projectId": project_id,
|
| 391 |
"tool": "PINHOLE",
|
| 392 |
"userPaygateTier": user_paygate_tier
|
|
|
|
| 440 |
"""
|
| 441 |
url = f"{self.api_base_url}/video:batchAsyncGenerateVideoReferenceImages"
|
| 442 |
|
| 443 |
+
# 获取 reCAPTCHA token
|
| 444 |
+
recaptcha_token = await self._get_recaptcha_token(project_id) or ""
|
| 445 |
+
session_id = self._generate_session_id()
|
| 446 |
scene_id = str(uuid.uuid4())
|
| 447 |
|
| 448 |
json_data = {
|
| 449 |
"clientContext": {
|
| 450 |
+
"recaptchaToken": recaptcha_token,
|
| 451 |
+
"sessionId": session_id,
|
| 452 |
"projectId": project_id,
|
| 453 |
"tool": "PINHOLE",
|
| 454 |
"userPaygateTier": user_paygate_tier
|
|
|
|
| 505 |
"""
|
| 506 |
url = f"{self.api_base_url}/video:batchAsyncGenerateVideoStartAndEndImage"
|
| 507 |
|
| 508 |
+
# 获取 reCAPTCHA token
|
| 509 |
+
recaptcha_token = await self._get_recaptcha_token(project_id) or ""
|
| 510 |
+
session_id = self._generate_session_id()
|
| 511 |
scene_id = str(uuid.uuid4())
|
| 512 |
|
| 513 |
json_data = {
|
| 514 |
"clientContext": {
|
| 515 |
+
"recaptchaToken": recaptcha_token,
|
| 516 |
+
"sessionId": session_id,
|
| 517 |
"projectId": project_id,
|
| 518 |
"tool": "PINHOLE",
|
| 519 |
"userPaygateTier": user_paygate_tier
|
|
|
|
| 573 |
"""
|
| 574 |
url = f"{self.api_base_url}/video:batchAsyncGenerateVideoStartAndEndImage"
|
| 575 |
|
| 576 |
+
# 获取 reCAPTCHA token
|
| 577 |
+
recaptcha_token = await self._get_recaptcha_token(project_id) or ""
|
| 578 |
+
session_id = self._generate_session_id()
|
| 579 |
scene_id = str(uuid.uuid4())
|
| 580 |
|
| 581 |
json_data = {
|
| 582 |
"clientContext": {
|
| 583 |
+
"recaptchaToken": recaptcha_token,
|
| 584 |
+
"sessionId": session_id,
|
| 585 |
"projectId": project_id,
|
| 586 |
"tool": "PINHOLE",
|
| 587 |
"userPaygateTier": user_paygate_tier
|
|
|
|
| 682 |
def _generate_scene_id(self) -> str:
|
| 683 |
"""生成sceneId: UUID"""
|
| 684 |
return str(uuid.uuid4())
|
| 685 |
+
|
| 686 |
+
async def _get_recaptcha_token(self, project_id: str) -> Optional[str]:
|
| 687 |
+
"""获取reCAPTCHA token - 支持两种方式"""
|
| 688 |
+
captcha_method = config.captcha_method
|
| 689 |
+
|
| 690 |
+
# 浏览器打码
|
| 691 |
+
if captcha_method == "browser":
|
| 692 |
+
try:
|
| 693 |
+
from .browser_captcha import BrowserCaptchaService
|
| 694 |
+
service = await BrowserCaptchaService.get_instance(self.proxy_manager)
|
| 695 |
+
return await service.get_token(project_id)
|
| 696 |
+
except Exception as e:
|
| 697 |
+
debug_logger.log_error(f"[reCAPTCHA Browser] error: {str(e)}")
|
| 698 |
+
return None
|
| 699 |
+
|
| 700 |
+
# YesCaptcha打码
|
| 701 |
+
client_key = config.yescaptcha_api_key
|
| 702 |
+
if not client_key:
|
| 703 |
+
debug_logger.log_info("[reCAPTCHA] API key not configured, skipping")
|
| 704 |
+
return None
|
| 705 |
+
|
| 706 |
+
website_key = "6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV"
|
| 707 |
+
website_url = f"https://labs.google/fx/tools/flow/project/{project_id}"
|
| 708 |
+
base_url = config.yescaptcha_base_url
|
| 709 |
+
page_action = "FLOW_GENERATION"
|
| 710 |
+
|
| 711 |
+
try:
|
| 712 |
+
async with AsyncSession() as session:
|
| 713 |
+
create_url = f"{base_url}/createTask"
|
| 714 |
+
create_data = {
|
| 715 |
+
"clientKey": client_key,
|
| 716 |
+
"task": {
|
| 717 |
+
"websiteURL": website_url,
|
| 718 |
+
"websiteKey": website_key,
|
| 719 |
+
"type": "RecaptchaV3TaskProxylessM1",
|
| 720 |
+
"pageAction": page_action
|
| 721 |
+
}
|
| 722 |
+
}
|
| 723 |
+
|
| 724 |
+
result = await session.post(create_url, json=create_data, impersonate="chrome110")
|
| 725 |
+
result_json = result.json()
|
| 726 |
+
task_id = result_json.get('taskId')
|
| 727 |
+
|
| 728 |
+
debug_logger.log_info(f"[reCAPTCHA] created task_id: {task_id}")
|
| 729 |
+
|
| 730 |
+
if not task_id:
|
| 731 |
+
return None
|
| 732 |
+
|
| 733 |
+
get_url = f"{base_url}/getTaskResult"
|
| 734 |
+
for i in range(40):
|
| 735 |
+
get_data = {
|
| 736 |
+
"clientKey": client_key,
|
| 737 |
+
"taskId": task_id
|
| 738 |
+
}
|
| 739 |
+
result = await session.post(get_url, json=get_data, impersonate="chrome110")
|
| 740 |
+
result_json = result.json()
|
| 741 |
+
|
| 742 |
+
debug_logger.log_info(f"[reCAPTCHA] polling #{i+1}: {result_json}")
|
| 743 |
+
|
| 744 |
+
solution = result_json.get('solution', {})
|
| 745 |
+
response = solution.get('gRecaptchaResponse')
|
| 746 |
+
|
| 747 |
+
if response:
|
| 748 |
+
return response
|
| 749 |
+
|
| 750 |
+
time.sleep(3)
|
| 751 |
+
|
| 752 |
+
return None
|
| 753 |
+
|
| 754 |
+
except Exception as e:
|
| 755 |
+
debug_logger.log_error(f"[reCAPTCHA] error: {str(e)}")
|
| 756 |
+
return None
|