genz27 Warp commited on
Commit ·
c42cf8e
1
Parent(s): f2d92d1
fix: 修复各个代码文件中对action参数的调用
Browse files- 将FLOW_GENERATION替换为IMAGE_GENERATION/VIDEO_GENERATION
- browser_captcha_personal.py: get_token/execute方法支持action参数
- flow_client.py: _get_api_captcha_token支持动态action
- 更新数据库和模型的默认值
- 添加GitHub Actions工作流用于构建ghcr.io镜像
Co-Authored-By: Warp <agent@warp.dev>
- .github/workflows/docker-publish.yml +64 -0
- .gitignore +2 -1
- Dockerfile +2 -33
- config/setting.toml +3 -3
- config/setting_example.toml +3 -3
- config/setting_warp.toml +0 -42
- config/setting_warp_example.toml +0 -42
- request.py +0 -150
- src/core/database.py +3 -2
- src/core/models.py +1 -1
- src/services/browser_captcha_personal.py +16 -11
- src/services/flow_client.py +10 -4
.github/workflows/docker-publish.yml
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Build and Push Docker Image
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
push:
|
| 5 |
+
branches:
|
| 6 |
+
- main
|
| 7 |
+
tags:
|
| 8 |
+
- 'v*'
|
| 9 |
+
pull_request:
|
| 10 |
+
branches:
|
| 11 |
+
- main
|
| 12 |
+
workflow_dispatch:
|
| 13 |
+
|
| 14 |
+
env:
|
| 15 |
+
REGISTRY: ghcr.io
|
| 16 |
+
IMAGE_NAME: ${{ github.repository }}
|
| 17 |
+
|
| 18 |
+
jobs:
|
| 19 |
+
build-and-push:
|
| 20 |
+
runs-on: ubuntu-latest
|
| 21 |
+
permissions:
|
| 22 |
+
contents: read
|
| 23 |
+
packages: write
|
| 24 |
+
|
| 25 |
+
steps:
|
| 26 |
+
- name: Checkout repository
|
| 27 |
+
uses: actions/checkout@v4
|
| 28 |
+
|
| 29 |
+
- name: Set up QEMU
|
| 30 |
+
uses: docker/setup-qemu-action@v3
|
| 31 |
+
|
| 32 |
+
- name: Set up Docker Buildx
|
| 33 |
+
uses: docker/setup-buildx-action@v3
|
| 34 |
+
|
| 35 |
+
- name: Log in to Container Registry
|
| 36 |
+
if: github.event_name != 'pull_request'
|
| 37 |
+
uses: docker/login-action@v3
|
| 38 |
+
with:
|
| 39 |
+
registry: ${{ env.REGISTRY }}
|
| 40 |
+
username: ${{ github.actor }}
|
| 41 |
+
password: ${{ secrets.GITHUB_TOKEN }}
|
| 42 |
+
|
| 43 |
+
- name: Extract metadata (tags, labels)
|
| 44 |
+
id: meta
|
| 45 |
+
uses: docker/metadata-action@v5
|
| 46 |
+
with:
|
| 47 |
+
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
| 48 |
+
tags: |
|
| 49 |
+
type=ref,event=branch
|
| 50 |
+
type=ref,event=pr
|
| 51 |
+
type=semver,pattern={{version}}
|
| 52 |
+
type=semver,pattern={{major}}.{{minor}}
|
| 53 |
+
type=raw,value=latest,enable={{is_default_branch}}
|
| 54 |
+
|
| 55 |
+
- name: Build and push Docker image
|
| 56 |
+
uses: docker/build-push-action@v5
|
| 57 |
+
with:
|
| 58 |
+
context: .
|
| 59 |
+
platforms: linux/amd64,linux/arm64
|
| 60 |
+
push: ${{ github.event_name != 'pull_request' }}
|
| 61 |
+
tags: ${{ steps.meta.outputs.tags }}
|
| 62 |
+
labels: ${{ steps.meta.outputs.labels }}
|
| 63 |
+
cache-from: type=gha
|
| 64 |
+
cache-to: type=gha,mode=max
|
.gitignore
CHANGED
|
@@ -57,4 +57,5 @@ browser_data
|
|
| 57 |
|
| 58 |
data
|
| 59 |
config/setting.toml
|
| 60 |
-
config/setting_warp.toml
|
|
|
|
|
|
| 57 |
|
| 58 |
data
|
| 59 |
config/setting.toml
|
| 60 |
+
config/setting_warp.toml
|
| 61 |
+
config/setting_warp_example.toml
|
Dockerfile
CHANGED
|
@@ -2,40 +2,9 @@ FROM python:3.11-slim
|
|
| 2 |
|
| 3 |
WORKDIR /app
|
| 4 |
|
| 5 |
-
#
|
| 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 |
|
|
|
|
| 2 |
|
| 3 |
WORKDIR /app
|
| 4 |
|
| 5 |
+
# 安装 Python 依赖
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
COPY requirements.txt .
|
| 7 |
+
RUN pip install --no-cache-dir --root-user-action=ignore -r requirements.txt
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
COPY . .
|
| 10 |
|
config/setting.toml
CHANGED
|
@@ -12,7 +12,7 @@ max_poll_attempts = 200
|
|
| 12 |
|
| 13 |
[server]
|
| 14 |
host = "0.0.0.0"
|
| 15 |
-
port =
|
| 16 |
|
| 17 |
[debug]
|
| 18 |
enabled = false
|
|
@@ -21,8 +21,8 @@ log_responses = true
|
|
| 21 |
mask_token = true
|
| 22 |
|
| 23 |
[proxy]
|
| 24 |
-
proxy_enabled =
|
| 25 |
-
proxy_url = "
|
| 26 |
|
| 27 |
[generation]
|
| 28 |
image_timeout = 300
|
|
|
|
| 12 |
|
| 13 |
[server]
|
| 14 |
host = "0.0.0.0"
|
| 15 |
+
port = 8000
|
| 16 |
|
| 17 |
[debug]
|
| 18 |
enabled = false
|
|
|
|
| 21 |
mask_token = true
|
| 22 |
|
| 23 |
[proxy]
|
| 24 |
+
proxy_enabled = false
|
| 25 |
+
proxy_url = ""
|
| 26 |
|
| 27 |
[generation]
|
| 28 |
image_timeout = 300
|
config/setting_example.toml
CHANGED
|
@@ -12,7 +12,7 @@ max_poll_attempts = 200
|
|
| 12 |
|
| 13 |
[server]
|
| 14 |
host = "0.0.0.0"
|
| 15 |
-
port =
|
| 16 |
|
| 17 |
[debug]
|
| 18 |
enabled = false
|
|
@@ -21,8 +21,8 @@ log_responses = true
|
|
| 21 |
mask_token = true
|
| 22 |
|
| 23 |
[proxy]
|
| 24 |
-
proxy_enabled =
|
| 25 |
-
proxy_url = "
|
| 26 |
|
| 27 |
[generation]
|
| 28 |
image_timeout = 300
|
|
|
|
| 12 |
|
| 13 |
[server]
|
| 14 |
host = "0.0.0.0"
|
| 15 |
+
port = 8000
|
| 16 |
|
| 17 |
[debug]
|
| 18 |
enabled = false
|
|
|
|
| 21 |
mask_token = true
|
| 22 |
|
| 23 |
[proxy]
|
| 24 |
+
proxy_enabled = false
|
| 25 |
+
proxy_url = ""
|
| 26 |
|
| 27 |
[generation]
|
| 28 |
image_timeout = 300
|
config/setting_warp.toml
DELETED
|
@@ -1,42 +0,0 @@
|
|
| 1 |
-
[global]
|
| 2 |
-
api_key = "han1234"
|
| 3 |
-
admin_username = "admin"
|
| 4 |
-
admin_password = "admin"
|
| 5 |
-
|
| 6 |
-
[flow]
|
| 7 |
-
labs_base_url = "https://labs.google/fx/api"
|
| 8 |
-
api_base_url = "https://aisandbox-pa.googleapis.com/v1"
|
| 9 |
-
timeout = 120
|
| 10 |
-
poll_interval = 3.0
|
| 11 |
-
max_poll_attempts = 200
|
| 12 |
-
|
| 13 |
-
[server]
|
| 14 |
-
host = "0.0.0.0"
|
| 15 |
-
port = 8000
|
| 16 |
-
|
| 17 |
-
[debug]
|
| 18 |
-
enabled = false
|
| 19 |
-
log_requests = true
|
| 20 |
-
log_responses = true
|
| 21 |
-
mask_token = true
|
| 22 |
-
|
| 23 |
-
[proxy]
|
| 24 |
-
proxy_enabled = true
|
| 25 |
-
proxy_url = "socks5://warp:1080"
|
| 26 |
-
|
| 27 |
-
[generation]
|
| 28 |
-
image_timeout = 300
|
| 29 |
-
video_timeout = 1500
|
| 30 |
-
|
| 31 |
-
[admin]
|
| 32 |
-
error_ban_threshold = 3
|
| 33 |
-
|
| 34 |
-
[cache]
|
| 35 |
-
enabled = false
|
| 36 |
-
timeout = 7200 # 缓存超时时间(秒), 默认2小时
|
| 37 |
-
base_url = "" # 缓存文件访问的基础URL, 留空则使用服务器地址
|
| 38 |
-
|
| 39 |
-
[captcha]
|
| 40 |
-
captcha_method = "browser" # 打码方式: yescaptcha 或 browser
|
| 41 |
-
yescaptcha_api_key = "" # YesCaptcha API密钥
|
| 42 |
-
yescaptcha_base_url = "https://api.yescaptcha.com"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
config/setting_warp_example.toml
DELETED
|
@@ -1,42 +0,0 @@
|
|
| 1 |
-
[global]
|
| 2 |
-
api_key = "han1234"
|
| 3 |
-
admin_username = "admin"
|
| 4 |
-
admin_password = "admin"
|
| 5 |
-
|
| 6 |
-
[flow]
|
| 7 |
-
labs_base_url = "https://labs.google/fx/api"
|
| 8 |
-
api_base_url = "https://aisandbox-pa.googleapis.com/v1"
|
| 9 |
-
timeout = 120
|
| 10 |
-
poll_interval = 3.0
|
| 11 |
-
max_poll_attempts = 200
|
| 12 |
-
|
| 13 |
-
[server]
|
| 14 |
-
host = "0.0.0.0"
|
| 15 |
-
port = 8000
|
| 16 |
-
|
| 17 |
-
[debug]
|
| 18 |
-
enabled = false
|
| 19 |
-
log_requests = true
|
| 20 |
-
log_responses = true
|
| 21 |
-
mask_token = true
|
| 22 |
-
|
| 23 |
-
[proxy]
|
| 24 |
-
proxy_enabled = true
|
| 25 |
-
proxy_url = "socks5://warp:1080"
|
| 26 |
-
|
| 27 |
-
[generation]
|
| 28 |
-
image_timeout = 300
|
| 29 |
-
video_timeout = 1500
|
| 30 |
-
|
| 31 |
-
[admin]
|
| 32 |
-
error_ban_threshold = 3
|
| 33 |
-
|
| 34 |
-
[cache]
|
| 35 |
-
enabled = false
|
| 36 |
-
timeout = 7200 # 缓存超时时间(秒), 默认2小时
|
| 37 |
-
base_url = "" # 缓存文件访问的基础URL, 留空则使用服务器地址
|
| 38 |
-
|
| 39 |
-
[captcha]
|
| 40 |
-
captcha_method = "browser" # 打码方式: yescaptcha 或 browser
|
| 41 |
-
yescaptcha_api_key = "" # YesCaptcha API密钥
|
| 42 |
-
yescaptcha_base_url = "https://api.yescaptcha.com"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
request.py
DELETED
|
@@ -1,150 +0,0 @@
|
|
| 1 |
-
import os
|
| 2 |
-
import json
|
| 3 |
-
import re
|
| 4 |
-
import base64
|
| 5 |
-
import aiohttp # Async test. Need to install
|
| 6 |
-
import asyncio
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
# --- 配置区域 ---
|
| 10 |
-
BASE_URL = os.getenv('GEMINI_FLOW2API_URL', 'http://127.0.0.1:8000')
|
| 11 |
-
BACKEND_URL = BASE_URL + "/v1/chat/completions"
|
| 12 |
-
API_KEY = os.getenv('GEMINI_FLOW2API_APIKEY', 'Bearer han1234')
|
| 13 |
-
if API_KEY is None:
|
| 14 |
-
raise ValueError('[gemini flow2api] api key not set')
|
| 15 |
-
MODEL_LANDSCAPE = "gemini-3.0-pro-image-landscape"
|
| 16 |
-
MODEL_PORTRAIT = "gemini-3.0-pro-image-portrait"
|
| 17 |
-
|
| 18 |
-
# 修改: 增加 model 参数,默认为 None
|
| 19 |
-
async def request_backend_generation(
|
| 20 |
-
prompt: str,
|
| 21 |
-
images: list[bytes] = None,
|
| 22 |
-
model: str = None) -> bytes | None:
|
| 23 |
-
"""
|
| 24 |
-
请求后端生成图片。
|
| 25 |
-
:param prompt: 提示词
|
| 26 |
-
:param images: 图片二进制列表
|
| 27 |
-
:param model: 指定模型名称 (可选)
|
| 28 |
-
:return: 成功返回图片bytes,失败返回None
|
| 29 |
-
"""
|
| 30 |
-
# 更新token
|
| 31 |
-
images = images or []
|
| 32 |
-
|
| 33 |
-
# 逻辑: 如果未指定 model,默认使用 Landscape
|
| 34 |
-
use_model = model if model else MODEL_LANDSCAPE
|
| 35 |
-
|
| 36 |
-
# 1. 构造 Payload
|
| 37 |
-
if images:
|
| 38 |
-
content_payload = [{"type": "text", "text": prompt}]
|
| 39 |
-
print(f"[Backend] 正在处理 {len(images)} 张图片输入...")
|
| 40 |
-
for img_bytes in images:
|
| 41 |
-
b64_str = base64.b64encode(img_bytes).decode('utf-8')
|
| 42 |
-
content_payload.append({
|
| 43 |
-
"type": "image_url",
|
| 44 |
-
"image_url": {"url": f"data:image/jpeg;base64,{b64_str}"}
|
| 45 |
-
})
|
| 46 |
-
else:
|
| 47 |
-
content_payload = prompt
|
| 48 |
-
|
| 49 |
-
payload = {
|
| 50 |
-
"model": use_model, # 使用选定的模型
|
| 51 |
-
"messages": [{"role": "user", "content": content_payload}],
|
| 52 |
-
"stream": True
|
| 53 |
-
}
|
| 54 |
-
|
| 55 |
-
headers = {
|
| 56 |
-
"Authorization": API_KEY,
|
| 57 |
-
"Content-Type": "application/json"
|
| 58 |
-
}
|
| 59 |
-
|
| 60 |
-
image_url = None
|
| 61 |
-
print(f"[Backend] Model: {use_model} | 发起请求: {prompt[:20]}...")
|
| 62 |
-
|
| 63 |
-
try:
|
| 64 |
-
async with aiohttp.ClientSession() as session:
|
| 65 |
-
async with session.post(BACKEND_URL, json=payload, headers=headers, timeout=120) as response:
|
| 66 |
-
if response.status != 200:
|
| 67 |
-
err_text = await response.text()
|
| 68 |
-
content = response.content
|
| 69 |
-
print(f"[Backend Error] Status {response.status}: {err_text} {content}")
|
| 70 |
-
raise Exception(f"API Error: {response.status}: {err_text}")
|
| 71 |
-
|
| 72 |
-
async for line in response.content:
|
| 73 |
-
line_str = line.decode('utf-8').strip()
|
| 74 |
-
if line_str.startswith('{"error'):
|
| 75 |
-
chunk = json.loads(data_str)
|
| 76 |
-
delta = chunk.get("choices", [{}])[0].get("delta", {})
|
| 77 |
-
msg = delta['reasoning_content']
|
| 78 |
-
if '401' in msg:
|
| 79 |
-
msg += '\nAccess Token 已失效,需重新配置。'
|
| 80 |
-
elif '400' in msg:
|
| 81 |
-
msg += '\n返回内容被拦截。'
|
| 82 |
-
raise Exception(msg)
|
| 83 |
-
|
| 84 |
-
if not line_str or not line_str.startswith('data: '):
|
| 85 |
-
continue
|
| 86 |
-
|
| 87 |
-
data_str = line_str[6:]
|
| 88 |
-
if data_str == '[DONE]':
|
| 89 |
-
break
|
| 90 |
-
|
| 91 |
-
try:
|
| 92 |
-
chunk = json.loads(data_str)
|
| 93 |
-
delta = chunk.get("choices", [{}])[0].get("delta", {})
|
| 94 |
-
|
| 95 |
-
# 打印思考过程
|
| 96 |
-
if "reasoning_content" in delta:
|
| 97 |
-
print(delta['reasoning_content'], end="", flush=True)
|
| 98 |
-
|
| 99 |
-
# 提取内容中的图片链接
|
| 100 |
-
if "content" in delta:
|
| 101 |
-
content_text = delta["content"]
|
| 102 |
-
img_match = re.search(r'!\[.*?\]\((.*?)\)', content_text)
|
| 103 |
-
if img_match:
|
| 104 |
-
image_url = img_match.group(1)
|
| 105 |
-
print(f"\n[Backend] 捕获图片链接: {image_url}")
|
| 106 |
-
except json.JSONDecodeError:
|
| 107 |
-
continue
|
| 108 |
-
|
| 109 |
-
# 3. 下载生成的图片
|
| 110 |
-
if image_url:
|
| 111 |
-
async with session.get(image_url) as img_resp:
|
| 112 |
-
if img_resp.status == 200:
|
| 113 |
-
image_bytes = await img_resp.read()
|
| 114 |
-
return image_bytes
|
| 115 |
-
else:
|
| 116 |
-
print(f"[Backend Error] 图片下载失败: {img_resp.status}")
|
| 117 |
-
except Exception as e:
|
| 118 |
-
print(f"[Backend Exception] {e}")
|
| 119 |
-
raise e
|
| 120 |
-
|
| 121 |
-
return None
|
| 122 |
-
|
| 123 |
-
if __name__ == '__main__':
|
| 124 |
-
async def main():
|
| 125 |
-
print("=== AI 绘图接口测试 ===")
|
| 126 |
-
user_prompt = input("请输入提示词 (例如 '一只猫'): ").strip()
|
| 127 |
-
if not user_prompt:
|
| 128 |
-
user_prompt = "A cute cat in the garden"
|
| 129 |
-
|
| 130 |
-
print(f"正在请求: {user_prompt}")
|
| 131 |
-
|
| 132 |
-
# 这里的 images 传空列表用于测试文生图
|
| 133 |
-
# 如果想测试图生图,你需要手动读取本地文件:
|
| 134 |
-
# with open("output_test.jpg", "rb") as f: img_data = f.read()
|
| 135 |
-
# result = await request_backend_generation(user_prompt, [img_data])
|
| 136 |
-
|
| 137 |
-
result = await request_backend_generation(user_prompt)
|
| 138 |
-
|
| 139 |
-
if result:
|
| 140 |
-
filename = "output_test.jpg"
|
| 141 |
-
with open(filename, "wb") as f:
|
| 142 |
-
f.write(result)
|
| 143 |
-
print(f"\n[Success] 图片已保存为 {filename},大小: {len(result)} bytes")
|
| 144 |
-
else:
|
| 145 |
-
print("\n[Failed] 生成失败")
|
| 146 |
-
|
| 147 |
-
# 运行测试
|
| 148 |
-
if os.name == 'nt': # Windows 兼容性
|
| 149 |
-
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
| 150 |
-
asyncio.run(main())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/core/database.py
CHANGED
|
@@ -223,7 +223,7 @@ class Database:
|
|
| 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 '
|
| 227 |
browser_proxy_enabled BOOLEAN DEFAULT 0,
|
| 228 |
browser_proxy_url TEXT,
|
| 229 |
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
@@ -509,7 +509,8 @@ class Database:
|
|
| 509 |
capsolver_api_key TEXT DEFAULT '',
|
| 510 |
capsolver_base_url TEXT DEFAULT 'https://api.capsolver.com',
|
| 511 |
website_key TEXT DEFAULT '6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV',
|
| 512 |
-
page_action TEXT DEFAULT '
|
|
|
|
| 513 |
browser_proxy_enabled BOOLEAN DEFAULT 0,
|
| 514 |
browser_proxy_url TEXT,
|
| 515 |
browser_count INTEGER DEFAULT 1,
|
|
|
|
| 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 'IMAGE_GENERATION',
|
| 227 |
browser_proxy_enabled BOOLEAN DEFAULT 0,
|
| 228 |
browser_proxy_url TEXT,
|
| 229 |
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
| 509 |
capsolver_api_key TEXT DEFAULT '',
|
| 510 |
capsolver_base_url TEXT DEFAULT 'https://api.capsolver.com',
|
| 511 |
website_key TEXT DEFAULT '6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV',
|
| 512 |
+
page_action TEXT DEFAULT 'IMAGE_GENERATION',
|
| 513 |
+
|
| 514 |
browser_proxy_enabled BOOLEAN DEFAULT 0,
|
| 515 |
browser_proxy_url TEXT,
|
| 516 |
browser_count INTEGER DEFAULT 1,
|
src/core/models.py
CHANGED
|
@@ -157,7 +157,7 @@ class CaptchaConfig(BaseModel):
|
|
| 157 |
capsolver_api_key: str = ""
|
| 158 |
capsolver_base_url: str = "https://api.capsolver.com"
|
| 159 |
website_key: str = "6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV"
|
| 160 |
-
page_action: str = "
|
| 161 |
browser_proxy_enabled: bool = False # 浏览器打码是否启用代理
|
| 162 |
browser_proxy_url: Optional[str] = None # 浏览器打码代理URL
|
| 163 |
browser_count: int = 1 # 浏览器打码实例数量
|
|
|
|
| 157 |
capsolver_api_key: str = ""
|
| 158 |
capsolver_base_url: str = "https://api.capsolver.com"
|
| 159 |
website_key: str = "6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV"
|
| 160 |
+
page_action: str = "IMAGE_GENERATION"
|
| 161 |
browser_proxy_enabled: bool = False # 浏览器打码是否启用代理
|
| 162 |
browser_proxy_url: Optional[str] = None # 浏览器打码代理URL
|
| 163 |
browser_count: int = 1 # 浏览器打码实例数量
|
src/services/browser_captcha_personal.py
CHANGED
|
@@ -251,11 +251,12 @@ class BrowserCaptchaService:
|
|
| 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
|
|
@@ -272,7 +273,7 @@ class BrowserCaptchaService:
|
|
| 272 |
|
| 273 |
try {{
|
| 274 |
grecaptcha.enterprise.ready(function() {{
|
| 275 |
-
grecaptcha.enterprise.execute('{self.website_key}', {{action: '
|
| 276 |
.then(function(token) {{
|
| 277 |
window.{token_var} = token;
|
| 278 |
}})
|
|
@@ -311,13 +312,16 @@ class BrowserCaptchaService:
|
|
| 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
|
|
@@ -335,16 +339,16 @@ class BrowserCaptchaService:
|
|
| 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)")
|
|
@@ -362,7 +366,7 @@ class BrowserCaptchaService:
|
|
| 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
|
|
@@ -371,7 +375,7 @@ class BrowserCaptchaService:
|
|
| 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 创建常驻标签页
|
|
@@ -449,11 +453,12 @@ class BrowserCaptchaService:
|
|
| 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
|
|
@@ -491,8 +496,8 @@ class BrowserCaptchaService:
|
|
| 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 |
|
|
|
|
| 251 |
debug_logger.log_warning("[BrowserCaptcha] reCAPTCHA 加载超时")
|
| 252 |
return False
|
| 253 |
|
| 254 |
+
async def _execute_recaptcha_on_tab(self, tab, action: str = "IMAGE_GENERATION") -> Optional[str]:
|
| 255 |
"""在指定标签页执行 reCAPTCHA 获取 token
|
| 256 |
|
| 257 |
Args:
|
| 258 |
tab: nodriver 标签页对象
|
| 259 |
+
action: reCAPTCHA action类型 (IMAGE_GENERATION 或 VIDEO_GENERATION)
|
| 260 |
|
| 261 |
Returns:
|
| 262 |
reCAPTCHA token 或 None
|
|
|
|
| 273 |
|
| 274 |
try {{
|
| 275 |
grecaptcha.enterprise.ready(function() {{
|
| 276 |
+
grecaptcha.enterprise.execute('{self.website_key}', {{action: '{action}'}})
|
| 277 |
.then(function(token) {{
|
| 278 |
window.{token_var} = token;
|
| 279 |
}})
|
|
|
|
| 312 |
|
| 313 |
# ========== 主要 API ==========
|
| 314 |
|
| 315 |
+
async def get_token(self, project_id: str, action: str = "IMAGE_GENERATION") -> Optional[str]:
|
| 316 |
"""获取 reCAPTCHA token
|
| 317 |
|
| 318 |
自动常驻模式:如果该 project_id 没有常驻标签页,则自动创建并常驻
|
| 319 |
|
| 320 |
Args:
|
| 321 |
project_id: Flow项目ID
|
| 322 |
+
action: reCAPTCHA action类型
|
| 323 |
+
- IMAGE_GENERATION: 图片生成和2K/4K图片放大 (默认)
|
| 324 |
+
- VIDEO_GENERATION: 视频生成和视频放大
|
| 325 |
|
| 326 |
Returns:
|
| 327 |
reCAPTCHA token字符串,如果获取失败返回None
|
|
|
|
| 339 |
resident_info = await self._create_resident_tab(project_id)
|
| 340 |
if resident_info is None:
|
| 341 |
debug_logger.log_warning(f"[BrowserCaptcha] 无法为 project_id={project_id} 创建常驻标签页,fallback 到传统模式")
|
| 342 |
+
return await self._get_token_legacy(project_id, action)
|
| 343 |
self._resident_tabs[project_id] = resident_info
|
| 344 |
debug_logger.log_info(f"[BrowserCaptcha] ✅ 已为 project_id={project_id} 创建常驻标签页 (当前共 {len(self._resident_tabs)} 个)")
|
| 345 |
|
| 346 |
# 使用常驻标签页生成 token
|
| 347 |
if resident_info and resident_info.recaptcha_ready and resident_info.tab:
|
| 348 |
start_time = time.time()
|
| 349 |
+
debug_logger.log_info(f"[BrowserCaptcha] 从常驻标签页即时生成 token (project: {project_id}, action: {action})...")
|
| 350 |
try:
|
| 351 |
+
token = await self._execute_recaptcha_on_tab(resident_info.tab, action)
|
| 352 |
duration_ms = (time.time() - start_time) * 1000
|
| 353 |
if token:
|
| 354 |
debug_logger.log_info(f"[BrowserCaptcha] ✅ Token生成成功(耗时 {duration_ms:.0f}ms)")
|
|
|
|
| 366 |
self._resident_tabs[project_id] = resident_info
|
| 367 |
# 重建后立即尝试生成
|
| 368 |
try:
|
| 369 |
+
token = await self._execute_recaptcha_on_tab(resident_info.tab, action)
|
| 370 |
if token:
|
| 371 |
debug_logger.log_info(f"[BrowserCaptcha] ✅ 重建后 Token生成成功")
|
| 372 |
return token
|
|
|
|
| 375 |
|
| 376 |
# 最终 Fallback: 使用传统模式
|
| 377 |
debug_logger.log_warning(f"[BrowserCaptcha] 所有常驻方式失败,fallback 到传统模式 (project: {project_id})")
|
| 378 |
+
return await self._get_token_legacy(project_id, action)
|
| 379 |
|
| 380 |
async def _create_resident_tab(self, project_id: str) -> Optional[ResidentTabInfo]:
|
| 381 |
"""为指定 project_id 创建常驻标签页
|
|
|
|
| 453 |
except Exception as e:
|
| 454 |
debug_logger.log_warning(f"[BrowserCaptcha] 关闭标签页时异常: {e}")
|
| 455 |
|
| 456 |
+
async def _get_token_legacy(self, project_id: str, action: str = "IMAGE_GENERATION") -> Optional[str]:
|
| 457 |
"""传统模式获取 reCAPTCHA token(每次创建新标签页)
|
| 458 |
|
| 459 |
Args:
|
| 460 |
project_id: Flow项目ID
|
| 461 |
+
action: reCAPTCHA action类型 (IMAGE_GENERATION 或 VIDEO_GENERATION)
|
| 462 |
|
| 463 |
Returns:
|
| 464 |
reCAPTCHA token字符串,如果获取失败返回None
|
|
|
|
| 496 |
return None
|
| 497 |
|
| 498 |
# 执行 reCAPTCHA
|
| 499 |
+
debug_logger.log_info(f"[BrowserCaptcha] [Legacy] 执行 reCAPTCHA 验证 (action: {action})...")
|
| 500 |
+
token = await self._execute_recaptcha_on_tab(tab, action)
|
| 501 |
|
| 502 |
duration_ms = (time.time() - start_time) * 1000
|
| 503 |
|
src/services/flow_client.py
CHANGED
|
@@ -1177,14 +1177,20 @@ class FlowClient:
|
|
| 1177 |
return None, None
|
| 1178 |
# API打码服务
|
| 1179 |
elif captcha_method in ["yescaptcha", "capmonster", "ezcaptcha", "capsolver"]:
|
| 1180 |
-
token = await self._get_api_captcha_token(captcha_method, project_id)
|
| 1181 |
return token, None
|
| 1182 |
else:
|
| 1183 |
debug_logger.log_info(f"[reCAPTCHA] 未知的打码方式: {captcha_method}")
|
| 1184 |
return None, None
|
| 1185 |
|
| 1186 |
-
async def _get_api_captcha_token(self, method: str, project_id: str) -> Optional[str]:
|
| 1187 |
-
"""通用API打码服务
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1188 |
# 获取配置
|
| 1189 |
if method == "yescaptcha":
|
| 1190 |
client_key = config.yescaptcha_api_key
|
|
@@ -1212,7 +1218,7 @@ class FlowClient:
|
|
| 1212 |
|
| 1213 |
website_key = "6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV"
|
| 1214 |
website_url = f"https://labs.google/fx/tools/flow/project/{project_id}"
|
| 1215 |
-
page_action =
|
| 1216 |
|
| 1217 |
try:
|
| 1218 |
async with AsyncSession() as session:
|
|
|
|
| 1177 |
return None, None
|
| 1178 |
# API打码服务
|
| 1179 |
elif captcha_method in ["yescaptcha", "capmonster", "ezcaptcha", "capsolver"]:
|
| 1180 |
+
token = await self._get_api_captcha_token(captcha_method, project_id, action)
|
| 1181 |
return token, None
|
| 1182 |
else:
|
| 1183 |
debug_logger.log_info(f"[reCAPTCHA] 未知的打码方式: {captcha_method}")
|
| 1184 |
return None, None
|
| 1185 |
|
| 1186 |
+
async def _get_api_captcha_token(self, method: str, project_id: str, action: str = "IMAGE_GENERATION") -> Optional[str]:
|
| 1187 |
+
"""通用API打码服务
|
| 1188 |
+
|
| 1189 |
+
Args:
|
| 1190 |
+
method: 打码服务类型
|
| 1191 |
+
project_id: 项目ID
|
| 1192 |
+
action: reCAPTCHA action类型 (IMAGE_GENERATION 或 VIDEO_GENERATION)
|
| 1193 |
+
"""
|
| 1194 |
# 获取配置
|
| 1195 |
if method == "yescaptcha":
|
| 1196 |
client_key = config.yescaptcha_api_key
|
|
|
|
| 1218 |
|
| 1219 |
website_key = "6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV"
|
| 1220 |
website_url = f"https://labs.google/fx/tools/flow/project/{project_id}"
|
| 1221 |
+
page_action = action
|
| 1222 |
|
| 1223 |
try:
|
| 1224 |
async with AsyncSession() as session:
|