TheSmallHanCat commited on
Commit
de31fe0
·
1 Parent(s): ea88387

feat: 无头打码与YesCaptcha打码

Browse files
Dockerfile CHANGED
@@ -2,9 +2,32 @@ FROM python:3.11-slim
2
 
3
  WORKDIR /app
4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  COPY requirements.txt .
6
  RUN pip install --no-cache-dir -r requirements.txt
7
 
 
 
 
8
  COPY . .
9
 
10
  EXPOSE 8000
 
2
 
3
  WORKDIR /app
4
 
5
+ # 安装 Playwright 所需的系统依赖
6
+ RUN apt-get update && apt-get install -y \
7
+ libnss3 \
8
+ libnspr4 \
9
+ libatk1.0-0 \
10
+ libatk-bridge2.0-0 \
11
+ libcups2 \
12
+ libdrm2 \
13
+ libxkbcommon0 \
14
+ libxcomposite1 \
15
+ libxdamage1 \
16
+ libxfixes3 \
17
+ libxrandr2 \
18
+ libgbm1 \
19
+ libasound2 \
20
+ libpango-1.0-0 \
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 浏览器(仅 Chromium)
29
+ RUN playwright install chromium --with-deps
30
+
31
  COPY . .
32
 
33
  EXPOSE 8000
README.md CHANGED
@@ -21,6 +21,7 @@
21
  - 🚀 **负载均衡** - 多 Token 轮询和并发控制
22
  - 🌐 **代理支持** - 支持 HTTP/SOCKS5 代理
23
  - 📱 **Web 管理界面** - 直观的 Token 和配置管理
 
24
 
25
  ## 🚀 快速开始
26
 
@@ -29,6 +30,9 @@
29
  - Docker 和 Docker Compose(推荐)
30
  - 或 Python 3.8+
31
 
 
 
 
32
  ### 方式一:Docker 部署(推荐)
33
 
34
  #### 标准模式(不使用代理)
@@ -80,13 +84,11 @@ python main.py
80
 
81
  ### 首次访问
82
 
83
- 服务启动后,访问管理后台: **http://localhost:8000**
84
 
85
  - **用户名**: `admin`
86
  - **密码**: `admin`
87
 
88
- ⚠️ **重要**: 首次登录后请立即修改密码!
89
-
90
  ## 📋 支持的模型
91
 
92
  ### 图片生成
@@ -246,6 +248,8 @@ curl -X POST "http://localhost:8000/v1/chat/completions" \
246
 
247
  ## 🙏 致谢
248
 
 
 
249
  感谢所有贡献者和使用者的支持!
250
 
251
  ---
@@ -253,7 +257,6 @@ curl -X POST "http://localhost:8000/v1/chat/completions" \
253
  ## 📞 联系方式
254
 
255
  - 提交 Issue:[GitHub Issues](https://github.com/TheSmallHanCat/flow2api/issues)
256
- - 讨论:[GitHub Discussions](https://github.com/TheSmallHanCat/flow2api/discussions)
257
 
258
  ---
259
 
 
21
  - 🚀 **负载均衡** - 多 Token 轮询和并发控制
22
  - 🌐 **代理支持** - 支持 HTTP/SOCKS5 代理
23
  - 📱 **Web 管理界面** - 直观的 Token 和配置管理
24
+ - 🎨 **图片生成连续对话**
25
 
26
  ## 🚀 快速开始
27
 
 
30
  - Docker 和 Docker Compose(推荐)
31
  - 或 Python 3.8+
32
 
33
+ - 由于Flow增加了额外的验证码,你可以自行选择使用浏览器打码或第三发打码:
34
+ 注册[YesCaptcha](https://yescaptcha.com/i/13Xd8K)并获取api key,将其填入系统配置页面```YesCaptcha API密钥```区域
35
+
36
  ### 方式一:Docker 部署(推荐)
37
 
38
  #### 标准模式(不使用代理)
 
84
 
85
  ### 首次访问
86
 
87
+ 服务启动后,访问管理后台: **http://localhost:8000**,首次登录后请立即修改密码!
88
 
89
  - **用户名**: `admin`
90
  - **密码**: `admin`
91
 
 
 
92
  ## 📋 支持的模型
93
 
94
  ### 图片生成
 
248
 
249
  ## 🙏 致谢
250
 
251
+ - [PearNoDec](https://github.com/PearNoDec) 提供的YesCaptcha打码方案
252
+ - [raomaiping](https://github.com/raomaiping) 提供的无头打码方案
253
  感谢所有贡献者和使用者的支持!
254
 
255
  ---
 
257
  ## 📞 联系方式
258
 
259
  - 提交 Issue:[GitHub Issues](https://github.com/TheSmallHanCat/flow2api/issues)
 
260
 
261
  ---
262
 
config/setting.toml CHANGED
@@ -35,3 +35,8 @@ error_ban_threshold = 3
35
  enabled = false
36
  timeout = 7200 # 缓存超时时间(秒), 默认2小时
37
  base_url = "" # 缓存文件访问的基础URL, 留空则使用服务器地址
 
 
 
 
 
 
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.toml CHANGED
@@ -35,3 +35,8 @@ error_ban_threshold = 3
35
  enabled = false
36
  timeout = 7200 # 缓存超时时间(秒), 默认2小时
37
  base_url = "" # 缓存文件访问的基础URL, 留空则使用服务器地址
 
 
 
 
 
 
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"
requirements.txt CHANGED
@@ -2,8 +2,9 @@ fastapi==0.119.0
2
  uvicorn[standard]==0.32.1
3
  aiosqlite==0.20.0
4
  pydantic==2.10.4
5
- curl-cffi
6
  tomli==2.2.1
7
  bcrypt==4.2.1
8
  python-multipart==0.0.20
9
  python-dateutil==2.8.2
 
 
2
  uvicorn[standard]==0.32.1
3
  aiosqlite==0.20.0
4
  pydantic==2.10.4
5
+ curl-cffi==0.7.3
6
  tomli==2.2.1
7
  bcrypt==4.2.1
8
  python-multipart==0.0.20
9
  python-dateutil==2.8.2
10
+ playwright==1.48.0
src/api/admin.py CHANGED
@@ -839,10 +839,12 @@ async def update_captcha_config(
839
  token: str = Depends(verify_admin_token)
840
  ):
841
  """Update captcha configuration"""
 
842
  yescaptcha_api_key = request.get("yescaptcha_api_key")
843
  yescaptcha_base_url = request.get("yescaptcha_base_url")
844
 
845
  await db.update_captcha_config(
 
846
  yescaptcha_api_key=yescaptcha_api_key,
847
  yescaptcha_base_url=yescaptcha_base_url
848
  )
@@ -858,6 +860,7 @@ async def get_captcha_config(token: str = Depends(verify_admin_token)):
858
  """Get captcha configuration"""
859
  captcha_config = await db.get_captcha_config()
860
  return {
 
861
  "yescaptcha_api_key": captcha_config.yescaptcha_api_key,
862
  "yescaptcha_base_url": captcha_config.yescaptcha_base_url
863
  }
 
839
  token: str = Depends(verify_admin_token)
840
  ):
841
  """Update captcha configuration"""
842
+ captcha_method = request.get("captcha_method")
843
  yescaptcha_api_key = request.get("yescaptcha_api_key")
844
  yescaptcha_base_url = request.get("yescaptcha_base_url")
845
 
846
  await db.update_captcha_config(
847
+ captcha_method=captcha_method,
848
  yescaptcha_api_key=yescaptcha_api_key,
849
  yescaptcha_base_url=yescaptcha_base_url
850
  )
 
860
  """Get captcha configuration"""
861
  captcha_config = await db.get_captcha_config()
862
  return {
863
+ "captcha_method": captcha_config.captcha_method,
864
  "yescaptcha_api_key": captcha_config.yescaptcha_api_key,
865
  "yescaptcha_base_url": captcha_config.yescaptcha_base_url
866
  }
src/core/config.py CHANGED
@@ -179,5 +179,40 @@ class Config:
179
  self._config["cache"] = {}
180
  self._config["cache"]["base_url"] = base_url
181
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
  # Global config instance
183
  config = Config()
 
179
  self._config["cache"] = {}
180
  self._config["cache"]["base_url"] = base_url
181
 
182
+ # Captcha configuration
183
+ @property
184
+ def captcha_method(self) -> str:
185
+ """Get captcha method"""
186
+ return self._config.get("captcha", {}).get("captcha_method", "yescaptcha")
187
+
188
+ def set_captcha_method(self, method: str):
189
+ """Set captcha method"""
190
+ if "captcha" not in self._config:
191
+ self._config["captcha"] = {}
192
+ self._config["captcha"]["captcha_method"] = method
193
+
194
+ @property
195
+ def yescaptcha_api_key(self) -> str:
196
+ """Get YesCaptcha API key"""
197
+ return self._config.get("captcha", {}).get("yescaptcha_api_key", "")
198
+
199
+ def set_yescaptcha_api_key(self, api_key: str):
200
+ """Set YesCaptcha API key"""
201
+ if "captcha" not in self._config:
202
+ self._config["captcha"] = {}
203
+ self._config["captcha"]["yescaptcha_api_key"] = api_key
204
+
205
+ @property
206
+ def yescaptcha_base_url(self) -> str:
207
+ """Get YesCaptcha base URL"""
208
+ return self._config.get("captcha", {}).get("yescaptcha_base_url", "https://api.yescaptcha.com")
209
+
210
+ def set_yescaptcha_base_url(self, base_url: str):
211
+ """Set YesCaptcha base URL"""
212
+ if "captcha" not in self._config:
213
+ self._config["captcha"] = {}
214
+ self._config["captcha"]["yescaptcha_base_url"] = base_url
215
+
216
+
217
  # Global config instance
218
  config = Config()
src/core/database.py CHANGED
@@ -4,7 +4,7 @@ import json
4
  from datetime import datetime
5
  from typing import Optional, List
6
  from pathlib import Path
7
- from .models import Token, TokenStats, Task, RequestLog, AdminConfig, ProxyConfig, GenerationConfig, CacheConfig, Project
8
 
9
 
10
  class Database:
@@ -148,6 +148,25 @@ class Database:
148
  VALUES (1, ?, ?, ?, ?)
149
  """, (debug_enabled, log_requests, log_responses, mask_token))
150
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  async def check_and_migrate_db(self, config_dict: dict = None):
152
  """Check database integrity and perform migrations if needed
153
 
@@ -179,6 +198,22 @@ class Database:
179
  )
180
  """)
181
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
  # ========== Step 2: Add missing columns to existing tables ==========
183
  # Check and add missing columns to tokens table
184
  if await self._table_exists(db, "tokens"):
@@ -234,8 +269,8 @@ class Database:
234
 
235
  # ========== Step 3: Ensure all config tables have default rows ==========
236
  # Note: This will NOT overwrite existing config rows
237
- # It only ensures missing rows are created with default values
238
- await self._ensure_config_rows(db, config_dict=None)
239
 
240
  await db.commit()
241
  print("Database migration check completed.")
@@ -395,6 +430,20 @@ class Database:
395
  )
396
  """)
397
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
398
  # Create indexes
399
  await db.execute("CREATE INDEX IF NOT EXISTS idx_task_id ON tasks(task_id)")
400
  await db.execute("CREATE INDEX IF NOT EXISTS idx_token_st ON tokens(st)")
@@ -944,6 +993,13 @@ class Database:
944
  if debug_config:
945
  config.set_debug_enabled(debug_config.enabled)
946
 
 
 
 
 
 
 
 
947
  # Cache config operations
948
  async def get_cache_config(self) -> CacheConfig:
949
  """Get cache configuration"""
@@ -1046,3 +1102,49 @@ class Database:
1046
  """, (new_enabled, new_log_requests, new_log_responses, new_mask_token))
1047
 
1048
  await db.commit()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  from datetime import datetime
5
  from typing import Optional, List
6
  from pathlib import Path
7
+ from .models import Token, TokenStats, Task, RequestLog, AdminConfig, ProxyConfig, GenerationConfig, CacheConfig, Project, CaptchaConfig
8
 
9
 
10
  class Database:
 
148
  VALUES (1, ?, ?, ?, ?)
149
  """, (debug_enabled, log_requests, log_responses, mask_token))
150
 
151
+ # Ensure captcha_config has a row
152
+ cursor = await db.execute("SELECT COUNT(*) FROM captcha_config")
153
+ count = await cursor.fetchone()
154
+ if count[0] == 0:
155
+ captcha_method = "browser"
156
+ yescaptcha_api_key = ""
157
+ yescaptcha_base_url = "https://api.yescaptcha.com"
158
+
159
+ if config_dict:
160
+ captcha_config = config_dict.get("captcha", {})
161
+ captcha_method = captcha_config.get("captcha_method", "browser")
162
+ yescaptcha_api_key = captcha_config.get("yescaptcha_api_key", "")
163
+ yescaptcha_base_url = captcha_config.get("yescaptcha_base_url", "https://api.yescaptcha.com")
164
+
165
+ await db.execute("""
166
+ INSERT INTO captcha_config (id, captcha_method, yescaptcha_api_key, yescaptcha_base_url)
167
+ VALUES (1, ?, ?, ?)
168
+ """, (captcha_method, yescaptcha_api_key, yescaptcha_base_url))
169
+
170
  async def check_and_migrate_db(self, config_dict: dict = None):
171
  """Check database integrity and perform migrations if needed
172
 
 
198
  )
199
  """)
200
 
201
+ # Check and create captcha_config table if missing
202
+ if not await self._table_exists(db, "captcha_config"):
203
+ print(" ✓ Creating missing table: captcha_config")
204
+ await db.execute("""
205
+ CREATE TABLE captcha_config (
206
+ id INTEGER PRIMARY KEY DEFAULT 1,
207
+ captcha_method TEXT DEFAULT 'browser',
208
+ yescaptcha_api_key TEXT DEFAULT '',
209
+ yescaptcha_base_url TEXT DEFAULT 'https://api.yescaptcha.com',
210
+ website_key TEXT DEFAULT '6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV',
211
+ page_action TEXT DEFAULT 'FLOW_GENERATION',
212
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
213
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
214
+ )
215
+ """)
216
+
217
  # ========== Step 2: Add missing columns to existing tables ==========
218
  # Check and add missing columns to tokens table
219
  if await self._table_exists(db, "tokens"):
 
269
 
270
  # ========== Step 3: Ensure all config tables have default rows ==========
271
  # Note: This will NOT overwrite existing config rows
272
+ # It only ensures missing rows are created with default values from setting.toml
273
+ await self._ensure_config_rows(db, config_dict=config_dict)
274
 
275
  await db.commit()
276
  print("Database migration check completed.")
 
430
  )
431
  """)
432
 
433
+ # Captcha config table
434
+ await db.execute("""
435
+ CREATE TABLE IF NOT EXISTS captcha_config (
436
+ id INTEGER PRIMARY KEY DEFAULT 1,
437
+ captcha_method TEXT DEFAULT 'browser',
438
+ yescaptcha_api_key TEXT DEFAULT '',
439
+ yescaptcha_base_url TEXT DEFAULT 'https://api.yescaptcha.com',
440
+ website_key TEXT DEFAULT '6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV',
441
+ page_action TEXT DEFAULT 'FLOW_GENERATION',
442
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
443
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
444
+ )
445
+ """)
446
+
447
  # Create indexes
448
  await db.execute("CREATE INDEX IF NOT EXISTS idx_task_id ON tasks(task_id)")
449
  await db.execute("CREATE INDEX IF NOT EXISTS idx_token_st ON tokens(st)")
 
993
  if debug_config:
994
  config.set_debug_enabled(debug_config.enabled)
995
 
996
+ # Reload captcha config
997
+ captcha_config = await self.get_captcha_config()
998
+ if captcha_config:
999
+ config.set_captcha_method(captcha_config.captcha_method)
1000
+ config.set_yescaptcha_api_key(captcha_config.yescaptcha_api_key)
1001
+ config.set_yescaptcha_base_url(captcha_config.yescaptcha_base_url)
1002
+
1003
  # Cache config operations
1004
  async def get_cache_config(self) -> CacheConfig:
1005
  """Get cache configuration"""
 
1102
  """, (new_enabled, new_log_requests, new_log_responses, new_mask_token))
1103
 
1104
  await db.commit()
1105
+
1106
+ # Captcha config operations
1107
+ async def get_captcha_config(self) -> CaptchaConfig:
1108
+ """Get captcha configuration"""
1109
+ async with aiosqlite.connect(self.db_path) as db:
1110
+ db.row_factory = aiosqlite.Row
1111
+ cursor = await db.execute("SELECT * FROM captcha_config WHERE id = 1")
1112
+ row = await cursor.fetchone()
1113
+ if row:
1114
+ return CaptchaConfig(**dict(row))
1115
+ return CaptchaConfig()
1116
+
1117
+ async def update_captcha_config(
1118
+ self,
1119
+ captcha_method: str = None,
1120
+ yescaptcha_api_key: str = None,
1121
+ yescaptcha_base_url: str = None
1122
+ ):
1123
+ """Update captcha configuration"""
1124
+ async with aiosqlite.connect(self.db_path) as db:
1125
+ db.row_factory = aiosqlite.Row
1126
+ cursor = await db.execute("SELECT * FROM captcha_config WHERE id = 1")
1127
+ row = await cursor.fetchone()
1128
+
1129
+ if row:
1130
+ current = dict(row)
1131
+ new_method = captcha_method if captcha_method is not None else current.get("captcha_method", "yescaptcha")
1132
+ new_api_key = yescaptcha_api_key if yescaptcha_api_key is not None else current.get("yescaptcha_api_key", "")
1133
+ new_base_url = yescaptcha_base_url if yescaptcha_base_url is not None else current.get("yescaptcha_base_url", "https://api.yescaptcha.com")
1134
+
1135
+ await db.execute("""
1136
+ UPDATE captcha_config
1137
+ SET captcha_method = ?, yescaptcha_api_key = ?, yescaptcha_base_url = ?, updated_at = CURRENT_TIMESTAMP
1138
+ WHERE id = 1
1139
+ """, (new_method, new_api_key, new_base_url))
1140
+ else:
1141
+ new_method = captcha_method if captcha_method is not None else "yescaptcha"
1142
+ new_api_key = yescaptcha_api_key if yescaptcha_api_key is not None else ""
1143
+ new_base_url = yescaptcha_base_url if yescaptcha_base_url is not None else "https://api.yescaptcha.com"
1144
+
1145
+ await db.execute("""
1146
+ INSERT INTO captcha_config (id, captcha_method, yescaptcha_api_key, yescaptcha_base_url)
1147
+ VALUES (1, ?, ?, ?)
1148
+ """, (new_method, new_api_key, new_base_url))
1149
+
1150
+ await db.commit()
src/core/models.py CHANGED
@@ -144,6 +144,18 @@ class DebugConfig(BaseModel):
144
  updated_at: Optional[datetime] = None
145
 
146
 
 
 
 
 
 
 
 
 
 
 
 
 
147
  # OpenAI Compatible Request Models
148
  class ChatMessage(BaseModel):
149
  """Chat message"""
 
144
  updated_at: Optional[datetime] = None
145
 
146
 
147
+ class CaptchaConfig(BaseModel):
148
+ """Captcha configuration"""
149
+ id: int = 1
150
+ captcha_method: str = "browser" # yescaptcha 或 browser
151
+ yescaptcha_api_key: str = ""
152
+ yescaptcha_base_url: str = "https://api.yescaptcha.com"
153
+ website_key: str = "6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV"
154
+ page_action: str = "FLOW_GENERATION"
155
+ created_at: Optional[datetime] = None
156
+ updated_at: Optional[datetime] = None
157
+
158
+
159
  # OpenAI Compatible Request Models
160
  class ChatMessage(BaseModel):
161
  """Chat message"""
src/main.py CHANGED
@@ -66,6 +66,19 @@ async def lifespan(app: FastAPI):
66
  debug_config = await db.get_debug_config()
67
  config.set_debug_enabled(debug_config.enabled)
68
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  # Initialize concurrency manager
70
  tokens = await token_manager.get_all_tokens()
71
  await concurrency_manager.initialize(tokens)
@@ -106,6 +119,10 @@ async def lifespan(app: FastAPI):
106
  await auto_unban_task_handle
107
  except asyncio.CancelledError:
108
  pass
 
 
 
 
109
  print("✓ File cache cleanup task stopped")
110
  print("✓ 429 auto-unban task stopped")
111
 
 
66
  debug_config = await db.get_debug_config()
67
  config.set_debug_enabled(debug_config.enabled)
68
 
69
+ # Load captcha configuration from database
70
+ captcha_config = await db.get_captcha_config()
71
+ config.set_captcha_method(captcha_config.captcha_method)
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 captcha_config.captcha_method == "browser":
78
+ from .services.browser_captcha import BrowserCaptchaService
79
+ browser_service = await BrowserCaptchaService.get_instance(proxy_manager)
80
+ print("✓ Browser captcha service initialized (headless mode)")
81
+
82
  # Initialize concurrency manager
83
  tokens = await token_manager.get_all_tokens()
84
  await concurrency_manager.initialize(tokens)
 
119
  await auto_unban_task_handle
120
  except asyncio.CancelledError:
121
  pass
122
+ # Close browser if initialized
123
+ if browser_service:
124
+ await browser_service.close()
125
+ print("✓ Browser captcha service closed")
126
  print("✓ File cache cleanup task stopped")
127
  print("✓ 429 auto-unban task stopped")
128
 
