feat: add auto-detection for headless environments and auto-switch captcha modes
Browse files- Add helper function to detect headless/Docker environments in main.py
- Implement auto-downgrade from personal to browser captcha mode in headless environments
- Show warning messages when switching modes automatically
- Update FlowClient to accept database instance for captcha configuration
config: update default server port and enable proxy by default
- Change default server port from 8000 to 18282 in config/setting.toml
- Enable proxy by default with localhost:7897 URL
- Update Docker and docker-compose configurations to use port 38000 for API
build: add China mirror sources for faster dependency installation
- Configure Debian apt to use Tsinghua University mirrors
- Set PyPI to use Tsinghua University index with trusted host
- Configure Playwright to download from npmmirror for faster installation
chore: update gitignore to exclude data and config files
- Add data directory to .gitignore
- Exclude config/setting.toml and config/setting_warp.toml from version control
docs: update docker-compose configurations for new port mappings
- Change exposed port from 8000 to 38000 in docker-compose.yml
- Update proxy service port from 1080 to 31080 in docker-compose.proxy.yml
- Ensure proper volume mapping for Cloudflare WARP data persistence
- .gitignore +4 -0
- Dockerfile +11 -2
- config/setting.toml +3 -3
- config/setting_example.toml +42 -0
- config/setting_warp_example.toml +42 -0
- docker-compose.local.yml +17 -0
- docker-compose.proxy.yml +3 -3
- docker-compose.yml +1 -1
- src/main.py +32 -4
- src/services/flow_client.py +4 -3
|
@@ -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
|
|
@@ -2,6 +2,10 @@ FROM python:3.11-slim
|
|
| 2 |
|
| 3 |
WORKDIR /app
|
| 4 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
# 安装 Playwright 所需的系统依赖
|
| 6 |
RUN apt-get update && apt-get install -y \
|
| 7 |
libnss3 \
|
|
@@ -21,9 +25,14 @@ RUN apt-get update && apt-get install -y \
|
|
| 21 |
libcairo2 \
|
| 22 |
&& rm -rf /var/lib/apt/lists/*
|
| 23 |
|
| 24 |
-
# 安装 Python 依赖
|
| 25 |
COPY requirements.txt .
|
| 26 |
-
RUN pip install --no-cache-dir -r requirements.txt
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
# 安装 Playwright 浏览器
|
| 29 |
RUN playwright install chromium
|
|
|
|
| 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 \
|
|
|
|
| 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
|
|
@@ -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 = 18282
|
| 16 |
|
| 17 |
[debug]
|
| 18 |
enabled = false
|
|
|
|
| 21 |
mask_token = true
|
| 22 |
|
| 23 |
[proxy]
|
| 24 |
+
proxy_enabled = true
|
| 25 |
+
proxy_url = "http://localhost:7897"
|
| 26 |
|
| 27 |
[generation]
|
| 28 |
image_timeout = 300
|
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 = 18282
|
| 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 = "http://localhost:7897"
|
| 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"
|
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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"
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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
|
|
@@ -68,18 +68,46 @@ async def lifespan(app: FastAPI):
|
|
| 68 |
|
| 69 |
# Load captcha configuration from database
|
| 70 |
captcha_config = await db.get_captcha_config()
|
| 71 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 78 |
from .services.browser_captcha_personal import BrowserCaptchaService
|
| 79 |
browser_service = await BrowserCaptchaService.get_instance(db)
|
| 80 |
await browser_service.open_login_window()
|
| 81 |
print("✓ Browser captcha service initialized (webui mode)")
|
| 82 |
-
elif
|
| 83 |
from .services.browser_captcha import BrowserCaptchaService
|
| 84 |
browser_service = await BrowserCaptchaService.get_instance(db)
|
| 85 |
print("✓ Browser captcha service initialized (headless mode)")
|
|
@@ -135,7 +163,7 @@ async def lifespan(app: FastAPI):
|
|
| 135 |
# Initialize components
|
| 136 |
db = Database()
|
| 137 |
proxy_manager = ProxyManager(db)
|
| 138 |
-
flow_client = FlowClient(proxy_manager)
|
| 139 |
token_manager = TokenManager(db, flow_client)
|
| 140 |
concurrency_manager = ConcurrencyManager()
|
| 141 |
load_balancer = LoadBalancer(token_manager, concurrency_manager)
|
|
|
|
| 68 |
|
| 69 |
# Load captcha configuration from database
|
| 70 |
captcha_config = await db.get_captcha_config()
|
| 71 |
+
|
| 72 |
+
# Helper function to detect headless/Docker environment
|
| 73 |
+
def is_headless_environment() -> bool:
|
| 74 |
+
"""Check if running in a headless environment (Docker, no display, etc.)"""
|
| 75 |
+
import os
|
| 76 |
+
# Check for DISPLAY environment variable (X11)
|
| 77 |
+
if not os.environ.get("DISPLAY"):
|
| 78 |
+
# Check if running in Docker
|
| 79 |
+
if os.path.exists("/.dockerenv") or os.environ.get("DOCKER_CONTAINER"):
|
| 80 |
+
return True
|
| 81 |
+
# Check for common CI/container indicators
|
| 82 |
+
if os.environ.get("CI") or os.environ.get("KUBERNETES_SERVICE_HOST"):
|
| 83 |
+
return True
|
| 84 |
+
# No DISPLAY and not explicitly local
|
| 85 |
+
return True
|
| 86 |
+
return False
|
| 87 |
+
|
| 88 |
+
# Determine effective captcha method
|
| 89 |
+
effective_captcha_method = captcha_config.captcha_method
|
| 90 |
+
|
| 91 |
+
# Auto-downgrade personal mode to browser mode in headless environments
|
| 92 |
+
if captcha_config.captcha_method == "personal" and is_headless_environment():
|
| 93 |
+
print("⚠️ WARNING: 'personal' captcha mode requires a display (X Server).")
|
| 94 |
+
print(" Detected headless environment (Docker/No Display).")
|
| 95 |
+
print(" Auto-switching to 'browser' (headless) mode.")
|
| 96 |
+
print(" To use 'personal' mode, run Flow2API on a machine with a display.")
|
| 97 |
+
effective_captcha_method = "browser"
|
| 98 |
+
|
| 99 |
+
config.set_captcha_method(effective_captcha_method)
|
| 100 |
config.set_yescaptcha_api_key(captcha_config.yescaptcha_api_key)
|
| 101 |
config.set_yescaptcha_base_url(captcha_config.yescaptcha_base_url)
|
| 102 |
|
| 103 |
# Initialize browser captcha service if needed
|
| 104 |
browser_service = None
|
| 105 |
+
if effective_captcha_method == "personal":
|
| 106 |
from .services.browser_captcha_personal import BrowserCaptchaService
|
| 107 |
browser_service = await BrowserCaptchaService.get_instance(db)
|
| 108 |
await browser_service.open_login_window()
|
| 109 |
print("✓ Browser captcha service initialized (webui mode)")
|
| 110 |
+
elif effective_captcha_method == "browser":
|
| 111 |
from .services.browser_captcha import BrowserCaptchaService
|
| 112 |
browser_service = await BrowserCaptchaService.get_instance(db)
|
| 113 |
print("✓ Browser captcha service initialized (headless mode)")
|
|
|
|
| 163 |
# Initialize components
|
| 164 |
db = Database()
|
| 165 |
proxy_manager = ProxyManager(db)
|
| 166 |
+
flow_client = FlowClient(proxy_manager, db)
|
| 167 |
token_manager = TokenManager(db, flow_client)
|
| 168 |
concurrency_manager = ConcurrencyManager()
|
| 169 |
load_balancer = LoadBalancer(token_manager, concurrency_manager)
|
|
@@ -12,8 +12,9 @@ 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
|
|
@@ -691,7 +692,7 @@ class FlowClient:
|
|
| 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,7 +701,7 @@ 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)}")
|
|
|
|
| 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
|
|
|
|
| 692 |
if captcha_method == "personal":
|
| 693 |
try:
|
| 694 |
from .browser_captcha_personal import BrowserCaptchaService
|
| 695 |
+
service = await BrowserCaptchaService.get_instance(self.db)
|
| 696 |
return await service.get_token(project_id)
|
| 697 |
except Exception as e:
|
| 698 |
debug_logger.log_error(f"[reCAPTCHA Browser] error: {str(e)}")
|
|
|
|
| 701 |
elif captcha_method == "browser":
|
| 702 |
try:
|
| 703 |
from .browser_captcha import BrowserCaptchaService
|
| 704 |
+
service = await BrowserCaptchaService.get_instance(self.db)
|
| 705 |
return await service.get_token(project_id)
|
| 706 |
except Exception as e:
|
| 707 |
debug_logger.log_error(f"[reCAPTCHA Browser] error: {str(e)}")
|