JXJBing commited on
Commit
f44c021
·
verified ·
1 Parent(s): 8cea441

我也不知道呢

Browse files
.gitignore CHANGED
@@ -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
Dockerfile CHANGED
@@ -1,20 +1,44 @@
1
- # Use an official Python runtime as a parent image
2
- FROM python:3.10-slim
3
 
4
- # Set the working directory in the container
5
  WORKDIR /app
6
 
7
- # Copy the current directory contents into the container at /app
8
- COPY . /app
 
9
 
10
- # Install any needed packages specified in requirements.txt
11
- RUN pip install --no-cache-dir -r requirements.txt
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
- # Make port 7860 available to the world outside this container
14
- EXPOSE 7860
 
 
 
15
 
16
- # Define environment variable
17
- ENV PORT=7860
18
 
19
- # Run app.py when the container launches
20
- CMD ["python", "app.py"]
 
 
 
 
 
 
 
1
+ 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
+
42
+ EXPOSE 8000
43
+
44
+ CMD ["python", "main.py"]
README.md CHANGED
@@ -1,14 +1,3 @@
1
- ---
2
- title: Flow2API
3
- emoji: 🚀
4
- colorFrom: blue
5
- colorTo: green
6
- sdk: docker
7
- sdk_version: '1.0'
8
- app_file: app.py
9
- pinned: false
10
- ---
11
-
12
  # Flow2API
13
 
14
  <div align="center">
@@ -27,7 +16,7 @@ pinned: false
27
  - 🎨 **文生图** / **图生图**
28
  - 🎬 **文生视频** / **图生视频**
29
  - 🎞️ **首尾帧视频**
30
- - 🔄 **AT自动刷新**
31
  - 📊 **余额显示** - 实时查询和显示 VideoFX Credits
32
  - 🚀 **负载均衡** - 多 Token 轮询和并发控制
33
  - 🌐 **代理支持** - 支持 HTTP/SOCKS5 代理
@@ -77,7 +66,7 @@ docker-compose -f docker-compose.warp.yml logs -f
77
  ```bash
78
  # 克隆项目
79
  git clone https://github.com/TheSmallHanCat/flow2api.git
80
- cd flow2api
81
 
82
  # 创建虚拟环境
83
  python -m venv venv
@@ -95,35 +84,6 @@ pip install -r requirements.txt
95
  python main.py
96
  ```
97
 
98
- ### 方式三:Hugging Face 部署
99
-
100
- 1. **准备工作**
101
- - 注册并登录 [Hugging Face](https://huggingface.co/)
102
- - 创建一个新的 Space
103
- - 选择 "Docker" 作为 Space 类型
104
-
105
- 2. **配置 Space**
106
- - **Repository name**: 输入一个名称(例如 `flow2api`)
107
- - **Visibility**: 选择 "Public" 或 "Private"
108
- - **Hardware**: 选择适当的硬件配置(最低推荐 2GB RAM)
109
-
110
- 3. **部署步骤**
111
- - 在 Space 的 "Files" 标签页中,上传项目的所有文件
112
- - 确保 `app.py` 文件存在(Hugging Face 的部署入口)
113
- - 确保 `requirements.txt` 文件包含所有必要的依赖
114
- - 点击 "Build" 按钮开始部署
115
-
116
- 4. **访问服务**
117
- - 部署完成后,通过 Space 提供的 URL 访问服务
118
- - 默认登录地址: `https://<your-space-name>.hf.space/`
119
- - 首次登录用户名: `admin`,密码: `admin`
120
-
121
- 5. **注意事项**
122
- - Hugging Face Spaces 使用端口 7860,配置已自动适配
123
- - 由于 Hugging Face 的网络环境,可能需要配置代理
124
- - 建议在管理界面中修改默认的管理员密码
125
- - 对于视频生成等耗时操作,可能会受到 Hugging Face 的资源限制
126
-
127
  ### 首次访问
128
 
129
  服务启动后,访问管理后台: **http://localhost:8000**,首次登录后请立即修改密码!
@@ -159,7 +119,11 @@ python main.py
159
  | `veo_2_0_t2v_landscape` | 文生视频 | 横屏 |
160
 
161
  #### 首尾帧模型 (I2V - Image to Video)
162
- 📸 **支持1-2张图片:首尾帧**
 
 
 
 
163
 
164
  | 模型名称 | 说明| 尺寸 |
165
  |---------|---------|--------|
 
 
 
 
 
 
 
 
 
 
 
 
1
  # Flow2API
2
 
3
  <div align="center">
 
16
  - 🎨 **文生图** / **图生图**
17
  - 🎬 **文生视频** / **图生视频**
18
  - 🎞️ **首尾帧视频**
19
+ - 🔄 **AT/ST自动刷新** - AT 过期自动刷新,ST 过期时自动通过浏览器更新(personal 模式)
20
  - 📊 **余额显示** - 实时查询和显示 VideoFX Credits
21
  - 🚀 **负载均衡** - 多 Token 轮询和并发控制
22
  - 🌐 **代理支持** - 支持 HTTP/SOCKS5 代理
 
66
  ```bash
67
  # 克隆项目
68
  git clone https://github.com/TheSmallHanCat/flow2api.git
69
+ cd sora2api
70
 
71
  # 创建虚拟环境
72
  python -m venv venv
 
84
  python main.py
85
  ```
86
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  ### 首次访问
88
 
89
  服务启动后,访问管理后台: **http://localhost:8000**,首次登录后请立即修改密码!
 
119
  | `veo_2_0_t2v_landscape` | 文生视频 | 横屏 |
120
 
121
  #### 首尾帧模型 (I2V - Image to Video)
122
+ 📸 **支持1-2张图片:1张作为帧,2张作为首尾帧**
123
+
124
+ > 💡 **自动适配**:系统会根据图片数量自动选择对应的 model_key
125
+ > - **单帧模式**(1张图):使用首帧生成视频
126
+ > - **双帧模式**(2张图):使用首帧+尾帧生成过渡视频
127
 
128
  | 模型名称 | 说明| 尺寸 |
129
  |---------|---------|--------|
docker-compose.local.yml ADDED
@@ -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
docker-compose.proxy.yml CHANGED
@@ -5,7 +5,7 @@ services:
5
  image: thesmallhancat/flow2api:latest
6
  container_name: flow2api
7
  ports:
8
- - "8000:8000"
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
- - "1080:1080"
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
docker-compose.yml CHANGED
@@ -5,7 +5,7 @@ services:
5
  image: thesmallhancat/flow2api:latest
6
  container_name: flow2api
7
  ports:
8
- - "8000:8000"
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
requirements.txt CHANGED
@@ -7,3 +7,5 @@ tomli==2.2.1
7
  bcrypt==4.2.1
8
  python-multipart==0.0.20
9
  python-dateutil==2.8.2
 
 
 
7
  bcrypt==4.2.1
8
  python-multipart==0.0.20
9
  python-dateutil==2.8.2
10
+ playwright==1.53.0
11
+ nodriver>=0.48.0
src/api/admin.py CHANGED
@@ -354,17 +354,32 @@ async def refresh_at(
354
  token_id: int,
355
  token: str = Depends(verify_admin_token)
356
  ):
357
- """手动刷新Token的AT (使用ST转换) 🆕"""
 
 
 
 
 
 
 
 
358
  try:
359
- # 调用token_manager的内部刷新方法
360
  success = await token_manager._refresh_at(token_id)
361
 
362
  if success:
363
  # 获取更新后的token信息
364
  updated_token = await token_manager.get_token(token_id)
 
 
 
 
 
 
 
365
  return {
366
  "success": True,
367
- "message": "AT刷新成功",
368
  "token": {
369
  "id": updated_token.id,
370
  "email": updated_token.email,
@@ -372,8 +387,17 @@ async def refresh_at(
372
  }
373
  }
374
  else:
375
- raise HTTPException(status_code=500, detail="AT刷新失败")
 
 
 
 
 
 
 
 
376
  except Exception as e:
 
377
  raise HTTPException(status_code=500, detail=f"刷新AT失败: {str(e)}")
378
 
379
 
@@ -519,13 +543,8 @@ async def update_proxy_config_alias(
519
  token: str = Depends(verify_admin_token)
520
  ):
521
  """Update proxy configuration (alias for frontend compatibility)"""
522
- try:
523
- await proxy_manager.update_proxy_config(request.proxy_enabled, request.proxy_url)
524
- return {"success": True, "message": "代理配置更新成功"}
525
- except Exception as e:
526
- # 捕获所有异常,确保返回有效的 JSON 响应
527
- print(f"Error updating proxy config: {e}")
528
- return {"success": False, "message": f"保存失败: {str(e)}"}
529
 
530
 
531
  @router.post("/api/config/proxy")
@@ -534,13 +553,8 @@ async def update_proxy_config(
534
  token: str = Depends(verify_admin_token)
535
  ):
536
  """Update proxy configuration"""
537
- try:
538
- await proxy_manager.update_proxy_config(request.proxy_enabled, request.proxy_url)
539
- return {"success": True, "message": "代理配置更新成功"}
540
- except Exception as e:
541
- # 捕获所有异常,确保返回有效的 JSON 响应
542
- print(f"Error updating proxy config: {e}")
543
- return {"success": False, "message": f"保存失败: {str(e)}"}
544
 
545
 
546
  @router.get("/api/config/generation")
@@ -692,18 +706,10 @@ async def update_admin_config(
692
  token: str = Depends(verify_admin_token)
693
  ):
694
  """Update admin configuration (error_ban_threshold)"""
695
- try:
696
- # Update error_ban_threshold in database
697
- await db.update_admin_config(error_ban_threshold=request.error_ban_threshold)
698
 
699
- # 🔥 Hot reload: sync database config to memory
700
- await db.reload_config_to_memory()
701
-
702
- return {"success": True, "message": "配置更新成功"}
703
- except Exception as e:
704
- # 捕获所有异常,确保返回有效的 JSON 响应
705
- print(f"Error updating admin config: {e}")
706
- return {"success": False, "message": f"保存失败: {str(e)}"}
707
 
708
 
709
  @router.post("/api/admin/password")
@@ -759,17 +765,12 @@ async def update_generation_timeout(
759
  token: str = Depends(verify_admin_token)
760
  ):
761
  """Update generation timeout configuration"""
762
- try:
763
- await db.update_generation_config(request.image_timeout, request.video_timeout)
764
 
765
- # 🔥 Hot reload: sync database config to memory
766
- await db.reload_config_to_memory()
767
 
768
- return {"success": True, "message": "生成配置更新成功"}
769
- except Exception as e:
770
- # 捕获所有异常,确保返回有效的 JSON 响应
771
- print(f"Error updating generation timeout: {e}")
772
- return {"success": False, "message": f"保存失败: {str(e)}"}
773
 
774
 
775
  # ========== AT Auto Refresh Config ==========
@@ -838,21 +839,16 @@ async def update_cache_config_full(
838
  token: str = Depends(verify_admin_token)
839
  ):
840
  """Update complete cache configuration"""
841
- try:
842
- enabled = request.get("enabled")
843
- timeout = request.get("timeout")
844
- base_url = request.get("base_url")
845
 
846
- await db.update_cache_config(enabled=enabled, timeout=timeout, base_url=base_url)
847
 
848
- # 🔥 Hot reload: sync database config to memory
849
- await db.reload_config_to_memory()
850
 
851
- return {"success": True, "message": "缓存配置更新成功"}
852
- except Exception as e:
853
- # 捕获所有异常,确保返回有效的 JSON 响应
854
- print(f"Error updating cache config: {e}")
855
- return {"success": False, "message": f"保存失败: {str(e)}"}
856
 
857
 
858
  @router.post("/api/cache/base-url")
@@ -861,18 +857,13 @@ async def update_cache_base_url(
861
  token: str = Depends(verify_admin_token)
862
  ):
863
  """Update cache base URL"""
864
- try:
865
- base_url = request.get("base_url", "")
866
- await db.update_cache_config(base_url=base_url)
867
 
868
- # 🔥 Hot reload: sync database config to memory
869
- await db.reload_config_to_memory()
870
 
871
- return {"success": True, "message": "缓存Base URL更新成功"}
872
- except Exception as e:
873
- # 捕获所有异常,确保返回有效的 JSON 响应
874
- print(f"Error updating cache base URL: {e}")
875
- return {"success": False, "message": f"保存失败: {str(e)}"}
876
 
877
 
878
  @router.post("/api/captcha/config")
@@ -881,37 +872,44 @@ async def update_captcha_config(
881
  token: str = Depends(verify_admin_token)
882
  ):
883
  """Update captcha configuration"""
884
- try:
885
- from ..services.browser_captcha import validate_browser_proxy_url
886
-
887
- captcha_method = request.get("captcha_method")
888
- yescaptcha_api_key = request.get("yescaptcha_api_key")
889
- yescaptcha_base_url = request.get("yescaptcha_base_url")
890
- browser_proxy_enabled = request.get("browser_proxy_enabled", False)
891
- browser_proxy_url = request.get("browser_proxy_url", "")
892
-
893
- # 验证浏览器代理URL格式
894
- if browser_proxy_enabled and browser_proxy_url:
895
- is_valid, error_msg = validate_browser_proxy_url(browser_proxy_url)
896
- if not is_valid:
897
- return {"success": False, "message": error_msg}
898
-
899
- await db.update_captcha_config(
900
- captcha_method=captcha_method,
901
- yescaptcha_api_key=yescaptcha_api_key,
902
- yescaptcha_base_url=yescaptcha_base_url,
903
- browser_proxy_enabled=browser_proxy_enabled,
904
- browser_proxy_url=browser_proxy_url if browser_proxy_enabled else None
905
- )
 
 
 
 
 
 
 
 
 
 
 
906
 
907
- # 🔥 Hot reload: sync database config to memory
908
- await db.reload_config_to_memory()
909
 
910
- return {"success": True, "message": "验证码配置更新成功"}
911
- except Exception as e:
912
- # 捕获所有异常,确保返回有效的 JSON 响应
913
- print(f"Error updating captcha config: {e}")
914
- return {"success": False, "message": f"保存失败: {str(e)}"}
915
 
916
 
917
  @router.get("/api/captcha/config")
@@ -922,6 +920,12 @@ async def get_captcha_config(token: str = Depends(verify_admin_token)):
922
  "captcha_method": captcha_config.captcha_method,
923
  "yescaptcha_api_key": captcha_config.yescaptcha_api_key,
924
  "yescaptcha_base_url": captcha_config.yescaptcha_base_url,
 
 
 
 
 
 
925
  "browser_proxy_enabled": captcha_config.browser_proxy_enabled,
926
  "browser_proxy_url": captcha_config.browser_proxy_url or ""
927
  }
@@ -969,29 +973,24 @@ async def update_plugin_config(
969
  token: str = Depends(verify_admin_token)
970
  ):
971
  """Update plugin configuration"""
972
- try:
973
- connection_token = request.get("connection_token", "")
974
- auto_enable_on_update = request.get("auto_enable_on_update", True) # 默认开启
975
 
976
- # Generate random token if empty
977
- if not connection_token:
978
- connection_token = secrets.token_urlsafe(32)
979
 
980
- await db.update_plugin_config(
981
- connection_token=connection_token,
982
- auto_enable_on_update=auto_enable_on_update
983
- )
984
 
985
- return {
986
- "success": True,
987
- "message": "插件配置更新成功",
988
- "connection_token": connection_token,
989
- "auto_enable_on_update": auto_enable_on_update
990
- }
991
- except Exception as e:
992
- # 捕获所有异常,确保返回有效的 JSON 响应
993
- print(f"Error updating plugin config: {e}")
994
- return {"success": False, "message": f"保存失败: {str(e)}"}
995
 
996
 
997
  @router.post("/api/plugin/update-token")
 
354
  token_id: int,
355
  token: str = Depends(verify_admin_token)
356
  ):
357
+ """手动刷新Token的AT (使用ST转换) 🆕
358
+
359
+ 如果 AT 刷新失败且处于 personal 模式,会自动尝试通过浏览器刷新 ST
360
+ """
361
+ from ..core.logger import debug_logger
362
+ from ..core.config import config
363
+
364
+ debug_logger.log_info(f"[API] 手动刷新 AT 请求: token_id={token_id}, captcha_method={config.captcha_method}")
365
+
366
  try:
367
+ # 调用token_manager的内部刷新方法(包含 ST 自动刷新逻辑)
368
  success = await token_manager._refresh_at(token_id)
369
 
370
  if success:
371
  # 获取更新后的token信息
372
  updated_token = await token_manager.get_token(token_id)
373
+
374
+ message = "AT刷新成功"
375
+ if config.captcha_method == "personal":
376
+ message += "(支持ST自动刷新)"
377
+
378
+ debug_logger.log_info(f"[API] AT 刷新成功: token_id={token_id}")
379
+
380
  return {
381
  "success": True,
382
+ "message": message,
383
  "token": {
384
  "id": updated_token.id,
385
  "email": updated_token.email,
 
387
  }
388
  }
389
  else:
390
+ debug_logger.log_error(f"[API] AT 刷新失败: token_id={token_id}")
391
+
392
+ error_detail = "AT刷新失败"
393
+ if config.captcha_method != "personal":
394
+ error_detail += f"(当前打码模式: {config.captcha_method},ST自动刷新仅在 personal 模式下可用)"
395
+
396
+ raise HTTPException(status_code=500, detail=error_detail)
397
+ except HTTPException:
398
+ raise
399
  except Exception as e:
400
+ debug_logger.log_error(f"[API] 刷新AT异常: {str(e)}")
401
  raise HTTPException(status_code=500, detail=f"刷新AT失败: {str(e)}")
402
 
403
 
 
543
  token: str = Depends(verify_admin_token)
544
  ):
545
  """Update proxy configuration (alias for frontend compatibility)"""
546
+ await proxy_manager.update_proxy_config(request.proxy_enabled, request.proxy_url)
547
+ return {"success": True, "message": "代理配置更新成功"}
 
 
 
 
 
548
 
549
 
550
  @router.post("/api/config/proxy")
 
553
  token: str = Depends(verify_admin_token)
554
  ):
555
  """Update proxy configuration"""
556
+ await proxy_manager.update_proxy_config(request.proxy_enabled, request.proxy_url)
557
+ return {"success": True, "message": "代理配置更新成功"}
 
 
 
 
 
558
 
559
 
560
  @router.get("/api/config/generation")
 
706
  token: str = Depends(verify_admin_token)
707
  ):
708
  """Update admin configuration (error_ban_threshold)"""
709
+ # Update error_ban_threshold in database
710
+ await db.update_admin_config(error_ban_threshold=request.error_ban_threshold)
 
711
 
712
+ return {"success": True, "message": "配置更新成功"}
 
 
 
 
 
 
 
713
 
714
 
715
  @router.post("/api/admin/password")
 
765
  token: str = Depends(verify_admin_token)
766
  ):
767
  """Update generation timeout configuration"""
768
+ await db.update_generation_config(request.image_timeout, request.video_timeout)
 
769
 
770
+ # 🔥 Hot reload: sync database config to memory
771
+ await db.reload_config_to_memory()
772
 
773
+ return {"success": True, "message": "生成配置更新成功"}
 
 
 
 
774
 
775
 
776
  # ========== AT Auto Refresh Config ==========
 
839
  token: str = Depends(verify_admin_token)
840
  ):
841
  """Update complete cache configuration"""
842
+ enabled = request.get("enabled")
843
+ timeout = request.get("timeout")
844
+ base_url = request.get("base_url")
 
845
 
846
+ await db.update_cache_config(enabled=enabled, timeout=timeout, base_url=base_url)
847
 
848
+ # 🔥 Hot reload: sync database config to memory
849
+ await db.reload_config_to_memory()
850
 
851
+ return {"success": True, "message": "缓存配置更新成功"}
 
 
 
 
852
 
853
 
854
  @router.post("/api/cache/base-url")
 
857
  token: str = Depends(verify_admin_token)
858
  ):
859
  """Update cache base URL"""
860
+ base_url = request.get("base_url", "")
861
+ await db.update_cache_config(base_url=base_url)
 
862
 
863
+ # 🔥 Hot reload: sync database config to memory
864
+ await db.reload_config_to_memory()
865
 
866
+ return {"success": True, "message": "缓存Base URL更新成功"}
 
 
 
 
867
 
868
 
869
  @router.post("/api/captcha/config")
 
872
  token: str = Depends(verify_admin_token)
873
  ):
874
  """Update captcha configuration"""
875
+ from ..services.browser_captcha import validate_browser_proxy_url
876
+
877
+ captcha_method = request.get("captcha_method")
878
+ yescaptcha_api_key = request.get("yescaptcha_api_key")
879
+ yescaptcha_base_url = request.get("yescaptcha_base_url")
880
+ capmonster_api_key = request.get("capmonster_api_key")
881
+ capmonster_base_url = request.get("capmonster_base_url")
882
+ ezcaptcha_api_key = request.get("ezcaptcha_api_key")
883
+ ezcaptcha_base_url = request.get("ezcaptcha_base_url")
884
+ capsolver_api_key = request.get("capsolver_api_key")
885
+ capsolver_base_url = request.get("capsolver_base_url")
886
+ browser_proxy_enabled = request.get("browser_proxy_enabled", False)
887
+ browser_proxy_url = request.get("browser_proxy_url", "")
888
+
889
+ # 验证浏览器代理URL格式
890
+ if browser_proxy_enabled and browser_proxy_url:
891
+ is_valid, error_msg = validate_browser_proxy_url(browser_proxy_url)
892
+ if not is_valid:
893
+ return {"success": False, "message": error_msg}
894
+
895
+ await db.update_captcha_config(
896
+ captcha_method=captcha_method,
897
+ yescaptcha_api_key=yescaptcha_api_key,
898
+ yescaptcha_base_url=yescaptcha_base_url,
899
+ capmonster_api_key=capmonster_api_key,
900
+ capmonster_base_url=capmonster_base_url,
901
+ ezcaptcha_api_key=ezcaptcha_api_key,
902
+ ezcaptcha_base_url=ezcaptcha_base_url,
903
+ capsolver_api_key=capsolver_api_key,
904
+ capsolver_base_url=capsolver_base_url,
905
+ browser_proxy_enabled=browser_proxy_enabled,
906
+ browser_proxy_url=browser_proxy_url if browser_proxy_enabled else None
907
+ )
908
 
909
+ # 🔥 Hot reload: sync database config to memory
910
+ await db.reload_config_to_memory()
911
 
912
+ return {"success": True, "message": "验证码配置更新成功"}
 
 
 
 
913
 
914
 
915
  @router.get("/api/captcha/config")
 
920
  "captcha_method": captcha_config.captcha_method,
921
  "yescaptcha_api_key": captcha_config.yescaptcha_api_key,
922
  "yescaptcha_base_url": captcha_config.yescaptcha_base_url,
923
+ "capmonster_api_key": captcha_config.capmonster_api_key,
924
+ "capmonster_base_url": captcha_config.capmonster_base_url,
925
+ "ezcaptcha_api_key": captcha_config.ezcaptcha_api_key,
926
+ "ezcaptcha_base_url": captcha_config.ezcaptcha_base_url,
927
+ "capsolver_api_key": captcha_config.capsolver_api_key,
928
+ "capsolver_base_url": captcha_config.capsolver_base_url,
929
  "browser_proxy_enabled": captcha_config.browser_proxy_enabled,
930
  "browser_proxy_url": captcha_config.browser_proxy_url or ""
931
  }
 
973
  token: str = Depends(verify_admin_token)
974
  ):
975
  """Update plugin configuration"""
976
+ connection_token = request.get("connection_token", "")
977
+ auto_enable_on_update = request.get("auto_enable_on_update", True) # 默认开启
 
978
 
979
+ # Generate random token if empty
980
+ if not connection_token:
981
+ connection_token = secrets.token_urlsafe(32)
982
 
983
+ await db.update_plugin_config(
984
+ connection_token=connection_token,
985
+ auto_enable_on_update=auto_enable_on_update
986
+ )
987
 
988
+ return {
989
+ "success": True,
990
+ "message": "插件配置更新成功",
991
+ "connection_token": connection_token,
992
+ "auto_enable_on_update": auto_enable_on_update
993
+ }
 
 
 
 
994
 
995
 
996
  @router.post("/api/plugin/update-token")
src/api/routes.py CHANGED
@@ -111,7 +111,7 @@ async def create_chat_completion(
111
  if item.get("type") == "text":
112
  prompt = item.get("text", "")
113
  elif item.get("type") == "image_url":
114
- # Extract base64 image
115
  image_url = item.get("image_url", {}).get("url", "")
116
  if image_url.startswith("data:image"):
117
  # Parse base64
@@ -120,6 +120,18 @@ async def create_chat_completion(
120
  image_base64 = match.group(1)
121
  image_bytes = base64.b64decode(image_base64)
122
  images.append(image_bytes)
 
 
 
 
 
 
 
 
 
 
 
 
123
 
124
  # Fallback to deprecated image parameter
125
  if request.image and not images:
 
111
  if item.get("type") == "text":
112
  prompt = item.get("text", "")
113
  elif item.get("type") == "image_url":
114
+ # Extract image from URL or base64
115
  image_url = item.get("image_url", {}).get("url", "")
116
  if image_url.startswith("data:image"):
117
  # Parse base64
 
120
  image_base64 = match.group(1)
121
  image_bytes = base64.b64decode(image_base64)
122
  images.append(image_bytes)
123
+ elif image_url.startswith("http://") or image_url.startswith("https://"):
124
+ # Download remote image URL
125
+ debug_logger.log_info(f"[IMAGE_URL] 下载远程图片: {image_url}")
126
+ try:
127
+ downloaded_bytes = await retrieve_image_data(image_url)
128
+ if downloaded_bytes and len(downloaded_bytes) > 0:
129
+ images.append(downloaded_bytes)
130
+ debug_logger.log_info(f"[IMAGE_URL] ✅ 远程图片下载成功: {len(downloaded_bytes)} 字节")
131
+ else:
132
+ debug_logger.log_warning(f"[IMAGE_URL] ⚠️ 远程图片下载失败或为空: {image_url}")
133
+ except Exception as e:
134
+ debug_logger.log_error(f"[IMAGE_URL] ❌ 远程图片下载异常: {str(e)}")
135
 
136
  # Fallback to deprecated image parameter
137
  if request.image and not images:
src/core/config.py CHANGED
@@ -213,6 +213,72 @@ class Config:
213
  self._config["captcha"] = {}
214
  self._config["captcha"]["yescaptcha_base_url"] = base_url
215
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
 
217
  # Global config instance
218
  config = Config()
 
213
  self._config["captcha"] = {}
214
  self._config["captcha"]["yescaptcha_base_url"] = base_url
215
 
216
+ @property
217
+ def capmonster_api_key(self) -> str:
218
+ """Get CapMonster API key"""
219
+ return self._config.get("captcha", {}).get("capmonster_api_key", "")
220
+
221
+ def set_capmonster_api_key(self, api_key: str):
222
+ """Set CapMonster API key"""
223
+ if "captcha" not in self._config:
224
+ self._config["captcha"] = {}
225
+ self._config["captcha"]["capmonster_api_key"] = api_key
226
+
227
+ @property
228
+ def capmonster_base_url(self) -> str:
229
+ """Get CapMonster base URL"""
230
+ return self._config.get("captcha", {}).get("capmonster_base_url", "https://api.capmonster.cloud")
231
+
232
+ def set_capmonster_base_url(self, base_url: str):
233
+ """Set CapMonster base URL"""
234
+ if "captcha" not in self._config:
235
+ self._config["captcha"] = {}
236
+ self._config["captcha"]["capmonster_base_url"] = base_url
237
+
238
+ @property
239
+ def ezcaptcha_api_key(self) -> str:
240
+ """Get EzCaptcha API key"""
241
+ return self._config.get("captcha", {}).get("ezcaptcha_api_key", "")
242
+
243
+ def set_ezcaptcha_api_key(self, api_key: str):
244
+ """Set EzCaptcha API key"""
245
+ if "captcha" not in self._config:
246
+ self._config["captcha"] = {}
247
+ self._config["captcha"]["ezcaptcha_api_key"] = api_key
248
+
249
+ @property
250
+ def ezcaptcha_base_url(self) -> str:
251
+ """Get EzCaptcha base URL"""
252
+ return self._config.get("captcha", {}).get("ezcaptcha_base_url", "https://api.ez-captcha.com")
253
+
254
+ def set_ezcaptcha_base_url(self, base_url: str):
255
+ """Set EzCaptcha base URL"""
256
+ if "captcha" not in self._config:
257
+ self._config["captcha"] = {}
258
+ self._config["captcha"]["ezcaptcha_base_url"] = base_url
259
+
260
+ @property
261
+ def capsolver_api_key(self) -> str:
262
+ """Get CapSolver API key"""
263
+ return self._config.get("captcha", {}).get("capsolver_api_key", "")
264
+
265
+ def set_capsolver_api_key(self, api_key: str):
266
+ """Set CapSolver API key"""
267
+ if "captcha" not in self._config:
268
+ self._config["captcha"] = {}
269
+ self._config["captcha"]["capsolver_api_key"] = api_key
270
+
271
+ @property
272
+ def capsolver_base_url(self) -> str:
273
+ """Get CapSolver base URL"""
274
+ return self._config.get("captcha", {}).get("capsolver_base_url", "https://api.capsolver.com")
275
+
276
+ def set_capsolver_base_url(self, base_url: str):
277
+ """Set CapSolver base URL"""
278
+ if "captcha" not in self._config:
279
+ self._config["captcha"] = {}
280
+ self._config["captcha"]["capsolver_base_url"] = base_url
281
+
282
 
283
  # Global config instance
284
  config = Config()
src/core/database.py CHANGED
@@ -172,8 +172,8 @@ class Database:
172
  count = await cursor.fetchone()
173
  if count[0] == 0:
174
  await db.execute("""
175
- INSERT INTO plugin_config (id, connection_token, auto_enable_on_update)
176
- VALUES (1, '', 1)
177
  """)
178
 
179
  async def check_and_migrate_db(self, config_dict: dict = None):
@@ -216,6 +216,12 @@ class Database:
216
  captcha_method TEXT DEFAULT 'browser',
217
  yescaptcha_api_key TEXT DEFAULT '',
218
  yescaptcha_base_url TEXT DEFAULT 'https://api.yescaptcha.com',
 
 
 
 
 
 
219
  website_key TEXT DEFAULT '6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV',
220
  page_action TEXT DEFAULT 'FLOW_GENERATION',
221
  browser_proxy_enabled BOOLEAN DEFAULT 0,
@@ -232,7 +238,6 @@ class Database:
232
  CREATE TABLE plugin_config (
233
  id INTEGER PRIMARY KEY DEFAULT 1,
234
  connection_token TEXT DEFAULT '',
235
- auto_enable_on_update BOOLEAN DEFAULT 1,
236
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
237
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
238
  )
@@ -278,6 +283,12 @@ class Database:
278
  captcha_columns_to_add = [
279
  ("browser_proxy_enabled", "BOOLEAN DEFAULT 0"),
280
  ("browser_proxy_url", "TEXT"),
 
 
 
 
 
 
281
  ]
282
 
283
  for col_name, col_type in captcha_columns_to_add:
@@ -490,6 +501,12 @@ class Database:
490
  captcha_method TEXT DEFAULT 'browser',
491
  yescaptcha_api_key TEXT DEFAULT '',
492
  yescaptcha_base_url TEXT DEFAULT 'https://api.yescaptcha.com',
 
 
 
 
 
 
493
  website_key TEXT DEFAULT '6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV',
494
  page_action TEXT DEFAULT 'FLOW_GENERATION',
495
  browser_proxy_enabled BOOLEAN DEFAULT 0,
@@ -504,7 +521,6 @@ class Database:
504
  CREATE TABLE IF NOT EXISTS plugin_config (
505
  id INTEGER PRIMARY KEY DEFAULT 1,
506
  connection_token TEXT DEFAULT '',
507
- auto_enable_on_update BOOLEAN DEFAULT 1,
508
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
509
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
510
  )
@@ -937,32 +953,23 @@ class Database:
937
 
938
  async def get_generation_config(self) -> Optional[GenerationConfig]:
939
  """Get generation configuration"""
940
- try:
941
- async with aiosqlite.connect(self.db_path) as db:
942
- db.row_factory = aiosqlite.Row
943
- cursor = await db.execute("SELECT * FROM generation_config WHERE id = 1")
944
- row = await cursor.fetchone()
945
- if row:
946
- return GenerationConfig(**dict(row))
947
- return None
948
- except Exception as e:
949
- print(f"Error getting generation config: {e}")
950
- # 返回默认配置,避免异常传播
951
- return GenerationConfig(image_timeout=300, video_timeout=1500)
952
 
953
  async def update_generation_config(self, image_timeout: int, video_timeout: int):
954
  """Update generation configuration"""
955
- try:
956
- async with aiosqlite.connect(self.db_path) as db:
957
- await db.execute("""
958
- UPDATE generation_config
959
- SET image_timeout = ?, video_timeout = ?, updated_at = CURRENT_TIMESTAMP
960
- WHERE id = 1
961
- """, (image_timeout, video_timeout))
962
- await db.commit()
963
- except Exception as e:
964
- print(f"Error updating generation config: {e}")
965
- raise
966
 
967
  # Request log operations
968
  async def add_request_log(self, log: RequestLog):
@@ -1057,43 +1064,39 @@ class Database:
1057
  - Generation config (image_timeout, video_timeout)
1058
  - Proxy config will be handled by ProxyManager
1059
  """
1060
- try:
1061
- from .config import config
1062
-
1063
- # Reload admin config
1064
- admin_config = await self.get_admin_config()
1065
- if admin_config:
1066
- config.set_admin_username_from_db(admin_config.username)
1067
- config.set_admin_password_from_db(admin_config.password)
1068
- config.api_key = admin_config.api_key
1069
-
1070
- # Reload cache config
1071
- cache_config = await self.get_cache_config()
1072
- if cache_config:
1073
- config.set_cache_enabled(cache_config.cache_enabled)
1074
- config.set_cache_timeout(cache_config.cache_timeout)
1075
- config.set_cache_base_url(cache_config.cache_base_url or "")
1076
-
1077
- # Reload generation config
1078
- generation_config = await self.get_generation_config()
1079
- if generation_config:
1080
- config.set_image_timeout(generation_config.image_timeout)
1081
- config.set_video_timeout(generation_config.video_timeout)
1082
-
1083
- # Reload debug config
1084
- debug_config = await self.get_debug_config()
1085
- if debug_config:
1086
- config.set_debug_enabled(debug_config.enabled)
1087
-
1088
- # Reload captcha config
1089
- captcha_config = await self.get_captcha_config()
1090
- if captcha_config:
1091
- config.set_captcha_method(captcha_config.captcha_method)
1092
- config.set_yescaptcha_api_key(captcha_config.yescaptcha_api_key)
1093
- config.set_yescaptcha_base_url(captcha_config.yescaptcha_base_url)
1094
- except Exception as e:
1095
- print(f"Error reloading config to memory: {e}")
1096
- # 不要让异常传播,避免返回 500 错误
1097
 
1098
  # Cache config operations
1099
  async def get_cache_config(self) -> CacheConfig:
@@ -1214,6 +1217,12 @@ class Database:
1214
  captcha_method: str = None,
1215
  yescaptcha_api_key: str = None,
1216
  yescaptcha_base_url: str = None,
 
 
 
 
 
 
1217
  browser_proxy_enabled: bool = None,
1218
  browser_proxy_url: str = None
1219
  ):
@@ -1226,28 +1235,47 @@ class Database:
1226
  if row:
1227
  current = dict(row)
1228
  new_method = captcha_method if captcha_method is not None else current.get("captcha_method", "yescaptcha")
1229
- new_api_key = yescaptcha_api_key if yescaptcha_api_key is not None else current.get("yescaptcha_api_key", "")
1230
- new_base_url = yescaptcha_base_url if yescaptcha_base_url is not None else current.get("yescaptcha_base_url", "https://api.yescaptcha.com")
 
 
 
 
 
 
1231
  new_proxy_enabled = browser_proxy_enabled if browser_proxy_enabled is not None else current.get("browser_proxy_enabled", False)
1232
  new_proxy_url = browser_proxy_url if browser_proxy_url is not None else current.get("browser_proxy_url")
1233
 
1234
  await db.execute("""
1235
  UPDATE captcha_config
1236
  SET captcha_method = ?, yescaptcha_api_key = ?, yescaptcha_base_url = ?,
 
 
 
1237
  browser_proxy_enabled = ?, browser_proxy_url = ?, updated_at = CURRENT_TIMESTAMP
1238
  WHERE id = 1
1239
- """, (new_method, new_api_key, new_base_url, new_proxy_enabled, new_proxy_url))
 
1240
  else:
1241
  new_method = captcha_method if captcha_method is not None else "yescaptcha"
1242
- new_api_key = yescaptcha_api_key if yescaptcha_api_key is not None else ""
1243
- new_base_url = yescaptcha_base_url if yescaptcha_base_url is not None else "https://api.yescaptcha.com"
 
 
 
 
 
 
1244
  new_proxy_enabled = browser_proxy_enabled if browser_proxy_enabled is not None else False
1245
  new_proxy_url = browser_proxy_url
1246
 
1247
  await db.execute("""
1248
- INSERT INTO captcha_config (id, captcha_method, yescaptcha_api_key, yescaptcha_base_url, browser_proxy_enabled, browser_proxy_url)
1249
- VALUES (1, ?, ?, ?, ?, ?)
1250
- """, (new_method, new_api_key, new_base_url, new_proxy_enabled, new_proxy_url))
 
 
 
1251
 
1252
  await db.commit()
1253
 
 
172
  count = await cursor.fetchone()
173
  if count[0] == 0:
174
  await db.execute("""
175
+ INSERT INTO plugin_config (id, connection_token)
176
+ VALUES (1, '')
177
  """)
178
 
179
  async def check_and_migrate_db(self, config_dict: dict = None):
 
216
  captcha_method TEXT DEFAULT 'browser',
217
  yescaptcha_api_key TEXT DEFAULT '',
218
  yescaptcha_base_url TEXT DEFAULT 'https://api.yescaptcha.com',
219
+ capmonster_api_key TEXT DEFAULT '',
220
+ capmonster_base_url TEXT DEFAULT 'https://api.capmonster.cloud',
221
+ ezcaptcha_api_key TEXT DEFAULT '',
222
+ ezcaptcha_base_url TEXT DEFAULT 'https://api.ez-captcha.com',
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,
 
238
  CREATE TABLE plugin_config (
239
  id INTEGER PRIMARY KEY DEFAULT 1,
240
  connection_token TEXT DEFAULT '',
 
241
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
242
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
243
  )
 
283
  captcha_columns_to_add = [
284
  ("browser_proxy_enabled", "BOOLEAN DEFAULT 0"),
285
  ("browser_proxy_url", "TEXT"),
286
+ ("capmonster_api_key", "TEXT DEFAULT ''"),
287
+ ("capmonster_base_url", "TEXT DEFAULT 'https://api.capmonster.cloud'"),
288
+ ("ezcaptcha_api_key", "TEXT DEFAULT ''"),
289
+ ("ezcaptcha_base_url", "TEXT DEFAULT 'https://api.ez-captcha.com'"),
290
+ ("capsolver_api_key", "TEXT DEFAULT ''"),
291
+ ("capsolver_base_url", "TEXT DEFAULT 'https://api.capsolver.com'"),
292
  ]
293
 
294
  for col_name, col_type in captcha_columns_to_add:
 
501
  captcha_method TEXT DEFAULT 'browser',
502
  yescaptcha_api_key TEXT DEFAULT '',
503
  yescaptcha_base_url TEXT DEFAULT 'https://api.yescaptcha.com',
504
+ capmonster_api_key TEXT DEFAULT '',
505
+ capmonster_base_url TEXT DEFAULT 'https://api.capmonster.cloud',
506
+ ezcaptcha_api_key TEXT DEFAULT '',
507
+ ezcaptcha_base_url TEXT DEFAULT 'https://api.ez-captcha.com',
508
+ capsolver_api_key TEXT DEFAULT '',
509
+ capsolver_base_url TEXT DEFAULT 'https://api.capsolver.com',
510
  website_key TEXT DEFAULT '6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV',
511
  page_action TEXT DEFAULT 'FLOW_GENERATION',
512
  browser_proxy_enabled BOOLEAN DEFAULT 0,
 
521
  CREATE TABLE IF NOT EXISTS plugin_config (
522
  id INTEGER PRIMARY KEY DEFAULT 1,
523
  connection_token TEXT DEFAULT '',
 
524
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
525
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
526
  )
 
953
 
954
  async def get_generation_config(self) -> Optional[GenerationConfig]:
955
  """Get generation configuration"""
956
+ async with aiosqlite.connect(self.db_path) as db:
957
+ db.row_factory = aiosqlite.Row
958
+ cursor = await db.execute("SELECT * FROM generation_config WHERE id = 1")
959
+ row = await cursor.fetchone()
960
+ if row:
961
+ return GenerationConfig(**dict(row))
962
+ return None
 
 
 
 
 
963
 
964
  async def update_generation_config(self, image_timeout: int, video_timeout: int):
965
  """Update generation configuration"""
966
+ async with aiosqlite.connect(self.db_path) as db:
967
+ await db.execute("""
968
+ UPDATE generation_config
969
+ SET image_timeout = ?, video_timeout = ?, updated_at = CURRENT_TIMESTAMP
970
+ WHERE id = 1
971
+ """, (image_timeout, video_timeout))
972
+ await db.commit()
 
 
 
 
973
 
974
  # Request log operations
975
  async def add_request_log(self, log: RequestLog):
 
1064
  - Generation config (image_timeout, video_timeout)
1065
  - Proxy config will be handled by ProxyManager
1066
  """
1067
+ from .config import config
1068
+
1069
+ # Reload admin config
1070
+ admin_config = await self.get_admin_config()
1071
+ if admin_config:
1072
+ config.set_admin_username_from_db(admin_config.username)
1073
+ config.set_admin_password_from_db(admin_config.password)
1074
+ config.api_key = admin_config.api_key
1075
+
1076
+ # Reload cache config
1077
+ cache_config = await self.get_cache_config()
1078
+ if cache_config:
1079
+ config.set_cache_enabled(cache_config.cache_enabled)
1080
+ config.set_cache_timeout(cache_config.cache_timeout)
1081
+ config.set_cache_base_url(cache_config.cache_base_url or "")
1082
+
1083
+ # Reload generation config
1084
+ generation_config = await self.get_generation_config()
1085
+ if generation_config:
1086
+ config.set_image_timeout(generation_config.image_timeout)
1087
+ config.set_video_timeout(generation_config.video_timeout)
1088
+
1089
+ # Reload debug config
1090
+ debug_config = await self.get_debug_config()
1091
+ if debug_config:
1092
+ config.set_debug_enabled(debug_config.enabled)
1093
+
1094
+ # Reload captcha config
1095
+ captcha_config = await self.get_captcha_config()
1096
+ if captcha_config:
1097
+ config.set_captcha_method(captcha_config.captcha_method)
1098
+ config.set_yescaptcha_api_key(captcha_config.yescaptcha_api_key)
1099
+ config.set_yescaptcha_base_url(captcha_config.yescaptcha_base_url)
 
 
 
 
1100
 
1101
  # Cache config operations
1102
  async def get_cache_config(self) -> CacheConfig:
 
1217
  captcha_method: str = None,
1218
  yescaptcha_api_key: str = None,
1219
  yescaptcha_base_url: str = None,
1220
+ capmonster_api_key: str = None,
1221
+ capmonster_base_url: str = None,
1222
+ ezcaptcha_api_key: str = None,
1223
+ ezcaptcha_base_url: str = None,
1224
+ capsolver_api_key: str = None,
1225
+ capsolver_base_url: str = None,
1226
  browser_proxy_enabled: bool = None,
1227
  browser_proxy_url: str = None
1228
  ):
 
1235
  if row:
1236
  current = dict(row)
1237
  new_method = captcha_method if captcha_method is not None else current.get("captcha_method", "yescaptcha")
1238
+ new_yes_key = yescaptcha_api_key if yescaptcha_api_key is not None else current.get("yescaptcha_api_key", "")
1239
+ new_yes_url = yescaptcha_base_url if yescaptcha_base_url is not None else current.get("yescaptcha_base_url", "https://api.yescaptcha.com")
1240
+ new_cap_key = capmonster_api_key if capmonster_api_key is not None else current.get("capmonster_api_key", "")
1241
+ new_cap_url = capmonster_base_url if capmonster_base_url is not None else current.get("capmonster_base_url", "https://api.capmonster.cloud")
1242
+ new_ez_key = ezcaptcha_api_key if ezcaptcha_api_key is not None else current.get("ezcaptcha_api_key", "")
1243
+ new_ez_url = ezcaptcha_base_url if ezcaptcha_base_url is not None else current.get("ezcaptcha_base_url", "https://api.ez-captcha.com")
1244
+ new_cs_key = capsolver_api_key if capsolver_api_key is not None else current.get("capsolver_api_key", "")
1245
+ new_cs_url = capsolver_base_url if capsolver_base_url is not None else current.get("capsolver_base_url", "https://api.capsolver.com")
1246
  new_proxy_enabled = browser_proxy_enabled if browser_proxy_enabled is not None else current.get("browser_proxy_enabled", False)
1247
  new_proxy_url = browser_proxy_url if browser_proxy_url is not None else current.get("browser_proxy_url")
1248
 
1249
  await db.execute("""
1250
  UPDATE captcha_config
1251
  SET captcha_method = ?, yescaptcha_api_key = ?, yescaptcha_base_url = ?,
1252
+ capmonster_api_key = ?, capmonster_base_url = ?,
1253
+ ezcaptcha_api_key = ?, ezcaptcha_base_url = ?,
1254
+ capsolver_api_key = ?, capsolver_base_url = ?,
1255
  browser_proxy_enabled = ?, browser_proxy_url = ?, updated_at = CURRENT_TIMESTAMP
1256
  WHERE id = 1
1257
+ """, (new_method, new_yes_key, new_yes_url, new_cap_key, new_cap_url,
1258
+ new_ez_key, new_ez_url, new_cs_key, new_cs_url, new_proxy_enabled, new_proxy_url))
1259
  else:
1260
  new_method = captcha_method if captcha_method is not None else "yescaptcha"
1261
+ new_yes_key = yescaptcha_api_key if yescaptcha_api_key is not None else ""
1262
+ new_yes_url = yescaptcha_base_url if yescaptcha_base_url is not None else "https://api.yescaptcha.com"
1263
+ new_cap_key = capmonster_api_key if capmonster_api_key is not None else ""
1264
+ new_cap_url = capmonster_base_url if capmonster_base_url is not None else "https://api.capmonster.cloud"
1265
+ new_ez_key = ezcaptcha_api_key if ezcaptcha_api_key is not None else ""
1266
+ new_ez_url = ezcaptcha_base_url if ezcaptcha_base_url is not None else "https://api.ez-captcha.com"
1267
+ new_cs_key = capsolver_api_key if capsolver_api_key is not None else ""
1268
+ new_cs_url = capsolver_base_url if capsolver_base_url is not None else "https://api.capsolver.com"
1269
  new_proxy_enabled = browser_proxy_enabled if browser_proxy_enabled is not None else False
1270
  new_proxy_url = browser_proxy_url
1271
 
1272
  await db.execute("""
1273
+ INSERT INTO captcha_config (id, captcha_method, yescaptcha_api_key, yescaptcha_base_url,
1274
+ capmonster_api_key, capmonster_base_url, ezcaptcha_api_key, ezcaptcha_base_url,
1275
+ capsolver_api_key, capsolver_base_url, browser_proxy_enabled, browser_proxy_url)
1276
+ VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1277
+ """, (new_method, new_yes_key, new_yes_url, new_cap_key, new_cap_url,
1278
+ new_ez_key, new_ez_url, new_cs_key, new_cs_url, new_proxy_enabled, new_proxy_url))
1279
 
1280
  await db.commit()
1281
 
src/core/models.py CHANGED
@@ -147,9 +147,15 @@ class DebugConfig(BaseModel):
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
  browser_proxy_enabled: bool = False # 浏览器打码是否启用代理
 
147
  class CaptchaConfig(BaseModel):
148
  """Captcha configuration"""
149
  id: int = 1
150
+ captcha_method: str = "browser" # yescaptcha, capmonster, ezcaptcha, capsolver 或 browser
151
  yescaptcha_api_key: str = ""
152
  yescaptcha_base_url: str = "https://api.yescaptcha.com"
153
+ capmonster_api_key: str = ""
154
+ capmonster_base_url: str = "https://api.capmonster.cloud"
155
+ ezcaptcha_api_key: str = ""
156
+ ezcaptcha_base_url: str = "https://api.ez-captcha.com"
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 # 浏览器打码是否启用代理
src/main.py CHANGED
@@ -68,66 +68,48 @@ async def lifespan(app: FastAPI):
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 == "personal":
78
- try:
79
- from .services.browser_captcha_personal import PLAYWRIGHT_AVAILABLE
80
- if PLAYWRIGHT_AVAILABLE:
81
- from .services.browser_captcha_personal import BrowserCaptchaService
82
- browser_service = await BrowserCaptchaService.get_instance(db)
83
- await browser_service.open_login_window()
84
- print("✓ Browser captcha service initialized (webui mode)")
85
- else:
86
- print("⚠️ Playwright not available. Please use YesCaptcha service instead.")
87
- # 自动切换到 yescaptcha 方法
88
- await db.update_captcha_config(
89
- captcha_method="yescaptcha",
90
- yescaptcha_api_key=captcha_config.yescaptcha_api_key,
91
- yescaptcha_base_url=captcha_config.yescaptcha_base_url
92
- )
93
- print("✓ Captcha method automatically switched to yescaptcha")
94
- except ImportError:
95
- print("⚠️ Playwright not available. Please use YesCaptcha service instead.")
96
- # 自动切换到 yescaptcha 方法
97
- await db.update_captcha_config(
98
- captcha_method="yescaptcha",
99
- yescaptcha_api_key=captcha_config.yescaptcha_api_key,
100
- yescaptcha_base_url=captcha_config.yescaptcha_base_url
101
- )
102
- print("✓ Captcha method automatically switched to yescaptcha")
103
  elif captcha_config.captcha_method == "browser":
104
- try:
105
- from .services.browser_captcha import PLAYWRIGHT_AVAILABLE
106
- if PLAYWRIGHT_AVAILABLE:
107
- from .services.browser_captcha import BrowserCaptchaService
108
- browser_service = await BrowserCaptchaService.get_instance(db)
109
- print("✓ Browser captcha service initialized (headless mode)")
110
- else:
111
- print("⚠️ Playwright not available. Please use YesCaptcha service instead.")
112
- # 自动切换到 yescaptcha 方法
113
- await db.update_captcha_config(
114
- captcha_method="yescaptcha",
115
- yescaptcha_api_key=captcha_config.yescaptcha_api_key,
116
- yescaptcha_base_url=captcha_config.yescaptcha_base_url
117
- )
118
- print("✓ Captcha method automatically switched to yescaptcha")
119
- except ImportError:
120
- print("⚠️ Playwright not available. Please use YesCaptcha service instead.")
121
- # 自动切换到 yescaptcha 方法
122
- await db.update_captcha_config(
123
- captcha_method="yescaptcha",
124
- yescaptcha_api_key=captcha_config.yescaptcha_api_key,
125
- yescaptcha_base_url=captcha_config.yescaptcha_base_url
126
- )
127
- print("✓ Captcha method automatically switched to yescaptcha")
128
 
129
  # Initialize concurrency manager
130
  tokens = await token_manager.get_all_tokens()
 
131
  await concurrency_manager.initialize(tokens)
132
 
133
  # Start file cache cleanup task
@@ -177,7 +159,7 @@ async def lifespan(app: FastAPI):
177
  # Initialize components
178
  db = Database()
179
  proxy_manager = ProxyManager(db)
180
- flow_client = FlowClient(proxy_manager)
181
  token_manager = TokenManager(db, flow_client)
182
  concurrency_manager = ConcurrencyManager()
183
  load_balancer = LoadBalancer(token_manager, concurrency_manager)
 
68
 
69
  # Load captcha configuration from database
70
  captcha_config = await db.get_captcha_config()
71
+
72
  config.set_captcha_method(captcha_config.captcha_method)
73
  config.set_yescaptcha_api_key(captcha_config.yescaptcha_api_key)
74
  config.set_yescaptcha_base_url(captcha_config.yescaptcha_base_url)
75
+ config.set_capmonster_api_key(captcha_config.capmonster_api_key)
76
+ config.set_capmonster_base_url(captcha_config.capmonster_base_url)
77
+ config.set_ezcaptcha_api_key(captcha_config.ezcaptcha_api_key)
78
+ config.set_ezcaptcha_base_url(captcha_config.ezcaptcha_base_url)
79
+ config.set_capsolver_api_key(captcha_config.capsolver_api_key)
80
+ config.set_capsolver_base_url(captcha_config.capsolver_base_url)
81
 
82
  # Initialize browser captcha service if needed
83
  browser_service = None
84
  if captcha_config.captcha_method == "personal":
85
+ from .services.browser_captcha_personal import BrowserCaptchaService
86
+ browser_service = await BrowserCaptchaService.get_instance(db)
87
+ print("✓ Browser captcha service initialized (nodriver mode)")
88
+
89
+ # 启动常驻模式:从第一个可用token获取project_id
90
+ tokens = await token_manager.get_all_tokens()
91
+ resident_project_id = None
92
+ for t in tokens:
93
+ if t.current_project_id and t.is_active:
94
+ resident_project_id = t.current_project_id
95
+ break
96
+
97
+ if resident_project_id:
98
+ # 直接启动常驻模式(会自动导航到项目页面,cookie已持久化)
99
+ await browser_service.start_resident_mode(resident_project_id)
100
+ print(f"✓ Browser captcha resident mode started (project: {resident_project_id[:8]}...)")
101
+ else:
102
+ # 没有可用的project_id时,打开登录窗口供用户手动操作
103
+ await browser_service.open_login_window()
104
+ print("⚠ No active token with project_id found, opened login window for manual setup")
 
 
 
 
 
105
  elif captcha_config.captcha_method == "browser":
106
+ from .services.browser_captcha import BrowserCaptchaService
107
+ browser_service = await BrowserCaptchaService.get_instance(db)
108
+ print("✓ Browser captcha service initialized (headless mode)")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
 
110
  # Initialize concurrency manager
111
  tokens = await token_manager.get_all_tokens()
112
+
113
  await concurrency_manager.initialize(tokens)
114
 
115
  # Start file cache cleanup task
 
159
  # Initialize components
160
  db = Database()
161
  proxy_manager = ProxyManager(db)
162
+ flow_client = FlowClient(proxy_manager, db)
163
  token_manager = TokenManager(db, flow_client)
164
  concurrency_manager = ConcurrencyManager()
165
  load_balancer = LoadBalancer(token_manager, concurrency_manager)
src/services/browser_captcha.py CHANGED
@@ -6,16 +6,10 @@ import asyncio
6
  import time
7
  import re
8
  from typing import Optional, Dict
 
9
 
10
  from ..core.logger import debug_logger
11
 
12
- # Conditionally import playwright
13
- try:
14
- from playwright.async_api import async_playwright, Browser, BrowserContext
15
- PLAYWRIGHT_AVAILABLE = True
16
- except ImportError:
17
- PLAYWRIGHT_AVAILABLE = False
18
-
19
 
20
  def parse_proxy_url(proxy_url: str) -> Optional[Dict[str, str]]:
21
  """解析代理URL,分离协议、主机、端口、认证信息
@@ -111,11 +105,6 @@ class BrowserCaptchaService:
111
  return
112
 
113
  try:
114
- # 检查 Playwright 是否可用
115
- if not PLAYWRIGHT_AVAILABLE:
116
- debug_logger.log_error("[BrowserCaptcha] ❌ Playwright 不可用,请使用 YesCaptcha 服务")
117
- raise ImportError("Playwright 未安装,请使用 YesCaptcha 服务")
118
-
119
  # 获取浏览器专用代理配置
120
  proxy_url = None
121
  if self.db:
 
6
  import time
7
  import re
8
  from typing import Optional, Dict
9
+ from playwright.async_api import async_playwright, Browser, BrowserContext
10
 
11
  from ..core.logger import debug_logger
12
 
 
 
 
 
 
 
 
13
 
14
  def parse_proxy_url(proxy_url: str) -> Optional[Dict[str, str]]:
15
  """解析代理URL,分离协议、主机、端口、认证信息
 
105
  return
106
 
107
  try:
 
 
 
 
 
108
  # 获取浏览器专用代理配置
109
  proxy_url = None
110
  if self.db:
src/services/browser_captcha_personal.py CHANGED
@@ -1,208 +1,677 @@
 
 
 
 
 
1
  import asyncio
2
  import time
3
- import re
4
  import os
5
- from typing import Optional, Dict
 
 
6
 
7
  from ..core.logger import debug_logger
8
 
9
- # Conditionally import playwright
10
- try:
11
- from playwright.async_api import async_playwright, BrowserContext, Page
12
- PLAYWRIGHT_AVAILABLE = True
13
- except ImportError:
14
- PLAYWRIGHT_AVAILABLE = False
15
-
16
- # ... (保持原来的 parse_proxy_url 和 validate_browser_proxy_url 函数不变) ...
17
- def parse_proxy_url(proxy_url: str) -> Optional[Dict[str, str]]:
18
- """解析代理URL,分离协议、主机、端口、认证信息"""
19
- proxy_pattern = r'^(socks5|http|https)://(?:([^:]+):([^@]+)@)?([^:]+):(\d+)$'
20
- match = re.match(proxy_pattern, proxy_url)
21
- if match:
22
- protocol, username, password, host, port = match.groups()
23
- proxy_config = {'server': f'{protocol}://{host}:{port}'}
24
- if username and password:
25
- proxy_config['username'] = username
26
- proxy_config['password'] = password
27
- return proxy_config
28
- return None
29
 
30
  class BrowserCaptchaService:
31
- """浏览器自动化获取 reCAPTCHA token(持久化有头模式)"""
 
 
 
 
 
32
 
33
  _instance: Optional['BrowserCaptchaService'] = None
34
  _lock = asyncio.Lock()
35
 
36
  def __init__(self, db=None):
37
  """初始化服务"""
38
- # === 修改点 1: 设置为有头模式 ===
39
- self.headless = False
40
- self.playwright = None
41
- # 注意: 持久化模式下,我们操作的是 context 而不是 browser
42
- self.context: Optional[BrowserContext] = None
43
  self._initialized = False
44
  self.website_key = "6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV"
45
  self.db = db
46
-
47
- # === 修改点 2: 指定本地数据存储目录 ===
48
- # 这会在脚本运行目录下生成 browser_data 文件夹,用于保存你的登录状态
49
  self.user_data_dir = os.path.join(os.getcwd(), "browser_data")
 
 
 
 
 
 
 
 
 
 
50
 
51
  @classmethod
52
  async def get_instance(cls, db=None) -> 'BrowserCaptchaService':
 
53
  if cls._instance is None:
54
  async with cls._lock:
55
  if cls._instance is None:
56
  cls._instance = cls(db)
57
- # 首次调用不强制初始化,等待 get_token 时懒加载,或者可以在这里await
58
  return cls._instance
59
 
60
  async def initialize(self):
61
- """初始化持久化浏览器上下文"""
62
- if self._initialized and self.context:
63
- return
 
 
 
 
 
 
 
 
 
 
64
 
65
  try:
66
- # 检查 Playwright 是否可
67
- if not PLAYWRIGHT_AVAILABLE:
68
- debug_logger.log_error("[BrowserCaptcha] Playwright 不可用,请使用 YesCaptcha 服务")
69
- raise ImportError("Playwright 未安装,请使用 YesCaptcha 服务")
70
-
71
- proxy_url = None
72
- if self.db:
73
- captcha_config = await self.db.get_captcha_config()
74
- if captcha_config.browser_proxy_enabled and captcha_config.browser_proxy_url:
75
- proxy_url = captcha_config.browser_proxy_url
76
-
77
- debug_logger.log_info(f"[BrowserCaptcha] 正在启动浏览器 (用户数据目录: {self.user_data_dir})...")
78
- self.playwright = await async_playwright().start()
79
-
80
- # 配置启动参数
81
- launch_options = {
82
- 'headless': self.headless,
83
- 'user_data_dir': self.user_data_dir, # 指定数据目录
84
- 'viewport': {'width': 1280, 'height': 720}, # 设置默认窗口大小
85
- 'args': [
86
- '--disable-blink-features=AutomationControlled',
87
- '--disable-infobars',
88
  '--no-sandbox',
 
89
  '--disable-setuid-sandbox',
 
 
 
90
  ]
91
- }
92
-
93
- # 代理配置
94
- if proxy_url:
95
- proxy_config = parse_proxy_url(proxy_url)
96
- if proxy_config:
97
- launch_options['proxy'] = proxy_config
98
- debug_logger.log_info(f"[BrowserCaptcha] 使用代理: {proxy_config['server']}")
99
-
100
- # === 修改点 3: 使用 launch_persistent_context ===
101
- # 这会启动一个带有状态的浏览器窗口
102
- self.context = await self.playwright.chromium.launch_persistent_context(**launch_options)
103
-
104
- # 设置默认超时
105
- self.context.set_default_timeout(30000)
106
 
107
  self._initialized = True
108
- debug_logger.log_info(f"[BrowserCaptcha] ✅ 浏览器已启动 (Profile: {self.user_data_dir})")
109
-
110
  except Exception as e:
111
  debug_logger.log_error(f"[BrowserCaptcha] ❌ 浏览器启动失败: {str(e)}")
112
  raise
113
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  async def get_token(self, project_id: str) -> Optional[str]:
115
- """获取 reCAPTCHA token"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  # 确保浏览器已启动
117
- if not self._initialized or not self.context:
118
  await self.initialize()
119
 
120
  start_time = time.time()
121
- page: Optional[Page] = None
122
 
123
  try:
124
- # === 修改点 4: 在现有上下文中新建标签页,而不是新建上下文 ===
125
- # 这样可以复用该上下文中已保存的 Cookie (你的登录状态)
126
- page = await self.context.new_page()
127
-
128
  website_url = f"https://labs.google/fx/tools/flow/project/{project_id}"
129
- debug_logger.log_info(f"[BrowserCaptcha] 访问页面: {website_url}")
130
 
131
- # 访问页面
132
- try:
133
- await page.goto(website_url, wait_until="domcontentloaded")
134
- except Exception as e:
135
- debug_logger.log_warning(f"[BrowserCaptcha] 页面加载警告: {str(e)}")
136
-
137
- # --- 关键点:如果需要人工介入 ---
138
- # 你可以在这里加入一段逻辑,如果是第一次运行,或者检测到未登录,
139
- # 可以暂停脚本,等你手动操作完再继续。
140
- # 例如: await asyncio.sleep(30)
141
-
142
- # ... (中间注入脚本和执行 reCAPTCHA 的代码逻辑与原版完全一致,此处省略以节省篇幅) ...
143
- # ... 请将原代码中从 "检查并注入 reCAPTCHA v3 脚本" 到 token 获取部分的代码复制到这里 ...
144
-
145
- # 这里为了演示,简写注入逻辑(请保留你原有的完整注入逻辑):
146
- script_loaded = await page.evaluate("() => { return !!(window.grecaptcha && window.grecaptcha.execute); }")
147
- if not script_loaded:
148
- await page.evaluate(f"""
149
- () => {{
150
- const script = document.createElement('script');
151
- script.src = 'https://www.google.com/recaptcha/api.js?render={self.website_key}';
152
- script.async = true; script.defer = true;
153
- document.head.appendChild(script);
154
- }}
155
- """)
156
- # 等待加载... (保留你原有的等待循环)
157
- await page.wait_for_timeout(2000)
158
-
159
- # 执行获取 Token (保留你原有的 execute 逻辑)
160
- token = await page.evaluate(f"""
161
- async () => {{
162
- try {{
163
- return await window.grecaptcha.execute('{self.website_key}', {{ action: 'FLOW_GENERATION' }});
164
- }} catch (e) {{ return null; }}
165
- }}
166
- """)
167
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  if token:
169
- debug_logger.log_info(f"[BrowserCaptcha] ✅ Token获取成功")
170
  return token
171
  else:
172
- debug_logger.log_error("[BrowserCaptcha] Token获取失败")
173
  return None
174
 
175
  except Exception as e:
176
- debug_logger.log_error(f"[BrowserCaptcha] 异常: {str(e)}")
177
  return None
178
  finally:
179
- # === 修改点 5: 只关闭 Page (标签页),不关闭 Context (浏览器窗口) ===
180
- if page:
181
  try:
182
- await page.close()
183
- except:
184
  pass
185
 
186
  async def close(self):
187
- """完全关闭浏览器(清理资源时调用)"""
 
 
 
188
  try:
189
- if self.context:
190
- await self.context.close() # 这会关闭整个浏览器窗口
191
- self.context = None
192
-
193
- if self.playwright:
194
- await self.playwright.stop()
195
- self.playwright = None
196
-
197
  self._initialized = False
198
- debug_logger.log_info("[BrowserCaptcha] 浏览器服务已关闭")
 
199
  except Exception as e:
200
- debug_logger.log_error(f"[BrowserCaptcha] 关闭异常: {str(e)}")
201
 
202
- # 增加一个辅助方法,用于手动登录
203
  async def open_login_window(self):
204
- """调用此方法打开一个永久窗口供登录Google"""
205
  await self.initialize()
206
- page = await self.context.new_page()
207
- await page.goto("https://accounts.google.com/")
208
- print("请在打开的浏览器中登录账号。登录完成后,无需关闭浏览器,脚本下次运行时会自动使用此状态。")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 浏览器自动化获取 reCAPTCHA token
3
+ 使用 nodriver (undetected-chromedriver 继任者) 实现反检测浏览器
4
+ 支持常驻模式:为每个 project_id 自动创建常驻标签页,即时生成 token
5
+ """
6
  import asyncio
7
  import time
 
8
  import os
9
+ from typing import Optional
10
+
11
+ import nodriver as uc
12
 
13
  from ..core.logger import debug_logger
14
 
15
+
16
+ class ResidentTabInfo:
17
+ """常驻标签页信息结构"""
18
+ def __init__(self, tab, project_id: str):
19
+ self.tab = tab
20
+ self.project_id = project_id
21
+ self.recaptcha_ready = False
22
+ self.created_at = time.time()
23
+
 
 
 
 
 
 
 
 
 
 
 
24
 
25
  class BrowserCaptchaService:
26
+ """浏览器自动化获取 reCAPTCHA token(nodriver 有头模式)
27
+
28
+ 支持两种模式:
29
+ 1. 常驻模式 (Resident Mode): 为每个 project_id 保持常驻标签页,即时生成 token
30
+ 2. 传统模式 (Legacy Mode): 每次请求创建新标签页 (fallback)
31
+ """
32
 
33
  _instance: Optional['BrowserCaptchaService'] = None
34
  _lock = asyncio.Lock()
35
 
36
  def __init__(self, db=None):
37
  """初始化服务"""
38
+ self.headless = False # nodriver 有头模式
39
+ self.browser = None
 
 
 
40
  self._initialized = False
41
  self.website_key = "6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV"
42
  self.db = db
43
+ # 持久化 profile 目录
 
 
44
  self.user_data_dir = os.path.join(os.getcwd(), "browser_data")
45
+
46
+ # 常驻模式相关属性 (支持多 project_id)
47
+ self._resident_tabs: dict[str, 'ResidentTabInfo'] = {} # project_id -> 常驻标签页信息
48
+ self._resident_lock = asyncio.Lock() # 保护常驻标签页操作
49
+
50
+ # 兼容旧 API(保留 single resident 属性作为别名)
51
+ self.resident_project_id: Optional[str] = None # 向后兼容
52
+ self.resident_tab = None # 向后兼容
53
+ self._running = False # 向后兼容
54
+ self._recaptcha_ready = False # 向后兼容
55
 
56
  @classmethod
57
  async def get_instance(cls, db=None) -> 'BrowserCaptchaService':
58
+ """获取单例实例"""
59
  if cls._instance is None:
60
  async with cls._lock:
61
  if cls._instance is None:
62
  cls._instance = cls(db)
 
63
  return cls._instance
64
 
65
  async def initialize(self):
66
+ """初始化 nodriver 浏览器"""
67
+ if self._initialized and self.browser:
68
+ # 检查浏览器是否仍然存活
69
+ try:
70
+ # 尝试获取浏览器信息验证存活
71
+ if self.browser.stopped:
72
+ debug_logger.log_warning("[BrowserCaptcha] 浏览器已停止,重新初始化...")
73
+ self._initialized = False
74
+ else:
75
+ return
76
+ except Exception:
77
+ debug_logger.log_warning("[BrowserCaptcha] 浏览器无响应,重新初始化...")
78
+ self._initialized = False
79
 
80
  try:
81
+ debug_logger.log_info(f"[BrowserCaptcha] 正在启动 nodriver 浏览器 (户数据目录: {self.user_data_dir})...")
82
+
83
+ # 确保 user_data_dir 存在
84
+ os.makedirs(self.user_data_dir, exist_ok=True)
85
+
86
+ # 启动 nodriver 浏览器
87
+ self.browser = await uc.start(
88
+ headless=self.headless,
89
+ user_data_dir=self.user_data_dir,
90
+ sandbox=False, # nodriver 需要此参数来禁用 sandbox
91
+ browser_args=[
 
 
 
 
 
 
 
 
 
 
 
92
  '--no-sandbox',
93
+ '--disable-dev-shm-usage',
94
  '--disable-setuid-sandbox',
95
+ '--disable-gpu',
96
+ '--window-size=1280,720',
97
+ '--profile-directory=Default', # 跳过 Profile 选择器页面
98
  ]
99
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
 
101
  self._initialized = True
102
+ debug_logger.log_info(f"[BrowserCaptcha] ✅ nodriver 浏览器已启动 (Profile: {self.user_data_dir})")
103
+
104
  except Exception as e:
105
  debug_logger.log_error(f"[BrowserCaptcha] ❌ 浏览器启动失败: {str(e)}")
106
  raise
107
 
108
+ # ========== 常驻模式 API ==========
109
+
110
+ async def start_resident_mode(self, project_id: str):
111
+ """启动常驻模式
112
+
113
+ Args:
114
+ project_id: 用于常驻的项目 ID
115
+ """
116
+ if self._running:
117
+ debug_logger.log_warning("[BrowserCaptcha] 常驻模式已在运行")
118
+ return
119
+
120
+ await self.initialize()
121
+
122
+ self.resident_project_id = project_id
123
+ website_url = f"https://labs.google/fx/tools/flow/project/{project_id}"
124
+
125
+ debug_logger.log_info(f"[BrowserCaptcha] 启动常驻模式,访问页面: {website_url}")
126
+
127
+ # 创建一个独立的新标签页(不使用 main_tab,避免被回收)
128
+ self.resident_tab = await self.browser.get(website_url, new_tab=True)
129
+
130
+ debug_logger.log_info("[BrowserCaptcha] 标签页已创建,等待页面加载...")
131
+
132
+ # 等待页面加载完成(带重试机制)
133
+ page_loaded = False
134
+ for retry in range(60):
135
+ try:
136
+ await asyncio.sleep(1)
137
+ ready_state = await self.resident_tab.evaluate("document.readyState")
138
+ debug_logger.log_info(f"[BrowserCaptcha] 页面状态: {ready_state} (重试 {retry + 1}/60)")
139
+ if ready_state == "complete":
140
+ page_loaded = True
141
+ break
142
+ except ConnectionRefusedError as e:
143
+ debug_logger.log_warning(f"[BrowserCaptcha] 标签页连接丢失: {e},尝试重新获取...")
144
+ # 标签页可能已关闭,尝试重新创建
145
+ try:
146
+ self.resident_tab = await self.browser.get(website_url, new_tab=True)
147
+ debug_logger.log_info("[BrowserCaptcha] 已重新创建标签页")
148
+ except Exception as e2:
149
+ debug_logger.log_error(f"[BrowserCaptcha] 重新创建标签页失败: {e2}")
150
+ await asyncio.sleep(2)
151
+ except Exception as e:
152
+ debug_logger.log_warning(f"[BrowserCaptcha] 等待页面异常: {e},重试 {retry + 1}/15...")
153
+ await asyncio.sleep(2)
154
+
155
+ if not page_loaded:
156
+ debug_logger.log_error("[BrowserCaptcha] 页面加载超时,常驻模式启动失败")
157
+ return
158
+
159
+ # 等待 reCAPTCHA 加载
160
+ self._recaptcha_ready = await self._wait_for_recaptcha(self.resident_tab)
161
+
162
+ if not self._recaptcha_ready:
163
+ debug_logger.log_error("[BrowserCaptcha] reCAPTCHA 加载失败,常驻模式启动失败")
164
+ return
165
+
166
+ self._running = True
167
+ debug_logger.log_info(f"[BrowserCaptcha] ✅ 常驻模式已启动 (project: {project_id})")
168
+
169
+ async def stop_resident_mode(self, project_id: Optional[str] = None):
170
+ """停止常驻模式
171
+
172
+ Args:
173
+ project_id: 指定要关闭的 project_id,如果为 None 则关闭所有常驻标签页
174
+ """
175
+ async with self._resident_lock:
176
+ if project_id:
177
+ # 关闭指定的常驻标签页
178
+ await self._close_resident_tab(project_id)
179
+ debug_logger.log_info(f"[BrowserCaptcha] 已关闭 project_id={project_id} 的常驻模式")
180
+ else:
181
+ # 关闭所有常驻标签页
182
+ project_ids = list(self._resident_tabs.keys())
183
+ for pid in project_ids:
184
+ resident_info = self._resident_tabs.pop(pid, None)
185
+ if resident_info and resident_info.tab:
186
+ try:
187
+ await resident_info.tab.close()
188
+ except Exception:
189
+ pass
190
+ debug_logger.log_info(f"[BrowserCaptcha] 已关闭所有常驻标签页 (共 {len(project_ids)} 个)")
191
+
192
+ # 向后兼容:清理旧属性
193
+ if not self._running:
194
+ return
195
+
196
+ self._running = False
197
+ if self.resident_tab:
198
+ try:
199
+ await self.resident_tab.close()
200
+ except Exception:
201
+ pass
202
+ self.resident_tab = None
203
+
204
+ self.resident_project_id = None
205
+ self._recaptcha_ready = False
206
+
207
+ async def _wait_for_recaptcha(self, tab) -> bool:
208
+ """等待 reCAPTCHA 加载
209
+
210
+ Returns:
211
+ True if reCAPTCHA loaded successfully
212
+ """
213
+ debug_logger.log_info("[BrowserCaptcha] 检测 reCAPTCHA...")
214
+
215
+ # 检查 grecaptcha.enterprise.execute
216
+ is_enterprise = await tab.evaluate(
217
+ "typeof grecaptcha !== 'undefined' && typeof grecaptcha.enterprise !== 'undefined' && typeof grecaptcha.enterprise.execute === 'function'"
218
+ )
219
+
220
+ if is_enterprise:
221
+ debug_logger.log_info("[BrowserCaptcha] reCAPTCHA Enterprise 已加载")
222
+ return True
223
+
224
+ # 尝试注入脚本
225
+ debug_logger.log_info("[BrowserCaptcha] 未检测到 reCAPTCHA,注入脚本...")
226
+
227
+ await tab.evaluate(f"""
228
+ (() => {{
229
+ if (document.querySelector('script[src*="recaptcha"]')) return;
230
+ const script = document.createElement('script');
231
+ script.src = 'https://www.google.com/recaptcha/api.js?render={self.website_key}';
232
+ script.async = true;
233
+ document.head.appendChild(script);
234
+ }})()
235
+ """)
236
+
237
+ # 等待脚本加载
238
+ await tab.sleep(3)
239
+
240
+ # 轮询等待 reCAPTCHA 加载
241
+ for i in range(20):
242
+ is_enterprise = await tab.evaluate(
243
+ "typeof grecaptcha !== 'undefined' && typeof grecaptcha.enterprise !== 'undefined' && typeof grecaptcha.enterprise.execute === 'function'"
244
+ )
245
+
246
+ if is_enterprise:
247
+ debug_logger.log_info(f"[BrowserCaptcha] reCAPTCHA Enterprise 已加载(等待了 {i * 0.5} 秒)")
248
+ return True
249
+ await tab.sleep(0.5)
250
+
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
262
+ """
263
+ # 生成唯一变量名避免冲突
264
+ ts = int(time.time() * 1000)
265
+ token_var = f"_recaptcha_token_{ts}"
266
+ error_var = f"_recaptcha_error_{ts}"
267
+
268
+ execute_script = f"""
269
+ (() => {{
270
+ window.{token_var} = null;
271
+ window.{error_var} = null;
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
+ }})
279
+ .catch(function(err) {{
280
+ window.{error_var} = err.message || 'execute failed';
281
+ }});
282
+ }});
283
+ }} catch (e) {{
284
+ window.{error_var} = e.message || 'exception';
285
+ }}
286
+ }})()
287
+ """
288
+
289
+ # 注入执行脚本
290
+ await tab.evaluate(execute_script)
291
+
292
+ # 轮询等待结果(最多 15 秒)
293
+ token = None
294
+ for i in range(30):
295
+ await tab.sleep(0.5)
296
+ token = await tab.evaluate(f"window.{token_var}")
297
+ if token:
298
+ break
299
+ error = await tab.evaluate(f"window.{error_var}")
300
+ if error:
301
+ debug_logger.log_error(f"[BrowserCaptcha] reCAPTCHA 错误: {error}")
302
+ break
303
+
304
+ # 清理临时变量
305
+ try:
306
+ await tab.evaluate(f"delete window.{token_var}; delete window.{error_var};")
307
+ except:
308
+ pass
309
+
310
+ return token
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
324
+ """
325
+ # 确保浏览器已初始化
326
+ await self.initialize()
327
+
328
+ # 尝试从常驻标签页获取 token
329
+ async with self._resident_lock:
330
+ resident_info = self._resident_tabs.get(project_id)
331
+
332
+ # 如果该 project_id 没有常驻标签页,则自动创建
333
+ if resident_info is None:
334
+ debug_logger.log_info(f"[BrowserCaptcha] project_id={project_id} 没有常驻标签页,正在创建...")
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)")
351
+ return token
352
+ else:
353
+ debug_logger.log_warning(f"[BrowserCaptcha] 常驻标签页生成失败 (project: {project_id}),尝试重建...")
354
+ except Exception as e:
355
+ debug_logger.log_warning(f"[BrowserCaptcha] 常驻标签页异常: {e},尝试重建...")
356
+
357
+ # 常驻标签页失效,尝试重建
358
+ async with self._resident_lock:
359
+ await self._close_resident_tab(project_id)
360
+ resident_info = await self._create_resident_tab(project_id)
361
+ if resident_info:
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
369
+ except Exception:
370
+ pass
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 创建常驻标签页
378
+
379
+ Args:
380
+ project_id: 项目 ID
381
+
382
+ Returns:
383
+ ResidentTabInfo 对象,或 None(创建失败)
384
+ """
385
+ try:
386
+ website_url = f"https://labs.google/fx/tools/flow/project/{project_id}"
387
+ debug_logger.log_info(f"[BrowserCaptcha] 为 project_id={project_id} 创建常驻标签页,访问: {website_url}")
388
+
389
+ # 创建新标签页
390
+ tab = await self.browser.get(website_url, new_tab=True)
391
+
392
+ # 等待页面加载完成
393
+ page_loaded = False
394
+ for retry in range(60):
395
+ try:
396
+ await asyncio.sleep(1)
397
+ ready_state = await tab.evaluate("document.readyState")
398
+ if ready_state == "complete":
399
+ page_loaded = True
400
+ break
401
+ except ConnectionRefusedError as e:
402
+ debug_logger.log_warning(f"[BrowserCaptcha] 标签页连接丢失: {e}")
403
+ return None
404
+ except Exception as e:
405
+ debug_logger.log_warning(f"[BrowserCaptcha] 等待页面异常: {e},重试 {retry + 1}/60...")
406
+ await asyncio.sleep(1)
407
+
408
+ if not page_loaded:
409
+ debug_logger.log_error(f"[BrowserCaptcha] 页面加载超时 (project: {project_id})")
410
+ try:
411
+ await tab.close()
412
+ except:
413
+ pass
414
+ return None
415
+
416
+ # 等待 reCAPTCHA 加载
417
+ recaptcha_ready = await self._wait_for_recaptcha(tab)
418
+
419
+ if not recaptcha_ready:
420
+ debug_logger.log_error(f"[BrowserCaptcha] reCAPTCHA 加载失败 (project: {project_id})")
421
+ try:
422
+ await tab.close()
423
+ except:
424
+ pass
425
+ return None
426
+
427
+ # 创建常驻信息对象
428
+ resident_info = ResidentTabInfo(tab, project_id)
429
+ resident_info.recaptcha_ready = True
430
+
431
+ debug_logger.log_info(f"[BrowserCaptcha] ✅ 常驻标签页创建成功 (project: {project_id})")
432
+ return resident_info
433
+
434
+ except Exception as e:
435
+ debug_logger.log_error(f"[BrowserCaptcha] 创建常驻标签页异常: {e}")
436
+ return None
437
+
438
+ async def _close_resident_tab(self, project_id: str):
439
+ """关闭指定 project_id 的常驻标签页
440
+
441
+ Args:
442
+ project_id: 项目 ID
443
+ """
444
+ resident_info = self._resident_tabs.pop(project_id, None)
445
+ if resident_info and resident_info.tab:
446
+ try:
447
+ await resident_info.tab.close()
448
+ debug_logger.log_info(f"[BrowserCaptcha] 已关闭 project_id={project_id} 的常驻标签页")
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
460
+ """
461
  # 确保浏览器已启动
462
+ if not self._initialized or not self.browser:
463
  await self.initialize()
464
 
465
  start_time = time.time()
466
+ tab = None
467
 
468
  try:
 
 
 
 
469
  website_url = f"https://labs.google/fx/tools/flow/project/{project_id}"
470
+ debug_logger.log_info(f"[BrowserCaptcha] [Legacy] 访问页面: {website_url}")
471
 
472
+ # 新建标签页并访问页面
473
+ tab = await self.browser.get(website_url)
474
+
475
+ # 等待页面完全加载(增加等待时间)
476
+ debug_logger.log_info("[BrowserCaptcha] [Legacy] 等待页面加载...")
477
+ await tab.sleep(3)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
478
 
479
+ # 等待页面 DOM 完成
480
+ for _ in range(10):
481
+ ready_state = await tab.evaluate("document.readyState")
482
+ if ready_state == "complete":
483
+ break
484
+ await tab.sleep(0.5)
485
+
486
+ # 等待 reCAPTCHA 加载
487
+ recaptcha_ready = await self._wait_for_recaptcha(tab)
488
+
489
+ if not recaptcha_ready:
490
+ debug_logger.log_error("[BrowserCaptcha] [Legacy] reCAPTCHA 无法加载")
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
+
499
  if token:
500
+ debug_logger.log_info(f"[BrowserCaptcha] [Legacy] ✅ Token获取成功(耗时 {duration_ms:.0f}ms)")
501
  return token
502
  else:
503
+ debug_logger.log_error("[BrowserCaptcha] [Legacy] Token获取失败(返回null)")
504
  return None
505
 
506
  except Exception as e:
507
+ debug_logger.log_error(f"[BrowserCaptcha] [Legacy] 获取token异常: {str(e)}")
508
  return None
509
  finally:
510
+ # 关闭标签页(但保留浏览器
511
+ if tab:
512
  try:
513
+ await tab.close()
514
+ except Exception:
515
  pass
516
 
517
  async def close(self):
518
+ """关闭浏览器"""
519
+ # 先停止所有常驻模式(关闭所有常驻标签页)
520
+ await self.stop_resident_mode()
521
+
522
  try:
523
+ if self.browser:
524
+ try:
525
+ self.browser.stop()
526
+ except Exception as e:
527
+ debug_logger.log_warning(f"[BrowserCaptcha] 关闭浏览器时出现异常: {str(e)}")
528
+ finally:
529
+ self.browser = None
530
+
531
  self._initialized = False
532
+ self._resident_tabs.clear() # 确保清空常驻字典
533
+ debug_logger.log_info("[BrowserCaptcha] 浏览器已关闭")
534
  except Exception as e:
535
+ debug_logger.log_error(f"[BrowserCaptcha] 关闭浏览器异常: {str(e)}")
536
 
 
537
  async def open_login_window(self):
538
+ """打开登录窗口供用户手动登录 Google"""
539
  await self.initialize()
540
+ tab = await self.browser.get("https://accounts.google.com/")
541
+ debug_logger.log_info("[BrowserCaptcha] 请在打开的浏览器中登录账号。登录完成后,无需关闭浏览器,脚本下次运行时会自动使用此状态。")
542
+ print("请在打开的浏览器中登录账号。登录完成后,无需关闭浏览器,脚本下次运行时会自动使用此状态。")
543
+
544
+ # ========== Session Token 刷新 ==========
545
+
546
+ async def refresh_session_token(self, project_id: str) -> Optional[str]:
547
+ """从常驻标签页获取最新的 Session Token
548
+
549
+ 复用 reCAPTCHA 常驻标签页,通过刷新页面并从 cookies 中提取
550
+ __Secure-next-auth.session-token
551
+
552
+ Args:
553
+ project_id: 项目ID,用于定位常驻标签页
554
+
555
+ Returns:
556
+ 新的 Session Token,如果获取失败返回 None
557
+ """
558
+ # 确保浏览器已初始化
559
+ await self.initialize()
560
+
561
+ start_time = time.time()
562
+ debug_logger.log_info(f"[BrowserCaptcha] 开始刷新 Session Token (project: {project_id})...")
563
+
564
+ # 尝试获取或创建常驻标签页
565
+ async with self._resident_lock:
566
+ resident_info = self._resident_tabs.get(project_id)
567
+
568
+ # 如果该 project_id 没有常驻标签页,则创建
569
+ if resident_info is None:
570
+ debug_logger.log_info(f"[BrowserCaptcha] project_id={project_id} 没有常驻标签页,正在创建...")
571
+ resident_info = await self._create_resident_tab(project_id)
572
+ if resident_info is None:
573
+ debug_logger.log_warning(f"[BrowserCaptcha] 无法为 project_id={project_id} 创建常驻标签页")
574
+ return None
575
+ self._resident_tabs[project_id] = resident_info
576
+
577
+ if not resident_info or not resident_info.tab:
578
+ debug_logger.log_error(f"[BrowserCaptcha] 无法获取常驻标签页")
579
+ return None
580
+
581
+ tab = resident_info.tab
582
+
583
+ try:
584
+ # 刷新页面以获取最新的 cookies
585
+ debug_logger.log_info(f"[BrowserCaptcha] 刷新常驻标签页以获取最新 cookies...")
586
+ await tab.reload()
587
+
588
+ # 等待页面加载完成
589
+ for i in range(30):
590
+ await asyncio.sleep(1)
591
+ try:
592
+ ready_state = await tab.evaluate("document.readyState")
593
+ if ready_state == "complete":
594
+ break
595
+ except Exception:
596
+ pass
597
+
598
+ # 额外等待确保 cookies 已设置
599
+ await asyncio.sleep(2)
600
+
601
+ # 从 cookies 中提取 __Secure-next-auth.session-token
602
+ # nodriver 可以通过 browser 获取 cookies
603
+ session_token = None
604
+
605
+ try:
606
+ # 使用 nodriver 的 cookies API 获取所有 cookies
607
+ cookies = await self.browser.cookies.get_all()
608
+
609
+ for cookie in cookies:
610
+ if cookie.name == "__Secure-next-auth.session-token":
611
+ session_token = cookie.value
612
+ break
613
+
614
+ except Exception as e:
615
+ debug_logger.log_warning(f"[BrowserCaptcha] 通过 cookies API 获取失败: {e},尝试从 document.cookie 获取...")
616
+
617
+ # 备选方案:通过 JavaScript 获取 (注意:HttpOnly cookies 可能无法通过此方式获取)
618
+ try:
619
+ all_cookies = await tab.evaluate("document.cookie")
620
+ if all_cookies:
621
+ for part in all_cookies.split(";"):
622
+ part = part.strip()
623
+ if part.startswith("__Secure-next-auth.session-token="):
624
+ session_token = part.split("=", 1)[1]
625
+ break
626
+ except Exception as e2:
627
+ debug_logger.log_error(f"[BrowserCaptcha] document.cookie 获取失败: {e2}")
628
+
629
+ duration_ms = (time.time() - start_time) * 1000
630
+
631
+ if session_token:
632
+ debug_logger.log_info(f"[BrowserCaptcha] ✅ Session Token 获取成功(耗时 {duration_ms:.0f}ms)")
633
+ return session_token
634
+ else:
635
+ debug_logger.log_error(f"[BrowserCaptcha] ❌ 未找到 __Secure-next-auth.session-token cookie")
636
+ return None
637
+
638
+ except Exception as e:
639
+ debug_logger.log_error(f"[BrowserCaptcha] 刷新 Session Token 异常: {str(e)}")
640
+
641
+ # 常驻标签页可能已失效,尝试重建
642
+ async with self._resident_lock:
643
+ await self._close_resident_tab(project_id)
644
+ resident_info = await self._create_resident_tab(project_id)
645
+ if resident_info:
646
+ self._resident_tabs[project_id] = resident_info
647
+ # 重建后再次尝试获取
648
+ try:
649
+ cookies = await self.browser.cookies.get_all()
650
+ for cookie in cookies:
651
+ if cookie.name == "__Secure-next-auth.session-token":
652
+ debug_logger.log_info(f"[BrowserCaptcha] ✅ 重建后 Session Token 获取成功")
653
+ return cookie.value
654
+ except Exception:
655
+ pass
656
+
657
+ return None
658
+
659
+ # ========== 状态查询 ==========
660
+
661
+ def is_resident_mode_active(self) -> bool:
662
+ """检查是否有任何常驻标签页激活"""
663
+ return len(self._resident_tabs) > 0 or self._running
664
+
665
+ def get_resident_count(self) -> int:
666
+ """获取当前常驻标签页数量"""
667
+ return len(self._resident_tabs)
668
+
669
+ def get_resident_project_ids(self) -> list[str]:
670
+ """获取所有当前常驻的 project_id 列表"""
671
+ return list(self._resident_tabs.keys())
672
+
673
+ def get_resident_project_id(self) -> Optional[str]:
674
+ """获取当前常驻的 project_id(向后兼容,返回第一个)"""
675
+ if self._resident_tabs:
676
+ return next(iter(self._resident_tabs.keys()))
677
+ return self.resident_project_id
src/services/flow_client.py CHANGED
@@ -12,11 +12,86 @@ 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
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
 
21
  async def _make_request(
22
  self,
@@ -52,12 +127,19 @@ class FlowClient:
52
 
53
  # AT认证 - 使用Bearer
54
  if use_at and at_token:
55
- headers["Authorization"] = f"Bearer {at_token}"
 
 
 
 
 
 
 
56
 
57
- # 通用请求头
58
  headers.update({
59
  "Content-Type": "application/json",
60
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
61
  })
62
 
63
  # Log request
@@ -571,7 +653,7 @@ class FlowClient:
571
  Returns:
572
  同 generate_video_text
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 ""
@@ -684,14 +766,14 @@ class FlowClient:
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 == "personal":
692
  try:
693
  from .browser_captcha_personal 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)}")
@@ -700,67 +782,98 @@ class FlowClient:
700
  elif captcha_method == "browser":
701
  try:
702
  from .browser_captcha import BrowserCaptchaService
703
- service = await BrowserCaptchaService.get_instance(self.proxy_manager)
704
  return await service.get_token(project_id)
705
  except Exception as e:
706
  debug_logger.log_error(f"[reCAPTCHA Browser] error: {str(e)}")
707
  return None
 
 
 
708
  else:
709
- # YesCaptcha打码
710
- client_key = config.yescaptcha_api_key
711
- if not client_key:
712
- debug_logger.log_info("[reCAPTCHA] API key not configured, skipping")
713
- return None
714
 
715
- website_key = "6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV"
716
- website_url = f"https://labs.google/fx/tools/flow/project/{project_id}"
 
 
 
717
  base_url = config.yescaptcha_base_url
718
- page_action = "FLOW_GENERATION"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
719
 
720
- try:
721
- async with AsyncSession() as session:
722
- create_url = f"{base_url}/createTask"
723
- create_data = {
724
- "clientKey": client_key,
725
- "softID": "42780",
726
- "task": {
727
- "websiteURL": website_url,
728
- "websiteKey": website_key,
729
- "type": "RecaptchaV3TaskProxylessM1",
730
- "pageAction": page_action
731
- }
 
 
 
 
 
 
 
732
  }
 
733
 
734
- result = await session.post(create_url, json=create_data, impersonate="chrome110")
735
- result_json = result.json()
736
- task_id = result_json.get('taskId')
737
 
738
- debug_logger.log_info(f"[reCAPTCHA] created task_id: {task_id}")
739
 
740
- if not task_id:
741
- return None
 
 
742
 
743
- get_url = f"{base_url}/getTaskResult"
744
- for i in range(40):
745
- get_data = {
746
- "clientKey": client_key,
747
- "taskId": task_id
748
- }
749
- result = await session.post(get_url, json=get_data, impersonate="chrome110")
750
- result_json = result.json()
751
 
752
- debug_logger.log_info(f"[reCAPTCHA] polling #{i+1}: {result_json}")
753
 
 
 
754
  solution = result_json.get('solution', {})
755
  response = solution.get('gRecaptchaResponse')
756
-
757
  if response:
 
758
  return response
759
 
760
- time.sleep(3)
761
 
762
- return None
763
-
764
- except Exception as e:
765
- debug_logger.log_error(f"[reCAPTCHA] error: {str(e)}")
766
  return None
 
 
 
 
 
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
21
+ # 缓存每个账号的 User-Agent
22
+ self._user_agent_cache = {}
23
+
24
+ def _generate_user_agent(self, account_id: str = None) -> str:
25
+ """基于账号ID生成固定的 User-Agent
26
+
27
+ Args:
28
+ account_id: 账号标识(如 email 或 token_id),相同账号返回相同 UA
29
+
30
+ Returns:
31
+ User-Agent 字符串
32
+ """
33
+ # 如果没有提供账号ID,生成随机UA
34
+ if not account_id:
35
+ account_id = f"random_{random.randint(1, 999999)}"
36
+
37
+ # 如果已缓存,直接返回
38
+ if account_id in self._user_agent_cache:
39
+ return self._user_agent_cache[account_id]
40
+
41
+ # 使用账号ID作为随机种子,确保同一账号生成相同的UA
42
+ import hashlib
43
+ seed = int(hashlib.md5(account_id.encode()).hexdigest()[:8], 16)
44
+ rng = random.Random(seed)
45
+
46
+ # Chrome 版本池
47
+ chrome_versions = ["130.0.0.0", "131.0.0.0", "132.0.0.0", "129.0.0.0"]
48
+ # Firefox 版本池
49
+ firefox_versions = ["133.0", "132.0", "131.0", "134.0"]
50
+ # Safari 版本池
51
+ safari_versions = ["18.2", "18.1", "18.0", "17.6"]
52
+ # Edge 版本池
53
+ edge_versions = ["130.0.0.0", "131.0.0.0", "132.0.0.0"]
54
+
55
+ # 操作系统配置
56
+ os_configs = [
57
+ # Windows
58
+ {
59
+ "platform": "Windows NT 10.0; Win64; x64",
60
+ "browsers": [
61
+ lambda r: f"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{r.choice(chrome_versions)} Safari/537.36",
62
+ lambda r: f"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:{r.choice(firefox_versions).split('.')[0]}.0) Gecko/20100101 Firefox/{r.choice(firefox_versions)}",
63
+ lambda r: f"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{r.choice(chrome_versions)} Safari/537.36 Edg/{r.choice(edge_versions)}",
64
+ ]
65
+ },
66
+ # macOS
67
+ {
68
+ "platform": "Macintosh; Intel Mac OS X 10_15_7",
69
+ "browsers": [
70
+ lambda r: f"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{r.choice(chrome_versions)} Safari/537.36",
71
+ lambda r: f"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/{r.choice(safari_versions)} Safari/605.1.15",
72
+ lambda r: f"Mozilla/5.0 (Macintosh; Intel Mac OS X 14.{r.randint(0, 7)}; rv:{r.choice(firefox_versions).split('.')[0]}.0) Gecko/20100101 Firefox/{r.choice(firefox_versions)}",
73
+ ]
74
+ },
75
+ # Linux
76
+ {
77
+ "platform": "X11; Linux x86_64",
78
+ "browsers": [
79
+ lambda r: f"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{r.choice(chrome_versions)} Safari/537.36",
80
+ lambda r: f"Mozilla/5.0 (X11; Linux x86_64; rv:{r.choice(firefox_versions).split('.')[0]}.0) Gecko/20100101 Firefox/{r.choice(firefox_versions)}",
81
+ lambda r: f"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:{r.choice(firefox_versions).split('.')[0]}.0) Gecko/20100101 Firefox/{r.choice(firefox_versions)}",
82
+ ]
83
+ }
84
+ ]
85
+
86
+ # 使用固定种子随机选择操作系统和浏览器
87
+ os_config = rng.choice(os_configs)
88
+ browser_generator = rng.choice(os_config["browsers"])
89
+ user_agent = browser_generator(rng)
90
+
91
+ # 缓存结果
92
+ self._user_agent_cache[account_id] = user_agent
93
+
94
+ return user_agent
95
 
