commit 7feccf246dd82e3038ad57b14560a7dae027b9df Author: sjk <2513533895@qq.com> Date: Tue Jan 13 18:59:26 2026 +0800 'init' diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..f9c891d --- /dev/null +++ b/.env.example @@ -0,0 +1,26 @@ +# 环境标识:development 或 production +ENV=development + +# AdsPower配置 +ADSPOWER_API_URL=http://127.0.0.1:50325 +ADSPOWER_USER_ID=e5afd5a4cead5589247febbeabc39bcb +ADSPOWER_API_KEY= + +# 服务配置 +SERVER_HOST=127.0.0.1 +SERVER_PORT=5000 + +# 点击策略配置 +MIN_CLICK_COUNT=1 +MAX_CLICK_COUNT=10 +CLICK_INTERVAL_MINUTES=30 +WORK_START_HOUR=9 +WORK_END_HOUR=21 +REPLY_WAIT_TIMEOUT=30 + +# 数据存储路径 +DATA_DIR=./data +LOG_DIR=./logs + +# 调试模式 +DEBUG=True diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fa10030 --- /dev/null +++ b/.gitignore @@ -0,0 +1,86 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# 虚拟环境 +venv/ +ENV/ +env/ +.venv + +# 环境变量配置文件 +.env +.env.local +.env.*.local +.env.development +.env.production + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ +.DS_Store + +# 日志文件 +*.log +logs/ +log/ + +# 测试截图 +test_screenshot*.png +screenshot*.png + +# 数据文件 +urls_data.json +*.db +*.sqlite +*.sqlite3 + +# 临时文件 +*.tmp +*.temp +*.bak +.cache/ + +# Playwright +.playwright/ + +# 操作系统 +Thumbs.db +.DS_Store + +# Flask session +flask_session/ +instance/ + +# PyCharm +.idea/ + +# VS Code +.vscode/ + +# Jupyter Notebook +.ipynb_checkpoints + +# 配置备份 +*.backup diff --git a/README.md b/README.md new file mode 100644 index 0000000..901df15 --- /dev/null +++ b/README.md @@ -0,0 +1,258 @@ +# MIP页广告自动点击服务 + +基于 AdsPower 的 MIP 页面广告自动化点击系统,支持自动识别商业广告、智能点击策略和数据统计反馈。 + +## 功能特性 + +- 自动识别 MIP 页面中的商业广告(排除 AI 健康管家等非商业内容) +- 智能点击策略:随机点击次数、固定间隔时间、工作时间限制 +- 自动等待广告主回复并记录 +- 完整的数据统计和反馈机制 +- RESTful API 接口,支持运营人员提交链接 +- 持久化存储,支持断点续传 + +## 系统要求 + +- Python 3.8+ +- AdsPower 浏览器客户端 +- Chrome/Chromium 浏览器 + +## 安装步骤 + +### 1. 克隆或下载项目 + +```bash +cd d:\project\Work\ai_mip +``` + +### 2. 创建虚拟环境(推荐) + +```bash +python -m venv venv +venv\Scripts\activate # Windows +``` + +### 3. 安装依赖 + +```bash +pip install -r requirements.txt +``` + +### 4. 配置环境变量 + +复制 `.env.example` 为 `.env` 并修改配置: + +```bash +copy .env.example .env +``` + +编辑 `.env` 文件,配置以下关键参数: + +```env +# AdsPower配置 +ADSPOWER_API_URL=http://local.adspower.net:50325 +ADSPOWER_USER_ID=your_user_id_here + +# 点击策略配置 +MIN_CLICK_COUNT=1 +MAX_CLICK_COUNT=10 +CLICK_INTERVAL_MINUTES=30 +WORK_START_HOUR=9 +WORK_END_HOUR=21 +REPLY_WAIT_TIMEOUT=30 +``` + +## 使用说明 + +### 启动服务 + +```bash +python app.py +``` + +服务默认运行在 `http://localhost:5000` + +### API 接口 + +#### 1. 健康检查 + +```bash +GET /health +``` + +#### 2. 添加单个 URL + +```bash +POST /api/urls +Content-Type: application/json + +{ + "url": "https://example.com/mip-page" +} +``` + +#### 3. 批量添加 URL + +```bash +POST /api/urls +Content-Type: application/json + +{ + "urls": [ + "https://example.com/mip-page-1", + "https://example.com/mip-page-2" + ] +} +``` + +#### 4. 获取所有 URL 列表 + +```bash +GET /api/urls +``` + +响应示例: +```json +{ + "success": true, + "data": [ + { + "url": "https://example.com/mip-page", + "status": "active", + "target_clicks": 5, + "click_count": 2, + "reply_count": 1, + "created_time": "2026-01-12T10:00:00", + "last_click_time": "2026-01-12T11:30:00" + } + ] +} +``` + +#### 5. 获取 URL 详细信息 + +```bash +GET /api/urls/{url} +``` + +#### 6. 删除 URL + +```bash +DELETE /api/urls/{url} +``` + +#### 7. 重置 URL(重新开始点击) + +```bash +POST /api/urls/{url}/reset +``` + +#### 8. 获取统计数据 + +```bash +GET /api/statistics +``` + +响应示例: +```json +{ + "success": true, + "data": { + "total_urls": 10, + "active_urls": 5, + "completed_urls": 4, + "failed_urls": 1, + "total_clicks": 35, + "total_replies": 28, + "reply_rate": "80.00%" + } +} +``` + +#### 9. 调度器控制 + +启动调度器: +```bash +POST /api/scheduler/start +``` + +停止调度器: +```bash +POST /api/scheduler/stop +``` + +查看调度器状态: +```bash +GET /api/scheduler/status +``` + +## 点击策略说明 + +系统会根据以下策略自动执行点击任务: + +1. **随机点击次数**:每个 URL 的目标点击次数在 1-10 次之间随机生成 +2. **点击间隔**:同一 URL 两次点击之间至少间隔 30 分钟 +3. **工作时间**:仅在 09:00-21:00 之间执行点击任务 +4. **回复等待**:点击广告后等待 30 秒检测广告主回复 + +## 广告识别逻辑 + +系统会自动识别 MIP 页面中的商业广告: + +- ✅ **点击**:带有"广告"标识的商业广告 +- ❌ **跳过**:无广告的页面 +- ❌ **跳过**:AI 健康管家等非商业内容 + +## 数据存储 + +所有数据存储在 `./data/urls_data.json` 文件中,包括: + +- URL 列表和状态 +- 点击统计 +- 回复统计 +- 点击历史记录 + +## 日志 + +日志文件存储在 `./logs/` 目录下,包含详细的运行日志和错误信息。 + +## 项目结构 + +``` +ai_mip/ +├── app.py # Flask 应用主入口 +├── config.py # 配置管理 +├── adspower_client.py # AdsPower API 客户端 +├── ad_automation.py # 广告自动化操作 +├── scheduler.py # 点击任务调度器 +├── data_manager.py # 数据存储管理 +├── requirements.txt # Python 依赖 +├── .env.example # 环境变量示例 +├── .gitignore # Git 忽略配置 +├── README.md # 项目文档 +└── data/ # 数据目录(自动创建) + └── urls_data.json # URL 数据文件 +└── logs/ # 日志目录(自动创建) +``` + +## 注意事项 + +1. **AdsPower 配置**:确保 AdsPower 客户端已安装并运行,正确配置 `ADSPOWER_USER_ID` +2. **页面结构适配**:如果实际 MIP 页面结构与预期不同,需要修改 `ad_automation.py` 中的元素选择器 +3. **网络环境**:确保网络稳定,能够正常访问目标 MIP 页面 +4. **浏览器驱动**:AdsPower 会自动管理 ChromeDriver,无需手动配置 + +## 常见问题 + +### Q: 启动服务后无法连接 AdsPower? +A: 检查 AdsPower 是否正在运行,API 地址是否正确(默认:http://local.adspower.net:50325) + +### Q: 无法识别广告? +A: 需要根据实际页面结构调整 `ad_automation.py` 中的广告识别逻辑和选择器 + +### Q: 如何查看点击记录? +A: 调用 `/api/statistics` 接口查看整体统计,或使用 `/api/urls/{url}` 查看单个 URL 的详细点击历史 + +## 技术支持 + +如有问题,请查看日志文件或联系开发团队。 diff --git a/VENV_GUIDE.txt b/VENV_GUIDE.txt new file mode 100644 index 0000000..9900dd5 --- /dev/null +++ b/VENV_GUIDE.txt @@ -0,0 +1,81 @@ +# 虚拟环境使用指南 + +## 快速开始(推荐) + +直接双击运行 `start.bat`,脚本会自动: +1. 检查并创建虚拟环境 +2. 激活虚拟环境 +3. 安装依赖包 +4. 启动服务 + +## 手动使用虚拟环境 + +### Windows 系统 + +```bash +# 1. 创建虚拟环境 +python -m venv venv + +# 2. 激活虚拟环境 +venv\Scripts\activate + +# 3. 安装依赖 +pip install -r requirements.txt + +# 4. 运行服务 +python app.py + +# 5. 退出虚拟环境(使用完毕后) +deactivate +``` + +### Linux/Mac 系统 + +```bash +# 1. 创建虚拟环境 +python3 -m venv venv + +# 2. 激活虚拟环境 +source venv/bin/activate + +# 3. 安装依赖 +pip install -r requirements.txt + +# 4. 运行服务 +python app.py + +# 5. 退出虚拟环境 +deactivate +``` + +## 配置说明 + +在启动服务前,需要配置 `.env` 文件: + +**必须配置项:** +- `ADSPOWER_USER_ID`: 在 AdsPower 中创建的用户 ID + +**可选配置项:** +- 其他配置项保持默认值即可 + +## 验证虚拟环境 + +激活虚拟环境后,命令行提示符前会显示 `(venv)`: + +``` +(venv) D:\project\Work\ai_mip> +``` + +## 常见问题 + +**Q: 如何确认已在虚拟环境中?** +A: 命令行提示符前有 `(venv)` 标识 + +**Q: 虚拟环境在哪里?** +A: 项目根目录下的 `venv` 文件夹 + +**Q: 需要每次都激活虚拟环境吗?** +A: 使用 `start.bat` 会自动激活,手动运行需要先激活 + +**Q: 如何重建虚拟环境?** +A: 删除 `venv` 文件夹,重新运行 `start.bat` 或手动创建 diff --git a/ad_automation.py b/ad_automation.py new file mode 100644 index 0000000..c61dddd --- /dev/null +++ b/ad_automation.py @@ -0,0 +1,219 @@ +import time +import random +from typing import Optional, Tuple +from playwright.sync_api import Page, ElementHandle +from loguru import logger +from config import Config + + +class MIPAdAutomation: + """MIP页面广告自动化操作""" + + def __init__(self, page: Page): + self.page = page + + def check_and_click_ad(self, url: str) -> Tuple[bool, bool]: + """ + 检查并点击广告 + + Args: + url: MIP页面链接 + + Returns: + (是否点击成功, 是否获得回复) + """ + try: + # 访问链接 + logger.info(f"访问链接: {url}") + self.page.goto(url, wait_until='domcontentloaded') + + # 等待页面加载 + time.sleep(3) + + # 检查是否存在商业广告 + has_ad, ad_element = self._detect_commercial_ad() + + if not has_ad: + logger.info("未检测到商业广告,跳过该链接") + return False, False + + # 点击广告 + logger.info("检测到商业广告,准备点击") + if not self._click_advertisement(ad_element): + logger.warning("点击广告失败") + return False, False + + # 等待并检查回复 + has_reply = self._wait_for_reply() + + return True, has_reply + + except Exception as e: + logger.error(f"处理链接异常: {str(e)}") + return False, False + finally: + # 尝试关闭当前标签页,返回主窗口 + self._close_current_tab() + + def _detect_commercial_ad(self) -> Tuple[bool, Optional[ElementHandle]]: + """ + 检测页面是否存在商业广告 + + Returns: + (是否存在商业广告, 广告元素) + """ + try: + # 等待评论区加载 + time.sleep(2) + + # 方法1: 查找包含"广告"标识的元素 + # 根据实际页面结构调整选择器 + ad_selectors = [ + "//div[contains(@class, 'ad') or contains(@class, 'advertisement')]", + "//div[contains(text(), '广告')]", + "//*[contains(text(), '广告')]//ancestor::div[contains(@class, 'card')]", + "//a[contains(@class, 'ad-link')]", + ] + + for selector in ad_selectors: + try: + elements = self.page.locator(f"xpath={selector}").all() + if elements: + # 检查元素是否可见 + for elem in elements: + if elem.is_visible(): + # 进一步验证是否是商业广告(非AI健康管家) + elem_text = elem.inner_text().lower() + if '广告' in elem_text and 'ai健康' not in elem_text: + logger.info("检测到商业广告") + return True, elem + except Exception: + continue + + logger.info("未检测到商业广告") + return False, None + + except Exception as e: + logger.error(f"检测广告异常: {str(e)}") + return False, None + + def _click_advertisement(self, ad_element: ElementHandle) -> bool: + """ + 点击广告元素 + + Args: + ad_element: 广告元素 + + Returns: + 是否点击成功 + """ + try: + # 获取当前页面 + context = self.page.context + + # 滚动到广告元素可见 + ad_element.scroll_into_view_if_needed() + time.sleep(1) + + # 监听新页面打开 + with context.expect_page() as new_page_info: + # 点击广告 + ad_element.click() + logger.info("已点击广告") + + # 等待新页面 + new_page = new_page_info.value + new_page.wait_for_load_state('domcontentloaded') + + # 切换到新页面 + self.page = new_page + logger.info("已切换到广告页面") + + return True + + except Exception as e: + logger.error(f"点击广告异常: {str(e)}") + return False + + def _wait_for_reply(self) -> bool: + """ + 等待广告主回复 + + Returns: + 是否收到回复 + """ + try: + logger.info(f"等待广告主回复(最多{Config.REPLY_WAIT_TIMEOUT}秒)") + + # 检查是否已经自动发送消息 + time.sleep(2) + + # 等待并检查回复 + start_time = time.time() + timeout = Config.REPLY_WAIT_TIMEOUT + + # 根据实际页面结构调整回复检测逻辑 + # 这里使用轮询方式检查是否有新消息 + initial_msg_count = self._count_messages() + + while time.time() - start_time < timeout: + time.sleep(2) + current_msg_count = self._count_messages() + + # 如果消息数量增加,说明收到了回复 + if current_msg_count > initial_msg_count: + logger.info("收到广告主回复") + return True + + logger.info("未收到广告主回复(超时)") + return False + + except Exception as e: + logger.error(f"等待回复异常: {str(e)}") + return False + + def _count_messages(self) -> int: + """ + 统计当前页面的消息数量 + + Returns: + 消息数量 + """ + try: + # 根据实际页面结构调整选择器 + # 这里是示例选择器,需要根据实际情况修改 + message_selectors = [ + "//div[contains(@class, 'message')]", + "//div[contains(@class, 'chat-message')]", + "//div[contains(@class, 'msg-item')]", + ] + + for selector in message_selectors: + try: + messages = self.page.locator(f"xpath={selector}").all() + if messages: + return len(messages) + except: + continue + + return 0 + + except Exception as e: + logger.error(f"统计消息数量异常: {str(e)}") + return 0 + + def _close_current_tab(self): + """关闭当前标签页并返回主窗口""" + try: + pages = self.page.context.pages + if len(pages) > 1: + self.page.close() + self.page = pages[0] + logger.info("已关闭广告页面") + except Exception as e: + logger.error(f"关闭标签页异常: {str(e)}") + + def random_delay(self, min_seconds: int = 2, max_seconds: int = 5): + """随机延迟,模拟人工操作""" + delay = random.uniform(min_seconds, max_seconds) + time.sleep(delay) diff --git a/adspower_client.py b/adspower_client.py new file mode 100644 index 0000000..3c5e2aa --- /dev/null +++ b/adspower_client.py @@ -0,0 +1,1090 @@ +import requests +from typing import Dict, Optional +from loguru import logger +from playwright.sync_api import sync_playwright, Browser, Page +from config import Config +import json + + +class AdsPowerClient: + """AdsPower API客户端 - 集成 Playwright CDP + 参考官方示例: localAPI-main/py-examples/example-start-profile.py + """ + + # 大麦IP代理配置 + DAMAI_API_URL = "https://api2.damaiip.com/index.php?s=/front/user/getIPlist&xsn=2912cb2b22d3b7ae724f045012790479&osn=TC_NO176707424165606223&tiqu=1" + DAMAI_USER = "69538fdef04e1" + DAMAI_PASSWORD = "63v0kQBr2yJXnjf" + + def __init__(self, api_url: str = None, user_id: str = None, api_key: str = None): + self.api_url = api_url or Config.ADSPOWER_API_URL + self.user_id = user_id or Config.ADSPOWER_USER_ID + self.api_key = api_key or Config.ADSPOWER_API_KEY + self.playwright = None + self.browser = None + + # 调试信息 + if self.api_key: + logger.debug(f"AdsPowerClient 初始化 - API Key: {self.api_key[:8]}... (len={len(self.api_key)})") + else: + logger.debug("AdsPowerClient 初始化 - 未配置 API Key") + + def update_profile_proxy(self, profile_id: str, proxy_id: str) -> bool: + """ + 更新 Profile 的代理配置 + 使用 AdsPower API v2 + + Args: + profile_id: Profile ID + proxy_id: 代理ID + + Returns: + 是否成功 + """ + try: + url = f"{self.api_url}/api/v2/browser-profile/update" + + # 准备请求头 + headers = { + 'Content-Type': 'application/json' + } + if self.api_key: + headers['Authorization'] = f'Bearer {self.api_key}' + + # 准备请求体 + payload = { + "profile_id": profile_id, + "proxy_id": proxy_id + } + + logger.info("\n" + "="*70) + logger.info("更新 Profile 代理") + logger.info("="*70) + logger.info(f"URL: {url}") + logger.info(f"Method: POST") + logger.info(f"Payload:") + logger.info(json.dumps(payload, indent=2, ensure_ascii=False)) + + response = requests.post(url, json=payload, headers=headers, timeout=30) + + logger.info("\n" + "-"*70) + logger.info("响应信息") + logger.info("-"*70) + logger.info(f"Status Code: {response.status_code}") + + try: + response_json = response.json() + logger.info("Response Body:") + logger.info(json.dumps(response_json, indent=2, ensure_ascii=False)) + except: + logger.info(f"Response Body (Raw):") + logger.info(response.text) + + logger.info("="*70 + "\n") + + result = response_json if 'response_json' in locals() else response.json() + + if result.get('code') == 0: + logger.success(f"成功更新 Profile 代理: {profile_id} -> Proxy {proxy_id}") + + # 验证代理是否真正更新 + import time + time.sleep(1) # 等待配置生效 + + verify_result = self.get_profile_info(profile_id) + if verify_result: + actual_proxy_id = verify_result.get('data', {}).get('proxy_id', '') + logger.info(f"验证代理配置 - 期望: {proxy_id}, 实际: {actual_proxy_id}") + if actual_proxy_id == proxy_id: + logger.success("✅ 代理配置验证通过") + return True + else: + logger.warning(f"❌ 代理配置验证失败 - Profile中的proxy_id不匹配") + return False + else: + logger.warning("无法验证代理配置,但API返回成功") + return True + else: + logger.error(f"更新 Profile 代理失败: {result.get('msg')}") + return False + + except Exception as e: + logger.error(f"更新 Profile 代理异常: {str(e)}") + return False + + def get_profile_info(self, profile_id: str) -> Optional[Dict]: + """ + 查询 Profile 详细信息 + 使用 AdsPower API v2 + + Args: + profile_id: Profile ID + + Returns: + Profile 信息 + """ + try: + url = f"{self.api_url}/api/v2/browser-profile/list" + + headers = { + 'Content-Type': 'application/json' + } + if self.api_key: + headers['Authorization'] = f'Bearer {self.api_key}' + + payload = { + "profile_id": profile_id + } + + response = requests.post(url, json=payload, headers=headers, timeout=30) + result = response.json() + + if result.get('code') == 0: + profiles = result.get('data', {}).get('list', []) + if profiles: + return {'code': 0, 'data': profiles[0]} + + return None + + except Exception as e: + logger.error(f"查询 Profile 信息异常: {str(e)}") + return None + + def update_profile_proxy_v1(self, profile_id: str, proxy_config: Dict) -> bool: + """ + 使用 API v1 方式直接更新 Profile 的代理配置 + 传入完整的 user_proxy_config 对象 + + Args: + profile_id: Profile ID (user_id) + proxy_config: 代理配置字典,格式: + { + "proxy_type": "http", + "proxy_host": "112.83.100.233", + "proxy_port": "11059", + "proxy_user": "69538fdef04e1", # 可选 + "proxy_password": "63v0kQBr2yJXnjf", # 可选 + "proxy_soft": "other" + } + + Returns: + 是否成功 + """ + try: + url = f"{self.api_url}/api/v1/user/update" + + # 准备请求头 + headers = { + 'Content-Type': 'application/json' + } + if self.api_key: + headers['Authorization'] = f'Bearer {self.api_key}' + + # 构建 user_proxy_config + proxy_port = proxy_config.get("proxy_port") + if isinstance(proxy_port, str): + proxy_port = int(proxy_port) + + user_proxy_config = { + "proxy_soft": proxy_config.get("proxy_soft", "other"), + "proxy_type": proxy_config.get("proxy_type", "http"), + "proxy_host": proxy_config["proxy_host"], + "proxy_port": proxy_port # 使用整数类型 + } + + # 如果有认证信息,添加到配置中 + if proxy_config.get("proxy_user") and proxy_config.get("proxy_password"): + user_proxy_config["proxy_user"] = proxy_config["proxy_user"] + user_proxy_config["proxy_password"] = proxy_config["proxy_password"] + logger.info("使用认证代理模式") + else: + logger.info("使用白名单代理模式(无认证)") + + # 准备请求体 + payload = { + "user_id": profile_id, + "user_proxy_config": user_proxy_config + } + + logger.info("\n" + "="*70) + logger.info("更新 Profile 代理 (API v1)") + logger.info("="*70) + logger.info(f"URL: {url}") + logger.info(f"Method: POST") + logger.info(f"Headers:") + for k, v in headers.items(): + if k == 'Authorization' and v: + logger.info(f" {k}: Bearer {v.split()[-1][:8]}...") + else: + logger.info(f" {k}: {v}") + logger.info(f"Payload:") + logger.info(json.dumps(payload, indent=2, ensure_ascii=False)) + + response = requests.post(url, json=payload, headers=headers, timeout=30) + + logger.info("\n" + "-"*70) + logger.info("响应信息") + logger.info("-"*70) + logger.info(f"Status Code: {response.status_code}") + + try: + response_json = response.json() + logger.info("Response Body:") + logger.info(json.dumps(response_json, indent=2, ensure_ascii=False)) + except: + logger.info(f"Response Body (Raw):") + logger.info(response.text) + + logger.info("="*70 + "\n") + + result = response_json if 'response_json' in locals() else response.json() + + if result.get('code') == 0: + logger.success(f"成功更新 Profile 代理 (API v1): {profile_id}") + + # 验证代理是否真正更新 + import time + time.sleep(1) # 等待配置生效 + + verify_result = self.get_profile_info(profile_id) + if verify_result: + actual_proxy = verify_result.get('data', {}).get('user_proxy_config', {}) + actual_host = actual_proxy.get('proxy_host', '') + actual_port = str(actual_proxy.get('proxy_port', '')) + expected_host = proxy_config['proxy_host'] + expected_port = str(proxy_port) + + logger.info(f"验证代理配置:") + logger.info(f" 期望: {expected_host}:{expected_port}") + logger.info(f" 实际: {actual_host}:{actual_port}") + + if actual_host == expected_host and actual_port == expected_port: + logger.success("✅ 代理配置验证通过") + return True + else: + logger.warning(f"❌ 代理配置验证失败 - 地址不匹配") + return False + else: + logger.warning("无法验证代理配置,但API返回成功") + return True + else: + logger.error(f"更新 Profile 代理失败 (API v1): {result.get('msg')}") + return False + + except Exception as e: + logger.error(f"更新 Profile 代理异常 (API v1): {str(e)}") + return False + + def update_profile_proxy_v2_direct(self, profile_id: str, proxy_config: Dict) -> bool: + """ + 使用 API v2 方式直接传入 user_proxy_config 更新代理 + 根据官方文档,API v2 支持 proxy_id 和 user_proxy_config 两种方式 + + Args: + profile_id: Profile ID + proxy_config: 代理配置字典,格式: + { + "proxy_type": "http", + "proxy_host": "112.83.100.233", + "proxy_port": "11059", # 可以是字符串或整数 + "proxy_user": "69538fdef04e1", # 可选 + "proxy_password": "63v0kQBr2yJXnjf", # 可选 + "proxy_soft": "other" + } + + Returns: + 是否成功 + """ + try: + url = f"{self.api_url}/api/v2/browser-profile/update" + + # 准备请求头 + headers = { + 'Content-Type': 'application/json' + } + if self.api_key: + headers['Authorization'] = f'Bearer {self.api_key}' + + # 解析端口(确保是整数类型) + proxy_port = proxy_config.get("proxy_port") + if isinstance(proxy_port, str): + proxy_port = int(proxy_port) + + # 构建 user_proxy_config + user_proxy_config = { + "proxy_soft": proxy_config.get("proxy_soft", "other"), + "proxy_type": proxy_config.get("proxy_type", "http"), + "proxy_host": proxy_config["proxy_host"], + "proxy_port": proxy_port # 使用整数类型! + } + + # 如果有认证信息,添加到配置中 + if proxy_config.get("proxy_user") and proxy_config.get("proxy_password"): + user_proxy_config["proxy_user"] = proxy_config["proxy_user"] + user_proxy_config["proxy_password"] = proxy_config["proxy_password"] + logger.info("使用认证代理模式") + else: + logger.info("使用白名单代理模式(无认证)") + + # 准备请求体(注意:API v2 使用 profile_id 而不是 user_id) + payload = { + "profile_id": profile_id, + "user_proxy_config": user_proxy_config + } + + logger.info("\n" + "="*70) + logger.info("更新 Profile 代理 (API v2 - Direct user_proxy_config)") + logger.info("="*70) + logger.info(f"URL: {url}") + logger.info(f"Method: POST") + logger.info(f"Headers:") + for k, v in headers.items(): + if k == 'Authorization' and v: + logger.info(f" {k}: Bearer {v.split()[-1][:8]}...") + else: + logger.info(f" {k}: {v}") + logger.info(f"Payload:") + logger.info(json.dumps(payload, indent=2, ensure_ascii=False)) + logger.info(f"注意: proxy_port类型为 {type(user_proxy_config['proxy_port']).__name__}") + + response = requests.post(url, json=payload, headers=headers, timeout=30) + + logger.info("\n" + "-"*70) + logger.info("响应信息") + logger.info("-"*70) + logger.info(f"Status Code: {response.status_code}") + + try: + response_json = response.json() + logger.info("Response Body:") + logger.info(json.dumps(response_json, indent=2, ensure_ascii=False)) + except: + logger.info(f"Response Body (Raw):") + logger.info(response.text) + + logger.info("="*70 + "\n") + + result = response_json if 'response_json' in locals() else response.json() + + if result.get('code') == 0: + logger.success(f"成功更新 Profile 代理 (API v2 Direct): {profile_id}") + + # 验证代理是否真正更新 + import time + time.sleep(1) + + verify_result = self.get_profile_info(profile_id) + if verify_result: + actual_proxy = verify_result.get('data', {}).get('user_proxy_config', {}) + actual_host = actual_proxy.get('proxy_host', '') + actual_port = str(actual_proxy.get('proxy_port', '')) + expected_host = proxy_config['proxy_host'] + expected_port = str(proxy_port) + + logger.info(f"验证代理配置:") + logger.info(f" 期望: {expected_host}:{expected_port}") + logger.info(f" 实际: {actual_host}:{actual_port}") + + if actual_host == expected_host and actual_port == expected_port: + logger.success("✅ 代理配置验证通过") + return True + else: + logger.warning(f"❌ 代理配置验证失败 - 地址不匹配") + return False + else: + logger.warning("无法验证代理配置,但API返回成功") + return True + else: + logger.error(f"更新 Profile 代理失败 (API v2 Direct): {result.get('msg')}") + return False + + except Exception as e: + logger.error(f"更新 Profile 代理异常 (API v2 Direct): {str(e)}") + return False + + def setup_proxy_and_start(self, user_id: str = None, use_proxy: bool = True) -> Optional[Dict]: + """ + 设置代理并启动浏览器 + + Args: + user_id: AdsPower用户ID + use_proxy: 是否使用代理 + + Returns: + 浏览器信息 + """ + target_user_id = user_id or self.user_id + + if use_proxy: + # 1. 获取大麦IP代理 + proxy_info = self.get_damai_proxy() + if not proxy_info: + logger.warning("获取代理失败,将不使用代理启动浏览器") + return self.start_browser(user_id=target_user_id) + + # 2. 创建 AdsPower 代理 + proxy_config = { + "type": "http", + "host": proxy_info["host"], + "port": proxy_info["port"], + "user": self.DAMAI_USER, + "password": self.DAMAI_PASSWORD, + "ipchecker": "ip2location", + "remark": "Damai Auto Proxy" + } + + proxy_id = self.create_proxy(proxy_config) + if not proxy_id: + logger.warning("创建代理失败,将不使用代理启动浏览器") + return self.start_browser(user_id=target_user_id) + + # 3. 更新 Profile 使用新代理 + if not self.update_profile_proxy(target_user_id, proxy_id): + logger.warning("更新 Profile 代理失败,将不使用代理启动浏览器") + return self.start_browser(user_id=target_user_id) + + # 4. 启动浏览器 + return self.start_browser(user_id=target_user_id) + + def start_browser(self, user_id: str = None) -> Optional[Dict]: + """ + 启动浏览器 + 使用 AdsPower API v2 + + Args: + user_id: AdsPower用户ID(Profile ID) + + Returns: + 包含浏览器连接信息的字典 + """ + target_user_id = user_id or self.user_id + if not target_user_id: + logger.error("未设置 AdsPower User ID") + return None + + try: + # 使用 AdsPower API v2 + url = f"{self.api_url}/api/v2/browser-profile/start" + + # 准备请求头 + headers = { + 'Content-Type': 'application/json' + } + if self.api_key: + headers['Authorization'] = f'Bearer {self.api_key}' + + # 准备请求体 + payload = { + "profile_id": target_user_id, + "launch_args": [], # 可以根据需要添加启动参数 + "headless": "0", + "last_opened_tabs": "1", + "proxy_detection": "1", + "password_filling": "0", + "password_saving": "0", + "cdp_mask": "1", + "delete_cache": "0", + "device_scale": "1" + } + + # 打印请求信息 + logger.info("\n" + "="*70) + logger.info("📤 启动浏览器请求 (API v2)") + logger.info("="*70) + logger.info(f"URL: {url}") + logger.info(f"Method: POST") + logger.info(f"Headers:") + for key, value in headers.items(): + if key == 'Authorization' and 'Bearer' in value: + display_value = f"Bearer {value.split(' ')[1][:8]}..." + else: + display_value = value + logger.info(f" {key}: {display_value}") + logger.info(f"Payload:") + logger.info(f" {json.dumps(payload, indent=2, ensure_ascii=False)}") + + response = requests.post(url, json=payload, headers=headers, timeout=30) + + # 打印响应信息 + logger.info("\n" + "-"*70) + logger.info("📥 响应信息") + logger.info("-"*70) + logger.info(f"Status Code: {response.status_code}") + logger.info(f"Headers:") + for key, value in response.headers.items(): + logger.info(f" {key}: {value}") + + # 格式化 JSON 响应 + try: + response_json = response.json() + logger.info(f"Response Body:") + logger.info(json.dumps(response_json, indent=2, ensure_ascii=False)) + except: + logger.info(f"Response Body (Raw):") + logger.info(response.text) + + logger.info("="*70 + "\n") + + result = response_json if 'response_json' in locals() else response.json() + + if result.get('code') == 0: + logger.info(f"成功启动浏览器,User ID: {target_user_id}") + # v2 API 的响应结构可能不同,需要根据实际返回调整 + if 'data' in result and 'ws' in result['data']: + logger.info(f"Selenium: {result['data']['ws'].get('selenium', 'N/A')}") + logger.info(f"Puppeteer: {result['data']['ws'].get('puppeteer', 'N/A')}") + return result + else: + logger.error(f"启动浏览器失败: {result.get('msg')}") + return None + + except Exception as e: + logger.error(f"启动浏览器异常: {str(e)}") + return None + + def connect_browser(self, browser_info: Dict) -> Optional[Browser]: + """ + 通过 CDP 连接到 AdsPower 浏览器 + + Args: + browser_info: start_browser 返回的浏览器信息 + + Returns: + Playwright Browser 实例 + """ + try: + # 获取 CDP WebSocket 端点 + ws_endpoint = browser_info['data']['ws']['puppeteer'] + + # 初始化 Playwright + if not self.playwright: + self.playwright = sync_playwright().start() + + # 通过 CDP 连接到浏览器 + self.browser = self.playwright.chromium.connect_over_cdp(ws_endpoint) + logger.info("成功通过 CDP 连接到 AdsPower 浏览器") + + return self.browser + + except Exception as e: + logger.error(f"CDP 连接失败: {str(e)}") + return None + + def get_page(self, browser: Browser) -> Optional[Page]: + """ + 获取或创建浏览器页面 + + Args: + browser: Browser 实例 + + Returns: + Page 实例 + """ + try: + # 获取默认上下文 + if browser.contexts: + context = browser.contexts[0] + else: + context = browser.new_context() + + # 获取或创建页面 + if context.pages: + page = context.pages[0] + else: + page = context.new_page() + + logger.info("成功获取浏览器页面") + return page + + except Exception as e: + logger.error(f"获取页面失败: {str(e)}") + return None + + def stop_browser(self, user_id: str = None) -> bool: + """ + 停止浏览器 + 使用 AdsPower API v2 + + Args: + user_id: AdsPower用户ID + + Returns: + 是否成功停止 + """ + target_user_id = user_id or self.user_id + if not target_user_id: + logger.error("未设置 AdsPower User ID") + return False + + # 清理 Playwright 资源 + try: + if self.browser: + self.browser.close() + self.browser = None + if self.playwright: + self.playwright.stop() + self.playwright = None + except Exception as e: + logger.warning(f"清理 Playwright 资源异常: {str(e)}") + + try: + # 使用 AdsPower API v2 + url = f"{self.api_url}/api/v2/browser-profile/stop" + + # 准备请求头 + headers = { + 'Content-Type': 'application/json' + } + if self.api_key: + headers['Authorization'] = f'Bearer {self.api_key}' + + # 准备请求体 + payload = { + "profile_id": target_user_id + } + + logger.info("\n" + "="*70) + logger.info("🛑 停止浏览器请求 (API v2)") + logger.info("="*70) + logger.info(f"URL: {url}") + logger.info(f"Method: POST") + logger.info(f"Headers:") + for key, value in headers.items(): + if key == 'Authorization' and 'Bearer' in value: + display_value = f"Bearer {value.split(' ')[1][:8]}..." + else: + display_value = value + logger.info(f" {key}: {display_value}") + logger.info(f"Payload:") + logger.info(f" {json.dumps(payload, indent=2, ensure_ascii=False)}") + + response = requests.post(url, json=payload, headers=headers, timeout=30) + + logger.info("\n" + "-"*70) + logger.info("📥 响应信息") + logger.info("-"*70) + logger.info(f"Status Code: {response.status_code}") + + # 格式化 JSON 响应 + try: + response_json = response.json() + logger.info(f"Response Body:") + logger.info(json.dumps(response_json, indent=2, ensure_ascii=False)) + except: + logger.info(f"Response Body (Raw):") + logger.info(response.text) + + logger.info("="*70 + "\n") + + result = response_json if 'response_json' in locals() else response.json() + + if result.get('code') == 0: + logger.info(f"成功停止浏览器,User ID: {target_user_id}") + return True + else: + logger.error(f"停止浏览器失败: {result.get('msg')}") + return False + + except Exception as e: + logger.error(f"停止浏览器异常: {str(e)}") + return False + + def get_damai_proxy(self) -> Optional[Dict]: + """ + 从大麦IP代理池获取代理 + + Returns: + 包含host和port的字典,失败返回None + """ + try: + logger.info("\n" + "="*70) + logger.info("获取大麦IP代理") + logger.info("="*70) + logger.info(f"URL: {self.DAMAI_API_URL}") + + # 禁用代理,直接连接大麦API + proxies = { + 'http': None, + 'https': None + } + + response = requests.get(self.DAMAI_API_URL, proxies=proxies, timeout=10) + + logger.info(f"Status Code: {response.status_code}") + logger.info(f"Response: {response.text}") + + if response.status_code == 200 and response.text: + # 解析返回的IP:端口格式 + proxy_str = response.text.strip() + if ':' in proxy_str: + host, port = proxy_str.split(':', 1) + logger.success(f"成功获取代理: {host}:{port}") + logger.info("="*70 + "\n") + return { + "host": host, + "port": port + } + else: + logger.error(f"代理格式错误: {proxy_str}") + return None + else: + logger.error(f"获取代理失败: {response.text}") + return None + + except Exception as e: + logger.error(f"获取大麦代理异常: {str(e)}") + return None + + def create_proxy(self, proxy_config: Dict) -> Optional[str]: + """ + 在 AdsPower 中创建代理 + 使用 AdsPower API v2 + + Args: + proxy_config: 代理配置字典 + + Returns: + 代理ID,失败返回None + """ + try: + url = f"{self.api_url}/api/v2/proxy-list/create" + + # 准备请求头 + headers = { + 'Content-Type': 'application/json' + } + if self.api_key: + headers['Authorization'] = f'Bearer {self.api_key}' + + # 准备请求体(数组格式) + payload = [proxy_config] + + logger.info("\n" + "="*70) + logger.info("创建 AdsPower 代理") + logger.info("="*70) + logger.info(f"URL: {url}") + logger.info(f"Method: POST") + logger.info(f"Payload:") + logger.info(json.dumps(payload, indent=2, ensure_ascii=False)) + + response = requests.post(url, json=payload, headers=headers, timeout=30) + + logger.info("\n" + "-"*70) + logger.info("响应信息") + logger.info("-"*70) + logger.info(f"Status Code: {response.status_code}") + + try: + response_json = response.json() + logger.info("Response Body:") + logger.info(json.dumps(response_json, indent=2, ensure_ascii=False)) + except: + logger.info(f"Response Body (Raw):") + logger.info(response.text) + + logger.info("="*70 + "\n") + + result = response_json if 'response_json' in locals() else response.json() + + if result.get('code') == 0: + # 获取创建的代理ID + proxy_data = result.get('data', {}) + proxy_ids = proxy_data.get('proxy_id', []) + if proxy_ids and len(proxy_ids) > 0: + proxy_id = proxy_ids[0] + logger.success(f"成功创建代理,ID: {proxy_id}") + return proxy_id + else: + logger.error("创建代理成功但未返回ID") + return None + else: + logger.error(f"创建代理失败: {result.get('msg')}") + return None + + except Exception as e: + logger.error(f"创建代理异常: {str(e)}") + return None + + def list_proxies(self, proxy_ids: list = None, page: int = 1, limit: int = 100) -> Optional[Dict]: + """ + 查询代理列表 + 使用 AdsPower API v2 + + Args: + proxy_ids: 代理ID列表,可选 + page: 页码,默认1 + limit: 每页数量,默认100 + + Returns: + 代理列表数据 + """ + try: + url = f"{self.api_url}/api/v2/proxy-list/list" + + # 准备请求头 + headers = { + 'Content-Type': 'application/json' + } + if self.api_key: + headers['Authorization'] = f'Bearer {self.api_key}' + + # 准备请求体 + payload = { + "page": str(page), + "limit": str(limit) + } + + if proxy_ids: + payload["proxy_id"] = proxy_ids + + logger.info("\n" + "="*70) + logger.info("查询代理列表 (API v2)") + logger.info("="*70) + logger.info(f"URL: {url}") + logger.info(f"Method: POST") + logger.info(f"Headers:") + for k, v in headers.items(): + if k == 'Authorization' and v: + logger.info(f" {k}: Bearer {v.split()[-1][:8]}...") + else: + logger.info(f" {k}: {v}") + logger.info(f"Payload:") + logger.info(f" {json.dumps(payload, indent=2, ensure_ascii=False)}") + + response = requests.post(url, json=payload, headers=headers, timeout=30) + + logger.info("\n" + "-"*70) + logger.info("响应信息") + logger.info("-"*70) + logger.info(f"Status Code: {response.status_code}") + logger.info("Response Body:") + + try: + response_json = response.json() + logger.info(json.dumps(response_json, indent=2, ensure_ascii=False)) + except: + logger.info(response.text) + + logger.info("="*70 + "\n") + + result = response_json if 'response_json' in locals() else response.json() + + if result.get('code') == 0: + proxy_list = result.get('data', {}).get('list', []) + logger.success(f"查询成功,找到 {len(proxy_list)} 个代理") + return result + else: + logger.error(f"查询代理列表失败: {result.get('msg')}") + return None + + except Exception as e: + logger.error(f"查询代理列表异常: {str(e)}") + return None + + def check_browser_status(self, user_id: str = None) -> Optional[Dict]: + """ + 检查浏览器状态 + + Args: + user_id: AdsPower用户ID + + Returns: + 浏览器状态信息 + """ + target_user_id = user_id or self.user_id + if not target_user_id: + logger.error("未设置 AdsPower User ID") + return None + + try: + url = f"{self.api_url}/api/v1/browser/active?user_id={target_user_id}" + + # 准备请求头 + headers = {} + if self.api_key: + headers['Authorization'] = f'Bearer {self.api_key}' + + response = requests.get(url, headers=headers, timeout=10) + result = response.json() + return result + + except Exception as e: + logger.error(f"检查浏览器状态异常: {str(e)}") + return None + + def list_profiles(self, group_id: str = None, page: int = 1, page_size: int = 100) -> Optional[Dict]: + """ + 查询 Profile 列表 + 使用 AdsPower API v2 + + Args: + group_id: 组ID(可选) + page: 页码 + page_size: 每页数量 + + Returns: + Profile 列表信息 + """ + try: + url = f"{self.api_url}/api/v2/browser-profile/list" + + # 准备请求头 + headers = { + 'Content-Type': 'application/json' + } + if self.api_key: + headers['Authorization'] = f'Bearer {self.api_key}' + + # 准备请求体 + payload = { + "page": page, + "page_size": page_size + } + if group_id: + payload["group_id"] = group_id + + # 打印请求信息 + logger.info("\n" + "="*70) + logger.info("📋 查询 Profile 列表 (API v2)") + logger.info("="*70) + logger.info(f"URL: {url}") + logger.info(f"Method: POST") + logger.info(f"Headers:") + for key, value in headers.items(): + if key == 'Authorization' and 'Bearer' in value: + display_value = f"Bearer {value.split(' ')[1][:8]}..." + else: + display_value = value + logger.info(f" {key}: {display_value}") + logger.info(f"Payload:") + logger.info(f" {json.dumps(payload, indent=2, ensure_ascii=False)}") + + response = requests.post(url, json=payload, headers=headers, timeout=30) + + # 打印响应信息 + logger.info("\n" + "-"*70) + logger.info("📥 响应信息") + logger.info("-"*70) + logger.info(f"Status Code: {response.status_code}") + + # 格式化 JSON 响应(限制长度) + try: + response_json = response.json() + # 如果响应太长,只显示关键部分 + if len(str(response_json)) > 2000: + logger.info(f"Response Body (摘要):") + summary = { + "code": response_json.get("code"), + "msg": response_json.get("msg"), + "data": { + "total": response_json.get("data", {}).get("total"), + "list_count": len(response_json.get("data", {}).get("list", [])) + } + } + logger.info(json.dumps(summary, indent=2, ensure_ascii=False)) + else: + logger.info(f"Response Body:") + logger.info(json.dumps(response_json, indent=2, ensure_ascii=False)) + except: + logger.info(f"Response Body (Raw):") + logger.info(response.text[:500]) + + logger.info("="*70 + "\n") + + result = response_json if 'response_json' in locals() else response.json() + + if result.get('code') == 0: + profiles = result.get('data', {}).get('list', []) + logger.success(f"查询成功,找到 {len(profiles)} 个 Profile") + + # 显示 Profile 信息 + if profiles: + logger.info("\nProfile 列表:") + for idx, profile in enumerate(profiles[:10], 1): # 只显示前10个 + profile_id = profile.get('profile_id', 'N/A') + profile_name = profile.get('name', 'N/A') + group_name = profile.get('group_name', 'N/A') + logger.info(f" {idx}. ID: {profile_id} | 名称: {profile_name} | 组: {group_name}") + + if len(profiles) > 10: + logger.info(f" ...还有 {len(profiles) - 10} 个 Profile") + + return result + else: + logger.error(f"查询 Profile 失败: {result.get('msg')}") + return None + + except Exception as e: + logger.error(f"查询 Profile 异常: {str(e)}") + return None + + def __del__(self): + """析构函数,确保资源清理""" + try: + if self.browser: + self.browser.close() + if self.playwright: + self.playwright.stop() + except: + pass + + +if __name__ == "__main__": + """快速测试 AdsPower 连接""" + logger.info("开始测试 AdsPower API v2 连接") + logger.info("="*50) + + # 创建客户端 + client = AdsPowerClient() + + # 步骤 1: 先查询 Profile 列表 + logger.info("\n步骤 1: 查询 Profile 列表") + result = client.list_profiles() + + if not result: + logger.error("❌ 查询 Profile 失败,测试终止") + exit(1) + + profiles = result.get('data', {}).get('list', []) + if not profiles: + logger.error("❌ 没有可用的 Profile,请先在 AdsPower 中创建 Profile") + exit(1) + + # 使用第一个 Profile + first_profile = profiles[0] + profile_id = first_profile.get('profile_id') + profile_name = first_profile.get('name', 'N/A') + + logger.success(f"\n将使用 Profile: {profile_name} (ID: {profile_id})") + + # 提示用户是否继续 + choice = input(f"\n是否启动此 Profile? (y/n): ").strip().lower() + if choice != 'y': + logger.info("用户取消操作") + exit(0) + + # 问是否使用代理 + use_proxy_choice = input(f"\n是否使用大麦IP代理? (y/n): ").strip().lower() + use_proxy = use_proxy_choice == 'y' + + # 步骤 2: 设置代理并启动浏览器 + logger.info(f"\n步骤 2: {'[使用代理] ' if use_proxy else ''}启动浏览器 {profile_id}") + result = client.setup_proxy_and_start(user_id=profile_id, use_proxy=use_proxy) + + if result: + logger.success("✅ 浏览器启动成功!") + + # 等待用户确认 + input("\n按 Enter 键停止浏览器...") + + # 步骤 3: 停止浏览器 + logger.info(f"\n步骤 3: 停止浏览器 {profile_id}") + if client.stop_browser(user_id=profile_id): + logger.success("✅ 浏览器停止成功!") + else: + logger.error("❌ 浏览器停止失败") + else: + logger.error("❌ 浏览器启动失败") + + logger.info("="*50) + logger.info("测试完成") diff --git a/app.py b/app.py new file mode 100644 index 0000000..41fed35 --- /dev/null +++ b/app.py @@ -0,0 +1,354 @@ +from flask import Flask, request, jsonify, send_from_directory, redirect +from flask_cors import CORS +from loguru import logger +import sys +from pathlib import Path + +from config import Config +from scheduler import ClickScheduler + +# 配置日志 +Config.ensure_dirs() +logger.remove() +logger.add( + sys.stdout, + format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}", + level="INFO" +) +logger.add( + Path(Config.LOG_DIR) / "mip_ad_service_{time}.log", + rotation="500 MB", + retention="10 days", + encoding="utf-8", + level="DEBUG" +) + +# 创建Flask应用 +app = Flask(__name__, static_folder='static', static_url_path='') +CORS(app) + +# 创建调度器实例 +scheduler = ClickScheduler() + + +@app.route('/') +def index(): + """首页 - 重定向到数据概览""" + return redirect('/dashboard.html') + + +@app.route('/health', methods=['GET']) +def health(): + """健康检查""" + return jsonify({'status': 'ok', 'message': '服务运行正常'}) + + +@app.route('/api/urls', methods=['POST']) +def add_urls(): + """添加URL(支持单个或批量)""" + try: + data = request.get_json() + + if not data: + return jsonify({'success': False, 'message': '请求数据为空'}), 400 + + # 支持单个URL或URL列表 + if 'url' in data: + # 单个URL + url = data['url'] + if not url: + return jsonify({'success': False, 'message': 'URL不能为空'}), 400 + + success = scheduler.add_url(url) + if success: + return jsonify({'success': True, 'message': '添加成功'}) + else: + return jsonify({'success': False, 'message': 'URL已存在或添加失败'}), 400 + + elif 'urls' in data: + # 批量URL + urls = data['urls'] + if not isinstance(urls, list) or not urls: + return jsonify({'success': False, 'message': 'URLs必须是非空列表'}), 400 + + count = scheduler.add_urls(urls) + return jsonify({ + 'success': True, + 'message': f'成功添加 {count}/{len(urls)} 个URL', + 'added_count': count, + 'total_count': len(urls) + }) + + else: + return jsonify({'success': False, 'message': '请提供url或urls参数'}), 400 + + except Exception as e: + logger.error(f"添加URL异常: {str(e)}") + return jsonify({'success': False, 'message': f'服务异常: {str(e)}'}), 500 + + +@app.route('/api/urls', methods=['GET']) +def get_urls(): + """获取所有URL列表""" + try: + urls = scheduler.data_manager.get_all_urls() + return jsonify({'success': True, 'data': urls}) + + except Exception as e: + logger.error(f"获取URL列表异常: {str(e)}") + return jsonify({'success': False, 'message': f'服务异常: {str(e)}'}), 500 + + +@app.route('/api/urls/', methods=['GET']) +def get_url_detail(url: str): + """获取URL详细信息""" + try: + url_info = scheduler.get_url_detail(url) + + if url_info: + return jsonify({'success': True, 'data': url_info}) + else: + return jsonify({'success': False, 'message': 'URL不存在'}), 404 + + except Exception as e: + logger.error(f"获取URL详情异常: {str(e)}") + return jsonify({'success': False, 'message': f'服务异常: {str(e)}'}), 500 + + +@app.route('/api/urls/', methods=['DELETE']) +def delete_url(url: str): + """删除URL""" + try: + success = scheduler.data_manager.delete_url(url) + + if success: + return jsonify({'success': True, 'message': '删除成功'}) + else: + return jsonify({'success': False, 'message': 'URL不存在'}), 404 + + except Exception as e: + logger.error(f"删除URL异常: {str(e)}") + return jsonify({'success': False, 'message': f'服务异常: {str(e)}'}), 500 + + +@app.route('/api/urls//reset', methods=['POST']) +def reset_url(url: str): + """重置URL(重新开始点击)""" + try: + success = scheduler.data_manager.reset_url(url) + + if success: + return jsonify({'success': True, 'message': '重置成功'}) + else: + return jsonify({'success': False, 'message': 'URL不存在'}), 404 + + except Exception as e: + logger.error(f"重置URL异常: {str(e)}") + return jsonify({'success': False, 'message': f'服务异常: {str(e)}'}), 500 + + +@app.route('/api/statistics', methods=['GET']) +def get_statistics(): + """获取统计数据""" + try: + stats = scheduler.get_statistics() + return jsonify({'success': True, 'data': stats}) + + except Exception as e: + logger.error(f"获取统计数据异常: {str(e)}") + return jsonify({'success': False, 'message': f'服务异常: {str(e)}'}), 500 + + +@app.route('/api/scheduler/start', methods=['POST']) +def start_scheduler(): + """启动调度器""" + try: + scheduler.start_scheduler() + return jsonify({'success': True, 'message': '调度器已启动'}) + + except Exception as e: + logger.error(f"启动调度器异常: {str(e)}") + return jsonify({'success': False, 'message': f'服务异常: {str(e)}'}), 500 + + +@app.route('/api/scheduler/stop', methods=['POST']) +def stop_scheduler(): + """停止调度器""" + try: + scheduler.stop_scheduler() + return jsonify({'success': True, 'message': '调度器已停止'}) + + except Exception as e: + logger.error(f"停止调度器异常: {str(e)}") + return jsonify({'success': False, 'message': f'服务异常: {str(e)}'}), 500 + + +@app.route('/api/scheduler/status', methods=['GET']) +def get_scheduler_status(): + """获取调度器状态""" + try: + status = 'running' if scheduler.running else 'stopped' + return jsonify({'success': True, 'data': {'status': status}}) + + except Exception as e: + logger.error(f"获取调度器状态异常: {str(e)}") + return jsonify({'success': False, 'message': f'服务异常: {str(e)}'}), 500 + + +# AdsPower 接口调试 +@app.route('/api/adspower/profiles', methods=['GET']) +def adspower_list_profiles(): + """查询Profile列表""" + try: + from adspower_client import AdsPowerClient + client = AdsPowerClient() + result = client.list_profiles() + return jsonify({'success': True, 'data': result}) + except Exception as e: + logger.error(f"AdsPower查询Profile异常: {str(e)}") + return jsonify({'success': False, 'message': str(e)}), 500 + + +@app.route('/api/adspower/browser/start', methods=['POST']) +def adspower_start_browser(): + """启动浏览器""" + try: + from adspower_client import AdsPowerClient + data = request.get_json() or {} + user_id = data.get('user_id') + + client = AdsPowerClient() + result = client.start_browser(user_id=user_id) + return jsonify({'success': True, 'data': result}) + except Exception as e: + logger.error(f"AdsPower启动浏览器异常: {str(e)}") + return jsonify({'success': False, 'message': str(e)}), 500 + + +@app.route('/api/adspower/browser/stop', methods=['POST']) +def adspower_stop_browser(): + """停止浏览器""" + try: + from adspower_client import AdsPowerClient + data = request.get_json() or {} + user_id = data.get('user_id') + + client = AdsPowerClient() + result = client.stop_browser(user_id=user_id) + return jsonify({'success': True, 'data': result}) + except Exception as e: + logger.error(f"AdsPower停止浏览器异常: {str(e)}") + return jsonify({'success': False, 'message': str(e)}), 500 + + +@app.route('/api/adspower/proxy/damai', methods=['GET']) +def adspower_get_damai_proxy(): + """获取大麦IP代理""" + try: + from adspower_client import AdsPowerClient + client = AdsPowerClient() + result = client.get_damai_proxy() + return jsonify({'success': True, 'data': result}) + except Exception as e: + logger.error(f"获取大麦IP异常: {str(e)}") + return jsonify({'success': False, 'message': str(e)}), 500 + + +@app.route('/api/adspower/proxy/create', methods=['POST']) +def adspower_create_proxy(): + """创建代理""" + try: + from adspower_client import AdsPowerClient + data = request.get_json() or {} + proxy_config = data.get('proxy_config') + + if not proxy_config: + return jsonify({'success': False, 'message': '缺少代理配置'}), 400 + + client = AdsPowerClient() + proxy_id = client.create_proxy(proxy_config) + return jsonify({'success': True, 'data': {'proxy_id': proxy_id}}) + except Exception as e: + logger.error(f"AdsPower创建代理异常: {str(e)}") + return jsonify({'success': False, 'message': str(e)}), 500 + + +@app.route('/api/adspower/proxy/list', methods=['GET']) +def adspower_list_proxies(): + """查询代理列表""" + try: + from adspower_client import AdsPowerClient + + page = request.args.get('page', 1, type=int) + limit = request.args.get('limit', 100, type=int) + + client = AdsPowerClient() + result = client.list_proxies(page=page, limit=limit) + + if result is None: + return jsonify({'success': False, 'message': '查询代理列表失败,请检查AdsPower是否运行'}), 500 + + return jsonify({'success': True, 'data': result}) + except Exception as e: + logger.error(f"AdsPower查询代理列表异常: {str(e)}") + return jsonify({'success': False, 'message': str(e)}), 500 + + +@app.route('/api/adspower/profile/update', methods=['POST']) +def adspower_update_profile(): + """更新Profile代理(API v2方式)""" + try: + from adspower_client import AdsPowerClient + data = request.get_json() or {} + profile_id = data.get('profile_id') + proxy_id = data.get('proxy_id') + + if not profile_id or not proxy_id: + return jsonify({'success': False, 'message': '缺少profile_id或proxy_id'}), 400 + + client = AdsPowerClient() + result = client.update_profile_proxy(profile_id, proxy_id) + return jsonify({'success': True, 'data': {'updated': result}}) + except Exception as e: + logger.error(f"AdsPower更新Profile异常: {str(e)}") + return jsonify({'success': False, 'message': str(e)}), 500 + + +@app.route('/api/adspower/profile/update-v1', methods=['POST']) +def adspower_update_profile_v1(): + """更新Profile代理(API v1方式,直接传入proxy_config)""" + try: + from adspower_client import AdsPowerClient + data = request.get_json() or {} + profile_id = data.get('profile_id') + proxy_config = data.get('proxy_config') + + if not profile_id or not proxy_config: + return jsonify({'success': False, 'message': '缺少profile_id或proxy_config'}), 400 + + # 验证必要字段 + if 'proxy_host' not in proxy_config or 'proxy_port' not in proxy_config: + return jsonify({'success': False, 'message': 'proxy_config中缺少proxy_host或proxy_port'}), 400 + + client = AdsPowerClient() + result = client.update_profile_proxy_v1(profile_id, proxy_config) + return jsonify({'success': True, 'data': {'updated': result}}) + except Exception as e: + logger.error(f"AdsPower更新Profile异常 (v1): {str(e)}") + return jsonify({'success': False, 'message': str(e)}), 500 + + +if __name__ == '__main__': + logger.info(f"启动MIP广告点击服务 - 环境: {Config.ENV}") + logger.info(f"服务地址: http://{Config.SERVER_HOST}:{Config.SERVER_PORT}") + logger.info(f"调试模式: {Config.DEBUG}") + + # 自动启动调度器 + scheduler.start_scheduler() + + # 启动Flask应用 + app.run( + host=Config.SERVER_HOST, + port=Config.SERVER_PORT, + debug=Config.DEBUG + ) diff --git a/config.py b/config.py new file mode 100644 index 0000000..282e425 --- /dev/null +++ b/config.py @@ -0,0 +1,81 @@ +import os +from dotenv import load_dotenv + +# 获取运行环境,默认为 development +ENV = os.getenv('ENV', 'development') + +# 根据环境加载对应的配置文件 +if ENV == 'production': + env_file = '.env.production' +else: + env_file = '.env.development' + +# 加载环境配置文件 +from pathlib import Path +env_path = Path(env_file) +if env_path.exists(): + load_dotenv(env_file) + # 始终显示配置加载信息(便于调试) + print(f"[Config] 加载配置文件: {env_path.absolute()}") + api_key = os.getenv('ADSPOWER_API_KEY', '') + if api_key: + print(f"[Config] ADSPOWER_API_KEY: {api_key[:8]}... (长度: {len(api_key)})") + else: + print(f"[Config] ADSPOWER_API_KEY: 未设置") +else: + print(f"[Config] 警告: 配置文件不存在 {env_path.absolute()}") + load_dotenv() # 尝试加载默认的 .env + + +class BaseConfig: + """基础配置类""" + + # 环境标识 + ENV = ENV + + # AdsPower配置 + ADSPOWER_API_URL = os.getenv('ADSPOWER_API_URL', 'http://local.adspower.net:50325') + ADSPOWER_USER_ID = os.getenv('ADSPOWER_USER_ID', '') + ADSPOWER_API_KEY = os.getenv('ADSPOWER_API_KEY', '') # 可选,某些版本需要 + + # 服务配置 + SERVER_HOST = os.getenv('SERVER_HOST', '0.0.0.0') + SERVER_PORT = int(os.getenv('SERVER_PORT', 5000)) + + # 点击策略配置 + MIN_CLICK_COUNT = int(os.getenv('MIN_CLICK_COUNT', 1)) + MAX_CLICK_COUNT = int(os.getenv('MAX_CLICK_COUNT', 10)) + CLICK_INTERVAL_MINUTES = int(os.getenv('CLICK_INTERVAL_MINUTES', 30)) + WORK_START_HOUR = int(os.getenv('WORK_START_HOUR', 9)) + WORK_END_HOUR = int(os.getenv('WORK_END_HOUR', 21)) + REPLY_WAIT_TIMEOUT = int(os.getenv('REPLY_WAIT_TIMEOUT', 30)) + + # 数据存储路径 + DATA_DIR = os.getenv('DATA_DIR', './data') + LOG_DIR = os.getenv('LOG_DIR', './logs') + + # 调试模式 + DEBUG = os.getenv('DEBUG', 'False').lower() == 'true' + + @classmethod + def ensure_dirs(cls): + """确保必要的目录存在""" + os.makedirs(cls.DATA_DIR, exist_ok=True) + os.makedirs(cls.LOG_DIR, exist_ok=True) + + +class DevelopmentConfig(BaseConfig): + """开发环境配置""" + DEBUG = True + + +class ProductionConfig(BaseConfig): + """生产环境配置""" + DEBUG = False + + +# 根据环境选择配置 +if ENV == 'production': + Config = ProductionConfig +else: + Config = DevelopmentConfig diff --git a/data_manager.py b/data_manager.py new file mode 100644 index 0000000..5b0c0b7 --- /dev/null +++ b/data_manager.py @@ -0,0 +1,280 @@ +import json +import random +from datetime import datetime +from typing import List, Dict, Optional +from pathlib import Path +from loguru import logger +from config import Config + + +class DataManager: + """数据管理器,负责URL和统计数据的存储与管理""" + + def __init__(self, data_file: str = None): + self.data_file = data_file or str(Path(Config.DATA_DIR) / 'urls_data.json') + self._ensure_data_file() + self.data = self._load_data() + + def _ensure_data_file(self): + """确保数据文件存在""" + Config.ensure_dirs() + if not Path(self.data_file).exists(): + self._save_data({'urls': {}}) + + def _load_data(self) -> Dict: + """加载数据""" + try: + with open(self.data_file, 'r', encoding='utf-8') as f: + return json.load(f) + except Exception as e: + logger.error(f"加载数据文件失败: {str(e)}") + return {'urls': {}} + + def _save_data(self, data: Dict = None): + """保存数据""" + try: + save_data = data if data is not None else self.data + with open(self.data_file, 'w', encoding='utf-8') as f: + json.dump(save_data, f, ensure_ascii=False, indent=2) + except Exception as e: + logger.error(f"保存数据文件失败: {str(e)}") + + def add_url(self, url: str) -> bool: + """ + 添加新URL + + Args: + url: MIP页面链接 + + Returns: + 是否添加成功 + """ + try: + if url in self.data['urls']: + logger.warning(f"URL已存在: {url}") + return False + + # 生成随机目标点击次数 + target_clicks = random.randint(Config.MIN_CLICK_COUNT, Config.MAX_CLICK_COUNT) + + self.data['urls'][url] = { + 'url': url, + 'status': 'active', # active, completed, failed + 'target_clicks': target_clicks, + 'click_count': 0, + 'reply_count': 0, + 'created_time': datetime.now().isoformat(), + 'last_click_time': None, + 'click_history': [] + } + + self._save_data() + logger.info(f"成功添加URL,目标点击次数: {target_clicks}") + return True + + except Exception as e: + logger.error(f"添加URL失败: {str(e)}") + return False + + def record_click(self, url: str, has_reply: bool = False): + """ + 记录一次点击 + + Args: + url: URL + has_reply: 是否获得回复 + """ + try: + if url not in self.data['urls']: + logger.error(f"URL不存在: {url}") + return + + url_data = self.data['urls'][url] + url_data['click_count'] += 1 + url_data['last_click_time'] = datetime.now().isoformat() + + if has_reply: + url_data['reply_count'] += 1 + + # 记录点击历史 + url_data['click_history'].append({ + 'time': datetime.now().isoformat(), + 'has_reply': has_reply + }) + + self._save_data() + logger.info(f"记录点击成功,总点击次数: {url_data['click_count']}/{url_data['target_clicks']}") + + except Exception as e: + logger.error(f"记录点击失败: {str(e)}") + + def mark_url_completed(self, url: str): + """ + 标记URL为已完成 + + Args: + url: URL + """ + try: + if url not in self.data['urls']: + logger.error(f"URL不存在: {url}") + return + + self.data['urls'][url]['status'] = 'completed' + self.data['urls'][url]['completed_time'] = datetime.now().isoformat() + self._save_data() + + logger.info(f"URL已完成: {url}") + + except Exception as e: + logger.error(f"标记URL完成失败: {str(e)}") + + def mark_url_failed(self, url: str, reason: str = ""): + """ + 标记URL为失败 + + Args: + url: URL + reason: 失败原因 + """ + try: + if url not in self.data['urls']: + logger.error(f"URL不存在: {url}") + return + + self.data['urls'][url]['status'] = 'failed' + self.data['urls'][url]['failed_reason'] = reason + self.data['urls'][url]['failed_time'] = datetime.now().isoformat() + self._save_data() + + logger.warning(f"URL标记为失败: {url}, 原因: {reason}") + + except Exception as e: + logger.error(f"标记URL失败状态失败: {str(e)}") + + def get_active_urls(self) -> List[Dict]: + """ + 获取所有活跃的URL + + Returns: + 活跃URL列表 + """ + try: + active_urls = [ + data for data in self.data['urls'].values() + if data['status'] == 'active' + ] + return active_urls + except Exception as e: + logger.error(f"获取活跃URL失败: {str(e)}") + return [] + + def get_url_info(self, url: str) -> Optional[Dict]: + """ + 获取URL详细信息 + + Args: + url: URL + + Returns: + URL信息 + """ + return self.data['urls'].get(url) + + def get_all_urls(self) -> List[Dict]: + """ + 获取所有URL + + Returns: + 所有URL列表 + """ + return list(self.data['urls'].values()) + + def get_statistics(self) -> Dict: + """ + 获取统计数据 + + Returns: + 统计数据 + """ + try: + total_urls = len(self.data['urls']) + active_urls = sum(1 for data in self.data['urls'].values() if data['status'] == 'active') + completed_urls = sum(1 for data in self.data['urls'].values() if data['status'] == 'completed') + failed_urls = sum(1 for data in self.data['urls'].values() if data['status'] == 'failed') + + total_clicks = sum(data['click_count'] for data in self.data['urls'].values()) + total_replies = sum(data['reply_count'] for data in self.data['urls'].values()) + + stats = { + 'total_urls': total_urls, + 'active_urls': active_urls, + 'completed_urls': completed_urls, + 'failed_urls': failed_urls, + 'total_clicks': total_clicks, + 'total_replies': total_replies, + 'reply_rate': f"{(total_replies / total_clicks * 100) if total_clicks > 0 else 0:.2f}%" + } + + return stats + + except Exception as e: + logger.error(f"获取统计数据失败: {str(e)}") + return {} + + def delete_url(self, url: str) -> bool: + """ + 删除URL + + Args: + url: URL + + Returns: + 是否删除成功 + """ + try: + if url in self.data['urls']: + del self.data['urls'][url] + self._save_data() + logger.info(f"已删除URL: {url}") + return True + else: + logger.warning(f"URL不存在: {url}") + return False + except Exception as e: + logger.error(f"删除URL失败: {str(e)}") + return False + + def reset_url(self, url: str) -> bool: + """ + 重置URL状态(重新开始点击) + + Args: + url: URL + + Returns: + 是否重置成功 + """ + try: + if url not in self.data['urls']: + logger.warning(f"URL不存在: {url}") + return False + + # 生成新的随机目标点击次数 + target_clicks = random.randint(Config.MIN_CLICK_COUNT, Config.MAX_CLICK_COUNT) + + self.data['urls'][url]['status'] = 'active' + self.data['urls'][url]['target_clicks'] = target_clicks + self.data['urls'][url]['click_count'] = 0 + self.data['urls'][url]['reply_count'] = 0 + self.data['urls'][url]['last_click_time'] = None + self.data['urls'][url]['click_history'] = [] + self.data['urls'][url]['reset_time'] = datetime.now().isoformat() + + self._save_data() + logger.info(f"已重置URL: {url}") + return True + + except Exception as e: + logger.error(f"重置URL失败: {str(e)}") + return False diff --git a/example_usage.py b/example_usage.py new file mode 100644 index 0000000..dc8c03a --- /dev/null +++ b/example_usage.py @@ -0,0 +1,165 @@ +""" +MIP广告点击服务使用示例 +展示如何通过 API 与服务交互 +""" + +import requests +import json +from typing import List + + +class MIPAdClient: + """MIP广告点击服务客户端""" + + def __init__(self, base_url: str = "http://localhost:5000"): + self.base_url = base_url + + def check_health(self): + """检查服务健康状态""" + response = requests.get(f"{self.base_url}/health") + return response.json() + + def add_url(self, url: str): + """添加单个URL""" + response = requests.post( + f"{self.base_url}/api/urls", + json={"url": url} + ) + return response.json() + + def add_urls_batch(self, urls: List[str]): + """批量添加URL""" + response = requests.post( + f"{self.base_url}/api/urls", + json={"urls": urls} + ) + return response.json() + + def get_all_urls(self): + """获取所有URL列表""" + response = requests.get(f"{self.base_url}/api/urls") + return response.json() + + def get_url_detail(self, url: str): + """获取URL详细信息""" + response = requests.get(f"{self.base_url}/api/urls/{url}") + return response.json() + + def delete_url(self, url: str): + """删除URL""" + response = requests.delete(f"{self.base_url}/api/urls/{url}") + return response.json() + + def reset_url(self, url: str): + """重置URL(重新开始点击)""" + response = requests.post(f"{self.base_url}/api/urls/{url}/reset") + return response.json() + + def get_statistics(self): + """获取统计数据""" + response = requests.get(f"{self.base_url}/api/statistics") + return response.json() + + def start_scheduler(self): + """启动调度器""" + response = requests.post(f"{self.base_url}/api/scheduler/start") + return response.json() + + def stop_scheduler(self): + """停止调度器""" + response = requests.post(f"{self.base_url}/api/scheduler/stop") + return response.json() + + def get_scheduler_status(self): + """获取调度器状态""" + response = requests.get(f"{self.base_url}/api/scheduler/status") + return response.json() + + +def example_usage(): + """使用示例""" + + # 创建客户端 + client = MIPAdClient() + + print("=" * 60) + print("MIP广告点击服务使用示例") + print("=" * 60) + + # 1. 检查服务状态 + print("\n1. 检查服务健康状态") + health = client.check_health() + print(f"服务状态: {health}") + + # 2. 添加单个URL + print("\n2. 添加单个URL") + test_url = "https://example.com/mip-page-1" + result = client.add_url(test_url) + print(f"添加结果: {result}") + + # 3. 批量添加URL + print("\n3. 批量添加URL") + test_urls = [ + "https://example.com/mip-page-2", + "https://example.com/mip-page-3", + "https://example.com/mip-page-4" + ] + result = client.add_urls_batch(test_urls) + print(f"批量添加结果: {result}") + + # 4. 获取所有URL + print("\n4. 获取所有URL列表") + urls = client.get_all_urls() + print(f"URL总数: {len(urls.get('data', []))}") + if urls.get('data'): + print("URL列表:") + for url_data in urls['data'][:3]: # 只显示前3个 + print(f" - {url_data['url']}") + print(f" 状态: {url_data['status']}") + print(f" 点击进度: {url_data['click_count']}/{url_data['target_clicks']}") + print(f" 回复数: {url_data['reply_count']}") + + # 5. 获取URL详细信息 + print("\n5. 获取URL详细信息") + detail = client.get_url_detail(test_url) + if detail.get('success'): + print(f"URL详情: {json.dumps(detail['data'], indent=2, ensure_ascii=False)}") + + # 6. 获取统计数据 + print("\n6. 获取统计数据") + stats = client.get_statistics() + if stats.get('success'): + print("统计信息:") + for key, value in stats['data'].items(): + print(f" {key}: {value}") + + # 7. 查看调度器状态 + print("\n7. 查看调度器状态") + status = client.get_scheduler_status() + print(f"调度器状态: {status}") + + # 8. 重置URL(如果需要) + print("\n8. 重置URL示例") + # reset_result = client.reset_url(test_url) + # print(f"重置结果: {reset_result}") + print("(跳过实际重置操作)") + + # 9. 删除URL(如果需要) + print("\n9. 删除URL示例") + # delete_result = client.delete_url(test_url) + # print(f"删除结果: {delete_result}") + print("(跳过实际删除操作)") + + print("\n" + "=" * 60) + print("示例完成") + print("=" * 60) + + +if __name__ == "__main__": + try: + example_usage() + except requests.exceptions.ConnectionError: + print("错误: 无法连接到服务,请确保服务已启动") + print("启动命令: python app.py") + except Exception as e: + print(f"错误: {str(e)}") diff --git a/fingerprint_browser.py b/fingerprint_browser.py new file mode 100644 index 0000000..357088c --- /dev/null +++ b/fingerprint_browser.py @@ -0,0 +1,649 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +指纹浏览器管理模块 +支持 AdsPower 指纹浏览器 + Playwright CDP 连接 +用于绕过小红书风控检测 +""" + +import requests +import time +import random +import logging +import asyncio +import os +from typing import Dict, Any, Optional, Tuple +from playwright.async_api import async_playwright, Browser, Page, BrowserContext + +# 创建不使用代理的 Session(用于本地 AdsPower API) +def get_no_proxy_session(): + """获取不使用代理的 requests Session""" + session = requests.Session() + session.trust_env = False # 禁用环境变量中的代理 + return session + +# 全局无代理 Session +_no_proxy_session = None + +def get_local_session(): + """获取本地API调用专用Session(无代理)""" + global _no_proxy_session + if _no_proxy_session is None: + _no_proxy_session = get_no_proxy_session() + return _no_proxy_session + +from log_config import setup_logger +from damai_proxy_config import get_proxy_ip + +# 初始化日志系统 - 统一使用xhs_server日志 +logger = setup_logger( + name='xhs_server', + log_file='logs/xhs_server.log', + error_log_file='logs/xhs_server_error.log', + level=logging.INFO, + backup_count=30, + error_backup_count=90, + console_output=True +) + +# AdsPower 本地API配置 +ADSPOWER_CONFIG = { + 'api_base': 'http://127.0.0.1:50325', # AdsPower 本地API地址 + 'enabled': True, # 是否启用指纹浏览器 + 'default_group_id': '0', # 默认分组ID + 'api_key': 'e5afd5a4cead5589247febbeabc39bcb', # AdsPower API Key +} + + +class FingerprintBrowserManager: + """ + 指纹浏览器管理器 + 支持 AdsPower 指纹浏览器的启动、连接和管理 + """ + + def __init__(self): + self.api_base = ADSPOWER_CONFIG['api_base'] + self.api_key = ADSPOWER_CONFIG.get('api_key', '') + self.enabled = ADSPOWER_CONFIG['enabled'] + self.current_browser = None + self.current_context = None + self.current_page = None + self.current_profile_id = None + self.playwright = None + + def _get_headers(self): + """获取API请求头(使用Bearer Token认证)""" + headers = {'Content-Type': 'application/json'} + if self.api_key: + headers['Authorization'] = f'Bearer {self.api_key}' + return headers + + def _add_api_key(self, params: dict) -> dict: + """添加API Key到请求参数(备用方法)""" + # 现在主要使用 Authorization header,这个方法作为备用 + return params + + async def check_adspower_status(self) -> bool: + """ + 检查 AdsPower 是否运行中 + + Returns: + bool: AdsPower 是否可用 + """ + try: + session = get_local_session() + response = session.get(f"{self.api_base}/status", timeout=5) + if response.status_code == 200: + data = response.json() + if data.get('code') == 0: + logger.info("[指纹浏览器] AdsPower 状态正常") + return True + logger.warning(f"[指纹浏览器] AdsPower 状态异常: {response.text}") + return False + except requests.exceptions.ConnectionError: + logger.warning("[指纹浏览器] AdsPower 未运行,请先启动 AdsPower") + return False + except Exception as e: + logger.error(f"[指纹浏览器] 检查 AdsPower 状态失败: {e}") + return False + + async def get_browser_profiles(self) -> list: + """ + 获取所有浏览器配置文件列表 + + Returns: + list: 配置文件列表 + """ + try: + session = get_local_session() + response = session.get( + f"{self.api_base}/api/v1/user/list", + params={'page_size': 100}, + headers=self._get_headers(), + timeout=10 + ) + if response.status_code == 200: + data = response.json() + if data.get('code') == 0: + profiles = data.get('data', {}).get('list', []) + logger.info(f"[指纹浏览器] 获取到 {len(profiles)} 个浏览器配置") + return profiles + logger.warning(f"[指纹浏览器] 获取配置列表失败: {response.text}") + return [] + except Exception as e: + logger.error(f"[指纹浏览器] 获取配置列表异常: {e}") + return [] + + async def query_profile_proxy(self, profile_id: str) -> dict: + """ + 查询指定配置的代理信息 + + Args: + profile_id: 配置文件ID + + Returns: + dict: 代理配置信息 + """ + try: + session = get_local_session() + response = session.get( + f"{self.api_base}/api/v1/user/list", + params={'user_id': profile_id}, + headers=self._get_headers(), + timeout=10 + ) + if response.status_code == 200: + data = response.json() + if data.get('code') == 0: + profiles = data.get('data', {}).get('list', []) + if profiles: + profile = profiles[0] + proxy_config = profile.get('user_proxy_config', {}) + logger.info(f"[指纹浏览器] 查询到配置 {profile_id} 的代理: {proxy_config}") + return proxy_config + return {} + except Exception as e: + logger.error(f"[指纹浏览器] 查询配置代理异常: {e}") + return {} + + async def create_browser_profile(self, name: str = None, proxy_config: dict = None) -> Optional[str]: + """ + 创建新的浏览器配置文件 + + Args: + name: 配置文件名称 + proxy_config: 代理配置 {'server': 'http://ip:port', 'username': '...', 'password': '...'} + + Returns: + str: 配置文件ID,失败返回None + """ + try: + if not name: + name = f"xhs_profile_{int(time.time())}" + + # 构建创建参数 + create_params = { + 'name': name, + 'group_id': ADSPOWER_CONFIG['default_group_id'], + 'fingerprint_config': { + 'automatic_timezone': '1', + 'language': ['zh-CN', 'zh'], + 'ua': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' + } + } + + # 如果有代理配置,添加代理 + if proxy_config: + # 解析代理服务器地址 + server = proxy_config.get('server', '') + if server.startswith('http://'): + server = server[7:] + parts = server.split(':') + if len(parts) == 2: + create_params['user_proxy_config'] = { + 'proxy_soft': 'other', + 'proxy_type': 'http', + 'proxy_host': parts[0], + 'proxy_port': parts[1], + 'proxy_user': proxy_config.get('username', ''), + 'proxy_password': proxy_config.get('password', '') + } + logger.info(f"[指纹浏览器] 配置代理: {parts[0]}:{parts[1]}") + + session = get_local_session() + response = session.post( + f"{self.api_base}/api/v1/user/create", + json=create_params, + headers=self._get_headers(), + timeout=30 + ) + + if response.status_code == 200: + data = response.json() + if data.get('code') == 0: + profile_id = data.get('data', {}).get('id') + logger.info(f"[指纹浏览器] 创建配置成功: {profile_id}") + return profile_id + + logger.warning(f"[指纹浏览器] 创建配置失败: {response.text}") + return None + except Exception as e: + logger.error(f"[指纹浏览器] 创建配置异常: {e}") + return None + + async def update_browser_proxy(self, profile_id: str, proxy_config: dict) -> bool: + """ + 更新指定配置的代理IP(使用AdsPower API动态更新) + + Args: + profile_id: 配置文件ID + proxy_config: 代理配置 {'server': 'http://ip:port', 'username': '...', 'password': '...'} + + Returns: + bool: 是否更新成功 + """ + try: + if not proxy_config: + logger.warning("[指纹浏览器] 没有代理配置,跳过更新") + return False + + # 解析代理服务器地址 + server = proxy_config.get('server', '') + if server.startswith('http://'): + server = server[7:] + elif server.startswith('https://'): + server = server[8:] + + parts = server.split(':') + if len(parts) != 2: + logger.warning(f"[指纹浏览器] 代理地址格式错误: {server}") + return False + + proxy_host = parts[0] + proxy_port = int(parts[1]) # 端口必须是整数 + + logger.info(f"[指纹浏览器] 更新代理配置: {proxy_host}:{proxy_port}") + + session = get_local_session() + + # 注意:不再清除旧代理配置,直接覆盖更新 + # 因为清除后再设置可能导致配置不一致 + + # 第二步:设置新的代理配置 + # 检查是否有认证信息(白名单模式不需要认证) + proxy_user = proxy_config.get('username', '') + proxy_password = proxy_config.get('password', '') + + user_proxy_config = { + 'proxy_soft': 'other', + 'proxy_type': 'http', + 'proxy_host': proxy_host, + 'proxy_port': proxy_port + } + + # 只有在有认证信息时才添加用户名密码 + if proxy_user and proxy_password: + user_proxy_config['proxy_user'] = proxy_user + user_proxy_config['proxy_password'] = proxy_password + logger.info(f"[指纹浏览器] 使用认证代理: {proxy_host}:{proxy_port}") + else: + logger.info(f"[指纹浏览器] 使用白名单代理(无认证): {proxy_host}:{proxy_port}") + + update_params = { + 'user_id': profile_id, + 'user_proxy_config': user_proxy_config + } + + # 打印完整的请求参数用于调试 + import json as json_module + logger.info(f"[指纹浏览器] 发送更新请求: {json_module.dumps(update_params, ensure_ascii=False)}") + + response = session.post( + f"{self.api_base}/api/v1/user/update", + json=update_params, + headers=self._get_headers(), + timeout=30 + ) + + # 打印完整的响应用于调试 + logger.info(f"[指纹浏览器] API响应: {response.text}") + + if response.status_code == 200: + data = response.json() + if data.get('code') == 0: + logger.info(f"[指纹浏览器] 代理配置API返回成功: {profile_id}") + + # 验证代理是否真正写入 + await asyncio.sleep(0.5) # 等待配置生效 + verify_config = await self.query_profile_proxy(profile_id) + actual_host = verify_config.get('proxy_host', '') + actual_port = verify_config.get('proxy_port', '') + + if actual_host == proxy_host and str(actual_port) == str(proxy_port): + logger.info(f"[指纹浏览器] ✅ 代理配置验证通过: {actual_host}:{actual_port}") + return True + else: + logger.warning(f"[指纹浏览器] ❌ 代理配置验证失败! 期望: {proxy_host}:{proxy_port}, 实际: {actual_host}:{actual_port}") + logger.warning(f"[指纹浏览器] 完整配置: {verify_config}") + return False + else: + logger.warning(f"[指纹浏览器] 更新代理失败: {data.get('msg', '未知错误')}") + return False + + logger.warning(f"[指纹浏览器] 更新代理请求失败: {response.text}") + return False + except Exception as e: + logger.error(f"[指纹浏览器] 更新代理异常: {e}") + return False + + async def start_browser(self, profile_id: str, proxy_config: dict = None) -> Optional[str]: + """ + 启动指定配置的浏览器,返回 CDP 调试地址 + + Args: + profile_id: 配置文件ID + proxy_config: 可选的代理配置,在启动前更新 + + Returns: + str: CDP WebSocket URL,失败返回None + """ + try: + # 先停止可能正在运行的旧浏览器,避免状态混乱 + logger.info(f"[指纹浏览器] 先停止可能存在的旧浏览器: {profile_id}") + await self.stop_browser(profile_id) + await asyncio.sleep(1) # 等待浏览器完全关闭 + + # 如果有代理配置,在启动前更新代理(关键:必须在stop之后、start之前) + if proxy_config: + logger.info(f"[指纹浏览器] 启动前更新代理配置...") + await self.update_browser_proxy(profile_id, proxy_config) + await asyncio.sleep(0.5) # 等待配置生效 + + logger.info(f"[指纹浏览器] 正在启动浏览器: {profile_id}") + + session = get_local_session() + response = session.get( + f"{self.api_base}/api/v1/browser/start", + params={'user_id': profile_id}, + headers=self._get_headers(), + timeout=60 + ) + + if response.status_code == 200: + data = response.json() + if data.get('code') == 0: + ws_url = data.get('data', {}).get('ws', {}).get('puppeteer') + if ws_url: + logger.info(f"[指纹浏览器] 浏览器启动成功,CDP地址: {ws_url}") + self.current_profile_id = profile_id + return ws_url + + logger.warning(f"[指纹浏览器] 启动浏览器失败: {response.text}") + return None + except Exception as e: + logger.error(f"[指纹浏览器] 启动浏览器异常: {e}") + return None + + async def stop_browser(self, profile_id: str = None) -> bool: + """ + 停止指定配置的浏览器 + + Args: + profile_id: 配置文件ID,不传则使用当前配置 + + Returns: + bool: 是否成功 + """ + try: + pid = profile_id or self.current_profile_id + if not pid: + logger.warning("[指纹浏览器] 没有需要停止的浏览器") + return False + + session = get_local_session() + response = session.get( + f"{self.api_base}/api/v1/browser/stop", + params={'user_id': pid}, + headers=self._get_headers(), + timeout=10 + ) + + if response.status_code == 200: + data = response.json() + if data.get('code') == 0: + logger.info(f"[指纹浏览器] 浏览器已停止: {pid}") + if pid == self.current_profile_id: + self.current_profile_id = None + return True + + logger.warning(f"[指纹浏览器] 停止浏览器失败: {response.text}") + return False + except Exception as e: + logger.error(f"[指纹浏览器] 停止浏览器异常: {e}") + return False + + async def connect_browser(self, cdp_url: str) -> Tuple[Optional[Browser], Optional[BrowserContext], Optional[Page]]: + """ + 通过 CDP 连接到指纹浏览器 + + Args: + cdp_url: CDP WebSocket URL + + Returns: + Tuple[Browser, BrowserContext, Page]: 浏览器、上下文、页面对象 + """ + try: + logger.info(f"[指纹浏览器] 正在连接 CDP: {cdp_url}") + + self.playwright = await async_playwright().start() + + # 连接到指纹浏览器 + browser = await self.playwright.chromium.connect_over_cdp(cdp_url) + + # 获取上下文(指纹浏览器通常只有一个context) + contexts = browser.contexts + if contexts: + context = contexts[0] + else: + context = await browser.new_context() + + # 获取或创建页面 + pages = context.pages + if pages: + page = pages[0] + else: + page = await context.new_page() + + self.current_browser = browser + self.current_context = context + self.current_page = page + + logger.info("[指纹浏览器] CDP 连接成功") + return browser, context, page + + except Exception as e: + logger.error(f"[指纹浏览器] CDP 连接失败: {e}") + return None, None, None + + async def disconnect(self): + """断开浏览器连接(不关闭浏览器)""" + try: + if self.current_browser: + # 注意:connect_over_cdp 模式下不要 close,只 disconnect + await self.current_browser.close() + self.current_browser = None + self.current_context = None + self.current_page = None + logger.info("[指纹浏览器] 已断开连接") + + if self.playwright: + await self.playwright.stop() + self.playwright = None + except Exception as e: + logger.error(f"[指纹浏览器] 断开连接异常: {e}") + + async def get_all_profiles(self) -> list: + """ + 获取所有可用的浏览器配置文件,随机排序 + + Returns: + list: 配置文件ID列表 + """ + profiles = await self.get_browser_profiles() + profile_ids = [] + + for profile in profiles: + user_id = profile.get('user_id', '') + name = profile.get('name', '') + if user_id: + profile_ids.append({'id': user_id, 'name': name}) + + # 随机打乱顺序,支持多配置轮换使用 + random.shuffle(profile_ids) + logger.info(f"[指纹浏览器] 获取到 {len(profile_ids)} 个配置,已随机排序") + return profile_ids + + async def get_or_create_profile(self, proxy_config: dict = None) -> Optional[str]: + """ + 获取或创建浏览器配置文件 + 优先使用已有的配置,没有则创建新的 + + Args: + proxy_config: 代理配置 + + Returns: + str: 配置文件ID + """ + # 获取所有配置(已随机排序) + profiles = await self.get_all_profiles() + + if profiles: + # 返回第一个(随机选择的) + profile = profiles[0] + logger.info(f"[指纹浏览器] 随机选择配置: {profile['id']} ({profile['name']})") + return profile['id'] + + # 没有可用配置,创建新的 + logger.info("[指纹浏览器] 没有可用配置,创建新配置...") + return await self.create_browser_profile(proxy_config=None) + + +async def human_type(page: Page, selector: str, text: str, clear_first: bool = True): + """ + 模拟人类打字速度输入文本 + + Args: + page: Playwright Page 对象 + selector: 输入框选择器 + text: 要输入的文本 + clear_first: 是否先清空输入框 + """ + try: + # 聚焦输入框 + await page.focus(selector) + + # 先清空 + if clear_first: + await page.fill(selector, '') + await asyncio.sleep(random.uniform(0.1, 0.3)) + + # 模拟人类打字 + for char in text: + await page.keyboard.type(char) + # 随机延迟 50ms - 150ms + await asyncio.sleep(random.uniform(0.05, 0.15)) + + logger.info(f"[人类输入] 已输入 {len(text)} 个字符") + except Exception as e: + logger.error(f"[人类输入] 输入失败: {e}") + raise + + +async def human_click(page: Page, selector: str, wait_after: float = 0.5): + """ + 模拟人类点击行为 + + Args: + page: Playwright Page 对象 + selector: 元素选择器 + wait_after: 点击后等待时间 + """ + try: + # 先移动到元素位置 + element = await page.query_selector(selector) + if element: + box = await element.bounding_box() + if box: + # 在元素范围内随机一个点击位置 + x = box['x'] + random.uniform(box['width'] * 0.3, box['width'] * 0.7) + y = box['y'] + random.uniform(box['height'] * 0.3, box['height'] * 0.7) + + # 移动鼠标 + await page.mouse.move(x, y) + await asyncio.sleep(random.uniform(0.1, 0.3)) + + # 点击 + await page.mouse.click(x, y) + logger.info(f"[人类点击] 点击位置: ({x:.0f}, {y:.0f})") + else: + await page.click(selector) + else: + await page.click(selector) + + await asyncio.sleep(wait_after) + except Exception as e: + logger.error(f"[人类点击] 点击失败: {e}") + raise + + +# 全局单例 +_fingerprint_manager = None + +def get_fingerprint_manager() -> FingerprintBrowserManager: + """获取指纹浏览器管理器单例""" + global _fingerprint_manager + if _fingerprint_manager is None: + _fingerprint_manager = FingerprintBrowserManager() + return _fingerprint_manager + + +if __name__ == "__main__": + # 测试代码 + async def test(): + manager = get_fingerprint_manager() + + # 检查 AdsPower 状态 + if await manager.check_adspower_status(): + print("AdsPower 运行正常") + + # 获取代理IP + proxy = get_proxy_ip() + print(f"代理IP: {proxy}") + + # 获取或创建配置 + profile_id = await manager.get_or_create_profile(proxy_config=proxy) + if profile_id: + print(f"配置ID: {profile_id}") + + # 启动浏览器 + cdp_url = await manager.start_browser(profile_id) + if cdp_url: + print(f"CDP URL: {cdp_url}") + + # 连接浏览器 + browser, context, page = await manager.connect_browser(cdp_url) + if page: + # 访问测试页面 + await page.goto("https://httpbin.org/ip") + content = await page.content() + print(f"页面内容: {content[:200]}") + + # 断开连接 + await manager.disconnect() + + # 停止浏览器 + await manager.stop_browser(profile_id) + else: + print("AdsPower 未运行") + + asyncio.run(test()) diff --git a/localAPI-main/README.md b/localAPI-main/README.md new file mode 100644 index 0000000..cec4c8b --- /dev/null +++ b/localAPI-main/README.md @@ -0,0 +1,44 @@ +# localAPI +AdsPower supports Local API, which has functions like reading and writing account configuration information, opening and closing browsers, searching for accounts. Besides, it can cooperate with Selenium and Puppeteer to execute browser operations automatically. + +
+ +> ## How to Use AdsPower Local API + + - Users of AdsPower team collaboration version have access to API + - Start AdsPower, log in the account with API permission + - Go to Account Management-> Setting-> Local API to check the following items + - API status: Success + - API address: http://local.adspower.net:50325/ or http://localhost:50325/ (port: 50325, which might change and subjects to the address in the setting). + - Script can go to Profile Management-> click Settings-> click Cache folder-> local_api file to obtain API address + - Use script or http request tool to invoke Local API, allow to configure account data, browser fingerprint, open or close browser and other operations + - API parameter type: string, Post format: JSON, unnecessary parameters are optional and can not be passed + - Access frequency control for all APIs, max. access frequency: 1 request/second + - At the same time, it supports the mode of no-interface api-key to start the Local API service. For details, see: [Local Api Doc](https://localapi-doc-en.adspower.com/) + +
+ +> ## **What the Local API supports** + +- [x] API Status +- [x] Browser Operation + - [x] Open Browser + - [x] Close Browser + - [x] Check Open Status +- [x] Group Management + - [x] Create Group + - [ ] Update Group(coming soon) + - [x] Query Group +- [x] Profile Management + - [x] Create Profile + - [x] Update Profile + - [x] Query Profile + - [x] Delete Profile + - [x] Update Profile Group + - [x] Delete Profile Cache + +
+ +## More Details + +👉[Local Api Doc](https://localapi-doc-en.adspower.com/) diff --git a/localAPI-main/countryCode.json b/localAPI-main/countryCode.json new file mode 100644 index 0000000..0098006 --- /dev/null +++ b/localAPI-main/countryCode.json @@ -0,0 +1,249 @@ +{ + "ad": "Andorra", + "ae": "United Arab Emirates", + "af": "Afghanistan", + "ag": "Antigua & Barbuda", + "ai": "Anguilla", + "al": "Albania", + "am": "Armenia", + "ao": "Angola", + "aq": "Antarctica", + "ar": "Argentina", + "as": "American Samoa", + "at": "Austria", + "au": "Australia", + "aw": "Aruba", + "ax": "_land Islands", + "az": "Azerbaijan", + "ba": "Bosnia & Herzegovina", + "bb": "Barbados", + "bd": "Bangladesh", + "be": "Belgium", + "bf": "Burkina", + "bg": "Bulgaria", + "bh": "Bahrain", + "bi": "Burundi", + "bj": "Benin", + "bl": "Saint Barthélemy", + "bm": "Bermuda", + "bn": "Brunei", + "bo": "Bolivia", + "bq": "Caribbean Netherlands", + "br": "Brazil", + "bs": "The Bahamas", + "bt": "Bhutan", + "bv": "Bouvet Island", + "bw": "Botswana", + "by": "Belarus", + "bz": "Belize", + "ca": "Canada", + "cc": "Cocos (Keeling) Islands", + "cf": "Central African Republic", + "ch": "Switzerland", + "cl": "Chile", + "cm": "Cameroon", + "co": "Colombia", + "cr": "Costa Rica", + "cu": "Cuba", + "cv": "Cape Verde", + "cx": "Christmas Island", + "cy": "Cyprus", + "cz": "Czech Republic", + "de": "Germany", + "dj": "Djibouti", + "dk": "Denmark", + "dm": "Dominica", + "do": "Dominican Republic", + "dz": "Algeria", + "ec": "Ecuador", + "ee": "Estonia", + "eg": "Egypt", + "eh": "Western Sahara", + "er": "Eritrea", + "es": "Spain", + "fi": "Finland", + "fj": "Fiji", + "fk": "Falkland Islands", + "fm": "Federated States of Micronesia", + "fo": "Faroe Islands", + "fr": "France", + "ga": "Gabon", + "gd": "Grenada", + "ge": "Georgia", + "gf": "French Guiana", + "gh": "Ghana", + "gi": "Gibraltar", + "gl": "Greenland", + "gn": "Guinea", + "gp": "Guadeloupe", + "gq": "Equatorial Guinea", + "gr": "Greece", + "gs": "South Georgia and the South Sandwich Islands", + "gt": "Guatemala", + "gu": "Guam", + "gw": "Guinea-Bissau", + "gy": "Guyana", + "hk": "China Hong Kong", + "hm": "Heard Island and McDonald Islands", + "hn": "Honduras", + "hr": "Croatia", + "ht": "Haiti", + "hu": "Hungary", + "id": "Indonesia", + "ie": "Ireland", + "il": "Israel", + "im": "Isle of Man", + "in": "India", + "io": "British Indian Ocean Territory", + "iq": "Iraq", + "ir": "Iran", + "is": "Iceland", + "it": "Italy", + "je": "Jersey", + "jm": "Jamaica", + "jo": "Jordan", + "jp": "Japan", + "kh": "Cambodia", + "ki": "Kiribati", + "km": "The Comoros", + "kw": "Kuwait", + "ky": "Cayman Islands", + "lb": "Lebanon", + "li": "Liechtenstein", + "lk": "Sri Lanka", + "lr": "Liberia", + "ls": "Lesotho", + "lt": "Lithuania", + "lu": "Luxembourg", + "lv": "Latvia", + "ly": "Libya", + "ma": "Morocco", + "mc": "Monaco", + "md": "Moldova", + "me": "Montenegro", + "mf": "Saint Martin (France)", + "mg": "Madagascar", + "mh": "Marshall islands", + "mk": "Republic of Macedonia (FYROM)", + "ml": "Mali", + "mm": "Myanmar (Burma)", + "mo": "China Macao", + "mq": "Martinique", + "mr": "Mauritania", + "ms": "Montserrat", + "mt": "Malta", + "mv": "Maldives", + "mw": "Malawi", + "mx": "Mexico", + "my": "Malaysia", + "na": "Namibia", + "ne": "Niger", + "nf": "Norfolk Island", + "ng": "Nigeria", + "ni": "Nicaragua", + "nl": "Netherlands", + "no": "Norway", + "np": "Nepal", + "nr": "Nauru", + "om": "Oman", + "pa": "Panama", + "pe": "Peru", + "pf": "French polynesia", + "pg": "Papua New Guinea", + "ph": "The Philippines", + "pk": "Pakistan", + "pl": "Poland", + "pn": "Pitcairn Islands", + "pr": "Puerto Rico", + "ps": "Palestinian territories", + "pw": "Palau", + "py": "Paraguay", + "qa": "Qatar", + "re": "Réunion", + "ro": "Romania", + "rs": "Serbia", + "ru": "Russian Federation", + "rw": "Rwanda", + "sb": "Solomon Islands", + "sc": "Seychelles", + "sd": "Sudan", + "se": "Sweden", + "sg": "Singapore", + "si": "Slovenia", + "sj": "Template:Country data SJM Svalbard", + "sk": "Slovakia", + "sl": "Sierra Leone", + "sm": "San Marino", + "sn": "Senegal", + "so": "Somalia", + "sr": "Suriname", + "ss": "South Sudan", + "st": "Sao Tome & Principe", + "sv": "El Salvador", + "sy": "Syria", + "sz": "Swaziland", + "tc": "Turks & Caicos Islands", + "td": "Chad", + "tg": "Togo", + "th": "Thailand", + "tk": "Tokelau", + "tl": "Timor-Leste (East Timor)", + "tn": "Tunisia", + "to": "Tonga", + "tr": "Turkey", + "tv": "Tuvalu", + "tz": "Tanzania", + "ua": "Ukraine", + "ug": "Uganda", + "us": "United States of America (USA)", + "uy": "Uruguay", + "va": "Vatican City (The Holy See)", + "ve": "Venezuela", + "vg": "British Virgin Islands", + "vi": "United States Virgin Islands", + "vn": "Vietnam", + "wf": "Wallis and Futuna", + "ws": "Samoa", + "ye": "Yemen", + "yt": "Mayotte", + "za": "South Africa", + "zm": "Zambia", + "zw": "Zimbabwe", + "cn": "China", + "cg": "Republic of the Congo", + "cd": "Democratic Republic of the Congo", + "mz": "Mozambique", + "gg": "Guernsey", + "gm": "Gambia", + "mp": "Northern Mariana Islands", + "et": "Ethiopia", + "nc": "New Caledonia", + "vu": "Vanuatu", + "tf": "French Southern Territories", + "nu": "Niue", + "um": "United States Minor Outlying Islands", + "ck": "Cook Islands", + "gb": "Great Britain", + "tt": "Trinidad & Tobago", + "vc": "St. Vincent & the Grenadines", + "tw": "China Taiwan", + "nz": "New Zealand", + "sa": "Saudi Arabia", + "la": "Laos", + "kp": "North Korea", + "kr": "South Korea", + "pt": "Portugal", + "kg": "Kyrgyzstan", + "kz": "Kazakhstan", + "tj": "Tajikistan", + "tm": "Turkmenistan", + "uz": "Uzbekistan", + "kn": "St. Kitts & Nevis", + "pm": "Saint-Pierre and Miquelon", + "sh": "St. Helena & Dependencies", + "lc": "St. Lucia", + "mu": "Mauritius", + "ci": "C_te d'Ivoire", + "ke": "Kenya", + "mn": "Mongolia" +} \ No newline at end of file diff --git a/localAPI-main/font.js b/localAPI-main/font.js new file mode 100644 index 0000000..6ab189f --- /dev/null +++ b/localAPI-main/font.js @@ -0,0 +1,185 @@ +const fonts = [ + "Arial", + "Calibri", + "Cambria", + "Cambria Math", + "Candara", + "Comic Sans MS", + "Comic Sans MS Bold", + "Comic Sans", + "Consolas", + "Constantia", + "Corbel", + "Courier New", + "Caurier Regular", + "Ebrima", + "Fixedsys Regular", + "Franklin Gothic", + "Gabriola Regular", + "Gadugi", + "Georgia", + "HoloLens MDL2 Assets Regular", + "Impact Regular", + "Javanese Text Regular", + "Leelawadee UI", + "Lucida Console Regular", + "Lucida Sans Unicode Regular", + "Malgun Gothic", + "Microsoft Himalaya Regular", + "Microsoft JhengHei", + "Microsoft JhengHei UI", + "Microsoft PhangsPa", + "Microsoft Sans Serif Regular", + "Microsoft Tai Le", + "Microsoft YaHei", + "Microsoft YaHei UI", + "Microsoft Yi Baiti Regular", + "MingLiU_HKSCS-ExtB Regular", + "MingLiu-ExtB Regular", + "Modern Regular", + "Mongolia Baiti Regular", + "MS Gothic Regular", + "MS PGothic Regular", + "MS Sans Serif Regular", + "MS Serif Regular", + "MS UI Gothic Regular", + "MV Boli Regular", + "Myanmar Text", + "Nimarla UI", + "MV Boli Regular", + "Myanmar Tet", + "Nirmala UI", + "NSimSun Regular", + "Palatino Linotype", + "PMingLiU-ExtB Regular", + "Roman Regular", + "Script Regular", + "Segoe MDL2 Assets Regular", + "Segoe Print", + "Segoe Script", + "Segoe UI", + "Segoe UI Emoji Regular", + "Segoe UI Historic Regular", + "Segoe UI Symbol Regular", + "SimSun Regular", + "SimSun-ExtB Regular", + "Sitka Banner", + "Sitka Display", + "Sitka Heading", + "Sitka Small", + "Sitka Subheading", + "Sitka Text", + "Small Fonts Regular", + "Sylfaen Regular", + "Symbol Regular", + "System Bold", + "Tahoma", + "Terminal", + "Times New Roman", + "Trebuchet MS", + "Verdana", + "Webdings Regular", + "Wingdings Regular", + "Yu Gothic", + "Yu Gothic UI", + "Arial", + "Arial Black", + "Calibri", + "Calibri Light", + "Cambria", + "Cambria Math", + "Candara", + "Comic Sans MS", + "Consolas", + "Constantia", + "Corbel", + "Courier", + "Courier New", + "Ebrima", + "Fixedsys", + "Franklin Gothic Medium", + "Gabriola", + "Gadugi", + "Georgia", + "HoloLens MDL2 Assets", + "Impact", + "Javanese Text", + "Leelawadee UI", + "Leelawadee UI Semilight", + "Lucida Console", + "Lucida Sans Unicode", + "MS Gothic", + "MS PGothic", + "MS Sans Serif", + "MS Serif", + "MS UI Gothic", + "MV Boli", + "Malgun Gothic", + "Malgun Gothic Semilight", + "Marlett", + "Microsoft Himalaya", + "Microsoft JhengHei", + "Microsoft JhengHei Light", + "Microsoft JhengHei UI", + "Microsoft JhengHei UI Light", + "Microsoft New Tai Lue", + "Microsoft PhagsPa", + "Microsoft Sans Serif", + "Microsoft Tai Le", + "Microsoft YaHei", + "Microsoft YaHei Light", + "Microsoft YaHei UI", + "Microsoft YaHei UI Light", + "Microsoft Yi Baiti", + "MingLiU-ExtB", + "MingLiU_HKSCS-ExtB", + "Modern", + "Mongolian Baiti", + "Myanmar Text", + "NSimSun", + "Nirmala UI", + "Nirmala UI Semilight", + "PMingLiU-ExtB", + "Palatino Linotype", + "Roman", + "Script", + "Segoe MDL2 Assets", + "Segoe Print", + "Segoe Script", + "Segoe UI", + "Segoe UI Black", + "Segoe UI Emoji", + "Segoe UI Historic", + "Segoe UI Light", + "Segoe UI Semibold", + "Segoe UI Semilight", + "Segoe UI Symbol", + "SimSun", + "SimSun-ExtB", + "Sitka Banner", + "Sitka Display", + "Sitka Heading", + "Sitka Small", + "Sitka Subheading", + "Sitka Text", + "Small Fonts", + "Sylfaen", + "Symbol", + "System", + "Tahoma", + "Terminal", + "Times New Roman", + "Trebuchet MS", + "Verdana", + "Webdings", + "Wingdings", + "Yu Gothic", + "Yu Gothic Light", + "Yu Gothic Medium", + "Yu Gothic UI", + "Yu Gothic UI Light", + "Yu Gothic UI Semibold", + "Yu Gothic UI Semilight" +]; + +module.exports = fonts; diff --git a/localAPI-main/js-examples/example-check-profile-status.js b/localAPI-main/js-examples/example-check-profile-status.js new file mode 100644 index 0000000..3e3b55e --- /dev/null +++ b/localAPI-main/js-examples/example-check-profile-status.js @@ -0,0 +1,17 @@ +const axios = require('axios'); + +const profileId = 'XX'; + +const config = { + method: 'get', + url: `http://localhost:50325/api/v1/browser/active?user_id=${profileId}`, + headers: { } +}; + +axios(config) +.then((response) => { + console.log(JSON.stringify(response.data)); +}) +.catch((error) => { + console.log(error); +}); diff --git a/localAPI-main/js-examples/example-create-group.js b/localAPI-main/js-examples/example-create-group.js new file mode 100644 index 0000000..a095856 --- /dev/null +++ b/localAPI-main/js-examples/example-create-group.js @@ -0,0 +1,21 @@ +const axios = require('axios'); +const data = { + group_name: "your_group_name" +}; + +const config = { + method: 'post', + url: 'http://local.adspower.net:50325/api/v1/group/create', + headers: { + 'Content-Type': 'application/json' + }, + data : data +}; + +axios(config) +.then((response) => { + console.log(JSON.stringify(response.data)); +}) +.catch((error) => { + console.log(error); +}); diff --git a/localAPI-main/js-examples/example-create-profile.js b/localAPI-main/js-examples/example-create-profile.js new file mode 100644 index 0000000..61b1d30 --- /dev/null +++ b/localAPI-main/js-examples/example-create-profile.js @@ -0,0 +1,48 @@ +var axios = require('axios'); +var data = { + "name": "test", + "group_id": "0", + "domain_name": "facebook.com", + "repeat_config": [ + "0" + ], + "country": "us", + "fingerprint_config": { + "language": [ + "en-US" + ], + "ua": "Mozilla/5.0 (Linux; Android 8.0.0; BND-AL10 Build/HONORBND-AL10; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/63.0.3239.83 Mobile Safari/537.36 T7/11.5 baiduboxapp/11.5.0.10 (Baidu; P1 8.0.0)", + "flash": "block", + "scan_port_type": "1", + "screen_resolution": "1024_600", + "fonts": [ + "all" + ], + "longitude": "180", + "latitude": "90", + "webrtc": "proxy", + "do_not_track": "true", + "hardware_concurrency": "default", + "device_memory": "default" + }, + "user_proxy_config": { + "proxy_soft": "no_proxy" + } +}; + +var config = { + method: 'post', + url: 'http://local.adspower.net:50325/api/v1/user/create', + headers: { + 'Content-Type': 'application/json' + }, + data : data +}; + +axios(config) +.then((response) => { + console.log(JSON.stringify(response.data)); +}) +.catch((error) => { + console.log(error); +}); diff --git a/localAPI-main/js-examples/example-delete-profile-cache.js b/localAPI-main/js-examples/example-delete-profile-cache.js new file mode 100644 index 0000000..06df94c --- /dev/null +++ b/localAPI-main/js-examples/example-delete-profile-cache.js @@ -0,0 +1,15 @@ +const axios = require('axios'); + +const config = { + method: 'post', + url: 'http://localhost:50325/api/v1/user/delete-cache', + headers: { } +}; + +axios(config) +.then((response) => { + console.log(JSON.stringify(response.data)); +}) +.catch((error) => { + console.log(error); +}); diff --git a/localAPI-main/js-examples/example-delete-profile.js b/localAPI-main/js-examples/example-delete-profile.js new file mode 100644 index 0000000..599382b --- /dev/null +++ b/localAPI-main/js-examples/example-delete-profile.js @@ -0,0 +1,23 @@ +const axios = require('axios'); +const data = { + "user_ids": [ + "XX" + ] +}; + +const config = { + method: 'post', + url: 'http://localhost:50325/api/v1/user/delete', + headers: { + 'Content-Type': 'application/json' + }, + data : data +}; + +axios(config) +.then((response) => { + console.log(JSON.stringify(response.data)); +}) +.catch((error) => { + console.log(error); +}); diff --git a/localAPI-main/js-examples/example-query-group.js b/localAPI-main/js-examples/example-query-group.js new file mode 100644 index 0000000..48f6f70 --- /dev/null +++ b/localAPI-main/js-examples/example-query-group.js @@ -0,0 +1,15 @@ +const axios = require('axios'); + +const config = { + method: 'get', + url: 'http://local.adspower.net:50325/api/v1/group/list?page=1&page_size=15', + headers: { } +}; + +axios(config) +.then((response) => { + console.log(JSON.stringify(response.data)); +}) +.catch((error) => { + console.log(error); +}); diff --git a/localAPI-main/js-examples/example-query-profile.js b/localAPI-main/js-examples/example-query-profile.js new file mode 100644 index 0000000..295346e --- /dev/null +++ b/localAPI-main/js-examples/example-query-profile.js @@ -0,0 +1,15 @@ +const axios = require('axios'); + +const config = { + method: 'get', + url: 'http://local.adspower.net:50325/api/v1/user/list?page=1&page_size=100', + headers: { } +}; + +axios(config) +.then((response) => { + console.log(JSON.stringify(response.data)); +}) +.catch((error) => { + console.log(error); +}); diff --git a/localAPI-main/js-examples/example-regroup-profile.js b/localAPI-main/js-examples/example-regroup-profile.js new file mode 100644 index 0000000..b499083 --- /dev/null +++ b/localAPI-main/js-examples/example-regroup-profile.js @@ -0,0 +1,24 @@ +const axios = require('axios'); +const data = { + "user_ids": [ + "XX" + ], + "group_id": "0" +}; + +const config = { + method: 'post', + url: 'http://local.adspower.net:50325/api/v1/user/regroup', + headers: { + 'Content-Type': 'application/json' + }, + data : data +}; + +axios(config) +.then((response) => { + console.log(JSON.stringify(response.data)); +}) +.catch((error) => { + console.log(error); +}); diff --git a/localAPI-main/js-examples/example-start-profile.js b/localAPI-main/js-examples/example-start-profile.js new file mode 100644 index 0000000..19fc84d --- /dev/null +++ b/localAPI-main/js-examples/example-start-profile.js @@ -0,0 +1,27 @@ +const axios = require('axios'); +const puppeteer = require('puppeteer-core'); + +const profileId = 'XXX'; + +// http://local.adspower.net:50325: Script can go to Profile Management-> click Settings-> click Cache folder-> local_api file to obtain API address +axios.get(`http://local.adspower.net:50325/api/v1/browser/start?user_id=${profileId}`).then(async (res) => { + console.log(res.data); + + if(res.data.code === 0 && res.data.data.ws && res.data.data.ws.puppeteer) { + try{ + const browser = await puppeteer.connect({ + browserWSEndpoint: res.data.data.ws.puppeteer, + defaultViewport:null + }); + + const page = await browser.newPage(); + await page.goto('https://www.adspower.com'); + await page.screenshot({ path: './adspower.png' }); + await browser.close(); + } catch(err){ + console.log(err.message); + } + } +}).catch((err) => { + console.log(err) +}) \ No newline at end of file diff --git a/localAPI-main/js-examples/example-stop-profile.js b/localAPI-main/js-examples/example-stop-profile.js new file mode 100644 index 0000000..4afef8a --- /dev/null +++ b/localAPI-main/js-examples/example-stop-profile.js @@ -0,0 +1,16 @@ +const axios = require('axios'); + +const profileId = 'XX'; +const config = { + method: 'get', + url: `http://local.adspower.net:50325/api/v1/browser/stop?user_id=${profileId}`, + headers: { } +}; + +axios(config) +.then((response) => { + console.log(JSON.stringify(response.data)); +}) +.catch((error) => { + console.log(error); +}); diff --git a/localAPI-main/js-examples/example-update-profile.js b/localAPI-main/js-examples/example-update-profile.js new file mode 100644 index 0000000..5e6f245 --- /dev/null +++ b/localAPI-main/js-examples/example-update-profile.js @@ -0,0 +1,41 @@ +const axios = require('axios'); +const data = { + "user_id": "XX", + "name": "test", + "domain_name": "facebook.com", + "repeat_config": [ + "0" + ], + "open_urls": [ + "http://www.baidu.com", + "https://www.google.com" + ], + "country": "us", + "remark": "remark", + "fingerprint_config": { + "webrtc": "proxy", + "do_not_track": "true", + "hardware_concurrency": "default", + "device_memory": "default" + }, + "user_proxy_config": { + "proxy_soft": "no_proxy" + } +}; + +const config = { + method: 'post', + url: 'http://local.adspower.net:50325/api/v1/user/update', + headers: { + 'Content-Type': 'application/json' + }, + data : data +}; + +axios(config) +.then(function (response) { + console.log(JSON.stringify(response.data)); +}) +.catch(function (error) { + console.log(error); +}); diff --git a/localAPI-main/language.js b/localAPI-main/language.js new file mode 100644 index 0000000..042fd01 --- /dev/null +++ b/localAPI-main/language.js @@ -0,0 +1,2160 @@ +const language = [ + { + cc: 'ad', + code: 'ca-ES', + prefix: 'ca', + en: 'Catalan', + nation: '安道尔', + lang: '加泰罗尼亚语' + }, + { + cc: 'af', + code: 'prs-AF', + prefix: 'prs', + en: 'Dari', + nation: '阿富汗', + lang: '达里语' + }, + { + cc: 'af', + code: 'ps-AF', + prefix: 'ps', + en: 'Pashto', + nation: '阿富汗', + lang: '普什图语' + }, + { + cc: 'al', + code: 'sq-AL', + prefix: 'sq', + en: 'Albanian', + nation: '阿尔巴尼亚', + lang: '阿尔巴尼亚语' + }, + { + cc: 'am', + code: 'hy-AM', + prefix: 'hy', + en: 'Armenian', + nation: '亚美尼亚', + lang: '亚美尼亚语' + }, + { + cc: 'ao', + code: 'pt-PT', + prefix: 'pt', + en: 'Portuguese', + nation: '安哥拉', + lang: '葡萄牙语' + }, + { + cc: 'aq', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '南极洲', + lang: '英语' + }, + { + cc: 'ar', + code: 'es-AR', + prefix: 'es', + en: 'Spanish', + nation: '阿根廷', + lang: '西班牙语' + }, + { + cc: 'as', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '美属萨摩亚', + lang: '英语' + }, + { + cc: 'at', + code: 'de-AT', + prefix: 'de', + en: 'German', + nation: '奥地利', + lang: '德语' + }, + { + cc: 'ag', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '安提瓜和巴布达', + lang: '英语' + }, + { + cc: 'ai', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '安圭拉', + lang: '英语' + }, + { + cc: 'ae', + code: 'ar-AE', + prefix: 'ar', + en: 'Arabic', + nation: '阿联酋', + lang: '阿拉伯语' + }, + { + cc: 'ax', + code: 'sv-SE', + prefix: 'sv', + en: 'Swedish', + nation: '奥兰群岛', + lang: '瑞典语' + }, + { + cc: 'ax', + code: 'fi-FI', + prefix: 'fi', + en: 'Finnish', + nation: '奥兰群岛', + lang: '芬兰语' + }, + { + cc: 'az', + code: 'az-Latn-AZ', + prefix: 'az', + en: 'Azerbaijani', + nation: '阿塞拜疆', + lang: '阿塞拜疆语' + }, + { + cc: 'az', + code: 'az-Cyrl-AZ', + prefix: 'az', + en: 'Azerbaijani', + nation: '阿塞拜疆', + lang: '阿塞拜疆语' + }, + { + cc: 'ba', + code: 'bs-BA', + prefix: 'bs', + en: 'Bosnian', + nation: '波黑', + lang: '波斯尼亚语' + }, + { + cc: 'ba', + code: 'hr-BA', + prefix: 'hr', + en: 'Croatian', + nation: '波黑', + lang: '克罗地亚语' + }, + { + cc: 'ba', + code: 'sr-BA', + prefix: 'sr', + en: 'Serbian', + nation: '波黑', + lang: '塞尔维亚语' + }, + { + cc: 'aw', + code: 'nl-NL', + prefix: 'nl', + en: 'Dutch', + nation: '阿鲁巴', + lang: '荷兰语' + }, + { + cc: 'bd', + code: 'bn-BD', + prefix: 'bn', + en: 'Bengali', + nation: '孟加拉', + lang: '孟加拉语' + }, + { + cc: 'au', + code: 'en-AU', + prefix: 'en', + en: 'English', + nation: '澳大利亚', + lang: '英语' + }, + { + cc: 'bf', + code: 'fr-FR', + prefix: 'fr', + en: 'French', + nation: '布基纳法索', + lang: '法语' + }, + { + cc: 'bg', + code: 'bg-BG', + prefix: 'bg', + en: 'Bulgarian', + nation: '保加利亚', + lang: '保加利亚语' + }, + { + cc: 'bh', + code: 'ar-BH', + prefix: 'ar', + en: 'Arabic', + nation: '巴林', + lang: '阿拉伯语' + }, + { + cc: 'bj', + code: 'fr-FR', + prefix: 'fr', + en: 'French', + nation: '贝宁', + lang: '法语' + }, + { + cc: 'bi', + code: 'fr-FR', + prefix: 'fr', + en: 'French', + nation: '布隆迪', + lang: '法语' + }, + { + cc: 'bi', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '布隆迪', + lang: '英语' + }, + { + cc: 'bm', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '百慕大', + lang: '英语' + }, + { + cc: 'bl', + code: 'fr-FR', + prefix: 'fr', + en: 'French', + nation: '圣巴泰勒米岛', + lang: '法语' + }, + { + cc: 'bb', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '巴巴多斯', + lang: '英语' + }, + { + cc: 'bo', + code: 'quz-BO', + prefix: 'quz', + en: 'Quechua', + nation: '玻利维亚', + lang: '克丘亚语' + }, + { + cc: 'be', + code: 'fr-BE', + prefix: 'fr', + en: 'French', + nation: '比利时', + lang: '法语' + }, + { + cc: 'bq', + code: 'nl-NL', + prefix: 'nl', + en: 'Dutch', + nation: '荷兰加勒比区', + lang: '荷兰语' + }, + { + cc: 'br', + code: 'pt-BR', + prefix: 'pt', + en: 'Portuguese', + nation: '巴西', + lang: '葡萄牙语' + }, + { + cc: 'bs', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '巴哈马', + lang: '英语' + }, + { + cc: 'bt', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '不丹', + lang: '英语' + }, + { + cc: 'bv', + code: '', + prefix: '', + nation: '布韦岛', + lang: '' + }, + { + cc: 'bw', + code: 'tn-ZA', + prefix: 'tn', + en: 'Tswana', + nation: '博茨瓦纳', + lang: '茨瓦纳语' + }, + { + cc: 'by', + code: 'be-BY', + prefix: 'be', + en: 'Belarus', + nation: '白俄罗斯', + lang: '白俄罗斯语' + }, + { + cc: 'bz', + code: 'en-BZ', + prefix: 'en', + en: 'English', + nation: '伯利兹', + lang: '英语' + }, + { + cc: 'ca', + code: 'fr-CA', + prefix: 'fr', + en: 'French', + nation: '加拿大', + lang: '法语' + }, + { + cc: 'cc', + code: '', + prefix: '', + nation: '科科斯群岛', + lang: '' + }, + { + cc: 'bn', + code: 'ms-BN', + prefix: 'ms', + en: 'Malay', + nation: '文莱', + lang: '马来语' + }, + { + cc: 'cf', + code: 'fr-FR', + prefix: 'fr', + en: 'French', + nation: '中非', + lang: '法语' + }, + { + cc: 'cl', + code: 'arn-CL', + prefix: 'arn', + en: 'Mapdangan', + nation: '智利', + lang: '马普丹冈语' + }, + { + cc: 'cm', + code: 'fr-FR', + prefix: 'fr', + en: 'French', + nation: '喀麦隆', + lang: '法语' + }, + { + cc: 'cm', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '喀麦隆', + lang: '英语' + }, + { + cc: 'co', + code: 'es-CO', + prefix: 'es', + en: 'Spanish', + nation: '哥伦比亚', + lang: '西班牙语' + }, + { + cc: 'cr', + code: 'es-CR', + prefix: 'es', + en: 'Spanish', + nation: '哥斯达黎加', + lang: '西班牙语' + }, + { + cc: 'cv', + code: 'pt-PT', + prefix: 'pt', + en: 'Portuguese', + nation: '佛得角', + lang: '葡萄牙语' + }, + { + cc: 'cu', + code: 'es-ES', + prefix: 'es', + en: 'Spanish', + nation: '古巴', + lang: '西班牙语' + }, + { + cc: 'cx', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '圣诞岛', + lang: '英语' + }, + { + cc: 'cy', + code: 'el-GR', + prefix: 'el', + en: 'Greek', + nation: '塞浦路斯', + lang: '希腊语' + }, + { + cc: 'cy', + code: 'tr-TR', + prefix: 'tr', + en: 'Turkish', + nation: '塞浦路斯', + lang: '土耳其语' + }, + { + cc: 'cz', + code: 'cs-CZ', + prefix: 'cs', + en: 'Czech', + nation: '捷克', + lang: '捷克语' + }, + { + cc: 'de', + code: 'de-DE', + prefix: 'de', + en: 'German', + nation: '德国', + lang: '德语' + }, + { + cc: 'dj', + code: 'fr-FR', + prefix: 'fr', + en: 'French', + nation: '吉布提', + lang: '法语' + }, + { + cc: 'dj', + code: 'ar-SA', + prefix: 'ar', + en: 'Arabic', + nation: '吉布提', + lang: '阿拉伯语' + }, + { + cc: 'dk', + code: 'da-DK', + prefix: 'da', + en: 'Danish', + nation: '丹麦', + lang: '丹麦语' + }, + { + cc: 'dm', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '多米尼克', + lang: '英语' + }, + { + cc: 'do', + code: 'es-DO', + prefix: 'es', + en: 'Spanish', + nation: '多米尼加', + lang: '西班牙语' + }, + { + cc: 'dz', + code: 'ar-DZ', + prefix: 'ar', + en: 'Arabic', + nation: '阿尔及利亚', + lang: '阿拉伯语' + }, + { + cc: 'ec', + code: 'quz-EC', + prefix: 'quz', + en: 'Quechua', + nation: '厄瓜多尔', + lang: '克丘亚语' + }, + { + cc: 'ee', + code: 'et-EE', + prefix: 'et', + en: 'Estonian', + nation: '爱沙尼亚', + lang: '爱沙尼亚语' + }, + { + cc: 'eh', + code: 'ar-SA', + prefix: 'ar', + en: 'Arabic', + nation: '西撒哈拉', + lang: '阿拉伯语' + }, + { + cc: 'er', + code: 'ar-SA', + prefix: 'ar', + en: 'Arabic', + nation: '厄立特里亚', + lang: '阿拉伯语' + }, + { + cc: 'er', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '厄立特里亚', + lang: '英语' + }, + { + cc: 'es', + code: 'es-ES', + prefix: 'es', + en: 'Spanish', + nation: '西班牙', + lang: '西班牙语' + }, + { + cc: 'eg', + code: 'ar-EG', + prefix: 'ar', + en: 'Arabic', + nation: '埃及', + lang: '阿拉伯语' + }, + { + cc: 'fi', + code: 'se-FI', + prefix: 'se', + en: 'Northern Sami', + nation: '芬兰', + lang: '北萨米语' + }, + { + cc: 'fj', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '斐济群岛', + lang: '英语' + }, + { + cc: 'fk', + code: 'es-ES', + prefix: 'es', + en: 'Spanish', + nation: '马尔维纳斯群岛(福克兰)', + lang: '西班牙语' + }, + { + cc: 'fk', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '马尔维纳斯群岛(福克兰)', + lang: '英语' + }, + { + cc: 'ch', + code: 'de-CH', + prefix: 'de', + en: 'German', + nation: '瑞士', + lang: '德语' + }, + { + cc: 'fo', + code: 'fo-FO', + prefix: 'fo', + en: 'Faroese', + nation: '法罗群岛', + lang: '法罗语' + }, + { + cc: 'fr', + code: 'fr-FR', + prefix: 'fr', + en: 'French', + nation: '法国', + lang: '法语' + }, + { + cc: 'fm', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '密克罗尼西亚联邦', + lang: '英语' + }, + { + cc: 'ga', + code: 'fr-FR', + prefix: 'fr', + en: 'French', + nation: '加蓬', + lang: '法语' + }, + { + cc: 'gd', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '格林纳达', + lang: '英语' + }, + { + cc: 'ge', + code: 'ka-GE', + prefix: 'ka', + en: 'Georgian', + nation: '格鲁吉亚', + lang: '格鲁吉亚语' + }, + { + cc: 'gf', + code: 'fr-FR', + prefix: 'fr', + en: 'French', + nation: '法属圭亚那', + lang: '法语' + }, + { + cc: 'gh', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '加纳', + lang: '英语' + }, + { + cc: 'gi', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '直布罗陀', + lang: '英语' + }, + { + cc: 'gl', + code: 'kl-GL', + prefix: 'kl', + en: 'Greenland', + nation: '格陵兰', + lang: '格陵兰语' + }, + { + cc: 'gn', + code: 'fr-FR', + prefix: 'fr', + en: 'French', + nation: '几内亚', + lang: '法语' + }, + { + cc: 'gp', + code: 'fr-FR', + prefix: 'fr', + en: 'French', + nation: '瓜德罗普', + lang: '法语' + }, + { + cc: 'gq', + code: 'es-ES', + prefix: 'es', + en: 'Spanish', + nation: '赤道几内亚', + lang: '西班牙语' + }, + { + cc: 'gq', + code: 'fr-FR', + prefix: 'fr', + en: 'French', + nation: '赤道几内亚', + lang: '法语' + }, + { + cc: 'gq', + code: 'pt-PT', + prefix: 'pt', + en: 'Portuguese', + nation: '赤道几内亚', + lang: '葡萄牙语' + }, + { + cc: 'gr', + code: 'el-GR', + prefix: 'el', + en: 'Greek', + nation: '希腊', + lang: '希腊语' + }, + { + cc: 'gs', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '南乔治亚岛和南桑威奇群岛', + lang: '英语' + }, + { + cc: 'gt', + code: 'qut-GT', + prefix: 'qut', + en: 'Keeche', + nation: '危地马拉', + lang: '基切语' + }, + { + cc: 'gu', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '关岛', + lang: '英语' + }, + { + cc: 'gw', + code: 'pt-PT', + prefix: 'pt', + en: 'Portuguese', + nation: '几内亚比绍', + lang: '葡萄牙语' + }, + { + cc: 'gy', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '圭亚那', + lang: '英语' + }, + { + cc: 'hk', + code: 'zh-HK', + prefix: 'zh', + en: 'Chinese', + nation: '中国香港', + lang: '中文' + }, + { + cc: 'hm', + code: '', + prefix: '', + nation: '赫德岛和麦克唐纳群岛', + lang: '' + }, + { + cc: 'hn', + code: 'es-HN', + prefix: 'es', + en: 'Spanish', + nation: '洪都拉斯', + lang: '西班牙语' + }, + { + cc: 'hr', + code: 'hr-HR', + prefix: 'hr', + en: 'Croatian', + nation: '克罗地亚', + lang: '克罗地亚语' + }, + { + cc: 'hu', + code: 'hu-HU', + prefix: 'hu', + en: 'Hungarian', + nation: '匈牙利', + lang: '匈牙利语' + }, + { + cc: 'il', + code: 'he-IL', + prefix: 'he', + en: 'Hebrew', + nation: '以色列', + lang: '希伯来语' + }, + { + cc: 'ie', + code: 'ga-IE', + prefix: 'ga', + en: 'Irish', + nation: '爱尔兰', + lang: '爱尔兰语' + }, + { + cc: 'id', + code: 'id-ID', + prefix: 'id', + en: 'Indonesian', + nation: '印尼', + lang: '印度尼西亚语' + }, + { + cc: 'ht', + code: 'fr-FR', + prefix: 'fr', + en: 'French', + nation: '海地', + lang: '法语' + }, + { + cc: 'im', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '马恩岛', + lang: '英语' + }, + { + cc: 'io', + code: 'en-GB', + prefix: 'en', + en: 'English', + nation: '英属印度洋领地', + lang: '英语' + }, + { + cc: 'in', + code: 'hi-IN', + prefix: 'hi', + en: 'Hindi', + nation: '印度', + lang: '印地语' + }, + { + cc: 'is', + code: 'is-IS', + prefix: 'is', + en: 'Island', + nation: '冰岛', + lang: '冰岛语' + }, + { + cc: 'ir', + code: 'fa-IR', + prefix: 'fa', + en: 'Persian', + nation: '伊朗', + lang: '波斯语' + }, + { + cc: 'iq', + code: 'ar-IQ', + prefix: 'ar', + en: 'Arabic', + nation: '伊拉克', + lang: '阿拉伯语' + }, + { + cc: 'it', + code: 'it-IT', + prefix: 'it', + en: 'Italian', + nation: '意大利', + lang: '意大利语' + }, + { + cc: 'je', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '泽西岛', + lang: '英语' + }, + { + cc: 'je', + code: 'fr-FR', + prefix: 'fr', + en: 'French', + nation: '泽西岛', + lang: '法语' + }, + { + cc: 'jm', + code: 'en-JM', + prefix: 'en', + en: 'English', + nation: '牙买加', + lang: '英语' + }, + { + cc: 'jo', + code: 'ar-JO', + prefix: 'ar', + en: 'Arabic', + nation: '约旦', + lang: '阿拉伯语' + }, + { + cc: 'jp', + code: 'ja-JP', + prefix: 'ja', + en: 'Japanese', + nation: '日本', + lang: '日语' + }, + { + cc: 'kh', + code: 'km-KH', + prefix: 'km', + en: 'Khmer', + nation: '柬埔寨', + lang: '高棉语' + }, + { + cc: 'ki', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '基里巴斯', + lang: '英语' + }, + { + cc: 'km', + code: 'fr-FR', + prefix: 'fr', + en: 'French', + nation: '科摩罗', + lang: '法语' + }, + { + cc: 'km', + code: 'ar-SA', + prefix: 'ar', + en: 'Arabic', + nation: '科摩罗', + lang: '阿拉伯语' + }, + { + cc: 'kw', + code: 'ar-KW', + prefix: 'ar', + en: 'Arabic Language', + nation: '科威特', + lang: '阿拉伯语' + }, + { + cc: 'ky', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '开曼群岛', + lang: '英语' + }, + { + cc: 'li', + code: 'de-LI', + prefix: 'de', + en: 'German', + nation: '列支敦士登', + lang: '德语' + }, + { + cc: 'lk', + code: 'si-LK', + prefix: 'si', + en: 'Sinhala', + nation: '斯里兰卡', + lang: '僧伽罗语' + }, + { + cc: 'lb', + code: 'ar-LB', + prefix: 'ar', + en: 'Arabic', + nation: '黎巴嫩', + lang: '阿拉伯语' + }, + { + cc: 'lt', + code: 'lt-LT', + prefix: 'lt', + en: 'Lithuanian', + nation: '立陶宛', + lang: '立陶宛语' + }, + { + cc: 'lu', + code: 'de-LU', + prefix: 'de', + en: 'German', + nation: '卢森堡', + lang: '德语' + }, + { + cc: 'lr', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '利比里亚', + lang: '英语' + }, + { + cc: 'lv', + code: 'lv-LV', + prefix: 'lv', + en: 'Latvian', + nation: '拉脱维亚', + lang: '拉脱维亚语' + }, + { + cc: 'ly', + code: 'ar-LY', + prefix: 'ar', + en: 'Arabic', + nation: '利比亚', + lang: '阿拉伯语' + }, + { + cc: 'ma', + code: 'ar-MA', + prefix: 'ar', + en: 'Arabic', + nation: '摩洛哥', + lang: '阿拉伯语' + }, + { + cc: 'mc', + code: 'fr-MC', + prefix: 'fr', + en: 'French', + nation: '摩纳哥', + lang: '法语' + }, + { + cc: 'md', + code: 'ro-RO', + prefix: 'ro', + en: 'Romanian', + nation: '摩尔多瓦', + lang: '罗马尼亚语' + }, + { + cc: 'me', + code: 'sr-Latn-ME', + prefix: 'sr', + en: 'Serbian', + nation: '黑山', + lang: '塞尔维亚语' + }, + { + cc: 'mf', + code: 'fr-FR', + prefix: 'fr', + en: 'French', + nation: '法属圣马丁', + lang: '法语' + }, + { + cc: 'mg', + code: 'fr-FR', + prefix: 'fr', + en: 'French', + nation: '马达加斯加', + lang: '法语' + }, + { + cc: 'mh', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '马绍尔群岛', + lang: '英语' + }, + { + cc: 'ls', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '莱索托', + lang: '英语' + }, + { + cc: 'mk', + code: 'mk-MK', + prefix: 'mk', + en: 'Macedonian', + nation: '马其顿', + lang: '马其顿语' + }, + { + cc: 'mq', + code: 'fr-FR', + prefix: 'fr', + en: 'French', + nation: '马提尼克', + lang: '法语' + }, + { + cc: 'mr', + code: 'ar-SA', + prefix: 'ar', + en: 'Arabic', + nation: '毛里塔尼亚', + lang: '阿拉伯语' + }, + { + cc: 'ms', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '蒙塞拉特岛', + lang: '英语' + }, + { + cc: 'mt', + code: 'mt-MT', + prefix: 'mt', + en: 'Maltese', + nation: '马耳他', + lang: '马耳他语' + }, + { + cc: 'mv', + code: 'dv-MV', + prefix: 'dv', + en: 'Dhivehi', + nation: '马尔代夫', + lang: '迪维希语' + }, + { + cc: 'mw', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '马拉维', + lang: '英语' + }, + { + cc: 'mx', + code: 'es-MX', + prefix: 'es', + en: 'Spanish', + nation: '墨西哥', + lang: '西班牙语' + }, + { + cc: 'ml', + code: 'fr-FR', + prefix: 'fr', + en: 'French', + nation: '马里', + lang: '法语' + }, + { + cc: 'my', + code: 'ms-MY', + prefix: 'ms', + en: 'Malay', + nation: '马来西亚', + lang: '马来语' + }, + { + cc: 'na', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '纳米比亚', + lang: '英语' + }, + { + cc: 'ne', + code: 'fr-FR', + prefix: 'fr', + en: 'French', + nation: '尼日尔', + lang: '法语' + }, + { + cc: 'nf', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '诺福克岛', + lang: '英语' + }, + { + cc: 'ng', + code: 'ha-Latn-NG', + prefix: 'ha', + en: 'Hausa', + nation: '尼日利亚', + lang: '豪撒语' + }, + { + cc: 'ni', + code: 'es-NI', + prefix: 'es', + en: 'Spain', + nation: '尼加拉瓜', + lang: '西班牙语' + }, + { + cc: 'nl', + code: 'fy-NL', + prefix: 'fy', + en: 'Frisian', + nation: '荷兰', + lang: '弗里西亚语' + }, + { + cc: 'no', + code: 'se-NO', + prefix: 'se', + en: 'Northern Sami', + nation: '挪威', + lang: '北萨米语' + }, + { + cc: 'np', + code: 'ne-NP', + prefix: 'ne', + en: 'Nepal', + nation: '尼泊尔', + lang: '尼泊尔语' + }, + { + cc: 'nr', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '瑙鲁', + lang: '英语' + }, + { + cc: 'om', + code: 'ar-OM', + prefix: 'ar', + en: 'Arabic', + nation: '阿曼', + lang: '阿拉伯语' + }, + { + cc: 'pa', + code: 'es-PA', + prefix: 'es', + en: 'Spanish', + nation: '巴拿马', + lang: '西班牙语' + }, + { + cc: 'pe', + code: 'quz-PE', + prefix: 'quz', + en: 'Quechua', + nation: '秘鲁', + lang: '克丘亚语' + }, + { + cc: 'mm', + code: '', + prefix: '', + nation: '缅甸', + lang: '' + }, + { + cc: 'pf', + code: 'fr-FR', + prefix: 'fr', + en: 'French', + nation: '法属波利尼西亚', + lang: '法语' + }, + { + cc: 'mo', + code: 'zh-MO', + prefix: 'zh', + en: 'Chinese', + nation: '中国澳门', + lang: '中文' + }, + { + cc: 'ph', + code: 'fil-PH', + prefix: 'fil', + en: 'Philippine', + nation: '菲律宾', + lang: '菲律宾语' + }, + { + cc: 'pg', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '巴布亚新几内亚', + lang: '英语' + }, + { + cc: 'pl', + code: 'pl-PL', + prefix: 'pl', + en: 'Polish', + nation: '波兰', + lang: '波兰语' + }, + { + cc: 'pk', + code: 'ur-PK', + prefix: 'ur', + en: 'Urdu', + nation: '巴基斯坦', + lang: '乌尔都语' + }, + { + cc: 'ps', + code: 'ar-SA', + prefix: 'ar', + en: 'Arabic', + nation: '巴勒斯坦', + lang: '阿拉伯语' + }, + { + cc: 'pr', + code: 'es-PR', + prefix: 'es', + en: 'Spain Language', + nation: '波多黎各', + lang: '西班牙语' + }, + { + cc: 'pn', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '皮特凯恩群岛', + lang: '英语' + }, + { + cc: 'pw', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '帕劳', + lang: '英语' + }, + { + cc: 'qa', + code: 'ar-QA', + prefix: 'ar', + en: 'Arabic', + nation: '卡塔尔', + lang: '阿拉伯语' + }, + { + cc: 'ro', + code: 'ro-RO', + prefix: 'ro', + en: 'Romanian', + nation: '罗马尼亚', + lang: '罗马尼亚语' + }, + { + cc: 're', + code: 'fr-FR', + prefix: 'fr', + en: 'French', + nation: '留尼汪', + lang: '法语' + }, + { + cc: 'rs', + code: 'sr-Latn-RS', + prefix: 'sr', + en: 'Serbian', + nation: '塞尔维亚', + lang: '塞尔维亚语' + }, + { + cc: 'py', + code: 'es-PY', + prefix: 'es', + en: 'Spanish', + nation: '巴拉圭', + lang: '西班牙语' + }, + { + cc: 'ru', + code: 'ru-RU', + prefix: 'ru', + en: 'Russian', + nation: '俄罗斯', + lang: '俄语' + }, + { + cc: 'sb', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '所罗门群岛', + lang: '英语' + }, + { + cc: 'se', + code: 'se-SE', + prefix: 'se', + en: 'Northern Sami', + nation: '瑞典', + lang: '北萨米语' + }, + { + cc: 'sc', + code: 'fr-FR', + prefix: 'fr', + en: 'French', + nation: '塞舌尔', + lang: '法语' + }, + { + cc: 'sc', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '塞舌尔', + lang: '英语' + }, + { + cc: 'sg', + code: 'en-SG', + prefix: 'en', + en: 'English', + nation: '新加坡', + lang: '英语' + }, + { + cc: 'sd', + code: 'ar-SA', + prefix: 'ar', + en: 'Arabic', + nation: '苏丹', + lang: '阿拉伯语' + }, + { + cc: 'sd', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '苏丹', + lang: '英语' + }, + { + cc: 'si', + code: 'sl-SI', + prefix: 'sl', + en: 'Slovenian', + nation: '斯洛文尼亚', + lang: '斯洛文尼亚语' + }, + { + cc: 'sj', + code: 'nn-no', + prefix: 'no', + en: 'Norwegian', + nation: '斯瓦尔巴群岛和扬马延岛', + lang: '挪威语' + }, + { + cc: 'sk', + code: 'sk-SK', + prefix: 'sk', + en: 'Slovak', + nation: '斯洛伐克', + lang: '斯洛伐克语' + }, + { + cc: 'sl', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '塞拉利昂', + lang: '英语' + }, + { + cc: 'sm', + code: 'it-IT', + prefix: 'it', + en: 'Italian', + nation: '圣马力诺', + lang: '意大利语' + }, + { + cc: 'sn', + code: 'wo-SN', + prefix: 'wo', + en: 'Wolof', + nation: '塞内加尔', + lang: '沃洛夫语' + }, + { + cc: 'so', + code: 'ar-SA', + prefix: 'ar', + en: 'Arabic', + nation: '索马里', + lang: '阿拉伯语' + }, + { + cc: 'sr', + code: 'nl-NL', + prefix: 'nl', + en: 'Dutch', + nation: '苏里南', + lang: '荷兰语' + }, + { + cc: 'ss', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '南苏丹', + lang: '英语' + }, + { + cc: 'st', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '圣多美和普林西比', + lang: '英语' + }, + { + cc: 'sv', + code: 'es-SV', + prefix: 'es', + en: 'Spanish', + nation: '萨尔瓦多', + lang: '西班牙语' + }, + { + cc: 'sy', + code: 'ar-SY', + prefix: 'ar', + en: 'Arabic', + nation: '叙利亚', + lang: '阿拉伯语' + }, + { + cc: 'sz', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '斯威士兰', + lang: '英语' + }, + { + cc: 'tc', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '特克斯和凯科斯群岛', + lang: '英语' + }, + { + cc: 'tg', + code: 'fr-FR', + prefix: 'fr', + en: 'French', + nation: '多哥', + lang: '法语' + }, + { + cc: 'td', + code: 'fr-FR', + prefix: 'fr', + en: 'French', + nation: '乍得', + lang: '法语' + }, + { + cc: 'td', + code: 'ar-SA', + prefix: 'ar', + en: 'Arabic', + nation: '乍得', + lang: '阿拉伯语' + }, + { + cc: 'th', + code: 'th-TH', + prefix: 'ta', + en: 'Thai', + nation: '泰国', + lang: '泰语' + }, + { + cc: 'tk', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '托克劳', + lang: '英语' + }, + { + cc: 'tl', + code: 'pt-PT', + prefix: 'pt', + en: 'Portuguese', + nation: '东帝汶', + lang: '葡萄牙语' + }, + { + cc: 'tn', + code: 'ar-TN', + prefix: 'ar', + en: 'Arabic', + nation: '突尼斯', + lang: '阿拉伯语' + }, + { + cc: 'tr', + code: 'tr-TR', + prefix: 'tr', + en: 'Turkish', + nation: '土耳其', + lang: '土耳其语' + }, + { + cc: 'to', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '汤加', + lang: '英语' + }, + { + cc: 'tz', + code: 'sw-KE', + prefix: 'sw', + en: 'Swah Greek', + nation: '坦桑尼亚', + lang: '斯瓦希里语' + }, + { + cc: 'tz', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '坦桑尼亚', + lang: '英语' + }, + { + cc: 'ua', + code: 'uk-UA', + prefix: 'uk', + en: 'Ukrainian', + nation: '乌克兰', + lang: '乌克兰语' + }, + { + cc: 'uy', + code: 'es-UY', + prefix: 'es', + en: 'Spanish', + nation: '乌拉圭', + lang: '西班牙语' + }, + { + cc: 'ug', + code: 'sw-KE', + prefix: 'sw', + en: 'Swah Greek', + nation: '乌干达', + lang: '斯瓦希里语' + }, + { + cc: 'ug', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '乌干达', + lang: '英语' + }, + { + cc: 'us', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '美国', + lang: '英语' + }, + { + cc: 'va', + code: 'it-IT', + prefix: 'it', + en: 'Italian', + nation: '梵蒂冈', + lang: '意大利语' + }, + { + cc: 'vi', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '美属维尔京群岛', + lang: '英语' + }, + { + cc: 've', + code: 'es-VE', + prefix: 'es', + en: 'Spanish', + nation: '委内瑞拉', + lang: '西班牙语' + }, + { + cc: 'ws', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '萨摩亚', + lang: '英语' + }, + { + cc: 'vn', + code: 'vi-VN', + prefix: 'vi', + en: 'Vietnamese', + nation: '越南', + lang: '越南语' + }, + { + cc: 'vg', + code: 'en-GB', + prefix: 'en', + en: 'English', + nation: '英属维尔京群岛', + lang: '英语' + }, + { + cc: 'yt', + code: 'fr-FR', + prefix: 'fr', + en: 'French', + nation: '马约特', + lang: '法语' + }, + { + cc: 'ye', + code: 'ar-YE', + prefix: 'ar', + en: 'Arabic', + nation: '也门', + lang: '阿拉伯语' + }, + { + cc: 'wf', + code: 'fr-FR', + prefix: 'fr', + en: 'French', + nation: '瓦利斯和富图纳', + lang: '法语' + }, + { + cc: 'zw', + code: 'en-ZW', + prefix: 'en', + en: 'English', + nation: '津巴布韦', + lang: '英语' + }, + { + cc: 'za', + code: 'nso-ZA', + prefix: 'nso', + en: 'Basotho', + nation: '南非', + lang: '巴索托语' + }, + { + cc: 'zm', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '赞比亚', + lang: '英语' + }, + { + cc: 'cn', + code: 'zh-CN', + prefix: 'zh', + en: 'Chinese', + nation: '中国内地', + lang: '中文' + }, + { + cc: 'cd', + code: 'fr-FR', + prefix: 'fr', + en: 'French', + nation: '刚果(金)', + lang: '法语' + }, + { + cc: 'mz', + code: 'pt-PT', + prefix: 'pt', + en: 'Portuguese', + nation: '莫桑比克', + lang: '葡萄牙语' + }, + { + cc: 'cg', + code: 'fr-FR', + prefix: 'fr', + en: 'French', + nation: '刚果(布)', + lang: '法语' + }, + { + cc: 'gg', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '根西岛', + lang: '英语' + }, + { + cc: 'gm', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '冈比亚', + lang: '英语' + }, + { + cc: 'mp', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '北马里亚纳群岛', + lang: '英语' + }, + { + cc: 'et', + code: 'am-ET', + prefix: 'am', + en: 'Amharic', + nation: '埃塞俄比亚', + lang: '阿姆哈拉语' + }, + { + cc: 'tf', + code: 'fr-FR', + prefix: 'fr', + en: 'French', + nation: '法属南部领地', + lang: '法语' + }, + { + cc: 'vu', + code: 'fr-FR', + prefix: 'fr', + en: 'French', + nation: '瓦努阿图', + lang: '法语' + }, + { + cc: 'vu', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '瓦努阿图', + lang: '英语' + }, + { + cc: 'nc', + code: 'fr-FR', + prefix: 'fr', + en: 'French', + nation: '新喀里多尼亚', + lang: '法语' + }, + { + cc: 'um', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '美国本土外小岛屿', + lang: '英语' + }, + { + cc: 'nu', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '纽埃', + lang: '英语' + }, + { + cc: 'tt', + code: 'en-TT', + prefix: 'en', + en: 'English', + nation: '特立尼达和多巴哥', + lang: '英语' + }, + { + cc: 'vc', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '圣文森特和格林纳丁斯', + lang: '英语' + }, + { + cc: 'ck', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '库克群岛', + lang: '英语' + }, + { + cc: 'uk', + code: 'en-GB', + prefix: 'en', + en: 'English', + nation: '英国', + lang: '英语' + }, + { + cc: 'tv', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '图瓦卢', + lang: '英语' + }, + { + cc: 'tw', + code: 'zh-TW', + prefix: 'zh', + en: 'Chinese (Traditional)', + nation: '中国台湾', + lang: '中文(繁体)' + }, + { + cc: 'nz', + code: 'en-NZ', + prefix: 'en', + en: 'English', + nation: '新西兰', + lang: '英语' + }, + { + cc: 'nz', + code: 'mi-NZ', + prefix: 'mi', + en: 'Maori', + nation: '新西兰', + lang: '毛利语' + }, + { + cc: 'sa', + code: 'ar-SA', + prefix: 'ar', + en: 'Arabic', + nation: '沙特阿拉伯', + lang: '阿拉伯语' + }, + { + cc: 'kr', + code: 'ko-KR', + prefix: 'ko', + en: 'North Korea', + nation: '韩国', + lang: '朝鲜语' + }, + { + cc: 'kp', + code: 'ko-KR', + prefix: 'ko', + en: 'North Korea', + nation: '朝鲜', + lang: '朝鲜语' + }, + { + cc: 'la', + code: 'lo-LA', + prefix: 'lo', + en: 'Lao', + nation: '老挝', + lang: '老挝语' + }, + { + cc: 'kg', + code: 'ky-KG', + prefix: 'ky', + en: 'Kyrgyz', + nation: '吉尔吉斯斯坦', + lang: '吉尔吉斯语' + }, + { + cc: 'pt', + code: 'pt-PT', + prefix: 'pt', + en: 'Portuguese', + nation: '葡萄牙', + lang: '葡萄牙语' + }, + { + cc: 'kz', + code: 'kk-KZ', + prefix: 'kk', + en: 'Kazakh', + nation: '哈萨克斯坦', + lang: '哈萨克语' + }, + { + cc: 'tj', + code: 'tg-Cyrl-TJ', + prefix: 'tg', + en: 'Tajik Language', + nation: '塔吉克斯坦', + lang: '塔吉克语' + }, + { + cc: 'tm', + code: 'tk-TM', + prefix: 'tk', + en: 'Turkmen', + nation: '土库曼斯坦', + lang: '土库曼语' + }, + { + cc: 'uz', + code: 'uz-Latn-UZ', + prefix: 'uz', + en: 'Uzbek', + nation: '乌兹别克斯坦', + lang: '乌兹别克语' + }, + { + cc: 'sh', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '圣赫勒拿', + lang: '英语' + }, + { + cc: 'pm', + code: 'fr-FR', + prefix: 'fr', + en: 'French', + nation: '圣皮埃尔和密克隆', + lang: '法语' + }, + { + cc: 'kn', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '圣基茨和尼维斯', + lang: '英语' + }, + { + cc: 'lc', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '圣卢西亚', + lang: '英语' + }, + { + cc: 'lc', + code: 'fr-FR', + prefix: 'fr', + en: 'French', + nation: '圣卢西亚', + lang: '法语' + }, + { + cc: 'ci', + code: 'fr-FR', + prefix: 'fr', + en: 'French', + nation: '科特迪瓦', + lang: '法语' + }, + { + cc: 'mu', + code: 'en-US', + prefix: 'en', + en: 'English', + nation: '毛里求斯', + lang: '英语' + }, + { + cc: 'ke', + code: 'sw-KE', + prefix: 'sw', + en: 'Swah Greek', + nation: '肯尼亚', + lang: '斯瓦希里语' + }, + { + cc: 'mn', + code: 'mn-Mong', + prefix: 'mn', + en: 'Mongolian', + nation: '蒙古国蒙古', + lang: '蒙古语' + }, + { + cc: 'rw', + code: 'rw-RW', + prefix: 'rw', + en: 'Rwanda', + nation: '卢旺达', + lang: '卢旺达语' + } +]; + +module.exports = language; \ No newline at end of file diff --git a/localAPI-main/py-examples/example-check-profile-status.py b/localAPI-main/py-examples/example-check-profile-status.py new file mode 100644 index 0000000..3df51af --- /dev/null +++ b/localAPI-main/py-examples/example-check-profile-status.py @@ -0,0 +1,8 @@ +import requests + +profileId = 'XX' +url = "http://localhost:50325/api/v1/browser/active?user_id=" + profileId + +response = requests.request("GET", url, headers={}, data={}) + +print(response.text) diff --git a/localAPI-main/py-examples/example-create-group.py b/localAPI-main/py-examples/example-create-group.py new file mode 100644 index 0000000..9a19ffb --- /dev/null +++ b/localAPI-main/py-examples/example-create-group.py @@ -0,0 +1,15 @@ +import json +import requests + +url = "http://local.adspower.net:50325/api/v1/group/create" + +payload = { + "group_name": "your_group_name" +} +headers = { + 'Content-Type': 'application/json' +} + +response = requests.request("POST", url, headers=headers, json=payload) + +print(response.text) diff --git a/localAPI-main/py-examples/example-create-profile.py b/localAPI-main/py-examples/example-create-profile.py new file mode 100644 index 0000000..6fdd744 --- /dev/null +++ b/localAPI-main/py-examples/example-create-profile.py @@ -0,0 +1,41 @@ +import requests + +url = "http://local.adspower.net:50325/api/v1/user/create" + +payload = { + "name": "test", + "group_id": "0", + "domain_name": "facebook.com", + "repeat_config": [ + "0" + ], + "country": "us", + "fingerprint_config": { + "language": [ + "en-US" + ], + "ua": "Mozilla/5.0 (Linux; Android 8.0.0; BND-AL10 Build/HONORBND-AL10; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/63.0.3239.83 Mobile Safari/537.36 T7/11.5 baiduboxapp/11.5.0.10 (Baidu; P1 8.0.0)", + "flash": "block", + "scan_port_type": "1", + "screen_resolution": "1024_600", + "fonts": [ + "all" + ], + "longitude": "180", + "latitude": "90", + "webrtc": "proxy", + "do_not_track": "true", + "hardware_concurrency": "default", + "device_memory": "default" + }, + "user_proxy_config": { + "proxy_soft": "no_proxy" + } +} +headers = { + 'Content-Type': 'application/json' +} + +response = requests.request("POST", url, headers=headers, json=payload) + +print(response.text) diff --git a/localAPI-main/py-examples/example-delete-profile-cache.py b/localAPI-main/py-examples/example-delete-profile-cache.py new file mode 100644 index 0000000..045ca67 --- /dev/null +++ b/localAPI-main/py-examples/example-delete-profile-cache.py @@ -0,0 +1,10 @@ +import requests + +url = "http://localhost:50325/api/v1/user/delete-cache" + +payload={} +headers = {} + +response = requests.request("POST", url, headers=headers, json=payload) + +print(response.text) diff --git a/localAPI-main/py-examples/example-delete-profile.py b/localAPI-main/py-examples/example-delete-profile.py new file mode 100644 index 0000000..294d916 --- /dev/null +++ b/localAPI-main/py-examples/example-delete-profile.py @@ -0,0 +1,16 @@ +import requests + +url = "http://localhost:50325/api/v1/user/delete" + +payload = { + "user_ids": [ + "XX" + ] +} +headers = { + 'Content-Type': 'application/json' +} + +response = requests.request("POST", url, headers=headers, json=payload) + +print(response.text) diff --git a/localAPI-main/py-examples/example-query-group.py b/localAPI-main/py-examples/example-query-group.py new file mode 100644 index 0000000..0f7acc7 --- /dev/null +++ b/localAPI-main/py-examples/example-query-group.py @@ -0,0 +1,10 @@ +import requests + +url = "http://local.adspower.net:50325/api/v1/group/list?page=1&page_size=15" + +payload={} +headers = {} + +response = requests.request("GET", url, headers=headers, data=payload) + +print(response.text) diff --git a/localAPI-main/py-examples/example-query-profile.py b/localAPI-main/py-examples/example-query-profile.py new file mode 100644 index 0000000..814551e --- /dev/null +++ b/localAPI-main/py-examples/example-query-profile.py @@ -0,0 +1,10 @@ +import requests + +url = "http://local.adspower.net:50325/api/v1/user/list?page=1&page_size=100" + +payload={} +headers = {} + +response = requests.request("GET", url, headers=headers, data=payload) + +print(response.text) diff --git a/localAPI-main/py-examples/example-regroup-profile.py b/localAPI-main/py-examples/example-regroup-profile.py new file mode 100644 index 0000000..0d5e1d9 --- /dev/null +++ b/localAPI-main/py-examples/example-regroup-profile.py @@ -0,0 +1,17 @@ +import requests + +url = "http://local.adspower.net:50325/api/v1/user/regroup" + +payload = { + "user_ids": [ + "XX" + ], + "group_id": "0" +} +headers = { + 'Content-Type': 'application/json' +} + +response = requests.request("POST", url, headers=headers, json=payload) + +print(response.text) diff --git a/localAPI-main/py-examples/example-start-profile.py b/localAPI-main/py-examples/example-start-profile.py new file mode 100644 index 0000000..57c57e0 --- /dev/null +++ b/localAPI-main/py-examples/example-start-profile.py @@ -0,0 +1,27 @@ +# The sample passed the test in selenium version 3.141.0 + +import requests,time +from selenium import webdriver +from selenium.webdriver.chrome.options import Options +import sys + +ads_id = "XXX" +# http://local.adspower.net:50325 Script can go to Profile Management-> click Settings-> click Cache folder-> local_api file to obtain API address +open_url = "http://local.adspower.net:50325/api/v1/browser/start?user_id=" + ads_id +close_url = "http://local.adspower.net:50325/api/v1/browser/stop?user_id=" + ads_id + +resp = requests.get(open_url).json() +if resp["code"] != 0: + print(resp["msg"]) + print("please check ads_id") + sys.exit() + +chrome_driver = resp["data"]["webdriver"] +chrome_options = Options() +chrome_options.add_experimental_option("debuggerAddress", resp["data"]["ws"]["selenium"]) +driver = webdriver.Chrome(chrome_driver, options=chrome_options) +print(driver.title) +driver.get("https://www.adspower.com") +time.sleep(5) +driver.quit() +requests.get(close_url) \ No newline at end of file diff --git a/localAPI-main/py-examples/example-stop-profile.py b/localAPI-main/py-examples/example-stop-profile.py new file mode 100644 index 0000000..3034b02 --- /dev/null +++ b/localAPI-main/py-examples/example-stop-profile.py @@ -0,0 +1,8 @@ +import requests + +profildId = 'XX' +url = "http://local.adspower.net:50325/api/v1/browser/stop?user_id=" + profildId + +response = requests.request("GET", url, headers={}, data={}) + +print(response.text) diff --git a/localAPI-main/py-examples/example-update-profile.py b/localAPI-main/py-examples/example-update-profile.py new file mode 100644 index 0000000..90fa759 --- /dev/null +++ b/localAPI-main/py-examples/example-update-profile.py @@ -0,0 +1,34 @@ +import requests + +url = "http://local.adspower.net:50325/api/v1/user/update" + +payload = { + "user_id": "XX", + "name": "test", + "domain_name": "facebook.com", + "repeat_config": [ + "0" + ], + "open_urls": [ + "http://www.baidu.com", + "https://www.google.com" + ], + "country": "us", + "remark": "remark", + "fingerprint_config": { + "webrtc": "proxy", + "do_not_track": "true", + "hardware_concurrency": "default", + "device_memory": "default" + }, + "user_proxy_config": { + "proxy_soft": "no_proxy" + } +} +headers = { + 'Content-Type': 'application/json' +} + +response = requests.request("POST", url, headers=headers, json=payload) + +print(response.text) diff --git a/localAPI-main/timezone.js b/localAPI-main/timezone.js new file mode 100644 index 0000000..d290222 --- /dev/null +++ b/localAPI-main/timezone.js @@ -0,0 +1,1916 @@ +const timezone = [ + { + tz: 'GMT-09:00', + gmt: 'America/Metlakatla' + }, + { + tz: 'GMT-12:00', + gmt: 'Etc/GMT+12' + }, + { + tz: 'GMT-11:00', + gmt: 'Etc/GMT+11' + }, + { + tz: 'GMT-11:00', + gmt: 'Pacific/Midway' + }, + { + tz: 'GMT-11:00', + gmt: 'Pacific/Niue' + }, + { + tz: 'GMT-11:00', + gmt: 'Pacific/Pago Pago' + }, + { + tz: 'GMT-10:00', + gmt: 'America/Adak' + }, + { + tz: 'GMT-10:00', + gmt: 'Etc/GMT+10' + }, + { + tz: 'GMT-10:00', + gmt: 'HST' + }, + { + tz: 'GMT-10:00', + gmt: 'Pacific/Honolulu' + }, + { + tz: 'GMT-10:00', + gmt: 'Pacific/Rarotonga' + }, + { + tz: 'GMT-10:00', + gmt: 'Pacific/Tahiti' + }, + { + tz: 'GMT-09:30', + gmt: 'Pacific/Marquesas' + }, + { + tz: 'GMT-09:00', + gmt: 'America/Anchorage' + }, + { + tz: 'GMT-09:00', + gmt: 'America/Juneau' + }, + { + tz: 'GMT-09:00', + gmt: 'America/Nome' + }, + { + tz: 'GMT-09:00', + gmt: 'America/Sitka' + }, + { + tz: 'GMT-09:00', + gmt: 'America/Yakutat' + }, + { + tz: 'GMT-09:00', + gmt: 'Etc/GMT+9' + }, + { + tz: 'GMT-09:00', + gmt: 'Pacific/Gambier' + }, + { + tz: 'GMT-08:00', + gmt: 'America/Los Angeles' + }, + { + tz: 'GMT-08:00', + gmt: 'America/Tijuana' + }, + { + tz: 'GMT-08:00', + gmt: 'America/Vancouver' + }, + { + tz: 'GMT-08:00', + gmt: 'Etc/GMT+8' + }, + { + tz: 'GMT-08:00', + gmt: 'PST8PDT' + }, + { + tz: 'GMT-08:00', + gmt: 'Pacific/Pitcairn' + }, + { + tz: 'GMT-07:00', + gmt: 'America/Boise' + }, + { + tz: 'GMT-07:00', + gmt: 'America/Cambridge Bay' + }, + { + tz: 'GMT-07:00', + gmt: 'America/Chihuahua' + }, + { + tz: 'GMT-07:00', + gmt: 'America/Creston' + }, + { + tz: 'GMT-07:00', + gmt: 'America/Dawson' + }, + { + tz: 'GMT-07:00', + gmt: 'America/Dawson Creek' + }, + { + tz: 'GMT-07:00', + gmt: 'America/Denver' + }, + { + tz: 'GMT-07:00', + gmt: 'America/Edmonton' + }, + { + tz: 'GMT-07:00', + gmt: 'America/Fort Nelson' + }, + { + tz: 'GMT-07:00', + gmt: 'America/Hermosillo' + }, + { + tz: 'GMT-07:00', + gmt: 'America/Inuvik' + }, + { + tz: 'GMT-07:00', + gmt: 'America/Mazatlan' + }, + { + tz: 'GMT-07:00', + gmt: 'America/Ojinaga' + }, + { + tz: 'GMT-07:00', + gmt: 'America/Phoenix' + }, + { + tz: 'GMT-07:00', + gmt: 'America/Whitehorse' + }, + { + tz: 'GMT-07:00', + gmt: 'America/Yellowknife' + }, + { + tz: 'GMT-07:00', + gmt: 'Etc/GMT+7' + }, + { + tz: 'GMT-07:00', + gmt: 'MST' + }, + { + tz: 'GMT-07:00', + gmt: 'MST7MDT' + }, + { + tz: 'GMT-06:00', + gmt: 'America/Bahia Banderas' + }, + { + tz: 'GMT-06:00', + gmt: 'America/Belize' + }, + { + tz: 'GMT-06:00', + gmt: 'America/Chicago' + }, + { + tz: 'GMT-06:00', + gmt: 'America/Costa Rica' + }, + { + tz: 'GMT-06:00', + gmt: 'America/El Salvador' + }, + { + tz: 'GMT-06:00', + gmt: 'America/Guatemala' + }, + { + tz: 'GMT-06:00', + gmt: 'America/Indiana/Knox' + }, + { + tz: 'GMT-06:00', + gmt: 'America/Indiana/Tell City' + }, + { + tz: 'GMT-06:00', + gmt: 'America/Managua' + }, + { + tz: 'GMT-06:00', + gmt: 'America/Matamoros' + }, + { + tz: 'GMT-06:00', + gmt: 'America/Menominee' + }, + { + tz: 'GMT-06:00', + gmt: 'America/Merida' + }, + { + tz: 'GMT-06:00', + gmt: 'America/Mexico City' + }, + { + tz: 'GMT-06:00', + gmt: 'America/Monterrey' + }, + { + tz: 'GMT-06:00', + gmt: 'America/North Dakota/Beulah' + }, + { + tz: 'GMT-06:00', + gmt: 'America/North Dakota/Center' + }, + { + tz: 'GMT-06:00', + gmt: 'America/North Dakota/New_Salem' + }, + { + tz: 'GMT-06:00', + gmt: 'America/Rainy River' + }, + { + tz: 'GMT-06:00', + gmt: 'America/Rankin Inlet' + }, + { + tz: 'GMT-06:00', + gmt: 'America/Regina' + }, + { + tz: 'GMT-06:00', + gmt: 'America/Resolute' + }, + { + tz: 'GMT-06:00', + gmt: 'America/Swift Current' + }, + { + tz: 'GMT-06:00', + gmt: 'America/Tegucigalpa' + }, + { + tz: 'GMT-06:00', + gmt: 'America/Winnipeg' + }, + { + tz: 'GMT-06:00', + gmt: 'CST6CDT' + }, + { + tz: 'GMT-06:00', + gmt: 'Etc/GMT+6' + }, + { + tz: 'GMT-06:00', + gmt: 'Pacific/Galapagos' + }, + { + tz: 'GMT-05:00', + gmt: 'America/Atikokan' + }, + { + tz: 'GMT-05:00', + gmt: 'America/Bogota' + }, + { + tz: 'GMT-05:00', + gmt: 'America/Cancun' + }, + { + tz: 'GMT-05:00', + gmt: 'America/Cayman' + }, + { + tz: 'GMT-05:00', + gmt: 'America/Detroit' + }, + { + tz: 'GMT-05:00', + gmt: 'America/Eirunepe' + }, + { + tz: 'GMT-05:00', + gmt: 'America/Grand Turk' + }, + { + tz: 'GMT-05:00', + gmt: 'America/Guayaquil' + }, + { + tz: 'GMT-05:00', + gmt: 'America/Havana' + }, + { + tz: 'GMT-05:00', + gmt: 'America/Indiana/Indianapolis' + }, + { + tz: 'GMT-05:00', + gmt: 'America/Indiana/Marengo' + }, + { + tz: 'GMT-05:00', + gmt: 'America/Indiana/Petersburg' + }, + { + tz: 'GMT-05:00', + gmt: 'America/Indiana/Vevay' + }, + { + tz: 'GMT-05:00', + gmt: 'America/Indiana/Vincennes' + }, + { + tz: 'GMT-05:00', + gmt: 'America/Indiana/Winamac' + }, + { + tz: 'GMT-05:00', + gmt: 'America/Indianapolis' + }, + { + tz: 'GMT-05:00', + gmt: 'America/Iqaluit' + }, + { + tz: 'GMT-05:00', + gmt: 'America/Jamaica' + }, + { + tz: 'GMT-05:00', + gmt: 'America/Kentucky/Louisville' + }, + { + tz: 'GMT-05:00', + gmt: 'America/Kentucky/Monticello' + }, + { + tz: 'GMT-05:00', + gmt: 'America/Lima' + }, + { + tz: 'GMT-05:00', + gmt: 'America/Montreal' + }, + { + tz: 'GMT-05:00', + gmt: 'America/Nassau' + }, + { + tz: 'GMT-05:00', + gmt: 'America/New York' + }, + { + tz: 'GMT-05:00', + gmt: 'America/Nipigon' + }, + { + tz: 'GMT-05:00', + gmt: 'America/Panama' + }, + { + tz: 'GMT-05:00', + gmt: 'America/Pangnirtung' + }, + { + tz: 'GMT-05:00', + gmt: 'America/Port-au-Prince' + }, + { + tz: 'GMT-05:00', + gmt: 'America/Rio Branco' + }, + { + tz: 'GMT-05:00', + gmt: 'America/Thunder Bay' + }, + { + tz: 'GMT-05:00', + gmt: 'America/Toronto' + }, + { + tz: 'GMT-05:00', + gmt: 'EST' + }, + { + tz: 'GMT-05:00', + gmt: 'EST5EDT' + }, + { + tz: 'GMT-05:00', + gmt: 'Etc/GMT+5' + }, + { + tz: 'GMT-05:00', + gmt: 'Pacific/Easter' + }, + { + tz: 'GMT-04:00', + gmt: 'America/Anguilla' + }, + { + tz: 'GMT-04:00', + gmt: 'America/Antigua' + }, + { + tz: 'GMT-04:00', + gmt: 'America/Aruba' + }, + { + tz: 'GMT-04:00', + gmt: 'America/Barbados' + }, + { + tz: 'GMT-04:00', + gmt: 'America/Blanc-Sablon' + }, + { + tz: 'GMT-04:00', + gmt: 'America/Boa Vista' + }, + { + tz: 'GMT-04:00', + gmt: 'America/Campo Grande' + }, + { + tz: 'GMT-04:00', + gmt: 'America/Caracas' + }, + { + tz: 'GMT-04:00', + gmt: 'America/Cuiaba' + }, + { + tz: 'GMT-04:00', + gmt: 'America/Curacao' + }, + { + tz: 'GMT-04:00', + gmt: 'America/Dominica' + }, + { + tz: 'GMT-04:00', + gmt: 'America/Glace Bay' + }, + { + tz: 'GMT-04:00', + gmt: 'America/Goose Bay' + }, + { + tz: 'GMT-04:00', + gmt: 'America/Grenada' + }, + { + tz: 'GMT-04:00', + gmt: 'America/Guadeloupe' + }, + { + tz: 'GMT-04:00', + gmt: 'America/Guyana' + }, + { + tz: 'GMT-04:00', + gmt: 'America/Halifax' + }, + { + tz: 'GMT-04:00', + gmt: 'America/Kralendijk' + }, + { + tz: 'GMT-04:00', + gmt: 'America/La Paz' + }, + { + tz: 'GMT-04:00', + gmt: 'America/Lower Princes' + }, + { + tz: 'GMT-04:00', + gmt: 'America/Manaus' + }, + { + tz: 'GMT-04:00', + gmt: 'America/Marigot' + }, + { + tz: 'GMT-04:00', + gmt: 'America/Martinique' + }, + { + tz: 'GMT-04:00', + gmt: 'America/Moncton' + }, + { + tz: 'GMT-04:00', + gmt: 'America/Montserrat' + }, + { + tz: 'GMT-04:00', + gmt: 'America/Port of_Spain' + }, + { + tz: 'GMT-04:00', + gmt: 'America/Porto Velho' + }, + { + tz: 'GMT-04:00', + gmt: 'America/Puerto Rico' + }, + { + tz: 'GMT-04:00', + gmt: 'America/Santo Domingo' + }, + { + tz: 'GMT-04:00', + gmt: 'America/St Barthelemy' + }, + { + tz: 'GMT-04:00', + gmt: 'America/St Kitts' + }, + { + tz: 'GMT-04:00', + gmt: 'America/St Lucia' + }, + { + tz: 'GMT-04:00', + gmt: 'America/St Thomas' + }, + { + tz: 'GMT-04:00', + gmt: 'America/St Vincent' + }, + { + tz: 'GMT-04:00', + gmt: 'America/Thule' + }, + { + tz: 'GMT-04:00', + gmt: 'America/Tortola' + }, + { + tz: 'GMT-04:00', + gmt: 'Atlantic/Bermuda' + }, + { + tz: 'GMT-04:00', + gmt: 'Etc/GMT+4' + }, + { + tz: 'GMT-03:30', + gmt: 'America/St Johns' + }, + { + tz: 'GMT-03:00', + gmt: 'America/Araguaina' + }, + { + tz: 'GMT-03:00', + gmt: 'America/Argentina/Buenos Aires' + }, + { + tz: 'GMT-03:00', + gmt: 'America/Argentina/Catamarca' + }, + { + tz: 'GMT-03:00', + gmt: 'America/Argentina/Cordoba' + }, + { + tz: 'GMT-03:00', + gmt: 'America/Argentina/Jujuy' + }, + { + tz: 'GMT-03:00', + gmt: 'America/Argentina/La Rioja' + }, + { + tz: 'GMT-03:00', + gmt: 'America/Argentina/Mendoza' + }, + { + tz: 'GMT-03:00', + gmt: 'America/Argentina/Rio Gallegos' + }, + { + tz: 'GMT-03:00', + gmt: 'America/Argentina/Salta' + }, + { + tz: 'GMT-03:00', + gmt: 'America/Argentina/San Juan' + }, + { + tz: 'GMT-03:00', + gmt: 'America/Argentina/San Luis' + }, + { + tz: 'GMT-03:00', + gmt: 'America/Argentina/Tucuman' + }, + { + tz: 'GMT-03:00', + gmt: 'America/Argentina/Ushuaia' + }, + { + tz: 'GMT-03:00', + gmt: 'America/Asuncion' + }, + { + tz: 'GMT-03:00', + gmt: 'America/Bahia' + }, + { + tz: 'GMT-03:00', + gmt: 'America/Belem' + }, + { + tz: 'GMT-03:00', + gmt: 'America/Cayenne' + }, + { + tz: 'GMT-03:00', + gmt: 'America/Fortaleza' + }, + { + tz: 'GMT-03:00', + gmt: 'America/Godthab' + }, + { + tz: 'GMT-03:00', + gmt: 'America/Maceio' + }, + { + tz: 'GMT-03:00', + gmt: 'America/Miquelon' + }, + { + tz: 'GMT-03:00', + gmt: 'America/Montevideo' + }, + { + tz: 'GMT-03:00', + gmt: 'America/Nuuk' + }, + { + tz: 'GMT-03:00', + gmt: 'America/Paramaribo' + }, + { + tz: 'GMT-03:00', + gmt: 'America/Punta Arenas' + }, + { + tz: 'GMT-03:00', + gmt: 'America/Recife' + }, + { + tz: 'GMT-03:00', + gmt: 'America/Santarem' + }, + { + tz: 'GMT-03:00', + gmt: 'America/Santiago' + }, + { + tz: 'GMT-03:00', + gmt: 'America/Sao Paulo' + }, + { + tz: 'GMT-03:00', + gmt: 'Antarctica/Palmer' + }, + { + tz: 'GMT-03:00', + gmt: 'Antarctica/Rothera' + }, + { + tz: 'GMT-03:00', + gmt: 'Atlantic/Stanley' + }, + { + tz: 'GMT-03:00', + gmt: 'Etc/GMT+3' + }, + { + tz: 'GMT-02:00', + gmt: 'America/Noronha' + }, + { + tz: 'GMT-02:00', + gmt: 'Atlantic/South Georgia' + }, + { + tz: 'GMT-02:00', + gmt: 'Etc/GMT+2' + }, + { + tz: 'GMT-01:00', + gmt: 'America/Scoresbysund' + }, + { + tz: 'GMT-01:00', + gmt: 'Atlantic/Azores' + }, + { + tz: 'GMT-01:00', + gmt: 'Atlantic/Cape Verde' + }, + { + tz: 'GMT-01:00', + gmt: 'Etc/GMT+1' + }, + { + tz: 'GMT+00:00', + gmt: 'Africa/Abidjan' + }, + { + tz: 'GMT+00:00', + gmt: 'Africa/Accra' + }, + { + tz: 'GMT+00:00', + gmt: 'Africa/Bamako' + }, + { + tz: 'GMT+00:00', + gmt: 'Africa/Banjul' + }, + { + tz: 'GMT+00:00', + gmt: 'Africa/Bissau' + }, + { + tz: 'GMT+00:00', + gmt: 'Africa/Conakry' + }, + { + tz: 'GMT+00:00', + gmt: 'Africa/Dakar' + }, + { + tz: 'GMT+00:00', + gmt: 'Africa/Freetown' + }, + { + tz: 'GMT+00:00', + gmt: 'Africa/Lome' + }, + { + tz: 'GMT+00:00', + gmt: 'Africa/Monrovia' + }, + { + tz: 'GMT+00:00', + gmt: 'Africa/Nouakchott' + }, + { + tz: 'GMT+00:00', + gmt: 'Africa/Ouagadougou' + }, + { + tz: 'GMT+00:00', + gmt: 'Africa/Sao Tome' + }, + { + tz: 'GMT+00:00', + gmt: 'America/Danmarkshavn' + }, + { + tz: 'GMT+00:00', + gmt: 'Antarctica/Troll' + }, + { + tz: 'GMT+00:00', + gmt: 'Atlantic/Canary' + }, + { + tz: 'GMT+00:00', + gmt: 'Atlantic/Faroe' + }, + { + tz: 'GMT+00:00', + gmt: 'Atlantic/Madeira' + }, + { + tz: 'GMT+00:00', + gmt: 'Atlantic/Reykjavik' + }, + { + tz: 'GMT+00:00', + gmt: 'Atlantic/St Helena' + }, + { + tz: 'GMT+00:00', + gmt: 'Etc/GMT' + }, + { + tz: 'GMT+00:00', + gmt: 'Etc/GMT+0' + }, + { + tz: 'GMT+00:00', + gmt: 'Etc/GMT-0' + }, + { + tz: 'GMT+00:00', + gmt: 'Etc/GMT0' + }, + { + tz: 'GMT+00:00', + gmt: 'Etc/Greenwich' + }, + { + tz: 'GMT+00:00', + gmt: 'Etc/Universal' + }, + { + tz: 'GMT+00:00', + gmt: 'Etc/Zulu' + }, + { + tz: 'GMT+00:00', + gmt: 'Europe/Dublin' + }, + { + tz: 'GMT+00:00', + gmt: 'Europe/Guernsey' + }, + { + tz: 'GMT+00:00', + gmt: 'Europe/Isle of_Man' + }, + { + tz: 'GMT+00:00', + gmt: 'Europe/Jersey' + }, + { + tz: 'GMT+00:00', + gmt: 'Europe/Lisbon' + }, + { + tz: 'GMT+00:00', + gmt: 'Europe/London' + }, + { + tz: 'GMT+00:00', + gmt: 'GMT' + }, + { + tz: 'GMT+00:00', + gmt: 'UTC' + }, + { + tz: 'GMT+00:00', + gmt: 'WET' + }, + { + tz: 'GMT+01:00', + gmt: 'Africa/Algiers' + }, + { + tz: 'GMT+01:00', + gmt: 'Africa/Bangui' + }, + { + tz: 'GMT+01:00', + gmt: 'Africa/Brazzaville' + }, + { + tz: 'GMT+01:00', + gmt: 'Africa/Casablanca' + }, + { + tz: 'GMT+01:00', + gmt: 'Africa/Ceuta' + }, + { + tz: 'GMT+01:00', + gmt: 'Africa/Douala' + }, + { + tz: 'GMT+01:00', + gmt: 'Africa/El Aaiun' + }, + { + tz: 'GMT+01:00', + gmt: 'Africa/Kinshasa' + }, + { + tz: 'GMT+01:00', + gmt: 'Africa/Lagos' + }, + { + tz: 'GMT+01:00', + gmt: 'Africa/Libreville' + }, + { + tz: 'GMT+01:00', + gmt: 'Africa/Luanda' + }, + { + tz: 'GMT+01:00', + gmt: 'Africa/Malabo' + }, + { + tz: 'GMT+01:00', + gmt: 'Africa/Ndjamena' + }, + { + tz: 'GMT+01:00', + gmt: 'Africa/Niamey' + }, + { + tz: 'GMT+01:00', + gmt: 'Africa/Porto-Novo' + }, + { + tz: 'GMT+01:00', + gmt: 'Africa/Tunis' + }, + { + tz: 'GMT+01:00', + gmt: 'Arctic/Longyearbyen' + }, + { + tz: 'GMT+01:00', + gmt: 'CET' + }, + { + tz: 'GMT+01:00', + gmt: 'Etc/GMT-1' + }, + { + tz: 'GMT+01:00', + gmt: 'Europe/Amsterdam' + }, + { + tz: 'GMT+01:00', + gmt: 'Europe/Andorra' + }, + { + tz: 'GMT+01:00', + gmt: 'Europe/Belgrade' + }, + { + tz: 'GMT+01:00', + gmt: 'Europe/Berlin' + }, + { + tz: 'GMT+01:00', + gmt: 'Europe/Bratislava' + }, + { + tz: 'GMT+01:00', + gmt: 'Europe/Brussels' + }, + { + tz: 'GMT+01:00', + gmt: 'Europe/Budapest' + }, + { + tz: 'GMT+01:00', + gmt: 'Europe/Busingen' + }, + { + tz: 'GMT+01:00', + gmt: 'Europe/Copenhagen' + }, + { + tz: 'GMT+01:00', + gmt: 'Europe/Gibraltar' + }, + { + tz: 'GMT+01:00', + gmt: 'Europe/Ljubljana' + }, + { + tz: 'GMT+01:00', + gmt: 'Europe/Luxembourg' + }, + { + tz: 'GMT+01:00', + gmt: 'Europe/Madrid' + }, + { + tz: 'GMT+01:00', + gmt: 'Europe/Malta' + }, + { + tz: 'GMT+01:00', + gmt: 'Europe/Monaco' + }, + { + tz: 'GMT+01:00', + gmt: 'Europe/Oslo' + }, + { + tz: 'GMT+01:00', + gmt: 'Europe/Paris' + }, + { + tz: 'GMT+01:00', + gmt: 'Europe/Podgorica' + }, + { + tz: 'GMT+01:00', + gmt: 'Europe/Prague' + }, + { + tz: 'GMT+01:00', + gmt: 'Europe/Rome' + }, + { + tz: 'GMT+01:00', + gmt: 'Europe/San Marino' + }, + { + tz: 'GMT+01:00', + gmt: 'Europe/Sarajevo' + }, + { + tz: 'GMT+01:00', + gmt: 'Europe/Skopje' + }, + { + tz: 'GMT+01:00', + gmt: 'Europe/Stockholm' + }, + { + tz: 'GMT+01:00', + gmt: 'Europe/Tirane' + }, + { + tz: 'GMT+01:00', + gmt: 'Europe/Vaduz' + }, + { + tz: 'GMT+01:00', + gmt: 'Europe/Vatican' + }, + { + tz: 'GMT+01:00', + gmt: 'Europe/Vienna' + }, + { + tz: 'GMT+01:00', + gmt: 'Europe/Warsaw' + }, + { + tz: 'GMT+01:00', + gmt: 'Europe/Zagreb' + }, + { + tz: 'GMT+01:00', + gmt: 'Europe/Zurich' + }, + { + tz: 'GMT+01:00', + gmt: 'MET' + }, + { + tz: 'GMT+02:00', + gmt: 'Africa/Blantyre' + }, + { + tz: 'GMT+02:00', + gmt: 'Africa/Bujumbura' + }, + { + tz: 'GMT+02:00', + gmt: 'Africa/Cairo' + }, + { + tz: 'GMT+02:00', + gmt: 'Africa/Gaborone' + }, + { + tz: 'GMT+02:00', + gmt: 'Africa/Harare' + }, + { + tz: 'GMT+02:00', + gmt: 'Africa/Johannesburg' + }, + { + tz: 'GMT+02:00', + gmt: 'Africa/Khartoum' + }, + { + tz: 'GMT+02:00', + gmt: 'Africa/Kigali' + }, + { + tz: 'GMT+02:00', + gmt: 'Africa/Lubumbashi' + }, + { + tz: 'GMT+02:00', + gmt: 'Africa/Lusaka' + }, + { + tz: 'GMT+02:00', + gmt: 'Africa/Maputo' + }, + { + tz: 'GMT+02:00', + gmt: 'Africa/Maseru' + }, + { + tz: 'GMT+02:00', + gmt: 'Africa/Mbabane' + }, + { + tz: 'GMT+02:00', + gmt: 'Africa/Tripoli' + }, + { + tz: 'GMT+02:00', + gmt: 'Africa/Windhoek' + }, + { + tz: 'GMT+02:00', + gmt: 'Asia/Amman' + }, + { + tz: 'GMT+02:00', + gmt: 'Asia/Beirut' + }, + { + tz: 'GMT+02:00', + gmt: 'Asia/Damascus' + }, + { + tz: 'GMT+02:00', + gmt: 'Asia/Famagusta' + }, + { + tz: 'GMT+02:00', + gmt: 'Asia/Gaza' + }, + { + tz: 'GMT+02:00', + gmt: 'Asia/Hebron' + }, + { + tz: 'GMT+02:00', + gmt: 'Asia/Jerusalem' + }, + { + tz: 'GMT+02:00', + gmt: 'Asia/Nicosia' + }, + { + tz: 'GMT+02:00', + gmt: 'EET' + }, + { + tz: 'GMT+02:00', + gmt: 'Etc/GMT-2' + }, + { + tz: 'GMT+02:00', + gmt: 'Europe/Athens' + }, + { + tz: 'GMT+02:00', + gmt: 'Europe/Bucharest' + }, + { + tz: 'GMT+02:00', + gmt: 'Europe/Chisinau' + }, + { + tz: 'GMT+02:00', + gmt: 'Europe/Helsinki' + }, + { + tz: 'GMT+02:00', + gmt: 'Europe/Kaliningrad' + }, + { + tz: 'GMT+02:00', + gmt: 'Europe/Kiev' + }, + { + tz: 'GMT+02:00', + gmt: 'Europe/Mariehamn' + }, + { + tz: 'GMT+02:00', + gmt: 'Europe/Nicosia' + }, + { + tz: 'GMT+02:00', + gmt: 'Europe/Riga' + }, + { + tz: 'GMT+02:00', + gmt: 'Europe/Sofia' + }, + { + tz: 'GMT+02:00', + gmt: 'Europe/Tallinn' + }, + { + tz: 'GMT+02:00', + gmt: 'Europe/Uzhgorod' + }, + { + tz: 'GMT+02:00', + gmt: 'Europe/Vilnius' + }, + { + tz: 'GMT+02:00', + gmt: 'Europe/Zaporozhye' + }, + { + tz: 'GMT+03:00', + gmt: 'Africa/Addis Ababa' + }, + { + tz: 'GMT+03:00', + gmt: 'Africa/Asmara' + }, + { + tz: 'GMT+03:00', + gmt: 'Africa/Dar es_Salaam' + }, + { + tz: 'GMT+03:00', + gmt: 'Africa/Djibouti' + }, + { + tz: 'GMT+03:00', + gmt: 'Africa/Juba' + }, + { + tz: 'GMT+03:00', + gmt: 'Africa/Kampala' + }, + { + tz: 'GMT+03:00', + gmt: 'Africa/Mogadishu' + }, + { + tz: 'GMT+03:00', + gmt: 'Africa/Nairobi' + }, + { + tz: 'GMT+03:00', + gmt: 'Antarctica/Syowa' + }, + { + tz: 'GMT+03:00', + gmt: 'Asia/Aden' + }, + { + tz: 'GMT+03:00', + gmt: 'Asia/Baghdad' + }, + { + tz: 'GMT+03:00', + gmt: 'Asia/Bahrain' + }, + { + tz: 'GMT+03:00', + gmt: 'Asia/Istanbul' + }, + { + tz: 'GMT+03:00', + gmt: 'Asia/Kuwait' + }, + { + tz: 'GMT+03:00', + gmt: 'Asia/Qatar' + }, + { + tz: 'GMT+03:00', + gmt: 'Asia/Riyadh' + }, + { + tz: 'GMT+03:00', + gmt: 'Etc/GMT-3' + }, + { + tz: 'GMT+03:00', + gmt: 'Europe/Istanbul' + }, + { + tz: 'GMT+03:00', + gmt: 'Europe/Kirov' + }, + { + tz: 'GMT+03:00', + gmt: 'Europe/Minsk' + }, + { + tz: 'GMT+03:00', + gmt: 'Europe/Moscow' + }, + { + tz: 'GMT+03:00', + gmt: 'Europe/Simferopol' + }, + { + tz: 'GMT+03:00', + gmt: 'Indian/Antananarivo' + }, + { + tz: 'GMT+03:00', + gmt: 'Indian/Comoro' + }, + { + tz: 'GMT+03:00', + gmt: 'Indian/Mayotte' + }, + { + tz: 'GMT+03:30', + gmt: 'Asia/Tehran' + }, + { + tz: 'GMT+04:00', + gmt: 'Asia/Baku' + }, + { + tz: 'GMT+04:00', + gmt: 'Asia/Dubai' + }, + { + tz: 'GMT+04:00', + gmt: 'Asia/Muscat' + }, + { + tz: 'GMT+04:00', + gmt: 'Asia/Tbilisi' + }, + { + tz: 'GMT+04:00', + gmt: 'Asia/Yerevan' + }, + { + tz: 'GMT+04:00', + gmt: 'Etc/GMT-4' + }, + { + tz: 'GMT+04:00', + gmt: 'Europe/Astrakhan' + }, + { + tz: 'GMT+04:00', + gmt: 'Europe/Samara' + }, + { + tz: 'GMT+04:00', + gmt: 'Europe/Saratov' + }, + { + tz: 'GMT+04:00', + gmt: 'Europe/Ulyanovsk' + }, + { + tz: 'GMT+04:00', + gmt: 'Europe/Volgograd' + }, + { + tz: 'GMT+04:00', + gmt: 'Indian/Mahe' + }, + { + tz: 'GMT+04:00', + gmt: 'Indian/Mauritius' + }, + { + tz: 'GMT+04:00', + gmt: 'Indian/Reunion' + }, + { + tz: 'GMT+04:30', + gmt: 'Asia/Kabul' + }, + { + tz: 'GMT+05:00', + gmt: 'Antarctica/Mawson' + }, + { + tz: 'GMT+05:00', + gmt: 'Asia/Aqtau' + }, + { + tz: 'GMT+05:00', + gmt: 'Asia/Aqtobe' + }, + { + tz: 'GMT+05:00', + gmt: 'Asia/Ashgabat' + }, + { + tz: 'GMT+05:00', + gmt: 'Asia/Atyrau' + }, + { + tz: 'GMT+05:00', + gmt: 'Asia/Dushanbe' + }, + { + tz: 'GMT+05:00', + gmt: 'Asia/Karachi' + }, + { + tz: 'GMT+05:00', + gmt: 'Asia/Oral' + }, + { + tz: 'GMT+05:00', + gmt: 'Asia/Qyzylorda' + }, + { + tz: 'GMT+05:00', + gmt: 'Asia/Samarkand' + }, + { + tz: 'GMT+05:00', + gmt: 'Asia/Tashkent' + }, + { + tz: 'GMT+05:00', + gmt: 'Asia/Yekaterinburg' + }, + { + tz: 'GMT+05:00', + gmt: 'Etc/GMT-5' + }, + { + tz: 'GMT+05:00', + gmt: 'Indian/Kerguelen' + }, + { + tz: 'GMT+05:00', + gmt: 'Indian/Maldives' + }, + { + tz: 'GMT+05:30', + gmt: 'Asia/Calcutta' + }, + { + tz: 'GMT+05:30', + gmt: 'Asia/Colombo' + }, + { + tz: 'GMT+05:30', + gmt: 'Asia/Kolkata' + }, + { + tz: 'GMT+05:45', + gmt: 'Asia/Kathmandu' + }, + { + tz: 'GMT+05:45', + gmt: 'Asia/Katmandu' + }, + { + tz: 'GMT+06:00', + gmt: 'Antarctica/Vostok' + }, + { + tz: 'GMT+06:00', + gmt: 'Asia/Almaty' + }, + { + tz: 'GMT+06:00', + gmt: 'Asia/Bishkek' + }, + { + tz: 'GMT+06:00', + gmt: 'Asia/Dhaka' + }, + { + tz: 'GMT+06:00', + gmt: 'Asia/Omsk' + }, + { + tz: 'GMT+06:00', + gmt: 'Asia/Qostanay' + }, + { + tz: 'GMT+06:00', + gmt: 'Asia/Thimphu' + }, + { + tz: 'GMT+06:00', + gmt: 'Asia/Urumqi' + }, + { + tz: 'GMT+06:00', + gmt: 'Etc/GMT-6' + }, + { + tz: 'GMT+06:00', + gmt: 'Indian/Chagos' + }, + { + tz: 'GMT+06:30', + gmt: 'Asia/Yangon' + }, + { + tz: 'GMT+06:30', + gmt: 'Indian/Cocos' + }, + { + tz: 'GMT+07:00', + gmt: 'Antarctica/Davis' + }, + { + tz: 'GMT+07:00', + gmt: 'Asia/Bangkok' + }, + { + tz: 'GMT+07:00', + gmt: 'Asia/Barnaul' + }, + { + tz: 'GMT+07:00', + gmt: 'Asia/Ho Chi_Minh' + }, + { + tz: 'GMT+07:00', + gmt: 'Asia/Hovd' + }, + { + tz: 'GMT+07:00', + gmt: 'Asia/Jakarta' + }, + { + tz: 'GMT+07:00', + gmt: 'Asia/Krasnoyarsk' + }, + { + tz: 'GMT+07:00', + gmt: 'Asia/Novokuznetsk' + }, + { + tz: 'GMT+07:00', + gmt: 'Asia/Novosibirsk' + }, + { + tz: 'GMT+07:00', + gmt: 'Asia/Phnom Penh' + }, + { + tz: 'GMT+07:00', + gmt: 'Asia/Pontianak' + }, + { + tz: 'GMT+07:00', + gmt: 'Asia/Tomsk' + }, + { + tz: 'GMT+07:00', + gmt: 'Asia/Vientiane' + }, + { + tz: 'GMT+07:00', + gmt: 'Etc/GMT-7' + }, + { + tz: 'GMT+07:00', + gmt: 'Indian/Christmas' + }, + { + tz: 'GMT+08:00', + gmt: 'Asia/Brunei' + }, + { + tz: 'GMT+08:00', + gmt: 'Asia/Choibalsan' + }, + { + tz: 'GMT+08:00', + gmt: 'Asia/Hong Kong' + }, + { + tz: 'GMT+08:00', + gmt: 'Asia/Irkutsk' + }, + { + tz: 'GMT+08:00', + gmt: 'Asia/Kuala Lumpur' + }, + { + tz: 'GMT+08:00', + gmt: 'Asia/Kuching' + }, + { + tz: 'GMT+08:00', + gmt: 'Asia/Macau' + }, + { + tz: 'GMT+08:00', + gmt: 'Asia/Makassar' + }, + { + tz: 'GMT+08:00', + gmt: 'Asia/Manila' + }, + { + tz: 'GMT+08:00', + gmt: 'Asia/Shanghai' + }, + { + tz: 'GMT+08:00', + gmt: 'Asia/Singapore' + }, + { + tz: 'GMT+08:00', + gmt: 'Asia/Taipei' + }, + { + tz: 'GMT+08:00', + gmt: 'Asia/Ulaanbaatar' + }, + { + tz: 'GMT+08:00', + gmt: 'Australia/Perth' + }, + { + tz: 'GMT+08:00', + gmt: 'Etc/GMT-8' + }, + { + tz: 'GMT+08:45', + gmt: 'Australia/Eucla' + }, + { + tz: 'GMT+09:00', + gmt: 'Asia/Chita' + }, + { + tz: 'GMT+09:00', + gmt: 'Asia/Dili' + }, + { + tz: 'GMT+09:00', + gmt: 'Asia/Jayapura' + }, + { + tz: 'GMT+09:00', + gmt: 'Asia/Khandyga' + }, + { + tz: 'GMT+09:00', + gmt: 'Asia/Pyongyang' + }, + { + tz: 'GMT+09:00', + gmt: 'Asia/Seoul' + }, + { + tz: 'GMT+09:00', + gmt: 'Asia/Tokyo' + }, + { + tz: 'GMT+09:00', + gmt: 'Asia/Yakutsk' + }, + { + tz: 'GMT+09:00', + gmt: 'Etc/GMT-9' + }, + { + tz: 'GMT+09:00', + gmt: 'Pacific/Palau' + }, + { + tz: 'GMT+09:30', + gmt: 'Australia/Darwin' + }, + { + tz: 'GMT+10:00', + gmt: 'Antarctica/DumontDUrville' + }, + { + tz: 'GMT+10:00', + gmt: 'Asia/Ust-Nera' + }, + { + tz: 'GMT+10:00', + gmt: 'Asia/Vladivostok' + }, + { + tz: 'GMT+10:00', + gmt: 'Australia/Brisbane' + }, + { + tz: 'GMT+10:00', + gmt: 'Australia/Lindeman' + }, + { + tz: 'GMT+10:00', + gmt: 'Etc/GMT-10' + }, + { + tz: 'GMT+10:00', + gmt: 'Pacific/Chuuk' + }, + { + tz: 'GMT+10:00', + gmt: 'Pacific/Guam' + }, + { + tz: 'GMT+10:00', + gmt: 'Pacific/Port Moresby' + }, + { + tz: 'GMT+10:00', + gmt: 'Pacific/Saipan' + }, + { + tz: 'GMT+10:30', + gmt: 'Australia/Adelaide' + }, + { + tz: 'GMT+10:30', + gmt: 'Australia/Broken Hill' + }, + { + tz: 'GMT+11:00', + gmt: 'Antarctica/Casey' + }, + { + tz: 'GMT+11:00', + gmt: 'Antarctica/Macquarie' + }, + { + tz: 'GMT+11:00', + gmt: 'Asia/Magadan' + }, + { + tz: 'GMT+11:00', + gmt: 'Asia/Sakhalin' + }, + { + tz: 'GMT+11:00', + gmt: 'Asia/Srednekolymsk' + }, + { + tz: 'GMT+11:00', + gmt: 'Australia/Currie' + }, + { + tz: 'GMT+11:00', + gmt: 'Australia/Hobart' + }, + { + tz: 'GMT+11:00', + gmt: 'Australia/Lord Howe' + }, + { + tz: 'GMT+11:00', + gmt: 'Australia/Melbourne' + }, + { + tz: 'GMT+11:00', + gmt: 'Australia/Sydney' + }, + { + tz: 'GMT+11:00', + gmt: 'Etc/GMT-11' + }, + { + tz: 'GMT+11:00', + gmt: 'Pacific/Bougainville' + }, + { + tz: 'GMT+11:00', + gmt: 'Pacific/Efate' + }, + { + tz: 'GMT+11:00', + gmt: 'Pacific/Guadalcanal' + }, + { + tz: 'GMT+11:00', + gmt: 'Pacific/Kosrae' + }, + { + tz: 'GMT+11:00', + gmt: 'Pacific/Noumea' + }, + { + tz: 'GMT+11:00', + gmt: 'Pacific/Pohnpei' + }, + { + tz: 'GMT+12:00', + gmt: 'Asia/Anadyr' + }, + { + tz: 'GMT+12:00', + gmt: 'Asia/Kamchatka' + }, + { + tz: 'GMT+12:00', + gmt: 'Etc/GMT-12' + }, + { + tz: 'GMT+12:00', + gmt: 'Pacific/Fiji' + }, + { + tz: 'GMT+12:00', + gmt: 'Pacific/Funafuti' + }, + { + tz: 'GMT+12:00', + gmt: 'Pacific/Kwajalein' + }, + { + tz: 'GMT+12:00', + gmt: 'Pacific/Majuro' + }, + { + tz: 'GMT+12:00', + gmt: 'Pacific/Nauru' + }, + { + tz: 'GMT+12:00', + gmt: 'Pacific/Norfolk' + }, + { + tz: 'GMT+12:00', + gmt: 'Pacific/Tarawa' + }, + { + tz: 'GMT+12:00', + gmt: 'Pacific/Wake' + }, + { + tz: 'GMT+12:00', + gmt: 'Pacific/Wallis' + }, + { + tz: 'GMT+13:00', + gmt: 'Antarctica/McMurdo' + }, + { + tz: 'GMT+13:00', + gmt: 'Etc/GMT-13' + }, + { + tz: 'GMT+13:00', + gmt: 'Pacific/Auckland' + }, + { + tz: 'GMT+13:00', + gmt: 'Pacific/Enderbury' + }, + { + tz: 'GMT+13:00', + gmt: 'Pacific/Fakaofo' + }, + { + tz: 'GMT+13:00', + gmt: 'Pacific/Tongatapu' + }, + { + tz: 'GMT+13:45', + gmt: 'Pacific/Chatham' + }, + { + tz: 'GMT+14:00', + gmt: 'Etc/GMT-14' + }, + { + tz: 'GMT+14:00', + gmt: 'Pacific/Apia' + }, + { + tz: 'GMT+14:00', + gmt: 'Pacific/Kiritimati' + } +]; + +module.exports = timezone; \ No newline at end of file diff --git a/postman_collection.json b/postman_collection.json new file mode 100644 index 0000000..c261cce --- /dev/null +++ b/postman_collection.json @@ -0,0 +1,166 @@ +{ + "info": { + "name": "MIP广告点击服务 API", + "description": "MIP页面广告自动化点击服务的API接口集合", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "健康检查", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/health", + "host": ["{{base_url}}"], + "path": ["health"] + } + } + }, + { + "name": "添加单个URL", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"url\": \"https://example.com/mip-page\"\n}" + }, + "url": { + "raw": "{{base_url}}/api/urls", + "host": ["{{base_url}}"], + "path": ["api", "urls"] + } + } + }, + { + "name": "批量添加URL", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"urls\": [\n \"https://example.com/mip-page-1\",\n \"https://example.com/mip-page-2\",\n \"https://example.com/mip-page-3\"\n ]\n}" + }, + "url": { + "raw": "{{base_url}}/api/urls", + "host": ["{{base_url}}"], + "path": ["api", "urls"] + } + } + }, + { + "name": "获取所有URL", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/api/urls", + "host": ["{{base_url}}"], + "path": ["api", "urls"] + } + } + }, + { + "name": "获取URL详情", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/api/urls/https://example.com/mip-page", + "host": ["{{base_url}}"], + "path": ["api", "urls", "https://example.com/mip-page"] + } + } + }, + { + "name": "删除URL", + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{base_url}}/api/urls/https://example.com/mip-page", + "host": ["{{base_url}}"], + "path": ["api", "urls", "https://example.com/mip-page"] + } + } + }, + { + "name": "重置URL", + "request": { + "method": "POST", + "header": [], + "url": { + "raw": "{{base_url}}/api/urls/https://example.com/mip-page/reset", + "host": ["{{base_url}}"], + "path": ["api", "urls", "https://example.com/mip-page", "reset"] + } + } + }, + { + "name": "获取统计数据", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/api/statistics", + "host": ["{{base_url}}"], + "path": ["api", "statistics"] + } + } + }, + { + "name": "启动调度器", + "request": { + "method": "POST", + "header": [], + "url": { + "raw": "{{base_url}}/api/scheduler/start", + "host": ["{{base_url}}"], + "path": ["api", "scheduler", "start"] + } + } + }, + { + "name": "停止调度器", + "request": { + "method": "POST", + "header": [], + "url": { + "raw": "{{base_url}}/api/scheduler/stop", + "host": ["{{base_url}}"], + "path": ["api", "scheduler", "stop"] + } + } + }, + { + "name": "查询调度器状态", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/api/scheduler/status", + "host": ["{{base_url}}"], + "path": ["api", "scheduler", "status"] + } + } + } + ], + "variable": [ + { + "key": "base_url", + "value": "http://localhost:5000", + "type": "string" + } + ] +} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..1778041 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,9 @@ +playwright>=1.35.0 +requests>=2.28.0 +urllib3<2.0 +flask>=2.0.0,<2.3.0 +flask-cors>=3.0.0 +apscheduler>=3.9.0 +python-dotenv>=0.19.0 +pyyaml>=5.4.0 +loguru>=0.6.0 diff --git a/scheduler.py b/scheduler.py new file mode 100644 index 0000000..7e1a345 --- /dev/null +++ b/scheduler.py @@ -0,0 +1,215 @@ +import random +import time +from datetime import datetime, timedelta +from typing import List, Dict +from threading import Thread, Lock +from loguru import logger + +from adspower_client import AdsPowerClient +from ad_automation import MIPAdAutomation +from data_manager import DataManager +from config import Config + + +class ClickScheduler: + """点击任务调度器""" + + def __init__(self): + self.adspower_client = AdsPowerClient() + self.data_manager = DataManager() + self.running = False + self.lock = Lock() + + def add_url(self, url: str) -> bool: + """ + 添加待点击的URL + + Args: + url: MIP页面链接 + + Returns: + 是否添加成功 + """ + return self.data_manager.add_url(url) + + def add_urls(self, urls: List[str]) -> int: + """ + 批量添加URL + + Args: + urls: URL列表 + + Returns: + 成功添加的数量 + """ + count = 0 + for url in urls: + if self.add_url(url): + count += 1 + return count + + def start_scheduler(self): + """启动调度器""" + if self.running: + logger.warning("调度器已在运行中") + return + + self.running = True + logger.info("启动点击调度器") + + # 启动调度线程 + thread = Thread(target=self._schedule_loop, daemon=True) + thread.start() + + def stop_scheduler(self): + """停止调度器""" + self.running = False + logger.info("停止点击调度器") + + def _schedule_loop(self): + """调度循环""" + while self.running: + try: + # 检查当前时间是否在工作时间内 + if not self._is_work_time(): + logger.debug("当前不在工作时间内,等待...") + time.sleep(60) + continue + + # 获取待处理的URL + url = self._get_next_url() + + if url: + logger.info(f"开始处理URL: {url}") + self._process_url(url) + else: + logger.debug("暂无待处理的URL,等待...") + time.sleep(30) + + except Exception as e: + logger.error(f"调度循环异常: {str(e)}") + time.sleep(10) + + def _is_work_time(self) -> bool: + """ + 检查当前是否在工作时间内 + + Returns: + 是否在工作时间 + """ + now = datetime.now() + current_hour = now.hour + + return Config.WORK_START_HOUR <= current_hour < Config.WORK_END_HOUR + + def _get_next_url(self) -> str: + """ + 获取下一个需要处理的URL + + Returns: + URL或None + """ + with self.lock: + # 获取所有活跃的URL + urls = self.data_manager.get_active_urls() + + for url_data in urls: + url = url_data['url'] + + # 检查是否已达到随机点击次数上限 + click_count = url_data.get('click_count', 0) + target_clicks = url_data.get('target_clicks', 0) + + if click_count >= target_clicks: + # 标记为已完成 + self.data_manager.mark_url_completed(url) + continue + + # 检查距离上次点击是否超过间隔时间 + last_click_time = url_data.get('last_click_time') + if last_click_time: + last_click = datetime.fromisoformat(last_click_time) + time_diff = datetime.now() - last_click + + if time_diff.total_seconds() < Config.CLICK_INTERVAL_MINUTES * 60: + continue + + return url + + return None + + def _process_url(self, url: str): + """ + 处理单个URL的点击任务 + + Args: + url: 待处理的URL + """ + page = None + + try: + # 启动 AdsPower 浏览器 + browser_info = self.adspower_client.start_browser() + if not browser_info: + logger.error("启动 AdsPower 浏览器失败") + return + + # 通过 CDP 连接到浏览器 + browser = self.adspower_client.connect_browser(browser_info) + if not browser: + logger.error("连接浏览器失败") + return + + # 获取页面 + page = self.adspower_client.get_page(browser) + if not page: + logger.error("获取页面失败") + return + + # 执行广告点击操作 + automation = MIPAdAutomation(page) + click_success, has_reply = automation.check_and_click_ad(url) + + # 更新数据统计 + with self.lock: + if click_success: + self.data_manager.record_click(url, has_reply) + logger.info(f"URL点击成功,获得回复: {has_reply}") + else: + logger.warning(f"URL点击失败: {url}") + + # 随机延迟 + delay = random.randint(10, 30) + time.sleep(delay) + + except Exception as e: + logger.error(f"处理URL异常: {str(e)}") + + finally: + # 停止浏览器(会自动清理 Playwright 资源) + try: + self.adspower_client.stop_browser() + except Exception as e: + logger.error(f"停止浏览器异常: {str(e)}") + + + def get_statistics(self) -> Dict: + """ + 获取统计数据 + + Returns: + 统计数据 + """ + return self.data_manager.get_statistics() + + def get_url_detail(self, url: str) -> Dict: + """ + 获取URL详细信息 + + Args: + url: URL + + Returns: + URL详细信息 + """ + return self.data_manager.get_url_info(url) diff --git a/start.bat b/start.bat new file mode 100644 index 0000000..deab07c --- /dev/null +++ b/start.bat @@ -0,0 +1,58 @@ +@echo off +chcp 65001 >nul +echo ======================================== +echo MIP广告点击服务 - 快速启动脚本 +echo ======================================== +echo. + +REM 检查Python是否安装 +python --version >nul 2>&1 +if %errorlevel% neq 0 ( + echo 错误: 未检测到Python,请先安装Python 3.8+ + pause + exit /b 1 +) + +REM 检查是否存在虚拟环境 +if not exist "venv" ( + echo 未检测到虚拟环境,正在创建... + python -m venv venv + echo 虚拟环境创建完成 + echo. +) + +REM 激活虚拟环境 +echo 激活虚拟环境... +call venv\Scripts\activate.bat + +REM 检查是否已安装依赖 +pip show flask >nul 2>&1 +if %errorlevel% neq 0 ( + echo 正在安装依赖包... + pip install -r requirements.txt + echo 依赖安装完成 + echo. + echo 正在安装 Playwright 浏览器... + python -m playwright install chromium + echo Playwright 浏览器安装完成 + echo. +) + +REM 检查.env文件 +if not exist ".env" ( + echo 警告: 未检测到.env配置文件 + echo 请复制.env.example为.env并配置相关参数 + echo. + echo 是否继续启动服务? + pause +) + +REM 启动服务 +echo 正在启动MIP广告点击服务... +echo. +echo 提示:默认使用开发环境配置 +echo 如需使用生产环境,请设置环境变量:set ENV=production +echo. +python app.py + +pause diff --git a/start_production.bat b/start_production.bat new file mode 100644 index 0000000..c2d1e6e --- /dev/null +++ b/start_production.bat @@ -0,0 +1,57 @@ +@echo off +chcp 65001 >nul +echo ======================================== +echo MIP广告点击服务 - 生产环境启动 +echo ======================================== +echo. + +REM 设置生产环境 +set ENV=production + +REM 检查Python是否安装 +python --version >nul 2>&1 +if %errorlevel% neq 0 ( + echo 错误: 未检测到Python,请先安装Python 3.8+ + pause + exit /b 1 +) + +REM 检查是否存在虚拟环境 +if not exist "venv" ( + echo 未检测到虚拟环境,正在创建... + python -m venv venv + echo 虚拟环境创建完成 + echo. +) + +REM 激活虚拟环境 +echo 激活虚拟环境... +call venv\Scripts\activate.bat + +REM 检查是否已安装依赖 +pip show flask >nul 2>&1 +if %errorlevel% neq 0 ( + echo 正在安装依赖包... + pip install -r requirements.txt + echo 依赖安装完成 + echo. + echo 正在安装 Playwright 浏览器... + python -m playwright install chromium + echo Playwright 浏览器安装完成 + echo. +) + +REM 检查生产环境配置文件 +if not exist ".env.production" ( + echo 错误: 未检测到生产环境配置文件 .env.production + echo 请先配置生产环境参数 + pause + exit /b 1 +) + +REM 启动服务 +echo 正在启动MIP广告点击服务(生产环境)... +echo. +python app.py + +pause diff --git a/static/README.txt b/static/README.txt new file mode 100644 index 0000000..d18f421 --- /dev/null +++ b/static/README.txt @@ -0,0 +1,40 @@ +MIP广告点击系统 - 前端控制面板使用说明 +=========================================== + +启动方式: +--------- +1. 确保后端服务已启动: + python app.py + +2. 打开浏览器访问: + http://127.0.0.1:5000 + +功能说明: +--------- +1. 调度器控制 + - 启动调度器:开始自动点击任务 + - 停止调度器:暂停所有自动任务 + - 实时显示调度器运行状态 + +2. 链接管理 + - 添加单个链接:输入MIP页面链接,点击添加 + - 批量添加链接:每行一个链接,支持批量导入 + - 重置链接:清空该链接的点击记录,重新开始 + - 删除链接:从系统中移除该链接 + +3. 统计数据 + - 总链接数:系统中管理的链接总数 + - 总点击次数:累计执行的点击次数 + - 获得回复:收到广告主回复的次数 + - 回复率:回复次数/点击次数的百分比 + +4. 自动刷新 + - 页面每5秒自动刷新一次数据 + - 无需手动刷新页面 + +注意事项: +--------- +- 调度器仅在09:00-21:00时间段执行任务 +- 每个链接每30分钟点击一次 +- 每次点击随机执行1-10次 +- 每次点击后等待30秒检查回复 diff --git a/static/browser.html b/static/browser.html new file mode 100644 index 0000000..6ee72df --- /dev/null +++ b/static/browser.html @@ -0,0 +1,112 @@ + + + + + + 浏览器测试 - MIP广告点击管理系统 + + + +
+ + + + +
+ + + + +
+
+
Profile管理
+
+
+ + + +
+
+
+
+ +
+
代理管理
+
+
+ + + +
+
+
+
+ +
+
Profile配置
+
+
+ + +
+
+ + +
+
+ + +
+
+
API v2: 使用proxy_id引用已创建的代理
+
API v1: 直接传入代理配置(需先获取大麦IP)
+
+
+
+
+ +
+
完整测试流程
+
+

完整测试:获取代理 → 创建代理 → 更新Profile → 启动浏览器

+
+ + +
+
+
+
+
+
+
+ + + + + diff --git a/static/css/common.css b/static/css/common.css new file mode 100644 index 0000000..423e22e --- /dev/null +++ b/static/css/common.css @@ -0,0 +1,366 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif; + background: #f0f2f5; + color: #333; + overflow: hidden; +} + +/* 布局 */ +.layout { + display: flex; + height: 100vh; + overflow: hidden; +} + +/* 侧边栏 */ +.sidebar { + width: 200px; + background: #001529; + color: white; + display: flex; + flex-direction: column; + flex-shrink: 0; +} + +.logo { + height: 64px; + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; + font-weight: bold; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); +} + +.menu { + flex: 1; + padding: 16px 0; +} + +.menu-item { + padding: 12px 24px; + cursor: pointer; + transition: all 0.3s; + display: flex; + align-items: center; + gap: 12px; + color: rgba(255, 255, 255, 0.65); +} + +.menu-item:hover { + background: rgba(255, 255, 255, 0.1); + color: white; +} + +.menu-item.active { + background: #1890ff; + color: white; +} + +.menu-icon { + font-size: 16px; +} + +/* 主内容区 */ +.main { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; + min-width: 0; +} + +/* 顶部导航栏 */ +.navbar { + height: 64px; + background: white; + box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08); + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 24px; + flex-shrink: 0; +} + +.breadcrumb { + font-size: 16px; + font-weight: 500; +} + +.user-info { + display: flex; + align-items: center; + gap: 8px; +} + +.status-indicator { + width: 8px; + height: 8px; + border-radius: 50%; + background: #52c41a; + animation: pulse 2s infinite; +} + +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} + +/* 内容区 */ +.content { + flex: 1; + padding: 24px; + overflow-y: auto; +} + +/* 卡片 */ +.card { + background: white; + border-radius: 2px; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03); + margin-bottom: 24px; +} + +.card-header { + padding: 16px 24px; + border-bottom: 1px solid #f0f0f0; + font-size: 16px; + font-weight: 500; +} + +.card-body { + padding: 24px; +} + +/* 统计卡片 */ +.stats-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 24px; + margin-bottom: 24px; +} + +.stat-card { + background: white; + padding: 24px; + border-radius: 2px; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03); + border-left: 3px solid #1890ff; +} + +.stat-card:nth-child(2) { + border-left-color: #52c41a; +} + +.stat-card:nth-child(3) { + border-left-color: #faad14; +} + +.stat-card:nth-child(4) { + border-left-color: #f5222d; +} + +.stat-label { + font-size: 14px; + color: #666; + margin-bottom: 8px; +} + +.stat-value { + font-size: 30px; + font-weight: 600; + color: #333; +} + +/* 表单 */ +.form-group { + margin-bottom: 16px; +} + +.form-label { + display: block; + margin-bottom: 8px; + font-size: 14px; + color: #333; +} + +.form-input { + width: 100%; + padding: 8px 12px; + border: 1px solid #d9d9d9; + border-radius: 2px; + font-size: 14px; + transition: all 0.3s; +} + +.form-input:focus { + outline: none; + border-color: #1890ff; + box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2); +} + +.form-textarea { + width: 100%; + padding: 8px 12px; + border: 1px solid #d9d9d9; + border-radius: 2px; + font-size: 14px; + resize: vertical; + min-height: 100px; + font-family: inherit; +} + +.form-textarea:focus { + outline: none; + border-color: #1890ff; + box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2); +} + +/* 按钮 */ +.btn { + padding: 8px 16px; + border: none; + border-radius: 2px; + font-size: 14px; + cursor: pointer; + transition: all 0.3s; + display: inline-flex; + align-items: center; + gap: 6px; +} + +.btn-primary { + background: #1890ff; + color: white; +} + +.btn-primary:hover { + background: #40a9ff; +} + +.btn-success { + background: #52c41a; + color: white; +} + +.btn-success:hover { + background: #73d13d; +} + +.btn-danger { + background: #ff4d4f; + color: white; +} + +.btn-danger:hover { + background: #ff7875; +} + +.btn-warning { + background: #faad14; + color: white; +} + +.btn-warning:hover { + background: #ffc53d; +} + +.btn-group { + display: flex; + gap: 12px; +} + +/* 表格 */ +.table-container { + overflow-x: auto; +} + +.table { + width: 100%; + border-collapse: collapse; +} + +.table th { + background: #fafafa; + padding: 12px 16px; + text-align: left; + font-size: 14px; + font-weight: 500; + color: #333; + border-bottom: 1px solid #f0f0f0; +} + +.table td { + padding: 12px 16px; + font-size: 14px; + border-bottom: 1px solid #f0f0f0; +} + +.table tr:hover { + background: #fafafa; +} + +.table-url { + color: #1890ff; + text-decoration: none; + max-width: 400px; + display: inline-block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.table-url:hover { + text-decoration: underline; +} + +/* Toast提示 */ +.toast { + position: fixed; + top: 24px; + right: 24px; + padding: 16px 24px; + border-radius: 2px; + color: white; + font-size: 14px; + z-index: 1000; + min-width: 300px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + animation: slideIn 0.3s ease; +} + +.toast-success { + background: #52c41a; +} + +.toast-error { + background: #ff4d4f; +} + +.toast-info { + background: #1890ff; +} + +@keyframes slideIn { + from { + transform: translateX(400px); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} + +/* 空状态 */ +.empty-state { + text-align: center; + padding: 48px; + color: #999; +} diff --git a/static/dashboard.html b/static/dashboard.html new file mode 100644 index 0000000..7733622 --- /dev/null +++ b/static/dashboard.html @@ -0,0 +1,98 @@ + + + + + + 数据概览 - MIP广告点击管理系统 + + + +
+ + + + +
+ + + + +
+ +
+
+
总链接数
+
0
+
+
+
总点击次数
+
0
+
+
+
获得回复
+
0
+
+
+
回复率
+
0%
+
+
+ + +
+
链接列表
+
+
+ + + + + + + + + + + + + + + +
链接地址点击次数回复次数上次点击时间操作
暂无数据
+
+
+
+
+
+
+ + + + + diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..332d65b --- /dev/null +++ b/static/index.html @@ -0,0 +1,843 @@ + + + + + + MIP广告点击管理系统 + + + +
+ + + + +
+ + + + +
+ +
+ +
+
+
总链接数
+
0
+
+
+
总点击次数
+
0
+
+
+
获得回复
+
0
+
+
+
回复率
+
0%
+
+
+ + +
+
链接列表
+
+
+ + + + + + + + + + + + + + + +
链接地址点击次数回复次数上次点击时间操作
暂无数据
+
+
+
+
+ + +
+
+
调度器控制
+
+
+ + +
+
+

调度规则说明

+
    +
  • 每30分钟点击一次添加的链接
  • +
  • 仅在09:00-21:00时间段执行
  • +
  • 每个链接随机点击1-10次
  • +
  • 等待最多30秒查看回复
  • +
+
+
+
+
+ + +
+
+
添加单个链接
+
+
+ + +
+ +
+
+ +
+
批量添加链接
+
+
+ + +
+ +
+
+
+ + +
+
+
AdsPower浏览器测试
+
+

此功能用于测试AdsPower浏览器连接是否正常

+
+ + +
+
+
+
+
+
+
+
+ + + + diff --git a/static/js/browser.js b/static/js/browser.js new file mode 100644 index 0000000..8c89c3b --- /dev/null +++ b/static/js/browser.js @@ -0,0 +1,515 @@ +// 全局变量存储当前选中的Profile和代理 +let currentProfileId = null; +let currentProxyInfo = null; + +// 格式化JSON显示 +function formatResult(data, elementId) { + const el = document.getElementById(elementId); + el.innerHTML = `
${JSON.stringify(data, null, 2)}
`; +} + +// 查询Profile列表 +async function listProfiles() { + try { + showToast('正在查询Profile列表...', 'info'); + const response = await fetch(`${API_BASE}/api/adspower/profiles`); + const data = await response.json(); + + if (data.success) { + const profiles = data.data?.data?.list || []; + + if (profiles.length > 0) { + // 美化显示Profile信息 + let html = '
'; + html += `

找到 ${profiles.length} 个Profile环境

`; + + profiles.forEach((profile, idx) => { + const proxyConfig = profile.user_proxy_config || {}; + const hasProxy = proxyConfig.proxy_host && proxyConfig.proxy_port; + + html += ` +
+
+
+
+ #${idx + 1} ${profile.name} + No.${profile.profile_no} +
+
+ Profile ID: ${profile.profile_id} +
+
+ +
+ +
+
+ 当前IP: + ${profile.ip || 'N/A'} + ${profile.ip_country ? `(${profile.ip_country.toUpperCase()})` : ''} +
+
+ 最后打开: + ${profile.last_open_time ? new Date(parseInt(profile.last_open_time) * 1000).toLocaleString('zh-CN') : 'N/A'} +
+
+ + ${hasProxy ? ` +
+
✓ 代理配置
+
+
+
+ 类型: + ${proxyConfig.proxy_type?.toUpperCase() || 'N/A'} +
+
+ 地址: + ${proxyConfig.proxy_host}:${proxyConfig.proxy_port} +
+ ${proxyConfig.latest_ip ? ` +
+ 最新IP: + ${proxyConfig.latest_ip} +
+ ` : ''} +
+ ${proxyConfig.proxy_user ? ` +
+ 认证: + ${proxyConfig.proxy_user} +
+ ` : ''} +
+
+ ` : ` +
+ ⚠ 未配置代理 +
+ `} + + ${profile.remark ? ` +
+ 备注: ${profile.remark} +
+ ` : ''} +
+ `; + }); + + html += '
'; + document.getElementById('profileResult').innerHTML = html; + + // 自动选择第一个Profile + currentProfileId = profiles[0].profile_id; + document.getElementById('profileId').value = currentProfileId; + showToast(`找到 ${profiles.length} 个Profile环境`, 'success'); + } else { + document.getElementById('profileResult').innerHTML = '

未找到Profile环境

'; + showToast('未找到Profile', 'info'); + } + } else { + showToast(data.message || '查询失败', 'error'); + } + } catch (error) { + console.error('查询Profile列表错误:', error); + showToast('查询失败: ' + error.message, 'error'); + } +} + +// 选择Profile +function selectProfile(profileId, profileName) { + currentProfileId = profileId; + document.getElementById('profileId').value = profileId; + showToast(`已选择环境: ${profileName}`, 'success'); +} + +// 启动浏览器 +async function startBrowser() { + const profileId = document.getElementById('profileId').value || currentProfileId; + + if (!profileId) { + showToast('请先查询Profile列表或输入Profile ID', 'error'); + return; + } + + try { + showToast('正在启动浏览器...', 'info'); + const response = await fetch(`${API_BASE}/api/adspower/browser/start`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ user_id: profileId }) + }); + const data = await response.json(); + + if (data.success) { + formatResult(data.data, 'profileResult'); + showToast('浏览器启动成功', 'success'); + } else { + showToast(data.message || '启动失败', 'error'); + } + } catch (error) { + showToast('启动失败: ' + error.message, 'error'); + } +} + +// 停止浏览器 +async function stopBrowser() { + const profileId = document.getElementById('profileId').value || currentProfileId; + + if (!profileId) { + showToast('请先输入Profile ID', 'error'); + return; + } + + try { + showToast('正在停止浏览器...', 'info'); + const response = await fetch(`${API_BASE}/api/adspower/browser/stop`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ user_id: profileId }) + }); + const data = await response.json(); + + if (data.success) { + formatResult(data.data, 'profileResult'); + showToast('浏览器已停止', 'success'); + } else { + showToast(data.message || '停止失败', 'error'); + } + } catch (error) { + showToast('停止失败: ' + error.message, 'error'); + } +} + +// 获取大麦IP +async function getDamaiProxy() { + try { + showToast('正在获取大麦IP...', 'info'); + const response = await fetch(`${API_BASE}/api/adspower/proxy/damai`); + const data = await response.json(); + + if (data.success) { + currentProxyInfo = data.data; + formatResult(data.data, 'proxyResult'); + showToast(`获取成功: ${data.data.host}:${data.data.port}`, 'success'); + } else { + showToast(data.message || '获取失败', 'error'); + } + } catch (error) { + showToast('获取失败: ' + error.message, 'error'); + } +} + +// 创建代理 +async function createProxy() { + if (!currentProxyInfo) { + showToast('请先获取大麦IP', 'error'); + return; + } + + try { + showToast('正在创建代理...', 'info'); + + const proxyConfig = { + type: 'http', + host: currentProxyInfo.host, + port: currentProxyInfo.port, + user: '69538fdef04e1', + password: '63v0kQBr2yJXnjf', + ipchecker: 'ip2location', + remark: 'Damai Auto Proxy' + }; + + const response = await fetch(`${API_BASE}/api/adspower/proxy/create`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ proxy_config: proxyConfig }) + }); + const data = await response.json(); + + if (data.success) { + document.getElementById('proxyId').value = data.data.proxy_id; + formatResult(data.data, 'proxyResult'); + showToast(`代理创建成功,ID: ${data.data.proxy_id}`, 'success'); + } else { + showToast(data.message || '创建失败', 'error'); + } + } catch (error) { + showToast('创建失败: ' + error.message, 'error'); + } +} + +// 查询代理列表 +async function listProxies() { + try { + showToast('正在查询代理列表...', 'info'); + const response = await fetch(`${API_BASE}/api/adspower/proxy/list?page=1&limit=50`); + + // 检查响应类型 + const contentType = response.headers.get('content-type'); + if (!contentType || !contentType.includes('application/json')) { + console.error('服务器返回非JSON响应:', contentType); + const text = await response.text(); + console.error('响应内容:', text.substring(0, 200)); + showToast('服务器响应错误,请检查后端服务', 'error'); + return; + } + + const data = await response.json(); + + if (data.success && data.data) { + const proxies = data.data?.data?.list || []; + + if (proxies.length > 0) { + // 格式化显示代理信息 + let html = '
'; + html += `

找到 ${proxies.length} 个代理

`; + + proxies.forEach((proxy, idx) => { + html += ` +
+
+
+
#${idx + 1} ID: ${proxy.proxy_id}
+
+ ${proxy.type} - ${proxy.host}:${proxy.port} +
+ ${proxy.remark ? `
备注: ${proxy.remark}
` : ''} +
+ 关联环境数: ${proxy.profile_count || 0} +
+
+ +
+
+ `; + }); + + html += '
'; + document.getElementById('proxyResult').innerHTML = html; + showToast(`找到 ${proxies.length} 个代理`, 'success'); + } else { + document.getElementById('proxyResult').innerHTML = '

暂无代理,请先创建代理

'; + showToast('暂无代理', 'info'); + } + } else { + showToast(data.message || '查询失败', 'error'); + document.getElementById('proxyResult').innerHTML = `

${data.message || '查询失败'}

`; + } + } catch (error) { + console.error('查询代理列表错误:', error); + showToast('查询失败: ' + error.message, 'error'); + document.getElementById('proxyResult').innerHTML = `

错误: ${error.message}

`; + } +} + +// 更新Profile代理(API v2方式) +async function updateProfileProxy() { + const profileId = document.getElementById('profileId').value; + const proxyId = document.getElementById('proxyId').value; + + if (!profileId || !proxyId) { + showToast('请输入Profile ID和代理ID', 'error'); + return; + } + + try { + showToast('正在更新Profile代理 (API v2)...', 'info'); + const response = await fetch(`${API_BASE}/api/adspower/profile/update`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ profile_id: profileId, proxy_id: proxyId }) + }); + const data = await response.json(); + + if (data.success) { + formatResult(data.data, 'updateResult'); + showToast('更新成功 (API v2)', 'success'); + } else { + showToast(data.message || '更新失败', 'error'); + } + } catch (error) { + showToast('更新失败: ' + error.message, 'error'); + } +} + +// 更新Profile代理(API v1方式) +async function updateProfileProxyV1() { + const profileId = document.getElementById('profileId').value; + + if (!profileId) { + showToast('请输入Profile ID', 'error'); + return; + } + + // 获取当前代理信息 + if (!currentProxyInfo) { + showToast('请先获取大麦IP', 'error'); + return; + } + + try { + showToast('正在更新Profile代理 (API v1)...', 'info'); + + // 构建 proxy_config + const proxyConfig = { + proxy_type: 'http', + proxy_host: currentProxyInfo.host, + proxy_port: currentProxyInfo.port, + proxy_user: '69538fdef04e1', + proxy_password: '63v0kQBr2yJXnjf', + proxy_soft: 'other' + }; + + const response = await fetch(`${API_BASE}/api/adspower/profile/update-v1`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ profile_id: profileId, proxy_config: proxyConfig }) + }); + const data = await response.json(); + + if (data.success) { + let resultHtml = '
'; + resultHtml += '
✅ 代理更新成功 (API v1)
'; + resultHtml += '
'; + resultHtml += `
Profile ID: ${profileId}
`; + resultHtml += `
代理地址: ${proxyConfig.proxy_host}:${proxyConfig.proxy_port}
`; + resultHtml += `
认证用户: ${proxyConfig.proxy_user}
`; + resultHtml += '
'; + document.getElementById('updateResult').innerHTML = resultHtml; + showToast('更新成功 (API v1)', 'success'); + } else { + showToast(data.message || '更新失败', 'error'); + document.getElementById('updateResult').innerHTML = `

${data.message || '更新失败'}

`; + } + } catch (error) { + console.error('更新Profile代理错误 (v1):', error); + showToast('更新失败: ' + error.message, 'error'); + document.getElementById('updateResult').innerHTML = `

错误: ${error.message}

`; + } +} + +// 完整测试流程 +async function fullTest(useProxy) { + const resultDiv = document.getElementById('fullTestResult'); + let log = []; + + function addLog(msg, type = 'info') { + const time = new Date().toLocaleTimeString(); + const color = type === 'error' ? '#ff4d4f' : type === 'success' ? '#52c41a' : '#666'; + log.push(`
[${time}] ${msg}
`); + resultDiv.innerHTML = log.join(''); + } + + try { + addLog('开始完整测试流程...'); + + // 1. 查询Profile + addLog('步骤1: 查询Profile列表'); + const profileRes = await fetch(`${API_BASE}/api/adspower/profiles`); + const profileData = await profileRes.json(); + + if (!profileData.success) { + addLog('查询Profile失败: ' + profileData.message, 'error'); + return; + } + + const profiles = profileData.data?.data?.list || []; + if (profiles.length === 0) { + addLog('未找到Profile', 'error'); + return; + } + + currentProfileId = profiles[0].profile_id; + document.getElementById('profileId').value = currentProfileId; + addLog(`找到Profile: ${currentProfileId}`, 'success'); + + if (useProxy) { + // 2. 获取大麦IP + addLog('步骤2: 获取大麦IP代理'); + const proxyRes = await fetch(`${API_BASE}/api/adspower/proxy/damai`); + const proxyData = await proxyRes.json(); + + if (!proxyData.success) { + addLog('获取代理失败: ' + proxyData.message, 'error'); + return; + } + + currentProxyInfo = proxyData.data; + addLog(`获取代理: ${currentProxyInfo.host}:${currentProxyInfo.port}`, 'success'); + + // 3. 创建代理 + addLog('步骤3: 创建AdsPower代理'); + const proxyConfig = { + type: 'http', + host: currentProxyInfo.host, + port: currentProxyInfo.port, + user: '69538fdef04e1', + password: '63v0kQBr2yJXnjf', + ipchecker: 'ip2location', + remark: 'Damai Auto Proxy' + }; + + const createProxyRes = await fetch(`${API_BASE}/api/adspower/proxy/create`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ proxy_config: proxyConfig }) + }); + const createProxyData = await createProxyRes.json(); + + if (!createProxyData.success) { + addLog('创建代理失败: ' + createProxyData.message, 'error'); + return; + } + + const proxyId = createProxyData.data.proxy_id; + document.getElementById('proxyId').value = proxyId; + addLog(`创建代理成功,ID: ${proxyId}`, 'success'); + + // 4. 更新Profile + addLog('步骤4: 更新Profile代理配置'); + const updateRes = await fetch(`${API_BASE}/api/adspower/profile/update`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ profile_id: currentProfileId, proxy_id: proxyId }) + }); + const updateData = await updateRes.json(); + + if (!updateData.success) { + addLog('更新Profile失败: ' + updateData.message, 'error'); + return; + } + + addLog('更新Profile成功', 'success'); + } + + // 5. 启动浏览器 + addLog(`步骤${useProxy ? 5 : 2}: 启动浏览器`); + const startRes = await fetch(`${API_BASE}/api/adspower/browser/start`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ user_id: currentProfileId }) + }); + const startData = await startRes.json(); + + if (!startData.success) { + addLog('启动浏览器失败: ' + startData.message, 'error'); + return; + } + + addLog('浏览器启动成功', 'success'); + addLog('测试流程完成!', 'success'); + showToast('完整测试流程执行成功', 'success'); + + } catch (error) { + addLog('测试异常: ' + error.message, 'error'); + showToast('测试失败: ' + error.message, 'error'); + } +} diff --git a/static/js/common.js b/static/js/common.js new file mode 100644 index 0000000..e56b41c --- /dev/null +++ b/static/js/common.js @@ -0,0 +1,44 @@ +const API_BASE = 'http://127.0.0.1:5000'; + +// 显示Toast提示 +function showToast(message, type = 'info') { + const toast = document.createElement('div'); + toast.className = `toast toast-${type}`; + toast.textContent = message; + document.body.appendChild(toast); + + setTimeout(() => { + toast.remove(); + }, 3000); +} + +// 获取调度器状态 +async function getSchedulerStatus() { + try { + const response = await fetch(`${API_BASE}/api/scheduler/status`); + const data = await response.json(); + + if (data.success) { + const isRunning = data.data.status === 'running'; + const indicator = document.getElementById('statusIndicator'); + const text = document.getElementById('statusText'); + + if (indicator && text) { + indicator.style.background = isRunning ? '#52c41a' : '#ff4d4f'; + text.textContent = isRunning ? '调度器运行中' : '调度器已停止'; + } + } + } catch (error) { + console.error('获取调度器状态失败:', error); + } +} + +// 页面加载时初始化 +document.addEventListener('DOMContentLoaded', () => { + getSchedulerStatus(); + + // 定时刷新状态 + setInterval(() => { + getSchedulerStatus(); + }, 5000); +}); diff --git a/static/js/dashboard.js b/static/js/dashboard.js new file mode 100644 index 0000000..a5158bf --- /dev/null +++ b/static/js/dashboard.js @@ -0,0 +1,111 @@ +// 获取统计数据 +async function getStatistics() { + try { + const response = await fetch(`${API_BASE}/api/statistics`); + const data = await response.json(); + + if (data.success) { + const stats = data.data; + document.getElementById('totalUrls').textContent = stats.total_urls; + document.getElementById('totalClicks').textContent = stats.total_clicks; + document.getElementById('totalReplies').textContent = stats.total_replies; + document.getElementById('replyRate').textContent = stats.reply_rate; + } + } catch (error) { + console.error('获取统计数据失败:', error); + } +} + +// 加载URL列表 +async function loadUrlList() { + try { + const response = await fetch(`${API_BASE}/api/urls`); + const data = await response.json(); + + if (data.success) { + const tbody = document.getElementById('urlTableBody'); + + if (data.data.length === 0) { + tbody.innerHTML = '暂无数据'; + return; + } + + tbody.innerHTML = data.data.map(item => ` + + + ${item.url} + + ${item.click_count} + ${item.reply_count} + ${item.last_click_time || '未点击'} + + + + + + `).join(''); + } + } catch (error) { + console.error('加载URL列表失败:', error); + } +} + +// 重置URL +async function resetUrl(encodedUrl) { + if (!confirm('确定要重置该链接吗?')) { + return; + } + + try { + const response = await fetch(`${API_BASE}/api/urls/${encodedUrl}/reset`, { + method: 'POST' + }); + const data = await response.json(); + + if (data.success) { + showToast('重置成功', 'success'); + loadUrlList(); + getStatistics(); + } else { + showToast(data.message || '重置失败', 'error'); + } + } catch (error) { + showToast('重置失败: ' + error.message, 'error'); + } +} + +// 删除URL +async function deleteUrl(encodedUrl) { + if (!confirm('确定要删除该链接吗?')) { + return; + } + + try { + const response = await fetch(`${API_BASE}/api/urls/${encodedUrl}`, { + method: 'DELETE' + }); + const data = await response.json(); + + if (data.success) { + showToast('删除成功', 'success'); + loadUrlList(); + getStatistics(); + } else { + showToast(data.message || '删除失败', 'error'); + } + } catch (error) { + showToast('删除失败: ' + error.message, 'error'); + } +} + +// 初始化 +document.addEventListener('DOMContentLoaded', () => { + getStatistics(); + loadUrlList(); + + // 定时刷新 + setInterval(() => { + getStatistics(); + loadUrlList(); + }, 5000); +}); diff --git a/static/js/scheduler.js b/static/js/scheduler.js new file mode 100644 index 0000000..1010999 --- /dev/null +++ b/static/js/scheduler.js @@ -0,0 +1,37 @@ +// 启动调度器 +async function startScheduler() { + try { + const response = await fetch(`${API_BASE}/api/scheduler/start`, { + method: 'POST' + }); + const data = await response.json(); + + if (data.success) { + showToast('调度器已启动', 'success'); + getSchedulerStatus(); + } else { + showToast(data.message || '启动失败', 'error'); + } + } catch (error) { + showToast('启动失败: ' + error.message, 'error'); + } +} + +// 停止调度器 +async function stopScheduler() { + try { + const response = await fetch(`${API_BASE}/api/scheduler/stop`, { + method: 'POST' + }); + const data = await response.json(); + + if (data.success) { + showToast('调度器已停止', 'success'); + getSchedulerStatus(); + } else { + showToast(data.message || '停止失败', 'error'); + } + } catch (error) { + showToast('停止失败: ' + error.message, 'error'); + } +} diff --git a/static/js/urls.js b/static/js/urls.js new file mode 100644 index 0000000..cce9450 --- /dev/null +++ b/static/js/urls.js @@ -0,0 +1,66 @@ +// 添加单个URL +async function addSingleUrl() { + const url = document.getElementById('singleUrl').value.trim(); + + if (!url) { + showToast('请输入链接', 'error'); + return; + } + + try { + const response = await fetch(`${API_BASE}/api/urls`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ url }) + }); + const data = await response.json(); + + if (data.success) { + showToast('添加成功', 'success'); + document.getElementById('singleUrl').value = ''; + } else { + showToast(data.message || '添加失败', 'error'); + } + } catch (error) { + showToast('添加失败: ' + error.message, 'error'); + } +} + +// 批量添加URL +async function addBatchUrls() { + const text = document.getElementById('batchUrls').value.trim(); + + if (!text) { + showToast('请输入链接', 'error'); + return; + } + + const urls = text.split('\n').map(u => u.trim()).filter(u => u); + + if (urls.length === 0) { + showToast('请输入有效链接', 'error'); + return; + } + + try { + const response = await fetch(`${API_BASE}/api/urls`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ urls }) + }); + const data = await response.json(); + + if (data.success) { + showToast(`成功添加 ${data.added_count}/${data.total_count} 个链接`, 'success'); + document.getElementById('batchUrls').value = ''; + } else { + showToast(data.message || '添加失败', 'error'); + } + } catch (error) { + showToast('添加失败: ' + error.message, 'error'); + } +} diff --git a/static/scheduler.html b/static/scheduler.html new file mode 100644 index 0000000..ead886d --- /dev/null +++ b/static/scheduler.html @@ -0,0 +1,72 @@ + + + + + + 调度器管理 - MIP广告点击管理系统 + + + +
+ + + + +
+ + + + +
+
+
调度器控制
+
+
+ + +
+
+

调度规则说明

+
    +
  • 每30分钟点击一次添加的链接
  • +
  • 仅在09:00-21:00时间段执行
  • +
  • 每个链接随机点击1-10次
  • +
  • 等待最多30秒查看回复
  • +
+
+
+
+
+
+
+ + + + + diff --git a/static/urls.html b/static/urls.html new file mode 100644 index 0000000..86cffad --- /dev/null +++ b/static/urls.html @@ -0,0 +1,75 @@ + + + + + + 链接管理 - MIP广告点击管理系统 + + + +
+ + + + +
+ + + + +
+
+
添加单个链接
+
+
+ + +
+ +
+
+ +
+
批量添加链接
+
+
+ + +
+ +
+
+
+
+
+ + + + + diff --git a/test_adspower_playwright.py b/test_adspower_playwright.py new file mode 100644 index 0000000..82188e4 --- /dev/null +++ b/test_adspower_playwright.py @@ -0,0 +1,411 @@ +""" +AdsPower + Playwright CDP 集成测试 +演示如何通过 CDP 连接到 AdsPower 指纹浏览器 +""" + +from loguru import logger +from adspower_client import AdsPowerClient +from config import Config +import sys + +# 配置日志 +logger.remove() +logger.add( + sys.stdout, + format="{time:HH:mm:ss} | {level: <8} | {message}", + level="INFO" +) + + +def test_adspower_connection(use_proxy: bool = False, proxy_info: dict = None, use_api_v1: bool = False): + """测试 AdsPower + Playwright CDP 连接 + + Args: + use_proxy: 是否使用大麦IP代理 + proxy_info: 已获取的代理信息 + use_api_v1: 是否使用API v1方式更新代理 + """ + + # ==================== 配置区 ==================== + # 访问的网页地址,在这里修改 + TEST_URL = "https://health.baidu.com/m/detail/ar_2366617956693492811" # IP检测网站,可查看代理是否生效 + # 其他可选项: + # TEST_URL = "https://www.baidu.com" # 百度 + # TEST_URL = "https://www.google.com" # Google + # TEST_URL = "https://你的MIP页面地址" # 你的目标网页 + # ===================================================== + + client = AdsPowerClient() + + try: + # 0. 先查询 Profile 列表 + logger.info("=" * 60) + logger.info("步骤 0: 查询 Profile 列表") + logger.info("=" * 60) + + result = client.list_profiles() + if not result: + logger.error("查询 Profile 失败") + return False + + profiles = result.get('data', {}).get('list', []) + if not profiles: + logger.error("没有可用的 Profile,请先在 AdsPower 中创建 Profile") + return False + + # 使用第一个 Profile + first_profile = profiles[0] + profile_id = first_profile.get('profile_id') + profile_name = first_profile.get('name', 'N/A') + + logger.info(f"将使用 Profile: {profile_name} (ID: {profile_id})") + logger.info("") + + # 1. 启动 AdsPower 浏览器(可选使用代理) + logger.info("=" * 60) + logger.info(f"步骤 1: {'[使用代理] ' if use_proxy else ''}启动 AdsPower 浏览器") + if use_proxy: + logger.info(f"代理更新方式: {'API v1 (直接传入proxy_config)' if use_api_v1 else 'API v2 (使用proxy_id引用)'}") + logger.info("=" * 60) + + if use_proxy and proxy_info: + if use_api_v1: + # 使用 API v1 方式:直接传入 proxy_config + logger.info("使用 API v1 方式更新代理...") + proxy_config_v1 = { + "proxy_type": "http", + "proxy_host": proxy_info["host"], + "proxy_port": proxy_info["port"], + "proxy_user": client.DAMAI_USER, + "proxy_password": client.DAMAI_PASSWORD, + "proxy_soft": "other" + } + + # 直接更新 Profile + success = client.update_profile_proxy_v1(profile_id, proxy_config_v1) + if not success: + logger.warning("更新代理失败 (API v1),将不使用代理启动") + else: + # 使用 API v2 方式:先创建代理,再引用 + logger.info("使用 API v2 方式更新代理...") + proxy_config = { + "type": "http", + "host": proxy_info["host"], + "port": proxy_info["port"], + "user": client.DAMAI_USER, + "password": client.DAMAI_PASSWORD, + "ipchecker": "ip2location", + "remark": "Damai Auto Proxy" + } + + # 创建代理 + proxy_id = client.create_proxy(proxy_config) + if proxy_id: + # 更新 Profile + client.update_profile_proxy(profile_id, proxy_id) + else: + logger.warning("创建代理失败,将不使用代理启动") + + browser_info = client.start_browser(user_id=profile_id) + else: + browser_info = client.start_browser(user_id=profile_id) + if not browser_info: + logger.error("启动 AdsPower 浏览器失败") + return False + + logger.info(f"浏览器信息: {browser_info}") + + # 2. 通过 CDP 连接到浏览器 + logger.info("") + logger.info("=" * 60) + logger.info("步骤 2: 通过 CDP 连接到浏览器") + logger.info("=" * 60) + + browser = client.connect_browser(browser_info) + if not browser: + logger.error("CDP 连接失败") + return False + + logger.info(f"浏览器版本: {browser.version}") + logger.info(f"上下文数量: {len(browser.contexts)}") + + # 3. 获取页面 + logger.info("") + logger.info("=" * 60) + logger.info("步骤 3: 获取浏览器页面") + logger.info("=" * 60) + + page = client.get_page(browser) + if not page: + logger.error("获取页面失败") + return False + + logger.info(f"页面 URL: {page.url}") + + # 3.5. 关闭其他标签页,只保留AdsPower启动页 + logger.info("") + logger.info("=" * 60) + logger.info("步骤 3.5: 清理多余标签页") + logger.info("=" * 60) + + context = browser.contexts[0] + all_pages = context.pages + logger.info(f"当前打开的标签页数: {len(all_pages)}") + + # 遍历所有页面,关闭非 AdsPower 启动页 + closed_count = 0 + for p in all_pages: + try: + page_url = p.url + # 保留 AdsPower 启动页 + if 'start.adspower.net' in page_url: + logger.info(f"保留启动页: {page_url}") + else: + logger.info(f"关闭标签页: {page_url}") + p.close() + closed_count += 1 + except Exception as e: + logger.warning(f"关闭页面失败: {str(e)}") + + logger.info(f"已关闭 {closed_count} 个标签页") + + # 重新获取当前页面列表 + remaining_pages = context.pages + logger.info(f"剩余标签页数: {len(remaining_pages)}") + + # 如果所有页面都被关闭了,创建一个新页面 + if len(remaining_pages) == 0: + logger.info("所有页面已关闭,创建新标签页") + page = context.new_page() + else: + # 使用第一个剩余页面 + page = remaining_pages[0] + logger.info(f"使用剩余页面: {page.url}") + + # 4. 测试页面操作 + logger.info("") + logger.info("=" * 60) + logger.info("步骤 4: 测试页面操作") + logger.info("=" * 60) + + # 访问配置的网页 + logger.info(f"访问测试页面: {TEST_URL}") + page.goto(TEST_URL, wait_until='domcontentloaded') + + # 等待广告接口响应完成 + logger.info("等待广告接口响应...") + try: + # 等待广告接口请求完成 + response = page.wait_for_response( + lambda r: 'getRefreshAdAssets' in r.url and r.status == 200, + timeout=10000 # 10秒超时 + ) + logger.info(f"广告接口已响应: {response.url}") + + # 等待DOM更新 + import time + time.sleep(2) + except Exception as e: + logger.warning(f"等待广告接口超时或失败: {str(e)}") + logger.info("继续执行...") + import time + time.sleep(2) + + # 获取页面标题 + title = page.title() + logger.info(f"页面标题: {title}") + + # 获取页面 URL + current_url = page.url + logger.info(f"当前 URL: {current_url}") + + # 截图测试(点击前) + screenshot_path = "./test_screenshot_before.png" + page.screenshot(path=screenshot_path) + logger.info(f"截图已保存: {screenshot_path}") + + # 查找并点击广告 + logger.info("") + logger.info("-" * 60) + logger.info("开始查找广告元素...") + + try: + # 查找所有广告元素 + ad_selector = 'span.ec-tuiguang.ecfc-tuiguang.xz81bbe' + ad_elements = page.locator(ad_selector) + ad_count = ad_elements.count() + + logger.info(f"找到 {ad_count} 个广告元素") + + if ad_count > 0: + # 点击第一个广告 + logger.info("准备点击第一个广告...") + + # 滚动到元素可见 + first_ad = ad_elements.first + first_ad.scroll_into_view_if_needed() + time.sleep(1) + + # 点击广告 + first_ad.click() + logger.info("✅ 已点击第一个广告") + + # 等待页面跳转 + time.sleep(3) + + # 获取点击后的页面信息 + new_url = page.url + new_title = page.title() + logger.info(f"点击后 URL: {new_url}") + logger.info(f"点击后标题: {new_title}") + + # 截图(点击后) + screenshot_path_after = "./test_screenshot_after.png" + page.screenshot(path=screenshot_path_after) + logger.info(f"点击后截图已保存: {screenshot_path_after}") + else: + logger.warning("⚠ 未找到广告元素") + + except Exception as e: + logger.error(f"查找/点击广告失败: {str(e)}") + # 保存错误时的截图 + page.screenshot(path="./test_screenshot_error.png") + logger.info("错误截图已保存: ./test_screenshot_error.png") + + # 5. 清理资源 + logger.info("") + logger.info("=" * 60) + logger.info("步骤 5: 测试完成") + logger.info("=" * 60) + + # 注意:不停止浏览器,保持运行状态供手动操作 + logger.info("浏览器保持运行状态,可继续手动操作") + logger.info("如需停止浏览器,请在AdsPower中手动关闭") + + logger.info("") + logger.info("=" * 60) + logger.info("测试完成!浏览器未关闭") + logger.info("=" * 60) + + return True + + except Exception as e: + logger.error(f"测试异常: {str(e)}") + return False + + finally: + # 注意:不自动清理资源,保持浏览器运行 + pass + + +def test_multiple_pages(): + """测试多页面操作""" + + client = AdsPowerClient() + profile_id = None + + try: + logger.info("=" * 60) + logger.info("测试多页面操作") + logger.info("=" * 60) + + # 查询 Profile + result = client.list_profiles() + if not result: + return False + + profiles = result.get('data', {}).get('list', []) + if not profiles: + logger.error("没有可用的 Profile") + return False + + profile_id = profiles[0].get('profile_id') + + # 启动并连接 + browser_info = client.start_browser(user_id=profile_id) + if not browser_info: + return False + + browser = client.connect_browser(browser_info) + if not browser: + return False + + # 获取第一个页面 + page1 = client.get_page(browser) + logger.info("访问百度...") + page1.goto("https://www.baidu.com") + logger.info(f"页面1标题: {page1.title()}") + + # 创建新页面 + context = browser.contexts[0] + page2 = context.new_page() + logger.info("访问必应...") + page2.goto("https://www.bing.com") + logger.info(f"页面2标题: {page2.title()}") + + logger.info(f"当前打开的页面数: {len(context.pages)}") + + # 关闭页面 + page2.close() + logger.info("已关闭页面2") + + return True + + except Exception as e: + logger.error(f"测试异常: {str(e)}") + return False + + finally: + try: + if profile_id: + client.stop_browser(user_id=profile_id) + else: + client.stop_browser() + except: + pass + + +if __name__ == "__main__": + logger.info("开始测试 AdsPower + Playwright CDP 集成") + logger.info("") + + logger.info(f"当前环境: {Config.ENV}") + logger.info(f"AdsPower API: {Config.ADSPOWER_API_URL}") + logger.info("") + + # 创建客户端 + client = AdsPowerClient() + + # ==================== 配置区 ==================== + # 默认使用代理,如不需要改为 False + use_proxy = True + # 默认使用 API v2 Direct 方式(0=v2 proxy_id, 1=v1, 2=v2 direct) + use_api_v1 = True # True=API v1, False=API v2 + # ===================================================== + + proxy_info = None + + # 如果使用代理,提前获取 + if use_proxy: + logger.info("") + logger.info(f"使用代理模式: {'API v1 (直接传入proxy_config)' if use_api_v1 else 'API v2 (使用proxy_id引用)'}") + logger.info("步骤 0: 提前获取大麦IP代理") + proxy_info = client.get_damai_proxy() + if not proxy_info: + logger.error("获取代理失败,终止测试") + sys.exit(1) + logger.info(f"代理地址: {proxy_info['host']}:{proxy_info['port']}") + logger.info("") + + # 测试基本连接 + if test_adspower_connection(use_proxy=use_proxy, proxy_info=proxy_info, use_api_v1=use_api_v1): + logger.info("\n基本连接测试通过\n") + else: + logger.error("\n基本连接测试失败\n") + sys.exit(1) + + # 测试多页面操作 + # if test_multiple_pages(): + # logger.info("\n多页面操作测试通过\n") + # else: + # logger.error("\n多页面操作测试失败\n")