first commit
This commit is contained in:
commit
4890793ce5
1029 files changed
+5992
No files matched your search
@@ -0,0 +1,3 @@
|
||||
"""抖音私信自动回复机器人模块."""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
Binary file not shown.
@@ -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()
|
||||
Binary file not shown.
Binary file not shown.
@@ -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()
|
||||
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
"""配置管理模块."""
|
||||
|
||||
# 使 config 目录成为 Python 包
|
||||
Binary file not shown.
Binary file not shown.
@@ -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()
|
||||
@@ -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")
|
||||
@@ -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()
|
||||
Binary file not shown.
Binary file not shown.
@@ -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
@@ -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...
|
||||
@@ -0,0 +1,3 @@
|
||||
"""工具函数模块."""
|
||||
|
||||
# 使 utils 目录成为 Python 包
|
||||
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
@@ -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
|
||||
Reference in new issue
Block a user