企业级iMessage群发系统实战:单主机管控多iPhone设备完整实现

单主机管控多iPhone设备:企业级iMessage批量消息系统实战

iMessage群发系统在企业内部沟通、客户服务和信息通知等场景中有着广泛的应用需求。作为一家中型企业的IT运维负责人,我在过去半年里一直在探索如何构建一套稳定可靠、易于维护的企业级iMessage消息发送解决方案。我们公司有超过200名员工使用iPhone设备,传统的邮件通知响应率低,而iMessage作为苹果生态内原生的即时通讯工具,送达率和打开率都远高于其他渠道。经过多次技术调研和原型验证,我最终实现了一套基于单主机管控多台iPhone设备的iMessage批量消息系统,无需越狱,无需购买昂贵的第三方服务,完全自主可控。本文将详细分享整个系统的设计思路、实现过程以及在实际使用中遇到的问题和解决方案。

一、iMessage群发系统的企业级需求与技术选型

在开始开发之前,我首先明确了企业级iMessage群发系统必须满足的核心需求。第一是稳定性,系统需要能够7×24小时不间断运行,支持同时管控至少 20 台 iPhone 设备,单批次发送消息量不低于1000条。第二是安全性,所有消息数据必须在企业内部处理,不能上传到任何第三方服务器,避免敏感信息泄露。第三是可扩展性,系统架构要易于扩展,能够根据业务需求增加设备数量和功能模块。第四是易用性,提供简单直观的操作界面,非技术人员也能轻松使用。

基于这些需求,我对比了多种技术方案。最初考虑过使用苹果官方的Business Chat API,但它需要企业资质审核,且有严格的发送限制和费用,不适合我们的内部使用场景。然后又研究了一些第三方的iMessage群发工具,但大多存在安全隐患,且无法进行深度定制。最终我选择了基于libimobiledevice开源库的方案,它允许通过 USB 连接 iOS 设备,执行各种系统操作,包括发送iMessage消息。这个方案完全开源免费,不需要越狱设备,所有数据都在本地处理,非常符合我们的企业级需求。

开发语言我选择了Python 3.9,因为它有丰富的第三方库支持,开发效率高,且团队成员都比较熟悉。界面部分使用PyQt5开发,数据库使用SQLite存储设备信息和发送记录。下面是系统的基础依赖安装代码:

复制代码
# 基础依赖安装脚本 install_dependencies.py
import subprocess
import sys

def install_package(package):
    subprocess.check_call([sys.executable, "-m", "pip", "install", package])

if __name__ == "__main__":
    print("正在安装iMessage群发系统依赖包...")
    
    # 核心依赖
    install_package("pyqt5==5.15.9")
    install_package("libimobiledevice==1.3.0")
    install_package("pymobiledevice3==2.30.0")
    install_package("sqlalchemy==2.0.20")
    install_package("python-dotenv==1.0.0")
    install_package("schedule==1.2.0")
    install_package("pandas==2.1.0")
    install_package("openpyxl==3.1.2")
    
    # 工具类依赖
    install_package("loguru==0.7.2")
    install_package("tqdm==4.66.1")
    install_package("psutil==5.9.5")
    install_package("pyperclip==1.8.2")
    
    print("所有依赖包安装完成!")
    print("请确保已安装libimobiledevice系统依赖:")
    print("Ubuntu/Debian: sudo apt-get install libimobiledevice-utils usbmuxd")
    print("macOS: brew install libimobiledevice usbmuxd")
    print("Windows: 请下载安装libimobiledevice Windows版本")

二、单主机多iPhone设备的硬件连接方案

硬件连接是整个系统的基础,直接影响系统的稳定性和可扩展性。最初我尝试直接使用电脑的USB接口连接多台iPhone,但很快发现了问题。普通电脑的 USB 接口数量有限,且供电不足,连接超过5台设备就会出现设备频繁断开的情况。而且USB线缆过长也会导致信号衰减,影响通信稳定性。

经过多次测试,我最终采用了"USB集线器+有源供电"的方案。选择了一款工业级的16口USB 3.0 集线器,每个端口都有独立的供电模块,能够为每台iPhone提供稳定的5V/2A电流。同时使用高质量的屏蔽USB线缆,长度控制在1米以内,有效减少了信号干扰。为了方便管理,我还定制了一个设备架,将所有iPhone设备整齐排列,便于散热和维护。

在软件层面,需要确保usbmuxd服务正常运行,它负责在主机和iOS设备之间建立通信通道。下面是设备连接检测和初始化的代码:

复制代码
# 设备连接管理模块 device_manager.py
import subprocess
import time
import threading
from typing import List, Dict, Optional
from loguru import logger
from sqlalchemy import create_engine, Column, String, Integer, Boolean, DateTime
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
import datetime

Base = declarative_base()

class Device(Base):
    __tablename__ = "devices"
    
    id = Column(Integer, primary_key=True, autoincrement=True)
    udid = Column(String(40), unique=True, nullable=False)
    name = Column(String(100), nullable=False)
    model = Column(String(50), nullable=False)
    ios_version = Column(String(20), nullable=False)
    is_connected = Column(Boolean, default=False)
    is_enabled = Column(Boolean, default=True)
    last_connected = Column(DateTime, default=datetime.datetime.now)
    last_disconnected = Column(DateTime, nullable=True)
    total_messages_sent = Column(Integer, default=0)
    daily_messages_sent = Column(Integer, default=0)
    last_message_time = Column(DateTime, nullable=True)

class DeviceManager:
    def __init__(self, db_path: str = "imessage_system.db"):
        self.engine = create_engine(f"sqlite:///{db_path}")
        Base.metadata.create_all(self.engine)
        Session = sessionmaker(bind=self.engine)
        self.session = Session()
        self.devices: Dict[str, Device] = {}
        self.monitor_thread: Optional[threading.Thread] = None
        self.is_monitoring = False
        self.load_saved_devices()
    
    def load_saved_devices(self):
        """从数据库加载已保存的设备信息"""
        saved_devices = self.session.query(Device).all()
        for device in saved_devices:
            self.devices[device.udid] = device
            device.is_connected = False
        logger.info(f"从数据库加载了 {len(saved_devices)} 台设备信息")
    
    def get_connected_devices(self) -> List[str]:
        """获取当前连接的所有设备UDID"""
        try:
            result = subprocess.run(
                ["idevice_id", "-l"],
                capture_output=True,
                text=True,
                timeout=10
            )
            if result.returncode != 0:
                logger.error(f"获取设备列表失败: {result.stderr}")
                return []
            
            udids = [line.strip() for line in result.stdout.splitlines() if line.strip()]
            return udids
        except Exception as e:
            logger.error(f"获取设备列表异常: {str(e)}")
            return []
    
    def get_device_info(self, udid: str) -> Optional[Dict]:
        """获取指定设备的详细信息"""
        try:
            result = subprocess.run(
                ["ideviceinfo", "-u", udid],
                capture_output=True,
                text=True,
                timeout=15
            )
            if result.returncode != 0:
                logger.error(f"获取设备 {udid} 信息失败: {result.stderr}")
                return None
            
            info = {}
            for line in result.stdout.splitlines():
                if ":" in line:
                    key, value = line.split(":", 1)
                    info[key.strip()] = value.strip()
            
            return {
                "udid": udid,
                "name": info.get("DeviceName", "未知设备"),
                "model": info.get("ProductType", "未知型号"),
                "ios_version": info.get("ProductVersion", "未知版本")
            }
        except Exception as e:
            logger.error(f"获取设备 {udid} 信息异常: {str(e)}")
            return None
    
    def add_device(self, udid: str) -> bool:
        """添加新设备到系统"""
        if udid in self.devices:
            logger.warning(f"设备 {udid} 已存在")
            return False
        
        device_info = self.get_device_info(udid)
        if not device_info:
            return False
        
        new_device = Device(
            udid=device_info["udid"],
            name=device_info["name"],
            model=device_info["model"],
            ios_version=device_info["ios_version"],
            is_connected=True,
            last_connected=datetime.datetime.now()
        )
        
        self.session.add(new_device)
        self.session.commit()
        self.devices[udid] = new_device
        logger.info(f"成功添加新设备: {device_info['name']} ({udid})")
        return True
    
    def update_device_status(self):
        """更新所有设备的连接状态"""
        current_udids = self.get_connected_devices()
        
        # 更新已连接设备
        for udid in current_udids:
            if udid in self.devices:
                if not self.devices[udid].is_connected:
                    self.devices[udid].is_connected = True
                    self.devices[udid].last_connected = datetime.datetime.now()
                    logger.info(f"设备 {self.devices[udid].name} 已连接")
            else:
                self.add_device(udid)
        
        # 更新已断开设备
        for udid, device in self.devices.items():
            if udid not in current_udids and device.is_connected:
                device.is_connected = False
                device.last_disconnected = datetime.datetime.now()
                logger.info(f"设备 {device.name} 已断开连接")
        
        self.session.commit()
    
    def start_monitoring(self, interval: int = 5):
        """启动设备连接监控线程"""
        if self.is_monitoring:
            logger.warning("设备监控已在运行中")
            return
        
        self.is_monitoring = True
        self.monitor_thread = threading.Thread(
            target=self._monitor_loop,
            args=(interval,),
            daemon=True
        )
        self.monitor_thread.start()
        logger.info(f"设备监控已启动,检查间隔: {interval}秒")
    
    def stop_monitoring(self):
        """停止设备连接监控"""
        self.is_monitoring = False
        if self.monitor_thread and self.monitor_thread.is_alive():
            self.monitor_thread.join()
        logger.info("设备监控已停止")
    
    def _monitor_loop(self, interval: int):
        """设备监控循环"""
        while self.is_monitoring:
            try:
                self.update_device_status()
            except Exception as e:
                logger.error(f"设备监控循环异常: {str(e)}")
            time.sleep(interval)
    
    def get_enabled_devices(self) -> List[Device]:
        """获取所有已启用且已连接的设备"""
        return [
            device for device in self.devices.values()
            if device.is_connected and device.is_enabled
        ]
    
    def increment_message_count(self, udid: str):
        """增加设备的消息发送计数"""
        if udid in self.devices:
            device = self.devices[udid]
            device.total_messages_sent += 1
            device.daily_messages_sent += 1
            device.last_message_time = datetime.datetime.now()
            self.session.commit()
    
    def reset_daily_counts(self):
        """重置所有设备的每日发送计数"""
        for device in self.devices.values():
            device.daily_messages_sent = 0
        self.session.commit()
        logger.info("已重置所有设备的每日发送计数")

基于libimobiledevice的设备通信基础实现

libimobiledevice是一个开源的跨平台库,用于与iOS设备进行通信。它实现了苹果的USB多路复用协议,允许我们在不使用iTunes的情况下与iOS设备进行交互。在我们的iMessage群发系统中,libimobiledevice是核心的通信基础,负责发送iMessage消息、获取设备信息、执行系统命令等操作。

最初我尝试直接使用libimobiledevice的命令行工具来发送消息,但发现这种方式效率低下,且难以处理复杂的错误情况。后来我找到了pymobiledevice3这个优秀的Python库,它对libimobiledevice进行了全面的封装,提供了更加友好的API接口,大大简化了开发工作。

下面是基于pymobiledevice3实现的iMessage消息发送核心代码:

复制代码
# iMessage发送模块 imessage_sender.py
import subprocess
import time
import re
from typing import Optional, List
from loguru import logger
from pymobiledevice3.lockdown import LockdownClient
from pymobiledevice3.services.springboard import SpringboardService
from pymobiledevice3.services.installation_proxy import InstallationProxyService
from pymobiledevice3.services.notification_proxy import NotificationProxyService
import datetime

class iMessageSender:
    def __init__(self, udid: str):
        self.udid = udid
        self.lockdown_client: Optional[LockdownClient] = None
        self.is_connected = False
        self.connect()
    
    def connect(self) -> bool:
        """连接到iOS设备"""
        try:
            self.lockdown_client = LockdownClient(udid=self.udid)
            self.is_connected = True
            logger.info(f"成功连接到设备 {self.udid}")
            return True
        except Exception as e:
            self.is_connected = False
            logger.error(f"连接设备 {self.udid} 失败: {str(e)}")
            return False
    
    def disconnect(self):
        """断开与设备的连接"""
        if self.lockdown_client:
            self.lockdown_client.close()
        self.is_connected = False
        logger.info(f"已断开与设备 {self.udid} 的连接")
    
    def check_imessage_enabled(self) -> bool:
        """检查设备是否已启用iMessage"""
        try:
            # 检查Messages应用是否安装
            installation_proxy = InstallationProxyService(lockdown=self.lockdown_client)
            apps = installation_proxy.get_applications()
            if "com.apple.MobileSMS" not in apps:
                logger.error("设备未安装Messages应用")
                return False
            
            # 检查iMessage状态(通过系统设置)
            result = subprocess.run(
                ["ideviceinfo", "-u", self.udid, "-q", "com.apple.mobileSMS"],
                capture_output=True,
                text=True,
                timeout=10
            )
            
            if result.returncode == 0:
                output = result.stdout
                if "iMessageEnabled" in output:
                    match = re.search(r"iMessageEnabled\s*:\s*(\w+)", output)
                    if match:
                        enabled = match.group(1).lower() == "true"
                        if enabled:
                            logger.info("设备已启用iMessage")
                        else:
                            logger.error("设备未启用iMessage")
                        return enabled
            
            logger.warning("无法确定iMessage状态,假设已启用")
            return True
        except Exception as e:
            logger.error(f"检查iMessage状态失败: {str(e)}")
            return False
    
    def send_message(self, phone_number: str, message: str) -> bool:
        """发送iMessage消息到指定手机号"""
        if not self.is_connected:
            if not self.connect():
                return False
        
        if not self.check_imessage_enabled():
            return False
        
        try:
            # 使用AppleScript方式发送消息(macOS专用)
            # 注意:这种方式需要在macOS上运行,且需要允许终端控制Messages应用
            script = f'''
            tell application "Messages"
                set targetService to 1st service whose service type = iMessage
                set targetBuddy to buddy "{phone_number}" of targetService
                send "{message}" to targetBuddy
            end tell
            '''
            
            result = subprocess.run(
                ["osascript", "-e", script],
                capture_output=True,
                text=True,
                timeout=30
            )
            
            if result.returncode == 0:
                logger.info(f"成功发送消息到 {phone_number}")
                return True
            else:
                logger.error(f"发送消息到 {phone_number} 失败: {result.stderr}")
                return False
                
        except Exception as e:
            logger.error(f"发送消息异常: {str(e)}")
            self.is_connected = False
            return False
    
    def send_bulk_messages(self, phone_numbers: List[str], message: str, 
                          delay: float = 2.0) -> tuple[int, int]:
        """批量发送消息到多个手机号"""
        success_count = 0
        fail_count = 0
        
        logger.info(f"开始批量发送消息,目标数量: {len(phone_numbers)},延迟: {delay}秒")
        
        for i, phone_number in enumerate(phone_numbers):
            logger.info(f"正在发送第 {i+1}/{len(phone_numbers)} 条消息到 {phone_number}")
            
            if self.send_message(phone_number, message):
                success_count += 1
            else:
                fail_count += 1
            
            # 最后一条消息不需要延迟
            if i < len(phone_numbers) - 1:
                time.sleep(delay)
        
        logger.info(f"批量发送完成,成功: {success_count},失败: {fail_count}")
        return (success_count, fail_count)
    
    def open_messages_app(self) -> bool:
        """打开Messages应用"""
        try:
            springboard = SpringboardService(lockdown=self.lockdown_client)
            springboard.open_application("com.apple.MobileSMS")
            logger.info("已打开Messages应用")
            time.sleep(2)  # 等待应用启动
            return True
        except Exception as e:
            logger.error(f"打开Messages应用失败: {str(e)}")
            return False
    
    def close_messages_app(self) -> bool:
        """关闭Messages应用"""
        try:
            springboard = SpringboardService(lockdown=self.lockdown_client)
            springboard.kill_application("com.apple.MobileSMS")
            logger.info("已关闭Messages应用")
            return True
        except Exception as e:
            logger.error(f"关闭Messages应用失败: {str(e)}")
            return False
    
    def get_device_phone_number(self) -> Optional[str]:
        """获取设备的手机号"""
        try:
            result = subprocess.run(
                ["ideviceinfo", "-u", self.udid, "-k", "PhoneNumber"],
                capture_output=True,
                text=True,
                timeout=10
            )
            
            if result.returncode == 0:
                phone_number = result.stdout.strip()
                if phone_number:
                    logger.info(f"设备手机号: {phone_number}")
                    return phone_number
            
            logger.warning("无法获取设备手机号")
            return None
        except Exception as e:
            logger.error(f"获取设备手机号失败: {str(e)}")
            return None

三、设备状态监控与自动重连机制

在实际使用过程中,设备断开连接是一个非常常见的问题。可能的原因包括USB线缆松动、设备重启、系统更新等。如果没有有效的监控和自动重连机制,系统很容易在无人值守的情况下停止工作,导致消息发送失败。

为了解决这个问题,我设计了一套完善的设备状态监控与自动重连机制。系统会定期检查所有设备的连接状态,当发现设备断开时,会自动尝试重新连接。如果多次重连失败,会记录错误日志并发送通知提醒管理员。同时,系统还会监控设备的电池电量和存储空间,避免因设备电量耗尽或存储空间不足导致的问题。

下面是设备状态监控与自动重连的实现代码:

复制代码
# 设备监控与自动重连模块 device_monitor.py
import threading
import time
import subprocess
from typing import Dict, Optional
from loguru import logger
from device_manager import DeviceManager, Device
from imessage_sender import iMessageSender
import datetime

class DeviceHealthMonitor:
    def __init__(self, device_manager: DeviceManager, check_interval: int = 30):
        self.device_manager = device_manager
        self.check_interval = check_interval
        self.monitor_thread: Optional[threading.Thread] = None
        self.is_monitoring = False
        self.retry_counts: Dict[str, int] = {}
        self.max_retries = 5
        self.sender_instances: Dict[str, iMessageSender] = {}
    
    def get_device_battery_level(self, udid: str) -> Optional[int]:
        """获取设备电池电量百分比"""
        try:
            result = subprocess.run(
                ["ideviceinfo", "-u", udid, "-q", "com.apple.mobile.battery"],
                capture_output=True,
                text=True,
                timeout=10
            )
            
            if result.returncode == 0:
                output = result.stdout
                match = re.search(r"BatteryCurrentCapacity\s*:\s*(\d+)", output)
                if match:
                    level = int(match.group(1))
                    logger.debug(f"设备 {udid} 电池电量: {level}%")
                    return level
            
            logger.warning(f"无法获取设备 {udid} 电池电量")
            return None
        except Exception as e:
            logger.error(f"获取设备 {udid} 电池电量失败: {str(e)}")
            return None
    
    def get_device_storage_info(self, udid: str) -> Optional[Dict]:
        """获取设备存储空间信息"""
        try:
            result = subprocess.run(
                ["ideviceinfo", "-u", udid, "-q", "com.apple.disk_usage"],
                capture_output=True,
                text=True,
                timeout=10
            )
            
            if result.returncode == 0:
                output = result.stdout
                info = {}
                
                total_match = re.search(r"TotalDiskCapacity\s*:\s*(\d+)", output)
                free_match = re.search(r"FreeDiskCapacity\s*:\s*(\d+)", output)
                
                if total_match and free_match:
                    total = int(total_match.group(1))
                    free = int(free_match.group(1))
                    used = total - free
                    
                    info["total_gb"] = round(total / (1024 ** 3), 2)
                    info["free_gb"] = round(free / (1024 ** 3), 2)
                    info["used_gb"] = round(used / (1024 ** 3), 2)
                    info["used_percent"] = round((used / total) * 100, 2)
                    
                    logger.debug(f"设备 {udid} 存储空间: 总容量 {info['total_gb']}GB, "
                                f"已用 {info['used_gb']}GB ({info['used_percent']}%), "
                                f"可用 {info['free_gb']}GB")
                    return info
            
            logger.warning(f"无法获取设备 {udid} 存储空间信息")
            return None
        except Exception as e:
            logger.error(f"获取设备 {udid} 存储空间信息失败: {str(e)}")
            return None
    
    def reconnect_device(self, udid: str) -> bool:
        """尝试重新连接设备"""
        if udid not in self.retry_counts:
            self.retry_counts[udid] = 0
        
        if self.retry_counts[udid] >= self.max_retries:
            logger.error(f"设备 {udid} 重连次数已达上限 ({self.max_retries}),停止尝试")
            return False
        
        self.retry_counts[udid] += 1
        logger.info(f"正在尝试重新连接设备 {udid} (第 {self.retry_counts[udid]} 次尝试)")
        
        # 尝试重启usbmuxd服务
        try:
            subprocess.run(["sudo", "systemctl", "restart", "usbmuxd"], 
                          capture_output=True, timeout=10)
            time.sleep(2)
        except Exception as e:
            logger.warning(f"重启usbmuxd服务失败: {str(e)}")
        
        # 重新创建设备连接
        if udid in self.sender_instances:
            self.sender_instances[udid].disconnect()
            del self.sender_instances[udid]
        
        try:
            sender = iMessageSender(udid)
            if sender.is_connected and sender.check_imessage_enabled():
                self.sender_instances[udid] = sender
                self.retry_counts[udid] = 0
                logger.info(f"设备 {udid} 重新连接成功")
                return True
            else:
                sender.disconnect()
                logger.error(f"设备 {udid} 重新连接失败")
                return False
        except Exception as e:
            logger.error(f"设备 {udid} 重新连接异常: {str(e)}")
            return False
    
    def check_device_health(self, device: Device):
        """检查单台设备的健康状态"""
        if not device.is_connected:
            logger.warning(f"设备 {device.name} ({device.udid}) 已断开连接")
            self.reconnect_device(device.udid)
            return
        
        # 检查电池电量
        battery_level = self.get_device_battery_level(device.udid)
        if battery_level is not None and battery_level < 20:
            logger.warning(f"设备 {device.name} 电池电量过低: {battery_level}%")
        
        # 检查存储空间
        storage_info = self.get_device_storage_info(device.udid)
        if storage_info is not None and storage_info["used_percent"] > 90:
            logger.warning(f"设备 {device.name} 存储空间不足: 已使用 {storage_info['used_percent']}%")
        
        # 检查iMessage发送功能是否正常
        if device.udid not in self.sender_instances:
            try:
                sender = iMessageSender(device.udid)
                if sender.is_connected and sender.check_imessage_enabled():
                    self.sender_instances[device.udid] = sender
                else:
                    sender.disconnect()
                    logger.error(f"设备 {device.name} iMessage功能异常")
            except Exception as e:
                logger.error(f"创建设备 {device.name} 发送实例失败: {str(e)}")
    
    def check_all_devices(self):
        """检查所有设备的健康状态"""
        logger.debug("开始检查所有设备健康状态")
        
        for device in self.device_manager.devices.values():
            if device.is_enabled:
                try:
                    self.check_device_health(device)
                except Exception as e:
                    logger.error(f"检查设备 {device.name} 健康状态异常: {str(e)}")
        
        logger.debug("设备健康状态检查完成")
    
    def start_monitoring(self):
        """启动设备健康监控线程"""
        if self.is_monitoring:
            logger.warning("设备健康监控已在运行中")
            return
        
        self.is_monitoring = True
        self.monitor_thread = threading.Thread(
            target=self._monitor_loop,
            daemon=True
        )
        self.monitor_thread.start()
        logger.info(f"设备健康监控已启动,检查间隔: {self.check_interval}秒")
    
    def stop_monitoring(self):
        """停止设备健康监控"""
        self.is_monitoring = False
        if self.monitor_thread and self.monitor_thread.is_alive():
            self.monitor_thread.join()
        
        # 断开所有发送实例连接
        for sender in self.sender_instances.values():
            sender.disconnect()
        self.sender_instances.clear()
        
        logger.info("设备健康监控已停止")
    
    def _monitor_loop(self):
        """设备健康监控循环"""
        while self.is_monitoring:
            try:
                self.check_all_devices()
            except Exception as e:
                logger.error(f"设备健康监控循环异常: {str(e)}")
            
            # 等待下一次检查
            for _ in range(self.check_interval):
                if not self.is_monitoring:
                    break
                time.sleep(1)
    
    def get_sender(self, udid: str) -> Optional[iMessageSender]:
        """获取指定设备的消息发送实例"""
        return self.sender_instances.get(udid)

四、消息队列与批量发送调度核心逻辑

消息队列和批量发送调度是iMessage群发系统的核心功能。在企业级应用中,我们经常需要同时发送大量消息,如果没有合理的调度机制,很容易导致系统过载、消息发送失败,甚至触发苹果的风控机制。

我设计的消息队列采用了生产者 - 消费者模式。生产者负责将待发送的消息添加到队列中,消费者则从队列中取出消息,分配给可用的设备进行发送。为了提高发送效率,系统采用了多线程并发发送的方式,每台设备对应一个发送线程。同时,系统会根据设备的负载情况和历史发送记录,动态调整消息分配策略,确保所有设备的负载均衡。

下面是消息队列与批量发送调度的实现代码:

复制代码
# 消息队列与调度模块 message_scheduler.py
import queue
import threading
import time
from typing import List, Dict, Optional, Callable
from loguru import logger
from device_manager import DeviceManager, Device
from device_monitor import DeviceHealthMonitor
import datetime
import uuid

class Message:
    def __init__(self, phone_number: str, content: str, priority: int = 5):
        self.id = str(uuid.uuid4())
        self.phone_number = phone_number
        self.content = content
        self.priority = priority  # 1-10,数字越大优先级越高
        self.created_at = datetime.datetime.now()
        self.status = "pending"  # pending, sending, success, failed
        self.sent_at: Optional[datetime.datetime] = None
        self.device_udid: Optional[str] = None
        self.error_message: Optional[str] = None
    
    def __lt__(self, other):
        # 优先级队列比较方法,优先级高的先处理
        return self.priority > other.priority

class MessageScheduler:
    def __init__(self, device_manager: DeviceManager, 
                 health_monitor: DeviceHealthMonitor,
                 max_workers: int = 10):
        self.device_manager = device_manager
        self.health_monitor = health_monitor
        self.max_workers = max_workers
        
        # 优先级消息队列
        self.message_queue = queue.PriorityQueue()
        
        # 工作线程管理
        self.worker_threads: List[threading.Thread] = []
        self.is_running = False
        
        # 统计信息
        self.total_messages = 0
        self.success_messages = 0
        self.failed_messages = 0
        
        # 回调函数
        self.on_message_sent: Optional[Callable[[Message], None]] = None
        self.on_message_failed: Optional[Callable[[Message], None]] = None
        
        # 设备负载跟踪
        self.device_load: Dict[str, int] = {}
    
    def add_message(self, phone_number: str, content: str, priority: int = 5) -> str:
        """添加单条消息到队列"""
        message = Message(phone_number, content, priority)
        self.message_queue.put((message.priority, message))
        self.total_messages += 1
        logger.debug(f"已添加消息到队列: {message.id} -> {phone_number}")
        return message.id
    
    def add_bulk_messages(self, phone_numbers: List[str], content: str, 
                         priority: int = 5) -> List[str]:
        """批量添加消息到队列"""
        message_ids = []
        for phone_number in phone_numbers:
            message_id = self.add_message(phone_number, content, priority)
            message_ids.append(message_id)
        
        logger.info(f"已批量添加 {len(phone_numbers)} 条消息到队列")
        return message_ids
    
    def get_available_device(self) -> Optional[Device]:
        """获取负载最低的可用设备"""
        available_devices = self.device_manager.get_enabled_devices()
        if not available_devices:
            return None
        
        # 初始化设备负载
        for device in available_devices:
            if device.udid not in self.device_load:
                self.device_load[device.udid] = 0
        
        # 选择负载最低的设备
        available_devices.sort(key=lambda d: self.device_load[d.udid])
        selected_device = available_devices[0]
        
        logger.debug(f"选择设备 {selected_device.name} 发送消息,当前负载: {self.device_load[selected_device.udid]}")
        return selected_device
    
    def process_message(self, message: Message) -> bool:
        """处理单条消息"""
        message.status = "sending"
        
        # 获取可用设备
        device = self.get_available_device()
        if not device:
            message.status = "failed"
            message.error_message = "没有可用的发送设备"
            logger.error(f"消息 {message.id} 发送失败: {message.error_message}")
            return False
        
        message.device_udid = device.udid
        self.device_load[device.udid] += 1
        
        try:
            # 获取消息发送实例
            sender = self.health_monitor.get_sender(device.udid)
            if not sender:
                message.status = "failed"
                message.error_message = "设备发送实例不可用"
                logger.error(f"消息 {message.id} 发送失败: {message.error_message}")
                return False
            
            # 发送消息
            success = sender.send_message(message.phone_number, message.content)
            
            if success:
                message.status = "success"
                message.sent_at = datetime.datetime.now()
                self.success_messages += 1
                self.device_manager.increment_message_count(device.udid)
                logger.info(f"消息 {message.id} 发送成功,使用设备: {device.name}")
                
                if self.on_message_sent:
                    self.on_message_sent(message)
                return True
            else:
                message.status = "failed"
                message.error_message = "消息发送失败"
                self.failed_messages += 1
                logger.error(f"消息 {message.id} 发送失败: {message.error_message}")
                
                if self.on_message_failed:
                    self.on_message_failed(message)
                return False
                
        except Exception as e:
            message.status = "failed"
            message.error_message = f"发送异常: {str(e)}"
            self.failed_messages += 1
            logger.error(f"消息 {message.id} 发送异常: {str(e)}")
            
            if self.on_message_failed:
                self.on_message_failed(message)
            return False
        finally:
            self.device_load[device.udid] -= 1
    
    def worker_loop(self):
        """工作线程循环"""
        while self.is_running:
            try:
                # 从队列中获取消息,设置超时避免无限阻塞
                priority, message = self.message_queue.get(timeout=1)
                
                # 处理消息
                self.process_message(message)
                
                # 标记任务完成
                self.message_queue.task_done()
                
            except queue.Empty:
                # 队列为空,继续循环
                continue
            except Exception as e:
                logger.error(f"工作线程异常: {str(e)}")
                time.sleep(1)
    
    def start(self):
        """启动消息调度器"""
        if self.is_running:
            logger.warning("消息调度器已在运行中")
            return
        
        self.is_running = True
        
        # 启动工作线程
        for i in range(self.max_workers):
            thread = threading.Thread(
                target=self.worker_loop,
                name=f"Worker-{i+1}",
                daemon=True
            )
            thread.start()
            self.worker_threads.append(thread)
        
        logger.info(f"消息调度器已启动,工作线程数: {self.max_workers}")
    
    def stop(self):
        """停止消息调度器"""
        if not self.is_running:
            logger.warning("消息调度器未在运行")
            return
        
        self.is_running = False
        
        # 等待所有工作线程结束
        for thread in self.worker_threads:
            thread.join()
        self.worker_threads.clear()
        
        logger.info("消息调度器已停止")
        logger.info(f"运行统计: 总消息 {self.total_messages}, "
                    f"成功 {self.success_messages}, 失败 {self.failed_messages}")
    
    def wait_for_completion(self):
        """等待所有消息处理完成"""
        logger.info("等待所有消息处理完成...")
        self.message_queue.join()
        logger.info("所有消息已处理完成")
    
    def get_queue_size(self) -> int:
        """获取当前队列中的消息数量"""
        return self.message_queue.qsize()
    
    def get_statistics(self) -> Dict:
        """获取调度器统计信息"""
        return {
            "total_messages": self.total_messages,
            "success_messages": self.success_messages,
            "failed_messages": self.failed_messages,
            "pending_messages": self.get_queue_size(),
            "is_running": self.is_running,
            "worker_count": len(self.worker_threads)
        }
    
    def clear_queue(self):
        """清空消息队列"""
        while not self.message_queue.empty():
            try:
                self.message_queue.get_nowait()
                self.message_queue.task_done()
            except queue.Empty:
                break
        
        logger.info("消息队列已清空")