src/services/browser_captcha.py ADDED
@@ -0,0 +1,245 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 浏览器自动化获取 reCAPTCHA token
3
+ 使用 Playwright 访问页面并执行 reCAPTCHA 验证
4
+ """
5
+ import asyncio
6
+ import time
7
+ from typing import Optional
8
+ from playwright.async_api import async_playwright, Browser, BrowserContext
9
+
10
+ from ..core.logger import debug_logger
11
+
12
+
13
+ class BrowserCaptchaService:
14
+ """浏览器自动化获取 reCAPTCHA token(单例模式)"""
15
+
16
+ _instance: Optional['BrowserCaptchaService'] = None
17
+ _lock = asyncio.Lock()
18
+
19
+ def __init__(self, proxy_manager=None):
20
+ """初始化服务(始终使用无头模式)"""
21
+ self.headless = True # 始终无头
22
+ self.playwright = None
23
+ self.browser: Optional[Browser] = None
24
+ self._initialized = False
25
+ self.website_key = "6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV"
26
+ self.proxy_manager = proxy_manager
27
+
28
+ @classmethod
29
+ async def get_instance(cls, proxy_manager=None) -> 'BrowserCaptchaService':
30
+ """获取单例实例"""
31
+ if cls._instance is None:
32
+ async with cls._lock:
33
+ if cls._instance is None:
34
+ cls._instance = cls(proxy_manager)
35
+ await cls._instance.initialize()
36
+ return cls._instance
37
+
38
+ async def initialize(self):
39
+ """初始化浏览器(启动一次)"""
40
+ if self._initialized:
41
+ return
42
+
43
+ try:
44
+ # 获取代理配置
45
+ proxy_url = None
46
+ if self.proxy_manager:
47
+ proxy_url = await self.proxy_manager.get_proxy_url()
48
+
49
+ debug_logger.log_info(f"[BrowserCaptcha] 正在启动浏览器... (proxy={proxy_url or 'None'})")
50
+ self.playwright = await async_playwright().start()
51
+
52
+ # 配置浏览器启动参数
53
+ launch_options = {
54
+ 'headless': self.headless,
55
+ 'args': [
56
+ '--disable-blink-features=AutomationControlled',
57
+ '--disable-dev-shm-usage',
58
+ '--no-sandbox',
59
+ '--disable-setuid-sandbox'
60
+ ]
61
+ }
62
+
63
+ # 如果有代理,添加代理配置
64
+ if proxy_url:
65
+ launch_options['proxy'] = {'server': proxy_url}
66
+
67
+ self.browser = await self.playwright.chromium.launch(**launch_options)
68
+ self._initialized = True
69
+ debug_logger.log_info(f"[BrowserCaptcha] ✅ 浏览器已启动 (headless={self.headless}, proxy={proxy_url or 'None'})")
70
+ except Exception as e:
71
+ debug_logger.log_error(f"[BrowserCaptcha] ❌ 浏览器启动失败: {str(e)}")
72
+ raise
73
+
74
+ async def get_token(self, project_id: str) -> Optional[str]:
75
+ """获取 reCAPTCHA token
76
+
77
+ Args:
78
+ project_id: Flow项目ID
79
+
80
+ Returns:
81
+ reCAPTCHA token字符串,如果获取失败返回None
82
+ """
83
+ if not self._initialized:
84
+ await self.initialize()
85
+
86
+ start_time = time.time()
87
+ context = None
88
+
89
+ try:
90
+ # 创建新的上下文
91
+ context = await self.browser.new_context(
92
+ viewport={'width': 1920, 'height': 1080},
93
+ user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
94
+ locale='en-US',
95
+ timezone_id='America/New_York'
96
+ )
97
+ page = await context.new_page()
98
+
99
+ website_url = f"https://labs.google/fx/tools/flow/project/{project_id}"
100
+
101
+ debug_logger.log_info(f"[BrowserCaptcha] 访问页面: {website_url}")
102
+
103
+ # 访问页面
104
+ try:
105
+ await page.goto(website_url, wait_until="domcontentloaded", timeout=30000)
106
+ except Exception as e:
107
+ debug_logger.log_warning(f"[BrowserCaptcha] 页面加载超时或失败: {str(e)}")
108
+
109
+ # 检查并注入 reCAPTCHA v3 脚本
110
+ debug_logger.log_info("[BrowserCaptcha] 检查并加载 reCAPTCHA v3 脚本...")
111
+ script_loaded = await page.evaluate("""
112
+ () => {
113
+ if (window.grecaptcha && typeof window.grecaptcha.execute === 'function') {
114
+ return true;
115
+ }
116
+ return false;
117
+ }
118
+ """)
119
+
120
+ if not script_loaded:
121
+ # 注入脚本
122
+ debug_logger.log_info("[BrowserCaptcha] 注入 reCAPTCHA v3 脚本...")
123
+ await page.evaluate(f"""
124
+ () => {{
125
+ return new Promise((resolve) => {{
126
+ const script = document.createElement('script');
127
+ script.src = 'https://www.google.com/recaptcha/api.js?render={self.website_key}';
128
+ script.async = true;
129
+ script.defer = true;
130
+ script.onload = () => resolve(true);
131
+ script.onerror = () => resolve(false);
132
+ document.head.appendChild(script);
133
+ }});
134
+ }}
135
+ """)
136
+
137
+ # 等待reCAPTCHA加载和初始化
138
+ debug_logger.log_info("[BrowserCaptcha] 等待reCAPTCHA初始化...")
139
+ for i in range(20):
140
+ grecaptcha_ready = await page.evaluate("""
141
+ () => {
142
+ return window.grecaptcha &&
143
+ typeof window.grecaptcha.execute === 'function';
144
+ }
145
+ """)
146
+ if grecaptcha_ready:
147
+ debug_logger.log_info(f"[BrowserCaptcha] reCAPTCHA 已准备好(等待了 {i*0.5} 秒)")
148
+ break
149
+ await asyncio.sleep(0.5)
150
+ else:
151
+ debug_logger.log_warning("[BrowserCaptcha] reCAPTCHA 初始化超时,继续尝试执行...")
152
+
153
+ # 额外等待确保完全初始化
154
+ await page.wait_for_timeout(1000)
155
+
156
+ # 执行reCAPTCHA并获取token
157
+ debug_logger.log_info("[BrowserCaptcha] 执行reCAPTCHA验证...")
158
+ token = await page.evaluate("""
159
+ async (websiteKey) => {
160
+ try {
161
+ if (!window.grecaptcha) {
162
+ console.error('[BrowserCaptcha] window.grecaptcha 不存在');
163
+ return null;
164
+ }
165
+
166
+ if (typeof window.grecaptcha.execute !== 'function') {
167
+ console.error('[BrowserCaptcha] window.grecaptcha.execute 不是函数');
168
+ return null;
169
+ }
170
+
171
+ // 确保grecaptcha已准备好
172
+ await new Promise((resolve, reject) => {
173
+ const timeout = setTimeout(() => {
174
+ reject(new Error('reCAPTCHA加载超时'));
175
+ }, 15000);
176
+
177
+ if (window.grecaptcha && window.grecaptcha.ready) {
178
+ window.grecaptcha.ready(() => {
179
+ clearTimeout(timeout);
180
+ resolve();
181
+ });
182
+ } else {
183
+ clearTimeout(timeout);
184
+ resolve();
185
+ }
186
+ });
187
+
188
+ // 执行reCAPTCHA v3
189
+ const token = await window.grecaptcha.execute(websiteKey, {
190
+ action: 'FLOW_GENERATION'
191
+ });
192
+
193
+ return token;
194
+ } catch (error) {
195
+ console.error('[BrowserCaptcha] reCAPTCHA执行错误:', error);
196
+ return null;
197
+ }
198
+ }
199
+ """, self.website_key)
200
+
201
+ duration_ms = (time.time() - start_time) * 1000
202
+
203
+ if token:
204
+ debug_logger.log_info(f"[BrowserCaptcha] ✅ Token获取成功(耗时 {duration_ms:.0f}ms)")
205
+ return token
206
+ else:
207
+ debug_logger.log_error("[BrowserCaptcha] Token获取失败(返回null)")
208
+ return None
209
+
210
+ except Exception as e:
211
+ debug_logger.log_error(f"[BrowserCaptcha] 获取token异常: {str(e)}")
212
+ return None
213
+ finally:
214
+ # 关闭上下文
215
+ if context:
216
+ try:
217
+ await context.close()
218
+ except:
219
+ pass
220
+
221
+ async def close(self):
222
+ """关闭浏览器"""
223
+ try:
224
+ if self.browser:
225
+ try:
226
+ await self.browser.close()
227
+ except Exception as e:
228
+ # 忽略连接关闭错误(正常关闭场景)
229
+ if "Connection closed" not in str(e):
230
+ debug_logger.log_warning(f"[BrowserCaptcha] 关闭浏览器时出现异常: {str(e)}")
231
+ finally:
232
+ self.browser = None
233
+
234
+ if self.playwright:
235
+ try:
236
+ await self.playwright.stop()
237
+ except Exception:
238
+ pass # 静默处理 playwright 停止异常
239
+ finally:
240
+ self.playwright = None
241
+
242
+ self._initialized = False
243
+ debug_logger.log_info("[BrowserCaptcha] 浏览器已关闭")
244
+ except Exception as e:
245
+ debug_logger.log_error(f"[BrowserCaptcha] 关闭浏览器异常: {str(e)}")
src/services/flow_client.py CHANGED
@@ -308,10 +308,17 @@ class FlowClient:
308
  """
309
  url = f"{self.api_base_url}/projects/{project_id}/flowMedia:batchGenerateImages"
310
 
 
 
 
 
311
  # 构建请求
312
  request_data = {
313
  "clientContext": {
314
- "sessionId": self._generate_session_id()
 
 
 
315
  },
316
  "seed": random.randint(1, 99999),
317
  "imageModelName": model_name,
@@ -321,6 +328,10 @@ class FlowClient:
321
  }
322
 
323
  json_data = {
 
 
 
 
324
  "requests": [request_data]
325
  }
326
 
@@ -367,11 +378,15 @@ class FlowClient:
367
  """
368
  url = f"{self.api_base_url}/video:batchAsyncGenerateVideoText"
369
 
 
 
 
370
  scene_id = str(uuid.uuid4())
371
 
372
  json_data = {
373
  "clientContext": {
374
- "sessionId": self._generate_session_id(),
 
375
  "projectId": project_id,
376
  "tool": "PINHOLE",
377
  "userPaygateTier": user_paygate_tier
@@ -425,11 +440,15 @@ class FlowClient:
425
  """
426
  url = f"{self.api_base_url}/video:batchAsyncGenerateVideoReferenceImages"
427
 
 
 
 
428
  scene_id = str(uuid.uuid4())
429
 
