fix: 修复各个代码文件中对action参数的调用

- 将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>
This commit is contained in:
genz27
2026-01-27 17:58:34 +08:00
parent f2d92d1caf
commit c42cf8e33b
12 changed files with 104 additions and 292 deletions

64
.github/workflows/docker-publish.yml vendored Normal file
View File

@@ -0,0 +1,64 @@
name: Build and Push Docker Image
on:
push:
branches:
- main
tags:
- 'v*'
pull_request:
branches:
- main
workflow_dispatch:
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Container Registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels)
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

3
.gitignore vendored
View File

@@ -57,4 +57,5 @@ browser_data
data
config/setting.toml
config/setting_warp.toml
config/setting_warp.toml
config/setting_warp_example.toml

View File

@@ -2,40 +2,9 @@ FROM python:3.11-slim
WORKDIR /app
# 使用清华镜像源加速 apt (Debian bookworm)
RUN sed -i 's|deb.debian.org|mirrors.tuna.tsinghua.edu.cn|g' /etc/apt/sources.list.d/debian.sources \
&& sed -i 's|security.debian.org|mirrors.tuna.tsinghua.edu.cn|g' /etc/apt/sources.list.d/debian.sources
# 安装 Playwright 所需的系统依赖
RUN apt-get update && apt-get install -y \
libnss3 \
libnspr4 \
libatk1.0-0 \
libatk-bridge2.0-0 \
libcups2 \
libdrm2 \
libxkbcommon0 \
libxcomposite1 \
libxdamage1 \
libxfixes3 \
libxrandr2 \
libgbm1 \
libasound2 \
libpango-1.0-0 \
libcairo2 \
&& rm -rf /var/lib/apt/lists/*
# 安装 Python 依赖(使用清华 PyPI 镜像)
# 安装 Python 依赖
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt \
-i https://pypi.tuna.tsinghua.edu.cn/simple/ \
--trusted-host pypi.tuna.tsinghua.edu.cn
# 设置 Playwright 下载镜像(使用 npmmirror
ENV PLAYWRIGHT_DOWNLOAD_HOST=https://registry.npmmirror.com/-/binary/playwright
# 安装 Playwright 浏览器
RUN playwright install chromium
RUN pip install --no-cache-dir --root-user-action=ignore -r requirements.txt
COPY . .

View File

@@ -12,7 +12,7 @@ max_poll_attempts = 200
[server]
host = "0.0.0.0"
port = 18282
port = 8000
[debug]
enabled = false
@@ -21,8 +21,8 @@ log_responses = true
mask_token = true
[proxy]
proxy_enabled = true
proxy_url = "http://localhost:7897"
proxy_enabled = false
proxy_url = ""
[generation]
image_timeout = 300

View File

@@ -12,7 +12,7 @@ max_poll_attempts = 200
[server]
host = "0.0.0.0"
port = 18282
port = 8000
[debug]
enabled = false
@@ -21,8 +21,8 @@ log_responses = true
mask_token = true
[proxy]
proxy_enabled = true
proxy_url = "http://localhost:7897"
proxy_enabled = false
proxy_url = ""
[generation]
image_timeout = 300

View File

@@ -1,42 +0,0 @@
[global]
api_key = "han1234"
admin_username = "admin"
admin_password = "admin"
[flow]
labs_base_url = "https://labs.google/fx/api"
api_base_url = "https://aisandbox-pa.googleapis.com/v1"
timeout = 120
poll_interval = 3.0
max_poll_attempts = 200
[server]
host = "0.0.0.0"
port = 8000
[debug]
enabled = false
log_requests = true
log_responses = true
mask_token = true
[proxy]
proxy_enabled = true
proxy_url = "socks5://warp:1080"
[generation]
image_timeout = 300
video_timeout = 1500
[admin]
error_ban_threshold = 3
[cache]
enabled = false
timeout = 7200 # 缓存超时时间(秒), 默认2小时
base_url = "" # 缓存文件访问的基础URL, 留空则使用服务器地址
[captcha]
captcha_method = "browser" # 打码方式: yescaptcha 或 browser
yescaptcha_api_key = "" # YesCaptcha API密钥
yescaptcha_base_url = "https://api.yescaptcha.com"

View File

@@ -1,42 +0,0 @@
[global]
api_key = "han1234"
admin_username = "admin"
admin_password = "admin"
[flow]
labs_base_url = "https://labs.google/fx/api"
api_base_url = "https://aisandbox-pa.googleapis.com/v1"
timeout = 120
poll_interval = 3.0
max_poll_attempts = 200
[server]
host = "0.0.0.0"
port = 8000
[debug]
enabled = false
log_requests = true
log_responses = true
mask_token = true
[proxy]
proxy_enabled = true
proxy_url = "socks5://warp:1080"
[generation]
image_timeout = 300
video_timeout = 1500
[admin]
error_ban_threshold = 3
[cache]
enabled = false
timeout = 7200 # 缓存超时时间(秒), 默认2小时
base_url = "" # 缓存文件访问的基础URL, 留空则使用服务器地址
[captcha]
captcha_method = "browser" # 打码方式: yescaptcha 或 browser
yescaptcha_api_key = "" # YesCaptcha API密钥
yescaptcha_base_url = "https://api.yescaptcha.com"

View File

@@ -1,150 +0,0 @@
import os
import json
import re
import base64
import aiohttp # Async test. Need to install
import asyncio
# --- 配置区域 ---
BASE_URL = os.getenv('GEMINI_FLOW2API_URL', 'http://127.0.0.1:8000')
BACKEND_URL = BASE_URL + "/v1/chat/completions"
API_KEY = os.getenv('GEMINI_FLOW2API_APIKEY', 'Bearer han1234')
if API_KEY is None:
raise ValueError('[gemini flow2api] api key not set')
MODEL_LANDSCAPE = "gemini-3.0-pro-image-landscape"
MODEL_PORTRAIT = "gemini-3.0-pro-image-portrait"
# 修改: 增加 model 参数,默认为 None
async def request_backend_generation(
prompt: str,
images: list[bytes] = None,
model: str = None) -> bytes | None:
"""
请求后端生成图片。
:param prompt: 提示词
:param images: 图片二进制列表
:param model: 指定模型名称 (可选)
:return: 成功返回图片bytes失败返回None
"""
# 更新token
images = images or []
# 逻辑: 如果未指定 model默认使用 Landscape
use_model = model if model else MODEL_LANDSCAPE
# 1. 构造 Payload
if images:
content_payload = [{"type": "text", "text": prompt}]
print(f"[Backend] 正在处理 {len(images)} 张图片输入...")
for img_bytes in images:
b64_str = base64.b64encode(img_bytes).decode('utf-8')
content_payload.append({
"type": "image_url",
"image_url": {"url": f"data:image/jpeg;base64,{b64_str}"}
})
else:
content_payload = prompt
payload = {
"model": use_model, # 使用选定的模型
"messages": [{"role": "user", "content": content_payload}],
"stream": True
}
headers = {
"Authorization": API_KEY,
"Content-Type": "application/json"
}
image_url = None
print(f"[Backend] Model: {use_model} | 发起请求: {prompt[:20]}...")
try:
async with aiohttp.ClientSession() as session:
async with session.post(BACKEND_URL, json=payload, headers=headers, timeout=120) as response:
if response.status != 200:
err_text = await response.text()
content = response.content
print(f"[Backend Error] Status {response.status}: {err_text} {content}")
raise Exception(f"API Error: {response.status}: {err_text}")
async for line in response.content:
line_str = line.decode('utf-8').strip()
if line_str.startswith('{"error'):
chunk = json.loads(data_str)
delta = chunk.get("choices", [{}])[0].get("delta", {})
msg = delta['reasoning_content']
if '401' in msg:
msg += '\nAccess Token 已失效,需重新配置。'
elif '400' in msg:
msg += '\n返回内容被拦截。'
raise Exception(msg)
if not line_str or not line_str.startswith('data: '):
continue
data_str = line_str[6:]
if data_str == '[DONE]':
break
try:
chunk = json.loads(data_str)
delta = chunk.get("choices", [{}])[0].get("delta", {})
# 打印思考过程
if "reasoning_content" in delta:
print(delta['reasoning_content'], end="", flush=True)
# 提取内容中的图片链接
if "content" in delta:
content_text = delta["content"]
img_match = re.search(r'!\[.*?\]\((.*?)\)', content_text)
if img_match:
image_url = img_match.group(1)
print(f"\n[Backend] 捕获图片链接: {image_url}")
except json.JSONDecodeError:
continue
# 3. 下载生成的图片
if image_url:
async with session.get(image_url) as img_resp:
if img_resp.status == 200:
image_bytes = await img_resp.read()
return image_bytes
else:
print(f"[Backend Error] 图片下载失败: {img_resp.status}")
except Exception as e:
print(f"[Backend Exception] {e}")
raise e
return None
if __name__ == '__main__':
async def main():
print("=== AI 绘图接口测试 ===")
user_prompt = input("请输入提示词 (例如 '一只猫'): ").strip()
if not user_prompt:
user_prompt = "A cute cat in the garden"
print(f"正在请求: {user_prompt}")
# 这里的 images 传空列表用于测试文生图
# 如果想测试图生图,你需要手动读取本地文件:
# with open("output_test.jpg", "rb") as f: img_data = f.read()
# result = await request_backend_generation(user_prompt, [img_data])
result = await request_backend_generation(user_prompt)
if result:
filename = "output_test.jpg"
with open(filename, "wb") as f:
f.write(result)
print(f"\n[Success] 图片已保存为 {filename},大小: {len(result)} bytes")
else:
print("\n[Failed] 生成失败")
# 运行测试
if os.name == 'nt': # Windows 兼容性
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
asyncio.run(main())

View File

@@ -223,7 +223,7 @@ class Database:
capsolver_api_key TEXT DEFAULT '',
capsolver_base_url TEXT DEFAULT 'https://api.capsolver.com',
website_key TEXT DEFAULT '6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV',
page_action TEXT DEFAULT 'FLOW_GENERATION',
page_action TEXT DEFAULT 'IMAGE_GENERATION',
browser_proxy_enabled BOOLEAN DEFAULT 0,
browser_proxy_url TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
@@ -509,7 +509,8 @@ class Database:
capsolver_api_key TEXT DEFAULT '',
capsolver_base_url TEXT DEFAULT 'https://api.capsolver.com',
website_key TEXT DEFAULT '6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV',
page_action TEXT DEFAULT 'FLOW_GENERATION',
page_action TEXT DEFAULT 'IMAGE_GENERATION',
browser_proxy_enabled BOOLEAN DEFAULT 0,
browser_proxy_url TEXT,
browser_count INTEGER DEFAULT 1,

View File

@@ -157,7 +157,7 @@ class CaptchaConfig(BaseModel):
capsolver_api_key: str = ""
capsolver_base_url: str = "https://api.capsolver.com"
website_key: str = "6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV"
page_action: str = "FLOW_GENERATION"
page_action: str = "IMAGE_GENERATION"
browser_proxy_enabled: bool = False # 浏览器打码是否启用代理
browser_proxy_url: Optional[str] = None # 浏览器打码代理URL
browser_count: int = 1 # 浏览器打码实例数量

View File

@@ -251,11 +251,12 @@ class BrowserCaptchaService:
debug_logger.log_warning("[BrowserCaptcha] reCAPTCHA 加载超时")
return False
async def _execute_recaptcha_on_tab(self, tab) -> Optional[str]:
async def _execute_recaptcha_on_tab(self, tab, action: str = "IMAGE_GENERATION") -> Optional[str]:
"""在指定标签页执行 reCAPTCHA 获取 token
Args:
tab: nodriver 标签页对象
action: reCAPTCHA action类型 (IMAGE_GENERATION 或 VIDEO_GENERATION)
Returns:
reCAPTCHA token 或 None
@@ -272,7 +273,7 @@ class BrowserCaptchaService:
try {{
grecaptcha.enterprise.ready(function() {{
grecaptcha.enterprise.execute('{self.website_key}', {{action: 'FLOW_GENERATION'}})
grecaptcha.enterprise.execute('{self.website_key}', {{action: '{action}'}})
.then(function(token) {{
window.{token_var} = token;
}})
@@ -311,13 +312,16 @@ class BrowserCaptchaService:
# ========== 主要 API ==========
async def get_token(self, project_id: str) -> Optional[str]:
async def get_token(self, project_id: str, action: str = "IMAGE_GENERATION") -> Optional[str]:
"""获取 reCAPTCHA token
自动常驻模式:如果该 project_id 没有常驻标签页,则自动创建并常驻
Args:
project_id: Flow项目ID
action: reCAPTCHA action类型
- IMAGE_GENERATION: 图片生成和2K/4K图片放大 (默认)
- VIDEO_GENERATION: 视频生成和视频放大
Returns:
reCAPTCHA token字符串如果获取失败返回None
@@ -335,16 +339,16 @@ class BrowserCaptchaService:
resident_info = await self._create_resident_tab(project_id)
if resident_info is None:
debug_logger.log_warning(f"[BrowserCaptcha] 无法为 project_id={project_id} 创建常驻标签页fallback 到传统模式")
return await self._get_token_legacy(project_id)
return await self._get_token_legacy(project_id, action)
self._resident_tabs[project_id] = resident_info
debug_logger.log_info(f"[BrowserCaptcha] ✅ 已为 project_id={project_id} 创建常驻标签页 (当前共 {len(self._resident_tabs)} 个)")
# 使用常驻标签页生成 token
if resident_info and resident_info.recaptcha_ready and resident_info.tab:
start_time = time.time()
debug_logger.log_info(f"[BrowserCaptcha] 从常驻标签页即时生成 token (project: {project_id})...")
debug_logger.log_info(f"[BrowserCaptcha] 从常驻标签页即时生成 token (project: {project_id}, action: {action})...")
try:
token = await self._execute_recaptcha_on_tab(resident_info.tab)
token = await self._execute_recaptcha_on_tab(resident_info.tab, action)
duration_ms = (time.time() - start_time) * 1000
if token:
debug_logger.log_info(f"[BrowserCaptcha] ✅ Token生成成功耗时 {duration_ms:.0f}ms")
@@ -362,7 +366,7 @@ class BrowserCaptchaService:
self._resident_tabs[project_id] = resident_info
# 重建后立即尝试生成
try:
token = await self._execute_recaptcha_on_tab(resident_info.tab)
token = await self._execute_recaptcha_on_tab(resident_info.tab, action)
if token:
debug_logger.log_info(f"[BrowserCaptcha] ✅ 重建后 Token生成成功")
return token
@@ -371,7 +375,7 @@ class BrowserCaptchaService:
# 最终 Fallback: 使用传统模式
debug_logger.log_warning(f"[BrowserCaptcha] 所有常驻方式失败fallback 到传统模式 (project: {project_id})")
return await self._get_token_legacy(project_id)
return await self._get_token_legacy(project_id, action)
async def _create_resident_tab(self, project_id: str) -> Optional[ResidentTabInfo]:
"""为指定 project_id 创建常驻标签页
@@ -449,11 +453,12 @@ class BrowserCaptchaService:
except Exception as e:
debug_logger.log_warning(f"[BrowserCaptcha] 关闭标签页时异常: {e}")
async def _get_token_legacy(self, project_id: str) -> Optional[str]:
async def _get_token_legacy(self, project_id: str, action: str = "IMAGE_GENERATION") -> Optional[str]:
"""传统模式获取 reCAPTCHA token每次创建新标签页
Args:
project_id: Flow项目ID
action: reCAPTCHA action类型 (IMAGE_GENERATION 或 VIDEO_GENERATION)
Returns:
reCAPTCHA token字符串如果获取失败返回None
@@ -491,8 +496,8 @@ class BrowserCaptchaService:
return None
# 执行 reCAPTCHA
debug_logger.log_info("[BrowserCaptcha] [Legacy] 执行 reCAPTCHA 验证...")
token = await self._execute_recaptcha_on_tab(tab)
debug_logger.log_info(f"[BrowserCaptcha] [Legacy] 执行 reCAPTCHA 验证 (action: {action})...")
token = await self._execute_recaptcha_on_tab(tab, action)
duration_ms = (time.time() - start_time) * 1000

View File

@@ -1177,14 +1177,20 @@ class FlowClient:
return None, None
# API打码服务
elif captcha_method in ["yescaptcha", "capmonster", "ezcaptcha", "capsolver"]:
token = await self._get_api_captcha_token(captcha_method, project_id)
token = await self._get_api_captcha_token(captcha_method, project_id, action)
return token, None
else:
debug_logger.log_info(f"[reCAPTCHA] 未知的打码方式: {captcha_method}")
return None, None
async def _get_api_captcha_token(self, method: str, project_id: str) -> Optional[str]:
"""通用API打码服务"""
async def _get_api_captcha_token(self, method: str, project_id: str, action: str = "IMAGE_GENERATION") -> Optional[str]:
"""通用API打码服务
Args:
method: 打码服务类型
project_id: 项目ID
action: reCAPTCHA action类型 (IMAGE_GENERATION 或 VIDEO_GENERATION)
"""
# 获取配置
if method == "yescaptcha":
client_key = config.yescaptcha_api_key
@@ -1212,7 +1218,7 @@ class FlowClient:
website_key = "6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV"
website_url = f"https://labs.google/fx/tools/flow/project/{project_id}"
page_action = "FLOW_GENERATION"
page_action = action
try:
async with AsyncSession() as session: