dantynoel commited on
Commit
3cb9e04
·
1 Parent(s): 89cede9

fork: 个人用途可以在内置浏览器上登录对应的谷歌账号,测试可行

Browse files
.gitignore CHANGED
@@ -52,3 +52,5 @@ logs.txt
52
  *.tmp
53
  *.bak
54
  *.cache
 
 
 
52
  *.tmp
53
  *.bak
54
  *.cache
55
+
56
+ browser_data
config/setting.toml CHANGED
@@ -12,7 +12,7 @@ max_poll_attempts = 200
12
 
13
  [server]
14
  host = "0.0.0.0"
15
- port = 8000
16
 
17
  [debug]
18
  enabled = false
 
12
 
13
  [server]
14
  host = "0.0.0.0"
15
+ port = 8106
16
 
17
  [debug]
18
  enabled = false
request.py ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import re
4
+ import base64
5
+ import aiohttp # Async test. Need to install
6
+ import asyncio
7
+
8
+
9
+ # --- 配置区域 ---
10
+ BASE_URL = os.getenv('GEMINI_FLOW2API_URL', 'http://127.0.0.1:8106')
11
+ BACKEND_URL = BASE_URL + "/v1/chat/completions"
12
+ API_KEY = os.getenv('GEMINI_FLOW2API_APIKEY', 'Bearer han1234')
13
+ if API_KEY is None:
14
+ raise ValueError('[gemini flow2api] api key not set')
15
+ MODEL_LANDSCAPE = "gemini-3.0-pro-image-landscape"
16
+ MODEL_PORTRAIT = "gemini-3.0-pro-image-portrait"
17
+
18
+ # 修改: 增加 model 参数,默认为 None
19
+ async def request_backend_generation(
20
+ prompt: str,
21
+ images: list[bytes] = None,
22
+ model: str = None) -> bytes | None:
23
+ """
24
+ 请求后端生成图片。
25
+ :param prompt: 提示词
26
+ :param images: 图片二进制列表
27
+ :param model: 指定模型名称 (可选)
28
+ :return: 成功返回图片bytes,失败返回None
29
+ """
30
+ # 更新token
31
+ images = images or []
32
+
33
+ # 逻辑: 如果未指定 model,默认使用 Landscape
34
+ use_model = model if model else MODEL_LANDSCAPE
35
+
36
+ # 1. 构造 Payload
37
+ if images:
38
+ content_payload = [{"type": "text", "text": prompt}]
39
+ print(f"[Backend] 正在处理 {len(images)} 张图片输入...")
40
+ for img_bytes in images:
41
+ b64_str = base64.b64encode(img_bytes).decode('utf-8')
42
+ content_payload.append({
43
+ "type": "image_url",
44
+ "image_url": {"url": f"data:image/jpeg;base64,{b64_str}"}
45
+ })
46
+ else:
47
+ content_payload = prompt
48
+
49
+ payload = {
50
+ "model": use_model, # 使用选定的模型
51
+ "messages": [{"role": "user", "content": content_payload}],
52
+ "stream": True
53
+ }
54
+
55
+ headers = {
56
+ "Authorization": API_KEY,
57
+ "Content-Type": "application/json"
58
+ }
59
+
60
+ image_url = None
61
+ print(f"[Backend] Model: {use_model} | 发起请求: {prompt[:20]}...")
62
+
63
+ try:
64
+ async with aiohttp.ClientSession() as session:
65
+ async with session.post(BACKEND_URL, json=payload, headers=headers, timeout=120) as response:
66
+ if response.status != 200:
67
+ err_text = await response.text()
68
+ content = response.content
69
+ print(f"[Backend Error] Status {response.status}: {err_text} {content}")
70
+ raise Exception(f"API Error: {response.status}: {err_text}")
71
+
72
+ async for line in response.content:
73
+ line_str = line.decode('utf-8').strip()
74
+ if line_str.startswith('{"error'):
75
+ chunk = json.loads(data_str)
76
+ delta = chunk.get("choices", [{}])[0].get("delta", {})
77
+ msg = delta['reasoning_content']
78
+ if '401' in msg:
79
+ msg += '\nAccess Token 已失效,需重新配置。'
80
+ elif '400' in msg:
81
+ msg += '\n返回内容被拦截。'
82
+ raise Exception(msg)
83
+
84
+ if not line_str or not line_str.startswith('data: '):
85
+ continue
86
+
87
+ data_str = line_str[6:]
88
+ if data_str == '[DONE]':
89
+ break
90
+
91
+ try:
92
+ chunk = json.loads(data_str)
93
+ delta = chunk.get("choices", [{}])[0].get("delta", {})
94
+
95
+ # 打印思考过程
96
+ if "reasoning_content" in delta:
97
+ print(delta['reasoning_content'], end="", flush=True)
98
+
99
+ # 提取内容中的图片链接
100
+ if "content" in delta:
101
+ content_text = delta["content"]
102
+ img_match = re.search(r'!\[.*?\]\((.*?)\)', content_text)
103
+ if img_match:
104
+ image_url = img_match.group(1)
105
+ print(f"\n[Backend] 捕获图片链接: {image_url}")
106
+ except json.JSONDecodeError:
107
+ continue
108
+
109
+ # 3. 下载生成的图片
110
+ if image_url:
111
+ async with session.get(image_url) as img_resp:
112
+ if img_resp.status == 200:
113
+ image_bytes = await img_resp.read()
114
+ return image_bytes
115
+ else:
116
+ print(f"[Backend Error] 图片下载失败: {img_resp.status}")
117
+ except Exception as e:
118
+ print(f"[Backend Exception] {e}")
119
+ raise e
120
+
121
+ return None
122
+
123
+ if __name__ == '__main__':
124
+ async def main():
125
+ print("=== AI 绘图接口测试 ===")
126
+ user_prompt = input("请输入提示词 (例如 '一只猫'): ").strip()
127
+ if not user_prompt:
128
+ user_prompt = "A cute cat in the garden"
129
+
130
+ print(f"正在请求: {user_prompt}")
131
+
132
+ # 这里的 images 传空列表用于测试文生图
133
+ # 如果想测试图生图,你需要手动读取本地文件:
134
+ # with open("output_test.jpg", "rb") as f: img_data = f.read()
135
+ # result = await request_backend_generation(user_prompt, [img_data])
136
+
137
+ result = await request_backend_generation(user_prompt)
138
+
139
+ if result:
140
+ filename = "output_test.jpg"
141
+ with open(filename, "wb") as f:
142
+ f.write(result)
143
+ print(f"\n[Success] 图片已保存为 {filename},大小: {len(result)} bytes")
144
+ else:
145
+ print("\n[Failed] 生成失败")
146
+
147
+ # 运行测试
148
+ if os.name == 'nt': # Windows 兼容性
149
+ asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
150
+ asyncio.run(main())
src/main.py CHANGED
@@ -74,7 +74,12 @@ async def lifespan(app: FastAPI):
74
 
75
  # Initialize browser captcha service if needed
76
  browser_service = None
77
- if captcha_config.captcha_method == "browser":
 
 
 
 
 
78
  from .services.browser_captcha import BrowserCaptchaService
79
  browser_service = await BrowserCaptchaService.get_instance(db)
80
  print("✓ Browser captcha service initialized (headless mode)")
 
74
 
75
  # Initialize browser captcha service if needed
76
  browser_service = None
77
+ if True:
78
+ from .services.browser_captcha_personal import BrowserCaptchaService
79
+ browser_service = await BrowserCaptchaService.get_instance(db)
80
+ await browser_service.open_login_window()
81
+ print("✓ Browser captcha service initialized (webui mode)")
82
+ elif captcha_config.captcha_method == "browser":
83
  from .services.browser_captcha import BrowserCaptchaService
84
  browser_service = await BrowserCaptchaService.get_instance(db)
85
  print("✓ Browser captcha service initialized (headless mode)")
src/services/browser_captcha_personal.py ADDED
@@ -0,0 +1,197 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import time
3
+ import re
4
+ import os
5
+ from typing import Optional, Dict
6
+ from playwright.async_api import async_playwright, BrowserContext, Page
7
+
8
+ from ..core.logger import debug_logger
9
+
10
+ # ... (保持原来的 parse_proxy_url 和 validate_browser_proxy_url 函数不变) ...
11
+ def parse_proxy_url(proxy_url: str) -> Optional[Dict[str, str]]:
12
+ """解析代理URL,分离协议、主机、端口、认证信息"""
13
+ proxy_pattern = r'^(socks5|http|https)://(?:([^:]+):([^@]+)@)?([^:]+):(\d+)$'
14
+ match = re.match(proxy_pattern, proxy_url)
15
+ if match:
16
+ protocol, username, password, host, port = match.groups()
17
+ proxy_config = {'server': f'{protocol}://{host}:{port}'}
18
+ if username and password:
19
+ proxy_config['username'] = username
20
+ proxy_config['password'] = password
21
+ return proxy_config
22
+ return None
23
+
24
+ class BrowserCaptchaService:
25
+ """浏览器自动化获取 reCAPTCHA token(持久化有头模式)"""
26
+
27
+ _instance: Optional['BrowserCaptchaService'] = None
28
+ _lock = asyncio.Lock()
29
+
30
+ def __init__(self, db=None):
31
+ """初始化服务"""
32
+ # === 修改点 1: 设置为有头模式 ===
33
+ self.headless = False
34
+ self.playwright = None
35
+ # 注意: 持久化模式下,我们操作的是 context 而不是 browser
36
+ self.context: Optional[BrowserContext] = None
37
+ self._initialized = False
38
+ self.website_key = "6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV"
39
+ self.db = db
40
+
41
+ # === 修改点 2: 指定本地数据存储目录 ===
42
+ # 这会在脚本运行目录下生成 browser_data 文件夹,用于保存你的登录状态
43
+ self.user_data_dir = os.path.join(os.getcwd(), "browser_data")
44
+
45
+ @classmethod
46
+ async def get_instance(cls, db=None) -> 'BrowserCaptchaService':
47
+ if cls._instance is None:
48
+ async with cls._lock:
49
+ if cls._instance is None:
50
+ cls._instance = cls(db)
51
+ # 首次调用不强制初始化,等待 get_token 时懒加载,或者可以在这里await
52
+ return cls._instance
53
+
54
+ async def initialize(self):
55
+ """初始化持久化浏览器上下文"""
56
+ if self._initialized and self.context:
57
+ return
58
+
59
+ try:
60
+ proxy_url = None
61
+ if self.db:
62
+ captcha_config = await self.db.get_captcha_config()
63
+ if captcha_config.browser_proxy_enabled and captcha_config.browser_proxy_url:
64
+ proxy_url = captcha_config.browser_proxy_url
65
+
66
+ debug_logger.log_info(f"[BrowserCaptcha] 正在启动浏览器 (用户数据目录: {self.user_data_dir})...")
67
+ self.playwright = await async_playwright().start()
68
+
69
+ # 配置启动参数
70
+ launch_options = {
71
+ 'headless': self.headless,
72
+ 'user_data_dir': self.user_data_dir, # 指定数据目录
73
+ 'viewport': {'width': 1280, 'height': 720}, # 设置默认窗口大小
74
+ 'args': [
75
+ '--disable-blink-features=AutomationControlled',
76
+ '--disable-infobars',
77
+ '--no-sandbox',
78
+ '--disable-setuid-sandbox',
79
+ ]
80
+ }
81
+
82
+ # 代理配置
83
+ if proxy_url:
84
+ proxy_config = parse_proxy_url(proxy_url)
85
+ if proxy_config:
86
+ launch_options['proxy'] = proxy_config
87
+ debug_logger.log_info(f"[BrowserCaptcha] 使用代理: {proxy_config['server']}")
88
+
89
+ # === 修改点 3: 使用 launch_persistent_context ===
90
+ # 这会启动一个带有状态的浏览器窗口
91
+ self.context = await self.playwright.chromium.launch_persistent_context(**launch_options)
92
+
93
+ # 设置默认超时
94
+ self.context.set_default_timeout(30000)
95
+
96
+ self._initialized = True
97
+ debug_logger.log_info(f"[BrowserCaptcha] ✅ 浏览器已启动 (Profile: {self.user_data_dir})")
98
+
99
+ except Exception as e:
100
+ debug_logger.log_error(f"[BrowserCaptcha] ❌ 浏览器启动失败: {str(e)}")
101
+ raise
102
+
103
+ async def get_token(self, project_id: str) -> Optional[str]:
104
+ """获取 reCAPTCHA token"""
105
+ # 确保浏览器已启动
106
+ if not self._initialized or not self.context:
107
+ await self.initialize()
108
+
109
+ start_time = time.time()
110
+ page: Optional[Page] = None
111
+
112
+ try:
113
+ # === 修改点 4: 在现有上下文中新建标签页,而不是新建上下文 ===
114
+ # 这样可以复用该上下文中已保存的 Cookie (你的登录状态)
115
+ page = await self.context.new_page()
116
+
117
+ website_url = f"https://labs.google/fx/tools/flow/project/{project_id}"
118
+ debug_logger.log_info(f"[BrowserCaptcha] 访问页面: {website_url}")
119
+
120
+ # 访问页面
121
+ try:
122
+ await page.goto(website_url, wait_until="domcontentloaded")
123
+ except Exception as e:
124
+ debug_logger.log_warning(f"[BrowserCaptcha] 页面加载警告: {str(e)}")
125
+
126
+ # --- 关键点:如果需要人工介入 ---
127
+ # 你可以在这里加入一段逻辑,如果是第一次运行,或者检测到未登录,
128
+ # 可以暂停脚本,等你手动操作完再继续。
129
+ # 例如: await asyncio.sleep(30)
130
+
131
+ # ... (中间注入脚本和执行 reCAPTCHA 的代码逻辑与原版完全一致,此处省略以节省篇幅) ...
132
+ # ... 请将原代码中从 "检查并注入 reCAPTCHA v3 脚本" 到 token 获取部分的代码复制到这里 ...
133
+
134
+ # 这里为了演示,简写注入逻辑(请保留你原有的完整注入逻辑):
135
+ script_loaded = await page.evaluate("() => { return !!(window.grecaptcha && window.grecaptcha.execute); }")
136
+ if not script_loaded:
137
+ await page.evaluate(f"""
138
+ () => {{
139
+ const script = document.createElement('script');
140
+ script.src = 'https://www.google.com/recaptcha/api.js?render={self.website_key}';
141
+ script.async = true; script.defer = true;
142
+ document.head.appendChild(script);
143
+ }}
144
+ """)
145
+ # 等待加载... (保留你原有的等待循环)
146
+ await page.wait_for_timeout(2000)
147
+
148
+ # 执行获取 Token (保留你原有的 execute 逻辑)
149
+ token = await page.evaluate(f"""
150
+ async () => {{
151
+ try {{
152
+ return await window.grecaptcha.execute('{self.website_key}', {{ action: 'FLOW_GENERATION' }});
153
+ }} catch (e) {{ return null; }}
154
+ }}
155
+ """)
156
+
157
+ if token:
158
+ debug_logger.log_info(f"[BrowserCaptcha] ✅ Token获取成功")
159
+ return token
160
+ else:
161
+ debug_logger.log_error("[BrowserCaptcha] Token获取失败")
162
+ return None
163
+
164
+ except Exception as e:
165
+ debug_logger.log_error(f"[BrowserCaptcha] 异常: {str(e)}")
166
+ return None
167
+ finally:
168
+ # === 修改点 5: 只关闭 Page (标签页),不关闭 Context (浏览器窗口) ===
169
+ if page:
170
+ try:
171
+ await page.close()
172
+ except:
173
+ pass
174
+
175
+ async def close(self):
176
+ """完全关闭浏览器(清理资源时调用)"""
177
+ try:
178
+ if self.context:
179
+ await self.context.close() # 这会关闭整个浏览器窗口
180
+ self.context = None
181
+
182
+ if self.playwright:
183
+ await self.playwright.stop()
184
+ self.playwright = None
185
+
186
+ self._initialized = False
187
+ debug_logger.log_info("[BrowserCaptcha] 浏览器服务已关闭")
188
+ except Exception as e:
189
+ debug_logger.log_error(f"[BrowserCaptcha] 关闭异常: {str(e)}")
190
+
191
+ # 增加一个辅助方法,用于手动登录
192
+ async def open_login_window(self):
193
+ """调用此方法打开一个永久窗口供你登录Google"""
194
+ await self.initialize()
195
+ page = await self.context.new_page()
196
+ await page.goto("https://accounts.google.com/")
197
+ print("请在打开的浏览器中登录账号。登录完成后,无需关闭浏览器,脚本下次运行时会自动使用此状态。")
src/services/flow_client.py CHANGED
@@ -687,8 +687,16 @@ class FlowClient:
687
  """获取reCAPTCHA token - 支持两种方式"""
688
  captcha_method = config.captcha_method
689
 
 
 
 
 
 
 
 
 
690
  # 浏览器打码
691
- if captcha_method == "browser":
692
  try:
693
  from .browser_captcha import BrowserCaptchaService
694
  service = await BrowserCaptchaService.get_instance(self.proxy_manager)
@@ -696,61 +704,61 @@ class FlowClient:
696
  except Exception as e:
697
  debug_logger.log_error(f"[reCAPTCHA Browser] error: {str(e)}")
698
  return None
 
 
 
 
 
 
699
 
700
- # YesCaptcha打码
701
- client_key = config.yescaptcha_api_key
702
- if not client_key:
703
- debug_logger.log_info("[reCAPTCHA] API key not configured, skipping")
704
- return None
705
-
706
- website_key = "6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV"
707
- website_url = f"https://labs.google/fx/tools/flow/project/{project_id}"
708
- base_url = config.yescaptcha_base_url
709
- page_action = "FLOW_GENERATION"
710
 
711
- try:
712
- async with AsyncSession() as session:
713
- create_url = f"{base_url}/createTask"
714
- create_data = {
715
- "clientKey": client_key,
716
- "task": {
717
- "websiteURL": website_url,
718
- "websiteKey": website_key,
719
- "type": "RecaptchaV3TaskProxylessM1",
720
- "pageAction": page_action
 
721
  }
722
- }
723
 
724
- result = await session.post(create_url, json=create_data, impersonate="chrome110")
725
- result_json = result.json()
726
- task_id = result_json.get('taskId')
727
 
728
- debug_logger.log_info(f"[reCAPTCHA] created task_id: {task_id}")
729
 
730
- if not task_id:
731
- return None
732
 
733
- get_url = f"{base_url}/getTaskResult"
734
- for i in range(40):
735
- get_data = {
736
- "clientKey": client_key,
737
- "taskId": task_id
738
- }
739
- result = await session.post(get_url, json=get_data, impersonate="chrome110")
740
- result_json = result.json()
741
 
742
- debug_logger.log_info(f"[reCAPTCHA] polling #{i+1}: {result_json}")
743
 
744
- solution = result_json.get('solution', {})
745
- response = solution.get('gRecaptchaResponse')
746
 
747
- if response:
748
- return response
749
 
750
- time.sleep(3)
751
 
752
- return None
753
 
754
- except Exception as e:
755
- debug_logger.log_error(f"[reCAPTCHA] error: {str(e)}")
756
- return None
 
687
  """获取reCAPTCHA token - 支持两种方式"""
688
  captcha_method = config.captcha_method
689
 
690
+ if True:
691
+ try:
692
+ from .browser_captcha_personal import BrowserCaptchaService
693
+ service = await BrowserCaptchaService.get_instance(self.proxy_manager)
694
+ return await service.get_token(project_id)
695
+ except Exception as e:
696
+ debug_logger.log_error(f"[reCAPTCHA Browser] error: {str(e)}")
697
+ return None
698
  # 浏览器打码
699
+ elif captcha_method == "browser":
700
  try:
701
  from .browser_captcha import BrowserCaptchaService
702
  service = await BrowserCaptchaService.get_instance(self.proxy_manager)
 
704
  except Exception as e:
705
  debug_logger.log_error(f"[reCAPTCHA Browser] error: {str(e)}")
706
  return None
707
+ else:
708
+ # YesCaptcha打码
709
+ client_key = config.yescaptcha_api_key
710
+ if not client_key:
711
+ debug_logger.log_info("[reCAPTCHA] API key not configured, skipping")
712
+ return None
713
 
714
+ website_key = "6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV"
715
+ website_url = f"https://labs.google/fx/tools/flow/project/{project_id}"
716
+ base_url = config.yescaptcha_base_url
717
+ page_action = "FLOW_GENERATION"
 
 
 
 
 
 
718
 
719
+ try:
720
+ async with AsyncSession() as session:
721
+ create_url = f"{base_url}/createTask"
722
+ create_data = {
723
+ "clientKey": client_key,
724
+ "task": {
725
+ "websiteURL": website_url,
726
+ "websiteKey": website_key,
727
+ "type": "RecaptchaV3TaskProxylessM1",
728
+ "pageAction": page_action
729
+ }
730
  }
 
731
 
732
+ result = await session.post(create_url, json=create_data, impersonate="chrome110")
733
+ result_json = result.json()
734
+ task_id = result_json.get('taskId')
735
 
736
+ debug_logger.log_info(f"[reCAPTCHA] created task_id: {task_id}")
737
 
738
+ if not task_id:
739
+ return None
740
 
741
+ get_url = f"{base_url}/getTaskResult"
742
+ for i in range(40):
743
+ get_data = {
744
+ "clientKey": client_key,
745
+ "taskId": task_id
746
+ }
747
+ result = await session.post(get_url, json=get_data, impersonate="chrome110")
748
+ result_json = result.json()
749
 
750
+ debug_logger.log_info(f"[reCAPTCHA] polling #{i+1}: {result_json}")
751
 
752
+ solution = result_json.get('solution', {})
753
+ response = solution.get('gRecaptchaResponse')
754
 
755
+ if response:
756
+ return response
757
 
758
+ time.sleep(3)
759
 
760
+ return None
761
 
762
+ except Exception as e:
763
+ debug_logger.log_error(f"[reCAPTCHA] error: {str(e)}")
764
+ return None