22333Misaka commited on
Commit
efd8f02
·
verified ·
1 Parent(s): 586c72a

Upload 5 files

Browse files
Files changed (5) hide show
  1. Dockerfile +27 -0
  2. app.py +787 -0
  3. browser_worker.py +881 -0
  4. config.py +617 -0
  5. email_manager.py +121 -0
Dockerfile ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim-bullseye
2
+
3
+ # 安装必要的系统依赖
4
+ # DrissionPage 需要浏览器才能工作
5
+ RUN apt-get update && apt-get install -y \
6
+ chromium \
7
+ chromium-driver \
8
+ && rm -rf /var/lib/apt/lists/*
9
+
10
+ WORKDIR /app
11
+
12
+ # 复制项目文件
13
+ COPY . .
14
+
15
+ # 安装 Python 依赖
16
+ RUN pip install --no-cache-dir \
17
+ flask \
18
+ python-dotenv \
19
+ DrissionPage \
20
+ requests
21
+
22
+ # 暴露端口
23
+ ENV PORT=7860
24
+ EXPOSE 7860
25
+
26
+ # 运行应用
27
+ CMD ["python", "app.py"]
app.py ADDED
@@ -0,0 +1,787 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import json
4
+ import threading
5
+ import queue
6
+ import time
7
+ import uuid
8
+ from datetime import datetime
9
+ from typing import Dict, List, Optional
10
+ from functools import wraps
11
+
12
+ from flask import Flask, request, jsonify, render_template, Response
13
+ from dotenv import load_dotenv
14
+
15
+ from browser_worker import BrowserWorker, AccountInfo, AccountStatus
16
+ from email_manager import EmailManager
17
+ from config import Config, setup_logging
18
+ import logging
19
+
20
+ log = logging.getLogger('werkzeug')
21
+ log.setLevel(logging.ERROR)
22
+ logger = logging.getLogger(__name__)
23
+
24
+ # 加载环境变量
25
+ load_dotenv()
26
+
27
+ if getattr(sys, 'frozen', False):
28
+ template_folder = os.path.join(os.getcwd(), 'templates')
29
+ static_folder = os.path.join(os.getcwd(), 'static')
30
+ app = Flask(__name__, template_folder=template_folder, static_folder=static_folder)
31
+ else:
32
+ app = Flask(__name__)
33
+ app.secret_key = os.getenv('SECRET_KEY', 'your-secret-key-change-in-production')
34
+
35
+ # 全局配置和状态
36
+ setup_logging()
37
+ config = Config()
38
+ accounts: Dict[str, AccountInfo] = {} # email -> AccountInfo
39
+ workers: Dict[int, BrowserWorker] = {} # worker_id -> BrowserWorker
40
+ task_queue = queue.Queue()
41
+ accounts_lock = threading.Lock()
42
+ workers_lock = threading.Lock()
43
+
44
+ # 管理员认证
45
+ ADMIN_USERNAME = os.getenv('ADMIN_USERNAME', 'admin')
46
+ ADMIN_PASSWORD = os.getenv('ADMIN_PASSWORD', 'admin123')
47
+
48
+
49
+ def check_auth(username, password):
50
+ """验证管理员凭据"""
51
+ return username == ADMIN_USERNAME and password == ADMIN_PASSWORD
52
+
53
+
54
+ def authenticate():
55
+ """返回401认证请求"""
56
+ return Response(
57
+ json.dumps({'error': '需要认证'}),
58
+ 401,
59
+ {'WWW-Authenticate': 'Basic realm="Admin Area"'}
60
+ )
61
+
62
+
63
+ def requires_auth(f):
64
+ """认证装饰器"""
65
+ @wraps(f)
66
+ def decorated(*args, **kwargs):
67
+ auth = request.authorization
68
+ if not auth or not check_auth(auth.username, auth.password):
69
+ api_key = (
70
+ request.headers.get("Authorization", "").replace("Bearer ", "") or
71
+ request.headers.get("X-API-Key")
72
+ )
73
+ adminpassword = os.getenv('ADMIN_PASSWORD', '')
74
+
75
+ if api_key != os.getenv('ADMIN_TOKEN', '') and api_key != os.getenv('ADMIN_PASSWORD', ''):
76
+ return authenticate()
77
+ return f(*args, **kwargs)
78
+ return decorated
79
+
80
+
81
+ def get_available_worker_slot() -> Optional[int]:
82
+ """获取可用的工作槽位"""
83
+ max_workers = config.get_max_workers()
84
+ with workers_lock:
85
+ active_count = sum(1 for w in workers.values() if w.is_alive())
86
+ if active_count >= max_workers:
87
+ return None
88
+ for i in range(max_workers):
89
+ if i not in workers or not workers[i].is_alive():
90
+ return i
91
+ return None
92
+
93
+
94
+ def start_worker(worker_id: int, account: AccountInfo, mode: str = "register"):
95
+ """启动工作线程"""
96
+ worker = BrowserWorker(
97
+ worker_id=worker_id,
98
+ account=account,
99
+ config=config,
100
+ mode=mode,
101
+ on_update=on_account_update,
102
+ on_complete=on_worker_complete
103
+ )
104
+ with workers_lock:
105
+ workers[worker_id] = worker
106
+ worker.start()
107
+ return worker
108
+
109
+
110
+ def on_account_update(email: str, account: AccountInfo):
111
+ """账号更新回调"""
112
+ with accounts_lock:
113
+ accounts[email] = account
114
+
115
+
116
+ def on_worker_complete(worker_id: int, email: str, success: bool):
117
+ """工作线程完成回调"""
118
+ with workers_lock:
119
+ if worker_id in workers:
120
+ del workers[worker_id]
121
+
122
+
123
+ def get_email_config_by_domain(email: str) -> Optional[Dict]:
124
+ """根据邮箱地址获取对应的邮箱配置"""
125
+ if '@' not in email:
126
+ return None
127
+
128
+ domain = email.split('@')[1].lower()
129
+ email_configs = config.get_email_configs()
130
+
131
+ for cfg in email_configs:
132
+ if cfg.get('email_domain', '').lower() == domain:
133
+ return cfg
134
+
135
+ return None
136
+
137
+
138
+ def create_account_from_email(email: str) -> Optional[AccountInfo]:
139
+ """根据邮箱地址创建账号(用于刷新场景)"""
140
+ email_config = get_email_config_by_domain(email)
141
+ if not email_config:
142
+ return None
143
+
144
+ # 尝试获取JWT
145
+ email_manager = EmailManager(
146
+ email_config['worker_domain'],
147
+ email_config['email_domain'],
148
+ email_config['admin_password']
149
+ )
150
+
151
+ account = AccountInfo(
152
+ email=email,
153
+ jwt= "",
154
+ status=AccountStatus.PENDING,
155
+ email_config=email_config,
156
+ created_at=datetime.now().isoformat()
157
+ )
158
+
159
+ return account
160
+
161
+
162
+ # ==================== API 路由 ====================
163
+
164
+ @app.route('/')
165
+ def index():
166
+ """管理面板首页"""
167
+ return render_template('index.html')
168
+
169
+
170
+ @app.route('/api/login', methods=['POST'])
171
+ def login():
172
+ """登录验证"""
173
+ data = request.get_json()
174
+ username = data.get('username', '')
175
+ password = data.get('password', '')
176
+
177
+ if check_auth(username, password):
178
+ token = str(uuid.uuid4())
179
+ return jsonify({
180
+ 'success': True,
181
+ 'token': token,
182
+ 'message': '登录成功'
183
+ })
184
+
185
+ return jsonify({
186
+ 'success': False,
187
+ 'message': '用户名或密码错误'
188
+ }), 401
189
+
190
+
191
+ @app.route('/api/accounts', methods=['POST'])
192
+ @requires_auth
193
+ def create_account():
194
+ """创建新账号"""
195
+ data = request.get_json() or {}
196
+ username = data.get('username', '')
197
+
198
+ worker_id = get_available_worker_slot()
199
+ if worker_id is None:
200
+ return jsonify({
201
+ 'success': False,
202
+ 'error': '浏览器线程已达上限,请稍后再试'
203
+ }), 429
204
+
205
+ email_config = config.get_random_email_config()
206
+ if not email_config:
207
+ return jsonify({
208
+ 'success': False,
209
+ 'error': '没有配置邮箱域'
210
+ }), 500
211
+
212
+ email_manager = EmailManager(
213
+ email_config['worker_domain'],
214
+ email_config['email_domain'],
215
+ email_config['admin_password']
216
+ )
217
+
218
+ jwt, email = email_manager.create_email(username)
219
+ if not jwt or not email:
220
+ return jsonify({
221
+ 'success': False,
222
+ 'error': '创建邮箱失败'
223
+ }), 500
224
+
225
+ account = AccountInfo(
226
+ email=email,
227
+ jwt=jwt,
228
+ status=AccountStatus.PENDING,
229
+ email_config=email_config,
230
+ created_at=datetime.now().isoformat()
231
+ )
232
+
233
+ with accounts_lock:
234
+ accounts[email] = account
235
+
236
+ worker = start_worker(worker_id, account, mode="register")
237
+ worker.join() # 等待任务完成
238
+
239
+ if account.status == AccountStatus.SUCCESS:
240
+ return jsonify({
241
+ 'success': True,
242
+ 'account': account.to_dict(),
243
+ 'message': '账号创建成功'
244
+ })
245
+ else:
246
+ return jsonify({
247
+ 'success': False,
248
+ 'error': account.error_message or '创建失败',
249
+ 'details': account.to_dict()
250
+ }), 500
251
+
252
+
253
+ @app.route('/api/accounts', methods=['GET'])
254
+ @requires_auth
255
+ def get_accounts():
256
+ """获取账号列表"""
257
+ email = request.args.get('email')
258
+ status_filter = request.args.get('status')
259
+ search = request.args.get('search', '')
260
+ page = int(request.args.get('page', 1))
261
+ per_page = int(request.args.get('per_page', 20))
262
+
263
+ with accounts_lock:
264
+ if email:
265
+ if email in accounts:
266
+ return jsonify({
267
+ 'success': True,
268
+ 'account': accounts[email].to_dict()
269
+ })
270
+ else:
271
+ return jsonify({
272
+ 'success': False,
273
+ 'error': '账号不存在'
274
+ }), 404
275
+
276
+ result = []
277
+ for acc in accounts.values():
278
+ if status_filter:
279
+ if status_filter == 'success' and acc.status != AccountStatus.SUCCESS:
280
+ continue
281
+ elif status_filter == 'failed' and acc.status != AccountStatus.FAILED:
282
+ continue
283
+ elif status_filter == 'creating' and acc.status not in [
284
+ AccountStatus.PENDING, AccountStatus.CREATING_EMAIL,
285
+ AccountStatus.ENTERING_EMAIL, AccountStatus.WAITING_CODE,
286
+ AccountStatus.VERIFYING, AccountStatus.COMPLETING
287
+ ]:
288
+ continue
289
+ elif status_filter == 'updating' and acc.status != AccountStatus.UPDATING:
290
+ continue
291
+
292
+ if search and search.lower() not in acc.email.lower():
293
+ continue
294
+
295
+ result.append(acc.to_dict())
296
+
297
+ total = len(result)
298
+ success_count = sum(1 for acc in accounts.values() if acc.status == AccountStatus.SUCCESS)
299
+ creating_count = sum(1 for acc in accounts.values() if acc.status in [
300
+ AccountStatus.PENDING, AccountStatus.CREATING_EMAIL,
301
+ AccountStatus.ENTERING_EMAIL, AccountStatus.WAITING_CODE,
302
+ AccountStatus.VERIFYING, AccountStatus.COMPLETING, AccountStatus.UPDATING
303
+ ])
304
+ failed_count = sum(1 for acc in accounts.values() if acc.status == AccountStatus.FAILED)
305
+
306
+ start = (page - 1) * per_page
307
+ end = start + per_page
308
+ paginated = result[start:end]
309
+
310
+ return jsonify({
311
+ 'success': True,
312
+ 'accounts': paginated,
313
+ 'total': total,
314
+ 'page': page,
315
+ 'per_page': per_page,
316
+ 'total_pages': (total + per_page - 1) // per_page,
317
+ 'stats': {
318
+ 'total': len(accounts),
319
+ 'success': success_count,
320
+ 'creating': creating_count,
321
+ 'failed': failed_count
322
+ }
323
+ })
324
+
325
+
326
+ @app.route('/api/accounts/<email>', methods=['DELETE'])
327
+ @requires_auth
328
+ def delete_account(email: str):
329
+ """删除账号"""
330
+ with accounts_lock:
331
+ if email in accounts:
332
+ del accounts[email]
333
+ return jsonify({
334
+ 'success': True,
335
+ 'message': '账号已删除'
336
+ })
337
+ else:
338
+ return jsonify({
339
+ 'success': False,
340
+ 'error': '账号不存在'
341
+ }), 404
342
+
343
+
344
+ @app.route('/api/accounts/<email>/refresh', methods=['POST'])
345
+ @requires_auth
346
+ def refresh_account(email: str):
347
+ """
348
+ 刷新账号Cookie
349
+ - 如果账号存在,直接刷新
350
+ - 如果账号不存在但邮箱域名匹配,自动创建并刷新
351
+ """
352
+ with accounts_lock:
353
+ account = accounts.get(email)
354
+
355
+ # 账号不存在,尝试根据邮箱域名创建
356
+ if not account:
357
+ account = create_account_from_email(email)
358
+ if not account:
359
+ return jsonify({
360
+ 'success': False,
361
+ 'error': f'账号不存在,且邮箱域名 {email.split("@")[1] if "@" in email else "unknown"} 未配置'
362
+ }), 404
363
+
364
+ # 保存新创建的账号
365
+ with accounts_lock:
366
+ accounts[email] = account
367
+
368
+ # 检查是否有可用的工作槽位
369
+ worker_id = get_available_worker_slot()
370
+ if worker_id is None:
371
+ return jsonify({
372
+ 'success': False,
373
+ 'error': '浏览器线程已达上限,请稍后再试'
374
+ }), 429
375
+
376
+ # 更新状态
377
+ account.status = AccountStatus.UPDATING
378
+ with accounts_lock:
379
+ accounts[email] = account
380
+
381
+ # 启动刷新工作线程
382
+ start_worker(worker_id, account, mode="refresh")
383
+
384
+ return jsonify({
385
+ 'success': True,
386
+ 'message': '刷新已开始',
387
+ 'email': email
388
+ })
389
+
390
+
391
+ @app.route('/api/accounts/refresh-all', methods=['POST'])
392
+ @requires_auth
393
+ def refresh_all_accounts():
394
+ """刷新所有成功的账号"""
395
+ with accounts_lock:
396
+ success_accounts = [acc for acc in accounts.values() if acc.status == AccountStatus.SUCCESS]
397
+
398
+ if not success_accounts:
399
+ return jsonify({
400
+ 'success': False,
401
+ 'error': '没有可刷新的账号'
402
+ }), 400
403
+
404
+ max_workers = config.get_max_workers()
405
+ queued_count = 0
406
+
407
+ for account in success_accounts:
408
+ worker_id = get_available_worker_slot()
409
+ if worker_id is not None:
410
+ account.status = AccountStatus.UPDATING
411
+ with accounts_lock:
412
+ accounts[account.email] = account
413
+ start_worker(worker_id, account, mode="refresh")
414
+ queued_count += 1
415
+ else:
416
+ task_queue.put(('refresh', account))
417
+ queued_count += 1
418
+
419
+ return jsonify({
420
+ 'success': True,
421
+ 'message': f'已开始刷新 {queued_count} 个账号'
422
+ })
423
+
424
+
425
+ @app.route('/api/accounts/<email>/retry', methods=['POST'])
426
+ @requires_auth
427
+ def retry_account(email: str):
428
+ """重试失败的账号"""
429
+ with accounts_lock:
430
+ if email not in accounts:
431
+ return jsonify({
432
+ 'success': False,
433
+ 'error': '账号不存在'
434
+ }), 404
435
+
436
+ account = accounts[email]
437
+
438
+ if account.status != AccountStatus.FAILED:
439
+ return jsonify({
440
+ 'success': False,
441
+ 'error': '只能重试失败的账号'
442
+ }), 400
443
+
444
+ worker_id = get_available_worker_slot()
445
+ if worker_id is None:
446
+ return jsonify({
447
+ 'success': False,
448
+ 'error': '浏览器线程已达上限,请稍后再试'
449
+ }), 429
450
+
451
+ account.status = AccountStatus.PENDING
452
+ account.error_message = ""
453
+ with accounts_lock:
454
+ accounts[email] = account
455
+
456
+ start_worker(worker_id, account, mode="register")
457
+
458
+ return jsonify({
459
+ 'success': True,
460
+ 'message': '重试已开始'
461
+ })
462
+
463
+
464
+ @app.route('/api/accounts/<email>/stop', methods=['POST'])
465
+ @requires_auth
466
+ def stop_account(email: str):
467
+ """停止账号操作"""
468
+ with workers_lock:
469
+ for worker_id, worker in list(workers.items()):
470
+ if worker.account.email == email:
471
+ worker.stop()
472
+ del workers[worker_id]
473
+
474
+ with accounts_lock:
475
+ if email in accounts:
476
+ accounts[email].status = AccountStatus.FAILED
477
+ accounts[email].error_message = "用户手动停止"
478
+
479
+ return jsonify({
480
+ 'success': True,
481
+ 'message': '已停止'
482
+ })
483
+
484
+ return jsonify({
485
+ 'success': False,
486
+ 'error': '未找到正在运行的任务'
487
+ }), 404
488
+
489
+
490
+ @app.route('/api/accounts/stop-all', methods=['POST'])
491
+ @requires_auth
492
+ def stop_all():
493
+ """停止所有操作"""
494
+ stopped_count = 0
495
+ with workers_lock:
496
+ for worker_id, worker in list(workers.items()):
497
+ worker.stop()
498
+ email = worker.account.email
499
+
500
+ with accounts_lock:
501
+ if email in accounts:
502
+ accounts[email].status = AccountStatus.FAILED
503
+ accounts[email].error_message = "用户手动停止"
504
+
505
+ stopped_count += 1
506
+ workers.clear()
507
+
508
+ while not task_queue.empty():
509
+ try:
510
+ task_queue.get_nowait()
511
+ except:
512
+ break
513
+
514
+ return jsonify({
515
+ 'success': True,
516
+ 'message': f'已停止 {stopped_count} 个任务'
517
+ })
518
+
519
+
520
+ @app.route('/api/accounts/export', methods=['GET'])
521
+ @requires_auth
522
+ def export_accounts():
523
+ """导出成功的账号(包含时间戳和邮箱)"""
524
+ with accounts_lock:
525
+ success_accounts = [acc for acc in accounts.values() if acc.status == AccountStatus.SUCCESS and acc.is_complete()]
526
+
527
+ export_data = {
528
+ 'accounts': []
529
+ }
530
+
531
+ for acc in success_accounts:
532
+ export_data['accounts'].append({
533
+ 'available': True,
534
+ 'email': acc.email, # 重要:包含邮箱用于刷新
535
+ 'csesidx': acc.csesidx,
536
+ 'host_c_oses': acc.c_oses,
537
+ 'secure_c_ses': acc.c_ses,
538
+ 'team_id': acc.config_id,
539
+ 'user_agent': config.get_user_agent(),
540
+ 'created_at': acc.created_at or datetime.now().isoformat(),
541
+ 'updated_at': acc.updated_at or acc.created_at or datetime.now().isoformat()
542
+ })
543
+
544
+ return jsonify(export_data)
545
+
546
+
547
+ @app.route('/api/settings', methods=['GET'])
548
+ @requires_auth
549
+ def get_settings():
550
+ """获取设置"""
551
+ return jsonify({
552
+ 'success': True,
553
+ 'settings': {
554
+ 'user_agent': config.get_user_agent(),
555
+ 'max_workers': config.get_max_workers(),
556
+ 'headless': config.get_headless(),
557
+ 'email_configs': config.get_email_configs_safe(),
558
+ 'browser_fingerprint': config.get_browser_fingerprint()
559
+ }
560
+ })
561
+
562
+
563
+ @app.route('/api/settings', methods=['POST'])
564
+ @requires_auth
565
+ def update_settings():
566
+ """更新设置"""
567
+ data = request.get_json()
568
+
569
+ if 'user_agent' in data:
570
+ config.set_user_agent(data['user_agent'])
571
+
572
+ if 'max_workers' in data:
573
+ config.set_max_workers(int(data['max_workers']))
574
+
575
+ if 'headless' in data:
576
+ config.set_headless(bool(data['headless']))
577
+
578
+ if 'browser_fingerprint' in data:
579
+ config.set_browser_fingerprint(data['browser_fingerprint'])
580
+
581
+ return jsonify({
582
+ 'success': True,
583
+ 'message': '设置已更新'
584
+ })
585
+
586
+
587
+ @app.route('/api/email-configs', methods=['GET'])
588
+ @requires_auth
589
+ def get_email_configs():
590
+ """获取邮箱配置列表"""
591
+ return jsonify({
592
+ 'success': True,
593
+ 'configs': config.get_email_configs_safe()
594
+ })
595
+
596
+
597
+ @app.route('/api/email-configs', methods=['POST'])
598
+ @requires_auth
599
+ def add_email_config():
600
+ """添加邮箱配置"""
601
+ data = request.get_json()
602
+
603
+ worker_domain = data.get('worker_domain', '')
604
+ email_domain = data.get('email_domain', '')
605
+ admin_password = data.get('admin_password', '')
606
+
607
+ if not all([worker_domain, email_domain, admin_password]):
608
+ return jsonify({
609
+ 'success': False,
610
+ 'error': '缺少必要参数'
611
+ }), 400
612
+
613
+ config.add_email_config(worker_domain, email_domain, admin_password)
614
+
615
+ return jsonify({
616
+ 'success': True,
617
+ 'message': '邮箱配置已添加'
618
+ })
619
+
620
+
621
+ @app.route('/api/email-configs/<int:index>', methods=['PUT'])
622
+ @requires_auth
623
+ def update_email_config(index: int):
624
+ """更新邮箱配置"""
625
+ data = request.get_json()
626
+
627
+ try:
628
+ config.update_email_config(
629
+ index,
630
+ data.get('worker_domain'),
631
+ data.get('email_domain'),
632
+ data.get('admin_password')
633
+ )
634
+ return jsonify({
635
+ 'success': True,
636
+ 'message': '邮箱配置已更新'
637
+ })
638
+ except IndexError:
639
+ return jsonify({
640
+ 'success': False,
641
+ 'error': '配置不存在'
642
+ }), 404
643
+
644
+
645
+ @app.route('/api/email-configs/<int:index>', methods=['DELETE'])
646
+ @requires_auth
647
+ def delete_email_config(index: int):
648
+ """删除邮箱配置"""
649
+ try:
650
+ config.delete_email_config(index)
651
+ return jsonify({
652
+ 'success': True,
653
+ 'message': '邮箱配置已删除'
654
+ })
655
+ except IndexError:
656
+ return jsonify({
657
+ 'success': False,
658
+ 'error': '配置不存在'
659
+ }), 404
660
+
661
+
662
+ @app.route('/api/status', methods=['GET'])
663
+ @requires_auth
664
+ def get_status():
665
+ """获取系统状态"""
666
+ with accounts_lock:
667
+ total = len(accounts)
668
+ success = sum(1 for acc in accounts.values() if acc.status == AccountStatus.SUCCESS)
669
+ creating = sum(1 for acc in accounts.values() if acc.status in [
670
+ AccountStatus.PENDING, AccountStatus.CREATING_EMAIL,
671
+ AccountStatus.ENTERING_EMAIL, AccountStatus.WAITING_CODE,
672
+ AccountStatus.VERIFYING, AccountStatus.COMPLETING
673
+ ])
674
+ updating = sum(1 for acc in accounts.values() if acc.status == AccountStatus.UPDATING)
675
+ failed = sum(1 for acc in accounts.values() if acc.status == AccountStatus.FAILED)
676
+
677
+ with workers_lock:
678
+ active_workers = sum(1 for w in workers.values() if w.is_alive())
679
+
680
+ return jsonify({
681
+ 'success': True,
682
+ 'status': {
683
+ 'accounts': {
684
+ 'total': total,
685
+ 'success': success,
686
+ 'creating': creating,
687
+ 'updating': updating,
688
+ 'failed': failed
689
+ },
690
+ 'workers': {
691
+ 'active': active_workers,
692
+ 'max': config.get_max_workers()
693
+ },
694
+ 'queue_size': task_queue.qsize()
695
+ }
696
+ })
697
+
698
+
699
+ @app.route('/api/screenshot', methods=['GET'])
700
+ @requires_auth
701
+ def get_screenshot():
702
+ """获取浏览器截图"""
703
+ email = request.args.get('email')
704
+
705
+ if email:
706
+ # 获取特定账号的截图
707
+ target_worker = None
708
+ with workers_lock:
709
+ for worker in workers.values():
710
+ if worker.account.email == email:
711
+ target_worker = worker
712
+ break
713
+
714
+ if not target_worker:
715
+ return jsonify({
716
+ 'success': False,
717
+ 'error': '未找到该账号的运行实例'
718
+ }), 404
719
+
720
+ screenshot = target_worker.get_screenshot()
721
+ if screenshot:
722
+ return jsonify({
723
+ 'success': True,
724
+ 'email': email,
725
+ 'image': f'![screenshot](data:image/png;base64,{screenshot})'
726
+ })
727
+ else:
728
+ return jsonify({
729
+ 'success': False,
730
+ 'error': '截图失败'
731
+ }), 500
732
+
733
+ else:
734
+ # 获取所有活跃实例的截图
735
+ results = []
736
+ with workers_lock:
737
+ active_workers = list(workers.values())
738
+
739
+ for worker in active_workers:
740
+ if worker.is_alive():
741
+ screenshot = worker.get_screenshot()
742
+ if screenshot:
743
+ results.append({
744
+ 'email': worker.account.email,
745
+ 'image': f'![screenshot](data:image/png;base64,{screenshot})'
746
+ })
747
+
748
+ return jsonify({
749
+ 'success': True,
750
+ 'screenshots': results
751
+ })
752
+
753
+
754
+ # 后台任务处理器
755
+ def background_task_processor():
756
+ """后台任务处理器"""
757
+ while True:
758
+ try:
759
+ task = task_queue.get(timeout=1)
760
+ if task is None:
761
+ break
762
+
763
+ mode, account = task
764
+ worker_id = get_available_worker_slot()
765
+
766
+ if worker_id is not None:
767
+ start_worker(worker_id, account, mode=mode)
768
+ else:
769
+ task_queue.put(task)
770
+ time.sleep(1)
771
+
772
+ except queue.Empty:
773
+ continue
774
+ except Exception as e:
775
+ logger.error(f"后台任务处理器错误: {e}")
776
+
777
+
778
+ # 启动后台任务处理器
779
+ task_processor_thread = threading.Thread(target=background_task_processor, daemon=True)
780
+ task_processor_thread.start()
781
+
782
+
783
+ if __name__ == '__main__':
784
+ port = int(os.getenv('PORT', 5000))
785
+ debug = os.getenv('DEBUG', 'false').lower() == 'true'
786
+ logger.info(f"启动 Flask 应用, http://127.0.0.1:{port}")
787
+ app.run(host='0.0.0.0', port=port, debug=debug, threaded=True)
browser_worker.py ADDED
@@ -0,0 +1,881 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ import time
3
+ import random
4
+ import threading
5
+ from enum import Enum
6
+ from typing import Optional, Callable
7
+ from dataclasses import dataclass, field
8
+ from datetime import datetime
9
+ from urllib.parse import urlparse, parse_qs
10
+ import logging
11
+
12
+ from DrissionPage import Chromium, ChromiumOptions
13
+
14
+ from email_manager import EmailManager
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ class AccountStatus(Enum):
19
+ PENDING = "pending"
20
+ CREATING_EMAIL = "creating_email"
21
+ OPENING_PAGE = "opening_page"
22
+ ENTERING_EMAIL = "entering_email"
23
+ WAITING_CODE = "waiting_code"
24
+ ENTERING_CODE = "entering_code"
25
+ VERIFYING = "verifying"
26
+ ENTERING_NAME = "entering_name"
27
+ AGREEING = "agreeing"
28
+ WAITING_REDIRECT = "waiting_redirect"
29
+ COMPLETING = "completing"
30
+ EXTRACTING_DATA = "extracting_data"
31
+ SUCCESS = "success"
32
+ FAILED = "failed"
33
+ UPDATING = "updating"
34
+
35
+
36
+ @dataclass
37
+ class AccountInfo:
38
+ email: str = ""
39
+ jwt: str = ""
40
+ status: AccountStatus = AccountStatus.PENDING
41
+ error_message: str = ""
42
+ verification_code: str = ""
43
+ c_oses: str = ""
44
+ c_ses: str = ""
45
+ csesidx: str = ""
46
+ config_id: str = ""
47
+ created_at: str = ""
48
+ updated_at: str = ""
49
+ email_config: dict = field(default_factory=dict)
50
+
51
+ def to_dict(self) -> dict:
52
+ return {
53
+ "email": self.email,
54
+ "status": self.status.value,
55
+ "error_message": self.error_message,
56
+ "verification_code": self.verification_code,
57
+ "c_oses": self.c_oses,
58
+ "c_ses": self.c_ses,
59
+ "csesidx": self.csesidx,
60
+ "config_id": self.config_id,
61
+ "created_at": self.created_at,
62
+ "updated_at": self.updated_at,
63
+ "is_complete": self.is_complete()
64
+ }
65
+
66
+ def to_export_dict(self) -> dict:
67
+ """导出格式,包含时间戳"""
68
+ return {
69
+ "available": True,
70
+ "csesidx": self.csesidx,
71
+ "host_c_oses": self.c_oses,
72
+ "secure_c_ses": self.c_ses,
73
+ "team_id": self.config_id,
74
+ "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36",
75
+ "created_at": self.created_at or datetime.now().isoformat(),
76
+ "updated_at": self.updated_at or self.created_at or datetime.now().isoformat()
77
+ }
78
+
79
+ def is_complete(self) -> bool:
80
+ return all([
81
+ self.c_oses,
82
+ self.c_ses,
83
+ self.csesidx,
84
+ self.config_id
85
+ ])
86
+
87
+
88
+ class BrowserWorker(threading.Thread):
89
+ """浏览器工作线程"""
90
+
91
+ def __init__(
92
+ self,
93
+ worker_id: int,
94
+ account: AccountInfo,
95
+ config,
96
+ mode: str = "register",
97
+ on_update: Optional[Callable] = None,
98
+ on_complete: Optional[Callable] = None
99
+ ):
100
+ super().__init__(daemon=True)
101
+ self.worker_id = worker_id
102
+ self.account = account
103
+ self.config = config
104
+ self.mode = mode
105
+ self.on_update = on_update
106
+ self.on_complete = on_complete
107
+ self.is_running = True
108
+ self.browser: Optional[Chromium] = None
109
+ self.page = None
110
+
111
+ def update_status(self, status: AccountStatus, error: str = ""):
112
+ """更新状态"""
113
+ self.account.status = status
114
+ self.account.error_message = error
115
+ self.account.updated_at = datetime.now().isoformat()
116
+ if self.on_update:
117
+ self.on_update(self.account.email, self.account)
118
+
119
+ def create_browser(self) -> bool:
120
+ """创建浏览器实例"""
121
+ try:
122
+ self.close_browser()
123
+
124
+ options = ChromiumOptions().auto_port()
125
+ # 设置浏览器路径
126
+ browser_path = self.config.get_browser_path()
127
+ if browser_path:
128
+ options.set_browser_path(browser_path)
129
+ logger.info(f"[{self.worker_id}] 使用浏览器: {browser_path}")
130
+
131
+ ua = self.config.get_user_agent()
132
+ options.set_user_agent(ua)
133
+
134
+ if self.config.get_headless():
135
+ options.set_argument('--headless=new')
136
+ options.set_argument('--incognito')
137
+
138
+ options.set_argument('--disable-blink-features=AutomationControlled')
139
+ options.set_argument('--no-sandbox')
140
+ options.set_argument('--disable-dev-shm-usage')
141
+ options.set_argument('--disable-gpu')
142
+ options.set_argument('--lang=zh-CN')
143
+ options.set_argument('--disable-web-security')
144
+ options.set_argument('--disable-features=VizDisplayCompositor')
145
+
146
+ fingerprint = self.config.get_browser_fingerprint()
147
+ if fingerprint:
148
+ if fingerprint.get('window_size'):
149
+ w, h = fingerprint['window_size'].split('x')
150
+ options.set_argument(f'--window-size={w},{h}')
151
+
152
+ if fingerprint.get('timezone'):
153
+ options.set_argument(f'--timezone={fingerprint["timezone"]}')
154
+
155
+ if fingerprint.get('locale'):
156
+ options.set_argument(f'--lang={fingerprint["locale"]}')
157
+
158
+ options.set_argument('--disable-reading-from-canvas')
159
+ options.set_pref('credentials_enable_service', False)
160
+ options.set_pref('profile.password_manager_enabled', False)
161
+
162
+ self.browser = Chromium(options)
163
+ self.page = self.browser.latest_tab
164
+
165
+ self._inject_fingerprint_script()
166
+
167
+ return True
168
+
169
+ except Exception as e:
170
+ logger.error(f"创建浏览器失败: {e}")
171
+ return False
172
+
173
+ def _inject_fingerprint_script(self):
174
+ """注入指纹混淆脚本"""
175
+ script = '''
176
+ Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
177
+ Object.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3, 4, 5] });
178
+ Object.defineProperty(navigator, 'languages', { get: () => ['zh-CN', 'zh', 'en'] });
179
+ window.chrome = { runtime: {} };
180
+ '''
181
+ try:
182
+ self.page.run_js(script)
183
+ except:
184
+ pass
185
+
186
+ def close_browser(self):
187
+ """关闭浏览器"""
188
+ if self.browser:
189
+ try:
190
+ self.browser.quit()
191
+ except:
192
+ pass
193
+ finally:
194
+ self.browser = None
195
+ self.page = None
196
+
197
+ def get_screenshot(self) -> Optional[str]:
198
+ """获取当前页面截图 (Base64)"""
199
+ if self.page:
200
+ try:
201
+ return self.page.get_screenshot(as_base64=True, full_page=False)
202
+ except Exception as e:
203
+ logger.error(f"截图失败: {e}")
204
+ return None
205
+ return None
206
+
207
+ def safe_input(self, selector: str, text: str, max_retries: int = 3) -> bool:
208
+ """安全输入文本"""
209
+ for attempt in range(max_retries):
210
+ try:
211
+ ele = self.page.ele(selector, timeout=10)
212
+ if not ele:
213
+ continue
214
+
215
+ ele.clear()
216
+ time.sleep(0.3)
217
+ clean_text = ''.join(c for c in text if ord(c) < 128)
218
+ ele.input(clean_text)
219
+ time.sleep(0.5)
220
+
221
+ input_value = ele.attr('value') or ele.value
222
+ if input_value and clean_text in input_value:
223
+ return True
224
+ else:
225
+ ele.clear()
226
+ time.sleep(0.5)
227
+
228
+ except Exception as e:
229
+ logger.error(f"输入失败: {e}")
230
+ time.sleep(1)
231
+
232
+ return False
233
+
234
+ def wait_and_input(self, selector: str, text: str, timeout: float = 10, description: str = "") -> bool:
235
+ """等待元素并输入"""
236
+ try:
237
+ ele = self.page.ele(selector, timeout=timeout)
238
+ if ele:
239
+ ele.clear()
240
+ ele.input(text)
241
+ logger.info(f"输入成功: {description}")
242
+ return True
243
+ else:
244
+ logger.warning(f"未找到输入框: {description}")
245
+ return False
246
+ except Exception as e:
247
+ logger.error(f"输入失败 {description}: {e}")
248
+ return False
249
+
250
+ def wait_and_click(self, selector: str, timeout: float = 10) -> bool:
251
+ """等待并点击元素"""
252
+ try:
253
+ ele = self.page.ele(selector, timeout=timeout)
254
+ if ele:
255
+ ele.click()
256
+ return True
257
+ return False
258
+ except Exception as e:
259
+ logger.error(f"点击失败: {e}")
260
+ return False
261
+
262
+ def wait_for_url_pattern(self, pattern: str, timeout: float = 60) -> bool:
263
+ """等待URL匹配模式"""
264
+ start_time = time.time()
265
+ while time.time() - start_time < timeout:
266
+ if not self.is_running:
267
+ return False
268
+ try:
269
+ current_url = self.page.url
270
+ if re.search(pattern, current_url):
271
+ return True
272
+ except:
273
+ pass
274
+ time.sleep(1)
275
+ return False
276
+
277
+ def wait_for_element(self, selector: str, timeout: float = 30) -> bool:
278
+ """等待元素出现"""
279
+ start_time = time.time()
280
+ while time.time() - start_time < timeout:
281
+ if not self.is_running:
282
+ return False
283
+ try:
284
+ ele = self.page.ele(selector, timeout=1)
285
+ if ele:
286
+ return True
287
+ except:
288
+ pass
289
+ time.sleep(0.5)
290
+ return False
291
+
292
+ def extract_data(self) -> bool:
293
+ """提取数据"""
294
+ try:
295
+ cookies = self.page.cookies()
296
+ for cookie in cookies:
297
+ name = cookie.get('name', '')
298
+ if name == '__Host-C_OSES':
299
+ self.account.c_oses = cookie.get('value', '')
300
+ elif name == '__Secure-C_SES':
301
+ self.account.c_ses = cookie.get('value', '')
302
+
303
+ current_url = self.page.url
304
+ parsed = urlparse(current_url)
305
+ query_params = parse_qs(parsed.query)
306
+ self.account.csesidx = query_params.get('csesidx', [''])[0]
307
+
308
+ path_match = re.search(r'/cid/([a-f0-9-]+)', parsed.path)
309
+ if path_match:
310
+ self.account.config_id = path_match.group(1)
311
+
312
+ # 设置更新时间
313
+ self.account.updated_at = datetime.now().isoformat()
314
+
315
+ return self.account.is_complete()
316
+
317
+ except Exception as e:
318
+ logger.error(f"提取数据失败: {e}")
319
+ return False
320
+
321
+ def handle_welcome_dialog(self) -> bool:
322
+ """处理欢迎对话框"""
323
+ try:
324
+ time.sleep(2)
325
+
326
+ try:
327
+ app_ele = self.page.ele('tag:ucs-standalone-app', timeout=5)
328
+ if app_ele:
329
+ sr1 = app_ele.shadow_root
330
+ if sr1:
331
+ dialog_ele = sr1.ele('tag:ucs-welcome-dialog', timeout=3)
332
+ if dialog_ele:
333
+ sr2 = dialog_ele.shadow_root
334
+ if sr2:
335
+ btn_ele = sr2.ele('tag:md-text-button', timeout=3)
336
+ if btn_ele:
337
+ sr3 = btn_ele.shadow_root
338
+ if sr3:
339
+ inner_btn = sr3.ele('tag:button', timeout=3)
340
+ if inner_btn:
341
+ inner_btn.click()
342
+ return True
343
+ except:
344
+ pass
345
+
346
+ try:
347
+ js_code = '''
348
+ function clickWelcomeButton() {
349
+ const app = document.querySelector('ucs-standalone-app');
350
+ if (app && app.shadowRoot) {
351
+ const dialog = app.shadowRoot.querySelector('ucs-welcome-dialog');
352
+ if (dialog && dialog.shadowRoot) {
353
+ const btn = dialog.shadowRoot.querySelector('md-text-button');
354
+ if (btn) {
355
+ if (btn.shadowRoot) {
356
+ const innerBtn = btn.shadowRoot.querySelector('button');
357
+ if (innerBtn) { innerBtn.click(); return true; }
358
+ }
359
+ btn.click();
360
+ return true;
361
+ }
362
+ }
363
+ }
364
+ return false;
365
+ }
366
+ return clickWelcomeButton();
367
+ '''
368
+ result = self.page.run_js(js_code)
369
+ return bool(result)
370
+ except:
371
+ pass
372
+
373
+ return False
374
+
375
+ except:
376
+ return False
377
+
378
+ def click_verify_button(self) -> bool:
379
+ """点击验证按钮"""
380
+ verify_selectors = [
381
+ 'xpath://button[@jsname="XooR8e"]',
382
+ 'xpath://button[@aria-label="验证"]',
383
+ 'xpath://button[contains(@class, "YUhpIc-LgbsSe") and @type="submit"]',
384
+ 'xpath://button[.//span[contains(text(), "验证")]]',
385
+ 'css:button[jsname="XooR8e"]',
386
+ 'css:button[aria-label="验证"]',
387
+ ]
388
+
389
+ for selector in verify_selectors:
390
+ try:
391
+ ele = self.page.ele(selector, timeout=3)
392
+ if ele:
393
+ time.sleep(0.5)
394
+ ele.click()
395
+ logger.info(f"成功点击验证按钮: {selector}")
396
+ return True
397
+ except:
398
+ continue
399
+
400
+ try:
401
+ js_code = '''
402
+ const buttons = document.querySelectorAll('button');
403
+ for (const btn of buttons) {
404
+ if (btn.getAttribute('jsname') === 'XooR8e' ||
405
+ btn.getAttribute('aria-label') === '验证' ||
406
+ btn.innerText.includes('验证')) {
407
+ btn.click();
408
+ return true;
409
+ }
410
+ }
411
+ return false;
412
+ '''
413
+ result = self.page.run_js(js_code)
414
+ if result:
415
+ logger.info("通过 JavaScript 成功点击验证按钮")
416
+ return True
417
+ except:
418
+ pass
419
+
420
+ return False
421
+
422
+ def click_resend_button(self) -> bool:
423
+ """点��重新发送按钮"""
424
+ resend_selectors = [
425
+ 'button[aria-label="Resend Code"]',
426
+ 'button[aria-label="重新发送"]',
427
+ 'xpath://button[contains(., "Resend")]',
428
+ 'xpath://button[contains(., "重新发送验证码")]',
429
+ ]
430
+
431
+ for sel in resend_selectors:
432
+ if self.wait_and_click(sel, timeout=5):
433
+ logger.info(f"[{self.worker_id}] 已点击重新发送按钮")
434
+ return True
435
+
436
+ return False
437
+
438
+ def get_verification_code_with_retry(self, email_manager) -> Optional[str]:
439
+ """带重试机制的获取验证码"""
440
+ max_loops = 1
441
+
442
+ for i in range(max_loops):
443
+ logger.info(f"[{self.worker_id}] 第 {i+1} 次尝试获取验证码流程")
444
+
445
+ # 1. 第一次检测 (15次)
446
+ logger.info(f"[{self.worker_id}] 正在检测邮件 (10次尝试)...")
447
+ code = email_manager.check_verification_code(self.account.email, max_retries=10)
448
+ if code:
449
+ return code
450
+
451
+ # 2. 如果没收到,检测是否有重发按钮
452
+ logger.info(f"[{self.worker_id}] 未收到邮件,检查重发按钮...")
453
+ if self.click_resend_button():
454
+ logger.info(f"[{self.worker_id}] 点击了重发按钮,等待邮件发送...")
455
+ time.sleep(10) # 等待邮件发送
456
+ continue # 重新开始循环,会再次检测邮件
457
+
458
+ # 3. 如果没有重发按钮,继续等待15次
459
+ logger.info(f"[{self.worker_id}] 未找到重发按钮,继续等待邮件 (10次尝试)...")
460
+ code = email_manager.check_verification_code(self.account.email, max_retries=10)
461
+ if code:
462
+ return code
463
+
464
+ # 4. 再次检测重发按钮
465
+ logger.info(f"[{self.worker_id}] 仍未收到邮件,再次检查重发按钮...")
466
+ if self.click_resend_button():
467
+ logger.info(f"[{self.worker_id}] 点击了重发按钮,等待邮件发送...")
468
+ time.sleep(10)
469
+ continue
470
+ else:
471
+ logger.warning(f"[{self.worker_id}] 无法找到重发按钮且未收到邮件,本轮失败")
472
+ return None
473
+
474
+ return None
475
+
476
+ def register_account(self) -> bool:
477
+ """注册账号"""
478
+ try:
479
+ self.update_status(AccountStatus.OPENING_PAGE)
480
+ logger.info(f"[{self.worker_id}] 正在打开页面...")
481
+
482
+ self.page.get('https://business.gemini.google')
483
+
484
+ email_input_selectors = [
485
+ 'xpath://input[@id="email-input"]',
486
+ 'xpath://input[@name="loginHint"]',
487
+ '#email-input',
488
+ ]
489
+
490
+ email_input_found = False
491
+ for selector in email_input_selectors:
492
+ if self.wait_for_element(selector, timeout=30):
493
+ email_input_found = True
494
+ break
495
+
496
+ if not email_input_found:
497
+ raise Exception("等待邮箱输入框超时")
498
+
499
+ time.sleep(1)
500
+
501
+ self.update_status(AccountStatus.ENTERING_EMAIL)
502
+ logger.info(f"[{self.worker_id}] 正在输入邮箱: {self.account.email}")
503
+
504
+ input_success = False
505
+ for selector in email_input_selectors:
506
+ if self.safe_input(selector, self.account.email):
507
+ input_success = True
508
+ break
509
+
510
+ if not input_success:
511
+ raise Exception("无法输入邮箱")
512
+
513
+ time.sleep(1)
514
+
515
+ continue_selectors = [
516
+ 'xpath://button[@id="log-in-button"]',
517
+ 'xpath://button[contains(@aria-label, "使用邮箱继续")]',
518
+ '#log-in-button',
519
+ ]
520
+
521
+ clicked = False
522
+ for selector in continue_selectors:
523
+ if self.wait_and_click(selector, timeout=5):
524
+ clicked = True
525
+ break
526
+
527
+ if not clicked:
528
+ raise Exception("无法点击继续按钮")
529
+
530
+ time.sleep(2)
531
+
532
+ self.update_status(AccountStatus.WAITING_CODE)
533
+ logger.info(f"[{self.worker_id}] 正在等待验证码...")
534
+
535
+ email_manager = EmailManager(
536
+ self.account.email_config.get('worker_domain', ''),
537
+ self.account.email_config.get('email_domain', ''),
538
+ self.account.email_config.get('admin_password', '')
539
+ )
540
+
541
+ verification_code = self.get_verification_code_with_retry(email_manager)
542
+
543
+ if not verification_code:
544
+ raise Exception("未收到验证码 (已重试)")
545
+
546
+ self.account.verification_code = verification_code
547
+ logger.info(f"[{self.worker_id}] 获取到验证码: {verification_code}")
548
+
549
+ self.update_status(AccountStatus.ENTERING_CODE)
550
+
551
+ code_input_selectors = [
552
+ 'xpath://input[@name="pinInput"]',
553
+ 'xpath://input[contains(@aria-label, "验证码")]',
554
+ 'input[name="pinInput"]',
555
+ ]
556
+ logger.info(f"[{self.worker_id}] 正在输入验证码: {self.account.email}")
557
+ input_success = False
558
+ for selector in code_input_selectors:
559
+ if self.wait_and_input(selector, verification_code, timeout=10, description="验证码输入框"):
560
+ input_success = True
561
+ break
562
+
563
+ if not input_success:
564
+ raise Exception("无法输入验证码")
565
+
566
+ time.sleep(1)
567
+
568
+ self.update_status(AccountStatus.VERIFYING)
569
+
570
+ if not self.click_verify_button():
571
+ raise Exception("无法点击验证按钮")
572
+
573
+ time.sleep(3)
574
+
575
+ # 检查验证码错误并重试
576
+ if self.page.ele('text:请输入验证码。', timeout=2):
577
+ logger.warning(f"[{self.worker_id}] 验证码失效,尝试重新发送...")
578
+
579
+ resend_selectors = [
580
+ 'button[aria-label="重新发送验证码"]',
581
+ 'xpath://button[contains(., "重新发送验证码")]'
582
+ ]
583
+
584
+ resend_clicked = False
585
+ for sel in resend_selectors:
586
+ if self.wait_and_click(sel, timeout=5):
587
+ resend_clicked = True
588
+ break
589
+
590
+ if resend_clicked:
591
+ logger.info(f"[{self.worker_id}] 已点击重新发送,等待新验证码...")
592
+ time.sleep(10) # 等待邮件发送
593
+
594
+ verification_code = email_manager.check_verification_code(self.account.email)
595
+ if not verification_code:
596
+ raise Exception("重发后未收到验证码")
597
+
598
+ self.account.verification_code = verification_code
599
+ logger.info(f"[{self.worker_id}] 获取到新验证码: {verification_code}")
600
+
601
+ input_success = False
602
+ for selector in code_input_selectors:
603
+ if self.wait_and_input(selector, verification_code, timeout=10, description="验证码输入框"):
604
+ input_success = True
605
+ break
606
+
607
+ if input_success:
608
+ time.sleep(1)
609
+ if not self.click_verify_button():
610
+ raise Exception("无法点击验证按钮(重试)")
611
+ time.sleep(3)
612
+ else:
613
+ raise Exception("无法重新输入验证码")
614
+ else:
615
+ logger.warning(f"[{self.worker_id}] 未找到重新发送按钮")
616
+
617
+ self.update_status(AccountStatus.ENTERING_NAME)
618
+
619
+ fullname = ''.join(random.choices('abcdefghijklmnopqrstuvwxyz', k=5))
620
+ name_selectors = [
621
+ 'xpath://input[@formcontrolname="fullName"]',
622
+ 'xpath://input[@placeholder="全名"]',
623
+ 'input[formcontrolname="fullName"]',
624
+ ]
625
+
626
+ name_input_found = False
627
+ for selector in name_selectors:
628
+ if self.wait_for_element(selector, timeout=15):
629
+ name_input_found = True
630
+ break
631
+
632
+ if not name_input_found:
633
+ raise Exception("等待姓名输入框超时")
634
+
635
+ input_success = False
636
+ for selector in name_selectors:
637
+ if self.safe_input(selector, fullname):
638
+ input_success = True
639
+ break
640
+
641
+ if not input_success:
642
+ raise Exception("无法输入姓名")
643
+
644
+ time.sleep(1)
645
+
646
+ self.update_status(AccountStatus.AGREEING)
647
+
648
+ agree_selectors = [
649
+ 'xpath://button[contains(@class, "agree-button")]',
650
+ 'xpath://button[contains(., "同意并开始使用")]',
651
+ 'xpath://button[contains(., "同意")]',
652
+ 'button.agree-button',
653
+ ]
654
+
655
+ clicked = False
656
+ for selector in agree_selectors:
657
+ if self.wait_and_click(selector, timeout=5):
658
+ clicked = True
659
+ break
660
+
661
+ if not clicked:
662
+ raise Exception("无法点击同意按钮")
663
+
664
+ self.update_status(AccountStatus.WAITING_REDIRECT)
665
+
666
+ target_pattern = r'business\.gemini\.google/home/cid/[a-f0-9-]+\?csesidx=\d+'
667
+ if not self.wait_for_url_pattern(target_pattern, timeout=90):
668
+ current_url = self.page.url
669
+ if '/admin/create' in current_url:
670
+ time.sleep(15)
671
+ if not self.wait_for_url_pattern(target_pattern, timeout=120):
672
+ raise Exception("页面跳转超时")
673
+ else:
674
+ raise Exception("页面跳转超时")
675
+
676
+ time.sleep(3)
677
+
678
+ self.update_status(AccountStatus.COMPLETING)
679
+ self.handle_welcome_dialog()
680
+ time.sleep(2)
681
+
682
+ self.update_status(AccountStatus.EXTRACTING_DATA)
683
+
684
+ if self.extract_data():
685
+ # 设置创建时间
686
+ if not self.account.created_at:
687
+ self.account.created_at = datetime.now().isoformat()
688
+ self.update_status(AccountStatus.SUCCESS)
689
+ logger.info(f"[{self.worker_id}] 注册成功!")
690
+ return True
691
+ else:
692
+ raise Exception("未能获取完整数据")
693
+
694
+ except Exception as e:
695
+ error_msg = str(e)
696
+ logger.error(f"[{self.worker_id}] 注册失败: {error_msg}")
697
+ self.update_status(AccountStatus.FAILED, error_msg)
698
+ return False
699
+
700
+ def refresh_account(self) -> bool:
701
+ """刷新账号Cookie"""
702
+ try:
703
+ self.update_status(AccountStatus.UPDATING)
704
+ logger.info(f"[{self.worker_id}] 正在刷新账号: {self.account.email}")
705
+
706
+ self.page.get('https://business.gemini.google')
707
+
708
+ email_input_selectors = [
709
+ 'xpath://input[@id="email-input"]',
710
+ 'xpath://input[@name="loginHint"]',
711
+ '#email-input',
712
+ ]
713
+ logger.info(f"[{self.worker_id}] 正在等待邮箱输入框: {self.account.email}")
714
+ email_input_found = False
715
+ for selector in email_input_selectors:
716
+ if self.wait_for_element(selector, timeout=30):
717
+ email_input_found = True
718
+ break
719
+
720
+ if not email_input_found:
721
+ raise Exception("等待邮箱输入框超时")
722
+
723
+ time.sleep(1)
724
+
725
+ input_success = False
726
+ for selector in email_input_selectors:
727
+ if self.safe_input(selector, self.account.email):
728
+ input_success = True
729
+ break
730
+
731
+ if not input_success:
732
+ raise Exception("无法输入邮箱")
733
+
734
+ time.sleep(1)
735
+
736
+ logger.info(f"[{self.worker_id}] 正在点击继续按钮: {self.account.email}")
737
+ continue_selectors = [
738
+ 'xpath://button[@id="log-in-button"]',
739
+ 'xpath://button[contains(@aria-label, "使用邮箱继续")]',
740
+ '#log-in-button',
741
+ ]
742
+
743
+ clicked = False
744
+ for selector in continue_selectors:
745
+ if self.wait_and_click(selector, timeout=5):
746
+ clicked = True
747
+ break
748
+
749
+ if not clicked:
750
+ raise Exception("无法点击继续按钮")
751
+
752
+ time.sleep(2)
753
+
754
+ email_manager = EmailManager(
755
+ self.account.email_config.get('worker_domain', ''),
756
+ self.account.email_config.get('email_domain', ''),
757
+ self.account.email_config.get('admin_password', '')
758
+ )
759
+
760
+ logger.info(f"[{self.worker_id}] 正在获取邮箱验证码: {self.account.email}")
761
+ verification_code = self.get_verification_code_with_retry(email_manager)
762
+
763
+ if not verification_code:
764
+ raise Exception("未收到验证码 (已重试)")
765
+
766
+ self.account.verification_code = verification_code
767
+
768
+ code_input_selectors = [
769
+ 'xpath://input[@name="pinInput"]',
770
+ 'xpath://input[contains(@aria-label, "验证码")]',
771
+ 'input[name="pinInput"]',
772
+ ]
773
+ logger.info(f"[{self.worker_id}] 正在输入验证码: {self.account.email}")
774
+ input_success = False
775
+ for selector in code_input_selectors:
776
+ if self.wait_and_input(selector, verification_code, timeout=10, description="验证码输入框"):
777
+ input_success = True
778
+ break
779
+
780
+ if not input_success:
781
+ raise Exception("无法输入验证码")
782
+
783
+ time.sleep(1)
784
+
785
+ if not self.click_verify_button():
786
+ raise Exception("无法点击验证按钮")
787
+
788
+ time.sleep(3)
789
+
790
+ # 检查验证码错误并重试
791
+ if self.page.ele('text:请输入验证码。', timeout=2):
792
+ logger.warning(f"[{self.worker_id}] 验证码失效,尝试重新发送...")
793
+
794
+ resend_selectors = [
795
+ 'button[aria-label="重新发送验证码"]',
796
+ 'xpath://button[contains(., "重新发送验证码")]'
797
+ ]
798
+
799
+ resend_clicked = False
800
+ for sel in resend_selectors:
801
+ if self.wait_and_click(sel, timeout=5):
802
+ resend_clicked = True
803
+ break
804
+
805
+ if resend_clicked:
806
+ logger.info(f"[{self.worker_id}] 已点击重新发送,等待新验证码...")
807
+ time.sleep(10) # 等待邮件发送
808
+
809
+ verification_code = email_manager.check_verification_code(self.account.email)
810
+ if not verification_code:
811
+ raise Exception("重发后未收到验证码")
812
+
813
+ self.account.verification_code = verification_code
814
+ logger.info(f"[{self.worker_id}] 获取到新验证码: {verification_code}")
815
+
816
+ input_success = False
817
+ for selector in code_input_selectors:
818
+ if self.wait_and_input(selector, verification_code, timeout=10, description="验证码输入框"):
819
+ input_success = True
820
+ break
821
+
822
+ if input_success:
823
+ time.sleep(1)
824
+ if not self.click_verify_button():
825
+ raise Exception("无法点击验证按钮(重试)")
826
+ time.sleep(3)
827
+ else:
828
+ raise Exception("无法重新输入验证码")
829
+ else:
830
+ logger.warning(f"[{self.worker_id}] 未找到重新发送按钮")
831
+
832
+ logger.info(f"[{self.worker_id}] 正在等待跳转: {self.account.email}")
833
+
834
+ target_pattern = r'business\.gemini\.google/home/cid/[a-f0-9-]+\?csesidx=\d+'
835
+ if not self.wait_for_url_pattern(target_pattern, timeout=120):
836
+ raise Exception("页面跳转超时")
837
+
838
+ time.sleep(3)
839
+
840
+ self.handle_welcome_dialog()
841
+ time.sleep(2)
842
+
843
+ if self.extract_data():
844
+ self.account.updated_at = datetime.now().isoformat()
845
+ self.update_status(AccountStatus.SUCCESS)
846
+ logger.info(f"[{self.worker_id}] 刷新成功!")
847
+ return True
848
+ else:
849
+ raise Exception("未能获取完整数据")
850
+
851
+ except Exception as e:
852
+ error_msg = str(e)
853
+ logger.error(f"[{self.worker_id}] 刷新失败: {error_msg}")
854
+ self.update_status(AccountStatus.FAILED, error_msg)
855
+ return False
856
+
857
+ def run(self):
858
+ """运行工作线程"""
859
+ success = False
860
+
861
+ try:
862
+ if not self.create_browser():
863
+ self.update_status(AccountStatus.FAILED, "创建浏览器失败")
864
+ return
865
+
866
+ if self.mode == "register":
867
+ success = self.register_account()
868
+ else:
869
+ success = self.refresh_account()
870
+
871
+ except Exception as e:
872
+ self.update_status(AccountStatus.FAILED, str(e))
873
+ finally:
874
+ self.close_browser()
875
+ if self.on_complete:
876
+ self.on_complete(self.worker_id, self.account.email, success)
877
+
878
+ def stop(self):
879
+ """停止工作线程"""
880
+ self.is_running = False
881
+ self.close_browser()
config.py ADDED
@@ -0,0 +1,617 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import glob
4
+ import random
5
+ import threading
6
+ import shutil
7
+ import platform
8
+ import subprocess
9
+ from typing import List, Dict, Optional
10
+ import logging
11
+
12
+ def setup_logging():
13
+ """配置日志"""
14
+ logging.basicConfig(
15
+ level=logging.INFO,
16
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
17
+ datefmt='%Y-%m-%d %H:%M:%S'
18
+ )
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class BrowserPathFinder:
24
+ """跨平台浏览器路径查找器"""
25
+
26
+ # 浏览器可执行文件名(按优先级排序)
27
+ BROWSER_EXECUTABLES = {
28
+ 'windows': [
29
+ 'chrome.exe',
30
+ 'msedge.exe',
31
+ 'chromium.exe',
32
+ 'brave.exe',
33
+ 'vivaldi.exe',
34
+ 'opera.exe',
35
+ ],
36
+ 'darwin': [ # macOS
37
+ 'Google Chrome',
38
+ 'Chromium',
39
+ 'Microsoft Edge',
40
+ 'Brave Browser',
41
+ 'Vivaldi',
42
+ 'Opera',
43
+ ],
44
+ 'linux': [
45
+ 'google-chrome',
46
+ 'google-chrome-stable',
47
+ 'google-chrome-beta',
48
+ 'google-chrome-dev',
49
+ 'google-chrome-unstable',
50
+ 'chromium',
51
+ 'chromium-browser',
52
+ 'microsoft-edge',
53
+ 'microsoft-edge-stable',
54
+ 'microsoft-edge-beta',
55
+ 'microsoft-edge-dev',
56
+ 'brave-browser',
57
+ 'brave-browser-stable',
58
+ 'brave',
59
+ 'vivaldi',
60
+ 'vivaldi-stable',
61
+ 'opera',
62
+ 'opera-stable',
63
+ 'chrome', # 通用名称
64
+ ],
65
+ }
66
+
67
+ # 各平台常见安装路径
68
+ COMMON_PATHS = {
69
+ 'windows': [
70
+ # Chrome
71
+ os.path.expandvars(r'%ProgramFiles%\Google\Chrome\Application'),
72
+ os.path.expandvars(r'%ProgramFiles(x86)%\Google\Chrome\Application'),
73
+ os.path.expandvars(r'%LocalAppData%\Google\Chrome\Application'),
74
+ # Edge
75
+ os.path.expandvars(r'%ProgramFiles%\Microsoft\Edge\Application'),
76
+ os.path.expandvars(r'%ProgramFiles(x86)%\Microsoft\Edge\Application'),
77
+ os.path.expandvars(r'%LocalAppData%\Microsoft\Edge\Application'),
78
+ # Brave
79
+ os.path.expandvars(r'%ProgramFiles%\BraveSoftware\Brave-Browser\Application'),
80
+ os.path.expandvars(r'%LocalAppData%\BraveSoftware\Brave-Browser\Application'),
81
+ # Chromium
82
+ os.path.expandvars(r'%LocalAppData%\Chromium\Application'),
83
+ # Vivaldi
84
+ os.path.expandvars(r'%LocalAppData%\Vivaldi\Application'),
85
+ # Opera
86
+ os.path.expandvars(r'%LocalAppData%\Programs\Opera'),
87
+ # Playwright
88
+ os.path.expandvars(r'%LocalAppData%\ms-playwright'),
89
+ os.path.expandvars(r'%UserProfile%\.cache\ms-playwright'),
90
+ # Puppeteer
91
+ os.path.expandvars(r'%LocalAppData%\puppeteer'),
92
+ os.path.expandvars(r'%UserProfile%\.cache\puppeteer'),
93
+ ],
94
+ 'darwin': [ # macOS
95
+ # 标准应用目录
96
+ '/Applications',
97
+ os.path.expanduser('~/Applications'),
98
+ # Chrome
99
+ '/Applications/Google Chrome.app/Contents/MacOS',
100
+ os.path.expanduser('~/Applications/Google Chrome.app/Contents/MacOS'),
101
+ # Chromium
102
+ '/Applications/Chromium.app/Contents/MacOS',
103
+ os.path.expanduser('~/Applications/Chromium.app/Contents/MacOS'),
104
+ # Edge
105
+ '/Applications/Microsoft Edge.app/Contents/MacOS',
106
+ os.path.expanduser('~/Applications/Microsoft Edge.app/Contents/MacOS'),
107
+ # Brave
108
+ '/Applications/Brave Browser.app/Contents/MacOS',
109
+ os.path.expanduser('~/Applications/Brave Browser.app/Contents/MacOS'),
110
+ # Vivaldi
111
+ '/Applications/Vivaldi.app/Contents/MacOS',
112
+ # Opera
113
+ '/Applications/Opera.app/Contents/MacOS',
114
+ # Homebrew
115
+ '/opt/homebrew/bin',
116
+ '/usr/local/bin',
117
+ '/opt/homebrew/Caskroom/google-chrome/latest/Google Chrome.app/Contents/MacOS',
118
+ # Playwright
119
+ os.path.expanduser('~/.cache/ms-playwright'),
120
+ os.path.expanduser('~/Library/Caches/ms-playwright'),
121
+ # Puppeteer
122
+ os.path.expanduser('~/.cache/puppeteer'),
123
+ ],
124
+ 'linux': [
125
+ # 标准路径
126
+ '/usr/bin',
127
+ '/usr/local/bin',
128
+ '/bin',
129
+ '/sbin',
130
+ # Chrome
131
+ '/opt/google/chrome',
132
+ '/opt/google/chrome-stable',
133
+ '/opt/google/chrome-beta',
134
+ '/opt/google/chrome-unstable',
135
+ # Chromium
136
+ '/opt/chromium',
137
+ '/opt/chromium-browser',
138
+ '/usr/lib/chromium',
139
+ '/usr/lib/chromium-browser',
140
+ '/usr/lib64/chromium-browser',
141
+ # Edge
142
+ '/opt/microsoft/msedge',
143
+ '/opt/microsoft/msedge-stable',
144
+ '/opt/microsoft/msedge-beta',
145
+ '/opt/microsoft/msedge-dev',
146
+ # Brave
147
+ '/opt/brave.com/brave',
148
+ '/opt/brave.com/brave-browser',
149
+ '/usr/lib/brave-browser',
150
+ # Snap 安装路径
151
+ '/snap/bin',
152
+ '/snap/chromium/current/usr/lib/chromium-browser',
153
+ '/snap/chromium/current/usr/lib/chromium',
154
+ '/var/lib/snapd/snap/bin',
155
+ # Flatpak 路径
156
+ '/var/lib/flatpak/exports/bin',
157
+ os.path.expanduser('~/.local/share/flatpak/exports/bin'),
158
+ # AppImage 路径
159
+ os.path.expanduser('~/Applications'),
160
+ os.path.expanduser('~/.local/bin'),
161
+ # Playwright
162
+ os.path.expanduser('~/.cache/ms-playwright'),
163
+ '/root/.cache/ms-playwright',
164
+ '/home/*/.cache/ms-playwright',
165
+ # Puppeteer
166
+ os.path.expanduser('~/.cache/puppeteer'),
167
+ '/root/.cache/puppeteer',
168
+ '/home/*/.cache/puppeteer',
169
+ # Docker 常见路径
170
+ '/headless-shell',
171
+ '/chrome',
172
+ '/chromium',
173
+ '/browser',
174
+ '/app/chrome',
175
+ '/app/chromium',
176
+ '/usr/share/chromium',
177
+ # NixOS
178
+ '/run/current-system/sw/bin',
179
+ os.path.expanduser('~/.nix-profile/bin'),
180
+ ],
181
+ }
182
+
183
+ # Playwright/Puppeteer 浏览器的 glob 模式
184
+ GLOB_PATTERNS = [
185
+ # Playwright Chromium
186
+ os.path.expanduser('~/.cache/ms-playwright/chromium-*/chrome-linux/chrome'),
187
+ os.path.expanduser('~/.cache/ms-playwright/chromium-*/chrome-mac/Chromium.app/Contents/MacOS/Chromium'),
188
+ os.path.expanduser('~/.cache/ms-playwright/chromium-*/chrome-win/chrome.exe'),
189
+ '/root/.cache/ms-playwright/chromium-*/chrome-linux/chrome',
190
+ '/home/*/.cache/ms-playwright/chromium-*/chrome-linux/chrome',
191
+ # Playwright Chrome
192
+ os.path.expanduser('~/.cache/ms-playwright/chrome-*/chrome-linux/chrome'),
193
+ os.path.expanduser('~/.cache/ms-playwright/chrome-*/chrome-mac/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing'),
194
+ '/root/.cache/ms-playwright/chrome-*/chrome-linux/chrome',
195
+ # Puppeteer
196
+ os.path.expanduser('~/.cache/puppeteer/chrome/*/chrome-linux64/chrome'),
197
+ os.path.expanduser('~/.cache/puppeteer/chrome/*/chrome-mac-x64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing'),
198
+ os.path.expanduser('~/.cache/puppeteer/chrome/*/chrome-win64/chrome.exe'),
199
+ '/root/.cache/puppeteer/chrome/*/chrome-linux64/chrome',
200
+ '/home/*/.cache/puppeteer/chrome/*/chrome-linux64/chrome',
201
+ # Windows Playwright
202
+ os.path.expandvars(r'%LocalAppData%\ms-playwright\chromium-*\chrome-win\chrome.exe'),
203
+ os.path.expandvars(r'%UserProfile%\.cache\ms-playwright\chromium-*\chrome-win\chrome.exe'),
204
+ ]
205
+
206
+ @classmethod
207
+ def get_platform(cls) -> str:
208
+ """获取当前平台"""
209
+ system = platform.system().lower()
210
+ if system == 'windows':
211
+ return 'windows'
212
+ elif system == 'darwin':
213
+ return 'darwin'
214
+ else:
215
+ return 'linux' # Linux 和其他 Unix-like 系统
216
+
217
+ @classmethod
218
+ def find_browser(cls) -> Optional[str]:
219
+ """
220
+ 查找可用的浏览器路径
221
+ 返回找到的第一个可执行浏览器路径,未找到返回None
222
+ """
223
+ current_platform = cls.get_platform()
224
+
225
+ # 1. 首先检查环境变量
226
+ for env_var in ['BROWSER_PATH', 'CHROME_PATH', 'CHROMIUM_PATH', 'CHROME_EXECUTABLE_PATH']:
227
+ env_path = os.getenv(env_var)
228
+ if env_path and cls._is_valid_executable(env_path):
229
+ return env_path
230
+
231
+ # 2. 使用 which/where 命令查找
232
+ executables = cls.BROWSER_EXECUTABLES.get(current_platform, cls.BROWSER_EXECUTABLES['linux'])
233
+ for name in executables:
234
+ path = shutil.which(name)
235
+ if path and cls._is_valid_executable(path):
236
+ return path
237
+
238
+ # 3. 检查 glob 模式(Playwright/Puppeteer)
239
+ for pattern in cls.GLOB_PATTERNS:
240
+ matches = glob.glob(pattern)
241
+ for match in sorted(matches, reverse=True): # 优先使用最新版本
242
+ if cls._is_valid_executable(match):
243
+ return match
244
+
245
+ # 4. 遍历常见路径
246
+ common_paths = cls.COMMON_PATHS.get(current_platform, cls.COMMON_PATHS['linux'])
247
+ for base_path in common_paths:
248
+ # 处理包含通配符的路径
249
+ if '*' in base_path:
250
+ expanded_paths = glob.glob(base_path)
251
+ else:
252
+ expanded_paths = [base_path]
253
+
254
+ for expanded_path in expanded_paths:
255
+ if not os.path.isdir(expanded_path):
256
+ continue
257
+
258
+ for name in executables:
259
+ full_path = os.path.join(expanded_path, name)
260
+ if cls._is_valid_executable(full_path):
261
+ return full_path
262
+
263
+ # 5. macOS 特殊处理:检查 .app 包
264
+ if current_platform == 'darwin':
265
+ found = cls._find_macos_app()
266
+ if found:
267
+ return found
268
+
269
+ # 6. 递归搜索特定目录
270
+ search_roots = {
271
+ 'windows': [
272
+ os.path.expandvars(r'%ProgramFiles%'),
273
+ os.path.expandvars(r'%LocalAppData%'),
274
+ ],
275
+ 'darwin': [
276
+ '/Applications',
277
+ os.path.expanduser('~/Applications'),
278
+ ],
279
+ 'linux': [
280
+ '/opt',
281
+ '/usr/lib',
282
+ '/snap',
283
+ os.path.expanduser('~/.cache'),
284
+ ],
285
+ }
286
+
287
+ for search_root in search_roots.get(current_platform, []):
288
+ if os.path.isdir(search_root):
289
+ found = cls._recursive_search(search_root, executables, max_depth=4)
290
+ if found:
291
+ return found
292
+
293
+ # 7. Linux: 使用 find 命令(最后手段)
294
+ if current_platform == 'linux':
295
+ found = cls._find_with_command(executables)
296
+ if found:
297
+ return found
298
+
299
+ return None
300
+
301
+ @classmethod
302
+ def _is_valid_executable(cls, path: str) -> bool:
303
+ """检查路径是否是有效的可执行文件"""
304
+ if not path:
305
+ return False
306
+
307
+ # 处理 Windows 路径
308
+ path = os.path.normpath(path)
309
+
310
+ if not os.path.isfile(path):
311
+ return False
312
+
313
+ # Windows 不需要检查执行权限
314
+ if cls.get_platform() == 'windows':
315
+ return path.lower().endswith('.exe')
316
+
317
+ return os.access(path, os.X_OK)
318
+
319
+ @classmethod
320
+ def _find_macos_app(cls) -> Optional[str]:
321
+ """macOS 特殊处理:查找 .app 包"""
322
+ app_dirs = ['/Applications', os.path.expanduser('~/Applications')]
323
+ app_names = [
324
+ ('Google Chrome.app', 'Contents/MacOS/Google Chrome'),
325
+ ('Chromium.app', 'Contents/MacOS/Chromium'),
326
+ ('Microsoft Edge.app', 'Contents/MacOS/Microsoft Edge'),
327
+ ('Brave Browser.app', 'Contents/MacOS/Brave Browser'),
328
+ ('Vivaldi.app', 'Contents/MacOS/Vivaldi'),
329
+ ('Opera.app', 'Contents/MacOS/Opera'),
330
+ ]
331
+
332
+ for app_dir in app_dirs:
333
+ for app_name, executable_path in app_names:
334
+ full_path = os.path.join(app_dir, app_name, executable_path)
335
+ if cls._is_valid_executable(full_path):
336
+ return full_path
337
+
338
+ return None
339
+
340
+ @classmethod
341
+ def _recursive_search(cls, root: str, executables: List[str], max_depth: int = 3, current_depth: int = 0) -> Optional[str]:
342
+ """递归搜索目录"""
343
+ if current_depth >= max_depth:
344
+ return None
345
+
346
+ # 跳过的目录
347
+ skip_dirs = {'.git', 'node_modules', '__pycache__', 'cache', 'tmp', 'temp',
348
+ 'logs', 'log', '.npm', '.yarn', 'site-packages', 'dist-packages'}
349
+
350
+ try:
351
+ for entry in os.scandir(root):
352
+ try:
353
+ if entry.is_file(follow_symlinks=True):
354
+ if entry.name in executables or entry.name.lower() in [e.lower() for e in executables]:
355
+ if cls._is_valid_executable(entry.path):
356
+ return entry.path
357
+ elif entry.is_dir(follow_symlinks=False):
358
+ if entry.name.lower() in skip_dirs:
359
+ continue
360
+ found = cls._recursive_search(entry.path, executables, max_depth, current_depth + 1)
361
+ if found:
362
+ return found
363
+ except (PermissionError, OSError):
364
+ continue
365
+ except (PermissionError, OSError):
366
+ pass
367
+
368
+ return None
369
+
370
+ @classmethod
371
+ def _find_with_command(cls, executables: List[str]) -> Optional[str]:
372
+ """使用系统命令查找浏览器(Linux)"""
373
+ for name in executables[:5]: # 只检查前几个常见的
374
+ try:
375
+ result = subprocess.run(
376
+ ['find', '/', '-maxdepth', '6', '-name', name, '-type', 'f', '-executable', '-print', '-quit'],
377
+ capture_output=True,
378
+ text=True,
379
+ timeout=30,
380
+ stderr=subprocess.DEVNULL
381
+ )
382
+ if result.returncode == 0 and result.stdout.strip():
383
+ path = result.stdout.strip().split('\n')[0]
384
+ if cls._is_valid_executable(path):
385
+ return path
386
+ except (subprocess.TimeoutExpired, FileNotFoundError, Exception):
387
+ continue
388
+
389
+ return None
390
+
391
+ @classmethod
392
+ def get_browser_info(cls) -> Dict:
393
+ """获取浏览器信息"""
394
+ path = cls.find_browser()
395
+ info = {
396
+ 'found': path is not None,
397
+ 'path': path,
398
+ 'version': None,
399
+ 'platform': cls.get_platform()
400
+ }
401
+
402
+ return info
403
+
404
+ @classmethod
405
+ def find_all_browsers(cls) -> List[Dict]:
406
+ """查找所有可用的浏览器"""
407
+ browsers = []
408
+ seen_paths = set()
409
+ current_platform = cls.get_platform()
410
+ executables = cls.BROWSER_EXECUTABLES.get(current_platform, cls.BROWSER_EXECUTABLES['linux'])
411
+
412
+ # 查找所有可能的浏览器
413
+ for name in executables:
414
+ path = shutil.which(name)
415
+ if path and path not in seen_paths and cls._is_valid_executable(path):
416
+ seen_paths.add(path)
417
+ browsers.append({'name': name, 'path': path})
418
+
419
+ # 检查 glob 模式
420
+ for pattern in cls.GLOB_PATTERNS:
421
+ matches = glob.glob(pattern)
422
+ for match in matches:
423
+ if match not in seen_paths and cls._is_valid_executable(match):
424
+ seen_paths.add(match)
425
+ browsers.append({'name': os.path.basename(match), 'path': match})
426
+
427
+ return browsers
428
+ class Config:
429
+ """配置管理器"""
430
+
431
+ def __init__(self):
432
+ self._lock = threading.Lock()
433
+ self._browser_path = None
434
+ self._user_agent = None
435
+ self._max_workers = 1
436
+ self._headless = True
437
+ self._email_configs = []
438
+ self._browser_fingerprint = {}
439
+ self._load_from_env()
440
+ self._detect_browser()
441
+
442
+ def _detect_browser(self):
443
+ """检测浏览器路径"""
444
+ import os
445
+
446
+ # 优先使用环境变量
447
+ env_path = os.getenv('BROWSER_PATH') or os.getenv('CHROME_PATH')
448
+ if env_path and os.path.isfile(env_path):
449
+ self._browser_path = env_path
450
+ logger.info(f"[Config] 使用环境变量指定的浏览器: {env_path}")
451
+ return
452
+
453
+ # 自动检测
454
+ # 自动检测
455
+ logger.info(f"[Config] 正在自动检测浏览器路径 (平台: {BrowserPathFinder.get_platform()})...")
456
+ browser_info = BrowserPathFinder.get_browser_info()
457
+
458
+ if browser_info['found']:
459
+ self._browser_path = browser_info['path']
460
+ if browser_info['found']:
461
+ self._browser_path = browser_info['path']
462
+ logger.info(f"[Config] ✓ 检测到浏览器: {browser_info['path']}")
463
+ if browser_info['version']:
464
+ logger.info(f"[Config] ✓ 浏览器版本: {browser_info['version']}")
465
+ else:
466
+ logger.warning("[Config] ✗ 警告: 未检测到可用的浏览器!")
467
+ logger.warning("[Config] 请确保已安装 Chrome/Chromium/Edge 或设置 BROWSER_PATH 环境变量")
468
+ logger.warning("[Config] 支持的浏览器: Chrome, Chromium, Edge, Brave, Vivaldi, Opera")
469
+
470
+ # 列出所有检查过的路径(调试用)
471
+ all_browsers = BrowserPathFinder.find_all_browsers()
472
+ if all_browsers:
473
+ logger.info(f"[Config] 找到以下浏览器但可能不兼容:")
474
+ for b in all_browsers:
475
+ logger.info(f"[Config] - {b['name']}: {b['path']}")
476
+
477
+ def get_browser_path(self) -> Optional[str]:
478
+ """获取浏览器路径"""
479
+ with self._lock:
480
+ return self._browser_path
481
+
482
+ def set_browser_path(self, path: str):
483
+ """设置浏览器路径"""
484
+ with self._lock:
485
+ if os.path.isfile(path):
486
+ self._browser_path = path
487
+ else:
488
+ raise ValueError(f"无效的浏览器路径: {path}")
489
+
490
+ def _load_from_env(self):
491
+ """从环境变量加载配置"""
492
+ # User-Agent
493
+ self._user_agent = os.getenv(
494
+ 'USER_AGENT',
495
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36'
496
+ )
497
+
498
+ # 最大工作线程数
499
+ self._max_workers = int(os.getenv('MAX_WORKERS', '1'))
500
+
501
+ # 无头模式
502
+ self._headless = os.getenv('HEADLESS', 'true').lower() == 'true'
503
+
504
+ # 邮箱配置(支持多个,用分号分隔)
505
+ self._email_configs = []
506
+
507
+ worker_domains = os.getenv('WORKER_DOMAINS', '').split(';')
508
+ email_domains = os.getenv('EMAIL_DOMAINS', '').split(';')
509
+ admin_passwords = os.getenv('ADMIN_PASSWORDS', '').split(';')
510
+
511
+ # 确保三个列表长度一致
512
+ max_len = max(len(worker_domains), len(email_domains), len(admin_passwords))
513
+
514
+ for i in range(max_len):
515
+ worker = worker_domains[i].strip() if i < len(worker_domains) else ''
516
+ email = email_domains[i].strip() if i < len(email_domains) else ''
517
+ password = admin_passwords[i].strip() if i < len(admin_passwords) else ''
518
+
519
+ if worker and email and password:
520
+ self._email_configs.append({
521
+ 'worker_domain': worker,
522
+ 'email_domain': email,
523
+ 'admin_password': password
524
+ })
525
+
526
+ # 浏览器指纹配置
527
+ self._browser_fingerprint = {
528
+ 'window_size': os.getenv('WINDOW_SIZE', '1920x1080'),
529
+ 'timezone': os.getenv('TIMEZONE', 'Asia/Shanghai'),
530
+ 'locale': os.getenv('LOCALE', 'zh-CN'),
531
+ 'platform': os.getenv('PLATFORM', 'Win64'),
532
+ 'color_depth': int(os.getenv('COLOR_DEPTH', '24')),
533
+ 'device_memory': int(os.getenv('DEVICE_MEMORY', '8')),
534
+ 'hardware_concurrency': int(os.getenv('HARDWARE_CONCURRENCY', '8'))
535
+ }
536
+
537
+ def get_user_agent(self) -> str:
538
+ with self._lock:
539
+ return self._user_agent
540
+
541
+ def set_user_agent(self, ua: str):
542
+ with self._lock:
543
+ self._user_agent = ua
544
+
545
+ def get_max_workers(self) -> int:
546
+ with self._lock:
547
+ return self._max_workers
548
+
549
+ def set_max_workers(self, count: int):
550
+ with self._lock:
551
+ self._max_workers = max(1, min(count, 10))
552
+
553
+ def get_headless(self) -> bool:
554
+ with self._lock:
555
+ return self._headless
556
+
557
+ def set_headless(self, headless: bool):
558
+ with self._lock:
559
+ self._headless = headless
560
+
561
+ def get_email_configs(self) -> List[Dict]:
562
+ with self._lock:
563
+ return self._email_configs.copy()
564
+
565
+ def get_email_configs_safe(self) -> List[Dict]:
566
+ """获取邮箱配置(隐藏密码)"""
567
+ with self._lock:
568
+ return [
569
+ {
570
+ 'worker_domain': c['worker_domain'],
571
+ 'email_domain': c['email_domain'],
572
+ 'admin_password': '***'
573
+ }
574
+ for c in self._email_configs
575
+ ]
576
+
577
+ def get_random_email_config(self) -> Optional[Dict]:
578
+ with self._lock:
579
+ if self._email_configs:
580
+ return random.choice(self._email_configs)
581
+ return None
582
+
583
+ def add_email_config(self, worker_domain: str, email_domain: str, admin_password: str):
584
+ with self._lock:
585
+ self._email_configs.append({
586
+ 'worker_domain': worker_domain,
587
+ 'email_domain': email_domain,
588
+ 'admin_password': admin_password
589
+ })
590
+
591
+ def update_email_config(self, index: int, worker_domain: str = None,
592
+ email_domain: str = None, admin_password: str = None):
593
+ with self._lock:
594
+ if 0 <= index < len(self._email_configs):
595
+ if worker_domain:
596
+ self._email_configs[index]['worker_domain'] = worker_domain
597
+ if email_domain:
598
+ self._email_configs[index]['email_domain'] = email_domain
599
+ if admin_password:
600
+ self._email_configs[index]['admin_password'] = admin_password
601
+ else:
602
+ raise IndexError("配置索引不存在")
603
+
604
+ def delete_email_config(self, index: int):
605
+ with self._lock:
606
+ if 0 <= index < len(self._email_configs):
607
+ del self._email_configs[index]
608
+ else:
609
+ raise IndexError("配置索引不存在")
610
+
611
+ def get_browser_fingerprint(self) -> Dict:
612
+ with self._lock:
613
+ return self._browser_fingerprint.copy()
614
+
615
+ def set_browser_fingerprint(self, fingerprint: Dict):
616
+ with self._lock:
617
+ self._browser_fingerprint.update(fingerprint)
email_manager.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ import time
3
+ import random
4
+ import string
5
+ from typing import Optional, Tuple
6
+ from datetime import datetime, timezone, timedelta
7
+ from email.utils import parsedate_to_datetime
8
+
9
+ import requests
10
+ import logging
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class EmailManager:
16
+ """邮箱管理器"""
17
+
18
+ def __init__(self, worker_domain: str, email_domain: str, admin_password: str):
19
+ self.worker_domain = worker_domain
20
+ self.email_domain = email_domain
21
+ self.admin_password = admin_password
22
+
23
+ @staticmethod
24
+ def generate_random_name() -> str:
25
+ """生成随机邮箱名称"""
26
+ letters1 = ''.join(random.choices(string.ascii_lowercase, k=4))
27
+ numbers = ''.join(random.choices(string.digits, k=2))
28
+ letters2 = ''.join(random.choices(string.ascii_lowercase, k=3))
29
+ return letters1 + numbers + letters2
30
+
31
+ def create_email(self, username: str = "") -> Tuple[Optional[str], Optional[str]]:
32
+ """创建邮箱"""
33
+ try:
34
+ name = username if username else self.generate_random_name()
35
+
36
+ res = requests.post(
37
+ f"https://{self.worker_domain}/admin/new_address",
38
+ json={
39
+ "enablePrefix": True,
40
+ "name": name,
41
+ "domain": self.email_domain,
42
+ },
43
+ headers={
44
+ 'x-admin-auth': self.admin_password,
45
+ "Content-Type": "application/json"
46
+ },
47
+ timeout=30
48
+ )
49
+
50
+ if res.status_code == 200:
51
+ data = res.json()
52
+ return data.get('jwt'), data.get('address')
53
+ else:
54
+ return None, None
55
+ except Exception as e:
56
+ logger.error(f"创建邮箱出错: {e}")
57
+ return None, None
58
+
59
+ def check_verification_code(self, email: str, max_retries: int = 15, interval: float = 3.0) -> Optional[str]:
60
+ """检查验证码邮件"""
61
+ for attempt in range(max_retries):
62
+ try:
63
+ api_url = f"https://{self.worker_domain}/admin/mails"
64
+ res = requests.get(
65
+ api_url,
66
+ params={"limit": 5, "offset": 0, "address": email},
67
+ headers={
68
+ "x-admin-auth": self.admin_password,
69
+ "Content-Type": "application/json"
70
+ },
71
+ timeout=30
72
+ )
73
+
74
+ if res.status_code == 200:
75
+ data = res.json()
76
+
77
+ if data.get('results') and len(data['results']) > 0:
78
+ email_data = data['results'][0]
79
+ raw_content = email_data.get('raw', '')
80
+
81
+ # 检查邮件时间
82
+ try:
83
+ # 提取 Received 头中的时间
84
+ received_match = re.search(r'Received:.*?;\s*(.*?)\r\n', raw_content, re.DOTALL)
85
+ if received_match:
86
+ date_str = received_match.group(1).strip()
87
+ email_time = parsedate_to_datetime(date_str)
88
+ current_time = datetime.now(timezone.utc)
89
+
90
+ # 如果邮件时间与当前时间相差超过1分钟,则认为是旧邮件
91
+ if (current_time - email_time) > timedelta(minutes=1):
92
+ logger.warning(f"忽略过期邮件 (时间: {email_time}, 当前: {current_time})")
93
+ time.sleep(interval)
94
+ continue
95
+ except Exception as e:
96
+ logger.warning(f"解析邮件时间失败: {e}")
97
+
98
+ cleaned_content = raw_content.replace('=\r\n', '').replace('=\n', '').replace('=3D', '=')
99
+
100
+ patterns = [
101
+ r'class=["\']?verification-code["\']?[^>]*>([A-Z0-9]{6})</span>',
102
+ r'verification-code[^>]*>([A-Z0-9]{6})<',
103
+ r'>([A-Z0-9]{6})</span>',
104
+ r'font-size:\s*28px[^>]*>([A-Z0-9]{6})<',
105
+ ]
106
+
107
+ for pattern in patterns:
108
+ match = re.search(pattern, cleaned_content, re.IGNORECASE | re.DOTALL)
109
+ if match:
110
+ code = match.group(1).upper()
111
+ if len(code) == 6 and code.isalnum():
112
+ logger.info(f"找到验证码: {code}")
113
+ return code
114
+
115
+ time.sleep(interval)
116
+
117
+ except Exception as e:
118
+ logger.error(f"检查验证码出错: {e}")
119
+ time.sleep(interval)
120
+
121
+ return None