From c42cf8e33b2c13228ec3659c302eb8ac1ba4dd6f Mon Sep 17 00:00:00 2001 From: genz27 Date: Tue, 27 Jan 2026 17:58:34 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=90=84=E4=B8=AA?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E6=96=87=E4=BB=B6=E4=B8=AD=E5=AF=B9action?= =?UTF-8?q?=E5=8F=82=E6=95=B0=E7=9A=84=E8=B0=83=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将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 --- .github/workflows/docker-publish.yml | 64 ++++++++++ .gitignore | 3 +- Dockerfile | 35 +----- config/setting.toml | 6 +- config/setting_example.toml | 6 +- config/setting_warp.toml | 42 ------- config/setting_warp_example.toml | 42 ------- request.py | 150 ----------------------- src/core/database.py | 5 +- src/core/models.py | 2 +- src/services/browser_captcha_personal.py | 27 ++-- src/services/flow_client.py | 14 ++- 12 files changed, 104 insertions(+), 292 deletions(-) create mode 100644 .github/workflows/docker-publish.yml delete mode 100644 config/setting_warp.toml delete mode 100644 config/setting_warp_example.toml delete mode 100644 request.py diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 0000000..d52e092 --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -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 diff --git a/.gitignore b/.gitignore index 6fa57a0..e7d0eaa 100644 --- a/.gitignore +++ b/.gitignore @@ -57,4 +57,5 @@ browser_data data config/setting.toml -config/setting_warp.toml \ No newline at end of file +config/setting_warp.toml +config/setting_warp_example.toml diff --git a/Dockerfile b/Dockerfile index fc337cc..7a1c809 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 . . diff --git a/config/setting.toml b/config/setting.toml index 3dbd623..051cfd3 100644 --- a/config/setting.toml +++ b/config/setting.toml @@ -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 diff --git a/config/setting_example.toml b/config/setting_example.toml index 3dbd623..051cfd3 100644 --- a/config/setting_example.toml +++ b/config/setting_example.toml @@ -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 diff --git a/config/setting_warp.toml b/config/setting_warp.toml deleted file mode 100644 index ac58190..0000000 --- a/config/setting_warp.toml +++ /dev/null @@ -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" diff --git a/config/setting_warp_example.toml b/config/setting_warp_example.toml deleted file mode 100644 index ac58190..0000000 --- a/config/setting_warp_example.toml +++ /dev/null @@ -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" diff --git a/request.py b/request.py deleted file mode 100644 index 48f73e5..0000000 --- a/request.py +++ /dev/null @@ -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()) \ No newline at end of file diff --git a/src/core/database.py b/src/core/database.py index 9ea0139..9c73993 100644 --- a/src/core/database.py +++ b/src/core/database.py @@ -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, diff --git a/src/core/models.py b/src/core/models.py index 074e74f..b005026 100644 --- a/src/core/models.py +++ b/src/core/models.py @@ -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 # 浏览器打码实例数量 diff --git a/src/services/browser_captcha_personal.py b/src/services/browser_captcha_personal.py index 05da2ae..8cda532 100644 --- a/src/services/browser_captcha_personal.py +++ b/src/services/browser_captcha_personal.py @@ -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 diff --git a/src/services/flow_client.py b/src/services/flow_client.py index 3b8db26..bc095cc 100644 --- a/src/services/flow_client.py +++ b/src/services/flow_client.py @@ -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: