我也不知道呢
Browse files- .gitignore +4 -0
- Dockerfile +37 -13
- README.md +7 -43
- docker-compose.local.yml +17 -0
- docker-compose.proxy.yml +3 -3
- docker-compose.yml +1 -1
- requirements.txt +2 -0
- src/api/admin.py +108 -109
- src/api/routes.py +13 -1
- src/core/config.py +66 -0
- src/core/database.py +100 -72
- src/core/models.py +7 -1
- src/main.py +32 -50
- src/services/browser_captcha.py +1 -12
- src/services/browser_captcha_personal.py +610 -141
- src/services/flow_client.py +162 -49
- src/services/generation_handler.py +71 -7
- src/services/token_manager.py +122 -35
- static/manage.html +50 -3
.gitignore
CHANGED
|
@@ -54,3 +54,7 @@ logs.txt
|
|
| 54 |
*.cache
|
| 55 |
|
| 56 |
browser_data
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
*.cache
|
| 55 |
|
| 56 |
browser_data
|
| 57 |
+
|
| 58 |
+
data
|
| 59 |
+
config/setting.toml
|
| 60 |
+
config/setting_warp.toml
|
Dockerfile
CHANGED
|
@@ -1,20 +1,44 @@
|
|
| 1 |
-
|
| 2 |
-
FROM python:3.10-slim
|
| 3 |
|
| 4 |
-
# Set the working directory in the container
|
| 5 |
WORKDIR /app
|
| 6 |
|
| 7 |
-
#
|
| 8 |
-
|
|
|
|
| 9 |
|
| 10 |
-
#
|
| 11 |
-
RUN
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
-
#
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
-
#
|
| 17 |
-
ENV
|
| 18 |
|
| 19 |
-
#
|
| 20 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
|
|
|
| 2 |
|
|
|
|
| 3 |
WORKDIR /app
|
| 4 |
|
| 5 |
+
# 使用清华镜像源加速 apt (Debian bookworm)
|
| 6 |
+
RUN sed -i 's|deb.debian.org|mirrors.tuna.tsinghua.edu.cn|g' /etc/apt/sources.list.d/debian.sources \
|
| 7 |
+
&& sed -i 's|security.debian.org|mirrors.tuna.tsinghua.edu.cn|g' /etc/apt/sources.list.d/debian.sources
|
| 8 |
|
| 9 |
+
# 安装 Playwright 所需的系统依赖
|
| 10 |
+
RUN apt-get update && apt-get install -y \
|
| 11 |
+
libnss3 \
|
| 12 |
+
libnspr4 \
|
| 13 |
+
libatk1.0-0 \
|
| 14 |
+
libatk-bridge2.0-0 \
|
| 15 |
+
libcups2 \
|
| 16 |
+
libdrm2 \
|
| 17 |
+
libxkbcommon0 \
|
| 18 |
+
libxcomposite1 \
|
| 19 |
+
libxdamage1 \
|
| 20 |
+
libxfixes3 \
|
| 21 |
+
libxrandr2 \
|
| 22 |
+
libgbm1 \
|
| 23 |
+
libasound2 \
|
| 24 |
+
libpango-1.0-0 \
|
| 25 |
+
libcairo2 \
|
| 26 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 27 |
|
| 28 |
+
# 安装 Python 依赖(使用清华 PyPI 镜像)
|
| 29 |
+
COPY requirements.txt .
|
| 30 |
+
RUN pip install --no-cache-dir -r requirements.txt \
|
| 31 |
+
-i https://pypi.tuna.tsinghua.edu.cn/simple/ \
|
| 32 |
+
--trusted-host pypi.tuna.tsinghua.edu.cn
|
| 33 |
|
| 34 |
+
# 设置 Playwright 下载镜像(使用 npmmirror)
|
| 35 |
+
ENV PLAYWRIGHT_DOWNLOAD_HOST=https://registry.npmmirror.com/-/binary/playwright
|
| 36 |
|
| 37 |
+
# 安装 Playwright 浏览器
|
| 38 |
+
RUN playwright install chromium
|
| 39 |
+
|
| 40 |
+
COPY . .
|
| 41 |
+
|
| 42 |
+
EXPOSE 8000
|
| 43 |
+
|
| 44 |
+
CMD ["python", "main.py"]
|
README.md
CHANGED
|
@@ -1,14 +1,3 @@
|
|
| 1 |
-
---
|
| 2 |
-
title: Flow2API
|
| 3 |
-
emoji: 🚀
|
| 4 |
-
colorFrom: blue
|
| 5 |
-
colorTo: green
|
| 6 |
-
sdk: docker
|
| 7 |
-
sdk_version: '1.0'
|
| 8 |
-
app_file: app.py
|
| 9 |
-
pinned: false
|
| 10 |
-
---
|
| 11 |
-
|
| 12 |
# Flow2API
|
| 13 |
|
| 14 |
<div align="center">
|
|
@@ -27,7 +16,7 @@ pinned: false
|
|
| 27 |
- 🎨 **文生图** / **图生图**
|
| 28 |
- 🎬 **文生视频** / **图生视频**
|
| 29 |
- 🎞️ **首尾帧视频**
|
| 30 |
-
- 🔄 **AT自动刷新**
|
| 31 |
- 📊 **余额显示** - 实时查询和显示 VideoFX Credits
|
| 32 |
- 🚀 **负载均衡** - 多 Token 轮询和并发控制
|
| 33 |
- 🌐 **代理支持** - 支持 HTTP/SOCKS5 代理
|
|
@@ -77,7 +66,7 @@ docker-compose -f docker-compose.warp.yml logs -f
|
|
| 77 |
```bash
|
| 78 |
# 克隆项目
|
| 79 |
git clone https://github.com/TheSmallHanCat/flow2api.git
|
| 80 |
-
cd
|
| 81 |
|
| 82 |
# 创建虚拟环境
|
| 83 |
python -m venv venv
|
|
@@ -95,35 +84,6 @@ pip install -r requirements.txt
|
|
| 95 |
python main.py
|
| 96 |
```
|
| 97 |
|
| 98 |
-
### 方式三:Hugging Face 部署
|
| 99 |
-
|
| 100 |
-
1. **准备工作**
|
| 101 |
-
- 注册并登录 [Hugging Face](https://huggingface.co/)
|
| 102 |
-
- 创建一个新的 Space
|
| 103 |
-
- 选择 "Docker" 作为 Space 类型
|
| 104 |
-
|
| 105 |
-
2. **配置 Space**
|
| 106 |
-
- **Repository name**: 输入一个名称(例如 `flow2api`)
|
| 107 |
-
- **Visibility**: 选择 "Public" 或 "Private"
|
| 108 |
-
- **Hardware**: 选择适当的硬件配置(最低推荐 2GB RAM)
|
| 109 |
-
|
| 110 |
-
3. **部署步骤**
|
| 111 |
-
- 在 Space 的 "Files" 标签页中,上传项目的所有文件
|
| 112 |
-
- 确保 `app.py` 文件存在(Hugging Face 的部署入口)
|
| 113 |
-
- 确保 `requirements.txt` 文件包含所有必要的依赖
|
| 114 |
-
- 点击 "Build" 按钮开始部署
|
| 115 |
-
|
| 116 |
-
4. **访问服务**
|
| 117 |
-
- 部署完成后,通过 Space 提供的 URL 访问服务
|
| 118 |
-
- 默认登录地址: `https://<your-space-name>.hf.space/`
|
| 119 |
-
- 首次登录用户名: `admin`,密码: `admin`
|
| 120 |
-
|
| 121 |
-
5. **注意事项**
|
| 122 |
-
- Hugging Face Spaces 使用端口 7860,配置已自动适配
|
| 123 |
-
- 由于 Hugging Face 的网络环境,可能需要配置代理
|
| 124 |
-
- 建议在管理界面中修改默认的管理员密码
|
| 125 |
-
- 对于视频生成等耗时操作,可能会受到 Hugging Face 的资源限制
|
| 126 |
-
|
| 127 |
### 首次访问
|
| 128 |
|
| 129 |
服务启动后,访问管理后台: **http://localhost:8000**,首次登录后请立即修改密码!
|
|
@@ -159,7 +119,11 @@ python main.py
|
|
| 159 |
| `veo_2_0_t2v_landscape` | 文生视频 | 横屏 |
|
| 160 |
|
| 161 |
#### 首尾帧模型 (I2V - Image to Video)
|
| 162 |
-
📸 **支持1-2张图片:首尾帧**
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
|
| 164 |
| 模型名称 | 说明| 尺寸 |
|
| 165 |
|---------|---------|--------|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
# Flow2API
|
| 2 |
|
| 3 |
<div align="center">
|
|
|
|
| 16 |
- 🎨 **文生图** / **图生图**
|
| 17 |
- 🎬 **文生视频** / **图生视频**
|
| 18 |
- 🎞️ **首尾帧视频**
|
| 19 |
+
- 🔄 **AT/ST自动刷新** - AT 过期自动刷新,ST 过期时自动通过浏览器更新(personal 模式)
|
| 20 |
- 📊 **余额显示** - 实时查询和显示 VideoFX Credits
|
| 21 |
- 🚀 **负载均衡** - 多 Token 轮询和并发控制
|
| 22 |
- 🌐 **代理支持** - 支持 HTTP/SOCKS5 代理
|
|
|
|
| 66 |
```bash
|
| 67 |
# 克隆项目
|
| 68 |
git clone https://github.com/TheSmallHanCat/flow2api.git
|
| 69 |
+
cd sora2api
|
| 70 |
|
| 71 |
# 创建虚拟环境
|
| 72 |
python -m venv venv
|
|
|
|
| 84 |
python main.py
|
| 85 |
```
|
| 86 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
### 首次访问
|
| 88 |
|
| 89 |
服务启动后,访问管理后台: **http://localhost:8000**,首次登录后请立即修改密码!
|
|
|
|
| 119 |
| `veo_2_0_t2v_landscape` | 文生视频 | 横屏 |
|
| 120 |
|
| 121 |
#### 首尾帧模型 (I2V - Image to Video)
|
| 122 |
+
📸 **支持1-2张图片:1张作为首帧,2张作为首尾帧**
|
| 123 |
+
|
| 124 |
+
> 💡 **自动适配**:系统会根据图片数量自动选择对应的 model_key
|
| 125 |
+
> - **单帧模式**(1张图):使用首帧生成视频
|
| 126 |
+
> - **双帧模式**(2张图):使用首帧+尾帧生成过渡视频
|
| 127 |
|
| 128 |
| 模型名称 | 说明| 尺寸 |
|
| 129 |
|---------|---------|--------|
|
docker-compose.local.yml
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version: '3.8'
|
| 2 |
+
|
| 3 |
+
services:
|
| 4 |
+
flow2api:
|
| 5 |
+
build:
|
| 6 |
+
context: .
|
| 7 |
+
dockerfile: Dockerfile
|
| 8 |
+
image: flow2api:local
|
| 9 |
+
container_name: flow2api
|
| 10 |
+
ports:
|
| 11 |
+
- "38000:8000"
|
| 12 |
+
volumes:
|
| 13 |
+
- ./data:/app/data
|
| 14 |
+
- ./config/setting.toml:/app/config/setting.toml
|
| 15 |
+
environment:
|
| 16 |
+
- PYTHONUNBUFFERED=1
|
| 17 |
+
restart: unless-stopped
|
docker-compose.proxy.yml
CHANGED
|
@@ -5,7 +5,7 @@ services:
|
|
| 5 |
image: thesmallhancat/flow2api:latest
|
| 6 |
container_name: flow2api
|
| 7 |
ports:
|
| 8 |
-
- "
|
| 9 |
volumes:
|
| 10 |
- ./data:/app/data
|
| 11 |
- ./config/setting_warp.toml:/app/config/setting.toml
|
|
@@ -22,7 +22,7 @@ services:
|
|
| 22 |
devices:
|
| 23 |
- /dev/net/tun:/dev/net/tun
|
| 24 |
ports:
|
| 25 |
-
- "
|
| 26 |
environment:
|
| 27 |
- WARP_SLEEP=2
|
| 28 |
cap_add:
|
|
@@ -33,4 +33,4 @@ services:
|
|
| 33 |
- net.ipv6.conf.all.disable_ipv6=0
|
| 34 |
- net.ipv4.conf.all.src_valid_mark=1
|
| 35 |
volumes:
|
| 36 |
-
- ./data:/var/lib/cloudflare-warp
|
|
|
|
| 5 |
image: thesmallhancat/flow2api:latest
|
| 6 |
container_name: flow2api
|
| 7 |
ports:
|
| 8 |
+
- "38000:8000"
|
| 9 |
volumes:
|
| 10 |
- ./data:/app/data
|
| 11 |
- ./config/setting_warp.toml:/app/config/setting.toml
|
|
|
|
| 22 |
devices:
|
| 23 |
- /dev/net/tun:/dev/net/tun
|
| 24 |
ports:
|
| 25 |
+
- "31080:1080"
|
| 26 |
environment:
|
| 27 |
- WARP_SLEEP=2
|
| 28 |
cap_add:
|
|
|
|
| 33 |
- net.ipv6.conf.all.disable_ipv6=0
|
| 34 |
- net.ipv4.conf.all.src_valid_mark=1
|
| 35 |
volumes:
|
| 36 |
+
- ./data:/var/lib/cloudflare-warp
|
docker-compose.yml
CHANGED
|
@@ -5,7 +5,7 @@ services:
|
|
| 5 |
image: thesmallhancat/flow2api:latest
|
| 6 |
container_name: flow2api
|
| 7 |
ports:
|
| 8 |
-
- "
|
| 9 |
volumes:
|
| 10 |
- ./data:/app/data
|
| 11 |
- ./config/setting.toml:/app/config/setting.toml
|
|
|
|
| 5 |
image: thesmallhancat/flow2api:latest
|
| 6 |
container_name: flow2api
|
| 7 |
ports:
|
| 8 |
+
- "38000:8000"
|
| 9 |
volumes:
|
| 10 |
- ./data:/app/data
|
| 11 |
- ./config/setting.toml:/app/config/setting.toml
|
requirements.txt
CHANGED
|
@@ -7,3 +7,5 @@ tomli==2.2.1
|
|
| 7 |
bcrypt==4.2.1
|
| 8 |
python-multipart==0.0.20
|
| 9 |
python-dateutil==2.8.2
|
|
|
|
|
|
|
|
|
| 7 |
bcrypt==4.2.1
|
| 8 |
python-multipart==0.0.20
|
| 9 |
python-dateutil==2.8.2
|
| 10 |
+
playwright==1.53.0
|
| 11 |
+
nodriver>=0.48.0
|
src/api/admin.py
CHANGED
|
@@ -354,17 +354,32 @@ async def refresh_at(
|
|
| 354 |
token_id: int,
|
| 355 |
token: str = Depends(verify_admin_token)
|
| 356 |
):
|
| 357 |
-
"""手动刷新Token的AT (使用ST转换) 🆕
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 358 |
try:
|
| 359 |
-
# 调用token_manager的内部刷新方法
|
| 360 |
success = await token_manager._refresh_at(token_id)
|
| 361 |
|
| 362 |
if success:
|
| 363 |
# 获取更新后的token信息
|
| 364 |
updated_token = await token_manager.get_token(token_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 365 |
return {
|
| 366 |
"success": True,
|
| 367 |
-
"message":
|
| 368 |
"token": {
|
| 369 |
"id": updated_token.id,
|
| 370 |
"email": updated_token.email,
|
|
@@ -372,8 +387,17 @@ async def refresh_at(
|
|
| 372 |
}
|
| 373 |
}
|
| 374 |
else:
|
| 375 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 376 |
except Exception as e:
|
|
|
|
| 377 |
raise HTTPException(status_code=500, detail=f"刷新AT失败: {str(e)}")
|
| 378 |
|
| 379 |
|
|
@@ -519,13 +543,8 @@ async def update_proxy_config_alias(
|
|
| 519 |
token: str = Depends(verify_admin_token)
|
| 520 |
):
|
| 521 |
"""Update proxy configuration (alias for frontend compatibility)"""
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
return {"success": True, "message": "代理配置更新成功"}
|
| 525 |
-
except Exception as e:
|
| 526 |
-
# 捕获所有异常,确保返回有效的 JSON 响应
|
| 527 |
-
print(f"Error updating proxy config: {e}")
|
| 528 |
-
return {"success": False, "message": f"保存失败: {str(e)}"}
|
| 529 |
|
| 530 |
|
| 531 |
@router.post("/api/config/proxy")
|
|
@@ -534,13 +553,8 @@ async def update_proxy_config(
|
|
| 534 |
token: str = Depends(verify_admin_token)
|
| 535 |
):
|
| 536 |
"""Update proxy configuration"""
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
return {"success": True, "message": "代理配置更新成功"}
|
| 540 |
-
except Exception as e:
|
| 541 |
-
# 捕获所有异常,确保返回有效的 JSON 响应
|
| 542 |
-
print(f"Error updating proxy config: {e}")
|
| 543 |
-
return {"success": False, "message": f"保存失败: {str(e)}"}
|
| 544 |
|
| 545 |
|
| 546 |
@router.get("/api/config/generation")
|
|
@@ -692,18 +706,10 @@ async def update_admin_config(
|
|
| 692 |
token: str = Depends(verify_admin_token)
|
| 693 |
):
|
| 694 |
"""Update admin configuration (error_ban_threshold)"""
|
| 695 |
-
|
| 696 |
-
|
| 697 |
-
await db.update_admin_config(error_ban_threshold=request.error_ban_threshold)
|
| 698 |
|
| 699 |
-
|
| 700 |
-
await db.reload_config_to_memory()
|
| 701 |
-
|
| 702 |
-
return {"success": True, "message": "配置更新成功"}
|
| 703 |
-
except Exception as e:
|
| 704 |
-
# 捕获所有异常,确保返回有效的 JSON 响应
|
| 705 |
-
print(f"Error updating admin config: {e}")
|
| 706 |
-
return {"success": False, "message": f"保存失败: {str(e)}"}
|
| 707 |
|
| 708 |
|
| 709 |
@router.post("/api/admin/password")
|
|
@@ -759,17 +765,12 @@ async def update_generation_timeout(
|
|
| 759 |
token: str = Depends(verify_admin_token)
|
| 760 |
):
|
| 761 |
"""Update generation timeout configuration"""
|
| 762 |
-
|
| 763 |
-
await db.update_generation_config(request.image_timeout, request.video_timeout)
|
| 764 |
|
| 765 |
-
|
| 766 |
-
|
| 767 |
|
| 768 |
-
|
| 769 |
-
except Exception as e:
|
| 770 |
-
# 捕获所有异常,确保返回有效的 JSON 响应
|
| 771 |
-
print(f"Error updating generation timeout: {e}")
|
| 772 |
-
return {"success": False, "message": f"保存失败: {str(e)}"}
|
| 773 |
|
| 774 |
|
| 775 |
# ========== AT Auto Refresh Config ==========
|
|
@@ -838,21 +839,16 @@ async def update_cache_config_full(
|
|
| 838 |
token: str = Depends(verify_admin_token)
|
| 839 |
):
|
| 840 |
"""Update complete cache configuration"""
|
| 841 |
-
|
| 842 |
-
|
| 843 |
-
|
| 844 |
-
base_url = request.get("base_url")
|
| 845 |
|
| 846 |
-
|
| 847 |
|
| 848 |
-
|
| 849 |
-
|
| 850 |
|
| 851 |
-
|
| 852 |
-
except Exception as e:
|
| 853 |
-
# 捕获所有异常,确保返回有效的 JSON 响应
|
| 854 |
-
print(f"Error updating cache config: {e}")
|
| 855 |
-
return {"success": False, "message": f"保存失败: {str(e)}"}
|
| 856 |
|
| 857 |
|
| 858 |
@router.post("/api/cache/base-url")
|
|
@@ -861,18 +857,13 @@ async def update_cache_base_url(
|
|
| 861 |
token: str = Depends(verify_admin_token)
|
| 862 |
):
|
| 863 |
"""Update cache base URL"""
|
| 864 |
-
|
| 865 |
-
|
| 866 |
-
await db.update_cache_config(base_url=base_url)
|
| 867 |
|
| 868 |
-
|
| 869 |
-
|
| 870 |
|
| 871 |
-
|
| 872 |
-
except Exception as e:
|
| 873 |
-
# 捕获所有异常,确保返回有效的 JSON 响应
|
| 874 |
-
print(f"Error updating cache base URL: {e}")
|
| 875 |
-
return {"success": False, "message": f"保存失败: {str(e)}"}
|
| 876 |
|
| 877 |
|
| 878 |
@router.post("/api/captcha/config")
|
|
@@ -881,37 +872,44 @@ async def update_captcha_config(
|
|
| 881 |
token: str = Depends(verify_admin_token)
|
| 882 |
):
|
| 883 |
"""Update captcha configuration"""
|
| 884 |
-
|
| 885 |
-
|
| 886 |
-
|
| 887 |
-
|
| 888 |
-
|
| 889 |
-
|
| 890 |
-
|
| 891 |
-
|
| 892 |
-
|
| 893 |
-
|
| 894 |
-
|
| 895 |
-
|
| 896 |
-
|
| 897 |
-
|
| 898 |
-
|
| 899 |
-
|
| 900 |
-
|
| 901 |
-
|
| 902 |
-
|
| 903 |
-
|
| 904 |
-
|
| 905 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 906 |
|
| 907 |
-
|
| 908 |
-
|
| 909 |
|
| 910 |
-
|
| 911 |
-
except Exception as e:
|
| 912 |
-
# 捕获所有异常,确保返回有效的 JSON 响应
|
| 913 |
-
print(f"Error updating captcha config: {e}")
|
| 914 |
-
return {"success": False, "message": f"保存失败: {str(e)}"}
|
| 915 |
|
| 916 |
|
| 917 |
@router.get("/api/captcha/config")
|
|
@@ -922,6 +920,12 @@ async def get_captcha_config(token: str = Depends(verify_admin_token)):
|
|
| 922 |
"captcha_method": captcha_config.captcha_method,
|
| 923 |
"yescaptcha_api_key": captcha_config.yescaptcha_api_key,
|
| 924 |
"yescaptcha_base_url": captcha_config.yescaptcha_base_url,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 925 |
"browser_proxy_enabled": captcha_config.browser_proxy_enabled,
|
| 926 |
"browser_proxy_url": captcha_config.browser_proxy_url or ""
|
| 927 |
}
|
|
@@ -969,29 +973,24 @@ async def update_plugin_config(
|
|
| 969 |
token: str = Depends(verify_admin_token)
|
| 970 |
):
|
| 971 |
"""Update plugin configuration"""
|
| 972 |
-
|
| 973 |
-
|
| 974 |
-
auto_enable_on_update = request.get("auto_enable_on_update", True) # 默认开启
|
| 975 |
|
| 976 |
-
|
| 977 |
-
|
| 978 |
-
|
| 979 |
|
| 980 |
-
|
| 981 |
-
|
| 982 |
-
|
| 983 |
-
|
| 984 |
|
| 985 |
-
|
| 986 |
-
|
| 987 |
-
|
| 988 |
-
|
| 989 |
-
|
| 990 |
-
|
| 991 |
-
except Exception as e:
|
| 992 |
-
# 捕获所有异常,确保返回有效的 JSON 响应
|
| 993 |
-
print(f"Error updating plugin config: {e}")
|
| 994 |
-
return {"success": False, "message": f"保存失败: {str(e)}"}
|
| 995 |
|
| 996 |
|
| 997 |
@router.post("/api/plugin/update-token")
|
|
|
|
| 354 |
token_id: int,
|
| 355 |
token: str = Depends(verify_admin_token)
|
| 356 |
):
|
| 357 |
+
"""手动刷新Token的AT (使用ST转换) 🆕
|
| 358 |
+
|
| 359 |
+
如果 AT 刷新失败且处于 personal 模式,会自动尝试通过浏览器刷新 ST
|
| 360 |
+
"""
|
| 361 |
+
from ..core.logger import debug_logger
|
| 362 |
+
from ..core.config import config
|
| 363 |
+
|
| 364 |
+
debug_logger.log_info(f"[API] 手动刷新 AT 请求: token_id={token_id}, captcha_method={config.captcha_method}")
|
| 365 |
+
|
| 366 |
try:
|
| 367 |
+
# 调用token_manager的内部刷新方法(包含 ST 自动刷新逻辑)
|
| 368 |
success = await token_manager._refresh_at(token_id)
|
| 369 |
|
| 370 |
if success:
|
| 371 |
# 获取更新后的token信息
|
| 372 |
updated_token = await token_manager.get_token(token_id)
|
| 373 |
+
|
| 374 |
+
message = "AT刷新成功"
|
| 375 |
+
if config.captcha_method == "personal":
|
| 376 |
+
message += "(支持ST自动刷新)"
|
| 377 |
+
|
| 378 |
+
debug_logger.log_info(f"[API] AT 刷新成功: token_id={token_id}")
|
| 379 |
+
|
| 380 |
return {
|
| 381 |
"success": True,
|
| 382 |
+
"message": message,
|
| 383 |
"token": {
|
| 384 |
"id": updated_token.id,
|
| 385 |
"email": updated_token.email,
|
|
|
|
| 387 |
}
|
| 388 |
}
|
| 389 |
else:
|
| 390 |
+
debug_logger.log_error(f"[API] AT 刷新失败: token_id={token_id}")
|
| 391 |
+
|
| 392 |
+
error_detail = "AT刷新失败"
|
| 393 |
+
if config.captcha_method != "personal":
|
| 394 |
+
error_detail += f"(当前打码模式: {config.captcha_method},ST自动刷新仅在 personal 模式下可用)"
|
| 395 |
+
|
| 396 |
+
raise HTTPException(status_code=500, detail=error_detail)
|
| 397 |
+
except HTTPException:
|
| 398 |
+
raise
|
| 399 |
except Exception as e:
|
| 400 |
+
debug_logger.log_error(f"[API] 刷新AT异常: {str(e)}")
|
| 401 |
raise HTTPException(status_code=500, detail=f"刷新AT失败: {str(e)}")
|
| 402 |
|
| 403 |
|
|
|
|
| 543 |
token: str = Depends(verify_admin_token)
|
| 544 |
):
|
| 545 |
"""Update proxy configuration (alias for frontend compatibility)"""
|
| 546 |
+
await proxy_manager.update_proxy_config(request.proxy_enabled, request.proxy_url)
|
| 547 |
+
return {"success": True, "message": "代理配置更新成功"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 548 |
|
| 549 |
|
| 550 |
@router.post("/api/config/proxy")
|
|
|
|
| 553 |
token: str = Depends(verify_admin_token)
|
| 554 |
):
|
| 555 |
"""Update proxy configuration"""
|
| 556 |
+
await proxy_manager.update_proxy_config(request.proxy_enabled, request.proxy_url)
|
| 557 |
+
return {"success": True, "message": "代理配置更新成功"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 558 |
|
| 559 |
|
| 560 |
@router.get("/api/config/generation")
|
|
|
|
| 706 |
token: str = Depends(verify_admin_token)
|
| 707 |
):
|
| 708 |
"""Update admin configuration (error_ban_threshold)"""
|
| 709 |
+
# Update error_ban_threshold in database
|
| 710 |
+
await db.update_admin_config(error_ban_threshold=request.error_ban_threshold)
|
|
|
|
| 711 |
|
| 712 |
+
return {"success": True, "message": "配置更新成功"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 713 |
|
| 714 |
|
| 715 |
@router.post("/api/admin/password")
|
|
|
|
| 765 |
token: str = Depends(verify_admin_token)
|
| 766 |
):
|
| 767 |
"""Update generation timeout configuration"""
|
| 768 |
+
await db.update_generation_config(request.image_timeout, request.video_timeout)
|
|
|
|
| 769 |
|
| 770 |
+
# 🔥 Hot reload: sync database config to memory
|
| 771 |
+
await db.reload_config_to_memory()
|
| 772 |
|
| 773 |
+
return {"success": True, "message": "生成配置更新成功"}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 774 |
|
| 775 |
|
| 776 |
# ========== AT Auto Refresh Config ==========
|
|
|
|
| 839 |
token: str = Depends(verify_admin_token)
|
| 840 |
):
|
| 841 |
"""Update complete cache configuration"""
|
| 842 |
+
enabled = request.get("enabled")
|
| 843 |
+
timeout = request.get("timeout")
|
| 844 |
+
base_url = request.get("base_url")
|
|
|
|
| 845 |
|
| 846 |
+
await db.update_cache_config(enabled=enabled, timeout=timeout, base_url=base_url)
|
| 847 |
|
| 848 |
+
# 🔥 Hot reload: sync database config to memory
|
| 849 |
+
await db.reload_config_to_memory()
|
| 850 |
|
| 851 |
+
return {"success": True, "message": "缓存配置更新成功"}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 852 |
|
| 853 |
|
| 854 |
@router.post("/api/cache/base-url")
|
|
|
|
| 857 |
token: str = Depends(verify_admin_token)
|
| 858 |
):
|
| 859 |
"""Update cache base URL"""
|
| 860 |
+
base_url = request.get("base_url", "")
|
| 861 |
+
await db.update_cache_config(base_url=base_url)
|
|
|
|
| 862 |
|
| 863 |
+
# 🔥 Hot reload: sync database config to memory
|
| 864 |
+
await db.reload_config_to_memory()
|
| 865 |
|
| 866 |
+
return {"success": True, "message": "缓存Base URL更新成功"}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 867 |
|
| 868 |
|
| 869 |
@router.post("/api/captcha/config")
|
|
|
|
| 872 |
token: str = Depends(verify_admin_token)
|
| 873 |
):
|
| 874 |
"""Update captcha configuration"""
|
| 875 |
+
from ..services.browser_captcha import validate_browser_proxy_url
|
| 876 |
+
|
| 877 |
+
captcha_method = request.get("captcha_method")
|
| 878 |
+
yescaptcha_api_key = request.get("yescaptcha_api_key")
|
| 879 |
+
yescaptcha_base_url = request.get("yescaptcha_base_url")
|
| 880 |
+
capmonster_api_key = request.get("capmonster_api_key")
|
| 881 |
+
capmonster_base_url = request.get("capmonster_base_url")
|
| 882 |
+
ezcaptcha_api_key = request.get("ezcaptcha_api_key")
|
| 883 |
+
ezcaptcha_base_url = request.get("ezcaptcha_base_url")
|
| 884 |
+
capsolver_api_key = request.get("capsolver_api_key")
|
| 885 |
+
capsolver_base_url = request.get("capsolver_base_url")
|
| 886 |
+
browser_proxy_enabled = request.get("browser_proxy_enabled", False)
|
| 887 |
+
browser_proxy_url = request.get("browser_proxy_url", "")
|
| 888 |
+
|
| 889 |
+
# 验证浏览器代理URL格式
|
| 890 |
+
if browser_proxy_enabled and browser_proxy_url:
|
| 891 |
+
is_valid, error_msg = validate_browser_proxy_url(browser_proxy_url)
|
| 892 |
+
if not is_valid:
|
| 893 |
+
return {"success": False, "message": error_msg}
|
| 894 |
+
|
| 895 |
+
await db.update_captcha_config(
|
| 896 |
+
captcha_method=captcha_method,
|
| 897 |
+
yescaptcha_api_key=yescaptcha_api_key,
|
| 898 |
+
yescaptcha_base_url=yescaptcha_base_url,
|
| 899 |
+
capmonster_api_key=capmonster_api_key,
|
| 900 |
+
capmonster_base_url=capmonster_base_url,
|
| 901 |
+
ezcaptcha_api_key=ezcaptcha_api_key,
|
| 902 |
+
ezcaptcha_base_url=ezcaptcha_base_url,
|
| 903 |
+
capsolver_api_key=capsolver_api_key,
|
| 904 |
+
capsolver_base_url=capsolver_base_url,
|
| 905 |
+
browser_proxy_enabled=browser_proxy_enabled,
|
| 906 |
+
browser_proxy_url=browser_proxy_url if browser_proxy_enabled else None
|
| 907 |
+
)
|
| 908 |
|
| 909 |
+
# 🔥 Hot reload: sync database config to memory
|
| 910 |
+
await db.reload_config_to_memory()
|
| 911 |
|
| 912 |
+
return {"success": True, "message": "验证码配置更新成功"}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 913 |
|
| 914 |
|
| 915 |
@router.get("/api/captcha/config")
|
|
|
|
| 920 |
"captcha_method": captcha_config.captcha_method,
|
| 921 |
"yescaptcha_api_key": captcha_config.yescaptcha_api_key,
|
| 922 |
"yescaptcha_base_url": captcha_config.yescaptcha_base_url,
|
| 923 |
+
"capmonster_api_key": captcha_config.capmonster_api_key,
|
| 924 |
+
"capmonster_base_url": captcha_config.capmonster_base_url,
|
| 925 |
+
"ezcaptcha_api_key": captcha_config.ezcaptcha_api_key,
|
| 926 |
+
"ezcaptcha_base_url": captcha_config.ezcaptcha_base_url,
|
| 927 |
+
"capsolver_api_key": captcha_config.capsolver_api_key,
|
| 928 |
+
"capsolver_base_url": captcha_config.capsolver_base_url,
|
| 929 |
"browser_proxy_enabled": captcha_config.browser_proxy_enabled,
|
| 930 |
"browser_proxy_url": captcha_config.browser_proxy_url or ""
|
| 931 |
}
|
|
|
|
| 973 |
token: str = Depends(verify_admin_token)
|
| 974 |
):
|
| 975 |
"""Update plugin configuration"""
|
| 976 |
+
connection_token = request.get("connection_token", "")
|
| 977 |
+
auto_enable_on_update = request.get("auto_enable_on_update", True) # 默认开启
|
|
|
|
| 978 |
|
| 979 |
+
# Generate random token if empty
|
| 980 |
+
if not connection_token:
|
| 981 |
+
connection_token = secrets.token_urlsafe(32)
|
| 982 |
|
| 983 |
+
await db.update_plugin_config(
|
| 984 |
+
connection_token=connection_token,
|
| 985 |
+
auto_enable_on_update=auto_enable_on_update
|
| 986 |
+
)
|
| 987 |
|
| 988 |
+
return {
|
| 989 |
+
"success": True,
|
| 990 |
+
"message": "插件配置更新成功",
|
| 991 |
+
"connection_token": connection_token,
|
| 992 |
+
"auto_enable_on_update": auto_enable_on_update
|
| 993 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 994 |
|
| 995 |
|
| 996 |
@router.post("/api/plugin/update-token")
|
src/api/routes.py
CHANGED
|
@@ -111,7 +111,7 @@ async def create_chat_completion(
|
|
| 111 |
if item.get("type") == "text":
|
| 112 |
prompt = item.get("text", "")
|
| 113 |
elif item.get("type") == "image_url":
|
| 114 |
-
# Extract
|
| 115 |
image_url = item.get("image_url", {}).get("url", "")
|
| 116 |
if image_url.startswith("data:image"):
|
| 117 |
# Parse base64
|
|
@@ -120,6 +120,18 @@ async def create_chat_completion(
|
|
| 120 |
image_base64 = match.group(1)
|
| 121 |
image_bytes = base64.b64decode(image_base64)
|
| 122 |
images.append(image_bytes)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
|
| 124 |
# Fallback to deprecated image parameter
|
| 125 |
if request.image and not images:
|
|
|
|
| 111 |
if item.get("type") == "text":
|
| 112 |
prompt = item.get("text", "")
|
| 113 |
elif item.get("type") == "image_url":
|
| 114 |
+
# Extract image from URL or base64
|
| 115 |
image_url = item.get("image_url", {}).get("url", "")
|
| 116 |
if image_url.startswith("data:image"):
|
| 117 |
# Parse base64
|
|
|
|
| 120 |
image_base64 = match.group(1)
|
| 121 |
image_bytes = base64.b64decode(image_base64)
|
| 122 |
images.append(image_bytes)
|
| 123 |
+
elif image_url.startswith("http://") or image_url.startswith("https://"):
|
| 124 |
+
# Download remote image URL
|
| 125 |
+
debug_logger.log_info(f"[IMAGE_URL] 下载远程图片: {image_url}")
|
| 126 |
+
try:
|
| 127 |
+
downloaded_bytes = await retrieve_image_data(image_url)
|
| 128 |
+
if downloaded_bytes and len(downloaded_bytes) > 0:
|
| 129 |
+
images.append(downloaded_bytes)
|
| 130 |
+
debug_logger.log_info(f"[IMAGE_URL] ✅ 远程图片下载成功: {len(downloaded_bytes)} 字节")
|
| 131 |
+
else:
|
| 132 |
+
debug_logger.log_warning(f"[IMAGE_URL] ⚠️ 远程图片下载失败或为空: {image_url}")
|
| 133 |
+
except Exception as e:
|
| 134 |
+
debug_logger.log_error(f"[IMAGE_URL] ❌ 远程图片下载异常: {str(e)}")
|
| 135 |
|
| 136 |
# Fallback to deprecated image parameter
|
| 137 |
if request.image and not images:
|
src/core/config.py
CHANGED
|
@@ -213,6 +213,72 @@ class Config:
|
|
| 213 |
self._config["captcha"] = {}
|
| 214 |
self._config["captcha"]["yescaptcha_base_url"] = base_url
|
| 215 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 216 |
|
| 217 |
# Global config instance
|
| 218 |
config = Config()
|
|
|
|
| 213 |
self._config["captcha"] = {}
|
| 214 |
self._config["captcha"]["yescaptcha_base_url"] = base_url
|
| 215 |
|
| 216 |
+
@property
|
| 217 |
+
def capmonster_api_key(self) -> str:
|
| 218 |
+
"""Get CapMonster API key"""
|
| 219 |
+
return self._config.get("captcha", {}).get("capmonster_api_key", "")
|
| 220 |
+
|
| 221 |
+
def set_capmonster_api_key(self, api_key: str):
|
| 222 |
+
"""Set CapMonster API key"""
|
| 223 |
+
if "captcha" not in self._config:
|
| 224 |
+
self._config["captcha"] = {}
|
| 225 |
+
self._config["captcha"]["capmonster_api_key"] = api_key
|
| 226 |
+
|
| 227 |
+
@property
|
| 228 |
+
def capmonster_base_url(self) -> str:
|
| 229 |
+
"""Get CapMonster base URL"""
|
| 230 |
+
return self._config.get("captcha", {}).get("capmonster_base_url", "https://api.capmonster.cloud")
|
| 231 |
+
|
| 232 |
+
def set_capmonster_base_url(self, base_url: str):
|
| 233 |
+
"""Set CapMonster base URL"""
|
| 234 |
+
if "captcha" not in self._config:
|
| 235 |
+
self._config["captcha"] = {}
|
| 236 |
+
self._config["captcha"]["capmonster_base_url"] = base_url
|
| 237 |
+
|
| 238 |
+
@property
|
| 239 |
+
def ezcaptcha_api_key(self) -> str:
|
| 240 |
+
"""Get EzCaptcha API key"""
|
| 241 |
+
return self._config.get("captcha", {}).get("ezcaptcha_api_key", "")
|
| 242 |
+
|
| 243 |
+
def set_ezcaptcha_api_key(self, api_key: str):
|
| 244 |
+
"""Set EzCaptcha API key"""
|
| 245 |
+
if "captcha" not in self._config:
|
| 246 |
+
self._config["captcha"] = {}
|
| 247 |
+
self._config["captcha"]["ezcaptcha_api_key"] = api_key
|
| 248 |
+
|
| 249 |
+
@property
|
| 250 |
+
def ezcaptcha_base_url(self) -> str:
|
| 251 |
+
"""Get EzCaptcha base URL"""
|
| 252 |
+
return self._config.get("captcha", {}).get("ezcaptcha_base_url", "https://api.ez-captcha.com")
|
| 253 |
+
|
| 254 |
+
def set_ezcaptcha_base_url(self, base_url: str):
|
| 255 |
+
"""Set EzCaptcha base URL"""
|
| 256 |
+
if "captcha" not in self._config:
|
| 257 |
+
self._config["captcha"] = {}
|
| 258 |
+
self._config["captcha"]["ezcaptcha_base_url"] = base_url
|
| 259 |
+
|
| 260 |
+
@property
|
| 261 |
+
def capsolver_api_key(self) -> str:
|
| 262 |
+
"""Get CapSolver API key"""
|
| 263 |
+
return self._config.get("captcha", {}).get("capsolver_api_key", "")
|
| 264 |
+
|
| 265 |
+
def set_capsolver_api_key(self, api_key: str):
|
| 266 |
+
"""Set CapSolver API key"""
|
| 267 |
+
if "captcha" not in self._config:
|
| 268 |
+
self._config["captcha"] = {}
|
| 269 |
+
self._config["captcha"]["capsolver_api_key"] = api_key
|
| 270 |
+
|
| 271 |
+
@property
|
| 272 |
+
def capsolver_base_url(self) -> str:
|
| 273 |
+
"""Get CapSolver base URL"""
|
| 274 |
+
return self._config.get("captcha", {}).get("capsolver_base_url", "https://api.capsolver.com")
|
| 275 |
+
|
| 276 |
+
def set_capsolver_base_url(self, base_url: str):
|
| 277 |
+
"""Set CapSolver base URL"""
|
| 278 |
+
if "captcha" not in self._config:
|
| 279 |
+
self._config["captcha"] = {}
|
| 280 |
+
self._config["captcha"]["capsolver_base_url"] = base_url
|
| 281 |
+
|
| 282 |
|
| 283 |
# Global config instance
|
| 284 |
config = Config()
|
src/core/database.py
CHANGED
|
@@ -172,8 +172,8 @@ class Database:
|
|
| 172 |
count = await cursor.fetchone()
|
| 173 |
if count[0] == 0:
|
| 174 |
await db.execute("""
|
| 175 |
-
INSERT INTO plugin_config (id, connection_token
|
| 176 |
-
VALUES (1, ''
|
| 177 |
""")
|
| 178 |
|
| 179 |
async def check_and_migrate_db(self, config_dict: dict = None):
|
|
@@ -216,6 +216,12 @@ class Database:
|
|
| 216 |
captcha_method TEXT DEFAULT 'browser',
|
| 217 |
yescaptcha_api_key TEXT DEFAULT '',
|
| 218 |
yescaptcha_base_url TEXT DEFAULT 'https://api.yescaptcha.com',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
website_key TEXT DEFAULT '6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV',
|
| 220 |
page_action TEXT DEFAULT 'FLOW_GENERATION',
|
| 221 |
browser_proxy_enabled BOOLEAN DEFAULT 0,
|
|
@@ -232,7 +238,6 @@ class Database:
|
|
| 232 |
CREATE TABLE plugin_config (
|
| 233 |
id INTEGER PRIMARY KEY DEFAULT 1,
|
| 234 |
connection_token TEXT DEFAULT '',
|
| 235 |
-
auto_enable_on_update BOOLEAN DEFAULT 1,
|
| 236 |
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 237 |
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
| 238 |
)
|
|
@@ -278,6 +283,12 @@ class Database:
|
|
| 278 |
captcha_columns_to_add = [
|
| 279 |
("browser_proxy_enabled", "BOOLEAN DEFAULT 0"),
|
| 280 |
("browser_proxy_url", "TEXT"),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 281 |
]
|
| 282 |
|
| 283 |
for col_name, col_type in captcha_columns_to_add:
|
|
@@ -490,6 +501,12 @@ class Database:
|
|
| 490 |
captcha_method TEXT DEFAULT 'browser',
|
| 491 |
yescaptcha_api_key TEXT DEFAULT '',
|
| 492 |
yescaptcha_base_url TEXT DEFAULT 'https://api.yescaptcha.com',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 493 |
website_key TEXT DEFAULT '6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV',
|
| 494 |
page_action TEXT DEFAULT 'FLOW_GENERATION',
|
| 495 |
browser_proxy_enabled BOOLEAN DEFAULT 0,
|
|
@@ -504,7 +521,6 @@ class Database:
|
|
| 504 |
CREATE TABLE IF NOT EXISTS plugin_config (
|
| 505 |
id INTEGER PRIMARY KEY DEFAULT 1,
|
| 506 |
connection_token TEXT DEFAULT '',
|
| 507 |
-
auto_enable_on_update BOOLEAN DEFAULT 1,
|
| 508 |
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 509 |
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
| 510 |
)
|
|
@@ -937,32 +953,23 @@ class Database:
|
|
| 937 |
|
| 938 |
async def get_generation_config(self) -> Optional[GenerationConfig]:
|
| 939 |
"""Get generation configuration"""
|
| 940 |
-
|
| 941 |
-
|
| 942 |
-
|
| 943 |
-
|
| 944 |
-
|
| 945 |
-
|
| 946 |
-
|
| 947 |
-
return None
|
| 948 |
-
except Exception as e:
|
| 949 |
-
print(f"Error getting generation config: {e}")
|
| 950 |
-
# 返回默认配置,避免异常传播
|
| 951 |
-
return GenerationConfig(image_timeout=300, video_timeout=1500)
|
| 952 |
|
| 953 |
async def update_generation_config(self, image_timeout: int, video_timeout: int):
|
| 954 |
"""Update generation configuration"""
|
| 955 |
-
|
| 956 |
-
|
| 957 |
-
|
| 958 |
-
|
| 959 |
-
|
| 960 |
-
|
| 961 |
-
|
| 962 |
-
await db.commit()
|
| 963 |
-
except Exception as e:
|
| 964 |
-
print(f"Error updating generation config: {e}")
|
| 965 |
-
raise
|
| 966 |
|
| 967 |
# Request log operations
|
| 968 |
async def add_request_log(self, log: RequestLog):
|
|
@@ -1057,43 +1064,39 @@ class Database:
|
|
| 1057 |
- Generation config (image_timeout, video_timeout)
|
| 1058 |
- Proxy config will be handled by ProxyManager
|
| 1059 |
"""
|
| 1060 |
-
|
| 1061 |
-
|
| 1062 |
-
|
| 1063 |
-
|
| 1064 |
-
|
| 1065 |
-
|
| 1066 |
-
|
| 1067 |
-
|
| 1068 |
-
|
| 1069 |
-
|
| 1070 |
-
|
| 1071 |
-
|
| 1072 |
-
|
| 1073 |
-
|
| 1074 |
-
|
| 1075 |
-
|
| 1076 |
-
|
| 1077 |
-
|
| 1078 |
-
|
| 1079 |
-
|
| 1080 |
-
|
| 1081 |
-
|
| 1082 |
-
|
| 1083 |
-
|
| 1084 |
-
|
| 1085 |
-
|
| 1086 |
-
|
| 1087 |
-
|
| 1088 |
-
|
| 1089 |
-
|
| 1090 |
-
|
| 1091 |
-
|
| 1092 |
-
|
| 1093 |
-
config.set_yescaptcha_base_url(captcha_config.yescaptcha_base_url)
|
| 1094 |
-
except Exception as e:
|
| 1095 |
-
print(f"Error reloading config to memory: {e}")
|
| 1096 |
-
# 不要让异常传播,避免返回 500 错误
|
| 1097 |
|
| 1098 |
# Cache config operations
|
| 1099 |
async def get_cache_config(self) -> CacheConfig:
|
|
@@ -1214,6 +1217,12 @@ class Database:
|
|
| 1214 |
captcha_method: str = None,
|
| 1215 |
yescaptcha_api_key: str = None,
|
| 1216 |
yescaptcha_base_url: str = None,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1217 |
browser_proxy_enabled: bool = None,
|
| 1218 |
browser_proxy_url: str = None
|
| 1219 |
):
|
|
@@ -1226,28 +1235,47 @@ class Database:
|
|
| 1226 |
if row:
|
| 1227 |
current = dict(row)
|
| 1228 |
new_method = captcha_method if captcha_method is not None else current.get("captcha_method", "yescaptcha")
|
| 1229 |
-
|
| 1230 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1231 |
new_proxy_enabled = browser_proxy_enabled if browser_proxy_enabled is not None else current.get("browser_proxy_enabled", False)
|
| 1232 |
new_proxy_url = browser_proxy_url if browser_proxy_url is not None else current.get("browser_proxy_url")
|
| 1233 |
|
| 1234 |
await db.execute("""
|
| 1235 |
UPDATE captcha_config
|
| 1236 |
SET captcha_method = ?, yescaptcha_api_key = ?, yescaptcha_base_url = ?,
|
|
|
|
|
|
|
|
|
|
| 1237 |
browser_proxy_enabled = ?, browser_proxy_url = ?, updated_at = CURRENT_TIMESTAMP
|
| 1238 |
WHERE id = 1
|
| 1239 |
-
""", (new_method,
|
|
|
|
| 1240 |
else:
|
| 1241 |
new_method = captcha_method if captcha_method is not None else "yescaptcha"
|
| 1242 |
-
|
| 1243 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1244 |
new_proxy_enabled = browser_proxy_enabled if browser_proxy_enabled is not None else False
|
| 1245 |
new_proxy_url = browser_proxy_url
|
| 1246 |
|
| 1247 |
await db.execute("""
|
| 1248 |
-
INSERT INTO captcha_config (id, captcha_method, yescaptcha_api_key, yescaptcha_base_url,
|
| 1249 |
-
|
| 1250 |
-
|
|
|
|
|
|
|
|
|
|
| 1251 |
|
| 1252 |
await db.commit()
|
| 1253 |
|
|
|
|
| 172 |
count = await cursor.fetchone()
|
| 173 |
if count[0] == 0:
|
| 174 |
await db.execute("""
|
| 175 |
+
INSERT INTO plugin_config (id, connection_token)
|
| 176 |
+
VALUES (1, '')
|
| 177 |
""")
|
| 178 |
|
| 179 |
async def check_and_migrate_db(self, config_dict: dict = None):
|
|
|
|
| 216 |
captcha_method TEXT DEFAULT 'browser',
|
| 217 |
yescaptcha_api_key TEXT DEFAULT '',
|
| 218 |
yescaptcha_base_url TEXT DEFAULT 'https://api.yescaptcha.com',
|
| 219 |
+
capmonster_api_key TEXT DEFAULT '',
|
| 220 |
+
capmonster_base_url TEXT DEFAULT 'https://api.capmonster.cloud',
|
| 221 |
+
ezcaptcha_api_key TEXT DEFAULT '',
|
| 222 |
+
ezcaptcha_base_url TEXT DEFAULT 'https://api.ez-captcha.com',
|
| 223 |
+
capsolver_api_key TEXT DEFAULT '',
|
| 224 |
+
capsolver_base_url TEXT DEFAULT 'https://api.capsolver.com',
|
| 225 |
website_key TEXT DEFAULT '6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV',
|
| 226 |
page_action TEXT DEFAULT 'FLOW_GENERATION',
|
| 227 |
browser_proxy_enabled BOOLEAN DEFAULT 0,
|
|
|
|
| 238 |
CREATE TABLE plugin_config (
|
| 239 |
id INTEGER PRIMARY KEY DEFAULT 1,
|
| 240 |
connection_token TEXT DEFAULT '',
|
|
|
|
| 241 |
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 242 |
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
| 243 |
)
|
|
|
|
| 283 |
captcha_columns_to_add = [
|
| 284 |
("browser_proxy_enabled", "BOOLEAN DEFAULT 0"),
|
| 285 |
("browser_proxy_url", "TEXT"),
|
| 286 |
+
("capmonster_api_key", "TEXT DEFAULT ''"),
|
| 287 |
+
("capmonster_base_url", "TEXT DEFAULT 'https://api.capmonster.cloud'"),
|
| 288 |
+
("ezcaptcha_api_key", "TEXT DEFAULT ''"),
|
| 289 |
+
("ezcaptcha_base_url", "TEXT DEFAULT 'https://api.ez-captcha.com'"),
|
| 290 |
+
("capsolver_api_key", "TEXT DEFAULT ''"),
|
| 291 |
+
("capsolver_base_url", "TEXT DEFAULT 'https://api.capsolver.com'"),
|
| 292 |
]
|
| 293 |
|
| 294 |
for col_name, col_type in captcha_columns_to_add:
|
|
|
|
| 501 |
captcha_method TEXT DEFAULT 'browser',
|
| 502 |
yescaptcha_api_key TEXT DEFAULT '',
|
| 503 |
yescaptcha_base_url TEXT DEFAULT 'https://api.yescaptcha.com',
|
| 504 |
+
capmonster_api_key TEXT DEFAULT '',
|
| 505 |
+
capmonster_base_url TEXT DEFAULT 'https://api.capmonster.cloud',
|
| 506 |
+
ezcaptcha_api_key TEXT DEFAULT '',
|
| 507 |
+
ezcaptcha_base_url TEXT DEFAULT 'https://api.ez-captcha.com',
|
| 508 |
+
capsolver_api_key TEXT DEFAULT '',
|
| 509 |
+
capsolver_base_url TEXT DEFAULT 'https://api.capsolver.com',
|
| 510 |
website_key TEXT DEFAULT '6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV',
|
| 511 |
page_action TEXT DEFAULT 'FLOW_GENERATION',
|
| 512 |
browser_proxy_enabled BOOLEAN DEFAULT 0,
|
|
|
|
| 521 |
CREATE TABLE IF NOT EXISTS plugin_config (
|
| 522 |
id INTEGER PRIMARY KEY DEFAULT 1,
|
| 523 |
connection_token TEXT DEFAULT '',
|
|
|
|
| 524 |
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 525 |
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
| 526 |
)
|
|
|
|
| 953 |
|
| 954 |
async def get_generation_config(self) -> Optional[GenerationConfig]:
|
| 955 |
"""Get generation configuration"""
|
| 956 |
+
async with aiosqlite.connect(self.db_path) as db:
|
| 957 |
+
db.row_factory = aiosqlite.Row
|
| 958 |
+
cursor = await db.execute("SELECT * FROM generation_config WHERE id = 1")
|
| 959 |
+
row = await cursor.fetchone()
|
| 960 |
+
if row:
|
| 961 |
+
return GenerationConfig(**dict(row))
|
| 962 |
+
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 963 |
|
| 964 |
async def update_generation_config(self, image_timeout: int, video_timeout: int):
|
| 965 |
"""Update generation configuration"""
|
| 966 |
+
async with aiosqlite.connect(self.db_path) as db:
|
| 967 |
+
await db.execute("""
|
| 968 |
+
UPDATE generation_config
|
| 969 |
+
SET image_timeout = ?, video_timeout = ?, updated_at = CURRENT_TIMESTAMP
|
| 970 |
+
WHERE id = 1
|
| 971 |
+
""", (image_timeout, video_timeout))
|
| 972 |
+
await db.commit()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 973 |
|
| 974 |
# Request log operations
|
| 975 |
async def add_request_log(self, log: RequestLog):
|
|
|
|
| 1064 |
- Generation config (image_timeout, video_timeout)
|
| 1065 |
- Proxy config will be handled by ProxyManager
|
| 1066 |
"""
|
| 1067 |
+
from .config import config
|
| 1068 |
+
|
| 1069 |
+
# Reload admin config
|
| 1070 |
+
admin_config = await self.get_admin_config()
|
| 1071 |
+
if admin_config:
|
| 1072 |
+
config.set_admin_username_from_db(admin_config.username)
|
| 1073 |
+
config.set_admin_password_from_db(admin_config.password)
|
| 1074 |
+
config.api_key = admin_config.api_key
|
| 1075 |
+
|
| 1076 |
+
# Reload cache config
|
| 1077 |
+
cache_config = await self.get_cache_config()
|
| 1078 |
+
if cache_config:
|
| 1079 |
+
config.set_cache_enabled(cache_config.cache_enabled)
|
| 1080 |
+
config.set_cache_timeout(cache_config.cache_timeout)
|
| 1081 |
+
config.set_cache_base_url(cache_config.cache_base_url or "")
|
| 1082 |
+
|
| 1083 |
+
# Reload generation config
|
| 1084 |
+
generation_config = await self.get_generation_config()
|
| 1085 |
+
if generation_config:
|
| 1086 |
+
config.set_image_timeout(generation_config.image_timeout)
|
| 1087 |
+
config.set_video_timeout(generation_config.video_timeout)
|
| 1088 |
+
|
| 1089 |
+
# Reload debug config
|
| 1090 |
+
debug_config = await self.get_debug_config()
|
| 1091 |
+
if debug_config:
|
| 1092 |
+
config.set_debug_enabled(debug_config.enabled)
|
| 1093 |
+
|
| 1094 |
+
# Reload captcha config
|
| 1095 |
+
captcha_config = await self.get_captcha_config()
|
| 1096 |
+
if captcha_config:
|
| 1097 |
+
config.set_captcha_method(captcha_config.captcha_method)
|
| 1098 |
+
config.set_yescaptcha_api_key(captcha_config.yescaptcha_api_key)
|
| 1099 |
+
config.set_yescaptcha_base_url(captcha_config.yescaptcha_base_url)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1100 |
|
| 1101 |
# Cache config operations
|
| 1102 |
async def get_cache_config(self) -> CacheConfig:
|
|
|
|
| 1217 |
captcha_method: str = None,
|
| 1218 |
yescaptcha_api_key: str = None,
|
| 1219 |
yescaptcha_base_url: str = None,
|
| 1220 |
+
capmonster_api_key: str = None,
|
| 1221 |
+
capmonster_base_url: str = None,
|
| 1222 |
+
ezcaptcha_api_key: str = None,
|
| 1223 |
+
ezcaptcha_base_url: str = None,
|
| 1224 |
+
capsolver_api_key: str = None,
|
| 1225 |
+
capsolver_base_url: str = None,
|
| 1226 |
browser_proxy_enabled: bool = None,
|
| 1227 |
browser_proxy_url: str = None
|
| 1228 |
):
|
|
|
|
| 1235 |
if row:
|
| 1236 |
current = dict(row)
|
| 1237 |
new_method = captcha_method if captcha_method is not None else current.get("captcha_method", "yescaptcha")
|
| 1238 |
+
new_yes_key = yescaptcha_api_key if yescaptcha_api_key is not None else current.get("yescaptcha_api_key", "")
|
| 1239 |
+
new_yes_url = yescaptcha_base_url if yescaptcha_base_url is not None else current.get("yescaptcha_base_url", "https://api.yescaptcha.com")
|
| 1240 |
+
new_cap_key = capmonster_api_key if capmonster_api_key is not None else current.get("capmonster_api_key", "")
|
| 1241 |
+
new_cap_url = capmonster_base_url if capmonster_base_url is not None else current.get("capmonster_base_url", "https://api.capmonster.cloud")
|
| 1242 |
+
new_ez_key = ezcaptcha_api_key if ezcaptcha_api_key is not None else current.get("ezcaptcha_api_key", "")
|
| 1243 |
+
new_ez_url = ezcaptcha_base_url if ezcaptcha_base_url is not None else current.get("ezcaptcha_base_url", "https://api.ez-captcha.com")
|
| 1244 |
+
new_cs_key = capsolver_api_key if capsolver_api_key is not None else current.get("capsolver_api_key", "")
|
| 1245 |
+
new_cs_url = capsolver_base_url if capsolver_base_url is not None else current.get("capsolver_base_url", "https://api.capsolver.com")
|
| 1246 |
new_proxy_enabled = browser_proxy_enabled if browser_proxy_enabled is not None else current.get("browser_proxy_enabled", False)
|
| 1247 |
new_proxy_url = browser_proxy_url if browser_proxy_url is not None else current.get("browser_proxy_url")
|
| 1248 |
|
| 1249 |
await db.execute("""
|
| 1250 |
UPDATE captcha_config
|
| 1251 |
SET captcha_method = ?, yescaptcha_api_key = ?, yescaptcha_base_url = ?,
|
| 1252 |
+
capmonster_api_key = ?, capmonster_base_url = ?,
|
| 1253 |
+
ezcaptcha_api_key = ?, ezcaptcha_base_url = ?,
|
| 1254 |
+
capsolver_api_key = ?, capsolver_base_url = ?,
|
| 1255 |
browser_proxy_enabled = ?, browser_proxy_url = ?, updated_at = CURRENT_TIMESTAMP
|
| 1256 |
WHERE id = 1
|
| 1257 |
+
""", (new_method, new_yes_key, new_yes_url, new_cap_key, new_cap_url,
|
| 1258 |
+
new_ez_key, new_ez_url, new_cs_key, new_cs_url, new_proxy_enabled, new_proxy_url))
|
| 1259 |
else:
|
| 1260 |
new_method = captcha_method if captcha_method is not None else "yescaptcha"
|
| 1261 |
+
new_yes_key = yescaptcha_api_key if yescaptcha_api_key is not None else ""
|
| 1262 |
+
new_yes_url = yescaptcha_base_url if yescaptcha_base_url is not None else "https://api.yescaptcha.com"
|
| 1263 |
+
new_cap_key = capmonster_api_key if capmonster_api_key is not None else ""
|
| 1264 |
+
new_cap_url = capmonster_base_url if capmonster_base_url is not None else "https://api.capmonster.cloud"
|
| 1265 |
+
new_ez_key = ezcaptcha_api_key if ezcaptcha_api_key is not None else ""
|
| 1266 |
+
new_ez_url = ezcaptcha_base_url if ezcaptcha_base_url is not None else "https://api.ez-captcha.com"
|
| 1267 |
+
new_cs_key = capsolver_api_key if capsolver_api_key is not None else ""
|
| 1268 |
+
new_cs_url = capsolver_base_url if capsolver_base_url is not None else "https://api.capsolver.com"
|
| 1269 |
new_proxy_enabled = browser_proxy_enabled if browser_proxy_enabled is not None else False
|
| 1270 |
new_proxy_url = browser_proxy_url
|
| 1271 |
|
| 1272 |
await db.execute("""
|
| 1273 |
+
INSERT INTO captcha_config (id, captcha_method, yescaptcha_api_key, yescaptcha_base_url,
|
| 1274 |
+
capmonster_api_key, capmonster_base_url, ezcaptcha_api_key, ezcaptcha_base_url,
|
| 1275 |
+
capsolver_api_key, capsolver_base_url, browser_proxy_enabled, browser_proxy_url)
|
| 1276 |
+
VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
| 1277 |
+
""", (new_method, new_yes_key, new_yes_url, new_cap_key, new_cap_url,
|
| 1278 |
+
new_ez_key, new_ez_url, new_cs_key, new_cs_url, new_proxy_enabled, new_proxy_url))
|
| 1279 |
|
| 1280 |
await db.commit()
|
| 1281 |
|
src/core/models.py
CHANGED
|
@@ -147,9 +147,15 @@ class DebugConfig(BaseModel):
|
|
| 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 |
browser_proxy_enabled: bool = False # 浏览器打码是否启用代理
|
|
|
|
| 147 |
class CaptchaConfig(BaseModel):
|
| 148 |
"""Captcha configuration"""
|
| 149 |
id: int = 1
|
| 150 |
+
captcha_method: str = "browser" # yescaptcha, capmonster, ezcaptcha, capsolver 或 browser
|
| 151 |
yescaptcha_api_key: str = ""
|
| 152 |
yescaptcha_base_url: str = "https://api.yescaptcha.com"
|
| 153 |
+
capmonster_api_key: str = ""
|
| 154 |
+
capmonster_base_url: str = "https://api.capmonster.cloud"
|
| 155 |
+
ezcaptcha_api_key: str = ""
|
| 156 |
+
ezcaptcha_base_url: str = "https://api.ez-captcha.com"
|
| 157 |
+
capsolver_api_key: str = ""
|
| 158 |
+
capsolver_base_url: str = "https://api.capsolver.com"
|
| 159 |
website_key: str = "6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV"
|
| 160 |
page_action: str = "FLOW_GENERATION"
|
| 161 |
browser_proxy_enabled: bool = False # 浏览器打码是否启用代理
|
src/main.py
CHANGED
|
@@ -68,66 +68,48 @@ async def lifespan(app: FastAPI):
|
|
| 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 == "personal":
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
captcha_method="yescaptcha",
|
| 99 |
-
yescaptcha_api_key=captcha_config.yescaptcha_api_key,
|
| 100 |
-
yescaptcha_base_url=captcha_config.yescaptcha_base_url
|
| 101 |
-
)
|
| 102 |
-
print("✓ Captcha method automatically switched to yescaptcha")
|
| 103 |
elif captcha_config.captcha_method == "browser":
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
from .services.browser_captcha import BrowserCaptchaService
|
| 108 |
-
browser_service = await BrowserCaptchaService.get_instance(db)
|
| 109 |
-
print("✓ Browser captcha service initialized (headless mode)")
|
| 110 |
-
else:
|
| 111 |
-
print("⚠️ Playwright not available. Please use YesCaptcha service instead.")
|
| 112 |
-
# 自动切换到 yescaptcha 方法
|
| 113 |
-
await db.update_captcha_config(
|
| 114 |
-
captcha_method="yescaptcha",
|
| 115 |
-
yescaptcha_api_key=captcha_config.yescaptcha_api_key,
|
| 116 |
-
yescaptcha_base_url=captcha_config.yescaptcha_base_url
|
| 117 |
-
)
|
| 118 |
-
print("✓ Captcha method automatically switched to yescaptcha")
|
| 119 |
-
except ImportError:
|
| 120 |
-
print("⚠️ Playwright not available. Please use YesCaptcha service instead.")
|
| 121 |
-
# 自动切换到 yescaptcha 方法
|
| 122 |
-
await db.update_captcha_config(
|
| 123 |
-
captcha_method="yescaptcha",
|
| 124 |
-
yescaptcha_api_key=captcha_config.yescaptcha_api_key,
|
| 125 |
-
yescaptcha_base_url=captcha_config.yescaptcha_base_url
|
| 126 |
-
)
|
| 127 |
-
print("✓ Captcha method automatically switched to yescaptcha")
|
| 128 |
|
| 129 |
# Initialize concurrency manager
|
| 130 |
tokens = await token_manager.get_all_tokens()
|
|
|
|
| 131 |
await concurrency_manager.initialize(tokens)
|
| 132 |
|
| 133 |
# Start file cache cleanup task
|
|
@@ -177,7 +159,7 @@ async def lifespan(app: FastAPI):
|
|
| 177 |
# Initialize components
|
| 178 |
db = Database()
|
| 179 |
proxy_manager = ProxyManager(db)
|
| 180 |
-
flow_client = FlowClient(proxy_manager)
|
| 181 |
token_manager = TokenManager(db, flow_client)
|
| 182 |
concurrency_manager = ConcurrencyManager()
|
| 183 |
load_balancer = LoadBalancer(token_manager, concurrency_manager)
|
|
|
|
| 68 |
|
| 69 |
# Load captcha configuration from database
|
| 70 |
captcha_config = await db.get_captcha_config()
|
| 71 |
+
|
| 72 |
config.set_captcha_method(captcha_config.captcha_method)
|
| 73 |
config.set_yescaptcha_api_key(captcha_config.yescaptcha_api_key)
|
| 74 |
config.set_yescaptcha_base_url(captcha_config.yescaptcha_base_url)
|
| 75 |
+
config.set_capmonster_api_key(captcha_config.capmonster_api_key)
|
| 76 |
+
config.set_capmonster_base_url(captcha_config.capmonster_base_url)
|
| 77 |
+
config.set_ezcaptcha_api_key(captcha_config.ezcaptcha_api_key)
|
| 78 |
+
config.set_ezcaptcha_base_url(captcha_config.ezcaptcha_base_url)
|
| 79 |
+
config.set_capsolver_api_key(captcha_config.capsolver_api_key)
|
| 80 |
+
config.set_capsolver_base_url(captcha_config.capsolver_base_url)
|
| 81 |
|
| 82 |
# Initialize browser captcha service if needed
|
| 83 |
browser_service = None
|
| 84 |
if captcha_config.captcha_method == "personal":
|
| 85 |
+
from .services.browser_captcha_personal import BrowserCaptchaService
|
| 86 |
+
browser_service = await BrowserCaptchaService.get_instance(db)
|
| 87 |
+
print("✓ Browser captcha service initialized (nodriver mode)")
|
| 88 |
+
|
| 89 |
+
# 启动常驻模式:从第一个可用token获取project_id
|
| 90 |
+
tokens = await token_manager.get_all_tokens()
|
| 91 |
+
resident_project_id = None
|
| 92 |
+
for t in tokens:
|
| 93 |
+
if t.current_project_id and t.is_active:
|
| 94 |
+
resident_project_id = t.current_project_id
|
| 95 |
+
break
|
| 96 |
+
|
| 97 |
+
if resident_project_id:
|
| 98 |
+
# 直接启动常驻模式(会自动导航到项目页面,cookie已持久化)
|
| 99 |
+
await browser_service.start_resident_mode(resident_project_id)
|
| 100 |
+
print(f"✓ Browser captcha resident mode started (project: {resident_project_id[:8]}...)")
|
| 101 |
+
else:
|
| 102 |
+
# 没有可用的project_id时,打开登录窗口供用户手动操作
|
| 103 |
+
await browser_service.open_login_window()
|
| 104 |
+
print("⚠ No active token with project_id found, opened login window for manual setup")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
elif captcha_config.captcha_method == "browser":
|
| 106 |
+
from .services.browser_captcha import BrowserCaptchaService
|
| 107 |
+
browser_service = await BrowserCaptchaService.get_instance(db)
|
| 108 |
+
print("✓ Browser captcha service initialized (headless mode)")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
|
| 110 |
# Initialize concurrency manager
|
| 111 |
tokens = await token_manager.get_all_tokens()
|
| 112 |
+
|
| 113 |
await concurrency_manager.initialize(tokens)
|
| 114 |
|
| 115 |
# Start file cache cleanup task
|
|
|
|
| 159 |
# Initialize components
|
| 160 |
db = Database()
|
| 161 |
proxy_manager = ProxyManager(db)
|
| 162 |
+
flow_client = FlowClient(proxy_manager, db)
|
| 163 |
token_manager = TokenManager(db, flow_client)
|
| 164 |
concurrency_manager = ConcurrencyManager()
|
| 165 |
load_balancer = LoadBalancer(token_manager, concurrency_manager)
|
src/services/browser_captcha.py
CHANGED
|
@@ -6,16 +6,10 @@ import asyncio
|
|
| 6 |
import time
|
| 7 |
import re
|
| 8 |
from typing import Optional, Dict
|
|
|
|
| 9 |
|
| 10 |
from ..core.logger import debug_logger
|
| 11 |
|
| 12 |
-
# Conditionally import playwright
|
| 13 |
-
try:
|
| 14 |
-
from playwright.async_api import async_playwright, Browser, BrowserContext
|
| 15 |
-
PLAYWRIGHT_AVAILABLE = True
|
| 16 |
-
except ImportError:
|
| 17 |
-
PLAYWRIGHT_AVAILABLE = False
|
| 18 |
-
|
| 19 |
|
| 20 |
def parse_proxy_url(proxy_url: str) -> Optional[Dict[str, str]]:
|
| 21 |
"""解析代理URL,分离协议、主机、端口、认证信息
|
|
@@ -111,11 +105,6 @@ class BrowserCaptchaService:
|
|
| 111 |
return
|
| 112 |
|
| 113 |
try:
|
| 114 |
-
# 检查 Playwright 是否可用
|
| 115 |
-
if not PLAYWRIGHT_AVAILABLE:
|
| 116 |
-
debug_logger.log_error("[BrowserCaptcha] ❌ Playwright 不可用,请使用 YesCaptcha 服务")
|
| 117 |
-
raise ImportError("Playwright 未安装,请使用 YesCaptcha 服务")
|
| 118 |
-
|
| 119 |
# 获取浏览器专用代理配置
|
| 120 |
proxy_url = None
|
| 121 |
if self.db:
|
|
|
|
| 6 |
import time
|
| 7 |
import re
|
| 8 |
from typing import Optional, Dict
|
| 9 |
+
from playwright.async_api import async_playwright, Browser, BrowserContext
|
| 10 |
|
| 11 |
from ..core.logger import debug_logger
|
| 12 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
def parse_proxy_url(proxy_url: str) -> Optional[Dict[str, str]]:
|
| 15 |
"""解析代理URL,分离协议、主机、端口、认证信息
|
|
|
|
| 105 |
return
|
| 106 |
|
| 107 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
# 获取浏览器专用代理配置
|
| 109 |
proxy_url = None
|
| 110 |
if self.db:
|
src/services/browser_captcha_personal.py
CHANGED
|
@@ -1,208 +1,677 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import asyncio
|
| 2 |
import time
|
| 3 |
-
import re
|
| 4 |
import os
|
| 5 |
-
from typing import Optional
|
|
|
|
|
|
|
| 6 |
|
| 7 |
from ..core.logger import debug_logger
|
| 8 |
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
"""解析代理URL,分离协议、主机、端口、认证信息"""
|
| 19 |
-
proxy_pattern = r'^(socks5|http|https)://(?:([^:]+):([^@]+)@)?([^:]+):(\d+)$'
|
| 20 |
-
match = re.match(proxy_pattern, proxy_url)
|
| 21 |
-
if match:
|
| 22 |
-
protocol, username, password, host, port = match.groups()
|
| 23 |
-
proxy_config = {'server': f'{protocol}://{host}:{port}'}
|
| 24 |
-
if username and password:
|
| 25 |
-
proxy_config['username'] = username
|
| 26 |
-
proxy_config['password'] = password
|
| 27 |
-
return proxy_config
|
| 28 |
-
return None
|
| 29 |
|
| 30 |
class BrowserCaptchaService:
|
| 31 |
-
"""浏览器自动化获取 reCAPTCHA token(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
|
| 33 |
_instance: Optional['BrowserCaptchaService'] = None
|
| 34 |
_lock = asyncio.Lock()
|
| 35 |
|
| 36 |
def __init__(self, db=None):
|
| 37 |
"""初始化服务"""
|
| 38 |
-
|
| 39 |
-
self.
|
| 40 |
-
self.playwright = None
|
| 41 |
-
# 注意: 持久化模式下,我们操作的是 context 而不是 browser
|
| 42 |
-
self.context: Optional[BrowserContext] = None
|
| 43 |
self._initialized = False
|
| 44 |
self.website_key = "6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV"
|
| 45 |
self.db = db
|
| 46 |
-
|
| 47 |
-
# === 修改点 2: 指定本地数据存储目录 ===
|
| 48 |
-
# 这会在脚本运行目录下生成 browser_data 文件夹,用于保存你的登录状态
|
| 49 |
self.user_data_dir = os.path.join(os.getcwd(), "browser_data")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
|
| 51 |
@classmethod
|
| 52 |
async def get_instance(cls, db=None) -> 'BrowserCaptchaService':
|
|
|
|
| 53 |
if cls._instance is None:
|
| 54 |
async with cls._lock:
|
| 55 |
if cls._instance is None:
|
| 56 |
cls._instance = cls(db)
|
| 57 |
-
# 首次调用不强制初始化,等待 get_token 时懒加载,或者可以在这里await
|
| 58 |
return cls._instance
|
| 59 |
|
| 60 |
async def initialize(self):
|
| 61 |
-
"""初始化
|
| 62 |
-
if self._initialized and self.
|
| 63 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
|
| 65 |
try:
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
debug_logger.log_info(f"[BrowserCaptcha] 正在启动浏览器 (用户数据目录: {self.user_data_dir})...")
|
| 78 |
-
self.playwright = await async_playwright().start()
|
| 79 |
-
|
| 80 |
-
# 配置启动参数
|
| 81 |
-
launch_options = {
|
| 82 |
-
'headless': self.headless,
|
| 83 |
-
'user_data_dir': self.user_data_dir, # 指定数据目录
|
| 84 |
-
'viewport': {'width': 1280, 'height': 720}, # 设置默认窗口大小
|
| 85 |
-
'args': [
|
| 86 |
-
'--disable-blink-features=AutomationControlled',
|
| 87 |
-
'--disable-infobars',
|
| 88 |
'--no-sandbox',
|
|
|
|
| 89 |
'--disable-setuid-sandbox',
|
|
|
|
|
|
|
|
|
|
| 90 |
]
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
# 代理配置
|
| 94 |
-
if proxy_url:
|
| 95 |
-
proxy_config = parse_proxy_url(proxy_url)
|
| 96 |
-
if proxy_config:
|
| 97 |
-
launch_options['proxy'] = proxy_config
|
| 98 |
-
debug_logger.log_info(f"[BrowserCaptcha] 使用代理: {proxy_config['server']}")
|
| 99 |
-
|
| 100 |
-
# === 修改点 3: 使用 launch_persistent_context ===
|
| 101 |
-
# 这会启动一个带有状态的浏览器窗口
|
| 102 |
-
self.context = await self.playwright.chromium.launch_persistent_context(**launch_options)
|
| 103 |
-
|
| 104 |
-
# 设置默认超时
|
| 105 |
-
self.context.set_default_timeout(30000)
|
| 106 |
|
| 107 |
self._initialized = True
|
| 108 |
-
debug_logger.log_info(f"[BrowserCaptcha] ✅ 浏览器已启动 (Profile: {self.user_data_dir})")
|
| 109 |
-
|
| 110 |
except Exception as e:
|
| 111 |
debug_logger.log_error(f"[BrowserCaptcha] ❌ 浏览器启动失败: {str(e)}")
|
| 112 |
raise
|
| 113 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
async def get_token(self, project_id: str) -> Optional[str]:
|
| 115 |
-
"""获取 reCAPTCHA token
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
# 确保浏览器已启动
|
| 117 |
-
if not self._initialized or not self.
|
| 118 |
await self.initialize()
|
| 119 |
|
| 120 |
start_time = time.time()
|
| 121 |
-
|
| 122 |
|
| 123 |
try:
|
| 124 |
-
# === 修改点 4: 在现有上下文中新建标签页,而不是新建上下文 ===
|
| 125 |
-
# 这样可以复用该上下文中已保存的 Cookie (你的登录状态)
|
| 126 |
-
page = await self.context.new_page()
|
| 127 |
-
|
| 128 |
website_url = f"https://labs.google/fx/tools/flow/project/{project_id}"
|
| 129 |
-
debug_logger.log_info(f"[BrowserCaptcha] 访问页面: {website_url}")
|
| 130 |
|
| 131 |
-
# 访问页面
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
# --- 关键点:如果需要人工介入 ---
|
| 138 |
-
# 你可以在这里加入一段逻辑,如果是第一次运行,或者检测到未登录,
|
| 139 |
-
# 可以暂停脚本,等你手动操作完再继续。
|
| 140 |
-
# 例如: await asyncio.sleep(30)
|
| 141 |
-
|
| 142 |
-
# ... (中间注入脚本和执行 reCAPTCHA 的代码逻辑与原版完全一致,此处省略以节省篇幅) ...
|
| 143 |
-
# ... 请将原代码中从 "检查并注入 reCAPTCHA v3 脚本" 到 token 获取部分的代码复制到这里 ...
|
| 144 |
-
|
| 145 |
-
# 这里为了演示,简写注入逻辑(请保留你原有的完整注入逻辑):
|
| 146 |
-
script_loaded = await page.evaluate("() => { return !!(window.grecaptcha && window.grecaptcha.execute); }")
|
| 147 |
-
if not script_loaded:
|
| 148 |
-
await page.evaluate(f"""
|
| 149 |
-
() => {{
|
| 150 |
-
const script = document.createElement('script');
|
| 151 |
-
script.src = 'https://www.google.com/recaptcha/api.js?render={self.website_key}';
|
| 152 |
-
script.async = true; script.defer = true;
|
| 153 |
-
document.head.appendChild(script);
|
| 154 |
-
}}
|
| 155 |
-
""")
|
| 156 |
-
# 等待加载... (保留你原有的等待循环)
|
| 157 |
-
await page.wait_for_timeout(2000)
|
| 158 |
-
|
| 159 |
-
# 执行获取 Token (保留你原有的 execute 逻辑)
|
| 160 |
-
token = await page.evaluate(f"""
|
| 161 |
-
async () => {{
|
| 162 |
-
try {{
|
| 163 |
-
return await window.grecaptcha.execute('{self.website_key}', {{ action: 'FLOW_GENERATION' }});
|
| 164 |
-
}} catch (e) {{ return null; }}
|
| 165 |
-
}}
|
| 166 |
-
""")
|
| 167 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
if token:
|
| 169 |
-
debug_logger.log_info(f"[BrowserCaptcha] ✅ Token获取成功")
|
| 170 |
return token
|
| 171 |
else:
|
| 172 |
-
debug_logger.log_error("[BrowserCaptcha] Token获取失败")
|
| 173 |
return None
|
| 174 |
|
| 175 |
except Exception as e:
|
| 176 |
-
debug_logger.log_error(f"[BrowserCaptcha] 异常: {str(e)}")
|
| 177 |
return None
|
| 178 |
finally:
|
| 179 |
-
#
|
| 180 |
-
if
|
| 181 |
try:
|
| 182 |
-
await
|
| 183 |
-
except:
|
| 184 |
pass
|
| 185 |
|
| 186 |
async def close(self):
|
| 187 |
-
"""
|
|
|
|
|
|
|
|
|
|
| 188 |
try:
|
| 189 |
-
if self.
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
self._initialized = False
|
| 198 |
-
|
|
|
|
| 199 |
except Exception as e:
|
| 200 |
-
debug_logger.log_error(f"[BrowserCaptcha] 关闭异常: {str(e)}")
|
| 201 |
|
| 202 |
-
# 增加一个辅助方法,用于手动登录
|
| 203 |
async def open_login_window(self):
|
| 204 |
-
"""
|
| 205 |
await self.initialize()
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
print("请在打开的浏览器中登录账号。登录完成后,无需关闭浏览器,脚本下次运行时会自动使用此状态。")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
浏览器自动化获取 reCAPTCHA token
|
| 3 |
+
使用 nodriver (undetected-chromedriver 继任者) 实现反检测浏览器
|
| 4 |
+
支持常驻模式:为每个 project_id 自动创建常驻标签页,即时生成 token
|
| 5 |
+
"""
|
| 6 |
import asyncio
|
| 7 |
import time
|
|
|
|
| 8 |
import os
|
| 9 |
+
from typing import Optional
|
| 10 |
+
|
| 11 |
+
import nodriver as uc
|
| 12 |
|
| 13 |
from ..core.logger import debug_logger
|
| 14 |
|
| 15 |
+
|
| 16 |
+
class ResidentTabInfo:
|
| 17 |
+
"""常驻标签页信息结构"""
|
| 18 |
+
def __init__(self, tab, project_id: str):
|
| 19 |
+
self.tab = tab
|
| 20 |
+
self.project_id = project_id
|
| 21 |
+
self.recaptcha_ready = False
|
| 22 |
+
self.created_at = time.time()
|
| 23 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
class BrowserCaptchaService:
|
| 26 |
+
"""浏览器自动化获取 reCAPTCHA token(nodriver 有头模式)
|
| 27 |
+
|
| 28 |
+
支持两种模式:
|
| 29 |
+
1. 常驻模式 (Resident Mode): 为每个 project_id 保持常驻标签页,即时生成 token
|
| 30 |
+
2. 传统模式 (Legacy Mode): 每次请求创建新标签页 (fallback)
|
| 31 |
+
"""
|
| 32 |
|
| 33 |
_instance: Optional['BrowserCaptchaService'] = None
|
| 34 |
_lock = asyncio.Lock()
|
| 35 |
|
| 36 |
def __init__(self, db=None):
|
| 37 |
"""初始化服务"""
|
| 38 |
+
self.headless = False # nodriver 有头模式
|
| 39 |
+
self.browser = None
|
|
|
|
|
|
|
|
|
|
| 40 |
self._initialized = False
|
| 41 |
self.website_key = "6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV"
|
| 42 |
self.db = db
|
| 43 |
+
# 持久化 profile 目录
|
|
|
|
|
|
|
| 44 |
self.user_data_dir = os.path.join(os.getcwd(), "browser_data")
|
| 45 |
+
|
| 46 |
+
# 常驻模式相关属性 (支持多 project_id)
|
| 47 |
+
self._resident_tabs: dict[str, 'ResidentTabInfo'] = {} # project_id -> 常驻标签页信息
|
| 48 |
+
self._resident_lock = asyncio.Lock() # 保护常驻标签页操作
|
| 49 |
+
|
| 50 |
+
# 兼容旧 API(保留 single resident 属性作为别名)
|
| 51 |
+
self.resident_project_id: Optional[str] = None # 向后兼容
|
| 52 |
+
self.resident_tab = None # 向后兼容
|
| 53 |
+
self._running = False # 向后兼容
|
| 54 |
+
self._recaptcha_ready = False # 向后兼容
|
| 55 |
|
| 56 |
@classmethod
|
| 57 |
async def get_instance(cls, db=None) -> 'BrowserCaptchaService':
|
| 58 |
+
"""获取单例实例"""
|
| 59 |
if cls._instance is None:
|
| 60 |
async with cls._lock:
|
| 61 |
if cls._instance is None:
|
| 62 |
cls._instance = cls(db)
|
|
|
|
| 63 |
return cls._instance
|
| 64 |
|
| 65 |
async def initialize(self):
|
| 66 |
+
"""初始化 nodriver 浏览器"""
|
| 67 |
+
if self._initialized and self.browser:
|
| 68 |
+
# 检查浏览器是否仍然存活
|
| 69 |
+
try:
|
| 70 |
+
# 尝试获取浏览器信息验证存活
|
| 71 |
+
if self.browser.stopped:
|
| 72 |
+
debug_logger.log_warning("[BrowserCaptcha] 浏览器已停止,重新初始化...")
|
| 73 |
+
self._initialized = False
|
| 74 |
+
else:
|
| 75 |
+
return
|
| 76 |
+
except Exception:
|
| 77 |
+
debug_logger.log_warning("[BrowserCaptcha] 浏览器无响应,重新初始化...")
|
| 78 |
+
self._initialized = False
|
| 79 |
|
| 80 |
try:
|
| 81 |
+
debug_logger.log_info(f"[BrowserCaptcha] 正在启动 nodriver 浏览器 (用户数据目录: {self.user_data_dir})...")
|
| 82 |
+
|
| 83 |
+
# 确保 user_data_dir 存在
|
| 84 |
+
os.makedirs(self.user_data_dir, exist_ok=True)
|
| 85 |
+
|
| 86 |
+
# 启动 nodriver 浏览器
|
| 87 |
+
self.browser = await uc.start(
|
| 88 |
+
headless=self.headless,
|
| 89 |
+
user_data_dir=self.user_data_dir,
|
| 90 |
+
sandbox=False, # nodriver 需要此参数来禁用 sandbox
|
| 91 |
+
browser_args=[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
'--no-sandbox',
|
| 93 |
+
'--disable-dev-shm-usage',
|
| 94 |
'--disable-setuid-sandbox',
|
| 95 |
+
'--disable-gpu',
|
| 96 |
+
'--window-size=1280,720',
|
| 97 |
+
'--profile-directory=Default', # 跳过 Profile 选择器页面
|
| 98 |
]
|
| 99 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
|
| 101 |
self._initialized = True
|
| 102 |
+
debug_logger.log_info(f"[BrowserCaptcha] ✅ nodriver 浏览器已启动 (Profile: {self.user_data_dir})")
|
| 103 |
+
|
| 104 |
except Exception as e:
|
| 105 |
debug_logger.log_error(f"[BrowserCaptcha] ❌ 浏览器启动失败: {str(e)}")
|
| 106 |
raise
|
| 107 |
|
| 108 |
+
# ========== 常驻模式 API ==========
|
| 109 |
+
|
| 110 |
+
async def start_resident_mode(self, project_id: str):
|
| 111 |
+
"""启动常驻模式
|
| 112 |
+
|
| 113 |
+
Args:
|
| 114 |
+
project_id: 用于常驻的项目 ID
|
| 115 |
+
"""
|
| 116 |
+
if self._running:
|
| 117 |
+
debug_logger.log_warning("[BrowserCaptcha] 常驻模式已在运行")
|
| 118 |
+
return
|
| 119 |
+
|
| 120 |
+
await self.initialize()
|
| 121 |
+
|
| 122 |
+
self.resident_project_id = project_id
|
| 123 |
+
website_url = f"https://labs.google/fx/tools/flow/project/{project_id}"
|
| 124 |
+
|
| 125 |
+
debug_logger.log_info(f"[BrowserCaptcha] 启动常驻模式,访问页面: {website_url}")
|
| 126 |
+
|
| 127 |
+
# 创建一个独立的新标签页(不使用 main_tab,避免被回收)
|
| 128 |
+
self.resident_tab = await self.browser.get(website_url, new_tab=True)
|
| 129 |
+
|
| 130 |
+
debug_logger.log_info("[BrowserCaptcha] 标签页已创建,等待页面加载...")
|
| 131 |
+
|
| 132 |
+
# 等待页面加载完成(带重试机制)
|
| 133 |
+
page_loaded = False
|
| 134 |
+
for retry in range(60):
|
| 135 |
+
try:
|
| 136 |
+
await asyncio.sleep(1)
|
| 137 |
+
ready_state = await self.resident_tab.evaluate("document.readyState")
|
| 138 |
+
debug_logger.log_info(f"[BrowserCaptcha] 页面状态: {ready_state} (重试 {retry + 1}/60)")
|
| 139 |
+
if ready_state == "complete":
|
| 140 |
+
page_loaded = True
|
| 141 |
+
break
|
| 142 |
+
except ConnectionRefusedError as e:
|
| 143 |
+
debug_logger.log_warning(f"[BrowserCaptcha] 标签页连接丢失: {e},尝试重新获取...")
|
| 144 |
+
# 标签页可能已关闭,尝试重新创建
|
| 145 |
+
try:
|
| 146 |
+
self.resident_tab = await self.browser.get(website_url, new_tab=True)
|
| 147 |
+
debug_logger.log_info("[BrowserCaptcha] 已重新创建标签页")
|
| 148 |
+
except Exception as e2:
|
| 149 |
+
debug_logger.log_error(f"[BrowserCaptcha] 重新创建标签页失败: {e2}")
|
| 150 |
+
await asyncio.sleep(2)
|
| 151 |
+
except Exception as e:
|
| 152 |
+
debug_logger.log_warning(f"[BrowserCaptcha] 等待页面异常: {e},重试 {retry + 1}/15...")
|
| 153 |
+
await asyncio.sleep(2)
|
| 154 |
+
|
| 155 |
+
if not page_loaded:
|
| 156 |
+
debug_logger.log_error("[BrowserCaptcha] 页面加载超时,常驻模式启动失败")
|
| 157 |
+
return
|
| 158 |
+
|
| 159 |
+
# 等待 reCAPTCHA 加载
|
| 160 |
+
self._recaptcha_ready = await self._wait_for_recaptcha(self.resident_tab)
|
| 161 |
+
|
| 162 |
+
if not self._recaptcha_ready:
|
| 163 |
+
debug_logger.log_error("[BrowserCaptcha] reCAPTCHA 加载失败,常驻模式启动失败")
|
| 164 |
+
return
|
| 165 |
+
|
| 166 |
+
self._running = True
|
| 167 |
+
debug_logger.log_info(f"[BrowserCaptcha] ✅ 常驻模式已启动 (project: {project_id})")
|
| 168 |
+
|
| 169 |
+
async def stop_resident_mode(self, project_id: Optional[str] = None):
|
| 170 |
+
"""停止常驻模式
|
| 171 |
+
|
| 172 |
+
Args:
|
| 173 |
+
project_id: 指定要关闭的 project_id,如果为 None 则关闭所有常驻标签页
|
| 174 |
+
"""
|
| 175 |
+
async with self._resident_lock:
|
| 176 |
+
if project_id:
|
| 177 |
+
# 关闭指定的常驻标签页
|
| 178 |
+
await self._close_resident_tab(project_id)
|
| 179 |
+
debug_logger.log_info(f"[BrowserCaptcha] 已关闭 project_id={project_id} 的常驻模式")
|
| 180 |
+
else:
|
| 181 |
+
# 关闭所有常驻标签页
|
| 182 |
+
project_ids = list(self._resident_tabs.keys())
|
| 183 |
+
for pid in project_ids:
|
| 184 |
+
resident_info = self._resident_tabs.pop(pid, None)
|
| 185 |
+
if resident_info and resident_info.tab:
|
| 186 |
+
try:
|
| 187 |
+
await resident_info.tab.close()
|
| 188 |
+
except Exception:
|
| 189 |
+
pass
|
| 190 |
+
debug_logger.log_info(f"[BrowserCaptcha] 已关闭所有常驻标签页 (共 {len(project_ids)} 个)")
|
| 191 |
+
|
| 192 |
+
# 向后兼容:清理旧属性
|
| 193 |
+
if not self._running:
|
| 194 |
+
return
|
| 195 |
+
|
| 196 |
+
self._running = False
|
| 197 |
+
if self.resident_tab:
|
| 198 |
+
try:
|
| 199 |
+
await self.resident_tab.close()
|
| 200 |
+
except Exception:
|
| 201 |
+
pass
|
| 202 |
+
self.resident_tab = None
|
| 203 |
+
|
| 204 |
+
self.resident_project_id = None
|
| 205 |
+
self._recaptcha_ready = False
|
| 206 |
+
|
| 207 |
+
async def _wait_for_recaptcha(self, tab) -> bool:
|
| 208 |
+
"""等待 reCAPTCHA 加载
|
| 209 |
+
|
| 210 |
+
Returns:
|
| 211 |
+
True if reCAPTCHA loaded successfully
|
| 212 |
+
"""
|
| 213 |
+
debug_logger.log_info("[BrowserCaptcha] 检测 reCAPTCHA...")
|
| 214 |
+
|
| 215 |
+
# 检查 grecaptcha.enterprise.execute
|
| 216 |
+
is_enterprise = await tab.evaluate(
|
| 217 |
+
"typeof grecaptcha !== 'undefined' && typeof grecaptcha.enterprise !== 'undefined' && typeof grecaptcha.enterprise.execute === 'function'"
|
| 218 |
+
)
|
| 219 |
+
|
| 220 |
+
if is_enterprise:
|
| 221 |
+
debug_logger.log_info("[BrowserCaptcha] reCAPTCHA Enterprise 已加载")
|
| 222 |
+
return True
|
| 223 |
+
|
| 224 |
+
# 尝试注入脚本
|
| 225 |
+
debug_logger.log_info("[BrowserCaptcha] 未检测到 reCAPTCHA,注入脚本...")
|
| 226 |
+
|
| 227 |
+
await tab.evaluate(f"""
|
| 228 |
+
(() => {{
|
| 229 |
+
if (document.querySelector('script[src*="recaptcha"]')) return;
|
| 230 |
+
const script = document.createElement('script');
|
| 231 |
+
script.src = 'https://www.google.com/recaptcha/api.js?render={self.website_key}';
|
| 232 |
+
script.async = true;
|
| 233 |
+
document.head.appendChild(script);
|
| 234 |
+
}})()
|
| 235 |
+
""")
|
| 236 |
+
|
| 237 |
+
# 等待脚本加载
|
| 238 |
+
await tab.sleep(3)
|
| 239 |
+
|
| 240 |
+
# 轮询等待 reCAPTCHA 加载
|
| 241 |
+
for i in range(20):
|
| 242 |
+
is_enterprise = await tab.evaluate(
|
| 243 |
+
"typeof grecaptcha !== 'undefined' && typeof grecaptcha.enterprise !== 'undefined' && typeof grecaptcha.enterprise.execute === 'function'"
|
| 244 |
+
)
|
| 245 |
+
|
| 246 |
+
if is_enterprise:
|
| 247 |
+
debug_logger.log_info(f"[BrowserCaptcha] reCAPTCHA Enterprise 已加载(等待了 {i * 0.5} 秒)")
|
| 248 |
+
return True
|
| 249 |
+
await tab.sleep(0.5)
|
| 250 |
+
|
| 251 |
+
debug_logger.log_warning("[BrowserCaptcha] reCAPTCHA 加载超时")
|
| 252 |
+
return False
|
| 253 |
+
|
| 254 |
+
async def _execute_recaptcha_on_tab(self, tab) -> Optional[str]:
|
| 255 |
+
"""在指定标签页执行 reCAPTCHA 获取 token
|
| 256 |
+
|
| 257 |
+
Args:
|
| 258 |
+
tab: nodriver 标签页对象
|
| 259 |
+
|
| 260 |
+
Returns:
|
| 261 |
+
reCAPTCHA token 或 None
|
| 262 |
+
"""
|
| 263 |
+
# 生成唯一变量名避免冲突
|
| 264 |
+
ts = int(time.time() * 1000)
|
| 265 |
+
token_var = f"_recaptcha_token_{ts}"
|
| 266 |
+
error_var = f"_recaptcha_error_{ts}"
|
| 267 |
+
|
| 268 |
+
execute_script = f"""
|
| 269 |
+
(() => {{
|
| 270 |
+
window.{token_var} = null;
|
| 271 |
+
window.{error_var} = null;
|
| 272 |
+
|
| 273 |
+
try {{
|
| 274 |
+
grecaptcha.enterprise.ready(function() {{
|
| 275 |
+
grecaptcha.enterprise.execute('{self.website_key}', {{action: 'FLOW_GENERATION'}})
|
| 276 |
+
.then(function(token) {{
|
| 277 |
+
window.{token_var} = token;
|
| 278 |
+
}})
|
| 279 |
+
.catch(function(err) {{
|
| 280 |
+
window.{error_var} = err.message || 'execute failed';
|
| 281 |
+
}});
|
| 282 |
+
}});
|
| 283 |
+
}} catch (e) {{
|
| 284 |
+
window.{error_var} = e.message || 'exception';
|
| 285 |
+
}}
|
| 286 |
+
}})()
|
| 287 |
+
"""
|
| 288 |
+
|
| 289 |
+
# 注入执行脚本
|
| 290 |
+
await tab.evaluate(execute_script)
|
| 291 |
+
|
| 292 |
+
# 轮询等待结果(最多 15 秒)
|
| 293 |
+
token = None
|
| 294 |
+
for i in range(30):
|
| 295 |
+
await tab.sleep(0.5)
|
| 296 |
+
token = await tab.evaluate(f"window.{token_var}")
|
| 297 |
+
if token:
|
| 298 |
+
break
|
| 299 |
+
error = await tab.evaluate(f"window.{error_var}")
|
| 300 |
+
if error:
|
| 301 |
+
debug_logger.log_error(f"[BrowserCaptcha] reCAPTCHA 错误: {error}")
|
| 302 |
+
break
|
| 303 |
+
|
| 304 |
+
# 清理临时变量
|
| 305 |
+
try:
|
| 306 |
+
await tab.evaluate(f"delete window.{token_var}; delete window.{error_var};")
|
| 307 |
+
except:
|
| 308 |
+
pass
|
| 309 |
+
|
| 310 |
+
return token
|
| 311 |
+
|
| 312 |
+
# ========== 主要 API ==========
|
| 313 |
+
|
| 314 |
async def get_token(self, project_id: str) -> Optional[str]:
|
| 315 |
+
"""获取 reCAPTCHA token
|
| 316 |
+
|
| 317 |
+
自动常驻模式:如果该 project_id 没有常驻标签页,则自动创建并常驻
|
| 318 |
+
|
| 319 |
+
Args:
|
| 320 |
+
project_id: Flow项目ID
|
| 321 |
+
|
| 322 |
+
Returns:
|
| 323 |
+
reCAPTCHA token字符串,如果获取失败返回None
|
| 324 |
+
"""
|
| 325 |
+
# 确保浏览器已初始化
|
| 326 |
+
await self.initialize()
|
| 327 |
+
|
| 328 |
+
# 尝试从常驻标签页获取 token
|
| 329 |
+
async with self._resident_lock:
|
| 330 |
+
resident_info = self._resident_tabs.get(project_id)
|
| 331 |
+
|
| 332 |
+
# 如果该 project_id 没有常驻标签页,则自动创建
|
| 333 |
+
if resident_info is None:
|
| 334 |
+
debug_logger.log_info(f"[BrowserCaptcha] project_id={project_id} 没有常驻标签页,正在创建...")
|
| 335 |
+
resident_info = await self._create_resident_tab(project_id)
|
| 336 |
+
if resident_info is None:
|
| 337 |
+
debug_logger.log_warning(f"[BrowserCaptcha] 无法为 project_id={project_id} 创建常驻标签页,fallback 到传统模式")
|
| 338 |
+
return await self._get_token_legacy(project_id)
|
| 339 |
+
self._resident_tabs[project_id] = resident_info
|
| 340 |
+
debug_logger.log_info(f"[BrowserCaptcha] ✅ 已为 project_id={project_id} 创建常驻标签页 (当前共 {len(self._resident_tabs)} 个)")
|
| 341 |
+
|
| 342 |
+
# 使用常驻标签页生成 token
|
| 343 |
+
if resident_info and resident_info.recaptcha_ready and resident_info.tab:
|
| 344 |
+
start_time = time.time()
|
| 345 |
+
debug_logger.log_info(f"[BrowserCaptcha] 从常驻标签页即时生成 token (project: {project_id})...")
|
| 346 |
+
try:
|
| 347 |
+
token = await self._execute_recaptcha_on_tab(resident_info.tab)
|
| 348 |
+
duration_ms = (time.time() - start_time) * 1000
|
| 349 |
+
if token:
|
| 350 |
+
debug_logger.log_info(f"[BrowserCaptcha] ✅ Token生成成功(耗时 {duration_ms:.0f}ms)")
|
| 351 |
+
return token
|
| 352 |
+
else:
|
| 353 |
+
debug_logger.log_warning(f"[BrowserCaptcha] 常驻标签页生成失败 (project: {project_id}),尝试重建...")
|
| 354 |
+
except Exception as e:
|
| 355 |
+
debug_logger.log_warning(f"[BrowserCaptcha] 常驻标签页异常: {e},尝试重建...")
|
| 356 |
+
|
| 357 |
+
# 常驻标签页失效,尝试重建
|
| 358 |
+
async with self._resident_lock:
|
| 359 |
+
await self._close_resident_tab(project_id)
|
| 360 |
+
resident_info = await self._create_resident_tab(project_id)
|
| 361 |
+
if resident_info:
|
| 362 |
+
self._resident_tabs[project_id] = resident_info
|
| 363 |
+
# 重建后立即尝试生成
|
| 364 |
+
try:
|
| 365 |
+
token = await self._execute_recaptcha_on_tab(resident_info.tab)
|
| 366 |
+
if token:
|
| 367 |
+
debug_logger.log_info(f"[BrowserCaptcha] ✅ 重建后 Token生成成功")
|
| 368 |
+
return token
|
| 369 |
+
except Exception:
|
| 370 |
+
pass
|
| 371 |
+
|
| 372 |
+
# 最终 Fallback: 使用传统模式
|
| 373 |
+
debug_logger.log_warning(f"[BrowserCaptcha] 所有常驻方式失败,fallback 到传统模式 (project: {project_id})")
|
| 374 |
+
return await self._get_token_legacy(project_id)
|
| 375 |
+
|
| 376 |
+
async def _create_resident_tab(self, project_id: str) -> Optional[ResidentTabInfo]:
|
| 377 |
+
"""为指定 project_id 创建常驻标签页
|
| 378 |
+
|
| 379 |
+
Args:
|
| 380 |
+
project_id: 项目 ID
|
| 381 |
+
|
| 382 |
+
Returns:
|
| 383 |
+
ResidentTabInfo 对象,或 None(创建失败)
|
| 384 |
+
"""
|
| 385 |
+
try:
|
| 386 |
+
website_url = f"https://labs.google/fx/tools/flow/project/{project_id}"
|
| 387 |
+
debug_logger.log_info(f"[BrowserCaptcha] 为 project_id={project_id} 创建常驻标签页,访问: {website_url}")
|
| 388 |
+
|
| 389 |
+
# 创建新标签页
|
| 390 |
+
tab = await self.browser.get(website_url, new_tab=True)
|
| 391 |
+
|
| 392 |
+
# 等待页面加载完成
|
| 393 |
+
page_loaded = False
|
| 394 |
+
for retry in range(60):
|
| 395 |
+
try:
|
| 396 |
+
await asyncio.sleep(1)
|
| 397 |
+
ready_state = await tab.evaluate("document.readyState")
|
| 398 |
+
if ready_state == "complete":
|
| 399 |
+
page_loaded = True
|
| 400 |
+
break
|
| 401 |
+
except ConnectionRefusedError as e:
|
| 402 |
+
debug_logger.log_warning(f"[BrowserCaptcha] 标签页连接丢失: {e}")
|
| 403 |
+
return None
|
| 404 |
+
except Exception as e:
|
| 405 |
+
debug_logger.log_warning(f"[BrowserCaptcha] 等待页面异常: {e},重试 {retry + 1}/60...")
|
| 406 |
+
await asyncio.sleep(1)
|
| 407 |
+
|
| 408 |
+
if not page_loaded:
|
| 409 |
+
debug_logger.log_error(f"[BrowserCaptcha] 页面加载超时 (project: {project_id})")
|
| 410 |
+
try:
|
| 411 |
+
await tab.close()
|
| 412 |
+
except:
|
| 413 |
+
pass
|
| 414 |
+
return None
|
| 415 |
+
|
| 416 |
+
# 等待 reCAPTCHA 加载
|
| 417 |
+
recaptcha_ready = await self._wait_for_recaptcha(tab)
|
| 418 |
+
|
| 419 |
+
if not recaptcha_ready:
|
| 420 |
+
debug_logger.log_error(f"[BrowserCaptcha] reCAPTCHA 加载失败 (project: {project_id})")
|
| 421 |
+
try:
|
| 422 |
+
await tab.close()
|
| 423 |
+
except:
|
| 424 |
+
pass
|
| 425 |
+
return None
|
| 426 |
+
|
| 427 |
+
# 创建常驻信息对象
|
| 428 |
+
resident_info = ResidentTabInfo(tab, project_id)
|
| 429 |
+
resident_info.recaptcha_ready = True
|
| 430 |
+
|
| 431 |
+
debug_logger.log_info(f"[BrowserCaptcha] ✅ 常驻标签页创建成功 (project: {project_id})")
|
| 432 |
+
return resident_info
|
| 433 |
+
|
| 434 |
+
except Exception as e:
|
| 435 |
+
debug_logger.log_error(f"[BrowserCaptcha] 创建常驻标签页异常: {e}")
|
| 436 |
+
return None
|
| 437 |
+
|
| 438 |
+
async def _close_resident_tab(self, project_id: str):
|
| 439 |
+
"""关闭指定 project_id 的常驻标签页
|
| 440 |
+
|
| 441 |
+
Args:
|
| 442 |
+
project_id: 项目 ID
|
| 443 |
+
"""
|
| 444 |
+
resident_info = self._resident_tabs.pop(project_id, None)
|
| 445 |
+
if resident_info and resident_info.tab:
|
| 446 |
+
try:
|
| 447 |
+
await resident_info.tab.close()
|
| 448 |
+
debug_logger.log_info(f"[BrowserCaptcha] 已关闭 project_id={project_id} 的常驻标签页")
|
| 449 |
+
except Exception as e:
|
| 450 |
+
debug_logger.log_warning(f"[BrowserCaptcha] 关闭标签页时异常: {e}")
|
| 451 |
+
|
| 452 |
+
async def _get_token_legacy(self, project_id: str) -> Optional[str]:
|
| 453 |
+
"""传统模式获取 reCAPTCHA token(每次创建新标签页)
|
| 454 |
+
|
| 455 |
+
Args:
|
| 456 |
+
project_id: Flow项目ID
|
| 457 |
+
|
| 458 |
+
Returns:
|
| 459 |
+
reCAPTCHA token字符串,如果获取失败返回None
|
| 460 |
+
"""
|
| 461 |
# 确保浏览器已启动
|
| 462 |
+
if not self._initialized or not self.browser:
|
| 463 |
await self.initialize()
|
| 464 |
|
| 465 |
start_time = time.time()
|
| 466 |
+
tab = None
|
| 467 |
|
| 468 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 469 |
website_url = f"https://labs.google/fx/tools/flow/project/{project_id}"
|
| 470 |
+
debug_logger.log_info(f"[BrowserCaptcha] [Legacy] 访问页面: {website_url}")
|
| 471 |
|
| 472 |
+
# 新建标签页并访问页面
|
| 473 |
+
tab = await self.browser.get(website_url)
|
| 474 |
+
|
| 475 |
+
# 等待页面完全加载(增加等待时间)
|
| 476 |
+
debug_logger.log_info("[BrowserCaptcha] [Legacy] 等待页面加载...")
|
| 477 |
+
await tab.sleep(3)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 478 |
|
| 479 |
+
# 等待页面 DOM 完成
|
| 480 |
+
for _ in range(10):
|
| 481 |
+
ready_state = await tab.evaluate("document.readyState")
|
| 482 |
+
if ready_state == "complete":
|
| 483 |
+
break
|
| 484 |
+
await tab.sleep(0.5)
|
| 485 |
+
|
| 486 |
+
# 等待 reCAPTCHA 加载
|
| 487 |
+
recaptcha_ready = await self._wait_for_recaptcha(tab)
|
| 488 |
+
|
| 489 |
+
if not recaptcha_ready:
|
| 490 |
+
debug_logger.log_error("[BrowserCaptcha] [Legacy] reCAPTCHA 无法加载")
|
| 491 |
+
return None
|
| 492 |
+
|
| 493 |
+
# 执行 reCAPTCHA
|
| 494 |
+
debug_logger.log_info("[BrowserCaptcha] [Legacy] 执行 reCAPTCHA 验证...")
|
| 495 |
+
token = await self._execute_recaptcha_on_tab(tab)
|
| 496 |
+
|
| 497 |
+
duration_ms = (time.time() - start_time) * 1000
|
| 498 |
+
|
| 499 |
if token:
|
| 500 |
+
debug_logger.log_info(f"[BrowserCaptcha] [Legacy] ✅ Token获取成功(耗时 {duration_ms:.0f}ms)")
|
| 501 |
return token
|
| 502 |
else:
|
| 503 |
+
debug_logger.log_error("[BrowserCaptcha] [Legacy] Token获取失败(返回null)")
|
| 504 |
return None
|
| 505 |
|
| 506 |
except Exception as e:
|
| 507 |
+
debug_logger.log_error(f"[BrowserCaptcha] [Legacy] 获取token异常: {str(e)}")
|
| 508 |
return None
|
| 509 |
finally:
|
| 510 |
+
# 关闭标签页(但保留浏览器)
|
| 511 |
+
if tab:
|
| 512 |
try:
|
| 513 |
+
await tab.close()
|
| 514 |
+
except Exception:
|
| 515 |
pass
|
| 516 |
|
| 517 |
async def close(self):
|
| 518 |
+
"""关闭浏览器"""
|
| 519 |
+
# 先停止所有常驻模式(关闭所有常驻标签页)
|
| 520 |
+
await self.stop_resident_mode()
|
| 521 |
+
|
| 522 |
try:
|
| 523 |
+
if self.browser:
|
| 524 |
+
try:
|
| 525 |
+
self.browser.stop()
|
| 526 |
+
except Exception as e:
|
| 527 |
+
debug_logger.log_warning(f"[BrowserCaptcha] 关闭浏览器时出现异常: {str(e)}")
|
| 528 |
+
finally:
|
| 529 |
+
self.browser = None
|
| 530 |
+
|
| 531 |
self._initialized = False
|
| 532 |
+
self._resident_tabs.clear() # 确保清空常驻字典
|
| 533 |
+
debug_logger.log_info("[BrowserCaptcha] 浏览器已关闭")
|
| 534 |
except Exception as e:
|
| 535 |
+
debug_logger.log_error(f"[BrowserCaptcha] 关闭浏览器异常: {str(e)}")
|
| 536 |
|
|
|
|
| 537 |
async def open_login_window(self):
|
| 538 |
+
"""打开登录窗口供用户手动登录 Google"""
|
| 539 |
await self.initialize()
|
| 540 |
+
tab = await self.browser.get("https://accounts.google.com/")
|
| 541 |
+
debug_logger.log_info("[BrowserCaptcha] 请在打开的浏览器中登录账号。登录完成后,无需关闭浏览器,脚本下次运行时会自动使用此状态。")
|
| 542 |
+
print("请在打开的浏览器中登录账号。登录完成后,无需关闭浏览器,脚本下次运行时会自动使用此状态。")
|
| 543 |
+
|
| 544 |
+
# ========== Session Token 刷新 ==========
|
| 545 |
+
|
| 546 |
+
async def refresh_session_token(self, project_id: str) -> Optional[str]:
|
| 547 |
+
"""从常驻标签页获取最新的 Session Token
|
| 548 |
+
|
| 549 |
+
复用 reCAPTCHA 常驻标签页,通过刷新页面并从 cookies 中提取
|
| 550 |
+
__Secure-next-auth.session-token
|
| 551 |
+
|
| 552 |
+
Args:
|
| 553 |
+
project_id: 项目ID,用于定位常驻标签页
|
| 554 |
+
|
| 555 |
+
Returns:
|
| 556 |
+
新的 Session Token,如果获取失败返回 None
|
| 557 |
+
"""
|
| 558 |
+
# 确保浏览器已初始化
|
| 559 |
+
await self.initialize()
|
| 560 |
+
|
| 561 |
+
start_time = time.time()
|
| 562 |
+
debug_logger.log_info(f"[BrowserCaptcha] 开始刷新 Session Token (project: {project_id})...")
|
| 563 |
+
|
| 564 |
+
# 尝试获取或创建常驻标签页
|
| 565 |
+
async with self._resident_lock:
|
| 566 |
+
resident_info = self._resident_tabs.get(project_id)
|
| 567 |
+
|
| 568 |
+
# 如果该 project_id 没有常驻标签页,则创建
|
| 569 |
+
if resident_info is None:
|
| 570 |
+
debug_logger.log_info(f"[BrowserCaptcha] project_id={project_id} 没有常驻标签页,正在创建...")
|
| 571 |
+
resident_info = await self._create_resident_tab(project_id)
|
| 572 |
+
if resident_info is None:
|
| 573 |
+
debug_logger.log_warning(f"[BrowserCaptcha] 无法为 project_id={project_id} 创建常驻标签页")
|
| 574 |
+
return None
|
| 575 |
+
self._resident_tabs[project_id] = resident_info
|
| 576 |
+
|
| 577 |
+
if not resident_info or not resident_info.tab:
|
| 578 |
+
debug_logger.log_error(f"[BrowserCaptcha] 无法获取常驻标签页")
|
| 579 |
+
return None
|
| 580 |
+
|
| 581 |
+
tab = resident_info.tab
|
| 582 |
+
|
| 583 |
+
try:
|
| 584 |
+
# 刷新页面以获取最新的 cookies
|
| 585 |
+
debug_logger.log_info(f"[BrowserCaptcha] 刷新常驻标签页以获取最新 cookies...")
|
| 586 |
+
await tab.reload()
|
| 587 |
+
|
| 588 |
+
# 等待页面加载完成
|
| 589 |
+
for i in range(30):
|
| 590 |
+
await asyncio.sleep(1)
|
| 591 |
+
try:
|
| 592 |
+
ready_state = await tab.evaluate("document.readyState")
|
| 593 |
+
if ready_state == "complete":
|
| 594 |
+
break
|
| 595 |
+
except Exception:
|
| 596 |
+
pass
|
| 597 |
+
|
| 598 |
+
# 额外等待确保 cookies 已设置
|
| 599 |
+
await asyncio.sleep(2)
|
| 600 |
+
|
| 601 |
+
# 从 cookies 中提取 __Secure-next-auth.session-token
|
| 602 |
+
# nodriver 可以通过 browser 获取 cookies
|
| 603 |
+
session_token = None
|
| 604 |
+
|
| 605 |
+
try:
|
| 606 |
+
# 使用 nodriver 的 cookies API 获取所有 cookies
|
| 607 |
+
cookies = await self.browser.cookies.get_all()
|
| 608 |
+
|
| 609 |
+
for cookie in cookies:
|
| 610 |
+
if cookie.name == "__Secure-next-auth.session-token":
|
| 611 |
+
session_token = cookie.value
|
| 612 |
+
break
|
| 613 |
+
|
| 614 |
+
except Exception as e:
|
| 615 |
+
debug_logger.log_warning(f"[BrowserCaptcha] 通过 cookies API 获取失败: {e},尝试从 document.cookie 获取...")
|
| 616 |
+
|
| 617 |
+
# 备选方案:通过 JavaScript 获取 (注意:HttpOnly cookies 可能无法通过此方式获取)
|
| 618 |
+
try:
|
| 619 |
+
all_cookies = await tab.evaluate("document.cookie")
|
| 620 |
+
if all_cookies:
|
| 621 |
+
for part in all_cookies.split(";"):
|
| 622 |
+
part = part.strip()
|
| 623 |
+
if part.startswith("__Secure-next-auth.session-token="):
|
| 624 |
+
session_token = part.split("=", 1)[1]
|
| 625 |
+
break
|
| 626 |
+
except Exception as e2:
|
| 627 |
+
debug_logger.log_error(f"[BrowserCaptcha] document.cookie 获取失败: {e2}")
|
| 628 |
+
|
| 629 |
+
duration_ms = (time.time() - start_time) * 1000
|
| 630 |
+
|
| 631 |
+
if session_token:
|
| 632 |
+
debug_logger.log_info(f"[BrowserCaptcha] ✅ Session Token 获取成功(耗时 {duration_ms:.0f}ms)")
|
| 633 |
+
return session_token
|
| 634 |
+
else:
|
| 635 |
+
debug_logger.log_error(f"[BrowserCaptcha] ❌ 未找到 __Secure-next-auth.session-token cookie")
|
| 636 |
+
return None
|
| 637 |
+
|
| 638 |
+
except Exception as e:
|
| 639 |
+
debug_logger.log_error(f"[BrowserCaptcha] 刷新 Session Token 异常: {str(e)}")
|
| 640 |
+
|
| 641 |
+
# 常驻标签页可能已失效,尝试重建
|
| 642 |
+
async with self._resident_lock:
|
| 643 |
+
await self._close_resident_tab(project_id)
|
| 644 |
+
resident_info = await self._create_resident_tab(project_id)
|
| 645 |
+
if resident_info:
|
| 646 |
+
self._resident_tabs[project_id] = resident_info
|
| 647 |
+
# 重建后再次尝试获取
|
| 648 |
+
try:
|
| 649 |
+
cookies = await self.browser.cookies.get_all()
|
| 650 |
+
for cookie in cookies:
|
| 651 |
+
if cookie.name == "__Secure-next-auth.session-token":
|
| 652 |
+
debug_logger.log_info(f"[BrowserCaptcha] ✅ 重建后 Session Token 获取成功")
|
| 653 |
+
return cookie.value
|
| 654 |
+
except Exception:
|
| 655 |
+
pass
|
| 656 |
+
|
| 657 |
+
return None
|
| 658 |
+
|
| 659 |
+
# ========== 状态查询 ==========
|
| 660 |
+
|
| 661 |
+
def is_resident_mode_active(self) -> bool:
|
| 662 |
+
"""检查是否有任何常驻标签页激活"""
|
| 663 |
+
return len(self._resident_tabs) > 0 or self._running
|
| 664 |
+
|
| 665 |
+
def get_resident_count(self) -> int:
|
| 666 |
+
"""获取当前常驻标签页数量"""
|
| 667 |
+
return len(self._resident_tabs)
|
| 668 |
+
|
| 669 |
+
def get_resident_project_ids(self) -> list[str]:
|
| 670 |
+
"""获取所有当前常驻的 project_id 列表"""
|
| 671 |
+
return list(self._resident_tabs.keys())
|
| 672 |
+
|
| 673 |
+
def get_resident_project_id(self) -> Optional[str]:
|
| 674 |
+
"""获取当前常驻的 project_id(向后兼容,返回第一个)"""
|
| 675 |
+
if self._resident_tabs:
|
| 676 |
+
return next(iter(self._resident_tabs.keys()))
|
| 677 |
+
return self.resident_project_id
|
src/services/flow_client.py
CHANGED
|
@@ -12,11 +12,86 @@ from ..core.config import config
|
|
| 12 |
class FlowClient:
|
| 13 |
"""VideoFX API客户端"""
|
| 14 |
|
| 15 |
-
def __init__(self, proxy_manager):
|
| 16 |
self.proxy_manager = proxy_manager
|
|
|
|
| 17 |
self.labs_base_url = config.flow_labs_base_url # https://labs.google/fx/api
|
| 18 |
self.api_base_url = config.flow_api_base_url # https://aisandbox-pa.googleapis.com/v1
|
| 19 |
self.timeout = config.flow_timeout
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
async def _make_request(
|
| 22 |
self,
|
|
@@ -52,12 +127,19 @@ class FlowClient:
|
|
| 52 |
|
| 53 |
# AT认证 - 使用Bearer
|
| 54 |
if use_at and at_token:
|
| 55 |
-
headers["
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
|
| 57 |
-
# 通用请求头
|
| 58 |
headers.update({
|
| 59 |
"Content-Type": "application/json",
|
| 60 |
-
"User-Agent":
|
| 61 |
})
|
| 62 |
|
| 63 |
# Log request
|
|
@@ -571,7 +653,7 @@ class FlowClient:
|
|
| 571 |
Returns:
|
| 572 |
同 generate_video_text
|
| 573 |
"""
|
| 574 |
-
url = f"{self.api_base_url}/video:
|
| 575 |
|
| 576 |
# 获取 reCAPTCHA token
|
| 577 |
recaptcha_token = await self._get_recaptcha_token(project_id) or ""
|
|
@@ -684,14 +766,14 @@ class FlowClient:
|
|
| 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 == "personal":
|
| 692 |
try:
|
| 693 |
from .browser_captcha_personal import BrowserCaptchaService
|
| 694 |
-
service = await BrowserCaptchaService.get_instance(self.
|
| 695 |
return await service.get_token(project_id)
|
| 696 |
except Exception as e:
|
| 697 |
debug_logger.log_error(f"[reCAPTCHA Browser] error: {str(e)}")
|
|
@@ -700,67 +782,98 @@ class FlowClient:
|
|
| 700 |
elif captcha_method == "browser":
|
| 701 |
try:
|
| 702 |
from .browser_captcha import BrowserCaptchaService
|
| 703 |
-
service = await BrowserCaptchaService.get_instance(self.
|
| 704 |
return await service.get_token(project_id)
|
| 705 |
except Exception as e:
|
| 706 |
debug_logger.log_error(f"[reCAPTCHA Browser] error: {str(e)}")
|
| 707 |
return None
|
|
|
|
|
|
|
|
|
|
| 708 |
else:
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
if not client_key:
|
| 712 |
-
debug_logger.log_info("[reCAPTCHA] API key not configured, skipping")
|
| 713 |
-
return None
|
| 714 |
|
| 715 |
-
|
| 716 |
-
|
|
|
|
|
|
|
|
|
|
| 717 |
base_url = config.yescaptcha_base_url
|
| 718 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 719 |
|
| 720 |
-
|
| 721 |
-
|
| 722 |
-
|
| 723 |
-
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 732 |
}
|
|
|
|
| 733 |
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
|
| 737 |
|
| 738 |
-
|
| 739 |
|
| 740 |
-
|
| 741 |
-
|
|
|
|
|
|
|
| 742 |
|
| 743 |
-
|
| 744 |
-
|
| 745 |
-
|
| 746 |
-
|
| 747 |
-
|
| 748 |
-
|
| 749 |
-
|
| 750 |
-
|
| 751 |
|
| 752 |
-
|
| 753 |
|
|
|
|
|
|
|
| 754 |
solution = result_json.get('solution', {})
|
| 755 |
response = solution.get('gRecaptchaResponse')
|
| 756 |
-
|
| 757 |
if response:
|
|
|
|
| 758 |
return response
|
| 759 |
|
| 760 |
-
|
| 761 |
|
| 762 |
-
|
| 763 |
-
|
| 764 |
-
except Exception as e:
|
| 765 |
-
debug_logger.log_error(f"[reCAPTCHA] error: {str(e)}")
|
| 766 |
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
class FlowClient:
|
| 13 |
"""VideoFX API客户端"""
|
| 14 |
|
| 15 |
+
def __init__(self, proxy_manager, db=None):
|
| 16 |
self.proxy_manager = proxy_manager
|
| 17 |
+
self.db = db # Database instance for captcha config
|
| 18 |
self.labs_base_url = config.flow_labs_base_url # https://labs.google/fx/api
|
| 19 |
self.api_base_url = config.flow_api_base_url # https://aisandbox-pa.googleapis.com/v1
|
| 20 |
self.timeout = config.flow_timeout
|
| 21 |
+
# 缓存每个账号的 User-Agent
|
| 22 |
+
self._user_agent_cache = {}
|
| 23 |
+
|
| 24 |
+
def _generate_user_agent(self, account_id: str = None) -> str:
|
| 25 |
+
"""基于账号ID生成固定的 User-Agent
|
| 26 |
+
|
| 27 |
+
Args:
|
| 28 |
+
account_id: 账号标识(如 email 或 token_id),相同账号返回相同 UA
|
| 29 |
+
|
| 30 |
+
Returns:
|
| 31 |
+
User-Agent 字符串
|
| 32 |
+
"""
|
| 33 |
+
# 如果没有提供账号ID,生成随机UA
|
| 34 |
+
if not account_id:
|
| 35 |
+
account_id = f"random_{random.randint(1, 999999)}"
|
| 36 |
+
|
| 37 |
+
# 如果已缓存,直接返回
|
| 38 |
+
if account_id in self._user_agent_cache:
|
| 39 |
+
return self._user_agent_cache[account_id]
|
| 40 |
+
|
| 41 |
+
# 使用账号ID作为随机种子,确保同一账号生成相同的UA
|
| 42 |
+
import hashlib
|
| 43 |
+
seed = int(hashlib.md5(account_id.encode()).hexdigest()[:8], 16)
|
| 44 |
+
rng = random.Random(seed)
|
| 45 |
+
|
| 46 |
+
# Chrome 版本池
|
| 47 |
+
chrome_versions = ["130.0.0.0", "131.0.0.0", "132.0.0.0", "129.0.0.0"]
|
| 48 |
+
# Firefox 版本池
|
| 49 |
+
firefox_versions = ["133.0", "132.0", "131.0", "134.0"]
|
| 50 |
+
# Safari 版本池
|
| 51 |
+
safari_versions = ["18.2", "18.1", "18.0", "17.6"]
|
| 52 |
+
# Edge 版本池
|
| 53 |
+
edge_versions = ["130.0.0.0", "131.0.0.0", "132.0.0.0"]
|
| 54 |
+
|
| 55 |
+
# 操作系统配置
|
| 56 |
+
os_configs = [
|
| 57 |
+
# Windows
|
| 58 |
+
{
|
| 59 |
+
"platform": "Windows NT 10.0; Win64; x64",
|
| 60 |
+
"browsers": [
|
| 61 |
+
lambda r: f"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{r.choice(chrome_versions)} Safari/537.36",
|
| 62 |
+
lambda r: f"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:{r.choice(firefox_versions).split('.')[0]}.0) Gecko/20100101 Firefox/{r.choice(firefox_versions)}",
|
| 63 |
+
lambda r: f"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{r.choice(chrome_versions)} Safari/537.36 Edg/{r.choice(edge_versions)}",
|
| 64 |
+
]
|
| 65 |
+
},
|
| 66 |
+
# macOS
|
| 67 |
+
{
|
| 68 |
+
"platform": "Macintosh; Intel Mac OS X 10_15_7",
|
| 69 |
+
"browsers": [
|
| 70 |
+
lambda r: f"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{r.choice(chrome_versions)} Safari/537.36",
|
| 71 |
+
lambda r: f"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/{r.choice(safari_versions)} Safari/605.1.15",
|
| 72 |
+
lambda r: f"Mozilla/5.0 (Macintosh; Intel Mac OS X 14.{r.randint(0, 7)}; rv:{r.choice(firefox_versions).split('.')[0]}.0) Gecko/20100101 Firefox/{r.choice(firefox_versions)}",
|
| 73 |
+
]
|
| 74 |
+
},
|
| 75 |
+
# Linux
|
| 76 |
+
{
|
| 77 |
+
"platform": "X11; Linux x86_64",
|
| 78 |
+
"browsers": [
|
| 79 |
+
lambda r: f"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{r.choice(chrome_versions)} Safari/537.36",
|
| 80 |
+
lambda r: f"Mozilla/5.0 (X11; Linux x86_64; rv:{r.choice(firefox_versions).split('.')[0]}.0) Gecko/20100101 Firefox/{r.choice(firefox_versions)}",
|
| 81 |
+
lambda r: f"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:{r.choice(firefox_versions).split('.')[0]}.0) Gecko/20100101 Firefox/{r.choice(firefox_versions)}",
|
| 82 |
+
]
|
| 83 |
+
}
|
| 84 |
+
]
|
| 85 |
+
|
| 86 |
+
# 使用固定种子随机选择操作系统和浏览器
|
| 87 |
+
os_config = rng.choice(os_configs)
|
| 88 |
+
browser_generator = rng.choice(os_config["browsers"])
|
| 89 |
+
user_agent = browser_generator(rng)
|
| 90 |
+
|
| 91 |
+
# 缓存结果
|
| 92 |
+
self._user_agent_cache[account_id] = user_agent
|
| 93 |
+
|
| 94 |
+
return user_agent
|
| 95 |
|
| 96 |
async def _make_request(
|
| 97 |
self,
|
|
|
|
| 127 |
|
| 128 |
# AT认证 - 使用Bearer
|
| 129 |
if use_at and at_token:
|
| 130 |
+
headers["authorization"] = f"Bearer {at_token}"
|
| 131 |
+
|
| 132 |
+
# 确定账号标识(优先使用 token 的前16个字符作为标识)
|
| 133 |
+
account_id = None
|
| 134 |
+
if st_token:
|
| 135 |
+
account_id = st_token[:16] # 使用 ST 的前16个字符
|
| 136 |
+
elif at_token:
|
| 137 |
+
account_id = at_token[:16] # 使用 AT 的前16个字符
|
| 138 |
|
| 139 |
+
# 通用请求头 - 基于账号生成固定的 User-Agent
|
| 140 |
headers.update({
|
| 141 |
"Content-Type": "application/json",
|
| 142 |
+
"User-Agent": self._generate_user_agent(account_id)
|
| 143 |
})
|
| 144 |
|
| 145 |
# Log request
|
|
|
|
| 653 |
Returns:
|
| 654 |
同 generate_video_text
|
| 655 |
"""
|
| 656 |
+
url = f"{self.api_base_url}/video:batchAsyncGenerateVideoStartImage"
|
| 657 |
|
| 658 |
# 获取 reCAPTCHA token
|
| 659 |
recaptcha_token = await self._get_recaptcha_token(project_id) or ""
|
|
|
|
| 766 |
return str(uuid.uuid4())
|
| 767 |
|
| 768 |
async def _get_recaptcha_token(self, project_id: str) -> Optional[str]:
|
| 769 |
+
"""获取reCAPTCHA token - 支持多种打码方式"""
|
| 770 |
captcha_method = config.captcha_method
|
| 771 |
|
| 772 |
# 恒定浏览器打码
|
| 773 |
if captcha_method == "personal":
|
| 774 |
try:
|
| 775 |
from .browser_captcha_personal import BrowserCaptchaService
|
| 776 |
+
service = await BrowserCaptchaService.get_instance(self.db)
|
| 777 |
return await service.get_token(project_id)
|
| 778 |
except Exception as e:
|
| 779 |
debug_logger.log_error(f"[reCAPTCHA Browser] error: {str(e)}")
|
|
|
|
| 782 |
elif captcha_method == "browser":
|
| 783 |
try:
|
| 784 |
from .browser_captcha import BrowserCaptchaService
|
| 785 |
+
service = await BrowserCaptchaService.get_instance(self.db)
|
| 786 |
return await service.get_token(project_id)
|
| 787 |
except Exception as e:
|
| 788 |
debug_logger.log_error(f"[reCAPTCHA Browser] error: {str(e)}")
|
| 789 |
return None
|
| 790 |
+
# API打码服务
|
| 791 |
+
elif captcha_method in ["yescaptcha", "capmonster", "ezcaptcha", "capsolver"]:
|
| 792 |
+
return await self._get_api_captcha_token(captcha_method, project_id)
|
| 793 |
else:
|
| 794 |
+
debug_logger.log_error(f"[reCAPTCHA] Unknown captcha method: {captcha_method}")
|
| 795 |
+
return None
|
|
|
|
|
|
|
|
|
|
| 796 |
|
| 797 |
+
async def _get_api_captcha_token(self, method: str, project_id: str) -> Optional[str]:
|
| 798 |
+
"""通用API打码服务"""
|
| 799 |
+
# 获取配置
|
| 800 |
+
if method == "yescaptcha":
|
| 801 |
+
client_key = config.yescaptcha_api_key
|
| 802 |
base_url = config.yescaptcha_base_url
|
| 803 |
+
task_type = "RecaptchaV3TaskProxylessM1"
|
| 804 |
+
elif method == "capmonster":
|
| 805 |
+
client_key = config.capmonster_api_key
|
| 806 |
+
base_url = config.capmonster_base_url
|
| 807 |
+
task_type = "RecaptchaV3EnterpriseTask"
|
| 808 |
+
elif method == "ezcaptcha":
|
| 809 |
+
client_key = config.ezcaptcha_api_key
|
| 810 |
+
base_url = config.ezcaptcha_base_url
|
| 811 |
+
task_type = "ReCaptchaV3EnterpriseTaskProxyless"
|
| 812 |
+
elif method == "capsolver":
|
| 813 |
+
client_key = config.capsolver_api_key
|
| 814 |
+
base_url = config.capsolver_base_url
|
| 815 |
+
task_type = "ReCaptchaV3EnterpriseTaskProxyLess"
|
| 816 |
+
else:
|
| 817 |
+
debug_logger.log_error(f"[reCAPTCHA] Unknown API method: {method}")
|
| 818 |
+
return None
|
| 819 |
|
| 820 |
+
if not client_key:
|
| 821 |
+
debug_logger.log_info(f"[reCAPTCHA] {method} API key not configured, skipping")
|
| 822 |
+
return None
|
| 823 |
+
|
| 824 |
+
website_key = "6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV"
|
| 825 |
+
website_url = f"https://labs.google/fx/tools/flow/project/{project_id}"
|
| 826 |
+
page_action = "FLOW_GENERATION"
|
| 827 |
+
|
| 828 |
+
try:
|
| 829 |
+
async with AsyncSession() as session:
|
| 830 |
+
create_url = f"{base_url}/createTask"
|
| 831 |
+
create_data = {
|
| 832 |
+
"clientKey": client_key,
|
| 833 |
+
"softID": "42780",
|
| 834 |
+
"task": {
|
| 835 |
+
"websiteURL": website_url,
|
| 836 |
+
"websiteKey": website_key,
|
| 837 |
+
"type": task_type,
|
| 838 |
+
"pageAction": page_action
|
| 839 |
}
|
| 840 |
+
}
|
| 841 |
|
| 842 |
+
result = await session.post(create_url, json=create_data, impersonate="chrome110")
|
| 843 |
+
result_json = result.json()
|
| 844 |
+
task_id = result_json.get('taskId')
|
| 845 |
|
| 846 |
+
debug_logger.log_info(f"[reCAPTCHA {method}] created task_id: {task_id}")
|
| 847 |
|
| 848 |
+
if not task_id:
|
| 849 |
+
error_desc = result_json.get('errorDescription', 'Unknown error')
|
| 850 |
+
debug_logger.log_error(f"[reCAPTCHA {method}] Failed to create task: {error_desc}")
|
| 851 |
+
return None
|
| 852 |
|
| 853 |
+
get_url = f"{base_url}/getTaskResult"
|
| 854 |
+
for i in range(40):
|
| 855 |
+
get_data = {
|
| 856 |
+
"clientKey": client_key,
|
| 857 |
+
"taskId": task_id
|
| 858 |
+
}
|
| 859 |
+
result = await session.post(get_url, json=get_data, impersonate="chrome110")
|
| 860 |
+
result_json = result.json()
|
| 861 |
|
| 862 |
+
debug_logger.log_info(f"[reCAPTCHA {method}] polling #{i+1}: {result_json}")
|
| 863 |
|
| 864 |
+
status = result_json.get('status')
|
| 865 |
+
if status == 'ready':
|
| 866 |
solution = result_json.get('solution', {})
|
| 867 |
response = solution.get('gRecaptchaResponse')
|
|
|
|
| 868 |
if response:
|
| 869 |
+
debug_logger.log_info(f"[reCAPTCHA {method}] Token获取成功")
|
| 870 |
return response
|
| 871 |
|
| 872 |
+
time.sleep(3)
|
| 873 |
|
| 874 |
+
debug_logger.log_error(f"[reCAPTCHA {method}] Timeout waiting for token")
|
|
|
|
|
|
|
|
|
|
| 875 |
return None
|
| 876 |
+
|
| 877 |
+
except Exception as e:
|
| 878 |
+
debug_logger.log_error(f"[reCAPTCHA {method}] error: {str(e)}")
|
| 879 |
+
return None
|
src/services/generation_handler.py
CHANGED
|
@@ -136,7 +136,7 @@ MODEL_CONFIG = {
|
|
| 136 |
"veo_3_1_i2v_s_fast_fl_portrait": {
|
| 137 |
"type": "video",
|
| 138 |
"video_type": "i2v",
|
| 139 |
-
"model_key": "
|
| 140 |
"aspect_ratio": "VIDEO_ASPECT_RATIO_PORTRAIT",
|
| 141 |
"supports_images": True,
|
| 142 |
"min_images": 1,
|
|
@@ -145,7 +145,7 @@ MODEL_CONFIG = {
|
|
| 145 |
"veo_3_1_i2v_s_fast_fl_landscape": {
|
| 146 |
"type": "video",
|
| 147 |
"video_type": "i2v",
|
| 148 |
-
"model_key": "
|
| 149 |
"aspect_ratio": "VIDEO_ASPECT_RATIO_LANDSCAPE",
|
| 150 |
"supports_images": True,
|
| 151 |
"min_images": 1,
|
|
@@ -259,7 +259,7 @@ MODEL_CONFIG = {
|
|
| 259 |
"veo_3_0_r2v_fast_portrait": {
|
| 260 |
"type": "video",
|
| 261 |
"video_type": "r2v",
|
| 262 |
-
"model_key": "
|
| 263 |
"aspect_ratio": "VIDEO_ASPECT_RATIO_PORTRAIT",
|
| 264 |
"supports_images": True,
|
| 265 |
"min_images": 0,
|
|
@@ -268,7 +268,7 @@ MODEL_CONFIG = {
|
|
| 268 |
"veo_3_0_r2v_fast_landscape": {
|
| 269 |
"type": "video",
|
| 270 |
"video_type": "r2v",
|
| 271 |
-
"model_key": "
|
| 272 |
"aspect_ratio": "VIDEO_ASPECT_RATIO_LANDSCAPE",
|
| 273 |
"supports_images": True,
|
| 274 |
"min_images": 0,
|
|
@@ -644,6 +644,44 @@ class GenerationHandler:
|
|
| 644 |
min_images = model_config.get("min_images", 0)
|
| 645 |
max_images = model_config.get("max_images", 0)
|
| 646 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 647 |
# 图片数量
|
| 648 |
image_count = len(images) if images else 0
|
| 649 |
|
|
@@ -734,12 +772,16 @@ class GenerationHandler:
|
|
| 734 |
user_paygate_tier=token.user_paygate_tier or "PAYGATE_TIER_ONE"
|
| 735 |
)
|
| 736 |
else:
|
| 737 |
-
# 只有首帧
|
|
|
|
|
|
|
|
|
|
|
|
|
| 738 |
result = await self.flow_client.generate_video_start_image(
|
| 739 |
at=token.at,
|
| 740 |
project_id=project_id,
|
| 741 |
prompt=prompt,
|
| 742 |
-
model_key=
|
| 743 |
aspect_ratio=model_config["aspect_ratio"],
|
| 744 |
start_media_id=start_media_id,
|
| 745 |
user_paygate_tier=token.user_paygate_tier or "PAYGATE_TIER_ONE"
|
|
@@ -888,8 +930,30 @@ class GenerationHandler:
|
|
| 888 |
)
|
| 889 |
return
|
| 890 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 891 |
elif status.startswith("MEDIA_GENERATION_STATUS_ERROR"):
|
| 892 |
-
#
|
| 893 |
yield self._create_error_response(f"视频生成失败: {status}")
|
| 894 |
return
|
| 895 |
|
|
|
|
| 136 |
"veo_3_1_i2v_s_fast_fl_portrait": {
|
| 137 |
"type": "video",
|
| 138 |
"video_type": "i2v",
|
| 139 |
+
"model_key": "veo_3_1_i2v_s_fast_portrait_fl_ultra_relaxed",
|
| 140 |
"aspect_ratio": "VIDEO_ASPECT_RATIO_PORTRAIT",
|
| 141 |
"supports_images": True,
|
| 142 |
"min_images": 1,
|
|
|
|
| 145 |
"veo_3_1_i2v_s_fast_fl_landscape": {
|
| 146 |
"type": "video",
|
| 147 |
"video_type": "i2v",
|
| 148 |
+
"model_key": "veo_3_1_i2v_s_fast_fl_ultra_relaxed",
|
| 149 |
"aspect_ratio": "VIDEO_ASPECT_RATIO_LANDSCAPE",
|
| 150 |
"supports_images": True,
|
| 151 |
"min_images": 1,
|
|
|
|
| 259 |
"veo_3_0_r2v_fast_portrait": {
|
| 260 |
"type": "video",
|
| 261 |
"video_type": "r2v",
|
| 262 |
+
"model_key": "veo_3_0_r2v_fast_portrait_ultra_relaxed",
|
| 263 |
"aspect_ratio": "VIDEO_ASPECT_RATIO_PORTRAIT",
|
| 264 |
"supports_images": True,
|
| 265 |
"min_images": 0,
|
|
|
|
| 268 |
"veo_3_0_r2v_fast_landscape": {
|
| 269 |
"type": "video",
|
| 270 |
"video_type": "r2v",
|
| 271 |
+
"model_key": "veo_3_0_r2v_fast_ultra_relaxed",
|
| 272 |
"aspect_ratio": "VIDEO_ASPECT_RATIO_LANDSCAPE",
|
| 273 |
"supports_images": True,
|
| 274 |
"min_images": 0,
|
|
|
|
| 644 |
min_images = model_config.get("min_images", 0)
|
| 645 |
max_images = model_config.get("max_images", 0)
|
| 646 |
|
| 647 |
+
# 根据账号tier自动调整模型 key
|
| 648 |
+
model_key = model_config["model_key"]
|
| 649 |
+
user_tier = token.user_paygate_tier or "PAYGATE_TIER_ONE"
|
| 650 |
+
|
| 651 |
+
# TIER_TWO 账号需要使用 ultra 版本的模型
|
| 652 |
+
if user_tier == "PAYGATE_TIER_TWO":
|
| 653 |
+
# 如果模型 key 不包含 ultra,自动添加
|
| 654 |
+
if "ultra" not in model_key:
|
| 655 |
+
# veo_3_1_i2v_s_fast_fl -> veo_3_1_i2v_s_fast_ultra_fl
|
| 656 |
+
# veo_3_1_t2v_fast -> veo_3_1_t2v_fast_ultra
|
| 657 |
+
# veo_3_0_r2v_fast -> veo_3_0_r2v_fast_ultra
|
| 658 |
+
if "_fl" in model_key:
|
| 659 |
+
model_key = model_key.replace("_fl", "_ultra_fl")
|
| 660 |
+
elif model_key.endswith("_fast"):
|
| 661 |
+
model_key = model_key + "_ultra"
|
| 662 |
+
elif "_fast_" in model_key:
|
| 663 |
+
model_key = model_key.replace("_fast_", "_fast_ultra_")
|
| 664 |
+
|
| 665 |
+
if stream:
|
| 666 |
+
yield self._create_stream_chunk(f"TIER_TWO 账号自动切换到 ultra 模型: {model_key}\n")
|
| 667 |
+
debug_logger.log_info(f"[VIDEO] TIER_TWO 账号,模型自动调整: {model_config['model_key']} -> {model_key}")
|
| 668 |
+
|
| 669 |
+
# TIER_ONE 账号需要使用非 ultra 版本
|
| 670 |
+
elif user_tier == "PAYGATE_TIER_ONE":
|
| 671 |
+
# 如果模型 key 包含 ultra,需要移除(避免用户误用)
|
| 672 |
+
if "ultra" in model_key:
|
| 673 |
+
# veo_3_1_i2v_s_fast_ultra_fl -> veo_3_1_i2v_s_fast_fl
|
| 674 |
+
# veo_3_1_t2v_fast_ultra -> veo_3_1_t2v_fast
|
| 675 |
+
model_key = model_key.replace("_ultra_fl", "_fl").replace("_ultra", "")
|
| 676 |
+
|
| 677 |
+
if stream:
|
| 678 |
+
yield self._create_stream_chunk(f"TIER_ONE 账号自动切换到标准模型: {model_key}\n")
|
| 679 |
+
debug_logger.log_info(f"[VIDEO] TIER_ONE 账号,模型自动调整: {model_config['model_key']} -> {model_key}")
|
| 680 |
+
|
| 681 |
+
# 更新 model_config 中的 model_key
|
| 682 |
+
model_config = dict(model_config) # 创建副本避免修改原配置
|
| 683 |
+
model_config["model_key"] = model_key
|
| 684 |
+
|
| 685 |
# 图片数量
|
| 686 |
image_count = len(images) if images else 0
|
| 687 |
|
|
|
|
| 772 |
user_paygate_tier=token.user_paygate_tier or "PAYGATE_TIER_ONE"
|
| 773 |
)
|
| 774 |
else:
|
| 775 |
+
# 只有首帧 - 需要将 model_key 中的 _fl_ 替换为 _
|
| 776 |
+
# 例如: veo_3_1_i2v_s_fast_fl_ultra_relaxed -> veo_3_1_i2v_s_fast_ultra_relaxed
|
| 777 |
+
# veo_3_1_i2v_s_fast_portrait_fl_ultra_relaxed -> veo_3_1_i2v_s_fast_portrait_ultra_relaxed
|
| 778 |
+
actual_model_key = model_config["model_key"].replace("_fl_", "_")
|
| 779 |
+
debug_logger.log_info(f"[I2V] 单帧模式,model_key: {model_config['model_key']} -> {actual_model_key}")
|
| 780 |
result = await self.flow_client.generate_video_start_image(
|
| 781 |
at=token.at,
|
| 782 |
project_id=project_id,
|
| 783 |
prompt=prompt,
|
| 784 |
+
model_key=actual_model_key,
|
| 785 |
aspect_ratio=model_config["aspect_ratio"],
|
| 786 |
start_media_id=start_media_id,
|
| 787 |
user_paygate_tier=token.user_paygate_tier or "PAYGATE_TIER_ONE"
|
|
|
|
| 930 |
)
|
| 931 |
return
|
| 932 |
|
| 933 |
+
elif status == "MEDIA_GENERATION_STATUS_FAILED":
|
| 934 |
+
# 生成失败 - 提取错误信息
|
| 935 |
+
error_info = operation.get("operation", {}).get("error", {})
|
| 936 |
+
error_code = error_info.get("code", "unknown")
|
| 937 |
+
error_message = error_info.get("message", "未知错误")
|
| 938 |
+
|
| 939 |
+
# 更新数据库任务状态
|
| 940 |
+
task_id = operation["operation"]["name"]
|
| 941 |
+
await self.db.update_task(
|
| 942 |
+
task_id,
|
| 943 |
+
status="failed",
|
| 944 |
+
error_message=f"{error_message} (code: {error_code})",
|
| 945 |
+
completed_at=time.time()
|
| 946 |
+
)
|
| 947 |
+
|
| 948 |
+
# 返回友好的错误消息,提示用户重试
|
| 949 |
+
friendly_error = f"视频生成失败: {error_message},请重试"
|
| 950 |
+
if stream:
|
| 951 |
+
yield self._create_stream_chunk(f"❌ {friendly_error}\n")
|
| 952 |
+
yield self._create_error_response(friendly_error)
|
| 953 |
+
return
|
| 954 |
+
|
| 955 |
elif status.startswith("MEDIA_GENERATION_STATUS_ERROR"):
|
| 956 |
+
# 其他错误状态
|
| 957 |
yield self._create_error_response(f"视频生成失败: {status}")
|
| 958 |
return
|
| 959 |
|
src/services/token_manager.py
CHANGED
|
@@ -268,9 +268,13 @@ class TokenManager:
|
|
| 268 |
# AT有效
|
| 269 |
return True
|
| 270 |
|
|
|
|
| 271 |
async def _refresh_at(self, token_id: int) -> bool:
|
| 272 |
"""内部方法: 刷新AT
|
| 273 |
|
|
|
|
|
|
|
|
|
|
| 274 |
Returns:
|
| 275 |
True if refresh successful, False otherwise
|
| 276 |
"""
|
|
@@ -279,49 +283,132 @@ class TokenManager:
|
|
| 279 |
if not token:
|
| 280 |
return False
|
| 281 |
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
result = await self.flow_client.st_to_at(token.st)
|
| 287 |
-
new_at = result["access_token"]
|
| 288 |
-
expires = result.get("expires")
|
| 289 |
-
|
| 290 |
-
# 解析过期时间
|
| 291 |
-
new_at_expires = None
|
| 292 |
-
if expires:
|
| 293 |
-
try:
|
| 294 |
-
new_at_expires = datetime.fromisoformat(expires.replace('Z', '+00:00'))
|
| 295 |
-
except:
|
| 296 |
-
pass
|
| 297 |
-
|
| 298 |
-
# 更新数据库
|
| 299 |
-
await self.db.update_token(
|
| 300 |
-
token_id,
|
| 301 |
-
at=new_at,
|
| 302 |
-
at_expires=new_at_expires
|
| 303 |
-
)
|
| 304 |
|
| 305 |
-
|
| 306 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 307 |
|
| 308 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 309 |
try:
|
| 310 |
-
|
| 311 |
-
await self.db.update_token(
|
| 312 |
-
token_id,
|
| 313 |
-
credits=credits_result.get("credits", 0)
|
| 314 |
-
)
|
| 315 |
except:
|
| 316 |
pass
|
| 317 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 318 |
return True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 319 |
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 325 |
|
| 326 |
async def ensure_project_exists(self, token_id: int) -> str:
|
| 327 |
"""确保Token有可用的Project
|
|
|
|
| 268 |
# AT有效
|
| 269 |
return True
|
| 270 |
|
| 271 |
+
|
| 272 |
async def _refresh_at(self, token_id: int) -> bool:
|
| 273 |
"""内部方法: 刷新AT
|
| 274 |
|
| 275 |
+
如果 AT 刷新失败(ST 可能过期),会尝试通过浏览器自动刷新 ST,
|
| 276 |
+
然后重试 AT 刷新。
|
| 277 |
+
|
| 278 |
Returns:
|
| 279 |
True if refresh successful, False otherwise
|
| 280 |
"""
|
|
|
|
| 283 |
if not token:
|
| 284 |
return False
|
| 285 |
|
| 286 |
+
# 第一次尝试刷新 AT
|
| 287 |
+
result = await self._do_refresh_at(token_id, token.st)
|
| 288 |
+
if result:
|
| 289 |
+
return True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 290 |
|
| 291 |
+
# AT 刷新失败,尝试自动更新 ST
|
| 292 |
+
debug_logger.log_info(f"[AT_REFRESH] Token {token_id}: 第一次 AT 刷新失败,尝试自动更新 ST...")
|
| 293 |
+
|
| 294 |
+
new_st = await self._try_refresh_st(token_id, token)
|
| 295 |
+
if new_st:
|
| 296 |
+
# ST 更新成功,重试 AT 刷新
|
| 297 |
+
debug_logger.log_info(f"[AT_REFRESH] Token {token_id}: ST 已更新,重试 AT 刷新...")
|
| 298 |
+
result = await self._do_refresh_at(token_id, new_st)
|
| 299 |
+
if result:
|
| 300 |
+
return True
|
| 301 |
+
|
| 302 |
+
# 所有刷新尝试都失败,禁用 Token
|
| 303 |
+
debug_logger.log_error(f"[AT_REFRESH] Token {token_id}: 所有刷新尝试失败,禁用 Token")
|
| 304 |
+
await self.disable_token(token_id)
|
| 305 |
+
return False
|
| 306 |
+
|
| 307 |
+
async def _do_refresh_at(self, token_id: int, st: str) -> bool:
|
| 308 |
+
"""执行 AT 刷新的核心逻辑
|
| 309 |
+
|
| 310 |
+
Args:
|
| 311 |
+
token_id: Token ID
|
| 312 |
+
st: Session Token
|
| 313 |
|
| 314 |
+
Returns:
|
| 315 |
+
True if refresh successful AND AT is valid, False otherwise
|
| 316 |
+
"""
|
| 317 |
+
try:
|
| 318 |
+
debug_logger.log_info(f"[AT_REFRESH] Token {token_id}: 开始刷新AT...")
|
| 319 |
+
|
| 320 |
+
# 使用ST转AT
|
| 321 |
+
result = await self.flow_client.st_to_at(st)
|
| 322 |
+
new_at = result["access_token"]
|
| 323 |
+
expires = result.get("expires")
|
| 324 |
+
|
| 325 |
+
# 解析过期时间
|
| 326 |
+
new_at_expires = None
|
| 327 |
+
if expires:
|
| 328 |
try:
|
| 329 |
+
new_at_expires = datetime.fromisoformat(expires.replace('Z', '+00:00'))
|
|
|
|
|
|
|
|
|
|
|
|
|
| 330 |
except:
|
| 331 |
pass
|
| 332 |
|
| 333 |
+
# 更新数据库
|
| 334 |
+
await self.db.update_token(
|
| 335 |
+
token_id,
|
| 336 |
+
at=new_at,
|
| 337 |
+
at_expires=new_at_expires
|
| 338 |
+
)
|
| 339 |
+
|
| 340 |
+
debug_logger.log_info(f"[AT_REFRESH] Token {token_id}: AT刷新成功")
|
| 341 |
+
debug_logger.log_info(f" - 新过期时间: {new_at_expires}")
|
| 342 |
+
|
| 343 |
+
# 验证 AT 有效性:通过 get_credits 测试
|
| 344 |
+
try:
|
| 345 |
+
credits_result = await self.flow_client.get_credits(new_at)
|
| 346 |
+
await self.db.update_token(
|
| 347 |
+
token_id,
|
| 348 |
+
credits=credits_result.get("credits", 0)
|
| 349 |
+
)
|
| 350 |
+
debug_logger.log_info(f"[AT_REFRESH] Token {token_id}: AT 验证成功(余额: {credits_result.get('credits', 0)})")
|
| 351 |
return True
|
| 352 |
+
except Exception as verify_err:
|
| 353 |
+
# AT 验证失败(可能返回 401),说明 ST 已过期
|
| 354 |
+
error_msg = str(verify_err)
|
| 355 |
+
if "401" in error_msg or "UNAUTHENTICATED" in error_msg:
|
| 356 |
+
debug_logger.log_warning(f"[AT_REFRESH] Token {token_id}: AT 验证失败 (401),ST 可能已过期")
|
| 357 |
+
return False
|
| 358 |
+
else:
|
| 359 |
+
# 其他错误(如网络问题),仍视为成功
|
| 360 |
+
debug_logger.log_warning(f"[AT_REFRESH] Token {token_id}: AT 验证时发生非认证错误: {error_msg}")
|
| 361 |
+
return True
|
| 362 |
|
| 363 |
+
except Exception as e:
|
| 364 |
+
debug_logger.log_error(f"[AT_REFRESH] Token {token_id}: AT刷新失败 - {str(e)}")
|
| 365 |
+
return False
|
| 366 |
+
|
| 367 |
+
async def _try_refresh_st(self, token_id: int, token) -> Optional[str]:
|
| 368 |
+
"""尝试通过浏览器刷新 Session Token
|
| 369 |
+
|
| 370 |
+
使用常驻 tab 获取新的 __Secure-next-auth.session-token
|
| 371 |
+
|
| 372 |
+
Args:
|
| 373 |
+
token_id: Token ID
|
| 374 |
+
token: Token 对象
|
| 375 |
+
|
| 376 |
+
Returns:
|
| 377 |
+
新的 ST 字符串,如果失败返回 None
|
| 378 |
+
"""
|
| 379 |
+
try:
|
| 380 |
+
from ..core.config import config
|
| 381 |
+
|
| 382 |
+
# 仅在 personal 模式下支持 ST 自动刷新
|
| 383 |
+
if config.captcha_method != "personal":
|
| 384 |
+
debug_logger.log_info(f"[ST_REFRESH] 非 personal 模式,跳过 ST 自动刷新")
|
| 385 |
+
return None
|
| 386 |
+
|
| 387 |
+
if not token.current_project_id:
|
| 388 |
+
debug_logger.log_warning(f"[ST_REFRESH] Token {token_id} 没有 project_id,无法刷新 ST")
|
| 389 |
+
return None
|
| 390 |
+
|
| 391 |
+
debug_logger.log_info(f"[ST_REFRESH] Token {token_id}: 尝试通过浏览器刷新 ST...")
|
| 392 |
+
|
| 393 |
+
from .browser_captcha_personal import BrowserCaptchaService
|
| 394 |
+
service = await BrowserCaptchaService.get_instance(self.db)
|
| 395 |
+
|
| 396 |
+
new_st = await service.refresh_session_token(token.current_project_id)
|
| 397 |
+
if new_st and new_st != token.st:
|
| 398 |
+
# 更新数据库中的 ST
|
| 399 |
+
await self.db.update_token(token_id, st=new_st)
|
| 400 |
+
debug_logger.log_info(f"[ST_REFRESH] Token {token_id}: ST 已自动更新")
|
| 401 |
+
return new_st
|
| 402 |
+
elif new_st == token.st:
|
| 403 |
+
debug_logger.log_warning(f"[ST_REFRESH] Token {token_id}: 获取到的 ST 与原 ST 相同,可能登录已失效")
|
| 404 |
+
return None
|
| 405 |
+
else:
|
| 406 |
+
debug_logger.log_warning(f"[ST_REFRESH] Token {token_id}: 无法获取新 ST")
|
| 407 |
+
return None
|
| 408 |
+
|
| 409 |
+
except Exception as e:
|
| 410 |
+
debug_logger.log_error(f"[ST_REFRESH] Token {token_id}: 刷新 ST 失败 - {str(e)}")
|
| 411 |
+
return None
|
| 412 |
|
| 413 |
async def ensure_project_exists(self, token_id: int) -> str:
|
| 414 |
"""确保Token有可用的Project
|
static/manage.html
CHANGED
|
@@ -269,6 +269,9 @@
|
|
| 269 |
<label class="text-sm font-medium mb-2 block">打码方式</label>
|
| 270 |
<select id="cfgCaptchaMethod" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" onchange="toggleCaptchaOptions()">
|
| 271 |
<option value="yescaptcha">YesCaptcha打码</option>
|
|
|
|
|
|
|
|
|
|
| 272 |
<option value="browser">无头浏览器打码</option>
|
| 273 |
<option value="personal">内置浏览器打码</option>
|
| 274 |
</select>
|
|
@@ -289,6 +292,48 @@
|
|
| 289 |
</div>
|
| 290 |
</div>
|
| 291 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 292 |
<!-- 浏览器打码配置选项 -->
|
| 293 |
<div id="browserCaptchaOptions" class="hidden space-y-4">
|
| 294 |
<div class="rounded-md bg-blue-50 dark:bg-blue-900/20 p-3 border border-blue-200 dark:border-blue-800">
|
|
@@ -335,6 +380,7 @@
|
|
| 335 |
<label class="text-sm font-semibold mb-2 block">连接Token</label>
|
| 336 |
<div class="flex gap-2">
|
| 337 |
<input id="cfgPluginConnectionToken" type="text" class="flex h-9 flex-1 rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="留空自动生成">
|
|
|
|
| 338 |
<button onclick="copyConnectionToken()" class="inline-flex items-center justify-center rounded-md bg-secondary text-secondary-foreground hover:bg-secondary/80 h-9 px-4">复制</button>
|
| 339 |
</div>
|
| 340 |
<p class="text-xs text-muted-foreground mt-1">用于验证Chrome扩展插件的身份,留空将自动生成随机token</p>
|
|
@@ -698,14 +744,15 @@
|
|
| 698 |
loadGenerationTimeout=async()=>{try{console.log('开始加载生成超时配置...');const r=await apiRequest('/api/generation/timeout');if(!r){console.error('API请求失败');return}const d=await r.json();console.log('生成超时配置数据:',d);if(d.success&&d.config){const imageTimeout=d.config.image_timeout||300;const videoTimeout=d.config.video_timeout||1500;console.log('设置图片超时:',imageTimeout);console.log('设置视频超时:',videoTimeout);$('cfgImageTimeout').value=imageTimeout;$('cfgVideoTimeout').value=videoTimeout;console.log('生成超时配置加载成功')}else{console.error('生成超时配置数据格式错误:',d)}}catch(e){console.error('加载生成超时配置失败:',e);showToast('加载生成超时配置失败: '+e.message,'error')}},
|
| 699 |
saveCacheConfig=async()=>{const enabled=$('cfgCacheEnabled').checked,timeout=parseInt($('cfgCacheTimeout').value)||7200,baseUrl=$('cfgCacheBaseUrl').value.trim();console.log('保存缓存配置:',{enabled,timeout,baseUrl});if(timeout<60||timeout>86400)return showToast('缓存超时时间必须在 60-86400 秒之间','error');if(baseUrl&&!baseUrl.startsWith('http://')&&!baseUrl.startsWith('https://'))return showToast('域名必须以 http:// 或 https:// 开头','error');try{console.log('保存缓存启用状态...');const r0=await apiRequest('/api/cache/enabled',{method:'POST',body:JSON.stringify({enabled:enabled})});if(!r0){console.error('保存缓存启用状态请求失败');return}const d0=await r0.json();console.log('缓存启用状态保存结果:',d0);if(!d0.success){console.error('保存缓存启用状态失败:',d0);return showToast('保存缓存启用状态失败','error')}console.log('保存超时时间...');const r1=await apiRequest('/api/cache/config',{method:'POST',body:JSON.stringify({timeout:timeout})});if(!r1){console.error('保存超时时间请求失败');return}const d1=await r1.json();console.log('超时时间保存结果:',d1);if(!d1.success){console.error('保存超时时间失败:',d1);return showToast('保存超时时间失败','error')}console.log('保存域名...');const r2=await apiRequest('/api/cache/base-url',{method:'POST',body:JSON.stringify({base_url:baseUrl})});if(!r2){console.error('保存域名请求失败');return}const d2=await r2.json();console.log('域名保存结果:',d2);if(d2.success){showToast('缓存配置保存成功','success');console.log('等待配置文件写入完成...');await new Promise(r=>setTimeout(r,200));console.log('重新加载配置...');await loadCacheConfig()}else{console.error('保存域名失败:',d2);showToast('保存域名失败','error')}}catch(e){console.error('保存失败:',e);showToast('保存失败: '+e.message,'error')}},
|
| 700 |
saveGenerationTimeout=async()=>{const imageTimeout=parseInt($('cfgImageTimeout').value)||300,videoTimeout=parseInt($('cfgVideoTimeout').value)||1500;console.log('保存生成超时配置:',{imageTimeout,videoTimeout});if(imageTimeout<60||imageTimeout>3600)return showToast('图片超时时间必须在 60-3600 秒之间','error');if(videoTimeout<60||videoTimeout>7200)return showToast('视频超时时间必须在 60-7200 秒之间','error');try{const r=await apiRequest('/api/generation/timeout',{method:'POST',body:JSON.stringify({image_timeout:imageTimeout,video_timeout:videoTimeout})});if(!r){console.error('保存请求失败');return}const d=await r.json();console.log('保存结果:',d);if(d.success){showToast('生成超时配置保存成功','success');await new Promise(r=>setTimeout(r,200));await loadGenerationTimeout()}else{console.error('保存失败:',d);showToast('保存失败','error')}}catch(e){console.error('保存失败:',e);showToast('保存失败: '+e.message,'error')}},
|
| 701 |
-
toggleCaptchaOptions=()=>{const method=$('cfgCaptchaMethod').value;$('yescaptchaOptions').style.display=method==='yescaptcha'?'block':'none';$('browserCaptchaOptions').classList.toggle('hidden',method!=='browser')},
|
| 702 |
toggleBrowserProxyInput=()=>{const enabled=$('cfgBrowserProxyEnabled').checked;$('browserProxyUrlInput').classList.toggle('hidden',!enabled)},
|
| 703 |
-
loadCaptchaConfig=async()=>{try{console.log('开始加载验证码配置...');const r=await apiRequest('/api/captcha/config');if(!r){console.error('API请求失败');return}const d=await r.json();console.log('验证码配置数据:',d);$('cfgCaptchaMethod').value=d.captcha_method||'yescaptcha';$('cfgYescaptchaApiKey').value=d.yescaptcha_api_key||'';$('cfgYescaptchaBaseUrl').value=d.yescaptcha_base_url||'https://api.yescaptcha.com';$('cfgBrowserProxyEnabled').checked=d.browser_proxy_enabled||false;$('cfgBrowserProxyUrl').value=d.browser_proxy_url||'';toggleCaptchaOptions();toggleBrowserProxyInput();console.log('验证码配置加载成功')}catch(e){console.error('加载验证码配置失败:',e);showToast('加载验证码配置失败: '+e.message,'error')}},
|
| 704 |
-
saveCaptchaConfig=async()=>{const method=$('cfgCaptchaMethod').value,
|
| 705 |
loadPluginConfig=async()=>{try{const r=await apiRequest('/api/plugin/config');if(!r)return;const d=await r.json();if(d.success&&d.config){$('cfgPluginConnectionUrl').value=d.config.connection_url||'';$('cfgPluginConnectionToken').value=d.config.connection_token||'';$('cfgAutoEnableOnUpdate').checked=d.config.auto_enable_on_update||false}}catch(e){console.error('加���插件配置失败:',e);showToast('加载插件配置失败: '+e.message,'error')}},
|
| 706 |
savePluginConfig=async()=>{const token=$('cfgPluginConnectionToken').value.trim();const autoEnable=$('cfgAutoEnableOnUpdate').checked;try{const r=await apiRequest('/api/plugin/config',{method:'POST',body:JSON.stringify({connection_token:token,auto_enable_on_update:autoEnable})});if(!r)return;const d=await r.json();if(d.success){showToast('插件配置保存成功','success');await loadPluginConfig()}else{showToast(d.message||'保存失败','error')}}catch(e){showToast('保存失败: '+e.message,'error')}},
|
| 707 |
copyConnectionUrl=()=>{const url=$('cfgPluginConnectionUrl').value;if(!url){showToast('连接接口为空','error');return}navigator.clipboard.writeText(url).then(()=>showToast('连接接口已复制','success')).catch(()=>showToast('复制失败','error'))},
|
| 708 |
copyConnectionToken=()=>{const token=$('cfgPluginConnectionToken').value;if(!token){showToast('连接Token为空','error');return}navigator.clipboard.writeText(token).then(()=>showToast('连接Token已复制','success')).catch(()=>showToast('复制失败','error'))},
|
|
|
|
| 709 |
toggleATAutoRefresh=async()=>{try{const enabled=$('atAutoRefreshToggle').checked;const r=await apiRequest('/api/token-refresh/enabled',{method:'POST',body:JSON.stringify({enabled:enabled})});if(!r){$('atAutoRefreshToggle').checked=!enabled;return}const d=await r.json();if(d.success){showToast(enabled?'AT自动刷新已启用':'AT自动刷新已禁用','success')}else{showToast('操作失败: '+(d.detail||'未知错误'),'error');$('atAutoRefreshToggle').checked=!enabled}}catch(e){showToast('操作失败: '+e.message,'error');$('atAutoRefreshToggle').checked=!enabled}},
|
| 710 |
loadATAutoRefreshConfig=async()=>{try{const r=await apiRequest('/api/token-refresh/config');if(!r)return;const d=await r.json();if(d.success&&d.config){$('atAutoRefreshToggle').checked=d.config.at_auto_refresh_enabled||false}else{console.error('AT自动刷新配置数据格式错误:',d)}}catch(e){console.error('加载AT自动刷新配置失败:',e)}},
|
| 711 |
loadLogs=async()=>{try{const r=await apiRequest('/api/logs?limit=100');if(!r)return;const logs=await r.json();window.allLogs=logs;const tb=$('logsTableBody');tb.innerHTML=logs.map(l=>`<tr><td class="py-2.5 px-3">${l.operation}</td><td class="py-2.5 px-3"><span class="text-xs ${l.token_email?'text-blue-600':'text-muted-foreground'}">${l.token_email||'未知'}</span></td><td class="py-2.5 px-3"><span class="inline-flex items-center rounded px-2 py-0.5 text-xs ${l.status_code===200?'bg-green-50 text-green-700':'bg-red-50 text-red-700'}">${l.status_code}</span></td><td class="py-2.5 px-3">${l.duration.toFixed(2)}</td><td class="py-2.5 px-3 text-xs text-muted-foreground">${l.created_at?new Date(l.created_at).toLocaleString('zh-CN'):'-'}</td><td class="py-2.5 px-3"><button onclick="showLogDetail(${l.id})" class="inline-flex items-center justify-center rounded-md hover:bg-blue-50 hover:text-blue-700 h-7 px-2 text-xs">查看</button></td></tr>`).join('')}catch(e){console.error('加载日志失败:',e)}},
|
|
|
|
| 269 |
<label class="text-sm font-medium mb-2 block">打码方式</label>
|
| 270 |
<select id="cfgCaptchaMethod" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" onchange="toggleCaptchaOptions()">
|
| 271 |
<option value="yescaptcha">YesCaptcha打码</option>
|
| 272 |
+
<option value="capmonster">CapMonster打码</option>
|
| 273 |
+
<option value="ezcaptcha">EzCaptcha打码</option>
|
| 274 |
+
<option value="capsolver">CapSolver打码</option>
|
| 275 |
<option value="browser">无头浏览器打码</option>
|
| 276 |
<option value="personal">内置浏览器打码</option>
|
| 277 |
</select>
|
|
|
|
| 292 |
</div>
|
| 293 |
</div>
|
| 294 |
|
| 295 |
+
<!-- CapMonster配置选项 -->
|
| 296 |
+
<div id="capmonsterOptions" class="hidden space-y-4">
|
| 297 |
+
<div>
|
| 298 |
+
<label class="text-sm font-medium mb-2 block">CapMonster API密钥</label>
|
| 299 |
+
<input id="cfgCapmonsterApiKey" type="text" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="请输入CapMonster API密钥">
|
| 300 |
+
<p class="text-xs text-muted-foreground mt-1">用于自动获取reCAPTCHA验证码</p>
|
| 301 |
+
</div>
|
| 302 |
+
<div>
|
| 303 |
+
<label class="text-sm font-medium mb-2 block">CapMonster API地址</label>
|
| 304 |
+
<input id="cfgCapmonsterBaseUrl" type="text" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="https://api.capmonster.cloud">
|
| 305 |
+
<p class="text-xs text-muted-foreground mt-1">CapMonster服务地址,默认:https://api.capmonster.cloud</p>
|
| 306 |
+
</div>
|
| 307 |
+
</div>
|
| 308 |
+
|
| 309 |
+
<!-- EzCaptcha配置选项 -->
|
| 310 |
+
<div id="ezcaptchaOptions" class="hidden space-y-4">
|
| 311 |
+
<div>
|
| 312 |
+
<label class="text-sm font-medium mb-2 block">EzCaptcha API密钥</label>
|
| 313 |
+
<input id="cfgEzcaptchaApiKey" type="text" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="请输入EzCaptcha API密钥">
|
| 314 |
+
<p class="text-xs text-muted-foreground mt-1">用于自动获取reCAPTCHA验证码</p>
|
| 315 |
+
</div>
|
| 316 |
+
<div>
|
| 317 |
+
<label class="text-sm font-medium mb-2 block">EzCaptcha API地址</label>
|
| 318 |
+
<input id="cfgEzcaptchaBaseUrl" type="text" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="https://api.ez-captcha.com">
|
| 319 |
+
<p class="text-xs text-muted-foreground mt-1">EzCaptcha服务地址,默认:https://api.ez-captcha.com</p>
|
| 320 |
+
</div>
|
| 321 |
+
</div>
|
| 322 |
+
|
| 323 |
+
<!-- CapSolver配置选项 -->
|
| 324 |
+
<div id="capsolverOptions" class="hidden space-y-4">
|
| 325 |
+
<div>
|
| 326 |
+
<label class="text-sm font-medium mb-2 block">CapSolver API密钥</label>
|
| 327 |
+
<input id="cfgCapsolverApiKey" type="text" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="请输入CapSolver API密钥">
|
| 328 |
+
<p class="text-xs text-muted-foreground mt-1">用于自动获取reCAPTCHA验证码</p>
|
| 329 |
+
</div>
|
| 330 |
+
<div>
|
| 331 |
+
<label class="text-sm font-medium mb-2 block">CapSolver API地址</label>
|
| 332 |
+
<input id="cfgCapsolverBaseUrl" type="text" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="https://api.capsolver.com">
|
| 333 |
+
<p class="text-xs text-muted-foreground mt-1">CapSolver服务地址,默认:https://api.capsolver.com</p>
|
| 334 |
+
</div>
|
| 335 |
+
</div>
|
| 336 |
+
|
| 337 |
<!-- 浏览器打码配置选项 -->
|
| 338 |
<div id="browserCaptchaOptions" class="hidden space-y-4">
|
| 339 |
<div class="rounded-md bg-blue-50 dark:bg-blue-900/20 p-3 border border-blue-200 dark:border-blue-800">
|
|
|
|
| 380 |
<label class="text-sm font-semibold mb-2 block">连接Token</label>
|
| 381 |
<div class="flex gap-2">
|
| 382 |
<input id="cfgPluginConnectionToken" type="text" class="flex h-9 flex-1 rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="留空自动生成">
|
| 383 |
+
<button onclick="generateRandomToken()" class="inline-flex items-center justify-center rounded-md bg-primary text-primary-foreground hover:bg-primary/90 h-9 px-4">随机</button>
|
| 384 |
<button onclick="copyConnectionToken()" class="inline-flex items-center justify-center rounded-md bg-secondary text-secondary-foreground hover:bg-secondary/80 h-9 px-4">复制</button>
|
| 385 |
</div>
|
| 386 |
<p class="text-xs text-muted-foreground mt-1">用于验证Chrome扩展插件的身份,留空将自动生成随机token</p>
|
|
|
|
| 744 |
loadGenerationTimeout=async()=>{try{console.log('开始加载生成超时配置...');const r=await apiRequest('/api/generation/timeout');if(!r){console.error('API请求失败');return}const d=await r.json();console.log('生成超时配置数据:',d);if(d.success&&d.config){const imageTimeout=d.config.image_timeout||300;const videoTimeout=d.config.video_timeout||1500;console.log('设置图片超时:',imageTimeout);console.log('设置视频超时:',videoTimeout);$('cfgImageTimeout').value=imageTimeout;$('cfgVideoTimeout').value=videoTimeout;console.log('生成超时配置加载成功')}else{console.error('生成超时配置数据格式错误:',d)}}catch(e){console.error('加载生成超时配置失败:',e);showToast('加载生成超时配置失败: '+e.message,'error')}},
|
| 745 |
saveCacheConfig=async()=>{const enabled=$('cfgCacheEnabled').checked,timeout=parseInt($('cfgCacheTimeout').value)||7200,baseUrl=$('cfgCacheBaseUrl').value.trim();console.log('保存缓存配置:',{enabled,timeout,baseUrl});if(timeout<60||timeout>86400)return showToast('缓存超时时间必须在 60-86400 秒之间','error');if(baseUrl&&!baseUrl.startsWith('http://')&&!baseUrl.startsWith('https://'))return showToast('域名必须以 http:// 或 https:// 开头','error');try{console.log('保存缓存启用状态...');const r0=await apiRequest('/api/cache/enabled',{method:'POST',body:JSON.stringify({enabled:enabled})});if(!r0){console.error('保存缓存启用状态请求失败');return}const d0=await r0.json();console.log('缓存启用状态保存结果:',d0);if(!d0.success){console.error('保存缓存启用状态失败:',d0);return showToast('保存缓存启用状态失败','error')}console.log('保存超时时间...');const r1=await apiRequest('/api/cache/config',{method:'POST',body:JSON.stringify({timeout:timeout})});if(!r1){console.error('保存超时时间请求失败');return}const d1=await r1.json();console.log('超时时间保存结果:',d1);if(!d1.success){console.error('保存超时时间失败:',d1);return showToast('保存超时时间失败','error')}console.log('保存域名...');const r2=await apiRequest('/api/cache/base-url',{method:'POST',body:JSON.stringify({base_url:baseUrl})});if(!r2){console.error('保存域名请求失败');return}const d2=await r2.json();console.log('域名保存结果:',d2);if(d2.success){showToast('缓存配置保存成功','success');console.log('等待配置文件写入完成...');await new Promise(r=>setTimeout(r,200));console.log('重新加载配置...');await loadCacheConfig()}else{console.error('保存域名失败:',d2);showToast('保存域名失败','error')}}catch(e){console.error('保存失败:',e);showToast('保存失败: '+e.message,'error')}},
|
| 746 |
saveGenerationTimeout=async()=>{const imageTimeout=parseInt($('cfgImageTimeout').value)||300,videoTimeout=parseInt($('cfgVideoTimeout').value)||1500;console.log('保存生成超时配置:',{imageTimeout,videoTimeout});if(imageTimeout<60||imageTimeout>3600)return showToast('图片超时时间必须在 60-3600 秒之间','error');if(videoTimeout<60||videoTimeout>7200)return showToast('视频超时时间必须在 60-7200 秒之间','error');try{const r=await apiRequest('/api/generation/timeout',{method:'POST',body:JSON.stringify({image_timeout:imageTimeout,video_timeout:videoTimeout})});if(!r){console.error('保存请求失败');return}const d=await r.json();console.log('保存结果:',d);if(d.success){showToast('生成超时配置保存成功','success');await new Promise(r=>setTimeout(r,200));await loadGenerationTimeout()}else{console.error('保存失败:',d);showToast('保存失败','error')}}catch(e){console.error('保存失败:',e);showToast('保存失败: '+e.message,'error')}},
|
| 747 |
+
toggleCaptchaOptions=()=>{const method=$('cfgCaptchaMethod').value;$('yescaptchaOptions').style.display=method==='yescaptcha'?'block':'none';$('capmonsterOptions').classList.toggle('hidden',method!=='capmonster');$('ezcaptchaOptions').classList.toggle('hidden',method!=='ezcaptcha');$('capsolverOptions').classList.toggle('hidden',method!=='capsolver');$('browserCaptchaOptions').classList.toggle('hidden',method!=='browser')},
|
| 748 |
toggleBrowserProxyInput=()=>{const enabled=$('cfgBrowserProxyEnabled').checked;$('browserProxyUrlInput').classList.toggle('hidden',!enabled)},
|
| 749 |
+
loadCaptchaConfig=async()=>{try{console.log('开始加载验证码配置...');const r=await apiRequest('/api/captcha/config');if(!r){console.error('API请求失败');return}const d=await r.json();console.log('验证码配置数据:',d);$('cfgCaptchaMethod').value=d.captcha_method||'yescaptcha';$('cfgYescaptchaApiKey').value=d.yescaptcha_api_key||'';$('cfgYescaptchaBaseUrl').value=d.yescaptcha_base_url||'https://api.yescaptcha.com';$('cfgCapmonsterApiKey').value=d.capmonster_api_key||'';$('cfgCapmonsterBaseUrl').value=d.capmonster_base_url||'https://api.capmonster.cloud';$('cfgEzcaptchaApiKey').value=d.ezcaptcha_api_key||'';$('cfgEzcaptchaBaseUrl').value=d.ezcaptcha_base_url||'https://api.ez-captcha.com';$('cfgCapsolverApiKey').value=d.capsolver_api_key||'';$('cfgCapsolverBaseUrl').value=d.capsolver_base_url||'https://api.capsolver.com';$('cfgBrowserProxyEnabled').checked=d.browser_proxy_enabled||false;$('cfgBrowserProxyUrl').value=d.browser_proxy_url||'';toggleCaptchaOptions();toggleBrowserProxyInput();console.log('验证码配置加载成功')}catch(e){console.error('加载验证码配置失败:',e);showToast('加载验证码配置失败: '+e.message,'error')}},
|
| 750 |
+
saveCaptchaConfig=async()=>{const method=$('cfgCaptchaMethod').value,yesApiKey=$('cfgYescaptchaApiKey').value.trim(),yesBaseUrl=$('cfgYescaptchaBaseUrl').value.trim(),capApiKey=$('cfgCapmonsterApiKey').value.trim(),capBaseUrl=$('cfgCapmonsterBaseUrl').value.trim(),ezApiKey=$('cfgEzcaptchaApiKey').value.trim(),ezBaseUrl=$('cfgEzcaptchaBaseUrl').value.trim(),solverApiKey=$('cfgCapsolverApiKey').value.trim(),solverBaseUrl=$('cfgCapsolverBaseUrl').value.trim(),browserProxyEnabled=$('cfgBrowserProxyEnabled').checked,browserProxyUrl=$('cfgBrowserProxyUrl').value.trim();console.log('保存验证码配置:',{method,yesApiKey,yesBaseUrl,capApiKey,capBaseUrl,ezApiKey,ezBaseUrl,solverApiKey,solverBaseUrl,browserProxyEnabled,browserProxyUrl});try{const r=await apiRequest('/api/captcha/config',{method:'POST',body:JSON.stringify({captcha_method:method,yescaptcha_api_key:yesApiKey,yescaptcha_base_url:yesBaseUrl,capmonster_api_key:capApiKey,capmonster_base_url:capBaseUrl,ezcaptcha_api_key:ezApiKey,ezcaptcha_base_url:ezBaseUrl,capsolver_api_key:solverApiKey,capsolver_base_url:solverBaseUrl,browser_proxy_enabled:browserProxyEnabled,browser_proxy_url:browserProxyUrl})});if(!r){console.error('保存请求失败');return}const d=await r.json();console.log('保存结果:',d);if(d.success){showToast('验证码配置保存成功','success');await new Promise(r=>setTimeout(r,200));await loadCaptchaConfig()}else{console.error('保存失败:',d);showToast(d.message||'保存失败','error')}}catch(e){console.error('保存失败:',e);showToast('保存失败: '+e.message,'error')}},
|
| 751 |
loadPluginConfig=async()=>{try{const r=await apiRequest('/api/plugin/config');if(!r)return;const d=await r.json();if(d.success&&d.config){$('cfgPluginConnectionUrl').value=d.config.connection_url||'';$('cfgPluginConnectionToken').value=d.config.connection_token||'';$('cfgAutoEnableOnUpdate').checked=d.config.auto_enable_on_update||false}}catch(e){console.error('加���插件配置失败:',e);showToast('加载插件配置失败: '+e.message,'error')}},
|
| 752 |
savePluginConfig=async()=>{const token=$('cfgPluginConnectionToken').value.trim();const autoEnable=$('cfgAutoEnableOnUpdate').checked;try{const r=await apiRequest('/api/plugin/config',{method:'POST',body:JSON.stringify({connection_token:token,auto_enable_on_update:autoEnable})});if(!r)return;const d=await r.json();if(d.success){showToast('插件配置保存成功','success');await loadPluginConfig()}else{showToast(d.message||'保存失败','error')}}catch(e){showToast('保存失败: '+e.message,'error')}},
|
| 753 |
copyConnectionUrl=()=>{const url=$('cfgPluginConnectionUrl').value;if(!url){showToast('连接接口为空','error');return}navigator.clipboard.writeText(url).then(()=>showToast('连接接口已复制','success')).catch(()=>showToast('复制失败','error'))},
|
| 754 |
copyConnectionToken=()=>{const token=$('cfgPluginConnectionToken').value;if(!token){showToast('连接Token为空','error');return}navigator.clipboard.writeText(token).then(()=>showToast('连接Token已复制','success')).catch(()=>showToast('复制失败','error'))},
|
| 755 |
+
generateRandomToken=()=>{const chars='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';let token='';for(let i=0;i<32;i++){token+=chars.charAt(Math.floor(Math.random()*chars.length))}$('cfgPluginConnectionToken').value=token;showToast('随机Token已生成','success')},
|
| 756 |
toggleATAutoRefresh=async()=>{try{const enabled=$('atAutoRefreshToggle').checked;const r=await apiRequest('/api/token-refresh/enabled',{method:'POST',body:JSON.stringify({enabled:enabled})});if(!r){$('atAutoRefreshToggle').checked=!enabled;return}const d=await r.json();if(d.success){showToast(enabled?'AT自动刷新已启用':'AT自动刷新已禁用','success')}else{showToast('操作失败: '+(d.detail||'未知错误'),'error');$('atAutoRefreshToggle').checked=!enabled}}catch(e){showToast('操作失败: '+e.message,'error');$('atAutoRefreshToggle').checked=!enabled}},
|
| 757 |
loadATAutoRefreshConfig=async()=>{try{const r=await apiRequest('/api/token-refresh/config');if(!r)return;const d=await r.json();if(d.success&&d.config){$('atAutoRefreshToggle').checked=d.config.at_auto_refresh_enabled||false}else{console.error('AT自动刷新配置数据格式错误:',d)}}catch(e){console.error('加载AT自动刷新配置失败:',e)}},
|
| 758 |
loadLogs=async()=>{try{const r=await apiRequest('/api/logs?limit=100');if(!r)return;const logs=await r.json();window.allLogs=logs;const tb=$('logsTableBody');tb.innerHTML=logs.map(l=>`<tr><td class="py-2.5 px-3">${l.operation}</td><td class="py-2.5 px-3"><span class="text-xs ${l.token_email?'text-blue-600':'text-muted-foreground'}">${l.token_email||'未知'}</span></td><td class="py-2.5 px-3"><span class="inline-flex items-center rounded px-2 py-0.5 text-xs ${l.status_code===200?'bg-green-50 text-green-700':'bg-red-50 text-red-700'}">${l.status_code}</span></td><td class="py-2.5 px-3">${l.duration.toFixed(2)}</td><td class="py-2.5 px-3 text-xs text-muted-foreground">${l.created_at?new Date(l.created_at).toLocaleString('zh-CN'):'-'}</td><td class="py-2.5 px-3"><button onclick="showLogDetail(${l.id})" class="inline-flex items-center justify-center rounded-md hover:bg-blue-50 hover:text-blue-700 h-7 px-2 text-xs">查看</button></td></tr>`).join('')}catch(e){console.error('加载日志失败:',e)}},
|