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 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
- # 使用清华镜像源加速 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
 
 
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 = 18282
16
 
17
  [debug]
18
  enabled = false
@@ -21,8 +21,8 @@ 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
 
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 = 18282
16
 
17
  [debug]
18
  enabled = false
@@ -21,8 +21,8 @@ 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
 
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 'FLOW_GENERATION',
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 'FLOW_GENERATION',
 
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 = "FLOW_GENERATION"
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: 'FLOW_GENERATION'}})
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 = "FLOW_GENERATION"
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: