first commit

This commit is contained in:
jianxin committed 2025-04-02 21:01:25 +08:00
commit 4890793ce5
1029 files changed
+5992

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
"""抖音私信自动回复机器人模块."""
__version__ = "0.1.0"
Binary file not shown.
+50
View File
@@ -0,0 +1,50 @@
import os
import logging
from pathlib import Path
from douyin_imbot.config.environment import get_environment_settings
# 创建一个全局设置字典
settings = {}
def initialize_app():
"""初始化应用程序设置和环境"""
global settings
# 设置基础配置
app_dir = Path(__file__).parent.parent
settings = {
"app_dir": str(app_dir),
"data_dir": str(app_dir / "data"),
"browser_profiles_dir": str(app_dir / "browser_profiles"),
"config_dir": str(app_dir / "config"),
}
# 确保必要目录存在
for dir_key in ["data_dir", "browser_profiles_dir", "config_dir"]:
os.makedirs(settings[dir_key], exist_ok=True)
# 加载环境设置并合并到应用设置中
env_settings = get_environment_settings()
settings.update(env_settings)
# 初始化日志设置 (环境管理器已经配置了基本的日志设置)
logger = logging.getLogger("douyin_imbot")
logger.info(
f"应用初始化完成,运行环境:{settings.get('environment', 'development')}"
)
return settings
def get_settings():
"""获取应用程序设置"""
global settings
if not settings:
return initialize_app()
return settings
# 程序启动时自动初始化
settings = initialize_app()
+388
View File
@@ -0,0 +1,388 @@
import os
import json
import logging
import time
import threading
from concurrent.futures import ThreadPoolExecutor, Future
from typing import Dict, List, Optional, Any, Tuple
from datetime import datetime
from playwright.sync_api import sync_playwright, Browser, Page
logger = logging.getLogger(__name__)
class BrowserManager:
"""管理浏览器实例和配置文件."""
def __init__(self, settings=None):
"""
初始化浏览器管理器.
Args:
settings: 应用程序设置
"""
self.settings = settings or {}
self.profiles_dir = self.settings.get(
"profiles_dir",
os.path.join(os.path.expanduser("~"), "douyin_imbot", "profiles"),
)
self.browsers: Dict[str, Browser] = {}
self.pages: Dict[str, Page] = {}
self.profile_details: Dict[str, Dict] = {}
self.playwright = None
self.executor = settings.get("browser_executor") if settings else None
self.running_tasks: Dict[str, Future] = {}
self.browser_locks: Dict[str, threading.Lock] = {}
self.load_profiles()
# 如果没有提供线程池,创建一个
if not self.executor:
max_workers = self.settings.get("browser_count", 3)
self.executor = ThreadPoolExecutor(max_workers=max_workers)
logger.info(f"已创建浏览器线程池,最大线程数: {max_workers}")
def load_profiles(self):
"""加载配置文件."""
try:
if not os.path.exists(self.profiles_dir):
os.makedirs(self.profiles_dir, exist_ok=True)
logger.info(f"创建配置文件目录: {self.profiles_dir}")
profiles_index_path = os.path.join(self.profiles_dir, "profiles.json")
if os.path.exists(profiles_index_path):
with open(profiles_index_path, "r", encoding="utf-8") as f:
self.profile_details = json.load(f)
logger.info(f"已加载 {len(self.profile_details)} 个配置文件")
else:
self.profile_details = {}
with open(profiles_index_path, "w", encoding="utf-8") as f:
json.dump({}, f)
logger.info("创建了空的配置文件索引")
except Exception as e:
logger.error(f"加载配置文件失败: {str(e)}")
self.profile_details = {}
def get_profiles(self):
"""获取所有配置文件."""
return list(self.profile_details.keys())
def get_profile_details(self, profile_name):
"""获取配置文件详情."""
return self.profile_details.get(profile_name, {})
def create_profile(self, profile_name, description=""):
"""创建新的配置文件."""
try:
if profile_name in self.profile_details:
logger.warning(f"配置文件 '{profile_name}' 已存在")
return False
# 创建配置目录
profile_dir = os.path.join(self.profiles_dir, profile_name)
os.makedirs(profile_dir, exist_ok=True)
# 记录配置信息
self.profile_details[profile_name] = {
"user_data_dir": profile_dir,
"created": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"last_used": "",
"description": description,
}
# 保存配置索引
self._save_profiles_index()
logger.info(f"创建了配置文件 '{profile_name}'")
return True
except Exception as e:
logger.error(f"创建配置文件失败: {str(e)}")
return False
def delete_profile(self, profile_name):
"""删除配置文件."""
try:
if profile_name not in self.profile_details:
logger.warning(f"配置文件 '{profile_name}' 不存在")
return False
# 如果浏览器正在运行,先关闭
self._close_browser(profile_name)
# 从列表中移除
del self.profile_details[profile_name]
# 保存配置索引
self._save_profiles_index()
logger.info(f"删除了配置文件 '{profile_name}'")
return True
except Exception as e:
logger.error(f"删除配置文件失败: {str(e)}")
return False
def _save_profiles_index(self):
"""保存配置文件索引."""
try:
profiles_index_path = os.path.join(self.profiles_dir, "profiles.json")
with open(profiles_index_path, "w", encoding="utf-8") as f:
json.dump(self.profile_details, f, ensure_ascii=False, indent=4)
logger.debug("配置文件索引已更新")
return True
except Exception as e:
logger.error(f"保存配置文件索引失败: {str(e)}")
return False
def _close_browser(self, profile_name):
"""关闭特定配置的浏览器."""
try:
# 获取此配置的锁
if profile_name in self.browser_locks:
with self.browser_locks[profile_name]:
if profile_name in self.browsers:
if profile_name in self.pages:
# 先关闭页面
try:
self.pages[profile_name].close()
except Exception as e:
logger.error(f"关闭页面失败: {str(e)}")
del self.pages[profile_name]
# 关闭浏览器
try:
self.browsers[profile_name].close()
except Exception as e:
logger.error(f"关闭浏览器失败: {str(e)}")
del self.browsers[profile_name]
logger.info(f"已关闭配置 '{profile_name}' 的浏览器")
# 删除锁
del self.browser_locks[profile_name]
elif profile_name in self.browsers:
# 没有锁但有浏览器实例,直接关闭
if profile_name in self.pages:
try:
self.pages[profile_name].close()
except Exception:
pass
del self.pages[profile_name]
try:
self.browsers[profile_name].close()
except Exception:
pass
del self.browsers[profile_name]
logger.info(f"已关闭配置 '{profile_name}' 的浏览器(无锁)")
# 取消正在运行的任务
if profile_name in self.running_tasks:
self.running_tasks[profile_name].cancel()
del self.running_tasks[profile_name]
logger.info(f"已取消配置 '{profile_name}' 的运行任务")
return True
except Exception as e:
logger.error(f"关闭浏览器 '{profile_name}' 时出错: {str(e)}")
return False
def launch_browser(self, profile_name, headless=False, on_complete=None):
"""
启动特定配置的浏览器.
Args:
profile_name: 配置文件名称
headless: 是否以无头模式启动
on_complete: 浏览器启动完成后的回调函数
Returns:
Future对象表示异步任务
"""
if profile_name not in self.profile_details:
logger.error(f"配置文件 '{profile_name}' 不存在")
return None
# 先检查该配置文件是否已有浏览器实例在运行
if profile_name in self.browsers:
logger.warning(f"配置 '{profile_name}' 已有浏览器实例在运行")
if on_complete:
on_complete(
self.browsers[profile_name], self.pages.get(profile_name), False
)
return None
# 为这个配置创建一个锁
if profile_name not in self.browser_locks:
self.browser_locks[profile_name] = threading.Lock()
# 提交任务到线程池
task = self.executor.submit(
self._launch_browser_task, profile_name, headless, on_complete
)
self.running_tasks[profile_name] = task
logger.info(f"已提交浏览器启动任务: {profile_name}")
return task
def _launch_browser_task(self, profile_name, headless, on_complete):
"""在线程中执行的浏览器启动任务."""
logger.info(f"开始在线程中启动浏览器: {profile_name}")
success = False
browser = None
page = None
try:
# 获取锁,确保同一配置文件只启动一个浏览器实例
with self.browser_locks[profile_name]:
# 再次检查浏览器是否已启动(双重检查锁定模式)
if profile_name in self.browsers:
logger.warning(
f"配置 '{profile_name}' 在获取锁后已有浏览器实例运行"
)
browser = self.browsers[profile_name]
page = self.pages.get(profile_name)
success = True
else:
# 获取配置信息
profile_info = self.profile_details[profile_name]
user_data_dir = profile_info.get("user_data_dir")
if not user_data_dir or not os.path.exists(
os.path.dirname(user_data_dir)
):
logger.error(
f"配置 '{profile_name}' 的用户数据目录无效: {user_data_dir}"
)
return False
# 启动浏览器
try:
self.playwright = sync_playwright().start()
logger.info(f"正在启动配置 '{profile_name}' 的浏览器...")
browser = self.playwright.chromium.launch_persistent_context(
user_data_dir=user_data_dir,
headless=headless,
args=[
"--disable-blink-features=AutomationControlled",
"--no-sandbox",
"--disable-setuid-sandbox",
],
)
# 等待浏览器完全启动
time.sleep(1)
# 创建或获取页面
if len(browser.pages) > 0:
page = browser.pages[0]
else:
page = browser.new_page()
# 存储浏览器和页面实例
self.browsers[profile_name] = browser
self.pages[profile_name] = page
# 更新最后使用时间
self.profile_details[profile_name][
"last_used"
] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
self._save_profiles_index()
logger.info(f"配置 '{profile_name}' 的浏览器启动成功")
success = True
except Exception as e:
logger.error(f"启动浏览器失败: {str(e)}")
if self.playwright:
try:
self.playwright.stop()
except Exception:
pass
raise e
except Exception as e:
logger.error(f"在线程中启动浏览器时出错: {str(e)}")
finally:
# 如果设置了回调,通知浏览器启动结果
if on_complete:
on_complete(browser, page, success)
# 删除运行任务记录
if profile_name in self.running_tasks:
del self.running_tasks[profile_name]
return success, browser, page
def navigate_to_url(self, profile_name, url):
"""
导航到指定URL.
Args:
profile_name: 配置文件名称
url: 要导航到的URL
Returns:
成功返回True,失败返回False
"""
try:
if profile_name not in self.browsers or profile_name not in self.pages:
logger.error(f"配置 '{profile_name}' 的浏览器未启动")
return False
page = self.pages[profile_name]
logger.info(f"浏览器 '{profile_name}' 导航到: {url}")
page.goto(url, wait_until="networkidle", timeout=60000)
return True
except Exception as e:
logger.error(f"导航到URL时出错: {str(e)}")
return False
def launch_multiple_browsers(
self, profile_names, headless=False, on_each_complete=None, on_all_complete=None
):
"""
并行启动多个浏览器.
Args:
profile_names: 配置文件名称列表
headless: 是否以无头模式启动
on_each_complete: 每个浏览器启动完成后的回调函数
on_all_complete: 所有浏览器启动完成后的回调函数
Returns:
包含所有Future对象的字典
"""
tasks = {}
results = {}
# 定义单个浏览器完成时的回调
def on_browser_complete(browser, page, success, profile):
results[profile] = (browser, page, success)
if on_each_complete:
on_each_complete(browser, page, success, profile)
# 检查是否所有浏览器都已启动
if len(results) == len(profile_names) and on_all_complete:
on_all_complete(results)
# 提交所有浏览器启动任务
for profile in profile_names:
# 为每个配置文件创建特定的回调
callback = lambda b, p, s, name=profile: on_browser_complete(b, p, s, name)
task = self.launch_browser(profile, headless, callback)
if task:
tasks[profile] = task
logger.info(f"已提交 {len(tasks)} 个浏览器启动任务")
return tasks
def close_all_browsers(self):
"""关闭所有浏览器实例."""
for profile_name in list(self.browsers.keys()):
self._close_browser(profile_name)
# 确保playwright实例被关闭
if self.playwright:
try:
self.playwright.stop()
self.playwright = None
except Exception as e:
logger.error(f"关闭Playwright实例失败: {str(e)}")
logger.info("已关闭所有浏览器实例")
return True
def __del__(self):
"""析构函数,确保资源被释放."""
self.close_all_browsers()
+223
View File
@@ -0,0 +1,223 @@
import os
import shutil
import logging
import uuid
from datetime import datetime
from typing import List, Dict, Optional, Any
logger = logging.getLogger(__name__)
class BrowserProfileManager:
"""浏览器用户配置文件管理器."""
def __init__(self, settings: Dict[str, Any]):
"""
初始化浏览器配置文件管理器.
Args:
settings: 应用程序设置字典
"""
self.settings = settings
self.base_cache_dir = settings.get("profiles_dir", "")
# 确保基础缓存目录存在
if self.base_cache_dir and not os.path.exists(self.base_cache_dir):
os.makedirs(self.base_cache_dir, exist_ok=True)
def get_all_profiles(self) -> List[Dict[str, Any]]:
"""
获取所有浏览器配置文件.
Returns:
List[Dict]: 配置文件列表
"""
return self.settings.get("browser_profiles", [])
def get_profile_by_id(self, profile_id: str) -> Optional[Dict[str, Any]]:
"""
通过ID获取浏览器配置文件.
Args:
profile_id: 配置文件ID
Returns:
Dict or None: 找到的配置文件或None
"""
profiles = self.get_all_profiles()
for profile in profiles:
if profile.get("profile_id") == profile_id:
return profile
return None
def create_profile(
self, profile_name: str, douyin_account: Optional[str] = None
) -> Dict[str, Any]:
"""
创建新的浏览器配置文件.
Args:
profile_name: 配置文件名称
douyin_account: 关联的抖音账号(可选)
Returns:
Dict: 新创建的配置文件
"""
if not self.base_cache_dir:
raise ValueError("未设置浏览器缓存目录")
profile_id = str(uuid.uuid4())
profile_dir = os.path.join(self.base_cache_dir, f"Profile_{profile_id}")
# 创建配置文件目录
os.makedirs(profile_dir, exist_ok=True)
new_profile = {
"profile_id": profile_id,
"profile_name": profile_name,
"profile_dir": profile_dir,
"enabled": True,
"douyin_account": douyin_account,
"auto_reply_enabled": True,
"creation_time": datetime.now().isoformat(),
"last_used_time": None,
}
# 添加到配置文件列表
profiles = self.get_all_profiles()
profiles.append(new_profile)
self.settings["browser_profiles"] = profiles
# 保存设置
from douyin_imbot.config.settings import save_settings
save_settings(self.settings)
logger.info(f"已创建新的浏览器配置文件: {profile_name}")
return new_profile
def update_profile(self, profile_id: str, **kwargs) -> Optional[Dict[str, Any]]:
"""
更新浏览器配置文件.
Args:
profile_id: 配置文件ID
**kwargs: 要更新的字段
Returns:
Dict or None: 更新后的配置文件或None
"""
profiles = self.get_all_profiles()
for i, profile in enumerate(profiles):
if profile.get("profile_id") == profile_id:
# 更新字段
for key, value in kwargs.items():
if key in profile and key != "profile_id": # 不允许更新profile_id
profile[key] = value
# 如果更新了名称,可能需要更新目录
if "profile_name" in kwargs:
logger.info(f"已更新配置文件名称: {kwargs['profile_name']}")
# 保存更新后的列表
self.settings["browser_profiles"] = profiles
from douyin_imbot.config.settings import save_settings
save_settings(self.settings)
return profile
logger.warning(f"未找到ID为{profile_id}的配置文件")
return None
def delete_profile(self, profile_id: str, delete_files: bool = False) -> bool:
"""
删除浏览器配置文件.
Args:
profile_id: 配置文件ID
delete_files: 是否同时删除配置文件目录
Returns:
bool: 删除成功返回True,否则返回False
"""
profiles = self.get_all_profiles()
for i, profile in enumerate(profiles):
if profile.get("profile_id") == profile_id:
# 如果需要,删除配置文件目录
if delete_files and "profile_dir" in profile:
profile_dir = profile["profile_dir"]
if os.path.exists(profile_dir):
try:
shutil.rmtree(profile_dir)
logger.info(f"已删除配置文件目录: {profile_dir}")
except Exception as e:
logger.error(f"删除配置文件目录时出错: {str(e)}")
# 从列表中删除
removed_profile = profiles.pop(i)
self.settings["browser_profiles"] = profiles
# 保存设置
from douyin_imbot.config.settings import save_settings
save_settings(self.settings)
logger.info(f"已删除配置文件: {removed_profile.get('profile_name')}")
return True
logger.warning(f"未找到ID为{profile_id}的配置文件")
return False
def set_base_cache_dir(self, directory: str) -> bool:
"""
设置浏览器缓存基础目录.
Args:
directory: 新的缓存目录路径
Returns:
bool: 设置成功返回True,否则返回False
"""
if not directory:
logger.error("缓存目录不能为空")
return False
# 确保目录存在
try:
os.makedirs(directory, exist_ok=True)
self.base_cache_dir = directory
self.settings["browser_cache_dir"] = directory
# 保存设置
from douyin_imbot.config.settings import save_settings
save_settings(self.settings)
logger.info(f"已设置浏览器缓存目录: {directory}")
return True
except Exception as e:
logger.error(f"设置缓存目录时出错: {str(e)}")
return False
def update_last_used_time(self, profile_id: str) -> None:
"""
更新配置文件的最后使用时间.
Args:
profile_id: 配置文件ID
"""
self.update_profile(profile_id, last_used_time=datetime.now().isoformat())
def clear_cache(self):
"""清除配置文件缓存"""
try:
# 清除所有缓存的配置文件信息
self._cached_profiles = None
# 强制下次请求重新从磁盘读取
self._last_refresh_time = 0
logger.debug("配置文件管理器缓存已清除")
return True
except Exception as e:
logger.error(f"清除配置文件管理器缓存失败: {str(e)}")
return False
+3
View File
@@ -0,0 +1,3 @@
"""配置管理模块."""
# 使 config 目录成为 Python 包
+127
View File
@@ -0,0 +1,127 @@
"""
环境配置模块,用于控制应用在不同环境(开发/生产)中的行为
"""
import os
import logging
import json
from pathlib import Path
from typing import Dict, Any
# 默认配置
DEFAULT_CONFIG = {
"environment": "development", # 可选:development, production
"log_level": {"development": "DEBUG", "production": "WARNING"},
"production_headless": True, # 生产环境是否使用无头浏览器
"data_dir": "data", # 数据存储目录
"config_dir": "config", # 配置文件目录
"log_dir": "logs", # 日志文件目录
}
class EnvironmentManager:
"""环境配置管理类"""
def __init__(self, config_file: str = None):
# 默认配置文件路径
self.app_dir = Path(__file__).parent.parent.parent
self.config_file = config_file or self.app_dir / "config" / "environment.json"
self.config = DEFAULT_CONFIG.copy()
# 尝试加载配置文件
self._load_config()
# 初始化日志配置
self._setup_logging()
def _load_config(self) -> Dict[str, Any]:
"""加载配置文件"""
try:
if os.path.exists(self.config_file):
with open(self.config_file, "r", encoding="utf-8") as f:
file_config = json.load(f)
self.config.update(file_config)
return self.config
except Exception as e:
print(f"加载配置文件失败: {str(e)},使用默认配置")
return self.config
def _setup_logging(self):
"""设置日志级别"""
env = self.config.get("environment", "development")
log_level_name = self.config.get("log_level", {}).get(env, "INFO")
# 将字符串转换为日志级别
log_level = getattr(logging, log_level_name, logging.INFO)
# 设置根日志记录器的级别
root_logger = logging.getLogger("douyin_imbot")
root_logger.setLevel(log_level)
# 如果没有处理器,添加一个控制台处理器
if not root_logger.handlers:
handler = logging.StreamHandler()
formatter = logging.Formatter(
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
handler.setFormatter(formatter)
root_logger.addHandler(handler)
# 如果是生产环境,还添加一个文件处理器
if env == "production":
log_dir = self.app_dir / self.config.get("log_dir", "logs")
os.makedirs(log_dir, exist_ok=True)
log_file = log_dir / f"douyin_imbot.log"
file_handler = logging.FileHandler(log_file)
file_handler.setFormatter(formatter)
root_logger.addHandler(file_handler)
def get_setting(self, key: str, default=None) -> Any:
"""获取特定配置项的值"""
return self.config.get(key, default)
def get_all_settings(self) -> Dict[str, Any]:
"""获取所有配置"""
return self.config.copy()
def update_setting(self, key: str, value: Any) -> bool:
"""更新配置项"""
try:
self.config[key] = value
return self._save_config()
except Exception as e:
print(f"更新配置失败: {str(e)}")
return False
def _save_config(self) -> bool:
"""保存配置到文件"""
try:
# 确保目录存在
os.makedirs(os.path.dirname(self.config_file), exist_ok=True)
with open(self.config_file, "w", encoding="utf-8") as f:
json.dump(self.config, f, indent=2, ensure_ascii=False)
return True
except Exception as e:
print(f"保存配置文件失败: {str(e)}")
return False
@property
def is_production(self) -> bool:
"""是否为生产环境"""
return self.config.get("environment", "").lower() == "production"
@property
def is_development(self) -> bool:
"""是否为开发环境"""
return not self.is_production
# 创建一个单例实例
env_manager = EnvironmentManager()
def get_environment_settings():
"""获取环境设置的辅助函数"""
return env_manager.get_all_settings()
+245
View File
@@ -0,0 +1,245 @@
import os
import json
import logging
import sys
from pathlib import Path
from typing import Dict, List, Optional, Any, Union
import uuid
from datetime import datetime
from pydantic import BaseModel, Field
logger = logging.getLogger(__name__)
# 更新默认设置以包含浏览器配置文件
DEFAULT_SETTINGS = {
"api_key": "",
"message_interval": 5,
"auto_start": False,
"theme": "system",
"color_theme": "blue",
"profiles_dir": os.path.join(os.path.expanduser("~"), "DouYinBot_Browser_Cache"),
"browser_profiles": [],
}
class UserProfile(BaseModel):
"""Chrome用户配置文件模型."""
profile_id: str
profile_name: str
douyin_account: Optional[str] = None
profile_path: Optional[str] = None
enabled: bool = True
auto_reply_enabled: bool = True
class ReplyTemplate(BaseModel):
"""回复模板模型."""
template_id: str
name: str
content: str
keywords: List[str] = Field(default_factory=list)
is_default: bool = False
class Settings(BaseModel):
"""应用程序设置模型."""
chrome_executable_path: Optional[str] = None
profiles: List[UserProfile] = Field(default_factory=list)
reply_templates: List[ReplyTemplate] = Field(default_factory=list)
check_interval_seconds: int = 60
headless: bool = False
auto_start_on_launch: bool = False
class BrowserProfile(BaseModel):
"""浏览器用户配置文件模型."""
profile_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
profile_name: str
profile_dir: str
enabled: bool = True
douyin_account: Optional[str] = None
auto_reply_enabled: bool = True
creation_time: str = Field(default_factory=lambda: datetime.now().isoformat())
last_used_time: Optional[str] = None
def get_config_dir() -> Path:
"""获取配置目录."""
config_dir = Path.home() / ".douyin-imbot"
config_dir.mkdir(exist_ok=True)
return config_dir
def get_config_path() -> Path:
"""获取配置文件路径."""
return get_config_dir() / "settings.json"
def load_settings() -> Settings:
"""加载应用程序设置."""
config_path = get_config_path()
if not config_path.exists():
# 创建默认设置
settings = Settings()
save_settings(settings)
return settings
try:
with open(config_path, "r", encoding="utf-8") as f:
data = json.load(f)
return Settings.model_validate(data)
except Exception as e:
print(f"加载设置时出错: {e}")
# 返回默认设置
return Settings()
def save_settings(settings: Settings) -> None:
"""保存应用程序设置."""
config_path = get_config_path()
try:
with open(config_path, "w", encoding="utf-8") as f:
json.dump(settings.model_dump(), f, ensure_ascii=False, indent=2)
except Exception as e:
print(f"保存设置时出错: {e}")
def find_chrome_installations() -> List[str]:
"""查找系统中的Chrome安装."""
possible_paths = []
# Windows上的常见Chrome安装路径
if os.name == "nt":
program_files = os.environ.get("PROGRAMFILES", "C:\\Program Files")
program_files_x86 = os.environ.get(
"PROGRAMFILES(X86)", "C:\\Program Files (x86)"
)
paths = [
Path(program_files) / "Google" / "Chrome" / "Application" / "chrome.exe",
Path(program_files_x86)
/ "Google"
/ "Chrome"
/ "Application"
/ "chrome.exe",
Path(os.environ.get("LOCALAPPDATA", ""))
/ "Google"
/ "Chrome"
/ "Application"
/ "chrome.exe",
]
for path in paths:
if path.exists():
possible_paths.append(str(path))
# macOS上的Chrome路径
elif sys.platform == "darwin":
paths = [
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
f"/Users/{os.getlogin()}/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
]
for path in paths:
if os.path.exists(path):
possible_paths.append(path)
# Linux上的Chrome路径
elif sys.platform.startswith("linux"):
paths = ["/usr/bin/google-chrome", "/usr/bin/google-chrome-stable"]
for path in paths:
if os.path.exists(path):
possible_paths.append(path)
return possible_paths
def find_chrome_user_data_dir() -> Optional[str]:
"""尝试找到Chrome用户数据目录."""
if os.name == "nt": # Windows
return os.path.join(
os.environ.get("LOCALAPPDATA", ""), "Google", "Chrome", "User Data"
)
elif sys.platform == "darwin": # macOS
return os.path.expanduser("~/Library/Application Support/Google/Chrome")
elif sys.platform.startswith("linux"): # Linux
return os.path.expanduser("~/.config/google-chrome")
return None
def load_settings():
"""
从配置文件加载设置,如果文件不存在则使用默认设置
Returns:
dict: 设置字典
"""
settings_path = get_settings_path()
try:
if os.path.exists(settings_path):
with open(settings_path, "r", encoding="utf-8") as f:
settings = json.load(f)
logger.info("已从文件加载设置")
# 确保所有默认设置键都存在
for key, value in DEFAULT_SETTINGS.items():
if key not in settings:
settings[key] = value
# 确保浏览器配置文件列表存在
if "browser_profiles" not in settings:
settings["browser_profiles"] = []
return settings
except Exception as e:
logger.error(f"加载设置时出错: {str(e)}")
logger.info("使用默认设置")
return DEFAULT_SETTINGS.copy()
def save_settings(settings):
"""
将设置保存到配置文件
Args:
settings (dict): 要保存的设置字典
Returns:
bool: 保存成功返回True,否则返回False
"""
settings_path = get_settings_path()
# 确保目录存在
os.makedirs(os.path.dirname(settings_path), exist_ok=True)
try:
with open(settings_path, "w", encoding="utf-8") as f:
json.dump(settings, f, ensure_ascii=False, indent=2)
logger.info("设置已保存到文件")
return True
except Exception as e:
logger.error(f"保存设置时出错: {str(e)}")
return False
def get_settings_path():
"""
获取设置文件的路径
Returns:
str: 设置文件路径
"""
base_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
return os.path.join(base_dir, "config", "settings.json")
+93
View File
@@ -0,0 +1,93 @@
import sys
import os
import subprocess
import logging
import customtkinter as ctk
import threading
from concurrent.futures import ThreadPoolExecutor
from douyin_imbot.ui.main_window import MainWindow
from douyin_imbot.config.settings import load_settings
from douyin_imbot.utils.logger import setup_logger
logger = logging.getLogger(__name__)
def ensure_playwright_browsers():
"""确保Playwright浏览器已安装"""
try:
# 尝试导入playwright,检查是否已安装
from playwright.sync_api import sync_playwright
# 检查是否已安装浏览器
try:
with sync_playwright() as p:
# 尝试启动浏览器,如果失败则需要安装
browser = p.chromium.launch()
browser.close()
logger.info("Playwright浏览器已安装")
return True
except Exception:
logger.warning("Playwright浏览器未安装,正在安装...")
# 安装浏览器
try:
result = subprocess.run(
[sys.executable, "-m", "playwright", "install", "chromium"],
check=True,
capture_output=True,
text=True,
)
logger.info("Playwright浏览器安装成功")
return True
except subprocess.CalledProcessError as e:
logger.error(f"安装Playwright浏览器失败: {e.stderr}")
return False
except ImportError:
logger.error("请先安装Playwright: pip install playwright")
return False
def main():
"""应用程序主入口点."""
# 设置日志记录器
setup_logger()
# 确保Playwright浏览器已安装
if not ensure_playwright_browsers():
print("无法启动应用程序,请确保Playwright及其浏览器已正确安装")
return
# 加载配置
settings = load_settings()
# 获取要启动的浏览器数量(默认为1)
browser_count = settings.get("browser_count", 1)
# 创建线程池用于管理浏览器实例
settings["browser_executor"] = ThreadPoolExecutor(max_workers=browser_count)
settings["browser_threads"] = []
logger.info(f"配置启动 {browser_count} 个浏览器实例")
# 设置 CustomTkinter 外观
ctk.set_appearance_mode(settings.get("theme", "System")) # 系统模式,跟随系统主题
ctk.set_default_color_theme(settings.get("color_theme", "blue")) # 默认颜色主题
# 创建并启动 CustomTkinter 应用
app = MainWindow(settings)
# 添加线程管理和关闭钩子
def on_closing():
logger.info("正在关闭应用程序和所有浏览器实例...")
# 关闭所有浏览器线程
if "browser_executor" in settings:
settings["browser_executor"].shutdown(wait=True)
app.destroy()
app.protocol("WM_DELETE_WINDOW", on_closing)
app.mainloop()
if __name__ == "__main__":
main()
+495
View File
@@ -0,0 +1,495 @@
import customtkinter as ctk
import logging
import traceback
import os
from douyin_imbot.browser.browser_manager import BrowserManager
from douyin_imbot.ui.profile_manager_panel import ProfileManagerPanel
logger = logging.getLogger(__name__)
class MainWindow(ctk.CTk):
"""抖音 IM 机器人主窗口."""
def __init__(self, settings=None):
"""
初始化主窗口.
Args:
settings: 应用程序配置
"""
try:
super().__init__()
self.settings = settings or {}
self.title("抖音自动回复机器人")
self.geometry("1100x700")
# 设置主题和外观
ctk.set_appearance_mode("System") # 系统主题
ctk.set_default_color_theme("blue") # 蓝色主题
# 创建浏览器管理器
self.browser_manager = BrowserManager(settings)
# 初始化变量
self.current_profile = None
self.profile_dialog = None
# 创建UI
self._create_widgets()
logger.info("主窗口初始化完成")
except Exception as e:
logger.error(f"主窗口初始化失败: {str(e)}")
logger.error(traceback.format_exc())
raise
def _create_widgets(self):
"""创建UI组件"""
try:
# 配置主网格
self.grid_columnconfigure(1, weight=1)
self.grid_rowconfigure(0, weight=1)
# 创建左侧导航栏
self.sidebar_frame = ctk.CTkFrame(self, width=200, corner_radius=0)
self.sidebar_frame.grid(row=0, column=0, sticky="nsew")
self.sidebar_frame.grid_rowconfigure(5, weight=1) # 增加行数以适应新的按钮
# 标题
self.app_title = ctk.CTkLabel(
self.sidebar_frame,
text="抖音 IM 机器人",
font=ctk.CTkFont(size=20, weight="bold"),
)
self.app_title.grid(row=0, column=0, padx=20, pady=(20, 20))
# 状态指示器
self.status_frame = ctk.CTkFrame(self.sidebar_frame)
self.status_frame.grid(row=1, column=0, padx=20, pady=(0, 20), sticky="ew")
self.status_label = ctk.CTkLabel(self.status_frame, text="状态:")
self.status_label.grid(row=0, column=0, padx=10, pady=5, sticky="w")
self.status_value = ctk.CTkLabel(
self.status_frame, text="未启动", text_color="gray"
)
self.status_value.grid(row=0, column=1, padx=10, pady=5, sticky="e")
# 导航按钮 - 修改为只有控制面板和设置按钮
self.dashboard_button = ctk.CTkButton(
self.sidebar_frame,
text="控制面板",
height=40,
command=self.show_dashboard,
)
self.dashboard_button.grid(row=2, column=0, padx=20, pady=10, sticky="ew")
self.settings_button = ctk.CTkButton(
self.sidebar_frame, text="设置", height=40, command=self.show_settings
)
self.settings_button.grid(row=3, column=0, padx=20, pady=10, sticky="ew")
self.about_button = ctk.CTkButton(
self.sidebar_frame, text="关于", height=40, command=self.show_about
)
self.about_button.grid(row=4, column=0, padx=20, pady=10, sticky="ew")
# 主内容区域 - 使用帧切换而不是选项卡
self.main_content = ctk.CTkFrame(self)
self.main_content.grid(row=0, column=1, padx=20, pady=20, sticky="nsew")
self.main_content.grid_columnconfigure(0, weight=1)
self.main_content.grid_rowconfigure(0, weight=1)
# 创建各个内容区域的帧,但暂时不显示
self.dashboard_frame = ctk.CTkFrame(self.main_content)
self.settings_frame = ctk.CTkFrame(self.main_content)
self.about_frame = ctk.CTkFrame(self.main_content)
# 初始化各个内容区域
self.setup_dashboard()
self.setup_settings_panel()
self.setup_about_panel()
# 默认显示控制面板
self.show_dashboard()
# 确保配置文件列表在UI加载后刷新
self.after(500, self.refresh_profile_list)
logger.info("UI 组件创建完成")
except Exception as e:
logger.error(f"创建UI组件失败: {str(e)}")
logger.error(traceback.format_exc())
self.update_log(f"UI初始化错误: {str(e)}")
def setup_dashboard(self):
"""设置控制面板."""
try:
# 配置控制面板帧
self.dashboard_frame.grid_columnconfigure(0, weight=1)
self.dashboard_frame.grid_rowconfigure(
2, weight=1
) # 减少行数,因为移除了控制按钮区域
# 集成浏览器配置管理面板
self.profile_manager_frame = ctk.CTkFrame(self.dashboard_frame)
self.profile_manager_frame.grid(
row=1, column=0, padx=10, pady=10, sticky="ew"
)
# 创建配置管理器
self.profile_manager = ProfileManagerPanel(
self.profile_manager_frame,
self.browser_manager,
on_profile_change=self.on_profile_change,
)
self.profile_manager.pack(fill="both", expand=True)
# 移除控制按钮区域
# 日志区域
log_frame = ctk.CTkFrame(self.dashboard_frame)
log_frame.grid(row=2, column=0, padx=10, pady=10, sticky="nsew")
log_frame.grid_columnconfigure(0, weight=1)
log_frame.grid_rowconfigure(1, weight=1)
log_label = ctk.CTkLabel(
log_frame, text="运行日志:", anchor="w", font=ctk.CTkFont(size=16)
)
log_label.grid(row=0, column=0, padx=10, pady=(10, 5), sticky="w")
self.log_textbox = ctk.CTkTextbox(log_frame, height=200)
self.log_textbox.grid(row=1, column=0, padx=10, pady=(0, 10), sticky="nsew")
self.log_textbox.insert("0.0", "准备就绪...\n")
self.log_textbox.configure(state="disabled")
# 清除日志按钮
self.clear_log_button = ctk.CTkButton(
log_frame, text="清除日志", command=self.clear_log, width=100
)
self.clear_log_button.grid(
row=2, column=0, padx=10, pady=(0, 10), sticky="e"
)
logger.info("控制面板设置完成")
except Exception as e:
logger.error(f"设置控制面板失败: {str(e)}")
logger.error(traceback.format_exc())
self.update_log(f"控制面板初始化错误: {str(e)}")
def setup_settings_panel(self):
"""设置设置面板."""
try:
self.settings_frame.grid_columnconfigure(0, weight=1)
# 标题
title = ctk.CTkLabel(
self.settings_frame,
text="设置",
font=ctk.CTkFont(size=24, weight="bold"),
)
title.grid(row=0, column=0, padx=20, pady=20)
# 设置选项
options_frame = ctk.CTkFrame(self.settings_frame)
options_frame.grid(row=1, column=0, padx=20, pady=10, sticky="ew")
options_frame.grid_columnconfigure(1, weight=1)
# API 密钥设置
api_label = ctk.CTkLabel(options_frame, text="API 密钥:", width=150)
api_label.grid(row=0, column=0, padx=10, pady=15, sticky="w")
self.entry_api_key = ctk.CTkEntry(options_frame, width=400)
self.entry_api_key.grid(row=0, column=1, padx=10, pady=15, sticky="ew")
self.entry_api_key.insert(0, self.settings.get("api_key", ""))
# 消息间隔设置
interval_label = ctk.CTkLabel(
options_frame, text="消息间隔 (秒):", width=150
)
interval_label.grid(row=1, column=0, padx=10, pady=15, sticky="w")
self.entry_message_interval = ctk.CTkEntry(options_frame, width=400)
self.entry_message_interval.grid(
row=1, column=1, padx=10, pady=15, sticky="ew"
)
self.entry_message_interval.insert(
0, self.settings.get("message_interval", "5")
)
# 高级设置区域
advanced_frame = ctk.CTkFrame(self.settings_frame)
advanced_frame.grid(row=2, column=0, padx=20, pady=(20, 10), sticky="ew")
advanced_label = ctk.CTkLabel(
advanced_frame,
text="高级设置",
font=ctk.CTkFont(size=18, weight="bold"),
)
advanced_label.pack(pady=10, padx=10, anchor="w")
# 保存按钮
save_button = ctk.CTkButton(
self.settings_frame,
text="保存设置",
command=self.save_settings,
width=200,
height=40,
font=ctk.CTkFont(size=16),
)
save_button.grid(row=3, column=0, pady=30)
logger.info("设置面板设置完成")
except Exception as e:
logger.error(f"设置设置面板失败: {str(e)}")
logger.error(traceback.format_exc())
self.update_log(f"设置面板初始化错误: {str(e)}")
def setup_about_panel(self):
"""设置关于面板."""
try:
self.about_frame.grid_columnconfigure(0, weight=1)
# 标题
title = ctk.CTkLabel(
self.about_frame, text="关于", font=ctk.CTkFont(size=24, weight="bold")
)
title.grid(row=0, column=0, padx=20, pady=20)
# 信息卡片
info_frame = ctk.CTkFrame(self.about_frame)
info_frame.grid(row=1, column=0, padx=40, pady=20, sticky="ew")
version_label = ctk.CTkLabel(
info_frame, text="抖音 IM 机器人 v1.0.0", font=ctk.CTkFont(size=18)
)
version_label.pack(pady=(20, 10))
description = ctk.CTkLabel(
info_frame,
text="抖音 IM 机器人是一个自动回复工具,可以帮助你自动处理抖音私信。",
wraplength=500,
font=ctk.CTkFont(size=14),
)
description.pack(pady=10, padx=30)
# 添加更多关于信息
features_label = ctk.CTkLabel(
info_frame,
text="主要功能:",
font=ctk.CTkFont(size=16, weight="bold"),
anchor="w",
)
features_label.pack(pady=(20, 10), padx=30, anchor="w")
features_text = """• 自动回复抖音私信
• 支持多种浏览器配置
• 自定义回复内容
• 日志记录与监控"""
features = ctk.CTkLabel(
info_frame,
text=features_text,
font=ctk.CTkFont(size=14),
justify="left",
anchor="w",
)
features.pack(pady=5, padx=30, anchor="w")
copyright_label = ctk.CTkLabel(
info_frame, text="© 2023 版权所有", font=ctk.CTkFont(size=12)
)
copyright_label.pack(pady=20)
logger.info("关于面板设置完成")
except Exception as e:
logger.error(f"设置关于面板失败: {str(e)}")
logger.error(traceback.format_exc())
self.update_log(f"关于面板初始化错误: {str(e)}")
def show_dashboard(self):
"""显示控制面板."""
try:
self._hide_all_frames()
self.dashboard_frame.grid(row=0, column=0, sticky="nsew")
self.highlight_button(self.dashboard_button)
logger.debug("显示控制面板")
except Exception as e:
logger.error(f"显示控制面板失败: {str(e)}")
logger.error(traceback.format_exc())
def show_settings(self):
"""显示设置界面."""
try:
self._hide_all_frames()
self.settings_frame.grid(row=0, column=0, sticky="nsew")
self.highlight_button(self.settings_button)
logger.debug("显示设置面板")
except Exception as e:
logger.error(f"显示设置面板失败: {str(e)}")
logger.error(traceback.format_exc())
def show_about(self):
"""显示关于界面."""
try:
self._hide_all_frames()
self.about_frame.grid(row=0, column=0, sticky="nsew")
self.highlight_button(self.about_button)
logger.debug("显示关于面板")
except Exception as e:
logger.error(f"显示关于面板失败: {str(e)}")
logger.error(traceback.format_exc())
def _hide_all_frames(self):
"""隐藏所有内容帧."""
try:
for frame in [
self.dashboard_frame,
self.settings_frame,
self.about_frame,
]:
frame.grid_forget()
except Exception as e:
logger.error(f"隐藏内容帧失败: {str(e)}")
logger.error(traceback.format_exc())
def highlight_button(self, active_button):
"""高亮当前活动按钮."""
try:
for button in [
self.dashboard_button,
self.settings_button,
self.about_button,
]:
if button == active_button:
button.configure(
fg_color=["#3a7ebf", "#1f538d"]
) # 使用主题较深的颜色
else:
button.configure(fg_color=["#3B8ED0", "#1E6FBA"]) # 恢复默认颜色
except Exception as e:
logger.error(f"高亮按钮失败: {str(e)}")
logger.error(traceback.format_exc())
def update_log(self, message):
"""更新日志文本框."""
try:
if hasattr(self, "log_textbox") and self.log_textbox:
self.log_textbox.configure(state="normal")
self.log_textbox.insert("end", f"{message}\n")
self.log_textbox.see("end")
self.log_textbox.configure(state="disabled")
else:
logger.warning(f"日志无法更新,日志组件未初始化: {message}")
except Exception as e:
logger.error(f"更新日志失败: {str(e)}")
logger.error(traceback.format_exc())
def clear_log(self):
"""清除日志内容."""
try:
self.log_textbox.configure(state="normal")
self.log_textbox.delete("0.0", "end")
self.log_textbox.insert("0.0", "日志已清除...\n")
self.log_textbox.configure(state="disabled")
logger.info("日志已清除")
except Exception as e:
logger.error(f"清除日志失败: {str(e)}")
logger.error(traceback.format_exc())
def save_settings(self):
"""保存设置."""
try:
# 收集所有设置项
for key in ["api_key", "message_interval"]:
entry = getattr(self, f"entry_{key}", None)
if entry:
self.settings[key] = entry.get()
# 保存设置到文件
from douyin_imbot.config.settings import save_settings
if save_settings(self.settings):
logger.info("设置已保存")
self.update_log("设置已保存")
else:
logger.error("保存设置失败")
self.update_log("保存设置失败")
except Exception as e:
logger.error(f"保存设置失败: {str(e)}")
logger.error(traceback.format_exc())
self.update_log(f"保存设置失败: {str(e)}")
def on_profile_selected(self, profile_name):
"""当用户选择不同的配置文件时触发"""
try:
if not profile_name:
logger.warning("配置文件名为空")
self.update_log("选择了无效的配置文件")
return
if profile_name == "无可用配置":
logger.warning("选择了无效的配置文件")
self.update_log("选择了无效的配置文件")
return
self.current_profile = profile_name
logger.info(f"用户选择配置文件: {profile_name}")
self.update_log(f"已选择配置文件: {profile_name}")
# 更新配置文件详情显示
try:
profile_details = (
self.browser_manager.get_profile_details(profile_name) or {}
)
details_text = (
f"用户目录: {profile_details.get('user_data_dir', '无')}\n"
)
details_text += f"上次使用: {profile_details.get('last_used', '从未')}"
except Exception as detail_error:
logger.error(f"获取配置详情失败: {str(detail_error)}")
logger.error(traceback.format_exc())
details_text = "获取配置详情失败"
# 检查组件是否已创建
if hasattr(self, "profile_details_label") and self.profile_details_label:
self.profile_details_label.configure(text=details_text)
else:
logger.warning("配置详情标签未创建")
except Exception as e:
logger.error(f"选择配置文件时出错: {str(e)}")
logger.error(traceback.format_exc())
self.update_log(f"选择配置文件时出错: {str(e)}")
def refresh_profile_list(self):
"""刷新配置文件列表"""
try:
# 使用配置管理器刷新配置
if hasattr(self, "profile_manager") and self.profile_manager:
self.profile_manager.refresh_profiles()
self.update_log("配置文件列表已刷新")
return True
return False
except Exception as e:
logger.error(f"刷新配置文件列表失败: {str(e)}")
logger.error(traceback.format_exc())
self.update_log(f"刷新配置文件列表失败: {str(e)}")
return False
def on_profile_change(self):
"""
当浏览器配置文件发生变化时调用
可以在这里更新UI或执行其他操作
"""
try:
if hasattr(self, "refresh_profile_list"):
# 使用after方法避免在对话框销毁期间刷新UI
self.after(100, self.refresh_profile_list)
logger.info("配置文件已更改,列表已刷新")
except Exception as e:
logger.error(f"响应配置文件变化失败: {str(e)}")
logger.error(traceback.format_exc())
self.update_log(f"更新配置列表失败: {str(e)}")
File diff suppressed because it is too large. Load diff
+55
View File
@@ -0,0 +1,55 @@
import customtkinter as ctk
from tkinter import filedialog
import os
# ...existing code...
class SettingsPanel(ctk.CTkFrame):
def __init__(self, master, settings, *args, **kwargs):
super().__init__(master, *args, **kwargs)
self.settings = settings
# ...existing code...
# Add browser cache directory setting
self.cache_dir_label = ctk.CTkLabel(self, text="浏览器缓存目录:")
self.cache_dir_label.grid(
row=self.next_row, column=0, sticky="w", padx=10, pady=5
)
cache_dir_frame = ctk.CTkFrame(self, fg_color="transparent")
cache_dir_frame.grid(row=self.next_row, column=1, sticky="ew", padx=10, pady=5)
self.cache_dir_entry = ctk.CTkEntry(cache_dir_frame, width=250)
self.cache_dir_entry.pack(side="left", fill="x", expand=True)
self.cache_dir_entry.insert(0, self.settings.get("profiles_dir", ""))
self.browse_cache_button = ctk.CTkButton(
cache_dir_frame, text="浏览...", width=80, command=self.browse_cache_dir
)
self.browse_cache_button.pack(side="right", padx=(5, 0))
self.next_row += 1
# ...existing code...
def browse_cache_dir(self):
"""浏览并选择浏览器缓存目录"""
directory = filedialog.askdirectory(
title="选择浏览器缓存目录",
initialdir=(
self.cache_dir_entry.get()
if self.cache_dir_entry.get()
else os.path.expanduser("~")
),
)
if directory:
self.cache_dir_entry.delete(0, "end")
self.cache_dir_entry.insert(0, directory)
def save_settings(self):
# ...existing code...
self.settings["profiles_dir"] = self.cache_dir_entry.get()
# ...existing code...
# ...existing code...
+3
View File
@@ -0,0 +1,3 @@
"""工具函数模块."""
# 使 utils 目录成为 Python 包
+147
View File
@@ -0,0 +1,147 @@
import logging
import time
from typing import Optional
from playwright.sync_api import Page, TimeoutError as PlaywrightTimeoutError
logger = logging.getLogger(__name__)
class BrowserUtils:
"""浏览器操作工具类"""
@staticmethod
def wait_for_element(page: Page, selector: str, timeout: int = 30000) -> bool:
"""
等待元素出现
Args:
page: Playwright页面对象
selector: CSS选择器
timeout: 超时时间(毫秒)
Returns:
bool: 成功返回True,超时返回False
"""
try:
page.wait_for_selector(selector, timeout=timeout)
return True
except PlaywrightTimeoutError:
logger.warning(f"等待元素超时: {selector}")
return False
@staticmethod
def fill_input(
page: Page, selector: str, text: str, clear_first: bool = True
) -> bool:
"""
填充输入框
Args:
page: Playwright页面对象
selector: CSS选择器
text: 要填充的文本
clear_first: 是否先清除原有内容
Returns:
bool: 成功返回True,失败返回False
"""
try:
# 等待元素出现
if not BrowserUtils.wait_for_element(page, selector):
return False
# 如果需要先清除原有内容
if clear_first:
page.fill(selector, "")
# 填充文本
page.fill(selector, text)
return True
except Exception as e:
logger.error(f"填充输入框时出错: {str(e)}")
return False
@staticmethod
def click_element(page: Page, selector: str, timeout: int = 30000) -> bool:
"""
点击元素
Args:
page: Playwright页面对象
selector: CSS选择器
timeout: 超时时间(毫秒)
Returns:
bool: 成功返回True,失败返回False
"""
try:
# 等待元素出现
if not BrowserUtils.wait_for_element(page, selector, timeout):
return False
# 点击元素
page.click(selector)
return True
except Exception as e:
logger.error(f"点击元素时出错: {str(e)}")
return False
@staticmethod
def get_text(page: Page, selector: str) -> Optional[str]:
"""
获取元素文本内容
Args:
page: Playwright页面对象
selector: CSS选择器
Returns:
str or None: 元素文本内容或None
"""
try:
# 等待元素出现
if not BrowserUtils.wait_for_element(page, selector):
return None
# 获取文本内容
return page.text_content(selector)
except Exception as e:
logger.error(f"获取元素文本内容时出错: {str(e)}")
return None
@staticmethod
def take_screenshot(page: Page, path: str) -> bool:
"""
截取屏幕截图
Args:
page: Playwright页面对象
path: 截图保存路径
Returns:
bool: 成功返回True,失败返回False
"""
try:
page.screenshot(path=path)
logger.info(f"已保存截图: {path}")
return True
except Exception as e:
logger.error(f"截图时出错: {str(e)}")
return False
@staticmethod
def is_element_visible(page: Page, selector: str) -> bool:
"""
检查元素是否可见
Args:
page: Playwright页面对象
selector: CSS选择器
Returns:
bool: 可见返回True,不可见返回False
"""
try:
return page.is_visible(selector)
except Exception:
return False
+57
View File
@@ -0,0 +1,57 @@
import os
import logging
from logging.handlers import RotatingFileHandler
import time
def setup_logger():
"""
配置应用程序日志记录器
- 将日志输出到控制台
- 将日志保存到文件(带有时间戳的文件名)
- 使用轮换文件处理程序限制日志文件大小
"""
# 确保日志目录存在
logs_dir = os.path.join(
os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "logs"
)
os.makedirs(logs_dir, exist_ok=True)
# 创建时间戳日志文件名
timestamp = time.strftime("%Y%m%d-%H%M%S")
log_file = os.path.join(logs_dir, f"douyin_imbot_{timestamp}.log")
# 配置根日志记录器
root_logger = logging.getLogger()
root_logger.setLevel(logging.INFO)
# 清除任何现有的处理程序
for handler in root_logger.handlers[:]:
root_logger.removeHandler(handler)
# 创建控制台处理程序
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
console_format = logging.Formatter(
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
console_handler.setFormatter(console_format)
root_logger.addHandler(console_handler)
# 创建文件处理程序(限制每个文件大小为5MB,最多保留5个备份)
file_handler = RotatingFileHandler(
log_file, maxBytes=5 * 1024 * 1024, backupCount=5, encoding="utf-8" # 5 MB
)
file_handler.setLevel(logging.INFO)
file_format = logging.Formatter(
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
file_handler.setFormatter(file_format)
root_logger.addHandler(file_handler)
# 记录启动信息
logging.info("日志系统初始化完成")
logging.info(f"日志文件: {log_file}")
return root_logger