96
  async def _make_request(
97
  self,
 
127
 
128
  # AT认证 - 使用Bearer
129
  if use_at and at_token:
130
+ headers["authorization"] = f"Bearer {at_token}"
131
+
132
+ # 确定账号标识(优先使用 token 的前16个字符作为标识)
133
+ account_id = None
134
+ if st_token:
135
+ account_id = st_token[:16] # 使用 ST 的前16个字符
136
+ elif at_token:
137
+ account_id = at_token[:16] # 使用 AT 的前16个字符
138
 
139
+ # 通用请求头 - 基于账号生成固定的 User-Agent
140
  headers.update({
141
  "Content-Type": "application/json",
142
+ "User-Agent": self._generate_user_agent(account_id)
143
  })
144
 
145
  # Log request
 
653
  Returns:
654
  同 generate_video_text
655
  """
656
+ url = f"{self.api_base_url}/video:batchAsyncGenerateVideoStartImage"
657
 
658
  # 获取 reCAPTCHA token
659
  recaptcha_token = await self._get_recaptcha_token(project_id) or ""
 
766
  return str(uuid.uuid4())
767
 
768
  async def _get_recaptcha_token(self, project_id: str) -> Optional[str]:
769
+ """获取reCAPTCHA token - 支持打码方式"""
770
  captcha_method = config.captcha_method
771
 
772
  # 恒定浏览器打码
773
  if captcha_method == "personal":
774
  try:
775
  from .browser_captcha_personal import BrowserCaptchaService
776
+ service = await BrowserCaptchaService.get_instance(self.db)
777
  return await service.get_token(project_id)
778
  except Exception as e:
779
  debug_logger.log_error(f"[reCAPTCHA Browser] error: {str(e)}")
 
782
  elif captcha_method == "browser":
783
  try:
784
  from .browser_captcha import BrowserCaptchaService
785
+ service = await BrowserCaptchaService.get_instance(self.db)
786
  return await service.get_token(project_id)
787
  except Exception as e:
788
  debug_logger.log_error(f"[reCAPTCHA Browser] error: {str(e)}")
789
  return None
790
+ # API打码服务
791
+ elif captcha_method in ["yescaptcha", "capmonster", "ezcaptcha", "capsolver"]:
792
+ return await self._get_api_captcha_token(captcha_method, project_id)
793
  else:
794
+ debug_logger.log_error(f"[reCAPTCHA] Unknown captcha method: {captcha_method}")
795
+ return None
 
 
 
796
 
797
+ async def _get_api_captcha_token(self, method: str, project_id: str) -> Optional[str]:
798
+ """通用API打码服务"""
799
+ # 获取配置
800
+ if method == "yescaptcha":
801
+ client_key = config.yescaptcha_api_key
802
  base_url = config.yescaptcha_base_url
803
+ task_type = "RecaptchaV3TaskProxylessM1"
804
+ elif method == "capmonster":
805
+ client_key = config.capmonster_api_key
806
+ base_url = config.capmonster_base_url
807
+ task_type = "RecaptchaV3EnterpriseTask"
808
+ elif method == "ezcaptcha":
809
+ client_key = config.ezcaptcha_api_key
810
+ base_url = config.ezcaptcha_base_url
811
+ task_type = "ReCaptchaV3EnterpriseTaskProxyless"
812
+ elif method == "capsolver":
813
+ client_key = config.capsolver_api_key
814
+ base_url = config.capsolver_base_url
815
+ task_type = "ReCaptchaV3EnterpriseTaskProxyLess"
816
+ else:
817
+ debug_logger.log_error(f"[reCAPTCHA] Unknown API method: {method}")
818
+ return None
819
 
820
+ if not client_key:
821
+ debug_logger.log_info(f"[reCAPTCHA] {method} API key not configured, skipping")
822
+ return None
823
+
824
+ website_key = "6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV"
825
+ website_url = f"https://labs.google/fx/tools/flow/project/{project_id}"
826
+ page_action = "FLOW_GENERATION"
827
+
828
+ try:
829
+ async with AsyncSession() as session:
830
+ create_url = f"{base_url}/createTask"
831
+ create_data = {
832
+ "clientKey": client_key,
833
+ "softID": "42780",
834
+ "task": {
835
+ "websiteURL": website_url,
836
+ "websiteKey": website_key,
837
+ "type": task_type,
838
+ "pageAction": page_action
839
  }
840
+ }
841
 
842
+ result = await session.post(create_url, json=create_data, impersonate="chrome110")
843
+ result_json = result.json()
844
+ task_id = result_json.get('taskId')
845
 
846
+ debug_logger.log_info(f"[reCAPTCHA {method}] created task_id: {task_id}")
847
 
848
+ if not task_id:
849
+ error_desc = result_json.get('errorDescription', 'Unknown error')
850
+ debug_logger.log_error(f"[reCAPTCHA {method}] Failed to create task: {error_desc}")
851
+ return None
852
 
853
+ get_url = f"{base_url}/getTaskResult"
854
+ for i in range(40):
855
+ get_data = {
856
+ "clientKey": client_key,
857
+ "taskId": task_id
858
+ }
859
+ result = await session.post(get_url, json=get_data, impersonate="chrome110")
860
+ result_json = result.json()
861
 
862
+ debug_logger.log_info(f"[reCAPTCHA {method}] polling #{i+1}: {result_json}")
863
 
864
+ status = result_json.get('status')
865
+ if status == 'ready':
866
  solution = result_json.get('solution', {})
867
  response = solution.get('gRecaptchaResponse')
 
868
  if response:
869
+ debug_logger.log_info(f"[reCAPTCHA {method}] Token获取成功")
870
  return response
871
 
872
+ time.sleep(3)
873
 
874
+ debug_logger.log_error(f"[reCAPTCHA {method}] Timeout waiting for token")
 
 
 
875
  return None
876
+
877
+ except Exception as e:
878
+ debug_logger.log_error(f"[reCAPTCHA {method}] error: {str(e)}")
879
+ return None
src/services/generation_handler.py CHANGED
@@ -136,7 +136,7 @@ MODEL_CONFIG = {
136
  "veo_3_1_i2v_s_fast_fl_portrait": {
137
  "type": "video",
138
  "video_type": "i2v",
139
- "model_key": "veo_3_1_i2v_s_fast_fl",
140
  "aspect_ratio": "VIDEO_ASPECT_RATIO_PORTRAIT",
141
  "supports_images": True,
142
  "min_images": 1,
@@ -145,7 +145,7 @@ MODEL_CONFIG = {
145
  "veo_3_1_i2v_s_fast_fl_landscape": {
146
  "type": "video",
147
  "video_type": "i2v",
148
- "model_key": "veo_3_1_i2v_s_fast_fl",
149
  "aspect_ratio": "VIDEO_ASPECT_RATIO_LANDSCAPE",
150
  "supports_images": True,
151
  "min_images": 1,
@@ -259,7 +259,7 @@ MODEL_CONFIG = {
259
  "veo_3_0_r2v_fast_portrait": {
260
  "type": "video",
261
  "video_type": "r2v",
262
- "model_key": "veo_3_0_r2v_fast",
263
  "aspect_ratio": "VIDEO_ASPECT_RATIO_PORTRAIT",
264
  "supports_images": True,
265
  "min_images": 0,
@@ -268,7 +268,7 @@ MODEL_CONFIG = {
268
  "veo_3_0_r2v_fast_landscape": {
269
  "type": "video",
270
  "video_type": "r2v",
271
- "model_key": "veo_3_0_r2v_fast",
272
  "aspect_ratio": "VIDEO_ASPECT_RATIO_LANDSCAPE",
273
  "supports_images": True,
274
  "min_images": 0,
@@ -644,6 +644,44 @@ class GenerationHandler:
644
  min_images = model_config.get("min_images", 0)
645
  max_images = model_config.get("max_images", 0)
646
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
647
  # 图片数量
648
  image_count = len(images) if images else 0
649
 
@@ -734,12 +772,16 @@ class GenerationHandler:
734
  user_paygate_tier=token.user_paygate_tier or "PAYGATE_TIER_ONE"
735
  )
736
  else:
737
- # 只有首帧
 
 
 
 
738
  result = await self.flow_client.generate_video_start_image(
739
  at=token.at,
740
  project_id=project_id,
741
  prompt=prompt,
742
- model_key=model_config["model_key"],
743
  aspect_ratio=model_config["aspect_ratio"],
744
  start_media_id=start_media_id,
745
  user_paygate_tier=token.user_paygate_tier or "PAYGATE_TIER_ONE"
@@ -888,8 +930,30 @@ class GenerationHandler:
888
  )
889
  return
890
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
891
  elif status.startswith("MEDIA_GENERATION_STATUS_ERROR"):
892
- # 失败
893
  yield self._create_error_response(f"视频生成失败: {status}")
894
  return
895
 
 
136
  "veo_3_1_i2v_s_fast_fl_portrait": {
137
  "type": "video",
138
  "video_type": "i2v",
139
+ "model_key": "veo_3_1_i2v_s_fast_portrait_fl_ultra_relaxed",
140
  "aspect_ratio": "VIDEO_ASPECT_RATIO_PORTRAIT",
141
  "supports_images": True,
142
  "min_images": 1,
 
145
  "veo_3_1_i2v_s_fast_fl_landscape": {
146
  "type": "video",
147
  "video_type": "i2v",
148
+ "model_key": "veo_3_1_i2v_s_fast_fl_ultra_relaxed",
149
  "aspect_ratio": "VIDEO_ASPECT_RATIO_LANDSCAPE",
150
  "supports_images": True,
151
  "min_images": 1,
 
259
  "veo_3_0_r2v_fast_portrait": {
260
  "type": "video",
261
  "video_type": "r2v",
262
+ "model_key": "veo_3_0_r2v_fast_portrait_ultra_relaxed",
263
  "aspect_ratio": "VIDEO_ASPECT_RATIO_PORTRAIT",
264
  "supports_images": True,
265
  "min_images": 0,
 
268
  "veo_3_0_r2v_fast_landscape": {
269
  "type": "video",
270
  "video_type": "r2v",
271
+ "model_key": "veo_3_0_r2v_fast_ultra_relaxed",
272
  "aspect_ratio": "VIDEO_ASPECT_RATIO_LANDSCAPE",
273
  "supports_images": True,
274
  "min_images": 0,
 
644
  min_images = model_config.get("min_images", 0)
645
  max_images = model_config.get("max_images", 0)
646
 
647
+ # 根据账号tier自动调整模型 key
648
+ model_key = model_config["model_key"]
649
+ user_tier = token.user_paygate_tier or "PAYGATE_TIER_ONE"
650
+
651
+ # TIER_TWO 账号需要使用 ultra 版本的模型
652
+ if user_tier == "PAYGATE_TIER_TWO":
653
+ # 如果模型 key 不包含 ultra,自动添加
654
+ if "ultra" not in model_key:
655
+ # veo_3_1_i2v_s_fast_fl -> veo_3_1_i2v_s_fast_ultra_fl
656
+ # veo_3_1_t2v_fast -> veo_3_1_t2v_fast_ultra
657
+ # veo_3_0_r2v_fast -> veo_3_0_r2v_fast_ultra
658
+ if "_fl" in model_key:
659
+ model_key = model_key.replace("_fl", "_ultra_fl")
660
+ elif model_key.endswith("_fast"):
661
+ model_key = model_key + "_ultra"
662
+ elif "_fast_" in model_key:
663
+ model_key = model_key.replace("_fast_", "_fast_ultra_")
664
+
665
+ if stream:
666
+ yield self._create_stream_chunk(f"TIER_TWO 账号自动切换到 ultra 模型: {model_key}\n")
667
+ debug_logger.log_info(f"[VIDEO] TIER_TWO 账号,模型自动调整: {model_config['model_key']} -> {model_key}")
668
+
669
+ # TIER_ONE 账号需要使用非 ultra 版本
670
+ elif user_tier == "PAYGATE_TIER_ONE":
671
+ # 如果模型 key 包含 ultra,需要移除(避免用户误用)
672
+ if "ultra" in model_key:
673
+ # veo_3_1_i2v_s_fast_ultra_fl -> veo_3_1_i2v_s_fast_fl
674
+ # veo_3_1_t2v_fast_ultra -> veo_3_1_t2v_fast
675
+ model_key = model_key.replace("_ultra_fl", "_fl").replace("_ultra", "")
676
+
677
+ if stream:
678
+ yield self._create_stream_chunk(f"TIER_ONE 账号自动切换到标准模型: {model_key}\n")
679
+ debug_logger.log_info(f"[VIDEO] TIER_ONE 账号,模型自动调整: {model_config['model_key']} -> {model_key}")
680
+
681
+ # 更新 model_config 中的 model_key
682
+ model_config = dict(model_config) # 创建副本避免修改原配置
683
+ model_config["model_key"] = model_key
684
+
685
  # 图片数量
686
  image_count = len(images) if images else 0
687
 
 
772
  user_paygate_tier=token.user_paygate_tier or "PAYGATE_TIER_ONE"
773
  )
774
  else:
775
+ # 只有首帧 - 需要将 model_key 中的 _fl_ 替换为 _
776
+ # 例如: veo_3_1_i2v_s_fast_fl_ultra_relaxed -> veo_3_1_i2v_s_fast_ultra_relaxed
777
+ # veo_3_1_i2v_s_fast_portrait_fl_ultra_relaxed -> veo_3_1_i2v_s_fast_portrait_ultra_relaxed
778
+ actual_model_key = model_config["model_key"].replace("_fl_", "_")
779
+ debug_logger.log_info(f"[I2V] 单帧模式,model_key: {model_config['model_key']} -> {actual_model_key}")
780
  result = await self.flow_client.generate_video_start_image(
781
  at=token.at,
782
  project_id=project_id,
783
  prompt=prompt,
784
+ model_key=actual_model_key,
785
  aspect_ratio=model_config["aspect_ratio"],
786
  start_media_id=start_media_id,
787
  user_paygate_tier=token.user_paygate_tier or "PAYGATE_TIER_ONE"
 
930
  )
931
  return
932
 
933
+ elif status == "MEDIA_GENERATION_STATUS_FAILED":
934
+ # 生成失败 - 提取错误信息
935
+ error_info = operation.get("operation", {}).get("error", {})
936
+ error_code = error_info.get("code", "unknown")
937
+ error_message = error_info.get("message", "未知错误")
938
+
939
+ # 更新数据库任务状态
940
+ task_id = operation["operation"]["name"]
941
+ await self.db.update_task(
942
+ task_id,
943
+ status="failed",
944
+ error_message=f"{error_message} (code: {error_code})",
945
+ completed_at=time.time()
946
+ )
947
+
948
+ # 返回友好的错误消息,提示用户重试
949
+ friendly_error = f"视频生成失败: {error_message},请重试"
950
+ if stream:
951
+ yield self._create_stream_chunk(f"❌ {friendly_error}\n")
952
+ yield self._create_error_response(friendly_error)
953
+ return
954
+
955
  elif status.startswith("MEDIA_GENERATION_STATUS_ERROR"):
956
+ # 其他错误状态
957
  yield self._create_error_response(f"视频生成失败: {status}")
958
  return
959
 
src/services/token_manager.py CHANGED
@@ -268,9 +268,13 @@ class TokenManager:
268
  # AT有效
269
  return True
270
 
 
271
  async def _refresh_at(self, token_id: int) -> bool:
272
  """内部方法: 刷新AT
273
 
 
 
 
274
  Returns:
275
  True if refresh successful, False otherwise
276
  """
@@ -279,49 +283,132 @@ class TokenManager:
279
  if not token:
280
  return False
281
 
282
- try:
283
- debug_logger.log_info(f"[AT_REFRESH] Token {token_id}: 开始刷新AT...")
284
-
285
- # 使用ST转AT
286
- result = await self.flow_client.st_to_at(token.st)
287
- new_at = result["access_token"]
288
- expires = result.get("expires")
289
-
290
- # 解析过期时间
291
- new_at_expires = None
292
- if expires:
293
- try:
294
- new_at_expires = datetime.fromisoformat(expires.replace('Z', '+00:00'))
295
- except:
296
- pass
297
-
298
- # 更新数据库
299
- await self.db.update_token(
300
- token_id,
301
- at=new_at,
302
- at_expires=new_at_expires
303
- )
304
 
305
- debug_logger.log_info(f"[AT_REFRESH] Token {token_id}: AT刷新成功")
306
- debug_logger.log_info(f" - 新过期时间: {new_at_expires}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
307
 
308
- # 同时刷新credits
 
 
 
 
 
 
 
 
 
 
 
 
 
309
  try:
310
- credits_result = await self.flow_client.get_credits(new_at)
311
- await self.db.update_token(
312
- token_id,
313
- credits=credits_result.get("credits", 0)
314
- )
315
  except:
316
  pass
317
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
318
  return True
 
 
 
 
 
 
 
 
 
 
319
 
320
- except Exception as e:
321
- debug_logger.log_error(f"[AT_REFRESH] Token {token_id}: AT刷新失败 - {str(e)}")
322
- # 刷新失败,禁用Token
323
- await self.disable_token(token_id)
324
- return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
325
 
326
  async def ensure_project_exists(self, token_id: int) -> str:
327
  """确保Token有可用的Project
 
268
  # AT有效
269
  return True
270
 
271
+
272
  async def _refresh_at(self, token_id: int) -> bool:
273
  """内部方法: 刷新AT
274
 
275
+ 如果 AT 刷新失败(ST 可能过期),会尝试通过浏览器自动刷新 ST,
276
+ 然后重试 AT 刷新。
277
+
278
  Returns:
279
  True if refresh successful, False otherwise
280
  """
 
283
  if not token:
284
  return False
285
 
286
+ # 第一次尝试刷新 AT
287
+ result = await self._do_refresh_at(token_id, token.st)
288
+ if result:
289
+ return True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
290
 
291
+ # AT 刷新失败,尝试自动更新 ST
292
+ debug_logger.log_info(f"[AT_REFRESH] Token {token_id}: 第一次 AT 刷新失败,尝试自动更新 ST...")
293
+
294
+ new_st = await self._try_refresh_st(token_id, token)
295
+ if new_st:
296
+ # ST 更新成功,重试 AT 刷新
297
+ debug_logger.log_info(f"[AT_REFRESH] Token {token_id}: ST 已更新,重试 AT 刷新...")
298
+ result = await self._do_refresh_at(token_id, new_st)
299
+ if result:
300
+ return True
301
+
302
+ # 所有刷新尝试都失败,禁用 Token
303
+ debug_logger.log_error(f"[AT_REFRESH] Token {token_id}: 所有刷新尝试失败,禁用 Token")
304
+ await self.disable_token(token_id)
305
+ return False
306
+
307
+ async def _do_refresh_at(self, token_id: int, st: str) -> bool:
308
+ """执行 AT 刷新的核心逻辑
309
+
310
+ Args:
311
+ token_id: Token ID
312
+ st: Session Token
313
 
314
+ Returns:
315
+ True if refresh successful AND AT is valid, False otherwise
316
+ """
317
+ try:
318
+ debug_logger.log_info(f"[AT_REFRESH] Token {token_id}: 开始刷新AT...")
319
+
320
+ # 使用ST转AT
321
+ result = await self.flow_client.st_to_at(st)
322
+ new_at = result["access_token"]
323
+ expires = result.get("expires")
324
+
325
+ # 解析过期时间
326
+ new_at_expires = None
327
+ if expires:
328
  try:
329
+ new_at_expires = datetime.fromisoformat(expires.replace('Z', '+00:00'))
 
 
 
 
330
  except:
331
  pass
332
 
333
+ # 更新数据库
334
+ await self.db.update_token(
335
+ token_id,
336
+ at=new_at,
337
+ at_expires=new_at_expires
338
+ )
339
+
340
+ debug_logger.log_info(f"[AT_REFRESH] Token {token_id}: AT刷新成功")
341
+ debug_logger.log_info(f" - 新过期时间: {new_at_expires}")
342
+
343
+ # 验证 AT 有效性:通过 get_credits 测试
344
+ try:
345
+ credits_result = await self.flow_client.get_credits(new_at)
346
+ await self.db.update_token(
347
+ token_id,
348
+ credits=credits_result.get("credits", 0)
349
+ )
350
+ debug_logger.log_info(f"[AT_REFRESH] Token {token_id}: AT 验证成功(余额: {credits_result.get('credits', 0)})")
351
  return True
352
+ except Exception as verify_err:
353
+ # AT 验证失败(可能返回 401),说明 ST 已过期
354
+ error_msg = str(verify_err)
355
+ if "401" in error_msg or "UNAUTHENTICATED" in error_msg:
356
+ debug_logger.log_warning(f"[AT_REFRESH] Token {token_id}: AT 验证失败 (401),ST 可能已过期")
357
+ return False
358
+ else:
359
+ # 其他错误(如网络问题),仍视为成功
360
+ debug_logger.log_warning(f"[AT_REFRESH] Token {token_id}: AT 验证时发生非认证错误: {error_msg}")
361
+ return True
362
 
363
+ except Exception as e:
364
+ debug_logger.log_error(f"[AT_REFRESH] Token {token_id}: AT刷新失败 - {str(e)}")
365
+ return False
366
+
367
+ async def _try_refresh_st(self, token_id: int, token) -> Optional[str]:
368
+ """尝试通过浏览器刷新 Session Token
369
+
370
+ 使用常驻 tab 获取新的 __Secure-next-auth.session-token
371
+
372
+ Args:
373
+ token_id: Token ID
374
+ token: Token 对象
375
+
376
+ Returns:
377
+ 新的 ST 字符串,如果失败返回 None
378
+ """
379
+ try:
380
+ from ..core.config import config
381
+
382
+ # 仅在 personal 模式下支持 ST 自动刷新
383
+ if config.captcha_method != "personal":
384
+ debug_logger.log_info(f"[ST_REFRESH] 非 personal 模式,跳过 ST 自动刷新")
385
+ return None
386
+
387
+ if not token.current_project_id:
388
+ debug_logger.log_warning(f"[ST_REFRESH] Token {token_id} 没有 project_id,无法刷新 ST")
389
+ return None
390
+
391
+ debug_logger.log_info(f"[ST_REFRESH] Token {token_id}: 尝试通过浏览器刷新 ST...")
392
+
393
+ from .browser_captcha_personal import BrowserCaptchaService
394
+ service = await BrowserCaptchaService.get_instance(self.db)
395
+
396
+ new_st = await service.refresh_session_token(token.current_project_id)
397
+ if new_st and new_st != token.st:
398
+ # 更新数据库中的 ST
399
+ await self.db.update_token(token_id, st=new_st)
400
+ debug_logger.log_info(f"[ST_REFRESH] Token {token_id}: ST 已自动更新")
401
+ return new_st
402
+ elif new_st == token.st:
403
+ debug_logger.log_warning(f"[ST_REFRESH] Token {token_id}: 获取到的 ST 与原 ST 相同,可能登录已失效")
404
+ return None
405
+ else:
406
+ debug_logger.log_warning(f"[ST_REFRESH] Token {token_id}: 无法获取新 ST")
407
+ return None
408
+
409
+ except Exception as e:
410
+ debug_logger.log_error(f"[ST_REFRESH] Token {token_id}: 刷新 ST 失败 - {str(e)}")
411
+ return None
412
 
413
  async def ensure_project_exists(self, token_id: int) -> str:
414
  """确保Token有可用的Project
static/manage.html CHANGED
@@ -269,6 +269,9 @@
269
  <label class="text-sm font-medium mb-2 block">打码方式</label>
270
  <select id="cfgCaptchaMethod" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" onchange="toggleCaptchaOptions()">
271
  <option value="yescaptcha">YesCaptcha打码</option>
 
 
 
272
  <option value="browser">无头浏览器打码</option>
273
  <option value="personal">内置浏览器打码</option>
274
  </select>
@@ -289,6 +292,48 @@
289
  </div>
290
  </div>
291
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
  <!-- 浏览器打码配置选项 -->
293
  <div id="browserCaptchaOptions" class="hidden space-y-4">
294
  <div class="rounded-md bg-blue-50 dark:bg-blue-900/20 p-3 border border-blue-200 dark:border-blue-800">
@@ -335,6 +380,7 @@
335
  <label class="text-sm font-semibold mb-2 block">连接Token</label>
336
  <div class="flex gap-2">
337
  <input id="cfgPluginConnectionToken" type="text" class="flex h-9 flex-1 rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="留空自动生成">
 
338
  <button onclick="copyConnectionToken()" class="inline-flex items-center justify-center rounded-md bg-secondary text-secondary-foreground hover:bg-secondary/80 h-9 px-4">复制</button>
339
  </div>
340
  <p class="text-xs text-muted-foreground mt-1">用于验证Chrome扩展插件的身份,留空将自动生成随机token</p>
@@ -698,14 +744,15 @@
698
  loadGenerationTimeout=async()=>{try{console.log('开始加载生成超时配置...');const r=await apiRequest('/api/generation/timeout');if(!r){console.error('API请求失败');return}const d=await r.json();console.log('生成超时配置数据:',d);if(d.success&&d.config){const imageTimeout=d.config.image_timeout||300;const videoTimeout=d.config.video_timeout||1500;console.log('设置图片超时:',imageTimeout);console.log('设置视频超时:',videoTimeout);$('cfgImageTimeout').value=imageTimeout;$('cfgVideoTimeout').value=videoTimeout;console.log('生成超时配置加载成功')}else{console.error('生成超时配置数据格式错误:',d)}}catch(e){console.error('加载生成超时配置失败:',e);showToast('加载生成超时配置失败: '+e.message,'error')}},
699
  saveCacheConfig=async()=>{const enabled=$('cfgCacheEnabled').checked,timeout=parseInt($('cfgCacheTimeout').value)||7200,baseUrl=$('cfgCacheBaseUrl').value.trim();console.log('保存缓存配置:',{enabled,timeout,baseUrl});if(timeout<60||timeout>86400)return showToast('缓存超时时间必须在 60-86400 秒之间','error');if(baseUrl&&!baseUrl.startsWith('http://')&&!baseUrl.startsWith('https://'))return showToast('域名必须以 http:// 或 https:// 开头','error');try{console.log('保存缓存启用状态...');const r0=await apiRequest('/api/cache/enabled',{method:'POST',body:JSON.stringify({enabled:enabled})});if(!r0){console.error('保存缓存启用状态请求失败');return}const d0=await r0.json();console.log('缓存启用状态保存结果:',d0);if(!d0.success){console.error('保存缓存启用状态失败:',d0);return showToast('保存缓存启用状态失败','error')}console.log('保存超时时间...');const r1=await apiRequest('/api/cache/config',{method:'POST',body:JSON.stringify({timeout:timeout})});if(!r1){console.error('保存超时时间请求失败');return}const d1=await r1.json();console.log('超时时间保存结果:',d1);if(!d1.success){console.error('保存超时时间失败:',d1);return showToast('保存超时时间失败','error')}console.log('保存域名...');const r2=await apiRequest('/api/cache/base-url',{method:'POST',body:JSON.stringify({base_url:baseUrl})});if(!r2){console.error('保存域名请求失败');return}const d2=await r2.json();console.log('域名保存结果:',d2);if(d2.success){showToast('缓存配置保存成功','success');console.log('等待配置文件写入完成...');await new Promise(r=>setTimeout(r,200));console.log('重新加载配置...');await loadCacheConfig()}else{console.error('保存域名失败:',d2);showToast('保存域名失败','error')}}catch(e){console.error('保存失败:',e);showToast('保存失败: '+e.message,'error')}},
700
  saveGenerationTimeout=async()=>{const imageTimeout=parseInt($('cfgImageTimeout').value)||300,videoTimeout=parseInt($('cfgVideoTimeout').value)||1500;console.log('保存生成超时配置:',{imageTimeout,videoTimeout});if(imageTimeout<60||imageTimeout>3600)return showToast('图片超时时间必须在 60-3600 秒之间','error');if(videoTimeout<60||videoTimeout>7200)return showToast('视频超时时间必须在 60-7200 秒之间','error');try{const r=await apiRequest('/api/generation/timeout',{method:'POST',body:JSON.stringify({image_timeout:imageTimeout,video_timeout:videoTimeout})});if(!r){console.error('保存请求失败');return}const d=await r.json();console.log('保存结果:',d);if(d.success){showToast('生成超时配置保存成功','success');await new Promise(r=>setTimeout(r,200));await loadGenerationTimeout()}else{console.error('保存失败:',d);showToast('保存失败','error')}}catch(e){console.error('保存失败:',e);showToast('保存失败: '+e.message,'error')}},
701
- toggleCaptchaOptions=()=>{const method=$('cfgCaptchaMethod').value;$('yescaptchaOptions').style.display=method==='yescaptcha'?'block':'none';$('browserCaptchaOptions').classList.toggle('hidden',method!=='browser')},
702
  toggleBrowserProxyInput=()=>{const enabled=$('cfgBrowserProxyEnabled').checked;$('browserProxyUrlInput').classList.toggle('hidden',!enabled)},
703
- loadCaptchaConfig=async()=>{try{console.log('开始加载验证码配置...');const r=await apiRequest('/api/captcha/config');if(!r){console.error('API请求失败');return}const d=await r.json();console.log('验证码配置数据:',d);$('cfgCaptchaMethod').value=d.captcha_method||'yescaptcha';$('cfgYescaptchaApiKey').value=d.yescaptcha_api_key||'';$('cfgYescaptchaBaseUrl').value=d.yescaptcha_base_url||'https://api.yescaptcha.com';$('cfgBrowserProxyEnabled').checked=d.browser_proxy_enabled||false;$('cfgBrowserProxyUrl').value=d.browser_proxy_url||'';toggleCaptchaOptions();toggleBrowserProxyInput();console.log('验证码配置加载成功')}catch(e){console.error('加载验证码配置失败:',e);showToast('加载验证码配置失败: '+e.message,'error')}},
704
- saveCaptchaConfig=async()=>{const method=$('cfgCaptchaMethod').value,apiKey=$('cfgYescaptchaApiKey').value.trim(),baseUrl=$('cfgYescaptchaBaseUrl').value.trim(),browserProxyEnabled=$('cfgBrowserProxyEnabled').checked,browserProxyUrl=$('cfgBrowserProxyUrl').value.trim();console.log('保存验证码配置:',{method,apiKey,baseUrl,browserProxyEnabled,browserProxyUrl});try{const r=await apiRequest('/api/captcha/config',{method:'POST',body:JSON.stringify({captcha_method:method,yescaptcha_api_key:apiKey,yescaptcha_base_url:baseUrl,browser_proxy_enabled:browserProxyEnabled,browser_proxy_url:browserProxyUrl})});if(!r){console.error('保存请求失败');return}const d=await r.json();console.log('保存结果:',d);if(d.success){showToast('验证码配置保存成功','success');await new Promise(r=>setTimeout(r,200));await loadCaptchaConfig()}else{console.error('保存失败:',d);showToast(d.message||'保存失败','error')}}catch(e){console.error('保存失败:',e);showToast('保存失败: '+e.message,'error')}},
705
  loadPluginConfig=async()=>{try{const r=await apiRequest('/api/plugin/config');if(!r)return;const d=await r.json();if(d.success&&d.config){$('cfgPluginConnectionUrl').value=d.config.connection_url||'';$('cfgPluginConnectionToken').value=d.config.connection_token||'';$('cfgAutoEnableOnUpdate').checked=d.config.auto_enable_on_update||false}}catch(e){console.error('加���插件配置失败:',e);showToast('加载插件配置失败: '+e.message,'error')}},
706
  savePluginConfig=async()=>{const token=$('cfgPluginConnectionToken').value.trim();const autoEnable=$('cfgAutoEnableOnUpdate').checked;try{const r=await apiRequest('/api/plugin/config',{method:'POST',body:JSON.stringify({connection_token:token,auto_enable_on_update:autoEnable})});if(!r)return;const d=await r.json();if(d.success){showToast('插件配置保存成功','success');await loadPluginConfig()}else{showToast(d.message||'保存失败','error')}}catch(e){showToast('保存失败: '+e.message,'error')}},
707
  copyConnectionUrl=()=>{const url=$('cfgPluginConnectionUrl').value;if(!url){showToast('连接接口为空','error');return}navigator.clipboard.writeText(url).then(()=>showToast('连接接口已复制','success')).catch(()=>showToast('复制失败','error'))},
708
  copyConnectionToken=()=>{const token=$('cfgPluginConnectionToken').value;if(!token){showToast('连接Token为空','error');return}navigator.clipboard.writeText(token).then(()=>showToast('连接Token已复制','success')).catch(()=>showToast('复制失败','error'))},
 
709
  toggleATAutoRefresh=async()=>{try{const enabled=$('atAutoRefreshToggle').checked;const r=await apiRequest('/api/token-refresh/enabled',{method:'POST',body:JSON.stringify({enabled:enabled})});if(!r){$('atAutoRefreshToggle').checked=!enabled;return}const d=await r.json();if(d.success){showToast(enabled?'AT自动刷新已启用':'AT自动刷新已禁用','success')}else{showToast('操作失败: '+(d.detail||'未知错误'),'error');$('atAutoRefreshToggle').checked=!enabled}}catch(e){showToast('操作失败: '+e.message,'error');$('atAutoRefreshToggle').checked=!enabled}},
710
  loadATAutoRefreshConfig=async()=>{try{const r=await apiRequest('/api/token-refresh/config');if(!r)return;const d=await r.json();if(d.success&&d.config){$('atAutoRefreshToggle').checked=d.config.at_auto_refresh_enabled||false}else{console.error('AT自动刷新配置数据格式错误:',d)}}catch(e){console.error('加载AT自动刷新配置失败:',e)}},
711
  loadLogs=async()=>{try{const r=await apiRequest('/api/logs?limit=100');if(!r)return;const logs=await r.json();window.allLogs=logs;const tb=$('logsTableBody');tb.innerHTML=logs.map(l=>`<tr><td class="py-2.5 px-3">${l.operation}</td><td class="py-2.5 px-3"><span class="text-xs ${l.token_email?'text-blue-600':'text-muted-foreground'}">${l.token_email||'未知'}</span></td><td class="py-2.5 px-3"><span class="inline-flex items-center rounded px-2 py-0.5 text-xs ${l.status_code===200?'bg-green-50 text-green-700':'bg-red-50 text-red-700'}">${l.status_code}</span></td><td class="py-2.5 px-3">${l.duration.toFixed(2)}</td><td class="py-2.5 px-3 text-xs text-muted-foreground">${l.created_at?new Date(l.created_at).toLocaleString('zh-CN'):'-'}</td><td class="py-2.5 px-3"><button onclick="showLogDetail(${l.id})" class="inline-flex items-center justify-center rounded-md hover:bg-blue-50 hover:text-blue-700 h-7 px-2 text-xs">查看</button></td></tr>`).join('')}catch(e){console.error('加载日志失败:',e)}},
 
269
  <label class="text-sm font-medium mb-2 block">打码方式</label>
270
  <select id="cfgCaptchaMethod" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" onchange="toggleCaptchaOptions()">
271
  <option value="yescaptcha">YesCaptcha打码</option>
272
+ <option value="capmonster">CapMonster打码</option>
273
+ <option value="ezcaptcha">EzCaptcha打码</option>
274
+ <option value="capsolver">CapSolver打码</option>
275
  <option value="browser">无头浏览器打码</option>
276
  <option value="personal">内置浏览器打码</option>
277
  </select>
 
292
  </div>
293
  </div>
294
 
295
+ <!-- CapMonster配置选项 -->
296
+ <div id="capmonsterOptions" class="hidden space-y-4">
297
+ <div>
298
+ <label class="text-sm font-medium mb-2 block">CapMonster API密钥</label>
299
+ <input id="cfgCapmonsterApiKey" type="text" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="请输入CapMonster API密钥">
300
+ <p class="text-xs text-muted-foreground mt-1">用于自动获取reCAPTCHA验证码</p>
301
+ </div>
302
+ <div>
303
+ <label class="text-sm font-medium mb-2 block">CapMonster API地址</label>
304
+ <input id="cfgCapmonsterBaseUrl" type="text" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="https://api.capmonster.cloud">
305
+ <p class="text-xs text-muted-foreground mt-1">CapMonster服务地址,默认:https://api.capmonster.cloud</p>
306
+ </div>
307
+ </div>
308
+
309
+ <!-- EzCaptcha配置选项 -->
310
+ <div id="ezcaptchaOptions" class="hidden space-y-4">
311
+ <div>
312
+ <label class="text-sm font-medium mb-2 block">EzCaptcha API密钥</label>
313
+ <input id="cfgEzcaptchaApiKey" type="text" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="请输入EzCaptcha API密钥">
314
+ <p class="text-xs text-muted-foreground mt-1">用于自动获取reCAPTCHA验证码</p>
315
+ </div>
316
+ <div>
317
+ <label class="text-sm font-medium mb-2 block">EzCaptcha API地址</label>
318
+ <input id="cfgEzcaptchaBaseUrl" type="text" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="https://api.ez-captcha.com">
319
+ <p class="text-xs text-muted-foreground mt-1">EzCaptcha服务地址,默认:https://api.ez-captcha.com</p>
320
+ </div>
321
+ </div>
322
+
323
+ <!-- CapSolver配置选项 -->
324
+ <div id="capsolverOptions" class="hidden space-y-4">
325
+ <div>
326
+ <label class="text-sm font-medium mb-2 block">CapSolver API密钥</label>
327
+ <input id="cfgCapsolverApiKey" type="text" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="请输入CapSolver API密钥">
328
+ <p class="text-xs text-muted-foreground mt-1">用于自动获取reCAPTCHA验证码</p>
329
+ </div>
330
+ <div>
331
+ <label class="text-sm font-medium mb-2 block">CapSolver API地址</label>
332
+ <input id="cfgCapsolverBaseUrl" type="text" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="https://api.capsolver.com">
333
+ <p class="text-xs text-muted-foreground mt-1">CapSolver服务地址,默认:https://api.capsolver.com</p>
334
+ </div>
335
+ </div>
336
+
337
  <!-- 浏览器打码配置选项 -->
338
  <div id="browserCaptchaOptions" class="hidden space-y-4">
339
  <div class="rounded-md bg-blue-50 dark:bg-blue-900/20 p-3 border border-blue-200 dark:border-blue-800">
 
380
  <label class="text-sm font-semibold mb-2 block">连接Token</label>
381
  <div class="flex gap-2">
382
  <input id="cfgPluginConnectionToken" type="text" class="flex h-9 flex-1 rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="留空自动生成">
383
+ <button onclick="generateRandomToken()" class="inline-flex items-center justify-center rounded-md bg-primary text-primary-foreground hover:bg-primary/90 h-9 px-4">随机</button>
384
  <button onclick="copyConnectionToken()" class="inline-flex items-center justify-center rounded-md bg-secondary text-secondary-foreground hover:bg-secondary/80 h-9 px-4">复制</button>
385
  </div>
386
  <p class="text-xs text-muted-foreground mt-1">用于验证Chrome扩展插件的身份,留空将自动生成随机token</p>
 
744
  loadGenerationTimeout=async()=>{try{console.log('开始加载生成超时配置...');const r=await apiRequest('/api/generation/timeout');if(!r){console.error('API请求失败');return}const d=await r.json();console.log('生成超时配置数据:',d);if(d.success&&d.config){const imageTimeout=d.config.image_timeout||300;const videoTimeout=d.config.video_timeout||1500;console.log('设置图片超时:',imageTimeout);console.log('设置视频超时:',videoTimeout);$('cfgImageTimeout').value=imageTimeout;$('cfgVideoTimeout').value=videoTimeout;console.log('生成超时配置加载成功')}else{console.error('生成超时配置数据格式错误:',d)}}catch(e){console.error('加载生成超时配置失败:',e);showToast('加载生成超时配置失败: '+e.message,'error')}},
745
  saveCacheConfig=async()=>{const enabled=$('cfgCacheEnabled').checked,timeout=parseInt($('cfgCacheTimeout').value)||7200,baseUrl=$('cfgCacheBaseUrl').value.trim();console.log('保存缓存配置:',{enabled,timeout,baseUrl});if(timeout<60||timeout>86400)return showToast('缓存超时时间必须在 60-86400 秒之间','error');if(baseUrl&&!baseUrl.startsWith('http://')&&!baseUrl.startsWith('https://'))return showToast('域名必须以 http:// 或 https:// 开头','error');try{console.log('保存缓存启用状态...');const r0=await apiRequest('/api/cache/enabled',{method:'POST',body:JSON.stringify({enabled:enabled})});if(!r0){console.error('保存缓存启用状态请求失败');return}const d0=await r0.json();console.log('缓存启用状态保存结果:',d0);if(!d0.success){console.error('保存缓存启用状态失败:',d0);return showToast('保存缓存启用状态失败','error')}console.log('保存超时时间...');const r1=await apiRequest('/api/cache/config',{method:'POST',body:JSON.stringify({timeout:timeout})});if(!r1){console.error('保存超时时间请求失败');return}const d1=await r1.json();console.log('超时时间保存结果:',d1);if(!d1.success){console.error('保存超时时间失败:',d1);return showToast('保存超时时间失败','error')}console.log('保存域名...');const r2=await apiRequest('/api/cache/base-url',{method:'POST',body:JSON.stringify({base_url:baseUrl})});if(!r2){console.error('保存域名请求失败');return}const d2=await r2.json();console.log('域名保存结果:',d2);if(d2.success){showToast('缓存配置保存成功','success');console.log('等待配置文件写入完成...');await new Promise(r=>setTimeout(r,200));console.log('重新加载配置...');await loadCacheConfig()}else{console.error('保存域名失败:',d2);showToast('保存域名失败','error')}}catch(e){console.error('保存失败:',e);showToast('保存失败: '+e.message,'error')}},
746
  saveGenerationTimeout=async()=>{const imageTimeout=parseInt($('cfgImageTimeout').value)||300,videoTimeout=parseInt($('cfgVideoTimeout').value)||1500;console.log('保存生成超时配置:',{imageTimeout,videoTimeout});if(imageTimeout<60||imageTimeout>3600)return showToast('图片超时时间必须在 60-3600 秒之间','error');if(videoTimeout<60||videoTimeout>7200)return showToast('视频超时时间必须在 60-7200 秒之间','error');try{const r=await apiRequest('/api/generation/timeout',{method:'POST',body:JSON.stringify({image_timeout:imageTimeout,video_timeout:videoTimeout})});if(!r){console.error('保存请求失败');return}const d=await r.json();console.log('保存结果:',d);if(d.success){showToast('生成超时配置保存成功','success');await new Promise(r=>setTimeout(r,200));await loadGenerationTimeout()}else{console.error('保存失败:',d);showToast('保存失败','error')}}catch(e){console.error('保存失败:',e);showToast('保存失败: '+e.message,'error')}},
747
+ toggleCaptchaOptions=()=>{const method=$('cfgCaptchaMethod').value;$('yescaptchaOptions').style.display=method==='yescaptcha'?'block':'none';$('capmonsterOptions').classList.toggle('hidden',method!=='capmonster');$('ezcaptchaOptions').classList.toggle('hidden',method!=='ezcaptcha');$('capsolverOptions').classList.toggle('hidden',method!=='capsolver');$('browserCaptchaOptions').classList.toggle('hidden',method!=='browser')},
748
  toggleBrowserProxyInput=()=>{const enabled=$('cfgBrowserProxyEnabled').checked;$('browserProxyUrlInput').classList.toggle('hidden',!enabled)},
749
+ loadCaptchaConfig=async()=>{try{console.log('开始加载验证码配置...');const r=await apiRequest('/api/captcha/config');if(!r){console.error('API请求失败');return}const d=await r.json();console.log('验证码配置数据:',d);$('cfgCaptchaMethod').value=d.captcha_method||'yescaptcha';$('cfgYescaptchaApiKey').value=d.yescaptcha_api_key||'';$('cfgYescaptchaBaseUrl').value=d.yescaptcha_base_url||'https://api.yescaptcha.com';$('cfgCapmonsterApiKey').value=d.capmonster_api_key||'';$('cfgCapmonsterBaseUrl').value=d.capmonster_base_url||'https://api.capmonster.cloud';$('cfgEzcaptchaApiKey').value=d.ezcaptcha_api_key||'';$('cfgEzcaptchaBaseUrl').value=d.ezcaptcha_base_url||'https://api.ez-captcha.com';$('cfgCapsolverApiKey').value=d.capsolver_api_key||'';$('cfgCapsolverBaseUrl').value=d.capsolver_base_url||'https://api.capsolver.com';$('cfgBrowserProxyEnabled').checked=d.browser_proxy_enabled||false;$('cfgBrowserProxyUrl').value=d.browser_proxy_url||'';toggleCaptchaOptions();toggleBrowserProxyInput();console.log('验证码配置加载成功')}catch(e){console.error('加载验证码配置失败:',e);showToast('加载验证码配置失败: '+e.message,'error')}},
750
+ saveCaptchaConfig=async()=>{const method=$('cfgCaptchaMethod').value,yesApiKey=$('cfgYescaptchaApiKey').value.trim(),yesBaseUrl=$('cfgYescaptchaBaseUrl').value.trim(),capApiKey=$('cfgCapmonsterApiKey').value.trim(),capBaseUrl=$('cfgCapmonsterBaseUrl').value.trim(),ezApiKey=$('cfgEzcaptchaApiKey').value.trim(),ezBaseUrl=$('cfgEzcaptchaBaseUrl').value.trim(),solverApiKey=$('cfgCapsolverApiKey').value.trim(),solverBaseUrl=$('cfgCapsolverBaseUrl').value.trim(),browserProxyEnabled=$('cfgBrowserProxyEnabled').checked,browserProxyUrl=$('cfgBrowserProxyUrl').value.trim();console.log('保存验证码配置:',{method,yesApiKey,yesBaseUrl,capApiKey,capBaseUrl,ezApiKey,ezBaseUrl,solverApiKey,solverBaseUrl,browserProxyEnabled,browserProxyUrl});try{const r=await apiRequest('/api/captcha/config',{method:'POST',body:JSON.stringify({captcha_method:method,yescaptcha_api_key:yesApiKey,yescaptcha_base_url:yesBaseUrl,capmonster_api_key:capApiKey,capmonster_base_url:capBaseUrl,ezcaptcha_api_key:ezApiKey,ezcaptcha_base_url:ezBaseUrl,capsolver_api_key:solverApiKey,capsolver_base_url:solverBaseUrl,browser_proxy_enabled:browserProxyEnabled,browser_proxy_url:browserProxyUrl})});if(!r){console.error('保存请求失败');return}const d=await r.json();console.log('保存结果:',d);if(d.success){showToast('验证码配置保存成功','success');await new Promise(r=>setTimeout(r,200));await loadCaptchaConfig()}else{console.error('保存失败:',d);showToast(d.message||'保存失败','error')}}catch(e){console.error('保存失败:',e);showToast('保存失败: '+e.message,'error')}},
751
  loadPluginConfig=async()=>{try{const r=await apiRequest('/api/plugin/config');if(!r)return;const d=await r.json();if(d.success&&d.config){$('cfgPluginConnectionUrl').value=d.config.connection_url||'';$('cfgPluginConnectionToken').value=d.config.connection_token||'';$('cfgAutoEnableOnUpdate').checked=d.config.auto_enable_on_update||false}}catch(e){console.error('加���插件配置失败:',e);showToast('加载插件配置失败: '+e.message,'error')}},
752
  savePluginConfig=async()=>{const token=$('cfgPluginConnectionToken').value.trim();const autoEnable=$('cfgAutoEnableOnUpdate').checked;try{const r=await apiRequest('/api/plugin/config',{method:'POST',body:JSON.stringify({connection_token:token,auto_enable_on_update:autoEnable})});if(!r)return;const d=await r.json();if(d.success){showToast('插件配置保存成功','success');await loadPluginConfig()}else{showToast(d.message||'保存失败','error')}}catch(e){showToast('保存失败: '+e.message,'error')}},
753
  copyConnectionUrl=()=>{const url=$('cfgPluginConnectionUrl').value;if(!url){showToast('连接接口为空','error');return}navigator.clipboard.writeText(url).then(()=>showToast('连接接口已复制','success')).catch(()=>showToast('复制失败','error'))},
754
  copyConnectionToken=()=>{const token=$('cfgPluginConnectionToken').value;if(!token){showToast('连接Token为空','error');return}navigator.clipboard.writeText(token).then(()=>showToast('连接Token已复制','success')).catch(()=>showToast('复制失败','error'))},
755
+ generateRandomToken=()=>{const chars='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';let token='';for(let i=0;i<32;i++){token+=chars.charAt(Math.floor(Math.random()*chars.length))}$('cfgPluginConnectionToken').value=token;showToast('随机Token已生成','success')},
756
  toggleATAutoRefresh=async()=>{try{const enabled=$('atAutoRefreshToggle').checked;const r=await apiRequest('/api/token-refresh/enabled',{method:'POST',body:JSON.stringify({enabled:enabled})});if(!r){$('atAutoRefreshToggle').checked=!enabled;return}const d=await r.json();if(d.success){showToast(enabled?'AT自动刷新已启用':'AT自动刷新已禁用','success')}else{showToast('操作失败: '+(d.detail||'未知错误'),'error');$('atAutoRefreshToggle').checked=!enabled}}catch(e){showToast('操作失败: '+e.message,'error');$('atAutoRefreshToggle').checked=!enabled}},
757
  loadATAutoRefreshConfig=async()=>{try{const r=await apiRequest('/api/token-refresh/config');if(!r)return;const d=await r.json();if(d.success&&d.config){$('atAutoRefreshToggle').checked=d.config.at_auto_refresh_enabled||false}else{console.error('AT自动刷新配置数据格式错误:',d)}}catch(e){console.error('加载AT自动刷新配置失败:',e)}},
758
  loadLogs=async()=>{try{const r=await apiRequest('/api/logs?limit=100');if(!r)return;const logs=await r.json();window.allLogs=logs;const tb=$('logsTableBody');tb.innerHTML=logs.map(l=>`<tr><td class="py-2.5 px-3">${l.operation}</td><td class="py-2.5 px-3"><span class="text-xs ${l.token_email?'text-blue-600':'text-muted-foreground'}">${l.token_email||'未知'}</span></td><td class="py-2.5 px-3"><span class="inline-flex items-center rounded px-2 py-0.5 text-xs ${l.status_code===200?'bg-green-50 text-green-700':'bg-red-50 text-red-700'}">${l.status_code}</span></td><td class="py-2.5 px-3">${l.duration.toFixed(2)}</td><td class="py-2.5 px-3 text-xs text-muted-foreground">${l.created_at?new Date(l.created_at).toLocaleString('zh-CN'):'-'}</td><td class="py-2.5 px-3"><button onclick="showLogDetail(${l.id})" class="inline-flex items-center justify-center rounded-md hover:bg-blue-50 hover:text-blue-700 h-7 px-2 text-xs">查看</button></td></tr>`).join('')}catch(e){console.error('加载日志失败:',e)}},