430
  json_data = {
431
  "clientContext": {
432
- "sessionId": self._generate_session_id(),
 
433
  "projectId": project_id,
434
  "tool": "PINHOLE",
435
  "userPaygateTier": user_paygate_tier
@@ -486,11 +505,15 @@ class FlowClient:
486
  """
487
  url = f"{self.api_base_url}/video:batchAsyncGenerateVideoStartAndEndImage"
488
 
 
 
 
489
  scene_id = str(uuid.uuid4())
490
 
491
  json_data = {
492
  "clientContext": {
493
- "sessionId": self._generate_session_id(),
 
494
  "projectId": project_id,
495
  "tool": "PINHOLE",
496
  "userPaygateTier": user_paygate_tier
@@ -550,11 +573,15 @@ class FlowClient:
550
  """
551
  url = f"{self.api_base_url}/video:batchAsyncGenerateVideoStartAndEndImage"
552
 
 
 
 
553
  scene_id = str(uuid.uuid4())
554
 
555
  json_data = {
556
  "clientContext": {
557
- "sessionId": self._generate_session_id(),
 
558
  "projectId": project_id,
559
  "tool": "PINHOLE",
560
  "userPaygateTier": user_paygate_tier
@@ -655,3 +682,75 @@ class FlowClient:
655
  def _generate_scene_id(self) -> str:
656
  """生成sceneId: UUID"""
657
  return str(uuid.uuid4())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
308
  """
309
  url = f"{self.api_base_url}/projects/{project_id}/flowMedia:batchGenerateImages"
310
 
311
+ # 获取 reCAPTCHA token
312
+ recaptcha_token = await self._get_recaptcha_token(project_id) or ""
313
+ session_id = self._generate_session_id()
314
+
315
  # 构建请求
316
  request_data = {
317
  "clientContext": {
318
+ "recaptchaToken": recaptcha_token,
319
+ "projectId": project_id,
320
+ "sessionId": session_id,
321
+ "tool": "PINHOLE"
322
  },
323
  "seed": random.randint(1, 99999),
324
  "imageModelName": model_name,
 
328
  }
329
 
330
  json_data = {
331
+ "clientContext": {
332
+ "recaptchaToken": recaptcha_token,
333
+ "sessionId": session_id
334
+ },
335
  "requests": [request_data]
336
  }
337
 
 
378
  """
379
  url = f"{self.api_base_url}/video:batchAsyncGenerateVideoText"
380
 
381
+ # 获取 reCAPTCHA token
382
+ recaptcha_token = await self._get_recaptcha_token(project_id) or ""
383
+ session_id = self._generate_session_id()
384
  scene_id = str(uuid.uuid4())
385
 
386
  json_data = {
387
  "clientContext": {
388
+ "recaptchaToken": recaptcha_token,
389
+ "sessionId": session_id,
390
  "projectId": project_id,
391
  "tool": "PINHOLE",
392
  "userPaygateTier": user_paygate_tier
 
440
  """
441
  url = f"{self.api_base_url}/video:batchAsyncGenerateVideoReferenceImages"
442
 
443
+ # 获取 reCAPTCHA token
444
+ recaptcha_token = await self._get_recaptcha_token(project_id) or ""
445
+ session_id = self._generate_session_id()
446
  scene_id = str(uuid.uuid4())
447
 
448
  json_data = {
449
  "clientContext": {
450
+ "recaptchaToken": recaptcha_token,
451
+ "sessionId": session_id,
452
  "projectId": project_id,
453
  "tool": "PINHOLE",
454
  "userPaygateTier": user_paygate_tier
 
505
  """
506
  url = f"{self.api_base_url}/video:batchAsyncGenerateVideoStartAndEndImage"
507
 
508
+ # 获取 reCAPTCHA token
509
+ recaptcha_token = await self._get_recaptcha_token(project_id) or ""
510
+ session_id = self._generate_session_id()
511
  scene_id = str(uuid.uuid4())
512
 
513
  json_data = {
514
  "clientContext": {
515
+ "recaptchaToken": recaptcha_token,
516
+ "sessionId": session_id,
517
  "projectId": project_id,
518
  "tool": "PINHOLE",
519
  "userPaygateTier": user_paygate_tier
 
573
  """
574
  url = f"{self.api_base_url}/video:batchAsyncGenerateVideoStartAndEndImage"
575
 
576
+ # 获取 reCAPTCHA token
577
+ recaptcha_token = await self._get_recaptcha_token(project_id) or ""
578
+ session_id = self._generate_session_id()
579
  scene_id = str(uuid.uuid4())
580
 
581
  json_data = {
582
  "clientContext": {
583
+ "recaptchaToken": recaptcha_token,
584
+ "sessionId": session_id,
585
  "projectId": project_id,
586
  "tool": "PINHOLE",
587
  "userPaygateTier": user_paygate_tier
 
682
  def _generate_scene_id(self) -> str:
683
  """生成sceneId: UUID"""
684
  return str(uuid.uuid4())
685
+
686
+ async def _get_recaptcha_token(self, project_id: str) -> Optional[str]:
687
+ """获取reCAPTCHA token - 支持两种方式"""
688
+ captcha_method = config.captcha_method
689
+
690
+ # 浏览器打码
691
+ if captcha_method == "browser":
692
+ try:
693
+ from .browser_captcha import BrowserCaptchaService
694
+ service = await BrowserCaptchaService.get_instance(self.proxy_manager)
695
+ return await service.get_token(project_id)
696
+ except Exception as e:
697
+ debug_logger.log_error(f"[reCAPTCHA Browser] error: {str(e)}")
698
+ return None
699
+
700
+ # YesCaptcha打码
701
+ client_key = config.yescaptcha_api_key
702
+ if not client_key:
703
+ debug_logger.log_info("[reCAPTCHA] API key not configured, skipping")
704
+ return None
705
+
706
+ website_key = "6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV"
707
+ website_url = f"https://labs.google/fx/tools/flow/project/{project_id}"
708
+ base_url = config.yescaptcha_base_url
709
+ page_action = "FLOW_GENERATION"
710
+
711
+ try:
712
+ async with AsyncSession() as session:
713
+ create_url = f"{base_url}/createTask"
714
+ create_data = {
715
+ "clientKey": client_key,
716
+ "task": {
717
+ "websiteURL": website_url,
718
+ "websiteKey": website_key,
719
+ "type": "RecaptchaV3TaskProxylessM1",
720
+ "pageAction": page_action
721
+ }
722
+ }
723
+
724
+ result = await session.post(create_url, json=create_data, impersonate="chrome110")
725
+ result_json = result.json()
726
+ task_id = result_json.get('taskId')
727
+
728
+ debug_logger.log_info(f"[reCAPTCHA] created task_id: {task_id}")
729
+
730
+ if not task_id:
731
+ return None
732
+
733
+ get_url = f"{base_url}/getTaskResult"
734
+ for i in range(40):
735
+ get_data = {
736
+ "clientKey": client_key,
737
+ "taskId": task_id
738
+ }
739
+ result = await session.post(get_url, json=get_data, impersonate="chrome110")
740
+ result_json = result.json()
741
+
742
+ debug_logger.log_info(f"[reCAPTCHA] polling #{i+1}: {result_json}")
743
+
744
+ solution = result_json.get('solution', {})
745
+ response = solution.get('gRecaptchaResponse')
746
+
747
+ if response:
748
+ return response
749
+
750
+ time.sleep(3)
751
+
752
+ return None
753
+
754
+ except Exception as e:
755
+ debug_logger.log_error(f"[reCAPTCHA] error: {str(e)}")
756
+ return None