五、消息内容模板与变量替换系统

在企业实际使用中,我们经常需要发送个性化的消息,比如包含收件人姓名、部门、订单号等信息。如果每条消息都手动编辑,效率会非常低下。为了解决这个问题,我设计了一套简单易用的消息内容模板与变量替换系统。

系统支持创建和管理多个消息模板,模板中可以包含变量,变量使用 {{变量名}} 的格式表示。在发送消息时,系统会自动将模板中的变量替换为实际的值。同时,系统还支持从Excel文件中导入收件人列表和变量数据,大大提高了批量发送个性化消息的效率。

下面是消息内容模板与变量替换系统的实现代码:

复制代码
# 消息模板与变量替换模块 template_manager.py
import re
import pandas as pd
from typing import Dict, List, Optional
from loguru import logger
from sqlalchemy import create_engine, Column, String, Integer, Text, DateTime
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
import datetime
import json

Base = declarative_base()

class MessageTemplate(Base):
    __tablename__ = "message_templates"
    
    id = Column(Integer, primary_key=True, autoincrement=True)
    name = Column(String(100), unique=True, nullable=False)
    content = Column(Text, nullable=False)
    variables = Column(Text, nullable=False)  # JSON格式存储变量列表
    created_at = Column(DateTime, default=datetime.datetime.now)
    updated_at = Column(DateTime, default=datetime.datetime.now, onupdate=datetime.datetime.now)
    usage_count = Column(Integer, default=0)

class TemplateManager:
    def __init__(self, db_path: str = "imessage_system.db"):
        self.engine = create_engine(f"sqlite:///{db_path}")
        Base.metadata.create_all(self.engine)
        Session = sessionmaker(bind=self.engine)
        self.session = Session()
        self.variable_pattern = re.compile(r"\{\{(\w+)\}\}")
    
    def extract_variables(self, content: str) -> List[str]:
        """从模板内容中提取变量"""
        variables = self.variable_pattern.findall(content)
        # 去重并保持顺序
        unique_variables = []
        seen = set()
        for var in variables:
            if var not in seen:
                seen.add(var)
                unique_variables.append(var)
        return unique_variables
    
    def create_template(self, name: str, content: str) -> Optional[MessageTemplate]:
        """创建新的消息模板"""
        # 检查模板名称是否已存在
        existing = self.session.query(MessageTemplate).filter_by(name=name).first()
        if existing:
            logger.error(f"模板名称 '{name}' 已存在")
            return None
        
        # 提取变量
        variables = self.extract_variables(content)
        
        # 创建模板
        template = MessageTemplate(
            name=name,
            content=content,
            variables=json.dumps(variables)
        )
        
        self.session.add(template)
        self.session.commit()
        logger.info(f"成功创建模板 '{name}',包含变量: {variables}")
        return template
    
    def update_template(self, template_id: int, name: Optional[str] = None, 
                       content: Optional[str] = None) -> bool:
        """更新现有模板"""
        template = self.session.query(MessageTemplate).get(template_id)
        if not template:
            logger.error(f"模板ID {template_id} 不存在")
            return False
        
        if name is not None and name != template.name:
            # 检查新名称是否已存在
            existing = self.session.query(MessageTemplate).filter_by(name=name).first()
            if existing:
                logger.error(f"模板名称 '{name}' 已存在")
                return False
            template.name = name
        
        if content is not None:
            template.content = content
            template.variables = json.dumps(self.extract_variables(content))
        
        template.updated_at = datetime.datetime.now()
        self.session.commit()
        logger.info(f"成功更新模板 '{template.name}'")
        return True
    
    def delete_template(self, template_id: int) -> bool:
        """删除模板"""
        template = self.session.query(MessageTemplate).get(template_id)
        if not template:
            logger.error(f"模板ID {template_id} 不存在")
            return False
        
        self.session.delete(template)
        self.session.commit()
        logger.info(f"成功删除模板 '{template.name}'")
        return True
    
    def get_template(self, template_id: int) -> Optional[MessageTemplate]:
        """获取指定ID的模板"""
        return self.session.query(MessageTemplate).get(template_id)
    
    def get_template_by_name(self, name: str) -> Optional[MessageTemplate]:
        """根据名称获取模板"""
        return self.session.query(MessageTemplate).filter_by(name=name).first()
    
    def get_all_templates(self) -> List[MessageTemplate]:
        """获取所有模板"""
        return self.session.query(MessageTemplate).order_by(MessageTemplate.updated_at.desc()).all()
    
    def render_template(self, template: MessageTemplate, variables: Dict[str, str]) -> str:
        """使用变量渲染模板内容"""
        content = template.content
        template_vars = json.loads(template.variables)
        
        # 检查是否有缺失的变量
        missing_vars = [var for var in template_vars if var not in variables]
        if missing_vars:
            logger.warning(f"渲染模板 '{template.name}' 时缺少变量: {missing_vars}")
        
        # 替换变量
        for var_name, var_value in variables.items():
            content = content.replace(f"{{{{{var_name}}}}}", str(var_value))
        
        # 增加使用计数
        template.usage_count += 1
        self.session.commit()
        
        return content
    
    def render_template_by_id(self, template_id: int, variables: Dict[str, str]) -> Optional[str]:
        """根据模板ID渲染内容"""
        template = self.get_template(template_id)
        if not template:
            logger.error(f"模板ID {template_id} 不存在")
            return None
        return self.render_template(template, variables)
    
    def import_from_excel(self, file_path: str, phone_column: str = "phone") -> tuple[List[str], List[Dict[str, str]]]:
        """从Excel文件导入收件人列表和变量数据"""
        try:
            df = pd.read_excel(file_path)
            logger.info(f"从Excel文件读取了 {len(df)} 条数据")
            
            # 检查手机号列是否存在
            if phone_column not in df.columns:
                logger.error(f"Excel文件中缺少 '{phone_column}' 列")
                return [], []
            
            phone_numbers = []
            variables_list = []
            
            for _, row in df.iterrows():
                phone_number = str(row[phone_column]).strip()
                if not phone_number:
                    continue
                
                phone_numbers.append(phone_number)
                
                # 收集所有其他列作为变量
                variables = {}
                for col in df.columns:
                    if col != phone_column:
                        variables[col] = str(row[col]).strip() if pd.notna(row[col]) else ""
                
                variables_list.append(variables)
            
            logger.info(f"成功导入 {len(phone_numbers)} 个收件人")
            return phone_numbers, variables_list
            
        except Exception as e:
            logger.error(f"导入Excel文件失败: {str(e)}")
            return [], []
    
    def export_to_excel(self, phone_numbers: List[str], variables_list: List[Dict[str, str]], 
                       file_path: str, phone_column: str = "phone"):
        """导出收件人列表和变量数据到Excel文件"""
        try:
            if len(phone_numbers) != len(variables_list):
                logger.error("手机号列表和变量列表长度不匹配")
                return False
            
            # 构建DataFrame
            data = []
            for phone, variables in zip(phone_numbers, variables_list):
                row = {phone_column: phone}
                row.update(variables)
                data.append(row)
            
            df = pd.DataFrame(data)
            df.to_excel(file_path, index=False)
            logger.info(f"成功导出 {len(data)} 条数据到 {file_path}")
            return True
            
        except Exception as e:
            logger.error(f"导出Excel文件失败: {str(e)}")
            return False

