dongguoliang 6 mesi fa
commit
b924299ade
61 ha cambiato i file con 3636 aggiunte e 0 eliminazioni
  1. 15 0
      .gitignore
  2. 34 0
      README.md
  3. 24 0
      build.py
  4. BIN
      dm/DmReg.dll
  5. BIN
      dm/dm.dll
  6. 68 0
      main.py
  7. 0 0
      model/__init__.py
  8. BIN
      model/__pycache__/__init__.cpython-312.pyc
  9. BIN
      model/__pycache__/custom_struct.cpython-312.pyc
  10. BIN
      model/__pycache__/globalmanager.cpython-312.pyc
  11. BIN
      model/__pycache__/helper.cpython-312.pyc
  12. 169 0
      model/custom_struct.py
  13. 193 0
      model/global_manager.py
  14. 417 0
      model/helper.py
  15. 0 0
      net/__init__.py
  16. BIN
      net/__pycache__/__init__.cpython-312.pyc
  17. BIN
      net/__pycache__/task_api.cpython-312.pyc
  18. 85 0
      net/http_base.py
  19. 52 0
      net/sms_api.py
  20. 161 0
      net/task_api.py
  21. 0 0
      scripts/__init__.py
  22. BIN
      scripts/__pycache__/__init__.cpython-312.pyc
  23. BIN
      scripts/__pycache__/script.cpython-312.pyc
  24. 119 0
      scripts/script.py
  25. 0 0
      server/__init__.py
  26. 17 0
      server/http_server.py
  27. 42 0
      server/log_server.py
  28. 0 0
      task/__init__.py
  29. BIN
      task/__pycache__/__init__.cpython-312.pyc
  30. BIN
      task/__pycache__/task.cpython-312.pyc
  31. 146 0
      task/task.py
  32. 0 0
      tools/__init__.py
  33. BIN
      tools/__pycache__/__init__.cpython-312.pyc
  34. BIN
      tools/__pycache__/dm_operate.cpython-312.pyc
  35. BIN
      tools/__pycache__/file_downloader.cpython-312.pyc
  36. BIN
      tools/__pycache__/ini_operate.cpython-312.pyc
  37. BIN
      tools/__pycache__/log.cpython-312.pyc
  38. BIN
      tools/__pycache__/thread_pool.cpython-312.pyc
  39. BIN
      tools/__pycache__/u2_operate.cpython-312.pyc
  40. BIN
      tools/__pycache__/utils.cpython-312.pyc
  41. BIN
      tools/__pycache__/wuyouip.cpython-312.pyc
  42. 228 0
      tools/dm_operate.py
  43. 0 0
      tools/emulator/__init__.py
  44. BIN
      tools/emulator/__pycache__/__init__.cpython-312.pyc
  45. BIN
      tools/emulator/__pycache__/emulator.cpython-312.pyc
  46. BIN
      tools/emulator/__pycache__/ld_operate.cpython-312.pyc
  47. BIN
      tools/emulator/__pycache__/ys_operate.cpython-312.pyc
  48. 236 0
      tools/emulator/emulator.py
  49. 298 0
      tools/emulator/ld_operate.py
  50. 256 0
      tools/emulator/ys_operate.py
  51. 99 0
      tools/file_downloader.py
  52. 54 0
      tools/ini_operate.py
  53. 107 0
      tools/log.py
  54. 27 0
      tools/thread_pool.py
  55. 233 0
      tools/upload_log.py
  56. 230 0
      tools/utils.py
  57. 185 0
      tools/wuyouip.py
  58. 0 0
      update/__init__.py
  59. BIN
      update/__pycache__/__init__.cpython-312.pyc
  60. BIN
      update/__pycache__/update.cpython-312.pyc
  61. 141 0
      update/update.py

+ 15 - 0
.gitignore

@@ -0,0 +1,15 @@
+
+/.idea
+/Log/*
+
+/config/*
+
+/venv
+
+/游戏母镜像/*
+
+/脚本代码/*
+
+/share/*
+/build
+/dist

+ 34 - 0
README.md

@@ -0,0 +1,34 @@
+# opt
+
+#### 介绍
+中控
+
+#### 软件架构
+软件架构说明
+
+
+#### 安装包
+
+1.  pip install dacite
+2.  pip install psutil
+3.  pip install requests
+4. pip install colorama
+5. pip install pywin32
+6. pip install comtypes
+7. pip install PyInstaller
+8. pip install winshell
+9. pip install pillow
+
+#### 使用说明
+
+1.  xxxx
+2.  xxxx
+3.  xxxx
+
+#### 参与贡献
+
+1.  Fork 本仓库
+2.  新建 Feat_xxx 分支
+3.  提交代码
+4.  新建 Pull Request
+

+ 24 - 0
build.py

@@ -0,0 +1,24 @@
+from PyInstaller.__main__ import run
+import os
+
+# Replace 'your_script.py' with the entry point script of your PyQt application
+script_name = 'main.py'
+Version = '2.1.3'
+output_name = f'OPT-{Version}'
+
+# 获取当前脚本的目录
+current_dir = os.path.dirname(os.path.abspath(__file__))
+
+options = [
+    '--onefile',  # Create a single executable file
+    '--hidden-import=winshell',
+    f'--add-data={os.path.join(current_dir, "D:/project/py/opt-gitee", "Lib", "site-packages", "winshell.py")};winshell',
+    f'--add-data={os.path.join(current_dir, "dm")};dm',
+    f'--name={output_name}',  # Specify the output file name
+]
+
+datas = [
+    ('path/to/plugin_file', 'destination_folder')
+]
+
+run([script_name] + options)

BIN
dm/DmReg.dll


BIN
dm/dm.dll


+ 68 - 0
main.py

@@ -0,0 +1,68 @@
+import os
+import sys
+import time
+
+from model.custom_struct import GameConfig
+from model.global_manager import GM
+from model.helper import MyHelper
+from net.task_api import task_api
+from task.task import Task
+from tools.log import logger
+from tools.thread_pool import MyThreadPoolExecutor
+from tools.utils import Utils
+
+
+def main():
+    try:
+        # 初始化全局管理
+        GM.init()
+        # 创建线程池
+        executor = MyThreadPoolExecutor(max_workers=GM.device_info.window_nums)
+        timer = Utils.Timer()
+        while GM.get_global_control():
+            if timer.timer('heartbeat', 60):
+                result,message = task_api.heartbeat(GM.device_info.officer)
+                logger.info(f'心跳:{message}')
+            idle_worker_count = executor.get_idle_worker_count()
+            # 只有当有空闲工作线程时才继续
+            if idle_worker_count > 0:
+                window_list = task_api.get_window_list()
+                for window in window_list:
+                    window_id = window.window_id
+                    # 只处理状态为 0 的窗口
+                    if GM.get_window_status(window_id) == 0:
+                        # 检查更新
+                        GM.updater.check_updates(GM.device_info, window.game_list)
+                        # 检查是否有任务需要执行
+                        for game in window.game_list:
+                            game_info = GameConfig.dict_to_GameConfig(game)
+                            if GM.updater.get_is_updating(game_info.task_id):  # 检查是否正在更新
+                                continue
+                            if not game_info.is_execute:
+                                continue
+                            # 取账号信息
+                            result, account_info = task_api.get_account(game_info.task_id,GM.device_info.only_new,GM.device_info.only_retained)
+                            logger.info(f'[窗口-{window_id}]{game_info.task_id}-获取账号:{account_info}',window_id)
+                            if result:
+                                task_api.up_log_profession(account_info.account, game_info.task_id, '拉取账号', '成功')
+                                helper = MyHelper(window_id, game_info, account_info)
+                                # 设置窗口状态为正在执行
+                                GM.set_window_status(window_id, 1)
+                                # 提交任务给线程池异步执行
+                                executor.submit(Task(helper).run)
+                                break  # 处理完一个窗口任务后,跳出当前循环,避免重复提交任务
+                            time.sleep(3)
+            time.sleep(10)  # 等待一定时间后重新检查
+    except Exception as e:
+        logger.exception(f'主线程异常:{e}')
+
+
+
+
+if __name__ == '__main__':
+
+    if Utils.is_admin():
+        main()
+        sys.exit()
+    else:
+        Utils.run_as_admin()

+ 0 - 0
model/__init__.py


BIN
model/__pycache__/__init__.cpython-312.pyc


BIN
model/__pycache__/custom_struct.cpython-312.pyc


BIN
model/__pycache__/globalmanager.cpython-312.pyc


BIN
model/__pycache__/helper.cpython-312.pyc


+ 169 - 0
model/custom_struct.py

@@ -0,0 +1,169 @@
+from dataclasses import dataclass
+from typing import LiteralString
+
+
+@dataclass
+class EmulatorInfo:
+    index: int
+    name: str
+    top_handle: int
+    bind_handle: int
+    is_enter_android: int | None
+    pid: int
+    vbox_pid: int
+    width: int | None
+    height: int | None
+    dpi: int | None
+
+
+@dataclass
+class PcConfig:
+    pc_name: str
+    officer: str | None
+    use_wuyouip: int | None
+    only_new: int | None
+    only_retained: int | None
+    check_md5_on_start: int | None
+    start_dkw_on_start: int | None
+    timed_start: int | None
+    check_script_update: int | None
+    check_image_update: int | None
+    window_nums: int | None
+
+@dataclass
+class GameConfig:
+    game_name: str
+    cpu: str
+    task_id: str  # "任务id"
+    memory: str  # "内存"
+    resolution: str  # "分辨率"
+    is_execute: bool  # "是否执行"
+    emulator_type: str  # "模拟器类型"
+    channel_id: str  # "渠道id"
+    game_id: str  # "游戏id"
+    game_type: str  # "游戏类型"
+    scale: str  # "缩放"
+    script: str  # "脚本"
+    timeout: int  # "超时时间"
+    image: str  # "镜像"
+
+    @staticmethod
+    def dict_to_GameConfig(game: dict):
+        info = GameConfig(
+            cpu=game['cpu'],
+            game_name=game['name'],
+            task_id=game['任务id'],
+            memory=game['内存'],
+            resolution=str(game['分辨率']).replace('x', ',').replace('(', ',').replace(')', '').replace('dpi', ''),
+            emulator_type=game['模拟器类型'],
+            channel_id=game['渠道id'],
+            game_id=game['游戏id'],
+            game_type=game['游戏类型'],
+            scale=game['缩放'],
+            script=game['脚本'],
+            timeout=int(game['超时时间']),
+            image=game['镜像'],
+            is_execute=game['是否执行'])
+        return info
+
+
+@dataclass
+class WindowInfo:
+    window_id: int
+    game_list: list[GameConfig]
+
+
+@dataclass
+class AccountInfo:
+    account: str
+    password: str
+    city: str
+    retained: int
+    retained_str: str
+    manufacturer: str
+    model: str
+    pnumber: str
+    imei: str
+    imsi: str
+    simserial: str
+    androidid: str
+    mac: str
+    mac_colon: str
+    iccid: str
+    resolution: str
+    cpuNum: str
+    memorySize: str
+    simulator_name: str
+    wechat_order_id: str
+    game_type: str
+    number_operatorTypes:str
+
+
+@dataclass
+class UpdateInfo:
+    game_id: str
+    md5: str
+    download_url: str
+    file_type: str
+    save_path: LiteralString | str | bytes
+
+
+@dataclass
+class UploadLog:
+    simulator_ip: str  # 模拟器ip
+    simulator_mac: str  # 模拟器mac
+    pc_code: str  # 电脑编号
+    pc_ip: str  # 电脑IP
+    pc_mac: str  # 电脑mac
+    device_id: str  # 手机_aid
+    account: str  # 游戏账号
+    account_type: int  # 游戏_账号类型
+    pwd: str  # 游戏账号密码
+    game_id: int  # 游戏编号
+    coding: int  # 错误码
+    log_uuid: str
+    operator: str  # 负责人
+    remarks: str  # 备注
+    task_type: int  # 任务类型新增0活跃1
+    script_type: int  # 脚本类型
+    simulator_code: str  # 模拟器编号
+    device_manufacturer: str  # 手机_厂商
+    device_model: str  # 手机_型号
+    device_imei: str  # 手机_imei
+    device_sdk: str  # 手机_sdk
+    device_mac: str  # 手机_mac
+    device_number: str  # 手机_号码
+    script_device_id: str  # 脚本中上传
+    err: str  # 0或不传表示没有异常,其他表示异常,脚本端自定义
+    simulator_ip_city: str  # 手机_IP_城市
+
+    # 定义一个方法,将对象转换为字典
+    def to_dict(self):
+        return {
+            'simulator_ip': self.simulator_ip,
+            'simulator_mac': self.simulator_mac,
+            'pc_code': self.pc_code,
+            'pc_ip': self.pc_ip,
+            'pc_mac': self.pc_mac,
+            'device_id': self.device_id,
+            'account': self.account,
+            'account_type': self.account_type,
+            'pwd': self.pwd,
+            'game_id': self.game_id,
+            'coding': self.coding,
+            'log_uuid': self.log_uuid,
+            'operator': self.operator,
+            'remarks': self.remarks,
+            'task_type': self.task_type,
+            'script_type': self.script_type,
+            'simulator_code': self.simulator_code,
+            'device_manufacturer': self.device_manufacturer,
+            'device_model': self.device_model,
+            'device_imei': self.device_imei,
+            'device_sdk': self.device_sdk,
+            'device_mac': self.device_mac,
+            'device_number': self.device_number,
+            'script_device_id': self.script_device_id,
+            'err': self.err,
+            'simulator_ip_city': self.simulator_ip_city
+        }

+ 193 - 0
model/global_manager.py

@@ -0,0 +1,193 @@
+import atexit
+import os
+import psutil
+import random
+import sys
+import threading
+import time
+
+from collections import defaultdict
+from model.custom_struct import PcConfig
+from net.task_api import task_api
+from tools.emulator.ld_operate import LD
+from tools.emulator.ys_operate import YS
+from tools.dm_operate import Dm
+from tools.log import logger
+from tools.utils import Utils
+from tools.wuyouip import WyIP
+from server import log_server
+from update.update import Updater
+
+
+class GlobalManager:
+    """全局管理器类,用于管理配置文件、线程、设备信息和模拟器实例。"""
+
+    def __init__(self):
+        """初始化 GlobalManager 实例。"""
+
+        self._lock = threading.Lock()
+        # 初始化全局控制标志
+        self.global_control = True
+        self.device_info: PcConfig | None = None
+        self.device_mac = ''
+        self.script_path = ''
+        self.image_path = ''
+        self.share_path = ''
+        self.updater = None
+        self.script_log_port = 0
+        self.httpd = None
+        self.wy_ip: WyIP | None = None
+        self.window_status = None
+        self.emulator_status = None
+        self.emulators = None
+
+    def init(self):
+        self.device_info = self._initialize_device_info()
+        self.device_mac = Utils.get_mac_address()
+        self.script_path = self._initialize_script_path()
+        self.image_path = self._initialize_image_path()
+        self.share_path = self._initialize_share_path()
+        self.updater = Updater(self.script_path, self.image_path)
+        threading.Thread(target=self.updater.run).start()
+        self._initialize_emulators()
+        self.script_log_port = random.randint(10000, 32767)
+        self.httpd = log_server.start_http(handler_class=log_server.SimpleHTTPRequestHandler,
+                                           port=self.script_log_port)
+        atexit.register(log_server.stop_http, self.httpd)
+        if self.device_info.use_wuyouip == '1':
+            self.wy_ip = WyIP()
+        self.window_status = defaultdict(int)
+        self.emulator_status = defaultdict(int)
+
+        # 免注册加载大漠
+        if hasattr(sys, '_MEIPASS'):
+            # 程序运行在打包状态
+
+            ret, msg = Dm.reg_free(os.path.join(sys._MEIPASS, 'dm'))
+        else:
+            ret, msg = Dm.reg_free(os.path.join(os.getcwd(), 'dm'))
+
+        if ret:
+            logger.info(f'大漠插件注册成功,版本号:{msg}')
+        else:
+            logger.error(f'大漠插件注册失败,错误信息:{msg}')
+
+    @staticmethod
+    def _initialize_device_info() -> PcConfig:
+        """初始化并返回设备信息。"""
+        while True:
+            result, device_info = task_api.get_device_info()
+            if result:
+                logger.info(f'当前设备名称:{device_info.pc_name},负责人:{device_info.officer}')
+                return device_info
+            logger.exception(f'未获取到设备信息:{device_info}')
+            time.sleep(3)
+
+    @staticmethod
+    def _initialize_script_path():
+        """初始化并返回脚本文件夹路径。"""
+        script_path = os.path.join(os.getcwd(), '脚本代码')
+        os.makedirs(script_path, exist_ok=True)
+        return script_path
+
+    @staticmethod
+    def _initialize_image_path():
+        """初始化并返回镜像文件夹路径。"""
+        image_path = os.path.join(os.getcwd(), '游戏母镜像')
+        os.makedirs(image_path, exist_ok=True)
+        return image_path
+
+    @staticmethod
+    def _initialize_share_path():
+        """初始化并返回共享文件夹路径。"""
+        share_path = os.path.join(os.getcwd(), 'share')
+        os.makedirs(share_path, exist_ok=True)
+        return share_path
+
+    def set_global_control(self, control):
+        """设置全局控制标志。"""
+        with self._lock:
+            self.global_control = control
+
+    def get_global_control(self):
+        """获取全局控制标志,如果传入线程 ID,则根据线程控制标志决定是否继续等待。"""
+        return self.global_control
+
+    def get_script_path(self):
+        """获取脚本文件夹路径。"""
+        return self.script_path
+
+    def set_script_path(self, path):
+        """设置脚本文件夹路径。"""
+        self.script_path = path
+
+    def get_image_path(self):
+        """获取镜像文件夹路径。"""
+        return self.image_path
+
+    def set_device_officer(self, officer):
+        """设置设备管理员。"""
+        self.device_officer = officer
+
+    def get_share_path(self):
+        """获取共享文件夹路径。"""
+        return self.share_path
+
+    def set_share_path(self, path):
+        """设置共享文件夹路径。"""
+        self.share_path = path
+
+    @staticmethod
+    def _initialize_emulators():
+        """初始化所有模拟器。"""
+        emulators = [
+            lambda: LD(4),
+            lambda: LD(64),
+            lambda: LD(9),
+            lambda: YS()
+        ]
+        for emulator in emulators:
+            try:
+                emu = emulator()
+                emu.close_all()
+                emu_list = emu.get_list()
+                for emu_info in emu_list:
+                    if emu_info.index != 0:
+                        emu.remove(emu_info.index)
+                        time.sleep(0.1)
+            except Exception as e:
+                logger.info(e)
+
+    def set_emulator_status(self, emulator_key, status):
+        """设置模拟器状态。"""
+        with self._lock:
+            self.emulator_status[emulator_key] = status
+
+    def get_emulator_status(self, emulator_key):
+
+        """获取模拟器状态。"""
+        with self._lock:
+            return self.emulator_status.get(emulator_key, 0)
+
+    def set_window_status(self, window_id, status):
+        """设置窗口状态。"""
+        with self._lock:
+            self.window_status[window_id] = status
+
+    def get_window_status(self, window_id):
+        """获取窗口状态。"""
+        with self._lock:
+            return self.window_status.get(window_id, 0)
+
+    @staticmethod
+    def is_self_running():
+        """检查当前进程是否已经在运行"""
+        current_process = os.path.basename(sys.argv[0])  # 获取当前脚本名
+        for proc in psutil.process_iter(attrs=['pid', 'name']):
+            if proc.info['name'] == current_process and proc.info['pid'] != os.getpid():
+                return True
+        return False
+
+
+# 实例化全局管理器
+GM = GlobalManager()

+ 417 - 0
model/helper.py

@@ -0,0 +1,417 @@
+import ctypes
+import os
+import threading
+import time
+
+from dataclasses import dataclass
+from model.custom_struct import GameConfig, AccountInfo
+from model.global_manager import GM
+from scripts.script import Script
+from tools.dm_operate import Dm
+from tools.emulator.emulator import Emulator
+from tools.emulator.ld_operate import LD
+from tools.emulator.ys_operate import YS
+from tools.upload_log import LogInfo
+from tools.utils import Utils
+
+
+@dataclass
+class ScriptHelper:
+    dm: Dm = None  # 大漠对象
+    bind_handle: int = None  # 绑定句柄
+    top_handle: int = None  # 顶层句柄
+    emulator_index: int = None  # 模拟器下标
+    timeout: int = None  # 超时时间
+    game_id: str = None  # 游戏id
+    game_type: str = None  # 游戏类型
+    account: str = None  # 账号
+    password: str = None  # 密码
+    retained: int = None  # 新增留存
+    log_uuid: str = None  # 日志uuid
+    log_port: int = None  # 日志端口
+    emu: Emulator = None  # 模拟器对象
+
+
+class MyHelper:
+    """模拟器任务助手"""
+
+    _lock = threading.Lock()
+
+    def __init__(self, window_id: int, game_config: GameConfig, account_info: AccountInfo):
+        self.window_id = window_id
+        self.game_config = game_config
+        self.account_info = account_info
+        self.emulator_type = self.game_config.emulator_type
+        self.emu = self._initialize_emulator_instance()
+        self.emulator_index = self._get_emulator()
+        self.dm = Dm()
+        self.emulator_info = None
+        self.wy_ip = GM.wy_ip
+        self.log_port = GM.script_log_port
+        self.script = Script(self.game_config.script)
+        self.log_uuid = f'{time.time()}_{self.account_info.account}'
+        self.upload_log = LogInfo(self.log_uuid)
+        self.upload_account_log(GM.device_info.pc_name, GM.device_info.officer, GM.device_mac)
+
+    def _initialize_emulator_instance(self):
+        emulator_mapping = {
+            '雷电模拟器4-32位': lambda: LD(4),
+            '雷电模拟器4-64位': lambda: LD(64),
+            '雷电模拟器9': lambda: LD(9),
+            '夜神模拟器7-32位': lambda: YS(7),
+            '夜神模拟器7-64位': lambda: YS(8),
+            '夜神模拟器9': lambda: YS(9)
+        }
+        # 仅当查找到类型时才会实例化,否则返回默认的 LD(9)
+        return emulator_mapping.get(self.emulator_type, lambda: LD(9))()
+
+    def _get_emulator(self):
+        with MyHelper._lock:
+            index = self.window_id
+            while GM.get_global_control():
+
+                if self.emu.is_exists(index):
+                    if GM.get_emulator_status(f'{self.emulator_type}-{index}') == 0:
+                        GM.set_emulator_status(f'{self.emulator_type}-{index}', 1)
+                        return index
+                    raise Exception(f'模拟器-{index}状态异常')
+                else:
+                    # 模拟器不存在,添加新的模拟器
+                    self.emu.add(f'new-{index}')
+                    time.sleep(3)
+
+    def get_emulator_index(self):
+        return self.emulator_index
+
+    def set_emulator_index(self, index):
+        self.emulator_index = index
+
+    def restore_emulator(self):
+        try:
+            self.emu.restore(self.emulator_index, os.path.join(GM.image_path, self.game_config.image))
+            time.sleep(1)
+            self.emu.rename(self.emulator_index, f'{self.game_config.task_id}-{self.emulator_index}')
+            return True, '还原模拟器成功'
+        except Exception as e:
+            return False, str(e)
+
+    def modify_emulator(self):
+        try:
+            self.emu.modify(self.emulator_index,
+                            cpu=self.game_config.cpu,
+                            memory=self.game_config.memory,
+                            resolution=self.game_config.resolution,
+                            manufacturer=self.account_info.manufacturer,
+                            model=self.account_info.model,
+                            pnumber=self.account_info.pnumber,
+                            imei=self.account_info.imei,
+                            imsi=self.account_info.imsi,
+                            simserial=self.account_info.simserial,
+                            androidid=self.account_info.androidid,
+                            mac=self.account_info.mac if self.emu.emulator_type == 'ld' else self.account_info.mac_colon
+                            )
+            return True, '修改模拟器信息成功'
+        except Exception as e:
+            return False, str(e)
+
+    def switch_emulator_area(self):
+        if self.wy_ip is None:
+            return False, "未初始化无忧IP"
+
+        self.wy_ip.set_wy_token()
+
+        if len(self.account_info.number_operatorTypes.split('_')) < 1:
+            return False, "number_operatorTypes参数错误"
+
+        return self.wy_ip.switch_emulator_area(self.emulator_index,
+                                               self.account_info.number_operatorTypes.split('_')[1],
+                                               self.account_info.number_operatorTypes.split('_')[0])
+
+    def confirm_emulator_area(self):
+        result, message = self.wy_ip.queryProcessProxyRegion(self.emulator_info.vbox_pid)
+        if not result:
+            return result, message
+        if message != self.account_info.number_operatorTypes.split('_')[0]:
+            return False, f'当前模拟器切换地区失败,目标地区:{self.account_info.number_operatorTypes.split("_")[0]},当前地区:{message}'
+        return True, '当前模拟器切换地区成功'
+
+    def start_emulator_and_set_position(self):
+        """启动模拟器并设置窗口位置"""
+        try:
+            # 获取共享路径
+            share_path = GM.get_share_path()
+            emulator_share_dir = os.path.join(share_path, str(self.window_id))
+
+            # 如果目录不存在则创建
+            if not os.path.exists(emulator_share_dir):
+                os.makedirs(emulator_share_dir)
+
+            # 设置共享目录
+            self.emu.set_share_dir(self.emulator_index, emulator_share_dir)
+
+            # 启动模拟器并确认启动状态
+            result, message = self.emu.start_and_confirm(self.emulator_index)
+
+            if result:
+                self.emulator_info = message
+                # 设置模拟器窗口位置
+                self.emu.set_emulator_position(self.emulator_info.top_handle, self.window_id)
+                time.sleep(1)
+                return True, f"模拟器编号:{self.emulator_index},启动成功"
+            return False, f"模拟器编号:{self.emulator_index},,启动失败,错误信息:{message}"
+        except Exception as e:
+            return False, f"模拟器编号:{self.emulator_index},启动失败,错误信息:{str(e)}"
+
+    def close_emulator_and_confirm(self):
+        """关闭模拟器并确保所有相关进程被终止。"""
+        # 检查模拟器是否正在运行
+        if self.emu.is_running(self.emulator_index):
+            self.emu.close(self.emulator_index)
+            time.sleep(3)  # 等待模拟器关闭的缓冲时间
+
+        # 获取模拟器信息
+        emu_info = self.emu.get_info(self.emulator_index)
+        if emu_info:
+            # 确认主进程 pid 有效
+            if emu_info.pid > 0:
+                Utils.kill_process(emu_info.pid)
+            # 确认 VBox 进程 pid 有效
+            if emu_info.vbox_pid > 0:
+                Utils.kill_process(emu_info.vbox_pid)
+
+    def bind_emulator(self):
+        try:
+            if self.game_config.script.endswith('.dll'):
+                result = self.script.execute('dm_bind',
+                                             self.emulator_index,
+                                             self.emulator_info.bind_handle,
+                                             timeout=10
+                                             )
+                return True, result
+            elif self.game_config.script.endswith('.py'):
+                script_helper = ScriptHelper(dm=self.dm,
+                                             emu=self.emu,
+                                             emulator_index=self.emulator_index,
+                                             top_handle=self.emulator_info.top_handle,
+                                             bind_handle=self.emulator_info.bind_handle,
+                                             game_type=self.game_config.game_type,
+                                             game_id=self.game_config.task_id,
+                                             account=self.account_info.account,
+                                             password=self.account_info.password,
+                                             retained=self.account_info.retained,
+                                             log_uuid=self.log_uuid,
+                                             log_port=self.log_port,
+                                             timeout=self.game_config.timeout * 60
+                                             )
+                result = self.script.execute('dm_bind', script_helper, timeout=10)
+                return True, result
+            else:
+                return False, '脚本类型错误'
+
+        except Exception as e:
+            return False, str(e)
+
+    def start_game(self):
+        """启动游戏并处理可能的异常。
+
+        Returns:
+            tuple: 成功与否的标志和结果或错误信息。
+        """
+        try:
+            if self.game_config.script.endswith('.dll'):
+                # 获取字符串的字节数据
+                task_id_bytes = self.game_config.task_id.encode('gbk')
+                account_bytes = self.account_info.account.encode('gbk')
+                script_path_bytes = self.script.script_path.encode('gbk')
+                log_uuid_bytes = self.log_uuid.encode('gbk')
+
+                # 创建 c_char_p 指针
+                task_id_ptr = ctypes.c_char_p(task_id_bytes)
+                account_ptr = ctypes.c_char_p(account_bytes)
+                script_path_ptr = ctypes.c_char_p(script_path_bytes)
+                log_uuid_ptr = ctypes.c_char_p(log_uuid_bytes)
+
+                result = self.script.execute(
+                    'script_start_game',
+                    self.emulator_index,
+                    task_id_ptr,
+                    self.emulator_info.bind_handle,
+                    account_ptr,
+                    self.game_config.timeout * 60 * 1000,
+                    script_path_ptr,
+                    GM.script_log_port,
+                    log_uuid_ptr,
+                    timeout=30 * 60
+                )
+                return True, result
+            elif self.game_config.script.endswith('.py'):
+                script_helper = ScriptHelper(dm=self.dm,
+                                             emu=self.emu,
+                                             emulator_index=self.emulator_index,
+                                             top_handle=self.emulator_info.top_handle,
+                                             bind_handle=self.emulator_info.bind_handle,
+                                             game_id=self.game_config.task_id,
+                                             game_type=self.game_config.game_type,
+                                             account=self.account_info.account,
+                                             password=self.account_info.password,
+                                             retained=self.account_info.retained,
+                                             log_uuid=self.log_uuid,
+                                             log_port=self.log_port,
+                                             timeout=self.game_config.timeout * 60
+                                             )
+                result = self.script.execute('script_start_game',script_helper, timeout=30*60)
+                return True, result
+            else:
+                return False, "脚本文件格式错误"
+
+        except Exception as e:
+            return False, f"脚本_启动游戏,错误: {e}"
+
+    def login_game(self):
+        """登录游戏并处理可能的异常。
+
+        Returns:
+            tuple: 成功与否的标志和结果或错误信息。
+        """
+        try:
+            if self.game_config.script.endswith('.dll'):
+                # 获取字符串的字节数据
+                account_bytes = self.account_info.account.encode('gbk')
+                password_bytes = self.account_info.password.encode('gbk')
+                wechat_order_id_bytes = self.account_info.wechat_order_id.encode('gbk')
+                task_id_bytes = self.game_config.task_id.encode('gbk')
+                script_path_bytes = self.script.script_path.encode('gbk')
+                channel_id_bytes = self.game_config.channel_id.encode('gbk')
+
+                # 创建 c_char_p 指针
+                account_ptr = ctypes.c_char_p(account_bytes)
+                password_ptr = ctypes.c_char_p(password_bytes)
+                wechat_order_id_ptr = ctypes.c_char_p(wechat_order_id_bytes)
+                task_id_ptr = ctypes.c_char_p(task_id_bytes)
+                script_path_ptr = ctypes.c_char_p(script_path_bytes)
+                channel_id_ptr = ctypes.c_char_p(channel_id_bytes)
+
+                result = self.script.execute(
+                    'script_login_game',
+                    self.emulator_index,  # 窗口下标
+                    self.game_config.timeout * 60 * 1000,  # 超时时间
+                    self.emulator_info.bind_handle,
+                    account_ptr,
+                    password_ptr,
+                    self.account_info.retained,
+                    wechat_order_id_ptr,
+                    1,
+                    task_id_ptr,
+                    script_path_ptr,
+                    GM.script_log_port,
+                    channel_id_ptr,
+                    timeout=30 * 60
+                )
+                return True, result
+            elif self.game_config.script.endswith('.py'):
+                script_helper = ScriptHelper(dm=self.dm,
+                                             emu=self.emu,
+                                             emulator_index=self.emulator_index,
+                                             top_handle=self.emulator_info.top_handle,
+                                             bind_handle=self.emulator_info.bind_handle,
+                                             game_id=self.game_config.task_id,
+                                             game_type=self.game_config.game_type,
+                                             account=self.account_info.account,
+                                             password=self.account_info.password,
+                                             retained=self.account_info.retained,
+                                             log_uuid=self.log_uuid,
+                                             log_port=self.log_port,
+                                             timeout=self.game_config.timeout * 60
+                                             )
+                result = self.script.execute('script_login_game',script_helper, timeout=30 * 60)
+                return True, result
+            else:
+                return False, "脚本文件格式错误"
+
+        except Exception as e:
+            return False, f'脚本_登陆游戏,错误: {e}'
+
+    def main_task(self):
+        """执行主线任务并处理可能的异常。
+
+        Returns:
+            tuple: 成功与否的标志和结果或错误信息。
+        """
+        try:
+            if self.game_config.script.endswith('.dll'):
+                # 获取字符串的字节数据
+                account_bytes = self.account_info.account.encode('gbk')
+                task_id_bytes = self.game_config.task_id.encode('gbk')
+                script_path_bytes = self.script.script_path.encode('gbk')
+
+                # 创建 c_char_p 指针
+                account_ptr = ctypes.c_char_p(account_bytes)
+                task_id_ptr = ctypes.c_char_p(task_id_bytes)
+                script_path_ptr = ctypes.c_char_p(script_path_bytes)
+
+                result = self.script.execute(
+                    'script_main_task',
+                    self.emulator_index,
+                    self.game_config.timeout * 60 * 1000,
+                    account_ptr,  # 直接传递数据地址
+                    0,
+                    self.emulator_info.bind_handle,
+                    task_id_ptr,  # 直接传递数据地址
+                    script_path_ptr,  # 直接传递数据地址
+                    GM.script_log_port,
+                    timeout=60 * 60
+                )
+                return True, result
+            elif self.game_config.script.endswith('.py'):
+                script_helper = ScriptHelper(dm=self.dm,
+                                             emu=self.emu,
+                                             emulator_index=self.emulator_index,
+                                             top_handle=self.emulator_info.top_handle,
+                                             bind_handle=self.emulator_info.bind_handle,
+                                             game_id=self.game_config.task_id,
+                                             game_type=self.game_config.game_type,
+                                             account=self.account_info.account,
+                                             password=self.account_info.password,
+                                             retained=self.account_info.retained,
+                                             log_uuid=self.log_uuid,
+                                             log_port=self.log_port,
+                                             timeout=self.game_config.timeout * 60
+                                             )
+                result = self.script.execute('script_main_task', script_helper, timeout=60 * 60)
+                return True, result
+            else:
+                return False, "脚本文件格式错误"
+        except Exception as e:
+            return False, f'脚本_教程主线,错误: {e}'
+
+    def upload_account_log(self, pc_code: str, operator: str, pc_mac: str):
+        self.upload_log.set_account_info(game_id=int(self.game_config.task_id),
+                                         account_type=self.account_info.game_type,
+                                         pwd=self.account_info.password,
+                                         account=self.account_info.account,
+                                         task_type=self.account_info.retained)
+        self.upload_log.set_pc_info(pc_code=pc_code, operator=operator, pc_mac=pc_mac, pc_ip='')
+        self.upload_log.upload_pull_account_log(1)
+
+    def upload_device_log(self, status: int):
+        try:
+            self.upload_log.set_device_info(device_id=self.emu.get_android_id(self.emulator_index),
+                                            device_manufacturer=self.emu.get_manufacturer(self.emulator_index),
+                                            device_model=self.emu.get_model(self.emulator_index),
+                                            device_imei=self.emu.get_IMEI(self.emulator_index),
+                                            device_sdk=self.emu.get_android_version(self.emulator_index),
+                                            device_mac=self.emu.get_mac(self.emulator_index),
+                                            device_number=self.emu.get_phone_number(self.emulator_index),
+                                            script_device_id='')
+
+            self.upload_log.set_simulator_info(simulator_code=str(self.emulator_index),
+                                               simulator_mac='',
+                                               simulator_ip_city='',
+                                               simulator_ip=self.emu.get_net_ip(self.emulator_index)
+                                               )
+
+            ret = self.upload_log.upload_start_simulator_log(status=status)
+            return ret
+
+        except Exception as e:
+            return False, f'脚本_设备信息,错误: {e}'

+ 0 - 0
net/__init__.py


BIN
net/__pycache__/__init__.cpython-312.pyc


BIN
net/__pycache__/task_api.cpython-312.pyc


+ 85 - 0
net/http_base.py

@@ -0,0 +1,85 @@
+
+import requests
+from tools.log import logger
+
+
+def http_post_proxy_request(url, form_data, headers=None, proxy=None, timeout=30):
+    """
+    发送带有 JSON 数据的 POST 请求,并使用代理
+
+    参数:
+    - url: 请求的目标 URL
+    - json_data: 要发送的 JSON 数据
+    - headers: 可选,请求头信息
+    - proxy: 可选,代理地址,例如 "http://127.0.0.1:10809"
+    - timeout: 可选,请求超时时间,默认为 10 秒
+
+    返回值:
+    - response: 请求的响应对象
+    """
+    try:
+        # 发送 POST 请求
+        proxies = {"http": proxy, "https": proxy} if proxy else None
+        response = requests.post(url, data=form_data, headers=headers, proxies=proxies, timeout=timeout)
+        # 检查响应状态码
+        response.raise_for_status()
+        # 如果状态码为 200,表示请求成功
+        if response.status_code == 200:
+            return response
+            #
+            # # 获取响应的内容类型
+            # content_type = response.headers.get('Content-Type', '')
+            # # 如果内容类型为 JSON,解析 JSON 数据
+            # if 'application/json' in content_type:
+            #     json_data = response.json()
+            #     return json_data
+            # else:
+            #     # 如果内容类型不是 JSON,按照其他方式处理
+            #     logger.error(f"http_post_request:{url}:{response}")
+            #     return response.text
+        else:
+            # 如果状态码不为 200,可以根据实际需求处理其他状态码
+            print(f"Unexpected status code: {response.status_code}")
+            logger.exception(f"http_post_request:{url}:{response}")
+            return None
+    except requests.exceptions.RequestException as e:
+        # 捕获请求异常
+        logger.exception(f"http_post_request:{url}:{e}:{response}")
+        return None
+
+
+def http_post_request(url, data, timeout=30):
+    try:
+        response = requests.post(url, json=data, timeout=timeout)
+        response.raise_for_status()  # 检查请求是否成功
+        if 200 == response.status_code:
+            return response
+        logger.exception(f"http_post_request - code: {response},url: {url},data: {data}")
+        return None
+
+    except requests.exceptions.RequestException as req_exc:
+        print(f"HTTP POST request error: {req_exc}")
+        logger.exception(f"http_post_request - Error: {req_exc}, url: {url}, data: {data}")
+        return None
+    except Exception as e:
+        print(f"An unexpected error occurred: {e}")
+        logger.exception(f"http_post_request - Unexpected error: {e}, url: {url}, data: {data}")
+        return None
+
+
+def http_post_request_proxy(url, headers, data, proxy=None, timeout=30):
+    try:
+        proxies = {"http": proxy, "https": proxy} if proxy else None
+        response = requests.post(url, headers=headers, data=data, proxies=proxies, timeout=timeout)
+        response.raise_for_status()  # 检查请求是否成功
+        if 200 == response.status_code:
+            return response
+        logger.error(f"http_post_request - code: {response},url: {url},data: {data}")
+        return None
+
+    except requests.exceptions.RequestException as req_exc:
+        logger.error(f"http_post_request_proxy - Error: {req_exc}, url: {url}, headers: {headers},data: {data}")
+        return None
+    except Exception as e:
+        logger.error(f"http_post_request_proxy - Unexpected error: {e}, url: {url}, headers: {headers},data: {data}")
+        return None

+ 52 - 0
net/sms_api.py

@@ -0,0 +1,52 @@
+import requests
+
+from tools.log import logger
+
+
+class SMS_Client:
+    def __init__(self, api_key, country_code):
+        self.api_key = api_key
+        self.country_code = country_code
+
+    def get_phone_number(self):
+        try:
+            url = 'https://sms-activate.org/stubs/handler_api.php'
+            data = {
+                'api_key': self.api_key,
+                'action': 'getNumberV2',
+                'service': 'fb',
+                'forward': 'false',
+                'operator': 'any',
+                'country': self.country_code,
+                'verification': 'false'
+            }
+            response = requests.get(url, params=data)
+            print(response.text)
+            if response.status_code == 200:
+                return response.json()
+            return None
+        except Exception as e:
+            logger.error(f"Error getting phone number: {e} : {response.text}")
+            return None
+
+    def get_verification_code(self, activation_id):
+        try:
+            url = 'https://sms-activate.org/stubs/handler_api.php'
+            data = {
+                'api_key': self.api_key,
+                'action': 'getStatus',
+                'id': activation_id,
+            }
+            response = requests.get(url, params=data)
+            print(response.text)
+            if response.status_code == 200:
+                if 'OK' in response.text:
+                    return response.text.split(':')[1]
+            return None
+        except Exception as e:
+            logger.error(f"Error getting phone number: {e} : {response.text}")
+            return None
+
+    def set_status(self, activation_id, status):
+        # Implement the logic to set the status of the verification code
+        pass

+ 161 - 0
net/task_api.py

@@ -0,0 +1,161 @@
+import importlib.util
+import json
+import os
+import requests
+
+from dacite import from_dict
+from model.custom_struct import PcConfig, AccountInfo, WindowInfo, UploadLog
+from tools.log import logger
+
+
+class TaskApi:
+    def __init__(self):
+        self.pc_name = os.getenv('COMPUTERNAME') or os.uname().nodename
+
+        # 配置内容及路径
+        config_content = """# 接口配置\n#http://dataset.skfzs.top/api\nconfig_url='http://dcf.loadfa.com/api'\n# http://dataset.skfzs.top:8001/api\nlog_url='http://assist.qiming321.cn:8888'"""
+        config_path = os.path.join(os.getcwd(), 'config', 'config.py')
+
+        if not os.path.exists(config_path):
+            # 文件不存在,创建并写入内容
+            os.makedirs(os.path.dirname(config_path), exist_ok=True)  # 确保目录存在
+            with open(config_path, 'w', encoding='utf-8') as file:
+                file.write(config_content)
+
+        # 动态导入配置文件
+        config_spec = importlib.util.spec_from_file_location("config", config_path)
+        config_module = importlib.util.module_from_spec(config_spec)
+        config_spec.loader.exec_module(config_module)
+
+        try:
+            self.config_url = config_module.config_url
+            self.log_url = config_module.log_url
+        except AttributeError as e:
+            raise ImportError("配置文件中缺少必要的变量 config_url 或 log_url") from e
+
+        self.task_url = 'http://xjf.lianyou.fun:8099'
+
+    def get_device_info(self) -> tuple[bool,PcConfig|str]:
+
+        url = f'{self.config_url}/deviceComputer/uploadAndFindCCVersion?pcNum={self.pc_name}'
+        try:
+            result = requests.get(url, timeout=10)
+            if result.status_code == 200:
+                json_obj = result.json()
+                if json_obj['code'] == 0:
+                    obj = json_obj['data']['config']
+                    pc_config = PcConfig(
+                        pc_name=json_obj['data']['pcNum'],
+                        officer=json_obj['data']['directorName'],
+                        use_wuyouip=obj['使用无忧ip'],
+                        only_new=obj['只做新增'],
+                        only_retained=obj['只做留存'],
+                        timed_start=0,
+                        check_md5_on_start=obj['启动时md5检测'],
+                        start_dkw_on_start=obj['多开王自启动'],
+                        check_script_update=obj['检查脚本更新'],
+                        check_image_update=obj['检查镜像更新'],
+                        window_nums=int(obj['窗口数量'])
+                    )
+                    return True, pc_config
+            return False, result.text
+        except Exception as e:
+            return False,str(e)
+
+    def get_window_list(self) -> list[WindowInfo]:
+        url = f'{self.config_url}/window/getWindowPublicV2?computerID={self.pc_name}'
+        try:
+            result = requests.get(url, timeout=10)
+            if result.status_code == 200:
+                json_obj = result.json()
+                if json_obj['code'] == 0:
+                    window_list = []
+                    for obj in json_obj['data']['list']:
+                        window_info = WindowInfo(
+                            window_id=obj['windowID'],
+                            game_list=obj['configGame']
+                        )
+                        window_list.append(window_info)
+                    return window_list
+            return []
+        except Exception as e:
+            logger.exception(f'get_window_list error: {e}')
+            return []
+
+    def heartbeat(self,operator) -> tuple[bool, str]:
+
+        url = f'http://assist.qiming321.cn:8888/loging/computerHeartbeat'
+        data = {
+            'pc_code': self.pc_name,
+            'operator': operator
+        }
+
+        try:
+            result = requests.post(url, data= json.dumps(data), timeout=10)
+
+            if result.status_code == 200 :
+                if result.json()['code'] == 0:
+                    return True,result.json()['msg']
+            return False,result.json()['msg']
+        except Exception as e:
+            logger.exception(f'heartbeat error: {e}')
+            return False, str(e)
+
+    def get_account(self, task_id: str,new,retained) -> tuple[bool, AccountInfo] | tuple[bool, str]:
+        if new == '1':
+            url = f'{self.task_url}/v1/task/get_account?game_id={task_id}&new=1'
+        elif retained == '1':
+            url = f'{self.task_url}/v1/task/get_account?game_id={task_id}&new=2'
+        else:
+            url = f'{self.task_url}/v1/task/get_account?game_id={task_id}'
+        try:
+            result = requests.get(url, timeout=10)
+            if result.status_code == 200:
+                if isinstance(result.json(), dict):
+                    json_obj = result.json()
+                    return True , from_dict(AccountInfo, json_obj)
+            return False , result.text
+        except Exception as e:
+            return False , str(e)
+
+    def up_log_profession(self, account, game_id, action, action_result) -> str:
+        url = f'{self.task_url}/v1/device/setAccountLog'
+        data = {
+            'account': account,
+            'game_id': game_id,
+            'action': action,
+            'action_result': action_result,
+        }
+        try:
+            result = requests.get(url, params=data, timeout=10)
+            return result.text
+        except Exception as e:
+            logger.exception(f'up_log_profession error: {e}')
+            return str(e)
+
+    def upload_log_to_server(self, params: UploadLog) -> (bool, str):
+        json_data = json.dumps(params.to_dict())
+        url = f'{self.log_url}/loging/setLog'
+
+        # 发送POST请求
+        try:
+            response = requests.post(url, data=json_data, headers={'Content-Type': 'application/json'}, timeout=10)
+            return response.text
+        except Exception as e:
+            return str(e)
+
+
+    def notify(self,desc:str):
+        url = f'{self.config_url}/loging/supConErr'
+        data = {
+            'pc_code': self.pc_name,
+            'describe': desc
+        }
+        try:
+            result = requests.post(url, data=data, timeout=10)
+            return result.text
+        except Exception as e:
+            return str(e)
+
+
+task_api = TaskApi()

+ 0 - 0
scripts/__init__.py


BIN
scripts/__pycache__/__init__.cpython-312.pyc


BIN
scripts/__pycache__/script.cpython-312.pyc


+ 119 - 0
scripts/script.py

@@ -0,0 +1,119 @@
+import ctypes
+import importlib
+import os
+import sys
+import threading
+import time
+
+from model.global_manager import GM
+from tools.utils import Utils
+
+
+class Script:
+    def __init__(self, script_name):
+        self.script_path = GM.script_path  # 设置脚本目录
+        script_real_name = self.get_real_script_name(script_name)
+        self.script = self._load_script(script_real_name)  # 加载脚本
+        self._initialize_env()  # 初始化脚本环境
+
+    @staticmethod
+    def _initialize_env():
+        """初始化脚本环境。"""
+        from PIL import Image  # 确保 PIL 库被导入
+
+    def _load_script(self, script_name):
+        """根据文件类型加载脚本(DLL 或 Python)"""
+        if script_name.endswith('.dll'):
+            return self._load_dll(script_name)  # 加载 DLL 脚本
+        elif script_name.endswith('.py'):
+            return self._load_py(script_name)  # 加载 Python 脚本
+        else:
+            raise ValueError("Unsupported script type")
+
+    def _load_dll(self, script_name):
+        """加载指定的 DLL 脚本"""
+        try:
+            dll_path = os.path.join(self.script_path, script_name)
+            return ctypes.WinDLL(dll_path)
+        except Exception as e:
+            raise RuntimeError(f"Failed to load DLL {script_name}: {e}")
+
+    def _load_py(self, script_name):
+        """加载指定的 Python 脚本"""
+        if self.script_path not in sys.path:
+            sys.path.append(self.script_path)  # 确保脚本目录在路径中
+        try:
+            module_name = script_name[:-3]  # 去掉 .py 后缀
+            return importlib.import_module(module_name)  # 返回模块对象
+        except ImportError as e:
+            raise RuntimeError(f"Failed to load Python script {script_name}: {e}")
+
+    def execute(self, method_name, *args, timeout=10*60):
+        """调用 DLL 或 Python 脚本中的指定方法并传入参数,支持超时机制"""
+        if not self.script:
+            raise RuntimeError("Script not loaded")
+
+        result = None
+        error = None
+
+        def target():
+            nonlocal result, error
+            try:
+                if isinstance(self.script, ctypes.CDLL):
+                    # 调用 DLL 方法
+                    result = self.script[method_name](*args)
+                else:
+                    # 动态调用 Python 方法并传入参数
+                    method = getattr(self.script, method_name)
+                    result = method(*args)
+            except Exception as e:
+                error = e
+
+        # 启动子线程执行目标方法
+        thread = threading.Thread(target=target)
+        thread.start()
+
+        # 等待执行,超时则停止线程
+        thread.join(timeout)
+
+        if thread.is_alive():
+            # 如果线程超时未完成,抛出超时异常
+            raise TimeoutError(f"Method '{method_name}' execution timed out after {timeout} seconds")
+
+        if error:
+            # 如果执行过程中有异常,抛出捕获到的异常
+            raise error
+
+        return result
+
+    def get_real_script_name(self, script_name: str) -> str:
+        """获取与当前时间戳最接近的文件名(DLL或Python脚本)"""
+        current_timestamp = int(time.time())
+        closest_file = script_name
+
+        def time_diff(file_name):
+            # 检查文件名格式并提取时间戳
+            if '#' in file_name:
+                # 假设时间戳在文件名中以 `#<timestamp>` 的形式存在
+                file_timestamp_str = Utils.extract_between_strings(file_name, '#', os.path.splitext(file_name)[1])
+                if file_timestamp_str.isdigit():  # 确保提取的时间戳是数字
+                    file_timestamp = int(file_timestamp_str)
+                    return abs(current_timestamp - file_timestamp)
+            return float('inf')
+
+        # 获取文件扩展名
+        valid_extensions = os.path.splitext(script_name)[1]
+        script_files = [f for f in os.listdir(self.script_path)
+                        if f.endswith(valid_extensions) and os.path.splitext(script_name)[0] in f]
+
+        if script_files:
+            closest_file = min(script_files, key=time_diff)
+
+        return closest_file
+
+    def __del__(self):
+        """释放 DLL 资源"""
+        if self.script:
+            if isinstance(self.script, ctypes.CDLL):
+                ctypes.windll.kernel32.FreeLibrary(ctypes.c_void_p(self.script._handle))
+            self.script = None

+ 0 - 0
server/__init__.py


+ 17 - 0
server/http_server.py

@@ -0,0 +1,17 @@
+from abc import abstractmethod
+from http.server import BaseHTTPRequestHandler, HTTPServer
+
+
+
+class MyHandler(BaseHTTPRequestHandler):
+
+    @abstractmethod
+    def do_GET(self):
+        pass
+
+    @abstractmethod
+    def do_POST(self):
+        pass
+
+
+

+ 42 - 0
server/log_server.py

@@ -0,0 +1,42 @@
+from urllib.parse import urlparse, parse_qs
+from http.server import HTTPServer, BaseHTTPRequestHandler
+import threading
+
+from tools.log import logger
+
+
+class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
+
+    def log_message(self, format, *args):
+        # 禁用默认的控制台日志输出
+        return
+
+    def do_GET(self):
+        parsed_url = urlparse(self.path)
+        query_params = parse_qs(parsed_url.query)
+
+        p = query_params.get('xxx', [''])[0]
+        p_list = p.split('----')
+
+        logger.info(f'[脚本][模拟器-{p_list[0]}]-{p_list[1]}-{p_list[2]}-{p_list[3]}-{p_list[4]}-{p_list[5]}',int(p_list[0]))
+
+        response = b'{"message": "ok"}'
+        self.send_response(200)
+        self.send_header('Content-type', 'application/json')
+        self.send_header('Content-Length', str(len(response)))
+        self.end_headers()
+        self.wfile.write(response)
+
+
+def start_http(server_class=HTTPServer, handler_class=SimpleHTTPRequestHandler, port = 8000):
+    server_address = ('', port + 1)
+    httpd = server_class(server_address, handler_class)
+    httpd_thread = threading.Thread(target=httpd.serve_forever)
+    httpd_thread.daemon = True
+    httpd_thread.start()
+    return httpd
+
+
+def stop_http(httpd):
+    httpd.shutdown()
+    httpd.server_close()

+ 0 - 0
task/__init__.py


BIN
task/__pycache__/__init__.cpython-312.pyc


BIN
task/__pycache__/task.cpython-312.pyc


+ 146 - 0
task/task.py

@@ -0,0 +1,146 @@
+import threading
+import time
+
+from model.global_manager import GM
+from model.helper import MyHelper
+from net.task_api import task_api
+from tools.log import logger
+
+
+class Task:
+    _lock = threading.Lock()
+
+    def __init__(self, helper: MyHelper):
+        self.helper = helper
+        self.error_count = 0
+
+    def run(self):
+        try:
+            logger.info(f"[窗口-{self.helper.window_id}][模拟器-{self.helper.emulator_index}]任务开始",self.helper.window_id)
+
+            for _ in range(1):
+
+                logger.info(f"[窗口-{self.helper.window_id}][模拟器-{self.helper.emulator_index}]模拟器还原",self.helper.window_id)
+
+                # 模拟器还原、改机、切换地区、启动加锁,主要避免还原和启动时的高硬盘读写以及切换Ip地区时刷新节点的频率
+                with self._lock:
+                    result, message = self.helper.restore_emulator()
+
+                if not result:
+                    logger.exception(f"[窗口-{self.helper.window_id}][模拟器-{self.helper.emulator_index}]模拟器还原失败,错误信息:{message}")
+                    break
+                logger.info(f"[窗口-{self.helper.window_id}][模拟器-{self.helper.emulator_index}]模拟器还原成功",self.helper.window_id)
+                time.sleep(1)
+
+                #改机
+                logger.info(f"[窗口-{self.helper.window_id}][模拟器-{self.helper.emulator_index}]模拟器改机",self.helper.window_id)
+                result, message = self.helper.modify_emulator()
+                if not result:
+                    logger.exception(f"[窗口-{self.helper.window_id}][模拟器-{self.helper.emulator_index}]模拟器改机失败,错误信息:{message}")
+                    break
+                logger.info(f"[窗口-{self.helper.window_id}][模拟器-{self.helper.emulator_index}]模拟器改机成功",self.helper.window_id)
+                time.sleep(1)
+
+                #切换地区
+                if GM.device_info.use_wuyouip == '1':
+                    logger.info(f"[窗口-{self.helper.window_id}][模拟器-{self.helper.emulator_index}]模拟器切换地区",self.helper.window_id)
+                    result, message = self.helper.switch_emulator_area()
+                    if not result:
+                        logger.error(f"[窗口-{self.helper.window_id}][模拟器-{self.helper.emulator_index}]切换模拟器地区失败,错误信息:{message}")
+                        break
+                    logger.info(f"[窗口-{self.helper.window_id}][模拟器-{self.helper.emulator_index}]模拟器地区切换成功",self.helper.window_id)
+                    time.sleep(1)
+
+                #启动
+                logger.info(f"[窗口-{self.helper.window_id}][模拟器-{self.helper.emulator_index}]模拟器启动",self.helper.window_id)
+                with self._lock:
+                    result, message = self.helper.start_emulator_and_set_position()
+
+                if not result:
+                    logger.error(f"[窗口-{self.helper.window_id}][模拟器-{self.helper.emulator_index}]模拟器启动失败,错误信息:{message}")
+                    up_log_profession = task_api.up_log_profession(self.helper.account_info.account, self.helper.game_config.task_id, '启动模拟器', '失败')
+                    logger.info(f"[窗口-{self.helper.window_id}][模拟器-{self.helper.emulator_index}]上报关键日志-{up_log_profession}",self.helper.window_id)
+                    self.helper.upload_device_log(status=0)
+                    break
+                logger.info(f"[窗口-{self.helper.window_id}][模拟器-{self.helper.emulator_index}]模拟器启动成功",self.helper.window_id)
+                up_log_profession = task_api.up_log_profession(self.helper.account_info.account, self.helper.game_config.task_id,'启动模拟器', '成功')
+                logger.info(f"[窗口-{self.helper.window_id}][模拟器-{self.helper.emulator_index}]上报关键日志-{up_log_profession}",self.helper.window_id)
+                self.helper.upload_device_log(status=1)
+                time.sleep(1)
+
+                #上报设备信息
+                logger.info(f"[窗口-{self.helper.window_id}][模拟器-{self.helper.emulator_index}]上报设备信息:{self.helper.upload_log.upload_log.to_dict()}",self.helper.window_id)
+                    # time.sleep(10)
+                    #
+                    # result, message = self.helper.confirm_emulator_area()
+                    # if not result:
+                    #     logger.error(f"[窗口-{self.helper.window_id}][模拟器-{self.helper.emulator_index}]确认模拟器地区错误,错误信息:{message}")
+                    #     break
+                time.sleep(1)
+
+                #大漠绑定
+                logger.info(f"[窗口-{self.helper.window_id}][模拟器-{self.helper.emulator_index}]执行脚本-大漠绑定",self.helper.window_id)
+                result, message = self.helper.bind_emulator()
+                if not result or message != 1:
+                    logger.exception(f'[窗口-{self.helper.window_id}][模拟器-{self.helper.emulator_index}]执行脚本"大漠绑定"失败,返回:{message}')
+                    break
+                logger.info(f'[窗口-{self.helper.window_id}][模拟器-{self.helper.emulator_index}]执行脚本-大漠绑定-成功,返回:{message}',self.helper.window_id)
+                time.sleep(1)
+
+                #启动游戏
+                logger.info(f'[窗口-{self.helper.window_id}][模拟器-{self.helper.emulator_index}]执行脚本-启动游戏',self.helper.window_id)
+                result, message = self.helper.start_game()
+                if not result or message != 1:
+                    self.helper.upload_log.upload_start_game_log(status=0)
+                    logger.exception(f'[窗口-{self.helper.window_id}][模拟器-{self.helper.emulator_index}]执行脚本-启动游戏-失败,错误信息:{message}')
+                    break
+                self.helper.upload_log.upload_start_game_log(status=1)
+                logger.info(f'[窗口-{self.helper.window_id}][模拟器-{self.helper.emulator_index}]执行脚本-启动游戏-成功,返回:{message}',self.helper.window_id)
+                time.sleep(1)
+
+                #登录游戏
+                logger.info(f'[窗口-{self.helper.window_id}][模拟器-{self.helper.emulator_index}]执行脚本-登录游戏',self.helper.window_id)
+                result, message = self.helper.login_game()
+                if not result or message != 1:
+                    logger.exception(f'[窗口-{self.helper.window_id}][模拟器-{self.helper.emulator_index}]执行脚本-登录游戏-失败,错误信息:{message}')
+                    self.helper.upload_log.upload_longin_log(status=0)
+                    break
+                logger.info(f'[窗口-{self.helper.window_id}][模拟器-{self.helper.emulator_index}]执行脚本-登录游戏-成功,返回:{message}',self.helper.window_id)
+                self.helper.upload_log.upload_longin_log(status=1)
+                time.sleep(1)
+
+                #执行主线任务
+                logger.info(f'[窗口-{self.helper.window_id}][模拟器-{self.helper.emulator_index}]执行脚本-教程主线',self.helper.window_id)
+                result, message = self.helper.main_task()
+                if not result:
+                    self.helper.upload_log.upload_main_log(status=0)
+                    logger.exception(f'[窗口-{self.helper.window_id}][模拟器-{self.helper.emulator_index}]执行脚本-教程主线-失败,错误信息:{message}')
+                    break
+                self.helper.upload_log.upload_main_log(status=1)
+                logger.info(f"[窗口-{self.helper.window_id}][模拟器-{self.helper.emulator_index}]执行脚本-教程主线-成功,返回:{message}",self.helper.window_id)
+
+                #上报新增留存状态
+                if self.helper.account_info.retained == 0:
+                    result = task_api.up_log_profession(self.helper.account_info.account, self.helper.game_config.task_id,
+                                                  '教程主线', '新增成功')
+                    logger.info(f"[窗口-{self.helper.window_id}][模拟器-{self.helper.emulator_index}]上报账号-新增成功:{result}",self.helper.window_id)
+
+                else:
+                    result = task_api.up_log_profession(self.helper.account_info.account, self.helper.game_config.task_id,
+                                                  '教程主线', '留存成功')
+                    logger.info(f"[窗口-{self.helper.window_id}][模拟器-{self.helper.emulator_index}]上报账号-留存成功:{result}",self.helper.window_id)
+
+        except Exception as e:
+            logger.exception(f"[窗口-{self.helper.window_id}][模拟器-{self.helper.emulator_index}]任务异常:{e}",self.helper.window_id)
+        finally:
+            time.sleep(1)
+            logger.info(f"[窗口-{self.helper.window_id}][模拟器-{self.helper.emulator_index}]关闭模拟器",
+                        self.helper.window_id)
+            self.helper.close_emulator_and_confirm()
+            logger.info(f"[窗口-{self.helper.window_id}][模拟器-{self.helper.emulator_index}]模拟器已关闭",
+                        self.helper.window_id)
+            GM.set_emulator_status(f'{self.helper.emulator_type}-{self.helper.emulator_index}', 0)
+            GM.set_window_status(self.helper.window_id, 0)
+            logger.info(f"[窗口-{self.helper.window_id}][模拟器-{self.helper.emulator_index}]任务结束",
+                        self.helper.window_id)
+            time.sleep(3)

+ 0 - 0
tools/__init__.py


BIN
tools/__pycache__/__init__.cpython-312.pyc


BIN
tools/__pycache__/dm_operate.cpython-312.pyc


BIN
tools/__pycache__/file_downloader.cpython-312.pyc


BIN
tools/__pycache__/ini_operate.cpython-312.pyc


BIN
tools/__pycache__/log.cpython-312.pyc


BIN
tools/__pycache__/thread_pool.cpython-312.pyc


BIN
tools/__pycache__/u2_operate.cpython-312.pyc


BIN
tools/__pycache__/utils.cpython-312.pyc


BIN
tools/__pycache__/wuyouip.cpython-312.pyc


+ 228 - 0
tools/dm_operate.py

@@ -0,0 +1,228 @@
+import ctypes
+import os
+import time
+
+from win32com.client import Dispatch
+
+from tools.log import logger
+
+
+class Dm:
+    def __init__(self):
+        """创建当前大漠对象。"""
+        self.dm = Dispatch('dm.dmsoft')
+
+    @staticmethod
+    def reg_free(dm_path):
+        """
+        免注册加载大漠插件的 DLL 文件。
+
+        :param dm_path: DLL 文件路径
+        """
+        try:
+            dm_dll = os.path.join(dm_path, 'dm.dll')
+            dm_reg_dll = os.path.join(dm_path, 'DmReg.dll')
+            # 确保 DLL 文件存在
+            if not os.path.exists(dm_dll):
+                return False, f"{dm_dll} not found."
+
+            # 加载 DLL 并创建大漠插件对象
+            patch = ctypes.windll.LoadLibrary(dm_reg_dll)
+            patch.SetDllPathW(dm_dll, 0)
+            dm = Dispatch('dm.dmsoft')  # 创建对象
+            ver = dm.ver()
+            return True, ver
+        except Exception as e:
+            return False, f"加载大漠插件失败: - {e}"
+
+    def __getattr__(self, name):
+        """
+        通过代理调用大漠插件的 COM 方法。
+
+        :param name: 大漠插件的方法名
+        :return: 对应的方法
+        """
+
+        def method(*args):
+            return getattr(self.dm, name)(*args)
+        return method
+
+    def register_vip(self, reg_code: str, extra_code: str):
+        """
+        注册大漠插件。
+
+        :param reg_code: 注册码
+        :param extra_code: 附加码
+        :return: 0 - 失败,1 - 成功
+        """
+        return self.dm.Reg(reg_code, extra_code)
+
+    def get_version(self):
+        """
+        获取大漠插件版本号。
+
+        :return: 版本号字符串
+        """
+        return self.dm.Ver()
+
+    def get_path(self):
+        """
+        获取全局路径
+        :return:当前设置的全局路径
+        """
+        return self.dm.GetPath()
+
+    def set_path(self, path):
+        """
+        设置大漠插件的工作路径。
+
+        :param path: 工作路径
+        :return: 0 - 失败,1 - 成功
+        """
+        return self.dm.SetPath(path)
+
+    def bind_window(self, hwnd, display="gdi", mouse="windows", keypad="windows", mode=0):
+        """
+        绑定指定的窗口。
+
+        :param hwnd: 窗口句柄
+        :param display: 显示模式
+        :param mouse: 鼠标模式
+        :param keypad: 键盘模式
+        :param mode: 绑定模式
+        :return: 0 - 失败,1 - 成功
+        """
+        return self.dm.BindWindow(hwnd, display, mouse, keypad, mode)
+
+    def unbind_window(self):
+        """
+        解除绑定窗口。
+        :return: 0 - 失败,1 - 成功
+        """
+        return self.dm.UnBindWindow()
+
+    def findPic(self, x1, y1, x2, y2, pic_name, delta_color, sim, direction):
+        """
+        查找图片。
+
+        :param x1: 起始坐标 x
+        :param y1: 起始坐标 y
+        :param x2: 结束坐标 x
+        :param y2: 结束坐标 y
+        :param pic_name: 图片文件名
+        :param delta_color: 颜色容差
+        :param sim: 相似度
+        :param direction: 查找方向
+        :return:
+        """
+        intX = -1
+        intY = -1
+        return self.dm.FindPic(x1, y1, x2, y2, pic_name, delta_color, sim, direction, intX, intY)
+
+    def findMultiColor(self, x1, y1, x2, y2, first_color, offset_color, sim=0.9, direction=0):
+        """
+        查找多个颜色。
+        long FindMultiColor(x1, y1, x2, y2,first_color,offset_color,sim, dir,intX,intY)
+        :param x1: 起始坐标 x
+        :param y1: 起始坐标 y
+        :param x2: 结束坐标 x
+        :param y2: 结束坐标 y
+        :param first_color: 颜色字符串
+        :param offset_color: 颜色容差
+        :param sim: 相似度
+        :param direction: 查找方向
+        :return: (1, intX, intY) 1表示找到 0没找到
+        """
+        intX = -1
+        intY = -1
+        ret = self.dm.FindMultiColor(x1, y1, x2, y2, first_color, offset_color, sim, direction, intX, intY)
+        return ret
+
+    def find_window(self, class_name, title_name):
+        """
+        查找窗口句柄。
+
+        :param class_name: 窗口类名
+        :param title_name: 窗口标题
+        :return: 窗口句柄
+        """
+        return self.dm.FindWindow(class_name, title_name)
+
+    def capture_screen(self, x1, y1, x2, y2, file_name):
+        """
+        屏幕截图。
+
+        :param x1: 起始坐标 x
+        :param y1: 起始坐标 y
+        :param x2: 结束坐标 x
+        :param y2: 结束坐标 y
+        :param file_name: 截图文件保存路径
+        :return: 0 - 失败,1 - 成功
+        """
+        return self.dm.Capture(x1, y1, x2, y2, file_name)
+
+    # 可以继续添加更多的大漠插件方法
+
+    def MoveTo(self, x, y):
+        self.dm.MoveTo(x, y)
+
+    # 鼠标左键点击
+    def LeftClick(self):
+        self.dm.LeftClick()
+
+    # 移动点击
+    def MoveClick(self, x, y, offset_x=0, offset_y=0):
+        self.MoveTo(x + offset_x, y + offset_y)
+        time.sleep(0.8)
+        self.LeftClick()
+
+    # 双击鼠标左键
+    def DoubleClick(self, x, y, offset_x=0, offset_y=0):
+        self.MoveTo(x + offset_x, y + offset_y)
+        time.sleep(0.5)
+        self.LeftDoubleClick()
+
+    # 左键长按
+    def LeftLongClick(self, x, y, offset_x=0, offset_y=0, duration=2000):
+        self.MoveTo(x + offset_x, y + offset_y)
+        time.sleep(0.5)
+        self.LeftDown()
+        time.sleep(duration)
+        self.LeftUp()
+
+    def DeleteText(self):
+        for i in range(20):
+            self.dm.KeyPress(8)
+            time.sleep(0.1)
+
+    def SendString(self, handle, text):
+        ds = self.dm.SendString(handle, text)
+        return ds
+
+    def Capture(self, x1, y1, x2, y2, file):
+        """抓取指定区域(x1, y1, x2, y2)的图像,保存为file(24位位图)"""
+        return self.dm.Capture(x1, y1, x2, y2, file)
+
+    def Drag(self, x1, y1, x2, y2, duration=1):
+        """
+        滑动坐标
+        :param duration: 耗时
+        :param x1: x1
+        :param y1: y1
+        :param x2: x2
+        :param y2: y2
+        :return:
+        """
+        self.dm.MoveTo(x1, y1)
+        time.sleep(0.5)
+        self.dm.LeftDown()
+        time.sleep(0.5)
+        x = x2 - x1
+        y = y2 - y1
+        for i in range(1, 10):
+            a = x1 + x / 10 * i
+            b = y1 + y / 10 * i
+            self.dm.MoveTo(a, b)
+            time.sleep(duration / 10)
+        self.dm.LeftUp()
+        time.sleep(0.5)

+ 0 - 0
tools/emulator/__init__.py


BIN
tools/emulator/__pycache__/__init__.cpython-312.pyc


BIN
tools/emulator/__pycache__/emulator.cpython-312.pyc


BIN
tools/emulator/__pycache__/ld_operate.cpython-312.pyc


BIN
tools/emulator/__pycache__/ys_operate.cpython-312.pyc


+ 236 - 0
tools/emulator/emulator.py

@@ -0,0 +1,236 @@
+import subprocess
+import time
+
+from model.custom_struct import EmulatorInfo
+from tools.utils import Utils
+
+class Emulator:
+    def __init__(self, path):
+        self.system_type = None
+        self.emulator_type = None
+        self.path = path
+
+    def start(self, index: int) -> None:
+        pass
+
+    def close(self, index: int) -> None:
+        pass
+
+    def close_all(self):
+        pass
+
+    def add(self, name: str):
+        pass
+
+    def remove(self, index: int):
+        pass
+
+    def rename(self, index: int, name: str):
+        pass
+
+    def restore(self, index: int, backup_file: str):
+        pass
+
+    def get_info(self, index: int) -> EmulatorInfo:
+        pass
+
+    def get_list(self) -> list[EmulatorInfo]:
+        pass
+
+    def is_started(self, index) -> bool:
+        pass
+
+    @staticmethod
+    def get_device_ip_by_index(index: int) -> str:
+        pass
+
+    @staticmethod
+    def get_index_by_device_ip(device_ip: str) -> int:
+        pass
+
+    def is_exists(self, index: int) -> bool:
+        pass
+
+    def is_running(self, index: int) -> bool:
+        pass
+
+    def modify(self,**kwargs):
+        pass
+
+
+    def _execute_cmd(self, cmd: str, timeout=100) -> str:
+        """
+        运行控制台命令。
+
+        :param cmd: 要执行的命令。
+        :param timeout: 超时时间(秒)。
+        :return: 控制台输出。
+        """
+        # 拼接命令
+        full_cmd = f'"{self.path}{cmd}'
+        #logger.debug(f"cmd: {full_cmd}")
+
+        try:
+            result = subprocess.run(full_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, text=True,
+                                    timeout=timeout)
+            if result.returncode >= 0:
+                return result.stdout
+            else:
+                raise RuntimeError(f"命令执行失败。错误信息: {result.stderr}")
+        except subprocess.TimeoutExpired:
+            raise RuntimeError(f"命令执行超时。超时时间: {timeout} 秒")
+        except Exception as e:
+            raise RuntimeError(f"执行命令时发生错误: {full_cmd}. 错误: {e}")
+
+    def read_emulator_file(self, index: int, file_path: str) -> str:
+        """
+        读取模拟器文本文件
+
+        :param index: 模拟器索引
+        :param file_path:模拟器文件路径
+        :return:
+        """
+        return self.shell(index, f'cat {file_path}')
+
+    def write_emulator_file(self, index: int, file_path: str, content: str) -> bool:
+        """
+        写入模拟器文本文件
+
+        :param index: 模拟器索引
+        :param file_path:模拟器文件路径
+        :param content: 写入内容
+        :return:
+        """
+        return self.shell(index, f"echo '{content}' > {file_path}") == ''
+
+    def start_and_confirm(self, index: int) -> (bool,EmulatorInfo | str):
+        try:
+            self.start(index)
+            time.sleep(10)
+            timer = int(time.time())
+            while True:
+                if  int(time.time()) - timer > 90:
+                    return False ,f'start emulator {index}: Timeout'
+                started, emulator_info = self.is_started(index)
+                if started:
+                    return True, emulator_info
+                time.sleep(5)
+        except Exception as e:
+            return False ,f'start emulator {index} error: {e}'
+
+    def close_and_confirm(self, index: int) -> (bool,str):
+        try:
+            timer = int(time.time())
+            while True:
+                self.close(index)
+                time.sleep(3)
+                if int(time.time()) - timer > 30:
+                    return False,f'closing emulator {index}: Timeout'
+                if self.get_info(index).pid == -1:
+                    return True,None
+                time.sleep(1)
+        except Exception as e:
+            return False , f'An error occurred while closing emulator {index}: {e}'
+
+
+    def start_app(self, index: int, package_name: str):
+        pass
+
+    def set_share_dir(self, index: int, share_dir: str, dir_type: int | None) -> bool:
+        pass
+
+    def get_share_dir(self, index: int) -> str:
+        pass
+
+    def get_config(self,index: int, key: str) -> str:
+        pass
+
+    def shell(self, index: int, cmd: str) -> str:
+        pass
+
+    def open_url(self, index: int, url: str):
+        self.shell(index, f'am start -a android.intent.action.VIEW -d {url}')
+
+    def get_manufacturer(self,index:int):
+        pass
+
+    def get_model(self,index:int):
+        pass
+
+    def get_android_version(self,index:int):
+        pass
+
+    def get_android_id(self, index:int):
+        pass
+
+    def get_IMEI(self, index:int):
+        pass
+
+    def get_IMSI(self, index:int):
+        pass
+
+    def get_mac(self, index:int):
+        pass
+
+    def get_sim_serial(self, index:int):
+        pass
+
+    def get_phone_number(self, index:int):
+        pass
+
+    def get_ip(self, index:int):
+        pass
+
+    def get_net_ip(self, index: int):
+        url = 'http://sjyh.kfzs.com/api/app/shuyou/game_task/getIp'
+        return self.shell(index, f'curl {url}')
+
+    def get_idle_emulator(self) -> int:
+        """
+        获取空闲模拟器的索引,如果没有符合条件的模拟器,则新建一个并等待。
+
+        :return: 模拟器索引 (int)
+        """
+
+        if not self.is_exists(0):
+            self.add('ld-0')
+            time.sleep(1)
+
+        while True:
+            emulator_list = self.get_list()
+            for emu in emulator_list:
+                # 跳过无效的模拟器索引或正在运行的模拟器
+                if emu.index == 0 or self.is_running(emu.index):
+                    continue
+
+                # 如果是 'ys' 类型的模拟器,检查系统类型是否匹配
+                if self.emulator_type == 'ys' and int(
+                        self.get_config(emu.index, 'system_type')) != self.system_type:
+                    continue
+
+                return emu.index
+            time.sleep(1)
+            #没有找到空闲模拟器,添加新的模拟器
+            self.add(f'new-{len(emulator_list)}')
+            time.sleep(1)
+
+
+    @staticmethod
+    def set_emulator_position(emulator_hwnd: int, pos_index: int, screen_width=1920, screen_height=1080, width=312,
+                              height=516,
+                              retries=5):
+        """设置模拟器位置"""
+        max_column = int(screen_width / width)
+        in_column = ((pos_index - 1) % max_column) + 1
+        in_row = int((pos_index - 1) / max_column) + 1
+        x = int(width * (in_column - 1))
+        y = int(height * (in_row - 1))
+        Utils.set_window_position_and_size(emulator_hwnd, x, y, width, height)
+        time.sleep(2)
+        for _ in range(retries):
+            current_x, current_y, current_width, current_height = Utils.get_window_position_and_size(emulator_hwnd)
+            if current_x == x and current_y == y and current_width == width and current_height == height:
+                return True
+            time.sleep(2)
+            Utils.set_window_position_and_size(emulator_hwnd, x, y, width, height)
+        raise Exception(f"Failed to set window position and size after {retries} retries.")

+ 298 - 0
tools/emulator/ld_operate.py

@@ -0,0 +1,298 @@
+import json
+import os
+
+from typing import TextIO
+from model.custom_struct import EmulatorInfo
+from tools.emulator.emulator import Emulator
+from tools.utils import Utils
+
+
+class LD(Emulator):
+    def __init__(self, system_type: int):
+        self.install_path = self.get_ld_install_dir(system_type)
+        super().__init__(self.install_path)
+        self.emulator_type = 'ld'
+        self.system_type = system_type
+
+        if not os.path.exists(self.install_path + r'\dnconsole.exe'):
+            raise ValueError(f"{self.emulator_type}-{system_type},安装路径错误")
+
+    def set_install_path(self, path: str):
+        self.install_path = path
+
+    def get_install_path(self) -> str:
+        return self.install_path
+
+    def dnc(self, cmd: str) -> str:
+        return self._execute_cmd('dnconsole.exe" ' + cmd)
+
+    def shell(self, index: int, cmd: str) -> str:
+        return self._execute_cmd(f'ld.exe" -s {index} "{cmd}"').rstrip('\n')
+
+    def start(self, index: int):
+        self.dnc(f'launch --index {index}')
+
+    def is_started(self, index) -> tuple[bool, EmulatorInfo | None]:
+        emulator_info = self.get_info(index)
+        if emulator_info :
+            return emulator_info.is_enter_android == 1 , emulator_info
+        return False, None
+
+    def close(self, index: int):
+        self.dnc(f'quit --index {index}')
+
+    def close_all(self):
+        self.dnc(f'quitall')
+
+    def add(self, name: str):
+        self.dnc(f'add --name {name}')
+
+    def remove(self, index: int):
+        self.dnc(f'remove --index {index}')
+
+    def rename(self, index: int, name: str):
+        self.dnc(f'rename --index {index} --title {name}')
+
+    def restore(self, index: int, backup_file: str):
+        self.dnc(f'restore --index {index} --file {backup_file}')
+
+    def is_exists(self, index: int) -> bool:
+        return os.path.exists(os.path.join(self.get_install_path(),'vms',f'leidian{index}'))
+
+    def is_running(self, index: int) -> bool:
+        return self.dnc(f'isrunning --index {index}') == 'running'
+
+    def modify(self,index,cpu=None, memory=None, resolution=None, manufacturer=None, model=None, pnumber=None,
+               imei=None, imsi=None, simserial=None, androidid=None, mac=None, autorotate=None, lockwindow=None, root=None):
+        """
+        修改模拟器机型
+        :param index: 模拟器索引
+        :param args:
+            [--resolution <w,h,dpi>]
+            [--cpu <1 | 2 | 3 | 4>]
+            [--memory <256 | 512 | 768 | 1024 | 1536 | 2048 | 4096 | 8192>]
+            [--manufacturer asus]
+            [--model ASUS_Z00DUO]
+            [--pnumber 13800000000]
+            [--imei <auto | 865166023949731>]
+            [--imsi <auto | 460000000000000>]
+            [--simserial <auto | 89860000000000000000>]
+            [--androidid <auto | 0123456789abcdef>]
+            [--mac <auto | 000000000000>]
+            [--autorotate <1 | 0>  自动旋转
+            [--lockwindow <1 | 0>  锁定窗口大小
+            [--root <1 | 0>
+        """
+        formatted_args = " ".join([f"--{key} {value}"  for key, value in  locals().items() if value and key not in ['index', 'self']])
+        self.dnc(f'modify --index {index} {formatted_args}')
+
+    def get_manufacturer(self, index:int):
+        return self.shell(index, 'getprop ro.product.manufacturer')
+
+    def get_model(self,index:int):
+        return self.shell(index, 'getprop ro.product.model')
+
+    def get_android_version(self,index:int):
+        return self.shell(index, 'getprop ro.build.version.release')
+
+    def get_android_id(self, index:int):
+        return self.shell(index, 'getprop phone.androidid')
+
+    def get_IMEI(self, index:int):
+        return self.shell(index, 'getprop phone.imei')
+
+    def get_IMSI(self, index:int):
+        return self.shell(index, 'getprop phone.imsi')
+
+    def get_mac(self, index:int):
+        if self.system_type == 9:
+            return self.shell(index, 'cat /sys/class/net/wlan0/address')
+        else:
+            return self.shell(index, 'cat /sys/class/net/eth0/address')
+
+    def get_resolution(self, index:int):
+        return self.shell(index, 'wm size')
+
+    def get_sim_serial(self, index:int):
+        return self.shell(index, 'getprop phone.simserial')
+
+    def get_phone_number(self, index:int):
+        return self.shell(index, 'getprop phone.number')
+
+    def get_ip(self, index:int):
+        tmp_str = self.shell(index, 'ifconfig eth0')
+        return Utils.extract_between_strings(tmp_str, 'ip ', ' ')
+
+    def read_emulator_file(self, index: int, file_path: str) -> str:
+        """
+        读取模拟器文本文件
+
+        :param index: 模拟器索引
+        :param file_path:模拟器文件路径
+        :return:
+        """
+        return super().read_emulator_file(index, file_path)
+
+    def write_emulator_file(self, index: int, file_path: str, content: str) -> bool:
+        """
+        写入模拟器文本文件
+
+        :param index: 模拟器索引
+        :param file_path:模拟器文件路径
+        :param content: 写入内容
+        :return:
+        """
+        return super().write_emulator_file(index, file_path, content)
+
+    def get_list(self) -> list[EmulatorInfo]:
+        emu_list = []
+        info_str = self.dnc('list2').strip()
+        for line in info_str.split("\n"):
+            tmp_list = line.split(',')
+            if len(tmp_list) == 7:
+                info = EmulatorInfo(
+                    index=int(tmp_list[0]), name=tmp_list[1], top_handle=int(tmp_list[2]),
+                    bind_handle=int(tmp_list[3]), is_enter_android=int(tmp_list[4]),
+                    pid=int(tmp_list[5]), vbox_pid=int(tmp_list[6]), width=None,
+                    height=None, dpi=None
+                )
+                emu_list.append(info)
+            elif len(tmp_list) == 10:
+                info = EmulatorInfo(
+                    index=int(tmp_list[0]), name=tmp_list[1], top_handle=int(tmp_list[2]),
+                    bind_handle=int(tmp_list[3]), is_enter_android=int(tmp_list[4]),
+                    pid=int(tmp_list[5]), vbox_pid=int(tmp_list[6]), width=int(tmp_list[7]),
+                    height=int(tmp_list[8]), dpi=int(tmp_list[9])
+                )
+                emu_list.append(info)
+        return emu_list
+
+    def get_info(self, index: int) -> EmulatorInfo | None:
+        info_str = self.dnc('list2').strip()
+        for line in info_str.split("\n"):
+            if line:
+                tmp_list = line.split(',')
+                if int(tmp_list[0]) == index:
+                    if len(tmp_list) == 7:
+                        return EmulatorInfo(
+                            index=int(tmp_list[0]), name=tmp_list[1], top_handle=int(tmp_list[2]),
+                            bind_handle=int(tmp_list[3]), is_enter_android=int(tmp_list[4]),
+                            pid=int(tmp_list[5]), vbox_pid=int(tmp_list[6]), width=None,
+                            height=None, dpi=None
+                        )
+                    elif len(tmp_list) == 10:
+                        return EmulatorInfo(
+                            index=int(tmp_list[0]), name=tmp_list[1], top_handle=int(tmp_list[2]),
+                            bind_handle=int(tmp_list[3]), is_enter_android=int(tmp_list[4]),
+                            pid=int(tmp_list[5]), vbox_pid=int(tmp_list[6]), width=int(tmp_list[7]),
+                            height=int(tmp_list[8]), dpi=int(tmp_list[9])
+                        )
+                    raise f"Emulator {index} not found"
+        return None
+
+    def set_share_dir(self, index: int, path: str, dir_type: int = 1) -> bool:
+        """
+        设置模拟器的共享目录。
+
+        :param index: 模拟器索引
+        :param path: 共享目录路径
+        :param dir_type: 共享目录类型(1: 图片, 2: 应用, 3: 杂项)
+        :return: 成功返回 True,失败返回 False
+        """
+
+        # 共享目录类型映射
+        key_map = {
+            1: 'statusSettings.sharedPictures',
+            2: 'statusSettings.sharedApplications',
+            3: 'statusSettings.sharedMisc'
+        }
+
+        # 获取对应的 key
+        key = key_map.get(dir_type, 'statusSettings.sharedPictures')
+
+        # 处理路径分隔符
+        path = path.replace("\\", "/")
+
+        # 拼接配置文件路径
+        config_file = os.path.join(self.install_path, 'vms', 'config', f'leidian{index}.config')
+
+        if not os.path.exists(config_file):
+            raise FileNotFoundError(f"Config file not found: {config_file}")
+
+        try:
+            # 读取配置文件
+            with open(config_file, 'r', encoding='utf-8') as file:
+                config = json.load(file)
+
+            # 设置共享目录
+            config[key] = path
+
+            # 写回配置文件
+            with open(config_file, 'w', encoding='utf-8') as file: # type: TextIO
+                json.dump(config, file, ensure_ascii=False, indent=4)
+
+            return True
+        except FileNotFoundError:
+            raise FileNotFoundError(f"Config file not found: {config_file}")
+        except json.JSONDecodeError:
+            raise json.JSONDecodeError(f"Error decoding JSON from {config_file}")
+        except Exception as e:
+            raise Exception(f"An unexpected error occurred: {e}")
+
+    def get_share_dir(self, index: int, dir_type: int = 1):
+        key_map = {
+            1: 'statusSettings.sharedPictures',
+            2: 'statusSettings.sharedApplications',
+            3: 'statusSettings.sharedMisc'
+        }
+        key = key_map.get(dir_type, 'statusSettings.sharedPictures')
+        config_file = self.install_path + f'\\vms\\config\\leidian{index}.config'
+        try:
+            with open(config_file, 'r', encoding='utf-8') as file:
+                config = json.load(file)
+            return True, config[key]
+        except FileNotFoundError:
+            raise FileNotFoundError(f"Config file not found: {config_file}")
+        except json.JSONDecodeError:
+            raise json.JSONDecodeError(f"Error decoding JSON from {config_file}")
+        except Exception as e:
+            raise Exception(f"An unexpected error occurred: {e}")
+
+    def start_app(self, index, packagename):
+        return self.dnc(f"runapp --index {index} --packagename {packagename}")
+
+    @staticmethod
+    def get_ld_install_dir(_type: int) -> str:
+        """
+        获取雷电模拟器的安装路径。
+
+        :param _type: 模拟器类型 (4 表示雷电模拟器4, 9 表示雷电模拟器9)
+        :return: 安装路径字符串,若未找到则返回空字符串
+        """
+        install_path = ""
+
+        if _type == 4:
+            install_path = Utils.get_registry_value(r"SOFTWARE\leidian\ldplayer", "InstallDir")
+            if not install_path:
+                install_path = Utils.get_install_dir_from_shortcut("雷电模拟器4.lnk").replace("dnplayer.exe", "")
+        if _type == 64:
+            install_path = Utils.get_registry_value(r"SOFTWARE\leidian\ldplayer64", "InstallDir")
+            if not install_path:
+                install_path = Utils.get_install_dir_from_shortcut("雷电模拟器64.lnk").replace("dnplayer.exe", "")
+        elif _type == 9:
+            install_path = Utils.get_registry_value(r"SOFTWARE\leidian\ldplayer9", "InstallDir")
+            if not install_path:
+                install_path = Utils.get_install_dir_from_shortcut("雷电模拟器9.lnk").replace("dnplayer.exe", "")
+
+        if not install_path:
+            raise FileNotFoundError(f"雷电模拟器-{_type}:安装路径未找到")
+
+        return install_path
+
+    @staticmethod
+    def get_device_ip_by_index(index: int):
+        return 'emulator-' + str(5554 + 2 * index)
+
+    @staticmethod
+    def get_index_by_device_ip(device_ip: str):
+        return (int(device_ip.replace('emulator-', '')) - 5554) // 2

+ 256 - 0
tools/emulator/ys_operate.py

@@ -0,0 +1,256 @@
+import os
+import threading
+import winreg
+
+from tools.emulator.emulator import Emulator
+from model.custom_struct import EmulatorInfo
+from tools.ini_operate import ConfigIni
+from tools.log import logger
+from tools.utils import Utils
+# from tools.window_operate import WindowOperate
+
+
+class YS(Emulator):
+    def __init__(self,system_type=9):
+        self.install_path = self.get_ys_install_dir()
+        super().__init__(self.install_path)
+        self.emulator_type = 'ys'
+        self.system_type = system_type
+        if not os.path.exists(self.install_path + r'\NoxConsole.exe'):
+            raise ValueError("模拟器安装路径错误")
+        self.config_path = os.path.join(os.getenv('LOCALAPPDATA'), 'Nox')
+        self._lock = threading.Lock()
+
+    def set_install_path(self, path: str):
+        self.install_path = path
+
+    def get_install_path(self) -> str:
+        return self.install_path
+
+    def noc(self, cmd: str) -> str:
+        return self._execute_cmd(r'\NoxConsole.exe" ' + cmd)
+
+    def adb(self, cmd: str,_timeout=60) -> str:
+        return self._execute_cmd(r'\nox_adb.exe" ' + cmd)
+
+    def shell(self, index: int, cmd: str) -> str:
+        return self.adb(f'-s {self.get_device_ip_by_index(index)} shell {cmd}')
+
+    def start(self, index: int):
+        super().start(index)
+        self.noc(f'launch -index:{index}')
+
+    def is_started(self, index: int) -> tuple[bool, EmulatorInfo | None]:
+        emulator_info = None
+        result = self.is_enter_android(index)
+        if result:
+            emulator_info = self.get_info(index)
+        return result, emulator_info
+
+    def is_exists(self, index: int) -> bool:
+        return os.path.exists(os.path.join(self.get_install_path(), 'BignoxVMS', f'Nox_{index}'))
+
+    def close(self, index: int):
+        super().close(index)
+        self.noc(f'quit -index:{index}')
+
+    def close_all(self):
+        self.noc(f'quitall')
+
+    def add(self, name: str):
+        self.noc(f'add -name:{name} -systemtype:{self.system_type}')
+
+    def remove(self, index: int):
+        self.noc(f'remove -index:{index}')
+
+    def rename(self, index: int, name: str):
+        self.noc(f'rename -index:{index} -title:{name}')
+
+    def restore(self, index: int, file_path: str):
+        self.noc(f'restore -index:{index} -file:{file_path}')
+
+    def is_enter_android(self, index: int) -> bool:
+        return self.shell(index, 'getprop sys.boot_completed').strip() == '1'
+
+    def read_emulator_file(self, index: int, file_path: str) -> str:
+        """
+        读取模拟器文本文件
+
+        :param index: 模拟器索引
+        :param file_path:模拟器文件路径
+        :return:
+        """
+        return super().read_emulator_file(index, file_path)
+
+    def write_emulator_file(self, index: int, file_path: str, content: str) -> bool:
+        """
+        写入模拟器文本文件
+
+        :param index: 模拟器索引
+        :param file_path:模拟器文件路径
+        :param content: 写入内容
+        :return:
+        """
+        return super().write_emulator_file(index, file_path, content)
+
+    def get_list(self) -> list[EmulatorInfo]:
+        """
+        获取模拟器列表
+
+        :return:
+        """
+
+        info_list = []
+        info_str = self.noc('list').strip()
+        for line in info_str.split("\n"):
+            tmp_list = line.split(',')
+            if len(tmp_list) >= 7:
+                info = EmulatorInfo(index=int(tmp_list[0]), name=tmp_list[2], top_handle=int(tmp_list[3], 16),
+                                bind_handle=int(tmp_list[4], 16), is_enter_android=None, pid=int(tmp_list[5]),
+                                vbox_pid=int(tmp_list[6]), width=None, height=None, dpi=None)
+                info_list.append(info)
+        return info_list
+
+    def get_info(self, index: int) -> EmulatorInfo | None:
+        info_str = self.noc('list').strip()
+        for line in info_str.split("\n"):
+            if line:
+                tmp_list = line.split(',')
+                # if int(tmp_list[0]) == index:
+                #     tmp_handle = WindowOperate.find_subwindow_by_title_and_class(int(tmp_list[3], 16),'sub','subWin')
+                #     bind_handle = WindowOperate.get_parent_window(tmp_handle)
+                #     return EmulatorInfo(index=int(tmp_list[0]), name=tmp_list[2], top_handle=int(tmp_list[3], 16),
+                #                     bind_handle=bind_handle, is_enter_android=None, pid=int(tmp_list[5]),
+                #                     vbox_pid=int(tmp_list[6]), width=None, height=None, dpi=None)
+        return None
+
+    def modify(self,index,cpu=None, memory=None, resolution=None, manufacturer=None, model=None, pnumber=None,
+               imei=None, imsi=None, simserial=None, androidid=None, mac=None, autorotate=None, lockwindow=None, root=None):
+        """
+        修改模拟器机型
+        :param index: 模拟器索引
+        :param args:
+            [--resolution <w,h,dpi>]
+            [--cpu <1 | 2 | 3 | 4>]
+            [--memory <256 | 512 | 768 | 1024 | 1536 | 2048 | 4096 | 8192>]
+            [--manufacturer asus]
+            [--model ASUS_Z00DUO]
+            [--pnumber 13800000000]
+            [--imei <auto | 865166023949731>]
+            [--imsi <auto | 460000000000000>]
+            [--simserial <auto | 89860000000000000000>]
+            [--androidid <auto | 0123456789abcdef>]
+            [--mac <auto | 000000000000>]
+            [--autorotate <1 | 0>  自动旋转
+            [--lockwindow <1 | 0>  锁定窗口大小
+            [--root <1 | 0>
+        """
+        formatted_args = " ".join([f"-{key}:{value}"  for key, value in  locals().items() if value and key not in ['index', 'self']])
+        self.noc(f'modify -index:{index} {formatted_args}')
+        self.set_config(index, 'vm_androidid', androidid)
+
+
+    def get_config(self,index:int, key:str):
+        try:
+            config_file = os.path.join(self.config_path, f'clone_Nox_{index}_conf.ini')
+            if os.path.exists(config_file):
+                config = ConfigIni(config_file)
+                return config.get('setting', key)
+            return None
+        except Exception as e:
+            logger.exception("get_config error: {e}")
+            return None
+
+    def set_config(self,index:int, key:str,value:str):
+        try:
+            config_file = os.path.join(self.config_path, f'clone_Nox_{index}_conf.ini')
+            if os.path.exists(config_file):
+                config = ConfigIni(config_file)
+                config.set('setting', key, value)
+        except Exception as e:
+            logger.exception("get_config error: {e}")
+
+    def get_manufacturer(self, index: int):
+        return self.shell(index, 'getprop persist.nox.manufacturer').strip()
+
+    def get_model(self, index: int):
+        return self.shell(index, 'getprop persist.nox.model').strip()
+
+    def get_android_version(self, index: int):
+        return self.shell(index, 'getprop ro.build.version.release').strip()
+
+    def get_android_id(self, index: int):
+        return self.shell(index, 'getprop persist.nox.androidid').strip()
+
+    def get_IMEI(self, index: int):
+        return self.shell(index, 'getprop persist.nox.modem.imei').strip()
+
+    def get_IMSI(self, index: int):
+        return self.shell(index, 'getprop persist.nox.modem.imsi').strip()
+
+    def get_mac(self, index: int):
+        return self.shell(index, 'cat /sys/class/net/wlan0/address').strip()
+
+    def get_resolution(self, index: int):
+        return self.shell(index, 'wm size')
+
+    def get_sim_serial(self, index: int):
+        return self.shell(index, 'persist.nox.modem.serial').strip()
+
+    def get_phone_number(self, index: int):
+        return self.shell(index, 'persist.nox.modem.phonumber').strip()
+
+    def get_ip(self, index: int):
+        tmp_str = self.shell(index, 'ifconfig eth0')
+        return Utils.extract_between_strings(tmp_str, 'addr:', ' ').strip()
+
+    def set_share_dir(self, index: int, path: str, dir_type=None) -> bool:
+        logger.info(f"Setting shared directory for emulator {index} to {path}")
+        try:
+            config_file = os.path.join(self.config_path, f'clone_Nox_{index}_conf.ini')
+            if os.path.exists(config_file):
+                config = ConfigIni(config_file)
+                config.set('setting', 'share_path', path)
+                return True
+            return False
+        except FileNotFoundError:
+            logger.exception("The file was not found.")
+        except Exception as e:
+            logger.exception(f"An unexpected error occurred: {e}")
+        return False
+
+    def get_share_dir(self, index: int):
+        try:
+            config_file = os.path.join(self.config_path, f'clone_Nox_{index}_conf.ini')
+            if os.path.exists(config_file):
+                config = ConfigIni(config_file)
+                share_path = config.get('setting', 'share_path')
+                if share_path:
+                    return True, share_path
+                return False, None
+            return False, None
+        except FileNotFoundError:
+            logger.exception("The file was not found.")
+        except Exception as e:
+            logger.exception(f"An unexpected error occurred: {e}")
+        return False
+
+    def start_app(self, index, packagename):
+        return self.noc(f"runapp -index:{index} -packagename:{packagename}")
+
+    @staticmethod
+    def get_ys_install_dir() -> str:
+        temp_path = Utils.get_registry_value(r"SOFTWARE\WOW6432Node\DuoDianOnline\SetupInfo", "InstallPath",winreg.HKEY_LOCAL_MACHINE)
+        if temp_path:
+            install_path = os.path.join(temp_path, "bin")
+        else:
+            install_path = Utils.get_install_dir_from_shortcut(r'\夜神模拟器.lnk').replace(r'\Nox.exe', "")
+        return install_path
+
+    @staticmethod
+    def get_device_ip_by_index(index: int):
+        return '127.0.0.1:' + str(62024 + index)
+
+    @staticmethod
+    def get_index_by_device_ip(device_ip: str):
+        return int(device_ip.replace('127.0.0.1:', '')) - 62024

+ 99 - 0
tools/file_downloader.py

@@ -0,0 +1,99 @@
+import os
+import time
+import requests
+from tools.log import logger
+from tools.utils import Utils
+
+
+class FileDownloader:
+    def __init__(self, url, local_filename, max_retries=1):
+        self.url = url
+        self.local_filename = local_filename
+        self.max_retries = max_retries
+        self.chunk_size = 8192  # 每块数据大小 (8 KB)
+        self.timer = Utils.Timer()
+        self.last_logged_time = 0
+        self.last_logged_size = 0
+        self.total_size = 0
+
+    def download(self, resume=True):
+        retry_count = 0
+        retry_delay = 2
+
+        while retry_count <= self.max_retries:
+            try:
+                return self._download(resume)
+            except requests.RequestException as e:
+                retry_count += 1
+                logger.error(f"Download failed: {e}. Retrying {retry_count}/{self.max_retries} after {retry_delay}s...")
+                time.sleep(retry_delay)
+                retry_delay *= 2  # 指数退避
+
+        raise Exception(f"Failed to download {self.url} after {self.max_retries} retries")
+
+    def _download(self, resume):
+        resume_header = {}
+        downloaded_size = 0
+
+        # 检查是否需要断点续传
+        if resume and os.path.exists(self.local_filename):
+            downloaded_size = os.path.getsize(self.local_filename)
+            resume_header = {'Range': f'bytes={downloaded_size}-'}
+            logger.info(f"Resuming download from byte: {downloaded_size}")
+        else:
+            logger.info("Starting new download.")
+
+        # 发起请求
+        with requests.get(self.url, headers=resume_header, stream=True, timeout=30) as response:
+            response.raise_for_status()
+            self.total_size = int(response.headers.get('Content-Length', 0)) + downloaded_size
+
+            with open(self.local_filename, 'ab') as f:
+                start_time = time.time()
+                previous_time = start_time
+
+                for chunk in response.iter_content(chunk_size=self.chunk_size):
+                    if not chunk:  # 忽略空数据块
+                        continue
+
+                    f.write(chunk)
+                    downloaded_size += len(chunk)
+
+                    current_time = time.time()
+                    time_elapsed = int(current_time - previous_time)
+
+                    # 每隔 1 秒更新日志
+                    if time_elapsed >= 1 or self.timer.timer("download", 5):
+                        self._log_progress(downloaded_size, self.total_size, start_time, previous_time)
+                        previous_time = current_time
+
+        logger.info(f"Download completed: {self.local_filename}")
+
+    def _log_progress(self, downloaded_size, total_size, start_time, previous_time):
+        """
+        输出下载进度日志,包含当前进度和下载速度。
+
+        :param downloaded_size: 已下载大小(字节)
+        :param total_size: 文件总大小(字节)
+        :param start_time: 下载开始时间
+        :param previous_time: 上一次记录日志的时间
+        """
+        percent = (downloaded_size / total_size) * 100
+        elapsed_time = time.time() - start_time
+        interval_time = time.time() - previous_time
+
+        # 计算平均速度和瞬时速度
+        average_speed = downloaded_size / elapsed_time if elapsed_time > 0 else 0
+        instant_speed = (downloaded_size - self.last_logged_size) / interval_time if interval_time > 0 else 0
+
+        # 转换为 MB/s
+        average_speed_mb = average_speed / 1024 / 1024
+        instant_speed_mb = instant_speed / 1024 / 1024
+
+        logger.info(f"{os.path.basename(self.local_filename)} - Downloaded: "
+                    f"{downloaded_size / 1024 / 1024:.1f}MB of {total_size / 1024 / 1024:.1f}MB "
+                    f"({percent:.2f}%) | {instant_speed_mb:.2f} MB/s")
+
+        # 更新记录的时间和大小
+        self.last_logged_time = time.time()
+        self.last_logged_size = downloaded_size

+ 54 - 0
tools/ini_operate.py

@@ -0,0 +1,54 @@
+import configparser
+import os
+from threading import Lock
+
+
+class ConfigIni:
+    def __init__(self, filename):
+        self.filename = filename
+
+        if not os.path.exists(self.filename):
+            # 如果不存在,创建文件并写入初始内容
+            with open(self.filename, 'w', encoding='utf-8') as config_file:
+                config_file.write("# 示例配置文件\n")
+
+        self.config = configparser.ConfigParser()
+        self.lock = Lock()
+        self.load()
+
+    def load(self):
+        with open(self.filename, 'r', encoding='utf-8') as configfile:
+            self.config.read_file(configfile)
+
+    def save(self):
+        with open(self.filename, 'w', encoding='utf-8') as configfile:
+            self.config.write(configfile)
+
+    def get(self, section, key, default=""):
+        with self.lock:
+            try:
+                if not self.config.has_option(section, key):
+                    return default
+                return self.config.get(section, key)
+            except (configparser.NoSectionError, configparser.NoOptionError):
+                return default
+
+    def get_int(self, section, key, default="0"):
+        return int(self.get(section, key, default))
+
+    def set(self, section, key, value):
+        with self.lock:
+            if not self.config.has_section(section):
+                self.config.add_section(section)
+            self.config.set(section, key, value)
+            self.save()
+
+    def set_int(self, section, key, value):
+        self.set(section, key, str(value))
+
+    def get_section_keys(self, section):
+        with self.lock:
+            if self.config.has_section(section):
+                return dict(self.config.items(section))
+            else:
+                return {}

+ 107 - 0
tools/log.py

@@ -0,0 +1,107 @@
+import logging
+import os
+import queue
+import threading
+import time
+
+from logging.handlers import RotatingFileHandler
+from colorama import init
+
+
+
+class LogColors:
+    colors = [
+        "\033[0m",    # RESET
+        "\033[36m",   # CYAN
+        "\033[92m",   # LIGHT_GREEN
+        "\033[94m",   # LIGHT_BLUE
+        "\033[95m",   # LIGHT_MAGENTA
+        "\033[96m",   # LIGHT_CYAN
+        "\033[90m",   # DARK_GRAY
+        "\033[93m",   # LIGHT_YELLOW
+        "\033[32m",   # GREEN
+        "\033[33m",   # YELLOW
+        "\033[34m",   # BLUE
+        "\033[35m",   # MAGENTA
+        "\033[91m",   # LIGHT_RED
+        "\033[37m",   # WHITE
+        "\033[31m",   # RED
+    ]
+
+    @classmethod
+    def get_color(cls, index):
+        return cls.colors[index]
+
+class Logger:
+    def __init__(self, log_file=r'Log\app.log', file_log_level=logging.DEBUG, console_log_level=logging.DEBUG):
+        init(autoreset=True)  # 初始化colorama并自动重置颜色
+
+        # 创建日志文件目录(如果不存在)
+        log_dir = os.path.dirname(log_file)
+        if not os.path.exists(log_dir):
+            os.makedirs(log_dir)
+
+        # 如果日志文件不存在,创建一个新的
+        if not os.path.exists(log_file):
+            with open(log_file, 'a'):
+                pass
+
+        # 创建Logger对象
+        self.logger = logging.getLogger('Logger')
+        self.logger.setLevel(logging.DEBUG)
+
+        # 创建文件处理器,并设置日志级别和格式
+        max_bytes = 5 * 1024 * 1024  # 最大文件大小 5MB
+        backup_count = 5  # 保留的日志文件数量
+        file_handler = RotatingFileHandler(log_file, maxBytes=max_bytes, backupCount=backup_count)
+        file_handler.setLevel(file_log_level)
+        formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
+        file_handler.setFormatter(formatter)
+        self.logger.addHandler(file_handler)
+
+        # 创建控制台处理器,并设置日志级别和格式
+        console_handler = logging.StreamHandler()
+        console_handler.setLevel(console_log_level)
+        console_handler.setFormatter(formatter)
+        self.logger.addHandler(console_handler)
+
+        self.log_queue = queue.Queue()
+        self.thread = threading.Thread(target=self._log_worker)
+        self.thread.start()
+
+    def get_logger(self):
+        return self.logger
+
+    def _log_worker(self):
+        while True:
+            try:
+                log_action = self.log_queue.get()
+                if log_action is None:
+                    break
+                log_action()  # 执行存储的 lambda 函数
+            except Exception as e:
+                self.logger.error(f"Error in log worker: {e}")
+            time.sleep(0.01)
+
+    def debug(self, message):
+        self.log_queue.put(lambda: self.logger.debug(f"{LogColors.get_color(8)}{message}{LogColors.get_color(0)}"))  # LIGHT_MAGENTA
+
+    def info(self, message, color_index=13):
+        self.log_queue.put(lambda: self.logger.info(f"{LogColors.get_color(color_index)}{message}{LogColors.get_color(0)}"))
+
+    def warning(self, message):
+        self.log_queue.put(lambda: self.logger.warning(f"{LogColors.get_color(9)}{message}{LogColors.get_color(0)}"))  # YELLOW
+
+    def error(self, message):
+        self.log_queue.put(lambda: self.logger.error(f"{LogColors.get_color(14)}{message}{LogColors.get_color(0)}"))  # RED
+
+    def exception(self, message):
+        self.log_queue.put(lambda: self.logger.exception(f"{LogColors.get_color(14)}{message}{LogColors.get_color(0)}"))  # RED
+
+    def critical(self, message):
+        self.log_queue.put(lambda: self.logger.critical(f"{LogColors.get_color(12)}{message}{LogColors.get_color(0)}"))  # MAGENTA
+
+
+# 使用示例
+logger = Logger(log_file=r'Log\app.log', file_log_level=logging.DEBUG, console_log_level=logging.DEBUG)
+

+ 27 - 0
tools/thread_pool.py

@@ -0,0 +1,27 @@
+from concurrent.futures import ThreadPoolExecutor
+import threading
+
+
+class MyThreadPoolExecutor(ThreadPoolExecutor):
+    def __init__(self, max_workers=None, thread_name_prefix='', *args, **kwargs):
+        super().__init__(max_workers, thread_name_prefix, *args, **kwargs)
+        self._idle_workers = max_workers
+        self._lock = threading.Lock()
+
+    def submit(self, fn, *args, **kwargs):
+        with self._lock:
+            self._idle_workers -= 1
+        future = super().submit(self._wrap_task, fn, *args, **kwargs)
+        return future
+
+    def _wrap_task(self, fn, *args, **kwargs):
+        try:
+            result = fn(*args, **kwargs)
+        finally:
+            with self._lock:
+                self._idle_workers += 1
+        return result
+
+    def get_idle_worker_count(self):
+        with self._lock:
+            return self._idle_workers

+ 233 - 0
tools/upload_log.py

@@ -0,0 +1,233 @@
+import time
+from dataclasses import dataclass
+
+from net.task_api import task_api
+
+PULL_ACCOUNT_OK = 4101099
+PULL_ACCOUNT_fail = 4101001
+START_SIMULATOR_OK = 4301099
+START_GAME_DEFAULT_OK = 4501099
+LOGING_GAME_OLD_OK = 4606099
+MAIN_DEFAULT_OK = 4701099
+FEE_XMY_OK = 4801099
+START_SIMULATOR_FAIL = 4301001
+START_GAME_DEFAULT_FAIL = 4501001
+LOGING_GAME_OLD_FAIL = 4606001
+MAIN_DEFAULT_FAIL = 4701099
+FEE_XMY_FAIL = 4801099
+
+@dataclass
+class UploadLog:
+    simulator_ip: str  # 模拟器ip
+    simulator_mac: str  # 模拟器mac
+    pc_code: str  # 电脑编号
+    pc_ip: str  # 电脑IP
+    pc_mac: str  # 电脑mac
+    device_id: str  # 手机_aid
+    account: str  # 游戏账号
+    account_type: int  # 游戏_账号类型
+    pwd: str  # 游戏账号密码
+    game_id: int  # 游戏编号
+    coding: int  # 错误码
+    log_uuid: str
+    operator: str  # 负责人
+    remarks: str  # 备注
+    task_type: int  # 任务类型新增0活跃1
+    script_type: int  # 脚本类型
+    simulator_code: str  # 模拟器编号
+    device_manufacturer: str  # 手机_厂商
+    device_model: str  # 手机_型号
+    device_imei: str  # 手机_imei
+    device_sdk: str  # 手机_sdk
+    device_mac: str  # 手机_mac
+    device_number: str  # 手机_号码
+    script_device_id: str  # 脚本中上传
+    err: str  # 0或不传表示没有异常,其他表示异常,脚本端自定义
+    simulator_ip_city: str  # 手机_IP_城市
+
+    # 定义一个方法,将对象转换为字典
+    def to_dict(self):
+        return {
+            'simulator_ip': self.simulator_ip,
+            'simulator_mac': self.simulator_mac,
+            'pc_code': self.pc_code,
+            'pc_ip': self.pc_ip,
+            'pc_mac': self.pc_mac,
+            'device_id': self.device_id,
+            'account': self.account,
+            'account_type': self.account_type,
+            'pwd': self.pwd,
+            'game_id': self.game_id,
+            'coding': self.coding,
+            'log_uuid': self.log_uuid,
+            'operator': self.operator,
+            'remarks': self.remarks,
+            'task_type': self.task_type,
+            'script_type': self.script_type,
+            'simulator_code': self.simulator_code,
+            'device_manufacturer': self.device_manufacturer,
+            'device_model': self.device_model,
+            'device_imei': self.device_imei,
+            'device_sdk': self.device_sdk,
+            'device_mac': self.device_mac,
+            'device_number': self.device_number,
+            'script_device_id': self.script_device_id,
+            'err': self.err,
+            'simulator_ip_city': self.simulator_ip_city
+        }
+
+
+def log_init(uuid: str):
+    return UploadLog(
+        simulator_ip='',
+        simulator_mac='',
+        pc_code='',
+        pc_ip='',
+        pc_mac='',
+        device_id='',
+        account='',
+        account_type=1,
+        pwd='',
+        game_id=5001,
+        coding=PULL_ACCOUNT_OK,
+        log_uuid=uuid,
+        operator='',
+        remarks='',
+        task_type=1,
+        script_type=1,
+        simulator_code='',
+        device_manufacturer='',
+        device_model='',
+        device_imei='',
+        device_sdk='',
+        device_mac='',
+        device_number='',
+        script_device_id='',
+        err='',
+        simulator_ip_city='',
+    )
+
+
+def log_uuid(account: str) -> str:
+    timestamp = int(time.time() * 1000)
+    timestamp_str = str(timestamp)
+    result = timestamp_str + "_" + account
+    return result
+
+
+
+
+def _get_account_type(account_type: str):
+    # 0:QQ 1:小绵羊 2:微信 3:魅族
+    if account_type == "qq":
+        return 0
+    elif account_type == "小绵羊":
+        return 1
+    elif account_type == "微信":
+        return 2
+    elif account_type == "魅族":
+        return 3
+    else:
+        return 1
+
+
+class LogInfo:
+
+    def __init__(self, uuid: str):
+        # uuid = log_uuid(account)
+        self.upload_log = log_init(uuid)
+
+    def set_account_info(self, game_id: int, account_type: str, pwd: str, account: str, task_type: int):
+        try:
+            self.upload_log.game_id = game_id
+            self.upload_log.account_type = _get_account_type(account_type)
+            self.upload_log.pwd = pwd
+            self.upload_log.account = account
+            self.upload_log.task_type = task_type
+            return True, ""
+        except Exception as e:
+            return False, str(e)
+
+    def set_device_info(self, device_id: str, device_manufacturer: str, device_model: str, device_imei: str,
+                        device_sdk: str, device_mac: str, device_number: str, script_device_id: str):
+        try:
+            self.upload_log.device_id = device_id
+            self.upload_log.device_manufacturer = device_manufacturer
+            self.upload_log.device_model = device_model
+            self.upload_log.device_imei = device_imei
+            self.upload_log.device_sdk = device_sdk
+            self.upload_log.device_mac = device_mac
+            self.upload_log.device_number = device_number
+            self.upload_log.script_device_id = script_device_id
+            return True, ""
+        except Exception as e:
+            return False, str(e)
+
+    def set_pc_info(self, pc_code: str, pc_ip: str, pc_mac: str, operator: str):
+        try:
+            self.upload_log.pc_code = pc_code
+            self.upload_log.pc_ip = pc_ip
+            self.upload_log.pc_mac = pc_mac
+            self.upload_log.operator = operator
+            return True, ""
+        except Exception as e:
+            return False, str(e)
+
+    def set_simulator_info(self, simulator_ip: str, simulator_mac: str, simulator_code: str, simulator_ip_city: str):
+        self.upload_log.simulator_ip = simulator_ip
+        self.upload_log.simulator_mac = simulator_mac
+        self.upload_log.simulator_code = simulator_code
+        self.upload_log.simulator_ip_city = simulator_ip_city
+
+    def set_coding(self, coding: int, remarks: str):
+        self.upload_log.coding = coding
+        self.upload_log.remarks = remarks
+
+    def set_err(self, err: str):
+        self.upload_log.err = err
+
+    def get_upload_log(self):
+        return self.upload_log
+
+    def upload_longin_log(self, status: int):
+        if status == 1:
+            self.upload_log.coding = LOGING_GAME_OLD_OK
+        else:
+            self.upload_log.coding = LOGING_GAME_OLD_FAIL
+        task_api.upload_log_to_server(self.upload_log)
+
+    def upload_main_log(self, status: int):
+        if status == 1:
+            self.upload_log.coding = MAIN_DEFAULT_OK
+        else:
+            self.upload_log.coding = MAIN_DEFAULT_FAIL
+        task_api.upload_log_to_server(self.upload_log)
+
+    def upload_fee_log(self, status: int):
+        if status == 1:
+            self.upload_log.coding = FEE_XMY_OK
+        else:
+            self.upload_log.coding = FEE_XMY_FAIL
+        task_api.upload_log_to_server(self.upload_log)
+
+    def upload_start_simulator_log(self, status: int):
+        if status == 1:
+            self.upload_log.coding = START_SIMULATOR_OK
+        else:
+            self.upload_log.coding = START_SIMULATOR_FAIL
+        ret = task_api.upload_log_to_server(self.upload_log)
+        return ret
+
+    def upload_start_game_log(self, status: int):
+        if status == 1:
+            self.upload_log.coding = START_GAME_DEFAULT_OK
+        else:
+            self.upload_log.coding = START_GAME_DEFAULT_FAIL
+        task_api.upload_log_to_server(self.upload_log)
+
+    def upload_pull_account_log(self, status: int):
+        if status == 1:
+            self.upload_log.coding = PULL_ACCOUNT_OK
+        else:
+            self.upload_log.coding = PULL_ACCOUNT_fail
+        task_api.upload_log_to_server(self.upload_log)

+ 230 - 0
tools/utils.py

@@ -0,0 +1,230 @@
+import os
+import sys
+import time
+import uuid
+import winreg
+from ctypes.wintypes import RECT
+
+import psutil
+import win32gui
+import winshell
+import ctypes
+import pythoncom
+import win32com.client
+
+import win32api
+from comtypes.typeinfo import LoadTypeLibEx
+from ctypes import OleDLL, c_void_p, byref, WINFUNCTYPE
+from uuid import UUID
+from tools.log import logger
+
+
+class Utils:
+    @staticmethod
+    def extract_between_strings(content, start_tag, end_tag):
+        """
+        提取content中从start_tag到end_tag之间的字符串。
+
+        参数:
+            content (str): 包含要提取内容的字符串。
+            start_tag (str): 起始标记字符串。
+            end_tag (str): 结束标记字符串。
+
+        返回:
+            str: start_tag和end_tag之间的内容。如果未找到匹配内容,则返回空字符串。
+        """
+        try:
+            # 查找起始标记的位置
+            start_index = content.find(start_tag)
+            if start_index == -1:
+                return ""
+            # 计算实际内容的起始位置
+            start_index += len(start_tag)
+            # 查找结束标记的位置
+            end_index = content.find(end_tag, start_index)
+            if end_index == -1:
+                return ""
+            # 提取中间的内容
+            return content[start_index:end_index]
+        except Exception as e:
+            # 错误处理,返回空字符串或抛出异常
+            return ""
+
+    @staticmethod
+    def get_registry_value(key_path, value_name,key=winreg.HKEY_CURRENT_USER):
+        try:
+            with winreg.OpenKey(key, key_path) as key:
+                value, _ = winreg.QueryValueEx(key, value_name)
+                return value
+        except FileNotFoundError:
+            return None
+
+    @staticmethod
+    def get_install_dir_from_shortcut(shortcut: str):
+        try:
+            with winreg.OpenKey(winreg.HKEY_CURRENT_USER,
+                                r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders") as key:
+                value, _ = winreg.QueryValueEx(key, "Desktop")
+            shortcut = winshell.shortcut(os.path.join(value, shortcut))
+            return shortcut.path
+        except Exception as e:
+            print(f"Error parsing shortcut {value + shortcut}: {e}")
+            return None
+
+    @staticmethod
+    def makelong(low, high):
+        return low | (high << 16)
+
+    @staticmethod
+    def set_window_position_and_size(hwnd, x, y, width, height):
+        """
+        设置指定窗口句柄的窗口的位置和大小。
+
+        :param hwnd: 窗口句柄
+        :param y: 窗口的新 y 坐标
+        :param x: 窗口的新 x 坐标
+        :param width: 窗口的新宽度
+        :param height: 窗口的新高度
+        """
+        if hwnd:
+            win32gui.MoveWindow(hwnd, x, y, width, height, True)
+        else:
+            logger.error("无效的窗口句柄")
+            print("无效的窗口句柄")
+
+    @staticmethod
+    def get_window_position_and_size(hwnd):
+        """
+        获取指定窗口句柄的窗口的位置和大小。
+
+        :param hwnd: 窗口句柄
+        :return: 窗口的位置和大小
+        """
+        if hwnd:
+            rect = win32gui.GetWindowRect(hwnd)
+            x, y, width, height = rect
+            return x, y, width-x, height-y
+        else:
+            raise Exception("无效的窗口句柄")
+
+    @staticmethod
+    def get_mac_address():
+        mac_address = ':'.join(hex(uuid.getnode())[2:].zfill(12)[i:i + 2] for i in range(0, 12, 2))
+        return mac_address
+
+    @staticmethod
+    def is_admin():
+        try:
+            return ctypes.windll.shell32.IsUserAnAdmin()
+        except Exception:
+            return False
+
+    @staticmethod
+    def run_as_admin():
+        # 以管理员权限重新运行当前脚本
+        script = sys.argv[0]
+        params = ' '.join([f'"{param}"' for param in sys.argv[1:]])
+        try:
+            ctypes.windll.shell32.ShellExecuteW(None, "runas", sys.executable, f'"{script}" {params}', None, 1)
+        except Exception as e:
+            print(f"Failed to elevate privileges: {e}")
+            sys.exit(1)
+
+    class TimedLock:
+        def __init__(self, lock, timeout):
+            self.lock = lock
+            self.timeout = timeout
+
+        def __enter__(self):
+            """尝试获取锁,如果在超时内无法获取,抛出 TimeoutError"""
+            if not self.lock.acquire(timeout=self.timeout):
+                raise TimeoutError(f"Could not acquire lock within {self.timeout} seconds.")
+            return self
+
+        def __exit__(self, exc_type, exc_val, exc_tb):
+            """确保锁被释放"""
+            self.lock.release()
+
+    @staticmethod
+    class Timer:
+        def __init__(self):
+            self._time_signs = {}
+
+        def time_sign(self, sign):
+            self._time_signs[sign] = time.time()
+
+        def timer(self, sign, t):
+            if sign not in self._time_signs:
+                self._time_signs[sign] = time.time()
+                return True
+            if time.time() - self._time_signs[sign] > t:
+                self._time_signs[sign] = time.time()
+                return True
+            return False
+
+    @staticmethod
+    def create_instance_from_com_dll(dll_path):
+        """从 DLL 中创建 COM 对象实例"""
+        com_classfactory = c_void_p(0)
+        try:
+            # 加载 DLL 和类型库
+            dll = OleDLL(dll_path)
+            typelib = LoadTypeLibEx(dll_path)
+
+            # 获取 COM 对象的 CLSID
+            co_class_info = typelib.GetTypeInfo(1)
+            co_class_attr = co_class_info.GetTypeAttr()
+            clsid = UUID(str(co_class_attr.guid)).bytes_le
+
+            # 获取 IClassFactory 接口
+            iclassfactory = UUID(str(pythoncom.IID_IClassFactory)).bytes_le
+            dll.DllGetClassObject(clsid, iclassfactory, byref(com_classfactory))
+
+            # 从 IClassFactory 接口创建 COM 对象实例
+            class_factory = pythoncom.ObjectFromAddress(com_classfactory.value, pythoncom.IID_IClassFactory)
+            iptr = class_factory.CreateInstance(None, pythoncom.IID_IDispatch)
+            return win32com.client.Dispatch(iptr, resultCLSID=None)
+
+        except Exception as e:
+            raise RuntimeError(f"Failed to create COM object from DLL: {e}")
+
+        finally:
+            # 释放 IClassFactory 接口
+            if com_classfactory:
+                IUnknown_Release = WINFUNCTYPE(c_void_p)(2, 'Release', (), pythoncom.IID_IUnknown)
+                IUnknown_Release(com_classfactory)
+
+    @staticmethod
+    def draw_text_on_window(hwnd, text):
+        print(text)
+        # 获取窗口的设备上下文
+        hdc = win32gui.GetDC(hwnd)
+
+        # 设置文本颜色
+        color = win32api.RGB(255, 0, 0)  # 红色
+        ctypes.windll.gdi32.SetTextColor(hdc, color)
+        ctypes.windll.gdi32.SetBkMode(hdc, 1)  # 透明背景
+
+        # 在指定位置绘制文本
+        x, y = 50, 50  # 文本的起始坐标
+        ctypes.windll.gdi32.ExtTextOutW(hdc, x, y, 0, None, text, len(text), None)
+
+        # 释放设备上下文
+        win32gui.ReleaseDC(hwnd, hdc)
+
+    @staticmethod
+    def kill_process(pid):
+        try:
+            p = psutil.Process(pid)
+            p.kill()  # 强制终止进程
+
+            # 可选:等待进程结束,设置超时
+            p.wait(timeout=3)  # 等待最多3秒
+        except psutil.NoSuchProcess:
+            logger.warning(f"Process {pid} does not exist.")
+        except psutil.AccessDenied:
+            logger.warning(f"Access denied to terminate process {pid}.")
+        except psutil.TimeoutExpired:
+            logger.warning(f"Process {pid} could not be terminated in time.")
+        except Exception as e:
+            logger.exception(f"Error: {e}")

+ 185 - 0
tools/wuyouip.py

@@ -0,0 +1,185 @@
+import time
+from typing import Any
+
+import requests
+
+from net.task_api import task_api
+from tools.log import logger
+
+
+class WyIP:
+    def __init__(self):
+        self.token = None
+        self.local_node_list = []
+        self.node_list = []
+        self._initialize_node_list()
+        self._initialize_local_node_list()
+        self.error_count = 0
+
+    def _initialize_node_list(self):
+        t = 0
+        while True:
+            result, _list = self.get_node_list()
+            if not result:
+                t += 1
+                if t == 10:
+                    task_api.notify('无忧IP取账号节点列表失败')
+                logger.error('无忧IP取账号节点列表失败,5秒后重新获取')
+                time.sleep(5)
+                continue
+            else:
+                self.node_list = _list
+                break
+
+    def _initialize_local_node_list(self):
+        t = 0
+        while True:
+            self.local_node_list = self.get_local_node_list()
+            if len(self.local_node_list) < 1:
+                t += 1
+                if t == 10:
+                    task_api.notify('无忧IP取本地节点列表失败')
+                logger.error('无忧IP取本地节点列表失败,10秒后重新获取')
+                time.sleep(10)
+                continue
+            else:
+                break
+
+    def set_wy_token(self):
+        try:
+            result = requests.get('http://assist.qiming321.cn:8888/loging/getWuYToken',timeout=10)
+            if result.status_code == 200:
+                data = result.json()
+                if data['code'] == 0:
+                    self.token = data['data']['token']
+                    return
+            raise Exception('获取token失败')
+        except Exception as e:
+            raise Exception(e)
+
+    def get_node_list(self) -> tuple[bool, str] | tuple[bool, list[Any]]:
+        self.set_wy_token()
+        if not self.token:
+            return False, 'token为空'
+
+        try:
+            node_list = []
+            for i in range(1, 5):
+                header = {
+                    'content-type': 'application/x-www-form-urlencoded',
+                    'cookie': self.token,
+                }
+                data = f'rows=500&page={i}&NodeWheres%5BType%5D=&NodeWheres%5BNodeType%5D=all&NodeWheres%5BDeviceType%5D=all&NodeWheres%5BItemNumber%5D=all&NodeWheres%5BRegionState%5D=all&NodeWheres%5BRegionNumber%5D=all&NodeWheres%5BNodeIsUsed%5D=all&NodeWheres%5BGroupNumber%5D=all&NodeWheres%5BNodePrice%5D=all&NodeWheres%5BIsGlobalDynamic%5D=all&TimeDesc='
+                result = requests.post('https://user.wuyouip.com/Node/Home/ListUserNodewData', data=data,
+                                       headers=header,timeout=10)
+                if result.status_code == 200:
+                    text = result.text
+                    if 'DOCTYPE' in text:
+                        return False, 'get_node_list:登录已过期,请重新登录'
+                    data = result.json()
+                    if data['success']:
+                        node_list.extend(data['rows'])
+                    if data['total'] <= 500 * i:
+                        return True, node_list
+            return False, '获取账号节点列表失败'
+        except Exception as e:
+            return False, str(e)
+
+    @staticmethod
+    def get_local_node_list() -> list[Any]:
+        try:
+            result = requests.get('http://127.0.0.1:54321/api/v1/getNodeBindDataList', timeout=10)
+            if result.status_code == 200:
+                data = result.json()
+                if data['message'] == '':
+                    return data['data']
+            return []
+        except Exception as e:
+            return []
+
+    def switch_node_area(self, node_id: str, price_id: str, region: str) -> tuple[bool, str]:
+        header = {
+            'content-type': 'application/x-www-form-urlencoded',
+            'cookie': self.token
+        }
+        data = f'id={node_id}&pId={price_id}&regions={region}'
+        try:
+            result = requests.post('https://user.wuyouip.com/Node/Home/SwitchNodeAreaV2', data=data, headers=header, timeout=10)
+            if result.status_code == 200:
+                text = result.text
+                logger.info(f'{node_id}-{region}-{price_id}-切换地区接口返回:{text}')
+                if 'DOCTYPE' in text:
+                    return False, 'switch_node_area:登录已过期,请重新登录'
+                data = result.json()
+                if data['success']:
+                    return True, 'switch_node_area:切换成功'
+                if data['error']:
+                    return False, f'switch_node_area:{data["message"]}'
+            return False, '切换失败'
+        except Exception as e:
+            return False, f'switch_node_area:{e}'
+
+    def get_node_id_by_emulator_index(self, emulator_index: int) -> tuple[bool, str] | tuple[
+        bool, tuple[Any, Any | None]]:
+        node_order = None
+        for node in self.local_node_list:
+            for order in node['simulatorOrders']:
+                if int(order) == emulator_index:
+                    node_order = node['nodeOrder']
+        if not node_order:
+            return False, 'get_node_id_by_emulator_index:未找到对应的本地节点'
+        for node in self.node_list:
+            if node['UserNodeBuyOrder'] == node_order:
+                return True, (node['NodeId'], node_order)
+        return False, 'get_node_id_by_emulator_index:未找到对应的账号节点'
+
+    def switch_emulator_area(self, emulator_index: int, price_id: str, region: str) -> tuple[bool, str]:
+        if not self.token:
+            self.error_count += 1
+            return False, '未登录'
+        if not self.node_list:
+            self._initialize_node_list()
+        if not self.local_node_list:
+            self._initialize_local_node_list()
+
+        result, tup = self.get_node_id_by_emulator_index(emulator_index)
+        if not result:
+            self.error_count += 1
+            if self.error_count == 5:
+                task_api.notify(f'模拟器{emulator_index}切换地区失败,请检查')
+            return result, tup
+
+        result, message = self.switch_node_area(tup[0], price_id, region)
+        if not result:
+            self.error_count += 1
+            if self.error_count == 5:
+                task_api.notify(f'模拟器{emulator_index}切换地区失败,请检查')
+            return result, message
+        self.error_count = 0
+        time.sleep(5)
+        result, message = self.update_node(tup[1])
+        return True, message
+
+    def queryProcessProxyRegion(self,pid) -> tuple[bool, str]:
+        try:
+            result = requests.get(f'http://127.0.0.1:54321/api/v1/queryProcessIsProxy?pid={pid}', timeout=10)
+            if result.status_code == 200:
+                data = result.json()
+                if data['result'] == 0:
+                    return True, data['data']['region']
+                return False, f'queryProcessIsProxy 错误:{data["message"]}'
+            return False, f'queryProcessIsProxy 错误:{result.status_code}'
+        except Exception as e:
+            return False, f'queryProcessIsProxy 错误:{str(e)}'
+
+    def update_node(self, node_order) -> tuple[bool, Any]:
+        try:
+            result = requests.get(f'http://127.0.0.1:54321/api/v1/nodeRequestSwitch?nodeOrder={node_order}', timeout=10)
+            if result.status_code == 200:
+                data = result.json()
+                if data['result'] == 0:
+                    return True, data['data']
+                return False, f'update_node 错误:{data["message"]}'
+            return False, f'update_node 错误:{result.status_code}'
+        except Exception as e:
+            return False, f'update_node 错误:{str(e)}'

+ 0 - 0
update/__init__.py


BIN
update/__pycache__/__init__.cpython-312.pyc


BIN
update/__pycache__/update.cpython-312.pyc


+ 141 - 0
update/update.py

@@ -0,0 +1,141 @@
+import os
+import queue
+import shutil
+import threading
+import time
+import requests
+
+from model.custom_struct import GameConfig, UpdateInfo
+from net.task_api import task_api
+from tools.file_downloader import FileDownloader
+from tools.ini_operate import ConfigIni
+from tools.log import logger
+
+
+class Updater:
+
+    def __init__(self, script_path, image_path):
+        self.url = task_api.log_url
+        self.update_config = self._initialize_update_config()
+        self.script_path = script_path
+        self.image_path = image_path
+        self.lock = threading.Lock()
+        self.updating_games = {}
+        self.download_queue = queue.Queue()
+
+    def set_updating_status(self, game_id, _type, status):
+        """设置更新状态。"""
+        with self.lock:
+            # 使用 setdefault 初始化 game_id 和 _type
+            self.updating_games.setdefault(game_id, {}).setdefault(_type, None)
+
+            # 更新状态
+            self.updating_games[game_id][_type] = status
+
+    def get_is_updating(self, game_id):
+        with self.lock:
+            tmp_dict = self.updating_games.get(game_id, {})
+            return tmp_dict.get('script') == 1 or tmp_dict.get('image') == 1
+
+    @staticmethod
+    def _initialize_update_config():
+        """初始化并返回配置文件对象。"""
+        config_dir = 'config'
+        os.makedirs(config_dir, exist_ok=True)
+        return ConfigIni(os.path.join(os.getcwd(), config_dir, 'update.ini'))
+
+    def check_updates(self, device_info, game_list):
+        for game in game_list:
+            game_info = GameConfig.dict_to_GameConfig(game)
+            if self.get_is_updating(game_info.task_id):  # 检查是否正在更新
+                return
+            if device_info.check_script_update == '1':
+                self._check_script_update(game_info)
+            if device_info.check_image_update == '1':
+                self._check_image_update(game_info)
+
+    def _check_script_update(self, game_info):
+        local_script_md5 = self.update_config.get(game_info.task_id, 'script_md5')
+        url = f'{self.url}/gameTask/downloadFile?taskId={game_info.task_id}&md5String={local_script_md5}'
+        try:
+            result = requests.get(url, timeout=10)
+            if result.status_code == 200:
+                json_obj = result.json()
+                if json_obj['code'] == 0 and json_obj['data']['flag']:
+                    remote_md5 = json_obj['data']['md5_string']
+                    download_url = json_obj['data']['url']
+                    update_info = UpdateInfo(
+                        game_id=game_info.task_id,
+                        md5=remote_md5,
+                        download_url=download_url,
+                        file_type='script',
+                        save_path=os.path.join(self.script_path, game_info.script)
+                    )
+                    self.download_queue.put(update_info)
+                    self.set_updating_status(game_info.task_id, 'script', 1)  # 标记为正在更新
+                    return True
+            return False
+        except Exception as e:
+            return False
+
+    def _check_image_update(self, game_info):
+        local_image_md5 = self.update_config.get(game_info.task_id, 'image_md5')
+        url = f'{self.url}/fileManager/getMirrorDownloadByTaskId?task_id={game_info.task_id}'
+        try:
+            result = requests.get(url, timeout=10)
+            if result.status_code == 200:
+                json_obj = result.json()
+                if json_obj['code'] == 0:
+                    remote_md5 = json_obj['data']['md5']
+                    if remote_md5 != local_image_md5:
+                        download_url = json_obj['data']['qiniu_address']
+                        update_info = UpdateInfo(
+                            game_id=game_info.task_id,
+                            md5=remote_md5,
+                            download_url=download_url,
+                            file_type='image',
+                            save_path=os.path.join(self.image_path, game_info.image)
+                        )
+                        logger.info(f'检测到 game_id:{update_info.game_id}-{game_info.image}有更新,准备下载')
+                        self.download_queue.put(update_info)
+                        self.set_updating_status(game_info.task_id, 'image', 1)  # 标记为正在更新
+                        return True
+            return False
+        except Exception as e:
+            return False
+
+    def run(self):
+        error_count = 0
+        update_info = None
+        while True:
+            try:
+                if not self.download_queue.empty():
+                    update_info = self.download_queue.get()
+                    if update_info:
+                        downloader = FileDownloader(update_info.download_url, f'{update_info.save_path}.tmp')
+                        if update_info.file_type == 'script':
+                            downloader.download(resume=False)
+                            # 获取当前时间戳(10位整数)
+                            current_timestamp = int(time.time())
+                            # 构建带有时间戳的文件名模式
+                            timestamped_file_name = f"{os.path.splitext(update_info.save_path)[0]}#{current_timestamp}{os.path.splitext(update_info.save_path)[1]}"
+                            shutil.move(f'{update_info.save_path}.tmp', timestamped_file_name)
+                            self.update_config.set(update_info.game_id, 'script_md5', update_info.md5)
+                            self.set_updating_status(update_info.game_id, update_info.file_type, 0)
+
+                        elif update_info.file_type == 'image':
+                            downloader.download()
+                            shutil.move(f'{update_info.save_path}.tmp', update_info.save_path)
+                            self.update_config.set(update_info.game_id, 'image_md5', update_info.md5)
+                            self.set_updating_status(update_info.game_id, update_info.file_type, 0)
+                error_count = 0
+                time.sleep(5)
+            except Exception as e:
+                error_count += 1
+                if error_count == 5:
+                    task_api.notify(f'下载更新连续错误5次,请检查')
+                logger.exception(f'更新线程异常,错误信息:{e}')
+                time.sleep(5)
+            finally:
+                if update_info:
+                    self.set_updating_status(update_info.game_id, update_info.file_type, 0)  # 标记为更新完成