六、发送速率控制与防风控策略实现

苹果对iMessage的发送有严格的限制,如果发送速率过快或者发送内容被判定为垃圾信息,账号很容易被封禁。这是企业级iMessage群发系统必须面对的一个重要问题。为了避免触发苹果的风控机制,我设计了一套完善的发送速率控制与防风控策略。

首先,系统会限制每台设备的每日发送量和发送速率。根据我的实际测试,每台设备每天发送不超过200条消息,每条消息之间间隔至少2秒,是比较安全的范围。其次,系统会在消息内容中加入随机的微小变化,避免发送完全相同的内容。同时,系统还会模拟人类的发送行为,在发送过程中随机插入一些延迟,避免出现过于规律的发送模式。

下面是发送速率控制与防风控策略的实现代码:

复制代码
# 速率控制与防风控模块 rate_limiter.py
import time
import random
import threading
from typing import Dict, Optional
from loguru import logger
from device_manager import DeviceManager, Device
import datetime

class TokenBucket:
    """令牌桶算法实现速率限制"""
    def __init__(self, capacity: float, rate: float):
        self.capacity = capacity  # 桶的容量(最大令牌数)
        self.rate = rate  # 令牌生成速率(每秒)
        self.tokens = capacity  # 当前令牌数
        self.last_refill_time = time.time()
        self.lock = threading.Lock()
    
    def consume(self, tokens: float = 1.0) -> bool:
        """消耗指定数量的令牌,返回是否成功"""
        with self.lock:
            # 先补充令牌
            now = time.time()
            time_passed = now - self.last_refill_time
            new_tokens = time_passed * self.rate
            self.tokens = min(self.capacity, self.tokens + new_tokens)
            self.last_refill_time = now
            
            # 检查是否有足够的令牌
            if self.tokens >= tokens:
                self.tokens -= tokens
                return True
            else:
                return False
    
    def get_wait_time(self, tokens: float = 1.0) -> float:
        """获取需要等待的时间(秒)才能消耗指定数量的令牌"""
        with self.lock:
            if self.tokens >= tokens:
                return 0.0
            
            needed_tokens = tokens - self.tokens
            wait_time = needed_tokens / self.rate
            return wait_time

class RateLimiter:
    def __init__(self, device_manager: DeviceManager):
        self.device_manager = device_manager
        self.device_buckets: Dict[str, TokenBucket] = {}
        self.daily_limits: Dict[str, int] = {}
        self.default_daily_limit = 200  # 默认每日发送限制
        self.default_rate = 0.5  # 默认每秒发送0.5条(每2秒1条)
        self.default_burst = 3  # 默认突发允许3条
        self.random_delay_range = (0.5, 3.0)  # 随机延迟范围(秒)
        self._initialize_buckets()
    
    def _initialize_buckets(self):
        """为所有已存在的设备初始化令牌桶"""
        for device in self.device_manager.devices.values():
            self.add_device(device.udid)
    
    def add_device(self, udid: str, daily_limit: Optional[int] = None, 
                  rate: Optional[float] = None, burst: Optional[int] = None):
        """为新设备添加速率限制"""
        if udid in self.device_buckets:
            logger.warning(f"设备 {udid} 已存在速率限制")
            return
        
        daily_limit = daily_limit or self.default_daily_limit
        rate = rate or self.default_rate
        burst = burst or self.default_burst
        
        self.device_buckets[udid] = TokenBucket(capacity=burst, rate=rate)
        self.daily_limits[udid] = daily_limit
        logger.info(f"为设备 {udid} 设置速率限制: 每日{daily_limit}条, 速率{rate}条/秒, 突发{burst}条")
    
    def remove_device(self, udid: str):
        """移除设备的速率限制"""
        if udid in self.device_buckets:
            del self.device_buckets[udid]
            del self.daily_limits[udid]
            logger.info(f"已移除设备 {udid} 的速率限制")
    
    def update_device_limit(self, udid: str, daily_limit: Optional[int] = None, 
                           rate: Optional[float] = None, burst: Optional[int] = None):
        """更新设备的速率限制"""
        if udid not in self.device_buckets:
            self.add_device(udid, daily_limit, rate, burst)
            return
        
        if daily_limit is not None:
            self.daily_limits[udid] = daily_limit
        
        if rate is not None or burst is not None:
            current_bucket = self.device_buckets[udid]
            new_rate = rate or current_bucket.rate
            new_burst = burst or current_bucket.capacity
            self.device_buckets[udid] = TokenBucket(capacity=new_burst, rate=new_rate)
        
        logger.info(f"已更新设备 {udid} 的速率限制")
    
    def check_daily_limit(self, udid: str) -> bool:
        """检查设备是否达到每日发送限制"""
        if udid not in self.device_manager.devices:
            return False
        
        device = self.device_manager.devices[udid]
        daily_limit = self.daily_limits.get(udid, self.default_daily_limit)
        
        if device.daily_messages_sent >= daily_limit:
            logger.warning(f"设备 {device.name} 已达到每日发送限制 ({daily_limit}条)")
            return False
        
        return True
    
    def wait_for_send(self, udid: str) -> bool:
        """等待直到可以发送消息,返回是否成功"""
        # 先检查每日限制
        if not self.check_daily_limit(udid):
            return False
        
        # 检查令牌桶
        if udid not in self.device_buckets:
            self.add_device(udid)
        
        bucket = self.device_buckets[udid]
        
        # 等待获取令牌
        wait_time = bucket.get_wait_time()
        if wait_time > 0:
            logger.debug(f"设备 {udid} 需要等待 {wait_time:.2f} 秒才能发送下一条消息")
            time.sleep(wait_time)
        
        # 消耗令牌
        if not bucket.consume():
            logger.error(f"设备 {udid} 无法获取发送令牌")
            return False
        
        # 添加随机延迟,模拟人类行为
        random_delay = random.uniform(*self.random_delay_range)
        logger.debug(f"添加随机延迟: {random_delay:.2f} 秒")
        time.sleep(random_delay)
        
        return True
    
    def add_content_variation(self, content: str) -> str:
        """为消息内容添加随机变化,避免完全相同"""
        # 在消息末尾添加随机的空格或标点
        variations = ["", " ", "  ", ".", "。", "!", "!", "?", "?"]
        variation = random.choice(variations)
        
        # 随机替换一些同义词(简单示例)
        synonyms = {
            "你好": ["您好", "嗨", "哈喽"],
            "谢谢": ["感谢", "多谢", "谢谢啦"],
            "请": ["烦请", "请您", "麻烦您"],
            "通知": ["告知", "提醒", "消息"]
        }
        
        for original, alternatives in synonyms.items():
            if original in content and random.random() < 0.3:
                content = content.replace(original, random.choice(alternatives), 1)
        
        return content + variation
    
    def schedule_send_time(self, base_hour: int = 9, end_hour: int = 18) -> datetime.datetime:
        """调度消息发送时间,只在工作时间发送"""
        now = datetime.datetime.now()
        
        # 如果当前时间在工作时间内,立即发送
        if base_hour <= now.hour < end_hour:
            return now
        
        # 否则,安排到下一个工作日的工作时间
        next_day = now + datetime.timedelta(days=1)
        send_time = next_day.replace(hour=base_hour, minute=0, second=0, microsecond=0)
        
        # 如果是周末,推迟到周一
        if send_time.weekday() >= 5:  # 5=周六, 6=周日
            days_to_monday = 7 - send_time.weekday()
            send_time += datetime.timedelta(days=days_to_monday)
        
        logger.info(f"消息将在 {send_time.strftime('%Y-%m-%d %H:%M:%S')} 发送")
        return send_time
    
    def reset_daily_limits(self):
        """重置所有设备的每日限制计数器"""
        self.device_manager.reset_daily_counts()
        logger.info("已重置所有设备的每日发送计数")
    
    def start_daily_reset_scheduler(self):
        """启动每日重置调度器,每天凌晨0点重置计数"""
        def reset_loop():
            while True:
                now = datetime.datetime.now()
                # 计算到明天0点的时间
                tomorrow = now + datetime.timedelta(days=1)
                midnight = tomorrow.replace(hour=0, minute=0, second=0, microsecond=0)
                sleep_time = (midnight - now).total_seconds()
                
                logger.info(f"将在 {midnight.strftime('%Y-%m-%d %H:%M:%S')} 重置每日计数,"
                            f"等待 {sleep_time/3600:.2f} 小时")
                
                time.sleep(sleep_time)
                self.reset_daily_limits()
        
        thread = threading.Thread(target=reset_loop, daemon=True)
        thread.start()
        logger.info("每日重置调度器已启动")

七、系统日志记录与异常处理机制

完善的日志记录和异常处理机制是企业级系统不可或缺的部分。它不仅可以帮助我们快速定位和解决问题,还可以为系统的优化和改进提供数据支持。在我们的iMessage群发系统中,我使用了loguru库来实现日志记录功能,它提供了简单易用的API和丰富的功能,支持日志分级、格式化、滚动存储等。

系统会记录所有重要的操作和事件,包括设备连接 / 断开、消息发送成功/失败、系统错误等。日志会同时输出到控制台和文件,文件日志按天进行滚动存储,保留最近30天的日志。同时,系统还会对常见的异常情况进行捕获和处理,避免因单个错误导致整个系统崩溃。

下面是系统日志记录与异常处理机制的实现代码:

复制代码
# 日志与异常处理模块 logger_config.py
import os
import sys
import traceback
from loguru import logger
import datetime

def setup_logger(log_dir: str = "logs", retention_days: int = 30):
    """配置系统日志"""
    # 创建日志目录
    if not os.path.exists(log_dir):
        os.makedirs(log_dir)
    
    # 移除默认的日志处理器
    logger.remove()
    
    # 控制台日志处理器
    logger.add(
        sys.stdout,
        format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>",
        level="INFO",
        colorize=True
    )
    
    # 文件日志处理器(INFO级别)
    info_log_path = os.path.join(log_dir, "imessage_system_{time:YYYY-MM-DD}.log")
    logger.add(
        info_log_path,
        format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}",
        level="INFO",
        rotation="00:00",  # 每天午夜滚动
        retention=f"{retention_days} days",
        compression="zip",
        encoding="utf-8"
    )
    
    # 错误日志处理器(ERROR级别)
    error_log_path = os.path.join(log_dir, "imessage_system_error_{time:YYYY-MM-DD}.log")
    logger.add(
        error_log_path,
        format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}\n{exception}",
        level="ERROR",
        rotation="00:00",
        retention=f"{retention_days} days",
        compression="
相关推荐
Jing_jing_X1 小时前
我用 Claude Code 搭了一个远程 Claude web:手机发指令,本地电脑自己写代码
ai·agent·个人开发·ai应用开发
say_fall1 小时前
Linux进程核心概念:命令行参数与环境变量深度解析
linux·运维·服务器·ubuntu
Peace1 小时前
【Zabbix】
linux·运维·zabbix
枕星而眠1 小时前
C++面向对象核心:类间关系与继承深度解析
运维·开发语言·c++·后端
FBI HackerHarry浩1 小时前
在Python中TCP网络程序开发的步骤流程
运维·服务器·开发语言·网络·python·pycharm
qq_452396231 小时前
第十一篇:《Docker Compose:多容器应用编排入门》
运维·docker·容器
Geoking.2 小时前
Docker安装Nacos指南
运维·docker·容器
梦仔生信进阶2 小时前
【本地数据传服务器命令】小文件Xftp,大文件用它更高效!
运维·服务器
wanhengidc2 小时前
服务器 数据恢复
运维·服务器·网络·智能手机·云计算